weloop-kosign 1.1.0 → 1.1.1
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/package.json +1 -1
- package/scripts/cli-remote.js +482 -56
package/package.json
CHANGED
package/scripts/cli-remote.js
CHANGED
|
@@ -33,21 +33,43 @@ const { execSync } = require('child_process');
|
|
|
33
33
|
// CONFIGURATION
|
|
34
34
|
// ============================================================================
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Determines the default registry URL to use.
|
|
38
|
+
*
|
|
39
|
+
* Priority:
|
|
40
|
+
* 1. Local registry directory (if running in the component library project)
|
|
41
|
+
* 2. Environment variable WELOOP_REGISTRY_URL
|
|
42
|
+
* 3. Default remote GitLab URL
|
|
43
|
+
*
|
|
44
|
+
* @returns {string} The registry URL or path
|
|
45
|
+
*/
|
|
36
46
|
function getDefaultRegistryUrl() {
|
|
37
47
|
const localRegistryPath = path.join(__dirname, '../registry');
|
|
48
|
+
|
|
49
|
+
// Check if we're running in the component library project itself
|
|
38
50
|
if (fs.existsSync(localRegistryPath) && fs.existsSync(path.join(localRegistryPath, 'index.json'))) {
|
|
39
51
|
return localRegistryPath;
|
|
40
52
|
}
|
|
53
|
+
|
|
54
|
+
// Fall back to environment variable or default remote URL
|
|
41
55
|
return process.env.WELOOP_REGISTRY_URL ||
|
|
42
56
|
'https://gitlab.com/Sophanithchrek/weloop-shadcn-next-app/-/raw/main/registry';
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
const DEFAULT_REGISTRY_URL = getDefaultRegistryUrl();
|
|
46
60
|
|
|
61
|
+
// Network timeout constants (in milliseconds)
|
|
62
|
+
const REQUEST_TIMEOUT = 15000;
|
|
63
|
+
const RETRY_DELAY = 1000;
|
|
64
|
+
|
|
47
65
|
// ============================================================================
|
|
48
|
-
// UTILITIES
|
|
66
|
+
// UTILITIES - Logging and File System Helpers
|
|
49
67
|
// ============================================================================
|
|
50
68
|
|
|
69
|
+
/**
|
|
70
|
+
* ANSI color codes for terminal output
|
|
71
|
+
* Makes the CLI output more readable with colored messages
|
|
72
|
+
*/
|
|
51
73
|
const colors = {
|
|
52
74
|
reset: '\x1b[0m',
|
|
53
75
|
green: '\x1b[32m',
|
|
@@ -57,52 +79,98 @@ const colors = {
|
|
|
57
79
|
cyan: '\x1b[36m',
|
|
58
80
|
};
|
|
59
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Logs a message with optional color formatting
|
|
84
|
+
* @param {string} message - The message to log
|
|
85
|
+
* @param {string} color - Color name from colors object
|
|
86
|
+
*/
|
|
60
87
|
function log(message, color = 'reset') {
|
|
61
88
|
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
62
89
|
}
|
|
63
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Logs an error message in red
|
|
93
|
+
*/
|
|
64
94
|
function error(message) {
|
|
65
95
|
log(`Error: ${message}`, 'red');
|
|
66
96
|
}
|
|
67
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Logs a success message in green
|
|
100
|
+
*/
|
|
68
101
|
function success(message) {
|
|
69
102
|
log(message, 'green');
|
|
70
103
|
}
|
|
71
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Logs an informational message in blue
|
|
107
|
+
*/
|
|
72
108
|
function info(message) {
|
|
73
109
|
log(message, 'blue');
|
|
74
110
|
}
|
|
75
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Logs a warning message in yellow
|
|
114
|
+
*/
|
|
76
115
|
function warn(message) {
|
|
77
116
|
log(`Warning: ${message}`, 'yellow');
|
|
78
117
|
}
|
|
79
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Creates a directory if it doesn't exist
|
|
121
|
+
* Uses recursive option to create parent directories as needed
|
|
122
|
+
* @param {string} dirPath - Path to the directory
|
|
123
|
+
*/
|
|
80
124
|
function ensureDirectoryExists(dirPath) {
|
|
81
125
|
if (!fs.existsSync(dirPath)) {
|
|
82
126
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
83
127
|
}
|
|
84
128
|
}
|
|
85
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Checks if a path is a local file path or a remote URL
|
|
132
|
+
* @param {string} pathOrUrl - Path or URL to check
|
|
133
|
+
* @returns {boolean} True if it's a local path, false if it's a URL
|
|
134
|
+
*/
|
|
86
135
|
function isLocalPath(pathOrUrl) {
|
|
87
136
|
return !pathOrUrl.startsWith('http://') && !pathOrUrl.startsWith('https://');
|
|
88
137
|
}
|
|
89
138
|
|
|
90
139
|
// ============================================================================
|
|
91
|
-
// PACKAGE MANAGEMENT
|
|
140
|
+
// PACKAGE MANAGEMENT - Detecting and Installing Dependencies
|
|
92
141
|
// ============================================================================
|
|
93
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Automatically detects which package manager the project is using
|
|
145
|
+
*
|
|
146
|
+
* Detection order:
|
|
147
|
+
* 1. Check for lock files (most reliable)
|
|
148
|
+
* 2. Check npm user agent (fallback)
|
|
149
|
+
* 3. Default to npm
|
|
150
|
+
*
|
|
151
|
+
* @returns {string} Package manager name: 'npm', 'yarn', or 'pnpm'
|
|
152
|
+
*/
|
|
94
153
|
function detectPackageManager() {
|
|
154
|
+
// Check for lock files first (most reliable indicator)
|
|
95
155
|
if (fs.existsSync(path.join(process.cwd(), 'yarn.lock'))) return 'yarn';
|
|
96
156
|
if (fs.existsSync(path.join(process.cwd(), 'pnpm-lock.yaml'))) return 'pnpm';
|
|
97
157
|
if (fs.existsSync(path.join(process.cwd(), 'package-lock.json'))) return 'npm';
|
|
98
158
|
|
|
159
|
+
// Fallback: check npm user agent (set by npm/yarn/pnpm when running)
|
|
99
160
|
const userAgent = process.env.npm_config_user_agent || '';
|
|
100
161
|
if (userAgent.includes('yarn')) return 'yarn';
|
|
101
162
|
if (userAgent.includes('pnpm')) return 'pnpm';
|
|
102
163
|
|
|
164
|
+
// Default to npm if we can't detect anything
|
|
103
165
|
return 'npm';
|
|
104
166
|
}
|
|
105
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Generates the install command for the given package manager
|
|
170
|
+
* @param {string} packageManager - 'npm', 'yarn', or 'pnpm'
|
|
171
|
+
* @param {string[]} packages - Array of package names to install
|
|
172
|
+
* @returns {string} The install command to run
|
|
173
|
+
*/
|
|
106
174
|
function getInstallCommand(packageManager, packages) {
|
|
107
175
|
const packagesStr = packages.join(' ');
|
|
108
176
|
switch (packageManager) {
|
|
@@ -113,36 +181,63 @@ function getInstallCommand(packageManager, packages) {
|
|
|
113
181
|
}
|
|
114
182
|
}
|
|
115
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Checks if a package is already installed in the project
|
|
186
|
+
* Searches both dependencies and devDependencies
|
|
187
|
+
* @param {string} packageName - Name of the package to check
|
|
188
|
+
* @returns {boolean} True if package is installed, false otherwise
|
|
189
|
+
*/
|
|
116
190
|
function checkPackageInstalled(packageName) {
|
|
117
191
|
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
118
192
|
if (!fs.existsSync(packageJsonPath)) return false;
|
|
119
193
|
|
|
120
194
|
try {
|
|
121
195
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
196
|
+
// Check both dependencies and devDependencies
|
|
122
197
|
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
123
198
|
return !!allDeps[packageName];
|
|
124
199
|
} catch (e) {
|
|
200
|
+
// If we can't read/parse package.json, assume package is not installed
|
|
125
201
|
return false;
|
|
126
202
|
}
|
|
127
203
|
}
|
|
128
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Filters out dependencies that are already installed
|
|
207
|
+
* Handles scoped packages (e.g., @radix-ui/react-button)
|
|
208
|
+
* @param {string[]} requiredDeps - List of required package names
|
|
209
|
+
* @returns {string[]} List of packages that need to be installed
|
|
210
|
+
*/
|
|
129
211
|
function getMissingDependencies(requiredDeps) {
|
|
130
212
|
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
131
|
-
if (!fs.existsSync(packageJsonPath))
|
|
213
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
214
|
+
// No package.json means we need to install everything
|
|
215
|
+
return requiredDeps;
|
|
216
|
+
}
|
|
132
217
|
|
|
133
218
|
try {
|
|
134
219
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
135
220
|
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
136
221
|
|
|
137
222
|
return requiredDeps.filter(dep => {
|
|
223
|
+
// For scoped packages like @radix-ui/react-button, also check @radix-ui
|
|
138
224
|
const depName = dep.split('/').slice(0, 2).join('/');
|
|
139
225
|
return !allDeps[dep] && !allDeps[depName];
|
|
140
226
|
});
|
|
141
227
|
} catch (e) {
|
|
228
|
+
// If we can't parse package.json, assume all deps are missing
|
|
142
229
|
return requiredDeps;
|
|
143
230
|
}
|
|
144
231
|
}
|
|
145
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Installs npm packages using the detected package manager
|
|
235
|
+
*
|
|
236
|
+
* For npm: Filters out ERESOLVE peer dependency warnings (common false positives)
|
|
237
|
+
* For yarn/pnpm: Uses standard execSync
|
|
238
|
+
*
|
|
239
|
+
* @param {string[]} packages - Array of package names to install
|
|
240
|
+
*/
|
|
146
241
|
async function installPackages(packages) {
|
|
147
242
|
if (packages.length === 0) return;
|
|
148
243
|
|
|
@@ -152,7 +247,8 @@ async function installPackages(packages) {
|
|
|
152
247
|
try {
|
|
153
248
|
const installCmd = getInstallCommand(packageManager, packages);
|
|
154
249
|
|
|
155
|
-
//
|
|
250
|
+
// npm has noisy ERESOLVE warnings that aren't real errors
|
|
251
|
+
// We filter these out while keeping actual error messages
|
|
156
252
|
if (packageManager === 'npm') {
|
|
157
253
|
const { spawn } = require('child_process');
|
|
158
254
|
const [cmd, ...args] = installCmd.split(' ');
|
|
@@ -167,7 +263,8 @@ async function installPackages(packages) {
|
|
|
167
263
|
let stderr = '';
|
|
168
264
|
proc.stderr.on('data', (data) => {
|
|
169
265
|
const output = data.toString();
|
|
170
|
-
// Filter out ERESOLVE warnings
|
|
266
|
+
// Filter out ERESOLVE peer dependency warnings (these are usually safe to ignore)
|
|
267
|
+
// but keep actual error messages visible to the user
|
|
171
268
|
if (!output.includes('ERESOLVE overriding peer dependency') &&
|
|
172
269
|
!output.includes('npm warn ERESOLVE')) {
|
|
173
270
|
process.stderr.write(data);
|
|
@@ -177,13 +274,14 @@ async function installPackages(packages) {
|
|
|
177
274
|
|
|
178
275
|
proc.on('close', (code) => {
|
|
179
276
|
if (code !== 0) {
|
|
180
|
-
//
|
|
277
|
+
// npm sometimes exits with non-zero code due to warnings, not real errors
|
|
278
|
+
// Check if there's an actual error message (not just ERESOLVE warnings)
|
|
181
279
|
const hasRealError = stderr.includes('npm error') &&
|
|
182
280
|
!stderr.match(/npm error.*ERESOLVE/);
|
|
183
281
|
if (hasRealError) {
|
|
184
282
|
reject(new Error(`Installation failed with code ${code}`));
|
|
185
283
|
} else {
|
|
186
|
-
// Installation succeeded despite warnings
|
|
284
|
+
// Installation succeeded despite warnings - this is common with peer deps
|
|
187
285
|
resolve();
|
|
188
286
|
}
|
|
189
287
|
} else {
|
|
@@ -206,13 +304,22 @@ async function installPackages(packages) {
|
|
|
206
304
|
}
|
|
207
305
|
|
|
208
306
|
// ============================================================================
|
|
209
|
-
// FILE OPERATIONS
|
|
307
|
+
// FILE OPERATIONS - Configuration and File Handling
|
|
210
308
|
// ============================================================================
|
|
211
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Creates a default components.json configuration file
|
|
312
|
+
*
|
|
313
|
+
* Auto-detects Next.js app directory structure and sets appropriate paths.
|
|
314
|
+
* This file stores project-specific settings like component paths and aliases.
|
|
315
|
+
*
|
|
316
|
+
* @returns {object} The default configuration object
|
|
317
|
+
*/
|
|
212
318
|
function createDefaultComponentsConfig() {
|
|
213
319
|
const configPath = path.join(process.cwd(), 'components.json');
|
|
214
320
|
|
|
215
|
-
// Detect
|
|
321
|
+
// Detect Next.js project structure
|
|
322
|
+
// App Router uses 'app/globals.css', Pages Router uses 'styles/globals.css'
|
|
216
323
|
const hasAppDir = fs.existsSync(path.join(process.cwd(), 'app'));
|
|
217
324
|
const cssPath = hasAppDir ? 'app/globals.css' : 'styles/globals.css';
|
|
218
325
|
|
|
@@ -245,6 +352,12 @@ function createDefaultComponentsConfig() {
|
|
|
245
352
|
return defaultConfig;
|
|
246
353
|
}
|
|
247
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Loads the components.json configuration file
|
|
357
|
+
* Creates a default one if it doesn't exist
|
|
358
|
+
*
|
|
359
|
+
* @returns {object} The configuration object
|
|
360
|
+
*/
|
|
248
361
|
function loadComponentsConfig() {
|
|
249
362
|
const configPath = path.join(process.cwd(), 'components.json');
|
|
250
363
|
|
|
@@ -257,10 +370,25 @@ function loadComponentsConfig() {
|
|
|
257
370
|
return JSON.parse(content);
|
|
258
371
|
}
|
|
259
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Fetches and parses a JSON file from a local path or remote URL
|
|
375
|
+
*
|
|
376
|
+
* Features:
|
|
377
|
+
* - Handles local files and remote URLs
|
|
378
|
+
* - Automatic retry on network errors
|
|
379
|
+
* - Follows HTTP redirects
|
|
380
|
+
* - Provides helpful error messages
|
|
381
|
+
*
|
|
382
|
+
* @param {string} urlOrPath - Local file path or remote URL
|
|
383
|
+
* @param {number} retries - Number of retry attempts (default: 3)
|
|
384
|
+
* @returns {Promise<object>} Parsed JSON object
|
|
385
|
+
*/
|
|
260
386
|
async function fetchJSON(urlOrPath, retries = 3) {
|
|
387
|
+
// Handle local file paths
|
|
261
388
|
if (isLocalPath(urlOrPath)) {
|
|
262
389
|
return new Promise((resolve, reject) => {
|
|
263
390
|
try {
|
|
391
|
+
// Resolve relative paths to absolute
|
|
264
392
|
const fullPath = path.isAbsolute(urlOrPath)
|
|
265
393
|
? urlOrPath
|
|
266
394
|
: path.join(process.cwd(), urlOrPath);
|
|
@@ -278,6 +406,7 @@ async function fetchJSON(urlOrPath, retries = 3) {
|
|
|
278
406
|
});
|
|
279
407
|
}
|
|
280
408
|
|
|
409
|
+
// Handle remote URLs
|
|
281
410
|
return new Promise((resolve, reject) => {
|
|
282
411
|
const client = urlOrPath.startsWith('https') ? https : http;
|
|
283
412
|
let timeout;
|
|
@@ -287,10 +416,12 @@ async function fetchJSON(urlOrPath, retries = 3) {
|
|
|
287
416
|
request = client.get(urlOrPath, (res) => {
|
|
288
417
|
clearTimeout(timeout);
|
|
289
418
|
|
|
419
|
+
// Handle HTTP redirects (301, 302)
|
|
290
420
|
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
291
421
|
return fetchJSON(res.headers.location, retries).then(resolve).catch(reject);
|
|
292
422
|
}
|
|
293
423
|
|
|
424
|
+
// Provide helpful error messages for common HTTP status codes
|
|
294
425
|
if (res.statusCode === 403) {
|
|
295
426
|
reject(new Error(`Access forbidden (403). Repository may be private. Make it public in GitLab/GitHub settings, or use --registry with a public URL.`));
|
|
296
427
|
return;
|
|
@@ -306,10 +437,12 @@ async function fetchJSON(urlOrPath, retries = 3) {
|
|
|
306
437
|
return;
|
|
307
438
|
}
|
|
308
439
|
|
|
440
|
+
// Collect response data
|
|
309
441
|
let data = '';
|
|
310
442
|
res.on('data', (chunk) => { data += chunk; });
|
|
311
443
|
res.on('end', () => {
|
|
312
444
|
try {
|
|
445
|
+
// Check if we got HTML instead of JSON (common when repo is private)
|
|
313
446
|
if (data.trim().startsWith('<')) {
|
|
314
447
|
reject(new Error('Received HTML instead of JSON. Repository may be private or URL incorrect.'));
|
|
315
448
|
return;
|
|
@@ -321,36 +454,46 @@ async function fetchJSON(urlOrPath, retries = 3) {
|
|
|
321
454
|
});
|
|
322
455
|
});
|
|
323
456
|
|
|
457
|
+
// Handle network errors with automatic retry
|
|
324
458
|
request.on('error', (err) => {
|
|
325
459
|
clearTimeout(timeout);
|
|
326
460
|
if (retries > 0 && (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT' || err.code === 'ENOTFOUND')) {
|
|
327
461
|
info(`Connection error, retrying... (${retries} attempts left)`);
|
|
328
462
|
setTimeout(() => {
|
|
329
463
|
fetchJSON(urlOrPath, retries - 1).then(resolve).catch(reject);
|
|
330
|
-
},
|
|
464
|
+
}, RETRY_DELAY);
|
|
331
465
|
} else {
|
|
332
466
|
reject(new Error(`Network error: ${err.message} (${err.code || 'UNKNOWN'})`));
|
|
333
467
|
}
|
|
334
468
|
});
|
|
335
469
|
|
|
470
|
+
// Set request timeout with retry logic
|
|
336
471
|
timeout = setTimeout(() => {
|
|
337
472
|
request.destroy();
|
|
338
473
|
if (retries > 0) {
|
|
339
474
|
info(`Request timeout, retrying... (${retries} attempts left)`);
|
|
340
475
|
setTimeout(() => {
|
|
341
476
|
fetchJSON(urlOrPath, retries - 1).then(resolve).catch(reject);
|
|
342
|
-
},
|
|
477
|
+
}, RETRY_DELAY);
|
|
343
478
|
} else {
|
|
344
479
|
reject(new Error('Request timeout after multiple attempts'));
|
|
345
480
|
}
|
|
346
|
-
},
|
|
481
|
+
}, REQUEST_TIMEOUT);
|
|
347
482
|
};
|
|
348
483
|
|
|
349
484
|
makeRequest();
|
|
350
485
|
});
|
|
351
486
|
}
|
|
352
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Fetches text content (like CSS) from a local path or remote URL
|
|
490
|
+
* Similar to fetchJSON but returns raw text instead of parsing JSON
|
|
491
|
+
*
|
|
492
|
+
* @param {string} urlOrPath - Local file path or remote URL
|
|
493
|
+
* @returns {Promise<string>} The file content as text
|
|
494
|
+
*/
|
|
353
495
|
async function fetchText(urlOrPath) {
|
|
496
|
+
// Handle local file paths
|
|
354
497
|
if (isLocalPath(urlOrPath)) {
|
|
355
498
|
if (!fs.existsSync(urlOrPath)) {
|
|
356
499
|
throw new Error(`File not found: ${urlOrPath}`);
|
|
@@ -358,15 +501,18 @@ async function fetchText(urlOrPath) {
|
|
|
358
501
|
return fs.readFileSync(urlOrPath, 'utf-8');
|
|
359
502
|
}
|
|
360
503
|
|
|
504
|
+
// Handle remote URLs
|
|
361
505
|
return new Promise((resolve, reject) => {
|
|
362
506
|
const client = urlOrPath.startsWith('https') ? https : http;
|
|
363
507
|
let data = '';
|
|
364
508
|
|
|
365
509
|
const req = client.get(urlOrPath, (res) => {
|
|
510
|
+
// Follow redirects
|
|
366
511
|
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
367
512
|
return fetchText(res.headers.location).then(resolve).catch(reject);
|
|
368
513
|
}
|
|
369
514
|
|
|
515
|
+
// Handle common HTTP errors
|
|
370
516
|
if (res.statusCode === 403) {
|
|
371
517
|
reject(new Error('Access forbidden - repository may be private'));
|
|
372
518
|
return;
|
|
@@ -382,8 +528,10 @@ async function fetchText(urlOrPath) {
|
|
|
382
528
|
return;
|
|
383
529
|
}
|
|
384
530
|
|
|
531
|
+
// Collect response data
|
|
385
532
|
res.on('data', (chunk) => { data += chunk; });
|
|
386
533
|
res.on('end', () => {
|
|
534
|
+
// Check if we got HTML instead of CSS (common when repo is private)
|
|
387
535
|
if (data.trim().startsWith('<')) {
|
|
388
536
|
reject(new Error('Received HTML instead of CSS'));
|
|
389
537
|
return;
|
|
@@ -392,42 +540,70 @@ async function fetchText(urlOrPath) {
|
|
|
392
540
|
});
|
|
393
541
|
});
|
|
394
542
|
|
|
543
|
+
// Handle network errors with automatic retry
|
|
395
544
|
req.on('error', (err) => {
|
|
396
545
|
if (err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT') {
|
|
397
546
|
setTimeout(() => {
|
|
398
547
|
fetchText(urlOrPath).then(resolve).catch(reject);
|
|
399
|
-
},
|
|
548
|
+
}, RETRY_DELAY);
|
|
400
549
|
} else {
|
|
401
550
|
reject(err);
|
|
402
551
|
}
|
|
403
552
|
});
|
|
404
553
|
|
|
405
|
-
|
|
554
|
+
// Set request timeout
|
|
555
|
+
req.setTimeout(REQUEST_TIMEOUT, () => {
|
|
406
556
|
req.destroy();
|
|
407
557
|
reject(new Error('Request timeout'));
|
|
408
558
|
});
|
|
409
559
|
});
|
|
410
560
|
}
|
|
411
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Resolves the full path/URL for a registry file
|
|
564
|
+
* Handles both local paths and remote URLs
|
|
565
|
+
*
|
|
566
|
+
* @param {string} baseUrl - Base registry URL or path
|
|
567
|
+
* @param {string} fileName - Name of the file to resolve
|
|
568
|
+
* @returns {string} Full path or URL to the file
|
|
569
|
+
*/
|
|
412
570
|
function resolveRegistryPath(baseUrl, fileName) {
|
|
413
571
|
if (isLocalPath(baseUrl)) {
|
|
572
|
+
// Resolve local paths (absolute or relative)
|
|
414
573
|
const basePath = path.isAbsolute(baseUrl)
|
|
415
574
|
? baseUrl
|
|
416
575
|
: path.join(process.cwd(), baseUrl);
|
|
417
576
|
return path.join(basePath, fileName);
|
|
418
577
|
}
|
|
578
|
+
// For remote URLs, just append the filename
|
|
419
579
|
return `${baseUrl}/${fileName}`;
|
|
420
580
|
}
|
|
421
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Loads a component registry file from the registry
|
|
584
|
+
* Returns null if component not found (doesn't throw)
|
|
585
|
+
*
|
|
586
|
+
* @param {string} componentName - Name of the component
|
|
587
|
+
* @param {string} registryBaseUrl - Base URL/path of the registry
|
|
588
|
+
* @returns {Promise<object|null>} Component registry object or null if not found
|
|
589
|
+
*/
|
|
422
590
|
async function loadRegistry(componentName, registryBaseUrl) {
|
|
423
591
|
try {
|
|
424
592
|
const registryPath = resolveRegistryPath(registryBaseUrl, `${componentName}.json`);
|
|
425
593
|
return await fetchJSON(registryPath);
|
|
426
594
|
} catch (err) {
|
|
595
|
+
// Component not found - return null instead of throwing
|
|
427
596
|
return null;
|
|
428
597
|
}
|
|
429
598
|
}
|
|
430
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Loads the registry index file (lists all available components)
|
|
602
|
+
* Throws an error if index cannot be loaded
|
|
603
|
+
*
|
|
604
|
+
* @param {string} registryBaseUrl - Base URL/path of the registry
|
|
605
|
+
* @returns {Promise<object>} Index object containing list of components
|
|
606
|
+
*/
|
|
431
607
|
async function loadIndex(registryBaseUrl) {
|
|
432
608
|
const indexPath = resolveRegistryPath(registryBaseUrl, 'index.json');
|
|
433
609
|
try {
|
|
@@ -439,31 +615,47 @@ async function loadIndex(registryBaseUrl) {
|
|
|
439
615
|
}
|
|
440
616
|
|
|
441
617
|
// ============================================================================
|
|
442
|
-
// CSS PROCESSING
|
|
618
|
+
// CSS PROCESSING - Style Installation and Formatting
|
|
443
619
|
// ============================================================================
|
|
444
620
|
|
|
621
|
+
/**
|
|
622
|
+
* Checks if CSS content contains Weloop design system variables
|
|
623
|
+
* Used to detect if styles have already been installed
|
|
624
|
+
*
|
|
625
|
+
* @param {string} content - CSS content to check
|
|
626
|
+
* @returns {boolean} True if Weloop styles are present
|
|
627
|
+
*/
|
|
445
628
|
function hasWeloopStyles(content) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
content.includes('--WLDS-
|
|
449
|
-
content.includes('--
|
|
629
|
+
// Check for Weloop-specific CSS variable prefixes
|
|
630
|
+
return content.includes('--WLDS-PRM-') || // Primary colors
|
|
631
|
+
content.includes('--WLDS-RED-') || // Red colors
|
|
632
|
+
content.includes('--WLDS-NTL-') || // Neutral colors
|
|
633
|
+
content.includes('--system-100') || // System colors
|
|
450
634
|
content.includes('--system-200');
|
|
451
635
|
}
|
|
452
636
|
|
|
637
|
+
/**
|
|
638
|
+
* Removes duplicate tw-animate-css imports and keeps only one
|
|
639
|
+
* Ensures the import appears right after tailwindcss import
|
|
640
|
+
*
|
|
641
|
+
* @param {string} cssContent - CSS content to clean
|
|
642
|
+
* @returns {string} CSS content with duplicates removed
|
|
643
|
+
*/
|
|
453
644
|
function removeDuplicateTwAnimateImports(cssContent) {
|
|
454
645
|
const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
|
|
455
646
|
const matches = cssContent.match(twAnimatePattern);
|
|
456
647
|
|
|
457
648
|
if (matches && matches.length > 1) {
|
|
458
|
-
// Remove all occurrences
|
|
649
|
+
// Remove all occurrences first
|
|
459
650
|
let cleaned = cssContent.replace(twAnimatePattern, '');
|
|
460
|
-
// Add it back once after tailwindcss import
|
|
651
|
+
// Add it back once, right after tailwindcss import if it exists
|
|
461
652
|
if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
|
|
462
653
|
cleaned = cleaned.replace(
|
|
463
654
|
/(@import\s+["']tailwindcss["'];?\s*\n?)/,
|
|
464
655
|
'$1@import "tw-animate-css";\n'
|
|
465
656
|
);
|
|
466
657
|
} else {
|
|
658
|
+
// If no tailwindcss import, add it at the beginning
|
|
467
659
|
cleaned = '@import "tw-animate-css";\n' + cleaned;
|
|
468
660
|
}
|
|
469
661
|
return cleaned;
|
|
@@ -472,11 +664,19 @@ function removeDuplicateTwAnimateImports(cssContent) {
|
|
|
472
664
|
return cssContent;
|
|
473
665
|
}
|
|
474
666
|
|
|
667
|
+
/**
|
|
668
|
+
* Removes duplicate @custom-variant declarations
|
|
669
|
+
* Keeps only one instance at the top
|
|
670
|
+
*
|
|
671
|
+
* @param {string} cssContent - CSS content to clean
|
|
672
|
+
* @returns {string} CSS content with duplicates removed
|
|
673
|
+
*/
|
|
475
674
|
function removeDuplicateCustomVariant(cssContent) {
|
|
476
675
|
const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
|
|
477
676
|
const matches = cssContent.match(variantPattern);
|
|
478
677
|
|
|
479
678
|
if (matches && matches.length > 1) {
|
|
679
|
+
// Remove all occurrences and add back one at the top
|
|
480
680
|
const withoutVariants = cssContent.replace(variantPattern, '');
|
|
481
681
|
return `@custom-variant dark (&:is(.dark *));\n\n${withoutVariants.trimStart()}`;
|
|
482
682
|
}
|
|
@@ -484,11 +684,93 @@ function removeDuplicateCustomVariant(cssContent) {
|
|
|
484
684
|
return cssContent;
|
|
485
685
|
}
|
|
486
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Normalizes CSS format to ensure consistent structure
|
|
689
|
+
*
|
|
690
|
+
* Format order:
|
|
691
|
+
* 1. @import statements (tailwindcss, then tw-animate-css, then others)
|
|
692
|
+
* 2. @custom-variant declaration
|
|
693
|
+
* 3. Rest of the CSS content
|
|
694
|
+
*
|
|
695
|
+
* This ensures the CSS file always has the same structure regardless of
|
|
696
|
+
* how it was generated or merged.
|
|
697
|
+
*
|
|
698
|
+
* @param {string} cssContent - CSS content to normalize
|
|
699
|
+
* @returns {string} Normalized CSS content
|
|
700
|
+
*/
|
|
701
|
+
function normalizeCSSFormat(cssContent) {
|
|
702
|
+
// Extract @custom-variant declaration
|
|
703
|
+
const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
|
|
704
|
+
const variantMatch = cssContent.match(variantPattern);
|
|
705
|
+
const hasVariant = variantMatch && variantMatch.length > 0;
|
|
706
|
+
|
|
707
|
+
// Extract all @import statements
|
|
708
|
+
const importPattern = /@import\s+["'][^"']+["'];?\s*\n?/g;
|
|
709
|
+
const imports = cssContent.match(importPattern) || [];
|
|
710
|
+
|
|
711
|
+
// Separate imports by type for proper ordering
|
|
712
|
+
const tailwindImport = imports.find(imp => imp.includes('tailwindcss'));
|
|
713
|
+
const twAnimateImport = imports.find(imp => imp.includes('tw-animate-css'));
|
|
714
|
+
const otherImports = imports.filter(imp =>
|
|
715
|
+
!imp.includes('tailwindcss') && !imp.includes('tw-animate-css')
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
// Remove all imports and variant from content to get the rest
|
|
719
|
+
let content = cssContent
|
|
720
|
+
.replace(variantPattern, '')
|
|
721
|
+
.replace(importPattern, '')
|
|
722
|
+
.trim();
|
|
723
|
+
|
|
724
|
+
// Build normalized format in the correct order
|
|
725
|
+
let normalized = '';
|
|
726
|
+
|
|
727
|
+
// Step 1: Imports first (tailwindcss, then tw-animate-css, then others)
|
|
728
|
+
if (tailwindImport) {
|
|
729
|
+
normalized += tailwindImport.trim() + '\n';
|
|
730
|
+
}
|
|
731
|
+
if (twAnimateImport) {
|
|
732
|
+
normalized += twAnimateImport.trim() + '\n';
|
|
733
|
+
}
|
|
734
|
+
if (otherImports.length > 0) {
|
|
735
|
+
normalized += otherImports.join('') + '\n';
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Step 2: @custom-variant second (if it exists)
|
|
739
|
+
if (hasVariant) {
|
|
740
|
+
if (normalized) {
|
|
741
|
+
normalized += '\n@custom-variant dark (&:is(.dark *));\n';
|
|
742
|
+
} else {
|
|
743
|
+
normalized += '@custom-variant dark (&:is(.dark *));\n';
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Step 3: Rest of content (theme variables, etc.)
|
|
748
|
+
if (normalized && content) {
|
|
749
|
+
normalized += '\n' + content;
|
|
750
|
+
} else if (content) {
|
|
751
|
+
normalized = content;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return normalized.trim() + (normalized ? '\n' : '');
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Processes tw-animate-css import based on whether the package is installed
|
|
759
|
+
*
|
|
760
|
+
* - If package is NOT installed: Removes the import to prevent build errors
|
|
761
|
+
* - If package IS installed: Ensures import exists and removes duplicates
|
|
762
|
+
*
|
|
763
|
+
* @param {string} cssContent - CSS content to process
|
|
764
|
+
* @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
|
|
765
|
+
* @param {boolean} forceUpdate - Whether this is a forced update
|
|
766
|
+
* @returns {string} Processed CSS content
|
|
767
|
+
*/
|
|
487
768
|
function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
|
|
488
769
|
const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
|
|
489
770
|
let processed = cssContent;
|
|
490
771
|
|
|
491
772
|
if (!hasTwAnimate) {
|
|
773
|
+
// Package not installed - remove import to prevent build errors
|
|
492
774
|
processed = cssContent.replace(twAnimatePattern, '');
|
|
493
775
|
if (cssContent.includes('tw-animate-css') && !processed.includes('tw-animate-css')) {
|
|
494
776
|
if (forceUpdate) {
|
|
@@ -502,10 +784,10 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
|
|
|
502
784
|
}
|
|
503
785
|
}
|
|
504
786
|
} else {
|
|
505
|
-
//
|
|
787
|
+
// Package is installed - ensure import exists and remove duplicates
|
|
506
788
|
processed = removeDuplicateTwAnimateImports(processed);
|
|
507
789
|
|
|
508
|
-
//
|
|
790
|
+
// Add import if it doesn't exist (right after tailwindcss if present)
|
|
509
791
|
if (!processed.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
|
|
510
792
|
if (processed.includes('@import "tailwindcss"') || processed.includes("@import 'tailwindcss'")) {
|
|
511
793
|
processed = processed.replace(
|
|
@@ -521,10 +803,25 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
|
|
|
521
803
|
return processed;
|
|
522
804
|
}
|
|
523
805
|
|
|
806
|
+
/**
|
|
807
|
+
* Removes tailwindcss import statements from CSS
|
|
808
|
+
* Used when merging styles to avoid duplicates
|
|
809
|
+
*
|
|
810
|
+
* @param {string} cssContent - CSS content
|
|
811
|
+
* @returns {string} CSS content without tailwindcss imports
|
|
812
|
+
*/
|
|
524
813
|
function removeTailwindImport(cssContent) {
|
|
525
814
|
return cssContent.replace(/@import\s+["']tailwindcss["'];?\s*\n?/g, '');
|
|
526
815
|
}
|
|
527
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Ensures tw-animate-css import exists if the package is installed
|
|
819
|
+
* Adds it right after tailwindcss import if present
|
|
820
|
+
*
|
|
821
|
+
* @param {string} cssContent - CSS content
|
|
822
|
+
* @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
|
|
823
|
+
* @returns {string} CSS content with tw-animate-css import ensured
|
|
824
|
+
*/
|
|
528
825
|
function ensureTwAnimateImport(cssContent, hasTwAnimate) {
|
|
529
826
|
if (!hasTwAnimate) {
|
|
530
827
|
return cssContent;
|
|
@@ -538,7 +835,7 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
|
|
|
538
835
|
return cleaned;
|
|
539
836
|
}
|
|
540
837
|
|
|
541
|
-
// Add import after tailwindcss if it exists
|
|
838
|
+
// Add import after tailwindcss if it exists, otherwise at the beginning
|
|
542
839
|
if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
|
|
543
840
|
return cleaned.replace(
|
|
544
841
|
/(@import\s+["']tailwindcss["'];?\s*\n?)/,
|
|
@@ -549,21 +846,36 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
|
|
|
549
846
|
return '@import "tw-animate-css";\n' + cleaned;
|
|
550
847
|
}
|
|
551
848
|
|
|
849
|
+
/**
|
|
850
|
+
* Merges Weloop styles with existing CSS that has Tailwind imports
|
|
851
|
+
*
|
|
852
|
+
* Strategy:
|
|
853
|
+
* - Finds the tailwindcss import in existing CSS
|
|
854
|
+
* - Inserts Weloop styles right after it
|
|
855
|
+
* - Handles tw-animate-css import to avoid duplicates
|
|
856
|
+
*
|
|
857
|
+
* @param {string} existing - Existing CSS content
|
|
858
|
+
* @param {string} weloopStyles - Weloop styles to merge in
|
|
859
|
+
* @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
|
|
860
|
+
* @returns {string} Merged CSS content
|
|
861
|
+
*/
|
|
552
862
|
function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
|
|
553
863
|
const tailwindMatch = existing.match(/(@import\s+["']tailwindcss["'];?\s*\n?)/);
|
|
554
864
|
if (!tailwindMatch) {
|
|
865
|
+
// No tailwindcss import found - just prepend Weloop styles
|
|
555
866
|
return weloopStyles + '\n\n' + existing;
|
|
556
867
|
}
|
|
557
868
|
|
|
869
|
+
// Split existing CSS around the tailwindcss import
|
|
558
870
|
const beforeTailwind = existing.substring(0, existing.indexOf(tailwindMatch[0]));
|
|
559
871
|
const afterTailwind = existing.substring(existing.indexOf(tailwindMatch[0]) + tailwindMatch[0].length);
|
|
560
872
|
|
|
561
|
-
// Check if tw-animate-css import already exists
|
|
873
|
+
// Check if tw-animate-css import already exists
|
|
562
874
|
const hasTwAnimateInExisting = existing.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
|
|
563
875
|
const hasTwAnimateInWeloop = weloopStyles.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
|
|
564
876
|
|
|
565
|
-
// If
|
|
566
|
-
//
|
|
877
|
+
// If existing file already has tw-animate import, remove it from Weloop styles
|
|
878
|
+
// to avoid duplicating it in the merge output
|
|
567
879
|
let weloopStylesCleaned = weloopStyles;
|
|
568
880
|
if (hasTwAnimateInExisting && hasTwAnimateInWeloop) {
|
|
569
881
|
weloopStylesCleaned = weloopStyles.replace(
|
|
@@ -572,44 +884,75 @@ function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
|
|
|
572
884
|
);
|
|
573
885
|
}
|
|
574
886
|
|
|
887
|
+
// Add tw-animate import if needed (package installed but import missing)
|
|
575
888
|
let importsToAdd = '';
|
|
576
889
|
if (hasTwAnimate && !hasTwAnimateInExisting && !hasTwAnimateInWeloop) {
|
|
577
890
|
importsToAdd = '@import "tw-animate-css";\n';
|
|
578
891
|
}
|
|
579
892
|
|
|
893
|
+
// Merge: before tailwind + tailwind import + tw-animate (if needed) + Weloop styles + after tailwind
|
|
580
894
|
return beforeTailwind + tailwindMatch[0] + importsToAdd + weloopStylesCleaned + '\n' + afterTailwind;
|
|
581
895
|
}
|
|
582
896
|
|
|
897
|
+
/**
|
|
898
|
+
* Replaces existing Weloop styles with new ones
|
|
899
|
+
*
|
|
900
|
+
* Finds where Weloop styles start (usually @theme inline or :root)
|
|
901
|
+
* and replaces everything from there with the new styles.
|
|
902
|
+
* Preserves imports that come before the Weloop styles.
|
|
903
|
+
*
|
|
904
|
+
* @param {string} existing - Existing CSS content
|
|
905
|
+
* @param {string} newStyles - New Weloop styles to replace with
|
|
906
|
+
* @param {boolean} hasTwAnimate - Whether tw-animate-css package is installed
|
|
907
|
+
* @returns {string} CSS content with Weloop styles replaced
|
|
908
|
+
*/
|
|
583
909
|
function replaceWeloopStyles(existing, newStyles, hasTwAnimate) {
|
|
584
910
|
const importPattern = /(@import\s+["'][^"']+["'];?\s*\n?)/g;
|
|
585
911
|
const imports = existing.match(importPattern) || [];
|
|
586
912
|
const importsText = imports.join('');
|
|
587
913
|
|
|
914
|
+
// Find where Weloop styles start (look for @theme inline or :root)
|
|
588
915
|
const weloopStartPattern = /(@theme\s+inline|:root\s*\{)/;
|
|
589
916
|
const weloopStartMatch = existing.search(weloopStartPattern);
|
|
590
917
|
|
|
591
918
|
if (weloopStartMatch === -1) {
|
|
919
|
+
// No existing Weloop styles found - just append
|
|
592
920
|
return existing + '\n\n' + newStyles;
|
|
593
921
|
}
|
|
594
922
|
|
|
923
|
+
// Extract content before Weloop styles
|
|
595
924
|
let contentBeforeWeloop = existing.substring(0, weloopStartMatch);
|
|
925
|
+
// Keep non-Weloop imports (filter out tw-animate if package not installed)
|
|
596
926
|
const nonWeloopImports = importsText.split('\n').filter(imp =>
|
|
597
927
|
!imp.includes('tw-animate-css') || hasTwAnimate
|
|
598
928
|
).join('\n');
|
|
929
|
+
// Remove all imports from contentBeforeWeloop (we'll add them back)
|
|
599
930
|
contentBeforeWeloop = nonWeloopImports + '\n' + contentBeforeWeloop.replace(importPattern, '');
|
|
600
931
|
|
|
932
|
+
// Return: preserved imports + content before Weloop + new Weloop styles
|
|
601
933
|
return contentBeforeWeloop.trim() + '\n\n' + newStyles.trim();
|
|
602
934
|
}
|
|
603
935
|
|
|
936
|
+
/**
|
|
937
|
+
* Fetches the CSS file from the registry
|
|
938
|
+
*
|
|
939
|
+
* For local paths: Looks for app/globals.css relative to registry directory
|
|
940
|
+
* For remote URLs: Constructs URL by replacing /registry with /app/globals.css
|
|
941
|
+
*
|
|
942
|
+
* @param {string} registryUrl - Registry base URL or path
|
|
943
|
+
* @returns {Promise<string>} CSS file content
|
|
944
|
+
*/
|
|
604
945
|
async function fetchCSSFromRegistry(registryUrl) {
|
|
605
946
|
let sourceCssPath;
|
|
606
947
|
|
|
607
948
|
if (isLocalPath(registryUrl)) {
|
|
949
|
+
// Local path: find app/globals.css relative to registry directory
|
|
608
950
|
const basePath = path.isAbsolute(registryUrl)
|
|
609
951
|
? path.dirname(registryUrl)
|
|
610
952
|
: path.join(process.cwd(), path.dirname(registryUrl));
|
|
611
953
|
sourceCssPath = path.join(basePath, 'app', 'globals.css');
|
|
612
954
|
} else {
|
|
955
|
+
// Remote URL: replace /registry with /app/globals.css
|
|
613
956
|
const baseUrl = registryUrl.replace('/registry', '');
|
|
614
957
|
sourceCssPath = `${baseUrl}/app/globals.css`;
|
|
615
958
|
}
|
|
@@ -617,17 +960,31 @@ async function fetchCSSFromRegistry(registryUrl) {
|
|
|
617
960
|
return await fetchText(sourceCssPath);
|
|
618
961
|
}
|
|
619
962
|
|
|
963
|
+
/**
|
|
964
|
+
* Installs or updates CSS styles from the registry
|
|
965
|
+
*
|
|
966
|
+
* Handles three scenarios:
|
|
967
|
+
* 1. --overwrite: Replaces entire file
|
|
968
|
+
* 2. Normal update: Intelligently merges with existing styles
|
|
969
|
+
* 3. New file: Creates file with Weloop styles
|
|
970
|
+
*
|
|
971
|
+
* @param {object} config - Components configuration object
|
|
972
|
+
* @param {string} registryUrl - Registry base URL or path
|
|
973
|
+
* @param {boolean} forceUpdate - Whether to overwrite existing file
|
|
974
|
+
* @param {boolean} silent - Whether to suppress output messages
|
|
975
|
+
*/
|
|
620
976
|
async function installCSSStyles(config, registryUrl, forceUpdate = false, silent = false) {
|
|
621
977
|
const cssPath = config.tailwind?.css || 'app/globals.css';
|
|
622
978
|
const fullCssPath = path.join(process.cwd(), cssPath);
|
|
623
979
|
|
|
624
|
-
// Check if Weloop styles already exist
|
|
980
|
+
// Check if Weloop styles already exist in the file
|
|
625
981
|
let hasWeloopStylesInFile = false;
|
|
626
982
|
if (fs.existsSync(fullCssPath)) {
|
|
627
983
|
const existingContent = fs.readFileSync(fullCssPath, 'utf-8');
|
|
628
984
|
hasWeloopStylesInFile = hasWeloopStyles(existingContent);
|
|
629
985
|
|
|
630
|
-
// If styles already exist and not forcing update, skip silently
|
|
986
|
+
// If styles already exist and we're not forcing update, skip silently
|
|
987
|
+
// This prevents unnecessary updates when installing components
|
|
631
988
|
if (hasWeloopStylesInFile && !forceUpdate && silent) {
|
|
632
989
|
return;
|
|
633
990
|
}
|
|
@@ -644,10 +1001,13 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
644
1001
|
const hasTwAnimate = checkPackageInstalled('tw-animate-css');
|
|
645
1002
|
let processedCssContent = processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate);
|
|
646
1003
|
|
|
647
|
-
// Handle file installation
|
|
1004
|
+
// Handle file installation based on mode and existing file state
|
|
648
1005
|
if (forceUpdate && fs.existsSync(fullCssPath)) {
|
|
649
|
-
// --overwrite
|
|
650
|
-
|
|
1006
|
+
// Mode 1: --overwrite flag - Replace entire file with fresh styles
|
|
1007
|
+
let normalized = normalizeCSSFormat(processedCssContent);
|
|
1008
|
+
normalized = removeDuplicateTwAnimateImports(normalized);
|
|
1009
|
+
normalized = removeDuplicateCustomVariant(normalized);
|
|
1010
|
+
fs.writeFileSync(fullCssPath, normalized);
|
|
651
1011
|
if (!silent) {
|
|
652
1012
|
success(`Overwritten ${cssPath} with Weloop styles`);
|
|
653
1013
|
if (hasTwAnimate) {
|
|
@@ -655,34 +1015,36 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
655
1015
|
}
|
|
656
1016
|
}
|
|
657
1017
|
} else if (fs.existsSync(fullCssPath)) {
|
|
658
|
-
//
|
|
1018
|
+
// Mode 2: Normal update - Intelligently merge with existing styles
|
|
659
1019
|
const existing = fs.readFileSync(fullCssPath, 'utf-8');
|
|
660
1020
|
const hasTailwindImport = existing.includes('@import "tailwindcss"') ||
|
|
661
1021
|
existing.includes('@tailwind base');
|
|
662
1022
|
|
|
663
1023
|
if (hasWeloopStylesInFile) {
|
|
664
|
-
//
|
|
1024
|
+
// Case 2a: Weloop styles already exist - replace them with updated versions
|
|
665
1025
|
let weloopStyles = removeTailwindImport(processedCssContent);
|
|
666
1026
|
weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
|
|
667
1027
|
let finalContent = replaceWeloopStyles(existing, weloopStyles, hasTwAnimate);
|
|
668
1028
|
finalContent = removeDuplicateTwAnimateImports(finalContent);
|
|
669
1029
|
finalContent = removeDuplicateCustomVariant(finalContent);
|
|
1030
|
+
finalContent = normalizeCSSFormat(finalContent);
|
|
670
1031
|
|
|
671
|
-
// Only
|
|
1032
|
+
// Only write if content actually changed (prevents unnecessary file updates)
|
|
672
1033
|
if (finalContent !== existing) {
|
|
673
1034
|
fs.writeFileSync(fullCssPath, finalContent);
|
|
674
1035
|
if (!silent) {
|
|
675
1036
|
success(`Updated ${cssPath} with Weloop styles`);
|
|
676
1037
|
}
|
|
677
1038
|
}
|
|
678
|
-
// If no changes, silently skip
|
|
1039
|
+
// If no changes, silently skip (file is already up to date)
|
|
679
1040
|
} else if (hasTailwindImport) {
|
|
680
|
-
//
|
|
1041
|
+
// Case 2b: No Weloop styles but Tailwind exists - merge after Tailwind imports
|
|
681
1042
|
let weloopStyles = removeTailwindImport(processedCssContent);
|
|
682
1043
|
weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
|
|
683
1044
|
let merged = mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate);
|
|
684
1045
|
merged = removeDuplicateTwAnimateImports(merged);
|
|
685
1046
|
merged = removeDuplicateCustomVariant(merged);
|
|
1047
|
+
merged = normalizeCSSFormat(merged);
|
|
686
1048
|
fs.writeFileSync(fullCssPath, merged);
|
|
687
1049
|
if (!silent) {
|
|
688
1050
|
success(`Updated ${cssPath} with Weloop styles`);
|
|
@@ -692,9 +1054,10 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
692
1054
|
info(` Your existing styles are preserved`);
|
|
693
1055
|
}
|
|
694
1056
|
} else {
|
|
695
|
-
// No Tailwind imports
|
|
1057
|
+
// Case 2c: No Tailwind imports - prepend Weloop styles to existing content
|
|
696
1058
|
let finalCssContent = removeDuplicateTwAnimateImports(processedCssContent);
|
|
697
1059
|
|
|
1060
|
+
// Ensure tw-animate import exists if package is installed
|
|
698
1061
|
if (hasTwAnimate && !finalCssContent.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
|
|
699
1062
|
if (finalCssContent.includes('@import "tailwindcss"')) {
|
|
700
1063
|
finalCssContent = finalCssContent.replace(
|
|
@@ -705,7 +1068,9 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
705
1068
|
finalCssContent = '@import "tailwindcss";\n@import "tw-animate-css";\n' + finalCssContent;
|
|
706
1069
|
}
|
|
707
1070
|
}
|
|
708
|
-
|
|
1071
|
+
let combined = finalCssContent + '\n\n' + existing;
|
|
1072
|
+
combined = normalizeCSSFormat(combined);
|
|
1073
|
+
fs.writeFileSync(fullCssPath, combined);
|
|
709
1074
|
if (!silent) {
|
|
710
1075
|
success(`Updated ${cssPath} with Weloop styles`);
|
|
711
1076
|
if (hasTwAnimate) {
|
|
@@ -714,8 +1079,11 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
714
1079
|
}
|
|
715
1080
|
}
|
|
716
1081
|
} else {
|
|
717
|
-
//
|
|
718
|
-
|
|
1082
|
+
// Mode 3: File doesn't exist - create new file with Weloop styles
|
|
1083
|
+
let normalized = normalizeCSSFormat(processedCssContent);
|
|
1084
|
+
normalized = removeDuplicateTwAnimateImports(normalized);
|
|
1085
|
+
normalized = removeDuplicateCustomVariant(normalized);
|
|
1086
|
+
fs.writeFileSync(fullCssPath, normalized);
|
|
719
1087
|
if (!silent) {
|
|
720
1088
|
success(`Created ${cssPath} with Weloop styles`);
|
|
721
1089
|
if (hasTwAnimate) {
|
|
@@ -735,13 +1103,22 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
735
1103
|
}
|
|
736
1104
|
|
|
737
1105
|
// ============================================================================
|
|
738
|
-
// COMPONENT INSTALLATION
|
|
1106
|
+
// COMPONENT INSTALLATION - Installing Components from Registry
|
|
739
1107
|
// ============================================================================
|
|
740
1108
|
|
|
1109
|
+
/**
|
|
1110
|
+
* Creates the utils.ts file if it doesn't exist
|
|
1111
|
+
*
|
|
1112
|
+
* This file contains the 'cn' utility function used by all components
|
|
1113
|
+
* for merging Tailwind classes. It's required for components to work.
|
|
1114
|
+
*
|
|
1115
|
+
* @param {string} utilsPath - Path where utils.ts should be created
|
|
1116
|
+
*/
|
|
741
1117
|
function createUtilsFile(utilsPath) {
|
|
742
1118
|
if (fs.existsSync(utilsPath)) return;
|
|
743
1119
|
|
|
744
1120
|
// Silently create utils file (matching shadcn/ui behavior)
|
|
1121
|
+
// Don't overwrite if user has customized it
|
|
745
1122
|
ensureDirectoryExists(path.dirname(utilsPath));
|
|
746
1123
|
|
|
747
1124
|
const utilsContent = `import { clsx, type ClassValue } from "clsx"
|
|
@@ -754,10 +1131,27 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
754
1131
|
fs.writeFileSync(utilsPath, utilsContent);
|
|
755
1132
|
}
|
|
756
1133
|
|
|
1134
|
+
/**
|
|
1135
|
+
* Installs a component from the registry
|
|
1136
|
+
*
|
|
1137
|
+
* Installation process:
|
|
1138
|
+
* 1. Install base dependencies (clsx, tailwind-merge)
|
|
1139
|
+
* 2. Install tw-animate-css for animations
|
|
1140
|
+
* 3. Install/update CSS styles (silently if already installed)
|
|
1141
|
+
* 4. Install component dependencies (recursive)
|
|
1142
|
+
* 5. Install component files
|
|
1143
|
+
* 6. Create utils.ts if needed
|
|
1144
|
+
* 7. Install npm dependencies
|
|
1145
|
+
*
|
|
1146
|
+
* @param {string} componentName - Name of the component to install
|
|
1147
|
+
* @param {object} options - Installation options
|
|
1148
|
+
* @param {boolean} options.overwrite - Whether to overwrite existing files
|
|
1149
|
+
* @param {string} options.registryUrl - Registry URL or path
|
|
1150
|
+
*/
|
|
757
1151
|
async function installComponent(componentName, options = {}) {
|
|
758
1152
|
const { overwrite = false, registryUrl = DEFAULT_REGISTRY_URL } = options;
|
|
759
1153
|
|
|
760
|
-
// Install base dependencies
|
|
1154
|
+
// Step 1: Install base dependencies (required for utils.ts to work)
|
|
761
1155
|
const baseDeps = [];
|
|
762
1156
|
if (!checkPackageInstalled('clsx')) {
|
|
763
1157
|
baseDeps.push('clsx');
|
|
@@ -770,19 +1164,20 @@ async function installComponent(componentName, options = {}) {
|
|
|
770
1164
|
await installPackages(baseDeps);
|
|
771
1165
|
}
|
|
772
1166
|
|
|
773
|
-
// Load or create components.json
|
|
1167
|
+
// Step 2: Load or create components.json configuration
|
|
774
1168
|
const config = loadComponentsConfig();
|
|
775
1169
|
|
|
776
|
-
// Install tw-animate-css automatically (required for animations)
|
|
1170
|
+
// Step 3: Install tw-animate-css automatically (required for component animations)
|
|
777
1171
|
if (!checkPackageInstalled('tw-animate-css')) {
|
|
778
1172
|
info('Installing tw-animate-css for animations...');
|
|
779
1173
|
await installPackages(['tw-animate-css']);
|
|
780
1174
|
}
|
|
781
1175
|
|
|
782
|
-
// Install CSS styles early (before component installation)
|
|
1176
|
+
// Step 4: Install CSS styles early (before component installation)
|
|
1177
|
+
// Silent mode prevents output when styles are already installed
|
|
783
1178
|
await installCSSStyles(config, registryUrl, false, true);
|
|
784
1179
|
|
|
785
|
-
//
|
|
1180
|
+
// Step 5: Resolve paths from components.json configuration
|
|
786
1181
|
const uiAlias = config.aliases?.ui || '@/components/ui';
|
|
787
1182
|
const utilsAlias = config.aliases?.utils || '@/lib/utils';
|
|
788
1183
|
|
|
@@ -793,30 +1188,34 @@ async function installComponent(componentName, options = {}) {
|
|
|
793
1188
|
}
|
|
794
1189
|
utilsPath = path.join(process.cwd(), utilsPath);
|
|
795
1190
|
|
|
796
|
-
// Load component registry
|
|
1191
|
+
// Step 6: Load component registry from remote or local source
|
|
797
1192
|
const registry = await loadRegistry(componentName, registryUrl);
|
|
798
1193
|
|
|
799
1194
|
if (!registry) {
|
|
800
1195
|
error(`Component "${componentName}" not found in registry.`);
|
|
801
1196
|
info('Available components:');
|
|
802
1197
|
try {
|
|
1198
|
+
// Show available components to help user
|
|
803
1199
|
const index = await loadIndex(registryUrl);
|
|
804
1200
|
index.registry.forEach(comp => {
|
|
805
1201
|
console.log(` - ${comp.name}`);
|
|
806
1202
|
});
|
|
807
1203
|
} catch (e) {
|
|
808
|
-
// Ignore if index fails
|
|
1204
|
+
// Ignore if index fails - we already showed the error
|
|
809
1205
|
}
|
|
810
1206
|
process.exit(1);
|
|
811
1207
|
}
|
|
812
1208
|
|
|
813
|
-
// Check if component already exists
|
|
1209
|
+
// Step 7: Check if component already exists
|
|
1210
|
+
// Silently skip if exists (matching shadcn/ui behavior)
|
|
1211
|
+
// Only install if overwrite flag is set
|
|
814
1212
|
const componentPath = path.join(componentsDir, `${componentName}.tsx`);
|
|
815
1213
|
if (fs.existsSync(componentPath) && !overwrite) {
|
|
816
1214
|
return;
|
|
817
1215
|
}
|
|
818
1216
|
|
|
819
|
-
// Install
|
|
1217
|
+
// Step 8: Install component dependencies first (recursive)
|
|
1218
|
+
// Some components depend on other components (e.g., button-group depends on button)
|
|
820
1219
|
if (registry.registryDependencies && registry.registryDependencies.length > 0) {
|
|
821
1220
|
info(`Installing dependencies: ${registry.registryDependencies.join(', ')}`);
|
|
822
1221
|
for (const dep of registry.registryDependencies) {
|
|
@@ -824,7 +1223,8 @@ async function installComponent(componentName, options = {}) {
|
|
|
824
1223
|
}
|
|
825
1224
|
}
|
|
826
1225
|
|
|
827
|
-
// Install component files
|
|
1226
|
+
// Step 9: Install component files
|
|
1227
|
+
// Components can have multiple files (e.g., component + types + styles)
|
|
828
1228
|
for (const file of registry.files) {
|
|
829
1229
|
const filePath = path.join(process.cwd(), file.path);
|
|
830
1230
|
ensureDirectoryExists(path.dirname(filePath));
|
|
@@ -832,10 +1232,11 @@ async function installComponent(componentName, options = {}) {
|
|
|
832
1232
|
success(`Installed: ${file.path}`);
|
|
833
1233
|
}
|
|
834
1234
|
|
|
835
|
-
// Create utils.ts if
|
|
1235
|
+
// Step 10: Create utils.ts if it doesn't exist
|
|
1236
|
+
// This is required for all components to work
|
|
836
1237
|
createUtilsFile(utilsPath);
|
|
837
1238
|
|
|
838
|
-
// Install npm dependencies
|
|
1239
|
+
// Step 11: Install npm dependencies required by the component
|
|
839
1240
|
if (registry.dependencies && registry.dependencies.length > 0) {
|
|
840
1241
|
const missingDeps = getMissingDependencies(registry.dependencies);
|
|
841
1242
|
if (missingDeps.length > 0) {
|
|
@@ -849,15 +1250,22 @@ async function installComponent(componentName, options = {}) {
|
|
|
849
1250
|
}
|
|
850
1251
|
|
|
851
1252
|
// ============================================================================
|
|
852
|
-
// COMMANDS
|
|
1253
|
+
// COMMANDS - CLI Command Handlers
|
|
853
1254
|
// ============================================================================
|
|
854
1255
|
|
|
1256
|
+
/**
|
|
1257
|
+
* Lists all available components from the registry
|
|
1258
|
+
* Shows component names and their dependencies
|
|
1259
|
+
*
|
|
1260
|
+
* @param {string} registryUrl - Registry URL or path
|
|
1261
|
+
*/
|
|
855
1262
|
async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
|
|
856
1263
|
try {
|
|
857
1264
|
const index = await loadIndex(registryUrl);
|
|
858
1265
|
|
|
859
1266
|
console.log('\nAvailable components:\n');
|
|
860
1267
|
index.registry.forEach(comp => {
|
|
1268
|
+
// Show dependencies if component has any
|
|
861
1269
|
const deps = comp.registryDependencies && comp.registryDependencies.length > 0
|
|
862
1270
|
? ` (depends on: ${comp.registryDependencies.join(', ')})`
|
|
863
1271
|
: '';
|
|
@@ -871,24 +1279,39 @@ async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
|
|
|
871
1279
|
}
|
|
872
1280
|
|
|
873
1281
|
// ============================================================================
|
|
874
|
-
// MAIN
|
|
1282
|
+
// MAIN - CLI Entry Point
|
|
875
1283
|
// ============================================================================
|
|
876
1284
|
|
|
1285
|
+
/**
|
|
1286
|
+
* Main CLI entry point
|
|
1287
|
+
*
|
|
1288
|
+
* Parses command line arguments and routes to appropriate command handler:
|
|
1289
|
+
* - add: Install a component
|
|
1290
|
+
* - list: List available components
|
|
1291
|
+
* - css: Install/update CSS styles
|
|
1292
|
+
*
|
|
1293
|
+
* Supports options:
|
|
1294
|
+
* - --registry: Custom registry URL or path
|
|
1295
|
+
* - --overwrite / -f: Overwrite existing files
|
|
1296
|
+
*/
|
|
877
1297
|
async function main() {
|
|
878
1298
|
const args = process.argv.slice(2);
|
|
879
1299
|
const command = args[0];
|
|
880
1300
|
const componentName = args[1];
|
|
881
1301
|
|
|
1302
|
+
// Parse --registry option if provided
|
|
882
1303
|
const registryIndex = args.indexOf('--registry');
|
|
883
1304
|
const registryUrl = registryIndex !== -1 && args[registryIndex + 1]
|
|
884
1305
|
? args[registryIndex + 1]
|
|
885
1306
|
: DEFAULT_REGISTRY_URL;
|
|
886
1307
|
|
|
1308
|
+
// Build options object
|
|
887
1309
|
const options = {
|
|
888
1310
|
overwrite: args.includes('--overwrite') || args.includes('-f'),
|
|
889
1311
|
registryUrl: registryUrl,
|
|
890
1312
|
};
|
|
891
1313
|
|
|
1314
|
+
// Show usage if no command provided
|
|
892
1315
|
if (!command) {
|
|
893
1316
|
console.log(`
|
|
894
1317
|
Usage:
|
|
@@ -910,6 +1333,7 @@ Examples:
|
|
|
910
1333
|
process.exit(0);
|
|
911
1334
|
}
|
|
912
1335
|
|
|
1336
|
+
// Route to appropriate command handler
|
|
913
1337
|
switch (command) {
|
|
914
1338
|
case 'add':
|
|
915
1339
|
if (!componentName) {
|
|
@@ -926,6 +1350,7 @@ Examples:
|
|
|
926
1350
|
|
|
927
1351
|
case 'css':
|
|
928
1352
|
case 'styles':
|
|
1353
|
+
// Both 'css' and 'styles' commands do the same thing
|
|
929
1354
|
const config = loadComponentsConfig();
|
|
930
1355
|
await installCSSStyles(config, registryUrl, options.overwrite, false);
|
|
931
1356
|
break;
|
|
@@ -937,6 +1362,7 @@ Examples:
|
|
|
937
1362
|
}
|
|
938
1363
|
}
|
|
939
1364
|
|
|
1365
|
+
// Run main function and handle errors
|
|
940
1366
|
main().catch(err => {
|
|
941
1367
|
error(`Error: ${err.message}`);
|
|
942
1368
|
process.exit(1);
|