astro 6.2.2 → 6.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 (113) hide show
  1. package/dist/actions/handler.d.ts +32 -0
  2. package/dist/actions/handler.js +45 -0
  3. package/dist/actions/runtime/server.js +1 -1
  4. package/dist/assets/build/generate.js +1 -1
  5. package/dist/assets/build/remote.d.ts +3 -2
  6. package/dist/assets/build/remote.js +16 -9
  7. package/dist/assets/endpoint/generic.js +8 -20
  8. package/dist/assets/endpoint/loadImage.d.ts +11 -0
  9. package/dist/assets/endpoint/loadImage.js +19 -0
  10. package/dist/assets/endpoint/shared.js +7 -2
  11. package/dist/assets/index.d.ts +1 -0
  12. package/dist/assets/index.js +2 -0
  13. package/dist/assets/services/sharp.js +7 -0
  14. package/dist/assets/utils/index.d.ts +1 -0
  15. package/dist/assets/utils/index.js +2 -0
  16. package/dist/assets/utils/redirectValidation.d.ts +48 -0
  17. package/dist/assets/utils/redirectValidation.js +48 -0
  18. package/dist/assets/utils/remoteProbe.js +25 -2
  19. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  20. package/dist/container/index.js +18 -14
  21. package/dist/content/content-layer.js +3 -4
  22. package/dist/content/server-listeners.js +0 -4
  23. package/dist/content/vite-plugin-content-virtual-mod.js +9 -1
  24. package/dist/core/app/base.d.ts +33 -15
  25. package/dist/core/app/base.js +120 -324
  26. package/dist/core/app/dev/app.d.ts +3 -2
  27. package/dist/core/app/dev/app.js +4 -60
  28. package/dist/core/app/entrypoints/virtual/dev.js +2 -0
  29. package/dist/core/app/entrypoints/virtual/prod.js +4 -1
  30. package/dist/core/app/prepare-response.d.ts +11 -0
  31. package/dist/core/app/prepare-response.js +18 -0
  32. package/dist/core/app/render-options.d.ts +11 -0
  33. package/dist/core/app/render-options.js +11 -0
  34. package/dist/core/base-pipeline.d.ts +38 -1
  35. package/dist/core/base-pipeline.js +50 -7
  36. package/dist/core/build/app.d.ts +3 -4
  37. package/dist/core/build/app.js +3 -17
  38. package/dist/core/cache/handler.d.ts +29 -0
  39. package/dist/core/cache/handler.js +81 -0
  40. package/dist/core/config/schemas/base.d.ts +4 -0
  41. package/dist/core/config/schemas/base.js +4 -0
  42. package/dist/core/config/schemas/relative.d.ts +6 -0
  43. package/dist/core/constants.d.ts +27 -1
  44. package/dist/core/constants.js +14 -1
  45. package/dist/core/cookies/cookies.d.ts +7 -2
  46. package/dist/core/cookies/cookies.js +11 -4
  47. package/dist/core/cookies/response.d.ts +1 -1
  48. package/dist/core/cookies/response.js +1 -2
  49. package/dist/core/create-vite.js +15 -0
  50. package/dist/core/csp/runtime.js +6 -4
  51. package/dist/core/dev/dev.js +1 -1
  52. package/dist/core/errors/build-handler.d.ts +17 -0
  53. package/dist/core/errors/build-handler.js +22 -0
  54. package/dist/core/errors/default-handler.d.ts +14 -0
  55. package/dist/core/errors/default-handler.js +144 -0
  56. package/dist/core/errors/dev-handler.d.ts +21 -0
  57. package/dist/core/errors/dev-handler.js +82 -0
  58. package/dist/core/errors/handler.d.ts +9 -0
  59. package/dist/core/errors/handler.js +0 -0
  60. package/dist/core/fetch/default-handler.d.ts +17 -0
  61. package/dist/core/fetch/default-handler.js +45 -0
  62. package/dist/core/fetch/fetch-state.d.ts +244 -0
  63. package/dist/core/fetch/fetch-state.js +779 -0
  64. package/dist/core/fetch/index.d.ts +61 -0
  65. package/dist/core/fetch/index.js +121 -0
  66. package/dist/core/fetch/types.d.ts +6 -0
  67. package/dist/core/fetch/types.js +0 -0
  68. package/dist/core/fetch/vite-plugin.d.ts +5 -0
  69. package/dist/core/fetch/vite-plugin.js +69 -0
  70. package/dist/core/hono/index.d.ts +21 -0
  71. package/dist/core/hono/index.js +98 -0
  72. package/dist/core/i18n/handler.d.ts +18 -0
  73. package/dist/core/i18n/handler.js +119 -0
  74. package/dist/core/logger/core.d.ts +8 -0
  75. package/dist/core/logger/core.js +16 -0
  76. package/dist/core/messages/runtime.js +1 -1
  77. package/dist/core/middleware/astro-middleware.d.ts +27 -0
  78. package/dist/core/middleware/astro-middleware.js +53 -0
  79. package/dist/core/pages/handler.d.ts +20 -0
  80. package/dist/core/pages/handler.js +74 -0
  81. package/dist/core/preview/static-preview-server.js +3 -1
  82. package/dist/core/redirects/render.d.ts +2 -2
  83. package/dist/core/redirects/render.js +7 -8
  84. package/dist/core/rewrites/handler.d.ts +37 -0
  85. package/dist/core/rewrites/handler.js +67 -0
  86. package/dist/core/routing/3xx.js +8 -4
  87. package/dist/core/routing/handler.d.ts +17 -0
  88. package/dist/core/routing/handler.js +172 -0
  89. package/dist/core/routing/match.d.ts +0 -7
  90. package/dist/core/routing/match.js +0 -5
  91. package/dist/core/routing/trailing-slash-handler.d.ts +18 -0
  92. package/dist/core/routing/trailing-slash-handler.js +67 -0
  93. package/dist/core/session/drivers.d.ts +1 -1
  94. package/dist/core/session/handler.d.ts +11 -0
  95. package/dist/core/session/handler.js +33 -0
  96. package/dist/core/util/normalized-url.d.ts +10 -0
  97. package/dist/core/util/normalized-url.js +21 -0
  98. package/dist/i18n/middleware.d.ts +10 -0
  99. package/dist/i18n/middleware.js +4 -88
  100. package/dist/i18n/utils.js +2 -2
  101. package/dist/runtime/server/astro-island.js +57 -20
  102. package/dist/runtime/server/astro-island.prebuilt-dev.d.ts +1 -1
  103. package/dist/runtime/server/astro-island.prebuilt-dev.js +1 -1
  104. package/dist/runtime/server/astro-island.prebuilt.d.ts +1 -1
  105. package/dist/runtime/server/astro-island.prebuilt.js +1 -1
  106. package/dist/runtime/server/render/server-islands.js +2 -1
  107. package/dist/types/public/config.d.ts +46 -12
  108. package/dist/types/public/internal.d.ts +1 -1
  109. package/dist/vite-plugin-app/app.d.ts +4 -5
  110. package/dist/vite-plugin-app/app.js +20 -65
  111. package/package.json +11 -7
  112. package/dist/core/render-context.d.ts +0 -77
  113. package/dist/core/render-context.js +0 -826
@@ -0,0 +1,32 @@
1
+ import type { APIContext } from '../types/public/context.js';
2
+ import type { FetchState } from '../core/fetch/fetch-state.js';
3
+ /**
4
+ * Handles Astro Action requests in both modes:
5
+ *
6
+ * - **RPC**: POST requests to `/_actions/<name>` (originating from the
7
+ * generated JS client). Runs the action and returns the serialized result
8
+ * as the response, so the caller can short-circuit rendering.
9
+ * - **Form**: POST requests with `?_action=<name>` targeting a page route
10
+ * (originating from an HTML `<form action={actions.foo}>`). Runs the
11
+ * action, stashes the result into `locals._actionPayload`, and returns
12
+ * `undefined` so the caller continues to render the page.
13
+ *
14
+ * Non-action requests are a no-op (`undefined`).
15
+ *
16
+ * This handler is invoked at the bottom of the middleware chain, before
17
+ * page dispatch. That placement preserves the existing behavior where
18
+ * user middleware sees action requests and response finalization (cookies,
19
+ * sessions, etc.) runs around the action response.
20
+ */
21
+ export declare class ActionHandler {
22
+ #private;
23
+ /**
24
+ * Run action handling for the current request. Expects the APIContext
25
+ * that is already being used by the render pipeline.
26
+ *
27
+ * Returns a `Response` when the action fully handles the request (RPC),
28
+ * or `undefined` when the caller should continue processing the
29
+ * request (form actions or non-action requests).
30
+ */
31
+ handle(apiContext: APIContext, state: FetchState): Promise<Response | undefined> | undefined;
32
+ }
@@ -0,0 +1,45 @@
1
+ import { PipelineFeatures } from "../core/base-pipeline.js";
2
+ import { getActionContext, serializeActionResult } from "./runtime/server.js";
3
+ class ActionHandler {
4
+ /**
5
+ * Run action handling for the current request. Expects the APIContext
6
+ * that is already being used by the render pipeline.
7
+ *
8
+ * Returns a `Response` when the action fully handles the request (RPC),
9
+ * or `undefined` when the caller should continue processing the
10
+ * request (form actions or non-action requests).
11
+ */
12
+ handle(apiContext, state) {
13
+ state.pipeline.usedFeatures |= PipelineFeatures.actions;
14
+ if (apiContext.isPrerendered) {
15
+ return void 0;
16
+ }
17
+ const { action, setActionResult } = getActionContext(apiContext);
18
+ if (!action) {
19
+ return void 0;
20
+ }
21
+ return this.#executeAction(action, setActionResult);
22
+ }
23
+ async #executeAction(action, setActionResult) {
24
+ const actionResult = await action.handler();
25
+ const serialized = serializeActionResult(actionResult);
26
+ if (action.calledFrom === "rpc") {
27
+ if (serialized.type === "empty") {
28
+ return new Response(null, {
29
+ status: serialized.status
30
+ });
31
+ }
32
+ return new Response(serialized.body, {
33
+ status: serialized.status,
34
+ headers: {
35
+ "Content-Type": serialized.contentType
36
+ }
37
+ });
38
+ }
39
+ setActionResult(action.name, serialized);
40
+ return void 0;
41
+ }
42
+ }
43
+ export {
44
+ ActionHandler
45
+ };
@@ -266,7 +266,7 @@ function handleFormDataGetAll(key, formData, validator) {
266
266
  if (elementValidator instanceof z.$ZodNumber) {
267
267
  return entries.map(Number);
268
268
  } else if (elementValidator instanceof z.$ZodBoolean) {
269
- return entries.map(Boolean);
269
+ return entries.map((v) => v === "true" ? true : v === "false" ? false : Boolean(v));
270
270
  }
271
271
  return entries;
272
272
  }
@@ -234,7 +234,7 @@ function getStaticImageList() {
234
234
  }
235
235
  async function loadImage(path, env) {
236
236
  if (isRemotePath(path)) {
237
- return await loadRemoteImage(path);
237
+ return await loadRemoteImage(path, void 0, env.imageConfig);
238
238
  }
239
239
  return {
240
240
  data: await fs.promises.readFile(getFullImagePath(path, env)),
@@ -1,10 +1,11 @@
1
+ import { type RemoteImageConfig } from '../utils/redirectValidation.js';
1
2
  export type RemoteCacheEntry = {
2
3
  data?: string;
3
4
  expires: number;
4
5
  etag?: string;
5
6
  lastModified?: string;
6
7
  };
7
- export declare function loadRemoteImage(src: string, fetchFn?: typeof fetch): Promise<{
8
+ export declare function loadRemoteImage(src: string, fetchFn?: typeof fetch, imageConfig?: RemoteImageConfig): Promise<{
8
9
  data: Buffer<ArrayBuffer>;
9
10
  expires: number;
10
11
  etag: string | undefined;
@@ -22,7 +23,7 @@ export declare function loadRemoteImage(src: string, fetchFn?: typeof fetch): Pr
22
23
  export declare function revalidateRemoteImage(src: string, revalidationData: {
23
24
  etag?: string;
24
25
  lastModified?: string;
25
- }, fetchFn?: typeof fetch): Promise<{
26
+ }, fetchFn?: typeof fetch, imageConfig?: RemoteImageConfig): Promise<{
26
27
  data: Buffer<ArrayBuffer> | null;
27
28
  expires: number;
28
29
  etag: string | undefined;
@@ -1,15 +1,17 @@
1
1
  import CachePolicy from "http-cache-semantics";
2
- async function loadRemoteImage(src, fetchFn = globalThis.fetch) {
3
- const req = new Request(src);
4
- const res = await fetchFn(req, { redirect: "manual" });
5
- if (res.status >= 300 && res.status < 400) {
6
- throw new Error(`Failed to load remote image ${src}. The request was redirected.`);
7
- }
2
+ import { fetchWithRedirects } from "../utils/redirectValidation.js";
3
+ async function loadRemoteImage(src, fetchFn = globalThis.fetch, imageConfig = { remotePatterns: [], domains: [] }) {
4
+ const res = await fetchWithRedirects({
5
+ url: src,
6
+ fetchFn,
7
+ imageConfig
8
+ });
8
9
  if (!res.ok) {
9
10
  throw new Error(
10
11
  `Failed to load remote image ${src}. The request did not return a 200 OK response. (received ${res.status}))`
11
12
  );
12
13
  }
14
+ const req = new Request(src);
13
15
  const policy = new CachePolicy(webToCachePolicyRequest(req), webToCachePolicyResponse(res));
14
16
  const expires = policy.storable() ? policy.timeToLive() : 0;
15
17
  return {
@@ -19,13 +21,18 @@ async function loadRemoteImage(src, fetchFn = globalThis.fetch) {
19
21
  lastModified: res.headers.get("Last-Modified") ?? void 0
20
22
  };
21
23
  }
22
- async function revalidateRemoteImage(src, revalidationData, fetchFn = globalThis.fetch) {
24
+ async function revalidateRemoteImage(src, revalidationData, fetchFn = globalThis.fetch, imageConfig = { remotePatterns: [], domains: [] }) {
23
25
  const headers = {
24
26
  ...revalidationData.etag && { "If-None-Match": revalidationData.etag },
25
27
  ...revalidationData.lastModified && { "If-Modified-Since": revalidationData.lastModified }
26
28
  };
27
29
  const req = new Request(src, { headers, cache: "no-cache" });
28
- const res = await fetchFn(req, { redirect: "manual" });
30
+ const res = await fetchWithRedirects({
31
+ url: src,
32
+ headers: new Headers(headers),
33
+ imageConfig,
34
+ fetchFn
35
+ });
29
36
  if (!res.ok && res.status !== 304) {
30
37
  if (res.status >= 300 && res.status < 400) {
31
38
  throw new Error(
@@ -38,7 +45,7 @@ async function revalidateRemoteImage(src, revalidationData, fetchFn = globalThis
38
45
  }
39
46
  const data = Buffer.from(await res.arrayBuffer());
40
47
  if (res.ok && !data.length) {
41
- return await loadRemoteImage(src, fetchFn);
48
+ return await loadRemoteImage(src, fetchFn, imageConfig);
42
49
  }
43
50
  const policy = new CachePolicy(
44
51
  webToCachePolicyRequest(req),
@@ -4,24 +4,7 @@ import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
4
4
  import * as mime from "mrmime";
5
5
  import { getConfiguredImageService } from "../internal.js";
6
6
  import { etag } from "../utils/etag.js";
7
- async function loadRemoteImage(src, headers) {
8
- try {
9
- const res = await fetch(src, {
10
- // Forward all headers from the original request
11
- headers,
12
- redirect: "manual"
13
- });
14
- if (res.status >= 300 && res.status < 400) {
15
- return void 0;
16
- }
17
- if (!res.ok) {
18
- return void 0;
19
- }
20
- return await res.arrayBuffer();
21
- } catch {
22
- return void 0;
23
- }
24
- }
7
+ import { loadImage } from "./loadImage.js";
25
8
  const GET = async ({ request }) => {
26
9
  try {
27
10
  const imageService = await getConfiguredImageService();
@@ -42,7 +25,12 @@ const GET = async ({ request }) => {
42
25
  if (!isRemoteImage && sourceUrl.origin !== url.origin) {
43
26
  return new Response("Forbidden", { status: 403 });
44
27
  }
45
- inputBuffer = await loadRemoteImage(sourceUrl, isRemoteImage ? new Headers() : request.headers);
28
+ inputBuffer = await loadImage(
29
+ sourceUrl,
30
+ isRemoteImage ? new Headers() : request.headers,
31
+ imageConfig,
32
+ isRemoteImage
33
+ );
46
34
  if (!inputBuffer) {
47
35
  return new Response("Not Found", { status: 404 });
48
36
  }
@@ -62,7 +50,7 @@ const GET = async ({ request }) => {
62
50
  });
63
51
  } catch (err) {
64
52
  console.error("Could not process image request:", err);
65
- return new Response(`Server Error: ${err}`, { status: 500 });
53
+ return new Response("Internal Server Error", { status: 500 });
66
54
  }
67
55
  };
68
56
  export {
@@ -0,0 +1,11 @@
1
+ import type { RemoteImageConfig } from '../utils/redirectValidation.js';
2
+ /**
3
+ * Fetches an image by URL. Used by the generic image endpoint for both
4
+ * remote images and local images (self-fetched from the same origin).
5
+ *
6
+ * For remote images, the final URL (after any redirects) is validated
7
+ * against `imageConfig.domains` and `imageConfig.remotePatterns`.
8
+ * Local images skip this check — they are already guarded by the
9
+ * same-origin check in the caller.
10
+ */
11
+ export declare function loadImage(src: URL, headers: Headers, imageConfig: RemoteImageConfig, isRemote: boolean, fetchFn?: typeof globalThis.fetch): Promise<ArrayBuffer | undefined>;
@@ -0,0 +1,19 @@
1
+ import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
2
+ import { fetchWithRedirects } from "../utils/redirectValidation.js";
3
+ async function loadImage(src, headers, imageConfig, isRemote, fetchFn) {
4
+ try {
5
+ const res = await fetchWithRedirects({ url: src, headers, imageConfig, fetchFn });
6
+ if (isRemote && !isRemoteAllowed(res.url, imageConfig)) {
7
+ return void 0;
8
+ }
9
+ if (!res.ok) {
10
+ return void 0;
11
+ }
12
+ return await res.arrayBuffer();
13
+ } catch {
14
+ return void 0;
15
+ }
16
+ }
17
+ export {
18
+ loadImage
19
+ };
@@ -5,10 +5,15 @@ import * as mime from "mrmime";
5
5
  import { getConfiguredImageService } from "../internal.js";
6
6
  import { etag } from "../utils/etag.js";
7
7
  import { inferSourceFormat } from "../utils/inferSourceFormat.js";
8
+ import { fetchWithRedirects } from "../utils/redirectValidation.js";
9
+ const isLocal = (url) => {
10
+ const hostname = new URL(url).hostname;
11
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
12
+ };
8
13
  async function loadRemoteImage(src) {
9
14
  try {
10
- const res = await fetch(src, { redirect: "manual" });
11
- if (res.status >= 300 && res.status < 400) {
15
+ const res = await fetchWithRedirects({ url: src, imageConfig });
16
+ if (!isRemoteAllowed(res.url, imageConfig) && !isLocal(res.url)) {
12
17
  return void 0;
13
18
  }
14
19
  if (!res.ok) {
@@ -2,3 +2,4 @@ export { getConfiguredImageService, getImage, verifyOptions } from './internal.j
2
2
  export { baseService, isLocalService } from './services/service.js';
3
3
  export { hashTransform, propsToFilename } from './utils/hash.js';
4
4
  export type { LocalImageProps, RemoteImageProps } from './types.js';
5
+ export { fetchWithRedirects } from './utils/redirectValidation.js';
@@ -1,8 +1,10 @@
1
1
  import { getConfiguredImageService, getImage, verifyOptions } from "./internal.js";
2
2
  import { baseService, isLocalService } from "./services/service.js";
3
3
  import { hashTransform, propsToFilename } from "./utils/hash.js";
4
+ import { fetchWithRedirects } from "./utils/redirectValidation.js";
4
5
  export {
5
6
  baseService,
7
+ fetchWithRedirects,
6
8
  getConfiguredImageService,
7
9
  getImage,
8
10
  hashTransform,
@@ -1,4 +1,5 @@
1
1
  import { AstroError, AstroErrorData } from "../../core/errors/index.js";
2
+ import { detector } from "../utils/vendor/image-size/detector.js";
2
3
  import {
3
4
  baseService,
4
5
  parseQuality
@@ -82,6 +83,12 @@ const sharpService = {
82
83
  const transform = transformOptions;
83
84
  const kernel = config.service.config.kernel;
84
85
  if (transform.format === "svg") return { data: inputBuffer, format: "svg" };
86
+ if (detector(inputBuffer) === "svg" && !config.dangerouslyProcessSVG) {
87
+ throw new AstroError({
88
+ ...AstroErrorData.UnsupportedImageFormat,
89
+ message: "SVG image processing is disabled. Set `image.dangerouslyProcessSVG: true` to allow processing of SVG sources."
90
+ });
91
+ }
85
92
  const result = sharp(inputBuffer, {
86
93
  failOnError: false,
87
94
  pages: -1,
@@ -10,3 +10,4 @@ export { isESMImportedImage, isRemoteImage, resolveSrc } from './imageKind.js';
10
10
  export { imageMetadata } from './metadata.js';
11
11
  export { getOrigQueryParams } from './queryParams.js';
12
12
  export { inferRemoteSize } from './remoteProbe.js';
13
+ export { fetchWithRedirects } from './redirectValidation.js';
@@ -7,8 +7,10 @@ import { isESMImportedImage, isRemoteImage, resolveSrc } from "./imageKind.js";
7
7
  import { imageMetadata } from "./metadata.js";
8
8
  import { getOrigQueryParams } from "./queryParams.js";
9
9
  import { inferRemoteSize } from "./remoteProbe.js";
10
+ import { fetchWithRedirects } from "./redirectValidation.js";
10
11
  export {
11
12
  emitClientAsset,
13
+ fetchWithRedirects,
12
14
  getOrigQueryParams,
13
15
  imageMetadata,
14
16
  inferRemoteSize,
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Utilities for handling HTTP redirects with validation
3
+ */
4
+ import type { AstroConfig } from '../../types/public/config.js';
5
+ export type RemoteImageConfig = Pick<AstroConfig['image'], 'remotePatterns' | 'domains'>;
6
+ export type FetchRedirectOptions = {
7
+ /**
8
+ * URL to fetch (either string or URL object)
9
+ */
10
+ url: string | URL;
11
+ /**
12
+ * Headers to include in the request (optional)
13
+ */
14
+ headers?: Headers;
15
+ /**
16
+ * Image config for validating redirect destinations (optional)
17
+ */
18
+ imageConfig: RemoteImageConfig;
19
+ /**
20
+ * Fetch function to use (default: globalThis.fetch)
21
+ */
22
+ fetchFn?: typeof fetch;
23
+ /**
24
+ * Maximum number of redirects to follow (default: 10)
25
+ */
26
+ redirectLimit?: number;
27
+ /**
28
+ * Error handler for redirect depth exceeded (default: generic Error)
29
+ */
30
+ onMaxRedirectsExceeded?: (url: string) => Error;
31
+ /**
32
+ * Error handler for missing Location header (default: generic Error)
33
+ */
34
+ onMissingLocationHeader?: (status: number, url: string) => Error;
35
+ /**
36
+ * Error handler for disallowed redirect (default: generic Error)
37
+ */
38
+ onDisallowedRedirect?: (currentUrl: string, targetUrl: string) => Error;
39
+ };
40
+ /**
41
+ * Recursively follows HTTP redirects with validation according to the image configuration.
42
+ *
43
+ * If any of the domains in the redirect chain are not allowed by either `image.remotePatterns`
44
+ * or `image.domains`, this function will throw an error for a disallowed redirect.
45
+ *
46
+ * @param options The options for this fetch call.
47
+ */
48
+ export declare function fetchWithRedirects(options: FetchRedirectOptions): Promise<Response>;
@@ -0,0 +1,48 @@
1
+ import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
2
+ async function fetchWithRedirects(options) {
3
+ const {
4
+ url,
5
+ headers,
6
+ imageConfig,
7
+ fetchFn = globalThis.fetch,
8
+ redirectLimit = 10,
9
+ onMaxRedirectsExceeded = (_u) => new Error("Maximum redirect depth exceeded"),
10
+ onMissingLocationHeader = (_s, _u) => new Error(`Redirect response ${_s} missing Location header`),
11
+ onDisallowedRedirect = (_current, _target) => new Error(
12
+ `The image at ${_current} redirected to ${_target}, which is not an allowed remote location.`
13
+ )
14
+ } = options;
15
+ if (redirectLimit <= 0) {
16
+ throw onMaxRedirectsExceeded(typeof url === "string" ? url : url.toString());
17
+ }
18
+ const urlString = typeof url === "string" ? url : url.toString();
19
+ const req = new Request(url, { headers });
20
+ const res = await fetchFn(req, { redirect: "manual" });
21
+ if ([301, 302, 303, 307, 308].includes(res.status)) {
22
+ const location = res.headers.get("Location");
23
+ if (!location) {
24
+ throw onMissingLocationHeader(res.status, urlString);
25
+ }
26
+ const redirectUrl = new URL(location, urlString).toString();
27
+ if (!isRemoteAllowed(redirectUrl, {
28
+ domains: imageConfig.domains ?? [],
29
+ remotePatterns: imageConfig.remotePatterns ?? []
30
+ })) {
31
+ throw onDisallowedRedirect(urlString, redirectUrl);
32
+ }
33
+ return fetchWithRedirects({
34
+ url: redirectUrl,
35
+ headers,
36
+ imageConfig,
37
+ fetchFn,
38
+ redirectLimit: redirectLimit - 1,
39
+ onMaxRedirectsExceeded,
40
+ onMissingLocationHeader,
41
+ onDisallowedRedirect
42
+ });
43
+ }
44
+ return res;
45
+ }
46
+ export {
47
+ fetchWithRedirects
48
+ };
@@ -1,6 +1,7 @@
1
1
  import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
2
2
  import { AstroError, AstroErrorData } from "../../core/errors/index.js";
3
3
  import { imageMetadata } from "./metadata.js";
4
+ import { fetchWithRedirects } from "./redirectValidation.js";
4
5
  async function inferRemoteSize(url, imageConfig) {
5
6
  if (!URL.canParse(url)) {
6
7
  throw new AstroError({
@@ -27,13 +28,35 @@ async function inferRemoteSize(url, imageConfig) {
27
28
  message: AstroErrorData.RemoteImageNotAllowed.message(url)
28
29
  });
29
30
  }
30
- const response = await fetch(url, { redirect: "manual" });
31
- if (response.status >= 300 && response.status < 400) {
31
+ let response;
32
+ try {
33
+ response = await fetchWithRedirects({
34
+ url,
35
+ onMaxRedirectsExceeded: (u) => new AstroError({
36
+ ...AstroErrorData.FailedToFetchRemoteImageDimensions,
37
+ message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(u)
38
+ }),
39
+ onMissingLocationHeader: (_status, u) => new AstroError({
40
+ ...AstroErrorData.FailedToFetchRemoteImageDimensions,
41
+ message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(u)
42
+ }),
43
+ imageConfig: imageConfig ?? {
44
+ remotePatterns: [],
45
+ domains: []
46
+ }
47
+ });
48
+ } catch (_err) {
32
49
  throw new AstroError({
33
50
  ...AstroErrorData.FailedToFetchRemoteImageDimensions,
34
51
  message: AstroErrorData.FailedToFetchRemoteImageDimensions.message(url)
35
52
  });
36
53
  }
54
+ if (allowlistConfig && !isRemoteAllowed(response.url, allowlistConfig)) {
55
+ throw new AstroError({
56
+ ...AstroErrorData.RemoteImageNotAllowed,
57
+ message: AstroErrorData.RemoteImageNotAllowed.message(url)
58
+ });
59
+ }
37
60
  if (!response.body || !response.ok) {
38
61
  throw new AstroError({
39
62
  ...AstroErrorData.FailedToFetchRemoteImageDimensions,
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "6.2.2";
3
+ version = "6.3.1";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -3,9 +3,11 @@ import { getDefaultClientDirectives } from "../core/client-directive/index.js";
3
3
  import { ASTRO_CONFIG_DEFAULTS } from "../core/config/schemas/index.js";
4
4
  import { validateConfig } from "../core/config/validate.js";
5
5
  import { createKey } from "../core/encryption.js";
6
+ import { FetchState } from "../core/fetch/fetch-state.js";
7
+ import { AstroMiddleware } from "../core/middleware/astro-middleware.js";
6
8
  import { NOOP_MIDDLEWARE_FN } from "../core/middleware/noop-middleware.js";
9
+ import { PagesHandler } from "../core/pages/handler.js";
7
10
  import { removeLeadingForwardSlash } from "../core/path.js";
8
- import { RenderContext } from "../core/render-context.js";
9
11
  import { getParts } from "../core/routing/parts.js";
10
12
  import { getPattern } from "../core/routing/pattern.js";
11
13
  import { validateSegment } from "../core/routing/segment.js";
@@ -69,6 +71,8 @@ function createManifest(manifest, renderers, middleware) {
69
71
  }
70
72
  class experimental_AstroContainer {
71
73
  #pipeline;
74
+ #astroMiddleware;
75
+ #pagesHandler;
72
76
  /**
73
77
  * Internally used to check if the container was created with a manifest.
74
78
  * @private
@@ -95,6 +99,8 @@ class experimental_AstroContainer {
95
99
  return specifier;
96
100
  }
97
101
  });
102
+ this.#astroMiddleware = new AstroMiddleware(this.#pipeline);
103
+ this.#pagesHandler = new PagesHandler(this.#pipeline);
98
104
  }
99
105
  async #containerResolve(specifier, astroConfig) {
100
106
  const found = this.#pipeline.manifest.entryModules[specifier];
@@ -285,23 +291,21 @@ class experimental_AstroContainer {
285
291
  params: options.params,
286
292
  type: routeType
287
293
  });
288
- const renderContext = await RenderContext.create({
289
- pipeline: this.#pipeline,
290
- routeData,
291
- status: 200,
292
- request,
293
- pathname: url.pathname,
294
- locals: options?.locals ?? {},
295
- partial: options?.partial ?? true,
296
- clientAddress: ""
297
- });
294
+ const state = new FetchState(this.#pipeline, request);
295
+ state.routeData = routeData;
296
+ state.pathname = url.pathname;
297
+ state.clientAddress = "";
298
+ state.partial = options?.partial ?? true;
299
+ state.componentInstance = componentInstance;
300
+ state.slots = slots ?? {};
298
301
  if (options.params) {
299
- renderContext.params = options.params;
302
+ state.params = options.params;
300
303
  }
304
+ state.locals = options?.locals ?? {};
301
305
  if (options.props) {
302
- renderContext.props = options.props;
306
+ state.initialProps = options.props;
303
307
  }
304
- return renderContext.render(componentInstance, slots);
308
+ return this.#astroMiddleware.handle(state, this.#pagesHandler.handle.bind(this.#pagesHandler));
305
309
  }
306
310
  /**
307
311
  * It stores an Astro **page** route. The first argument, `route`, gets associated to the `component`.
@@ -39,7 +39,6 @@ class ContentLayer {
39
39
  watcher,
40
40
  contentConfigObserver = globalContentConfigObserver
41
41
  }) {
42
- watcher?.setMaxListeners(50);
43
42
  this.#logger = logger;
44
43
  this.#store = store;
45
44
  this.#settings = settings;
@@ -192,7 +191,7 @@ ${contentConfig.error.message}`
192
191
  logger.info("Content config changed");
193
192
  shouldClear = true;
194
193
  }
195
- if (previousAstroVersion && previousAstroVersion !== "6.2.2") {
194
+ if (previousAstroVersion && previousAstroVersion !== "6.3.1") {
196
195
  logger.info("Astro version changed");
197
196
  shouldClear = true;
198
197
  }
@@ -200,8 +199,8 @@ ${contentConfig.error.message}`
200
199
  logger.info("Clearing content store");
201
200
  this.#store.clearAll();
202
201
  }
203
- if ("6.2.2") {
204
- this.#store.metaStore().set("astro-version", "6.2.2");
202
+ if ("6.3.1") {
203
+ this.#store.metaStore().set("astro-version", "6.3.1");
205
204
  }
206
205
  if (currentConfigDigest) {
207
206
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -6,10 +6,6 @@ async function attachContentServerListeners({
6
6
  logger,
7
7
  settings
8
8
  }) {
9
- const maxListeners = viteServer.watcher.getMaxListeners();
10
- if (maxListeners !== 0 && maxListeners < 50) {
11
- viteServer.watcher.setMaxListeners(50);
12
- }
13
9
  const contentGenerator = await createContentTypesGenerator({
14
10
  fs,
15
11
  settings,
@@ -1,7 +1,7 @@
1
1
  import nodeFs from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { dataToEsm } from "@rollup/pluginutils";
4
- import { normalizePath } from "vite";
4
+ import { isRunnableDevEnvironment, normalizePath } from "vite";
5
5
  import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../core/constants.js";
6
6
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
7
7
  import { rootRelativePath } from "../core/viteUtils.js";
@@ -30,6 +30,14 @@ function invalidateDataStore(viteServer) {
30
30
  const timestamp = Date.now();
31
31
  environment.moduleGraph.invalidateModule(module, void 0, timestamp, true);
32
32
  }
33
+ if (isRunnableDevEnvironment(environment)) {
34
+ const runnerModule = environment.runner.evaluatedModules.getModuleById(
35
+ RESOLVED_DATA_STORE_VIRTUAL_ID
36
+ );
37
+ if (runnerModule) {
38
+ environment.runner.evaluatedModules.invalidateModule(runnerModule);
39
+ }
40
+ }
33
41
  environment.hot.send("astro:content-changed", {});
34
42
  viteServer.environments.client.hot.send({
35
43
  type: "full-reload",