@quillmark/quiver 0.13.0 → 0.16.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 +137 -19
- package/dist/bin/quiver.js +27 -15
- package/dist/build.d.ts +1 -1
- package/dist/build.js +3 -3
- package/dist/built-loader.d.ts +7 -1
- package/dist/built-loader.js +45 -30
- package/dist/index.js +5 -3
- package/dist/node.d.ts +1 -1
- package/dist/preview.d.ts +9 -7
- package/dist/preview.js +16 -11
- package/dist/quiver.d.ts +26 -23
- package/dist/quiver.js +77 -56
- package/dist/testing.d.ts +6 -7
- package/dist/testing.js +15 -7
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -29,6 +29,24 @@ This keeps the author flow to a single command (`npm publish` or `git tag`)
|
|
|
29
29
|
and puts the deployment-topology decision where it belongs: with the
|
|
30
30
|
consumer.
|
|
31
31
|
|
|
32
|
+
## The render model
|
|
33
|
+
|
|
34
|
+
`@quillmark/quiver` produces quills; `@quillmark/wasm` renders them.
|
|
35
|
+
|
|
36
|
+
- `quiver.getQuill(ref)` returns a core `Quill` — engine-free, portable data,
|
|
37
|
+
good for schema inspection, validation, blueprint access, and seeding. It is
|
|
38
|
+
the **only** way to obtain a quill from a quiver.
|
|
39
|
+
- `const engine = new Engine()` (from the `@quillmark/wasm` root) renders it:
|
|
40
|
+
`await engine.render(quill, doc, opts?)`. The Engine routes on
|
|
41
|
+
`quill.backendId`, lazily loads that backend, clones the quill and document
|
|
42
|
+
into the backend's WASM memory, renders, and frees the clones — so a core
|
|
43
|
+
`Quill` passes straight to `engine.render` with no boundary-crossing step.
|
|
44
|
+
|
|
45
|
+
`Engine.render`, `open`, `supportedFormats`, and `supportsCanvas` are all
|
|
46
|
+
**async** — always `await` them. The canonical `Quill`/`Document`/`Engine`
|
|
47
|
+
types are not re-exported from `@quillmark/quiver`; import them straight from
|
|
48
|
+
the `@quillmark/wasm` peer dependency, which is the single source of truth.
|
|
49
|
+
|
|
32
50
|
## Authoring a quiver
|
|
33
51
|
|
|
34
52
|
Lay out the source per the spec, then publish to npm (or push a git tag):
|
|
@@ -50,6 +68,14 @@ consumer's build. The harness uses `node:test` (built into Node 18+); no extra
|
|
|
50
68
|
test-runner dependency required. If you prefer vitest/jest/mocha, write a
|
|
51
69
|
12-line loop against the main API instead.
|
|
52
70
|
|
|
71
|
+
```ts
|
|
72
|
+
// quiver.test.ts — run with: node --test
|
|
73
|
+
import { Engine } from "@quillmark/wasm";
|
|
74
|
+
import { runQuiverTests } from "@quillmark/quiver/testing";
|
|
75
|
+
|
|
76
|
+
runQuiverTests(import.meta.url, new Engine());
|
|
77
|
+
```
|
|
78
|
+
|
|
53
79
|
## Manual validation (rendering samples)
|
|
54
80
|
|
|
55
81
|
The CI harness proves every quill _compiles_; it does not produce output a
|
|
@@ -58,11 +84,11 @@ human can look at. To eyeball real renders, run the
|
|
|
58
84
|
|
|
59
85
|
```ts
|
|
60
86
|
// scripts/preview.ts — run with: node --experimental-strip-types scripts/preview.ts
|
|
61
|
-
import {
|
|
87
|
+
import { Engine } from "@quillmark/wasm";
|
|
62
88
|
import { renderQuiverSamples } from "@quillmark/quiver/preview";
|
|
63
89
|
|
|
64
90
|
await renderQuiverSamples(import.meta.url, {
|
|
65
|
-
engine: new
|
|
91
|
+
engine: new Engine(),
|
|
66
92
|
});
|
|
67
93
|
// → writes ./preview/<name>@<version>.<fmt> + index.html
|
|
68
94
|
```
|
|
@@ -82,7 +108,7 @@ quill name or canonical ref):
|
|
|
82
108
|
|
|
83
109
|
```ts
|
|
84
110
|
await renderQuiverSamples(import.meta.url, {
|
|
85
|
-
engine: new
|
|
111
|
+
engine: new Engine(),
|
|
86
112
|
exclude: ["broken-quill"], // or: include: ["memo@1.0.0"]
|
|
87
113
|
});
|
|
88
114
|
```
|
|
@@ -95,21 +121,38 @@ await renderQuiverSamples(import.meta.url, {
|
|
|
95
121
|
## Consuming a quiver (Node)
|
|
96
122
|
|
|
97
123
|
```ts
|
|
98
|
-
import {
|
|
124
|
+
import { Engine, Document } from "@quillmark/wasm";
|
|
99
125
|
import { Quiver } from "@quillmark/quiver/node";
|
|
100
126
|
|
|
101
|
-
const engine = new Quillmark();
|
|
102
127
|
const quiver = await Quiver.fromPackage("@org/my-quiver");
|
|
128
|
+
const engine = new Engine();
|
|
103
129
|
|
|
104
130
|
const doc = Document.fromMarkdown(markdownString);
|
|
105
|
-
const quill = await quiver.getQuill(doc.quillRef
|
|
106
|
-
const result =
|
|
131
|
+
const quill = await quiver.getQuill(doc.quillRef);
|
|
132
|
+
const result = await engine.render(quill, doc, { format: "pdf" });
|
|
107
133
|
```
|
|
108
134
|
|
|
109
135
|
`getQuill` accepts both selector refs (`"memo"`, `"memo@1"`) and canonical
|
|
110
136
|
refs (`"memo@1.0.0"`). It resolves the selector, materializes the quill via
|
|
111
|
-
`
|
|
112
|
-
|
|
137
|
+
`Quill.fromTree(tree)` (engine-free), and caches one instance per canonical
|
|
138
|
+
ref for the lifetime of the `Quiver`. Concurrent calls for the same ref
|
|
139
|
+
coalesce into a single load.
|
|
140
|
+
|
|
141
|
+
The returned quill lives in the core build's WASM memory and is suitable for
|
|
142
|
+
schema inspection, validation, blueprint access, and seeding. To render it,
|
|
143
|
+
hand it straight to `engine.render(quill, doc)` — the Engine clones both
|
|
144
|
+
handles into the backend on demand, so no separate boundary-crossing step is
|
|
145
|
+
needed:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// Editor path — core only, ~0.66 MB gzip
|
|
149
|
+
const coreQuill = await quiver.getQuill(ref);
|
|
150
|
+
coreQuill.schema; // ✓ schema, validate, blueprint, seed — all fine
|
|
151
|
+
const doc = coreQuill.seedDocument(); // a fully-filled example document
|
|
152
|
+
|
|
153
|
+
// Render path — the same core handles render directly.
|
|
154
|
+
const result = await engine.render(coreQuill, doc, { format: "pdf" });
|
|
155
|
+
```
|
|
113
156
|
|
|
114
157
|
If you only need the canonical ref (without materializing), use `resolve`:
|
|
115
158
|
|
|
@@ -117,6 +160,10 @@ If you only need the canonical ref (without materializing), use `resolve`:
|
|
|
117
160
|
const canonicalRef = await quiver.resolve("memo"); // "memo@1.1.0"
|
|
118
161
|
```
|
|
119
162
|
|
|
163
|
+
If you need the raw file tree (e.g. to drive a backend binary directly), call
|
|
164
|
+
`(await quiver.getQuill(ref)).toTree()` on the core `Quill` — it is I/O-free
|
|
165
|
+
once the quill is materialized.
|
|
166
|
+
|
|
120
167
|
## Consuming a quiver (browser)
|
|
121
168
|
|
|
122
169
|
Browsers cannot read the source layout directly, so build at deploy time and
|
|
@@ -134,12 +181,64 @@ await Quiver.build(
|
|
|
134
181
|
|
|
135
182
|
```ts
|
|
136
183
|
// browser runtime
|
|
184
|
+
import { Engine, Document } from "@quillmark/wasm";
|
|
137
185
|
import { Quiver } from "@quillmark/quiver";
|
|
138
186
|
|
|
139
187
|
const quiver = await Quiver.fromBuiltUrl("/quivers/my-quiver/");
|
|
140
|
-
const
|
|
188
|
+
const engine = new Engine();
|
|
189
|
+
|
|
190
|
+
const doc = Document.fromMarkdown(markdownString);
|
|
191
|
+
const quill = await quiver.getQuill(doc.quillRef);
|
|
192
|
+
const result = await engine.render(quill, doc, { format: "pdf" });
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## SSR seeding (skip the `latest.json` pointer)
|
|
196
|
+
|
|
197
|
+
`Quiver.fromBuiltUrl(url)` first fetches `<url>/latest.json` — a stable-named,
|
|
198
|
+
non-content-addressed pointer to the current manifest — before fetching the
|
|
199
|
+
manifest itself. Because that one filename is stable, a CDN edge or browser
|
|
200
|
+
cache can serve a **stale pointer** after a release and silently pin the
|
|
201
|
+
client to the old catalog, with no error. Per-host cache headers only fix this
|
|
202
|
+
one serving layer at a time.
|
|
203
|
+
|
|
204
|
+
If you already hold the manifest bytes at build time — a common case for SSR
|
|
205
|
+
consumers, which read the built artifact during their own build — seed the
|
|
206
|
+
catalog from them directly with `Quiver.fromManifest`. It never requests
|
|
207
|
+
`latest.json`; bundles and fonts are still fetched lazily and
|
|
208
|
+
content-addressed, relative to `baseUrl`, exactly as with `fromBuiltUrl`:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
// Server build step — read the manifest the build wrote.
|
|
212
|
+
// build-output/latest.json → { "manifest": "manifest.<hash>.json" }
|
|
213
|
+
import { readFile } from "node:fs/promises";
|
|
214
|
+
|
|
215
|
+
const { manifest } = JSON.parse(
|
|
216
|
+
await readFile("./public/quivers/my-quiver/latest.json", "utf8"),
|
|
217
|
+
);
|
|
218
|
+
const manifestBytes = new Uint8Array(
|
|
219
|
+
await readFile(`./public/quivers/my-quiver/${manifest}`),
|
|
220
|
+
);
|
|
221
|
+
// Inline manifestBytes into the page payload (e.g. base64) for the client.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// Browser / SSR runtime — seed from the bytes you shipped, no pointer fetch.
|
|
226
|
+
import { Engine, Document } from "@quillmark/wasm";
|
|
227
|
+
import { Quiver } from "@quillmark/quiver";
|
|
228
|
+
|
|
229
|
+
const quiver = await Quiver.fromManifest("/quivers/my-quiver/", manifestBytes);
|
|
230
|
+
const engine = new Engine();
|
|
231
|
+
|
|
232
|
+
const doc = Document.fromMarkdown(markdownString);
|
|
233
|
+
const quill = await quiver.getQuill(doc.quillRef);
|
|
234
|
+
const result = await engine.render(quill, doc, { format: "pdf" });
|
|
141
235
|
```
|
|
142
236
|
|
|
237
|
+
`fromManifest` is browser-safe (no `node:*` imports) and shares
|
|
238
|
+
`fromBuiltUrl`'s error semantics: `quiver_invalid` for malformed manifest
|
|
239
|
+
bytes, `transport_error` for a `file://` `baseUrl` or a later bundle-fetch
|
|
240
|
+
failure.
|
|
241
|
+
|
|
143
242
|
## Server-side runtime (Node, packed artifact on disk)
|
|
144
243
|
|
|
145
244
|
For server-side rendering where the packed artifact ships in the deployment
|
|
@@ -177,15 +276,10 @@ await quiver.warm();
|
|
|
177
276
|
|
|
178
277
|
`warm()` is I/O-only: it loads every quill's tree (over the network for
|
|
179
278
|
`fromBuiltUrl`, off the filesystem for `fromPackage` / `fromDir` /
|
|
180
|
-
`fromBuiltDir`) and caches
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
the load.
|
|
185
|
-
|
|
186
|
-
Once a tree has been turned into a Quill, the cached tree is dropped so
|
|
187
|
-
its bytes can be GC'd — the materialized Quill is the runtime artifact.
|
|
188
|
-
Calling `warm()` again refills the tree cache.
|
|
279
|
+
`fromBuiltDir`) and caches them. It does not require an engine and does
|
|
280
|
+
not materialize Quill instances — that happens lazily on the first
|
|
281
|
+
`getQuill` call, which is microseconds. A subsequent `getQuill` reuses
|
|
282
|
+
the cached tree, skipping the load.
|
|
189
283
|
|
|
190
284
|
## Error handling
|
|
191
285
|
|
|
@@ -206,3 +300,27 @@ try {
|
|
|
206
300
|
```
|
|
207
301
|
|
|
208
302
|
Error codes: `invalid_ref`, `quill_not_found`, `quiver_invalid`, `transport_error`.
|
|
303
|
+
|
|
304
|
+
## Using `getQuill` vs calling `Quill.fromTree` directly
|
|
305
|
+
|
|
306
|
+
`getQuill(ref)` is the correct entry point for any code that loads a quill
|
|
307
|
+
through a `Quiver`. It handles ref resolution, tree fetching, `Quill.fromTree`
|
|
308
|
+
construction, and per-ref caching in one call. Do **not** reach for
|
|
309
|
+
`Quill.fromTree` directly inside a Quiver consumer:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
import { Quill } from "@quillmark/wasm";
|
|
313
|
+
|
|
314
|
+
// wrong — bypasses Quiver's cache; duplicates work getQuill already does
|
|
315
|
+
const tree = new Map<string, Uint8Array>();
|
|
316
|
+
tree.set("Quill.yaml", new TextEncoder().encode("name: memo\n..."));
|
|
317
|
+
// ...assemble the rest of the file tree by hand...
|
|
318
|
+
const quill = Quill.fromTree(tree);
|
|
319
|
+
|
|
320
|
+
// right
|
|
321
|
+
const quill = await quiver.getQuill(ref);
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`Quill.fromTree` is for code that builds quills **outside** of a Quiver (e.g.
|
|
325
|
+
a server route that receives a raw file tree over the network, or a test
|
|
326
|
+
fixture that constructs a quill from a hand-rolled in-memory tree).
|
package/dist/bin/quiver.js
CHANGED
|
@@ -42,29 +42,35 @@ function hasFlag(name) {
|
|
|
42
42
|
// Engine discovery
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
44
44
|
async function loadEngine(cwd) {
|
|
45
|
-
//
|
|
45
|
+
// Resolve @quillmark/wasm from the collection's node_modules (best-effort).
|
|
46
|
+
// Its `Engine` renders every quill (cloning the core Quill/Document into the
|
|
47
|
+
// backend itself). A quiver.config.js may export a custom `engine`.
|
|
48
|
+
let wasm;
|
|
49
|
+
try {
|
|
50
|
+
const req = createRequire(pathToFileURL(join(cwd, "package.json")).href);
|
|
51
|
+
const wasmPath = req.resolve("@quillmark/wasm");
|
|
52
|
+
wasm = (await import(pathToFileURL(wasmPath).href));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Not installed — a quiver.config.js may still provide an engine.
|
|
56
|
+
}
|
|
57
|
+
// 1. Named `engine` export from quiver.config.js, if present.
|
|
46
58
|
try {
|
|
47
59
|
const cfg = await import(pathToFileURL(join(cwd, "quiver.config.js")).href);
|
|
48
60
|
if (cfg.engine != null) {
|
|
49
|
-
return
|
|
61
|
+
return cfg.engine;
|
|
50
62
|
}
|
|
51
63
|
}
|
|
52
64
|
catch {
|
|
53
65
|
// File absent or incomplete — fall through to auto-discovery.
|
|
54
66
|
}
|
|
55
|
-
// 2. Auto-discover @quillmark/wasm
|
|
56
|
-
|
|
57
|
-
let wasmPath;
|
|
58
|
-
try {
|
|
59
|
-
wasmPath = req.resolve("@quillmark/wasm");
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
67
|
+
// 2. Auto-discover from @quillmark/wasm.
|
|
68
|
+
if (wasm == null) {
|
|
62
69
|
throw new Error("Cannot find @quillmark/wasm in this collection's node_modules.\n" +
|
|
63
70
|
" Install it: npm install @quillmark/wasm\n" +
|
|
64
71
|
" Or export { engine } from quiver.config.js for a custom engine.");
|
|
65
72
|
}
|
|
66
|
-
|
|
67
|
-
return { engine: new wasm.Quillmark() };
|
|
73
|
+
return new wasm.Engine();
|
|
68
74
|
}
|
|
69
75
|
// ---------------------------------------------------------------------------
|
|
70
76
|
// Commands
|
|
@@ -78,7 +84,7 @@ async function build() {
|
|
|
78
84
|
}
|
|
79
85
|
async function test() {
|
|
80
86
|
const cwd = process.cwd();
|
|
81
|
-
const
|
|
87
|
+
const engine = await loadEngine(cwd);
|
|
82
88
|
const quiver = await Quiver.fromDir(cwd);
|
|
83
89
|
const names = quiver.quillNames();
|
|
84
90
|
if (names.length === 0) {
|
|
@@ -91,9 +97,15 @@ async function test() {
|
|
|
91
97
|
for (const version of quiver.versionsOf(name)) {
|
|
92
98
|
const ref = `${name}@${version}`;
|
|
93
99
|
try {
|
|
94
|
-
const quill = await quiver.getQuill(ref
|
|
100
|
+
const quill = await quiver.getQuill(ref);
|
|
95
101
|
const doc = quill.seedDocument();
|
|
96
|
-
|
|
102
|
+
let result;
|
|
103
|
+
try {
|
|
104
|
+
result = await engine.render(quill, doc);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
doc.free();
|
|
108
|
+
}
|
|
97
109
|
if (!Array.isArray(result.artifacts) || result.artifacts.length === 0) {
|
|
98
110
|
throw new Error("example render produced no artifacts");
|
|
99
111
|
}
|
|
@@ -113,7 +125,7 @@ async function test() {
|
|
|
113
125
|
}
|
|
114
126
|
async function preview() {
|
|
115
127
|
const cwd = process.cwd();
|
|
116
|
-
const
|
|
128
|
+
const engine = await loadEngine(cwd);
|
|
117
129
|
const outDir = flag("--out");
|
|
118
130
|
const format = flag("--format");
|
|
119
131
|
const quiet = hasFlag("--quiet");
|
package/dist/build.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type BuildOptions = Record<string, never>;
|
|
|
12
12
|
*
|
|
13
13
|
* Output layout:
|
|
14
14
|
* outDir/
|
|
15
|
-
*
|
|
15
|
+
* latest.json # stable pointer to the current manifest
|
|
16
16
|
* manifest.<md5prefix6>.json # hashed manifest
|
|
17
17
|
* <name>@<version>.<md5>.zip # one bundle per quill
|
|
18
18
|
* store/
|
package/dist/build.js
CHANGED
|
@@ -14,7 +14,7 @@ const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
|
|
|
14
14
|
*
|
|
15
15
|
* Output layout:
|
|
16
16
|
* outDir/
|
|
17
|
-
*
|
|
17
|
+
* latest.json # stable pointer to the current manifest
|
|
18
18
|
* manifest.<md5prefix6>.json # hashed manifest
|
|
19
19
|
* <name>@<version>.<md5>.zip # one bundle per quill
|
|
20
20
|
* store/
|
|
@@ -108,9 +108,9 @@ export async function buildQuiver(sourceDir, outDir, _opts) {
|
|
|
108
108
|
catch (err) {
|
|
109
109
|
throw new QuiverError("transport_error", `Failed to write manifest "${manifestPath}": ${err.message}`, { cause: err });
|
|
110
110
|
}
|
|
111
|
-
// 9–10. Write stable pointer
|
|
111
|
+
// 9–10. Write stable pointer latest.json.
|
|
112
112
|
const pointer = { manifest: manifestFileName };
|
|
113
|
-
const pointerPath = join(outDir, "
|
|
113
|
+
const pointerPath = join(outDir, "latest.json");
|
|
114
114
|
try {
|
|
115
115
|
await writeFile(pointerPath, JSON.stringify(pointer), "utf-8");
|
|
116
116
|
}
|
package/dist/built-loader.d.ts
CHANGED
|
@@ -19,9 +19,15 @@ export interface BuiltTransport {
|
|
|
19
19
|
/**
|
|
20
20
|
* Load a build-output quiver via the given transport.
|
|
21
21
|
*
|
|
22
|
-
* 1. Fetches
|
|
22
|
+
* 1. Fetches latest.json (pointer) and parses it.
|
|
23
23
|
* 2. Fetches the manifest file it points to and validates it.
|
|
24
24
|
* 3. Builds a catalog from manifest entries (versions sorted descending).
|
|
25
25
|
* 4. Returns a Quiver instance backed by a BuiltLoader.
|
|
26
26
|
*/
|
|
27
27
|
export declare function loadBuiltQuiver(transport: BuiltTransport): Promise<Quiver>;
|
|
28
|
+
/**
|
|
29
|
+
* Seed a quiver from caller-provided manifest bytes, skipping the latest.json
|
|
30
|
+
* pointer fetch. `transport` is used only for lazy bundle/font fetches.
|
|
31
|
+
* Throws `quiver_invalid` on malformed bytes.
|
|
32
|
+
*/
|
|
33
|
+
export declare function seedBuiltQuiver(transport: BuiltTransport, manifestBytes: Uint8Array): Quiver;
|
package/dist/built-loader.js
CHANGED
|
@@ -96,15 +96,15 @@ function parsePointer(raw) {
|
|
|
96
96
|
parsed = JSON.parse(raw);
|
|
97
97
|
}
|
|
98
98
|
catch {
|
|
99
|
-
throw new QuiverError("quiver_invalid", "
|
|
99
|
+
throw new QuiverError("quiver_invalid", "latest.json contains invalid JSON");
|
|
100
100
|
}
|
|
101
101
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
102
|
-
throw new QuiverError("quiver_invalid", "
|
|
102
|
+
throw new QuiverError("quiver_invalid", "latest.json must be a JSON object");
|
|
103
103
|
}
|
|
104
104
|
const obj = parsed;
|
|
105
|
-
assertNoUnknownKeys(obj, ["manifest"], "
|
|
105
|
+
assertNoUnknownKeys(obj, ["manifest"], "latest.json");
|
|
106
106
|
if (typeof obj["manifest"] !== "string" || obj["manifest"].length === 0) {
|
|
107
|
-
throw new QuiverError("quiver_invalid", '
|
|
107
|
+
throw new QuiverError("quiver_invalid", 'latest.json must have a non-empty string "manifest" field');
|
|
108
108
|
}
|
|
109
109
|
return obj["manifest"];
|
|
110
110
|
}
|
|
@@ -175,11 +175,38 @@ function parseManifest(raw) {
|
|
|
175
175
|
quills,
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
|
-
// ───
|
|
178
|
+
// ─── Catalog assembly ─────────────────────────────────────────────────────────
|
|
179
|
+
/**
|
|
180
|
+
* Validate manifest bytes (`quiver_invalid` on format errors) and assemble a
|
|
181
|
+
* Quiver backed by a BuiltLoader over `transport`. Shared by `loadBuiltQuiver`
|
|
182
|
+
* (pointer-following) and `seedBuiltQuiver` (seed).
|
|
183
|
+
*/
|
|
184
|
+
function buildQuiverFromManifestBytes(transport, manifestBytes) {
|
|
185
|
+
const manifest = parseManifest(new TextDecoder().decode(manifestBytes));
|
|
186
|
+
// catalog: name → versions (desc); index: "name@version" → entry (dedup).
|
|
187
|
+
const catalogRaw = new Map();
|
|
188
|
+
const index = new Map();
|
|
189
|
+
for (const entry of manifest.quills) {
|
|
190
|
+
const key = `${entry.name}@${entry.version}`;
|
|
191
|
+
if (index.has(key)) {
|
|
192
|
+
throw new QuiverError("quiver_invalid", `Duplicate quill entry in manifest: "${key}"`);
|
|
193
|
+
}
|
|
194
|
+
index.set(key, entry);
|
|
195
|
+
const versions = catalogRaw.get(entry.name) ?? [];
|
|
196
|
+
versions.push(entry.version);
|
|
197
|
+
catalogRaw.set(entry.name, versions);
|
|
198
|
+
}
|
|
199
|
+
for (const [, versions] of catalogRaw) {
|
|
200
|
+
versions.sort((a, b) => compareSemver(b, a));
|
|
201
|
+
}
|
|
202
|
+
const loader = new BuiltLoader(transport, index);
|
|
203
|
+
return Quiver._fromLoader(manifest.name, catalogRaw, loader);
|
|
204
|
+
}
|
|
205
|
+
// ─── Main entry points ────────────────────────────────────────────────────────
|
|
179
206
|
/**
|
|
180
207
|
* Load a build-output quiver via the given transport.
|
|
181
208
|
*
|
|
182
|
-
* 1. Fetches
|
|
209
|
+
* 1. Fetches latest.json (pointer) and parses it.
|
|
183
210
|
* 2. Fetches the manifest file it points to and validates it.
|
|
184
211
|
* 3. Builds a catalog from manifest entries (versions sorted descending).
|
|
185
212
|
* 4. Returns a Quiver instance backed by a BuiltLoader.
|
|
@@ -188,12 +215,12 @@ export async function loadBuiltQuiver(transport) {
|
|
|
188
215
|
// 1. Fetch and parse pointer.
|
|
189
216
|
let pointerBytes;
|
|
190
217
|
try {
|
|
191
|
-
pointerBytes = await transport.fetchBytes("
|
|
218
|
+
pointerBytes = await transport.fetchBytes("latest.json");
|
|
192
219
|
}
|
|
193
220
|
catch (err) {
|
|
194
221
|
if (err instanceof QuiverError)
|
|
195
222
|
throw err;
|
|
196
|
-
throw new QuiverError("transport_error", `Failed to fetch
|
|
223
|
+
throw new QuiverError("transport_error", `Failed to fetch latest.json: ${err.message}`, { cause: err });
|
|
197
224
|
}
|
|
198
225
|
const manifestFileName = parsePointer(new TextDecoder().decode(pointerBytes));
|
|
199
226
|
validateManifestFileName(manifestFileName);
|
|
@@ -207,26 +234,14 @@ export async function loadBuiltQuiver(transport) {
|
|
|
207
234
|
throw err;
|
|
208
235
|
throw new QuiverError("transport_error", `Failed to fetch manifest "${manifestFileName}": ${err.message}`, { cause: err });
|
|
209
236
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
index.set(key, entry);
|
|
221
|
-
const versions = catalogRaw.get(entry.name) ?? [];
|
|
222
|
-
versions.push(entry.version);
|
|
223
|
-
catalogRaw.set(entry.name, versions);
|
|
224
|
-
}
|
|
225
|
-
for (const [, versions] of catalogRaw) {
|
|
226
|
-
versions.sort((a, b) => compareSemver(b, a));
|
|
227
|
-
}
|
|
228
|
-
// 4. Build loader.
|
|
229
|
-
const loader = new BuiltLoader(transport, index);
|
|
230
|
-
// 5. Return Quiver via internal factory.
|
|
231
|
-
return Quiver._fromLoader(manifest.name, catalogRaw, loader);
|
|
237
|
+
// 3–4. Validate manifest + assemble catalog/loader.
|
|
238
|
+
return buildQuiverFromManifestBytes(transport, manifestBytes);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Seed a quiver from caller-provided manifest bytes, skipping the latest.json
|
|
242
|
+
* pointer fetch. `transport` is used only for lazy bundle/font fetches.
|
|
243
|
+
* Throws `quiver_invalid` on malformed bytes.
|
|
244
|
+
*/
|
|
245
|
+
export function seedBuiltQuiver(transport, manifestBytes) {
|
|
246
|
+
return buildQuiverFromManifestBytes(transport, manifestBytes);
|
|
232
247
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// live at `@quillmark/quiver/node`.
|
|
6
6
|
export { QuiverError } from "./errors.js";
|
|
7
7
|
export { Quiver } from "./quiver.js";
|
|
8
|
-
//
|
|
9
|
-
// re-exported: import
|
|
10
|
-
// which is the single source of truth
|
|
8
|
+
// The canonical API (`Quill`, `Document`, `Engine`, `RenderResult`, …) is not
|
|
9
|
+
// re-exported: import it straight from the `@quillmark/wasm` peer dependency,
|
|
10
|
+
// which is the single source of truth. `quiver.getQuill(ref)` returns a core
|
|
11
|
+
// `Quill` that an `Engine` renders directly — the engine clones it into the
|
|
12
|
+
// backend, so no boundary-crossing helper is needed.
|
package/dist/node.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ type NodeQuiverStatics = {
|
|
|
43
43
|
fromDir(pathOrFileUrl: string): Promise<Base>;
|
|
44
44
|
/**
|
|
45
45
|
* Loads a packed (build-output) quiver from a local directory containing
|
|
46
|
-
* `
|
|
46
|
+
* `latest.json` and the manifest/bundle/store files written by
|
|
47
47
|
* `Quiver.build`. Symmetric to `fromBuiltUrl(url)` but reads from disk
|
|
48
48
|
* instead of HTTP — no network required.
|
|
49
49
|
*
|
package/dist/preview.d.ts
CHANGED
|
@@ -11,12 +11,10 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Usage (place a script next to your Quiver.yaml):
|
|
13
13
|
*
|
|
14
|
-
* import {
|
|
14
|
+
* import { Engine } from "@quillmark/wasm";
|
|
15
15
|
* import { renderQuiverSamples } from "@quillmark/quiver/preview";
|
|
16
16
|
*
|
|
17
|
-
* await renderQuiverSamples(import.meta.url, {
|
|
18
|
-
* engine: new Quillmark(),
|
|
19
|
-
* });
|
|
17
|
+
* await renderQuiverSamples(import.meta.url, { engine: new Engine() });
|
|
20
18
|
* // → open ./preview/index.html
|
|
21
19
|
*
|
|
22
20
|
* The sample document is the illustrative example seeded by
|
|
@@ -28,10 +26,14 @@
|
|
|
28
26
|
* A `.gitignore` is written into `outDir` so the generated artifacts are not
|
|
29
27
|
* accidentally committed.
|
|
30
28
|
*/
|
|
31
|
-
import type {
|
|
29
|
+
import type { Engine } from "@quillmark/wasm";
|
|
32
30
|
export interface RenderQuiverSamplesOptions {
|
|
33
|
-
/**
|
|
34
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Canonical render engine (`new Engine()` from `@quillmark/wasm`). It takes the
|
|
33
|
+
* core `Quill`/`Document` that quiver produces and clones them into the
|
|
34
|
+
* backend on demand — no build-matching or handle-crossing for the caller.
|
|
35
|
+
*/
|
|
36
|
+
engine: Engine;
|
|
35
37
|
/** Directory to write rendered artifacts into. Default: `preview`. */
|
|
36
38
|
outDir?: string;
|
|
37
39
|
/** Force an output format (`pdf`/`svg`/`png`/`txt`). Default: engine's choice. */
|
package/dist/preview.js
CHANGED
|
@@ -11,12 +11,10 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Usage (place a script next to your Quiver.yaml):
|
|
13
13
|
*
|
|
14
|
-
* import {
|
|
14
|
+
* import { Engine } from "@quillmark/wasm";
|
|
15
15
|
* import { renderQuiverSamples } from "@quillmark/quiver/preview";
|
|
16
16
|
*
|
|
17
|
-
* await renderQuiverSamples(import.meta.url, {
|
|
18
|
-
* engine: new Quillmark(),
|
|
19
|
-
* });
|
|
17
|
+
* await renderQuiverSamples(import.meta.url, { engine: new Engine() });
|
|
20
18
|
* // → open ./preview/index.html
|
|
21
19
|
*
|
|
22
20
|
* The sample document is the illustrative example seeded by
|
|
@@ -31,6 +29,7 @@
|
|
|
31
29
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
32
30
|
import { join } from "node:path";
|
|
33
31
|
import { Quiver } from "./node.js";
|
|
32
|
+
import { isQuillmarkError } from "@quillmark/wasm";
|
|
34
33
|
/** Default directory rendered artifacts are written to. */
|
|
35
34
|
const DEFAULT_OUT_DIR = "preview";
|
|
36
35
|
/**
|
|
@@ -73,13 +72,12 @@ function isSelected(name, ref, opts) {
|
|
|
73
72
|
}
|
|
74
73
|
/**
|
|
75
74
|
* Formats a thrown render error into one string per diagnostic. `@quillmark/wasm`
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* throws `QuillmarkError`s (an `Error` carrying `diagnostics`); fall back to
|
|
76
|
+
* the message for anything else.
|
|
78
77
|
*/
|
|
79
78
|
function failureReasons(err) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return diagnostics.map((d) => `${d.severity}: ${d.message}`);
|
|
79
|
+
if (isQuillmarkError(err) && err.diagnostics.length > 0) {
|
|
80
|
+
return err.diagnostics.map((d) => `${d.severity}: ${d.message}`);
|
|
83
81
|
}
|
|
84
82
|
return [err.message];
|
|
85
83
|
}
|
|
@@ -87,9 +85,16 @@ async function renderOne(quiver, name, version, outDir, opts) {
|
|
|
87
85
|
const ref = `${name}@${version}`;
|
|
88
86
|
let result;
|
|
89
87
|
try {
|
|
90
|
-
|
|
88
|
+
// Canonical render path: a core Quill + its seeded Document handed straight
|
|
89
|
+
// to the Engine, which clones them into the backend and frees the clones.
|
|
90
|
+
const quill = await quiver.getQuill(ref);
|
|
91
91
|
const doc = quill.seedDocument();
|
|
92
|
-
|
|
92
|
+
try {
|
|
93
|
+
result = await opts.engine.render(quill, doc, opts.format ? { format: opts.format } : undefined);
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
doc.free();
|
|
97
|
+
}
|
|
93
98
|
}
|
|
94
99
|
catch (err) {
|
|
95
100
|
return {
|
package/dist/quiver.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `build`) are installed on this class by `./node.js`, which is the
|
|
10
10
|
* consumer's explicit opt-in to the Node API surface.
|
|
11
11
|
*/
|
|
12
|
-
import
|
|
12
|
+
import { Quill } from "@quillmark/wasm";
|
|
13
13
|
/** @internal Internal loader strategy: source or build output. */
|
|
14
14
|
export interface QuiverLoader {
|
|
15
15
|
loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
|
|
@@ -42,6 +42,16 @@ export declare class Quiver {
|
|
|
42
42
|
* on format errors.
|
|
43
43
|
*/
|
|
44
44
|
static fromBuiltUrl(url: string): Promise<Quiver>;
|
|
45
|
+
/**
|
|
46
|
+
* Browser-safe factory. Seeds the catalog from caller-provided manifest
|
|
47
|
+
* bytes — never fetches `latest.json` — then fetches bundles lazily and
|
|
48
|
+
* content-addressed, relative to `baseUrl`, like `fromBuiltUrl`. For SSR
|
|
49
|
+
* consumers that already hold the manifest at build time.
|
|
50
|
+
*
|
|
51
|
+
* Throws `quiver_invalid` on malformed manifest bytes, `transport_error`
|
|
52
|
+
* on a `file://` baseUrl or a later bundle fetch failure.
|
|
53
|
+
*/
|
|
54
|
+
static fromManifest(baseUrl: string, manifestBytes: Uint8Array): Promise<Quiver>;
|
|
45
55
|
/** Returns all known quill names, sorted lexicographically. */
|
|
46
56
|
quillNames(): string[];
|
|
47
57
|
/**
|
|
@@ -49,16 +59,6 @@ export declare class Quiver {
|
|
|
49
59
|
* Returns an empty array if the quill name is not in the catalog.
|
|
50
60
|
*/
|
|
51
61
|
versionsOf(name: string): string[];
|
|
52
|
-
/**
|
|
53
|
-
* Lazily loads the file tree for a specific quill version.
|
|
54
|
-
*
|
|
55
|
-
* Returns `Map<string, Uint8Array>` suitable for `engine.quill(tree)`.
|
|
56
|
-
* Does NOT cache the result — caching of materialized Quill instances
|
|
57
|
-
* happens in `getQuill`.
|
|
58
|
-
*
|
|
59
|
-
* Throws `transport_error` if name/version not in catalog or I/O fails.
|
|
60
|
-
*/
|
|
61
|
-
loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
|
|
62
62
|
/**
|
|
63
63
|
* Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
|
|
64
64
|
*
|
|
@@ -71,29 +71,32 @@ export declare class Quiver {
|
|
|
71
71
|
*/
|
|
72
72
|
resolve(ref: string): Promise<string>;
|
|
73
73
|
/**
|
|
74
|
-
* Returns a
|
|
74
|
+
* Returns a `Quill` for a ref (selector or canonical).
|
|
75
|
+
*
|
|
76
|
+
* The returned quill is materialized from the `@quillmark/wasm` root export
|
|
77
|
+
* — it is engine-free, portable data suitable for schema inspection,
|
|
78
|
+
* validation, blueprint access, and document seeding.
|
|
79
|
+
*
|
|
80
|
+
* A core `Quill` renders directly: pass it to `engine.render(quill, doc)`.
|
|
75
81
|
*
|
|
76
|
-
* Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* (or a previous `getQuill`) so the network fetch isn't paid twice.
|
|
80
|
-
* Concurrent calls for the same ref coalesce into a single load.
|
|
82
|
+
* Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical form
|
|
83
|
+
* first. Materializes once and caches per canonical ref — concurrent calls
|
|
84
|
+
* coalesce into a single load.
|
|
81
85
|
*
|
|
82
86
|
* Throws:
|
|
83
87
|
* - `invalid_ref` if ref is malformed
|
|
84
88
|
* - `quill_not_found` if ref does not match any version in this quiver
|
|
85
|
-
* - propagates I/O errors from
|
|
86
|
-
* - propagates
|
|
89
|
+
* - propagates I/O errors from the loader unchanged
|
|
90
|
+
* - propagates validation errors from Quill.fromTree() unchanged
|
|
87
91
|
*/
|
|
88
|
-
getQuill(ref: string
|
|
89
|
-
engine: Quillmark;
|
|
90
|
-
}): Promise<Quill>;
|
|
92
|
+
getQuill(ref: string): Promise<Quill>;
|
|
91
93
|
/**
|
|
92
94
|
* Prefetches the tree for every quill version in this quiver. Fail-fast.
|
|
93
95
|
*
|
|
94
96
|
* Network-bound only — does not materialize Quill instances and does not
|
|
95
97
|
* require an engine. Subsequent `getQuill` calls reuse the cached trees,
|
|
96
|
-
* skipping the fetch
|
|
98
|
+
* skipping the fetch (the tree then evicts as its quill materializes).
|
|
99
|
+
* Rejects on the first fetch failure.
|
|
97
100
|
*/
|
|
98
101
|
warm(): Promise<void>;
|
|
99
102
|
}
|
package/dist/quiver.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* consumer's explicit opt-in to the Node API surface.
|
|
11
11
|
*/
|
|
12
12
|
import { QuiverError } from "./errors.js";
|
|
13
|
+
import { Quill } from "@quillmark/wasm";
|
|
13
14
|
import { parseQuillRef } from "./ref.js";
|
|
14
15
|
import { matchesSemverSelector, chooseHighestVersion } from "./semver.js";
|
|
15
16
|
export class Quiver {
|
|
@@ -17,15 +18,16 @@ export class Quiver {
|
|
|
17
18
|
#catalog;
|
|
18
19
|
#loader;
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Cache of materialized quills, keyed by canonical ref. A `Quill` is now
|
|
22
|
+
* engine-free, portable data (`Quill.fromTree`), so one instance per ref is
|
|
23
|
+
* shared across every engine. Promise values so concurrent getQuill calls
|
|
24
|
+
* coalesce into a single materialization.
|
|
23
25
|
*/
|
|
24
|
-
#quillCache = new
|
|
26
|
+
#quillCache = new Map();
|
|
25
27
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* values so concurrent fetches coalesce.
|
|
28
|
+
* Cache of fetched trees, keyed by canonical ref. Populated by `warm()`
|
|
29
|
+
* and on first `getQuill` for a ref; an entry is evicted once its quill
|
|
30
|
+
* materializes successfully. Promise values so concurrent fetches coalesce.
|
|
29
31
|
*/
|
|
30
32
|
#treeCache = new Map();
|
|
31
33
|
/**
|
|
@@ -67,6 +69,24 @@ export class Quiver {
|
|
|
67
69
|
const transport = new HttpTransport(url);
|
|
68
70
|
return loadBuiltQuiver(transport);
|
|
69
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Browser-safe factory. Seeds the catalog from caller-provided manifest
|
|
74
|
+
* bytes — never fetches `latest.json` — then fetches bundles lazily and
|
|
75
|
+
* content-addressed, relative to `baseUrl`, like `fromBuiltUrl`. For SSR
|
|
76
|
+
* consumers that already hold the manifest at build time.
|
|
77
|
+
*
|
|
78
|
+
* Throws `quiver_invalid` on malformed manifest bytes, `transport_error`
|
|
79
|
+
* on a `file://` baseUrl or a later bundle fetch failure.
|
|
80
|
+
*/
|
|
81
|
+
static async fromManifest(baseUrl, manifestBytes) {
|
|
82
|
+
if (baseUrl.startsWith("file://")) {
|
|
83
|
+
throw new QuiverError("transport_error", `Quiver.fromManifest requires an http(s):// or origin-relative baseUrl; got "${baseUrl}". For local build output, use Quiver.fromBuiltDir from @quillmark/quiver/node.`);
|
|
84
|
+
}
|
|
85
|
+
const { HttpTransport } = await import("./transports/http-transport.js");
|
|
86
|
+
const { seedBuiltQuiver } = await import("./built-loader.js");
|
|
87
|
+
const transport = new HttpTransport(baseUrl);
|
|
88
|
+
return seedBuiltQuiver(transport, manifestBytes);
|
|
89
|
+
}
|
|
70
90
|
/** Returns all known quill names, sorted lexicographically. */
|
|
71
91
|
quillNames() {
|
|
72
92
|
return [...this.#catalog.keys()].sort();
|
|
@@ -78,22 +98,6 @@ export class Quiver {
|
|
|
78
98
|
versionsOf(name) {
|
|
79
99
|
return [...(this.#catalog.get(name) ?? [])];
|
|
80
100
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Lazily loads the file tree for a specific quill version.
|
|
83
|
-
*
|
|
84
|
-
* Returns `Map<string, Uint8Array>` suitable for `engine.quill(tree)`.
|
|
85
|
-
* Does NOT cache the result — caching of materialized Quill instances
|
|
86
|
-
* happens in `getQuill`.
|
|
87
|
-
*
|
|
88
|
-
* Throws `transport_error` if name/version not in catalog or I/O fails.
|
|
89
|
-
*/
|
|
90
|
-
async loadTree(name, version) {
|
|
91
|
-
const versions = this.#catalog.get(name);
|
|
92
|
-
if (!versions || !versions.includes(version)) {
|
|
93
|
-
throw new QuiverError("transport_error", `Quill "${name}@${version}" not found in quiver "${this.name}"`, { quiverName: this.name, version, ref: `${name}@${version}` });
|
|
94
|
-
}
|
|
95
|
-
return this.#loader.loadTree(name, version);
|
|
96
|
-
}
|
|
97
101
|
/**
|
|
98
102
|
* Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
|
|
99
103
|
*
|
|
@@ -120,63 +124,65 @@ export class Quiver {
|
|
|
120
124
|
throw new QuiverError("quill_not_found", `No quill found for ref "${ref}" in quiver "${this.name}".`, { ref, quiverName: this.name });
|
|
121
125
|
}
|
|
122
126
|
/**
|
|
123
|
-
* Returns a
|
|
127
|
+
* Returns a `Quill` for a ref (selector or canonical).
|
|
128
|
+
*
|
|
129
|
+
* The returned quill is materialized from the `@quillmark/wasm` root export
|
|
130
|
+
* — it is engine-free, portable data suitable for schema inspection,
|
|
131
|
+
* validation, blueprint access, and document seeding.
|
|
124
132
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
133
|
+
* A core `Quill` renders directly: pass it to `engine.render(quill, doc)`.
|
|
134
|
+
*
|
|
135
|
+
* Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical form
|
|
136
|
+
* first. Materializes once and caches per canonical ref — concurrent calls
|
|
137
|
+
* coalesce into a single load.
|
|
130
138
|
*
|
|
131
139
|
* Throws:
|
|
132
140
|
* - `invalid_ref` if ref is malformed
|
|
133
141
|
* - `quill_not_found` if ref does not match any version in this quiver
|
|
134
|
-
* - propagates I/O errors from
|
|
135
|
-
* - propagates
|
|
142
|
+
* - propagates I/O errors from the loader unchanged
|
|
143
|
+
* - propagates validation errors from Quill.fromTree() unchanged
|
|
136
144
|
*/
|
|
137
|
-
async getQuill(ref
|
|
145
|
+
async getQuill(ref) {
|
|
138
146
|
const canonicalRef = await this.resolve(ref);
|
|
139
|
-
|
|
140
|
-
let perEngine = this.#quillCache.get(engine);
|
|
141
|
-
if (perEngine === undefined) {
|
|
142
|
-
perEngine = new Map();
|
|
143
|
-
this.#quillCache.set(engine, perEngine);
|
|
144
|
-
}
|
|
145
|
-
let entry = perEngine.get(canonicalRef);
|
|
147
|
+
let entry = this.#quillCache.get(canonicalRef);
|
|
146
148
|
if (entry === undefined) {
|
|
147
|
-
entry = this.#materializeQuill(canonicalRef
|
|
148
|
-
|
|
149
|
+
entry = this.#materializeQuill(canonicalRef).catch((err) => {
|
|
150
|
+
this.#quillCache.delete(canonicalRef);
|
|
149
151
|
throw err;
|
|
150
152
|
});
|
|
151
|
-
|
|
153
|
+
this.#quillCache.set(canonicalRef, entry);
|
|
152
154
|
}
|
|
153
155
|
return entry;
|
|
154
156
|
}
|
|
155
157
|
/**
|
|
156
|
-
* Internal: load tree (cached) +
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* the network.
|
|
158
|
+
* Internal: load tree (cached) + construct via Quill.fromTree. Errors
|
|
159
|
+
* propagate. On success the tree is evicted from `#treeCache` (the quill
|
|
160
|
+
* cache now holds the materialized result), so the tree is not retained
|
|
161
|
+
* past materialization. On `Quill.fromTree` failure the tree stays cached,
|
|
162
|
+
* so a retry skips the refetch.
|
|
162
163
|
*/
|
|
163
|
-
async #materializeQuill(canonicalRef
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
164
|
+
async #materializeQuill(canonicalRef) {
|
|
165
|
+
const entry = this.#getTreeCached(canonicalRef);
|
|
166
|
+
const tree = await entry;
|
|
167
|
+
const quill = Quill.fromTree(tree);
|
|
168
|
+
// Evict by identity: only drop the entry if it is still the promise this
|
|
169
|
+
// materialization consumed (a concurrent refetch may have replaced it).
|
|
170
|
+
if (this.#treeCache.get(canonicalRef) === entry) {
|
|
171
|
+
this.#treeCache.delete(canonicalRef);
|
|
172
|
+
}
|
|
167
173
|
return quill;
|
|
168
174
|
}
|
|
169
175
|
/**
|
|
170
|
-
* Internal: tree cache reader. On miss, fetches via
|
|
176
|
+
* Internal: tree cache reader. On miss, fetches via `#loadTree` and stores
|
|
171
177
|
* the in-flight Promise. On rejection, evicts so a retry can succeed.
|
|
172
178
|
*/
|
|
173
|
-
|
|
179
|
+
#getTreeCached(canonicalRef) {
|
|
174
180
|
let entry = this.#treeCache.get(canonicalRef);
|
|
175
181
|
if (entry === undefined) {
|
|
176
182
|
const at = canonicalRef.indexOf("@");
|
|
177
183
|
const name = canonicalRef.slice(0, at);
|
|
178
184
|
const version = canonicalRef.slice(at + 1);
|
|
179
|
-
entry = this
|
|
185
|
+
entry = this.#loadTree(name, version).catch((err) => {
|
|
180
186
|
this.#treeCache.delete(canonicalRef);
|
|
181
187
|
throw err;
|
|
182
188
|
});
|
|
@@ -184,12 +190,27 @@ export class Quiver {
|
|
|
184
190
|
}
|
|
185
191
|
return entry;
|
|
186
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Internal: validates name/version against the catalog, then delegates to
|
|
195
|
+
* the loader. Returns `Map<string, Uint8Array>` suitable for
|
|
196
|
+
* `Quill.fromTree(tree)`. Does NOT cache — caching lives in `#getTreeCached`.
|
|
197
|
+
*
|
|
198
|
+
* Throws `transport_error` if name/version not in catalog or I/O fails.
|
|
199
|
+
*/
|
|
200
|
+
async #loadTree(name, version) {
|
|
201
|
+
const versions = this.#catalog.get(name);
|
|
202
|
+
if (!versions || !versions.includes(version)) {
|
|
203
|
+
throw new QuiverError("transport_error", `Quill "${name}@${version}" not found in quiver "${this.name}"`, { quiverName: this.name, version, ref: `${name}@${version}` });
|
|
204
|
+
}
|
|
205
|
+
return this.#loader.loadTree(name, version);
|
|
206
|
+
}
|
|
187
207
|
/**
|
|
188
208
|
* Prefetches the tree for every quill version in this quiver. Fail-fast.
|
|
189
209
|
*
|
|
190
210
|
* Network-bound only — does not materialize Quill instances and does not
|
|
191
211
|
* require an engine. Subsequent `getQuill` calls reuse the cached trees,
|
|
192
|
-
* skipping the fetch
|
|
212
|
+
* skipping the fetch (the tree then evicts as its quill materializes).
|
|
213
|
+
* Rejects on the first fetch failure.
|
|
193
214
|
*/
|
|
194
215
|
async warm() {
|
|
195
216
|
const promises = [];
|
package/dist/testing.d.ts
CHANGED
|
@@ -7,14 +7,13 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Usage (place this file next to your Quiver.yaml):
|
|
9
9
|
*
|
|
10
|
-
* import {
|
|
10
|
+
* import { Engine } from "@quillmark/wasm";
|
|
11
11
|
* import { runQuiverTests } from "@quillmark/quiver/testing";
|
|
12
|
-
*
|
|
13
|
-
* runQuiverTests(import.meta.url, engine);
|
|
12
|
+
* runQuiverTests(import.meta.url, new Engine());
|
|
14
13
|
*
|
|
15
14
|
* Run with `node --test`.
|
|
16
15
|
*/
|
|
17
|
-
import type {
|
|
16
|
+
import type { Engine } from "@quillmark/wasm";
|
|
18
17
|
/**
|
|
19
18
|
* Registers a `node:test` describe block that validates every quill
|
|
20
19
|
* version in the quiver at `metaUrlOrDir` against the provided engine.
|
|
@@ -23,7 +22,7 @@ import type { Quillmark } from "@quillmark/wasm";
|
|
|
23
22
|
* to Quiver.yaml). Pass an absolute directory path for any other layout.
|
|
24
23
|
*
|
|
25
24
|
* Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml,
|
|
26
|
-
* all template files,
|
|
27
|
-
* full render of each quill's example document.
|
|
25
|
+
* all template files, quill construction via Quill.fromTree(tree), and a
|
|
26
|
+
* full render of each quill's example document via engine.render(quill, doc).
|
|
28
27
|
*/
|
|
29
|
-
export declare function runQuiverTests(metaUrlOrDir: string, engine:
|
|
28
|
+
export declare function runQuiverTests(metaUrlOrDir: string, engine: Engine): void;
|
package/dist/testing.js
CHANGED
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Usage (place this file next to your Quiver.yaml):
|
|
9
9
|
*
|
|
10
|
-
* import {
|
|
10
|
+
* import { Engine } from "@quillmark/wasm";
|
|
11
11
|
* import { runQuiverTests } from "@quillmark/quiver/testing";
|
|
12
|
-
*
|
|
13
|
-
* runQuiverTests(import.meta.url, engine);
|
|
12
|
+
* runQuiverTests(import.meta.url, new Engine());
|
|
14
13
|
*
|
|
15
14
|
* Run with `node --test`.
|
|
16
15
|
*/
|
|
@@ -27,8 +26,8 @@ import { Quiver } from "./node.js";
|
|
|
27
26
|
* to Quiver.yaml). Pass an absolute directory path for any other layout.
|
|
28
27
|
*
|
|
29
28
|
* Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml,
|
|
30
|
-
* all template files,
|
|
31
|
-
* full render of each quill's example document.
|
|
29
|
+
* all template files, quill construction via Quill.fromTree(tree), and a
|
|
30
|
+
* full render of each quill's example document via engine.render(quill, doc).
|
|
32
31
|
*/
|
|
33
32
|
export function runQuiverTests(metaUrlOrDir, engine) {
|
|
34
33
|
describe("Quiver", () => {
|
|
@@ -45,9 +44,18 @@ export function runQuiverTests(metaUrlOrDir, engine) {
|
|
|
45
44
|
for (const name of quiver.quillNames()) {
|
|
46
45
|
for (const version of quiver.versionsOf(name)) {
|
|
47
46
|
const ref = `${name}@${version}`;
|
|
48
|
-
|
|
47
|
+
// The Engine takes the core Quill + its seeded Document directly and
|
|
48
|
+
// clones them into the backend; the doc clone is freed inside render,
|
|
49
|
+
// and we free our own core Document here.
|
|
50
|
+
const quill = await quiver.getQuill(ref);
|
|
49
51
|
const doc = quill.seedDocument();
|
|
50
|
-
|
|
52
|
+
let result;
|
|
53
|
+
try {
|
|
54
|
+
result = await engine.render(quill, doc);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
doc.free();
|
|
58
|
+
}
|
|
51
59
|
if (!Array.isArray(result.artifacts) || result.artifacts.length === 0) {
|
|
52
60
|
throw new Error(`${ref}: example render produced no artifacts`);
|
|
53
61
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/quiver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Quiver registry and build tooling for Quillmark",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"author": "Quillmark Contributors",
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
23
|
+
"node": ">=22"
|
|
24
24
|
},
|
|
25
25
|
"exports": {
|
|
26
26
|
".": {
|
|
@@ -50,14 +50,14 @@
|
|
|
50
50
|
"README.md"
|
|
51
51
|
],
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@quillmark/wasm": ">=0.
|
|
53
|
+
"@quillmark/wasm": ">=0.90.0-0"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"fflate": "^0.8.2",
|
|
57
57
|
"yaml": "^2.8.3"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@quillmark/wasm": "
|
|
60
|
+
"@quillmark/wasm": "0.91.0",
|
|
61
61
|
"@types/node": "^25.3.3",
|
|
62
62
|
"typescript": "^5.9.3",
|
|
63
63
|
"vitest": "^4.0.18"
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "tsc",
|
|
70
|
+
"prepack": "rm -rf dist && tsc",
|
|
70
71
|
"test": "vitest run",
|
|
71
72
|
"lint": "tsc --noEmit"
|
|
72
73
|
}
|