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
package/scanner/cli.ts
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* that the Figma plugin uses to create components.
|
|
7
7
|
*
|
|
8
8
|
* Usage (consumer project, after `pnpm add -D inkbridge && pnpm exec inkbridge setup`):
|
|
9
|
-
* pnpm
|
|
10
|
-
* GET /api/
|
|
9
|
+
* pnpm inkbridge:scan — run scanner manually
|
|
10
|
+
* GET /api/inkbridge/scan-components — called automatically by the Figma plugin
|
|
11
11
|
*
|
|
12
12
|
* Usage (monorepo dev):
|
|
13
|
-
* npx tsx tools/figma-plugin
|
|
13
|
+
* npx tsx tools/figma-plugin/scanner/cli.ts
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import * as fs from 'fs';
|
|
@@ -22,8 +22,9 @@ import { extractColorTokens, extractPaletteTokens, TAILWIND_SPACING, inferLayout
|
|
|
22
22
|
import { collectAllClasses } from './class-collector';
|
|
23
23
|
import { buildStyleMap } from './style-map';
|
|
24
24
|
import { readTokenSourceMap } from './css-token-reader';
|
|
25
|
-
import type { ComponentDefinitions, ScannerConfig,
|
|
26
|
-
import type { TokenSourceMode } from '../src/token-source';
|
|
25
|
+
import type { ComponentDefinitions, ScannerConfig, ResponsiveInfo, IconImportSpec, IconRegistryEntry, ComponentAnalysis, JsxNode, AtomicKind } from './types';
|
|
26
|
+
import type { TokenSourceMode } from '../src/tokens/token-source';
|
|
27
|
+
import { inferSafeTextOverridePropKeys } from '../src/components/symbol-instance-policy';
|
|
27
28
|
|
|
28
29
|
// ============================================================================
|
|
29
30
|
// CLI flags
|
|
@@ -56,6 +57,14 @@ const DEFAULT_CONFIG: ScannerConfig = {
|
|
|
56
57
|
|
|
57
58
|
const COMPONENT_DEFS_SCHEMA_VERSION = 1;
|
|
58
59
|
|
|
60
|
+
type GraphMeta = {
|
|
61
|
+
kind: AtomicKind;
|
|
62
|
+
usesCount: number;
|
|
63
|
+
usedByCount: number;
|
|
64
|
+
isLeaf: boolean;
|
|
65
|
+
symbolCandidate: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
59
68
|
// ============================================================================
|
|
60
69
|
// Main
|
|
61
70
|
// ============================================================================
|
|
@@ -77,7 +86,9 @@ async function main() {
|
|
|
77
86
|
const styleMap = await buildStyleMap(allClasses);
|
|
78
87
|
const iconRegistry = await buildIconRegistry(scanner.getIconRegistry());
|
|
79
88
|
|
|
80
|
-
|
|
89
|
+
const graphMetaByName = buildGraphMetadata(analyses);
|
|
90
|
+
|
|
91
|
+
// Enrich each analysis with layout, responsive, color-scheme, and graph metadata
|
|
81
92
|
const enriched = analyses.map(analysis => {
|
|
82
93
|
const classes = getAllClassesFromAnalysis(analysis);
|
|
83
94
|
const layout = inferLayout(classes);
|
|
@@ -88,8 +99,21 @@ async function main() {
|
|
|
88
99
|
);
|
|
89
100
|
const responsive: ResponsiveInfo = { breakpoints, hasResponsiveVisibility };
|
|
90
101
|
const colorScheme = groupClassesByColorScheme(classes);
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
const graphMeta = graphMetaByName.get(analysis.name);
|
|
103
|
+
const safeTextOverrideProps = inferSafeTextOverridePropKeys(analysis);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
analysis,
|
|
107
|
+
layout,
|
|
108
|
+
responsive,
|
|
109
|
+
colorScheme,
|
|
110
|
+
kind: graphMeta?.kind,
|
|
111
|
+
usesCount: graphMeta?.usesCount,
|
|
112
|
+
usedByCount: graphMeta?.usedByCount,
|
|
113
|
+
isLeaf: graphMeta?.isLeaf,
|
|
114
|
+
symbolCandidate: graphMeta?.symbolCandidate,
|
|
115
|
+
safeTextOverrideProps: safeTextOverrideProps.length > 0 ? safeTextOverrideProps : undefined,
|
|
116
|
+
};
|
|
93
117
|
});
|
|
94
118
|
|
|
95
119
|
// Build output
|
|
@@ -116,13 +140,23 @@ async function main() {
|
|
|
116
140
|
});
|
|
117
141
|
}
|
|
118
142
|
|
|
119
|
-
// Write to JSON file
|
|
120
|
-
|
|
143
|
+
// Write to JSON file. Default matches the consumer-side path used by
|
|
144
|
+
// the generated `scan-components` API route (see
|
|
145
|
+
// `templates/scan-components-route.ts`), so a manual
|
|
146
|
+
// `pnpm inkbridge:scan` and a plugin-triggered scan land the JSON in
|
|
147
|
+
// the same place. The path lives under `.inkbridge/` at the consumer
|
|
148
|
+
// project root; in this repo the scanner is also used internally
|
|
149
|
+
// (e.g. by regression fixtures) and the same path applies.
|
|
150
|
+
const defaultOutputPath = '.inkbridge/component-definitions.json';
|
|
121
151
|
const outputPath = CLI_OUTPUT
|
|
122
152
|
? path.resolve(process.cwd(), CLI_OUTPUT)
|
|
123
153
|
: path.resolve(process.cwd(), defaultOutputPath);
|
|
124
154
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
125
|
-
|
|
155
|
+
// Compact JSON (no pretty-print). The scan output is only read
|
|
156
|
+
// programmatically by the plugin runtime — the indentation cost was
|
|
157
|
+
// ~30% of the file size for no functional benefit. If you need to
|
|
158
|
+
// hand-inspect it, pipe through `jq` (`jq . component-definitions.json`).
|
|
159
|
+
fs.writeFileSync(outputPath, JSON.stringify(finalOutput));
|
|
126
160
|
|
|
127
161
|
// Print summary
|
|
128
162
|
console.log('✅ Scanned components:\n');
|
|
@@ -193,12 +227,109 @@ async function main() {
|
|
|
193
227
|
console.log(`\n📦 Output written to: ${outputPath}`);
|
|
194
228
|
console.log(` ${analyses.length} components, ${totalStories} stories, ${colorTokens.length} color tokens`);
|
|
195
229
|
|
|
230
|
+
if (analyses.length === 0 || totalStories === 0) {
|
|
231
|
+
printEmptyScanDiagnostic({
|
|
232
|
+
config,
|
|
233
|
+
componentCount: analyses.length,
|
|
234
|
+
storyCount: totalStories,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
196
238
|
} catch (err) {
|
|
197
239
|
console.error('❌ Scanner failed:', err);
|
|
198
240
|
process.exit(1);
|
|
199
241
|
}
|
|
200
242
|
}
|
|
201
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Print actionable hints when the scan returns 0 components or 0 stories.
|
|
246
|
+
* Walks the configured component paths looking for story-like files
|
|
247
|
+
* (`*.stories.ts`, `*.stories.tsx`, `*.stories.jsx`, `*.stories.js`,
|
|
248
|
+
* `*.stories.mdx`) and names what it found vs what the scanner was
|
|
249
|
+
* looking for. Better than a silent "0 stories" line.
|
|
250
|
+
*/
|
|
251
|
+
function printEmptyScanDiagnostic(opts: {
|
|
252
|
+
config: ScannerConfig;
|
|
253
|
+
componentCount: number;
|
|
254
|
+
storyCount: number;
|
|
255
|
+
}): void {
|
|
256
|
+
const { config, componentCount, storyCount } = opts;
|
|
257
|
+
const cwd = process.cwd();
|
|
258
|
+
const STORY_EXTS = ['.stories.tsx', '.stories.ts', '.stories.jsx', '.stories.js', '.stories.mdx'];
|
|
259
|
+
const componentExts = ['.tsx'];
|
|
260
|
+
const found: { stories: string[]; components: string[] } = { stories: [], components: [] };
|
|
261
|
+
|
|
262
|
+
function walk(dir: string, depth: number): void {
|
|
263
|
+
if (depth > 8) return;
|
|
264
|
+
let entries: fs.Dirent[];
|
|
265
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
266
|
+
catch (_e) { return; }
|
|
267
|
+
for (const entry of entries) {
|
|
268
|
+
if (entry.name === 'node_modules' || entry.name === '.next' || entry.name === '.git' || entry.name.startsWith('.')) continue;
|
|
269
|
+
const full = path.join(dir, entry.name);
|
|
270
|
+
if (entry.isDirectory()) {
|
|
271
|
+
walk(full, depth + 1);
|
|
272
|
+
} else if (entry.isFile()) {
|
|
273
|
+
const rel = path.relative(cwd, full);
|
|
274
|
+
if (STORY_EXTS.some(ext => entry.name.endsWith(ext))) {
|
|
275
|
+
found.stories.push(rel);
|
|
276
|
+
} else if (componentExts.some(ext => entry.name.endsWith(ext))) {
|
|
277
|
+
// Only count files that aren't tests / index barrels
|
|
278
|
+
if (!entry.name.endsWith('.test.tsx') && entry.name !== 'index.tsx') {
|
|
279
|
+
found.components.push(rel);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const cp of config.componentPaths) {
|
|
287
|
+
const abs = path.resolve(cwd, cp);
|
|
288
|
+
if (fs.existsSync(abs)) walk(abs, 0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log('');
|
|
292
|
+
if (componentCount === 0) {
|
|
293
|
+
console.log('⚠️ No components matched. Scanner looked under:');
|
|
294
|
+
for (const cp of config.componentPaths) {
|
|
295
|
+
const abs = path.resolve(cwd, cp);
|
|
296
|
+
const exists = fs.existsSync(abs);
|
|
297
|
+
console.log(` - ${cp} ${exists ? '' : '(missing)'}`);
|
|
298
|
+
}
|
|
299
|
+
if (found.components.length > 0) {
|
|
300
|
+
console.log('');
|
|
301
|
+
console.log(` Found ${found.components.length} .tsx file(s) but none qualified as components. Check`);
|
|
302
|
+
console.log(' `inkbridge.config.json` — `componentPaths`, `exclude`, and `onlyWithStories`.');
|
|
303
|
+
}
|
|
304
|
+
} else if (storyCount === 0) {
|
|
305
|
+
console.log(`⚠️ Found ${componentCount} component(s) but 0 stories.`);
|
|
306
|
+
console.log(' Scanner pairs `<Component>.tsx` with `<Component>.stories.tsx`');
|
|
307
|
+
console.log(' or `<Component>.stories.ts` in the same folder.');
|
|
308
|
+
if (found.stories.length > 0) {
|
|
309
|
+
const wrongExt = found.stories.filter(f => f.endsWith('.stories.jsx') || f.endsWith('.stories.js') || f.endsWith('.stories.mdx'));
|
|
310
|
+
if (wrongExt.length > 0) {
|
|
311
|
+
console.log('');
|
|
312
|
+
console.log(' Found story files with unsupported extensions:');
|
|
313
|
+
for (const f of wrongExt.slice(0, 10)) console.log(` - ${f}`);
|
|
314
|
+
if (wrongExt.length > 10) console.log(` … and ${wrongExt.length - 10} more`);
|
|
315
|
+
console.log(' Rename to `.stories.tsx` (preferred) or `.stories.ts`.');
|
|
316
|
+
} else {
|
|
317
|
+
console.log('');
|
|
318
|
+
console.log(' Story files exist but none are co-located with a component:');
|
|
319
|
+
for (const f of found.stories.slice(0, 10)) console.log(` - ${f}`);
|
|
320
|
+
if (found.stories.length > 10) console.log(` … and ${found.stories.length - 10} more`);
|
|
321
|
+
console.log(' Move each story next to its component, or update');
|
|
322
|
+
console.log(' `inkbridge.config.json:componentPaths` to include the story folder.');
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
console.log('');
|
|
326
|
+
console.log(' No story files found anywhere under the component paths.');
|
|
327
|
+
console.log(' Add one (e.g. `src/components/Button/Button.stories.tsx`) and re-run.');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
console.log('');
|
|
331
|
+
}
|
|
332
|
+
|
|
202
333
|
/**
|
|
203
334
|
* Extract all classes from any component analysis type
|
|
204
335
|
*/
|
|
@@ -226,6 +357,204 @@ function getAllClassesFromAnalysis(analysis: import('./types').ComponentAnalysis
|
|
|
226
357
|
return classes;
|
|
227
358
|
}
|
|
228
359
|
|
|
360
|
+
type StructureMetrics = {
|
|
361
|
+
elementCount: number;
|
|
362
|
+
componentNodeCount: number;
|
|
363
|
+
maxDepth: number;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
function normalizeComponentName(name: string): string {
|
|
367
|
+
return String(name || '')
|
|
368
|
+
.replace(/[^a-zA-Z0-9]/g, '')
|
|
369
|
+
.toLowerCase();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function collectStoryRefsFromJsxTree(node: JsxNode | undefined, out: Set<string>): void {
|
|
373
|
+
if (!node || node.type !== 'element') return;
|
|
374
|
+
const tagName = String(node.tagName || '');
|
|
375
|
+
const isComponent = !!node.isComponent || /^[A-Z]/.test(tagName);
|
|
376
|
+
if (isComponent && tagName) {
|
|
377
|
+
out.add(tagName);
|
|
378
|
+
}
|
|
379
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
380
|
+
collectStoryRefsFromJsxTree(node.children[i], out);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function collectStructureMetricsFromJsxTree(node: JsxNode | undefined, depth: number, metrics: StructureMetrics): void {
|
|
385
|
+
if (!node || node.type !== 'element') return;
|
|
386
|
+
metrics.elementCount++;
|
|
387
|
+
metrics.maxDepth = Math.max(metrics.maxDepth, depth);
|
|
388
|
+
if (node.isComponent || /^[A-Z]/.test(String(node.tagName || ''))) {
|
|
389
|
+
metrics.componentNodeCount++;
|
|
390
|
+
}
|
|
391
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
392
|
+
collectStructureMetricsFromJsxTree(node.children[i], depth + 1, metrics);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function collectStoryComponentRefs(analysis: ComponentAnalysis): Set<string> {
|
|
397
|
+
const refs = new Set<string>();
|
|
398
|
+
const stories = analysis.stories || [];
|
|
399
|
+
for (let i = 0; i < stories.length; i++) {
|
|
400
|
+
const story = stories[i];
|
|
401
|
+
const instances = story.instances || [];
|
|
402
|
+
for (let j = 0; j < instances.length; j++) {
|
|
403
|
+
const ref = String(instances[j]?.componentName || '').trim();
|
|
404
|
+
if (ref) refs.add(ref);
|
|
405
|
+
}
|
|
406
|
+
collectStoryRefsFromJsxTree(story.jsxTree, refs);
|
|
407
|
+
}
|
|
408
|
+
return refs;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function collectStoryStructureMetrics(analysis: ComponentAnalysis): StructureMetrics {
|
|
412
|
+
const metrics: StructureMetrics = { elementCount: 0, componentNodeCount: 0, maxDepth: 0 };
|
|
413
|
+
const stories = analysis.stories || [];
|
|
414
|
+
for (let i = 0; i < stories.length; i++) {
|
|
415
|
+
const perStory: StructureMetrics = { elementCount: 0, componentNodeCount: 0, maxDepth: 0 };
|
|
416
|
+
collectStructureMetricsFromJsxTree(stories[i].jsxTree, 1, perStory);
|
|
417
|
+
metrics.elementCount = Math.max(metrics.elementCount, perStory.elementCount);
|
|
418
|
+
metrics.componentNodeCount = Math.max(metrics.componentNodeCount, perStory.componentNodeCount);
|
|
419
|
+
metrics.maxDepth = Math.max(metrics.maxDepth, perStory.maxDepth);
|
|
420
|
+
}
|
|
421
|
+
return metrics;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function createAnalysisResolver(analyses: ComponentAnalysis[]): (refName: string) => string | null {
|
|
425
|
+
const byName = new Map<string, string>();
|
|
426
|
+
const byNormalized = new Map<string, string[]>();
|
|
427
|
+
|
|
428
|
+
for (let i = 0; i < analyses.length; i++) {
|
|
429
|
+
const name = analyses[i].name;
|
|
430
|
+
if (!name) continue;
|
|
431
|
+
byName.set(name, name);
|
|
432
|
+
const normalized = normalizeComponentName(name);
|
|
433
|
+
const list = byNormalized.get(normalized) || [];
|
|
434
|
+
list.push(name);
|
|
435
|
+
byNormalized.set(normalized, list);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return function resolve(refName: string): string | null {
|
|
439
|
+
const ref = String(refName || '').trim();
|
|
440
|
+
if (!ref) return null;
|
|
441
|
+
if (byName.has(ref)) return byName.get(ref) || null;
|
|
442
|
+
|
|
443
|
+
const normalized = normalizeComponentName(ref);
|
|
444
|
+
const direct = byNormalized.get(normalized);
|
|
445
|
+
if (direct && direct.length === 1) return direct[0];
|
|
446
|
+
return null;
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function inferAtomicKind(
|
|
451
|
+
analysis: ComponentAnalysis,
|
|
452
|
+
metrics: StructureMetrics,
|
|
453
|
+
usesCount: number,
|
|
454
|
+
usedByCount: number
|
|
455
|
+
): AtomicKind {
|
|
456
|
+
const hasStory = !!analysis.hasStory;
|
|
457
|
+
const isLeaf = usesCount === 0;
|
|
458
|
+
const classCount = getAllClassesFromAnalysis(analysis).length;
|
|
459
|
+
const isPrimitiveType = analysis.type === 'cva' || analysis.type === 'state';
|
|
460
|
+
const hasComposition = usesCount > 0 || metrics.componentNodeCount > 0;
|
|
461
|
+
const isHighComplexity =
|
|
462
|
+
metrics.maxDepth >= 5
|
|
463
|
+
|| metrics.elementCount >= 24
|
|
464
|
+
|| (hasComposition && metrics.elementCount >= 16)
|
|
465
|
+
|| usesCount >= 2;
|
|
466
|
+
|
|
467
|
+
if (!hasStory && usedByCount === 0 && isLeaf) {
|
|
468
|
+
return 'utility';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (isPrimitiveType) {
|
|
472
|
+
return 'atom';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (isLeaf && usedByCount > 0 && metrics.maxDepth <= 3 && metrics.elementCount <= 10) {
|
|
476
|
+
return 'atom';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (isHighComplexity) {
|
|
480
|
+
return 'organism';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (analysis.type === 'compound' || hasComposition || usedByCount > 0) {
|
|
484
|
+
return 'molecule';
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (isLeaf && classCount > 0 && metrics.maxDepth <= 2) {
|
|
488
|
+
return 'atom';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return 'other';
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function isSymbolCandidate(kind: AtomicKind, analysis: ComponentAnalysis, usedByCount: number): boolean {
|
|
495
|
+
if (analysis.type === 'cva' || analysis.type === 'state') {
|
|
496
|
+
return kind === 'atom' || kind === 'molecule';
|
|
497
|
+
}
|
|
498
|
+
if (analysis.type === 'compound') {
|
|
499
|
+
return kind === 'molecule' || kind === 'atom';
|
|
500
|
+
}
|
|
501
|
+
if (analysis.type === 'simple') {
|
|
502
|
+
return (kind === 'atom' || kind === 'molecule') && usedByCount > 0;
|
|
503
|
+
}
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function buildGraphMetadata(analyses: ComponentAnalysis[]): Map<string, GraphMeta> {
|
|
508
|
+
const outgoing = new Map<string, Set<string>>();
|
|
509
|
+
const incoming = new Map<string, Set<string>>();
|
|
510
|
+
const metricsByName = new Map<string, StructureMetrics>();
|
|
511
|
+
const resolveAnalysisName = createAnalysisResolver(analyses);
|
|
512
|
+
|
|
513
|
+
for (let i = 0; i < analyses.length; i++) {
|
|
514
|
+
const analysis = analyses[i];
|
|
515
|
+
outgoing.set(analysis.name, new Set<string>());
|
|
516
|
+
incoming.set(analysis.name, new Set<string>());
|
|
517
|
+
metricsByName.set(analysis.name, collectStoryStructureMetrics(analysis));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
for (let i = 0; i < analyses.length; i++) {
|
|
521
|
+
const analysis = analyses[i];
|
|
522
|
+
const refs = collectStoryComponentRefs(analysis);
|
|
523
|
+
const from = outgoing.get(analysis.name);
|
|
524
|
+
if (!from) continue;
|
|
525
|
+
refs.forEach((refName) => {
|
|
526
|
+
const target = resolveAnalysisName(refName);
|
|
527
|
+
if (!target || target === analysis.name) return;
|
|
528
|
+
from.add(target);
|
|
529
|
+
const targetIncoming = incoming.get(target);
|
|
530
|
+
if (targetIncoming) targetIncoming.add(analysis.name);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const out = new Map<string, GraphMeta>();
|
|
535
|
+
for (let i = 0; i < analyses.length; i++) {
|
|
536
|
+
const analysis = analyses[i];
|
|
537
|
+
const usesCount = outgoing.get(analysis.name)?.size || 0;
|
|
538
|
+
const usedByCount = incoming.get(analysis.name)?.size || 0;
|
|
539
|
+
const isLeaf = usesCount === 0;
|
|
540
|
+
const metrics = metricsByName.get(analysis.name) || {
|
|
541
|
+
elementCount: 0,
|
|
542
|
+
componentNodeCount: 0,
|
|
543
|
+
maxDepth: 0,
|
|
544
|
+
};
|
|
545
|
+
const kind = inferAtomicKind(analysis, metrics, usesCount, usedByCount);
|
|
546
|
+
out.set(analysis.name, {
|
|
547
|
+
kind,
|
|
548
|
+
usesCount,
|
|
549
|
+
usedByCount,
|
|
550
|
+
isLeaf,
|
|
551
|
+
symbolCandidate: isSymbolCandidate(kind, analysis, usedByCount),
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
|
|
229
558
|
/**
|
|
230
559
|
* Extract component paths from Storybook's stories globs.
|
|
231
560
|
* e.g. "../src/**\/\*.stories.@(ts|tsx)" → ["src"]
|
|
@@ -304,8 +633,8 @@ async function buildIconRegistry(imports: Record<string, IconImportSpec>): Promi
|
|
|
304
633
|
const spec = imports[localName];
|
|
305
634
|
if (!spec) continue;
|
|
306
635
|
try {
|
|
307
|
-
const mod = await import(spec.module);
|
|
308
|
-
const IconComponent =
|
|
636
|
+
const mod = (await import(spec.module)) as Record<string, React.ComponentType<{ size?: number; color?: string }>>;
|
|
637
|
+
const IconComponent = mod[spec.exportName];
|
|
309
638
|
if (!IconComponent) continue;
|
|
310
639
|
const element = React.createElement(IconComponent, { size: 24, color: '#000' });
|
|
311
640
|
let svg = renderToStaticMarkup(element);
|