astro 6.0.2 → 6.0.4

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.
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "6.0.2";
3
+ version = "6.0.4";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -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.2") {
192
+ if (previousAstroVersion && previousAstroVersion !== "6.0.4") {
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.2") {
201
- this.#store.metaStore().set("astro-version", "6.0.2");
200
+ if ("6.0.4") {
201
+ this.#store.metaStore().set("astro-version", "6.0.4");
202
202
  }
203
203
  if (currentConfigDigest) {
204
204
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -139,6 +139,16 @@ ${colors.bgGreen(colors.black(` ${verb} static routes `))}`);
139
139
  );
140
140
  }
141
141
  }
142
+ for (const { route: generatedRoute } of filteredPaths) {
143
+ if (generatedRoute.distURL && generatedRoute.distURL.length > 0) {
144
+ for (const pageData of Object.values(options.allPages)) {
145
+ if (pageData.route.route === generatedRoute.route && pageData.route.component === generatedRoute.component) {
146
+ pageData.route.distURL = generatedRoute.distURL;
147
+ break;
148
+ }
149
+ }
150
+ }
151
+ }
142
152
  const staticImageList = getStaticImageList();
143
153
  if (prerenderer.collectStaticImages) {
144
154
  const adapterImages = await prerenderer.collectStaticImages();
@@ -1,4 +1,6 @@
1
- import type { AstroInlineConfig } from '../../types/public/config.js';
1
+ import type { AstroSettings, RoutesList } from '../../types/astro.js';
2
+ import type { AstroInlineConfig, RuntimeMode } from '../../types/public/config.js';
3
+ import type { Logger } from '../logger/core.js';
2
4
  interface BuildOptions {
3
5
  /**
4
6
  * Output a development-based build similar to code transformed in `astro dev`. This
@@ -25,4 +27,40 @@ interface BuildOptions {
25
27
  * @experimental The JavaScript API is experimental
26
28
  */
27
29
  export default function build(inlineConfig: AstroInlineConfig, options?: BuildOptions): Promise<void>;
30
+ interface AstroBuilderOptions extends BuildOptions {
31
+ logger: Logger;
32
+ mode: string;
33
+ runtimeMode: RuntimeMode;
34
+ /**
35
+ * Provide a pre-built routes list to skip filesystem route scanning.
36
+ * Useful for testing builds with in-memory virtual modules.
37
+ */
38
+ routesList?: RoutesList;
39
+ /**
40
+ * Whether to run `syncInternal` during setup. Defaults to true.
41
+ * Set to false for in-memory builds that don't need type generation.
42
+ */
43
+ sync?: boolean;
44
+ }
45
+ export declare class AstroBuilder {
46
+ private settings;
47
+ private logger;
48
+ private mode;
49
+ private runtimeMode;
50
+ private origin;
51
+ private routesList;
52
+ private timer;
53
+ private teardownCompiler;
54
+ private sync;
55
+ constructor(settings: AstroSettings, options: AstroBuilderOptions);
56
+ /** Setup Vite and run any async setup logic that couldn't run inside of the constructor. */
57
+ private setup;
58
+ /** Run the build logic. build() is marked private because usage should go through ".run()" */
59
+ private build;
60
+ /** Build the given Astro project. */
61
+ run(): Promise<void>;
62
+ private validateConfig;
63
+ /** Stats */
64
+ private printStats;
65
+ }
28
66
  export {};
@@ -56,14 +56,16 @@ class AstroBuilder {
56
56
  routesList;
57
57
  timer;
58
58
  teardownCompiler;
59
+ sync;
59
60
  constructor(settings, options) {
60
61
  this.mode = options.mode;
61
62
  this.runtimeMode = options.runtimeMode;
62
63
  this.settings = settings;
63
64
  this.logger = options.logger;
64
65
  this.teardownCompiler = options.teardownCompiler ?? true;
66
+ this.sync = options.sync ?? true;
65
67
  this.origin = settings.config.site ? new URL(settings.config.site).origin : `http://localhost:${settings.config.server.port}`;
66
- this.routesList = { routes: [] };
68
+ this.routesList = options.routesList ?? { routes: [] };
67
69
  this.timer = {};
68
70
  }
69
71
  /** Setup Vite and run any async setup logic that couldn't run inside of the constructor. */
@@ -77,7 +79,9 @@ class AstroBuilder {
77
79
  logger
78
80
  });
79
81
  this.settings.buildOutput = getPrerenderDefault(this.settings.config) ? "static" : "server";
80
- this.routesList = await createRoutesList({ settings: this.settings }, this.logger);
82
+ if (this.routesList.routes.length === 0) {
83
+ this.routesList = await createRoutesList({ settings: this.settings }, this.logger);
84
+ }
81
85
  await runHookConfigDone({ settings: this.settings, logger, command: "build" });
82
86
  if (!this.settings.config.adapter && this.settings.buildOutput === "server") {
83
87
  throw new AstroError(AstroErrorData.NoAdapterInstalled);
@@ -98,14 +102,16 @@ class AstroBuilder {
98
102
  sync: false
99
103
  }
100
104
  );
101
- const { syncInternal } = await import("../sync/index.js");
102
- await syncInternal({
103
- mode: this.mode,
104
- settings: this.settings,
105
- logger,
106
- fs,
107
- command: "build"
108
- });
105
+ if (this.sync) {
106
+ const { syncInternal } = await import("../sync/index.js");
107
+ await syncInternal({
108
+ mode: this.mode,
109
+ settings: this.settings,
110
+ logger,
111
+ fs,
112
+ command: "build"
113
+ });
114
+ }
109
115
  return { viteConfig };
110
116
  }
111
117
  /** Run the build logic. build() is marked private because usage should go through ".run()" */
@@ -215,5 +221,6 @@ class AstroBuilder {
215
221
  }
216
222
  }
217
223
  export {
224
+ AstroBuilder,
218
225
  build as default
219
226
  };
@@ -10,7 +10,11 @@ import { emptyDir, removeEmptyDirs } from "../../core/fs/index.js";
10
10
  import { appendForwardSlash, prependForwardSlash } from "../../core/path.js";
11
11
  import { runHookBuildSetup } from "../../integrations/hooks.js";
12
12
  import { SERIALIZED_MANIFEST_RESOLVED_ID } from "../../manifest/serialized.js";
13
- import { getClientOutputDirectory, getServerOutputDirectory } from "../../prerender/utils.js";
13
+ import {
14
+ getClientOutputDirectory,
15
+ getPrerenderOutputDirectory,
16
+ getServerOutputDirectory
17
+ } from "../../prerender/utils.js";
14
18
  import { VIRTUAL_PAGE_RESOLVED_MODULE_ID } from "../../vite-plugin-pages/const.js";
15
19
  import { PAGE_SCRIPT_ID } from "../../vite-plugin-scripts/index.js";
16
20
  import { routeIsRedirect } from "../routing/helpers.js";
@@ -30,6 +34,7 @@ import { encodeName, getTimeStat, viteBuildReturnToRollupOutputs } from "./util.
30
34
  import { NOOP_MODULE_ID } from "./plugins/plugin-noop.js";
31
35
  import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../constants.js";
32
36
  import { getSSRAssets } from "./internal.js";
37
+ import { serverIslandPlaceholderMap } from "../server-islands/vite-plugin-server-islands.js";
33
38
  const PRERENDER_ENTRY_FILENAME_PREFIX = "prerender-entry";
34
39
  function extractRelevantChunks(outputs, prerender) {
35
40
  const extracted = [];
@@ -38,7 +43,8 @@ function extractRelevantChunks(outputs, prerender) {
38
43
  if (chunk.type === "asset") continue;
39
44
  const needsContentInjection = chunk.code.includes(LINKS_PLACEHOLDER);
40
45
  const needsManifestInjection = chunk.moduleIds.includes(SERIALIZED_MANIFEST_RESOLVED_ID);
41
- if (needsContentInjection || needsManifestInjection) {
46
+ const needsServerIslandInjection = chunk.code.includes(serverIslandPlaceholderMap);
47
+ if (needsContentInjection || needsManifestInjection || needsServerIslandInjection) {
42
48
  extracted.push({
43
49
  fileName: chunk.fileName,
44
50
  code: chunk.code,
@@ -82,6 +88,7 @@ async function buildEnvironments(opts, internals) {
82
88
  const flatPlugins = buildPlugins.flat().filter(Boolean);
83
89
  const plugins = [...flatPlugins, ...viteConfig.plugins || []];
84
90
  let currentRollupInput = void 0;
91
+ let buildPostHooks = [];
85
92
  plugins.push({
86
93
  name: "astro:resolve-input",
87
94
  // When the rollup input is safe to update, we normalize it to always be an object
@@ -107,8 +114,13 @@ async function buildEnvironments(opts, internals) {
107
114
  buildApp: {
108
115
  order: "post",
109
116
  async handler() {
110
- await runManifestInjection(opts, internals, internals.extractedChunks ?? []);
111
- const prerenderOutputDir = new URL("./.prerender/", getServerOutputDirectory(settings));
117
+ await runManifestInjection(
118
+ opts,
119
+ internals,
120
+ internals.extractedChunks ?? [],
121
+ buildPostHooks
122
+ );
123
+ const prerenderOutputDir = getPrerenderOutputDirectory(settings);
112
124
  if (settings.buildOutput === "static") {
113
125
  settings.timer.start("Static generate");
114
126
  await ssrMoveAssets(opts, internals, prerenderOutputDir);
@@ -224,6 +236,10 @@ async function buildEnvironments(opts, internals) {
224
236
  const prerenderOutputs = viteBuildReturnToRollupOutputs(prerenderOutput);
225
237
  const prerenderChunks = extractRelevantChunks(prerenderOutputs, true);
226
238
  prerenderOutput = void 0;
239
+ const ssrPlugins = builder2.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr]?.config.plugins ?? [];
240
+ buildPostHooks = ssrPlugins.map(
241
+ (plugin) => typeof plugin.api?.buildPostHook === "function" ? plugin.api.buildPostHook : void 0
242
+ ).filter(Boolean);
227
243
  internals.clientInput = getClientInput(internals, settings);
228
244
  if (!internals.clientInput.size) {
229
245
  internals.clientInput.add(NOOP_MODULE_ID);
@@ -244,7 +260,7 @@ async function buildEnvironments(opts, internals) {
244
260
  [ASTRO_VITE_ENVIRONMENT_NAMES.prerender]: {
245
261
  build: {
246
262
  emitAssets: true,
247
- outDir: fileURLToPath(new URL("./.prerender/", getServerOutputDirectory(settings))),
263
+ outDir: fileURLToPath(getPrerenderOutputDirectory(settings)),
248
264
  rollupOptions: {
249
265
  // Only skip the default prerender entrypoint if an adapter with `entrypointResolution: 'self'` is used
250
266
  // AND provides a custom prerenderer. Otherwise, use the default.
@@ -318,7 +334,7 @@ function getPrerenderEntryFileName(prerenderOutput) {
318
334
  function extractPrerenderEntryFileName(internals, prerenderOutput) {
319
335
  internals.prerenderEntryFileName = getPrerenderEntryFileName(prerenderOutput);
320
336
  }
321
- async function runManifestInjection(opts, internals, chunks) {
337
+ async function runManifestInjection(opts, internals, chunks, buildPostHooks) {
322
338
  const mutations = /* @__PURE__ */ new Map();
323
339
  const mutate = (fileName, newCode, prerender) => {
324
340
  mutations.set(fileName, { code: newCode, prerender });
@@ -330,16 +346,18 @@ async function runManifestInjection(opts, internals, chunks) {
330
346
  internals,
331
347
  { chunks, mutate }
332
348
  );
349
+ for (const buildPostHook of buildPostHooks) {
350
+ await buildPostHook({ chunks, mutate });
351
+ }
333
352
  await writeMutatedChunks(opts, mutations);
334
353
  }
335
354
  async function writeMutatedChunks(opts, mutations) {
336
355
  const { settings } = opts;
337
356
  const config = settings.config;
338
- const serverOutputDir = getServerOutputDirectory(settings);
339
357
  for (const [fileName, mutation] of mutations) {
340
358
  let root;
341
359
  if (mutation.prerender) {
342
- root = new URL("./.prerender/", serverOutputDir);
360
+ root = getPrerenderOutputDirectory(settings);
343
361
  } else if (settings.buildOutput === "server") {
344
362
  root = config.build.server;
345
363
  } else {
@@ -66,6 +66,10 @@ export declare const originPathnameSymbol: unique symbol;
66
66
  * Use this symbol to set and retrieve the pipeline.
67
67
  */
68
68
  export declare const pipelineSymbol: unique symbol;
69
+ /**
70
+ * Use this symbol to opt into handling prerender routes in Astro core dev middleware.
71
+ */
72
+ export declare const devPrerenderMiddlewareSymbol: unique symbol;
69
73
  /**
70
74
  * The symbol used as a field on the request object to store a cleanup callback associated with aborting the request when the underlying socket closes.
71
75
  */
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "6.0.2";
1
+ const ASTRO_VERSION = "6.0.4";
2
2
  const ASTRO_GENERATOR = `Astro v${ASTRO_VERSION}`;
3
3
  const REROUTE_DIRECTIVE_HEADER = "X-Astro-Reroute";
4
4
  const REWRITE_DIRECTIVE_HEADER_KEY = "X-Astro-Rewrite";
@@ -12,6 +12,7 @@ const clientAddressSymbol = /* @__PURE__ */ Symbol.for("astro.clientAddress");
12
12
  const clientLocalsSymbol = /* @__PURE__ */ Symbol.for("astro.locals");
13
13
  const originPathnameSymbol = /* @__PURE__ */ Symbol.for("astro.originPathname");
14
14
  const pipelineSymbol = /* @__PURE__ */ Symbol.for("astro.pipeline");
15
+ const devPrerenderMiddlewareSymbol = /* @__PURE__ */ Symbol.for("astro.devPrerenderMiddleware");
15
16
  const nodeRequestAbortControllerCleanupSymbol = /* @__PURE__ */ Symbol.for(
16
17
  "astro.nodeRequestAbortControllerCleanup"
17
18
  );
@@ -57,6 +58,7 @@ export {
57
58
  SUPPORTED_MARKDOWN_FILE_EXTENSIONS,
58
59
  clientAddressSymbol,
59
60
  clientLocalsSymbol,
61
+ devPrerenderMiddlewareSymbol,
60
62
  nodeRequestAbortControllerCleanupSymbol,
61
63
  originPathnameSymbol,
62
64
  pipelineSymbol,
@@ -26,7 +26,7 @@ async function dev(inlineConfig) {
26
26
  await telemetry.record([]);
27
27
  const restart = await createContainerWithAutomaticRestart({ inlineConfig, fs });
28
28
  const logger = restart.container.logger;
29
- const currentVersion = "6.0.2";
29
+ const currentVersion = "6.0.4";
30
30
  const isPrerelease = currentVersion.includes("-");
31
31
  if (!isPrerelease) {
32
32
  try {
@@ -90,7 +90,7 @@ function collectErrorMetadata(e, rootFolder) {
90
90
  function generateHint(err) {
91
91
  const commonBrowserAPIs = ["document", "window"];
92
92
  if (/Unknown file extension "\.(?:jsx|vue|svelte|astro|css)" for /.test(err.message)) {
93
- return "You likely need to add this package to `vite.ssr.noExternal` in your astro config file.";
93
+ return "You likely need to add this package to `vite.resolve.noExternal` in your astro config file.";
94
94
  } else if (commonBrowserAPIs.some((api) => err.toString().includes(api))) {
95
95
  const hint = `Browser APIs are not available on the server.
96
96
 
@@ -269,7 +269,7 @@ function printHelp({
269
269
  message.push(
270
270
  linebreak(),
271
271
  ` ${bgGreen(black(` ${commandName} `))} ${green(
272
- `v${"6.0.2"}`
272
+ `v${"6.0.4"}`
273
273
  )} ${headline}`
274
274
  );
275
275
  }
@@ -59,10 +59,10 @@ async function getRequestData(request, bodySizeLimit = DEFAULT_BODY_SIZE_LIMIT)
59
59
  const body = await readBodyWithLimit(request, bodySizeLimit);
60
60
  const raw = new TextDecoder().decode(body);
61
61
  const data = JSON.parse(raw);
62
- if ("slots" in data && typeof data.slots === "object") {
62
+ if (Object.hasOwn(data, "slots") && typeof data.slots === "object") {
63
63
  return badRequest("Plaintext slots are not allowed. Slots must be encrypted.");
64
64
  }
65
- if ("componentExport" in data && typeof data.componentExport === "string") {
65
+ if (Object.hasOwn(data, "componentExport") && typeof data.componentExport === "string") {
66
66
  return badRequest(
67
67
  "Plaintext componentExport is not allowed. componentExport must be encrypted."
68
68
  );
@@ -1,4 +1,5 @@
1
1
  import type { Plugin as VitePlugin } from 'vite';
2
2
  import type { AstroPluginOptions } from '../../types/astro.js';
3
3
  export declare const SERVER_ISLAND_MANIFEST = "virtual:astro:server-island-manifest";
4
+ export declare const serverIslandPlaceholderMap = "'$$server-islands-map$$'";
4
5
  export declare function vitePluginServerIslands({ settings }: AstroPluginOptions): VitePlugin;
@@ -1,16 +1,32 @@
1
- import MagicString from "magic-string";
1
+ import fs from "node:fs";
2
+ import { getPrerenderOutputDirectory, getServerOutputDirectory } from "../../prerender/utils.js";
2
3
  import { AstroError, AstroErrorData } from "../errors/index.js";
4
+ import { appendForwardSlash } from "../path.js";
3
5
  import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../constants.js";
4
6
  const SERVER_ISLAND_MANIFEST = "virtual:astro:server-island-manifest";
5
7
  const RESOLVED_SERVER_ISLAND_MANIFEST = "\0" + SERVER_ISLAND_MANIFEST;
6
8
  const serverIslandPlaceholderMap = "'$$server-islands-map$$'";
7
9
  const serverIslandPlaceholderNameMap = "'$$server-islands-name-map$$'";
10
+ function createServerIslandImportMapSource(entries, toImportPath) {
11
+ const mappings = Array.from(entries, ([islandName, fileName]) => {
12
+ const importPath = toImportPath(fileName);
13
+ return ` [${JSON.stringify(islandName)}, () => import(${JSON.stringify(importPath)})],`;
14
+ });
15
+ return `new Map([
16
+ ${mappings.join("\n")}
17
+ ])`;
18
+ }
19
+ function createNameMapSource(entries) {
20
+ return `new Map(${JSON.stringify(Array.from(entries), null, 2)})`;
21
+ }
8
22
  function vitePluginServerIslands({ settings }) {
9
23
  let command = "serve";
10
24
  let ssrEnvironment = null;
11
25
  const referenceIdMap = /* @__PURE__ */ new Map();
12
26
  const serverIslandMap = /* @__PURE__ */ new Map();
13
27
  const serverIslandNameMap = /* @__PURE__ */ new Map();
28
+ const resolvedIslandImports = /* @__PURE__ */ new Map();
29
+ let ssrManifestChunk = null;
14
30
  return {
15
31
  name: "astro:server-islands",
16
32
  enforce: "post",
@@ -70,7 +86,7 @@ export const serverIslandNameMap = ${serverIslandPlaceholderNameMap};`
70
86
  serverIslandNameMap.set(comp.resolvedPath, name);
71
87
  serverIslandMap.set(name, comp.resolvedPath);
72
88
  if (command === "build") {
73
- let referenceId = this.emitFile({
89
+ const referenceId = this.emitFile({
74
90
  type: "chunk",
75
91
  id: comp.specifier,
76
92
  importer: id,
@@ -95,18 +111,17 @@ export const serverIslandNameMap = ${serverIslandPlaceholderNameMap};`
95
111
  }
96
112
  }
97
113
  if (serverIslandNameMap.size > 0 && serverIslandMap.size > 0) {
98
- let mapSource = "new Map([\n ";
99
- for (let [name, path] of serverIslandMap) {
100
- mapSource += `
101
- ['${name}', () => import('${path}')],`;
102
- }
103
- mapSource += "]);";
114
+ const mapSource = createServerIslandImportMapSource(
115
+ serverIslandMap,
116
+ (fileName) => fileName
117
+ );
118
+ const nameMapSource = createNameMapSource(serverIslandNameMap);
104
119
  return {
105
120
  code: `
106
121
  export const serverIslandMap = ${mapSource};
107
122
 
108
123
 
109
- export const serverIslandNameMap = new Map(${JSON.stringify(Array.from(serverIslandNameMap.entries()), null, 2)});
124
+ export const serverIslandNameMap = ${nameMapSource};
110
125
  `
111
126
  };
112
127
  }
@@ -115,38 +130,116 @@ export const serverIslandNameMap = new Map(${JSON.stringify(Array.from(serverIsl
115
130
  },
116
131
  renderChunk(code, chunk) {
117
132
  if (code.includes(serverIslandPlaceholderMap)) {
118
- if (referenceIdMap.size === 0) {
133
+ if (command === "build") {
134
+ if (referenceIdMap.size === 0) {
135
+ return;
136
+ }
137
+ const isRelativeChunk = !chunk.isEntry;
138
+ const dots = isRelativeChunk ? ".." : ".";
139
+ const mapEntries = [];
140
+ for (const [resolvedPath, referenceId] of referenceIdMap) {
141
+ const fileName = this.getFileName(referenceId);
142
+ const islandName = serverIslandNameMap.get(resolvedPath);
143
+ if (!islandName) continue;
144
+ if (!resolvedIslandImports.has(islandName)) {
145
+ resolvedIslandImports.set(islandName, fileName);
146
+ }
147
+ mapEntries.push([islandName, fileName]);
148
+ }
149
+ const mapSource = createServerIslandImportMapSource(
150
+ mapEntries,
151
+ (fileName) => `${dots}/${fileName}`
152
+ );
153
+ const nameMapSource = createNameMapSource(serverIslandNameMap);
119
154
  return {
120
- code: code.replace(serverIslandPlaceholderMap, "new Map();").replace(serverIslandPlaceholderNameMap, "new Map()"),
155
+ code: code.replace(serverIslandPlaceholderMap, mapSource).replace(serverIslandPlaceholderNameMap, nameMapSource),
121
156
  map: null
122
157
  };
123
158
  }
124
- const isRelativeChunk = !chunk.isEntry;
125
- const dots = isRelativeChunk ? ".." : ".";
126
- let mapSource = "new Map([";
127
- let nameMapSource = "new Map(";
128
- for (let [resolvedPath, referenceId] of referenceIdMap) {
129
- const fileName = this.getFileName(referenceId);
130
- const islandName = serverIslandNameMap.get(resolvedPath);
131
- mapSource += `
132
- ['${islandName}', () => import('${dots}/${fileName}')],`;
133
- }
134
- nameMapSource += `${JSON.stringify(Array.from(serverIslandNameMap.entries()), null, 2)}`;
135
- mapSource += "\n])";
136
- nameMapSource += "\n)";
137
- referenceIdMap.clear();
138
- const ms = new MagicString(code);
139
- ms.replace(serverIslandPlaceholderMap, mapSource);
140
- ms.replace(serverIslandPlaceholderNameMap, nameMapSource);
141
159
  return {
142
- code: ms.toString(),
143
- map: ms.generateMap({ hires: "boundary" })
160
+ code: code.replace(serverIslandPlaceholderMap, "new Map();").replace(serverIslandPlaceholderNameMap, "new Map()"),
161
+ map: null
144
162
  };
145
163
  }
164
+ },
165
+ generateBundle(_options, bundle) {
166
+ const envName = this.environment?.name;
167
+ if (envName === ASTRO_VITE_ENVIRONMENT_NAMES.ssr) {
168
+ for (const chunk of Object.values(bundle)) {
169
+ if (chunk.type === "chunk" && chunk.code.includes(serverIslandPlaceholderMap)) {
170
+ ssrManifestChunk = chunk;
171
+ break;
172
+ }
173
+ }
174
+ }
175
+ if (envName === ASTRO_VITE_ENVIRONMENT_NAMES.prerender && ssrManifestChunk) {
176
+ if (resolvedIslandImports.size > 0) {
177
+ const isRelativeChunk = ssrManifestChunk.fileName.includes("/");
178
+ const dots = isRelativeChunk ? ".." : ".";
179
+ const mapSource = createServerIslandImportMapSource(
180
+ resolvedIslandImports,
181
+ (fileName) => `${dots}/${fileName}`
182
+ );
183
+ const nameMapSource = createNameMapSource(serverIslandNameMap);
184
+ ssrManifestChunk.code = ssrManifestChunk.code.replace(serverIslandPlaceholderMap, mapSource).replace(serverIslandPlaceholderNameMap, nameMapSource);
185
+ } else {
186
+ ssrManifestChunk.code = ssrManifestChunk.code.replace(serverIslandPlaceholderMap, "new Map()").replace(serverIslandPlaceholderNameMap, "new Map()");
187
+ }
188
+ }
189
+ },
190
+ api: {
191
+ /**
192
+ * Post-build hook that patches SSR chunks containing server island placeholders.
193
+ *
194
+ * During build, SSR can run before all server islands are discovered (e.g. islands
195
+ * only used in prerendered pages). This hook runs after SSR + prerender builds and:
196
+ * 1) replaces placeholders with the complete map of discovered islands
197
+ * 2) copies island chunks emitted in prerender into the SSR output directory
198
+ *
199
+ * Two cases:
200
+ * 1. Islands were discovered: Replace placeholders with real import maps.
201
+ * 2. No islands found: Replace placeholders with empty maps.
202
+ */
203
+ async buildPostHook({
204
+ chunks,
205
+ mutate
206
+ }) {
207
+ const ssrChunkWithPlaceholder = chunks.find(
208
+ (c) => !c.prerender && c.code.includes(serverIslandPlaceholderMap)
209
+ );
210
+ if (!ssrChunkWithPlaceholder) {
211
+ return;
212
+ }
213
+ if (resolvedIslandImports.size > 0) {
214
+ const isRelativeChunk = ssrChunkWithPlaceholder.fileName.includes("/");
215
+ const dots = isRelativeChunk ? ".." : ".";
216
+ const mapSource = createServerIslandImportMapSource(
217
+ resolvedIslandImports,
218
+ (fileName) => `${dots}/${fileName}`
219
+ );
220
+ const nameMapSource = createNameMapSource(serverIslandNameMap);
221
+ const newCode = ssrChunkWithPlaceholder.code.replace(serverIslandPlaceholderMap, mapSource).replace(serverIslandPlaceholderNameMap, nameMapSource);
222
+ mutate(ssrChunkWithPlaceholder.fileName, newCode, false);
223
+ const serverOutputDir = getServerOutputDirectory(settings);
224
+ const prerenderOutputDir = getPrerenderOutputDirectory(settings);
225
+ for (const [, fileName] of resolvedIslandImports) {
226
+ const srcPath = new URL(fileName, appendForwardSlash(prerenderOutputDir.toString()));
227
+ const destPath = new URL(fileName, appendForwardSlash(serverOutputDir.toString()));
228
+ if (!fs.existsSync(srcPath)) continue;
229
+ const destDir = new URL("./", destPath);
230
+ await fs.promises.mkdir(destDir, { recursive: true });
231
+ await fs.promises.copyFile(srcPath, destPath);
232
+ }
233
+ } else {
234
+ const newCode = ssrChunkWithPlaceholder.code.replace(serverIslandPlaceholderMap, "new Map()").replace(serverIslandPlaceholderNameMap, "new Map()");
235
+ mutate(ssrChunkWithPlaceholder.fileName, newCode, false);
236
+ }
237
+ }
146
238
  }
147
239
  };
148
240
  }
149
241
  export {
150
242
  SERVER_ISLAND_MANIFEST,
243
+ serverIslandPlaceholderMap,
151
244
  vitePluginServerIslands
152
245
  };
@@ -74,9 +74,10 @@ class I18nRouter {
74
74
  matchPrefixAlways(pathname, _context) {
75
75
  const isRoot = pathname === this.#base + "/" || pathname === this.#base;
76
76
  if (isRoot) {
77
+ const basePrefix = this.#base === "/" ? "" : this.#base;
77
78
  return {
78
79
  type: "redirect",
79
- location: `${this.#base}/${this.#defaultLocale}`
80
+ location: `${basePrefix}/${this.#defaultLocale}`
80
81
  };
81
82
  }
82
83
  if (!pathHasLocale(pathname, this.#locales)) {
@@ -5,6 +5,10 @@ export declare function getPrerenderDefault(config: AstroConfig): boolean;
5
5
  * Returns the correct output directory of the SSR build based on the configuration
6
6
  */
7
7
  export declare function getServerOutputDirectory(settings: AstroSettings): URL;
8
+ /**
9
+ * Returns the output directory used by the prerender environment.
10
+ */
11
+ export declare function getPrerenderOutputDirectory(settings: AstroSettings): URL;
8
12
  /**
9
13
  * Returns the correct output directory of the client build based on the configuration
10
14
  */
@@ -5,6 +5,9 @@ function getPrerenderDefault(config) {
5
5
  function getServerOutputDirectory(settings) {
6
6
  return settings.buildOutput === "server" ? settings.config.build.server : getOutDirWithinCwd(settings.config.outDir);
7
7
  }
8
+ function getPrerenderOutputDirectory(settings) {
9
+ return new URL("./.prerender/", getServerOutputDirectory(settings));
10
+ }
8
11
  function getClientOutputDirectory(settings) {
9
12
  const preserveStructure = settings.adapter?.adapterFeatures?.preserveBuildClientDir;
10
13
  if (settings.buildOutput === "server" || preserveStructure) {
@@ -15,5 +18,6 @@ function getClientOutputDirectory(settings) {
15
18
  export {
16
19
  getClientOutputDirectory,
17
20
  getPrerenderDefault,
21
+ getPrerenderOutputDirectory,
18
22
  getServerOutputDirectory
19
23
  };
@@ -10,7 +10,11 @@ function astroDevToolbar({ settings, logger }) {
10
10
  return {
11
11
  optimizeDeps: {
12
12
  // Optimize CJS dependencies used by the dev toolbar
13
- include: ["astro > aria-query", "astro > axobject-query"]
13
+ include: [
14
+ "astro > aria-query",
15
+ "astro > axobject-query",
16
+ ...settings.devToolbarApps.length > 0 ? ["astro/toolbar"] : []
17
+ ]
14
18
  }
15
19
  };
16
20
  },
@@ -3,7 +3,7 @@ import { IncomingMessage } from "node:http";
3
3
  import { isRunnableDevEnvironment } from "vite";
4
4
  import { toFallbackType } from "../core/app/common.js";
5
5
  import { toRoutingStrategy } from "../core/app/entrypoints/index.js";
6
- import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../core/constants.js";
6
+ import { ASTRO_VITE_ENVIRONMENT_NAMES, devPrerenderMiddlewareSymbol } from "../core/constants.js";
7
7
  import {
8
8
  getAlgorithm,
9
9
  getDirectives,
@@ -19,6 +19,7 @@ import { getViteErrorPayload } from "../core/errors/dev/index.js";
19
19
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
20
20
  import { NOOP_MIDDLEWARE_FN } from "../core/middleware/noop-middleware.js";
21
21
  import { createViteLoader } from "../core/module-loader/index.js";
22
+ import { matchAllRoutes } from "../core/routing/match.js";
22
23
  import { resolveMiddlewareMode } from "../integrations/adapter-utils.js";
23
24
  import { SERIALIZED_MANIFEST_ID } from "../manifest/serialized.js";
24
25
  import { ASTRO_DEV_SERVER_APP_ID } from "../vite-plugin-app/index.js";
@@ -40,15 +41,23 @@ function createVitePluginAstroServer({
40
41
  return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr;
41
42
  },
42
43
  async configureServer(viteServer) {
43
- if (!isRunnableDevEnvironment(viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr])) {
44
+ const ssrEnvironment = viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr];
45
+ const prerenderEnvironment = viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.prerender];
46
+ const runnableSsrEnvironment = isRunnableDevEnvironment(ssrEnvironment) ? ssrEnvironment : void 0;
47
+ const runnablePrerenderEnvironment = isRunnableDevEnvironment(prerenderEnvironment) ? prerenderEnvironment : void 0;
48
+ if (!runnableSsrEnvironment && !runnablePrerenderEnvironment) {
44
49
  return;
45
50
  }
46
- const environment = viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr];
47
- const loader = createViteLoader(viteServer, environment);
48
- const { default: createAstroServerApp } = await environment.runner.import(ASTRO_DEV_SERVER_APP_ID);
49
- const controller = createController({ loader });
50
- const { handler } = await createAstroServerApp(controller, settings, loader, logger);
51
- const { manifest } = await environment.runner.import(SERIALIZED_MANIFEST_ID);
51
+ async function createHandler(environment) {
52
+ const loader = createViteLoader(viteServer, environment);
53
+ const { default: createAstroServerApp } = await environment.runner.import(ASTRO_DEV_SERVER_APP_ID);
54
+ const controller = createController({ loader });
55
+ const { handler } = await createAstroServerApp(controller, settings, loader, logger);
56
+ const { manifest } = await environment.runner.import(SERIALIZED_MANIFEST_ID);
57
+ return { controller, handler, loader, manifest, environment };
58
+ }
59
+ const ssrHandler = runnableSsrEnvironment ? await createHandler(runnableSsrEnvironment) : void 0;
60
+ const prerenderHandler = runnablePrerenderEnvironment ? await createHandler(runnablePrerenderEnvironment) : void 0;
52
61
  const localStorage = new AsyncLocalStorage();
53
62
  function handleUnhandledRejection(rejection) {
54
63
  const error = AstroError.is(rejection) ? rejection : new AstroError({
@@ -56,20 +65,36 @@ function createVitePluginAstroServer({
56
65
  message: AstroErrorData.UnhandledRejection.message(rejection?.stack || rejection)
57
66
  });
58
67
  const store = localStorage.getStore();
59
- if (store instanceof IncomingMessage) {
60
- setRouteError(controller.state, store.url, error);
68
+ const handlers = [];
69
+ if (ssrHandler) handlers.push(ssrHandler);
70
+ if (prerenderHandler) handlers.push(prerenderHandler);
71
+ for (const currentHandler of handlers) {
72
+ if (store instanceof IncomingMessage) {
73
+ setRouteError(currentHandler.controller.state, store.url, error);
74
+ }
75
+ const { errorWithMetadata } = recordServerError(
76
+ currentHandler.loader,
77
+ currentHandler.manifest,
78
+ logger,
79
+ error
80
+ );
81
+ setTimeout(
82
+ async () => currentHandler.loader.webSocketSend(await getViteErrorPayload(errorWithMetadata)),
83
+ 200
84
+ );
61
85
  }
62
- const { errorWithMetadata } = recordServerError(loader, manifest, logger, error);
63
- setTimeout(
64
- async () => loader.webSocketSend(await getViteErrorPayload(errorWithMetadata)),
65
- 200
66
- );
67
86
  }
68
87
  process.on("unhandledRejection", handleUnhandledRejection);
69
88
  viteServer.httpServer?.on("close", () => {
70
89
  process.off("unhandledRejection", handleUnhandledRejection);
71
90
  });
72
91
  return () => {
92
+ const shouldHandlePrerenderInCore = Boolean(
93
+ viteServer[devPrerenderMiddlewareSymbol]
94
+ );
95
+ if (!ssrHandler && !(prerenderHandler && shouldHandlePrerenderInCore)) {
96
+ return;
97
+ }
73
98
  viteServer.middlewares.stack.unshift({
74
99
  route: "",
75
100
  handle: baseMiddleware(settings, logger)
@@ -86,16 +111,49 @@ function createVitePluginAstroServer({
86
111
  route: "",
87
112
  handle: secFetchMiddleware(logger, settings.config.security?.allowedDomains)
88
113
  });
89
- viteServer.middlewares.use(async function astroDevHandler(request, response) {
90
- if (request.url === void 0 || !request.method) {
91
- response.writeHead(500, "Incomplete request");
92
- response.end();
93
- return;
94
- }
95
- localStorage.run(request, () => {
96
- handler(request, response);
114
+ if (prerenderHandler && shouldHandlePrerenderInCore) {
115
+ viteServer.middlewares.use(
116
+ async function astroDevPrerenderHandler(request, response, next) {
117
+ if (request.url === void 0 || !request.method) {
118
+ response.writeHead(500, "Incomplete request");
119
+ response.end();
120
+ return;
121
+ }
122
+ if (request.url.startsWith("/@") || request.url.startsWith("/__")) {
123
+ return next();
124
+ }
125
+ if (request.url.includes("/node_modules/")) {
126
+ return next();
127
+ }
128
+ try {
129
+ const pathname = decodeURI(new URL(request.url, "http://localhost").pathname);
130
+ const { routes } = await prerenderHandler.environment.runner.import("virtual:astro:routes");
131
+ const routesList = { routes: routes.map((r) => r.routeData) };
132
+ const matches = matchAllRoutes(pathname, routesList);
133
+ if (!matches.some((route) => route.prerender)) {
134
+ return next();
135
+ }
136
+ localStorage.run(request, () => {
137
+ prerenderHandler.handler(request, response);
138
+ });
139
+ } catch (err) {
140
+ next(err);
141
+ }
142
+ }
143
+ );
144
+ }
145
+ if (ssrHandler) {
146
+ viteServer.middlewares.use(async function astroDevHandler(request, response) {
147
+ if (request.url === void 0 || !request.method) {
148
+ response.writeHead(500, "Incomplete request");
149
+ response.end();
150
+ return;
151
+ }
152
+ localStorage.run(request, () => {
153
+ ssrHandler.handler(request, response);
154
+ });
97
155
  });
98
- });
156
+ }
99
157
  };
100
158
  }
101
159
  };
@@ -42,9 +42,13 @@ function routeGuardMiddleware(settings) {
42
42
  return next();
43
43
  }
44
44
  const rootFilePath = new URL("." + pathname, config.root);
45
- if (fs.existsSync(rootFilePath)) {
46
- const html = notFoundTemplate(pathname);
47
- return writeHtmlResponse(res, 404, html);
45
+ try {
46
+ const stat = fs.statSync(rootFilePath);
47
+ if (stat.isFile()) {
48
+ const html = notFoundTemplate(pathname);
49
+ return writeHtmlResponse(res, 404, html);
50
+ }
51
+ } catch {
48
52
  }
49
53
  return next();
50
54
  };
@@ -42,16 +42,19 @@ function* collectCSSWithOrder(id, mod, seen = /* @__PURE__ */ new Set()) {
42
42
  }
43
43
  }
44
44
  function astroDevCssPlugin({ routesList, command }) {
45
- let ssrEnvironment = void 0;
45
+ let server;
46
46
  const cssContentCache = /* @__PURE__ */ new Map();
47
+ function getCurrentEnvironment(pluginEnv) {
48
+ return pluginEnv ?? server?.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr];
49
+ }
47
50
  return [
48
51
  {
49
52
  name: MODULE_DEV_CSS,
50
- async configureServer(server) {
51
- ssrEnvironment = server.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr];
53
+ async configureServer(viteServer) {
54
+ server = viteServer;
52
55
  },
53
56
  applyToEnvironment(env) {
54
- return env.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.client;
57
+ return env.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.client || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
55
58
  },
56
59
  resolveId: {
57
60
  filter: {
@@ -81,14 +84,15 @@ function astroDevCssPlugin({ routesList, command }) {
81
84
  );
82
85
  const cssWithOrder = /* @__PURE__ */ new Map();
83
86
  const componentPageId = getVirtualModulePageNameForComponent(componentPath);
84
- await ssrEnvironment?.fetchModule(componentPageId);
85
- const resolved = await ssrEnvironment?.pluginContainer.resolveId(componentPageId);
87
+ const env = getCurrentEnvironment(this.environment);
88
+ await env?.fetchModule(componentPageId);
89
+ const resolved = await env?.pluginContainer.resolveId(componentPageId);
86
90
  if (!resolved?.id) {
87
91
  return {
88
92
  code: "export const css = new Set()"
89
93
  };
90
94
  }
91
- const mod = ssrEnvironment?.moduleGraph.getModuleById(resolved.id);
95
+ const mod = env?.moduleGraph.getModuleById(resolved.id);
92
96
  if (!mod) {
93
97
  return {
94
98
  code: "export const css = new Set()"
@@ -123,7 +127,8 @@ function astroDevCssPlugin({ routesList, command }) {
123
127
  if (command === "build") {
124
128
  return;
125
129
  }
126
- const mod = ssrEnvironment?.moduleGraph.getModuleById(id);
130
+ const env = getCurrentEnvironment(this.environment);
131
+ const mod = env?.moduleGraph.getModuleById(id);
127
132
  if (mod) {
128
133
  cssContentCache.set(id, code);
129
134
  }
@@ -133,7 +138,7 @@ function astroDevCssPlugin({ routesList, command }) {
133
138
  {
134
139
  name: MODULE_DEV_CSS_ALL,
135
140
  applyToEnvironment(env) {
136
- return env.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.client || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.astro;
141
+ return env.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.client || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
137
142
  },
138
143
  resolveId: {
139
144
  filter: {
@@ -62,7 +62,7 @@ function vitePluginEnvironment({
62
62
  // For the dev toolbar
63
63
  "astro > html-escaper"
64
64
  ],
65
- exclude: ["astro:*", "virtual:astro:*"],
65
+ exclude: ["astro:*", "virtual:astro:*", "astro/virtual-modules/prefetch.js"],
66
66
  // Astro files can't be rendered on the client
67
67
  entries: [`${srcDirPattern}**/*.{jsx,tsx,vue,svelte,html}`]
68
68
  };
@@ -11,6 +11,7 @@ import { rootRelativePath } from "../core/viteUtils.js";
11
11
  import { createDefaultAstroMetadata } from "../vite-plugin-astro/metadata.js";
12
12
  import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../core/constants.js";
13
13
  import { isAstroServerEnvironment } from "../environments.js";
14
+ import { RESOLVED_MODULE_DEV_CSS_ALL } from "../vite-plugin-css/const.js";
14
15
  import { PAGE_SCRIPT_ID } from "../vite-plugin-scripts/index.js";
15
16
  const ASTRO_ROUTES_MODULE_ID = "virtual:astro:routes";
16
17
  const ASTRO_ROUTES_MODULE_ID_RESOLVED = "\0" + ASTRO_ROUTES_MODULE_ID;
@@ -77,11 +78,26 @@ async function astroPluginRoutes({
77
78
  routeData: serializeRouteData(r, settings.config.trailingSlash)
78
79
  };
79
80
  });
80
- let environment = server.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr];
81
- const virtualMod = environment.moduleGraph.getModuleById(ASTRO_ROUTES_MODULE_ID_RESOLVED);
82
- if (!virtualMod) return;
83
- environment.moduleGraph.invalidateModule(virtualMod);
84
- environment.hot.send("astro:routes-updated", {});
81
+ const environmentsToInvalidate = [];
82
+ for (const name of [
83
+ ASTRO_VITE_ENVIRONMENT_NAMES.ssr,
84
+ ASTRO_VITE_ENVIRONMENT_NAMES.prerender
85
+ ]) {
86
+ const environment = server.environments[name];
87
+ if (environment) {
88
+ environmentsToInvalidate.push(environment);
89
+ }
90
+ }
91
+ for (const environment of environmentsToInvalidate) {
92
+ const virtualMod = environment.moduleGraph.getModuleById(ASTRO_ROUTES_MODULE_ID_RESOLVED);
93
+ if (!virtualMod) continue;
94
+ environment.moduleGraph.invalidateModule(virtualMod);
95
+ const cssMod = environment.moduleGraph.getModuleById(RESOLVED_MODULE_DEV_CSS_ALL);
96
+ if (cssMod) {
97
+ environment.moduleGraph.invalidateModule(cssMod);
98
+ }
99
+ environment.hot.send("astro:routes-updated", {});
100
+ }
85
101
  }
86
102
  }
87
103
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "6.0.2",
3
+ "version": "6.0.4",
4
4
  "description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
5
5
  "type": "module",
6
6
  "author": "withastro",
@@ -154,8 +154,8 @@
154
154
  "yargs-parser": "^22.0.0",
155
155
  "zod": "^4.3.6",
156
156
  "@astrojs/internal-helpers": "0.8.0",
157
- "@astrojs/telemetry": "3.3.0",
158
- "@astrojs/markdown-remark": "7.0.0"
157
+ "@astrojs/markdown-remark": "7.0.0",
158
+ "@astrojs/telemetry": "3.3.0"
159
159
  },
160
160
  "optionalDependencies": {
161
161
  "sharp": "^0.34.0"
package/templates/env.mjs CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
1
  // @ts-check
3
2
  import { schema } from 'virtual:astro:env/internal';
4
3
  import {