astro 6.0.0-beta.19 → 6.0.0-beta.20

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 (82) hide show
  1. package/components/ClientRouter.astro +3 -3
  2. package/dist/actions/runtime/server.js +29 -45
  3. package/dist/assets/fonts/providers/local.d.ts +1 -1
  4. package/dist/assets/utils/imageKind.d.ts +2 -2
  5. package/dist/assets/utils/resolveImports.d.ts +2 -2
  6. package/dist/assets/vite-plugin-assets.js +40 -35
  7. package/dist/cli/index.js +3 -5
  8. package/dist/cli/info/infra/tinyclip-clipboard.d.ts +10 -0
  9. package/dist/cli/info/infra/tinyclip-clipboard.js +32 -0
  10. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  11. package/dist/container/index.d.ts +1 -1
  12. package/dist/container/index.js +1 -0
  13. package/dist/content/content-layer.js +3 -3
  14. package/dist/content/data-store.d.ts +1 -1
  15. package/dist/core/app/base.d.ts +1 -1
  16. package/dist/core/app/base.js +2 -0
  17. package/dist/core/app/dev/pipeline.js +3 -1
  18. package/dist/core/app/manifest.d.ts +0 -1
  19. package/dist/core/app/manifest.js +0 -4
  20. package/dist/core/app/node.d.ts +3 -1
  21. package/dist/core/app/node.js +22 -8
  22. package/dist/core/app/types.d.ts +2 -1
  23. package/dist/core/app/validate-headers.d.ts +1 -1
  24. package/dist/core/app/validate-headers.js +0 -2
  25. package/dist/core/base-pipeline.d.ts +1 -1
  26. package/dist/core/base-pipeline.js +2 -3
  27. package/dist/core/build/common.d.ts +1 -1
  28. package/dist/core/build/pipeline.js +3 -1
  29. package/dist/core/build/plugins/plugin-component-entry.d.ts +1 -1
  30. package/dist/core/build/plugins/plugin-manifest.js +1 -0
  31. package/dist/core/build/static-build.d.ts +1 -1
  32. package/dist/core/config/schemas/base.d.ts +2 -0
  33. package/dist/core/config/schemas/base.js +4 -2
  34. package/dist/core/config/schemas/refined.js +1 -1
  35. package/dist/core/config/schemas/relative.d.ts +3 -0
  36. package/dist/core/constants.js +1 -1
  37. package/dist/core/cookies/cookies.js +1 -1
  38. package/dist/core/create-vite.js +2 -0
  39. package/dist/core/dev/dev.js +1 -1
  40. package/dist/core/errors/errors-data.d.ts +46 -3
  41. package/dist/core/errors/errors-data.js +21 -4
  42. package/dist/core/errors/userError.d.ts +1 -0
  43. package/dist/core/errors/userError.js +3 -1
  44. package/dist/core/messages/runtime.js +1 -1
  45. package/dist/core/middleware/callMiddleware.d.ts +1 -1
  46. package/dist/core/middleware/index.d.ts +8 -1
  47. package/dist/core/middleware/index.js +5 -10
  48. package/dist/core/render/slots.js +1 -1
  49. package/dist/core/render-context.js +4 -4
  50. package/dist/core/request-body.d.ts +17 -0
  51. package/dist/core/request-body.js +43 -0
  52. package/dist/core/routing/create-manifest.js +2 -2
  53. package/dist/core/routing/match.d.ts +3 -3
  54. package/dist/core/server-islands/endpoint.d.ts +1 -1
  55. package/dist/core/server-islands/endpoint.js +12 -3
  56. package/dist/env/env-loader.js +25 -3
  57. package/dist/env/validators.d.ts +10 -0
  58. package/dist/env/validators.js +22 -0
  59. package/dist/i18n/index.d.ts +2 -2
  60. package/dist/manifest/serialized.js +2 -0
  61. package/dist/runtime/client/dev-toolbar/apps/audit/rules/a11y.js +3 -3
  62. package/dist/runtime/client/dev-toolbar/apps/audit/ui/audit-list-window.js +1 -1
  63. package/dist/runtime/client/visible.d.ts +1 -1
  64. package/dist/runtime/server/html-string-cache.d.ts +12 -0
  65. package/dist/runtime/server/html-string-cache.js +71 -0
  66. package/dist/runtime/server/render/component.js +8 -1
  67. package/dist/runtime/server/render/queue/pool.d.ts +12 -185
  68. package/dist/runtime/server/render/queue/pool.js +10 -128
  69. package/dist/types/astro.d.ts +1 -1
  70. package/dist/types/public/config.d.ts +28 -2
  71. package/dist/types/public/context.d.ts +3 -3
  72. package/dist/types/public/integrations.d.ts +1 -1
  73. package/dist/types/public/internal.d.ts +1 -1
  74. package/dist/types/public/manifest.d.ts +1 -1
  75. package/dist/vite-plugin-app/pipeline.js +3 -1
  76. package/dist/vite-plugin-astro-server/plugin.js +2 -0
  77. package/package.json +5 -4
  78. package/tsconfigs/base.json +1 -3
  79. package/dist/cli/info/infra/cli-clipboard.d.ts +0 -13
  80. package/dist/cli/info/infra/cli-clipboard.js +0 -78
  81. package/dist/core/routing/request.d.ts +0 -9
  82. package/dist/core/routing/request.js +0 -9
@@ -115,14 +115,14 @@ const { fallback = 'animate' } = Astro.props;
115
115
  const form = el as HTMLFormElement;
116
116
  const formData = new FormData(form, submitter);
117
117
  // form.action and form.method can point to an <input name="action"> or <input name="method">
118
- // in which case should fallback to the form attribute
118
+ // in which case should fall back to the form attribute
119
119
  const formAction =
120
120
  typeof form.action === 'string' ? form.action : form.getAttribute('action');
121
121
  const formMethod =
122
122
  typeof form.method === 'string' ? form.method : form.getAttribute('method');
123
- // Use the form action, if defined, otherwise fallback to current path.
123
+ // Use the form action, if defined; otherwise, fall back to current path.
124
124
  let action = submitter?.getAttribute('formaction') ?? formAction ?? location.pathname;
125
- // Use the form method, if defined, otherwise fallback to "get"
125
+ // Use the form method, if defined; otherwise, fall back to "get"
126
126
  const method = submitter?.getAttribute('formmethod') ?? formMethod ?? 'get';
127
127
 
128
128
  // the "dialog" method is a special keyword used within <dialog> elements
@@ -9,6 +9,7 @@ import {
9
9
  } from "../../core/errors/errors-data.js";
10
10
  import { AstroError } from "../../core/errors/errors.js";
11
11
  import { removeTrailingForwardSlash } from "../../core/path.js";
12
+ import { BodySizeLimitError, readBodyWithLimit } from "../../core/request-body.js";
12
13
  import { ACTION_QUERY_PARAMS, ACTION_RPC_ROUTE_PATTERN } from "../consts.js";
13
14
  import {
14
15
  ActionError,
@@ -162,26 +163,36 @@ async function parseRequestBody(request, bodySizeLimit) {
162
163
  message: `Request body exceeds ${bodySizeLimit} bytes`
163
164
  });
164
165
  }
165
- if (hasContentType(contentType, formContentTypes)) {
166
- if (!hasContentLength) {
167
- const body = await readRequestBodyWithLimit(request.clone(), bodySizeLimit);
168
- const formRequest = new Request(request.url, {
169
- method: request.method,
170
- headers: request.headers,
171
- body: toArrayBuffer(body)
172
- });
173
- return await formRequest.formData();
166
+ try {
167
+ if (hasContentType(contentType, formContentTypes)) {
168
+ if (!hasContentLength) {
169
+ const body = await readBodyWithLimit(request.clone(), bodySizeLimit);
170
+ const formRequest = new Request(request.url, {
171
+ method: request.method,
172
+ headers: request.headers,
173
+ body: toArrayBuffer(body)
174
+ });
175
+ return await formRequest.formData();
176
+ }
177
+ return await request.clone().formData();
174
178
  }
175
- return await request.clone().formData();
176
- }
177
- if (hasContentType(contentType, ["application/json"])) {
178
- if (contentLength === 0) return void 0;
179
- if (!hasContentLength) {
180
- const body = await readRequestBodyWithLimit(request.clone(), bodySizeLimit);
181
- if (body.byteLength === 0) return void 0;
182
- return JSON.parse(new TextDecoder().decode(body));
179
+ if (hasContentType(contentType, ["application/json"])) {
180
+ if (contentLength === 0) return void 0;
181
+ if (!hasContentLength) {
182
+ const body = await readBodyWithLimit(request.clone(), bodySizeLimit);
183
+ if (body.byteLength === 0) return void 0;
184
+ return JSON.parse(new TextDecoder().decode(body));
185
+ }
186
+ return await request.clone().json();
187
+ }
188
+ } catch (e) {
189
+ if (e instanceof BodySizeLimitError) {
190
+ throw new ActionError({
191
+ code: "CONTENT_TOO_LARGE",
192
+ message: `Request body exceeds ${bodySizeLimit} bytes`
193
+ });
183
194
  }
184
- return await request.clone().json();
195
+ throw e;
185
196
  }
186
197
  throw new TypeError("Unsupported content type");
187
198
  }
@@ -323,33 +334,6 @@ function serializeActionResult(res) {
323
334
  body
324
335
  };
325
336
  }
326
- async function readRequestBodyWithLimit(request, limit) {
327
- if (!request.body) return new Uint8Array();
328
- const reader = request.body.getReader();
329
- const chunks = [];
330
- let received = 0;
331
- while (true) {
332
- const { done, value } = await reader.read();
333
- if (done) break;
334
- if (value) {
335
- received += value.byteLength;
336
- if (received > limit) {
337
- throw new ActionError({
338
- code: "CONTENT_TOO_LARGE",
339
- message: `Request body exceeds ${limit} bytes`
340
- });
341
- }
342
- chunks.push(value);
343
- }
344
- }
345
- const buffer = new Uint8Array(received);
346
- let offset = 0;
347
- for (const chunk of chunks) {
348
- buffer.set(chunk, offset);
349
- offset += chunk.byteLength;
350
- }
351
- return buffer;
352
- }
353
337
  function toArrayBuffer(buffer) {
354
338
  const copy = new Uint8Array(buffer.byteLength);
355
339
  copy.set(buffer);
@@ -7,7 +7,7 @@ type RawSource = string | URL | {
7
7
  };
8
8
  interface Variant extends FamilyProperties {
9
9
  /**
10
- * Font [sources](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src). It can be a path relative to the root, a package import or a URL. URLs are particularly useful if you inject local fonts through an integration.
10
+ * Font [sources](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src). It can be a path relative to the root, a package import, or a URL. URLs are particularly useful if you inject local fonts through an integration.
11
11
  *
12
12
  * We recommend not putting your font files in [the `public/` directory](/en/reference/configuration-reference/#publicdir). Since Astro will copy these files into that folder at build time, this will result in duplicated files
13
13
  * in your build output. Instead, store them somewhere else in your project, such as in `src/`.
@@ -3,14 +3,14 @@ import type { ImageMetadata, UnresolvedImageTransform } from '../types.js';
3
3
  * Determines if the given source is an ECMAScript Module (ESM) imported image.
4
4
  *
5
5
  * @param {ImageMetadata | string} src - The source to check. It can be an `ImageMetadata` object or a string.
6
- * @return {boolean} Returns `true` if the source is an `ImageMetadata` object, otherwise `false`.
6
+ * @return {boolean} Returns `true` if the source is an `ImageMetadata` object; otherwise, `false`.
7
7
  */
8
8
  export declare function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata;
9
9
  /**
10
10
  * Determines if the provided source is a remote image URL in the form of a string.
11
11
  *
12
12
  * @param {ImageMetadata | string} src - The source to check, which can either be an `ImageMetadata` object or a string.
13
- * @return {boolean} Returns `true` if the source is a string, otherwise `false`.
13
+ * @return {boolean} Returns `true` if the source is a string; otherwise, `false`.
14
14
  */
15
15
  export declare function isRemoteImage(src: ImageMetadata | string): src is string;
16
16
  /**
@@ -2,8 +2,8 @@
2
2
  * Resolves an image src from a content file (such as markdown) to a module ID or import that can be resolved by Vite.
3
3
  *
4
4
  * @param imageSrc The src attribute of an image tag
5
- * @param filePath The path to the file that contains the imagem relative to the site root
6
- * @returns A module id of the image that can be rsolved by Vite, or undefined if it is not a local image
5
+ * @param filePath The path to the file that contains the image relative to the site root
6
+ * @returns A module id of the image that can be resolved by Vite, or undefined if it is not a local image
7
7
  */
8
8
  export declare function imageSrcToImportId(imageSrc: string, filePath?: string): string | undefined;
9
9
  export declare const importIdToSymbolName: (importId: string) => string;
@@ -118,59 +118,64 @@ function assets({ fs, settings, sync, logger }) {
118
118
  id: new RegExp(`^(${RESOLVED_VIRTUAL_MODULE_ID})$`)
119
119
  },
120
120
  handler() {
121
+ const isServerEnvironment = isAstroServerEnvironment(this.environment);
122
+ const getImageExport = isServerEnvironment ? `import { getImage as getImageInternal } from "astro/assets";
123
+ export const getImage = async (options) => await getImageInternal(options, imageConfig);` : `import { AstroError, AstroErrorData } from "astro/errors";
124
+ export const getImage = async () => {
125
+ throw new AstroError(AstroErrorData.GetImageNotUsedOnServer);
126
+ };`;
121
127
  return {
122
128
  code: `
123
- import { getConfiguredImageService as _getConfiguredImageService } from "astro/assets";
124
- export { isLocalService } from "astro/assets";
125
- import { getImage as getImageInternal } from "astro/assets";
126
- ${settings.config.image.responsiveStyles ? `import "${VIRTUAL_IMAGE_STYLES_ID}";` : ""}
127
- export { default as Image } from "astro/components/${imageComponentPrefix}Image.astro";
128
- export { default as Picture } from "astro/components/${imageComponentPrefix}Picture.astro";
129
- import { inferRemoteSize as inferRemoteSizeInternal } from "astro/assets/utils/inferRemoteSize.js";
129
+ import { getConfiguredImageService as _getConfiguredImageService } from "astro/assets";
130
+ export { isLocalService } from "astro/assets";
131
+ ${settings.config.image.responsiveStyles ? `import "${VIRTUAL_IMAGE_STYLES_ID}";` : ""}
132
+ export { default as Image } from "astro/components/${imageComponentPrefix}Image.astro";
133
+ export { default as Picture } from "astro/components/${imageComponentPrefix}Picture.astro";
134
+ import { inferRemoteSize as inferRemoteSizeInternal } from "astro/assets/utils/inferRemoteSize.js";
130
135
 
131
- export { default as Font } from "astro/components/Font.astro";
132
- export * from "${RUNTIME_VIRTUAL_MODULE_ID}";
133
-
134
- export const getConfiguredImageService = _getConfiguredImageService;
136
+ export { default as Font } from "astro/components/Font.astro";
137
+ export * from "${RUNTIME_VIRTUAL_MODULE_ID}";
135
138
 
136
- export const viteFSConfig = ${JSON.stringify(resolvedConfig.server.fs ?? {})};
139
+ export const getConfiguredImageService = _getConfiguredImageService;
137
140
 
138
- export const safeModulePaths = new Set(${JSON.stringify(
141
+ export const viteFSConfig = ${JSON.stringify(resolvedConfig.server.fs ?? {})};
142
+
143
+ export const safeModulePaths = new Set(${JSON.stringify(
139
144
  // @ts-expect-error safeModulePaths is internal to Vite
140
145
  Array.from(resolvedConfig.safeModulePaths ?? [])
141
146
  )});
142
147
 
143
- export const fsDenyGlob = ${serializeFsDenyGlob(resolvedConfig.server.fs?.deny ?? [])};
148
+ export const fsDenyGlob = ${serializeFsDenyGlob(resolvedConfig.server.fs?.deny ?? [])};
144
149
 
145
- const assetQueryParams = ${settings.adapter?.client?.assetQueryParams ? `new URLSearchParams(${JSON.stringify(
150
+ const assetQueryParams = ${settings.adapter?.client?.assetQueryParams ? `new URLSearchParams(${JSON.stringify(
146
151
  Array.from(settings.adapter.client.assetQueryParams.entries())
147
152
  )})` : "undefined"};
148
- export const imageConfig = ${JSON.stringify(settings.config.image)};
149
- Object.defineProperty(imageConfig, 'assetQueryParams', {
150
- value: assetQueryParams,
151
- enumerable: false,
152
- configurable: true,
153
- });
154
- export const inferRemoteSize = async (url) => {
155
- const service = await _getConfiguredImageService();
156
- return service.getRemoteSize?.(url, imageConfig) ?? inferRemoteSizeInternal(url, imageConfig);
157
- }
158
- // This is used by the @astrojs/node integration to locate images.
159
- // It's unused on other platforms, but on some platforms like Netlify (and presumably also Vercel)
160
- // new URL("dist/...") is interpreted by the bundler as a signal to include that directory
161
- // in the Lambda bundle, which would bloat the bundle with images.
162
- // To prevent this, we mark the URL construction as pure,
163
- // so that it's tree-shaken away for all platforms that don't need it.
164
- export const outDir = /* #__PURE__ */ new URL(${JSON.stringify(
153
+ export const imageConfig = ${JSON.stringify(settings.config.image)};
154
+ Object.defineProperty(imageConfig, 'assetQueryParams', {
155
+ value: assetQueryParams,
156
+ enumerable: false,
157
+ configurable: true,
158
+ });
159
+ export const inferRemoteSize = async (url) => {
160
+ const service = await _getConfiguredImageService();
161
+ return service.getRemoteSize?.(url, imageConfig) ?? inferRemoteSizeInternal(url, imageConfig);
162
+ }
163
+ // This is used by the @astrojs/node integration to locate images.
164
+ // It's unused on other platforms, but on some platforms like Netlify (and presumably also Vercel)
165
+ // new URL("dist/...") is interpreted by the bundler as a signal to include that directory
166
+ // in the Lambda bundle, which would bloat the bundle with images.
167
+ // To prevent this, we mark the URL construction as pure,
168
+ // so that it's tree-shaken away for all platforms that don't need it.
169
+ export const outDir = /* #__PURE__ */ new URL(${JSON.stringify(
165
170
  new URL(
166
171
  settings.buildOutput === "server" ? settings.config.build.client : settings.config.outDir
167
172
  )
168
173
  )});
169
- export const serverDir = /* #__PURE__ */ new URL(${JSON.stringify(
174
+ export const serverDir = /* #__PURE__ */ new URL(${JSON.stringify(
170
175
  new URL(settings.config.build.server)
171
176
  )});
172
- export const getImage = async (options) => await getImageInternal(options, imageConfig);
173
- `
177
+ ${getImageExport}
178
+ `
174
179
  };
175
180
  }
176
181
  },
package/dist/cli/index.js CHANGED
@@ -75,7 +75,7 @@ async function runCommand(cmd, flags) {
75
75
  { getPackageManager },
76
76
  { StyledDebugInfoFormatter },
77
77
  { ClackPrompt },
78
- { CliClipboard },
78
+ { TinyclipClipboard },
79
79
  { PassthroughTextStyler },
80
80
  { infoCommand }
81
81
  ] = await Promise.all([
@@ -88,7 +88,7 @@ async function runCommand(cmd, flags) {
88
88
  import("./info/core/get-package-manager.js"),
89
89
  import("./info/infra/styled-debug-info-formatter.js"),
90
90
  import("./info/infra/clack-prompt.js"),
91
- import("./info/infra/cli-clipboard.js"),
91
+ import("./info/infra/tinyclip-clipboard.js"),
92
92
  import("./infra/passthrough-text-styler.js"),
93
93
  import("./info/core/info.js")
94
94
  ]);
@@ -108,10 +108,8 @@ async function runCommand(cmd, flags) {
108
108
  nodeVersionProvider
109
109
  });
110
110
  const prompt = new ClackPrompt({ force: flags.copy });
111
- const clipboard = new CliClipboard({
112
- commandExecutor,
111
+ const clipboard = new TinyclipClipboard({
113
112
  logger,
114
- operatingSystemProvider,
115
113
  prompt
116
114
  });
117
115
  return await runner.run(infoCommand, {
@@ -0,0 +1,10 @@
1
+ import type { Logger } from '../../../core/logger/core.js';
2
+ import type { Clipboard, Prompt } from '../definitions.js';
3
+ export declare class TinyclipClipboard implements Clipboard {
4
+ #private;
5
+ constructor({ logger, prompt, }: {
6
+ logger: Logger;
7
+ prompt: Prompt;
8
+ });
9
+ copy(text: string): Promise<void>;
10
+ }
@@ -0,0 +1,32 @@
1
+ import { writeText } from "tinyclip";
2
+ class TinyclipClipboard {
3
+ #logger;
4
+ #prompt;
5
+ constructor({
6
+ logger,
7
+ prompt
8
+ }) {
9
+ this.#logger = logger;
10
+ this.#prompt = prompt;
11
+ }
12
+ async copy(text) {
13
+ if (!await this.#prompt.confirm({
14
+ message: "Copy to clipboard?",
15
+ defaultValue: true
16
+ })) {
17
+ return;
18
+ }
19
+ try {
20
+ await writeText(text.trim());
21
+ this.#logger.info("SKIP_FORMAT", "Copied to clipboard!");
22
+ } catch {
23
+ this.#logger.error(
24
+ "SKIP_FORMAT",
25
+ "Sorry, something went wrong! Please copy the text above manually."
26
+ );
27
+ }
28
+ }
29
+ }
30
+ export {
31
+ TinyclipClipboard
32
+ };
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "6.0.0-beta.19";
3
+ version = "6.0.0-beta.20";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -53,7 +53,7 @@ export type ContainerRenderOptions = {
53
53
  */
54
54
  params?: Record<string, string | undefined>;
55
55
  /**
56
- * Useful if your component needs to access some locals without the use a middleware.
56
+ * Useful if your component needs to access some locals without the use of middleware.
57
57
  * ```js
58
58
  * container.renderToString(Component, { locals: { getSomeValue() {} } });
59
59
  * ```
@@ -49,6 +49,7 @@ function createManifest(manifest, renderers, middleware) {
49
49
  checkOrigin: false,
50
50
  allowedDomains: manifest?.allowedDomains ?? [],
51
51
  actionBodySizeLimit: 1024 * 1024,
52
+ serverIslandBodySizeLimit: 1024 * 1024,
52
53
  middleware: manifest?.middleware ?? middlewareInstance,
53
54
  key: createKey(),
54
55
  csp: manifest?.csp,
@@ -189,7 +189,7 @@ ${contentConfig.error.message}`
189
189
  logger.info("Content config changed");
190
190
  shouldClear = true;
191
191
  }
192
- if (previousAstroVersion && previousAstroVersion !== "6.0.0-beta.19") {
192
+ if (previousAstroVersion && previousAstroVersion !== "6.0.0-beta.20") {
193
193
  logger.info("Astro version changed");
194
194
  shouldClear = true;
195
195
  }
@@ -197,8 +197,8 @@ ${contentConfig.error.message}`
197
197
  logger.info("Clearing content store");
198
198
  this.#store.clearAll();
199
199
  }
200
- if ("6.0.0-beta.19") {
201
- this.#store.metaStore().set("astro-version", "6.0.0-beta.19");
200
+ if ("6.0.0-beta.20") {
201
+ this.#store.metaStore().set("astro-version", "6.0.0-beta.20");
202
202
  }
203
203
  if (currentConfigDigest) {
204
204
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -7,7 +7,7 @@ export interface RenderedContent {
7
7
  imagePaths?: Array<string>;
8
8
  /** Any headings that are present in this file. */
9
9
  headings?: MarkdownHeading[];
10
- /** Raw frontmatter, parsed parsed from the file. This may include data from remark plugins. */
10
+ /** Raw frontmatter, parsed from the file. This may include data from remark plugins. */
11
11
  frontmatter?: Record<string, any>;
12
12
  /** Any other metadata that is present in this file. */
13
13
  [key: string]: unknown;
@@ -34,7 +34,7 @@ export interface RenderOptions {
34
34
  /**
35
35
  * A custom fetch function for retrieving prerendered pages - 404 or 500.
36
36
  *
37
- * If not provided, Astro will fallback to its default behavior for fetching error pages.
37
+ * If not provided, Astro will fall back to its default behavior for fetching error pages.
38
38
  *
39
39
  * When a dynamic route is matched but ultimately results in a 404, this function will be used
40
40
  * to fetch the prerendered 404 page if available. Similarly, it may be used to fetch a
@@ -493,6 +493,8 @@ class BaseApp {
493
493
  const status = override?.status ? override.status : originalResponse.status === 200 ? newResponse.status : originalResponse.status;
494
494
  try {
495
495
  originalResponse.headers.delete("Content-type");
496
+ originalResponse.headers.delete("Content-Length");
497
+ originalResponse.headers.delete("Transfer-Encoding");
496
498
  } catch {
497
499
  }
498
500
  const newHeaders = new Headers();
@@ -35,7 +35,9 @@ class NonRunnablePipeline extends Pipeline {
35
35
  );
36
36
  if (queueRenderingEnabled(manifest.experimentalQueuedRendering)) {
37
37
  pipeline.nodePool = newNodePool(manifest.experimentalQueuedRendering);
38
- pipeline.htmlStringCache = new HTMLStringCache(1e3);
38
+ if (manifest.experimentalQueuedRendering.contentCache) {
39
+ pipeline.htmlStringCache = new HTMLStringCache(1e3);
40
+ }
39
41
  }
40
42
  return pipeline;
41
43
  }
@@ -9,5 +9,4 @@ export declare function deserializeRouteData(rawRouteData: SerializedRouteData):
9
9
  export declare function serializeRouteInfo(routeInfo: RouteInfo, trailingSlash: AstroConfig['trailingSlash']): SerializedRouteInfo;
10
10
  export declare function deserializeRouteInfo(rawRouteInfo: SerializedRouteInfo): RouteInfo;
11
11
  export declare function queuePoolSize(config: NonNullable<SSRManifest['experimentalQueuedRendering']>): number;
12
- export declare function queueContentCache(config: NonNullable<SSRManifest['experimentalQueuedRendering']>): boolean;
13
12
  export declare function queueRenderingEnabled(config: SSRManifest['experimentalQueuedRendering']): boolean;
@@ -101,9 +101,6 @@ function deserializeRouteInfo(rawRouteInfo) {
101
101
  function queuePoolSize(config) {
102
102
  return config?.poolSize ?? 1e3;
103
103
  }
104
- function queueContentCache(config) {
105
- return config?.contentCache ?? false;
106
- }
107
104
  function queueRenderingEnabled(config) {
108
105
  return config?.enabled ?? false;
109
106
  }
@@ -111,7 +108,6 @@ export {
111
108
  deserializeManifest,
112
109
  deserializeRouteData,
113
110
  deserializeRouteInfo,
114
- queueContentCache,
115
111
  queuePoolSize,
116
112
  queueRenderingEnabled,
117
113
  serializeRouteData,
@@ -25,9 +25,11 @@ interface NodeRequest extends IncomingMessage {
25
25
  * })
26
26
  * ```
27
27
  */
28
- export declare function createRequest(req: NodeRequest, { skipBody, allowedDomains, }?: {
28
+ export declare function createRequest(req: NodeRequest, { skipBody, allowedDomains, bodySizeLimit, port: serverPort, }?: {
29
29
  skipBody?: boolean;
30
30
  allowedDomains?: Partial<RemotePattern>[];
31
+ bodySizeLimit?: number;
32
+ port?: number;
31
33
  }): Request;
32
34
  /**
33
35
  * Streams a web-standard Response into a NodeJS Server Response.
@@ -7,7 +7,9 @@ import { App } from "./app.js";
7
7
  import { validateForwardedHeaders, validateHost } from "./validate-headers.js";
8
8
  function createRequest(req, {
9
9
  skipBody = false,
10
- allowedDomains = []
10
+ allowedDomains = [],
11
+ bodySizeLimit,
12
+ port: serverPort
11
13
  } = {}) {
12
14
  const controller = new AbortController();
13
15
  const isEncrypted = "encrypted" in req.socket && req.socket.encrypted;
@@ -29,7 +31,7 @@ function createRequest(req, {
29
31
  allowedDomains
30
32
  );
31
33
  const hostname = validated.host ?? validatedHostname ?? "localhost";
32
- const port = validated.port;
34
+ const port = validated.port ?? (!validated.host && !validatedHostname && serverPort ? String(serverPort) : void 0);
33
35
  let url;
34
36
  try {
35
37
  const hostnamePort = getHostnamePort(hostname, port);
@@ -45,7 +47,7 @@ function createRequest(req, {
45
47
  };
46
48
  const bodyAllowed = options.method !== "HEAD" && options.method !== "GET" && skipBody === false;
47
49
  if (bodyAllowed) {
48
- Object.assign(options, makeRequestBody(req));
50
+ Object.assign(options, makeRequestBody(req, bodySizeLimit));
49
51
  }
50
52
  const request = new Request(url, options);
51
53
  const socket = getRequestSocket(req);
@@ -210,7 +212,7 @@ function makeRequestHeaders(req) {
210
212
  }
211
213
  return headers;
212
214
  }
213
- function makeRequestBody(req) {
215
+ function makeRequestBody(req, bodySizeLimit) {
214
216
  if (req.body !== void 0) {
215
217
  if (typeof req.body === "string" && req.body.length > 0) {
216
218
  return { body: Buffer.from(req.body) };
@@ -219,23 +221,35 @@ function makeRequestBody(req) {
219
221
  return { body: Buffer.from(JSON.stringify(req.body)) };
220
222
  }
221
223
  if (typeof req.body === "object" && req.body !== null && typeof req.body[Symbol.asyncIterator] !== "undefined") {
222
- return asyncIterableToBodyProps(req.body);
224
+ return asyncIterableToBodyProps(req.body, bodySizeLimit);
223
225
  }
224
226
  }
225
- return asyncIterableToBodyProps(req);
227
+ return asyncIterableToBodyProps(req, bodySizeLimit);
226
228
  }
227
- function asyncIterableToBodyProps(iterable) {
229
+ function asyncIterableToBodyProps(iterable, bodySizeLimit) {
230
+ const source = bodySizeLimit != null ? limitAsyncIterable(iterable, bodySizeLimit) : iterable;
228
231
  return {
229
232
  // Node uses undici for the Request implementation. Undici accepts
230
233
  // a non-standard async iterable for the body.
231
234
  // @ts-expect-error
232
- body: iterable,
235
+ body: source,
233
236
  // The duplex property is required when using a ReadableStream or async
234
237
  // iterable for the body. The type definitions do not include the duplex
235
238
  // property because they are not up-to-date.
236
239
  duplex: "half"
237
240
  };
238
241
  }
242
+ async function* limitAsyncIterable(iterable, limit) {
243
+ let received = 0;
244
+ for await (const chunk of iterable) {
245
+ const byteLength = chunk instanceof Uint8Array ? chunk.byteLength : typeof chunk === "string" ? Buffer.byteLength(chunk) : 0;
246
+ received += byteLength;
247
+ if (received > limit) {
248
+ throw new Error(`Body size limit exceeded: received more than ${limit} bytes`);
249
+ }
250
+ yield chunk;
251
+ }
252
+ }
239
253
  function getAbortControllerCleanup(req) {
240
254
  if (!req) return void 0;
241
255
  const cleanup = Reflect.get(req, nodeRequestAbortControllerCleanupSymbol);
@@ -64,7 +64,7 @@ export type SSRManifest = {
64
64
  enabled: boolean;
65
65
  /** Node pool size for memory reuse (default: 1000, set to 0 to disable pooling) */
66
66
  poolSize?: number;
67
- /** Whether to enable HTMLString caching (default: true) */
67
+ /** Whether to enable HTMLString caching for deduplicating repeated HTML fragments (default: true) */
68
68
  contentCache?: boolean;
69
69
  };
70
70
  assetsPrefix?: AssetsPrefix;
@@ -106,6 +106,7 @@ export type SSRManifest = {
106
106
  checkOrigin: boolean;
107
107
  allowedDomains?: Partial<RemotePattern>[];
108
108
  actionBodySizeLimit: number;
109
+ serverIslandBodySizeLimit: number;
109
110
  sessionConfig?: SSRManifestSession;
110
111
  cacheConfig?: SSRManifestCache;
111
112
  cacheDir: URL;
@@ -1,7 +1,7 @@
1
1
  import { type RemotePattern } from '@astrojs/internal-helpers/remote';
2
2
  /**
3
3
  * Validate a host against allowedDomains.
4
- * Returns the host only if it matches an allowed pattern, otherwise undefined.
4
+ * Returns the host only if it matches an allowed pattern; otherwise, undefined.
5
5
  * This prevents SSRF attacks by ensuring the Host header is trusted.
6
6
  */
7
7
  export declare function validateHost(host: string | undefined, protocol: string, allowedDomains?: Partial<RemotePattern>[]): string | undefined;
@@ -50,8 +50,6 @@ function validateForwardedHeaders(forwardedProtocol, forwardedHost, forwardedPor
50
50
  } else if (/^https?$/.test(forwardedProtocol)) {
51
51
  result.protocol = forwardedProtocol;
52
52
  }
53
- } else if (/^https?$/.test(forwardedProtocol)) {
54
- result.protocol = forwardedProtocol;
55
53
  }
56
54
  }
57
55
  if (forwardedPort && allowedDomains && allowedDomains.length > 0) {
@@ -130,7 +130,7 @@ export declare abstract class Pipeline {
130
130
  getServerIslands(): Promise<ServerIslandMappings>;
131
131
  getAction(path: string): Promise<ActionClient<unknown, ActionAccept, $ZodType>>;
132
132
  getModuleForRoute(route: RouteData): Promise<SinglePageBuiltModule>;
133
- createNodePool(poolSize: number, contentCache: boolean, stats: boolean): NodePool;
133
+ createNodePool(poolSize: number, stats: boolean): NodePool;
134
134
  createStringCache(): HTMLStringCache;
135
135
  }
136
136
  export interface HeadElements extends Pick<SSRResult, 'scripts' | 'styles' | 'links'> {
@@ -41,7 +41,6 @@ class Pipeline {
41
41
  if (manifest.experimentalQueuedRendering.enabled) {
42
42
  this.nodePool = this.createNodePool(
43
43
  manifest.experimentalQueuedRendering.poolSize ?? 1e3,
44
- manifest.experimentalQueuedRendering.contentCache ?? false,
45
44
  false
46
45
  );
47
46
  if (manifest.experimentalQueuedRendering.contentCache) {
@@ -172,8 +171,8 @@ class Pipeline {
172
171
  );
173
172
  }
174
173
  }
175
- createNodePool(poolSize, contentCache, stats) {
176
- return new NodePool(poolSize, contentCache, stats);
174
+ createNodePool(poolSize, stats) {
175
+ return new NodePool(poolSize, stats);
177
176
  }
178
177
  createStringCache() {
179
178
  return new HTMLStringCache(1e3);
@@ -4,7 +4,7 @@ import type { RouteData } from '../../types/public/internal.js';
4
4
  export declare function getOutFolder(astroSettings: AstroSettings, pathname: string, routeData: RouteData): URL;
5
5
  export declare function getOutFile(buildFormat: NonNullable<AstroConfig['build']>['format'], outFolder: URL, pathname: string, routeData: RouteData): URL;
6
6
  /**
7
- * Ensures the `outDir` is within `process.cwd()`. If not it will fallback to `<cwd>/.astro`.
7
+ * Ensures the `outDir` is within `process.cwd()`. If not it will fall back to `<cwd>/.astro`.
8
8
  * This is used for static `ssrBuild` so the output can access node_modules when we import
9
9
  * the output files. A hardcoded fallback dir is fine as it would be cleaned up after build.
10
10
  */
@@ -37,7 +37,9 @@ class BuildPipeline extends Pipeline {
37
37
  this.defaultRoutes = defaultRoutes;
38
38
  if (queueRenderingEnabled(this.manifest.experimentalQueuedRendering)) {
39
39
  this.nodePool = newNodePool(this.manifest.experimentalQueuedRendering);
40
- this.htmlStringCache = new HTMLStringCache(1e3);
40
+ if (this.manifest.experimentalQueuedRendering.contentCache) {
41
+ this.htmlStringCache = new HTMLStringCache(1e3);
42
+ }
41
43
  }
42
44
  }
43
45
  internals;