@fragments-sdk/cli 0.15.0 → 0.15.2

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 (120) hide show
  1. package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
  2. package/dist/bin.js +565 -548
  3. package/dist/bin.js.map +1 -1
  4. package/dist/chunk-5JF26E55.js +1255 -0
  5. package/dist/chunk-5JF26E55.js.map +1 -0
  6. package/dist/{chunk-XJQ5BIWI.js → chunk-6SQPP47U.js} +30 -314
  7. package/dist/chunk-6SQPP47U.js.map +1 -0
  8. package/dist/{chunk-65WSVDV5.js → chunk-HQ6A6DTV.js} +1386 -1097
  9. package/dist/chunk-HQ6A6DTV.js.map +1 -0
  10. package/dist/chunk-MHIBEEW4.js +511 -0
  11. package/dist/chunk-MHIBEEW4.js.map +1 -0
  12. package/dist/{chunk-CZD3AD4Q.js → chunk-ONUP6Z4W.js} +17 -6
  13. package/dist/chunk-ONUP6Z4W.js.map +1 -0
  14. package/dist/{codebase-scanner-VOTPXRYW.js → codebase-scanner-MQHUZC2G.js} +1 -2
  15. package/dist/{converter-JLINP7CJ.js → converter-7XM3Y6NJ.js} +1 -2
  16. package/dist/{converter-JLINP7CJ.js.map → converter-7XM3Y6NJ.js.map} +1 -1
  17. package/dist/core/index.js +0 -1
  18. package/dist/create-JVAU3YKN.js +852 -0
  19. package/dist/create-JVAU3YKN.js.map +1 -0
  20. package/dist/doctor-BDPMYYE6.js +385 -0
  21. package/dist/doctor-BDPMYYE6.js.map +1 -0
  22. package/dist/{generate-A4FP5426.js → generate-PVOLUAAC.js} +3 -4
  23. package/dist/{generate-A4FP5426.js.map → generate-PVOLUAAC.js.map} +1 -1
  24. package/dist/{govern-scan-UCBZR6D6.js → govern-scan-OYFZYOQW.js} +142 -9
  25. package/dist/govern-scan-OYFZYOQW.js.map +1 -0
  26. package/dist/index.d.ts +2 -22
  27. package/dist/index.js +8 -7
  28. package/dist/index.js.map +1 -1
  29. package/dist/{init-HGSM35XA.js → init-SSGUSP7Z.js} +3 -4
  30. package/dist/{init-HGSM35XA.js.map → init-SSGUSP7Z.js.map} +1 -1
  31. package/dist/{init-cloud-MQ6GRJAZ.js → init-cloud-3DNKPWFB.js} +29 -4
  32. package/dist/{init-cloud-MQ6GRJAZ.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
  33. package/dist/mcp-bin.js +1 -2
  34. package/dist/mcp-bin.js.map +1 -1
  35. package/dist/node-37AUE74M.js +65 -0
  36. package/dist/push-contracts-WY32TFP6.js +84 -0
  37. package/dist/push-contracts-WY32TFP6.js.map +1 -0
  38. package/dist/{scan-VNNKACG2.js → scan-PKSYSTRR.js} +5 -5
  39. package/dist/{scan-generate-TWRHNU5M.js → scan-generate-VY27PIOX.js} +8 -9
  40. package/dist/scan-generate-VY27PIOX.js.map +1 -0
  41. package/dist/{scanner-7LAZYPWZ.js → scanner-4KZNOXAK.js} +1 -2
  42. package/dist/{service-FHQU7YS7.js → service-QJGWUIVL.js} +16 -9
  43. package/dist/{snapshot-KQEQ6XHL.js → snapshot-WIJMEIFT.js} +1 -2
  44. package/dist/{snapshot-KQEQ6XHL.js.map → snapshot-WIJMEIFT.js.map} +1 -1
  45. package/dist/{static-viewer-63PG6FWY.js → static-viewer-7QIBQZRC.js} +1 -2
  46. package/dist/{test-UQYUCZIS.js → test-64Z5BKBA.js} +2 -3
  47. package/dist/{test-UQYUCZIS.js.map → test-64Z5BKBA.js.map} +1 -1
  48. package/dist/token-normalizer-TEPOVBPV.js +312 -0
  49. package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
  50. package/dist/token-parser-32KOIOFN.js +22 -0
  51. package/dist/token-parser-32KOIOFN.js.map +1 -0
  52. package/dist/{tokens-6GYKDV6U.js → tokens-NZWFQIAB.js} +7 -7
  53. package/dist/{tokens-generate-VTZV5EEW.js → tokens-generate-5JQSJ27E.js} +1 -2
  54. package/dist/{tokens-generate-VTZV5EEW.js.map → tokens-generate-5JQSJ27E.js.map} +1 -1
  55. package/dist/tokens-push-HY3KO36V.js +148 -0
  56. package/dist/tokens-push-HY3KO36V.js.map +1 -0
  57. package/package.json +18 -16
  58. package/src/bin.ts +94 -1
  59. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
  60. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
  61. package/src/commands/__tests__/build-freshness.test.ts +231 -0
  62. package/src/commands/__tests__/create.test.ts +71 -0
  63. package/src/commands/__tests__/drift-sync.test.ts +1 -1
  64. package/src/commands/__tests__/govern.test.ts +258 -0
  65. package/src/commands/__tests__/init.test.ts +9 -1
  66. package/src/commands/__tests__/scan-generate.test.ts +1 -1
  67. package/src/commands/build.ts +54 -1
  68. package/src/commands/context.ts +1 -1
  69. package/src/commands/create.ts +590 -0
  70. package/src/commands/doctor.ts +3 -2
  71. package/src/commands/govern-scan.ts +187 -8
  72. package/src/commands/govern.ts +65 -2
  73. package/src/commands/init-cloud.ts +32 -4
  74. package/src/commands/push-contracts.ts +112 -0
  75. package/src/commands/scan-generate.ts +1 -1
  76. package/src/commands/scan.ts +13 -0
  77. package/src/commands/sync.ts +2 -2
  78. package/src/commands/tokens-push.ts +199 -0
  79. package/src/core/__tests__/token-resolver.test.ts +1 -1
  80. package/src/core/component-extractor.test.ts +1 -1
  81. package/src/core/drift-verifier.ts +1 -1
  82. package/src/core/extractor-adapter.ts +1 -1
  83. package/src/index.ts +3 -3
  84. package/src/migrate/fragment-to-contract.ts +2 -2
  85. package/src/service/index.ts +8 -0
  86. package/src/service/tailwind-v4-parser.ts +314 -0
  87. package/src/service/token-parser.ts +56 -0
  88. package/src/setup.ts +10 -39
  89. package/src/theme/__tests__/component-contrast.test.ts +2 -2
  90. package/src/theme/__tests__/serializer.test.ts +1 -1
  91. package/src/theme/generator.ts +30 -1
  92. package/src/theme/schema.ts +8 -0
  93. package/src/theme/serializer.ts +13 -9
  94. package/src/theme/types.ts +8 -0
  95. package/src/validators.ts +1 -2
  96. package/dist/chunk-65WSVDV5.js.map +0 -1
  97. package/dist/chunk-7WHVW72L.js +0 -2664
  98. package/dist/chunk-7WHVW72L.js.map +0 -1
  99. package/dist/chunk-CZD3AD4Q.js.map +0 -1
  100. package/dist/chunk-MN3TJ3D5.js +0 -695
  101. package/dist/chunk-MN3TJ3D5.js.map +0 -1
  102. package/dist/chunk-XJQ5BIWI.js.map +0 -1
  103. package/dist/chunk-Z7EY4VHE.js +0 -50
  104. package/dist/govern-scan-UCBZR6D6.js.map +0 -1
  105. package/dist/sass.node-4XJK6YBF.js +0 -130708
  106. package/dist/sass.node-4XJK6YBF.js.map +0 -1
  107. package/dist/scan-generate-TWRHNU5M.js.map +0 -1
  108. package/src/build.ts +0 -736
  109. package/src/core/auto-props.ts +0 -464
  110. package/src/core/component-extractor.ts +0 -1121
  111. package/src/core/token-resolver.ts +0 -155
  112. package/src/viewer/preview-adapter.ts +0 -116
  113. /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
  114. /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
  115. /package/dist/{codebase-scanner-VOTPXRYW.js.map → node-37AUE74M.js.map} +0 -0
  116. /package/dist/{scan-VNNKACG2.js.map → scan-PKSYSTRR.js.map} +0 -0
  117. /package/dist/{scanner-7LAZYPWZ.js.map → scanner-4KZNOXAK.js.map} +0 -0
  118. /package/dist/{service-FHQU7YS7.js.map → service-QJGWUIVL.js.map} +0 -0
  119. /package/dist/{static-viewer-63PG6FWY.js.map → static-viewer-7QIBQZRC.js.map} +0 -0
  120. /package/dist/{tokens-6GYKDV6U.js.map → tokens-NZWFQIAB.js.map} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragments-sdk/cli",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "license": "FSL-1.1-MIT",
5
5
  "description": "CLI, MCP server, and dev tools for Fragments design system",
6
6
  "author": "Conan McNicholl",
@@ -50,16 +50,31 @@
50
50
  "dist",
51
51
  "src"
52
52
  ],
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "dev": "tsup --watch",
56
+ "lint": "eslint src",
57
+ "test": "vitest run",
58
+ "typecheck": "tsc --noEmit",
59
+ "clean": "rm -rf dist"
60
+ },
53
61
  "scarfSettings": {
54
62
  "defaultOptIn": true
55
63
  },
56
64
  "dependencies": {
57
65
  "@anthropic-ai/sdk": "^0.71.2",
66
+ "@fragments-sdk/compiler": "workspace:*",
67
+ "@fragments-sdk/extract": "workspace:*",
58
68
  "@babel/generator": "^7.23.6",
59
69
  "@babel/parser": "^7.23.6",
60
70
  "@babel/traverse": "^7.23.6",
61
71
  "@babel/types": "^7.23.6",
62
72
  "@figma/rest-api-spec": "^0.35.0",
73
+ "@fragments-sdk/context": "workspace:*",
74
+ "@fragments-sdk/core": "workspace:*",
75
+ "@fragments-sdk/govern": "workspace:^",
76
+ "@fragments-sdk/viewer": "workspace:*",
77
+ "@fragments-sdk/webmcp": "workspace:*",
63
78
  "@inquirer/prompts": "^7.2.1",
64
79
  "@modelcontextprotocol/sdk": "^1.0.0",
65
80
  "@phosphor-icons/react": "^2.1.10",
@@ -80,12 +95,7 @@
80
95
  "shiki": "^3.21.0",
81
96
  "vite": "^6.0.0",
82
97
  "vite-plugin-svgr": "^4.5.0",
83
- "zod": "^3.24.1",
84
- "@fragments-sdk/core": "2.0.0",
85
- "@fragments-sdk/webmcp": "3.0.0",
86
- "@fragments-sdk/viewer": "0.2.7",
87
- "@fragments-sdk/context": "0.6.0",
88
- "@fragments-sdk/govern": "^0.3.0"
98
+ "zod": "^3.24.1"
89
99
  },
90
100
  "devDependencies": {
91
101
  "@types/babel__generator": "^7.6.8",
@@ -118,13 +128,5 @@
118
128
  "react-dom": {
119
129
  "optional": true
120
130
  }
121
- },
122
- "scripts": {
123
- "build": "tsup",
124
- "dev": "tsup --watch",
125
- "lint": "eslint src",
126
- "test": "vitest run",
127
- "typecheck": "tsc --noEmit",
128
- "clean": "rm -rf dist"
129
131
  }
130
- }
132
+ }
package/src/bin.ts CHANGED
@@ -40,7 +40,9 @@ import { graph } from './commands/graph.js';
40
40
  import { inspect } from './commands/inspect.js';
41
41
  import { discover } from './commands/discover.js';
42
42
  import { perf } from './commands/perf.js';
43
- import { doctor } from './commands/doctor.js';
43
+ // doctor is imported lazily — it pulls @fragments-sdk/viewer/docs-data which
44
+ // ships as raw TS source and cannot be resolved by Node at startup.
45
+ type DoctorFn = typeof import('./commands/doctor.js')['doctor'];
44
46
  import { setup } from './commands/setup.js';
45
47
  import { sync } from './commands/sync.js';
46
48
  import { governCheck, governInit, governReport, governConnect } from './commands/govern.js';
@@ -123,6 +125,8 @@ program
123
125
  .option('--registry', `Also generate ${BRAND.dataDir}/${BRAND.registryFile} and ${BRAND.contextFile}`)
124
126
  .option('--registry-only', `Only generate ${BRAND.dataDir}/ directory (skip ${BRAND.outFile})`)
125
127
  .option('--from-source', 'Build from source code (zero-config, no fragment files needed)')
128
+ .option('--if-needed', `Skip rebuilding when ${BRAND.outFile} is already fresh`)
129
+ .option('--check', `Check whether ${BRAND.outFile} is fresh and exit non-zero if it is stale`)
126
130
  .option('--skip-usage', 'Skip usage analysis when building from source')
127
131
  .option('--skip-storybook', 'Skip Storybook parsing when building from source')
128
132
  .option('-v, --verbose', 'Verbose output')
@@ -134,6 +138,8 @@ program
134
138
  registry: options.registry,
135
139
  registryOnly: options.registryOnly,
136
140
  fromSource: options.fromSource,
141
+ ifNeeded: options.ifNeeded,
142
+ check: options.check,
137
143
  skipUsage: options.skipUsage,
138
144
  skipStorybook: options.skipStorybook,
139
145
  verbose: options.verbose,
@@ -791,6 +797,47 @@ program
791
797
  }
792
798
  });
793
799
 
800
+ // ============================================================================
801
+ // CREATE COMMAND
802
+ // ============================================================================
803
+ program
804
+ .command('create')
805
+ .argument('[name]', 'Project name')
806
+ .description('Create a new project with Fragments UI and your custom theme')
807
+ .option('-t, --template <template>', 'Framework template (nextjs, vite)', 'nextjs')
808
+ .option('--pm <manager>', 'Package manager (npm, pnpm, yarn, bun)')
809
+ .option('--theme <encoded>', 'Encoded theme string')
810
+ .option('--preset <id>', 'Theme preset ID from usefragments.com/create')
811
+ .option('--brand <color>', 'Brand color hex (e.g., #6366f1)')
812
+ .option('--scss', 'Use SCSS output (installs sass)')
813
+ .option('--mcp', 'Configure MCP server for AI tooling')
814
+ .option('-y, --yes', 'Skip interactive prompts')
815
+ .option('--no-git', 'Skip git initialization')
816
+ .action(async (name, options) => {
817
+ try {
818
+ const { create } = await import('./commands/create.js');
819
+ const result = await create({
820
+ name,
821
+ template: options.template,
822
+ packageManager: options.pm,
823
+ theme: options.theme,
824
+ preset: options.preset,
825
+ brand: options.brand,
826
+ scss: options.scss,
827
+ mcp: options.mcp,
828
+ yes: options.yes,
829
+ noGit: !options.git,
830
+ });
831
+ if (!result.success) {
832
+ if (result.error) console.error(pc.red(`Error: ${result.error}`));
833
+ process.exit(1);
834
+ }
835
+ } catch (error) {
836
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
837
+ process.exit(1);
838
+ }
839
+ });
840
+
794
841
  // ============================================================================
795
842
  // SETUP COMMAND
796
843
  // ============================================================================
@@ -989,6 +1036,28 @@ tokensCmd
989
1036
  }
990
1037
  });
991
1038
 
1039
+ tokensCmd
1040
+ .command('push')
1041
+ .description('Push code tokens to Fragments Cloud for drift comparison')
1042
+ .option('-c, --config <path>', 'Path to fragments config file')
1043
+ .option('--tailwind-v4 <path>', 'Path to Tailwind v4 CSS file with @theme block')
1044
+ .option('--dry-run', 'Parse and display tokens without pushing')
1045
+ .option('--verbose', 'Show detailed output')
1046
+ .action(async (options) => {
1047
+ try {
1048
+ const { tokensPush } = await import('./commands/tokens-push.js');
1049
+ await tokensPush({
1050
+ config: options.config,
1051
+ tailwindV4: options.tailwindV4,
1052
+ dryRun: options.dryRun,
1053
+ verbose: options.verbose,
1054
+ });
1055
+ } catch (error) {
1056
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
1057
+ process.exit(1);
1058
+ }
1059
+ });
1060
+
992
1061
  // ============================================================================
993
1062
  // GENERATE COMMAND
994
1063
  // ============================================================================
@@ -1229,6 +1298,7 @@ program
1229
1298
  .option('--fix', 'Auto-fix issues where possible')
1230
1299
  .action(async (options) => {
1231
1300
  try {
1301
+ const { doctor } = await import('./commands/doctor.js');
1232
1302
  const result = await doctor({
1233
1303
  root: options.root,
1234
1304
  json: options.json,
@@ -1361,5 +1431,28 @@ governCmd
1361
1431
  }
1362
1432
  });
1363
1433
 
1434
+ governCmd
1435
+ .command('push-contracts')
1436
+ .description('Push component contracts to Fragments Cloud')
1437
+ .option('-i, --input <path>', 'Path to fragments.json (default: ./fragments.json)')
1438
+ .option('--url <url>', 'Fragments Cloud URL')
1439
+ .option('--api-key <key>', 'API key (default: FRAGMENTS_API_KEY env var)')
1440
+ .option('-q, --quiet', 'Suppress non-error output')
1441
+ .action(async (options) => {
1442
+ try {
1443
+ const { pushContracts } = await import('./commands/push-contracts.js');
1444
+ const { exitCode } = await pushContracts({
1445
+ input: options.input,
1446
+ url: options.url,
1447
+ apiKey: options.apiKey,
1448
+ quiet: options.quiet,
1449
+ });
1450
+ process.exit(exitCode);
1451
+ } catch (error) {
1452
+ console.error(pc.red('Error:'), error instanceof Error ? error.message : error);
1453
+ process.exit(1);
1454
+ }
1455
+ });
1456
+
1364
1457
  // Parse command line arguments
1365
1458
  program.parse();
@@ -37,6 +37,6 @@
37
37
  "source": "extracted",
38
38
  "verified": false,
39
39
  "frameworkSupport": "native",
40
- "extractedAt": "2026-03-13T23:33:02.488Z"
40
+ "extractedAt": "2026-03-25T18:56:02.138Z"
41
41
  }
42
42
  }
@@ -15,6 +15,6 @@
15
15
  "source": "extracted",
16
16
  "verified": false,
17
17
  "frameworkSupport": "native",
18
- "extractedAt": "2026-03-13T23:33:02.489Z"
18
+ "extractedAt": "2026-03-25T18:56:02.138Z"
19
19
  }
20
20
  }
@@ -0,0 +1,231 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
2
+ import { mkdtemp, mkdir, rm, utimes, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+
6
+ import { getFragmentsBuildInputs, getFragmentsJsonStatus, getGeneratorVersion } from '@fragments-sdk/compiler';
7
+ import type { FragmentsConfig } from '@fragments-sdk/core';
8
+
9
+ describe('getFragmentsJsonStatus', () => {
10
+ let tmpDir: string;
11
+
12
+ beforeAll(async () => {
13
+ tmpDir = await mkdtemp(join(tmpdir(), 'build-freshness-test-'));
14
+ });
15
+
16
+ afterAll(async () => {
17
+ await rm(tmpDir, { recursive: true, force: true });
18
+ });
19
+
20
+ async function createProject(projectName: string) {
21
+ const projectDir = join(tmpDir, projectName);
22
+ const srcDir = join(projectDir, 'src', 'components');
23
+ await mkdir(srcDir, { recursive: true });
24
+
25
+ const configPath = join(projectDir, 'fragments.config.ts');
26
+ const fragmentPath = join(srcDir, 'Button.contract.json');
27
+ const componentPath = join(srcDir, 'Button.tsx');
28
+ const outputPath = join(projectDir, 'fragments.json');
29
+
30
+ await writeFile(
31
+ configPath,
32
+ `export default {
33
+ include: ['src/**/*.contract.json'],
34
+ exclude: [],
35
+ components: ['src/**/*.tsx'],
36
+ framework: 'react',
37
+ outFile: 'fragments.json',
38
+ };
39
+ `
40
+ );
41
+
42
+ await writeFile(
43
+ fragmentPath,
44
+ JSON.stringify(
45
+ {
46
+ name: 'Button',
47
+ description: 'Button component',
48
+ category: 'actions',
49
+ variants: [
50
+ {
51
+ name: 'Default',
52
+ code: '<Button>Press</Button>',
53
+ },
54
+ ],
55
+ sourcePath: 'src/components/Button.tsx',
56
+ exportName: 'Button',
57
+ },
58
+ null,
59
+ 2
60
+ )
61
+ );
62
+
63
+ await writeFile(
64
+ componentPath,
65
+ `export function Button() {
66
+ return null;
67
+ }
68
+ `
69
+ );
70
+
71
+ const config: FragmentsConfig = {
72
+ include: ['src/**/*.contract.json'],
73
+ exclude: [],
74
+ components: ['src/**/*.tsx'],
75
+ framework: 'react',
76
+ outFile: 'fragments.json',
77
+ };
78
+
79
+ return {
80
+ config,
81
+ projectDir,
82
+ configPath,
83
+ fragmentPath,
84
+ componentPath,
85
+ outputPath,
86
+ };
87
+ }
88
+
89
+ it('reports missing output files', async () => {
90
+ const project = await createProject('missing-output');
91
+
92
+ const status = await getFragmentsJsonStatus(project.config, project.projectDir, {
93
+ configPath: project.configPath,
94
+ });
95
+
96
+ expect(status.missing).toBe(true);
97
+ expect(status.stale).toBe(false);
98
+ expect(status.reason).toBe('missing output file');
99
+ });
100
+
101
+ it('accepts a fresh fragments.json with matching inputs and generator version', async () => {
102
+ const project = await createProject('fresh-output');
103
+ const generatorVersion = await getGeneratorVersion();
104
+ const buildInputs = await getFragmentsBuildInputs(project.config, project.projectDir, {
105
+ configPath: project.configPath,
106
+ });
107
+
108
+ await writeFile(
109
+ project.outputPath,
110
+ JSON.stringify(
111
+ {
112
+ version: '1.0.0',
113
+ generatedAt: new Date().toISOString(),
114
+ generatorVersion,
115
+ buildInputs: buildInputs.relativePaths,
116
+ fragments: {},
117
+ },
118
+ null,
119
+ 2
120
+ )
121
+ );
122
+
123
+ const now = new Date();
124
+ const earlier = new Date(now.getTime() - 10_000);
125
+ await utimes(project.configPath, earlier, earlier);
126
+ await utimes(project.fragmentPath, earlier, earlier);
127
+ await utimes(project.componentPath, earlier, earlier);
128
+ await utimes(project.outputPath, now, now);
129
+
130
+ const status = await getFragmentsJsonStatus(project.config, project.projectDir, {
131
+ configPath: project.configPath,
132
+ });
133
+
134
+ expect(status.missing).toBe(false);
135
+ expect(status.stale).toBe(false);
136
+ expect(status.reason).toBeNull();
137
+ });
138
+
139
+ it('marks fragments.json stale when a new input file is added', async () => {
140
+ const project = await createProject('added-input');
141
+ const generatorVersion = await getGeneratorVersion();
142
+ const buildInputs = await getFragmentsBuildInputs(project.config, project.projectDir, {
143
+ configPath: project.configPath,
144
+ });
145
+
146
+ // Write fragments.json with current inputs
147
+ await writeFile(
148
+ project.outputPath,
149
+ JSON.stringify(
150
+ {
151
+ version: '1.0.0',
152
+ generatedAt: new Date().toISOString(),
153
+ generatorVersion,
154
+ buildInputs: buildInputs.relativePaths,
155
+ fragments: {},
156
+ },
157
+ null,
158
+ 2
159
+ )
160
+ );
161
+
162
+ // Add a new component file — buildInputs list will differ
163
+ const newComponentPath = join(project.projectDir, 'src', 'components', 'Input.tsx');
164
+ await writeFile(newComponentPath, 'export function Input() { return null; }\n');
165
+
166
+ const status = await getFragmentsJsonStatus(project.config, project.projectDir, {
167
+ configPath: project.configPath,
168
+ });
169
+
170
+ expect(status.stale).toBe(true);
171
+ expect(status.reason).toContain('build inputs changed');
172
+ });
173
+
174
+ it('marks legacy fragments.json without build metadata as stale', async () => {
175
+ const project = await createProject('legacy-output');
176
+ const generatorVersion = await getGeneratorVersion();
177
+
178
+ await writeFile(
179
+ project.outputPath,
180
+ JSON.stringify(
181
+ {
182
+ version: '1.0.0',
183
+ generatedAt: new Date().toISOString(),
184
+ generatorVersion,
185
+ fragments: {},
186
+ },
187
+ null,
188
+ 2
189
+ )
190
+ );
191
+
192
+ const status = await getFragmentsJsonStatus(project.config, project.projectDir, {
193
+ configPath: project.configPath,
194
+ });
195
+
196
+ expect(status.stale).toBe(true);
197
+ expect(status.reason).toBe('missing build input metadata');
198
+ });
199
+
200
+ it('marks fragments.json stale when an input file disappears', async () => {
201
+ const project = await createProject('deleted-input');
202
+ const generatorVersion = await getGeneratorVersion();
203
+ const buildInputs = await getFragmentsBuildInputs(project.config, project.projectDir, {
204
+ configPath: project.configPath,
205
+ });
206
+
207
+ await writeFile(
208
+ project.outputPath,
209
+ JSON.stringify(
210
+ {
211
+ version: '1.0.0',
212
+ generatedAt: new Date().toISOString(),
213
+ generatorVersion,
214
+ buildInputs: buildInputs.relativePaths,
215
+ fragments: {},
216
+ },
217
+ null,
218
+ 2
219
+ )
220
+ );
221
+
222
+ await rm(project.componentPath);
223
+
224
+ const status = await getFragmentsJsonStatus(project.config, project.projectDir, {
225
+ configPath: project.configPath,
226
+ });
227
+
228
+ expect(status.stale).toBe(true);
229
+ expect(status.reason).toContain('build inputs changed');
230
+ });
231
+ });
@@ -0,0 +1,71 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
2
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+
6
+ import {
7
+ addNextTranspilePackages,
8
+ generateNextjsLayout,
9
+ generateNextjsPage,
10
+ generateNextjsProviders,
11
+ } from "../create.js";
12
+
13
+ describe("create command templates", () => {
14
+ let tmpDir: string;
15
+
16
+ beforeAll(async () => {
17
+ tmpDir = await mkdtemp(join(tmpdir(), "fragments-create-test-"));
18
+ });
19
+
20
+ afterAll(async () => {
21
+ await rm(tmpDir, { recursive: true, force: true });
22
+ });
23
+
24
+ it("generates a Next.js layout that routes providers through a client wrapper", () => {
25
+ const layout = generateNextjsLayout("../styles/theme.css");
26
+
27
+ expect(layout).toContain("import { Providers } from './providers';");
28
+ expect(layout).toContain("<Providers>{children}</Providers>");
29
+ expect(layout).not.toContain("ThemeProvider");
30
+ });
31
+
32
+ it("generates a client providers module for Next.js", () => {
33
+ const providers = generateNextjsProviders();
34
+
35
+ expect(providers).toContain("'use client';");
36
+ expect(providers).toContain("ThemeProvider");
37
+ expect(providers).toContain("ToastProvider");
38
+ });
39
+
40
+ it("marks the sample Next.js page as a client component", () => {
41
+ const page = generateNextjsPage();
42
+
43
+ expect(page).toContain("'use client';");
44
+ expect(page).toContain("import { Button, Card, Stack, Text, Input } from '@fragments-sdk/ui';");
45
+ });
46
+
47
+ it("adds transpilePackages for @fragments-sdk/ui to next.config.ts", async () => {
48
+ const projectDir = join(tmpDir, "next-app");
49
+ await mkdir(projectDir, { recursive: true });
50
+ const configPath = join(projectDir, "next.config.ts");
51
+
52
+ await writeFile(
53
+ configPath,
54
+ [
55
+ 'import type { NextConfig } from "next";',
56
+ "",
57
+ "const nextConfig: NextConfig = {",
58
+ " /* config options here */",
59
+ "};",
60
+ "",
61
+ "export default nextConfig;",
62
+ "",
63
+ ].join("\n"),
64
+ );
65
+
66
+ addNextTranspilePackages(projectDir);
67
+
68
+ const updated = await readFile(configPath, "utf-8");
69
+ expect(updated).toContain("transpilePackages: ['@fragments-sdk/ui']");
70
+ });
71
+ });
@@ -3,7 +3,7 @@ import { mkdtemp, writeFile, mkdir, rm, readFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { diffProps, validateDrift, type DriftItem } from '../../validators.js';
6
- import type { PropMeta } from '../../core/component-extractor.js';
6
+ import type { PropMeta } from '@fragments-sdk/extract';
7
7
  import type { FragmentsConfig } from '@fragments-sdk/core';
8
8
 
9
9
  // ---------------------------------------------------------------------------