Skip to content

feat: emit native token outputs for iOS (Swift) and Android (Kotlin) - #53

Open
thomasdigby wants to merge 6 commits into
mainfrom
feat/native-token-outputs
Open

feat: emit native token outputs for iOS (Swift) and Android (Kotlin)#53
thomasdigby wants to merge 6 commits into
mainfrom
feat/native-token-outputs

Conversation

@thomasdigby

@thomasdigby thomasdigby commented Jul 30, 2026

Copy link
Copy Markdown
Member

Closes #52

What

The build now emits tokens in native-consumable form alongside the existing outputs in lib/, per theme:

  • lib/theme-*.swift — a public enum ThemeTokens of SwiftUI Color(red:green:blue:opacity:) and CGFloat constants
  • lib/theme-*.kt — a Compose object ThemeTokens (package uk.co.atomlearning.theme) of Color(0xAARRGGBB), .sp / .dp constants

How

Rather than hand-rolling the conversions, this uses style-dictionary's built-in formats and transforms (ios-swift/enum.swift, compose/object; color/ColorSwiftUI, color/composeColor, size/swift/remToCGFloat, size/compose/remToSp|Dp). To enable that, the token sources are migrated to the DTCG format ($value, plus group-level $type), which is what the built-in transforms key off.

Custom code is reduced to src/native.ts:

  • the name/native/camel transform — the flat camelCase naming contract (color.blue.800blue800, color.subject.gcse-mathssubjectGcseMaths, size.font.smfontSm); these names are the native apps' API, renames are breaking
  • the file filter — token coverage (colours, size.font|leading|radii|space; font stacks/breakpoints/effects excluded per the issue) and per-theme scoping

size.leading deliberately has no transform — the unitless multipliers pass through as-is.

Web outputs unchanged — verified byte-identical

The previously-listed color/hsl and size/rem transforms were silent no-ops (they filter on $type, which no token declared); they're removed from the web platforms so the DTCG migration cannot alter web output. All 15 web artifacts (*.css, *.js, *.d.ts, themes.css, media.*) were diffed byte-for-byte against a pre-migration build: identical.

Test coverage (135 tests, was 37)

This PR also closes long-standing gaps in output coverage:

  • CI (.github/workflows/ci.yml) — the repo had none. Runs build + tests + tsc on every PR, with a macOS job that installs swiftc/kotlinc
  • pretest hooksyarn test/test:run now build first, so the suite can't pass against stale lib/ (we hit exactly that during development)
  • test/completeness.test.ts — reconciles source tokens against every output. Previously nothing asserted that all tokens reach the outputs; now a filter regression that silently drops e.g. color.gl.* fails the build
  • test/values.test.ts — exact shadow / breakpoint / font-stack values (shadows were only checked as a wildcard reset before) and cross-output consistency: Swift colours are re-derived from the source hsl() and checked against both the JS theme and the Kotlin ARGB output
  • test/native-compile.test.ts — compiles every generated file with swiftc and kotlinc (against minimal Compose stubs). Verified locally with a real kotlinc; skips cleanly when the toolchain is absent
  • test/assets.test.tslib/assets/ had zero coverage despite backing ~20 export paths. Now every export target, typesVersions path and copied asset is checked for existence, non-emptiness and file signature

All new suites were mutation-tested (dropping one Swift constant and emptying one asset produced 7 failures), so they're not passing vacuously.

Pre-existing bug found, not fixed

The new completeness test surfaced a real defect that predates this PR: the CSS formatters build names from property.name, emitting --color-coolGrey-100, while the JS/.d.ts properties map kebab-cases it to --color-cool-grey-100. So var(--color-cool-grey-100) resolves to nothing in CSS. It's encoded as an it.fails expected failure with a comment rather than hidden — fixing it changes published CSS variable names, so it's a call for you to make (either rename the CSS vars to kebab or stop kebab-casing in the JS map).

Also note the build's "Token collisions detected" warnings pre-date this PR (intentional theme overrides of base tokens).

Token coverage

Group Emit as Conversion
color.* Color hsl()/hex → sRGB at build time; alpha tokens carry their opacity
size.font.* CGFloat / TextUnit rem × 16 (sm 0.875 → CGFloat(14.00) / 14.00.sp)
size.radii.* / size.space.* CGFloat / Dp rem × 16
size.leading.* number unitless multipliers, as-is

No packaging changes: files: ["lib"] already ships the new files; the native repos vendor them for a pinned version (e.g. via unpkg).

🤖 Generated with Claude Code

Thomas Digby and others added 6 commits July 30, 2026 12:05
Adds Swift and Kotlin formatters that emit colours, type scale,
spacing, radii and line heights as ThemeTokens constants alongside
the existing CSS/JS/Tailwind outputs, per theme.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… outputs

Migrates token sources to the DTCG format ($value + group-level $type)
so style-dictionary's built-in transforms can drive the conversions:

- Swift/Kotlin files are now emitted by the built-in ios-swift/enum.swift
  and compose/object formats, with colour and rem->pt conversion handled
  by color/ColorSwiftUI, color/composeColor, size/swift/remToCGFloat and
  size/compose/remToSp|Dp — the hand-rolled formatters are deleted
- Custom code shrinks to src/native.ts: the flat camelCase naming
  contract and the native token filter (coverage + theme scoping)
- Web formatters read $value; the dormant color/hsl and size/rem
  transforms are removed from the web platforms so their outputs stay
  byte-identical (verified against a pre-migration snapshot)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… coverage

- CI workflow runs build + tests + tsc on every PR; a macOS job installs
  swiftc/kotlinc so the native compile checks actually execute
- pretest hooks build before testing, so the suite can no longer pass
  against stale lib/ output
- completeness tests reconcile the token sources against every output,
  catching filter/naming regressions that silently drop tokens
- value fidelity tests assert exact shadow, breakpoint and font-stack
  values and check colours agree across the JS, Swift and Kotlin outputs
  by re-deriving sRGB from the source hsl
- native-compile tests compile the generated files with swiftc and
  kotlinc (against minimal Compose stubs), skipping when absent
- assets tests cover every export target, typesVersions path and copied
  asset — previously untested

Documents one pre-existing bug as an expected failure: the CSS formatters
emit --color-coolGrey-100 while the JS properties map declares
--color-cool-grey-100, so that var() resolves to nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
style-dictionary 5.1.1 requires node >=22, and the build relies on
native TypeScript support, so Node 20 failed at install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GitHub's ubuntu-latest image ships both swiftc and kotlinc, so these
tests run there rather than skipping, and invoking a real compiler
exceeds vitest's 5s default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the bespoke hsl->rgb maths and the separate hex branch with
color2k's parseToRgba, which handles hex, hsl and hsla uniformly. Also
routes the Compose AARRGGBB comparison through the same parser.

color2k is deliberately not tinycolor2, which style-dictionary converts
with, so the assertions stay an independent check rather than a
restatement of the build's own maths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

Emit native token outputs for iOS (Swift) and Android (Kotlin): colours, type scale, spacing, radii

1 participant