@quillmark/quiver 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PROGRAM.md CHANGED
@@ -18,28 +18,43 @@ This means Quiver must own quill selection and lifecycle entirely. There is no e
18
18
 
19
19
  ## Core Decisions
20
20
 
21
- ### 1) Quiver Shapes and Transports Are Orthogonal
22
-
23
- Two on-disk/data shapes:
24
-
25
- - **Source Quiver** (authoring shape)
26
- - Human-authored, git-friendly
27
- - Root `Quiver.yaml`
28
- - Quills under `quills/<name>/<version>/Quill.yaml`
29
- - Assets in normal source layout
30
- - **Packed Quiver** (distribution/runtime artifact)
31
- - Hashed manifest
32
- - Bundle zips
33
- - Dehydrated shared font store (`store/<md5>`)
34
- - Stable pointer file to current hashed manifest
35
-
36
- Transport is a separate axis for **Packed Quiver** loading:
37
-
38
- - `http` (browser/runtime delivery)
39
- - `fs` (packed artifact loaded from local disk)
40
- - future transports can be added without changing packed format
41
-
42
- **Naming decision:** use `pack()` (format operation), not transport-specific names.
21
+ ### 1) One Authored Shape; A Build Output Derived From It
22
+
23
+ There is one user-facing shape: the **Source Quiver**.
24
+
25
+ - Human-authored, git-friendly
26
+ - Root `Quiver.yaml`
27
+ - Quills under `quills/<name>/<version>/Quill.yaml`
28
+ - Assets in normal source layout
29
+
30
+ A build step (`Quiver.build`) derives a **runtime artifact** from the source.
31
+ This artifact is not a peer "format" — it is the source layout's `dist/`
32
+ output, optimized for browser delivery (hashed manifest, per-quill bundle
33
+ zips, dehydrated shared font store, stable pointer file). Authors do not
34
+ version it, publish it, or check it into git.
35
+
36
+ Loaders are paired 1:1 with what they load:
37
+
38
+ - `Quiver.fromPackage(specifier)` and `Quiver.fromDir(path)` load the
39
+ **source** layout (Node only).
40
+ - `Quiver.fromBuilt(url)` loads the **build output** over HTTP/HTTPS
41
+ (browser-safe; works in Node too).
42
+
43
+ There is no auto-detection: each loader's name commits to what it expects.
44
+
45
+ **Naming decisions:**
46
+ - The verb is `build()`. Pairs with `fromBuilt()` (past participle = "the
47
+ built one"). Avoids collision with `npm pack`, JS bundler vocabulary,
48
+ and the internal "bundle zips" term.
49
+ - Loaders for source layouts are named by **where the source lives**
50
+ (`Package` / `Dir`). The loader for build output is named by **what it
51
+ loads** (`Built`), not by transport — `fromUrl` was rejected because it
52
+ hides the artifact type.
53
+ - Build output is HTTP-served only. Loading a built artifact from local
54
+ disk is intentionally unsupported in V1; consumers either spin up a
55
+ local HTTP server or use `fromPackage`/`fromDir` against the source.
56
+ Adding `Quiver.fromBuiltDir(path)` later is purely additive if a real
57
+ use case appears.
43
58
 
44
59
  ### 2) `Quiver.yaml` Is Required in Source Quivers
45
60
 
@@ -57,10 +72,16 @@ Unknown fields in `Quiver.yaml` are a **validation error** (`quiver_invalid`). S
57
72
  ### 3) `QuillSource` Becomes Quiver-Centric
58
73
 
59
74
  - Re-express `QuillSource` concepts around Quivers
60
- - Split old filesystem concept into:
61
- - **Source filesystem loader** (reads Source Quiver directly)
62
- - **Packed filesystem transport** (loads Packed Quiver artifact from disk)
63
- - Treat HTTP as a Packed transport, not a separate format concept
75
+ - Three loaders, each with one input shape and one shape-of-thing-loaded:
76
+ - `Quiver.fromPackage(specifier)` Node-only; resolves an npm specifier
77
+ against `node_modules` and loads the source layout at the package root
78
+ - `Quiver.fromDir(path)` Node-only; loads source layout from a local
79
+ directory. Also accepts `import.meta.url`-style `file://` URLs as a
80
+ convenience for tests (the URL's parent directory is used)
81
+ - `Quiver.fromBuilt(url)` — browser-safe; loads build output from an
82
+ `http(s)://` or origin-relative URL
83
+ - "Transport" is not a first-class concept; HTTP fetching is an internal
84
+ detail of `fromBuilt`
64
85
 
65
86
  ### 4) Multi-Quiver Composition with Deterministic Precedence
66
87
 
@@ -118,7 +139,10 @@ Canonicalization:
118
139
  - `warm()` warms all by default in V1
119
140
  - `resolve()` must work even if nothing is warmed
120
141
 
121
- Warm now means "load/prepare quills and artifacts", not "register in engine". There is no engine registration step anymore.
142
+ Warm means "load/prepare quills and materialize render-ready instances",
143
+ not "register in engine". There is no engine registration step anymore.
144
+ Warm semantics are identical for source-loaded and built-output-loaded
145
+ quivers; the loader hides the difference.
122
146
 
123
147
  ### 7) Engine Boundary: New Canonical Contract (Node / JS–WASM only)
124
148
 
@@ -150,16 +174,41 @@ Upstream behavior note:
150
174
 
151
175
  ### 9) Distribution Strategy
152
176
 
153
- V1 supports:
154
-
155
- - npm distribution for Source Quiver projects
156
- - git/folder-copy consumption of Source Quivers
157
- - `pack()` output for Packed runtime distribution
158
-
159
- Clarification:
160
-
161
- - npm/git are developer distribution channels
162
- - packed artifacts are runtime delivery artifacts
177
+ **Source-first distribution.** The published artifact is the **Source
178
+ Quiver** — an npm package whose root contains `Quiver.yaml`. Consumers
179
+ choose how to consume it:
180
+
181
+ - **Node consumers** load the source layout directly:
182
+ ```ts
183
+ const quiver = await Quiver.fromPackage("@org/my-quiver");
184
+ ```
185
+ - **Browser consumers** run a build step against the resolved source dir
186
+ and serve the output as static assets:
187
+ ```ts
188
+ // build step (Node)
189
+ await Quiver.build("./node_modules/@org/my-quiver", "./public/quivers/my-quiver");
190
+ // browser runtime
191
+ const quiver = await Quiver.fromBuilt("/quivers/my-quiver/");
192
+ ```
193
+
194
+ Rationale:
195
+
196
+ - Author release pipeline is `npm publish` (or `git tag`). No second
197
+ artifact, no CDN, no hash bookkeeping outside the npm tarball.
198
+ - Deployment topology is the consumer's concern, not the author's.
199
+ - The runtime artifact is a build output of the source, not a peer
200
+ distribution shape (see §1).
201
+
202
+ **Pre-built distribution as a published artifact is supported but not
203
+ the default.** Authors who need to ship runtime-ready output directly
204
+ (e.g. their consumers cannot run a Node build step) may publish
205
+ `Quiver.build(...)` output to a CDN and instruct consumers to use
206
+ `Quiver.fromBuilt(<cdn-url>)`. Treated as the exception.
207
+
208
+ Validation responsibility shifts left: authors should run
209
+ `Quiver.fromDir` and `Quiver.build` in CI so `quiver_invalid` errors
210
+ surface on publish, not on the consumer's build. The bundled
211
+ `@quillmark/quiver/testing` harness covers this.
163
212
 
164
213
  ---
165
214
 
@@ -167,19 +216,20 @@ Clarification:
167
216
 
168
217
  V1 intentionally retains:
169
218
 
170
- 1. Font dehydration as a Packed Quiver property
171
- 2. Consumer validation tooling for Source Quivers (+ optional Packed parity checks)
172
- 3. Manifest pointer resolution for Packed format
173
- 4. HTTP loading as a Packed transport
174
- 5. Source filesystem loading as first-class dev loop
175
- 6. Packed filesystem loading as first-class runtime option
176
- 7. Typed errors (`QuiverError`) with quiver/transport context
177
- 8. Concurrency coalescing for in-flight loads
178
- 9. Preload/fail-fast helpers where they still add value
219
+ 1. Font dehydration as a build-output property
220
+ 2. Consumer validation tooling for source layouts (+ optional build-parity checks)
221
+ 3. Manifest pointer resolution for build output
222
+ 4. HTTP/HTTPS loading via `Quiver.fromBuilt`
223
+ 5. Source layout loading as first-class dev loop (`fromPackage` / `fromDir`)
224
+ 6. Typed errors (`QuiverError`) with quiver/source context
225
+ 7. Concurrency coalescing for in-flight loads
226
+ 8. Preload/fail-fast helpers where they still add value
179
227
 
180
228
  Removed from carryover assumptions:
181
229
 
182
230
  - Any engine-registration cache fast path (`register`/`has`) because upstream removed the capability
231
+ - "Transport" as a user-facing concept (folded into `fromBuilt` internals)
232
+ - Loading build output from local disk (no `fromBuiltDir` in V1; YAGNI)
183
233
 
184
234
  ---
185
235
 
@@ -188,7 +238,7 @@ Removed from carryover assumptions:
188
238
  V1 should trim public API where behavior can stay internal:
189
239
 
190
240
  - Drop internal-only utility exports
191
- - Keep engine payload and transport internals opaque
241
+ - Keep engine payload and loader internals opaque
192
242
  - Consolidate validation exports
193
243
  - Avoid duplicate entry points for equivalent validation workflows
194
244
  - Do not expose internal quill-object cache mechanisms as public contract
@@ -218,18 +268,22 @@ Errors must include offending ref/version/quiver identifiers when available.
218
268
 
219
269
  ---
220
270
 
221
- ## Runtime + Packaging Model
271
+ ## Runtime + Build Model
222
272
 
223
273
  V1 runtime loading paths:
224
274
 
225
- 1. Source Quiver from filesystem (authoring/dev)
226
- 2. Packed Quiver over HTTP (browser/runtime)
227
- 3. Packed Quiver from filesystem (air-gapped/container/runtime)
275
+ 1. `Quiver.fromPackage(specifier)` npm package resolution; loads source
276
+ (authoring/dev/Node runtime)
277
+ 2. `Quiver.fromDir(path)` local directory; loads source (Node)
278
+ 3. `Quiver.fromBuilt(url)` — HTTP(S); loads build output (browser; also
279
+ works in Node)
228
280
 
229
- V1 packaging behavior:
281
+ V1 build behavior:
230
282
 
231
- - `Quiver.pack()` produces Packed Quiver artifact independent of transport
232
- - packed output includes pointer + hashed manifest + bundles + dehydrated font store
283
+ - `Quiver.build(srcDir, outDir)` produces the runtime artifact from a
284
+ source layout
285
+ - output includes pointer + hashed manifest + bundles + dehydrated font store
286
+ (see "Runtime Artifact Format" below)
233
287
 
234
288
  Execution behavior:
235
289
 
@@ -261,7 +315,11 @@ Caching scope:
261
315
  - Non-canonical version directories are a validation error (`quiver_invalid`).
262
316
  - Dedup of identical fonts across quills happens at pack time (into `store/<md5>`), not at the source layer.
263
317
 
264
- ## Packed Quiver Format (normative)
318
+ ## Runtime Artifact Format (normative)
319
+
320
+ Produced by `Quiver.build()`. Authors do not author this layout; consumers
321
+ do not need to inspect it. It is an implementation detail of build output,
322
+ specified here only because loaders must agree on its shape.
265
323
 
266
324
  ```
267
325
  <root>/
@@ -272,7 +330,7 @@ Caching scope:
272
330
  <md5> # raw font bytes, no extension
273
331
  ```
274
332
 
275
- **Hash:** MD5 prefix-6, computed with `node:crypto` at `pack()` time only (dev/tooling; not browser runtime).
333
+ **Hash:** MD5 prefix-6, computed with `node:crypto` at `build()` time only (dev/tooling; not browser runtime).
276
334
 
277
335
  **Pointer** `Quiver.json`:
278
336
  ```json
@@ -295,23 +353,36 @@ Caching scope:
295
353
  }
296
354
  ```
297
355
 
298
- **Bundle zips** contain pure quill content (`Quill.yaml` + templates + partials + non-font assets). Fonts are dehydrated at pack time: their bytes live only in `store/<md5>`; their path→hash mapping lives only in the hashed manifest. Bundles do **not** embed a `fonts.json`.
356
+ **Bundle zips** contain pure quill content (`Quill.yaml` + templates + partials + non-font assets). Fonts are dehydrated at build time: their bytes live only in `store/<md5>`; their path→hash mapping lives only in the hashed manifest. Bundles do **not** embed a `fonts.json`.
299
357
 
300
- Rehydration on load: transport fetches the pointer → hashed manifest → required bundle(s) → required `store/<md5>` blobs; library reconstructs the full in-memory tree and builds a render-ready quill via `engine.quill(tree)`.
358
+ Rehydration on load: the loader fetches the pointer → hashed manifest → required bundle(s) → required `store/<md5>` blobs; library reconstructs the full in-memory tree and builds a render-ready quill via `engine.quill(tree)`.
301
359
 
302
360
  ## API Surface (V1)
303
361
 
304
- Single class, three factories:
362
+ Single class, three loaders + one builder. Each loader has one input
363
+ shape and one shape-of-thing-loaded; no auto-detection.
305
364
 
306
365
  ```ts
307
366
  class Quiver {
308
- // Node-only factories (from `@quillmark/quiver/node`). Fail fast in browser.
309
- static fromSourceDir(path: string): Promise<Quiver>;
310
- static fromPackedDir(path: string): Promise<Quiver>;
311
- // Browser-safe factory (from `@quillmark/quiver` main).
312
- static fromHttp(url: string): Promise<Quiver>;
313
- // Node-only tooling. Writes a Packed Quiver to outDir.
314
- static pack(sourceDir: string, outDir: string, opts?: PackOptions): Promise<void>;
367
+ // Node-only loader: resolve an npm specifier against node_modules and
368
+ // load the source layout at the package root.
369
+ // (From `@quillmark/quiver/node`.)
370
+ static fromPackage(specifier: string): Promise<Quiver>;
371
+
372
+ // Node-only loader: load source layout from a local directory.
373
+ // Also accepts `import.meta.url`-style `file://` URLs (the URL's parent
374
+ // directory is used) as a convenience for tests.
375
+ // (From `@quillmark/quiver/node`.)
376
+ static fromDir(pathOrFileUrl: string): Promise<Quiver>;
377
+
378
+ // Browser-safe loader: load build output from an http(s):// or
379
+ // origin-relative URL. Throws `transport_error` on file:// inputs.
380
+ // (From `@quillmark/quiver` main.)
381
+ static fromBuilt(url: string): Promise<Quiver>;
382
+
383
+ // Node-only tooling: produce the runtime artifact from a source layout.
384
+ // (From `@quillmark/quiver/node`.)
385
+ static build(sourceDir: string, outDir: string, opts?: BuildOptions): Promise<void>;
315
386
 
316
387
  readonly name: string; // from Quiver.yaml
317
388
 
@@ -345,7 +416,7 @@ class QuiverError extends Error {
345
416
 
346
417
  **No render wrapper.** Callers invoke `quill.render(doc, opts?)` (and `quill.open(doc)` when needed) after `resolve()` + `getQuill()`. Quiver never mirrors Quillmark render APIs.
347
418
 
348
- **Internal (not exported):** `QuiverTransport`, `QuiverManifest` (runtime shape), `parseQuillRef`, in-flight coalescing state.
419
+ **Internal (not exported):** `QuiverManifest` (runtime shape), `parseQuillRef`, in-flight coalescing state, source-vs-built layout detection.
349
420
 
350
421
  Hot-path flow:
351
422
  ```ts
@@ -360,13 +431,23 @@ const result = quill.render(doc, { format: "pdf" });
360
431
  **Name:** `@quillmark/quiver`
361
432
 
362
433
  **Entrypoints:**
363
- - `@quillmark/quiver` (main, browser-safe): `Quiver` class with only `fromHttp` functional (Node-only factories/pack throw `transport_error` if reached in browser), `QuiverRegistry`, `QuiverError`, shared types.
364
- - `@quillmark/quiver/node`: adds `Quiver.fromSourceDir`, `Quiver.fromPackedDir`, `Quiver.pack` behaviors. Single `Quiver` class — Node-only factories fail fast outside Node.
434
+ - `@quillmark/quiver` (main, browser-safe): `Quiver` class with only
435
+ `fromBuilt` functional (Node-only loaders/builder throw
436
+ `transport_error` if reached in browser), `QuiverRegistry`,
437
+ `QuiverError`, `QuillmarkLike`, `QuillLike`, shared types.
438
+ - `@quillmark/quiver/node`: adds `Quiver.fromPackage`, `Quiver.fromDir`,
439
+ `Quiver.build` behaviors. Single `Quiver` class — Node-only factories
440
+ fail fast outside Node.
441
+ - `@quillmark/quiver/testing` (Node-only): single export
442
+ `runQuiverTests(metaUrlOrDir, engine)` built on `node:test` (zero
443
+ external test-runner dependency). Optional convenience; users on other
444
+ test runners wire their own loops against the main API.
365
445
 
366
446
  **Dependencies:**
367
447
  - Peer: `@quillmark/wasm@>=0.59.0-rc.2` with `Quillmark`, `Document.fromMarkdown`, `engine.quill(tree)`, and `quill.render(doc, opts?)` APIs.
368
448
  - Runtime: `fflate ^0.8.2` for zip read/write (Node + browser)
369
- - Dev-only: `node:crypto` (MD5 hashing in `pack()` — never reached at runtime)
449
+ - Dev-only: `node:crypto` (MD5 hashing in `build()` — never reached at runtime)
450
+ - No test-runner peer dependency; `/testing` uses `node:test` (built-in)
370
451
 
371
452
  ---
372
453
 
@@ -388,12 +469,12 @@ const result = quill.render(doc, { format: "pdf" });
388
469
 
389
470
  All V1 planner questions resolved; implementation plan can proceed against the spec above.
390
471
 
391
- 1. ~~Final `Quiver` interface shape and transport factoring style~~ → Single `Quiver` class, three static factories (`fromHttp`, `fromSourceDir`, `fromPackedDir`). Transport kept internal (no `fromTransport` in V1; YAGNI).
472
+ 1. ~~Final `Quiver` interface shape and transport factoring style~~ → Single `Quiver` class, three loaders (`fromPackage`, `fromDir`, `fromBuilt`) + one builder (`build`). Each loader names what it loads; no auto-detection.
392
473
  2. ~~Final `Quiver.yaml` schema and unknown-field policy~~ → See §2: alphanumeric `name` and optional tooling-only `description`. Unknown fields are `quiver_invalid`.
393
474
  3. ~~Canonical ref grammar and parser API contract~~ → Internal `parseQuillRef`, not exported. Selector syntax per §5. Throws `invalid_ref`.
394
475
  4. ~~Exact warning policy for shadowed refs across quivers~~ → No warnings in V1. Precedence is a hard filter (§4); duplicate quiver names error as `quiver_collision`.
395
- 5. ~~Validation API shape consolidation~~ → No separate validation API. Validation errors surface as `QuiverError('quiver_invalid')` during load or `pack()`.
396
- 6. ~~Pack artifact directory structure~~ → See "Packed Quiver Format (normative)".
476
+ 5. ~~Validation API shape consolidation~~ → No separate validation API. Validation errors surface as `QuiverError('quiver_invalid')` during load or `build()`.
477
+ 6. ~~Build output directory structure~~ → See "Runtime Artifact Format (normative)".
397
478
  7. ~~Node/browser entrypoint split~~ → See "Package Structure": main + `/node` subpath, single `Quiver` class.
398
479
  8. ~~Final exported type names~~ → `Quiver`, `QuiverRegistry`, `QuiverError`. Hot-path entry is `QuiverRegistry.resolve(ref)` + `QuiverRegistry.getQuill(canonicalRef)`.
399
480
 
@@ -413,7 +494,8 @@ Local copies in this repo for `@quillmark/quiver` implementation:
413
494
  ## Success Criteria
414
495
 
415
496
  - A team can author and validate a Source Quiver locally with fast filesystem loops
416
- - A packed artifact can be loaded via HTTP or local filesystem with equivalent semantics
497
+ - Build output can be loaded over HTTP/HTTPS with parity behavior in browser and Node
498
+ - Each loader names exactly what it loads — no auto-detection, no ambiguous return shape
417
499
  - Multi-quiver resolution is deterministic and matches precedence hard-filter rules
418
500
  - Selector behavior is predictable and explicitly documented
419
501
  - Quiver (Node) integrates via `engine.quill(tree)` + `quill.render(...)` only (no engine quill registration path)
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @quillmark/quiver
2
2
 
3
- Quiver registry and packaging for Quillmark — load, compose, and pack collections of quills for rendering with `@quillmark/wasm`.
3
+ Quiver registry and build tooling for Quillmark — load, compose, and build collections of quills for rendering with `@quillmark/wasm`.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,49 +8,97 @@ Quiver registry and packaging for Quillmark — load, compose, and pack collecti
8
8
  npm install @quillmark/quiver @quillmark/wasm
9
9
  ```
10
10
 
11
- ## Quick start
11
+ ## Distribution model
12
+
13
+ A Quiver has one authored shape: the **source layout** (`Quiver.yaml` at the
14
+ package root, quills under `quills/<name>/<x.y.z>/`). Authors publish it as
15
+ an npm package. Consumers decide how to consume it:
16
+
17
+ - **Node consumers** load the source layout directly with `Quiver.fromPackage`.
18
+ - **Browser consumers** run `Quiver.build(...)` as a build step and serve the
19
+ output as static assets, loading it with `Quiver.fromBuilt`.
20
+
21
+ Each loader names exactly what it loads: `fromPackage` and `fromDir` always
22
+ read source layouts; `fromBuilt` always reads build output over HTTP/HTTPS.
23
+ No auto-detection, no branching on artifact shape.
24
+
25
+ This keeps the author flow to a single command (`npm publish` or `git tag`)
26
+ and puts the deployment-topology decision where it belongs: with the
27
+ consumer.
28
+
29
+ ## Authoring a quiver
30
+
31
+ Lay out the source per the spec, then publish to npm (or push a git tag):
32
+
33
+ ```
34
+ my-quiver/
35
+ Quiver.yaml
36
+ quills/
37
+ <name>/<x.y.z>/
38
+ Quill.yaml
39
+ ...
40
+ package.json
41
+ ```
42
+
43
+ Recommended CI: use the bundled `@quillmark/quiver/testing` harness — it
44
+ loads with `Quiver.fromDir` and exercises every quill so validation errors
45
+ surface on publish, not on the consumer's build. The harness uses
46
+ `node:test` (built into Node 18+); no extra test-runner dependency
47
+ required. If you prefer vitest/jest/mocha, write a 12-line loop against
48
+ the main API instead.
49
+
50
+ ## Consuming a quiver (Node)
12
51
 
13
52
  ```ts
14
53
  import { Quillmark, Document } from "@quillmark/wasm";
15
54
  import { Quiver, QuiverRegistry } from "@quillmark/quiver/node";
16
55
 
17
- // 1. Load a source quiver from disk (Node.js only)
18
- const quiver = await Quiver.fromSourceDir("./my-quiver");
56
+ const quiver = await Quiver.fromPackage("@org/my-quiver");
19
57
 
20
- // 2. Build a registry with one or more quivers
21
58
  const engine = new Quillmark();
22
59
  const registry = new QuiverRegistry({ engine, quivers: [quiver] });
23
60
 
24
- // 3. Resolve a ref, obtain a render-ready quill, and render
25
61
  const doc = Document.fromMarkdown(markdownString);
26
62
  const canonicalRef = await registry.resolve(doc.quillRef);
27
63
  const quill = await registry.getQuill(canonicalRef);
28
64
  const result = quill.render(doc, { format: "pdf" });
29
65
  ```
30
66
 
31
- ## HTTP (browser / CDN)
67
+ ## Consuming a quiver (browser)
68
+
69
+ Browsers cannot read the source layout directly, so build at deploy time and
70
+ serve the output as static files:
32
71
 
33
72
  ```ts
34
- import { Quiver, QuiverRegistry } from "@quillmark/quiver";
73
+ // build script (Node) typically wired into your existing build pipeline
74
+ import { Quiver } from "@quillmark/quiver/node";
35
75
 
36
- const quiver = await Quiver.fromHttp("https://cdn.example.com/my-quiver");
37
- const registry = new QuiverRegistry({ engine, quivers: [quiver] });
76
+ await Quiver.build(
77
+ "./node_modules/@org/my-quiver",
78
+ "./public/quivers/my-quiver",
79
+ );
38
80
  ```
39
81
 
40
- ## Packed directory (Node.js)
41
-
42
82
  ```ts
43
- import { Quiver } from "@quillmark/quiver/node";
83
+ // browser runtime
84
+ import { Quiver, QuiverRegistry } from "@quillmark/quiver";
44
85
 
45
- const quiver = await Quiver.fromPackedDir("./dist/my-quiver");
86
+ const quiver = await Quiver.fromBuilt("/quivers/my-quiver/");
87
+ const registry = new QuiverRegistry({ engine, quivers: [quiver] });
46
88
  ```
47
89
 
48
- ## Pack a source quiver
90
+ ## Advanced: pre-built distribution to a CDN
91
+
92
+ If you need to ship the runtime artifact directly (e.g. consumers cannot run
93
+ a Node build step), publish `Quiver.build` output to a CDN and have
94
+ consumers point `fromBuilt` at the CDN URL:
49
95
 
50
96
  ```ts
51
97
  import { Quiver } from "@quillmark/quiver/node";
52
98
 
53
- await Quiver.pack("./my-quiver", "./dist/my-quiver");
99
+ await Quiver.build("./my-quiver", "./dist/my-quiver");
100
+ // upload ./dist/my-quiver to https://cdn.example.com/quivers/my-quiver/
101
+ const quiver = await Quiver.fromBuilt("https://cdn.example.com/quivers/my-quiver/");
54
102
  ```
55
103
 
56
104
  ## Warm (prefetch all quills)
@@ -93,4 +141,4 @@ Error codes: `invalid_ref`, `quill_not_found`, `quiver_invalid`, `transport_erro
93
141
 
94
142
  ## Full specification
95
143
 
96
- See [PROGRAM.md](./PROGRAM.md) for the complete API surface, packed format specification, and design decisions.
144
+ See [PROGRAM.md](./PROGRAM.md) for the complete API surface, runtime artifact format specification, and design decisions.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Build logic — internal, Node-only.
3
+ *
4
+ * All Node.js built-in imports are done dynamically inside `buildQuiver`
5
+ * so that a type-only import of `BuildOptions` from `src/index.ts` does
6
+ * NOT pull `node:fs` or `node:crypto` into browser bundles.
7
+ */
8
+ /** Reserved for future build options (e.g. compression level, filters). */
9
+ export type BuildOptions = Record<string, never>;
10
+ /**
11
+ * Reads a Source Quiver, validates it, and writes the build output to outDir.
12
+ *
13
+ * Output layout:
14
+ * outDir/
15
+ * Quiver.json # stable pointer
16
+ * manifest.<md5prefix6>.json # hashed manifest
17
+ * <name>@<version>.<md5>.zip # one bundle per quill
18
+ * store/
19
+ * <md5> # dehydrated font bytes (full hash, no ext)
20
+ *
21
+ * Throws:
22
+ * - `quiver_invalid` on source validation failures (propagated from scanner)
23
+ * - `transport_error` on I/O failures
24
+ */
25
+ export declare function buildQuiver(sourceDir: string, outDir: string, _opts?: BuildOptions): Promise<void>;
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Pack logic — internal, Node-only.
2
+ * Build logic — internal, Node-only.
3
3
  *
4
- * All Node.js built-in imports are done dynamically inside `packQuiver` so
5
- * that a type-only import of `PackOptions` from `src/index.ts` does NOT
6
- * pull `node:fs` or `node:crypto` into browser bundles.
4
+ * All Node.js built-in imports are done dynamically inside `buildQuiver`
5
+ * so that a type-only import of `BuildOptions` from `src/index.ts` does
6
+ * NOT pull `node:fs` or `node:crypto` into browser bundles.
7
7
  */
8
8
  import { QuiverError } from "./errors.js";
9
9
  import { packFiles } from "./bundle.js";
10
- /** Font file extensions recognised by the packer (case-insensitive). */
10
+ /** Font file extensions recognised by the builder (case-insensitive). */
11
11
  const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
12
12
  /**
13
- * Reads a Source Quiver, validates it, and writes a Packed Quiver to outDir.
13
+ * Reads a Source Quiver, validates it, and writes the build output to outDir.
14
14
  *
15
15
  * Output layout:
16
16
  * outDir/
@@ -24,7 +24,7 @@ const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
24
24
  * - `quiver_invalid` on source validation failures (propagated from scanner)
25
25
  * - `transport_error` on I/O failures
26
26
  */
27
- export async function packQuiver(sourceDir, outDir, _opts) {
27
+ export async function buildQuiver(sourceDir, outDir, _opts) {
28
28
  // Dynamic imports keep this module safe to type-import from browser contexts.
29
29
  const { join } = await import("node:path");
30
30
  const { mkdir, rm, writeFile, } = await import("node:fs/promises");
@@ -1,27 +1,27 @@
1
1
  /**
2
- * Packed Quiver loader — browser-safe at module level.
2
+ * Built-quiver loader — browser-safe at module level.
3
3
  * Internal; not exported from index.ts.
4
4
  *
5
5
  * Exposes:
6
- * - PackedTransport interface (also used by FsTransport / HttpTransport)
7
- * - loadPackedQuiver(transport) → Quiver
6
+ * - BuiltTransport interface (implemented by HttpTransport)
7
+ * - loadBuiltQuiver(transport) → Quiver
8
8
  *
9
9
  * NO static node: imports — this module is safe to load in browser contexts.
10
10
  */
11
11
  import { Quiver } from "./quiver.js";
12
12
  /**
13
13
  * Transport abstraction: fetch raw bytes by relative path within the packed
14
- * artifact. Implementations are FsTransport (Node) and HttpTransport (browser).
14
+ * artifact. Sole implementation is HttpTransport (browser + Node).
15
15
  */
16
- export interface PackedTransport {
16
+ export interface BuiltTransport {
17
17
  fetchBytes(relativePath: string): Promise<Uint8Array>;
18
18
  }
19
19
  /**
20
- * Load a Packed Quiver via the given transport.
20
+ * Load a build-output quiver via the given transport.
21
21
  *
22
22
  * 1. Fetches Quiver.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
- * 4. Returns a Quiver instance backed by a PackedLoader.
25
+ * 4. Returns a Quiver instance backed by a BuiltLoader.
26
26
  */
27
- export declare function loadPackedQuiver(transport: PackedTransport): Promise<Quiver>;
27
+ export declare function loadBuiltQuiver(transport: BuiltTransport): Promise<Quiver>;
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Packed Quiver loader — browser-safe at module level.
2
+ * Built-quiver loader — browser-safe at module level.
3
3
  * Internal; not exported from index.ts.
4
4
  *
5
5
  * Exposes:
6
- * - PackedTransport interface (also used by FsTransport / HttpTransport)
7
- * - loadPackedQuiver(transport) → Quiver
6
+ * - BuiltTransport interface (implemented by HttpTransport)
7
+ * - loadBuiltQuiver(transport) → Quiver
8
8
  *
9
9
  * NO static node: imports — this module is safe to load in browser contexts.
10
10
  */
@@ -31,8 +31,8 @@ function validateFontHash(hash, context) {
31
31
  throw new QuiverError("quiver_invalid", `${context}: font hash is invalid: "${hash}"`);
32
32
  }
33
33
  }
34
- // ─── PackedLoader implementation ─────────────────────────────────────────────
35
- class PackedLoader {
34
+ // ─── BuiltLoader implementation ─────────────────────────────────────────────
35
+ class BuiltLoader {
36
36
  transport;
37
37
  index;
38
38
  /** Font byte cache: hash → in-flight or resolved Promise. */
@@ -50,7 +50,7 @@ class PackedLoader {
50
50
  // through. The transport will surface a transport_error naturally.
51
51
  // (Defensive: this should not be reachable in normal operation.)
52
52
  if (!entry) {
53
- throw new QuiverError("transport_error", `Quill "${name}@${version}" not found in packed quiver manifest`, { version, ref: `${name}@${version}` });
53
+ throw new QuiverError("transport_error", `Quill "${name}@${version}" not found in built-quiver manifest`, { version, ref: `${name}@${version}` });
54
54
  }
55
55
  // 1. Fetch + unpack bundle zip.
56
56
  const zipBytes = await this.transport.fetchBytes(entry.bundle);
@@ -177,14 +177,14 @@ function parseManifest(raw) {
177
177
  }
178
178
  // ─── Main entry point ─────────────────────────────────────────────────────────
179
179
  /**
180
- * Load a Packed Quiver via the given transport.
180
+ * Load a build-output quiver via the given transport.
181
181
  *
182
182
  * 1. Fetches Quiver.json (pointer) and parses it.
183
183
  * 2. Fetches the manifest file it points to and validates it.
184
184
  * 3. Builds a catalog from manifest entries (versions sorted descending).
185
- * 4. Returns a Quiver instance backed by a PackedLoader.
185
+ * 4. Returns a Quiver instance backed by a BuiltLoader.
186
186
  */
187
- export async function loadPackedQuiver(transport) {
187
+ export async function loadBuiltQuiver(transport) {
188
188
  // 1. Fetch and parse pointer.
189
189
  let pointerBytes;
190
190
  try {
@@ -226,7 +226,7 @@ export async function loadPackedQuiver(transport) {
226
226
  versions.sort((a, b) => compareSemver(b, a));
227
227
  }
228
228
  // 4. Build loader.
229
- const loader = new PackedLoader(transport, index);
229
+ const loader = new BuiltLoader(transport, index);
230
230
  // 5. Return Quiver via internal factory.
231
231
  return Quiver._fromLoader(manifest.name, catalogRaw, loader);
232
232
  }
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export { QuiverError } from "./errors.js";
2
2
  export type { QuiverErrorCode } from "./errors.js";
3
3
  export { Quiver } from "./quiver.js";
4
4
  export { QuiverRegistry } from "./registry.js";
5
- export type { PackOptions } from "./pack.js";
5
+ export type { BuildOptions } from "./build.js";
6
+ export type { QuillmarkLike, QuillLike } from "./engine-types.js";
package/dist/node.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Node-only entrypoint.
2
2
  // Re-export everything from the main browser-safe entrypoint.
3
- // Node-only factories (fromSourceDir, fromPackedDir, pack) are methods on
3
+ // Node-only factories (fromPackage, fromDir, build) are methods on
4
4
  // Quiver itself — no additional exports are needed here.
5
5
  export * from "./index.js";
package/dist/quiver.d.ts CHANGED
@@ -2,10 +2,10 @@
2
2
  * Quiver — primary runtime abstraction for a collection of quills.
3
3
  *
4
4
  * Polymorphism via composition: internally stores a pluggable loader
5
- * (either source-backed or packed-backed).
5
+ * (either source-backed or build-output-backed).
6
6
  */
7
- import type { PackOptions } from "./pack.js";
8
- /** @internal Internal loader strategy: source or packed. */
7
+ import type { BuildOptions } from "./build.js";
8
+ /** @internal Internal loader strategy: source or build output. */
9
9
  export interface QuiverLoader {
10
10
  loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
11
11
  }
@@ -18,33 +18,40 @@ export declare class Quiver {
18
18
  * Static methods inside can still call it.
19
19
  */
20
20
  private constructor();
21
- /** @internal Used by loadPackedQuiver. Not part of the public API. */
21
+ /** @internal Used by loadBuiltQuiver. Not part of the public API. */
22
22
  static _fromLoader(name: string, catalog: Map<string, string[]>, loader: QuiverLoader): Quiver;
23
23
  /**
24
- * Node-only factory. Reads a Source Quiver from a directory containing
25
- * `Quiver.yaml` and `quills/<name>/<version>/Quill.yaml` entries.
24
+ * Node-only factory. Resolves an npm specifier against `node_modules` and
25
+ * loads the source layout at the package root.
26
26
  *
27
- * Uses dynamic import of `./source-loader.js` so that importing this module
28
- * in a browser environment does not cause a crash at module evaluation time.
27
+ * The resolved package must have `Quiver.yaml` at its root.
29
28
  *
30
- * Throws `quiver_invalid` on schema violations, `transport_error` on I/O failure.
29
+ * Throws `transport_error` on resolution/I/O failure, `quiver_invalid`
30
+ * on schema violations.
31
31
  */
32
- static fromSourceDir(path: string): Promise<Quiver>;
32
+ static fromPackage(specifier: string): Promise<Quiver>;
33
33
  /**
34
- * Node-only factory. Loads a Packed Quiver from a local directory.
34
+ * Node-only factory. Reads a Source Quiver from a local directory containing
35
+ * `Quiver.yaml` and `quills/<name>/<version>/Quill.yaml` entries.
35
36
  *
36
- * Uses dynamic imports so this module stays browser-safe at evaluation time.
37
+ * Also accepts `import.meta.url`-style `file://` URLs as a convenience for
38
+ * tests; the URL's parent directory is used as the source root.
37
39
  *
38
- * Throws `transport_error` on I/O failure, `quiver_invalid` on format errors.
40
+ * Throws `quiver_invalid` on schema violations, `transport_error` on I/O failure.
39
41
  */
40
- static fromPackedDir(path: string): Promise<Quiver>;
42
+ static fromDir(pathOrFileUrl: string): Promise<Quiver>;
41
43
  /**
42
- * Browser-safe factory. Loads a Packed Quiver from an HTTP base URL.
44
+ * Browser-safe factory. Loads build output from an HTTP/HTTPS URL.
45
+ *
46
+ * Origin-relative URLs (e.g. `/quivers/foo/`) are accepted in browser
47
+ * environments. `file://` URLs are rejected — local build output is
48
+ * not loadable in V1; serve over HTTP or use `fromPackage`/`fromDir`
49
+ * against the source.
43
50
  *
44
- * Throws `transport_error` on network/HTTP failure, `quiver_invalid` on
45
- * format errors.
51
+ * Throws `transport_error` on network/HTTP failure, `quiver_invalid`
52
+ * on format errors.
46
53
  */
47
- static fromHttp(url: string): Promise<Quiver>;
54
+ static fromBuilt(url: string): Promise<Quiver>;
48
55
  /** Returns all known quill names, sorted lexicographically. */
49
56
  quillNames(): string[];
50
57
  /**
@@ -53,15 +60,16 @@ export declare class Quiver {
53
60
  */
54
61
  versionsOf(name: string): string[];
55
62
  /**
56
- * Node-only tooling. Writes a Packed Quiver artifact to outDir.
63
+ * Node-only tooling. Reads the Source Quiver at sourceDir, validates it,
64
+ * and writes the runtime build artifact to outDir.
57
65
  *
58
- * Uses dynamic import of `./pack.js` so that this module stays browser-safe
59
- * at evaluation time.
66
+ * Uses dynamic import of `./build.js` so that this module stays
67
+ * browser-safe at evaluation time.
60
68
  *
61
69
  * Throws `quiver_invalid` on source validation failures,
62
70
  * `transport_error` on I/O failures.
63
71
  */
64
- static pack(sourceDir: string, outDir: string, opts?: PackOptions): Promise<void>;
72
+ static build(sourceDir: string, outDir: string, opts?: BuildOptions): Promise<void>;
65
73
  /**
66
74
  * Lazily loads the file tree for a specific quill version.
67
75
  *
package/dist/quiver.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Quiver — primary runtime abstraction for a collection of quills.
3
3
  *
4
4
  * Polymorphism via composition: internally stores a pluggable loader
5
- * (either source-backed or packed-backed).
5
+ * (either source-backed or build-output-backed).
6
6
  */
7
7
  import { QuiverError } from "./errors.js";
8
8
  import { assertNode } from "./assert-node.js";
@@ -20,51 +20,73 @@ export class Quiver {
20
20
  this.#catalog = new Map(catalog);
21
21
  this.#loader = loader;
22
22
  }
23
- /** @internal Used by loadPackedQuiver. Not part of the public API. */
23
+ /** @internal Used by loadBuiltQuiver. Not part of the public API. */
24
24
  static _fromLoader(name, catalog, loader) {
25
25
  return new Quiver(name, catalog, loader);
26
26
  }
27
27
  /**
28
- * Node-only factory. Reads a Source Quiver from a directory containing
29
- * `Quiver.yaml` and `quills/<name>/<version>/Quill.yaml` entries.
28
+ * Node-only factory. Resolves an npm specifier against `node_modules` and
29
+ * loads the source layout at the package root.
30
30
  *
31
- * Uses dynamic import of `./source-loader.js` so that importing this module
32
- * in a browser environment does not cause a crash at module evaluation time.
31
+ * The resolved package must have `Quiver.yaml` at its root.
33
32
  *
34
- * Throws `quiver_invalid` on schema violations, `transport_error` on I/O failure.
33
+ * Throws `transport_error` on resolution/I/O failure, `quiver_invalid`
34
+ * on schema violations.
35
35
  */
36
- static async fromSourceDir(path) {
37
- assertNode("Quiver.fromSourceDir");
38
- const { scanSourceQuiver, SourceLoader } = await import("./source-loader.js");
39
- const { meta, catalog } = await scanSourceQuiver(path);
40
- const loader = new SourceLoader(path);
41
- return new Quiver(meta.name, catalog, loader);
36
+ static async fromPackage(specifier) {
37
+ assertNode("Quiver.fromPackage");
38
+ const { createRequire } = await import("node:module");
39
+ const { dirname } = await import("node:path");
40
+ const req = createRequire(import.meta.url);
41
+ let yamlPath;
42
+ try {
43
+ yamlPath = req.resolve(`${specifier}/Quiver.yaml`);
44
+ }
45
+ catch (err) {
46
+ throw new QuiverError("transport_error", `Failed to resolve quiver package "${specifier}": ${err.message}`, { cause: err });
47
+ }
48
+ return Quiver.fromDir(dirname(yamlPath));
42
49
  }
43
50
  /**
44
- * Node-only factory. Loads a Packed Quiver from a local directory.
51
+ * Node-only factory. Reads a Source Quiver from a local directory containing
52
+ * `Quiver.yaml` and `quills/<name>/<version>/Quill.yaml` entries.
45
53
  *
46
- * Uses dynamic imports so this module stays browser-safe at evaluation time.
54
+ * Also accepts `import.meta.url`-style `file://` URLs as a convenience for
55
+ * tests; the URL's parent directory is used as the source root.
47
56
  *
48
- * Throws `transport_error` on I/O failure, `quiver_invalid` on format errors.
57
+ * Throws `quiver_invalid` on schema violations, `transport_error` on I/O failure.
49
58
  */
50
- static async fromPackedDir(path) {
51
- assertNode("Quiver.fromPackedDir");
52
- const { FsTransport } = await import("./transports/fs-transport.js");
53
- const { loadPackedQuiver } = await import("./packed-loader.js");
54
- const transport = new FsTransport(path);
55
- return loadPackedQuiver(transport);
59
+ static async fromDir(pathOrFileUrl) {
60
+ assertNode("Quiver.fromDir");
61
+ let dir = pathOrFileUrl;
62
+ if (pathOrFileUrl.startsWith("file://")) {
63
+ const { fileURLToPath } = await import("node:url");
64
+ dir = fileURLToPath(new URL(".", pathOrFileUrl));
65
+ }
66
+ const { scanSourceQuiver, SourceLoader } = await import("./source-loader.js");
67
+ const { meta, catalog } = await scanSourceQuiver(dir);
68
+ const loader = new SourceLoader(dir);
69
+ return new Quiver(meta.name, catalog, loader);
56
70
  }
57
71
  /**
58
- * Browser-safe factory. Loads a Packed Quiver from an HTTP base URL.
72
+ * Browser-safe factory. Loads build output from an HTTP/HTTPS URL.
73
+ *
74
+ * Origin-relative URLs (e.g. `/quivers/foo/`) are accepted in browser
75
+ * environments. `file://` URLs are rejected — local build output is
76
+ * not loadable in V1; serve over HTTP or use `fromPackage`/`fromDir`
77
+ * against the source.
59
78
  *
60
- * Throws `transport_error` on network/HTTP failure, `quiver_invalid` on
61
- * format errors.
79
+ * Throws `transport_error` on network/HTTP failure, `quiver_invalid`
80
+ * on format errors.
62
81
  */
63
- static async fromHttp(url) {
82
+ static async fromBuilt(url) {
83
+ if (url.startsWith("file://")) {
84
+ throw new QuiverError("transport_error", `Quiver.fromBuilt requires an http(s):// or origin-relative URL; got "${url}". Local build output is not loadable in V1 — serve it over HTTP or load source via fromPackage/fromDir.`);
85
+ }
64
86
  const { HttpTransport } = await import("./transports/http-transport.js");
65
- const { loadPackedQuiver } = await import("./packed-loader.js");
87
+ const { loadBuiltQuiver } = await import("./built-loader.js");
66
88
  const transport = new HttpTransport(url);
67
- return loadPackedQuiver(transport);
89
+ return loadBuiltQuiver(transport);
68
90
  }
69
91
  /** Returns all known quill names, sorted lexicographically. */
70
92
  quillNames() {
@@ -78,18 +100,19 @@ export class Quiver {
78
100
  return [...(this.#catalog.get(name) ?? [])];
79
101
  }
80
102
  /**
81
- * Node-only tooling. Writes a Packed Quiver artifact to outDir.
103
+ * Node-only tooling. Reads the Source Quiver at sourceDir, validates it,
104
+ * and writes the runtime build artifact to outDir.
82
105
  *
83
- * Uses dynamic import of `./pack.js` so that this module stays browser-safe
84
- * at evaluation time.
106
+ * Uses dynamic import of `./build.js` so that this module stays
107
+ * browser-safe at evaluation time.
85
108
  *
86
109
  * Throws `quiver_invalid` on source validation failures,
87
110
  * `transport_error` on I/O failures.
88
111
  */
89
- static async pack(sourceDir, outDir, opts) {
90
- assertNode("Quiver.pack");
91
- const { packQuiver } = await import("./pack.js");
92
- return packQuiver(sourceDir, outDir, opts);
112
+ static async build(sourceDir, outDir, opts) {
113
+ assertNode("Quiver.build");
114
+ const { buildQuiver } = await import("./build.js");
115
+ return buildQuiver(sourceDir, outDir, opts);
93
116
  }
94
117
  /**
95
118
  * Lazily loads the file tree for a specific quill version.
@@ -2,7 +2,7 @@
2
2
  * Internal filesystem scanner for Source Quiver layout.
3
3
  *
4
4
  * Uses Node.js `fs/promises` — this module must only be imported from
5
- * Node-only contexts (fromSourceDir, etc.).
5
+ * Node-only contexts (fromDir, fromPackage, etc.).
6
6
  */
7
7
  import type { QuiverMeta } from "./quiver-yaml.js";
8
8
  import type { QuiverLoader } from "./quiver.js";
@@ -2,7 +2,7 @@
2
2
  * Internal filesystem scanner for Source Quiver layout.
3
3
  *
4
4
  * Uses Node.js `fs/promises` — this module must only be imported from
5
- * Node-only contexts (fromSourceDir, etc.).
5
+ * Node-only contexts (fromDir, fromPackage, etc.).
6
6
  */
7
7
  import { readdir, readFile, stat } from "node:fs/promises";
8
8
  import { join, relative, sep } from "node:path";
package/dist/testing.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  /**
2
- * Plug-and-play test suite for Quiver authors.
2
+ * Convenience test harness for Quiver authors using `node:test`.
3
+ *
4
+ * Built into Node 18+; no extra test-runner dependency required. If you
5
+ * prefer vitest, jest, or another runner, write a 12-line loop against
6
+ * the main API instead — every primitive used here is public.
3
7
  *
4
8
  * Usage (place this file next to your Quiver.yaml):
5
9
  *
@@ -8,32 +12,17 @@
8
12
  * const engine = await Quillmark.load();
9
13
  * runQuiverTests(import.meta.url, engine);
10
14
  *
11
- * Requires vitest in your devDependencies.
12
- */
13
- import type { QuillmarkLike, QuillLike } from "./engine-types.js";
14
- export type { QuillmarkLike, QuillLike };
15
- /**
16
- * Returns a lightweight mock engine and a record of every tree passed to it.
17
- * Useful for writing custom test helpers; not intended as a substitute for the
18
- * real engine in runQuiverTests (the mock performs no template compilation).
15
+ * Run with `node --test`.
19
16
  */
20
- export declare function makeMockEngine(): {
21
- calls: Array<Map<string, Uint8Array>>;
22
- engine: QuillmarkLike;
23
- };
17
+ import type { QuillmarkLike } from "./engine-types.js";
24
18
  /**
25
- * Registers a Vitest describe block that validates every quill version in the
26
- * quiver at `sourceDirOrMetaUrl` against the provided Quillmark engine.
27
- *
28
- * Pass `import.meta.url` when your test file lives at the quiver root (next to
29
- * Quiver.yaml). Pass an absolute directory path for any other layout.
19
+ * Registers a `node:test` describe block that validates every quill
20
+ * version in the quiver at `metaUrlOrDir` against the provided engine.
30
21
  *
31
- * Each (quill, version) pair gets its own `it()` so failures are reported
32
- * individually. The quiver and engine are initialised once in `beforeAll`.
22
+ * Pass `import.meta.url` when this file lives at the quiver root (next
23
+ * to Quiver.yaml). Pass an absolute directory path for any other layout.
33
24
  *
34
- * Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml, all
35
- * template files, and engine compilation via engine.quill(tree). A quill that
36
- * contains a template error will cause its test to fail with the engine's own
37
- * error message.
25
+ * Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml,
26
+ * all template files, and engine compilation via engine.quill(tree).
38
27
  */
39
- export declare function runQuiverTests(sourceDirOrMetaUrl: string, engine: QuillmarkLike): void;
28
+ export declare function runQuiverTests(metaUrlOrDir: string, engine: QuillmarkLike): void;
package/dist/testing.js CHANGED
@@ -1,5 +1,9 @@
1
1
  /**
2
- * Plug-and-play test suite for Quiver authors.
2
+ * Convenience test harness for Quiver authors using `node:test`.
3
+ *
4
+ * Built into Node 18+; no extra test-runner dependency required. If you
5
+ * prefer vitest, jest, or another runner, write a 12-line loop against
6
+ * the main API instead — every primitive used here is public.
3
7
  *
4
8
  * Usage (place this file next to your Quiver.yaml):
5
9
  *
@@ -8,101 +12,43 @@
8
12
  * const engine = await Quillmark.load();
9
13
  * runQuiverTests(import.meta.url, engine);
10
14
  *
11
- * Requires vitest in your devDependencies.
15
+ * Run with `node --test`.
12
16
  */
13
- import { describe, it, expect, beforeAll } from "vitest";
14
- import { readdirSync } from "node:fs";
15
- import { join, basename } from "node:path";
16
- import { fileURLToPath } from "node:url";
17
+ import { describe, it, before } from "node:test";
17
18
  import { Quiver } from "./quiver.js";
18
19
  import { QuiverRegistry } from "./registry.js";
19
20
  /**
20
- * Returns a lightweight mock engine and a record of every tree passed to it.
21
- * Useful for writing custom test helpers; not intended as a substitute for the
22
- * real engine in runQuiverTests (the mock performs no template compilation).
23
- */
24
- export function makeMockEngine() {
25
- const calls = [];
26
- const engine = {
27
- quill(tree) {
28
- calls.push(tree);
29
- return { render: () => ({ ok: true }) };
30
- },
31
- };
32
- return { calls, engine };
33
- }
34
- function resolveSourceDir(sourceDirOrMetaUrl) {
35
- if (sourceDirOrMetaUrl.startsWith("file://")) {
36
- return fileURLToPath(new URL(".", sourceDirOrMetaUrl));
37
- }
38
- return sourceDirOrMetaUrl;
39
- }
40
- function discoverQuills(sourceDir) {
41
- const quillsDir = join(sourceDir, "quills");
42
- const results = [];
43
- let nameDirs;
44
- try {
45
- nameDirs = readdirSync(quillsDir, { withFileTypes: true });
46
- }
47
- catch {
48
- return results;
49
- }
50
- for (const nameEntry of nameDirs) {
51
- if (!nameEntry.isDirectory() || nameEntry.name.startsWith("."))
52
- continue;
53
- let versionDirs;
54
- try {
55
- versionDirs = readdirSync(join(quillsDir, nameEntry.name), {
56
- withFileTypes: true,
57
- });
58
- }
59
- catch {
60
- continue;
61
- }
62
- for (const versionEntry of versionDirs) {
63
- if (!versionEntry.isDirectory() || versionEntry.name.startsWith("."))
64
- continue;
65
- results.push({ name: nameEntry.name, version: versionEntry.name });
66
- }
67
- }
68
- return results;
69
- }
70
- /**
71
- * Registers a Vitest describe block that validates every quill version in the
72
- * quiver at `sourceDirOrMetaUrl` against the provided Quillmark engine.
21
+ * Registers a `node:test` describe block that validates every quill
22
+ * version in the quiver at `metaUrlOrDir` against the provided engine.
73
23
  *
74
- * Pass `import.meta.url` when your test file lives at the quiver root (next to
75
- * Quiver.yaml). Pass an absolute directory path for any other layout.
24
+ * Pass `import.meta.url` when this file lives at the quiver root (next
25
+ * to Quiver.yaml). Pass an absolute directory path for any other layout.
76
26
  *
77
- * Each (quill, version) pair gets its own `it()` so failures are reported
78
- * individually. The quiver and engine are initialised once in `beforeAll`.
79
- *
80
- * Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml, all
81
- * template files, and engine compilation via engine.quill(tree). A quill that
82
- * contains a template error will cause its test to fail with the engine's own
83
- * error message.
27
+ * Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml,
28
+ * all template files, and engine compilation via engine.quill(tree).
84
29
  */
85
- export function runQuiverTests(sourceDirOrMetaUrl, engine) {
86
- const sourceDir = resolveSourceDir(sourceDirOrMetaUrl);
87
- // Enumerate quills synchronously so Vitest can collect test cases before
88
- // any async work begins. Errors here (missing quills/ dir, unreadable dirs)
89
- // surface as the "has at least one quill" test failing rather than a
90
- // collection-time crash.
91
- const quills = discoverQuills(sourceDir);
92
- describe(`Quiver: ${basename(sourceDir)}`, () => {
30
+ export function runQuiverTests(metaUrlOrDir, engine) {
31
+ describe("Quiver", () => {
93
32
  let registry;
94
- beforeAll(async () => {
95
- const quiver = await Quiver.fromSourceDir(sourceDir);
33
+ let quiver;
34
+ before(async () => {
35
+ quiver = await Quiver.fromDir(metaUrlOrDir);
96
36
  registry = new QuiverRegistry({ engine, quivers: [quiver] });
97
37
  });
98
38
  it("has at least one quill", () => {
99
- expect(quills).not.toHaveLength(0);
39
+ if (quiver.quillNames().length === 0) {
40
+ throw new Error("Quiver has no quills");
41
+ }
42
+ });
43
+ it("compiles every quill version without error", async () => {
44
+ for (const name of quiver.quillNames()) {
45
+ for (const version of quiver.versionsOf(name)) {
46
+ const quill = await registry.getQuill(`${name}@${version}`);
47
+ if (typeof quill.render !== "function") {
48
+ throw new Error(`${name}@${version}: engine returned non-conforming Quill`);
49
+ }
50
+ }
51
+ }
100
52
  });
101
- for (const { name, version } of quills) {
102
- it(`${name}@${version} compiles without error`, async () => {
103
- const quill = await registry.getQuill(`${name}@${version}`);
104
- expect(typeof quill.render).toBe("function");
105
- });
106
- }
107
53
  });
108
54
  }
@@ -1,11 +1,11 @@
1
1
  /**
2
- * HttpTransport — browser-safe packed quiver transport that fetches via HTTP.
2
+ * HttpTransport — browser-safe built-quiver transport that fetches via HTTP.
3
3
  * Internal; not exported from index.ts.
4
4
  *
5
5
  * Uses globalThis.fetch — no node: imports at any level.
6
6
  */
7
- import type { PackedTransport } from "../packed-loader.js";
8
- export declare class HttpTransport implements PackedTransport {
7
+ import type { BuiltTransport } from "../built-loader.js";
8
+ export declare class HttpTransport implements BuiltTransport {
9
9
  private readonly baseUrl;
10
10
  constructor(baseUrl: string);
11
11
  fetchBytes(relativePath: string): Promise<Uint8Array>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * HttpTransport — browser-safe packed quiver transport that fetches via HTTP.
2
+ * HttpTransport — browser-safe built-quiver transport that fetches via HTTP.
3
3
  * Internal; not exported from index.ts.
4
4
  *
5
5
  * Uses globalThis.fetch — no node: imports at any level.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@quillmark/quiver",
3
- "version": "0.2.0",
4
- "description": "Quiver registry and packaging for Quillmark",
3
+ "version": "0.3.0",
4
+ "description": "Quiver registry and build tooling for Quillmark",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -42,20 +42,14 @@
42
42
  "README.md"
43
43
  ],
44
44
  "peerDependencies": {
45
- "@quillmark/wasm": ">=0.59.0-rc.2",
46
- "vitest": ">=4.0.0"
47
- },
48
- "peerDependenciesMeta": {
49
- "vitest": {
50
- "optional": true
51
- }
45
+ "@quillmark/wasm": ">=0.59.0"
52
46
  },
53
47
  "dependencies": {
54
48
  "fflate": "^0.8.2",
55
49
  "yaml": "^2.8.3"
56
50
  },
57
51
  "devDependencies": {
58
- "@quillmark/wasm": "0.59.0-rc.2",
52
+ "@quillmark/wasm": "0.59.0",
59
53
  "@types/node": "^25.3.3",
60
54
  "typescript": "^5.9.3",
61
55
  "vitest": "^4.0.18"
package/dist/pack.d.ts DELETED
@@ -1,25 +0,0 @@
1
- /**
2
- * Pack logic — internal, Node-only.
3
- *
4
- * All Node.js built-in imports are done dynamically inside `packQuiver` so
5
- * that a type-only import of `PackOptions` from `src/index.ts` does NOT
6
- * pull `node:fs` or `node:crypto` into browser bundles.
7
- */
8
- /** Reserved for future pack options (e.g. compression level, filters). */
9
- export type PackOptions = Record<string, never>;
10
- /**
11
- * Reads a Source Quiver, validates it, and writes a Packed Quiver to outDir.
12
- *
13
- * Output layout:
14
- * outDir/
15
- * Quiver.json # stable pointer
16
- * manifest.<md5prefix6>.json # hashed manifest
17
- * <name>@<version>.<md5>.zip # one bundle per quill
18
- * store/
19
- * <md5> # dehydrated font bytes (full hash, no ext)
20
- *
21
- * Throws:
22
- * - `quiver_invalid` on source validation failures (propagated from scanner)
23
- * - `transport_error` on I/O failures
24
- */
25
- export declare function packQuiver(sourceDir: string, outDir: string, _opts?: PackOptions): Promise<void>;
@@ -1,14 +0,0 @@
1
- /**
2
- * FsTransport — Node-only packed quiver transport that reads from local disk.
3
- * Internal; not exported from index.ts.
4
- *
5
- * Uses dynamic imports of node:fs/promises and node:path so the module can be
6
- * imported in environments where those builtins are not available (the dynamic
7
- * import only executes when fetchBytes is actually called).
8
- */
9
- import type { PackedTransport } from "../packed-loader.js";
10
- export declare class FsTransport implements PackedTransport {
11
- private rootDir;
12
- constructor(rootDir: string);
13
- fetchBytes(relativePath: string): Promise<Uint8Array>;
14
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * FsTransport — Node-only packed quiver transport that reads from local disk.
3
- * Internal; not exported from index.ts.
4
- *
5
- * Uses dynamic imports of node:fs/promises and node:path so the module can be
6
- * imported in environments where those builtins are not available (the dynamic
7
- * import only executes when fetchBytes is actually called).
8
- */
9
- import { QuiverError } from "../errors.js";
10
- export class FsTransport {
11
- rootDir;
12
- constructor(rootDir) {
13
- this.rootDir = rootDir;
14
- }
15
- async fetchBytes(relativePath) {
16
- const { join, resolve } = await import("node:path");
17
- const { readFile } = await import("node:fs/promises");
18
- const rootResolved = resolve(this.rootDir);
19
- const fullPath = resolve(join(this.rootDir, relativePath));
20
- // Defense-in-depth: ensure resolved path stays within rootDir.
21
- if (!fullPath.startsWith(rootResolved + "/") && fullPath !== rootResolved) {
22
- throw new QuiverError("transport_error", `Path traversal detected: "${relativePath}" escapes quiver root`);
23
- }
24
- try {
25
- const buf = await readFile(fullPath);
26
- // Ensure we return a plain Uint8Array, not a Node.js Buffer subclass.
27
- return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
28
- }
29
- catch (err) {
30
- throw new QuiverError("transport_error", `Failed to read "${relativePath}" from packed quiver at "${this.rootDir}": ${err.message}`, { cause: err });
31
- }
32
- }
33
- }