astro 5.0.0-beta.8 → 5.0.0-beta.9

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 (77) hide show
  1. package/client.d.ts +24 -5
  2. package/components/Image.astro +26 -3
  3. package/components/Picture.astro +33 -6
  4. package/components/image.css +17 -0
  5. package/dist/actions/integration.js +3 -2
  6. package/dist/actions/runtime/virtual/server.js +5 -2
  7. package/dist/actions/utils.d.ts +1 -1
  8. package/dist/actions/utils.js +2 -0
  9. package/dist/assets/build/generate.js +2 -2
  10. package/dist/assets/consts.js +9 -1
  11. package/dist/assets/endpoint/config.js +2 -1
  12. package/dist/assets/internal.d.ts +5 -1
  13. package/dist/assets/internal.js +66 -12
  14. package/dist/assets/layout.d.ts +25 -0
  15. package/dist/assets/layout.js +103 -0
  16. package/dist/assets/runtime.d.ts +10 -0
  17. package/dist/assets/runtime.js +69 -0
  18. package/dist/assets/services/service.d.ts +3 -1
  19. package/dist/assets/services/service.js +57 -37
  20. package/dist/assets/services/sharp.js +28 -3
  21. package/dist/assets/types.d.ts +53 -0
  22. package/dist/assets/utils/imageAttributes.d.ts +10 -0
  23. package/dist/assets/utils/imageAttributes.js +33 -0
  24. package/dist/assets/utils/imageKind.js +1 -1
  25. package/dist/assets/utils/index.d.ts +1 -0
  26. package/dist/assets/utils/index.js +2 -0
  27. package/dist/assets/utils/node/emitAsset.d.ts +4 -1
  28. package/dist/assets/utils/node/emitAsset.js +3 -0
  29. package/dist/assets/utils/svg.d.ts +5 -0
  30. package/dist/assets/utils/svg.js +22 -0
  31. package/dist/assets/vite-plugin-assets.js +20 -10
  32. package/dist/config/index.js +2 -2
  33. package/dist/container/index.js +2 -1
  34. package/dist/content/content-layer.js +3 -3
  35. package/dist/content/loaders/glob.js +2 -1
  36. package/dist/content/runtime.d.ts +1 -0
  37. package/dist/content/runtime.js +10 -2
  38. package/dist/content/server-listeners.js +10 -1
  39. package/dist/content/types-generator.js +23 -30
  40. package/dist/content/utils.d.ts +1 -1
  41. package/dist/content/utils.js +12 -8
  42. package/dist/core/app/pipeline.js +0 -1
  43. package/dist/core/base-pipeline.d.ts +1 -2
  44. package/dist/core/base-pipeline.js +1 -7
  45. package/dist/core/build/generate.js +2 -7
  46. package/dist/core/build/pipeline.js +2 -2
  47. package/dist/core/build/plugins/plugin-ssr.js +0 -1
  48. package/dist/core/config/schema.d.ts +148 -0
  49. package/dist/core/config/schema.js +30 -4
  50. package/dist/core/constants.js +1 -1
  51. package/dist/core/dev/container.js +3 -1
  52. package/dist/core/dev/dev.js +1 -1
  53. package/dist/core/errors/errors-data.d.ts +5 -15
  54. package/dist/core/errors/errors-data.js +1 -7
  55. package/dist/core/messages.js +2 -2
  56. package/dist/core/middleware/vite-plugin.js +1 -13
  57. package/dist/core/render-context.js +3 -5
  58. package/dist/core/routing/astro-designed-error-pages.js +4 -2
  59. package/dist/core/routing/manifest/create.d.ts +3 -1
  60. package/dist/core/routing/manifest/create.js +12 -5
  61. package/dist/core/routing/manifest/serialization.js +2 -1
  62. package/dist/core/routing/match.d.ts +7 -0
  63. package/dist/core/routing/match.js +4 -0
  64. package/dist/core/server-islands/endpoint.js +2 -1
  65. package/dist/core/sync/index.js +12 -3
  66. package/dist/integrations/hooks.d.ts +5 -0
  67. package/dist/integrations/hooks.js +41 -1
  68. package/dist/runtime/server/render/server-islands.js +15 -14
  69. package/dist/runtime/server/render/util.d.ts +1 -0
  70. package/dist/runtime/server/render/util.js +1 -0
  71. package/dist/transitions/router.js +1 -1
  72. package/dist/types/astro.d.ts +3 -3
  73. package/dist/types/public/config.d.ts +266 -6
  74. package/dist/types/public/integrations.d.ts +32 -10
  75. package/dist/types/public/internal.d.ts +13 -0
  76. package/dist/vite-plugin-astro-server/plugin.js +28 -10
  77. package/package.json +6 -4
package/client.d.ts CHANGED
@@ -47,7 +47,9 @@ declare module 'astro:assets' {
47
47
  getImage: (
48
48
  options: import('./dist/assets/types.js').UnresolvedImageTransform,
49
49
  ) => Promise<import('./dist/assets/types.js').GetImageResult>;
50
- imageConfig: import('./dist/types/public/config.js').AstroConfig['image'];
50
+ imageConfig: import('./dist/types/public/config.js').AstroConfig['image'] & {
51
+ experimentalResponsiveImages: boolean;
52
+ };
51
53
  getConfiguredImageService: typeof import('./dist/assets/index.js').getConfiguredImageService;
52
54
  inferRemoteSize: typeof import('./dist/assets/utils/index.js').inferRemoteSize;
53
55
  Image: typeof import('./components/Image.astro').default;
@@ -101,14 +103,31 @@ declare module '*.webp' {
101
103
  const metadata: ImageMetadata;
102
104
  export default metadata;
103
105
  }
104
- declare module '*.svg' {
105
- const metadata: ImageMetadata;
106
- export default metadata;
107
- }
108
106
  declare module '*.avif' {
109
107
  const metadata: ImageMetadata;
110
108
  export default metadata;
111
109
  }
110
+ declare module '*.svg' {
111
+ type Props = {
112
+ /**
113
+ * Accesible, short-text description
114
+ *
115
+ * {@link https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title|MDN Reference}
116
+ */
117
+ title?: string;
118
+ /**
119
+ * Shorthand for setting the `height` and `width` properties
120
+ */
121
+ size?: number | string;
122
+ /**
123
+ * Override the default rendering mode for SVGs
124
+ */
125
+ mode?: import('./dist/assets/utils/svg.js').SvgRenderMode;
126
+ } & astroHTML.JSX.SVGAttributes;
127
+
128
+ const Component: ((_props: Props) => any) & ImageMetadata;
129
+ export default Component;
130
+ }
112
131
 
113
132
  declare module 'astro:transitions' {
114
133
  type TransitionModule = typeof import('./dist/virtual-modules/transitions.js');
@@ -1,7 +1,10 @@
1
1
  ---
2
- import { type LocalImageProps, type RemoteImageProps, getImage } from 'astro:assets';
2
+ import { type LocalImageProps, type RemoteImageProps, getImage, imageConfig } from 'astro:assets';
3
+ import type { UnresolvedImageTransform } from '../dist/assets/types';
4
+ import { applyResponsiveAttributes } from '../dist/assets/utils/imageAttributes.js';
3
5
  import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
4
6
  import type { HTMLAttributes } from '../types';
7
+ import './image.css';
5
8
 
6
9
  // The TypeScript diagnostic for JSX props uses the last member of the union to suggest props, so it would be better for
7
10
  // LocalImageProps to be last. Unfortunately, when we do this the error messages that remote images get are complete nonsense
@@ -23,7 +26,17 @@ if (typeof props.height === 'string') {
23
26
  props.height = parseInt(props.height);
24
27
  }
25
28
 
26
- const image = await getImage(props);
29
+ const layout = props.layout ?? imageConfig.experimentalLayout ?? 'none';
30
+ const useResponsive = imageConfig.experimentalResponsiveImages && layout !== 'none';
31
+
32
+ if (useResponsive) {
33
+ // Apply defaults from imageConfig if not provided
34
+ props.layout ??= imageConfig.experimentalLayout;
35
+ props.fit ??= imageConfig.experimentalObjectFit ?? 'cover';
36
+ props.position ??= imageConfig.experimentalObjectPosition ?? 'center';
37
+ }
38
+
39
+ const image = await getImage(props as UnresolvedImageTransform);
27
40
 
28
41
  const additionalAttributes: HTMLAttributes<'img'> = {};
29
42
  if (image.srcSet.values.length > 0) {
@@ -33,6 +46,16 @@ if (image.srcSet.values.length > 0) {
33
46
  if (import.meta.env.DEV) {
34
47
  additionalAttributes['data-image-component'] = 'true';
35
48
  }
49
+
50
+ const attributes = useResponsive
51
+ ? applyResponsiveAttributes({
52
+ layout,
53
+ image,
54
+ props,
55
+ additionalAttributes,
56
+ })
57
+ : { ...additionalAttributes, ...image.attributes };
36
58
  ---
37
59
 
38
- <img src={image.src} {...additionalAttributes} {...image.attributes} />
60
+ {/* Applying class outside of the spread prevents it from applying unnecessary astro-* classes */}
61
+ <img src={image.src} {...attributes} class={attributes.class} />
@@ -1,10 +1,16 @@
1
1
  ---
2
- import { type LocalImageProps, type RemoteImageProps, getImage } from 'astro:assets';
2
+ import { type LocalImageProps, type RemoteImageProps, getImage, imageConfig } from 'astro:assets';
3
3
  import * as mime from 'mrmime';
4
+ import { applyResponsiveAttributes } from '../dist/assets/utils/imageAttributes';
4
5
  import { isESMImportedImage, resolveSrc } from '../dist/assets/utils/imageKind';
5
6
  import { AstroError, AstroErrorData } from '../dist/core/errors/index.js';
6
- import type { GetImageResult, ImageOutputFormat } from '../dist/types/public/index.js';
7
+ import type {
8
+ GetImageResult,
9
+ ImageOutputFormat,
10
+ UnresolvedImageTransform,
11
+ } from '../dist/types/public/index.js';
7
12
  import type { HTMLAttributes } from '../types';
13
+ import './image.css';
8
14
 
9
15
  type Props = (LocalImageProps | RemoteImageProps) & {
10
16
  formats?: ImageOutputFormat[];
@@ -37,6 +43,17 @@ if (scopedStyleClass) {
37
43
  pictureAttributes.class = scopedStyleClass;
38
44
  }
39
45
  }
46
+
47
+ const layout = props.layout ?? imageConfig.experimentalLayout ?? 'none';
48
+ const useResponsive = imageConfig.experimentalResponsiveImages && layout !== 'none';
49
+
50
+ if (useResponsive) {
51
+ // Apply defaults from imageConfig if not provided
52
+ props.layout ??= imageConfig.experimentalLayout;
53
+ props.fit ??= imageConfig.experimentalObjectFit ?? 'cover';
54
+ props.position ??= imageConfig.experimentalObjectPosition ?? 'center';
55
+ }
56
+
40
57
  for (const key in props) {
41
58
  if (key.startsWith('data-astro-cid')) {
42
59
  pictureAttributes[key] = props[key];
@@ -53,7 +70,7 @@ const optimizedImages: GetImageResult[] = await Promise.all(
53
70
  format: format,
54
71
  widths: props.widths,
55
72
  densities: props.densities,
56
- }),
73
+ } as UnresolvedImageTransform),
57
74
  ),
58
75
  );
59
76
 
@@ -71,7 +88,7 @@ const fallbackImage = await getImage({
71
88
  format: resultFallbackFormat,
72
89
  widths: props.widths,
73
90
  densities: props.densities,
74
- });
91
+ } as UnresolvedImageTransform);
75
92
 
76
93
  const imgAdditionalAttributes: HTMLAttributes<'img'> = {};
77
94
  const sourceAdditionalAttributes: HTMLAttributes<'source'> = {};
@@ -85,6 +102,15 @@ if (fallbackImage.srcSet.values.length > 0) {
85
102
  imgAdditionalAttributes.srcset = fallbackImage.srcSet.attribute;
86
103
  }
87
104
 
105
+ const attributes = useResponsive
106
+ ? applyResponsiveAttributes({
107
+ layout,
108
+ image: fallbackImage,
109
+ props,
110
+ additionalAttributes: imgAdditionalAttributes,
111
+ })
112
+ : { ...imgAdditionalAttributes, ...fallbackImage.attributes };
113
+
88
114
  if (import.meta.env.DEV) {
89
115
  imgAdditionalAttributes['data-image-component'] = 'true';
90
116
  }
@@ -94,7 +120,7 @@ if (import.meta.env.DEV) {
94
120
  {
95
121
  Object.entries(optimizedImages).map(([_, image]) => {
96
122
  const srcsetAttribute =
97
- props.densities || (!props.densities && !props.widths)
123
+ props.densities || (!props.densities && !props.widths && !useResponsive)
98
124
  ? `${image.src}${image.srcSet.values.length > 0 ? ', ' + image.srcSet.attribute : ''}`
99
125
  : image.srcSet.attribute;
100
126
  return (
@@ -106,5 +132,6 @@ if (import.meta.env.DEV) {
106
132
  );
107
133
  })
108
134
  }
109
- <img src={fallbackImage.src} {...imgAdditionalAttributes} {...fallbackImage.attributes} />
135
+ {/* Applying class outside of the spread prevents it from applying unnecessary astro-* classes */}
136
+ <img src={fallbackImage.src} {...attributes} class={attributes.class} />
110
137
  </picture>
@@ -0,0 +1,17 @@
1
+ [data-astro-image] {
2
+ width: 100%;
3
+ height: auto;
4
+ object-fit: var(--fit);
5
+ object-position: var(--pos);
6
+ aspect-ratio: var(--w) / var(--h);
7
+ }
8
+ /* Styles for responsive layout */
9
+ [data-astro-image='responsive'] {
10
+ max-width: calc(var(--w) * 1px);
11
+ max-height: calc(var(--h) * 1px);
12
+ }
13
+ /* Styles for fixed layout */
14
+ [data-astro-image='fixed'] {
15
+ width: calc(var(--w) * 1px);
16
+ height: calc(var(--h) * 1px);
17
+ }
@@ -9,10 +9,11 @@ function astroIntegrationActionsRouteHandler({
9
9
  name: VIRTUAL_MODULE_ID,
10
10
  hooks: {
11
11
  async "astro:config:setup"(params) {
12
- params.injectRoute({
12
+ settings.injectedRoutes.push({
13
13
  pattern: ACTION_RPC_ROUTE_PATTERN,
14
14
  entrypoint: "astro/actions/runtime/route.js",
15
- prerender: false
15
+ prerender: false,
16
+ origin: "internal"
16
17
  });
17
18
  params.addMiddleware({
18
19
  entrypoint: "astro/actions/runtime/middleware.js",
@@ -3,8 +3,10 @@ import { ActionCalledFromServerError } from "../../../core/errors/errors-data.js
3
3
  import { AstroError } from "../../../core/errors/errors.js";
4
4
  import { ACTION_RPC_ROUTE_PATTERN } from "../../consts.js";
5
5
  import {
6
+ ACTION_API_CONTEXT_SYMBOL,
6
7
  formContentTypes,
7
- hasContentType
8
+ hasContentType,
9
+ isActionAPIContext
8
10
  } from "../utils.js";
9
11
  import { getAction } from "./get-action.js";
10
12
  import {
@@ -23,7 +25,7 @@ function defineAction({
23
25
  }) {
24
26
  const serverHandler = accept === "form" ? getFormServerHandler(handler, inputSchema) : getJsonServerHandler(handler, inputSchema);
25
27
  async function safeServerHandler(unparsedInput) {
26
- if (typeof this === "function") {
28
+ if (typeof this === "function" || !isActionAPIContext(this)) {
27
29
  throw new AstroError(ActionCalledFromServerError);
28
30
  }
29
31
  return callSafely(() => serverHandler(unparsedInput, this));
@@ -158,6 +160,7 @@ function getActionContext(context) {
158
160
  redirect: _redirect,
159
161
  ...actionAPIContext
160
162
  } = context;
163
+ Reflect.set(actionAPIContext, ACTION_API_CONTEXT_SYMBOL, true);
161
164
  const handler = baseAction.bind(actionAPIContext);
162
165
  return handler(input);
163
166
  }
@@ -1,6 +1,6 @@
1
1
  import type fsMod from 'node:fs';
2
2
  import type { APIContext } from '../types/public/context.js';
3
- import type { ActionAPIContext, Locals } from './runtime/utils.js';
3
+ import { type ActionAPIContext, type Locals } from './runtime/utils.js';
4
4
  export declare function hasActionPayload(locals: APIContext['locals']): locals is Locals;
5
5
  export declare function createGetActionResult(locals: APIContext['locals']): APIContext['getActionResult'];
6
6
  export declare function createCallAction(context: ActionAPIContext): APIContext['callAction'];
@@ -1,4 +1,5 @@
1
1
  import * as eslexer from "es-module-lexer";
2
+ import { ACTION_API_CONTEXT_SYMBOL } from "./runtime/utils.js";
2
3
  import { deserializeActionResult, getActionQueryString } from "./runtime/virtual/shared.js";
3
4
  function hasActionPayload(locals) {
4
5
  return "_actionPayload" in locals;
@@ -13,6 +14,7 @@ function createGetActionResult(locals) {
13
14
  }
14
15
  function createCallAction(context) {
15
16
  return (baseAction, input) => {
17
+ Reflect.set(context, ACTION_API_CONTEXT_SYMBOL, true);
16
18
  const action = baseAction.bind(context);
17
19
  return action(input);
18
20
  };
@@ -83,6 +83,8 @@ async function generateImagesForPath(originalFilePath, transformsAndPath, env, q
83
83
  async function generateImageInternal(filepath, options) {
84
84
  const isLocalImage = isESMImportedImage(options.src);
85
85
  const finalFileURL = new URL("." + filepath, env.clientRoot);
86
+ const finalFolderURL = new URL("./", finalFileURL);
87
+ await fs.promises.mkdir(finalFolderURL, { recursive: true });
86
88
  const cacheFile = basename(filepath) + (isLocalImage ? "" : ".json");
87
89
  const cachedFileURL = new URL(cacheFile, env.assetsCacheDir);
88
90
  try {
@@ -113,8 +115,6 @@ async function generateImagesForPath(originalFilePath, transformsAndPath, env, q
113
115
  throw new Error(`An error was encountered while reading the cache file. Error: ${e}`);
114
116
  }
115
117
  }
116
- const finalFolderURL = new URL("./", finalFileURL);
117
- await fs.promises.mkdir(finalFolderURL, { recursive: true });
118
118
  const originalImagePath = isLocalImage ? options.src.src : options.src;
119
119
  if (!originalImage) {
120
120
  originalImage = await loadImage(originalFilePath, env);
@@ -22,7 +22,15 @@ const VALID_SUPPORTED_FORMATS = [
22
22
  ];
23
23
  const DEFAULT_OUTPUT_FORMAT = "webp";
24
24
  const VALID_OUTPUT_FORMATS = ["avif", "png", "webp", "jpeg", "jpg", "svg"];
25
- const DEFAULT_HASH_PROPS = ["src", "width", "height", "format", "quality"];
25
+ const DEFAULT_HASH_PROPS = [
26
+ "src",
27
+ "width",
28
+ "height",
29
+ "format",
30
+ "quality",
31
+ "fit",
32
+ "position"
33
+ ];
26
34
  export {
27
35
  DEFAULT_HASH_PROPS,
28
36
  DEFAULT_OUTPUT_FORMAT,
@@ -36,7 +36,8 @@ function getImageEndpointData(settings, mode, cwd) {
36
36
  generate: () => "",
37
37
  pathname: settings.config.image.endpoint.route,
38
38
  prerender: false,
39
- fallbackRoutes: []
39
+ fallbackRoutes: [],
40
+ origin: "internal"
40
41
  };
41
42
  }
42
43
  export {
@@ -2,4 +2,8 @@ import type { AstroConfig } from '../types/public/config.js';
2
2
  import { type ImageService } from './services/service.js';
3
3
  import { type GetImageResult, type UnresolvedImageTransform } from './types.js';
4
4
  export declare function getConfiguredImageService(): Promise<ImageService>;
5
- export declare function getImage(options: UnresolvedImageTransform, imageConfig: AstroConfig['image']): Promise<GetImageResult>;
5
+ type ImageConfig = AstroConfig['image'] & {
6
+ experimentalResponsiveImages: boolean;
7
+ };
8
+ export declare function getImage(options: UnresolvedImageTransform, imageConfig: ImageConfig): Promise<GetImageResult>;
9
+ export {};
@@ -1,6 +1,12 @@
1
1
  import { isRemotePath } from "@astrojs/internal-helpers/path";
2
2
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
3
3
  import { DEFAULT_HASH_PROPS } from "./consts.js";
4
+ import {
5
+ DEFAULT_RESOLUTIONS,
6
+ LIMITED_RESOLUTIONS,
7
+ getSizesAttribute,
8
+ getWidths
9
+ } from "./layout.js";
4
10
  import { isLocalService } from "./services/service.js";
5
11
  import {
6
12
  isImageMetadata
@@ -48,10 +54,16 @@ async function getImage(options, imageConfig) {
48
54
  ...options,
49
55
  src: await resolveSrc(options.src)
50
56
  };
57
+ let originalWidth;
58
+ let originalHeight;
59
+ let originalFormat;
51
60
  if (options.inferSize && isRemoteImage(resolvedOptions.src) && isRemotePath(resolvedOptions.src)) {
52
61
  const result = await inferRemoteSize(resolvedOptions.src);
53
62
  resolvedOptions.width ??= result.width;
54
63
  resolvedOptions.height ??= result.height;
64
+ originalWidth = result.width;
65
+ originalHeight = result.height;
66
+ originalFormat = result.format;
55
67
  delete resolvedOptions.inferSize;
56
68
  }
57
69
  const originalFilePath = isESMImportedImage(resolvedOptions.src) ? resolvedOptions.src.fsPath : void 0;
@@ -59,17 +71,57 @@ async function getImage(options, imageConfig) {
59
71
  // @ts-expect-error - clone is a private, hidden prop
60
72
  resolvedOptions.src.clone ?? resolvedOptions.src
61
73
  ) : resolvedOptions.src;
74
+ if (isESMImportedImage(clonedSrc)) {
75
+ originalWidth = clonedSrc.width;
76
+ originalHeight = clonedSrc.height;
77
+ originalFormat = clonedSrc.format;
78
+ }
79
+ if (originalWidth && originalHeight) {
80
+ const aspectRatio = originalWidth / originalHeight;
81
+ if (resolvedOptions.height && !resolvedOptions.width) {
82
+ resolvedOptions.width = Math.round(resolvedOptions.height * aspectRatio);
83
+ } else if (resolvedOptions.width && !resolvedOptions.height) {
84
+ resolvedOptions.height = Math.round(resolvedOptions.width / aspectRatio);
85
+ } else if (!resolvedOptions.width && !resolvedOptions.height) {
86
+ resolvedOptions.width = originalWidth;
87
+ resolvedOptions.height = originalHeight;
88
+ }
89
+ }
62
90
  resolvedOptions.src = clonedSrc;
91
+ const layout = options.layout ?? imageConfig.experimentalLayout;
92
+ if (imageConfig.experimentalResponsiveImages && layout) {
93
+ resolvedOptions.widths ||= getWidths({
94
+ width: resolvedOptions.width,
95
+ layout,
96
+ originalWidth,
97
+ breakpoints: imageConfig.experimentalBreakpoints?.length ? imageConfig.experimentalBreakpoints : isLocalService(service) ? LIMITED_RESOLUTIONS : DEFAULT_RESOLUTIONS
98
+ });
99
+ resolvedOptions.sizes ||= getSizesAttribute({ width: resolvedOptions.width, layout });
100
+ if (resolvedOptions.priority) {
101
+ resolvedOptions.loading ??= "eager";
102
+ resolvedOptions.decoding ??= "sync";
103
+ resolvedOptions.fetchpriority ??= "high";
104
+ } else {
105
+ resolvedOptions.loading ??= "lazy";
106
+ resolvedOptions.decoding ??= "async";
107
+ resolvedOptions.fetchpriority ??= "auto";
108
+ }
109
+ delete resolvedOptions.priority;
110
+ delete resolvedOptions.densities;
111
+ }
63
112
  const validatedOptions = service.validateOptions ? await service.validateOptions(resolvedOptions, imageConfig) : resolvedOptions;
64
113
  const srcSetTransforms = service.getSrcSet ? await service.getSrcSet(validatedOptions, imageConfig) : [];
65
114
  let imageURL = await service.getURL(validatedOptions, imageConfig);
115
+ const matchesOriginal = (transform) => transform.width === originalWidth && transform.height === originalHeight && transform.format === originalFormat;
66
116
  let srcSets = await Promise.all(
67
- srcSetTransforms.map(async (srcSet) => ({
68
- transform: srcSet.transform,
69
- url: await service.getURL(srcSet.transform, imageConfig),
70
- descriptor: srcSet.descriptor,
71
- attributes: srcSet.attributes
72
- }))
117
+ srcSetTransforms.map(async (srcSet) => {
118
+ return {
119
+ transform: srcSet.transform,
120
+ url: matchesOriginal(srcSet.transform) ? imageURL : await service.getURL(srcSet.transform, imageConfig),
121
+ descriptor: srcSet.descriptor,
122
+ attributes: srcSet.attributes
123
+ };
124
+ })
73
125
  );
74
126
  if (isLocalService(service) && globalThis.astroAsset.addStaticImage && !(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)) {
75
127
  const propsToHash = service.propertiesToHash ?? DEFAULT_HASH_PROPS;
@@ -78,12 +130,14 @@ async function getImage(options, imageConfig) {
78
130
  propsToHash,
79
131
  originalFilePath
80
132
  );
81
- srcSets = srcSetTransforms.map((srcSet) => ({
82
- transform: srcSet.transform,
83
- url: globalThis.astroAsset.addStaticImage(srcSet.transform, propsToHash, originalFilePath),
84
- descriptor: srcSet.descriptor,
85
- attributes: srcSet.attributes
86
- }));
133
+ srcSets = srcSetTransforms.map((srcSet) => {
134
+ return {
135
+ transform: srcSet.transform,
136
+ url: matchesOriginal(srcSet.transform) ? imageURL : globalThis.astroAsset.addStaticImage(srcSet.transform, propsToHash, originalFilePath),
137
+ descriptor: srcSet.descriptor,
138
+ attributes: srcSet.attributes
139
+ };
140
+ });
87
141
  }
88
142
  return {
89
143
  rawOptions: resolvedOptions,
@@ -0,0 +1,25 @@
1
+ import type { ImageLayout } from './types.js';
2
+ export declare const DEFAULT_RESOLUTIONS: number[];
3
+ export declare const LIMITED_RESOLUTIONS: number[];
4
+ /**
5
+ * Gets the breakpoints for an image, based on the layout and width
6
+ *
7
+ * The rules are as follows:
8
+ *
9
+ * - For full-width layout we return all breakpoints smaller than the original image width
10
+ * - For fixed layout we return 1x and 2x the requested width, unless the original image is smaller than that.
11
+ * - For responsive layout we return all breakpoints smaller than 2x the requested width, unless the original image is smaller than that.
12
+ */
13
+ export declare const getWidths: ({ width, layout, breakpoints, originalWidth, }: {
14
+ width?: number;
15
+ layout: ImageLayout;
16
+ breakpoints?: Array<number>;
17
+ originalWidth?: number;
18
+ }) => Array<number>;
19
+ /**
20
+ * Gets the `sizes` attribute for an image, based on the layout and width
21
+ */
22
+ export declare const getSizesAttribute: ({ width, layout, }: {
23
+ width?: number;
24
+ layout?: ImageLayout;
25
+ }) => string | undefined;
@@ -0,0 +1,103 @@
1
+ const DEFAULT_RESOLUTIONS = [
2
+ 640,
3
+ // older and lower-end phones
4
+ 750,
5
+ // iPhone 6-8
6
+ 828,
7
+ // iPhone XR/11
8
+ 960,
9
+ // older horizontal phones
10
+ 1080,
11
+ // iPhone 6-8 Plus
12
+ 1280,
13
+ // 720p
14
+ 1668,
15
+ // Various iPads
16
+ 1920,
17
+ // 1080p
18
+ 2048,
19
+ // QXGA
20
+ 2560,
21
+ // WQXGA
22
+ 3200,
23
+ // QHD+
24
+ 3840,
25
+ // 4K
26
+ 4480,
27
+ // 4.5K
28
+ 5120,
29
+ // 5K
30
+ 6016
31
+ // 6K
32
+ ];
33
+ const LIMITED_RESOLUTIONS = [
34
+ 640,
35
+ // older and lower-end phones
36
+ 750,
37
+ // iPhone 6-8
38
+ 828,
39
+ // iPhone XR/11
40
+ 1080,
41
+ // iPhone 6-8 Plus
42
+ 1280,
43
+ // 720p
44
+ 1668,
45
+ // Various iPads
46
+ 2048,
47
+ // QXGA
48
+ 2560
49
+ // WQXGA
50
+ ];
51
+ const getWidths = ({
52
+ width,
53
+ layout,
54
+ breakpoints = DEFAULT_RESOLUTIONS,
55
+ originalWidth
56
+ }) => {
57
+ const smallerThanOriginal = (w) => !originalWidth || w <= originalWidth;
58
+ if (layout === "full-width") {
59
+ return breakpoints.filter(smallerThanOriginal);
60
+ }
61
+ if (!width) {
62
+ return [];
63
+ }
64
+ const doubleWidth = width * 2;
65
+ const maxSize = originalWidth ? Math.min(doubleWidth, originalWidth) : doubleWidth;
66
+ if (layout === "fixed") {
67
+ return originalWidth && width > originalWidth ? [originalWidth] : [width, maxSize];
68
+ }
69
+ if (layout === "responsive") {
70
+ return [
71
+ // Always include the image at 1x and 2x the specified width
72
+ width,
73
+ doubleWidth,
74
+ ...breakpoints
75
+ ].filter((w) => w <= maxSize).sort((a, b) => a - b);
76
+ }
77
+ return [];
78
+ };
79
+ const getSizesAttribute = ({
80
+ width,
81
+ layout
82
+ }) => {
83
+ if (!width || !layout) {
84
+ return void 0;
85
+ }
86
+ switch (layout) {
87
+ case `responsive`:
88
+ return `(min-width: ${width}px) ${width}px, 100vw`;
89
+ case `fixed`:
90
+ return `${width}px`;
91
+ case `full-width`:
92
+ return `100vw`;
93
+ case "none":
94
+ default:
95
+ return void 0;
96
+ }
97
+ };
98
+ export {
99
+ DEFAULT_RESOLUTIONS,
100
+ LIMITED_RESOLUTIONS,
101
+ getSizesAttribute,
102
+ getWidths
103
+ };
@@ -0,0 +1,10 @@
1
+ import type { ImageMetadata } from './types.js';
2
+ export interface SvgComponentProps {
3
+ meta: ImageMetadata;
4
+ attributes: Record<string, string>;
5
+ children: string;
6
+ }
7
+ export declare function createSvgComponent({ meta, attributes, children }: SvgComponentProps): import("../runtime/server/index.js").AstroComponentFactory & ImageMetadata;
8
+ type SvgAttributes = Record<string, any>;
9
+ export declare function dropAttributes(attributes: SvgAttributes): SvgAttributes;
10
+ export {};
@@ -0,0 +1,69 @@
1
+ import {
2
+ createComponent,
3
+ render,
4
+ spreadAttributes,
5
+ unescapeHTML
6
+ } from "../runtime/server/index.js";
7
+ const ids = /* @__PURE__ */ new WeakMap();
8
+ let counter = 0;
9
+ function createSvgComponent({ meta, attributes, children }) {
10
+ const rendered = /* @__PURE__ */ new WeakSet();
11
+ const Component = createComponent((result, props) => {
12
+ let id;
13
+ if (ids.has(result)) {
14
+ id = ids.get(result);
15
+ } else {
16
+ counter += 1;
17
+ ids.set(result, counter);
18
+ id = counter;
19
+ }
20
+ id = `a:${id}`;
21
+ const {
22
+ title: titleProp,
23
+ viewBox,
24
+ mode,
25
+ ...normalizedProps
26
+ } = normalizeProps(attributes, props);
27
+ const title = titleProp ? unescapeHTML(`<title>${titleProp}</title>`) : "";
28
+ if (mode === "sprite") {
29
+ let symbol = "";
30
+ if (!rendered.has(result.response)) {
31
+ symbol = unescapeHTML(`<symbol${spreadAttributes({ viewBox, id })}>${children}</symbol>`);
32
+ rendered.add(result.response);
33
+ }
34
+ return render`<svg${spreadAttributes(normalizedProps)}>${title}${symbol}<use href="#${id}" /></svg>`;
35
+ }
36
+ return render`<svg${spreadAttributes({ viewBox, ...normalizedProps })}>${title}${unescapeHTML(children)}</svg>`;
37
+ });
38
+ if (import.meta.env.DEV) {
39
+ makeNonEnumerable(Component);
40
+ Object.defineProperty(Component, Symbol.for("nodejs.util.inspect.custom"), {
41
+ value: (_, opts, inspect) => inspect(meta, opts)
42
+ });
43
+ }
44
+ return Object.assign(Component, meta);
45
+ }
46
+ const ATTRS_TO_DROP = ["xmlns", "xmlns:xlink", "version"];
47
+ const DEFAULT_ATTRS = { role: "img" };
48
+ function dropAttributes(attributes) {
49
+ for (const attr of ATTRS_TO_DROP) {
50
+ delete attributes[attr];
51
+ }
52
+ return attributes;
53
+ }
54
+ function normalizeProps(attributes, { size, ...props }) {
55
+ if (size !== void 0 && props.width === void 0 && props.height === void 0) {
56
+ props.height = size;
57
+ props.width = size;
58
+ }
59
+ return dropAttributes({ ...DEFAULT_ATTRS, ...attributes, ...props });
60
+ }
61
+ function makeNonEnumerable(object) {
62
+ for (const property in object) {
63
+ Object.defineProperty(object, property, { enumerable: false });
64
+ }
65
+ }
66
+ export {
67
+ createSvgComponent,
68
+ dropAttributes
69
+ };
@@ -1,5 +1,5 @@
1
1
  import type { AstroConfig } from '../../types/public/config.js';
2
- import type { ImageOutputFormat, ImageTransform, UnresolvedSrcSetValue } from '../types.js';
2
+ import type { ImageFit, ImageOutputFormat, ImageTransform, UnresolvedSrcSetValue } from '../types.js';
3
3
  export type ImageService = LocalImageService | ExternalImageService;
4
4
  export declare function isLocalService(service: ImageService | undefined): service is LocalImageService;
5
5
  export declare function parseQuality(quality: string): string | number;
@@ -76,6 +76,8 @@ export type BaseServiceTransform = {
76
76
  height?: number;
77
77
  format: string;
78
78
  quality?: string | null;
79
+ fit?: ImageFit;
80
+ position?: string;
79
81
  };
80
82
  /**
81
83
  * Basic local service using the included `_image` endpoint.