content-query 0.4.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 +38 -27
- package/dist/{build-CcqIpkes.mjs → build-DWpB69_c.mjs} +138 -51
- package/dist/{build-DL1GzmRy.d.mts → build-DfoncKL8.d.mts} +10 -5
- package/dist/build.d.mts +1 -1
- package/dist/build.mjs +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{content-BXrgP7nL.mjs → collection-Cb5eWGnv.mjs} +871 -407
- package/dist/collection-types-CsQtGw7_.d.mts +119 -0
- package/dist/computed.d.mts +1 -1
- package/dist/index.d.mts +17 -2
- package/dist/internal.d.mts +48 -8
- package/dist/internal.mjs +398 -17
- package/dist/next.d.mts +3 -2
- package/dist/next.mjs +66 -13
- package/dist/{operators-Cwy8Kx4o.mjs → operators-D-w1-gDg.mjs} +134 -2
- package/dist/query-style-C2FcT1f9.d.mts +23 -0
- package/dist/query.d.mts +2 -2
- package/dist/query.mjs +1 -1
- package/dist/{relational-types-C8xJtLG5.d.mts → relational-types-CTcalUNW.d.mts} +135 -19
- package/dist/revision-DBwU-aC9.d.mts +56 -0
- package/dist/runtime.d.mts +3 -3
- package/dist/{types-DbFfF6JE.d.mts → types-Dz1vTXa2.d.mts} +35 -2
- package/dist/vite.d.mts +1 -1
- package/dist/vite.mjs +76 -34
- package/package.json +1 -1
- package/dist/content-CwTKqRs9.d.mts +0 -134
- package/dist/query-style-eXENQJ0O.d.mts +0 -22
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ 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
|
+
- **Per-collection entries.** Import each collection from its own generated module with named types such as `ContentPost`, so a route bundles and traces only the collections it uses.
|
|
12
13
|
- **Markdown rendering.** Request HTML, headings, and syntax highlighting as query payloads, loaded only when a query asks for them.
|
|
13
14
|
- **Custom payloads.** Declare schema-validated per-entry data, optionally by variant such as locale, resolved during the build from tracked related files, and loaded only when a query names it. Development edits re-resolve only the variants whose files changed.
|
|
14
15
|
- **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.
|
|
@@ -45,55 +46,65 @@ Add Markdown files with `title` frontmatter under `content/posts/`, then build:
|
|
|
45
46
|
npx content-query build
|
|
46
47
|
```
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
Each collection gets its own generated entry, which exports the collection under its name. Query it from server code:
|
|
49
50
|
|
|
50
51
|
```ts
|
|
51
|
-
import {
|
|
52
|
+
import { posts } from "./.content-query/collections/posts/server.js";
|
|
52
53
|
import { eq } from "content-query/query";
|
|
53
54
|
|
|
54
|
-
const
|
|
55
|
-
.from("posts")
|
|
55
|
+
const summaries = await posts
|
|
56
56
|
.select("id", "title")
|
|
57
57
|
.where((fields) => eq(fields.draft, false))
|
|
58
58
|
.orderBy("title");
|
|
59
59
|
|
|
60
|
-
const post = await
|
|
61
|
-
where: { id:
|
|
60
|
+
const post = await posts.findFirst({
|
|
61
|
+
where: { id: summaries[0]!.id },
|
|
62
62
|
payloads: { _rendered: true },
|
|
63
63
|
});
|
|
64
64
|
const { html, headings } = post!._payloads._rendered;
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Built-in payloads are `_body` and `_rendered`. JSON, YAML, and TOML sources and `file()` collections have none, and a custom `BodyLoader` offers only `_body`. A query for a missing entry returns `undefined`.
|
|
68
|
+
|
|
69
|
+
An entry imports only its own collection's data, so a route that imports `posts` never bundles or traces another collection. With Vite, import from `content-query:collections/posts`. Elsewhere, such as in Next.js, map the entries in `package.json` to import `{ posts }` from `#content-query/posts` at any directory depth:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"imports": {
|
|
74
|
+
"#content-query/*": "./.content-query/collections/*/server.js"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
67
79
|
Queries return only the data they select, not even `id` implicitly. A query must select fields, include a relationship, request a payload, or select adjacent entries; otherwise it fails to type-check and throws an `invalid-query` error. Counts need no selection.
|
|
68
80
|
|
|
69
|
-
|
|
81
|
+
To follow a relationship, pass its target collection to `using()`. Nothing related loads implicitly:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { authors } from "#content-query/authors";
|
|
85
|
+
import { posts } from "#content-query/posts";
|
|
70
86
|
|
|
71
|
-
|
|
87
|
+
const withAuthors = await posts.using(authors).findMany({
|
|
88
|
+
columns: { title: true },
|
|
89
|
+
with: { author: { columns: { name: true } } },
|
|
90
|
+
});
|
|
91
|
+
```
|
|
72
92
|
|
|
73
|
-
|
|
93
|
+
A relationship whose target was not supplied is a type error and throws `invalid-query`. Nested relationships need every collection they reach, such as `posts.using(authors, publishers)`. Self-relationships need nothing.
|
|
74
94
|
|
|
75
|
-
|
|
76
|
-
| --------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
77
|
-
| `content.select("id", "title").from("posts")` | `content.from("posts").select("id", "title")` |
|
|
78
|
-
| `content.select().from("posts")` | `content.from("posts").select(...)` listing the fields used |
|
|
79
|
-
| `findMany()` or `findMany({ where })` | Add `columns: { field: true }` for each field used |
|
|
80
|
-
| `columns: { draft: false }` | List the included fields with `true` |
|
|
81
|
-
| `with: { author: true }` | `with: { author: { columns: { name: true } } }` |
|
|
82
|
-
| `adjacent: true` or `.selectAdjacent()` | `adjacent: { scope: "results", columns: { id: true } }` or `.selectAdjacent("id")` |
|
|
95
|
+
Each entry also exports the collection's types. `ContentPost` is a full entry; pass the selected fields to type query results, such as props for a list component:
|
|
83
96
|
|
|
84
|
-
|
|
97
|
+
```ts
|
|
98
|
+
import type { ContentPost } from "#content-query/posts";
|
|
85
99
|
|
|
86
|
-
|
|
100
|
+
type PostLink = ContentPost<"id" | "title">; // What select("id", "title") returns.
|
|
101
|
+
```
|
|
87
102
|
|
|
88
|
-
|
|
103
|
+
Type names are `Content` plus the export name, capitalized and made singular by dropping one trailing `s` (`jobs` gives `ContentJob`, `coding` gives `ContentCoding`). Set `typeName: "Category"` in `defineCollection()` for irregular plurals. Declared payloads are typed as `ContentPostPayload<"detail">`. Collection names that are not identifiers are converted for the export and import path, such as `blog-posts` to `blogPosts`; two collections that would share an export or type name fail the build.
|
|
89
104
|
|
|
90
|
-
|
|
91
|
-
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
92
|
-
| `await content.readBody("posts", id)` | `(await content.query.posts.findFirst({ where: { id }, payloads: { _body: true } }))?._payloads._body` |
|
|
93
|
-
| `await content.render("posts", id)` | `(await content.query.posts.findFirst({ where: { id }, payloads: { _rendered: true } }))?._payloads._rendered` |
|
|
94
|
-
| Fluent queries | Add `.withPayload("_rendered")` or `.withPayload("_body")` before the terminal. |
|
|
105
|
+
Paginate root queries in one call. `paginate({ page: 2, size: 10 })` returns `items`, `total`, `page`, `pageCount`, `hasNextPage`, and `hasPreviousPage` for numbered archive pages. `paginate({ size: 10, after: previous.endCursor })` returns cursor pages with `startCursor` and `endCursor` for "load more" lists. Both work in fluent queries and in `posts.paginate()`; see [pagination](https://github.com/yangshun/content-query/blob/main/docs/API.md#pagination).
|
|
95
106
|
|
|
96
|
-
|
|
107
|
+
Add `.content-query/` to `.gitignore`. Follow the quick start for a complete walkthrough, or choose a framework example below.
|
|
97
108
|
|
|
98
109
|
## Documentation
|
|
99
110
|
|
|
@@ -104,7 +115,7 @@ Built-in payload names start with an underscore: `_body` and `_rendered`. A quer
|
|
|
104
115
|
|
|
105
116
|
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.
|
|
106
117
|
|
|
107
|
-
To allow only one query API, export `queryStyle` from the config: `export const queryStyle = "relational";` keeps `
|
|
118
|
+
To allow only one query API, export `queryStyle` from the config: `export const queryStyle = "relational";` keeps `posts.findMany()`, `findFirst()`, `paginate()`, and `count(options)`, and `"fluent"` keeps `posts.select()` and the other fluent starters. `count()` without options works in both. Generated types report the disabled API in the editor, and the runtime throws an `invalid-query` error for it. See [query style](https://github.com/yangshun/content-query/blob/main/docs/API.md#query-style).
|
|
108
119
|
|
|
109
120
|
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.
|
|
110
121
|
|
|
@@ -3,7 +3,8 @@ import { i as groupDiagnostics, n as contentError, o as payloadIdentity, r as de
|
|
|
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
5
|
import { n as loadFile } from "./file-DSOcdFj0.mjs";
|
|
6
|
-
import {
|
|
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";
|
|
7
8
|
import { t as renderMarkdown } from "./markdown-CHvBBJNa.mjs";
|
|
8
9
|
import { access, link, mkdir, readFile, readdir, realpath, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
9
10
|
import path from "node:path";
|
|
@@ -19,7 +20,13 @@ import picomatch from "picomatch";
|
|
|
19
20
|
function digest(value) {
|
|
20
21
|
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
21
22
|
}
|
|
22
|
-
/**
|
|
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
|
+
*/
|
|
23
30
|
function directoryName(name) {
|
|
24
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")}`;
|
|
25
32
|
}
|
|
@@ -30,20 +37,43 @@ function compactJson(source) {
|
|
|
30
37
|
const compressed = JSON.stringify(gzipSync(source, { level: 1 }).toString("base64"));
|
|
31
38
|
return compressed.length < bytes * .9 ? compressed : void 0;
|
|
32
39
|
}
|
|
33
|
-
/**
|
|
34
|
-
|
|
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 } = {}) {
|
|
35
67
|
const modules = /* @__PURE__ */ new Map();
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
...revision.queryStyle ? { queryStyle: revision.queryStyle } : {}
|
|
41
|
-
};
|
|
42
|
-
const imports = [];
|
|
43
|
-
const names = [];
|
|
44
|
-
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;
|
|
45
72
|
for (const [name, collection] of Object.entries(revision.collections)) {
|
|
46
|
-
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;
|
|
47
77
|
const payloads = [];
|
|
48
78
|
const chunks = [];
|
|
49
79
|
let batch = [];
|
|
@@ -67,7 +97,7 @@ async function revisionModules(revision, { payloadChunkBytes = 0 } = {}) {
|
|
|
67
97
|
for (const [id, load] of collection.builtins) {
|
|
68
98
|
const payload = await load();
|
|
69
99
|
const encoded = encode(payload);
|
|
70
|
-
|
|
100
|
+
markdown ||= payload.markdown !== void 0;
|
|
71
101
|
if (payloadChunkBytes) {
|
|
72
102
|
const bytes = Buffer.byteLength(`[${JSON.stringify(id)}]: ${JSON.stringify(encoded)},\n`);
|
|
73
103
|
if (batchBytes && batchBytes + bytes > payloadChunkBytes) flushBatch();
|
|
@@ -114,53 +144,91 @@ async function revisionModules(revision, { payloadChunkBytes = 0 } = {}) {
|
|
|
114
144
|
const code = `${loaders}export default { records: ${compressedRecords ? `decodeCompressed(${compressedRecords})` : recordSource}, builtins: ${loading}${custom.length ? `, payloads: {\n${custom.join(",\n")}\n}` : ""} };\n`;
|
|
115
145
|
const index = `index-${digest(code)}.js`;
|
|
116
146
|
modules.set(`${directory}/${index}`, code);
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
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`);
|
|
121
173
|
}
|
|
122
|
-
modules.set("manifest.js", `export default JSON.parse(${JSON.stringify(JSON.stringify(manifest))});\n`);
|
|
123
|
-
const renderer = needsRenderer ? `, body => import("content-query/internal/markdown").then(module => module.renderMarkdown(body))` : "";
|
|
124
|
-
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`);
|
|
125
174
|
return modules;
|
|
126
175
|
}
|
|
127
176
|
//#endregion
|
|
128
177
|
//#region src/artifacts/declarations.ts
|
|
178
|
+
/** Declares the config-bound `Registry` and `ConfiguredQueryStyle` aliases for one directory. */
|
|
179
|
+
function configTypes(directory, config) {
|
|
180
|
+
let relative = path.relative(directory, config).replace(/\\/g, "/");
|
|
181
|
+
if (!relative.startsWith(".")) relative = `./${relative}`;
|
|
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;`];
|
|
184
|
+
}
|
|
129
185
|
/**
|
|
130
|
-
* Generates type-only
|
|
186
|
+
* Generates type-only bindings for one collection entry without loading schemas or adding runtime
|
|
187
|
+
* imports.
|
|
131
188
|
*
|
|
132
189
|
* The registry and the optional `queryStyle` both come from the config module's types, so a
|
|
133
|
-
* literal `export const queryStyle = "relational"` narrows
|
|
190
|
+
* literal `export const queryStyle = "relational"` narrows the export without a rebuild. A missing
|
|
134
191
|
* export, or one typed as the whole `QueryStyle` union, allows both query styles.
|
|
135
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
|
+
*
|
|
136
197
|
* @param directory - Directory that will contain the declaration file.
|
|
137
198
|
* @param config - Absolute path of content-query.config.
|
|
138
|
-
* @
|
|
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`.
|
|
139
202
|
*/
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
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}>`;
|
|
144
207
|
return [
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
`export
|
|
148
|
-
|
|
149
|
-
`export type
|
|
150
|
-
|
|
151
|
-
`export
|
|
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>;`,
|
|
152
215
|
``
|
|
153
216
|
].join("\n");
|
|
154
217
|
}
|
|
155
218
|
/**
|
|
156
|
-
* Declares project-bound virtual
|
|
219
|
+
* Declares project-bound virtual collection modules and the framework-neutral development bridge.
|
|
157
220
|
*
|
|
158
221
|
* @param directory - Directory that will contain the declaration file.
|
|
159
222
|
* @param config - Absolute path of content-query.config.
|
|
160
|
-
* @
|
|
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.
|
|
161
227
|
*/
|
|
162
|
-
function virtualDeclarations(directory, config) {
|
|
163
|
-
|
|
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`;
|
|
164
232
|
}
|
|
165
233
|
//#endregion
|
|
166
234
|
//#region src/artifacts/write.ts
|
|
@@ -213,6 +281,8 @@ async function writeImmutable(filename, source) {
|
|
|
213
281
|
}
|
|
214
282
|
}
|
|
215
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$/;
|
|
216
286
|
/** Lists one directory's entries, treating a missing directory or a file as empty. */
|
|
217
287
|
async function entriesOf(directory) {
|
|
218
288
|
try {
|
|
@@ -250,20 +320,35 @@ async function pruneArtifacts(outDir, modules) {
|
|
|
250
320
|
const payloadDirectories = await entriesOf(payloads);
|
|
251
321
|
const candidates = [...(await entriesOf(owned)).map((name) => `collections/${directory}/${name}`), ...(await entriesOf(entries)).map((name) => `collections/${directory}/entries/${name}`)];
|
|
252
322
|
for (const payload of payloadDirectories) candidates.push(...(await entriesOf(path.join(payloads, payload))).map((name) => `collections/${directory}/payloads/${payload}/${name}`));
|
|
253
|
-
for (const filename of candidates)
|
|
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
|
+
}
|
|
254
328
|
await removeIfEmpty(entries);
|
|
255
329
|
for (const payload of payloadDirectories) await removeIfEmpty(path.join(payloads, payload));
|
|
256
330
|
await removeIfEmpty(payloads);
|
|
257
331
|
await removeIfEmpty(owned);
|
|
258
332
|
}
|
|
259
333
|
}
|
|
260
|
-
/**
|
|
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
|
+
*/
|
|
261
342
|
async function writeArtifacts(revision, outDir, config, { signal, prune = false } = {}) {
|
|
262
343
|
const modules = await revisionModules(revision);
|
|
344
|
+
const names = collectionNames(revision.registry);
|
|
263
345
|
await writeChanged(path.join(outDir, "package.json"), "{\"type\":\"module\",\"private\":true}\n");
|
|
264
|
-
await writeChanged(path.join(outDir, "
|
|
265
|
-
|
|
266
|
-
|
|
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));
|
|
267
352
|
const directories = new Set(staged.map(([filename]) => path.dirname(path.join(outDir, filename))));
|
|
268
353
|
await Promise.all([...directories].map(async (directory) => mkdir(directory, { recursive: true })));
|
|
269
354
|
const pending = staged.values();
|
|
@@ -275,7 +360,7 @@ async function writeArtifacts(revision, outDir, config, { signal, prune = false
|
|
|
275
360
|
}
|
|
276
361
|
}))).find((outcome) => outcome.status === "rejected");
|
|
277
362
|
if (failure?.status === "rejected") throw failure.reason;
|
|
278
|
-
await writeChanged(path.join(outDir,
|
|
363
|
+
for (const [filename, code] of modules) if (collectionEntry.test(filename)) await writeChanged(path.join(outDir, filename), code, signal);
|
|
279
364
|
if (prune) await pruneArtifacts(outDir, modules);
|
|
280
365
|
}
|
|
281
366
|
//#endregion
|
|
@@ -319,7 +404,8 @@ function queryRegistry(registry) {
|
|
|
319
404
|
payloads: {
|
|
320
405
|
...hasBuiltinPayloads(definition) ? builtinPayloads : {},
|
|
321
406
|
...customPayloads(definition)
|
|
322
|
-
}
|
|
407
|
+
},
|
|
408
|
+
...definition.typeName === void 0 ? {} : { typeName: definition.typeName }
|
|
323
409
|
}]));
|
|
324
410
|
}
|
|
325
411
|
//#endregion
|
|
@@ -872,6 +958,7 @@ async function loadRegistry(config) {
|
|
|
872
958
|
const registry = imported.collections;
|
|
873
959
|
if (!registry || typeof registry !== "object" || Array.isArray(registry)) throw new Error("content-query.config must export a collections registry.");
|
|
874
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);
|
|
875
962
|
return {
|
|
876
963
|
registry,
|
|
877
964
|
dependencies,
|
|
@@ -1626,8 +1713,8 @@ function sessionInternals(session) {
|
|
|
1626
1713
|
* Creates a preparation session with ordered source commands and atomic revision publication.
|
|
1627
1714
|
*
|
|
1628
1715
|
* @param options - Root, configuration, watch, and publication settings.
|
|
1629
|
-
* @returns A session whose `
|
|
1630
|
-
* configured `queryStyle`, which the runtime enforces.
|
|
1716
|
+
* @returns A session whose `collections` are typed by `Style`; each published revision carries
|
|
1717
|
+
* the configured `queryStyle`, which the runtime enforces.
|
|
1631
1718
|
*/
|
|
1632
1719
|
async function createSession(options) {
|
|
1633
1720
|
options = {
|
|
@@ -1996,7 +2083,7 @@ async function createSession(options) {
|
|
|
1996
2083
|
});
|
|
1997
2084
|
}
|
|
1998
2085
|
const session = {
|
|
1999
|
-
|
|
2086
|
+
collections: createCollections(state),
|
|
2000
2087
|
sources,
|
|
2001
2088
|
subscribe(listener) {
|
|
2002
2089
|
if (disposed) throw new Error("Content session has been disposed.");
|
|
@@ -2160,4 +2247,4 @@ async function buildAtRoot(root, options, change) {
|
|
|
2160
2247
|
});
|
|
2161
2248
|
}
|
|
2162
2249
|
//#endregion
|
|
2163
|
-
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 };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as CollectionRegistry } from "./types-
|
|
3
|
-
import { n as QueryStyle } from "./query-style-
|
|
1
|
+
import { a as InvalidRelationships, r as CollectionHandles, s as ContentError } from "./collection-types-CsQtGw7_.mjs";
|
|
2
|
+
import { i as CollectionRegistry } from "./types-Dz1vTXa2.mjs";
|
|
3
|
+
import { n as QueryStyle } from "./query-style-C2FcT1f9.mjs";
|
|
4
|
+
import "./revision-DBwU-aC9.mjs";
|
|
4
5
|
//#region src/preparation/session.d.ts
|
|
5
6
|
/** A local source captured by a successfully prepared content revision. */
|
|
6
7
|
type ContentSourceEntry = Readonly<{
|
|
@@ -22,8 +23,12 @@ type ContentSourceEvent = {
|
|
|
22
23
|
error: ContentError;
|
|
23
24
|
};
|
|
24
25
|
type ContentSession<Registry extends CollectionRegistry = {}, Style extends QueryStyle = QueryStyle> = {
|
|
25
|
-
/**
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* One query handle per collection, keyed by collection name, like the generated entries export.
|
|
28
|
+
* Each loads only its own collection; pass others to `using()` to follow relationships. Members
|
|
29
|
+
* are typed by `Style`; the configured `queryStyle` is enforced at runtime.
|
|
30
|
+
*/
|
|
31
|
+
collections: CollectionHandles<Registry, Style>;
|
|
27
32
|
/** Reads the latest successful source snapshot; current preparation errors are rethrown. */
|
|
28
33
|
sources(): ContentSourceSnapshot;
|
|
29
34
|
/** Observes subsequent publications and failures; returns an idempotent unsubscribe function. */
|
package/dist/build.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as ContentSourceEvent, i as ContentSourceEntry, n as buildContent, o as ContentSourceSnapshot, r as ContentSession, t as BuildContentOptions } from "./build-
|
|
1
|
+
import { a as ContentSourceEvent, i as ContentSourceEntry, n as buildContent, o as ContentSourceSnapshot, r as ContentSession, t as BuildContentOptions } from "./build-DfoncKL8.mjs";
|
|
2
2
|
export { type BuildContentOptions, type ContentSession, type ContentSourceEntry, type ContentSourceEvent, type ContentSourceSnapshot, buildContent };
|
package/dist/build.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { n as buildContent } from "./build-
|
|
1
|
+
export { n as buildContent } from "./build-DWpB69_c.mjs";
|
package/dist/cli.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { o as payloadIdentity, t as ContentError } from "./errors-Bfd0R4_P.mjs";
|
|
3
|
-
import { t as buildAtRoot } from "./build-
|
|
3
|
+
import { t as buildAtRoot } from "./build-DWpB69_c.mjs";
|
|
4
4
|
import { cac } from "cac";
|
|
5
5
|
//#region package.json
|
|
6
|
-
var version = "0.
|
|
6
|
+
var version = "0.5.0";
|
|
7
7
|
//#endregion
|
|
8
8
|
//#region src/integrations/cli/arguments.ts
|
|
9
9
|
/** Validates one path option after CAC's automatic numeric conversion. */
|