inkbridge 0.1.0-beta.2 → 0.1.0-beta.21
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 +108 -25
- package/bin/inkbridge.mjs +354 -83
- package/code.js +40 -11802
- package/manifest.json +1 -0
- package/package.json +74 -23
- package/scanner/adapter-utils-regression.ts +159 -0
- package/scanner/aspect-percent-position-regression.ts +237 -0
- package/scanner/aspect-ratio-regression.ts +90 -0
- package/scanner/blob-placement-regression.ts +2 -2
- package/scanner/block-cache-regression.ts +195 -0
- package/scanner/bundle-size-regression.ts +50 -0
- package/scanner/child-sizing-matrix-regression.ts +303 -0
- package/scanner/cli.ts +342 -13
- package/scanner/component-scanner.ts +2108 -174
- package/scanner/component-sections-regression.ts +198 -0
- package/scanner/compound-classes-lookup-regression.ts +163 -0
- package/scanner/css-token-reader-regression.ts +7 -6
- package/scanner/css-token-reader.ts +152 -31
- package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
- package/scanner/cva-master-icon-regression.ts +315 -0
- package/scanner/data-attr-prop-alias-regression.ts +129 -0
- package/scanner/explicit-size-root-regression.ts +102 -0
- package/scanner/font-family-extract-regression.ts +113 -0
- package/scanner/font-style-resolver-regression.ts +1 -1
- package/scanner/framework-adapter-shadcn-regression.ts +480 -0
- package/scanner/full-width-matrix-regression.ts +338 -0
- package/scanner/grid-cols-extraction-regression.ts +110 -0
- package/scanner/image-src-collector-regression.ts +204 -0
- package/scanner/inline-flex-regression.ts +235 -0
- package/scanner/input-range-regression.ts +217 -0
- package/scanner/instance-rendering-regression.ts +224 -0
- package/scanner/jsx-prop-unresolved-regression.ts +178 -0
- package/scanner/jsx-text-regression.ts +178 -0
- package/scanner/layout-alignment-regression.ts +108 -0
- package/scanner/layout-flex-regression.ts +90 -0
- package/scanner/layout-mode-regression.ts +71 -0
- package/scanner/layout-sizing-regression.ts +227 -0
- package/scanner/layout-spacing-regression.ts +135 -0
- package/scanner/local-const-className-regression.ts +331 -0
- package/scanner/percent-position-regression.ts +105 -0
- package/scanner/provider-cascade-regression.ts +224 -0
- package/scanner/provider-flatten-regression.ts +235 -0
- package/scanner/radial-gradient-regression.ts +1 -1
- package/scanner/render-prop-parser-regression.ts +161 -0
- package/scanner/ring-utility-regression.ts +153 -0
- package/scanner/sandbox-spread-regression.ts +125 -0
- package/scanner/selection-pressed-regression.ts +241 -0
- package/scanner/size-full-normalization-regression.ts +127 -0
- package/scanner/state-classification-regression.ts +175 -0
- package/scanner/story-diagnostics-regression.ts +216 -0
- package/scanner/story-dimensioning-regression.ts +298 -0
- package/scanner/story-render-strategy-regression.ts +205 -0
- package/scanner/stretch-to-parent-width-regression.ts +147 -0
- package/scanner/svg-fill-parent-regression.ts +98 -0
- package/scanner/svg-group-inheritance-regression.ts +166 -0
- package/scanner/svg-marker-inline-regression.ts +211 -0
- package/scanner/svg-marker-regression.ts +116 -0
- package/scanner/tailwind-parser.ts +46 -4
- package/scanner/text-resize-matrix-regression.ts +173 -0
- package/scanner/transform-math-regression.ts +1 -1
- package/scanner/types.ts +26 -2
- package/src/cache/frame-cache.ts +150 -0
- package/src/cache/index.ts +2 -0
- package/src/{component-defs.ts → components/component-defs.ts} +25 -10
- package/src/{component-gen.ts → components/component-gen.ts} +43 -116
- package/src/components/component-instance.ts +386 -0
- package/src/components/component-library.ts +44 -0
- package/src/components/component-lookup.ts +161 -0
- package/src/components/index.ts +7 -0
- package/src/components/scanner-types.ts +39 -0
- package/src/components/symbol-instance-policy.ts +312 -0
- package/src/design-system/block-cache.ts +130 -0
- package/src/design-system/component-sections.ts +107 -0
- package/src/design-system/cva-inference.ts +187 -0
- package/src/design-system/cva-master.ts +427 -0
- package/src/design-system/cva-utils.ts +29 -0
- package/src/design-system/design-system.ts +334 -0
- package/src/design-system/frame-stabilizers.ts +191 -0
- package/src/design-system/frame-utils.ts +46 -0
- package/src/design-system/generated-node.ts +84 -0
- package/src/design-system/icon-rendering.ts +229 -0
- package/src/design-system/index.ts +13 -0
- package/src/design-system/instance-rendering.ts +307 -0
- package/src/design-system/master-shared.ts +133 -0
- package/src/design-system/node-helpers.ts +237 -0
- package/src/design-system/node-variants.ts +196 -0
- package/src/design-system/non-cva-master.ts +104 -0
- package/src/design-system/portal-handling.ts +138 -0
- package/src/design-system/preview-builder.ts +738 -0
- package/src/{render-context.ts → design-system/render-context.ts} +32 -6
- package/src/design-system/render-prop-parser.ts +50 -0
- package/src/design-system/responsive-resolver.ts +180 -0
- package/src/design-system/selectable-state.ts +157 -0
- package/src/design-system/state-master.ts +267 -0
- package/src/design-system/state-utils.ts +15 -0
- package/src/design-system/story-builder-context.ts +40 -0
- package/src/design-system/story-builder.ts +1322 -0
- package/src/design-system/story-diagnostics.ts +80 -0
- package/src/design-system/story-dimensioning.ts +272 -0
- package/src/design-system/story-frames.ts +400 -0
- package/src/design-system/story-instance.ts +333 -0
- package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
- package/src/design-system/story-render-strategy.ts +150 -0
- package/src/design-system/story-tree-search.ts +110 -0
- package/src/design-system/symbol-fallback.ts +89 -0
- package/src/design-system/symbol-source.ts +172 -0
- package/src/design-system/table-helpers.ts +56 -0
- package/src/design-system/tag-predicates.ts +99 -0
- package/src/design-system/theme-context.ts +52 -0
- package/src/design-system/typography.ts +100 -0
- package/src/design-system/ui-builder.ts +2676 -0
- package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
- package/src/effects/icon-builder.ts +1074 -0
- package/src/effects/index.ts +5 -0
- package/src/effects/portal-panel.ts +369 -0
- package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
- package/src/framework-adapters/index.ts +47 -0
- package/src/framework-adapters/shadcn.ts +541 -0
- package/src/{github.ts → github/github.ts} +46 -21
- package/src/github/index.ts +1 -0
- package/src/layout/deferred-layout.ts +1556 -0
- package/src/layout/index.ts +24 -0
- package/src/layout/layout-parser.ts +375 -0
- package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
- package/src/layout/parser/alignment.ts +54 -0
- package/src/layout/parser/flex.ts +59 -0
- package/src/layout/parser/index.ts +65 -0
- package/src/layout/parser/ir.ts +80 -0
- package/src/layout/parser/layout-mode.ts +57 -0
- package/src/layout/parser/sizing.ts +241 -0
- package/src/layout/parser/spacing-scale.ts +78 -0
- package/src/layout/parser/spacing.ts +134 -0
- package/src/layout/ring-utils.ts +120 -0
- package/src/layout/size-utils.ts +143 -0
- package/src/layout/text-resize-decision.ts +51 -0
- package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
- package/src/main.ts +444 -162
- package/src/{config.ts → plugin/config.ts} +12 -12
- package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
- package/src/plugin/image-src-collector.ts +52 -0
- package/src/plugin/index.ts +3 -0
- package/src/plugin/packs/index.ts +2 -0
- package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
- package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
- package/src/render-engine-version.ts +2 -0
- package/src/tailwind/adapter-utils.ts +137 -0
- package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
- package/src/tailwind/index.ts +8 -0
- package/src/tailwind/jsx-utils.ts +319 -0
- package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
- package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
- package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
- package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
- package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
- package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
- package/src/text/index.ts +4 -0
- package/src/{inline-text.ts → text/inline-text.ts} +13 -13
- package/src/{text-builder.ts → text/text-builder.ts} +24 -7
- package/src/{text-line.ts → text/text-line.ts} +2 -2
- package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
- package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
- package/src/{colors.ts → tokens/colors.ts} +13 -6
- package/src/tokens/index.ts +6 -0
- package/src/{token-source.ts → tokens/token-source.ts} +4 -1
- package/src/{tokens.ts → tokens/tokens.ts} +116 -20
- package/src/{variables.ts → tokens/variables.ts} +447 -102
- package/templates/patch-tokens-route.ts +25 -6
- package/templates/scan-components-route.ts +26 -5
- package/ui.html +485 -37
- package/src/component-lookup.ts +0 -82
- package/src/design-system.ts +0 -59
- package/src/icon-builder.ts +0 -607
- package/src/layout-parser.ts +0 -667
- package/src/story-builder.ts +0 -1706
- package/src/ui-builder.ts +0 -1996
- /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
- /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
- /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { makeEmptyIR } from '../src/layout/parser/ir';
|
|
3
|
+
import { parseSizing } from '../src/layout/parser/sizing';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Locks in the Tailwind sizing → LayoutIR mapping that previously
|
|
7
|
+
* lived inside LayoutParser as a private static method. Phase 3 of
|
|
8
|
+
* the layout-parser split moved this logic into `parser/sizing.ts` —
|
|
9
|
+
* this fixture catches accidental behavioural regressions.
|
|
10
|
+
*
|
|
11
|
+
* Covers: w-* / h-* (scale, arbitrary, fractional), size-*, w-full,
|
|
12
|
+
* h-full / h-screen / min-h-screen, max-w-N, named max-w (xs..7xl,
|
|
13
|
+
* prose, screen-*).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Fixed width / height — scale values
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
{
|
|
20
|
+
const ir = makeEmptyIR();
|
|
21
|
+
parseSizing(['w-20'], ir);
|
|
22
|
+
assert.equal(ir.widthMode, 'FIXED');
|
|
23
|
+
assert.equal(ir.fixedWidth, 80);
|
|
24
|
+
assert.equal(ir.heightMode, 'HUG');
|
|
25
|
+
}
|
|
26
|
+
{
|
|
27
|
+
const ir = makeEmptyIR();
|
|
28
|
+
parseSizing(['h-10'], ir);
|
|
29
|
+
assert.equal(ir.heightMode, 'FIXED');
|
|
30
|
+
assert.equal(ir.fixedHeight, 40);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Arbitrary values [Xpx], [Yrem], [Zem]
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
{
|
|
37
|
+
const ir = makeEmptyIR();
|
|
38
|
+
parseSizing(['w-[100px]', 'h-[1.5rem]'], ir);
|
|
39
|
+
assert.equal(ir.widthMode, 'FIXED');
|
|
40
|
+
assert.equal(ir.fixedWidth, 100);
|
|
41
|
+
assert.equal(ir.heightMode, 'FIXED');
|
|
42
|
+
assert.equal(ir.fixedHeight, 24); // 1.5rem * 16
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Fractional widths (FILL with widthFraction)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
{
|
|
49
|
+
const ir = makeEmptyIR();
|
|
50
|
+
parseSizing(['w-1/2'], ir);
|
|
51
|
+
assert.equal(ir.widthMode, 'FILL');
|
|
52
|
+
assert.equal(ir.widthFraction, 0.5);
|
|
53
|
+
}
|
|
54
|
+
{
|
|
55
|
+
const ir = makeEmptyIR();
|
|
56
|
+
parseSizing(['w-2/3'], ir);
|
|
57
|
+
assert.equal(ir.widthMode, 'FILL');
|
|
58
|
+
assert.ok(Math.abs((ir.widthFraction ?? 0) - 2 / 3) < 1e-9);
|
|
59
|
+
}
|
|
60
|
+
{
|
|
61
|
+
const ir = makeEmptyIR();
|
|
62
|
+
parseSizing(['h-3/4'], ir);
|
|
63
|
+
assert.equal(ir.heightMode, 'FILL');
|
|
64
|
+
assert.equal(ir.heightFraction, 0.75);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// size-N → both axes FIXED at the same value
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
{
|
|
71
|
+
const ir = makeEmptyIR();
|
|
72
|
+
parseSizing(['size-12'], ir);
|
|
73
|
+
assert.equal(ir.widthMode, 'FIXED');
|
|
74
|
+
assert.equal(ir.heightMode, 'FIXED');
|
|
75
|
+
assert.equal(ir.fixedWidth, 48);
|
|
76
|
+
assert.equal(ir.fixedHeight, 48);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// w-full / h-full / h-screen / min-h-screen → FILL
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
{
|
|
83
|
+
const ir = makeEmptyIR();
|
|
84
|
+
parseSizing(['w-full'], ir);
|
|
85
|
+
assert.equal(ir.widthMode, 'FILL');
|
|
86
|
+
assert.equal(ir.heightMode, 'HUG');
|
|
87
|
+
}
|
|
88
|
+
{
|
|
89
|
+
const ir = makeEmptyIR();
|
|
90
|
+
parseSizing(['h-full'], ir);
|
|
91
|
+
assert.equal(ir.heightMode, 'FILL');
|
|
92
|
+
}
|
|
93
|
+
{
|
|
94
|
+
const ir = makeEmptyIR();
|
|
95
|
+
parseSizing(['h-screen'], ir);
|
|
96
|
+
assert.equal(ir.heightMode, 'FILL');
|
|
97
|
+
}
|
|
98
|
+
{
|
|
99
|
+
const ir = makeEmptyIR();
|
|
100
|
+
parseSizing(['min-h-screen'], ir);
|
|
101
|
+
assert.equal(ir.heightMode, 'FILL');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// max-w with scale value — collapses to fixed width
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
{
|
|
108
|
+
const ir = makeEmptyIR();
|
|
109
|
+
parseSizing(['max-w-96'], ir);
|
|
110
|
+
assert.equal(ir.widthMode, 'FIXED');
|
|
111
|
+
assert.equal(ir.fixedWidth, 384);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Named max-w tokens
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
{
|
|
118
|
+
const cases: Array<[string, number]> = [
|
|
119
|
+
['max-w-xs', 320],
|
|
120
|
+
['max-w-sm', 384],
|
|
121
|
+
['max-w-md', 448],
|
|
122
|
+
['max-w-lg', 512],
|
|
123
|
+
['max-w-xl', 576],
|
|
124
|
+
['max-w-2xl', 672],
|
|
125
|
+
['max-w-3xl', 768],
|
|
126
|
+
['max-w-4xl', 896],
|
|
127
|
+
['max-w-5xl', 1024],
|
|
128
|
+
['max-w-6xl', 1152],
|
|
129
|
+
['max-w-7xl', 1280],
|
|
130
|
+
['max-w-prose', 640],
|
|
131
|
+
['max-w-screen-sm', 640],
|
|
132
|
+
['max-w-screen-md', 768],
|
|
133
|
+
['max-w-screen-lg', 1024],
|
|
134
|
+
['max-w-screen-xl', 1280],
|
|
135
|
+
['max-w-screen-2xl', 1536],
|
|
136
|
+
];
|
|
137
|
+
for (const [cls, expected] of cases) {
|
|
138
|
+
const ir = makeEmptyIR();
|
|
139
|
+
parseSizing([cls], ir);
|
|
140
|
+
assert.equal(ir.widthMode, 'FIXED', cls);
|
|
141
|
+
assert.equal(ir.fixedWidth, expected, cls);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Combinations — w + h together
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
{
|
|
149
|
+
const ir = makeEmptyIR();
|
|
150
|
+
parseSizing(['w-full', 'max-w-7xl'], ir);
|
|
151
|
+
// max-w fires AFTER w-full in the loop and overrides to FIXED at 1280.
|
|
152
|
+
assert.equal(ir.widthMode, 'FIXED');
|
|
153
|
+
assert.equal(ir.fixedWidth, 1280);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Arbitrary percent widths / heights — w-[60%], h-[40%]
|
|
158
|
+
// Used by the shadcn Progress adapter (indicator inside track) and by any
|
|
159
|
+
// consumer who writes percent widths directly in className.
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
{
|
|
162
|
+
const cases: Array<[string, number]> = [
|
|
163
|
+
['w-[0%]', 0],
|
|
164
|
+
['w-[33%]', 0.33],
|
|
165
|
+
['w-[60%]', 0.6],
|
|
166
|
+
['w-[100%]', 1],
|
|
167
|
+
['w-[50.5%]', 0.505],
|
|
168
|
+
];
|
|
169
|
+
for (const [cls, expected] of cases) {
|
|
170
|
+
const ir = makeEmptyIR();
|
|
171
|
+
parseSizing([cls], ir);
|
|
172
|
+
assert.equal(ir.widthMode, 'FILL', cls);
|
|
173
|
+
assert.ok(
|
|
174
|
+
Math.abs((ir.widthFraction ?? -1) - expected) < 1e-9,
|
|
175
|
+
`${cls} → widthFraction ${expected} (got ${ir.widthFraction})`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
{
|
|
181
|
+
const ir = makeEmptyIR();
|
|
182
|
+
parseSizing(['h-[40%]'], ir);
|
|
183
|
+
assert.equal(ir.heightMode, 'FILL', 'h-[40%] → heightMode=FILL');
|
|
184
|
+
assert.ok(
|
|
185
|
+
Math.abs((ir.heightFraction ?? -1) - 0.4) < 1e-9,
|
|
186
|
+
'h-[40%] → heightFraction=0.4',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Out-of-range percents clamp to [0, 1].
|
|
191
|
+
{
|
|
192
|
+
const ir = makeEmptyIR();
|
|
193
|
+
parseSizing(['w-[250%]'], ir);
|
|
194
|
+
assert.equal(ir.widthFraction, 1, 'w-[250%] clamps to 1');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// min-h-* / min-w-* — pixel floors. shadcn Textarea uses `min-h-20` to keep
|
|
199
|
+
// a multi-line area visible even when empty; the renderer enforces this
|
|
200
|
+
// via Figma's auto-layout minHeight property plus a resize floor.
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
{
|
|
203
|
+
const cases: Array<[string, 'minHeight' | 'minWidth', number]> = [
|
|
204
|
+
['min-h-20', 'minHeight', 80],
|
|
205
|
+
['min-h-10', 'minHeight', 40],
|
|
206
|
+
['min-h-[120px]', 'minHeight', 120],
|
|
207
|
+
['min-h-[8rem]', 'minHeight', 128],
|
|
208
|
+
['min-w-32', 'minWidth', 128],
|
|
209
|
+
['min-w-[8rem]', 'minWidth', 128],
|
|
210
|
+
];
|
|
211
|
+
for (const [cls, key, expected] of cases) {
|
|
212
|
+
const ir = makeEmptyIR();
|
|
213
|
+
parseSizing([cls], ir);
|
|
214
|
+
assert.equal(ir[key], expected, `${cls} → ir.${key}=${expected}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// min-h-screen is already special-cased upstream (FILL mode); make sure the
|
|
219
|
+
// new min-h-N matcher doesn't shadow it.
|
|
220
|
+
{
|
|
221
|
+
const ir = makeEmptyIR();
|
|
222
|
+
parseSizing(['min-h-screen'], ir);
|
|
223
|
+
assert.equal(ir.heightMode, 'FILL', 'min-h-screen still maps to FILL');
|
|
224
|
+
assert.equal(ir.minHeight, undefined, 'min-h-screen does NOT set a pixel minHeight');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log('layout-sizing-regression: ok');
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { makeEmptyIR } from '../src/layout/parser/ir';
|
|
3
|
+
import { resolveSpacing } from '../src/layout/parser/spacing-scale';
|
|
4
|
+
import { parseGap, parsePadding } from '../src/layout/parser/spacing';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Locks in the Tailwind spacing → LayoutIR mapping that previously
|
|
8
|
+
* lived inside LayoutParser as private static methods. Phase 2 of the
|
|
9
|
+
* layout-parser split moved this logic into `parser/spacing.ts` and
|
|
10
|
+
* `parser/spacing-scale.ts` — this fixture catches accidental
|
|
11
|
+
* behavioural regressions introduced by future edits to either file.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// resolveSpacing — pure value resolution
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
{
|
|
18
|
+
// Tailwind scale
|
|
19
|
+
assert.equal(resolveSpacing('0'), 0);
|
|
20
|
+
assert.equal(resolveSpacing('px'), 1);
|
|
21
|
+
assert.equal(resolveSpacing('0.5'), 2);
|
|
22
|
+
assert.equal(resolveSpacing('1'), 4);
|
|
23
|
+
assert.equal(resolveSpacing('4'), 16);
|
|
24
|
+
assert.equal(resolveSpacing('8'), 32);
|
|
25
|
+
assert.equal(resolveSpacing('96'), 384);
|
|
26
|
+
|
|
27
|
+
// Arbitrary values
|
|
28
|
+
assert.equal(resolveSpacing('[12px]'), 12);
|
|
29
|
+
assert.equal(resolveSpacing('[1rem]'), 16);
|
|
30
|
+
assert.equal(resolveSpacing('[1.5rem]'), 24);
|
|
31
|
+
assert.equal(resolveSpacing('[2em]'), 32);
|
|
32
|
+
assert.equal(resolveSpacing('[24]'), 24); // unitless arbitrary defaults to px
|
|
33
|
+
|
|
34
|
+
// Bare numbers off the scale → multiplied by 4
|
|
35
|
+
assert.equal(resolveSpacing('100'), 400);
|
|
36
|
+
|
|
37
|
+
// Unknown
|
|
38
|
+
assert.equal(resolveSpacing('???'), 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// parseGap — gap-* / gap-x-* / gap-y-* / space-x-* / space-y-*
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
{
|
|
45
|
+
// gap-N sets all three
|
|
46
|
+
const ir = makeEmptyIR();
|
|
47
|
+
parseGap(['gap-4'], ir);
|
|
48
|
+
assert.equal(ir.gap, 16);
|
|
49
|
+
assert.equal(ir.gapX, 16);
|
|
50
|
+
assert.equal(ir.gapY, 16);
|
|
51
|
+
}
|
|
52
|
+
{
|
|
53
|
+
// gap-x in a HORIZONTAL layout becomes the primary `ir.gap`
|
|
54
|
+
const ir = makeEmptyIR();
|
|
55
|
+
ir.layoutMode = 'HORIZONTAL';
|
|
56
|
+
parseGap(['gap-x-2'], ir);
|
|
57
|
+
assert.equal(ir.gap, 8);
|
|
58
|
+
assert.equal(ir.gapX, 8);
|
|
59
|
+
assert.equal(ir.gapY, undefined);
|
|
60
|
+
}
|
|
61
|
+
{
|
|
62
|
+
// gap-x in a VERTICAL layout sets gapX but does NOT update primary gap
|
|
63
|
+
const ir = makeEmptyIR();
|
|
64
|
+
ir.layoutMode = 'VERTICAL';
|
|
65
|
+
parseGap(['gap-x-2'], ir);
|
|
66
|
+
assert.equal(ir.gap, 0);
|
|
67
|
+
assert.equal(ir.gapX, 8);
|
|
68
|
+
}
|
|
69
|
+
{
|
|
70
|
+
// gap-y in VERTICAL flow becomes primary gap
|
|
71
|
+
const ir = makeEmptyIR();
|
|
72
|
+
ir.layoutMode = 'VERTICAL';
|
|
73
|
+
parseGap(['gap-y-3'], ir);
|
|
74
|
+
assert.equal(ir.gap, 12);
|
|
75
|
+
assert.equal(ir.gapY, 12);
|
|
76
|
+
}
|
|
77
|
+
{
|
|
78
|
+
// space-x in HORIZONTAL becomes gap; space-y in HORIZONTAL is ignored
|
|
79
|
+
const ir = makeEmptyIR();
|
|
80
|
+
ir.layoutMode = 'HORIZONTAL';
|
|
81
|
+
parseGap(['space-x-4', 'space-y-8'], ir);
|
|
82
|
+
assert.equal(ir.gap, 16);
|
|
83
|
+
}
|
|
84
|
+
{
|
|
85
|
+
// Arbitrary value in gap
|
|
86
|
+
const ir = makeEmptyIR();
|
|
87
|
+
parseGap(['gap-[10px]'], ir);
|
|
88
|
+
assert.equal(ir.gap, 10);
|
|
89
|
+
assert.equal(ir.gapX, 10);
|
|
90
|
+
assert.equal(ir.gapY, 10);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// parsePadding — p-* / px-* / py-* / pt/pr/pb/pl-*
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
{
|
|
97
|
+
// p-N sets all four
|
|
98
|
+
const ir = makeEmptyIR();
|
|
99
|
+
parsePadding(['p-6'], ir);
|
|
100
|
+
assert.equal(ir.paddingTop, 24);
|
|
101
|
+
assert.equal(ir.paddingRight, 24);
|
|
102
|
+
assert.equal(ir.paddingBottom, 24);
|
|
103
|
+
assert.equal(ir.paddingLeft, 24);
|
|
104
|
+
}
|
|
105
|
+
{
|
|
106
|
+
// px-N + py-N + per-side overrides
|
|
107
|
+
const ir = makeEmptyIR();
|
|
108
|
+
parsePadding(['px-4', 'py-2', 'pt-8'], ir);
|
|
109
|
+
// px-4 left + right
|
|
110
|
+
assert.equal(ir.paddingLeft, 16);
|
|
111
|
+
assert.equal(ir.paddingRight, 16);
|
|
112
|
+
// py-2 sets bottom; pt-8 overrides top
|
|
113
|
+
assert.equal(ir.paddingBottom, 8);
|
|
114
|
+
assert.equal(ir.paddingTop, 32);
|
|
115
|
+
}
|
|
116
|
+
{
|
|
117
|
+
// Arbitrary padding
|
|
118
|
+
const ir = makeEmptyIR();
|
|
119
|
+
parsePadding(['p-[20px]'], ir);
|
|
120
|
+
assert.equal(ir.paddingTop, 20);
|
|
121
|
+
assert.equal(ir.paddingRight, 20);
|
|
122
|
+
assert.equal(ir.paddingBottom, 20);
|
|
123
|
+
assert.equal(ir.paddingLeft, 20);
|
|
124
|
+
}
|
|
125
|
+
{
|
|
126
|
+
// pl/pr individual
|
|
127
|
+
const ir = makeEmptyIR();
|
|
128
|
+
parsePadding(['pl-3', 'pr-5'], ir);
|
|
129
|
+
assert.equal(ir.paddingLeft, 12);
|
|
130
|
+
assert.equal(ir.paddingRight, 20);
|
|
131
|
+
assert.equal(ir.paddingTop, 0);
|
|
132
|
+
assert.equal(ir.paddingBottom, 0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log('layout-spacing-regression: ok');
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { ComponentScanner } from './component-scanner';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression: function-body-local `const` declarations whose initializer
|
|
7
|
+
* depends on a prop value must be resolved when the identifier is
|
|
8
|
+
* referenced in a child JSX `className`. Without this, the common pattern
|
|
9
|
+
*
|
|
10
|
+
* const StatusPill = ({ sourceLabel }) => {
|
|
11
|
+
* const dotClass =
|
|
12
|
+
* sourceLabel === "db" ? "bg-emerald-500"
|
|
13
|
+
* : sourceLabel === "live" ? "bg-sky-400"
|
|
14
|
+
* : "bg-muted-foreground";
|
|
15
|
+
* return <span className={cn("h-2 w-2 rounded-full", dotClass)} />;
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* collapses to just `"h-2 w-2 rounded-full"` in the scanned output —
|
|
19
|
+
* the conditional color silently disappears.
|
|
20
|
+
*
|
|
21
|
+
* History: the inkbridge greenhouse-app `DataSourcesCard` shipped this
|
|
22
|
+
* pattern for its source-status pill. Stories passed `overallSourceLabel:
|
|
23
|
+
* "live"`, the runtime evaluated `dotClass = "bg-sky-400"`, but Figma
|
|
24
|
+
* rendered every pill with a black dot (the `bg-muted-foreground` fallback
|
|
25
|
+
* was being picked up incorrectly because the `cn(base, dotClass)` call
|
|
26
|
+
* lost the `dotClass` argument entirely).
|
|
27
|
+
*
|
|
28
|
+
* Root cause: `resolveExpressionValue` looked up identifiers in
|
|
29
|
+
* `propsContext` (function params), then fell back to
|
|
30
|
+
* `SourceFile.getVariableDeclaration` — which only searches MODULE-LEVEL
|
|
31
|
+
* declarations. Function-body `const`s were invisible.
|
|
32
|
+
*
|
|
33
|
+
* Fix: walk ancestor scopes (Block / CaseClause / DefaultClause) from the
|
|
34
|
+
* identifier site outward and resolve the matching `const`'s initializer
|
|
35
|
+
* against the current `propsContext`. This is the universal version of the
|
|
36
|
+
* pattern — applies to any local-const-derived className, not just pill
|
|
37
|
+
* colors.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
interface JsxNodeLike {
|
|
41
|
+
type: 'element' | 'text';
|
|
42
|
+
tagName?: string;
|
|
43
|
+
content?: string;
|
|
44
|
+
props?: Record<string, unknown>;
|
|
45
|
+
children?: JsxNodeLike[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface TestScannerView {
|
|
49
|
+
project: import('ts-morph').Project;
|
|
50
|
+
extractComponentJsxTree: (
|
|
51
|
+
sourceFile: import('ts-morph').SourceFile,
|
|
52
|
+
componentName: string,
|
|
53
|
+
) => JsxNodeLike | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeScanner(): TestScannerView {
|
|
57
|
+
return new ComponentScanner({
|
|
58
|
+
componentPaths: [],
|
|
59
|
+
filePattern: '*.tsx',
|
|
60
|
+
exclude: [],
|
|
61
|
+
}) as unknown as TestScannerView;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fixturePath(relative: string): string {
|
|
65
|
+
return path.resolve(process.cwd(), 'tools/figma-plugin/scanner/__fixtures__', relative);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findElement(node: JsxNodeLike | null, tag: string): JsxNodeLike | null {
|
|
69
|
+
if (!node || node.type !== 'element') return null;
|
|
70
|
+
if (node.tagName === tag) return node;
|
|
71
|
+
for (const child of node.children || []) {
|
|
72
|
+
const found = findElement(child, tag);
|
|
73
|
+
if (found) return found;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function classList(node: JsxNodeLike | null): string[] {
|
|
79
|
+
const cls = node?.props?.className;
|
|
80
|
+
if (typeof cls !== 'string') return [];
|
|
81
|
+
return cls.split(/\s+/).filter(Boolean);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const scanner = makeScanner();
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Case 1: the canonical StatusPill shape. A wrapper component renders a
|
|
88
|
+
// local-defined inner component with a literal prop value. The inner has a
|
|
89
|
+
// local `const dotClass = sourceLabel === "..." ? "class" : ...` whose
|
|
90
|
+
// resolved class must appear in the rendered `<span>`'s className.
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
{
|
|
93
|
+
const file = scanner.project.createSourceFile(
|
|
94
|
+
fixturePath('local-const-classname-status-pill.tsx'),
|
|
95
|
+
`
|
|
96
|
+
const cn = (...args: any[]) => args.filter(Boolean).join(' ');
|
|
97
|
+
|
|
98
|
+
const StatusPill = ({ sourceLabel }: { sourceLabel: "db" | "live" | "inactive" }) => {
|
|
99
|
+
const dotClass =
|
|
100
|
+
sourceLabel === "db"
|
|
101
|
+
? "bg-emerald-500"
|
|
102
|
+
: sourceLabel === "live"
|
|
103
|
+
? "bg-sky-400"
|
|
104
|
+
: "bg-muted-foreground";
|
|
105
|
+
return <span data-slot="dot" className={cn("h-2 w-2 rounded-full", dotClass)} />;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export function Card() {
|
|
109
|
+
return <StatusPill sourceLabel="live" />;
|
|
110
|
+
}
|
|
111
|
+
`,
|
|
112
|
+
{ overwrite: true },
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const tree = scanner.extractComponentJsxTree(file, 'Card');
|
|
116
|
+
assert.ok(tree, 'Card must produce a tree');
|
|
117
|
+
const dot = findElement(tree, 'span');
|
|
118
|
+
assert.ok(dot, 'must find the rendered <span>');
|
|
119
|
+
const classes = classList(dot);
|
|
120
|
+
assert.ok(
|
|
121
|
+
classes.includes('bg-sky-400'),
|
|
122
|
+
`dot must inherit bg-sky-400 from the "live" branch of the local-const ternary; got: ${classes.join(' ')}`,
|
|
123
|
+
);
|
|
124
|
+
assert.ok(classes.includes('h-2'), 'base utilities must survive cn() resolution');
|
|
125
|
+
assert.ok(classes.includes('rounded-full'), 'base utilities must survive cn() resolution');
|
|
126
|
+
// The non-chosen branches must NOT appear.
|
|
127
|
+
assert.ok(!classes.includes('bg-emerald-500'), 'losing branch "db" must not appear');
|
|
128
|
+
assert.ok(!classes.includes('bg-muted-foreground'), 'losing branch fallback must not appear');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Case 2: literal pass-through with a different prop name. Verifies the
|
|
133
|
+
// resolver picks the right branch when a wrapper forwards a literal value
|
|
134
|
+
// (the StatusPill shape but a different name, to catch regressions tied to
|
|
135
|
+
// hard-coded identifier matching).
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
{
|
|
138
|
+
const file = scanner.project.createSourceFile(
|
|
139
|
+
fixturePath('local-const-classname-passthrough.tsx'),
|
|
140
|
+
`
|
|
141
|
+
const cn = (...args: any[]) => args.filter(Boolean).join(' ');
|
|
142
|
+
|
|
143
|
+
const StatusPill = ({ tone }: { tone: "ok" | "warn" }) => {
|
|
144
|
+
const dotClass = tone === "warn" ? "bg-amber-500" : "bg-emerald-500";
|
|
145
|
+
return <span className={cn("dot", dotClass)} />;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export function Card() {
|
|
149
|
+
return <StatusPill tone="warn" />;
|
|
150
|
+
}
|
|
151
|
+
`,
|
|
152
|
+
{ overwrite: true },
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const tree = scanner.extractComponentJsxTree(file, 'Card');
|
|
156
|
+
assert.ok(tree, 'Card must produce a tree');
|
|
157
|
+
const dot = findElement(tree, 'span');
|
|
158
|
+
assert.ok(dot, 'must find the rendered <span>');
|
|
159
|
+
const classes = classList(dot);
|
|
160
|
+
assert.ok(
|
|
161
|
+
classes.includes('bg-amber-500'),
|
|
162
|
+
`the "warn" branch must be selected; got: ${classes.join(' ')}`,
|
|
163
|
+
);
|
|
164
|
+
assert.ok(!classes.includes('bg-emerald-500'), 'the losing branch must not appear');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Case 3: forward-reference TDZ. A const declared AFTER the JSX reference
|
|
169
|
+
// site in the same scope MUST NOT be picked up — that would resolve a
|
|
170
|
+
// real TDZ error as if it succeeded, masking bugs.
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
{
|
|
173
|
+
const file = scanner.project.createSourceFile(
|
|
174
|
+
fixturePath('local-const-classname-tdz.tsx'),
|
|
175
|
+
`
|
|
176
|
+
const cn = (...args: any[]) => args.filter(Boolean).join(' ');
|
|
177
|
+
|
|
178
|
+
export function Card() {
|
|
179
|
+
// Reference comes BEFORE the declaration — JS would throw at runtime.
|
|
180
|
+
// The scanner must not silently "fix" it.
|
|
181
|
+
// @ts-ignore — intentional TDZ for the test.
|
|
182
|
+
const out = <span className={cn("base", lateClass)} />;
|
|
183
|
+
const lateClass = "should-not-appear";
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
`,
|
|
187
|
+
{ overwrite: true },
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const tree = scanner.extractComponentJsxTree(file, 'Card');
|
|
191
|
+
assert.ok(tree, 'Card must produce a tree');
|
|
192
|
+
const span = findElement(tree, 'span');
|
|
193
|
+
assert.ok(span, 'must find <span>');
|
|
194
|
+
const classes = classList(span);
|
|
195
|
+
assert.ok(
|
|
196
|
+
!classes.includes('should-not-appear'),
|
|
197
|
+
'forward-declared const must not be resolved (TDZ semantics)',
|
|
198
|
+
);
|
|
199
|
+
assert.ok(classes.includes('base'), 'literal class survives');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Case 4: cycle protection. `const A = B` and `const B = A` must not
|
|
204
|
+
// infinite-loop — both should resolve to undefined and the className
|
|
205
|
+
// should fall back to just the literal base.
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
{
|
|
208
|
+
const file = scanner.project.createSourceFile(
|
|
209
|
+
fixturePath('local-const-classname-cycle.tsx'),
|
|
210
|
+
`
|
|
211
|
+
const cn = (...args: any[]) => args.filter(Boolean).join(' ');
|
|
212
|
+
|
|
213
|
+
export function Card() {
|
|
214
|
+
const A = B;
|
|
215
|
+
const B = A;
|
|
216
|
+
return <span className={cn("base", A)} />;
|
|
217
|
+
}
|
|
218
|
+
`,
|
|
219
|
+
{ overwrite: true },
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const tree = scanner.extractComponentJsxTree(file, 'Card');
|
|
223
|
+
assert.ok(tree, 'Card must produce a tree');
|
|
224
|
+
const span = findElement(tree, 'span');
|
|
225
|
+
assert.ok(span, 'must find <span>');
|
|
226
|
+
const classes = classList(span);
|
|
227
|
+
assert.ok(classes.includes('base'), 'literal class survives cycle protection');
|
|
228
|
+
// A and B are mutually recursive — neither should resolve to a literal
|
|
229
|
+
// string. The cn() call should silently drop the unresolved arg.
|
|
230
|
+
assert.equal(classes.length, 1, `only the literal "base" should remain; got: ${classes.join(' ')}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Case 5: scope-locality. Two functions in the same file both have a local
|
|
235
|
+
// `const colorClass = ...` with different values. The resolver must pick
|
|
236
|
+
// the one in the enclosing scope, not bleed across functions.
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
{
|
|
239
|
+
const file = scanner.project.createSourceFile(
|
|
240
|
+
fixturePath('local-const-classname-scope.tsx'),
|
|
241
|
+
`
|
|
242
|
+
const cn = (...args: any[]) => args.filter(Boolean).join(' ');
|
|
243
|
+
|
|
244
|
+
const Inner = () => {
|
|
245
|
+
const colorClass = "text-sky-500";
|
|
246
|
+
return <span data-which="inner" className={cn("a", colorClass)} />;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export function Card() {
|
|
250
|
+
const colorClass = "text-rose-500";
|
|
251
|
+
return (
|
|
252
|
+
<div>
|
|
253
|
+
<span data-which="outer" className={cn("b", colorClass)} />
|
|
254
|
+
<Inner />
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
`,
|
|
259
|
+
{ overwrite: true },
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const tree = scanner.extractComponentJsxTree(file, 'Card');
|
|
263
|
+
assert.ok(tree, 'Card must produce a tree');
|
|
264
|
+
|
|
265
|
+
// Find the two <span>s by their data-which marker.
|
|
266
|
+
function findByMarker(node: JsxNodeLike | null, value: string): JsxNodeLike | null {
|
|
267
|
+
if (!node || node.type !== 'element') return null;
|
|
268
|
+
if (node.props?.['data-which'] === value) return node;
|
|
269
|
+
for (const child of node.children || []) {
|
|
270
|
+
const found = findByMarker(child, value);
|
|
271
|
+
if (found) return found;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const outer = findByMarker(tree, 'outer');
|
|
276
|
+
const inner = findByMarker(tree, 'inner');
|
|
277
|
+
assert.ok(outer, 'outer <span> must be present');
|
|
278
|
+
assert.ok(inner, 'inner <span> must be present');
|
|
279
|
+
|
|
280
|
+
const outerClasses = classList(outer);
|
|
281
|
+
const innerClasses = classList(inner);
|
|
282
|
+
assert.ok(outerClasses.includes('text-rose-500'), `outer scope wins for outer span; got: ${outerClasses.join(' ')}`);
|
|
283
|
+
assert.ok(!outerClasses.includes('text-sky-500'), 'inner-scope value must not bleed into outer span');
|
|
284
|
+
assert.ok(innerClasses.includes('text-sky-500'), `inner scope wins for inner span; got: ${innerClasses.join(' ')}`);
|
|
285
|
+
assert.ok(!innerClasses.includes('text-rose-500'), 'outer-scope value must not be picked when inner scope shadows');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Case 6: text-children expressions. `<span>{statusText}</span>` where
|
|
290
|
+
// `statusText` is a local-const ternary must render the resolved text.
|
|
291
|
+
// History: the original DataSourcesCard fix landed for className identifiers
|
|
292
|
+
// but the main `buildJsxTree` flow had a separate, narrower lookup for JSX
|
|
293
|
+
// text-children that only checked module-level vars — so the "Live - DB"
|
|
294
|
+
// label inside the StatusPill was missing even after the dot color worked.
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
{
|
|
297
|
+
const file = scanner.project.createSourceFile(
|
|
298
|
+
fixturePath('local-const-text-children.tsx'),
|
|
299
|
+
`
|
|
300
|
+
const cn = (...args: any[]) => args.filter(Boolean).join(' ');
|
|
301
|
+
|
|
302
|
+
const StatusPill = ({ sourceLabel }: { sourceLabel: "db" | "live" | "inactive" }) => {
|
|
303
|
+
const statusText =
|
|
304
|
+
sourceLabel === "db"
|
|
305
|
+
? "Live - DB"
|
|
306
|
+
: sourceLabel === "live"
|
|
307
|
+
? "Live - API"
|
|
308
|
+
: "Inactive";
|
|
309
|
+
return <span data-slot="label">{statusText}</span>;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export function Card() {
|
|
313
|
+
return <StatusPill sourceLabel="db" />;
|
|
314
|
+
}
|
|
315
|
+
`,
|
|
316
|
+
{ overwrite: true },
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const tree = scanner.extractComponentJsxTree(file, 'Card');
|
|
320
|
+
assert.ok(tree, 'Card must produce a tree');
|
|
321
|
+
const span = findElement(tree, 'span');
|
|
322
|
+
assert.ok(span, 'must find <span>');
|
|
323
|
+
const textChildren = (span.children || []).filter((c) => c.type === 'text');
|
|
324
|
+
const combined = textChildren.map((c) => c.content ?? '').join('');
|
|
325
|
+
assert.ok(
|
|
326
|
+
combined.includes('Live - DB'),
|
|
327
|
+
`text child must resolve to "Live - DB" via local-const lookup; got: ${JSON.stringify(textChildren)}`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
console.log('local-const-className-regression: PASS (6 cases)');
|