@gilav21/shadcn-angular 0.0.22 → 0.0.24
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/commands/add.d.ts +13 -1
- package/dist/commands/add.js +39 -20
- package/dist/commands/add.spec.js +301 -11
- package/dist/index.js +1 -1
- package/dist/registry/index.js +16 -0
- package/package.json +1 -1
- package/src/commands/add.spec.ts +441 -12
- package/src/commands/add.ts +621 -598
- package/src/commands/init.ts +314 -314
- package/src/registry/index.ts +16 -0
package/src/commands/add.spec.ts
CHANGED
|
@@ -1,6 +1,445 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
resolveDependencies,
|
|
4
|
+
promptOptionalDependencies,
|
|
5
|
+
normalizeContent,
|
|
6
|
+
fetchAndTransform,
|
|
7
|
+
checkFileConflict,
|
|
8
|
+
classifyComponent,
|
|
9
|
+
detectConflicts,
|
|
10
|
+
type AddOptions,
|
|
11
|
+
} from './add.js';
|
|
3
12
|
import { registry, type ComponentName, type ComponentDefinition } from '../registry/index.js';
|
|
13
|
+
import fs from 'fs-extra';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Module-level mocks
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
vi.mock('fs-extra', () => ({
|
|
20
|
+
default: {
|
|
21
|
+
existsSync: vi.fn(() => false),
|
|
22
|
+
pathExists: vi.fn(() => Promise.resolve(false)),
|
|
23
|
+
readFile: vi.fn(() => Promise.resolve('')),
|
|
24
|
+
writeFile: vi.fn(() => Promise.resolve()),
|
|
25
|
+
ensureDir: vi.fn(() => Promise.resolve()),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('prompts', () => ({
|
|
30
|
+
default: vi.fn(),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const mockedFs = vi.mocked(fs);
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// normalizeContent
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
describe('normalizeContent', () => {
|
|
40
|
+
it('trims whitespace from both ends', () => {
|
|
41
|
+
expect(normalizeContent(' hello ')).toBe('hello');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('converts CRLF to LF', () => {
|
|
45
|
+
expect(normalizeContent('line1\r\nline2\r\n')).toBe('line1\nline2');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('handles mixed line endings', () => {
|
|
49
|
+
expect(normalizeContent('a\r\nb\nc\r\n')).toBe('a\nb\nc');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns empty string for whitespace-only input', () => {
|
|
53
|
+
expect(normalizeContent(' \r\n ')).toBe('');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// fetchAndTransform
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
describe('fetchAndTransform', () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.restoreAllMocks();
|
|
64
|
+
mockedFs.existsSync.mockReturnValue(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('replaces single-level relative lib imports with alias', async () => {
|
|
68
|
+
const mockContent = "import { cn } from '../lib/utils';";
|
|
69
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
70
|
+
new Response(mockContent, { status: 200 }),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const result = await fetchAndTransform('button/button.ts', { branch: 'master', remote: true }, '@/components/lib');
|
|
74
|
+
expect(result).toBe("import { cn } from '@/components/lib/utils';");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('replaces multi-level relative lib imports with alias', async () => {
|
|
78
|
+
const mockContent = "import { cn } from '../../lib/utils';";
|
|
79
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
80
|
+
new Response(mockContent, { status: 200 }),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const result = await fetchAndTransform('deep/nested/comp.ts', { branch: 'master', remote: true }, '@/components/lib');
|
|
84
|
+
expect(result).toBe("import { cn } from '@/components/lib/utils';");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('replaces all occurrences in a file (global regex)', async () => {
|
|
88
|
+
const mockContent = [
|
|
89
|
+
"import { cn } from '../lib/utils';",
|
|
90
|
+
"import { foo } from '../lib/helpers';",
|
|
91
|
+
].join('\n');
|
|
92
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
93
|
+
new Response(mockContent, { status: 200 }),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const result = await fetchAndTransform('comp.ts', { branch: 'master', remote: true }, '@/lib');
|
|
97
|
+
expect(result).toBe([
|
|
98
|
+
"import { cn } from '@/lib/utils';",
|
|
99
|
+
"import { foo } from '@/lib/helpers';",
|
|
100
|
+
].join('\n'));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('does not throw with replaceAll and regex (the original bug)', async () => {
|
|
104
|
+
const mockContent = "import { cn } from '../lib/utils';";
|
|
105
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
106
|
+
new Response(mockContent, { status: 200 }),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
await expect(
|
|
110
|
+
fetchAndTransform('comp.ts', { branch: 'master', remote: true }, '@/lib'),
|
|
111
|
+
).resolves.not.toThrow();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('leaves content unchanged when no lib imports exist', async () => {
|
|
115
|
+
const mockContent = "import { Component } from '@angular/core';";
|
|
116
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
117
|
+
new Response(mockContent, { status: 200 }),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const result = await fetchAndTransform('comp.ts', { branch: 'master', remote: true }, '@/lib');
|
|
121
|
+
expect(result).toBe("import { Component } from '@angular/core';");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// checkFileConflict
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
describe('checkFileConflict', () => {
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
vi.restoreAllMocks();
|
|
132
|
+
mockedFs.existsSync.mockReturnValue(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('returns "missing" when file does not exist on disk', async () => {
|
|
136
|
+
mockedFs.pathExists.mockResolvedValue(false as never);
|
|
137
|
+
const cache = new Map<string, string>();
|
|
138
|
+
|
|
139
|
+
const result = await checkFileConflict(
|
|
140
|
+
'button/button.ts', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache,
|
|
141
|
+
);
|
|
142
|
+
expect(result).toBe('missing');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns "identical" when local and remote content match', async () => {
|
|
146
|
+
const content = "import { cn } from '@/lib/utils';\nexport class Button {}";
|
|
147
|
+
mockedFs.pathExists.mockResolvedValue(true as never);
|
|
148
|
+
mockedFs.readFile.mockResolvedValue(content as never);
|
|
149
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
150
|
+
new Response(content.replaceAll('@/lib/', '../lib/'), { status: 200 }),
|
|
151
|
+
);
|
|
152
|
+
const cache = new Map<string, string>();
|
|
153
|
+
|
|
154
|
+
const result = await checkFileConflict(
|
|
155
|
+
'button/button.ts', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache,
|
|
156
|
+
);
|
|
157
|
+
expect(result).toBe('identical');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('returns "changed" when local and remote content differ', async () => {
|
|
161
|
+
mockedFs.pathExists.mockResolvedValue(true as never);
|
|
162
|
+
mockedFs.readFile.mockResolvedValue('// local modified version' as never);
|
|
163
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
164
|
+
new Response('// remote original version', { status: 200 }),
|
|
165
|
+
);
|
|
166
|
+
const cache = new Map<string, string>();
|
|
167
|
+
|
|
168
|
+
const result = await checkFileConflict(
|
|
169
|
+
'button/button.ts', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache,
|
|
170
|
+
);
|
|
171
|
+
expect(result).toBe('changed');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('populates the content cache on successful fetch', async () => {
|
|
175
|
+
mockedFs.pathExists.mockResolvedValue(true as never);
|
|
176
|
+
mockedFs.readFile.mockResolvedValue('// local' as never);
|
|
177
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
178
|
+
new Response('// remote', { status: 200 }),
|
|
179
|
+
);
|
|
180
|
+
const cache = new Map<string, string>();
|
|
181
|
+
|
|
182
|
+
await checkFileConflict(
|
|
183
|
+
'button/button.ts', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache,
|
|
184
|
+
);
|
|
185
|
+
expect(cache.has('button/button.ts')).toBe(true);
|
|
186
|
+
expect(cache.get('button/button.ts')).toBe('// remote');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// classifyComponent — peer-file decoupling
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
describe('classifyComponent', () => {
|
|
195
|
+
beforeEach(() => {
|
|
196
|
+
vi.restoreAllMocks();
|
|
197
|
+
mockedFs.existsSync.mockReturnValue(false);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('returns "install" when component files are missing', async () => {
|
|
201
|
+
mockedFs.pathExists.mockResolvedValue(false as never);
|
|
202
|
+
const cache = new Map<string, string>();
|
|
203
|
+
const peerFiles = new Set<string>();
|
|
204
|
+
|
|
205
|
+
const result = await classifyComponent(
|
|
206
|
+
'badge', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache, peerFiles,
|
|
207
|
+
);
|
|
208
|
+
expect(result).toBe('install');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('returns "skip" when all component files are present and identical', async () => {
|
|
212
|
+
const content = '// badge content';
|
|
213
|
+
mockedFs.pathExists.mockResolvedValue(true as never);
|
|
214
|
+
mockedFs.readFile.mockResolvedValue(content as never);
|
|
215
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
216
|
+
new Response(content, { status: 200 }),
|
|
217
|
+
);
|
|
218
|
+
const cache = new Map<string, string>();
|
|
219
|
+
const peerFiles = new Set<string>();
|
|
220
|
+
|
|
221
|
+
const result = await classifyComponent(
|
|
222
|
+
'badge', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache, peerFiles,
|
|
223
|
+
);
|
|
224
|
+
expect(result).toBe('skip');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns "conflict" when own files have changed', async () => {
|
|
228
|
+
mockedFs.pathExists.mockResolvedValue(true as never);
|
|
229
|
+
mockedFs.readFile.mockResolvedValue('// modified locally' as never);
|
|
230
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
231
|
+
new Response('// original remote', { status: 200 }),
|
|
232
|
+
);
|
|
233
|
+
const cache = new Map<string, string>();
|
|
234
|
+
const peerFiles = new Set<string>();
|
|
235
|
+
|
|
236
|
+
const result = await classifyComponent(
|
|
237
|
+
'badge', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache, peerFiles,
|
|
238
|
+
);
|
|
239
|
+
expect(result).toBe('conflict');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('does NOT return "conflict" when only peer files have changed', async () => {
|
|
243
|
+
const componentWithPeers = Object.entries(registry).find(
|
|
244
|
+
([, def]) => def.peerFiles && def.peerFiles.length > 0,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (!componentWithPeers) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const [name, def] = componentWithPeers;
|
|
252
|
+
let readCallCount = 0;
|
|
253
|
+
|
|
254
|
+
mockedFs.pathExists.mockResolvedValue(true as never);
|
|
255
|
+
mockedFs.readFile.mockImplementation((() => {
|
|
256
|
+
readCallCount++;
|
|
257
|
+
if (readCallCount <= def.files.length) {
|
|
258
|
+
return Promise.resolve('// identical content');
|
|
259
|
+
}
|
|
260
|
+
return Promise.resolve('// modified peer file');
|
|
261
|
+
}) as never);
|
|
262
|
+
|
|
263
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(() =>
|
|
264
|
+
Promise.resolve(new Response('// identical content', { status: 200 })),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const cache = new Map<string, string>();
|
|
268
|
+
const peerFilesToUpdate = new Set<string>();
|
|
269
|
+
|
|
270
|
+
const result = await classifyComponent(
|
|
271
|
+
name as ComponentName, '/project/ui', { branch: 'master', remote: true }, '@/lib',
|
|
272
|
+
cache, peerFilesToUpdate,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
expect(result).toBe('skip');
|
|
276
|
+
expect(peerFilesToUpdate.size).toBeGreaterThan(0);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// detectConflicts
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
describe('detectConflicts', () => {
|
|
285
|
+
beforeEach(() => {
|
|
286
|
+
vi.restoreAllMocks();
|
|
287
|
+
mockedFs.existsSync.mockReturnValue(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('classifies missing components as toInstall', async () => {
|
|
291
|
+
mockedFs.pathExists.mockResolvedValue(false as never);
|
|
292
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
293
|
+
new Response('// content', { status: 200 }),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const components = new Set<ComponentName>(['badge']);
|
|
297
|
+
const result = await detectConflicts(components, '/project/ui', { branch: 'master', remote: true }, '@/lib');
|
|
298
|
+
|
|
299
|
+
expect(result.toInstall).toContain('badge');
|
|
300
|
+
expect(result.conflicting).not.toContain('badge');
|
|
301
|
+
expect(result.toSkip).not.toContain('badge');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('classifies identical components as toSkip', async () => {
|
|
305
|
+
const content = '// badge content';
|
|
306
|
+
mockedFs.pathExists.mockResolvedValue(true as never);
|
|
307
|
+
mockedFs.readFile.mockResolvedValue(content as never);
|
|
308
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
309
|
+
new Response(content, { status: 200 }),
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const components = new Set<ComponentName>(['badge']);
|
|
313
|
+
const result = await detectConflicts(components, '/project/ui', { branch: 'master', remote: true }, '@/lib');
|
|
314
|
+
|
|
315
|
+
expect(result.toSkip).toContain('badge');
|
|
316
|
+
expect(result.toInstall).not.toContain('badge');
|
|
317
|
+
expect(result.conflicting).not.toContain('badge');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('classifies changed components as conflicting', async () => {
|
|
321
|
+
mockedFs.pathExists.mockResolvedValue(true as never);
|
|
322
|
+
mockedFs.readFile.mockResolvedValue('// modified locally' as never);
|
|
323
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
324
|
+
new Response('// remote content', { status: 200 }),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const components = new Set<ComponentName>(['badge']);
|
|
328
|
+
const result = await detectConflicts(components, '/project/ui', { branch: 'master', remote: true }, '@/lib');
|
|
329
|
+
|
|
330
|
+
expect(result.conflicting).toContain('badge');
|
|
331
|
+
expect(result.toInstall).not.toContain('badge');
|
|
332
|
+
expect(result.toSkip).not.toContain('badge');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('populates contentCache for files it checks', async () => {
|
|
336
|
+
mockedFs.pathExists.mockResolvedValue(true as never);
|
|
337
|
+
mockedFs.readFile.mockResolvedValue('// local' as never);
|
|
338
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
339
|
+
new Response('// remote', { status: 200 }),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const components = new Set<ComponentName>(['badge']);
|
|
343
|
+
const result = await detectConflicts(components, '/project/ui', { branch: 'master', remote: true }, '@/lib');
|
|
344
|
+
|
|
345
|
+
expect(result.contentCache.size).toBeGreaterThan(0);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// Peer file filtering after overwrite selection
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
describe('peer file filtering logic', () => {
|
|
354
|
+
it('removes peer files of declined components from peerFilesToUpdate', () => {
|
|
355
|
+
const peerFilesToUpdate = new Set(['shared/peer-a.ts', 'shared/peer-b.ts', 'shared/peer-c.ts']);
|
|
356
|
+
const conflicting: string[] = ['compA', 'compB'];
|
|
357
|
+
const toOverwrite: string[] = ['compA'];
|
|
358
|
+
const finalComponents: string[] = ['compA'];
|
|
359
|
+
|
|
360
|
+
const mockPeerFiles: Record<string, string[] | undefined> = {
|
|
361
|
+
compA: ['shared/peer-a.ts'],
|
|
362
|
+
compB: ['shared/peer-b.ts', 'shared/peer-c.ts'],
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const declined = conflicting.filter(c => !toOverwrite.includes(c));
|
|
366
|
+
|
|
367
|
+
for (const name of declined) {
|
|
368
|
+
const peerFiles = mockPeerFiles[name];
|
|
369
|
+
if (!peerFiles) continue;
|
|
370
|
+
for (const file of peerFiles) {
|
|
371
|
+
const stillNeeded = finalComponents.some(fc =>
|
|
372
|
+
mockPeerFiles[fc]?.includes(file),
|
|
373
|
+
);
|
|
374
|
+
if (!stillNeeded) {
|
|
375
|
+
peerFilesToUpdate.delete(file);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
expect(peerFilesToUpdate.has('shared/peer-a.ts')).toBe(true);
|
|
381
|
+
expect(peerFilesToUpdate.has('shared/peer-b.ts')).toBe(false);
|
|
382
|
+
expect(peerFilesToUpdate.has('shared/peer-c.ts')).toBe(false);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('keeps shared peer files when another final component still needs them', () => {
|
|
386
|
+
const peerFilesToUpdate = new Set(['shared/common.ts']);
|
|
387
|
+
const conflicting: string[] = ['compA', 'compB'];
|
|
388
|
+
const toOverwrite: string[] = ['compA'];
|
|
389
|
+
const finalComponents: string[] = ['compA'];
|
|
390
|
+
|
|
391
|
+
const mockPeerFiles: Record<string, string[] | undefined> = {
|
|
392
|
+
compA: ['shared/common.ts'],
|
|
393
|
+
compB: ['shared/common.ts'],
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const declined = conflicting.filter(c => !toOverwrite.includes(c));
|
|
397
|
+
|
|
398
|
+
for (const name of declined) {
|
|
399
|
+
const peerFiles = mockPeerFiles[name];
|
|
400
|
+
if (!peerFiles) continue;
|
|
401
|
+
for (const file of peerFiles) {
|
|
402
|
+
const stillNeeded = finalComponents.some(fc =>
|
|
403
|
+
mockPeerFiles[fc]?.includes(file),
|
|
404
|
+
);
|
|
405
|
+
if (!stillNeeded) {
|
|
406
|
+
peerFilesToUpdate.delete(file);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
expect(peerFilesToUpdate.has('shared/common.ts')).toBe(true);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('removes all peer files when all components are declined', () => {
|
|
415
|
+
const peerFilesToUpdate = new Set(['shared/peer-a.ts', 'shared/peer-b.ts']);
|
|
416
|
+
const conflicting: string[] = ['compA', 'compB'];
|
|
417
|
+
const toOverwrite: string[] = [];
|
|
418
|
+
const finalComponents: string[] = [];
|
|
419
|
+
|
|
420
|
+
const mockPeerFiles: Record<string, string[] | undefined> = {
|
|
421
|
+
compA: ['shared/peer-a.ts'],
|
|
422
|
+
compB: ['shared/peer-b.ts'],
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const declined = conflicting.filter(c => !toOverwrite.includes(c));
|
|
426
|
+
|
|
427
|
+
for (const name of declined) {
|
|
428
|
+
const peerFiles = mockPeerFiles[name];
|
|
429
|
+
if (!peerFiles) continue;
|
|
430
|
+
for (const file of peerFiles) {
|
|
431
|
+
const stillNeeded = finalComponents.some(fc =>
|
|
432
|
+
mockPeerFiles[fc]?.includes(file),
|
|
433
|
+
);
|
|
434
|
+
if (!stillNeeded) {
|
|
435
|
+
peerFilesToUpdate.delete(file);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
expect(peerFilesToUpdate.size).toBe(0);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
4
443
|
|
|
5
444
|
// ---------------------------------------------------------------------------
|
|
6
445
|
// resolveDependencies
|
|
@@ -14,14 +453,12 @@ describe('resolveDependencies', () => {
|
|
|
14
453
|
});
|
|
15
454
|
|
|
16
455
|
it('includes transitive dependencies', () => {
|
|
17
|
-
// button depends on ripple
|
|
18
456
|
const result = resolveDependencies(['button']);
|
|
19
457
|
expect(result).toContain('button');
|
|
20
458
|
expect(result).toContain('ripple');
|
|
21
459
|
});
|
|
22
460
|
|
|
23
461
|
it('resolves deep transitive chains', () => {
|
|
24
|
-
// date-picker -> calendar -> button -> ripple, calendar -> select
|
|
25
462
|
const result = resolveDependencies(['date-picker']);
|
|
26
463
|
expect(result).toContain('date-picker');
|
|
27
464
|
expect(result).toContain('calendar');
|
|
@@ -31,14 +468,12 @@ describe('resolveDependencies', () => {
|
|
|
31
468
|
});
|
|
32
469
|
|
|
33
470
|
it('deduplicates shared dependencies across multiple inputs', () => {
|
|
34
|
-
// Both button-group and speed-dial depend on button
|
|
35
471
|
const result = resolveDependencies(['button-group', 'speed-dial']);
|
|
36
472
|
expect(result).toContain('button-group');
|
|
37
473
|
expect(result).toContain('speed-dial');
|
|
38
474
|
expect(result).toContain('button');
|
|
39
475
|
expect(result).toContain('ripple');
|
|
40
476
|
|
|
41
|
-
// Count how many times button appears — should be exactly once (it's a Set)
|
|
42
477
|
const asArray = [...result];
|
|
43
478
|
expect(asArray.filter((n: string) => n === 'button')).toHaveLength(1);
|
|
44
479
|
});
|
|
@@ -59,10 +494,6 @@ describe('resolveDependencies', () => {
|
|
|
59
494
|
// promptOptionalDependencies
|
|
60
495
|
// ---------------------------------------------------------------------------
|
|
61
496
|
|
|
62
|
-
vi.mock('prompts', () => ({
|
|
63
|
-
default: vi.fn(),
|
|
64
|
-
}));
|
|
65
|
-
|
|
66
497
|
describe('promptOptionalDependencies', () => {
|
|
67
498
|
it('returns empty array when no components have optional deps', async () => {
|
|
68
499
|
const resolved = new Set<ComponentName>(['badge', 'button', 'ripple']);
|
|
@@ -83,14 +514,12 @@ describe('promptOptionalDependencies', () => {
|
|
|
83
514
|
});
|
|
84
515
|
|
|
85
516
|
it('filters out optional deps already in the resolved set', async () => {
|
|
86
|
-
// context-menu is already resolved, so it should not be offered
|
|
87
517
|
const resolved = new Set<ComponentName>(['data-table', 'context-menu', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
|
|
88
518
|
const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
|
|
89
519
|
expect(result).not.toContain('context-menu');
|
|
90
520
|
});
|
|
91
521
|
|
|
92
522
|
it('deduplicates optional deps across components', async () => {
|
|
93
|
-
// Both data-table and tree have context-menu as optional
|
|
94
523
|
const resolved = new Set<ComponentName>(['data-table', 'tree', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
|
|
95
524
|
const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
|
|
96
525
|
const contextMenuCount = result.filter((n: string) => n === 'context-menu').length;
|