@fragments-sdk/cli 0.6.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 +529 -285
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-F7ITZPDJ.js → chunk-32VIEOQY.js} +18 -18
- package/dist/chunk-32VIEOQY.js.map +1 -0
- package/dist/{chunk-SSLQXHNX.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-Q7GOHVOK.js → chunk-GCZMFLDI.js} +67 -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-D35RGPAG.js → chunk-U6VTHBNI.js} +499 -83
- package/dist/chunk-U6VTHBNI.js.map +1 -0
- package/dist/{core-SKRPJQZG.js → core-SFHPYR5H.js} +24 -26
- package/dist/{generate-7AF7WRVK.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-WKGDPYI4.js → init-EIM5WNMP.js} +5 -5
- package/dist/{init-WKGDPYI4.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-F3E4JJM7.js → service-ED2LNCTU.js} +6 -6
- package/dist/{static-viewer-4LQZ5AGA.js → static-viewer-Q4F4QP5M.js} +4 -4
- package/dist/{test-CJDNJTPZ.js → test-6VN2DA3S.js} +19 -19
- package/dist/test-6VN2DA3S.js.map +1 -0
- package/dist/{tokens-JAJABYXP.js → tokens-P2B7ZAM3.js} +5 -5
- package/dist/{viewer-R3Q6WAMJ.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 +24 -1
- package/src/build.ts +64 -21
- 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 +274 -0
- 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 +83 -20
- 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 +542 -0
- package/src/core/graph-extractor.ts +601 -0
- package/src/core/importAnalyzer.ts +6 -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 +201 -103
- 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/Icons.tsx +53 -1
- 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/Layout.tsx +7 -3
- package/src/viewer/components/LeftSidebar.tsx +92 -114
- package/src/viewer/components/MultiViewportPreview.tsx +14 -14
- package/src/viewer/components/PreviewArea.tsx +11 -11
- package/src/viewer/components/PreviewFrameHost.tsx +77 -48
- package/src/viewer/components/PreviewToolbar.tsx +57 -10
- 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/ViewportSelector.tsx +56 -45
- package/src/viewer/components/_future/CreatePage.tsx +6 -6
- package/src/viewer/composition-renderer.ts +11 -11
- package/src/viewer/constants/ui.ts +4 -4
- 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/preview-frame.html +22 -13
- 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/styles/globals.css +42 -81
- 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-D35RGPAG.js.map +0 -1
- package/dist/chunk-F7ITZPDJ.js.map +0 -1
- package/dist/chunk-NWQ4CJOQ.js.map +0 -1
- package/dist/chunk-Q7GOHVOK.js.map +0 -1
- package/dist/chunk-RVRTRESS.js.map +0 -1
- package/dist/chunk-SSLQXHNX.js.map +0 -1
- package/dist/generate-7AF7WRVK.js.map +0 -1
- package/dist/scan-K6JNMCGM.js +0 -12
- package/dist/test-CJDNJTPZ.js.map +0 -1
- package/dist/viewer-R3Q6WAMJ.js.map +0 -1
- package/src/shared/segment-loader.ts +0 -59
- /package/dist/{core-SKRPJQZG.js.map → core-SFHPYR5H.js.map} +0 -0
- /package/dist/{scan-K6JNMCGM.js.map → scan-KQBKUS64.js.map} +0 -0
- /package/dist/{service-F3E4JJM7.js.map → service-ED2LNCTU.js.map} +0 -0
- /package/dist/{static-viewer-4LQZ5AGA.js.map → static-viewer-Q4F4QP5M.js.map} +0 -0
- /package/dist/{tokens-JAJABYXP.js.map → tokens-P2B7ZAM3.js.map} +0 -0
|
@@ -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
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* fragments link - Link external resources to
|
|
2
|
+
* fragments link - Link external resources to fragments
|
|
3
3
|
*
|
|
4
4
|
* This module provides subcommands for:
|
|
5
|
-
* - figma: Link Figma components to
|
|
6
|
-
* - storybook: Bootstrap
|
|
5
|
+
* - figma: Link Figma components to fragments
|
|
6
|
+
* - storybook: Bootstrap fragments from Storybook stories
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
export { linkFigma, type LinkFigmaOptions, type LinkFigmaResult } from './figma.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* fragments link storybook - Bootstrap
|
|
2
|
+
* fragments link storybook - Bootstrap fragments from Storybook stories
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { writeFile, mkdir } from 'node:fs/promises';
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
detectStorybookConfig,
|
|
11
11
|
discoverStoryFiles as discoverStorybookFiles,
|
|
12
12
|
parseStoryFile,
|
|
13
|
-
|
|
13
|
+
convertToFragment,
|
|
14
14
|
} from '../../migrate/index.js';
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
export interface LinkStorybookOptions {
|
|
20
20
|
/** Path to .storybook/main.* config */
|
|
21
21
|
config?: string;
|
|
22
|
-
/** Output directory for
|
|
22
|
+
/** Output directory for fragment files */
|
|
23
23
|
out?: string;
|
|
24
24
|
/** Skip confirmation prompts */
|
|
25
25
|
yes?: boolean;
|
|
@@ -114,7 +114,7 @@ export async function linkStorybook(
|
|
|
114
114
|
|
|
115
115
|
try {
|
|
116
116
|
const parsed = await parseStoryFile(storyFile);
|
|
117
|
-
const result =
|
|
117
|
+
const result = convertToFragment(parsed);
|
|
118
118
|
|
|
119
119
|
// Determine output path
|
|
120
120
|
const outputFile = out
|
|
@@ -177,7 +177,7 @@ export async function linkStorybook(
|
|
|
177
177
|
|
|
178
178
|
// Dry run stops here
|
|
179
179
|
if (dryRun) {
|
|
180
|
-
console.log(pc.dim(`\n${previews.length}
|
|
180
|
+
console.log(pc.dim(`\n${previews.length} fragment file(s) would be created`));
|
|
181
181
|
console.log(pc.yellow('\n[Dry run - no files were written]'));
|
|
182
182
|
return { success: true, generated: 0 };
|
|
183
183
|
}
|
|
@@ -186,7 +186,7 @@ export async function linkStorybook(
|
|
|
186
186
|
let selectedPreviews = previews;
|
|
187
187
|
|
|
188
188
|
if (yes) {
|
|
189
|
-
console.log(pc.dim(`\n${previews.length}
|
|
189
|
+
console.log(pc.dim(`\n${previews.length} fragment file(s) will be created`));
|
|
190
190
|
} else {
|
|
191
191
|
const { checkbox } = await import('@inquirer/prompts');
|
|
192
192
|
|
|
@@ -222,7 +222,7 @@ export async function linkStorybook(
|
|
|
222
222
|
|
|
223
223
|
// Generate files
|
|
224
224
|
const genTotal = selectedPreviews.length;
|
|
225
|
-
console.log(pc.dim(`\nGenerating ${genTotal}
|
|
225
|
+
console.log(pc.dim(`\nGenerating ${genTotal} fragment file(s)...\n`));
|
|
226
226
|
|
|
227
227
|
let generated = 0;
|
|
228
228
|
let genErrors = 0;
|
|
@@ -231,7 +231,7 @@ export async function linkStorybook(
|
|
|
231
231
|
const storyFile = join(projectRoot, preview.sourceFile);
|
|
232
232
|
try {
|
|
233
233
|
const parsed = await parseStoryFile(storyFile);
|
|
234
|
-
const result =
|
|
234
|
+
const result = convertToFragment(parsed);
|
|
235
235
|
|
|
236
236
|
// Determine output path
|
|
237
237
|
const outputFile = out
|
|
@@ -252,7 +252,7 @@ export async function linkStorybook(
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
console.log(pc.green(`\n✓ Generated ${generated}
|
|
255
|
+
console.log(pc.green(`\n✓ Generated ${generated} fragment file(s)\n`));
|
|
256
256
|
|
|
257
257
|
// Next steps
|
|
258
258
|
console.log(pc.dim('───────────────────────────────────────'));
|
package/src/commands/list.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import { BRAND } from '../core/index.js';
|
|
7
|
-
import { loadConfig,
|
|
7
|
+
import { loadConfig, discoverFragmentFiles } from '../core/node.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Options for list command
|
|
@@ -30,7 +30,7 @@ export interface ListResult {
|
|
|
30
30
|
*/
|
|
31
31
|
export async function list(options: ListOptions = {}): Promise<ListResult> {
|
|
32
32
|
const { config, configDir } = await loadConfig(options.config);
|
|
33
|
-
const files = await
|
|
33
|
+
const files = await discoverFragmentFiles(config, configDir);
|
|
34
34
|
|
|
35
35
|
console.log(pc.cyan(`\n${BRAND.name} - Discovered Fragments\n`));
|
|
36
36
|
|
package/src/commands/reset.ts
CHANGED
|
@@ -42,7 +42,7 @@ export async function reset(options: ResetOptions = {}): Promise<ResetResult> {
|
|
|
42
42
|
const filesToDelete: string[] = [];
|
|
43
43
|
const dirsToDelete: string[] = [];
|
|
44
44
|
|
|
45
|
-
// Check data directory (.
|
|
45
|
+
// Check data directory (.fragments/)
|
|
46
46
|
const dataDir = join(projectRoot, BRAND.dataDir);
|
|
47
47
|
try {
|
|
48
48
|
const dataDirStat = await stat(dataDir);
|
|
@@ -53,8 +53,8 @@ export async function reset(options: ResetOptions = {}): Promise<ResetResult> {
|
|
|
53
53
|
// Directory doesn't exist
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
// Check for
|
|
57
|
-
const defaultOutFile = join(projectRoot, '
|
|
56
|
+
// Check for fragments.json (default output file)
|
|
57
|
+
const defaultOutFile = join(projectRoot, 'fragments.json');
|
|
58
58
|
try {
|
|
59
59
|
const fileStat = await stat(defaultOutFile);
|
|
60
60
|
if (fileStat.isFile()) {
|
|
@@ -65,10 +65,10 @@ export async function reset(options: ResetOptions = {}): Promise<ResetResult> {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// Try to load config for custom outFile and include patterns
|
|
68
|
-
let
|
|
68
|
+
let fragmentPatterns = [`**/*${BRAND.fileExtension}`];
|
|
69
69
|
try {
|
|
70
70
|
const { config } = await loadConfig();
|
|
71
|
-
if (config.outFile && config.outFile !== '
|
|
71
|
+
if (config.outFile && config.outFile !== 'fragments.json') {
|
|
72
72
|
const customOutFile = join(projectRoot, config.outFile);
|
|
73
73
|
try {
|
|
74
74
|
const fileStat = await stat(customOutFile);
|
|
@@ -81,15 +81,15 @@ export async function reset(options: ResetOptions = {}): Promise<ResetResult> {
|
|
|
81
81
|
}
|
|
82
82
|
// Use config include patterns if available
|
|
83
83
|
if (config.include && config.include.length > 0) {
|
|
84
|
-
|
|
84
|
+
fragmentPatterns = config.include;
|
|
85
85
|
}
|
|
86
86
|
} catch {
|
|
87
87
|
// No config file, use defaults
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// Find all
|
|
90
|
+
// Find all fragment files (*.fragment.tsx)
|
|
91
91
|
console.log(pc.dim('Scanning for generated files...\n'));
|
|
92
|
-
for (const pattern of
|
|
92
|
+
for (const pattern of fragmentPatterns) {
|
|
93
93
|
const matches = await fg(pattern, {
|
|
94
94
|
cwd: projectRoot,
|
|
95
95
|
ignore: ['**/node_modules/**'],
|
|
@@ -130,23 +130,23 @@ export async function reset(options: ResetOptions = {}): Promise<ResetResult> {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// Group files by type for cleaner output
|
|
133
|
-
const
|
|
133
|
+
const fragmentFiles = filesToDelete.filter((f) => f.endsWith(BRAND.fileExtension));
|
|
134
134
|
const mdxFilesFound = filesToDelete.filter((f) => f.endsWith('.mdx'));
|
|
135
135
|
const otherFiles = filesToDelete.filter(
|
|
136
136
|
(f) => !f.endsWith(BRAND.fileExtension) && !f.endsWith('.mdx')
|
|
137
137
|
);
|
|
138
138
|
|
|
139
|
-
if (
|
|
140
|
-
console.log(` 📄 ${
|
|
141
|
-
if (
|
|
142
|
-
for (const f of
|
|
139
|
+
if (fragmentFiles.length > 0) {
|
|
140
|
+
console.log(` 📄 ${fragmentFiles.length} fragment file(s) (*${BRAND.fileExtension})`);
|
|
141
|
+
if (fragmentFiles.length <= 5) {
|
|
142
|
+
for (const f of fragmentFiles) {
|
|
143
143
|
console.log(pc.dim(` ${relative(projectRoot, f)}`));
|
|
144
144
|
}
|
|
145
145
|
} else {
|
|
146
|
-
for (const f of
|
|
146
|
+
for (const f of fragmentFiles.slice(0, 3)) {
|
|
147
147
|
console.log(pc.dim(` ${relative(projectRoot, f)}`));
|
|
148
148
|
}
|
|
149
|
-
console.log(pc.dim(` ... and ${
|
|
149
|
+
console.log(pc.dim(` ... and ${fragmentFiles.length - 3} more`));
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|