@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/registry.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { QuiverError } from "./errors.js";
|
|
2
|
-
import { parseQuillRef } from "./ref.js";
|
|
3
|
-
import { matchesSemverSelector, chooseHighestVersion } from "./semver.js";
|
|
4
|
-
export class QuiverRegistry {
|
|
5
|
-
#engine;
|
|
6
|
-
#quivers;
|
|
7
|
-
/** Cache: canonical ref → pending or resolved Promise<QuillLike>. */
|
|
8
|
-
#cache = new Map();
|
|
9
|
-
constructor(args) {
|
|
10
|
-
this.#engine = args.engine;
|
|
11
|
-
// Validate no two quivers share Quiver.yaml.name → quiver_collision.
|
|
12
|
-
const seen = new Map();
|
|
13
|
-
for (const quiver of args.quivers) {
|
|
14
|
-
const existing = seen.get(quiver.name);
|
|
15
|
-
if (existing !== undefined) {
|
|
16
|
-
throw new QuiverError("quiver_collision", `Two quivers share the name "${quiver.name}": first quiver and a later quiver both declare this name. ` +
|
|
17
|
-
`Quiver names must be unique within a registry.`, { quiverName: quiver.name });
|
|
18
|
-
}
|
|
19
|
-
seen.set(quiver.name, quiver.name);
|
|
20
|
-
}
|
|
21
|
-
this.#quivers = Object.freeze([...args.quivers]);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
|
|
25
|
-
*
|
|
26
|
-
* Applies multi-quiver precedence (§4): scan quivers in order, first quiver
|
|
27
|
-
* with any matching candidate wins, highest match within that quiver returned.
|
|
28
|
-
*
|
|
29
|
-
* Throws:
|
|
30
|
-
* - `invalid_ref` if ref fails parseQuillRef
|
|
31
|
-
* - `quill_not_found` if no quiver has a matching candidate
|
|
32
|
-
*/
|
|
33
|
-
async resolve(ref) {
|
|
34
|
-
// Throws invalid_ref on malformed input.
|
|
35
|
-
const parsed = parseQuillRef(ref);
|
|
36
|
-
for (const quiver of this.#quivers) {
|
|
37
|
-
const versions = quiver.versionsOf(parsed.name);
|
|
38
|
-
if (versions.length === 0)
|
|
39
|
-
continue;
|
|
40
|
-
// Filter by selector if present; otherwise all versions are candidates.
|
|
41
|
-
const candidates = parsed.selector === undefined
|
|
42
|
-
? versions
|
|
43
|
-
: versions.filter((v) => matchesSemverSelector(v, parsed.selector));
|
|
44
|
-
if (candidates.length === 0)
|
|
45
|
-
continue;
|
|
46
|
-
// First quiver with any candidate wins — pick highest within that quiver.
|
|
47
|
-
const winner = chooseHighestVersion(candidates);
|
|
48
|
-
// chooseHighestVersion returns null only for empty arrays; candidates is non-empty.
|
|
49
|
-
return `${parsed.name}@${winner}`;
|
|
50
|
-
}
|
|
51
|
-
throw new QuiverError("quill_not_found", `No quill found for ref "${ref}" in any registered quiver.`, { ref });
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Returns a render-ready QuillLike instance for a canonical ref.
|
|
55
|
-
* Materializes via engine.quill(tree) on first call; caches by canonical ref.
|
|
56
|
-
*
|
|
57
|
-
* Throws:
|
|
58
|
-
* - `invalid_ref` if canonicalRef is not valid canonical x.y.z form
|
|
59
|
-
* - `quill_not_found` if canonical ref doesn't map to a loaded quiver
|
|
60
|
-
* - propagates I/O errors from loadTree unchanged
|
|
61
|
-
* - propagates engine errors from engine.quill() unchanged (not wrapped)
|
|
62
|
-
*/
|
|
63
|
-
async getQuill(canonicalRef) {
|
|
64
|
-
let entry = this.#cache.get(canonicalRef);
|
|
65
|
-
if (entry === undefined) {
|
|
66
|
-
entry = this.#loadQuill(canonicalRef).catch((err) => {
|
|
67
|
-
this.#cache.delete(canonicalRef);
|
|
68
|
-
throw err;
|
|
69
|
-
});
|
|
70
|
-
this.#cache.set(canonicalRef, entry);
|
|
71
|
-
}
|
|
72
|
-
return entry;
|
|
73
|
-
}
|
|
74
|
-
/** Internal: does the actual loading work for getQuill. */
|
|
75
|
-
async #loadQuill(canonicalRef) {
|
|
76
|
-
// Parse and validate canonical form (must be x.y.z).
|
|
77
|
-
const parsed = parseQuillRef(canonicalRef);
|
|
78
|
-
if (parsed.selectorDepth !== 3) {
|
|
79
|
-
throw new QuiverError("invalid_ref", `getQuill requires a canonical ref (x.y.z) but received "${canonicalRef}". ` +
|
|
80
|
-
`Use resolve() first to obtain a canonical ref.`, { ref: canonicalRef });
|
|
81
|
-
}
|
|
82
|
-
const version = parsed.selector;
|
|
83
|
-
// Find the first quiver that owns this exact (name, version) pair.
|
|
84
|
-
const owningQuiver = this.#quivers.find((q) => q.versionsOf(parsed.name).includes(version));
|
|
85
|
-
if (owningQuiver === undefined) {
|
|
86
|
-
throw new QuiverError("quill_not_found", `Quill "${canonicalRef}" was not found in any registered quiver.`, { ref: canonicalRef, version });
|
|
87
|
-
}
|
|
88
|
-
// Load the file tree — I/O errors propagate as-is.
|
|
89
|
-
const tree = await owningQuiver.loadTree(parsed.name, version);
|
|
90
|
-
// Materialize the Quill via the engine.
|
|
91
|
-
// Engine errors propagate unchanged — they are not QuiverErrors and we
|
|
92
|
-
// should not mask them. The caller's error-handling stack will see the
|
|
93
|
-
// engine's own error type directly.
|
|
94
|
-
const quill = this.#engine.quill(tree);
|
|
95
|
-
return quill;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Warms all quill refs across all composed quivers. Fail-fast.
|
|
99
|
-
*
|
|
100
|
-
* Calls loadTree + engine.quill(tree) for every known quill version in
|
|
101
|
-
* parallel. Already-cached refs resolve instantly (idempotent). Rejects on
|
|
102
|
-
* the first failure (Promise.all fail-fast semantics).
|
|
103
|
-
*/
|
|
104
|
-
async warm() {
|
|
105
|
-
const promises = [];
|
|
106
|
-
for (const quiver of this.#quivers) {
|
|
107
|
-
for (const name of quiver.quillNames()) {
|
|
108
|
-
for (const version of quiver.versionsOf(name)) {
|
|
109
|
-
promises.push(this.getQuill(`${name}@${version}`));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
await Promise.all(promises);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FsTransport — Node-only packed quiver transport that reads from local disk.
|
|
3
|
-
* Internal; not exported from index.ts.
|
|
4
|
-
*
|
|
5
|
-
* Uses dynamic imports of node:fs/promises and node:path so the module can be
|
|
6
|
-
* imported in environments where those builtins are not available (the dynamic
|
|
7
|
-
* import only executes when fetchBytes is actually called).
|
|
8
|
-
*/
|
|
9
|
-
import type { PackedTransport } from "../packed-loader.js";
|
|
10
|
-
export declare class FsTransport implements PackedTransport {
|
|
11
|
-
private rootDir;
|
|
12
|
-
constructor(rootDir: string);
|
|
13
|
-
fetchBytes(relativePath: string): Promise<Uint8Array>;
|
|
14
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FsTransport — Node-only packed quiver transport that reads from local disk.
|
|
3
|
-
* Internal; not exported from index.ts.
|
|
4
|
-
*
|
|
5
|
-
* Uses dynamic imports of node:fs/promises and node:path so the module can be
|
|
6
|
-
* imported in environments where those builtins are not available (the dynamic
|
|
7
|
-
* import only executes when fetchBytes is actually called).
|
|
8
|
-
*/
|
|
9
|
-
import { QuiverError } from "../errors.js";
|
|
10
|
-
export class FsTransport {
|
|
11
|
-
rootDir;
|
|
12
|
-
constructor(rootDir) {
|
|
13
|
-
this.rootDir = rootDir;
|
|
14
|
-
}
|
|
15
|
-
async fetchBytes(relativePath) {
|
|
16
|
-
const { join, resolve } = await import("node:path");
|
|
17
|
-
const { readFile } = await import("node:fs/promises");
|
|
18
|
-
const rootResolved = resolve(this.rootDir);
|
|
19
|
-
const fullPath = resolve(join(this.rootDir, relativePath));
|
|
20
|
-
// Defense-in-depth: ensure resolved path stays within rootDir.
|
|
21
|
-
if (!fullPath.startsWith(rootResolved + "/") && fullPath !== rootResolved) {
|
|
22
|
-
throw new QuiverError("transport_error", `Path traversal detected: "${relativePath}" escapes quiver root`);
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
const buf = await readFile(fullPath);
|
|
26
|
-
// Ensure we return a plain Uint8Array, not a Node.js Buffer subclass.
|
|
27
|
-
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
28
|
-
}
|
|
29
|
-
catch (err) {
|
|
30
|
-
throw new QuiverError("transport_error", `Failed to read "${relativePath}" from packed quiver at "${this.rootDir}": ${err.message}`, { cause: err });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|