@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.
- package/dist/ai-client-I6MDWNYA.js +21 -0
- package/dist/bin.js +419 -410
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-HRFUSSZI.js → chunk-3SOAPJDX.js} +2 -2
- package/dist/{chunk-D5PYOXEI.js → chunk-4K7EAQ5L.js} +148 -13
- package/dist/{chunk-D5PYOXEI.js.map → chunk-4K7EAQ5L.js.map} +1 -1
- package/dist/chunk-DXX6HADE.js +443 -0
- package/dist/chunk-DXX6HADE.js.map +1 -0
- package/dist/chunk-EYXVAMEX.js +626 -0
- package/dist/chunk-EYXVAMEX.js.map +1 -0
- package/dist/{chunk-ZM4ZQZWZ.js → chunk-FO6EBJWP.js} +39 -37
- package/dist/chunk-FO6EBJWP.js.map +1 -0
- package/dist/{chunk-OQO55NKV.js → chunk-QM7SVOGF.js} +120 -12
- package/dist/chunk-QM7SVOGF.js.map +1 -0
- package/dist/{chunk-5G3VZH43.js → chunk-RF3C6LGA.js} +281 -351
- package/dist/chunk-RF3C6LGA.js.map +1 -0
- package/dist/{chunk-WXSR2II7.js → chunk-SM674YAS.js} +58 -6
- package/dist/chunk-SM674YAS.js.map +1 -0
- package/dist/chunk-SXTKFDCR.js +104 -0
- package/dist/chunk-SXTKFDCR.js.map +1 -0
- package/dist/{chunk-PW7QTQA6.js → chunk-UV5JQV3R.js} +2 -2
- package/dist/core/index.js +13 -1
- package/dist/{discovery-NEOY4MPN.js → discovery-VSGC76JN.js} +3 -3
- package/dist/{generate-FBHSXR3D.js → generate-QZXOXYFW.js} +4 -4
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/init-XK6PRUE5.js +636 -0
- package/dist/init-XK6PRUE5.js.map +1 -0
- package/dist/mcp-bin.js +2 -2
- package/dist/{scan-CJF2DOQW.js → scan-CHQHXWVD.js} +6 -6
- package/dist/scan-generate-U3RFVDTX.js +1115 -0
- package/dist/scan-generate-U3RFVDTX.js.map +1 -0
- package/dist/{service-TQYWY65E.js → service-MMEKG4MZ.js} +3 -3
- package/dist/{snapshot-SV2JOFZH.js → snapshot-53TUR3HW.js} +2 -2
- package/dist/{static-viewer-NUBFPKWH.js → static-viewer-KKCR4KXR.js} +3 -3
- package/dist/static-viewer-KKCR4KXR.js.map +1 -0
- package/dist/{test-Z5LVO724.js → test-5UCKXYSC.js} +4 -4
- package/dist/{tokens-CE46OTMD.js → tokens-L46MK5AW.js} +5 -5
- package/dist/{viewer-DLLJIMCK.js → viewer-M2EQQSGE.js} +14 -14
- package/dist/viewer-M2EQQSGE.js.map +1 -0
- package/package.json +11 -9
- package/src/ai-client.ts +156 -0
- package/src/bin.ts +99 -2
- package/src/build.ts +95 -33
- package/src/commands/__tests__/drift-sync.test.ts +252 -0
- package/src/commands/__tests__/scan-generate.test.ts +497 -45
- package/src/commands/enhance.ts +11 -35
- package/src/commands/govern.ts +122 -0
- package/src/commands/init.ts +288 -260
- package/src/commands/scan-generate.ts +740 -139
- package/src/commands/scan.ts +37 -32
- package/src/commands/setup.ts +143 -52
- package/src/commands/sync.ts +357 -0
- package/src/commands/validate.ts +43 -1
- package/src/core/component-extractor.test.ts +282 -0
- package/src/core/component-extractor.ts +1030 -0
- package/src/core/discovery.ts +93 -7
- package/src/service/enhance/props-extractor.ts +235 -13
- package/src/validators.ts +236 -0
- package/src/viewer/vite-plugin.ts +1 -1
- package/dist/chunk-5G3VZH43.js.map +0 -1
- package/dist/chunk-OQO55NKV.js.map +0 -1
- package/dist/chunk-WXSR2II7.js.map +0 -1
- package/dist/chunk-ZM4ZQZWZ.js.map +0 -1
- package/dist/init-UFGK5TCN.js +0 -867
- package/dist/init-UFGK5TCN.js.map +0 -1
- package/dist/scan-generate-SJAN5MVI.js +0 -691
- package/dist/scan-generate-SJAN5MVI.js.map +0 -1
- package/dist/viewer-DLLJIMCK.js.map +0 -1
- package/src/ai.ts +0 -266
- package/src/commands/init-framework.ts +0 -414
- package/src/mcp/bin.ts +0 -36
- package/src/migrate/bin.ts +0 -114
- package/src/theme/index.ts +0 -77
- package/src/viewer/bin.ts +0 -86
- package/src/viewer/cli/health.ts +0 -256
- package/src/viewer/cli/index.ts +0 -33
- package/src/viewer/cli/scan.ts +0 -124
- package/src/viewer/cli/utils.ts +0 -174
- /package/dist/{discovery-NEOY4MPN.js.map → ai-client-I6MDWNYA.js.map} +0 -0
- /package/dist/{chunk-HRFUSSZI.js.map → chunk-3SOAPJDX.js.map} +0 -0
- /package/dist/{chunk-PW7QTQA6.js.map → chunk-UV5JQV3R.js.map} +0 -0
- /package/dist/{scan-CJF2DOQW.js.map → discovery-VSGC76JN.js.map} +0 -0
- /package/dist/{generate-FBHSXR3D.js.map → generate-QZXOXYFW.js.map} +0 -0
- /package/dist/{service-TQYWY65E.js.map → scan-CHQHXWVD.js.map} +0 -0
- /package/dist/{static-viewer-NUBFPKWH.js.map → service-MMEKG4MZ.js.map} +0 -0
- /package/dist/{snapshot-SV2JOFZH.js.map → snapshot-53TUR3HW.js.map} +0 -0
- /package/dist/{test-Z5LVO724.js.map → test-5UCKXYSC.js.map} +0 -0
- /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
|
+
});
|