@fragments-sdk/cli 0.7.0 → 0.7.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/dist/bin.js +245 -245
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-XHUDJNN3.js → chunk-32VIEOQY.js} +18 -18
- package/dist/chunk-32VIEOQY.js.map +1 -0
- package/dist/{chunk-CVXKXVOY.js → chunk-5ITIP3ES.js} +27 -27
- package/dist/chunk-5ITIP3ES.js.map +1 -0
- package/dist/{chunk-RVRTRESS.js → chunk-DQHWLAUV.js} +29 -29
- package/dist/chunk-DQHWLAUV.js.map +1 -0
- package/dist/{chunk-TJ34N7C7.js → chunk-GCZMFLDI.js} +30 -32
- package/dist/chunk-GCZMFLDI.js.map +1 -0
- package/dist/{chunk-6JBGU74P.js → chunk-GHYYFAQN.js} +23 -23
- package/dist/chunk-GHYYFAQN.js.map +1 -0
- package/dist/{chunk-NWQ4CJOQ.js → chunk-GKX2HPZ6.js} +40 -40
- package/dist/chunk-GKX2HPZ6.js.map +1 -0
- package/dist/{chunk-7OPWMLOE.js → chunk-U6VTHBNI.js} +110 -110
- package/dist/chunk-U6VTHBNI.js.map +1 -0
- package/dist/{core-W2HYIQW6.js → core-SFHPYR5H.js} +24 -26
- package/dist/{generate-LMTISDIJ.js → generate-54GJAWUY.js} +5 -5
- package/dist/generate-54GJAWUY.js.map +1 -0
- package/dist/index.d.ts +23 -27
- package/dist/index.js +10 -10
- package/dist/{init-7CHRKQ7P.js → init-EIM5WNMP.js} +5 -5
- package/dist/{init-7CHRKQ7P.js.map → init-EIM5WNMP.js.map} +1 -1
- package/dist/mcp-bin.js +73 -73
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-KQBKUS64.js +12 -0
- package/dist/{service-T2L7VLTE.js → service-ED2LNCTU.js} +6 -6
- package/dist/{static-viewer-GBR7YNF3.js → static-viewer-Q4F4QP5M.js} +4 -4
- package/dist/{test-OJRXNDO2.js → test-6VN2DA3S.js} +19 -19
- package/dist/test-6VN2DA3S.js.map +1 -0
- package/dist/{tokens-3BWDESVM.js → tokens-P2B7ZAM3.js} +5 -5
- package/dist/{viewer-SUFOISZM.js → viewer-GM7IQPPB.js} +199 -199
- package/dist/viewer-GM7IQPPB.js.map +1 -0
- package/package.json +2 -2
- package/src/ai.ts +5 -5
- package/src/analyze.ts +11 -11
- package/src/bin.ts +1 -1
- package/src/build.ts +33 -33
- package/src/commands/a11y.ts +6 -6
- package/src/commands/add.ts +11 -11
- package/src/commands/audit.ts +4 -4
- package/src/commands/baseline.ts +3 -3
- package/src/commands/build.ts +8 -8
- package/src/commands/compare.ts +20 -20
- package/src/commands/context.ts +16 -16
- package/src/commands/enhance.ts +36 -36
- package/src/commands/generate.ts +1 -1
- package/src/commands/graph.ts +3 -3
- package/src/commands/init.ts +1 -1
- package/src/commands/link/figma.ts +82 -82
- package/src/commands/link/index.ts +3 -3
- package/src/commands/link/storybook.ts +9 -9
- package/src/commands/list.ts +2 -2
- package/src/commands/reset.ts +15 -15
- package/src/commands/scan.ts +27 -27
- package/src/commands/storygen.ts +24 -24
- package/src/commands/validate.ts +2 -2
- package/src/commands/verify.ts +8 -8
- package/src/core/auto-props.ts +4 -4
- package/src/core/composition.test.ts +36 -36
- package/src/core/composition.ts +19 -19
- package/src/core/config.ts +6 -6
- package/src/core/{defineSegment.ts → defineFragment.ts} +16 -22
- package/src/core/discovery.ts +6 -6
- package/src/core/figma.ts +2 -2
- package/src/core/graph-extractor.test.ts +77 -77
- package/src/core/graph-extractor.ts +32 -32
- package/src/core/importAnalyzer.ts +1 -1
- package/src/core/index.ts +22 -23
- package/src/core/loader.ts +22 -22
- package/src/core/node.ts +5 -5
- package/src/core/parser.ts +31 -31
- package/src/core/previewLoader.ts +1 -1
- package/src/core/schema.ts +16 -16
- package/src/core/storyAdapter.test.ts +87 -87
- package/src/core/storyAdapter.ts +16 -16
- package/src/core/types.ts +21 -26
- package/src/diff.ts +22 -22
- package/src/index.ts +2 -2
- package/src/mcp/server.ts +80 -80
- package/src/migrate/__tests__/utils/utils.test.ts +3 -3
- package/src/migrate/bin.ts +4 -4
- package/src/migrate/converter.ts +16 -16
- package/src/migrate/index.ts +3 -3
- package/src/migrate/migrate.ts +3 -3
- package/src/migrate/parser.ts +8 -8
- package/src/migrate/report.ts +2 -2
- package/src/migrate/types.ts +4 -4
- package/src/screenshot.ts +22 -22
- package/src/service/__tests__/props-extractor.test.ts +15 -15
- package/src/service/analytics.ts +39 -39
- package/src/service/enhance/codebase-scanner.ts +1 -1
- package/src/service/enhance/index.ts +1 -1
- package/src/service/enhance/props-extractor.ts +2 -2
- package/src/service/enhance/types.ts +2 -2
- package/src/service/index.ts +2 -2
- package/src/service/metrics-store.ts +1 -1
- package/src/service/patch-generator.ts +1 -1
- package/src/setup.ts +52 -52
- package/src/shared/dev-server-client.ts +7 -7
- package/src/shared/fragment-loader.ts +59 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/types.ts +4 -4
- package/src/static-viewer.ts +35 -35
- package/src/test/discovery.ts +6 -6
- package/src/test/index.ts +5 -5
- package/src/test/reporters/console.ts +1 -1
- package/src/test/reporters/junit.ts +1 -1
- package/src/test/runner.ts +7 -7
- package/src/test/types.ts +3 -3
- package/src/test/watch.ts +9 -9
- package/src/validators.ts +26 -26
- package/src/viewer/__tests__/render-utils.test.ts +28 -28
- package/src/viewer/__tests__/viewer-integration.test.ts +4 -4
- package/src/viewer/cli/health.ts +26 -26
- package/src/viewer/components/App.tsx +79 -79
- package/src/viewer/components/BottomPanel.tsx +17 -17
- package/src/viewer/components/CodePanel.tsx +3 -3
- package/src/viewer/components/CommandPalette.tsx +11 -11
- package/src/viewer/components/ComponentGraph.tsx +28 -28
- package/src/viewer/components/ComponentHeader.tsx +2 -2
- package/src/viewer/components/ContractPanel.tsx +6 -6
- package/src/viewer/components/FigmaEmbed.tsx +9 -9
- package/src/viewer/components/HealthDashboard.tsx +17 -17
- package/src/viewer/components/InteractionsPanel.tsx +2 -2
- package/src/viewer/components/IsolatedPreviewFrame.tsx +6 -6
- package/src/viewer/components/IsolatedRender.tsx +10 -10
- package/src/viewer/components/LeftSidebar.tsx +28 -28
- package/src/viewer/components/MultiViewportPreview.tsx +14 -14
- package/src/viewer/components/PreviewArea.tsx +11 -11
- package/src/viewer/components/PreviewFrameHost.tsx +51 -51
- package/src/viewer/components/RightSidebar.tsx +9 -9
- package/src/viewer/components/Sidebar.tsx +17 -17
- package/src/viewer/components/StoryRenderer.tsx +2 -2
- package/src/viewer/components/TokenStylePanel.tsx +1 -1
- package/src/viewer/components/UsageSection.tsx +2 -2
- package/src/viewer/components/VariantMatrix.tsx +11 -11
- package/src/viewer/components/VariantRenderer.tsx +3 -3
- package/src/viewer/components/VariantTabs.tsx +2 -2
- package/src/viewer/components/_future/CreatePage.tsx +6 -6
- package/src/viewer/composition-renderer.ts +11 -11
- package/src/viewer/entry.tsx +40 -40
- package/src/viewer/hooks/useFigmaIntegration.ts +1 -1
- package/src/viewer/hooks/usePreviewBridge.ts +5 -5
- package/src/viewer/hooks/useUrlState.ts +6 -6
- package/src/viewer/index.ts +2 -2
- package/src/viewer/intelligence/healthReport.ts +17 -17
- package/src/viewer/intelligence/styleDrift.ts +1 -1
- package/src/viewer/intelligence/usageScanner.ts +1 -1
- package/src/viewer/render-template.html +1 -1
- package/src/viewer/render-utils.ts +21 -21
- package/src/viewer/server.ts +18 -18
- package/src/viewer/utils/detectRelationships.ts +22 -22
- package/src/viewer/vite-plugin.ts +213 -213
- package/dist/chunk-6JBGU74P.js.map +0 -1
- package/dist/chunk-7OPWMLOE.js.map +0 -1
- package/dist/chunk-CVXKXVOY.js.map +0 -1
- package/dist/chunk-NWQ4CJOQ.js.map +0 -1
- package/dist/chunk-RVRTRESS.js.map +0 -1
- package/dist/chunk-TJ34N7C7.js.map +0 -1
- package/dist/chunk-XHUDJNN3.js.map +0 -1
- package/dist/generate-LMTISDIJ.js.map +0 -1
- package/dist/scan-WY23TJCP.js +0 -12
- package/dist/test-OJRXNDO2.js.map +0 -1
- package/dist/viewer-SUFOISZM.js.map +0 -1
- package/src/shared/segment-loader.ts +0 -59
- /package/dist/{core-W2HYIQW6.js.map → core-SFHPYR5H.js.map} +0 -0
- /package/dist/{scan-WY23TJCP.js.map → scan-KQBKUS64.js.map} +0 -0
- /package/dist/{service-T2L7VLTE.js.map → service-ED2LNCTU.js.map} +0 -0
- /package/dist/{static-viewer-GBR7YNF3.js.map → static-viewer-Q4F4QP5M.js.map} +0 -0
- /package/dist/{tokens-3BWDESVM.js.map → tokens-P2B7ZAM3.js.map} +0 -0
package/src/commands/enhance.ts
CHANGED
|
@@ -71,9 +71,9 @@ export interface EnhanceOptions {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Enhanced
|
|
74
|
+
* Enhanced fragment data
|
|
75
75
|
*/
|
|
76
|
-
export interface
|
|
76
|
+
export interface EnhancedFragment {
|
|
77
77
|
componentName: string;
|
|
78
78
|
added: {
|
|
79
79
|
when: string[];
|
|
@@ -90,7 +90,7 @@ export interface EnhancedSegment {
|
|
|
90
90
|
*/
|
|
91
91
|
export interface EnhanceResult {
|
|
92
92
|
success: boolean;
|
|
93
|
-
enhanced:
|
|
93
|
+
enhanced: EnhancedFragment[];
|
|
94
94
|
totalTokens: number;
|
|
95
95
|
estimatedCost: number;
|
|
96
96
|
/** Context output for IDE AI mode */
|
|
@@ -211,9 +211,9 @@ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResu
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
// Search entire root dir, not just src/
|
|
214
|
-
const
|
|
214
|
+
const fragmentFiles = await findFragmentFiles(rootDir);
|
|
215
215
|
|
|
216
|
-
if (
|
|
216
|
+
if (fragmentFiles.length === 0) {
|
|
217
217
|
const msg = 'No fragment files found';
|
|
218
218
|
if (format === 'json') {
|
|
219
219
|
console.log(JSON.stringify({ success: false, error: msg }));
|
|
@@ -224,7 +224,7 @@ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResu
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
if (isInteractive) {
|
|
227
|
-
console.log(pc.green(` Found ${
|
|
227
|
+
console.log(pc.green(` Found ${fragmentFiles.length} fragment files`));
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
// Filter components if specified
|
|
@@ -232,7 +232,7 @@ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResu
|
|
|
232
232
|
if (component && component !== 'all') {
|
|
233
233
|
componentsToEnhance = [component];
|
|
234
234
|
} else {
|
|
235
|
-
componentsToEnhance =
|
|
235
|
+
componentsToEnhance = fragmentFiles.map(f => extractComponentName(f));
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
// Phase 4: Extract props from TypeScript source files
|
|
@@ -242,16 +242,16 @@ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResu
|
|
|
242
242
|
|
|
243
243
|
const propsExtractions = new Map<string, PropsExtractionResult>();
|
|
244
244
|
for (const compName of componentsToEnhance) {
|
|
245
|
-
const
|
|
246
|
-
if (!
|
|
245
|
+
const fragmentFile = fragmentFiles.find(f => extractComponentName(f) === compName);
|
|
246
|
+
if (!fragmentFile) continue;
|
|
247
247
|
|
|
248
|
-
// Try to find the component source file relative to the
|
|
249
|
-
const
|
|
248
|
+
// Try to find the component source file relative to the fragment file
|
|
249
|
+
const fragmentDir = fragmentFile.replace(/\.fragment\.(tsx?|jsx?)$/, '');
|
|
250
250
|
const possiblePaths = [
|
|
251
|
-
`${
|
|
252
|
-
`${
|
|
253
|
-
`${
|
|
254
|
-
`${
|
|
251
|
+
`${fragmentDir}.tsx`,
|
|
252
|
+
`${fragmentDir}.ts`,
|
|
253
|
+
`${fragmentDir}/index.tsx`,
|
|
254
|
+
`${fragmentDir}/index.ts`,
|
|
255
255
|
join(rootDir, 'src', 'components', `${compName}.tsx`),
|
|
256
256
|
join(rootDir, 'src', 'components', compName, `${compName}.tsx`),
|
|
257
257
|
join(rootDir, 'src', 'components', compName, 'index.tsx'),
|
|
@@ -332,14 +332,14 @@ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResu
|
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
// Build contexts for all components
|
|
335
|
-
const contexts: Array<{ name: string; context: ComponentContext;
|
|
335
|
+
const contexts: Array<{ name: string; context: ComponentContext; fragmentFile: string }> = [];
|
|
336
336
|
for (const compName of componentsToEnhance) {
|
|
337
337
|
const analysis = usageAnalysis.components[compName];
|
|
338
338
|
const stories = storyFiles.get(compName);
|
|
339
|
-
const
|
|
339
|
+
const fragmentFile = fragmentFiles.find(f => extractComponentName(f) === compName);
|
|
340
340
|
const propsExtraction = propsExtractions.get(compName);
|
|
341
341
|
|
|
342
|
-
if (!
|
|
342
|
+
if (!fragmentFile) continue;
|
|
343
343
|
|
|
344
344
|
const context = generateComponentContext(
|
|
345
345
|
compName,
|
|
@@ -348,7 +348,7 @@ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResu
|
|
|
348
348
|
stories,
|
|
349
349
|
propsExtraction
|
|
350
350
|
);
|
|
351
|
-
contexts.push({ name: compName, context,
|
|
351
|
+
contexts.push({ name: compName, context, fragmentFile });
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
// Context-only mode: output prompts for IDE AI
|
|
@@ -361,13 +361,13 @@ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResu
|
|
|
361
361
|
console.log(pc.dim(`\nPhase 6: Generating AI enhancements for ${componentsToEnhance.length} component(s)...\n`));
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
const enhanced:
|
|
364
|
+
const enhanced: EnhancedFragment[] = [];
|
|
365
365
|
let totalTokens = 0;
|
|
366
366
|
|
|
367
367
|
// Initialize AI client
|
|
368
368
|
const aiClient = await createAIClient(provider, apiKey!);
|
|
369
369
|
|
|
370
|
-
for (const { name: compName, context,
|
|
370
|
+
for (const { name: compName, context, fragmentFile } of contexts) {
|
|
371
371
|
// Check if we have enough data
|
|
372
372
|
if (!context.usageAnalysis || context.usageAnalysis.totalUsages < 2) {
|
|
373
373
|
enhanced.push({
|
|
@@ -426,7 +426,7 @@ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResu
|
|
|
426
426
|
// Phase 7: Apply changes
|
|
427
427
|
if (!dryRun) {
|
|
428
428
|
if (isInteractive) {
|
|
429
|
-
console.log(pc.dim('\nPhase 7: Updating
|
|
429
|
+
console.log(pc.dim('\nPhase 7: Updating fragment files...'));
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
for (const result of enhanced) {
|
|
@@ -434,17 +434,17 @@ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResu
|
|
|
434
434
|
continue;
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
const
|
|
438
|
-
if (!
|
|
437
|
+
const fragmentFile = fragmentFiles.find(f => extractComponentName(f) === result.componentName);
|
|
438
|
+
if (!fragmentFile) continue;
|
|
439
439
|
|
|
440
440
|
try {
|
|
441
|
-
await
|
|
441
|
+
await updateFragmentFile(fragmentFile, result.added);
|
|
442
442
|
if (isInteractive) {
|
|
443
|
-
console.log(pc.green(` Updated: ${relative(rootDir,
|
|
443
|
+
console.log(pc.green(` Updated: ${relative(rootDir, fragmentFile)}`));
|
|
444
444
|
}
|
|
445
445
|
} catch {
|
|
446
446
|
if (isInteractive) {
|
|
447
|
-
console.log(pc.red(` Failed to update: ${relative(rootDir,
|
|
447
|
+
console.log(pc.red(` Failed to update: ${relative(rootDir, fragmentFile)}`));
|
|
448
448
|
}
|
|
449
449
|
}
|
|
450
450
|
}
|
|
@@ -488,7 +488,7 @@ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResu
|
|
|
488
488
|
* Handle context-only mode for IDE AI
|
|
489
489
|
*/
|
|
490
490
|
function handleContextOnlyMode(
|
|
491
|
-
contexts: Array<{ name: string; context: ComponentContext;
|
|
491
|
+
contexts: Array<{ name: string; context: ComponentContext; fragmentFile: string }>,
|
|
492
492
|
format: string,
|
|
493
493
|
isInteractive: boolean
|
|
494
494
|
): EnhanceResult {
|
|
@@ -544,7 +544,7 @@ For each component, provide your response in JSON format:
|
|
|
544
544
|
console.log(pc.dim('─'.repeat(60)));
|
|
545
545
|
console.log();
|
|
546
546
|
console.log(pc.green('Tip: In Cursor, press Cmd+L to open chat and paste this prompt.'));
|
|
547
|
-
console.log(pc.dim('After getting suggestions, manually update your
|
|
547
|
+
console.log(pc.dim('After getting suggestions, manually update your fragment files.'));
|
|
548
548
|
console.log();
|
|
549
549
|
}
|
|
550
550
|
|
|
@@ -783,11 +783,11 @@ function calculateCost(provider: AIProvider, tokens: number): number {
|
|
|
783
783
|
}
|
|
784
784
|
|
|
785
785
|
/**
|
|
786
|
-
* Find all
|
|
786
|
+
* Find all fragment files in a directory
|
|
787
787
|
*/
|
|
788
|
-
async function
|
|
788
|
+
async function findFragmentFiles(dir: string): Promise<string[]> {
|
|
789
789
|
const fg = await import('fast-glob');
|
|
790
|
-
return fg.default(['**/*.
|
|
790
|
+
return fg.default(['**/*.fragment.tsx', '**/*.fragment.ts'], {
|
|
791
791
|
cwd: dir,
|
|
792
792
|
absolute: true,
|
|
793
793
|
ignore: ['**/node_modules/**', '**/dist/**'],
|
|
@@ -795,17 +795,17 @@ async function findSegmentFiles(dir: string): Promise<string[]> {
|
|
|
795
795
|
}
|
|
796
796
|
|
|
797
797
|
/**
|
|
798
|
-
* Extract component name from
|
|
798
|
+
* Extract component name from fragment file path
|
|
799
799
|
*/
|
|
800
800
|
function extractComponentName(filePath: string): string {
|
|
801
|
-
const match = filePath.match(/([^/\\]+)\.
|
|
801
|
+
const match = filePath.match(/([^/\\]+)\.fragment\.(tsx?|jsx?)$/);
|
|
802
802
|
return match ? match[1] : '';
|
|
803
803
|
}
|
|
804
804
|
|
|
805
805
|
/**
|
|
806
|
-
* Update a
|
|
806
|
+
* Update a fragment file with new when/whenNot suggestions
|
|
807
807
|
*/
|
|
808
|
-
async function
|
|
808
|
+
async function updateFragmentFile(
|
|
809
809
|
filePath: string,
|
|
810
810
|
suggestions: { when: string[]; whenNot: string[] }
|
|
811
811
|
): Promise<void> {
|
package/src/commands/generate.ts
CHANGED
package/src/commands/graph.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import pc from 'picocolors';
|
|
9
9
|
import { readFile } from 'node:fs/promises';
|
|
10
10
|
import { resolve } from 'node:path';
|
|
11
|
-
import type {
|
|
11
|
+
import type { CompiledFragmentsFile } from '../core/index.js';
|
|
12
12
|
import { BRAND } from '../core/index.js';
|
|
13
13
|
import { loadConfig } from '../core/node.js';
|
|
14
14
|
import {
|
|
@@ -33,10 +33,10 @@ export async function graph(
|
|
|
33
33
|
const { config, configDir } = await loadConfig(options.config);
|
|
34
34
|
const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
|
|
35
35
|
|
|
36
|
-
let data:
|
|
36
|
+
let data: CompiledFragmentsFile;
|
|
37
37
|
try {
|
|
38
38
|
const content = await readFile(outputPath, 'utf-8');
|
|
39
|
-
data = JSON.parse(content) as
|
|
39
|
+
data = JSON.parse(content) as CompiledFragmentsFile;
|
|
40
40
|
} catch {
|
|
41
41
|
console.error(
|
|
42
42
|
pc.red(`Error: Could not load ${BRAND.outFile}. Run \`${BRAND.cliCommand} build\` first.`),
|
package/src/commands/init.ts
CHANGED
|
@@ -428,7 +428,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
428
428
|
// Note: Stories are loaded separately by the viewer, not via include patterns
|
|
429
429
|
const includePaths: string[] = [
|
|
430
430
|
`${componentPath}/**/*.fragment.tsx`,
|
|
431
|
-
`${componentPath}/**/*.
|
|
431
|
+
`${componentPath}/**/*.fragment.tsx`, // Legacy support
|
|
432
432
|
];
|
|
433
433
|
|
|
434
434
|
// Create config file
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* fragments link figma - Link Figma components to
|
|
2
|
+
* fragments link figma - Link Figma components to fragments
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { relative } from 'node:path';
|
|
7
7
|
import pc from 'picocolors';
|
|
8
8
|
import { BRAND } from '../../core/index.js';
|
|
9
|
-
import { loadConfig,
|
|
9
|
+
import { loadConfig, discoverFragmentFiles } from '../../core/node.js';
|
|
10
10
|
import { FigmaClient } from '../../service/index.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -33,29 +33,29 @@ export interface LinkFigmaResult {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* Fragment variant info
|
|
37
37
|
*/
|
|
38
|
-
interface
|
|
38
|
+
interface FragmentVariantInfo {
|
|
39
39
|
name: string;
|
|
40
40
|
hasFigma: boolean;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Fragment info
|
|
45
45
|
*/
|
|
46
|
-
interface
|
|
46
|
+
interface FragmentInfo {
|
|
47
47
|
name: string;
|
|
48
48
|
filePath: string;
|
|
49
49
|
relativePath: string;
|
|
50
50
|
hasFigma: boolean;
|
|
51
|
-
variants:
|
|
51
|
+
variants: FragmentVariantInfo[];
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Match result
|
|
56
56
|
*/
|
|
57
57
|
interface Match {
|
|
58
|
-
|
|
58
|
+
fragment: FragmentInfo;
|
|
59
59
|
figmaComponent: {
|
|
60
60
|
name: string;
|
|
61
61
|
description: string;
|
|
@@ -145,20 +145,20 @@ export async function linkFigma(
|
|
|
145
145
|
console.log();
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
// Discover local
|
|
149
|
-
const
|
|
148
|
+
// Discover local fragments
|
|
149
|
+
const fragmentFiles = await discoverFragmentFiles(config, configDir);
|
|
150
150
|
|
|
151
|
-
if (
|
|
152
|
-
console.log(pc.yellow('No
|
|
151
|
+
if (fragmentFiles.length === 0) {
|
|
152
|
+
console.log(pc.yellow('No fragment files found in codebase.'));
|
|
153
153
|
console.log(pc.dim(`Looking for: ${config.include.join(', ')}`));
|
|
154
154
|
process.exit(0);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
console.log(pc.dim(`Found ${
|
|
157
|
+
console.log(pc.dim(`Found ${fragmentFiles.length} fragment file(s)\n`));
|
|
158
158
|
|
|
159
|
-
// Load
|
|
160
|
-
const
|
|
161
|
-
for (const file of
|
|
159
|
+
// Load fragments to get names
|
|
160
|
+
const fragments: FragmentInfo[] = [];
|
|
161
|
+
for (const file of fragmentFiles) {
|
|
162
162
|
try {
|
|
163
163
|
const content = await readFile(file.absolutePath, 'utf-8');
|
|
164
164
|
// Extract name from meta.name in the file
|
|
@@ -167,15 +167,15 @@ export async function linkFigma(
|
|
|
167
167
|
const hasFigma = /meta:\s*\{[^}]*figma:\s*['"]https?:/.test(content);
|
|
168
168
|
|
|
169
169
|
// Extract variant names and their figma status
|
|
170
|
-
const
|
|
170
|
+
const fragmentVariants = extractVariants(content, nameMatch?.[1]);
|
|
171
171
|
|
|
172
172
|
if (nameMatch) {
|
|
173
|
-
|
|
173
|
+
fragments.push({
|
|
174
174
|
name: nameMatch[1],
|
|
175
175
|
filePath: file.absolutePath,
|
|
176
176
|
relativePath: file.relativePath,
|
|
177
177
|
hasFigma,
|
|
178
|
-
variants:
|
|
178
|
+
variants: fragmentVariants,
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
} catch {
|
|
@@ -185,15 +185,15 @@ export async function linkFigma(
|
|
|
185
185
|
|
|
186
186
|
// Find matches
|
|
187
187
|
const matches: Match[] = [];
|
|
188
|
-
const
|
|
188
|
+
const unmatchedFragments: FragmentInfo[] = [];
|
|
189
189
|
|
|
190
|
-
for (const
|
|
190
|
+
for (const fragment of fragments) {
|
|
191
191
|
// Find best matching Figma component
|
|
192
192
|
let bestMatch: typeof allFigmaComponents[0] | null = null;
|
|
193
193
|
let bestScore = 0;
|
|
194
194
|
|
|
195
195
|
for (const figmaComp of allFigmaComponents) {
|
|
196
|
-
const score = calculateMatchScore(
|
|
196
|
+
const score = calculateMatchScore(fragment.name, figmaComp.name);
|
|
197
197
|
|
|
198
198
|
if (score > bestScore) {
|
|
199
199
|
bestMatch = figmaComp;
|
|
@@ -206,19 +206,19 @@ export async function linkFigma(
|
|
|
206
206
|
|
|
207
207
|
// Accept matches with 65%+ score
|
|
208
208
|
if (bestMatch && bestScore >= 65) {
|
|
209
|
-
const alreadyLinked =
|
|
209
|
+
const alreadyLinked = fragment.hasFigma;
|
|
210
210
|
if (alreadyLinked && !auto) {
|
|
211
|
-
console.log(pc.dim(`⏭️ ${
|
|
211
|
+
console.log(pc.dim(`⏭️ ${fragment.name} (already linked)`));
|
|
212
212
|
}
|
|
213
|
-
matches.push({
|
|
213
|
+
matches.push({ fragment, figmaComponent: bestMatch, score: bestScore, alreadyLinked });
|
|
214
214
|
} else {
|
|
215
|
-
|
|
215
|
+
unmatchedFragments.push(fragment);
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
if (
|
|
220
|
-
console.log(pc.dim('Unmatched
|
|
221
|
-
for (const seg of
|
|
219
|
+
if (unmatchedFragments.length > 0) {
|
|
220
|
+
console.log(pc.dim('Unmatched fragments:'));
|
|
221
|
+
for (const seg of unmatchedFragments) {
|
|
222
222
|
console.log(` ${pc.dim('•')} ${seg.name}`);
|
|
223
223
|
}
|
|
224
224
|
console.log();
|
|
@@ -230,7 +230,7 @@ export async function linkFigma(
|
|
|
230
230
|
|
|
231
231
|
if (matches.length === 0) {
|
|
232
232
|
console.log(pc.yellow('\nNo automatic matches found.'));
|
|
233
|
-
console.log(pc.dim('You can manually add figma URLs to your
|
|
233
|
+
console.log(pc.dim('You can manually add figma URLs to your fragment definitions.'));
|
|
234
234
|
process.exit(0);
|
|
235
235
|
}
|
|
236
236
|
|
|
@@ -241,7 +241,7 @@ export async function linkFigma(
|
|
|
241
241
|
for (const match of newMatches) {
|
|
242
242
|
const scoreColor = match.score === 100 ? pc.green : pc.yellow;
|
|
243
243
|
console.log(
|
|
244
|
-
` ${pc.green('✓')} ${pc.bold(match.
|
|
244
|
+
` ${pc.green('✓')} ${pc.bold(match.fragment.name)} → ${match.figmaComponent.name} ${scoreColor(`(${Math.round(match.score)}%)`)}`
|
|
245
245
|
);
|
|
246
246
|
}
|
|
247
247
|
}
|
|
@@ -259,7 +259,7 @@ export async function linkFigma(
|
|
|
259
259
|
for (const match of newMatches) {
|
|
260
260
|
const scoreColor = match.score === 100 ? pc.green : pc.yellow;
|
|
261
261
|
console.log(
|
|
262
|
-
` ${pc.green('✓')} ${pc.bold(match.
|
|
262
|
+
` ${pc.green('✓')} ${pc.bold(match.fragment.name)} → ${match.figmaComponent.name} ${scoreColor(`(${Math.round(match.score)}%)`)}`
|
|
263
263
|
);
|
|
264
264
|
}
|
|
265
265
|
} else {
|
|
@@ -272,7 +272,7 @@ export async function linkFigma(
|
|
|
272
272
|
const choices = newMatches.map((match) => {
|
|
273
273
|
const scoreColor = match.score === 100 ? pc.green : pc.yellow;
|
|
274
274
|
return {
|
|
275
|
-
name: `${pc.bold(match.
|
|
275
|
+
name: `${pc.bold(match.fragment.name)} → ${match.figmaComponent.name} ${scoreColor(`(${Math.round(match.score)}%)`)}`,
|
|
276
276
|
value: match,
|
|
277
277
|
checked: true,
|
|
278
278
|
};
|
|
@@ -295,13 +295,13 @@ export async function linkFigma(
|
|
|
295
295
|
// Include already-linked matches for variant linking
|
|
296
296
|
const allSelectedMatches = [...selectedMatches, ...alreadyLinkedMatches];
|
|
297
297
|
|
|
298
|
-
// Update
|
|
298
|
+
// Update fragment files (only for new matches, not already-linked)
|
|
299
299
|
let updated = 0;
|
|
300
300
|
for (const match of selectedMatches) {
|
|
301
301
|
if (match.alreadyLinked) continue;
|
|
302
302
|
|
|
303
303
|
try {
|
|
304
|
-
let content = await readFile(match.
|
|
304
|
+
let content = await readFile(match.fragment.filePath, 'utf-8');
|
|
305
305
|
const figmaUrlToInsert = figmaClient.buildNodeUrl(
|
|
306
306
|
match.figmaComponent.file_key,
|
|
307
307
|
match.figmaComponent.node_id,
|
|
@@ -323,16 +323,16 @@ export async function linkFigma(
|
|
|
323
323
|
);
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
await writeFile(match.
|
|
326
|
+
await writeFile(match.fragment.filePath, content);
|
|
327
327
|
updated++;
|
|
328
|
-
console.log(` ${pc.green('✓')} Updated ${match.
|
|
328
|
+
console.log(` ${pc.green('✓')} Updated ${match.fragment.relativePath}`);
|
|
329
329
|
} catch (error) {
|
|
330
|
-
console.log(` ${pc.red('✗')} Failed to update ${match.
|
|
330
|
+
console.log(` ${pc.red('✗')} Failed to update ${match.fragment.relativePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
if (updated > 0) {
|
|
335
|
-
console.log(pc.green(`\n✓ Updated ${updated}
|
|
335
|
+
console.log(pc.green(`\n✓ Updated ${updated} fragment file(s)\n`));
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
// Variant linking
|
|
@@ -357,10 +357,10 @@ export async function linkFigma(
|
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
/**
|
|
360
|
-
* Extract variants from
|
|
360
|
+
* Extract variants from fragment file content
|
|
361
361
|
*/
|
|
362
|
-
function extractVariants(content: string, componentName?: string):
|
|
363
|
-
const variants:
|
|
362
|
+
function extractVariants(content: string, componentName?: string): FragmentVariantInfo[] {
|
|
363
|
+
const variants: FragmentVariantInfo[] = [];
|
|
364
364
|
|
|
365
365
|
// Find variants: [ ... ] section
|
|
366
366
|
const variantsArrayMatch = content.match(/variants:\s*\[/);
|
|
@@ -411,27 +411,27 @@ function extractVariants(content: string, componentName?: string): SegmentVarian
|
|
|
411
411
|
/**
|
|
412
412
|
* Calculate match score between two names
|
|
413
413
|
*/
|
|
414
|
-
function calculateMatchScore(
|
|
414
|
+
function calculateMatchScore(fragmentName: string, figmaName: string): number {
|
|
415
415
|
const normalizeForMatch = (s: string) =>
|
|
416
416
|
s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
417
417
|
|
|
418
|
-
const
|
|
418
|
+
const normalizedFragment = normalizeForMatch(fragmentName);
|
|
419
419
|
const normalizedFigma = normalizeForMatch(figmaName);
|
|
420
420
|
|
|
421
421
|
// Exact match after normalization
|
|
422
|
-
if (
|
|
422
|
+
if (normalizedFragment === normalizedFigma) {
|
|
423
423
|
return 100;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
-
// Check if
|
|
427
|
-
if (normalizedFigma.startsWith(
|
|
428
|
-
const coverage =
|
|
426
|
+
// Check if fragment name appears at the START of figma name
|
|
427
|
+
if (normalizedFigma.startsWith(normalizedFragment)) {
|
|
428
|
+
const coverage = normalizedFragment.length / normalizedFigma.length;
|
|
429
429
|
return Math.max(85, coverage * 100);
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
-
// Check if figma name appears at the START of
|
|
433
|
-
if (
|
|
434
|
-
const coverage = normalizedFigma.length /
|
|
432
|
+
// Check if figma name appears at the START of fragment name
|
|
433
|
+
if (normalizedFragment.startsWith(normalizedFigma)) {
|
|
434
|
+
const coverage = normalizedFigma.length / normalizedFragment.length;
|
|
435
435
|
return Math.max(80, coverage * 100);
|
|
436
436
|
}
|
|
437
437
|
|
|
@@ -445,24 +445,24 @@ function calculateMatchScore(segmentName: string, figmaName: string): number {
|
|
|
445
445
|
.filter((w) => w.length > 0);
|
|
446
446
|
};
|
|
447
447
|
|
|
448
|
-
const
|
|
448
|
+
const fragmentWords = getWords(fragmentName);
|
|
449
449
|
const figmaWords = getWords(figmaName);
|
|
450
450
|
|
|
451
|
-
// Check if all
|
|
452
|
-
const
|
|
451
|
+
// Check if all fragment words appear in figma words
|
|
452
|
+
const allFragmentWordsInFigma = fragmentWords.every((sw) =>
|
|
453
453
|
figmaWords.some((fw) => fw === sw || fw.startsWith(sw) || sw.startsWith(fw))
|
|
454
454
|
);
|
|
455
455
|
|
|
456
|
-
if (
|
|
457
|
-
const wordOverlap =
|
|
456
|
+
if (allFragmentWordsInFigma && fragmentWords.length > 0) {
|
|
457
|
+
const wordOverlap = fragmentWords.length / Math.max(fragmentWords.length, figmaWords.length);
|
|
458
458
|
return Math.max(75, wordOverlap * 95);
|
|
459
459
|
}
|
|
460
460
|
|
|
461
461
|
// Partial containment
|
|
462
|
-
if (normalizedFigma.includes(
|
|
462
|
+
if (normalizedFigma.includes(normalizedFragment)) {
|
|
463
463
|
return 70;
|
|
464
464
|
}
|
|
465
|
-
if (
|
|
465
|
+
if (normalizedFragment.includes(normalizedFigma)) {
|
|
466
466
|
return 65;
|
|
467
467
|
}
|
|
468
468
|
|
|
@@ -510,16 +510,16 @@ async function linkVariants(
|
|
|
510
510
|
continue;
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
-
// Match
|
|
514
|
-
const
|
|
515
|
-
if (
|
|
516
|
-
console.log(pc.dim(` ⏭️ ${match.
|
|
513
|
+
// Match fragment variants to Figma variants
|
|
514
|
+
const fragmentVariants = match.fragment.variants.filter((v) => !v.hasFigma);
|
|
515
|
+
if (fragmentVariants.length === 0) {
|
|
516
|
+
console.log(pc.dim(` ⏭️ ${match.fragment.name}: all variants already linked`));
|
|
517
517
|
continue;
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
-
console.log(pc.dim(` ${match.
|
|
520
|
+
console.log(pc.dim(` ${match.fragment.name}: ${csWithVariants.variants.length} Figma variants`));
|
|
521
521
|
|
|
522
|
-
for (const
|
|
522
|
+
for (const fragmentVariant of fragmentVariants) {
|
|
523
523
|
// Find matching Figma variants by score
|
|
524
524
|
const variantMatches: Array<{
|
|
525
525
|
figmaVariant: typeof csWithVariants.variants[0];
|
|
@@ -527,18 +527,18 @@ async function linkVariants(
|
|
|
527
527
|
}> = [];
|
|
528
528
|
|
|
529
529
|
for (const fv of csWithVariants.variants) {
|
|
530
|
-
// Check if any property value matches the
|
|
531
|
-
const
|
|
530
|
+
// Check if any property value matches the fragment variant name
|
|
531
|
+
const normalizedFragment = normalizeForMatch(fragmentVariant.name);
|
|
532
532
|
|
|
533
533
|
for (const value of fv.values) {
|
|
534
534
|
const normalizedValue = normalizeForMatch(value);
|
|
535
535
|
|
|
536
|
-
if (
|
|
536
|
+
if (normalizedFragment === normalizedValue) {
|
|
537
537
|
variantMatches.push({ figmaVariant: fv, score: 100 });
|
|
538
538
|
break;
|
|
539
|
-
} else if (normalizedValue.includes(
|
|
539
|
+
} else if (normalizedValue.includes(normalizedFragment)) {
|
|
540
540
|
variantMatches.push({ figmaVariant: fv, score: 85 });
|
|
541
|
-
} else if (
|
|
541
|
+
} else if (normalizedFragment.includes(normalizedValue)) {
|
|
542
542
|
variantMatches.push({ figmaVariant: fv, score: 75 });
|
|
543
543
|
}
|
|
544
544
|
}
|
|
@@ -557,11 +557,11 @@ async function linkVariants(
|
|
|
557
557
|
);
|
|
558
558
|
|
|
559
559
|
try {
|
|
560
|
-
let content = await readFile(match.
|
|
560
|
+
let content = await readFile(match.fragment.filePath, 'utf-8');
|
|
561
561
|
|
|
562
562
|
// Add figma URL after the variant's name field
|
|
563
563
|
const namePattern = new RegExp(
|
|
564
|
-
`(name:\\s*['"]${escapeRegExp(
|
|
564
|
+
`(name:\\s*['"]${escapeRegExp(fragmentVariant.name)}['"],?)`,
|
|
565
565
|
'g'
|
|
566
566
|
);
|
|
567
567
|
|
|
@@ -572,14 +572,14 @@ async function linkVariants(
|
|
|
572
572
|
return `${matchedStr}\n figma: '${variantUrl}',`;
|
|
573
573
|
});
|
|
574
574
|
|
|
575
|
-
await writeFile(match.
|
|
575
|
+
await writeFile(match.fragment.filePath, content);
|
|
576
576
|
variantUpdates++;
|
|
577
577
|
console.log(
|
|
578
|
-
` ${pc.green('✓')} ${
|
|
578
|
+
` ${pc.green('✓')} ${fragmentVariant.name} → ${bestMatch.figmaVariant.name}`
|
|
579
579
|
);
|
|
580
580
|
} catch (error) {
|
|
581
581
|
console.log(
|
|
582
|
-
` ${pc.red('✗')} ${
|
|
582
|
+
` ${pc.red('✗')} ${fragmentVariant.name}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
583
583
|
);
|
|
584
584
|
}
|
|
585
585
|
} else if (variantMatches.length > 0) {
|
|
@@ -594,7 +594,7 @@ async function linkVariants(
|
|
|
594
594
|
|
|
595
595
|
try {
|
|
596
596
|
const selectedVariant = await select({
|
|
597
|
-
message: ` Match for "${
|
|
597
|
+
message: ` Match for "${fragmentVariant.name}":`,
|
|
598
598
|
choices,
|
|
599
599
|
});
|
|
600
600
|
|
|
@@ -605,9 +605,9 @@ async function linkVariants(
|
|
|
605
605
|
figmaData.fileName
|
|
606
606
|
);
|
|
607
607
|
|
|
608
|
-
let content = await readFile(match.
|
|
608
|
+
let content = await readFile(match.fragment.filePath, 'utf-8');
|
|
609
609
|
const namePattern = new RegExp(
|
|
610
|
-
`(name:\\s*['"]${escapeRegExp(
|
|
610
|
+
`(name:\\s*['"]${escapeRegExp(fragmentVariant.name)}['"],?)`,
|
|
611
611
|
'g'
|
|
612
612
|
);
|
|
613
613
|
|
|
@@ -618,19 +618,19 @@ async function linkVariants(
|
|
|
618
618
|
return `${matchedStr}\n figma: '${variantUrl}',`;
|
|
619
619
|
});
|
|
620
620
|
|
|
621
|
-
await writeFile(match.
|
|
621
|
+
await writeFile(match.fragment.filePath, content);
|
|
622
622
|
variantUpdates++;
|
|
623
623
|
console.log(
|
|
624
|
-
` ${pc.green('✓')} ${
|
|
624
|
+
` ${pc.green('✓')} ${fragmentVariant.name} → ${selectedVariant.name}`
|
|
625
625
|
);
|
|
626
626
|
} else {
|
|
627
|
-
console.log(` ${pc.dim('⏭️')} ${
|
|
627
|
+
console.log(` ${pc.dim('⏭️')} ${fragmentVariant.name} (skipped)`);
|
|
628
628
|
}
|
|
629
629
|
} catch {
|
|
630
|
-
console.log(` ${pc.dim('⏭️')} ${
|
|
630
|
+
console.log(` ${pc.dim('⏭️')} ${fragmentVariant.name} (cancelled)`);
|
|
631
631
|
}
|
|
632
632
|
} else {
|
|
633
|
-
console.log(` ${pc.yellow('?')} ${
|
|
633
|
+
console.log(` ${pc.yellow('?')} ${fragmentVariant.name}: no matching Figma variant`);
|
|
634
634
|
}
|
|
635
635
|
}
|
|
636
636
|
}
|