@gilav21/shadcn-angular 0.0.19 → 0.0.21
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/README.md +101 -2
- package/dist/commands/add.d.ts +4 -0
- package/dist/commands/add.js +318 -223
- package/dist/commands/add.spec.d.ts +1 -0
- package/dist/commands/add.spec.js +140 -0
- package/dist/commands/help.d.ts +1 -0
- package/dist/commands/help.js +108 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +8 -7
- package/dist/index.js +7 -0
- package/dist/registry/index.d.ts +6 -0
- package/dist/registry/index.js +10 -0
- package/dist/utils/config.js +1 -1
- package/dist/utils/package-manager.js +2 -7
- package/dist/utils/shortcut-registry.js +1 -1
- package/package.json +1 -1
- package/src/commands/add.spec.ts +170 -0
- package/src/commands/add.ts +438 -241
- package/src/commands/help.ts +131 -0
- package/src/commands/init.ts +9 -7
- package/src/index.ts +8 -0
- package/src/registry/index.ts +17 -0
- package/src/utils/config.ts +1 -1
- package/src/utils/package-manager.ts +2 -5
- package/src/utils/shortcut-registry.ts +1 -1
package/src/commands/add.ts
CHANGED
|
@@ -1,42 +1,57 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import prompts from 'prompts';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
|
-
import ora from 'ora';
|
|
7
|
-
import { getConfig } from '../utils/config.js';
|
|
8
|
-
import { registry, type ComponentName } from '../registry/index.js';
|
|
6
|
+
import ora, { type Ora } from 'ora';
|
|
7
|
+
import { getConfig, type Config } from '../utils/config.js';
|
|
8
|
+
import { registry, type ComponentDefinition, type ComponentName } from '../registry/index.js';
|
|
9
9
|
import { installPackages } from '../utils/package-manager.js';
|
|
10
10
|
import { writeShortcutRegistryIndex, type ShortcutRegistryEntry } from '../utils/shortcut-registry.js';
|
|
11
11
|
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
13
|
const __dirname = path.dirname(__filename);
|
|
14
14
|
|
|
15
|
-
// Base URL for the component registry (GitHub raw content)
|
|
16
|
-
const REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/ui';
|
|
17
|
-
const LIB_REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/lib';
|
|
18
|
-
|
|
19
|
-
// Components source directory (relative to CLI dist folder) for local dev
|
|
20
|
-
function getLocalComponentsDir(): string | null {
|
|
21
|
-
// From dist/commands/add.js -> packages/components/ui
|
|
22
|
-
const fromDist = path.resolve(__dirname, '../../../components/ui');
|
|
23
|
-
if (fs.existsSync(fromDist)) {
|
|
24
|
-
return fromDist;
|
|
25
|
-
}
|
|
26
|
-
// Fallback: from src/commands/add.ts -> packages/components/ui
|
|
27
|
-
const fromSrc = path.resolve(__dirname, '../../../components/ui');
|
|
28
|
-
if (fs.existsSync(fromSrc)) {
|
|
29
|
-
return fromSrc;
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
15
|
interface AddOptions {
|
|
35
16
|
yes?: boolean;
|
|
36
17
|
overwrite?: boolean;
|
|
37
18
|
all?: boolean;
|
|
38
19
|
path?: string;
|
|
39
|
-
remote?: boolean;
|
|
20
|
+
remote?: boolean;
|
|
21
|
+
branch: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ConflictCheckResult {
|
|
25
|
+
toInstall: ComponentName[];
|
|
26
|
+
toSkip: string[];
|
|
27
|
+
conflicting: ComponentName[];
|
|
28
|
+
peerFilesToUpdate: Set<string>;
|
|
29
|
+
contentCache: Map<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Path & URL helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
function validateBranch(branch: string): void {
|
|
37
|
+
if (!/^[\w.\-/]+$/.test(branch)) {
|
|
38
|
+
throw new Error(`Invalid branch name: ${branch}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getRegistryBaseUrl(branch: string) {
|
|
43
|
+
validateBranch(branch);
|
|
44
|
+
return `https://raw.githubusercontent.com/gilav21/shadcn-angular/${branch}/packages/components/ui`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getLibRegistryBaseUrl(branch: string) {
|
|
48
|
+
validateBranch(branch);
|
|
49
|
+
return `https://raw.githubusercontent.com/gilav21/shadcn-angular/${branch}/packages/components/lib`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getLocalComponentsDir(): string | null {
|
|
53
|
+
const localPath = path.resolve(__dirname, '../../../components/ui');
|
|
54
|
+
return fs.existsSync(localPath) ? localPath : null;
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
function getLocalLibDir(): string | null {
|
|
@@ -62,10 +77,17 @@ function aliasToProjectPath(aliasOrPath: string): string {
|
|
|
62
77
|
: aliasOrPath;
|
|
63
78
|
}
|
|
64
79
|
|
|
80
|
+
function normalizeContent(str: string): string {
|
|
81
|
+
return str.replaceAll('\r\n', '\n').trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Remote content fetching
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
65
88
|
async function fetchComponentContent(file: string, options: AddOptions): Promise<string> {
|
|
66
89
|
const localDir = getLocalComponentsDir();
|
|
67
90
|
|
|
68
|
-
// 1. Prefer local if available and not forced remote
|
|
69
91
|
if (localDir && !options.remote) {
|
|
70
92
|
const localPath = path.join(localDir, file);
|
|
71
93
|
if (await fs.pathExists(localPath)) {
|
|
@@ -73,8 +95,7 @@ async function fetchComponentContent(file: string, options: AddOptions): Promise
|
|
|
73
95
|
}
|
|
74
96
|
}
|
|
75
97
|
|
|
76
|
-
|
|
77
|
-
const url = `${REGISTRY_BASE_URL}/${file}`;
|
|
98
|
+
const url = `${getRegistryBaseUrl(options.branch)}/${file}`;
|
|
78
99
|
try {
|
|
79
100
|
const response = await fetch(url);
|
|
80
101
|
if (!response.ok) {
|
|
@@ -99,7 +120,7 @@ async function fetchLibContent(file: string, options: AddOptions): Promise<strin
|
|
|
99
120
|
}
|
|
100
121
|
}
|
|
101
122
|
|
|
102
|
-
const url = `${
|
|
123
|
+
const url = `${getLibRegistryBaseUrl(options.branch)}/${file}`;
|
|
103
124
|
const response = await fetch(url);
|
|
104
125
|
if (!response.ok) {
|
|
105
126
|
throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
|
|
@@ -107,39 +128,22 @@ async function fetchLibContent(file: string, options: AddOptions): Promise<strin
|
|
|
107
128
|
return response.text();
|
|
108
129
|
}
|
|
109
130
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
for (const shortcutDefinition of definition.shortcutDefinitions) {
|
|
117
|
-
const sourcePath = path.join(targetDir, shortcutDefinition.sourceFile);
|
|
118
|
-
if (fs.existsSync(sourcePath)) {
|
|
119
|
-
entries.push(shortcutDefinition);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return entries;
|
|
131
|
+
async function fetchAndTransform(file: string, options: AddOptions, utilsAlias: string): Promise<string> {
|
|
132
|
+
let content = await fetchComponentContent(file, options);
|
|
133
|
+
content = content.replaceAll(/(\.\.\/)+lib\//, utilsAlias + '/');
|
|
134
|
+
return content;
|
|
124
135
|
}
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Component selection & dependency resolution
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
128
140
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
console.log(chalk.red('Error: components.json not found.'));
|
|
133
|
-
console.log(chalk.dim('Run `npx @gilav21/shadcn-angular init` first.'));
|
|
134
|
-
process.exit(1);
|
|
141
|
+
async function selectComponents(components: string[], options: AddOptions): Promise<ComponentName[]> {
|
|
142
|
+
if (options.all) {
|
|
143
|
+
return Object.keys(registry);
|
|
135
144
|
}
|
|
136
145
|
|
|
137
|
-
|
|
138
|
-
let componentsToAdd: ComponentName[] = [];
|
|
139
|
-
|
|
140
|
-
if (options.all) {
|
|
141
|
-
componentsToAdd = Object.keys(registry) as ComponentName[];
|
|
142
|
-
} else if (components.length === 0) {
|
|
146
|
+
if (components.length === 0) {
|
|
143
147
|
const { selected } = await prompts({
|
|
144
148
|
type: 'multiselect',
|
|
145
149
|
name: 'selected',
|
|
@@ -150,131 +154,400 @@ export async function add(components: string[], options: AddOptions) {
|
|
|
150
154
|
})),
|
|
151
155
|
hint: '- Space to select, Enter to confirm',
|
|
152
156
|
});
|
|
153
|
-
|
|
154
|
-
} else {
|
|
155
|
-
componentsToAdd = components as ComponentName[];
|
|
157
|
+
return selected;
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
160
|
+
return components;
|
|
161
|
+
}
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
166
|
-
console.log(chalk.red(`Invalid component(s): ${
|
|
163
|
+
function validateComponents(names: ComponentName[]): void {
|
|
164
|
+
const invalid = names.filter(c => !registry[c]);
|
|
165
|
+
if (invalid.length > 0) {
|
|
166
|
+
console.log(chalk.red(`Invalid component(s): ${invalid.join(', ')}`));
|
|
167
167
|
console.log(chalk.dim('Available components: ' + Object.keys(registry).join(', ')));
|
|
168
168
|
process.exit(1);
|
|
169
169
|
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function resolveDependencies(names: ComponentName[]): Set<ComponentName> {
|
|
173
|
+
const all = new Set<ComponentName>();
|
|
174
|
+
const walk = (name: ComponentName) => {
|
|
175
|
+
if (all.has(name)) return;
|
|
176
|
+
all.add(name);
|
|
177
|
+
registry[name].dependencies?.forEach(dep => walk(dep));
|
|
178
|
+
};
|
|
179
|
+
names.forEach(walk);
|
|
180
|
+
return all;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Optional dependency prompt
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
interface OptionalChoice {
|
|
188
|
+
readonly name: string;
|
|
189
|
+
readonly description: string;
|
|
190
|
+
readonly requestedBy: string;
|
|
191
|
+
}
|
|
170
192
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
export async function promptOptionalDependencies(
|
|
194
|
+
resolved: Set<ComponentName>,
|
|
195
|
+
options: AddOptions,
|
|
196
|
+
): Promise<ComponentName[]> {
|
|
197
|
+
const seen = new Set<string>();
|
|
198
|
+
const choices: OptionalChoice[] = [];
|
|
199
|
+
|
|
200
|
+
for (const name of resolved) {
|
|
176
201
|
const component = registry[name];
|
|
177
|
-
if (component.
|
|
178
|
-
|
|
202
|
+
if (!component.optionalDependencies) continue;
|
|
203
|
+
|
|
204
|
+
for (const opt of component.optionalDependencies) {
|
|
205
|
+
if (resolved.has(opt.name) || seen.has(opt.name)) continue;
|
|
206
|
+
seen.add(opt.name);
|
|
207
|
+
choices.push({ name: opt.name, description: opt.description, requestedBy: name });
|
|
179
208
|
}
|
|
180
|
-
}
|
|
181
|
-
componentsToAdd.forEach(c => resolveDeps(c));
|
|
209
|
+
}
|
|
182
210
|
|
|
183
|
-
|
|
184
|
-
|
|
211
|
+
if (choices.length === 0) return [];
|
|
212
|
+
if (options.yes) return [];
|
|
213
|
+
if (options.all) return choices.map(c => c.name);
|
|
214
|
+
|
|
215
|
+
const { selected } = await prompts({
|
|
216
|
+
type: 'multiselect',
|
|
217
|
+
name: 'selected',
|
|
218
|
+
message: 'Optional companion components available:',
|
|
219
|
+
choices: choices.map(c => ({
|
|
220
|
+
title: c.name + ' ' + chalk.dim('- ' + c.description + ' (for ' + c.requestedBy + ')'),
|
|
221
|
+
value: c.name,
|
|
222
|
+
})),
|
|
223
|
+
hint: '- Space to select, Enter to confirm (or press Enter to skip)',
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return selected || [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Conflict detection
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
async function checkFileConflict(
|
|
234
|
+
file: string,
|
|
235
|
+
targetDir: string,
|
|
236
|
+
options: AddOptions,
|
|
237
|
+
utilsAlias: string,
|
|
238
|
+
contentCache: Map<string, string>,
|
|
239
|
+
): Promise<'identical' | 'changed' | 'missing'> {
|
|
240
|
+
const targetPath = path.join(targetDir, file);
|
|
241
|
+
|
|
242
|
+
if (!await fs.pathExists(targetPath)) {
|
|
243
|
+
return 'missing';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const localContent = await fs.readFile(targetPath, 'utf-8');
|
|
247
|
+
try {
|
|
248
|
+
const remoteContent = await fetchAndTransform(file, options, utilsAlias);
|
|
249
|
+
contentCache.set(file, remoteContent);
|
|
250
|
+
|
|
251
|
+
return normalizeContent(localContent) === normalizeContent(remoteContent)
|
|
252
|
+
? 'identical'
|
|
253
|
+
: 'changed';
|
|
254
|
+
} catch {
|
|
255
|
+
return 'changed';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
185
258
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
259
|
+
async function checkPeerFiles(
|
|
260
|
+
component: ComponentDefinition,
|
|
261
|
+
targetDir: string,
|
|
262
|
+
options: AddOptions,
|
|
263
|
+
utilsAlias: string,
|
|
264
|
+
contentCache: Map<string, string>,
|
|
265
|
+
peerFilesToUpdate: Set<string>,
|
|
266
|
+
): Promise<boolean> {
|
|
267
|
+
if (!component.peerFiles) return false;
|
|
268
|
+
|
|
269
|
+
let hasChanges = false;
|
|
270
|
+
for (const file of component.peerFiles) {
|
|
271
|
+
const status = await checkFileConflict(file, targetDir, options, utilsAlias, contentCache);
|
|
272
|
+
if (status === 'changed') {
|
|
273
|
+
hasChanges = true;
|
|
274
|
+
peerFilesToUpdate.add(file);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return hasChanges;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function classifyComponent(
|
|
281
|
+
name: ComponentName,
|
|
282
|
+
targetDir: string,
|
|
283
|
+
options: AddOptions,
|
|
284
|
+
utilsAlias: string,
|
|
285
|
+
contentCache: Map<string, string>,
|
|
286
|
+
peerFilesToUpdate: Set<string>,
|
|
287
|
+
): Promise<'install' | 'skip' | 'conflict'> {
|
|
288
|
+
const component = registry[name];
|
|
289
|
+
let hasChanges = false;
|
|
290
|
+
let isFullyPresent = true;
|
|
291
|
+
|
|
292
|
+
for (const file of component.files) {
|
|
293
|
+
const status = await checkFileConflict(file, targetDir, options, utilsAlias, contentCache);
|
|
294
|
+
if (status === 'missing') isFullyPresent = false;
|
|
295
|
+
if (status === 'changed') hasChanges = true;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const peerChanged = await checkPeerFiles(
|
|
299
|
+
component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate,
|
|
300
|
+
);
|
|
301
|
+
if (peerChanged) hasChanges = true;
|
|
302
|
+
|
|
303
|
+
if (isFullyPresent && !hasChanges) return 'skip';
|
|
304
|
+
if (hasChanges) return 'conflict';
|
|
305
|
+
return 'install';
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function detectConflicts(
|
|
309
|
+
allComponents: Set<ComponentName>,
|
|
310
|
+
targetDir: string,
|
|
311
|
+
options: AddOptions,
|
|
312
|
+
utilsAlias: string,
|
|
313
|
+
): Promise<ConflictCheckResult> {
|
|
314
|
+
const toInstall: ComponentName[] = [];
|
|
315
|
+
const toSkip: string[] = [];
|
|
316
|
+
const conflicting: ComponentName[] = [];
|
|
317
|
+
const peerFilesToUpdate = new Set<string>();
|
|
190
318
|
const contentCache = new Map<string, string>();
|
|
191
319
|
|
|
192
|
-
const
|
|
320
|
+
for (const name of allComponents) {
|
|
321
|
+
const result = await classifyComponent(
|
|
322
|
+
name, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate,
|
|
323
|
+
);
|
|
193
324
|
|
|
325
|
+
if (result === 'skip') toSkip.push(name);
|
|
326
|
+
else if (result === 'conflict') conflicting.push(name);
|
|
327
|
+
else toInstall.push(name);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { toInstall, toSkip, conflicting, peerFilesToUpdate, contentCache };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Overwrite prompt
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
async function promptOverwrite(
|
|
338
|
+
conflicting: ComponentName[],
|
|
339
|
+
options: AddOptions,
|
|
340
|
+
): Promise<ComponentName[]> {
|
|
341
|
+
if (conflicting.length === 0) return [];
|
|
342
|
+
|
|
343
|
+
if (options.overwrite) return conflicting;
|
|
344
|
+
if (options.yes) return [];
|
|
345
|
+
|
|
346
|
+
console.log(chalk.yellow(`\n${conflicting.length} component(s) have local changes or are different from remote.`));
|
|
347
|
+
const { selected } = await prompts({
|
|
348
|
+
type: 'multiselect',
|
|
349
|
+
name: 'selected',
|
|
350
|
+
message: 'Select components to OVERWRITE (Unselected will be skipped):',
|
|
351
|
+
choices: conflicting.map(name => ({ title: name, value: name })),
|
|
352
|
+
hint: '- Space to select, Enter to confirm',
|
|
353
|
+
});
|
|
354
|
+
return selected || [];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// File writing
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
async function writeComponentFiles(
|
|
362
|
+
component: ComponentDefinition,
|
|
363
|
+
targetDir: string,
|
|
364
|
+
options: AddOptions,
|
|
365
|
+
utilsAlias: string,
|
|
366
|
+
contentCache: Map<string, string>,
|
|
367
|
+
spinner: Ora,
|
|
368
|
+
): Promise<boolean> {
|
|
369
|
+
let success = true;
|
|
370
|
+
|
|
371
|
+
for (const file of component.files) {
|
|
372
|
+
const targetPath = path.join(targetDir, file);
|
|
373
|
+
try {
|
|
374
|
+
const content = contentCache.get(file)
|
|
375
|
+
?? await fetchAndTransform(file, options, utilsAlias);
|
|
376
|
+
|
|
377
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
378
|
+
await fs.writeFile(targetPath, content);
|
|
379
|
+
} catch (err: unknown) {
|
|
380
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
381
|
+
spinner.warn(`Could not add ${file}: ${message}`);
|
|
382
|
+
success = false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return success;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function writePeerFiles(
|
|
390
|
+
component: ComponentDefinition,
|
|
391
|
+
targetDir: string,
|
|
392
|
+
options: AddOptions,
|
|
393
|
+
utilsAlias: string,
|
|
394
|
+
contentCache: Map<string, string>,
|
|
395
|
+
peerFilesToUpdate: Set<string>,
|
|
396
|
+
spinner: Ora,
|
|
397
|
+
): Promise<void> {
|
|
398
|
+
if (!component.peerFiles) return;
|
|
399
|
+
|
|
400
|
+
for (const file of component.peerFiles) {
|
|
401
|
+
if (!peerFilesToUpdate.has(file)) continue;
|
|
402
|
+
|
|
403
|
+
const targetPath = path.join(targetDir, file);
|
|
404
|
+
try {
|
|
405
|
+
const content = contentCache.get(file)
|
|
406
|
+
?? await fetchAndTransform(file, options, utilsAlias);
|
|
407
|
+
|
|
408
|
+
await fs.writeFile(targetPath, content);
|
|
409
|
+
spinner.text = `Updated peer file ${file}`;
|
|
410
|
+
} catch (err: unknown) {
|
|
411
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
412
|
+
spinner.warn(`Could not update peer file ${file}: ${message}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function installLibFiles(
|
|
418
|
+
allComponents: Set<ComponentName>,
|
|
419
|
+
cwd: string,
|
|
420
|
+
libDir: string,
|
|
421
|
+
options: AddOptions,
|
|
422
|
+
): Promise<void> {
|
|
423
|
+
const required = new Set<string>();
|
|
194
424
|
for (const name of allComponents) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (normalize(localContent) !== normalize(remoteContent)) {
|
|
211
|
-
hasChanges = true;
|
|
212
|
-
}
|
|
213
|
-
contentCache.set(file, remoteContent); // Cache for installation
|
|
214
|
-
} catch (error) {
|
|
215
|
-
// unexpected error fetching remote
|
|
216
|
-
console.warn(`Could not fetch remote content for comparison: ${file}`);
|
|
217
|
-
hasChanges = true; // Assume changed/unknown
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
isFullyPresent = false;
|
|
425
|
+
registry[name].libFiles?.forEach(f => required.add(f));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (required.size === 0) return;
|
|
429
|
+
|
|
430
|
+
await fs.ensureDir(libDir);
|
|
431
|
+
for (const libFile of required) {
|
|
432
|
+
const targetPath = path.join(libDir, libFile);
|
|
433
|
+
if (!await fs.pathExists(targetPath) || options.overwrite) {
|
|
434
|
+
try {
|
|
435
|
+
const content = await fetchLibContent(libFile, options);
|
|
436
|
+
await fs.writeFile(targetPath, content);
|
|
437
|
+
} catch (err: unknown) {
|
|
438
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
439
|
+
console.warn(chalk.yellow(`Could not install lib file ${libFile}: ${message}`));
|
|
221
440
|
}
|
|
222
441
|
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
223
444
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
445
|
+
async function installNpmDependencies(
|
|
446
|
+
finalComponents: ComponentName[],
|
|
447
|
+
cwd: string,
|
|
448
|
+
): Promise<void> {
|
|
449
|
+
const deps = new Set<string>();
|
|
450
|
+
for (const name of finalComponents) {
|
|
451
|
+
registry[name].npmDependencies?.forEach(dep => deps.add(dep));
|
|
231
452
|
}
|
|
232
453
|
|
|
233
|
-
|
|
454
|
+
if (deps.size === 0) return;
|
|
234
455
|
|
|
235
|
-
|
|
456
|
+
const spinner = ora('Installing dependencies...').start();
|
|
457
|
+
try {
|
|
458
|
+
await installPackages(Array.from(deps), { cwd });
|
|
459
|
+
spinner.succeed('Dependencies installed.');
|
|
460
|
+
} catch (e) {
|
|
461
|
+
spinner.fail('Failed to install dependencies.');
|
|
462
|
+
console.error(e);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
236
465
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
hint: '- Space to select, Enter to confirm',
|
|
253
|
-
});
|
|
254
|
-
componentsToOverwrite = selected || [];
|
|
466
|
+
async function ensureShortcutService(
|
|
467
|
+
targetDir: string,
|
|
468
|
+
cwd: string,
|
|
469
|
+
config: Config,
|
|
470
|
+
options: AddOptions,
|
|
471
|
+
): Promise<void> {
|
|
472
|
+
const entries = collectInstalledShortcutEntries(targetDir);
|
|
473
|
+
|
|
474
|
+
if (entries.length > 0) {
|
|
475
|
+
const libDir = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils));
|
|
476
|
+
const servicePath = path.join(libDir, 'shortcut-binding.service.ts');
|
|
477
|
+
if (!await fs.pathExists(servicePath)) {
|
|
478
|
+
const content = await fetchLibContent('shortcut-binding.service.ts', options);
|
|
479
|
+
await fs.ensureDir(libDir);
|
|
480
|
+
await fs.writeFile(servicePath, content);
|
|
255
481
|
}
|
|
256
482
|
}
|
|
257
483
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
484
|
+
await writeShortcutRegistryIndex(cwd, config, entries);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function collectInstalledShortcutEntries(targetDir: string): ShortcutRegistryEntry[] {
|
|
488
|
+
const entries: ShortcutRegistryEntry[] = [];
|
|
489
|
+
for (const definition of Object.values(registry)) {
|
|
490
|
+
if (!definition.shortcutDefinitions?.length) continue;
|
|
491
|
+
for (const sd of definition.shortcutDefinitions) {
|
|
492
|
+
if (fs.existsSync(path.join(targetDir, sd.sourceFile))) {
|
|
493
|
+
entries.push(sd);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return entries;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ---------------------------------------------------------------------------
|
|
501
|
+
// Main entry point
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
export async function add(components: string[], options: AddOptions) {
|
|
505
|
+
const cwd = process.cwd();
|
|
265
506
|
|
|
266
|
-
const
|
|
507
|
+
const config = await getConfig(cwd);
|
|
508
|
+
if (!config) {
|
|
509
|
+
console.log(chalk.red('Error: components.json not found.'));
|
|
510
|
+
console.log(chalk.dim('Run `npx @gilav21/shadcn-angular init` first.'));
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
267
513
|
|
|
268
|
-
|
|
269
|
-
|
|
514
|
+
const componentsToAdd = await selectComponents(components, options);
|
|
515
|
+
if (!componentsToAdd || componentsToAdd.length === 0) {
|
|
516
|
+
console.log(chalk.dim('No components selected.'));
|
|
270
517
|
return;
|
|
271
518
|
}
|
|
272
519
|
|
|
520
|
+
validateComponents(componentsToAdd);
|
|
521
|
+
|
|
522
|
+
const resolvedComponents = resolveDependencies(componentsToAdd);
|
|
523
|
+
const optionalChoices = await promptOptionalDependencies(resolvedComponents, options);
|
|
524
|
+
const allComponents = optionalChoices.length > 0
|
|
525
|
+
? resolveDependencies([...resolvedComponents, ...optionalChoices])
|
|
526
|
+
: resolvedComponents;
|
|
527
|
+
const uiBasePath = options.path ?? aliasToProjectPath(config.aliases.ui || 'src/components/ui');
|
|
528
|
+
const targetDir = resolveProjectPath(cwd, uiBasePath);
|
|
529
|
+
const utilsAlias = config.aliases.utils;
|
|
530
|
+
|
|
531
|
+
// Detect conflicts
|
|
532
|
+
const checkSpinner = ora('Checking for conflicts...').start();
|
|
533
|
+
const { toInstall, toSkip, conflicting, peerFilesToUpdate, contentCache } =
|
|
534
|
+
await detectConflicts(allComponents, targetDir, options, utilsAlias);
|
|
535
|
+
checkSpinner.stop();
|
|
536
|
+
|
|
537
|
+
// Prompt user for overwrite decisions
|
|
538
|
+
const toOverwrite = await promptOverwrite(conflicting, options);
|
|
539
|
+
const finalComponents = [...toInstall, ...toOverwrite];
|
|
540
|
+
|
|
273
541
|
if (finalComponents.length === 0) {
|
|
274
|
-
|
|
542
|
+
if (toSkip.length > 0) {
|
|
543
|
+
console.log(chalk.green(`\nAll components are up to date! (${toSkip.length} skipped)`));
|
|
544
|
+
} else {
|
|
545
|
+
console.log(chalk.dim('\nNo components to install.'));
|
|
546
|
+
}
|
|
275
547
|
return;
|
|
276
548
|
}
|
|
277
549
|
|
|
550
|
+
// Install component files
|
|
278
551
|
const spinner = ora('Installing components...').start();
|
|
279
552
|
let successCount = 0;
|
|
280
553
|
|
|
@@ -283,28 +556,15 @@ export async function add(components: string[], options: AddOptions) {
|
|
|
283
556
|
|
|
284
557
|
for (const name of finalComponents) {
|
|
285
558
|
const component = registry[name];
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
// Transform all lib/ imports if not already transformed (cached is transformed)
|
|
296
|
-
content = content.replace(/(\.\.\/)+lib\//g, config.aliases.utils + '/');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
await fs.ensureDir(path.dirname(targetPath));
|
|
300
|
-
await fs.writeFile(targetPath, content);
|
|
301
|
-
// spinner.text = `Added ${file}`; // Too verbose?
|
|
302
|
-
} catch (err: any) {
|
|
303
|
-
spinner.warn(`Could not add ${file}: ${err.message}`);
|
|
304
|
-
componentSuccess = false;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
if (componentSuccess) {
|
|
559
|
+
|
|
560
|
+
const ok = await writeComponentFiles(
|
|
561
|
+
component, targetDir, options, utilsAlias, contentCache, spinner,
|
|
562
|
+
);
|
|
563
|
+
await writePeerFiles(
|
|
564
|
+
component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate, spinner,
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
if (ok) {
|
|
308
568
|
successCount++;
|
|
309
569
|
spinner.text = `Added ${name}`;
|
|
310
570
|
}
|
|
@@ -312,87 +572,24 @@ export async function add(components: string[], options: AddOptions) {
|
|
|
312
572
|
|
|
313
573
|
if (successCount > 0) {
|
|
314
574
|
spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
|
|
315
|
-
|
|
316
575
|
console.log('\n' + chalk.dim('Components added:'));
|
|
317
|
-
finalComponents.forEach(name =>
|
|
318
|
-
console.log(chalk.dim(' - ') + chalk.cyan(name));
|
|
319
|
-
});
|
|
576
|
+
finalComponents.forEach(name => console.log(chalk.dim(' - ') + chalk.cyan(name)));
|
|
320
577
|
} else {
|
|
321
578
|
spinner.info('No new components installed.');
|
|
322
579
|
}
|
|
323
580
|
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (component.libFiles) {
|
|
330
|
-
component.libFiles.forEach(f => requiredLibFiles.add(f));
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (requiredLibFiles.size > 0) {
|
|
335
|
-
const libDir = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils));
|
|
336
|
-
await fs.ensureDir(libDir);
|
|
337
|
-
|
|
338
|
-
for (const libFile of requiredLibFiles) {
|
|
339
|
-
const libTargetPath = path.join(libDir, libFile);
|
|
340
|
-
if (!await fs.pathExists(libTargetPath) || options.overwrite) {
|
|
341
|
-
try {
|
|
342
|
-
const libContent = await fetchLibContent(libFile, options);
|
|
343
|
-
await fs.writeFile(libTargetPath, libContent);
|
|
344
|
-
} catch (err: any) {
|
|
345
|
-
console.warn(chalk.yellow(`Could not install lib file ${libFile}: ${err.message}`));
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (finalComponents.length > 0) {
|
|
353
|
-
const npmDependencies = new Set<string>();
|
|
354
|
-
for (const name of finalComponents) {
|
|
355
|
-
const component = registry[name];
|
|
356
|
-
if (component.npmDependencies) {
|
|
357
|
-
component.npmDependencies.forEach(dep => npmDependencies.add(dep));
|
|
358
|
-
}
|
|
359
|
-
}
|
|
581
|
+
// Post-install: lib files, npm deps, shortcuts
|
|
582
|
+
const libDir = resolveProjectPath(cwd, aliasToProjectPath(utilsAlias));
|
|
583
|
+
await installLibFiles(allComponents, cwd, libDir, options);
|
|
584
|
+
await installNpmDependencies(finalComponents, cwd);
|
|
585
|
+
await ensureShortcutService(targetDir, cwd, config, options);
|
|
360
586
|
|
|
361
|
-
|
|
362
|
-
const depSpinner = ora('Installing dependencies...').start();
|
|
363
|
-
try {
|
|
364
|
-
await installPackages(Array.from(npmDependencies), { cwd });
|
|
365
|
-
depSpinner.succeed('Dependencies installed.');
|
|
366
|
-
} catch (e) {
|
|
367
|
-
depSpinner.fail('Failed to install dependencies.');
|
|
368
|
-
console.error(e);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const shortcutEntries = collectInstalledShortcutEntries(targetDir);
|
|
374
|
-
if (shortcutEntries.length > 0) {
|
|
375
|
-
const libDir2 = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils));
|
|
376
|
-
const shortcutServicePath = path.join(libDir2, 'shortcut-binding.service.ts');
|
|
377
|
-
|
|
378
|
-
if (!await fs.pathExists(shortcutServicePath)) {
|
|
379
|
-
const shortcutServiceContent = await fetchLibContent('shortcut-binding.service.ts', options);
|
|
380
|
-
await fs.ensureDir(libDir2);
|
|
381
|
-
await fs.writeFile(shortcutServicePath, shortcutServiceContent);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
await writeShortcutRegistryIndex(cwd, config, shortcutEntries);
|
|
386
|
-
|
|
387
|
-
if (componentsToSkip.length > 0) {
|
|
587
|
+
if (toSkip.length > 0) {
|
|
388
588
|
console.log('\n' + chalk.dim('Components skipped (up to date):'));
|
|
389
|
-
|
|
390
|
-
console.log(chalk.dim(' - ') + chalk.gray(name));
|
|
391
|
-
});
|
|
589
|
+
toSkip.forEach(name => console.log(chalk.dim(' - ') + chalk.gray(name)));
|
|
392
590
|
}
|
|
393
591
|
|
|
394
592
|
console.log('');
|
|
395
|
-
|
|
396
593
|
} catch (error) {
|
|
397
594
|
spinner.fail('Failed to add components');
|
|
398
595
|
console.error(error);
|