@gilav21/shadcn-angular 0.0.25 → 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.
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +169 -114
- package/dist/commands/add.spec.js +17 -23
- package/dist/commands/diff.d.ts +8 -0
- package/dist/commands/diff.js +99 -0
- package/dist/commands/help.js +15 -6
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +171 -185
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +50 -0
- package/dist/index.js +21 -1
- package/dist/registry/index.d.ts +122 -12
- package/dist/registry/index.js +56 -168
- package/dist/utils/config.d.ts +1 -1
- package/dist/utils/config.js +22 -2
- package/dist/utils/paths.d.ts +7 -0
- package/dist/utils/paths.js +43 -0
- package/dist/utils/shortcut-registry.js +1 -13
- package/package.json +1 -1
- package/scripts/sync-registry.ts +347 -0
- package/src/commands/add.spec.ts +22 -32
- package/src/commands/add.ts +211 -137
- package/src/commands/diff.ts +133 -0
- package/src/commands/help.ts +15 -6
- package/src/commands/init.ts +329 -314
- package/src/commands/list.ts +66 -0
- package/src/index.ts +24 -1
- package/src/registry/index.ts +71 -180
- package/src/utils/config.ts +22 -3
- package/src/utils/paths.ts +52 -0
- package/src/utils/shortcut-registry.ts +1 -15
- package/vitest.config.ts +7 -0
package/dist/commands/add.d.ts
CHANGED
package/dist/commands/add.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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:
|
|
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
|
|
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: ' +
|
|
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
|
|
100
|
+
for (const dep of registry[name].dependencies ?? []) {
|
|
101
|
+
walk(dep);
|
|
102
|
+
}
|
|
139
103
|
};
|
|
140
|
-
names
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
console.log(chalk.
|
|
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
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
512
|
+
await installLibFiles(new Set(finalComponents), cwd, libDir, options);
|
|
451
513
|
await installNpmDependencies(finalComponents, cwd);
|
|
452
514
|
await ensureShortcutService(targetDir, cwd, config, options);
|
|
453
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(['
|
|
327
|
-
expect(result).toContain('
|
|
328
|
-
expect(result).toContain('
|
|
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(
|
|
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
|
|
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 {};
|