Getting started
Development uses Bun 1.4+. The library itself has no Bun or Node dependency: it runs in Node 20+, Bun and browsers.
bun installbun run typecheckbun run format # Biome: format and apply safe lint fixesbun run lint # Biome lint onlybun testbun run sweep:fixtures # known-answer check of every fixturebun scripts/sweep.ts /path/to/inbound/files # what you actually receivebun scripts/sweep.ts /path/to/files --expect answers.json(cd site && bun install --frozen-lockfile) # once: the site's own dependencies (Astro, Starlight, TypeDoc)bun run build:site && bun run serve:site # the site on http://localhost:4173/eu-einvoice-canonical/bun run check:lib # all library CI gates, including coverage and generated filesbun run check:site # build, site typecheck, benchmark and bundle-size gates(cd site && bunx playwright install --with-deps --only-shell chromium) # once: accessibility browserbun run check:a11y # accessibility/privacy gates against the built dist/CI runs the library and site checks in parallel and requires both plus the Docker smoke test before deployment.
The optional scripts/fetch-corpus-report.sh downloads the latest corpus report before building the site; local
checks work without it. Accessibility checks print per-scenario timings; A11Y_JOBS=1 bun run check:a11y runs
them serially for profiling (the default is two isolated browser contexts at a time).
import { convert } from 'eu-einvoice-canonical';
const { fileSha256, outcomes, skipped } = await convert(bytes, 'invoice.xml');for (const o of outcomes) { if (o.status === 'ok') console.log(o.invoices); // CanonicalInvoice[] else console.log(o.status, o.detection); // 'unsupported' | 'not-an-invoice' | 'error'}console.log(skipped); // DOCTYPE, size limits, detached signatures...
// Opt-in field provenance: source.provenance maps canonical paths to the XPath they were read from.const traced = await convert(bytes, 'invoice.xml', undefined, { provenance: true });// '/totals/payable' -> '/Invoice/cac:LegalMonetaryTotal/cbc:PayableAmount', '/lines/1/netAmount' -> '/Invoice/cac:InvoiceLine[2]/cbc:LineExtensionAmount'Field provenance in a parser
Section titled “Field provenance in a parser”Canonical output can also carry source.valueOrigins, a partial map of canonical field paths to source, derived or defaulted, without enabling XPath provenance. Missing entries mean unrecorded origins. source.warnings[].field can identify the affected canonical value. See Canonical evidence metadata for coverage and how consumers distinguish an explicit zero from a substituted one.
XPaths use the qualified names as written in the document, with a positional predicate only where siblings share a name; attributes end in /@name. Values built from several nodes list them joined with |. Values the mapper computed, defaulted or translated (code mappings, sums) have no entry and are listed in source.derived or source.warnings instead.
Parsers get this from Ctx in src/parsers/context.ts: construct it with new Ctx(ns, input.provenance), take a scope for a canonical path (const s = c.in(''), s.in('seller'), s.in('lines').in(i)), and read through the scope with the field key as the last argument: s.text(node, 'cbc:ID', 'id'), s.dec(m, 'cbc:PayableAmount', 'payable'), s.id(el, 'cbc:EndpointID', 'endpoint') (records /value and /schemeId). Use c.mark(path, el) / c.markAll(path, els) for values assembled by hand, and c.nonEmpty(els) before mapping to arrays so indexes match the output. Entries for values that did not survive into the output are pruned by finish().