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,338 @@
|
|
|
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
|
+
applyFullWidthIfPossible,
|
|
11
|
+
markFullWidthNode,
|
|
12
|
+
markFullHeightNode,
|
|
13
|
+
markAbsoluteNode,
|
|
14
|
+
markCssGridVerticalFrame,
|
|
15
|
+
} from '../src/layout/deferred-layout';
|
|
16
|
+
|
|
17
|
+
// MATRIX REGRESSION — full-width / full-height resolution under varying
|
|
18
|
+
// parent context.
|
|
19
|
+
//
|
|
20
|
+
// `applyFullWidthIfPossible` is the SECOND major pass where the "context-
|
|
21
|
+
// dependent sizing" class of bug recurs. The per-class parser writes a
|
|
22
|
+
// `FULL_WIDTH_NODES` / `FULL_HEIGHT_NODES` mark for `w-full` / `h-full`;
|
|
23
|
+
// this pass later reads the mark + parent context and decides whether to:
|
|
24
|
+
// - set `layoutAlign=STRETCH` (vertical flex parent)
|
|
25
|
+
// - set `layoutGrow=1` (horizontal flex parent)
|
|
26
|
+
// - direct-resize (NONE parent, OR absolute child of any parent)
|
|
27
|
+
//
|
|
28
|
+
// Historical fix sites in this class (see troubleshooting.md and commit
|
|
29
|
+
// d817a58):
|
|
30
|
+
// 1. Absolute child with `h-full w-full` not resized in flex parents —
|
|
31
|
+
// old code only direct-resized when `parent.layoutMode === 'NONE'`,
|
|
32
|
+
// so absolute children of VERTICAL/HORIZONTAL parents got dead
|
|
33
|
+
// `layoutAlign='STRETCH'`/`layoutGrow=1` no-ops. Fix: added direct-
|
|
34
|
+
// resize branch for `childIsAbsolutePositioned`.
|
|
35
|
+
// 2. CSS grid (single-column) children failed to stretch — fix:
|
|
36
|
+
// `CSS_GRID_VERTICAL_FRAMES.has(parent)` implies `hasFullWidth`.
|
|
37
|
+
// 3. SPACE_BETWEEN parents had children consume all space — fix: set
|
|
38
|
+
// `layoutGrow=0` + `primaryAxisSizingMode='AUTO'` on `w-full` children
|
|
39
|
+
// of SPACE_BETWEEN parents.
|
|
40
|
+
// 4. `mx-auto + max-w-N` resized child back to wrapper width — fix:
|
|
41
|
+
// structural check (`parent.name === 'mx-auto'` + `MAX_WIDTH_NODES`)
|
|
42
|
+
// bypasses the HORIZONTAL stretch branch.
|
|
43
|
+
// 5. Reflow re-application stacked grid cards — fix in caller
|
|
44
|
+
// (`HORIZONTAL+WRAP` skip), NOT in this function directly. Out of
|
|
45
|
+
// scope for this matrix; covered by `aspect-percent-position-
|
|
46
|
+
// regression.ts`.
|
|
47
|
+
//
|
|
48
|
+
// Cells assert the final state of `layoutAlign`, `layoutGrow`, and
|
|
49
|
+
// post-resize `width` for every (child intent, parent context) cross
|
|
50
|
+
// product.
|
|
51
|
+
|
|
52
|
+
type StubFrame = {
|
|
53
|
+
type: 'FRAME';
|
|
54
|
+
name: string;
|
|
55
|
+
layoutMode: 'NONE' | 'HORIZONTAL' | 'VERTICAL';
|
|
56
|
+
layoutPositioning: 'AUTO' | 'ABSOLUTE';
|
|
57
|
+
primaryAxisSizingMode: 'AUTO' | 'FIXED';
|
|
58
|
+
counterAxisSizingMode: 'AUTO' | 'FIXED';
|
|
59
|
+
primaryAxisAlignItems: string;
|
|
60
|
+
counterAxisAlignItems: string;
|
|
61
|
+
layoutAlign: string;
|
|
62
|
+
layoutGrow: number;
|
|
63
|
+
layoutSizingHorizontal: 'HUG' | 'FIXED' | 'FILL';
|
|
64
|
+
layoutSizingVertical: 'HUG' | 'FIXED' | 'FILL';
|
|
65
|
+
counterAxisAlignSelf: string;
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
paddingLeft: number;
|
|
69
|
+
paddingRight: number;
|
|
70
|
+
paddingTop: number;
|
|
71
|
+
paddingBottom: number;
|
|
72
|
+
fills: unknown[];
|
|
73
|
+
strokes: unknown[];
|
|
74
|
+
effects: unknown[];
|
|
75
|
+
children: never[];
|
|
76
|
+
appendChild(): void;
|
|
77
|
+
resize(w: number, h: number): void;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function makeStubFrame(): StubFrame {
|
|
81
|
+
return {
|
|
82
|
+
type: 'FRAME',
|
|
83
|
+
name: '',
|
|
84
|
+
layoutMode: 'NONE',
|
|
85
|
+
layoutPositioning: 'AUTO',
|
|
86
|
+
primaryAxisSizingMode: 'AUTO',
|
|
87
|
+
counterAxisSizingMode: 'AUTO',
|
|
88
|
+
primaryAxisAlignItems: 'MIN',
|
|
89
|
+
counterAxisAlignItems: 'MIN',
|
|
90
|
+
layoutAlign: 'INHERIT',
|
|
91
|
+
layoutGrow: 0,
|
|
92
|
+
layoutSizingHorizontal: 'HUG',
|
|
93
|
+
layoutSizingVertical: 'HUG',
|
|
94
|
+
counterAxisAlignSelf: 'AUTO',
|
|
95
|
+
width: 0,
|
|
96
|
+
height: 0,
|
|
97
|
+
paddingLeft: 0,
|
|
98
|
+
paddingRight: 0,
|
|
99
|
+
paddingTop: 0,
|
|
100
|
+
paddingBottom: 0,
|
|
101
|
+
fills: [],
|
|
102
|
+
strokes: [],
|
|
103
|
+
effects: [],
|
|
104
|
+
children: [],
|
|
105
|
+
appendChild() { /* no-op */ },
|
|
106
|
+
resize(w, h) { this.width = w; this.height = h; },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Parent factories. Sized at width=600 so cells can assert numeric resizes
|
|
112
|
+
// where applicable.
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
type ParentKind =
|
|
116
|
+
| 'flex-col-fixed-cross' // VERTICAL, counterAxisSizing=FIXED — resize-on-stretch fires
|
|
117
|
+
| 'flex-col-hug-cross' // VERTICAL, counterAxisSizing=AUTO — STRETCH only, no resize
|
|
118
|
+
| 'flex-row-fixed-primary' // HORIZONTAL, primaryAxisSizing=FIXED — grow + resize
|
|
119
|
+
| 'positioning-container' // NONE — direct resize for anything marked w-full
|
|
120
|
+
| 'css-grid-vertical' // VERTICAL + CSS_GRID_VERTICAL_FRAMES → implicit w-full;
|
|
121
|
+
| 'flex-row-space-between'; // HORIZONTAL with SPACE_BETWEEN — special case
|
|
122
|
+
|
|
123
|
+
function makeParent(kind: ParentKind): FrameNode {
|
|
124
|
+
const parent = makeStubFrame();
|
|
125
|
+
parent.width = 600;
|
|
126
|
+
switch (kind) {
|
|
127
|
+
case 'flex-col-fixed-cross':
|
|
128
|
+
parent.layoutMode = 'VERTICAL';
|
|
129
|
+
parent.primaryAxisSizingMode = 'AUTO';
|
|
130
|
+
parent.counterAxisSizingMode = 'FIXED';
|
|
131
|
+
break;
|
|
132
|
+
case 'flex-col-hug-cross':
|
|
133
|
+
parent.layoutMode = 'VERTICAL';
|
|
134
|
+
parent.primaryAxisSizingMode = 'AUTO';
|
|
135
|
+
parent.counterAxisSizingMode = 'AUTO';
|
|
136
|
+
break;
|
|
137
|
+
case 'flex-row-fixed-primary':
|
|
138
|
+
parent.layoutMode = 'HORIZONTAL';
|
|
139
|
+
parent.primaryAxisSizingMode = 'FIXED';
|
|
140
|
+
parent.counterAxisSizingMode = 'AUTO';
|
|
141
|
+
break;
|
|
142
|
+
case 'positioning-container':
|
|
143
|
+
parent.layoutMode = 'NONE';
|
|
144
|
+
break;
|
|
145
|
+
case 'css-grid-vertical':
|
|
146
|
+
parent.layoutMode = 'VERTICAL';
|
|
147
|
+
parent.primaryAxisSizingMode = 'AUTO';
|
|
148
|
+
parent.counterAxisSizingMode = 'FIXED';
|
|
149
|
+
markCssGridVerticalFrame(parent as unknown as FrameNode);
|
|
150
|
+
break;
|
|
151
|
+
case 'flex-row-space-between':
|
|
152
|
+
parent.layoutMode = 'HORIZONTAL';
|
|
153
|
+
parent.primaryAxisSizingMode = 'FIXED';
|
|
154
|
+
parent.primaryAxisAlignItems = 'SPACE_BETWEEN';
|
|
155
|
+
parent.counterAxisSizingMode = 'AUTO';
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
return parent as unknown as FrameNode;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Child factories. Marks set here mimic what tailwind.ts records for the
|
|
163
|
+
// relevant utility classes.
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
type ChildKind =
|
|
167
|
+
| 'w-full' // <div class="w-full">
|
|
168
|
+
| 'absolute-w-full' // <div class="absolute w-full">
|
|
169
|
+
| 'absolute-w-h-full' // <div class="absolute h-full w-full"> — SVG overlay
|
|
170
|
+
| 'unmarked'; // <div class="p-4"> — no width directive; sanity check
|
|
171
|
+
|
|
172
|
+
function makeChild(kind: ChildKind): FrameNode {
|
|
173
|
+
const child = makeStubFrame();
|
|
174
|
+
child.width = 24; // simulate Figma default
|
|
175
|
+
child.height = 24;
|
|
176
|
+
switch (kind) {
|
|
177
|
+
case 'w-full':
|
|
178
|
+
markFullWidthNode(child as unknown as FrameNode);
|
|
179
|
+
break;
|
|
180
|
+
case 'absolute-w-full':
|
|
181
|
+
child.layoutPositioning = 'ABSOLUTE';
|
|
182
|
+
markAbsoluteNode(child as unknown as FrameNode);
|
|
183
|
+
markFullWidthNode(child as unknown as FrameNode);
|
|
184
|
+
break;
|
|
185
|
+
case 'absolute-w-h-full':
|
|
186
|
+
child.layoutPositioning = 'ABSOLUTE';
|
|
187
|
+
markAbsoluteNode(child as unknown as FrameNode);
|
|
188
|
+
markFullWidthNode(child as unknown as FrameNode);
|
|
189
|
+
markFullHeightNode(child as unknown as FrameNode);
|
|
190
|
+
break;
|
|
191
|
+
case 'unmarked':
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
return child as unknown as FrameNode;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Truth table. Each cell asserts the parts of the contract that matter for
|
|
199
|
+
// the (child, parent) pair. `undefined` expectations are unchecked.
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
interface Cell {
|
|
203
|
+
child: ChildKind;
|
|
204
|
+
parent: ParentKind;
|
|
205
|
+
expect: {
|
|
206
|
+
layoutAlign?: 'INHERIT' | 'STRETCH';
|
|
207
|
+
layoutGrow?: number;
|
|
208
|
+
width?: number;
|
|
209
|
+
height?: number;
|
|
210
|
+
};
|
|
211
|
+
note: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const CELLS: Cell[] = [
|
|
215
|
+
// ---- w-full block child ------------------------------------------------
|
|
216
|
+
{ child: 'w-full', parent: 'flex-col-fixed-cross',
|
|
217
|
+
expect: { layoutAlign: 'STRETCH', width: 600 },
|
|
218
|
+
note: 'w-full in vertical flex with FIXED cross-axis: STRETCH + resize' },
|
|
219
|
+
{ child: 'w-full', parent: 'flex-col-hug-cross',
|
|
220
|
+
expect: { layoutAlign: 'STRETCH' },
|
|
221
|
+
note: 'w-full in vertical flex with AUTO cross-axis: STRETCH only, parent will hug to widest' },
|
|
222
|
+
{ child: 'w-full', parent: 'flex-row-fixed-primary',
|
|
223
|
+
expect: { layoutGrow: 1, width: 600 },
|
|
224
|
+
note: 'w-full in horizontal flex: layoutGrow=1 + resize to parent.width' },
|
|
225
|
+
{ child: 'w-full', parent: 'positioning-container',
|
|
226
|
+
expect: { width: 600 },
|
|
227
|
+
note: 'w-full in NONE parent (positioning container): direct resize' },
|
|
228
|
+
{ child: 'w-full', parent: 'css-grid-vertical',
|
|
229
|
+
expect: { layoutAlign: 'STRETCH', width: 600 },
|
|
230
|
+
note: 'CSS-grid vertical (single column): children implicitly stretch' },
|
|
231
|
+
{ child: 'w-full', parent: 'flex-row-space-between',
|
|
232
|
+
expect: { layoutGrow: 0 },
|
|
233
|
+
note: 'SPACE_BETWEEN parent: layoutGrow=0 so siblings get spaced (CSS justify-content)' },
|
|
234
|
+
|
|
235
|
+
// ---- absolute w-full child (no h-full) ---------------------------------
|
|
236
|
+
{ child: 'absolute-w-full', parent: 'flex-col-fixed-cross',
|
|
237
|
+
expect: { width: 600 },
|
|
238
|
+
note: 'absolute child in vertical flex: direct-resize (per d817a58 fix)' },
|
|
239
|
+
{ child: 'absolute-w-full', parent: 'flex-row-fixed-primary',
|
|
240
|
+
expect: { width: 600 },
|
|
241
|
+
note: 'absolute child in horizontal flex: direct-resize (NOT layoutGrow=1; that is a no-op)' },
|
|
242
|
+
{ child: 'absolute-w-full', parent: 'positioning-container',
|
|
243
|
+
expect: { width: 600 },
|
|
244
|
+
note: 'absolute child in NONE parent: direct-resize (canonical absolute overlay)' },
|
|
245
|
+
|
|
246
|
+
// ---- absolute w-full + h-full (SVG overlay) ----------------------------
|
|
247
|
+
{ child: 'absolute-w-h-full', parent: 'flex-col-fixed-cross',
|
|
248
|
+
expect: { width: 600 },
|
|
249
|
+
note: 'absolute overlay: width resizes regardless of parent layoutMode' },
|
|
250
|
+
{ child: 'absolute-w-h-full', parent: 'positioning-container',
|
|
251
|
+
expect: { width: 600 },
|
|
252
|
+
note: 'absolute overlay in aspect-ratio container: width+height both resize (height tested below)' },
|
|
253
|
+
|
|
254
|
+
// ---- unmarked control (sanity) -----------------------------------------
|
|
255
|
+
{ child: 'unmarked', parent: 'flex-col-fixed-cross',
|
|
256
|
+
expect: { layoutAlign: 'INHERIT', layoutGrow: 0, width: 24 },
|
|
257
|
+
note: 'no width directive: no changes' },
|
|
258
|
+
{ child: 'unmarked', parent: 'flex-row-fixed-primary',
|
|
259
|
+
expect: { layoutAlign: 'INHERIT', layoutGrow: 0, width: 24 },
|
|
260
|
+
note: 'no width directive in horizontal parent: no changes' },
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
function runRegression(): void {
|
|
264
|
+
const failures: string[] = [];
|
|
265
|
+
|
|
266
|
+
for (const cell of CELLS) {
|
|
267
|
+
const parent = makeParent(cell.parent);
|
|
268
|
+
const child = makeChild(cell.child);
|
|
269
|
+
|
|
270
|
+
applyFullWidthIfPossible(child, parent);
|
|
271
|
+
|
|
272
|
+
const cellId = `${cell.child} × ${cell.parent}`;
|
|
273
|
+
const stub = child as unknown as StubFrame;
|
|
274
|
+
|
|
275
|
+
if (cell.expect.layoutAlign !== undefined) {
|
|
276
|
+
if (stub.layoutAlign !== cell.expect.layoutAlign) {
|
|
277
|
+
failures.push(
|
|
278
|
+
`${cellId}: expected layoutAlign=${cell.expect.layoutAlign}, got ${stub.layoutAlign}\n → ${cell.note}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (cell.expect.layoutGrow !== undefined) {
|
|
283
|
+
if (stub.layoutGrow !== cell.expect.layoutGrow) {
|
|
284
|
+
failures.push(
|
|
285
|
+
`${cellId}: expected layoutGrow=${cell.expect.layoutGrow}, got ${stub.layoutGrow}\n → ${cell.note}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (cell.expect.width !== undefined) {
|
|
290
|
+
if (stub.width !== cell.expect.width) {
|
|
291
|
+
failures.push(
|
|
292
|
+
`${cellId}: expected width=${cell.expect.width}, got ${stub.width}\n → ${cell.note}`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (cell.expect.height !== undefined) {
|
|
297
|
+
if (stub.height !== cell.expect.height) {
|
|
298
|
+
failures.push(
|
|
299
|
+
`${cellId}: expected height=${cell.expect.height}, got ${stub.height}\n → ${cell.note}`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Extra: absolute overlay in NONE parent should resize BOTH axes. The
|
|
306
|
+
// height branch fires after the width branch — assert here so the matrix
|
|
307
|
+
// covers the full-height direct-resize path that fix-sites have touched.
|
|
308
|
+
{
|
|
309
|
+
const parent = makeParent('positioning-container');
|
|
310
|
+
(parent as unknown as StubFrame).height = 400;
|
|
311
|
+
const child = makeChild('absolute-w-h-full');
|
|
312
|
+
applyFullWidthIfPossible(child, parent);
|
|
313
|
+
const stub = child as unknown as StubFrame;
|
|
314
|
+
if (stub.width !== 600) {
|
|
315
|
+
failures.push(`absolute-w-h-full × NONE: width should be 600, got ${stub.width}`);
|
|
316
|
+
}
|
|
317
|
+
if (stub.height !== 400) {
|
|
318
|
+
failures.push(
|
|
319
|
+
`absolute-w-h-full × NONE: height should be 400 (parent.height direct-resize), got ${stub.height}`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (failures.length > 0) {
|
|
325
|
+
for (const msg of failures) console.error(' ✗ ' + msg);
|
|
326
|
+
assert.fail(`${failures.length} full-width matrix cells failed`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log(`full-width-matrix-regression: PASS (${CELLS.length} cells + height assertion)`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
runRegression();
|
|
334
|
+
} catch (err) {
|
|
335
|
+
console.error('full-width-matrix-regression: FAIL');
|
|
336
|
+
console.error(err);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import { extractGridColumns } from '../src/layout';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression: `extractGridColumns` is the single source of truth for
|
|
7
|
+
* "how many columns does this Tailwind class list specify?" — consumed
|
|
8
|
+
* by both the story-bench and preview-builder reflow pipelines.
|
|
9
|
+
*
|
|
10
|
+
* The function intentionally returns `1` for plain `grid` (no explicit
|
|
11
|
+
* `grid-cols-N`) because Tailwind's default for `display: grid` is
|
|
12
|
+
* single-column flow, and the value is useful for responsive previews
|
|
13
|
+
* (so `sm:grid-cols-3` can flip the layout at a breakpoint). The
|
|
14
|
+
* downstream hazard: `applyGridColumnsIfPossible` would
|
|
15
|
+
* unconditionally flip the frame to `HORIZONTAL` + `WRAP` +
|
|
16
|
+
* `counterAxisSizingMode = AUTO`. A "1-column" reflow on a VERTICAL
|
|
17
|
+
* bench produces a single Hug row of children laid side-by-side
|
|
18
|
+
* instead of stacked.
|
|
19
|
+
*
|
|
20
|
+
* History: this bug class recurred at multiple call sites — every
|
|
21
|
+
* place that forwarded a `cols` value to `applyGridColumnsWithReflow`
|
|
22
|
+
* had to remember the `> 1` guard. preview-builder.ts:668 had the
|
|
23
|
+
* guard; story-builder.ts:491 didn't, so every `<div className="grid
|
|
24
|
+
* w-[Npx] gap-N">` story root rendered as a horizontal Hug-width row.
|
|
25
|
+
* The guard now lives inside `applyGridColumnsIfPossible` itself, so
|
|
26
|
+
* the contract is "forward whatever cols you extracted, the helper
|
|
27
|
+
* decides".
|
|
28
|
+
*
|
|
29
|
+
* This file locks the function's return values so the helper's
|
|
30
|
+
* `cols <= 1` early-return keeps its meaning. Drift in either
|
|
31
|
+
* direction (returning `null` for plain grid, or returning `> 1` for a
|
|
32
|
+
* single-column source) would either silently break responsive
|
|
33
|
+
* previews or re-introduce the horizontal-bench bug.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
// Plain `grid` with no explicit columns → implicit single-column flow.
|
|
37
|
+
// Call sites MUST guard with `> 1` before applying a column reflow.
|
|
38
|
+
assert.equal(
|
|
39
|
+
extractGridColumns(['grid']),
|
|
40
|
+
1,
|
|
41
|
+
'plain `grid` → 1 (Tailwind default single-column flow)',
|
|
42
|
+
);
|
|
43
|
+
assert.equal(
|
|
44
|
+
extractGridColumns(['grid', 'gap-2']),
|
|
45
|
+
1,
|
|
46
|
+
'`grid gap-N` → 1',
|
|
47
|
+
);
|
|
48
|
+
assert.equal(
|
|
49
|
+
extractGridColumns(['grid', 'w-[360px]', 'gap-2']),
|
|
50
|
+
1,
|
|
51
|
+
'`grid w-[N] gap-N` → 1 (the FormField story shape)',
|
|
52
|
+
);
|
|
53
|
+
assert.equal(
|
|
54
|
+
extractGridColumns(['inline-grid']),
|
|
55
|
+
1,
|
|
56
|
+
'plain `inline-grid` → 1',
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Explicit `grid-cols-N` → that number, and downstream reflow IS
|
|
60
|
+
// expected to fire.
|
|
61
|
+
assert.equal(
|
|
62
|
+
extractGridColumns(['grid', 'grid-cols-2']),
|
|
63
|
+
2,
|
|
64
|
+
'`grid grid-cols-2` → 2',
|
|
65
|
+
);
|
|
66
|
+
assert.equal(
|
|
67
|
+
extractGridColumns(['grid', 'grid-cols-3', 'gap-4']),
|
|
68
|
+
3,
|
|
69
|
+
'`grid grid-cols-3 gap-N` → 3',
|
|
70
|
+
);
|
|
71
|
+
assert.equal(
|
|
72
|
+
extractGridColumns(['grid', 'grid-cols-12']),
|
|
73
|
+
12,
|
|
74
|
+
'multi-digit column count parsed',
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// No grid at all → null. Call sites short-circuit without ambiguity.
|
|
78
|
+
assert.equal(
|
|
79
|
+
extractGridColumns(['flex', 'flex-col', 'gap-2']),
|
|
80
|
+
null,
|
|
81
|
+
'flex layouts → null',
|
|
82
|
+
);
|
|
83
|
+
assert.equal(
|
|
84
|
+
extractGridColumns(['p-4', 'rounded-md']),
|
|
85
|
+
null,
|
|
86
|
+
'no display utility → null',
|
|
87
|
+
);
|
|
88
|
+
assert.equal(
|
|
89
|
+
extractGridColumns([]),
|
|
90
|
+
null,
|
|
91
|
+
'empty class list → null',
|
|
92
|
+
);
|
|
93
|
+
assert.equal(
|
|
94
|
+
extractGridColumns(undefined),
|
|
95
|
+
null,
|
|
96
|
+
'undefined input → null',
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Responsive `sm:grid-cols-N` without a base `grid-cols-N` — the function
|
|
100
|
+
// reports the responsive column count when the BASE already has `grid`.
|
|
101
|
+
// (The bench renders at `base` width by default, so without an
|
|
102
|
+
// `availableWidth` hint, the function returns the responsive value as
|
|
103
|
+
// the prevailing column count.)
|
|
104
|
+
assert.equal(
|
|
105
|
+
extractGridColumns(['grid', 'sm:grid-cols-3']),
|
|
106
|
+
3,
|
|
107
|
+
'responsive grid-cols (no base cols) → responsive value',
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
console.log('grid-cols-extraction-regression: PASS (11 cases)');
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
collectImageSrcs,
|
|
5
|
+
collectImageSrcsFromJsxTree,
|
|
6
|
+
looksLikeImageSrc,
|
|
7
|
+
} from '../src/plugin/image-src-collector';
|
|
8
|
+
import type { ComponentDef } from '../src/components';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Regression: the prefetch step at the top of generation needs to collect
|
|
12
|
+
* every image `src` reachable from the scanned JSX trees. The old tagName
|
|
13
|
+
* filter (`tag === 'img' || tag === 'Image'`) silently skipped shadcn-style
|
|
14
|
+
* image wrappers like `<AvatarImage>` and `<AvatarPrimitive.Image>` — even
|
|
15
|
+
* though the renderer is willing to draw any leaf element whose `src` prop
|
|
16
|
+
* points at a fetchable path.
|
|
17
|
+
*
|
|
18
|
+
* Symptom: `<Avatar><AvatarImage src="/images/avatar.svg" /></Avatar>`
|
|
19
|
+
* rendered as an empty 40×40 frame in Figma because the src was never
|
|
20
|
+
* prefetched into `imageMap` / `svgMap`. Storybook showed the image fine.
|
|
21
|
+
*
|
|
22
|
+
* Fix: widen the collector to "any node with a `src` prop that looks like a
|
|
23
|
+
* path/URL," matching the renderer's tolerance. This file locks both
|
|
24
|
+
* primitives together — drift between scanner and renderer is what caused
|
|
25
|
+
* the bug in the first place.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// looksLikeImageSrc — what shapes the prefetch can resolve
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const positiveSrcs: string[] = [
|
|
33
|
+
'/images/avatar.svg',
|
|
34
|
+
'/foo.png',
|
|
35
|
+
'/api/avatar?u=1',
|
|
36
|
+
'http://localhost:3000/img.png',
|
|
37
|
+
'https://cdn.example.com/x.jpg',
|
|
38
|
+
'./local.svg',
|
|
39
|
+
'../parent.png',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const src of positiveSrcs) {
|
|
43
|
+
assert.equal(looksLikeImageSrc(src), true, 'must accept: ' + src);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const negativeSrcs: string[] = [
|
|
47
|
+
'',
|
|
48
|
+
'avatar.svg', // bare filename — no path anchor, can't resolve
|
|
49
|
+
'src', // literal placeholder some scanners emit
|
|
50
|
+
'data:image/png;base64,AAAA', // browser handles data URLs directly
|
|
51
|
+
'blob:http://x/y',
|
|
52
|
+
'javascript:alert(1)',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
for (const src of negativeSrcs) {
|
|
56
|
+
assert.equal(looksLikeImageSrc(src), false, 'must reject: ' + JSON.stringify(src));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// collectImageSrcsFromJsxTree — walks the tree, captures any leaf with src
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
function el(tagName: string, props: Record<string, unknown>, children: unknown[] = []) {
|
|
64
|
+
return { type: 'element', tagName, props, children };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// (a) raw <img> still captured
|
|
68
|
+
{
|
|
69
|
+
const out = new Set<string>();
|
|
70
|
+
collectImageSrcsFromJsxTree(el('img', { src: '/foo.png' }), out);
|
|
71
|
+
assert.deepEqual([...out], ['/foo.png'], '<img> src captured');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// (b) Next.js <Image> still captured
|
|
75
|
+
{
|
|
76
|
+
const out = new Set<string>();
|
|
77
|
+
collectImageSrcsFromJsxTree(el('Image', { src: '/next.png' }), out);
|
|
78
|
+
assert.deepEqual([...out], ['/next.png'], 'Next.js <Image> src captured');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// (c) <AvatarImage> — the regression case
|
|
82
|
+
{
|
|
83
|
+
const out = new Set<string>();
|
|
84
|
+
collectImageSrcsFromJsxTree(el('AvatarImage', { src: '/images/avatar.svg' }), out);
|
|
85
|
+
assert.deepEqual([...out], ['/images/avatar.svg'], 'AvatarImage src captured (new behaviour)');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// (d) <AvatarPrimitive.Image> — namespaced primitive
|
|
89
|
+
{
|
|
90
|
+
const out = new Set<string>();
|
|
91
|
+
collectImageSrcsFromJsxTree(el('AvatarPrimitive.Image', { src: '/x.svg' }), out);
|
|
92
|
+
assert.deepEqual([...out], ['/x.svg'], 'namespaced image primitive src captured');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// (e) nested children — recurses
|
|
96
|
+
{
|
|
97
|
+
const out = new Set<string>();
|
|
98
|
+
collectImageSrcsFromJsxTree(
|
|
99
|
+
el('Avatar', {}, [
|
|
100
|
+
el('AvatarImage', { src: '/a.svg' }),
|
|
101
|
+
el('AvatarFallback', {}, [{ type: 'text', content: 'CN' }]),
|
|
102
|
+
]),
|
|
103
|
+
out,
|
|
104
|
+
);
|
|
105
|
+
assert.deepEqual([...out], ['/a.svg'], 'recursed into Avatar wrapper');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// (f) multiple images deduped
|
|
109
|
+
{
|
|
110
|
+
const out = new Set<string>();
|
|
111
|
+
collectImageSrcsFromJsxTree(
|
|
112
|
+
el('section', {}, [
|
|
113
|
+
el('img', { src: '/a.png' }),
|
|
114
|
+
el('AvatarImage', { src: '/a.png' }),
|
|
115
|
+
el('img', { src: '/b.png' }),
|
|
116
|
+
]),
|
|
117
|
+
out,
|
|
118
|
+
);
|
|
119
|
+
assert.deepEqual([...out].sort(), ['/a.png', '/b.png'], 'duplicates collapsed via Set');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// (g) literal 'src' placeholder and missing src — ignored
|
|
123
|
+
{
|
|
124
|
+
const out = new Set<string>();
|
|
125
|
+
collectImageSrcsFromJsxTree(
|
|
126
|
+
el('img', { src: 'src' }), // some scanners emit this for unresolved attrs
|
|
127
|
+
out,
|
|
128
|
+
);
|
|
129
|
+
collectImageSrcsFromJsxTree(el('img', {}), out);
|
|
130
|
+
assert.equal(out.size, 0, 'placeholder + missing src do not pollute output');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// (h) data: URL — not captured (browser handles directly)
|
|
134
|
+
{
|
|
135
|
+
const out = new Set<string>();
|
|
136
|
+
collectImageSrcsFromJsxTree(
|
|
137
|
+
el('img', { src: 'data:image/png;base64,AAAA' }),
|
|
138
|
+
out,
|
|
139
|
+
);
|
|
140
|
+
assert.equal(out.size, 0, 'data: URL is not a prefetch candidate');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// (i) src prop on a wrapping component with children — STILL captured
|
|
144
|
+
// (the renderer's "leaf" rule is enforced there, not here; the collector
|
|
145
|
+
// is intentionally generous so renderer changes never need a follow-up
|
|
146
|
+
// collector change).
|
|
147
|
+
{
|
|
148
|
+
const out = new Set<string>();
|
|
149
|
+
collectImageSrcsFromJsxTree(
|
|
150
|
+
el('CustomFrame', { src: '/wrap.svg' }, [el('span', {}, [])]),
|
|
151
|
+
out,
|
|
152
|
+
);
|
|
153
|
+
assert.deepEqual([...out], ['/wrap.svg'], 'src on non-leaf is still collected (renderer decides usage)');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// (j) non-object / null tolerated
|
|
157
|
+
{
|
|
158
|
+
const out = new Set<string>();
|
|
159
|
+
collectImageSrcsFromJsxTree(null, out);
|
|
160
|
+
collectImageSrcsFromJsxTree('not-a-node' as unknown, out);
|
|
161
|
+
collectImageSrcsFromJsxTree(undefined, out);
|
|
162
|
+
assert.equal(out.size, 0, 'null / non-object inputs are no-ops');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// collectImageSrcs — walks every component's analysis and every story
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
{
|
|
170
|
+
const components: ComponentDef[] = [
|
|
171
|
+
{
|
|
172
|
+
analysis: {
|
|
173
|
+
jsxTree: el('Avatar', {}, [el('AvatarImage', { src: '/comp.svg' })]),
|
|
174
|
+
stories: [
|
|
175
|
+
{ jsxTree: el('Avatar', {}, [el('AvatarImage', { src: '/story1.svg' })]) },
|
|
176
|
+
{ jsxTree: el('Avatar', {}, [el('AvatarImage', { src: '/story2.svg' })]) },
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
} as unknown as ComponentDef,
|
|
180
|
+
{
|
|
181
|
+
analysis: {
|
|
182
|
+
jsxTree: el('Card', {}, [el('img', { src: '/card.png' })]),
|
|
183
|
+
stories: [],
|
|
184
|
+
},
|
|
185
|
+
} as unknown as ComponentDef,
|
|
186
|
+
// Component with no analysis is tolerated.
|
|
187
|
+
{ analysis: null } as unknown as ComponentDef,
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const srcs = collectImageSrcs(components).sort();
|
|
191
|
+
assert.deepEqual(
|
|
192
|
+
srcs,
|
|
193
|
+
['/card.png', '/comp.svg', '/story1.svg', '/story2.svg'],
|
|
194
|
+
'collectImageSrcs walks component analysis + every story, tolerates missing analysis',
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(
|
|
199
|
+
'image-src-collector-regression: PASS ('
|
|
200
|
+
+ positiveSrcs.length
|
|
201
|
+
+ ' positive + '
|
|
202
|
+
+ negativeSrcs.length
|
|
203
|
+
+ ' negative URL cases + 10 tree cases + 1 component-level case)',
|
|
204
|
+
);
|