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,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}`);
|