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.
Files changed (178) hide show
  1. package/README.md +108 -25
  2. package/bin/inkbridge.mjs +354 -83
  3. package/code.js +40 -11802
  4. package/manifest.json +1 -0
  5. package/package.json +74 -23
  6. package/scanner/adapter-utils-regression.ts +159 -0
  7. package/scanner/aspect-percent-position-regression.ts +237 -0
  8. package/scanner/aspect-ratio-regression.ts +90 -0
  9. package/scanner/blob-placement-regression.ts +2 -2
  10. package/scanner/block-cache-regression.ts +195 -0
  11. package/scanner/bundle-size-regression.ts +50 -0
  12. package/scanner/child-sizing-matrix-regression.ts +303 -0
  13. package/scanner/cli.ts +342 -13
  14. package/scanner/component-scanner.ts +2108 -174
  15. package/scanner/component-sections-regression.ts +198 -0
  16. package/scanner/compound-classes-lookup-regression.ts +163 -0
  17. package/scanner/css-token-reader-regression.ts +7 -6
  18. package/scanner/css-token-reader.ts +152 -31
  19. package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
  20. package/scanner/cva-master-icon-regression.ts +315 -0
  21. package/scanner/data-attr-prop-alias-regression.ts +129 -0
  22. package/scanner/explicit-size-root-regression.ts +102 -0
  23. package/scanner/font-family-extract-regression.ts +113 -0
  24. package/scanner/font-style-resolver-regression.ts +1 -1
  25. package/scanner/framework-adapter-shadcn-regression.ts +480 -0
  26. package/scanner/full-width-matrix-regression.ts +338 -0
  27. package/scanner/grid-cols-extraction-regression.ts +110 -0
  28. package/scanner/image-src-collector-regression.ts +204 -0
  29. package/scanner/inline-flex-regression.ts +235 -0
  30. package/scanner/input-range-regression.ts +217 -0
  31. package/scanner/instance-rendering-regression.ts +224 -0
  32. package/scanner/jsx-prop-unresolved-regression.ts +178 -0
  33. package/scanner/jsx-text-regression.ts +178 -0
  34. package/scanner/layout-alignment-regression.ts +108 -0
  35. package/scanner/layout-flex-regression.ts +90 -0
  36. package/scanner/layout-mode-regression.ts +71 -0
  37. package/scanner/layout-sizing-regression.ts +227 -0
  38. package/scanner/layout-spacing-regression.ts +135 -0
  39. package/scanner/local-const-className-regression.ts +331 -0
  40. package/scanner/percent-position-regression.ts +105 -0
  41. package/scanner/provider-cascade-regression.ts +224 -0
  42. package/scanner/provider-flatten-regression.ts +235 -0
  43. package/scanner/radial-gradient-regression.ts +1 -1
  44. package/scanner/render-prop-parser-regression.ts +161 -0
  45. package/scanner/ring-utility-regression.ts +153 -0
  46. package/scanner/sandbox-spread-regression.ts +125 -0
  47. package/scanner/selection-pressed-regression.ts +241 -0
  48. package/scanner/size-full-normalization-regression.ts +127 -0
  49. package/scanner/state-classification-regression.ts +175 -0
  50. package/scanner/story-diagnostics-regression.ts +216 -0
  51. package/scanner/story-dimensioning-regression.ts +298 -0
  52. package/scanner/story-render-strategy-regression.ts +205 -0
  53. package/scanner/stretch-to-parent-width-regression.ts +147 -0
  54. package/scanner/svg-fill-parent-regression.ts +98 -0
  55. package/scanner/svg-group-inheritance-regression.ts +166 -0
  56. package/scanner/svg-marker-inline-regression.ts +211 -0
  57. package/scanner/svg-marker-regression.ts +116 -0
  58. package/scanner/tailwind-parser.ts +46 -4
  59. package/scanner/text-resize-matrix-regression.ts +173 -0
  60. package/scanner/transform-math-regression.ts +1 -1
  61. package/scanner/types.ts +26 -2
  62. package/src/cache/frame-cache.ts +150 -0
  63. package/src/cache/index.ts +2 -0
  64. package/src/{component-defs.ts → components/component-defs.ts} +25 -10
  65. package/src/{component-gen.ts → components/component-gen.ts} +43 -116
  66. package/src/components/component-instance.ts +386 -0
  67. package/src/components/component-library.ts +44 -0
  68. package/src/components/component-lookup.ts +161 -0
  69. package/src/components/index.ts +7 -0
  70. package/src/components/scanner-types.ts +39 -0
  71. package/src/components/symbol-instance-policy.ts +312 -0
  72. package/src/design-system/block-cache.ts +130 -0
  73. package/src/design-system/component-sections.ts +107 -0
  74. package/src/design-system/cva-inference.ts +187 -0
  75. package/src/design-system/cva-master.ts +427 -0
  76. package/src/design-system/cva-utils.ts +29 -0
  77. package/src/design-system/design-system.ts +334 -0
  78. package/src/design-system/frame-stabilizers.ts +191 -0
  79. package/src/design-system/frame-utils.ts +46 -0
  80. package/src/design-system/generated-node.ts +84 -0
  81. package/src/design-system/icon-rendering.ts +229 -0
  82. package/src/design-system/index.ts +13 -0
  83. package/src/design-system/instance-rendering.ts +307 -0
  84. package/src/design-system/master-shared.ts +133 -0
  85. package/src/design-system/node-helpers.ts +237 -0
  86. package/src/design-system/node-variants.ts +196 -0
  87. package/src/design-system/non-cva-master.ts +104 -0
  88. package/src/design-system/portal-handling.ts +138 -0
  89. package/src/design-system/preview-builder.ts +738 -0
  90. package/src/{render-context.ts → design-system/render-context.ts} +32 -6
  91. package/src/design-system/render-prop-parser.ts +50 -0
  92. package/src/design-system/responsive-resolver.ts +180 -0
  93. package/src/design-system/selectable-state.ts +157 -0
  94. package/src/design-system/state-master.ts +267 -0
  95. package/src/design-system/state-utils.ts +15 -0
  96. package/src/design-system/story-builder-context.ts +40 -0
  97. package/src/design-system/story-builder.ts +1322 -0
  98. package/src/design-system/story-diagnostics.ts +80 -0
  99. package/src/design-system/story-dimensioning.ts +272 -0
  100. package/src/design-system/story-frames.ts +400 -0
  101. package/src/design-system/story-instance.ts +333 -0
  102. package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
  103. package/src/design-system/story-render-strategy.ts +150 -0
  104. package/src/design-system/story-tree-search.ts +110 -0
  105. package/src/design-system/symbol-fallback.ts +89 -0
  106. package/src/design-system/symbol-source.ts +172 -0
  107. package/src/design-system/table-helpers.ts +56 -0
  108. package/src/design-system/tag-predicates.ts +99 -0
  109. package/src/design-system/theme-context.ts +52 -0
  110. package/src/design-system/typography.ts +100 -0
  111. package/src/design-system/ui-builder.ts +2676 -0
  112. package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
  113. package/src/effects/icon-builder.ts +1074 -0
  114. package/src/effects/index.ts +5 -0
  115. package/src/effects/portal-panel.ts +369 -0
  116. package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
  117. package/src/framework-adapters/index.ts +47 -0
  118. package/src/framework-adapters/shadcn.ts +541 -0
  119. package/src/{github.ts → github/github.ts} +46 -21
  120. package/src/github/index.ts +1 -0
  121. package/src/layout/deferred-layout.ts +1556 -0
  122. package/src/layout/index.ts +24 -0
  123. package/src/layout/layout-parser.ts +375 -0
  124. package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
  125. package/src/layout/parser/alignment.ts +54 -0
  126. package/src/layout/parser/flex.ts +59 -0
  127. package/src/layout/parser/index.ts +65 -0
  128. package/src/layout/parser/ir.ts +80 -0
  129. package/src/layout/parser/layout-mode.ts +57 -0
  130. package/src/layout/parser/sizing.ts +241 -0
  131. package/src/layout/parser/spacing-scale.ts +78 -0
  132. package/src/layout/parser/spacing.ts +134 -0
  133. package/src/layout/ring-utils.ts +120 -0
  134. package/src/layout/size-utils.ts +143 -0
  135. package/src/layout/text-resize-decision.ts +51 -0
  136. package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
  137. package/src/main.ts +444 -162
  138. package/src/{config.ts → plugin/config.ts} +12 -12
  139. package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
  140. package/src/plugin/image-src-collector.ts +52 -0
  141. package/src/plugin/index.ts +3 -0
  142. package/src/plugin/packs/index.ts +2 -0
  143. package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
  144. package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
  145. package/src/render-engine-version.ts +2 -0
  146. package/src/tailwind/adapter-utils.ts +137 -0
  147. package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
  148. package/src/tailwind/index.ts +8 -0
  149. package/src/tailwind/jsx-utils.ts +319 -0
  150. package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
  151. package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
  152. package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
  153. package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
  154. package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
  155. package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
  156. package/src/text/index.ts +4 -0
  157. package/src/{inline-text.ts → text/inline-text.ts} +13 -13
  158. package/src/{text-builder.ts → text/text-builder.ts} +24 -7
  159. package/src/{text-line.ts → text/text-line.ts} +2 -2
  160. package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
  161. package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
  162. package/src/{colors.ts → tokens/colors.ts} +13 -6
  163. package/src/tokens/index.ts +6 -0
  164. package/src/{token-source.ts → tokens/token-source.ts} +4 -1
  165. package/src/{tokens.ts → tokens/tokens.ts} +116 -20
  166. package/src/{variables.ts → tokens/variables.ts} +447 -102
  167. package/templates/patch-tokens-route.ts +25 -6
  168. package/templates/scan-components-route.ts +26 -5
  169. package/ui.html +485 -37
  170. package/src/component-lookup.ts +0 -82
  171. package/src/design-system.ts +0 -59
  172. package/src/icon-builder.ts +0 -607
  173. package/src/layout-parser.ts +0 -667
  174. package/src/story-builder.ts +0 -1706
  175. package/src/ui-builder.ts +0 -1996
  176. /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
  177. /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
  178. /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
@@ -0,0 +1,105 @@
1
+ import assert from 'node:assert/strict';
2
+
3
+ (globalThis as unknown as { figma: unknown }).figma = {
4
+ notify: () => undefined,
5
+ showUI: () => undefined,
6
+ };
7
+
8
+ import {
9
+ applyDeferredPercentPositioning,
10
+ markPositionInfo,
11
+ } from '../src/layout/deferred-layout';
12
+
13
+ // Stub a SceneNode with just the bits the resolver inspects.
14
+ type StubChild = {
15
+ type: 'FRAME';
16
+ width: number;
17
+ height: number;
18
+ x: number;
19
+ y: number;
20
+ };
21
+
22
+ type StubParent = {
23
+ type: 'FRAME';
24
+ width: number;
25
+ height: number;
26
+ layoutMode: 'NONE' | 'VERTICAL' | 'HORIZONTAL';
27
+ children: StubChild[];
28
+ };
29
+
30
+ function child(width: number, height: number): StubChild {
31
+ return { type: 'FRAME', width, height, x: 0, y: 0 };
32
+ }
33
+
34
+ function parent(width: number, height: number, kids: StubChild[]): StubParent {
35
+ return { type: 'FRAME', width, height, layoutMode: 'NONE', children: kids };
36
+ }
37
+
38
+ function runRegression(): void {
39
+ // ---- left/top percent: positions on the parent's content box -----------
40
+ // Parent 600 × 360 (the round-trip diagram in viewBox units). Logo at
41
+ // left-[18.33%] top-[30.56%] should land at (109.98, 110.016) — i.e. very
42
+ // close to the (110, 110) Next.js node coords used in the section.
43
+ const p1 = parent(600, 360, [child(40, 40)]);
44
+ markPositionInfo(p1.children[0] as unknown as SceneNode, {
45
+ leftPercent: 0.1833,
46
+ topPercent: 0.3056,
47
+ });
48
+ applyDeferredPercentPositioning(p1 as unknown as FrameNode);
49
+ assert.equal(Math.round(p1.children[0].x), 110, 'left-[18.33%] of 600 ≈ 110');
50
+ assert.equal(Math.round(p1.children[0].y), 110, 'top-[30.56%] of 360 ≈ 110');
51
+
52
+ // ---- bottom/right percent: anchor child's bottom/right edge -----------
53
+ // Drop is 28×40, in a parent 600×100, with bottom-[70%]. CSS bottom: 70%
54
+ // means the child's BOTTOM edge sits 70% of parent height from the parent's
55
+ // bottom = 30% from top. So child.y = 100 - 40 - 100*0.7 = -10 (the drop
56
+ // overflows above the top edge — that's correct CSS, the parent has
57
+ // overflow:hidden in the original component).
58
+ const p2 = parent(600, 100, [child(28, 40)]);
59
+ markPositionInfo(p2.children[0] as unknown as SceneNode, {
60
+ leftPercent: 0.5,
61
+ bottomPercent: 0.7,
62
+ });
63
+ applyDeferredPercentPositioning(p2 as unknown as FrameNode);
64
+ assert.equal(p2.children[0].x, 300, 'left-[50%] of 600 = 300');
65
+ assert.equal(p2.children[0].y, 100 - 40 - 100 * 0.7, 'bottom-[70%]: y = ph - ch - ph*0.7');
66
+
67
+ // ---- collapsed parent: no resize, mark retained for retry --------------
68
+ const p3 = parent(0, 0, [child(20, 20)]);
69
+ markPositionInfo(p3.children[0] as unknown as SceneNode, {
70
+ leftPercent: 0.5,
71
+ topPercent: 0.5,
72
+ });
73
+ applyDeferredPercentPositioning(p3 as unknown as FrameNode);
74
+ assert.equal(p3.children[0].x, 0, 'collapsed parent: x untouched');
75
+ assert.equal(p3.children[0].y, 0, 'collapsed parent: y untouched');
76
+
77
+ // Retry once parent has dimensions: should now resolve.
78
+ p3.width = 200;
79
+ p3.height = 100;
80
+ applyDeferredPercentPositioning(p3 as unknown as FrameNode);
81
+ assert.equal(p3.children[0].x, 100, 'retry: left-[50%] of 200 = 100');
82
+ assert.equal(p3.children[0].y, 50, 'retry: top-[50%] of 100 = 50');
83
+
84
+ // ---- mixed pixel + percent: pixel takes precedence on its axis ---------
85
+ // (Matches CSS where `top: 50px` and `top: 30%` would both target `top`,
86
+ // and the cascade picks one. For our marks, pixel `top` is consumed by
87
+ // applyAbsoluteIfPossible first; only percent fields reach this resolver.
88
+ // Verify the resolver doesn't overwrite a pre-set y when only LEFT
89
+ // percent is provided.)
90
+ const p4 = parent(400, 200, [child(20, 20)]);
91
+ p4.children[0].y = 42;
92
+ markPositionInfo(p4.children[0] as unknown as SceneNode, { leftPercent: 0.25 });
93
+ applyDeferredPercentPositioning(p4 as unknown as FrameNode);
94
+ assert.equal(p4.children[0].x, 100, 'leftPercent applied');
95
+ assert.equal(p4.children[0].y, 42, 'y untouched when no top/bottom percent');
96
+ }
97
+
98
+ try {
99
+ runRegression();
100
+ console.log('percent-position-regression: PASS');
101
+ } catch (err) {
102
+ console.error('percent-position-regression: FAIL');
103
+ console.error(err);
104
+ process.exit(1);
105
+ }
@@ -0,0 +1,224 @@
1
+ import assert from 'node:assert/strict';
2
+ import * as path from 'node:path';
3
+ import { ComponentScanner } from './component-scanner';
4
+
5
+ /**
6
+ * Regression: React Context cascade detection in the scanner.
7
+ *
8
+ * Background: shadcn-style compound components (ToggleGroup → ToggleGroupItem,
9
+ * RadioGroup → RadioGroupItem, etc.) cascade variant/size props from the
10
+ * group to each item via React.Context.Provider:
11
+ *
12
+ * function ToggleGroup({ variant, size, children }) {
13
+ * return (
14
+ * <ToggleGroupPrimitive>
15
+ * <ToggleGroupContext.Provider value={{ variant, size }}>
16
+ * {children}
17
+ * </ToggleGroupContext.Provider>
18
+ * </ToggleGroupPrimitive>
19
+ * );
20
+ * }
21
+ * function ToggleGroupItem({ variant, size, ...props }) {
22
+ * const ctx = React.useContext(ToggleGroupContext);
23
+ * return <Toggle variant={variant ?? ctx.variant ?? "default"} ... />;
24
+ * }
25
+ *
26
+ * The scanner is a static analyzer — it can't run `useContext`. Before
27
+ * this fix, every ToggleGroupItem invocation resolved with `variant=undefined`
28
+ * regardless of what the wrapping ToggleGroup passed, so the rendered
29
+ * Toggle always picked the CVA default ("default" variant). Result:
30
+ * `<ToggleGroup variant="outline">` rendered as `default` everywhere.
31
+ *
32
+ * Fix: detect `<*.Provider value={{ ... }}>` wrappers in component bodies,
33
+ * extract the value object's key names, and at invocation time push the
34
+ * invocation's matching props as defaults onto a "provider stack" that
35
+ * descendant invocations consult. Statically models the runtime cascade.
36
+ */
37
+
38
+ interface JsxTextNode { type: 'text'; content: string }
39
+ interface JsxElementNode {
40
+ type: 'element';
41
+ tagName?: string;
42
+ props?: Record<string, string>;
43
+ children?: JsxNodeLike[];
44
+ }
45
+ type JsxNodeLike = JsxTextNode | JsxElementNode;
46
+
47
+ interface TestScannerView {
48
+ project: import('ts-morph').Project;
49
+ extractComponentJsxTree: (
50
+ sourceFile: import('ts-morph').SourceFile,
51
+ componentName: string
52
+ ) => JsxNodeLike | null;
53
+ }
54
+
55
+ function makeScanner(): TestScannerView {
56
+ const scanner = new ComponentScanner({
57
+ componentPaths: [],
58
+ filePattern: '*.tsx',
59
+ exclude: [],
60
+ });
61
+ return scanner 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 findElementsByTag(
69
+ node: JsxNodeLike | null | undefined,
70
+ tagName: string,
71
+ out: JsxElementNode[] = []
72
+ ): JsxElementNode[] {
73
+ if (!node || node.type !== 'element') return out;
74
+ if (node.tagName === tagName) out.push(node);
75
+ if (node.children) for (const c of node.children) findElementsByTag(c, tagName, out);
76
+ return out;
77
+ }
78
+
79
+ const scanner = makeScanner();
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Case 1: ToggleGroup-style component cascades variant + size through a
83
+ // Context.Provider value object with shorthand properties.
84
+ // ---------------------------------------------------------------------------
85
+ {
86
+ // The fixture defines a minimal ToggleGroup that wraps children in a
87
+ // Provider, and the story passes variant + size at the group level.
88
+ // Each <Item> invocation has no explicit variant/size — they should be
89
+ // injected by the cascade.
90
+ const file = scanner.project.createSourceFile(
91
+ fixturePath('toggle-group-style.tsx'),
92
+ `
93
+ import * as React from 'react';
94
+ const CtxA = React.createContext({ variant: 'default', size: 'default' });
95
+ function Group({ variant = 'default', size = 'default', children }: {
96
+ variant?: string;
97
+ size?: string;
98
+ children?: React.ReactNode;
99
+ }) {
100
+ return (
101
+ <div>
102
+ <CtxA.Provider value={{ variant, size }}>
103
+ {children}
104
+ </CtxA.Provider>
105
+ </div>
106
+ );
107
+ }
108
+ function Item({ variant, size, value }: { variant?: string; size?: string; value: string }) {
109
+ return <button data-variant={variant ?? 'default'} data-size={size ?? 'default'} data-value={value} />;
110
+ }
111
+ function Story() {
112
+ return (
113
+ <Group variant="outline" size="lg">
114
+ <Item value="a" />
115
+ <Item value="b" />
116
+ </Group>
117
+ );
118
+ }
119
+ export { Story };
120
+ `,
121
+ { overwrite: true }
122
+ );
123
+
124
+ const tree = scanner.extractComponentJsxTree(file, 'Story');
125
+ assert.ok(tree && tree.type === 'element', 'Story tree must build');
126
+
127
+ const buttons = findElementsByTag(tree, 'button');
128
+ assert.equal(buttons.length, 2, `expected 2 buttons after expansion, got ${buttons.length}`);
129
+ for (let i = 0; i < buttons.length; i++) {
130
+ const b = buttons[i];
131
+ assert.equal(
132
+ b.props?.['data-variant'],
133
+ 'outline',
134
+ `button ${i}: variant must cascade ("outline"), got ${b.props?.['data-variant']}`,
135
+ );
136
+ assert.equal(
137
+ b.props?.['data-size'],
138
+ 'lg',
139
+ `button ${i}: size must cascade ("lg"), got ${b.props?.['data-size']}`,
140
+ );
141
+ }
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Case 2: an Item that explicitly overrides one of the cascaded props
146
+ // keeps its explicit value — cascade only fills MISSING props.
147
+ // ---------------------------------------------------------------------------
148
+ {
149
+ const file = scanner.project.createSourceFile(
150
+ fixturePath('toggle-group-explicit-override.tsx'),
151
+ `
152
+ import * as React from 'react';
153
+ const CtxB = React.createContext({ variant: 'default' });
154
+ function Group({ variant = 'default', children }: { variant?: string; children?: React.ReactNode }) {
155
+ return (
156
+ <CtxB.Provider value={{ variant }}>
157
+ {children}
158
+ </CtxB.Provider>
159
+ );
160
+ }
161
+ function Item({ variant }: { variant?: string }) {
162
+ return <span data-variant={variant ?? 'default'} />;
163
+ }
164
+ function Story() {
165
+ return (
166
+ <Group variant="outline">
167
+ <Item />
168
+ <Item variant="ghost" />
169
+ </Group>
170
+ );
171
+ }
172
+ export { Story };
173
+ `,
174
+ { overwrite: true }
175
+ );
176
+
177
+ const tree = scanner.extractComponentJsxTree(file, 'Story');
178
+ assert.ok(tree && tree.type === 'element', 'Story tree must build');
179
+ const spans = findElementsByTag(tree, 'span');
180
+ assert.equal(spans.length, 2, `expected 2 spans, got ${spans.length}`);
181
+ assert.equal(spans[0].props?.['data-variant'], 'outline', 'first span inherits "outline" from group');
182
+ assert.equal(spans[1].props?.['data-variant'], 'ghost', 'second span keeps explicit "ghost"');
183
+ }
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // Case 3: a component with NO Provider wrapper doesn't push a cascade,
187
+ // so descendants resolve as they would without the feature.
188
+ // ---------------------------------------------------------------------------
189
+ {
190
+ const file = scanner.project.createSourceFile(
191
+ fixturePath('no-provider.tsx'),
192
+ `
193
+ import * as React from 'react';
194
+ function Group({ variant = 'default', children }: { variant?: string; children?: React.ReactNode }) {
195
+ // Just a plain wrapper — no Provider — so variant does NOT cascade.
196
+ return <section data-variant={variant}>{children}</section>;
197
+ }
198
+ function Item({ variant }: { variant?: string }) {
199
+ return <p data-variant={variant ?? 'fallback'} />;
200
+ }
201
+ function Story() {
202
+ return (
203
+ <Group variant="outline">
204
+ <Item />
205
+ </Group>
206
+ );
207
+ }
208
+ export { Story };
209
+ `,
210
+ { overwrite: true }
211
+ );
212
+
213
+ const tree = scanner.extractComponentJsxTree(file, 'Story');
214
+ assert.ok(tree && tree.type === 'element', 'Story tree must build');
215
+ const ps = findElementsByTag(tree, 'p');
216
+ assert.equal(ps.length, 1, `expected 1 p, got ${ps.length}`);
217
+ assert.equal(
218
+ ps[0].props?.['data-variant'],
219
+ 'fallback',
220
+ 'without Provider wrapper, no cascade — Item falls back to its own default',
221
+ );
222
+ }
223
+
224
+ console.log('provider-cascade-regression: PASS (3 cases)');
@@ -0,0 +1,235 @@
1
+ import assert from 'node:assert/strict';
2
+ import * as path from 'node:path';
3
+ import { ComponentScanner } from './component-scanner';
4
+
5
+ /**
6
+ * Regression: a React Context Provider that wraps MULTIPLE children
7
+ * (typical pattern for compound components that cascade variant/size:
8
+ * `<ToggleGroup>` wraps items in `<ToggleGroupContext.Provider>`) must
9
+ * be transparently stripped from the JSX tree. Otherwise the renderer
10
+ * builds an unsized inner frame for the Provider, breaking the parent's
11
+ * auto-layout (e.g. ToggleGroup's `inline-flex` no longer applies to
12
+ * the actual items).
13
+ *
14
+ * History: scanner had a single-child Provider stripper at
15
+ * `buildJsxTree`'s return point (line ~1454) but no multi-child case.
16
+ * When a starter consumer added ToggleGroup with 3 toggle items, all
17
+ * 3 stacked vertically because the Provider barrier prevented inline-flex
18
+ * from reaching them as direct children. Universal fix in the scanner
19
+ * because the pattern (cascading context Providers in compound components)
20
+ * is canonical in shadcn-style libraries — not specific to this consumer.
21
+ */
22
+
23
+ interface JsxTextNode { type: 'text'; content: string }
24
+ interface JsxElementNode {
25
+ type: 'element';
26
+ tagName?: string;
27
+ props?: Record<string, string>;
28
+ children?: JsxNodeLike[];
29
+ }
30
+ type JsxNodeLike = JsxTextNode | JsxElementNode;
31
+
32
+ interface TestScannerView {
33
+ project: import('ts-morph').Project;
34
+ extractComponentJsxTree: (
35
+ sourceFile: import('ts-morph').SourceFile,
36
+ componentName: string
37
+ ) => JsxNodeLike | null;
38
+ }
39
+
40
+ function makeScanner(): TestScannerView {
41
+ const scanner = new ComponentScanner({
42
+ componentPaths: [],
43
+ filePattern: '*.tsx',
44
+ exclude: [],
45
+ });
46
+ return scanner as unknown as TestScannerView;
47
+ }
48
+
49
+ function fixturePath(relative: string): string {
50
+ return path.resolve(process.cwd(), 'tools/figma-plugin/scanner/__fixtures__', relative);
51
+ }
52
+
53
+ function findElementsByTag(
54
+ node: JsxNodeLike | null | undefined,
55
+ tagName: string,
56
+ out: JsxElementNode[] = []
57
+ ): JsxElementNode[] {
58
+ if (!node || node.type !== 'element') return out;
59
+ if (node.tagName === tagName) out.push(node);
60
+ if (node.children) {
61
+ for (const c of node.children) findElementsByTag(c, tagName, out);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ const scanner = makeScanner();
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Case 1: multi-child Context.Provider inside a compound component body
70
+ // must be flattened — its 3 children become direct children of the
71
+ // enclosing root.
72
+ // ---------------------------------------------------------------------------
73
+ {
74
+ const file = scanner.project.createSourceFile(
75
+ fixturePath('multi-child-provider.tsx'),
76
+ `
77
+ import * as React from 'react';
78
+ const ToggleGroupContext = React.createContext({ size: 'default' });
79
+ function ToggleGroup({ children }: { children?: React.ReactNode }) {
80
+ return (
81
+ <div className="inline-flex items-center gap-1">
82
+ <ToggleGroupContext.Provider value={{ size: 'default' }}>
83
+ <button>A</button>
84
+ <button>B</button>
85
+ <button>C</button>
86
+ </ToggleGroupContext.Provider>
87
+ </div>
88
+ );
89
+ }
90
+ export { ToggleGroup };
91
+ `,
92
+ { overwrite: true }
93
+ );
94
+
95
+ const tree = scanner.extractComponentJsxTree(file, 'ToggleGroup');
96
+ assert.ok(tree && tree.type === 'element', 'ToggleGroup tree must build');
97
+
98
+ const providers = findElementsByTag(tree, 'ToggleGroupContext.Provider');
99
+ assert.equal(
100
+ providers.length,
101
+ 0,
102
+ `transparent multi-child Provider must be flattened, but found ${providers.length} Provider node(s) in tree`
103
+ );
104
+
105
+ // After flattening the Provider, the inline-flex div should have the
106
+ // 3 buttons as direct children.
107
+ const root = tree as JsxElementNode;
108
+ assert.equal(root.tagName, 'div', `root should be the inline-flex div, got ${root.tagName}`);
109
+ const childTags = (root.children ?? [])
110
+ .filter((c): c is JsxElementNode => c.type === 'element')
111
+ .map((c) => c.tagName);
112
+ assert.deepEqual(
113
+ childTags,
114
+ ['button', 'button', 'button'],
115
+ `inline-flex div should have 3 button children after Provider flatten, got ${JSON.stringify(childTags)}`
116
+ );
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Case 2: NESTED transparent Providers (rare but the flatten must recurse).
121
+ // ---------------------------------------------------------------------------
122
+ {
123
+ const file = scanner.project.createSourceFile(
124
+ fixturePath('nested-providers.tsx'),
125
+ `
126
+ import * as React from 'react';
127
+ const A = React.createContext(null);
128
+ const B = React.createContext(null);
129
+ function Group({ children }: { children?: React.ReactNode }) {
130
+ return (
131
+ <ul className="grid">
132
+ <A.Provider value={null}>
133
+ <B.Provider value={null}>
134
+ <li>One</li>
135
+ <li>Two</li>
136
+ </B.Provider>
137
+ </A.Provider>
138
+ </ul>
139
+ );
140
+ }
141
+ export { Group };
142
+ `,
143
+ { overwrite: true }
144
+ );
145
+
146
+ const tree = scanner.extractComponentJsxTree(file, 'Group');
147
+ assert.ok(tree && tree.type === 'element', 'Group tree must build');
148
+ const providers = [
149
+ ...findElementsByTag(tree, 'A.Provider'),
150
+ ...findElementsByTag(tree, 'B.Provider'),
151
+ ];
152
+ assert.equal(providers.length, 0, 'nested transparent Providers must both be flattened');
153
+ const ul = tree as JsxElementNode;
154
+ assert.equal(ul.tagName, 'ul');
155
+ const itemTags = (ul.children ?? [])
156
+ .filter((c): c is JsxElementNode => c.type === 'element')
157
+ .map((c) => c.tagName);
158
+ assert.deepEqual(
159
+ itemTags,
160
+ ['li', 'li'],
161
+ `ul should have 2 li children after nested Provider flatten, got ${JSON.stringify(itemTags)}`
162
+ );
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Case 3: a Provider WITH className (rare; not actually possible on a real
167
+ // React Context.Provider, but tests the guard) must NOT be flattened.
168
+ // Asserts the guard so future refactors can't accidentally collapse a
169
+ // styled wrapper that happens to be named *.Provider.
170
+ // ---------------------------------------------------------------------------
171
+ {
172
+ const file = scanner.project.createSourceFile(
173
+ fixturePath('styled-provider.tsx'),
174
+ `
175
+ import * as React from 'react';
176
+ function Group() {
177
+ return (
178
+ <section>
179
+ <FooProvider className="rounded-md p-4">
180
+ <span>one</span>
181
+ <span>two</span>
182
+ </FooProvider>
183
+ </section>
184
+ );
185
+ }
186
+ export { Group };
187
+ `,
188
+ { overwrite: true }
189
+ );
190
+
191
+ const tree = scanner.extractComponentJsxTree(file, 'Group');
192
+ assert.ok(tree && tree.type === 'element', 'Group tree must build');
193
+ const providers = findElementsByTag(tree, 'FooProvider');
194
+ // FooProvider doesn't end with .Provider — it ends with "Provider". Should NOT be flattened
195
+ // by the .endsWith('.Provider') check. This asserts the dot-prefix discipline.
196
+ assert.equal(
197
+ providers.length,
198
+ 1,
199
+ `FooProvider (no dot) must NOT match transparent-Provider rule; got ${providers.length}`
200
+ );
201
+ }
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // Case 4: single-child Provider — was already handled before this fix.
205
+ // Ensure the existing behaviour still works (Provider stripped, child
206
+ // elevated).
207
+ // ---------------------------------------------------------------------------
208
+ {
209
+ const file = scanner.project.createSourceFile(
210
+ fixturePath('single-child-provider.tsx'),
211
+ `
212
+ import * as React from 'react';
213
+ const Ctx = React.createContext(null);
214
+ function Group() {
215
+ return (
216
+ <Ctx.Provider value={null}>
217
+ <article className="card">hello</article>
218
+ </Ctx.Provider>
219
+ );
220
+ }
221
+ export { Group };
222
+ `,
223
+ { overwrite: true }
224
+ );
225
+
226
+ const tree = scanner.extractComponentJsxTree(file, 'Group');
227
+ assert.ok(tree && tree.type === 'element', 'Group tree must build');
228
+ assert.equal(
229
+ (tree as JsxElementNode).tagName,
230
+ 'article',
231
+ `single-child Provider must be stripped, leaving the article as root; got ${(tree as JsxElementNode).tagName}`
232
+ );
233
+ }
234
+
235
+ console.log('provider-flatten-regression: PASS (4 cases)');
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
- import { parseRadialAnchorFromUtility, radialGradientTransformFromAnchor } from '../src/radial-gradient';
2
+ import { parseRadialAnchorFromUtility, radialGradientTransformFromAnchor } from '../src/effects/radial-gradient';
3
3
 
4
4
  function approxEqual(actual: number, expected: number, epsilon = 0.0001): void {
5
5
  assert.ok(Math.abs(actual - expected) <= epsilon, `expected ${expected}, got ${actual}`);