Skip to content

Commit 3b8f06f

Browse files
authored
Merge pull request #110 from PMDevSolutions/82-design-system-export-command-export-design-system
feat(design-system): token re-import + round-trip guarantee and documented consumers
2 parents 2e1be4a + 167278f commit 3b8f06f

9 files changed

Lines changed: 777 additions & 0 deletions

File tree

.claude/commands/export-design-system.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ If the script exited non-zero, surface the exit code and the stderr message verb
8686
- **Output directory exists (exit 1):** Ask the user whether to `--force` overwrite or pick a new path.
8787
- **Unsupported framework (exit 3):** Only `react`, `vue`, `svelte`, `react-native` are supported.
8888

89+
## Round-trip / re-import
90+
91+
The export is reversible. To reconstruct a `design-tokens.lock.json` from an
92+
exported workspace — or to verify the round-trip in CI — use the inverse script:
93+
94+
```bash
95+
./scripts/import-design-tokens.sh --from dist/design-system --out design-tokens.lock.json
96+
./scripts/import-design-tokens.sh --from dist/design-system --verify src/styles/design-tokens.lock.json
97+
```
98+
99+
Only `tokens.json`/`tokens.ts` round-trip losslessly (`tokens.css` and the
100+
Tailwind preset are derived views). See `docs/design-system-export/consumers.md`
101+
for the interchange contract and how Flavian (WordPress `theme.json`) and Nerva
102+
(typed tokens) consume the export.
103+
89104
## Out of scope
90105

91106
This command does NOT:

.claude/skills/export-design-system/SKILL.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,33 @@ Common flag combinations:
167167
- **The script does not run `pnpm install` or `pnpm build`.** It only writes files. The user runs install/build to verify the workspace.
168168
- **Component file filtering is conservative.** `*.test.*`, `*.spec.*`, `*.stories.*`, `*.d.ts` are excluded. Co-located CSS modules and helper files with matching framework extensions ARE included.
169169

170+
## Re-importing & Round-Trip
171+
172+
The exported `tokens.json` is a **lossless** snapshot of the lockfile, so the
173+
export is fully reversible. `scripts/import-design-tokens.js` (the inverse of this
174+
skill) reconstructs a `design-tokens.lock.json` from an exported package:
175+
176+
```bash
177+
# Reconstruct a lockfile from an exported workspace
178+
./scripts/import-design-tokens.sh --from dist/design-system --out design-tokens.lock.json
179+
180+
# Verify the round-trip (exit 0 if identical to the source, 1 if it drifted)
181+
./scripts/import-design-tokens.sh --from dist/design-system --verify src/styles/design-tokens.lock.json
182+
```
183+
184+
`--from` also accepts an installed package or a raw `tokens.json` path. Only
185+
`tokens.json`/`tokens.ts` round-trip; `tokens.css` and `tailwind-preset.ts` are
186+
derived, one-way views. See **`docs/design-system-export/consumers.md`** for the
187+
interchange contract and how the named consumers ingest the export — **Flavian**
188+
(WordPress/FSE `theme.json`) and **Nerva** (typed tokens in a Hono/Workers
189+
backend). The guarantee is proved by `scripts/__tests__/design-system-roundtrip.test.js`.
190+
170191
## Reference
171192

172193
- Script: `scripts/export-design-system.js`
173194
- Wrapper: `scripts/export-design-system.sh`
174195
- Slash command: `/export-design-system`
196+
- Re-importer: `scripts/import-design-tokens.js` / `scripts/import-design-tokens.sh`
197+
- Round-trip test: `scripts/__tests__/design-system-roundtrip.test.js`
198+
- Consumer contract: `docs/design-system-export/consumers.md`
175199
- Inputs: `design-tokens.lock.json` (from `design-token-lock` skill), `build-spec.json` (from `figma-intake`/`canva-intake`/`screenshot-intake` skills)

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ node scripts/validate-pipeline-config.js --config <path> --schema <path>
127127
# Export generated components as a publishable design-system workspace
128128
./scripts/export-design-system.sh [--scope @org] [--output dir] [--framework <react|vue|svelte|react-native>] [--dry-run] [--json]
129129

130+
# Re-import: reconstruct design-tokens.lock.json from an export (inverse of export; round-trip / consumer)
131+
./scripts/import-design-tokens.sh [--from dir|tokens.json] [--out path|-] [--verify lockfile] [--force] [--json]
132+
130133
# Renderer registry: list / resolve / detect framework renderers (see docs/multi-framework/renderers.md)
131134
node scripts/renderer-registry.js (list | resolve <name> | detect [dir]) [--json]
132135
node scripts/validate-renderer.js --dir <renderer-dir> # or --all [--renderers-root <dir>]
@@ -584,6 +587,7 @@ gh issue create # Create issue
584587
./scripts/capture-baselines.sh # Capture regression baselines
585588
./scripts/regression-test.sh # Visual regression testing
586589
./scripts/export-design-system.sh # Export components + tokens as pnpm workspace
590+
./scripts/import-design-tokens.sh # Reconstruct lockfile from an export (round-trip / consumer)
587591
./scripts/validate-pipeline-config.sh # Validate pipeline.config.json against schema
588592
./scripts/check-doc-counts.sh # Flag drift between documented agent/skill counts and disk
589593
./scripts/verify-all.sh # Run all quality checks with summary

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ Details: `.claude/PLUGINS-REFERENCE.md`
345345
| Canva pipeline guide | `docs/canva-to-react/README.md` | Canva pipeline overview and troubleshooting |
346346
| Conversation pipeline guide | `docs/conversation-to-app/README.md` | Conversational app creation via generated Figma designs |
347347
| InDesign pipeline guide | `docs/pipelines/indesign.md` | Convert exported IDML/PDF into React components, tokens, and Storybook stories |
348+
| Design system export | `docs/design-system-export/consumers.md` | Export format, round-trip re-import, and how Flavian/Nerva consume the tokens |
348349
| Agent catalog | `.claude/CUSTOM-AGENTS-GUIDE.md` | All 56 agents with use cases |
349350
| Skills catalog | `.claude/skills/README.md` | All 24 skills with triggers |
350351
| Plugin reference | `.claude/PLUGINS-REFERENCE.md` | Plugin configuration and commands |
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Design System Export — Consumers & Round-Trip Contract
2+
3+
`/export-design-system` turns a project's `design-tokens.lock.json` + components
4+
into a publishable pnpm workspace (see the `export-design-system` skill for the
5+
full output layout). This document defines **what a downstream project imports**
6+
and the **round-trip guarantee** that makes the export a reliable interchange.
7+
8+
## The interchange contract
9+
10+
The exported `@scope/design-tokens` package ships four token artifacts. Two are
11+
lossless reconstructions of the source lockfile; two are derived, ergonomic
12+
_views_:
13+
14+
| Artifact | Contents | Lossless? | Use it for |
15+
|----------|----------|-----------|------------|
16+
| `tokens.json` | verbatim JSON snapshot of `design-tokens.lock.json` | **Yes** | machine interchange, re-import, custom tooling |
17+
| `tokens.ts` | `export const tokens = <lockfile> as const` | **Yes** | typed, tree-shakeable access in TS/JS consumers |
18+
| `tokens.css` | flattened `--color-* / --space-* / --radius-*` custom properties | No | drop-in theming for web UIs |
19+
| `tailwind-preset.ts` | `{ colors, spacing, borderRadius, fontFamily }` subset | No | extend a consumer's Tailwind config |
20+
21+
**`tokens.json` is the canonical interchange.** It is a byte-for-byte snapshot of
22+
the lockfile, so any consumer that reads it sees exactly the tokens the design
23+
system was locked to. `tokens.css` and `tailwind-preset.ts` are one-way
24+
projections — they intentionally drop structure (nested color scales, typography
25+
families, `textContent`, `rgb`/`tailwind` metadata) and therefore **cannot** be
26+
reversed back into a lockfile.
27+
28+
## Round-trip guarantee
29+
30+
`import-design-tokens` is the inverse of the exporter and the reference consumer.
31+
It reads the lossless `tokens.json` and reconstructs an identical
32+
`design-tokens.lock.json`:
33+
34+
```
35+
lock ──/export-design-system──▶ @scope/design-tokens ──import-design-tokens──▶ lock′
36+
lock′ ≡ lock
37+
```
38+
39+
```bash
40+
# Reconstruct a lockfile from an exported workspace
41+
./scripts/import-design-tokens.sh --from dist/design-system --out design-tokens.lock.json
42+
43+
# From a published/installed package, or a raw tokens.json path
44+
./scripts/import-design-tokens.sh --from node_modules/@acme/design-tokens/dist/tokens.json --out -
45+
46+
# Verify a round-trip in CI (exit 0 when identical, 1 when it drifts)
47+
./scripts/import-design-tokens.sh --from dist/design-system --verify src/styles/design-tokens.lock.json
48+
```
49+
50+
The `--verify` mode is the executable form of the guarantee: it fails loudly if an
51+
export ever stops being a faithful representation of its source tokens. The
52+
automated proof lives in `scripts/__tests__/design-system-roundtrip.test.js`
53+
(tokens → export → reimport → deep-equal).
54+
55+
`--from` accepts an export root, a `design-tokens` package directory, or a
56+
`tokens.json` file directly. It probes, in order:
57+
`packages/design-tokens/src/tokens.json``.../dist/tokens.json`
58+
`src/tokens.json``dist/tokens.json``tokens.json`.
59+
60+
## Consumers
61+
62+
### Aurelius (self-consumer)
63+
64+
Aurelius re-imports its own export to move a locked design system between
65+
projects, or to validate that a hand-edited token package still round-trips. This
66+
is the `import-design-tokens` path above.
67+
68+
### Flavian — WordPress / FSE `theme.json`
69+
70+
[Flavian](https://github.com/PMDevSolutions/Flavian) is a Claude Code-integrated
71+
WordPress framework. A block theme's design surface is `theme.json`, so the
72+
consumer installs the tokens package and maps `tokens.json` into
73+
`theme.json.settings`:
74+
75+
| Lockfile section | `theme.json` target |
76+
|------------------|---------------------|
77+
| `colors.semantic.<k>.hex` (fallback `colors.primitives`) | `settings.color.palette[]``{ slug, color, name }` |
78+
| `typography.families.<k>` (`value` + `fallback`) | `settings.typography.fontFamilies[]``{ slug, fontFamily, name }` |
79+
| `spacing.scale.<k>.px` | `settings.spacing.spacingSizes[]``{ slug, size: "<px>px", name }` |
80+
| `borderRadius.<k>.px` | `settings.custom.radius.<k>` → emits `--wp--custom--radius--<k>` |
81+
82+
Example adapter (run in the theme's build step):
83+
84+
```js
85+
// scripts/tokens-to-theme-json.mjs
86+
import { readFileSync, writeFileSync } from "node:fs";
87+
// From the installed package: @acme/design-tokens/tokens.json
88+
import tokens from "@acme/design-tokens/tokens.json" with { type: "json" };
89+
90+
const palette = Object.entries(tokens.colors?.semantic ?? {}).map(([slug, v]) => ({
91+
slug,
92+
color: v.hex,
93+
name: slug.replace(/(^|-)\w/g, (m) => m.toUpperCase().replace("-", " ")),
94+
}));
95+
96+
const fontFamilies = Object.entries(tokens.typography?.families ?? {}).map(([slug, v]) => ({
97+
slug,
98+
fontFamily: [v.value, v.fallback].filter(Boolean).join(", "),
99+
name: slug,
100+
}));
101+
102+
const spacingSizes = Object.entries(tokens.spacing?.scale ?? {}).map(([slug, v]) => ({
103+
slug,
104+
size: `${v.px}px`,
105+
name: slug,
106+
}));
107+
108+
const radius = Object.fromEntries(
109+
Object.entries(tokens.borderRadius ?? {}).map(([k, v]) => [k, `${v.px}px`]),
110+
);
111+
112+
const themeJson = {
113+
version: 3,
114+
settings: {
115+
color: { palette },
116+
typography: { fontFamilies },
117+
spacing: { spacingSizes },
118+
custom: { radius },
119+
},
120+
};
121+
122+
writeFileSync("theme.json", JSON.stringify(themeJson, null, 2) + "\n");
123+
```
124+
125+
Bumping the tokens package version (via Changesets) and re-running the adapter is
126+
all that is needed to propagate a token change into the theme.
127+
128+
### Nerva — Hono / Cloudflare Workers backend
129+
130+
[Nerva](https://github.com/PMDevSolutions/Nerva) is a Claude Code-integrated API
131+
framework (Hono, Cloudflare Workers, Drizzle ORM). Backends need tokens for
132+
server-rendered surfaces — transactional emails, PDFs, or a design-tokens API —
133+
where CSS is not available. The consumer imports the **typed** tokens, which are
134+
tree-shakeable and require no runtime CSS:
135+
136+
```ts
137+
import { Hono } from "hono";
138+
import { tokens, type Tokens } from "@acme/design-tokens"; // typed, from tokens.ts
139+
140+
const app = new Hono();
141+
142+
// Serve the canonical tokens to any client (mobile app, docs site, design tool)
143+
app.get("/design-tokens", (c) => c.json(tokens));
144+
145+
// Or use tokens directly when rendering an email template server-side
146+
function emailButton(label: string) {
147+
const primary = tokens.colors.semantic.primary.hex;
148+
const radius = tokens.borderRadius.md.px;
149+
return `<a style="background:${primary};border-radius:${radius}px;color:#fff">${label}</a>`;
150+
}
151+
152+
export default app;
153+
```
154+
155+
Because `@acme/design-tokens` is framework-agnostic and side-effect free (apart
156+
from the optional `tokens.css`), it bundles cleanly into a Workers runtime.
157+
158+
## Style Dictionary
159+
160+
`tokens.json` is a plain nested map, so a consumer that standardizes on
161+
[Style Dictionary](https://styledictionary.com/) can register it as a custom
162+
source and run its own transforms. Aurelius does **not** require a Style
163+
Dictionary build step — it ships ready-to-use CSS variables, a typed TS export,
164+
and a Tailwind preset directly — but nothing prevents layering Style Dictionary on
165+
top of the lossless `tokens.json`.
166+
167+
## Reference
168+
169+
- Exporter: `scripts/export-design-system.js` (skill: `export-design-system`, command: `/export-design-system`)
170+
- Re-importer: `scripts/import-design-tokens.js` / `scripts/import-design-tokens.sh`
171+
- Round-trip test: `scripts/__tests__/design-system-roundtrip.test.js`
172+
- Lockfile source of truth: `design-token-lock` skill → `design-tokens.lock.json`
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Design System Export — Consumer Contract & Round-Trip
2+
3+
**Issue:** #82 — Design system export command (`/export-design-system`)
4+
**Date:** 2026-07-01
5+
**Milestone:** v2.0.0
6+
7+
## Problem
8+
9+
`/export-design-system`, `scripts/export-design-system.{js,sh}`, and the
10+
`export-design-system` skill already ship on `main` (acceptance criteria #1 and
11+
#2). Two gaps remain from the v2.0.0 scope:
12+
13+
- **#3 — a defined consumer.** The export is produced but nothing documents how a
14+
downstream project ingests it. Sibling frameworks
15+
[Flavian](https://github.com/PMDevSolutions/Flavian) (WordPress/FSE) and
16+
[Nerva](https://github.com/PMDevSolutions/Nerva) (Hono + Cloudflare Workers) are
17+
the named consumers.
18+
- **#4 — a round-trip guarantee.** Nothing proves the export is a faithful,
19+
reversible representation of the source tokens.
20+
21+
## Key insight: which artifact round-trips
22+
23+
The exported `@scope/design-tokens` package ships four token artifacts:
24+
25+
| Artifact | Contents | Lossless? |
26+
|----------|----------|-----------|
27+
| `tokens.json` | `JSON.stringify(lockfile)` — verbatim snapshot | **Yes** |
28+
| `tokens.ts` | `export const tokens = <lockfile> as const` | **Yes** |
29+
| `tokens.css` | flattened `--color/--space/--radius` custom properties | No (drops `typography`, `textContent`, `rgb`/`tailwind` metadata, nesting) |
30+
| `tailwind-preset.ts` | `{colors, spacing, borderRadius, fontFamily}` subset | No |
31+
32+
The lockfile is a rich nested object (`colors.primitives`/`colors.semantic`,
33+
`typography.families`, `spacing.scale`, `borderRadius`, `textContent`, plus
34+
`version`/`figmaFileKey`/`figmaLastModified` metadata). Only `tokens.json` and
35+
`tokens.ts` preserve it fully. **`tokens.json` is therefore the canonical lossless
36+
interchange**; `tokens.css`/`tailwind-preset.ts` are derived, consumer-facing
37+
*views*, never the source of truth for a re-import.
38+
39+
## Solution
40+
41+
### 1. Reimporter — `scripts/import-design-tokens.js` (+ `.sh` wrapper)
42+
43+
The inverse of the exporter and the first concrete consumer. Reads an exported
44+
tokens package and reconstructs a `design-tokens.lock.json`.
45+
46+
**CLI**
47+
48+
```
49+
node scripts/import-design-tokens.js [options]
50+
--from <path> export root, tokens package dir, or a tokens.json file
51+
(default: dist/design-system — matches the exporter default)
52+
--out <path> where to write the reconstructed lockfile
53+
(default: design-tokens.lock.json; use "-" for stdout)
54+
--verify <path> compare the reconstructed lockfile against a reference
55+
lockfile; exit 0 if identical, 1 if not (no file written)
56+
--force overwrite --out if it exists
57+
--json machine-readable summary
58+
-h, --help
59+
```
60+
61+
**`--from` resolution order** (first match wins): a `*.json` file used directly;
62+
otherwise a directory is probed at `packages/design-tokens/src/tokens.json`
63+
`packages/design-tokens/dist/tokens.json``src/tokens.json`
64+
`dist/tokens.json``tokens.json`.
65+
66+
**Behavior:** parse `tokens.json`; reject input that is not a token lockfile
67+
(must contain at least one of `colors`/`typography`/`spacing`/`borderRadius`);
68+
then either `--verify` (deep-equal vs reference) or write the lockfile
69+
(refusing to clobber `--out` without `--force`).
70+
71+
**Exit codes** mirror the exporter: `0` ok, `1` output exists / verify mismatch,
72+
`2` no tokens.json found under `--from`, `3` input is not a token lockfile.
73+
74+
### 2. Round-trip test — `scripts/__tests__/design-system-roundtrip.test.js`
75+
76+
Vitest, driving the real scripts via `execFileSync`:
77+
78+
1. Write a rich fixture `design-tokens.lock.json` (every top-level section).
79+
2. Export: `export-design-system.js --lockfile <fixture> --framework react --output <tmp> --force`.
80+
3. Assert `<tmp>/packages/design-tokens/src/tokens.json` deep-equals the fixture.
81+
4. Reimport: `import-design-tokens.js --from <tmp> --out <tmp>/reimported.lock.json`.
82+
5. **Deep-equal the reimported lockfile against the original** — the core guarantee.
83+
6. `--verify` returns exit 0 for the matching lockfile, non-zero for a mutated one.
84+
7. Import directly from a `tokens.json` path also round-trips.
85+
86+
### 3. Consumer documentation — `docs/design-system-export/consumers.md`
87+
88+
- The interchange contract: `tokens.json` is lossless and stable; `tokens.css` /
89+
`tailwind-preset.ts` / typed `tokens.ts` are the ergonomic views.
90+
- **Aurelius** (self-consumer): the reimporter + the round-trip guarantee.
91+
- **Flavian** (WordPress/FSE): `pnpm add @scope/design-tokens`, then map
92+
`tokens.json``theme.json` (`settings.color.palette`,
93+
`settings.typography.fontFamilies`, `settings.spacing.spacingSizes`) with a
94+
documented mapping table and an example adapter snippet.
95+
- **Nerva** (Hono/Workers): `import { tokens } from '@scope/design-tokens'` for
96+
typed, tree-shakeable tokens in backend/email/PDF theming; example.
97+
- Style Dictionary note: `tokens.json` is a plain nested map and can be fed to
98+
Style Dictionary as a custom source if a consumer prefers that toolchain — we
99+
do not require it.
100+
101+
### 4. Wiring
102+
103+
- `export-design-system` skill + `/export-design-system` command: add a
104+
"Re-importing / round-trip" section pointing at the reimporter and the doc.
105+
- `CLAUDE.md` + `README.md`: document the new script; bump the manual script count.
106+
107+
## Out of scope (YAGNI)
108+
109+
- Generating consumer-specific adapter files (e.g. emitting `theme.json`) — the
110+
export→adapter mapping is *documented* with example snippets, not generated.
111+
- Reconstructing a lockfile from lossy `tokens.css` — impossible to do faithfully;
112+
`tokens.json` is the interchange.
113+
114+
## Files
115+
116+
| File | Action |
117+
|------|--------|
118+
| `scripts/import-design-tokens.js` | Create — reimporter |
119+
| `scripts/import-design-tokens.sh` | Create — thin wrapper |
120+
| `scripts/__tests__/design-system-roundtrip.test.js` | Create — round-trip test |
121+
| `docs/design-system-export/consumers.md` | Create — consumer contract + Flavian/Nerva |
122+
| `.claude/skills/export-design-system/SKILL.md` | Update — round-trip/reimport section |
123+
| `.claude/commands/export-design-system.md` | Update — reference reimport + round-trip |
124+
| `CLAUDE.md`, `README.md` | Update — script docs + counts |

0 commit comments

Comments
 (0)