@quillmark/quiver 0.19.0 → 0.21.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 +39 -9
- package/dist/build.js +4 -18
- package/dist/built-loader.d.ts +1 -8
- package/dist/built-loader.js +4 -16
- package/dist/format.d.ts +1 -1
- package/dist/format.js +1 -1
- package/dist/node.d.ts +2 -3
- package/dist/node.js +2 -3
- package/dist/quiver-yaml.d.ts +3 -5
- package/dist/quiver-yaml.js +3 -8
- package/dist/quiver.d.ts +38 -12
- package/dist/quiver.js +54 -16
- package/dist/ref.d.ts +1 -1
- package/dist/ref.js +0 -3
- package/dist/source-loader.d.ts +5 -7
- package/dist/source-loader.js +5 -17
- package/dist/transports/http-transport.js +0 -2
- package/dist/transports/memory-transport.d.ts +15 -0
- package/dist/transports/memory-transport.js +34 -0
- package/package.json +3 -2
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
|
|
|
@@ -66,12 +70,13 @@ await build('./node_modules/@org/my-quiver', './public/quivers/my-quiver');
|
|
|
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.
|
package/dist/build.js
CHANGED
|
@@ -77,12 +77,9 @@ export async function buildQuiver(sourceDir, outDir) {
|
|
|
77
77
|
const { existsSync } = await import('node:fs');
|
|
78
78
|
const { createHash } = await import('node:crypto');
|
|
79
79
|
const { scanSourceQuiver, readQuillTree } = await import('./source-loader.js');
|
|
80
|
-
// 0. Three trees get cleared here; refuse the paths where that deletes the
|
|
81
|
-
// caller instead of a previous build.
|
|
82
80
|
assertSafeOutDir(path, sourceDir, outDir);
|
|
83
81
|
const out = resolve(outDir);
|
|
84
82
|
const { stage, prev } = stagesOf(out);
|
|
85
|
-
// 1. Scan + validate source quiver (throws quiver_invalid on bad input).
|
|
86
83
|
const { meta, catalog } = await scanSourceQuiver(sourceDir);
|
|
87
84
|
try {
|
|
88
85
|
await packGeneration();
|
|
@@ -103,7 +100,6 @@ export async function buildQuiver(sourceDir, outDir) {
|
|
|
103
100
|
* wide against the seconds a build takes.
|
|
104
101
|
*/
|
|
105
102
|
async function packGeneration() {
|
|
106
|
-
// 2. Clear and recreate the staged tree + its store/.
|
|
107
103
|
try {
|
|
108
104
|
await rm(stage, { recursive: true, force: true });
|
|
109
105
|
await mkdir(join(stage, 'store'), { recursive: true });
|
|
@@ -111,14 +107,11 @@ export async function buildQuiver(sourceDir, outDir) {
|
|
|
111
107
|
catch (err) {
|
|
112
108
|
throw new QuiverError('transport_error', `Failed to prepare output directory "${outDir}": ${err.message}`, { cause: err });
|
|
113
109
|
}
|
|
114
|
-
// 3. Process each quill version.
|
|
115
110
|
const manifestQuills = [];
|
|
116
111
|
for (const [quillName, versions] of catalog) {
|
|
117
112
|
for (const version of versions) {
|
|
118
113
|
const quillDir = join(sourceDir, 'quills', quillName, version);
|
|
119
|
-
// a. Read quill file tree.
|
|
120
114
|
const tree = await readQuillTree(quillDir);
|
|
121
|
-
// b. Partition fonts vs content.
|
|
122
115
|
const fontEntries = [];
|
|
123
116
|
const contentEntries = [];
|
|
124
117
|
for (const [path, bytes] of tree) {
|
|
@@ -129,7 +122,6 @@ export async function buildQuiver(sourceDir, outDir) {
|
|
|
129
122
|
contentEntries.push([path, bytes]);
|
|
130
123
|
}
|
|
131
124
|
}
|
|
132
|
-
// c. Dehydrate fonts into store/.
|
|
133
125
|
const fonts = {};
|
|
134
126
|
for (const [path, bytes] of fontEntries) {
|
|
135
127
|
// Full width: the store is keyed by hash, so two distinct fonts
|
|
@@ -143,30 +135,25 @@ export async function buildQuiver(sourceDir, outDir) {
|
|
|
143
135
|
}
|
|
144
136
|
fonts[path] = hash;
|
|
145
137
|
}
|
|
146
|
-
// d. Zip content files (deterministic: sorted paths, fixed mtime).
|
|
147
138
|
const contentRecord = {};
|
|
148
139
|
for (const [path, bytes] of contentEntries) {
|
|
149
140
|
contentRecord[path] = bytes;
|
|
150
141
|
}
|
|
151
142
|
const zipBytes = packFiles(contentRecord);
|
|
152
|
-
// e–f. Compute bundle hash and name.
|
|
153
143
|
const bundleHash = createHash('sha256')
|
|
154
144
|
.update(zipBytes)
|
|
155
145
|
.digest('hex')
|
|
156
146
|
.slice(0, NAME_DIGEST_LENGTH);
|
|
157
147
|
const bundleName = `${quillName}@${version}.${bundleHash}.zip`;
|
|
158
|
-
// g. Write bundle zip.
|
|
159
148
|
try {
|
|
160
149
|
await writeFile(join(stage, bundleName), zipBytes);
|
|
161
150
|
}
|
|
162
151
|
catch (err) {
|
|
163
152
|
throw new QuiverError('transport_error', `Failed to write bundle "${join(outDir, bundleName)}": ${err.message}`, { cause: err });
|
|
164
153
|
}
|
|
165
|
-
// h. Record manifest entry.
|
|
166
154
|
manifestQuills.push({ name: quillName, version, bundle: bundleName, fonts });
|
|
167
155
|
}
|
|
168
156
|
}
|
|
169
|
-
// 4–8. Build and write hashed manifest.
|
|
170
157
|
const manifest = {
|
|
171
158
|
version: 1,
|
|
172
159
|
name: meta.name,
|
|
@@ -184,8 +171,8 @@ export async function buildQuiver(sourceDir, outDir) {
|
|
|
184
171
|
catch (err) {
|
|
185
172
|
throw new QuiverError('transport_error', `Failed to write manifest "${join(outDir, manifestFileName)}": ${err.message}`, { cause: err });
|
|
186
173
|
}
|
|
187
|
-
//
|
|
188
|
-
//
|
|
174
|
+
// The format is stamped here and read first, so a client older than the tree
|
|
175
|
+
// says so rather than misreading it.
|
|
189
176
|
const pointer = { format: POINTER_FORMAT, manifest: manifestFileName };
|
|
190
177
|
try {
|
|
191
178
|
await writeFile(join(stage, 'latest.json'), JSON.stringify(pointer), 'utf-8');
|
|
@@ -193,9 +180,8 @@ export async function buildQuiver(sourceDir, outDir) {
|
|
|
193
180
|
catch (err) {
|
|
194
181
|
throw new QuiverError('transport_error', `Failed to write pointer "${join(outDir, 'latest.json')}": ${err.message}`, { cause: err });
|
|
195
182
|
}
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// one.
|
|
183
|
+
// `prev` is cleared first: a rename onto a non-empty directory fails, and the
|
|
184
|
+
// tree left by an interrupted run is one.
|
|
199
185
|
try {
|
|
200
186
|
await mkdir(dirname(out), { recursive: true });
|
|
201
187
|
await rm(prev, { recursive: true, force: true });
|
package/dist/built-loader.d.ts
CHANGED
|
@@ -22,12 +22,5 @@ export interface BuiltTransport {
|
|
|
22
22
|
export interface FetchOptions {
|
|
23
23
|
revalidate?: boolean;
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Load a build-output quiver via the given transport.
|
|
27
|
-
*
|
|
28
|
-
* 1. Fetches latest.json (pointer) and parses it.
|
|
29
|
-
* 2. Fetches the manifest file it points to and validates it.
|
|
30
|
-
* 3. Builds a catalog from manifest entries (versions sorted descending).
|
|
31
|
-
* 4. Returns a Quiver instance backed by a BuiltLoader.
|
|
32
|
-
*/
|
|
25
|
+
/** Load a build-output quiver via the given transport. */
|
|
33
26
|
export declare function loadBuiltQuiver(transport: BuiltTransport): Promise<Quiver>;
|
package/dist/built-loader.js
CHANGED
|
@@ -68,15 +68,12 @@ class BuiltLoader {
|
|
|
68
68
|
// The catalog the outer Quiver resolves against derives from this index,
|
|
69
69
|
// so every ref reaching here has an entry.
|
|
70
70
|
const entry = this.index.get(`${name}@${version}`);
|
|
71
|
-
// 1. Fetch + unpack bundle zip.
|
|
72
71
|
const zipBytes = await fetchVerified(this.transport, entry.bundle, digestOfName(BUNDLE_FILENAME_RE, entry.bundle, 'bundle filename'));
|
|
73
72
|
const files = unpackFiles(zipBytes);
|
|
74
|
-
// 2. Rehydrate fonts from store (coalesced).
|
|
75
73
|
const fontEntries = Object.entries(entry.fonts);
|
|
76
74
|
await Promise.all(fontEntries.map(async ([path, hash]) => {
|
|
77
75
|
files[path] = await this.fetchFont(hash);
|
|
78
76
|
}));
|
|
79
|
-
// 3. Convert to Map.
|
|
80
77
|
return new Map(Object.entries(files));
|
|
81
78
|
}
|
|
82
79
|
/**
|
|
@@ -116,7 +113,7 @@ function parsePointer(raw) {
|
|
|
116
113
|
throw new QuiverError('quiver_invalid', 'latest.json must be a JSON object');
|
|
117
114
|
}
|
|
118
115
|
const obj = parsed;
|
|
119
|
-
// The format, read before anything is believed about the rest. An unknown key is
|
|
116
|
+
// The format, read before anything is believed about the rest. An unknown key is not
|
|
120
117
|
// a failure here: this is the document a newer format announces itself on, and a
|
|
121
118
|
// reader rejecting what it does not recognise refuses the announcement too. Absent
|
|
122
119
|
// means a build from before the marker (`format.ts`).
|
|
@@ -215,17 +212,10 @@ function catalogOf(index) {
|
|
|
215
212
|
return catalog;
|
|
216
213
|
}
|
|
217
214
|
// ─── Main entry point ─────────────────────────────────────────────────────────
|
|
218
|
-
/**
|
|
219
|
-
* Load a build-output quiver via the given transport.
|
|
220
|
-
*
|
|
221
|
-
* 1. Fetches latest.json (pointer) and parses it.
|
|
222
|
-
* 2. Fetches the manifest file it points to and validates it.
|
|
223
|
-
* 3. Builds a catalog from manifest entries (versions sorted descending).
|
|
224
|
-
* 4. Returns a Quiver instance backed by a BuiltLoader.
|
|
225
|
-
*/
|
|
215
|
+
/** Load a build-output quiver via the given transport. */
|
|
226
216
|
export async function loadBuiltQuiver(transport) {
|
|
227
|
-
//
|
|
228
|
-
//
|
|
217
|
+
// The pointer is the one name that is not content-addressed, so it is also the
|
|
218
|
+
// one fetch that must revalidate.
|
|
229
219
|
let pointerBytes;
|
|
230
220
|
try {
|
|
231
221
|
pointerBytes = await transport.fetchBytes('latest.json', { revalidate: true });
|
|
@@ -237,7 +227,6 @@ export async function loadBuiltQuiver(transport) {
|
|
|
237
227
|
}
|
|
238
228
|
const manifestFileName = parsePointer(new TextDecoder().decode(pointerBytes));
|
|
239
229
|
const manifestDigest = digestOfName(MANIFEST_FILENAME_RE, manifestFileName, 'Pointer manifest filename');
|
|
240
|
-
// 2. Fetch and parse manifest.
|
|
241
230
|
let manifestBytes;
|
|
242
231
|
try {
|
|
243
232
|
manifestBytes = await fetchVerified(transport, manifestFileName, manifestDigest);
|
|
@@ -247,7 +236,6 @@ export async function loadBuiltQuiver(transport) {
|
|
|
247
236
|
throw err;
|
|
248
237
|
throw new QuiverError('transport_error', `Failed to fetch manifest "${manifestFileName}": ${err.message}`, { cause: err });
|
|
249
238
|
}
|
|
250
|
-
// 3–4. Validate manifest + assemble catalog/loader.
|
|
251
239
|
const manifest = parseManifest(new TextDecoder().decode(manifestBytes));
|
|
252
240
|
const index = new Map();
|
|
253
241
|
for (const entry of manifest.quills) {
|
package/dist/format.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (QUIVER §"The pointer" for why the pointer carries it and reads past keys it does not
|
|
4
4
|
* know).
|
|
5
5
|
*
|
|
6
|
-
* It moves when a reader assuming it would
|
|
6
|
+
* It moves when a reader assuming it would misread a tree rather than fail on it: a
|
|
7
7
|
* renamed pointer field, a second pointer, a store keyed by something other than the
|
|
8
8
|
* full hash. A change confined to the manifest's own shape moves the manifest's
|
|
9
9
|
* `version` instead.
|
package/dist/format.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (QUIVER §"The pointer" for why the pointer carries it and reads past keys it does not
|
|
4
4
|
* know).
|
|
5
5
|
*
|
|
6
|
-
* It moves when a reader assuming it would
|
|
6
|
+
* It moves when a reader assuming it would misread a tree rather than fail on it: a
|
|
7
7
|
* renamed pointer field, a second pointer, a store keyed by something other than the
|
|
8
8
|
* full hash. A change confined to the manifest's own shape moves the manifest's
|
|
9
9
|
* `version` instead.
|
package/dist/node.d.ts
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* from the main entry, unmodified. Importing this module installs nothing, so
|
|
5
5
|
* the import path is the whole contract and a bundler drops what is not called.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* builtins. Browser bundles must never reach it. The main entry (`./index.js`)
|
|
7
|
+
* The modules behind these factories statically import `node:*` builtins, so a
|
|
8
|
+
* browser bundle must never reach this entry. The main entry (`./index.js`)
|
|
10
9
|
* makes no static or dynamic reference to it.
|
|
11
10
|
*/
|
|
12
11
|
import { Quiver } from './quiver.js';
|
package/dist/node.js
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
* from the main entry, unmodified. Importing this module installs nothing, so
|
|
5
5
|
* the import path is the whole contract and a bundler drops what is not called.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* builtins. Browser bundles must never reach it. The main entry (`./index.js`)
|
|
7
|
+
* The modules behind these factories statically import `node:*` builtins, so a
|
|
8
|
+
* browser bundle must never reach this entry. The main entry (`./index.js`)
|
|
10
9
|
* makes no static or dynamic reference to it.
|
|
11
10
|
*/
|
|
12
11
|
import { createQuiver } from './quiver.js';
|
package/dist/quiver-yaml.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Internal parser/validator for Quiver.yaml
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* simple two-field schema (name, description), but using a proper YAML parser
|
|
6
|
-
* ensures correct handling of quoting, escaping, and multi-line strings.
|
|
2
|
+
* Internal parser/validator for Quiver.yaml: two fields, `name` and
|
|
3
|
+
* `description`, strict about everything else. Parsed through the `yaml`
|
|
4
|
+
* package, so quoting, escaping and multi-line strings behave as YAML.
|
|
7
5
|
*/
|
|
8
6
|
export interface QuiverMeta {
|
|
9
7
|
name: string;
|
package/dist/quiver-yaml.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Internal parser/validator for Quiver.yaml
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* simple two-field schema (name, description), but using a proper YAML parser
|
|
6
|
-
* ensures correct handling of quoting, escaping, and multi-line strings.
|
|
2
|
+
* Internal parser/validator for Quiver.yaml: two fields, `name` and
|
|
3
|
+
* `description`, strict about everything else. Parsed through the `yaml`
|
|
4
|
+
* package, so quoting, escaping and multi-line strings behave as YAML.
|
|
7
5
|
*/
|
|
8
6
|
import { parse as parseYaml } from 'yaml';
|
|
9
7
|
import { QuiverError } from './errors.js';
|
|
@@ -32,13 +30,11 @@ export function parseQuiverYaml(raw) {
|
|
|
32
30
|
throw new QuiverError('quiver_invalid', `Quiver.yaml: expected a mapping at the top level, got ${Array.isArray(parsed) ? 'array' : String(parsed)}`);
|
|
33
31
|
}
|
|
34
32
|
const doc = parsed;
|
|
35
|
-
// Check for unknown fields (strict mode)
|
|
36
33
|
for (const key of Object.keys(doc)) {
|
|
37
34
|
if (!KNOWN_FIELDS.has(key)) {
|
|
38
35
|
throw new QuiverError('quiver_invalid', `Quiver.yaml: unknown field "${key}" — only "name" and "description" are valid in V1`);
|
|
39
36
|
}
|
|
40
37
|
}
|
|
41
|
-
// Validate `name`
|
|
42
38
|
if (!('name' in doc)) {
|
|
43
39
|
throw new QuiverError('quiver_invalid', `Quiver.yaml: required field "name" is missing`);
|
|
44
40
|
}
|
|
@@ -49,7 +45,6 @@ export function parseQuiverYaml(raw) {
|
|
|
49
45
|
if (!NAME_RE.test(name)) {
|
|
50
46
|
throw new QuiverError('quiver_invalid', `Quiver.yaml: "name" value "${name}" contains invalid characters — only [A-Za-z0-9_-] are allowed`);
|
|
51
47
|
}
|
|
52
|
-
// Validate optional `description`
|
|
53
48
|
if ('description' in doc && doc['description'] !== undefined) {
|
|
54
49
|
if (typeof doc['description'] !== 'string') {
|
|
55
50
|
throw new QuiverError('quiver_invalid', `Quiver.yaml: "description" must be a string if present, got ${typeof doc['description']}`);
|
package/dist/quiver.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* A collection of quills, addressed by ref. One class over a composed
|
|
3
|
+
* `QuiverLoader`, source-backed or built, so what a quiver reads is the loader's
|
|
4
|
+
* business and never the class's.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* This module is browser-safe: only `fromBuiltUrl` and the instance API live
|
|
8
|
-
* here. The filesystem factories are free functions in `./node.js`; the class
|
|
9
|
-
* is the same either way.
|
|
6
|
+
* Browser-safe: only `fromBuiltUrl`, `fromBuiltFiles` and the instance API live
|
|
7
|
+
* here. The filesystem factories are free functions in `./node.js`; the class is
|
|
8
|
+
* the same either way.
|
|
10
9
|
*/
|
|
11
|
-
import { Quill } from '@quillmark/wasm';
|
|
10
|
+
import type { Quill } from '@quillmark/wasm';
|
|
12
11
|
/** Loader strategy: source or build output. Package-internal. */
|
|
13
12
|
export interface QuiverLoader {
|
|
14
13
|
loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
|
|
@@ -24,8 +23,8 @@ export declare class Quiver {
|
|
|
24
23
|
readonly name: string;
|
|
25
24
|
/**
|
|
26
25
|
* Private constructor. A Quiver comes from a factory (`Quiver.fromBuiltUrl`,
|
|
27
|
-
* or `fromDir` / `fromBuiltDir` from
|
|
28
|
-
* names the thing being read.
|
|
26
|
+
* `Quiver.fromBuiltFiles`, or `fromDir` / `fromBuiltDir` from
|
|
27
|
+
* `@quillmark/quiver/node`), which is what names the thing being read.
|
|
29
28
|
*/
|
|
30
29
|
private constructor();
|
|
31
30
|
/**
|
|
@@ -35,10 +34,37 @@ export declare class Quiver {
|
|
|
35
34
|
* environments. `file://` URLs are rejected — to load build output from
|
|
36
35
|
* disk in Node, use `fromBuiltDir(path)` from `@quillmark/quiver/node`.
|
|
37
36
|
*
|
|
37
|
+
* `seed` answers for the artifact bytes the caller already holds, keyed by
|
|
38
|
+
* artifact-relative path; the URL serves the rest. Seeding `latest.json`
|
|
39
|
+
* settles which catalog this process reads at deploy time rather than at
|
|
40
|
+
* cache-revalidation time. Seeded bytes are digest-checked as fetched ones
|
|
41
|
+
* are.
|
|
42
|
+
*
|
|
38
43
|
* Throws `transport_error` on network/HTTP failure, `quiver_invalid`
|
|
39
44
|
* on format errors.
|
|
40
45
|
*/
|
|
41
|
-
static fromBuiltUrl(url: string
|
|
46
|
+
static fromBuiltUrl(url: string, opts?: {
|
|
47
|
+
seed?: ReadonlyMap<string, Uint8Array>;
|
|
48
|
+
}): Promise<Quiver>;
|
|
49
|
+
/**
|
|
50
|
+
* Browser-safe factory. Loads build output from the bytes themselves, keyed
|
|
51
|
+
* by artifact-relative path (`latest.json`, `manifest.<digest>.json`,
|
|
52
|
+
* `<name>@<x.y.z>.<digest>.zip`, `store/<hash>`) — the shape `build` writes
|
|
53
|
+
* and `fromBuiltDir` reads back.
|
|
54
|
+
*
|
|
55
|
+
* Nothing is fetched, so a runtime whose artifact is not on a path it can
|
|
56
|
+
* read (a serverless function, a bundler that inlines it, a test) reaches
|
|
57
|
+
* one without fetching its own static output back over its own load
|
|
58
|
+
* balancer.
|
|
59
|
+
*
|
|
60
|
+
* The map must carry the whole artifact; a missing path is a
|
|
61
|
+
* `transport_error` naming it. To hold part and fetch the rest, pass the map
|
|
62
|
+
* as `fromBuiltUrl`'s `seed`.
|
|
63
|
+
*
|
|
64
|
+
* Throws `quiver_invalid` on format errors, `transport_error` on a path the
|
|
65
|
+
* map does not carry.
|
|
66
|
+
*/
|
|
67
|
+
static fromBuiltFiles(files: ReadonlyMap<string, Uint8Array>): Promise<Quiver>;
|
|
42
68
|
/** Returns all known quill names, sorted lexicographically. */
|
|
43
69
|
quillNames(): string[];
|
|
44
70
|
/**
|
|
@@ -74,7 +100,7 @@ export declare class Quiver {
|
|
|
74
100
|
* first. Materializes once and caches per canonical ref — concurrent calls
|
|
75
101
|
* coalesce into a single load.
|
|
76
102
|
*
|
|
77
|
-
*
|
|
103
|
+
* Borrowed, not handed over: the quill is the quiver's, shared with every
|
|
78
104
|
* other caller for the same ref, and lives as long as the quiver. Do not
|
|
79
105
|
* `free()` it: the next `getQuill` for that ref would hand out a freed
|
|
80
106
|
* handle. A caller wanting one of its own mints it from `.toTree()`.
|
package/dist/quiver.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* A collection of quills, addressed by ref. One class over a composed
|
|
3
|
+
* `QuiverLoader`, source-backed or built, so what a quiver reads is the loader's
|
|
4
|
+
* business and never the class's.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* This module is browser-safe: only `fromBuiltUrl` and the instance API live
|
|
8
|
-
* here. The filesystem factories are free functions in `./node.js`; the class
|
|
9
|
-
* is the same either way.
|
|
6
|
+
* Browser-safe: only `fromBuiltUrl`, `fromBuiltFiles` and the instance API live
|
|
7
|
+
* here. The filesystem factories are free functions in `./node.js`; the class is
|
|
8
|
+
* the same either way.
|
|
10
9
|
*/
|
|
11
10
|
import { QuiverError } from './errors.js';
|
|
12
|
-
import {
|
|
11
|
+
import { init } from '@quillmark/wasm';
|
|
13
12
|
import { parseQuillRef } from './ref.js';
|
|
14
13
|
import { matchesSemverSelector, chooseHighestVersion } from './semver.js';
|
|
15
14
|
/**
|
|
@@ -23,7 +22,7 @@ export class Quiver {
|
|
|
23
22
|
#catalog;
|
|
24
23
|
#loader;
|
|
25
24
|
/**
|
|
26
|
-
* Cache of materialized quills, keyed by canonical ref. A `Quill` is
|
|
25
|
+
* Cache of materialized quills, keyed by canonical ref. A `Quill` is
|
|
27
26
|
* engine-free, portable data (`Quill.fromTree`), so one instance per ref is
|
|
28
27
|
* shared across every engine. Promise values so concurrent getQuill calls
|
|
29
28
|
* coalesce into a single materialization.
|
|
@@ -31,8 +30,8 @@ export class Quiver {
|
|
|
31
30
|
#quillCache = new Map();
|
|
32
31
|
/**
|
|
33
32
|
* Private constructor. A Quiver comes from a factory (`Quiver.fromBuiltUrl`,
|
|
34
|
-
* or `fromDir` / `fromBuiltDir` from
|
|
35
|
-
* names the thing being read.
|
|
33
|
+
* `Quiver.fromBuiltFiles`, or `fromDir` / `fromBuiltDir` from
|
|
34
|
+
* `@quillmark/quiver/node`), which is what names the thing being read.
|
|
36
35
|
*/
|
|
37
36
|
constructor(name, catalog, loader) {
|
|
38
37
|
this.name = name;
|
|
@@ -49,17 +48,49 @@ export class Quiver {
|
|
|
49
48
|
* environments. `file://` URLs are rejected — to load build output from
|
|
50
49
|
* disk in Node, use `fromBuiltDir(path)` from `@quillmark/quiver/node`.
|
|
51
50
|
*
|
|
51
|
+
* `seed` answers for the artifact bytes the caller already holds, keyed by
|
|
52
|
+
* artifact-relative path; the URL serves the rest. Seeding `latest.json`
|
|
53
|
+
* settles which catalog this process reads at deploy time rather than at
|
|
54
|
+
* cache-revalidation time. Seeded bytes are digest-checked as fetched ones
|
|
55
|
+
* are.
|
|
56
|
+
*
|
|
52
57
|
* Throws `transport_error` on network/HTTP failure, `quiver_invalid`
|
|
53
58
|
* on format errors.
|
|
54
59
|
*/
|
|
55
|
-
static async fromBuiltUrl(url) {
|
|
60
|
+
static async fromBuiltUrl(url, opts) {
|
|
56
61
|
if (url.startsWith('file://')) {
|
|
57
62
|
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
63
|
}
|
|
59
64
|
const { HttpTransport } = await import('./transports/http-transport.js');
|
|
60
65
|
const { loadBuiltQuiver } = await import('./built-loader.js');
|
|
61
|
-
const
|
|
62
|
-
|
|
66
|
+
const http = new HttpTransport(url);
|
|
67
|
+
if (opts?.seed === undefined)
|
|
68
|
+
return loadBuiltQuiver(http);
|
|
69
|
+
const { MemoryTransport } = await import('./transports/memory-transport.js');
|
|
70
|
+
return loadBuiltQuiver(new MemoryTransport(opts.seed, http));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Browser-safe factory. Loads build output from the bytes themselves, keyed
|
|
74
|
+
* by artifact-relative path (`latest.json`, `manifest.<digest>.json`,
|
|
75
|
+
* `<name>@<x.y.z>.<digest>.zip`, `store/<hash>`) — the shape `build` writes
|
|
76
|
+
* and `fromBuiltDir` reads back.
|
|
77
|
+
*
|
|
78
|
+
* Nothing is fetched, so a runtime whose artifact is not on a path it can
|
|
79
|
+
* read (a serverless function, a bundler that inlines it, a test) reaches
|
|
80
|
+
* one without fetching its own static output back over its own load
|
|
81
|
+
* balancer.
|
|
82
|
+
*
|
|
83
|
+
* The map must carry the whole artifact; a missing path is a
|
|
84
|
+
* `transport_error` naming it. To hold part and fetch the rest, pass the map
|
|
85
|
+
* as `fromBuiltUrl`'s `seed`.
|
|
86
|
+
*
|
|
87
|
+
* Throws `quiver_invalid` on format errors, `transport_error` on a path the
|
|
88
|
+
* map does not carry.
|
|
89
|
+
*/
|
|
90
|
+
static async fromBuiltFiles(files) {
|
|
91
|
+
const { MemoryTransport } = await import('./transports/memory-transport.js');
|
|
92
|
+
const { loadBuiltQuiver } = await import('./built-loader.js');
|
|
93
|
+
return loadBuiltQuiver(new MemoryTransport(files));
|
|
63
94
|
}
|
|
64
95
|
/** Returns all known quill names, sorted lexicographically. */
|
|
65
96
|
quillNames() {
|
|
@@ -114,7 +145,7 @@ export class Quiver {
|
|
|
114
145
|
* first. Materializes once and caches per canonical ref — concurrent calls
|
|
115
146
|
* coalesce into a single load.
|
|
116
147
|
*
|
|
117
|
-
*
|
|
148
|
+
* Borrowed, not handed over: the quill is the quiver's, shared with every
|
|
118
149
|
* other caller for the same ref, and lives as long as the quiver. Do not
|
|
119
150
|
* `free()` it: the next `getQuill` for that ref would hand out a freed
|
|
120
151
|
* handle. A caller wanting one of its own mints it from `.toTree()`.
|
|
@@ -147,10 +178,17 @@ export class Quiver {
|
|
|
147
178
|
*
|
|
148
179
|
* Every caller arrives through `resolve` or the catalog itself, so the ref is
|
|
149
180
|
* catalog-backed by construction and the loader needs no gate.
|
|
181
|
+
*
|
|
182
|
+
* `init()` is the only door to `Quill`, awaited here rather than left a
|
|
183
|
+
* precondition of `getQuill`. The gate is memoized, so this is one instantiation
|
|
184
|
+
* across every caller, overlapped with the load that pays for it.
|
|
150
185
|
*/
|
|
151
186
|
async #materializeQuill(canonicalRef) {
|
|
152
187
|
const at = canonicalRef.indexOf('@');
|
|
153
|
-
const tree = await
|
|
188
|
+
const [{ Quill }, tree] = await Promise.all([
|
|
189
|
+
init(),
|
|
190
|
+
this.#loader.loadTree(canonicalRef.slice(0, at), canonicalRef.slice(at + 1))
|
|
191
|
+
]);
|
|
154
192
|
return Quill.fromTree(tree);
|
|
155
193
|
}
|
|
156
194
|
}
|
package/dist/ref.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Internal parsed representation of a quill reference. */
|
|
2
2
|
export interface ParsedQuillRef {
|
|
3
3
|
name: string;
|
|
4
|
-
/** Undefined
|
|
4
|
+
/** Undefined selects the highest version present. */
|
|
5
5
|
selector?: string;
|
|
6
6
|
/** Selector part count: 1 = `x`, 2 = `x.y`, 3 = `x.y.z` (exact). */
|
|
7
7
|
selectorDepth?: 1 | 2 | 3;
|
package/dist/ref.js
CHANGED
|
@@ -12,18 +12,15 @@ export function parseQuillRef(ref) {
|
|
|
12
12
|
}
|
|
13
13
|
const atIndex = ref.indexOf('@');
|
|
14
14
|
if (atIndex === 0) {
|
|
15
|
-
// Starts with @, no name
|
|
16
15
|
throw new QuiverError('invalid_ref', `Invalid ref: missing name in "${ref}"`, { ref });
|
|
17
16
|
}
|
|
18
17
|
if (atIndex === -1) {
|
|
19
|
-
// No selector — just a name
|
|
20
18
|
const name = ref;
|
|
21
19
|
if (!NAME_RE.test(name)) {
|
|
22
20
|
throw new QuiverError('invalid_ref', `Invalid ref: name "${name}" contains invalid characters`, { ref });
|
|
23
21
|
}
|
|
24
22
|
return { name };
|
|
25
23
|
}
|
|
26
|
-
// Has @
|
|
27
24
|
const name = ref.slice(0, atIndex);
|
|
28
25
|
const selector = ref.slice(atIndex + 1);
|
|
29
26
|
if (!name) {
|
package/dist/source-loader.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Internal filesystem scanner for Source Quiver layout.
|
|
3
|
-
*
|
|
4
|
-
* Uses Node.js `fs/promises` — this module must only be imported from
|
|
5
|
-
* Node-only contexts (`fromDir`, `build`).
|
|
2
|
+
* Internal filesystem scanner for the Source Quiver layout. Statically imports
|
|
3
|
+
* `node:fs`, so it is reachable from the Node entry alone.
|
|
6
4
|
*/
|
|
7
5
|
import type { QuiverMeta } from './quiver-yaml.js';
|
|
8
6
|
import type { QuiverLoader } from './quiver.js';
|
|
@@ -17,7 +15,7 @@ import type { QuiverLoader } from './quiver.js';
|
|
|
17
15
|
* non-canonical, or a version dir is missing its Quill.yaml sentinel.
|
|
18
16
|
* - `transport_error` for I/O failures (permissions, etc.).
|
|
19
17
|
*
|
|
20
|
-
* Missing `quills/` directory is
|
|
18
|
+
* Missing `quills/` directory is not an error — the quiver is valid but empty.
|
|
21
19
|
*/
|
|
22
20
|
export declare function scanSourceQuiver(rootDir: string): Promise<{
|
|
23
21
|
meta: QuiverMeta;
|
|
@@ -32,8 +30,8 @@ export declare function scanSourceQuiver(rootDir: string): Promise<{
|
|
|
32
30
|
export declare function readQuillTree(quillDir: string): Promise<Map<string, Uint8Array>>;
|
|
33
31
|
/**
|
|
34
32
|
* Source-backed QuiverLoader: loads file trees from a Source Quiver on disk.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
33
|
+
* `Quiver` resolves every ref against its catalog before reaching a loader, so
|
|
34
|
+
* the name and version arrive catalog-backed and go straight to the disk.
|
|
37
35
|
*/
|
|
38
36
|
export declare class SourceLoader implements QuiverLoader {
|
|
39
37
|
private readonly rootDir;
|
package/dist/source-loader.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Internal filesystem scanner for Source Quiver layout.
|
|
3
|
-
*
|
|
4
|
-
* Uses Node.js `fs/promises` — this module must only be imported from
|
|
5
|
-
* Node-only contexts (`fromDir`, `build`).
|
|
2
|
+
* Internal filesystem scanner for the Source Quiver layout. Statically imports
|
|
3
|
+
* `node:fs`, so it is reachable from the Node entry alone.
|
|
6
4
|
*/
|
|
7
5
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
8
6
|
import { join, relative, sep } from 'node:path';
|
|
@@ -20,10 +18,9 @@ import { isCanonicalSemver, compareSemver } from './semver.js';
|
|
|
20
18
|
* non-canonical, or a version dir is missing its Quill.yaml sentinel.
|
|
21
19
|
* - `transport_error` for I/O failures (permissions, etc.).
|
|
22
20
|
*
|
|
23
|
-
* Missing `quills/` directory is
|
|
21
|
+
* Missing `quills/` directory is not an error — the quiver is valid but empty.
|
|
24
22
|
*/
|
|
25
23
|
export async function scanSourceQuiver(rootDir) {
|
|
26
|
-
// --- Read Quiver.yaml ---
|
|
27
24
|
const quiverYamlPath = join(rootDir, 'Quiver.yaml');
|
|
28
25
|
let raw;
|
|
29
26
|
try {
|
|
@@ -42,7 +39,6 @@ export async function scanSourceQuiver(rootDir) {
|
|
|
42
39
|
throw new QuiverError('transport_error', `Failed to read "Quiver.yaml" at "${quiverYamlPath}": ${err.message}`, { cause: err });
|
|
43
40
|
}
|
|
44
41
|
const meta = parseQuiverYaml(raw);
|
|
45
|
-
// --- Walk quills/ directory ---
|
|
46
42
|
const quillsDir = join(rootDir, 'quills');
|
|
47
43
|
let quillNames;
|
|
48
44
|
try {
|
|
@@ -51,7 +47,6 @@ export async function scanSourceQuiver(rootDir) {
|
|
|
51
47
|
catch (err) {
|
|
52
48
|
const code = err.code;
|
|
53
49
|
if (code === 'ENOENT') {
|
|
54
|
-
// Missing quills/ is fine — empty catalog
|
|
55
50
|
return { meta, catalog: new Map() };
|
|
56
51
|
}
|
|
57
52
|
throw new QuiverError('transport_error', `Failed to read "quills/" directory at "${quillsDir}": ${err.message}`, { cause: err });
|
|
@@ -59,7 +54,6 @@ export async function scanSourceQuiver(rootDir) {
|
|
|
59
54
|
const catalog = new Map();
|
|
60
55
|
for (const quillName of quillNames) {
|
|
61
56
|
const quillNameDir = join(quillsDir, quillName);
|
|
62
|
-
// Ensure it's a directory
|
|
63
57
|
let st;
|
|
64
58
|
try {
|
|
65
59
|
st = await stat(quillNameDir);
|
|
@@ -69,7 +63,6 @@ export async function scanSourceQuiver(rootDir) {
|
|
|
69
63
|
}
|
|
70
64
|
if (!st.isDirectory())
|
|
71
65
|
continue;
|
|
72
|
-
// Read version directories
|
|
73
66
|
let versionDirs;
|
|
74
67
|
try {
|
|
75
68
|
versionDirs = await readdir(quillNameDir);
|
|
@@ -80,7 +73,6 @@ export async function scanSourceQuiver(rootDir) {
|
|
|
80
73
|
const versions = [];
|
|
81
74
|
for (const versionDir of versionDirs) {
|
|
82
75
|
const versionPath = join(quillNameDir, versionDir);
|
|
83
|
-
// Ensure it's a directory
|
|
84
76
|
let vst;
|
|
85
77
|
try {
|
|
86
78
|
vst = await stat(versionPath);
|
|
@@ -90,11 +82,9 @@ export async function scanSourceQuiver(rootDir) {
|
|
|
90
82
|
}
|
|
91
83
|
if (!vst.isDirectory())
|
|
92
84
|
continue;
|
|
93
|
-
// Non-canonical version → quiver_invalid
|
|
94
85
|
if (!isCanonicalSemver(versionDir)) {
|
|
95
86
|
throw new QuiverError('quiver_invalid', `Quill "${quillName}" has non-canonical version directory "${versionDir}" — only x.y.z format is allowed`, { quiverName: meta.name, version: versionDir });
|
|
96
87
|
}
|
|
97
|
-
// Require Quill.yaml sentinel inside the version dir
|
|
98
88
|
const quillYamlPath = join(versionPath, 'Quill.yaml');
|
|
99
89
|
try {
|
|
100
90
|
await stat(quillYamlPath);
|
|
@@ -109,7 +99,6 @@ export async function scanSourceQuiver(rootDir) {
|
|
|
109
99
|
versions.push(versionDir);
|
|
110
100
|
}
|
|
111
101
|
if (versions.length > 0) {
|
|
112
|
-
// Sort descending
|
|
113
102
|
versions.sort((a, b) => compareSemver(b, a));
|
|
114
103
|
catalog.set(quillName, versions);
|
|
115
104
|
}
|
|
@@ -148,7 +137,6 @@ async function walkDir(baseDir, currentDir, tree) {
|
|
|
148
137
|
await walkDir(baseDir, fullPath, tree);
|
|
149
138
|
}
|
|
150
139
|
else {
|
|
151
|
-
// Compute relative POSIX path
|
|
152
140
|
const rel = relative(baseDir, fullPath);
|
|
153
141
|
const posixRel = sep === '/' ? rel : rel.split(sep).join('/');
|
|
154
142
|
let bytes;
|
|
@@ -164,8 +152,8 @@ async function walkDir(baseDir, currentDir, tree) {
|
|
|
164
152
|
}
|
|
165
153
|
/**
|
|
166
154
|
* Source-backed QuiverLoader: loads file trees from a Source Quiver on disk.
|
|
167
|
-
*
|
|
168
|
-
*
|
|
155
|
+
* `Quiver` resolves every ref against its catalog before reaching a loader, so
|
|
156
|
+
* the name and version arrive catalog-backed and go straight to the disk.
|
|
169
157
|
*/
|
|
170
158
|
export class SourceLoader {
|
|
171
159
|
rootDir;
|
|
@@ -8,11 +8,9 @@ import { QuiverError } from '../errors.js';
|
|
|
8
8
|
export class HttpTransport {
|
|
9
9
|
baseUrl;
|
|
10
10
|
constructor(baseUrl) {
|
|
11
|
-
// Normalize: ensure exactly one trailing slash.
|
|
12
11
|
this.baseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
|
|
13
12
|
}
|
|
14
13
|
async fetchBytes(relativePath, opts) {
|
|
15
|
-
// Strip any leading slash from relativePath to avoid double slashes.
|
|
16
14
|
const cleanPath = relativePath.startsWith('/') ? relativePath.slice(1) : relativePath;
|
|
17
15
|
const url = `${this.baseUrl}${cleanPath}`;
|
|
18
16
|
let response;
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/quiver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "A quiver of Quillmark quills: load one, pack one, hand out quills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -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",
|