@quillmark/quiver 0.1.1 → 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/assert-node.d.ts +5 -0
- package/dist/assert-node.js +12 -0
- package/dist/build.d.ts +25 -0
- package/dist/build.js +120 -0
- package/dist/built-loader.d.ts +27 -0
- package/dist/built-loader.js +232 -0
- package/dist/bundle.d.ts +13 -0
- package/dist/bundle.js +33 -0
- package/dist/engine-types.d.ts +26 -0
- package/dist/engine-types.js +20 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +5 -0
- package/dist/quiver-yaml.d.ts +22 -0
- package/dist/quiver-yaml.js +63 -0
- package/dist/quiver.d.ts +82 -0
- package/dist/quiver.js +132 -0
- package/dist/ref.d.ts +14 -0
- package/dist/ref.js +44 -0
- package/dist/registry.d.ts +39 -0
- package/dist/registry.js +115 -0
- package/dist/semver.d.ts +8 -0
- package/dist/semver.js +44 -0
- package/dist/source-loader.d.ts +42 -0
- package/dist/source-loader.js +179 -0
- package/dist/testing.d.ts +28 -0
- package/dist/testing.js +54 -0
- package/dist/transports/http-transport.d.ts +12 -0
- package/dist/transports/http-transport.js +33 -0
- package/package.json +9 -11
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.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { QuiverError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Throws a `transport_error` when called outside a Node.js environment.
|
|
4
|
+
* Call at the top of each Node-only static factory to fail fast in browsers.
|
|
5
|
+
*/
|
|
6
|
+
export function assertNode(method) {
|
|
7
|
+
if (typeof globalThis.process === "undefined" ||
|
|
8
|
+
!globalThis.process
|
|
9
|
+
?.versions?.node) {
|
|
10
|
+
throw new QuiverError("transport_error", `${method} is only available in Node.js`);
|
|
11
|
+
}
|
|
12
|
+
}
|
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>;
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
import { QuiverError } from "./errors.js";
|
|
9
|
+
import { packFiles } from "./bundle.js";
|
|
10
|
+
/** Font file extensions recognised by the builder (case-insensitive). */
|
|
11
|
+
const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
|
|
12
|
+
/**
|
|
13
|
+
* Reads a Source Quiver, validates it, and writes the build output to outDir.
|
|
14
|
+
*
|
|
15
|
+
* Output layout:
|
|
16
|
+
* outDir/
|
|
17
|
+
* Quiver.json # stable pointer
|
|
18
|
+
* manifest.<md5prefix6>.json # hashed manifest
|
|
19
|
+
* <name>@<version>.<md5>.zip # one bundle per quill
|
|
20
|
+
* store/
|
|
21
|
+
* <md5> # dehydrated font bytes (full hash, no ext)
|
|
22
|
+
*
|
|
23
|
+
* Throws:
|
|
24
|
+
* - `quiver_invalid` on source validation failures (propagated from scanner)
|
|
25
|
+
* - `transport_error` on I/O failures
|
|
26
|
+
*/
|
|
27
|
+
export async function buildQuiver(sourceDir, outDir, _opts) {
|
|
28
|
+
// Dynamic imports keep this module safe to type-import from browser contexts.
|
|
29
|
+
const { join } = await import("node:path");
|
|
30
|
+
const { mkdir, rm, writeFile, } = await import("node:fs/promises");
|
|
31
|
+
const { createHash } = await import("node:crypto");
|
|
32
|
+
const { scanSourceQuiver, readQuillTree } = await import("./source-loader.js");
|
|
33
|
+
// 1. Scan + validate source quiver (throws quiver_invalid on bad input).
|
|
34
|
+
const { meta, catalog } = await scanSourceQuiver(sourceDir);
|
|
35
|
+
// 2. Clear and recreate outDir + outDir/store/.
|
|
36
|
+
try {
|
|
37
|
+
await rm(outDir, { recursive: true, force: true });
|
|
38
|
+
await mkdir(join(outDir, "store"), { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw new QuiverError("transport_error", `Failed to prepare output directory "${outDir}": ${err.message}`, { cause: err });
|
|
42
|
+
}
|
|
43
|
+
// 3. Process each quill version.
|
|
44
|
+
const manifestQuills = [];
|
|
45
|
+
for (const [quillName, versions] of catalog) {
|
|
46
|
+
for (const version of versions) {
|
|
47
|
+
const quillDir = join(sourceDir, "quills", quillName, version);
|
|
48
|
+
// a. Read quill file tree.
|
|
49
|
+
const tree = await readQuillTree(quillDir);
|
|
50
|
+
// b. Partition fonts vs content.
|
|
51
|
+
const fontEntries = [];
|
|
52
|
+
const contentEntries = [];
|
|
53
|
+
for (const [path, bytes] of tree) {
|
|
54
|
+
if (FONT_EXT.test(path)) {
|
|
55
|
+
fontEntries.push([path, bytes]);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
contentEntries.push([path, bytes]);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// c. Dehydrate fonts into store/.
|
|
62
|
+
const fonts = {};
|
|
63
|
+
for (const [path, bytes] of fontEntries) {
|
|
64
|
+
const hash = createHash("md5").update(bytes).digest("hex");
|
|
65
|
+
const storePath = join(outDir, "store", hash);
|
|
66
|
+
try {
|
|
67
|
+
await writeFile(storePath, bytes);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
throw new QuiverError("transport_error", `Failed to write font store entry "${storePath}": ${err.message}`, { cause: err });
|
|
71
|
+
}
|
|
72
|
+
fonts[path] = hash;
|
|
73
|
+
}
|
|
74
|
+
// d. Zip content files (deterministic: sorted paths, fixed mtime).
|
|
75
|
+
const contentRecord = {};
|
|
76
|
+
for (const [path, bytes] of contentEntries) {
|
|
77
|
+
contentRecord[path] = bytes;
|
|
78
|
+
}
|
|
79
|
+
const zipBytes = packFiles(contentRecord);
|
|
80
|
+
// e–f. Compute bundle hash and name.
|
|
81
|
+
const bundleHash = createHash("md5").update(zipBytes).digest("hex").slice(0, 6);
|
|
82
|
+
const bundleName = `${quillName}@${version}.${bundleHash}.zip`;
|
|
83
|
+
// g. Write bundle zip.
|
|
84
|
+
const bundlePath = join(outDir, bundleName);
|
|
85
|
+
try {
|
|
86
|
+
await writeFile(bundlePath, zipBytes);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
throw new QuiverError("transport_error", `Failed to write bundle "${bundlePath}": ${err.message}`, { cause: err });
|
|
90
|
+
}
|
|
91
|
+
// h. Record manifest entry.
|
|
92
|
+
manifestQuills.push({ name: quillName, version, bundle: bundleName, fonts });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 4–8. Build and write hashed manifest.
|
|
96
|
+
const manifest = {
|
|
97
|
+
version: 1,
|
|
98
|
+
name: meta.name,
|
|
99
|
+
quills: manifestQuills,
|
|
100
|
+
};
|
|
101
|
+
const manifestJson = JSON.stringify(manifest, null, 2);
|
|
102
|
+
const manifestHash = createHash("md5").update(manifestJson).digest("hex").slice(0, 6);
|
|
103
|
+
const manifestFileName = `manifest.${manifestHash}.json`;
|
|
104
|
+
const manifestPath = join(outDir, manifestFileName);
|
|
105
|
+
try {
|
|
106
|
+
await writeFile(manifestPath, manifestJson, "utf-8");
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
throw new QuiverError("transport_error", `Failed to write manifest "${manifestPath}": ${err.message}`, { cause: err });
|
|
110
|
+
}
|
|
111
|
+
// 9–10. Write stable pointer Quiver.json.
|
|
112
|
+
const pointer = { manifest: manifestFileName };
|
|
113
|
+
const pointerPath = join(outDir, "Quiver.json");
|
|
114
|
+
try {
|
|
115
|
+
await writeFile(pointerPath, JSON.stringify(pointer), "utf-8");
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
throw new QuiverError("transport_error", `Failed to write pointer "${pointerPath}": ${err.message}`, { cause: err });
|
|
119
|
+
}
|
|
120
|
+
}
|