@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/dist/commands/add.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
import ora from 'ora';
|
|
@@ -10,22 +10,25 @@ import { installPackages } from '../utils/package-manager.js';
|
|
|
10
10
|
import { writeShortcutRegistryIndex } from '../utils/shortcut-registry.js';
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
12
|
const __dirname = path.dirname(__filename);
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const fromDist = path.resolve(__dirname, '../../../components/ui');
|
|
20
|
-
if (fs.existsSync(fromDist)) {
|
|
21
|
-
return fromDist;
|
|
22
|
-
}
|
|
23
|
-
// Fallback: from src/commands/add.ts -> packages/components/ui
|
|
24
|
-
const fromSrc = path.resolve(__dirname, '../../../components/ui');
|
|
25
|
-
if (fs.existsSync(fromSrc)) {
|
|
26
|
-
return fromSrc;
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Path & URL helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
function validateBranch(branch) {
|
|
17
|
+
if (!/^[\w.\-/]+$/.test(branch)) {
|
|
18
|
+
throw new Error(`Invalid branch name: ${branch}`);
|
|
27
19
|
}
|
|
28
|
-
|
|
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;
|
|
29
32
|
}
|
|
30
33
|
function getLocalLibDir() {
|
|
31
34
|
const fromDist = path.resolve(__dirname, '../../../components/lib');
|
|
@@ -47,17 +50,21 @@ function aliasToProjectPath(aliasOrPath) {
|
|
|
47
50
|
? path.join('src', aliasOrPath.slice(2))
|
|
48
51
|
: aliasOrPath;
|
|
49
52
|
}
|
|
53
|
+
function normalizeContent(str) {
|
|
54
|
+
return str.replaceAll('\r\n', '\n').trim();
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Remote content fetching
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
50
59
|
async function fetchComponentContent(file, options) {
|
|
51
60
|
const localDir = getLocalComponentsDir();
|
|
52
|
-
// 1. Prefer local if available and not forced remote
|
|
53
61
|
if (localDir && !options.remote) {
|
|
54
62
|
const localPath = path.join(localDir, file);
|
|
55
63
|
if (await fs.pathExists(localPath)) {
|
|
56
64
|
return fs.readFile(localPath, 'utf-8');
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
|
-
|
|
60
|
-
const url = `${REGISTRY_BASE_URL}/${file}`;
|
|
67
|
+
const url = `${getRegistryBaseUrl(options.branch)}/${file}`;
|
|
61
68
|
try {
|
|
62
69
|
const response = await fetch(url);
|
|
63
70
|
if (!response.ok) {
|
|
@@ -80,43 +87,26 @@ async function fetchLibContent(file, options) {
|
|
|
80
87
|
return fs.readFile(localPath, 'utf-8');
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
|
-
const url = `${
|
|
90
|
+
const url = `${getLibRegistryBaseUrl(options.branch)}/${file}`;
|
|
84
91
|
const response = await fetch(url);
|
|
85
92
|
if (!response.ok) {
|
|
86
93
|
throw new Error(`Failed to fetch library file from ${url}: ${response.statusText}`);
|
|
87
94
|
}
|
|
88
95
|
return response.text();
|
|
89
96
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
for (const shortcutDefinition of definition.shortcutDefinitions) {
|
|
97
|
-
const sourcePath = path.join(targetDir, shortcutDefinition.sourceFile);
|
|
98
|
-
if (fs.existsSync(sourcePath)) {
|
|
99
|
-
entries.push(shortcutDefinition);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return entries;
|
|
97
|
+
async function fetchAndTransform(file, options, utilsAlias) {
|
|
98
|
+
let content = await fetchComponentContent(file, options);
|
|
99
|
+
content = content.replaceAll(/(\.\.\/)+lib\//, utilsAlias + '/');
|
|
100
|
+
return content;
|
|
104
101
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (!config) {
|
|
110
|
-
console.log(chalk.red('Error: components.json not found.'));
|
|
111
|
-
console.log(chalk.dim('Run `npx @gilav21/shadcn-angular init` first.'));
|
|
112
|
-
process.exit(1);
|
|
113
|
-
}
|
|
114
|
-
// Get components to add
|
|
115
|
-
let componentsToAdd = [];
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Component selection & dependency resolution
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
async function selectComponents(components, options) {
|
|
116
106
|
if (options.all) {
|
|
117
|
-
|
|
107
|
+
return Object.keys(registry);
|
|
118
108
|
}
|
|
119
|
-
|
|
109
|
+
if (components.length === 0) {
|
|
120
110
|
const { selected } = await prompts({
|
|
121
111
|
type: 'multiselect',
|
|
122
112
|
name: 'selected',
|
|
@@ -127,146 +117,307 @@ export async function add(components, options) {
|
|
|
127
117
|
})),
|
|
128
118
|
hint: '- Space to select, Enter to confirm',
|
|
129
119
|
});
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
componentsToAdd = components;
|
|
134
|
-
}
|
|
135
|
-
if (!componentsToAdd || componentsToAdd.length === 0) {
|
|
136
|
-
console.log(chalk.dim('No components selected.'));
|
|
137
|
-
return;
|
|
120
|
+
return selected;
|
|
138
121
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
122
|
+
return components;
|
|
123
|
+
}
|
|
124
|
+
function validateComponents(names) {
|
|
125
|
+
const invalid = names.filter(c => !registry[c]);
|
|
126
|
+
if (invalid.length > 0) {
|
|
127
|
+
console.log(chalk.red(`Invalid component(s): ${invalid.join(', ')}`));
|
|
143
128
|
console.log(chalk.dim('Available components: ' + Object.keys(registry).join(', ')));
|
|
144
129
|
process.exit(1);
|
|
145
130
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
131
|
+
}
|
|
132
|
+
export function resolveDependencies(names) {
|
|
133
|
+
const all = new Set();
|
|
134
|
+
const walk = (name) => {
|
|
135
|
+
if (all.has(name))
|
|
150
136
|
return;
|
|
151
|
-
|
|
137
|
+
all.add(name);
|
|
138
|
+
registry[name].dependencies?.forEach(dep => walk(dep));
|
|
139
|
+
};
|
|
140
|
+
names.forEach(walk);
|
|
141
|
+
return all;
|
|
142
|
+
}
|
|
143
|
+
export async function promptOptionalDependencies(resolved, options) {
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
const choices = [];
|
|
146
|
+
for (const name of resolved) {
|
|
152
147
|
const component = registry[name];
|
|
153
|
-
if (component.
|
|
154
|
-
|
|
148
|
+
if (!component.optionalDependencies)
|
|
149
|
+
continue;
|
|
150
|
+
for (const opt of component.optionalDependencies) {
|
|
151
|
+
if (resolved.has(opt.name) || seen.has(opt.name))
|
|
152
|
+
continue;
|
|
153
|
+
seen.add(opt.name);
|
|
154
|
+
choices.push({ name: opt.name, description: opt.description, requestedBy: name });
|
|
155
155
|
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
156
|
+
}
|
|
157
|
+
if (choices.length === 0)
|
|
158
|
+
return [];
|
|
159
|
+
if (options.yes)
|
|
160
|
+
return [];
|
|
161
|
+
if (options.all)
|
|
162
|
+
return choices.map(c => c.name);
|
|
163
|
+
const { selected } = await prompts({
|
|
164
|
+
type: 'multiselect',
|
|
165
|
+
name: 'selected',
|
|
166
|
+
message: 'Optional companion components available:',
|
|
167
|
+
choices: choices.map(c => ({
|
|
168
|
+
title: c.name + ' ' + chalk.dim('- ' + c.description + ' (for ' + c.requestedBy + ')'),
|
|
169
|
+
value: c.name,
|
|
170
|
+
})),
|
|
171
|
+
hint: '- Space to select, Enter to confirm (or press Enter to skip)',
|
|
172
|
+
});
|
|
173
|
+
return selected || [];
|
|
174
|
+
}
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Conflict detection
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
async function checkFileConflict(file, targetDir, options, utilsAlias, contentCache) {
|
|
179
|
+
const targetPath = path.join(targetDir, file);
|
|
180
|
+
if (!await fs.pathExists(targetPath)) {
|
|
181
|
+
return 'missing';
|
|
182
|
+
}
|
|
183
|
+
const localContent = await fs.readFile(targetPath, 'utf-8');
|
|
184
|
+
try {
|
|
185
|
+
const remoteContent = await fetchAndTransform(file, options, utilsAlias);
|
|
186
|
+
contentCache.set(file, remoteContent);
|
|
187
|
+
return normalizeContent(localContent) === normalizeContent(remoteContent)
|
|
188
|
+
? 'identical'
|
|
189
|
+
: 'changed';
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return 'changed';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function checkPeerFiles(component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate) {
|
|
196
|
+
if (!component.peerFiles)
|
|
197
|
+
return false;
|
|
198
|
+
let hasChanges = false;
|
|
199
|
+
for (const file of component.peerFiles) {
|
|
200
|
+
const status = await checkFileConflict(file, targetDir, options, utilsAlias, contentCache);
|
|
201
|
+
if (status === 'changed') {
|
|
202
|
+
hasChanges = true;
|
|
203
|
+
peerFilesToUpdate.add(file);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return hasChanges;
|
|
207
|
+
}
|
|
208
|
+
async function classifyComponent(name, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate) {
|
|
209
|
+
const component = registry[name];
|
|
210
|
+
let hasChanges = false;
|
|
211
|
+
let isFullyPresent = true;
|
|
212
|
+
for (const file of component.files) {
|
|
213
|
+
const status = await checkFileConflict(file, targetDir, options, utilsAlias, contentCache);
|
|
214
|
+
if (status === 'missing')
|
|
215
|
+
isFullyPresent = false;
|
|
216
|
+
if (status === 'changed')
|
|
217
|
+
hasChanges = true;
|
|
218
|
+
}
|
|
219
|
+
const peerChanged = await checkPeerFiles(component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate);
|
|
220
|
+
if (peerChanged)
|
|
221
|
+
hasChanges = true;
|
|
222
|
+
if (isFullyPresent && !hasChanges)
|
|
223
|
+
return 'skip';
|
|
224
|
+
if (hasChanges)
|
|
225
|
+
return 'conflict';
|
|
226
|
+
return 'install';
|
|
227
|
+
}
|
|
228
|
+
async function detectConflicts(allComponents, targetDir, options, utilsAlias) {
|
|
229
|
+
const toInstall = [];
|
|
230
|
+
const toSkip = [];
|
|
231
|
+
const conflicting = [];
|
|
232
|
+
const peerFilesToUpdate = new Set();
|
|
164
233
|
const contentCache = new Map();
|
|
165
|
-
const checkSpinner = ora('Checking for conflicts...').start();
|
|
166
234
|
for (const name of allComponents) {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
235
|
+
const result = await classifyComponent(name, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate);
|
|
236
|
+
if (result === 'skip')
|
|
237
|
+
toSkip.push(name);
|
|
238
|
+
else if (result === 'conflict')
|
|
239
|
+
conflicting.push(name);
|
|
240
|
+
else
|
|
241
|
+
toInstall.push(name);
|
|
242
|
+
}
|
|
243
|
+
return { toInstall, toSkip, conflicting, peerFilesToUpdate, contentCache };
|
|
244
|
+
}
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Overwrite prompt
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
async function promptOverwrite(conflicting, options) {
|
|
249
|
+
if (conflicting.length === 0)
|
|
250
|
+
return [];
|
|
251
|
+
if (options.overwrite)
|
|
252
|
+
return conflicting;
|
|
253
|
+
if (options.yes)
|
|
254
|
+
return [];
|
|
255
|
+
console.log(chalk.yellow(`\n${conflicting.length} component(s) have local changes or are different from remote.`));
|
|
256
|
+
const { selected } = await prompts({
|
|
257
|
+
type: 'multiselect',
|
|
258
|
+
name: 'selected',
|
|
259
|
+
message: 'Select components to OVERWRITE (Unselected will be skipped):',
|
|
260
|
+
choices: conflicting.map(name => ({ title: name, value: name })),
|
|
261
|
+
hint: '- Space to select, Enter to confirm',
|
|
262
|
+
});
|
|
263
|
+
return selected || [];
|
|
264
|
+
}
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// File writing
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
async function writeComponentFiles(component, targetDir, options, utilsAlias, contentCache, spinner) {
|
|
269
|
+
let success = true;
|
|
270
|
+
for (const file of component.files) {
|
|
271
|
+
const targetPath = path.join(targetDir, file);
|
|
272
|
+
try {
|
|
273
|
+
const content = contentCache.get(file)
|
|
274
|
+
?? await fetchAndTransform(file, options, utilsAlias);
|
|
275
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
276
|
+
await fs.writeFile(targetPath, content);
|
|
193
277
|
}
|
|
194
|
-
|
|
195
|
-
|
|
278
|
+
catch (err) {
|
|
279
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
280
|
+
spinner.warn(`Could not add ${file}: ${message}`);
|
|
281
|
+
success = false;
|
|
196
282
|
}
|
|
197
|
-
|
|
198
|
-
|
|
283
|
+
}
|
|
284
|
+
return success;
|
|
285
|
+
}
|
|
286
|
+
async function writePeerFiles(component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate, spinner) {
|
|
287
|
+
if (!component.peerFiles)
|
|
288
|
+
return;
|
|
289
|
+
for (const file of component.peerFiles) {
|
|
290
|
+
if (!peerFilesToUpdate.has(file))
|
|
291
|
+
continue;
|
|
292
|
+
const targetPath = path.join(targetDir, file);
|
|
293
|
+
try {
|
|
294
|
+
const content = contentCache.get(file)
|
|
295
|
+
?? await fetchAndTransform(file, options, utilsAlias);
|
|
296
|
+
await fs.writeFile(targetPath, content);
|
|
297
|
+
spinner.text = `Updated peer file ${file}`;
|
|
199
298
|
}
|
|
200
|
-
|
|
201
|
-
|
|
299
|
+
catch (err) {
|
|
300
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
301
|
+
spinner.warn(`Could not update peer file ${file}: ${message}`);
|
|
202
302
|
}
|
|
203
303
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
304
|
+
}
|
|
305
|
+
async function installLibFiles(allComponents, cwd, libDir, options) {
|
|
306
|
+
const required = new Set();
|
|
307
|
+
for (const name of allComponents) {
|
|
308
|
+
registry[name].libFiles?.forEach(f => required.add(f));
|
|
309
|
+
}
|
|
310
|
+
if (required.size === 0)
|
|
311
|
+
return;
|
|
312
|
+
await fs.ensureDir(libDir);
|
|
313
|
+
for (const libFile of required) {
|
|
314
|
+
const targetPath = path.join(libDir, libFile);
|
|
315
|
+
if (!await fs.pathExists(targetPath) || options.overwrite) {
|
|
316
|
+
try {
|
|
317
|
+
const content = await fetchLibContent(libFile, options);
|
|
318
|
+
await fs.writeFile(targetPath, content);
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
322
|
+
console.warn(chalk.yellow(`Could not install lib file ${libFile}: ${message}`));
|
|
323
|
+
}
|
|
209
324
|
}
|
|
210
|
-
|
|
211
|
-
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function installNpmDependencies(finalComponents, cwd) {
|
|
328
|
+
const deps = new Set();
|
|
329
|
+
for (const name of finalComponents) {
|
|
330
|
+
registry[name].npmDependencies?.forEach(dep => deps.add(dep));
|
|
331
|
+
}
|
|
332
|
+
if (deps.size === 0)
|
|
333
|
+
return;
|
|
334
|
+
const spinner = ora('Installing dependencies...').start();
|
|
335
|
+
try {
|
|
336
|
+
await installPackages(Array.from(deps), { cwd });
|
|
337
|
+
spinner.succeed('Dependencies installed.');
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
spinner.fail('Failed to install dependencies.');
|
|
341
|
+
console.error(e);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function ensureShortcutService(targetDir, cwd, config, options) {
|
|
345
|
+
const entries = collectInstalledShortcutEntries(targetDir);
|
|
346
|
+
if (entries.length > 0) {
|
|
347
|
+
const libDir = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils));
|
|
348
|
+
const servicePath = path.join(libDir, 'shortcut-binding.service.ts');
|
|
349
|
+
if (!await fs.pathExists(servicePath)) {
|
|
350
|
+
const content = await fetchLibContent('shortcut-binding.service.ts', options);
|
|
351
|
+
await fs.ensureDir(libDir);
|
|
352
|
+
await fs.writeFile(servicePath, content);
|
|
212
353
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
componentsToOverwrite = selected || [];
|
|
354
|
+
}
|
|
355
|
+
await writeShortcutRegistryIndex(cwd, config, entries);
|
|
356
|
+
}
|
|
357
|
+
function collectInstalledShortcutEntries(targetDir) {
|
|
358
|
+
const entries = [];
|
|
359
|
+
for (const definition of Object.values(registry)) {
|
|
360
|
+
if (!definition.shortcutDefinitions?.length)
|
|
361
|
+
continue;
|
|
362
|
+
for (const sd of definition.shortcutDefinitions) {
|
|
363
|
+
if (fs.existsSync(path.join(targetDir, sd.sourceFile))) {
|
|
364
|
+
entries.push(sd);
|
|
365
|
+
}
|
|
226
366
|
}
|
|
227
367
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
console.log(chalk.
|
|
368
|
+
return entries;
|
|
369
|
+
}
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Main entry point
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
export async function add(components, options) {
|
|
374
|
+
const cwd = process.cwd();
|
|
375
|
+
const config = await getConfig(cwd);
|
|
376
|
+
if (!config) {
|
|
377
|
+
console.log(chalk.red('Error: components.json not found.'));
|
|
378
|
+
console.log(chalk.dim('Run `npx @gilav21/shadcn-angular init` first.'));
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
const componentsToAdd = await selectComponents(components, options);
|
|
382
|
+
if (!componentsToAdd || componentsToAdd.length === 0) {
|
|
383
|
+
console.log(chalk.dim('No components selected.'));
|
|
238
384
|
return;
|
|
239
385
|
}
|
|
386
|
+
validateComponents(componentsToAdd);
|
|
387
|
+
const resolvedComponents = resolveDependencies(componentsToAdd);
|
|
388
|
+
const optionalChoices = await promptOptionalDependencies(resolvedComponents, options);
|
|
389
|
+
const allComponents = optionalChoices.length > 0
|
|
390
|
+
? resolveDependencies([...resolvedComponents, ...optionalChoices])
|
|
391
|
+
: resolvedComponents;
|
|
392
|
+
const uiBasePath = options.path ?? aliasToProjectPath(config.aliases.ui || 'src/components/ui');
|
|
393
|
+
const targetDir = resolveProjectPath(cwd, uiBasePath);
|
|
394
|
+
const utilsAlias = config.aliases.utils;
|
|
395
|
+
// Detect conflicts
|
|
396
|
+
const checkSpinner = ora('Checking for conflicts...').start();
|
|
397
|
+
const { toInstall, toSkip, conflicting, peerFilesToUpdate, contentCache } = await detectConflicts(allComponents, targetDir, options, utilsAlias);
|
|
398
|
+
checkSpinner.stop();
|
|
399
|
+
// Prompt user for overwrite decisions
|
|
400
|
+
const toOverwrite = await promptOverwrite(conflicting, options);
|
|
401
|
+
const finalComponents = [...toInstall, ...toOverwrite];
|
|
240
402
|
if (finalComponents.length === 0) {
|
|
241
|
-
|
|
403
|
+
if (toSkip.length > 0) {
|
|
404
|
+
console.log(chalk.green(`\nAll components are up to date! (${toSkip.length} skipped)`));
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
console.log(chalk.dim('\nNo components to install.'));
|
|
408
|
+
}
|
|
242
409
|
return;
|
|
243
410
|
}
|
|
411
|
+
// Install component files
|
|
244
412
|
const spinner = ora('Installing components...').start();
|
|
245
413
|
let successCount = 0;
|
|
246
414
|
try {
|
|
247
415
|
await fs.ensureDir(targetDir);
|
|
248
416
|
for (const name of finalComponents) {
|
|
249
417
|
const component = registry[name];
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
let content = contentCache.get(file);
|
|
255
|
-
if (!content) {
|
|
256
|
-
content = await fetchComponentContent(file, options);
|
|
257
|
-
// Transform all lib/ imports if not already transformed (cached is transformed)
|
|
258
|
-
content = content.replace(/(\.\.\/)+lib\//g, config.aliases.utils + '/');
|
|
259
|
-
}
|
|
260
|
-
await fs.ensureDir(path.dirname(targetPath));
|
|
261
|
-
await fs.writeFile(targetPath, content);
|
|
262
|
-
// spinner.text = `Added ${file}`; // Too verbose?
|
|
263
|
-
}
|
|
264
|
-
catch (err) {
|
|
265
|
-
spinner.warn(`Could not add ${file}: ${err.message}`);
|
|
266
|
-
componentSuccess = false;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (componentSuccess) {
|
|
418
|
+
const ok = await writeComponentFiles(component, targetDir, options, utilsAlias, contentCache, spinner);
|
|
419
|
+
await writePeerFiles(component, targetDir, options, utilsAlias, contentCache, peerFilesToUpdate, spinner);
|
|
420
|
+
if (ok) {
|
|
270
421
|
successCount++;
|
|
271
422
|
spinner.text = `Added ${name}`;
|
|
272
423
|
}
|
|
@@ -274,75 +425,19 @@ export async function add(components, options) {
|
|
|
274
425
|
if (successCount > 0) {
|
|
275
426
|
spinner.succeed(chalk.green(`Success! Added ${successCount} component(s)`));
|
|
276
427
|
console.log('\n' + chalk.dim('Components added:'));
|
|
277
|
-
finalComponents.forEach(name =>
|
|
278
|
-
console.log(chalk.dim(' - ') + chalk.cyan(name));
|
|
279
|
-
});
|
|
428
|
+
finalComponents.forEach(name => console.log(chalk.dim(' - ') + chalk.cyan(name)));
|
|
280
429
|
}
|
|
281
430
|
else {
|
|
282
431
|
spinner.info('No new components installed.');
|
|
283
432
|
}
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
component.libFiles.forEach(f => requiredLibFiles.add(f));
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
if (requiredLibFiles.size > 0) {
|
|
294
|
-
const libDir = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils));
|
|
295
|
-
await fs.ensureDir(libDir);
|
|
296
|
-
for (const libFile of requiredLibFiles) {
|
|
297
|
-
const libTargetPath = path.join(libDir, libFile);
|
|
298
|
-
if (!await fs.pathExists(libTargetPath) || options.overwrite) {
|
|
299
|
-
try {
|
|
300
|
-
const libContent = await fetchLibContent(libFile, options);
|
|
301
|
-
await fs.writeFile(libTargetPath, libContent);
|
|
302
|
-
}
|
|
303
|
-
catch (err) {
|
|
304
|
-
console.warn(chalk.yellow(`Could not install lib file ${libFile}: ${err.message}`));
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
if (finalComponents.length > 0) {
|
|
311
|
-
const npmDependencies = new Set();
|
|
312
|
-
for (const name of finalComponents) {
|
|
313
|
-
const component = registry[name];
|
|
314
|
-
if (component.npmDependencies) {
|
|
315
|
-
component.npmDependencies.forEach(dep => npmDependencies.add(dep));
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
if (npmDependencies.size > 0) {
|
|
319
|
-
const depSpinner = ora('Installing dependencies...').start();
|
|
320
|
-
try {
|
|
321
|
-
await installPackages(Array.from(npmDependencies), { cwd });
|
|
322
|
-
depSpinner.succeed('Dependencies installed.');
|
|
323
|
-
}
|
|
324
|
-
catch (e) {
|
|
325
|
-
depSpinner.fail('Failed to install dependencies.');
|
|
326
|
-
console.error(e);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
const shortcutEntries = collectInstalledShortcutEntries(targetDir);
|
|
331
|
-
if (shortcutEntries.length > 0) {
|
|
332
|
-
const libDir2 = resolveProjectPath(cwd, aliasToProjectPath(config.aliases.utils));
|
|
333
|
-
const shortcutServicePath = path.join(libDir2, 'shortcut-binding.service.ts');
|
|
334
|
-
if (!await fs.pathExists(shortcutServicePath)) {
|
|
335
|
-
const shortcutServiceContent = await fetchLibContent('shortcut-binding.service.ts', options);
|
|
336
|
-
await fs.ensureDir(libDir2);
|
|
337
|
-
await fs.writeFile(shortcutServicePath, shortcutServiceContent);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
await writeShortcutRegistryIndex(cwd, config, shortcutEntries);
|
|
341
|
-
if (componentsToSkip.length > 0) {
|
|
433
|
+
// Post-install: lib files, npm deps, shortcuts
|
|
434
|
+
const libDir = resolveProjectPath(cwd, aliasToProjectPath(utilsAlias));
|
|
435
|
+
await installLibFiles(allComponents, cwd, libDir, options);
|
|
436
|
+
await installNpmDependencies(finalComponents, cwd);
|
|
437
|
+
await ensureShortcutService(targetDir, cwd, config, options);
|
|
438
|
+
if (toSkip.length > 0) {
|
|
342
439
|
console.log('\n' + chalk.dim('Components skipped (up to date):'));
|
|
343
|
-
|
|
344
|
-
console.log(chalk.dim(' - ') + chalk.gray(name));
|
|
345
|
-
});
|
|
440
|
+
toSkip.forEach(name => console.log(chalk.dim(' - ') + chalk.gray(name)));
|
|
346
441
|
}
|
|
347
442
|
console.log('');
|
|
348
443
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|