astro 7.2.0 → 7.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,4 +6,3 @@
6
6
  * @returns A module id of the image that can be resolved by Vite, or undefined if it is not a local image
7
7
  */
8
8
  export declare function imageSrcToImportId(imageSrc: string, filePath?: string): string | undefined;
9
- export declare const importIdToSymbolName: (importId: string) => string;
@@ -1,6 +1,5 @@
1
1
  import { isRemotePath, removeBase } from "@astrojs/internal-helpers/path";
2
2
  import { CONTENT_IMAGE_FLAG, IMAGE_IMPORT_PREFIX } from "../../content/consts.js";
3
- import { shorthash } from "../../runtime/server/shorthash.js";
4
3
  import { VALID_INPUT_FORMATS } from "../consts.js";
5
4
  function imageSrcToImportId(imageSrc, filePath) {
6
5
  imageSrc = removeBase(imageSrc, IMAGE_IMPORT_PREFIX);
@@ -17,8 +16,6 @@ function imageSrcToImportId(imageSrc, filePath) {
17
16
  }
18
17
  return `${imageSrc}?${params.toString()}`;
19
18
  }
20
- const importIdToSymbolName = (importId) => `__ASTRO_IMAGE_IMPORT_${shorthash(importId)}`;
21
19
  export {
22
- imageSrcToImportId,
23
- importIdToSymbolName
20
+ imageSrcToImportId
24
21
  };
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "7.2.0";
3
+ version = "7.2.1";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -196,7 +196,7 @@ ${contentConfig.error.message}`
196
196
  logger.info("Content config changed");
197
197
  shouldClear = true;
198
198
  }
199
- if (previousAstroVersion && previousAstroVersion !== "7.2.0") {
199
+ if (previousAstroVersion && previousAstroVersion !== "7.2.1") {
200
200
  logger.info("Astro version changed");
201
201
  shouldClear = true;
202
202
  }
@@ -204,8 +204,8 @@ ${contentConfig.error.message}`
204
204
  logger.info("Clearing content store");
205
205
  this.#store.clearAll();
206
206
  }
207
- if ("7.2.0") {
208
- this.#store.metaStore().set("astro-version", "7.2.0");
207
+ if ("7.2.1") {
208
+ this.#store.metaStore().set("astro-version", "7.2.1");
209
209
  }
210
210
  if (currentConfigDigest) {
211
211
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -265,6 +265,7 @@ ${contentConfig.error.message}`
265
265
  }
266
266
  })
267
267
  );
268
+ this.#validateReferences(contentConfig.config.collections, logger);
268
269
  await fs.mkdir(this.#settings.config.cacheDir, { recursive: true });
269
270
  await fs.mkdir(this.#settings.dotAstroDir, { recursive: true });
270
271
  const assetImportsFile = new URL(ASSET_IMPORTS_FILE, this.#settings.dotAstroDir);
@@ -277,6 +278,65 @@ ${contentConfig.error.message}`
277
278
  await this.regenerateCollectionFileManifest();
278
279
  }
279
280
  }
281
+ /**
282
+ * After all loaders complete, walks every entry's data to find reference objects
283
+ * (`{ id, collection }`) and checks that the referenced entry exists in the store.
284
+ * This replaces the inline Zod validation that was removed in the Zod 4 upgrade.
285
+ */
286
+ #validateReferences(collections, logger) {
287
+ const collectionNames = new Set(Object.keys(collections));
288
+ for (const collectionName of collectionNames) {
289
+ for (const entry of this.#store.values(collectionName)) {
290
+ if (entry?.data) {
291
+ this.#findInvalidReferences(
292
+ entry.data,
293
+ collectionNames,
294
+ collectionName,
295
+ entry.id,
296
+ logger,
297
+ ""
298
+ );
299
+ }
300
+ }
301
+ }
302
+ }
303
+ #findInvalidReferences(value, collectionNames, ownerCollection, ownerId, logger, path) {
304
+ if (value == null || typeof value !== "object") return;
305
+ if (Array.isArray(value)) {
306
+ for (let i = 0; i < value.length; i++) {
307
+ this.#findInvalidReferences(
308
+ value[i],
309
+ collectionNames,
310
+ ownerCollection,
311
+ ownerId,
312
+ logger,
313
+ `${path}[${i}]`
314
+ );
315
+ }
316
+ return;
317
+ }
318
+ const obj = value;
319
+ if (typeof obj.collection === "string" && collectionNames.has(obj.collection)) {
320
+ const refId = typeof obj.id === "string" ? obj.id : typeof obj.slug === "string" ? obj.slug : void 0;
321
+ if (refId !== void 0 && !this.#store.has(obj.collection, refId)) {
322
+ const fieldPath = path ? ` (field: ${path})` : "";
323
+ logger.error(
324
+ `Invalid content reference: entry "${ownerId}" in collection "${ownerCollection}"${fieldPath} references "${refId}" in collection "${obj.collection}", but that entry does not exist.`
325
+ );
326
+ }
327
+ return;
328
+ }
329
+ for (const [key, val] of Object.entries(obj)) {
330
+ this.#findInvalidReferences(
331
+ val,
332
+ collectionNames,
333
+ ownerCollection,
334
+ ownerId,
335
+ logger,
336
+ path ? `${path}.${key}` : key
337
+ );
338
+ }
339
+ }
280
340
  async regenerateCollectionFileManifest() {
281
341
  const collectionsManifest = new URL(COLLECTIONS_MANIFEST_FILE, this.#settings.dotAstroDir);
282
342
  this.#logger.debug("content", "Regenerating collection file manifest");
@@ -1,6 +1,6 @@
1
1
  import { existsSync, promises as fs } from "node:fs";
2
2
  import { fileURLToPath } 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 { FileGlobNotSupported, FileParserNotFound } from "../../core/errors/errors-data.js";
6
6
  import { AstroError } from "../../core/errors/index.js";
@@ -8,7 +8,7 @@ import { glob as tinyglobby } from "tinyglobby";
8
8
  import { getContentEntryIdAndSlug, posixRelative } from "../utils.js";
9
9
  function generateIdDefault({ entry, base, data }, isLegacy) {
10
10
  if (data.slug) {
11
- return data.slug;
11
+ return String(data.slug);
12
12
  }
13
13
  const entryURL = new URL(encodeURI(entry), base);
14
14
  if (isLegacy) {
@@ -45,7 +45,8 @@ function glob(globOptions) {
45
45
  );
46
46
  }
47
47
  const isLegacy = !!globOptions[secretLegacyFlag];
48
- const generateId = globOptions?.generateId ?? ((opts) => generateIdDefault(opts, isLegacy));
48
+ const userGenerateId = globOptions?.generateId ?? ((opts) => generateIdDefault(opts, isLegacy));
49
+ const generateId = (opts) => String(userGenerateId(opts));
49
50
  const fileToIdMap = /* @__PURE__ */ new Map();
50
51
  return {
51
52
  name: "glob-loader",
@@ -2,7 +2,7 @@ import { existsSync, promises as fs } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import * as devalue from "devalue";
4
4
  import { forEach } from "neotraverse";
5
- import { imageSrcToImportId, importIdToSymbolName } from "../assets/utils/resolveImports.js";
5
+ import { imageSrcToImportId } from "../assets/utils/resolveImports.js";
6
6
  import { AstroError, AstroErrorData } from "../core/errors/index.js";
7
7
  import { DATA_STORE_MANIFEST_FILE, IMAGE_IMPORT_PREFIX } from "./consts.js";
8
8
  import {
@@ -109,8 +109,8 @@ class MutableDataStore extends ImmutableDataStore {
109
109
  const imports = [];
110
110
  const exports = [];
111
111
  const sortedAssetImports = [...this.#assetImports].sort();
112
- sortedAssetImports.forEach((id) => {
113
- const symbol = importIdToSymbolName(id);
112
+ sortedAssetImports.forEach((id, index) => {
113
+ const symbol = `__ASTRO_IMAGE_IMPORT_${index}`;
114
114
  imports.push(`import ${symbol} from ${JSON.stringify(id)};`);
115
115
  exports.push(`[${JSON.stringify(id)}, ${symbol}]`);
116
116
  });
@@ -71,6 +71,7 @@ type RenderResult = {
71
71
  remarkPluginFrontmatter: Record<string, any>;
72
72
  };
73
73
  export declare function updateImageReferencesInData<T extends Record<string, unknown>>(data: T, fileName?: string, imageAssetMap?: Map<string, ImageMetadata>): T;
74
+ export declare function resolveEntryData<T extends Record<string, unknown>>(entry: DataEntry<T>, imageAssetMap?: Map<string, ImageMetadata>): T;
74
75
  export declare function renderEntry(entry: DataEntry): Promise<RenderResult>;
75
76
  export declare function createReference(): (collection: string) => z.ZodPipe<z.ZodUnion<readonly [z.ZodPipe<z.ZodNumber, z.ZodTransform<string, number>>, z.ZodString, z.ZodObject<{
76
77
  id: z.ZodString;
@@ -78,7 +78,7 @@ function createGetCollection({
78
78
  const { default: imageAssetMap } = await import("astro:asset-imports");
79
79
  const result = [];
80
80
  for (const rawEntry of await store.values(collection)) {
81
- const data = updateImageReferencesInData(rawEntry.data, rawEntry.filePath, imageAssetMap);
81
+ const data = resolveEntryData(rawEntry, imageAssetMap);
82
82
  let entry = {
83
83
  ...rawEntry,
84
84
  data,
@@ -135,7 +135,7 @@ function createGetEntry({ liveCollections }) {
135
135
  return;
136
136
  }
137
137
  const { default: imageAssetMap } = await import("astro:asset-imports");
138
- const data = updateImageReferencesInData(entry.data, entry.filePath, imageAssetMap);
138
+ const data = resolveEntryData(entry, imageAssetMap);
139
139
  const result = {
140
140
  ...entry,
141
141
  data,
@@ -382,6 +382,9 @@ function updateImageReferencesInData(data, fileName, imageAssetMap) {
382
382
  });
383
383
  return copy;
384
384
  }
385
+ function resolveEntryData(entry, imageAssetMap) {
386
+ return entry.assetImports?.length ? updateImageReferencesInData(entry.data, entry.filePath, imageAssetMap) : structuredClone(entry.data);
387
+ }
385
388
  async function renderEntry(entry) {
386
389
  if (!entry) {
387
390
  throw new AstroError(AstroErrorData.RenderUndefinedEntryError);
@@ -563,5 +566,6 @@ export {
563
566
  defineCollection,
564
567
  defineLiveCollection,
565
568
  renderEntry,
569
+ resolveEntryData,
566
570
  updateImageReferencesInData
567
571
  };
@@ -193,6 +193,8 @@ export type SSRManifestCSP = {
193
193
  resources: CspResourceEntry[];
194
194
  hashes: CspHashEntry[];
195
195
  };
196
+ /** Static speculation rules JSON to inject in the head when CSP + clientPrerender are both enabled. */
197
+ speculationRulesContent?: string;
196
198
  };
197
199
  export interface SSRManifestSession extends BaseSessionConfig {
198
200
  driver: string;
@@ -26,6 +26,24 @@ function collectTransitiveDeps(graph, rootId) {
26
26
  }
27
27
  return [...deps].sort();
28
28
  }
29
+ const ASSET_PLACEHOLDERS = [
30
+ { token: "__ASTRO_ASSET_IMAGE__", pattern: /__ASTRO_ASSET_IMAGE__([\w$]+)__(?:_(.*?)__)?/g },
31
+ { token: "__VITE_ASSET__", pattern: /__VITE_ASSET__([\w$]+)__(?:\$_(.*?)__)?/g }
32
+ ];
33
+ function resolveAssetPlaceholders(graph, code) {
34
+ let resolved = code;
35
+ for (const { token, pattern } of ASSET_PLACEHOLDERS) {
36
+ if (!resolved.includes(token)) continue;
37
+ resolved = resolved.replace(pattern, (placeholder, handle, postfix = "") => {
38
+ try {
39
+ return graph.getFileName(handle) + postfix;
40
+ } catch {
41
+ return placeholder;
42
+ }
43
+ });
44
+ }
45
+ return resolved;
46
+ }
29
47
  function hashModules(graph, sortedIds) {
30
48
  const hasher = crypto.createHash("sha256");
31
49
  for (const id of sortedIds) {
@@ -33,7 +51,7 @@ function hashModules(graph, sortedIds) {
33
51
  hasher.update("\n");
34
52
  const code = graph.getModuleInfo(id)?.code;
35
53
  if (code != null) {
36
- hasher.update(code);
54
+ hasher.update(resolveAssetPlaceholders(graph, code));
37
55
  }
38
56
  hasher.update("\n");
39
57
  }
@@ -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.1";
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.1";
30
30
  const isPrerelease = currentVersion.includes("-");
31
31
  if (!isPrerelease) {
32
32
  try {
@@ -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.1"}`
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);
@@ -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
  }
@@ -1,7 +1,30 @@
1
1
  import { SERVER_ISLAND_START } from "../runtime/server/render/server-islands-shared.js";
2
2
  const PERSIST_ATTR = "data-astro-transition-persist";
3
3
  const NON_OVERRIDABLE_ASTRO_ATTRS = ["data-astro-transition", "data-astro-transition-fallback"];
4
- const knownVueScopedStyles = /* @__PURE__ */ new Map();
4
+ const viteStyleState = import.meta.env.DEV ? /* @__PURE__ */ (() => {
5
+ const styles = /* @__PURE__ */ new Map();
6
+ let observer;
7
+ return {
8
+ styles,
9
+ observe() {
10
+ if (observer) return;
11
+ observer = new MutationObserver((records) => {
12
+ for (const record of records) {
13
+ for (const node of record.addedNodes) {
14
+ if (!(node instanceof HTMLStyleElement)) continue;
15
+ const viteDevId = node.dataset.viteDevId;
16
+ if (!viteDevId) continue;
17
+ const knownStyle = styles.get(viteDevId);
18
+ if (node === knownStyle) continue;
19
+ knownStyle?.remove();
20
+ styles.set(viteDevId, node);
21
+ }
22
+ }
23
+ });
24
+ observer.observe(document.head, { childList: true });
25
+ }
26
+ };
27
+ })() : void 0;
5
28
  const scriptsAlreadyRan = /* @__PURE__ */ new Set();
6
29
  function detectScriptExecuted(script) {
7
30
  const key = script.src ? new URL(script.src, location.href).href : script.textContent;
@@ -41,8 +64,8 @@ function swapHeadElements(doc) {
41
64
  newEl.remove();
42
65
  } else {
43
66
  if (import.meta.env.DEV && el instanceof HTMLStyleElement) {
44
- const viteDevId = vueScopedStyleId(el);
45
- viteDevId && knownVueScopedStyles.set(viteDevId, el);
67
+ const viteDevId = el.dataset.viteDevId;
68
+ viteDevId && viteStyleState?.styles.set(viteDevId, el);
46
69
  }
47
70
  el.remove();
48
71
  }
@@ -50,7 +73,18 @@ function swapHeadElements(doc) {
50
73
  relevantNodes(document.head, "commentsOnly").forEach((node) => node.remove());
51
74
  if (import.meta.env.DEV) {
52
75
  relevantNodes(doc.head).forEach((child) => {
53
- document.head.append(knownVueScopedStyles.get(child.dataset?.viteDevId) || child);
76
+ const viteDevId = child instanceof HTMLStyleElement && child.dataset.viteDevId;
77
+ const knownStyle = viteDevId && viteStyleState?.styles.get(viteDevId);
78
+ if (knownStyle) {
79
+ if (!vueScopedStyleId(knownStyle)) knownStyle.textContent = child.textContent;
80
+ document.head.append(knownStyle);
81
+ } else {
82
+ if (viteDevId) {
83
+ viteStyleState?.styles.set(viteDevId, child);
84
+ viteStyleState?.observe();
85
+ }
86
+ document.head.append(child);
87
+ }
54
88
  });
55
89
  } else {
56
90
  document.head.append(...relevantNodes(doc.head));
@@ -85,6 +119,17 @@ function swapBodyElement(newElement, oldElement) {
85
119
  }
86
120
  }
87
121
  attachShadowRoots(newElement);
122
+ reifyMediaElements(newElement);
123
+ }
124
+ function reifyMediaElements(root) {
125
+ for (const media of root.querySelectorAll("video, audio")) {
126
+ const fresh = document.createElement(media.localName);
127
+ for (const attr of media.attributes) {
128
+ fresh.setAttribute(attr.name, attr.value);
129
+ }
130
+ fresh.innerHTML = media.innerHTML;
131
+ media.replaceWith(fresh);
132
+ }
88
133
  }
89
134
  function attachShadowRoots(root) {
90
135
  root.querySelectorAll("template[shadowrootmode]").forEach((template) => {
@@ -2246,7 +2246,9 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
2246
2246
  * @docs
2247
2247
  * @name markdown.remarkPlugins
2248
2248
  * @type {RemarkPlugins}
2249
- * @deprecated Pass `remarkPlugins` to `unified({ remarkPlugins })` from `@astrojs/markdown-remark` and set it as `markdown.processor` instead. Will be removed in a future major.
2249
+ * @deprecated This property is deprecated and will be removed in a future major version. Pass plugins to the configured [`markdown.processor`](https://docs.astro.build/en/reference/configuration-reference/#markdownprocessor) instead.
2250
+ *
2251
+ * Learn more about [setting up a Markdown processor](https://docs.astro.build/en/guides/markdown-content/#setting-up-a-markdown-processor) and [using plugins](https://docs.astro.build/en/guides/markdown-content/#markdown-processor-plugins) in the Markdown guide.
2250
2252
  * @description
2251
2253
  * Pass [remark plugins](https://github.com/remarkjs/remark) to customize how your Markdown is built. You can import and apply the plugin function (recommended), or pass the plugin name as a string.
2252
2254
  *
@@ -2264,7 +2266,9 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
2264
2266
  * @docs
2265
2267
  * @name markdown.rehypePlugins
2266
2268
  * @type {RehypePlugins}
2267
- * @deprecated Pass `rehypePlugins` to `unified({ rehypePlugins })` from `@astrojs/markdown-remark` and set it as `markdown.processor` instead. Will be removed in a future major.
2269
+ * @deprecated This property is deprecated and will be removed in a future major version. Pass plugins to the configured [`markdown.processor`](https://docs.astro.build/en/reference/configuration-reference/#markdownprocessor) instead.
2270
+ *
2271
+ * Learn more about [setting up a Markdown processor](https://docs.astro.build/en/guides/markdown-content/#setting-up-a-markdown-processor) and [using plugins](https://docs.astro.build/en/guides/markdown-content/#markdown-processor-plugins) in the Markdown guide.
2268
2272
  * @description
2269
2273
  * Pass [rehype plugins](https://github.com/remarkjs/remark-rehype) to customize how your Markdown's output HTML is processed. You can import and apply the plugin function (recommended), or pass the plugin name as a string.
2270
2274
  *
@@ -2284,7 +2288,9 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
2284
2288
  * @type {boolean}
2285
2289
  * @default `true`
2286
2290
  * @version 2.0.0
2287
- * @deprecated Pass `gfm` to your processor instead (e.g. `unified({ gfm: false })`). Will be removed in a future major.
2291
+ * @deprecated This property is deprecated and will be removed in a future major version. Pass `gfm` to the configured [`markdown.processor`](https://docs.astro.build/en/reference/configuration-reference/#markdownprocessor) instead.
2292
+ *
2293
+ * Learn more about [setting up a Markdown processor](https://docs.astro.build/en/guides/markdown-content/#setting-up-a-markdown-processor) and [using GitHub-flavored Markdown](https://docs.astro.build/en/guides/markdown-content/#github-flavored-markdown) in the Markdown guide.
2288
2294
  * @description
2289
2295
  * Astro uses [GitHub-flavored Markdown](https://github.com/remarkjs/remark-gfm) by default. To disable this, set the `gfm` flag to `false`:
2290
2296
  *
@@ -2303,7 +2309,9 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
2303
2309
  * @type {boolean | Smartypants}
2304
2310
  * @default `true`
2305
2311
  * @version 2.0.0
2306
- * @deprecated Pass `smartypants` to your processor instead (e.g. `unified({ smartypants: false })`). Will be removed in a future major.
2312
+ * @deprecated This property is deprecated and will be removed in a future major version. Pass it to the configured [`markdown.processor`](https://docs.astro.build/en/reference/configuration-reference/#markdownprocessor) instead. Use `smartypants` for `unified()` or `smartPunctuation` for `satteri()`.
2313
+ *
2314
+ * Learn more about [setting up a Markdown processor](https://docs.astro.build/en/guides/markdown-content/#setting-up-a-markdown-processor) and [using smart punctuation](https://docs.astro.build/en/guides/markdown-content/#smart-punctuation) in the Markdown guide.
2307
2315
  * @description
2308
2316
  * Whether to use the [SmartyPants formatter](https://daringfireball.net/projects/smartypants/) to transform straight quotes into smart quotes, dashes into en/em dashes, and triple dots into ellipses.
2309
2317
  *
@@ -2316,7 +2324,11 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
2316
2324
  * @docs
2317
2325
  * @name markdown.remarkRehype
2318
2326
  * @type {RemarkRehype}
2319
- * @deprecated Pass `remarkRehype` to `unified({ remarkRehype })` from `@astrojs/markdown-remark` and set it as `markdown.processor` instead. Will be removed in a future major.
2327
+ * @deprecated This property is deprecated and will be removed in a future major version.
2328
+ *
2329
+ * To configure footnotes, pass `remarkRehype` to the `unified()` processor or `gfm.footnotes` to the `satteri()` processor instead. Other `remark-rehype` options are only supported when using `unified()`.
2330
+ *
2331
+ * Learn more about [setting up a Markdown processor](https://docs.astro.build/en/guides/markdown-content/#setting-up-a-markdown-processor) and [using built-in features](https://docs.astro.build/en/guides/markdown-content/#built-in-features) in the Markdown guide.
2320
2332
  * @description
2321
2333
  * Pass options to [remark-rehype](https://github.com/remarkjs/remark-rehype#api).
2322
2334
  *
@@ -2336,8 +2348,9 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
2336
2348
  * @type {MarkdownProcessor}
2337
2349
  * @version 6.4.0
2338
2350
  * @description
2339
- * Configures the Markdown processor used to render `.md` files. Defaults to `satteri()` from
2340
- * `@astrojs/markdown-satteri`, Astro's native Markdown pipeline.
2351
+ * Configures the [Markdown processor](https://docs.astro.build/en/guides/markdown-content/#markdown-processors) used to render `.md` files.
2352
+ *
2353
+ * Sätteri, Astro’s native Markdown pipeline, is the default processor. To configure it, install `@astrojs/markdown-satteri` and pass options to `satteri()`:
2341
2354
  *
2342
2355
  * ```js
2343
2356
  * // astro.config.mjs
@@ -2369,6 +2382,8 @@ export interface AstroUserConfig<TLocales extends Locales = never, TDriver exten
2369
2382
  * },
2370
2383
  * });
2371
2384
  * ```
2385
+ *
2386
+ * Learn more about the [official Markdown processors and how to choose one](https://docs.astro.build/en/guides/markdown-content/#choosing-a-markdown-processor) in the Markdown guide.
2372
2387
  */
2373
2388
  processor?: MarkdownProcessor;
2374
2389
  };
@@ -231,6 +231,8 @@ export interface SSRResult {
231
231
  isStrictDynamic: SSRManifestCSP['isStrictDynamic'];
232
232
  scriptDirective: SSRManifestCSP['scriptDirective'];
233
233
  styleDirective: SSRManifestCSP['styleDirective'];
234
+ /** Static speculation rules JSON to inject in the head when CSP + clientPrerender are both enabled. */
235
+ speculationRulesContent?: string;
234
236
  internalFetchHeaders?: Record<string, string>;
235
237
  }
236
238
  /**
@@ -35,7 +35,9 @@ export declare class AstroServerApp extends BaseApp<RunnablePipeline> {
35
35
  * Called via HMR when action files change.
36
36
  */
37
37
  clearActions(): void;
38
- devMatch(pathname: string): Promise<DevMatch | undefined>;
38
+ devMatch(pathname: string, { prerenderOnly }?: {
39
+ prerenderOnly?: boolean;
40
+ }): Promise<DevMatch | undefined>;
39
41
  static create(manifest: SSRManifest, routesList: RoutesList, logger: AstroLogger, loader: ModuleLoader, settings: AstroSettings, getDebugInfo: () => Promise<string>): Promise<AstroServerApp>;
40
42
  createPipeline(_streaming: boolean, manifest: SSRManifest, settings: AstroSettings, logger: AstroLogger, loader: ModuleLoader, manifestData: RoutesList, getDebugInfo: () => Promise<string>): RunnablePipeline;
41
43
  /**
@@ -70,12 +70,13 @@ class AstroServerApp extends BaseApp {
70
70
  clearActions() {
71
71
  this.pipeline.clearActions();
72
72
  }
73
- async devMatch(pathname) {
73
+ async devMatch(pathname, { prerenderOnly } = {}) {
74
74
  const matchedRoute = await matchRoute(
75
75
  pathname,
76
76
  this.manifestData,
77
77
  this.pipeline,
78
- this.manifest
78
+ this.manifest,
79
+ { prerenderOnly }
79
80
  );
80
81
  if (!matchedRoute) {
81
82
  return void 0;
@@ -132,7 +133,7 @@ class AstroServerApp extends BaseApp {
132
133
  controller,
133
134
  pathname,
134
135
  async run() {
135
- const matchedRoute = await self.devMatch(pathname);
136
+ const matchedRoute = await self.devMatch(pathname, { prerenderOnly });
136
137
  if (!matchedRoute) {
137
138
  if (prerenderOnly) {
138
139
  handled = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro",
3
- "version": "7.2.0",
3
+ "version": "7.2.1",
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",