@fragments-sdk/cli 0.14.3 → 0.15.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 (135) hide show
  1. package/README.md +0 -3
  2. package/dist/bin.js +4290 -3754
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
  5. package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
  6. package/dist/chunk-32LIWN2P.js.map +1 -0
  7. package/dist/{chunk-55KERLWL.js → chunk-65WSVDV5.js} +314 -89
  8. package/dist/chunk-65WSVDV5.js.map +1 -0
  9. package/dist/chunk-7DZC4YEV.js +294 -0
  10. package/dist/chunk-7DZC4YEV.js.map +1 -0
  11. package/dist/{chunk-LOYS64QS.js → chunk-7WHVW72L.js} +230 -19
  12. package/dist/chunk-7WHVW72L.js.map +1 -0
  13. package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
  14. package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
  15. package/dist/{chunk-5A6X2Y73.js → chunk-CZD3AD4Q.js} +12 -11
  16. package/dist/chunk-CZD3AD4Q.js.map +1 -0
  17. package/dist/{chunk-EYXVAMEX.js → chunk-MN3TJ3D5.js} +72 -3
  18. package/dist/chunk-MN3TJ3D5.js.map +1 -0
  19. package/dist/chunk-QCN35LJU.js +630 -0
  20. package/dist/chunk-QCN35LJU.js.map +1 -0
  21. package/dist/chunk-T47OLCSF.js +36 -0
  22. package/dist/chunk-T47OLCSF.js.map +1 -0
  23. package/dist/{chunk-APTQIBS5.js → chunk-XJQ5BIWI.js} +144 -1049
  24. package/dist/chunk-XJQ5BIWI.js.map +1 -0
  25. package/dist/codebase-scanner-VOTPXRYW.js +22 -0
  26. package/dist/converter-JLINP7CJ.js +34 -0
  27. package/dist/converter-JLINP7CJ.js.map +1 -0
  28. package/dist/core/index.js +43 -1
  29. package/dist/{generate-RYWIPDN2.js → generate-A4FP5426.js} +3 -4
  30. package/dist/{generate-RYWIPDN2.js.map → generate-A4FP5426.js.map} +1 -1
  31. package/dist/govern-scan-UCBZR6D6.js +280 -0
  32. package/dist/govern-scan-UCBZR6D6.js.map +1 -0
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +11 -11
  35. package/dist/{init-WRUSW7R5.js → init-HGSM35XA.js} +131 -128
  36. package/dist/init-HGSM35XA.js.map +1 -0
  37. package/dist/{init-cloud-REQ3XLHO.js → init-cloud-MQ6GRJAZ.js} +2 -2
  38. package/dist/mcp-bin.js +5 -36
  39. package/dist/mcp-bin.js.map +1 -1
  40. package/dist/scan-VNNKACG2.js +15 -0
  41. package/dist/{scan-generate-TFZVL3BT.js → scan-generate-TWRHNU5M.js} +335 -46
  42. package/dist/scan-generate-TWRHNU5M.js.map +1 -0
  43. package/dist/scanner-7LAZYPWZ.js +13 -0
  44. package/dist/{service-HKJ6B7P7.js → service-FHQU7YS7.js} +27 -23
  45. package/dist/{snapshot-C5DYIGIV.js → snapshot-KQEQ6XHL.js} +2 -2
  46. package/dist/{static-viewer-DUVC4UIM.js → static-viewer-63PG6FWY.js} +3 -3
  47. package/dist/static-viewer-63PG6FWY.js.map +1 -0
  48. package/dist/{test-JW7JIDFG.js → test-UQYUCZIS.js} +4 -6
  49. package/dist/{test-JW7JIDFG.js.map → test-UQYUCZIS.js.map} +1 -1
  50. package/dist/{tokens-KE73G5JC.js → tokens-6GYKDV6U.js} +6 -5
  51. package/dist/{tokens-KE73G5JC.js.map → tokens-6GYKDV6U.js.map} +1 -1
  52. package/dist/tokens-generate-VTZV5EEW.js +86 -0
  53. package/dist/tokens-generate-VTZV5EEW.js.map +1 -0
  54. package/package.json +6 -6
  55. package/src/bin.ts +210 -48
  56. package/src/build.ts +130 -6
  57. package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
  58. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
  59. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
  60. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
  61. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
  62. package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
  63. package/src/commands/__tests__/init.test.ts +113 -0
  64. package/src/commands/__tests__/scan-generate.test.ts +188 -69
  65. package/src/commands/__tests__/verify.test.ts +91 -0
  66. package/src/commands/discover.ts +151 -0
  67. package/src/commands/enhance.ts +3 -1
  68. package/src/commands/govern-scan.ts +386 -0
  69. package/src/commands/govern.ts +2 -2
  70. package/src/commands/init.ts +152 -28
  71. package/src/commands/inspect.ts +290 -0
  72. package/src/commands/migrate-contract.ts +85 -0
  73. package/src/commands/scan-generate.ts +438 -50
  74. package/src/commands/scan.ts +1 -0
  75. package/src/commands/setup.ts +27 -50
  76. package/src/commands/tokens-generate.ts +113 -0
  77. package/src/commands/verify.ts +195 -1
  78. package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
  79. package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
  80. package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
  81. package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
  82. package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
  83. package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
  84. package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
  85. package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
  86. package/src/core/__tests__/contract-parity.test.ts +316 -0
  87. package/src/core/component-extractor.test.ts +39 -0
  88. package/src/core/component-extractor.ts +92 -1
  89. package/src/core/config.ts +2 -1
  90. package/src/core/discovery.ts +13 -2
  91. package/src/core/drift-verifier.ts +123 -0
  92. package/src/core/extractor-adapter.ts +80 -0
  93. package/src/mcp/__tests__/projectFields.test.ts +1 -1
  94. package/src/mcp/utils.ts +1 -50
  95. package/src/migrate/converter.ts +3 -3
  96. package/src/migrate/fragment-to-contract.ts +253 -0
  97. package/src/migrate/report.ts +1 -1
  98. package/src/scripts/token-benchmark.ts +121 -0
  99. package/src/service/__tests__/props-extractor.test.ts +94 -0
  100. package/src/service/__tests__/token-normalizer.test.ts +690 -0
  101. package/src/service/ast-utils.ts +4 -23
  102. package/src/service/babel-config.ts +23 -0
  103. package/src/service/enhance/converter.ts +61 -0
  104. package/src/service/enhance/props-extractor.ts +25 -8
  105. package/src/service/enhance/scanner.ts +5 -24
  106. package/src/service/snippet-validation.ts +9 -3
  107. package/src/service/token-normalizer.ts +510 -0
  108. package/src/shared/index.ts +1 -0
  109. package/src/shared/project-fields.ts +46 -0
  110. package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
  111. package/src/viewer/preview-adapter.ts +116 -0
  112. package/src/viewer/style-utils.ts +27 -412
  113. package/src/viewer/vite-plugin.ts +2 -2
  114. package/dist/chunk-55KERLWL.js.map +0 -1
  115. package/dist/chunk-5A6X2Y73.js.map +0 -1
  116. package/dist/chunk-APTQIBS5.js.map +0 -1
  117. package/dist/chunk-EYXVAMEX.js.map +0 -1
  118. package/dist/chunk-I34BC3CU.js.map +0 -1
  119. package/dist/chunk-LOYS64QS.js.map +0 -1
  120. package/dist/chunk-ZKTFKHWN.js +0 -324
  121. package/dist/chunk-ZKTFKHWN.js.map +0 -1
  122. package/dist/discovery-VDANZAJ2.js +0 -28
  123. package/dist/init-WRUSW7R5.js.map +0 -1
  124. package/dist/scan-YJHQIRKG.js +0 -14
  125. package/dist/scan-generate-TFZVL3BT.js.map +0 -1
  126. package/dist/viewer-2TZS3NDL.js +0 -2730
  127. package/dist/viewer-2TZS3NDL.js.map +0 -1
  128. package/src/commands/dev.ts +0 -107
  129. /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
  130. /package/dist/{discovery-VDANZAJ2.js.map → codebase-scanner-VOTPXRYW.js.map} +0 -0
  131. /package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-MQ6GRJAZ.js.map} +0 -0
  132. /package/dist/{scan-YJHQIRKG.js.map → scan-VNNKACG2.js.map} +0 -0
  133. /package/dist/{service-HKJ6B7P7.js.map → scanner-7LAZYPWZ.js.map} +0 -0
  134. /package/dist/{static-viewer-DUVC4UIM.js.map → service-FHQU7YS7.js.map} +0 -0
  135. /package/dist/{snapshot-C5DYIGIV.js.map → snapshot-KQEQ6XHL.js.map} +0 -0
@@ -6,10 +6,11 @@
6
6
  * accurate patches for token replacement.
7
7
  */
8
8
 
9
- import { parse, type ParserOptions } from "@babel/parser";
9
+ import { parse } from "@babel/parser";
10
10
  import traverse, { type NodePath } from "@babel/traverse";
11
11
  import generate from "@babel/generator";
12
12
  import * as t from "@babel/types";
13
+ import { BABEL_PARSER_OPTIONS } from "./babel-config.js";
13
14
 
14
15
  /**
15
16
  * Style location information
@@ -43,26 +44,6 @@ export interface StyleLocation {
43
44
  context?: string;
44
45
  }
45
46
 
46
- /**
47
- * Babel parser options for React/TypeScript files
48
- */
49
- const PARSER_OPTIONS: ParserOptions = {
50
- sourceType: "module",
51
- plugins: [
52
- "jsx",
53
- "typescript",
54
- ["decorators", { decoratorsBeforeExport: true }],
55
- "classProperties",
56
- "classPrivateProperties",
57
- "classPrivateMethods",
58
- "exportDefaultFrom",
59
- "exportNamespaceFrom",
60
- "dynamicImport",
61
- "nullishCoalescingOperator",
62
- "optionalChaining",
63
- "objectRestSpread",
64
- ],
65
- };
66
47
 
67
48
  /**
68
49
  * Extract style locations from a React/TypeScript source file
@@ -81,7 +62,7 @@ export function extractStyleLocations(
81
62
 
82
63
  let ast: t.File;
83
64
  try {
84
- ast = parse(sourceCode, PARSER_OPTIONS);
65
+ ast = parse(sourceCode, BABEL_PARSER_OPTIONS);
85
66
  } catch (error) {
86
67
  console.error(`Failed to parse ${filePath}:`, error);
87
68
  return locations;
@@ -267,7 +248,7 @@ export function applyPatch(
267
248
  ): PatchResult {
268
249
  let ast: t.File;
269
250
  try {
270
- ast = parse(sourceCode, PARSER_OPTIONS);
251
+ ast = parse(sourceCode, BABEL_PARSER_OPTIONS);
271
252
  } catch (error) {
272
253
  return {
273
254
  success: false,
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared Babel parser configuration for React/TypeScript files.
3
+ */
4
+
5
+ import type { ParserOptions } from "@babel/parser";
6
+
7
+ export const BABEL_PARSER_OPTIONS: ParserOptions = {
8
+ sourceType: "module",
9
+ plugins: [
10
+ "jsx",
11
+ "typescript",
12
+ ["decorators", { decoratorsBeforeExport: true }],
13
+ "classProperties",
14
+ "classPrivateProperties",
15
+ "classPrivateMethods",
16
+ "exportDefaultFrom",
17
+ "exportNamespaceFrom",
18
+ "dynamicImport",
19
+ "nullishCoalescingOperator",
20
+ "optionalChaining",
21
+ "objectRestSpread",
22
+ ],
23
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Converter — transforms scanner output (ComponentUsage[]) to governance engine input (UISpec)
3
+ *
4
+ * Bridge between the codebase scanner (Babel AST analysis) and the govern engine.
5
+ * Each file's usages become a UISpec with one UINode per component usage.
6
+ */
7
+
8
+ import { relative } from 'node:path';
9
+ import type { ComponentUsage } from './types.js';
10
+ import type { UISpec, UINode } from '@fragments-sdk/govern';
11
+
12
+ /**
13
+ * Convert a list of component usages from a single file into a UISpec
14
+ * suitable for the governance engine.
15
+ *
16
+ * Each ComponentUsage becomes a UINode:
17
+ * - id: `${relativePath}:${line}:${column}` (unique, traceable back to source)
18
+ * - type: componentName (e.g. "Button", "Card.Header")
19
+ * - props: merged static + dynamic markers + spread markers
20
+ */
21
+ export function usagesToSpec(
22
+ usages: ComponentUsage[],
23
+ filePath: string,
24
+ rootDir: string,
25
+ ): UISpec {
26
+ const relativePath = relative(rootDir, filePath);
27
+
28
+ const nodes: UINode[] = usages.map((usage) => ({
29
+ id: `${relativePath}:${usage.line}:${usage.column}`,
30
+ type: usage.componentName,
31
+ props: mergeProps(usage),
32
+ }));
33
+
34
+ return {
35
+ nodes,
36
+ metadata: {
37
+ agent: 'scan',
38
+ timestamp: new Date().toISOString(),
39
+ },
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Merge static props, dynamic prop markers, and spread markers into
45
+ * a single Record<string, unknown> for the UINode.
46
+ */
47
+ function mergeProps(usage: ComponentUsage): Record<string, unknown> {
48
+ const props: Record<string, unknown> = { ...usage.props.static };
49
+
50
+ // Mark dynamic props so governance rules can see them
51
+ for (const name of usage.props.dynamic) {
52
+ props[name] = '(dynamic)';
53
+ }
54
+
55
+ // Mark spread props
56
+ for (const name of usage.props.spreads) {
57
+ props[`...${name}`] = true;
58
+ }
59
+
60
+ return props;
61
+ }
@@ -586,13 +586,23 @@ function extractPropsFromInlineParams(
586
586
  // const ComponentName = (...) => ...
587
587
  if (ts.isVariableStatement(node)) {
588
588
  for (const decl of node.declarationList.declarations) {
589
- if (
590
- ts.isIdentifier(decl.name) &&
591
- decl.name.text === componentName &&
592
- decl.initializer &&
593
- (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))
594
- ) {
595
- targetFunc = decl.initializer;
589
+ if (ts.isIdentifier(decl.name) && decl.name.text === componentName && decl.initializer) {
590
+ if (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) {
591
+ targetFunc = decl.initializer;
592
+ }
593
+ // Handle React.forwardRef(...) or forwardRef(...)
594
+ else if (ts.isCallExpression(decl.initializer)) {
595
+ const callee = decl.initializer.expression;
596
+ const isForwardRef =
597
+ (ts.isPropertyAccessExpression(callee) && callee.name.text === 'forwardRef') ||
598
+ (ts.isIdentifier(callee) && callee.text === 'forwardRef');
599
+ if (isForwardRef && decl.initializer.arguments.length > 0) {
600
+ const firstArg = decl.initializer.arguments[0];
601
+ if (ts.isArrowFunction(firstArg) || ts.isFunctionExpression(firstArg)) {
602
+ targetFunc = firstArg;
603
+ }
604
+ }
605
+ }
596
606
  }
597
607
  }
598
608
  }
@@ -777,7 +787,12 @@ function inferComponentName(filePath: string): string {
777
787
  if (name === "index") {
778
788
  name = basename(dirname(filePath));
779
789
  }
780
- return name;
790
+
791
+ return name
792
+ .split(/[^a-zA-Z0-9]+/)
793
+ .filter(Boolean)
794
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
795
+ .join("");
781
796
  }
782
797
 
783
798
  /**
@@ -850,6 +865,7 @@ export async function extractPropsForComponent(
850
865
  if (existsSync(fullPath)) {
851
866
  return extractPropsFromFile(fullPath, {
852
867
  propsTypeName: `${componentName}Props`,
868
+ componentName,
853
869
  });
854
870
  }
855
871
  }
@@ -870,6 +886,7 @@ export async function extractAllComponentProps(
870
886
  try {
871
887
  const extraction = await extractPropsFromFile(filePath, {
872
888
  propsTypeName: `${componentName}Props`,
889
+ componentName,
873
890
  });
874
891
  results.set(componentName, extraction);
875
892
  } catch (error) {
@@ -6,7 +6,8 @@
6
6
  * - Component usages in JSX with props
7
7
  */
8
8
 
9
- import { parse, type ParserOptions } from "@babel/parser";
9
+ import { parse } from "@babel/parser";
10
+ import { BABEL_PARSER_OPTIONS } from "../babel-config.js";
10
11
  import _traverse from "@babel/traverse";
11
12
  import * as t from "@babel/types";
12
13
 
@@ -19,26 +20,6 @@ import type {
19
20
  UsageProps,
20
21
  } from "./types.js";
21
22
 
22
- /**
23
- * Babel parser options for React/TypeScript files
24
- */
25
- const PARSER_OPTIONS: ParserOptions = {
26
- sourceType: "module",
27
- plugins: [
28
- "jsx",
29
- "typescript",
30
- ["decorators", { decoratorsBeforeExport: true }],
31
- "classProperties",
32
- "classPrivateProperties",
33
- "classPrivateMethods",
34
- "exportDefaultFrom",
35
- "exportNamespaceFrom",
36
- "dynamicImport",
37
- "nullishCoalescingOperator",
38
- "optionalChaining",
39
- "objectRestSpread",
40
- ],
41
- };
42
23
 
43
24
  /**
44
25
  * Scan a file for component imports
@@ -56,7 +37,7 @@ export async function scanFileForImports(
56
37
 
57
38
  try {
58
39
  const sourceCode = await readFile(filePath, "utf-8");
59
- const ast = parse(sourceCode, PARSER_OPTIONS);
40
+ const ast = parse(sourceCode, BABEL_PARSER_OPTIONS);
60
41
 
61
42
  traverse(ast, {
62
43
  ImportDeclaration(path) {
@@ -128,7 +109,7 @@ export async function scanFileForUsages(
128
109
  try {
129
110
  const sourceCode = await readFile(filePath, "utf-8");
130
111
  const lines = sourceCode.split("\n");
131
- const ast = parse(sourceCode, PARSER_OPTIONS);
112
+ const ast = parse(sourceCode, BABEL_PARSER_OPTIONS);
132
113
 
133
114
  traverse(ast, {
134
115
  JSXOpeningElement(path) {
@@ -189,7 +170,7 @@ export async function scanFile(
189
170
  try {
190
171
  const sourceCode = await readFile(filePath, "utf-8");
191
172
  const lines = sourceCode.split("\n");
192
- const ast = parse(sourceCode, PARSER_OPTIONS);
173
+ const ast = parse(sourceCode, BABEL_PARSER_OPTIONS);
193
174
 
194
175
  // Track local names to component names mapping within this file
195
176
  const localToComponent = new Map<string, string>();
@@ -494,8 +494,11 @@ function sortAndFilterBatch(
494
494
  const getComponentName = (relativePath: string): string => {
495
495
  const normalized = relativePath.replace(/\\/g, '/');
496
496
  const fileName = normalized.split('/').pop() ?? normalized;
497
- if (fileName.endsWith(BRAND.fileExtension)) {
498
- return fileName.slice(0, -BRAND.fileExtension.length);
497
+ // Handle both V2 (.contract.json) and legacy (.fragment.tsx/.fragment.ts)
498
+ for (const ext of [BRAND.fileExtension, '.fragment.tsx', '.fragment.ts']) {
499
+ if (fileName.endsWith(ext)) {
500
+ return fileName.slice(0, -ext.length);
501
+ }
499
502
  }
500
503
  return extractComponentName(relativePath);
501
504
  };
@@ -572,7 +575,10 @@ export async function validateSnippetPolicy(
572
575
  const issues: FileIssue[] = [];
573
576
 
574
577
  const discovered = await discoverFragmentFiles(config, configDir);
575
- const fragmentFiles = discovered.filter((file) => file.relativePath.endsWith(BRAND.fileExtension));
578
+ // Snippet validation applies to TSX fragment files (not .contract.json which has no render code)
579
+ const fragmentFiles = discovered.filter((file) =>
580
+ file.relativePath.endsWith('.fragment.tsx') || file.relativePath.endsWith('.fragment.ts')
581
+ );
576
582
 
577
583
  const batchResult = sortAndFilterBatch(fragmentFiles, policy.componentStart, policy.componentLimit);
578
584
  if (batchResult.warning) {