0.102 → 0.103 — the init gate is the only door, and the CLI's progress lines leave stdout¶
Two breaks, on unrelated surfaces. On WASM, init() resolves to the core surface instead of void, and Quill, Document and the free functions are no longer static exports of @quillmark/wasm. On the CLI, quillmark render --verbose writes its progress lines to stderr. The Rust and Python libraries are untouched, as are documents and stored blobs.
| Break | Surface | Action |
|---|---|---|
Quill, Document, importMarkdown, exportMarkdown, rebase, mapPos, parseDocPath, formatDocPath leave the static exports |
WASM | Destructure them from await init() |
init(): Promise<void> → Promise<CoreSurface> |
WASM | Nothing, unless you asserted the resolved value |
render --verbose progress moves from stdout to stderr |
CLI | Read it from stderr; a script parsing it off stdout gets nothing |
Engine, MAIN_CARD_ADDR, isQuillmarkError, the open-set guards (isTableIsland, isHeadingLine, …) and the writer/reader classes stay exactly where they were.
The change¶
// 0.102
import { init, Quill, Document, Engine } from '@quillmark/wasm';
await init();
// 0.103
import { init, Engine } from '@quillmark/wasm';
const { Quill, Document } = await init();
init is still idempotent and concurrency-safe, so destructure at each entry point (route loader, hydration path, worker) rather than threading one result around. Every await after the first is free.
Why the door moved¶
init() returning void left the precondition carried entirely by a signature, and a floating promise is an ESLint rule (@typescript-eslint/no-floating-promises), not a tsc diagnostic. So this type-checked:
const wasm = await import('@quillmark/wasm');
wasm.init(); // floating
this.Document = wasm.Document; // throws, or does not, by load order
Load order decided the rest: an entry point that awaited something slower first reached an initialized runtime and passed, in dev and in the test that covered it. With the values reachable only through the gate, that call site has no name to call, and the outcome no longer depends on load order: a static import { Document } fails to link, and a namespace read (wasm.Document, the shape above) is undefined at every entry point rather than at some of them.
What did not change¶
Class identity. The Quill and Document the gate hands out ARE the core build's classes, verbatim — no wrapper, no subclass, no proxy. instanceof remains the whole membership test for one-wasm-per-process, and a handle from a second copy is still refused with runtime::foreign_handle.
The type exports. Quill and Document are still exported as types, so annotations and import type compile unchanged:
import type { Quill, Document } from '@quillmark/wasm';
function validate(q: Quill, d: Document) { /* … */ }
Only obtaining a value moved.
Everything gated by its arguments. Every Engine verb takes a Quill first, and the writer/reader constructors take both handles, so those classes need no gate of their own: a caller who has not awaited cannot produce an argument to call them with. new Engine() touches no WASM — it validates a descriptor map.
runtime::not_initialized is gone¶
The diagnostic code retires with the door it guarded. Nothing raises it, so a consumer routing on it can drop the arm. There is no path left to an uninstantiated build: core's values come out of the gate, the backends are loaded by Engine itself, and neither is reachable by subpath.
render --verbose writes to stderr¶
quillmark render's progress lines (Loading quill from: … through Rendering completed successfully) go to stderr, where the warning printer already writes. Under --stdout they framed the artifact:
That put Loading quill from: … ahead of the PDF header and Rendering completed successfully past its trailer, and no reader accepted the file. It writes a valid PDF now.
A script reading those lines redirects stderr (2>) rather than stdout. Only render moves: validate --verbose still writes its detail lines to stdout, as does Output written to: <path>, which prints only when the artifact goes to a file.
Checklist¶
- Replace each
import { Quill, Document, … } from '@quillmark/wasm'value import with a destructure ofawait init(). A static named import fails to link, so those sites name themselves. - Grep for namespace reads —
wasm.Document,mod.Quilloff aawait import('@quillmark/wasm')— which readundefinedinstead of failing to link. This is the one shape a build does not report. - Move type-only uses to
import type, which needs no await. - Drop any
runtime::not_initializedarm from diagnostic routing. - Repoint anything parsing
quillmark render --verbosefrom stdout to stderr.