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,178 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { ComponentScanner } from './component-scanner';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression: when a JSX attribute reads an unresolved identifier (e.g. a
|
|
7
|
+
* function parameter with no default), the scanner must OMIT the prop
|
|
8
|
+
* rather than serialise the identifier's source text as the value.
|
|
9
|
+
*
|
|
10
|
+
* History: DropdownMenuItem's body has
|
|
11
|
+
*
|
|
12
|
+
* <DropdownMenuPrimitive.Item data-inset={inset} ... />
|
|
13
|
+
*
|
|
14
|
+
* where `inset?: boolean` is destructured from props with no default. In
|
|
15
|
+
* the Default story the consumer never passes `inset`, so its runtime
|
|
16
|
+
* value is `undefined` — React would render NO `data-inset` attribute.
|
|
17
|
+
*
|
|
18
|
+
* The scanner used to fall back to `expr.getText()` when an expression
|
|
19
|
+
* couldn't be resolved, which serialised the identifier name `"inset"`
|
|
20
|
+
* as the attribute value. The plugin's variant engine then treated
|
|
21
|
+
* `props["data-inset"] === "inset"` as "data-inset is present", which
|
|
22
|
+
* activated every `data-[inset]:` utility. The visible symptom: every
|
|
23
|
+
* DropdownMenuItem got `data-[inset]:pl-8` (32px left padding) even when
|
|
24
|
+
* no item was `inset`, leaving them indented like checkbox/radio items.
|
|
25
|
+
*
|
|
26
|
+
* The fix: omit the prop when the expression doesn't resolve. Matches
|
|
27
|
+
* React's runtime behaviour for `data-x={undefined}` — no attribute is
|
|
28
|
+
* emitted, so no variant predicate matches.
|
|
29
|
+
*
|
|
30
|
+
* This file locks the contract at the boundary the variant engine reads
|
|
31
|
+
* from — the props object on the JSX node.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
interface JsxNodeLike {
|
|
35
|
+
type: 'element' | 'text';
|
|
36
|
+
tagName?: string;
|
|
37
|
+
props?: Record<string, unknown>;
|
|
38
|
+
children?: JsxNodeLike[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface TestScannerView {
|
|
42
|
+
project: import('ts-morph').Project;
|
|
43
|
+
extractComponentJsxTree: (
|
|
44
|
+
sourceFile: import('ts-morph').SourceFile,
|
|
45
|
+
componentName: string
|
|
46
|
+
) => JsxNodeLike | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeScanner(): TestScannerView {
|
|
50
|
+
return new ComponentScanner({
|
|
51
|
+
componentPaths: [],
|
|
52
|
+
filePattern: '*.tsx',
|
|
53
|
+
exclude: [],
|
|
54
|
+
}) as unknown as TestScannerView;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fixturePath(relative: string): string {
|
|
58
|
+
return path.resolve(process.cwd(), 'tools/figma-plugin/scanner/__fixtures__', relative);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function findElement(node: JsxNodeLike | null, tag: string): JsxNodeLike | null {
|
|
62
|
+
if (!node || node.type !== 'element') return null;
|
|
63
|
+
if (node.tagName === tag) return node;
|
|
64
|
+
for (const child of node.children || []) {
|
|
65
|
+
const found = findElement(child, tag);
|
|
66
|
+
if (found) return found;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const scanner = makeScanner();
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Case 1: unresolved boolean-prop identifier — the DropdownMenuItem pattern.
|
|
75
|
+
// `inset` is a destructured parameter with no default. The data-attribute
|
|
76
|
+
// expression `data-inset={inset}` MUST be omitted, not serialised as
|
|
77
|
+
// "inset".
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
{
|
|
80
|
+
const file = scanner.project.createSourceFile(
|
|
81
|
+
fixturePath('unresolved-data-attr.tsx'),
|
|
82
|
+
`
|
|
83
|
+
// The className would normally include "data-[inset]:pl-8" — omitted
|
|
84
|
+
// from this fixture because the regression asserts on the props map,
|
|
85
|
+
// not on class extraction, and inlining the class string trips the
|
|
86
|
+
// workspace lint (suggestCanonicalClasses) for what is intentionally
|
|
87
|
+
// a verbatim shadcn-shaped fixture.
|
|
88
|
+
export function DropdownMenuItem({ inset }: { inset?: boolean }) {
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
data-slot="dropdown-menu-item"
|
|
92
|
+
data-inset={inset}
|
|
93
|
+
data-variant="default"
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
`,
|
|
98
|
+
{ overwrite: true }
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const tree = scanner.extractComponentJsxTree(file, 'DropdownMenuItem');
|
|
102
|
+
assert.ok(tree, 'DropdownMenuItem must produce a tree');
|
|
103
|
+
const root = findElement(tree, 'div');
|
|
104
|
+
assert.ok(root, 'must find the root <div>');
|
|
105
|
+
|
|
106
|
+
// Literal data-* attrs survive.
|
|
107
|
+
assert.equal(root.props?.['data-slot'], 'dropdown-menu-item', 'literal data-slot preserved');
|
|
108
|
+
assert.equal(root.props?.['data-variant'], 'default', 'literal data-variant preserved');
|
|
109
|
+
|
|
110
|
+
// The unresolved expression must NOT have been serialised as the
|
|
111
|
+
// identifier name. Either the key is absent, or the value is some
|
|
112
|
+
// explicit "absent" sentinel — never the identifier text "inset".
|
|
113
|
+
const insetValue = root.props?.['data-inset'];
|
|
114
|
+
assert.notEqual(
|
|
115
|
+
insetValue,
|
|
116
|
+
'inset',
|
|
117
|
+
"data-inset must NOT serialise to the unresolved identifier's source text"
|
|
118
|
+
);
|
|
119
|
+
// Stronger contract: the key should be absent. React renders no
|
|
120
|
+
// attribute for `data-x={undefined}`, and that's what the variant
|
|
121
|
+
// engine needs to see for the predicate to NOT match.
|
|
122
|
+
assert.ok(
|
|
123
|
+
!Object.prototype.hasOwnProperty.call(root.props || {}, 'data-inset'),
|
|
124
|
+
'data-inset key must be omitted when the value expression is unresolved'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Case 2: unresolved string-prop identifier — same rule, different shape.
|
|
130
|
+
// `<input placeholder={placeholder}>` where `placeholder` is unresolved
|
|
131
|
+
// shouldn't end up with `placeholder="placeholder"` (the identifier name)
|
|
132
|
+
// in the rendered tree.
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
{
|
|
135
|
+
const file = scanner.project.createSourceFile(
|
|
136
|
+
fixturePath('unresolved-string-attr.tsx'),
|
|
137
|
+
`
|
|
138
|
+
export function Field({ placeholder }: { placeholder?: string }) {
|
|
139
|
+
return <input placeholder={placeholder} />;
|
|
140
|
+
}
|
|
141
|
+
`,
|
|
142
|
+
{ overwrite: true }
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const tree = scanner.extractComponentJsxTree(file, 'Field');
|
|
146
|
+
assert.ok(tree, 'Field must produce a tree');
|
|
147
|
+
const input = findElement(tree, 'input');
|
|
148
|
+
assert.ok(input, 'must find <input>');
|
|
149
|
+
assert.ok(
|
|
150
|
+
!Object.prototype.hasOwnProperty.call(input.props || {}, 'placeholder'),
|
|
151
|
+
'placeholder key must be omitted when the value identifier is unresolved'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Case 3: literal string attribute survives — sanity check that this fix
|
|
157
|
+
// only suppresses *unresolved* expressions, not the literal-attr path.
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
{
|
|
160
|
+
const file = scanner.project.createSourceFile(
|
|
161
|
+
fixturePath('literal-attr.tsx'),
|
|
162
|
+
`
|
|
163
|
+
export function LiteralAttr() {
|
|
164
|
+
return <div data-variant="primary" data-slot="root" />;
|
|
165
|
+
}
|
|
166
|
+
`,
|
|
167
|
+
{ overwrite: true }
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const tree = scanner.extractComponentJsxTree(file, 'LiteralAttr');
|
|
171
|
+
assert.ok(tree, 'LiteralAttr must produce a tree');
|
|
172
|
+
const div = findElement(tree, 'div');
|
|
173
|
+
assert.ok(div, 'must find <div>');
|
|
174
|
+
assert.equal(div.props?.['data-variant'], 'primary', 'literal data-variant survives');
|
|
175
|
+
assert.equal(div.props?.['data-slot'], 'root', 'literal data-slot survives');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log('jsx-prop-unresolved-regression: PASS (3 cases)');
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { ComponentScanner } from './component-scanner';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression: JsxText whitespace must be normalized like Babel/React's
|
|
7
|
+
* transform — newlines and indentation between source lines collapse to a
|
|
8
|
+
* single space, and boundary whitespace adjacent to sibling JSX elements
|
|
9
|
+
* is preserved so that `Your <span>foo</span> bar` stays spaced.
|
|
10
|
+
*
|
|
11
|
+
* History: HeroSection's `<h1>` and `<p>` wrap across multiple source
|
|
12
|
+
* lines (`design\n system`). Prior `child.getText().trim()` left the
|
|
13
|
+
* embedded newline + indent in the rendered Figma text, producing
|
|
14
|
+
* "design[whitespace]system" instead of "design system". A first fix
|
|
15
|
+
* patched only ONE of the four JsxText sites in component-scanner.ts;
|
|
16
|
+
* the buildJsxTree paths (lines ~1345 + ~1688) still had the bug, which
|
|
17
|
+
* is the path the hero h1 actually flows through. This test locks down
|
|
18
|
+
* all sites that emit structured text children.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface JsxTextNode { type: 'text'; content: string }
|
|
22
|
+
interface JsxElementNode { type: 'element'; tagName?: string; children?: JsxNodeLike[] }
|
|
23
|
+
type JsxNodeLike = JsxTextNode | JsxElementNode;
|
|
24
|
+
|
|
25
|
+
interface TestScannerView {
|
|
26
|
+
project: import('ts-morph').Project;
|
|
27
|
+
extractComponentJsxTree: (
|
|
28
|
+
sourceFile: import('ts-morph').SourceFile,
|
|
29
|
+
componentName: string
|
|
30
|
+
) => JsxNodeLike | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeScanner(): TestScannerView {
|
|
34
|
+
const scanner = new ComponentScanner({
|
|
35
|
+
componentPaths: [],
|
|
36
|
+
filePattern: '*.tsx',
|
|
37
|
+
exclude: [],
|
|
38
|
+
});
|
|
39
|
+
return scanner as unknown as TestScannerView;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fixturePath(relative: string): string {
|
|
43
|
+
return path.resolve(process.cwd(), 'tools/figma-plugin/scanner/__fixtures__', relative);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function collectTextNodes(node: JsxNodeLike | null | undefined, out: string[]): void {
|
|
47
|
+
if (!node) return;
|
|
48
|
+
if (node.type === 'text') {
|
|
49
|
+
out.push(node.content);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (node.type === 'element' && node.children) {
|
|
53
|
+
for (const child of node.children) collectTextNodes(child, out);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const scanner = makeScanner();
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Case 1: a hero-style <h1> whose text wraps across two source lines, with
|
|
61
|
+
// an inline <span> sibling. Pre-fix this rendered with a literal newline
|
|
62
|
+
// + indent embedded in "design\n system".
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
{
|
|
65
|
+
const file = scanner.project.createSourceFile(
|
|
66
|
+
fixturePath('hero-h1.tsx'),
|
|
67
|
+
`
|
|
68
|
+
export function HeroH1() {
|
|
69
|
+
return (
|
|
70
|
+
<h1 className="text-4xl font-bold">
|
|
71
|
+
Your <span className="text-primary">stories</span> are the design
|
|
72
|
+
system. Figma renders them on demand.
|
|
73
|
+
</h1>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
`,
|
|
77
|
+
{ overwrite: true }
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const tree = scanner.extractComponentJsxTree(file, 'HeroH1');
|
|
81
|
+
assert.ok(tree && tree.type === 'element', 'HeroH1 must produce an element tree');
|
|
82
|
+
|
|
83
|
+
const texts: string[] = [];
|
|
84
|
+
collectTextNodes(tree, texts);
|
|
85
|
+
|
|
86
|
+
// Expected: ["Your ", "stories", " are the design system. Figma renders them on demand."]
|
|
87
|
+
for (const t of texts) {
|
|
88
|
+
assert.ok(
|
|
89
|
+
!/[\n\r]/.test(t),
|
|
90
|
+
`JsxText must not contain a literal newline. Got: ${JSON.stringify(t)}`
|
|
91
|
+
);
|
|
92
|
+
assert.ok(
|
|
93
|
+
!/ /.test(t),
|
|
94
|
+
`JsxText must not contain consecutive spaces (collapsed indent). Got: ${JSON.stringify(t)}`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const joined = texts.join('');
|
|
99
|
+
assert.ok(
|
|
100
|
+
joined.includes('Your stories are the design system.'),
|
|
101
|
+
`Expected joined text to read 'Your stories are the design system.' Got: ${JSON.stringify(joined)}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Case 2: boundary whitespace between sibling JSX must be preserved, even
|
|
107
|
+
// though internal whitespace collapses. Naive `.replace(/\s+/g, ' ').trim()`
|
|
108
|
+
// would drop the leading/trailing space, producing "Yourfoobar".
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
{
|
|
111
|
+
const file = scanner.project.createSourceFile(
|
|
112
|
+
fixturePath('inline-spans.tsx'),
|
|
113
|
+
`
|
|
114
|
+
export function InlineSpans() {
|
|
115
|
+
return (
|
|
116
|
+
<p>
|
|
117
|
+
Your <span>foo</span> bar
|
|
118
|
+
</p>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
{ overwrite: true }
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const tree = scanner.extractComponentJsxTree(file, 'InlineSpans');
|
|
126
|
+
assert.ok(tree && tree.type === 'element');
|
|
127
|
+
|
|
128
|
+
const texts: string[] = [];
|
|
129
|
+
collectTextNodes(tree, texts);
|
|
130
|
+
const joined = texts.join('');
|
|
131
|
+
|
|
132
|
+
assert.ok(
|
|
133
|
+
joined.includes('Your foo bar'),
|
|
134
|
+
`Boundary whitespace must be preserved between sibling text and elements. Got: ${JSON.stringify(joined)}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Case 3: fragment-rooted component with multi-line text inside a child
|
|
140
|
+
// element — exercises the OTHER buildJsxTree branch (line ~1688) which had
|
|
141
|
+
// the same `.trim()` bug.
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
{
|
|
144
|
+
const file = scanner.project.createSourceFile(
|
|
145
|
+
fixturePath('fragment-text.tsx'),
|
|
146
|
+
`
|
|
147
|
+
export function FragmentText() {
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
<p>
|
|
151
|
+
First line
|
|
152
|
+
and second line.
|
|
153
|
+
</p>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
`,
|
|
158
|
+
{ overwrite: true }
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const tree = scanner.extractComponentJsxTree(file, 'FragmentText');
|
|
162
|
+
assert.ok(tree && tree.type === 'element');
|
|
163
|
+
|
|
164
|
+
const texts: string[] = [];
|
|
165
|
+
collectTextNodes(tree, texts);
|
|
166
|
+
const joined = texts.join('');
|
|
167
|
+
|
|
168
|
+
assert.ok(
|
|
169
|
+
!/[\n\r]/.test(joined),
|
|
170
|
+
`Fragment-rooted multi-line JsxText must not retain newlines. Got: ${JSON.stringify(joined)}`
|
|
171
|
+
);
|
|
172
|
+
assert.ok(
|
|
173
|
+
joined.includes('First line and second line.'),
|
|
174
|
+
`Expected 'First line and second line.' Got: ${JSON.stringify(joined)}`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log('jsx-text-regression: ok');
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { makeEmptyIR } from '../src/layout/parser/ir';
|
|
3
|
+
import { parseAlignment } from '../src/layout/parser/alignment';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Locks in the Tailwind alignment → LayoutIR mapping that previously
|
|
7
|
+
* lived inside LayoutParser as part of `parseAlignment` and the self-*
|
|
8
|
+
* portion of `parseChildProperties`. Phase 4 of the layout-parser
|
|
9
|
+
* split moved both into `parser/alignment.ts` — this fixture catches
|
|
10
|
+
* accidental behavioural regressions.
|
|
11
|
+
*
|
|
12
|
+
* Covers:
|
|
13
|
+
* - justify-start / center / end / between → mainAlign
|
|
14
|
+
* - items-start / center / end / stretch / baseline → crossAlign
|
|
15
|
+
* - self-start / center / end / stretch / auto → selfAlign
|
|
16
|
+
* - the implicit STRETCH crossAlign default applied when layoutMode
|
|
17
|
+
* is HORIZONTAL or VERTICAL (CSS flex default)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Implicit crossAlign default — STRETCH when a flex/grid layoutMode is set
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
{
|
|
24
|
+
const ir = makeEmptyIR();
|
|
25
|
+
ir.layoutMode = 'HORIZONTAL';
|
|
26
|
+
parseAlignment([], ir);
|
|
27
|
+
assert.equal(ir.crossAlign, 'STRETCH', 'HORIZONTAL with no items-* defaults to STRETCH');
|
|
28
|
+
}
|
|
29
|
+
{
|
|
30
|
+
const ir = makeEmptyIR();
|
|
31
|
+
ir.layoutMode = 'VERTICAL';
|
|
32
|
+
parseAlignment([], ir);
|
|
33
|
+
assert.equal(ir.crossAlign, 'STRETCH', 'VERTICAL with no items-* defaults to STRETCH');
|
|
34
|
+
}
|
|
35
|
+
{
|
|
36
|
+
const ir = makeEmptyIR();
|
|
37
|
+
// layoutMode = NONE (default) — no implicit STRETCH applied
|
|
38
|
+
parseAlignment([], ir);
|
|
39
|
+
assert.equal(ir.crossAlign, 'MIN', 'NONE keeps crossAlign at MIN');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// justify-* → mainAlign
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
{
|
|
46
|
+
const cases: Array<[string, 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN']> = [
|
|
47
|
+
['justify-start', 'MIN'],
|
|
48
|
+
['justify-center', 'CENTER'],
|
|
49
|
+
['justify-end', 'MAX'],
|
|
50
|
+
['justify-between', 'SPACE_BETWEEN'],
|
|
51
|
+
];
|
|
52
|
+
for (const [cls, expected] of cases) {
|
|
53
|
+
const ir = makeEmptyIR();
|
|
54
|
+
parseAlignment([cls], ir);
|
|
55
|
+
assert.equal(ir.mainAlign, expected, cls);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// items-* → crossAlign (overrides the implicit STRETCH default)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
{
|
|
63
|
+
const cases: Array<[string, 'MIN' | 'CENTER' | 'MAX' | 'STRETCH' | 'BASELINE']> = [
|
|
64
|
+
['items-start', 'MIN'],
|
|
65
|
+
['items-center', 'CENTER'],
|
|
66
|
+
['items-end', 'MAX'],
|
|
67
|
+
['items-stretch', 'STRETCH'],
|
|
68
|
+
['items-baseline', 'BASELINE'],
|
|
69
|
+
];
|
|
70
|
+
for (const [cls, expected] of cases) {
|
|
71
|
+
const ir = makeEmptyIR();
|
|
72
|
+
ir.layoutMode = 'HORIZONTAL';
|
|
73
|
+
parseAlignment([cls], ir);
|
|
74
|
+
assert.equal(ir.crossAlign, expected, cls);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// self-* → selfAlign
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
{
|
|
82
|
+
const cases: Array<[string, 'MIN' | 'CENTER' | 'MAX' | 'STRETCH' | undefined]> = [
|
|
83
|
+
['self-start', 'MIN'],
|
|
84
|
+
['self-center', 'CENTER'],
|
|
85
|
+
['self-end', 'MAX'],
|
|
86
|
+
['self-stretch', 'STRETCH'],
|
|
87
|
+
['self-auto', undefined],
|
|
88
|
+
];
|
|
89
|
+
for (const [cls, expected] of cases) {
|
|
90
|
+
const ir = makeEmptyIR();
|
|
91
|
+
parseAlignment([cls], ir);
|
|
92
|
+
assert.equal(ir.selfAlign, expected, cls);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Combinations — justify + items + self in one class list
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
{
|
|
100
|
+
const ir = makeEmptyIR();
|
|
101
|
+
ir.layoutMode = 'HORIZONTAL';
|
|
102
|
+
parseAlignment(['justify-center', 'items-end', 'self-stretch'], ir);
|
|
103
|
+
assert.equal(ir.mainAlign, 'CENTER');
|
|
104
|
+
assert.equal(ir.crossAlign, 'MAX');
|
|
105
|
+
assert.equal(ir.selfAlign, 'STRETCH');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log('layout-alignment-regression: ok');
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { makeEmptyIR } from '../src/layout/parser/ir';
|
|
3
|
+
import { parseFlexChildren, parseWrap } from '../src/layout/parser/flex';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Locks in Tailwind flex-child + flex-wrap → LayoutIR mapping that
|
|
7
|
+
* previously lived inside LayoutParser as `parseChildProperties` and
|
|
8
|
+
* `parseWrap`. Phase 5 of the layout-parser split moved them into
|
|
9
|
+
* `parser/flex.ts` — this fixture catches accidental behavioural
|
|
10
|
+
* regressions.
|
|
11
|
+
*
|
|
12
|
+
* Covers:
|
|
13
|
+
* - flex-1 / flex-grow / grow → grow = 1
|
|
14
|
+
* - flex-grow-0 / grow-0 → grow = 0
|
|
15
|
+
* - shrink-0 / flex-shrink-0 → shrinkZero = true
|
|
16
|
+
* - flex-wrap / flex-wrap-reverse → wrap = true
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Grow aliases — flex-1 / flex-grow / grow all set ir.grow = 1
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
{
|
|
23
|
+
for (const cls of ['flex-1', 'flex-grow', 'grow']) {
|
|
24
|
+
const ir = makeEmptyIR();
|
|
25
|
+
parseFlexChildren([cls], ir);
|
|
26
|
+
assert.equal(ir.grow, 1, cls);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// flex-grow-0 / grow-0 → grow = 0
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
{
|
|
34
|
+
for (const cls of ['flex-grow-0', 'grow-0']) {
|
|
35
|
+
const ir = makeEmptyIR();
|
|
36
|
+
parseFlexChildren([cls], ir);
|
|
37
|
+
assert.equal(ir.grow, 0, cls);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// shrink-0 / flex-shrink-0 → shrinkZero = true
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
{
|
|
45
|
+
for (const cls of ['shrink-0', 'flex-shrink-0']) {
|
|
46
|
+
const ir = makeEmptyIR();
|
|
47
|
+
parseFlexChildren([cls], ir);
|
|
48
|
+
assert.equal(ir.shrinkZero, true, cls);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Combination — grow=1 AND shrink-0 (rare but valid)
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
{
|
|
56
|
+
const ir = makeEmptyIR();
|
|
57
|
+
parseFlexChildren(['flex-1', 'shrink-0'], ir);
|
|
58
|
+
assert.equal(ir.grow, 1);
|
|
59
|
+
assert.equal(ir.shrinkZero, true);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// flex-wrap / flex-wrap-reverse → wrap = true (reverse collapses to wrap)
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
{
|
|
66
|
+
for (const cls of ['flex-wrap', 'flex-wrap-reverse']) {
|
|
67
|
+
const ir = makeEmptyIR();
|
|
68
|
+
parseWrap([cls], ir);
|
|
69
|
+
assert.equal(ir.wrap, true, cls);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
{
|
|
74
|
+
// No wrap class → wrap stays false
|
|
75
|
+
const ir = makeEmptyIR();
|
|
76
|
+
parseWrap(['flex', 'flex-row', 'gap-4'], ir);
|
|
77
|
+
assert.equal(ir.wrap, false);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Unknown classes are ignored (don't crash, don't mutate)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
{
|
|
84
|
+
const ir = makeEmptyIR();
|
|
85
|
+
parseFlexChildren(['some-other-class', 'p-4', 'text-lg'], ir);
|
|
86
|
+
assert.equal(ir.grow, 0);
|
|
87
|
+
assert.equal(ir.shrinkZero, undefined);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('layout-flex-regression: ok');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { parseLayoutMode } from '../src/layout/parser/layout-mode';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Locks in Tailwind display + direction → IR layoutMode mapping that
|
|
6
|
+
* previously lived inside LayoutParser as `parseLayoutMode`. Phase 6
|
|
7
|
+
* of the layout-parser split moved it into `parser/layout-mode.ts` —
|
|
8
|
+
* this fixture catches accidental behavioural regressions.
|
|
9
|
+
*
|
|
10
|
+
* Covers: flex / inline-flex / flex-row / flex-col (+ -reverse), grid
|
|
11
|
+
* / inline-grid, grid-cols-N detection, the trailing-`flex` no-direction-
|
|
12
|
+
* reset rule, and the responsive-strip class-order semantics.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// flex variants — default direction is HORIZONTAL
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
assert.equal(parseLayoutMode(['flex']), 'HORIZONTAL');
|
|
19
|
+
assert.equal(parseLayoutMode(['inline-flex']), 'HORIZONTAL');
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// flex-col / flex-row → explicit direction
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
assert.equal(parseLayoutMode(['flex', 'flex-col']), 'VERTICAL');
|
|
25
|
+
assert.equal(parseLayoutMode(['flex', 'flex-col-reverse']), 'VERTICAL');
|
|
26
|
+
assert.equal(parseLayoutMode(['flex', 'flex-row']), 'HORIZONTAL');
|
|
27
|
+
assert.equal(parseLayoutMode(['flex', 'flex-row-reverse']), 'HORIZONTAL');
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Trailing `flex` after `flex-col` does NOT reset direction
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
assert.equal(
|
|
33
|
+
parseLayoutMode(['flex-col', 'flex']),
|
|
34
|
+
'VERTICAL',
|
|
35
|
+
'trailing `flex` must not reset flex-direction once explicitly set'
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Class-order semantics — later utility wins (responsive strip pattern)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
assert.equal(
|
|
42
|
+
parseLayoutMode(['flex', 'flex-col', 'flex-row']),
|
|
43
|
+
'HORIZONTAL',
|
|
44
|
+
'later flex-row should override earlier flex-col (responsive sm: pattern)'
|
|
45
|
+
);
|
|
46
|
+
assert.equal(
|
|
47
|
+
parseLayoutMode(['flex', 'flex-row', 'flex-col']),
|
|
48
|
+
'VERTICAL',
|
|
49
|
+
'later flex-col should override earlier flex-row'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// grid / inline-grid — VERTICAL by default, HORIZONTAL when grid-cols-N present
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
assert.equal(parseLayoutMode(['grid']), 'VERTICAL');
|
|
56
|
+
assert.equal(parseLayoutMode(['inline-grid']), 'VERTICAL');
|
|
57
|
+
assert.equal(parseLayoutMode(['grid', 'grid-cols-2']), 'HORIZONTAL');
|
|
58
|
+
assert.equal(parseLayoutMode(['grid', 'grid-cols-12']), 'HORIZONTAL');
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// No flex / grid utilities → NONE
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
assert.equal(parseLayoutMode([]), 'NONE');
|
|
64
|
+
assert.equal(parseLayoutMode(['p-4', 'rounded-lg', 'bg-white']), 'NONE');
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Mixed classes — flex on, then unrelated, still HORIZONTAL
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
assert.equal(parseLayoutMode(['flex', 'p-4', 'gap-2']), 'HORIZONTAL');
|
|
70
|
+
|
|
71
|
+
console.log('layout-mode-regression: ok');
|