@gilav21/shadcn-angular 0.0.16 → 0.0.18
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.js +33 -10
- package/dist/commands/init.js +4 -5
- package/dist/registry/index.d.ts +1 -0
- package/dist/registry/index.js +2 -0
- package/dist/templates/utils.js +31 -1
- package/dist/utils/config.js +1 -1
- package/dist/utils/shortcut-registry.js +3 -7
- package/package.json +1 -1
- package/src/commands/add.ts +310 -285
- package/src/commands/init.ts +173 -174
- package/src/registry/index.ts +3 -0
- package/src/templates/utils.ts +31 -1
- package/src/utils/config.ts +1 -1
- package/src/utils/shortcut-registry.ts +3 -7
package/src/commands/add.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import prompts from 'prompts';
|
|
5
|
-
import chalk from 'chalk';
|
|
6
|
-
import ora from 'ora';
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import prompts from 'prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
7
|
import { getConfig } from '../utils/config.js';
|
|
8
8
|
import { registry, 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
|
-
|
|
12
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
const __dirname = path.dirname(__filename);
|
|
14
|
-
|
|
15
|
-
// Base URL for the component registry (GitHub raw content)
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
// Base URL for the component registry (GitHub raw content)
|
|
16
16
|
const REGISTRY_BASE_URL = 'https://raw.githubusercontent.com/gilav21/shadcn-angular/master/packages/components/ui';
|
|
17
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
|
|
18
|
+
|
|
19
|
+
// Components source directory (relative to CLI dist folder) for local dev
|
|
20
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
|
-
|
|
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
34
|
interface AddOptions {
|
|
35
|
-
yes?: boolean;
|
|
36
|
-
overwrite?: boolean;
|
|
37
|
-
all?: boolean;
|
|
38
|
-
path?: string;
|
|
35
|
+
yes?: boolean;
|
|
36
|
+
overwrite?: boolean;
|
|
37
|
+
all?: boolean;
|
|
38
|
+
path?: string;
|
|
39
39
|
remote?: boolean; // Force remote fetch
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -63,30 +63,30 @@ function aliasToProjectPath(aliasOrPath: string): string {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
async function fetchComponentContent(file: string, options: AddOptions): Promise<string> {
|
|
66
|
-
const localDir = getLocalComponentsDir();
|
|
67
|
-
|
|
68
|
-
// 1. Prefer local if available and not forced remote
|
|
69
|
-
if (localDir && !options.remote) {
|
|
70
|
-
const localPath = path.join(localDir, file);
|
|
71
|
-
if (await fs.pathExists(localPath)) {
|
|
72
|
-
return fs.readFile(localPath, 'utf-8');
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 2. Fetch from remote registry
|
|
77
|
-
const url = `${REGISTRY_BASE_URL}/${file}`;
|
|
78
|
-
try {
|
|
79
|
-
const response = await fetch(url);
|
|
80
|
-
if (!response.ok) {
|
|
81
|
-
throw new Error(`Failed to fetch component from ${url}: ${response.statusText}`);
|
|
82
|
-
}
|
|
83
|
-
return await response.text();
|
|
84
|
-
} catch (error) {
|
|
85
|
-
if (localDir) {
|
|
86
|
-
throw new Error(`Component file not found locally or remotely: ${file}`);
|
|
87
|
-
}
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
66
|
+
const localDir = getLocalComponentsDir();
|
|
67
|
+
|
|
68
|
+
// 1. Prefer local if available and not forced remote
|
|
69
|
+
if (localDir && !options.remote) {
|
|
70
|
+
const localPath = path.join(localDir, file);
|
|
71
|
+
if (await fs.pathExists(localPath)) {
|
|
72
|
+
return fs.readFile(localPath, 'utf-8');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2. Fetch from remote registry
|
|
77
|
+
const url = `${REGISTRY_BASE_URL}/${file}`;
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(url);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`Failed to fetch component from ${url}: ${response.statusText}`);
|
|
82
|
+
}
|
|
83
|
+
return await response.text();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (localDir) {
|
|
86
|
+
throw new Error(`Component file not found locally or remotely: ${file}`);
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
async function fetchLibContent(file: string, options: AddOptions): Promise<string> {
|
|
@@ -122,237 +122,262 @@ function collectInstalledShortcutEntries(targetDir: string): ShortcutRegistryEnt
|
|
|
122
122
|
}
|
|
123
123
|
return entries;
|
|
124
124
|
}
|
|
125
|
-
|
|
126
|
-
export async function add(components: string[], options: AddOptions) {
|
|
127
|
-
const cwd = process.cwd();
|
|
128
|
-
|
|
129
|
-
// Load config
|
|
130
|
-
const config = await getConfig(cwd);
|
|
131
|
-
if (!config) {
|
|
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);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Get components to add
|
|
138
|
-
let componentsToAdd: ComponentName[] = [];
|
|
139
|
-
|
|
140
|
-
if (options.all) {
|
|
141
|
-
componentsToAdd = Object.keys(registry) as ComponentName[];
|
|
142
|
-
} else if (components.length === 0) {
|
|
143
|
-
const { selected } = await prompts({
|
|
144
|
-
type: 'multiselect',
|
|
145
|
-
name: 'selected',
|
|
146
|
-
message: 'Which components would you like to add?',
|
|
147
|
-
choices: Object.keys(registry).map(name => ({
|
|
148
|
-
title: name,
|
|
149
|
-
value: name,
|
|
150
|
-
})),
|
|
151
|
-
hint: '- Space to select, Enter to confirm',
|
|
152
|
-
});
|
|
153
|
-
componentsToAdd = selected;
|
|
154
|
-
} else {
|
|
155
|
-
componentsToAdd = components as ComponentName[];
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (!componentsToAdd || componentsToAdd.length === 0) {
|
|
159
|
-
console.log(chalk.dim('No components selected.'));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Validate components exist
|
|
164
|
-
const invalidComponents = componentsToAdd.filter(c => !registry[c]);
|
|
165
|
-
if (invalidComponents.length > 0) {
|
|
166
|
-
console.log(chalk.red(`Invalid component(s): ${invalidComponents.join(', ')}`));
|
|
167
|
-
console.log(chalk.dim('Available components: ' + Object.keys(registry).join(', ')));
|
|
168
|
-
process.exit(1);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Resolve dependencies
|
|
172
|
-
const allComponents = new Set<ComponentName>();
|
|
173
|
-
const resolveDeps = (name: ComponentName) => {
|
|
174
|
-
if (allComponents.has(name)) return;
|
|
175
|
-
allComponents.add(name);
|
|
176
|
-
const component = registry[name];
|
|
177
|
-
if (component.dependencies) {
|
|
178
|
-
component.dependencies.forEach(dep => resolveDeps(dep as ComponentName));
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
componentsToAdd.forEach(c => resolveDeps(c));
|
|
182
|
-
|
|
125
|
+
|
|
126
|
+
export async function add(components: string[], options: AddOptions) {
|
|
127
|
+
const cwd = process.cwd();
|
|
128
|
+
|
|
129
|
+
// Load config
|
|
130
|
+
const config = await getConfig(cwd);
|
|
131
|
+
if (!config) {
|
|
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);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Get components to add
|
|
138
|
+
let componentsToAdd: ComponentName[] = [];
|
|
139
|
+
|
|
140
|
+
if (options.all) {
|
|
141
|
+
componentsToAdd = Object.keys(registry) as ComponentName[];
|
|
142
|
+
} else if (components.length === 0) {
|
|
143
|
+
const { selected } = await prompts({
|
|
144
|
+
type: 'multiselect',
|
|
145
|
+
name: 'selected',
|
|
146
|
+
message: 'Which components would you like to add?',
|
|
147
|
+
choices: Object.keys(registry).map(name => ({
|
|
148
|
+
title: name,
|
|
149
|
+
value: name,
|
|
150
|
+
})),
|
|
151
|
+
hint: '- Space to select, Enter to confirm',
|
|
152
|
+
});
|
|
153
|
+
componentsToAdd = selected;
|
|
154
|
+
} else {
|
|
155
|
+
componentsToAdd = components as ComponentName[];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!componentsToAdd || componentsToAdd.length === 0) {
|
|
159
|
+
console.log(chalk.dim('No components selected.'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate components exist
|
|
164
|
+
const invalidComponents = componentsToAdd.filter(c => !registry[c]);
|
|
165
|
+
if (invalidComponents.length > 0) {
|
|
166
|
+
console.log(chalk.red(`Invalid component(s): ${invalidComponents.join(', ')}`));
|
|
167
|
+
console.log(chalk.dim('Available components: ' + Object.keys(registry).join(', ')));
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Resolve dependencies
|
|
172
|
+
const allComponents = new Set<ComponentName>();
|
|
173
|
+
const resolveDeps = (name: ComponentName) => {
|
|
174
|
+
if (allComponents.has(name)) return;
|
|
175
|
+
allComponents.add(name);
|
|
176
|
+
const component = registry[name];
|
|
177
|
+
if (component.dependencies) {
|
|
178
|
+
component.dependencies.forEach(dep => resolveDeps(dep as ComponentName));
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
componentsToAdd.forEach(c => resolveDeps(c));
|
|
182
|
+
|
|
183
183
|
const uiBasePath = options.path ?? aliasToProjectPath(config.aliases.ui || 'src/components/ui');
|
|
184
184
|
const targetDir = resolveProjectPath(cwd, uiBasePath);
|
|
185
|
-
|
|
186
|
-
// Check for existing files and diff
|
|
187
|
-
const componentsToInstall: ComponentName[] = [];
|
|
188
|
-
const componentsToSkip: string[] = [];
|
|
189
|
-
const conflictingComponents: ComponentName[] = [];
|
|
190
|
-
const contentCache = new Map<string, string>();
|
|
191
|
-
|
|
192
|
-
const checkSpinner = ora('Checking for conflicts...').start();
|
|
193
|
-
|
|
194
|
-
for (const name of allComponents) {
|
|
195
|
-
const component = registry[name];
|
|
196
|
-
let hasChanges = false;
|
|
197
|
-
let isFullyPresent = true;
|
|
198
|
-
|
|
199
|
-
for (const file of component.files) {
|
|
200
|
-
const targetPath = path.join(targetDir, file);
|
|
201
|
-
if (await fs.pathExists(targetPath)) {
|
|
202
|
-
const localContent = await fs.readFile(targetPath, 'utf-8');
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
let remoteContent = await fetchComponentContent(file, options);
|
|
206
|
-
// Transform imports for comparison
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
185
|
+
|
|
186
|
+
// Check for existing files and diff
|
|
187
|
+
const componentsToInstall: ComponentName[] = [];
|
|
188
|
+
const componentsToSkip: string[] = [];
|
|
189
|
+
const conflictingComponents: ComponentName[] = [];
|
|
190
|
+
const contentCache = new Map<string, string>();
|
|
191
|
+
|
|
192
|
+
const checkSpinner = ora('Checking for conflicts...').start();
|
|
193
|
+
|
|
194
|
+
for (const name of allComponents) {
|
|
195
|
+
const component = registry[name];
|
|
196
|
+
let hasChanges = false;
|
|
197
|
+
let isFullyPresent = true;
|
|
198
|
+
|
|
199
|
+
for (const file of component.files) {
|
|
200
|
+
const targetPath = path.join(targetDir, file);
|
|
201
|
+
if (await fs.pathExists(targetPath)) {
|
|
202
|
+
const localContent = await fs.readFile(targetPath, 'utf-8');
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
let remoteContent = await fetchComponentContent(file, options);
|
|
206
|
+
// Transform all lib/ imports for comparison
|
|
207
|
+
remoteContent = remoteContent.replace(/(\.\.\/)+lib\//g, config.aliases.utils + '/');
|
|
208
|
+
|
|
210
209
|
const normalize = (str: string) => str.replace(/\r\n/g, '\n').trim();
|
|
211
|
-
if (normalize(localContent) !== normalize(remoteContent)) {
|
|
212
|
-
hasChanges = true;
|
|
213
|
-
}
|
|
214
|
-
contentCache.set(file, remoteContent); // Cache for installation
|
|
215
|
-
} catch (error) {
|
|
216
|
-
// unexpected error fetching remote
|
|
217
|
-
console.warn(`Could not fetch remote content for comparison: ${file}`);
|
|
218
|
-
hasChanges = true; // Assume changed/unknown
|
|
219
|
-
}
|
|
220
|
-
} else {
|
|
221
|
-
isFullyPresent = false;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (isFullyPresent && !hasChanges) {
|
|
226
|
-
componentsToSkip.push(name);
|
|
227
|
-
} else if (hasChanges) {
|
|
228
|
-
conflictingComponents.push(name);
|
|
229
|
-
} else {
|
|
230
|
-
componentsToInstall.push(name);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
checkSpinner.stop();
|
|
235
|
-
|
|
236
|
-
let componentsToOverwrite: ComponentName[] = [];
|
|
237
|
-
|
|
238
|
-
if (conflictingComponents.length > 0) {
|
|
239
|
-
if (options.overwrite) {
|
|
240
|
-
componentsToOverwrite = conflictingComponents;
|
|
241
|
-
} else if (options.yes) {
|
|
242
|
-
componentsToOverwrite = []; // Skip conflicts in non-interactive mode unless --overwrite
|
|
243
|
-
} else {
|
|
244
|
-
console.log(chalk.yellow(`\n${conflictingComponents.length} component(s) have local changes or are different from remote.`));
|
|
245
|
-
const { selected } = await prompts({
|
|
246
|
-
type: 'multiselect',
|
|
247
|
-
name: 'selected',
|
|
248
|
-
message: 'Select components to OVERWRITE (Unselected will be skipped):',
|
|
249
|
-
choices: conflictingComponents.map(name => ({
|
|
250
|
-
title: name,
|
|
251
|
-
value: name,
|
|
252
|
-
})),
|
|
253
|
-
hint: '- Space to select, Enter to confirm',
|
|
254
|
-
});
|
|
255
|
-
componentsToOverwrite = selected || [];
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Final list of components to process
|
|
260
|
-
// We process:
|
|
261
|
-
// 1. componentsToInstall (Brand new or partial)
|
|
262
|
-
// 2. componentsToOverwrite (User selected)
|
|
263
|
-
// We SKIP:
|
|
264
|
-
// 1. componentsToSkip (Identical)
|
|
265
|
-
// 2. conflictingComponents NOT in componentsToOverwrite
|
|
266
|
-
|
|
267
|
-
const finalComponents = [...componentsToInstall, ...componentsToOverwrite];
|
|
268
|
-
|
|
269
|
-
if (finalComponents.length === 0 && componentsToSkip.length > 0) {
|
|
270
|
-
console.log(chalk.green(`\nAll components are up to date! (${componentsToSkip.length} skipped)`));
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (finalComponents.length === 0) {
|
|
275
|
-
console.log(chalk.dim('\nNo components to install.'));
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const spinner = ora('Installing components...').start();
|
|
280
|
-
let successCount = 0;
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
await fs.ensureDir(targetDir);
|
|
284
|
-
|
|
285
|
-
for (const name of finalComponents) {
|
|
286
|
-
const component = registry[name];
|
|
287
|
-
let componentSuccess = true;
|
|
288
|
-
|
|
289
|
-
for (const file of component.files) {
|
|
290
|
-
const targetPath = path.join(targetDir, file);
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
let content = contentCache.get(file);
|
|
294
|
-
if (!content) {
|
|
295
|
-
content = await fetchComponentContent(file, options);
|
|
296
|
-
// Transform imports if not already transformed (cached is transformed)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
await fs.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (isFullyPresent && !hasChanges) {
|
|
225
|
+
componentsToSkip.push(name);
|
|
226
|
+
} else if (hasChanges) {
|
|
227
|
+
conflictingComponents.push(name);
|
|
228
|
+
} else {
|
|
229
|
+
componentsToInstall.push(name);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
checkSpinner.stop();
|
|
234
|
+
|
|
235
|
+
let componentsToOverwrite: ComponentName[] = [];
|
|
236
|
+
|
|
237
|
+
if (conflictingComponents.length > 0) {
|
|
238
|
+
if (options.overwrite) {
|
|
239
|
+
componentsToOverwrite = conflictingComponents;
|
|
240
|
+
} else if (options.yes) {
|
|
241
|
+
componentsToOverwrite = []; // Skip conflicts in non-interactive mode unless --overwrite
|
|
242
|
+
} else {
|
|
243
|
+
console.log(chalk.yellow(`\n${conflictingComponents.length} component(s) have local changes or are different from remote.`));
|
|
244
|
+
const { selected } = await prompts({
|
|
245
|
+
type: 'multiselect',
|
|
246
|
+
name: 'selected',
|
|
247
|
+
message: 'Select components to OVERWRITE (Unselected will be skipped):',
|
|
248
|
+
choices: conflictingComponents.map(name => ({
|
|
249
|
+
title: name,
|
|
250
|
+
value: name,
|
|
251
|
+
})),
|
|
252
|
+
hint: '- Space to select, Enter to confirm',
|
|
253
|
+
});
|
|
254
|
+
componentsToOverwrite = selected || [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Final list of components to process
|
|
259
|
+
// We process:
|
|
260
|
+
// 1. componentsToInstall (Brand new or partial)
|
|
261
|
+
// 2. componentsToOverwrite (User selected)
|
|
262
|
+
// We SKIP:
|
|
263
|
+
// 1. componentsToSkip (Identical)
|
|
264
|
+
// 2. conflictingComponents NOT in componentsToOverwrite
|
|
265
|
+
|
|
266
|
+
const finalComponents = [...componentsToInstall, ...componentsToOverwrite];
|
|
267
|
+
|
|
268
|
+
if (finalComponents.length === 0 && componentsToSkip.length > 0) {
|
|
269
|
+
console.log(chalk.green(`\nAll components are up to date! (${componentsToSkip.length} skipped)`));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (finalComponents.length === 0) {
|
|
274
|
+
console.log(chalk.dim('\nNo components to install.'));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const spinner = ora('Installing components...').start();
|
|
279
|
+
let successCount = 0;
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
await fs.ensureDir(targetDir);
|
|
283
|
+
|
|
284
|
+
for (const name of finalComponents) {
|
|
285
|
+
const component = registry[name];
|
|
286
|
+
let componentSuccess = true;
|
|
287
|
+
|
|
288
|
+
for (const file of component.files) {
|
|
289
|
+
const targetPath = path.join(targetDir, file);
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
let content = contentCache.get(file);
|
|
293
|
+
if (!content) {
|
|
294
|
+
content = await fetchComponentContent(file, options);
|
|
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) {
|
|
308
|
+
successCount++;
|
|
309
|
+
spinner.text = `Added ${name}`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (successCount > 0) {
|
|
314
|
+
spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
|
|
315
|
+
|
|
316
|
+
console.log('\n' + chalk.dim('Components added:'));
|
|
317
|
+
finalComponents.forEach(name => {
|
|
318
|
+
console.log(chalk.dim(' - ') + chalk.cyan(name));
|
|
319
|
+
});
|
|
320
|
+
} else {
|
|
321
|
+
spinner.info('No new components installed.');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Install required lib utility files
|
|
325
|
+
if (finalComponents.length > 0) {
|
|
326
|
+
const requiredLibFiles = new Set<string>();
|
|
327
|
+
for (const name of allComponents) {
|
|
328
|
+
const component = registry[name];
|
|
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
|
+
|
|
326
352
|
if (finalComponents.length > 0) {
|
|
327
353
|
const npmDependencies = new Set<string>();
|
|
328
|
-
for (const name of finalComponents) {
|
|
329
|
-
const component = registry[name];
|
|
330
|
-
if (component.npmDependencies) {
|
|
331
|
-
component.npmDependencies.forEach(dep => npmDependencies.add(dep));
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (npmDependencies.size > 0) {
|
|
336
|
-
const depSpinner = ora('Installing dependencies...').start();
|
|
337
|
-
try {
|
|
338
|
-
await installPackages(Array.from(npmDependencies), { cwd });
|
|
339
|
-
depSpinner.succeed('Dependencies installed.');
|
|
340
|
-
} catch (e) {
|
|
341
|
-
depSpinner.fail('Failed to install dependencies.');
|
|
342
|
-
console.error(e);
|
|
343
|
-
}
|
|
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
|
+
}
|
|
360
|
+
|
|
361
|
+
if (npmDependencies.size > 0) {
|
|
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
|
+
}
|
|
344
370
|
}
|
|
345
371
|
}
|
|
346
372
|
|
|
347
373
|
const shortcutEntries = collectInstalledShortcutEntries(targetDir);
|
|
348
374
|
if (shortcutEntries.length > 0) {
|
|
349
|
-
const
|
|
350
|
-
const
|
|
351
|
-
const shortcutServicePath = path.join(utilsDir, 'shortcut-binding.service.ts');
|
|
375
|
+
const libDir2 = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils));
|
|
376
|
+
const shortcutServicePath = path.join(libDir2, 'shortcut-binding.service.ts');
|
|
352
377
|
|
|
353
378
|
if (!await fs.pathExists(shortcutServicePath)) {
|
|
354
379
|
const shortcutServiceContent = await fetchLibContent('shortcut-binding.service.ts', options);
|
|
355
|
-
await fs.ensureDir(
|
|
380
|
+
await fs.ensureDir(libDir2);
|
|
356
381
|
await fs.writeFile(shortcutServicePath, shortcutServiceContent);
|
|
357
382
|
}
|
|
358
383
|
}
|
|
@@ -361,16 +386,16 @@ export async function add(components: string[], options: AddOptions) {
|
|
|
361
386
|
|
|
362
387
|
if (componentsToSkip.length > 0) {
|
|
363
388
|
console.log('\n' + chalk.dim('Components skipped (up to date):'));
|
|
364
|
-
componentsToSkip.forEach(name => {
|
|
365
|
-
console.log(chalk.dim(' - ') + chalk.gray(name));
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
console.log('');
|
|
370
|
-
|
|
371
|
-
} catch (error) {
|
|
372
|
-
spinner.fail('Failed to add components');
|
|
373
|
-
console.error(error);
|
|
374
|
-
process.exit(1);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
389
|
+
componentsToSkip.forEach(name => {
|
|
390
|
+
console.log(chalk.dim(' - ') + chalk.gray(name));
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log('');
|
|
395
|
+
|
|
396
|
+
} catch (error) {
|
|
397
|
+
spinner.fail('Failed to add components');
|
|
398
|
+
console.error(error);
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
}
|