@gilav21/shadcn-angular 0.0.24 → 0.0.26

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.
@@ -5,7 +5,9 @@ export interface AddOptions {
5
5
  all?: boolean;
6
6
  path?: string;
7
7
  remote?: boolean;
8
+ dryRun?: boolean;
8
9
  branch: string;
10
+ registry?: string;
9
11
  }
10
12
  interface ConflictCheckResult {
11
13
  toInstall: ComponentName[];
@@ -1,55 +1,17 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
3
  import prompts from 'prompts';
5
4
  import chalk from 'chalk';
6
5
  import ora from 'ora';
7
6
  import { getConfig } from '../utils/config.js';
8
- import { registry } from '../registry/index.js';
7
+ import { registry, getComponentNames } from '../registry/index.js';
9
8
  import { installPackages } from '../utils/package-manager.js';
10
9
  import { writeShortcutRegistryIndex } from '../utils/shortcut-registry.js';
11
- const __filename = fileURLToPath(import.meta.url);
12
- const __dirname = path.dirname(__filename);
13
- // ---------------------------------------------------------------------------
14
- // Path & URL helpers
15
- // ---------------------------------------------------------------------------
16
- function validateBranch(branch) {
17
- if (!/^[\w.\-/]+$/.test(branch)) {
18
- throw new Error(`Invalid branch name: ${branch}`);
19
- }
20
- }
21
- function getRegistryBaseUrl(branch) {
22
- validateBranch(branch);
23
- return `https://raw.githubusercontent.com/gilav21/shadcn-angular/${branch}/packages/components/ui`;
24
- }
25
- function getLibRegistryBaseUrl(branch) {
26
- validateBranch(branch);
27
- return `https://raw.githubusercontent.com/gilav21/shadcn-angular/${branch}/packages/components/lib`;
28
- }
29
- function getLocalComponentsDir() {
30
- const localPath = path.resolve(__dirname, '../../../components/ui');
31
- return fs.existsSync(localPath) ? localPath : null;
32
- }
33
- function getLocalLibDir() {
34
- const fromDist = path.resolve(__dirname, '../../../components/lib');
35
- if (fs.existsSync(fromDist)) {
36
- return fromDist;
37
- }
38
- return null;
39
- }
40
- function resolveProjectPath(cwd, inputPath) {
41
- const resolved = path.resolve(cwd, inputPath);
42
- const relative = path.relative(cwd, resolved);
43
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
44
- throw new Error(`Path must stay inside the project directory: ${inputPath}`);
45
- }
46
- return resolved;
47
- }
48
- function aliasToProjectPath(aliasOrPath) {
49
- return aliasOrPath.startsWith('@/')
50
- ? path.join('src', aliasOrPath.slice(2))
51
- : aliasOrPath;
52
- }
10
+ import { getRegistryBaseUrl, getLibRegistryBaseUrl, getLocalComponentsDir, getLocalLibDir, resolveProjectPath, aliasToProjectPath, } from '../utils/paths.js';
11
+ const onCancel = () => {
12
+ console.log(chalk.dim('\nCancelled.'));
13
+ process.exit(0);
14
+ };
53
15
  export function normalizeContent(str) {
54
16
  return str.replaceAll('\r\n', '\n').trim();
55
17
  }
@@ -64,7 +26,7 @@ async function fetchComponentContent(file, options) {
64
26
  return fs.readFile(localPath, 'utf-8');
65
27
  }
66
28
  }
67
- const url = `${getRegistryBaseUrl(options.branch)}/${file}`;
29
+ const url = `${getRegistryBaseUrl(options.branch, options.registry)}/${file}`;
68
30
  try {
69
31
  const response = await fetch(url);
70
32
  if (!response.ok) {
@@ -87,7 +49,7 @@ async function fetchLibContent(file, options) {
87
49
  return fs.readFile(localPath, 'utf-8');
88
50
  }
89
51
  }
90
- const url = `${getLibRegistryBaseUrl(options.branch)}/${file}`;
52
+ const url = `${getLibRegistryBaseUrl(options.branch, options.registry)}/${file}`;
91
53
  const response = await fetch(url);
92
54
  if (!response.ok) {
93
55
  throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
@@ -104,28 +66,28 @@ export async function fetchAndTransform(file, options, utilsAlias) {
104
66
  // ---------------------------------------------------------------------------
105
67
  async function selectComponents(components, options) {
106
68
  if (options.all) {
107
- return Object.keys(registry);
69
+ return getComponentNames();
108
70
  }
109
71
  if (components.length === 0) {
110
72
  const { selected } = await prompts({
111
73
  type: 'multiselect',
112
74
  name: 'selected',
113
75
  message: 'Which components would you like to add?',
114
- choices: Object.keys(registry).map(name => ({
76
+ choices: getComponentNames().map(name => ({
115
77
  title: name,
116
78
  value: name,
117
79
  })),
118
80
  hint: '- Space to select, Enter to confirm',
119
- });
81
+ }, { onCancel });
120
82
  return selected;
121
83
  }
122
84
  return components;
123
85
  }
124
86
  function validateComponents(names) {
125
- const invalid = names.filter(c => !registry[c]);
87
+ const invalid = names.filter(c => !(c in registry));
126
88
  if (invalid.length > 0) {
127
89
  console.log(chalk.red(`Invalid component(s): ${invalid.join(', ')}`));
128
- console.log(chalk.dim('Available components: ' + Object.keys(registry).join(', ')));
90
+ console.log(chalk.dim('Available components: ' + getComponentNames().join(', ')));
129
91
  process.exit(1);
130
92
  }
131
93
  }
@@ -135,9 +97,12 @@ export function resolveDependencies(names) {
135
97
  if (all.has(name))
136
98
  return;
137
99
  all.add(name);
138
- registry[name].dependencies?.forEach(dep => walk(dep));
100
+ for (const dep of registry[name].dependencies ?? []) {
101
+ walk(dep);
102
+ }
139
103
  };
140
- names.forEach(walk);
104
+ for (const name of names)
105
+ walk(name);
141
106
  return all;
142
107
  }
143
108
  export async function promptOptionalDependencies(resolved, options) {
@@ -169,7 +134,7 @@ export async function promptOptionalDependencies(resolved, options) {
169
134
  value: c.name,
170
135
  })),
171
136
  hint: '- Space to select, Enter to confirm (or press Enter to skip)',
172
- });
137
+ }, { onCancel });
173
138
  return selected || [];
174
139
  }
175
140
  // ---------------------------------------------------------------------------
@@ -214,20 +179,48 @@ export async function classifyComponent(name, targetDir, options, utilsAlias, co
214
179
  ownFilesChanged = true;
215
180
  }
216
181
  await checkPeerFiles(component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate);
182
+ if (options.overwrite)
183
+ return isFullyPresent ? 'conflict' : 'install';
217
184
  if (isFullyPresent && !ownFilesChanged)
218
185
  return 'skip';
219
186
  if (ownFilesChanged)
220
187
  return 'conflict';
221
188
  return 'install';
222
189
  }
190
+ class ConcurrencyLimiter {
191
+ concurrency;
192
+ active = 0;
193
+ queue = [];
194
+ constructor(concurrency) {
195
+ this.concurrency = concurrency;
196
+ }
197
+ async run(fn) {
198
+ if (this.active >= this.concurrency) {
199
+ await new Promise(resolve => this.queue.push(resolve));
200
+ }
201
+ this.active++;
202
+ try {
203
+ return await fn();
204
+ }
205
+ finally {
206
+ this.active--;
207
+ if (this.queue.length > 0)
208
+ this.queue.shift()();
209
+ }
210
+ }
211
+ }
223
212
  export async function detectConflicts(allComponents, targetDir, options, utilsAlias) {
224
213
  const toInstall = [];
225
214
  const toSkip = [];
226
215
  const conflicting = [];
227
216
  const peerFilesToUpdate = new Set();
228
217
  const contentCache = new Map();
229
- for (const name of allComponents) {
230
- const result = await classifyComponent(name, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate);
218
+ const limiter = new ConcurrencyLimiter(8);
219
+ const results = await Promise.all([...allComponents].map(name => limiter.run(async () => ({
220
+ name,
221
+ result: await classifyComponent(name, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate),
222
+ }))));
223
+ for (const { name, result } of results) {
231
224
  if (result === 'skip')
232
225
  toSkip.push(name);
233
226
  else if (result === 'conflict')
@@ -240,21 +233,42 @@ export async function detectConflicts(allComponents, targetDir, options, utilsAl
240
233
  // ---------------------------------------------------------------------------
241
234
  // Overwrite prompt
242
235
  // ---------------------------------------------------------------------------
243
- async function promptOverwrite(conflicting, options) {
236
+ function showConflictDiffs(conflicting, targetDir, contentCache) {
237
+ for (const name of conflicting) {
238
+ const component = registry[name];
239
+ const changedFiles = [];
240
+ for (const file of component.files) {
241
+ const remote = contentCache.get(file);
242
+ if (!remote)
243
+ continue;
244
+ const localPath = path.join(targetDir, file);
245
+ if (!fs.existsSync(localPath))
246
+ continue;
247
+ const local = normalizeContent(fs.readFileSync(localPath, 'utf-8'));
248
+ if (local !== normalizeContent(remote)) {
249
+ changedFiles.push(file);
250
+ }
251
+ }
252
+ if (changedFiles.length > 0) {
253
+ console.log(chalk.dim(` ${name}: `) + chalk.yellow(changedFiles.join(', ')));
254
+ }
255
+ }
256
+ }
257
+ async function promptOverwrite(conflicting, options, targetDir, contentCache) {
244
258
  if (conflicting.length === 0)
245
259
  return [];
246
- if (options.overwrite)
260
+ if (options.overwrite || options.yes)
247
261
  return conflicting;
248
- if (options.yes)
249
- return [];
250
- console.log(chalk.yellow(`\n${conflicting.length} component(s) have local changes or are different from remote.`));
262
+ console.log(chalk.yellow(`\n${conflicting.length} component(s) have local changes or are different from remote:`));
263
+ showConflictDiffs(conflicting, targetDir, contentCache);
264
+ console.log(chalk.dim('\n Use `npx shadcn-angular diff <component>` for full diffs.\n'));
251
265
  const { selected } = await prompts({
252
266
  type: 'multiselect',
253
267
  name: 'selected',
254
268
  message: 'Select components to OVERWRITE (Unselected will be skipped):',
255
269
  choices: conflicting.map(name => ({ title: name, value: name })),
256
270
  hint: '- Space to select, Enter to confirm',
257
- });
271
+ }, { onCancel });
258
272
  return selected || [];
259
273
  }
260
274
  // ---------------------------------------------------------------------------
@@ -297,32 +311,42 @@ async function writePeerFiles(component, targetDir, options, utilsAlias, content
297
311
  }
298
312
  }
299
313
  }
314
+ async function installSingleLibFile(libFile, libDir, options) {
315
+ const targetPath = path.join(libDir, libFile);
316
+ const content = await fetchLibContent(libFile, options);
317
+ if (!await fs.pathExists(targetPath) || options.overwrite) {
318
+ await fs.writeFile(targetPath, content);
319
+ return;
320
+ }
321
+ const local = normalizeContent(await fs.readFile(targetPath, 'utf-8'));
322
+ if (local !== normalizeContent(content)) {
323
+ console.log(chalk.yellow(` Lib file ${libFile} differs from remote (use --overwrite to update)`));
324
+ }
325
+ }
300
326
  async function installLibFiles(allComponents, cwd, libDir, options) {
301
327
  const required = new Set();
302
328
  for (const name of allComponents) {
303
- registry[name].libFiles?.forEach(f => required.add(f));
329
+ for (const f of registry[name].libFiles ?? [])
330
+ required.add(f);
304
331
  }
305
332
  if (required.size === 0)
306
333
  return;
307
334
  await fs.ensureDir(libDir);
308
335
  for (const libFile of required) {
309
- const targetPath = path.join(libDir, libFile);
310
- if (!await fs.pathExists(targetPath) || options.overwrite) {
311
- try {
312
- const content = await fetchLibContent(libFile, options);
313
- await fs.writeFile(targetPath, content);
314
- }
315
- catch (err) {
316
- const message = err instanceof Error ? err.message : String(err);
317
- console.warn(chalk.yellow(`Could not install lib file ${libFile}: ${message}`));
318
- }
336
+ try {
337
+ await installSingleLibFile(libFile, libDir, options);
338
+ }
339
+ catch (err) {
340
+ const message = err instanceof Error ? err.message : String(err);
341
+ console.warn(chalk.yellow(`Could not install lib file ${libFile}: ${message}`));
319
342
  }
320
343
  }
321
344
  }
322
345
  async function installNpmDependencies(finalComponents, cwd) {
323
346
  const deps = new Set();
324
347
  for (const name of finalComponents) {
325
- registry[name].npmDependencies?.forEach(dep => deps.add(dep));
348
+ for (const dep of registry[name].npmDependencies ?? [])
349
+ deps.add(dep);
326
350
  }
327
351
  if (deps.size === 0)
328
352
  return;
@@ -333,7 +357,12 @@ async function installNpmDependencies(finalComponents, cwd) {
333
357
  }
334
358
  catch (e) {
335
359
  spinner.fail('Failed to install dependencies.');
336
- console.error(e);
360
+ if (e && typeof e === 'object' && 'stderr' in e && typeof e.stderr === 'string') {
361
+ console.error(chalk.red(e.stderr));
362
+ }
363
+ else {
364
+ console.error(e);
365
+ }
337
366
  }
338
367
  }
339
368
  async function ensureShortcutService(targetDir, cwd, config, options) {
@@ -363,6 +392,57 @@ function collectInstalledShortcutEntries(targetDir) {
363
392
  return entries;
364
393
  }
365
394
  // ---------------------------------------------------------------------------
395
+ // Peer file & summary helpers
396
+ // ---------------------------------------------------------------------------
397
+ function pruneDeclinedPeerFiles(declined, finalComponents, peerFilesToUpdate) {
398
+ for (const name of declined) {
399
+ const component = registry[name];
400
+ if (!component.peerFiles)
401
+ continue;
402
+ for (const file of component.peerFiles) {
403
+ const stillNeeded = finalComponents.some(fc => registry[fc].peerFiles?.includes(file));
404
+ if (!stillNeeded) {
405
+ peerFilesToUpdate.delete(file);
406
+ }
407
+ }
408
+ }
409
+ }
410
+ function printNothingToInstall(toSkip, declined) {
411
+ if (toSkip.length > 0 || declined.length > 0) {
412
+ printSkipSummary(toSkip, declined);
413
+ }
414
+ else {
415
+ console.log(chalk.dim('\nNo components to install.'));
416
+ }
417
+ }
418
+ function printDryRunSummary(toInstall, toOverwrite, toSkip, declined) {
419
+ console.log(chalk.bold('\n[Dry Run] No changes will be made.\n'));
420
+ if (toInstall.length > 0) {
421
+ console.log(chalk.green(` Would install ${toInstall.length} component(s):`));
422
+ for (const name of toInstall)
423
+ console.log(chalk.dim(' + ') + chalk.cyan(name));
424
+ }
425
+ if (toOverwrite.length > 0) {
426
+ console.log(chalk.yellow(` Would overwrite ${toOverwrite.length} component(s):`));
427
+ for (const name of toOverwrite)
428
+ console.log(chalk.dim(' ~ ') + chalk.yellow(name));
429
+ }
430
+ printSkipSummary(toSkip, declined);
431
+ console.log('');
432
+ }
433
+ function printSkipSummary(toSkip, declined) {
434
+ if (toSkip.length > 0) {
435
+ console.log('\n' + chalk.dim('Components skipped (up to date):'));
436
+ for (const name of toSkip)
437
+ console.log(chalk.dim(' - ') + chalk.gray(name));
438
+ }
439
+ if (declined.length > 0) {
440
+ console.log('\n' + chalk.dim('Components skipped (kept local changes):'));
441
+ for (const name of declined)
442
+ console.log(chalk.dim(' - ') + chalk.yellow(name));
443
+ }
444
+ }
445
+ // ---------------------------------------------------------------------------
366
446
  // Main entry point
367
447
  // ---------------------------------------------------------------------------
368
448
  export async function add(components, options) {
@@ -373,6 +453,10 @@ export async function add(components, options) {
373
453
  console.log(chalk.dim('Run `npx @gilav21/shadcn-angular init` first.'));
374
454
  process.exit(1);
375
455
  }
456
+ // CLI flag takes priority over components.json
457
+ if (!options.registry && config.registry) {
458
+ options.registry = config.registry;
459
+ }
376
460
  const componentsToAdd = await selectComponents(components, options);
377
461
  if (!componentsToAdd || componentsToAdd.length === 0) {
378
462
  console.log(chalk.dim('No components selected.'));
@@ -387,45 +471,23 @@ export async function add(components, options) {
387
471
  const uiBasePath = options.path ?? aliasToProjectPath(config.aliases.ui || 'src/components/ui');
388
472
  const targetDir = resolveProjectPath(cwd, uiBasePath);
389
473
  const utilsAlias = config.aliases.utils;
390
- // Detect conflicts
391
474
  const checkSpinner = ora('Checking for conflicts...').start();
392
475
  const { toInstall, toSkip, conflicting, peerFilesToUpdate, contentCache } = await detectConflicts(allComponents, targetDir, options, utilsAlias);
393
476
  checkSpinner.stop();
394
- // Prompt user for overwrite decisions
395
- const toOverwrite = await promptOverwrite(conflicting, options);
477
+ const toOverwrite = await promptOverwrite(conflicting, options, targetDir, contentCache);
396
478
  const finalComponents = [...toInstall, ...toOverwrite];
397
- // Remove peer files that belong only to declined components
398
479
  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
- }
480
+ pruneDeclinedPeerFiles(declined, finalComponents, peerFilesToUpdate);
481
+ if (options.dryRun) {
482
+ printDryRunSummary(toInstall, toOverwrite, toSkip, declined);
483
+ return;
409
484
  }
410
485
  if (finalComponents.length === 0) {
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
- }
419
- }
420
- else {
421
- console.log(chalk.dim('\nNo components to install.'));
422
- }
486
+ printNothingToInstall(toSkip, declined);
423
487
  return;
424
488
  }
425
- // Install component files
426
489
  const spinner = ora('Installing components...').start();
427
490
  let successCount = 0;
428
- const finalComponentSet = new Set(finalComponents);
429
491
  try {
430
492
  await fs.ensureDir(targetDir);
431
493
  for (const name of finalComponents) {
@@ -440,24 +502,17 @@ export async function add(components, options) {
440
502
  if (successCount > 0) {
441
503
  spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
442
504
  console.log('\n' + chalk.dim('Components added:'));
443
- finalComponents.forEach(name => console.log(chalk.dim(' - ') + chalk.cyan(name)));
505
+ for (const name of finalComponents)
506
+ console.log(chalk.dim(' - ') + chalk.cyan(name));
444
507
  }
445
508
  else {
446
509
  spinner.info('No new components installed.');
447
510
  }
448
- // Post-install: lib files, npm deps, shortcuts
449
511
  const libDir = resolveProjectPath(cwd, aliasToProjectPath(utilsAlias));
450
- await installLibFiles(finalComponentSet, cwd, libDir, options);
512
+ await installLibFiles(new Set(finalComponents), cwd, libDir, options);
451
513
  await installNpmDependencies(finalComponents, cwd);
452
514
  await ensureShortcutService(targetDir, cwd, config, options);
453
- if (toSkip.length > 0) {
454
- console.log('\n' + chalk.dim('Components skipped (up to date):'));
455
- toSkip.forEach(name => console.log(chalk.dim(' - ') + chalk.gray(name)));
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
- }
515
+ printSkipSummary(toSkip, declined);
461
516
  console.log('');
462
517
  }
463
518
  catch (error) {
@@ -230,20 +230,20 @@ describe('detectConflicts', () => {
230
230
  describe('peer file filtering logic', () => {
231
231
  it('removes peer files of declined components from peerFilesToUpdate', () => {
232
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'];
233
+ const conflicting = new Set(['compA', 'compB']);
234
+ const toOverwrite = new Set(['compA']);
235
+ const finalComponents = new Set(['compA']);
236
236
  const mockPeerFiles = {
237
237
  compA: ['shared/peer-a.ts'],
238
238
  compB: ['shared/peer-b.ts', 'shared/peer-c.ts'],
239
239
  };
240
- const declined = conflicting.filter(c => !toOverwrite.includes(c));
240
+ const declined = [...conflicting].filter(c => !toOverwrite.has(c));
241
241
  for (const name of declined) {
242
242
  const peerFiles = mockPeerFiles[name];
243
243
  if (!peerFiles)
244
244
  continue;
245
245
  for (const file of peerFiles) {
246
- const stillNeeded = finalComponents.some(fc => mockPeerFiles[fc]?.includes(file));
246
+ const stillNeeded = [...finalComponents].some(fc => mockPeerFiles[fc]?.includes(file));
247
247
  if (!stillNeeded) {
248
248
  peerFilesToUpdate.delete(file);
249
249
  }
@@ -255,20 +255,20 @@ describe('peer file filtering logic', () => {
255
255
  });
256
256
  it('keeps shared peer files when another final component still needs them', () => {
257
257
  const peerFilesToUpdate = new Set(['shared/common.ts']);
258
- const conflicting = ['compA', 'compB'];
259
- const toOverwrite = ['compA'];
260
- const finalComponents = ['compA'];
258
+ const conflicting = new Set(['compA', 'compB']);
259
+ const toOverwrite = new Set(['compA']);
260
+ const finalComponents = new Set(['compA']);
261
261
  const mockPeerFiles = {
262
262
  compA: ['shared/common.ts'],
263
263
  compB: ['shared/common.ts'],
264
264
  };
265
- const declined = conflicting.filter(c => !toOverwrite.includes(c));
265
+ const declined = [...conflicting].filter(c => !toOverwrite.has(c));
266
266
  for (const name of declined) {
267
267
  const peerFiles = mockPeerFiles[name];
268
268
  if (!peerFiles)
269
269
  continue;
270
270
  for (const file of peerFiles) {
271
- const stillNeeded = finalComponents.some(fc => mockPeerFiles[fc]?.includes(file));
271
+ const stillNeeded = [...finalComponents].some(fc => mockPeerFiles[fc]?.includes(file));
272
272
  if (!stillNeeded) {
273
273
  peerFilesToUpdate.delete(file);
274
274
  }
@@ -278,23 +278,17 @@ describe('peer file filtering logic', () => {
278
278
  });
279
279
  it('removes all peer files when all components are declined', () => {
280
280
  const peerFilesToUpdate = new Set(['shared/peer-a.ts', 'shared/peer-b.ts']);
281
- const conflicting = ['compA', 'compB'];
282
- const toOverwrite = [];
283
- const finalComponents = [];
281
+ const declined = ['compA', 'compB'];
284
282
  const mockPeerFiles = {
285
283
  compA: ['shared/peer-a.ts'],
286
284
  compB: ['shared/peer-b.ts'],
287
285
  };
288
- const declined = conflicting.filter(c => !toOverwrite.includes(c));
289
286
  for (const name of declined) {
290
287
  const peerFiles = mockPeerFiles[name];
291
288
  if (!peerFiles)
292
289
  continue;
293
290
  for (const file of peerFiles) {
294
- const stillNeeded = finalComponents.some(fc => mockPeerFiles[fc]?.includes(file));
295
- if (!stillNeeded) {
296
- peerFilesToUpdate.delete(file);
297
- }
291
+ peerFilesToUpdate.delete(file);
298
292
  }
299
293
  }
300
294
  expect(peerFilesToUpdate.size).toBe(0);
@@ -323,9 +317,9 @@ describe('resolveDependencies', () => {
323
317
  expect(result).toContain('select');
324
318
  });
325
319
  it('deduplicates shared dependencies across multiple inputs', () => {
326
- const result = resolveDependencies(['button-group', 'speed-dial']);
327
- expect(result).toContain('button-group');
328
- expect(result).toContain('speed-dial');
320
+ const result = resolveDependencies(['date-picker', 'sparkles']);
321
+ expect(result).toContain('date-picker');
322
+ expect(result).toContain('sparkles');
329
323
  expect(result).toContain('button');
330
324
  expect(result).toContain('ripple');
331
325
  const asArray = [...result];
@@ -393,7 +387,7 @@ describe('registry optional dependencies', () => {
393
387
  if (!definition.optionalDependencies)
394
388
  continue;
395
389
  for (const opt of definition.optionalDependencies) {
396
- expect(registry[opt.name], `Optional dep "${opt.name}" in "${componentName}" is not a valid registry key`).toBeDefined();
390
+ expect(opt.name in registry, `Optional dep "${opt.name}" in "${componentName}" is not a valid registry key`).toBe(true);
397
391
  }
398
392
  }
399
393
  });
@@ -402,7 +396,7 @@ describe('registry optional dependencies', () => {
402
396
  if (!definition.dependencies)
403
397
  continue;
404
398
  for (const dep of definition.dependencies) {
405
- expect(registry[dep], `Dependency "${dep}" in "${componentName}" is not a valid registry key`).toBeDefined();
399
+ expect(dep in registry, `Dependency "${dep}" in "${componentName}" is not a valid registry key`).toBe(true);
406
400
  }
407
401
  }
408
402
  });
@@ -0,0 +1,8 @@
1
+ interface DiffOptions {
2
+ branch: string;
3
+ remote?: boolean;
4
+ registry?: string;
5
+ }
6
+ export declare function formatUnifiedDiff(fileName: string, localContent: string, remoteContent: string): string;
7
+ export declare function diff(components: string[], options: DiffOptions): Promise<void>;
8
+ export {};