@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,645 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fragments link figma - Link Figma components to segments
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { relative } from 'node:path';
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
import { BRAND } from '../../core/index.js';
|
|
9
|
+
import { loadConfig, discoverSegmentFiles } from '../../core/node.js';
|
|
10
|
+
import { FigmaClient } from '../../service/index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for link figma command
|
|
14
|
+
*/
|
|
15
|
+
export interface LinkFigmaOptions {
|
|
16
|
+
/** Path to config file */
|
|
17
|
+
config?: string;
|
|
18
|
+
/** Auto-link matching components without prompts */
|
|
19
|
+
auto?: boolean;
|
|
20
|
+
/** Show matches without updating files */
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
/** Link individual variants to their Figma frames */
|
|
23
|
+
variants?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Result of link figma command
|
|
28
|
+
*/
|
|
29
|
+
export interface LinkFigmaResult {
|
|
30
|
+
success: boolean;
|
|
31
|
+
updated: number;
|
|
32
|
+
variantUpdates: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Segment variant info
|
|
37
|
+
*/
|
|
38
|
+
interface SegmentVariantInfo {
|
|
39
|
+
name: string;
|
|
40
|
+
hasFigma: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Segment info
|
|
45
|
+
*/
|
|
46
|
+
interface SegmentInfo {
|
|
47
|
+
name: string;
|
|
48
|
+
filePath: string;
|
|
49
|
+
relativePath: string;
|
|
50
|
+
hasFigma: boolean;
|
|
51
|
+
variants: SegmentVariantInfo[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Match result
|
|
56
|
+
*/
|
|
57
|
+
interface Match {
|
|
58
|
+
segment: SegmentInfo;
|
|
59
|
+
figmaComponent: {
|
|
60
|
+
name: string;
|
|
61
|
+
description: string;
|
|
62
|
+
key: string;
|
|
63
|
+
file_key: string;
|
|
64
|
+
node_id: string;
|
|
65
|
+
};
|
|
66
|
+
score: number;
|
|
67
|
+
alreadyLinked?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Run the link figma command
|
|
72
|
+
*/
|
|
73
|
+
export async function linkFigma(
|
|
74
|
+
figmaUrl: string | undefined,
|
|
75
|
+
options: LinkFigmaOptions = {}
|
|
76
|
+
): Promise<LinkFigmaResult> {
|
|
77
|
+
const { config: configPath, auto = false, dryRun = false, variants = true } = options;
|
|
78
|
+
|
|
79
|
+
// Check for Figma access token
|
|
80
|
+
if (!process.env.FIGMA_ACCESS_TOKEN) {
|
|
81
|
+
console.error(pc.red('\nFIGMA_ACCESS_TOKEN environment variable required.'));
|
|
82
|
+
console.log(pc.dim('Generate at: https://www.figma.com/developers/api#access-tokens'));
|
|
83
|
+
console.log(pc.dim(' export FIGMA_ACCESS_TOKEN=figd_xxx'));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(pc.cyan(`\n${BRAND.name} Link Wizard\n`));
|
|
88
|
+
|
|
89
|
+
// Load config to check for figmaFile
|
|
90
|
+
const { config, configDir } = await loadConfig(configPath);
|
|
91
|
+
|
|
92
|
+
// Get Figma URL: CLI arg > config > interactive prompt
|
|
93
|
+
let fileUrl = figmaUrl || config.figmaFile;
|
|
94
|
+
|
|
95
|
+
if (!fileUrl) {
|
|
96
|
+
const { input } = await import('@inquirer/prompts');
|
|
97
|
+
|
|
98
|
+
console.log(pc.dim('Tip: Add `figmaFile` to fragments.config.ts to skip this prompt\n'));
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
fileUrl = await input({
|
|
102
|
+
message: 'Enter Figma file URL:',
|
|
103
|
+
validate: (value) => {
|
|
104
|
+
if (!value.trim()) return 'URL is required';
|
|
105
|
+
if (!value.includes('figma.com')) return 'Please enter a valid Figma URL';
|
|
106
|
+
return true;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
} catch {
|
|
110
|
+
// User cancelled (Ctrl+C)
|
|
111
|
+
console.log(pc.dim('\nNo changes made.'));
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
} else if (config.figmaFile && !figmaUrl) {
|
|
115
|
+
console.log(pc.dim(`Using Figma file from config: ${config.figmaFile}\n`));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Import FigmaClient
|
|
119
|
+
const figmaClient = new FigmaClient({ accessToken: process.env.FIGMA_ACCESS_TOKEN });
|
|
120
|
+
|
|
121
|
+
// Parse the URL to get file key
|
|
122
|
+
console.log(pc.dim('Parsing Figma URL...'));
|
|
123
|
+
const { fileKey } = figmaClient.parseFileUrl(fileUrl);
|
|
124
|
+
|
|
125
|
+
// Fetch components from Figma
|
|
126
|
+
console.log(pc.dim('Fetching components from Figma...'));
|
|
127
|
+
const figmaData = await figmaClient.getFileComponents(fileKey);
|
|
128
|
+
|
|
129
|
+
// Use component sets (main components) preferentially
|
|
130
|
+
const allFigmaComponents = figmaData.componentSets.length > 0
|
|
131
|
+
? figmaData.componentSets
|
|
132
|
+
: figmaData.components;
|
|
133
|
+
|
|
134
|
+
if (allFigmaComponents.length === 0) {
|
|
135
|
+
console.log(pc.yellow('\nNo components found in Figma file.'));
|
|
136
|
+
console.log(pc.dim('Make sure the file contains published components.'));
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const componentType = figmaData.componentSets.length > 0 ? 'component set' : 'component';
|
|
141
|
+
console.log(pc.green(`✓ Found ${allFigmaComponents.length} Figma ${componentType}(s) in "${figmaData.fileName}"`));
|
|
142
|
+
if (figmaData.components.length > 0 && figmaData.componentSets.length > 0) {
|
|
143
|
+
console.log(pc.dim(` (${figmaData.components.length} individual components also available)\n`));
|
|
144
|
+
} else {
|
|
145
|
+
console.log();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Discover local segments
|
|
149
|
+
const segmentFiles = await discoverSegmentFiles(config, configDir);
|
|
150
|
+
|
|
151
|
+
if (segmentFiles.length === 0) {
|
|
152
|
+
console.log(pc.yellow('No segment files found in codebase.'));
|
|
153
|
+
console.log(pc.dim(`Looking for: ${config.include.join(', ')}`));
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(pc.dim(`Found ${segmentFiles.length} segment file(s)\n`));
|
|
158
|
+
|
|
159
|
+
// Load segments to get names
|
|
160
|
+
const segments: SegmentInfo[] = [];
|
|
161
|
+
for (const file of segmentFiles) {
|
|
162
|
+
try {
|
|
163
|
+
const content = await readFile(file.absolutePath, 'utf-8');
|
|
164
|
+
// Extract name from meta.name in the file
|
|
165
|
+
const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
|
|
166
|
+
// Check if figma is already set in meta
|
|
167
|
+
const hasFigma = /meta:\s*\{[^}]*figma:\s*['"]https?:/.test(content);
|
|
168
|
+
|
|
169
|
+
// Extract variant names and their figma status
|
|
170
|
+
const segmentVariants = extractVariants(content, nameMatch?.[1]);
|
|
171
|
+
|
|
172
|
+
if (nameMatch) {
|
|
173
|
+
segments.push({
|
|
174
|
+
name: nameMatch[1],
|
|
175
|
+
filePath: file.absolutePath,
|
|
176
|
+
relativePath: file.relativePath,
|
|
177
|
+
hasFigma,
|
|
178
|
+
variants: segmentVariants,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Skip files that can't be read
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Find matches
|
|
187
|
+
const matches: Match[] = [];
|
|
188
|
+
const unmatchedSegments: SegmentInfo[] = [];
|
|
189
|
+
|
|
190
|
+
for (const segment of segments) {
|
|
191
|
+
// Find best matching Figma component
|
|
192
|
+
let bestMatch: typeof allFigmaComponents[0] | null = null;
|
|
193
|
+
let bestScore = 0;
|
|
194
|
+
|
|
195
|
+
for (const figmaComp of allFigmaComponents) {
|
|
196
|
+
const score = calculateMatchScore(segment.name, figmaComp.name);
|
|
197
|
+
|
|
198
|
+
if (score > bestScore) {
|
|
199
|
+
bestMatch = figmaComp;
|
|
200
|
+
bestScore = score;
|
|
201
|
+
|
|
202
|
+
// Perfect match, no need to continue
|
|
203
|
+
if (score === 100) break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Accept matches with 65%+ score
|
|
208
|
+
if (bestMatch && bestScore >= 65) {
|
|
209
|
+
const alreadyLinked = segment.hasFigma;
|
|
210
|
+
if (alreadyLinked && !auto) {
|
|
211
|
+
console.log(pc.dim(`⏭️ ${segment.name} (already linked)`));
|
|
212
|
+
}
|
|
213
|
+
matches.push({ segment, figmaComponent: bestMatch, score: bestScore, alreadyLinked });
|
|
214
|
+
} else {
|
|
215
|
+
unmatchedSegments.push(segment);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (unmatchedSegments.length > 0) {
|
|
220
|
+
console.log(pc.dim('Unmatched segments:'));
|
|
221
|
+
for (const seg of unmatchedSegments) {
|
|
222
|
+
console.log(` ${pc.dim('•')} ${seg.name}`);
|
|
223
|
+
}
|
|
224
|
+
console.log();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Split matches into new and already-linked
|
|
228
|
+
const newMatches = matches.filter((m) => !m.alreadyLinked);
|
|
229
|
+
const alreadyLinkedMatches = matches.filter((m) => m.alreadyLinked);
|
|
230
|
+
|
|
231
|
+
if (matches.length === 0) {
|
|
232
|
+
console.log(pc.yellow('\nNo automatic matches found.'));
|
|
233
|
+
console.log(pc.dim('You can manually add figma URLs to your segment definitions.'));
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Dry run - just show what would be done
|
|
238
|
+
if (dryRun) {
|
|
239
|
+
if (newMatches.length > 0) {
|
|
240
|
+
console.log(pc.bold('\nMatched Components:\n'));
|
|
241
|
+
for (const match of newMatches) {
|
|
242
|
+
const scoreColor = match.score === 100 ? pc.green : pc.yellow;
|
|
243
|
+
console.log(
|
|
244
|
+
` ${pc.green('✓')} ${pc.bold(match.segment.name)} → ${match.figmaComponent.name} ${scoreColor(`(${Math.round(match.score)}%)`)}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
console.log(pc.yellow('\n[Dry run - no files were updated]'));
|
|
249
|
+
return { success: true, updated: 0, variantUpdates: 0 };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Interactive selection or auto mode (only for new components)
|
|
253
|
+
let selectedMatches: Match[] = newMatches;
|
|
254
|
+
|
|
255
|
+
if (newMatches.length > 0) {
|
|
256
|
+
if (auto) {
|
|
257
|
+
// Auto mode - show matches and proceed
|
|
258
|
+
console.log(pc.bold('\nMatched Components:\n'));
|
|
259
|
+
for (const match of newMatches) {
|
|
260
|
+
const scoreColor = match.score === 100 ? pc.green : pc.yellow;
|
|
261
|
+
console.log(
|
|
262
|
+
` ${pc.green('✓')} ${pc.bold(match.segment.name)} → ${match.figmaComponent.name} ${scoreColor(`(${Math.round(match.score)}%)`)}`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
// Interactive selection with checkbox
|
|
267
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
268
|
+
|
|
269
|
+
console.log(pc.bold('\nMatched Components:\n'));
|
|
270
|
+
console.log(pc.dim('Use ↑/↓ to navigate, Space to toggle, Enter to confirm\n'));
|
|
271
|
+
|
|
272
|
+
const choices = newMatches.map((match) => {
|
|
273
|
+
const scoreColor = match.score === 100 ? pc.green : pc.yellow;
|
|
274
|
+
return {
|
|
275
|
+
name: `${pc.bold(match.segment.name)} → ${match.figmaComponent.name} ${scoreColor(`(${Math.round(match.score)}%)`)}`,
|
|
276
|
+
value: match,
|
|
277
|
+
checked: true,
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
selectedMatches = await checkbox({
|
|
283
|
+
message: 'Select components to link:',
|
|
284
|
+
choices,
|
|
285
|
+
pageSize: 20,
|
|
286
|
+
});
|
|
287
|
+
} catch {
|
|
288
|
+
// User cancelled (Ctrl+C)
|
|
289
|
+
console.log(pc.dim('\nNo changes made.'));
|
|
290
|
+
return { success: true, updated: 0, variantUpdates: 0 };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Include already-linked matches for variant linking
|
|
296
|
+
const allSelectedMatches = [...selectedMatches, ...alreadyLinkedMatches];
|
|
297
|
+
|
|
298
|
+
// Update segment files (only for new matches, not already-linked)
|
|
299
|
+
let updated = 0;
|
|
300
|
+
for (const match of selectedMatches) {
|
|
301
|
+
if (match.alreadyLinked) continue;
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
let content = await readFile(match.segment.filePath, 'utf-8');
|
|
305
|
+
const figmaUrlToInsert = figmaClient.buildNodeUrl(
|
|
306
|
+
match.figmaComponent.file_key,
|
|
307
|
+
match.figmaComponent.node_id,
|
|
308
|
+
figmaData.fileName
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Check if figma field already exists in meta
|
|
312
|
+
if (/meta:\s*\{[^}]*figma:/.test(content)) {
|
|
313
|
+
// Replace existing figma URL
|
|
314
|
+
content = content.replace(
|
|
315
|
+
/(meta:\s*\{[^}]*figma:\s*['"])([^'"]*)['"]/,
|
|
316
|
+
`$1${figmaUrlToInsert}'`
|
|
317
|
+
);
|
|
318
|
+
} else {
|
|
319
|
+
// Add figma field after category in meta
|
|
320
|
+
content = content.replace(
|
|
321
|
+
/(meta:\s*\{[^}]*category:\s*['"][^'"]*['"],?)/,
|
|
322
|
+
`$1\n figma: '${figmaUrlToInsert}',`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await writeFile(match.segment.filePath, content);
|
|
327
|
+
updated++;
|
|
328
|
+
console.log(` ${pc.green('✓')} Updated ${match.segment.relativePath}`);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.log(` ${pc.red('✗')} Failed to update ${match.segment.relativePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (updated > 0) {
|
|
335
|
+
console.log(pc.green(`\n✓ Updated ${updated} segment file(s)\n`));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Variant linking
|
|
339
|
+
let variantUpdates = 0;
|
|
340
|
+
if (variants && figmaData.componentSets.length > 0) {
|
|
341
|
+
variantUpdates = await linkVariants(
|
|
342
|
+
allSelectedMatches,
|
|
343
|
+
figmaData,
|
|
344
|
+
figmaClient,
|
|
345
|
+
fileKey
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Next steps
|
|
350
|
+
console.log(pc.dim('───────────────────────────────────────'));
|
|
351
|
+
console.log(pc.bold('\nNext steps:'));
|
|
352
|
+
console.log(` 1. Run ${pc.cyan(`${BRAND.cliCommand} compare --all`)} to verify designs`);
|
|
353
|
+
console.log(` 2. Add figmaProps mappings for prop-level linking`);
|
|
354
|
+
console.log();
|
|
355
|
+
|
|
356
|
+
return { success: true, updated, variantUpdates };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Extract variants from segment file content
|
|
361
|
+
*/
|
|
362
|
+
function extractVariants(content: string, componentName?: string): SegmentVariantInfo[] {
|
|
363
|
+
const variants: SegmentVariantInfo[] = [];
|
|
364
|
+
|
|
365
|
+
// Find variants: [ ... ] section
|
|
366
|
+
const variantsArrayMatch = content.match(/variants:\s*\[/);
|
|
367
|
+
if (variantsArrayMatch && variantsArrayMatch.index !== undefined) {
|
|
368
|
+
const variantsStart = variantsArrayMatch.index + variantsArrayMatch[0].length;
|
|
369
|
+
// Find matching closing bracket
|
|
370
|
+
let bracketCount = 1;
|
|
371
|
+
let variantsEnd = variantsStart;
|
|
372
|
+
while (bracketCount > 0 && variantsEnd < content.length) {
|
|
373
|
+
if (content[variantsEnd] === '[') bracketCount++;
|
|
374
|
+
if (content[variantsEnd] === ']') bracketCount--;
|
|
375
|
+
variantsEnd++;
|
|
376
|
+
}
|
|
377
|
+
const variantsSection = content.slice(variantsStart, variantsEnd - 1);
|
|
378
|
+
|
|
379
|
+
// Find all variant objects within the variants array
|
|
380
|
+
const variantNameRegex = /\{\s*\n?\s*name:\s*['"]([^'"]+)['"]/g;
|
|
381
|
+
let variantMatch;
|
|
382
|
+
while ((variantMatch = variantNameRegex.exec(variantsSection)) !== null) {
|
|
383
|
+
const variantName = variantMatch[1];
|
|
384
|
+
// Skip if this matches the component name
|
|
385
|
+
if (componentName && variantName === componentName) continue;
|
|
386
|
+
|
|
387
|
+
// Find the extent of this variant object
|
|
388
|
+
const objectStart = variantMatch.index;
|
|
389
|
+
let braceCount = 1;
|
|
390
|
+
let objectEnd = objectStart + 1;
|
|
391
|
+
while (braceCount > 0 && objectEnd < variantsSection.length) {
|
|
392
|
+
if (variantsSection[objectEnd] === '{') braceCount++;
|
|
393
|
+
if (variantsSection[objectEnd] === '}') braceCount--;
|
|
394
|
+
objectEnd++;
|
|
395
|
+
}
|
|
396
|
+
const objectContent = variantsSection.slice(objectStart, objectEnd);
|
|
397
|
+
|
|
398
|
+
// Check if this variant has a figma URL
|
|
399
|
+
const variantHasFigma = /figma:\s*['"]https?:/.test(objectContent);
|
|
400
|
+
|
|
401
|
+
variants.push({
|
|
402
|
+
name: variantName,
|
|
403
|
+
hasFigma: variantHasFigma,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return variants;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Calculate match score between two names
|
|
413
|
+
*/
|
|
414
|
+
function calculateMatchScore(segmentName: string, figmaName: string): number {
|
|
415
|
+
const normalizeForMatch = (s: string) =>
|
|
416
|
+
s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
417
|
+
|
|
418
|
+
const normalizedSegment = normalizeForMatch(segmentName);
|
|
419
|
+
const normalizedFigma = normalizeForMatch(figmaName);
|
|
420
|
+
|
|
421
|
+
// Exact match after normalization
|
|
422
|
+
if (normalizedSegment === normalizedFigma) {
|
|
423
|
+
return 100;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Check if segment name appears at the START of figma name
|
|
427
|
+
if (normalizedFigma.startsWith(normalizedSegment)) {
|
|
428
|
+
const coverage = normalizedSegment.length / normalizedFigma.length;
|
|
429
|
+
return Math.max(85, coverage * 100);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Check if figma name appears at the START of segment name
|
|
433
|
+
if (normalizedSegment.startsWith(normalizedFigma)) {
|
|
434
|
+
const coverage = normalizedFigma.length / normalizedSegment.length;
|
|
435
|
+
return Math.max(80, coverage * 100);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Word-based matching
|
|
439
|
+
const getWords = (s: string): string[] => {
|
|
440
|
+
return s
|
|
441
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
442
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
443
|
+
.toLowerCase()
|
|
444
|
+
.split(/\s+/)
|
|
445
|
+
.filter((w) => w.length > 0);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const segmentWords = getWords(segmentName);
|
|
449
|
+
const figmaWords = getWords(figmaName);
|
|
450
|
+
|
|
451
|
+
// Check if all segment words appear in figma words
|
|
452
|
+
const allSegmentWordsInFigma = segmentWords.every((sw) =>
|
|
453
|
+
figmaWords.some((fw) => fw === sw || fw.startsWith(sw) || sw.startsWith(fw))
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
if (allSegmentWordsInFigma && segmentWords.length > 0) {
|
|
457
|
+
const wordOverlap = segmentWords.length / Math.max(segmentWords.length, figmaWords.length);
|
|
458
|
+
return Math.max(75, wordOverlap * 95);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Partial containment
|
|
462
|
+
if (normalizedFigma.includes(normalizedSegment)) {
|
|
463
|
+
return 70;
|
|
464
|
+
}
|
|
465
|
+
if (normalizedSegment.includes(normalizedFigma)) {
|
|
466
|
+
return 65;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return 0;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Link variants to Figma frames
|
|
474
|
+
*/
|
|
475
|
+
async function linkVariants(
|
|
476
|
+
matches: Match[],
|
|
477
|
+
figmaData: any,
|
|
478
|
+
figmaClient: FigmaClient,
|
|
479
|
+
fileKey: string
|
|
480
|
+
): Promise<number> {
|
|
481
|
+
console.log(pc.bold('\nLinking variants...\n'));
|
|
482
|
+
|
|
483
|
+
// Fetch variants for matched component sets
|
|
484
|
+
const matchedComponentSets = matches
|
|
485
|
+
.map((m) => m.figmaComponent)
|
|
486
|
+
.filter((c) => figmaData.componentSets.some((cs: any) => cs.key === c.key));
|
|
487
|
+
|
|
488
|
+
if (matchedComponentSets.length === 0) {
|
|
489
|
+
return 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
console.log(pc.dim('Fetching Figma variants...'));
|
|
493
|
+
const componentSetVariants = await figmaClient.getComponentSetVariants(fileKey, matchedComponentSets);
|
|
494
|
+
|
|
495
|
+
let variantUpdates = 0;
|
|
496
|
+
const { select } = await import('@inquirer/prompts');
|
|
497
|
+
|
|
498
|
+
const normalizeForMatch = (s: string) =>
|
|
499
|
+
s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
500
|
+
|
|
501
|
+
const escapeRegExp = (s: string) =>
|
|
502
|
+
s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
503
|
+
|
|
504
|
+
for (const match of matches) {
|
|
505
|
+
const csWithVariants = componentSetVariants.find(
|
|
506
|
+
(cs) => cs.componentSet.key === match.figmaComponent.key
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
if (!csWithVariants || csWithVariants.variants.length === 0) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Match segment variants to Figma variants
|
|
514
|
+
const segmentVariants = match.segment.variants.filter((v) => !v.hasFigma);
|
|
515
|
+
if (segmentVariants.length === 0) {
|
|
516
|
+
console.log(pc.dim(` ⏭️ ${match.segment.name}: all variants already linked`));
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
console.log(pc.dim(` ${match.segment.name}: ${csWithVariants.variants.length} Figma variants`));
|
|
521
|
+
|
|
522
|
+
for (const segmentVariant of segmentVariants) {
|
|
523
|
+
// Find matching Figma variants by score
|
|
524
|
+
const variantMatches: Array<{
|
|
525
|
+
figmaVariant: typeof csWithVariants.variants[0];
|
|
526
|
+
score: number;
|
|
527
|
+
}> = [];
|
|
528
|
+
|
|
529
|
+
for (const fv of csWithVariants.variants) {
|
|
530
|
+
// Check if any property value matches the segment variant name
|
|
531
|
+
const normalizedSegment = normalizeForMatch(segmentVariant.name);
|
|
532
|
+
|
|
533
|
+
for (const value of fv.values) {
|
|
534
|
+
const normalizedValue = normalizeForMatch(value);
|
|
535
|
+
|
|
536
|
+
if (normalizedSegment === normalizedValue) {
|
|
537
|
+
variantMatches.push({ figmaVariant: fv, score: 100 });
|
|
538
|
+
break;
|
|
539
|
+
} else if (normalizedValue.includes(normalizedSegment)) {
|
|
540
|
+
variantMatches.push({ figmaVariant: fv, score: 85 });
|
|
541
|
+
} else if (normalizedSegment.includes(normalizedValue)) {
|
|
542
|
+
variantMatches.push({ figmaVariant: fv, score: 75 });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Sort by score
|
|
548
|
+
variantMatches.sort((a, b) => b.score - a.score);
|
|
549
|
+
|
|
550
|
+
// If perfect match, use it automatically
|
|
551
|
+
if (variantMatches.length > 0 && variantMatches[0].score === 100) {
|
|
552
|
+
const bestMatch = variantMatches[0];
|
|
553
|
+
const variantUrl = figmaClient.buildNodeUrl(
|
|
554
|
+
match.figmaComponent.file_key,
|
|
555
|
+
bestMatch.figmaVariant.node_id,
|
|
556
|
+
figmaData.fileName
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
try {
|
|
560
|
+
let content = await readFile(match.segment.filePath, 'utf-8');
|
|
561
|
+
|
|
562
|
+
// Add figma URL after the variant's name field
|
|
563
|
+
const namePattern = new RegExp(
|
|
564
|
+
`(name:\\s*['"]${escapeRegExp(segmentVariant.name)}['"],?)`,
|
|
565
|
+
'g'
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
let replaced = false;
|
|
569
|
+
content = content.replace(namePattern, (matchedStr) => {
|
|
570
|
+
if (replaced) return matchedStr;
|
|
571
|
+
replaced = true;
|
|
572
|
+
return `${matchedStr}\n figma: '${variantUrl}',`;
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
await writeFile(match.segment.filePath, content);
|
|
576
|
+
variantUpdates++;
|
|
577
|
+
console.log(
|
|
578
|
+
` ${pc.green('✓')} ${segmentVariant.name} → ${bestMatch.figmaVariant.name}`
|
|
579
|
+
);
|
|
580
|
+
} catch (error) {
|
|
581
|
+
console.log(
|
|
582
|
+
` ${pc.red('✗')} ${segmentVariant.name}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
} else if (variantMatches.length > 0) {
|
|
586
|
+
// Multiple possible matches - ask user
|
|
587
|
+
const choices = [
|
|
588
|
+
...variantMatches.slice(0, 4).map((m) => ({
|
|
589
|
+
name: `${m.figmaVariant.name} (${m.score}%)`,
|
|
590
|
+
value: m.figmaVariant,
|
|
591
|
+
})),
|
|
592
|
+
{ name: 'Skip this variant', value: null },
|
|
593
|
+
];
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
const selectedVariant = await select({
|
|
597
|
+
message: ` Match for "${segmentVariant.name}":`,
|
|
598
|
+
choices,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
if (selectedVariant) {
|
|
602
|
+
const variantUrl = figmaClient.buildNodeUrl(
|
|
603
|
+
match.figmaComponent.file_key,
|
|
604
|
+
selectedVariant.node_id,
|
|
605
|
+
figmaData.fileName
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
let content = await readFile(match.segment.filePath, 'utf-8');
|
|
609
|
+
const namePattern = new RegExp(
|
|
610
|
+
`(name:\\s*['"]${escapeRegExp(segmentVariant.name)}['"],?)`,
|
|
611
|
+
'g'
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
let replaced = false;
|
|
615
|
+
content = content.replace(namePattern, (matchedStr) => {
|
|
616
|
+
if (replaced) return matchedStr;
|
|
617
|
+
replaced = true;
|
|
618
|
+
return `${matchedStr}\n figma: '${variantUrl}',`;
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
await writeFile(match.segment.filePath, content);
|
|
622
|
+
variantUpdates++;
|
|
623
|
+
console.log(
|
|
624
|
+
` ${pc.green('✓')} ${segmentVariant.name} → ${selectedVariant.name}`
|
|
625
|
+
);
|
|
626
|
+
} else {
|
|
627
|
+
console.log(` ${pc.dim('⏭️')} ${segmentVariant.name} (skipped)`);
|
|
628
|
+
}
|
|
629
|
+
} catch {
|
|
630
|
+
console.log(` ${pc.dim('⏭️')} ${segmentVariant.name} (cancelled)`);
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
console.log(` ${pc.yellow('?')} ${segmentVariant.name}: no matching Figma variant`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (variantUpdates > 0) {
|
|
639
|
+
console.log(pc.green(`\n✓ Linked ${variantUpdates} variant(s)\n`));
|
|
640
|
+
} else {
|
|
641
|
+
console.log(pc.dim('\nNo variant updates made.\n'));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return variantUpdates;
|
|
645
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fragments link - Link external resources to segments
|
|
3
|
+
*
|
|
4
|
+
* This module provides subcommands for:
|
|
5
|
+
* - figma: Link Figma components to segments
|
|
6
|
+
* - storybook: Bootstrap segments from Storybook stories
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { linkFigma, type LinkFigmaOptions, type LinkFigmaResult } from './figma.js';
|
|
10
|
+
export { linkStorybook, type LinkStorybookOptions, type LinkStorybookResult } from './storybook.js';
|