Skip to content

fix(tokens): export round trip: DESIGN.md spacing steps, typography aliases, no DTCG duplicates - #254

Merged
dgesteves merged 2 commits into
mainfrom
fix/os2-52-token-export-round-trip
Oct 29, 2026
Merged

dgesteves merged 2 commits into
mainfrom
fix/os2-52-token-export-round-trip

Conversation

@dgesteves

Copy link
Copy Markdown
Owner

Roadmap item: OS2-52 (token export round trip).

What was wrong

The export-then-import pair that docs/tokens.md advertises gave wrong results in three ways. Each was reproduced on main with a stock shadcn/ui theme (test/fixtures/shadcn-neutral/globals.css):

  1. DESIGN.md spacing. export --format design-md wrote Tailwind's spacing unit as spacing: { base: 0.25rem }. DESIGN.md has no multiplier: its spec reads every spacing entry as a step of the scale, so a reader (onsystem included) took 4px as the only step. With that DESIGN.md as the only token source, check said padding: "16px" → "Use p-[4px] (4px)", gap-[8px] → nearest gap-[4px], and p-[13px] → nearest p-[4px].
  2. DESIGN.md typography alias. --font-heading: var(--font-sans) was written as font-heading: "{typography.font-sans}". A front matter reference must name a single value, not a whole typography level. onsystem's own reader warned "DESIGN.md: unresolved reference {typography.font-sans}". @google/design.md@0.4.0 lint reported 22 warnings, one per character of the string ("'0' is not a recognized typography property", and so on).
  3. DTCG duplicates. Zero config reads **/*.tokens.json, so export --format dtcg --out design.tokens.json (the command in the docs) is read back next to the stylesheet. Its @theme inline tokens have no custom property, so mergeTokens could not match them, and inspect went from 42 tokens to 50 (radius 8 → 14, typography 2 → 4). A later DESIGN.md export then listed the radii twice and warned six times "radius-… is left out of the front matter: another token has its key".

What changed

  • src/tokens/export.ts: the DESIGN.md export writes Tailwind's spacing unit as the steps of Tailwind v3's default scale, which v4 still makes: "0.5": 0.125rem … "96": 24rem, computed from the project's unit. A step the project sets itself (--spacing-4) keeps its own value. The section is a Map, so "0.5" stays before "1"; a plain object puts integer-like keys first. A typography alias is written as the level it names (fontFamily: …), and other aliases stay references as before. The Layout table is unchanged, and still explains the unit and p-4.
  • src/tokens/index.ts (mergeTokens): a token with no custom property and a Tailwind key now merges with the token of the same name and key, as tokens with a custom property already merged. The key leaves out the category on purpose: an exported -0.025em letter spacing has no DTCG type and reads back as category other. The name keeps leading-tight and tracking-tight apart, although they share the key tight.
  • docs/tokens.md: says how @theme inline tokens merge, that an export written into the project is read back without adding tokens, how the spacing unit and typography aliases are written to DESIGN.md, and what check does with that file. It also says the test suite runs the DESIGN.md round trip.
  • Changeset (patch).

Why export the steps instead of reading base as a multiplier

The finding offered both. A reader-side rule ("a lone base is a multiplier") would disagree with the DESIGN.md spec, which uses base: 16px as an ordinary step next to xs … xl. It would also fix only onsystem: Google's linter and its Tailwind export would still see one 4px step. Writing the steps makes the file correct for any reader. A DESIGN.md token names no class, so a length on the scale is now left alone, and one off it gets the nearest step as an arbitrary value (p-[13px] → p-[12px]), as docs/tokens.md already describes. A DESIGN.md written by an earlier version is read as before, with 4px as its only step; exporting again rewrites it, and the changeset says so.

Outside the listed files

The plan for this item listed docs/tokens.md and src/tokens/{design-md,dtcg,export}.ts. The DTCG duplicates cannot be fixed in those files: the reader sees one file at a time, and the merge happens in mergeTokens in src/tokens/index.ts. The change there is the merge key, about six lines. No open pull request and no later item on the roadmap touches that file. design-md.ts and dtcg.ts needed no change.

How it was tested

New tests, each of which fails on main:

  • test/export.test.ts, "export, then read back":
    • Exports a stock shadcn/ui app to DESIGN.md, then checks the same page (gap-[8px] p-[13px], style={{ padding: "16px" }}) in that app and in a project that has only the DESIGN.md. From the CSS, the fixes are gap-2, p-3 and p-4. From the DESIGN.md, the only finding is p-[13px] → nearest p-[12px]. On main, all three were 4px.
    • A typography alias (--font-heading: var(--font-sans)) is written as a fontFamily level, and reads back with no warning.
    • In a stock shadcn/ui app: DESIGN.md export, then DTCG export into the project, then DESIGN.md export again. Both DESIGN.md exports write 42 tokens, print nothing on stderr and are byte-identical. check findings are unchanged. On main the second export wrote 50 tokens, with warnings.
    • The existing design-md export test now expects the spacing steps instead of base: 0.25rem.
  • test/tokens.test.ts: @theme inline tokens (--radius-sm, --leading-tight, --tracking-tight) merge with the DTCG tokens toDtcg writes for them. A token with the same key but another name (leading-tight against tracking-tight) stays separate.

By hand, with the built CLI on a copy of the stock app: the DESIGN.md-only check now reports only p-[13px] (nearest p-[12px]) and rounded-[7px] (nearest rounded-[6px], documented behaviour for DESIGN.md radii). inspect shows 42 tokens before and after the DTCG export, and the DESIGN.md export after it prints no warnings. @google/design.md@0.4.0 lint on the export with a real font: 22 warnings on main, 0 on this branch.

Gate: pnpm install --frozen-lockfile --prefer-offline, pnpm format:check, pnpm lint, pnpm typecheck, pnpm test (105 files, 1359 passed, 3 skipped), pnpm build and pnpm smoke all pass. mergeTokens runs for every project, so I also ran the real-world corpus (node scripts/corpus.mjs, after the build). It printed "Counts match corpus/snapshot.json", and all 80 labels of the blind sample are still reported; the snapshot is unchanged.

…liases, no DTCG duplicates

The DESIGN.md export wrote Tailwind's spacing unit as `base: 0.25rem`,
which a DESIGN.md reader takes as the only step: read back, `padding:
"16px"` got "Use `p-[4px]`". It now writes the steps of the scale
(`"0.5": 0.125rem` to `"96": 24rem`) from the project's unit.

A typography alias was written as `"{typography.font-sans}"`, a reference
to a whole level, which onsystem's reader could not resolve and Google's
linter read as 22 unknown properties. It is now written as the level.

A DTCG file exported into the project is read back by zero config next
to the stylesheet, and its `@theme inline` tokens, which have no custom
property, were added a second time (42 tokens became 50). mergeTokens
now merges a token without a custom property by its name and Tailwind
key.
…in scale order

A DTCG file listed before the stylesheet read -0.025em as other, and the merged token kept that, so tracking-tight left the DESIGN.md typography. Steps the project sets itself are now written in scale order with the generated ones. docs/tokens.md no longer says the read-back gives every spacing step: v4's other steps are off the scale for a project that reads only the DESIGN.md.
@dgesteves
dgesteves merged commit 6a9657c into main Oct 29, 2026
19 checks passed
@dgesteves
dgesteves deleted the fix/os2-52-token-export-round-trip branch October 29, 2026 09:47
@dgesteves-release dgesteves-release Bot mentioned this pull request Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant