@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.
- package/README.md +0 -3
- package/dist/bin.js +4290 -3754
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
- package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
- package/dist/chunk-32LIWN2P.js.map +1 -0
- package/dist/{chunk-55KERLWL.js → chunk-65WSVDV5.js} +314 -89
- package/dist/chunk-65WSVDV5.js.map +1 -0
- package/dist/chunk-7DZC4YEV.js +294 -0
- package/dist/chunk-7DZC4YEV.js.map +1 -0
- package/dist/{chunk-LOYS64QS.js → chunk-7WHVW72L.js} +230 -19
- package/dist/chunk-7WHVW72L.js.map +1 -0
- package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
- package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
- package/dist/{chunk-5A6X2Y73.js → chunk-CZD3AD4Q.js} +12 -11
- package/dist/chunk-CZD3AD4Q.js.map +1 -0
- package/dist/{chunk-EYXVAMEX.js → chunk-MN3TJ3D5.js} +72 -3
- package/dist/chunk-MN3TJ3D5.js.map +1 -0
- package/dist/chunk-QCN35LJU.js +630 -0
- package/dist/chunk-QCN35LJU.js.map +1 -0
- package/dist/chunk-T47OLCSF.js +36 -0
- package/dist/chunk-T47OLCSF.js.map +1 -0
- package/dist/{chunk-APTQIBS5.js → chunk-XJQ5BIWI.js} +144 -1049
- package/dist/chunk-XJQ5BIWI.js.map +1 -0
- package/dist/codebase-scanner-VOTPXRYW.js +22 -0
- package/dist/converter-JLINP7CJ.js +34 -0
- package/dist/converter-JLINP7CJ.js.map +1 -0
- package/dist/core/index.js +43 -1
- package/dist/{generate-RYWIPDN2.js → generate-A4FP5426.js} +3 -4
- package/dist/{generate-RYWIPDN2.js.map → generate-A4FP5426.js.map} +1 -1
- package/dist/govern-scan-UCBZR6D6.js +280 -0
- package/dist/govern-scan-UCBZR6D6.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +11 -11
- package/dist/{init-WRUSW7R5.js → init-HGSM35XA.js} +131 -128
- package/dist/init-HGSM35XA.js.map +1 -0
- package/dist/{init-cloud-REQ3XLHO.js → init-cloud-MQ6GRJAZ.js} +2 -2
- package/dist/mcp-bin.js +5 -36
- package/dist/mcp-bin.js.map +1 -1
- package/dist/scan-VNNKACG2.js +15 -0
- package/dist/{scan-generate-TFZVL3BT.js → scan-generate-TWRHNU5M.js} +335 -46
- package/dist/scan-generate-TWRHNU5M.js.map +1 -0
- package/dist/scanner-7LAZYPWZ.js +13 -0
- package/dist/{service-HKJ6B7P7.js → service-FHQU7YS7.js} +27 -23
- package/dist/{snapshot-C5DYIGIV.js → snapshot-KQEQ6XHL.js} +2 -2
- package/dist/{static-viewer-DUVC4UIM.js → static-viewer-63PG6FWY.js} +3 -3
- package/dist/static-viewer-63PG6FWY.js.map +1 -0
- package/dist/{test-JW7JIDFG.js → test-UQYUCZIS.js} +4 -6
- package/dist/{test-JW7JIDFG.js.map → test-UQYUCZIS.js.map} +1 -1
- package/dist/{tokens-KE73G5JC.js → tokens-6GYKDV6U.js} +6 -5
- package/dist/{tokens-KE73G5JC.js.map → tokens-6GYKDV6U.js.map} +1 -1
- package/dist/tokens-generate-VTZV5EEW.js +86 -0
- package/dist/tokens-generate-VTZV5EEW.js.map +1 -0
- package/package.json +6 -6
- package/src/bin.ts +210 -48
- package/src/build.ts +130 -6
- package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
- package/src/commands/__tests__/init.test.ts +113 -0
- package/src/commands/__tests__/scan-generate.test.ts +188 -69
- package/src/commands/__tests__/verify.test.ts +91 -0
- package/src/commands/discover.ts +151 -0
- package/src/commands/enhance.ts +3 -1
- package/src/commands/govern-scan.ts +386 -0
- package/src/commands/govern.ts +2 -2
- package/src/commands/init.ts +152 -28
- package/src/commands/inspect.ts +290 -0
- package/src/commands/migrate-contract.ts +85 -0
- package/src/commands/scan-generate.ts +438 -50
- package/src/commands/scan.ts +1 -0
- package/src/commands/setup.ts +27 -50
- package/src/commands/tokens-generate.ts +113 -0
- package/src/commands/verify.ts +195 -1
- package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
- package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
- package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
- package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
- package/src/core/__tests__/contract-parity.test.ts +316 -0
- package/src/core/component-extractor.test.ts +39 -0
- package/src/core/component-extractor.ts +92 -1
- package/src/core/config.ts +2 -1
- package/src/core/discovery.ts +13 -2
- package/src/core/drift-verifier.ts +123 -0
- package/src/core/extractor-adapter.ts +80 -0
- package/src/mcp/__tests__/projectFields.test.ts +1 -1
- package/src/mcp/utils.ts +1 -50
- package/src/migrate/converter.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +253 -0
- package/src/migrate/report.ts +1 -1
- package/src/scripts/token-benchmark.ts +121 -0
- package/src/service/__tests__/props-extractor.test.ts +94 -0
- package/src/service/__tests__/token-normalizer.test.ts +690 -0
- package/src/service/ast-utils.ts +4 -23
- package/src/service/babel-config.ts +23 -0
- package/src/service/enhance/converter.ts +61 -0
- package/src/service/enhance/props-extractor.ts +25 -8
- package/src/service/enhance/scanner.ts +5 -24
- package/src/service/snippet-validation.ts +9 -3
- package/src/service/token-normalizer.ts +510 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/project-fields.ts +46 -0
- package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
- package/src/viewer/preview-adapter.ts +116 -0
- package/src/viewer/style-utils.ts +27 -412
- package/src/viewer/vite-plugin.ts +2 -2
- package/dist/chunk-55KERLWL.js.map +0 -1
- package/dist/chunk-5A6X2Y73.js.map +0 -1
- package/dist/chunk-APTQIBS5.js.map +0 -1
- package/dist/chunk-EYXVAMEX.js.map +0 -1
- package/dist/chunk-I34BC3CU.js.map +0 -1
- package/dist/chunk-LOYS64QS.js.map +0 -1
- package/dist/chunk-ZKTFKHWN.js +0 -324
- package/dist/chunk-ZKTFKHWN.js.map +0 -1
- package/dist/discovery-VDANZAJ2.js +0 -28
- package/dist/init-WRUSW7R5.js.map +0 -1
- package/dist/scan-YJHQIRKG.js +0 -14
- package/dist/scan-generate-TFZVL3BT.js.map +0 -1
- package/dist/viewer-2TZS3NDL.js +0 -2730
- package/dist/viewer-2TZS3NDL.js.map +0 -1
- package/src/commands/dev.ts +0 -107
- /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
- /package/dist/{discovery-VDANZAJ2.js.map → codebase-scanner-VOTPXRYW.js.map} +0 -0
- /package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-MQ6GRJAZ.js.map} +0 -0
- /package/dist/{scan-YJHQIRKG.js.map → scan-VNNKACG2.js.map} +0 -0
- /package/dist/{service-HKJ6B7P7.js.map → scanner-7LAZYPWZ.js.map} +0 -0
- /package/dist/{static-viewer-DUVC4UIM.js.map → service-FHQU7YS7.js.map} +0 -0
- /package/dist/{snapshot-C5DYIGIV.js.map → snapshot-KQEQ6XHL.js.map} +0 -0
package/src/bin.ts
CHANGED
|
@@ -17,13 +17,14 @@ import { loadConfig } from './core/node.js';
|
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8')) as { version: string };
|
|
19
19
|
|
|
20
|
+
const EXPERIMENTAL = process.env.FRAGMENTS_EXPERIMENTAL === '1';
|
|
21
|
+
|
|
20
22
|
// Import command implementations
|
|
21
23
|
import { validate } from './commands/validate.js';
|
|
22
24
|
import { build } from './commands/build.js';
|
|
23
25
|
import { context } from './commands/context.js';
|
|
24
26
|
import { list } from './commands/list.js';
|
|
25
27
|
import { reset } from './commands/reset.js';
|
|
26
|
-
import { dev } from './commands/dev.js';
|
|
27
28
|
import { compare } from './commands/compare.js';
|
|
28
29
|
import { verify } from './commands/verify.js';
|
|
29
30
|
import { audit } from './commands/audit.js';
|
|
@@ -36,11 +37,14 @@ import { linkFigma, linkStorybook } from './commands/link/index.js';
|
|
|
36
37
|
import { enhance } from './commands/enhance.js';
|
|
37
38
|
import { scan } from './commands/scan.js';
|
|
38
39
|
import { graph } from './commands/graph.js';
|
|
40
|
+
import { inspect } from './commands/inspect.js';
|
|
41
|
+
import { discover } from './commands/discover.js';
|
|
39
42
|
import { perf } from './commands/perf.js';
|
|
40
43
|
import { doctor } from './commands/doctor.js';
|
|
41
44
|
import { setup } from './commands/setup.js';
|
|
42
45
|
import { sync } from './commands/sync.js';
|
|
43
46
|
import { governCheck, governInit, governReport, governConnect } from './commands/govern.js';
|
|
47
|
+
import { migrateContract } from './commands/migrate-contract.js';
|
|
44
48
|
|
|
45
49
|
// Import existing commands that were already extracted
|
|
46
50
|
import { runScreenshotCommand } from './screenshot.js';
|
|
@@ -304,37 +308,6 @@ linkCommand
|
|
|
304
308
|
}
|
|
305
309
|
});
|
|
306
310
|
|
|
307
|
-
// ============================================================================
|
|
308
|
-
// DEV COMMAND
|
|
309
|
-
// ============================================================================
|
|
310
|
-
program
|
|
311
|
-
.command('dev')
|
|
312
|
-
.description('Start the development server with live component rendering')
|
|
313
|
-
.option('-p, --port <port>', 'Port to run on', '6006')
|
|
314
|
-
.option('--no-open', 'Do not open browser')
|
|
315
|
-
.option('--skip-setup', 'Skip auto-setup (Storybook import, build, Figma link)')
|
|
316
|
-
.option('--skip-storybook', 'Skip auto-importing from Storybook')
|
|
317
|
-
.option('--skip-figma', 'Skip Figma link check')
|
|
318
|
-
.option('--skip-build', `Skip auto-building ${BRAND.outFile}`)
|
|
319
|
-
.action(async (options) => {
|
|
320
|
-
try {
|
|
321
|
-
await dev({
|
|
322
|
-
port: options.port,
|
|
323
|
-
open: options.open,
|
|
324
|
-
skipSetup: options.skipSetup,
|
|
325
|
-
skipStorybook: options.skipStorybook,
|
|
326
|
-
skipFigma: options.skipFigma,
|
|
327
|
-
skipBuild: options.skipBuild,
|
|
328
|
-
});
|
|
329
|
-
} catch (error) {
|
|
330
|
-
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
331
|
-
if (error instanceof Error && error.stack) {
|
|
332
|
-
console.error(pc.dim(error.stack));
|
|
333
|
-
}
|
|
334
|
-
process.exit(1);
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
|
|
338
311
|
// ============================================================================
|
|
339
312
|
// SCREENSHOT COMMAND
|
|
340
313
|
// ============================================================================
|
|
@@ -433,7 +406,7 @@ program
|
|
|
433
406
|
}
|
|
434
407
|
} catch (error) {
|
|
435
408
|
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
436
|
-
console.log(pc.dim(`\nMake sure
|
|
409
|
+
console.log(pc.dim(`\nMake sure a dev server is running on the expected port.`));
|
|
437
410
|
process.exit(1);
|
|
438
411
|
}
|
|
439
412
|
});
|
|
@@ -642,6 +615,33 @@ program
|
|
|
642
615
|
}
|
|
643
616
|
});
|
|
644
617
|
|
|
618
|
+
// ============================================================================
|
|
619
|
+
// MIGRATE-CONTRACT COMMAND
|
|
620
|
+
// ============================================================================
|
|
621
|
+
program
|
|
622
|
+
.command('migrate-contract')
|
|
623
|
+
.description('Migrate .fragment.tsx files to .contract.json format')
|
|
624
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
625
|
+
.option('--glob <pattern>', 'Glob pattern for fragment files', 'src/**/*.fragment.tsx')
|
|
626
|
+
.option('--dry-run', 'Preview migration without writing files')
|
|
627
|
+
.option('--tsconfig <path>', 'Path to tsconfig.json')
|
|
628
|
+
.action(async (options) => {
|
|
629
|
+
try {
|
|
630
|
+
const result = await migrateContract({
|
|
631
|
+
config: options.config,
|
|
632
|
+
glob: options.glob,
|
|
633
|
+
dryRun: options.dryRun,
|
|
634
|
+
tsconfig: options.tsconfig,
|
|
635
|
+
});
|
|
636
|
+
if (result.failed > 0) {
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
} catch (error) {
|
|
640
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
|
|
645
645
|
// ============================================================================
|
|
646
646
|
// STORYGEN COMMAND
|
|
647
647
|
// ============================================================================
|
|
@@ -714,7 +714,7 @@ program
|
|
|
714
714
|
} catch (error) {
|
|
715
715
|
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
716
716
|
if (action === 'update') {
|
|
717
|
-
console.log(pc.dim(`\nMake sure
|
|
717
|
+
console.log(pc.dim(`\nMake sure a dev server is running on the expected port.`));
|
|
718
718
|
}
|
|
719
719
|
process.exit(1);
|
|
720
720
|
}
|
|
@@ -823,27 +823,40 @@ program
|
|
|
823
823
|
// ============================================================================
|
|
824
824
|
// INIT COMMAND
|
|
825
825
|
// ============================================================================
|
|
826
|
-
program
|
|
826
|
+
const initCmd = program
|
|
827
827
|
.command('init')
|
|
828
828
|
.description('Initialize fragments in a project (zero-config by default)')
|
|
829
829
|
.option('--force', 'Overwrite existing config')
|
|
830
830
|
.option('-y, --yes', 'Non-interactive mode (now the default)')
|
|
831
|
-
.option('--cloud', 'Set up Fragments Cloud governance (zero-config browser auth)')
|
|
832
|
-
.option('--cloud-url <url>', 'Cloud dashboard URL (default: https://app.usefragments.com)')
|
|
833
|
-
.option('--port <port>', 'Localhost port for auth callback (default: 9876)')
|
|
834
|
-
.option('--auth-only', 'Only authenticate, skip project setup')
|
|
835
|
-
.option('--skip-check', 'Skip running the first governance check')
|
|
836
831
|
.option('--configure', 'Interactive mode for theme seeds, snapshots, etc.')
|
|
832
|
+
.option('--metadata-only', 'Generate config and metadata without modifying runtime app setup')
|
|
833
|
+
.option('--govern', 'Alias for --metadata-only')
|
|
837
834
|
.option('--scan <path>', 'Scan a TypeScript component directory and generate fragment files')
|
|
838
835
|
.option('--enrich', 'Use AI to fill knowledge fields during --scan (requires API key)')
|
|
839
836
|
.option('--dry-run', 'Show what --enrich would generate without calling API')
|
|
840
837
|
.option('--provider <provider>', 'AI provider for enrichment: anthropic or openai')
|
|
841
838
|
.option('--api-key <key>', 'API key for AI enrichment')
|
|
842
|
-
.option('--model <model>', 'Override AI model for enrichment')
|
|
843
|
-
|
|
839
|
+
.option('--model <model>', 'Override AI model for enrichment');
|
|
840
|
+
|
|
841
|
+
// Cloud governance flags — only visible with FRAGMENTS_EXPERIMENTAL=1
|
|
842
|
+
if (EXPERIMENTAL) {
|
|
843
|
+
initCmd
|
|
844
|
+
.option('--cloud', 'Set up Fragments Cloud governance (zero-config browser auth)')
|
|
845
|
+
.option('--cloud-url <url>', 'Cloud dashboard URL (default: https://app.usefragments.com)')
|
|
846
|
+
.option('--port <port>', 'Localhost port for auth callback (default: 9876)')
|
|
847
|
+
.option('--auth-only', 'Only authenticate, skip project setup')
|
|
848
|
+
.option('--skip-check', 'Skip running the first governance check');
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
initCmd.action(async (options) => {
|
|
844
852
|
try {
|
|
845
|
-
// Cloud init —
|
|
853
|
+
// Cloud init — experimental, requires FRAGMENTS_EXPERIMENTAL=1
|
|
846
854
|
if (options.cloud) {
|
|
855
|
+
if (!EXPERIMENTAL) {
|
|
856
|
+
console.log(pc.yellow(`\n Fragments Cloud is not yet publicly available.`));
|
|
857
|
+
console.log(pc.dim(` Set FRAGMENTS_EXPERIMENTAL=1 to enable preview features.\n`));
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
847
860
|
const { initCloud } = await import('./commands/init-cloud.js');
|
|
848
861
|
await initCloud({
|
|
849
862
|
url: options.cloudUrl,
|
|
@@ -866,6 +879,8 @@ program
|
|
|
866
879
|
provider: options.provider,
|
|
867
880
|
apiKey: options.apiKey,
|
|
868
881
|
model: options.model,
|
|
882
|
+
metadataOnly: options.metadataOnly,
|
|
883
|
+
govern: options.govern,
|
|
869
884
|
});
|
|
870
885
|
|
|
871
886
|
if (!result.success) {
|
|
@@ -912,11 +927,15 @@ program
|
|
|
912
927
|
});
|
|
913
928
|
|
|
914
929
|
// ============================================================================
|
|
915
|
-
// TOKENS COMMAND
|
|
930
|
+
// TOKENS COMMAND GROUP
|
|
916
931
|
// ============================================================================
|
|
917
|
-
program
|
|
932
|
+
const tokensCmd = program
|
|
918
933
|
.command('tokens')
|
|
919
|
-
.description('
|
|
934
|
+
.description('Design token discovery, listing, and generation');
|
|
935
|
+
|
|
936
|
+
tokensCmd
|
|
937
|
+
.command('list', { isDefault: true })
|
|
938
|
+
.description('Discover and list design tokens from CSS/SCSS/DTCG files')
|
|
920
939
|
.option('-c, --config <path>', 'Path to config file')
|
|
921
940
|
.option('--json', 'Output as JSON')
|
|
922
941
|
.option('--categories', 'Group tokens by category')
|
|
@@ -944,6 +963,32 @@ program
|
|
|
944
963
|
}
|
|
945
964
|
});
|
|
946
965
|
|
|
966
|
+
tokensCmd
|
|
967
|
+
.command('generate')
|
|
968
|
+
.description('Generate CSS, SCSS, Tailwind, or Figma output from a DTCG .tokens.json file')
|
|
969
|
+
.requiredOption('--from <path>', 'Path to DTCG .tokens.json source file')
|
|
970
|
+
.requiredOption('--format <formats>', 'Output formats (comma-separated: css, scss, tailwind, figma)')
|
|
971
|
+
.option('--out <dir>', 'Output directory (default: same directory as source)')
|
|
972
|
+
.option('--prefix <prefix>', 'Token name prefix')
|
|
973
|
+
.option('--selector <selector>', 'CSS selector for custom properties (default: :root)')
|
|
974
|
+
.option('--verbose', 'Verbose output')
|
|
975
|
+
.action(async (options) => {
|
|
976
|
+
try {
|
|
977
|
+
const { tokensGenerate } = await import('./commands/tokens-generate.js');
|
|
978
|
+
await tokensGenerate({
|
|
979
|
+
from: options.from,
|
|
980
|
+
format: options.format,
|
|
981
|
+
out: options.out,
|
|
982
|
+
prefix: options.prefix,
|
|
983
|
+
selector: options.selector,
|
|
984
|
+
verbose: options.verbose,
|
|
985
|
+
});
|
|
986
|
+
} catch (error) {
|
|
987
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
|
|
947
992
|
// ============================================================================
|
|
948
993
|
// GENERATE COMMAND
|
|
949
994
|
// ============================================================================
|
|
@@ -995,6 +1040,71 @@ program
|
|
|
995
1040
|
}
|
|
996
1041
|
});
|
|
997
1042
|
|
|
1043
|
+
// ============================================================================
|
|
1044
|
+
// INSPECT COMMAND
|
|
1045
|
+
// ============================================================================
|
|
1046
|
+
program
|
|
1047
|
+
.command('inspect')
|
|
1048
|
+
.description('Inspect a single component from compiled fragments data')
|
|
1049
|
+
.argument('<component>', 'Component name to inspect')
|
|
1050
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
1051
|
+
.option('--fields <fields>', 'Comma-separated fields to include (dot notation: meta, props, examples)')
|
|
1052
|
+
.option('--variant <name>', 'Filter to a specific variant')
|
|
1053
|
+
.option('--verbosity <level>', 'Output verbosity: compact, standard, full', 'standard')
|
|
1054
|
+
.option('--maxExamples <n>', 'Max variant examples to include', (v) => Number.parseInt(v, 10))
|
|
1055
|
+
.option('--json', 'Output as JSON')
|
|
1056
|
+
.action(async (component, options) => {
|
|
1057
|
+
try {
|
|
1058
|
+
await inspect(component, {
|
|
1059
|
+
config: options.config,
|
|
1060
|
+
fields: options.fields,
|
|
1061
|
+
variant: options.variant,
|
|
1062
|
+
verbosity: options.verbosity,
|
|
1063
|
+
maxExamples: options.maxExamples,
|
|
1064
|
+
json: options.json,
|
|
1065
|
+
});
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
if (options.json) {
|
|
1068
|
+
console.log(JSON.stringify({ error: error instanceof Error ? error.message : 'Inspect failed' }));
|
|
1069
|
+
} else {
|
|
1070
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1071
|
+
}
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// ============================================================================
|
|
1077
|
+
// DISCOVER COMMAND
|
|
1078
|
+
// ============================================================================
|
|
1079
|
+
program
|
|
1080
|
+
.command('discover')
|
|
1081
|
+
.description('List and filter components from compiled fragments data')
|
|
1082
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
1083
|
+
.option('--search <term>', 'Search by name, description, or tags')
|
|
1084
|
+
.option('--category <category>', 'Filter by category')
|
|
1085
|
+
.option('--status <status>', 'Filter by status: stable, beta, deprecated, experimental')
|
|
1086
|
+
.option('--compact', 'Output component names only')
|
|
1087
|
+
.option('--json', 'Output as JSON')
|
|
1088
|
+
.action(async (options) => {
|
|
1089
|
+
try {
|
|
1090
|
+
await discover({
|
|
1091
|
+
config: options.config,
|
|
1092
|
+
search: options.search,
|
|
1093
|
+
category: options.category,
|
|
1094
|
+
status: options.status,
|
|
1095
|
+
compact: options.compact,
|
|
1096
|
+
json: options.json,
|
|
1097
|
+
});
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
if (options.json) {
|
|
1100
|
+
console.log(JSON.stringify({ error: error instanceof Error ? error.message : 'Discover failed' }));
|
|
1101
|
+
} else {
|
|
1102
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1103
|
+
}
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
|
|
998
1108
|
// ============================================================================
|
|
999
1109
|
// PERF COMMAND
|
|
1000
1110
|
// ============================================================================
|
|
@@ -1134,11 +1244,18 @@ program
|
|
|
1134
1244
|
});
|
|
1135
1245
|
|
|
1136
1246
|
// ============================================================================
|
|
1137
|
-
// GOVERN COMMAND
|
|
1247
|
+
// GOVERN COMMAND (experimental — FRAGMENTS_EXPERIMENTAL=1)
|
|
1138
1248
|
// ============================================================================
|
|
1139
1249
|
const governCmd = program
|
|
1140
1250
|
.command('govern')
|
|
1141
|
-
.description('AI UI governance checks')
|
|
1251
|
+
.description(EXPERIMENTAL ? 'AI UI governance checks' : 'AI UI governance checks (preview — set FRAGMENTS_EXPERIMENTAL=1)')
|
|
1252
|
+
.hook('preAction', () => {
|
|
1253
|
+
if (!EXPERIMENTAL) {
|
|
1254
|
+
console.log(pc.yellow(`\n Fragments governance is not yet publicly available.`));
|
|
1255
|
+
console.log(pc.dim(` Set FRAGMENTS_EXPERIMENTAL=1 to enable preview features.\n`));
|
|
1256
|
+
process.exit(1);
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1142
1259
|
|
|
1143
1260
|
governCmd
|
|
1144
1261
|
.command('check')
|
|
@@ -1199,5 +1316,50 @@ governCmd
|
|
|
1199
1316
|
}
|
|
1200
1317
|
});
|
|
1201
1318
|
|
|
1319
|
+
governCmd
|
|
1320
|
+
.command('scan')
|
|
1321
|
+
.description('Scan JSX/TSX codebase for governance violations')
|
|
1322
|
+
.option('-d, --dir <path>', 'Root directory (default: auto-detect)')
|
|
1323
|
+
.option('-c, --config <path>', 'Path to govern.config.ts')
|
|
1324
|
+
.option('-f, --format <format>', 'Output format: summary, json, sarif', 'summary')
|
|
1325
|
+
.option('-q, --quiet', 'Suppress non-error output')
|
|
1326
|
+
.action(async (options) => {
|
|
1327
|
+
try {
|
|
1328
|
+
const { governScan } = await import('./commands/govern-scan.js');
|
|
1329
|
+
const { exitCode } = await governScan({
|
|
1330
|
+
dir: options.dir,
|
|
1331
|
+
config: options.config,
|
|
1332
|
+
format: options.format,
|
|
1333
|
+
quiet: options.quiet,
|
|
1334
|
+
});
|
|
1335
|
+
process.exit(exitCode);
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1338
|
+
process.exit(1);
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
governCmd
|
|
1343
|
+
.command('watch')
|
|
1344
|
+
.description('Watch JSX/TSX files and re-check on changes')
|
|
1345
|
+
.option('-d, --dir <path>', 'Root directory (default: auto-detect)')
|
|
1346
|
+
.option('-c, --config <path>', 'Path to govern.config.ts')
|
|
1347
|
+
.option('-q, --quiet', 'Suppress non-error output')
|
|
1348
|
+
.option('--debounce <ms>', 'Debounce interval in ms', '300')
|
|
1349
|
+
.action(async (options) => {
|
|
1350
|
+
try {
|
|
1351
|
+
const { governWatch } = await import('./commands/govern-scan.js');
|
|
1352
|
+
await governWatch({
|
|
1353
|
+
dir: options.dir,
|
|
1354
|
+
config: options.config,
|
|
1355
|
+
quiet: options.quiet,
|
|
1356
|
+
debounce: parseInt(options.debounce, 10),
|
|
1357
|
+
});
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
|
|
1360
|
+
process.exit(1);
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1202
1364
|
// Parse command line arguments
|
|
1203
1365
|
program.parse();
|
package/src/build.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
CompiledBlock,
|
|
9
9
|
CompiledTokenData,
|
|
10
10
|
} from "./core/index.js";
|
|
11
|
-
import { BRAND, compileBlock, parseTokenFile } from "./core/index.js";
|
|
11
|
+
import { BRAND, compileBlock, parseTokenFile, parseDTCGFile, isDTCGFile, isContractFile, parseComponentContract } from "./core/index.js";
|
|
12
12
|
import { resolveTokensWithSass } from "./core/token-resolver.js";
|
|
13
13
|
import type { BlockDefinition } from "./core/index.js";
|
|
14
14
|
import {
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
type PropMeta,
|
|
29
29
|
type ComponentMeta,
|
|
30
30
|
} from "./core/component-extractor.js";
|
|
31
|
+
import { createExtractorAdapter } from "./core/extractor-adapter.js";
|
|
32
|
+
import { verifyContractDrift, formatDriftReport } from "./core/drift-verifier.js";
|
|
31
33
|
import { buildComponentGraph } from "./core/graph-extractor.js";
|
|
32
34
|
import { serializeGraph } from "@fragments-sdk/context/graph";
|
|
33
35
|
import { resolvePerformanceConfig } from "./core/index.js";
|
|
@@ -128,6 +130,7 @@ export async function buildFragments(
|
|
|
128
130
|
const errors: Array<{ file: string; error: string }> = [];
|
|
129
131
|
const warnings: Array<{ file: string; warning: string }> = [];
|
|
130
132
|
const fragments: CompiledFragmentsFile["fragments"] = {};
|
|
133
|
+
const contractSourcedNames = new Set<string>();
|
|
131
134
|
|
|
132
135
|
// Create a persistent extractor — shared LanguageService across all fragments
|
|
133
136
|
// Try to find a tsconfig.json in the config directory
|
|
@@ -143,6 +146,105 @@ export async function buildFragments(
|
|
|
143
146
|
// Read file content as text
|
|
144
147
|
const content = await readFile(file.absolutePath, "utf-8");
|
|
145
148
|
|
|
149
|
+
// Handle .contract.json files (framework-agnostic component contracts)
|
|
150
|
+
if (isContractFile(file.absolutePath)) {
|
|
151
|
+
try {
|
|
152
|
+
const parsed = parseComponentContract(content, file.relativePath);
|
|
153
|
+
const framework = parsed.framework ?? config.framework ?? 'react';
|
|
154
|
+
const contractAdapter = createExtractorAdapter(framework, tsconfigPath);
|
|
155
|
+
|
|
156
|
+
// Extract props from source (if adapter supports it)
|
|
157
|
+
let extractedMeta: ComponentMeta | null = null;
|
|
158
|
+
if (parsed.sourcePath && parsed.exportName && contractAdapter.canHandle(framework)) {
|
|
159
|
+
try {
|
|
160
|
+
// sourcePath is relative to fragments.config.ts root
|
|
161
|
+
const absSourcePath = resolve(configDir, parsed.sourcePath);
|
|
162
|
+
extractedMeta = contractAdapter.extract(absSourcePath, parsed.exportName);
|
|
163
|
+
} catch {
|
|
164
|
+
// Extraction failure is non-fatal for contracts
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Merge authored + extracted props
|
|
169
|
+
let mergedProps = parsed.props as Record<string, CompiledProp>;
|
|
170
|
+
if (extractedMeta && Object.keys(extractedMeta.props).length > 0) {
|
|
171
|
+
mergedProps = mergeDocumentedAndAutoProps(mergedProps, extractedMeta.props);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Auto-compile propsSummary if not manually authored
|
|
175
|
+
let contractPropsSummary = parsed.propsSummary;
|
|
176
|
+
if ((!contractPropsSummary || contractPropsSummary.length === 0) && extractedMeta) {
|
|
177
|
+
contractPropsSummary = compilePropsSummary(extractedMeta.props);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Auto-enrich AI metadata from composition detection
|
|
181
|
+
let ai = parsed.ai;
|
|
182
|
+
if (extractedMeta?.composition) {
|
|
183
|
+
const comp = extractedMeta.composition;
|
|
184
|
+
ai = {
|
|
185
|
+
compositionPattern: comp.pattern,
|
|
186
|
+
subComponents: comp.parts.map((p) => p.name),
|
|
187
|
+
...ai,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Drift verification for verified contracts
|
|
192
|
+
if (parsed.provenance?.verified && extractedMeta) {
|
|
193
|
+
const drift = verifyContractDrift(parsed, extractedMeta);
|
|
194
|
+
if (!drift.isClean) {
|
|
195
|
+
errors.push({
|
|
196
|
+
file: file.relativePath,
|
|
197
|
+
error: formatDriftReport(drift),
|
|
198
|
+
});
|
|
199
|
+
contractAdapter.dispose();
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Build compiled fragment with all enrichments
|
|
205
|
+
const compiled: CompiledFragment = {
|
|
206
|
+
filePath: file.relativePath,
|
|
207
|
+
meta: {
|
|
208
|
+
name: parsed.meta.name,
|
|
209
|
+
description: parsed.meta.description,
|
|
210
|
+
category: parsed.meta.category,
|
|
211
|
+
tags: parsed.meta.tags,
|
|
212
|
+
status: parsed.meta.status,
|
|
213
|
+
figma: parsed.meta.figma,
|
|
214
|
+
},
|
|
215
|
+
usage: parsed.usage,
|
|
216
|
+
props: mergedProps,
|
|
217
|
+
relations: parsed.relations,
|
|
218
|
+
variants: parsed.variants,
|
|
219
|
+
...(parsed.contract && { contract: parsed.contract }),
|
|
220
|
+
...(ai && { ai }),
|
|
221
|
+
framework: parsed.framework,
|
|
222
|
+
propsSummary: contractPropsSummary,
|
|
223
|
+
sourcePath: parsed.sourcePath,
|
|
224
|
+
exportName: parsed.exportName,
|
|
225
|
+
provenance: parsed.provenance,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
fragments[compiled.meta.name] = compiled;
|
|
229
|
+
contractSourcedNames.add(compiled.meta.name);
|
|
230
|
+
contractAdapter.dispose();
|
|
231
|
+
} catch (error) {
|
|
232
|
+
errors.push({
|
|
233
|
+
file: file.relativePath,
|
|
234
|
+
error: `Contract parse error: ${error instanceof Error ? error.message : String(error)}`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Deprecation notice for .fragment.tsx files
|
|
241
|
+
if (file.absolutePath.endsWith('.fragment.tsx') || file.absolutePath.endsWith('.fragment.ts')) {
|
|
242
|
+
warnings.push({
|
|
243
|
+
file: file.relativePath,
|
|
244
|
+
warning: 'Deprecated format: .fragment.tsx — run `fragments migrate-contract` to generate .contract.json',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
146
248
|
// Skip files that don't contain defineFragment() — e.g., story files
|
|
147
249
|
// that were accidentally included in the config
|
|
148
250
|
if (!content.includes("defineFragment")) {
|
|
@@ -285,9 +387,22 @@ export async function buildFragments(
|
|
|
285
387
|
...(ai && { ai }),
|
|
286
388
|
// Include contract metadata (auto-compiled or manual)
|
|
287
389
|
...(contract && { contract }),
|
|
390
|
+
// Provenance from TSX path
|
|
391
|
+
provenance: {
|
|
392
|
+
source: extractedMeta ? 'extracted' : 'manual',
|
|
393
|
+
verified: !!extractedMeta,
|
|
394
|
+
},
|
|
288
395
|
};
|
|
289
396
|
|
|
290
|
-
|
|
397
|
+
// When both .contract.json and .fragment.tsx exist, prefer contract
|
|
398
|
+
if (contractSourcedNames.has(parsed.meta.name)) {
|
|
399
|
+
warnings.push({
|
|
400
|
+
file: file.relativePath,
|
|
401
|
+
warning: `Duplicate: "${parsed.meta.name}" already loaded from .contract.json — skipping .fragment.tsx`,
|
|
402
|
+
});
|
|
403
|
+
} else {
|
|
404
|
+
fragments[parsed.meta.name] = compiled;
|
|
405
|
+
}
|
|
291
406
|
} catch (error) {
|
|
292
407
|
errors.push({
|
|
293
408
|
file: file.relativePath,
|
|
@@ -349,10 +464,19 @@ export async function buildFragments(
|
|
|
349
464
|
const allContent = fileContents.map((f) => f.content).join('\n');
|
|
350
465
|
|
|
351
466
|
for (const { content, path } of fileContents) {
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
467
|
+
// Route to the correct parser based on file extension
|
|
468
|
+
let fileParsed;
|
|
469
|
+
let parsed;
|
|
470
|
+
if (isDTCGFile(path)) {
|
|
471
|
+
// DTCG files are self-contained — no cross-file resolution needed
|
|
472
|
+
fileParsed = parseDTCGFile(content, path);
|
|
473
|
+
parsed = fileParsed;
|
|
474
|
+
} else {
|
|
475
|
+
// Parse SCSS/CSS with the combined content to enable cross-file var resolution
|
|
476
|
+
parsed = parseTokenFile(allContent, path);
|
|
477
|
+
// But only use tokens from THIS file's content to avoid duplicates
|
|
478
|
+
fileParsed = parseTokenFile(content, path);
|
|
479
|
+
}
|
|
356
480
|
prefix = fileParsed.prefix;
|
|
357
481
|
total += fileParsed.total;
|
|
358
482
|
for (const [cat, catTokens] of Object.entries(fileParsed.categories)) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://usefragments.com/schemas/contract.v1.json",
|
|
3
|
+
"name": "Label",
|
|
4
|
+
"description": "Label component",
|
|
5
|
+
"category": "Typography",
|
|
6
|
+
"sourcePath": "label.tsx",
|
|
7
|
+
"exportName": "Label",
|
|
8
|
+
"propsSummary": [
|
|
9
|
+
"className: string",
|
|
10
|
+
"htmlFor: string",
|
|
11
|
+
"form: string",
|
|
12
|
+
"children: node"
|
|
13
|
+
],
|
|
14
|
+
"props": {
|
|
15
|
+
"className": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": ""
|
|
18
|
+
},
|
|
19
|
+
"htmlFor": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": ""
|
|
22
|
+
},
|
|
23
|
+
"form": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": ""
|
|
26
|
+
},
|
|
27
|
+
"children": {
|
|
28
|
+
"type": "node",
|
|
29
|
+
"description": ""
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"usage": {
|
|
33
|
+
"when": [],
|
|
34
|
+
"whenNot": []
|
|
35
|
+
},
|
|
36
|
+
"provenance": {
|
|
37
|
+
"source": "extracted",
|
|
38
|
+
"verified": false,
|
|
39
|
+
"frameworkSupport": "native",
|
|
40
|
+
"extractedAt": "2026-03-13T23:33:02.488Z"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -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 };
|
package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://usefragments.com/schemas/contract.v1.json",
|
|
3
|
+
"name": "Primitive",
|
|
4
|
+
"description": "Primitive component",
|
|
5
|
+
"category": "Components",
|
|
6
|
+
"sourcePath": "primitive.tsx",
|
|
7
|
+
"exportName": "Primitive",
|
|
8
|
+
"propsSummary": [],
|
|
9
|
+
"props": {},
|
|
10
|
+
"usage": {
|
|
11
|
+
"when": [],
|
|
12
|
+
"whenNot": []
|
|
13
|
+
},
|
|
14
|
+
"provenance": {
|
|
15
|
+
"source": "extracted",
|
|
16
|
+
"verified": false,
|
|
17
|
+
"frameworkSupport": "native",
|
|
18
|
+
"extractedAt": "2026-03-13T23:33:02.489Z"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -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,23 @@
|
|
|
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": [
|
|
12
|
+
"react",
|
|
13
|
+
"react-dom"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"include": [
|
|
17
|
+
"src/**/*.tsx"
|
|
18
|
+
],
|
|
19
|
+
"exclude": [
|
|
20
|
+
"**/*.fragment.tsx",
|
|
21
|
+
"**/*.contract.json"
|
|
22
|
+
]
|
|
23
|
+
}
|