@fragments-sdk/cli 0.2.2
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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- package/src/viewer/vite-plugin.ts +2143 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses CSS/SCSS files to extract CSS custom properties (design tokens).
|
|
5
|
+
* Handles:
|
|
6
|
+
* - CSS custom property declarations (--token-name: value)
|
|
7
|
+
* - Reference resolution (var(--other-token))
|
|
8
|
+
* - Theme detection via selectors
|
|
9
|
+
* - Category inference from naming conventions
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { resolve, relative } from "node:path";
|
|
14
|
+
import fastGlob from "fast-glob";
|
|
15
|
+
import type {
|
|
16
|
+
DesignToken,
|
|
17
|
+
TokenCategory,
|
|
18
|
+
TokenConfig,
|
|
19
|
+
TokenParseResult,
|
|
20
|
+
TokenParseError,
|
|
21
|
+
} from "../core/index.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pattern to match CSS custom property declarations
|
|
25
|
+
* Captures: [full match, property name, value]
|
|
26
|
+
* Example: "--color-primary: var(--color-cobalt-50);"
|
|
27
|
+
*/
|
|
28
|
+
const TOKEN_DECLARATION_PATTERN =
|
|
29
|
+
/--([a-zA-Z0-9_-]+)\s*:\s*([^;]+);/g;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pattern to match var() references
|
|
33
|
+
* Captures: [full match, token name, fallback value?]
|
|
34
|
+
* Example: "var(--color-cobalt-50)" or "var(--color-primary, #fff)"
|
|
35
|
+
*/
|
|
36
|
+
const VAR_REFERENCE_PATTERN =
|
|
37
|
+
/var\(\s*--([a-zA-Z0-9_-]+)(?:\s*,\s*([^)]+))?\s*\)/g;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pattern to match CSS selectors (to detect theme blocks)
|
|
41
|
+
*/
|
|
42
|
+
const SELECTOR_PATTERN = /([^{]+)\s*\{/g;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Category inference patterns - maps naming conventions to categories
|
|
46
|
+
*/
|
|
47
|
+
const CATEGORY_PATTERNS: Array<{
|
|
48
|
+
pattern: RegExp;
|
|
49
|
+
category: TokenCategory;
|
|
50
|
+
}> = [
|
|
51
|
+
{ pattern: /color|bg|background|border-color|fill|stroke/i, category: "color" },
|
|
52
|
+
{ pattern: /spacing|margin|padding|gap|space|inset/i, category: "spacing" },
|
|
53
|
+
{ pattern: /font|text|line-height|letter-spacing|typography/i, category: "typography" },
|
|
54
|
+
{ pattern: /radius|rounded|corner/i, category: "radius" },
|
|
55
|
+
{ pattern: /shadow|elevation/i, category: "shadow" },
|
|
56
|
+
{ pattern: /size|width|height|min|max/i, category: "sizing" },
|
|
57
|
+
{ pattern: /border(?!-color)|stroke-width|outline/i, category: "border" },
|
|
58
|
+
{ pattern: /animation|transition|duration|timing|delay/i, category: "animation" },
|
|
59
|
+
{ pattern: /z-index|layer|stack/i, category: "z-index" },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse a single CSS/SCSS file for tokens
|
|
64
|
+
*/
|
|
65
|
+
export async function parseTokenFile(
|
|
66
|
+
filePath: string,
|
|
67
|
+
themeSelectors: Record<string, string> = { ":root": "default" },
|
|
68
|
+
projectRoot?: string
|
|
69
|
+
): Promise<TokenParseResult> {
|
|
70
|
+
const startTime = performance.now();
|
|
71
|
+
const tokens: DesignToken[] = [];
|
|
72
|
+
const errors: TokenParseError[] = [];
|
|
73
|
+
const warnings: string[] = [];
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const content = await readFile(filePath, "utf-8");
|
|
77
|
+
const relativePath = projectRoot
|
|
78
|
+
? relative(projectRoot, filePath)
|
|
79
|
+
: filePath;
|
|
80
|
+
|
|
81
|
+
// Track which tokens we've seen for reference resolution
|
|
82
|
+
const tokensByName = new Map<string, { rawValue: string; line?: number }>();
|
|
83
|
+
|
|
84
|
+
// First pass: collect all token declarations
|
|
85
|
+
let lineNumber = 1;
|
|
86
|
+
const lines = content.split("\n");
|
|
87
|
+
|
|
88
|
+
// Track current selector/theme context
|
|
89
|
+
let currentSelector = ":root";
|
|
90
|
+
let braceDepth = 0;
|
|
91
|
+
const selectorStack: string[] = [];
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const line = lines[i];
|
|
95
|
+
lineNumber = i + 1;
|
|
96
|
+
|
|
97
|
+
// Track brace depth for selector context
|
|
98
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
99
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
100
|
+
|
|
101
|
+
// Check for selector at start of line (simple heuristic)
|
|
102
|
+
if (openBraces > closeBraces) {
|
|
103
|
+
// Entering a new block
|
|
104
|
+
const selectorMatch = line.match(/^\s*([^{]+)\s*\{/);
|
|
105
|
+
if (selectorMatch) {
|
|
106
|
+
selectorStack.push(selectorMatch[1].trim());
|
|
107
|
+
currentSelector = selectorStack[selectorStack.length - 1];
|
|
108
|
+
}
|
|
109
|
+
braceDepth += openBraces - closeBraces;
|
|
110
|
+
} else if (closeBraces > openBraces) {
|
|
111
|
+
braceDepth -= closeBraces - openBraces;
|
|
112
|
+
if (braceDepth >= 0 && selectorStack.length > 0) {
|
|
113
|
+
selectorStack.pop();
|
|
114
|
+
currentSelector = selectorStack.length > 0
|
|
115
|
+
? selectorStack[selectorStack.length - 1]
|
|
116
|
+
: ":root";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Find token declarations in this line
|
|
121
|
+
const tokenMatches = [...line.matchAll(TOKEN_DECLARATION_PATTERN)];
|
|
122
|
+
for (const match of tokenMatches) {
|
|
123
|
+
const [, name, rawValue] = match;
|
|
124
|
+
const fullName = `--${name}`;
|
|
125
|
+
tokensByName.set(fullName, { rawValue: rawValue.trim(), line: lineNumber });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Second pass: resolve references and build token objects
|
|
130
|
+
for (const [name, { rawValue, line }] of tokensByName) {
|
|
131
|
+
// Find the selector context for this token
|
|
132
|
+
// Re-parse to get correct selector (simplified - uses last found)
|
|
133
|
+
const selector = findSelectorForLine(content, line || 1);
|
|
134
|
+
const theme = themeSelectors[selector] || "default";
|
|
135
|
+
|
|
136
|
+
// Resolve the value
|
|
137
|
+
const { resolvedValue, chain, hasCircular, unresolvedRef } = resolveValue(
|
|
138
|
+
rawValue,
|
|
139
|
+
tokensByName
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (hasCircular) {
|
|
143
|
+
warnings.push(
|
|
144
|
+
`Circular reference detected for ${name} at line ${line}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (unresolvedRef) {
|
|
149
|
+
warnings.push(
|
|
150
|
+
`Unresolved reference in ${name}: ${unresolvedRef}`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Infer category from name
|
|
155
|
+
const category = inferCategory(name);
|
|
156
|
+
|
|
157
|
+
// Infer token level
|
|
158
|
+
const level = inferLevel(name, rawValue, chain);
|
|
159
|
+
|
|
160
|
+
// Extract description from preceding comment (if any)
|
|
161
|
+
const description = extractDescription(content, line || 1);
|
|
162
|
+
|
|
163
|
+
tokens.push({
|
|
164
|
+
name,
|
|
165
|
+
rawValue,
|
|
166
|
+
resolvedValue,
|
|
167
|
+
category,
|
|
168
|
+
level,
|
|
169
|
+
referenceChain: chain,
|
|
170
|
+
sourceFile: relativePath,
|
|
171
|
+
lineNumber: line,
|
|
172
|
+
theme,
|
|
173
|
+
selector,
|
|
174
|
+
description,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
errors.push({
|
|
179
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
180
|
+
file: filePath,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
tokens,
|
|
186
|
+
errors,
|
|
187
|
+
warnings,
|
|
188
|
+
parseTimeMs: performance.now() - startTime,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parse multiple files based on config
|
|
194
|
+
*/
|
|
195
|
+
export async function parseTokenFiles(
|
|
196
|
+
config: TokenConfig,
|
|
197
|
+
projectRoot: string
|
|
198
|
+
): Promise<TokenParseResult> {
|
|
199
|
+
const startTime = performance.now();
|
|
200
|
+
const allTokens: DesignToken[] = [];
|
|
201
|
+
const allErrors: TokenParseError[] = [];
|
|
202
|
+
const allWarnings: string[] = [];
|
|
203
|
+
|
|
204
|
+
// Discover files
|
|
205
|
+
const files = await fastGlob(config.include, {
|
|
206
|
+
cwd: projectRoot,
|
|
207
|
+
ignore: config.exclude || ["**/node_modules/**"],
|
|
208
|
+
absolute: true,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (files.length === 0) {
|
|
212
|
+
allWarnings.push(
|
|
213
|
+
`No token files found matching: ${config.include.join(", ")}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Parse each file
|
|
218
|
+
for (const file of files) {
|
|
219
|
+
const result = await parseTokenFile(
|
|
220
|
+
file,
|
|
221
|
+
config.themeSelectors,
|
|
222
|
+
projectRoot
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
allTokens.push(...result.tokens);
|
|
226
|
+
allErrors.push(...result.errors);
|
|
227
|
+
allWarnings.push(...result.warnings);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
tokens: allTokens,
|
|
232
|
+
errors: allErrors,
|
|
233
|
+
warnings: allWarnings,
|
|
234
|
+
parseTimeMs: performance.now() - startTime,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve a token value, following var() references
|
|
240
|
+
*/
|
|
241
|
+
function resolveValue(
|
|
242
|
+
rawValue: string,
|
|
243
|
+
tokensByName: Map<string, { rawValue: string; line?: number }>,
|
|
244
|
+
visited = new Set<string>()
|
|
245
|
+
): {
|
|
246
|
+
resolvedValue: string;
|
|
247
|
+
chain: string[];
|
|
248
|
+
hasCircular: boolean;
|
|
249
|
+
unresolvedRef?: string;
|
|
250
|
+
} {
|
|
251
|
+
const chain: string[] = [];
|
|
252
|
+
let current = rawValue;
|
|
253
|
+
let hasCircular = false;
|
|
254
|
+
let unresolvedRef: string | undefined;
|
|
255
|
+
|
|
256
|
+
// Maximum iterations to prevent infinite loops
|
|
257
|
+
const maxIterations = 20;
|
|
258
|
+
let iterations = 0;
|
|
259
|
+
|
|
260
|
+
while (iterations < maxIterations) {
|
|
261
|
+
iterations++;
|
|
262
|
+
|
|
263
|
+
// Check for var() reference
|
|
264
|
+
const varMatch = current.match(/var\(\s*--([a-zA-Z0-9_-]+)(?:\s*,\s*([^)]+))?\s*\)/);
|
|
265
|
+
|
|
266
|
+
if (!varMatch) {
|
|
267
|
+
// No more var() references, we have the resolved value
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const [, refName, fallback] = varMatch;
|
|
272
|
+
const fullRefName = `--${refName}`;
|
|
273
|
+
|
|
274
|
+
// Check for circular reference
|
|
275
|
+
if (visited.has(fullRefName)) {
|
|
276
|
+
hasCircular = true;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
visited.add(fullRefName);
|
|
281
|
+
chain.push(fullRefName);
|
|
282
|
+
|
|
283
|
+
// Look up the referenced token
|
|
284
|
+
const refToken = tokensByName.get(fullRefName);
|
|
285
|
+
|
|
286
|
+
if (refToken) {
|
|
287
|
+
// Replace var() with the referenced value
|
|
288
|
+
current = current.replace(
|
|
289
|
+
varMatch[0],
|
|
290
|
+
refToken.rawValue
|
|
291
|
+
);
|
|
292
|
+
} else if (fallback) {
|
|
293
|
+
// Use fallback value
|
|
294
|
+
current = current.replace(varMatch[0], fallback.trim());
|
|
295
|
+
} else {
|
|
296
|
+
// Unresolved reference
|
|
297
|
+
unresolvedRef = fullRefName;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
resolvedValue: normalizeValue(current.trim()),
|
|
304
|
+
chain,
|
|
305
|
+
hasCircular,
|
|
306
|
+
unresolvedRef,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Normalize a CSS value for consistent comparison
|
|
312
|
+
*/
|
|
313
|
+
function normalizeValue(value: string): string {
|
|
314
|
+
// Lowercase hex colors
|
|
315
|
+
value = value.replace(/#[0-9a-fA-F]+/g, (match) => match.toLowerCase());
|
|
316
|
+
|
|
317
|
+
// Normalize whitespace
|
|
318
|
+
value = value.replace(/\s+/g, " ").trim();
|
|
319
|
+
|
|
320
|
+
// Normalize rgb/rgba spacing
|
|
321
|
+
value = value.replace(
|
|
322
|
+
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/g,
|
|
323
|
+
(_, r, g, b, a) => a !== undefined ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Infer token category from name
|
|
331
|
+
*/
|
|
332
|
+
function inferCategory(name: string): TokenCategory {
|
|
333
|
+
const lowerName = name.toLowerCase();
|
|
334
|
+
|
|
335
|
+
for (const { pattern, category } of CATEGORY_PATTERNS) {
|
|
336
|
+
if (pattern.test(lowerName)) {
|
|
337
|
+
return category;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return "other";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Infer token level (1=base, 2=semantic, 3=component)
|
|
346
|
+
*/
|
|
347
|
+
function inferLevel(
|
|
348
|
+
name: string,
|
|
349
|
+
rawValue: string,
|
|
350
|
+
referenceChain: string[]
|
|
351
|
+
): 1 | 2 | 3 {
|
|
352
|
+
const lowerName = name.toLowerCase();
|
|
353
|
+
|
|
354
|
+
// Component-specific tokens (often contain component names)
|
|
355
|
+
if (
|
|
356
|
+
/btn|button|input|card|modal|dialog|menu|nav|header|footer|table|form/i.test(
|
|
357
|
+
lowerName
|
|
358
|
+
)
|
|
359
|
+
) {
|
|
360
|
+
return 3;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Semantic tokens (references other tokens)
|
|
364
|
+
if (referenceChain.length > 0) {
|
|
365
|
+
return 2;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Base tokens (raw values like hex colors, numbers)
|
|
369
|
+
if (
|
|
370
|
+
rawValue.match(/^#[0-9a-fA-F]+$/) ||
|
|
371
|
+
rawValue.match(/^\d+(\.\d+)?(px|rem|em|%|vh|vw)?$/)
|
|
372
|
+
) {
|
|
373
|
+
return 1;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Default to semantic if unclear
|
|
377
|
+
return 2;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Find the CSS selector that contains a given line
|
|
382
|
+
*/
|
|
383
|
+
function findSelectorForLine(content: string, targetLine: number): string {
|
|
384
|
+
const lines = content.split("\n");
|
|
385
|
+
let currentSelector = ":root";
|
|
386
|
+
let braceDepth = 0;
|
|
387
|
+
|
|
388
|
+
for (let i = 0; i < Math.min(targetLine, lines.length); i++) {
|
|
389
|
+
const line = lines[i];
|
|
390
|
+
|
|
391
|
+
// Check for selector at start of block
|
|
392
|
+
const selectorMatch = line.match(/^\s*([^{]+)\s*\{/);
|
|
393
|
+
if (selectorMatch) {
|
|
394
|
+
const selector = selectorMatch[1].trim();
|
|
395
|
+
// Only update if entering a new block
|
|
396
|
+
if ((line.match(/\{/g) || []).length > (line.match(/\}/g) || []).length) {
|
|
397
|
+
currentSelector = selector;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Track brace depth
|
|
402
|
+
braceDepth += (line.match(/\{/g) || []).length;
|
|
403
|
+
braceDepth -= (line.match(/\}/g) || []).length;
|
|
404
|
+
|
|
405
|
+
// If we close all braces, reset to root
|
|
406
|
+
if (braceDepth === 0) {
|
|
407
|
+
currentSelector = ":root";
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return currentSelector;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Extract description from comment preceding the token
|
|
416
|
+
*/
|
|
417
|
+
function extractDescription(content: string, line: number): string | undefined {
|
|
418
|
+
const lines = content.split("\n");
|
|
419
|
+
|
|
420
|
+
// Look at the line before
|
|
421
|
+
if (line <= 1) return undefined;
|
|
422
|
+
|
|
423
|
+
const prevLine = lines[line - 2]?.trim();
|
|
424
|
+
|
|
425
|
+
// Check for single-line comment
|
|
426
|
+
const singleLineMatch = prevLine?.match(/\/\/\s*(.+)$/);
|
|
427
|
+
if (singleLineMatch) {
|
|
428
|
+
return singleLineMatch[1].trim();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Check for multi-line comment ending
|
|
432
|
+
const multiLineMatch = prevLine?.match(/\*\s*(.+)\s*\*\//);
|
|
433
|
+
if (multiLineMatch) {
|
|
434
|
+
return multiLineMatch[1].trim();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Check for simple /* comment */
|
|
438
|
+
const inlineMatch = prevLine?.match(/\/\*\s*(.+)\s*\*\//);
|
|
439
|
+
if (inlineMatch) {
|
|
440
|
+
return inlineMatch[1].trim();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Convert a hex color to RGB
|
|
448
|
+
*/
|
|
449
|
+
export function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
|
|
450
|
+
const match = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
|
|
451
|
+
if (!match) return null;
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
r: parseInt(match[1], 16),
|
|
455
|
+
g: parseInt(match[2], 16),
|
|
456
|
+
b: parseInt(match[3], 16),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Convert RGB to hex
|
|
462
|
+
*/
|
|
463
|
+
export function rgbToHex(r: number, g: number, b: number): string {
|
|
464
|
+
return `#${[r, g, b]
|
|
465
|
+
.map((x) => Math.round(x).toString(16).padStart(2, "0"))
|
|
466
|
+
.join("")}`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Parse an RGB/RGBA string
|
|
471
|
+
*/
|
|
472
|
+
export function parseRgb(
|
|
473
|
+
color: string
|
|
474
|
+
): { r: number; g: number; b: number; a?: number } | null {
|
|
475
|
+
const match = color.match(
|
|
476
|
+
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/
|
|
477
|
+
);
|
|
478
|
+
if (!match) return null;
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
r: parseInt(match[1], 10),
|
|
482
|
+
g: parseInt(match[2], 10),
|
|
483
|
+
b: parseInt(match[3], 10),
|
|
484
|
+
a: match[4] ? parseFloat(match[4]) : undefined,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Normalize a color value to lowercase hex
|
|
490
|
+
*/
|
|
491
|
+
export function normalizeColor(color: string): string {
|
|
492
|
+
// Already hex
|
|
493
|
+
if (color.startsWith("#")) {
|
|
494
|
+
return color.toLowerCase();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// RGB/RGBA to hex
|
|
498
|
+
const rgb = parseRgb(color);
|
|
499
|
+
if (rgb) {
|
|
500
|
+
return rgbToHex(rgb.r, rgb.g, rgb.b);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return color.toLowerCase();
|
|
504
|
+
}
|