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,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');