astro 3.2.4 → 3.3.1

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 (49) 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 +69 -0
  5. package/dist/@types/astro.d.ts +2 -2
  6. package/dist/assets/consts.d.ts +1 -0
  7. package/dist/assets/consts.js +2 -0
  8. package/dist/assets/internal.js +18 -2
  9. package/dist/assets/services/service.d.ts +12 -0
  10. package/dist/assets/services/service.js +93 -16
  11. package/dist/assets/services/sharp.js +3 -2
  12. package/dist/assets/services/squoosh.js +3 -2
  13. package/dist/assets/types.d.ts +29 -2
  14. package/dist/assets/utils/transformToPath.js +3 -2
  15. package/dist/assets/vite-plugin-assets.js +1 -0
  16. package/dist/cli/add/index.js +18 -12
  17. package/dist/cli/build/index.js +1 -0
  18. package/dist/cli/flags.js +1 -0
  19. package/dist/config/index.js +1 -1
  20. package/dist/content/server-listeners.js +8 -7
  21. package/dist/core/build/index.js +1 -1
  22. package/dist/core/build/plugins/plugin-internals.js +0 -2
  23. package/dist/core/config/schema.d.ts +39 -38
  24. package/dist/core/config/schema.js +15 -3
  25. package/dist/core/config/settings.d.ts +1 -1
  26. package/dist/core/config/settings.js +10 -5
  27. package/dist/core/config/tsconfig.d.ts +24 -7
  28. package/dist/core/config/tsconfig.js +44 -22
  29. package/dist/core/constants.js +1 -1
  30. package/dist/core/create-vite.js +0 -2
  31. package/dist/core/dev/dev.js +1 -1
  32. package/dist/core/dev/restart.js +2 -2
  33. package/dist/core/endpoint/index.js +24 -18
  34. package/dist/core/errors/dev/vite.js +12 -3
  35. package/dist/core/errors/errors-data.d.ts +25 -0
  36. package/dist/core/errors/errors-data.js +13 -0
  37. package/dist/core/errors/overlay.js +20 -20
  38. package/dist/core/messages.js +2 -2
  39. package/dist/core/preview/index.js +1 -1
  40. package/dist/core/shiki.d.ts +8 -0
  41. package/dist/core/shiki.js +35 -0
  42. package/dist/core/sync/index.js +1 -1
  43. package/dist/runtime/server/render/page.js +3 -0
  44. package/dist/transitions/router.d.ts +1 -1
  45. package/dist/transitions/router.js +84 -41
  46. package/package.json +6 -5
  47. package/components/Shiki.js +0 -97
  48. package/components/shiki-languages.js +0 -176
  49. 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 '../dist/core/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,69 @@
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 defaultFormats = ['webp'] as const;
15
+ const defaultFallbackFormat = 'png' as const;
16
+
17
+ // Certain formats don't want PNG fallbacks:
18
+ // - GIF will typically want to stay as a gif, either for animation or for the lower amount of colors
19
+ // - SVGs can't be converted to raster formats in most cases
20
+ // For those, we'll use the original format as the fallback instead.
21
+ const specialFormatsFallback = ['gif', 'svg'] as const;
22
+
23
+ const { formats = defaultFormats, pictureAttributes = {}, fallbackFormat, ...props } = Astro.props;
24
+
25
+ if (props.alt === undefined || props.alt === null) {
26
+ throw new AstroError(AstroErrorData.ImageMissingAlt);
27
+ }
28
+
29
+ const optimizedImages: GetImageResult[] = await Promise.all(
30
+ formats.map(
31
+ async (format) =>
32
+ await getImage({ ...props, format: format, widths: props.widths, densities: props.densities })
33
+ )
34
+ );
35
+
36
+ let resultFallbackFormat = fallbackFormat ?? defaultFallbackFormat;
37
+ if (
38
+ !fallbackFormat &&
39
+ isESMImportedImage(props.src) &&
40
+ specialFormatsFallback.includes(props.src.format)
41
+ ) {
42
+ resultFallbackFormat = props.src.format;
43
+ }
44
+
45
+ const fallbackImage = await getImage({
46
+ ...props,
47
+ format: resultFallbackFormat,
48
+ widths: props.widths,
49
+ densities: props.densities,
50
+ });
51
+
52
+ const additionalAttributes: Record<string, any> = {};
53
+ if (fallbackImage.srcSet.values.length > 0) {
54
+ additionalAttributes.srcset = fallbackImage.srcSet.attribute;
55
+ }
56
+ ---
57
+
58
+ <picture {...pictureAttributes}>
59
+ {
60
+ Object.entries(optimizedImages).map(([_, image]) => {
61
+ const srcsetAttribute =
62
+ props.densities || (!props.densities && !props.widths)
63
+ ? `${image.src}${image.srcSet.values.length > 0 ? ', ' + image.srcSet.attribute : ''}`
64
+ : image.srcSet.attribute;
65
+ return <source srcset={srcsetAttribute} type={'image/' + image.options.format} />;
66
+ })
67
+ }
68
+ <img src={fallbackImage.src} {...additionalAttributes} {...fallbackImage.attributes} />
69
+ </picture>
@@ -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,28 @@ 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
  }
68
+ if (options.src.format === "svg" && options.format !== "svg" || options.src.format !== "svg" && options.format === "svg") {
69
+ throw new AstroError(AstroErrorData.UnsupportedImageConversion);
70
+ }
65
71
  }
66
72
  if (!options.format) {
67
- options.format = "webp";
73
+ options.format = DEFAULT_OUTPUT_FORMAT;
68
74
  }
75
+ if (options.width)
76
+ options.width = Math.round(options.width);
77
+ if (options.height)
78
+ options.height = Math.round(options.height);
69
79
  return options;
70
80
  },
71
81
  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;
82
+ const { targetWidth, targetHeight } = getTargetDimensions(options);
83
+ const { src, width, height, format, quality, densities, widths, formats, ...attributes } = options;
86
84
  return {
87
85
  ...attributes,
88
86
  width: targetWidth,
@@ -91,6 +89,66 @@ const baseService = {
91
89
  decoding: attributes.decoding ?? "async"
92
90
  };
93
91
  },
92
+ getSrcSet(options) {
93
+ const srcSet = [];
94
+ const { targetWidth } = getTargetDimensions(options);
95
+ const { widths, densities } = options;
96
+ const targetFormat = options.format ?? DEFAULT_OUTPUT_FORMAT;
97
+ let imageWidth = options.width;
98
+ let maxWidth = Infinity;
99
+ if (isESMImportedImage(options.src)) {
100
+ imageWidth = options.src.width;
101
+ maxWidth = imageWidth;
102
+ }
103
+ const {
104
+ width: transformWidth,
105
+ height: transformHeight,
106
+ ...transformWithoutDimensions
107
+ } = options;
108
+ const allWidths = [];
109
+ if (densities) {
110
+ const densityValues = densities.map((density) => {
111
+ if (typeof density === "number") {
112
+ return density;
113
+ } else {
114
+ return parseFloat(density);
115
+ }
116
+ });
117
+ const densityWidths = densityValues.sort().map((density) => Math.round(targetWidth * density));
118
+ allWidths.push(
119
+ ...densityWidths.map((width, index) => ({
120
+ maxTargetWidth: Math.min(width, maxWidth),
121
+ descriptor: `${densityValues[index]}x`
122
+ }))
123
+ );
124
+ } else if (widths) {
125
+ allWidths.push(
126
+ ...widths.map((width) => ({
127
+ maxTargetWidth: Math.min(width, maxWidth),
128
+ descriptor: `${width}w`
129
+ }))
130
+ );
131
+ }
132
+ for (const { maxTargetWidth, descriptor } of allWidths) {
133
+ const srcSetTransform = { ...transformWithoutDimensions };
134
+ if (maxTargetWidth !== imageWidth) {
135
+ srcSetTransform.width = maxTargetWidth;
136
+ } else {
137
+ if (options.width && options.height) {
138
+ srcSetTransform.width = options.width;
139
+ srcSetTransform.height = options.height;
140
+ }
141
+ }
142
+ srcSet.push({
143
+ transform: srcSetTransform,
144
+ descriptor,
145
+ attributes: {
146
+ type: `image/${targetFormat}`
147
+ }
148
+ });
149
+ }
150
+ return srcSet;
151
+ },
94
152
  getURL(options, imageConfig) {
95
153
  const searchParams = new URLSearchParams();
96
154
  if (isESMImportedImage(options.src)) {
@@ -127,6 +185,25 @@ const baseService = {
127
185
  return transform;
128
186
  }
129
187
  };
188
+ function getTargetDimensions(options) {
189
+ let targetWidth = options.width;
190
+ let targetHeight = options.height;
191
+ if (isESMImportedImage(options.src)) {
192
+ const aspectRatio = options.src.width / options.src.height;
193
+ if (targetHeight && !targetWidth) {
194
+ targetWidth = Math.round(targetHeight * aspectRatio);
195
+ } else if (targetWidth && !targetHeight) {
196
+ targetHeight = Math.round(targetWidth / aspectRatio);
197
+ } else if (!targetWidth && !targetHeight) {
198
+ targetWidth = options.src.width;
199
+ targetHeight = options.src.height;
200
+ }
201
+ }
202
+ return {
203
+ targetWidth,
204
+ targetHeight
205
+ };
206
+ }
130
207
  export {
131
208
  baseService,
132
209
  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.
@@ -1,3 +1,4 @@
1
+ import { deterministicString } from "deterministic-object-hash";
1
2
  import { basename, extname } from "node:path";
2
3
  import { removeQueryString } from "../../core/path.js";
3
4
  import { shorthash } from "../../runtime/server/shorthash.js";
@@ -12,9 +13,9 @@ function propsToFilename(transform, hash) {
12
13
  return `/${filename}_${hash}${outputExt}`;
13
14
  }
14
15
  function hashTransform(transform, imageService) {
15
- const { alt, ...rest } = transform;
16
+ const { alt, class: className, style, widths, densities, ...rest } = transform;
16
17
  const hashFields = { ...rest, imageService };
17
- return shorthash(JSON.stringify(hashFields));
18
+ return shorthash(deterministicString(hashFields));
18
19
  }
19
20
  export {
20
21
  hashTransform,
@@ -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(