weloop-kosign 1.0.9 → 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 +4 -1
- package/scripts/cli-remote.js +510 -58
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weloop-kosign",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "CLI tool for installing Weloop UI components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"weloop",
|
|
@@ -26,5 +26,8 @@
|
|
|
26
26
|
],
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"postpublish": "node scripts/postpublish.js"
|
|
29
32
|
}
|
|
30
33
|
}
|
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,113 @@ 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
|
+
*/
|
|
674
|
+
function removeDuplicateCustomVariant(cssContent) {
|
|
675
|
+
const variantPattern = /@custom-variant\s+dark\s+\(&:is\(\.dark \*\)\);\s*\n?/g;
|
|
676
|
+
const matches = cssContent.match(variantPattern);
|
|
677
|
+
|
|
678
|
+
if (matches && matches.length > 1) {
|
|
679
|
+
// Remove all occurrences and add back one at the top
|
|
680
|
+
const withoutVariants = cssContent.replace(variantPattern, '');
|
|
681
|
+
return `@custom-variant dark (&:is(.dark *));\n\n${withoutVariants.trimStart()}`;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return cssContent;
|
|
685
|
+
}
|
|
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
|
+
*/
|
|
475
768
|
function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
|
|
476
769
|
const twAnimatePattern = /@import\s+["']tw-animate-css["'];?\s*\n?/g;
|
|
477
770
|
let processed = cssContent;
|
|
478
771
|
|
|
479
772
|
if (!hasTwAnimate) {
|
|
773
|
+
// Package not installed - remove import to prevent build errors
|
|
480
774
|
processed = cssContent.replace(twAnimatePattern, '');
|
|
481
775
|
if (cssContent.includes('tw-animate-css') && !processed.includes('tw-animate-css')) {
|
|
482
776
|
if (forceUpdate) {
|
|
@@ -490,10 +784,10 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
|
|
|
490
784
|
}
|
|
491
785
|
}
|
|
492
786
|
} else {
|
|
493
|
-
//
|
|
787
|
+
// Package is installed - ensure import exists and remove duplicates
|
|
494
788
|
processed = removeDuplicateTwAnimateImports(processed);
|
|
495
789
|
|
|
496
|
-
//
|
|
790
|
+
// Add import if it doesn't exist (right after tailwindcss if present)
|
|
497
791
|
if (!processed.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
|
|
498
792
|
if (processed.includes('@import "tailwindcss"') || processed.includes("@import 'tailwindcss'")) {
|
|
499
793
|
processed = processed.replace(
|
|
@@ -509,10 +803,25 @@ function processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate = false) {
|
|
|
509
803
|
return processed;
|
|
510
804
|
}
|
|
511
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
|
+
*/
|
|
512
813
|
function removeTailwindImport(cssContent) {
|
|
513
814
|
return cssContent.replace(/@import\s+["']tailwindcss["'];?\s*\n?/g, '');
|
|
514
815
|
}
|
|
515
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
|
+
*/
|
|
516
825
|
function ensureTwAnimateImport(cssContent, hasTwAnimate) {
|
|
517
826
|
if (!hasTwAnimate) {
|
|
518
827
|
return cssContent;
|
|
@@ -526,7 +835,7 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
|
|
|
526
835
|
return cleaned;
|
|
527
836
|
}
|
|
528
837
|
|
|
529
|
-
// Add import after tailwindcss if it exists
|
|
838
|
+
// Add import after tailwindcss if it exists, otherwise at the beginning
|
|
530
839
|
if (cleaned.includes('@import "tailwindcss"') || cleaned.includes("@import 'tailwindcss'")) {
|
|
531
840
|
return cleaned.replace(
|
|
532
841
|
/(@import\s+["']tailwindcss["'];?\s*\n?)/,
|
|
@@ -537,57 +846,113 @@ function ensureTwAnimateImport(cssContent, hasTwAnimate) {
|
|
|
537
846
|
return '@import "tw-animate-css";\n' + cleaned;
|
|
538
847
|
}
|
|
539
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
|
+
*/
|
|
540
862
|
function mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate) {
|
|
541
863
|
const tailwindMatch = existing.match(/(@import\s+["']tailwindcss["'];?\s*\n?)/);
|
|
542
864
|
if (!tailwindMatch) {
|
|
865
|
+
// No tailwindcss import found - just prepend Weloop styles
|
|
543
866
|
return weloopStyles + '\n\n' + existing;
|
|
544
867
|
}
|
|
545
868
|
|
|
869
|
+
// Split existing CSS around the tailwindcss import
|
|
546
870
|
const beforeTailwind = existing.substring(0, existing.indexOf(tailwindMatch[0]));
|
|
547
871
|
const afterTailwind = existing.substring(existing.indexOf(tailwindMatch[0]) + tailwindMatch[0].length);
|
|
548
872
|
|
|
549
|
-
// Check if tw-animate-css import already exists
|
|
873
|
+
// Check if tw-animate-css import already exists
|
|
550
874
|
const hasTwAnimateInExisting = existing.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
|
|
551
875
|
const hasTwAnimateInWeloop = weloopStyles.match(/@import\s+["']tw-animate-css["'];?\s*\n?/);
|
|
552
876
|
|
|
877
|
+
// If existing file already has tw-animate import, remove it from Weloop styles
|
|
878
|
+
// to avoid duplicating it in the merge output
|
|
879
|
+
let weloopStylesCleaned = weloopStyles;
|
|
880
|
+
if (hasTwAnimateInExisting && hasTwAnimateInWeloop) {
|
|
881
|
+
weloopStylesCleaned = weloopStyles.replace(
|
|
882
|
+
/@import\s+["']tw-animate-css["'];?\s*\n?/g,
|
|
883
|
+
''
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Add tw-animate import if needed (package installed but import missing)
|
|
553
888
|
let importsToAdd = '';
|
|
554
889
|
if (hasTwAnimate && !hasTwAnimateInExisting && !hasTwAnimateInWeloop) {
|
|
555
890
|
importsToAdd = '@import "tw-animate-css";\n';
|
|
556
891
|
}
|
|
557
|
-
|
|
558
|
-
|
|
892
|
+
|
|
893
|
+
// Merge: before tailwind + tailwind import + tw-animate (if needed) + Weloop styles + after tailwind
|
|
894
|
+
return beforeTailwind + tailwindMatch[0] + importsToAdd + weloopStylesCleaned + '\n' + afterTailwind;
|
|
559
895
|
}
|
|
560
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
|
+
*/
|
|
561
909
|
function replaceWeloopStyles(existing, newStyles, hasTwAnimate) {
|
|
562
910
|
const importPattern = /(@import\s+["'][^"']+["'];?\s*\n?)/g;
|
|
563
911
|
const imports = existing.match(importPattern) || [];
|
|
564
912
|
const importsText = imports.join('');
|
|
565
913
|
|
|
914
|
+
// Find where Weloop styles start (look for @theme inline or :root)
|
|
566
915
|
const weloopStartPattern = /(@theme\s+inline|:root\s*\{)/;
|
|
567
916
|
const weloopStartMatch = existing.search(weloopStartPattern);
|
|
568
917
|
|
|
569
918
|
if (weloopStartMatch === -1) {
|
|
919
|
+
// No existing Weloop styles found - just append
|
|
570
920
|
return existing + '\n\n' + newStyles;
|
|
571
921
|
}
|
|
572
922
|
|
|
923
|
+
// Extract content before Weloop styles
|
|
573
924
|
let contentBeforeWeloop = existing.substring(0, weloopStartMatch);
|
|
925
|
+
// Keep non-Weloop imports (filter out tw-animate if package not installed)
|
|
574
926
|
const nonWeloopImports = importsText.split('\n').filter(imp =>
|
|
575
927
|
!imp.includes('tw-animate-css') || hasTwAnimate
|
|
576
928
|
).join('\n');
|
|
929
|
+
// Remove all imports from contentBeforeWeloop (we'll add them back)
|
|
577
930
|
contentBeforeWeloop = nonWeloopImports + '\n' + contentBeforeWeloop.replace(importPattern, '');
|
|
578
931
|
|
|
932
|
+
// Return: preserved imports + content before Weloop + new Weloop styles
|
|
579
933
|
return contentBeforeWeloop.trim() + '\n\n' + newStyles.trim();
|
|
580
934
|
}
|
|
581
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
|
+
*/
|
|
582
945
|
async function fetchCSSFromRegistry(registryUrl) {
|
|
583
946
|
let sourceCssPath;
|
|
584
947
|
|
|
585
948
|
if (isLocalPath(registryUrl)) {
|
|
949
|
+
// Local path: find app/globals.css relative to registry directory
|
|
586
950
|
const basePath = path.isAbsolute(registryUrl)
|
|
587
951
|
? path.dirname(registryUrl)
|
|
588
952
|
: path.join(process.cwd(), path.dirname(registryUrl));
|
|
589
953
|
sourceCssPath = path.join(basePath, 'app', 'globals.css');
|
|
590
954
|
} else {
|
|
955
|
+
// Remote URL: replace /registry with /app/globals.css
|
|
591
956
|
const baseUrl = registryUrl.replace('/registry', '');
|
|
592
957
|
sourceCssPath = `${baseUrl}/app/globals.css`;
|
|
593
958
|
}
|
|
@@ -595,17 +960,31 @@ async function fetchCSSFromRegistry(registryUrl) {
|
|
|
595
960
|
return await fetchText(sourceCssPath);
|
|
596
961
|
}
|
|
597
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
|
+
*/
|
|
598
976
|
async function installCSSStyles(config, registryUrl, forceUpdate = false, silent = false) {
|
|
599
977
|
const cssPath = config.tailwind?.css || 'app/globals.css';
|
|
600
978
|
const fullCssPath = path.join(process.cwd(), cssPath);
|
|
601
979
|
|
|
602
|
-
// Check if Weloop styles already exist
|
|
980
|
+
// Check if Weloop styles already exist in the file
|
|
603
981
|
let hasWeloopStylesInFile = false;
|
|
604
982
|
if (fs.existsSync(fullCssPath)) {
|
|
605
983
|
const existingContent = fs.readFileSync(fullCssPath, 'utf-8');
|
|
606
984
|
hasWeloopStylesInFile = hasWeloopStyles(existingContent);
|
|
607
985
|
|
|
608
|
-
// 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
|
|
609
988
|
if (hasWeloopStylesInFile && !forceUpdate && silent) {
|
|
610
989
|
return;
|
|
611
990
|
}
|
|
@@ -622,10 +1001,13 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
622
1001
|
const hasTwAnimate = checkPackageInstalled('tw-animate-css');
|
|
623
1002
|
let processedCssContent = processTwAnimateImport(cssContent, hasTwAnimate, forceUpdate);
|
|
624
1003
|
|
|
625
|
-
// Handle file installation
|
|
1004
|
+
// Handle file installation based on mode and existing file state
|
|
626
1005
|
if (forceUpdate && fs.existsSync(fullCssPath)) {
|
|
627
|
-
// --overwrite
|
|
628
|
-
|
|
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);
|
|
629
1011
|
if (!silent) {
|
|
630
1012
|
success(`Overwritten ${cssPath} with Weloop styles`);
|
|
631
1013
|
if (hasTwAnimate) {
|
|
@@ -633,30 +1015,36 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
633
1015
|
}
|
|
634
1016
|
}
|
|
635
1017
|
} else if (fs.existsSync(fullCssPath)) {
|
|
636
|
-
//
|
|
1018
|
+
// Mode 2: Normal update - Intelligently merge with existing styles
|
|
637
1019
|
const existing = fs.readFileSync(fullCssPath, 'utf-8');
|
|
638
1020
|
const hasTailwindImport = existing.includes('@import "tailwindcss"') ||
|
|
639
1021
|
existing.includes('@tailwind base');
|
|
640
1022
|
|
|
641
1023
|
if (hasWeloopStylesInFile) {
|
|
642
|
-
//
|
|
1024
|
+
// Case 2a: Weloop styles already exist - replace them with updated versions
|
|
643
1025
|
let weloopStyles = removeTailwindImport(processedCssContent);
|
|
644
1026
|
weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
|
|
645
|
-
|
|
1027
|
+
let finalContent = replaceWeloopStyles(existing, weloopStyles, hasTwAnimate);
|
|
1028
|
+
finalContent = removeDuplicateTwAnimateImports(finalContent);
|
|
1029
|
+
finalContent = removeDuplicateCustomVariant(finalContent);
|
|
1030
|
+
finalContent = normalizeCSSFormat(finalContent);
|
|
646
1031
|
|
|
647
|
-
// Only
|
|
1032
|
+
// Only write if content actually changed (prevents unnecessary file updates)
|
|
648
1033
|
if (finalContent !== existing) {
|
|
649
1034
|
fs.writeFileSync(fullCssPath, finalContent);
|
|
650
1035
|
if (!silent) {
|
|
651
1036
|
success(`Updated ${cssPath} with Weloop styles`);
|
|
652
1037
|
}
|
|
653
1038
|
}
|
|
654
|
-
// If no changes, silently skip
|
|
1039
|
+
// If no changes, silently skip (file is already up to date)
|
|
655
1040
|
} else if (hasTailwindImport) {
|
|
656
|
-
//
|
|
1041
|
+
// Case 2b: No Weloop styles but Tailwind exists - merge after Tailwind imports
|
|
657
1042
|
let weloopStyles = removeTailwindImport(processedCssContent);
|
|
658
1043
|
weloopStyles = ensureTwAnimateImport(weloopStyles, hasTwAnimate);
|
|
659
|
-
|
|
1044
|
+
let merged = mergeCSSWithTailwind(existing, weloopStyles, hasTwAnimate);
|
|
1045
|
+
merged = removeDuplicateTwAnimateImports(merged);
|
|
1046
|
+
merged = removeDuplicateCustomVariant(merged);
|
|
1047
|
+
merged = normalizeCSSFormat(merged);
|
|
660
1048
|
fs.writeFileSync(fullCssPath, merged);
|
|
661
1049
|
if (!silent) {
|
|
662
1050
|
success(`Updated ${cssPath} with Weloop styles`);
|
|
@@ -666,9 +1054,10 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
666
1054
|
info(` Your existing styles are preserved`);
|
|
667
1055
|
}
|
|
668
1056
|
} else {
|
|
669
|
-
// No Tailwind imports
|
|
1057
|
+
// Case 2c: No Tailwind imports - prepend Weloop styles to existing content
|
|
670
1058
|
let finalCssContent = removeDuplicateTwAnimateImports(processedCssContent);
|
|
671
1059
|
|
|
1060
|
+
// Ensure tw-animate import exists if package is installed
|
|
672
1061
|
if (hasTwAnimate && !finalCssContent.match(/@import\s+["']tw-animate-css["'];?\s*\n?/)) {
|
|
673
1062
|
if (finalCssContent.includes('@import "tailwindcss"')) {
|
|
674
1063
|
finalCssContent = finalCssContent.replace(
|
|
@@ -679,7 +1068,9 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
679
1068
|
finalCssContent = '@import "tailwindcss";\n@import "tw-animate-css";\n' + finalCssContent;
|
|
680
1069
|
}
|
|
681
1070
|
}
|
|
682
|
-
|
|
1071
|
+
let combined = finalCssContent + '\n\n' + existing;
|
|
1072
|
+
combined = normalizeCSSFormat(combined);
|
|
1073
|
+
fs.writeFileSync(fullCssPath, combined);
|
|
683
1074
|
if (!silent) {
|
|
684
1075
|
success(`Updated ${cssPath} with Weloop styles`);
|
|
685
1076
|
if (hasTwAnimate) {
|
|
@@ -688,8 +1079,11 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
688
1079
|
}
|
|
689
1080
|
}
|
|
690
1081
|
} else {
|
|
691
|
-
//
|
|
692
|
-
|
|
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);
|
|
693
1087
|
if (!silent) {
|
|
694
1088
|
success(`Created ${cssPath} with Weloop styles`);
|
|
695
1089
|
if (hasTwAnimate) {
|
|
@@ -709,13 +1103,22 @@ async function installCSSStyles(config, registryUrl, forceUpdate = false, silent
|
|
|
709
1103
|
}
|
|
710
1104
|
|
|
711
1105
|
// ============================================================================
|
|
712
|
-
// COMPONENT INSTALLATION
|
|
1106
|
+
// COMPONENT INSTALLATION - Installing Components from Registry
|
|
713
1107
|
// ============================================================================
|
|
714
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
|
+
*/
|
|
715
1117
|
function createUtilsFile(utilsPath) {
|
|
716
1118
|
if (fs.existsSync(utilsPath)) return;
|
|
717
1119
|
|
|
718
1120
|
// Silently create utils file (matching shadcn/ui behavior)
|
|
1121
|
+
// Don't overwrite if user has customized it
|
|
719
1122
|
ensureDirectoryExists(path.dirname(utilsPath));
|
|
720
1123
|
|
|
721
1124
|
const utilsContent = `import { clsx, type ClassValue } from "clsx"
|
|
@@ -728,10 +1131,27 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
728
1131
|
fs.writeFileSync(utilsPath, utilsContent);
|
|
729
1132
|
}
|
|
730
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
|
+
*/
|
|
731
1151
|
async function installComponent(componentName, options = {}) {
|
|
732
1152
|
const { overwrite = false, registryUrl = DEFAULT_REGISTRY_URL } = options;
|
|
733
1153
|
|
|
734
|
-
// Install base dependencies
|
|
1154
|
+
// Step 1: Install base dependencies (required for utils.ts to work)
|
|
735
1155
|
const baseDeps = [];
|
|
736
1156
|
if (!checkPackageInstalled('clsx')) {
|
|
737
1157
|
baseDeps.push('clsx');
|
|
@@ -744,19 +1164,20 @@ async function installComponent(componentName, options = {}) {
|
|
|
744
1164
|
await installPackages(baseDeps);
|
|
745
1165
|
}
|
|
746
1166
|
|
|
747
|
-
// Load or create components.json
|
|
1167
|
+
// Step 2: Load or create components.json configuration
|
|
748
1168
|
const config = loadComponentsConfig();
|
|
749
1169
|
|
|
750
|
-
// Install tw-animate-css automatically (required for animations)
|
|
1170
|
+
// Step 3: Install tw-animate-css automatically (required for component animations)
|
|
751
1171
|
if (!checkPackageInstalled('tw-animate-css')) {
|
|
752
1172
|
info('Installing tw-animate-css for animations...');
|
|
753
1173
|
await installPackages(['tw-animate-css']);
|
|
754
1174
|
}
|
|
755
1175
|
|
|
756
|
-
// 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
|
|
757
1178
|
await installCSSStyles(config, registryUrl, false, true);
|
|
758
1179
|
|
|
759
|
-
//
|
|
1180
|
+
// Step 5: Resolve paths from components.json configuration
|
|
760
1181
|
const uiAlias = config.aliases?.ui || '@/components/ui';
|
|
761
1182
|
const utilsAlias = config.aliases?.utils || '@/lib/utils';
|
|
762
1183
|
|
|
@@ -767,30 +1188,34 @@ async function installComponent(componentName, options = {}) {
|
|
|
767
1188
|
}
|
|
768
1189
|
utilsPath = path.join(process.cwd(), utilsPath);
|
|
769
1190
|
|
|
770
|
-
// Load component registry
|
|
1191
|
+
// Step 6: Load component registry from remote or local source
|
|
771
1192
|
const registry = await loadRegistry(componentName, registryUrl);
|
|
772
1193
|
|
|
773
1194
|
if (!registry) {
|
|
774
1195
|
error(`Component "${componentName}" not found in registry.`);
|
|
775
1196
|
info('Available components:');
|
|
776
1197
|
try {
|
|
1198
|
+
// Show available components to help user
|
|
777
1199
|
const index = await loadIndex(registryUrl);
|
|
778
1200
|
index.registry.forEach(comp => {
|
|
779
1201
|
console.log(` - ${comp.name}`);
|
|
780
1202
|
});
|
|
781
1203
|
} catch (e) {
|
|
782
|
-
// Ignore if index fails
|
|
1204
|
+
// Ignore if index fails - we already showed the error
|
|
783
1205
|
}
|
|
784
1206
|
process.exit(1);
|
|
785
1207
|
}
|
|
786
1208
|
|
|
787
|
-
// 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
|
|
788
1212
|
const componentPath = path.join(componentsDir, `${componentName}.tsx`);
|
|
789
1213
|
if (fs.existsSync(componentPath) && !overwrite) {
|
|
790
1214
|
return;
|
|
791
1215
|
}
|
|
792
1216
|
|
|
793
|
-
// Install
|
|
1217
|
+
// Step 8: Install component dependencies first (recursive)
|
|
1218
|
+
// Some components depend on other components (e.g., button-group depends on button)
|
|
794
1219
|
if (registry.registryDependencies && registry.registryDependencies.length > 0) {
|
|
795
1220
|
info(`Installing dependencies: ${registry.registryDependencies.join(', ')}`);
|
|
796
1221
|
for (const dep of registry.registryDependencies) {
|
|
@@ -798,7 +1223,8 @@ async function installComponent(componentName, options = {}) {
|
|
|
798
1223
|
}
|
|
799
1224
|
}
|
|
800
1225
|
|
|
801
|
-
// Install component files
|
|
1226
|
+
// Step 9: Install component files
|
|
1227
|
+
// Components can have multiple files (e.g., component + types + styles)
|
|
802
1228
|
for (const file of registry.files) {
|
|
803
1229
|
const filePath = path.join(process.cwd(), file.path);
|
|
804
1230
|
ensureDirectoryExists(path.dirname(filePath));
|
|
@@ -806,10 +1232,11 @@ async function installComponent(componentName, options = {}) {
|
|
|
806
1232
|
success(`Installed: ${file.path}`);
|
|
807
1233
|
}
|
|
808
1234
|
|
|
809
|
-
// Create utils.ts if
|
|
1235
|
+
// Step 10: Create utils.ts if it doesn't exist
|
|
1236
|
+
// This is required for all components to work
|
|
810
1237
|
createUtilsFile(utilsPath);
|
|
811
1238
|
|
|
812
|
-
// Install npm dependencies
|
|
1239
|
+
// Step 11: Install npm dependencies required by the component
|
|
813
1240
|
if (registry.dependencies && registry.dependencies.length > 0) {
|
|
814
1241
|
const missingDeps = getMissingDependencies(registry.dependencies);
|
|
815
1242
|
if (missingDeps.length > 0) {
|
|
@@ -823,15 +1250,22 @@ async function installComponent(componentName, options = {}) {
|
|
|
823
1250
|
}
|
|
824
1251
|
|
|
825
1252
|
// ============================================================================
|
|
826
|
-
// COMMANDS
|
|
1253
|
+
// COMMANDS - CLI Command Handlers
|
|
827
1254
|
// ============================================================================
|
|
828
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
|
+
*/
|
|
829
1262
|
async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
|
|
830
1263
|
try {
|
|
831
1264
|
const index = await loadIndex(registryUrl);
|
|
832
1265
|
|
|
833
1266
|
console.log('\nAvailable components:\n');
|
|
834
1267
|
index.registry.forEach(comp => {
|
|
1268
|
+
// Show dependencies if component has any
|
|
835
1269
|
const deps = comp.registryDependencies && comp.registryDependencies.length > 0
|
|
836
1270
|
? ` (depends on: ${comp.registryDependencies.join(', ')})`
|
|
837
1271
|
: '';
|
|
@@ -845,24 +1279,39 @@ async function listComponents(registryUrl = DEFAULT_REGISTRY_URL) {
|
|
|
845
1279
|
}
|
|
846
1280
|
|
|
847
1281
|
// ============================================================================
|
|
848
|
-
// MAIN
|
|
1282
|
+
// MAIN - CLI Entry Point
|
|
849
1283
|
// ============================================================================
|
|
850
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
|
+
*/
|
|
851
1297
|
async function main() {
|
|
852
1298
|
const args = process.argv.slice(2);
|
|
853
1299
|
const command = args[0];
|
|
854
1300
|
const componentName = args[1];
|
|
855
1301
|
|
|
1302
|
+
// Parse --registry option if provided
|
|
856
1303
|
const registryIndex = args.indexOf('--registry');
|
|
857
1304
|
const registryUrl = registryIndex !== -1 && args[registryIndex + 1]
|
|
858
1305
|
? args[registryIndex + 1]
|
|
859
1306
|
: DEFAULT_REGISTRY_URL;
|
|
860
1307
|
|
|
1308
|
+
// Build options object
|
|
861
1309
|
const options = {
|
|
862
1310
|
overwrite: args.includes('--overwrite') || args.includes('-f'),
|
|
863
1311
|
registryUrl: registryUrl,
|
|
864
1312
|
};
|
|
865
1313
|
|
|
1314
|
+
// Show usage if no command provided
|
|
866
1315
|
if (!command) {
|
|
867
1316
|
console.log(`
|
|
868
1317
|
Usage:
|
|
@@ -884,6 +1333,7 @@ Examples:
|
|
|
884
1333
|
process.exit(0);
|
|
885
1334
|
}
|
|
886
1335
|
|
|
1336
|
+
// Route to appropriate command handler
|
|
887
1337
|
switch (command) {
|
|
888
1338
|
case 'add':
|
|
889
1339
|
if (!componentName) {
|
|
@@ -900,6 +1350,7 @@ Examples:
|
|
|
900
1350
|
|
|
901
1351
|
case 'css':
|
|
902
1352
|
case 'styles':
|
|
1353
|
+
// Both 'css' and 'styles' commands do the same thing
|
|
903
1354
|
const config = loadComponentsConfig();
|
|
904
1355
|
await installCSSStyles(config, registryUrl, options.overwrite, false);
|
|
905
1356
|
break;
|
|
@@ -911,6 +1362,7 @@ Examples:
|
|
|
911
1362
|
}
|
|
912
1363
|
}
|
|
913
1364
|
|
|
1365
|
+
// Run main function and handle errors
|
|
914
1366
|
main().catch(err => {
|
|
915
1367
|
error(`Error: ${err.message}`);
|
|
916
1368
|
process.exit(1);
|