Embed Wit in an app
Embedding Wit is the five-stage pipeline plus your own DataSource —
no CLI, no config file, no subprocess. This page is the copy-paste starting
point: wrap the stages in one function, catch the two error families, and choose
fragment or document output.
The worked helper
Verified end to end. Drop it into a server, a build step, or a static-site generator:
import { parse, WitError } from '@witlang/parser';
import {
loadExternalData, resolve, expand, scanCloseless, RuntimeError,
} from '@witlang/runtime';
import { renderHtml } from '@witlang/render-html';
export function compileWit(
src: string,
file: string, // absolute path: `reference` resolves against it
data: Record<string, unknown> = {},
): string {
try {
const closeless = scanCloseless(file, src); // names needing no close
const prepare = (d) => loadExternalData(d, data); // dict DataSource
const parsed = parse(src, file, { closeless });
const resolved = resolve(prepare(parsed), { rootPath: file, closeless, prepare });
const expanded = expand(resolved);
return renderHtml(expanded, { mode: 'fragment' });
} catch (err) {
if (err instanceof WitError || err instanceof RuntimeError) {
const { line, col } = err.loc;
throw new Error(`${file}:${line}:${col}: ${err.code}: ${err.message}`);
}
throw err;
}
}WitError covers parse; RuntimeError (and its subclasses ResolverError and ExpanderError) covers resolve and
expand. Both carry .code and .loc, so one branch formats
them all.
The embedded data path
The data argument is a DataSource: a plain dict for
static data, or a function for dynamic. No config file, no subprocess:
compileWit(src, 'report.wit', {
meta: { version: '0.4.0' },
sales: [ { site: 'Dunmore', lit: 'yes' } ],
});The document names each alias it needs with a @load line; a
key in the dict with no matching @load is never read:
@load meta load@ @load sales load@ Version @meta.version. @ul (each @sales as s) (li @s.site — @s.lit li) (end) ul@
From there @meta.version and (each @sales as s) read the data exactly as if it had been written inline.
Cross-file documents
If your documents use reference, pass rootPath and a
synchronous fileReader so the referenced files resolve against your
real or virtual filesystem. Give the same reader to scanCloseless,
so a chapter can use a closeless definition from a shared file:
const reader = (abs) => readFileFromCacheSync(abs); // must be synchronous
const closeless = scanCloseless('/content/main.wit', src, reader);
resolve(parse(src, '/content/main.wit', { closeless }), {
rootPath: '/content/main.wit',
fileReader: reader,
closeless, // used when parsing each referenced file
prepare, // optional: per-file hook, e.g. loadExternalData
});prepare runs on each referenced file right after it is parsed; the
CLI uses it so a chapter's own @load lines load too.
Output choice
renderHtml(expanded, { mode: 'fragment' })— an<article>to inject into a page you already own.renderHtml(expanded, { mode: 'document' })— a standalone<!doctype html>file.renderMarkdown(expanded)— for a text or Markdown pipeline.
Common mistakes
- Forgetting
rootPath— a document with areferencethrowsE_MISSING_REFERENCE_FILEwithout it. - Skipping
scanCloselessfor multi-file documents — a chapter's@sigdefined in a shared file then raisesE_UNCLOSED_NODEwhere it opens a line. - Using an
asyncfileReader— it must be synchronous. - Catching only one error family — parse throws
WitError, the runtime throwsRuntimeError; catch both. - Rendering per request without caching.
See also
External data · Resolve & expand · Rendering · CLI reference (the host wiring to contrast).