@fragments-sdk/cli 0.14.2 → 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
@@ -456,6 +456,28 @@ function generateScssSeedImport(brand?: string): string {
456
456
  `;
457
457
  }
458
458
 
459
+ // ============================================
460
+ // Step runner
461
+ // ============================================
462
+
463
+ async function runSetupStep(
464
+ fn: () => Promise<{ modified: boolean; message: string }>,
465
+ failureLabel: string,
466
+ actions: string[],
467
+ errors: string[],
468
+ ): Promise<void> {
469
+ try {
470
+ const result = await fn();
471
+ const icon = result.modified ? pc.green('+') : pc.dim('·');
472
+ console.log(` ${icon} ${result.message}`);
473
+ if (result.modified) actions.push(result.message);
474
+ } catch (error) {
475
+ const msg = `${failureLabel}: ${error instanceof Error ? error.message : error}`;
476
+ console.log(` ${pc.red('✗')} ${msg}`);
477
+ errors.push(msg);
478
+ }
479
+ }
480
+
459
481
  // ============================================
460
482
  // Main Setup Function
461
483
  // ============================================
@@ -491,72 +513,27 @@ export async function setup(options: SetupOptions = {}): Promise<SetupResult> {
491
513
 
492
514
  // 3. Add styles import
493
515
  if (entryFile) {
494
- try {
495
- const result = await addStylesImport(root, entryFile);
496
- const icon = result.modified ? pc.green('+') : pc.dim('·');
497
- console.log(` ${icon} ${result.message}`);
498
- if (result.modified) actions.push(result.message);
499
- } catch (error) {
500
- const msg = `Failed to add styles import: ${error instanceof Error ? error.message : error}`;
501
- console.log(` ${pc.red('✗')} ${msg}`);
502
- errors.push(msg);
503
- }
516
+ await runSetupStep(() => addStylesImport(root, entryFile), 'Failed to add styles import', actions, errors);
504
517
  }
505
518
 
506
519
  // 4. Add ThemeProvider imports
507
520
  if (entryFile) {
508
- try {
509
- const result = await addThemeProvider(root, entryFile, framework);
510
- const icon = result.modified ? pc.green('+') : pc.dim('·');
511
- console.log(` ${icon} ${result.message}`);
512
- if (result.modified) actions.push(result.message);
513
- } catch (error) {
514
- const msg = `Failed to add ThemeProvider: ${error instanceof Error ? error.message : error}`;
515
- console.log(` ${pc.red('✗')} ${msg}`);
516
- errors.push(msg);
517
- }
521
+ await runSetupStep(() => addThemeProvider(root, entryFile, framework), 'Failed to add ThemeProvider', actions, errors);
518
522
  }
519
523
 
520
524
  // 5. Next.js: add transpilePackages
521
525
  if (framework === 'nextjs-app' || framework === 'nextjs-pages') {
522
- try {
523
- const result = await addTranspilePackages(root);
524
- const icon = result.modified ? pc.green('+') : pc.dim('·');
525
- console.log(` ${icon} ${result.message}`);
526
- if (result.modified) actions.push(result.message);
527
- } catch (error) {
528
- const msg = `Failed to update next.config: ${error instanceof Error ? error.message : error}`;
529
- console.log(` ${pc.red('✗')} ${msg}`);
530
- errors.push(msg);
531
- }
526
+ await runSetupStep(() => addTranspilePackages(root), 'Failed to update next.config', actions, errors);
532
527
  }
533
528
 
534
529
  // 6. Create SCSS seeds file (if --scss flag or brand color specified)
535
530
  if (options.scss || options.brand) {
536
- try {
537
- const result = await createScssSeeds(root, options.brand);
538
- const icon = result.modified ? pc.green('+') : pc.dim('·');
539
- console.log(` ${icon} ${result.message}`);
540
- if (result.modified) actions.push(result.message);
541
- } catch (error) {
542
- const msg = `Failed to create SCSS seeds: ${error instanceof Error ? error.message : error}`;
543
- console.log(` ${pc.red('✗')} ${msg}`);
544
- errors.push(msg);
545
- }
531
+ await runSetupStep(() => createScssSeeds(root, options.brand), 'Failed to create SCSS seeds', actions, errors);
546
532
  }
547
533
 
548
534
  // 7. Configure MCP server (if --mcp flag)
549
535
  if (options.mcp) {
550
- try {
551
- const result = await setupMcpConfig(root);
552
- const icon = result.modified ? pc.green('+') : pc.dim('·');
553
- console.log(` ${icon} ${result.message}`);
554
- if (result.modified) actions.push(result.message);
555
- } catch (error) {
556
- const msg = `Failed to configure MCP: ${error instanceof Error ? error.message : error}`;
557
- console.log(` ${pc.red('✗')} ${msg}`);
558
- errors.push(msg);
559
- }
536
+ await runSetupStep(() => setupMcpConfig(root), 'Failed to configure MCP', actions, errors);
560
537
  }
561
538
 
562
539
  // Summary
@@ -0,0 +1,113 @@
1
+ /**
2
+ * fragments tokens generate — Generate CSS, SCSS, Tailwind, or Figma output
3
+ * from a DTCG .tokens.json source file.
4
+ */
5
+
6
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
7
+ import { resolve, dirname, basename, extname } from 'node:path';
8
+ import pc from 'picocolors';
9
+ import {
10
+ generateCSSCustomProperties,
11
+ generateSCSSVariables,
12
+ generateTailwindConfig,
13
+ generateFigmaVariables,
14
+ } from '../core/index.js';
15
+ import type { DTCGTokenFile } from '../core/index.js';
16
+
17
+ export interface TokensGenerateOptions {
18
+ /** Path to DTCG .tokens.json source file */
19
+ from: string;
20
+ /** Output formats (comma-separated: css, scss, tailwind, figma) */
21
+ format: string;
22
+ /** Output directory */
23
+ out?: string;
24
+ /** Token name prefix */
25
+ prefix?: string;
26
+ /** CSS selector for custom properties (default: ':root') */
27
+ selector?: string;
28
+ /** Verbose output */
29
+ verbose?: boolean;
30
+ }
31
+
32
+ type OutputFormat = 'css' | 'scss' | 'tailwind' | 'figma';
33
+
34
+ const VALID_FORMATS = new Set<OutputFormat>(['css', 'scss', 'tailwind', 'figma']);
35
+
36
+ export async function tokensGenerate(options: TokensGenerateOptions): Promise<void> {
37
+ const { from, format, out, prefix, selector, verbose } = options;
38
+
39
+ // Parse formats
40
+ const formats = format.split(',').map((f) => f.trim().toLowerCase()) as OutputFormat[];
41
+ const invalidFormats = formats.filter((f) => !VALID_FORMATS.has(f));
42
+ if (invalidFormats.length > 0) {
43
+ console.error(pc.red(`Invalid format(s): ${invalidFormats.join(', ')}`));
44
+ console.error(`Valid formats: ${[...VALID_FORMATS].join(', ')}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ // Read source file
49
+ const sourcePath = resolve(process.cwd(), from);
50
+ let content: string;
51
+ try {
52
+ content = await readFile(sourcePath, 'utf-8');
53
+ } catch {
54
+ console.error(pc.red(`Could not read file: ${sourcePath}`));
55
+ process.exit(1);
56
+ }
57
+
58
+ let tokens: DTCGTokenFile;
59
+ try {
60
+ tokens = JSON.parse(content) as DTCGTokenFile;
61
+ } catch {
62
+ console.error(pc.red(`Invalid JSON in: ${sourcePath}`));
63
+ process.exit(1);
64
+ }
65
+
66
+ // Determine output directory
67
+ const outDir = out ? resolve(process.cwd(), out) : dirname(sourcePath);
68
+ await mkdir(outDir, { recursive: true });
69
+
70
+ const baseName = basename(from, extname(from)).replace(/\.tokens$/, '');
71
+
72
+ // Generate each format
73
+ for (const fmt of formats) {
74
+ let output: string;
75
+ let fileName: string;
76
+
77
+ switch (fmt) {
78
+ case 'css': {
79
+ output = generateCSSCustomProperties(tokens, { prefix, selector });
80
+ fileName = `${baseName}.css`;
81
+ break;
82
+ }
83
+ case 'scss': {
84
+ output = generateSCSSVariables(tokens, { prefix });
85
+ fileName = `_${baseName}.scss`;
86
+ break;
87
+ }
88
+ case 'tailwind': {
89
+ const config = generateTailwindConfig(tokens);
90
+ output = `// Auto-generated Tailwind config from DTCG tokens\n// Do not edit directly\nexport default ${JSON.stringify(config, null, 2)};\n`;
91
+ fileName = `tailwind.tokens.js`;
92
+ break;
93
+ }
94
+ case 'figma': {
95
+ const collections = generateFigmaVariables(tokens);
96
+ output = JSON.stringify(collections, null, 2);
97
+ fileName = `${baseName}.figma-variables.json`;
98
+ break;
99
+ }
100
+ default:
101
+ continue;
102
+ }
103
+
104
+ const outputPath = resolve(outDir, fileName);
105
+ await writeFile(outputPath, output, 'utf-8');
106
+
107
+ if (verbose) {
108
+ console.log(pc.green(` ✓ ${fmt}`), pc.dim(outputPath));
109
+ }
110
+ }
111
+
112
+ console.log(pc.green(`Generated ${formats.length} output(s) from ${from}`));
113
+ }
@@ -6,7 +6,14 @@
6
6
  */
7
7
 
8
8
  import pc from 'picocolors';
9
- import { BRAND } from '../core/index.js';
9
+ import { readFile } from 'node:fs/promises';
10
+ import { resolve } from 'node:path';
11
+ import { existsSync } from 'node:fs';
12
+ import {
13
+ BRAND,
14
+ type CompiledFragment,
15
+ type CompiledFragmentsFile,
16
+ } from '../core/index.js';
10
17
  import { loadConfig } from '../core/node.js';
11
18
  import {
12
19
  createDevServerClient,
@@ -86,6 +93,10 @@ export async function verify(
86
93
  console.log(pc.dim(`Minimum compliance: ${minCompliance}%\n`));
87
94
  }
88
95
 
96
+ if (ci) {
97
+ return verifyFromLocalFragments(configPath, minCompliance, component);
98
+ }
99
+
89
100
  // Check if dev server is reachable
90
101
  const isReachable = await client.ping();
91
102
  if (!isReachable) {
@@ -213,3 +224,186 @@ export async function verify(
213
224
 
214
225
  return summary;
215
226
  }
227
+
228
+ /**
229
+ * Compute metadata completeness score for a compiled fragment.
230
+ *
231
+ * Scoring breakdown (max 100):
232
+ * - Has description: +20
233
+ * - Has category that isn't "Components": +15
234
+ * - Has when/whenNot usage guidelines: +20
235
+ * - Has props with types: +20
236
+ * - Has variants: +15
237
+ * - Has relations: +10
238
+ */
239
+ function computeMetadataScore(fragment: CompiledFragment): {
240
+ score: number;
241
+ violations: ViolationItem[];
242
+ } {
243
+ let score = 0;
244
+ const violations: ViolationItem[] = [];
245
+
246
+ // Description (+20)
247
+ if (fragment.meta.description && fragment.meta.description.trim().length > 0) {
248
+ score += 20;
249
+ } else {
250
+ violations.push({
251
+ property: 'meta.description',
252
+ issue: 'Missing component description',
253
+ severity: 'warning',
254
+ suggestion: 'Add a description to the fragment definition',
255
+ });
256
+ }
257
+
258
+ // Category that isn't the default "Components" (+15)
259
+ if (fragment.meta.category && fragment.meta.category !== 'Components') {
260
+ score += 15;
261
+ } else {
262
+ violations.push({
263
+ property: 'meta.category',
264
+ issue: fragment.meta.category === 'Components'
265
+ ? 'Using default category "Components"'
266
+ : 'Missing category',
267
+ severity: 'warning',
268
+ suggestion: 'Set a specific category (e.g., "forms", "layout", "feedback")',
269
+ });
270
+ }
271
+
272
+ // Usage guidelines — when/whenNot (+20)
273
+ const hasWhen = fragment.usage?.when && fragment.usage.when.length > 0;
274
+ const hasWhenNot = fragment.usage?.whenNot && fragment.usage.whenNot.length > 0;
275
+ if (hasWhen || hasWhenNot) {
276
+ score += 20;
277
+ } else {
278
+ violations.push({
279
+ property: 'usage.when / usage.whenNot',
280
+ issue: 'Missing usage guidelines',
281
+ severity: 'warning',
282
+ suggestion: 'Add when[] and whenNot[] arrays to describe appropriate usage',
283
+ });
284
+ }
285
+
286
+ // Props with types (+20)
287
+ const propKeys = Object.keys(fragment.props ?? {});
288
+ if (propKeys.length > 0) {
289
+ score += 20;
290
+ } else {
291
+ violations.push({
292
+ property: 'props',
293
+ issue: 'No props defined',
294
+ severity: 'warning',
295
+ suggestion: 'Define props with types in the fragment definition',
296
+ });
297
+ }
298
+
299
+ // Variants (+15)
300
+ if (fragment.variants && fragment.variants.length > 0) {
301
+ score += 15;
302
+ } else {
303
+ violations.push({
304
+ property: 'variants',
305
+ issue: 'No variants defined',
306
+ severity: 'warning',
307
+ suggestion: 'Add at least one variant showing component usage',
308
+ });
309
+ }
310
+
311
+ // Relations (+10)
312
+ if (fragment.relations && fragment.relations.length > 0) {
313
+ score += 10;
314
+ } else {
315
+ violations.push({
316
+ property: 'relations',
317
+ issue: 'No component relations defined',
318
+ severity: 'warning',
319
+ suggestion: 'Define relations to related components (parent, child, alternative)',
320
+ });
321
+ }
322
+
323
+ return { score, violations };
324
+ }
325
+
326
+ /**
327
+ * CI fallback: verify components from the local fragments.json file
328
+ * without requiring a running dev server.
329
+ */
330
+ async function verifyFromLocalFragments(
331
+ configPath: string | undefined,
332
+ minCompliance: number,
333
+ component: string | undefined,
334
+ ): Promise<VerifySummary> {
335
+ // Load config to resolve outFile
336
+ const { config, configDir } = await loadConfig(configPath);
337
+ const outFile = config.outFile ?? 'fragments.json';
338
+ const fragmentsPath = resolve(configDir ?? process.cwd(), outFile);
339
+
340
+ if (!existsSync(fragmentsPath)) {
341
+ const error = {
342
+ error: `fragments.json not found at ${fragmentsPath}. Run "fragments build" first.`,
343
+ };
344
+ console.log(JSON.stringify(error));
345
+ process.exit(1);
346
+ }
347
+
348
+ const raw = await readFile(fragmentsPath, 'utf-8');
349
+ const data: CompiledFragmentsFile = JSON.parse(raw);
350
+
351
+ // Filter to only local fragments (skip anything from node_modules)
352
+ let entries = Object.entries(data.fragments).filter(
353
+ ([, frag]) => !frag.filePath.includes('node_modules')
354
+ );
355
+
356
+ // Filter by specific component if requested
357
+ if (component) {
358
+ entries = entries.filter(
359
+ ([name]) => name.toLowerCase() === component.toLowerCase()
360
+ );
361
+
362
+ if (entries.length === 0) {
363
+ const error = { error: `Component "${component}" not found in local fragments` };
364
+ console.log(JSON.stringify(error));
365
+ process.exit(1);
366
+ }
367
+ }
368
+
369
+ const results: VerifyResultItem[] = [];
370
+ let totalCompliance = 0;
371
+
372
+ for (const [name, fragment] of entries) {
373
+ const { score, violations } = computeMetadataScore(fragment);
374
+ const passed = score >= minCompliance;
375
+
376
+ results.push({
377
+ component: name,
378
+ compliance: score,
379
+ passed,
380
+ violations,
381
+ totalProperties: Object.keys(fragment.props ?? {}).length,
382
+ hardcoded: 0,
383
+ usingTokens: 0,
384
+ });
385
+
386
+ totalCompliance += score;
387
+ }
388
+
389
+ const componentCount = entries.length;
390
+ const averageCompliance = componentCount > 0 ? totalCompliance / componentCount : 100;
391
+ const allPassed = results.every(r => r.passed);
392
+
393
+ const summary: VerifySummary = {
394
+ passed: allPassed,
395
+ compliance: Math.round(averageCompliance * 100) / 100,
396
+ threshold: minCompliance,
397
+ totalComponents: componentCount,
398
+ passedComponents: results.filter(r => r.passed).length,
399
+ failedComponents: results.filter(r => !r.passed).length,
400
+ results,
401
+ violations: results.flatMap(r => r.violations.map(v => ({
402
+ component: r.component,
403
+ ...v,
404
+ }))),
405
+ };
406
+
407
+ console.log(JSON.stringify(summary, null, 2));
408
+ return summary;
409
+ }
@@ -0,0 +1,7 @@
1
+ import * as React from 'react';
2
+
3
+ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
4
+ return <input className={className} type={type} {...props} />;
5
+ }
6
+
7
+ export { Input };
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "allowSyntheticDefaultImports": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": ["react", "react-dom"]
12
+ },
13
+ "include": ["input.tsx"]
14
+ }
@@ -0,0 +1,11 @@
1
+ import * as React from 'react';
2
+ import { Primitive } from './primitive';
3
+
4
+ function Label({
5
+ className,
6
+ ...props
7
+ }: React.ComponentProps<typeof Primitive.Root>) {
8
+ return <Primitive.Root data-class={className} {...props} />;
9
+ }
10
+
11
+ export { Label };
@@ -0,0 +1,14 @@
1
+ import * as React from 'react';
2
+
3
+ export interface PrimitiveLabelProps {
4
+ className?: string;
5
+ htmlFor?: string;
6
+ form?: string;
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ function Root(props: PrimitiveLabelProps) {
11
+ return <label {...props} />;
12
+ }
13
+
14
+ export const Primitive = { Root };
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "allowSyntheticDefaultImports": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": ["react", "react-dom"]
12
+ },
13
+ "include": ["*.tsx"]
14
+ }
@@ -0,0 +1,11 @@
1
+ import * as React from 'react';
2
+ import { Label as LabelPrimitive } from 'radix-ui';
3
+
4
+ function Label({
5
+ className,
6
+ ...props
7
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
8
+ return <LabelPrimitive.Root data-class={className} {...props} />;
9
+ }
10
+
11
+ export { Label };
@@ -0,0 +1,12 @@
1
+ import * as React from 'react';
2
+
3
+ export interface LabelRootProps {
4
+ className?: string;
5
+ htmlFor?: string;
6
+ form?: string;
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ export const Label: {
11
+ Root: (props: LabelRootProps) => JSX.Element;
12
+ };
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "allowSyntheticDefaultImports": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": ["react", "react-dom"]
12
+ },
13
+ "include": ["*.tsx", "node_modules/**/*.d.ts"]
14
+ }