@quillmark/quiver 0.18.1 → 0.20.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/MIGRATION.md +91 -0
- package/README.md +44 -29
- package/dist/build.d.ts +4 -0
- package/dist/build.js +141 -96
- package/dist/quiver.d.ts +34 -7
- package/dist/quiver.js +49 -10
- package/dist/transports/memory-transport.d.ts +15 -0
- package/dist/transports/memory-transport.js +34 -0
- package/package.json +4 -6
- package/dist/bin/quiver.d.ts +0 -16
- package/dist/bin/quiver.js +0 -141
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Migrating `@quillmark/quiver`
|
|
2
|
+
|
|
3
|
+
Upgrade notes per range. The [CHANGELOG](CHANGELOG.md) is the full record; this page is the subset that breaks a working consumer, ordered by how late you find out.
|
|
4
|
+
|
|
5
|
+
## 0.16 → 0.19
|
|
6
|
+
|
|
7
|
+
### Runtime, not build time
|
|
8
|
+
|
|
9
|
+
Four removals leave valid JavaScript. A consumer on plain JS, or on TypeScript reaching these through an `any`, learns about them from a `TypeError` at runtime naming neither the version nor the replacement.
|
|
10
|
+
|
|
11
|
+
**`Quiver.buildPackage(specifier, outDir)` and `Quiver.fromPackage(specifier)` are gone.** An npm-installed quiver is a directory, and resolving it belongs to the caller: the resolution base has to be your module, because under an isolated `node_modules` layout your dependencies are reachable from you and not from this package.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// 0.16
|
|
15
|
+
await Quiver.buildPackage('@org/my-quiver', './public/quills');
|
|
16
|
+
const quiver = await Quiver.fromPackage('@org/my-quiver');
|
|
17
|
+
|
|
18
|
+
// 0.19
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { dirname } from 'node:path';
|
|
21
|
+
import { build, fromDir } from '@quillmark/quiver/node';
|
|
22
|
+
|
|
23
|
+
const root = dirname(createRequire(import.meta.url).resolve('@org/my-quiver/Quiver.yaml'));
|
|
24
|
+
await build(root, './public/quills');
|
|
25
|
+
const quiver = await fromDir(root);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That resolution goes through the collection's `exports` map. A collection that declares one and does not expose `./Quiver.yaml` fails here with a module-resolution error naming neither the collection nor the convention; the fix is in the collection's `package.json`, not at your call site.
|
|
29
|
+
|
|
30
|
+
**`quiver.warm()` is gone.** It prefetched every tree into a second cache that no longer exists. `getQuill` caches and coalesces per canonical ref, so prefetching is calling it early:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const refs = quiver.quillNames().flatMap((n) => quiver.versionsOf(n).map((v) => `${n}@${v}`));
|
|
34
|
+
await Promise.allSettled(refs.map((r) => quiver.getQuill(r)));
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Skipping it costs one fetch on first open of each quill, cached from then on.
|
|
38
|
+
|
|
39
|
+
**`Quiver.fromManifest(baseUrl, bytes)` is gone**, replaced by two doors that cover more than it did. Pass the whole artifact and fetch nothing, or seed what you hold and let the URL serve the rest:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
// 0.16
|
|
43
|
+
const quiver = await Quiver.fromManifest('/quills/my-quiver/', manifestBytes);
|
|
44
|
+
|
|
45
|
+
// 0.19 — whole artifact in hand, no filesystem, no fetch
|
|
46
|
+
const quiver = await Quiver.fromBuiltFiles(artifactFiles);
|
|
47
|
+
|
|
48
|
+
// 0.19 — hold the small documents, fetch the heavy ones
|
|
49
|
+
const quiver = await Quiver.fromBuiltUrl('/quills/my-quiver/', {
|
|
50
|
+
seed: new Map([
|
|
51
|
+
['latest.json', pointerBytes],
|
|
52
|
+
[manifestName, manifestBytes]
|
|
53
|
+
])
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Keys are artifact-relative, as `build` writes them. `fromManifest` took manifest bytes and skipped the pointer; the seed takes any subset of the artifact, so it covers that and the case where the bundles are in hand too.
|
|
58
|
+
|
|
59
|
+
**`quiver.resolve(ref)` is sync.** `await` on it still works, so this breaks only a call site that used the promise as one: `.then()`, `Promise.all([...])`, or a `.catch()` that now has a throw to catch instead of a rejection.
|
|
60
|
+
|
|
61
|
+
### Rebuild every artifact
|
|
62
|
+
|
|
63
|
+
Build output moved from MD5 to SHA-256, and the loader verifies fetched bytes against the digest in their name. An artifact packed by 0.16 fails to load under 0.19. Re-run `build`; nothing in the source layout changes.
|
|
64
|
+
|
|
65
|
+
### The compiler-caught rest
|
|
66
|
+
|
|
67
|
+
| 0.16 | 0.19 |
|
|
68
|
+
| ----------------------------------------------------------------- | ---------------------------------------------- |
|
|
69
|
+
| `Quiver.fromDir`, `Quiver.fromBuiltDir`, `Quiver.build` (statics) | free functions from `@quillmark/quiver/node` |
|
|
70
|
+
| `Quiver._fromLoader` | removed; construction is sealed |
|
|
71
|
+
| `BuildOptions` | removed; `build(src, out)` takes two arguments |
|
|
72
|
+
| `@quillmark/quiver/preview`, `renderQuiverSamples` | removed; `quillkit studio` answers live |
|
|
73
|
+
| `@quillmark/quiver/testing`, `runQuiverTests` | removed; `quillkit test` is the gate |
|
|
74
|
+
| `quiver` / `quillmark-quiver` bin | removed; the verbs are `quillkit`'s |
|
|
75
|
+
| `quiver.config.js` | `quillkit.config.js` |
|
|
76
|
+
|
|
77
|
+
The `@quillmark/wasm` peer floor is `>=0.101.0-0`. That range carries its own boundary changes, several of which a type checker also misses; the ledger is [`DOCUMENT_MODEL.md`](../svelte/prose/canon/DOCUMENT_MODEL.md) in `@quillmark/svelte`.
|
|
78
|
+
|
|
79
|
+
### Installing the tooling
|
|
80
|
+
|
|
81
|
+
The bin left this package in 0.19. A collection that ran `quiver test` or `quillmark-quiver build` from its `scripts` installs `quillkit` and keeps depending on this package for the format its quiver is packed in:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
npm install --save-dev @quillmark/quiver @quillmark/wasm quillkit
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```jsonc
|
|
88
|
+
{
|
|
89
|
+
"scripts": { "test": "quillkit test", "build": "quillkit build" }
|
|
90
|
+
}
|
|
91
|
+
```
|
package/README.md
CHANGED
|
@@ -8,17 +8,20 @@ Load and build collections of quills for rendering with `@quillmark/wasm`.
|
|
|
8
8
|
npm install @quillmark/quiver @quillmark/wasm
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Upgrading from an earlier version: [`MIGRATION.md`](MIGRATION.md).
|
|
12
|
+
|
|
11
13
|
## Loading a quiver
|
|
12
14
|
|
|
13
15
|
A quiver has one authored shape, the **source layout** (`Quiver.yaml` at the package root, quills under `quills/<name>/<x.y.z>/`), published as an npm package. Browsers cannot read that layout, so `build(src, out)` packs it at deploy time and the output is served as static assets. Each loader names exactly what it reads; there is no auto-detection.
|
|
14
16
|
|
|
15
|
-
| Loader
|
|
16
|
-
|
|
|
17
|
-
| `fromDir(path)`
|
|
18
|
-
| `fromBuiltDir(path)`
|
|
19
|
-
| `Quiver.fromBuiltUrl(url)`
|
|
17
|
+
| Loader | Reads | Import |
|
|
18
|
+
| ------------------------------ | ------------------------- | ------------------------ |
|
|
19
|
+
| `fromDir(path)` | the source layout | `@quillmark/quiver/node` |
|
|
20
|
+
| `fromBuiltDir(path)` | build output, off disk | `@quillmark/quiver/node` |
|
|
21
|
+
| `Quiver.fromBuiltUrl(url)` | build output, over HTTP | `@quillmark/quiver` |
|
|
22
|
+
| `Quiver.fromBuiltFiles(files)` | build output, from memory | `@quillmark/quiver` |
|
|
20
23
|
|
|
21
|
-
The filesystem factories are free functions from `/node`; the
|
|
24
|
+
The filesystem factories are free functions from `/node`; the two that reach no filesystem are statics on `Quiver`. Importing `/node` adds nothing to the class, so a bundler drops the verbs you do not call.
|
|
22
25
|
|
|
23
26
|
## Consuming a quiver (Node)
|
|
24
27
|
|
|
@@ -27,19 +30,20 @@ A quiver installed from npm is a directory like any other. Resolve its root from
|
|
|
27
30
|
```ts
|
|
28
31
|
import { createRequire } from 'node:module';
|
|
29
32
|
import { dirname } from 'node:path';
|
|
30
|
-
import { Engine,
|
|
33
|
+
import { Engine, init } from '@quillmark/wasm';
|
|
31
34
|
import { fromDir } from '@quillmark/quiver/node';
|
|
32
35
|
|
|
33
36
|
const root = dirname(createRequire(import.meta.url).resolve('@org/my-quiver/Quiver.yaml'));
|
|
34
37
|
const quiver = await fromDir(root);
|
|
35
38
|
const engine = new Engine();
|
|
36
39
|
|
|
40
|
+
const { Document } = await init(); // the gate is the only door to Document
|
|
37
41
|
const doc = Document.fromMarkdown(markdownString);
|
|
38
42
|
const quill = await quiver.getQuill(doc.quillRef);
|
|
39
43
|
const result = await engine.render(quill, doc, { format: 'pdf' });
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
`getQuill(ref)` is the only way to obtain a quill from a quiver, and the only entry point most consumers need. It accepts selector refs (`"memo"`, `"memo@1"`) and canonical ones (`"memo@1.0.0"`), resolves the selector, materializes the quill via `Quill.fromTree`, and caches one instance per canonical ref for the quiver's lifetime; concurrent calls for the same ref coalesce into a single load.
|
|
46
|
+
`getQuill(ref)` is the only way to obtain a quill from a quiver, and the only entry point most consumers need. It accepts selector refs (`"memo"`, `"memo@1"`) and canonical ones (`"memo@1.0.0"`), resolves the selector, materializes the quill via `Quill.fromTree`, and caches one instance per canonical ref for the quiver's lifetime; concurrent calls for the same ref coalesce into a single load. It awaits `init()` itself, so instantiating the core is not a precondition of calling it.
|
|
43
47
|
|
|
44
48
|
That quill is **borrowed, not owned**: every caller asking for that ref gets the same instance, so `free()` on it hands the next caller a freed handle. Code that owns its quill mints one from `(await quiver.getQuill(ref)).toTree()` and frees that.
|
|
45
49
|
|
|
@@ -62,16 +66,17 @@ import { build } from '@quillmark/quiver/node';
|
|
|
62
66
|
await build('./node_modules/@org/my-quiver', './public/quivers/my-quiver');
|
|
63
67
|
```
|
|
64
68
|
|
|
65
|
-
`build`
|
|
69
|
+
`build` owns its output path outright: it assembles a generation in `<outDir>.stage`, moves it in whole, and deletes the one it replaced. A reader fetching mid-build sees the previous generation rather than a torn tree, and a build that throws leaves it serving. An `outDir` that is, or contains, the source quiver or the working directory is refused with a `transport_error` rather than deleted.
|
|
66
70
|
|
|
67
71
|
```ts
|
|
68
72
|
// browser runtime
|
|
69
|
-
import { Engine,
|
|
73
|
+
import { Engine, init } from '@quillmark/wasm';
|
|
70
74
|
import { Quiver } from '@quillmark/quiver';
|
|
71
75
|
|
|
72
76
|
const quiver = await Quiver.fromBuiltUrl('/quivers/my-quiver/');
|
|
73
77
|
const engine = new Engine();
|
|
74
78
|
|
|
79
|
+
const { Document } = await init(); // the gate is the only door to Document
|
|
75
80
|
const doc = Document.fromMarkdown(markdownString);
|
|
76
81
|
const quill = await quiver.getQuill(doc.quillRef);
|
|
77
82
|
const result = await engine.render(quill, doc, { format: 'pdf' });
|
|
@@ -90,6 +95,31 @@ import { fromBuiltDir } from '@quillmark/quiver/node';
|
|
|
90
95
|
const quiver = await fromBuiltDir('./static/quills/my-quiver');
|
|
91
96
|
```
|
|
92
97
|
|
|
98
|
+
## Server-side runtime (no filesystem)
|
|
99
|
+
|
|
100
|
+
A serverless function's packed artifact is not on a path the invocation can read, so `fromBuiltDir` cannot see it. Hand over the bytes instead, keyed by artifact-relative path as `build` writes them (`latest.json`, `manifest.<digest>.json`, `<name>@<x.y.z>.<digest>.zip`, `store/<hash>`):
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { Quiver } from '@quillmark/quiver';
|
|
104
|
+
|
|
105
|
+
const quiver = await Quiver.fromBuiltFiles(artifactFiles); // Map<string, Uint8Array>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Nothing is fetched, so nothing self-fetches over your own load balancer. The map must carry the whole artifact; a path it lacks is a `transport_error` naming that path.
|
|
109
|
+
|
|
110
|
+
Where inlining the bundles and fonts is not practical, hold the two small documents and let the host serve the rest:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const quiver = await Quiver.fromBuiltUrl('/quills/my-quiver/', {
|
|
114
|
+
seed: new Map([
|
|
115
|
+
['latest.json', pointerBytes],
|
|
116
|
+
[manifestName, manifestBytes]
|
|
117
|
+
])
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The seed answers first and the URL serves what it does not carry. Seeding `latest.json` also settles which catalog the process reads at deploy time rather than at cache-revalidation time. Seeded bytes are checked against the digest in their name exactly as fetched bytes are.
|
|
122
|
+
|
|
93
123
|
## The `latest.json` pointer
|
|
94
124
|
|
|
95
125
|
`Quiver.fromBuiltUrl(url)` first fetches `<url>/latest.json`, a stable-named pointer to the current manifest. Everything behind that pointer is content-addressed and checked against the digest in its name; the pointer itself is not, so a cache layer can serve a stale one and silently pin the client to the old catalog. It is therefore the one request fetched `no-cache` (revalidate with the origin; a 304 still serves from disk), which closes the browser-cache layer. A stale CDN edge is answered by that host's cache headers.
|
|
@@ -134,32 +164,17 @@ A consumer reaches an installed quiver by resolving `<specifier>/Quiver.yaml`, w
|
|
|
134
164
|
}
|
|
135
165
|
```
|
|
136
166
|
|
|
137
|
-
|
|
167
|
+
## Authoring is [quillkit](../quillkit#readme)'s
|
|
138
168
|
|
|
139
|
-
|
|
169
|
+
This package is a library: it loads a quiver and packs one, and it has no CLI. The verbs a quill author runs (gate, pack, look at, deploy) are `quillkit`'s, and it resolves this package out of the collection's own `node_modules`. Depend on it here and the version you pin is the format your quiver is packed in:
|
|
140
170
|
|
|
141
171
|
```sh
|
|
142
|
-
npm install --save-dev @quillmark/quiver @quillmark/wasm
|
|
172
|
+
npm install --save-dev @quillmark/quiver @quillmark/wasm quillkit
|
|
143
173
|
```
|
|
144
174
|
|
|
145
175
|
```jsonc
|
|
146
176
|
// package.json
|
|
147
177
|
{
|
|
148
|
-
"scripts": { "test": "
|
|
178
|
+
"scripts": { "test": "quillkit test" }
|
|
149
179
|
}
|
|
150
180
|
```
|
|
151
|
-
|
|
152
|
-
It finds the engine itself: a named `engine` export from `quiver.config.js` at the collection root, else `@quillmark/wasm` from your own `node_modules`. `quillmark-quiver build [--out <dir>]` packs the same source into a servable artifact.
|
|
153
|
-
|
|
154
|
-
On vitest, jest or `node:test`, spawn the bin from a test rather than rebuilding the loop against the main API. The gate stays one implementation, and the case gates what CI gates:
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
// quiver.test.ts
|
|
158
|
-
import { execFileSync } from 'node:child_process';
|
|
159
|
-
|
|
160
|
-
it('gates the quiver', () => {
|
|
161
|
-
execFileSync('quillmark-quiver', ['test'], { stdio: 'inherit' });
|
|
162
|
-
});
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
It answers _does it work_, and that is the whole of what this package offers a quill author: _what is it like to use_ is answered by working a real document through the quill live, which is an app rather than a subpath.
|
package/dist/build.d.ts
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
* store/
|
|
18
18
|
* <sha256> # dehydrated font bytes (full hash, no ext)
|
|
19
19
|
*
|
|
20
|
+
* A generation is assembled beside outDir and moved in whole, so a reader
|
|
21
|
+
* fetching mid-build reads the previous one rather than a torn tree, and a
|
|
22
|
+
* build that throws leaves the previous one serving.
|
|
23
|
+
*
|
|
20
24
|
* Throws:
|
|
21
25
|
* - `quiver_invalid` on source validation failures (propagated from scanner)
|
|
22
26
|
* - `transport_error` on I/O failures, and on an outDir the build would have
|
package/dist/build.js
CHANGED
|
@@ -11,12 +11,20 @@ import { NAME_DIGEST_LENGTH } from './digest.js';
|
|
|
11
11
|
import { POINTER_FORMAT } from './format.js';
|
|
12
12
|
/** Font file extensions recognised by the builder (case-insensitive). */
|
|
13
13
|
const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
|
|
14
|
+
/** The two trees a build owns beside its output: where a generation is assembled, and
|
|
15
|
+
* where the one it replaces waits to be deleted. Siblings of outDir, so the swap is a
|
|
16
|
+
* rename rather than a copy across filesystems. */
|
|
17
|
+
const stagesOf = (out) => ({
|
|
18
|
+
stage: `${out}.stage`,
|
|
19
|
+
prev: `${out}.prev`
|
|
20
|
+
});
|
|
14
21
|
/**
|
|
15
|
-
* The build
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* The build deletes each of the three trees it owns, so an outDir that is, or contains,
|
|
23
|
+
* the source quiver or the working directory deletes the thing the caller was building
|
|
24
|
+
* from. `quillkit build --out .` and a mistyped `--out ..` are both one keystroke away,
|
|
25
|
+
* and the failure is unrecoverable, so an outDir that owns the caller is refused up
|
|
26
|
+
* front. The staging siblings are checked too: they are named off outDir and cleared
|
|
27
|
+
* with it, so a caller who avoids one spelling has not avoided the others.
|
|
20
28
|
*
|
|
21
29
|
* An outDir *inside* sourceDir stays allowed: the scan reads the source before
|
|
22
30
|
* any write, and `dist/` under the quiver root is the ordinary layout.
|
|
@@ -25,18 +33,19 @@ const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
|
|
|
25
33
|
*/
|
|
26
34
|
function assertSafeOutDir(path, sourceDir, outDir) {
|
|
27
35
|
const out = path.resolve(outDir);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
const { stage, prev } = stagesOf(out);
|
|
37
|
+
/** True when one of the owned trees is `target` or an ancestor of it. */
|
|
38
|
+
const owns = (target) => [out, stage, prev].some((dir) => {
|
|
39
|
+
const rel = path.relative(dir, path.resolve(target));
|
|
31
40
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
32
|
-
};
|
|
41
|
+
});
|
|
33
42
|
const what = owns(sourceDir)
|
|
34
43
|
? 'the source quiver'
|
|
35
44
|
: owns(process.cwd())
|
|
36
45
|
? 'the working directory'
|
|
37
46
|
: undefined;
|
|
38
47
|
if (what !== undefined) {
|
|
39
|
-
throw new QuiverError('transport_error', `Refusing to build into "${outDir}": the build clears
|
|
48
|
+
throw new QuiverError('transport_error', `Refusing to build into "${outDir}": the build clears that directory and two staging siblings beside it, and this one holds ${what} ("${out}"). Point --out somewhere the build can own outright.`);
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
/**
|
|
@@ -51,6 +60,10 @@ function assertSafeOutDir(path, sourceDir, outDir) {
|
|
|
51
60
|
* store/
|
|
52
61
|
* <sha256> # dehydrated font bytes (full hash, no ext)
|
|
53
62
|
*
|
|
63
|
+
* A generation is assembled beside outDir and moved in whole, so a reader
|
|
64
|
+
* fetching mid-build reads the previous one rather than a torn tree, and a
|
|
65
|
+
* build that throws leaves the previous one serving.
|
|
66
|
+
*
|
|
54
67
|
* Throws:
|
|
55
68
|
* - `quiver_invalid` on source validation failures (propagated from scanner)
|
|
56
69
|
* - `transport_error` on I/O failures, and on an outDir the build would have
|
|
@@ -59,107 +72,139 @@ function assertSafeOutDir(path, sourceDir, outDir) {
|
|
|
59
72
|
export async function buildQuiver(sourceDir, outDir) {
|
|
60
73
|
// Dynamic imports keep this module safe to type-import from browser contexts.
|
|
61
74
|
const path = await import('node:path');
|
|
62
|
-
const { join } = path;
|
|
63
|
-
const { mkdir, rm, writeFile } = await import('node:fs/promises');
|
|
75
|
+
const { dirname, join, resolve } = path;
|
|
76
|
+
const { mkdir, rename, rm, writeFile } = await import('node:fs/promises');
|
|
77
|
+
const { existsSync } = await import('node:fs');
|
|
64
78
|
const { createHash } = await import('node:crypto');
|
|
65
79
|
const { scanSourceQuiver, readQuillTree } = await import('./source-loader.js');
|
|
66
|
-
// 0.
|
|
67
|
-
//
|
|
80
|
+
// 0. Three trees get cleared here; refuse the paths where that deletes the
|
|
81
|
+
// caller instead of a previous build.
|
|
68
82
|
assertSafeOutDir(path, sourceDir, outDir);
|
|
83
|
+
const out = resolve(outDir);
|
|
84
|
+
const { stage, prev } = stagesOf(out);
|
|
69
85
|
// 1. Scan + validate source quiver (throws quiver_invalid on bad input).
|
|
70
86
|
const { meta, catalog } = await scanSourceQuiver(sourceDir);
|
|
71
|
-
// 2. Clear and recreate outDir + outDir/store/.
|
|
72
87
|
try {
|
|
73
|
-
await
|
|
74
|
-
await mkdir(join(outDir, 'store'), { recursive: true });
|
|
88
|
+
await packGeneration();
|
|
75
89
|
}
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
finally {
|
|
91
|
+
// A throw anywhere above leaves a partial generation staged, and outDir
|
|
92
|
+
// untouched. Neither tree outlives the call either way. Swept rather than
|
|
93
|
+
// checked: a sweep that threw would answer for the build, and what a caller
|
|
94
|
+
// needs to hear is why the build failed.
|
|
95
|
+
for (const at of [stage, prev])
|
|
96
|
+
await rm(at, { recursive: true, force: true }).catch(() => { });
|
|
78
97
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Write a whole generation into `stage`, then move it to outDir.
|
|
100
|
+
*
|
|
101
|
+
* The move is two renames: a directory rename refuses a non-empty target, so the
|
|
102
|
+
* outgoing generation steps aside first. That is the window, and it is two syscalls
|
|
103
|
+
* wide against the seconds a build takes.
|
|
104
|
+
*/
|
|
105
|
+
async function packGeneration() {
|
|
106
|
+
// 2. Clear and recreate the staged tree + its store/.
|
|
107
|
+
try {
|
|
108
|
+
await rm(stage, { recursive: true, force: true });
|
|
109
|
+
await mkdir(join(stage, 'store'), { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
throw new QuiverError('transport_error', `Failed to prepare output directory "${outDir}": ${err.message}`, { cause: err });
|
|
113
|
+
}
|
|
114
|
+
// 3. Process each quill version.
|
|
115
|
+
const manifestQuills = [];
|
|
116
|
+
for (const [quillName, versions] of catalog) {
|
|
117
|
+
for (const version of versions) {
|
|
118
|
+
const quillDir = join(sourceDir, 'quills', quillName, version);
|
|
119
|
+
// a. Read quill file tree.
|
|
120
|
+
const tree = await readQuillTree(quillDir);
|
|
121
|
+
// b. Partition fonts vs content.
|
|
122
|
+
const fontEntries = [];
|
|
123
|
+
const contentEntries = [];
|
|
124
|
+
for (const [path, bytes] of tree) {
|
|
125
|
+
if (FONT_EXT.test(path)) {
|
|
126
|
+
fontEntries.push([path, bytes]);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
contentEntries.push([path, bytes]);
|
|
130
|
+
}
|
|
92
131
|
}
|
|
93
|
-
|
|
94
|
-
|
|
132
|
+
// c. Dehydrate fonts into store/.
|
|
133
|
+
const fonts = {};
|
|
134
|
+
for (const [path, bytes] of fontEntries) {
|
|
135
|
+
// Full width: the store is keyed by hash, so two distinct fonts
|
|
136
|
+
// sharing a prefix would merge into one entry.
|
|
137
|
+
const hash = createHash('sha256').update(bytes).digest('hex');
|
|
138
|
+
try {
|
|
139
|
+
await writeFile(join(stage, 'store', hash), bytes);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
throw new QuiverError('transport_error', `Failed to write font store entry "${join(outDir, 'store', hash)}": ${err.message}`, { cause: err });
|
|
143
|
+
}
|
|
144
|
+
fonts[path] = hash;
|
|
95
145
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
146
|
+
// d. Zip content files (deterministic: sorted paths, fixed mtime).
|
|
147
|
+
const contentRecord = {};
|
|
148
|
+
for (const [path, bytes] of contentEntries) {
|
|
149
|
+
contentRecord[path] = bytes;
|
|
150
|
+
}
|
|
151
|
+
const zipBytes = packFiles(contentRecord);
|
|
152
|
+
// e–f. Compute bundle hash and name.
|
|
153
|
+
const bundleHash = createHash('sha256')
|
|
154
|
+
.update(zipBytes)
|
|
155
|
+
.digest('hex')
|
|
156
|
+
.slice(0, NAME_DIGEST_LENGTH);
|
|
157
|
+
const bundleName = `${quillName}@${version}.${bundleHash}.zip`;
|
|
158
|
+
// g. Write bundle zip.
|
|
104
159
|
try {
|
|
105
|
-
await writeFile(
|
|
160
|
+
await writeFile(join(stage, bundleName), zipBytes);
|
|
106
161
|
}
|
|
107
162
|
catch (err) {
|
|
108
|
-
throw new QuiverError('transport_error', `Failed to write
|
|
163
|
+
throw new QuiverError('transport_error', `Failed to write bundle "${join(outDir, bundleName)}": ${err.message}`, { cause: err });
|
|
109
164
|
}
|
|
110
|
-
|
|
165
|
+
// h. Record manifest entry.
|
|
166
|
+
manifestQuills.push({ name: quillName, version, bundle: bundleName, fonts });
|
|
111
167
|
}
|
|
112
|
-
// d. Zip content files (deterministic: sorted paths, fixed mtime).
|
|
113
|
-
const contentRecord = {};
|
|
114
|
-
for (const [path, bytes] of contentEntries) {
|
|
115
|
-
contentRecord[path] = bytes;
|
|
116
|
-
}
|
|
117
|
-
const zipBytes = packFiles(contentRecord);
|
|
118
|
-
// e–f. Compute bundle hash and name.
|
|
119
|
-
const bundleHash = createHash('sha256')
|
|
120
|
-
.update(zipBytes)
|
|
121
|
-
.digest('hex')
|
|
122
|
-
.slice(0, NAME_DIGEST_LENGTH);
|
|
123
|
-
const bundleName = `${quillName}@${version}.${bundleHash}.zip`;
|
|
124
|
-
// g. Write bundle zip.
|
|
125
|
-
const bundlePath = join(outDir, bundleName);
|
|
126
|
-
try {
|
|
127
|
-
await writeFile(bundlePath, zipBytes);
|
|
128
|
-
}
|
|
129
|
-
catch (err) {
|
|
130
|
-
throw new QuiverError('transport_error', `Failed to write bundle "${bundlePath}": ${err.message}`, { cause: err });
|
|
131
|
-
}
|
|
132
|
-
// h. Record manifest entry.
|
|
133
|
-
manifestQuills.push({ name: quillName, version, bundle: bundleName, fonts });
|
|
134
168
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
// 4–8. Build and write hashed manifest.
|
|
170
|
+
const manifest = {
|
|
171
|
+
version: 1,
|
|
172
|
+
name: meta.name,
|
|
173
|
+
quills: manifestQuills
|
|
174
|
+
};
|
|
175
|
+
const manifestJson = JSON.stringify(manifest, null, 2);
|
|
176
|
+
const manifestHash = createHash('sha256')
|
|
177
|
+
.update(manifestJson)
|
|
178
|
+
.digest('hex')
|
|
179
|
+
.slice(0, NAME_DIGEST_LENGTH);
|
|
180
|
+
const manifestFileName = `manifest.${manifestHash}.json`;
|
|
181
|
+
try {
|
|
182
|
+
await writeFile(join(stage, manifestFileName), manifestJson, 'utf-8');
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
throw new QuiverError('transport_error', `Failed to write manifest "${join(outDir, manifestFileName)}": ${err.message}`, { cause: err });
|
|
186
|
+
}
|
|
187
|
+
// 9–10. Write stable pointer latest.json. The format is stamped here and read first,
|
|
188
|
+
// so a client older than the tree says so rather than misreading it.
|
|
189
|
+
const pointer = { format: POINTER_FORMAT, manifest: manifestFileName };
|
|
190
|
+
try {
|
|
191
|
+
await writeFile(join(stage, 'latest.json'), JSON.stringify(pointer), 'utf-8');
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
throw new QuiverError('transport_error', `Failed to write pointer "${join(outDir, 'latest.json')}": ${err.message}`, { cause: err });
|
|
195
|
+
}
|
|
196
|
+
// 11. Move the generation in. `prev` is cleared first: a rename onto a
|
|
197
|
+
// non-empty directory fails, and the tree left by an interrupted run is
|
|
198
|
+
// one.
|
|
199
|
+
try {
|
|
200
|
+
await mkdir(dirname(out), { recursive: true });
|
|
201
|
+
await rm(prev, { recursive: true, force: true });
|
|
202
|
+
if (existsSync(out))
|
|
203
|
+
await rename(out, prev);
|
|
204
|
+
await rename(stage, out);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
throw new QuiverError('transport_error', `Failed to move the build into "${outDir}": ${err.message}`, { cause: err });
|
|
208
|
+
}
|
|
164
209
|
}
|
|
165
210
|
}
|
package/dist/quiver.d.ts
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Polymorphism via composition: internally stores a pluggable loader
|
|
5
5
|
* (either source-backed or build-output-backed).
|
|
6
6
|
*
|
|
7
|
-
* This module is browser-safe: only `fromBuiltUrl` and the
|
|
8
|
-
* here. The filesystem factories are free functions in
|
|
9
|
-
* is the same either way.
|
|
7
|
+
* This module is browser-safe: only `fromBuiltUrl`, `fromBuiltFiles` and the
|
|
8
|
+
* instance API live here. The filesystem factories are free functions in
|
|
9
|
+
* `./node.js`; the class is the same either way.
|
|
10
10
|
*/
|
|
11
|
-
import { Quill } from '@quillmark/wasm';
|
|
11
|
+
import type { Quill } from '@quillmark/wasm';
|
|
12
12
|
/** Loader strategy: source or build output. Package-internal. */
|
|
13
13
|
export interface QuiverLoader {
|
|
14
14
|
loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
|
|
@@ -24,8 +24,8 @@ export declare class Quiver {
|
|
|
24
24
|
readonly name: string;
|
|
25
25
|
/**
|
|
26
26
|
* Private constructor. A Quiver comes from a factory (`Quiver.fromBuiltUrl`,
|
|
27
|
-
* or `fromDir` / `fromBuiltDir` from
|
|
28
|
-
* names the thing being read.
|
|
27
|
+
* `Quiver.fromBuiltFiles`, or `fromDir` / `fromBuiltDir` from
|
|
28
|
+
* `@quillmark/quiver/node`), which is what names the thing being read.
|
|
29
29
|
*/
|
|
30
30
|
private constructor();
|
|
31
31
|
/**
|
|
@@ -35,10 +35,37 @@ export declare class Quiver {
|
|
|
35
35
|
* environments. `file://` URLs are rejected — to load build output from
|
|
36
36
|
* disk in Node, use `fromBuiltDir(path)` from `@quillmark/quiver/node`.
|
|
37
37
|
*
|
|
38
|
+
* `seed` answers for the artifact bytes the caller already holds, keyed by
|
|
39
|
+
* artifact-relative path; the URL serves the rest. Seeding `latest.json`
|
|
40
|
+
* settles which catalog this process reads at deploy time rather than at
|
|
41
|
+
* cache-revalidation time. Seeded bytes are digest-checked as fetched ones
|
|
42
|
+
* are.
|
|
43
|
+
*
|
|
38
44
|
* Throws `transport_error` on network/HTTP failure, `quiver_invalid`
|
|
39
45
|
* on format errors.
|
|
40
46
|
*/
|
|
41
|
-
static fromBuiltUrl(url: string
|
|
47
|
+
static fromBuiltUrl(url: string, opts?: {
|
|
48
|
+
seed?: ReadonlyMap<string, Uint8Array>;
|
|
49
|
+
}): Promise<Quiver>;
|
|
50
|
+
/**
|
|
51
|
+
* Browser-safe factory. Loads build output from the bytes themselves, keyed
|
|
52
|
+
* by artifact-relative path (`latest.json`, `manifest.<digest>.json`,
|
|
53
|
+
* `<name>@<x.y.z>.<digest>.zip`, `store/<hash>`) — the shape `build` writes
|
|
54
|
+
* and `fromBuiltDir` reads back.
|
|
55
|
+
*
|
|
56
|
+
* Nothing is fetched, so a runtime whose artifact is not on a path it can
|
|
57
|
+
* read (a serverless function, a bundler that inlines it, a test) reaches
|
|
58
|
+
* one without fetching its own static output back over its own load
|
|
59
|
+
* balancer.
|
|
60
|
+
*
|
|
61
|
+
* The map must carry the whole artifact; a missing path is a
|
|
62
|
+
* `transport_error` naming it. To hold part and fetch the rest, pass the map
|
|
63
|
+
* as `fromBuiltUrl`'s `seed`.
|
|
64
|
+
*
|
|
65
|
+
* Throws `quiver_invalid` on format errors, `transport_error` on a path the
|
|
66
|
+
* map does not carry.
|
|
67
|
+
*/
|
|
68
|
+
static fromBuiltFiles(files: ReadonlyMap<string, Uint8Array>): Promise<Quiver>;
|
|
42
69
|
/** Returns all known quill names, sorted lexicographically. */
|
|
43
70
|
quillNames(): string[];
|
|
44
71
|
/**
|
package/dist/quiver.js
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
* Polymorphism via composition: internally stores a pluggable loader
|
|
5
5
|
* (either source-backed or build-output-backed).
|
|
6
6
|
*
|
|
7
|
-
* This module is browser-safe: only `fromBuiltUrl` and the
|
|
8
|
-
* here. The filesystem factories are free functions in
|
|
9
|
-
* is the same either way.
|
|
7
|
+
* This module is browser-safe: only `fromBuiltUrl`, `fromBuiltFiles` and the
|
|
8
|
+
* instance API live here. The filesystem factories are free functions in
|
|
9
|
+
* `./node.js`; the class is the same either way.
|
|
10
10
|
*/
|
|
11
11
|
import { QuiverError } from './errors.js';
|
|
12
|
-
import {
|
|
12
|
+
import { init } from '@quillmark/wasm';
|
|
13
13
|
import { parseQuillRef } from './ref.js';
|
|
14
14
|
import { matchesSemverSelector, chooseHighestVersion } from './semver.js';
|
|
15
15
|
/**
|
|
@@ -31,8 +31,8 @@ export class Quiver {
|
|
|
31
31
|
#quillCache = new Map();
|
|
32
32
|
/**
|
|
33
33
|
* Private constructor. A Quiver comes from a factory (`Quiver.fromBuiltUrl`,
|
|
34
|
-
* or `fromDir` / `fromBuiltDir` from
|
|
35
|
-
* names the thing being read.
|
|
34
|
+
* `Quiver.fromBuiltFiles`, or `fromDir` / `fromBuiltDir` from
|
|
35
|
+
* `@quillmark/quiver/node`), which is what names the thing being read.
|
|
36
36
|
*/
|
|
37
37
|
constructor(name, catalog, loader) {
|
|
38
38
|
this.name = name;
|
|
@@ -49,17 +49,49 @@ export class Quiver {
|
|
|
49
49
|
* environments. `file://` URLs are rejected — to load build output from
|
|
50
50
|
* disk in Node, use `fromBuiltDir(path)` from `@quillmark/quiver/node`.
|
|
51
51
|
*
|
|
52
|
+
* `seed` answers for the artifact bytes the caller already holds, keyed by
|
|
53
|
+
* artifact-relative path; the URL serves the rest. Seeding `latest.json`
|
|
54
|
+
* settles which catalog this process reads at deploy time rather than at
|
|
55
|
+
* cache-revalidation time. Seeded bytes are digest-checked as fetched ones
|
|
56
|
+
* are.
|
|
57
|
+
*
|
|
52
58
|
* Throws `transport_error` on network/HTTP failure, `quiver_invalid`
|
|
53
59
|
* on format errors.
|
|
54
60
|
*/
|
|
55
|
-
static async fromBuiltUrl(url) {
|
|
61
|
+
static async fromBuiltUrl(url, opts) {
|
|
56
62
|
if (url.startsWith('file://')) {
|
|
57
63
|
throw new QuiverError('transport_error', `Quiver.fromBuiltUrl requires an http(s):// or origin-relative URL; got "${url}". For local build output, use import { fromBuiltDir } from '@quillmark/quiver/node'.`);
|
|
58
64
|
}
|
|
59
65
|
const { HttpTransport } = await import('./transports/http-transport.js');
|
|
60
66
|
const { loadBuiltQuiver } = await import('./built-loader.js');
|
|
61
|
-
const
|
|
62
|
-
|
|
67
|
+
const http = new HttpTransport(url);
|
|
68
|
+
if (opts?.seed === undefined)
|
|
69
|
+
return loadBuiltQuiver(http);
|
|
70
|
+
const { MemoryTransport } = await import('./transports/memory-transport.js');
|
|
71
|
+
return loadBuiltQuiver(new MemoryTransport(opts.seed, http));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Browser-safe factory. Loads build output from the bytes themselves, keyed
|
|
75
|
+
* by artifact-relative path (`latest.json`, `manifest.<digest>.json`,
|
|
76
|
+
* `<name>@<x.y.z>.<digest>.zip`, `store/<hash>`) — the shape `build` writes
|
|
77
|
+
* and `fromBuiltDir` reads back.
|
|
78
|
+
*
|
|
79
|
+
* Nothing is fetched, so a runtime whose artifact is not on a path it can
|
|
80
|
+
* read (a serverless function, a bundler that inlines it, a test) reaches
|
|
81
|
+
* one without fetching its own static output back over its own load
|
|
82
|
+
* balancer.
|
|
83
|
+
*
|
|
84
|
+
* The map must carry the whole artifact; a missing path is a
|
|
85
|
+
* `transport_error` naming it. To hold part and fetch the rest, pass the map
|
|
86
|
+
* as `fromBuiltUrl`'s `seed`.
|
|
87
|
+
*
|
|
88
|
+
* Throws `quiver_invalid` on format errors, `transport_error` on a path the
|
|
89
|
+
* map does not carry.
|
|
90
|
+
*/
|
|
91
|
+
static async fromBuiltFiles(files) {
|
|
92
|
+
const { MemoryTransport } = await import('./transports/memory-transport.js');
|
|
93
|
+
const { loadBuiltQuiver } = await import('./built-loader.js');
|
|
94
|
+
return loadBuiltQuiver(new MemoryTransport(files));
|
|
63
95
|
}
|
|
64
96
|
/** Returns all known quill names, sorted lexicographically. */
|
|
65
97
|
quillNames() {
|
|
@@ -147,10 +179,17 @@ export class Quiver {
|
|
|
147
179
|
*
|
|
148
180
|
* Every caller arrives through `resolve` or the catalog itself, so the ref is
|
|
149
181
|
* catalog-backed by construction and the loader needs no gate.
|
|
182
|
+
*
|
|
183
|
+
* `init()` is the only door to `Quill`, awaited here rather than left a
|
|
184
|
+
* precondition of `getQuill`. The gate is memoized, so this is one instantiation
|
|
185
|
+
* across every caller, overlapped with the load that pays for it.
|
|
150
186
|
*/
|
|
151
187
|
async #materializeQuill(canonicalRef) {
|
|
152
188
|
const at = canonicalRef.indexOf('@');
|
|
153
|
-
const tree = await
|
|
189
|
+
const [{ Quill }, tree] = await Promise.all([
|
|
190
|
+
init(),
|
|
191
|
+
this.#loader.loadTree(canonicalRef.slice(0, at), canonicalRef.slice(at + 1))
|
|
192
|
+
]);
|
|
154
193
|
return Quill.fromTree(tree);
|
|
155
194
|
}
|
|
156
195
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryTransport — built-quiver transport over bytes the caller already holds,
|
|
3
|
+
* falling through to another transport for what the map does not carry.
|
|
4
|
+
* Internal; not exported from index.ts.
|
|
5
|
+
*
|
|
6
|
+
* Browser-safe: no `node:` imports at any level.
|
|
7
|
+
*/
|
|
8
|
+
import type { BuiltTransport, FetchOptions } from '../built-loader.js';
|
|
9
|
+
export declare class MemoryTransport implements BuiltTransport {
|
|
10
|
+
private readonly fallback?;
|
|
11
|
+
private readonly files;
|
|
12
|
+
constructor(files: ReadonlyMap<string, Uint8Array>, fallback?: BuiltTransport | undefined);
|
|
13
|
+
/** The map answers first. `FetchOptions` reaches only the fallback: a held byte has no cache to revalidate against. */
|
|
14
|
+
fetchBytes(relativePath: string, opts?: FetchOptions): Promise<Uint8Array>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemoryTransport — built-quiver transport over bytes the caller already holds,
|
|
3
|
+
* falling through to another transport for what the map does not carry.
|
|
4
|
+
* Internal; not exported from index.ts.
|
|
5
|
+
*
|
|
6
|
+
* Browser-safe: no `node:` imports at any level.
|
|
7
|
+
*/
|
|
8
|
+
import { QuiverError } from '../errors.js';
|
|
9
|
+
/** Keys are artifact-relative, as the loader asks for them; a leading `./` or `/` is stripped. */
|
|
10
|
+
function normalizeKeys(files) {
|
|
11
|
+
const out = new Map();
|
|
12
|
+
for (const [key, bytes] of files) {
|
|
13
|
+
out.set(key.replace(/^\.?\//, ''), bytes);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
export class MemoryTransport {
|
|
18
|
+
fallback;
|
|
19
|
+
files;
|
|
20
|
+
constructor(files, fallback) {
|
|
21
|
+
this.fallback = fallback;
|
|
22
|
+
this.files = normalizeKeys(files);
|
|
23
|
+
}
|
|
24
|
+
/** The map answers first. `FetchOptions` reaches only the fallback: a held byte has no cache to revalidate against. */
|
|
25
|
+
async fetchBytes(relativePath, opts) {
|
|
26
|
+
const bytes = this.files.get(relativePath);
|
|
27
|
+
if (bytes !== undefined)
|
|
28
|
+
return bytes;
|
|
29
|
+
if (this.fallback === undefined) {
|
|
30
|
+
throw new QuiverError('transport_error', `No bytes held for "${relativePath}". Quiver.fromBuiltFiles needs the whole artifact; to hold part and fetch the rest, pass the map as Quiver.fromBuiltUrl's \`seed\`.`);
|
|
31
|
+
}
|
|
32
|
+
return this.fallback.fetchBytes(relativePath, opts);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/quiver",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "A quiver of Quillmark quills: load one, pack one, hand out quills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -38,10 +38,11 @@
|
|
|
38
38
|
"sideEffects": false,
|
|
39
39
|
"files": [
|
|
40
40
|
"dist",
|
|
41
|
+
"MIGRATION.md",
|
|
41
42
|
"NOTICE"
|
|
42
43
|
],
|
|
43
44
|
"peerDependencies": {
|
|
44
|
-
"@quillmark/wasm": ">=0.
|
|
45
|
+
"@quillmark/wasm": ">=0.103.0-0"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"fflate": "^0.8.2",
|
|
@@ -53,9 +54,6 @@
|
|
|
53
54
|
"typescript": "^5.9.3",
|
|
54
55
|
"vitest": "^4.0.18"
|
|
55
56
|
},
|
|
56
|
-
"bin": {
|
|
57
|
-
"quillmark-quiver": "./dist/bin/quiver.js"
|
|
58
|
-
},
|
|
59
57
|
"scripts": {
|
|
60
58
|
"build": "tsc",
|
|
61
59
|
"prepare": "npm run build",
|
package/dist/bin/quiver.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Quiver CLI — build and test a quiver collection. Two verbs, and they are the
|
|
4
|
-
* whole of what a quiver author gets from here: `build` packs, `test` gates.
|
|
5
|
-
* Looking at rendered output is studio's, an app rather than a verb.
|
|
6
|
-
*
|
|
7
|
-
* Commands:
|
|
8
|
-
* quillmark-quiver build [--out <dir>]
|
|
9
|
-
* quillmark-quiver test
|
|
10
|
-
*
|
|
11
|
-
* Engine discovery (for `test`):
|
|
12
|
-
* 1. Named export `engine` from `quiver.config.js` at the collection root,
|
|
13
|
-
* if the file exists.
|
|
14
|
-
* 2. Auto-imported `@quillmark/wasm` from the collection's node_modules.
|
|
15
|
-
*/
|
|
16
|
-
export {};
|
package/dist/bin/quiver.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Quiver CLI — build and test a quiver collection. Two verbs, and they are the
|
|
4
|
-
* whole of what a quiver author gets from here: `build` packs, `test` gates.
|
|
5
|
-
* Looking at rendered output is studio's, an app rather than a verb.
|
|
6
|
-
*
|
|
7
|
-
* Commands:
|
|
8
|
-
* quillmark-quiver build [--out <dir>]
|
|
9
|
-
* quillmark-quiver test
|
|
10
|
-
*
|
|
11
|
-
* Engine discovery (for `test`):
|
|
12
|
-
* 1. Named export `engine` from `quiver.config.js` at the collection root,
|
|
13
|
-
* if the file exists.
|
|
14
|
-
* 2. Auto-imported `@quillmark/wasm` from the collection's node_modules.
|
|
15
|
-
*/
|
|
16
|
-
import { createRequire } from 'node:module';
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
import { pathToFileURL } from 'node:url';
|
|
19
|
-
import { build as buildQuiver, fromDir } from '../node.js';
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Arg parsing helpers
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
const argv = process.argv.slice(2);
|
|
24
|
-
const command = argv[0];
|
|
25
|
-
/** One flag with a value. `build --out` is the only one either verb takes. */
|
|
26
|
-
function flag(name) {
|
|
27
|
-
const i = argv.indexOf(name);
|
|
28
|
-
return i !== -1 && i + 1 < argv.length ? argv[i + 1] : undefined;
|
|
29
|
-
}
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Engine discovery
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
async function loadEngine(cwd) {
|
|
34
|
-
// Resolve @quillmark/wasm from the collection's node_modules (best-effort).
|
|
35
|
-
// Its `Engine` renders every quill (cloning the core Quill/Document into the
|
|
36
|
-
// backend itself). A quiver.config.js may export a custom `engine`.
|
|
37
|
-
let wasm;
|
|
38
|
-
try {
|
|
39
|
-
const req = createRequire(pathToFileURL(join(cwd, 'package.json')).href);
|
|
40
|
-
const wasmPath = req.resolve('@quillmark/wasm');
|
|
41
|
-
wasm = (await import(pathToFileURL(wasmPath).href));
|
|
42
|
-
// Instantiate the core. Every `@quillmark/wasm` export throws
|
|
43
|
-
// `runtime::not_initialized` until `init()` resolves, and `new Engine()` is
|
|
44
|
-
// lazy, so without this the throw arrives at the first render and reads as a
|
|
45
|
-
// failing quill rather than an uninitialized runtime.
|
|
46
|
-
await wasm.init();
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
// Not installed — a quiver.config.js may still provide an engine.
|
|
50
|
-
}
|
|
51
|
-
// 1. Named `engine` export from quiver.config.js, if present.
|
|
52
|
-
try {
|
|
53
|
-
const cfg = await import(pathToFileURL(join(cwd, 'quiver.config.js')).href);
|
|
54
|
-
if (cfg.engine != null) {
|
|
55
|
-
return cfg.engine;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
// File absent or incomplete — fall through to auto-discovery.
|
|
60
|
-
}
|
|
61
|
-
// 2. Auto-discover from @quillmark/wasm.
|
|
62
|
-
if (wasm == null) {
|
|
63
|
-
throw new Error("Cannot find @quillmark/wasm in this collection's node_modules.\n" +
|
|
64
|
-
' Install it: npm install @quillmark/wasm\n' +
|
|
65
|
-
' Or export { engine } from quiver.config.js for a custom engine.');
|
|
66
|
-
}
|
|
67
|
-
return new wasm.Engine();
|
|
68
|
-
}
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// Commands
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
async function build() {
|
|
73
|
-
const cwd = process.cwd();
|
|
74
|
-
const outDir = flag('--out') ?? 'dist';
|
|
75
|
-
console.log(`quillmark-quiver build: ${cwd} → ${outDir}`);
|
|
76
|
-
await buildQuiver(cwd, outDir);
|
|
77
|
-
console.log('done.');
|
|
78
|
-
}
|
|
79
|
-
async function test() {
|
|
80
|
-
const cwd = process.cwd();
|
|
81
|
-
const engine = await loadEngine(cwd);
|
|
82
|
-
const quiver = await fromDir(cwd);
|
|
83
|
-
const names = quiver.quillNames();
|
|
84
|
-
if (names.length === 0) {
|
|
85
|
-
console.error('error: quiver has no quills');
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
let pass = 0;
|
|
89
|
-
let fail = 0;
|
|
90
|
-
for (const name of names) {
|
|
91
|
-
for (const version of quiver.versionsOf(name)) {
|
|
92
|
-
const ref = `${name}@${version}`;
|
|
93
|
-
try {
|
|
94
|
-
const quill = await quiver.getQuill(ref);
|
|
95
|
-
const doc = quill.seedDocument();
|
|
96
|
-
let result;
|
|
97
|
-
try {
|
|
98
|
-
result = await engine.render(quill, doc);
|
|
99
|
-
}
|
|
100
|
-
finally {
|
|
101
|
-
doc.free();
|
|
102
|
-
}
|
|
103
|
-
if (!Array.isArray(result.artifacts) || result.artifacts.length === 0) {
|
|
104
|
-
throw new Error('example render produced no artifacts');
|
|
105
|
-
}
|
|
106
|
-
console.log(`pass ${ref}`);
|
|
107
|
-
pass++;
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
console.error(`FAIL ${ref} — ${err.message}`);
|
|
111
|
-
fail++;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
const total = pass + fail;
|
|
116
|
-
console.log(`\n${pass}/${total} passed`);
|
|
117
|
-
if (fail > 0)
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// Dispatch
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
function usage() {
|
|
124
|
-
console.error(['Usage:', ' quillmark-quiver build [--out <dir>]', ' quillmark-quiver test'].join('\n'));
|
|
125
|
-
}
|
|
126
|
-
function die(err) {
|
|
127
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
128
|
-
console.error(`error: ${msg}`);
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
switch (command) {
|
|
132
|
-
case 'build':
|
|
133
|
-
build().catch(die);
|
|
134
|
-
break;
|
|
135
|
-
case 'test':
|
|
136
|
-
test().catch(die);
|
|
137
|
-
break;
|
|
138
|
-
default:
|
|
139
|
-
usage();
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|