astro 4.2.7 → 4.3.0

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 (44) hide show
  1. package/dist/@types/astro.d.ts +88 -7
  2. package/dist/content/types-generator.js +2 -1
  3. package/dist/content/utils.d.ts +1 -1
  4. package/dist/content/utils.js +23 -11
  5. package/dist/core/app/index.js +55 -2
  6. package/dist/core/app/node.js +2 -2
  7. package/dist/core/app/types.d.ts +2 -1
  8. package/dist/core/build/common.d.ts +3 -3
  9. package/dist/core/build/common.js +20 -2
  10. package/dist/core/build/generate.js +15 -5
  11. package/dist/core/build/plugins/plugin-manifest.js +12 -3
  12. package/dist/core/build/plugins/plugin-ssr.js +8 -1
  13. package/dist/core/build/util.js +1 -0
  14. package/dist/core/config/schema.d.ts +305 -36
  15. package/dist/core/config/schema.js +95 -18
  16. package/dist/core/constants.js +1 -1
  17. package/dist/core/dev/dev.js +1 -1
  18. package/dist/core/dev/restart.js +11 -3
  19. package/dist/core/errors/errors-data.d.ts +10 -0
  20. package/dist/core/errors/errors-data.js +6 -0
  21. package/dist/core/logger/vite.js +7 -1
  22. package/dist/core/messages.d.ts +5 -0
  23. package/dist/core/messages.js +6 -2
  24. package/dist/core/render/context.js +1 -1
  25. package/dist/core/routing/manifest/serialization.js +2 -1
  26. package/dist/i18n/index.d.ts +9 -7
  27. package/dist/i18n/index.js +61 -12
  28. package/dist/i18n/middleware.js +82 -24
  29. package/dist/i18n/vite-plugin-i18n.d.ts +1 -0
  30. package/dist/i18n/vite-plugin-i18n.js +9 -2
  31. package/dist/integrations/astroFeaturesValidation.d.ts +2 -2
  32. package/dist/integrations/astroFeaturesValidation.js +20 -2
  33. package/dist/integrations/index.d.ts +0 -1
  34. package/dist/integrations/index.js +2 -8
  35. package/dist/runtime/server/endpoint.js +3 -1
  36. package/dist/virtual-modules/i18n.js +12 -3
  37. package/dist/vite-plugin-astro-server/plugin.js +2 -1
  38. package/dist/vite-plugin-astro-server/response.js +1 -5
  39. package/dist/vite-plugin-astro-server/route.js +3 -2
  40. package/dist/vite-plugin-markdown/content-entry-type.js +4 -3
  41. package/dist/vite-plugin-markdown/images.js +5 -2
  42. package/dist/vite-plugin-markdown/index.js +3 -26
  43. package/package.json +1 -1
  44. package/types.d.ts +4 -1
@@ -689,14 +689,15 @@ export interface AstroUserConfig {
689
689
  * @default `'directory'`
690
690
  * @description
691
691
  * Control the output file format of each page. This value may be set by an adapter for you.
692
- * - If `'file'`, Astro will generate an HTML file (ex: "/foo.html") for each page.
693
- * - If `'directory'`, Astro will generate a directory with a nested `index.html` file (ex: "/foo/index.html") for each page.
692
+ * - `'file'`: Astro will generate an HTML file named for each page route. (e.g. `src/pages/about.astro` and `src/pages/about/index.astro` both build the file `/about.html`)
693
+ * - `'directory'`: Astro will generate a directory with a nested `index.html` file for each page. (e.g. `src/pages/about.astro` and `src/pages/about/index.astro` both build the file `/about/index.html`)
694
+ * - `'preserve'`: Astro will generate HTML files exactly as they appear in your source folder. (e.g. `src/pages/about.astro` builds `/about.html` and `src/pages/about/index.astro` builds the file `/about/index.html`)
694
695
  *
695
696
  * ```js
696
697
  * {
697
698
  * build: {
698
699
  * // Example: Generate `page.html` instead of `page/index.html` during build.
699
- * format: 'file'
700
+ * format: 'preserve'
700
701
  * }
701
702
  * }
702
703
  * ```
@@ -714,7 +715,7 @@ export interface AstroUserConfig {
714
715
  * - `directory` - Set `trailingSlash: 'always'`
715
716
  * - `file` - Set `trailingSlash: 'never'`
716
717
  */
717
- format?: 'file' | 'directory';
718
+ format?: 'file' | 'directory' | 'preserve';
718
719
  /**
719
720
  * @docs
720
721
  * @name build.client
@@ -1412,6 +1413,44 @@ export interface AstroUserConfig {
1412
1413
  * - `"pathanme": The strategy is applied to the pathname of the URLs
1413
1414
  */
1414
1415
  strategy: 'pathname';
1416
+ /**
1417
+ * @name i18n.domains
1418
+ * @type {Record<string, string> }
1419
+ * @default '{}'
1420
+ * @version 4.3.0
1421
+ * @description
1422
+ *
1423
+ * Configures the URL pattern of one or more supported languages to use a custom domain (or sub-domain).
1424
+ *
1425
+ * When a locale is mapped to a domain, a `/[locale]/` path prefix will not be used.
1426
+ * However, localized folders within `src/pages/` are still required, including for your configured `defaultLocale`.
1427
+ *
1428
+ * Any other locale not configured will default to a localized path-based URL according to your `prefixDefaultLocale` strategy (e.g. `https://example.com/[locale]/blog`).
1429
+ *
1430
+ * ```js
1431
+ * //astro.config.mjs
1432
+ * export default defineConfig({
1433
+ * site: "https://example.com",
1434
+ * output: "server", // required, with no prerendered pages
1435
+ * adapter: node({
1436
+ * mode: 'standalone',
1437
+ * }),
1438
+ * i18n: {
1439
+ * defaultLocale: "en",
1440
+ * locales: ["en", "fr", "pt-br", "es"],
1441
+ * prefixDefaultLocale: false,
1442
+ * domains: {
1443
+ * fr: "https://fr.example.com",
1444
+ * es: "https://example.es"
1445
+ * },
1446
+ * })
1447
+ * ```
1448
+ *
1449
+ * Both page routes built and URLs returned by the `astro:i18n` helper functions [`getAbsoluteLocaleUrl()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurl) and [`getAbsoluteLocaleUrlList()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurllist) will use the options set in `i18n.domains`.
1450
+ *
1451
+ * See the [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains) for more details, including the limitations of this feature.
1452
+ */
1453
+ domains?: Record<string, string>;
1415
1454
  };
1416
1455
  };
1417
1456
  /** ⚠️ WARNING: SUBJECT TO CHANGE */
@@ -1538,6 +1577,47 @@ export interface AstroUserConfig {
1538
1577
  * 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.
1539
1578
  */
1540
1579
  globalRoutePriority?: boolean;
1580
+ /**
1581
+ * @docs
1582
+ * @name experimental.i18nDomains
1583
+ * @type {boolean}
1584
+ * @default `false`
1585
+ * @version 4.3.0
1586
+ * @description
1587
+ *
1588
+ * Enables domain support for the [experimental `domains` routing strategy](https://docs.astro.build/en/guides/internationalization/#domains-experimental) which allows you to configure the URL pattern of one or more supported languages to use a custom domain (or sub-domain).
1589
+ *
1590
+ * When a locale is mapped to a domain, a `/[locale]/` path prefix will not be used. However, localized folders within `src/pages/` are still required, including for your configured `defaultLocale`.
1591
+ *
1592
+ * Any other locale not configured will default to a localized path-based URL according to your `prefixDefaultLocale` strategy (e.g. `https://example.com/[locale]/blog`).
1593
+ *
1594
+ * ```js
1595
+ * //astro.config.mjs
1596
+ * export default defineConfig({
1597
+ * site: "https://example.com",
1598
+ * output: "server", // required, with no prerendered pages
1599
+ * adapter: node({
1600
+ * mode: 'standalone',
1601
+ * }),
1602
+ * i18n: {
1603
+ * defaultLocale: "en",
1604
+ * locales: ["en", "fr", "pt-br", "es"],
1605
+ * prefixDefaultLocale: false,
1606
+ * domains: {
1607
+ * fr: "https://fr.example.com",
1608
+ * es: "https://example.es"
1609
+ * },
1610
+ * experimental: {
1611
+ * i18nDomains: true
1612
+ * }
1613
+ * })
1614
+ * ```
1615
+ *
1616
+ * Both page routes built and URLs returned by the `astro:i18n` helper functions [`getAbsoluteLocaleUrl()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurl) and [`getAbsoluteLocaleUrlList()`](https://docs.astro.build/en/guides/internationalization/#getabsolutelocaleurllist) will use the options set in `i18n.domains`.
1617
+ *
1618
+ * See the [Internationalization Guide](https://docs.astro.build/en/guides/internationalization/#domains-experimental) for more details, including the limitations of this experimental feature.
1619
+ */
1620
+ i18nDomains?: boolean;
1541
1621
  };
1542
1622
  }
1543
1623
  /**
@@ -1918,7 +1998,7 @@ export type AstroFeatureMap = {
1918
1998
  /**
1919
1999
  * List of features that orbit around the i18n routing
1920
2000
  */
1921
- i18n?: AstroInternationalizationFeature;
2001
+ i18nDomains?: SupportsKind;
1922
2002
  };
1923
2003
  export interface AstroAssetsFeature {
1924
2004
  supportKind?: SupportsKind;
@@ -1933,9 +2013,9 @@ export interface AstroAssetsFeature {
1933
2013
  }
1934
2014
  export interface AstroInternationalizationFeature {
1935
2015
  /**
1936
- * Whether the adapter is able to detect the language of the browser, usually using the `Accept-Language` header.
2016
+ * The adapter should be able to create the proper redirects
1937
2017
  */
1938
- detectBrowserLanguage?: SupportsKind;
2018
+ domains?: SupportsKind;
1939
2019
  }
1940
2020
  export type Locales = (string | {
1941
2021
  codes: string[];
@@ -2288,6 +2368,7 @@ export interface RouteData {
2288
2368
  redirect?: RedirectConfig;
2289
2369
  redirectRoute?: RouteData;
2290
2370
  fallbackRoutes: RouteData[];
2371
+ isIndex: boolean;
2291
2372
  }
2292
2373
  export type RedirectRouteData = RouteData & {
2293
2374
  redirect: string;
@@ -266,7 +266,8 @@ function invalidateVirtualMod(viteServer) {
266
266
  }
267
267
  function normalizeConfigPath(from, to) {
268
268
  const configPath = path.relative(from, to).replace(/\.ts$/, ".js");
269
- return `"${isRelativePath(configPath) ? "" : "./"}${configPath}"`;
269
+ const normalizedPath = configPath.replaceAll("\\", "/");
270
+ return `"${isRelativePath(configPath) ? "" : "./"}${normalizedPath}"`;
270
271
  }
271
272
  async function writeContentFiles({
272
273
  fs,
@@ -119,7 +119,7 @@ export declare function getContentEntryIdAndSlug({ entry, contentDir, collection
119
119
  };
120
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
- export declare function parseFrontmatter(fileContents: string): matter.GrayMatterFile<string>;
122
+ export declare function safeParseFrontmatter(source: string, id?: string): matter.GrayMatterFile<string>;
123
123
  /**
124
124
  * The content config is loaded separately from other `src/` files.
125
125
  * This global observable lets dependent plugins (like the content flag plugin)
@@ -6,7 +6,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { normalizePath } from "vite";
7
7
  import { z } from "zod";
8
8
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
9
- import { formatYAMLException, isYAMLException } from "../core/errors/utils.js";
9
+ import { MarkdownError } from "../core/errors/index.js";
10
+ import { isYAMLException } from "../core/errors/utils.js";
10
11
  import { CONTENT_FLAGS, CONTENT_TYPES_FILE } from "./consts.js";
11
12
  import { errorMap } from "./error-map.js";
12
13
  import { createImage } from "./runtime-assets.js";
@@ -199,16 +200,27 @@ function getYAMLErrorLine(rawData, objectKey) {
199
200
  const numNewlinesBeforeKey = dataBeforeKey.split("\n").length;
200
201
  return numNewlinesBeforeKey;
201
202
  }
202
- function parseFrontmatter(fileContents) {
203
+ function safeParseFrontmatter(source, id) {
203
204
  try {
204
- matter.clearCache();
205
- return matter(fileContents);
206
- } catch (e) {
207
- if (isYAMLException(e)) {
208
- throw formatYAMLException(e);
209
- } else {
210
- throw e;
205
+ return matter(source);
206
+ } catch (err) {
207
+ const markdownError = new MarkdownError({
208
+ name: "MarkdownError",
209
+ message: err.message,
210
+ stack: err.stack,
211
+ location: id ? {
212
+ file: id
213
+ } : void 0
214
+ });
215
+ if (isYAMLException(err)) {
216
+ markdownError.setLocation({
217
+ file: id,
218
+ line: err.mark.line,
219
+ column: err.mark.column
220
+ });
221
+ markdownError.setMessage(err.reason);
211
222
  }
223
+ throw markdownError;
212
224
  }
213
225
  }
214
226
  const globalContentConfigObserver = contentObservable({ status: "init" });
@@ -356,6 +368,6 @@ export {
356
368
  loadContentConfig,
357
369
  msg,
358
370
  parseEntrySlug,
359
- parseFrontmatter,
360
- reloadContentConfigObserver
371
+ reloadContentConfigObserver,
372
+ safeParseFrontmatter
361
373
  };
@@ -5,7 +5,9 @@ import { consoleLogDestination } from "../logger/console.js";
5
5
  import { AstroIntegrationLogger, Logger } from "../logger/core.js";
6
6
  import { sequence } from "../middleware/index.js";
7
7
  import {
8
+ appendForwardSlash,
8
9
  collapseDuplicateSlashes,
10
+ joinPaths,
9
11
  prependForwardSlash,
10
12
  removeTrailingForwardSlash
11
13
  } from "../path.js";
@@ -19,6 +21,7 @@ import {
19
21
  } from "../render/ssr-element.js";
20
22
  import { matchRoute } from "../routing/match.js";
21
23
  import { SSRRoutePipeline } from "./ssrPipeline.js";
24
+ import { normalizeTheLocale } from "../../i18n/index.js";
22
25
  import { deserializeManifest } from "./common.js";
23
26
  const localsSymbol = Symbol.for("astro.locals");
24
27
  const clientAddressSymbol = Symbol.for("astro.clientAddress");
@@ -108,12 +111,62 @@ class App {
108
111
  const url = new URL(request.url);
109
112
  if (this.#manifest.assets.has(url.pathname))
110
113
  return void 0;
111
- const pathname = prependForwardSlash(this.removeBase(url.pathname));
112
- const routeData = matchRoute(pathname, this.#manifestData);
114
+ let pathname = this.#computePathnameFromDomain(request);
115
+ if (!pathname) {
116
+ pathname = prependForwardSlash(this.removeBase(url.pathname));
117
+ }
118
+ let routeData = matchRoute(pathname, this.#manifestData);
113
119
  if (!routeData || routeData.prerender)
114
120
  return void 0;
115
121
  return routeData;
116
122
  }
123
+ #computePathnameFromDomain(request) {
124
+ let pathname = void 0;
125
+ const url = new URL(request.url);
126
+ if (this.#manifest.i18n && (this.#manifest.i18n.routing === "domains-prefix-always" || this.#manifest.i18n.routing === "domains-prefix-other-locales" || this.#manifest.i18n.routing === "domains-prefix-other-no-redirect")) {
127
+ let host = request.headers.get("X-Forwarded-Host");
128
+ let protocol = request.headers.get("X-Forwarded-Proto");
129
+ if (protocol) {
130
+ protocol = protocol + ":";
131
+ } else {
132
+ protocol = url.protocol;
133
+ }
134
+ if (!host) {
135
+ host = request.headers.get("Host");
136
+ }
137
+ if (host && protocol) {
138
+ host = host.split(":")[0];
139
+ try {
140
+ let locale;
141
+ const hostAsUrl = new URL(`${protocol}//${host}`);
142
+ for (const [domainKey, localeValue] of Object.entries(
143
+ this.#manifest.i18n.domainLookupTable
144
+ )) {
145
+ const domainKeyAsUrl = new URL(domainKey);
146
+ if (hostAsUrl.host === domainKeyAsUrl.host && hostAsUrl.protocol === domainKeyAsUrl.protocol) {
147
+ locale = localeValue;
148
+ break;
149
+ }
150
+ }
151
+ if (locale) {
152
+ pathname = prependForwardSlash(
153
+ joinPaths(normalizeTheLocale(locale), this.removeBase(url.pathname))
154
+ );
155
+ if (url.pathname.endsWith("/")) {
156
+ pathname = appendForwardSlash(pathname);
157
+ }
158
+ }
159
+ } catch (e) {
160
+ this.#logger.error(
161
+ "router",
162
+ `Astro tried to parse ${protocol}//${host} as an URL, but it threw a parsing error. Check the X-Forwarded-Host and X-Forwarded-Proto headers.`
163
+ );
164
+ this.#logger.error("router", `Error: ${e}`);
165
+ }
166
+ }
167
+ }
168
+ return pathname;
169
+ }
117
170
  async render(request, routeDataOrOptions, maybeLocals) {
118
171
  let routeData;
119
172
  let locals;
@@ -84,11 +84,11 @@ class NodeApp extends App {
84
84
  destination.write(result.value);
85
85
  result = await reader.read();
86
86
  }
87
+ destination.end();
87
88
  } catch {
88
- destination.write("Internal server error");
89
+ destination.end("Internal server error");
89
90
  }
90
91
  }
91
- destination.end();
92
92
  }
93
93
  }
94
94
  function makeRequestHeaders(req) {
@@ -32,7 +32,7 @@ export type SSRManifest = {
32
32
  site?: string;
33
33
  base: string;
34
34
  trailingSlash: 'always' | 'never' | 'ignore';
35
- buildFormat: 'file' | 'directory';
35
+ buildFormat: 'file' | 'directory' | 'preserve';
36
36
  compressHTML: boolean;
37
37
  assetsPrefix?: string;
38
38
  renderers: SSRLoadedRenderer[];
@@ -53,6 +53,7 @@ export type SSRManifestI18n = {
53
53
  routing: RoutingStrategies;
54
54
  locales: Locales;
55
55
  defaultLocale: string;
56
+ domainLookupTable: Record<string, string>;
56
57
  };
57
58
  export type SerializedSSRManifest = Omit<SSRManifest, 'middleware' | 'routes' | 'assets' | 'componentMetadata' | 'clientDirectives'> & {
58
59
  routes: SerializedRouteInfo[];
@@ -1,6 +1,6 @@
1
- import type { AstroConfig, RouteType } from '../../@types/astro.js';
2
- export declare function getOutFolder(astroConfig: AstroConfig, pathname: string, routeType: RouteType): URL;
3
- export declare function getOutFile(astroConfig: AstroConfig, outFolder: URL, pathname: string, routeType: RouteType): URL;
1
+ import type { AstroConfig, RouteData } from '../../@types/astro.js';
2
+ export declare function getOutFolder(astroConfig: AstroConfig, pathname: string, routeData: RouteData): URL;
3
+ export declare function getOutFile(astroConfig: AstroConfig, outFolder: URL, pathname: string, routeData: RouteData): URL;
4
4
  /**
5
5
  * Ensures the `outDir` is within `process.cwd()`. If not it will fallback to `<cwd>/.astro`.
6
6
  * This is used for static `ssrBuild` so the output can access node_modules when we import
@@ -10,8 +10,9 @@ function getOutRoot(astroConfig) {
10
10
  return new URL("./", astroConfig.build.client);
11
11
  }
12
12
  }
13
- function getOutFolder(astroConfig, pathname, routeType) {
13
+ function getOutFolder(astroConfig, pathname, routeData) {
14
14
  const outRoot = getOutRoot(astroConfig);
15
+ const routeType = routeData.type;
15
16
  switch (routeType) {
16
17
  case "endpoint":
17
18
  return new URL("." + appendForwardSlash(npath.dirname(pathname)), outRoot);
@@ -29,10 +30,20 @@ function getOutFolder(astroConfig, pathname, routeType) {
29
30
  const d = pathname === "" ? pathname : npath.dirname(pathname);
30
31
  return new URL("." + appendForwardSlash(d), outRoot);
31
32
  }
33
+ case "preserve": {
34
+ let dir;
35
+ if (pathname === "" || routeData.isIndex) {
36
+ dir = pathname;
37
+ } else {
38
+ dir = npath.dirname(pathname);
39
+ }
40
+ return new URL("." + appendForwardSlash(dir), outRoot);
41
+ }
32
42
  }
33
43
  }
34
44
  }
35
- function getOutFile(astroConfig, outFolder, pathname, routeType) {
45
+ function getOutFile(astroConfig, outFolder, pathname, routeData) {
46
+ const routeType = routeData.type;
36
47
  switch (routeType) {
37
48
  case "endpoint":
38
49
  return new URL(npath.basename(pathname), outFolder);
@@ -51,6 +62,13 @@ function getOutFile(astroConfig, outFolder, pathname, routeType) {
51
62
  const baseName = npath.basename(pathname);
52
63
  return new URL("./" + (baseName || "index") + ".html", outFolder);
53
64
  }
65
+ case "preserve": {
66
+ let baseName = npath.basename(pathname);
67
+ if (!baseName || routeData.isIndex) {
68
+ baseName = "index";
69
+ }
70
+ return new URL(`./${baseName}.html`, outFolder);
71
+ }
54
72
  }
55
73
  }
56
74
  }
@@ -47,6 +47,7 @@ import {
47
47
  mergeInlineCss
48
48
  } from "./internal.js";
49
49
  import { getTimeStat, shouldAppendForwardSlash } from "./util.js";
50
+ import { NoPrerenderedRoutesWithDomains } from "../errors/errors-data.js";
50
51
  function createEntryURL(filePath, outFolder) {
51
52
  return new URL("./" + filePath + `?time=${Date.now()}`, outFolder);
52
53
  }
@@ -130,9 +131,16 @@ async function generatePages(opts, internals) {
130
131
  ${bgGreen(black(` ${verb} static routes `))}`);
131
132
  const builtPaths = /* @__PURE__ */ new Set();
132
133
  const pagesToGenerate = pipeline.retrieveRoutesToGenerate();
134
+ const config = pipeline.getConfig();
133
135
  if (ssr) {
134
136
  for (const [pageData, filePath] of pagesToGenerate) {
135
137
  if (pageData.route.prerender) {
138
+ if (config.experimental.i18nDomains) {
139
+ throw new AstroError({
140
+ ...NoPrerenderedRoutesWithDomains,
141
+ message: NoPrerenderedRoutesWithDomains.message(pageData.component)
142
+ });
143
+ }
136
144
  const ssrEntryURLPage = createEntryURL(filePath, outFolder);
137
145
  const ssrEntryPage = await import(ssrEntryURLPage.toString());
138
146
  if (opts.settings.adapter?.adapterFeatures?.functionPerRoute) {
@@ -334,8 +342,8 @@ function addPageName(pathname, opts) {
334
342
  const pageName = shouldAppendForwardSlash(trailingSlash, buildFormat) ? pathname.replace(/\/?$/, "/").replace(/^\//, "") : pathname.replace(/^\//, "");
335
343
  opts.pageNames.push(pageName);
336
344
  }
337
- function getUrlForPath(pathname, base, origin, format, routeType) {
338
- const ending = format === "directory" ? "/" : ".html";
345
+ function getUrlForPath(pathname, base, origin, format, trailingSlash, routeType) {
346
+ const ending = format === "directory" ? trailingSlash === "never" ? "" : "/" : ".html";
339
347
  let buildPathname;
340
348
  if (pathname === "/" || pathname === "") {
341
349
  buildPathname = base;
@@ -389,6 +397,7 @@ async function generatePath(pathname, pipeline, gopts, route) {
389
397
  pipeline.getConfig().base,
390
398
  pipeline.getStaticBuildOptions().origin,
391
399
  pipeline.getConfig().build.format,
400
+ pipeline.getConfig().trailingSlash,
392
401
  route.type
393
402
  );
394
403
  const request = createRequest({
@@ -450,8 +459,8 @@ async function generatePath(pathname, pipeline, gopts, route) {
450
459
  return;
451
460
  body = Buffer.from(await response.arrayBuffer());
452
461
  }
453
- const outFolder = getOutFolder(pipeline.getConfig(), pathname, route.type);
454
- const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, route.type);
462
+ const outFolder = getOutFolder(pipeline.getConfig(), pathname, route);
463
+ const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, route);
455
464
  route.distURL = outFile;
456
465
  await fs.promises.mkdir(outFolder, { recursive: true });
457
466
  await fs.promises.writeFile(outFile, body);
@@ -472,7 +481,8 @@ function createBuildManifest(settings, internals, renderers, middleware) {
472
481
  fallback: settings.config.i18n.fallback,
473
482
  routing: settings.config.i18n.routing,
474
483
  defaultLocale: settings.config.i18n.defaultLocale,
475
- locales: settings.config.i18n.locales
484
+ locales: settings.config.i18n.locales,
485
+ domainLookupTable: {}
476
486
  };
477
487
  }
478
488
  return {
@@ -8,6 +8,7 @@ import { serializeRouteData } from "../../routing/index.js";
8
8
  import { addRollupInput } from "../add-rollup-input.js";
9
9
  import { getOutFile, getOutFolder } from "../common.js";
10
10
  import { cssOrder, mergeInlineCss } from "../internal.js";
11
+ import { normalizeTheLocale } from "../../../i18n/index.js";
11
12
  const manifestReplace = "@@ASTRO_MANIFEST_REPLACE@@";
12
13
  const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, "g");
13
14
  const SSR_MANIFEST_VIRTUAL_MODULE_ID = "@astrojs-manifest";
@@ -111,6 +112,7 @@ function injectManifest(manifest, chunk) {
111
112
  function buildManifest(opts, internals, staticFiles) {
112
113
  const { settings } = opts;
113
114
  const routes = [];
115
+ const domainLookupTable = {};
114
116
  const entryModules = Object.fromEntries(internals.entrySpecifierToBundleMap.entries());
115
117
  if (settings.scripts.some((script) => script.stage === "page")) {
116
118
  staticFiles.push(entryModules[PAGE_SCRIPT_ID]);
@@ -127,8 +129,8 @@ function buildManifest(opts, internals, staticFiles) {
127
129
  continue;
128
130
  if (!route.pathname)
129
131
  continue;
130
- const outFolder = getOutFolder(opts.settings.config, route.pathname, route.type);
131
- const outFile = getOutFile(opts.settings.config, outFolder, route.pathname, route.type);
132
+ const outFolder = getOutFolder(opts.settings.config, route.pathname, route);
133
+ const outFile = getOutFile(opts.settings.config, outFolder, route.pathname, route);
132
134
  const file = outFile.toString().replace(opts.settings.config.build.client.toString(), "");
133
135
  routes.push({
134
136
  file,
@@ -174,6 +176,12 @@ function buildManifest(opts, internals, staticFiles) {
174
176
  routeData: serializeRouteData(route, settings.config.trailingSlash)
175
177
  });
176
178
  }
179
+ const i18n = settings.config.i18n;
180
+ if (settings.config.experimental.i18nDomains && i18n && i18n.domains && (i18n.routing === "domains-prefix-always" || i18n.routing === "domains-prefix-other-locales" || i18n.routing === "domains-prefix-other-no-redirect")) {
181
+ for (const [locale, domainValue] of Object.entries(i18n.domains)) {
182
+ domainLookupTable[domainValue] = normalizeTheLocale(locale);
183
+ }
184
+ }
177
185
  if (!(BEFORE_HYDRATION_SCRIPT_ID in entryModules)) {
178
186
  entryModules[BEFORE_HYDRATION_SCRIPT_ID] = "";
179
187
  }
@@ -183,7 +191,8 @@ function buildManifest(opts, internals, staticFiles) {
183
191
  fallback: settings.config.i18n.fallback,
184
192
  routing: settings.config.i18n.routing,
185
193
  locales: settings.config.i18n.locales,
186
- defaultLocale: settings.config.i18n.defaultLocale
194
+ defaultLocale: settings.config.i18n.defaultLocale,
195
+ domainLookupTable
187
196
  };
188
197
  }
189
198
  return {
@@ -210,7 +210,14 @@ function generateSSRCode(adapter, middlewareId) {
210
210
  return `export const ${name} = _exports['${name}'];`;
211
211
  }
212
212
  }) ?? [],
213
- `serverEntrypointModule.start?.(_manifest, _args);`
213
+ // NOTE: This is intentionally obfuscated!
214
+ // Do NOT simplify this to something like `serverEntrypointModule.start?.(_manifest, _args)`
215
+ // They are NOT equivalent! Some bundlers will throw if `start` is not exported, but we
216
+ // only want to silently ignore it... hence the dynamic, obfuscated weirdness.
217
+ `const _start = 'start';
218
+ if (_start in serverEntrypointModule) {
219
+ serverEntrypointModule[_start](_manifest, _args);
220
+ }`
214
221
  ];
215
222
  return {
216
223
  imports,
@@ -12,6 +12,7 @@ function shouldAppendForwardSlash(trailingSlash, buildFormat) {
12
12
  switch (buildFormat) {
13
13
  case "directory":
14
14
  return true;
15
+ case "preserve":
15
16
  case "file":
16
17
  return false;
17
18
  }