@fragments-sdk/cli 0.6.0 → 0.7.0
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 +294 -50
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-D35RGPAG.js → chunk-7OPWMLOE.js} +435 -19
- package/dist/chunk-7OPWMLOE.js.map +1 -0
- package/dist/{chunk-SSLQXHNX.js → chunk-CVXKXVOY.js} +1 -1
- package/dist/{chunk-SSLQXHNX.js.map → chunk-CVXKXVOY.js.map} +1 -1
- package/dist/{chunk-Q7GOHVOK.js → chunk-TJ34N7C7.js} +39 -2
- package/dist/{chunk-Q7GOHVOK.js.map → chunk-TJ34N7C7.js.map} +1 -1
- package/dist/{chunk-F7ITZPDJ.js → chunk-XHUDJNN3.js} +2 -2
- package/dist/{core-SKRPJQZG.js → core-W2HYIQW6.js} +2 -2
- package/dist/{generate-7AF7WRVK.js → generate-LMTISDIJ.js} +3 -3
- package/dist/index.js +3 -3
- package/dist/{init-WKGDPYI4.js → init-7CHRKQ7P.js} +3 -3
- package/dist/mcp-bin.js +2 -2
- package/dist/{scan-K6JNMCGM.js → scan-WY23TJCP.js} +4 -4
- package/dist/{service-F3E4JJM7.js → service-T2L7VLTE.js} +2 -2
- package/dist/{static-viewer-4LQZ5AGA.js → static-viewer-GBR7YNF3.js} +2 -2
- package/dist/{test-CJDNJTPZ.js → test-OJRXNDO2.js} +2 -2
- package/dist/{tokens-JAJABYXP.js → tokens-3BWDESVM.js} +3 -3
- package/dist/{viewer-R3Q6WAMJ.js → viewer-SUFOISZM.js} +12 -12
- package/package.json +2 -2
- package/src/bin.ts +23 -0
- package/src/build.ts +43 -0
- package/src/commands/graph.ts +274 -0
- package/src/core/composition.ts +64 -1
- package/src/core/graph-extractor.test.ts +542 -0
- package/src/core/graph-extractor.ts +601 -0
- package/src/core/importAnalyzer.ts +5 -0
- package/src/viewer/components/App.tsx +128 -30
- package/src/viewer/components/Icons.tsx +53 -1
- package/src/viewer/components/Layout.tsx +7 -3
- package/src/viewer/components/LeftSidebar.tsx +65 -87
- package/src/viewer/components/PreviewFrameHost.tsx +30 -1
- package/src/viewer/components/PreviewToolbar.tsx +57 -10
- package/src/viewer/components/ViewportSelector.tsx +56 -45
- package/src/viewer/constants/ui.ts +4 -4
- package/src/viewer/preview-frame.html +22 -13
- package/src/viewer/styles/globals.css +42 -81
- package/dist/chunk-D35RGPAG.js.map +0 -1
- /package/dist/{chunk-F7ITZPDJ.js.map → chunk-XHUDJNN3.js.map} +0 -0
- /package/dist/{core-SKRPJQZG.js.map → core-W2HYIQW6.js.map} +0 -0
- /package/dist/{generate-7AF7WRVK.js.map → generate-LMTISDIJ.js.map} +0 -0
- /package/dist/{init-WKGDPYI4.js.map → init-7CHRKQ7P.js.map} +0 -0
- /package/dist/{scan-K6JNMCGM.js.map → scan-WY23TJCP.js.map} +0 -0
- /package/dist/{service-F3E4JJM7.js.map → service-T2L7VLTE.js.map} +0 -0
- /package/dist/{static-viewer-4LQZ5AGA.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
- /package/dist/{test-CJDNJTPZ.js.map → test-OJRXNDO2.js.map} +0 -0
- /package/dist/{tokens-JAJABYXP.js.map → tokens-3BWDESVM.js.map} +0 -0
- /package/dist/{viewer-R3Q6WAMJ.js.map → viewer-SUFOISZM.js.map} +0 -0
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
findStorybookDir,
|
|
7
7
|
generatePreviewModule,
|
|
8
8
|
loadConfig
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-CVXKXVOY.js";
|
|
10
10
|
import {
|
|
11
11
|
generateContext
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-TJ34N7C7.js";
|
|
13
13
|
import {
|
|
14
14
|
BRAND
|
|
15
15
|
} from "./chunk-6JBGU74P.js";
|
|
@@ -240,7 +240,7 @@ var sharedRenderPool = null;
|
|
|
240
240
|
var browserPoolModule = null;
|
|
241
241
|
async function getSharedRenderPool() {
|
|
242
242
|
if (!browserPoolModule) {
|
|
243
|
-
browserPoolModule = await import("./service-
|
|
243
|
+
browserPoolModule = await import("./service-T2L7VLTE.js");
|
|
244
244
|
}
|
|
245
245
|
if (!sharedRenderPool) {
|
|
246
246
|
sharedRenderPool = new browserPoolModule.BrowserPool({
|
|
@@ -469,7 +469,7 @@ function segmentsPlugin(options) {
|
|
|
469
469
|
const address = _server.httpServer?.address();
|
|
470
470
|
const port = typeof address === "object" && address ? address.port : 6006;
|
|
471
471
|
const renderViewport = viewport || { width: 800, height: 600 };
|
|
472
|
-
const { FigmaClient, bufferToBase64Url } = await import("./service-
|
|
472
|
+
const { FigmaClient, bufferToBase64Url } = await import("./service-T2L7VLTE.js");
|
|
473
473
|
const figmaClient = new FigmaClient({
|
|
474
474
|
accessToken: figmaToken
|
|
475
475
|
});
|
|
@@ -560,7 +560,7 @@ function segmentsPlugin(options) {
|
|
|
560
560
|
);
|
|
561
561
|
return;
|
|
562
562
|
}
|
|
563
|
-
const { FigmaClient } = await import("./service-
|
|
563
|
+
const { FigmaClient } = await import("./service-T2L7VLTE.js");
|
|
564
564
|
const figmaClient = new FigmaClient({ accessToken: figmaToken });
|
|
565
565
|
const { fileKey, nodeId } = figmaClient.parseUrl(figmaUrl);
|
|
566
566
|
const figmaDesignProps = await figmaClient.getNodeProperties(
|
|
@@ -601,7 +601,7 @@ function segmentsPlugin(options) {
|
|
|
601
601
|
}));
|
|
602
602
|
return;
|
|
603
603
|
}
|
|
604
|
-
const { getSharedTokenRegistry } = await import("./service-
|
|
604
|
+
const { getSharedTokenRegistry } = await import("./service-T2L7VLTE.js");
|
|
605
605
|
const registry = getSharedTokenRegistry();
|
|
606
606
|
if (!registry.isInitialized()) {
|
|
607
607
|
await registry.initialize(config.tokens, projectRoot);
|
|
@@ -661,7 +661,7 @@ function segmentsPlugin(options) {
|
|
|
661
661
|
}));
|
|
662
662
|
return;
|
|
663
663
|
}
|
|
664
|
-
const { getSharedTokenRegistry } = await import("./service-
|
|
664
|
+
const { getSharedTokenRegistry } = await import("./service-T2L7VLTE.js");
|
|
665
665
|
const registry = getSharedTokenRegistry();
|
|
666
666
|
if (!registry.isInitialized()) {
|
|
667
667
|
await registry.initialize(config.tokens, projectRoot);
|
|
@@ -723,7 +723,7 @@ function segmentsPlugin(options) {
|
|
|
723
723
|
res.end(JSON.stringify({ error: "Could not resolve segment file path" }));
|
|
724
724
|
return;
|
|
725
725
|
}
|
|
726
|
-
const { getSharedTokenRegistry } = await import("./service-
|
|
726
|
+
const { getSharedTokenRegistry } = await import("./service-T2L7VLTE.js");
|
|
727
727
|
const registry = getSharedTokenRegistry();
|
|
728
728
|
if (!registry.isInitialized()) {
|
|
729
729
|
await registry.initialize(config.tokens, projectRoot);
|
|
@@ -849,7 +849,7 @@ function segmentsPlugin(options) {
|
|
|
849
849
|
}
|
|
850
850
|
const { writeFile, mkdir } = await import("fs/promises");
|
|
851
851
|
const { join: join2 } = await import("path");
|
|
852
|
-
const { BRAND: BRAND2 } = await import("./core-
|
|
852
|
+
const { BRAND: BRAND2 } = await import("./core-W2HYIQW6.js");
|
|
853
853
|
const fragmentsDir = join2(projectRoot, BRAND2.dataDir, BRAND2.componentsDir);
|
|
854
854
|
await mkdir(fragmentsDir, { recursive: true });
|
|
855
855
|
const fragmentPath = join2(
|
|
@@ -904,7 +904,7 @@ function segmentsPlugin(options) {
|
|
|
904
904
|
const {
|
|
905
905
|
getSharedTokenRegistry,
|
|
906
906
|
generateTokenPatches
|
|
907
|
-
} = await import("./service-
|
|
907
|
+
} = await import("./service-T2L7VLTE.js");
|
|
908
908
|
const registry = getSharedTokenRegistry();
|
|
909
909
|
if (!registry.isInitialized()) {
|
|
910
910
|
await registry.initialize(config.tokens, projectRoot);
|
|
@@ -1566,7 +1566,7 @@ async function loadFullSegmentForCompare(_server, _segmentFiles, componentName,
|
|
|
1566
1566
|
}
|
|
1567
1567
|
}
|
|
1568
1568
|
async function compareImages(image1Base64, image2Base64, threshold) {
|
|
1569
|
-
const { DiffEngine, base64UrlToBuffer, bufferToBase64Url } = await import("./service-
|
|
1569
|
+
const { DiffEngine, base64UrlToBuffer, bufferToBase64Url } = await import("./service-T2L7VLTE.js");
|
|
1570
1570
|
const { PNG } = await import("pngjs");
|
|
1571
1571
|
const buffer1 = base64UrlToBuffer(image1Base64);
|
|
1572
1572
|
const buffer2 = base64UrlToBuffer(image2Base64);
|
|
@@ -1819,4 +1819,4 @@ export {
|
|
|
1819
1819
|
createDevServer,
|
|
1820
1820
|
segmentsPlugin
|
|
1821
1821
|
};
|
|
1822
|
-
//# sourceMappingURL=viewer-
|
|
1822
|
+
//# sourceMappingURL=viewer-SUFOISZM.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fragments-sdk/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CLI, MCP server, and dev tools for Fragments design system",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"vite": "^6.0.0",
|
|
66
66
|
"vite-plugin-svgr": "^4.5.0",
|
|
67
67
|
"zod": "^3.24.1",
|
|
68
|
-
"@fragments-sdk/context": "0.
|
|
68
|
+
"@fragments-sdk/context": "0.3.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@types/babel__generator": "^7.6.8",
|
package/src/bin.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { add } from './commands/add.js';
|
|
|
35
35
|
import { linkFigma, linkStorybook } from './commands/link/index.js';
|
|
36
36
|
import { enhance } from './commands/enhance.js';
|
|
37
37
|
import { scan } from './commands/scan.js';
|
|
38
|
+
import { graph } from './commands/graph.js';
|
|
38
39
|
|
|
39
40
|
// Import existing commands that were already extracted
|
|
40
41
|
import { runScreenshotCommand } from './screenshot.js';
|
|
@@ -844,6 +845,28 @@ program
|
|
|
844
845
|
}
|
|
845
846
|
});
|
|
846
847
|
|
|
848
|
+
// ============================================================================
|
|
849
|
+
// GRAPH COMMAND
|
|
850
|
+
// ============================================================================
|
|
851
|
+
program
|
|
852
|
+
.command('graph')
|
|
853
|
+
.description('Query the component relationship graph')
|
|
854
|
+
.argument('[component]', 'Component name (optional)')
|
|
855
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
856
|
+
.option('-m, --mode <mode>', 'Query mode: health, dependencies, dependents, impact, path, composition, alternatives, islands')
|
|
857
|
+
.option('-t, --target <component>', 'Target component for path mode')
|
|
858
|
+
.option('--edge-types <types>', 'Comma-separated edge types to filter by')
|
|
859
|
+
.option('--depth <number>', 'Max traversal depth for impact mode', parseInt)
|
|
860
|
+
.option('--format <format>', 'Output format: table, json, dot', 'table')
|
|
861
|
+
.action(async (component, options) => {
|
|
862
|
+
try {
|
|
863
|
+
await graph(component, options);
|
|
864
|
+
} catch (error) {
|
|
865
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
|
|
847
870
|
// ============================================================================
|
|
848
871
|
// TEST COMMAND
|
|
849
872
|
// ============================================================================
|
package/src/build.ts
CHANGED
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
resolveComponentSourcePath,
|
|
25
25
|
type AutoDetectedPropDefinition,
|
|
26
26
|
} from "./core/auto-props.js";
|
|
27
|
+
import { buildComponentGraph } from "./core/graph-extractor.js";
|
|
28
|
+
import { serializeGraph } from "@fragments-sdk/context/graph";
|
|
27
29
|
|
|
28
30
|
type CompiledProp = CompiledSegment["props"][string];
|
|
29
31
|
|
|
@@ -295,6 +297,46 @@ export async function buildSegments(
|
|
|
295
297
|
}
|
|
296
298
|
}
|
|
297
299
|
|
|
300
|
+
// Build component graph for AI structural queries
|
|
301
|
+
// Derive component directory from configDir (typically src/components/)
|
|
302
|
+
const componentDir = resolve(configDir, "src", "components");
|
|
303
|
+
let graphData: ReturnType<typeof serializeGraph> | undefined;
|
|
304
|
+
try {
|
|
305
|
+
const graphResult = await buildComponentGraph(segments, blocks, componentDir);
|
|
306
|
+
|
|
307
|
+
// Auto-enrich segments with detected metadata
|
|
308
|
+
for (const [name, segment] of Object.entries(segments)) {
|
|
309
|
+
const detected = graphResult.autoDetected.get(name);
|
|
310
|
+
if (!detected) continue;
|
|
311
|
+
|
|
312
|
+
if (!segment.ai) segment.ai = {};
|
|
313
|
+
if (!segment.ai.subComponents && detected.subComponents) {
|
|
314
|
+
segment.ai.subComponents = detected.subComponents;
|
|
315
|
+
}
|
|
316
|
+
if (!segment.ai.compositionPattern && detected.compositionPattern) {
|
|
317
|
+
segment.ai.compositionPattern = detected.compositionPattern;
|
|
318
|
+
}
|
|
319
|
+
if (!segment.ai.commonPatterns && detected.commonPatterns) {
|
|
320
|
+
segment.ai.commonPatterns = detected.commonPatterns;
|
|
321
|
+
}
|
|
322
|
+
if (!segment.ai.requiredChildren && detected.requiredChildren) {
|
|
323
|
+
segment.ai.requiredChildren = detected.requiredChildren;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Report drift warnings
|
|
328
|
+
for (const w of graphResult.warnings) {
|
|
329
|
+
warnings.push({ file: "graph", warning: w });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
graphData = serializeGraph(graphResult.graph);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
warnings.push({
|
|
335
|
+
file: "graph",
|
|
336
|
+
warning: `Graph extraction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
298
340
|
const output: CompiledSegmentsFile = {
|
|
299
341
|
version: "1.0.0",
|
|
300
342
|
generatedAt: new Date().toISOString(),
|
|
@@ -302,6 +344,7 @@ export async function buildSegments(
|
|
|
302
344
|
segments,
|
|
303
345
|
...(Object.keys(blocks).length > 0 && { blocks }),
|
|
304
346
|
...(tokens && { tokens }),
|
|
347
|
+
...(graphData && { graph: graphData }),
|
|
305
348
|
};
|
|
306
349
|
|
|
307
350
|
const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fragments graph` — query the component relationship graph from the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Loads fragments.json, instantiates the ComponentGraphEngine, and runs
|
|
5
|
+
* the requested query mode. Output formats: table (colored terminal), json, dot.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { readFile } from 'node:fs/promises';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
import type { CompiledSegmentsFile } from '../core/index.js';
|
|
12
|
+
import { BRAND } from '../core/index.js';
|
|
13
|
+
import { loadConfig } from '../core/node.js';
|
|
14
|
+
import {
|
|
15
|
+
ComponentGraphEngine,
|
|
16
|
+
deserializeGraph,
|
|
17
|
+
} from '@fragments-sdk/context/graph';
|
|
18
|
+
import type { GraphEdgeType } from '@fragments-sdk/context/graph';
|
|
19
|
+
|
|
20
|
+
export interface GraphCommandOptions {
|
|
21
|
+
config?: string;
|
|
22
|
+
mode?: string;
|
|
23
|
+
target?: string;
|
|
24
|
+
edgeTypes?: string;
|
|
25
|
+
depth?: number;
|
|
26
|
+
format?: 'table' | 'json' | 'dot';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function graph(
|
|
30
|
+
component: string | undefined,
|
|
31
|
+
options: GraphCommandOptions,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
34
|
+
const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
|
|
35
|
+
|
|
36
|
+
let data: CompiledSegmentsFile;
|
|
37
|
+
try {
|
|
38
|
+
const content = await readFile(outputPath, 'utf-8');
|
|
39
|
+
data = JSON.parse(content) as CompiledSegmentsFile;
|
|
40
|
+
} catch {
|
|
41
|
+
console.error(
|
|
42
|
+
pc.red(`Error: Could not load ${BRAND.outFile}. Run \`${BRAND.cliCommand} build\` first.`),
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!data.graph) {
|
|
48
|
+
console.error(
|
|
49
|
+
pc.red(`Error: No graph data in ${BRAND.outFile}. Rebuild with the latest CLI.`),
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const graph = deserializeGraph(data.graph);
|
|
55
|
+
const blocks = data.blocks
|
|
56
|
+
? Object.fromEntries(
|
|
57
|
+
Object.entries(data.blocks).map(([k, v]) => [k, { components: v.components }]),
|
|
58
|
+
)
|
|
59
|
+
: undefined;
|
|
60
|
+
const engine = new ComponentGraphEngine(graph, blocks);
|
|
61
|
+
|
|
62
|
+
const mode = options.mode ?? (component ? 'dependencies' : 'health');
|
|
63
|
+
const format = options.format ?? 'table';
|
|
64
|
+
const edgeTypes = options.edgeTypes
|
|
65
|
+
? (options.edgeTypes.split(',') as GraphEdgeType[])
|
|
66
|
+
: undefined;
|
|
67
|
+
|
|
68
|
+
// Validate component exists for modes that require it
|
|
69
|
+
const needsComponent = ['dependencies', 'dependents', 'impact', 'path', 'composition', 'alternatives'];
|
|
70
|
+
if (needsComponent.includes(mode) && !component) {
|
|
71
|
+
console.error(pc.red(`Error: "${mode}" mode requires a component name.`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
if (component && !engine.hasNode(component)) {
|
|
75
|
+
console.error(pc.red(`Error: Component "${component}" not found in graph.`));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
switch (mode) {
|
|
80
|
+
case 'health': {
|
|
81
|
+
const health = engine.getHealth();
|
|
82
|
+
if (format === 'json') {
|
|
83
|
+
console.log(JSON.stringify(health, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log(pc.bold('\nComponent Graph Health\n'));
|
|
88
|
+
console.log(` ${pc.cyan('Nodes:')} ${health.nodeCount}`);
|
|
89
|
+
console.log(` ${pc.cyan('Edges:')} ${health.edgeCount}`);
|
|
90
|
+
console.log(` ${pc.cyan('Avg degree:')} ${health.averageDegree}`);
|
|
91
|
+
console.log(` ${pc.cyan('Islands:')} ${health.connectedComponents.length}`);
|
|
92
|
+
console.log(` ${pc.cyan('Coverage:')} ${health.compositionCoverage}% in blocks`);
|
|
93
|
+
console.log(` ${pc.cyan('Orphans:')} ${health.orphans.length > 0 ? health.orphans.join(', ') : pc.green('none')}`);
|
|
94
|
+
|
|
95
|
+
if (health.hubs.length > 0) {
|
|
96
|
+
console.log(`\n ${pc.bold('Top hubs:')}`);
|
|
97
|
+
for (const hub of health.hubs.slice(0, 5)) {
|
|
98
|
+
console.log(` ${pc.yellow(hub.name)} — ${hub.degree} connections`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case 'dependencies': {
|
|
106
|
+
const deps = engine.dependencies(component!, edgeTypes);
|
|
107
|
+
if (format === 'json') {
|
|
108
|
+
console.log(JSON.stringify({ component, dependencies: deps }, null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(pc.bold(`\nDependencies of ${component}\n`));
|
|
113
|
+
if (deps.length === 0) {
|
|
114
|
+
console.log(' No outgoing dependencies.');
|
|
115
|
+
} else {
|
|
116
|
+
for (const dep of deps) {
|
|
117
|
+
console.log(` ${pc.yellow(dep.target)} ${pc.dim(`(${dep.type})`)}${dep.note ? ` — ${dep.note}` : ''}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
console.log();
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case 'dependents': {
|
|
125
|
+
const deps = engine.dependents(component!, edgeTypes);
|
|
126
|
+
if (format === 'json') {
|
|
127
|
+
console.log(JSON.stringify({ component, dependents: deps }, null, 2));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(pc.bold(`\nDependents of ${component}\n`));
|
|
132
|
+
if (deps.length === 0) {
|
|
133
|
+
console.log(' No incoming dependents.');
|
|
134
|
+
} else {
|
|
135
|
+
for (const dep of deps) {
|
|
136
|
+
console.log(` ${pc.yellow(dep.source)} ${pc.dim(`(${dep.type})`)}${dep.note ? ` — ${dep.note}` : ''}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
console.log();
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case 'impact': {
|
|
144
|
+
const result = engine.impact(component!, options.depth ?? 3);
|
|
145
|
+
if (format === 'json') {
|
|
146
|
+
console.log(JSON.stringify(result, null, 2));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log(pc.bold(`\nImpact analysis: ${component}\n`));
|
|
151
|
+
console.log(` ${pc.red(`${result.totalAffected}`)} affected components, ${pc.red(`${result.affectedBlocks.length}`)} affected blocks\n`);
|
|
152
|
+
|
|
153
|
+
if (result.affected.length > 0) {
|
|
154
|
+
console.log(` ${pc.bold('Affected components:')}`);
|
|
155
|
+
for (const entry of result.affected) {
|
|
156
|
+
const indent = ' '.repeat(entry.depth + 1);
|
|
157
|
+
console.log(`${indent}${pc.yellow(entry.component)} ${pc.dim(`(depth ${entry.depth}, via ${entry.edgeType})`)}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (result.affectedBlocks.length > 0) {
|
|
162
|
+
console.log(`\n ${pc.bold('Affected blocks:')} ${result.affectedBlocks.join(', ')}`);
|
|
163
|
+
}
|
|
164
|
+
console.log();
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case 'path': {
|
|
169
|
+
if (!options.target) {
|
|
170
|
+
console.error(pc.red('Error: --target is required for path mode.'));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const result = engine.path(component!, options.target);
|
|
175
|
+
if (format === 'json') {
|
|
176
|
+
console.log(JSON.stringify(result, null, 2));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(pc.bold(`\nPath: ${component} → ${options.target}\n`));
|
|
181
|
+
if (!result.found) {
|
|
182
|
+
console.log(` ${pc.red('No path found.')}`);
|
|
183
|
+
} else {
|
|
184
|
+
console.log(` ${result.path.map(n => pc.yellow(n)).join(pc.dim(' → '))}`);
|
|
185
|
+
if (result.edges.length > 0) {
|
|
186
|
+
console.log(` ${pc.dim(`(${result.edges.map(e => e.type).join(' → ')})`)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case 'composition': {
|
|
194
|
+
const tree = engine.composition(component!);
|
|
195
|
+
if (format === 'json') {
|
|
196
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log(pc.bold(`\nComposition: ${component}\n`));
|
|
201
|
+
console.log(` ${pc.cyan('Pattern:')} ${tree.compositionPattern ?? 'unknown'}`);
|
|
202
|
+
if (tree.parent) {
|
|
203
|
+
console.log(` ${pc.cyan('Parent:')} ${tree.parent}`);
|
|
204
|
+
}
|
|
205
|
+
if (tree.subComponents.length > 0) {
|
|
206
|
+
console.log(` ${pc.cyan('Sub-components:')}`);
|
|
207
|
+
for (const sub of tree.subComponents) {
|
|
208
|
+
const isRequired = tree.requiredChildren.includes(sub);
|
|
209
|
+
console.log(` ${pc.yellow(sub)}${isRequired ? pc.red(' (required)') : ''}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (tree.siblings.length > 0) {
|
|
213
|
+
console.log(` ${pc.cyan('Siblings:')} ${tree.siblings.join(', ')}`);
|
|
214
|
+
}
|
|
215
|
+
if (tree.blocks.length > 0) {
|
|
216
|
+
console.log(` ${pc.cyan('In blocks:')} ${tree.blocks.join(', ')}`);
|
|
217
|
+
}
|
|
218
|
+
console.log();
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case 'alternatives': {
|
|
223
|
+
const alts = engine.alternatives(component!);
|
|
224
|
+
if (format === 'json') {
|
|
225
|
+
console.log(JSON.stringify({ component, alternatives: alts }, null, 2));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(pc.bold(`\nAlternatives for ${component}\n`));
|
|
230
|
+
if (alts.length === 0) {
|
|
231
|
+
console.log(' No known alternatives.');
|
|
232
|
+
} else {
|
|
233
|
+
for (const alt of alts) {
|
|
234
|
+
console.log(` ${pc.yellow(alt.component)}${alt.note ? ` — ${alt.note}` : ''}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
console.log();
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case 'islands': {
|
|
242
|
+
const islands = engine.islands();
|
|
243
|
+
if (format === 'json') {
|
|
244
|
+
console.log(JSON.stringify({ islands }, null, 2));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(pc.bold(`\nConnected Islands (${islands.length})\n`));
|
|
249
|
+
for (let i = 0; i < islands.length; i++) {
|
|
250
|
+
console.log(` ${pc.cyan(`Island ${i + 1}`)} (${islands[i].length} components): ${islands[i].join(', ')}`);
|
|
251
|
+
}
|
|
252
|
+
console.log();
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
default:
|
|
257
|
+
console.error(pc.red(`Unknown mode: "${mode}". Valid: health, dependencies, dependents, impact, path, composition, alternatives, islands`));
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Graphviz dot output
|
|
262
|
+
if (format === 'dot') {
|
|
263
|
+
const lines = ['digraph ComponentGraph {', ' rankdir=LR;', ' node [shape=box, style=rounded];'];
|
|
264
|
+
for (const node of graph.nodes) {
|
|
265
|
+
lines.push(` "${node.name}" [label="${node.name}\\n(${node.category})"];`);
|
|
266
|
+
}
|
|
267
|
+
for (const edge of graph.edges) {
|
|
268
|
+
const style = edge.type === 'alternative-to' ? 'dashed' : 'solid';
|
|
269
|
+
lines.push(` "${edge.source}" -> "${edge.target}" [label="${edge.type}", style=${style}];`);
|
|
270
|
+
}
|
|
271
|
+
lines.push('}');
|
|
272
|
+
console.log(lines.join('\n'));
|
|
273
|
+
}
|
|
274
|
+
}
|
package/src/core/composition.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { CompiledSegment, RelationshipType } from "./types.js";
|
|
2
|
+
import type { ComponentGraph } from "@fragments-sdk/context/graph";
|
|
3
|
+
import { ComponentGraphEngine } from "@fragments-sdk/context/graph";
|
|
2
4
|
|
|
3
5
|
// --- Public types ---
|
|
4
6
|
|
|
@@ -58,12 +60,16 @@ const CATEGORY_AFFINITIES: Record<string, string[]> = {
|
|
|
58
60
|
* Returns warnings about missing relations, usage conflicts,
|
|
59
61
|
* and suggestions for additional components.
|
|
60
62
|
*
|
|
63
|
+
* When a ComponentGraph is provided via `options.graph`, the analysis is
|
|
64
|
+
* enhanced with graph-based dependency detection and block-based suggestions.
|
|
65
|
+
*
|
|
61
66
|
* Browser-safe: no Node.js APIs used.
|
|
62
67
|
*/
|
|
63
68
|
export function analyzeComposition(
|
|
64
69
|
segments: Record<string, CompiledSegment>,
|
|
65
70
|
componentNames: string[],
|
|
66
|
-
_context?: string
|
|
71
|
+
_context?: string,
|
|
72
|
+
options?: { graph?: ComponentGraph },
|
|
67
73
|
): CompositionAnalysis {
|
|
68
74
|
const allNames = new Set(Object.keys(segments));
|
|
69
75
|
|
|
@@ -218,6 +224,63 @@ export function analyzeComposition(
|
|
|
218
224
|
}
|
|
219
225
|
}
|
|
220
226
|
|
|
227
|
+
// 6. Graph-enhanced analysis (when graph data is available)
|
|
228
|
+
if (options?.graph) {
|
|
229
|
+
const engine = new ComponentGraphEngine(options.graph);
|
|
230
|
+
|
|
231
|
+
// Add graph-based dependency warnings
|
|
232
|
+
for (const name of components) {
|
|
233
|
+
const deps = engine.dependencies(name, ["imports", "hook-depends"]);
|
|
234
|
+
for (const dep of deps) {
|
|
235
|
+
if (
|
|
236
|
+
!selectedSet.has(dep.target) &&
|
|
237
|
+
!suggestedSet.has(dep.target) &&
|
|
238
|
+
allNames.has(dep.target)
|
|
239
|
+
) {
|
|
240
|
+
suggestions.push({
|
|
241
|
+
component: dep.target,
|
|
242
|
+
reason: `"${name}" ${dep.type === "hook-depends" ? "uses a hook from" : "imports"} "${dep.target}"`,
|
|
243
|
+
relationship: "composition",
|
|
244
|
+
sourceComponent: name,
|
|
245
|
+
});
|
|
246
|
+
suggestedSet.add(dep.target);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Add block-based suggestions
|
|
252
|
+
for (const name of components) {
|
|
253
|
+
const blocks = engine.blocksUsing(name);
|
|
254
|
+
for (const blockName of blocks) {
|
|
255
|
+
// Find other components in this block that aren't selected
|
|
256
|
+
const blockComps = options.graph.edges
|
|
257
|
+
.filter(
|
|
258
|
+
(e) =>
|
|
259
|
+
e.type === "composes" &&
|
|
260
|
+
e.provenance === `block:${blockName}` &&
|
|
261
|
+
(e.source === name || e.target === name)
|
|
262
|
+
)
|
|
263
|
+
.map((e) => (e.source === name ? e.target : e.source));
|
|
264
|
+
|
|
265
|
+
for (const comp of blockComps) {
|
|
266
|
+
if (
|
|
267
|
+
!selectedSet.has(comp) &&
|
|
268
|
+
!suggestedSet.has(comp) &&
|
|
269
|
+
allNames.has(comp)
|
|
270
|
+
) {
|
|
271
|
+
suggestions.push({
|
|
272
|
+
component: comp,
|
|
273
|
+
reason: `"${name}" and "${comp}" are used together in the "${blockName}" block`,
|
|
274
|
+
relationship: "composition",
|
|
275
|
+
sourceComponent: name,
|
|
276
|
+
});
|
|
277
|
+
suggestedSet.add(comp);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
221
284
|
return { components, unknown, warnings, suggestions, guidelines };
|
|
222
285
|
}
|
|
223
286
|
|