content-query 0.0.2 → 0.2.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -2
  3. package/dist/build-DfKd2bpf.d.mts +25 -0
  4. package/dist/build-Dxz7P1ur.mjs +1439 -0
  5. package/dist/build.d.mts +2 -0
  6. package/dist/build.mjs +1 -0
  7. package/dist/cli.d.mts +1 -0
  8. package/dist/cli.mjs +97 -0
  9. package/dist/computed.d.mts +43 -0
  10. package/dist/computed.mjs +139 -0
  11. package/dist/content-G69dPPuo.d.mts +109 -0
  12. package/dist/content-N1gYshjg.mjs +1028 -0
  13. package/dist/descriptor-D9Th5hzk.d.mts +18 -0
  14. package/dist/diagnostics-Cu94mZhl.mjs +80 -0
  15. package/dist/dist-C7_FOzY_.mjs +5405 -0
  16. package/dist/errors-C2iuBM0G.mjs +72 -0
  17. package/dist/github-slugger-BMgR_Q0I.mjs +72 -0
  18. package/dist/glob-Bv0dpvCQ.mjs +159 -0
  19. package/dist/index.d.mts +78 -0
  20. package/dist/index.mjs +46 -0
  21. package/dist/internal/markdown.d.mts +5 -0
  22. package/dist/internal/markdown.mjs +1 -0
  23. package/dist/internal.d.mts +54 -0
  24. package/dist/internal.mjs +89 -0
  25. package/dist/loaders.d.mts +11 -0
  26. package/dist/loaders.mjs +86 -0
  27. package/dist/markdown-CHvBBJNa.mjs +54 -0
  28. package/dist/next.d.mts +20 -0
  29. package/dist/next.mjs +142 -0
  30. package/dist/operators-UqWUhKQJ.mjs +173 -0
  31. package/dist/query.d.mts +44 -0
  32. package/dist/query.mjs +1 -0
  33. package/dist/relational-types-DgK3rLuw.d.mts +278 -0
  34. package/dist/rolldown-runtime-Bg1v5YCA.mjs +34 -0
  35. package/dist/runtime.d.mts +3 -0
  36. package/dist/runtime.mjs +5 -0
  37. package/dist/schema-C5Str-E8.mjs +20 -0
  38. package/dist/schema.d.mts +11 -0
  39. package/dist/schema.mjs +1 -0
  40. package/dist/server-only-CIJUlyL-.mjs +7 -0
  41. package/dist/types-C-kdMblM.d.mts +69 -0
  42. package/dist/types-ChaNhj42.d.mts +143 -0
  43. package/dist/types-Dh-Sydyw.d.mts +20 -0
  44. package/dist/validation-D_XdE1pF.mjs +84 -0
  45. package/dist/values-BbTV4QT-.mjs +901 -0
  46. package/dist/vite.d.mts +6 -0
  47. package/dist/vite.mjs +296 -0
  48. package/package.json +92 -10
@@ -0,0 +1,1439 @@
1
+ import { a as builtinLoader, i as validate, n as normalizeSnapshot, o as builtinPath, r as renderedPayload, t as isRecord } from "./validation-D_XdE1pF.mjs";
2
+ import { i as groupDiagnostics, n as contentError, o as withContext, t as ContentError } from "./errors-C2iuBM0G.mjs";
3
+ import { i as detach, l as valuePath, o as encode } from "./values-BbTV4QT-.mjs";
4
+ import { t as assertServerOnly } from "./server-only-CIJUlyL-.mjs";
5
+ import { a as CODEC_VERSION, i as prepareRelationships, n as createRuntime, r as isRelationship } from "./content-N1gYshjg.mjs";
6
+ import { t as renderMarkdown } from "./markdown-CHvBBJNa.mjs";
7
+ import { access, link, mkdir, readFile, readdir, realpath, rm, rmdir, stat, writeFile } from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+ import { createHash, randomUUID } from "node:crypto";
11
+ import { realpathSync, renameSync, unwatchFile, watchFile } from "node:fs";
12
+ import { gzipSync } from "node:zlib";
13
+ import { createJiti } from "jiti";
14
+ import { watch } from "chokidar";
15
+ import picomatch from "picomatch";
16
+ //#region src/artifacts/modules.ts
17
+ /** Creates an opaque content-addressed key with an unambiguous serialization boundary. */
18
+ function digest(value) {
19
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
20
+ }
21
+ /** Maps collection names to distinct safe directory names without changing public identities. */
22
+ function directoryName(name) {
23
+ return /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name) && !/^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/i.test(name) ? name : `~${Buffer.from(name).toString("base64url")}`;
24
+ }
25
+ /** Returns a gzip/base64 JavaScript literal only when it saves at least ten percent. */
26
+ function compactJson(source) {
27
+ const bytes = Buffer.byteLength(source);
28
+ if (bytes < 4096) return;
29
+ const compressed = JSON.stringify(gzipSync(source, { level: 1 }).toString("base64"));
30
+ return compressed.length < bytes * .9 ? compressed : void 0;
31
+ }
32
+ /** Creates portable ESM; build integrations can batch payload sources before a strict bundler runs. */
33
+ async function revisionModules(revision, { payloadChunkBytes = 0 } = {}) {
34
+ const modules = /* @__PURE__ */ new Map();
35
+ const manifest = {
36
+ version: 2,
37
+ codec: CODEC_VERSION,
38
+ collections: revision.registry
39
+ };
40
+ const imports = [];
41
+ const names = [];
42
+ let needsRenderer = false;
43
+ for (const [name, collection] of Object.entries(revision.collections)) {
44
+ const directory = `collections/${directoryName(name)}`;
45
+ const payloads = [];
46
+ const chunks = [];
47
+ let batch = [];
48
+ let batchBytes = 0;
49
+ /** Retains collision checks for both individual payloads and batched source modules. */
50
+ function publishPayload(filename, code) {
51
+ if (modules.has(filename) && modules.get(filename) !== code) throw new Error("Payload hash collision.");
52
+ modules.set(filename, code);
53
+ }
54
+ /** Emits one lazy source module without creating a dynamic-import entry per item. */
55
+ function flushBatch() {
56
+ if (!batch.length) return;
57
+ const source = JSON.stringify(Object.fromEntries(batch));
58
+ const code = `export default ${compactJson(source) ?? `JSON.parse(${JSON.stringify(source)})`};\n`;
59
+ const key = digest([name, code]);
60
+ publishPayload(`${directory}/entries/${key}.js`, code);
61
+ chunks.push(`{ ids: ${JSON.stringify(batch.map(([id]) => id))}, load: () => import(${JSON.stringify(`./entries/${key}.js`)}) }`);
62
+ batch = [];
63
+ batchBytes = 0;
64
+ }
65
+ for (const [id, load] of collection.builtins) {
66
+ const payload = await load();
67
+ const encoded = encode(payload);
68
+ needsRenderer ||= payload.markdown !== void 0;
69
+ if (payloadChunkBytes) {
70
+ const bytes = Buffer.byteLength(`[${JSON.stringify(id)}]: ${JSON.stringify(encoded)},\n`);
71
+ if (batchBytes && batchBytes + bytes > payloadChunkBytes) flushBatch();
72
+ batch.push([id, encoded]);
73
+ batchBytes += bytes;
74
+ continue;
75
+ }
76
+ const key = digest([
77
+ name,
78
+ id,
79
+ encoded
80
+ ]);
81
+ publishPayload(`${directory}/entries/${key}.js`, `export default ${JSON.stringify(encoded)};\n`);
82
+ payloads.push(`[${JSON.stringify(id)}]: () => import(${JSON.stringify(`./entries/${key}.js`)})`);
83
+ }
84
+ flushBatch();
85
+ const recordSource = JSON.stringify(collection.records.map(encode));
86
+ const compressedRecords = compactJson(recordSource);
87
+ const helpers = [...chunks.length ? ["createPayloadLoaders"] : [], ...compressedRecords ? ["decodeCompressed"] : []];
88
+ const loaders = helpers.length ? `import { ${helpers.join(", ")} } from "content-query/internal";\n` : "";
89
+ const loading = chunks.length ? `createPayloadLoaders([${chunks.join(",\n")}])` : `{ ${payloads.join(",\n")} }`;
90
+ const code = `${loaders}export default { records: ${compressedRecords ? `decodeCompressed(${compressedRecords})` : recordSource}, builtins: ${loading} };\n`;
91
+ const index = `index-${digest(code)}.js`;
92
+ modules.set(`${directory}/${index}`, code);
93
+ modules.set(`${directory}/index.js`, `export { default } from ${JSON.stringify(`./${index}`)};\n`);
94
+ const binding = `collection${imports.length}`;
95
+ imports.push(`import ${binding} from ${JSON.stringify(`./${directory}/${index}`)};`);
96
+ names.push(`[${JSON.stringify(name)}]: ${binding}`);
97
+ }
98
+ modules.set("manifest.js", `export default JSON.parse(${JSON.stringify(JSON.stringify(manifest))});\n`);
99
+ const renderer = needsRenderer ? `, body => import("content-query/internal/markdown").then(module => module.renderMarkdown(body))` : "";
100
+ modules.set("server.js", `import { createArtifactRuntime } from "content-query/internal";\n${imports.join("\n")}\nexport const content = createArtifactRuntime(JSON.parse(${JSON.stringify(JSON.stringify(manifest))}), { ${names.join(", ")} }${renderer});\n`);
101
+ return modules;
102
+ }
103
+ //#endregion
104
+ //#region src/artifacts/declarations.ts
105
+ /** Generates type-only consumer bindings without loading schemas or adding runtime imports. */
106
+ function contentDeclarations(directory, config) {
107
+ let relative = path.relative(directory, config).replace(/\\/g, "/");
108
+ if (!relative.startsWith(".")) relative = `./${relative}`;
109
+ return `type Registry = typeof import(${JSON.stringify(relative)}).collections;\nexport type CollectionKey = keyof Registry & string;\nexport type CollectionData<Name extends CollectionKey> = import("content-query").QueryData<Registry[Name]>;\nexport type Collections = { [Name in CollectionKey]: CollectionData<Name> };\nexport declare const content: import("content-query/runtime").Content<Registry>;\n`;
110
+ }
111
+ /** Declares project-bound virtual server types and the framework-neutral development bridge. */
112
+ function virtualDeclarations(directory, config) {
113
+ return `declare module "content-query:server" {\n${contentDeclarations(directory, config).replace("export declare const", "export const")}\n}\ndeclare module "content-query:client" {\n export function onContentUpdate(callback: () => void | Promise<void>): () => void;\n}\n`;
114
+ }
115
+ //#endregion
116
+ //#region src/artifacts/write.ts
117
+ /** Writes changed bytes only, keeping unchanged importer targets stable. */
118
+ async function writeChanged(filename, source, signal) {
119
+ try {
120
+ if (await readFile(filename, "utf8") === source) return;
121
+ } catch {}
122
+ await mkdir(path.dirname(filename), { recursive: true });
123
+ const temporary = `${filename}.tmp-${process.pid}-${randomUUID()}`;
124
+ await writeFile(temporary, source);
125
+ try {
126
+ signal?.throwIfAborted();
127
+ renameSync(temporary, filename);
128
+ } finally {
129
+ await rm(temporary, { force: true });
130
+ }
131
+ }
132
+ /** Checks reusable immutable bytes, retaining corruption diagnostics instead of overwriting them. */
133
+ async function existingImmutable(filename, source) {
134
+ let existing;
135
+ try {
136
+ existing = await readFile(filename, "utf8");
137
+ } catch (cause) {
138
+ if (cause && typeof cause === "object" && "code" in cause && cause.code === "ENOENT") return false;
139
+ throw cause;
140
+ }
141
+ if (existing !== source) throw new ContentError({
142
+ code: "artifact-error",
143
+ operation: "build",
144
+ source: filename,
145
+ message: "An immutable artifact key has conflicting bytes. Regenerate the output directory."
146
+ });
147
+ return true;
148
+ }
149
+ /** Publishes into an existing directory without rewriting an earlier revision's immutable bytes. */
150
+ async function writeImmutable(filename, source) {
151
+ if (await existingImmutable(filename, source)) return;
152
+ const temporary = `${filename}.tmp-${process.pid}-${randomUUID()}`;
153
+ await writeFile(temporary, source);
154
+ try {
155
+ try {
156
+ await link(temporary, filename);
157
+ } catch (cause) {
158
+ if (!cause || typeof cause !== "object" || !("code" in cause) || cause.code !== "EEXIST") throw cause;
159
+ if (!await existingImmutable(filename, source)) throw cause;
160
+ }
161
+ } finally {
162
+ await rm(temporary, { force: true });
163
+ }
164
+ }
165
+ const immutableModule = /\/(?:entries\/[a-f0-9]{64}|index-[a-f0-9]{64})\.js$/;
166
+ /** Lists one directory's entries, treating a missing directory or a file as empty. */
167
+ async function entriesOf(directory) {
168
+ try {
169
+ return await readdir(directory);
170
+ } catch (cause) {
171
+ const code = cause && typeof cause === "object" && "code" in cause ? cause.code : void 0;
172
+ if (code === "ENOENT" || code === "ENOTDIR") return [];
173
+ throw cause;
174
+ }
175
+ }
176
+ /** Removes a directory only when nothing else remains in it. */
177
+ async function removeIfEmpty(directory) {
178
+ try {
179
+ await rmdir(directory);
180
+ } catch (cause) {
181
+ const code = cause && typeof cause === "object" && "code" in cause ? cause.code : void 0;
182
+ if (![
183
+ "ENOTEMPTY",
184
+ "EEXIST",
185
+ "ENOENT",
186
+ "ENOTDIR"
187
+ ].includes(String(code))) throw cause;
188
+ }
189
+ }
190
+ /**
191
+ * Deletes generated collection modules the published revision no longer references. Only
192
+ * library-named files are removed, so unrelated files in the output directory survive.
193
+ */
194
+ async function pruneArtifacts(outDir, modules) {
195
+ const collections = path.join(outDir, "collections");
196
+ for (const directory of await entriesOf(collections)) {
197
+ const owned = path.join(collections, directory);
198
+ const entries = path.join(owned, "entries");
199
+ const candidates = [...(await entriesOf(owned)).map((name) => `collections/${directory}/${name}`), ...(await entriesOf(entries)).map((name) => `collections/${directory}/entries/${name}`)];
200
+ for (const filename of candidates) if ((immutableModule.test(`/${filename}`) || filename.endsWith(`/${directory}/index.js`)) && !modules.has(filename)) await rm(path.join(outDir, filename), { force: true });
201
+ await removeIfEmpty(entries);
202
+ await removeIfEmpty(owned);
203
+ }
204
+ }
205
+ /** Stages modules with bounded I/O before atomically switching the public server entry. */
206
+ async function writeArtifacts(revision, outDir, config, { signal, prune = false } = {}) {
207
+ const modules = await revisionModules(revision);
208
+ await writeChanged(path.join(outDir, "package.json"), "{\"type\":\"module\",\"private\":true}\n");
209
+ await writeChanged(path.join(outDir, "server.d.ts"), contentDeclarations(outDir, config));
210
+ await writeChanged(path.join(outDir, "types.d.ts"), virtualDeclarations(outDir, config));
211
+ const staged = [...modules].filter(([filename]) => filename !== "server.js");
212
+ const directories = new Set(staged.map(([filename]) => path.dirname(path.join(outDir, filename))));
213
+ await Promise.all([...directories].map(async (directory) => mkdir(directory, { recursive: true })));
214
+ const pending = staged.values();
215
+ const failure = (await Promise.allSettled(Array.from({ length: Math.min(16, staged.length) }, async () => {
216
+ for (const [filename, code] of pending) {
217
+ signal?.throwIfAborted();
218
+ if (immutableModule.test(`/${filename}`)) await writeImmutable(path.join(outDir, filename), code);
219
+ else await writeChanged(path.join(outDir, filename), code);
220
+ }
221
+ }))).find((outcome) => outcome.status === "rejected");
222
+ if (failure?.status === "rejected") throw failure.reason;
223
+ await writeChanged(path.join(outDir, "server.js"), modules.get("server.js"), signal);
224
+ if (prune) await pruneArtifacts(outDir, modules);
225
+ }
226
+ //#endregion
227
+ //#region src/collection/registry.ts
228
+ /** Built-in payload names start with an underscore, which custom payload names cannot use. */
229
+ const builtinPayloads = {
230
+ _body: { variants: [] },
231
+ _rendered: { variants: [] }
232
+ };
233
+ /**
234
+ * Reports whether a collection can carry built-in `_body` and `_rendered` payloads.
235
+ * This depends only on the declaration, never on the entries a source currently contains.
236
+ *
237
+ * @param definition - A collection definition, including the `source` shorthand when used.
238
+ * @returns False for JSON, YAML, and TOML sources and `file()` loaders; true otherwise.
239
+ */
240
+ function hasBuiltinPayloads(definition) {
241
+ const source = definition.source;
242
+ if (source !== void 0) return typeof source === "string" || source.type === "markdown";
243
+ return builtinLoader(definition.loader)?.kind !== "file";
244
+ }
245
+ /**
246
+ * Builds the query registry that runtimes validate queries against, without resolvers or schemas.
247
+ *
248
+ * @param registry - The configured collection definitions.
249
+ * @returns Relationship and payload descriptors for each collection.
250
+ */
251
+ function queryRegistry(registry) {
252
+ return Object.fromEntries(Object.entries(registry).map(([name, definition]) => [name, {
253
+ relationships: definition.relationships ?? {},
254
+ payloads: hasBuiltinPayloads(definition) ? { ...builtinPayloads } : {}
255
+ }]));
256
+ }
257
+ //#endregion
258
+ //#region src/preparation/prepare.ts
259
+ /** Prepares one immutable revision with loader-defined source metadata, aggregating failures. */
260
+ async function prepareRevision(registry, sources, cache, signal, development, sourceDiagnostics = [], parsedData) {
261
+ const diagnostics = [...sourceDiagnostics];
262
+ const collections = Object.create(null);
263
+ for (const [collectionName, definition] of Object.entries(registry)) {
264
+ const records = [];
265
+ const builtins = /* @__PURE__ */ new Map();
266
+ const computed = definition.computed ?? {};
267
+ const relationships = definition.relationships ?? {};
268
+ const declarationNames = [...Object.keys(computed), ...Object.keys(relationships)];
269
+ if (declarationNames.some((name) => [
270
+ "id",
271
+ "_adjacent",
272
+ "_payloads"
273
+ ].includes(name)) || new Set(declarationNames).size !== declarationNames.length || Object.values(relationships).some((relation) => !isRelationship(relation))) {
274
+ diagnostics.push({
275
+ code: "validation-error",
276
+ operation: "build",
277
+ collection: collectionName,
278
+ message: "Invalid or overlapping computed/relationship declarations."
279
+ });
280
+ continue;
281
+ }
282
+ const descriptor = builtinLoader(definition.loader);
283
+ for (const entry of sources.get(collectionName)?.values() ?? []) {
284
+ signal.throwIfAborted();
285
+ const context = {
286
+ operation: "build",
287
+ collection: collectionName,
288
+ id: entry.id,
289
+ source: entry.source
290
+ };
291
+ const key = JSON.stringify([collectionName, entry.id]);
292
+ let signature;
293
+ try {
294
+ signature = encode(entry);
295
+ } catch {}
296
+ const previous = cache.get(key);
297
+ if (signature !== void 0 && previous?.signature === signature && previous.definition === definition) {
298
+ records.push(previous.record);
299
+ if (Object.keys(previous.payload).length) builtins.set(entry.id, async () => previous.payload);
300
+ continue;
301
+ }
302
+ try {
303
+ const parsed = parsedData?.get(collectionName);
304
+ const validated = parsed?.has(entry.data) && parsed.get(entry.data) === definition.schema ? entry.data : await validate(definition.schema, entry.data, context);
305
+ signal.throwIfAborted();
306
+ if (!isRecord(validated)) throw new Error("Validated metadata must be a record of named fields.");
307
+ if (Object.hasOwn(validated, "id") || Object.hasOwn(validated, "_adjacent") || Object.hasOwn(validated, "_payloads") || declarationNames.some((name) => Object.hasOwn(validated, name))) throw new Error("Metadata, computed fields, relationships, and reserved id/_adjacent/_payloads must have distinct names.");
308
+ const metadata = detach(validated);
309
+ const values = Object.create(null);
310
+ const before = diagnostics.length;
311
+ for (const [field, computedField] of Object.entries(computed)) try {
312
+ const result = await computedField.resolve({
313
+ collection: collectionName,
314
+ id: entry.id,
315
+ source: detach(entry.sourceData),
316
+ data: detach(metadata),
317
+ readBody: async () => entry.body,
318
+ signal
319
+ });
320
+ values[field] = await validate(computedField.schema, result, {
321
+ ...context,
322
+ path: [field]
323
+ });
324
+ } catch (cause) {
325
+ diagnostics.push(...contentError(cause, {
326
+ ...context,
327
+ path: [field],
328
+ code: "validation-error",
329
+ message: `Computed field "${field}" failed.`
330
+ }).diagnostics);
331
+ }
332
+ signal.throwIfAborted();
333
+ if (before !== diagnostics.length) continue;
334
+ const record = detach({
335
+ ...metadata,
336
+ ...values,
337
+ id: entry.id
338
+ });
339
+ const payload = {};
340
+ const markdown = descriptor?.kind === "glob" && entry.source?.toLowerCase().endsWith(".md");
341
+ if (entry.body !== void 0 && (!markdown || descriptor?.kind !== "glob" || descriptor.options.retainBody !== false)) payload.body = entry.body;
342
+ const supplied = renderedPayload(entry);
343
+ if (supplied) payload.rendered = supplied;
344
+ else if (markdown && entry.body !== void 0) {
345
+ if (development || descriptor.options.deferRender === true) payload.markdown = entry.body;
346
+ else try {
347
+ payload.rendered = await renderMarkdown(entry.body);
348
+ } catch (cause) {
349
+ throw new ContentError({
350
+ ...context,
351
+ code: "render-error",
352
+ message: "Eager Markdown compilation failed.",
353
+ cause
354
+ });
355
+ }
356
+ }
357
+ encode(payload);
358
+ cache.set(key, {
359
+ definition,
360
+ signature,
361
+ record,
362
+ payload
363
+ });
364
+ records.push(record);
365
+ if (Object.keys(payload).length) builtins.set(entry.id, async () => payload);
366
+ } catch (cause) {
367
+ const formatted = cause && typeof cause === "object" && "path" in cause && typeof cause.path === "string" ? cause.path : "";
368
+ const message = cause instanceof Error ? cause.message : "Entry preparation failed.";
369
+ diagnostics.push(...contentError(cause, {
370
+ ...context,
371
+ path: formatted ? valuePath(formatted) : void 0,
372
+ code: "validation-error",
373
+ message: formatted ? `${message} (at ${formatted})` : message
374
+ }).diagnostics);
375
+ }
376
+ }
377
+ collections[collectionName] = {
378
+ records,
379
+ relationships,
380
+ builtins,
381
+ sources: new Map([...sources.get(collectionName)?.values() ?? []].map((entry) => [entry.id, entry.source]))
382
+ };
383
+ }
384
+ signal.throwIfAborted();
385
+ const revision = {
386
+ collections,
387
+ registry: queryRegistry(Object.fromEntries(Object.keys(collections).map((name) => [name, registry[name]]))),
388
+ relations: /* @__PURE__ */ new Map(),
389
+ active: 0,
390
+ payloadCache: /* @__PURE__ */ new Map(),
391
+ renderCache: /* @__PURE__ */ new Map(),
392
+ renderMarkdown
393
+ };
394
+ const failed = diagnostics.map((diagnostic) => diagnostic.collection);
395
+ if (!failed.includes(void 0)) diagnostics.push(...prepareRelationships(revision, new Set(failed)).flatMap((error) => error.diagnostics));
396
+ if (diagnostics.length) throw groupDiagnostics(diagnostics);
397
+ return revision;
398
+ }
399
+ //#endregion
400
+ //#region src/preparation/config.ts
401
+ /** Loads build-only configuration without retaining a previous config module value. */
402
+ async function loadRegistry(config) {
403
+ const filename = await realpath(config);
404
+ const dependencies = /* @__PURE__ */ new Set([filename]);
405
+ const transpiler = createJiti(filename, {
406
+ moduleCache: false,
407
+ fsCache: false
408
+ });
409
+ const imported = await createJiti(filename, {
410
+ moduleCache: false,
411
+ fsCache: false,
412
+ transform: (options) => {
413
+ if (options.filename) dependencies.add(path.resolve(options.filename));
414
+ return { code: transpiler.transform(options) };
415
+ }
416
+ }).evalModule(await readFile(filename, "utf8"), {
417
+ filename,
418
+ async: true,
419
+ forceTranspile: true
420
+ });
421
+ if (imported.loaderConfig !== void 0 && (!imported.loaderConfig || typeof imported.loaderConfig !== "object" || Array.isArray(imported.loaderConfig))) throw new Error("content-query.config loaderConfig must be an object.");
422
+ const registry = imported.collections;
423
+ if (!registry || typeof registry !== "object" || Array.isArray(registry)) throw new Error("content-query.config must export a collections registry.");
424
+ for (const [name, definition] of Object.entries(registry)) if (!name || !definition || typeof definition !== "object" || !("loader" in definition) || !definition.loader || typeof definition.loader !== "object" || !("name" in definition.loader) || typeof definition.loader.name !== "string" || !definition.loader.name || !("load" in definition.loader) || typeof definition.loader.load !== "function") throw new Error(`Collection "${name}" requires a named loader object with load(context).`);
425
+ return {
426
+ registry,
427
+ dependencies,
428
+ loaderConfig: imported.loaderConfig
429
+ };
430
+ }
431
+ //#endregion
432
+ //#region src/shared/paths.ts
433
+ /** Checks containment without confusing sibling prefixes, drive roots, or trailing separators. */
434
+ function withinDirectory(directory, filename) {
435
+ const relative = path.relative(directory, filename);
436
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
437
+ }
438
+ /** Resolves existing symlink ancestors even when the output directory does not exist yet. */
439
+ async function canonicalPath(filename) {
440
+ try {
441
+ return await realpath(filename);
442
+ } catch (cause) {
443
+ if (!cause || typeof cause !== "object" || !("code" in cause) || cause.code !== "ENOENT") throw cause;
444
+ const parent = path.dirname(filename);
445
+ if (parent === filename) throw cause;
446
+ return path.join(await canonicalPath(parent), path.basename(filename));
447
+ }
448
+ }
449
+ //#endregion
450
+ //#region src/loaders/match.ts
451
+ const matchers = /* @__PURE__ */ new WeakMap();
452
+ /**
453
+ * Builds a watch-time superset of tinyglobby discovery: every file discovery could return
454
+ * matches, so an edit is never missed, while unrelated files are ignored.
455
+ */
456
+ function globMatcher(options) {
457
+ const cached = matchers.get(options);
458
+ if (cached) return cached;
459
+ const patterns = typeof options.pattern === "string" ? [options.pattern] : options.pattern;
460
+ const negated = (pattern) => pattern.startsWith("!") && !pattern.startsWith("!(");
461
+ const included = patterns.filter((pattern) => pattern && !negated(pattern));
462
+ const excluded = patterns.filter((pattern) => pattern && negated(pattern));
463
+ const matchOptions = {
464
+ dot: false,
465
+ posix: true
466
+ };
467
+ let matcher;
468
+ if (included.some((pattern) => pattern.startsWith("..") || path.posix.isAbsolute(pattern))) matcher = () => true;
469
+ else {
470
+ const match = picomatch(included.flatMap((pattern) => {
471
+ const normalized = path.posix.normalize(pattern).replace(/\/$/, "");
472
+ return [normalized, `${normalized}/**`];
473
+ }), matchOptions);
474
+ const ignore = excluded.length ? picomatch(excluded.map((pattern) => path.posix.normalize(pattern.slice(1))), matchOptions) : () => false;
475
+ matcher = (relative) => match(relative) && !ignore(relative);
476
+ }
477
+ matchers.set(options, matcher);
478
+ return matcher;
479
+ }
480
+ /** Reports whether a filesystem event can change a built-in loader's next snapshot. */
481
+ function affectsSnapshot(descriptor, base, event, filename) {
482
+ if (event === "addDir") return false;
483
+ if (filename === base) return true;
484
+ if (event === "unlinkDir" && (withinDirectory(filename, base) || descriptor.kind === "glob" && withinDirectory(base, filename))) return true;
485
+ if (descriptor.kind !== "glob" || !withinDirectory(base, filename)) return false;
486
+ return globMatcher(descriptor.options)(path.relative(base, filename).split(path.sep).join("/"));
487
+ }
488
+ //#endregion
489
+ //#region src/preparation/watch.ts
490
+ const awaitWriteFinish = {
491
+ stabilityThreshold: 50,
492
+ pollInterval: 10
493
+ };
494
+ /** Finds an existing ancestor so watching also detects a missing base directory's creation. */
495
+ async function watchAncestor(filename) {
496
+ let ancestor = filename;
497
+ while (true) try {
498
+ await access(ancestor);
499
+ return ancestor;
500
+ } catch {
501
+ const parent = path.dirname(ancestor);
502
+ if (parent === ancestor) return parent;
503
+ ancestor = parent;
504
+ }
505
+ }
506
+ /** Resolves symlinked ancestors so watcher paths compare with Jiti's physical module paths. */
507
+ function physicalPath(filename) {
508
+ try {
509
+ return realpathSync(filename);
510
+ } catch {
511
+ try {
512
+ return path.join(realpathSync(path.dirname(filename)), path.basename(filename));
513
+ } catch {
514
+ return filename;
515
+ }
516
+ }
517
+ }
518
+ /** Matches edits a config reload can observe, conservatively while the module graph is unknown. */
519
+ function configDependency(options, filename) {
520
+ const resolved = path.resolve(filename);
521
+ if (resolved === options.config) return true;
522
+ const dependencies = options.dependencies();
523
+ if (!dependencies) return /\.(?:[cm]?[jt]sx?)$/.test(resolved);
524
+ return dependencies.has(resolved) || dependencies.has(physicalPath(resolved));
525
+ }
526
+ /** Resolves once a watcher has subscribed to its initial paths. */
527
+ function ready(watcher) {
528
+ return new Promise((resolve, reject) => {
529
+ watcher.once("ready", resolve);
530
+ watcher.once("error", reject);
531
+ });
532
+ }
533
+ /** Shares the project ignore policy with declared-input subscription coverage checks. */
534
+ function projectIgnored(options) {
535
+ const ignoredRoots = [...options.output ? [options.output] : [], path.join(options.root, ".content-query")];
536
+ return (filename) => filename.split(path.sep).some((segment) => [
537
+ "node_modules",
538
+ ".git",
539
+ ".next",
540
+ ".void",
541
+ ".wrangler",
542
+ "dist",
543
+ "coverage"
544
+ ].includes(segment)) || ignoredRoots.some((ignored) => withinDirectory(ignored, filename));
545
+ }
546
+ /** Registers ownership before awaiting readiness so initialization failures retain cleanup. */
547
+ async function watchProject(options, register) {
548
+ const watcher = watch([options.root, await watchAncestor(path.dirname(options.config))], {
549
+ ignoreInitial: true,
550
+ awaitWriteFinish,
551
+ ignored: projectIgnored(options)
552
+ });
553
+ register(watcher);
554
+ watcher.on("all", (event, filename) => {
555
+ if (event === "addDir") return;
556
+ if (configDependency(options, filename)) {
557
+ options.configChanged();
558
+ return;
559
+ }
560
+ });
561
+ watcher.on("error", (cause) => options.failed(cause));
562
+ await ready(watcher);
563
+ }
564
+ /** Owns scoped source listeners and external subscriptions for one configuration. */
565
+ async function watchSources(options, register) {
566
+ const ignoredByProject = projectIgnored(options);
567
+ const modules = [...options.dependencies() ?? []].filter((filename) => !withinDirectory(options.root, filename) && !withinDirectory(path.dirname(options.config), filename) && !filename.split(path.sep).includes("node_modules"));
568
+ if (modules.length) {
569
+ const external = watch(modules, {
570
+ ignoreInitial: true,
571
+ awaitWriteFinish
572
+ });
573
+ register(() => external.close());
574
+ external.on("all", () => options.configChanged());
575
+ external.on("error", (cause) => options.failed(cause));
576
+ await ready(external);
577
+ }
578
+ for (const [name, definition] of Object.entries(options.registry())) {
579
+ const builtin = builtinLoader(definition.loader);
580
+ if (builtin) {
581
+ const target = builtinPath(builtin, options.root);
582
+ const outputRoots = [path.join(options.root, ".content-query"), ...options.output ? [options.output] : []];
583
+ const ancestor = await watchAncestor(target);
584
+ const shared = !ignoredByProject(target) && (withinDirectory(options.root, target) || withinDirectory(path.dirname(options.config), target)) ? options.projectWatcher : void 0;
585
+ const dedicated = !shared || builtin.kind === "glob";
586
+ const external = dedicated ? watch(ancestor, {
587
+ ignoreInitial: true,
588
+ awaitWriteFinish,
589
+ ignored: (filename, stats) => !(withinDirectory(target, filename) || withinDirectory(filename, target)) || outputRoots.some((output) => withinDirectory(output, filename)) || !!(shared && stats?.isFile() && !ignoredByProject(filename))
590
+ }) : shared;
591
+ /** Invalidates only the collection whose declared input can match this event. */
592
+ function changed(event, filename) {
593
+ if (affectsSnapshot(builtin, target, event, filename)) options.invalidated(name);
594
+ }
595
+ if (shared) {
596
+ shared.on("all", changed);
597
+ register(() => {
598
+ shared.off("all", changed);
599
+ });
600
+ }
601
+ if (dedicated) register(() => external.close());
602
+ if (ancestor !== target && !outputRoots.some((output) => withinDirectory(output, target))) {
603
+ /** Recovers a missing declared path even if the ancestor's creation event is lost. */
604
+ function recheck(current, previous) {
605
+ if (current.nlink === 0 && previous.nlink === 0) return;
606
+ external.add(target);
607
+ if (shared && shared !== external) shared.add(target);
608
+ options.invalidated(name);
609
+ }
610
+ watchFile(target, {
611
+ interval: 100,
612
+ persistent: false
613
+ }, recheck);
614
+ register(() => unwatchFile(target, recheck));
615
+ }
616
+ if (dedicated) {
617
+ external.on("all", changed);
618
+ external.on("error", (cause) => options.failed(cause));
619
+ await ready(external);
620
+ }
621
+ }
622
+ }
623
+ }
624
+ //#endregion
625
+ //#region src/preparation/loaders/context.ts
626
+ /** Supplies portable filesystem defaults and fails explicitly for unavailable Astro settings. */
627
+ function loaderConfig(root, overrides = {}) {
628
+ const defaults = {
629
+ root: pathToFileURL(`${path.resolve(root)}${path.sep}`),
630
+ srcDir: pathToFileURL(`${path.resolve(root, "src")}${path.sep}`),
631
+ publicDir: pathToFileURL(`${path.resolve(root, "public")}${path.sep}`),
632
+ outDir: pathToFileURL(`${path.resolve(root, "dist")}${path.sep}`),
633
+ cacheDir: pathToFileURL(`${path.resolve(root, "node_modules/.astro")}${path.sep}`),
634
+ base: "/",
635
+ site: void 0,
636
+ output: "static",
637
+ ...overrides
638
+ };
639
+ return new Proxy(defaults, { get(target, property, receiver) {
640
+ if (typeof property === "string" && !(property in target)) throw new Error(`Content loader requires config.${property}; provide loaderConfig.${property} in content-query.config.ts.`);
641
+ return Reflect.get(target, property, receiver);
642
+ } });
643
+ }
644
+ /** Creates the public Astro logger shape without importing Astro's framework runtime. */
645
+ function loaderLogger(label, options = {
646
+ level: "info",
647
+ destination: { write: ({ label: name, level, message }) => {
648
+ if (level !== "silent") console[level](`[${name}] ${message}`);
649
+ } }
650
+ }) {
651
+ /** Routes messages through the shared logger options and configured level. */
652
+ function emit(level, message) {
653
+ const ranks = {
654
+ debug: 0,
655
+ info: 1,
656
+ warn: 2,
657
+ error: 3,
658
+ silent: 4
659
+ };
660
+ if (ranks[level] >= ranks[options.level]) options.destination.write({
661
+ label,
662
+ level,
663
+ message,
664
+ newLine: true
665
+ });
666
+ }
667
+ return {
668
+ label,
669
+ options,
670
+ fork: (name) => loaderLogger(name, options),
671
+ info: (message) => emit("info", message),
672
+ warn: (message) => emit("warn", message),
673
+ error: (message) => emit("error", message),
674
+ debug: (message) => emit("debug", message),
675
+ flush: () => {
676
+ options.destination.flush?.();
677
+ },
678
+ close: () => {
679
+ options.destination.close?.();
680
+ }
681
+ };
682
+ }
683
+ //#endregion
684
+ //#region src/preparation/loaders/store.ts
685
+ /** Creates an isolated native loader store, retaining validation identity across defensive copies. */
686
+ function createStores(current, parsed, changed) {
687
+ /** Copies entries without exposing mutable preparation state to loader code. */
688
+ function copy(entry) {
689
+ const copied = detach(entry);
690
+ if (parsed.has(entry.data)) parsed.set(copied.data, parsed.get(entry.data));
691
+ return copied;
692
+ }
693
+ return {
694
+ store: {
695
+ get(key) {
696
+ const entry = current().entries.get(String(key));
697
+ return entry ? copy(entry) : void 0;
698
+ },
699
+ entries: () => [...current().entries].map(([key, entry]) => [key, copy(entry)]),
700
+ values: () => [...current().entries.values()].map(copy),
701
+ keys: () => [...current().entries.keys()],
702
+ has: (key) => current().entries.has(String(key)),
703
+ set(entry) {
704
+ if (entry.deferredRender || entry.assetImports?.length || entry.imageImports?.length || entry.rendered?.metadata?.imagePaths?.length) throw new Error("Astro loader module and image imports require Astro's rendering pipeline; supply portable HTML or use a ContentQuery loader.");
705
+ const id = entry.id;
706
+ if (typeof id !== "string" || !id || !entry.data || typeof entry.data !== "object" || Array.isArray(entry.data)) throw new Error("Astro store entries require a nonempty id and record data.");
707
+ const previous = current().entries.get(id);
708
+ if (entry.digest !== void 0 && previous?.digest === entry.digest) return false;
709
+ current().entries.set(id, copy({
710
+ ...entry,
711
+ id
712
+ }));
713
+ changed();
714
+ return true;
715
+ },
716
+ delete(key) {
717
+ if (current().entries.delete(String(key))) changed();
718
+ },
719
+ clear() {
720
+ if (current().entries.size) {
721
+ current().entries.clear();
722
+ changed();
723
+ }
724
+ },
725
+ addModuleImport() {
726
+ throw new Error("Astro loader module imports require Astro's rendering pipeline.");
727
+ }
728
+ },
729
+ meta: {
730
+ get: (key) => current().metadata.get(key),
731
+ has: (key) => current().metadata.has(key),
732
+ set: (key, value) => {
733
+ current().metadata.set(key, value);
734
+ },
735
+ delete: (key) => {
736
+ current().metadata.delete(key);
737
+ }
738
+ }
739
+ };
740
+ }
741
+ //#endregion
742
+ //#region src/preparation/loaders/watcher.ts
743
+ /** Guards loader-owned watcher callbacks and exposes cleanup for abandoned load attempts. */
744
+ function guardedWatcher(watcher, run, failed) {
745
+ const listeners = [];
746
+ let running = Promise.resolve();
747
+ const registration = /* @__PURE__ */ new Set([
748
+ "on",
749
+ "once",
750
+ "addListener",
751
+ "prependListener",
752
+ "prependOnceListener"
753
+ ]);
754
+ const facade = new Proxy(watcher, { get(target, property, receiver) {
755
+ if (typeof property === "string" && registration.has(property)) return (event, original) => {
756
+ const wrapped = (...arguments_) => {
757
+ running = running.then(() => run(() => Reflect.apply(original, facade, arguments_))).catch(failed);
758
+ };
759
+ listeners.push({
760
+ event,
761
+ original,
762
+ wrapped
763
+ });
764
+ target[property](event, wrapped);
765
+ return facade;
766
+ };
767
+ if (property === "off" || property === "removeListener" || property === "removeAllListeners") return (event, original) => {
768
+ for (let index = listeners.length - 1; index >= 0; index--) {
769
+ const listener = listeners[index];
770
+ if ((event === void 0 || listener.event === event) && (original === void 0 || listener.original === original)) {
771
+ target.off(listener.event, listener.wrapped);
772
+ listeners.splice(index, 1);
773
+ }
774
+ }
775
+ return facade;
776
+ };
777
+ const value = Reflect.get(target, property, receiver);
778
+ return typeof value === "function" ? (...arguments_) => {
779
+ const result = Reflect.apply(value, target, arguments_);
780
+ return result === target ? facade : result;
781
+ } : value;
782
+ } });
783
+ return {
784
+ watcher: facade,
785
+ cleanup() {
786
+ for (const { event, wrapped } of listeners) watcher.off(event, wrapped);
787
+ listeners.length = 0;
788
+ }
789
+ };
790
+ }
791
+ //#endregion
792
+ //#region src/preparation/loaders/session.ts
793
+ /** Narrows schema providers without coupling native ContentQuery definitions to Zod. */
794
+ function standardSchema(value) {
795
+ return !!value && typeof value === "object" && "~standard" in value && isRecord(value["~standard"]) && typeof value["~standard"].validate === "function";
796
+ }
797
+ /** Owns a collection's mutable loader store and converts completed loads into immutable sources. */
798
+ function createLoaderSession(source, options) {
799
+ const parsed = /* @__PURE__ */ new WeakMap();
800
+ const state = {
801
+ root: path.resolve(options.root),
802
+ lifetime: options.signal,
803
+ watcher: options.watcher,
804
+ entries: /* @__PURE__ */ new Map(),
805
+ metadata: /* @__PURE__ */ new Map(),
806
+ changed: false,
807
+ queued: false,
808
+ initialized: false,
809
+ failedInitialLoad: false,
810
+ guards: /* @__PURE__ */ new Set()
811
+ };
812
+ let declaredSchema;
813
+ const schema = options.schema ?? { "~standard": {
814
+ version: 1,
815
+ vendor: "content-query",
816
+ async validate(value) {
817
+ declaredSchema ??= Promise.resolve().then(async () => {
818
+ const authored = "schema" in source ? source.schema : void 0;
819
+ if (authored) return typeof authored === "function" ? authored() : authored;
820
+ return "createSchema" in source ? (await source.createSchema?.())?.schema : void 0;
821
+ }).catch((cause) => {
822
+ declaredSchema = void 0;
823
+ throw cause;
824
+ });
825
+ const provider = await declaredSchema;
826
+ if (!provider) return { value };
827
+ if (standardSchema(provider)) {
828
+ const result = await provider["~standard"].validate(value);
829
+ return result.issues ? { issues: result.issues } : isRecord(result.value) ? { value: result.value } : { issues: [{ message: "Astro loader schemas must produce a record of named fields." }] };
830
+ }
831
+ const legacy = provider && typeof provider === "object" && "safeParseAsync" in provider ? provider.safeParseAsync : void 0;
832
+ const result = typeof legacy === "function" ? await legacy.call(provider, value) : await (async () => {
833
+ return (await import("zod/v4/core")).safeParseAsync(provider, value);
834
+ })();
835
+ return result.success ? { value: result.data } : { issues: result.error.issues };
836
+ }
837
+ } };
838
+ /** Converts the private store into source-independent ContentQuery entries. */
839
+ function snapshot(state) {
840
+ return [...state.entries.values()].map((entry) => ({
841
+ id: entry.id,
842
+ data: entry.data,
843
+ ...entry.body === void 0 ? {} : { body: entry.body },
844
+ ...entry.rendered === void 0 ? {} : { rendered: entry.rendered },
845
+ ...entry.filePath === void 0 ? {} : { source: path.resolve(state.root, entry.filePath) },
846
+ ...entry.source === void 0 ? {} : { sourceData: entry.source }
847
+ }));
848
+ }
849
+ /** Coalesces synchronous store batches into a complete validated publication. */
850
+ function changed(state) {
851
+ state.changed = true;
852
+ if (!state.queued) {
853
+ state.queued = true;
854
+ queueMicrotask(() => {
855
+ state.queued = false;
856
+ if (!state.lifetime.aborted) state.subscription?.invalidate();
857
+ });
858
+ }
859
+ }
860
+ return {
861
+ schema,
862
+ parsed,
863
+ async load(context) {
864
+ context.signal.throwIfAborted();
865
+ if (state.changed) {
866
+ state.changed = false;
867
+ return snapshot(state);
868
+ }
869
+ const candidate = {
870
+ entries: new Map(state.entries),
871
+ metadata: new Map(state.metadata)
872
+ };
873
+ let committed = false;
874
+ let retired = false;
875
+ let batch;
876
+ let batchChanged = false;
877
+ const pending = /* @__PURE__ */ new Set();
878
+ let parseFailure;
879
+ const effectiveSchema = schema;
880
+ const stores = createStores(() => batch ?? (committed ? state : candidate), parsed, () => {
881
+ if (batch) batchChanged = true;
882
+ else if (committed && !retired && !state.lifetime.aborted) changed(state);
883
+ });
884
+ /** Awaits parsing work even when a source uses unawaited stream callbacks. */
885
+ async function settled() {
886
+ while (pending.size) await Promise.all(pending);
887
+ if (parseFailure) {
888
+ const { cause } = parseFailure;
889
+ parseFailure = void 0;
890
+ throw cause;
891
+ }
892
+ }
893
+ const guarded = state.watcher ? guardedWatcher(state.watcher, async (callback) => {
894
+ if (retired || state.lifetime.aborted) return;
895
+ const current = committed ? state : candidate;
896
+ batch = {
897
+ entries: new Map(current.entries),
898
+ metadata: new Map(current.metadata)
899
+ };
900
+ batchChanged = false;
901
+ parseFailure = void 0;
902
+ try {
903
+ await callback();
904
+ await settled();
905
+ state.lifetime.throwIfAborted();
906
+ current.entries = batch.entries;
907
+ current.metadata = batch.metadata;
908
+ if (committed && batchChanged) changed(state);
909
+ } catch (cause) {
910
+ await Promise.allSettled(pending);
911
+ parseFailure = void 0;
912
+ throw cause;
913
+ } finally {
914
+ batch = void 0;
915
+ }
916
+ }, (cause) => {
917
+ if (!state.lifetime.aborted) state.subscription?.error(cause);
918
+ }) : void 0;
919
+ const loaderContext = {
920
+ ...stores,
921
+ collection: options.collection,
922
+ config: loaderConfig(state.root, options.config),
923
+ logger: loaderLogger(source.name),
924
+ signal: context.signal,
925
+ watcher: guarded?.watcher,
926
+ generateDigest: (data) => createHash("sha256").update(encode(data)).digest("hex"),
927
+ async renderMarkdown(body) {
928
+ const { html, headings } = await renderMarkdown(body);
929
+ return {
930
+ html,
931
+ metadata: { headings }
932
+ };
933
+ },
934
+ parseData({ id, data, filePath }) {
935
+ const operation = (async () => {
936
+ state.lifetime.throwIfAborted();
937
+ const result = await validate(effectiveSchema, data, {
938
+ operation: "build",
939
+ collection: options.collection,
940
+ id,
941
+ source: filePath === void 0 ? void 0 : path.resolve(state.root, filePath)
942
+ });
943
+ if (!isRecord(result)) throw new Error("Validated Astro loader metadata must be a record of named fields.");
944
+ let copied;
945
+ try {
946
+ copied = detach(result);
947
+ } catch (cause) {
948
+ const formatted = cause && typeof cause === "object" && "path" in cause && typeof cause.path === "string" ? cause.path : "";
949
+ throw contentError(cause, {
950
+ code: "validation-error",
951
+ operation: "build",
952
+ collection: options.collection,
953
+ id,
954
+ source: filePath === void 0 ? void 0 : path.resolve(state.root, filePath),
955
+ path: formatted ? valuePath(formatted) : void 0,
956
+ message: formatted ? `${cause instanceof Error ? cause.message : "Unsupported parsed metadata."} (at ${formatted})` : cause instanceof Error ? cause.message : "Unsupported parsed metadata."
957
+ });
958
+ }
959
+ parsed.set(copied, effectiveSchema);
960
+ return copied;
961
+ })();
962
+ pending.add(operation);
963
+ operation.then(() => pending.delete(operation), (cause) => {
964
+ pending.delete(operation);
965
+ parseFailure = { cause };
966
+ if (committed && !batch && !retired) state.subscription?.error(cause);
967
+ });
968
+ return operation;
969
+ }
970
+ };
971
+ try {
972
+ state.failedInitialLoad = false;
973
+ await source.load(loaderContext);
974
+ await settled();
975
+ context.signal.throwIfAborted();
976
+ state.entries = candidate.entries;
977
+ state.metadata = candidate.metadata;
978
+ committed = true;
979
+ state.initialized = true;
980
+ if (guarded) state.guards.add(() => guarded.cleanup());
981
+ return snapshot(state);
982
+ } catch (cause) {
983
+ retired = true;
984
+ if (!state.initialized && !context.signal.aborted) state.failedInitialLoad = true;
985
+ guarded?.cleanup();
986
+ throw cause;
987
+ }
988
+ },
989
+ async watch(context) {
990
+ state.subscription = context;
991
+ /** Retries failed startup loads whose source code could not install its own listener yet. */
992
+ function retryInitialLoad() {
993
+ if (state.failedInitialLoad && !state.lifetime.aborted) context.invalidate();
994
+ }
995
+ state.watcher?.on("all", retryInitialLoad);
996
+ return () => {
997
+ state.subscription = void 0;
998
+ state.watcher?.off("all", retryInitialLoad);
999
+ for (const cleanup of state.guards) cleanup();
1000
+ state.guards.clear();
1001
+ };
1002
+ }
1003
+ };
1004
+ }
1005
+ //#endregion
1006
+ //#region src/preparation/session.ts
1007
+ const internals = /* @__PURE__ */ new WeakMap();
1008
+ /** Gives integrations private lifecycle access without expanding the application session API. */
1009
+ function sessionInternals(session) {
1010
+ const internal = internals.get(session);
1011
+ if (!internal) throw new Error("Unknown ContentQuery session.");
1012
+ return internal;
1013
+ }
1014
+ /** Creates a preparation session with ordered source commands and atomic revision publication. */
1015
+ async function createSession(options) {
1016
+ options = {
1017
+ ...options,
1018
+ root: path.resolve(options.root),
1019
+ config: path.resolve(options.root, options.config),
1020
+ outDir: path.resolve(options.root, options.outDir)
1021
+ };
1022
+ const state = {};
1023
+ let registry = {};
1024
+ let loaders = /* @__PURE__ */ new Map();
1025
+ let configDependencies;
1026
+ let configurationError;
1027
+ const candidates = /* @__PURE__ */ new Map();
1028
+ const cache = /* @__PURE__ */ new Map();
1029
+ let cleanups = [];
1030
+ let subscriptions = new AbortController();
1031
+ let attempt;
1032
+ let watcher;
1033
+ let disposed = false;
1034
+ let initializing = true;
1035
+ let running;
1036
+ let commands = [];
1037
+ const stale = /* @__PURE__ */ new Set();
1038
+ let loading = /* @__PURE__ */ new Set();
1039
+ let generation = 0;
1040
+ const loadFailures = /* @__PURE__ */ new Map();
1041
+ const watchOptions = {
1042
+ root: options.root,
1043
+ config: options.config,
1044
+ output: options.publish ? options.outDir : void 0,
1045
+ registry: () => registry,
1046
+ dependencies: () => configDependencies,
1047
+ configChanged: () => enqueue({ kind: "config" }),
1048
+ invalidated: (collection) => enqueue({
1049
+ kind: "invalidate",
1050
+ collection
1051
+ }),
1052
+ failed: (cause) => enqueue({
1053
+ kind: "error",
1054
+ cause
1055
+ })
1056
+ };
1057
+ /** Queues ordered notifications and makes any in-flight preparation obsolete. */
1058
+ function enqueue(command) {
1059
+ if (disposed) return;
1060
+ for (const name of loading) if (!commands.some((queued) => queued.kind === "invalidate" && queued.collection === name)) commands.push({
1061
+ kind: "invalidate",
1062
+ collection: name
1063
+ });
1064
+ commands.push(command);
1065
+ generation++;
1066
+ attempt?.abort();
1067
+ if (!running && !initializing) running = Promise.resolve().then(drain).finally(() => {
1068
+ running = void 0;
1069
+ if (commands.length && !disposed) enqueueDrain();
1070
+ });
1071
+ }
1072
+ /** Restarts draining without inventing another source notification. */
1073
+ function enqueueDrain() {
1074
+ if (!running && !disposed) running = Promise.resolve().then(drain).finally(() => {
1075
+ running = void 0;
1076
+ if (commands.length && !disposed) enqueueDrain();
1077
+ });
1078
+ }
1079
+ /** Reports current preparation failures while keeping recovery subscriptions alive. */
1080
+ function fail(cause) {
1081
+ state.error = contentError(cause, {
1082
+ code: "source-error",
1083
+ operation: "build",
1084
+ message: cause instanceof Error ? cause.message : "Content preparation failed."
1085
+ });
1086
+ options.change?.(state);
1087
+ }
1088
+ /** Aborts an owned subscription group and attempts every cleanup even when one fails. */
1089
+ async function releaseResources(controller, handlers) {
1090
+ controller.abort();
1091
+ const failure = (await Promise.allSettled(handlers.map(async (cleanup) => cleanup()))).find((result) => result.status === "rejected");
1092
+ if (failure?.status === "rejected") throw failure.reason;
1093
+ }
1094
+ /** Releases the active configuration independently from load-attempt cancellation. */
1095
+ async function releaseSubscriptions() {
1096
+ const previous = cleanups;
1097
+ cleanups = [];
1098
+ await releaseResources(subscriptions, previous);
1099
+ }
1100
+ /** Stages all replacement subscriptions before adopting a valid configuration. */
1101
+ async function configure(initial) {
1102
+ configDependencies = void 0;
1103
+ const loaded = options.registry ? {
1104
+ registry: options.registry,
1105
+ dependencies: /* @__PURE__ */ new Set(),
1106
+ loaderConfig: void 0
1107
+ } : await loadRegistry(options.config);
1108
+ const nextSubscriptions = new AbortController();
1109
+ const nextCleanups = [];
1110
+ const nextLoaders = /* @__PURE__ */ new Map();
1111
+ const nextRegistry = Object.create(null);
1112
+ const notifications = [];
1113
+ let installed = false;
1114
+ /** Retains setup notifications and ignores callbacks from aborted configurations. */
1115
+ function receive(command) {
1116
+ if (nextSubscriptions.signal.aborted) return;
1117
+ if (installed) enqueue(command);
1118
+ else notifications.push(command);
1119
+ }
1120
+ try {
1121
+ for (const [name, definition] of Object.entries(loaded.registry)) {
1122
+ const loader = createLoaderSession(definition.loader, {
1123
+ root: options.root,
1124
+ config: loaded.loaderConfig,
1125
+ collection: name,
1126
+ schema: definition.schema,
1127
+ signal: nextSubscriptions.signal,
1128
+ watcher
1129
+ });
1130
+ nextLoaders.set(name, loader);
1131
+ nextRegistry[name] = {
1132
+ ...definition,
1133
+ schema: loader.schema
1134
+ };
1135
+ if (options.watch) {
1136
+ const cleanup = await loader.watch({
1137
+ signal: nextSubscriptions.signal,
1138
+ invalidate: () => receive({
1139
+ kind: "invalidate",
1140
+ collection: name
1141
+ }),
1142
+ error: (cause) => receive({
1143
+ kind: "error",
1144
+ cause
1145
+ })
1146
+ });
1147
+ if (typeof cleanup !== "function") throw new Error(`Loader watch() for "${name}" must return a cleanup function.`);
1148
+ nextCleanups.push(cleanup);
1149
+ }
1150
+ }
1151
+ if (watcher) await watchSources({
1152
+ ...watchOptions,
1153
+ projectWatcher: watcher,
1154
+ registry: () => loaded.registry,
1155
+ dependencies: () => loaded.dependencies,
1156
+ configChanged: () => receive({ kind: "config" }),
1157
+ invalidated: (collection) => receive({
1158
+ kind: "invalidate",
1159
+ collection
1160
+ }),
1161
+ failed: (cause) => receive({
1162
+ kind: "error",
1163
+ cause
1164
+ })
1165
+ }, (cleanup) => nextCleanups.push(cleanup));
1166
+ if (disposed) throw new Error("Content session was disposed during configuration setup.");
1167
+ if (!initial) await releaseSubscriptions();
1168
+ } catch (cause) {
1169
+ const failure = contentError(cause, {
1170
+ code: "source-error",
1171
+ operation: "build",
1172
+ source: options.config,
1173
+ message: cause instanceof Error ? cause.message : "Configuration setup failed."
1174
+ });
1175
+ try {
1176
+ await releaseResources(nextSubscriptions, nextCleanups);
1177
+ } catch (cleanup) {
1178
+ throw groupDiagnostics([...failure.diagnostics, ...contentError(cleanup, {
1179
+ code: "source-error",
1180
+ operation: "dispose",
1181
+ message: "Subscription cleanup failed."
1182
+ }).diagnostics]);
1183
+ }
1184
+ throw failure;
1185
+ }
1186
+ subscriptions = nextSubscriptions;
1187
+ cleanups = nextCleanups;
1188
+ registry = nextRegistry;
1189
+ loaders = nextLoaders;
1190
+ configDependencies = loaded.dependencies;
1191
+ state.registry = queryRegistry(registry);
1192
+ candidates.clear();
1193
+ cache.clear();
1194
+ loadFailures.clear();
1195
+ configurationError = void 0;
1196
+ installed = true;
1197
+ for (const notification of notifications) enqueue(notification);
1198
+ }
1199
+ /** Acquires invalidated snapshots and publishes only the complete current attempt. */
1200
+ async function prepare(names) {
1201
+ const currentGeneration = generation;
1202
+ attempt = new AbortController();
1203
+ const signal = attempt.signal;
1204
+ loading = names;
1205
+ const acquired = await Promise.allSettled([...names].map(async (name) => {
1206
+ const entries = await loaders.get(name).load({ signal });
1207
+ return [name, normalizeSnapshot(entries, name)];
1208
+ }));
1209
+ if (disposed || currentGeneration !== generation) return;
1210
+ loading = /* @__PURE__ */ new Set();
1211
+ acquired.forEach((result, index) => {
1212
+ const name = [...names][index];
1213
+ if (result.status === "fulfilled") {
1214
+ candidates.set(...result.value);
1215
+ loadFailures.delete(name);
1216
+ } else loadFailures.set(name, withContext(result.reason, {
1217
+ code: "source-error",
1218
+ operation: "build",
1219
+ collection: name,
1220
+ message: `Could not load collection "${name}"`
1221
+ }));
1222
+ });
1223
+ try {
1224
+ const failures = [...loadFailures.values()];
1225
+ const revision = await prepareRevision(registry, candidates, cache, signal, options.watch, failures.flatMap((error) => error.diagnostics), new Map([...loaders].map(([name, loader]) => [name, loader.parsed])));
1226
+ if (disposed || currentGeneration !== generation) return;
1227
+ await options.publish?.(revision, registry, signal);
1228
+ if (disposed || currentGeneration !== generation) return;
1229
+ state.revision = revision;
1230
+ state.error = void 0;
1231
+ options.change?.(state);
1232
+ } catch (cause) {
1233
+ if (!disposed && currentGeneration === generation) fail(cause);
1234
+ }
1235
+ }
1236
+ /** Lets a newer attempt progress even when obsolete user code ignores its signal. */
1237
+ async function runAttempt(names) {
1238
+ const work = prepare(names);
1239
+ const signal = attempt.signal;
1240
+ if (signal.aborted) return;
1241
+ let finish;
1242
+ const aborted = new Promise((resolve) => {
1243
+ finish = () => resolve();
1244
+ signal.addEventListener("abort", finish, { once: true });
1245
+ });
1246
+ try {
1247
+ await Promise.race([work, aborted]);
1248
+ } finally {
1249
+ if (finish) signal.removeEventListener("abort", finish);
1250
+ }
1251
+ }
1252
+ /** Applies queued source state separately from publishing validated content. */
1253
+ async function drain() {
1254
+ while (commands.length && !disposed) {
1255
+ const pending = commands;
1256
+ commands = [];
1257
+ let failedCommand = false;
1258
+ for (const command of pending) try {
1259
+ if (command.kind === "error") throw command.cause;
1260
+ if (command.kind === "config") {
1261
+ await configure(false);
1262
+ for (const name of Object.keys(registry)) stale.add(name);
1263
+ } else if (command.kind === "invalidate") stale.add(command.collection);
1264
+ } catch (cause) {
1265
+ if (command.kind === "config") configurationError = contentError(cause, {
1266
+ code: "source-error",
1267
+ operation: "build",
1268
+ source: options.config,
1269
+ message: cause instanceof Error ? cause.message : "Configuration reload failed."
1270
+ });
1271
+ fail(configurationError ?? cause);
1272
+ failedCommand = true;
1273
+ }
1274
+ if (!failedCommand && !configurationError) {
1275
+ const names = new Set([...stale].filter((name) => Object.hasOwn(registry, name)));
1276
+ stale.clear();
1277
+ await runAttempt(names);
1278
+ }
1279
+ }
1280
+ }
1281
+ /** Idempotently stops watchers and prevents obsolete attempts from publishing. */
1282
+ async function dispose() {
1283
+ if (disposed) return;
1284
+ disposed = true;
1285
+ generation++;
1286
+ attempt?.abort();
1287
+ const failed = (await Promise.allSettled([watcher?.close() ?? Promise.resolve(), releaseSubscriptions()])).find((result) => result.status === "rejected");
1288
+ if (failed?.status === "rejected") throw contentError(failed.reason, {
1289
+ code: "source-error",
1290
+ operation: "dispose",
1291
+ message: "A content subscription could not be cleaned up."
1292
+ });
1293
+ }
1294
+ const session = {
1295
+ content: createRuntime(state),
1296
+ dispose
1297
+ };
1298
+ internals.set(session, {
1299
+ state,
1300
+ async idle() {
1301
+ await Promise.resolve();
1302
+ while (running) await running;
1303
+ },
1304
+ invalidate() {
1305
+ enqueue({ kind: "config" });
1306
+ }
1307
+ });
1308
+ try {
1309
+ if (options.watch) await watchProject(watchOptions, (created) => {
1310
+ watcher = created;
1311
+ });
1312
+ await configure(true);
1313
+ await runAttempt(new Set(Object.keys(registry)));
1314
+ initializing = false;
1315
+ if (commands.length) {
1316
+ enqueueDrain();
1317
+ while (running) await running;
1318
+ }
1319
+ if (state.error && !options.watch) throw state.error;
1320
+ return session;
1321
+ } catch (cause) {
1322
+ const failure = contentError(cause, {
1323
+ code: "source-error",
1324
+ operation: "build",
1325
+ message: "Content configuration or subscription initialization failed."
1326
+ });
1327
+ try {
1328
+ await dispose();
1329
+ } catch (cleanup) {
1330
+ throw new ContentError({
1331
+ ...failure.diagnostics[0],
1332
+ message: failure.message,
1333
+ diagnostics: [...failure.diagnostics, ...contentError(cleanup, {
1334
+ code: "source-error",
1335
+ operation: "dispose",
1336
+ message: "Subscription cleanup failed."
1337
+ }).diagnostics]
1338
+ });
1339
+ }
1340
+ throw failure;
1341
+ }
1342
+ }
1343
+ //#endregion
1344
+ //#region src/preparation/config-path.ts
1345
+ const extensions = [
1346
+ "ts",
1347
+ "mts",
1348
+ "cts",
1349
+ "js",
1350
+ "mjs",
1351
+ "cjs"
1352
+ ];
1353
+ /** Lists default config paths, or the exact explicitly selected file, from the host root. */
1354
+ function configCandidates(root, config) {
1355
+ return config === void 0 ? extensions.map((extension) => path.resolve(root, `content-query.config.${extension}`)) : [path.resolve(root, config)];
1356
+ }
1357
+ /** Discovers exactly one config file without evaluating candidates or guessing explicit paths. */
1358
+ async function resolveConfigPath(root, config) {
1359
+ const candidates = configCandidates(root, config);
1360
+ if (config !== void 0) return candidates[0];
1361
+ const files = await Promise.all(candidates.map(async (filename) => {
1362
+ try {
1363
+ return (await stat(filename)).isFile();
1364
+ } catch (cause) {
1365
+ if (cause && typeof cause === "object" && "code" in cause && (cause.code === "ENOENT" || cause.code === "ENOTDIR")) return false;
1366
+ throw contentError(cause, {
1367
+ code: "source-error",
1368
+ operation: "build",
1369
+ source: filename,
1370
+ message: `Could not inspect ContentQuery configuration "${filename}".`
1371
+ });
1372
+ }
1373
+ }));
1374
+ const matches = candidates.filter((_filename, index) => files[index]);
1375
+ if (matches.length === 1) return matches[0];
1376
+ throw new ContentError({
1377
+ code: "source-error",
1378
+ operation: "build",
1379
+ source: path.resolve(root),
1380
+ message: matches.length ? `Multiple ContentQuery configuration files found: ${matches.map((filename) => path.basename(filename)).join(", ")}. Set the config option to choose one.` : `No ContentQuery configuration found in "${path.resolve(root)}". Expected content-query.config.{${extensions.join(",")}}, or set the config option to an exact path.`
1381
+ });
1382
+ }
1383
+ //#endregion
1384
+ //#region src/preparation/build.ts
1385
+ assertServerOnly();
1386
+ /**
1387
+ * Builds content independently of a bundler; bind the registry through an erased type import.
1388
+ * Generated watch sessions prune previous-run artifacts after their first successful publication.
1389
+ */
1390
+ async function buildContent(...arguments_) {
1391
+ return buildAtRoot(process.cwd(), arguments_[0] ?? {});
1392
+ }
1393
+ /** Resolves host paths and prunes previous-run artifacts on the first successful publication. */
1394
+ async function buildAtRoot(root, options, change) {
1395
+ const { backend = "generated", watch = false } = options;
1396
+ if (!["generated", "in-memory"].includes(backend) || typeof watch !== "boolean" || options.config !== void 0 && typeof options.config !== "string" || options.outDir !== void 0 && typeof options.outDir !== "string") throw new ContentError({
1397
+ code: "source-error",
1398
+ message: "Invalid build options."
1399
+ });
1400
+ const config = await resolveConfigPath(root, options.config);
1401
+ const outDir = path.resolve(root, options.outDir ?? "./.content-query");
1402
+ if (backend === "generated") {
1403
+ const [physicalRoot, physicalOutput, physicalConfig] = await Promise.all([
1404
+ canonicalPath(path.resolve(root)),
1405
+ canonicalPath(outDir),
1406
+ canonicalPath(config)
1407
+ ]);
1408
+ if (withinDirectory(physicalOutput, physicalRoot) || withinDirectory(physicalOutput, physicalConfig)) throw new ContentError({
1409
+ code: "source-error",
1410
+ operation: "build",
1411
+ message: "Generated output must be a dedicated directory outside collection configuration and cannot contain the application root."
1412
+ });
1413
+ }
1414
+ let firstPublication = true;
1415
+ let publication = Promise.resolve();
1416
+ /** Keeps startup pruning from racing an artifact write from a newer content attempt. */
1417
+ function publish(revision, _registry, signal) {
1418
+ const pending = publication.catch(() => {}).then(async () => {
1419
+ signal.throwIfAborted();
1420
+ await writeArtifacts(revision, outDir, config, {
1421
+ signal,
1422
+ prune: !watch || firstPublication
1423
+ });
1424
+ firstPublication = false;
1425
+ });
1426
+ publication = pending;
1427
+ return pending;
1428
+ }
1429
+ return createSession({
1430
+ root,
1431
+ config,
1432
+ outDir,
1433
+ watch,
1434
+ change,
1435
+ publish: backend === "generated" ? publish : void 0
1436
+ });
1437
+ }
1438
+ //#endregion
1439
+ export { sessionInternals as a, contentDeclarations as c, resolveConfigPath as i, virtualDeclarations as l, buildContent as n, withinDirectory as o, configCandidates as r, writeChanged as s, buildAtRoot as t, revisionModules as u };