Skip to content

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:

quillmark render ./my-quill doc.md --stdout --verbose > out.pdf

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

  1. Replace each import { Quill, Document, … } from '@quillmark/wasm' value import with a destructure of await init(). A static named import fails to link, so those sites name themselves.
  2. Grep for namespace reads — wasm.Document, mod.Quill off a await import('@quillmark/wasm') — which read undefined instead of failing to link. This is the one shape a build does not report.
  3. Move type-only uses to import type, which needs no await.
  4. Drop any runtime::not_initialized arm from diagnostic routing.
  5. Repoint anything parsing quillmark render --verbose from stdout to stderr.