-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Implement RFC "A core story for images" #6344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 107 commits
d714a7a
1f7c4a9
21416d0
af1bc13
063aa27
25fc671
bf0dfff
f2a69bd
e5ca26e
1a9d939
848d326
02295be
bca6ebe
b0f4dd0
7e95c5b
633a4d8
110c56a
cf0bc4e
07f2b2e
092c348
3b0d49c
1a0ea86
308a850
23ac7a4
3f85ccd
aac6bfc
ef92cca
2614cfa
6e8c795
b75cd2f
e872d8d
3753445
544d6ce
b7d6f68
b15b666
4f0cf02
08c4240
52a4388
cf2da10
87fbfc7
5f0463b
33238d4
406eff6
4c92818
7eb51b4
d1da8e2
0cdf5d7
9dc6a56
6bb2e34
e908862
57ae057
dc87411
3932c73
2aec7ef
baa44ae
9526d1c
57a8e0e
8446fb3
4256bb5
6108458
46e319b
acd56b1
49da47c
fab176b
46d5ae2
051798e
cdc77e2
caa2900
b86c995
f29be13
f919012
15538fe
eb20571
7ef6b27
1ed8652
abb72a0
309dcee
87a0d6a
4db291a
edc007d
f67e6c4
6bc4ce9
b620671
c88a998
e606a5f
3ba14ab
e0fa179
252cea8
d5560e2
ad701ad
aacfe61
a87c7a7
13a9974
c57a47d
0414d49
be5d6e5
7fd3eda
2d54346
ad7a15e
2f38e34
a124a71
84c4997
c1299d3
8c13a6b
1f7d910
9b53c92
97f0cdd
93acb22
516493b
7404291
b7f9f9c
5f5a06c
c912ffb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| --- | ||
| 'astro': minor | ||
| '@astrojs/mdx': minor | ||
| '@astrojs/markdown-remark': minor | ||
| --- | ||
|
|
||
| Add a new experimental flag (`experimental.assets`) to enable our new core Assets story. | ||
|
|
||
| This unlocks a few features: | ||
| - A new built-in image component and JavaScript API to transform and optimize images. | ||
| - Relative images with automatic optimization in Markdown. | ||
| - Support for validating assets using content collections. | ||
| - and more! | ||
|
|
||
| See [Assets (Experimental)](https://docs.astro.build/en/guides/assets/) on our docs site for more information on how to use this feature! | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /// <reference path="./client-base.d.ts" /> | ||
|
|
||
| type InputFormat = 'avif' | 'gif' | 'heic' | 'heif' | 'jpeg' | 'jpg' | 'png' | 'tiff' | 'webp'; | ||
|
|
||
| interface ImageMetadata { | ||
| src: string; | ||
| width: number; | ||
| height: number; | ||
| format: InputFormat; | ||
| } | ||
|
|
||
| // images | ||
| declare module '*.avif' { | ||
| const metadata: ImageMetadata; | ||
| export default metadata; | ||
| } | ||
| declare module '*.gif' { | ||
| const metadata: ImageMetadata; | ||
| export default metadata; | ||
| } | ||
| declare module '*.heic' { | ||
| const metadata: ImageMetadata; | ||
| export default metadata; | ||
| } | ||
| declare module '*.heif' { | ||
| const metadata: ImageMetadata; | ||
| export default metadata; | ||
| } | ||
| declare module '*.jpeg' { | ||
| const metadata: ImageMetadata; | ||
| export default metadata; | ||
| } | ||
| declare module '*.jpg' { | ||
| const metadata: ImageMetadata; | ||
| export default metadata; | ||
| } | ||
| declare module '*.png' { | ||
| const metadata: ImageMetadata; | ||
| export default metadata; | ||
| } | ||
| declare module '*.tiff' { | ||
| const metadata: ImageMetadata; | ||
| export default metadata; | ||
| } | ||
| declare module '*.webp' { | ||
| const metadata: ImageMetadata; | ||
| export default metadata; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| --- | ||
| import { getImage, type LocalImageProps, type RemoteImageProps } from 'astro:assets'; | ||
| import { AstroError, AstroErrorData } from '../dist/core/errors/index.js'; | ||
|
|
||
| // The TypeScript diagnostic for JSX props uses the last member of the union to suggest props, so it would be better for | ||
| // LocalImageProps to be last. Unfortunately, when we do this the error messages that remote images get are complete nonsense | ||
| // Not 100% sure how to fix this, seems to be a TypeScript issue. Unfortunate. | ||
| type Props = LocalImageProps | RemoteImageProps; | ||
Princesseuh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const props = Astro.props; | ||
|
|
||
| if (props.alt === undefined || props.alt === null) { | ||
| throw new AstroError(AstroErrorData.ImageMissingAlt); | ||
| } | ||
|
|
||
| // As a convenience, allow width and height to be string with a number in them, to match HTML's native `img`. | ||
| if (typeof props.width === 'string') { | ||
| props.width = parseInt(props.width); | ||
| } | ||
|
|
||
| if (typeof props.height === 'string') { | ||
| props.height = parseInt(props.height); | ||
| } | ||
|
|
||
| const image = await getImage(props); | ||
| --- | ||
|
|
||
| <img src={image.src} {...image.attributes} /> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # assets | ||
|
|
||
| This directory powers the Assets story in Astro. Notably, it contains all the code related to optimizing images and serving them in the different modes Astro can run in (SSG, SSR, dev, build etc). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| export const VIRTUAL_MODULE_ID = 'astro:assets'; | ||
| export const VIRTUAL_SERVICE_ID = 'virtual:image-service'; | ||
| export const VALID_INPUT_FORMATS = [ | ||
| 'heic', | ||
| 'heif', | ||
| 'avif', | ||
| 'jpeg', | ||
| 'jpg', | ||
| 'png', | ||
| 'tiff', | ||
| 'webp', | ||
| 'gif', | ||
Princesseuh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ] as const; | ||
| export const VALID_OUTPUT_FORMATS = ['avif', 'png', 'webp', 'jpeg', 'jpg'] as const; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import mime from 'mime'; | ||
| import type { APIRoute } from '../@types/astro.js'; | ||
| import { isRemotePath } from '../core/path.js'; | ||
| import { getConfiguredImageService } from './internal.js'; | ||
| import { isLocalService } from './services/service.js'; | ||
| import { etag } from './utils/etag.js'; | ||
|
|
||
| async function loadRemoteImage(src: URL) { | ||
| try { | ||
| const res = await fetch(src); | ||
|
|
||
| if (!res.ok) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return Buffer.from(await res.arrayBuffer()); | ||
| } catch (err: unknown) { | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Endpoint used in SSR to serve optimized images | ||
| */ | ||
| export const get: APIRoute = async ({ request }) => { | ||
| try { | ||
| const imageService = await getConfiguredImageService(); | ||
|
|
||
| if (!isLocalService(imageService)) { | ||
| throw new Error('Configured image service is not a local service'); | ||
| } | ||
|
|
||
| const url = new URL(request.url); | ||
| const transform = await imageService.parseURL(url); | ||
|
|
||
| if (!transform || !transform.src) { | ||
| throw new Error('Incorrect transform returned by `parseURL`'); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we give a hint about why this error occurred or a clue about how to solve this error?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can definitely add more information to the message, however since this error is shown to users of the user's website, we actually need to be careful about what we say. |
||
| } | ||
|
|
||
| let inputBuffer: Buffer | undefined = undefined; | ||
|
|
||
| // TODO: handle config subpaths? | ||
| const sourceUrl = isRemotePath(transform.src) | ||
| ? new URL(transform.src) | ||
| : new URL(transform.src, url.origin); | ||
| inputBuffer = await loadRemoteImage(sourceUrl); | ||
|
|
||
| if (!inputBuffer) { | ||
| return new Response('Not Found', { status: 404 }); | ||
| } | ||
|
|
||
| const { data, format } = await imageService.transform(inputBuffer, transform); | ||
|
|
||
| return new Response(data, { | ||
| status: 200, | ||
| headers: { | ||
| 'Content-Type': mime.getType(format) || '', | ||
| 'Cache-Control': 'public, max-age=31536000', | ||
| ETag: etag(data.toString()), | ||
| Date: new Date().toUTCString(), | ||
| }, | ||
| }); | ||
| } catch (err: unknown) { | ||
| return new Response(`Server Error: ${err}`, { status: 500 }); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export { getConfiguredImageService, getImage } from './internal.js'; | ||
| export { baseService } from './services/service.js'; | ||
| export { type LocalImageProps, type RemoteImageProps } from './types.js'; | ||
| export { imageMetadata } from './utils/metadata.js'; |
Uh oh!
There was an error while loading. Please reload this page.