@vantageos/charter-css-exporter
v0.1.0
Published
Turns a DTCG charter (charter-dtcg v1 contract) into CSS custom properties in @vantageos/mosaic-tokens shape. Consumes the mosaic naming convention as a pinned contract; never writes to the mosaic repo.
Readme
@vantageos/charter-css-exporter
Turns a DTCG charter into CSS custom properties in @vantageos/mosaic-tokens shape.
Owned by Rho. It consumes the mosaic naming convention as a pinned contract and never writes into the mosaic repository — the mosaic package keeps a single writer.
npm install @vantageos/charter-css-exporterimport { exportCss } from "@vantageos/charter-css-exporter";
const { css, cssHost, mapped, extensions } = await exportCss(charter);A host application installs this package from the registry. It does not clone
vantage-taste-engine — that was true until Day 131, when the contract was
declared here by a relative path (file:../../contracts/charter-dtcg) that
resolves inside a checkout and nowhere else. npm install from outside then
exited 0, wrote a dangling symlink, and the break surfaced later as
ERR_MODULE_NOT_FOUND inside the consumer's process. The contract is now a
published package consumed by version range, and test/publishable.test.mjs
fails the build if either that or a load-time dependency slides back out of
dependencies.
Inside this repository the two packages resolve each other through the npm workspace declared at the root — by name, never by path, so what runs here and what ships are the same manifest.
Node version, and a mismatch this package does not resolve
[email protected] declares engines.node >= 22, and this package now
declares the same. It did not at first: the constraint was inherited through a
dependency and left unsaid, so a consumer on node 20 would have met it as an
EBADENGINE line naming a package they never chose. An inherited constraint
that is not declared is a surprise in somebody else's container.
engines warns, it does not block. Every test and consumer run recorded here
was produced on v20.20.1, all of them emitting that warning, all of them
working. Node 22 or later is the version this package is declared to work on;
node 20 is the version it happens to work on today, and only the first of
those two is a support statement.
The seam is consumed, not assumed
exportCss validates its input against @vantageos/charter-dtcg-contract (v1) before doing anything, and throws if the charter does not conform. A charter that never passed the contract cannot become CSS through this package.
Where the surface comes from
The list of --mosaic-* names this exporter may emit is read from the naming contract the package publishes (@vantageos/mosaic-tokens/naming-contract), and surfaceDigest is pinned here.
It used to be derived locally from the package's tokens.css. That read reality correctly, but it produced a value only this package could speak: on a disagreement there was nothing to hold against the other side except a second instrument. The contract ships one fact, so a future divergence is a divergence rather than two measurements of the same thing taken two ways.
surfaceDigest is the guarded value — it is the identity of the surface, and a mismatch throws at import. contractVersion states compatibility intent and is deliberately not guarded: a hand-written version number does not move when a token is renamed, which is exactly why it cannot be the thing that guards.
The package version and the contract version are different facts, and every message that prints one names which one it is — the emitted CSS header, the digest-mismatch throw, and the refusal to emit an unknown name. A header printing mosaic-tokens 1.0.0 would read as a package version and be wrong — quietly, since nothing would fail and a reader would later chase a version never installed.
One check still reads tokens.css: an assertion that the published contract still describes the package that ships it. That is a cross-check on the vendor artefact, not a second source of truth — without it, a contract that drifted from its own stylesheet would be believed on its own word.
Two failure modes it refuses to have
A dead variable. If a mapping ever points at a name mosaic does not declare, exportCss throws instead of emitting it. A var(--mosaic-nope) at the use site resolves to nothing, the whole declaration is dropped, and the page looks slightly wrong with no error anywhere.
A token that vanishes. mosaic-tokens ships a sans and a mono face; it has no display face. A charter has one. Rather than drop it, unmapped charter tokens are emitted under the declared --charter-* namespace and returned in extensions. Nothing disappears silently — the caller can see exactly what the consumer surface had no home for.
At the time of writing, the tokens with no mosaic home are font.display, font.classification, typography.ratio, and shadow.raised.
:root and :host
css targets :root; cssHost carries the identical declarations under :host, for consumers that inject into a Shadow DOM root rather than the document.
Shadow DOM
test/shadow-dom.test.mjs proves the crossing in real Chromium via Playwright: it declares the charter on :root, attaches a shadow root, and reads the computed values inside it — with a positive control asserting that an undeclared variable does not resolve, so the passing assertions cannot pass for the wrong reason.
This matters because it is the property the layered architecture rests on: the GPTPowerups extension is Preact + Lit inside a Shadow DOM, so no React component can be shared with it — but custom properties cross. jsdom does not implement that part of the cascade, which is why this test needs a browser or it skips loudly.
Running
cd contracts/charter-dtcg && npm install
cd ../../packages/charter-css-exporter && npm install && npm testOrchestrator: Rho — VantageOS Team | 2026-07-24
