@quillmark/quiver 0.16.0 → 0.18.1
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/LICENSE +201 -21
- package/NOTICE +2 -0
- package/README.md +84 -245
- package/dist/bin/quiver.d.ts +6 -6
- package/dist/bin/quiver.js +31 -63
- package/dist/build.d.ts +12 -12
- package/dist/build.js +78 -33
- package/dist/built-loader.d.ts +16 -16
- package/dist/built-loader.js +126 -112
- package/dist/bundle.js +1 -1
- package/dist/digest.d.ts +19 -0
- package/dist/digest.js +25 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/format.d.ts +11 -0
- package/dist/format.js +11 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -5
- package/dist/node.d.ts +47 -76
- package/dist/node.js +59 -64
- package/dist/quiver-yaml.js +19 -19
- package/dist/quiver.d.ts +25 -37
- package/dist/quiver.js +46 -113
- package/dist/ref.d.ts +3 -3
- package/dist/ref.js +15 -13
- package/dist/semver.d.ts +5 -1
- package/dist/semver.js +9 -10
- package/dist/source-loader.d.ts +3 -3
- package/dist/source-loader.js +26 -26
- package/dist/transports/fs-built-transport.d.ts +2 -2
- package/dist/transports/fs-built-transport.js +10 -8
- package/dist/transports/http-transport.d.ts +2 -2
- package/dist/transports/http-transport.js +11 -9
- package/package.json +68 -72
- package/dist/preview.d.ts +0 -81
- package/dist/preview.js +0 -208
- package/dist/testing.d.ts +0 -28
- package/dist/testing.js +0 -66
package/dist/node.js
CHANGED
|
@@ -1,75 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Node-only entrypoint
|
|
2
|
+
* Node-only entrypoint: the factories that read a filesystem, as free
|
|
3
|
+
* functions. Each returns a `Quiver`; the class itself is the browser-safe one
|
|
4
|
+
* from the main entry, unmodified. Importing this module installs nothing, so
|
|
5
|
+
* the import path is the whole contract and a bundler drops what is not called.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* Bundler note: this entry pulls in `./source-loader.js`, `./build.js`, and
|
|
8
|
+
* `./transports/fs-built-transport.js`, all of which statically import `node:*`
|
|
9
|
+
* builtins. Browser bundles must never reach it. The main entry (`./index.js`)
|
|
10
|
+
* makes no static or dynamic reference to it.
|
|
11
|
+
*/
|
|
12
|
+
import { createQuiver } from './quiver.js';
|
|
13
|
+
import { scanSourceQuiver, SourceLoader } from './source-loader.js';
|
|
14
|
+
import { buildQuiver } from './build.js';
|
|
15
|
+
import { loadBuiltQuiver } from './built-loader.js';
|
|
16
|
+
import { FsBuiltTransport } from './transports/fs-built-transport.js';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
/**
|
|
19
|
+
* Reads a Source Quiver from a local directory containing `Quiver.yaml` and
|
|
20
|
+
* `quills/<name>/<version>/Quill.yaml` entries.
|
|
21
|
+
*
|
|
22
|
+
* Also accepts an `import.meta.url`-style `file://` URL; the URL's parent
|
|
23
|
+
* directory is used as the source root.
|
|
8
24
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* here, so the import path remains the contract.
|
|
25
|
+
* A quiver installed from npm is a directory like any other:
|
|
26
|
+
* `dirname(createRequire(import.meta.url).resolve('<pkg>/Quiver.yaml'))` is its
|
|
27
|
+
* root, resolved from the caller, whose dependencies are reachable from the
|
|
28
|
+
* caller alone under an isolated `node_modules` layout.
|
|
14
29
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* statically import `node:*` builtins. Browser bundles must never reach this
|
|
18
|
-
* module. The main entry (`./index.js`) makes no static or dynamic reference
|
|
19
|
-
* to it.
|
|
30
|
+
* Throws `quiver_invalid` on schema violations, `transport_error` on I/O
|
|
31
|
+
* failure.
|
|
20
32
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import { buildQuiver } from "./build.js";
|
|
25
|
-
import { loadBuiltQuiver } from "./built-loader.js";
|
|
26
|
-
import { FsBuiltTransport } from "./transports/fs-built-transport.js";
|
|
27
|
-
import { createRequire } from "node:module";
|
|
28
|
-
import { dirname } from "node:path";
|
|
29
|
-
import { fileURLToPath } from "node:url";
|
|
30
|
-
export const Quiver = Base;
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// 2. Runtime patch — install Node-only statics on the shared class.
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
Quiver.fromDir = async function fromDir(pathOrFileUrl) {
|
|
35
|
-
const dir = pathOrFileUrl.startsWith("file://")
|
|
36
|
-
? fileURLToPath(new URL(".", pathOrFileUrl))
|
|
33
|
+
export async function fromDir(pathOrFileUrl) {
|
|
34
|
+
const dir = pathOrFileUrl.startsWith('file://')
|
|
35
|
+
? fileURLToPath(new URL('.', pathOrFileUrl))
|
|
37
36
|
: pathOrFileUrl;
|
|
38
37
|
const { meta, catalog } = await scanSourceQuiver(dir);
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
|
|
38
|
+
return createQuiver(meta.name, catalog, new SourceLoader(dir));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Loads a packed (build-output) quiver from a local directory containing
|
|
42
|
+
* `latest.json` and the manifest/bundle/store files written by `build`.
|
|
43
|
+
* Symmetric to `Quiver.fromBuiltUrl(url)` but reads from disk instead of HTTP:
|
|
44
|
+
* no network required.
|
|
45
|
+
*
|
|
46
|
+
* Use this for server-side runtime when a packed artifact ships in the
|
|
47
|
+
* deployment image; consumers can keep source quivers as devDependencies and
|
|
48
|
+
* avoid self-fetching over their own load balancer.
|
|
49
|
+
*
|
|
50
|
+
* Throws `quiver_invalid` on format errors, `transport_error` on I/O failure.
|
|
51
|
+
*/
|
|
52
|
+
export async function fromBuiltDir(dirPath) {
|
|
42
53
|
return loadBuiltQuiver(new FsBuiltTransport(dirPath));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
58
|
-
Quiver.buildPackage = async function buildPackage(specifier, outDir, opts) {
|
|
59
|
-
const req = createRequire(import.meta.url);
|
|
60
|
-
let yamlPath;
|
|
61
|
-
try {
|
|
62
|
-
yamlPath = req.resolve(`${specifier}/Quiver.yaml`);
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
throw new QuiverError("transport_error", `Failed to resolve quiver package "${specifier}": ${err.message}`, { cause: err });
|
|
66
|
-
}
|
|
67
|
-
return buildQuiver(dirname(yamlPath), outDir, opts);
|
|
68
|
-
};
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// 3. Re-export the rest of the public surface so consumers get one import.
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
export { QuiverError } from "./errors.js";
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reads the Source Quiver at sourceDir, validates it, and writes the runtime
|
|
57
|
+
* build artifact to outDir, which the build clears first and therefore owns.
|
|
58
|
+
*
|
|
59
|
+
* Throws `quiver_invalid` on source validation failures, `transport_error` on
|
|
60
|
+
* I/O failures and on an outDir holding the source quiver or the cwd.
|
|
61
|
+
*/
|
|
62
|
+
export async function build(sourceDir, outDir) {
|
|
63
|
+
return buildQuiver(sourceDir, outDir);
|
|
64
|
+
}
|
|
65
|
+
// The rest of the public surface, so a Node consumer needs one import.
|
|
66
|
+
export { Quiver } from './quiver.js';
|
|
67
|
+
export { QuiverError } from './errors.js';
|
|
73
68
|
// Engine types (`Quillmark`, `Quill`, `Document`, `RenderResult`, …) are not
|
|
74
69
|
// re-exported: import them straight from the `@quillmark/wasm` peer dependency,
|
|
75
70
|
// which is the single source of truth for the engine contract.
|
package/dist/quiver-yaml.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* simple two-field schema (name, description), but using a proper YAML parser
|
|
6
6
|
* ensures correct handling of quoting, escaping, and multi-line strings.
|
|
7
7
|
*/
|
|
8
|
-
import { parse as parseYaml } from
|
|
9
|
-
import { QuiverError } from
|
|
8
|
+
import { parse as parseYaml } from 'yaml';
|
|
9
|
+
import { QuiverError } from './errors.js';
|
|
10
10
|
const NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
11
|
-
const KNOWN_FIELDS = new Set([
|
|
11
|
+
const KNOWN_FIELDS = new Set(['name', 'description']);
|
|
12
12
|
/**
|
|
13
13
|
* Parses and validates Quiver.yaml contents.
|
|
14
14
|
*
|
|
@@ -20,44 +20,44 @@ const KNOWN_FIELDS = new Set(["name", "description"]);
|
|
|
20
20
|
* - `description` present but not a string
|
|
21
21
|
*/
|
|
22
22
|
export function parseQuiverYaml(raw) {
|
|
23
|
-
const text = typeof raw ===
|
|
23
|
+
const text = typeof raw === 'string' ? raw : new TextDecoder().decode(raw);
|
|
24
24
|
let parsed;
|
|
25
25
|
try {
|
|
26
26
|
parsed = parseYaml(text);
|
|
27
27
|
}
|
|
28
28
|
catch (err) {
|
|
29
|
-
throw new QuiverError(
|
|
29
|
+
throw new QuiverError('quiver_invalid', `Quiver.yaml: YAML parse failure — ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
30
30
|
}
|
|
31
|
-
if (parsed === null || typeof parsed !==
|
|
32
|
-
throw new QuiverError(
|
|
31
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
32
|
+
throw new QuiverError('quiver_invalid', `Quiver.yaml: expected a mapping at the top level, got ${Array.isArray(parsed) ? 'array' : String(parsed)}`);
|
|
33
33
|
}
|
|
34
34
|
const doc = parsed;
|
|
35
35
|
// Check for unknown fields (strict mode)
|
|
36
36
|
for (const key of Object.keys(doc)) {
|
|
37
37
|
if (!KNOWN_FIELDS.has(key)) {
|
|
38
|
-
throw new QuiverError(
|
|
38
|
+
throw new QuiverError('quiver_invalid', `Quiver.yaml: unknown field "${key}" — only "name" and "description" are valid in V1`);
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
// Validate `name`
|
|
42
|
-
if (!(
|
|
43
|
-
throw new QuiverError(
|
|
42
|
+
if (!('name' in doc)) {
|
|
43
|
+
throw new QuiverError('quiver_invalid', `Quiver.yaml: required field "name" is missing`);
|
|
44
44
|
}
|
|
45
|
-
if (typeof doc[
|
|
46
|
-
throw new QuiverError(
|
|
45
|
+
if (typeof doc['name'] !== 'string') {
|
|
46
|
+
throw new QuiverError('quiver_invalid', `Quiver.yaml: "name" must be a string, got ${typeof doc['name']}`);
|
|
47
47
|
}
|
|
48
|
-
const name = doc[
|
|
48
|
+
const name = doc['name'];
|
|
49
49
|
if (!NAME_RE.test(name)) {
|
|
50
|
-
throw new QuiverError(
|
|
50
|
+
throw new QuiverError('quiver_invalid', `Quiver.yaml: "name" value "${name}" contains invalid characters — only [A-Za-z0-9_-] are allowed`);
|
|
51
51
|
}
|
|
52
52
|
// Validate optional `description`
|
|
53
|
-
if (
|
|
54
|
-
if (typeof doc[
|
|
55
|
-
throw new QuiverError(
|
|
53
|
+
if ('description' in doc && doc['description'] !== undefined) {
|
|
54
|
+
if (typeof doc['description'] !== 'string') {
|
|
55
|
+
throw new QuiverError('quiver_invalid', `Quiver.yaml: "description" must be a string if present, got ${typeof doc['description']}`);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
const meta = { name };
|
|
59
|
-
if (typeof doc[
|
|
60
|
-
meta.description = doc[
|
|
59
|
+
if (typeof doc['description'] === 'string') {
|
|
60
|
+
meta.description = doc['description'];
|
|
61
61
|
}
|
|
62
62
|
return meta;
|
|
63
63
|
}
|
package/dist/quiver.d.ts
CHANGED
|
@@ -5,53 +5,40 @@
|
|
|
5
5
|
* (either source-backed or build-output-backed).
|
|
6
6
|
*
|
|
7
7
|
* This module is browser-safe: only `fromBuiltUrl` and the instance API live
|
|
8
|
-
* here.
|
|
9
|
-
*
|
|
10
|
-
* consumer's explicit opt-in to the Node API surface.
|
|
8
|
+
* here. The filesystem factories are free functions in `./node.js`; the class
|
|
9
|
+
* is the same either way.
|
|
11
10
|
*/
|
|
12
|
-
import { Quill } from
|
|
13
|
-
/**
|
|
11
|
+
import { Quill } from '@quillmark/wasm';
|
|
12
|
+
/** Loader strategy: source or build output. Package-internal. */
|
|
14
13
|
export interface QuiverLoader {
|
|
15
14
|
loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
|
|
16
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The private constructor's only door, closed over by the static block below.
|
|
18
|
+
* Exported as `createQuiver` for this package's loaders and reachable from no
|
|
19
|
+
* entry in `exports`, so a consumer holds the factories and nothing else.
|
|
20
|
+
*/
|
|
21
|
+
declare let create: (name: string, catalog: Map<string, string[]>, loader: QuiverLoader) => Quiver;
|
|
17
22
|
export declare class Quiver {
|
|
18
23
|
#private;
|
|
19
24
|
readonly name: string;
|
|
20
25
|
/**
|
|
21
|
-
* Private constructor
|
|
22
|
-
* or
|
|
23
|
-
*
|
|
24
|
-
* external `new Quiver(...)` at compile time.
|
|
26
|
+
* Private constructor. A Quiver comes from a factory (`Quiver.fromBuiltUrl`,
|
|
27
|
+
* or `fromDir` / `fromBuiltDir` from `@quillmark/quiver/node`), which is what
|
|
28
|
+
* names the thing being read.
|
|
25
29
|
*/
|
|
26
30
|
private constructor();
|
|
27
|
-
/**
|
|
28
|
-
* @internal Construction escape hatch around the private constructor. Used
|
|
29
|
-
* by `loadBuiltQuiver` and by the Node entry (`./node.js`) when installing
|
|
30
|
-
* `fromDir` / `fromPackage`. Not part of the public API.
|
|
31
|
-
*/
|
|
32
|
-
static _fromLoader(name: string, catalog: Map<string, string[]>, loader: QuiverLoader): Quiver;
|
|
33
31
|
/**
|
|
34
32
|
* Browser-safe factory. Loads build output from an HTTP/HTTPS URL.
|
|
35
33
|
*
|
|
36
34
|
* Origin-relative URLs (e.g. `/quivers/foo/`) are accepted in browser
|
|
37
35
|
* environments. `file://` URLs are rejected — to load build output from
|
|
38
|
-
* disk in Node, use `
|
|
39
|
-
* `@quillmark/quiver/node`.
|
|
36
|
+
* disk in Node, use `fromBuiltDir(path)` from `@quillmark/quiver/node`.
|
|
40
37
|
*
|
|
41
38
|
* Throws `transport_error` on network/HTTP failure, `quiver_invalid`
|
|
42
39
|
* on format errors.
|
|
43
40
|
*/
|
|
44
41
|
static fromBuiltUrl(url: string): Promise<Quiver>;
|
|
45
|
-
/**
|
|
46
|
-
* Browser-safe factory. Seeds the catalog from caller-provided manifest
|
|
47
|
-
* bytes — never fetches `latest.json` — then fetches bundles lazily and
|
|
48
|
-
* content-addressed, relative to `baseUrl`, like `fromBuiltUrl`. For SSR
|
|
49
|
-
* consumers that already hold the manifest at build time.
|
|
50
|
-
*
|
|
51
|
-
* Throws `quiver_invalid` on malformed manifest bytes, `transport_error`
|
|
52
|
-
* on a `file://` baseUrl or a later bundle fetch failure.
|
|
53
|
-
*/
|
|
54
|
-
static fromManifest(baseUrl: string, manifestBytes: Uint8Array): Promise<Quiver>;
|
|
55
42
|
/** Returns all known quill names, sorted lexicographically. */
|
|
56
43
|
quillNames(): string[];
|
|
57
44
|
/**
|
|
@@ -65,11 +52,15 @@ export declare class Quiver {
|
|
|
65
52
|
* Selector forms: `name`, `name@x`, `name@x.y`, `name@x.y.z`. Picks the
|
|
66
53
|
* highest matching version in this quiver.
|
|
67
54
|
*
|
|
55
|
+
* Sync, and in-memory by construction rather than by implementation: every
|
|
56
|
+
* loader materializes the catalog as the quiver is built, and `QuiverLoader`
|
|
57
|
+
* carries one verb, `loadTree`, which resolution never reaches.
|
|
58
|
+
*
|
|
68
59
|
* Throws:
|
|
69
60
|
* - `invalid_ref` if ref fails parseQuillRef
|
|
70
61
|
* - `quill_not_found` if no version matches
|
|
71
62
|
*/
|
|
72
|
-
resolve(ref: string):
|
|
63
|
+
resolve(ref: string): string;
|
|
73
64
|
/**
|
|
74
65
|
* Returns a `Quill` for a ref (selector or canonical).
|
|
75
66
|
*
|
|
@@ -83,6 +74,11 @@ export declare class Quiver {
|
|
|
83
74
|
* first. Materializes once and caches per canonical ref — concurrent calls
|
|
84
75
|
* coalesce into a single load.
|
|
85
76
|
*
|
|
77
|
+
* BORROWED, not handed over: the quill is the quiver's, shared with every
|
|
78
|
+
* other caller for the same ref, and lives as long as the quiver. Do not
|
|
79
|
+
* `free()` it: the next `getQuill` for that ref would hand out a freed
|
|
80
|
+
* handle. A caller wanting one of its own mints it from `.toTree()`.
|
|
81
|
+
*
|
|
86
82
|
* Throws:
|
|
87
83
|
* - `invalid_ref` if ref is malformed
|
|
88
84
|
* - `quill_not_found` if ref does not match any version in this quiver
|
|
@@ -90,13 +86,5 @@ export declare class Quiver {
|
|
|
90
86
|
* - propagates validation errors from Quill.fromTree() unchanged
|
|
91
87
|
*/
|
|
92
88
|
getQuill(ref: string): Promise<Quill>;
|
|
93
|
-
/**
|
|
94
|
-
* Prefetches the tree for every quill version in this quiver. Fail-fast.
|
|
95
|
-
*
|
|
96
|
-
* Network-bound only — does not materialize Quill instances and does not
|
|
97
|
-
* require an engine. Subsequent `getQuill` calls reuse the cached trees,
|
|
98
|
-
* skipping the fetch (the tree then evicts as its quill materializes).
|
|
99
|
-
* Rejects on the first fetch failure.
|
|
100
|
-
*/
|
|
101
|
-
warm(): Promise<void>;
|
|
102
89
|
}
|
|
90
|
+
export { create as createQuiver };
|
package/dist/quiver.js
CHANGED
|
@@ -5,14 +5,19 @@
|
|
|
5
5
|
* (either source-backed or build-output-backed).
|
|
6
6
|
*
|
|
7
7
|
* This module is browser-safe: only `fromBuiltUrl` and the instance API live
|
|
8
|
-
* here.
|
|
9
|
-
*
|
|
10
|
-
* consumer's explicit opt-in to the Node API surface.
|
|
8
|
+
* here. The filesystem factories are free functions in `./node.js`; the class
|
|
9
|
+
* is the same either way.
|
|
11
10
|
*/
|
|
12
|
-
import { QuiverError } from
|
|
13
|
-
import { Quill } from
|
|
14
|
-
import { parseQuillRef } from
|
|
15
|
-
import { matchesSemverSelector, chooseHighestVersion } from
|
|
11
|
+
import { QuiverError } from './errors.js';
|
|
12
|
+
import { Quill } from '@quillmark/wasm';
|
|
13
|
+
import { parseQuillRef } from './ref.js';
|
|
14
|
+
import { matchesSemverSelector, chooseHighestVersion } from './semver.js';
|
|
15
|
+
/**
|
|
16
|
+
* The private constructor's only door, closed over by the static block below.
|
|
17
|
+
* Exported as `createQuiver` for this package's loaders and reachable from no
|
|
18
|
+
* entry in `exports`, so a consumer holds the factories and nothing else.
|
|
19
|
+
*/
|
|
20
|
+
let create;
|
|
16
21
|
export class Quiver {
|
|
17
22
|
name;
|
|
18
23
|
#catalog;
|
|
@@ -25,68 +30,37 @@ export class Quiver {
|
|
|
25
30
|
*/
|
|
26
31
|
#quillCache = new Map();
|
|
27
32
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*/
|
|
32
|
-
#treeCache = new Map();
|
|
33
|
-
/**
|
|
34
|
-
* Private constructor — use static factory methods (`Quiver.fromBuiltUrl`,
|
|
35
|
-
* or the Node-only `Quiver.fromDir` / `Quiver.fromPackage` /
|
|
36
|
-
* `Quiver.fromBuiltDir` installed by `@quillmark/quiver/node`). TS prevents
|
|
37
|
-
* external `new Quiver(...)` at compile time.
|
|
33
|
+
* Private constructor. A Quiver comes from a factory (`Quiver.fromBuiltUrl`,
|
|
34
|
+
* or `fromDir` / `fromBuiltDir` from `@quillmark/quiver/node`), which is what
|
|
35
|
+
* names the thing being read.
|
|
38
36
|
*/
|
|
39
37
|
constructor(name, catalog, loader) {
|
|
40
38
|
this.name = name;
|
|
41
39
|
this.#catalog = new Map(catalog);
|
|
42
40
|
this.#loader = loader;
|
|
43
41
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* by `loadBuiltQuiver` and by the Node entry (`./node.js`) when installing
|
|
47
|
-
* `fromDir` / `fromPackage`. Not part of the public API.
|
|
48
|
-
*/
|
|
49
|
-
static _fromLoader(name, catalog, loader) {
|
|
50
|
-
return new Quiver(name, catalog, loader);
|
|
42
|
+
static {
|
|
43
|
+
create = (name, catalog, loader) => new Quiver(name, catalog, loader);
|
|
51
44
|
}
|
|
52
45
|
/**
|
|
53
46
|
* Browser-safe factory. Loads build output from an HTTP/HTTPS URL.
|
|
54
47
|
*
|
|
55
48
|
* Origin-relative URLs (e.g. `/quivers/foo/`) are accepted in browser
|
|
56
49
|
* environments. `file://` URLs are rejected — to load build output from
|
|
57
|
-
* disk in Node, use `
|
|
58
|
-
* `@quillmark/quiver/node`.
|
|
50
|
+
* disk in Node, use `fromBuiltDir(path)` from `@quillmark/quiver/node`.
|
|
59
51
|
*
|
|
60
52
|
* Throws `transport_error` on network/HTTP failure, `quiver_invalid`
|
|
61
53
|
* on format errors.
|
|
62
54
|
*/
|
|
63
55
|
static async fromBuiltUrl(url) {
|
|
64
|
-
if (url.startsWith(
|
|
65
|
-
throw new QuiverError(
|
|
56
|
+
if (url.startsWith('file://')) {
|
|
57
|
+
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'.`);
|
|
66
58
|
}
|
|
67
|
-
const { HttpTransport } = await import(
|
|
68
|
-
const { loadBuiltQuiver } = await import(
|
|
59
|
+
const { HttpTransport } = await import('./transports/http-transport.js');
|
|
60
|
+
const { loadBuiltQuiver } = await import('./built-loader.js');
|
|
69
61
|
const transport = new HttpTransport(url);
|
|
70
62
|
return loadBuiltQuiver(transport);
|
|
71
63
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Browser-safe factory. Seeds the catalog from caller-provided manifest
|
|
74
|
-
* bytes — never fetches `latest.json` — then fetches bundles lazily and
|
|
75
|
-
* content-addressed, relative to `baseUrl`, like `fromBuiltUrl`. For SSR
|
|
76
|
-
* consumers that already hold the manifest at build time.
|
|
77
|
-
*
|
|
78
|
-
* Throws `quiver_invalid` on malformed manifest bytes, `transport_error`
|
|
79
|
-
* on a `file://` baseUrl or a later bundle fetch failure.
|
|
80
|
-
*/
|
|
81
|
-
static async fromManifest(baseUrl, manifestBytes) {
|
|
82
|
-
if (baseUrl.startsWith("file://")) {
|
|
83
|
-
throw new QuiverError("transport_error", `Quiver.fromManifest requires an http(s):// or origin-relative baseUrl; got "${baseUrl}". For local build output, use Quiver.fromBuiltDir from @quillmark/quiver/node.`);
|
|
84
|
-
}
|
|
85
|
-
const { HttpTransport } = await import("./transports/http-transport.js");
|
|
86
|
-
const { seedBuiltQuiver } = await import("./built-loader.js");
|
|
87
|
-
const transport = new HttpTransport(baseUrl);
|
|
88
|
-
return seedBuiltQuiver(transport, manifestBytes);
|
|
89
|
-
}
|
|
90
64
|
/** Returns all known quill names, sorted lexicographically. */
|
|
91
65
|
quillNames() {
|
|
92
66
|
return [...this.#catalog.keys()].sort();
|
|
@@ -104,11 +78,15 @@ export class Quiver {
|
|
|
104
78
|
* Selector forms: `name`, `name@x`, `name@x.y`, `name@x.y.z`. Picks the
|
|
105
79
|
* highest matching version in this quiver.
|
|
106
80
|
*
|
|
81
|
+
* Sync, and in-memory by construction rather than by implementation: every
|
|
82
|
+
* loader materializes the catalog as the quiver is built, and `QuiverLoader`
|
|
83
|
+
* carries one verb, `loadTree`, which resolution never reaches.
|
|
84
|
+
*
|
|
107
85
|
* Throws:
|
|
108
86
|
* - `invalid_ref` if ref fails parseQuillRef
|
|
109
87
|
* - `quill_not_found` if no version matches
|
|
110
88
|
*/
|
|
111
|
-
|
|
89
|
+
resolve(ref) {
|
|
112
90
|
const parsed = parseQuillRef(ref);
|
|
113
91
|
const versions = this.#catalog.get(parsed.name);
|
|
114
92
|
if (versions && versions.length > 0) {
|
|
@@ -121,7 +99,7 @@ export class Quiver {
|
|
|
121
99
|
return `${parsed.name}@${winner}`;
|
|
122
100
|
}
|
|
123
101
|
}
|
|
124
|
-
throw new QuiverError(
|
|
102
|
+
throw new QuiverError('quill_not_found', `No quill found for ref "${ref}" in quiver "${this.name}".`, { ref, quiverName: this.name });
|
|
125
103
|
}
|
|
126
104
|
/**
|
|
127
105
|
* Returns a `Quill` for a ref (selector or canonical).
|
|
@@ -136,6 +114,11 @@ export class Quiver {
|
|
|
136
114
|
* first. Materializes once and caches per canonical ref — concurrent calls
|
|
137
115
|
* coalesce into a single load.
|
|
138
116
|
*
|
|
117
|
+
* BORROWED, not handed over: the quill is the quiver's, shared with every
|
|
118
|
+
* other caller for the same ref, and lives as long as the quiver. Do not
|
|
119
|
+
* `free()` it: the next `getQuill` for that ref would hand out a freed
|
|
120
|
+
* handle. A caller wanting one of its own mints it from `.toTree()`.
|
|
121
|
+
*
|
|
139
122
|
* Throws:
|
|
140
123
|
* - `invalid_ref` if ref is malformed
|
|
141
124
|
* - `quill_not_found` if ref does not match any version in this quiver
|
|
@@ -143,7 +126,7 @@ export class Quiver {
|
|
|
143
126
|
* - propagates validation errors from Quill.fromTree() unchanged
|
|
144
127
|
*/
|
|
145
128
|
async getQuill(ref) {
|
|
146
|
-
const canonicalRef =
|
|
129
|
+
const canonicalRef = this.resolve(ref);
|
|
147
130
|
let entry = this.#quillCache.get(canonicalRef);
|
|
148
131
|
if (entry === undefined) {
|
|
149
132
|
entry = this.#materializeQuill(canonicalRef).catch((err) => {
|
|
@@ -155,70 +138,20 @@ export class Quiver {
|
|
|
155
138
|
return entry;
|
|
156
139
|
}
|
|
157
140
|
/**
|
|
158
|
-
* Internal: load tree
|
|
159
|
-
* propagate. On success the tree is evicted from `#treeCache` (the quill
|
|
160
|
-
* cache now holds the materialized result), so the tree is not retained
|
|
161
|
-
* past materialization. On `Quill.fromTree` failure the tree stays cached,
|
|
162
|
-
* so a retry skips the refetch.
|
|
163
|
-
*/
|
|
164
|
-
async #materializeQuill(canonicalRef) {
|
|
165
|
-
const entry = this.#getTreeCached(canonicalRef);
|
|
166
|
-
const tree = await entry;
|
|
167
|
-
const quill = Quill.fromTree(tree);
|
|
168
|
-
// Evict by identity: only drop the entry if it is still the promise this
|
|
169
|
-
// materialization consumed (a concurrent refetch may have replaced it).
|
|
170
|
-
if (this.#treeCache.get(canonicalRef) === entry) {
|
|
171
|
-
this.#treeCache.delete(canonicalRef);
|
|
172
|
-
}
|
|
173
|
-
return quill;
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Internal: tree cache reader. On miss, fetches via `#loadTree` and stores
|
|
177
|
-
* the in-flight Promise. On rejection, evicts so a retry can succeed.
|
|
178
|
-
*/
|
|
179
|
-
#getTreeCached(canonicalRef) {
|
|
180
|
-
let entry = this.#treeCache.get(canonicalRef);
|
|
181
|
-
if (entry === undefined) {
|
|
182
|
-
const at = canonicalRef.indexOf("@");
|
|
183
|
-
const name = canonicalRef.slice(0, at);
|
|
184
|
-
const version = canonicalRef.slice(at + 1);
|
|
185
|
-
entry = this.#loadTree(name, version).catch((err) => {
|
|
186
|
-
this.#treeCache.delete(canonicalRef);
|
|
187
|
-
throw err;
|
|
188
|
-
});
|
|
189
|
-
this.#treeCache.set(canonicalRef, entry);
|
|
190
|
-
}
|
|
191
|
-
return entry;
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Internal: validates name/version against the catalog, then delegates to
|
|
195
|
-
* the loader. Returns `Map<string, Uint8Array>` suitable for
|
|
196
|
-
* `Quill.fromTree(tree)`. Does NOT cache — caching lives in `#getTreeCached`.
|
|
141
|
+
* Internal: load the tree + construct via Quill.fromTree. Errors propagate.
|
|
197
142
|
*
|
|
198
|
-
*
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (!versions || !versions.includes(version)) {
|
|
203
|
-
throw new QuiverError("transport_error", `Quill "${name}@${version}" not found in quiver "${this.name}"`, { quiverName: this.name, version, ref: `${name}@${version}` });
|
|
204
|
-
}
|
|
205
|
-
return this.#loader.loadTree(name, version);
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Prefetches the tree for every quill version in this quiver. Fail-fast.
|
|
143
|
+
* The tree is held only for the length of this call: the quill cache holds the
|
|
144
|
+
* materialized result, and it is the one cache. A second tree cache beside it
|
|
145
|
+
* would buy a retry after a `Quill.fromTree` throw its refetch, which is a
|
|
146
|
+
* saved round-trip on the path where the quill is broken.
|
|
209
147
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* skipping the fetch (the tree then evicts as its quill materializes).
|
|
213
|
-
* Rejects on the first fetch failure.
|
|
148
|
+
* Every caller arrives through `resolve` or the catalog itself, so the ref is
|
|
149
|
+
* catalog-backed by construction and the loader needs no gate.
|
|
214
150
|
*/
|
|
215
|
-
async
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
promises.push(this.#getTreeCached(`${name}@${version}`));
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
await Promise.all(promises);
|
|
151
|
+
async #materializeQuill(canonicalRef) {
|
|
152
|
+
const at = canonicalRef.indexOf('@');
|
|
153
|
+
const tree = await this.#loader.loadTree(canonicalRef.slice(0, at), canonicalRef.slice(at + 1));
|
|
154
|
+
return Quill.fromTree(tree);
|
|
223
155
|
}
|
|
224
156
|
}
|
|
157
|
+
export { create as createQuiver };
|
package/dist/ref.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ export interface ParsedQuillRef {
|
|
|
7
7
|
selectorDepth?: 1 | 2 | 3;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Throws QuiverError('invalid_ref') on malformed input.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Throws QuiverError('invalid_ref') on malformed input. The name is
|
|
11
|
+
* `[A-Za-z0-9_-]+`; the selector is `x`, `x.y` or `x.y.z`, digits only, with no
|
|
12
|
+
* ranges or operators — a quiver resolves a prefix, it does not solve a range.
|
|
13
13
|
*/
|
|
14
14
|
export declare function parseQuillRef(ref: string): ParsedQuillRef;
|
package/dist/ref.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { QuiverError } from
|
|
1
|
+
import { QuiverError } from './errors.js';
|
|
2
2
|
const NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
3
3
|
const SELECTOR_RE = /^\d+(\.\d+){0,2}$/;
|
|
4
4
|
/**
|
|
5
|
-
* Throws QuiverError('invalid_ref') on malformed input.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Throws QuiverError('invalid_ref') on malformed input. The name is
|
|
6
|
+
* `[A-Za-z0-9_-]+`; the selector is `x`, `x.y` or `x.y.z`, digits only, with no
|
|
7
|
+
* ranges or operators — a quiver resolves a prefix, it does not solve a range.
|
|
8
8
|
*/
|
|
9
9
|
export function parseQuillRef(ref) {
|
|
10
10
|
if (!ref) {
|
|
11
|
-
throw new QuiverError(
|
|
11
|
+
throw new QuiverError('invalid_ref', `Invalid ref: empty string`, { ref });
|
|
12
12
|
}
|
|
13
|
-
const atIndex = ref.indexOf(
|
|
13
|
+
const atIndex = ref.indexOf('@');
|
|
14
14
|
if (atIndex === 0) {
|
|
15
15
|
// Starts with @, no name
|
|
16
|
-
throw new QuiverError(
|
|
16
|
+
throw new QuiverError('invalid_ref', `Invalid ref: missing name in "${ref}"`, { ref });
|
|
17
17
|
}
|
|
18
18
|
if (atIndex === -1) {
|
|
19
19
|
// No selector — just a name
|
|
20
20
|
const name = ref;
|
|
21
21
|
if (!NAME_RE.test(name)) {
|
|
22
|
-
throw new QuiverError(
|
|
22
|
+
throw new QuiverError('invalid_ref', `Invalid ref: name "${name}" contains invalid characters`, { ref });
|
|
23
23
|
}
|
|
24
24
|
return { name };
|
|
25
25
|
}
|
|
@@ -27,18 +27,20 @@ export function parseQuillRef(ref) {
|
|
|
27
27
|
const name = ref.slice(0, atIndex);
|
|
28
28
|
const selector = ref.slice(atIndex + 1);
|
|
29
29
|
if (!name) {
|
|
30
|
-
throw new QuiverError(
|
|
30
|
+
throw new QuiverError('invalid_ref', `Invalid ref: missing name in "${ref}"`, { ref });
|
|
31
31
|
}
|
|
32
32
|
if (!selector) {
|
|
33
|
-
throw new QuiverError(
|
|
33
|
+
throw new QuiverError('invalid_ref', `Invalid ref: missing selector after "@" in "${ref}"`, {
|
|
34
|
+
ref
|
|
35
|
+
});
|
|
34
36
|
}
|
|
35
37
|
if (!NAME_RE.test(name)) {
|
|
36
|
-
throw new QuiverError(
|
|
38
|
+
throw new QuiverError('invalid_ref', `Invalid ref: name "${name}" contains invalid characters`, { ref });
|
|
37
39
|
}
|
|
38
40
|
if (!SELECTOR_RE.test(selector)) {
|
|
39
|
-
throw new QuiverError(
|
|
41
|
+
throw new QuiverError('invalid_ref', `Invalid ref: selector "${selector}" is not a valid semver selector (only x, x.y, x.y.z with digits allowed)`, { ref });
|
|
40
42
|
}
|
|
41
|
-
const parts = selector.split(
|
|
43
|
+
const parts = selector.split('.');
|
|
42
44
|
const depth = parts.length;
|
|
43
45
|
return { name, selector, selectorDepth: depth };
|
|
44
46
|
}
|