astro 7.2.0 → 7.2.2

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 (43) hide show
  1. package/dist/assets/fonts/constants.d.ts +8 -0
  2. package/dist/assets/fonts/constants.js +2 -0
  3. package/dist/assets/fonts/vite-plugin-fonts.js +3 -1
  4. package/dist/assets/utils/node.js +35 -3
  5. package/dist/assets/utils/resolveImports.d.ts +0 -1
  6. package/dist/assets/utils/resolveImports.js +1 -4
  7. package/dist/cli/dev/index.js +2 -2
  8. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  9. package/dist/cli/preview/index.js +1 -1
  10. package/dist/cli/server.js +4 -4
  11. package/dist/content/content-layer.js +63 -3
  12. package/dist/content/loaders/file.js +20 -5
  13. package/dist/content/loaders/glob.js +18 -4
  14. package/dist/content/mutable-data-store.js +3 -3
  15. package/dist/content/runtime.d.ts +1 -0
  16. package/dist/content/runtime.js +6 -2
  17. package/dist/core/app/types.d.ts +2 -0
  18. package/dist/core/build/generate.js +10 -1
  19. package/dist/core/build/plugins/plugin-incremental.js +162 -36
  20. package/dist/core/build/plugins/plugin-manifest.js +11 -2
  21. package/dist/core/config/settings.js +1 -1
  22. package/dist/core/constants.js +1 -1
  23. package/dist/core/dev/dev.js +1 -1
  24. package/dist/core/dev/lockfile.d.ts +19 -2
  25. package/dist/core/dev/lockfile.js +25 -2
  26. package/dist/core/fetch/fetch-state.js +1 -0
  27. package/dist/core/messages/runtime.js +1 -1
  28. package/dist/core/middleware/vite-plugin.d.ts +1 -1
  29. package/dist/core/middleware/vite-plugin.js +11 -24
  30. package/dist/core/routing/dev.d.ts +3 -1
  31. package/dist/core/routing/dev.js +10 -2
  32. package/dist/core/util.js +2 -2
  33. package/dist/prefetch/index.js +18 -1
  34. package/dist/prefetch/speculation-rules.d.ts +6 -0
  35. package/dist/prefetch/speculation-rules.js +22 -0
  36. package/dist/runtime/server/render/head.js +10 -0
  37. package/dist/transitions/swap-functions.js +49 -4
  38. package/dist/types/public/config.d.ts +22 -7
  39. package/dist/types/public/internal.d.ts +2 -0
  40. package/dist/vite-plugin-app/app.d.ts +3 -1
  41. package/dist/vite-plugin-app/app.js +4 -3
  42. package/dist/vite-plugin-css/index.js +2 -2
  43. package/package.json +5 -4
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { FONTS_SERVER_ADDRESS_PLACEHOLDER } from "../../../assets/fonts/constants.js";
2
3
  import { PROPAGATED_ASSET_FLAG } from "../../../content/consts.js";
3
4
  import { hasContentFlag } from "../../../content/utils.js";
4
5
  import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../../constants.js";
@@ -7,24 +8,29 @@ import { rootRelativePath } from "../../viteUtils.js";
7
8
  import { moduleIsTopLevelPage } from "../graph.js";
8
9
  import { isContentDataIncrementalModule } from "../incremental-metadata.js";
9
10
  import { getPageDataByViteID } from "../internal.js";
10
- function collectTransitiveDeps(graph, rootId) {
11
- const deps = /* @__PURE__ */ new Set();
12
- const queue = [rootId];
13
- while (queue.length > 0) {
14
- const current = queue.pop();
15
- if (deps.has(current)) continue;
16
- const modInfo = graph.getModuleInfo(current);
17
- if (isContentDataIncrementalModule(modInfo)) continue;
18
- deps.add(current);
19
- if (!modInfo) continue;
20
- for (const dep of modInfo.importedIds) {
21
- if (!deps.has(dep)) queue.push(dep);
22
- }
23
- for (const dep of modInfo.dynamicallyImportedIds) {
24
- if (!deps.has(dep)) queue.push(dep);
25
- }
11
+ const ASSET_PLACEHOLDERS = [
12
+ { token: "__ASTRO_ASSET_IMAGE__", pattern: /__ASTRO_ASSET_IMAGE__([\w$]+)__(?:_(.*?)__)?/g },
13
+ { token: "__VITE_ASSET__", pattern: /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g }
14
+ ];
15
+ const FONTS_ADDRESS_DECLARATION = new RegExp(
16
+ `(?:const|let|var)\\s+${FONTS_SERVER_ADDRESS_PLACEHOLDER}\\s*=[^;]+;`
17
+ );
18
+ function resolveAssetPlaceholders(graph, code) {
19
+ let resolved = code;
20
+ for (const { token, pattern } of ASSET_PLACEHOLDERS) {
21
+ if (!resolved.includes(token)) continue;
22
+ resolved = resolved.replace(pattern, (placeholder, handle, postfix = "") => {
23
+ try {
24
+ return graph.getFileName(handle) + postfix;
25
+ } catch {
26
+ return placeholder;
27
+ }
28
+ });
29
+ }
30
+ if (resolved.includes(FONTS_SERVER_ADDRESS_PLACEHOLDER)) {
31
+ resolved = resolved.replace(FONTS_ADDRESS_DECLARATION, "");
26
32
  }
27
- return [...deps].sort();
33
+ return resolved;
28
34
  }
29
35
  function hashModules(graph, sortedIds) {
30
36
  const hasher = crypto.createHash("sha256");
@@ -33,17 +39,137 @@ function hashModules(graph, sortedIds) {
33
39
  hasher.update("\n");
34
40
  const code = graph.getModuleInfo(id)?.code;
35
41
  if (code != null) {
36
- hasher.update(code);
42
+ hasher.update(resolveAssetPlaceholders(graph, code));
37
43
  }
38
44
  hasher.update("\n");
39
45
  }
40
46
  return hasher.digest("hex");
41
47
  }
42
- function collectClientEntrypointHashes(graph, entrypointIds, pagesByEntrypoint, hashesByComponent) {
48
+ function createTransitiveGraphCache(graph) {
49
+ const modules = /* @__PURE__ */ new Map();
50
+ const dependencies = /* @__PURE__ */ new Map();
51
+ const excludedModules = /* @__PURE__ */ new Set();
52
+ const pending = [...graph.getModuleIds()];
53
+ for (const id of pending) {
54
+ if (modules.has(id)) continue;
55
+ const info = graph.getModuleInfo(id);
56
+ modules.set(id, info);
57
+ if (isContentDataIncrementalModule(info)) {
58
+ excludedModules.add(id);
59
+ continue;
60
+ }
61
+ const importedIds = [...info?.importedIds ?? [], ...info?.dynamicallyImportedIds ?? []];
62
+ dependencies.set(id, importedIds);
63
+ pending.push(...importedIds);
64
+ }
65
+ for (const id of excludedModules) modules.delete(id);
66
+ for (const [id, importedIds] of dependencies) {
67
+ dependencies.set(
68
+ id,
69
+ importedIds.filter((importedId) => !excludedModules.has(importedId))
70
+ );
71
+ }
72
+ const reverseDependencies = /* @__PURE__ */ new Map();
73
+ for (const id of modules.keys()) reverseDependencies.set(id, []);
74
+ for (const [id, importedIds] of dependencies) {
75
+ for (const importedId of importedIds) reverseDependencies.get(importedId)?.push(id);
76
+ }
77
+ const visited = /* @__PURE__ */ new Set();
78
+ const finishOrder = [];
79
+ for (const rootId of modules.keys()) {
80
+ if (visited.has(rootId)) continue;
81
+ visited.add(rootId);
82
+ const stack = [[rootId, 0]];
83
+ while (stack.length > 0) {
84
+ const frame = stack[stack.length - 1];
85
+ const importedIds = dependencies.get(frame[0]) ?? [];
86
+ if (frame[1] < importedIds.length) {
87
+ const importedId = importedIds[frame[1]++];
88
+ if (!visited.has(importedId)) {
89
+ visited.add(importedId);
90
+ stack.push([importedId, 0]);
91
+ }
92
+ } else {
93
+ finishOrder.push(frame[0]);
94
+ stack.pop();
95
+ }
96
+ }
97
+ }
98
+ const componentByModule = /* @__PURE__ */ new Map();
99
+ const components = [];
100
+ for (const rootId of finishOrder.toReversed()) {
101
+ if (componentByModule.has(rootId)) continue;
102
+ const componentIndex = components.length;
103
+ const component = [];
104
+ const stack = [rootId];
105
+ componentByModule.set(rootId, componentIndex);
106
+ while (stack.length > 0) {
107
+ const id = stack.pop();
108
+ component.push(id);
109
+ for (const importerId of reverseDependencies.get(id) ?? []) {
110
+ if (!componentByModule.has(importerId)) {
111
+ componentByModule.set(importerId, componentIndex);
112
+ stack.push(importerId);
113
+ }
114
+ }
115
+ }
116
+ components.push(component.sort());
117
+ }
118
+ const componentDependencies = components.map(() => /* @__PURE__ */ new Set());
119
+ const componentImporters = components.map(() => /* @__PURE__ */ new Set());
120
+ for (const [id, importedIds] of dependencies) {
121
+ const componentIndex = componentByModule.get(id);
122
+ for (const importedId of importedIds) {
123
+ const dependencyIndex = componentByModule.get(importedId);
124
+ if (dependencyIndex === componentIndex) continue;
125
+ componentDependencies[componentIndex].add(dependencyIndex);
126
+ componentImporters[dependencyIndex].add(componentIndex);
127
+ }
128
+ }
129
+ const componentHashes = /* @__PURE__ */ new Map();
130
+ const componentHasServerIsland = /* @__PURE__ */ new Map();
131
+ const unresolvedDependencies = componentDependencies.map((items) => items.size);
132
+ const ready = unresolvedDependencies.flatMap((count, index) => count === 0 ? [index] : []);
133
+ for (const componentIndex of ready) {
134
+ const hasher = crypto.createHash("sha256");
135
+ hasher.update(hashModules(graph, components[componentIndex]));
136
+ const dependencyHashes = [...componentDependencies[componentIndex]].map((dependencyIndex) => componentHashes.get(dependencyIndex)).sort();
137
+ for (const dependencyHash of dependencyHashes) {
138
+ hasher.update("\n");
139
+ hasher.update(dependencyHash);
140
+ }
141
+ componentHashes.set(componentIndex, hasher.digest("hex"));
142
+ componentHasServerIsland.set(
143
+ componentIndex,
144
+ components[componentIndex].some(
145
+ (id) => (modules.get(id)?.meta?.astro?.serverComponents?.length ?? 0) > 0
146
+ ) || [...componentDependencies[componentIndex]].some(
147
+ (dependencyIndex) => componentHasServerIsland.get(dependencyIndex)
148
+ )
149
+ );
150
+ for (const importerIndex of componentImporters[componentIndex]) {
151
+ unresolvedDependencies[importerIndex]--;
152
+ if (unresolvedDependencies[importerIndex] === 0) ready.push(importerIndex);
153
+ }
154
+ }
155
+ return {
156
+ hashes: new Map(
157
+ [...componentByModule].map(([id, componentIndex]) => [
158
+ id,
159
+ componentHashes.get(componentIndex)
160
+ ])
161
+ ),
162
+ serverIslandModules: new Set(
163
+ [...componentByModule].filter(([, componentIndex]) => componentHasServerIsland.get(componentIndex)).map(([id]) => id)
164
+ )
165
+ };
166
+ }
167
+ function collectClientEntrypointHashes(transitiveHashes, entrypointIds, pagesByEntrypoint, hashesByComponent) {
43
168
  for (const entrypointId of entrypointIds) {
44
169
  const pages = pagesByEntrypoint.get(entrypointId);
45
170
  if (!pages?.size) continue;
46
- const hash = hashModules(graph, collectTransitiveDeps(graph, entrypointId));
171
+ const hash = transitiveHashes.get(entrypointId);
172
+ if (!hash) continue;
47
173
  for (const pageData of pages) {
48
174
  let list = hashesByComponent.get(pageData.component);
49
175
  if (!list) {
@@ -57,15 +183,16 @@ function collectClientEntrypointHashes(graph, entrypointIds, pagesByEntrypoint,
57
183
  function foldClientDependencies(graph, internals) {
58
184
  const baseHashes = internals.pageDependencyHashes;
59
185
  if (!baseHashes) return;
186
+ const { hashes: transitiveHashes } = createTransitiveGraphCache(graph);
60
187
  const hashesByComponent = /* @__PURE__ */ new Map();
61
188
  collectClientEntrypointHashes(
62
- graph,
189
+ transitiveHashes,
63
190
  internals.discoveredClientOnlyComponents.keys(),
64
191
  internals.pagesByClientOnly,
65
192
  hashesByComponent
66
193
  );
67
194
  collectClientEntrypointHashes(
68
- graph,
195
+ transitiveHashes,
69
196
  internals.discoveredScripts,
70
197
  internals.pagesByScriptId,
71
198
  hashesByComponent
@@ -80,24 +207,17 @@ function foldClientDependencies(graph, internals) {
80
207
  baseHashes.set(component, hasher.digest("hex"));
81
208
  }
82
209
  }
83
- function collectContentEntryHashes(graph, root) {
210
+ function collectContentEntryHashes(graph, root, transitiveHashes) {
84
211
  const entryHashes = /* @__PURE__ */ new Map();
85
212
  for (const id of graph.getModuleIds()) {
86
213
  if (!hasContentFlag(id, PROPAGATED_ASSET_FLAG)) continue;
87
214
  const renderModuleId = removeQueryString(id);
88
215
  const key = rootRelativePath(root, renderModuleId, false);
89
- const deps = collectTransitiveDeps(graph, renderModuleId);
90
- entryHashes.set(key, hashModules(graph, deps));
216
+ const hash = transitiveHashes.get(renderModuleId);
217
+ if (hash) entryHashes.set(key, hash);
91
218
  }
92
219
  return entryHashes;
93
220
  }
94
- function pageContainsServerIsland(graph, ids) {
95
- for (const id of ids) {
96
- const serverComponents = graph.getModuleInfo(id)?.meta?.astro?.serverComponents;
97
- if (serverComponents?.length) return true;
98
- }
99
- return false;
100
- }
101
221
  function pluginIncremental(internals, root) {
102
222
  return {
103
223
  name: "@astro/plugin-incremental",
@@ -109,6 +229,7 @@ function pluginIncremental(internals, root) {
109
229
  foldClientDependencies(this, internals);
110
230
  return;
111
231
  }
232
+ const transitiveGraph = createTransitiveGraphCache(this);
112
233
  const hashes = /* @__PURE__ */ new Map();
113
234
  const serverIslandComponents = /* @__PURE__ */ new Set();
114
235
  for (const id of this.getModuleIds()) {
@@ -117,14 +238,19 @@ function pluginIncremental(internals, root) {
117
238
  if (!moduleIsTopLevelPage(info)) continue;
118
239
  const pageData = getPageDataByViteID(internals, info.id);
119
240
  if (!pageData) continue;
120
- const deps = collectTransitiveDeps(this, info.id);
121
- hashes.set(pageData.component, hashModules(this, deps));
122
- if (pageContainsServerIsland(this, deps)) {
241
+ const hash = transitiveGraph.hashes.get(info.id);
242
+ if (!hash) continue;
243
+ hashes.set(pageData.component, hash);
244
+ if (transitiveGraph.serverIslandModules.has(info.id)) {
123
245
  serverIslandComponents.add(pageData.component);
124
246
  }
125
247
  }
126
248
  internals.pageDependencyHashes = hashes;
127
- internals.contentEntryRenderHashes = collectContentEntryHashes(this, root);
249
+ internals.contentEntryRenderHashes = collectContentEntryHashes(
250
+ this,
251
+ root,
252
+ transitiveGraph.hashes
253
+ );
128
254
  internals.serverIslandPageComponents = serverIslandComponents;
129
255
  }
130
256
  };
@@ -21,7 +21,8 @@ import {
21
21
  trackStyleHashes
22
22
  } from "../../csp/common.js";
23
23
  import { partitionByKind } from "../../csp/runtime.js";
24
- import { encodeKey } from "../../encryption.js";
24
+ import { generateSpeculationRulesContent } from "../../../prefetch/speculation-rules.js";
25
+ import { encodeKey, generateCspDigest } from "../../encryption.js";
25
26
  import { fileExtension, joinPaths, prependForwardSlash } from "../../path.js";
26
27
  import { DEFAULT_COMPONENTS } from "../../routing/default.js";
27
28
  import { getOutFile, getOutFolder } from "../common.js";
@@ -204,6 +205,13 @@ async function buildManifest(opts, internals, staticFiles, encodedKey) {
204
205
  ...settings.injectedCsp.styleHashes,
205
206
  ...await trackStyleHashes(internals, settings, algorithm)
206
207
  ];
208
+ let speculationRulesContent;
209
+ if (settings.config.experimental.clientPrerender && settings.config.prefetch) {
210
+ const prefetchAll = typeof settings.config.prefetch === "object" ? settings.config.prefetch.prefetchAll ?? false : false;
211
+ speculationRulesContent = generateSpeculationRulesContent(prefetchAll);
212
+ const speculationRulesHash = await generateCspDigest(speculationRulesContent, algorithm);
213
+ scriptHashes.push(speculationRulesHash);
214
+ }
207
215
  const scriptDirective = {
208
216
  resources: getScriptResources(cspConfig),
209
217
  hashes: scriptHashes,
@@ -225,7 +233,8 @@ async function buildManifest(opts, internals, staticFiles, encodedKey) {
225
233
  styleHashes: styleDefault.hashes,
226
234
  styleResources: styleDefault.resources,
227
235
  scriptDirective,
228
- styleDirective
236
+ styleDirective,
237
+ speculationRulesContent
229
238
  };
230
239
  }
231
240
  let internalFetchHeaders = void 0;
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
- import yaml from "js-yaml";
3
+ import * as yaml from "js-yaml";
4
4
  import * as toml from "smol-toml";
5
5
  import { getContentPaths } from "../../content/index.js";
6
6
  import createPreferences from "../../preferences/index.js";
@@ -1,4 +1,4 @@
1
- const ASTRO_VERSION = "7.2.0";
1
+ const ASTRO_VERSION = "7.2.2";
2
2
  const ASTRO_GENERATOR = `Astro v${ASTRO_VERSION}`;
3
3
  const ASTRO_ERROR_HEADER = "X-Astro-Error";
4
4
  const DEFAULT_404_COMPONENT = "astro-default-404.astro";
@@ -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 = "7.2.0";
29
+ const currentVersion = "7.2.2";
30
30
  const isPrerelease = currentVersion.includes("-");
31
31
  if (!isPrerelease) {
32
32
  try {
@@ -32,6 +32,22 @@ export declare function serializeLockFile(data: LockFileData): string;
32
32
  * Signal 0 does not kill the process — it only checks whether the process exists.
33
33
  */
34
34
  export declare function isProcessAlive(pid: number): boolean;
35
+ /**
36
+ * Check whether a process command points to the Astro CLI.
37
+ */
38
+ export declare function isAstroCommand(command: string): boolean;
39
+ interface ProcessInfo {
40
+ pid: number;
41
+ cmd?: string;
42
+ }
43
+ type ProcessLookup = (by: 'pid', value: number, options: {
44
+ logLevel: 'error';
45
+ }) => Promise<ProcessInfo[]>;
46
+ /**
47
+ * Check whether the live process recorded in a lock file is still Astro.
48
+ * If the command cannot be inspected, keep the existing PID-only behavior.
49
+ */
50
+ export declare function isLockFileProcessAlive(data: LockFileData, find?: ProcessLookup): Promise<boolean>;
35
51
  /**
36
52
  * Read the lock file from disk. Returns null if it doesn't exist or is invalid.
37
53
  */
@@ -58,8 +74,9 @@ export declare function evaluateExistingServer(data: LockFileData | null, alive:
58
74
  */
59
75
  export declare function killDevServer(root: URL, data: LockFileData): Promise<void>;
60
76
  /**
61
- * Check for an existing server by reading the lock file and checking process liveness.
77
+ * Check for an existing server by reading the lock file and checking process identity.
62
78
  * Automatically cleans up stale lock files.
63
79
  * Returns the server info if a live server is found, null otherwise.
64
80
  */
65
- export declare function checkExistingServer(root: URL, command?: ServerCommand): LockFileData | null;
81
+ export declare function checkExistingServer(root: URL, command?: ServerCommand): Promise<LockFileData | null>;
82
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
+ import findProcess from "find-process";
3
4
  const GRACEFUL_SHUTDOWN_TIMEOUT = 5e3;
4
5
  function getLockFileURL(root, command = "dev") {
5
6
  return new URL(`.astro/${command}.json`, root);
@@ -42,6 +43,23 @@ function isProcessAlive(pid) {
42
43
  return false;
43
44
  }
44
45
  }
46
+ const ASTRO_COMMAND_PATTERN = /(?:^|[\\/\s"'])(?:astro[\\/]bin[\\/]astro\.mjs|\.bin[\\/]astro(?:\.cmd)?)(?=$|[\s"'])/i;
47
+ function isAstroCommand(command) {
48
+ return ASTRO_COMMAND_PATTERN.test(command);
49
+ }
50
+ async function isLockFileProcessAlive(data, find = findProcess) {
51
+ if (!isProcessAlive(data.pid)) {
52
+ return false;
53
+ }
54
+ try {
55
+ const processInfo = (await find("pid", data.pid, { logLevel: "error" })).find(
56
+ ({ pid }) => pid === data.pid
57
+ );
58
+ return processInfo?.cmd === void 0 || isAstroCommand(processInfo.cmd);
59
+ } catch {
60
+ return true;
61
+ }
62
+ }
45
63
  function readLockFile(root, command = "dev") {
46
64
  const lockFileURL = getLockFileURL(root, command);
47
65
  try {
@@ -98,9 +116,12 @@ async function killDevServer(root, data) {
98
116
  }
99
117
  removeLockFile(root);
100
118
  }
101
- function checkExistingServer(root, command = "dev") {
119
+ async function checkExistingServer(root, command = "dev") {
102
120
  const data = readLockFile(root, command);
103
- const result = evaluateExistingServer(data, data !== null && isProcessAlive(data.pid));
121
+ const result = evaluateExistingServer(
122
+ data,
123
+ data !== null && await isLockFileProcessAlive(data)
124
+ );
104
125
  if (result === null) {
105
126
  return null;
106
127
  }
@@ -115,6 +136,8 @@ export {
115
136
  checkExistingServer,
116
137
  evaluateExistingServer,
117
138
  getLogFileURL,
139
+ isAstroCommand,
140
+ isLockFileProcessAlive,
118
141
  isProcessAlive,
119
142
  killDevServer,
120
143
  parseLockFile,
@@ -330,6 +330,7 @@ class FetchState {
330
330
  resources: manifest.csp?.styleDirective ? [...manifest.csp.styleDirective.resources] : [],
331
331
  hashes: manifest.csp?.styleDirective ? [...manifest.csp.styleDirective.hashes] : []
332
332
  },
333
+ speculationRulesContent: manifest.csp?.speculationRulesContent,
333
334
  internalFetchHeaders: manifest.internalFetchHeaders
334
335
  };
335
336
  this.result = result;
@@ -270,7 +270,7 @@ function printHelp({
270
270
  message.push(
271
271
  linebreak(),
272
272
  ` ${bgGreen(black(` ${commandName} `))} ${green(
273
- `v${"7.2.0"}`
273
+ `v${"7.2.2"}`
274
274
  )} ${headline}`
275
275
  );
276
276
  }
@@ -1,4 +1,4 @@
1
- import { type Plugin as VitePlugin } from 'vite';
1
+ import type { Plugin as VitePlugin } from 'vite';
2
2
  import type { AstroSettings } from '../../types/astro.js';
3
3
  import type { BuildInternals } from '../build/internal.js';
4
4
  import type { StaticBuildOptions } from '../build/types.js';
@@ -1,13 +1,10 @@
1
- import { fileURLToPath } from "node:url";
2
- import {
3
- normalizePath as viteNormalizePath
4
- } from "vite";
5
1
  import { getServerOutputDirectory } from "../../prerender/utils.js";
6
2
  import { addRolldownInput } from "../build/add-rolldown-input.js";
7
3
  import { ASTRO_VITE_ENVIRONMENT_NAMES, MIDDLEWARE_PATH_SEGMENT_NAME } from "../constants.js";
8
4
  import { MissingMiddlewareForInternationalization } from "../errors/errors-data.js";
9
5
  import { AstroError } from "../errors/index.js";
10
6
  import { normalizePath } from "../viteUtils.js";
7
+ import { isAstroServerEnvironment } from "../../environments.js";
11
8
  const MIDDLEWARE_MODULE_ID = "virtual:astro:middleware";
12
9
  const MIDDLEWARE_RESOLVED_MODULE_ID = "\0" + MIDDLEWARE_MODULE_ID;
13
10
  const NOOP_MIDDLEWARE = "\0noop-middleware";
@@ -18,31 +15,21 @@ function vitePluginMiddleware({ settings }) {
18
15
  let resolvedMiddlewareId = void 0;
19
16
  const hasIntegrationMiddleware = settings.middlewares.pre.length > 0 || settings.middlewares.post.length > 0;
20
17
  let userMiddlewareIsPresent = false;
21
- const normalizedSrcDir = viteNormalizePath(fileURLToPath(settings.config.srcDir));
22
18
  return {
23
19
  name: "@astro/plugin-middleware",
24
20
  applyToEnvironment(environment) {
25
21
  return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.astro || environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
26
22
  },
27
- configureServer(server) {
28
- server.watcher.on("change", (path) => {
29
- const normalizedPath = viteNormalizePath(path);
30
- if (!normalizedPath.startsWith(normalizedSrcDir)) return;
31
- const relativePath = normalizedPath.slice(normalizedSrcDir.length);
32
- if (!isMiddlewarePath(relativePath)) return;
33
- for (const name of [
34
- ASTRO_VITE_ENVIRONMENT_NAMES.ssr,
35
- ASTRO_VITE_ENVIRONMENT_NAMES.astro
36
- ]) {
37
- const environment = server.environments[name];
38
- if (!environment) continue;
39
- const virtualMod = environment.moduleGraph.getModuleById(MIDDLEWARE_RESOLVED_MODULE_ID);
40
- if (virtualMod) {
41
- environment.moduleGraph.invalidateModule(virtualMod);
42
- }
43
- environment.hot.send("astro:middleware-updated", {});
44
- }
45
- });
23
+ hotUpdate: {
24
+ handler() {
25
+ if (!isAstroServerEnvironment(this.environment)) return;
26
+ const middlewareVirtualMod = this.environment.moduleGraph.getModuleById(
27
+ MIDDLEWARE_RESOLVED_MODULE_ID
28
+ );
29
+ if (!middlewareVirtualMod) return;
30
+ this.environment.moduleGraph.invalidateModule(middlewareVirtualMod);
31
+ this.environment.hot.send("astro:middleware-updated", {});
32
+ }
46
33
  },
47
34
  resolveId: {
48
35
  filter: {
@@ -10,5 +10,7 @@ interface MatchedRoute {
10
10
  filePath: URL;
11
11
  resolvedPathname: string;
12
12
  }
13
- export declare function matchRoute(pathname: string, routesList: RoutesList, pipeline: RunnablePipeline, manifest: SSRManifest): Promise<MatchedRoute | undefined>;
13
+ export declare function matchRoute(pathname: string, routesList: RoutesList, pipeline: RunnablePipeline, manifest: SSRManifest, { prerenderOnly }?: {
14
+ prerenderOnly?: boolean;
15
+ }): Promise<MatchedRoute | undefined>;
14
16
  export {};
@@ -5,7 +5,7 @@ import { getCustom404Route } from "./helpers.js";
5
5
  import { NoMatchingStaticPathFound } from "../errors/errors-data.js";
6
6
  import { isAstroError } from "../errors/errors.js";
7
7
  import { getErrorRoutePath } from "../../i18n/error-routes.js";
8
- async function matchRoute(pathname, routesList, pipeline, manifest) {
8
+ async function matchRoute(pathname, routesList, pipeline, manifest, { prerenderOnly } = {}) {
9
9
  const { logger, routeCache } = pipeline;
10
10
  const matches = matchAllRoutes(pathname, routesList);
11
11
  const preloadedMatches = getSortedPreloadedMatches({
@@ -13,7 +13,12 @@ async function matchRoute(pathname, routesList, pipeline, manifest) {
13
13
  manifest
14
14
  });
15
15
  let firstError = null;
16
+ let skippedPrerenderOnly = false;
16
17
  for await (const { route: maybeRoute, filePath } of preloadedMatches) {
18
+ if (prerenderOnly && !maybeRoute.prerender) {
19
+ skippedPrerenderOnly = true;
20
+ continue;
21
+ }
17
22
  try {
18
23
  await getProps({
19
24
  mod: await pipeline.getComponentByRoute(maybeRoute),
@@ -43,7 +48,10 @@ async function matchRoute(pathname, routesList, pipeline, manifest) {
43
48
  }
44
49
  const altPathname = pathname.replace(/\/index\.html$/, "/").replace(/\.html$/, "");
45
50
  if (altPathname !== pathname) {
46
- return await matchRoute(altPathname, routesList, pipeline, manifest);
51
+ return await matchRoute(altPathname, routesList, pipeline, manifest, { prerenderOnly });
52
+ }
53
+ if (skippedPrerenderOnly) {
54
+ return void 0;
47
55
  }
48
56
  if (matches.length) {
49
57
  const possibleRoutes = matches.flatMap((route) => route.component);
package/dist/core/util.js CHANGED
@@ -47,8 +47,8 @@ function resolvePages(config) {
47
47
  return new URL("./pages", config.srcDir);
48
48
  }
49
49
  function isInPagesDir(file, config) {
50
- const pagesDir = resolvePages(config);
51
- return file.toString().startsWith(pagesDir.toString());
50
+ const pagesDir = `${resolvePages(config).toString()}/`;
51
+ return file.toString().startsWith(pagesDir);
52
52
  }
53
53
  function isInjectedRoute(file, settings) {
54
54
  let fileURL = file.toString();
@@ -129,7 +129,7 @@ function prefetch(url, opts) {
129
129
  const ignoreSlowConnection = opts?.ignoreSlowConnection ?? false;
130
130
  if (!canPrefetchUrl(url, ignoreSlowConnection)) return;
131
131
  prefetchedUrls.add(url);
132
- if (clientPrerender && HTMLScriptElement.supports?.("speculationrules")) {
132
+ if (clientPrerender && HTMLScriptElement.supports?.("speculationrules") && !hasStaticSpeculationRules()) {
133
133
  debug?.(`[astro] Prefetching ${url} with <script type="speculationrules">`);
134
134
  appendSpeculationRules(url, opts?.eagerness ?? "immediate");
135
135
  } else if (document.createElement("link").relList?.supports?.("prefetch")) {
@@ -203,6 +203,23 @@ function onPageLoad(cb) {
203
203
  }
204
204
  }).observe(document.body, { childList: true, subtree: true });
205
205
  }
206
+ let _hasStaticRules;
207
+ function hasStaticSpeculationRules() {
208
+ if (_hasStaticRules === void 0) {
209
+ _hasStaticRules = Array.from(document.querySelectorAll('script[type="speculationrules"]')).some(
210
+ (el) => {
211
+ try {
212
+ const rules = JSON.parse(el.textContent ?? "");
213
+ const entries = [...rules.prerender ?? [], ...rules.prefetch ?? []];
214
+ return entries.some((entry) => entry.source === "document");
215
+ } catch {
216
+ return false;
217
+ }
218
+ }
219
+ );
220
+ }
221
+ return _hasStaticRules;
222
+ }
206
223
  function appendSpeculationRules(url, eagerness) {
207
224
  const script = document.createElement("script");
208
225
  script.type = "speculationrules";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Generates static speculation rules JSON using `"source": "document"` with CSS selector matching.
3
+ * This produces a deterministic payload that can be hashed at build time for CSP compatibility,
4
+ * unlike the dynamic per-URL `"source": "list"` approach in `appendSpeculationRules()`.
5
+ */
6
+ export declare function generateSpeculationRulesContent(prefetchAll: boolean): string;
@@ -0,0 +1,22 @@
1
+ function generateSpeculationRulesContent(prefetchAll) {
2
+ const selector = prefetchAll ? "a" : "a[data-astro-prefetch]";
3
+ return JSON.stringify({
4
+ prerender: [
5
+ {
6
+ source: "document",
7
+ where: { selector_matches: selector },
8
+ eagerness: "moderate"
9
+ }
10
+ ],
11
+ prefetch: [
12
+ {
13
+ source: "document",
14
+ where: { selector_matches: selector },
15
+ eagerness: "moderate"
16
+ }
17
+ ]
18
+ });
19
+ }
20
+ export {
21
+ generateSpeculationRulesContent
22
+ };
@@ -53,6 +53,16 @@ function renderAllHeadContent(result) {
53
53
  );
54
54
  const sep = result.compressHTML === true || result.compressHTML === "jsx" ? "" : "\n";
55
55
  content += styles.join(sep) + links.join(sep) + scripts.join(sep);
56
+ if (result.speculationRulesContent) {
57
+ content += renderElement(
58
+ "script",
59
+ {
60
+ props: { type: "speculationrules" },
61
+ children: result.speculationRulesContent
62
+ },
63
+ false
64
+ );
65
+ }
56
66
  content += result._metadata.extraHead.join("");
57
67
  return markHTMLString(content);
58
68
  }