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.
Files changed (48) hide show
  1. package/README.md +29 -2
  2. package/dist/{build-C5ef0kkR.mjs → build-CI4s0oFP.mjs} +122 -30
  3. package/dist/build-jKnjDjQE.d.mts +48 -0
  4. package/dist/build.d.mts +2 -2
  5. package/dist/build.mjs +1 -2
  6. package/dist/cli.d.mts +1 -1
  7. package/dist/cli.mjs +3 -3
  8. package/dist/computed.d.mts +19 -13
  9. package/dist/computed.mjs +6 -5
  10. package/dist/{content-Dj2fQCJ5.mjs → content-N1gYshjg.mjs} +203 -77
  11. package/dist/{content-Crag1Uch.d.mts → content-lwPxE9Yu.d.mts} +40 -30
  12. package/dist/{descriptor-Xbo9u5eR.d.mts → descriptor-BUPT2J7X.d.mts} +1 -2
  13. package/dist/{diagnostics-Dol6ANtq.mjs → diagnostics-Cu94mZhl.mjs} +16 -15
  14. package/dist/{dist-BQGVLZyl.mjs → dist-C7_FOzY_.mjs} +21 -12
  15. package/dist/{glob-DJmebCMM.mjs → glob-Bv0dpvCQ.mjs} +3 -3
  16. package/dist/index.d.mts +35 -16
  17. package/dist/index.mjs +7 -4
  18. package/dist/internal/markdown.d.mts +3 -5
  19. package/dist/internal/markdown.mjs +1 -2
  20. package/dist/internal.d.mts +12 -12
  21. package/dist/internal.mjs +7 -7
  22. package/dist/loaders.d.mts +5 -6
  23. package/dist/loaders.mjs +6 -5
  24. package/dist/{markdown-CZBL4Ux_.mjs → markdown-CHvBBJNa.mjs} +2 -2
  25. package/dist/next.d.mts +3 -4
  26. package/dist/next.mjs +2 -2
  27. package/dist/{operators-BQpLfJlx.mjs → operators-UqWUhKQJ.mjs} +2 -2
  28. package/dist/query.d.mts +22 -23
  29. package/dist/query.mjs +1 -2
  30. package/dist/{relational-types-DLniKi5S.d.mts → relational-types-B45zM34W.d.mts} +62 -29
  31. package/dist/{chunk-CJs4Fogs.mjs → rolldown-runtime-Bg1v5YCA.mjs} +2 -2
  32. package/dist/runtime.d.mts +2 -2
  33. package/dist/runtime.mjs +3 -4
  34. package/dist/{schema-CnCbMosm.mjs → schema-C5Str-E8.mjs} +1 -1
  35. package/dist/schema.d.mts +0 -2
  36. package/dist/schema.mjs +1 -2
  37. package/dist/{types-BPh2cBPs.d.mts → types-BC8gVJ6u.d.mts} +27 -6
  38. package/dist/{types-hmDOw87q.d.mts → types-FmUJ3cH1.d.mts} +23 -6
  39. package/dist/{validation-BvvjSUFc.mjs → validation-D_XdE1pF.mjs} +2 -2
  40. package/dist/{values-B7lo_2_x.mjs → values-BbTV4QT-.mjs} +109 -108
  41. package/dist/vite.d.mts +14 -4
  42. package/dist/vite.mjs +28 -5
  43. package/package.json +7 -5
  44. package/dist/build-DSyoRxkZ.d.mts +0 -24
  45. /package/dist/{errors-C8lWyiy2.mjs → errors-C2iuBM0G.mjs} +0 -0
  46. /package/dist/{github-slugger-BCAkwEOL.mjs → github-slugger-BMgR_Q0I.mjs} +0 -0
  47. /package/dist/{server-only-CIE-FHx0.mjs → server-only-CIJUlyL-.mjs} +0 -0
  48. /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.** Get HTML, headings, and syntax highlighting separately from metadata queries.
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-BvvjSUFc.mjs";
2
- import { i as groupDiagnostics, n as contentError, o as withContext, t as ContentError } from "./errors-C8lWyiy2.mjs";
3
- import { i as detach, l as valuePath, o as encode } from "./values-B7lo_2_x.mjs";
4
- import { t as assertServerOnly } from "./server-only-CIE-FHx0.mjs";
5
- import { a as CODEC_VERSION, i as prepareRelationships, n as createRuntime, r as isRelationship } from "./content-Dj2fQCJ5.mjs";
6
- import { t as renderMarkdown } from "./markdown-CZBL4Ux_.mjs";
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: 1,
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.payloads) {
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}, payloads: ${loading} };\n`;
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 payloads = /* @__PURE__ */ new Map();
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) => name === "id" || name === "_adjacent") || new Set(declarationNames).size !== declarationNames.length || Object.values(relationships).some((relation) => !isRelationship(relation))) {
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) payloads.set(entry.id, async () => previous.payload);
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) if (development || descriptor.options.deferRender === true) payload.markdown = entry.body;
310
- else try {
311
- payload.rendered = await renderMarkdown(entry.body);
312
- } catch (cause) {
313
- throw new ContentError({
314
- ...context,
315
- code: "render-error",
316
- message: "Eager Markdown compilation failed.",
317
- cause
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) payloads.set(entry.id, async () => payload);
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
- payloads,
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.entries(collections).map(([name, collection]) => [name, { relationships: collection.relationships }])),
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 = Object.fromEntries(Object.entries(registry).map(([name, definition]) => [name, { relationships: definition.relationships ?? {} }]));
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
- return [name, normalizeSnapshot(await loaders.get(name).load({ signal }), name)];
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-DSyoRxkZ.mjs";
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
- import { n as buildContent } from "./build-C5ef0kkR.mjs";
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-C8lWyiy2.mjs";
3
- import { t as buildAtRoot } from "./build-C5ef0kkR.mjs";
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.1.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. */
@@ -1,37 +1,43 @@
1
- import { f as MaybePromise } from "./types-hmDOw87q.mjs";
2
- import { r as ComputedContext } from "./types-BPh2cBPs.mjs";
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. */from?: (context: ComputedContext<Data, Source>) => MaybePromise<string | undefined>; /** Input syntax, defaulting to markdown. */
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. */wordsPerMinute?: number;
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. */maxLength?: number;
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>; /** Resolves selected text during preparation and honors cancellation around asynchronous input. */
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>; /** Resolves selected text during preparation and honors cancellation around asynchronous input. */
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>; /** Resolves selected text during preparation and honors cancellation around asynchronous input. */
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 { type ExcerptOptions, type ReadingTimeOptions, type TextFormat, type TextOptions, excerpt, readingTime, wordCount };
43
+ export type { ExcerptOptions, ReadingTimeOptions, TextFormat, TextOptions };
package/dist/computed.mjs CHANGED
@@ -1,18 +1,18 @@
1
- import { t as assertServerOnly } from "./server-only-CIE-FHx0.mjs";
2
- import { n as number, r as string } from "./schema-CnCbMosm.mjs";
3
- import { t as src_default } from "./dist-BQGVLZyl.mjs";
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());