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,195 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {
|
|
3
|
+
blockHashMatchesCurrent,
|
|
4
|
+
normalizeForPreflight,
|
|
5
|
+
parseStoredBlockHash,
|
|
6
|
+
} from '../src/design-system/block-cache';
|
|
7
|
+
import type { ComponentDef } from '../src/components';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Regression: the block-cache helpers decide which component blocks survive
|
|
11
|
+
* across re-renders. They run on every preflight and every Generate. If the
|
|
12
|
+
* matching logic drifts, the plugin either rebuilds blocks unnecessarily
|
|
13
|
+
* (destroying user annotations) or keeps stale blocks the user expects to
|
|
14
|
+
* have been regenerated.
|
|
15
|
+
*
|
|
16
|
+
* Three pure-data helpers are covered here. `findComponentBlocksInPage` is
|
|
17
|
+
* Figma-API-bound (reads plugin data + walks the scene graph) and is exercised
|
|
18
|
+
* by the actual plugin under real Figma — not in this fixture.
|
|
19
|
+
*
|
|
20
|
+
* Extracted from `src/design-system/story-builder.ts` into
|
|
21
|
+
* `src/design-system/block-cache.ts`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// parseStoredBlockHash — string parsing of `def:token` and `def:token:engine`
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
// Null / empty / non-string inputs return null (defensive: plugin data reads
|
|
29
|
+
// can return null for absent keys).
|
|
30
|
+
assert.equal(parseStoredBlockHash(null), null, 'null input must parse as null');
|
|
31
|
+
assert.equal(parseStoredBlockHash(''), null, 'empty string must parse as null');
|
|
32
|
+
assert.equal(
|
|
33
|
+
parseStoredBlockHash('only-one-part'),
|
|
34
|
+
null,
|
|
35
|
+
'single-segment string (no colon) must parse as null',
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Two-part legacy format (pre-engine-aware blocks).
|
|
39
|
+
assert.deepEqual(
|
|
40
|
+
parseStoredBlockHash('abc:def'),
|
|
41
|
+
{ defHash: 'abc', tokenHash: 'def', engineHash: null },
|
|
42
|
+
'two-part hash parses without engine — must support legacy blocks',
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Three-part current format.
|
|
46
|
+
assert.deepEqual(
|
|
47
|
+
parseStoredBlockHash('abc:def:1.2.3'),
|
|
48
|
+
{ defHash: 'abc', tokenHash: 'def', engineHash: '1.2.3' },
|
|
49
|
+
'three-part hash parses with engine segment',
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Four+ parts (engine segment carries colons) — anything after the second
|
|
53
|
+
// colon joins back into engineHash so SemVer prereleases (1.0.0-rc.1) or
|
|
54
|
+
// fingerprints with colons don't truncate.
|
|
55
|
+
assert.deepEqual(
|
|
56
|
+
parseStoredBlockHash('a:b:c:d:e'),
|
|
57
|
+
{ defHash: 'a', tokenHash: 'b', engineHash: 'c:d:e' },
|
|
58
|
+
'multi-colon engine segment must rejoin into engineHash, not truncate',
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// blockHashMatchesCurrent — exact-or-tolerant comparison
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
// No stored hash → never matches (treated as "this block was never tagged").
|
|
66
|
+
assert.equal(
|
|
67
|
+
blockHashMatchesCurrent(null, 'def', 'tok', 'eng'),
|
|
68
|
+
false,
|
|
69
|
+
'null stored hash must not match — forces a rebuild for untagged blocks',
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Exact match of all three parts → true.
|
|
73
|
+
assert.equal(
|
|
74
|
+
blockHashMatchesCurrent('def:tok:eng', 'def', 'tok', 'eng'),
|
|
75
|
+
true,
|
|
76
|
+
'exact 3-part match is the canonical hit',
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Backward compat: legacy 2-part hash with matching def+token → true (engine
|
|
80
|
+
// is tolerated as a free variable so engine upgrades don't force rebuilds).
|
|
81
|
+
assert.equal(
|
|
82
|
+
blockHashMatchesCurrent('def:tok', 'def', 'tok', 'eng'),
|
|
83
|
+
true,
|
|
84
|
+
'legacy 2-part stored hash matches when def+token equal — engine upgrade does not rebuild',
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Engine drift on a 3-part stored hash → still matches (def+token wins).
|
|
88
|
+
assert.equal(
|
|
89
|
+
blockHashMatchesCurrent('def:tok:old-engine', 'def', 'tok', 'new-engine'),
|
|
90
|
+
true,
|
|
91
|
+
'def+token match with different engine segment must still match — supports render-engine version migration',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Different defHash → must not match.
|
|
95
|
+
assert.equal(
|
|
96
|
+
blockHashMatchesCurrent('OTHER:tok:eng', 'def', 'tok', 'eng'),
|
|
97
|
+
false,
|
|
98
|
+
'different defHash must force rebuild',
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Different tokenHash → must not match (token change is a real rebuild trigger).
|
|
102
|
+
assert.equal(
|
|
103
|
+
blockHashMatchesCurrent('def:OTHER:eng', 'def', 'tok', 'eng'),
|
|
104
|
+
false,
|
|
105
|
+
'different tokenHash must force rebuild',
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Garbage stored hash → unparseable, no match.
|
|
109
|
+
assert.equal(
|
|
110
|
+
blockHashMatchesCurrent('garbage', 'def', 'tok', 'eng'),
|
|
111
|
+
false,
|
|
112
|
+
'unparseable stored hash must not match',
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// normalizeForPreflight — hoist analysis fields onto top-level def shape
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
// def without analysis is returned as-is (defensive: not all defs come from
|
|
120
|
+
// the scanner — some are constructed in tests / preflight only).
|
|
121
|
+
{
|
|
122
|
+
const def = { name: 'X', stories: [] } as unknown as ComponentDef;
|
|
123
|
+
assert.equal(
|
|
124
|
+
normalizeForPreflight(def),
|
|
125
|
+
def,
|
|
126
|
+
'def without analysis must return identity (no normalization needed)',
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// def with analysis: analysis fields hoist, top-level classification fields preserved.
|
|
131
|
+
{
|
|
132
|
+
const raw = {
|
|
133
|
+
filePath: 'top-file',
|
|
134
|
+
stories: [{ name: 'TopStory' }],
|
|
135
|
+
kind: 'atom',
|
|
136
|
+
usesCount: 3,
|
|
137
|
+
usedByCount: 5,
|
|
138
|
+
isLeaf: false,
|
|
139
|
+
hasStory: true,
|
|
140
|
+
analysis: {
|
|
141
|
+
filePath: 'analysis-file',
|
|
142
|
+
stories: [{ name: 'AnalysisStory' }],
|
|
143
|
+
hasStory: true,
|
|
144
|
+
otherAnalysisField: 'kept',
|
|
145
|
+
},
|
|
146
|
+
} as unknown as ComponentDef;
|
|
147
|
+
|
|
148
|
+
const out = normalizeForPreflight(raw);
|
|
149
|
+
assert.equal(out.filePath, 'analysis-file', 'analysis.filePath wins when present');
|
|
150
|
+
assert.deepEqual(
|
|
151
|
+
out.stories,
|
|
152
|
+
[{ name: 'AnalysisStory' }],
|
|
153
|
+
'analysis.stories wins when present (array, not top-level)',
|
|
154
|
+
);
|
|
155
|
+
assert.equal(out.hasStory, true, 'analysis.hasStory wins when boolean');
|
|
156
|
+
assert.equal(out.kind, 'atom', 'top-level kind is preserved (classification stays on the raw def)');
|
|
157
|
+
assert.equal(out.usesCount, 3, 'top-level usesCount is preserved');
|
|
158
|
+
assert.equal(out.usedByCount, 5, 'top-level usedByCount is preserved');
|
|
159
|
+
assert.equal(out.isLeaf, false, 'top-level isLeaf is preserved');
|
|
160
|
+
assert.equal((out as { otherAnalysisField?: string }).otherAnalysisField, 'kept', 'unknown analysis fields are kept');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Fallback: analysis lacks fields → top-level values fill in.
|
|
164
|
+
{
|
|
165
|
+
const raw = {
|
|
166
|
+
filePath: 'top-file',
|
|
167
|
+
stories: [{ name: 'TopStory' }],
|
|
168
|
+
hasStory: true,
|
|
169
|
+
analysis: {
|
|
170
|
+
// No filePath, no stories, no hasStory — exercise every fallback path.
|
|
171
|
+
otherField: 'x',
|
|
172
|
+
},
|
|
173
|
+
} as unknown as ComponentDef;
|
|
174
|
+
|
|
175
|
+
const out = normalizeForPreflight(raw);
|
|
176
|
+
assert.equal(out.filePath, 'top-file', 'top-level filePath fills in when analysis.filePath absent');
|
|
177
|
+
assert.deepEqual(
|
|
178
|
+
out.stories,
|
|
179
|
+
[{ name: 'TopStory' }],
|
|
180
|
+
'top-level stories fill in when analysis.stories absent',
|
|
181
|
+
);
|
|
182
|
+
assert.equal(out.hasStory, true, 'top-level hasStory fills in when analysis.hasStory missing');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// hasStory falsy-coercion: analysis without boolean hasStory + falsy raw.hasStory → false.
|
|
186
|
+
{
|
|
187
|
+
const raw = {
|
|
188
|
+
stories: [],
|
|
189
|
+
analysis: {},
|
|
190
|
+
} as unknown as ComponentDef;
|
|
191
|
+
const out = normalizeForPreflight(raw);
|
|
192
|
+
assert.equal(out.hasStory, false, 'falsy fallbacks coerce to false (not undefined)');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log('block-cache-regression: PASS');
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression: lock in the minified `code.js` bundle size so accidental
|
|
7
|
+
* imports (a heavy third-party lib, an unintentional non-tree-shakable
|
|
8
|
+
* re-export, dropping `minify: true` from build.mjs) don't silently
|
|
9
|
+
* double the plugin payload.
|
|
10
|
+
*
|
|
11
|
+
* Threshold has ~10% headroom over the current minified size. Bump it
|
|
12
|
+
* deliberately if real new functionality justifies the growth — the
|
|
13
|
+
* commit message should explain why.
|
|
14
|
+
*
|
|
15
|
+
* Must run AFTER `pnpm build` (the verify chain does that). If
|
|
16
|
+
* `code.js` is missing or stale, the assertion fails informatively.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const CODE_JS_PATH = path.resolve(process.cwd(), 'tools/figma-plugin/code.js');
|
|
20
|
+
const MAX_BYTES = 360 * 1024; // 360 KB ceiling (current ~325 KB)
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(CODE_JS_PATH)) {
|
|
23
|
+
console.error('bundle-size-regression: FAIL — code.js missing at ' + CODE_JS_PATH);
|
|
24
|
+
console.error(' Run `pnpm build` first.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const stat = fs.statSync(CODE_JS_PATH);
|
|
29
|
+
const sizeKB = (stat.size / 1024).toFixed(1);
|
|
30
|
+
|
|
31
|
+
assert.ok(
|
|
32
|
+
stat.size <= MAX_BYTES,
|
|
33
|
+
`code.js is ${sizeKB} KB, exceeds ceiling of ${(MAX_BYTES / 1024).toFixed(0)} KB.\n` +
|
|
34
|
+
`If the growth is intentional (real new functionality, not accidental bloat),\n` +
|
|
35
|
+
`bump MAX_BYTES in scanner/bundle-size-regression.ts and explain in the commit.`
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Also sanity-check that minification actually ran — minified IIFE
|
|
39
|
+
// should be on a single long line, not many short lines. If somebody
|
|
40
|
+
// drops `minify: !isWatch` from build.mjs, this catches it before the
|
|
41
|
+
// raw size assertion would.
|
|
42
|
+
const content = fs.readFileSync(CODE_JS_PATH, 'utf8');
|
|
43
|
+
const linesOverThreshold = content.split('\n').filter(l => l.length > 1000).length;
|
|
44
|
+
assert.ok(
|
|
45
|
+
linesOverThreshold > 0,
|
|
46
|
+
'code.js does not appear to be minified — no lines over 1000 characters found.\n' +
|
|
47
|
+
'Check that build.mjs still has `minify: !isWatch`.'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
console.log(`bundle-size-regression: PASS (code.js = ${sizeKB} KB, ceiling ${(MAX_BYTES / 1024).toFixed(0)} KB)`);
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
(globalThis as unknown as { figma: unknown }).figma = {
|
|
4
|
+
notify: () => undefined,
|
|
5
|
+
showUI: () => undefined,
|
|
6
|
+
createFrame: () => makeStubFrame(),
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
LayoutParser,
|
|
11
|
+
setFrameCrossAlign,
|
|
12
|
+
setFrameFromBlockFlow,
|
|
13
|
+
setFrameInlineAlign,
|
|
14
|
+
} from '../src/layout/layout-parser';
|
|
15
|
+
|
|
16
|
+
// MATRIX REGRESSION — child sizing under varying parent context.
|
|
17
|
+
//
|
|
18
|
+
// The recurring "context-dependent sizing" class of bug:
|
|
19
|
+
// The per-class parser correctly records a child's sizing intent (HUG /
|
|
20
|
+
// inline-display / w-full / etc.), then a LATER pass overrides it based on
|
|
21
|
+
// parent-side state alone, never reading the child's recorded intent.
|
|
22
|
+
//
|
|
23
|
+
// Historical fix sites in this class (see troubleshooting.md):
|
|
24
|
+
// 1. `inline-flex` pill in BLOCK-FLOW VERTICAL parent → re-stretched.
|
|
25
|
+
// Fix-site recurred 3× across refactors (tailwind.ts, layout-parser
|
|
26
|
+
// implicit-stretch pass, ui-builder.ts text-resize branch).
|
|
27
|
+
// 2. `inline-flex` button in REAL FLEX-col STRETCH parent → did NOT
|
|
28
|
+
// stretch after over-broad inline-display exclusion. Dialog footer
|
|
29
|
+
// base-breakpoint regression.
|
|
30
|
+
// 3. `text-center` on a block-flow parent → inline children not centered.
|
|
31
|
+
// 4. `w-full` block child → correct STRETCH in vertical parent, `grow=1`
|
|
32
|
+
// in horizontal parent. Easy to break when refactoring either branch.
|
|
33
|
+
//
|
|
34
|
+
// This fixture asserts the truth table — every cell is one (child intent,
|
|
35
|
+
// parent context) cross product. A future refactor that drifts ANY cell
|
|
36
|
+
// triggers here, regardless of which pass introduced the drift.
|
|
37
|
+
//
|
|
38
|
+
// Cells overlap with `inline-flex-regression.ts` by design — that file
|
|
39
|
+
// documents the incident narrative; this file is the systematic net.
|
|
40
|
+
|
|
41
|
+
type StubFrame = {
|
|
42
|
+
type: 'FRAME';
|
|
43
|
+
layoutMode: 'NONE' | 'HORIZONTAL' | 'VERTICAL';
|
|
44
|
+
primaryAxisSizingMode: 'AUTO' | 'FIXED';
|
|
45
|
+
counterAxisSizingMode: 'AUTO' | 'FIXED';
|
|
46
|
+
primaryAxisAlignItems: string;
|
|
47
|
+
counterAxisAlignItems: string;
|
|
48
|
+
layoutAlign: string;
|
|
49
|
+
layoutGrow: number;
|
|
50
|
+
layoutSizingHorizontal: 'HUG' | 'FIXED' | 'FILL';
|
|
51
|
+
layoutSizingVertical: 'HUG' | 'FIXED' | 'FILL';
|
|
52
|
+
counterAxisAlignSelf: string;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
paddingLeft: number;
|
|
56
|
+
paddingRight: number;
|
|
57
|
+
paddingTop: number;
|
|
58
|
+
paddingBottom: number;
|
|
59
|
+
fills: unknown[];
|
|
60
|
+
strokes: unknown[];
|
|
61
|
+
effects: unknown[];
|
|
62
|
+
children: never[];
|
|
63
|
+
appendChild(): void;
|
|
64
|
+
resize(w: number, h: number): void;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function makeStubFrame(): StubFrame {
|
|
68
|
+
return {
|
|
69
|
+
type: 'FRAME',
|
|
70
|
+
layoutMode: 'NONE',
|
|
71
|
+
primaryAxisSizingMode: 'AUTO',
|
|
72
|
+
counterAxisSizingMode: 'AUTO',
|
|
73
|
+
primaryAxisAlignItems: 'MIN',
|
|
74
|
+
counterAxisAlignItems: 'MIN',
|
|
75
|
+
layoutAlign: 'INHERIT',
|
|
76
|
+
layoutGrow: 0,
|
|
77
|
+
layoutSizingHorizontal: 'HUG',
|
|
78
|
+
layoutSizingVertical: 'HUG',
|
|
79
|
+
counterAxisAlignSelf: 'AUTO',
|
|
80
|
+
width: 0,
|
|
81
|
+
height: 0,
|
|
82
|
+
paddingLeft: 0,
|
|
83
|
+
paddingRight: 0,
|
|
84
|
+
paddingTop: 0,
|
|
85
|
+
paddingBottom: 0,
|
|
86
|
+
fills: [],
|
|
87
|
+
strokes: [],
|
|
88
|
+
effects: [],
|
|
89
|
+
children: [],
|
|
90
|
+
appendChild() { /* no-op */ },
|
|
91
|
+
resize(w, h) { this.width = w; this.height = h; },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Parent + child factories
|
|
97
|
+
//
|
|
98
|
+
// Each factory mimics the state the per-class parser would have already
|
|
99
|
+
// written to the frame BEFORE `applyChildProperties` runs. Keeping these
|
|
100
|
+
// here (rather than running tailwind.ts in-process) means the matrix tests
|
|
101
|
+
// the implicit-stretch / w-full / text-align passes in isolation, which is
|
|
102
|
+
// where the historical recurrences happened.
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
type ParentKind =
|
|
106
|
+
| 'flex-col-stretch' // real flex container, CSS items-stretch default
|
|
107
|
+
| 'block-flow' // <div> promoted to VERTICAL but inline-flow
|
|
108
|
+
| 'block-flow-text-center' // block-flow with text-align: center
|
|
109
|
+
| 'horizontal-row'; // <div class="flex flex-row">
|
|
110
|
+
|
|
111
|
+
function makeParent(kind: ParentKind): FrameNode {
|
|
112
|
+
const parent = makeStubFrame();
|
|
113
|
+
if (kind === 'horizontal-row') {
|
|
114
|
+
parent.layoutMode = 'HORIZONTAL';
|
|
115
|
+
parent.width = 600;
|
|
116
|
+
parent.primaryAxisSizingMode = 'FIXED';
|
|
117
|
+
setFrameCrossAlign(parent as unknown as FrameNode, 'MIN');
|
|
118
|
+
setFrameFromBlockFlow(parent as unknown as FrameNode, false);
|
|
119
|
+
} else {
|
|
120
|
+
parent.layoutMode = 'VERTICAL';
|
|
121
|
+
parent.width = 600;
|
|
122
|
+
parent.primaryAxisSizingMode = 'FIXED';
|
|
123
|
+
if (kind === 'flex-col-stretch') {
|
|
124
|
+
setFrameCrossAlign(parent as unknown as FrameNode, 'STRETCH');
|
|
125
|
+
setFrameFromBlockFlow(parent as unknown as FrameNode, false);
|
|
126
|
+
} else {
|
|
127
|
+
setFrameCrossAlign(parent as unknown as FrameNode, 'MIN');
|
|
128
|
+
setFrameFromBlockFlow(parent as unknown as FrameNode, true);
|
|
129
|
+
if (kind === 'block-flow-text-center') {
|
|
130
|
+
setFrameInlineAlign(parent as unknown as FrameNode, 'CENTER');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return parent as unknown as FrameNode;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type ChildKind =
|
|
138
|
+
| 'inline-flex-pill' // <span class="inline-flex items-center rounded-full px-3 py-1">
|
|
139
|
+
| 'block-w-full' // <div class="w-full p-4">
|
|
140
|
+
| 'block-plain' // <div class="p-4">
|
|
141
|
+
| 'absolute-overlay'; // <div class="absolute inset-0">
|
|
142
|
+
|
|
143
|
+
// NOTE on `absolute-overlay`: classes deliberately omit `w-full` / `h-full`.
|
|
144
|
+
// The implicit-stretch exclusion for out-of-flow children is what this matrix
|
|
145
|
+
// tests. The `w-full` handler in applyChildProperties writes `STRETCH` even
|
|
146
|
+
// for absolute children (dead — Figma ignores layoutAlign on absolutely-
|
|
147
|
+
// positioned children); the actual full-resize happens downstream in
|
|
148
|
+
// `applyFullWidthIfPossible` (commit d817a58). That sizing surface has its
|
|
149
|
+
// own regression in `aspect-percent-position-regression.ts`.
|
|
150
|
+
|
|
151
|
+
function makeChild(kind: ChildKind): { child: FrameNode; classes: string[] } {
|
|
152
|
+
const child = makeStubFrame();
|
|
153
|
+
if (kind === 'inline-flex-pill') {
|
|
154
|
+
// Per-class parser already wrote HORIZONTAL + HUG-mode for inline-flex.
|
|
155
|
+
child.layoutMode = 'HORIZONTAL';
|
|
156
|
+
return {
|
|
157
|
+
child: child as unknown as FrameNode,
|
|
158
|
+
classes: 'inline-flex items-center rounded-full px-3 py-1 text-sm font-medium'.split(' '),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (kind === 'block-w-full') {
|
|
162
|
+
return {
|
|
163
|
+
child: child as unknown as FrameNode,
|
|
164
|
+
classes: 'w-full p-4'.split(' '),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (kind === 'block-plain') {
|
|
168
|
+
return {
|
|
169
|
+
child: child as unknown as FrameNode,
|
|
170
|
+
classes: 'p-4'.split(' '),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// absolute-overlay (deliberately NO w-full / h-full — see note above)
|
|
174
|
+
return {
|
|
175
|
+
child: child as unknown as FrameNode,
|
|
176
|
+
classes: 'absolute inset-0'.split(' '),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Truth table — every cell catches a class of historical drift.
|
|
182
|
+
// `note` documents which incident the row guards against.
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
interface Cell {
|
|
186
|
+
child: ChildKind;
|
|
187
|
+
parent: ParentKind;
|
|
188
|
+
expect: {
|
|
189
|
+
layoutAlign?: 'INHERIT' | 'STRETCH';
|
|
190
|
+
layoutGrow?: number;
|
|
191
|
+
counterAxisAlignSelf?: 'AUTO' | 'CENTER' | 'MIN' | 'MAX';
|
|
192
|
+
};
|
|
193
|
+
note: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const CELLS: Cell[] = [
|
|
197
|
+
// ---- inline-flex pill ---------------------------------------------------
|
|
198
|
+
{ child: 'inline-flex-pill', parent: 'flex-col-stretch',
|
|
199
|
+
expect: { layoutAlign: 'STRETCH' },
|
|
200
|
+
note: 'dialog footer base-bp: inline-flex button SHOULD stretch in real flex parent' },
|
|
201
|
+
{ child: 'inline-flex-pill', parent: 'block-flow',
|
|
202
|
+
expect: { layoutAlign: 'INHERIT' },
|
|
203
|
+
note: 'recurring 3-site pill bug: must NOT stretch in block-flow vertical card' },
|
|
204
|
+
{ child: 'inline-flex-pill', parent: 'block-flow-text-center',
|
|
205
|
+
expect: { layoutAlign: 'INHERIT', counterAxisAlignSelf: 'CENTER' },
|
|
206
|
+
note: 'round-trip "in motion" pill: NOT stretched, BUT centered via text-align inheritance' },
|
|
207
|
+
{ child: 'inline-flex-pill', parent: 'horizontal-row',
|
|
208
|
+
expect: { layoutAlign: 'INHERIT' },
|
|
209
|
+
note: 'horizontal parent has no implicit-stretch pass; inline-flex passes through' },
|
|
210
|
+
|
|
211
|
+
// ---- block w-full -------------------------------------------------------
|
|
212
|
+
{ child: 'block-w-full', parent: 'flex-col-stretch',
|
|
213
|
+
expect: { layoutAlign: 'STRETCH' },
|
|
214
|
+
note: 'w-full in vertical flex parent → STRETCH cross-axis' },
|
|
215
|
+
{ child: 'block-w-full', parent: 'block-flow',
|
|
216
|
+
expect: { layoutAlign: 'STRETCH' },
|
|
217
|
+
note: 'w-full in block-flow parent → STRETCH (CSS items-stretch emulation)' },
|
|
218
|
+
{ child: 'block-w-full', parent: 'block-flow-text-center',
|
|
219
|
+
expect: { layoutAlign: 'STRETCH' },
|
|
220
|
+
note: 'block child not affected by text-align — full-width regardless' },
|
|
221
|
+
{ child: 'block-w-full', parent: 'horizontal-row',
|
|
222
|
+
expect: { layoutGrow: 1 },
|
|
223
|
+
note: 'w-full in horizontal parent → grow primary axis (NOT layoutAlign)' },
|
|
224
|
+
|
|
225
|
+
// ---- plain block (no width directive) -----------------------------------
|
|
226
|
+
{ child: 'block-plain', parent: 'flex-col-stretch',
|
|
227
|
+
expect: { layoutAlign: 'STRETCH' },
|
|
228
|
+
note: 'CSS items-stretch default applies to block child' },
|
|
229
|
+
{ child: 'block-plain', parent: 'block-flow',
|
|
230
|
+
expect: { layoutAlign: 'STRETCH' },
|
|
231
|
+
note: 'plugin emulates items-stretch for block children in block-flow' },
|
|
232
|
+
{ child: 'block-plain', parent: 'block-flow-text-center',
|
|
233
|
+
expect: { layoutAlign: 'STRETCH' },
|
|
234
|
+
note: 'text-center is for inline children only; block child still stretches' },
|
|
235
|
+
{ child: 'block-plain', parent: 'horizontal-row',
|
|
236
|
+
expect: { layoutAlign: 'INHERIT', layoutGrow: 0 },
|
|
237
|
+
note: 'no width directive in horizontal parent: neither grow nor stretch' },
|
|
238
|
+
|
|
239
|
+
// ---- absolute overlay (out-of-flow) -------------------------------------
|
|
240
|
+
{ child: 'absolute-overlay', parent: 'flex-col-stretch',
|
|
241
|
+
expect: { layoutAlign: 'INHERIT' },
|
|
242
|
+
note: 'out-of-flow children opt out of implicit-stretch regardless of parent' },
|
|
243
|
+
{ child: 'absolute-overlay', parent: 'block-flow',
|
|
244
|
+
expect: { layoutAlign: 'INHERIT' },
|
|
245
|
+
note: 'absolute child in block-flow: position handled by deferred-layout, not stretch' },
|
|
246
|
+
{ child: 'absolute-overlay', parent: 'block-flow-text-center',
|
|
247
|
+
expect: { layoutAlign: 'INHERIT' },
|
|
248
|
+
note: 'absolute child not affected by text-center' },
|
|
249
|
+
{ child: 'absolute-overlay', parent: 'horizontal-row',
|
|
250
|
+
expect: { layoutAlign: 'INHERIT' },
|
|
251
|
+
note: 'absolute child in horizontal parent: position handled separately' },
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
function runRegression(): void {
|
|
255
|
+
const failures: string[] = [];
|
|
256
|
+
|
|
257
|
+
for (const cell of CELLS) {
|
|
258
|
+
const parent = makeParent(cell.parent);
|
|
259
|
+
const { child, classes } = makeChild(cell.child);
|
|
260
|
+
|
|
261
|
+
LayoutParser.applyChildProperties(child, classes, parent);
|
|
262
|
+
|
|
263
|
+
const cellId = `${cell.child} × ${cell.parent}`;
|
|
264
|
+
const stub = child as unknown as StubFrame;
|
|
265
|
+
|
|
266
|
+
if (cell.expect.layoutAlign !== undefined) {
|
|
267
|
+
if (stub.layoutAlign !== cell.expect.layoutAlign) {
|
|
268
|
+
failures.push(
|
|
269
|
+
`${cellId}: expected layoutAlign=${cell.expect.layoutAlign}, got ${stub.layoutAlign}\n → ${cell.note}`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (cell.expect.layoutGrow !== undefined) {
|
|
274
|
+
if (stub.layoutGrow !== cell.expect.layoutGrow) {
|
|
275
|
+
failures.push(
|
|
276
|
+
`${cellId}: expected layoutGrow=${cell.expect.layoutGrow}, got ${stub.layoutGrow}\n → ${cell.note}`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (cell.expect.counterAxisAlignSelf !== undefined) {
|
|
281
|
+
if (stub.counterAxisAlignSelf !== cell.expect.counterAxisAlignSelf) {
|
|
282
|
+
failures.push(
|
|
283
|
+
`${cellId}: expected counterAxisAlignSelf=${cell.expect.counterAxisAlignSelf}, got ${stub.counterAxisAlignSelf}\n → ${cell.note}`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (failures.length > 0) {
|
|
290
|
+
for (const msg of failures) console.error(' ✗ ' + msg);
|
|
291
|
+
assert.fail(`${failures.length}/${CELLS.length} matrix cells failed`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log(`child-sizing-matrix-regression: PASS (${CELLS.length} cells)`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
runRegression();
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.error('child-sizing-matrix-regression: FAIL');
|
|
301
|
+
console.error(err);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|