@fragments-sdk/cli 0.14.3 → 0.15.1

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 (181) hide show
  1. package/README.md +0 -3
  2. package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
  3. package/dist/bin.js +4745 -3817
  4. package/dist/bin.js.map +1 -1
  5. package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
  6. package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
  7. package/dist/chunk-32LIWN2P.js.map +1 -0
  8. package/dist/chunk-5JF26E55.js +1255 -0
  9. package/dist/chunk-5JF26E55.js.map +1 -0
  10. package/dist/{chunk-APTQIBS5.js → chunk-6SQPP47U.js} +153 -1342
  11. package/dist/chunk-6SQPP47U.js.map +1 -0
  12. package/dist/chunk-7DZC4YEV.js +294 -0
  13. package/dist/chunk-7DZC4YEV.js.map +1 -0
  14. package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
  15. package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
  16. package/dist/{chunk-55KERLWL.js → chunk-HQ6A6DTV.js} +1587 -1073
  17. package/dist/chunk-HQ6A6DTV.js.map +1 -0
  18. package/dist/chunk-MHIBEEW4.js +511 -0
  19. package/dist/chunk-MHIBEEW4.js.map +1 -0
  20. package/dist/{chunk-5A6X2Y73.js → chunk-ONUP6Z4W.js} +25 -13
  21. package/dist/chunk-ONUP6Z4W.js.map +1 -0
  22. package/dist/chunk-QCN35LJU.js +630 -0
  23. package/dist/chunk-QCN35LJU.js.map +1 -0
  24. package/dist/chunk-T47OLCSF.js +36 -0
  25. package/dist/chunk-T47OLCSF.js.map +1 -0
  26. package/dist/codebase-scanner-MQHUZC2G.js +21 -0
  27. package/dist/converter-7XM3Y6NJ.js +33 -0
  28. package/dist/converter-7XM3Y6NJ.js.map +1 -0
  29. package/dist/core/index.js +43 -2
  30. package/dist/create-IH4R45GE.js +806 -0
  31. package/dist/create-IH4R45GE.js.map +1 -0
  32. package/dist/{generate-RYWIPDN2.js → generate-PVOLUAAC.js} +4 -6
  33. package/dist/{generate-RYWIPDN2.js.map → generate-PVOLUAAC.js.map} +1 -1
  34. package/dist/govern-scan-OYFZYOQW.js +413 -0
  35. package/dist/govern-scan-OYFZYOQW.js.map +1 -0
  36. package/dist/index.d.ts +4 -23
  37. package/dist/index.js +15 -14
  38. package/dist/index.js.map +1 -1
  39. package/dist/{init-WRUSW7R5.js → init-SSGUSP7Z.js} +131 -129
  40. package/dist/init-SSGUSP7Z.js.map +1 -0
  41. package/dist/{init-cloud-REQ3XLHO.js → init-cloud-3DNKPWFB.js} +30 -5
  42. package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
  43. package/dist/mcp-bin.js +5 -37
  44. package/dist/mcp-bin.js.map +1 -1
  45. package/dist/node-37AUE74M.js +65 -0
  46. package/dist/push-contracts-WY32TFP6.js +84 -0
  47. package/dist/push-contracts-WY32TFP6.js.map +1 -0
  48. package/dist/scan-PKSYSTRR.js +15 -0
  49. package/dist/{scan-generate-TFZVL3BT.js → scan-generate-VY27PIOX.js} +340 -52
  50. package/dist/scan-generate-VY27PIOX.js.map +1 -0
  51. package/dist/scanner-4KZNOXAK.js +12 -0
  52. package/dist/{service-HKJ6B7P7.js → service-QJGWUIVL.js} +41 -30
  53. package/dist/{snapshot-C5DYIGIV.js → snapshot-WIJMEIFT.js} +2 -3
  54. package/dist/{snapshot-C5DYIGIV.js.map → snapshot-WIJMEIFT.js.map} +1 -1
  55. package/dist/{static-viewer-DUVC4UIM.js → static-viewer-7QIBQZRC.js} +3 -4
  56. package/dist/static-viewer-7QIBQZRC.js.map +1 -0
  57. package/dist/{test-JW7JIDFG.js → test-64Z5BKBA.js} +4 -7
  58. package/dist/{test-JW7JIDFG.js.map → test-64Z5BKBA.js.map} +1 -1
  59. package/dist/token-normalizer-TEPOVBPV.js +312 -0
  60. package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
  61. package/dist/token-parser-32KOIOFN.js +22 -0
  62. package/dist/token-parser-32KOIOFN.js.map +1 -0
  63. package/dist/{tokens-KE73G5JC.js → tokens-NZWFQIAB.js} +10 -9
  64. package/dist/{tokens-KE73G5JC.js.map → tokens-NZWFQIAB.js.map} +1 -1
  65. package/dist/tokens-generate-5JQSJ27E.js +85 -0
  66. package/dist/tokens-generate-5JQSJ27E.js.map +1 -0
  67. package/dist/tokens-push-HY3KO36V.js +148 -0
  68. package/dist/tokens-push-HY3KO36V.js.map +1 -0
  69. package/package.json +8 -6
  70. package/src/bin.ts +300 -48
  71. package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
  72. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
  73. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
  74. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
  75. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
  76. package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
  77. package/src/commands/__tests__/build-freshness.test.ts +231 -0
  78. package/src/commands/__tests__/create.test.ts +71 -0
  79. package/src/commands/__tests__/drift-sync.test.ts +1 -1
  80. package/src/commands/__tests__/govern.test.ts +258 -0
  81. package/src/commands/__tests__/init.test.ts +113 -0
  82. package/src/commands/__tests__/scan-generate.test.ts +189 -70
  83. package/src/commands/__tests__/verify.test.ts +91 -0
  84. package/src/commands/build.ts +54 -1
  85. package/src/commands/context.ts +1 -1
  86. package/src/commands/create.ts +536 -0
  87. package/src/commands/discover.ts +151 -0
  88. package/src/commands/doctor.ts +3 -2
  89. package/src/commands/enhance.ts +3 -1
  90. package/src/commands/govern-scan.ts +565 -0
  91. package/src/commands/govern.ts +67 -4
  92. package/src/commands/init-cloud.ts +32 -4
  93. package/src/commands/init.ts +152 -28
  94. package/src/commands/inspect.ts +290 -0
  95. package/src/commands/migrate-contract.ts +85 -0
  96. package/src/commands/push-contracts.ts +112 -0
  97. package/src/commands/scan-generate.ts +439 -51
  98. package/src/commands/scan.ts +14 -0
  99. package/src/commands/setup.ts +27 -50
  100. package/src/commands/sync.ts +2 -2
  101. package/src/commands/tokens-generate.ts +113 -0
  102. package/src/commands/tokens-push.ts +199 -0
  103. package/src/commands/verify.ts +195 -1
  104. package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
  105. package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
  106. package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
  107. package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
  108. package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
  109. package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
  110. package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
  111. package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
  112. package/src/core/__tests__/contract-parity.test.ts +316 -0
  113. package/src/core/__tests__/token-resolver.test.ts +1 -1
  114. package/src/core/component-extractor.test.ts +40 -1
  115. package/src/core/config.ts +2 -1
  116. package/src/core/discovery.ts +13 -2
  117. package/src/core/drift-verifier.ts +123 -0
  118. package/src/core/extractor-adapter.ts +80 -0
  119. package/src/index.ts +3 -3
  120. package/src/mcp/__tests__/projectFields.test.ts +1 -1
  121. package/src/mcp/utils.ts +1 -50
  122. package/src/migrate/converter.ts +3 -3
  123. package/src/migrate/fragment-to-contract.ts +253 -0
  124. package/src/migrate/report.ts +1 -1
  125. package/src/scripts/token-benchmark.ts +121 -0
  126. package/src/service/__tests__/props-extractor.test.ts +94 -0
  127. package/src/service/__tests__/token-normalizer.test.ts +690 -0
  128. package/src/service/ast-utils.ts +4 -23
  129. package/src/service/babel-config.ts +23 -0
  130. package/src/service/enhance/converter.ts +61 -0
  131. package/src/service/enhance/props-extractor.ts +25 -8
  132. package/src/service/enhance/scanner.ts +5 -24
  133. package/src/service/index.ts +8 -0
  134. package/src/service/snippet-validation.ts +9 -3
  135. package/src/service/tailwind-v4-parser.ts +314 -0
  136. package/src/service/token-normalizer.ts +510 -0
  137. package/src/service/token-parser.ts +56 -0
  138. package/src/setup.ts +10 -39
  139. package/src/shared/index.ts +1 -0
  140. package/src/shared/project-fields.ts +46 -0
  141. package/src/theme/__tests__/component-contrast.test.ts +2 -2
  142. package/src/theme/__tests__/serializer.test.ts +1 -1
  143. package/src/theme/generator.ts +16 -1
  144. package/src/theme/schema.ts +8 -0
  145. package/src/theme/serializer.ts +13 -9
  146. package/src/theme/types.ts +8 -0
  147. package/src/validators.ts +1 -2
  148. package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
  149. package/src/viewer/style-utils.ts +27 -412
  150. package/src/viewer/vite-plugin.ts +2 -2
  151. package/dist/chunk-55KERLWL.js.map +0 -1
  152. package/dist/chunk-5A6X2Y73.js.map +0 -1
  153. package/dist/chunk-APTQIBS5.js.map +0 -1
  154. package/dist/chunk-EYXVAMEX.js +0 -626
  155. package/dist/chunk-EYXVAMEX.js.map +0 -1
  156. package/dist/chunk-I34BC3CU.js.map +0 -1
  157. package/dist/chunk-LOYS64QS.js +0 -2453
  158. package/dist/chunk-LOYS64QS.js.map +0 -1
  159. package/dist/chunk-Z7EY4VHE.js +0 -50
  160. package/dist/chunk-ZKTFKHWN.js +0 -324
  161. package/dist/chunk-ZKTFKHWN.js.map +0 -1
  162. package/dist/discovery-VDANZAJ2.js +0 -28
  163. package/dist/init-WRUSW7R5.js.map +0 -1
  164. package/dist/sass.node-4XJK6YBF.js +0 -130708
  165. package/dist/sass.node-4XJK6YBF.js.map +0 -1
  166. package/dist/scan-YJHQIRKG.js +0 -14
  167. package/dist/scan-generate-TFZVL3BT.js.map +0 -1
  168. package/dist/viewer-2TZS3NDL.js +0 -2730
  169. package/dist/viewer-2TZS3NDL.js.map +0 -1
  170. package/src/build.ts +0 -612
  171. package/src/commands/dev.ts +0 -107
  172. package/src/core/auto-props.ts +0 -464
  173. package/src/core/component-extractor.ts +0 -1030
  174. package/src/core/token-resolver.ts +0 -155
  175. /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
  176. /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
  177. /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
  178. /package/dist/{discovery-VDANZAJ2.js.map → node-37AUE74M.js.map} +0 -0
  179. /package/dist/{scan-YJHQIRKG.js.map → scan-PKSYSTRR.js.map} +0 -0
  180. /package/dist/{service-HKJ6B7P7.js.map → scanner-4KZNOXAK.js.map} +0 -0
  181. /package/dist/{static-viewer-DUVC4UIM.js.map → service-QJGWUIVL.js.map} +0 -0
@@ -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
  // ---------------------------------------------------------------------------
@@ -0,0 +1,258 @@
1
+ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
2
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+
7
+ describe('govern init', () => {
8
+ let tmpDir: string;
9
+ let originalCwd: string;
10
+
11
+ beforeAll(async () => {
12
+ tmpDir = await mkdtemp(join(tmpdir(), 'fragments-govern-test-'));
13
+ originalCwd = process.cwd();
14
+ });
15
+
16
+ afterAll(async () => {
17
+ process.chdir(originalCwd);
18
+ await rm(tmpDir, { recursive: true, force: true });
19
+ });
20
+
21
+ it(
22
+ 'generates a fragments.config.ts with default output path',
23
+ { timeout: 15_000 },
24
+ async () => {
25
+ const projectDir = join(tmpDir, 'default-init');
26
+ await mkdir(projectDir, { recursive: true });
27
+ process.chdir(projectDir);
28
+
29
+ const { governInit } = await import('../govern.js');
30
+ await governInit();
31
+
32
+ const configPath = join(projectDir, 'fragments.config.ts');
33
+ expect(existsSync(configPath)).toBe(true);
34
+
35
+ const content = await readFile(configPath, 'utf-8');
36
+ expect(content).toContain('rules');
37
+ },
38
+ );
39
+
40
+ it(
41
+ 'generates config at custom output path',
42
+ { timeout: 15_000 },
43
+ async () => {
44
+ const projectDir = join(tmpDir, 'custom-output');
45
+ await mkdir(projectDir, { recursive: true });
46
+ process.chdir(projectDir);
47
+
48
+ const customPath = join(projectDir, 'my-config.ts');
49
+ const { governInit } = await import('../govern.js');
50
+ await governInit({ output: customPath });
51
+
52
+ expect(existsSync(customPath)).toBe(true);
53
+ const content = await readFile(customPath, 'utf-8');
54
+ expect(content).toContain('rules');
55
+ },
56
+ );
57
+
58
+ it(
59
+ 'auto-detects Tailwind config and includes it in token sources',
60
+ { timeout: 15_000 },
61
+ async () => {
62
+ const projectDir = join(tmpDir, 'tailwind-detect');
63
+ await mkdir(projectDir, { recursive: true });
64
+ process.chdir(projectDir);
65
+
66
+ // Create a tailwind config file
67
+ await writeFile(
68
+ join(projectDir, 'tailwind.config.ts'),
69
+ 'export default { content: ["./src/**/*.tsx"] };',
70
+ );
71
+
72
+ const { governInit } = await import('../govern.js');
73
+ await governInit();
74
+
75
+ const configPath = join(projectDir, 'fragments.config.ts');
76
+ const content = await readFile(configPath, 'utf-8');
77
+ expect(content).toContain('tailwind.config.ts');
78
+ },
79
+ );
80
+
81
+ it(
82
+ 'auto-detects SCSS token files',
83
+ { timeout: 15_000 },
84
+ async () => {
85
+ const projectDir = join(tmpDir, 'scss-detect');
86
+ await mkdir(join(projectDir, 'src', 'styles'), { recursive: true });
87
+ process.chdir(projectDir);
88
+
89
+ await writeFile(
90
+ join(projectDir, 'src', 'styles', 'tokens.scss'),
91
+ '$color-primary: #2563EB;',
92
+ );
93
+
94
+ const { governInit } = await import('../govern.js');
95
+ await governInit();
96
+
97
+ const configPath = join(projectDir, 'fragments.config.ts');
98
+ const content = await readFile(configPath, 'utf-8');
99
+ expect(content).toContain('src/styles/tokens.scss');
100
+ },
101
+ );
102
+
103
+ it(
104
+ 'handles empty project gracefully (no token sources)',
105
+ { timeout: 15_000 },
106
+ async () => {
107
+ const projectDir = join(tmpDir, 'empty-project');
108
+ await mkdir(projectDir, { recursive: true });
109
+ process.chdir(projectDir);
110
+
111
+ const { governInit } = await import('../govern.js');
112
+ await governInit();
113
+
114
+ const configPath = join(projectDir, 'fragments.config.ts');
115
+ expect(existsSync(configPath)).toBe(true);
116
+ },
117
+ );
118
+ });
119
+
120
+ describe('govern check', () => {
121
+ let tmpDir: string;
122
+ let originalCwd: string;
123
+
124
+ beforeAll(async () => {
125
+ tmpDir = await mkdtemp(join(tmpdir(), 'fragments-govern-check-'));
126
+ originalCwd = process.cwd();
127
+ });
128
+
129
+ afterAll(async () => {
130
+ process.chdir(originalCwd);
131
+ await rm(tmpDir, { recursive: true, force: true });
132
+ });
133
+
134
+ it(
135
+ 'returns exitCode 0 for a passing spec',
136
+ { timeout: 15_000 },
137
+ async () => {
138
+ const projectDir = join(tmpDir, 'check-pass');
139
+ await mkdir(projectDir, { recursive: true });
140
+ process.chdir(projectDir);
141
+
142
+ // Write a minimal UISpec that should pass basic checks
143
+ const spec = {
144
+ nodes: [
145
+ {
146
+ id: 'root',
147
+ type: 'div',
148
+ props: {},
149
+ children: ['btn'],
150
+ },
151
+ {
152
+ id: 'btn',
153
+ type: 'Button',
154
+ props: { variant: 'primary' },
155
+ },
156
+ ],
157
+ root: 'root',
158
+ };
159
+ await writeFile(
160
+ join(projectDir, 'spec.json'),
161
+ JSON.stringify(spec, null, 2),
162
+ );
163
+
164
+ const { governCheck } = await import('../govern.js');
165
+ const result = await governCheck({
166
+ input: join(projectDir, 'spec.json'),
167
+ quiet: true,
168
+ });
169
+
170
+ // With no config (empty rules), should pass
171
+ expect(typeof result.exitCode).toBe('number');
172
+ expect(result.exitCode === 0 || result.exitCode === 1).toBe(true);
173
+ },
174
+ );
175
+
176
+ it(
177
+ 'returns a valid result structure',
178
+ { timeout: 15_000 },
179
+ async () => {
180
+ const projectDir = join(tmpDir, 'check-structure');
181
+ await mkdir(projectDir, { recursive: true });
182
+ process.chdir(projectDir);
183
+
184
+ const spec = {
185
+ nodes: [{ id: 'root', type: 'div' }],
186
+ root: 'root',
187
+ };
188
+ await writeFile(
189
+ join(projectDir, 'spec.json'),
190
+ JSON.stringify(spec, null, 2),
191
+ );
192
+
193
+ const { governCheck } = await import('../govern.js');
194
+ const result = await governCheck({
195
+ input: join(projectDir, 'spec.json'),
196
+ quiet: true,
197
+ });
198
+
199
+ expect(result).toHaveProperty('exitCode');
200
+ },
201
+ );
202
+ });
203
+
204
+ describe('govern report', () => {
205
+ let tmpDir: string;
206
+ let originalCwd: string;
207
+
208
+ beforeAll(async () => {
209
+ tmpDir = await mkdtemp(join(tmpdir(), 'fragments-govern-report-'));
210
+ originalCwd = process.cwd();
211
+ });
212
+
213
+ afterAll(async () => {
214
+ process.chdir(originalCwd);
215
+ await rm(tmpDir, { recursive: true, force: true });
216
+ });
217
+
218
+ it(
219
+ 'handles missing audit log gracefully',
220
+ { timeout: 10_000 },
221
+ async () => {
222
+ const projectDir = join(tmpDir, 'no-log');
223
+ await mkdir(projectDir, { recursive: true });
224
+ process.chdir(projectDir);
225
+
226
+ const { governReport } = await import('../govern.js');
227
+ // Should not throw
228
+ await governReport();
229
+ },
230
+ );
231
+
232
+ it(
233
+ 'parses and summarizes audit log',
234
+ { timeout: 10_000 },
235
+ async () => {
236
+ const projectDir = join(tmpDir, 'with-log');
237
+ await mkdir(projectDir, { recursive: true });
238
+ process.chdir(projectDir);
239
+
240
+ const entries = [
241
+ { score: 90, passed: true, violationCount: 1 },
242
+ { score: 70, passed: false, violationCount: 5 },
243
+ { score: 100, passed: true, violationCount: 0 },
244
+ ];
245
+ const logContent = entries
246
+ .map((e) => JSON.stringify(e))
247
+ .join('\n');
248
+ await writeFile(join(projectDir, '.govern-audit.jsonl'), logContent);
249
+
250
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
251
+ const { governReport } = await import('../govern.js');
252
+ await governReport();
253
+ consoleSpy.mockRestore();
254
+
255
+ // Verify it ran without error — the console output is tested implicitly
256
+ },
257
+ );
258
+ });
@@ -0,0 +1,113 @@
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 { init } from "../init.js";
7
+
8
+ describe("init", () => {
9
+ let tmpDir: string;
10
+
11
+ beforeAll(async () => {
12
+ tmpDir = await mkdtemp(join(tmpdir(), "fragments-init-test-"));
13
+ });
14
+
15
+ afterAll(async () => {
16
+ await rm(tmpDir, { recursive: true, force: true });
17
+ });
18
+
19
+ it("skips Fragments UI runtime injection for shadcn-style projects", { timeout: 10_000 }, async () => {
20
+ const projectDir = join(tmpDir, "shadcn-app");
21
+ await mkdir(join(projectDir, "src", "components", "ui"), { recursive: true });
22
+
23
+ await writeFile(
24
+ join(projectDir, "package.json"),
25
+ JSON.stringify(
26
+ {
27
+ name: "shadcn-app",
28
+ private: true,
29
+ devDependencies: {
30
+ tailwindcss: "^4.0.0",
31
+ },
32
+ },
33
+ null,
34
+ 2
35
+ )
36
+ );
37
+ await writeFile(join(projectDir, "vite.config.ts"), "export default {};\n");
38
+ await writeFile(
39
+ join(projectDir, "src", "main.tsx"),
40
+ [
41
+ "import { StrictMode } from 'react';",
42
+ "import { createRoot } from 'react-dom/client';",
43
+ "import App from './App';",
44
+ "",
45
+ "createRoot(document.getElementById('root')!).render(",
46
+ " <StrictMode><App /></StrictMode>",
47
+ ");",
48
+ "",
49
+ ].join("\n")
50
+ );
51
+ await writeFile(join(projectDir, "src", "App.tsx"), "export default function App() { return <div />; }\n");
52
+ await writeFile(
53
+ join(projectDir, "src", "components", "ui", "button.tsx"),
54
+ [
55
+ "export interface ButtonProps {",
56
+ " variant?: 'default' | 'secondary';",
57
+ "}",
58
+ "export function Button({ variant = 'default' }: ButtonProps) {",
59
+ " return <button data-variant={variant} />;",
60
+ "}",
61
+ "",
62
+ ].join("\n")
63
+ );
64
+
65
+ const result = await init({ projectRoot: projectDir });
66
+
67
+ expect(result.success).toBe(true);
68
+
69
+ const main = await readFile(join(projectDir, "src", "main.tsx"), "utf-8");
70
+ expect(main).not.toContain("@fragments-sdk/ui");
71
+ expect(main).not.toContain("ThemeProvider");
72
+ });
73
+
74
+ it("respects explicit metadata-only mode even without an existing UI library", async () => {
75
+ const projectDir = join(tmpDir, "metadata-only-app");
76
+ await mkdir(join(projectDir, "src"), { recursive: true });
77
+
78
+ await writeFile(
79
+ join(projectDir, "package.json"),
80
+ JSON.stringify(
81
+ {
82
+ name: "metadata-only-app",
83
+ private: true,
84
+ },
85
+ null,
86
+ 2
87
+ )
88
+ );
89
+ await writeFile(join(projectDir, "vite.config.ts"), "export default {};\n");
90
+ await writeFile(
91
+ join(projectDir, "src", "main.tsx"),
92
+ [
93
+ "import { StrictMode } from 'react';",
94
+ "import { createRoot } from 'react-dom/client';",
95
+ "import App from './App';",
96
+ "",
97
+ "createRoot(document.getElementById('root')!).render(",
98
+ " <StrictMode><App /></StrictMode>",
99
+ ");",
100
+ "",
101
+ ].join("\n")
102
+ );
103
+ await writeFile(join(projectDir, "src", "App.tsx"), "export default function App() { return <div />; }\n");
104
+
105
+ const result = await init({ projectRoot: projectDir, metadataOnly: true });
106
+
107
+ expect(result.success).toBe(true);
108
+
109
+ const main = await readFile(join(projectDir, "src", "main.tsx"), "utf-8");
110
+ expect(main).not.toContain("@fragments-sdk/ui");
111
+ expect(main).not.toContain("ThemeProvider");
112
+ });
113
+ });