inkbridge 0.1.0-beta.21 → 0.1.0-beta.22
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/code.js +10 -10
- package/package.json +5 -2
- package/scanner/component-scanner.ts +132 -2
- package/scanner/imported-array-map-regression.ts +195 -0
- package/scanner/intrinsic-sizing-regression.ts +333 -0
- package/scanner/story-args-resolution-regression.ts +270 -0
- package/src/design-system/story-builder.ts +12 -0
- package/src/layout/index.ts +2 -0
- package/src/layout/intrinsic-applier.ts +105 -0
- package/src/layout/intrinsic-sizing.ts +183 -0
- package/src/layout/parser/layout-mode.ts +14 -4
- package/src/render-engine-version.ts +1 -1
- package/src/tailwind/tailwind.ts +29 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "inkbridge",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.22",
|
|
4
4
|
"description": "Generate a Figma design system from your Storybook stories — Tailwind React components rendered as native frames, design tokens, component states, and round-trip code sync.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -95,8 +95,11 @@
|
|
|
95
95
|
"test:input-range": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin/scanner/input-range-regression.ts",
|
|
96
96
|
"test:font-family-extract": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin/scanner/font-family-extract-regression.ts",
|
|
97
97
|
"test:local-const-className": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin/scanner/local-const-className-regression.ts",
|
|
98
|
+
"test:imported-array-map": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin/scanner/imported-array-map-regression.ts",
|
|
99
|
+
"test:story-args-resolution": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin/scanner/story-args-resolution-regression.ts",
|
|
100
|
+
"test:intrinsic-sizing": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin/scanner/intrinsic-sizing-regression.ts",
|
|
98
101
|
"docs:audit": "node ./scripts/docs-audit.mjs",
|
|
99
|
-
"verify": "pnpm run scan && pnpm run test:blob && pnpm run test:tokens && pnpm run test:font && pnpm run test:radial && pnpm run test:transform && pnpm run test:csspatch && pnpm run test:state-classification && pnpm run test:component-sections && pnpm run test:block-cache && pnpm run test:story-dimensioning && pnpm run test:story-render-strategy && pnpm run test:render-prop-parser && pnpm run test:story-diagnostics && pnpm run test:instance-rendering && pnpm run test:layout-spacing && pnpm run test:layout-sizing && pnpm run test:layout-alignment && pnpm run test:layout-flex && pnpm run test:layout-mode && pnpm run test:svg-marker && pnpm run test:aspect-ratio && pnpm run test:percent-position && pnpm run test:svg-fill-parent && pnpm run test:inline-flex && pnpm run test:child-sizing-matrix && pnpm run test:full-width-matrix && pnpm run test:text-resize-matrix && pnpm run test:provider-flatten && pnpm run test:provider-cascade && pnpm run test:selection-pressed && pnpm run test:cva-jsx-child-fallback && pnpm run test:cva-master-icon && pnpm run test:data-attr-prop-alias && pnpm run test:jsx-prop-unresolved && pnpm run test:grid-cols-extraction && pnpm run test:explicit-size-root && pnpm run test:image-src-collector && pnpm run test:size-full-normalization && pnpm run test:stretch-to-parent-width && pnpm run test:framework-adapter-shadcn && pnpm run test:compound-classes-lookup && pnpm run test:sandbox-spread && pnpm run test:jsx-text && pnpm run test:aspect-percent && pnpm run test:svg-group-inherit && pnpm run test:svg-marker-inline && pnpm run test:ring-utility && pnpm run test:adapter-utils && pnpm run test:input-range && pnpm run test:font-family-extract && pnpm run test:local-const-className && pnpm run docs:audit && pnpm run build && pnpm run test:bundle-size",
|
|
102
|
+
"verify": "pnpm run scan && pnpm run test:blob && pnpm run test:tokens && pnpm run test:font && pnpm run test:radial && pnpm run test:transform && pnpm run test:csspatch && pnpm run test:state-classification && pnpm run test:component-sections && pnpm run test:block-cache && pnpm run test:story-dimensioning && pnpm run test:story-render-strategy && pnpm run test:render-prop-parser && pnpm run test:story-diagnostics && pnpm run test:instance-rendering && pnpm run test:layout-spacing && pnpm run test:layout-sizing && pnpm run test:layout-alignment && pnpm run test:layout-flex && pnpm run test:layout-mode && pnpm run test:svg-marker && pnpm run test:aspect-ratio && pnpm run test:percent-position && pnpm run test:svg-fill-parent && pnpm run test:inline-flex && pnpm run test:child-sizing-matrix && pnpm run test:full-width-matrix && pnpm run test:text-resize-matrix && pnpm run test:provider-flatten && pnpm run test:provider-cascade && pnpm run test:selection-pressed && pnpm run test:cva-jsx-child-fallback && pnpm run test:cva-master-icon && pnpm run test:data-attr-prop-alias && pnpm run test:jsx-prop-unresolved && pnpm run test:grid-cols-extraction && pnpm run test:explicit-size-root && pnpm run test:image-src-collector && pnpm run test:size-full-normalization && pnpm run test:stretch-to-parent-width && pnpm run test:framework-adapter-shadcn && pnpm run test:compound-classes-lookup && pnpm run test:sandbox-spread && pnpm run test:jsx-text && pnpm run test:aspect-percent && pnpm run test:svg-group-inherit && pnpm run test:svg-marker-inline && pnpm run test:ring-utility && pnpm run test:adapter-utils && pnpm run test:input-range && pnpm run test:font-family-extract && pnpm run test:local-const-className && pnpm run test:imported-array-map && pnpm run test:story-args-resolution && pnpm run test:intrinsic-sizing && pnpm run docs:audit && pnpm run build && pnpm run test:bundle-size",
|
|
100
103
|
"release:beta": "pnpm publish --tag beta && npm dist-tag add inkbridge@$npm_package_version latest && npm view inkbridge dist-tags"
|
|
101
104
|
}
|
|
102
105
|
}
|
|
@@ -934,13 +934,38 @@ export class ComponentScanner {
|
|
|
934
934
|
// this an args-based BlockTable story would see
|
|
935
935
|
// `blocks = "mockBlocks"` and the .map call inside the
|
|
936
936
|
// component couldn't resolve the iteration source.
|
|
937
|
+
//
|
|
938
|
+
// Handles both literal property assignments AND spread
|
|
939
|
+
// assignments (`...BASE_ARGS`). Without spread handling,
|
|
940
|
+
// shared-args patterns like
|
|
941
|
+
// const BASE_ARGS = { marketSymbol: "SOL", ... };
|
|
942
|
+
// export const WithLegend = { args: { ...BASE_ARGS, markers: [...] } };
|
|
943
|
+
// would silently drop every base arg, so the component
|
|
944
|
+
// body sees `marketSymbol = undefined` and renders blank.
|
|
937
945
|
for (const prop of argsInit.getProperties()) {
|
|
946
|
+
if (Node.isSpreadAssignment(prop)) {
|
|
947
|
+
const spreadResolved = this.resolveExpressionValue(prop.getExpression(), new Map());
|
|
948
|
+
if (spreadResolved && typeof spreadResolved === 'object' && !Array.isArray(spreadResolved)) {
|
|
949
|
+
for (const [k, v] of Object.entries(spreadResolved as Record<string, ResolvedExpressionValue>)) {
|
|
950
|
+
if (v !== undefined && v !== null) argsContext.set(k, v);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
938
955
|
if (!Node.isPropertyAssignment(prop)) continue;
|
|
939
956
|
const propInit = prop.getInitializer();
|
|
940
957
|
if (!propInit) continue;
|
|
941
958
|
const resolved = this.resolveExpressionValue(propInit, new Map());
|
|
942
|
-
|
|
943
|
-
|
|
959
|
+
// Fallback to parseLiteralValue for shapes resolveExpressionValue
|
|
960
|
+
// doesn't cover (array literals, in particular — needed for
|
|
961
|
+
// `markers: [...]` and similar inline-array story args).
|
|
962
|
+
// `null` / `undefined` are deliberately skipped — they have
|
|
963
|
+
// no propsContext-bearing value to thread through.
|
|
964
|
+
const finalValue = resolved !== undefined
|
|
965
|
+
? resolved
|
|
966
|
+
: this.parseLiteralValue(propInit);
|
|
967
|
+
if (finalValue !== undefined && finalValue !== null) {
|
|
968
|
+
argsContext.set(prop.getName(), finalValue);
|
|
944
969
|
}
|
|
945
970
|
}
|
|
946
971
|
const localCompBody = localComponents.get(metaComponentName);
|
|
@@ -2568,6 +2593,17 @@ export class ComponentScanner {
|
|
|
2568
2593
|
return jsonImported;
|
|
2569
2594
|
}
|
|
2570
2595
|
|
|
2596
|
+
// Named import from a TypeScript / JavaScript file. Covers the common
|
|
2597
|
+
// pattern `import { PRICE_CHART_TIMEFRAMES } from "~/domains/perps/constants"`
|
|
2598
|
+
// followed by `PRICE_CHART_TIMEFRAMES.map(...)` in the JSX body. Without
|
|
2599
|
+
// this branch, `.map()` falls through to the placeholder-iteration path
|
|
2600
|
+
// (one synthetic item) and the rendered tree shows zero buttons.
|
|
2601
|
+
const fromNamedImport = this.findArrayInNamedImports(arrayName, sourceFile);
|
|
2602
|
+
if (fromNamedImport) {
|
|
2603
|
+
this.arrayValueCache.set(cacheKey, fromNamedImport);
|
|
2604
|
+
return fromNamedImport;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2571
2607
|
// Look for default parameter value in function: function Comp({ features = defaultFeatures })
|
|
2572
2608
|
for (const func of sourceFile.getFunctions()) {
|
|
2573
2609
|
const params = func.getParameters();
|
|
@@ -2630,6 +2666,92 @@ export class ComponentScanner {
|
|
|
2630
2666
|
return null;
|
|
2631
2667
|
}
|
|
2632
2668
|
|
|
2669
|
+
/**
|
|
2670
|
+
* Resolve a named-imported array constant by following the import
|
|
2671
|
+
* declaration to its source file and recursing into `findArrayValue`
|
|
2672
|
+
* against that file. Covers patterns like:
|
|
2673
|
+
*
|
|
2674
|
+
* import { PRICE_CHART_TIMEFRAMES } from "~/domains/perps/constants";
|
|
2675
|
+
* ...
|
|
2676
|
+
* {PRICE_CHART_TIMEFRAMES.map((option) => <Button>{option.label}</Button>)}
|
|
2677
|
+
*
|
|
2678
|
+
* Without this, `findArrayValue` only inspects the current source file's
|
|
2679
|
+
* declarations, returns `null` for cross-file named imports, and the
|
|
2680
|
+
* `.map()` falls through to the placeholder-iteration path (one synthetic
|
|
2681
|
+
* item) — so consumer JSX renders zero buttons instead of N.
|
|
2682
|
+
*
|
|
2683
|
+
* Returns `null` for default imports (the JSON branch handles `import x from
|
|
2684
|
+
* "./foo.json"` separately) and for any import whose target file can't be
|
|
2685
|
+
* resolved to disk via `resolveImportedComponentPath`.
|
|
2686
|
+
*/
|
|
2687
|
+
private findArrayInNamedImports(name: string, sourceFile: SourceFile): ResolvedExpressionValue[] | null {
|
|
2688
|
+
const fileDir = path.dirname(sourceFile.getFilePath());
|
|
2689
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
2690
|
+
const named = imp.getNamedImports().find((n) => {
|
|
2691
|
+
const localName = n.getAliasNode()?.getText() || n.getName();
|
|
2692
|
+
return localName === name;
|
|
2693
|
+
});
|
|
2694
|
+
if (!named) continue;
|
|
2695
|
+
const moduleSpec = imp.getModuleSpecifierValue();
|
|
2696
|
+
const importedFile = this.openImportedSourceFile(fileDir, moduleSpec);
|
|
2697
|
+
if (!importedFile) return null;
|
|
2698
|
+
// Resolve under the IMPORTED source's name (the export may rename
|
|
2699
|
+
// via `import { Foo as Bar }` — we follow the exported name).
|
|
2700
|
+
const exportedName = named.getName();
|
|
2701
|
+
return this.findArrayValue(exportedName, importedFile);
|
|
2702
|
+
}
|
|
2703
|
+
return null;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
/**
|
|
2707
|
+
* Try to load the source file the import declaration points at, against
|
|
2708
|
+
* both the disk (production scan) and the in-memory ts-morph project
|
|
2709
|
+
* (regression fixtures that don't touch the filesystem). Returns `null`
|
|
2710
|
+
* for imports whose target can't be located in either place.
|
|
2711
|
+
*
|
|
2712
|
+
* Built on top of `resolveImportedComponentPath`'s extension-trying logic
|
|
2713
|
+
* — we re-implement the same candidate list and probe the project's
|
|
2714
|
+
* tracked files when `fs.existsSync` says "not on disk". Keeps the
|
|
2715
|
+
* regression fixtures self-contained (no temp-dir writes) while staying
|
|
2716
|
+
* correct for real consumer scans.
|
|
2717
|
+
*/
|
|
2718
|
+
private openImportedSourceFile(baseDir: string, moduleSpec: string): SourceFile | null {
|
|
2719
|
+
// Fast path: real files on disk.
|
|
2720
|
+
const onDiskPath = this.resolveImportedComponentPath(baseDir, moduleSpec);
|
|
2721
|
+
if (onDiskPath) {
|
|
2722
|
+
try {
|
|
2723
|
+
const fromDisk = this.project.addSourceFileAtPathIfExists(onDiskPath);
|
|
2724
|
+
if (fromDisk) return fromDisk;
|
|
2725
|
+
} catch (_e) {
|
|
2726
|
+
/* fall through to in-memory probe */
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
// In-memory probe: build the same candidate list (with extension
|
|
2731
|
+
// variants) and check the project's tracked files. Lets fixtures call
|
|
2732
|
+
// `project.createSourceFile(path, content)` and have those files be
|
|
2733
|
+
// findable through normal import resolution.
|
|
2734
|
+
let basePath: string | null = null;
|
|
2735
|
+
if (moduleSpec.startsWith('./') || moduleSpec.startsWith('../')) {
|
|
2736
|
+
basePath = path.resolve(baseDir, moduleSpec);
|
|
2737
|
+
} else if (moduleSpec.startsWith('~/') || moduleSpec.startsWith('@/')) {
|
|
2738
|
+
basePath = path.resolve(process.cwd(), 'src', moduleSpec.slice(2));
|
|
2739
|
+
}
|
|
2740
|
+
if (!basePath) return null;
|
|
2741
|
+
const candidates = [
|
|
2742
|
+
basePath,
|
|
2743
|
+
`${basePath}.tsx`,
|
|
2744
|
+
`${basePath}.ts`,
|
|
2745
|
+
path.join(basePath, 'index.tsx'),
|
|
2746
|
+
path.join(basePath, 'index.ts'),
|
|
2747
|
+
];
|
|
2748
|
+
for (const candidate of candidates) {
|
|
2749
|
+
const inProject = this.project.getSourceFile(candidate);
|
|
2750
|
+
if (inProject) return inProject;
|
|
2751
|
+
}
|
|
2752
|
+
return null;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2633
2755
|
/**
|
|
2634
2756
|
* Resolve an expression to an array of items. Handles:
|
|
2635
2757
|
*
|
|
@@ -3884,6 +4006,14 @@ export class ComponentScanner {
|
|
|
3884
4006
|
const text = val.getText();
|
|
3885
4007
|
if (text === 'true') return true;
|
|
3886
4008
|
if (text === 'false') return false;
|
|
4009
|
+
// `null` / `undefined` keywords resolve to their JS values, not the
|
|
4010
|
+
// strings "null" / "undefined". Previously they fell through to the
|
|
4011
|
+
// `return text` branch and (since "null" is a non-empty truthy string)
|
|
4012
|
+
// caused conditionals like `{error && <Banner>{error}</Banner>}` to
|
|
4013
|
+
// render the banner with literal "null" text when stories passed
|
|
4014
|
+
// `error: null`.
|
|
4015
|
+
if (text === 'null') return null;
|
|
4016
|
+
if (text === 'undefined') return undefined;
|
|
3887
4017
|
if (Node.isArrayLiteralExpression(val)) {
|
|
3888
4018
|
return val.getElements().map((el) => this.parseLiteralValue(el));
|
|
3889
4019
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { ComponentScanner } from './component-scanner';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression: `.map()` over a NAMED imported array must expand into N
|
|
7
|
+
* JSX children, not fall through to the placeholder-iteration path.
|
|
8
|
+
*
|
|
9
|
+
* History: greenhouse-app's `MarketPriceCard` renders its timeframe row
|
|
10
|
+
* with
|
|
11
|
+
*
|
|
12
|
+
* import { PRICE_CHART_TIMEFRAMES } from "~/domains/perps/constants";
|
|
13
|
+
* ...
|
|
14
|
+
* {PRICE_CHART_TIMEFRAMES.map((option) => (
|
|
15
|
+
* <Button variant={timeframe === option.value ? "default" : "outline"}>
|
|
16
|
+
* {option.label}
|
|
17
|
+
* </Button>
|
|
18
|
+
* ))}
|
|
19
|
+
*
|
|
20
|
+
* `findArrayValue` only inspected the current source file's variable
|
|
21
|
+
* declarations + default JSON imports. Named imports from a TypeScript
|
|
22
|
+
* file (`export const PRICE_CHART_TIMEFRAMES = [...]` in
|
|
23
|
+
* `~/domains/perps/constants.ts`) were invisible. The `.map()` fell
|
|
24
|
+
* through to the "no array found" branch and emitted ONE synthetic
|
|
25
|
+
* placeholder iteration — so Figma rendered zero buttons.
|
|
26
|
+
*
|
|
27
|
+
* Fix: `findArrayInNamedImports` follows named imports to the source
|
|
28
|
+
* file via `resolveImportedComponentPath`, opens the file, and recurses
|
|
29
|
+
* into `findArrayValue` against the imported source. Wired into
|
|
30
|
+
* `findArrayValue` after the JSON-import branch.
|
|
31
|
+
*
|
|
32
|
+
* Generalizes to ANY named-imported const array, not just chart
|
|
33
|
+
* timeframes — covers role lists, theme enums, navigation items, etc.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
interface JsxNodeLike {
|
|
37
|
+
type: 'element' | 'text';
|
|
38
|
+
tagName?: string;
|
|
39
|
+
content?: string;
|
|
40
|
+
props?: Record<string, unknown>;
|
|
41
|
+
children?: JsxNodeLike[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface TestScannerView {
|
|
45
|
+
project: import('ts-morph').Project;
|
|
46
|
+
extractComponentJsxTree: (
|
|
47
|
+
sourceFile: import('ts-morph').SourceFile,
|
|
48
|
+
componentName: string,
|
|
49
|
+
) => JsxNodeLike | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeScanner(): TestScannerView {
|
|
53
|
+
return new ComponentScanner({
|
|
54
|
+
componentPaths: [],
|
|
55
|
+
filePattern: '*.tsx',
|
|
56
|
+
exclude: [],
|
|
57
|
+
}) as unknown as TestScannerView;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fixturePath(relative: string): string {
|
|
61
|
+
return path.resolve(process.cwd(), 'tools/figma-plugin/scanner/__fixtures__', relative);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function findAllElements(node: JsxNodeLike | null, tag: string, out: JsxNodeLike[] = []): JsxNodeLike[] {
|
|
65
|
+
if (!node || node.type !== 'element') return out;
|
|
66
|
+
if (node.tagName === tag) out.push(node);
|
|
67
|
+
for (const child of node.children || []) findAllElements(child, tag, out);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const scanner = makeScanner();
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Case 1: relative-path named import (`./constants`). Asserts `.map()` over
|
|
75
|
+
// the imported array expands into N JSX elements with the right label text.
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
{
|
|
78
|
+
scanner.project.createSourceFile(
|
|
79
|
+
fixturePath('imported-array/constants.ts'),
|
|
80
|
+
`
|
|
81
|
+
export const PRICE_CHART_TIMEFRAMES: Array<{ label: string; value: string }> = [
|
|
82
|
+
{ label: "Tick", value: "tick" },
|
|
83
|
+
{ label: "1h", value: "1h" },
|
|
84
|
+
{ label: "1d", value: "1d" },
|
|
85
|
+
];
|
|
86
|
+
`,
|
|
87
|
+
{ overwrite: true },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const file = scanner.project.createSourceFile(
|
|
91
|
+
fixturePath('imported-array/Card.tsx'),
|
|
92
|
+
`
|
|
93
|
+
import { PRICE_CHART_TIMEFRAMES } from "./constants";
|
|
94
|
+
export function Card() {
|
|
95
|
+
return (
|
|
96
|
+
<div>
|
|
97
|
+
{PRICE_CHART_TIMEFRAMES.map((option) => (
|
|
98
|
+
<button data-key={option.value}>{option.label}</button>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
`,
|
|
104
|
+
{ overwrite: true },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const tree = scanner.extractComponentJsxTree(file, 'Card');
|
|
108
|
+
assert.ok(tree, 'Card must produce a tree');
|
|
109
|
+
const buttons = findAllElements(tree, 'button');
|
|
110
|
+
assert.equal(
|
|
111
|
+
buttons.length,
|
|
112
|
+
3,
|
|
113
|
+
`expected 3 buttons from the named-imported array .map(); got ${buttons.length}`,
|
|
114
|
+
);
|
|
115
|
+
const labels = buttons
|
|
116
|
+
.map((b) => (b.children || []).map((c) => c.content ?? '').join(''))
|
|
117
|
+
.join('|');
|
|
118
|
+
assert.ok(
|
|
119
|
+
labels.includes('Tick') && labels.includes('1h') && labels.includes('1d'),
|
|
120
|
+
`expected each button's text child to come from option.label; got "${labels}"`,
|
|
121
|
+
);
|
|
122
|
+
const keys = buttons.map((b) => b.props?.['data-key']).join(',');
|
|
123
|
+
assert.equal(
|
|
124
|
+
keys,
|
|
125
|
+
'tick,1h,1d',
|
|
126
|
+
`expected data-key props to come from option.value; got "${keys}"`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Case 2: aliased named import (`import { Foo as Bar } from "./x"`). The
|
|
132
|
+
// scanner must follow the imported name even when locally renamed.
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
{
|
|
135
|
+
scanner.project.createSourceFile(
|
|
136
|
+
fixturePath('imported-array/aliased-constants.ts'),
|
|
137
|
+
`
|
|
138
|
+
export const ROLES = [
|
|
139
|
+
{ id: "admin", label: "Admin" },
|
|
140
|
+
{ id: "viewer", label: "Viewer" },
|
|
141
|
+
];
|
|
142
|
+
`,
|
|
143
|
+
{ overwrite: true },
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const file = scanner.project.createSourceFile(
|
|
147
|
+
fixturePath('imported-array/Aliased.tsx'),
|
|
148
|
+
`
|
|
149
|
+
import { ROLES as RolesList } from "./aliased-constants";
|
|
150
|
+
export function Aliased() {
|
|
151
|
+
return (
|
|
152
|
+
<ul>
|
|
153
|
+
{RolesList.map((r) => <li data-id={r.id}>{r.label}</li>)}
|
|
154
|
+
</ul>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
`,
|
|
158
|
+
{ overwrite: true },
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const tree = scanner.extractComponentJsxTree(file, 'Aliased');
|
|
162
|
+
assert.ok(tree, 'Aliased must produce a tree');
|
|
163
|
+
const items = findAllElements(tree, 'li');
|
|
164
|
+
assert.equal(items.length, 2, `expected 2 <li> from aliased import .map(); got ${items.length}`);
|
|
165
|
+
const ids = items.map((b) => b.props?.['data-id']).join(',');
|
|
166
|
+
assert.equal(ids, 'admin,viewer', `expected ids resolved through the alias; got "${ids}"`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Case 3: unresolvable import (path doesn't exist on disk) MUST NOT crash.
|
|
171
|
+
// `.map()` falls through to the existing placeholder-iteration path. Lock the
|
|
172
|
+
// graceful degradation so a future change can't turn this into a hard error.
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
{
|
|
175
|
+
const file = scanner.project.createSourceFile(
|
|
176
|
+
fixturePath('imported-array/Unresolvable.tsx'),
|
|
177
|
+
`
|
|
178
|
+
import { NOPE } from "./does-not-exist";
|
|
179
|
+
export function Unresolvable() {
|
|
180
|
+
return <div>{NOPE.map((x) => <span>{x.label}</span>)}</div>;
|
|
181
|
+
}
|
|
182
|
+
`,
|
|
183
|
+
{ overwrite: true },
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const tree = scanner.extractComponentJsxTree(file, 'Unresolvable');
|
|
187
|
+
assert.ok(tree, 'must not throw on an unresolvable import');
|
|
188
|
+
// Should produce the placeholder iteration (one synthetic <span>) — the
|
|
189
|
+
// existing fallback path. Just assert we got *something* without
|
|
190
|
+
// checking the placeholder shape, which is implementation-detail.
|
|
191
|
+
const spans = findAllElements(tree, 'span');
|
|
192
|
+
assert.ok(spans.length >= 0, 'should not throw');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log('imported-array-map-regression: PASS (3 cases)');
|