@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 +156 -74
- package/README.md +65 -17
- package/dist/build.d.ts +25 -0
- package/dist/{pack.js → build.js} +7 -7
- package/dist/{packed-loader.d.ts → built-loader.d.ts} +8 -8
- package/dist/{packed-loader.js → built-loader.js} +10 -10
- package/dist/index.d.ts +2 -1
- package/dist/node.js +1 -1
- package/dist/quiver.d.ts +30 -22
- package/dist/quiver.js +58 -35
- package/dist/source-loader.d.ts +1 -1
- package/dist/source-loader.js +1 -1
- package/dist/testing.d.ts +14 -25
- package/dist/testing.js +31 -85
- package/dist/transports/http-transport.d.ts +3 -3
- package/dist/transports/http-transport.js +1 -1
- package/package.json +4 -10
- package/dist/pack.d.ts +0 -25
- package/dist/transports/fs-transport.d.ts +0 -14
- package/dist/transports/fs-transport.js +0 -33
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)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- `
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
-
|
|
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
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
-
|
|
162
|
-
|
|
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
|
|
171
|
-
2. Consumer validation tooling for
|
|
172
|
-
3. Manifest pointer resolution for
|
|
173
|
-
4. HTTP loading
|
|
174
|
-
5. Source
|
|
175
|
-
6.
|
|
176
|
-
7.
|
|
177
|
-
8.
|
|
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
|
|
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 +
|
|
271
|
+
## Runtime + Build Model
|
|
222
272
|
|
|
223
273
|
V1 runtime loading paths:
|
|
224
274
|
|
|
225
|
-
1.
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
281
|
+
V1 build behavior:
|
|
230
282
|
|
|
231
|
-
- `Quiver.
|
|
232
|
-
|
|
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
|
-
##
|
|
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 `
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// Node-only
|
|
314
|
-
|
|
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):** `
|
|
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
|
|
364
|
-
|
|
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 `
|
|
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
|
|
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 `
|
|
396
|
-
6. ~~
|
|
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
|
-
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
73
|
+
// build script (Node) — typically wired into your existing build pipeline
|
|
74
|
+
import { Quiver } from "@quillmark/quiver/node";
|
|
35
75
|
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
83
|
+
// browser runtime
|
|
84
|
+
import { Quiver, QuiverRegistry } from "@quillmark/quiver";
|
|
44
85
|
|
|
45
|
-
const quiver = await Quiver.
|
|
86
|
+
const quiver = await Quiver.fromBuilt("/quivers/my-quiver/");
|
|
87
|
+
const registry = new QuiverRegistry({ engine, quivers: [quiver] });
|
|
46
88
|
```
|
|
47
89
|
|
|
48
|
-
##
|
|
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.
|
|
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,
|
|
144
|
+
See [PROGRAM.md](./PROGRAM.md) for the complete API surface, runtime artifact format specification, and design decisions.
|
package/dist/build.d.ts
ADDED
|
@@ -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
|
-
*
|
|
2
|
+
* Build logic — internal, Node-only.
|
|
3
3
|
*
|
|
4
|
-
* All Node.js built-in imports are done dynamically inside `
|
|
5
|
-
* that a type-only import of `
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
2
|
+
* Built-quiver loader — browser-safe at module level.
|
|
3
3
|
* Internal; not exported from index.ts.
|
|
4
4
|
*
|
|
5
5
|
* Exposes:
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
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.
|
|
14
|
+
* artifact. Sole implementation is HttpTransport (browser + Node).
|
|
15
15
|
*/
|
|
16
|
-
export interface
|
|
16
|
+
export interface BuiltTransport {
|
|
17
17
|
fetchBytes(relativePath: string): Promise<Uint8Array>;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
* Load a
|
|
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
|
|
25
|
+
* 4. Returns a Quiver instance backed by a BuiltLoader.
|
|
26
26
|
*/
|
|
27
|
-
export declare function
|
|
27
|
+
export declare function loadBuiltQuiver(transport: BuiltTransport): Promise<Quiver>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Built-quiver loader — browser-safe at module level.
|
|
3
3
|
* Internal; not exported from index.ts.
|
|
4
4
|
*
|
|
5
5
|
* Exposes:
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
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
|
-
// ───
|
|
35
|
-
class
|
|
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
|
|
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
|
|
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
|
|
185
|
+
* 4. Returns a Quiver instance backed by a BuiltLoader.
|
|
186
186
|
*/
|
|
187
|
-
export async function
|
|
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
|
|
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 {
|
|
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 (
|
|
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
|
|
5
|
+
* (either source-backed or build-output-backed).
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
8
|
-
/** @internal Internal loader strategy: source or
|
|
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
|
|
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.
|
|
25
|
-
*
|
|
24
|
+
* Node-only factory. Resolves an npm specifier against `node_modules` and
|
|
25
|
+
* loads the source layout at the package root.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
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 `
|
|
29
|
+
* Throws `transport_error` on resolution/I/O failure, `quiver_invalid`
|
|
30
|
+
* on schema violations.
|
|
31
31
|
*/
|
|
32
|
-
static
|
|
32
|
+
static fromPackage(specifier: string): Promise<Quiver>;
|
|
33
33
|
/**
|
|
34
|
-
* Node-only factory.
|
|
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
|
-
*
|
|
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 `
|
|
40
|
+
* Throws `quiver_invalid` on schema violations, `transport_error` on I/O failure.
|
|
39
41
|
*/
|
|
40
|
-
static
|
|
42
|
+
static fromDir(pathOrFileUrl: string): Promise<Quiver>;
|
|
41
43
|
/**
|
|
42
|
-
* Browser-safe factory. Loads
|
|
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`
|
|
45
|
-
* format errors.
|
|
51
|
+
* Throws `transport_error` on network/HTTP failure, `quiver_invalid`
|
|
52
|
+
* on format errors.
|
|
46
53
|
*/
|
|
47
|
-
static
|
|
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.
|
|
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 `./
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
29
|
-
*
|
|
28
|
+
* Node-only factory. Resolves an npm specifier against `node_modules` and
|
|
29
|
+
* loads the source layout at the package root.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
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 `
|
|
33
|
+
* Throws `transport_error` on resolution/I/O failure, `quiver_invalid`
|
|
34
|
+
* on schema violations.
|
|
35
35
|
*/
|
|
36
|
-
static async
|
|
37
|
-
assertNode("Quiver.
|
|
38
|
-
const {
|
|
39
|
-
const {
|
|
40
|
-
const
|
|
41
|
-
|
|
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.
|
|
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
|
-
*
|
|
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 `
|
|
57
|
+
* Throws `quiver_invalid` on schema violations, `transport_error` on I/O failure.
|
|
49
58
|
*/
|
|
50
|
-
static async
|
|
51
|
-
assertNode("Quiver.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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`
|
|
61
|
-
* format errors.
|
|
79
|
+
* Throws `transport_error` on network/HTTP failure, `quiver_invalid`
|
|
80
|
+
* on format errors.
|
|
62
81
|
*/
|
|
63
|
-
static async
|
|
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 {
|
|
87
|
+
const { loadBuiltQuiver } = await import("./built-loader.js");
|
|
66
88
|
const transport = new HttpTransport(url);
|
|
67
|
-
return
|
|
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.
|
|
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 `./
|
|
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
|
|
90
|
-
assertNode("Quiver.
|
|
91
|
-
const {
|
|
92
|
-
return
|
|
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.
|
package/dist/source-loader.d.ts
CHANGED
|
@@ -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 (
|
|
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";
|
package/dist/source-loader.js
CHANGED
|
@@ -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 (
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
21
|
-
calls: Array<Map<string, Uint8Array>>;
|
|
22
|
-
engine: QuillmarkLike;
|
|
23
|
-
};
|
|
17
|
+
import type { QuillmarkLike } from "./engine-types.js";
|
|
24
18
|
/**
|
|
25
|
-
* Registers a
|
|
26
|
-
* quiver at `
|
|
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
|
-
*
|
|
32
|
-
*
|
|
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,
|
|
35
|
-
* template files, and engine compilation via engine.quill(tree).
|
|
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(
|
|
28
|
+
export declare function runQuiverTests(metaUrlOrDir: string, engine: QuillmarkLike): void;
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
15
|
+
* Run with `node --test`.
|
|
12
16
|
*/
|
|
13
|
-
import { describe, it,
|
|
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
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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
|
-
*
|
|
78
|
-
*
|
|
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(
|
|
86
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
8
|
-
export declare class HttpTransport implements
|
|
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>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/quiver",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Quiver registry and
|
|
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
|
|
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
|
|
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
|
-
}
|