astro 3.2.4 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/client.d.ts +3 -9
  2. package/components/Code.astro +60 -29
  3. package/components/Image.astro +7 -1
  4. package/components/Picture.astro +59 -0
  5. package/components/shiki.ts +46 -0
  6. package/dist/@types/astro.d.ts +2 -2
  7. package/dist/assets/consts.d.ts +1 -0
  8. package/dist/assets/consts.js +2 -0
  9. package/dist/assets/internal.js +18 -2
  10. package/dist/assets/services/service.d.ts +12 -0
  11. package/dist/assets/services/service.js +89 -16
  12. package/dist/assets/services/sharp.js +3 -2
  13. package/dist/assets/services/squoosh.js +3 -2
  14. package/dist/assets/types.d.ts +29 -2
  15. package/dist/assets/utils/transformToPath.js +1 -1
  16. package/dist/assets/vite-plugin-assets.js +1 -0
  17. package/dist/cli/add/index.js +15 -9
  18. package/dist/cli/build/index.js +1 -0
  19. package/dist/cli/flags.js +1 -0
  20. package/dist/config/index.js +1 -1
  21. package/dist/content/server-listeners.js +8 -7
  22. package/dist/core/build/index.js +1 -1
  23. package/dist/core/build/plugins/plugin-internals.js +0 -2
  24. package/dist/core/config/schema.d.ts +39 -38
  25. package/dist/core/config/schema.js +15 -3
  26. package/dist/core/config/settings.d.ts +1 -1
  27. package/dist/core/config/settings.js +10 -5
  28. package/dist/core/config/tsconfig.d.ts +24 -7
  29. package/dist/core/config/tsconfig.js +44 -22
  30. package/dist/core/constants.js +1 -1
  31. package/dist/core/create-vite.js +0 -2
  32. package/dist/core/dev/dev.js +1 -1
  33. package/dist/core/dev/restart.js +2 -2
  34. package/dist/core/endpoint/index.js +24 -18
  35. package/dist/core/errors/dev/vite.js +4 -3
  36. package/dist/core/errors/errors-data.d.ts +13 -0
  37. package/dist/core/errors/errors-data.js +7 -0
  38. package/dist/core/messages.js +2 -2
  39. package/dist/core/preview/index.js +1 -1
  40. package/dist/core/sync/index.js +1 -1
  41. package/dist/runtime/server/render/page.js +3 -0
  42. package/dist/transitions/router.d.ts +1 -1
  43. package/dist/transitions/router.js +84 -41
  44. package/package.json +5 -5
  45. package/components/Shiki.js +0 -97
  46. package/components/shiki-languages.js +0 -176
  47. package/components/shiki-themes.js +0 -37
package/client.d.ts CHANGED
@@ -53,6 +53,7 @@ declare module 'astro:assets' {
53
53
  imageConfig: import('./dist/@types/astro.js').AstroConfig['image'];
54
54
  getConfiguredImageService: typeof import('./dist/assets/index.js').getConfiguredImageService;
55
55
  Image: typeof import('./components/Image.astro').default;
56
+ Picture: typeof import('./components/Picture.astro').default;
56
57
  };
57
58
 
58
59
  type ImgAttributes = import('./dist/type-utils.js').WithRequired<
@@ -66,17 +67,10 @@ declare module 'astro:assets' {
66
67
  export type RemoteImageProps = import('./dist/type-utils.js').Simplify<
67
68
  import('./dist/assets/types.js').RemoteImageProps<ImgAttributes>
68
69
  >;
69
- export const { getImage, getConfiguredImageService, imageConfig, Image }: AstroAssets;
70
+ export const { getImage, getConfiguredImageService, imageConfig, Image, Picture }: AstroAssets;
70
71
  }
71
72
 
72
- type InputFormat = import('./dist/assets/types.js').ImageInputFormat;
73
-
74
- interface ImageMetadata {
75
- src: string;
76
- width: number;
77
- height: number;
78
- format: InputFormat;
79
- }
73
+ type ImageMetadata = import('./dist/assets/types.js').ImageMetadata;
80
74
 
81
75
  declare module '*.gif' {
82
76
  const metadata: ImageMetadata;
@@ -1,7 +1,14 @@
1
1
  ---
2
- import type * as shiki from 'shiki';
3
- import { renderToHtml } from 'shiki';
4
- import { getHighlighter } from './Shiki.js';
2
+ import type {
3
+ BuiltinLanguage,
4
+ BuiltinTheme,
5
+ LanguageRegistration,
6
+ SpecialLanguage,
7
+ ThemeRegistration,
8
+ ThemeRegistrationRaw,
9
+ } from 'shikiji';
10
+ import { visit } from 'unist-util-visit';
11
+ import { getCachedHighlighter, replaceCssVariables } from './shiki.js';
5
12
 
6
13
  interface Props {
7
14
  /** The code to highlight. Required. */
@@ -13,7 +20,7 @@ interface Props {
13
20
  *
14
21
  * @default "plaintext"
15
22
  */
16
- lang?: shiki.Lang | shiki.ILanguageRegistration;
23
+ lang?: BuiltinLanguage | SpecialLanguage | LanguageRegistration;
17
24
  /**
18
25
  * The styling theme.
19
26
  * Supports all themes listed here: https://github.com/shikijs/shiki/blob/main/docs/themes.md#all-themes
@@ -21,7 +28,7 @@ interface Props {
21
28
  *
22
29
  * @default "github-dark"
23
30
  */
24
- theme?: shiki.IThemeRegistration;
31
+ theme?: BuiltinTheme | ThemeRegistration | ThemeRegistrationRaw;
25
32
  /**
26
33
  * Enable word wrapping.
27
34
  * - true: enabled.
@@ -47,41 +54,65 @@ const {
47
54
  inline = false,
48
55
  } = Astro.props;
49
56
 
50
- // 1. Get the shiki syntax highlighter
51
- const highlighter = await getHighlighter({
52
- theme,
53
- // Load custom lang if passed an object, otherwise load the default
54
- langs: typeof lang !== 'string' ? [lang] : undefined,
57
+ // shiki -> shikiji compat
58
+ if (typeof lang === 'object') {
59
+ // `id` renamed to `name
60
+ if ((lang as any).id && !lang.name) {
61
+ lang.name = (lang as any).id;
62
+ }
63
+ // `grammar` flattened to lang itself
64
+ if ((lang as any).grammar) {
65
+ Object.assign(lang, (lang as any).grammar);
66
+ }
67
+ }
68
+
69
+ const highlighter = await getCachedHighlighter({
70
+ langs: [lang],
71
+ themes: [theme],
55
72
  });
56
73
 
57
- // 2. Turn code into shiki theme tokens
58
- const tokens = highlighter.codeToThemedTokens(code, typeof lang === 'string' ? lang : lang.id);
74
+ const html = highlighter.codeToHtml(code, {
75
+ lang: typeof lang === 'string' ? lang : lang.name,
76
+ theme,
77
+ transforms: {
78
+ pre(node) {
79
+ // Swap to `code` tag if inline
80
+ if (inline) {
81
+ node.tagName = 'code';
82
+ }
59
83
 
60
- // 3. Get shiki theme object
61
- const _theme = highlighter.getTheme();
84
+ // Cast to string as shikiji will always pass them as strings instead of any other types
85
+ const classValue = (node.properties.class as string) ?? '';
86
+ const styleValue = (node.properties.style as string) ?? '';
62
87
 
63
- // 4. Render the theme tokens as html
64
- const html = renderToHtml(tokens, {
65
- themeName: _theme.name,
66
- fg: _theme.fg,
67
- bg: _theme.bg,
68
- elements: {
69
- pre({ className, style, children }) {
70
- // Swap to `code` tag if inline
71
- const tag = inline ? 'code' : 'pre';
72
88
  // Replace "shiki" class naming with "astro-code"
73
- className = className.replace(/shiki/g, 'astro-code');
89
+ node.properties.class = classValue.replace(/shiki/g, 'astro-code');
90
+
74
91
  // Handle code wrapping
75
92
  // if wrap=null, do nothing.
76
93
  if (wrap === false) {
77
- style += '; overflow-x: auto;';
94
+ node.properties.style = styleValue + '; overflow-x: auto;';
78
95
  } else if (wrap === true) {
79
- style += '; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;';
96
+ node.properties.style =
97
+ styleValue + '; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;';
80
98
  }
81
- return `<${tag} class="${className}" style="${style}" tabindex="0">${children}</${tag}>`;
82
99
  },
83
- code({ children }) {
84
- return inline ? children : `<code>${children}</code>`;
100
+ code(node) {
101
+ if (inline) {
102
+ return node.children[0] as typeof node;
103
+ }
104
+ },
105
+ root(node) {
106
+ // theme.id for shiki -> shikiji compat
107
+ const themeName = typeof theme === 'string' ? theme : theme.name;
108
+ if (themeName === 'css-variables') {
109
+ // Replace special color tokens to CSS variables
110
+ visit(node as any, 'element', (child) => {
111
+ if (child.properties?.style) {
112
+ child.properties.style = replaceCssVariables(child.properties.style);
113
+ }
114
+ });
115
+ }
85
116
  },
86
117
  },
87
118
  });
@@ -23,6 +23,12 @@ if (typeof props.height === 'string') {
23
23
  }
24
24
 
25
25
  const image = await getImage(props);
26
+
27
+ const additionalAttributes: Record<string, any> = {};
28
+
29
+ if (image.srcSet.values.length > 0) {
30
+ additionalAttributes.srcset = image.srcSet.attribute;
31
+ }
26
32
  ---
27
33
 
28
- <img src={image.src} {...image.attributes} />
34
+ <img src={image.src} {...additionalAttributes} {...image.attributes} />
@@ -0,0 +1,59 @@
1
+ ---
2
+ import { getImage, type LocalImageProps, type RemoteImageProps } from 'astro:assets';
3
+ import type { GetImageResult, ImageOutputFormat } from '../dist/@types/astro';
4
+ import { isESMImportedImage } from '../dist/assets/internal';
5
+ import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
6
+ import type { HTMLAttributes } from '../types';
7
+
8
+ type Props = (LocalImageProps | RemoteImageProps) & {
9
+ formats?: ImageOutputFormat[];
10
+ fallbackFormat?: ImageOutputFormat;
11
+ pictureAttributes?: HTMLAttributes<'picture'>;
12
+ };
13
+
14
+ const { formats = ['webp'], pictureAttributes = {}, ...props } = Astro.props;
15
+
16
+ if (props.alt === undefined || props.alt === null) {
17
+ throw new AstroError(AstroErrorData.ImageMissingAlt);
18
+ }
19
+
20
+ const optimizedImages: GetImageResult[] = await Promise.all(
21
+ formats.map(
22
+ async (format) =>
23
+ await getImage({ ...props, format: format, widths: props.widths, densities: props.densities })
24
+ )
25
+ );
26
+
27
+ const fallbackFormat =
28
+ props.fallbackFormat ?? isESMImportedImage(props.src)
29
+ ? ['svg', 'gif'].includes(props.src.format)
30
+ ? props.src.format
31
+ : 'png'
32
+ : 'png';
33
+
34
+ const fallbackImage = await getImage({
35
+ ...props,
36
+ format: fallbackFormat,
37
+ widths: props.widths,
38
+ densities: props.densities,
39
+ });
40
+
41
+ const additionalAttributes: Record<string, any> = {};
42
+ if (fallbackImage.srcSet.values.length > 0) {
43
+ additionalAttributes.srcset = fallbackImage.srcSet.attribute;
44
+ }
45
+ ---
46
+
47
+ <picture {...pictureAttributes}>
48
+ {
49
+ Object.entries(optimizedImages).map(([_, image]) => (
50
+ <source
51
+ srcset={`${image.src}${
52
+ image.srcSet.values.length > 0 ? ' , ' + image.srcSet.attribute : ''
53
+ }`}
54
+ type={'image/' + image.options.format}
55
+ />
56
+ ))
57
+ }
58
+ <img src={fallbackImage.src} {...additionalAttributes} {...fallbackImage.attributes} />
59
+ </picture>
@@ -0,0 +1,46 @@
1
+ import { type Highlighter, getHighlighter } from 'shikiji';
2
+
3
+ type HighlighterOptions = NonNullable<Parameters<typeof getHighlighter>[0]>;
4
+
5
+ const ASTRO_COLOR_REPLACEMENTS: Record<string, string> = {
6
+ '#000001': 'var(--astro-code-color-text)',
7
+ '#000002': 'var(--astro-code-color-background)',
8
+ '#000004': 'var(--astro-code-token-constant)',
9
+ '#000005': 'var(--astro-code-token-string)',
10
+ '#000006': 'var(--astro-code-token-comment)',
11
+ '#000007': 'var(--astro-code-token-keyword)',
12
+ '#000008': 'var(--astro-code-token-parameter)',
13
+ '#000009': 'var(--astro-code-token-function)',
14
+ '#000010': 'var(--astro-code-token-string-expression)',
15
+ '#000011': 'var(--astro-code-token-punctuation)',
16
+ '#000012': 'var(--astro-code-token-link)',
17
+ };
18
+ const COLOR_REPLACEMENT_REGEX = new RegExp(
19
+ `(${Object.keys(ASTRO_COLOR_REPLACEMENTS).join('|')})`,
20
+ 'g'
21
+ );
22
+
23
+ // Caches Promise<Highlighter> for reuse when the same theme and langs are provided
24
+ const cachedHighlighters = new Map();
25
+
26
+ /**
27
+ * shiki -> shikiji compat as we need to manually replace it
28
+ */
29
+ export function replaceCssVariables(str: string) {
30
+ return str.replace(COLOR_REPLACEMENT_REGEX, (match) => ASTRO_COLOR_REPLACEMENTS[match] || match);
31
+ }
32
+
33
+ export function getCachedHighlighter(opts: HighlighterOptions): Promise<Highlighter> {
34
+ // Always sort keys before stringifying to make sure objects match regardless of parameter ordering
35
+ const key = JSON.stringify(opts, Object.keys(opts).sort());
36
+
37
+ // Highlighter has already been requested, reuse the same instance
38
+ if (cachedHighlighters.has(key)) {
39
+ return cachedHighlighters.get(key);
40
+ }
41
+
42
+ const highlighter = getHighlighter(opts);
43
+ cachedHighlighters.set(key, highlighter);
44
+
45
+ return highlighter;
46
+ }
@@ -6,13 +6,13 @@ import type * as babel from '@babel/core';
6
6
  import type { OutgoingHttpHeaders } from 'node:http';
7
7
  import type { AddressInfo } from 'node:net';
8
8
  import type * as rollup from 'rollup';
9
- import type { TsConfigJson } from 'tsconfig-resolver';
10
9
  import type * as vite from 'vite';
11
10
  import type { RemotePattern } from '../assets/utils/remotePattern.js';
12
11
  import type { SerializedSSRManifest } from '../core/app/types.js';
13
12
  import type { PageBuildData } from '../core/build/types.js';
14
13
  import type { AstroConfigType } from '../core/config/index.js';
15
14
  import type { AstroTimer } from '../core/config/timer.js';
15
+ import type { TSConfig } from '../core/config/tsconfig.js';
16
16
  import type { AstroCookies } from '../core/cookies/index.js';
17
17
  import type { ResponseWithEncoding } from '../core/endpoint/index.js';
18
18
  import type { AstroIntegrationLogger, Logger, LoggerLevel } from '../core/logger/core.js';
@@ -1381,7 +1381,7 @@ export interface AstroSettings {
1381
1381
  * Map of directive name (e.g. `load`) to the directive script code
1382
1382
  */
1383
1383
  clientDirectives: Map<string, string>;
1384
- tsConfig: TsConfigJson | undefined;
1384
+ tsConfig: TSConfig | undefined;
1385
1385
  tsConfigPath: string | undefined;
1386
1386
  watchFiles: string[];
1387
1387
  timer: AstroTimer;
@@ -6,4 +6,5 @@ export declare const VALID_INPUT_FORMATS: readonly ["jpeg", "jpg", "png", "tiff"
6
6
  * Certain formats can be imported (namely SVGs) but will not be processed.
7
7
  */
8
8
  export declare const VALID_SUPPORTED_FORMATS: readonly ["jpeg", "jpg", "png", "tiff", "webp", "gif", "svg", "avif"];
9
+ export declare const DEFAULT_OUTPUT_FORMAT: "webp";
9
10
  export declare const VALID_OUTPUT_FORMATS: readonly ["avif", "png", "webp", "jpeg", "jpg", "svg"];
@@ -20,8 +20,10 @@ const VALID_SUPPORTED_FORMATS = [
20
20
  "svg",
21
21
  "avif"
22
22
  ];
23
+ const DEFAULT_OUTPUT_FORMAT = "webp";
23
24
  const VALID_OUTPUT_FORMATS = ["avif", "png", "webp", "jpeg", "jpg", "svg"];
24
25
  export {
26
+ DEFAULT_OUTPUT_FORMAT,
25
27
  VALID_INPUT_FORMATS,
26
28
  VALID_OUTPUT_FORMATS,
27
29
  VALID_SUPPORTED_FORMATS,
@@ -56,15 +56,31 @@ async function getImage(options, imageConfig) {
56
56
  src: typeof options.src === "object" && "then" in options.src ? (await options.src).default ?? await options.src : options.src
57
57
  };
58
58
  const validatedOptions = service.validateOptions ? await service.validateOptions(resolvedOptions, imageConfig) : resolvedOptions;
59
+ const srcSetTransforms = service.getSrcSet ? await service.getSrcSet(validatedOptions, imageConfig) : [];
59
60
  let imageURL = await service.getURL(validatedOptions, imageConfig);
60
- if (isLocalService(service) && globalThis.astroAsset.addStaticImage && // If `getURL` returned the same URL as the user provided, it means the service doesn't need to do anything
61
- !(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)) {
61
+ let srcSets = await Promise.all(
62
+ srcSetTransforms.map(async (srcSet) => ({
63
+ url: await service.getURL(srcSet.transform, imageConfig),
64
+ descriptor: srcSet.descriptor,
65
+ attributes: srcSet.attributes
66
+ }))
67
+ );
68
+ if (isLocalService(service) && globalThis.astroAsset.addStaticImage && !(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)) {
62
69
  imageURL = globalThis.astroAsset.addStaticImage(validatedOptions);
70
+ srcSets = srcSetTransforms.map((srcSet) => ({
71
+ url: globalThis.astroAsset.addStaticImage(srcSet.transform),
72
+ descriptor: srcSet.descriptor,
73
+ attributes: srcSet.attributes
74
+ }));
63
75
  }
64
76
  return {
65
77
  rawOptions: resolvedOptions,
66
78
  options: validatedOptions,
67
79
  src: imageURL,
80
+ srcSet: {
81
+ values: srcSets,
82
+ attribute: srcSets.map((srcSet) => `${srcSet.url} ${srcSet.descriptor}`).join(", ")
83
+ },
68
84
  attributes: service.getHTMLAttributes !== void 0 ? await service.getHTMLAttributes(validatedOptions, imageConfig) : {}
69
85
  };
70
86
  }
@@ -10,6 +10,11 @@ type ImageConfig<T> = Omit<AstroConfig['image'], 'service'> & {
10
10
  config: T;
11
11
  };
12
12
  };
13
+ type SrcSetValue = {
14
+ transform: ImageTransform;
15
+ descriptor?: string;
16
+ attributes?: Record<string, any>;
17
+ };
13
18
  interface SharedServiceProps<T extends Record<string, any> = Record<string, any>> {
14
19
  /**
15
20
  * Return the URL to the endpoint or URL your images are generated from.
@@ -20,6 +25,13 @@ interface SharedServiceProps<T extends Record<string, any> = Record<string, any>
20
25
  *
21
26
  */
22
27
  getURL: (options: ImageTransform, imageConfig: ImageConfig<T>) => string | Promise<string>;
28
+ /**
29
+ * Generate additional `srcset` values for the image.
30
+ *
31
+ * While in most cases this is exclusively used for `srcset`, it can also be used in a more generic way to generate
32
+ * multiple variants of the same image. For instance, you can use this to generate multiple aspect ratios or multiple formats.
33
+ */
34
+ getSrcSet?: (options: ImageTransform, imageConfig: ImageConfig<T>) => SrcSetValue[] | Promise<SrcSetValue[]>;
23
35
  /**
24
36
  * Return any additional HTML attributes separate from `src` that your service requires to show the image properly.
25
37
  *
@@ -1,6 +1,6 @@
1
1
  import { AstroError, AstroErrorData } from "../../core/errors/index.js";
2
2
  import { isRemotePath, joinPaths } from "../../core/path.js";
3
- import { VALID_SUPPORTED_FORMATS } from "../consts.js";
3
+ import { DEFAULT_OUTPUT_FORMAT, VALID_SUPPORTED_FORMATS } from "../consts.js";
4
4
  import { isESMImportedImage, isRemoteAllowed } from "../internal.js";
5
5
  function isLocalService(service) {
6
6
  if (!service) {
@@ -59,30 +59,21 @@ const baseService = {
59
59
  )
60
60
  });
61
61
  }
62
+ if (options.widths && options.densities) {
63
+ throw new AstroError(AstroErrorData.IncompatibleDescriptorOptions);
64
+ }
62
65
  if (options.src.format === "svg") {
63
66
  options.format = "svg";
64
67
  }
65
68
  }
66
69
  if (!options.format) {
67
- options.format = "webp";
70
+ options.format = DEFAULT_OUTPUT_FORMAT;
68
71
  }
69
72
  return options;
70
73
  },
71
74
  getHTMLAttributes(options) {
72
- let targetWidth = options.width;
73
- let targetHeight = options.height;
74
- if (isESMImportedImage(options.src)) {
75
- const aspectRatio = options.src.width / options.src.height;
76
- if (targetHeight && !targetWidth) {
77
- targetWidth = Math.round(targetHeight * aspectRatio);
78
- } else if (targetWidth && !targetHeight) {
79
- targetHeight = Math.round(targetWidth / aspectRatio);
80
- } else if (!targetWidth && !targetHeight) {
81
- targetWidth = options.src.width;
82
- targetHeight = options.src.height;
83
- }
84
- }
85
- const { src, width, height, format, quality, ...attributes } = options;
75
+ const { targetWidth, targetHeight } = getTargetDimensions(options);
76
+ const { src, width, height, format, quality, densities, widths, formats, ...attributes } = options;
86
77
  return {
87
78
  ...attributes,
88
79
  width: targetWidth,
@@ -91,6 +82,69 @@ const baseService = {
91
82
  decoding: attributes.decoding ?? "async"
92
83
  };
93
84
  },
85
+ getSrcSet(options) {
86
+ const srcSet = [];
87
+ const { targetWidth, targetHeight } = getTargetDimensions(options);
88
+ const { widths, densities } = options;
89
+ const targetFormat = options.format ?? DEFAULT_OUTPUT_FORMAT;
90
+ const aspectRatio = targetWidth / targetHeight;
91
+ const imageWidth = isESMImportedImage(options.src) ? options.src.width : options.width;
92
+ const maxWidth = imageWidth ?? Infinity;
93
+ if (densities) {
94
+ const densityValues = densities.map((density) => {
95
+ if (typeof density === "number") {
96
+ return density;
97
+ } else {
98
+ return parseFloat(density);
99
+ }
100
+ });
101
+ const densityWidths = densityValues.sort().map((density) => Math.round(targetWidth * density));
102
+ densityWidths.forEach((width, index) => {
103
+ const maxTargetWidth = Math.min(width, maxWidth);
104
+ const { width: transformWidth, height: transformHeight, ...rest } = options;
105
+ const srcSetValue = {
106
+ transform: {
107
+ ...rest
108
+ },
109
+ descriptor: `${densityValues[index]}x`,
110
+ attributes: {
111
+ type: `image/${targetFormat}`
112
+ }
113
+ };
114
+ if (maxTargetWidth !== imageWidth) {
115
+ srcSetValue.transform.width = maxTargetWidth;
116
+ srcSetValue.transform.height = Math.round(maxTargetWidth / aspectRatio);
117
+ }
118
+ if (targetFormat !== options.format) {
119
+ srcSetValue.transform.format = targetFormat;
120
+ }
121
+ srcSet.push(srcSetValue);
122
+ });
123
+ } else if (widths) {
124
+ widths.forEach((width) => {
125
+ const maxTargetWidth = Math.min(width, maxWidth);
126
+ const { width: transformWidth, height: transformHeight, ...rest } = options;
127
+ const srcSetValue = {
128
+ transform: {
129
+ ...rest
130
+ },
131
+ descriptor: `${width}w`,
132
+ attributes: {
133
+ type: `image/${targetFormat}`
134
+ }
135
+ };
136
+ if (maxTargetWidth !== imageWidth) {
137
+ srcSetValue.transform.width = maxTargetWidth;
138
+ srcSetValue.transform.height = Math.round(maxTargetWidth / aspectRatio);
139
+ }
140
+ if (targetFormat !== options.format) {
141
+ srcSetValue.transform.format = targetFormat;
142
+ }
143
+ srcSet.push(srcSetValue);
144
+ });
145
+ }
146
+ return srcSet;
147
+ },
94
148
  getURL(options, imageConfig) {
95
149
  const searchParams = new URLSearchParams();
96
150
  if (isESMImportedImage(options.src)) {
@@ -127,6 +181,25 @@ const baseService = {
127
181
  return transform;
128
182
  }
129
183
  };
184
+ function getTargetDimensions(options) {
185
+ let targetWidth = options.width;
186
+ let targetHeight = options.height;
187
+ if (isESMImportedImage(options.src)) {
188
+ const aspectRatio = options.src.width / options.src.height;
189
+ if (targetHeight && !targetWidth) {
190
+ targetWidth = Math.round(targetHeight * aspectRatio);
191
+ } else if (targetWidth && !targetHeight) {
192
+ targetHeight = Math.round(targetWidth / aspectRatio);
193
+ } else if (!targetWidth && !targetHeight) {
194
+ targetWidth = options.src.width;
195
+ targetHeight = options.src.height;
196
+ }
197
+ }
198
+ return {
199
+ targetWidth,
200
+ targetHeight
201
+ };
202
+ }
130
203
  export {
131
204
  baseService,
132
205
  isLocalService,
@@ -24,6 +24,7 @@ const sharpService = {
24
24
  getURL: baseService.getURL,
25
25
  parseURL: baseService.parseURL,
26
26
  getHTMLAttributes: baseService.getHTMLAttributes,
27
+ getSrcSet: baseService.getSrcSet,
27
28
  async transform(inputBuffer, transformOptions) {
28
29
  if (!sharp)
29
30
  sharp = await loadSharp();
@@ -33,9 +34,9 @@ const sharpService = {
33
34
  let result = sharp(inputBuffer, { failOnError: false, pages: -1 });
34
35
  result.rotate();
35
36
  if (transform.height && !transform.width) {
36
- result.resize({ height: transform.height });
37
+ result.resize({ height: Math.round(transform.height) });
37
38
  } else if (transform.width) {
38
- result.resize({ width: transform.width });
39
+ result.resize({ width: Math.round(transform.width) });
39
40
  }
40
41
  if (transform.format) {
41
42
  let quality = void 0;
@@ -39,6 +39,7 @@ const service = {
39
39
  getURL: baseService.getURL,
40
40
  parseURL: baseService.parseURL,
41
41
  getHTMLAttributes: baseService.getHTMLAttributes,
42
+ getSrcSet: baseService.getSrcSet,
42
43
  async transform(inputBuffer, transformOptions) {
43
44
  const transform = transformOptions;
44
45
  let format = transform.format;
@@ -52,12 +53,12 @@ const service = {
52
53
  if (transform.height && !transform.width) {
53
54
  operations.push({
54
55
  type: "resize",
55
- height: transform.height
56
+ height: Math.round(transform.height)
56
57
  });
57
58
  } else if (transform.width) {
58
59
  operations.push({
59
60
  type: "resize",
60
- width: transform.width
61
+ width: Math.round(transform.width)
61
62
  });
62
63
  }
63
64
  let quality = void 0;
@@ -25,6 +25,11 @@ export interface ImageMetadata {
25
25
  format: ImageInputFormat;
26
26
  orientation?: number;
27
27
  }
28
+ export interface SrcSetValue {
29
+ url: string;
30
+ descriptor?: string;
31
+ attributes?: Record<string, string>;
32
+ }
28
33
  /**
29
34
  * A yet to be resolved image transform. Used by `getImage`
30
35
  */
@@ -39,6 +44,8 @@ export type UnresolvedImageTransform = Omit<ImageTransform, 'src'> & {
39
44
  export type ImageTransform = {
40
45
  src: ImageMetadata | string;
41
46
  width?: number | undefined;
47
+ widths?: number[] | undefined;
48
+ densities?: (number | `${number}x`)[] | undefined;
42
49
  height?: number | undefined;
43
50
  quality?: ImageQuality | undefined;
44
51
  format?: ImageOutputFormat | undefined;
@@ -48,13 +55,17 @@ export interface GetImageResult {
48
55
  rawOptions: ImageTransform;
49
56
  options: ImageTransform;
50
57
  src: string;
58
+ srcSet: {
59
+ values: SrcSetValue[];
60
+ attribute: string;
61
+ };
51
62
  attributes: Record<string, any>;
52
63
  }
53
64
  type ImageSharedProps<T> = T & {
54
65
  /**
55
66
  * Width of the image, the value of this property will be used to assign the `width` property on the final `img` element.
56
67
  *
57
- * For local images, this value will additionally be used to resize the image to the desired width, taking into account the original aspect ratio of the image.
68
+ * This value will additionally be used to resize the image to the desired width, taking into account the original aspect ratio of the image.
58
69
  *
59
70
  * **Example**:
60
71
  * ```astro
@@ -81,7 +92,23 @@ type ImageSharedProps<T> = T & {
81
92
  * ```
82
93
  */
83
94
  height?: number | `${number}`;
84
- };
95
+ } & ({
96
+ /**
97
+ * A list of widths to generate images for. The value of this property will be used to assign the `srcset` property on the final `img` element.
98
+ *
99
+ * This attribute is incompatible with `densities`.
100
+ */
101
+ widths?: number[];
102
+ densities?: never;
103
+ } | {
104
+ /**
105
+ * A list of pixel densities to generate images for. The value of this property will be used to assign the `srcset` property on the final `img` element.
106
+ *
107
+ * This attribute is incompatible with `widths`.
108
+ */
109
+ densities?: (number | `${number}x`)[];
110
+ widths?: never;
111
+ });
85
112
  export type LocalImageProps<T> = ImageSharedProps<T> & {
86
113
  /**
87
114
  * A reference to a local image imported through an ESM import.
@@ -12,7 +12,7 @@ function propsToFilename(transform, hash) {
12
12
  return `/${filename}_${hash}${outputExt}`;
13
13
  }
14
14
  function hashTransform(transform, imageService) {
15
- const { alt, ...rest } = transform;
15
+ const { alt, class: className, style, widths, densities, ...rest } = transform;
16
16
  const hashFields = { ...rest, imageService };
17
17
  return shorthash(JSON.stringify(hashFields));
18
18
  }
@@ -47,6 +47,7 @@ function assets({
47
47
  export { getConfiguredImageService, isLocalService } from "astro/assets";
48
48
  import { getImage as getImageInternal } from "astro/assets";
49
49
  export { default as Image } from "astro/components/Image.astro";
50
+ export { default as Picture } from "astro/components/Picture.astro";
50
51
 
51
52
  export const imageConfig = ${JSON.stringify(settings.config.image)};
52
53
  export const assetsDir = new URL(${JSON.stringify(