0.89.0 → 0.90.0 — engine-free Quill, split WASM, one core type¶
A structural release that decouples holding a quill from rendering one, and splits the WASM package so an editor can load and validate without downloading Typst.
Three connected changes:
Quillis engine-free data. It no longer holds a backend. A newEngineis the render dispatcher; rendering and backend-dependent capability move onto it. The backend-existence check moves from load time to render time.- The WASM package ships split builds behind one canonical API. There is
exactly one public import — the root
@quillmark/wasm. It is the canonical layer: it re-exportsQuill/Documentfrom the internal Typst-less core build verbatim and adds anEnginethat lazily loads the Typst backend on first render. Engine-free editor/validation code (Quill.fromTree,Document.fromMarkdown) loads only that small core binary (~0.66 MB gzip); no backend is loaded until you render. QuillSourceandQuillcollapse into one core type,Quill(Rust surface only — bindings already hidQuillSource).
WASM / JS¶
Construction: the engine no longer loads quills¶
engine.quill(tree) is removed. Construct a quill directly — no engine needed:
- import type { Quill } from '@quillmark/wasm';
+ import { Quill } from '@quillmark/wasm'; // value import — needed for Quill.fromTree
- const engine = new Quillmark();
- const quill = engine.quill(tree);
+ const quill = Quill.fromTree(tree);
Quill (and Document) are re-exported verbatim from the internal core build,
so the single root @quillmark/wasm import is the only place you get them — no
backend is loaded to construct or validate a quill.
Note: Quill must be a value import (not import type { Quill }). Projects
using verbatimModuleSyntax or aggressive type-stripping will hit a compile
error on the static call site until the import is changed.
Quill.fromTree accepts the same Map<string, Uint8Array> (or plain object) as
before. It is the full parse-and-validate step — it throws on a malformed or
schema-invalid tree. A Quill is now portable, validated data: backendId,
metadata, schema, blueprint, validate, seedDocument / seedMain /
seedCard all work without an engine.
Testing note: Code that previously mocked engine.quill(tree) to return a
fake quill must now stub the static constructor directly. With a single public
entry point there is exactly one Quill class to spy on — the wrong-build
vi.spyOn trap no longer exists:
import { vi } from 'vitest';
import { Quill } from '@quillmark/wasm';
const spy = vi.spyOn(Quill, 'fromTree').mockImplementation((tree) => {
return fakeQuill as unknown as Quill;
});
Rendering and capability move onto the engine¶
render, open, and capability are no longer methods on Quill; they live on
the new Engine and take the quill as the first argument. The Engine methods
are async — the first call lazily loads the backend binary:
- const result = quill.render(doc, opts);
- const session = quill.open(doc);
- const canPreview = quill.supportsCanvas;
+ import { Engine } from '@quillmark/wasm';
+ const engine = new Engine();
+ const result = await engine.render(quill, doc, opts);
+ const session = await engine.open(quill, doc);
+ const canPreview = await engine.supportsCanvas(quill);
RenderSession is unchanged: session.render(opts), pageSize, paint,
pageCount, backendId, supportsCanvas, warnings. (engine.open is now
async, but the session methods it returns stay synchronous.)
supportedFormats left metadata¶
Quill.metadata is now pure config (identity only). The backend's output
formats are a resolved-backend capability — read them from the engine:
- const formats = quill.metadata.supportedFormats;
+ const formats = await engine.supportedFormats(quill); // OutputFormat[]
The [key: string]: unknown index signature has been removed from
QuillMetadata. Code that reads quill.metadata.supportedFormats now produces
a compile-time error (Property 'supportedFormats' does not exist on type
'QuillMetadata') instead of silently returning undefined at runtime.
If you genuinely need to read arbitrary extra quill: YAML keys, cast to
Record<string, unknown> (e.g. (quill.metadata as Record<string, unknown>).myKey)
as the escape hatch.
Backend errors surface at render time, not load time¶
Quill.fromTree now succeeds even when the declared backend isn't available
(the core build has no backend at all). The UnsupportedBackend error moves to
the first engine call — open / render / supportedFormats /
supportsCanvas.
One entry point¶
// Everything: Quill/Document (schema + validation, no backend loaded) plus
// the Engine that renders them.
import { Document, Quill, Engine } from '@quillmark/wasm';
The package exports map has exactly one subpath (.) — there is no /core or
/render subpath, and the root no longer exports a Quillmark class. Quill
and Document are re-exported verbatim from the internal Typst-less core build,
so engine-free editor/validation code loads only that small core binary; the
Typst backend is a separate private build with its own linear memory, lazily
loaded on the first render. The Engine clones the quill and document into that
memory and frees the clones for you, so you never hold a backend handle or cross
the seam yourself.
Rust¶
Constructors and rendering¶
- let engine = Quillmark::new();
- let quill = engine.quill_from_path(path)?; // or engine.quill(tree)
- let result = quill.render(&doc, &opts)?;
- let formats = quill.supported_formats();
+ let quill = quillmark::quill_from_path(path)?; // or Quill::from_tree(tree)
+ let engine = Quillmark::new();
+ let result = engine.render(&quill, &doc, &opts)?;
+ let formats = engine.supported_formats(&quill)?; // now returns Result
Quillmark::quill/quill_from_pathmethods are removed. Use the core constructorQuill::from_tree(tree) -> Result<Quill, Vec<Diagnostic>>, or thequillmark::quill_from_pathfree function for filesystem loading (which surfaces aRenderError; fs stays out of core).render,open,supported_formats, and the newsupports_canvasare now methods onQuillmark, each taking&quill.supported_formatsreturnsResult<_, RenderError>(it resolves the backend).validate,compile_data,dry_run,seed_*,backend_idstay onQuill.
QuillSource is gone; Backend::open takes &Quill¶
QuillSource was renamed to Quill and absorbed the wrapper's methods; the
orchestration Quill is deleted. Backends now receive &Quill:
impl Backend for MyBackend {
- fn open(&self, plate: &str, source: &QuillSource, json: &serde_json::Value)
+ fn open(&self, plate: &str, source: &Quill, json: &serde_json::Value)
-> Result<RenderSession, RenderError> { … }
+
+ // Optional: override if your backend can paint to a canvas.
+ fn supports_canvas(&self) -> bool { false }
}
If you reached the inner data via quill.source(), drop the call —
quill.config() / quill.metadata() / quill.plate() are now direct on
Quill.
Python¶
The Python binding moves to the same engine-free shape as Rust and WASM: a
Quill is portable, validated config data; render and capability live on the
Quillmark engine, against a quill.
# Before (0.89): load via the engine, render via the quill
engine = Quillmark()
quill = engine.quill_from_path("path/to/quill")
result = quill.render(doc, OutputFormat.PDF)
formats = quill.supported_formats
canvas = quill.supports_canvas
# After (0.90): load engine-free, render via the engine
engine = Quillmark()
quill = Quill.from_path("path/to/quill") # was engine.quill_from_path
result = engine.render(quill, doc, OutputFormat.PDF) # was quill.render(doc, ...)
formats = engine.supported_formats(quill) # was quill.supported_formats
canvas = engine.supports_canvas(quill) # was quill.supports_canvas
session = engine.open(quill, doc) # was quill.open(doc)
Unchanged on Quill: backend_id, quill_ref, schema, blueprint,
metadata, validate(doc), and the seed_* / $ext methods.
Two contract changes:
quill.metadatano longer carriessupportedFormatsand is now a pure, infallible config read — it never resolves a backend, so it never raises. Read capability fromengine.supported_formats(quill)instead.- Backend resolution moves from load time to render time.
Quill.from_pathsucceeds for a quill whose declared backend isn't registered; theUnsupportedBackenderror surfaces from the first engine call (render/open/supported_formats).supports_canvasis non-raising (Falsefor an unregistered backend). Unreachable in the stock build, which always registers Typst.
@quillmark/quiver¶
Coordinated release¶
@quillmark/quiver must be updated alongside this WASM release. A consumer who
updates @quillmark/wasm to >=0.90.0 must also update @quillmark/quiver to
>=0.14.0 at the same time — the older Quiver still calls the removed
engine.quill(tree) method.
Code that bypassed Quiver should migrate to getQuill, not fromTree¶
Code that previously called engine.quill(tree) directly — bypassing a
Quiver instance — should migrate to quiver.getQuill(ref), not to a
manual tree-assembly + Quill.fromTree sequence (Quiver no longer exposes
any public tree-fetch method):
// wrong — duplicates Quiver's resolution, caching, and construction
const tree = buildTreeByHand(files); // hand-assembled file tree
const quill = Quill.fromTree(tree);
// right — Quiver handles resolution, construction, and caching
const quill = await quiver.getQuill(ref);
Holding a QuillClass reference (e.g. storing wasmModule.Quill on a
service field) is only needed when constructing quills outside of a
Quiver context (e.g. a raw-tree endpoint, a test fixture). Inside a Quiver
consumer, getQuill is the entire entry point.
Quiver cache semantics changed¶
Under 0.89, Quiver.getQuill(ref, { engine }) produced a separate Quill
instance per engine. Under 0.90 there is one Quill per canonical ref for
the lifetime of the Quiver instance, shared across every engine that calls
getQuill. The tree is fetched once, Quill.fromTree is called once, and the
result is cached. This halves the number of instances in multi-engine
deployments (e.g. a live-preview engine and a thumbnail-worker engine on the
same page).
Tests that asserted per-engine isolation behavior (e.g. "distinct engines get distinct quill objects") should be removed; the shared-instance guarantee is the new invariant.
A Quiver quill goes straight into engine.render¶
@quillmark/quiver@0.14.0 materializes quills from @quillmark/wasm.
getQuill(ref) returns a Quill you use for schema inspection, validation, and
blueprint access — and for rendering. That same quill passes directly into
engine.render; the Engine does the memory crossing internally:
import { Engine } from "@quillmark/wasm";
const engine = new Engine();
// one quill — schema, validation, AND rendering
const quill = await quiver.getQuill(ref);
quill.schema; // ✓
const doc = quill.seedDocument();
quill.validate(doc); // ✓
const result = await engine.render(quill, doc, { format: "pdf" }); // ✓
Do not re-materialize the quill by hand. The 0.89-era dance of fetching a
raw tree and re-running Quill.fromTree(tree) plus a
Document.fromJson(coreDoc.toJson()) round-trip to "cross into render memory"
is an anti-pattern — the Engine clones into the backend's memory for you.
getQuill(ref) + engine.render(quill, doc) is the whole story.
Raw tree bytes. If you genuinely need the quill's underlying file tree (e.g. to inspect or re-pack it), call
quill.toTree()on the quillgetQuillalready returned — it round-trips withQuill.fromTree. There is no separate tree-fetch escape hatch: Quiver no longer exposesgetTree, and you do not need it to render.
See also¶
This release moves render onto the engine but does not change when a
mismatched $quill is rejected — that enforcement landed in
0.88 → 0.89. Read it too if you are jumping straight from
0.88.