astro 7.2.1 → 7.2.3

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 (166) hide show
  1. package/dist/actions/handler.d.ts +6 -12
  2. package/dist/actions/handler.js +30 -40
  3. package/dist/actions/load.d.ts +12 -0
  4. package/dist/actions/load.js +55 -0
  5. package/dist/actions/runtime/entrypoints/server.js +5 -4
  6. package/dist/actions/runtime/server.js +8 -6
  7. package/dist/assets/fonts/constants.d.ts +8 -0
  8. package/dist/assets/fonts/constants.js +2 -0
  9. package/dist/assets/fonts/providers/index.d.ts +1 -1
  10. package/dist/assets/fonts/providers/index.js +2 -12
  11. package/dist/assets/fonts/vite-plugin-fonts.js +3 -1
  12. package/dist/assets/utils/node.js +35 -3
  13. package/dist/cli/dev/index.js +2 -2
  14. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  15. package/dist/cli/preview/index.js +1 -1
  16. package/dist/cli/server.js +4 -4
  17. package/dist/container/environment.d.ts +20 -0
  18. package/dist/container/environment.js +106 -0
  19. package/dist/container/index.js +65 -32
  20. package/dist/content/content-layer.js +3 -3
  21. package/dist/content/data-store-writer.d.ts +7 -10
  22. package/dist/content/data-store-writer.js +62 -27
  23. package/dist/content/data-store.d.ts +20 -3
  24. package/dist/content/data-store.js +28 -7
  25. package/dist/content/loaders/file.js +19 -4
  26. package/dist/content/loaders/glob.js +15 -2
  27. package/dist/content/mutable-data-store.js +15 -10
  28. package/dist/content/runtime.d.ts +3 -2
  29. package/dist/content/runtime.js +43 -26
  30. package/dist/core/app/app.d.ts +0 -2
  31. package/dist/core/app/app.js +0 -7
  32. package/dist/core/app/base.d.ts +32 -20
  33. package/dist/core/app/base.js +76 -66
  34. package/dist/core/app/dev-facade.d.ts +27 -0
  35. package/dist/core/app/dev-facade.js +51 -0
  36. package/dist/core/app/entrypoints/index.d.ts +0 -1
  37. package/dist/core/app/entrypoints/index.js +0 -2
  38. package/dist/core/app/entrypoints/virtual/dev.js +24 -19
  39. package/dist/core/app/node.js +13 -14
  40. package/dist/core/app/prepare-response.d.ts +1 -1
  41. package/dist/core/app/validate-headers.js +7 -3
  42. package/dist/core/build/app.d.ts +31 -6
  43. package/dist/core/build/app.js +31 -13
  44. package/dist/core/build/environment.d.ts +25 -0
  45. package/dist/core/build/environment.js +157 -0
  46. package/dist/core/build/generate.js +10 -1
  47. package/dist/core/build/plugins/plugin-incremental.js +145 -37
  48. package/dist/core/cache/handler.d.ts +13 -7
  49. package/dist/core/cache/handler.js +55 -51
  50. package/dist/core/cache/provider.d.ts +4 -0
  51. package/dist/core/cache/provider.js +15 -0
  52. package/dist/core/constants.d.ts +0 -11
  53. package/dist/core/constants.js +1 -5
  54. package/dist/core/dev/dev.js +1 -1
  55. package/dist/core/dev/lockfile.d.ts +19 -2
  56. package/dist/core/dev/lockfile.js +25 -2
  57. package/dist/core/environment/dev-nonrunnable.d.ts +6 -0
  58. package/dist/core/environment/dev-nonrunnable.js +170 -0
  59. package/dist/core/environment/index.d.ts +90 -0
  60. package/dist/core/environment/index.js +12 -0
  61. package/dist/core/environment/production.d.ts +6 -0
  62. package/dist/core/environment/production.js +117 -0
  63. package/dist/core/errors/build-handler.d.ts +5 -10
  64. package/dist/core/errors/build-handler.js +11 -17
  65. package/dist/core/errors/default-handler.d.ts +4 -9
  66. package/dist/core/errors/default-handler.js +92 -107
  67. package/dist/core/errors/dev-handler.d.ts +7 -11
  68. package/dist/core/errors/dev-handler.js +71 -69
  69. package/dist/core/errors/errors-data.d.ts +18 -0
  70. package/dist/core/errors/errors-data.js +7 -0
  71. package/dist/core/errors/handler.d.ts +22 -3
  72. package/dist/core/errors/handler.js +25 -0
  73. package/dist/core/fetch/default-handler.d.ts +15 -8
  74. package/dist/core/fetch/default-handler.js +12 -34
  75. package/dist/core/fetch/features.d.ts +23 -0
  76. package/dist/core/fetch/features.js +27 -0
  77. package/dist/core/fetch/fetch-state.d.ts +52 -7
  78. package/dist/core/fetch/fetch-state.js +92 -81
  79. package/dist/core/fetch/index.d.ts +9 -2
  80. package/dist/core/fetch/index.js +18 -79
  81. package/dist/core/fetch/vite-plugin.js +2 -1
  82. package/dist/core/i18n/handler.d.ts +28 -13
  83. package/dist/core/i18n/handler.js +100 -100
  84. package/dist/core/logger/manifest-logger.d.ts +31 -0
  85. package/dist/core/logger/manifest-logger.js +37 -0
  86. package/dist/core/manifest/ambient-source.d.ts +2 -0
  87. package/dist/core/manifest/ambient-source.js +4 -0
  88. package/dist/core/manifest/ambient.d.ts +16 -0
  89. package/dist/core/manifest/ambient.js +22 -0
  90. package/dist/core/manifest/derived.d.ts +3 -0
  91. package/dist/core/manifest/derived.js +10 -0
  92. package/dist/core/manifest/memo.d.ts +31 -0
  93. package/dist/core/manifest/memo.js +47 -0
  94. package/dist/core/messages/runtime.js +1 -1
  95. package/dist/core/middleware/astro-middleware.d.ts +25 -35
  96. package/dist/core/middleware/astro-middleware.js +61 -81
  97. package/dist/core/middleware/load.d.ts +19 -0
  98. package/dist/core/middleware/load.js +36 -0
  99. package/dist/core/middleware/sequence.js +15 -7
  100. package/dist/core/pages/handler.d.ts +18 -25
  101. package/dist/core/pages/handler.js +68 -84
  102. package/dist/core/redirects/render.js +3 -8
  103. package/dist/core/render/index.d.ts +0 -1
  104. package/dist/core/render/index.js +0 -2
  105. package/dist/core/render/route-cache.d.ts +6 -1
  106. package/dist/core/render/route-cache.js +11 -1
  107. package/dist/core/rewrites/handler.d.ts +4 -6
  108. package/dist/core/rewrites/handler.js +19 -20
  109. package/dist/core/routing/default.d.ts +2 -0
  110. package/dist/core/routing/default.js +7 -1
  111. package/dist/core/routing/dev.d.ts +1 -3
  112. package/dist/core/routing/dev.js +12 -5
  113. package/dist/core/routing/handler.d.ts +6 -16
  114. package/dist/core/routing/handler.js +126 -164
  115. package/dist/core/routing/helpers.d.ts +7 -1
  116. package/dist/core/routing/helpers.js +21 -0
  117. package/dist/core/routing/match-request.d.ts +10 -0
  118. package/dist/core/routing/match-request.js +57 -0
  119. package/dist/core/routing/route-table.d.ts +34 -0
  120. package/dist/core/routing/route-table.js +41 -0
  121. package/dist/core/routing/trailing-slash-handler.d.ts +4 -14
  122. package/dist/core/routing/trailing-slash-handler.js +39 -50
  123. package/dist/core/server-islands/mappings.d.ts +6 -0
  124. package/dist/core/server-islands/mappings.js +12 -0
  125. package/dist/core/session/driver.d.ts +4 -0
  126. package/dist/core/session/driver.js +14 -0
  127. package/dist/core/session/handler.d.ts +1 -1
  128. package/dist/core/session/handler.js +8 -8
  129. package/dist/core/session/provider-disabled.js +2 -2
  130. package/dist/core/util.js +2 -2
  131. package/dist/entrypoints/prerender.js +5 -1
  132. package/dist/i18n/middleware.d.ts +4 -4
  133. package/dist/i18n/middleware.js +3 -3
  134. package/dist/manifest/serialized.d.ts +1 -0
  135. package/dist/manifest/serialized.js +9 -2
  136. package/dist/runtime/prerender/static-paths.d.ts +14 -2
  137. package/dist/runtime/prerender/static-paths.js +4 -3
  138. package/dist/types/public/config.d.ts +433 -241
  139. package/dist/vite-plugin-app/createAstroServerApp.js +22 -18
  140. package/dist/vite-plugin-app/environment.d.ts +18 -0
  141. package/dist/vite-plugin-app/environment.js +210 -0
  142. package/dist/vite-plugin-app/handle-request.d.ts +30 -0
  143. package/dist/vite-plugin-app/handle-request.js +129 -0
  144. package/dist/vite-plugin-app/index.d.ts +1 -1
  145. package/dist/vite-plugin-app/index.js +1 -1
  146. package/dist/vite-plugin-css/index.js +2 -2
  147. package/package.json +14 -6
  148. package/src/core/README.md +37 -0
  149. package/src/core/manifest/ambient-source.ts +9 -0
  150. package/src/types/README.md +5 -0
  151. package/dist/container/pipeline.d.ts +0 -14
  152. package/dist/container/pipeline.js +0 -78
  153. package/dist/core/app/dev/app.d.ts +0 -31
  154. package/dist/core/app/dev/app.js +0 -80
  155. package/dist/core/app/dev/pipeline.d.ts +0 -17
  156. package/dist/core/app/dev/pipeline.js +0 -133
  157. package/dist/core/app/pipeline.d.ts +0 -14
  158. package/dist/core/app/pipeline.js +0 -123
  159. package/dist/core/base-pipeline.d.ts +0 -183
  160. package/dist/core/base-pipeline.js +0 -293
  161. package/dist/core/build/pipeline.d.ts +0 -38
  162. package/dist/core/build/pipeline.js +0 -202
  163. package/dist/vite-plugin-app/app.d.ts +0 -62
  164. package/dist/vite-plugin-app/app.js +0 -237
  165. package/dist/vite-plugin-app/pipeline.d.ts +0 -25
  166. package/dist/vite-plugin-app/pipeline.js +0 -189
@@ -2,16 +2,20 @@ import { getDefaultClientDirectives } from "../core/client-directive/default.js"
2
2
  import { ASTRO_CONFIG_DEFAULTS } from "../core/config/schemas/defaults.js";
3
3
  import { createKey } from "../core/encryption.js";
4
4
  import { FetchState } from "../core/fetch/fetch-state.js";
5
- import { AstroMiddleware } from "../core/middleware/astro-middleware.js";
5
+ import { handleMiddleware } from "../core/middleware/astro-middleware.js";
6
6
  import { NOOP_MIDDLEWARE_FN } from "../core/middleware/noop-middleware.js";
7
- import { PagesHandler } from "../core/pages/handler.js";
7
+ import { handlePages } from "../core/pages/handler.js";
8
8
  import { removeLeadingForwardSlash } from "../core/path.js";
9
9
  import { getParts } from "../core/routing/parts.js";
10
10
  import { getPattern } from "../core/routing/pattern.js";
11
11
  import { validateSegment } from "../core/routing/segment.js";
12
12
  import { SlotString } from "../runtime/server/render/slot.js";
13
- import { ContainerPipeline } from "./pipeline.js";
13
+ import { createContainerEnvironment } from "./environment.js";
14
+ import { setEnvironment } from "../core/environment/index.js";
14
15
  import { createConsoleLogger } from "../core/logger/impls/console.js";
16
+ import { setLogger } from "../core/logger/manifest-logger.js";
17
+ import { peekMiddleware } from "../core/middleware/load.js";
18
+ import { getRouteTable } from "../core/routing/route-table.js";
15
19
  function createManifest(manifest, renderers, middleware) {
16
20
  function middlewareInstance() {
17
21
  return {
@@ -69,9 +73,17 @@ function createManifest(manifest, renderers, middleware) {
69
73
  };
70
74
  }
71
75
  class experimental_AstroContainer {
72
- #pipeline;
73
- #astroMiddleware;
74
- #pagesHandler;
76
+ /**
77
+ * The container's fabricated manifest — the source of truth all the
78
+ * functional-core accessors key off. The container never touches the
79
+ * ambient manifest, so multiple containers in one process stay isolated.
80
+ */
81
+ #manifest;
82
+ /**
83
+ * The route → module interner, shared between the environment record
84
+ * (lookups) and the `insertRoute` writes below.
85
+ */
86
+ #interner;
75
87
  /**
76
88
  * Internally used to check if the container was created with a manifest.
77
89
  * @private
@@ -84,22 +96,29 @@ class experimental_AstroContainer {
84
96
  resolve
85
97
  }) {
86
98
  const ssrManifest = createManifest(manifest, renderers);
87
- this.#pipeline = ContainerPipeline.create({
88
- logger: createConsoleLogger({ level: "error" }),
89
- manifest: ssrManifest,
90
- streaming,
91
- renderers: renderers ?? manifest?.renderers ?? [],
92
- resolve: async (specifier) => {
93
- if (this.#withManifest) {
94
- return this.#containerResolve(specifier, ssrManifest);
95
- } else if (resolve) {
96
- return resolve(specifier);
97
- }
98
- return specifier;
99
+ const containerRenderers = renderers ?? manifest?.renderers ?? [];
100
+ const containerResolve = async (specifier) => {
101
+ if (this.#withManifest) {
102
+ return this.#containerResolve(specifier, ssrManifest);
103
+ } else if (resolve) {
104
+ return resolve(specifier);
99
105
  }
100
- });
101
- this.#astroMiddleware = new AstroMiddleware(this.#pipeline);
102
- this.#pagesHandler = new PagesHandler(this.#pipeline);
106
+ return specifier;
107
+ };
108
+ const interner = /* @__PURE__ */ new WeakMap();
109
+ setLogger(ssrManifest, createConsoleLogger({ level: "error" }));
110
+ setEnvironment(
111
+ ssrManifest,
112
+ createContainerEnvironment({
113
+ interner,
114
+ resolve: containerResolve,
115
+ renderers: containerRenderers,
116
+ streaming
117
+ })
118
+ );
119
+ getRouteTable(ssrManifest);
120
+ this.#manifest = ssrManifest;
121
+ this.#interner = interner;
103
122
  }
104
123
  async #containerResolve(specifier, manifest) {
105
124
  const found = manifest.entryModules[specifier];
@@ -153,12 +172,12 @@ class experimental_AstroContainer {
153
172
  );
154
173
  }
155
174
  if (isNamedRenderer(renderer)) {
156
- this.#pipeline.manifest.renderers.push({
175
+ this.#manifest.renderers.push({
157
176
  name: renderer.name,
158
177
  ssr: renderer
159
178
  });
160
179
  } else if ("name" in options) {
161
- this.#pipeline.manifest.renderers.push({
180
+ this.#manifest.renderers.push({
162
181
  name: options.name,
163
182
  ssr: renderer
164
183
  });
@@ -193,15 +212,15 @@ class experimental_AstroContainer {
193
212
  */
194
213
  addClientRenderer(options) {
195
214
  const { entrypoint, name } = options;
196
- const rendererIndex = this.#pipeline.manifest.renderers.findIndex((r) => r.name === name);
215
+ const rendererIndex = this.#manifest.renderers.findIndex((r) => r.name === name);
197
216
  if (rendererIndex === -1) {
198
217
  throw new Error(
199
218
  "You tried to add the " + name + " client renderer, but its server renderer wasn't added. You must add the server renderer first. Use the `addServerRenderer` function."
200
219
  );
201
220
  }
202
- const renderer = this.#pipeline.manifest.renderers[rendererIndex];
221
+ const renderer = this.#manifest.renderers[rendererIndex];
203
222
  renderer.clientEntrypoint = entrypoint;
204
- this.#pipeline.manifest.renderers[rendererIndex] = renderer;
223
+ this.#manifest.renderers[rendererIndex] = renderer;
205
224
  }
206
225
  // NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it.
207
226
  // @ts-expect-error @ematipico: I plan to use it for a possible integration that could help people
@@ -212,6 +231,20 @@ class experimental_AstroContainer {
212
231
  container.#withManifest = true;
213
232
  return container;
214
233
  }
234
+ /**
235
+ * Associates a runtime-inserted route with its component module in the
236
+ * interner shared with the container environment record. Snapshots the
237
+ * already-resolved middleware synchronously via `peekMiddleware` —
238
+ * `undefined` when `getMiddleware` has not settled yet.
239
+ */
240
+ #internRoute(routeData, componentInstance) {
241
+ this.#interner.set(routeData, {
242
+ page() {
243
+ return Promise.resolve(componentInstance);
244
+ },
245
+ onRequest: peekMiddleware(this.#manifest)
246
+ });
247
+ }
215
248
  #insertRoute({
216
249
  path,
217
250
  componentInstance,
@@ -220,14 +253,14 @@ class experimental_AstroContainer {
220
253
  }) {
221
254
  const pathUrl = new URL(path, "https://example.com");
222
255
  const routeData = this.#createRoute(pathUrl, params, type);
223
- this.#pipeline.manifest.routes.push({
256
+ this.#manifest.routes.push({
224
257
  routeData,
225
258
  file: "",
226
259
  links: [],
227
260
  styles: [],
228
261
  scripts: []
229
262
  });
230
- this.#pipeline.insertRoute(routeData, componentInstance);
263
+ this.#internRoute(routeData, componentInstance);
231
264
  return routeData;
232
265
  }
233
266
  /**
@@ -286,7 +319,7 @@ class experimental_AstroContainer {
286
319
  params: options.params,
287
320
  type: routeType
288
321
  });
289
- const state = new FetchState(this.#pipeline, request);
322
+ const state = new FetchState(this.#manifest, request);
290
323
  state.routeData = routeData;
291
324
  state.pathname = url.pathname;
292
325
  state.clientAddress = "";
@@ -300,7 +333,7 @@ class experimental_AstroContainer {
300
333
  if (options.props) {
301
334
  state.initialProps = options.props;
302
335
  }
303
- return this.#astroMiddleware.handle(state, this.#pagesHandler.handle.bind(this.#pagesHandler));
336
+ return handleMiddleware(state, handlePages);
304
337
  }
305
338
  /**
306
339
  * It stores an Astro **page** route. The first argument, `route`, gets associated to the `component`.
@@ -315,7 +348,7 @@ class experimental_AstroContainer {
315
348
  insertPageRoute(route, component, params) {
316
349
  const url = new URL(route, "https://example.com/");
317
350
  const routeData = this.#createRoute(url, params ?? {}, "page");
318
- this.#pipeline.manifest.routes.push({
351
+ this.#manifest.routes.push({
319
352
  routeData,
320
353
  file: "",
321
354
  links: [],
@@ -323,7 +356,7 @@ class experimental_AstroContainer {
323
356
  scripts: []
324
357
  });
325
358
  const componentInstance = this.#wrapComponent(component, params);
326
- this.#pipeline.insertRoute(routeData, componentInstance);
359
+ this.#internRoute(routeData, componentInstance);
327
360
  }
328
361
  #createRoute(url, params, type) {
329
362
  const segments = removeLeadingForwardSlash(url.pathname).split("/").filter(Boolean).map((s) => {
@@ -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.1") {
199
+ if (previousAstroVersion && previousAstroVersion !== "7.2.3") {
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.1") {
208
- this.#store.metaStore().set("astro-version", "7.2.1");
207
+ if ("7.2.3") {
208
+ this.#store.metaStore().set("astro-version", "7.2.3");
209
209
  }
210
210
  if (currentConfigDigest) {
211
211
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -1,8 +1,5 @@
1
1
  import { type PathLike } from 'node:fs';
2
- /**
3
- * A chunked store manifest: each collection maps to the list of part file names
4
- * whose contents concatenate back into that collection's serialized string.
5
- */
2
+ /** A chunked store manifest: each collection maps to its content-addressed part files. */
6
3
  export type DataStoreManifest = Record<string, string[]>;
7
4
  /**
8
5
  * Persists the content collection data produced by the content layer. This is
@@ -26,9 +23,9 @@ export declare function serializeDataStore(collections: Map<string, Map<string,
26
23
  * on UTF-16 code-unit boundaries (e.g. `String.prototype.slice`) can cut a
27
24
  * surrogate pair in half; encoding a lone surrogate to UTF-8 substitutes U+FFFD,
28
25
  * so concatenating the re-read parts would corrupt any astral-plane character
29
- * (emoji, some CJK, etc.). Iterating with `for...of` yields whole code points,
30
- * so `str.slice` is only ever called on code-point boundaries and the parts
31
- * always rejoin to the exact original string.
26
+ * (emoji, some CJK, etc.). `getChunkEnd()` advances by Unicode code point, so
27
+ * `str.slice` is only ever called on code-point boundaries and the parts always
28
+ * rejoin to the exact original string.
32
29
  */
33
30
  export declare function chunkString(str: string, maxBytes: number): string[];
34
31
  /**
@@ -52,9 +49,9 @@ export declare class FileWriter implements DataStoreWriter {
52
49
  * A {@link DataStoreWriter} that splits the store across many content-addressed
53
50
  * files inside a directory, described by a manifest.
54
51
  *
55
- * Each collection is serialized to a string and split into parts no larger than
56
- * a fixed byte size, so no single file grows unbounded (platform file-size
57
- * limits). Each part file is named by the xxhash of its contents, so unchanged
52
+ * Entries are serialized independently into parts no larger than a fixed byte
53
+ * size, so writing a collection does not require a collection-wide serialized
54
+ * string. Each part file is named by the xxhash of its contents, so unchanged
58
55
  * parts keep the same name across builds and their writes are skipped, and two
59
56
  * identical parts are naturally deduplicated. The manifest is written last as
60
57
  * the commit point, and stale files are pruned afterwards. This is the inverse
@@ -15,23 +15,31 @@ function serializeDataStore(collections) {
15
15
  return devalue.stringify(sortCollections(collections));
16
16
  }
17
17
  const ENCODER = new TextEncoder();
18
+ function getChunkEnd(str, startIndex, maxBytes) {
19
+ let index = startIndex;
20
+ let bytes = 0;
21
+ while (index < str.length) {
22
+ const codePoint = str.codePointAt(index);
23
+ const charLength = codePoint > 65535 ? 2 : 1;
24
+ const charBytes = ENCODER.encode(str.slice(index, index + charLength)).length;
25
+ if (bytes + charBytes > maxBytes && index > startIndex) {
26
+ break;
27
+ }
28
+ index += charLength;
29
+ bytes += charBytes;
30
+ if (bytes >= maxBytes) {
31
+ break;
32
+ }
33
+ }
34
+ return { endIndex: index, bytes };
35
+ }
18
36
  function chunkString(str, maxBytes) {
19
37
  const chunks = [];
20
38
  let startIndex = 0;
21
- let index = 0;
22
- let currentBytes = 0;
23
- for (const char of str) {
24
- const charBytes = ENCODER.encode(char).length;
25
- if (currentBytes + charBytes > maxBytes && index > startIndex) {
26
- chunks.push(str.slice(startIndex, index));
27
- startIndex = index;
28
- currentBytes = 0;
29
- }
30
- index += char.length;
31
- currentBytes += charBytes;
32
- }
33
- if (startIndex < str.length) {
34
- chunks.push(str.slice(startIndex));
39
+ while (startIndex < str.length) {
40
+ const { endIndex } = getChunkEnd(str, startIndex, maxBytes);
41
+ chunks.push(str.slice(startIndex, endIndex));
42
+ startIndex = endIndex;
35
43
  }
36
44
  return chunks;
37
45
  }
@@ -58,6 +66,7 @@ class ChunkedWriter {
58
66
  #manifestFile;
59
67
  #chunkSize;
60
68
  #hasher;
69
+ #writtenFiles = /* @__PURE__ */ new Set();
61
70
  constructor(dir, chunkSize) {
62
71
  this.#dir = dir;
63
72
  this.#manifestFile = new URL(`./${DATA_STORE_MANIFEST_FILE}`, dir);
@@ -67,23 +76,49 @@ class ChunkedWriter {
67
76
  if (!this.#hasher) {
68
77
  this.#hasher = await xxhash();
69
78
  }
70
- const { h64ToString } = this.#hasher;
71
- const writtenFiles = /* @__PURE__ */ new Set();
79
+ this.#writtenFiles = /* @__PURE__ */ new Set();
72
80
  const manifest = {};
73
81
  for (const [collectionName, entries] of sortCollections(collections)) {
74
- const stringified = devalue.stringify(entries);
75
- const parts = [];
76
- for (const part of chunkString(stringified, this.#chunkSize)) {
77
- const fileName = `${h64ToString(part)}.txt`;
78
- await writeFileAtomic(new URL(`./${fileName}`, this.#dir), part);
79
- parts.push(fileName);
80
- writtenFiles.add(fileName);
81
- }
82
- manifest[collectionName] = parts;
82
+ manifest[collectionName] = await this.#writeCollection(entries);
83
83
  }
84
84
  await writeFileAtomic(this.#manifestFile, JSON.stringify(manifest));
85
- writtenFiles.add(DATA_STORE_MANIFEST_FILE);
86
- emptyDir(this.#dir, writtenFiles);
85
+ this.#writtenFiles.add(DATA_STORE_MANIFEST_FILE);
86
+ emptyDir(this.#dir, this.#writtenFiles);
87
+ }
88
+ async #writeCollection(entries) {
89
+ const parts = [];
90
+ let chunk = "";
91
+ let chunkBytes = 0;
92
+ const writeChunk = async () => {
93
+ const fileName = `${this.#hasher.h64ToString(chunk)}.txt`;
94
+ await writeFileAtomic(new URL(`./${fileName}`, this.#dir), chunk);
95
+ parts.push(fileName);
96
+ this.#writtenFiles.add(fileName);
97
+ chunk = "";
98
+ chunkBytes = 0;
99
+ };
100
+ for (const [id, entry] of entries) {
101
+ const serialized = `${devalue.stringify([id, entry])}
102
+ `;
103
+ let startIndex = 0;
104
+ while (startIndex < serialized.length) {
105
+ const { endIndex, bytes } = getChunkEnd(
106
+ serialized,
107
+ startIndex,
108
+ this.#chunkSize - chunkBytes
109
+ );
110
+ chunk += serialized.slice(startIndex, endIndex);
111
+ chunkBytes += bytes;
112
+ startIndex = endIndex;
113
+ if (chunkBytes >= this.#chunkSize) {
114
+ await writeChunk();
115
+ }
116
+ }
117
+ }
118
+ if (chunk) {
119
+ await writeChunk();
120
+ }
121
+ return parts;
87
122
  }
88
123
  }
89
124
  export {
@@ -31,6 +31,24 @@ export interface DataEntry<TData extends Record<string, unknown> = Record<string
31
31
  */
32
32
  deferredRender?: boolean;
33
33
  assetImports?: Array<string>;
34
+ /**
35
+ * Locations of image fields within `data`, recorded when the entry is stored.
36
+ * Each path is the sequence of keys from `data` to a field that holds an image
37
+ * src string. At read time these fields are resolved to `ImageMetadata` without
38
+ * traversing or cloning the rest of `data`, so sibling values that devalue can
39
+ * serialize but `structuredClone` cannot (e.g. class instances) are left
40
+ * untouched.
41
+ */
42
+ imageImports?: (string | number)[][];
43
+ }
44
+ /**
45
+ * Rebuilds a collection from ordered parts containing newline-delimited
46
+ * `devalue.stringify([id, entry])` records.
47
+ */
48
+ export declare class ChunkedCollectionParser {
49
+ #private;
50
+ add(part: string): void;
51
+ finish(): Map<string, any>;
34
52
  }
35
53
  /**
36
54
  * A read-only data store for content collections. This is used to retrieve data from the content layer at runtime.
@@ -52,9 +70,8 @@ export declare class ImmutableDataStore {
52
70
  *
53
71
  * Each collection maps to a list of parts. A part is either a raw string
54
72
  * (when the store is loaded from disk) or an ESM namespace from a virtual
55
- * chunk import (`{ default: string }`, when emitted at runtime). A collection's
56
- * parts are concatenated back into the exact
57
- * serialized string, then parsed with devalue. This is the inverse of
73
+ * chunk import (`{ default: string }`, when emitted at runtime). Each part
74
+ * contains independently serialized entry records. This is the inverse of
58
75
  * {@link import('./data-store-writer.js').ChunkedWriter} and stays free of
59
76
  * Node built-ins so it can run at runtime.
60
77
  */
@@ -1,5 +1,27 @@
1
1
  import * as devalue from "devalue";
2
2
  import { InMemorySource } from "./data-store-source.js";
3
+ class ChunkedCollectionParser {
4
+ #entries = /* @__PURE__ */ new Map();
5
+ #remainder = "";
6
+ add(part) {
7
+ const content = this.#remainder + part;
8
+ const records = content.split("\n");
9
+ this.#remainder = records.pop();
10
+ for (const record of records) {
11
+ const parsed = devalue.parse(record);
12
+ if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== "string") {
13
+ throw new Error("Invalid chunked data store entry");
14
+ }
15
+ this.#entries.set(parsed[0], parsed[1]);
16
+ }
17
+ }
18
+ finish() {
19
+ if (this.#remainder) {
20
+ throw new Error("Invalid chunked data store entry");
21
+ }
22
+ return this.#entries;
23
+ }
24
+ }
3
25
  class ImmutableDataStore {
4
26
  _collections = /* @__PURE__ */ new Map();
5
27
  constructor() {
@@ -39,21 +61,19 @@ class ImmutableDataStore {
39
61
  *
40
62
  * Each collection maps to a list of parts. A part is either a raw string
41
63
  * (when the store is loaded from disk) or an ESM namespace from a virtual
42
- * chunk import (`{ default: string }`, when emitted at runtime). A collection's
43
- * parts are concatenated back into the exact
44
- * serialized string, then parsed with devalue. This is the inverse of
64
+ * chunk import (`{ default: string }`, when emitted at runtime). Each part
65
+ * contains independently serialized entry records. This is the inverse of
45
66
  * {@link import('./data-store-writer.js').ChunkedWriter} and stays free of
46
67
  * Node built-ins so it can run at runtime.
47
68
  */
48
69
  static manifestToMap(manifest) {
49
70
  const collections = /* @__PURE__ */ new Map();
50
71
  for (const [collectionName, parts] of Object.entries(manifest)) {
51
- let stringified = "";
72
+ const parser = new ChunkedCollectionParser();
52
73
  for (const part of parts) {
53
- stringified += typeof part === "string" ? part : part.default;
74
+ parser.add(typeof part === "string" ? part : part.default);
54
75
  }
55
- const entries = devalue.parse(stringified);
56
- collections.set(collectionName, entries);
76
+ collections.set(collectionName, parser.finish());
57
77
  }
58
78
  return collections;
59
79
  }
@@ -100,6 +120,7 @@ function dataStoreSingleton() {
100
120
  }
101
121
  const globalDataStore = dataStoreSingleton();
102
122
  export {
123
+ ChunkedCollectionParser,
103
124
  ImmutableDataStore,
104
125
  globalDataStore
105
126
  };
@@ -2,7 +2,11 @@ import { existsSync, promises as fs } from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
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,6 +5,8 @@ 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) {
@@ -107,9 +109,20 @@ function glob(globOptions) {
107
109
  if (existingEntry && existingEntry.filePath && existingEntry.filePath !== relativePath2) {
108
110
  const oldFilePath = new URL(existingEntry.filePath, config.root);
109
111
  if (existsSync(oldFilePath)) {
110
- logger.warn(
111
- `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
112
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
+ }
113
126
  }
114
127
  }
115
128
  if (entryType.getRenderFunction && !globOptions.deferRender) {
@@ -10,7 +10,7 @@ import {
10
10
  FileWriter,
11
11
  serializeDataStore
12
12
  } from "./data-store-writer.js";
13
- import { ImmutableDataStore } from "./data-store.js";
13
+ import { ChunkedCollectionParser, ImmutableDataStore } from "./data-store.js";
14
14
  import { contentModuleToId } from "./utils.js";
15
15
  const SAVE_DEBOUNCE_MS = 500;
16
16
  const MAX_DEPTH = 10;
@@ -276,10 +276,13 @@ ${lines.join(",\n")}]);
276
276
  }
277
277
  }
278
278
  const foundAssets = new Set(assetImports);
279
- forEach(data, (_, val) => {
279
+ const imageImports = [];
280
+ forEach(data, function(ctx, val) {
280
281
  if (typeof val === "string" && val.startsWith(IMAGE_IMPORT_PREFIX)) {
281
282
  const src = val.replace(IMAGE_IMPORT_PREFIX, "");
282
283
  foundAssets.add(src);
284
+ imageImports.push(ctx.path.map((segment) => segment));
285
+ ctx.update(src);
283
286
  }
284
287
  });
285
288
  const entry = {
@@ -299,6 +302,9 @@ ${lines.join(",\n")}]);
299
302
  entry.assetImports = Array.from(foundAssets);
300
303
  this.addAssetImports(entry.assetImports, filePath);
301
304
  }
305
+ if (imageImports.length) {
306
+ entry.imageImports = imageImports;
307
+ }
302
308
  if (digest) {
303
309
  entry.digest = digest;
304
310
  }
@@ -424,16 +430,15 @@ ${lines.join(",\n")}]);
424
430
  try {
425
431
  const manifestData = await fs.readFile(manifestFile, "utf-8");
426
432
  const manifest = JSON.parse(manifestData);
427
- const expanded = {};
433
+ const collections = /* @__PURE__ */ new Map();
428
434
  for (const collectionName in manifest) {
429
- expanded[collectionName] = await Promise.all(
430
- manifest[collectionName].map(
431
- (fileName) => fs.readFile(new URL(`./${fileName}`, dirPath), "utf-8")
432
- )
433
- );
435
+ const parser = new ChunkedCollectionParser();
436
+ for (const fileName of manifest[collectionName]) {
437
+ parser.add(await fs.readFile(new URL(`./${fileName}`, dirPath), "utf-8"));
438
+ }
439
+ collections.set(collectionName, parser.finish());
434
440
  }
435
- const map = ImmutableDataStore.manifestToMap(expanded);
436
- const store2 = await MutableDataStore.fromMap(map);
441
+ const store2 = await MutableDataStore.fromMap(collections);
437
442
  store2.#writer = new ChunkedWriter(dirPath, chunkSize);
438
443
  return store2;
439
444
  } catch (err) {
@@ -4,7 +4,7 @@ import type * as zCore from 'zod/v4/core';
4
4
  import type { ImageMetadata } from '../assets/types.js';
5
5
  import { type AstroComponentFactory } from '../runtime/server/index.js';
6
6
  import type { LiveDataCollectionResult, LiveDataEntryResult } from '../types/public/content.js';
7
- import { type LIVE_CONTENT_TYPE } from './consts.js';
7
+ import type { LIVE_CONTENT_TYPE } from './consts.js';
8
8
  import { type DataEntry } from './data-store.js';
9
9
  import { LiveCollectionCacheHintError, LiveCollectionError, LiveCollectionValidationError, LiveEntryNotFoundError } from './loaders/errors.js';
10
10
  import type { LiveLoader } from './loaders/types.js';
@@ -26,6 +26,7 @@ export declare function createGetCollection({ liveCollections, }: {
26
26
  rendered?: import("./data-store.js").RenderedContent;
27
27
  deferredRender?: boolean;
28
28
  assetImports?: Array<string>;
29
+ imageImports?: (string | number)[][];
29
30
  }[]>;
30
31
  type ContentEntryResult = {
31
32
  id: string;
@@ -70,7 +71,7 @@ type RenderResult = {
70
71
  headings: MarkdownHeading[];
71
72
  remarkPluginFrontmatter: Record<string, any>;
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 updateImageReferencesInData<T extends Record<string, unknown>>(data: T, fileName?: string, imageAssetMap?: Map<string, ImageMetadata>, imageImports?: (string | number)[][]): T;
74
75
  export declare function resolveEntryData<T extends Record<string, unknown>>(entry: DataEntry<T>, imageAssetMap?: Map<string, ImageMetadata>): T;
75
76
  export declare function renderEntry(entry: DataEntry): Promise<RenderResult>;
76
77
  export declare function createReference(): (collection: string) => z.ZodPipe<z.ZodUnion<readonly [z.ZodPipe<z.ZodNumber, z.ZodTransform<string, number>>, z.ZodString, z.ZodObject<{