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
@@ -15,3 +15,11 @@ export declare const FONT_FORMATS: Array<{
15
15
  }>;
16
16
  export declare const GENERIC_FALLBACK_NAMES: readonly ["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "emoji", "math", "fangsong"];
17
17
  export declare const FONTS_TYPES_FILE = "fonts.d.ts";
18
+ /**
19
+ * Variable name used in the font-file-url-resolver virtual module to hold
20
+ * the ephemeral font HTTP server address. The incremental build plugin
21
+ * strips the variable declaration (which contains an OS-assigned port that
22
+ * changes every build) from the module source before hashing so that the
23
+ * dependency hash is deterministic across builds.
24
+ */
25
+ export declare const FONTS_SERVER_ADDRESS_PLACEHOLDER = "__ASTRO_FONTS_SERVER_ADDRESS__";
@@ -39,10 +39,12 @@ const GENERIC_FALLBACK_NAMES = [
39
39
  "fangsong"
40
40
  ];
41
41
  const FONTS_TYPES_FILE = "fonts.d.ts";
42
+ const FONTS_SERVER_ADDRESS_PLACEHOLDER = "__ASTRO_FONTS_SERVER_ADDRESS__";
42
43
  export {
43
44
  ASSETS_DIR,
44
45
  CACHE_DIR,
45
46
  DEFAULTS,
47
+ FONTS_SERVER_ADDRESS_PLACEHOLDER,
46
48
  FONTS_TYPES_FILE,
47
49
  FONT_FORMATS,
48
50
  FONT_TYPES,
@@ -13,6 +13,7 @@ import {
13
13
  ASSETS_DIR,
14
14
  CACHE_DIR,
15
15
  DEFAULTS,
16
+ FONTS_SERVER_ADDRESS_PLACEHOLDER,
16
17
  RESOLVED_RUNTIME_FONT_FILE_URL_RESOLVER_VIRTUAL_MODULE_ID,
17
18
  RESOLVED_RUNTIME_VIRTUAL_MODULE_ID,
18
19
  RESOLVED_VIRTUAL_MODULE_ID,
@@ -285,9 +286,10 @@ function fontsPlugin({ settings, sync, logger }) {
285
286
  return {
286
287
  code: `
287
288
  import { RemoteRuntimeFontFileUrlResolver } from ${JSON.stringify(new URL("./infra/remote-runtime-font-file-url-resolver.js", import.meta.url))};
289
+ const ${FONTS_SERVER_ADDRESS_PLACEHOLDER} = ${JSON.stringify(serverAddress)};
288
290
  export const runtimeFontFileUrlResolver = new RemoteRuntimeFontFileUrlResolver({
289
291
  urls: new Set(${JSON.stringify(urls)}),
290
- address: ${JSON.stringify(serverAddress)},
292
+ address: ${FONTS_SERVER_ADDRESS_PLACEHOLDER},
291
293
  });
292
294
  `
293
295
  };
@@ -36,6 +36,35 @@ async function handleSvgDeduplication(fileData, filename, fileEmitter) {
36
36
  return handle;
37
37
  }
38
38
  }
39
+ const TRANSIENT_ERROR_CODES = /* @__PURE__ */ new Set(["EMFILE", "ENFILE", "EAGAIN", "EBUSY"]);
40
+ const MAX_CONCURRENT_READS = 200;
41
+ let activeReads = 0;
42
+ const readQueue = [];
43
+ async function readFileWithRetry(url, maxRetries = 5) {
44
+ if (activeReads >= MAX_CONCURRENT_READS) {
45
+ await new Promise((resolve) => readQueue.push(resolve));
46
+ }
47
+ activeReads++;
48
+ try {
49
+ for (let attempt = 0; ; attempt++) {
50
+ try {
51
+ return await fs.readFile(url);
52
+ } catch (err) {
53
+ const code = err instanceof Error && "code" in err ? err.code : void 0;
54
+ if (code && TRANSIENT_ERROR_CODES.has(code) && attempt < maxRetries) {
55
+ await new Promise((resolve) => setTimeout(resolve, 50 * 2 ** attempt));
56
+ continue;
57
+ }
58
+ throw err;
59
+ }
60
+ }
61
+ } finally {
62
+ activeReads--;
63
+ if (readQueue.length > 0) {
64
+ readQueue.shift()();
65
+ }
66
+ }
67
+ }
39
68
  async function emitImageMetadata(id, fileEmitter) {
40
69
  if (!id) {
41
70
  return void 0;
@@ -43,9 +72,12 @@ async function emitImageMetadata(id, fileEmitter) {
43
72
  const url = pathToFileURL(id);
44
73
  let fileData;
45
74
  try {
46
- fileData = await fs.readFile(url);
47
- } catch {
48
- return void 0;
75
+ fileData = await readFileWithRetry(url);
76
+ } catch (err) {
77
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
78
+ return void 0;
79
+ }
80
+ throw err;
49
81
  }
50
82
  const fileMetadata = await imageMetadata(fileData, id);
51
83
  const emittedImage = {
@@ -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
  };
@@ -128,7 +128,7 @@ Run \`astro dev --help\` to see available commands.`
128
128
  }
129
129
  const root = pathToFileURL(resolveRoot(flags.root) + "/");
130
130
  if (ignoreLock) {
131
- const existingServer2 = checkExistingServer(root);
131
+ const existingServer2 = await checkExistingServer(root);
132
132
  if (existingServer2) {
133
133
  logger.info(
134
134
  "SKIP_FORMAT",
@@ -141,7 +141,7 @@ Run \`astro dev --help\` to see available commands.`
141
141
  const inlineConfig2 = flagsToAstroInlineConfig(flags);
142
142
  return await devServer(inlineConfig2);
143
143
  }
144
- const existingServer = checkExistingServer(root);
144
+ const existingServer = await checkExistingServer(root);
145
145
  if (existingServer) {
146
146
  if (flags.force) {
147
147
  await killDevServer(root, existingServer);
@@ -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.2";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -69,7 +69,7 @@ Run \`astro preview --help\` to see available commands.`
69
69
  process.exit(1);
70
70
  }
71
71
  const root = pathToFileURL(resolveRoot(flags.root) + "/");
72
- const existingServer = checkExistingServer(root, "preview");
72
+ const existingServer = await checkExistingServer(root, "preview");
73
73
  if (existingServer) {
74
74
  const message = [
75
75
  "Another astro preview server is already running.",
@@ -101,7 +101,7 @@ async function background({
101
101
  config
102
102
  }) {
103
103
  const root = getRootURL(flags);
104
- const existing = checkExistingServer(root, config.command);
104
+ const existing = await checkExistingServer(root, config.command);
105
105
  if (existing && !flags.force) {
106
106
  logger.info("SKIP_FORMAT", formatServerRunningMessage(existing, config, { existing: true }));
107
107
  return;
@@ -160,7 +160,7 @@ async function stop({
160
160
  config
161
161
  }) {
162
162
  const root = getRootURL(flags);
163
- const existing = checkExistingServer(root, config.command);
163
+ const existing = await checkExistingServer(root, config.command);
164
164
  if (!existing) {
165
165
  logger.info("SKIP_FORMAT", `No ${config.command} server is running.`);
166
166
  return;
@@ -174,7 +174,7 @@ async function status({
174
174
  config
175
175
  }) {
176
176
  const root = getRootURL(flags);
177
- const existing = checkExistingServer(root, config.command);
177
+ const existing = await checkExistingServer(root, config.command);
178
178
  if (!existing) {
179
179
  logger.info("SKIP_FORMAT", `No ${config.command} server is running.`);
180
180
  return;
@@ -198,7 +198,7 @@ async function logs({
198
198
  config
199
199
  }) {
200
200
  const root = getRootURL(flags);
201
- const existing = checkExistingServer(root, config.command);
201
+ const existing = await checkExistingServer(root, config.command);
202
202
  if (!existing) {
203
203
  logger.error("SKIP_FORMAT", `No ${config.command} server is running.`);
204
204
  process.exit(1);
@@ -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.2") {
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.2") {
208
+ this.#store.metaStore().set("astro-version", "7.2.2");
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,8 +1,12 @@
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
- import { FileGlobNotSupported, FileParserNotFound } from "../../core/errors/errors-data.js";
5
+ import {
6
+ DuplicateContentEntrySlugError,
7
+ FileGlobNotSupported,
8
+ FileParserNotFound
9
+ } from "../../core/errors/errors-data.js";
6
10
  import { AstroError } from "../../core/errors/index.js";
7
11
  import { posixRelative } from "../utils.js";
8
12
  function file(fileName, options) {
@@ -27,7 +31,7 @@ function file(fileName, options) {
27
31
  message: FileParserNotFound.message(fileName)
28
32
  });
29
33
  }
30
- async function syncData(filePath, { logger, parseData, store, config }) {
34
+ async function syncData(filePath, { logger, parseData, store, config, collection }) {
31
35
  let data;
32
36
  try {
33
37
  const contents = await fs.readFile(filePath, "utf-8");
@@ -52,9 +56,20 @@ function file(fileName, options) {
52
56
  continue;
53
57
  }
54
58
  if (idList.has(id)) {
55
- logger.warn(
56
- `Duplicate id "${id}" found in ${fileName}. Later items with the same id will overwrite earlier ones.`
59
+ const message = DuplicateContentEntrySlugError.message(
60
+ collection,
61
+ id,
62
+ fileName,
63
+ fileName
57
64
  );
65
+ if (config.prerenderConflictBehavior === "error") {
66
+ throw new AstroError({
67
+ ...DuplicateContentEntrySlugError,
68
+ message
69
+ });
70
+ } else if (config.prerenderConflictBehavior !== "ignore") {
71
+ logger.warn(message);
72
+ }
58
73
  }
59
74
  idList.add(id);
60
75
  const parsedData = await parseData({ id, data: rawItem, filePath });
@@ -5,10 +5,12 @@ import pLimit from "p-limit";
5
5
  import colors from "piccolore";
6
6
  import picomatch from "picomatch";
7
7
  import { glob as tinyglobby } from "tinyglobby";
8
+ import * as AstroErrorData from "../../core/errors/errors-data.js";
9
+ import { AstroError } from "../../core/errors/index.js";
8
10
  import { getContentEntryIdAndSlug, posixRelative } from "../utils.js";
9
11
  function generateIdDefault({ entry, base, data }, isLegacy) {
10
12
  if (data.slug) {
11
- return data.slug;
13
+ return String(data.slug);
12
14
  }
13
15
  const entryURL = new URL(encodeURI(entry), base);
14
16
  if (isLegacy) {
@@ -45,7 +47,8 @@ function glob(globOptions) {
45
47
  );
46
48
  }
47
49
  const isLegacy = !!globOptions[secretLegacyFlag];
48
- const generateId = globOptions?.generateId ?? ((opts) => generateIdDefault(opts, isLegacy));
50
+ const userGenerateId = globOptions?.generateId ?? ((opts) => generateIdDefault(opts, isLegacy));
51
+ const generateId = (opts) => String(userGenerateId(opts));
49
52
  const fileToIdMap = /* @__PURE__ */ new Map();
50
53
  return {
51
54
  name: "glob-loader",
@@ -106,9 +109,20 @@ function glob(globOptions) {
106
109
  if (existingEntry && existingEntry.filePath && existingEntry.filePath !== relativePath2) {
107
110
  const oldFilePath = new URL(existingEntry.filePath, config.root);
108
111
  if (existsSync(oldFilePath)) {
109
- logger.warn(
110
- `Duplicate id "${id}" found in ${filePath2}. Later items with the same id will overwrite earlier ones.`
112
+ const message = AstroErrorData.DuplicateContentEntrySlugError.message(
113
+ collection,
114
+ id,
115
+ existingEntry.filePath,
116
+ relativePath2
111
117
  );
118
+ if (config.prerenderConflictBehavior === "error") {
119
+ throw new AstroError({
120
+ ...AstroErrorData.DuplicateContentEntrySlugError,
121
+ message
122
+ });
123
+ } else if (config.prerenderConflictBehavior !== "ignore") {
124
+ logger.warn(message);
125
+ }
112
126
  }
113
127
  }
114
128
  if (entryType.getRenderFunction && !globOptions.deferRender) {
@@ -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;
@@ -222,7 +222,16 @@ ${colors.bgGreen(colors.black(` ${verb} static routes `))}`);
222
222
  if (prerenderer.collectStaticImages) {
223
223
  const adapterImages = await prerenderer.collectStaticImages();
224
224
  for (const [path, entry] of adapterImages) {
225
- staticImageList.set(path, entry);
225
+ const existing = staticImageList.get(path);
226
+ if (existing) {
227
+ for (const [hash, transform] of entry.transforms) {
228
+ if (!existing.transforms.has(hash)) {
229
+ existing.transforms.set(hash, transform);
230
+ }
231
+ }
232
+ } else {
233
+ staticImageList.set(path, entry);
234
+ }
226
235
  }
227
236
  }
228
237
  } finally {