@gilav21/shadcn-angular 0.0.22 → 0.0.23

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.
@@ -1,5 +1,5 @@
1
1
  import { type ComponentName } from '../registry/index.js';
2
- interface AddOptions {
2
+ export interface AddOptions {
3
3
  yes?: boolean;
4
4
  overwrite?: boolean;
5
5
  all?: boolean;
@@ -7,7 +7,19 @@ interface AddOptions {
7
7
  remote?: boolean;
8
8
  branch: string;
9
9
  }
10
+ interface ConflictCheckResult {
11
+ toInstall: ComponentName[];
12
+ toSkip: string[];
13
+ conflicting: ComponentName[];
14
+ peerFilesToUpdate: Set<string>;
15
+ contentCache: Map<string, string>;
16
+ }
17
+ export declare function normalizeContent(str: string): string;
18
+ export declare function fetchAndTransform(file: string, options: AddOptions, utilsAlias: string): Promise<string>;
10
19
  export declare function resolveDependencies(names: ComponentName[]): Set<ComponentName>;
11
20
  export declare function promptOptionalDependencies(resolved: Set<ComponentName>, options: AddOptions): Promise<ComponentName[]>;
21
+ export declare function checkFileConflict(file: string, targetDir: string, options: AddOptions, utilsAlias: string, contentCache: Map<string, string>): Promise<'identical' | 'changed' | 'missing'>;
22
+ export declare function classifyComponent(name: ComponentName, targetDir: string, options: AddOptions, utilsAlias: string, contentCache: Map<string, string>, peerFilesToUpdate: Set<string>): Promise<'install' | 'skip' | 'conflict'>;
23
+ export declare function detectConflicts(allComponents: Set<ComponentName>, targetDir: string, options: AddOptions, utilsAlias: string): Promise<ConflictCheckResult>;
12
24
  export declare function add(components: string[], options: AddOptions): Promise<void>;
13
25
  export {};
@@ -50,7 +50,7 @@ function aliasToProjectPath(aliasOrPath) {
50
50
  ? path.join('src', aliasOrPath.slice(2))
51
51
  : aliasOrPath;
52
52
  }
53
- function normalizeContent(str) {
53
+ export function normalizeContent(str) {
54
54
  return str.replaceAll('\r\n', '\n').trim();
55
55
  }
56
56
  // ---------------------------------------------------------------------------
@@ -94,9 +94,9 @@ async function fetchLibContent(file, options) {
94
94
  }
95
95
  return response.text();
96
96
  }
97
- async function fetchAndTransform(file, options, utilsAlias) {
97
+ export async function fetchAndTransform(file, options, utilsAlias) {
98
98
  let content = await fetchComponentContent(file, options);
99
- content = content.replaceAll(/(\.\.\/)+lib\//, utilsAlias + '/');
99
+ content = content.replaceAll(/(\.\.\/)+lib\//g, utilsAlias + '/');
100
100
  return content;
101
101
  }
102
102
  // ---------------------------------------------------------------------------
@@ -175,7 +175,7 @@ export async function promptOptionalDependencies(resolved, options) {
175
175
  // ---------------------------------------------------------------------------
176
176
  // Conflict detection
177
177
  // ---------------------------------------------------------------------------
178
- async function checkFileConflict(file, targetDir, options, utilsAlias, contentCache) {
178
+ export async function checkFileConflict(file, targetDir, options, utilsAlias, contentCache) {
179
179
  const targetPath = path.join(targetDir, file);
180
180
  if (!await fs.pathExists(targetPath)) {
181
181
  return 'missing';
@@ -194,38 +194,33 @@ async function checkFileConflict(file, targetDir, options, utilsAlias, contentCa
194
194
  }
195
195
  async function checkPeerFiles(component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate) {
196
196
  if (!component.peerFiles)
197
- return false;
198
- let hasChanges = false;
197
+ return;
199
198
  for (const file of component.peerFiles) {
200
199
  const status = await checkFileConflict(file, targetDir, options, utilsAlias, contentCache);
201
200
  if (status === 'changed') {
202
- hasChanges = true;
203
201
  peerFilesToUpdate.add(file);
204
202
  }
205
203
  }
206
- return hasChanges;
207
204
  }
208
- async function classifyComponent(name, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate) {
205
+ export async function classifyComponent(name, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate) {
209
206
  const component = registry[name];
210
- let hasChanges = false;
207
+ let ownFilesChanged = false;
211
208
  let isFullyPresent = true;
212
209
  for (const file of component.files) {
213
210
  const status = await checkFileConflict(file, targetDir, options, utilsAlias, contentCache);
214
211
  if (status === 'missing')
215
212
  isFullyPresent = false;
216
213
  if (status === 'changed')
217
- hasChanges = true;
214
+ ownFilesChanged = true;
218
215
  }
219
- const peerChanged = await checkPeerFiles(component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate);
220
- if (peerChanged)
221
- hasChanges = true;
222
- if (isFullyPresent && !hasChanges)
216
+ await checkPeerFiles(component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate);
217
+ if (isFullyPresent && !ownFilesChanged)
223
218
  return 'skip';
224
- if (hasChanges)
219
+ if (ownFilesChanged)
225
220
  return 'conflict';
226
221
  return 'install';
227
222
  }
228
- async function detectConflicts(allComponents, targetDir, options, utilsAlias) {
223
+ export async function detectConflicts(allComponents, targetDir, options, utilsAlias) {
229
224
  const toInstall = [];
230
225
  const toSkip = [];
231
226
  const conflicting = [];
@@ -399,9 +394,28 @@ export async function add(components, options) {
399
394
  // Prompt user for overwrite decisions
400
395
  const toOverwrite = await promptOverwrite(conflicting, options);
401
396
  const finalComponents = [...toInstall, ...toOverwrite];
397
+ // Remove peer files that belong only to declined components
398
+ const declined = conflicting.filter(c => !toOverwrite.includes(c));
399
+ for (const name of declined) {
400
+ const component = registry[name];
401
+ if (!component.peerFiles)
402
+ continue;
403
+ for (const file of component.peerFiles) {
404
+ const stillNeeded = finalComponents.some(fc => registry[fc].peerFiles?.includes(file));
405
+ if (!stillNeeded) {
406
+ peerFilesToUpdate.delete(file);
407
+ }
408
+ }
409
+ }
402
410
  if (finalComponents.length === 0) {
403
- if (toSkip.length > 0) {
404
- console.log(chalk.green(`\nAll components are up to date! (${toSkip.length} skipped)`));
411
+ if (toSkip.length > 0 || declined.length > 0) {
412
+ if (toSkip.length > 0) {
413
+ console.log(chalk.green(`\nAll components are up to date! (${toSkip.length} skipped)`));
414
+ }
415
+ if (declined.length > 0) {
416
+ console.log('\n' + chalk.dim('Components skipped (kept local changes):'));
417
+ declined.forEach(name => console.log(chalk.dim(' - ') + chalk.yellow(name)));
418
+ }
405
419
  }
406
420
  else {
407
421
  console.log(chalk.dim('\nNo components to install.'));
@@ -411,6 +425,7 @@ export async function add(components, options) {
411
425
  // Install component files
412
426
  const spinner = ora('Installing components...').start();
413
427
  let successCount = 0;
428
+ const finalComponentSet = new Set(finalComponents);
414
429
  try {
415
430
  await fs.ensureDir(targetDir);
416
431
  for (const name of finalComponents) {
@@ -432,13 +447,17 @@ export async function add(components, options) {
432
447
  }
433
448
  // Post-install: lib files, npm deps, shortcuts
434
449
  const libDir = resolveProjectPath(cwd, aliasToProjectPath(utilsAlias));
435
- await installLibFiles(allComponents, cwd, libDir, options);
450
+ await installLibFiles(finalComponentSet, cwd, libDir, options);
436
451
  await installNpmDependencies(finalComponents, cwd);
437
452
  await ensureShortcutService(targetDir, cwd, config, options);
438
453
  if (toSkip.length > 0) {
439
454
  console.log('\n' + chalk.dim('Components skipped (up to date):'));
440
455
  toSkip.forEach(name => console.log(chalk.dim(' - ') + chalk.gray(name)));
441
456
  }
457
+ if (declined.length > 0) {
458
+ console.log('\n' + chalk.dim('Components skipped (kept local changes):'));
459
+ declined.forEach(name => console.log(chalk.dim(' - ') + chalk.yellow(name)));
460
+ }
442
461
  console.log('');
443
462
  }
444
463
  catch (error) {
@@ -1,6 +1,305 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { resolveDependencies, promptOptionalDependencies } from './add.js';
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { resolveDependencies, promptOptionalDependencies, normalizeContent, fetchAndTransform, checkFileConflict, classifyComponent, detectConflicts, } from './add.js';
3
3
  import { registry } from '../registry/index.js';
4
+ import fs from 'fs-extra';
5
+ // ---------------------------------------------------------------------------
6
+ // Module-level mocks
7
+ // ---------------------------------------------------------------------------
8
+ vi.mock('fs-extra', () => ({
9
+ default: {
10
+ existsSync: vi.fn(() => false),
11
+ pathExists: vi.fn(() => Promise.resolve(false)),
12
+ readFile: vi.fn(() => Promise.resolve('')),
13
+ writeFile: vi.fn(() => Promise.resolve()),
14
+ ensureDir: vi.fn(() => Promise.resolve()),
15
+ },
16
+ }));
17
+ vi.mock('prompts', () => ({
18
+ default: vi.fn(),
19
+ }));
20
+ const mockedFs = vi.mocked(fs);
21
+ // ---------------------------------------------------------------------------
22
+ // normalizeContent
23
+ // ---------------------------------------------------------------------------
24
+ describe('normalizeContent', () => {
25
+ it('trims whitespace from both ends', () => {
26
+ expect(normalizeContent(' hello ')).toBe('hello');
27
+ });
28
+ it('converts CRLF to LF', () => {
29
+ expect(normalizeContent('line1\r\nline2\r\n')).toBe('line1\nline2');
30
+ });
31
+ it('handles mixed line endings', () => {
32
+ expect(normalizeContent('a\r\nb\nc\r\n')).toBe('a\nb\nc');
33
+ });
34
+ it('returns empty string for whitespace-only input', () => {
35
+ expect(normalizeContent(' \r\n ')).toBe('');
36
+ });
37
+ });
38
+ // ---------------------------------------------------------------------------
39
+ // fetchAndTransform
40
+ // ---------------------------------------------------------------------------
41
+ describe('fetchAndTransform', () => {
42
+ beforeEach(() => {
43
+ vi.restoreAllMocks();
44
+ mockedFs.existsSync.mockReturnValue(false);
45
+ });
46
+ it('replaces single-level relative lib imports with alias', async () => {
47
+ const mockContent = "import { cn } from '../lib/utils';";
48
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(mockContent, { status: 200 }));
49
+ const result = await fetchAndTransform('button/button.ts', { branch: 'master', remote: true }, '@/components/lib');
50
+ expect(result).toBe("import { cn } from '@/components/lib/utils';");
51
+ });
52
+ it('replaces multi-level relative lib imports with alias', async () => {
53
+ const mockContent = "import { cn } from '../../lib/utils';";
54
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(mockContent, { status: 200 }));
55
+ const result = await fetchAndTransform('deep/nested/comp.ts', { branch: 'master', remote: true }, '@/components/lib');
56
+ expect(result).toBe("import { cn } from '@/components/lib/utils';");
57
+ });
58
+ it('replaces all occurrences in a file (global regex)', async () => {
59
+ const mockContent = [
60
+ "import { cn } from '../lib/utils';",
61
+ "import { foo } from '../lib/helpers';",
62
+ ].join('\n');
63
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(mockContent, { status: 200 }));
64
+ const result = await fetchAndTransform('comp.ts', { branch: 'master', remote: true }, '@/lib');
65
+ expect(result).toBe([
66
+ "import { cn } from '@/lib/utils';",
67
+ "import { foo } from '@/lib/helpers';",
68
+ ].join('\n'));
69
+ });
70
+ it('does not throw with replaceAll and regex (the original bug)', async () => {
71
+ const mockContent = "import { cn } from '../lib/utils';";
72
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(mockContent, { status: 200 }));
73
+ await expect(fetchAndTransform('comp.ts', { branch: 'master', remote: true }, '@/lib')).resolves.not.toThrow();
74
+ });
75
+ it('leaves content unchanged when no lib imports exist', async () => {
76
+ const mockContent = "import { Component } from '@angular/core';";
77
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(mockContent, { status: 200 }));
78
+ const result = await fetchAndTransform('comp.ts', { branch: 'master', remote: true }, '@/lib');
79
+ expect(result).toBe("import { Component } from '@angular/core';");
80
+ });
81
+ });
82
+ // ---------------------------------------------------------------------------
83
+ // checkFileConflict
84
+ // ---------------------------------------------------------------------------
85
+ describe('checkFileConflict', () => {
86
+ beforeEach(() => {
87
+ vi.restoreAllMocks();
88
+ mockedFs.existsSync.mockReturnValue(false);
89
+ });
90
+ it('returns "missing" when file does not exist on disk', async () => {
91
+ mockedFs.pathExists.mockResolvedValue(false);
92
+ const cache = new Map();
93
+ const result = await checkFileConflict('button/button.ts', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache);
94
+ expect(result).toBe('missing');
95
+ });
96
+ it('returns "identical" when local and remote content match', async () => {
97
+ const content = "import { cn } from '@/lib/utils';\nexport class Button {}";
98
+ mockedFs.pathExists.mockResolvedValue(true);
99
+ mockedFs.readFile.mockResolvedValue(content);
100
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(content.replaceAll('@/lib/', '../lib/'), { status: 200 }));
101
+ const cache = new Map();
102
+ const result = await checkFileConflict('button/button.ts', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache);
103
+ expect(result).toBe('identical');
104
+ });
105
+ it('returns "changed" when local and remote content differ', async () => {
106
+ mockedFs.pathExists.mockResolvedValue(true);
107
+ mockedFs.readFile.mockResolvedValue('// local modified version');
108
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('// remote original version', { status: 200 }));
109
+ const cache = new Map();
110
+ const result = await checkFileConflict('button/button.ts', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache);
111
+ expect(result).toBe('changed');
112
+ });
113
+ it('populates the content cache on successful fetch', async () => {
114
+ mockedFs.pathExists.mockResolvedValue(true);
115
+ mockedFs.readFile.mockResolvedValue('// local');
116
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('// remote', { status: 200 }));
117
+ const cache = new Map();
118
+ await checkFileConflict('button/button.ts', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache);
119
+ expect(cache.has('button/button.ts')).toBe(true);
120
+ expect(cache.get('button/button.ts')).toBe('// remote');
121
+ });
122
+ });
123
+ // ---------------------------------------------------------------------------
124
+ // classifyComponent — peer-file decoupling
125
+ // ---------------------------------------------------------------------------
126
+ describe('classifyComponent', () => {
127
+ beforeEach(() => {
128
+ vi.restoreAllMocks();
129
+ mockedFs.existsSync.mockReturnValue(false);
130
+ });
131
+ it('returns "install" when component files are missing', async () => {
132
+ mockedFs.pathExists.mockResolvedValue(false);
133
+ const cache = new Map();
134
+ const peerFiles = new Set();
135
+ const result = await classifyComponent('badge', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache, peerFiles);
136
+ expect(result).toBe('install');
137
+ });
138
+ it('returns "skip" when all component files are present and identical', async () => {
139
+ const content = '// badge content';
140
+ mockedFs.pathExists.mockResolvedValue(true);
141
+ mockedFs.readFile.mockResolvedValue(content);
142
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(content, { status: 200 }));
143
+ const cache = new Map();
144
+ const peerFiles = new Set();
145
+ const result = await classifyComponent('badge', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache, peerFiles);
146
+ expect(result).toBe('skip');
147
+ });
148
+ it('returns "conflict" when own files have changed', async () => {
149
+ mockedFs.pathExists.mockResolvedValue(true);
150
+ mockedFs.readFile.mockResolvedValue('// modified locally');
151
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('// original remote', { status: 200 }));
152
+ const cache = new Map();
153
+ const peerFiles = new Set();
154
+ const result = await classifyComponent('badge', '/project/ui', { branch: 'master', remote: true }, '@/lib', cache, peerFiles);
155
+ expect(result).toBe('conflict');
156
+ });
157
+ it('does NOT return "conflict" when only peer files have changed', async () => {
158
+ const componentWithPeers = Object.entries(registry).find(([, def]) => def.peerFiles && def.peerFiles.length > 0);
159
+ if (!componentWithPeers) {
160
+ return;
161
+ }
162
+ const [name, def] = componentWithPeers;
163
+ let readCallCount = 0;
164
+ mockedFs.pathExists.mockResolvedValue(true);
165
+ mockedFs.readFile.mockImplementation((() => {
166
+ readCallCount++;
167
+ if (readCallCount <= def.files.length) {
168
+ return Promise.resolve('// identical content');
169
+ }
170
+ return Promise.resolve('// modified peer file');
171
+ }));
172
+ vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve(new Response('// identical content', { status: 200 })));
173
+ const cache = new Map();
174
+ const peerFilesToUpdate = new Set();
175
+ const result = await classifyComponent(name, '/project/ui', { branch: 'master', remote: true }, '@/lib', cache, peerFilesToUpdate);
176
+ expect(result).toBe('skip');
177
+ expect(peerFilesToUpdate.size).toBeGreaterThan(0);
178
+ });
179
+ });
180
+ // ---------------------------------------------------------------------------
181
+ // detectConflicts
182
+ // ---------------------------------------------------------------------------
183
+ describe('detectConflicts', () => {
184
+ beforeEach(() => {
185
+ vi.restoreAllMocks();
186
+ mockedFs.existsSync.mockReturnValue(false);
187
+ });
188
+ it('classifies missing components as toInstall', async () => {
189
+ mockedFs.pathExists.mockResolvedValue(false);
190
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('// content', { status: 200 }));
191
+ const components = new Set(['badge']);
192
+ const result = await detectConflicts(components, '/project/ui', { branch: 'master', remote: true }, '@/lib');
193
+ expect(result.toInstall).toContain('badge');
194
+ expect(result.conflicting).not.toContain('badge');
195
+ expect(result.toSkip).not.toContain('badge');
196
+ });
197
+ it('classifies identical components as toSkip', async () => {
198
+ const content = '// badge content';
199
+ mockedFs.pathExists.mockResolvedValue(true);
200
+ mockedFs.readFile.mockResolvedValue(content);
201
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(content, { status: 200 }));
202
+ const components = new Set(['badge']);
203
+ const result = await detectConflicts(components, '/project/ui', { branch: 'master', remote: true }, '@/lib');
204
+ expect(result.toSkip).toContain('badge');
205
+ expect(result.toInstall).not.toContain('badge');
206
+ expect(result.conflicting).not.toContain('badge');
207
+ });
208
+ it('classifies changed components as conflicting', async () => {
209
+ mockedFs.pathExists.mockResolvedValue(true);
210
+ mockedFs.readFile.mockResolvedValue('// modified locally');
211
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('// remote content', { status: 200 }));
212
+ const components = new Set(['badge']);
213
+ const result = await detectConflicts(components, '/project/ui', { branch: 'master', remote: true }, '@/lib');
214
+ expect(result.conflicting).toContain('badge');
215
+ expect(result.toInstall).not.toContain('badge');
216
+ expect(result.toSkip).not.toContain('badge');
217
+ });
218
+ it('populates contentCache for files it checks', async () => {
219
+ mockedFs.pathExists.mockResolvedValue(true);
220
+ mockedFs.readFile.mockResolvedValue('// local');
221
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('// remote', { status: 200 }));
222
+ const components = new Set(['badge']);
223
+ const result = await detectConflicts(components, '/project/ui', { branch: 'master', remote: true }, '@/lib');
224
+ expect(result.contentCache.size).toBeGreaterThan(0);
225
+ });
226
+ });
227
+ // ---------------------------------------------------------------------------
228
+ // Peer file filtering after overwrite selection
229
+ // ---------------------------------------------------------------------------
230
+ describe('peer file filtering logic', () => {
231
+ it('removes peer files of declined components from peerFilesToUpdate', () => {
232
+ const peerFilesToUpdate = new Set(['shared/peer-a.ts', 'shared/peer-b.ts', 'shared/peer-c.ts']);
233
+ const conflicting = ['compA', 'compB'];
234
+ const toOverwrite = ['compA'];
235
+ const finalComponents = ['compA'];
236
+ const mockPeerFiles = {
237
+ compA: ['shared/peer-a.ts'],
238
+ compB: ['shared/peer-b.ts', 'shared/peer-c.ts'],
239
+ };
240
+ const declined = conflicting.filter(c => !toOverwrite.includes(c));
241
+ for (const name of declined) {
242
+ const peerFiles = mockPeerFiles[name];
243
+ if (!peerFiles)
244
+ continue;
245
+ for (const file of peerFiles) {
246
+ const stillNeeded = finalComponents.some(fc => mockPeerFiles[fc]?.includes(file));
247
+ if (!stillNeeded) {
248
+ peerFilesToUpdate.delete(file);
249
+ }
250
+ }
251
+ }
252
+ expect(peerFilesToUpdate.has('shared/peer-a.ts')).toBe(true);
253
+ expect(peerFilesToUpdate.has('shared/peer-b.ts')).toBe(false);
254
+ expect(peerFilesToUpdate.has('shared/peer-c.ts')).toBe(false);
255
+ });
256
+ it('keeps shared peer files when another final component still needs them', () => {
257
+ const peerFilesToUpdate = new Set(['shared/common.ts']);
258
+ const conflicting = ['compA', 'compB'];
259
+ const toOverwrite = ['compA'];
260
+ const finalComponents = ['compA'];
261
+ const mockPeerFiles = {
262
+ compA: ['shared/common.ts'],
263
+ compB: ['shared/common.ts'],
264
+ };
265
+ const declined = conflicting.filter(c => !toOverwrite.includes(c));
266
+ for (const name of declined) {
267
+ const peerFiles = mockPeerFiles[name];
268
+ if (!peerFiles)
269
+ continue;
270
+ for (const file of peerFiles) {
271
+ const stillNeeded = finalComponents.some(fc => mockPeerFiles[fc]?.includes(file));
272
+ if (!stillNeeded) {
273
+ peerFilesToUpdate.delete(file);
274
+ }
275
+ }
276
+ }
277
+ expect(peerFilesToUpdate.has('shared/common.ts')).toBe(true);
278
+ });
279
+ it('removes all peer files when all components are declined', () => {
280
+ const peerFilesToUpdate = new Set(['shared/peer-a.ts', 'shared/peer-b.ts']);
281
+ const conflicting = ['compA', 'compB'];
282
+ const toOverwrite = [];
283
+ const finalComponents = [];
284
+ const mockPeerFiles = {
285
+ compA: ['shared/peer-a.ts'],
286
+ compB: ['shared/peer-b.ts'],
287
+ };
288
+ const declined = conflicting.filter(c => !toOverwrite.includes(c));
289
+ for (const name of declined) {
290
+ const peerFiles = mockPeerFiles[name];
291
+ if (!peerFiles)
292
+ continue;
293
+ for (const file of peerFiles) {
294
+ const stillNeeded = finalComponents.some(fc => mockPeerFiles[fc]?.includes(file));
295
+ if (!stillNeeded) {
296
+ peerFilesToUpdate.delete(file);
297
+ }
298
+ }
299
+ }
300
+ expect(peerFilesToUpdate.size).toBe(0);
301
+ });
302
+ });
4
303
  // ---------------------------------------------------------------------------
5
304
  // resolveDependencies
6
305
  // ---------------------------------------------------------------------------
@@ -11,13 +310,11 @@ describe('resolveDependencies', () => {
11
310
  expect(result.size).toBe(1);
12
311
  });
13
312
  it('includes transitive dependencies', () => {
14
- // button depends on ripple
15
313
  const result = resolveDependencies(['button']);
16
314
  expect(result).toContain('button');
17
315
  expect(result).toContain('ripple');
18
316
  });
19
317
  it('resolves deep transitive chains', () => {
20
- // date-picker -> calendar -> button -> ripple, calendar -> select
21
318
  const result = resolveDependencies(['date-picker']);
22
319
  expect(result).toContain('date-picker');
23
320
  expect(result).toContain('calendar');
@@ -26,13 +323,11 @@ describe('resolveDependencies', () => {
26
323
  expect(result).toContain('select');
27
324
  });
28
325
  it('deduplicates shared dependencies across multiple inputs', () => {
29
- // Both button-group and speed-dial depend on button
30
326
  const result = resolveDependencies(['button-group', 'speed-dial']);
31
327
  expect(result).toContain('button-group');
32
328
  expect(result).toContain('speed-dial');
33
329
  expect(result).toContain('button');
34
330
  expect(result).toContain('ripple');
35
- // Count how many times button appears — should be exactly once (it's a Set)
36
331
  const asArray = [...result];
37
332
  expect(asArray.filter((n) => n === 'button')).toHaveLength(1);
38
333
  });
@@ -49,9 +344,6 @@ describe('resolveDependencies', () => {
49
344
  // ---------------------------------------------------------------------------
50
345
  // promptOptionalDependencies
51
346
  // ---------------------------------------------------------------------------
52
- vi.mock('prompts', () => ({
53
- default: vi.fn(),
54
- }));
55
347
  describe('promptOptionalDependencies', () => {
56
348
  it('returns empty array when no components have optional deps', async () => {
57
349
  const resolved = new Set(['badge', 'button', 'ripple']);
@@ -69,13 +361,11 @@ describe('promptOptionalDependencies', () => {
69
361
  expect(result).toContain('context-menu');
70
362
  });
71
363
  it('filters out optional deps already in the resolved set', async () => {
72
- // context-menu is already resolved, so it should not be offered
73
364
  const resolved = new Set(['data-table', 'context-menu', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
74
365
  const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
75
366
  expect(result).not.toContain('context-menu');
76
367
  });
77
368
  it('deduplicates optional deps across components', async () => {
78
- // Both data-table and tree have context-menu as optional
79
369
  const resolved = new Set(['data-table', 'tree', 'table', 'input', 'button', 'ripple', 'checkbox', 'select', 'pagination', 'popover', 'component-outlet', 'icon']);
80
370
  const result = await promptOptionalDependencies(resolved, { all: true, branch: 'master' });
81
371
  const contextMenuCount = result.filter((n) => n === 'context-menu').length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gilav21/shadcn-angular",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "CLI for adding shadcn-angular components to your project",
5
5
  "bin": {
6
6
  "shadcn-angular": "./dist/index.js"