@quillmark/quiver 0.1.1 → 0.3.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 +156 -74
- package/README.md +65 -17
- package/dist/assert-node.d.ts +5 -0
- package/dist/assert-node.js +12 -0
- package/dist/build.d.ts +25 -0
- package/dist/build.js +120 -0
- package/dist/built-loader.d.ts +27 -0
- package/dist/built-loader.js +232 -0
- package/dist/bundle.d.ts +13 -0
- package/dist/bundle.js +33 -0
- package/dist/engine-types.d.ts +26 -0
- package/dist/engine-types.js +20 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +5 -0
- package/dist/quiver-yaml.d.ts +22 -0
- package/dist/quiver-yaml.js +63 -0
- package/dist/quiver.d.ts +82 -0
- package/dist/quiver.js +132 -0
- package/dist/ref.d.ts +14 -0
- package/dist/ref.js +44 -0
- package/dist/registry.d.ts +39 -0
- package/dist/registry.js +115 -0
- package/dist/semver.d.ts +8 -0
- package/dist/semver.js +44 -0
- package/dist/source-loader.d.ts +42 -0
- package/dist/source-loader.js +179 -0
- package/dist/testing.d.ts +28 -0
- package/dist/testing.js +54 -0
- package/dist/transports/http-transport.d.ts +12 -0
- package/dist/transports/http-transport.js +33 -0
- package/package.json +9 -11
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-quiver loader — browser-safe at module level.
|
|
3
|
+
* Internal; not exported from index.ts.
|
|
4
|
+
*
|
|
5
|
+
* Exposes:
|
|
6
|
+
* - BuiltTransport interface (implemented by HttpTransport)
|
|
7
|
+
* - loadBuiltQuiver(transport) → Quiver
|
|
8
|
+
*
|
|
9
|
+
* NO static node: imports — this module is safe to load in browser contexts.
|
|
10
|
+
*/
|
|
11
|
+
import { Quiver } from "./quiver.js";
|
|
12
|
+
/**
|
|
13
|
+
* Transport abstraction: fetch raw bytes by relative path within the packed
|
|
14
|
+
* artifact. Sole implementation is HttpTransport (browser + Node).
|
|
15
|
+
*/
|
|
16
|
+
export interface BuiltTransport {
|
|
17
|
+
fetchBytes(relativePath: string): Promise<Uint8Array>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load a build-output quiver via the given transport.
|
|
21
|
+
*
|
|
22
|
+
* 1. Fetches Quiver.json (pointer) and parses it.
|
|
23
|
+
* 2. Fetches the manifest file it points to and validates it.
|
|
24
|
+
* 3. Builds a catalog from manifest entries (versions sorted descending).
|
|
25
|
+
* 4. Returns a Quiver instance backed by a BuiltLoader.
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadBuiltQuiver(transport: BuiltTransport): Promise<Quiver>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-quiver loader — browser-safe at module level.
|
|
3
|
+
* Internal; not exported from index.ts.
|
|
4
|
+
*
|
|
5
|
+
* Exposes:
|
|
6
|
+
* - BuiltTransport interface (implemented by HttpTransport)
|
|
7
|
+
* - loadBuiltQuiver(transport) → Quiver
|
|
8
|
+
*
|
|
9
|
+
* NO static node: imports — this module is safe to load in browser contexts.
|
|
10
|
+
*/
|
|
11
|
+
import { QuiverError } from "./errors.js";
|
|
12
|
+
import { unpackFiles } from "./bundle.js";
|
|
13
|
+
import { isCanonicalSemver, compareSemver } from "./semver.js";
|
|
14
|
+
import { Quiver } from "./quiver.js";
|
|
15
|
+
// ─── Path validation ──────────────────────────────────────────────────────────
|
|
16
|
+
const MANIFEST_FILENAME_RE = /^manifest\.[0-9a-f]+\.json$/;
|
|
17
|
+
const BUNDLE_FILENAME_RE = /^[A-Za-z0-9_.-]+@[0-9]+\.[0-9]+\.[0-9]+\.[0-9a-f]+\.zip$/;
|
|
18
|
+
const FONT_HASH_RE = /^[0-9a-f]{32}$/;
|
|
19
|
+
function validateManifestFileName(name) {
|
|
20
|
+
if (!MANIFEST_FILENAME_RE.test(name)) {
|
|
21
|
+
throw new QuiverError("quiver_invalid", `Pointer manifest filename is invalid: "${name}"`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function validateBundleFileName(bundle, context) {
|
|
25
|
+
if (!BUNDLE_FILENAME_RE.test(bundle)) {
|
|
26
|
+
throw new QuiverError("quiver_invalid", `${context}: bundle filename is invalid: "${bundle}"`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function validateFontHash(hash, context) {
|
|
30
|
+
if (!FONT_HASH_RE.test(hash)) {
|
|
31
|
+
throw new QuiverError("quiver_invalid", `${context}: font hash is invalid: "${hash}"`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// ─── BuiltLoader implementation ─────────────────────────────────────────────
|
|
35
|
+
class BuiltLoader {
|
|
36
|
+
transport;
|
|
37
|
+
index;
|
|
38
|
+
/** Font byte cache: hash → in-flight or resolved Promise. */
|
|
39
|
+
fontCache = new Map();
|
|
40
|
+
constructor(transport,
|
|
41
|
+
/** Index map from "name@version" to its manifest entry. */
|
|
42
|
+
index) {
|
|
43
|
+
this.transport = transport;
|
|
44
|
+
this.index = index;
|
|
45
|
+
}
|
|
46
|
+
async loadTree(name, version) {
|
|
47
|
+
const entry = this.index.get(`${name}@${version}`);
|
|
48
|
+
// If entry is missing, the outer Quiver.loadTree gate already validated
|
|
49
|
+
// that this name/version is in the catalog; trust that gate and fall
|
|
50
|
+
// through. The transport will surface a transport_error naturally.
|
|
51
|
+
// (Defensive: this should not be reachable in normal operation.)
|
|
52
|
+
if (!entry) {
|
|
53
|
+
throw new QuiverError("transport_error", `Quill "${name}@${version}" not found in built-quiver manifest`, { version, ref: `${name}@${version}` });
|
|
54
|
+
}
|
|
55
|
+
// 1. Fetch + unpack bundle zip.
|
|
56
|
+
const zipBytes = await this.transport.fetchBytes(entry.bundle);
|
|
57
|
+
const files = unpackFiles(zipBytes);
|
|
58
|
+
// 2. Rehydrate fonts from store (coalesced).
|
|
59
|
+
const fontEntries = Object.entries(entry.fonts);
|
|
60
|
+
await Promise.all(fontEntries.map(async ([path, hash]) => {
|
|
61
|
+
files[path] = await this.fetchFont(hash);
|
|
62
|
+
}));
|
|
63
|
+
// 3. Convert to Map.
|
|
64
|
+
return new Map(Object.entries(files));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Fetch a font by hash from store/<hash>, coalescing concurrent requests for
|
|
68
|
+
* the same hash into a single fetch. On error, removes the cache entry so
|
|
69
|
+
* callers can retry.
|
|
70
|
+
*/
|
|
71
|
+
fetchFont(hash) {
|
|
72
|
+
let promise = this.fontCache.get(hash);
|
|
73
|
+
if (!promise) {
|
|
74
|
+
promise = this.transport
|
|
75
|
+
.fetchBytes(`store/${hash}`)
|
|
76
|
+
.catch((err) => {
|
|
77
|
+
this.fontCache.delete(hash);
|
|
78
|
+
throw err;
|
|
79
|
+
});
|
|
80
|
+
this.fontCache.set(hash, promise);
|
|
81
|
+
}
|
|
82
|
+
return promise;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ─── Pointer + manifest validation helpers ────────────────────────────────────
|
|
86
|
+
function assertNoUnknownKeys(obj, allowed, context) {
|
|
87
|
+
for (const key of Object.keys(obj)) {
|
|
88
|
+
if (!allowed.includes(key)) {
|
|
89
|
+
throw new QuiverError("quiver_invalid", `${context}: unknown field "${key}"`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function parsePointer(raw) {
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(raw);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new QuiverError("quiver_invalid", "Quiver.json contains invalid JSON");
|
|
100
|
+
}
|
|
101
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
102
|
+
throw new QuiverError("quiver_invalid", "Quiver.json must be a JSON object");
|
|
103
|
+
}
|
|
104
|
+
const obj = parsed;
|
|
105
|
+
assertNoUnknownKeys(obj, ["manifest"], "Quiver.json");
|
|
106
|
+
if (typeof obj["manifest"] !== "string" || obj["manifest"].length === 0) {
|
|
107
|
+
throw new QuiverError("quiver_invalid", 'Quiver.json must have a non-empty string "manifest" field');
|
|
108
|
+
}
|
|
109
|
+
return obj["manifest"];
|
|
110
|
+
}
|
|
111
|
+
function parseManifest(raw) {
|
|
112
|
+
let parsed;
|
|
113
|
+
try {
|
|
114
|
+
parsed = JSON.parse(raw);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new QuiverError("quiver_invalid", "Manifest file contains invalid JSON");
|
|
118
|
+
}
|
|
119
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
120
|
+
throw new QuiverError("quiver_invalid", "Manifest must be a JSON object");
|
|
121
|
+
}
|
|
122
|
+
const obj = parsed;
|
|
123
|
+
assertNoUnknownKeys(obj, ["version", "name", "quills"], "manifest");
|
|
124
|
+
if (obj["version"] !== 1) {
|
|
125
|
+
throw new QuiverError("quiver_invalid", `Manifest version must be 1, got ${String(obj["version"])}`);
|
|
126
|
+
}
|
|
127
|
+
if (typeof obj["name"] !== "string" || obj["name"].length === 0) {
|
|
128
|
+
throw new QuiverError("quiver_invalid", 'Manifest must have a non-empty string "name" field');
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(obj["quills"])) {
|
|
131
|
+
throw new QuiverError("quiver_invalid", 'Manifest must have a "quills" array');
|
|
132
|
+
}
|
|
133
|
+
const quills = [];
|
|
134
|
+
for (let i = 0; i < obj["quills"].length; i++) {
|
|
135
|
+
const entry = obj["quills"][i];
|
|
136
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
137
|
+
throw new QuiverError("quiver_invalid", `manifest.quills[${i}] must be an object`);
|
|
138
|
+
}
|
|
139
|
+
const e = entry;
|
|
140
|
+
assertNoUnknownKeys(e, ["name", "version", "bundle", "fonts"], `manifest.quills[${i}]`);
|
|
141
|
+
if (typeof e["name"] !== "string" || e["name"].length === 0) {
|
|
142
|
+
throw new QuiverError("quiver_invalid", `manifest.quills[${i}].name must be a non-empty string`);
|
|
143
|
+
}
|
|
144
|
+
if (typeof e["version"] !== "string" ||
|
|
145
|
+
!isCanonicalSemver(e["version"])) {
|
|
146
|
+
throw new QuiverError("quiver_invalid", `manifest.quills[${i}].version must be canonical semver (x.y.z), got "${String(e["version"])}"`);
|
|
147
|
+
}
|
|
148
|
+
if (typeof e["bundle"] !== "string" ||
|
|
149
|
+
e["bundle"].length === 0) {
|
|
150
|
+
throw new QuiverError("quiver_invalid", `manifest.quills[${i}].bundle must be a non-empty string`);
|
|
151
|
+
}
|
|
152
|
+
validateBundleFileName(e["bundle"], `manifest.quills[${i}].bundle`);
|
|
153
|
+
if (typeof e["fonts"] !== "object" ||
|
|
154
|
+
e["fonts"] === null ||
|
|
155
|
+
Array.isArray(e["fonts"])) {
|
|
156
|
+
throw new QuiverError("quiver_invalid", `manifest.quills[${i}].fonts must be an object`);
|
|
157
|
+
}
|
|
158
|
+
const fonts = e["fonts"];
|
|
159
|
+
for (const [k, v] of Object.entries(fonts)) {
|
|
160
|
+
if (typeof v !== "string") {
|
|
161
|
+
throw new QuiverError("quiver_invalid", `manifest.quills[${i}].fonts["${k}"] must be a string`);
|
|
162
|
+
}
|
|
163
|
+
validateFontHash(v, `manifest.quills[${i}].fonts["${k}"]`);
|
|
164
|
+
}
|
|
165
|
+
quills.push({
|
|
166
|
+
name: e["name"],
|
|
167
|
+
version: e["version"],
|
|
168
|
+
bundle: e["bundle"],
|
|
169
|
+
fonts: fonts,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
version: 1,
|
|
174
|
+
name: obj["name"],
|
|
175
|
+
quills,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// ─── Main entry point ─────────────────────────────────────────────────────────
|
|
179
|
+
/**
|
|
180
|
+
* Load a build-output quiver via the given transport.
|
|
181
|
+
*
|
|
182
|
+
* 1. Fetches Quiver.json (pointer) and parses it.
|
|
183
|
+
* 2. Fetches the manifest file it points to and validates it.
|
|
184
|
+
* 3. Builds a catalog from manifest entries (versions sorted descending).
|
|
185
|
+
* 4. Returns a Quiver instance backed by a BuiltLoader.
|
|
186
|
+
*/
|
|
187
|
+
export async function loadBuiltQuiver(transport) {
|
|
188
|
+
// 1. Fetch and parse pointer.
|
|
189
|
+
let pointerBytes;
|
|
190
|
+
try {
|
|
191
|
+
pointerBytes = await transport.fetchBytes("Quiver.json");
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
if (err instanceof QuiverError)
|
|
195
|
+
throw err;
|
|
196
|
+
throw new QuiverError("transport_error", `Failed to fetch Quiver.json: ${err.message}`, { cause: err });
|
|
197
|
+
}
|
|
198
|
+
const manifestFileName = parsePointer(new TextDecoder().decode(pointerBytes));
|
|
199
|
+
validateManifestFileName(manifestFileName);
|
|
200
|
+
// 2. Fetch and parse manifest.
|
|
201
|
+
let manifestBytes;
|
|
202
|
+
try {
|
|
203
|
+
manifestBytes = await transport.fetchBytes(manifestFileName);
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
if (err instanceof QuiverError)
|
|
207
|
+
throw err;
|
|
208
|
+
throw new QuiverError("transport_error", `Failed to fetch manifest "${manifestFileName}": ${err.message}`, { cause: err });
|
|
209
|
+
}
|
|
210
|
+
const manifest = parseManifest(new TextDecoder().decode(manifestBytes));
|
|
211
|
+
// 3. Build catalog: name → versions sorted descending.
|
|
212
|
+
// Also build index map: "name@version" → entry (with duplicate detection).
|
|
213
|
+
const catalogRaw = new Map();
|
|
214
|
+
const index = new Map();
|
|
215
|
+
for (const entry of manifest.quills) {
|
|
216
|
+
const key = `${entry.name}@${entry.version}`;
|
|
217
|
+
if (index.has(key)) {
|
|
218
|
+
throw new QuiverError("quiver_invalid", `Duplicate quill entry in manifest: "${key}"`);
|
|
219
|
+
}
|
|
220
|
+
index.set(key, entry);
|
|
221
|
+
const versions = catalogRaw.get(entry.name) ?? [];
|
|
222
|
+
versions.push(entry.version);
|
|
223
|
+
catalogRaw.set(entry.name, versions);
|
|
224
|
+
}
|
|
225
|
+
for (const [, versions] of catalogRaw) {
|
|
226
|
+
versions.sort((a, b) => compareSemver(b, a));
|
|
227
|
+
}
|
|
228
|
+
// 4. Build loader.
|
|
229
|
+
const loader = new BuiltLoader(transport, index);
|
|
230
|
+
// 5. Return Quiver via internal factory.
|
|
231
|
+
return Quiver._fromLoader(manifest.name, catalogRaw, loader);
|
|
232
|
+
}
|
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zip utilities — browser-safe (uses fflate only).
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Pack a flat file map into a deterministic zip.
|
|
6
|
+
* Keys are sorted before zipping so insertion order doesn't affect output.
|
|
7
|
+
*/
|
|
8
|
+
export declare function packFiles(files: Record<string, Uint8Array>): Uint8Array;
|
|
9
|
+
/**
|
|
10
|
+
* Unpack a zip into a flat file map.
|
|
11
|
+
* Returns { path: Uint8Array } for every file entry in the archive.
|
|
12
|
+
*/
|
|
13
|
+
export declare function unpackFiles(data: Uint8Array): Record<string, Uint8Array>;
|
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zip utilities — browser-safe (uses fflate only).
|
|
3
|
+
*/
|
|
4
|
+
import { zipSync, unzipSync } from "fflate";
|
|
5
|
+
/**
|
|
6
|
+
* Fixed epoch mtime for deterministic zip output.
|
|
7
|
+
* All entries get this timestamp so byte-identical inputs → byte-identical zips.
|
|
8
|
+
*/
|
|
9
|
+
const ZIP_EPOCH = new Date(Date.UTC(1980, 0, 1));
|
|
10
|
+
/**
|
|
11
|
+
* Pack a flat file map into a deterministic zip.
|
|
12
|
+
* Keys are sorted before zipping so insertion order doesn't affect output.
|
|
13
|
+
*/
|
|
14
|
+
export function packFiles(files) {
|
|
15
|
+
const sorted = Object.keys(files).sort();
|
|
16
|
+
const input = {};
|
|
17
|
+
for (const key of sorted) {
|
|
18
|
+
input[key] = [files[key], { mtime: ZIP_EPOCH }];
|
|
19
|
+
}
|
|
20
|
+
return zipSync(input, { level: 6 });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Unpack a zip into a flat file map.
|
|
24
|
+
* Returns { path: Uint8Array } for every file entry in the archive.
|
|
25
|
+
*/
|
|
26
|
+
export function unpackFiles(data) {
|
|
27
|
+
const raw = unzipSync(data);
|
|
28
|
+
const result = {};
|
|
29
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
30
|
+
result[key] = value;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structural types matching @quillmark/wasm >=0.57.0 (verified against 0.59.0-rc.2).
|
|
3
|
+
*
|
|
4
|
+
* Shape:
|
|
5
|
+
* class Quillmark { quill(tree: Map<string, Uint8Array>): Quill }
|
|
6
|
+
* class Quill { render(doc, opts?): RenderResult; open(doc): RenderSession }
|
|
7
|
+
*
|
|
8
|
+
* Note: as of 0.59.0-rc.2 the first arg to render/open is a `Document` instance
|
|
9
|
+
* (from `Document.fromMarkdown(...)`), not the old `ParsedDocument` interface.
|
|
10
|
+
* Quiver keeps the arg typed as `unknown` so consumers of either shape (and
|
|
11
|
+
* test doubles) satisfy the contract structurally.
|
|
12
|
+
*
|
|
13
|
+
* These types are INTERNAL — never re-exported from index.ts. They exist so
|
|
14
|
+
* registry.ts never imports from @quillmark/wasm directly and so test doubles
|
|
15
|
+
* can satisfy the contract without pulling the real WASM module.
|
|
16
|
+
*
|
|
17
|
+
* Call-site note: Quiver never invokes `render` or `open` itself; consumers do
|
|
18
|
+
* after `getQuill()`. The loose `unknown` parameter typing is intentional.
|
|
19
|
+
*/
|
|
20
|
+
export interface QuillmarkLike {
|
|
21
|
+
quill(tree: Map<string, Uint8Array>): QuillLike;
|
|
22
|
+
}
|
|
23
|
+
export interface QuillLike {
|
|
24
|
+
render(doc: unknown, opts?: unknown): unknown;
|
|
25
|
+
open?: (doc: unknown) => unknown;
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structural types matching @quillmark/wasm >=0.57.0 (verified against 0.59.0-rc.2).
|
|
3
|
+
*
|
|
4
|
+
* Shape:
|
|
5
|
+
* class Quillmark { quill(tree: Map<string, Uint8Array>): Quill }
|
|
6
|
+
* class Quill { render(doc, opts?): RenderResult; open(doc): RenderSession }
|
|
7
|
+
*
|
|
8
|
+
* Note: as of 0.59.0-rc.2 the first arg to render/open is a `Document` instance
|
|
9
|
+
* (from `Document.fromMarkdown(...)`), not the old `ParsedDocument` interface.
|
|
10
|
+
* Quiver keeps the arg typed as `unknown` so consumers of either shape (and
|
|
11
|
+
* test doubles) satisfy the contract structurally.
|
|
12
|
+
*
|
|
13
|
+
* These types are INTERNAL — never re-exported from index.ts. They exist so
|
|
14
|
+
* registry.ts never imports from @quillmark/wasm directly and so test doubles
|
|
15
|
+
* can satisfy the contract without pulling the real WASM module.
|
|
16
|
+
*
|
|
17
|
+
* Call-site note: Quiver never invokes `render` or `open` itself; consumers do
|
|
18
|
+
* after `getQuill()`. The loose `unknown` parameter typing is intentional.
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type QuiverErrorCode = "invalid_ref" | "quill_not_found" | "quiver_invalid" | "transport_error" | "quiver_collision";
|
|
2
|
+
export declare class QuiverError extends Error {
|
|
3
|
+
readonly code: QuiverErrorCode;
|
|
4
|
+
/** Offending ref string, when available. */
|
|
5
|
+
readonly ref?: string;
|
|
6
|
+
/** Offending version, when available. */
|
|
7
|
+
readonly version?: string;
|
|
8
|
+
/** Quiver `name` from Quiver.yaml, when available. */
|
|
9
|
+
readonly quiverName?: string;
|
|
10
|
+
constructor(code: QuiverErrorCode, message: string, options?: {
|
|
11
|
+
ref?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
quiverName?: string;
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
});
|
|
16
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class QuiverError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
/** Offending ref string, when available. */
|
|
4
|
+
ref;
|
|
5
|
+
/** Offending version, when available. */
|
|
6
|
+
version;
|
|
7
|
+
/** Quiver `name` from Quiver.yaml, when available. */
|
|
8
|
+
quiverName;
|
|
9
|
+
constructor(code, message, options) {
|
|
10
|
+
super(message, { cause: options?.cause });
|
|
11
|
+
this.name = "QuiverError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.ref = options?.ref;
|
|
14
|
+
this.version = options?.version;
|
|
15
|
+
this.quiverName = options?.quiverName;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { QuiverError } from "./errors.js";
|
|
2
|
+
export type { QuiverErrorCode } from "./errors.js";
|
|
3
|
+
export { Quiver } from "./quiver.js";
|
|
4
|
+
export { QuiverRegistry } from "./registry.js";
|
|
5
|
+
export type { BuildOptions } from "./build.js";
|
|
6
|
+
export type { QuillmarkLike, QuillLike } from "./engine-types.js";
|
package/dist/index.js
ADDED
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./index.js";
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal parser/validator for Quiver.yaml files.
|
|
3
|
+
*
|
|
4
|
+
* Uses the `yaml` npm package for robust YAML parsing. Quiver.yaml has a
|
|
5
|
+
* simple two-field schema (name, description), but using a proper YAML parser
|
|
6
|
+
* ensures correct handling of quoting, escaping, and multi-line strings.
|
|
7
|
+
*/
|
|
8
|
+
export interface QuiverMeta {
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parses and validates Quiver.yaml contents.
|
|
14
|
+
*
|
|
15
|
+
* Throws `QuiverError('quiver_invalid')` on:
|
|
16
|
+
* - YAML parse failure
|
|
17
|
+
* - Missing or non-string `name`
|
|
18
|
+
* - `name` fails charset validation [A-Za-z0-9_-]+
|
|
19
|
+
* - Unknown fields (strict)
|
|
20
|
+
* - `description` present but not a string
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseQuiverYaml(raw: string | Uint8Array): QuiverMeta;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal parser/validator for Quiver.yaml files.
|
|
3
|
+
*
|
|
4
|
+
* Uses the `yaml` npm package for robust YAML parsing. Quiver.yaml has a
|
|
5
|
+
* simple two-field schema (name, description), but using a proper YAML parser
|
|
6
|
+
* ensures correct handling of quoting, escaping, and multi-line strings.
|
|
7
|
+
*/
|
|
8
|
+
import { parse as parseYaml } from "yaml";
|
|
9
|
+
import { QuiverError } from "./errors.js";
|
|
10
|
+
const NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
11
|
+
const KNOWN_FIELDS = new Set(["name", "description"]);
|
|
12
|
+
/**
|
|
13
|
+
* Parses and validates Quiver.yaml contents.
|
|
14
|
+
*
|
|
15
|
+
* Throws `QuiverError('quiver_invalid')` on:
|
|
16
|
+
* - YAML parse failure
|
|
17
|
+
* - Missing or non-string `name`
|
|
18
|
+
* - `name` fails charset validation [A-Za-z0-9_-]+
|
|
19
|
+
* - Unknown fields (strict)
|
|
20
|
+
* - `description` present but not a string
|
|
21
|
+
*/
|
|
22
|
+
export function parseQuiverYaml(raw) {
|
|
23
|
+
const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
|
|
24
|
+
let parsed;
|
|
25
|
+
try {
|
|
26
|
+
parsed = parseYaml(text);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
throw new QuiverError("quiver_invalid", `Quiver.yaml: YAML parse failure — ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
30
|
+
}
|
|
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
|
+
}
|
|
34
|
+
const doc = parsed;
|
|
35
|
+
// Check for unknown fields (strict mode)
|
|
36
|
+
for (const key of Object.keys(doc)) {
|
|
37
|
+
if (!KNOWN_FIELDS.has(key)) {
|
|
38
|
+
throw new QuiverError("quiver_invalid", `Quiver.yaml: unknown field "${key}" — only "name" and "description" are valid in V1`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Validate `name`
|
|
42
|
+
if (!("name" in doc)) {
|
|
43
|
+
throw new QuiverError("quiver_invalid", `Quiver.yaml: required field "name" is missing`);
|
|
44
|
+
}
|
|
45
|
+
if (typeof doc["name"] !== "string") {
|
|
46
|
+
throw new QuiverError("quiver_invalid", `Quiver.yaml: "name" must be a string, got ${typeof doc["name"]}`);
|
|
47
|
+
}
|
|
48
|
+
const name = doc["name"];
|
|
49
|
+
if (!NAME_RE.test(name)) {
|
|
50
|
+
throw new QuiverError("quiver_invalid", `Quiver.yaml: "name" value "${name}" contains invalid characters — only [A-Za-z0-9_-] are allowed`);
|
|
51
|
+
}
|
|
52
|
+
// Validate optional `description`
|
|
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
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const meta = { name };
|
|
59
|
+
if (typeof doc["description"] === "string") {
|
|
60
|
+
meta.description = doc["description"];
|
|
61
|
+
}
|
|
62
|
+
return meta;
|
|
63
|
+
}
|
package/dist/quiver.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quiver — primary runtime abstraction for a collection of quills.
|
|
3
|
+
*
|
|
4
|
+
* Polymorphism via composition: internally stores a pluggable loader
|
|
5
|
+
* (either source-backed or build-output-backed).
|
|
6
|
+
*/
|
|
7
|
+
import type { BuildOptions } from "./build.js";
|
|
8
|
+
/** @internal Internal loader strategy: source or build output. */
|
|
9
|
+
export interface QuiverLoader {
|
|
10
|
+
loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
|
|
11
|
+
}
|
|
12
|
+
export declare class Quiver {
|
|
13
|
+
#private;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
/**
|
|
16
|
+
* Private constructor — use static factory methods.
|
|
17
|
+
* TS prevents external `new Quiver(...)` at compile time.
|
|
18
|
+
* Static methods inside can still call it.
|
|
19
|
+
*/
|
|
20
|
+
private constructor();
|
|
21
|
+
/** @internal Used by loadBuiltQuiver. Not part of the public API. */
|
|
22
|
+
static _fromLoader(name: string, catalog: Map<string, string[]>, loader: QuiverLoader): Quiver;
|
|
23
|
+
/**
|
|
24
|
+
* Node-only factory. Resolves an npm specifier against `node_modules` and
|
|
25
|
+
* loads the source layout at the package root.
|
|
26
|
+
*
|
|
27
|
+
* The resolved package must have `Quiver.yaml` at its root.
|
|
28
|
+
*
|
|
29
|
+
* Throws `transport_error` on resolution/I/O failure, `quiver_invalid`
|
|
30
|
+
* on schema violations.
|
|
31
|
+
*/
|
|
32
|
+
static fromPackage(specifier: string): Promise<Quiver>;
|
|
33
|
+
/**
|
|
34
|
+
* Node-only factory. Reads a Source Quiver from a local directory containing
|
|
35
|
+
* `Quiver.yaml` and `quills/<name>/<version>/Quill.yaml` entries.
|
|
36
|
+
*
|
|
37
|
+
* Also accepts `import.meta.url`-style `file://` URLs as a convenience for
|
|
38
|
+
* tests; the URL's parent directory is used as the source root.
|
|
39
|
+
*
|
|
40
|
+
* Throws `quiver_invalid` on schema violations, `transport_error` on I/O failure.
|
|
41
|
+
*/
|
|
42
|
+
static fromDir(pathOrFileUrl: string): Promise<Quiver>;
|
|
43
|
+
/**
|
|
44
|
+
* Browser-safe factory. Loads build output from an HTTP/HTTPS URL.
|
|
45
|
+
*
|
|
46
|
+
* Origin-relative URLs (e.g. `/quivers/foo/`) are accepted in browser
|
|
47
|
+
* environments. `file://` URLs are rejected — local build output is
|
|
48
|
+
* not loadable in V1; serve over HTTP or use `fromPackage`/`fromDir`
|
|
49
|
+
* against the source.
|
|
50
|
+
*
|
|
51
|
+
* Throws `transport_error` on network/HTTP failure, `quiver_invalid`
|
|
52
|
+
* on format errors.
|
|
53
|
+
*/
|
|
54
|
+
static fromBuilt(url: string): Promise<Quiver>;
|
|
55
|
+
/** Returns all known quill names, sorted lexicographically. */
|
|
56
|
+
quillNames(): string[];
|
|
57
|
+
/**
|
|
58
|
+
* Returns all canonical versions for a given quill name, sorted descending.
|
|
59
|
+
* Returns an empty array if the quill name is not in the catalog.
|
|
60
|
+
*/
|
|
61
|
+
versionsOf(name: string): string[];
|
|
62
|
+
/**
|
|
63
|
+
* Node-only tooling. Reads the Source Quiver at sourceDir, validates it,
|
|
64
|
+
* and writes the runtime build artifact to outDir.
|
|
65
|
+
*
|
|
66
|
+
* Uses dynamic import of `./build.js` so that this module stays
|
|
67
|
+
* browser-safe at evaluation time.
|
|
68
|
+
*
|
|
69
|
+
* Throws `quiver_invalid` on source validation failures,
|
|
70
|
+
* `transport_error` on I/O failures.
|
|
71
|
+
*/
|
|
72
|
+
static build(sourceDir: string, outDir: string, opts?: BuildOptions): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Lazily loads the file tree for a specific quill version.
|
|
75
|
+
*
|
|
76
|
+
* Returns `Map<string, Uint8Array>` suitable for `engine.quill(tree)`.
|
|
77
|
+
* Does NOT cache the result — caching is the registry's concern.
|
|
78
|
+
*
|
|
79
|
+
* Throws `transport_error` if name/version not in catalog or I/O fails.
|
|
80
|
+
*/
|
|
81
|
+
loadTree(name: string, version: string): Promise<Map<string, Uint8Array>>;
|
|
82
|
+
}
|