@radicool/throughline 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/adapters/codex/AGENTS.md +10 -10
- package/adapters/codex/prompts/component-builder.md +59 -15
- package/adapters/codex/prompts/document-component.md +42 -10
- package/adapters/codex/prompts/storybook-chromatic-builder.md +52 -9
- package/adapters/codex/prompts/token-crosswalk-builder.md +2 -0
- package/adapters/codex/prompts/token-sync-layer.md +64 -4
- package/adapters/cursor/.cursor/commands/document-component.md +42 -10
- package/adapters/cursor/.cursor/rules/component-builder.mdc +60 -16
- package/adapters/cursor/.cursor/rules/component-pipeline.mdc +1 -1
- package/adapters/cursor/.cursor/rules/design-system-audit.mdc +1 -1
- package/adapters/cursor/.cursor/rules/figma-environment-setup.mdc +1 -1
- package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/repository-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/retrofit-planner.mdc +1 -1
- package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +53 -10
- package/adapters/cursor/.cursor/rules/token-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +3 -1
- package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +1 -1
- package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +65 -5
- package/adapters/generic/AGENTS.md +10 -10
- package/adapters/generic/commands/document-component.md +42 -10
- package/adapters/generic/skills/component-builder/SKILL.md +59 -15
- package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +52 -9
- package/adapters/generic/skills/token-crosswalk-builder/SKILL.md +2 -0
- package/adapters/generic/skills/token-sync-layer/SKILL.md +64 -4
- package/package.json +1 -1
- package/references/component-doc-archetypes.md +15 -11
- package/references/component-doc-schema.md +23 -5
- package/references/doc-card-builder.md +565 -0
- package/references/doc-writing-standard.md +144 -0
- package/references/figma-component-standards.md +63 -16
- package/references/guide-voice.md +96 -0
- package/references/manifest-schema.md +24 -6
- package/references/native-adapter-config.md +930 -0
- package/references/sync-adapters.md +94 -12
- package/scripts/README.md +37 -3
- package/scripts/build-doc-card-builder.mjs +143 -0
- package/scripts/build-native-adapter-config.mjs +280 -0
- package/scripts/docs-check.mjs +18 -4
- package/scripts/docs-lint.mjs +163 -0
- package/scripts/install.mjs +14 -1
- package/scripts/lib/doc-card-plan.mjs +101 -0
- package/scripts/lib/doc-card-render.figma.js +371 -0
- package/scripts/lib/dtcg.mjs +87 -0
- package/scripts/lib/native-literal.mjs +205 -0
- package/scripts/lib/sd-native.mjs +770 -0
- package/scripts/validate-crosswalk.mjs +3 -29
- package/scripts/validate-token-output.mjs +338 -0
|
@@ -35,16 +35,54 @@ user names one of these, use the vetted preset — high confidence, no guessing.
|
|
|
35
35
|
| `tailwind` | tailwind theme config | `dark:` variant / class strategy | preserved (via CSS vars) | `colors.background` |
|
|
36
36
|
| `mui` | JS theme object | `createTheme` palettes | preserved (object refs) | `palette.primary.main` |
|
|
37
37
|
| `vanilla-css` | one CSS file | `:root` + `[data-theme]` | preserved | `--color-bg-default` |
|
|
38
|
-
| `ios-swift` | Swift enum
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
38
|
+
| `ios-swift` | Swift enum constants (`Tokens.swift`) | one build per mode, one output directory per mode | flattened | `Tokens.textSm` |
|
|
39
|
+
|
|
40
|
+
Four of the five cover this plugin's web-first, design-led audience: three React
|
|
41
|
+
framework adapters (shadcn — the dominant new-project choice; standalone
|
|
42
|
+
Tailwind — for the large Tailwind-without-shadcn population; MUI — the
|
|
43
|
+
enterprise/Material standard), plus the universal `vanilla-css` escape hatch
|
|
44
|
+
(plain CSS custom properties, no framework). `ios-swift` is the one native
|
|
45
|
+
member, and it is curated because its configuration ships as tested code rather
|
|
46
|
+
than as advice — see `${CLAUDE_PLUGIN_ROOT}/references/native-adapter-config.md`.
|
|
47
|
+
Everything else — Ant Design, Chakra, HeroUI, Android/Kotlin, Flutter, React
|
|
48
|
+
Native, etc. — is fully supported via Tier 2.
|
|
49
|
+
|
|
50
|
+
**`ios-swift` was demoted, and has been restored.** Run against a real DTCG
|
|
51
|
+
source under the *stock* transform group it emitted px-authored dimensions at
|
|
52
|
+
×16 their authored value (valid, compiling Swift), leaked `color-mix()`
|
|
53
|
+
expressions, and left dual-node aliases as bare `px` literals. The tier badge
|
|
54
|
+
claimed a confidence the stock configuration had not earned, so it came off.
|
|
55
|
+
|
|
56
|
+
The cause was the transform group, not the adapter concept. That configuration
|
|
57
|
+
now ships as tested code at
|
|
58
|
+
`${CLAUDE_PLUGIN_ROOT}/scripts/lib/sd-native.mjs`, is installed into the
|
|
59
|
+
consumer's repo, and is verified end to end against a real source (195 emitted
|
|
60
|
+
symbols matched, zero rule failures under the shipping ruleset, on both light
|
|
61
|
+
and dark builds). The badge is back on that basis.
|
|
62
|
+
|
|
63
|
+
**What the badge does not cover: nothing is compiled.** Every emitted value is
|
|
64
|
+
checked to be a well-formed Swift or Kotlin *literal* — `tokens:validate-output`'s
|
|
65
|
+
`invalid-literal` rule parses each one and fails on anything that is not
|
|
66
|
+
(#53) — but no `swiftc` or `kotlinc` runs, so a type mismatch or a call to an
|
|
67
|
+
undefined symbol would still pass. String-valued tokens (`fontFamily`, `string`,
|
|
68
|
+
keyword `fontWeight`) are quoted by the module's own transforms, and a value with
|
|
69
|
+
no native form at all — a CSS `linear-gradient(...)` — is filtered out of native
|
|
70
|
+
output rather than emitted broken. That drop is reported as an unemitted token,
|
|
71
|
+
not hidden.
|
|
72
|
+
|
|
73
|
+
`android-kotlin` uses the same module and stays Tier 2: its remaining unknowns
|
|
74
|
+
are on the consumption side — Compose `dp`/`sp` behaviour against a real Compose
|
|
75
|
+
app, resource-qualifier conventions, package layout — which building tokens does
|
|
76
|
+
not exercise. The `dp`/`sp` split itself is no longer among them: font sizes and
|
|
77
|
+
line heights whose role a DTCG source states — the `fontSize`, `letterSpacing`
|
|
78
|
+
and `lineHeight` member names of §9.8's typography composite — now emit as
|
|
79
|
+
`sp`. What remains is narrower and documented in
|
|
80
|
+
`${CLAUDE_PLUGIN_ROOT}/references/native-adapter-config.md`: a bare scale
|
|
81
|
+
primitive carries no role and stays `dp`, and an `em` letterSpacing is
|
|
82
|
+
filtered out rather than emitted as `.em`.
|
|
83
|
+
`tokens:validate-output` remains what decides whether any adapter can be
|
|
84
|
+
trusted, and re-promotion is available to any adapter that passes it against a
|
|
85
|
+
real source.
|
|
48
86
|
|
|
49
87
|
`shadcn` vs `tailwind`: shadcn emits CSS vars *plus* the specific var names
|
|
50
88
|
shadcn components expect; `tailwind` targets Tailwind used on its own, mapping
|
|
@@ -99,8 +137,52 @@ emit as references to primitive vars rather than flattened literals.
|
|
|
99
137
|
- **Native adapters** (`ios-swift`, and generated native targets like
|
|
100
138
|
Android/Kotlin): `outputReferences: false` — references resolve to literal
|
|
101
139
|
values at build time, because the target language has no runtime var
|
|
102
|
-
indirection. Modes map to
|
|
103
|
-
|
|
140
|
+
indirection. Modes map to **one build per mode, one output directory per
|
|
141
|
+
mode** — each build passing only that mode's sources, through `nativeSources`.
|
|
142
|
+
Asset-catalog variants and resource qualifiers are *not* implemented: nothing
|
|
143
|
+
here emits an `.xcassets` catalog or a `values-night/` resource tree.
|
|
144
|
+
|
|
145
|
+
## What the stock configuration gets wrong
|
|
146
|
+
|
|
147
|
+
These are all legal in a real DTCG source, and a **stock** Style Dictionary
|
|
148
|
+
transform group mishandles every one of them silently — emitting output that
|
|
149
|
+
compiles and is wrong, which is why `tokens:validate-output` exists.
|
|
150
|
+
|
|
151
|
+
**None of these are Style Dictionary limitations.** All four are fixed by
|
|
152
|
+
roughly 80 lines of preprocessor and transform code, which ships as a tested
|
|
153
|
+
module at `${CLAUDE_PLUGIN_ROOT}/scripts/lib/sd-native.mjs` and is documented in
|
|
154
|
+
`${CLAUDE_PLUGIN_ROOT}/references/native-adapter-config.md`. Against a real
|
|
155
|
+
322-token source it emitted 195 symbols that all map to a source token, 107 of
|
|
156
|
+
them with their numeric magnitude additionally verified, with zero rule failures
|
|
157
|
+
— colour and string values are matched by name only and are checked by no rule.
|
|
158
|
+
**Import that module** rather than configuring a native adapter from a stock
|
|
159
|
+
`transformGroup` or transcribing the reference doc.
|
|
160
|
+
|
|
161
|
+
- **CSS expressions in a value** — `color-mix(in srgb, {color.brand.500} 12%,
|
|
162
|
+
transparent)` is a runtime CSS construct. Style Dictionary does no colour
|
|
163
|
+
math, so it resolves only the inner reference and leaves the function wrapper
|
|
164
|
+
in the output. *Fix: a transform that computes the blend to a literal.*
|
|
165
|
+
- **Dual-node tokens** — a node carrying both a `$value` and children (`text.sm`
|
|
166
|
+
with `$value: "14px"` plus a `text.sm.lineHeight` child). Style Dictionary's
|
|
167
|
+
resolver will not traverse into one, so every alias to the child fails to
|
|
168
|
+
resolve and emits as a bare literal; its collector also stops there, so the
|
|
169
|
+
child is never emitted at all. *Fix: a preprocessor that resolves aliases and
|
|
170
|
+
hoists the children.*
|
|
171
|
+
- **`%` and `em` dimensions** — parent-relative or container-relative, so there
|
|
172
|
+
genuinely is no build-time native magnitude. This one is a real limit rather
|
|
173
|
+
than a configuration gap. *Handling: filter them out of native builds — on the
|
|
174
|
+
authored value, since a `100%` token may be typed `string`, not `dimension`.*
|
|
175
|
+
- **A third mode axis** — this reference models theme (`.dark` /
|
|
176
|
+
`[data-theme]`) and brand (`[data-brand]`). A viewport axis carrying its own
|
|
177
|
+
spacing and type scales is common and has no mapping here; on native it is
|
|
178
|
+
size classes and resource qualifiers, resolved by a different mechanism
|
|
179
|
+
entirely.
|
|
180
|
+
|
|
181
|
+
**Native dimension transforms must read the authored unit.** The stock
|
|
182
|
+
`ios-swift` and `compose` transform groups assume `rem` input and multiply by
|
|
183
|
+
16. Against a `px`-authored source that silently produces output at sixteen
|
|
184
|
+
times scale which compiles and ships. Emit 1:1 for `px` and unitless ratios;
|
|
185
|
+
×16 only for `rem`.
|
|
104
186
|
|
|
105
187
|
## Brownfield value transforms
|
|
106
188
|
|
package/scripts/README.md
CHANGED
|
@@ -10,10 +10,38 @@ tested here; copied verbatim by `token-crosswalk-builder` into the user's
|
|
|
10
10
|
| `validate-crosswalk.mjs` | Resolve every `newToken` against the DTCG token source; assert resolved value == `newValue`, N/N. The CI gate. | `tokens:validate` |
|
|
11
11
|
| `build-reverse-index.mjs` | Emit a `codeToken -> newToken` map from the crosswalk to semi-automate SCSS/Tailwind swaps. | `tokens:reverse-index` |
|
|
12
12
|
| `guard-token-removal.mjs` | Grep `.ts/.tsx` (minus generated + tests) for about-to-be-deleted symbols; blocks cleanup until zero references remain. | run during the cleanup phase |
|
|
13
|
+
| `validate-token-output.mjs` | Assert generated native token output matches its DTCG source: authored-unit fidelity, no leaked CSS syntax, no bare unit literals, no mode collisions. Fails when no emitted symbol matches a source token, and reports match rate, unparsed lines, and unemitted tokens on every run. | `tokens:validate-output` |
|
|
13
14
|
| `lib/crosswalk.mjs` | Shared loader + structural validation for `crosswalk.json` (used by the validator and reverse-index). | copied alongside |
|
|
15
|
+
| `lib/dtcg.mjs` | Shared DTCG flatten + `{alias}` resolution. Dual-node aware: a node carrying both a `$value` and children yields its own value **and** is descended into. Used by `validate-crosswalk.mjs` and `validate-token-output.mjs`. | copied alongside both |
|
|
16
|
+
| `lib/sd-native.mjs` | The Style Dictionary native configuration as code: unit-aware dimension transforms, `color-mix` computation, dual-node preprocessing, platform assembly, and a per-mode source guard. Style Dictionary is a parameter, never an import. | copied alongside `validate-token-output.mjs` |
|
|
17
|
+
| `lib/native-literal.mjs` | Shared grammar for "is this a well-formed Swift or Kotlin literal": parses rather than pattern-matches, so an unquoted string, a raw CSS function, or any other unanticipated case fails the same way. Used by `sd-native.mjs`'s output filter and by `validate-token-output.mjs`'s invalid-literal rule. | copied alongside `validate-token-output.mjs` and `lib/sd-native.mjs` |
|
|
14
18
|
| `crosswalk.schema.json` | The finalized JSON Schema for `crosswalk.json` (contract + editor support). | copied beside `crosswalk.json` |
|
|
15
19
|
| `build-docs-digest.mjs` | Aggregate every `design-system/docs/components/*.doc.json` into `design-system/docs/index.json` + `llms.txt` for AI/human consumers. | `docs:digest` |
|
|
16
20
|
| `docs-check.mjs` | Drift gate — verifies each component's doc surfaces still match its canonical record (via `lib/doc-record.mjs` fingerprints). Exits 1 on drift. | `docs:check` |
|
|
21
|
+
| `docs-lint.mjs` | Copy lint for .doc.json records — warnings only, always exits 0 on a parseable record; the mechanical subset of `references/doc-writing-standard.md`. | `docs:lint` |
|
|
22
|
+
| `lib/doc-record.mjs` | Canonical record load + `canonicalFingerprint` (sha256 over the record minus `provenance`). The fingerprint every surface is stamped with. | copied alongside docs-check.mjs |
|
|
23
|
+
| `lib/doc-card-render.figma.js` | Figma renderer template for the doc card's `Usage` band and header. Inlined into `references/doc-card-builder.md`; never executed as a module. | plugin-internal (not installed) |
|
|
24
|
+
| `lib/doc-card-plan.mjs` | Pure layout planner for the doc card's `Usage` band + `DOC_CARD_RENDERER_VERSION` (single source of the layout version). Inlined into `references/doc-card-builder.md`; imported by `docs-check.mjs`. | copied alongside docs-check.mjs; also inlined into the generated builder |
|
|
25
|
+
| `build-doc-card-builder.mjs` | Generate `references/doc-card-builder.md` from the planner + the Figma renderer template (`lib/doc-card-render.figma.js`). `--check` gates CI. | plugin-internal (not installed) |
|
|
26
|
+
| `build-native-adapter-config.mjs` | Generate `references/native-adapter-config.md` by slicing `lib/sd-native.mjs` on its `@doc-section` markers and interleaving each fragment under its prose. Fails when module code falls outside every section, so the doc cannot silently ship incomplete. `--check` gates CI. | plugin-internal (not installed) |
|
|
27
|
+
|
|
28
|
+
**Documentation scripts — install as a set.** Copying these files without
|
|
29
|
+
registering them leaves a repo with a script on disk and no entry point, which
|
|
30
|
+
is how a stale `docs:check` went unnoticed for a full release. Both
|
|
31
|
+
`storybook-chromatic-builder` (first-time setup) and `/document-component`
|
|
32
|
+
(freshness refresh) install the same five files and register the same three
|
|
33
|
+
scripts:
|
|
34
|
+
|
|
35
|
+
| File | npm script |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `build-docs-digest.mjs` | `"docs:digest": "node scripts/build-docs-digest.mjs"` |
|
|
38
|
+
| `docs-check.mjs` | `"docs:check": "node scripts/docs-check.mjs"` |
|
|
39
|
+
| `docs-lint.mjs` | `"docs:lint": "node scripts/docs-lint.mjs"` |
|
|
40
|
+
| `lib/doc-record.mjs` | — (imported by the above) |
|
|
41
|
+
| `lib/doc-card-plan.mjs` | — (imported by the above) |
|
|
42
|
+
|
|
43
|
+
A refresh that adds a file must also add its npm script; check `package.json`
|
|
44
|
+
for all three every time, not just the file that changed.
|
|
17
45
|
|
|
18
46
|
The crosswalk contract is documented in
|
|
19
47
|
`${CLAUDE_PLUGIN_ROOT}/references/crosswalk-schema.md`.
|
|
@@ -31,9 +59,9 @@ conflict, or remaining reference), `2` bad CLI arguments.
|
|
|
31
59
|
|
|
32
60
|
## How the skill installs these
|
|
33
61
|
|
|
34
|
-
`token-crosswalk-builder` copies `lib/crosswalk.mjs`, `
|
|
35
|
-
`build-reverse-index.mjs`, `guard-token-removal.mjs`, and
|
|
36
|
-
into the user's `packages/tokens/scripts/` (schema beside `crosswalk.json`), then
|
|
62
|
+
`token-crosswalk-builder` copies `lib/crosswalk.mjs`, `lib/dtcg.mjs`,
|
|
63
|
+
`validate-crosswalk.mjs`, `build-reverse-index.mjs`, `guard-token-removal.mjs`, and
|
|
64
|
+
`crosswalk.schema.json` into the user's `packages/tokens/scripts/` (schema beside `crosswalk.json`), then
|
|
37
65
|
wires `packages/tokens/package.json`:
|
|
38
66
|
|
|
39
67
|
```jsonc
|
|
@@ -43,6 +71,12 @@ wires `packages/tokens/package.json`:
|
|
|
43
71
|
}
|
|
44
72
|
```
|
|
45
73
|
|
|
74
|
+
`token-sync-layer` copies `validate-token-output.mjs`, `lib/dtcg.mjs`,
|
|
75
|
+
`lib/native-literal.mjs`, **and** `lib/sd-native.mjs`, and wires
|
|
76
|
+
`"tokens:validate-output"`. All four travel together: `sd-native.mjs` and the
|
|
77
|
+
validator both import `lib/dtcg.mjs` and `lib/native-literal.mjs`, so
|
|
78
|
+
installing any one of them alone breaks at import time.
|
|
79
|
+
|
|
46
80
|
The scripts version with the user's repo so their CI runs them locally — a path
|
|
47
81
|
inside the plugin install would not be reachable from the user's CI.
|
|
48
82
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Generates references/doc-card-builder.md — the canonical figma_execute
|
|
2
|
+
// snippet that renders a doc card's Usage band — by inlining the pure planner
|
|
3
|
+
// (lib/doc-card-plan.mjs) above the Figma renderer template
|
|
4
|
+
// (lib/doc-card-render.figma.js). Mirrors the adapters generate.mjs idiom:
|
|
5
|
+
// run bare to write, run with --check to gate CI. Zero dependencies.
|
|
6
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const PLANNER = join(REPO_ROOT, 'scripts', 'lib', 'doc-card-plan.mjs');
|
|
12
|
+
const RENDERER = join(REPO_ROOT, 'scripts', 'lib', 'doc-card-render.figma.js');
|
|
13
|
+
const OUT = join(REPO_ROOT, 'references', 'doc-card-builder.md');
|
|
14
|
+
|
|
15
|
+
const HEADER = [
|
|
16
|
+
'# Doc-card Usage-band builder (GENERATED)',
|
|
17
|
+
'',
|
|
18
|
+
'> **GENERATED FILE — do not edit by hand.** Sources: `scripts/lib/doc-card-plan.mjs`',
|
|
19
|
+
'> (the pure planner, unit-tested in Node) + `scripts/lib/doc-card-render.figma.js`',
|
|
20
|
+
'> (the Figma renderer). Regenerate with `node scripts/build-doc-card-builder.mjs`;',
|
|
21
|
+
'> CI gates freshness with `--check`.',
|
|
22
|
+
'',
|
|
23
|
+
'The canonical `figma_execute` snippet that renders a component doc card\'s',
|
|
24
|
+
'`Usage` band from its `.doc.json` record. Every card is identical by',
|
|
25
|
+
'construction — never hand-build the usage body. The builder owns the `Usage`',
|
|
26
|
+
'band and the header\'s record-derived content (its short description and date);',
|
|
27
|
+
'it reads the specimen and never writes it. The status chip keeps its own owner',
|
|
28
|
+
'— the finalize write-back in `references/figma-component-standards.md`.',
|
|
29
|
+
'',
|
|
30
|
+
'## How to call it',
|
|
31
|
+
'',
|
|
32
|
+
'1. Load the record and compute its canonical fingerprint in Node',
|
|
33
|
+
' (`canonicalFingerprint` in `scripts/lib/doc-record.mjs`).',
|
|
34
|
+
'2. Read `figma.docCardVariables` from `design-system.json`.',
|
|
35
|
+
' - If present, resolve each of the nine roles to a Variable object **by',
|
|
36
|
+
' the recorded name** — do not re-derive, do not substitute a similar',
|
|
37
|
+
' name. Look each name up via `figma_get_variables`, then in the script',
|
|
38
|
+
' fetch it as a Variable object with',
|
|
39
|
+
' `figma.variables.getVariableByIdAsync(id)`. If a recorded name no',
|
|
40
|
+
' longer resolves to exactly one variable in the file, **throw** rather',
|
|
41
|
+
' than guess — the token was renamed or removed, and silently picking a',
|
|
42
|
+
' neighbour is how cards drift apart.',
|
|
43
|
+
' - If the field is absent (a project\'s first doc-card render, or any',
|
|
44
|
+
' render after the field is cleared), do not resolve fresh by judgement',
|
|
45
|
+
' yet — first check whether a doc card already exists in the file. If',
|
|
46
|
+
' one does, recover all nine roles from it by resolving each bound',
|
|
47
|
+
' variable id back to its name (`figma.variables.getVariableByIdAsync(id)`):',
|
|
48
|
+
' - `spacePadding` ← the `Usage` frame\'s `paddingLeft`.',
|
|
49
|
+
' - `spaceRowGap` ← the `Usage` frame\'s `itemSpacing`.',
|
|
50
|
+
' - `spaceBlockGap` ← a `Usage Row *` frame\'s `itemSpacing`.',
|
|
51
|
+
' - `spaceItemGap` ← a `Block: *` frame\'s `itemSpacing` (blocks are the',
|
|
52
|
+
' children of a `Usage Row *`).',
|
|
53
|
+
' - `border` ← a `Row Divider` frame\'s',
|
|
54
|
+
' `fills[0].boundVariables.color`.',
|
|
55
|
+
' - `tonePositive` ← the first TEXT child of the `Block: Do` frame\'s',
|
|
56
|
+
' `fills[0].boundVariables.color`.',
|
|
57
|
+
' - `toneNegative` ← the first TEXT child of the `Block: Don\'t` frame,',
|
|
58
|
+
' same property.',
|
|
59
|
+
' - `textMuted` ← the first TEXT child of any block other than',
|
|
60
|
+
' `Block: Do` / `Block: Don\'t`, same property (tone blocks colour',
|
|
61
|
+
' their eyebrow differently, so exclude them here).',
|
|
62
|
+
' - `textDefault` ← the second child of that same block when it is a',
|
|
63
|
+
' TEXT node — `Block: Overview` is reliable; definition blocks nest',
|
|
64
|
+
' frames there instead, so skip those. Same property.',
|
|
65
|
+
' A single-row card has no `Row Divider` (no `border`); a card without',
|
|
66
|
+
' `Block: Do` / `Block: Don\'t` yields no `tonePositive` / `toneNegative`.',
|
|
67
|
+
' Read another rendered card for the roles that specific card can\'t',
|
|
68
|
+
' yield, or fall back to judgement for just those. Only when no',
|
|
69
|
+
' rendered card exists at all does the caller choose every role by',
|
|
70
|
+
' judgement — establishing the project\'s rhythm, not guessing at one.',
|
|
71
|
+
' Either way, resolve the nine roles once, **write the mapping back to',
|
|
72
|
+
' `design-system.json`** as `figma.docCardVariables`, then render. Every',
|
|
73
|
+
' later render reads it.',
|
|
74
|
+
' The nine roles: `textDefault`, `textMuted` (text colors), `tonePositive`,',
|
|
75
|
+
' `toneNegative` (Do/Don\'t eyebrow colors — success/danger roles), `border`',
|
|
76
|
+
' (row dividers), `spacePadding`, `spaceRowGap`, `spaceBlockGap`,',
|
|
77
|
+
' `spaceItemGap` (spacing roles: band padding, row gap, block gutter,',
|
|
78
|
+
' within-block gap).',
|
|
79
|
+
'3. Find the body text style: `(await figma.getLocalTextStylesAsync())',
|
|
80
|
+
' .find((s) => s.name === \'Body/Default\')`. Missing variables or style =',
|
|
81
|
+
' the builder throws (bind-or-throw — the gap is in the token set; fix it',
|
|
82
|
+
' there, never hardcode around it).',
|
|
83
|
+
'4. Prepend the two slots, then the snippet below, then the call:',
|
|
84
|
+
'',
|
|
85
|
+
'```js',
|
|
86
|
+
'const RECORD = /* the parsed .doc.json object */;',
|
|
87
|
+
'const CANONICAL_FP = \'/* canonicalFingerprint(RECORD), 16 hex chars */\';',
|
|
88
|
+
'// … the generated snippet …',
|
|
89
|
+
'const card = await figma.getNodeByIdAsync(cardNodeId);',
|
|
90
|
+
'const summary = await renderDocCard({ card, record: RECORD, vars, bodyTextStyle });',
|
|
91
|
+
'```',
|
|
92
|
+
'',
|
|
93
|
+
'5. Pass an explicit `timeout` (30000 is right for one card; the ~30s',
|
|
94
|
+
' `figma_execute` ceiling fits a single card comfortably — render cards one',
|
|
95
|
+
' call at a time, never batched).',
|
|
96
|
+
'6. Verify from the returned summary — `rowsRendered`, `blocksCreated`,',
|
|
97
|
+
' `cardWidth` — not from a screenshot, then stamp the manifest from it:',
|
|
98
|
+
' `surfaces.docCard = { src: summary.fingerprint, render: summary.renderHash,',
|
|
99
|
+
' renderer: summary.rendererVersion }`. Never re-read the card to stamp.',
|
|
100
|
+
'',
|
|
101
|
+
'## The snippet',
|
|
102
|
+
'',
|
|
103
|
+
'```js',
|
|
104
|
+
].join('\n');
|
|
105
|
+
|
|
106
|
+
const FOOTER = [
|
|
107
|
+
'```',
|
|
108
|
+
'',
|
|
109
|
+
'Layout contract and rationale:',
|
|
110
|
+
'`docs/superpowers/specs/2026-08-09-doc-card-layout-and-voice-design.md`.',
|
|
111
|
+
'',
|
|
112
|
+
].join('\n');
|
|
113
|
+
|
|
114
|
+
export function buildDocCardBuilder({ plannerSource, rendererSource }) {
|
|
115
|
+
const inlined = plannerSource
|
|
116
|
+
.replace(/^export const /gm, 'const ')
|
|
117
|
+
.replace(/^export function /gm, 'function ');
|
|
118
|
+
for (const [name, src] of [['doc-card-plan.mjs', inlined], ['doc-card-render.figma.js', rendererSource]]) {
|
|
119
|
+
if (/^\s*(import|export)\b/m.test(src)) {
|
|
120
|
+
throw new Error(`${name} must stay import-free (only top-level \`export const\`/\`export function\` allowed in the planner) — it is inlined into the Figma snippet where no module system exists`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return `${HEADER}\n${inlined}\n${rendererSource}${FOOTER}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
127
|
+
const result = buildDocCardBuilder({
|
|
128
|
+
plannerSource: readFileSync(PLANNER, 'utf8'),
|
|
129
|
+
rendererSource: readFileSync(RENDERER, 'utf8'),
|
|
130
|
+
});
|
|
131
|
+
if (process.argv.includes('--check')) {
|
|
132
|
+
let onDisk = null;
|
|
133
|
+
try { onDisk = readFileSync(OUT, 'utf8'); } catch (e) { /* missing counts as drift */ }
|
|
134
|
+
if (onDisk !== result) {
|
|
135
|
+
console.error('✗ references/doc-card-builder.md out of date; run: node scripts/build-doc-card-builder.mjs');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
console.log('✓ doc-card builder in sync');
|
|
139
|
+
} else {
|
|
140
|
+
writeFileSync(OUT, result);
|
|
141
|
+
console.log('✓ wrote references/doc-card-builder.md');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// Generates references/native-adapter-config.md from scripts/lib/sd-native.mjs.
|
|
2
|
+
// The prose lives here, keyed by section id; the code is sliced out of the
|
|
3
|
+
// module's real source between @doc-section markers and interleaved beneath its
|
|
4
|
+
// prose. Mirrors build-doc-card-builder.mjs: run bare to write, --check to gate
|
|
5
|
+
// CI. Zero dependencies.
|
|
6
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
export const SOURCE = join(REPO_ROOT, 'scripts', 'lib', 'sd-native.mjs');
|
|
12
|
+
export const OUT = join(REPO_ROOT, 'references', 'native-adapter-config.md');
|
|
13
|
+
|
|
14
|
+
const OPEN = /^\s*\/\/\s*@doc-section\s+(\S+)\s*$/;
|
|
15
|
+
const CLOSE = /^\s*\/\/\s*@doc-section-end\s+(\S+)\s*$/;
|
|
16
|
+
|
|
17
|
+
export function sliceSections(source) {
|
|
18
|
+
const sections = new Map();
|
|
19
|
+
let open = null;
|
|
20
|
+
let buffer = [];
|
|
21
|
+
for (const line of source.split('\n')) {
|
|
22
|
+
const closeMatch = line.match(CLOSE);
|
|
23
|
+
if (closeMatch) {
|
|
24
|
+
if (!open) throw new Error(`@doc-section-end ${closeMatch[1]} with no open section`);
|
|
25
|
+
if (closeMatch[1] !== open) {
|
|
26
|
+
throw new Error(`@doc-section-end ${closeMatch[1]} does not close ${open}`);
|
|
27
|
+
}
|
|
28
|
+
sections.set(open, buffer.join('\n').trim());
|
|
29
|
+
open = null;
|
|
30
|
+
buffer = [];
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const openMatch = line.match(OPEN);
|
|
34
|
+
if (openMatch) {
|
|
35
|
+
if (open) throw new Error(`unclosed @doc-section ${open}`);
|
|
36
|
+
if (sections.has(openMatch[1])) throw new Error(`duplicate @doc-section ${openMatch[1]}`);
|
|
37
|
+
open = openMatch[1];
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (open) buffer.push(line);
|
|
41
|
+
}
|
|
42
|
+
if (open) throw new Error(`unclosed @doc-section ${open}`);
|
|
43
|
+
return sections;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Every line of module source must live inside some @doc-section pair, so that
|
|
47
|
+
// adding a piece to the module without documenting it breaks CI rather than
|
|
48
|
+
// silently shipping a doc whose code does not run. Legitimately outside: blank
|
|
49
|
+
// lines, `//` comment lines (the file's leading banner), and the markers
|
|
50
|
+
// themselves. Anything else — any line with executable code on it — is a gap.
|
|
51
|
+
export function assertCovered(source) {
|
|
52
|
+
const uncovered = [];
|
|
53
|
+
let open = false;
|
|
54
|
+
source.split('\n').forEach((line, i) => {
|
|
55
|
+
if (CLOSE.test(line)) {
|
|
56
|
+
open = false;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (OPEN.test(line)) {
|
|
60
|
+
open = true;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (open) return;
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (trimmed === '' || trimmed.startsWith('//')) return;
|
|
66
|
+
uncovered.push(` ${i + 1}: ${line}`);
|
|
67
|
+
});
|
|
68
|
+
if (uncovered.length === 0) return;
|
|
69
|
+
throw new Error(
|
|
70
|
+
`${uncovered.length} line(s) of ${SOURCE} fall outside every @doc-section pair.\n` +
|
|
71
|
+
'Only blank lines and // comments may sit outside a section; code must be\n' +
|
|
72
|
+
'inside one, or the generated doc shows code that cannot run.\n' +
|
|
73
|
+
uncovered.join('\n'),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Section id -> the prose that introduces it. Order here is the doc's order.
|
|
78
|
+
const PROSE = [
|
|
79
|
+
['imports', `## Imports
|
|
80
|
+
|
|
81
|
+
\`node:fs\` plus the siblings \`lib/dtcg.mjs\` and \`lib/native-literal.mjs\` this
|
|
82
|
+
plugin already installs — nothing else. Style Dictionary is passed in as a
|
|
83
|
+
parameter, never imported, which is what keeps this module installable into a
|
|
84
|
+
consumer's repo.`],
|
|
85
|
+
['unit-aware', `## 1. Read the authored unit
|
|
86
|
+
|
|
87
|
+
**This replaces \`size/swift/remToCGFloat\` and the \`size/compose/*\` transforms,
|
|
88
|
+
and it is the single most important piece.** Those assume every dimension is
|
|
89
|
+
authored in \`rem\` and multiply by 16. Against a \`px\`-authored source that
|
|
90
|
+
silently produces output at sixteen times scale which compiles and ships.`],
|
|
91
|
+
['color-mix', `## 2. Compute \`color-mix()\` to a literal
|
|
92
|
+
|
|
93
|
+
A CSS expression has no native equivalent, and Style Dictionary does no colour
|
|
94
|
+
math. Native adapters resolve to literals; for a \`color-mix\` that means
|
|
95
|
+
actually computing the blend. Register this **before** the platform's colour
|
|
96
|
+
transform, so the colour transform receives a valid hex8 rather than a CSS
|
|
97
|
+
function.`],
|
|
98
|
+
['preprocess', `## 3. Resolve aliases and hoist dual-node children
|
|
99
|
+
|
|
100
|
+
Style Dictionary's resolver will not traverse into a node that carries both a
|
|
101
|
+
\`$value\` and children, and its collector stops there too. The dual-node pattern
|
|
102
|
+
is invalid DTCG — the Design Tokens Format Module's 30 July 2026 draft, §6.1,
|
|
103
|
+
requires tools to report it as an error, and §6.2's \`$root\` is the sanctioned
|
|
104
|
+
way to pair a value with children. Figma-derived sources emit it anyway:
|
|
105
|
+
\`text.sm\` holds \`$value: "14px"\` *and* a \`text.sm.lineHeight\` child. So every
|
|
106
|
+
alias to such a child fails to resolve, and the child is never emitted at all.
|
|
107
|
+
|
|
108
|
+
Both are fixed before Style Dictionary sees the tree.`],
|
|
109
|
+
['platform', `## 4. Assemble the platform from the stock list
|
|
110
|
+
|
|
111
|
+
Build the transform list from Style Dictionary's **stock group**, replacing only
|
|
112
|
+
the rem-assuming size transforms. A hand-picked list silently drops whatever it
|
|
113
|
+
forgets — three real defects arose exactly that way.
|
|
114
|
+
|
|
115
|
+
**The \`dp\`/\`sp\` split is fixed here; two narrower Android-only limits remain.**
|
|
116
|
+
Style Dictionary's Compose transforms select on \`$type\`, and DTCG's type set
|
|
117
|
+
does not line up with what they expect — there is no \`fontSize\` type, because
|
|
118
|
+
DTCG types font sizes as \`dimension\`. So the role is taken instead from the
|
|
119
|
+
member names DTCG §9.8 fixes for the typography composite, stamped onto
|
|
120
|
+
\`$extensions\` during preprocessing, and the two Compose transforms partition on
|
|
121
|
+
that stamp. Measured against a real source: 39 declarations that emitted \`dp\`
|
|
122
|
+
now emit \`sp\`, with the Swift output byte-identical.
|
|
123
|
+
|
|
124
|
+
What remains:
|
|
125
|
+
|
|
126
|
+
- **A bare scale primitive emits as \`dp\`.** \`text.base: "16px"\` is a font size
|
|
127
|
+
only to a human — no nominal or structural signal marks it — so it is not
|
|
128
|
+
stamped. The semantic tokens that reference it are, and those are what a
|
|
129
|
+
consumer should reach for.
|
|
130
|
+
- **An \`em\` letter spacing reaches Compose but not Swift.** \`size/unit-aware/compose-em\`
|
|
131
|
+
emits it as a real \`.em\` TextUnit, parenthesised — \`(-0.03).em\` — because
|
|
132
|
+
\`-0.03.em\` parses as \`-(0.03.em)\` and needs an \`unaryMinus\` operator, while
|
|
133
|
+
the parenthesised form compiles regardless. iOS is excluded deliberately, not
|
|
134
|
+
pending: letter spacing there is an \`NSAttributedString\` kern in points,
|
|
135
|
+
which needs the font size the token does not carry, so no constant Swift
|
|
136
|
+
could emit would be right at every font size.
|
|
137
|
+
|
|
138
|
+
The first is Android-only. \`size/unit-aware/swift\` filters
|
|
139
|
+
\`dimension || fontSize\` and emits \`CGFloat\`, which carries no unit to be wrong
|
|
140
|
+
about; iOS handles Dynamic Type at the use site via \`UIFontMetrics\`.
|
|
141
|
+
\`tokens:validate-output\` passes in both cases: it checks magnitude, not unit.
|
|
142
|
+
|
|
143
|
+
**A unitless value is no longer one of them.** DTCG §8.2.1 requires a dimension
|
|
144
|
+
to carry a unit, §8.7's \`number\` is the type for a ratio, and §9.8 types
|
|
145
|
+
\`lineHeight\` as one — so \`leading.normal: "1.5"\` typed \`dimension\` is malformed
|
|
146
|
+
input. No size transform claims it: it emits bare on both platforms, which is
|
|
147
|
+
byte-for-byte what a correctly typed \`number\` already produced, so correcting
|
|
148
|
+
the source's \`$type\` changes no output. \`tokens:validate-output\` reports it as
|
|
149
|
+
a \`unitless-dimension\` advisory, which does not gate — the emitted value is
|
|
150
|
+
right under the ratio reading, and only the author can say whether a ratio is
|
|
151
|
+
what was meant.
|
|
152
|
+
|
|
153
|
+
**The stock list is accounted for, not transcribed.** \`PLATFORMS\` records the
|
|
154
|
+
stock group each platform mirrors, and \`auditStockGroups\` checks at
|
|
155
|
+
registration that every transform in that live group is either run here or
|
|
156
|
+
declined in writing, with a reason. A stock transform this config has never
|
|
157
|
+
decided about warns; it is never silently dropped. The check warns and never
|
|
158
|
+
throws — a new stock transform is usually harmless, and the fatal direction, a
|
|
159
|
+
transform we run being removed, already makes Style Dictionary throw on an
|
|
160
|
+
unknown name. It runs in your build because that is the only place the
|
|
161
|
+
installed Style Dictionary version is knowable: ThroughLine declares no
|
|
162
|
+
dependency on it.`],
|
|
163
|
+
['sources', `## 5. Guard the per-mode source list
|
|
164
|
+
|
|
165
|
+
Style Dictionary deduplicates by dot-path, so one build over both a light and a
|
|
166
|
+
dark definition of the same token keeps whichever file sorts last and drops the
|
|
167
|
+
other mode with no diagnostic. Pass every build's sources through
|
|
168
|
+
\`nativeSources\`, which returns them, so the check cannot be skipped by
|
|
169
|
+
forgetting it.`],
|
|
170
|
+
['register', `## 6. Register with Style Dictionary
|
|
171
|
+
|
|
172
|
+
One call. Style Dictionary is a parameter, never an import, which is what lets
|
|
173
|
+
this module install into a consumer's \`packages/tokens/scripts/lib/\`.`],
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const HEADER = `# Native adapter configuration (GENERATED)
|
|
177
|
+
|
|
178
|
+
> **GENERATED FILE — do not edit by hand.** Source: \`scripts/lib/sd-native.mjs\`,
|
|
179
|
+
> which is unit-tested in Node and installed into the consumer's repo.
|
|
180
|
+
> Regenerate with \`node scripts/build-native-adapter-config.mjs\`; CI gates
|
|
181
|
+
> freshness with \`--check\`.
|
|
182
|
+
|
|
183
|
+
The Style Dictionary configuration a native adapter (\`ios-swift\`,
|
|
184
|
+
\`android-kotlin\`, or any generated native target) needs in order to emit
|
|
185
|
+
**correct** output from a real DTCG token source.
|
|
186
|
+
|
|
187
|
+
**Why this exists.** The stock \`ios-swift\` and \`compose\` transform groups
|
|
188
|
+
produce output that compiles and is wrong. Run against a real source, the stock
|
|
189
|
+
configuration emitted every \`px\`-authored dimension at ×16 its authored value,
|
|
190
|
+
leaked \`color-mix()\` expressions into Swift, and left dual-node aliases as bare
|
|
191
|
+
\`px\` literals — all at exit \`0\`. None of that is a Style Dictionary limitation.
|
|
192
|
+
All of it is configuration.
|
|
193
|
+
|
|
194
|
+
**You do not need to copy any of this.** It ships as
|
|
195
|
+
\`\${CLAUDE_PLUGIN_ROOT}/scripts/lib/sd-native.mjs\`. Install it beside
|
|
196
|
+
\`lib/dtcg.mjs\` and \`lib/native-literal.mjs\` and call it:
|
|
197
|
+
|
|
198
|
+
\`\`\`js
|
|
199
|
+
import StyleDictionary from 'style-dictionary';
|
|
200
|
+
import { registerNativeTransforms, nativePlatform, nativeSources }
|
|
201
|
+
from './scripts/lib/sd-native.mjs';
|
|
202
|
+
|
|
203
|
+
registerNativeTransforms(StyleDictionary);
|
|
204
|
+
|
|
205
|
+
for (const mode of ['light', 'dark']) {
|
|
206
|
+
const sd = new StyleDictionary({
|
|
207
|
+
source: nativeSources(sourcesFor(mode)),
|
|
208
|
+
preprocessors: ['dtcg/resolve-dual-node'],
|
|
209
|
+
platforms: {
|
|
210
|
+
ios: nativePlatform({ platform: 'ios-swift', buildPath: \`out/\${mode}/\` }),
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
await sd.buildAllPlatforms();
|
|
214
|
+
}
|
|
215
|
+
\`\`\`
|
|
216
|
+
|
|
217
|
+
The sections below are the module's own source, inlined so the configuration
|
|
218
|
+
stays reviewable. Pair this with \`\${CLAUDE_PLUGIN_ROOT}/references/sync-adapters.md\`,
|
|
219
|
+
which covers the adapter contract itself.
|
|
220
|
+
`;
|
|
221
|
+
|
|
222
|
+
const FOOTER = `## Verify, always
|
|
223
|
+
|
|
224
|
+
Configuration this specific is exactly what regresses unnoticed, because every
|
|
225
|
+
failure mode above produces output that compiles. Run \`tokens:validate-output\`
|
|
226
|
+
against each generated file with the same source list that file's build used,
|
|
227
|
+
and treat it as a gate rather than a spot check:
|
|
228
|
+
|
|
229
|
+
\`\`\`
|
|
230
|
+
node scripts/validate-token-output.mjs \\
|
|
231
|
+
--source tokens/color-primitives.json --source tokens/text-primitives.json \\
|
|
232
|
+
--output out/light/Tokens.swift --platform ios-swift --min-match 1
|
|
233
|
+
\`\`\`
|
|
234
|
+
|
|
235
|
+
A clean run reports 100% of emitted symbols matched with zero rule failures.
|
|
236
|
+
Anything less means the configuration drifted — so **pass \`--min-match 1\`**.
|
|
237
|
+
The flag's default is \`0.5\`, which is a floor against wholly unparseable output
|
|
238
|
+
rather than the gate this doc describes; without it a 60% match rate exits \`0\`.
|
|
239
|
+
See \`\${CLAUDE_PLUGIN_ROOT}/scripts/README.md\`.
|
|
240
|
+
|
|
241
|
+
"Matched" means an emitted symbol's name resolved to a source token. Numeric
|
|
242
|
+
magnitudes are additionally compared; colour and string values are matched by
|
|
243
|
+
name only, and no rule checks that the output compiles.
|
|
244
|
+
`;
|
|
245
|
+
|
|
246
|
+
export function render(sections) {
|
|
247
|
+
const declared = PROSE.map(([id]) => id);
|
|
248
|
+
for (const id of declared) {
|
|
249
|
+
if (!sections.has(id)) throw new Error(`@doc-section ${id} is declared in PROSE but missing from ${SOURCE}`);
|
|
250
|
+
}
|
|
251
|
+
for (const id of sections.keys()) {
|
|
252
|
+
if (!declared.includes(id)) throw new Error(`@doc-section ${id} exists in ${SOURCE} but has no prose entry`);
|
|
253
|
+
}
|
|
254
|
+
const body = PROSE.map(([id, prose]) => `${prose}\n\n\`\`\`js\n${sections.get(id)}\n\`\`\`\n`).join('\n');
|
|
255
|
+
return `${HEADER}\n${body}\n${FOOTER}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function main() {
|
|
259
|
+
const check = process.argv.includes('--check');
|
|
260
|
+
const source = readFileSync(SOURCE, 'utf8');
|
|
261
|
+
assertCovered(source);
|
|
262
|
+
const rendered = render(sliceSections(source));
|
|
263
|
+
if (!check) {
|
|
264
|
+
writeFileSync(OUT, rendered);
|
|
265
|
+
console.log(`wrote ${OUT}`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const current = readFileSync(OUT, 'utf8');
|
|
269
|
+
if (current === rendered) {
|
|
270
|
+
console.log('references/native-adapter-config.md is up to date');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
console.error(
|
|
274
|
+
'references/native-adapter-config.md is stale.\n' +
|
|
275
|
+
'Run: node scripts/build-native-adapter-config.mjs',
|
|
276
|
+
);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main();
|