content-query 0.3.0 → 0.5.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.
- package/README.md +46 -15
- package/dist/{build-CI4s0oFP.mjs → build-DWpB69_c.mjs} +826 -69
- package/dist/{build-jKnjDjQE.d.mts → build-DfoncKL8.d.mts} +17 -5
- package/dist/build.d.mts +1 -1
- package/dist/build.mjs +1 -1
- package/dist/cli.mjs +14 -8
- package/dist/{content-N1gYshjg.mjs → collection-Cb5eWGnv.mjs} +998 -344
- package/dist/collection-types-CsQtGw7_.d.mts +119 -0
- package/dist/computed.d.mts +2 -2
- package/dist/{types-FmUJ3cH1.d.mts → descriptor-bcPa6XLk.d.mts} +17 -1
- package/dist/{diagnostics-Cu94mZhl.mjs → diagnostics-DmUZtrJ6.mjs} +3 -1
- package/dist/{errors-C2iuBM0G.mjs → errors-Bfd0R4_P.mjs} +16 -1
- package/dist/file-DSOcdFj0.mjs +81 -0
- package/dist/{glob-Bv0dpvCQ.mjs → glob-BXJuwy_p.mjs} +94 -6
- package/dist/index.d.mts +76 -18
- package/dist/index.mjs +1 -1
- package/dist/internal.d.mts +54 -7
- package/dist/internal.mjs +424 -19
- package/dist/loaders.d.mts +1 -2
- package/dist/loaders.mjs +2 -81
- package/dist/next.d.mts +3 -2
- package/dist/next.mjs +67 -14
- package/dist/operators-D-w1-gDg.mjs +391 -0
- package/dist/query-style-C2FcT1f9.d.mts +23 -0
- package/dist/query.d.mts +25 -2
- package/dist/query.mjs +1 -1
- package/dist/{relational-types-B45zM34W.d.mts → relational-types-CTcalUNW.d.mts} +241 -50
- package/dist/revision-DBwU-aC9.d.mts +56 -0
- package/dist/runtime.d.mts +3 -2
- package/dist/runtime.mjs +1 -1
- package/dist/types-Dz1vTXa2.d.mts +214 -0
- package/dist/vite.d.mts +1 -1
- package/dist/vite.mjs +77 -35
- package/package.json +1 -1
- package/dist/content-lwPxE9Yu.d.mts +0 -109
- package/dist/descriptor-BUPT2J7X.d.mts +0 -18
- package/dist/operators-UqWUhKQJ.mjs +0 -173
- package/dist/types-BC8gVJ6u.d.mts +0 -69
- package/dist/validation-D_XdE1pF.mjs +0 -84
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as groupDiagnostics, n as contentError, o as withContext, t as ContentError } from "./errors-
|
|
1
|
+
import { c as normalizeSnapshot, d as builtinLoader, f as builtinPath, l as renderedPayload, n as loadGlob, s as isRecord, u as validate } from "./glob-BXJuwy_p.mjs";
|
|
2
|
+
import { i as groupDiagnostics, n as contentError, o as payloadIdentity, r as describeCause, s as withContext, t as ContentError } from "./errors-Bfd0R4_P.mjs";
|
|
3
3
|
import { i as detach, l as valuePath, o as encode } from "./values-BbTV4QT-.mjs";
|
|
4
4
|
import { t as assertServerOnly } from "./server-only-CIJUlyL-.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { n as loadFile } from "./file-DSOcdFj0.mjs";
|
|
6
|
+
import { I as collectionNames } from "./operators-D-w1-gDg.mjs";
|
|
7
|
+
import { a as variantKey, c as CODEC_VERSION, i as isQueryStyle, n as createCollections, o as isRelationship, s as prepareRelationships } from "./collection-Cb5eWGnv.mjs";
|
|
6
8
|
import { t as renderMarkdown } from "./markdown-CHvBBJNa.mjs";
|
|
7
9
|
import { access, link, mkdir, readFile, readdir, realpath, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
8
10
|
import path from "node:path";
|
|
@@ -18,7 +20,13 @@ import picomatch from "picomatch";
|
|
|
18
20
|
function digest(value) {
|
|
19
21
|
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
20
22
|
}
|
|
21
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Maps collection and payload names to distinct safe directory names without changing public
|
|
25
|
+
* identities. Names that are not portable directory names are base64url-encoded behind `~`.
|
|
26
|
+
*
|
|
27
|
+
* @param name - The collection or payload name.
|
|
28
|
+
* @returns The directory name under `collections/` or a collection's `payloads/`.
|
|
29
|
+
*/
|
|
22
30
|
function directoryName(name) {
|
|
23
31
|
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
32
|
}
|
|
@@ -29,19 +37,43 @@ function compactJson(source) {
|
|
|
29
37
|
const compressed = JSON.stringify(gzipSync(source, { level: 1 }).toString("base64"));
|
|
30
38
|
return compressed.length < bytes * .9 ? compressed : void 0;
|
|
31
39
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
40
|
+
/** Picks a module-local binding that cannot shadow the entry's export name. */
|
|
41
|
+
function binding(base, exportName) {
|
|
42
|
+
return base === exportName ? `${base}_` : base;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates one collection's entry, which exports the collection's query handle under its export
|
|
46
|
+
* name and imports only that collection's modules.
|
|
47
|
+
*/
|
|
48
|
+
function collectionEntry$1(manifest, name, exportName, index, renderer) {
|
|
49
|
+
const runtime = binding("runtime", exportName);
|
|
50
|
+
const data = binding("data", exportName);
|
|
51
|
+
const render = renderer ? `, body => import("content-query/internal/markdown").then(module => module.renderMarkdown(body))` : "";
|
|
52
|
+
return `import * as ${runtime} from "content-query/internal";\nimport ${data} from ${JSON.stringify(`./${index}`)};\nexport const ${exportName} = ${runtime}.createArtifactCollection(JSON.parse(${JSON.stringify(JSON.stringify(manifest))}), ${JSON.stringify(name)}, ${data}${render});\n`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Creates portable ESM; build integrations can batch payload sources before a strict bundler runs.
|
|
56
|
+
*
|
|
57
|
+
* Each collection gets `collections/<export name>/server.js`, which exports its query handle and
|
|
58
|
+
* imports only that collection's modules, so a bundle that imports it never references the
|
|
59
|
+
* modules of another collection.
|
|
60
|
+
*
|
|
61
|
+
* @param revision - The prepared revision to publish.
|
|
62
|
+
* @param options - `payloadChunkBytes` batches payload modules for bundlers. `preparedEntry`
|
|
63
|
+
* also emits `prepared-server.js`, which exports the whole revision for development facades.
|
|
64
|
+
* @returns Module sources keyed by path relative to the output directory.
|
|
65
|
+
*/
|
|
66
|
+
async function revisionModules(revision, { payloadChunkBytes = 0, preparedEntry = false } = {}) {
|
|
34
67
|
const modules = /* @__PURE__ */ new Map();
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
const imports = [];
|
|
41
|
-
const names = [];
|
|
42
|
-
let needsRenderer = false;
|
|
68
|
+
const names = collectionNames(revision.registry);
|
|
69
|
+
/** Each collection's index module, for the optional prepared entry. */
|
|
70
|
+
const indexes = [];
|
|
71
|
+
let anyMarkdown = false;
|
|
43
72
|
for (const [name, collection] of Object.entries(revision.collections)) {
|
|
44
|
-
const
|
|
73
|
+
const { exportName } = names.get(name);
|
|
74
|
+
const directory = `collections/${exportName}`;
|
|
75
|
+
/** Whether the collection captured Markdown, which the packaged renderer renders on demand. */
|
|
76
|
+
let markdown = false;
|
|
45
77
|
const payloads = [];
|
|
46
78
|
const chunks = [];
|
|
47
79
|
let batch = [];
|
|
@@ -65,7 +97,7 @@ async function revisionModules(revision, { payloadChunkBytes = 0 } = {}) {
|
|
|
65
97
|
for (const [id, load] of collection.builtins) {
|
|
66
98
|
const payload = await load();
|
|
67
99
|
const encoded = encode(payload);
|
|
68
|
-
|
|
100
|
+
markdown ||= payload.markdown !== void 0;
|
|
69
101
|
if (payloadChunkBytes) {
|
|
70
102
|
const bytes = Buffer.byteLength(`[${JSON.stringify(id)}]: ${JSON.stringify(encoded)},\n`);
|
|
71
103
|
if (batchBytes && batchBytes + bytes > payloadChunkBytes) flushBatch();
|
|
@@ -82,35 +114,121 @@ async function revisionModules(revision, { payloadChunkBytes = 0 } = {}) {
|
|
|
82
114
|
payloads.push(`[${JSON.stringify(id)}]: () => import(${JSON.stringify(`./entries/${key}.js`)})`);
|
|
83
115
|
}
|
|
84
116
|
flushBatch();
|
|
117
|
+
const custom = [];
|
|
118
|
+
for (const [payload, entries] of collection.payloads ?? []) {
|
|
119
|
+
const payloadDirectory = `payloads/${directoryName(payload)}`;
|
|
120
|
+
const rows = [];
|
|
121
|
+
for (const [id, variants] of entries) {
|
|
122
|
+
const loaders = [];
|
|
123
|
+
for (const [variant, load] of variants) {
|
|
124
|
+
const encoded = load.encoded ?? encode(await load());
|
|
125
|
+
const key = digest([
|
|
126
|
+
name,
|
|
127
|
+
payload,
|
|
128
|
+
id,
|
|
129
|
+
variant,
|
|
130
|
+
encoded
|
|
131
|
+
]);
|
|
132
|
+
publishPayload(`${directory}/${payloadDirectory}/${key}.js`, `export default ${JSON.stringify(encoded)};\n`);
|
|
133
|
+
loaders.push(`${JSON.stringify(variant)}: () => import(${JSON.stringify(`./${payloadDirectory}/${key}.js`)})`);
|
|
134
|
+
}
|
|
135
|
+
rows.push(`${JSON.stringify(id)}: { ${loaders.join(", ")} }`);
|
|
136
|
+
}
|
|
137
|
+
custom.push(`${JSON.stringify(payload)}: {\n${rows.join(",\n")}\n}`);
|
|
138
|
+
}
|
|
85
139
|
const recordSource = JSON.stringify(collection.records.map(encode));
|
|
86
140
|
const compressedRecords = compactJson(recordSource);
|
|
87
141
|
const helpers = [...chunks.length ? ["createPayloadLoaders"] : [], ...compressedRecords ? ["decodeCompressed"] : []];
|
|
88
142
|
const loaders = helpers.length ? `import { ${helpers.join(", ")} } from "content-query/internal";\n` : "";
|
|
89
143
|
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`;
|
|
144
|
+
const code = `${loaders}export default { records: ${compressedRecords ? `decodeCompressed(${compressedRecords})` : recordSource}, builtins: ${loading}${custom.length ? `, payloads: {\n${custom.join(",\n")}\n}` : ""} };\n`;
|
|
91
145
|
const index = `index-${digest(code)}.js`;
|
|
92
146
|
modules.set(`${directory}/${index}`, code);
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
147
|
+
const descriptor = revision.registry[name];
|
|
148
|
+
const manifest = {
|
|
149
|
+
version: 3,
|
|
150
|
+
codec: CODEC_VERSION,
|
|
151
|
+
collections: { [name]: {
|
|
152
|
+
relationships: descriptor.relationships,
|
|
153
|
+
...descriptor.payloads ? { payloads: descriptor.payloads } : {}
|
|
154
|
+
} },
|
|
155
|
+
...revision.queryStyle ? { queryStyle: revision.queryStyle } : {}
|
|
156
|
+
};
|
|
157
|
+
modules.set(`${directory}/server.js`, collectionEntry$1(manifest, name, exportName, index, markdown));
|
|
158
|
+
indexes.push([name, `./${directory}/${index}`]);
|
|
159
|
+
anyMarkdown ||= markdown;
|
|
160
|
+
}
|
|
161
|
+
if (preparedEntry) {
|
|
162
|
+
const manifest = {
|
|
163
|
+
version: 3,
|
|
164
|
+
codec: CODEC_VERSION,
|
|
165
|
+
collections: Object.fromEntries(Object.entries(revision.registry).map(([name, { relationships, payloads }]) => [name, {
|
|
166
|
+
relationships,
|
|
167
|
+
...payloads ? { payloads } : {}
|
|
168
|
+
}])),
|
|
169
|
+
...revision.queryStyle ? { queryStyle: revision.queryStyle } : {}
|
|
170
|
+
};
|
|
171
|
+
const render = anyMarkdown ? `, body => import("content-query/internal/markdown").then(module => module.renderMarkdown(body))` : "";
|
|
172
|
+
modules.set("prepared-server.js", `import { createArtifactRevision } from "content-query/internal";\n${indexes.map(([, specifier], index) => `import collection${index} from ${JSON.stringify(specifier)};`).join("\n")}\nexport const revision = createArtifactRevision(JSON.parse(${JSON.stringify(JSON.stringify(manifest))}), { ${indexes.map(([name], index) => `[${JSON.stringify(name)}]: collection${index}`).join(", ")} }${render});\nexport const registry = revision.registry;\nexport const error = undefined;\n`);
|
|
97
173
|
}
|
|
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
174
|
return modules;
|
|
102
175
|
}
|
|
103
176
|
//#endregion
|
|
104
177
|
//#region src/artifacts/declarations.ts
|
|
105
|
-
/**
|
|
106
|
-
function
|
|
178
|
+
/** Declares the config-bound `Registry` and `ConfiguredQueryStyle` aliases for one directory. */
|
|
179
|
+
function configTypes(directory, config) {
|
|
107
180
|
let relative = path.relative(directory, config).replace(/\\/g, "/");
|
|
108
181
|
if (!relative.startsWith(".")) relative = `./${relative}`;
|
|
109
|
-
|
|
182
|
+
const source = JSON.stringify(relative);
|
|
183
|
+
return [`type Registry = typeof import(${source}).collections;`, `type ConfiguredQueryStyle = typeof import(${source}) extends { queryStyle: infer Style extends import("content-query/runtime").QueryStyle } ? Style : import("content-query/runtime").QueryStyle;`];
|
|
110
184
|
}
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Generates type-only bindings for one collection entry without loading schemas or adding runtime
|
|
187
|
+
* imports.
|
|
188
|
+
*
|
|
189
|
+
* The registry and the optional `queryStyle` both come from the config module's types, so a
|
|
190
|
+
* literal `export const queryStyle = "relational"` narrows the export without a rebuild. A missing
|
|
191
|
+
* export, or one typed as the whole `QueryStyle` union, allows both query styles.
|
|
192
|
+
*
|
|
193
|
+
* The entry type, such as `ContentJob`, is the full entry. `ContentJob<Field>` is the shape a
|
|
194
|
+
* query returns when it selects `Field`: every selected key is present, and optional fields read
|
|
195
|
+
* as `undefined` when missing, so query results and the type are assignable in both directions.
|
|
196
|
+
*
|
|
197
|
+
* @param directory - Directory that will contain the declaration file.
|
|
198
|
+
* @param config - Absolute path of content-query.config.
|
|
199
|
+
* @param name - The collection's name in the registry.
|
|
200
|
+
* @param names - The collection's generated export and type names.
|
|
201
|
+
* @returns Declaration source for the entry's `server.d.ts`.
|
|
202
|
+
*/
|
|
203
|
+
function collectionDeclarations(directory, config, name, { exportName, typeName, payloadTypeName }) {
|
|
204
|
+
const key = JSON.stringify(name);
|
|
205
|
+
const definition = `Registry[${key}]`;
|
|
206
|
+
const data = `import("content-query").QueryData<${definition}>`;
|
|
207
|
+
return [
|
|
208
|
+
...configTypes(directory, config),
|
|
209
|
+
`/** The ${exportName} collection. Start queries here, or pass it to another collection's \`using()\` to follow relationships into it. */`,
|
|
210
|
+
`export declare const ${exportName}: import("content-query/runtime").Collection<Registry, ${key}, ConfiguredQueryStyle>;`,
|
|
211
|
+
`/** A ${exportName} entry: its ID, validated metadata, and computed fields. Pass field names, such as \`${typeName}<"id">\`, for the shape a query returns when it selects them. */`,
|
|
212
|
+
`export type ${typeName}<Field extends keyof ${data} = keyof ${data}> = keyof ${data} extends Field ? ${data} : { [Key in Field]: ${data}[Key] };`,
|
|
213
|
+
`/** The value of one payload that the ${exportName} collection declares. */`,
|
|
214
|
+
`export type ${payloadTypeName}<Payload extends keyof import("content-query").PayloadsOf<${definition}>> = import("content-query").PayloadData<${definition}, Payload>;`,
|
|
215
|
+
``
|
|
216
|
+
].join("\n");
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Declares project-bound virtual collection modules and the framework-neutral development bridge.
|
|
220
|
+
*
|
|
221
|
+
* @param directory - Directory that will contain the declaration file.
|
|
222
|
+
* @param config - Absolute path of content-query.config.
|
|
223
|
+
* @param names - Every collection's generated names, keyed by collection name.
|
|
224
|
+
* @returns Declaration source for the `content-query:collections/<export name>` and
|
|
225
|
+
* `content-query:client` modules, plus the type-only `content-query:collections` module, whose
|
|
226
|
+
* `CollectionKey` names every collection for integrations that accept a collection name.
|
|
227
|
+
*/
|
|
228
|
+
function virtualDeclarations(directory, config, names) {
|
|
229
|
+
const collections = [...names].map(([name, generated]) => `declare module "content-query:collections/${generated.exportName}" {\n${collectionDeclarations(directory, config, name, generated).replace("export declare const", "export const")}}\n`);
|
|
230
|
+
const keys = `declare module "content-query:collections" {\n${configTypes(directory, config)[0]}\n/** Names of the configured collections, for integrations that accept a collection name. Types only: import collections from their own modules. */\nexport type CollectionKey = keyof Registry & string;\n}\n`;
|
|
231
|
+
return `${collections.join("")}${keys}declare module "content-query:client" {\n export function onContentUpdate(callback: () => void | Promise<void>): () => void;\n}\n`;
|
|
114
232
|
}
|
|
115
233
|
//#endregion
|
|
116
234
|
//#region src/artifacts/write.ts
|
|
@@ -162,7 +280,9 @@ async function writeImmutable(filename, source) {
|
|
|
162
280
|
await rm(temporary, { force: true });
|
|
163
281
|
}
|
|
164
282
|
}
|
|
165
|
-
const immutableModule = /\/(?:entries\/[a-f0-9]{64}|index-[a-f0-9]{64})\.js$/;
|
|
283
|
+
const immutableModule = /\/(?:entries\/[a-f0-9]{64}|payloads\/[^/]+\/[a-f0-9]{64}|index-[a-f0-9]{64})\.js$/;
|
|
284
|
+
/** Matches each collection's entry, which imports the collection's other modules. */
|
|
285
|
+
const collectionEntry = /^collections\/[^/]+\/server\.js$/;
|
|
166
286
|
/** Lists one directory's entries, treating a missing directory or a file as empty. */
|
|
167
287
|
async function entriesOf(directory) {
|
|
168
288
|
try {
|
|
@@ -196,19 +316,39 @@ async function pruneArtifacts(outDir, modules) {
|
|
|
196
316
|
for (const directory of await entriesOf(collections)) {
|
|
197
317
|
const owned = path.join(collections, directory);
|
|
198
318
|
const entries = path.join(owned, "entries");
|
|
319
|
+
const payloads = path.join(owned, "payloads");
|
|
320
|
+
const payloadDirectories = await entriesOf(payloads);
|
|
199
321
|
const candidates = [...(await entriesOf(owned)).map((name) => `collections/${directory}/${name}`), ...(await entriesOf(entries)).map((name) => `collections/${directory}/entries/${name}`)];
|
|
200
|
-
for (const
|
|
322
|
+
for (const payload of payloadDirectories) candidates.push(...(await entriesOf(path.join(payloads, payload))).map((name) => `collections/${directory}/payloads/${payload}/${name}`));
|
|
323
|
+
for (const filename of candidates) {
|
|
324
|
+
const generated = immutableModule.test(`/${filename}`) || filename.endsWith(`/${directory}/server.js`);
|
|
325
|
+
const declarations = filename.endsWith(`/${directory}/server.d.ts`) && !modules.has(filename.replace(/\.d\.ts$/, ".js"));
|
|
326
|
+
if (generated && !modules.has(filename) || declarations) await rm(path.join(outDir, filename), { force: true });
|
|
327
|
+
}
|
|
201
328
|
await removeIfEmpty(entries);
|
|
329
|
+
for (const payload of payloadDirectories) await removeIfEmpty(path.join(payloads, payload));
|
|
330
|
+
await removeIfEmpty(payloads);
|
|
202
331
|
await removeIfEmpty(owned);
|
|
203
332
|
}
|
|
204
333
|
}
|
|
205
|
-
/**
|
|
334
|
+
/**
|
|
335
|
+
* Stages modules with bounded I/O before switching each collection's public entry.
|
|
336
|
+
*
|
|
337
|
+
* @param revision - The prepared revision to publish.
|
|
338
|
+
* @param outDir - The generated output directory.
|
|
339
|
+
* @param config - Absolute path of content-query.config, which declarations import types from.
|
|
340
|
+
* @param options - `signal` cancels the write; `prune` removes superseded modules.
|
|
341
|
+
*/
|
|
206
342
|
async function writeArtifacts(revision, outDir, config, { signal, prune = false } = {}) {
|
|
207
343
|
const modules = await revisionModules(revision);
|
|
344
|
+
const names = collectionNames(revision.registry);
|
|
208
345
|
await writeChanged(path.join(outDir, "package.json"), "{\"type\":\"module\",\"private\":true}\n");
|
|
209
|
-
await writeChanged(path.join(outDir, "
|
|
210
|
-
|
|
211
|
-
|
|
346
|
+
await writeChanged(path.join(outDir, "types.d.ts"), virtualDeclarations(outDir, config, names));
|
|
347
|
+
for (const [name, generated] of names) {
|
|
348
|
+
const directory = path.join(outDir, "collections", generated.exportName);
|
|
349
|
+
await writeChanged(path.join(directory, "server.d.ts"), collectionDeclarations(directory, config, name, generated));
|
|
350
|
+
}
|
|
351
|
+
const staged = [...modules].filter(([filename]) => !collectionEntry.test(filename));
|
|
212
352
|
const directories = new Set(staged.map(([filename]) => path.dirname(path.join(outDir, filename))));
|
|
213
353
|
await Promise.all([...directories].map(async (directory) => mkdir(directory, { recursive: true })));
|
|
214
354
|
const pending = staged.values();
|
|
@@ -220,7 +360,7 @@ async function writeArtifacts(revision, outDir, config, { signal, prune = false
|
|
|
220
360
|
}
|
|
221
361
|
}))).find((outcome) => outcome.status === "rejected");
|
|
222
362
|
if (failure?.status === "rejected") throw failure.reason;
|
|
223
|
-
await writeChanged(path.join(outDir,
|
|
363
|
+
for (const [filename, code] of modules) if (collectionEntry.test(filename)) await writeChanged(path.join(outDir, filename), code, signal);
|
|
224
364
|
if (prune) await pruneArtifacts(outDir, modules);
|
|
225
365
|
}
|
|
226
366
|
//#endregion
|
|
@@ -243,21 +383,196 @@ function hasBuiltinPayloads(definition) {
|
|
|
243
383
|
return builtinLoader(definition.loader)?.kind !== "file";
|
|
244
384
|
}
|
|
245
385
|
/**
|
|
386
|
+
* Describes a collection's custom payloads by their sorted variant keys. Underscore names belong
|
|
387
|
+
* to built-ins, so an invalid declaration never shadows them; preparation reports it instead.
|
|
388
|
+
*
|
|
389
|
+
* @param definition - A collection definition.
|
|
390
|
+
* @returns Payload descriptors keyed by custom payload name.
|
|
391
|
+
*/
|
|
392
|
+
function customPayloads(definition) {
|
|
393
|
+
return Object.fromEntries(Object.entries(definition.payloads ?? {}).filter(([name]) => !name.startsWith("_")).map(([name, payload]) => [name, { variants: Object.keys(payload?.variants ?? {}).sort() }]));
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
246
396
|
* Builds the query registry that runtimes validate queries against, without resolvers or schemas.
|
|
247
397
|
*
|
|
248
398
|
* @param registry - The configured collection definitions.
|
|
249
|
-
* @returns Relationship and payload descriptors for each collection.
|
|
399
|
+
* @returns Relationship and payload descriptors for each collection, built-in and custom.
|
|
250
400
|
*/
|
|
251
401
|
function queryRegistry(registry) {
|
|
252
402
|
return Object.fromEntries(Object.entries(registry).map(([name, definition]) => [name, {
|
|
253
403
|
relationships: definition.relationships ?? {},
|
|
254
|
-
payloads:
|
|
404
|
+
payloads: {
|
|
405
|
+
...hasBuiltinPayloads(definition) ? builtinPayloads : {},
|
|
406
|
+
...customPayloads(definition)
|
|
407
|
+
},
|
|
408
|
+
...definition.typeName === void 0 ? {} : { typeName: definition.typeName }
|
|
255
409
|
}]));
|
|
256
410
|
}
|
|
257
411
|
//#endregion
|
|
412
|
+
//#region src/shared/paths.ts
|
|
413
|
+
/** Checks containment without confusing sibling prefixes, drive roots, or trailing separators. */
|
|
414
|
+
function withinDirectory(directory, filename) {
|
|
415
|
+
const relative = path.relative(directory, filename);
|
|
416
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
417
|
+
}
|
|
418
|
+
/** Resolves existing symlink ancestors even when the output directory does not exist yet. */
|
|
419
|
+
async function canonicalPath(filename) {
|
|
420
|
+
try {
|
|
421
|
+
return await realpath(filename);
|
|
422
|
+
} catch (cause) {
|
|
423
|
+
if (!cause || typeof cause !== "object" || !("code" in cause) || cause.code !== "ENOENT") throw cause;
|
|
424
|
+
const parent = path.dirname(filename);
|
|
425
|
+
if (parent === filename) throw cause;
|
|
426
|
+
return path.join(await canonicalPath(parent), path.basename(filename));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region src/preparation/files.ts
|
|
431
|
+
/**
|
|
432
|
+
* Finds the folder an entry's `files` helpers read from: the loader's `source.directory`, or the
|
|
433
|
+
* folder of its `source.filePath`. Both are relative to the project root.
|
|
434
|
+
*
|
|
435
|
+
* @param root - The project root.
|
|
436
|
+
* @param source - The entry's loader-supplied source metadata.
|
|
437
|
+
* @returns The absolute folder, or undefined when the source names neither.
|
|
438
|
+
*/
|
|
439
|
+
function entryFolder(root, source) {
|
|
440
|
+
if (!source || typeof source !== "object") return;
|
|
441
|
+
const { directory, filePath } = source;
|
|
442
|
+
if (typeof directory === "string" && directory) return path.resolve(root, directory);
|
|
443
|
+
if (typeof filePath === "string" && filePath) return path.resolve(root, path.dirname(filePath));
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Creates tracked file helpers for one resolver call. Reads are scoped to the entry's folder and
|
|
447
|
+
* the collection's shared directories, and each read is recorded as a dependency.
|
|
448
|
+
*
|
|
449
|
+
* @param options - The entry's roots, cancellation, and the dependency list to record into.
|
|
450
|
+
* @returns The `files` object resolvers receive.
|
|
451
|
+
*/
|
|
452
|
+
function createEntryFiles(options) {
|
|
453
|
+
const { root, folder, shared, entry, signal, dependencies } = options;
|
|
454
|
+
const roots = [...folder ? [folder] : [], ...shared];
|
|
455
|
+
/** Resolves a path from the entry's folder and rejects one outside every allowed root. */
|
|
456
|
+
function resolve(filePath) {
|
|
457
|
+
if (typeof filePath !== "string" || !filePath) throw new ContentError({
|
|
458
|
+
code: "source-error",
|
|
459
|
+
message: "files helpers require a nonempty path."
|
|
460
|
+
});
|
|
461
|
+
if (!folder && !path.isAbsolute(filePath)) throw new ContentError({
|
|
462
|
+
code: "source-error",
|
|
463
|
+
message: `Entry "${entry}" has no folder for files helpers. glob() and file() entries use their file's folder, and custom loaders can set source.directory.`
|
|
464
|
+
});
|
|
465
|
+
const resolved = path.resolve(folder ?? root, filePath);
|
|
466
|
+
if (!roots.some((allowed) => withinDirectory(allowed, resolved))) throw new ContentError({
|
|
467
|
+
code: "source-error",
|
|
468
|
+
source: resolved,
|
|
469
|
+
message: `"${filePath}" is outside entry "${entry}"'s folder and the collection's sharedFiles directories.`
|
|
470
|
+
});
|
|
471
|
+
return resolved;
|
|
472
|
+
}
|
|
473
|
+
/** Reads a file and records it, reporting a missing or unreadable file as a source error. */
|
|
474
|
+
async function read(filePath, reader) {
|
|
475
|
+
const resolved = resolve(filePath);
|
|
476
|
+
dependencies.push({
|
|
477
|
+
kind: "file",
|
|
478
|
+
path: resolved
|
|
479
|
+
});
|
|
480
|
+
try {
|
|
481
|
+
return await reader(resolved);
|
|
482
|
+
} catch (cause) {
|
|
483
|
+
signal.throwIfAborted();
|
|
484
|
+
throw contentError(cause, {
|
|
485
|
+
code: "source-error",
|
|
486
|
+
source: resolved,
|
|
487
|
+
message: `Could not read "${filePath}": ${describeCause(cause)}`
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/** Validates one helper entry's data, reporting failures against that file. */
|
|
492
|
+
async function entryOf(schema, value, filePath) {
|
|
493
|
+
const data = await validate(schema, value.data, { source: value.source });
|
|
494
|
+
return {
|
|
495
|
+
id: value.id,
|
|
496
|
+
data,
|
|
497
|
+
...value.body === void 0 ? {} : { body: value.body },
|
|
498
|
+
filePath
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
async glob(globOptions) {
|
|
503
|
+
const { schema, base, ...loaderOptions } = globOptions ?? {};
|
|
504
|
+
const patterns = typeof loaderOptions.pattern === "string" ? [loaderOptions.pattern] : loaderOptions.pattern;
|
|
505
|
+
if (!Array.isArray(patterns) || patterns.some((pattern) => typeof pattern !== "string" || path.posix.isAbsolute(pattern.replace(/^!/, "")) || pattern.replace(/^!/, "").split("/").includes(".."))) throw new ContentError({
|
|
506
|
+
code: "source-error",
|
|
507
|
+
message: "files.glob() requires relative patterns that stay inside their base."
|
|
508
|
+
});
|
|
509
|
+
const directory = resolve(base ?? ".");
|
|
510
|
+
const options = {
|
|
511
|
+
...loaderOptions,
|
|
512
|
+
base: directory
|
|
513
|
+
};
|
|
514
|
+
dependencies.push({
|
|
515
|
+
kind: "glob",
|
|
516
|
+
base: directory,
|
|
517
|
+
options
|
|
518
|
+
});
|
|
519
|
+
const entries = await loadGlob({
|
|
520
|
+
kind: "glob",
|
|
521
|
+
options
|
|
522
|
+
}, root, signal, { warnEmpty: false });
|
|
523
|
+
const retained = loaderOptions.retainBody !== false;
|
|
524
|
+
return Promise.all(entries.map((value) => {
|
|
525
|
+
dependencies.push({
|
|
526
|
+
kind: "file",
|
|
527
|
+
path: value.source
|
|
528
|
+
});
|
|
529
|
+
return entryOf(schema, {
|
|
530
|
+
...value,
|
|
531
|
+
body: retained ? value.body : void 0
|
|
532
|
+
}, value.sourceData.filePath);
|
|
533
|
+
}));
|
|
534
|
+
},
|
|
535
|
+
async file(filePath, fileOptions = {}) {
|
|
536
|
+
const { schema, ...loaderOptions } = fileOptions;
|
|
537
|
+
const resolved = resolve(filePath);
|
|
538
|
+
dependencies.push({
|
|
539
|
+
kind: "file",
|
|
540
|
+
path: resolved
|
|
541
|
+
});
|
|
542
|
+
const descriptor = {
|
|
543
|
+
kind: "file",
|
|
544
|
+
filename: resolved,
|
|
545
|
+
options: loaderOptions
|
|
546
|
+
};
|
|
547
|
+
const entries = await loadFile(descriptor, root, signal);
|
|
548
|
+
const relative = path.relative(root, builtinPath(descriptor, root)).split(path.sep).join("/");
|
|
549
|
+
return Promise.all(entries.map((value) => entryOf(schema, value, relative)));
|
|
550
|
+
},
|
|
551
|
+
text(filePath) {
|
|
552
|
+
return read(filePath, (resolved) => readFile(resolved, {
|
|
553
|
+
encoding: "utf8",
|
|
554
|
+
signal
|
|
555
|
+
}));
|
|
556
|
+
},
|
|
557
|
+
bytes(filePath) {
|
|
558
|
+
return read(filePath, async (resolved) => new Uint8Array(await readFile(resolved, { signal })));
|
|
559
|
+
},
|
|
560
|
+
async renderMarkdown(body) {
|
|
561
|
+
const { html, headings } = await renderMarkdown(body);
|
|
562
|
+
return {
|
|
563
|
+
html,
|
|
564
|
+
metadata: { headings }
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
258
570
|
//#region src/preparation/prepare.ts
|
|
259
|
-
/**
|
|
260
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Prepares one immutable revision with loader-defined source metadata, aggregating failures.
|
|
573
|
+
* `root` is the project root that entry folders and `sharedFiles` resolve against.
|
|
574
|
+
*/
|
|
575
|
+
async function prepareRevision(registry, sources, cache, signal, development, sourceDiagnostics = [], parsedData, { root = process.cwd() } = {}) {
|
|
261
576
|
const diagnostics = [...sourceDiagnostics];
|
|
262
577
|
const collections = Object.create(null);
|
|
263
578
|
for (const [collectionName, definition] of Object.entries(registry)) {
|
|
@@ -279,7 +594,18 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
279
594
|
});
|
|
280
595
|
continue;
|
|
281
596
|
}
|
|
597
|
+
const payloadProblem = invalidPayloads(definition.payloads, declarationNames) ?? invalidSharedFiles(definition.sharedFiles);
|
|
598
|
+
if (payloadProblem) {
|
|
599
|
+
diagnostics.push({
|
|
600
|
+
code: "validation-error",
|
|
601
|
+
operation: "build",
|
|
602
|
+
collection: collectionName,
|
|
603
|
+
message: payloadProblem
|
|
604
|
+
});
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
282
607
|
const descriptor = builtinLoader(definition.loader);
|
|
608
|
+
const shared = (definition.sharedFiles ?? []).map((directory) => path.resolve(root, directory));
|
|
283
609
|
for (const entry of sources.get(collectionName)?.values() ?? []) {
|
|
284
610
|
signal.throwIfAborted();
|
|
285
611
|
const context = {
|
|
@@ -308,6 +634,15 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
308
634
|
const metadata = detach(validated);
|
|
309
635
|
const values = Object.create(null);
|
|
310
636
|
const before = diagnostics.length;
|
|
637
|
+
const dependencies = [];
|
|
638
|
+
const files = createEntryFiles({
|
|
639
|
+
root,
|
|
640
|
+
folder: entryFolder(root, entry.sourceData),
|
|
641
|
+
shared,
|
|
642
|
+
entry: entry.id,
|
|
643
|
+
signal,
|
|
644
|
+
dependencies
|
|
645
|
+
});
|
|
311
646
|
for (const [field, computedField] of Object.entries(computed)) try {
|
|
312
647
|
const result = await computedField.resolve({
|
|
313
648
|
collection: collectionName,
|
|
@@ -315,6 +650,7 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
315
650
|
source: detach(entry.sourceData),
|
|
316
651
|
data: detach(metadata),
|
|
317
652
|
readBody: async () => entry.body,
|
|
653
|
+
files,
|
|
318
654
|
signal
|
|
319
655
|
});
|
|
320
656
|
values[field] = await validate(computedField.schema, result, {
|
|
@@ -322,7 +658,7 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
322
658
|
path: [field]
|
|
323
659
|
});
|
|
324
660
|
} catch (cause) {
|
|
325
|
-
diagnostics.push(...
|
|
661
|
+
diagnostics.push(...located(cause, {
|
|
326
662
|
...context,
|
|
327
663
|
path: [field],
|
|
328
664
|
code: "validation-error",
|
|
@@ -359,7 +695,8 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
359
695
|
definition,
|
|
360
696
|
signature,
|
|
361
697
|
record,
|
|
362
|
-
payload
|
|
698
|
+
payload,
|
|
699
|
+
dependencies
|
|
363
700
|
});
|
|
364
701
|
records.push(record);
|
|
365
702
|
if (Object.keys(payload).length) builtins.set(entry.id, async () => payload);
|
|
@@ -393,9 +730,207 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
393
730
|
};
|
|
394
731
|
const failed = diagnostics.map((diagnostic) => diagnostic.collection);
|
|
395
732
|
if (!failed.includes(void 0)) diagnostics.push(...prepareRelationships(revision, new Set(failed)).flatMap((error) => error.diagnostics));
|
|
733
|
+
const unresolved = new Set(diagnostics.map((diagnostic) => diagnostic.collection));
|
|
734
|
+
if (!unresolved.has(void 0)) {
|
|
735
|
+
for (const [collectionName, definition] of Object.entries(registry)) if (!unresolved.has(collectionName) && Object.keys(definition.payloads ?? {}).length) collections[collectionName].payloads = await preparePayloads(collectionName, definition.payloads, collections[collectionName].records, sources.get(collectionName), cache, signal, diagnostics, {
|
|
736
|
+
root,
|
|
737
|
+
shared: (definition.sharedFiles ?? []).map((directory) => path.resolve(root, directory))
|
|
738
|
+
});
|
|
739
|
+
}
|
|
396
740
|
if (diagnostics.length) throw groupDiagnostics(diagnostics);
|
|
397
741
|
return revision;
|
|
398
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* Checks custom payload declarations before any entry is prepared.
|
|
745
|
+
*
|
|
746
|
+
* @param payloads - The collection's payload declarations.
|
|
747
|
+
* @param declarationNames - Computed field and relationship names the payloads must not repeat.
|
|
748
|
+
* @returns A diagnostic message, or undefined when every declaration is valid.
|
|
749
|
+
*/
|
|
750
|
+
function invalidPayloads(payloads, declarationNames) {
|
|
751
|
+
if (payloads === void 0) return;
|
|
752
|
+
if (!payloads || typeof payloads !== "object" || Array.isArray(payloads)) return "Payloads must be an object of named payload declarations.";
|
|
753
|
+
for (const [name, declaration] of Object.entries(payloads)) {
|
|
754
|
+
if (!name || name.startsWith("_") || declarationNames.includes(name)) return `Custom payload "${name}" must be named without a leading "_" and must not repeat a computed field or relationship.`;
|
|
755
|
+
const { schema, resolve, variants, combinations, cache } = declaration ?? {};
|
|
756
|
+
if (!schema || typeof schema !== "object" || !("~standard" in schema) || typeof resolve !== "function") return `Payload "${name}" requires a schema and a resolve() function.`;
|
|
757
|
+
if (variants !== void 0 && (!variants || typeof variants !== "object" || Array.isArray(variants) || Object.entries(variants).some(([key, values]) => !key || (typeof values === "string" ? !values : !Array.isArray(values) || !values.every((value) => typeof value === "string" && value))))) return `Payload "${name}" variants must map each key to a data field name or a list of values.`;
|
|
758
|
+
if (combinations !== void 0 && (typeof combinations !== "function" || !Object.keys(variants ?? {}).length)) return `Payload "${name}" combinations() must be a function and requires declared variants.`;
|
|
759
|
+
if (cache !== void 0 && typeof cache !== "boolean") return `Payload "${name}" cache must be a boolean.`;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Lists an entry's variant combinations: the cross product of each key's values, or the
|
|
764
|
+
* declaration's validated `combinations()` override. A key's values come from a named string-array
|
|
765
|
+
* field of the entry's data, where a missing value means none, or from a fixed list.
|
|
766
|
+
* Combinations have sorted keys.
|
|
767
|
+
*
|
|
768
|
+
* @param name - The payload name, for error messages.
|
|
769
|
+
* @param payload - The payload declaration.
|
|
770
|
+
* @param context - The enumerator context for one entry.
|
|
771
|
+
* @returns Distinct combinations; a payload without variants has exactly one, `{}`.
|
|
772
|
+
*/
|
|
773
|
+
async function variantCombinations(name, payload, context) {
|
|
774
|
+
const keys = Object.keys(payload.variants ?? {}).sort();
|
|
775
|
+
const values = Object.create(null);
|
|
776
|
+
for (const key of keys) {
|
|
777
|
+
const source = payload.variants[key];
|
|
778
|
+
const listed = typeof source === "string" ? context.data[source] ?? [] : source;
|
|
779
|
+
if (!Array.isArray(listed) || !listed.every((value) => typeof value === "string" && value)) {
|
|
780
|
+
const field = typeof source === "string" ? ` in field "${String(source)}"` : "";
|
|
781
|
+
throw new Error(`Variant "${key}" of payload "${name}" must list non-empty strings${field}.`);
|
|
782
|
+
}
|
|
783
|
+
values[key] = [...new Set(listed)];
|
|
784
|
+
}
|
|
785
|
+
let combinations;
|
|
786
|
+
if (payload.combinations) {
|
|
787
|
+
const listed = await payload.combinations({
|
|
788
|
+
...context,
|
|
789
|
+
variants: values
|
|
790
|
+
});
|
|
791
|
+
if (!listed || typeof listed !== "object" || !(Symbol.iterator in listed)) throw new Error(`Payload "${name}" combinations() must return an iterable of variants.`);
|
|
792
|
+
combinations = [...listed].map((combination) => {
|
|
793
|
+
const variant = combination;
|
|
794
|
+
if (!variant || typeof variant !== "object" || Object.keys(variant).length !== keys.length || keys.some((key) => !values[key].includes(variant[key]))) throw new Error(`Each combination of payload "${name}" must give every variant key (${keys.join(", ")}) one enumerated value.`);
|
|
795
|
+
return Object.fromEntries(keys.map((key) => [key, variant[key]]));
|
|
796
|
+
});
|
|
797
|
+
} else combinations = keys.reduce((partial, key) => partial.flatMap((variant) => values[key].map((value) => ({
|
|
798
|
+
...variant,
|
|
799
|
+
[key]: value
|
|
800
|
+
}))), [{}]);
|
|
801
|
+
return [...new Map(combinations.map((variant) => [variantKey(variant), variant])).values()];
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Adds the location of a resolver failure to every diagnostic that lacks it. A `files` helper error
|
|
805
|
+
* keeps its own source file and field path; the entry, payload, and variant come from `location`.
|
|
806
|
+
*
|
|
807
|
+
* @param cause - The resolver failure.
|
|
808
|
+
* @param location - The entry, and the payload and variant when resolving one.
|
|
809
|
+
* @returns A contextual error whose diagnostics all name the location.
|
|
810
|
+
*/
|
|
811
|
+
function located(cause, location) {
|
|
812
|
+
if (!(cause instanceof ContentError)) return contentError(cause, location);
|
|
813
|
+
return groupDiagnostics(cause.diagnostics.map((diagnostic) => ({
|
|
814
|
+
...location,
|
|
815
|
+
...Object.fromEntries(Object.entries(diagnostic).filter(([, value]) => value !== void 0))
|
|
816
|
+
})));
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Checks a collection's `sharedFiles` directories.
|
|
820
|
+
*
|
|
821
|
+
* @param sharedFiles - The declared shared directories.
|
|
822
|
+
* @returns A diagnostic message, or undefined when the declaration is valid.
|
|
823
|
+
*/
|
|
824
|
+
function invalidSharedFiles(sharedFiles) {
|
|
825
|
+
if (sharedFiles !== void 0 && (!Array.isArray(sharedFiles) || !sharedFiles.every((directory) => typeof directory === "string" && directory))) return "sharedFiles must list directories as nonempty strings.";
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Resolves, validates, and encodes a collection's custom payloads for every entry and variant.
|
|
829
|
+
* Each variant is cached on its own with the files it read, so unchanged entries reuse every
|
|
830
|
+
* variant and an invalidated variant re-resolves alone.
|
|
831
|
+
*
|
|
832
|
+
* @returns Lazy loaders by payload name, entry ID, and canonical variant key.
|
|
833
|
+
*/
|
|
834
|
+
async function preparePayloads(collection, payloads, records, sources, cache, signal, diagnostics, roots) {
|
|
835
|
+
const table = new Map(Object.keys(payloads).map((name) => [name, /* @__PURE__ */ new Map()]));
|
|
836
|
+
for (const record of records) {
|
|
837
|
+
const entry = sources?.get(record.id);
|
|
838
|
+
const cached = cache.get(JSON.stringify([collection, record.id]));
|
|
839
|
+
const data = Object.fromEntries(Object.entries(record).filter(([field]) => field !== "id"));
|
|
840
|
+
const folder = entryFolder(roots.root, entry?.sourceData);
|
|
841
|
+
/** Each resolver receives its own copies, so it cannot change another's inputs. */
|
|
842
|
+
const context = () => ({
|
|
843
|
+
collection,
|
|
844
|
+
id: record.id,
|
|
845
|
+
source: detach(entry?.sourceData),
|
|
846
|
+
data: detach(data),
|
|
847
|
+
signal
|
|
848
|
+
});
|
|
849
|
+
for (const [name, payload] of Object.entries(payloads)) {
|
|
850
|
+
signal.throwIfAborted();
|
|
851
|
+
const reuse = payload.cache !== false;
|
|
852
|
+
const previous = reuse ? cached?.payloads?.get(name) : void 0;
|
|
853
|
+
const base = {
|
|
854
|
+
code: "validation-error",
|
|
855
|
+
operation: "build",
|
|
856
|
+
collection,
|
|
857
|
+
id: record.id,
|
|
858
|
+
source: entry?.source,
|
|
859
|
+
payload: name,
|
|
860
|
+
message: ""
|
|
861
|
+
};
|
|
862
|
+
let combinations = previous?.combinations;
|
|
863
|
+
if (!combinations) try {
|
|
864
|
+
combinations = await variantCombinations(name, payload, {
|
|
865
|
+
...context(),
|
|
866
|
+
variants: {}
|
|
867
|
+
});
|
|
868
|
+
} catch (cause) {
|
|
869
|
+
diagnostics.push(...contentError(cause, {
|
|
870
|
+
...base,
|
|
871
|
+
message: cause instanceof Error ? cause.message : `Payload "${name}" variants failed.`
|
|
872
|
+
}).diagnostics);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
876
|
+
let complete = true;
|
|
877
|
+
for (const variant of combinations) {
|
|
878
|
+
signal.throwIfAborted();
|
|
879
|
+
const key = variantKey(variant);
|
|
880
|
+
const reused = previous?.nodes.get(key);
|
|
881
|
+
if (reused) {
|
|
882
|
+
nodes.set(key, reused);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
const location = {
|
|
886
|
+
...base,
|
|
887
|
+
variant
|
|
888
|
+
};
|
|
889
|
+
const dependencies = [];
|
|
890
|
+
try {
|
|
891
|
+
const result = await payload.resolve({
|
|
892
|
+
...context(),
|
|
893
|
+
variant: { ...variant },
|
|
894
|
+
readBody: async () => entry?.body,
|
|
895
|
+
files: createEntryFiles({
|
|
896
|
+
root: roots.root,
|
|
897
|
+
folder,
|
|
898
|
+
shared: roots.shared,
|
|
899
|
+
entry: record.id,
|
|
900
|
+
signal,
|
|
901
|
+
dependencies
|
|
902
|
+
})
|
|
903
|
+
});
|
|
904
|
+
const value = detach(await validate(payload.schema, result, location));
|
|
905
|
+
nodes.set(key, {
|
|
906
|
+
value,
|
|
907
|
+
encoded: encode(value),
|
|
908
|
+
dependencies
|
|
909
|
+
});
|
|
910
|
+
} catch (cause) {
|
|
911
|
+
complete = false;
|
|
912
|
+
const formatted = cause && typeof cause === "object" && "path" in cause && typeof cause.path === "string" ? cause.path : "";
|
|
913
|
+
diagnostics.push(...located(cause, {
|
|
914
|
+
...location,
|
|
915
|
+
path: formatted ? valuePath(formatted) : void 0,
|
|
916
|
+
message: `Payload ${payloadIdentity(name, variant)} failed: ${cause instanceof Error ? cause.message : String(cause)}${formatted ? ` (at ${formatted})` : ""}`
|
|
917
|
+
}).diagnostics);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
signal.throwIfAborted();
|
|
921
|
+
if (cached && reuse) {
|
|
922
|
+
cached.payloads ??= /* @__PURE__ */ new Map();
|
|
923
|
+
cached.payloads.set(name, {
|
|
924
|
+
combinations,
|
|
925
|
+
nodes
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
if (!complete) continue;
|
|
929
|
+
table.get(name).set(record.id, new Map([...nodes].map(([key, node]) => [key, Object.assign(async () => node.value, { encoded: node.encoded })])));
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return table;
|
|
933
|
+
}
|
|
399
934
|
//#endregion
|
|
400
935
|
//#region src/preparation/config.ts
|
|
401
936
|
/** Loads build-only configuration without retaining a previous config module value. */
|
|
@@ -418,35 +953,20 @@ async function loadRegistry(config) {
|
|
|
418
953
|
async: true,
|
|
419
954
|
forceTranspile: true
|
|
420
955
|
});
|
|
956
|
+
if (imported.queryStyle !== void 0 && !isQueryStyle(imported.queryStyle)) throw new Error("content-query.config queryStyle must be \"fluent\" or \"relational\".");
|
|
421
957
|
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
958
|
const registry = imported.collections;
|
|
423
959
|
if (!registry || typeof registry !== "object" || Array.isArray(registry)) throw new Error("content-query.config must export a collections registry.");
|
|
424
960
|
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).`);
|
|
961
|
+
collectionNames(registry);
|
|
425
962
|
return {
|
|
426
963
|
registry,
|
|
427
964
|
dependencies,
|
|
428
|
-
loaderConfig: imported.loaderConfig
|
|
965
|
+
loaderConfig: imported.loaderConfig,
|
|
966
|
+
queryStyle: imported.queryStyle
|
|
429
967
|
};
|
|
430
968
|
}
|
|
431
969
|
//#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
970
|
//#region src/loaders/match.ts
|
|
451
971
|
const matchers = /* @__PURE__ */ new WeakMap();
|
|
452
972
|
/**
|
|
@@ -491,6 +1011,7 @@ const awaitWriteFinish = {
|
|
|
491
1011
|
stabilityThreshold: 50,
|
|
492
1012
|
pollInterval: 10
|
|
493
1013
|
};
|
|
1014
|
+
const NEW_DIRECTORY_RECHECKS_MS = [100, 1e3];
|
|
494
1015
|
/** Finds an existing ancestor so watching also detects a missing base directory's creation. */
|
|
495
1016
|
async function watchAncestor(filename) {
|
|
496
1017
|
let ancestor = filename;
|
|
@@ -552,6 +1073,7 @@ async function watchProject(options, register) {
|
|
|
552
1073
|
});
|
|
553
1074
|
register(watcher);
|
|
554
1075
|
watcher.on("all", (event, filename) => {
|
|
1076
|
+
options.dependencyChanged?.(event, filename);
|
|
555
1077
|
if (event === "addDir") return;
|
|
556
1078
|
if (configDependency(options, filename)) {
|
|
557
1079
|
options.configChanged();
|
|
@@ -615,12 +1137,188 @@ async function watchSources(options, register) {
|
|
|
615
1137
|
}
|
|
616
1138
|
if (dedicated) {
|
|
617
1139
|
external.on("all", changed);
|
|
1140
|
+
if (external !== shared) external.on("all", (event, filename) => options.dependencyChanged?.(event, filename));
|
|
618
1141
|
external.on("error", (cause) => options.failed(cause));
|
|
1142
|
+
if (builtin.kind === "glob") recheckNewDirectories(external, shared, (filename) => affectsSnapshot(builtin, target, "add", filename), () => options.invalidated(name), register);
|
|
619
1143
|
await ready(external);
|
|
620
1144
|
}
|
|
621
1145
|
}
|
|
622
1146
|
}
|
|
623
1147
|
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Invalidates a glob collection when a new directory holds matching files that no watcher event
|
|
1150
|
+
* reported. Chokidar subscribes to a new directory only after reading it, so a file created in
|
|
1151
|
+
* that gap is otherwise missed until something else reloads the collection.
|
|
1152
|
+
*
|
|
1153
|
+
* @param directories - The watcher that reports new directories under the glob base.
|
|
1154
|
+
* @param shared - The project watcher, which reports ordinary files when it covers the base.
|
|
1155
|
+
* @param matches - Whether a file belongs to the collection's snapshot.
|
|
1156
|
+
* @param invalidate - Reloads the collection from disk.
|
|
1157
|
+
* @param register - Registers cleanup for pending rechecks and listeners.
|
|
1158
|
+
*/
|
|
1159
|
+
function recheckNewDirectories(directories, shared, matches, invalidate, register) {
|
|
1160
|
+
const pending = /* @__PURE__ */ new Set();
|
|
1161
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1162
|
+
let closed = false;
|
|
1163
|
+
/** Records added or changed files while a recheck could otherwise report them twice. */
|
|
1164
|
+
function track(event, filename) {
|
|
1165
|
+
if (pending.size && (event === "add" || event === "change")) reported.add(path.resolve(filename));
|
|
1166
|
+
}
|
|
1167
|
+
/** Reloads only when the directory holds a matching file that no event reported. */
|
|
1168
|
+
async function recheck(directory) {
|
|
1169
|
+
let files;
|
|
1170
|
+
try {
|
|
1171
|
+
files = (await readdir(directory, {
|
|
1172
|
+
recursive: true,
|
|
1173
|
+
withFileTypes: true
|
|
1174
|
+
})).filter((entry) => entry.isFile()).map((entry) => path.resolve(entry.parentPath, entry.name));
|
|
1175
|
+
} catch {
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (!closed && files.some((filename) => matches(filename) && !reported.has(filename))) invalidate();
|
|
1179
|
+
}
|
|
1180
|
+
/** Schedules rechecks for a newly created directory. */
|
|
1181
|
+
function added(directory) {
|
|
1182
|
+
for (const delay of NEW_DIRECTORY_RECHECKS_MS) {
|
|
1183
|
+
const timer = setTimeout(() => {
|
|
1184
|
+
recheck(directory).finally(() => {
|
|
1185
|
+
pending.delete(timer);
|
|
1186
|
+
if (!pending.size) reported.clear();
|
|
1187
|
+
});
|
|
1188
|
+
}, delay);
|
|
1189
|
+
timer.unref();
|
|
1190
|
+
pending.add(timer);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
directories.on("all", track);
|
|
1194
|
+
directories.on("addDir", added);
|
|
1195
|
+
shared?.on("all", track);
|
|
1196
|
+
register(() => {
|
|
1197
|
+
closed = true;
|
|
1198
|
+
directories.off("all", track);
|
|
1199
|
+
directories.off("addDir", added);
|
|
1200
|
+
shared?.off("all", track);
|
|
1201
|
+
pending.forEach(clearTimeout);
|
|
1202
|
+
pending.clear();
|
|
1203
|
+
reported.clear();
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Watches files and directories that resolvers read when no other watcher reports them: paths
|
|
1208
|
+
* outside the project root or below a directory the project watcher ignores, such as `dist/`.
|
|
1209
|
+
*
|
|
1210
|
+
* @param options - Watch options with the project root and the dependency callback.
|
|
1211
|
+
* @returns `add()` to watch further dependency paths, and `close()` to stop watching.
|
|
1212
|
+
*/
|
|
1213
|
+
function watchDependencies(options) {
|
|
1214
|
+
const ignoredByProject = projectIgnored(options);
|
|
1215
|
+
const watched = /* @__PURE__ */ new Set();
|
|
1216
|
+
let watcher;
|
|
1217
|
+
return {
|
|
1218
|
+
add(paths) {
|
|
1219
|
+
const uncovered = [...paths].filter((filename) => !watched.has(filename) && (ignoredByProject(filename) || !(withinDirectory(options.root, filename) || withinDirectory(path.dirname(options.config), filename))));
|
|
1220
|
+
if (!uncovered.length) return;
|
|
1221
|
+
uncovered.forEach((filename) => watched.add(filename));
|
|
1222
|
+
if (watcher) {
|
|
1223
|
+
watcher.add(uncovered);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
watcher = watch(uncovered, {
|
|
1227
|
+
ignoreInitial: true,
|
|
1228
|
+
awaitWriteFinish
|
|
1229
|
+
});
|
|
1230
|
+
watcher.on("all", (event, filename) => options.dependencyChanged?.(event, filename));
|
|
1231
|
+
watcher.on("error", (cause) => options.failed(cause));
|
|
1232
|
+
},
|
|
1233
|
+
async close() {
|
|
1234
|
+
await watcher?.close();
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
//#endregion
|
|
1239
|
+
//#region src/preparation/dependencies.ts
|
|
1240
|
+
/**
|
|
1241
|
+
* Indexes every file and glob that cached entries and payload variants read.
|
|
1242
|
+
*
|
|
1243
|
+
* @param cache - The preparation cache, whose nodes record their dependencies.
|
|
1244
|
+
* @returns A reverse index from files and globs to nodes.
|
|
1245
|
+
*/
|
|
1246
|
+
function dependencyIndex(cache) {
|
|
1247
|
+
const index = {
|
|
1248
|
+
files: /* @__PURE__ */ new Map(),
|
|
1249
|
+
globs: []
|
|
1250
|
+
};
|
|
1251
|
+
/** Records one node's dependencies. */
|
|
1252
|
+
function add(node, dependencies) {
|
|
1253
|
+
for (const dependency of dependencies) if (dependency.kind === "file") {
|
|
1254
|
+
const nodes = index.files.get(dependency.path);
|
|
1255
|
+
if (nodes) nodes.push(node);
|
|
1256
|
+
else index.files.set(dependency.path, [node]);
|
|
1257
|
+
} else index.globs.push({
|
|
1258
|
+
base: dependency.base,
|
|
1259
|
+
options: dependency.options,
|
|
1260
|
+
node
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
for (const [key, entry] of cache) {
|
|
1264
|
+
add({ key }, entry.dependencies);
|
|
1265
|
+
for (const [payload, cached] of entry.payloads ?? []) for (const [variant, node] of cached.nodes) add({
|
|
1266
|
+
key,
|
|
1267
|
+
payload,
|
|
1268
|
+
variant
|
|
1269
|
+
}, node.dependencies);
|
|
1270
|
+
}
|
|
1271
|
+
return index;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Finds the nodes a file event affects: nodes that read the file, nodes whose glob gains or loses
|
|
1275
|
+
* a match, nodes whose glob base gains a directory, and, for a removed directory, nodes that read
|
|
1276
|
+
* anything inside it.
|
|
1277
|
+
*
|
|
1278
|
+
* @param index - The dependency index.
|
|
1279
|
+
* @param event - The watcher event, such as `add`, `change`, `unlink`, or `unlinkDir`.
|
|
1280
|
+
* @param filename - The affected path.
|
|
1281
|
+
* @returns Distinct affected nodes.
|
|
1282
|
+
*/
|
|
1283
|
+
function dependents(index, event, filename) {
|
|
1284
|
+
const resolved = path.resolve(filename);
|
|
1285
|
+
const found = /* @__PURE__ */ new Map();
|
|
1286
|
+
/** Adds a node once. */
|
|
1287
|
+
const include = (node) => found.set(JSON.stringify([
|
|
1288
|
+
node.key,
|
|
1289
|
+
node.payload,
|
|
1290
|
+
node.variant
|
|
1291
|
+
]), node);
|
|
1292
|
+
for (const node of index.files.get(resolved) ?? []) include(node);
|
|
1293
|
+
if (event === "unlinkDir") {
|
|
1294
|
+
for (const [file, nodes] of index.files) if (withinDirectory(resolved, file)) nodes.forEach(include);
|
|
1295
|
+
}
|
|
1296
|
+
for (const glob of index.globs) if (event === "addDir" ? resolved !== glob.base && withinDirectory(glob.base, resolved) : affectsSnapshot({
|
|
1297
|
+
kind: "glob",
|
|
1298
|
+
options: glob.options
|
|
1299
|
+
}, glob.base, event, resolved)) include(glob.node);
|
|
1300
|
+
return [...found.values()];
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Drops affected nodes from the cache, so the next preparation resolves them again. An entry node
|
|
1304
|
+
* drops the entry's whole cache record; a payload node drops only that variant.
|
|
1305
|
+
*
|
|
1306
|
+
* @param cache - The preparation cache.
|
|
1307
|
+
* @param nodes - The nodes to invalidate.
|
|
1308
|
+
*/
|
|
1309
|
+
function invalidateNodes(cache, nodes) {
|
|
1310
|
+
for (const node of nodes) if (node.payload === void 0) cache.delete(node.key);
|
|
1311
|
+
else cache.get(node.key)?.payloads?.get(node.payload)?.nodes.delete(node.variant);
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Lists the paths that dependencies need watched: each read file and each glob base.
|
|
1315
|
+
*
|
|
1316
|
+
* @param index - The dependency index.
|
|
1317
|
+
* @returns Distinct absolute paths.
|
|
1318
|
+
*/
|
|
1319
|
+
function dependencyPaths(index) {
|
|
1320
|
+
return [.../* @__PURE__ */ new Set([...index.files.keys(), ...index.globs.map((glob) => glob.base)])];
|
|
1321
|
+
}
|
|
624
1322
|
//#endregion
|
|
625
1323
|
//#region src/preparation/loaders/context.ts
|
|
626
1324
|
/** Supplies portable filesystem defaults and fails explicitly for unavailable Astro settings. */
|
|
@@ -1011,7 +1709,13 @@ function sessionInternals(session) {
|
|
|
1011
1709
|
if (!internal) throw new Error("Unknown ContentQuery session.");
|
|
1012
1710
|
return internal;
|
|
1013
1711
|
}
|
|
1014
|
-
/**
|
|
1712
|
+
/**
|
|
1713
|
+
* Creates a preparation session with ordered source commands and atomic revision publication.
|
|
1714
|
+
*
|
|
1715
|
+
* @param options - Root, configuration, watch, and publication settings.
|
|
1716
|
+
* @returns A session whose `collections` are typed by `Style`; each published revision carries
|
|
1717
|
+
* the configured `queryStyle`, which the runtime enforces.
|
|
1718
|
+
*/
|
|
1015
1719
|
async function createSession(options) {
|
|
1016
1720
|
options = {
|
|
1017
1721
|
...options,
|
|
@@ -1060,6 +1764,7 @@ async function createSession(options) {
|
|
|
1060
1764
|
let registry = {};
|
|
1061
1765
|
let loaders = /* @__PURE__ */ new Map();
|
|
1062
1766
|
let configDependencies;
|
|
1767
|
+
let queryStyle;
|
|
1063
1768
|
let configurationError;
|
|
1064
1769
|
const candidates = /* @__PURE__ */ new Map();
|
|
1065
1770
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -1086,11 +1791,26 @@ async function createSession(options) {
|
|
|
1086
1791
|
kind: "invalidate",
|
|
1087
1792
|
collection
|
|
1088
1793
|
}),
|
|
1794
|
+
dependencyChanged: (event, filename) => dependencyChanged(event, filename),
|
|
1089
1795
|
failed: (cause) => enqueue({
|
|
1090
1796
|
kind: "error",
|
|
1091
1797
|
cause
|
|
1092
1798
|
})
|
|
1093
1799
|
};
|
|
1800
|
+
let index;
|
|
1801
|
+
let preparing = false;
|
|
1802
|
+
const recentEvents = [];
|
|
1803
|
+
const dependencyWatcher = options.watch ? watchDependencies(watchOptions) : void 0;
|
|
1804
|
+
/** Invalidates only the cached nodes that read a changed file. */
|
|
1805
|
+
function dependencyChanged(event, filename) {
|
|
1806
|
+
if (preparing) recentEvents.push([event, filename]);
|
|
1807
|
+
index ??= dependencyIndex(cache);
|
|
1808
|
+
const nodes = dependents(index, event, filename);
|
|
1809
|
+
if (nodes.length) enqueue({
|
|
1810
|
+
kind: "payloads",
|
|
1811
|
+
nodes
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1094
1814
|
/** Queues ordered notifications and makes any in-flight preparation obsolete. */
|
|
1095
1815
|
function enqueue(command) {
|
|
1096
1816
|
if (disposed) return;
|
|
@@ -1144,7 +1864,8 @@ async function createSession(options) {
|
|
|
1144
1864
|
const loaded = options.registry ? {
|
|
1145
1865
|
registry: options.registry,
|
|
1146
1866
|
dependencies: /* @__PURE__ */ new Set(),
|
|
1147
|
-
loaderConfig: void 0
|
|
1867
|
+
loaderConfig: void 0,
|
|
1868
|
+
queryStyle: options.queryStyle
|
|
1148
1869
|
} : await loadRegistry(options.config);
|
|
1149
1870
|
const nextSubscriptions = new AbortController();
|
|
1150
1871
|
const nextCleanups = [];
|
|
@@ -1229,9 +1950,11 @@ async function createSession(options) {
|
|
|
1229
1950
|
registry = nextRegistry;
|
|
1230
1951
|
loaders = nextLoaders;
|
|
1231
1952
|
configDependencies = loaded.dependencies;
|
|
1953
|
+
queryStyle = loaded.queryStyle;
|
|
1232
1954
|
state.registry = queryRegistry(registry);
|
|
1233
1955
|
candidates.clear();
|
|
1234
1956
|
cache.clear();
|
|
1957
|
+
index = void 0;
|
|
1235
1958
|
loadFailures.clear();
|
|
1236
1959
|
configurationError = void 0;
|
|
1237
1960
|
installed = true;
|
|
@@ -1239,6 +1962,17 @@ async function createSession(options) {
|
|
|
1239
1962
|
}
|
|
1240
1963
|
/** Acquires invalidated snapshots and publishes only the complete current attempt. */
|
|
1241
1964
|
async function prepare(names) {
|
|
1965
|
+
preparing = true;
|
|
1966
|
+
try {
|
|
1967
|
+
await prepareAttempt(names);
|
|
1968
|
+
} finally {
|
|
1969
|
+
preparing = false;
|
|
1970
|
+
index = void 0;
|
|
1971
|
+
for (const [event, filename] of recentEvents.splice(0)) dependencyChanged(event, filename);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
/** Acquires snapshots, prepares, and publishes one attempt. */
|
|
1975
|
+
async function prepareAttempt(names) {
|
|
1242
1976
|
const currentGeneration = generation;
|
|
1243
1977
|
attempt = new AbortController();
|
|
1244
1978
|
const signal = attempt.signal;
|
|
@@ -1263,12 +1997,15 @@ async function createSession(options) {
|
|
|
1263
1997
|
});
|
|
1264
1998
|
try {
|
|
1265
1999
|
const failures = [...loadFailures.values()];
|
|
1266
|
-
const
|
|
2000
|
+
const style = queryStyle;
|
|
2001
|
+
const revision = await prepareRevision(registry, candidates, cache, signal, options.watch, failures.flatMap((error) => error.diagnostics), new Map([...loaders].map(([name, loader]) => [name, loader.parsed])), { root: options.root });
|
|
1267
2002
|
if (disposed || currentGeneration !== generation) return;
|
|
2003
|
+
if (style) revision.queryStyle = style;
|
|
1268
2004
|
await options.publish?.(revision, registry, signal);
|
|
1269
2005
|
if (disposed || currentGeneration !== generation) return;
|
|
1270
2006
|
state.revision = revision;
|
|
1271
2007
|
state.error = void 0;
|
|
2008
|
+
dependencyWatcher?.add(dependencyPaths(index = dependencyIndex(cache)));
|
|
1272
2009
|
options.change?.(state);
|
|
1273
2010
|
notifySources({
|
|
1274
2011
|
type: "ready",
|
|
@@ -1306,6 +2043,10 @@ async function createSession(options) {
|
|
|
1306
2043
|
await configure(false);
|
|
1307
2044
|
for (const name of Object.keys(registry)) stale.add(name);
|
|
1308
2045
|
} else if (command.kind === "invalidate") stale.add(command.collection);
|
|
2046
|
+
else if (command.kind === "payloads") {
|
|
2047
|
+
invalidateNodes(cache, command.nodes);
|
|
2048
|
+
index = void 0;
|
|
2049
|
+
}
|
|
1309
2050
|
} catch (cause) {
|
|
1310
2051
|
if (command.kind === "config") configurationError = contentError(cause, {
|
|
1311
2052
|
code: "source-error",
|
|
@@ -1330,7 +2071,11 @@ async function createSession(options) {
|
|
|
1330
2071
|
sourceListeners.clear();
|
|
1331
2072
|
generation++;
|
|
1332
2073
|
attempt?.abort();
|
|
1333
|
-
const failed = (await Promise.allSettled([
|
|
2074
|
+
const failed = (await Promise.allSettled([
|
|
2075
|
+
watcher?.close() ?? Promise.resolve(),
|
|
2076
|
+
dependencyWatcher?.close() ?? Promise.resolve(),
|
|
2077
|
+
releaseSubscriptions()
|
|
2078
|
+
])).find((result) => result.status === "rejected");
|
|
1334
2079
|
if (failed?.status === "rejected") throw contentError(failed.reason, {
|
|
1335
2080
|
code: "source-error",
|
|
1336
2081
|
operation: "dispose",
|
|
@@ -1338,7 +2083,7 @@ async function createSession(options) {
|
|
|
1338
2083
|
});
|
|
1339
2084
|
}
|
|
1340
2085
|
const session = {
|
|
1341
|
-
|
|
2086
|
+
collections: createCollections(state),
|
|
1342
2087
|
sources,
|
|
1343
2088
|
subscribe(listener) {
|
|
1344
2089
|
if (disposed) throw new Error("Content session has been disposed.");
|
|
@@ -1440,11 +2185,23 @@ assertServerOnly();
|
|
|
1440
2185
|
/**
|
|
1441
2186
|
* Builds content independently of a bundler; bind the registry through an erased type import.
|
|
1442
2187
|
* Generated watch sessions prune previous-run artifacts after their first successful publication.
|
|
2188
|
+
*
|
|
2189
|
+
* @param arguments_ - Optional build options.
|
|
2190
|
+
* @returns A session whose `content` exposes the root query APIs allowed by `Style`. Pass
|
|
2191
|
+
* `typeof queryStyle` from the config to type it; the configured `queryStyle` is always enforced
|
|
2192
|
+
* at runtime.
|
|
1443
2193
|
*/
|
|
1444
2194
|
async function buildContent(...arguments_) {
|
|
1445
2195
|
return buildAtRoot(process.cwd(), arguments_[0] ?? {});
|
|
1446
2196
|
}
|
|
1447
|
-
/**
|
|
2197
|
+
/**
|
|
2198
|
+
* Resolves host paths and prunes previous-run artifacts on the first successful publication.
|
|
2199
|
+
*
|
|
2200
|
+
* @param root - Application root that relative config and output paths resolve against.
|
|
2201
|
+
* @param options - Build options.
|
|
2202
|
+
* @param change - Optional state observer for host integrations.
|
|
2203
|
+
* @returns A session typed by `Registry` and `Style`.
|
|
2204
|
+
*/
|
|
1448
2205
|
async function buildAtRoot(root, options, change) {
|
|
1449
2206
|
const { backend = "generated", watch = false } = options;
|
|
1450
2207
|
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({
|
|
@@ -1490,4 +2247,4 @@ async function buildAtRoot(root, options, change) {
|
|
|
1490
2247
|
});
|
|
1491
2248
|
}
|
|
1492
2249
|
//#endregion
|
|
1493
|
-
export { sessionInternals as a,
|
|
2250
|
+
export { sessionInternals as a, collectionDeclarations 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 };
|