@quillmark/quiver 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROGRAM.md +202 -123
- package/README.md +88 -35
- package/dist/build.d.ts +25 -0
- package/dist/{pack.js → build.js} +7 -7
- package/dist/{packed-loader.d.ts → built-loader.d.ts} +8 -8
- package/dist/{packed-loader.js → built-loader.js} +10 -10
- package/dist/engine-types.d.ts +1 -1
- package/dist/engine-types.js +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +0 -1
- package/dist/node.js +1 -1
- package/dist/quiver.d.ts +70 -23
- package/dist/quiver.js +180 -36
- package/dist/source-loader.d.ts +1 -1
- package/dist/source-loader.js +1 -1
- package/dist/testing.d.ts +14 -25
- package/dist/testing.js +31 -88
- package/dist/transports/http-transport.d.ts +3 -3
- package/dist/transports/http-transport.js +1 -1
- package/package.json +4 -10
- package/dist/pack.d.ts +0 -25
- package/dist/registry.d.ts +0 -39
- package/dist/registry.js +0 -115
- package/dist/transports/fs-transport.d.ts +0 -14
- package/dist/transports/fs-transport.js +0 -33
package/dist/quiver.js
CHANGED
|
@@ -2,14 +2,28 @@
|
|
|
2
2
|
* Quiver — primary runtime abstraction for a collection of quills.
|
|
3
3
|
*
|
|
4
4
|
* Polymorphism via composition: internally stores a pluggable loader
|
|
5
|
-
* (either source-backed or
|
|
5
|
+
* (either source-backed or build-output-backed).
|
|
6
6
|
*/
|
|
7
7
|
import { QuiverError } from "./errors.js";
|
|
8
8
|
import { assertNode } from "./assert-node.js";
|
|
9
|
+
import { parseQuillRef } from "./ref.js";
|
|
10
|
+
import { matchesSemverSelector, chooseHighestVersion } from "./semver.js";
|
|
9
11
|
export class Quiver {
|
|
10
12
|
name;
|
|
11
13
|
#catalog;
|
|
12
14
|
#loader;
|
|
15
|
+
/**
|
|
16
|
+
* Per-engine cache of materialized quills, keyed by canonical ref.
|
|
17
|
+
* WeakMap so engines can be GC'd; Promise values so concurrent
|
|
18
|
+
* getQuill calls coalesce into a single materialization.
|
|
19
|
+
*/
|
|
20
|
+
#quillCache = new WeakMap();
|
|
21
|
+
/**
|
|
22
|
+
* Engine-independent cache of fetched trees, keyed by canonical ref.
|
|
23
|
+
* Populated by `warm()` and on first `getQuill` for a ref. Promise
|
|
24
|
+
* values so concurrent fetches coalesce.
|
|
25
|
+
*/
|
|
26
|
+
#treeCache = new Map();
|
|
13
27
|
/**
|
|
14
28
|
* Private constructor — use static factory methods.
|
|
15
29
|
* TS prevents external `new Quiver(...)` at compile time.
|
|
@@ -20,51 +34,73 @@ export class Quiver {
|
|
|
20
34
|
this.#catalog = new Map(catalog);
|
|
21
35
|
this.#loader = loader;
|
|
22
36
|
}
|
|
23
|
-
/** @internal Used by
|
|
37
|
+
/** @internal Used by loadBuiltQuiver. Not part of the public API. */
|
|
24
38
|
static _fromLoader(name, catalog, loader) {
|
|
25
39
|
return new Quiver(name, catalog, loader);
|
|
26
40
|
}
|
|
27
41
|
/**
|
|
28
|
-
* Node-only factory.
|
|
29
|
-
*
|
|
42
|
+
* Node-only factory. Resolves an npm specifier against `node_modules` and
|
|
43
|
+
* loads the source layout at the package root.
|
|
30
44
|
*
|
|
31
|
-
*
|
|
32
|
-
* in a browser environment does not cause a crash at module evaluation time.
|
|
45
|
+
* The resolved package must have `Quiver.yaml` at its root.
|
|
33
46
|
*
|
|
34
|
-
* Throws `
|
|
47
|
+
* Throws `transport_error` on resolution/I/O failure, `quiver_invalid`
|
|
48
|
+
* on schema violations.
|
|
35
49
|
*/
|
|
36
|
-
static async
|
|
37
|
-
assertNode("Quiver.
|
|
38
|
-
const {
|
|
39
|
-
const {
|
|
40
|
-
const
|
|
41
|
-
|
|
50
|
+
static async fromPackage(specifier) {
|
|
51
|
+
assertNode("Quiver.fromPackage");
|
|
52
|
+
const { createRequire } = await import("node:module");
|
|
53
|
+
const { dirname } = await import("node:path");
|
|
54
|
+
const req = createRequire(import.meta.url);
|
|
55
|
+
let yamlPath;
|
|
56
|
+
try {
|
|
57
|
+
yamlPath = req.resolve(`${specifier}/Quiver.yaml`);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new QuiverError("transport_error", `Failed to resolve quiver package "${specifier}": ${err.message}`, { cause: err });
|
|
61
|
+
}
|
|
62
|
+
return Quiver.fromDir(dirname(yamlPath));
|
|
42
63
|
}
|
|
43
64
|
/**
|
|
44
|
-
* Node-only factory.
|
|
65
|
+
* Node-only factory. Reads a Source Quiver from a local directory containing
|
|
66
|
+
* `Quiver.yaml` and `quills/<name>/<version>/Quill.yaml` entries.
|
|
45
67
|
*
|
|
46
|
-
*
|
|
68
|
+
* Also accepts `import.meta.url`-style `file://` URLs as a convenience for
|
|
69
|
+
* tests; the URL's parent directory is used as the source root.
|
|
47
70
|
*
|
|
48
|
-
* Throws `
|
|
71
|
+
* Throws `quiver_invalid` on schema violations, `transport_error` on I/O failure.
|
|
49
72
|
*/
|
|
50
|
-
static async
|
|
51
|
-
assertNode("Quiver.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
static async fromDir(pathOrFileUrl) {
|
|
74
|
+
assertNode("Quiver.fromDir");
|
|
75
|
+
let dir = pathOrFileUrl;
|
|
76
|
+
if (pathOrFileUrl.startsWith("file://")) {
|
|
77
|
+
const { fileURLToPath } = await import("node:url");
|
|
78
|
+
dir = fileURLToPath(new URL(".", pathOrFileUrl));
|
|
79
|
+
}
|
|
80
|
+
const { scanSourceQuiver, SourceLoader } = await import("./source-loader.js");
|
|
81
|
+
const { meta, catalog } = await scanSourceQuiver(dir);
|
|
82
|
+
const loader = new SourceLoader(dir);
|
|
83
|
+
return new Quiver(meta.name, catalog, loader);
|
|
56
84
|
}
|
|
57
85
|
/**
|
|
58
|
-
* Browser-safe factory. Loads
|
|
86
|
+
* Browser-safe factory. Loads build output from an HTTP/HTTPS URL.
|
|
87
|
+
*
|
|
88
|
+
* Origin-relative URLs (e.g. `/quivers/foo/`) are accepted in browser
|
|
89
|
+
* environments. `file://` URLs are rejected — local build output is
|
|
90
|
+
* not loadable in V1; serve over HTTP or use `fromPackage`/`fromDir`
|
|
91
|
+
* against the source.
|
|
59
92
|
*
|
|
60
|
-
* Throws `transport_error` on network/HTTP failure, `quiver_invalid`
|
|
61
|
-
* format errors.
|
|
93
|
+
* Throws `transport_error` on network/HTTP failure, `quiver_invalid`
|
|
94
|
+
* on format errors.
|
|
62
95
|
*/
|
|
63
|
-
static async
|
|
96
|
+
static async fromBuilt(url) {
|
|
97
|
+
if (url.startsWith("file://")) {
|
|
98
|
+
throw new QuiverError("transport_error", `Quiver.fromBuilt requires an http(s):// or origin-relative URL; got "${url}". Local build output is not loadable in V1 — serve it over HTTP or load source via fromPackage/fromDir.`);
|
|
99
|
+
}
|
|
64
100
|
const { HttpTransport } = await import("./transports/http-transport.js");
|
|
65
|
-
const {
|
|
101
|
+
const { loadBuiltQuiver } = await import("./built-loader.js");
|
|
66
102
|
const transport = new HttpTransport(url);
|
|
67
|
-
return
|
|
103
|
+
return loadBuiltQuiver(transport);
|
|
68
104
|
}
|
|
69
105
|
/** Returns all known quill names, sorted lexicographically. */
|
|
70
106
|
quillNames() {
|
|
@@ -78,24 +114,26 @@ export class Quiver {
|
|
|
78
114
|
return [...(this.#catalog.get(name) ?? [])];
|
|
79
115
|
}
|
|
80
116
|
/**
|
|
81
|
-
* Node-only tooling.
|
|
117
|
+
* Node-only tooling. Reads the Source Quiver at sourceDir, validates it,
|
|
118
|
+
* and writes the runtime build artifact to outDir.
|
|
82
119
|
*
|
|
83
|
-
* Uses dynamic import of `./
|
|
84
|
-
* at evaluation time.
|
|
120
|
+
* Uses dynamic import of `./build.js` so that this module stays
|
|
121
|
+
* browser-safe at evaluation time.
|
|
85
122
|
*
|
|
86
123
|
* Throws `quiver_invalid` on source validation failures,
|
|
87
124
|
* `transport_error` on I/O failures.
|
|
88
125
|
*/
|
|
89
|
-
static async
|
|
90
|
-
assertNode("Quiver.
|
|
91
|
-
const {
|
|
92
|
-
return
|
|
126
|
+
static async build(sourceDir, outDir, opts) {
|
|
127
|
+
assertNode("Quiver.build");
|
|
128
|
+
const { buildQuiver } = await import("./build.js");
|
|
129
|
+
return buildQuiver(sourceDir, outDir, opts);
|
|
93
130
|
}
|
|
94
131
|
/**
|
|
95
132
|
* Lazily loads the file tree for a specific quill version.
|
|
96
133
|
*
|
|
97
134
|
* Returns `Map<string, Uint8Array>` suitable for `engine.quill(tree)`.
|
|
98
|
-
* Does NOT cache the result — caching
|
|
135
|
+
* Does NOT cache the result — caching of materialized Quill instances
|
|
136
|
+
* happens in `getQuill`.
|
|
99
137
|
*
|
|
100
138
|
* Throws `transport_error` if name/version not in catalog or I/O fails.
|
|
101
139
|
*/
|
|
@@ -106,4 +144,110 @@ export class Quiver {
|
|
|
106
144
|
}
|
|
107
145
|
return this.#loader.loadTree(name, version);
|
|
108
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
|
|
149
|
+
*
|
|
150
|
+
* Selector forms: `name`, `name@x`, `name@x.y`, `name@x.y.z`. Picks the
|
|
151
|
+
* highest matching version in this quiver.
|
|
152
|
+
*
|
|
153
|
+
* Throws:
|
|
154
|
+
* - `invalid_ref` if ref fails parseQuillRef
|
|
155
|
+
* - `quill_not_found` if no version matches
|
|
156
|
+
*/
|
|
157
|
+
async resolve(ref) {
|
|
158
|
+
const parsed = parseQuillRef(ref);
|
|
159
|
+
const versions = this.#catalog.get(parsed.name);
|
|
160
|
+
if (versions && versions.length > 0) {
|
|
161
|
+
const candidates = parsed.selector === undefined
|
|
162
|
+
? [...versions]
|
|
163
|
+
: versions.filter((v) => matchesSemverSelector(v, parsed.selector));
|
|
164
|
+
if (candidates.length > 0) {
|
|
165
|
+
// chooseHighestVersion returns null only for empty arrays; candidates is non-empty.
|
|
166
|
+
const winner = chooseHighestVersion(candidates);
|
|
167
|
+
return `${parsed.name}@${winner}`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw new QuiverError("quill_not_found", `No quill found for ref "${ref}" in quiver "${this.name}".`, { ref, quiverName: this.name });
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Returns a render-ready `Quill` for a ref (selector or canonical).
|
|
174
|
+
*
|
|
175
|
+
* Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical
|
|
176
|
+
* form first. Materializes via `engine.quill(tree)` on first call;
|
|
177
|
+
* caches per (engine, canonical-ref). Reuses a tree cached by `warm()`
|
|
178
|
+
* (or a previous `getQuill`) so the network fetch isn't paid twice.
|
|
179
|
+
* Concurrent calls for the same ref coalesce into a single load.
|
|
180
|
+
*
|
|
181
|
+
* Throws:
|
|
182
|
+
* - `invalid_ref` if ref is malformed
|
|
183
|
+
* - `quill_not_found` if ref does not match any version in this quiver
|
|
184
|
+
* - propagates I/O errors from loadTree unchanged
|
|
185
|
+
* - propagates engine errors from engine.quill() unchanged
|
|
186
|
+
*/
|
|
187
|
+
async getQuill(ref, opts) {
|
|
188
|
+
const canonicalRef = await this.resolve(ref);
|
|
189
|
+
const engine = opts.engine;
|
|
190
|
+
let perEngine = this.#quillCache.get(engine);
|
|
191
|
+
if (perEngine === undefined) {
|
|
192
|
+
perEngine = new Map();
|
|
193
|
+
this.#quillCache.set(engine, perEngine);
|
|
194
|
+
}
|
|
195
|
+
let entry = perEngine.get(canonicalRef);
|
|
196
|
+
if (entry === undefined) {
|
|
197
|
+
entry = this.#materializeQuill(canonicalRef, engine).catch((err) => {
|
|
198
|
+
perEngine.delete(canonicalRef);
|
|
199
|
+
throw err;
|
|
200
|
+
});
|
|
201
|
+
perEngine.set(canonicalRef, entry);
|
|
202
|
+
}
|
|
203
|
+
return entry;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Internal: load tree (cached) + invoke engine.quill. Errors propagate.
|
|
207
|
+
*
|
|
208
|
+
* On success, evicts the tree from the cache so its bytes can be GC'd —
|
|
209
|
+
* the materialized Quill is the runtime artifact; the tree is dead weight
|
|
210
|
+
* once a Quill exists. On failure, the tree is retained so retries skip
|
|
211
|
+
* the network.
|
|
212
|
+
*/
|
|
213
|
+
async #materializeQuill(canonicalRef, engine) {
|
|
214
|
+
const tree = await this.#getTreeCached(canonicalRef);
|
|
215
|
+
const quill = engine.quill(tree);
|
|
216
|
+
this.#treeCache.delete(canonicalRef);
|
|
217
|
+
return quill;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Internal: tree cache reader. On miss, fetches via `loadTree` and stores
|
|
221
|
+
* the in-flight Promise. On rejection, evicts so a retry can succeed.
|
|
222
|
+
*/
|
|
223
|
+
async #getTreeCached(canonicalRef) {
|
|
224
|
+
let entry = this.#treeCache.get(canonicalRef);
|
|
225
|
+
if (entry === undefined) {
|
|
226
|
+
const at = canonicalRef.indexOf("@");
|
|
227
|
+
const name = canonicalRef.slice(0, at);
|
|
228
|
+
const version = canonicalRef.slice(at + 1);
|
|
229
|
+
entry = this.loadTree(name, version).catch((err) => {
|
|
230
|
+
this.#treeCache.delete(canonicalRef);
|
|
231
|
+
throw err;
|
|
232
|
+
});
|
|
233
|
+
this.#treeCache.set(canonicalRef, entry);
|
|
234
|
+
}
|
|
235
|
+
return entry;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Prefetches the tree for every quill version in this quiver. Fail-fast.
|
|
239
|
+
*
|
|
240
|
+
* Network-bound only — does not materialize Quill instances and does not
|
|
241
|
+
* require an engine. Subsequent `getQuill` calls reuse the cached trees,
|
|
242
|
+
* skipping the fetch. Rejects on the first fetch failure.
|
|
243
|
+
*/
|
|
244
|
+
async warm() {
|
|
245
|
+
const promises = [];
|
|
246
|
+
for (const name of this.quillNames()) {
|
|
247
|
+
for (const version of this.versionsOf(name)) {
|
|
248
|
+
promises.push(this.#getTreeCached(`${name}@${version}`));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
await Promise.all(promises);
|
|
252
|
+
}
|
|
109
253
|
}
|
package/dist/source-loader.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Internal filesystem scanner for Source Quiver layout.
|
|
3
3
|
*
|
|
4
4
|
* Uses Node.js `fs/promises` — this module must only be imported from
|
|
5
|
-
* Node-only contexts (
|
|
5
|
+
* Node-only contexts (fromDir, fromPackage, etc.).
|
|
6
6
|
*/
|
|
7
7
|
import type { QuiverMeta } from "./quiver-yaml.js";
|
|
8
8
|
import type { QuiverLoader } from "./quiver.js";
|
package/dist/source-loader.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Internal filesystem scanner for Source Quiver layout.
|
|
3
3
|
*
|
|
4
4
|
* Uses Node.js `fs/promises` — this module must only be imported from
|
|
5
|
-
* Node-only contexts (
|
|
5
|
+
* Node-only contexts (fromDir, fromPackage, etc.).
|
|
6
6
|
*/
|
|
7
7
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
8
8
|
import { join, relative, sep } from "node:path";
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Convenience test harness for Quiver authors using `node:test`.
|
|
3
|
+
*
|
|
4
|
+
* Built into Node 18+; no extra test-runner dependency required. If you
|
|
5
|
+
* prefer vitest, jest, or another runner, write a 12-line loop against
|
|
6
|
+
* the main API instead — every primitive used here is public.
|
|
3
7
|
*
|
|
4
8
|
* Usage (place this file next to your Quiver.yaml):
|
|
5
9
|
*
|
|
@@ -8,32 +12,17 @@
|
|
|
8
12
|
* const engine = await Quillmark.load();
|
|
9
13
|
* runQuiverTests(import.meta.url, engine);
|
|
10
14
|
*
|
|
11
|
-
*
|
|
12
|
-
*/
|
|
13
|
-
import type { QuillmarkLike, QuillLike } from "./engine-types.js";
|
|
14
|
-
export type { QuillmarkLike, QuillLike };
|
|
15
|
-
/**
|
|
16
|
-
* Returns a lightweight mock engine and a record of every tree passed to it.
|
|
17
|
-
* Useful for writing custom test helpers; not intended as a substitute for the
|
|
18
|
-
* real engine in runQuiverTests (the mock performs no template compilation).
|
|
15
|
+
* Run with `node --test`.
|
|
19
16
|
*/
|
|
20
|
-
|
|
21
|
-
calls: Array<Map<string, Uint8Array>>;
|
|
22
|
-
engine: QuillmarkLike;
|
|
23
|
-
};
|
|
17
|
+
import type { QuillmarkLike } from "./engine-types.js";
|
|
24
18
|
/**
|
|
25
|
-
* Registers a
|
|
26
|
-
* quiver at `
|
|
27
|
-
*
|
|
28
|
-
* Pass `import.meta.url` when your test file lives at the quiver root (next to
|
|
29
|
-
* Quiver.yaml). Pass an absolute directory path for any other layout.
|
|
19
|
+
* Registers a `node:test` describe block that validates every quill
|
|
20
|
+
* version in the quiver at `metaUrlOrDir` against the provided engine.
|
|
30
21
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
22
|
+
* Pass `import.meta.url` when this file lives at the quiver root (next
|
|
23
|
+
* to Quiver.yaml). Pass an absolute directory path for any other layout.
|
|
33
24
|
*
|
|
34
|
-
* Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml,
|
|
35
|
-
* template files, and engine compilation via engine.quill(tree).
|
|
36
|
-
* contains a template error will cause its test to fail with the engine's own
|
|
37
|
-
* error message.
|
|
25
|
+
* Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml,
|
|
26
|
+
* all template files, and engine compilation via engine.quill(tree).
|
|
38
27
|
*/
|
|
39
|
-
export declare function runQuiverTests(
|
|
28
|
+
export declare function runQuiverTests(metaUrlOrDir: string, engine: QuillmarkLike): void;
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Convenience test harness for Quiver authors using `node:test`.
|
|
3
|
+
*
|
|
4
|
+
* Built into Node 18+; no extra test-runner dependency required. If you
|
|
5
|
+
* prefer vitest, jest, or another runner, write a 12-line loop against
|
|
6
|
+
* the main API instead — every primitive used here is public.
|
|
3
7
|
*
|
|
4
8
|
* Usage (place this file next to your Quiver.yaml):
|
|
5
9
|
*
|
|
@@ -8,101 +12,40 @@
|
|
|
8
12
|
* const engine = await Quillmark.load();
|
|
9
13
|
* runQuiverTests(import.meta.url, engine);
|
|
10
14
|
*
|
|
11
|
-
*
|
|
15
|
+
* Run with `node --test`.
|
|
12
16
|
*/
|
|
13
|
-
import { describe, it,
|
|
14
|
-
import { readdirSync } from "node:fs";
|
|
15
|
-
import { join, basename } from "node:path";
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { describe, it, before } from "node:test";
|
|
17
18
|
import { Quiver } from "./quiver.js";
|
|
18
|
-
import { QuiverRegistry } from "./registry.js";
|
|
19
|
-
/**
|
|
20
|
-
* Returns a lightweight mock engine and a record of every tree passed to it.
|
|
21
|
-
* Useful for writing custom test helpers; not intended as a substitute for the
|
|
22
|
-
* real engine in runQuiverTests (the mock performs no template compilation).
|
|
23
|
-
*/
|
|
24
|
-
export function makeMockEngine() {
|
|
25
|
-
const calls = [];
|
|
26
|
-
const engine = {
|
|
27
|
-
quill(tree) {
|
|
28
|
-
calls.push(tree);
|
|
29
|
-
return { render: () => ({ ok: true }) };
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
return { calls, engine };
|
|
33
|
-
}
|
|
34
|
-
function resolveSourceDir(sourceDirOrMetaUrl) {
|
|
35
|
-
if (sourceDirOrMetaUrl.startsWith("file://")) {
|
|
36
|
-
return fileURLToPath(new URL(".", sourceDirOrMetaUrl));
|
|
37
|
-
}
|
|
38
|
-
return sourceDirOrMetaUrl;
|
|
39
|
-
}
|
|
40
|
-
function discoverQuills(sourceDir) {
|
|
41
|
-
const quillsDir = join(sourceDir, "quills");
|
|
42
|
-
const results = [];
|
|
43
|
-
let nameDirs;
|
|
44
|
-
try {
|
|
45
|
-
nameDirs = readdirSync(quillsDir, { withFileTypes: true });
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return results;
|
|
49
|
-
}
|
|
50
|
-
for (const nameEntry of nameDirs) {
|
|
51
|
-
if (!nameEntry.isDirectory() || nameEntry.name.startsWith("."))
|
|
52
|
-
continue;
|
|
53
|
-
let versionDirs;
|
|
54
|
-
try {
|
|
55
|
-
versionDirs = readdirSync(join(quillsDir, nameEntry.name), {
|
|
56
|
-
withFileTypes: true,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
for (const versionEntry of versionDirs) {
|
|
63
|
-
if (!versionEntry.isDirectory() || versionEntry.name.startsWith("."))
|
|
64
|
-
continue;
|
|
65
|
-
results.push({ name: nameEntry.name, version: versionEntry.name });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return results;
|
|
69
|
-
}
|
|
70
19
|
/**
|
|
71
|
-
* Registers a
|
|
72
|
-
* quiver at `
|
|
20
|
+
* Registers a `node:test` describe block that validates every quill
|
|
21
|
+
* version in the quiver at `metaUrlOrDir` against the provided engine.
|
|
73
22
|
*
|
|
74
|
-
* Pass `import.meta.url` when
|
|
75
|
-
* Quiver.yaml). Pass an absolute directory path for any other layout.
|
|
23
|
+
* Pass `import.meta.url` when this file lives at the quiver root (next
|
|
24
|
+
* to Quiver.yaml). Pass an absolute directory path for any other layout.
|
|
76
25
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml, all
|
|
81
|
-
* template files, and engine compilation via engine.quill(tree). A quill that
|
|
82
|
-
* contains a template error will cause its test to fail with the engine's own
|
|
83
|
-
* error message.
|
|
26
|
+
* Validation covers the full loading pipeline: Quiver.yaml, Quill.yaml,
|
|
27
|
+
* all template files, and engine compilation via engine.quill(tree).
|
|
84
28
|
*/
|
|
85
|
-
export function runQuiverTests(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// collection-time crash.
|
|
91
|
-
const quills = discoverQuills(sourceDir);
|
|
92
|
-
describe(`Quiver: ${basename(sourceDir)}`, () => {
|
|
93
|
-
let registry;
|
|
94
|
-
beforeAll(async () => {
|
|
95
|
-
const quiver = await Quiver.fromSourceDir(sourceDir);
|
|
96
|
-
registry = new QuiverRegistry({ engine, quivers: [quiver] });
|
|
29
|
+
export function runQuiverTests(metaUrlOrDir, engine) {
|
|
30
|
+
describe("Quiver", () => {
|
|
31
|
+
let quiver;
|
|
32
|
+
before(async () => {
|
|
33
|
+
quiver = await Quiver.fromDir(metaUrlOrDir);
|
|
97
34
|
});
|
|
98
35
|
it("has at least one quill", () => {
|
|
99
|
-
|
|
36
|
+
if (quiver.quillNames().length === 0) {
|
|
37
|
+
throw new Error("Quiver has no quills");
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
it("compiles every quill version without error", async () => {
|
|
41
|
+
for (const name of quiver.quillNames()) {
|
|
42
|
+
for (const version of quiver.versionsOf(name)) {
|
|
43
|
+
const quill = await quiver.getQuill(`${name}@${version}`, { engine });
|
|
44
|
+
if (typeof quill.render !== "function") {
|
|
45
|
+
throw new Error(`${name}@${version}: engine returned non-conforming Quill`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
100
49
|
});
|
|
101
|
-
for (const { name, version } of quills) {
|
|
102
|
-
it(`${name}@${version} compiles without error`, async () => {
|
|
103
|
-
const quill = await registry.getQuill(`${name}@${version}`);
|
|
104
|
-
expect(typeof quill.render).toBe("function");
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
50
|
});
|
|
108
51
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HttpTransport — browser-safe
|
|
2
|
+
* HttpTransport — browser-safe built-quiver transport that fetches via HTTP.
|
|
3
3
|
* Internal; not exported from index.ts.
|
|
4
4
|
*
|
|
5
5
|
* Uses globalThis.fetch — no node: imports at any level.
|
|
6
6
|
*/
|
|
7
|
-
import type {
|
|
8
|
-
export declare class HttpTransport implements
|
|
7
|
+
import type { BuiltTransport } from "../built-loader.js";
|
|
8
|
+
export declare class HttpTransport implements BuiltTransport {
|
|
9
9
|
private readonly baseUrl;
|
|
10
10
|
constructor(baseUrl: string);
|
|
11
11
|
fetchBytes(relativePath: string): Promise<Uint8Array>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/quiver",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Quiver registry and
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Quiver registry and build tooling for Quillmark",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -42,20 +42,14 @@
|
|
|
42
42
|
"README.md"
|
|
43
43
|
],
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@quillmark/wasm": ">=0.59.0
|
|
46
|
-
"vitest": ">=4.0.0"
|
|
47
|
-
},
|
|
48
|
-
"peerDependenciesMeta": {
|
|
49
|
-
"vitest": {
|
|
50
|
-
"optional": true
|
|
51
|
-
}
|
|
45
|
+
"@quillmark/wasm": ">=0.59.0"
|
|
52
46
|
},
|
|
53
47
|
"dependencies": {
|
|
54
48
|
"fflate": "^0.8.2",
|
|
55
49
|
"yaml": "^2.8.3"
|
|
56
50
|
},
|
|
57
51
|
"devDependencies": {
|
|
58
|
-
"@quillmark/wasm": "0.59.0
|
|
52
|
+
"@quillmark/wasm": "0.59.0",
|
|
59
53
|
"@types/node": "^25.3.3",
|
|
60
54
|
"typescript": "^5.9.3",
|
|
61
55
|
"vitest": "^4.0.18"
|
package/dist/pack.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pack logic — internal, Node-only.
|
|
3
|
-
*
|
|
4
|
-
* All Node.js built-in imports are done dynamically inside `packQuiver` so
|
|
5
|
-
* that a type-only import of `PackOptions` from `src/index.ts` does NOT
|
|
6
|
-
* pull `node:fs` or `node:crypto` into browser bundles.
|
|
7
|
-
*/
|
|
8
|
-
/** Reserved for future pack options (e.g. compression level, filters). */
|
|
9
|
-
export type PackOptions = Record<string, never>;
|
|
10
|
-
/**
|
|
11
|
-
* Reads a Source Quiver, validates it, and writes a Packed Quiver to outDir.
|
|
12
|
-
*
|
|
13
|
-
* Output layout:
|
|
14
|
-
* outDir/
|
|
15
|
-
* Quiver.json # stable pointer
|
|
16
|
-
* manifest.<md5prefix6>.json # hashed manifest
|
|
17
|
-
* <name>@<version>.<md5>.zip # one bundle per quill
|
|
18
|
-
* store/
|
|
19
|
-
* <md5> # dehydrated font bytes (full hash, no ext)
|
|
20
|
-
*
|
|
21
|
-
* Throws:
|
|
22
|
-
* - `quiver_invalid` on source validation failures (propagated from scanner)
|
|
23
|
-
* - `transport_error` on I/O failures
|
|
24
|
-
*/
|
|
25
|
-
export declare function packQuiver(sourceDir: string, outDir: string, _opts?: PackOptions): Promise<void>;
|
package/dist/registry.d.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import type { QuillmarkLike, QuillLike } from "./engine-types.js";
|
|
2
|
-
import type { Quiver } from "./quiver.js";
|
|
3
|
-
export declare class QuiverRegistry {
|
|
4
|
-
#private;
|
|
5
|
-
constructor(args: {
|
|
6
|
-
engine: QuillmarkLike;
|
|
7
|
-
quivers: Quiver[];
|
|
8
|
-
});
|
|
9
|
-
/**
|
|
10
|
-
* Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
|
|
11
|
-
*
|
|
12
|
-
* Applies multi-quiver precedence (§4): scan quivers in order, first quiver
|
|
13
|
-
* with any matching candidate wins, highest match within that quiver returned.
|
|
14
|
-
*
|
|
15
|
-
* Throws:
|
|
16
|
-
* - `invalid_ref` if ref fails parseQuillRef
|
|
17
|
-
* - `quill_not_found` if no quiver has a matching candidate
|
|
18
|
-
*/
|
|
19
|
-
resolve(ref: string): Promise<string>;
|
|
20
|
-
/**
|
|
21
|
-
* Returns a render-ready QuillLike instance for a canonical ref.
|
|
22
|
-
* Materializes via engine.quill(tree) on first call; caches by canonical ref.
|
|
23
|
-
*
|
|
24
|
-
* Throws:
|
|
25
|
-
* - `invalid_ref` if canonicalRef is not valid canonical x.y.z form
|
|
26
|
-
* - `quill_not_found` if canonical ref doesn't map to a loaded quiver
|
|
27
|
-
* - propagates I/O errors from loadTree unchanged
|
|
28
|
-
* - propagates engine errors from engine.quill() unchanged (not wrapped)
|
|
29
|
-
*/
|
|
30
|
-
getQuill(canonicalRef: string): Promise<QuillLike>;
|
|
31
|
-
/**
|
|
32
|
-
* Warms all quill refs across all composed quivers. Fail-fast.
|
|
33
|
-
*
|
|
34
|
-
* Calls loadTree + engine.quill(tree) for every known quill version in
|
|
35
|
-
* parallel. Already-cached refs resolve instantly (idempotent). Rejects on
|
|
36
|
-
* the first failure (Promise.all fail-fast semantics).
|
|
37
|
-
*/
|
|
38
|
-
warm(): Promise<void>;
|
|
39
|
-
}
|