@quillmark/quiver 0.16.0 → 0.17.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.
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Quiver CLI — build, test, and preview a quiver collection.
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.
4
6
  *
5
7
  * Commands:
6
8
  * quiver build [--out <dir>]
7
9
  * quiver test
8
- * quiver preview [--out <dir>] [--format <fmt>] [--quiet]
9
- * [--include <ref>...] [--exclude <ref>...]
10
10
  *
11
- * Engine discovery (for `test` and `preview`):
11
+ * Engine discovery (for `test`):
12
12
  * 1. Named export `engine` from `quiver.config.js` at the collection root,
13
13
  * if the file exists.
14
14
  * 2. Auto-imported `@quillmark/wasm` from the collection's node_modules.
@@ -1,43 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Quiver CLI — build, test, and preview a quiver collection.
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.
4
6
  *
5
7
  * Commands:
6
8
  * quiver build [--out <dir>]
7
9
  * quiver test
8
- * quiver preview [--out <dir>] [--format <fmt>] [--quiet]
9
- * [--include <ref>...] [--exclude <ref>...]
10
10
  *
11
- * Engine discovery (for `test` and `preview`):
11
+ * Engine discovery (for `test`):
12
12
  * 1. Named export `engine` from `quiver.config.js` at the collection root,
13
13
  * if the file exists.
14
14
  * 2. Auto-imported `@quillmark/wasm` from the collection's node_modules.
15
15
  */
16
- import { createRequire } from "node:module";
17
- import { join } from "node:path";
18
- import { pathToFileURL } from "node:url";
19
- import { Quiver } from "../node.js";
20
- import { renderQuiverSamples } from "../preview.js";
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';
21
20
  // ---------------------------------------------------------------------------
22
21
  // Arg parsing helpers
23
22
  // ---------------------------------------------------------------------------
24
23
  const argv = process.argv.slice(2);
25
24
  const command = argv[0];
25
+ /** One flag with a value. `build --out` is the only one either verb takes. */
26
26
  function flag(name) {
27
27
  const i = argv.indexOf(name);
28
28
  return i !== -1 && i + 1 < argv.length ? argv[i + 1] : undefined;
29
29
  }
30
- function multiFlag(name) {
31
- const out = [];
32
- for (let i = 0; i < argv.length - 1; i++) {
33
- if (argv[i] === name)
34
- out.push(argv[i + 1]);
35
- }
36
- return out;
37
- }
38
- function hasFlag(name) {
39
- return argv.includes(name);
40
- }
41
30
  // ---------------------------------------------------------------------------
42
31
  // Engine discovery
43
32
  // ---------------------------------------------------------------------------
@@ -47,16 +36,21 @@ async function loadEngine(cwd) {
47
36
  // backend itself). A quiver.config.js may export a custom `engine`.
48
37
  let wasm;
49
38
  try {
50
- const req = createRequire(pathToFileURL(join(cwd, "package.json")).href);
51
- const wasmPath = req.resolve("@quillmark/wasm");
39
+ const req = createRequire(pathToFileURL(join(cwd, 'package.json')).href);
40
+ const wasmPath = req.resolve('@quillmark/wasm');
52
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();
53
47
  }
54
48
  catch {
55
49
  // Not installed — a quiver.config.js may still provide an engine.
56
50
  }
57
51
  // 1. Named `engine` export from quiver.config.js, if present.
58
52
  try {
59
- const cfg = await import(pathToFileURL(join(cwd, "quiver.config.js")).href);
53
+ const cfg = await import(pathToFileURL(join(cwd, 'quiver.config.js')).href);
60
54
  if (cfg.engine != null) {
61
55
  return cfg.engine;
62
56
  }
@@ -67,8 +61,8 @@ async function loadEngine(cwd) {
67
61
  // 2. Auto-discover from @quillmark/wasm.
68
62
  if (wasm == null) {
69
63
  throw new Error("Cannot find @quillmark/wasm in this collection's node_modules.\n" +
70
- " Install it: npm install @quillmark/wasm\n" +
71
- " Or export { engine } from quiver.config.js for a custom engine.");
64
+ ' Install it: npm install @quillmark/wasm\n' +
65
+ ' Or export { engine } from quiver.config.js for a custom engine.');
72
66
  }
73
67
  return new wasm.Engine();
74
68
  }
@@ -77,18 +71,18 @@ async function loadEngine(cwd) {
77
71
  // ---------------------------------------------------------------------------
78
72
  async function build() {
79
73
  const cwd = process.cwd();
80
- const outDir = flag("--out") ?? "dist";
74
+ const outDir = flag('--out') ?? 'dist';
81
75
  console.log(`quiver build: ${cwd} → ${outDir}`);
82
- await Quiver.build(cwd, outDir);
83
- console.log("done.");
76
+ await buildQuiver(cwd, outDir);
77
+ console.log('done.');
84
78
  }
85
79
  async function test() {
86
80
  const cwd = process.cwd();
87
81
  const engine = await loadEngine(cwd);
88
- const quiver = await Quiver.fromDir(cwd);
82
+ const quiver = await fromDir(cwd);
89
83
  const names = quiver.quillNames();
90
84
  if (names.length === 0) {
91
- console.error("error: quiver has no quills");
85
+ console.error('error: quiver has no quills');
92
86
  process.exit(1);
93
87
  }
94
88
  let pass = 0;
@@ -107,7 +101,7 @@ async function test() {
107
101
  doc.free();
108
102
  }
109
103
  if (!Array.isArray(result.artifacts) || result.artifacts.length === 0) {
110
- throw new Error("example render produced no artifacts");
104
+ throw new Error('example render produced no artifacts');
111
105
  }
112
106
  console.log(`pass ${ref}`);
113
107
  pass++;
@@ -123,34 +117,11 @@ async function test() {
123
117
  if (fail > 0)
124
118
  process.exit(1);
125
119
  }
126
- async function preview() {
127
- const cwd = process.cwd();
128
- const engine = await loadEngine(cwd);
129
- const outDir = flag("--out");
130
- const format = flag("--format");
131
- const quiet = hasFlag("--quiet");
132
- const include = multiFlag("--include");
133
- const exclude = multiFlag("--exclude");
134
- await renderQuiverSamples(cwd, {
135
- engine,
136
- ...(outDir !== undefined && { outDir }),
137
- ...(format !== undefined && { format }),
138
- quiet,
139
- ...(include.length > 0 && { include }),
140
- ...(exclude.length > 0 && { exclude }),
141
- });
142
- }
143
120
  // ---------------------------------------------------------------------------
144
121
  // Dispatch
145
122
  // ---------------------------------------------------------------------------
146
123
  function usage() {
147
- console.error([
148
- "Usage:",
149
- " quiver build [--out <dir>]",
150
- " quiver test",
151
- " quiver preview [--out <dir>] [--format <fmt>] [--quiet]",
152
- " [--include <ref>...] [--exclude <ref>...]",
153
- ].join("\n"));
124
+ console.error(['Usage:', ' quiver build [--out <dir>]', ' quiver test'].join('\n'));
154
125
  }
155
126
  function die(err) {
156
127
  const msg = err instanceof Error ? err.message : String(err);
@@ -158,15 +129,12 @@ function die(err) {
158
129
  process.exit(1);
159
130
  }
160
131
  switch (command) {
161
- case "build":
132
+ case 'build':
162
133
  build().catch(die);
163
134
  break;
164
- case "test":
135
+ case 'test':
165
136
  test().catch(die);
166
137
  break;
167
- case "preview":
168
- preview().catch(die);
169
- break;
170
138
  default:
171
139
  usage();
172
140
  process.exit(1);
package/dist/build.d.ts CHANGED
@@ -1,25 +1,25 @@
1
1
  /**
2
2
  * Build logic — internal, Node-only.
3
3
  *
4
- * All Node.js built-in imports are done dynamically inside `buildQuiver`
5
- * so that a type-only import of `BuildOptions` from `src/index.ts` does
6
- * NOT pull `node:fs` or `node:crypto` into browser bundles.
4
+ * All Node.js built-in imports are done dynamically inside `buildQuiver`, so
5
+ * a module reaching this one for types alone does not pull `node:fs` or
6
+ * `node:crypto` into a browser bundle.
7
7
  */
8
- /** Reserved for future build options (e.g. compression level, filters). */
9
- export type BuildOptions = Record<string, never>;
10
8
  /**
11
9
  * Reads a Source Quiver, validates it, and writes the build output to outDir.
12
10
  *
13
- * Output layout:
11
+ * Output layout. Every name but the pointer carries the SHA-256 of what it
12
+ * names, which is what the loader checks on fetch:
14
13
  * outDir/
15
- * latest.json # stable pointer to the current manifest
16
- * manifest.<md5prefix6>.json # hashed manifest
17
- * <name>@<version>.<md5>.zip # one bundle per quill
14
+ * latest.json # stable pointer to the current manifest
15
+ * manifest.<sha256:12>.json # hashed manifest
16
+ * <name>@<version>.<sha256:12>.zip # one bundle per quill
18
17
  * store/
19
- * <md5> # dehydrated font bytes (full hash, no ext)
18
+ * <sha256> # dehydrated font bytes (full hash, no ext)
20
19
  *
21
20
  * Throws:
22
21
  * - `quiver_invalid` on source validation failures (propagated from scanner)
23
- * - `transport_error` on I/O failures
22
+ * - `transport_error` on I/O failures, and on an outDir the build would have
23
+ * to delete something it does not own to write (see `assertSafeOutDir`)
24
24
  */
25
- export declare function buildQuiver(sourceDir: string, outDir: string, _opts?: BuildOptions): Promise<void>;
25
+ export declare function buildQuiver(sourceDir: string, outDir: string): Promise<void>;
package/dist/build.js CHANGED
@@ -1,50 +1,85 @@
1
1
  /**
2
2
  * Build logic — internal, Node-only.
3
3
  *
4
- * All Node.js built-in imports are done dynamically inside `buildQuiver`
5
- * so that a type-only import of `BuildOptions` from `src/index.ts` does
6
- * NOT pull `node:fs` or `node:crypto` into browser bundles.
4
+ * All Node.js built-in imports are done dynamically inside `buildQuiver`, so
5
+ * a module reaching this one for types alone does not pull `node:fs` or
6
+ * `node:crypto` into a browser bundle.
7
7
  */
8
- import { QuiverError } from "./errors.js";
9
- import { packFiles } from "./bundle.js";
8
+ import { QuiverError } from './errors.js';
9
+ import { packFiles } from './bundle.js';
10
+ import { NAME_DIGEST_LENGTH } from './digest.js';
10
11
  /** Font file extensions recognised by the builder (case-insensitive). */
11
12
  const FONT_EXT = /\.(ttf|otf|woff|woff2)$/i;
13
+ /**
14
+ * The build's first act is `rm(outDir, { recursive: true })`, so an outDir that
15
+ * is, or contains, the source quiver or the working directory deletes the
16
+ * thing the caller was building from. `quiver build --out .` and a mistyped
17
+ * `--out ..` are both one keystroke away, and the failure is unrecoverable, so
18
+ * an outDir that owns the caller is refused up front.
19
+ *
20
+ * An outDir *inside* sourceDir stays allowed: the scan reads the source before
21
+ * any write, and `dist/` under the quiver root is the ordinary layout.
22
+ *
23
+ * Throws `transport_error`.
24
+ */
25
+ function assertSafeOutDir(path, sourceDir, outDir) {
26
+ const out = path.resolve(outDir);
27
+ /** True when `dir` is `target` or an ancestor of it. */
28
+ const owns = (target) => {
29
+ const rel = path.relative(out, path.resolve(target));
30
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
31
+ };
32
+ const what = owns(sourceDir)
33
+ ? 'the source quiver'
34
+ : owns(process.cwd())
35
+ ? 'the working directory'
36
+ : undefined;
37
+ if (what !== undefined) {
38
+ throw new QuiverError('transport_error', `Refusing to build into "${outDir}": the build clears its output directory, and this one holds ${what} ("${out}"). Point --out at a directory the build owns.`);
39
+ }
40
+ }
12
41
  /**
13
42
  * Reads a Source Quiver, validates it, and writes the build output to outDir.
14
43
  *
15
- * Output layout:
44
+ * Output layout. Every name but the pointer carries the SHA-256 of what it
45
+ * names, which is what the loader checks on fetch:
16
46
  * outDir/
17
- * latest.json # stable pointer to the current manifest
18
- * manifest.<md5prefix6>.json # hashed manifest
19
- * <name>@<version>.<md5>.zip # one bundle per quill
47
+ * latest.json # stable pointer to the current manifest
48
+ * manifest.<sha256:12>.json # hashed manifest
49
+ * <name>@<version>.<sha256:12>.zip # one bundle per quill
20
50
  * store/
21
- * <md5> # dehydrated font bytes (full hash, no ext)
51
+ * <sha256> # dehydrated font bytes (full hash, no ext)
22
52
  *
23
53
  * Throws:
24
54
  * - `quiver_invalid` on source validation failures (propagated from scanner)
25
- * - `transport_error` on I/O failures
55
+ * - `transport_error` on I/O failures, and on an outDir the build would have
56
+ * to delete something it does not own to write (see `assertSafeOutDir`)
26
57
  */
27
- export async function buildQuiver(sourceDir, outDir, _opts) {
58
+ export async function buildQuiver(sourceDir, outDir) {
28
59
  // Dynamic imports keep this module safe to type-import from browser contexts.
29
- const { join } = await import("node:path");
30
- const { mkdir, rm, writeFile, } = await import("node:fs/promises");
31
- const { createHash } = await import("node:crypto");
32
- const { scanSourceQuiver, readQuillTree } = await import("./source-loader.js");
60
+ const path = await import('node:path');
61
+ const { join } = path;
62
+ const { mkdir, rm, writeFile } = await import('node:fs/promises');
63
+ const { createHash } = await import('node:crypto');
64
+ const { scanSourceQuiver, readQuillTree } = await import('./source-loader.js');
65
+ // 0. The first write is a recursive delete of outDir; refuse the paths where
66
+ // that deletes the caller instead of a previous build.
67
+ assertSafeOutDir(path, sourceDir, outDir);
33
68
  // 1. Scan + validate source quiver (throws quiver_invalid on bad input).
34
69
  const { meta, catalog } = await scanSourceQuiver(sourceDir);
35
70
  // 2. Clear and recreate outDir + outDir/store/.
36
71
  try {
37
72
  await rm(outDir, { recursive: true, force: true });
38
- await mkdir(join(outDir, "store"), { recursive: true });
73
+ await mkdir(join(outDir, 'store'), { recursive: true });
39
74
  }
40
75
  catch (err) {
41
- throw new QuiverError("transport_error", `Failed to prepare output directory "${outDir}": ${err.message}`, { cause: err });
76
+ throw new QuiverError('transport_error', `Failed to prepare output directory "${outDir}": ${err.message}`, { cause: err });
42
77
  }
43
78
  // 3. Process each quill version.
44
79
  const manifestQuills = [];
45
80
  for (const [quillName, versions] of catalog) {
46
81
  for (const version of versions) {
47
- const quillDir = join(sourceDir, "quills", quillName, version);
82
+ const quillDir = join(sourceDir, 'quills', quillName, version);
48
83
  // a. Read quill file tree.
49
84
  const tree = await readQuillTree(quillDir);
50
85
  // b. Partition fonts vs content.
@@ -61,13 +96,15 @@ export async function buildQuiver(sourceDir, outDir, _opts) {
61
96
  // c. Dehydrate fonts into store/.
62
97
  const fonts = {};
63
98
  for (const [path, bytes] of fontEntries) {
64
- const hash = createHash("md5").update(bytes).digest("hex");
65
- const storePath = join(outDir, "store", hash);
99
+ // Full width: the store is keyed by hash, so two distinct fonts
100
+ // sharing a prefix would merge into one entry.
101
+ const hash = createHash('sha256').update(bytes).digest('hex');
102
+ const storePath = join(outDir, 'store', hash);
66
103
  try {
67
104
  await writeFile(storePath, bytes);
68
105
  }
69
106
  catch (err) {
70
- throw new QuiverError("transport_error", `Failed to write font store entry "${storePath}": ${err.message}`, { cause: err });
107
+ throw new QuiverError('transport_error', `Failed to write font store entry "${storePath}": ${err.message}`, { cause: err });
71
108
  }
72
109
  fonts[path] = hash;
73
110
  }
@@ -78,7 +115,10 @@ export async function buildQuiver(sourceDir, outDir, _opts) {
78
115
  }
79
116
  const zipBytes = packFiles(contentRecord);
80
117
  // e–f. Compute bundle hash and name.
81
- const bundleHash = createHash("md5").update(zipBytes).digest("hex").slice(0, 6);
118
+ const bundleHash = createHash('sha256')
119
+ .update(zipBytes)
120
+ .digest('hex')
121
+ .slice(0, NAME_DIGEST_LENGTH);
82
122
  const bundleName = `${quillName}@${version}.${bundleHash}.zip`;
83
123
  // g. Write bundle zip.
84
124
  const bundlePath = join(outDir, bundleName);
@@ -86,7 +126,7 @@ export async function buildQuiver(sourceDir, outDir, _opts) {
86
126
  await writeFile(bundlePath, zipBytes);
87
127
  }
88
128
  catch (err) {
89
- throw new QuiverError("transport_error", `Failed to write bundle "${bundlePath}": ${err.message}`, { cause: err });
129
+ throw new QuiverError('transport_error', `Failed to write bundle "${bundlePath}": ${err.message}`, { cause: err });
90
130
  }
91
131
  // h. Record manifest entry.
92
132
  manifestQuills.push({ name: quillName, version, bundle: bundleName, fonts });
@@ -96,25 +136,28 @@ export async function buildQuiver(sourceDir, outDir, _opts) {
96
136
  const manifest = {
97
137
  version: 1,
98
138
  name: meta.name,
99
- quills: manifestQuills,
139
+ quills: manifestQuills
100
140
  };
101
141
  const manifestJson = JSON.stringify(manifest, null, 2);
102
- const manifestHash = createHash("md5").update(manifestJson).digest("hex").slice(0, 6);
142
+ const manifestHash = createHash('sha256')
143
+ .update(manifestJson)
144
+ .digest('hex')
145
+ .slice(0, NAME_DIGEST_LENGTH);
103
146
  const manifestFileName = `manifest.${manifestHash}.json`;
104
147
  const manifestPath = join(outDir, manifestFileName);
105
148
  try {
106
- await writeFile(manifestPath, manifestJson, "utf-8");
149
+ await writeFile(manifestPath, manifestJson, 'utf-8');
107
150
  }
108
151
  catch (err) {
109
- throw new QuiverError("transport_error", `Failed to write manifest "${manifestPath}": ${err.message}`, { cause: err });
152
+ throw new QuiverError('transport_error', `Failed to write manifest "${manifestPath}": ${err.message}`, { cause: err });
110
153
  }
111
154
  // 9–10. Write stable pointer latest.json.
112
155
  const pointer = { manifest: manifestFileName };
113
- const pointerPath = join(outDir, "latest.json");
156
+ const pointerPath = join(outDir, 'latest.json');
114
157
  try {
115
- await writeFile(pointerPath, JSON.stringify(pointer), "utf-8");
158
+ await writeFile(pointerPath, JSON.stringify(pointer), 'utf-8');
116
159
  }
117
160
  catch (err) {
118
- throw new QuiverError("transport_error", `Failed to write pointer "${pointerPath}": ${err.message}`, { cause: err });
161
+ throw new QuiverError('transport_error', `Failed to write pointer "${pointerPath}": ${err.message}`, { cause: err });
119
162
  }
120
163
  }
@@ -1,20 +1,26 @@
1
1
  /**
2
- * Built-quiver loader — browser-safe at module level.
3
- * Internal; not exported from index.ts.
2
+ * Built-quiver loader: reads a packed artifact through a transport, validates
3
+ * its pointer and manifest, and hands back a Quiver whose loader fetches
4
+ * bundles and fonts on demand, each checked against the digest in its name.
4
5
  *
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.
6
+ * Package-internal, and browser-safe: no static `node:` imports at any level.
10
7
  */
11
- import { Quiver } from "./quiver.js";
8
+ import type { Quiver } from './quiver.js';
12
9
  /**
13
10
  * Transport abstraction: fetch raw bytes by relative path within the packed
14
- * artifact. Sole implementation is HttpTransport (browser + Node).
11
+ * artifact. Implemented by HttpTransport (browser + Node) and FsBuiltTransport
12
+ * (Node).
15
13
  */
16
14
  export interface BuiltTransport {
17
- fetchBytes(relativePath: string): Promise<Uint8Array>;
15
+ fetchBytes(relativePath: string, opts?: FetchOptions): Promise<Uint8Array>;
16
+ }
17
+ /**
18
+ * `revalidate` marks the one request that must not be answered from a cache:
19
+ * `latest.json`, the only name in the artifact that is not content-addressed.
20
+ * Everything else is safe to cache forever by construction.
21
+ */
22
+ export interface FetchOptions {
23
+ revalidate?: boolean;
18
24
  }
19
25
  /**
20
26
  * Load a build-output quiver via the given transport.
@@ -25,9 +31,3 @@ export interface BuiltTransport {
25
31
  * 4. Returns a Quiver instance backed by a BuiltLoader.
26
32
  */
27
33
  export declare function loadBuiltQuiver(transport: BuiltTransport): Promise<Quiver>;
28
- /**
29
- * Seed a quiver from caller-provided manifest bytes, skipping the latest.json
30
- * pointer fetch. `transport` is used only for lazy bundle/font fetches.
31
- * Throws `quiver_invalid` on malformed bytes.
32
- */
33
- export declare function seedBuiltQuiver(transport: BuiltTransport, manifestBytes: Uint8Array): Quiver;