Skip to content

Commit 694918a

Browse files
Princesseuhmatthewpdelucissarah11918
authored
Implement RFC "A core story for images" (#6344)
* feat(assets): Add Vite plugin * feat(images): Set up Image component * fix(types): Attempt to fix type generation * Revert "fix(types): Attempt to fix type generation" This reverts commit 063aa27. * fix(image): Fix image types causing build to fail * feat(image): Implement client side part * feat(services): Allow arbitrary transforms parameters * fix(image): Fix paths and types * config(types): Update config types to provide completions for available services * feat(image): Add serving in dev * feat(image): Improve type error messages * refactor(image): Move sharp's parseParams to baseService * refactor(image): Skip work in dev for remote servies * feat(image): Add support for remote images * feat(image): Add squoosh service * chore: update export map * refactor(image): Abstract attributes handling by services * config(vercel): Remove test image service * feat(image): Support for relative images in Markdown (WIP) * feat(images): Add support for relative images in Markdown * feat(image): Update with RFC feedback * fix(image): Fix alt error on getImage * feat(image): Add support for assets validation through content collections * feat(image): Remove validateTransform * feat(image): Move to assets folder * fix(image): Fix package exports * feat(image): Add static imports references to virtual moduel * fix(image): Fix images from content collections not working when embedded * chore: lockfile * fix(markdown): Fix type * fix(images): Flag enhanced images behing an experimental flag * config(example): Update images example conifg * fix(image): Fix types * fix(image): Fix asset type for strict, allow arbritary input and output formats * chore: fix example check * feat(image): Emit assets for ESM imported images * Add initial core image tests (#6381) * feat(images): Make frontmatter extraction more generic than images for future * feat(image): Add support for building * fix(image): Fix types * fix(images): Fix compatibility with image integration * feat(images): Cuter generation stats * fix(images): Globals are unsafe, it turns out * fix(images): Only generate images if flag is enabled * fix(images): Only create `addStaticImage` in build * feat(images): Add SSR endpoint * fix(images): Only inject route in SSR * Add tests for SSR * Remove console.log * Updated lockfile * rename to satisfy the link gods * skip build tests for now * fix(images): Fix WASM files not being copied in dev * feat(images): Add quality presets * fix build tests running * Remove console.log * Add tests for getImage * Test local services * Test the content collections API * Add tests for quality * Skipping content collections test * feat(image): Add support for `~/assets` alias * test(image): Add tests for aliases in dev * Fix windows + content collections * test(image): Add tests for aliased images and images in Markdown * Fix markdown images being built * Should be posix join * Use the optimized image * fix test * Fixes windows smoke * fix(image): Nits * feat(images): Add automatic update for `env.d.ts` when experimental images are enabled * fix(images): Revert env.d.ts change if the user opted-out of the experimental image support * chore: remove bad image example project * feat(image): Rename `experimental.images` to `experimental.assets` * fix(images): Remove unused code in MDX integration * chore: Remove unrelated change * fix(images): Remove export from astro/components * Fix, esm import on Win * test(images): Add test for format * fix(images): Add `client-image.d.ts` to export map * chore: changeset * fix(images): Adjust with feedback, no more automatic refine, asset() -> image() * fix(images): Fix types * fix(images): Remove unnecessary spread * fix(images): Better types for parseUrl and transform * fix(images): Fix types * fix(images): Adjust from feedback * fix(images): Pass width and height through getHTMLAttributes even if they're not added by the uesr * fix(images): Recusirsively extract frontmatter assets * fix(images): Use a reduce instead * feat(images): Add support for data: URIs * chore: changeset * docs(images): Misc docs fixes * Update .changeset/gold-rocks-cry.md Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update .changeset/gold-rocks-cry.md Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca> * Update packages/astro/src/@types/astro.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update packages/astro/src/assets/services/service.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update packages/astro/src/assets/services/service.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update packages/astro/src/assets/services/service.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update packages/astro/src/assets/types.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> * Update packages/astro/src/assets/types.ts Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> --------- Co-authored-by: Matthew Phillips <matthew@skypack.dev> Co-authored-by: Matthew Phillips <matthew@matthewphillips.info> Co-authored-by: Chris Swithinbank <swithinbank@gmail.com> Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
1 parent 377530a commit 694918a

File tree

113 files changed

+14978
-149
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

113 files changed

+14978
-149
lines changed

.changeset/gold-rocks-cry.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'astro': minor
3+
'@astrojs/mdx': minor
4+
'@astrojs/markdown-remark': minor
5+
---
6+
7+
Add a new experimental flag (`experimental.assets`) to enable our new core Assets story.
8+
9+
This unlocks a few features:
10+
- A new built-in image component and JavaScript API to transform and optimize images.
11+
- Relative images with automatic optimization in Markdown.
12+
- Support for validating assets using content collections.
13+
- and more!
14+
15+
See [Assets (Experimental)](https://docs.astro.build/en/guides/assets/) on our docs site for more information on how to use this feature!

packages/astro/client-base.d.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
/// <reference path="./import-meta.d.ts" />
22

3+
declare module 'astro:assets' {
4+
// Exporting things one by one is a bit cumbersome, not sure if there's a better way - erika, 2023-02-03
5+
type AstroAssets = {
6+
getImage: typeof import('./dist/assets/index.js').getImage;
7+
Image: typeof import('./components/Image.astro').default;
8+
};
9+
10+
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
11+
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] };
12+
type ImgAttributes = WithRequired<
13+
Omit<import('./types').HTMLAttributes<'img'>, 'src' | 'width' | 'height'>,
14+
'alt'
15+
>;
16+
17+
export type LocalImageProps = Simplify<
18+
import('./dist/assets/types.js').LocalImageProps<ImgAttributes>
19+
>;
20+
export type RemoteImageProps = Simplify<
21+
import('./dist/assets/types.js').RemoteImageProps<ImgAttributes>
22+
>;
23+
export const { getImage, Image }: AstroAssets;
24+
}
25+
326
type MD = import('./dist/@types/astro').MarkdownInstance<Record<string, any>>;
427
interface ExportedMarkdownModuleEntities {
528
frontmatter: MD['frontmatter'];

packages/astro/client-image.d.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/// <reference path="./client-base.d.ts" />
2+
3+
type InputFormat = 'avif' | 'gif' | 'heic' | 'heif' | 'jpeg' | 'jpg' | 'png' | 'tiff' | 'webp';
4+
5+
interface ImageMetadata {
6+
src: string;
7+
width: number;
8+
height: number;
9+
format: InputFormat;
10+
}
11+
12+
// images
13+
declare module '*.avif' {
14+
const metadata: ImageMetadata;
15+
export default metadata;
16+
}
17+
declare module '*.gif' {
18+
const metadata: ImageMetadata;
19+
export default metadata;
20+
}
21+
declare module '*.heic' {
22+
const metadata: ImageMetadata;
23+
export default metadata;
24+
}
25+
declare module '*.heif' {
26+
const metadata: ImageMetadata;
27+
export default metadata;
28+
}
29+
declare module '*.jpeg' {
30+
const metadata: ImageMetadata;
31+
export default metadata;
32+
}
33+
declare module '*.jpg' {
34+
const metadata: ImageMetadata;
35+
export default metadata;
36+
}
37+
declare module '*.png' {
38+
const metadata: ImageMetadata;
39+
export default metadata;
40+
}
41+
declare module '*.tiff' {
42+
const metadata: ImageMetadata;
43+
export default metadata;
44+
}
45+
declare module '*.webp' {
46+
const metadata: ImageMetadata;
47+
export default metadata;
48+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
import { getImage, type LocalImageProps, type RemoteImageProps } from 'astro:assets';
3+
import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
4+
5+
// The TypeScript diagnostic for JSX props uses the last member of the union to suggest props, so it would be better for
6+
// LocalImageProps to be last. Unfortunately, when we do this the error messages that remote images get are complete nonsense
7+
// Not 100% sure how to fix this, seems to be a TypeScript issue. Unfortunate.
8+
type Props = LocalImageProps | RemoteImageProps;
9+
10+
const props = Astro.props;
11+
12+
if (props.alt === undefined || props.alt === null) {
13+
throw new AstroError(AstroErrorData.ImageMissingAlt);
14+
}
15+
16+
// As a convenience, allow width and height to be string with a number in them, to match HTML's native `img`.
17+
if (typeof props.width === 'string') {
18+
props.width = parseInt(props.width);
19+
}
20+
21+
if (typeof props.height === 'string') {
22+
props.height = parseInt(props.height);
23+
}
24+
25+
const image = await getImage(props);
26+
---
27+
28+
<img src={image.src} {...image.attributes} />

packages/astro/package.json

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"./types": "./types.d.ts",
3333
"./client": "./client.d.ts",
3434
"./client-base": "./client-base.d.ts",
35+
"./client-image": "./client-image.d.ts",
3536
"./import-meta": "./import-meta.d.ts",
3637
"./astro-jsx": "./astro-jsx.d.ts",
3738
"./tsconfigs/*.json": "./tsconfigs/*",
@@ -47,6 +48,10 @@
4748
"./client/*": "./dist/runtime/client/*",
4849
"./components": "./components/index.ts",
4950
"./components/*": "./components/*",
51+
"./assets": "./dist/assets/index.js",
52+
"./assets/image-endpoint": "./dist/assets/image-endpoint.js",
53+
"./assets/services/sharp": "./dist/assets/services/sharp.js",
54+
"./assets/services/squoosh": "./dist/assets/services/squoosh.js",
5055
"./content/internal": "./dist/content/internal.js",
5156
"./debug": "./components/Debug.astro",
5257
"./internal/*": "./dist/runtime/server/*",
@@ -77,6 +82,7 @@
7782
"env.d.ts",
7883
"client.d.ts",
7984
"client-base.d.ts",
85+
"client-image.d.ts",
8086
"import-meta.d.ts",
8187
"astro-jsx.d.ts",
8288
"types.d.ts",
@@ -86,10 +92,10 @@
8692
],
8793
"scripts": {
8894
"prebuild": "astro-scripts prebuild --to-string \"src/runtime/server/astro-island.ts\" \"src/runtime/client/{idle,load,media,only,visible}.ts\"",
89-
"build": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\" && tsc",
95+
"build": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\" && tsc && pnpm run postbuild",
9096
"build:ci": "pnpm run prebuild && astro-scripts build \"src/**/*.ts\"",
91-
"dev": "astro-scripts dev --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.ts\"",
92-
"postbuild": "astro-scripts copy \"src/**/*.astro\"",
97+
"dev": "astro-scripts dev --copy-wasm --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.ts\"",
98+
"postbuild": "astro-scripts copy \"src/**/*.astro\" && astro-scripts copy \"src/**/*.wasm\"",
9399
"test:unit": "mocha --exit --timeout 30000 ./test/units/**/*.test.js",
94100
"test:unit:match": "mocha --exit --timeout 30000 ./test/units/**/*.test.js -g",
95101
"test": "pnpm run test:unit && mocha --exit --timeout 20000 --ignore **/lit-element.test.js && mocha --timeout 20000 **/lit-element.test.js",
@@ -128,6 +134,7 @@
128134
"github-slugger": "^2.0.0",
129135
"gray-matter": "^4.0.3",
130136
"html-escaper": "^3.0.3",
137+
"image-size": "^1.0.2",
131138
"kleur": "^4.1.4",
132139
"magic-string": "^0.27.0",
133140
"mime": "^3.0.0",
@@ -173,6 +180,7 @@
173180
"@types/rimraf": "^3.0.2",
174181
"@types/send": "^0.17.1",
175182
"@types/server-destroy": "^1.0.1",
183+
"@types/sharp": "^0.31.1",
176184
"@types/unist": "^2.0.6",
177185
"astro-scripts": "workspace:*",
178186
"chai": "^4.3.6",
@@ -187,10 +195,19 @@
187195
"remark-code-titles": "^0.1.2",
188196
"rollup": "^3.9.0",
189197
"sass": "^1.52.2",
198+
"sharp": "^0.31.3",
190199
"srcset-parse": "^1.1.0",
191200
"undici": "^5.20.0",
192201
"unified": "^10.1.2"
193202
},
203+
"peerDependencies": {
204+
"sharp": "^0.31.3"
205+
},
206+
"peerDependenciesMeta": {
207+
"sharp": {
208+
"optional": true
209+
}
210+
},
194211
"engines": {
195212
"node": ">=16.12.0",
196213
"npm": ">=6.14.0"

packages/astro/src/@types/astro.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { PageBuildData } from '../core/build/types';
1818
import type { AstroConfigSchema } from '../core/config';
1919
import type { AstroTimer } from '../core/config/timer';
2020
import type { AstroCookies } from '../core/cookies';
21+
import type { LogOptions } from '../core/logger/core';
2122
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server';
2223
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
2324
export type {
@@ -28,6 +29,8 @@ export type {
2829
RemarkPlugins,
2930
ShikiConfig,
3031
} from '@astrojs/markdown-remark';
32+
export type { ExternalImageService, LocalImageService } from '../assets/services/service';
33+
export type { ImageTransform } from '../assets/types';
3134
export type { SSRManifest } from '../core/app/types';
3235
export type { AstroCookies } from '../core/cookies';
3336

@@ -85,6 +88,7 @@ export interface CLIFlags {
8588
port?: number;
8689
config?: string;
8790
drafts?: boolean;
91+
experimentalAssets?: boolean;
8892
}
8993

9094
export interface BuildConfig {
@@ -696,6 +700,16 @@ export interface AstroUserConfig {
696700

697701
server?: ServerConfig | ((options: { command: 'dev' | 'preview' }) => ServerConfig);
698702

703+
/**
704+
* @docs
705+
* @kind heading
706+
* @name Image options
707+
*/
708+
image?: {
709+
// eslint-disable-next-line @typescript-eslint/ban-types
710+
service: 'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | (string & {});
711+
};
712+
699713
/**
700714
* @docs
701715
* @kind heading
@@ -918,7 +932,27 @@ export interface AstroUserConfig {
918932
* Astro offers experimental flags to give users early access to new features.
919933
* These flags are not guaranteed to be stable.
920934
*/
921-
experimental?: object;
935+
experimental?: {
936+
/**
937+
* @docs
938+
* @name experimental.assets
939+
* @type {boolean}
940+
* @default `false`
941+
* @version 2.1.0
942+
* @description
943+
* Enable experimental support for optimizing and resizing images. With this enabled, a new `astro:assets` module will be exposed.
944+
*
945+
* To enable this feature, set `experimental.assets` to `true` in your Astro config:
946+
*
947+
* ```js
948+
* {
949+
* experimental: {
950+
* assets: true,
951+
* },
952+
* }
953+
*/
954+
assets?: boolean;
955+
};
922956

923957
// Legacy options to be removed
924958

@@ -1432,6 +1466,11 @@ export interface AstroIntegration {
14321466
};
14331467
}
14341468

1469+
export interface AstroPluginOptions {
1470+
settings: AstroSettings;
1471+
logging: LogOptions;
1472+
}
1473+
14351474
export type RouteType = 'page' | 'endpoint';
14361475

14371476
export interface RoutePart {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# assets
2+
3+
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).
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const VIRTUAL_MODULE_ID = 'astro:assets';
2+
export const VIRTUAL_SERVICE_ID = 'virtual:image-service';
3+
export const VALID_INPUT_FORMATS = [
4+
'heic',
5+
'heif',
6+
'avif',
7+
'jpeg',
8+
'jpg',
9+
'png',
10+
'tiff',
11+
'webp',
12+
'gif',
13+
] as const;
14+
export const VALID_OUTPUT_FORMATS = ['avif', 'png', 'webp', 'jpeg', 'jpg'] as const;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import mime from 'mime';
2+
import type { APIRoute } from '../@types/astro.js';
3+
import { isRemotePath } from '../core/path.js';
4+
import { getConfiguredImageService } from './internal.js';
5+
import { isLocalService } from './services/service.js';
6+
import { etag } from './utils/etag.js';
7+
8+
async function loadRemoteImage(src: URL) {
9+
try {
10+
const res = await fetch(src);
11+
12+
if (!res.ok) {
13+
return undefined;
14+
}
15+
16+
return Buffer.from(await res.arrayBuffer());
17+
} catch (err: unknown) {
18+
return undefined;
19+
}
20+
}
21+
22+
/**
23+
* Endpoint used in SSR to serve optimized images
24+
*/
25+
export const get: APIRoute = async ({ request }) => {
26+
try {
27+
const imageService = await getConfiguredImageService();
28+
29+
if (!isLocalService(imageService)) {
30+
throw new Error('Configured image service is not a local service');
31+
}
32+
33+
const url = new URL(request.url);
34+
const transform = await imageService.parseURL(url);
35+
36+
if (!transform || !transform.src) {
37+
throw new Error('Incorrect transform returned by `parseURL`');
38+
}
39+
40+
let inputBuffer: Buffer | undefined = undefined;
41+
42+
// TODO: handle config subpaths?
43+
const sourceUrl = isRemotePath(transform.src)
44+
? new URL(transform.src)
45+
: new URL(transform.src, url.origin);
46+
inputBuffer = await loadRemoteImage(sourceUrl);
47+
48+
if (!inputBuffer) {
49+
return new Response('Not Found', { status: 404 });
50+
}
51+
52+
const { data, format } = await imageService.transform(inputBuffer, transform);
53+
54+
return new Response(data, {
55+
status: 200,
56+
headers: {
57+
'Content-Type': mime.getType(format) || '',
58+
'Cache-Control': 'public, max-age=31536000',
59+
ETag: etag(data.toString()),
60+
Date: new Date().toUTCString(),
61+
},
62+
});
63+
} catch (err: unknown) {
64+
return new Response(`Server Error: ${err}`, { status: 500 });
65+
}
66+
};

packages/astro/src/assets/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { getConfiguredImageService, getImage } from './internal.js';
2+
export { baseService } from './services/service.js';
3+
export { type LocalImageProps, type RemoteImageProps } from './types.js';
4+
export { imageMetadata } from './utils/metadata.js';

0 commit comments

Comments
 (0)