astro 4.1.3 → 4.2.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 (46) hide show
  1. package/dist/@types/astro.d.ts +101 -0
  2. package/dist/cli/preferences/index.js +1 -4
  3. package/dist/content/types-generator.js +0 -26
  4. package/dist/content/utils.d.ts +1 -1
  5. package/dist/content/utils.js +3 -10
  6. package/dist/core/app/createOutgoingHttpHeaders.d.ts +9 -0
  7. package/dist/core/app/createOutgoingHttpHeaders.js +19 -0
  8. package/dist/core/app/index.d.ts +38 -1
  9. package/dist/core/app/index.js +38 -8
  10. package/dist/core/app/node.d.ts +42 -10
  11. package/dist/core/app/node.js +87 -46
  12. package/dist/core/app/types.d.ts +2 -1
  13. package/dist/core/build/generate.js +1 -2
  14. package/dist/core/config/schema.d.ts +146 -66
  15. package/dist/core/config/schema.js +24 -6
  16. package/dist/core/config/vite-load.js +1 -1
  17. package/dist/core/constants.js +1 -1
  18. package/dist/core/dev/dev.js +1 -1
  19. package/dist/core/endpoint/index.d.ts +2 -1
  20. package/dist/core/errors/dev/vite.js +5 -11
  21. package/dist/core/errors/overlay.js +2 -2
  22. package/dist/core/logger/node.js +1 -1
  23. package/dist/core/logger/vite.d.ts +0 -1
  24. package/dist/core/logger/vite.js +1 -2
  25. package/dist/core/messages.js +2 -2
  26. package/dist/core/render/context.d.ts +3 -2
  27. package/dist/core/render/context.js +1 -1
  28. package/dist/core/render/result.d.ts +2 -1
  29. package/dist/core/routing/manifest/create.d.ts +1 -1
  30. package/dist/core/routing/manifest/create.js +166 -116
  31. package/dist/core/sync/index.js +1 -1
  32. package/dist/i18n/index.d.ts +3 -2
  33. package/dist/i18n/index.js +4 -4
  34. package/dist/i18n/middleware.js +35 -19
  35. package/dist/prefetch/index.js +17 -1
  36. package/dist/prefetch/vite-plugin-prefetch.js +4 -1
  37. package/dist/runtime/client/dev-toolbar/apps/audit/a11y.js +52 -4
  38. package/dist/runtime/server/render/util.js +1 -1
  39. package/dist/vite-plugin-astro/compile.js +0 -4
  40. package/dist/vite-plugin-astro/hmr.d.ts +3 -2
  41. package/dist/vite-plugin-astro/hmr.js +20 -41
  42. package/dist/vite-plugin-astro/index.js +24 -1
  43. package/dist/vite-plugin-astro/query.d.ts +0 -1
  44. package/dist/vite-plugin-astro/query.js +0 -5
  45. package/dist/vite-plugin-markdown/images.js +46 -19
  46. package/package.json +6 -5
@@ -1374,6 +1374,34 @@ export interface AstroUserConfig {
1374
1374
  * Localized folders are used for every language, including the default.
1375
1375
  */
1376
1376
  prefixDefaultLocale: boolean;
1377
+ /**
1378
+ * @docs
1379
+ * @name i18n.routing.redirectToDefaultLocale
1380
+ * @kind h4
1381
+ * @type {boolean}
1382
+ * @default `true`
1383
+ * @version 4.2.0
1384
+ * @description
1385
+ *
1386
+ * Configures whether or not the home URL (`/`) generated by `src/pages/index.astro`
1387
+ * will redirect to `/[defaultLocale]` when `prefixDefaultLocale: true` is set.
1388
+ *
1389
+ * Set `redirectToDefaultLocale: false` to disable this automatic redirection at the root of your site:
1390
+ * ```js
1391
+ * // astro.config.mjs
1392
+ * export default defineConfig({
1393
+ * i18n:{
1394
+ * defaultLocale: "en",
1395
+ * locales: ["en", "fr"],
1396
+ * routing: {
1397
+ * prefixDefaultLocale: true,
1398
+ * redirectToDefaultLocale: false
1399
+ * }
1400
+ * }
1401
+ * })
1402
+ *```
1403
+ * */
1404
+ redirectToDefaultLocale: boolean;
1377
1405
  /**
1378
1406
  * @name i18n.routing.strategy
1379
1407
  * @type {"pathname"}
@@ -1444,6 +1472,70 @@ export interface AstroUserConfig {
1444
1472
  * ```
1445
1473
  */
1446
1474
  contentCollectionCache?: boolean;
1475
+ /**
1476
+ * @docs
1477
+ * @name experimental.clientPrerender
1478
+ * @type {boolean}
1479
+ * @default `false`
1480
+ * @version 4.2.0
1481
+ * @description
1482
+ * Enables pre-rendering your prefetched pages on the client in supported browsers.
1483
+ *
1484
+ * This feature uses the experimental [Speculation Rules Web API](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API) and overrides the default `prefetch` behavior globally to prerender links on the client.
1485
+ * You may wish to review the [possible risks when prerendering on the client](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API#unsafe_prefetching) before enabling this feature.
1486
+ *
1487
+ * Enable client side prerendering in your `astro.config.mjs` along with any desired `prefetch` configuration options:
1488
+ *
1489
+ * ```js
1490
+ * // astro.config.mjs
1491
+ * {
1492
+ * prefetch: {
1493
+ * prefetchAll: true,
1494
+ * defaultStrategy: 'viewport',
1495
+ * },
1496
+ * experimental: {
1497
+ * clientPrerender: true,
1498
+ * },
1499
+ * }
1500
+ * ```
1501
+ *
1502
+ * Continue to use the `data-astro-prefetch` attribute on any `<a />` link on your site to opt in to prefetching.
1503
+ * Instead of appending a `<link>` tag to the head of the document or fetching the page with JavaScript, a `<script>` tag will be appended with the corresponding speculation rules.
1504
+ *
1505
+ * Client side prerendering requires browser support. If the Speculation Rules API is not supported, `prefetch` will fallback to the supported strategy.
1506
+ *
1507
+ * See the [Prefetch Guide](https://docs.astro.build/en/guides/prefetch/) for more `prefetch` options and usage.
1508
+ */
1509
+ clientPrerender?: boolean;
1510
+ /**
1511
+ * @docs
1512
+ * @name experimental.globalRoutePriority
1513
+ * @type {boolean}
1514
+ * @default `false`
1515
+ * @version 4.2.0
1516
+ * @description
1517
+ *
1518
+ * Prioritizes redirects and injected routes equally alongside file-based project routes, following the same [route priority order rules](https://docs.astro.build/en/core-concepts/routing/#route-priority-order) for all routes.
1519
+ *
1520
+ * This allows more control over routing in your project by not automatically prioritizing certain types of routes, and standardizes the route priority ordering for all routes.
1521
+ *
1522
+ * The following example shows which route will build certain page URLs when file-based routes, injected routes, and redirects are combined as shown below:
1523
+ * - File-based route: `/blog/post/[pid]`
1524
+ * - File-based route: `/[page]`
1525
+ * - Injected route: `/blog/[...slug]`
1526
+ * - Redirect: `/blog/tags/[tag]` -> `/[tag]`
1527
+ * - Redirect: `/posts` -> `/blog`
1528
+ *
1529
+ * With `experimental.globalRoutingPriority` enabled (instead of Astro 4.0 default route priority order):
1530
+ *
1531
+ * - `/blog/tags/astro` is built by the redirect to `/tags/[tag]` (instead of the injected route `/blog/[...slug]`)
1532
+ * - `/blog/post/0` is built by the file-based route `/blog/post/[pid]` (instead of the injected route `/blog/[...slug]`)
1533
+ * - `/posts` is built by the redirect to `/blog` (instead of the file-based route `/[page]`)
1534
+ *
1535
+ *
1536
+ * In the event of route collisions, where two routes of equal route priority attempt to build the same URL, Astro will log a warning identifying the conflicting routes.
1537
+ */
1538
+ globalRoutePriority?: boolean;
1447
1539
  };
1448
1540
  }
1449
1541
  /**
@@ -1454,6 +1546,14 @@ export interface AstroUserConfig {
1454
1546
  * - "page-ssr": Injected into the frontmatter of every Astro page. Processed & resolved by Vite.
1455
1547
  */
1456
1548
  export type InjectedScriptStage = 'before-hydration' | 'head-inline' | 'page' | 'page-ssr';
1549
+ /**
1550
+ * IDs for different priorities of injected routes and redirects:
1551
+ * - "normal": Merge with discovered file-based project routes, behaving the same as if the route
1552
+ * was defined as a file in the project.
1553
+ * - "legacy": Use the old ordering of routes. Inject routes will override any file-based project route,
1554
+ * and redirects will be overridden by any project route on conflict.
1555
+ */
1556
+ export type RoutePriorityOverride = 'normal' | 'legacy';
1457
1557
  export interface InjectedRoute {
1458
1558
  pattern: string;
1459
1559
  entrypoint: string;
@@ -2170,6 +2270,7 @@ export interface RoutePart {
2170
2270
  type RedirectConfig = string | {
2171
2271
  status: ValidRedirectStatus;
2172
2272
  destination: string;
2273
+ priority?: RoutePriorityOverride;
2173
2274
  };
2174
2275
  export interface RouteData {
2175
2276
  route: string;
@@ -5,10 +5,7 @@ import { resolveConfig } from "../../core/config/config.js";
5
5
  import { createSettings } from "../../core/config/settings.js";
6
6
  import * as msg from "../../core/messages.js";
7
7
  import { DEFAULT_PREFERENCES } from "../../preferences/defaults.js";
8
- import {
9
- coerce,
10
- isValidKey
11
- } from "../../preferences/index.js";
8
+ import { coerce, isValidKey } from "../../preferences/index.js";
12
9
  import { createLoggerFromFlags, flagsToAstroInlineConfig } from "../flags.js";
13
10
  import { flattie } from "flattie";
14
11
  import { formatWithOptions } from "node:util";
@@ -18,8 +18,6 @@ import {
18
18
  getEntryType,
19
19
  reloadContentConfigObserver
20
20
  } from "./utils.js";
21
- class UnsupportedFileTypeError extends Error {
22
- }
23
21
  async function createContentTypesGenerator({
24
22
  contentConfigObserver,
25
23
  fs,
@@ -98,20 +96,6 @@ async function createContentTypesGenerator({
98
96
  await reloadContentConfigObserver({ fs, settings, viteServer });
99
97
  return { shouldGenerateTypes: true };
100
98
  }
101
- if (fileType === "unsupported") {
102
- if (event.name === "unlink") {
103
- return { shouldGenerateTypes: false };
104
- }
105
- const { id: id2 } = getContentEntryIdAndSlug({
106
- entry: event.entry,
107
- contentDir: contentPaths.contentDir,
108
- collection: ""
109
- });
110
- return {
111
- shouldGenerateTypes: false,
112
- error: new UnsupportedFileTypeError(id2)
113
- };
114
- }
115
99
  const { entry } = event;
116
100
  const { contentDir } = contentPaths;
117
101
  const collection = getEntryCollectionName({ entry, contentDir });
@@ -251,16 +235,6 @@ async function createContentTypesGenerator({
251
235
  eventResponses.push(response);
252
236
  }
253
237
  events = [];
254
- for (const response of eventResponses) {
255
- if (response.error instanceof UnsupportedFileTypeError) {
256
- logger.warn(
257
- "content",
258
- `Unsupported file type ${bold(
259
- response.error.message
260
- )} found. Prefix filename with an underscore (\`_\`) to ignore.`
261
- );
262
- }
263
- }
264
238
  const observable = contentConfigObserver.get();
265
239
  if (eventResponses.some((r) => r.shouldGenerateTypes)) {
266
240
  await writeContentFiles({
@@ -117,7 +117,7 @@ export declare function getContentEntryIdAndSlug({ entry, contentDir, collection
117
117
  id: string;
118
118
  slug: string;
119
119
  };
120
- export declare function getEntryType(entryPath: string, paths: Pick<ContentPaths, 'config' | 'contentDir'>, contentFileExts: string[], dataFileExts: string[]): 'content' | 'data' | 'config' | 'ignored' | 'unsupported';
120
+ export declare function getEntryType(entryPath: string, paths: Pick<ContentPaths, 'config' | 'contentDir'>, contentFileExts: string[], dataFileExts: string[]): 'content' | 'data' | 'config' | 'ignored';
121
121
  export declare function hasUnderscoreBelowContentDirectoryPath(fileUrl: URL, contentDir: ContentPaths['contentDir']): boolean;
122
122
  export declare function parseFrontmatter(fileContents: string): matter.GrayMatterFile<string>;
123
123
  /**
@@ -5,7 +5,6 @@ import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { normalizePath } from "vite";
7
7
  import { z } from "zod";
8
- import { VALID_INPUT_FORMATS } from "../assets/consts.js";
9
8
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
10
9
  import { formatYAMLException, isYAMLException } from "../core/errors/utils.js";
11
10
  import { CONTENT_FLAGS, CONTENT_TYPES_FILE } from "./consts.js";
@@ -163,9 +162,9 @@ function getRelativeEntryPath(entry, collection, contentDir) {
163
162
  return relativeToCollection;
164
163
  }
165
164
  function getEntryType(entryPath, paths, contentFileExts, dataFileExts) {
166
- const { ext, base } = path.parse(entryPath);
165
+ const { ext } = path.parse(entryPath);
167
166
  const fileUrl = pathToFileURL(entryPath);
168
- if (hasUnderscoreBelowContentDirectoryPath(fileUrl, paths.contentDir) || isOnIgnoreList(base) || isImageAsset(ext)) {
167
+ if (hasUnderscoreBelowContentDirectoryPath(fileUrl, paths.contentDir)) {
169
168
  return "ignored";
170
169
  } else if (contentFileExts.includes(ext)) {
171
170
  return "content";
@@ -174,15 +173,9 @@ function getEntryType(entryPath, paths, contentFileExts, dataFileExts) {
174
173
  } else if (fileUrl.href === paths.config.url.href) {
175
174
  return "config";
176
175
  } else {
177
- return "unsupported";
176
+ return "ignored";
178
177
  }
179
178
  }
180
- function isOnIgnoreList(fileName) {
181
- return [".DS_Store"].includes(fileName);
182
- }
183
- function isImageAsset(fileExt) {
184
- return VALID_INPUT_FORMATS.includes(fileExt.slice(1));
185
- }
186
179
  function hasUnderscoreBelowContentDirectoryPath(fileUrl, contentDir) {
187
180
  const parts = fileUrl.pathname.replace(contentDir.pathname, "").split("/");
188
181
  for (const part of parts) {
@@ -0,0 +1,9 @@
1
+ import type { OutgoingHttpHeaders } from 'node:http';
2
+ /**
3
+ * Takes in a nullable WebAPI Headers object and produces a NodeJS OutgoingHttpHeaders object suitable for usage
4
+ * with ServerResponse.writeHead(..) or ServerResponse.setHeader(..)
5
+ *
6
+ * @param headers WebAPI Headers object
7
+ * @returns {OutgoingHttpHeaders} NodeJS OutgoingHttpHeaders object with multiple set-cookie handled as an array of values
8
+ */
9
+ export declare const createOutgoingHttpHeaders: (headers: Headers | undefined | null) => OutgoingHttpHeaders | undefined;
@@ -0,0 +1,19 @@
1
+ const createOutgoingHttpHeaders = (headers) => {
2
+ if (!headers) {
3
+ return void 0;
4
+ }
5
+ const nodeHeaders = Object.fromEntries(headers.entries());
6
+ if (Object.keys(nodeHeaders).length === 0) {
7
+ return void 0;
8
+ }
9
+ if (headers.has("set-cookie")) {
10
+ const cookieHeaders = headers.getSetCookie();
11
+ if (cookieHeaders.length > 1) {
12
+ nodeHeaders["set-cookie"] = cookieHeaders;
13
+ }
14
+ }
15
+ return nodeHeaders;
16
+ };
17
+ export {
18
+ createOutgoingHttpHeaders
19
+ };
@@ -1,9 +1,34 @@
1
1
  import type { ManifestData, RouteData, SSRManifest } from '../../@types/astro.js';
2
+ import { getSetCookiesFromResponse } from '../cookies/index.js';
2
3
  import { AstroIntegrationLogger } from '../logger/core.js';
3
4
  export { deserializeManifest } from './common.js';
4
5
  export interface RenderOptions {
5
- routeData?: RouteData;
6
+ /**
7
+ * Whether to automatically add all cookies written by `Astro.cookie.set()` to the response headers.
8
+ *
9
+ * When set to `true`, they will be added to the `Set-Cookie` header as comma-separated key=value pairs. You can use the standard `response.headers.getSetCookie()` API to read them individually.
10
+ *
11
+ * When set to `false`, the cookies will only be available from `App.getSetCookieFromResponse(response)`.
12
+ *
13
+ * @default {false}
14
+ */
15
+ addCookieHeader?: boolean;
16
+ /**
17
+ * The client IP address that will be made available as `Astro.clientAddress` in pages, and as `ctx.clientAddress` in API routes and middleware.
18
+ *
19
+ * Default: `request[Symbol.for("astro.clientAddress")]`
20
+ */
21
+ clientAddress?: string;
22
+ /**
23
+ * The mutable object that will be made available as `Astro.locals` in pages, and as `ctx.locals` in API routes and middleware.
24
+ */
6
25
  locals?: object;
26
+ /**
27
+ * **Advanced API**: you probably do not need to use this.
28
+ *
29
+ * Default: `app.match(request)`
30
+ */
31
+ routeData?: RouteData;
7
32
  }
8
33
  export interface RenderErrorOptions {
9
34
  routeData?: RouteData;
@@ -28,4 +53,16 @@ export declare class App {
28
53
  */
29
54
  render(request: Request, routeData?: RouteData, locals?: object): Promise<Response>;
30
55
  setCookieHeaders(response: Response): Generator<string, string[], unknown>;
56
+ /**
57
+ * Reads all the cookies written by `Astro.cookie.set()` onto the passed response.
58
+ * For example,
59
+ * ```ts
60
+ * for (const cookie_ of App.getSetCookieFromResponse(response)) {
61
+ * const cookie: string = cookie_
62
+ * }
63
+ * ```
64
+ * @param response The response to read cookies from.
65
+ * @returns An iterator that yields key-value pairs as equal-sign-separated strings.
66
+ */
67
+ static getSetCookieFromResponse: typeof getSetCookiesFromResponse;
31
68
  }
@@ -19,9 +19,10 @@ import {
19
19
  import { matchRoute } from "../routing/match.js";
20
20
  import { EndpointNotFoundError, SSRRoutePipeline } from "./ssrPipeline.js";
21
21
  import { deserializeManifest } from "./common.js";
22
- const clientLocalsSymbol = Symbol.for("astro.locals");
22
+ const localsSymbol = Symbol.for("astro.locals");
23
+ const clientAddressSymbol = Symbol.for("astro.clientAddress");
23
24
  const responseSentSymbol = Symbol.for("astro.responseSent");
24
- const STATUS_CODES = /* @__PURE__ */ new Set([404, 500]);
25
+ const REROUTABLE_STATUS_CODES = /* @__PURE__ */ new Set([404, 500]);
25
26
  class App {
26
27
  /**
27
28
  * The current environment of the application
@@ -115,7 +116,15 @@ class App {
115
116
  async render(request, routeDataOrOptions, maybeLocals) {
116
117
  let routeData;
117
118
  let locals;
118
- if (routeDataOrOptions && ("routeData" in routeDataOrOptions || "locals" in routeDataOrOptions)) {
119
+ let clientAddress;
120
+ let addCookieHeader;
121
+ if (routeDataOrOptions && ("addCookieHeader" in routeDataOrOptions || "clientAddress" in routeDataOrOptions || "locals" in routeDataOrOptions || "routeData" in routeDataOrOptions)) {
122
+ if ("addCookieHeader" in routeDataOrOptions) {
123
+ addCookieHeader = routeDataOrOptions.addCookieHeader;
124
+ }
125
+ if ("clientAddress" in routeDataOrOptions) {
126
+ clientAddress = routeDataOrOptions.clientAddress;
127
+ }
119
128
  if ("routeData" in routeDataOrOptions) {
120
129
  routeData = routeDataOrOptions.routeData;
121
130
  }
@@ -129,6 +138,12 @@ class App {
129
138
  this.#logRenderOptionsDeprecationWarning();
130
139
  }
131
140
  }
141
+ if (locals) {
142
+ Reflect.set(request, localsSymbol, locals);
143
+ }
144
+ if (clientAddress) {
145
+ Reflect.set(request, clientAddressSymbol, clientAddress);
146
+ }
132
147
  if (request.url !== collapseDuplicateSlashes(request.url)) {
133
148
  request = new Request(collapseDuplicateSlashes(request.url), request);
134
149
  }
@@ -138,7 +153,6 @@ class App {
138
153
  if (!routeData) {
139
154
  return this.#renderError(request, { status: 404 });
140
155
  }
141
- Reflect.set(request, clientLocalsSymbol, locals ?? {});
142
156
  const pathname = this.#getPathnameFromRequest(request);
143
157
  const defaultStatus = this.#getDefaultStatusCode(routeData, pathname);
144
158
  const mod = await this.#getModuleForRoute(routeData);
@@ -153,7 +167,7 @@ class App {
153
167
  );
154
168
  let response;
155
169
  try {
156
- let i18nMiddleware = createI18nMiddleware(
170
+ const i18nMiddleware = createI18nMiddleware(
157
171
  this.#manifest.i18n,
158
172
  this.#manifest.base,
159
173
  this.#manifest.trailingSlash
@@ -180,15 +194,19 @@ class App {
180
194
  }
181
195
  }
182
196
  if (routeData.type === "page" || routeData.type === "redirect") {
183
- if (STATUS_CODES.has(response.status)) {
197
+ if (REROUTABLE_STATUS_CODES.has(response.status)) {
184
198
  return this.#renderError(request, {
185
199
  response,
186
200
  status: response.status
187
201
  });
188
202
  }
189
- Reflect.set(response, responseSentSymbol, true);
190
- return response;
191
203
  }
204
+ if (addCookieHeader) {
205
+ for (const setCookieHeaderValue of App.getSetCookieFromResponse(response)) {
206
+ response.headers.append("set-cookie", setCookieHeaderValue);
207
+ }
208
+ }
209
+ Reflect.set(response, responseSentSymbol, true);
192
210
  return response;
193
211
  }
194
212
  #logRenderOptionsDeprecationWarning() {
@@ -203,6 +221,18 @@ class App {
203
221
  setCookieHeaders(response) {
204
222
  return getSetCookiesFromResponse(response);
205
223
  }
224
+ /**
225
+ * Reads all the cookies written by `Astro.cookie.set()` onto the passed response.
226
+ * For example,
227
+ * ```ts
228
+ * for (const cookie_ of App.getSetCookieFromResponse(response)) {
229
+ * const cookie: string = cookie_
230
+ * }
231
+ * ```
232
+ * @param response The response to read cookies from.
233
+ * @returns An iterator that yields key-value pairs as equal-sign-separated strings.
234
+ */
235
+ static getSetCookieFromResponse = getSetCookiesFromResponse;
206
236
  /**
207
237
  * Creates the render context of the current route
208
238
  */
@@ -1,25 +1,57 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
+ import { App } from './index.js';
3
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
4
  import type { RouteData } from '../../@types/astro.js';
3
5
  import type { RenderOptions } from './index.js';
4
6
  import type { SSRManifest } from './types.js';
5
- import { IncomingMessage } from 'node:http';
6
- import { App } from './index.js';
7
7
  export { apply as applyPolyfills } from '../polyfill.js';
8
- declare class NodeIncomingMessage extends IncomingMessage {
9
- /**
10
- * Allow the request body to be explicitly overridden. For example, this
11
- * is used by the Express JSON middleware.
12
- */
8
+ /**
9
+ * Allow the request body to be explicitly overridden. For example, this
10
+ * is used by the Express JSON middleware.
11
+ */
12
+ interface NodeRequest extends IncomingMessage {
13
13
  body?: unknown;
14
14
  }
15
15
  export declare class NodeApp extends App {
16
- match(req: NodeIncomingMessage | Request): RouteData | undefined;
17
- render(request: NodeIncomingMessage | Request, options?: RenderOptions): Promise<Response>;
16
+ match(req: NodeRequest | Request): RouteData | undefined;
17
+ render(request: NodeRequest | Request, options?: RenderOptions): Promise<Response>;
18
18
  /**
19
19
  * @deprecated Instead of passing `RouteData` and locals individually, pass an object with `routeData` and `locals` properties.
20
20
  * See https://github.com/withastro/astro/pull/9199 for more information.
21
21
  */
22
- render(request: NodeIncomingMessage | Request, routeData?: RouteData, locals?: object): Promise<Response>;
22
+ render(request: NodeRequest | Request, routeData?: RouteData, locals?: object): Promise<Response>;
23
+ /**
24
+ * Converts a NodeJS IncomingMessage into a web standard Request.
25
+ * ```js
26
+ * import { NodeApp } from 'astro/app/node';
27
+ * import { createServer } from 'node:http';
28
+ *
29
+ * const server = createServer(async (req, res) => {
30
+ * const request = NodeApp.createRequest(req);
31
+ * const response = await app.render(request);
32
+ * await NodeApp.writeResponse(response, res);
33
+ * })
34
+ * ```
35
+ */
36
+ static createRequest(req: NodeRequest, { skipBody }?: {
37
+ skipBody?: boolean | undefined;
38
+ }): Request;
39
+ /**
40
+ * Streams a web-standard Response into a NodeJS Server Response.
41
+ * ```js
42
+ * import { NodeApp } from 'astro/app/node';
43
+ * import { createServer } from 'node:http';
44
+ *
45
+ * const server = createServer(async (req, res) => {
46
+ * const request = NodeApp.createRequest(req);
47
+ * const response = await app.render(request);
48
+ * await NodeApp.writeResponse(response, res);
49
+ * })
50
+ * ```
51
+ * @param source WhatWG Response
52
+ * @param destination NodeJS ServerResponse
53
+ */
54
+ static writeResponse(source: Response, destination: ServerResponse): Promise<void>;
23
55
  }
24
56
  export declare function loadManifest(rootFolder: URL): Promise<SSRManifest>;
25
57
  export declare function loadApp(rootFolder: URL): Promise<NodeApp>;
@@ -1,30 +1,95 @@
1
- import * as fs from "node:fs";
2
- import { IncomingMessage } from "node:http";
3
- import { TLSSocket } from "node:tls";
4
- import { deserializeManifest } from "./common.js";
1
+ import fs from "node:fs";
5
2
  import { App } from "./index.js";
3
+ import { deserializeManifest } from "./common.js";
4
+ import { createOutgoingHttpHeaders } from "./createOutgoingHttpHeaders.js";
6
5
  import { apply } from "../polyfill.js";
7
6
  const clientAddressSymbol = Symbol.for("astro.clientAddress");
8
- function createRequestFromNodeRequest(req, options) {
9
- const protocol = req.socket instanceof TLSSocket || req.headers["x-forwarded-proto"] === "https" ? "https" : "http";
10
- const hostname = req.headers.host || req.headers[":authority"];
11
- const url = `${protocol}://${hostname}${req.url}`;
12
- const headers = makeRequestHeaders(req);
13
- const method = req.method || "GET";
14
- let bodyProps = {};
15
- const bodyAllowed = method !== "HEAD" && method !== "GET" && !options?.emptyBody;
16
- if (bodyAllowed) {
17
- bodyProps = makeRequestBody(req);
7
+ class NodeApp extends App {
8
+ match(req) {
9
+ if (!(req instanceof Request)) {
10
+ req = NodeApp.createRequest(req, {
11
+ skipBody: true
12
+ });
13
+ }
14
+ return super.match(req);
15
+ }
16
+ render(req, routeDataOrOptions, maybeLocals) {
17
+ if (!(req instanceof Request)) {
18
+ req = NodeApp.createRequest(req);
19
+ }
20
+ return super.render(req, routeDataOrOptions, maybeLocals);
18
21
  }
19
- const request = new Request(url, {
20
- method,
21
- headers,
22
- ...bodyProps
23
- });
24
- if (req.socket?.remoteAddress) {
25
- Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress);
22
+ /**
23
+ * Converts a NodeJS IncomingMessage into a web standard Request.
24
+ * ```js
25
+ * import { NodeApp } from 'astro/app/node';
26
+ * import { createServer } from 'node:http';
27
+ *
28
+ * const server = createServer(async (req, res) => {
29
+ * const request = NodeApp.createRequest(req);
30
+ * const response = await app.render(request);
31
+ * await NodeApp.writeResponse(response, res);
32
+ * })
33
+ * ```
34
+ */
35
+ static createRequest(req, { skipBody = false } = {}) {
36
+ const protocol = req.headers["x-forwarded-proto"] ?? ("encrypted" in req.socket && req.socket.encrypted ? "https" : "http");
37
+ const hostname = req.headers.host || req.headers[":authority"];
38
+ const url = `${protocol}://${hostname}${req.url}`;
39
+ const options = {
40
+ method: req.method || "GET",
41
+ headers: makeRequestHeaders(req)
42
+ };
43
+ const bodyAllowed = options.method !== "HEAD" && options.method !== "GET" && skipBody === false;
44
+ if (bodyAllowed) {
45
+ Object.assign(options, makeRequestBody(req));
46
+ }
47
+ const request = new Request(url, options);
48
+ if (req.socket?.remoteAddress) {
49
+ Reflect.set(request, clientAddressSymbol, req.socket.remoteAddress);
50
+ }
51
+ return request;
52
+ }
53
+ /**
54
+ * Streams a web-standard Response into a NodeJS Server Response.
55
+ * ```js
56
+ * import { NodeApp } from 'astro/app/node';
57
+ * import { createServer } from 'node:http';
58
+ *
59
+ * const server = createServer(async (req, res) => {
60
+ * const request = NodeApp.createRequest(req);
61
+ * const response = await app.render(request);
62
+ * await NodeApp.writeResponse(response, res);
63
+ * })
64
+ * ```
65
+ * @param source WhatWG Response
66
+ * @param destination NodeJS ServerResponse
67
+ */
68
+ static async writeResponse(source, destination) {
69
+ const { status, headers, body } = source;
70
+ destination.writeHead(status, createOutgoingHttpHeaders(headers));
71
+ if (body) {
72
+ try {
73
+ const reader = body.getReader();
74
+ destination.on("close", () => {
75
+ reader.cancel().catch((err) => {
76
+ console.error(
77
+ `There was an uncaught error in the middle of the stream while rendering ${destination.req.url}.`,
78
+ err
79
+ );
80
+ });
81
+ });
82
+ let result = await reader.read();
83
+ while (!result.done) {
84
+ destination.write(result.value);
85
+ result = await reader.read();
86
+ }
87
+ } catch {
88
+ destination.write("Internal server error");
89
+ }
90
+ }
91
+ destination.end();
26
92
  }
27
- return request;
28
93
  }
29
94
  function makeRequestHeaders(req) {
30
95
  const headers = new Headers();
@@ -65,33 +130,9 @@ function asyncIterableToBodyProps(iterable) {
65
130
  // The duplex property is required when using a ReadableStream or async
66
131
  // iterable for the body. The type definitions do not include the duplex
67
132
  // property because they are not up-to-date.
68
- // @ts-expect-error
69
133
  duplex: "half"
70
134
  };
71
135
  }
72
- class NodeIncomingMessage extends IncomingMessage {
73
- /**
74
- * Allow the request body to be explicitly overridden. For example, this
75
- * is used by the Express JSON middleware.
76
- */
77
- body;
78
- }
79
- class NodeApp extends App {
80
- match(req) {
81
- if (!(req instanceof Request)) {
82
- req = createRequestFromNodeRequest(req, {
83
- emptyBody: true
84
- });
85
- }
86
- return super.match(req);
87
- }
88
- render(req, routeDataOrOptions, maybeLocals) {
89
- if (!(req instanceof Request)) {
90
- req = createRequestFromNodeRequest(req);
91
- }
92
- return super.render(req, routeDataOrOptions, maybeLocals);
93
- }
94
- }
95
136
  async function loadManifest(rootFolder) {
96
137
  const manifestFile = new URL("./manifest.json", rootFolder);
97
138
  const rawManifest = await fs.promises.readFile(manifestFile, "utf-8");
@@ -1,5 +1,6 @@
1
1
  import type { Locales, RouteData, SerializedRouteData, SSRComponentMetadata, SSRLoadedRenderer, SSRResult } from '../../@types/astro.js';
2
2
  import type { SinglePageBuiltModule } from '../build/types.js';
3
+ import type { RoutingStrategies } from '../config/schema.js';
3
4
  export type ComponentPath = string;
4
5
  export type StylesheetAsset = {
5
6
  type: 'inline';
@@ -47,7 +48,7 @@ export type SSRManifest = {
47
48
  };
48
49
  export type SSRManifestI18n = {
49
50
  fallback?: Record<string, string>;
50
- routing?: 'prefix-always' | 'prefix-other-locales';
51
+ routing?: RoutingStrategies;
51
52
  locales: Locales;
52
53
  defaultLocale: string;
53
54
  };
@@ -344,9 +344,8 @@ function getUrlForPath(pathname, base, origin, format, routeType) {
344
344
  return url;
345
345
  }
346
346
  async function generatePath(pathname, pipeline, gopts, route) {
347
- const { mod, scripts: hoistedScripts, styles: _styles, pageData } = gopts;
347
+ const { mod, scripts: hoistedScripts, styles: _styles } = gopts;
348
348
  const manifest = pipeline.getManifest();
349
- const logger = pipeline.getLogger();
350
349
  pipeline.getEnvironment().logger.debug("build", `Generating: ${pathname}`);
351
350
  const links = /* @__PURE__ */ new Set();
352
351
  const scripts = createModuleScriptsSet(