@quillmark/wasm 0.102.0 → 0.103.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/CHANGELOG.md +76 -0
- package/README.md +36 -23
- package/backends/pdfform/wasm.js +3 -4
- package/backends/pdfform/wasm_bg.wasm +0 -0
- package/backends/typst/wasm.js +3 -4
- package/backends/typst/wasm_bg.wasm +0 -0
- package/core/wasm.js +3 -4
- package/core/wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/runtime/runtime.d.ts +64 -33
- package/runtime/runtime.js +119 -61
- package/runtime/uninit.js +0 -57
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.103.0 - 2026-08-09
|
|
4
|
+
|
|
5
|
+
- docs: `docs/integration/operations.md`, carrying what the other integration
|
|
6
|
+
pages leave unsaid: that **render is not bounded** — no deadline, no
|
|
7
|
+
cancellation, and the parse limits do not carry through — with the
|
|
8
|
+
worker-termination recipe that is the only abort a browser has; that
|
|
9
|
+
`Quillmark`, `Quill` and `Document` are `Send + Sync`, pinned by a test rather
|
|
10
|
+
than asserted; `comemo` eviction as what a long-lived process's memory tracks;
|
|
11
|
+
the no-network, no-ambient-filesystem isolation properties; and that a panic
|
|
12
|
+
is terminal on every surface.
|
|
13
|
+
- docs: `parse::input_too_large` carries four of the five §8 caps, separable
|
|
14
|
+
only by its `max` arg, which `error-handling.md` now says where it names the
|
|
15
|
+
code.
|
|
16
|
+
- test(cli): `quillmark-cli` gets its first tests. The bin carries
|
|
17
|
+
`test = false`, so twelve cases drive the built executable instead — every
|
|
18
|
+
subcommand, `-o` and `--stdout`, PDF and SVG output, and the error paths,
|
|
19
|
+
which must exit 1 rather than panic. The crate had no `[dev-dependencies]`
|
|
20
|
+
and no workflow invoked it (#1068).
|
|
21
|
+
- fix(cli)!: `render --verbose` writes its progress lines to stderr, as the
|
|
22
|
+
warning printer already did. Under `--stdout` they went to stdout ahead of and
|
|
23
|
+
after the artifact, so `quillmark render q --stdout --verbose > out.pdf`
|
|
24
|
+
produced a PDF with `Loading quill from: …` before its header and
|
|
25
|
+
`Rendering completed successfully` past its trailer. A script that parses
|
|
26
|
+
`--verbose` output from stdout reads it from stderr now.
|
|
27
|
+
- test(fuzz): `pdf_fuzz` covers the AcroForm stamp spine's byte-level reads,
|
|
28
|
+
the one hand-rolled parser with no fuzz target. Arbitrary bytes, and a real
|
|
29
|
+
form truncated, single-byte-corrupted, or spliced, all through
|
|
30
|
+
`page_media_boxes` / `PdfUpdate::begin` / `stamp`. The oracle is no panic:
|
|
31
|
+
nothing in the workspace catches unwind, so a panic there kills the CLI and
|
|
32
|
+
the Python extension and poisons the WASM module. No failures found.
|
|
33
|
+
- fix(core): `MAX_FIELD_COUNT`'s rustdoc said "per document"; the check is per
|
|
34
|
+
card-yaml block, counted after `$`-key extraction.
|
|
35
|
+
- refactor(wasm)!: `init()` resolves to the core surface, and it is the only way
|
|
36
|
+
to reach one. `Quill`, `Document`, `importMarkdown`, `exportMarkdown`,
|
|
37
|
+
`rebase`, `mapPos`, `parseDocPath` and `formatDocPath` leave the static
|
|
38
|
+
exports of `@quillmark/wasm`: `const { Quill, Document } = await init()`
|
|
39
|
+
replaces the value import. The precondition was carried entirely by
|
|
40
|
+
`init`'s signature, and a floating promise is an ESLint rule rather than a
|
|
41
|
+
`tsc` diagnostic, so a call site that skipped the await type-checked and then
|
|
42
|
+
passed or failed by load order. It now has no name to call. `Engine`,
|
|
43
|
+
`MAIN_CARD_ADDR`, `isQuillmarkError`, the open-set guards and the
|
|
44
|
+
writer/reader classes are unchanged, needing no instance or gated by their
|
|
45
|
+
arguments; the `Quill` / `Document` **type** exports are unchanged, so
|
|
46
|
+
annotations and `import type` compile as before. Class identity is untouched:
|
|
47
|
+
the gate hands out the core build's classes verbatim, and `instanceof` stays
|
|
48
|
+
the whole membership test. `runtime::not_initialized` and the build-time
|
|
49
|
+
sentinel that raised it retire with the door they guarded. Rust, Python,
|
|
50
|
+
documents and stored blobs are unaffected. See
|
|
51
|
+
`docs/migrations/0.102-to-0.103.md`
|
|
52
|
+
|
|
53
|
+
<!-- seed: commits since v0.102.0, confirm the entries above cover them, then delete this comment
|
|
54
|
+
- ci: tag the merge commit, and serialize a release against itself
|
|
55
|
+
- ci: drop the tagger GitHub App from the release workflows
|
|
56
|
+
- Cut SECURITY.md, and operations.md down to what only it can say
|
|
57
|
+
- Drop the audit gate; keep the reachability finding in ci.yml's note
|
|
58
|
+
- Fix two wrong claims, a dead property, and the stdout bug the review found
|
|
59
|
+
- dense-prose pass over the added prose, and read the fixture once
|
|
60
|
+
- Close four production-readiness gaps: audit gate, ops docs, CLI tests, PDF fuzzing
|
|
61
|
+
- Drop the README's claim about the deleted sentinel patch
|
|
62
|
+
- dense-prose pass: cut the restatement the change introduced
|
|
63
|
+
- docs: the 0.102 → 0.103 step, and correct the surface it describes
|
|
64
|
+
- The gate is the only door to the core surface
|
|
65
|
+
- docs(migration): a span guide for 0.92 → 0.102
|
|
66
|
+
- Cut over-specified style rules; author preference wins
|
|
67
|
+
- dense-prose: remove the em-dash ban
|
|
68
|
+
- dense-prose: drop the line-budget clause
|
|
69
|
+
- dense-prose: wrong is worse than missing; relax em-dash ban
|
|
70
|
+
- docs: dense-prose pass over the conform-scope and date-grammar prose
|
|
71
|
+
- docs: migration guides are era-stamped, not immutable
|
|
72
|
+
- docs(migrations): give the datetime split its corpus audit step
|
|
73
|
+
- docs: scope conform to content fields, state the date corpus precondition
|
|
74
|
+
- Tighten the failure-delivery prose
|
|
75
|
+
- Deliver init's conflict as a rejection, not a synchronous throw
|
|
76
|
+
-->
|
|
77
|
+
|
|
78
|
+
|
|
3
79
|
## v0.102.0 - 2026-08-04
|
|
4
80
|
|
|
5
81
|
The pre-1.0 vocabulary reset. Verbs, diagnostic codes, and two words that meant
|
package/README.md
CHANGED
|
@@ -10,11 +10,11 @@ Use Quillmark in browsers/Node.js with explicit in-memory trees (`Map<string, Ui
|
|
|
10
10
|
|
|
11
11
|
The package exposes **one import surface**:
|
|
12
12
|
|
|
13
|
-
- `@quillmark/wasm` (the root), the **canonical API**: `
|
|
14
|
-
an `Engine` that renders them.
|
|
13
|
+
- `@quillmark/wasm` (the root), the **canonical API**: `init`, resolving to
|
|
14
|
+
`Quill` and `Document`, and an `Engine` that renders them.
|
|
15
15
|
|
|
16
|
-
`Quill` and `Document` are
|
|
17
|
-
|
|
16
|
+
`Quill` and `Document` are the internal Typst-less core build's own classes,
|
|
17
|
+
handed out verbatim by `init`, so editor/validation code (`Quill.fromTree`,
|
|
18
18
|
`Document.fromMarkdown`) loads only that small core binary: no backend is
|
|
19
19
|
loaded until you render. The `Engine` hides everything else: each backend
|
|
20
20
|
(`typst`, `pdfform`) is a separate, private WASM binary with its own linear
|
|
@@ -31,9 +31,8 @@ bash scripts/build-wasm.sh
|
|
|
31
31
|
The script builds three variants: the core (no backend), the Typst backend
|
|
32
32
|
(default features), and the Typst-free pdfform backend (`pdfform` feature):
|
|
33
33
|
each with `--target web` and `--weak-refs` enabled (see
|
|
34
|
-
[Initialization](#initialization) and [Lifecycle](#lifecycle)). It then
|
|
35
|
-
|
|
36
|
-
asserts none of them carries a `.wasm` ESM import or a top-level await.
|
|
34
|
+
[Initialization](#initialization) and [Lifecycle](#lifecycle)). It then asserts
|
|
35
|
+
none of them carries a `.wasm` ESM import or a top-level await.
|
|
37
36
|
|
|
38
37
|
## Test
|
|
39
38
|
|
|
@@ -47,9 +46,9 @@ npm test
|
|
|
47
46
|
## Usage
|
|
48
47
|
|
|
49
48
|
```ts
|
|
50
|
-
import { init,
|
|
49
|
+
import { init, Engine } from "@quillmark/wasm";
|
|
51
50
|
|
|
52
|
-
await init();
|
|
51
|
+
const { Quill, Document } = await init(); // see Initialization
|
|
53
52
|
|
|
54
53
|
const quill = Quill.fromTree(tree); // no engine needed: build + validate
|
|
55
54
|
const engine = new Engine(); // loads a backend lazily on first render
|
|
@@ -68,12 +67,13 @@ const result = await engine.render(quill, parsed, { format: "pdf" });
|
|
|
68
67
|
|
|
69
68
|
## Initialization
|
|
70
69
|
|
|
71
|
-
`
|
|
72
|
-
after
|
|
70
|
+
`init` resolves to the core surface: `Quill`, `Document`, and the free
|
|
71
|
+
functions. Everything after the await is the synchronous surface the rest of
|
|
72
|
+
this README describes.
|
|
73
73
|
|
|
74
74
|
```js
|
|
75
|
-
import { init,
|
|
76
|
-
await init();
|
|
75
|
+
import { init, Engine } from "@quillmark/wasm";
|
|
76
|
+
const { Quill, Document } = await init();
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
The same line works everywhere: the binary streams from a URL in a browser and
|
|
@@ -83,10 +83,11 @@ the builds are `--target web`, so nothing in the package graph imports a `.wasm`
|
|
|
83
83
|
module or carries a top-level await, and a static `import` of this package is
|
|
84
84
|
safe anywhere, SSR included.
|
|
85
85
|
|
|
86
|
-
`init` is idempotent and concurrency-safe: every call returns
|
|
87
|
-
so several entry points may each `await init()` for one
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
`init` is idempotent and concurrency-safe: every non-conflicting call returns
|
|
87
|
+
the same promise, so several entry points may each `await init()` for one
|
|
88
|
+
instantiation. Destructure at **every** entry point (route loader, hydration
|
|
89
|
+
path, worker) rather than threading one result around. A failed init clears the
|
|
90
|
+
memo, so a retry works. Each realm initializes its own copy, a Worker included.
|
|
90
91
|
|
|
91
92
|
**Backends need nothing.** `Engine` instantiates a backend inside its lazy load,
|
|
92
93
|
on the first render against it.
|
|
@@ -94,13 +95,17 @@ on the first render against it.
|
|
|
94
95
|
**Overriding the source.** `init(source)` accepts bytes, a `Response`, a
|
|
95
96
|
`WebAssembly.Module`, or a URL, for hosts that route assets themselves or embed
|
|
96
97
|
the binary. Pass it on the first call; a later call passing a *different* source
|
|
97
|
-
|
|
98
|
-
same value again is fine, so several entry points may each
|
|
99
|
-
against one constant.
|
|
98
|
+
rejects with `runtime::init_conflict` rather than silently ignoring it. Passing
|
|
99
|
+
the same value again is fine, so several entry points may each
|
|
100
|
+
`await init(BYTES)` against one constant.
|
|
100
101
|
|
|
101
|
-
**
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
**Both failures reject.** `runtime::init_conflict` and `runtime::init_failed`
|
|
103
|
+
alike ride the returned promise, so one `catch` around `await init(...)` covers
|
|
104
|
+
the gate. See [Errors](#errors) for the rule this follows.
|
|
105
|
+
|
|
106
|
+
**You cannot forget.** The core surface has no static export, so a call site
|
|
107
|
+
that skips the await has no name to call. The precondition is structural rather
|
|
108
|
+
than a convention: no load order can make one entry point pass and another fail.
|
|
104
109
|
|
|
105
110
|
**Vite's dev server** pre-bundles dependencies, which moves the package away
|
|
106
111
|
from its binary. Exclude it:
|
|
@@ -547,6 +552,14 @@ try {
|
|
|
547
552
|
}
|
|
548
553
|
```
|
|
549
554
|
|
|
555
|
+
**Delivery follows the function, not the failure.** A synchronous method throws;
|
|
556
|
+
a promise-returning one rejects. The promise-returning surface is `init` and the
|
|
557
|
+
four `Engine` verbs (`render`, `open`, `supportedFormats`, `supportsCanvas`), so
|
|
558
|
+
a programming error reached through one of them (a foreign handle, an
|
|
559
|
+
unregistered backend) rejects like any other failure. Nothing here both returns
|
|
560
|
+
a promise and throws, so a `.catch` on a promise-returning call is a whole
|
|
561
|
+
guard.
|
|
562
|
+
|
|
550
563
|
`QuillmarkError` is a **structural interface, not a class**: the WASM layer
|
|
551
564
|
throws a real `Error` and attaches the property, so there is no constructor to
|
|
552
565
|
`instanceof` against; narrow with `isQuillmarkError` (which also works on
|
package/backends/pdfform/wasm.js
CHANGED
|
@@ -2885,8 +2885,7 @@ if (!('encodeInto' in cachedTextEncoder)) {
|
|
|
2885
2885
|
|
|
2886
2886
|
let WASM_VECTOR_LEN = 0;
|
|
2887
2887
|
|
|
2888
|
-
|
|
2889
|
-
let wasmModule, wasm = uninitSentinel("@quillmark/wasm internal error: the 'pdfform' backend was used before instantiation.", "This is a bug in @quillmark/wasm, not in your code. Please report it.");
|
|
2888
|
+
let wasmModule, wasm;
|
|
2890
2889
|
function __wbg_finalize_init(instance, module) {
|
|
2891
2890
|
wasm = instance.exports;
|
|
2892
2891
|
wasmModule = module;
|
|
@@ -2933,7 +2932,7 @@ async function __wbg_load(module, imports) {
|
|
|
2933
2932
|
}
|
|
2934
2933
|
|
|
2935
2934
|
function initSync(module) {
|
|
2936
|
-
if (wasm !== undefined
|
|
2935
|
+
if (wasm !== undefined) return wasm;
|
|
2937
2936
|
|
|
2938
2937
|
|
|
2939
2938
|
if (module !== undefined) {
|
|
@@ -2953,7 +2952,7 @@ function initSync(module) {
|
|
|
2953
2952
|
}
|
|
2954
2953
|
|
|
2955
2954
|
async function __wbg_init(module_or_path) {
|
|
2956
|
-
if (wasm !== undefined
|
|
2955
|
+
if (wasm !== undefined) return wasm;
|
|
2957
2956
|
|
|
2958
2957
|
|
|
2959
2958
|
if (module_or_path !== undefined) {
|
|
Binary file
|
package/backends/typst/wasm.js
CHANGED
|
@@ -2909,8 +2909,7 @@ if (!('encodeInto' in cachedTextEncoder)) {
|
|
|
2909
2909
|
|
|
2910
2910
|
let WASM_VECTOR_LEN = 0;
|
|
2911
2911
|
|
|
2912
|
-
|
|
2913
|
-
let wasmModule, wasm = uninitSentinel("@quillmark/wasm internal error: the 'typst' backend was used before instantiation.", "This is a bug in @quillmark/wasm, not in your code. Please report it.");
|
|
2912
|
+
let wasmModule, wasm;
|
|
2914
2913
|
function __wbg_finalize_init(instance, module) {
|
|
2915
2914
|
wasm = instance.exports;
|
|
2916
2915
|
wasmModule = module;
|
|
@@ -2957,7 +2956,7 @@ async function __wbg_load(module, imports) {
|
|
|
2957
2956
|
}
|
|
2958
2957
|
|
|
2959
2958
|
function initSync(module) {
|
|
2960
|
-
if (wasm !== undefined
|
|
2959
|
+
if (wasm !== undefined) return wasm;
|
|
2961
2960
|
|
|
2962
2961
|
|
|
2963
2962
|
if (module !== undefined) {
|
|
@@ -2977,7 +2976,7 @@ function initSync(module) {
|
|
|
2977
2976
|
}
|
|
2978
2977
|
|
|
2979
2978
|
async function __wbg_init(module_or_path) {
|
|
2980
|
-
if (wasm !== undefined
|
|
2979
|
+
if (wasm !== undefined) return wasm;
|
|
2981
2980
|
|
|
2982
2981
|
|
|
2983
2982
|
if (module_or_path !== undefined) {
|
|
Binary file
|
package/core/wasm.js
CHANGED
|
@@ -2377,8 +2377,7 @@ if (!('encodeInto' in cachedTextEncoder)) {
|
|
|
2377
2377
|
|
|
2378
2378
|
let WASM_VECTOR_LEN = 0;
|
|
2379
2379
|
|
|
2380
|
-
|
|
2381
|
-
let wasmModule, wasm = uninitSentinel("@quillmark/wasm is not initialized. Call 'await init()' once at startup, before Quill, Document, Engine, or any other export is used.", "Add: import { init } from '@quillmark/wasm'; await init(); once, anywhere before first use. Extra calls are free.");
|
|
2380
|
+
let wasmModule, wasm;
|
|
2382
2381
|
function __wbg_finalize_init(instance, module) {
|
|
2383
2382
|
wasm = instance.exports;
|
|
2384
2383
|
wasmModule = module;
|
|
@@ -2424,7 +2423,7 @@ async function __wbg_load(module, imports) {
|
|
|
2424
2423
|
}
|
|
2425
2424
|
|
|
2426
2425
|
function initSync(module) {
|
|
2427
|
-
if (wasm !== undefined
|
|
2426
|
+
if (wasm !== undefined) return wasm;
|
|
2428
2427
|
|
|
2429
2428
|
|
|
2430
2429
|
if (module !== undefined) {
|
|
@@ -2444,7 +2443,7 @@ function initSync(module) {
|
|
|
2444
2443
|
}
|
|
2445
2444
|
|
|
2446
2445
|
async function __wbg_init(module_or_path) {
|
|
2447
|
-
if (wasm !== undefined
|
|
2446
|
+
if (wasm !== undefined) return wasm;
|
|
2448
2447
|
|
|
2449
2448
|
|
|
2450
2449
|
if (module_or_path !== undefined) {
|
package/core/wasm_bg.wasm
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/runtime/runtime.d.ts
CHANGED
|
@@ -1,46 +1,82 @@
|
|
|
1
1
|
// @quillmark/wasm/runtime: canonical consumer API.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// with a wrapper is a breaking design change, not a refactor. See runtime.js.
|
|
3
|
+
// Render-side types (`RenderResult`, `RenderOptions`, `Artifact`,
|
|
4
|
+
// `OutputFormat`, `PageSize`, `PaintOptions`, `PaintResult`) are defined HERE as
|
|
5
|
+
// the canonical, backend-neutral render contract: NOT sourced from any one
|
|
6
|
+
// private backend build. A type-level drift guard (`runtime.types.test-d.ts`,
|
|
7
|
+
// via `npm run typecheck`) asserts they stay mutually assignable with the Typst
|
|
8
|
+
// backend's generated declarations. `Engine` is the render dispatcher that hides
|
|
9
|
+
// the cross-WASM-memory seam.
|
|
10
|
+
|
|
11
|
+
// CANONICAL INVARIANT: the `Quill`/`Document` `init` resolves to ARE the core
|
|
12
|
+
// build's classes, their full surface, never wrappers. There is exactly one
|
|
13
|
+
// public entry point, so this is a structural fact. Handing out a wrapper is a
|
|
14
|
+
// breaking design change, not a refactor. See runtime.js.
|
|
16
15
|
//
|
|
17
16
|
// ONE COPY PER PROCESS: two copies of this package are two WASM linear memories
|
|
18
17
|
// and two `Quill`/`Document` classes. Every method taking a handle refuses one
|
|
19
18
|
// belonging to another copy, with a `QuillmarkError` naming `npm ls
|
|
20
19
|
// @quillmark/wasm`. Errors are the exception: `isQuillmarkError` is structural.
|
|
21
|
-
export { Quill, Document } from '../core/wasm.js';
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
// The instance types, so an annotation (`let q: Quill`) needs no await. Their
|
|
22
|
+
// values are `CoreSurface`'s.
|
|
23
|
+
export type { Quill, Document } from '../core/wasm.js';
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
InitInput,
|
|
27
|
+
Quill as CoreQuill,
|
|
28
|
+
Document as CoreDocument,
|
|
29
|
+
importMarkdown,
|
|
30
|
+
exportMarkdown,
|
|
31
|
+
rebase,
|
|
32
|
+
mapPos,
|
|
33
|
+
parseDocPath,
|
|
34
|
+
formatDocPath
|
|
35
|
+
} from '../core/wasm.js';
|
|
24
36
|
|
|
25
37
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
38
|
+
* The core build's surface: what its WASM instance stands behind, and therefore
|
|
39
|
+
* what `init` resolves to. Exported nowhere statically, so awaiting is the only
|
|
40
|
+
* way to hold one.
|
|
28
41
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
42
|
+
* `Quill` and `Document` here are the classes, statics included
|
|
43
|
+
* (`Quill.fromTree`, `Document.fromMarkdown`), not the instance types above.
|
|
44
|
+
* Each member carries the core build's own declaration, docs and all.
|
|
45
|
+
*/
|
|
46
|
+
export interface CoreSurface {
|
|
47
|
+
Quill: typeof CoreQuill;
|
|
48
|
+
Document: typeof CoreDocument;
|
|
49
|
+
importMarkdown: typeof importMarkdown;
|
|
50
|
+
exportMarkdown: typeof exportMarkdown;
|
|
51
|
+
rebase: typeof rebase;
|
|
52
|
+
mapPos: typeof mapPos;
|
|
53
|
+
parseDocPath: typeof parseDocPath;
|
|
54
|
+
formatDocPath: typeof formatDocPath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Instantiate the core WASM build and resolve to its surface.
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { init } from '@quillmark/wasm';
|
|
62
|
+
* const { Quill, Document } = await init();
|
|
32
63
|
* ```
|
|
33
64
|
*
|
|
34
65
|
* The builds are `--target web`: classes export synchronously, the instance
|
|
35
66
|
* behind them arrives here. Identical in every environment: the binary is
|
|
36
67
|
* fetched and streamed in a browser, read off disk under Node, and the call
|
|
37
|
-
* site is the same line.
|
|
38
|
-
*
|
|
68
|
+
* site is the same line.
|
|
69
|
+
*
|
|
70
|
+
* THE ONLY DOOR to `Quill`, `Document` and the free functions, so the pre-init
|
|
71
|
+
* mistake is not expressible. Destructure at each entry point (route loader,
|
|
72
|
+
* hydration path, worker) rather than threading one result around: the gate is
|
|
73
|
+
* memoized and concurrency-safe, so every await after the first is free. A
|
|
74
|
+
* failed init clears the memo, so a retry is possible. Per realm: a Worker
|
|
75
|
+
* loads and initializes its own copy.
|
|
39
76
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* initializes its own copy.
|
|
77
|
+
* Both failure codes REJECT, so one `catch` covers the gate. Delivery follows
|
|
78
|
+
* the function kind across this surface: a sync verb throws, a
|
|
79
|
+
* promise-returning verb rejects, and nothing does both.
|
|
44
80
|
*
|
|
45
81
|
* Backends are NOT initialized here. `Engine` instantiates a backend inside its
|
|
46
82
|
* lazy load, on first render against it.
|
|
@@ -48,15 +84,10 @@ import type { InitInput } from '../core/wasm.js';
|
|
|
48
84
|
* @param source override the binary's source (bytes, a `Response`, a
|
|
49
85
|
* `WebAssembly.Module`, a URL) for hosts that route assets themselves or
|
|
50
86
|
* embed the binary. Pass it on the FIRST call; a later call passing a
|
|
51
|
-
* different source
|
|
87
|
+
* different source rejects with `runtime::init_conflict` rather than silently
|
|
52
88
|
* ignoring it. Passing the same value again is fine.
|
|
53
89
|
*/
|
|
54
|
-
export declare function init(source?: InitInput): Promise<
|
|
55
|
-
// The document-free content codec, re-exported from the core build.
|
|
56
|
-
export { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
|
|
57
|
-
// The document-model path parser/serializer: route on `Diagnostic.path`
|
|
58
|
-
// segments instead of regexing the string.
|
|
59
|
-
export { parseDocPath, formatDocPath } from '../core/wasm.js';
|
|
90
|
+
export declare function init(source?: InitInput): Promise<CoreSurface>;
|
|
60
91
|
|
|
61
92
|
import type { CardAddr } from '../core/wasm.js';
|
|
62
93
|
|
package/runtime/runtime.js
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
//
|
|
3
3
|
// @quillmark/wasm/runtime: the canonical consumer API.
|
|
4
4
|
//
|
|
5
|
-
// Consumers
|
|
6
|
-
// the build-specific subpaths. The package
|
|
7
|
-
// SEPARATE linear memories: a Typst-less
|
|
8
|
-
// the canonical home of `Quill`/`Document`,
|
|
9
|
-
// backend (`backends/typst/` today; more
|
|
10
|
-
// handle from one memory cannot be used by
|
|
11
|
-
// and is exposed at the package root
|
|
5
|
+
// Consumers reach `Quill` and `Document` through `await init()` and `Engine`
|
|
6
|
+
// as a static export, and never touch the build-specific subpaths. The package
|
|
7
|
+
// ships multiple WASM binaries with SEPARATE linear memories: a Typst-less
|
|
8
|
+
// `core` build (small, eager) that is the canonical home of `Quill`/`Document`,
|
|
9
|
+
// and one private backend binary per backend (`backends/typst/` today; more
|
|
10
|
+
// later) that carries an engine. A handle from one memory cannot be used by
|
|
11
|
+
// another. This module hides that seam and is exposed at the package root
|
|
12
|
+
// (`@quillmark/wasm`):
|
|
12
13
|
//
|
|
13
|
-
// - `Quill` and `Document` ARE the core build's classes,
|
|
14
|
+
// - `Quill` and `Document` ARE the core build's classes, handed out by the
|
|
15
|
+
// gate (§ "Initialization"). They
|
|
14
16
|
// hold the canonical data and the full sync surface (schema / validate /
|
|
15
17
|
// seed / mutate / toJson / toTree). No backend is loaded to use them, so
|
|
16
18
|
// the editor/validation path never pays for a multi-MB backend binary.
|
|
@@ -38,51 +40,42 @@
|
|
|
38
40
|
// The cross-memory crossing is therefore invisible: a consumer hands canonical
|
|
39
41
|
// `Quill`/`Document` to `engine.render(...)` and gets a `RenderResult` back.
|
|
40
42
|
|
|
41
|
-
// ── CANONICAL INVARIANT:
|
|
42
|
-
// The
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// (`toTree`/`toJson`).
|
|
43
|
+
// ── CANONICAL INVARIANT: hand out the core build's classes, never wrap ──────
|
|
44
|
+
// The `Quill`/`Document` a consumer holds ARE the core build's classes, NOT
|
|
45
|
+
// subclasses or wrappers. `init` resolves to them (§ "Initialization"); which
|
|
46
|
+
// door they come through changes nothing about the identity. The only boundary
|
|
47
|
+
// that needs crossing is core→backend (a separate WASM memory), which `Engine`
|
|
48
|
+
// does internally as data (`toTree`/`toJson`).
|
|
48
49
|
//
|
|
49
|
-
// Do NOT
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// invariant.
|
|
54
|
-
//
|
|
55
|
-
// It is also why the pre-init guard sits inside the generated builds instead
|
|
56
|
-
// of here: nothing may stand between a consumer and these classes
|
|
57
|
-
// (runtime/uninit.js).
|
|
50
|
+
// Do NOT hand out a wrapper: that breaks the identity and turns a structural
|
|
51
|
+
// fact into a converted type (a breaking design change, not a refactor). The
|
|
52
|
+
// `runtime.test.js` "hands out the internal core build classes verbatim" case
|
|
53
|
+
// (`Quill === CoreQuill`) is the executable guard for this invariant.
|
|
58
54
|
//
|
|
59
55
|
// The identity is what makes `instanceof` the whole membership test: a handle
|
|
60
56
|
// either belongs to this copy's classes or it belongs to another copy, and the
|
|
61
57
|
// second is always a consumer bug. `Engine` is NOT duck-typed on its inputs; it
|
|
62
58
|
// checks them. See § "Handles from another copy" below.
|
|
63
59
|
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// re-export keeps the identity: the exported `Quill` IS the core class.
|
|
60
|
+
// Local bindings, so this module can augment them: `quill.writer(doc)` is
|
|
61
|
+
// patched onto the prototype below, and `instanceof` reads them directly.
|
|
67
62
|
//
|
|
68
63
|
// The default import is the core build's generated instantiation entry
|
|
69
64
|
// (`--target web`); `init` below is the only thing that calls it.
|
|
70
65
|
import initCore, { Quill, Document } from '../core/wasm.js';
|
|
71
|
-
export { Quill, Document };
|
|
72
66
|
// The wasm byte source, resolved per environment by package.json's `imports`
|
|
73
67
|
// map: a pass-through in a browser (the glue fetches and streams the URL
|
|
74
68
|
// itself), a `node:fs` read under Node, whose `fetch` rejects `file:` URLs.
|
|
75
69
|
// Resolution-time, so `node:fs` never enters a browser graph.
|
|
76
70
|
import { toModuleSource } from '#quillmark-env';
|
|
77
|
-
// The document-free content codec:
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
export { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
|
|
71
|
+
// The document-free content codec: `exportMarkdown(body)` (the on-demand
|
|
72
|
+
// markdown projection), `importMarkdown`, and the position-mapping pair
|
|
73
|
+
// (`rebase`, `mapPos`).
|
|
74
|
+
import { importMarkdown, exportMarkdown, rebase, mapPos } from '../core/wasm.js';
|
|
82
75
|
// The document-model path parser/serializer: `parseDocPath(str) => DocPathSeg[]`
|
|
83
76
|
// and its inverse `formatDocPath`, so a consumer routes on `Diagnostic.path`
|
|
84
77
|
// segments instead of reverse-engineering the grammar.
|
|
85
|
-
|
|
78
|
+
import { parseDocPath, formatDocPath } from '../core/wasm.js';
|
|
86
79
|
|
|
87
80
|
// ── Initialization ──────────────────────────────────────────────────────────
|
|
88
81
|
// The builds are `--target web`: they export their classes synchronously but
|
|
@@ -90,64 +83,129 @@ export { parseDocPath, formatDocPath } from '../core/wasm.js';
|
|
|
90
83
|
// that for core, behind one awaited gate; `Engine` owns it for the backends,
|
|
91
84
|
// inside their lazy load, so a consumer never initializes a backend by hand.
|
|
92
85
|
//
|
|
86
|
+
// THE GATE IS THE ONLY DOOR. `init` resolves to the core surface, and this
|
|
87
|
+
// module exports none of it statically, so a handle is unobtainable without
|
|
88
|
+
// having awaited: the precondition is structural rather than a convention the
|
|
89
|
+
// caller has to know. package.json's `exports` map carries exactly one entry,
|
|
90
|
+
// so there is no subpath around the gate either.
|
|
91
|
+
//
|
|
93
92
|
// The gate is the shape the lazy-backend idiom (§ DEFAULT_BACKENDS) takes when
|
|
94
93
|
// the surface it guards cannot be async: `Quill.fromTree` and `seedDocument`
|
|
95
94
|
// are sync and static, so there is nowhere to hide an await except in front.
|
|
96
95
|
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
96
|
+
// WHAT STAYS STATIC is what needs no instance. `MAIN_CARD_ADDR`, the open-set
|
|
97
|
+
// guards and `isQuillmarkError` are pure JS over plain objects; gating them
|
|
98
|
+
// would cost a consumer of one an await it has no use for.
|
|
99
|
+
//
|
|
100
|
+
// The classes stay static too, and are gated by their ARGUMENTS. Every `Engine`
|
|
101
|
+
// verb takes a `Quill` first (`#backendOf` is the single reader), and the
|
|
102
|
+
// writer/reader constructors take both handles, so a caller who has not awaited
|
|
103
|
+
// cannot produce an argument to call them with. `new Engine()` alone touches no
|
|
104
|
+
// wasm: it validates a descriptor map. Holding them out of the gate keeps them
|
|
105
|
+
// tree-shakable, so the editor path drops the dispatcher it never calls.
|
|
106
|
+
//
|
|
107
|
+
// FAILURE DELIVERY follows the FUNCTION kind, not the failure kind: a sync verb
|
|
108
|
+
// throws, a promise-returning verb rejects, and nothing does both. A
|
|
109
|
+
// programming error reached through a promise-returning verb
|
|
110
|
+
// (`runtime::foreign_handle` inside `Engine.render`) rejects like any other.
|
|
111
|
+
// `init` is the one promise-returning export not declared `async`, because the
|
|
112
|
+
// memo is returned by identity; its conflict guard rejects explicitly to hold
|
|
113
|
+
// the rule, which the return type cannot declare and a `.catch` would not see.
|
|
100
114
|
|
|
101
|
-
/**
|
|
102
|
-
*
|
|
115
|
+
/**
|
|
116
|
+
* The gated surface: the core build's values, which are exactly the ones its
|
|
117
|
+
* instance stands behind. Frozen and built at module scope, because the classes
|
|
118
|
+
* and functions themselves resolve synchronously (only the instance behind them
|
|
119
|
+
* is late), so there is nothing to defer and no per-call allocation.
|
|
120
|
+
*
|
|
121
|
+
* Membership is derived, not chosen: it is the core build's exports minus its
|
|
122
|
+
* instantiation machinery (`default`, `initSync`, and the start-section
|
|
123
|
+
* `start`), which `init` owns and no consumer calls. `init.test.js` § "the
|
|
124
|
+
* gated surface" computes that set and pins it, so a new core export that never
|
|
125
|
+
* reaches here fails there rather than going missing.
|
|
126
|
+
* @type {import('./runtime.js').CoreSurface}
|
|
127
|
+
*/
|
|
128
|
+
const CORE_SURFACE = Object.freeze({
|
|
129
|
+
Quill,
|
|
130
|
+
Document,
|
|
131
|
+
importMarkdown,
|
|
132
|
+
exportMarkdown,
|
|
133
|
+
rebase,
|
|
134
|
+
mapPos,
|
|
135
|
+
parseDocPath,
|
|
136
|
+
formatDocPath
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
/** The in-flight or settled core instantiation, resolving to `CORE_SURFACE`.
|
|
140
|
+
* The memo is the PROMISE, not a boolean, so concurrent callers share one
|
|
141
|
+
* instantiation instead of racing.
|
|
142
|
+
* @type {Promise<import('./runtime.js').CoreSurface> | undefined} */
|
|
103
143
|
let coreInit;
|
|
104
144
|
/** The source `init` was first called with; the conflict check reads it. */
|
|
105
145
|
let coreInitSource;
|
|
106
146
|
|
|
107
147
|
/**
|
|
108
|
-
* Instantiate the core WASM build
|
|
109
|
-
* export is used; extra calls are free.
|
|
148
|
+
* Instantiate the core WASM build and resolve to its surface.
|
|
110
149
|
*
|
|
111
150
|
* ```js
|
|
112
|
-
* import { init
|
|
113
|
-
* await init();
|
|
151
|
+
* import { init } from '@quillmark/wasm';
|
|
152
|
+
* const { Quill, Document } = await init();
|
|
114
153
|
* ```
|
|
115
154
|
*
|
|
155
|
+
* The classes and the free functions come from here and nowhere else, so the
|
|
156
|
+
* pre-init mistake is not expressible. Destructure at each entry point (route
|
|
157
|
+
* loader, hydration path, worker) rather than threading one result around: the
|
|
158
|
+
* gate is memoized, so every await after the first is free.
|
|
159
|
+
*
|
|
116
160
|
* Identical in every environment: in a browser the binary is fetched and
|
|
117
161
|
* streamed, under Node it is read off disk, and the call site is the same line.
|
|
118
162
|
*
|
|
119
|
-
* Idempotent and concurrency-safe: every call returns the same
|
|
120
|
-
*
|
|
121
|
-
*
|
|
163
|
+
* Idempotent and concurrency-safe: every non-conflicting call returns the same
|
|
164
|
+
* promise, so several entry points cost one instantiation. A failed init clears
|
|
165
|
+
* the memo, so a retry is possible.
|
|
166
|
+
*
|
|
167
|
+
* Both failures reject (§ "Initialization", FAILURE DELIVERY): one `catch`
|
|
168
|
+
* around `await init(...)` covers `runtime::init_conflict` and
|
|
169
|
+
* `runtime::init_failed` alike.
|
|
122
170
|
*
|
|
123
171
|
* @param {import('../core/wasm.js').InitInput} [source] override the binary's
|
|
124
172
|
* source (bytes, a `Response`, a `WebAssembly.Module`, a URL) for hosts that
|
|
125
173
|
* route assets themselves or embed the binary. Pass it on the FIRST call; a
|
|
126
|
-
* later call passing a *different* source
|
|
127
|
-
* rather than silently ignoring it. Passing the same
|
|
128
|
-
* several entry points may each `await init(BYTES)`
|
|
129
|
-
*
|
|
174
|
+
* later call passing a *different* source rejects with
|
|
175
|
+
* `runtime::init_conflict` rather than silently ignoring it. Passing the same
|
|
176
|
+
* value again is fine, so several entry points may each `await init(BYTES)`
|
|
177
|
+
* against one constant.
|
|
178
|
+
* @returns {Promise<import('./runtime.js').CoreSurface>} the core surface, once
|
|
179
|
+
* its instance is live
|
|
130
180
|
*/
|
|
131
181
|
export function init(source) {
|
|
132
182
|
if (coreInit) {
|
|
133
183
|
if (source !== undefined && source !== coreInitSource) {
|
|
134
|
-
throw
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
184
|
+
// A rejection, not a throw: this is the one failure on the
|
|
185
|
+
// promise-returning surface that could land on the caller's stack, where
|
|
186
|
+
// `init(BYTES).catch(…)` would not see it.
|
|
187
|
+
return Promise.reject(
|
|
188
|
+
quillmarkError(
|
|
189
|
+
'runtime::init_conflict',
|
|
190
|
+
'init(source): core is already initializing or initialized from a different source.',
|
|
191
|
+
'Pass a source on the first call only, or pass the same value every time.'
|
|
192
|
+
)
|
|
138
193
|
);
|
|
139
194
|
}
|
|
140
195
|
return coreInit;
|
|
141
196
|
}
|
|
142
197
|
coreInitSource = source;
|
|
143
198
|
// Assign before the first await so a synchronous second call sees the memo.
|
|
144
|
-
coreInit = instantiateCore(source).
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
coreInit = instantiateCore(source).then(
|
|
200
|
+
() => CORE_SURFACE,
|
|
201
|
+
(err) => {
|
|
202
|
+
// Self-heal, as `#resolveBackend` does: one transient failure (a 404, an
|
|
203
|
+
// offline fetch) must not poison every later attempt.
|
|
204
|
+
coreInit = undefined;
|
|
205
|
+
coreInitSource = undefined;
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
);
|
|
151
209
|
return coreInit;
|
|
152
210
|
}
|
|
153
211
|
|
package/runtime/uninit.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// The pre-initialization sentinel.
|
|
2
|
-
//
|
|
3
|
-
// wasm-bindgen's `--target web` glue holds its instance exports in one
|
|
4
|
-
// module-level binding (`let wasmModule, wasm;`) that every generated path
|
|
5
|
-
// reads through: constructors, statics, methods, free functions. Until
|
|
6
|
-
// instantiation assigns it, that binding is `undefined`, so a consumer who
|
|
7
|
-
// skipped `await init()` gets `Cannot read properties of undefined (reading
|
|
8
|
-
// 'quill_fromTree')` from inside generated code.
|
|
9
|
-
//
|
|
10
|
-
// `scripts/build-wasm.sh` (`guard_wasm_js`) patches the binding to start as
|
|
11
|
-
// this sentinel, so the same access throws a `QuillmarkError` naming the cause
|
|
12
|
-
// and the fix. The patch is three line edits per variant, shape-asserted before
|
|
13
|
-
// it applies; the logic lives here rather than inside the awk program.
|
|
14
|
-
//
|
|
15
|
-
// It sits in the generated build, not in the runtime layer, because the
|
|
16
|
-
// canonical invariant (`runtime.js` § "CANONICAL INVARIANT") requires the root
|
|
17
|
-
// to re-export `Quill`/`Document` VERBATIM: no wrapper, subclass, or proxy may
|
|
18
|
-
// stand between a consumer and those classes. Prototype patching preserves
|
|
19
|
-
// identity but cannot cover a public constructor (`new Document(...)`) without
|
|
20
|
-
// wrapping the class. The binding they all read through is the one place that
|
|
21
|
-
// covers every path and touches no class.
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* A stand-in for a wasm build's exports that throws on use. Every property read
|
|
25
|
-
* throws except the marker the patched init guards test, and `then`,
|
|
26
|
-
* `constructor`, and symbols, which return `undefined`: an incidental `await`
|
|
27
|
-
* or `util.inspect` reports nothing rather than throwing somewhere unrelated to
|
|
28
|
-
* the cause.
|
|
29
|
-
*
|
|
30
|
-
* @param {string} message what a consumer sees when they reach a build early
|
|
31
|
-
* @param {string} hint the fix, as a `Diagnostic.hint`
|
|
32
|
-
* @returns {any} the sentinel
|
|
33
|
-
*/
|
|
34
|
-
export function uninitSentinel(message, hint) {
|
|
35
|
-
return new Proxy(
|
|
36
|
-
{},
|
|
37
|
-
{
|
|
38
|
-
get(_target, prop) {
|
|
39
|
-
if (prop === UNINIT) return true;
|
|
40
|
-
if (prop === 'then' || prop === 'constructor' || typeof prop === 'symbol') {
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
|
-
const err = /** @type {any} */ (new Error(message));
|
|
44
|
-
err.diagnostics = [
|
|
45
|
-
{ severity: 'error', code: 'runtime::not_initialized', message, hint }
|
|
46
|
-
];
|
|
47
|
-
throw err;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* The marker the patched `if (wasm !== undefined)` guards read to tell a
|
|
55
|
-
* sentinel from real instance exports, which never carry it.
|
|
56
|
-
*/
|
|
57
|
-
export const UNINIT = '__quillmarkUninit';
|