content-query 0.1.0 → 0.3.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 +29 -2
- package/dist/{build-C5ef0kkR.mjs → build-CI4s0oFP.mjs} +122 -30
- package/dist/build-jKnjDjQE.d.mts +48 -0
- package/dist/build.d.mts +2 -2
- package/dist/build.mjs +1 -2
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +3 -3
- package/dist/computed.d.mts +19 -13
- package/dist/computed.mjs +6 -5
- package/dist/{content-Dj2fQCJ5.mjs → content-N1gYshjg.mjs} +203 -77
- package/dist/{content-Crag1Uch.d.mts → content-lwPxE9Yu.d.mts} +40 -30
- package/dist/{descriptor-Xbo9u5eR.d.mts → descriptor-BUPT2J7X.d.mts} +1 -2
- package/dist/{diagnostics-Dol6ANtq.mjs → diagnostics-Cu94mZhl.mjs} +16 -15
- package/dist/{dist-BQGVLZyl.mjs → dist-C7_FOzY_.mjs} +21 -12
- package/dist/{glob-DJmebCMM.mjs → glob-Bv0dpvCQ.mjs} +3 -3
- package/dist/index.d.mts +35 -16
- package/dist/index.mjs +7 -4
- package/dist/internal/markdown.d.mts +3 -5
- package/dist/internal/markdown.mjs +1 -2
- package/dist/internal.d.mts +12 -12
- package/dist/internal.mjs +7 -7
- package/dist/loaders.d.mts +5 -6
- package/dist/loaders.mjs +6 -5
- package/dist/{markdown-CZBL4Ux_.mjs → markdown-CHvBBJNa.mjs} +2 -2
- package/dist/next.d.mts +3 -4
- package/dist/next.mjs +2 -2
- package/dist/{operators-BQpLfJlx.mjs → operators-UqWUhKQJ.mjs} +2 -2
- package/dist/query.d.mts +22 -23
- package/dist/query.mjs +1 -2
- package/dist/{relational-types-DLniKi5S.d.mts → relational-types-B45zM34W.d.mts} +62 -29
- package/dist/{chunk-CJs4Fogs.mjs → rolldown-runtime-Bg1v5YCA.mjs} +2 -2
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.mjs +3 -4
- package/dist/{schema-CnCbMosm.mjs → schema-C5Str-E8.mjs} +1 -1
- package/dist/schema.d.mts +0 -2
- package/dist/schema.mjs +1 -2
- package/dist/{types-BPh2cBPs.d.mts → types-BC8gVJ6u.d.mts} +27 -6
- package/dist/{types-hmDOw87q.d.mts → types-FmUJ3cH1.d.mts} +23 -6
- package/dist/{validation-BvvjSUFc.mjs → validation-D_XdE1pF.mjs} +2 -2
- package/dist/{values-B7lo_2_x.mjs → values-BbTV4QT-.mjs} +109 -108
- package/dist/vite.d.mts +14 -4
- package/dist/vite.mjs +28 -5
- package/package.json +7 -5
- package/dist/build-DSyoRxkZ.d.mts +0 -24
- /package/dist/{errors-C8lWyiy2.mjs → errors-C2iuBM0G.mjs} +0 -0
- /package/dist/{github-slugger-BCAkwEOL.mjs → github-slugger-BMgR_Q0I.mjs} +0 -0
- /package/dist/{server-only-CIE-FHx0.mjs → server-only-CIJUlyL-.mjs} +0 -0
- /package/dist/{types-DcvHRmxO.d.mts → types-Dh-Sydyw.d.mts} +0 -0
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ Use it with your existing framework. Your application owns its routes, layouts,
|
|
|
9
9
|
- **Typed schemas.** Validate metadata with bundled schema builders or any Standard Schema V1 provider, with inferred TypeScript types.
|
|
10
10
|
- **Flexible sources.** Load local files, use Astro-compatible content loaders, or write your own loader.
|
|
11
11
|
- **Queries and relationships.** Select fields, filter, sort, paginate, and load related entries with validated references.
|
|
12
|
-
- **Markdown rendering.**
|
|
12
|
+
- **Markdown rendering.** Request HTML, headings, and syntax highlighting as query payloads, loaded only when a query asks for them.
|
|
13
|
+
- **Optional MDX companion.** [`@content-query/mdx`](https://github.com/yangshun/content-query/tree/main/packages/mdx) supplies React islands and ordinary React document modules with Vite SSR. Core has no MDX or React dependency.
|
|
13
14
|
- **Framework integrations.** Use Vite, Next.js, or the standalone CLI, with content updates during development.
|
|
14
15
|
|
|
15
16
|
## Usage
|
|
@@ -54,10 +55,28 @@ const posts = await content
|
|
|
54
55
|
.from("posts")
|
|
55
56
|
.where((fields) => eq(fields.draft, false))
|
|
56
57
|
.orderBy("title");
|
|
58
|
+
|
|
59
|
+
const post = await content.query.posts.findFirst({
|
|
60
|
+
where: { id: posts[0]!.id },
|
|
61
|
+
payloads: { _rendered: true },
|
|
62
|
+
});
|
|
63
|
+
const { html, headings } = post!._payloads._rendered;
|
|
57
64
|
```
|
|
58
65
|
|
|
59
66
|
Add `.content-query/` to `.gitignore`. Follow the quick start for a complete walkthrough, or choose a framework example below.
|
|
60
67
|
|
|
68
|
+
## Migrating to 0.2
|
|
69
|
+
|
|
70
|
+
Version 0.2.0 removes `content.readBody()` and `content.render()`. Request bodies and rendered output as payloads in the query that loads the entry:
|
|
71
|
+
|
|
72
|
+
| 0.1 | 0.2 |
|
|
73
|
+
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
74
|
+
| `await content.readBody("posts", id)` | `(await content.query.posts.findFirst({ where: { id }, payloads: { _body: true } }))?._payloads._body` |
|
|
75
|
+
| `await content.render("posts", id)` | `(await content.query.posts.findFirst({ where: { id }, payloads: { _rendered: true } }))?._payloads._rendered` |
|
|
76
|
+
| Fluent queries | Add `.withPayload("_rendered")` or `.withPayload("_body")` before the terminal. |
|
|
77
|
+
|
|
78
|
+
Built-in payload names start with an underscore: `_body` and `_rendered`. A query for a missing entry returns `undefined` instead of rejecting with `entry-not-found`, which no longer exists. JSON, YAML, and TOML sources and `file()` collections offer no payloads, and custom loaders typed as `BodyLoader` offer only `_body`. Rebuild generated content after upgrading; 0.1 artifacts are rejected.
|
|
79
|
+
|
|
61
80
|
## Documentation
|
|
62
81
|
|
|
63
82
|
- [Quick start](https://github.com/yangshun/content-query/blob/main/website/content/guides/quick-start.md)
|
|
@@ -65,6 +84,14 @@ Add `.content-query/` to `.gitignore`. Follow the quick start for a complete wal
|
|
|
65
84
|
- [API and usage](https://github.com/yangshun/content-query/blob/main/docs/API.md)
|
|
66
85
|
- [Guides and reference](https://github.com/yangshun/content-query/blob/main/docs/README.md)
|
|
67
86
|
|
|
68
|
-
The Vite integration supports Vite 8, and the Next.js integration supports Next.js 16. Both integrations are optional; standalone builds and queries require neither framework.
|
|
87
|
+
The Vite integration supports Vite 8, and the Next.js integration supports Next.js 15 and 16. Both integrations are optional; standalone builds and queries require neither framework.
|
|
88
|
+
|
|
89
|
+
Pass `contentQuery()` directly to Vite or Vite+'s `plugins` array. No `Plugin` type cast is needed; workspace consumers share the pinned toolchain versions in the dependency catalog.
|
|
90
|
+
|
|
91
|
+
Repository development checks package types and isolated consumers with [tsrs](https://github.com/oxc-project/tsrs). Run `vp run check` in the package directory for formatting, linting, and type checks; build tools use the shared TypeScript 6 compatibility package for its `6.0.3` JavaScript API. Native TypeScript `7.1.0-beta` is available at the repository root as `vp exec tsc`.
|
|
92
|
+
|
|
93
|
+
Build integrations can reuse the active source session without reading schemas or bodies. `ContentSession.sources()` from `content-query/build` returns an immutable `{ revision, entries: [{ collection, id, filePath? }] }` snapshot; `session.subscribe(listener)` reports successful publications (`{ type: "ready", snapshot }`) and errors (`{ type: "error", error }`) and returns an unsubscribe function. Revisions are opaque and session-scoped. In Vite, `contentQuery().api.prepare()` asynchronously prepares the shared session and returns its source snapshot, and `.api.subscribe()` forwards subsequent events. The [MDX architecture](https://github.com/yangshun/content-query/blob/main/docs/MDX-ARCHITECTURE.md) documents its use by companion integrations.
|
|
94
|
+
|
|
95
|
+
`prepare()` waits for pending preparation and rejects while validation errors are active. Successful unchanged reads reuse the frozen snapshot; publications supply a new revision. Subscribers can observe errors and recovery, and unsubscribing stops subsequent notifications. Development integrations should let core display recoverable errors without aborting the server or replacing the host's refresh subscription.
|
|
69
96
|
|
|
70
97
|
Released under the [MIT license](https://github.com/yangshun/content-query/blob/main/LICENSE).
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { a as builtinLoader, i as validate, n as normalizeSnapshot, o as builtinPath, r as renderedPayload, t as isRecord } from "./validation-
|
|
2
|
-
import { i as groupDiagnostics, n as contentError, o as withContext, t as ContentError } from "./errors-
|
|
3
|
-
import { i as detach, l as valuePath, o as encode } from "./values-
|
|
4
|
-
import { t as assertServerOnly } from "./server-only-
|
|
5
|
-
import { a as CODEC_VERSION, i as prepareRelationships, n as createRuntime, r as isRelationship } from "./content-
|
|
6
|
-
import { t as renderMarkdown } from "./markdown-
|
|
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
7
|
import { access, link, mkdir, readFile, readdir, realpath, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
@@ -33,7 +33,7 @@ function compactJson(source) {
|
|
|
33
33
|
async function revisionModules(revision, { payloadChunkBytes = 0 } = {}) {
|
|
34
34
|
const modules = /* @__PURE__ */ new Map();
|
|
35
35
|
const manifest = {
|
|
36
|
-
version:
|
|
36
|
+
version: 2,
|
|
37
37
|
codec: CODEC_VERSION,
|
|
38
38
|
collections: revision.registry
|
|
39
39
|
};
|
|
@@ -62,7 +62,7 @@ async function revisionModules(revision, { payloadChunkBytes = 0 } = {}) {
|
|
|
62
62
|
batch = [];
|
|
63
63
|
batchBytes = 0;
|
|
64
64
|
}
|
|
65
|
-
for (const [id, load] of collection.
|
|
65
|
+
for (const [id, load] of collection.builtins) {
|
|
66
66
|
const payload = await load();
|
|
67
67
|
const encoded = encode(payload);
|
|
68
68
|
needsRenderer ||= payload.markdown !== void 0;
|
|
@@ -87,7 +87,7 @@ async function revisionModules(revision, { payloadChunkBytes = 0 } = {}) {
|
|
|
87
87
|
const helpers = [...chunks.length ? ["createPayloadLoaders"] : [], ...compressedRecords ? ["decodeCompressed"] : []];
|
|
88
88
|
const loaders = helpers.length ? `import { ${helpers.join(", ")} } from "content-query/internal";\n` : "";
|
|
89
89
|
const loading = chunks.length ? `createPayloadLoaders([${chunks.join(",\n")}])` : `{ ${payloads.join(",\n")} }`;
|
|
90
|
-
const code = `${loaders}export default { records: ${compressedRecords ? `decodeCompressed(${compressedRecords})` : recordSource},
|
|
90
|
+
const code = `${loaders}export default { records: ${compressedRecords ? `decodeCompressed(${compressedRecords})` : recordSource}, builtins: ${loading} };\n`;
|
|
91
91
|
const index = `index-${digest(code)}.js`;
|
|
92
92
|
modules.set(`${directory}/${index}`, code);
|
|
93
93
|
modules.set(`${directory}/index.js`, `export { default } from ${JSON.stringify(`./${index}`)};\n`);
|
|
@@ -224,6 +224,37 @@ async function writeArtifacts(revision, outDir, config, { signal, prune = false
|
|
|
224
224
|
if (prune) await pruneArtifacts(outDir, modules);
|
|
225
225
|
}
|
|
226
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
|
|
227
258
|
//#region src/preparation/prepare.ts
|
|
228
259
|
/** Prepares one immutable revision with loader-defined source metadata, aggregating failures. */
|
|
229
260
|
async function prepareRevision(registry, sources, cache, signal, development, sourceDiagnostics = [], parsedData) {
|
|
@@ -231,11 +262,15 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
231
262
|
const collections = Object.create(null);
|
|
232
263
|
for (const [collectionName, definition] of Object.entries(registry)) {
|
|
233
264
|
const records = [];
|
|
234
|
-
const
|
|
265
|
+
const builtins = /* @__PURE__ */ new Map();
|
|
235
266
|
const computed = definition.computed ?? {};
|
|
236
267
|
const relationships = definition.relationships ?? {};
|
|
237
268
|
const declarationNames = [...Object.keys(computed), ...Object.keys(relationships)];
|
|
238
|
-
if (declarationNames.some((name) =>
|
|
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))) {
|
|
239
274
|
diagnostics.push({
|
|
240
275
|
code: "validation-error",
|
|
241
276
|
operation: "build",
|
|
@@ -261,7 +296,7 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
261
296
|
const previous = cache.get(key);
|
|
262
297
|
if (signature !== void 0 && previous?.signature === signature && previous.definition === definition) {
|
|
263
298
|
records.push(previous.record);
|
|
264
|
-
if (Object.keys(previous.payload).length)
|
|
299
|
+
if (Object.keys(previous.payload).length) builtins.set(entry.id, async () => previous.payload);
|
|
265
300
|
continue;
|
|
266
301
|
}
|
|
267
302
|
try {
|
|
@@ -269,7 +304,7 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
269
304
|
const validated = parsed?.has(entry.data) && parsed.get(entry.data) === definition.schema ? entry.data : await validate(definition.schema, entry.data, context);
|
|
270
305
|
signal.throwIfAborted();
|
|
271
306
|
if (!isRecord(validated)) throw new Error("Validated metadata must be a record of named fields.");
|
|
272
|
-
if (Object.hasOwn(validated, "id") || Object.hasOwn(validated, "_adjacent") || declarationNames.some((name) => Object.hasOwn(validated, name))) throw new Error("Metadata, computed fields, relationships, and reserved id/_adjacent must have distinct names.");
|
|
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.");
|
|
273
308
|
const metadata = detach(validated);
|
|
274
309
|
const values = Object.create(null);
|
|
275
310
|
const before = diagnostics.length;
|
|
@@ -306,16 +341,18 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
306
341
|
if (entry.body !== void 0 && (!markdown || descriptor?.kind !== "glob" || descriptor.options.retainBody !== false)) payload.body = entry.body;
|
|
307
342
|
const supplied = renderedPayload(entry);
|
|
308
343
|
if (supplied) payload.rendered = supplied;
|
|
309
|
-
else if (markdown && entry.body !== void 0)
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
+
}
|
|
319
356
|
}
|
|
320
357
|
encode(payload);
|
|
321
358
|
cache.set(key, {
|
|
@@ -325,7 +362,7 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
325
362
|
payload
|
|
326
363
|
});
|
|
327
364
|
records.push(record);
|
|
328
|
-
if (Object.keys(payload).length)
|
|
365
|
+
if (Object.keys(payload).length) builtins.set(entry.id, async () => payload);
|
|
329
366
|
} catch (cause) {
|
|
330
367
|
const formatted = cause && typeof cause === "object" && "path" in cause && typeof cause.path === "string" ? cause.path : "";
|
|
331
368
|
const message = cause instanceof Error ? cause.message : "Entry preparation failed.";
|
|
@@ -340,14 +377,14 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
340
377
|
collections[collectionName] = {
|
|
341
378
|
records,
|
|
342
379
|
relationships,
|
|
343
|
-
|
|
380
|
+
builtins,
|
|
344
381
|
sources: new Map([...sources.get(collectionName)?.values() ?? []].map((entry) => [entry.id, entry.source]))
|
|
345
382
|
};
|
|
346
383
|
}
|
|
347
384
|
signal.throwIfAborted();
|
|
348
385
|
const revision = {
|
|
349
386
|
collections,
|
|
350
|
-
registry: Object.fromEntries(Object.
|
|
387
|
+
registry: queryRegistry(Object.fromEntries(Object.keys(collections).map((name) => [name, registry[name]]))),
|
|
351
388
|
relations: /* @__PURE__ */ new Map(),
|
|
352
389
|
active: 0,
|
|
353
390
|
payloadCache: /* @__PURE__ */ new Map(),
|
|
@@ -364,7 +401,7 @@ async function prepareRevision(registry, sources, cache, signal, development, so
|
|
|
364
401
|
/** Loads build-only configuration without retaining a previous config module value. */
|
|
365
402
|
async function loadRegistry(config) {
|
|
366
403
|
const filename = await realpath(config);
|
|
367
|
-
const dependencies = new Set([filename]);
|
|
404
|
+
const dependencies = /* @__PURE__ */ new Set([filename]);
|
|
368
405
|
const transpiler = createJiti(filename, {
|
|
369
406
|
moduleCache: false,
|
|
370
407
|
fsCache: false
|
|
@@ -707,7 +744,7 @@ function createStores(current, parsed, changed) {
|
|
|
707
744
|
function guardedWatcher(watcher, run, failed) {
|
|
708
745
|
const listeners = [];
|
|
709
746
|
let running = Promise.resolve();
|
|
710
|
-
const registration = new Set([
|
|
747
|
+
const registration = /* @__PURE__ */ new Set([
|
|
711
748
|
"on",
|
|
712
749
|
"once",
|
|
713
750
|
"addListener",
|
|
@@ -983,6 +1020,43 @@ async function createSession(options) {
|
|
|
983
1020
|
outDir: path.resolve(options.root, options.outDir)
|
|
984
1021
|
};
|
|
985
1022
|
const state = {};
|
|
1023
|
+
const sourceListeners = /* @__PURE__ */ new Set();
|
|
1024
|
+
const sourceSnapshots = /* @__PURE__ */ new WeakMap();
|
|
1025
|
+
const sourceOwner = randomUUID();
|
|
1026
|
+
let sourceVersion = 0;
|
|
1027
|
+
/** Detaches and freezes integration metadata without exposing prepared payloads or schemas. */
|
|
1028
|
+
function sources() {
|
|
1029
|
+
if (state.error) throw state.error;
|
|
1030
|
+
if (!state.revision) throw new ContentError({
|
|
1031
|
+
code: "artifact-error",
|
|
1032
|
+
message: "No prepared source revision is available."
|
|
1033
|
+
});
|
|
1034
|
+
let snapshot = sourceSnapshots.get(state.revision);
|
|
1035
|
+
if (!snapshot) {
|
|
1036
|
+
snapshot = Object.freeze({
|
|
1037
|
+
revision: `${sourceOwner}:${++sourceVersion}`,
|
|
1038
|
+
entries: Object.freeze(Object.entries(state.revision.collections).flatMap(([collection, prepared]) => prepared.records.map(({ id }) => {
|
|
1039
|
+
const filePath = prepared.sources?.get(id);
|
|
1040
|
+
return Object.freeze({
|
|
1041
|
+
collection,
|
|
1042
|
+
id,
|
|
1043
|
+
...filePath === void 0 ? {} : { filePath }
|
|
1044
|
+
});
|
|
1045
|
+
})))
|
|
1046
|
+
});
|
|
1047
|
+
sourceSnapshots.set(state.revision, snapshot);
|
|
1048
|
+
}
|
|
1049
|
+
return snapshot;
|
|
1050
|
+
}
|
|
1051
|
+
/** Keeps a consumer notification failure from corrupting successfully prepared content. */
|
|
1052
|
+
function notifySources(event) {
|
|
1053
|
+
const listeners = [...sourceListeners];
|
|
1054
|
+
for (const listener of listeners) try {
|
|
1055
|
+
listener(event);
|
|
1056
|
+
} catch (cause) {
|
|
1057
|
+
console.error("[content-query] Source subscriber failed", cause);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
986
1060
|
let registry = {};
|
|
987
1061
|
let loaders = /* @__PURE__ */ new Map();
|
|
988
1062
|
let configDependencies;
|
|
@@ -1047,6 +1121,10 @@ async function createSession(options) {
|
|
|
1047
1121
|
message: cause instanceof Error ? cause.message : "Content preparation failed."
|
|
1048
1122
|
});
|
|
1049
1123
|
options.change?.(state);
|
|
1124
|
+
notifySources({
|
|
1125
|
+
type: "error",
|
|
1126
|
+
error: state.error
|
|
1127
|
+
});
|
|
1050
1128
|
}
|
|
1051
1129
|
/** Aborts an owned subscription group and attempts every cleanup even when one fails. */
|
|
1052
1130
|
async function releaseResources(controller, handlers) {
|
|
@@ -1151,7 +1229,7 @@ async function createSession(options) {
|
|
|
1151
1229
|
registry = nextRegistry;
|
|
1152
1230
|
loaders = nextLoaders;
|
|
1153
1231
|
configDependencies = loaded.dependencies;
|
|
1154
|
-
state.registry =
|
|
1232
|
+
state.registry = queryRegistry(registry);
|
|
1155
1233
|
candidates.clear();
|
|
1156
1234
|
cache.clear();
|
|
1157
1235
|
loadFailures.clear();
|
|
@@ -1166,7 +1244,8 @@ async function createSession(options) {
|
|
|
1166
1244
|
const signal = attempt.signal;
|
|
1167
1245
|
loading = names;
|
|
1168
1246
|
const acquired = await Promise.allSettled([...names].map(async (name) => {
|
|
1169
|
-
|
|
1247
|
+
const entries = await loaders.get(name).load({ signal });
|
|
1248
|
+
return [name, normalizeSnapshot(entries, name)];
|
|
1170
1249
|
}));
|
|
1171
1250
|
if (disposed || currentGeneration !== generation) return;
|
|
1172
1251
|
loading = /* @__PURE__ */ new Set();
|
|
@@ -1191,6 +1270,10 @@ async function createSession(options) {
|
|
|
1191
1270
|
state.revision = revision;
|
|
1192
1271
|
state.error = void 0;
|
|
1193
1272
|
options.change?.(state);
|
|
1273
|
+
notifySources({
|
|
1274
|
+
type: "ready",
|
|
1275
|
+
snapshot: sources()
|
|
1276
|
+
});
|
|
1194
1277
|
} catch (cause) {
|
|
1195
1278
|
if (!disposed && currentGeneration === generation) fail(cause);
|
|
1196
1279
|
}
|
|
@@ -1244,6 +1327,7 @@ async function createSession(options) {
|
|
|
1244
1327
|
async function dispose() {
|
|
1245
1328
|
if (disposed) return;
|
|
1246
1329
|
disposed = true;
|
|
1330
|
+
sourceListeners.clear();
|
|
1247
1331
|
generation++;
|
|
1248
1332
|
attempt?.abort();
|
|
1249
1333
|
const failed = (await Promise.allSettled([watcher?.close() ?? Promise.resolve(), releaseSubscriptions()])).find((result) => result.status === "rejected");
|
|
@@ -1255,6 +1339,14 @@ async function createSession(options) {
|
|
|
1255
1339
|
}
|
|
1256
1340
|
const session = {
|
|
1257
1341
|
content: createRuntime(state),
|
|
1342
|
+
sources,
|
|
1343
|
+
subscribe(listener) {
|
|
1344
|
+
if (disposed) throw new Error("Content session has been disposed.");
|
|
1345
|
+
sourceListeners.add(listener);
|
|
1346
|
+
return () => {
|
|
1347
|
+
sourceListeners.delete(listener);
|
|
1348
|
+
};
|
|
1349
|
+
},
|
|
1258
1350
|
dispose
|
|
1259
1351
|
};
|
|
1260
1352
|
internals.set(session, {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { i as InvalidRelationships, o as ContentError, t as Content } from "./content-lwPxE9Yu.mjs";
|
|
2
|
+
import { i as CollectionRegistry } from "./types-BC8gVJ6u.mjs";
|
|
3
|
+
//#region src/preparation/session.d.ts
|
|
4
|
+
/** A local source captured by a successfully prepared content revision. */
|
|
5
|
+
type ContentSourceEntry = Readonly<{
|
|
6
|
+
collection: string;
|
|
7
|
+
id: string;
|
|
8
|
+
filePath?: string;
|
|
9
|
+
}>;
|
|
10
|
+
/** Format-neutral integration metadata; the revision token is opaque and session-scoped. */
|
|
11
|
+
type ContentSourceSnapshot = Readonly<{
|
|
12
|
+
revision: string;
|
|
13
|
+
entries: readonly ContentSourceEntry[];
|
|
14
|
+
}>;
|
|
15
|
+
/** Notifications are delivered only after atomic publication, or after a failed attempt. */
|
|
16
|
+
type ContentSourceEvent = {
|
|
17
|
+
type: "ready";
|
|
18
|
+
snapshot: ContentSourceSnapshot;
|
|
19
|
+
} | {
|
|
20
|
+
type: "error";
|
|
21
|
+
error: ContentError;
|
|
22
|
+
};
|
|
23
|
+
type ContentSession<Registry extends CollectionRegistry = {}> = {
|
|
24
|
+
content: Content<Registry>;
|
|
25
|
+
/** Reads the latest successful source snapshot; current preparation errors are rethrown. */
|
|
26
|
+
sources(): ContentSourceSnapshot;
|
|
27
|
+
/** Observes subsequent publications and failures; returns an idempotent unsubscribe function. */
|
|
28
|
+
subscribe(listener: (event: ContentSourceEvent) => void): () => void;
|
|
29
|
+
dispose(): Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/preparation/build.d.ts
|
|
33
|
+
/** Options shared by standalone content builds and host integrations. */
|
|
34
|
+
type BuildContentOptions = {
|
|
35
|
+
/** Exact config path; omission discovers one content-query.config.{ts,mts,cts,js,mjs,cjs} file. */
|
|
36
|
+
config?: string;
|
|
37
|
+
backend?: "in-memory" | "generated";
|
|
38
|
+
/** Generated backend output directory, relative to the application root; defaults to ./.content-query. */
|
|
39
|
+
outDir?: string;
|
|
40
|
+
watch?: boolean;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Builds content independently of a bundler; bind the registry through an erased type import.
|
|
44
|
+
* Generated watch sessions prune previous-run artifacts after their first successful publication.
|
|
45
|
+
*/
|
|
46
|
+
declare function buildContent<Registry extends CollectionRegistry = {}>(...arguments_: InvalidRelationships<Registry> extends never ? [options?: BuildContentOptions] : [invalidRegistry: never]): Promise<ContentSession<Registry>>;
|
|
47
|
+
//#endregion
|
|
48
|
+
export { ContentSourceEvent as a, ContentSourceEntry as i, buildContent as n, ContentSourceSnapshot as o, ContentSession as r, BuildContentOptions as t };
|
package/dist/build.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as buildContent, r as ContentSession, t as BuildContentOptions } from "./build-
|
|
2
|
-
export { type BuildContentOptions, type ContentSession, buildContent };
|
|
1
|
+
import { a as ContentSourceEvent, i as ContentSourceEntry, n as buildContent, o as ContentSourceSnapshot, r as ContentSession, t as BuildContentOptions } from "./build-jKnjDjQE.mjs";
|
|
2
|
+
export { type BuildContentOptions, type ContentSession, type ContentSourceEntry, type ContentSourceEvent, type ContentSourceSnapshot, buildContent };
|
package/dist/build.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { buildContent };
|
|
1
|
+
export { n as buildContent } from "./build-CI4s0oFP.mjs";
|
package/dist/cli.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as ContentError } from "./errors-
|
|
3
|
-
import { t as buildAtRoot } from "./build-
|
|
2
|
+
import { t as ContentError } from "./errors-C2iuBM0G.mjs";
|
|
3
|
+
import { t as buildAtRoot } from "./build-CI4s0oFP.mjs";
|
|
4
4
|
import { cac } from "cac";
|
|
5
5
|
//#region package.json
|
|
6
|
-
var version = "0.
|
|
6
|
+
var version = "0.3.0";
|
|
7
7
|
//#endregion
|
|
8
8
|
//#region src/integrations/cli/arguments.ts
|
|
9
9
|
/** Validates one path option after CAC's automatic numeric conversion. */
|
package/dist/computed.d.mts
CHANGED
|
@@ -1,37 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { m as MaybePromise } from "./types-FmUJ3cH1.mjs";
|
|
2
|
+
import { a as ComputedContext } from "./types-BC8gVJ6u.mjs";
|
|
3
3
|
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
4
|
-
|
|
5
4
|
//#region src/computed/text.d.ts
|
|
6
5
|
/** The syntax removed before computing text-derived fields; literal text preserves all characters. */
|
|
7
6
|
type TextFormat = "markdown" | "text" | "html";
|
|
8
7
|
/** Selects text from validated entry data or loader metadata instead of the captured body. */
|
|
9
8
|
type TextOptions<Data = Record<string, unknown>, Source = unknown> = {
|
|
10
|
-
/** Defaults to readBody(); undefined text is treated as an empty string. Async selectors are supported. */
|
|
9
|
+
/** Defaults to readBody(); undefined text is treated as an empty string. Async selectors are supported. */
|
|
10
|
+
from?: (context: ComputedContext<Data, Source>) => MaybePromise<string | undefined>;
|
|
11
|
+
/** Input syntax, defaulting to markdown. */
|
|
11
12
|
format?: TextFormat;
|
|
12
13
|
};
|
|
13
14
|
/** Controls the reading-speed estimate in addition to selecting its input text. */
|
|
14
15
|
type ReadingTimeOptions<Data = Record<string, unknown>, Source = unknown> = TextOptions<Data, Source> & {
|
|
15
|
-
/** A finite positive reading speed, defaulting to 200 words per minute. */
|
|
16
|
+
/** A finite positive reading speed, defaulting to 200 words per minute. */
|
|
17
|
+
wordsPerMinute?: number;
|
|
16
18
|
};
|
|
17
19
|
/** Controls the maximum excerpt size in addition to selecting its input text. */
|
|
18
20
|
type ExcerptOptions<Data = Record<string, unknown>, Source = unknown> = TextOptions<Data, Source> & {
|
|
19
|
-
/** A positive integer, defaulting to 160. Counts graphemes and includes the trailing ellipsis. */
|
|
21
|
+
/** A positive integer, defaulting to 160. Counts graphemes and includes the trailing ellipsis. */
|
|
22
|
+
maxLength?: number;
|
|
20
23
|
};
|
|
21
24
|
/** Creates a numeric computed field counting normalized text with reading-time's word boundaries. */
|
|
22
|
-
declare function wordCount<Data = Record<string, unknown>, Source = unknown>(options?: TextOptions<Data, Source>): {
|
|
23
|
-
schema: StandardSchemaV1<unknown, number>;
|
|
25
|
+
export declare function wordCount<Data = Record<string, unknown>, Source = unknown>(options?: TextOptions<Data, Source>): {
|
|
26
|
+
schema: StandardSchemaV1<unknown, number>;
|
|
27
|
+
/** Resolves selected text during preparation and honors cancellation around asynchronous input. */
|
|
24
28
|
resolve(context: ComputedContext<Data, Source>): Promise<number>;
|
|
25
29
|
};
|
|
26
30
|
/** Creates a numeric computed field estimating whole reading minutes, rounded up; empty text returns zero. */
|
|
27
|
-
declare function readingTime<Data = Record<string, unknown>, Source = unknown>(options?: ReadingTimeOptions<Data, Source>): {
|
|
28
|
-
schema: StandardSchemaV1<unknown, number>;
|
|
31
|
+
export declare function readingTime<Data = Record<string, unknown>, Source = unknown>(options?: ReadingTimeOptions<Data, Source>): {
|
|
32
|
+
schema: StandardSchemaV1<unknown, number>;
|
|
33
|
+
/** Resolves selected text during preparation and honors cancellation around asynchronous input. */
|
|
29
34
|
resolve(context: ComputedContext<Data, Source>): Promise<number>;
|
|
30
35
|
};
|
|
31
36
|
/** Creates a string computed field containing a normalized plain-text excerpt with a bounded length. */
|
|
32
|
-
declare function excerpt<Data = Record<string, unknown>, Source = unknown>(options?: ExcerptOptions<Data, Source>): {
|
|
33
|
-
schema: StandardSchemaV1<unknown, string>;
|
|
37
|
+
export declare function excerpt<Data = Record<string, unknown>, Source = unknown>(options?: ExcerptOptions<Data, Source>): {
|
|
38
|
+
schema: StandardSchemaV1<unknown, string>;
|
|
39
|
+
/** Resolves selected text during preparation and honors cancellation around asynchronous input. */
|
|
34
40
|
resolve(context: ComputedContext<Data, Source>): Promise<string>;
|
|
35
41
|
};
|
|
36
42
|
//#endregion
|
|
37
|
-
export {
|
|
43
|
+
export type { ExcerptOptions, ReadingTimeOptions, TextFormat, TextOptions };
|
package/dist/computed.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { t as assertServerOnly } from "./server-only-
|
|
2
|
-
import { n as number, r as string } from "./schema-
|
|
3
|
-
import { t as src_default } from "./dist-
|
|
1
|
+
import { t as assertServerOnly } from "./server-only-CIJUlyL-.mjs";
|
|
2
|
+
import { n as number, r as string } from "./schema-C5Str-E8.mjs";
|
|
3
|
+
import { t as src_default } from "./dist-C7_FOzY_.mjs";
|
|
4
4
|
import getReadingTime from "reading-time";
|
|
5
5
|
import { Parser } from "htmlparser2";
|
|
6
6
|
//#region src/computed/plain-text.ts
|
|
7
7
|
const markdown = new src_default({ html: true });
|
|
8
|
-
const ignoredElements = new Set([
|
|
8
|
+
const ignoredElements = /* @__PURE__ */ new Set([
|
|
9
9
|
"head",
|
|
10
10
|
"script",
|
|
11
11
|
"style",
|
|
12
12
|
"template",
|
|
13
13
|
"noscript"
|
|
14
14
|
]);
|
|
15
|
-
const blockElements = new Set([
|
|
15
|
+
const blockElements = /* @__PURE__ */ new Set([
|
|
16
16
|
"address",
|
|
17
17
|
"article",
|
|
18
18
|
"aside",
|
|
@@ -96,6 +96,7 @@ function textField(schema, options, compute) {
|
|
|
96
96
|
if (from !== void 0 && typeof from !== "function") throw new TypeError("Text from must be a function.");
|
|
97
97
|
return {
|
|
98
98
|
schema,
|
|
99
|
+
/** Resolves selected text during preparation and honors cancellation around asynchronous input. */
|
|
99
100
|
async resolve(context) {
|
|
100
101
|
context.signal.throwIfAborted();
|
|
101
102
|
const input = await (from ? from(context) : context.readBody());
|