inkbridge 0.1.0-beta.1
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 +201 -0
- package/bin/inkhouse.mjs +171 -0
- package/code.js +11802 -0
- package/manifest.json +30 -0
- package/package.json +45 -0
- package/scanner/blob-placement-regression.ts +132 -0
- package/scanner/class-collector.ts +69 -0
- package/scanner/cli.ts +336 -0
- package/scanner/component-scanner.ts +2876 -0
- package/scanner/css-patch-regression.ts +112 -0
- package/scanner/css-token-reader-regression.ts +92 -0
- package/scanner/css-token-reader.ts +477 -0
- package/scanner/font-style-resolver-regression.ts +32 -0
- package/scanner/index.ts +9 -0
- package/scanner/radial-gradient-regression.ts +53 -0
- package/scanner/style-map.ts +149 -0
- package/scanner/tailwind-parser.ts +644 -0
- package/scanner/transform-math-regression.ts +42 -0
- package/scanner/types.ts +298 -0
- package/src/blob-placement.ts +111 -0
- package/src/change-detection.ts +204 -0
- package/src/class-utils.ts +105 -0
- package/src/clip-path-decorative.ts +194 -0
- package/src/color-resolver.ts +98 -0
- package/src/colors.ts +196 -0
- package/src/component-defs.ts +54 -0
- package/src/component-gen.ts +561 -0
- package/src/component-lookup.ts +82 -0
- package/src/config.ts +115 -0
- package/src/design-system.ts +59 -0
- package/src/dev-server.ts +173 -0
- package/src/figma-globals.d.ts +3 -0
- package/src/font-style-resolver.ts +171 -0
- package/src/github.ts +1465 -0
- package/src/icon-builder.ts +607 -0
- package/src/image-cache.ts +22 -0
- package/src/inline-text.ts +271 -0
- package/src/layout-parser.ts +667 -0
- package/src/layout-utils.ts +155 -0
- package/src/main.ts +687 -0
- package/src/node-ir.ts +595 -0
- package/src/pack-provider.ts +148 -0
- package/src/packs.ts +126 -0
- package/src/radial-gradient.ts +84 -0
- package/src/render-context.ts +138 -0
- package/src/responsive-analyzer.ts +139 -0
- package/src/state-analyzer.ts +143 -0
- package/src/story-builder.ts +1706 -0
- package/src/story-layout.ts +38 -0
- package/src/tailwind.ts +2379 -0
- package/src/text-builder.ts +116 -0
- package/src/text-line.ts +42 -0
- package/src/token-source.ts +43 -0
- package/src/tokens.ts +717 -0
- package/src/transform-math.ts +44 -0
- package/src/ui-builder.ts +1996 -0
- package/src/utility-resolver.ts +125 -0
- package/src/variables.ts +1042 -0
- package/src/width-solver.ts +466 -0
- package/templates/patch-tokens-route.ts +165 -0
- package/templates/scan-components-route.ts +57 -0
- package/ui.html +1222 -0
package/manifest.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Inkbridge",
|
|
3
|
+
"id": "inkbridge-tokens-plugin",
|
|
4
|
+
"api": "1.0.0",
|
|
5
|
+
"main": "code.js",
|
|
6
|
+
"ui": "ui.html",
|
|
7
|
+
"capabilities": ["codegen"],
|
|
8
|
+
"codegenLanguages": [
|
|
9
|
+
{ "label": "Tailwind CSS", "value": "PLAINTEXT" }
|
|
10
|
+
],
|
|
11
|
+
"editorType": ["figma", "dev"],
|
|
12
|
+
"networkAccess": {
|
|
13
|
+
"allowedDomains": [
|
|
14
|
+
"https://api.github.com"
|
|
15
|
+
],
|
|
16
|
+
"devAllowedDomains": [
|
|
17
|
+
"http://localhost:4000",
|
|
18
|
+
"http://localhost:3000",
|
|
19
|
+
"http://localhost:5173"
|
|
20
|
+
],
|
|
21
|
+
"reasoning": "Fetches component definitions from local dev server for Design System generation, and uses GitHub API to create PRs for design token syncing."
|
|
22
|
+
},
|
|
23
|
+
"menu": [
|
|
24
|
+
{ "name": "Push to Code", "command": "push" },
|
|
25
|
+
{ "name": "Settings", "command": "settings" },
|
|
26
|
+
{ "separator": true },
|
|
27
|
+
{ "name": "Generate Design System Page", "command": "generate" },
|
|
28
|
+
{ "name": "Debug Selection (Console)", "command": "debug-selection" }
|
|
29
|
+
]
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "inkbridge",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"description": "Figma plugin that generates a pixel-accurate design system from your Tailwind React components.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"inkbridge": "./bin/inkbridge.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"scanner/",
|
|
12
|
+
"src/",
|
|
13
|
+
"templates/",
|
|
14
|
+
"code.js",
|
|
15
|
+
"ui.html",
|
|
16
|
+
"manifest.json",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "node build.mjs",
|
|
24
|
+
"watch": "node build.mjs --watch",
|
|
25
|
+
"doctor": "cd ../.. && node scripts/figma-doctor.mjs",
|
|
26
|
+
"scan": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin-tailwind-tokens/scanner/cli.ts",
|
|
27
|
+
"test:blob": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin-tailwind-tokens/scanner/blob-placement-regression.ts",
|
|
28
|
+
"test:tokens": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin-tailwind-tokens/scanner/css-token-reader-regression.ts",
|
|
29
|
+
"test:font": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin-tailwind-tokens/scanner/font-style-resolver-regression.ts",
|
|
30
|
+
"test:radial": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin-tailwind-tokens/scanner/radial-gradient-regression.ts",
|
|
31
|
+
"test:transform": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin-tailwind-tokens/scanner/transform-math-regression.ts",
|
|
32
|
+
"test:csspatch": "cd ../.. && ./node_modules/.bin/tsx tools/figma-plugin-tailwind-tokens/scanner/css-patch-regression.ts",
|
|
33
|
+
"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 build",
|
|
34
|
+
"postinstall": "node ./bin/inkbridge.mjs postinstall",
|
|
35
|
+
"prepublishOnly": "node build.mjs"
|
|
36
|
+
},
|
|
37
|
+
"keywords": ["figma", "plugin", "tailwind", "design-tokens", "storybook"],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"homepage": "https://inkbridge.io",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/inkn9ne/inkbridge.git",
|
|
43
|
+
"directory": "tools/figma-plugin-tailwind-tokens"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { resolveBlobDimensions, resolveBlobPlacement } from '../src/blob-placement';
|
|
3
|
+
import { getClassesForBreakpoint } from '../src/responsive-analyzer';
|
|
4
|
+
|
|
5
|
+
type CaseDef = {
|
|
6
|
+
name: string;
|
|
7
|
+
classes: string[];
|
|
8
|
+
breakpoint: 'base' | 'sm';
|
|
9
|
+
containerWidth: number;
|
|
10
|
+
expected: {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
layoutLeft: number;
|
|
14
|
+
centerX: number;
|
|
15
|
+
rotateDeg: number;
|
|
16
|
+
topOffset: number;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const BLOB1_CLASSES = [
|
|
21
|
+
'relative',
|
|
22
|
+
'left-[calc(50%-11rem)]',
|
|
23
|
+
'aspect-1155/678',
|
|
24
|
+
'w-144.5',
|
|
25
|
+
'-translate-x-1/2',
|
|
26
|
+
'rotate-30',
|
|
27
|
+
'bg-linear-to-tr',
|
|
28
|
+
'from-primary-foreground',
|
|
29
|
+
'to-primary',
|
|
30
|
+
'opacity-30',
|
|
31
|
+
'sm:left-[calc(50%-30rem)]',
|
|
32
|
+
'sm:w-288.75',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const BLOB2_CLASSES = [
|
|
36
|
+
'relative',
|
|
37
|
+
'left-[calc(50%+3rem)]',
|
|
38
|
+
'aspect-1155/678',
|
|
39
|
+
'w-144.5',
|
|
40
|
+
'-translate-x-1/2',
|
|
41
|
+
'bg-linear-to-tr',
|
|
42
|
+
'from-primary-foreground',
|
|
43
|
+
'to-primary',
|
|
44
|
+
'opacity-30',
|
|
45
|
+
'sm:left-[calc(50%+36rem)]',
|
|
46
|
+
'sm:w-288.75',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const CASES: CaseDef[] = [
|
|
50
|
+
{
|
|
51
|
+
name: 'blob1-base',
|
|
52
|
+
classes: BLOB1_CLASSES,
|
|
53
|
+
breakpoint: 'base',
|
|
54
|
+
containerWidth: 600,
|
|
55
|
+
expected: {
|
|
56
|
+
width: 578,
|
|
57
|
+
height: (578 * 678) / 1155,
|
|
58
|
+
layoutLeft: 124,
|
|
59
|
+
centerX: 124,
|
|
60
|
+
rotateDeg: 30,
|
|
61
|
+
topOffset: 0,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'blob1-sm',
|
|
66
|
+
classes: BLOB1_CLASSES,
|
|
67
|
+
breakpoint: 'sm',
|
|
68
|
+
containerWidth: 1024,
|
|
69
|
+
expected: {
|
|
70
|
+
width: 1155,
|
|
71
|
+
height: 678,
|
|
72
|
+
layoutLeft: 32,
|
|
73
|
+
centerX: 32,
|
|
74
|
+
rotateDeg: 30,
|
|
75
|
+
topOffset: 0,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'blob2-base',
|
|
80
|
+
classes: BLOB2_CLASSES,
|
|
81
|
+
breakpoint: 'base',
|
|
82
|
+
containerWidth: 600,
|
|
83
|
+
expected: {
|
|
84
|
+
width: 578,
|
|
85
|
+
height: (578 * 678) / 1155,
|
|
86
|
+
layoutLeft: 348,
|
|
87
|
+
centerX: 348,
|
|
88
|
+
rotateDeg: 0,
|
|
89
|
+
topOffset: 0,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'blob2-sm',
|
|
94
|
+
classes: BLOB2_CLASSES,
|
|
95
|
+
breakpoint: 'sm',
|
|
96
|
+
containerWidth: 1024,
|
|
97
|
+
expected: {
|
|
98
|
+
width: 1155,
|
|
99
|
+
height: 678,
|
|
100
|
+
layoutLeft: 1088,
|
|
101
|
+
centerX: 1088,
|
|
102
|
+
rotateDeg: 0,
|
|
103
|
+
topOffset: 0,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
function approxEqual(actual: number, expected: number, epsilon = 0.01): void {
|
|
109
|
+
assert.ok(Math.abs(actual - expected) <= epsilon, `expected ${expected}, got ${actual}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const testCase of CASES) {
|
|
113
|
+
const active = testCase.breakpoint === 'base'
|
|
114
|
+
? testCase.classes
|
|
115
|
+
: getClassesForBreakpoint(testCase.classes, testCase.breakpoint);
|
|
116
|
+
const dimensions = resolveBlobDimensions(active);
|
|
117
|
+
const placement = resolveBlobPlacement(
|
|
118
|
+
active,
|
|
119
|
+
testCase.containerWidth,
|
|
120
|
+
dimensions.vectorWidth,
|
|
121
|
+
dimensions.vectorHeight,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
approxEqual(dimensions.vectorWidth, testCase.expected.width);
|
|
125
|
+
approxEqual(dimensions.vectorHeight, testCase.expected.height);
|
|
126
|
+
approxEqual(placement.layoutLeft, testCase.expected.layoutLeft);
|
|
127
|
+
approxEqual(placement.desiredCenterX, testCase.expected.centerX);
|
|
128
|
+
approxEqual(placement.cssRotateDeg, testCase.expected.rotateDeg);
|
|
129
|
+
approxEqual(placement.topOffset, testCase.expected.topOffset);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`blob placement regression passed (${CASES.length} cases)`);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ComponentAnalysis, StoryInfo, JsxNode, JsxElement } from './types';
|
|
2
|
+
|
|
3
|
+
function splitClasses(value?: string): string[] {
|
|
4
|
+
if (!value) return [];
|
|
5
|
+
return value.split(/\s+/).filter(Boolean);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function collectFromJsx(node?: JsxNode, out: string[] = []): string[] {
|
|
9
|
+
if (!node) return out;
|
|
10
|
+
if (node.type === 'element') {
|
|
11
|
+
const el = node as JsxElement;
|
|
12
|
+
const className = (el.props && el.props.className) || '';
|
|
13
|
+
out.push(...splitClasses(className));
|
|
14
|
+
for (const child of el.children || []) {
|
|
15
|
+
collectFromJsx(child, out);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectFromStory(story: StoryInfo): string[] {
|
|
22
|
+
const classes: string[] = [];
|
|
23
|
+
if (story.layoutClasses) classes.push(...story.layoutClasses);
|
|
24
|
+
if (story.instances) {
|
|
25
|
+
for (const inst of story.instances) {
|
|
26
|
+
if (inst.props && inst.props.className) {
|
|
27
|
+
classes.push(...splitClasses(inst.props.className));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (story.jsxTree) {
|
|
32
|
+
collectFromJsx(story.jsxTree, classes);
|
|
33
|
+
}
|
|
34
|
+
return classes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function collectAllClasses(analyses: ComponentAnalysis[]): string[] {
|
|
38
|
+
const classes: string[] = [];
|
|
39
|
+
|
|
40
|
+
for (const analysis of analyses) {
|
|
41
|
+
if (analysis.type === 'cva') {
|
|
42
|
+
classes.push(...analysis.baseClasses);
|
|
43
|
+
for (const variantGroup of Object.values(analysis.variantClasses)) {
|
|
44
|
+
for (const cls of Object.values(variantGroup)) {
|
|
45
|
+
classes.push(...cls);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} else if (analysis.type === 'compound') {
|
|
49
|
+
for (const sub of analysis.subComponents) {
|
|
50
|
+
classes.push(...sub.classes);
|
|
51
|
+
}
|
|
52
|
+
} else if (analysis.type === 'state') {
|
|
53
|
+
classes.push(...analysis.baseClasses);
|
|
54
|
+
for (const state of Object.values(analysis.states)) {
|
|
55
|
+
classes.push(...state.classes);
|
|
56
|
+
}
|
|
57
|
+
} else if (analysis.type === 'simple') {
|
|
58
|
+
classes.push(...analysis.classes);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (analysis.stories) {
|
|
62
|
+
for (const story of analysis.stories) {
|
|
63
|
+
classes.push(...collectFromStory(story));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return classes;
|
|
69
|
+
}
|
package/scanner/cli.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Component Scanner CLI
|
|
4
|
+
*
|
|
5
|
+
* Scans component files and generates a JSON definitions file
|
|
6
|
+
* that the Figma plugin uses to create components.
|
|
7
|
+
*
|
|
8
|
+
* Usage (consumer project, after `pnpm add -D inkbridge && pnpm exec inkbridge setup`):
|
|
9
|
+
* pnpm figma:scan ā run scanner manually
|
|
10
|
+
* GET /api/figma/scan-components ā called automatically by the Figma plugin
|
|
11
|
+
*
|
|
12
|
+
* Usage (monorepo dev):
|
|
13
|
+
* npx tsx tools/figma-plugin-tailwind-tokens/scanner/cli.ts
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import React from 'react';
|
|
19
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
20
|
+
import { ComponentScanner } from './component-scanner';
|
|
21
|
+
import { extractColorTokens, extractPaletteTokens, TAILWIND_SPACING, inferLayout, groupClassesByBreakpoint, groupClassesByColorScheme } from './tailwind-parser';
|
|
22
|
+
import { collectAllClasses } from './class-collector';
|
|
23
|
+
import { buildStyleMap } from './style-map';
|
|
24
|
+
import { readTokenSourceMap } from './css-token-reader';
|
|
25
|
+
import type { ComponentDefinitions, ScannerConfig, LayoutInfo, ResponsiveInfo, ColorSchemeInfo, IconImportSpec, IconRegistryEntry } from './types';
|
|
26
|
+
import type { TokenSourceMode } from '../src/token-source';
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// CLI flags
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
const argv = process.argv.slice(2);
|
|
33
|
+
const getFlag = (flag: string): string | undefined => {
|
|
34
|
+
const i = argv.indexOf(flag);
|
|
35
|
+
return i !== -1 ? argv[i + 1] : undefined;
|
|
36
|
+
};
|
|
37
|
+
const hasFlag = (flag: string): boolean => argv.includes(flag);
|
|
38
|
+
|
|
39
|
+
const CLI_OUTPUT = getFlag('--output');
|
|
40
|
+
const CLI_INCLUDE_TOKENS = hasFlag('--include-tokens');
|
|
41
|
+
const CLI_TOKEN_MODE = (getFlag('--token-mode') || 'auto') as TokenSourceMode;
|
|
42
|
+
const CLI_CSS_TOKEN_PATH = getFlag('--css-token-path');
|
|
43
|
+
const CLI_DTCG_TOKEN_PATH = getFlag('--dtcg-token-path');
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Configuration
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
const DEFAULT_CONFIG: ScannerConfig = {
|
|
50
|
+
componentPaths: ['src'],
|
|
51
|
+
filePattern: '**/*.tsx',
|
|
52
|
+
exclude: ['index'],
|
|
53
|
+
onlyWithStories: true,
|
|
54
|
+
iconPackages: ['lucide-react', 'react-icons/'],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const COMPONENT_DEFS_SCHEMA_VERSION = 1;
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// Main
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
console.log('š Scanning components...\n');
|
|
65
|
+
|
|
66
|
+
const config = loadConfig();
|
|
67
|
+
const scanner = new ComponentScanner(config);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const analyses = await scanner.scanAll();
|
|
71
|
+
|
|
72
|
+
// Collect all classes used across components + stories
|
|
73
|
+
const allClasses = collectAllClasses(analyses);
|
|
74
|
+
|
|
75
|
+
const colorTokens = extractColorTokens(allClasses);
|
|
76
|
+
const paletteTokens = extractPaletteTokens(allClasses);
|
|
77
|
+
const styleMap = await buildStyleMap(allClasses);
|
|
78
|
+
const iconRegistry = await buildIconRegistry(scanner.getIconRegistry());
|
|
79
|
+
|
|
80
|
+
// Enrich each analysis with layout, responsive, and color-scheme info
|
|
81
|
+
const enriched = analyses.map(analysis => {
|
|
82
|
+
const classes = getAllClassesFromAnalysis(analysis);
|
|
83
|
+
const layout = inferLayout(classes);
|
|
84
|
+
const breakpoints = groupClassesByBreakpoint(classes);
|
|
85
|
+
const hasResponsiveVisibility = classes.some(c =>
|
|
86
|
+
/^(sm|md|lg|xl|2xl):(hidden|flex|block|grid|inline|table|contents)$/.test(c) ||
|
|
87
|
+
/^(hidden)\s/.test(c) // "hidden" at base with responsive show
|
|
88
|
+
);
|
|
89
|
+
const responsive: ResponsiveInfo = { breakpoints, hasResponsiveVisibility };
|
|
90
|
+
const colorScheme = groupClassesByColorScheme(classes);
|
|
91
|
+
|
|
92
|
+
return { analysis, layout, responsive, colorScheme };
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Build output
|
|
96
|
+
const output: ComponentDefinitions = {
|
|
97
|
+
schemaVersion: COMPONENT_DEFS_SCHEMA_VERSION,
|
|
98
|
+
version: '1.0.0',
|
|
99
|
+
generatedAt: new Date().toISOString(),
|
|
100
|
+
components: enriched,
|
|
101
|
+
spacingScale: TAILWIND_SPACING,
|
|
102
|
+
colorTokens,
|
|
103
|
+
paletteTokens,
|
|
104
|
+
iconRegistry,
|
|
105
|
+
styleMap,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Optionally include token map when called with --include-tokens
|
|
109
|
+
const finalOutput: typeof output & { tokens?: ReturnType<typeof readTokenSourceMap> } = output;
|
|
110
|
+
if (CLI_INCLUDE_TOKENS) {
|
|
111
|
+
finalOutput.tokens = readTokenSourceMap({
|
|
112
|
+
projectRoot: process.cwd(),
|
|
113
|
+
tokenSourceMode: CLI_TOKEN_MODE,
|
|
114
|
+
cssTokenPath: CLI_CSS_TOKEN_PATH,
|
|
115
|
+
dtcgTokenPath: CLI_DTCG_TOKEN_PATH,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Write to JSON file
|
|
120
|
+
const defaultOutputPath = 'tools/figma-plugin-tailwind-tokens/component-definitions.json';
|
|
121
|
+
const outputPath = CLI_OUTPUT
|
|
122
|
+
? path.resolve(process.cwd(), CLI_OUTPUT)
|
|
123
|
+
: path.resolve(process.cwd(), defaultOutputPath);
|
|
124
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
125
|
+
fs.writeFileSync(outputPath, JSON.stringify(finalOutput, null, 2));
|
|
126
|
+
|
|
127
|
+
// Print summary
|
|
128
|
+
console.log('ā
Scanned components:\n');
|
|
129
|
+
|
|
130
|
+
const byType = {
|
|
131
|
+
cva: enriched.filter(e => e.analysis.type === 'cva'),
|
|
132
|
+
compound: enriched.filter(e => e.analysis.type === 'compound'),
|
|
133
|
+
state: enriched.filter(e => e.analysis.type === 'state'),
|
|
134
|
+
simple: enriched.filter(e => e.analysis.type === 'simple'),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Helper to format story info
|
|
138
|
+
const storyStr = (a: import('./types').ComponentAnalysis): string => {
|
|
139
|
+
if (!a.stories || a.stories.length === 0) return '';
|
|
140
|
+
const names = a.stories.map(s => s.name).join(', ');
|
|
141
|
+
const totalInstances = a.stories.reduce((sum, s) => sum + s.instances.length, 0);
|
|
142
|
+
return ` ā ${a.stories.length} stories (${names}) [${totalInstances} instances]`;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (byType.cva.length > 0) {
|
|
146
|
+
console.log(' CVA Components (variants + states):');
|
|
147
|
+
for (const e of byType.cva) {
|
|
148
|
+
const a = e.analysis;
|
|
149
|
+
if (a.type === 'cva') {
|
|
150
|
+
const variantCount = Object.keys(a.variants).length;
|
|
151
|
+
const totalVariants = Object.values(a.variants).reduce((sum, arr) => sum + arr.length, 0);
|
|
152
|
+
console.log(` ⢠${a.name} (${variantCount} variant groups, ${totalVariants} values)${storyStr(a)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.log();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (byType.compound.length > 0) {
|
|
159
|
+
console.log(' Compound Components (sub-components):');
|
|
160
|
+
for (const e of byType.compound) {
|
|
161
|
+
const a = e.analysis;
|
|
162
|
+
if (a.type === 'compound') {
|
|
163
|
+
console.log(` ⢠${a.name} (${a.subComponents.length} sub-components)${storyStr(a)}`);
|
|
164
|
+
for (const sub of a.subComponents) {
|
|
165
|
+
console.log(` - ${sub.name} [${sub.slot}]`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
console.log();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (byType.state.length > 0) {
|
|
173
|
+
console.log(' State Components (state modifiers):');
|
|
174
|
+
for (const e of byType.state) {
|
|
175
|
+
const a = e.analysis;
|
|
176
|
+
if (a.type === 'state') {
|
|
177
|
+
const states = Object.keys(a.states).join(', ');
|
|
178
|
+
console.log(` ⢠${a.name} (states: ${states})${storyStr(a)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
console.log();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (byType.simple.length > 0) {
|
|
185
|
+
console.log(' Simple Components:');
|
|
186
|
+
for (const e of byType.simple) {
|
|
187
|
+
console.log(` ⢠${e.analysis.name}${storyStr(e.analysis)}`);
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const totalStories = analyses.reduce((sum, a) => sum + (a.stories?.length || 0), 0);
|
|
193
|
+
console.log(`\nš¦ Output written to: ${outputPath}`);
|
|
194
|
+
console.log(` ${analyses.length} components, ${totalStories} stories, ${colorTokens.length} color tokens`);
|
|
195
|
+
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error('ā Scanner failed:', err);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Extract all classes from any component analysis type
|
|
204
|
+
*/
|
|
205
|
+
function getAllClassesFromAnalysis(analysis: import('./types').ComponentAnalysis): string[] {
|
|
206
|
+
const classes: string[] = [];
|
|
207
|
+
if (analysis.type === 'cva') {
|
|
208
|
+
classes.push(...analysis.baseClasses);
|
|
209
|
+
for (const variantGroup of Object.values(analysis.variantClasses)) {
|
|
210
|
+
for (const cls of Object.values(variantGroup)) {
|
|
211
|
+
classes.push(...cls);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} else if (analysis.type === 'compound') {
|
|
215
|
+
for (const sub of analysis.subComponents) {
|
|
216
|
+
classes.push(...sub.classes);
|
|
217
|
+
}
|
|
218
|
+
} else if (analysis.type === 'state') {
|
|
219
|
+
classes.push(...analysis.baseClasses);
|
|
220
|
+
for (const state of Object.values(analysis.states)) {
|
|
221
|
+
classes.push(...state.classes);
|
|
222
|
+
}
|
|
223
|
+
} else if (analysis.type === 'simple') {
|
|
224
|
+
classes.push(...analysis.classes);
|
|
225
|
+
}
|
|
226
|
+
return classes;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Extract component paths from Storybook's stories globs.
|
|
231
|
+
* e.g. "../src/**\/\*.stories.@(ts|tsx)" ā ["src"]
|
|
232
|
+
*/
|
|
233
|
+
function detectStorybookPaths(cwd: string): string[] {
|
|
234
|
+
const candidates = [
|
|
235
|
+
'.storybook/main.ts',
|
|
236
|
+
'.storybook/main.js',
|
|
237
|
+
'.storybook/main.mjs',
|
|
238
|
+
'.storybook/main.cjs',
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
for (const candidate of candidates) {
|
|
242
|
+
const fullPath = path.resolve(cwd, candidate);
|
|
243
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
244
|
+
|
|
245
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
246
|
+
|
|
247
|
+
// Extract the stories array value (handles single or multi-line)
|
|
248
|
+
const storiesMatch = content.match(/stories\s*:\s*\[([^\]]+)\]/s);
|
|
249
|
+
if (!storiesMatch) continue;
|
|
250
|
+
|
|
251
|
+
const globs = [...storiesMatch[1].matchAll(/["'`]([^"'`]+)["'`]/g)].map(m => m[1]);
|
|
252
|
+
const dirs = new Set<string>();
|
|
253
|
+
|
|
254
|
+
for (const glob of globs) {
|
|
255
|
+
// Resolve relative to the project root (strip leading "../" from .storybook/ context)
|
|
256
|
+
const normalized = glob.replace(/^\.\.\//, '');
|
|
257
|
+
// Take the first non-glob path segment as the root directory
|
|
258
|
+
const firstSegment = normalized.split('/')[0];
|
|
259
|
+
if (firstSegment && !firstSegment.includes('*') && !firstSegment.includes('{')) {
|
|
260
|
+
dirs.add(firstSegment);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (dirs.size > 0) {
|
|
265
|
+
console.log(` ā³ Detected story paths from ${candidate}: ${[...dirs].join(', ')}`);
|
|
266
|
+
return [...dirs];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function loadConfig(): ScannerConfig {
|
|
274
|
+
const cwd = process.cwd();
|
|
275
|
+
|
|
276
|
+
// 1. inkbridge.config.json (primary)
|
|
277
|
+
const inkbridgeCfg = path.resolve(cwd, 'inkbridge.config.json');
|
|
278
|
+
if (fs.existsSync(inkbridgeCfg)) {
|
|
279
|
+
const custom = JSON.parse(fs.readFileSync(inkbridgeCfg, 'utf-8'));
|
|
280
|
+
return { ...DEFAULT_CONFIG, ...custom };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 2. Legacy figma-scanner.config.json
|
|
284
|
+
const legacyCfg = path.resolve(cwd, 'figma-scanner.config.json');
|
|
285
|
+
if (fs.existsSync(legacyCfg)) {
|
|
286
|
+
const custom = JSON.parse(fs.readFileSync(legacyCfg, 'utf-8'));
|
|
287
|
+
return { ...DEFAULT_CONFIG, ...custom };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 3. Auto-detect from .storybook/main.{ts,js,mjs}
|
|
291
|
+
const storybookPaths = detectStorybookPaths(cwd);
|
|
292
|
+
if (storybookPaths.length > 0) {
|
|
293
|
+
return { ...DEFAULT_CONFIG, componentPaths: storybookPaths };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// 4. Fallback: scan all of src/
|
|
297
|
+
return DEFAULT_CONFIG;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function buildIconRegistry(imports: Record<string, IconImportSpec>): Promise<Record<string, IconRegistryEntry>> {
|
|
301
|
+
const entries: Record<string, IconRegistryEntry> = {};
|
|
302
|
+
const names = Object.keys(imports);
|
|
303
|
+
for (const localName of names) {
|
|
304
|
+
const spec = imports[localName];
|
|
305
|
+
if (!spec) continue;
|
|
306
|
+
try {
|
|
307
|
+
const mod = await import(spec.module);
|
|
308
|
+
const IconComponent = (mod as any)[spec.exportName];
|
|
309
|
+
if (!IconComponent) continue;
|
|
310
|
+
const element = React.createElement(IconComponent, { size: 24, color: '#000' });
|
|
311
|
+
let svg = renderToStaticMarkup(element);
|
|
312
|
+
if (!svg.startsWith('<svg')) {
|
|
313
|
+
svg = `<svg>${svg}</svg>`;
|
|
314
|
+
}
|
|
315
|
+
if (!/viewBox=/.test(svg)) {
|
|
316
|
+
svg = svg.replace('<svg', '<svg viewBox="0 0 24 24"');
|
|
317
|
+
}
|
|
318
|
+
if (!/width=/.test(svg)) {
|
|
319
|
+
svg = svg.replace('<svg', '<svg width="24"');
|
|
320
|
+
}
|
|
321
|
+
if (!/height=/.test(svg)) {
|
|
322
|
+
svg = svg.replace('<svg', '<svg height="24"');
|
|
323
|
+
}
|
|
324
|
+
entries[localName] = { ...spec, svg };
|
|
325
|
+
} catch (err) {
|
|
326
|
+
console.warn(`Failed to render icon ${spec.module}:${spec.exportName}`, err);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return entries;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Run
|
|
333
|
+
main().catch(err => {
|
|
334
|
+
console.error(err);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
});
|