astro 7.0.8 → 7.1.0

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 (74) hide show
  1. package/dist/assets/fonts/infra/fs-font-file-content-resolver.js +1 -1
  2. package/dist/cli/dev/index.d.ts +17 -0
  3. package/dist/cli/dev/index.js +56 -2
  4. package/dist/cli/infra/build-time-astro-version-provider.js +1 -1
  5. package/dist/content/consts.d.ts +2 -0
  6. package/dist/content/consts.js +4 -0
  7. package/dist/content/content-layer.d.ts +0 -6
  8. package/dist/content/content-layer.js +4 -9
  9. package/dist/content/data-store-source.d.ts +36 -0
  10. package/dist/content/data-store-source.js +30 -0
  11. package/dist/content/data-store-writer.d.ts +67 -0
  12. package/dist/content/data-store-writer.js +94 -0
  13. package/dist/content/data-store.d.ts +15 -0
  14. package/dist/content/data-store.js +33 -3
  15. package/dist/content/loaders/glob.d.ts +7 -0
  16. package/dist/content/loaders/glob.js +2 -2
  17. package/dist/content/mutable-data-store.d.ts +8 -0
  18. package/dist/content/mutable-data-store.js +52 -15
  19. package/dist/content/paths.d.ts +14 -0
  20. package/dist/content/paths.js +11 -0
  21. package/dist/content/runtime.js +4 -4
  22. package/dist/content/vite-plugin-content-virtual-mod.js +37 -8
  23. package/dist/core/app/dev/pipeline.js +2 -1
  24. package/dist/core/app/types.d.ts +39 -5
  25. package/dist/core/base-pipeline.d.ts +1 -1
  26. package/dist/core/base-pipeline.js +6 -2
  27. package/dist/core/build/index.js +2 -1
  28. package/dist/core/build/plugins/plugin-manifest.js +23 -8
  29. package/dist/core/config/schemas/base.d.ts +38 -5
  30. package/dist/core/config/schemas/base.js +15 -8
  31. package/dist/core/config/schemas/relative.d.ts +73 -15
  32. package/dist/core/constants.js +1 -1
  33. package/dist/core/csp/common.d.ts +7 -7
  34. package/dist/core/csp/common.js +8 -9
  35. package/dist/core/csp/config.d.ts +45 -0
  36. package/dist/core/csp/config.js +44 -2
  37. package/dist/core/csp/runtime.d.ts +21 -1
  38. package/dist/core/csp/runtime.js +31 -0
  39. package/dist/core/dev/dev.js +9 -4
  40. package/dist/core/dev/restart.js +7 -1
  41. package/dist/core/errors/zod-error-map.js +7 -1
  42. package/dist/core/fetch/fetch-state.js +52 -10
  43. package/dist/core/logger/config.d.ts +1 -1
  44. package/dist/core/logger/core.d.ts +4 -4
  45. package/dist/core/logger/impls/console.d.ts +2 -2
  46. package/dist/core/logger/impls/json.d.ts +2 -2
  47. package/dist/core/logger/impls/json.js +3 -14
  48. package/dist/core/logger/impls/node.d.ts +2 -2
  49. package/dist/core/logger/load.d.ts +2 -2
  50. package/dist/core/logger/load.js +19 -27
  51. package/dist/core/messages/runtime.d.ts +8 -0
  52. package/dist/core/messages/runtime.js +34 -1
  53. package/dist/core/render/paginate.js +21 -16
  54. package/dist/core/sync/index.js +23 -8
  55. package/dist/integrations/hooks.js +8 -0
  56. package/dist/manifest/serialized.js +21 -6
  57. package/dist/runtime/server/astro-island.js +1 -3
  58. package/dist/runtime/server/astro-island.prebuilt-dev.d.ts +1 -1
  59. package/dist/runtime/server/astro-island.prebuilt-dev.js +1 -1
  60. package/dist/runtime/server/astro-island.prebuilt.d.ts +1 -1
  61. package/dist/runtime/server/astro-island.prebuilt.js +1 -1
  62. package/dist/runtime/server/escape.d.ts +2 -0
  63. package/dist/runtime/server/escape.js +4 -0
  64. package/dist/runtime/server/render/csp.js +80 -19
  65. package/dist/runtime/server/render/server-islands.js +6 -4
  66. package/dist/runtime/server/transition.js +2 -2
  67. package/dist/types/astro.d.ts +2 -3
  68. package/dist/types/public/common.d.ts +13 -0
  69. package/dist/types/public/config.d.ts +196 -15
  70. package/dist/types/public/context.d.ts +27 -11
  71. package/dist/types/public/internal.d.ts +12 -11
  72. package/dist/vite-plugin-app/pipeline.js +2 -1
  73. package/dist/vite-plugin-astro-server/route-guard.js +3 -2
  74. package/package.json +3 -3
@@ -10,7 +10,7 @@ class FsFontFileContentResolver {
10
10
  return url;
11
11
  }
12
12
  try {
13
- return url + this.#readFileSync(url);
13
+ return this.#readFileSync(url);
14
14
  } catch (cause) {
15
15
  throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause });
16
16
  }
@@ -2,5 +2,22 @@ import { type Flags } from '../flags.js';
2
2
  interface DevOptions {
3
3
  flags: Flags;
4
4
  }
5
+ /**
6
+ * `yargs-parser` camel-cases `--ignore-lock` to `flags.ignoreLock`.
7
+ */
8
+ export declare function isIgnoreLock(flags: Flags): boolean;
9
+ /**
10
+ * `--ignore-lock` skips the lock file entirely, so a background dev server started with it
11
+ * could never be found by `astro dev stop`/`status`/`logs`. Returns an error message if
12
+ * background mode (explicit `--background`, or implied by AI agent detection) is combined
13
+ * with `--ignore-lock`, or `null` if there's no conflict.
14
+ */
15
+ export declare function getBackgroundIgnoreLockConflict(flags: Flags, wantsBackground: boolean): string | null;
16
+ /**
17
+ * `--force` (replace the existing server) and `--ignore-lock` (start alongside it,
18
+ * untracked) express contradictory intent. Returns an error message if both are set,
19
+ * or `null` otherwise.
20
+ */
21
+ export declare function getForceIgnoreLockConflict(flags: Flags): string | null;
5
22
  export declare function dev({ flags }: DevOptions): Promise<import("../../core/dev/dev.js").DevServer | undefined>;
6
23
  export {};
@@ -18,6 +18,31 @@ function isRunByAgent() {
18
18
  return false;
19
19
  }
20
20
  }
21
+ function isIgnoreLock(flags) {
22
+ return flags.ignoreLock === true;
23
+ }
24
+ function getBackgroundIgnoreLockConflict(flags, wantsBackground) {
25
+ if (!wantsBackground) {
26
+ return null;
27
+ }
28
+ const reason = flags.background ? "`--background`" : "an auto-detected AI agent environment, which runs the dev server in the background automatically";
29
+ return [
30
+ `\`--ignore-lock\` cannot be used together with ${reason}.`,
31
+ "",
32
+ "Background dev servers rely on the lock file so `astro dev stop`, `astro dev status`, and `astro dev logs` can find them.",
33
+ "Run the dev server in the foreground to use --ignore-lock."
34
+ ].join("\n");
35
+ }
36
+ function getForceIgnoreLockConflict(flags) {
37
+ if (!flags.force) {
38
+ return null;
39
+ }
40
+ return [
41
+ "`--force` and `--ignore-lock` cannot be used together.",
42
+ "",
43
+ "`--force` replaces the existing dev server; `--ignore-lock` starts a new one alongside it without touching the lock file. Choose one."
44
+ ].join("\n");
45
+ }
21
46
  async function dev({ flags }) {
22
47
  if (flags.help || flags.h) {
23
48
  printHelp({
@@ -37,6 +62,10 @@ async function dev({ flags }) {
37
62
  ["--host <custom-address>", `Expose on a network IP address at <custom-address>`],
38
63
  ["--open", "Automatically open the app in the browser on server start"],
39
64
  ["--force", "Clear the content layer cache, forcing a full rebuild."],
65
+ [
66
+ "--ignore-lock",
67
+ "Start the dev server even if another one is already running, without checking or writing the lock file."
68
+ ],
40
69
  [
41
70
  "--allowed-hosts",
42
71
  "Specify a comma-separated list of allowed hosts or allow any hostname."
@@ -54,6 +83,8 @@ async function dev({ flags }) {
54
83
  if (agentDetected) {
55
84
  flags.json = true;
56
85
  }
86
+ const ignoreLock = isIgnoreLock(flags);
87
+ const wantsBackground = !!flags.background || agentDetected;
57
88
  const logger = createLoggerFromFlags(flags);
58
89
  const subcommand = flags._[3]?.toString();
59
90
  if (subcommand === "stop") {
@@ -71,7 +102,13 @@ async function dev({ flags }) {
71
102
  await logs({ flags, logger });
72
103
  return;
73
104
  }
74
- if (flags.background || agentDetected) {
105
+ if (ignoreLock) {
106
+ const conflict = getBackgroundIgnoreLockConflict(flags, wantsBackground) ?? getForceIgnoreLockConflict(flags);
107
+ if (conflict) {
108
+ throw new Error(conflict);
109
+ }
110
+ }
111
+ if (wantsBackground) {
75
112
  const { background } = await import("./background.js");
76
113
  await background({ flags, logger });
77
114
  return;
@@ -86,6 +123,20 @@ Run \`astro dev --help\` to see available commands.`
86
123
  process.exit(1);
87
124
  }
88
125
  const root = pathToFileURL(resolveRoot(flags.root) + "/");
126
+ if (ignoreLock) {
127
+ const existingServer2 = checkExistingServer(root);
128
+ if (existingServer2) {
129
+ logger.info(
130
+ "SKIP_FORMAT",
131
+ [
132
+ `Starting a new dev server alongside the one already running at ${existingServer2.url} (pid ${existingServer2.pid}).`,
133
+ "This instance is not tracked by `astro dev stop`, `astro dev status`, or `astro dev logs`."
134
+ ].join("\n")
135
+ );
136
+ }
137
+ const inlineConfig2 = flagsToAstroInlineConfig(flags);
138
+ return await devServer(inlineConfig2);
139
+ }
89
140
  const existingServer = checkExistingServer(root);
90
141
  if (existingServer) {
91
142
  if (flags.force) {
@@ -121,5 +172,8 @@ Run \`astro dev --help\` to see available commands.`
121
172
  return server;
122
173
  }
123
174
  export {
124
- dev
175
+ dev,
176
+ getBackgroundIgnoreLockConflict,
177
+ getForceIgnoreLockConflict,
178
+ isIgnoreLock
125
179
  };
@@ -1,6 +1,6 @@
1
1
  class BuildTimeAstroVersionProvider {
2
2
  // Injected during the build through esbuild define
3
- version = "7.0.8";
3
+ version = "7.1.0";
4
4
  }
5
5
  export {
6
6
  BuildTimeAstroVersionProvider
@@ -20,6 +20,8 @@ export declare const IMAGE_IMPORT_PREFIX = "__ASTRO_IMAGE_";
20
20
  export declare const CONTENT_FLAGS: readonly ["astroContentCollectionEntry", "astroRenderContent", "astroDataCollectionEntry", "astroPropagatedAssets", "astroContentImageFlag", "astroContentModuleFlag"];
21
21
  export declare const CONTENT_TYPES_FILE = "content.d.ts";
22
22
  export declare const DATA_STORE_FILE = "data-store.json";
23
+ export declare const DATA_STORE_DIR = "data-store/";
24
+ export declare const DATA_STORE_MANIFEST_FILE = "manifest.json";
23
25
  export declare const ASSET_IMPORTS_FILE = "content-assets.mjs";
24
26
  export declare const MODULES_IMPORTS_FILE = "content-modules.mjs";
25
27
  export declare const COLLECTIONS_MANIFEST_FILE = "collections/collections.json";
@@ -27,6 +27,8 @@ const CONTENT_FLAGS = [
27
27
  ];
28
28
  const CONTENT_TYPES_FILE = "content.d.ts";
29
29
  const DATA_STORE_FILE = "data-store.json";
30
+ const DATA_STORE_DIR = "data-store/";
31
+ const DATA_STORE_MANIFEST_FILE = "manifest.json";
30
32
  const ASSET_IMPORTS_FILE = "content-assets.mjs";
31
33
  const MODULES_IMPORTS_FILE = "content-modules.mjs";
32
34
  const COLLECTIONS_MANIFEST_FILE = "collections/collections.json";
@@ -47,7 +49,9 @@ export {
47
49
  CONTENT_RENDER_FLAG,
48
50
  CONTENT_TYPES_FILE,
49
51
  DATA_FLAG,
52
+ DATA_STORE_DIR,
50
53
  DATA_STORE_FILE,
54
+ DATA_STORE_MANIFEST_FILE,
51
55
  DATA_STORE_VIRTUAL_ID,
52
56
  DEFERRED_MODULE,
53
57
  IMAGE_IMPORT_PREFIX,
@@ -33,9 +33,3 @@ export declare class ContentLayer {
33
33
  sync(options?: RefreshContentOptions): Promise<void>;
34
34
  regenerateCollectionFileManifest(): Promise<void>;
35
35
  }
36
- /**
37
- * Get the path to the data store file.
38
- * During development, this is in the `.astro` directory so that the Vite watcher can see it.
39
- * In production, it's in the cache directory so that it's preserved between builds.
40
- */
41
- export declare function getDataStoreFile(settings: AstroSettings, isDev: boolean): URL;
@@ -7,7 +7,6 @@ import {
7
7
  ASSET_IMPORTS_FILE,
8
8
  COLLECTIONS_MANIFEST_FILE,
9
9
  CONTENT_LAYER_TYPE,
10
- DATA_STORE_FILE,
11
10
  MODULES_IMPORTS_FILE
12
11
  } from "./consts.js";
13
12
  import {
@@ -197,7 +196,7 @@ ${contentConfig.error.message}`
197
196
  logger.info("Content config changed");
198
197
  shouldClear = true;
199
198
  }
200
- if (previousAstroVersion && previousAstroVersion !== "7.0.8") {
199
+ if (previousAstroVersion && previousAstroVersion !== "7.1.0") {
201
200
  logger.info("Astro version changed");
202
201
  shouldClear = true;
203
202
  }
@@ -205,8 +204,8 @@ ${contentConfig.error.message}`
205
204
  logger.info("Clearing content store");
206
205
  this.#store.clearAll();
207
206
  }
208
- if ("7.0.8") {
209
- this.#store.metaStore().set("astro-version", "7.0.8");
207
+ if ("7.1.0") {
208
+ this.#store.metaStore().set("astro-version", "7.1.0");
210
209
  }
211
210
  if (currentConfigDigest) {
212
211
  this.#store.metaStore().set("content-config-digest", currentConfigDigest);
@@ -367,10 +366,6 @@ ${JSON.stringify({ ...raw, id: void 0 }, null, 2)}`
367
366
  )
368
367
  });
369
368
  }
370
- function getDataStoreFile(settings, isDev) {
371
- return new URL(DATA_STORE_FILE, isDev ? settings.dotAstroDir : settings.config.cacheDir);
372
- }
373
369
  export {
374
- ContentLayer,
375
- getDataStoreFile
370
+ ContentLayer
376
371
  };
@@ -0,0 +1,36 @@
1
+ import type { DataEntry, ImmutableDataStore } from './data-store.js';
2
+ /**
3
+ * A read-only, async view over content collection data, used at runtime by
4
+ * `getCollection()` and `getEntry()`. It lets the runtime read from different
5
+ * data sources (an in-memory snapshot today, potentially a database or remote
6
+ * source in the future) through one interface, without depending on Node.js
7
+ * APIs.
8
+ *
9
+ * The query methods are async so a source can perform I/O when resolving data.
10
+ * The default {@link InMemorySource} resolves synchronously.
11
+ */
12
+ export interface DataStoreSource {
13
+ hasCollection(collection: string): Promise<boolean> | boolean;
14
+ get<T = DataEntry>(collection: string, key: string): Promise<T | undefined> | T | undefined;
15
+ entries<T = DataEntry>(collection: string): Promise<Array<[id: string, T]>> | Array<[id: string, T]>;
16
+ values<T = DataEntry>(collection: string): Promise<Array<T>> | Array<T>;
17
+ keys(collection: string): Promise<Array<string>> | Array<string>;
18
+ has(collection: string, key: string): Promise<boolean> | boolean;
19
+ collections(): Promise<Map<string, Map<string, any>>> | Map<string, Map<string, any>>;
20
+ }
21
+ /**
22
+ * A {@link DataStoreSource} backed by an in-memory {@link ImmutableDataStore}.
23
+ * All queries resolve synchronously; the async signatures exist to satisfy the
24
+ * {@link DataStoreSource} contract.
25
+ */
26
+ export declare class InMemorySource implements DataStoreSource {
27
+ #private;
28
+ constructor(store: ImmutableDataStore);
29
+ hasCollection(collection: string): boolean;
30
+ get<T = DataEntry>(collection: string, key: string): T | undefined;
31
+ entries<T = DataEntry>(collection: string): Array<[id: string, T]>;
32
+ values<T = DataEntry>(collection: string): Array<T>;
33
+ keys(collection: string): Array<string>;
34
+ has(collection: string, key: string): boolean;
35
+ collections(): Map<string, Map<string, any>>;
36
+ }
@@ -0,0 +1,30 @@
1
+ class InMemorySource {
2
+ #store;
3
+ constructor(store) {
4
+ this.#store = store;
5
+ }
6
+ hasCollection(collection) {
7
+ return this.#store.hasCollection(collection);
8
+ }
9
+ get(collection, key) {
10
+ return this.#store.get(collection, key);
11
+ }
12
+ entries(collection) {
13
+ return this.#store.entries(collection);
14
+ }
15
+ values(collection) {
16
+ return this.#store.values(collection);
17
+ }
18
+ keys(collection) {
19
+ return this.#store.keys(collection);
20
+ }
21
+ has(collection, key) {
22
+ return this.#store.has(collection, key);
23
+ }
24
+ collections() {
25
+ return this.#store.collections();
26
+ }
27
+ }
28
+ export {
29
+ InMemorySource
30
+ };
@@ -0,0 +1,67 @@
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
+ */
6
+ export type DataStoreManifest = Record<string, string[]>;
7
+ /**
8
+ * Persists the content collection data produced by the content layer. This is
9
+ * the write side that saves the data; {@link import('./data-store-source.js').DataStoreSource}
10
+ * is the read side that loads it back. Implementations run in Node.js
11
+ * (build/dev) and are never imported at runtime.
12
+ */
13
+ export interface DataStoreWriter {
14
+ /** Serialize and persist the given collections. */
15
+ write(collections: Map<string, Map<string, any>>): Promise<void>;
16
+ }
17
+ /**
18
+ * Serialize collections to a deterministic devalue string.
19
+ */
20
+ export declare function serializeDataStore(collections: Map<string, Map<string, any>>): string;
21
+ /**
22
+ * Split a string into parts each at most `maxBytes` UTF-8 bytes, never splitting
23
+ * a Unicode code point across parts.
24
+ *
25
+ * The store is serialized with devalue and written to disk as UTF-8. Splitting
26
+ * on UTF-16 code-unit boundaries (e.g. `String.prototype.slice`) can cut a
27
+ * surrogate pair in half; encoding a lone surrogate to UTF-8 substitutes U+FFFD,
28
+ * 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.
32
+ */
33
+ export declare function chunkString(str: string, maxBytes: number): string[];
34
+ /**
35
+ * Atomically write `data` to `file`.
36
+ *
37
+ * The data is written to a temporary file and then renamed into place to avoid
38
+ * partial reads. If the file already contains identical data, the write is
39
+ * skipped. Callers are responsible for serializing concurrent writes to the
40
+ * same file.
41
+ */
42
+ export declare function writeFileAtomic(file: PathLike, data: string): Promise<void>;
43
+ /**
44
+ * A {@link DataStoreWriter} that serializes the whole store to a single file.
45
+ */
46
+ export declare class FileWriter implements DataStoreWriter {
47
+ #private;
48
+ constructor(file: PathLike);
49
+ write(collections: Map<string, Map<string, any>>): Promise<void>;
50
+ }
51
+ /**
52
+ * A {@link DataStoreWriter} that splits the store across many content-addressed
53
+ * files inside a directory, described by a manifest.
54
+ *
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
58
+ * parts keep the same name across builds and their writes are skipped, and two
59
+ * identical parts are naturally deduplicated. The manifest is written last as
60
+ * the commit point, and stale files are pruned afterwards. This is the inverse
61
+ * of {@link import('./data-store.js').ImmutableDataStore.manifestToMap}.
62
+ */
63
+ export declare class ChunkedWriter implements DataStoreWriter {
64
+ #private;
65
+ constructor(dir: URL);
66
+ write(collections: Map<string, Map<string, any>>): Promise<void>;
67
+ }
@@ -0,0 +1,94 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as devalue from "devalue";
3
+ import xxhash, {} from "xxhash-wasm";
4
+ import { emptyDir } from "../core/fs/index.js";
5
+ import { DATA_STORE_MANIFEST_FILE } from "./consts.js";
6
+ const CHUNK_SIZE_LIMIT = 20 * 1024 * 1024;
7
+ function sortCollections(collections) {
8
+ return new Map(
9
+ [...collections.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, collection]) => [
10
+ key,
11
+ new Map([...collection.entries()].sort(([a], [b]) => a.localeCompare(b)))
12
+ ])
13
+ );
14
+ }
15
+ function serializeDataStore(collections) {
16
+ return devalue.stringify(sortCollections(collections));
17
+ }
18
+ const ENCODER = new TextEncoder();
19
+ function chunkString(str, maxBytes) {
20
+ const chunks = [];
21
+ let startIndex = 0;
22
+ let index = 0;
23
+ let currentBytes = 0;
24
+ for (const char of str) {
25
+ const charBytes = ENCODER.encode(char).length;
26
+ if (currentBytes + charBytes > maxBytes && index > startIndex) {
27
+ chunks.push(str.slice(startIndex, index));
28
+ startIndex = index;
29
+ currentBytes = 0;
30
+ }
31
+ index += char.length;
32
+ currentBytes += charBytes;
33
+ }
34
+ if (startIndex < str.length) {
35
+ chunks.push(str.slice(startIndex));
36
+ }
37
+ return chunks;
38
+ }
39
+ async function writeFileAtomic(file, data) {
40
+ const tempFile = file instanceof URL ? new URL(`${file.href}.tmp`) : `${file}.tmp`;
41
+ const oldData = await fs.readFile(file, "utf-8").catch(() => "");
42
+ if (oldData === data) {
43
+ return;
44
+ }
45
+ await fs.writeFile(tempFile, data);
46
+ await fs.rename(tempFile, file);
47
+ }
48
+ class FileWriter {
49
+ #file;
50
+ constructor(file) {
51
+ this.#file = file;
52
+ }
53
+ async write(collections) {
54
+ await writeFileAtomic(this.#file, serializeDataStore(collections));
55
+ }
56
+ }
57
+ class ChunkedWriter {
58
+ #dir;
59
+ #manifestFile;
60
+ #hasher;
61
+ constructor(dir) {
62
+ this.#dir = dir;
63
+ this.#manifestFile = new URL(`./${DATA_STORE_MANIFEST_FILE}`, dir);
64
+ }
65
+ async write(collections) {
66
+ if (!this.#hasher) {
67
+ this.#hasher = await xxhash();
68
+ }
69
+ const { h64ToString } = this.#hasher;
70
+ const writtenFiles = /* @__PURE__ */ new Set();
71
+ const manifest = {};
72
+ for (const [collectionName, entries] of sortCollections(collections)) {
73
+ const stringified = devalue.stringify(entries);
74
+ const parts = [];
75
+ for (const part of chunkString(stringified, CHUNK_SIZE_LIMIT)) {
76
+ const fileName = `${h64ToString(part)}.txt`;
77
+ await writeFileAtomic(new URL(`./${fileName}`, this.#dir), part);
78
+ parts.push(fileName);
79
+ writtenFiles.add(fileName);
80
+ }
81
+ manifest[collectionName] = parts;
82
+ }
83
+ await writeFileAtomic(this.#manifestFile, JSON.stringify(manifest));
84
+ writtenFiles.add(DATA_STORE_MANIFEST_FILE);
85
+ emptyDir(this.#dir, writtenFiles);
86
+ }
87
+ }
88
+ export {
89
+ ChunkedWriter,
90
+ FileWriter,
91
+ chunkString,
92
+ serializeDataStore,
93
+ writeFileAtomic
94
+ };
@@ -46,6 +46,21 @@ export declare class ImmutableDataStore {
46
46
  has(collectionName: string, key: string): boolean;
47
47
  hasCollection(collectionName: string): boolean;
48
48
  collections(): Map<string, Map<string, any>>;
49
+ /**
50
+ * Rebuilds a collections map from a chunked-store manifest whose part file
51
+ * names have already been swapped for their contents.
52
+ *
53
+ * Each collection maps to a list of parts. A part is either a raw string
54
+ * (when the store is loaded from disk) or an ESM namespace from a `?raw`
55
+ * import (`{ default: string }`, when emitted into the virtual module at
56
+ * runtime). A collection's parts are concatenated back into the exact
57
+ * serialized string, then parsed with devalue. This is the inverse of
58
+ * {@link import('./data-store-writer.js').ChunkedWriter} and stays free of
59
+ * Node built-ins so it can run at runtime.
60
+ */
61
+ static manifestToMap(manifest: Record<string, Array<string | {
62
+ default: string;
63
+ }>>): Map<string, Map<string, any>>;
49
64
  /**
50
65
  * Attempts to load a DataStore from the virtual module.
51
66
  * This only works in Vite.
@@ -1,4 +1,5 @@
1
1
  import * as devalue from "devalue";
2
+ import { InMemorySource } from "./data-store-source.js";
2
3
  class ImmutableDataStore {
3
4
  _collections = /* @__PURE__ */ new Map();
4
5
  constructor() {
@@ -32,6 +33,30 @@ class ImmutableDataStore {
32
33
  collections() {
33
34
  return this._collections;
34
35
  }
36
+ /**
37
+ * Rebuilds a collections map from a chunked-store manifest whose part file
38
+ * names have already been swapped for their contents.
39
+ *
40
+ * Each collection maps to a list of parts. A part is either a raw string
41
+ * (when the store is loaded from disk) or an ESM namespace from a `?raw`
42
+ * import (`{ default: string }`, when emitted into the virtual module at
43
+ * runtime). A collection's parts are concatenated back into the exact
44
+ * serialized string, then parsed with devalue. This is the inverse of
45
+ * {@link import('./data-store-writer.js').ChunkedWriter} and stays free of
46
+ * Node built-ins so it can run at runtime.
47
+ */
48
+ static manifestToMap(manifest) {
49
+ const collections = /* @__PURE__ */ new Map();
50
+ for (const [collectionName, parts] of Object.entries(manifest)) {
51
+ let stringified = "";
52
+ for (const part of parts) {
53
+ stringified += typeof part === "string" ? part : part.default;
54
+ }
55
+ const entries = devalue.parse(stringified);
56
+ collections.set(collectionName, entries);
57
+ }
58
+ return collections;
59
+ }
35
60
  /**
36
61
  * Attempts to load a DataStore from the virtual module.
37
62
  * This only works in Vite.
@@ -42,7 +67,11 @@ class ImmutableDataStore {
42
67
  if (data.default instanceof Map) {
43
68
  return ImmutableDataStore.fromMap(data.default);
44
69
  }
45
- const map = devalue.unflatten(data.default);
70
+ if (Array.isArray(data.default)) {
71
+ const map2 = devalue.unflatten(data.default);
72
+ return ImmutableDataStore.fromMap(map2);
73
+ }
74
+ const map = ImmutableDataStore.manifestToMap(data.default);
46
75
  return ImmutableDataStore.fromMap(map);
47
76
  } catch {
48
77
  }
@@ -59,12 +88,13 @@ function dataStoreSingleton() {
59
88
  return {
60
89
  get: async () => {
61
90
  if (!instance) {
62
- instance = ImmutableDataStore.fromModule();
91
+ instance = ImmutableDataStore.fromModule().then((store) => new InMemorySource(store));
63
92
  }
64
93
  return instance;
65
94
  },
95
+ // Note: currently unused, but kept for API stability.
66
96
  set: (store) => {
67
- instance = store;
97
+ instance = new InMemorySource(store);
68
98
  }
69
99
  };
70
100
  }
@@ -23,6 +23,13 @@ interface GlobOptions {
23
23
  * Defaults to `true`.
24
24
  */
25
25
  retainBody?: boolean;
26
+ /**
27
+ * When `true`, renderable content entries (e.g. Markdown) will not be eagerly rendered
28
+ * during content sync. Instead, rendering is deferred until the entry is actually rendered
29
+ * in a page. This reduces memory usage for large collections with heavy rendered output.
30
+ * Defaults to `false`.
31
+ */
32
+ deferRender?: boolean;
26
33
  }
27
34
  export declare const secretLegacyFlag: unique symbol;
28
35
  /**
@@ -111,7 +111,7 @@ function glob(globOptions) {
111
111
  );
112
112
  }
113
113
  }
114
- if (entryType.getRenderFunction) {
114
+ if (entryType.getRenderFunction && !globOptions.deferRender) {
115
115
  let render = renderFunctionByContentType.get(entryType);
116
116
  if (!render) {
117
117
  render = await entryType.getRenderFunction(config);
@@ -138,7 +138,7 @@ function glob(globOptions) {
138
138
  rendered,
139
139
  assetImports: rendered?.metadata?.imagePaths
140
140
  });
141
- } else if ("contentModuleTypes" in entryType) {
141
+ } else if (entryType.getRenderFunction && globOptions.deferRender || "contentModuleTypes" in entryType) {
142
142
  store.set({
143
143
  id,
144
144
  data: parsedData,
@@ -35,6 +35,14 @@ export declare class MutableDataStore extends ImmutableDataStore {
35
35
  static fromMap(data: Map<string, Map<string, any>>): Promise<MutableDataStore>;
36
36
  static fromString(data: string): Promise<MutableDataStore>;
37
37
  static fromFile(filePath: string | URL): Promise<MutableDataStore>;
38
+ /**
39
+ * Loads a MutableDataStore from a chunked store directory (experimental
40
+ * `collectionStorage: 'chunked'`), reading the manifest and its referenced parts.
41
+ * If the directory has no manifest yet (fresh build) it starts empty. If the
42
+ * manifest exists but can't be read (corrupt cache), it warns and starts
43
+ * empty so loaders rebuild it, rather than failing the sync.
44
+ */
45
+ static fromDir(dirPath: URL): Promise<MutableDataStore>;
38
46
  }
39
47
  export interface DataStore {
40
48
  get: <TData extends Record<string, unknown> = Record<string, unknown>>(key: string) => DataEntry<TData> | undefined;