@fragments-sdk/cli 0.5.2 → 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.
Files changed (124) hide show
  1. package/dist/bin.js +996 -79
  2. package/dist/bin.js.map +1 -1
  3. package/dist/{chunk-ICAIQ57V.js → chunk-6JBGU74P.js} +5 -3
  4. package/dist/chunk-6JBGU74P.js.map +1 -0
  5. package/dist/chunk-7OPWMLOE.js +1625 -0
  6. package/dist/chunk-7OPWMLOE.js.map +1 -0
  7. package/dist/{chunk-2H2JAA3U.js → chunk-CVXKXVOY.js} +3 -3
  8. package/dist/{chunk-2H2JAA3U.js.map → chunk-CVXKXVOY.js.map} +1 -1
  9. package/dist/{chunk-IOJE35DZ.js → chunk-NWQ4CJOQ.js} +3 -3
  10. package/dist/{chunk-2DJH4F4P.js → chunk-RVRTRESS.js} +3 -3
  11. package/dist/{chunk-V7YLRR4C.js → chunk-TJ34N7C7.js} +41 -4
  12. package/dist/{chunk-V7YLRR4C.js.map → chunk-TJ34N7C7.js.map} +1 -1
  13. package/dist/{chunk-XNWDI6UT.js → chunk-XHUDJNN3.js} +5 -5
  14. package/dist/{core-DKHB7FYV.js → core-W2HYIQW6.js} +4 -4
  15. package/dist/{generate-KL24VZVD.js → generate-LMTISDIJ.js} +5 -5
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +15 -7
  18. package/dist/index.js.map +1 -1
  19. package/dist/{init-NION5S3M.js → init-7CHRKQ7P.js} +5 -5
  20. package/dist/mcp-bin.js +8 -220
  21. package/dist/mcp-bin.js.map +1 -1
  22. package/dist/scan-WY23TJCP.js +12 -0
  23. package/dist/{service-RWUMZ3EW.js → service-T2L7VLTE.js} +5 -5
  24. package/dist/static-viewer-GBR7YNF3.js +12 -0
  25. package/dist/{test-ECPEXFDN.js → test-OJRXNDO2.js} +4 -4
  26. package/dist/{tokens-ITADYVPF.js → tokens-3BWDESVM.js} +6 -6
  27. package/dist/viewer-SUFOISZM.js +1822 -0
  28. package/dist/viewer-SUFOISZM.js.map +1 -0
  29. package/package.json +6 -5
  30. package/src/bin.ts +31 -0
  31. package/src/build.ts +147 -13
  32. package/src/cli-commands.ts +18 -0
  33. package/src/commands/__tests__/a11y-scoring.test.ts +278 -0
  34. package/src/commands/a11y-report.ts +625 -0
  35. package/src/commands/a11y.ts +168 -14
  36. package/src/commands/build.ts +16 -0
  37. package/src/commands/graph.ts +274 -0
  38. package/src/core/auto-props.ts +464 -0
  39. package/src/core/composition.ts +64 -1
  40. package/src/core/graph-extractor.test.ts +542 -0
  41. package/src/core/graph-extractor.ts +601 -0
  42. package/src/core/importAnalyzer.ts +5 -0
  43. package/src/core/schema.ts +2 -0
  44. package/src/core/types.ts +3 -1
  45. package/src/index.ts +4 -0
  46. package/src/mcp/server.ts +13 -220
  47. package/src/theme/__tests__/component-contrast.test.ts +338 -0
  48. package/src/theme/__tests__/contrast-validation.test.ts +326 -0
  49. package/src/theme/contrast.test.ts +331 -0
  50. package/src/theme/contrast.ts +246 -0
  51. package/src/theme/generator.ts +213 -1
  52. package/src/theme/index.ts +16 -0
  53. package/src/theme/types.ts +51 -0
  54. package/src/viewer/__tests__/a11y-fixes.test.ts +358 -0
  55. package/src/viewer/__tests__/viewer-integration.test.ts +2 -7
  56. package/src/viewer/components/AccessibilityPanel.tsx +493 -433
  57. package/src/viewer/components/ActionCapture.tsx +1 -1
  58. package/src/viewer/components/ActionsPanel.tsx +142 -183
  59. package/src/viewer/components/App.tsx +276 -183
  60. package/src/viewer/components/BottomPanel.tsx +40 -80
  61. package/src/viewer/components/CodePanel.tsx +9 -87
  62. package/src/viewer/components/CommandPalette.tsx +117 -74
  63. package/src/viewer/components/ComponentGraph.tsx +143 -126
  64. package/src/viewer/components/ComponentHeader.tsx +46 -43
  65. package/src/viewer/components/ContractPanel.tsx +124 -117
  66. package/src/viewer/components/ErrorBoundary.tsx +47 -35
  67. package/src/viewer/components/FigmaEmbed.tsx +18 -13
  68. package/src/viewer/components/FragmentEditor.tsx +126 -63
  69. package/src/viewer/components/HealthDashboard.tsx +146 -171
  70. package/src/viewer/components/HmrStatusIndicator.tsx +31 -41
  71. package/src/viewer/components/Icons.tsx +151 -98
  72. package/src/viewer/components/InteractionsPanel.tsx +317 -264
  73. package/src/viewer/components/IsolatedPreviewFrame.tsx +52 -27
  74. package/src/viewer/components/IsolatedRender.tsx +12 -6
  75. package/src/viewer/components/KeyboardShortcutsHelp.tsx +34 -70
  76. package/src/viewer/components/LandingPage.tsx +285 -305
  77. package/src/viewer/components/Layout.tsx +12 -10
  78. package/src/viewer/components/LeftSidebar.tsx +103 -155
  79. package/src/viewer/components/MultiViewportPreview.tsx +254 -63
  80. package/src/viewer/components/PreviewArea.tsx +113 -44
  81. package/src/viewer/components/PreviewFrameHost.tsx +36 -6
  82. package/src/viewer/components/PreviewPane.tsx +2 -3
  83. package/src/viewer/components/PreviewToolbar.tsx +109 -105
  84. package/src/viewer/components/PropsEditor.tsx +154 -74
  85. package/src/viewer/components/PropsTable.tsx +95 -82
  86. package/src/viewer/components/RelationsSection.tsx +71 -40
  87. package/src/viewer/components/ResizablePanel.tsx +158 -55
  88. package/src/viewer/components/RightSidebar.tsx +46 -56
  89. package/src/viewer/components/ScreenshotButton.tsx +12 -12
  90. package/src/viewer/components/SkeletonLoader.tsx +99 -83
  91. package/src/viewer/components/StoryRenderer.tsx +4 -11
  92. package/src/viewer/components/Toast.tsx +3 -67
  93. package/src/viewer/components/TokenStylePanel.tsx +136 -118
  94. package/src/viewer/components/UsageSection.tsx +26 -26
  95. package/src/viewer/components/VariantMatrix.tsx +140 -47
  96. package/src/viewer/components/VariantTabs.tsx +24 -68
  97. package/src/viewer/components/ViewportSelector.tsx +121 -114
  98. package/src/viewer/constants/ui.ts +23 -22
  99. package/src/viewer/entry.tsx +8 -3
  100. package/src/viewer/index.ts +3 -6
  101. package/src/viewer/preview-frame.html +43 -18
  102. package/src/viewer/server.ts +7 -16
  103. package/src/viewer/styles/globals.css +46 -85
  104. package/src/viewer/utils/a11y-fixes.ts +53 -30
  105. package/dist/chunk-ICAIQ57V.js.map +0 -1
  106. package/dist/chunk-U4GQ2JTD.js +0 -832
  107. package/dist/chunk-U4GQ2JTD.js.map +0 -1
  108. package/dist/scan-ESEXV7LF.js +0 -12
  109. package/dist/static-viewer-O37MJ5B6.js +0 -12
  110. package/dist/viewer-YDGFDTK5.js +0 -11104
  111. package/dist/viewer-YDGFDTK5.js.map +0 -1
  112. package/src/viewer/postcss.config.js +0 -6
  113. package/src/viewer/tailwind.config.js +0 -37
  114. /package/dist/{chunk-IOJE35DZ.js.map → chunk-NWQ4CJOQ.js.map} +0 -0
  115. /package/dist/{chunk-2DJH4F4P.js.map → chunk-RVRTRESS.js.map} +0 -0
  116. /package/dist/{chunk-XNWDI6UT.js.map → chunk-XHUDJNN3.js.map} +0 -0
  117. /package/dist/{core-DKHB7FYV.js.map → core-W2HYIQW6.js.map} +0 -0
  118. /package/dist/{generate-KL24VZVD.js.map → generate-LMTISDIJ.js.map} +0 -0
  119. /package/dist/{init-NION5S3M.js.map → init-7CHRKQ7P.js.map} +0 -0
  120. /package/dist/{scan-ESEXV7LF.js.map → scan-WY23TJCP.js.map} +0 -0
  121. /package/dist/{service-RWUMZ3EW.js.map → service-T2L7VLTE.js.map} +0 -0
  122. /package/dist/{static-viewer-O37MJ5B6.js.map → static-viewer-GBR7YNF3.js.map} +0 -0
  123. /package/dist/{test-ECPEXFDN.js.map → test-OJRXNDO2.js.map} +0 -0
  124. /package/dist/{tokens-ITADYVPF.js.map → tokens-3BWDESVM.js.map} +0 -0
@@ -0,0 +1,464 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { dirname, extname, join, resolve } from "node:path";
3
+ import ts from "typescript";
4
+ import type { PropDefinition } from "./types.js";
5
+
6
+ export interface AutoDetectedPropDefinition {
7
+ type: PropDefinition["type"];
8
+ description: string;
9
+ required: boolean;
10
+ default?: unknown;
11
+ values?: readonly string[];
12
+ }
13
+
14
+ export interface AutoPropsExtractionResult {
15
+ props: Record<string, AutoDetectedPropDefinition>;
16
+ warnings: string[];
17
+ resolved: boolean;
18
+ }
19
+
20
+ interface ResolvedComponentSignature {
21
+ propsTypeNode: ts.TypeNode | null;
22
+ componentNode: ts.FunctionLikeDeclarationBase | null;
23
+ }
24
+
25
+ function toPosixPath(filePath: string): string {
26
+ return filePath.replace(/\\/g, "/");
27
+ }
28
+
29
+ function isFile(filePath: string): boolean {
30
+ if (!existsSync(filePath)) return false;
31
+ try {
32
+ return statSync(filePath).isFile();
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ function resolveModulePath(basePath: string): string | null {
39
+ const candidates: string[] = [];
40
+ const extension = extname(basePath);
41
+
42
+ if (extension) {
43
+ candidates.push(basePath);
44
+ } else {
45
+ candidates.push(
46
+ `${basePath}.tsx`,
47
+ `${basePath}.ts`,
48
+ `${basePath}.jsx`,
49
+ `${basePath}.js`,
50
+ join(basePath, "index.tsx"),
51
+ join(basePath, "index.ts"),
52
+ join(basePath, "index.jsx"),
53
+ join(basePath, "index.js")
54
+ );
55
+ }
56
+
57
+ for (const candidate of candidates) {
58
+ if (isFile(candidate)) {
59
+ return resolve(candidate);
60
+ }
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Resolve a component source file from the segment file path + component import path.
68
+ * Supports relative imports like ".", "./Button", "../components/Button".
69
+ */
70
+ export function resolveComponentSourcePath(
71
+ segmentFileAbsolutePath: string,
72
+ componentImportPath: string | null
73
+ ): string | null {
74
+ if (!componentImportPath) return null;
75
+ if (!componentImportPath.startsWith(".")) return null;
76
+
77
+ const segmentDir = dirname(segmentFileAbsolutePath);
78
+ const basePath = resolve(segmentDir, componentImportPath);
79
+ return resolveModulePath(basePath);
80
+ }
81
+
82
+ function collectTopLevelDeclarations(sourceFile: ts.SourceFile): {
83
+ typeDeclarations: Map<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>;
84
+ functionDeclarations: Map<string, ts.FunctionDeclaration>;
85
+ variableDeclarations: Map<string, ts.VariableDeclaration>;
86
+ } {
87
+ const typeDeclarations = new Map<string, ts.InterfaceDeclaration | ts.TypeAliasDeclaration>();
88
+ const functionDeclarations = new Map<string, ts.FunctionDeclaration>();
89
+ const variableDeclarations = new Map<string, ts.VariableDeclaration>();
90
+
91
+ for (const node of sourceFile.statements) {
92
+ if (ts.isInterfaceDeclaration(node)) {
93
+ typeDeclarations.set(node.name.text, node);
94
+ continue;
95
+ }
96
+
97
+ if (ts.isTypeAliasDeclaration(node)) {
98
+ typeDeclarations.set(node.name.text, node);
99
+ continue;
100
+ }
101
+
102
+ if (ts.isFunctionDeclaration(node) && node.name) {
103
+ functionDeclarations.set(node.name.text, node);
104
+ continue;
105
+ }
106
+
107
+ if (ts.isVariableStatement(node)) {
108
+ for (const declaration of node.declarationList.declarations) {
109
+ if (ts.isIdentifier(declaration.name)) {
110
+ variableDeclarations.set(declaration.name.text, declaration);
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ return { typeDeclarations, functionDeclarations, variableDeclarations };
117
+ }
118
+
119
+ function readDefaultValue(expression: ts.Expression): unknown {
120
+ if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
121
+ return expression.text;
122
+ }
123
+
124
+ if (ts.isNumericLiteral(expression)) {
125
+ return Number(expression.text);
126
+ }
127
+
128
+ if (expression.kind === ts.SyntaxKind.TrueKeyword) return true;
129
+ if (expression.kind === ts.SyntaxKind.FalseKeyword) return false;
130
+ if (expression.kind === ts.SyntaxKind.NullKeyword) return null;
131
+
132
+ if (
133
+ ts.isPrefixUnaryExpression(expression) &&
134
+ expression.operator === ts.SyntaxKind.MinusToken &&
135
+ ts.isNumericLiteral(expression.operand)
136
+ ) {
137
+ return -Number(expression.operand.text);
138
+ }
139
+
140
+ return undefined;
141
+ }
142
+
143
+ function extractDefaultValues(
144
+ componentNode: ts.FunctionLikeDeclarationBase | null
145
+ ): Record<string, unknown> {
146
+ const defaults: Record<string, unknown> = {};
147
+ if (!componentNode?.parameters?.length) return defaults;
148
+
149
+ const firstParam = componentNode.parameters[0];
150
+ if (!ts.isObjectBindingPattern(firstParam.name)) return defaults;
151
+
152
+ for (const element of firstParam.name.elements) {
153
+ let propName: string | null = null;
154
+
155
+ if (element.propertyName) {
156
+ if (ts.isIdentifier(element.propertyName) || ts.isStringLiteral(element.propertyName)) {
157
+ propName = element.propertyName.text;
158
+ }
159
+ } else if (ts.isIdentifier(element.name)) {
160
+ propName = element.name.text;
161
+ }
162
+
163
+ if (!propName || !element.initializer) continue;
164
+
165
+ const value = readDefaultValue(element.initializer);
166
+ if (value !== undefined) {
167
+ defaults[propName] = value;
168
+ }
169
+ }
170
+
171
+ return defaults;
172
+ }
173
+
174
+ function isNullishType(type: ts.Type): boolean {
175
+ return (
176
+ (type.flags & ts.TypeFlags.Null) !== 0 ||
177
+ (type.flags & ts.TypeFlags.Undefined) !== 0 ||
178
+ (type.flags & ts.TypeFlags.Void) !== 0
179
+ );
180
+ }
181
+
182
+ function isBooleanLikeType(type: ts.Type): boolean {
183
+ return (
184
+ (type.flags & ts.TypeFlags.BooleanLike) !== 0 ||
185
+ type.flags === ts.TypeFlags.BooleanLiteral
186
+ );
187
+ }
188
+
189
+ function inferPropType(
190
+ type: ts.Type,
191
+ checker: ts.TypeChecker
192
+ ): Pick<AutoDetectedPropDefinition, "type" | "values"> {
193
+ const typeText = checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation);
194
+
195
+ if (typeText.includes("ReactNode")) {
196
+ return { type: "node" };
197
+ }
198
+ if (typeText.includes("ReactElement") || typeText.includes("JSX.Element")) {
199
+ return { type: "element" };
200
+ }
201
+
202
+ if (type.getCallSignatures().length > 0) {
203
+ return { type: "function" };
204
+ }
205
+
206
+ if (checker.isArrayType(type) || checker.isTupleType(type)) {
207
+ return { type: "array" };
208
+ }
209
+
210
+ if (type.isUnion()) {
211
+ const nonNullableTypes = type.types.filter((unionType) => !isNullishType(unionType));
212
+
213
+ if (nonNullableTypes.length === 1) {
214
+ return inferPropType(nonNullableTypes[0], checker);
215
+ }
216
+
217
+ const stringLiteralValues = nonNullableTypes
218
+ .filter((unionType) => (unionType.flags & ts.TypeFlags.StringLiteral) !== 0)
219
+ .map((unionType) => (unionType as ts.StringLiteralType).value);
220
+
221
+ if (stringLiteralValues.length > 0 && stringLiteralValues.length === nonNullableTypes.length) {
222
+ return { type: "enum", values: stringLiteralValues };
223
+ }
224
+
225
+ if (nonNullableTypes.every((unionType) => isBooleanLikeType(unionType))) {
226
+ return { type: "boolean" };
227
+ }
228
+
229
+ return { type: "union" };
230
+ }
231
+
232
+ if ((type.flags & ts.TypeFlags.StringLike) !== 0) {
233
+ return { type: "string" };
234
+ }
235
+ if ((type.flags & ts.TypeFlags.NumberLike) !== 0) {
236
+ return { type: "number" };
237
+ }
238
+ if ((type.flags & ts.TypeFlags.BooleanLike) !== 0) {
239
+ return { type: "boolean" };
240
+ }
241
+
242
+ if ((type.flags & ts.TypeFlags.Object) !== 0) {
243
+ return { type: "object" };
244
+ }
245
+
246
+ return { type: "custom" };
247
+ }
248
+
249
+ function resolveComponentSignature(
250
+ exportName: string,
251
+ declarations: ReturnType<typeof collectTopLevelDeclarations>,
252
+ sourceFile: ts.SourceFile
253
+ ): ResolvedComponentSignature {
254
+ const visitedNames = new Set<string>();
255
+
256
+ const typeNodeFromFunction = (
257
+ node: ts.FunctionLikeDeclarationBase
258
+ ): ResolvedComponentSignature => ({
259
+ propsTypeNode: node.parameters[0]?.type ?? null,
260
+ componentNode: node,
261
+ });
262
+
263
+ const resolveFromExpression = (expression: ts.Expression): ResolvedComponentSignature => {
264
+ if (ts.isParenthesizedExpression(expression)) {
265
+ return resolveFromExpression(expression.expression);
266
+ }
267
+ if (ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression)) {
268
+ return resolveFromExpression(expression.expression);
269
+ }
270
+ if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
271
+ return typeNodeFromFunction(expression);
272
+ }
273
+ if (ts.isIdentifier(expression)) {
274
+ return resolveFromIdentifier(expression.text);
275
+ }
276
+
277
+ if (ts.isCallExpression(expression)) {
278
+ if (
279
+ ts.isPropertyAccessExpression(expression.expression) &&
280
+ expression.expression.name.text === "forwardRef"
281
+ ) {
282
+ const forwardRefPropsType = expression.typeArguments?.[1] ?? null;
283
+ const innerArg = expression.arguments[0];
284
+ const inner = innerArg && (ts.isArrowFunction(innerArg) || ts.isFunctionExpression(innerArg))
285
+ ? typeNodeFromFunction(innerArg)
286
+ : innerArg && ts.isIdentifier(innerArg)
287
+ ? resolveFromIdentifier(innerArg.text)
288
+ : { propsTypeNode: null, componentNode: null };
289
+
290
+ return {
291
+ propsTypeNode: forwardRefPropsType ?? inner.propsTypeNode,
292
+ componentNode: inner.componentNode,
293
+ };
294
+ }
295
+
296
+ if (
297
+ ts.isPropertyAccessExpression(expression.expression) &&
298
+ expression.expression.name.text === "memo" &&
299
+ expression.arguments[0]
300
+ ) {
301
+ return resolveFromExpression(expression.arguments[0]);
302
+ }
303
+
304
+ if (
305
+ ts.isPropertyAccessExpression(expression.expression) &&
306
+ expression.expression.expression.getText(sourceFile) === "Object" &&
307
+ expression.expression.name.text === "assign" &&
308
+ expression.arguments[0]
309
+ ) {
310
+ return resolveFromExpression(expression.arguments[0]);
311
+ }
312
+ }
313
+
314
+ return { propsTypeNode: null, componentNode: null };
315
+ };
316
+
317
+ const resolveFromVariable = (declaration: ts.VariableDeclaration): ResolvedComponentSignature => {
318
+ if (
319
+ declaration.type &&
320
+ ts.isTypeReferenceNode(declaration.type) &&
321
+ declaration.type.typeArguments?.length
322
+ ) {
323
+ const typeName = declaration.type.typeName.getText(sourceFile);
324
+ if (typeName.includes("FC") || typeName.includes("FunctionComponent")) {
325
+ const componentNode =
326
+ declaration.initializer &&
327
+ (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))
328
+ ? declaration.initializer
329
+ : null;
330
+
331
+ return {
332
+ propsTypeNode: declaration.type.typeArguments[0] ?? null,
333
+ componentNode,
334
+ };
335
+ }
336
+ }
337
+
338
+ if (declaration.initializer) {
339
+ return resolveFromExpression(declaration.initializer);
340
+ }
341
+
342
+ return { propsTypeNode: null, componentNode: null };
343
+ };
344
+
345
+ const resolveFromIdentifier = (name: string): ResolvedComponentSignature => {
346
+ if (!name || visitedNames.has(name)) {
347
+ return { propsTypeNode: null, componentNode: null };
348
+ }
349
+ visitedNames.add(name);
350
+
351
+ const functionDeclaration = declarations.functionDeclarations.get(name);
352
+ if (functionDeclaration) {
353
+ return typeNodeFromFunction(functionDeclaration);
354
+ }
355
+
356
+ const variableDeclaration = declarations.variableDeclarations.get(name);
357
+ if (variableDeclaration) {
358
+ return resolveFromVariable(variableDeclaration);
359
+ }
360
+
361
+ return { propsTypeNode: null, componentNode: null };
362
+ };
363
+
364
+ return resolveFromIdentifier(exportName);
365
+ }
366
+
367
+ /**
368
+ * Extract custom component props from a source file.
369
+ * Custom props are identified as props whose declarations originate from the component's source file.
370
+ */
371
+ export function extractCustomPropsFromComponentFile(
372
+ componentFilePath: string,
373
+ exportName: string
374
+ ): AutoPropsExtractionResult {
375
+ const warnings: string[] = [];
376
+ const resolvedPath = resolve(componentFilePath);
377
+
378
+ if (!existsSync(resolvedPath)) {
379
+ return {
380
+ props: {},
381
+ warnings: [`Component file not found: ${resolvedPath}`],
382
+ resolved: false,
383
+ };
384
+ }
385
+
386
+ const compilerOptions: ts.CompilerOptions = {
387
+ target: ts.ScriptTarget.ESNext,
388
+ module: ts.ModuleKind.ESNext,
389
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
390
+ jsx: ts.JsxEmit.ReactJSX,
391
+ allowSyntheticDefaultImports: true,
392
+ esModuleInterop: true,
393
+ skipLibCheck: true,
394
+ strict: false,
395
+ noEmit: true,
396
+ };
397
+
398
+ const program = ts.createProgram([resolvedPath], compilerOptions);
399
+ const sourceFile = program.getSourceFile(resolvedPath);
400
+ if (!sourceFile) {
401
+ return {
402
+ props: {},
403
+ warnings: [`Unable to parse component source: ${resolvedPath}`],
404
+ resolved: false,
405
+ };
406
+ }
407
+
408
+ const checker = program.getTypeChecker();
409
+ const declarations = collectTopLevelDeclarations(sourceFile);
410
+ const signature = resolveComponentSignature(exportName, declarations, sourceFile);
411
+
412
+ if (!signature.propsTypeNode) {
413
+ return {
414
+ props: {},
415
+ warnings: [`Unable to resolve props type for export: ${exportName}`],
416
+ resolved: false,
417
+ };
418
+ }
419
+
420
+ const propsType = checker.getTypeFromTypeNode(signature.propsTypeNode);
421
+ const defaultValues = extractDefaultValues(signature.componentNode);
422
+ const sourceFilePath = toPosixPath(sourceFile.fileName);
423
+
424
+ const extractedProps: Record<string, AutoDetectedPropDefinition> = {};
425
+ for (const symbol of checker.getPropertiesOfType(propsType)) {
426
+ const propName = symbol.getName();
427
+ if (propName.startsWith("_") || propName.startsWith("$")) {
428
+ continue;
429
+ }
430
+
431
+ const declarationsForSymbol = symbol.getDeclarations() ?? [];
432
+ const localDeclarations = declarationsForSymbol.filter(
433
+ (declaration) => toPosixPath(declaration.getSourceFile().fileName) === sourceFilePath
434
+ );
435
+
436
+ if (localDeclarations.length === 0) {
437
+ continue;
438
+ }
439
+
440
+ const referenceNode = localDeclarations[0];
441
+ const inferredType = inferPropType(checker.getTypeOfSymbolAtLocation(symbol, referenceNode), checker);
442
+ const description = ts
443
+ .displayPartsToString(symbol.getDocumentationComment(checker))
444
+ .trim();
445
+
446
+ extractedProps[propName] = {
447
+ type: inferredType.type,
448
+ description,
449
+ required: (symbol.getFlags() & ts.SymbolFlags.Optional) === 0,
450
+ ...(inferredType.values && { values: inferredType.values }),
451
+ ...(defaultValues[propName] !== undefined && { default: defaultValues[propName] }),
452
+ };
453
+ }
454
+
455
+ if (Object.keys(extractedProps).length === 0) {
456
+ warnings.push(`Resolved props type for ${exportName}, but no local custom props were found`);
457
+ }
458
+
459
+ return {
460
+ props: extractedProps,
461
+ warnings,
462
+ resolved: true,
463
+ };
464
+ }
@@ -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