@quillmark/quiver 0.18.1 → 0.19.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/README.md +5 -20
- package/dist/build.d.ts +4 -0
- package/dist/build.js +141 -96
- package/package.json +2 -5
- package/dist/bin/quiver.d.ts +0 -16
- package/dist/bin/quiver.js +0 -141
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ import { build } from '@quillmark/quiver/node';
|
|
|
62
62
|
await build('./node_modules/@org/my-quiver', './public/quivers/my-quiver');
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
`build`
|
|
65
|
+
`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.
|
|
66
66
|
|
|
67
67
|
```ts
|
|
68
68
|
// browser runtime
|
|
@@ -134,32 +134,17 @@ A consumer reaches an installed quiver by resolving `<specifier>/Quiver.yaml`, w
|
|
|
134
134
|
}
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
## Authoring is [quillkit](../quillkit#readme)'s
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
This package is a library: it loads a quiver and packs one, and it has no CLI. The verbs a quill author runs (gate, pack, look at, deploy) are `quillkit`'s, and it resolves this package out of the collection's own `node_modules`. Depend on it here and the version you pin is the format your quiver is packed in:
|
|
140
140
|
|
|
141
141
|
```sh
|
|
142
|
-
npm install --save-dev @quillmark/quiver @quillmark/wasm
|
|
142
|
+
npm install --save-dev @quillmark/quiver @quillmark/wasm quillkit
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
```jsonc
|
|
146
146
|
// package.json
|
|
147
147
|
{
|
|
148
|
-
"scripts": { "test": "
|
|
148
|
+
"scripts": { "test": "quillkit test" }
|
|
149
149
|
}
|
|
150
150
|
```
|
|
151
|
-
|
|
152
|
-
It finds the engine itself: a named `engine` export from `quiver.config.js` at the collection root, else `@quillmark/wasm` from your own `node_modules`. `quillmark-quiver build [--out <dir>]` packs the same source into a servable artifact.
|
|
153
|
-
|
|
154
|
-
On vitest, jest or `node:test`, spawn the bin from a test rather than rebuilding the loop against the main API. The gate stays one implementation, and the case gates what CI gates:
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
// quiver.test.ts
|
|
158
|
-
import { execFileSync } from 'node:child_process';
|
|
159
|
-
|
|
160
|
-
it('gates the quiver', () => {
|
|
161
|
-
execFileSync('quillmark-quiver', ['test'], { stdio: 'inherit' });
|
|
162
|
-
});
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
It answers _does it work_, and that is the whole of what this package offers a quill author: _what is it like to use_ is answered by working a real document through the quill live, which is an app rather than a subpath.
|
package/dist/build.d.ts
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
* store/
|
|
18
18
|
* <sha256> # dehydrated font bytes (full hash, no ext)
|
|
19
19
|
*
|
|
20
|
+
* A generation is assembled beside outDir and moved in whole, so a reader
|
|
21
|
+
* fetching mid-build reads the previous one rather than a torn tree, and a
|
|
22
|
+
* build that throws leaves the previous one serving.
|
|
23
|
+
*
|
|
20
24
|
* Throws:
|
|
21
25
|
* - `quiver_invalid` on source validation failures (propagated from scanner)
|
|
22
26
|
* - `transport_error` on I/O failures, and on an outDir the build would have
|
package/dist/build.js
CHANGED
|
@@ -11,12 +11,20 @@ import { NAME_DIGEST_LENGTH } from './digest.js';
|
|
|
11
11
|
import { POINTER_FORMAT } from './format.js';
|
|
12
12
|
/** Font file extensions recognised by the builder (case-insensitive). */
|
|
13
13
|
const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
|
|
14
|
+
/** The two trees a build owns beside its output: where a generation is assembled, and
|
|
15
|
+
* where the one it replaces waits to be deleted. Siblings of outDir, so the swap is a
|
|
16
|
+
* rename rather than a copy across filesystems. */
|
|
17
|
+
const stagesOf = (out) => ({
|
|
18
|
+
stage: `${out}.stage`,
|
|
19
|
+
prev: `${out}.prev`
|
|
20
|
+
});
|
|
14
21
|
/**
|
|
15
|
-
* The build
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* The build deletes each of the three trees it owns, so an outDir that is, or contains,
|
|
23
|
+
* the source quiver or the working directory deletes the thing the caller was building
|
|
24
|
+
* from. `quillkit build --out .` and a mistyped `--out ..` are both one keystroke away,
|
|
25
|
+
* and the failure is unrecoverable, so an outDir that owns the caller is refused up
|
|
26
|
+
* front. The staging siblings are checked too: they are named off outDir and cleared
|
|
27
|
+
* with it, so a caller who avoids one spelling has not avoided the others.
|
|
20
28
|
*
|
|
21
29
|
* An outDir *inside* sourceDir stays allowed: the scan reads the source before
|
|
22
30
|
* any write, and `dist/` under the quiver root is the ordinary layout.
|
|
@@ -25,18 +33,19 @@ const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
|
|
|
25
33
|
*/
|
|
26
34
|
function assertSafeOutDir(path, sourceDir, outDir) {
|
|
27
35
|
const out = path.resolve(outDir);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
const { stage, prev } = stagesOf(out);
|
|
37
|
+
/** True when one of the owned trees is `target` or an ancestor of it. */
|
|
38
|
+
const owns = (target) => [out, stage, prev].some((dir) => {
|
|
39
|
+
const rel = path.relative(dir, path.resolve(target));
|
|
31
40
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
32
|
-
};
|
|
41
|
+
});
|
|
33
42
|
const what = owns(sourceDir)
|
|
34
43
|
? 'the source quiver'
|
|
35
44
|
: owns(process.cwd())
|
|
36
45
|
? 'the working directory'
|
|
37
46
|
: undefined;
|
|
38
47
|
if (what !== undefined) {
|
|
39
|
-
throw new QuiverError('transport_error', `Refusing to build into "${outDir}": the build clears
|
|
48
|
+
throw new QuiverError('transport_error', `Refusing to build into "${outDir}": the build clears that directory and two staging siblings beside it, and this one holds ${what} ("${out}"). Point --out somewhere the build can own outright.`);
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
/**
|
|
@@ -51,6 +60,10 @@ function assertSafeOutDir(path, sourceDir, outDir) {
|
|
|
51
60
|
* store/
|
|
52
61
|
* <sha256> # dehydrated font bytes (full hash, no ext)
|
|
53
62
|
*
|
|
63
|
+
* A generation is assembled beside outDir and moved in whole, so a reader
|
|
64
|
+
* fetching mid-build reads the previous one rather than a torn tree, and a
|
|
65
|
+
* build that throws leaves the previous one serving.
|
|
66
|
+
*
|
|
54
67
|
* Throws:
|
|
55
68
|
* - `quiver_invalid` on source validation failures (propagated from scanner)
|
|
56
69
|
* - `transport_error` on I/O failures, and on an outDir the build would have
|
|
@@ -59,107 +72,139 @@ function assertSafeOutDir(path, sourceDir, outDir) {
|
|
|
59
72
|
export async function buildQuiver(sourceDir, outDir) {
|
|
60
73
|
// Dynamic imports keep this module safe to type-import from browser contexts.
|
|
61
74
|
const path = await import('node:path');
|
|
62
|
-
const { join } = path;
|
|
63
|
-
const { mkdir, rm, writeFile } = await import('node:fs/promises');
|
|
75
|
+
const { dirname, join, resolve } = path;
|
|
76
|
+
const { mkdir, rename, rm, writeFile } = await import('node:fs/promises');
|
|
77
|
+
const { existsSync } = await import('node:fs');
|
|
64
78
|
const { createHash } = await import('node:crypto');
|
|
65
79
|
const { scanSourceQuiver, readQuillTree } = await import('./source-loader.js');
|
|
66
|
-
// 0.
|
|
67
|
-
//
|
|
80
|
+
// 0. Three trees get cleared here; refuse the paths where that deletes the
|
|
81
|
+
// caller instead of a previous build.
|
|
68
82
|
assertSafeOutDir(path, sourceDir, outDir);
|
|
83
|
+
const out = resolve(outDir);
|
|
84
|
+
const { stage, prev } = stagesOf(out);
|
|
69
85
|
// 1. Scan + validate source quiver (throws quiver_invalid on bad input).
|
|
70
86
|
const { meta, catalog } = await scanSourceQuiver(sourceDir);
|
|
71
|
-
// 2. Clear and recreate outDir + outDir/store/.
|
|
72
87
|
try {
|
|
73
|
-
await
|
|
74
|
-
await mkdir(join(outDir, 'store'), { recursive: true });
|
|
88
|
+
await packGeneration();
|
|
75
89
|
}
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
finally {
|
|
91
|
+
// A throw anywhere above leaves a partial generation staged, and outDir
|
|
92
|
+
// untouched. Neither tree outlives the call either way. Swept rather than
|
|
93
|
+
// checked: a sweep that threw would answer for the build, and what a caller
|
|
94
|
+
// needs to hear is why the build failed.
|
|
95
|
+
for (const at of [stage, prev])
|
|
96
|
+
await rm(at, { recursive: true, force: true }).catch(() => { });
|
|
78
97
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Write a whole generation into `stage`, then move it to outDir.
|
|
100
|
+
*
|
|
101
|
+
* The move is two renames: a directory rename refuses a non-empty target, so the
|
|
102
|
+
* outgoing generation steps aside first. That is the window, and it is two syscalls
|
|
103
|
+
* wide against the seconds a build takes.
|
|
104
|
+
*/
|
|
105
|
+
async function packGeneration() {
|
|
106
|
+
// 2. Clear and recreate the staged tree + its store/.
|
|
107
|
+
try {
|
|
108
|
+
await rm(stage, { recursive: true, force: true });
|
|
109
|
+
await mkdir(join(stage, 'store'), { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
throw new QuiverError('transport_error', `Failed to prepare output directory "${outDir}": ${err.message}`, { cause: err });
|
|
113
|
+
}
|
|
114
|
+
// 3. Process each quill version.
|
|
115
|
+
const manifestQuills = [];
|
|
116
|
+
for (const [quillName, versions] of catalog) {
|
|
117
|
+
for (const version of versions) {
|
|
118
|
+
const quillDir = join(sourceDir, 'quills', quillName, version);
|
|
119
|
+
// a. Read quill file tree.
|
|
120
|
+
const tree = await readQuillTree(quillDir);
|
|
121
|
+
// b. Partition fonts vs content.
|
|
122
|
+
const fontEntries = [];
|
|
123
|
+
const contentEntries = [];
|
|
124
|
+
for (const [path, bytes] of tree) {
|
|
125
|
+
if (FONT_EXT.test(path)) {
|
|
126
|
+
fontEntries.push([path, bytes]);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
contentEntries.push([path, bytes]);
|
|
130
|
+
}
|
|
92
131
|
}
|
|
93
|
-
|
|
94
|
-
|
|
132
|
+
// c. Dehydrate fonts into store/.
|
|
133
|
+
const fonts = {};
|
|
134
|
+
for (const [path, bytes] of fontEntries) {
|
|
135
|
+
// Full width: the store is keyed by hash, so two distinct fonts
|
|
136
|
+
// sharing a prefix would merge into one entry.
|
|
137
|
+
const hash = createHash('sha256').update(bytes).digest('hex');
|
|
138
|
+
try {
|
|
139
|
+
await writeFile(join(stage, 'store', hash), bytes);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
throw new QuiverError('transport_error', `Failed to write font store entry "${join(outDir, 'store', hash)}": ${err.message}`, { cause: err });
|
|
143
|
+
}
|
|
144
|
+
fonts[path] = hash;
|
|
95
145
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
146
|
+
// d. Zip content files (deterministic: sorted paths, fixed mtime).
|
|
147
|
+
const contentRecord = {};
|
|
148
|
+
for (const [path, bytes] of contentEntries) {
|
|
149
|
+
contentRecord[path] = bytes;
|
|
150
|
+
}
|
|
151
|
+
const zipBytes = packFiles(contentRecord);
|
|
152
|
+
// e–f. Compute bundle hash and name.
|
|
153
|
+
const bundleHash = createHash('sha256')
|
|
154
|
+
.update(zipBytes)
|
|
155
|
+
.digest('hex')
|
|
156
|
+
.slice(0, NAME_DIGEST_LENGTH);
|
|
157
|
+
const bundleName = `${quillName}@${version}.${bundleHash}.zip`;
|
|
158
|
+
// g. Write bundle zip.
|
|
104
159
|
try {
|
|
105
|
-
await writeFile(
|
|
160
|
+
await writeFile(join(stage, bundleName), zipBytes);
|
|
106
161
|
}
|
|
107
162
|
catch (err) {
|
|
108
|
-
throw new QuiverError('transport_error', `Failed to write
|
|
163
|
+
throw new QuiverError('transport_error', `Failed to write bundle "${join(outDir, bundleName)}": ${err.message}`, { cause: err });
|
|
109
164
|
}
|
|
110
|
-
|
|
165
|
+
// h. Record manifest entry.
|
|
166
|
+
manifestQuills.push({ name: quillName, version, bundle: bundleName, fonts });
|
|
111
167
|
}
|
|
112
|
-
// d. Zip content files (deterministic: sorted paths, fixed mtime).
|
|
113
|
-
const contentRecord = {};
|
|
114
|
-
for (const [path, bytes] of contentEntries) {
|
|
115
|
-
contentRecord[path] = bytes;
|
|
116
|
-
}
|
|
117
|
-
const zipBytes = packFiles(contentRecord);
|
|
118
|
-
// e–f. Compute bundle hash and name.
|
|
119
|
-
const bundleHash = createHash('sha256')
|
|
120
|
-
.update(zipBytes)
|
|
121
|
-
.digest('hex')
|
|
122
|
-
.slice(0, NAME_DIGEST_LENGTH);
|
|
123
|
-
const bundleName = `${quillName}@${version}.${bundleHash}.zip`;
|
|
124
|
-
// g. Write bundle zip.
|
|
125
|
-
const bundlePath = join(outDir, bundleName);
|
|
126
|
-
try {
|
|
127
|
-
await writeFile(bundlePath, zipBytes);
|
|
128
|
-
}
|
|
129
|
-
catch (err) {
|
|
130
|
-
throw new QuiverError('transport_error', `Failed to write bundle "${bundlePath}": ${err.message}`, { cause: err });
|
|
131
|
-
}
|
|
132
|
-
// h. Record manifest entry.
|
|
133
|
-
manifestQuills.push({ name: quillName, version, bundle: bundleName, fonts });
|
|
134
168
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
169
|
+
// 4–8. Build and write hashed manifest.
|
|
170
|
+
const manifest = {
|
|
171
|
+
version: 1,
|
|
172
|
+
name: meta.name,
|
|
173
|
+
quills: manifestQuills
|
|
174
|
+
};
|
|
175
|
+
const manifestJson = JSON.stringify(manifest, null, 2);
|
|
176
|
+
const manifestHash = createHash('sha256')
|
|
177
|
+
.update(manifestJson)
|
|
178
|
+
.digest('hex')
|
|
179
|
+
.slice(0, NAME_DIGEST_LENGTH);
|
|
180
|
+
const manifestFileName = `manifest.${manifestHash}.json`;
|
|
181
|
+
try {
|
|
182
|
+
await writeFile(join(stage, manifestFileName), manifestJson, 'utf-8');
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
throw new QuiverError('transport_error', `Failed to write manifest "${join(outDir, manifestFileName)}": ${err.message}`, { cause: err });
|
|
186
|
+
}
|
|
187
|
+
// 9–10. Write stable pointer latest.json. The format is stamped here and read first,
|
|
188
|
+
// so a client older than the tree says so rather than misreading it.
|
|
189
|
+
const pointer = { format: POINTER_FORMAT, manifest: manifestFileName };
|
|
190
|
+
try {
|
|
191
|
+
await writeFile(join(stage, 'latest.json'), JSON.stringify(pointer), 'utf-8');
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
throw new QuiverError('transport_error', `Failed to write pointer "${join(outDir, 'latest.json')}": ${err.message}`, { cause: err });
|
|
195
|
+
}
|
|
196
|
+
// 11. Move the generation in. `prev` is cleared first: a rename onto a
|
|
197
|
+
// non-empty directory fails, and the tree left by an interrupted run is
|
|
198
|
+
// one.
|
|
199
|
+
try {
|
|
200
|
+
await mkdir(dirname(out), { recursive: true });
|
|
201
|
+
await rm(prev, { recursive: true, force: true });
|
|
202
|
+
if (existsSync(out))
|
|
203
|
+
await rename(out, prev);
|
|
204
|
+
await rename(stage, out);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
throw new QuiverError('transport_error', `Failed to move the build into "${outDir}": ${err.message}`, { cause: err });
|
|
208
|
+
}
|
|
164
209
|
}
|
|
165
210
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quillmark/quiver",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"description": "A quiver of Quillmark quills: load one, pack one, hand out quills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -53,9 +53,6 @@
|
|
|
53
53
|
"typescript": "^5.9.3",
|
|
54
54
|
"vitest": "^4.0.18"
|
|
55
55
|
},
|
|
56
|
-
"bin": {
|
|
57
|
-
"quillmark-quiver": "./dist/bin/quiver.js"
|
|
58
|
-
},
|
|
59
56
|
"scripts": {
|
|
60
57
|
"build": "tsc",
|
|
61
58
|
"prepare": "npm run build",
|
package/dist/bin/quiver.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Quiver CLI — build and test a quiver collection. Two verbs, and they are the
|
|
4
|
-
* whole of what a quiver author gets from here: `build` packs, `test` gates.
|
|
5
|
-
* Looking at rendered output is studio's, an app rather than a verb.
|
|
6
|
-
*
|
|
7
|
-
* Commands:
|
|
8
|
-
* quillmark-quiver build [--out <dir>]
|
|
9
|
-
* quillmark-quiver test
|
|
10
|
-
*
|
|
11
|
-
* Engine discovery (for `test`):
|
|
12
|
-
* 1. Named export `engine` from `quiver.config.js` at the collection root,
|
|
13
|
-
* if the file exists.
|
|
14
|
-
* 2. Auto-imported `@quillmark/wasm` from the collection's node_modules.
|
|
15
|
-
*/
|
|
16
|
-
export {};
|
package/dist/bin/quiver.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Quiver CLI — build and test a quiver collection. Two verbs, and they are the
|
|
4
|
-
* whole of what a quiver author gets from here: `build` packs, `test` gates.
|
|
5
|
-
* Looking at rendered output is studio's, an app rather than a verb.
|
|
6
|
-
*
|
|
7
|
-
* Commands:
|
|
8
|
-
* quillmark-quiver build [--out <dir>]
|
|
9
|
-
* quillmark-quiver test
|
|
10
|
-
*
|
|
11
|
-
* Engine discovery (for `test`):
|
|
12
|
-
* 1. Named export `engine` from `quiver.config.js` at the collection root,
|
|
13
|
-
* if the file exists.
|
|
14
|
-
* 2. Auto-imported `@quillmark/wasm` from the collection's node_modules.
|
|
15
|
-
*/
|
|
16
|
-
import { createRequire } from 'node:module';
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
import { pathToFileURL } from 'node:url';
|
|
19
|
-
import { build as buildQuiver, fromDir } from '../node.js';
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Arg parsing helpers
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
const argv = process.argv.slice(2);
|
|
24
|
-
const command = argv[0];
|
|
25
|
-
/** One flag with a value. `build --out` is the only one either verb takes. */
|
|
26
|
-
function flag(name) {
|
|
27
|
-
const i = argv.indexOf(name);
|
|
28
|
-
return i !== -1 && i + 1 < argv.length ? argv[i + 1] : undefined;
|
|
29
|
-
}
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Engine discovery
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
async function loadEngine(cwd) {
|
|
34
|
-
// Resolve @quillmark/wasm from the collection's node_modules (best-effort).
|
|
35
|
-
// Its `Engine` renders every quill (cloning the core Quill/Document into the
|
|
36
|
-
// backend itself). A quiver.config.js may export a custom `engine`.
|
|
37
|
-
let wasm;
|
|
38
|
-
try {
|
|
39
|
-
const req = createRequire(pathToFileURL(join(cwd, 'package.json')).href);
|
|
40
|
-
const wasmPath = req.resolve('@quillmark/wasm');
|
|
41
|
-
wasm = (await import(pathToFileURL(wasmPath).href));
|
|
42
|
-
// Instantiate the core. Every `@quillmark/wasm` export throws
|
|
43
|
-
// `runtime::not_initialized` until `init()` resolves, and `new Engine()` is
|
|
44
|
-
// lazy, so without this the throw arrives at the first render and reads as a
|
|
45
|
-
// failing quill rather than an uninitialized runtime.
|
|
46
|
-
await wasm.init();
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
// Not installed — a quiver.config.js may still provide an engine.
|
|
50
|
-
}
|
|
51
|
-
// 1. Named `engine` export from quiver.config.js, if present.
|
|
52
|
-
try {
|
|
53
|
-
const cfg = await import(pathToFileURL(join(cwd, 'quiver.config.js')).href);
|
|
54
|
-
if (cfg.engine != null) {
|
|
55
|
-
return cfg.engine;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
// File absent or incomplete — fall through to auto-discovery.
|
|
60
|
-
}
|
|
61
|
-
// 2. Auto-discover from @quillmark/wasm.
|
|
62
|
-
if (wasm == null) {
|
|
63
|
-
throw new Error("Cannot find @quillmark/wasm in this collection's node_modules.\n" +
|
|
64
|
-
' Install it: npm install @quillmark/wasm\n' +
|
|
65
|
-
' Or export { engine } from quiver.config.js for a custom engine.');
|
|
66
|
-
}
|
|
67
|
-
return new wasm.Engine();
|
|
68
|
-
}
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// Commands
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
async function build() {
|
|
73
|
-
const cwd = process.cwd();
|
|
74
|
-
const outDir = flag('--out') ?? 'dist';
|
|
75
|
-
console.log(`quillmark-quiver build: ${cwd} → ${outDir}`);
|
|
76
|
-
await buildQuiver(cwd, outDir);
|
|
77
|
-
console.log('done.');
|
|
78
|
-
}
|
|
79
|
-
async function test() {
|
|
80
|
-
const cwd = process.cwd();
|
|
81
|
-
const engine = await loadEngine(cwd);
|
|
82
|
-
const quiver = await fromDir(cwd);
|
|
83
|
-
const names = quiver.quillNames();
|
|
84
|
-
if (names.length === 0) {
|
|
85
|
-
console.error('error: quiver has no quills');
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
let pass = 0;
|
|
89
|
-
let fail = 0;
|
|
90
|
-
for (const name of names) {
|
|
91
|
-
for (const version of quiver.versionsOf(name)) {
|
|
92
|
-
const ref = `${name}@${version}`;
|
|
93
|
-
try {
|
|
94
|
-
const quill = await quiver.getQuill(ref);
|
|
95
|
-
const doc = quill.seedDocument();
|
|
96
|
-
let result;
|
|
97
|
-
try {
|
|
98
|
-
result = await engine.render(quill, doc);
|
|
99
|
-
}
|
|
100
|
-
finally {
|
|
101
|
-
doc.free();
|
|
102
|
-
}
|
|
103
|
-
if (!Array.isArray(result.artifacts) || result.artifacts.length === 0) {
|
|
104
|
-
throw new Error('example render produced no artifacts');
|
|
105
|
-
}
|
|
106
|
-
console.log(`pass ${ref}`);
|
|
107
|
-
pass++;
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
console.error(`FAIL ${ref} — ${err.message}`);
|
|
111
|
-
fail++;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
const total = pass + fail;
|
|
116
|
-
console.log(`\n${pass}/${total} passed`);
|
|
117
|
-
if (fail > 0)
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
|
-
// Dispatch
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
function usage() {
|
|
124
|
-
console.error(['Usage:', ' quillmark-quiver build [--out <dir>]', ' quillmark-quiver test'].join('\n'));
|
|
125
|
-
}
|
|
126
|
-
function die(err) {
|
|
127
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
128
|
-
console.error(`error: ${msg}`);
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
switch (command) {
|
|
132
|
-
case 'build':
|
|
133
|
-
build().catch(die);
|
|
134
|
-
break;
|
|
135
|
-
case 'test':
|
|
136
|
-
test().catch(die);
|
|
137
|
-
break;
|
|
138
|
-
default:
|
|
139
|
-
usage();
|
|
140
|
-
process.exit(1);
|
|
141
|
-
}
|