@fragments-sdk/cli 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/ai-client-I6MDWNYA.js +21 -0
  2. package/dist/bin.js +419 -410
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-HRFUSSZI.js → chunk-3SOAPJDX.js} +2 -2
  5. package/dist/{chunk-D5PYOXEI.js → chunk-4K7EAQ5L.js} +148 -13
  6. package/dist/{chunk-D5PYOXEI.js.map → chunk-4K7EAQ5L.js.map} +1 -1
  7. package/dist/chunk-DXX6HADE.js +443 -0
  8. package/dist/chunk-DXX6HADE.js.map +1 -0
  9. package/dist/chunk-EYXVAMEX.js +626 -0
  10. package/dist/chunk-EYXVAMEX.js.map +1 -0
  11. package/dist/{chunk-ZM4ZQZWZ.js → chunk-FO6EBJWP.js} +39 -37
  12. package/dist/chunk-FO6EBJWP.js.map +1 -0
  13. package/dist/{chunk-OQO55NKV.js → chunk-QM7SVOGF.js} +120 -12
  14. package/dist/chunk-QM7SVOGF.js.map +1 -0
  15. package/dist/{chunk-5G3VZH43.js → chunk-RF3C6LGA.js} +281 -351
  16. package/dist/chunk-RF3C6LGA.js.map +1 -0
  17. package/dist/{chunk-WXSR2II7.js → chunk-SM674YAS.js} +58 -6
  18. package/dist/chunk-SM674YAS.js.map +1 -0
  19. package/dist/chunk-SXTKFDCR.js +104 -0
  20. package/dist/chunk-SXTKFDCR.js.map +1 -0
  21. package/dist/{chunk-PW7QTQA6.js → chunk-UV5JQV3R.js} +2 -2
  22. package/dist/core/index.js +13 -1
  23. package/dist/{discovery-NEOY4MPN.js → discovery-VSGC76JN.js} +3 -3
  24. package/dist/{generate-FBHSXR3D.js → generate-QZXOXYFW.js} +4 -4
  25. package/dist/index.js +7 -6
  26. package/dist/index.js.map +1 -1
  27. package/dist/init-XK6PRUE5.js +636 -0
  28. package/dist/init-XK6PRUE5.js.map +1 -0
  29. package/dist/mcp-bin.js +2 -2
  30. package/dist/{scan-CJF2DOQW.js → scan-CHQHXWVD.js} +6 -6
  31. package/dist/scan-generate-U3RFVDTX.js +1115 -0
  32. package/dist/scan-generate-U3RFVDTX.js.map +1 -0
  33. package/dist/{service-TQYWY65E.js → service-MMEKG4MZ.js} +3 -3
  34. package/dist/{snapshot-SV2JOFZH.js → snapshot-53TUR3HW.js} +2 -2
  35. package/dist/{static-viewer-NUBFPKWH.js → static-viewer-KKCR4KXR.js} +3 -3
  36. package/dist/static-viewer-KKCR4KXR.js.map +1 -0
  37. package/dist/{test-Z5LVO724.js → test-5UCKXYSC.js} +4 -4
  38. package/dist/{tokens-CE46OTMD.js → tokens-L46MK5AW.js} +5 -5
  39. package/dist/{viewer-DLLJIMCK.js → viewer-M2EQQSGE.js} +14 -14
  40. package/dist/viewer-M2EQQSGE.js.map +1 -0
  41. package/package.json +11 -9
  42. package/src/ai-client.ts +156 -0
  43. package/src/bin.ts +99 -2
  44. package/src/build.ts +95 -33
  45. package/src/commands/__tests__/drift-sync.test.ts +252 -0
  46. package/src/commands/__tests__/scan-generate.test.ts +497 -45
  47. package/src/commands/enhance.ts +11 -35
  48. package/src/commands/govern.ts +122 -0
  49. package/src/commands/init.ts +288 -260
  50. package/src/commands/scan-generate.ts +740 -139
  51. package/src/commands/scan.ts +37 -32
  52. package/src/commands/setup.ts +143 -52
  53. package/src/commands/sync.ts +357 -0
  54. package/src/commands/validate.ts +43 -1
  55. package/src/core/component-extractor.test.ts +282 -0
  56. package/src/core/component-extractor.ts +1030 -0
  57. package/src/core/discovery.ts +93 -7
  58. package/src/service/enhance/props-extractor.ts +235 -13
  59. package/src/validators.ts +236 -0
  60. package/src/viewer/vite-plugin.ts +1 -1
  61. package/dist/chunk-5G3VZH43.js.map +0 -1
  62. package/dist/chunk-OQO55NKV.js.map +0 -1
  63. package/dist/chunk-WXSR2II7.js.map +0 -1
  64. package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
  65. package/dist/init-UFGK5TCN.js +0 -867
  66. package/dist/init-UFGK5TCN.js.map +0 -1
  67. package/dist/scan-generate-SJAN5MVI.js +0 -691
  68. package/dist/scan-generate-SJAN5MVI.js.map +0 -1
  69. package/dist/viewer-DLLJIMCK.js.map +0 -1
  70. package/src/ai.ts +0 -266
  71. package/src/commands/init-framework.ts +0 -414
  72. package/src/mcp/bin.ts +0 -36
  73. package/src/migrate/bin.ts +0 -114
  74. package/src/theme/index.ts +0 -77
  75. package/src/viewer/bin.ts +0 -86
  76. package/src/viewer/cli/health.ts +0 -256
  77. package/src/viewer/cli/index.ts +0 -33
  78. package/src/viewer/cli/scan.ts +0 -124
  79. package/src/viewer/cli/utils.ts +0 -174
  80. /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
  81. /package/dist/{chunk-HRFUSSZI.js.map → chunk-3SOAPJDX.js.map} +0 -0
  82. /package/dist/{chunk-PW7QTQA6.js.map → chunk-UV5JQV3R.js.map} +0 -0
  83. /package/dist/{scan-CJF2DOQW.js.map → discovery-VSGC76JN.js.map} +0 -0
  84. /package/dist/{generate-FBHSXR3D.js.map → generate-QZXOXYFW.js.map} +0 -0
  85. /package/dist/{service-TQYWY65E.js.map → scan-CHQHXWVD.js.map} +0 -0
  86. /package/dist/{static-viewer-NUBFPKWH.js.map → service-MMEKG4MZ.js.map} +0 -0
  87. /package/dist/{snapshot-SV2JOFZH.js.map → snapshot-53TUR3HW.js.map} +0 -0
  88. /package/dist/{test-Z5LVO724.js.map → test-5UCKXYSC.js.map} +0 -0
  89. /package/dist/{tokens-CE46OTMD.js.map → tokens-L46MK5AW.js.map} +0 -0
@@ -0,0 +1,252 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2
+ import { mkdtemp, writeFile, mkdir, rm, readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { diffProps, validateDrift, type DriftItem } from '../../validators.js';
6
+ import type { PropMeta } from '../../core/component-extractor.js';
7
+ import type { FragmentsConfig } from '@fragments-sdk/core';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Helper: build a PropMeta for tests
11
+ // ---------------------------------------------------------------------------
12
+
13
+ function makeProp(overrides: Partial<PropMeta> & { name: string }): PropMeta {
14
+ return {
15
+ type: overrides.type ?? 'string',
16
+ typeKind: overrides.typeKind ?? 'string',
17
+ required: overrides.required ?? false,
18
+ source: overrides.source ?? 'local',
19
+ name: overrides.name,
20
+ ...(overrides.values && { values: overrides.values }),
21
+ ...(overrides.default !== undefined && { default: overrides.default }),
22
+ ...(overrides.description && { description: overrides.description }),
23
+ };
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Unit tests: diffProps
28
+ // ---------------------------------------------------------------------------
29
+
30
+ describe('diffProps', () => {
31
+ it('detects added props (in source but not in fragment)', () => {
32
+ const fragmentProps = {
33
+ children: { type: 'node', required: true },
34
+ };
35
+ const sourceProps: Record<string, PropMeta> = {
36
+ children: makeProp({ name: 'children', typeKind: 'node', required: true }),
37
+ variant: makeProp({ name: 'variant', typeKind: 'enum', values: ['primary', 'secondary'] }),
38
+ };
39
+
40
+ const drifts = diffProps(fragmentProps, sourceProps);
41
+ const added = drifts.filter(d => d.kind === 'added');
42
+ expect(added.length).toBe(1);
43
+ expect(added[0].prop).toBe('variant');
44
+ });
45
+
46
+ it('detects removed props (in fragment but not in source)', () => {
47
+ const fragmentProps = {
48
+ children: { type: 'node', required: true },
49
+ loading: { type: 'boolean', description: 'Show loading' },
50
+ };
51
+ const sourceProps: Record<string, PropMeta> = {
52
+ children: makeProp({ name: 'children', typeKind: 'node', required: true }),
53
+ };
54
+
55
+ const drifts = diffProps(fragmentProps, sourceProps);
56
+ const removed = drifts.filter(d => d.kind === 'removed');
57
+ expect(removed.length).toBe(1);
58
+ expect(removed[0].prop).toBe('loading');
59
+ });
60
+
61
+ it('detects type changes', () => {
62
+ const fragmentProps = {
63
+ value: { type: 'string', required: false },
64
+ };
65
+ const sourceProps: Record<string, PropMeta> = {
66
+ value: makeProp({ name: 'value', typeKind: 'number' }),
67
+ };
68
+
69
+ const drifts = diffProps(fragmentProps, sourceProps);
70
+ const typeChanged = drifts.filter(d => d.kind === 'type_changed');
71
+ expect(typeChanged.length).toBe(1);
72
+ expect(typeChanged[0].source).toBe('number');
73
+ expect(typeChanged[0].fragment).toBe('string');
74
+ });
75
+
76
+ it('detects required status changes', () => {
77
+ const fragmentProps = {
78
+ name: { type: 'string', required: false },
79
+ };
80
+ const sourceProps: Record<string, PropMeta> = {
81
+ name: makeProp({ name: 'name', typeKind: 'string', required: true }),
82
+ };
83
+
84
+ const drifts = diffProps(fragmentProps, sourceProps);
85
+ const reqChanged = drifts.filter(d => d.kind === 'required_changed');
86
+ expect(reqChanged.length).toBe(1);
87
+ expect(reqChanged[0].source).toBe('true');
88
+ expect(reqChanged[0].fragment).toBe('false');
89
+ });
90
+
91
+ it('detects enum value changes', () => {
92
+ const fragmentProps = {
93
+ variant: { type: 'enum', values: ['primary', 'secondary'] as const },
94
+ };
95
+ const sourceProps: Record<string, PropMeta> = {
96
+ variant: makeProp({
97
+ name: 'variant',
98
+ typeKind: 'enum',
99
+ values: ['primary', 'secondary', 'ghost'],
100
+ }),
101
+ };
102
+
103
+ const drifts = diffProps(fragmentProps, sourceProps);
104
+ const valChanged = drifts.filter(d => d.kind === 'values_changed');
105
+ expect(valChanged.length).toBe(1);
106
+ expect(valChanged[0].source).toContain('ghost');
107
+ });
108
+
109
+ it('detects default value changes', () => {
110
+ const fragmentProps = {
111
+ size: { type: 'enum', default: 'md' },
112
+ };
113
+ const sourceProps: Record<string, PropMeta> = {
114
+ size: makeProp({ name: 'size', typeKind: 'enum', default: 'lg' }),
115
+ };
116
+
117
+ const drifts = diffProps(fragmentProps, sourceProps);
118
+ const defChanged = drifts.filter(d => d.kind === 'default_changed');
119
+ expect(defChanged.length).toBe(1);
120
+ expect(defChanged[0].source).toBe('lg');
121
+ expect(defChanged[0].fragment).toBe('md');
122
+ });
123
+
124
+ it('filters out inherited source props', () => {
125
+ const fragmentProps = {};
126
+ const sourceProps: Record<string, PropMeta> = {
127
+ className: makeProp({ name: 'className', typeKind: 'string', source: 'inherited' }),
128
+ style: makeProp({ name: 'style', typeKind: 'object', source: 'inherited' }),
129
+ };
130
+
131
+ const drifts = diffProps(fragmentProps, sourceProps);
132
+ expect(drifts.length).toBe(0);
133
+ });
134
+
135
+ it('returns empty array when in sync', () => {
136
+ const fragmentProps = {
137
+ children: { type: 'node', required: true },
138
+ variant: { type: 'enum', values: ['a', 'b'] as const },
139
+ };
140
+ const sourceProps: Record<string, PropMeta> = {
141
+ children: makeProp({ name: 'children', typeKind: 'node', required: true }),
142
+ variant: makeProp({ name: 'variant', typeKind: 'enum', values: ['a', 'b'] }),
143
+ };
144
+
145
+ const drifts = diffProps(fragmentProps, sourceProps);
146
+ expect(drifts.length).toBe(0);
147
+ });
148
+
149
+ it('detects multiple drift types simultaneously', () => {
150
+ const fragmentProps = {
151
+ children: { type: 'node', required: true },
152
+ loading: { type: 'boolean' },
153
+ variant: { type: 'enum', values: ['a'] as const },
154
+ };
155
+ const sourceProps: Record<string, PropMeta> = {
156
+ children: makeProp({ name: 'children', typeKind: 'node', required: true }),
157
+ size: makeProp({ name: 'size', typeKind: 'enum', values: ['sm', 'md'] }),
158
+ variant: makeProp({ name: 'variant', typeKind: 'enum', values: ['a', 'b'] }),
159
+ };
160
+
161
+ const drifts = diffProps(fragmentProps, sourceProps);
162
+ expect(drifts.length).toBe(3);
163
+
164
+ const kinds = drifts.map(d => d.kind);
165
+ expect(kinds).toContain('added'); // size
166
+ expect(kinds).toContain('removed'); // loading
167
+ expect(kinds).toContain('values_changed'); // variant
168
+ });
169
+ });
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Integration: validateDrift with real files
173
+ // ---------------------------------------------------------------------------
174
+
175
+ describe('validateDrift integration', () => {
176
+ let tmpDir: string;
177
+
178
+ beforeAll(async () => {
179
+ tmpDir = await mkdtemp(join(tmpdir(), 'drift-int-'));
180
+
181
+ const compDir = join(tmpDir, 'components', 'Tag');
182
+ await mkdir(compDir, { recursive: true });
183
+
184
+ // Simple component — no JSX to avoid esbuild issues
185
+ await writeFile(
186
+ join(compDir, 'index.tsx'),
187
+ `export interface TagProps {
188
+ /** Tag label text */
189
+ label: string;
190
+ /** Color variant */
191
+ color?: 'blue' | 'green' | 'red';
192
+ }
193
+
194
+ export function Tag({ label, color = 'blue' }: TagProps) {
195
+ return label;
196
+ }
197
+ `,
198
+ 'utf-8'
199
+ );
200
+
201
+ // Fragment with missing 'color' prop and stale 'icon' prop
202
+ await writeFile(
203
+ join(compDir, 'Tag.fragment.tsx'),
204
+ `import { defineFragment } from '@fragments-sdk/core';
205
+ import { Tag } from './index';
206
+
207
+ export default defineFragment({
208
+ component: Tag,
209
+ meta: { name: 'Tag', description: 'A tag component', category: 'Display', status: 'stable' },
210
+ usage: { when: ['Label items'], whenNot: ['Navigation'] },
211
+ props: {
212
+ label: { type: 'string', description: 'Tag label text', required: true },
213
+ icon: { type: 'node', description: 'Leading icon' },
214
+ },
215
+ variants: [],
216
+ });
217
+ `,
218
+ 'utf-8'
219
+ );
220
+ });
221
+
222
+ afterAll(async () => {
223
+ await rm(tmpDir, { recursive: true, force: true });
224
+ });
225
+
226
+ it('detects drift in real fragment files', async () => {
227
+ const config: FragmentsConfig = {
228
+ include: ['components/**/*.fragment.tsx'],
229
+ exclude: [],
230
+ components: ['components/**/index.tsx'],
231
+ outFile: 'fragments.json',
232
+ dataDir: '.fragments',
233
+ } as FragmentsConfig;
234
+
235
+ const result = await validateDrift(config, tmpDir);
236
+
237
+ // Should find drift: 'color' added (in source, not in fragment), 'icon' removed (in fragment, not in source)
238
+ expect(result.reports.length).toBe(1);
239
+ expect(result.reports[0].component).toBe('Tag');
240
+
241
+ const driftKinds = result.reports[0].drifts.map(d => d.kind);
242
+ expect(driftKinds).toContain('added');
243
+ expect(driftKinds).toContain('removed');
244
+
245
+ // 'icon' removal should be an error (breaks existing code)
246
+ expect(result.valid).toBe(false);
247
+ expect(result.errors.some(e => e.message.includes('icon'))).toBe(true);
248
+
249
+ // 'color' addition should be a warning (new undocumented prop)
250
+ expect(result.warnings.some(w => w.message.includes('color'))).toBe(true);
251
+ });
252
+ });