@radicool/throughline 0.17.0 → 0.18.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/package.json +1 -1
- package/references/native-adapter-config.md +38 -10
- package/scripts/lib/dtcg.mjs +7 -1
- package/scripts/lib/sd-native.mjs +38 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@radicool/throughline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Build a complete design system end to end — author in Figma, sync tokens to code, generate Storybook. Usable from Claude Code, Cursor, Codex, or any AGENTS.md agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -54,6 +54,7 @@ consumer's repo.
|
|
|
54
54
|
import { readFileSync } from 'node:fs';
|
|
55
55
|
import {
|
|
56
56
|
flattenDtcg,
|
|
57
|
+
flattenDtcgTypes,
|
|
57
58
|
resolveValue,
|
|
58
59
|
findModeCollisions,
|
|
59
60
|
TEXT_UNIT_NAMES,
|
|
@@ -342,13 +343,32 @@ export { EXT_NS };
|
|
|
342
343
|
// on is gone. Matching a suffix against the camel-joined name instead would
|
|
343
344
|
// couple the rule to the hoist's naming scheme, and case-insensitively it
|
|
344
345
|
// false-positives on names like baselineHeight.
|
|
345
|
-
|
|
346
|
+
//
|
|
347
|
+
// The type comes from `types`, a DTCG 5.2.2 resolution of the whole tree, not
|
|
348
|
+
// from the token's own literal $type. Reading val.$type made this pass blind to
|
|
349
|
+
// a source that declares $type once on the group — legal DTCG, and on such a
|
|
350
|
+
// source the reference-graph inference stamped nothing at all (#85). A token's
|
|
351
|
+
// own $type still wins; the group's applies only where the token states none.
|
|
352
|
+
//
|
|
353
|
+
// WHERE THIS MATTERS, measured rather than assumed. Style Dictionary runs
|
|
354
|
+
// global preprocessors, THEN its own typeDtcgDelegate — which is 5.2.2, pushing
|
|
355
|
+
// each group's $type onto its descendants — then platform preprocessors
|
|
356
|
+
// (StyleDictionary.js:340, :348, :440 in 4.4.0). nativePlatform registers this
|
|
357
|
+
// preprocessor at PLATFORM level, downstream of that delegation, so a build
|
|
358
|
+
// wired only through nativePlatform never saw the defect: on a group-typed
|
|
359
|
+
// re-encoding of a real system it emits the same 208 declarations and 48 sp
|
|
360
|
+
// either way. The defect reaches the build that ALSO declares this preprocessor
|
|
361
|
+
// at top level, which is the wiring the usage snippet above shows — there the
|
|
362
|
+
// first pass runs before any delegation. Resolving the type here makes both
|
|
363
|
+
// wirings agree instead of depending on which one a consumer copied.
|
|
364
|
+
function classifyTextUnits(node, types, prefix = []) {
|
|
346
365
|
for (const [key, val] of Object.entries(node)) {
|
|
347
366
|
if (key.startsWith('$') || !val || typeof val !== 'object') continue;
|
|
367
|
+
const path = [...prefix, key];
|
|
348
368
|
if (
|
|
349
369
|
TEXT_UNIT_NAMES.has(key) &&
|
|
350
370
|
'$value' in val &&
|
|
351
|
-
|
|
371
|
+
types[path.join('.')] === 'dimension' &&
|
|
352
372
|
TEXT_ROLE_UNIT.test(String(val.$value).trim())
|
|
353
373
|
) {
|
|
354
374
|
val.$extensions ??= {};
|
|
@@ -362,7 +382,7 @@ function classifyTextUnits(node) {
|
|
|
362
382
|
// the pass idempotent.
|
|
363
383
|
if (!('nativeUnit' in ns)) ns.nativeUnit = 'text';
|
|
364
384
|
}
|
|
365
|
-
classifyTextUnits(val);
|
|
385
|
+
classifyTextUnits(val, types, path);
|
|
366
386
|
}
|
|
367
387
|
return node;
|
|
368
388
|
}
|
|
@@ -373,7 +393,9 @@ function classifyTextUnits(node) {
|
|
|
373
393
|
// text.xsLineHeight and the graph's paths are written in pre-hoist names.
|
|
374
394
|
//
|
|
375
395
|
// The three gates are classifyTextUnits's, verbatim — a dimension, a value with
|
|
376
|
-
// a unit, and no role already recorded.
|
|
396
|
+
// a unit, and no role already recorded. Both passes read the SAME resolved-type
|
|
397
|
+
// map, which is what makes them agree by construction rather than by two copies
|
|
398
|
+
// of DTCG 5.2.2 staying in step. A unitless value is never stamped: no
|
|
377
399
|
// size transform claims one since #52, and stamping a ratio as text would still
|
|
378
400
|
// be a claim the source never made.
|
|
379
401
|
//
|
|
@@ -381,14 +403,14 @@ function classifyTextUnits(node) {
|
|
|
381
403
|
// unresolvable reference in place for Style Dictionary to report, so the graph
|
|
382
404
|
// can hold an edge to a token that does not exist. Skip it. This is also what
|
|
383
405
|
// keeps the second preprocess pass from throwing, and idempotency with it.
|
|
384
|
-
function applyTextRoleGraph(node, typographic) {
|
|
406
|
+
function applyTextRoleGraph(node, typographic, types) {
|
|
385
407
|
for (const path of typographic) {
|
|
386
408
|
let target = node;
|
|
387
409
|
for (const segment of path.split('.')) {
|
|
388
410
|
target = target && typeof target === 'object' ? target[segment] : undefined;
|
|
389
411
|
}
|
|
390
412
|
if (!target || typeof target !== 'object' || !('$value' in target)) continue;
|
|
391
|
-
if (
|
|
413
|
+
if (types[path] !== 'dimension') continue;
|
|
392
414
|
if (!TEXT_ROLE_UNIT.test(String(target.$value).trim())) continue;
|
|
393
415
|
target.$extensions ??= {};
|
|
394
416
|
target.$extensions[EXT_NS] ??= {};
|
|
@@ -403,11 +425,17 @@ export function preprocess(dict) {
|
|
|
403
425
|
// Read from the UNRESOLVED dict, before resolveInPlace flattens the aliases
|
|
404
426
|
// the graph is made of.
|
|
405
427
|
const { typographic } = textRoleGraph(dict);
|
|
428
|
+
const resolved = resolveInPlace(structuredClone(dict), flattenDtcg(dict));
|
|
429
|
+
// DTCG 5.2.2 types for the whole tree, walked once and shared by both passes
|
|
430
|
+
// below. Neither writes $type or moves a node, so one map is correct for
|
|
431
|
+
// both, and sharing it is what makes them agree by construction rather than
|
|
432
|
+
// by two copies of the same rule staying in step (#85).
|
|
433
|
+
//
|
|
434
|
+
// Computed on the RESOLVED clone, which is the tree both passes read.
|
|
435
|
+
// resolveInPlace rewrites $value strings only, so the types are the source's.
|
|
436
|
+
const types = flattenDtcgTypes(resolved);
|
|
406
437
|
const out = hoistDualNodes(
|
|
407
|
-
applyTextRoleGraph(
|
|
408
|
-
classifyTextUnits(resolveInPlace(structuredClone(dict), flattenDtcg(dict))),
|
|
409
|
-
typographic,
|
|
410
|
-
),
|
|
438
|
+
applyTextRoleGraph(classifyTextUnits(resolved, types), typographic, types),
|
|
411
439
|
collisions,
|
|
412
440
|
);
|
|
413
441
|
if (collisions.length) {
|
package/scripts/lib/dtcg.mjs
CHANGED
|
@@ -178,6 +178,12 @@ export function textRoleGraph(dict) {
|
|
|
178
178
|
// failure this module exists to prevent. A token whose source already stamps
|
|
179
179
|
// nativeUnit is closed and is not reported.
|
|
180
180
|
const inferredGroups = new Set([...typographic].map((p) => p.split('.').slice(0, -1).join('.')));
|
|
181
|
+
// DTCG 5.2.2, not the token's own literal $type: a source that declares
|
|
182
|
+
// $type once on the group and not on each token is legal DTCG, and gating on
|
|
183
|
+
// val.$type made this walk blind to it — so the advisory that exists to name
|
|
184
|
+
// a silent gap was itself silent on the shape where the whole pipeline goes
|
|
185
|
+
// quiet (#85).
|
|
186
|
+
const types = flattenDtcgTypes(dict);
|
|
181
187
|
const unreferencedSiblings = [];
|
|
182
188
|
(function walk(node, prefix) {
|
|
183
189
|
for (const [key, val] of Object.entries(node)) {
|
|
@@ -187,7 +193,7 @@ export function textRoleGraph(dict) {
|
|
|
187
193
|
const group = prefix.join('.');
|
|
188
194
|
if (
|
|
189
195
|
'$value' in val &&
|
|
190
|
-
|
|
196
|
+
types[dotted] === 'dimension' &&
|
|
191
197
|
TEXT_ROLE_UNIT.test(String(val.$value).trim()) &&
|
|
192
198
|
!referrers.has(dotted) &&
|
|
193
199
|
!('nativeUnit' in (val.$extensions?.[EXT_NS] ?? {})) &&
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readFileSync } from 'node:fs';
|
|
13
13
|
import {
|
|
14
14
|
flattenDtcg,
|
|
15
|
+
flattenDtcgTypes,
|
|
15
16
|
resolveValue,
|
|
16
17
|
findModeCollisions,
|
|
17
18
|
TEXT_UNIT_NAMES,
|
|
@@ -273,13 +274,32 @@ export { EXT_NS };
|
|
|
273
274
|
// on is gone. Matching a suffix against the camel-joined name instead would
|
|
274
275
|
// couple the rule to the hoist's naming scheme, and case-insensitively it
|
|
275
276
|
// false-positives on names like baselineHeight.
|
|
276
|
-
|
|
277
|
+
//
|
|
278
|
+
// The type comes from `types`, a DTCG 5.2.2 resolution of the whole tree, not
|
|
279
|
+
// from the token's own literal $type. Reading val.$type made this pass blind to
|
|
280
|
+
// a source that declares $type once on the group — legal DTCG, and on such a
|
|
281
|
+
// source the reference-graph inference stamped nothing at all (#85). A token's
|
|
282
|
+
// own $type still wins; the group's applies only where the token states none.
|
|
283
|
+
//
|
|
284
|
+
// WHERE THIS MATTERS, measured rather than assumed. Style Dictionary runs
|
|
285
|
+
// global preprocessors, THEN its own typeDtcgDelegate — which is 5.2.2, pushing
|
|
286
|
+
// each group's $type onto its descendants — then platform preprocessors
|
|
287
|
+
// (StyleDictionary.js:340, :348, :440 in 4.4.0). nativePlatform registers this
|
|
288
|
+
// preprocessor at PLATFORM level, downstream of that delegation, so a build
|
|
289
|
+
// wired only through nativePlatform never saw the defect: on a group-typed
|
|
290
|
+
// re-encoding of a real system it emits the same 208 declarations and 48 sp
|
|
291
|
+
// either way. The defect reaches the build that ALSO declares this preprocessor
|
|
292
|
+
// at top level, which is the wiring the usage snippet above shows — there the
|
|
293
|
+
// first pass runs before any delegation. Resolving the type here makes both
|
|
294
|
+
// wirings agree instead of depending on which one a consumer copied.
|
|
295
|
+
function classifyTextUnits(node, types, prefix = []) {
|
|
277
296
|
for (const [key, val] of Object.entries(node)) {
|
|
278
297
|
if (key.startsWith('$') || !val || typeof val !== 'object') continue;
|
|
298
|
+
const path = [...prefix, key];
|
|
279
299
|
if (
|
|
280
300
|
TEXT_UNIT_NAMES.has(key) &&
|
|
281
301
|
'$value' in val &&
|
|
282
|
-
|
|
302
|
+
types[path.join('.')] === 'dimension' &&
|
|
283
303
|
TEXT_ROLE_UNIT.test(String(val.$value).trim())
|
|
284
304
|
) {
|
|
285
305
|
val.$extensions ??= {};
|
|
@@ -293,7 +313,7 @@ function classifyTextUnits(node) {
|
|
|
293
313
|
// the pass idempotent.
|
|
294
314
|
if (!('nativeUnit' in ns)) ns.nativeUnit = 'text';
|
|
295
315
|
}
|
|
296
|
-
classifyTextUnits(val);
|
|
316
|
+
classifyTextUnits(val, types, path);
|
|
297
317
|
}
|
|
298
318
|
return node;
|
|
299
319
|
}
|
|
@@ -304,7 +324,9 @@ function classifyTextUnits(node) {
|
|
|
304
324
|
// text.xsLineHeight and the graph's paths are written in pre-hoist names.
|
|
305
325
|
//
|
|
306
326
|
// The three gates are classifyTextUnits's, verbatim — a dimension, a value with
|
|
307
|
-
// a unit, and no role already recorded.
|
|
327
|
+
// a unit, and no role already recorded. Both passes read the SAME resolved-type
|
|
328
|
+
// map, which is what makes them agree by construction rather than by two copies
|
|
329
|
+
// of DTCG 5.2.2 staying in step. A unitless value is never stamped: no
|
|
308
330
|
// size transform claims one since #52, and stamping a ratio as text would still
|
|
309
331
|
// be a claim the source never made.
|
|
310
332
|
//
|
|
@@ -312,14 +334,14 @@ function classifyTextUnits(node) {
|
|
|
312
334
|
// unresolvable reference in place for Style Dictionary to report, so the graph
|
|
313
335
|
// can hold an edge to a token that does not exist. Skip it. This is also what
|
|
314
336
|
// keeps the second preprocess pass from throwing, and idempotency with it.
|
|
315
|
-
function applyTextRoleGraph(node, typographic) {
|
|
337
|
+
function applyTextRoleGraph(node, typographic, types) {
|
|
316
338
|
for (const path of typographic) {
|
|
317
339
|
let target = node;
|
|
318
340
|
for (const segment of path.split('.')) {
|
|
319
341
|
target = target && typeof target === 'object' ? target[segment] : undefined;
|
|
320
342
|
}
|
|
321
343
|
if (!target || typeof target !== 'object' || !('$value' in target)) continue;
|
|
322
|
-
if (
|
|
344
|
+
if (types[path] !== 'dimension') continue;
|
|
323
345
|
if (!TEXT_ROLE_UNIT.test(String(target.$value).trim())) continue;
|
|
324
346
|
target.$extensions ??= {};
|
|
325
347
|
target.$extensions[EXT_NS] ??= {};
|
|
@@ -334,11 +356,17 @@ export function preprocess(dict) {
|
|
|
334
356
|
// Read from the UNRESOLVED dict, before resolveInPlace flattens the aliases
|
|
335
357
|
// the graph is made of.
|
|
336
358
|
const { typographic } = textRoleGraph(dict);
|
|
359
|
+
const resolved = resolveInPlace(structuredClone(dict), flattenDtcg(dict));
|
|
360
|
+
// DTCG 5.2.2 types for the whole tree, walked once and shared by both passes
|
|
361
|
+
// below. Neither writes $type or moves a node, so one map is correct for
|
|
362
|
+
// both, and sharing it is what makes them agree by construction rather than
|
|
363
|
+
// by two copies of the same rule staying in step (#85).
|
|
364
|
+
//
|
|
365
|
+
// Computed on the RESOLVED clone, which is the tree both passes read.
|
|
366
|
+
// resolveInPlace rewrites $value strings only, so the types are the source's.
|
|
367
|
+
const types = flattenDtcgTypes(resolved);
|
|
337
368
|
const out = hoistDualNodes(
|
|
338
|
-
applyTextRoleGraph(
|
|
339
|
-
classifyTextUnits(resolveInPlace(structuredClone(dict), flattenDtcg(dict))),
|
|
340
|
-
typographic,
|
|
341
|
-
),
|
|
369
|
+
applyTextRoleGraph(classifyTextUnits(resolved, types), typographic, types),
|
|
342
370
|
collisions,
|
|
343
371
|
);
|
|
344
372
|
if (collisions.length) {
|