Skip to content
GitHub

Getting started

Development uses Bun 1.4+. The library itself has no Bun or Node dependency: it runs in Node 20+, Bun and browsers.

Terminal window
bun install
bun run typecheck
bun run format # Biome: format and apply safe lint fixes
bun run lint # Biome lint only
bun test
bun run sweep:fixtures # known-answer check of every fixture
bun scripts/sweep.ts /path/to/inbound/files # what you actually receive
bun 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 files
bun run check:site # build, site typecheck, benchmark and bundle-size gates
(cd site && bunx playwright install --with-deps --only-shell chromium) # once: accessibility browser
bun 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'

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().