@quillmark/quiver 0.37.0 → 0.38.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/README.md +8 -2
- package/dist/build.js +1 -1
- package/dist/built-loader.js +4 -12
- package/dist/format.d.ts +3 -2
- package/dist/format.js +3 -2
- package/dist/node.d.ts +1 -0
- package/dist/node.js +1 -0
- package/dist/quiver-yaml.d.ts +1 -1
- package/dist/quiver-yaml.js +2 -2
- package/dist/quiver.d.ts +5 -3
- package/dist/quiver.js +7 -5
- package/dist/ref.d.ts +8 -6
- package/dist/ref.js +23 -14
- package/dist/semver.d.ts +3 -3
- package/dist/semver.js +3 -3
- package/dist/source-loader.d.ts +3 -3
- package/dist/source-loader.js +47 -9
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ const quill = await quiver.getQuill(doc.quillRef);
|
|
|
43
43
|
const result = await engine.render(quill, doc, { format: 'pdf' });
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
`getQuill(ref)` is the only way to obtain a quill from a quiver, and the only entry point most consumers need. It accepts selector refs (`"memo"`, `"memo@1"`) and canonical ones (`"memo@1.0.0"`), resolves the selector, materializes the quill via `Quill.fromTree`, and caches one instance per canonical ref for the quiver's lifetime; concurrent calls for the same ref coalesce into a single load. It awaits `init()` itself, so instantiating the core is not a precondition of calling it.
|
|
46
|
+
`getQuill(ref)` is the only way to obtain a quill from a quiver, and the only entry point most consumers need. It accepts selector refs (`"memo"`, `"memo@1.0"`) and canonical ones (`"memo@1.0.0"`), resolves the selector, materializes the quill via `Quill.fromTree`, and caches one instance per canonical ref for the quiver's lifetime; concurrent calls for the same ref coalesce into a single load. It awaits `init()` itself, so instantiating the core is not a precondition of calling it.
|
|
47
47
|
|
|
48
48
|
That quill is **borrowed, not owned**: every caller asking for that ref gets the same instance, so `free()` on it hands the next caller a freed handle. Code that owns its quill mints one from `(await quiver.getQuill(ref)).toTree()` and frees that.
|
|
49
49
|
|
|
@@ -55,6 +55,10 @@ One narrower verb sits beside `getQuill`: `resolve(ref)` returns the canonical r
|
|
|
55
55
|
const canonicalRef = quiver.resolve('memo'); // "memo@1.1.0"
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
A selector is `x.y` or `x.y.z`. `memo@1.0` is the highest `1.0.z` the quiver holds, and `memo@1.0.0` is exact. `memo@1` is `invalid_ref`: a minor step asks a stored document to be rewritten, so a major alone names no compatible series.
|
|
59
|
+
|
|
60
|
+
A ref may name the collection, `<quiver>.memo@1.0`, the form a document's `$quill` takes to say which collection it was written for. A quiver answers its own name as it answers the bare one, and refuses any other as `quill_not_found`; the canonical ref is bare.
|
|
61
|
+
|
|
58
62
|
## Consuming a quiver (browser)
|
|
59
63
|
|
|
60
64
|
Build at deploy time, serve the output as static files:
|
|
@@ -68,7 +72,7 @@ await build('./node_modules/@org/my-quiver', './public/quivers/my-quiver');
|
|
|
68
72
|
|
|
69
73
|
`build` owns its output path outright: it assembles a generation in `<outDir>.stage`, moves it in whole, and deletes the one it replaced. A reader fetching mid-build sees the previous generation rather than a torn tree, and a build that throws leaves it serving. An `outDir` that is, or contains, the source quiver or the working directory is refused with a `transport_error` rather than deleted.
|
|
70
74
|
|
|
71
|
-
Quills below `0.1.0` are drafts and are left out: they stay in the source layout, which `fromDir` reads whole, and reach the artifact only under `build(src, out, { drafts: true })`. A quill with no version above the floor is absent from the built catalog entirely.
|
|
75
|
+
Quills below `0.1.0` are drafts and are left out: they stay in the source layout, which `fromDir` reads whole, and reach the artifact only under `build(src, out, { drafts: true })`. A quill with no version above the floor is absent from the built catalog entirely. `isDraft(version)`, from `@quillmark/quiver/node`, is the same reading of the floor.
|
|
72
76
|
|
|
73
77
|
```ts
|
|
74
78
|
// browser runtime
|
|
@@ -149,6 +153,8 @@ my-quiver/
|
|
|
149
153
|
package.json
|
|
150
154
|
```
|
|
151
155
|
|
|
156
|
+
A quill's `Quill.yaml` declares the `quill.name` and `quill.version` of the directory it sits in, the version as `x.y.z`.
|
|
157
|
+
|
|
152
158
|
A consumer reaches an installed quiver by resolving `<specifier>/Quiver.yaml`, which goes through your `exports` map. A package that declares one must expose that subpath, or the quiver is unreachable however correct its layout; a package with no `exports` map needs nothing.
|
|
153
159
|
|
|
154
160
|
```jsonc
|
package/dist/build.js
CHANGED
|
@@ -13,7 +13,7 @@ import { isDraft } from './semver.js';
|
|
|
13
13
|
/** Hex chars of SHA-256 in a bundle's name: enough that a changed bundle never reuses one. */
|
|
14
14
|
const NAME_DIGEST_LENGTH = 32;
|
|
15
15
|
/** Font file extensions recognised by the builder (case-insensitive). */
|
|
16
|
-
const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
|
|
16
|
+
const FONT_EXT = /\.(ttf|otf|ttc|otc|woff|woff2)$/i;
|
|
17
17
|
/** The two trees a build owns beside its output: where a generation is assembled, and
|
|
18
18
|
* where the one it replaces waits to be deleted. Siblings of outDir, so the swap is a
|
|
19
19
|
* rename rather than a copy across filesystems. */
|
package/dist/built-loader.js
CHANGED
|
@@ -142,13 +142,6 @@ class BuiltLoader {
|
|
|
142
142
|
function sameFiles(a, b) {
|
|
143
143
|
return a.bundle === b.bundle && JSON.stringify(a.fonts) === JSON.stringify(b.fonts);
|
|
144
144
|
}
|
|
145
|
-
function assertNoUnknownKeys(obj, allowed, context) {
|
|
146
|
-
for (const key of Object.keys(obj)) {
|
|
147
|
-
if (!allowed.includes(key)) {
|
|
148
|
-
throw new QuiverError('quiver_invalid', `${context}: unknown field "${key}"`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
145
|
async function readIndex(read) {
|
|
153
146
|
let bytes;
|
|
154
147
|
try {
|
|
@@ -175,8 +168,6 @@ function parseIndex(raw) {
|
|
|
175
168
|
throw new QuiverError('quiver_invalid', `${INDEX} must be a JSON object`);
|
|
176
169
|
}
|
|
177
170
|
const obj = parsed;
|
|
178
|
-
// Read before the key check, so a newer document is refused as newer rather than as
|
|
179
|
-
// carrying a field this reader does not know.
|
|
180
171
|
const format = obj['format'];
|
|
181
172
|
if (typeof format !== 'number' || !Number.isInteger(format) || format < 1) {
|
|
182
173
|
throw new QuiverError('quiver_invalid', `${INDEX}: "format" must be a positive integer, got ${JSON.stringify(format)}`);
|
|
@@ -185,11 +176,13 @@ function parseIndex(raw) {
|
|
|
185
176
|
throw new QuiverError('quiver_invalid', `This quiver is built in format ${format} and this loader reads ${FORMAT}. ` +
|
|
186
177
|
`Upgrade @quillmark/quiver, or for a served client the client itself, which carries the copy that reads this.`);
|
|
187
178
|
}
|
|
188
|
-
assertNoUnknownKeys(obj, ['format', 'name', 'description', 'quills'], INDEX);
|
|
189
179
|
const name = obj['name'];
|
|
190
180
|
if (typeof name !== 'string' || name.length === 0) {
|
|
191
181
|
throw new QuiverError('quiver_invalid', `${INDEX} must have a non-empty string "name" field`);
|
|
192
182
|
}
|
|
183
|
+
if (!isQuillName(name)) {
|
|
184
|
+
throw new QuiverError('quiver_invalid', `${INDEX} name "${name}" is not a quill name — a name is [a-z][a-z0-9_]*, at most 64 characters`);
|
|
185
|
+
}
|
|
193
186
|
const description = obj['description'];
|
|
194
187
|
if (description !== undefined && typeof description !== 'string') {
|
|
195
188
|
throw new QuiverError('quiver_invalid', `${INDEX} "description" must be a string if present, got ${typeof description}`);
|
|
@@ -206,13 +199,12 @@ function parseIndex(raw) {
|
|
|
206
199
|
throw new QuiverError('quiver_invalid', `${at} must be an object`);
|
|
207
200
|
}
|
|
208
201
|
const e = entry;
|
|
209
|
-
assertNoUnknownKeys(e, ['name', 'version', 'bundle', 'fonts'], at);
|
|
210
202
|
const quillName = e['name'];
|
|
211
203
|
if (typeof quillName !== 'string' || quillName.length === 0) {
|
|
212
204
|
throw new QuiverError('quiver_invalid', `${at}.name must be a non-empty string`);
|
|
213
205
|
}
|
|
214
206
|
if (!isQuillName(quillName)) {
|
|
215
|
-
throw new QuiverError('quiver_invalid', `${at}.name "${quillName}" is not a name a
|
|
207
|
+
throw new QuiverError('quiver_invalid', `${at}.name "${quillName}" is not a quill name — a name is [a-z][a-z0-9_]*, at most 64 characters`);
|
|
216
208
|
}
|
|
217
209
|
const version = e['version'];
|
|
218
210
|
if (typeof version !== 'string' || !isCanonicalSemver(version)) {
|
package/dist/format.d.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
export declare const INDEX = "quiver.json";
|
|
4
4
|
/**
|
|
5
5
|
* The artifact's format, stamped into `quiver.json` and refused to be read past (QUIVER
|
|
6
|
-
* §"The index").
|
|
7
|
-
*
|
|
6
|
+
* §"The index"). A change a reader assuming the previous shape would misread moves it, and
|
|
7
|
+
* so does the first added field, which format-2 readers before 0.38 refuse; from 3 on, an
|
|
8
|
+
* added field a reader can skip does not.
|
|
8
9
|
*/
|
|
9
10
|
export declare const FORMAT = 2;
|
|
10
11
|
/**
|
package/dist/format.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
export const INDEX = 'quiver.json';
|
|
4
4
|
/**
|
|
5
5
|
* The artifact's format, stamped into `quiver.json` and refused to be read past (QUIVER
|
|
6
|
-
* §"The index").
|
|
7
|
-
*
|
|
6
|
+
* §"The index"). A change a reader assuming the previous shape would misread moves it, and
|
|
7
|
+
* so does the first added field, which format-2 readers before 0.38 refuse; from 3 on, an
|
|
8
|
+
* added field a reader can skip does not.
|
|
8
9
|
*/
|
|
9
10
|
export const FORMAT = 2;
|
|
10
11
|
/**
|
package/dist/node.d.ts
CHANGED
package/dist/node.js
CHANGED
|
@@ -78,6 +78,7 @@ export async function build(sourceDir, outDir, options) {
|
|
|
78
78
|
// The rest of the public surface, so a Node consumer needs one import.
|
|
79
79
|
export { Quiver } from './quiver.js';
|
|
80
80
|
export { QuiverError } from './errors.js';
|
|
81
|
+
export { isDraft } from './semver.js';
|
|
81
82
|
// Engine types (`Quillmark`, `Quill`, `Document`, `RenderResult`, …) are not
|
|
82
83
|
// re-exported: import them straight from the `@quillmark/wasm` peer dependency,
|
|
83
84
|
// which is the single source of truth for the engine contract.
|
package/dist/quiver-yaml.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface QuiverMeta {
|
|
|
13
13
|
* Throws `QuiverError('quiver_invalid')` on:
|
|
14
14
|
* - YAML parse failure
|
|
15
15
|
* - Missing or non-string `name`
|
|
16
|
-
* - `name`
|
|
16
|
+
* - `name` is not a quill name, `[a-z][a-z0-9_]*`
|
|
17
17
|
* - Unknown fields (strict)
|
|
18
18
|
* - `description` present but not a string
|
|
19
19
|
*/
|
package/dist/quiver-yaml.js
CHANGED
|
@@ -13,7 +13,7 @@ const KNOWN_FIELDS = new Set(['name', 'description']);
|
|
|
13
13
|
* Throws `QuiverError('quiver_invalid')` on:
|
|
14
14
|
* - YAML parse failure
|
|
15
15
|
* - Missing or non-string `name`
|
|
16
|
-
* - `name`
|
|
16
|
+
* - `name` is not a quill name, `[a-z][a-z0-9_]*`
|
|
17
17
|
* - Unknown fields (strict)
|
|
18
18
|
* - `description` present but not a string
|
|
19
19
|
*/
|
|
@@ -43,7 +43,7 @@ export function parseQuiverYaml(raw) {
|
|
|
43
43
|
}
|
|
44
44
|
const name = doc['name'];
|
|
45
45
|
if (!isQuillName(name)) {
|
|
46
|
-
throw new QuiverError('quiver_invalid', `Quiver.yaml: "name" value "${name}"
|
|
46
|
+
throw new QuiverError('quiver_invalid', `Quiver.yaml: "name" value "${name}" is not a quill name — a name is [a-z][a-z0-9_]*, at most 64 characters`);
|
|
47
47
|
}
|
|
48
48
|
const meta = { name };
|
|
49
49
|
if (doc['description'] !== undefined) {
|
package/dist/quiver.d.ts
CHANGED
|
@@ -64,8 +64,10 @@ export declare class Quiver {
|
|
|
64
64
|
/**
|
|
65
65
|
* Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
|
|
66
66
|
*
|
|
67
|
-
* Selector forms: `name`, `name@x
|
|
68
|
-
*
|
|
67
|
+
* Selector forms: `name`, `name@x.y`, `name@x.y.z`. Picks the highest
|
|
68
|
+
* matching version in this quiver. A name prefixed with this quiver's
|
|
69
|
+
* own name (`<quiver>.name`) resolves as the bare name does; one prefixed with
|
|
70
|
+
* any other collection is `quill_not_found`. The canonical ref is bare.
|
|
69
71
|
*
|
|
70
72
|
* Sync, and in-memory by construction rather than by implementation: every
|
|
71
73
|
* loader materializes the catalog as the quiver is built, and `QuiverLoader`
|
|
@@ -85,7 +87,7 @@ export declare class Quiver {
|
|
|
85
87
|
*
|
|
86
88
|
* A core `Quill` renders directly: pass it to `engine.render(quill, doc)`.
|
|
87
89
|
*
|
|
88
|
-
* Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical form
|
|
90
|
+
* Selector refs (e.g. `"memo"`, `"memo@1.0"`) are resolved to canonical form
|
|
89
91
|
* first. Materializes once and caches per canonical ref — concurrent calls
|
|
90
92
|
* coalesce into a single load.
|
|
91
93
|
*
|
package/dist/quiver.js
CHANGED
|
@@ -92,8 +92,10 @@ export class Quiver {
|
|
|
92
92
|
/**
|
|
93
93
|
* Resolves a selector ref → canonical ref (e.g. "memo" → "memo@1.1.0").
|
|
94
94
|
*
|
|
95
|
-
* Selector forms: `name`, `name@x
|
|
96
|
-
*
|
|
95
|
+
* Selector forms: `name`, `name@x.y`, `name@x.y.z`. Picks the highest
|
|
96
|
+
* matching version in this quiver. A name prefixed with this quiver's
|
|
97
|
+
* own name (`<quiver>.name`) resolves as the bare name does; one prefixed with
|
|
98
|
+
* any other collection is `quill_not_found`. The canonical ref is bare.
|
|
97
99
|
*
|
|
98
100
|
* Sync, and in-memory by construction rather than by implementation: every
|
|
99
101
|
* loader materializes the catalog as the quiver is built, and `QuiverLoader`
|
|
@@ -104,8 +106,8 @@ export class Quiver {
|
|
|
104
106
|
* - `quill_not_found` if no version matches
|
|
105
107
|
*/
|
|
106
108
|
resolve(ref) {
|
|
107
|
-
const { name, selector } = parseQuillRef(ref);
|
|
108
|
-
const versions = this.#catalog.get(name);
|
|
109
|
+
const { collection, name, selector } = parseQuillRef(ref);
|
|
110
|
+
const versions = collection === undefined || collection === this.name ? this.#catalog.get(name) : undefined;
|
|
109
111
|
if (versions !== undefined) {
|
|
110
112
|
// Every loader materializes a catalog sorted descending, so the first match is
|
|
111
113
|
// the highest.
|
|
@@ -126,7 +128,7 @@ export class Quiver {
|
|
|
126
128
|
*
|
|
127
129
|
* A core `Quill` renders directly: pass it to `engine.render(quill, doc)`.
|
|
128
130
|
*
|
|
129
|
-
* Selector refs (e.g. `"memo"`, `"memo@1"`) are resolved to canonical form
|
|
131
|
+
* Selector refs (e.g. `"memo"`, `"memo@1.0"`) are resolved to canonical form
|
|
130
132
|
* first. Materializes once and caches per canonical ref — concurrent calls
|
|
131
133
|
* coalesce into a single load.
|
|
132
134
|
*
|
package/dist/ref.d.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/** Internal parsed representation of a quill reference. */
|
|
2
2
|
export interface ParsedQuillRef {
|
|
3
|
+
/** The collection the ref names, the part before a `.`; absent on a bare name. */
|
|
4
|
+
collection?: string;
|
|
3
5
|
name: string;
|
|
4
6
|
/** Undefined selects the highest version present. */
|
|
5
7
|
selector?: string;
|
|
6
8
|
}
|
|
7
9
|
/**
|
|
8
|
-
* Whether `name` is a name a
|
|
9
|
-
*
|
|
10
|
-
* `getQuill` refuses.
|
|
10
|
+
* Whether `name` is a quill name: `[a-z][a-z0-9_]*`, at most 64 characters, the
|
|
11
|
+
* engine's rule. A quiver's own name and every catalog row are held to it.
|
|
11
12
|
*/
|
|
12
13
|
export declare function isQuillName(name: string): boolean;
|
|
13
14
|
/**
|
|
14
|
-
* Throws QuiverError('invalid_ref') on malformed input. The name is
|
|
15
|
-
*
|
|
16
|
-
* ranges or operators — a quiver resolves a prefix, it does not
|
|
15
|
+
* Throws QuiverError('invalid_ref') on malformed input. The name is a quill name,
|
|
16
|
+
* optionally prefixed `<collection>.`; the selector is `x.y` or `x.y.z`, digits
|
|
17
|
+
* only, with no ranges or operators — a quiver resolves a prefix, it does not
|
|
18
|
+
* solve a range.
|
|
17
19
|
*/
|
|
18
20
|
export declare function parseQuillRef(ref: string): ParsedQuillRef;
|
package/dist/ref.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { QuiverError } from './errors.js';
|
|
2
|
-
const NAME_RE = /^[
|
|
3
|
-
const SELECTOR_RE = /^\d+(\.\d+)
|
|
2
|
+
const NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
3
|
+
const SELECTOR_RE = /^\d+\.\d+(\.\d+)?$/;
|
|
4
4
|
/**
|
|
5
|
-
* Whether `name` is a name a
|
|
6
|
-
*
|
|
7
|
-
* `getQuill` refuses.
|
|
5
|
+
* Whether `name` is a quill name: `[a-z][a-z0-9_]*`, at most 64 characters, the
|
|
6
|
+
* engine's rule. A quiver's own name and every catalog row are held to it.
|
|
8
7
|
*/
|
|
9
8
|
export function isQuillName(name) {
|
|
10
9
|
return NAME_RE.test(name);
|
|
11
10
|
}
|
|
12
11
|
/**
|
|
13
|
-
* Throws QuiverError('invalid_ref') on malformed input. The name is
|
|
14
|
-
*
|
|
15
|
-
* ranges or operators — a quiver resolves a prefix, it does not
|
|
12
|
+
* Throws QuiverError('invalid_ref') on malformed input. The name is a quill name,
|
|
13
|
+
* optionally prefixed `<collection>.`; the selector is `x.y` or `x.y.z`, digits
|
|
14
|
+
* only, with no ranges or operators — a quiver resolves a prefix, it does not
|
|
15
|
+
* solve a range.
|
|
16
16
|
*/
|
|
17
17
|
export function parseQuillRef(ref) {
|
|
18
18
|
if (!ref) {
|
|
@@ -24,21 +24,30 @@ export function parseQuillRef(ref) {
|
|
|
24
24
|
}
|
|
25
25
|
// Split once: the charset the name is held to is one rule, and a ref with a selector
|
|
26
26
|
// is under it exactly as a bare one is.
|
|
27
|
-
const
|
|
27
|
+
const fullName = atIndex === -1 ? ref : ref.slice(0, atIndex);
|
|
28
28
|
const selector = atIndex === -1 ? undefined : ref.slice(atIndex + 1);
|
|
29
29
|
if (selector === '') {
|
|
30
30
|
throw new QuiverError('invalid_ref', `Invalid ref: missing selector after "@" in "${ref}"`, {
|
|
31
31
|
ref
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const dot = fullName.indexOf('.');
|
|
35
|
+
const collection = dot === -1 ? undefined : fullName.slice(0, dot);
|
|
36
|
+
const name = dot === -1 ? fullName : fullName.slice(dot + 1);
|
|
37
|
+
for (const segment of collection === undefined ? [name] : [collection, name]) {
|
|
38
|
+
if (!NAME_RE.test(segment)) {
|
|
39
|
+
throw new QuiverError('invalid_ref', `Invalid ref: name "${fullName}" is not [collection.]name, each [a-z][a-z0-9_]* and at most 64 characters`, { ref });
|
|
40
|
+
}
|
|
36
41
|
}
|
|
42
|
+
const parsed = collection === undefined ? { name } : { collection, name };
|
|
37
43
|
if (selector === undefined) {
|
|
38
|
-
return
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
if (/^\d+$/.test(selector)) {
|
|
47
|
+
throw new QuiverError('invalid_ref', `Invalid ref: selector "${selector}" is a major alone, which names no compatible series; write "${fullName}@${selector}.y" or "${fullName}@${selector}.y.z"`, { ref });
|
|
39
48
|
}
|
|
40
49
|
if (!SELECTOR_RE.test(selector)) {
|
|
41
|
-
throw new QuiverError('invalid_ref', `Invalid ref: selector "${selector}" is not a
|
|
50
|
+
throw new QuiverError('invalid_ref', `Invalid ref: selector "${selector}" is not x.y (a compatible series) or x.y.z (an exact version), digits only`, { ref });
|
|
42
51
|
}
|
|
43
|
-
return {
|
|
52
|
+
return { ...parsed, selector };
|
|
44
53
|
}
|
package/dist/semver.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ export declare const MIN_PUBLISHED_VERSION = "0.1.0";
|
|
|
3
3
|
/** Returns true for exactly `x.y.z` with non-negative integer parts. */
|
|
4
4
|
export declare function isCanonicalSemver(version: string): boolean;
|
|
5
5
|
/**
|
|
6
|
-
* Returns true if `version` (canonical) matches `selector` (
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
6
|
+
* Returns true if `version` (canonical) matches `selector` (`x.y` or `x.y.z`).
|
|
7
|
+
* `parseQuillRef` owns selector validation; this is a prefix comparison over an
|
|
8
|
+
* already-valid selector.
|
|
9
9
|
*/
|
|
10
10
|
export declare function matchesSemverSelector(version: string, selector: string): boolean;
|
|
11
11
|
/** Compares two canonical semver strings. Returns <0, 0, or >0. Three positions,
|
package/dist/semver.js
CHANGED
|
@@ -5,9 +5,9 @@ export function isCanonicalSemver(version) {
|
|
|
5
5
|
return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version);
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* Returns true if `version` (canonical) matches `selector` (
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
8
|
+
* Returns true if `version` (canonical) matches `selector` (`x.y` or `x.y.z`).
|
|
9
|
+
* `parseQuillRef` owns selector validation; this is a prefix comparison over an
|
|
10
|
+
* already-valid selector.
|
|
11
11
|
*/
|
|
12
12
|
export function matchesSemverSelector(version, selector) {
|
|
13
13
|
const selectorParts = selector.split('.');
|
package/dist/source-loader.d.ts
CHANGED
|
@@ -12,9 +12,9 @@ import type { QuiverLoader } from './quiver.js';
|
|
|
12
12
|
*
|
|
13
13
|
* Throws:
|
|
14
14
|
* - `quiver_invalid` if Quiver.yaml is invalid, any rung it opens or reads is a
|
|
15
|
-
* symlink, a quill dir holding versions is
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* symlink, a quill dir holding versions is not a quill name, a version dir
|
|
16
|
+
* name is non-canonical, or a version dir is missing its Quill.yaml or holds
|
|
17
|
+
* one naming another quill or version.
|
|
18
18
|
* - `transport_error` for I/O failures (permissions, etc.).
|
|
19
19
|
*
|
|
20
20
|
* Missing `quills/` directory is not an error — the quiver is valid but empty.
|
package/dist/source-loader.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readdir, readFile, lstat } from 'node:fs/promises';
|
|
6
6
|
import { join, relative, sep } from 'node:path';
|
|
7
|
+
import { parse as parseYaml } from 'yaml';
|
|
7
8
|
import { QuiverError } from './errors.js';
|
|
8
9
|
import { parseQuiverYaml } from './quiver-yaml.js';
|
|
9
10
|
import { isQuillName } from './ref.js';
|
|
@@ -20,6 +21,44 @@ function refuseSymlink(st, path) {
|
|
|
20
21
|
throw new QuiverError('quiver_invalid', `"${path}" is a symlink; a quiver holds its own content, and nothing under it may point outside`);
|
|
21
22
|
}
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Asked of a directory holding a quill rather than of every directory, so a stray
|
|
26
|
+
* one holding none stays ignored: what has to be addressable is a catalog row.
|
|
27
|
+
*/
|
|
28
|
+
function refuseUnnamedQuill(quillName, quiverName) {
|
|
29
|
+
if (!isQuillName(quillName)) {
|
|
30
|
+
throw new QuiverError('quiver_invalid', `Quill directory "${quillName}" is not a quill name — a name is [a-z][a-z0-9_]*, at most 64 characters`, { quiverName });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Refuse a `Quill.yaml` whose `quill.name` or `quill.version` disagrees with the
|
|
35
|
+
* path it sits at: the catalog serves the path, and a document saved from the quill
|
|
36
|
+
* names what `Quill.yaml` says. A canonical version has one spelling, so the declared
|
|
37
|
+
* one is compared as written. A field absent or not a string is the engine's to report
|
|
38
|
+
* when the quill loads.
|
|
39
|
+
*/
|
|
40
|
+
function refuseMisplacedQuill(raw, name, version, quiverName) {
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = parseYaml(new TextDecoder().decode(raw), { schema: 'failsafe' });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const quill = parsed?.quill;
|
|
49
|
+
if (typeof quill !== 'object' || quill === null)
|
|
50
|
+
return;
|
|
51
|
+
const declared = quill;
|
|
52
|
+
if (typeof declared.name === 'string' && declared.name !== name) {
|
|
53
|
+
throw new QuiverError('quiver_invalid', `Quill "${name}@${version}" declares quill.name "${declared.name}": a quill sits at quills/<quill.name>/<quill.version>/`, { quiverName, version });
|
|
54
|
+
}
|
|
55
|
+
if (typeof declared.version === 'string' && declared.version !== version) {
|
|
56
|
+
const rule = isCanonicalSemver(declared.version)
|
|
57
|
+
? 'a quill sits at quills/<quill.name>/<quill.version>/'
|
|
58
|
+
: 'a quill version is x.y.z';
|
|
59
|
+
throw new QuiverError('quiver_invalid', `Quill "${name}@${version}" declares quill.version "${declared.version}": ${rule}`, { quiverName, version });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
23
62
|
/**
|
|
24
63
|
* Scans a Source Quiver root directory.
|
|
25
64
|
*
|
|
@@ -28,9 +67,9 @@ function refuseSymlink(st, path) {
|
|
|
28
67
|
*
|
|
29
68
|
* Throws:
|
|
30
69
|
* - `quiver_invalid` if Quiver.yaml is invalid, any rung it opens or reads is a
|
|
31
|
-
* symlink, a quill dir holding versions is
|
|
32
|
-
*
|
|
33
|
-
*
|
|
70
|
+
* symlink, a quill dir holding versions is not a quill name, a version dir
|
|
71
|
+
* name is non-canonical, or a version dir is missing its Quill.yaml or holds
|
|
72
|
+
* one naming another quill or version.
|
|
34
73
|
* - `transport_error` for I/O failures (permissions, etc.).
|
|
35
74
|
*
|
|
36
75
|
* Missing `quills/` directory is not an error — the quiver is valid but empty.
|
|
@@ -106,8 +145,10 @@ export async function scanSourceQuiver(rootDir) {
|
|
|
106
145
|
throw new QuiverError('quiver_invalid', `Quill "${quillName}" has non-canonical version directory "${versionDir}" — only x.y.z format is allowed`, { quiverName: meta.name, version: versionDir });
|
|
107
146
|
}
|
|
108
147
|
const quillYamlPath = join(versionPath, 'Quill.yaml');
|
|
148
|
+
let quillYaml;
|
|
109
149
|
try {
|
|
110
150
|
refuseSymlink(await lstat(quillYamlPath), quillYamlPath);
|
|
151
|
+
quillYaml = await readFile(quillYamlPath);
|
|
111
152
|
}
|
|
112
153
|
catch (err) {
|
|
113
154
|
if (err instanceof QuiverError)
|
|
@@ -116,16 +157,13 @@ export async function scanSourceQuiver(rootDir) {
|
|
|
116
157
|
if (code === 'ENOENT') {
|
|
117
158
|
throw new QuiverError('quiver_invalid', `Quill "${quillName}@${versionDir}" is missing required "Quill.yaml"`, { quiverName: meta.name, version: versionDir });
|
|
118
159
|
}
|
|
119
|
-
throw new QuiverError('transport_error', `Failed to
|
|
160
|
+
throw new QuiverError('transport_error', `Failed to read "Quill.yaml" at "${quillYamlPath}": ${err.message}`, { cause: err });
|
|
120
161
|
}
|
|
162
|
+
refuseUnnamedQuill(quillName, meta.name);
|
|
163
|
+
refuseMisplacedQuill(quillYaml, quillName, versionDir, meta.name);
|
|
121
164
|
versions.push(versionDir);
|
|
122
165
|
}
|
|
123
166
|
if (versions.length > 0) {
|
|
124
|
-
// Asked here rather than at the directory, so a stray one holding no quill
|
|
125
|
-
// stays ignored: what has to be addressable is a catalog row.
|
|
126
|
-
if (!isQuillName(quillName)) {
|
|
127
|
-
throw new QuiverError('quiver_invalid', `Quill directory "${quillName}" is not a name a ref can spell — only [A-Za-z0-9_-] are allowed`, { quiverName: meta.name });
|
|
128
|
-
}
|
|
129
167
|
versions.sort((a, b) => compareSemver(b, a));
|
|
130
168
|
catalog.set(quillName, versions);
|
|
131
169
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/quiver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.1",
|
|
4
4
|
"description": "A quiver of Quillmark quills: load one, pack one, hand out quills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"author": "Quillmark Contributors",
|
|
23
23
|
"engines": {
|
|
24
|
-
"node": ">=
|
|
24
|
+
"node": ">=22"
|
|
25
25
|
},
|
|
26
26
|
"exports": {
|
|
27
27
|
".": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"NOTICE"
|
|
43
43
|
],
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@quillmark/wasm": ">=0.
|
|
45
|
+
"@quillmark/wasm": ">=0.126.0-0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"fflate": "^0.8.2",
|