astro 2.10.8 → 2.10.10

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 (40) hide show
  1. package/components/ViewTransitions.astro +30 -12
  2. package/dist/@types/astro.d.ts +68 -6
  3. package/dist/assets/{generate.d.ts → build/generate.d.ts} +2 -2
  4. package/dist/assets/build/generate.js +123 -0
  5. package/dist/assets/build/remote.d.ts +9 -0
  6. package/dist/assets/build/remote.js +42 -0
  7. package/dist/assets/image-endpoint.js +7 -8
  8. package/dist/assets/internal.d.ts +4 -2
  9. package/dist/assets/internal.js +23 -6
  10. package/dist/assets/services/service.d.ts +15 -8
  11. package/dist/assets/services/service.js +19 -10
  12. package/dist/assets/utils/remotePattern.d.ts +11 -0
  13. package/dist/assets/utils/remotePattern.js +46 -0
  14. package/dist/assets/utils/transformToPath.js +4 -5
  15. package/dist/assets/vite-plugin-assets.js +2 -6
  16. package/dist/core/app/index.js +21 -9
  17. package/dist/core/build/generate.js +4 -2
  18. package/dist/core/build/static-build.js +14 -4
  19. package/dist/core/config/schema.d.ts +104 -0
  20. package/dist/core/config/schema.js +20 -2
  21. package/dist/core/constants.js +1 -1
  22. package/dist/core/dev/container.js +5 -1
  23. package/dist/core/dev/dev.js +1 -1
  24. package/dist/core/errors/errors-data.js +1 -1
  25. package/dist/core/messages.js +2 -2
  26. package/dist/runtime/server/render/common.d.ts +1 -1
  27. package/dist/runtime/server/render/common.js +13 -15
  28. package/dist/runtime/server/render/component.d.ts +1 -1
  29. package/dist/runtime/server/render/component.js +2 -1
  30. package/dist/runtime/server/render/head.d.ts +1 -1
  31. package/dist/runtime/server/render/head.js +3 -2
  32. package/dist/runtime/server/render/index.d.ts +1 -1
  33. package/dist/runtime/server/render/instruction.d.ts +16 -0
  34. package/dist/runtime/server/render/instruction.js +13 -0
  35. package/dist/runtime/server/render/slot.d.ts +1 -1
  36. package/dist/vite-plugin-integrations-container/index.js +2 -2
  37. package/package.json +3 -1
  38. package/dist/assets/generate.js +0 -90
  39. package/dist/runtime/server/render/types.d.ts +0 -12
  40. package/dist/runtime/server/render/types.js +0 -0
@@ -38,12 +38,21 @@ const { fallback = 'animate' } = Astro.props as Props;
38
38
 
39
39
  const throttle = (cb: (...args: any[]) => any, delay: number) => {
40
40
  let wait = false;
41
+ // During the waiting time additional events are lost.
42
+ // So repeat the callback at the end if we have swallowed events.
43
+ let onceMore = false;
41
44
  return (...args: any[]) => {
42
- if (wait) return;
43
-
45
+ if (wait) {
46
+ onceMore = true;
47
+ return;
48
+ }
44
49
  cb(...args);
45
50
  wait = true;
46
51
  setTimeout(() => {
52
+ if (onceMore) {
53
+ onceMore = false;
54
+ cb(...args);
55
+ }
47
56
  wait = false;
48
57
  }, delay);
49
58
  };
@@ -92,9 +101,8 @@ const { fallback = 'animate' } = Astro.props as Props;
92
101
 
93
102
  const parser = new DOMParser();
94
103
 
95
- async function updateDOM(dir: Direction, html: string, state?: State, fallback?: Fallback) {
104
+ async function updateDOM(html: string, state?: State, fallback?: Fallback) {
96
105
  const doc = parser.parseFromString(html, 'text/html');
97
- doc.documentElement.dataset.astroTransition = dir;
98
106
 
99
107
  // Check for a head element that should persist, either because it has the data
100
108
  // attribute or is a link el.
@@ -125,6 +133,10 @@ const { fallback = 'animate' } = Astro.props as Props;
125
133
  };
126
134
 
127
135
  const swap = () => {
136
+ // noscript tags inside head element are not honored on swap (#7969).
137
+ // Remove them before swapping.
138
+ doc.querySelectorAll('head noscript').forEach((el) => el.remove());
139
+
128
140
  // Swap head
129
141
  for (const el of Array.from(document.head.children)) {
130
142
  const newEl = persistedHeadElement(el);
@@ -159,6 +171,8 @@ const { fallback = 'animate' } = Astro.props as Props;
159
171
  }
160
172
  if (state?.scrollY != null) {
161
173
  scrollTo(0, state.scrollY);
174
+ // Overwrite erroneous updates by the scroll handler during transition
175
+ persistState(state);
162
176
  }
163
177
 
164
178
  triggerEvent('astro:beforeload');
@@ -218,15 +232,17 @@ const { fallback = 'animate' } = Astro.props as Props;
218
232
  location.href = href;
219
233
  return;
220
234
  }
235
+ document.documentElement.dataset.astroTransition = dir;
221
236
  if (supportsViewTransitions) {
222
- finished = document.startViewTransition(() => updateDOM(dir, html, state)).finished;
237
+ finished = document.startViewTransition(() => updateDOM(html, state)).finished;
223
238
  } else {
224
- finished = updateDOM(dir, html, state, getFallback());
239
+ finished = updateDOM(html, state, getFallback());
225
240
  }
226
241
  try {
227
242
  await finished;
228
243
  } finally {
229
- document.documentElement.removeAttribute('data-astro-transition');
244
+ // skip this for the moment as it tends to stop fallback animations
245
+ // document.documentElement.removeAttribute('data-astro-transition');
230
246
  await runScripts();
231
247
  markScriptsExec();
232
248
  onload();
@@ -276,8 +292,7 @@ const { fallback = 'animate' } = Astro.props as Props;
276
292
  transitionEnabledOnThisPage()
277
293
  ) {
278
294
  ev.preventDefault();
279
- navigate('forward', link.href, { index: currentHistoryIndex, scrollY: 0 });
280
- currentHistoryIndex++;
295
+ navigate('forward', link.href, { index: ++currentHistoryIndex, scrollY: 0 });
281
296
  const newState: State = { index: currentHistoryIndex, scrollY };
282
297
  persistState({ index: currentHistoryIndex - 1, scrollY });
283
298
  history.pushState(newState, '', link.href);
@@ -291,10 +306,11 @@ const { fallback = 'animate' } = Astro.props as Props;
291
306
  return;
292
307
  }
293
308
 
294
- // hash change creates no state.
309
+ // History entries without state are created by the browser (e.g. for hash links)
310
+ // Our view transition entries always have state.
311
+ // Just ignore stateless entries.
312
+ // The browser will handle navigation fine without our help
295
313
  if (ev.state === null) {
296
- persistState({ index: currentHistoryIndex, scrollY });
297
- ev.preventDefault();
298
314
  return;
299
315
  }
300
316
 
@@ -329,6 +345,8 @@ const { fallback = 'animate' } = Astro.props as Props;
329
345
  addEventListener(
330
346
  'scroll',
331
347
  throttle(() => {
348
+ // only updste history entries that are managed by us
349
+ // leave other entries alone and do not accidently add state.
332
350
  if (history.state) {
333
351
  persistState({ ...history.state, scrollY });
334
352
  }
@@ -8,6 +8,7 @@ import type { AddressInfo } from 'node:net';
8
8
  import type * as rollup from 'rollup';
9
9
  import type { TsConfigJson } from 'tsconfig-resolver';
10
10
  import type * as vite from 'vite';
11
+ import type { RemotePattern } from '../assets/utils/remotePattern';
11
12
  import type { SerializedSSRManifest } from '../core/app/types';
12
13
  import type { PageBuildData } from '../core/build/types';
13
14
  import type { AstroConfigType } from '../core/config';
@@ -19,6 +20,7 @@ import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js'
19
20
  export type { MarkdownHeading, MarkdownMetadata, MarkdownRenderingResult, RehypePlugins, RemarkPlugins, ShikiConfig, } from '@astrojs/markdown-remark';
20
21
  export type { ExternalImageService, ImageService, LocalImageService, } from '../assets/services/service';
21
22
  export type { GetImageResult, ImageInputFormat, ImageMetadata, ImageOutputFormat, ImageQuality, ImageQualityPreset, ImageTransform, } from '../assets/types';
23
+ export type { RemotePattern } from '../assets/utils/remotePattern';
22
24
  export type { SSRManifest } from '../core/app/types';
23
25
  export type { AstroCookies } from '../core/cookies';
24
26
  export interface AstroBuiltinProps {
@@ -312,9 +314,9 @@ type ServerConfig = {
312
314
  export interface ViteUserConfig extends vite.UserConfig {
313
315
  ssr?: vite.SSROptions;
314
316
  }
315
- export interface ImageServiceConfig {
317
+ export interface ImageServiceConfig<T extends Record<string, any> = Record<string, any>> {
316
318
  entrypoint: 'astro/assets/services/sharp' | 'astro/assets/services/squoosh' | (string & {});
317
- config?: Record<string, any>;
319
+ config?: T;
318
320
  }
319
321
  /**
320
322
  * Astro User Config
@@ -769,10 +771,10 @@ export interface AstroUserConfig {
769
771
  * @default `never`
770
772
  * @version 2.6.0
771
773
  * @description
772
- * Control whether styles are sent to the browser in a separate css file or inlined into `<style>` tags. Choose from the following options:
773
- * - `'always'` - all styles are inlined into `<style>` tags
774
- * - `'auto'` - only stylesheets smaller than `ViteConfig.build.assetsInlineLimit` (default: 4kb) are inlined. Otherwise, styles are sent in external stylesheets.
775
- * - `'never'` - all styles are sent in external stylesheets
774
+ * Control whether project styles are sent to the browser in a separate css file or inlined into `<style>` tags. Choose from the following options:
775
+ * - `'always'` - project styles are inlined into `<style>` tags
776
+ * - `'auto'` - only stylesheets smaller than `ViteConfig.build.assetsInlineLimit` (default: 4kb) are inlined. Otherwise, project styles are sent in external stylesheets.
777
+ * - `'never'` - project styles are sent in external stylesheets
776
778
  *
777
779
  * ```js
778
780
  * {
@@ -933,6 +935,66 @@ export interface AstroUserConfig {
933
935
  * ```
934
936
  */
935
937
  service: ImageServiceConfig;
938
+ /**
939
+ * @docs
940
+ * @name image.domains (Experimental)
941
+ * @type {string[]}
942
+ * @default `{domains: []}`
943
+ * @version 2.10.10
944
+ * @description
945
+ * Defines a list of permitted image source domains for local image optimization. No other remote images will be optimized by Astro.
946
+ *
947
+ * This option requires an array of individual domain names as strings. Wildcards are not permitted. Instead, use [`image.remotePatterns`](#imageremotepatterns-experimental) to define a list of allowed source URL patterns.
948
+ *
949
+ * ```js
950
+ * // astro.config.mjs
951
+ * {
952
+ * image: {
953
+ * // Example: Allow remote image optimization from a single domain
954
+ * domains: ['astro.build'],
955
+ * },
956
+ * }
957
+ * ```
958
+ */
959
+ domains?: string[];
960
+ /**
961
+ * @docs
962
+ * @name image.remotePatterns (Experimental)
963
+ * @type {RemotePattern[]}
964
+ * @default `{remotePatterns: []}`
965
+ * @version 2.10.10
966
+ * @description
967
+ * Defines a list of permitted image source URL patterns for local image optimization.
968
+ *
969
+ * `remotePatterns` can be configured with four properties:
970
+ * 1. protocol
971
+ * 2. hostname
972
+ * 3. port
973
+ * 4. pathname
974
+ *
975
+ * ```js
976
+ * {
977
+ * image: {
978
+ * // Example: allow processing all images from your aws s3 bucket
979
+ * remotePatterns: [{
980
+ * protocol: 'https',
981
+ * hostname: '**.amazonaws.com',
982
+ * }],
983
+ * },
984
+ * }
985
+ * ```
986
+ *
987
+ * You can use wildcards to define the permitted `hostname` and `pathname` values as described below. Otherwise, only the exact values provided will be configured:
988
+ * `hostname`:
989
+ * - Start with '**.' to allow all subdomains ('endsWith').
990
+ * - Start with '*.' to allow only one level of subdomain.
991
+ *
992
+ * `pathname`:
993
+ * - End with '/**' to allow all sub-routes ('startsWith').
994
+ * - End with '/*' to allow only one level of sub-route.
995
+
996
+ */
997
+ remotePatterns?: Partial<RemotePattern>[];
936
998
  };
937
999
  /**
938
1000
  * @docs
@@ -1,5 +1,5 @@
1
- import type { StaticBuildOptions } from '../core/build/types.js';
2
- import type { ImageTransform } from './types.js';
1
+ import type { StaticBuildOptions } from '../../core/build/types.js';
2
+ import type { ImageTransform } from '../types.js';
3
3
  interface GenerationDataUncached {
4
4
  cached: false;
5
5
  weight: {
@@ -0,0 +1,123 @@
1
+ import fs, { readFileSync } from "node:fs";
2
+ import { basename, join } from "node:path/posix";
3
+ import { warn } from "../../core/logger/core.js";
4
+ import { prependForwardSlash } from "../../core/path.js";
5
+ import { isServerLikeOutput } from "../../prerender/utils.js";
6
+ import { getConfiguredImageService, isESMImportedImage } from "../internal.js";
7
+ import { loadRemoteImage } from "./remote.js";
8
+ async function generateImage(buildOpts, options, filepath) {
9
+ let useCache = true;
10
+ const assetsCacheDir = new URL("assets/", buildOpts.settings.config.cacheDir);
11
+ try {
12
+ await fs.promises.mkdir(assetsCacheDir, { recursive: true });
13
+ } catch (err) {
14
+ warn(
15
+ buildOpts.logging,
16
+ "astro:assets",
17
+ `An error was encountered while creating the cache directory. Proceeding without caching. Error: ${err}`
18
+ );
19
+ useCache = false;
20
+ }
21
+ let serverRoot, clientRoot;
22
+ if (isServerLikeOutput(buildOpts.settings.config)) {
23
+ serverRoot = buildOpts.settings.config.build.server;
24
+ clientRoot = buildOpts.settings.config.build.client;
25
+ } else {
26
+ serverRoot = buildOpts.settings.config.outDir;
27
+ clientRoot = buildOpts.settings.config.outDir;
28
+ }
29
+ const isLocalImage = isESMImportedImage(options.src);
30
+ const finalFileURL = new URL("." + filepath, clientRoot);
31
+ const finalFolderURL = new URL("./", finalFileURL);
32
+ const cacheFile = basename(filepath) + (isLocalImage ? "" : ".json");
33
+ const cachedFileURL = new URL(cacheFile, assetsCacheDir);
34
+ await fs.promises.mkdir(finalFolderURL, { recursive: true });
35
+ try {
36
+ if (isLocalImage) {
37
+ await fs.promises.copyFile(cachedFileURL, finalFileURL);
38
+ return {
39
+ cached: true
40
+ };
41
+ } else {
42
+ const JSONData = JSON.parse(readFileSync(cachedFileURL, "utf-8"));
43
+ if (JSONData.expires < Date.now()) {
44
+ await fs.promises.writeFile(finalFileURL, Buffer.from(JSONData.data, "base64"));
45
+ return {
46
+ cached: true
47
+ };
48
+ }
49
+ }
50
+ } catch (e) {
51
+ if (e.code !== "ENOENT") {
52
+ throw new Error(`An error was encountered while reading the cache file. Error: ${e}`);
53
+ }
54
+ }
55
+ const originalImagePath = isLocalImage ? options.src.src : options.src;
56
+ let imageData;
57
+ let resultData = {
58
+ data: void 0,
59
+ expires: void 0
60
+ };
61
+ if (isLocalImage) {
62
+ imageData = await fs.promises.readFile(
63
+ new URL(
64
+ "." + prependForwardSlash(
65
+ join(buildOpts.settings.config.build.assets, basename(originalImagePath))
66
+ ),
67
+ serverRoot
68
+ )
69
+ );
70
+ } else {
71
+ const remoteImage = await loadRemoteImage(originalImagePath);
72
+ resultData.expires = remoteImage.expires;
73
+ imageData = remoteImage.data;
74
+ }
75
+ const imageService = await getConfiguredImageService();
76
+ resultData.data = (await imageService.transform(
77
+ imageData,
78
+ { ...options, src: originalImagePath },
79
+ buildOpts.settings.config.image
80
+ )).data;
81
+ try {
82
+ if (useCache) {
83
+ if (isLocalImage) {
84
+ await fs.promises.writeFile(cachedFileURL, resultData.data);
85
+ } else {
86
+ await fs.promises.writeFile(
87
+ cachedFileURL,
88
+ JSON.stringify({
89
+ data: Buffer.from(resultData.data).toString("base64"),
90
+ expires: resultData.expires
91
+ })
92
+ );
93
+ }
94
+ }
95
+ } catch (e) {
96
+ warn(
97
+ buildOpts.logging,
98
+ "astro:assets",
99
+ `An error was encountered while creating the cache directory. Proceeding without caching. Error: ${e}`
100
+ );
101
+ } finally {
102
+ await fs.promises.writeFile(finalFileURL, resultData.data);
103
+ }
104
+ return {
105
+ cached: false,
106
+ weight: {
107
+ // Divide by 1024 to get size in kilobytes
108
+ before: Math.trunc(imageData.byteLength / 1024),
109
+ after: Math.trunc(Buffer.from(resultData.data).byteLength / 1024)
110
+ }
111
+ };
112
+ }
113
+ function getStaticImageList() {
114
+ var _a, _b;
115
+ if (!((_a = globalThis == null ? void 0 : globalThis.astroAsset) == null ? void 0 : _a.staticImages)) {
116
+ return [];
117
+ }
118
+ return (_b = globalThis.astroAsset.staticImages) == null ? void 0 : _b.entries();
119
+ }
120
+ export {
121
+ generateImage,
122
+ getStaticImageList
123
+ };
@@ -0,0 +1,9 @@
1
+ /// <reference types="node" />
2
+ export type RemoteCacheEntry = {
3
+ data: string;
4
+ expires: number;
5
+ };
6
+ export declare function loadRemoteImage(src: string): Promise<{
7
+ data: Buffer;
8
+ expires: number;
9
+ }>;
@@ -0,0 +1,42 @@
1
+ import CachePolicy from "http-cache-semantics";
2
+ async function loadRemoteImage(src) {
3
+ const req = new Request(src);
4
+ const res = await fetch(req);
5
+ if (!res.ok) {
6
+ throw new Error(
7
+ `Failed to load remote image ${src}. The request did not return a 200 OK response. (received ${res.status}))`
8
+ );
9
+ }
10
+ const policy = new CachePolicy(webToCachePolicyRequest(req), webToCachePolicyResponse(res));
11
+ const expires = policy.storable() ? policy.timeToLive() : 0;
12
+ return {
13
+ data: Buffer.from(await res.arrayBuffer()),
14
+ expires: Date.now() + expires
15
+ };
16
+ }
17
+ function webToCachePolicyRequest({ url, method, headers: _headers }) {
18
+ let headers = {};
19
+ try {
20
+ headers = Object.fromEntries(_headers.entries());
21
+ } catch {
22
+ }
23
+ return {
24
+ method,
25
+ url,
26
+ headers
27
+ };
28
+ }
29
+ function webToCachePolicyResponse({ status, headers: _headers }) {
30
+ let headers = {};
31
+ try {
32
+ headers = Object.fromEntries(_headers.entries());
33
+ } catch {
34
+ }
35
+ return {
36
+ status,
37
+ headers
38
+ };
39
+ }
40
+ export {
41
+ loadRemoteImage
42
+ };
@@ -1,9 +1,9 @@
1
1
  import mime from "mime/lite.js";
2
2
  import { isRemotePath } from "../core/path.js";
3
- import { getConfiguredImageService } from "./internal.js";
3
+ import { getConfiguredImageService, isRemoteAllowed } from "./internal.js";
4
4
  import { isLocalService } from "./services/service.js";
5
5
  import { etag } from "./utils/etag.js";
6
- import { imageServiceConfig } from "astro:assets";
6
+ import { imageConfig } from "astro:assets";
7
7
  async function loadRemoteImage(src) {
8
8
  try {
9
9
  const res = await fetch(src);
@@ -22,21 +22,20 @@ const get = async ({ request }) => {
22
22
  throw new Error("Configured image service is not a local service");
23
23
  }
24
24
  const url = new URL(request.url);
25
- const transform = await imageService.parseURL(url, imageServiceConfig);
25
+ const transform = await imageService.parseURL(url, imageConfig);
26
26
  if (!(transform == null ? void 0 : transform.src)) {
27
27
  throw new Error("Incorrect transform returned by `parseURL`");
28
28
  }
29
29
  let inputBuffer = void 0;
30
30
  const sourceUrl = isRemotePath(transform.src) ? new URL(transform.src) : new URL(transform.src, url.origin);
31
+ if (isRemotePath(transform.src) && isRemoteAllowed(transform.src, imageConfig) === false) {
32
+ return new Response("Forbidden", { status: 403 });
33
+ }
31
34
  inputBuffer = await loadRemoteImage(sourceUrl);
32
35
  if (!inputBuffer) {
33
36
  return new Response("Not Found", { status: 404 });
34
37
  }
35
- const { data, format } = await imageService.transform(
36
- inputBuffer,
37
- transform,
38
- imageServiceConfig
39
- );
38
+ const { data, format } = await imageService.transform(inputBuffer, transform, imageConfig);
40
39
  return new Response(data, {
41
40
  status: 200,
42
41
  headers: {
@@ -1,7 +1,9 @@
1
- import type { AstroSettings } from '../@types/astro.js';
1
+ import type { AstroConfig, AstroSettings } from '../@types/astro.js';
2
2
  import { type ImageService } from './services/service.js';
3
3
  import type { GetImageResult, ImageMetadata, ImageTransform, UnresolvedImageTransform } from './types.js';
4
4
  export declare function injectImageEndpoint(settings: AstroSettings): AstroSettings;
5
5
  export declare function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata;
6
+ export declare function isRemoteImage(src: ImageMetadata | string): src is string;
7
+ export declare function isRemoteAllowed(src: string, { domains, remotePatterns, }: Partial<Pick<AstroConfig['image'], 'domains' | 'remotePatterns'>>): boolean;
6
8
  export declare function getConfiguredImageService(): Promise<ImageService>;
7
- export declare function getImage(options: ImageTransform | UnresolvedImageTransform, serviceConfig: Record<string, any>): Promise<GetImageResult>;
9
+ export declare function getImage(options: ImageTransform | UnresolvedImageTransform, imageConfig: AstroConfig['image']): Promise<GetImageResult>;
@@ -1,5 +1,7 @@
1
+ import { isRemotePath } from "@astrojs/internal-helpers/path";
1
2
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
2
3
  import { isLocalService } from "./services/service.js";
4
+ import { matchHostname, matchPattern } from "./utils/remotePattern.js";
3
5
  function injectImageEndpoint(settings) {
4
6
  settings.injectedRoutes.push({
5
7
  pattern: "/_image",
@@ -11,6 +13,18 @@ function injectImageEndpoint(settings) {
11
13
  function isESMImportedImage(src) {
12
14
  return typeof src === "object";
13
15
  }
16
+ function isRemoteImage(src) {
17
+ return typeof src === "string";
18
+ }
19
+ function isRemoteAllowed(src, {
20
+ domains = [],
21
+ remotePatterns = []
22
+ }) {
23
+ if (!isRemotePath(src))
24
+ return false;
25
+ const url = new URL(src);
26
+ return domains.some((domain) => matchHostname(url, domain)) || remotePatterns.some((remotePattern) => matchPattern(url, remotePattern));
27
+ }
14
28
  async function getConfiguredImageService() {
15
29
  var _a;
16
30
  if (!((_a = globalThis == null ? void 0 : globalThis.astroAsset) == null ? void 0 : _a.imageService)) {
@@ -29,7 +43,7 @@ async function getConfiguredImageService() {
29
43
  }
30
44
  return globalThis.astroAsset.imageService;
31
45
  }
32
- async function getImage(options, serviceConfig) {
46
+ async function getImage(options, imageConfig) {
33
47
  if (!options || typeof options !== "object") {
34
48
  throw new AstroError({
35
49
  ...AstroErrorData.ExpectedImageOptions,
@@ -41,21 +55,24 @@ async function getImage(options, serviceConfig) {
41
55
  ...options,
42
56
  src: typeof options.src === "object" && "then" in options.src ? (await options.src).default : options.src
43
57
  };
44
- const validatedOptions = service.validateOptions ? await service.validateOptions(resolvedOptions, serviceConfig) : resolvedOptions;
45
- let imageURL = await service.getURL(validatedOptions, serviceConfig);
46
- if (isLocalService(service) && globalThis.astroAsset.addStaticImage) {
58
+ const validatedOptions = service.validateOptions ? await service.validateOptions(resolvedOptions, imageConfig) : resolvedOptions;
59
+ 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)) {
47
62
  imageURL = globalThis.astroAsset.addStaticImage(validatedOptions);
48
63
  }
49
64
  return {
50
65
  rawOptions: resolvedOptions,
51
66
  options: validatedOptions,
52
67
  src: imageURL,
53
- attributes: service.getHTMLAttributes !== void 0 ? service.getHTMLAttributes(validatedOptions, serviceConfig) : {}
68
+ attributes: service.getHTMLAttributes !== void 0 ? service.getHTMLAttributes(validatedOptions, imageConfig) : {}
54
69
  };
55
70
  }
56
71
  export {
57
72
  getConfiguredImageService,
58
73
  getImage,
59
74
  injectImageEndpoint,
60
- isESMImportedImage
75
+ isESMImportedImage,
76
+ isRemoteAllowed,
77
+ isRemoteImage
61
78
  };
@@ -1,9 +1,16 @@
1
1
  /// <reference types="node" />
2
+ import type { AstroConfig } from '../../@types/astro.js';
2
3
  import type { ImageOutputFormat, ImageTransform } from '../types.js';
3
4
  export type ImageService = LocalImageService | ExternalImageService;
4
5
  export declare function isLocalService(service: ImageService | undefined): service is LocalImageService;
5
6
  export declare function parseQuality(quality: string): string | number;
6
- interface SharedServiceProps {
7
+ type ImageConfig<T> = Omit<AstroConfig['image'], 'service'> & {
8
+ service: {
9
+ entrypoint: string;
10
+ config: T;
11
+ };
12
+ };
13
+ interface SharedServiceProps<T extends Record<string, any> = Record<string, any>> {
7
14
  /**
8
15
  * Return the URL to the endpoint or URL your images are generated from.
9
16
  *
@@ -12,14 +19,14 @@ interface SharedServiceProps {
12
19
  * For external services, this should point to the URL your images are coming from, for instance, `/_vercel/image`
13
20
  *
14
21
  */
15
- getURL: (options: ImageTransform, serviceConfig: Record<string, any>) => string | Promise<string>;
22
+ getURL: (options: ImageTransform, imageConfig: ImageConfig<T>) => string | Promise<string>;
16
23
  /**
17
24
  * Return any additional HTML attributes separate from `src` that your service requires to show the image properly.
18
25
  *
19
26
  * For example, you might want to return the `width` and `height` to avoid CLS, or a particular `class` or `style`.
20
27
  * In most cases, you'll want to return directly what your user supplied you, minus the attributes that were used to generate the image.
21
28
  */
22
- getHTMLAttributes?: (options: ImageTransform, serviceConfig: Record<string, any>) => Record<string, any> | Promise<Record<string, any>>;
29
+ getHTMLAttributes?: (options: ImageTransform, imageConfig: ImageConfig<T>) => Record<string, any> | Promise<Record<string, any>>;
23
30
  /**
24
31
  * Validate and return the options passed by the user.
25
32
  *
@@ -28,25 +35,25 @@ interface SharedServiceProps {
28
35
  *
29
36
  * This method should returns options, and can be used to set defaults (ex: a default output format to be used if the user didn't specify one.)
30
37
  */
31
- validateOptions?: (options: ImageTransform, serviceConfig: Record<string, any>) => ImageTransform | Promise<ImageTransform>;
38
+ validateOptions?: (options: ImageTransform, imageConfig: ImageConfig<T>) => ImageTransform | Promise<ImageTransform>;
32
39
  }
33
- export type ExternalImageService = SharedServiceProps;
40
+ export type ExternalImageService<T extends Record<string, any> = Record<string, any>> = SharedServiceProps<T>;
34
41
  export type LocalImageTransform = {
35
42
  src: string;
36
43
  [key: string]: any;
37
44
  };
38
- export interface LocalImageService extends SharedServiceProps {
45
+ export interface LocalImageService<T extends Record<string, any> = Record<string, any>> extends SharedServiceProps<T> {
39
46
  /**
40
47
  * Parse the requested parameters passed in the URL from `getURL` back into an object to be used later by `transform`.
41
48
  *
42
49
  * In most cases, this will get query parameters using, for example, `params.get('width')` and return those.
43
50
  */
44
- parseURL: (url: URL, serviceConfig: Record<string, any>) => LocalImageTransform | undefined | Promise<LocalImageTransform> | Promise<undefined>;
51
+ parseURL: (url: URL, imageConfig: ImageConfig<T>) => LocalImageTransform | undefined | Promise<LocalImageTransform> | Promise<undefined>;
45
52
  /**
46
53
  * Performs the image transformations on the input image and returns both the binary data and
47
54
  * final image format of the optimized image.
48
55
  */
49
- transform: (inputBuffer: Buffer, transform: LocalImageTransform, serviceConfig: Record<string, any>) => Promise<{
56
+ transform: (inputBuffer: Buffer, transform: LocalImageTransform, imageConfig: ImageConfig<T>) => Promise<{
50
57
  data: Buffer;
51
58
  format: ImageOutputFormat;
52
59
  }>;
@@ -1,7 +1,7 @@
1
1
  import { AstroError, AstroErrorData } from "../../core/errors/index.js";
2
2
  import { joinPaths } from "../../core/path.js";
3
3
  import { VALID_SUPPORTED_FORMATS } from "../consts.js";
4
- import { isESMImportedImage } from "../internal.js";
4
+ import { isESMImportedImage, isRemoteAllowed } from "../internal.js";
5
5
  function isLocalService(service) {
6
6
  if (!service) {
7
7
  return false;
@@ -87,17 +87,26 @@ const baseService = {
87
87
  decoding: attributes.decoding ?? "async"
88
88
  };
89
89
  },
90
- getURL(options) {
91
- if (!isESMImportedImage(options.src)) {
90
+ getURL(options, imageConfig) {
91
+ const searchParams = new URLSearchParams();
92
+ if (isESMImportedImage(options.src)) {
93
+ searchParams.append("href", options.src.src);
94
+ } else if (isRemoteAllowed(options.src, imageConfig)) {
95
+ searchParams.append("href", options.src);
96
+ } else {
92
97
  return options.src;
93
98
  }
94
- const searchParams = new URLSearchParams();
95
- searchParams.append("href", options.src.src);
96
- options.width && searchParams.append("w", options.width.toString());
97
- options.height && searchParams.append("h", options.height.toString());
98
- options.quality && searchParams.append("q", options.quality.toString());
99
- options.format && searchParams.append("f", options.format);
100
- return joinPaths(import.meta.env.BASE_URL, "/_image?") + searchParams;
99
+ const params = {
100
+ w: "width",
101
+ h: "height",
102
+ q: "quality",
103
+ f: "format"
104
+ };
105
+ Object.entries(params).forEach(([param, key]) => {
106
+ options[key] && searchParams.append(param, options[key].toString());
107
+ });
108
+ const imageEndpoint = joinPaths(import.meta.env.BASE_URL, "/_image");
109
+ return `${imageEndpoint}?${searchParams}`;
101
110
  },
102
111
  parseURL(url) {
103
112
  const params = url.searchParams;
@@ -0,0 +1,11 @@
1
+ export type RemotePattern = {
2
+ hostname?: string;
3
+ pathname?: string;
4
+ protocol?: string;
5
+ port?: string;
6
+ };
7
+ export declare function matchPattern(url: URL, remotePattern: RemotePattern): boolean;
8
+ export declare function matchPort(url: URL, port?: string): boolean;
9
+ export declare function matchProtocol(url: URL, protocol?: string): boolean;
10
+ export declare function matchHostname(url: URL, hostname?: string, allowWildcard?: boolean): boolean;
11
+ export declare function matchPathname(url: URL, pathname?: string, allowWildcard?: boolean): boolean;