@uiscore/cli 0.1.5 → 0.1.6

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.
Files changed (3) hide show
  1. package/README.md +13 -9
  2. package/dist/index.js +396 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -32,7 +32,17 @@ uiscore add button
32
32
 
33
33
  ## How It Works
34
34
 
35
- `uiscore add <name>` does four things:
35
+ `uiscore init` prepares the consumer project infrastructure:
36
+
37
+ 1. Creates `uiscore.config.json`.
38
+ 2. Installs `tailwindcss`, `postcss` and `autoprefixer` if they are missing.
39
+ 3. Creates or updates `tailwind.config.js` with UIScore content globs.
40
+ 4. Creates `postcss.config.js` if it is missing.
41
+ 5. Creates `src/shared/ui/styles/uiscore.css` with font import and token markers.
42
+ 6. Injects Tailwind directives and the UIScore styles import into the project's global CSS.
43
+ 7. Ensures the application entrypoint imports the global CSS file.
44
+
45
+ `uiscore add <name>` then does four things:
36
46
 
37
47
  1. Reads `uiscore.config.json` from the current project.
38
48
  2. Downloads the registry item JSON from `registryUrl`.
@@ -45,7 +55,7 @@ If the registry item contains CSS variables, the CLI also writes them into the c
45
55
 
46
56
  ### `uiscore init`
47
57
 
48
- Creates `uiscore.config.json` in the current project root.
58
+ Bootstraps the consumer project for UIScore components.
49
59
 
50
60
  Example:
51
61
 
@@ -113,13 +123,7 @@ Then:
113
123
  npx @uiscore/cli add button
114
124
  ```
115
125
 
116
- After that, import your generated styles once in the app entrypoint or root layout:
117
-
118
- ```ts
119
- import "@/shared/ui/styles/uiscore.css";
120
- ```
121
-
122
- The CLI does not inject this import automatically.
126
+ If `uiscore init` was used first, the global CSS import is already wired automatically.
123
127
 
124
128
  ## Notes
125
129
 
package/dist/index.js CHANGED
@@ -71,6 +71,15 @@ function dependencyInstallArgs(packageManager, dependencies) {
71
71
  }
72
72
  return ["install", ...dependencies];
73
73
  }
74
+ function devDependencyInstallArgs(packageManager, dependencies) {
75
+ if (packageManager === "pnpm") {
76
+ return ["add", "-D", ...dependencies];
77
+ }
78
+ if (packageManager === "yarn") {
79
+ return ["add", "-D", ...dependencies];
80
+ }
81
+ return ["install", "-D", ...dependencies];
82
+ }
74
83
 
75
84
  // src/commands/add.ts
76
85
  async function runAddCommand(options) {
@@ -124,7 +133,7 @@ async function runAddCommand(options) {
124
133
  for (const filePath of writtenFiles) {
125
134
  info(`- ${filePath}`);
126
135
  }
127
- info(`Import your generated styles once if needed: ${config.stylesPath}`);
136
+ info(`UIScore styles file: ${config.stylesPath}`);
128
137
  }
129
138
  function writeCssVars({
130
139
  cwd,
@@ -168,19 +177,400 @@ function escapeForRegExp(value) {
168
177
  }
169
178
 
170
179
  // src/commands/init.ts
171
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
180
+ import {
181
+ existsSync as existsSync4,
182
+ mkdirSync as mkdirSync2,
183
+ readFileSync as readFileSync3,
184
+ writeFileSync as writeFileSync2
185
+ } from "fs";
186
+ import { spawnSync as spawnSync2 } from "child_process";
172
187
  import path4 from "path";
173
188
  function runInitCommand(cwd) {
174
189
  const configPath = getConfigPath(cwd);
190
+ const packageJsonPath = path4.join(cwd, "package.json");
191
+ if (!existsSync4(packageJsonPath)) {
192
+ throw new Error(
193
+ "No package.json found in the current directory. Run `uiscore init` inside a project root."
194
+ );
195
+ }
175
196
  if (existsSync4(configPath)) {
176
197
  warn(`Config already exists: ${configPath}`);
198
+ } else {
199
+ mkdirSync2(path4.dirname(configPath), { recursive: true });
200
+ writeFileSync2(
201
+ configPath,
202
+ `${JSON.stringify(DEFAULT_CONFIG, null, 2)}
203
+ `,
204
+ "utf8"
205
+ );
206
+ info(`Created ${configPath}`);
207
+ }
208
+ const config = loadConfig(cwd);
209
+ const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
210
+ const packageManager = detectPackageManager(cwd);
211
+ ensureTailwindDependencies({
212
+ cwd,
213
+ packageManager,
214
+ packageJson
215
+ });
216
+ const environment = detectEnvironment(cwd);
217
+ ensureTailwindConfig({
218
+ cwd,
219
+ isEsmProject: packageJson.type === "module",
220
+ sourceRoot: config.sourceRoot
221
+ });
222
+ ensurePostcssConfig({
223
+ cwd,
224
+ isEsmProject: packageJson.type === "module"
225
+ });
226
+ ensureDirectory(path4.join(cwd, config.sourceRoot, "components"));
227
+ ensureDirectory(path4.join(cwd, config.sourceRoot, "lib"));
228
+ ensureDirectory(path4.join(cwd, path4.dirname(config.stylesPath)));
229
+ ensureUiscoreStylesFile({
230
+ cwd,
231
+ stylesPath: config.stylesPath
232
+ });
233
+ const globalCssPath = ensureGlobalCssFile({
234
+ cwd,
235
+ environment
236
+ });
237
+ ensureGlobalCssContent({
238
+ globalCssPath,
239
+ stylesPath: path4.join(cwd, config.stylesPath)
240
+ });
241
+ ensureEntryImportsGlobalCss({
242
+ cwd,
243
+ environment,
244
+ globalCssPath
245
+ });
246
+ info("UIScore infrastructure initialized.");
247
+ info("You can now run `uiscore add button`.");
248
+ }
249
+ function ensureTailwindDependencies({
250
+ cwd,
251
+ packageManager,
252
+ packageJson
253
+ }) {
254
+ const requiredDependencies = [
255
+ "tailwindcss@^3.4.17",
256
+ "postcss@^8.5.8",
257
+ "autoprefixer@^10.4.27"
258
+ ];
259
+ const installedDependencies = {
260
+ ...packageJson.dependencies,
261
+ ...packageJson.devDependencies
262
+ };
263
+ const missingDependencies = requiredDependencies.filter((dependency) => {
264
+ const [name] = dependency.split("@", 1);
265
+ return !installedDependencies[name];
266
+ });
267
+ if (!missingDependencies.length) {
177
268
  return;
178
269
  }
179
- mkdirSync2(path4.dirname(configPath), { recursive: true });
180
- writeFileSync2(configPath, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}
181
- `, "utf8");
270
+ info(`Installing infrastructure dependencies with ${packageManager}...`);
271
+ const result = spawnSync2(
272
+ packageManager,
273
+ devDependencyInstallArgs(packageManager, requiredDependencies),
274
+ {
275
+ cwd,
276
+ stdio: "inherit",
277
+ shell: process.platform === "win32"
278
+ }
279
+ );
280
+ if (result.status !== 0) {
281
+ throw new Error("Failed to install Tailwind infrastructure dependencies.");
282
+ }
283
+ }
284
+ function detectEnvironment(cwd) {
285
+ const nextLayoutFiles = [
286
+ path4.join(cwd, "src", "app", "layout.tsx"),
287
+ path4.join(cwd, "src", "app", "layout.jsx"),
288
+ path4.join(cwd, "app", "layout.tsx"),
289
+ path4.join(cwd, "app", "layout.jsx")
290
+ ];
291
+ for (const layoutPath of nextLayoutFiles) {
292
+ if (existsSync4(layoutPath)) {
293
+ return {
294
+ type: "next",
295
+ layoutPath
296
+ };
297
+ }
298
+ }
299
+ const viteMainFiles = [
300
+ path4.join(cwd, "src", "main.tsx"),
301
+ path4.join(cwd, "src", "main.jsx"),
302
+ path4.join(cwd, "src", "main.ts"),
303
+ path4.join(cwd, "src", "main.js")
304
+ ];
305
+ for (const mainPath of viteMainFiles) {
306
+ if (existsSync4(mainPath)) {
307
+ return {
308
+ type: "vite",
309
+ mainPath
310
+ };
311
+ }
312
+ }
313
+ return {
314
+ type: "generic"
315
+ };
316
+ }
317
+ function ensureTailwindConfig({
318
+ cwd,
319
+ isEsmProject,
320
+ sourceRoot
321
+ }) {
322
+ const configPath = path4.join(cwd, "tailwind.config.js");
323
+ const contentPatterns = [
324
+ "./index.html",
325
+ "./src/**/*.{js,ts,jsx,tsx,mdx}",
326
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
327
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
328
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
329
+ `./${normalizeForGlob(sourceRoot)}/**/*.{js,ts,jsx,tsx,mdx}`
330
+ ];
331
+ const nextConfigSource = buildTailwindConfigSource({
332
+ isEsmProject,
333
+ contentPatterns
334
+ });
335
+ if (!existsSync4(configPath)) {
336
+ writeFileSync2(configPath, nextConfigSource, "utf8");
337
+ info(`Created ${configPath}`);
338
+ return;
339
+ }
340
+ const current = readFileSync3(configPath, "utf8");
341
+ if (contentPatterns.every((pattern) => current.includes(pattern))) {
342
+ return;
343
+ }
344
+ if (!current.includes("content")) {
345
+ warn(`Tailwind config exists but has no content field: ${configPath}`);
346
+ return;
347
+ }
348
+ const markerPattern = /content\s*:\s*\[([\s\S]*?)\]/m;
349
+ const match = current.match(markerPattern);
350
+ if (!match) {
351
+ warn(`Could not safely update Tailwind content globs in ${configPath}`);
352
+ return;
353
+ }
354
+ const existingBlock = match[1];
355
+ const missingPatterns = contentPatterns.filter(
356
+ (pattern) => !existingBlock.includes(pattern)
357
+ );
358
+ if (!missingPatterns.length) {
359
+ return;
360
+ }
361
+ const insertion = missingPatterns.map((pattern) => ` "${pattern}",`).join("\n");
362
+ const replacement = `content: [
363
+ ${existingBlock.trimEnd()}
364
+ ${insertion}
365
+ ]`;
366
+ const updated = current.replace(markerPattern, replacement);
367
+ writeFileSync2(configPath, updated, "utf8");
368
+ info(`Updated Tailwind content globs in ${configPath}`);
369
+ }
370
+ function buildTailwindConfigSource({
371
+ isEsmProject,
372
+ contentPatterns
373
+ }) {
374
+ const lines = contentPatterns.map((pattern) => ` "${pattern}",`).join("\n");
375
+ if (isEsmProject) {
376
+ return `/** @type {import("tailwindcss").Config} */
377
+ export default {
378
+ content: [
379
+ ${lines}
380
+ ],
381
+ theme: {
382
+ extend: {},
383
+ },
384
+ plugins: [],
385
+ };
386
+ `;
387
+ }
388
+ return `/** @type {import("tailwindcss").Config} */
389
+ module.exports = {
390
+ content: [
391
+ ${lines}
392
+ ],
393
+ theme: {
394
+ extend: {},
395
+ },
396
+ plugins: [],
397
+ };
398
+ `;
399
+ }
400
+ function ensurePostcssConfig({
401
+ cwd,
402
+ isEsmProject
403
+ }) {
404
+ const configPath = path4.join(cwd, "postcss.config.js");
405
+ if (existsSync4(configPath)) {
406
+ return;
407
+ }
408
+ const content = isEsmProject ? `export default {
409
+ plugins: {
410
+ tailwindcss: {},
411
+ autoprefixer: {},
412
+ },
413
+ };
414
+ ` : `module.exports = {
415
+ plugins: {
416
+ tailwindcss: {},
417
+ autoprefixer: {},
418
+ },
419
+ };
420
+ `;
421
+ writeFileSync2(configPath, content, "utf8");
182
422
  info(`Created ${configPath}`);
183
- info("Update registryUrl before running `uiscore add` if needed.");
423
+ }
424
+ function ensureUiscoreStylesFile({
425
+ cwd,
426
+ stylesPath
427
+ }) {
428
+ const outputPath = path4.join(cwd, stylesPath);
429
+ const fontImport = '@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700&display=swap");';
430
+ const markerStart = "/* uiscore:tokens:start */";
431
+ const markerEnd = "/* uiscore:tokens:end */";
432
+ if (!existsSync4(outputPath)) {
433
+ writeFileSync2(
434
+ outputPath,
435
+ `${fontImport}
436
+
437
+ ${markerStart}
438
+ :root {
439
+ }
440
+ ${markerEnd}
441
+ `,
442
+ "utf8"
443
+ );
444
+ info(`Created ${outputPath}`);
445
+ return;
446
+ }
447
+ let current = readFileSync3(outputPath, "utf8");
448
+ if (!current.includes(fontImport)) {
449
+ current = `${fontImport}
450
+
451
+ ${current.trimStart()}`;
452
+ }
453
+ if (!current.includes(markerStart) || !current.includes(markerEnd)) {
454
+ current = `${current.trimEnd()}
455
+
456
+ ${markerStart}
457
+ :root {
458
+ }
459
+ ${markerEnd}
460
+ `;
461
+ }
462
+ writeFileSync2(outputPath, `${current.trimEnd()}
463
+ `, "utf8");
464
+ }
465
+ function ensureGlobalCssFile({
466
+ cwd,
467
+ environment
468
+ }) {
469
+ const candidates = environment.type === "next" ? [path4.join(cwd, "src", "app", "globals.css"), path4.join(cwd, "app", "globals.css")] : [path4.join(cwd, "src", "index.css")];
470
+ for (const candidate of candidates) {
471
+ if (existsSync4(candidate)) {
472
+ return candidate;
473
+ }
474
+ }
475
+ const fallbackPath = environment.type === "next" ? candidates.find((candidate) => candidate.includes(path4.join("src", "app"))) ?? candidates[0] : path4.join(cwd, "src", "index.css");
476
+ ensureDirectory(path4.dirname(fallbackPath));
477
+ writeFileSync2(fallbackPath, "", "utf8");
478
+ info(`Created ${fallbackPath}`);
479
+ return fallbackPath;
480
+ }
481
+ function ensureGlobalCssContent({
482
+ globalCssPath,
483
+ stylesPath
484
+ }) {
485
+ const stylesImportPath = toCssRelativeImport(globalCssPath, stylesPath);
486
+ const stylesBlock = `/* uiscore:styles:start */
487
+ @import "${stylesImportPath}";
488
+ /* uiscore:styles:end */`;
489
+ const tailwindBlock = `/* uiscore:tailwind:start */
490
+ @tailwind base;
491
+ @tailwind components;
492
+ @tailwind utilities;
493
+ /* uiscore:tailwind:end */`;
494
+ const current = existsSync4(globalCssPath) ? readFileSync3(globalCssPath, "utf8") : "";
495
+ const withoutManagedBlocks = current.replace(
496
+ /\/\* uiscore:styles:start \*\/[\s\S]*?\/\* uiscore:styles:end \*\/\s*/gm,
497
+ ""
498
+ ).replace(
499
+ /\/\* uiscore:tailwind:start \*\/[\s\S]*?\/\* uiscore:tailwind:end \*\/\s*/gm,
500
+ ""
501
+ ).trimStart();
502
+ const updated = `${stylesBlock}
503
+
504
+ ${tailwindBlock}${withoutManagedBlocks ? `
505
+
506
+ ${withoutManagedBlocks}` : ""}`;
507
+ writeFileSync2(globalCssPath, `${updated.trimEnd()}
508
+ `, "utf8");
509
+ }
510
+ function ensureEntryImportsGlobalCss({
511
+ cwd,
512
+ environment,
513
+ globalCssPath
514
+ }) {
515
+ if (environment.type === "vite") {
516
+ ensureImportInCodeFile({
517
+ filePath: environment.mainPath,
518
+ importPath: toCodeRelativeImport(environment.mainPath, globalCssPath)
519
+ });
520
+ return;
521
+ }
522
+ if (environment.type === "next") {
523
+ ensureImportInCodeFile({
524
+ filePath: environment.layoutPath,
525
+ importPath: toCodeRelativeImport(environment.layoutPath, globalCssPath)
526
+ });
527
+ }
528
+ }
529
+ function ensureImportInCodeFile({
530
+ filePath,
531
+ importPath
532
+ }) {
533
+ const source = readFileSync3(filePath, "utf8");
534
+ const normalizedImport = normalizeImportPath(importPath);
535
+ const importStatement = `import "${normalizedImport}";`;
536
+ const importPattern = new RegExp(
537
+ `^\\s*import\\s+["']${escapeForRegExp2(normalizedImport)}["'];?\\s*$`,
538
+ "m"
539
+ );
540
+ const allImportPattern = new RegExp(
541
+ `^\\s*import\\s+["']${escapeForRegExp2(normalizedImport)}["'];?\\s*$\\n?`,
542
+ "gm"
543
+ );
544
+ if (!importPattern.test(source)) {
545
+ writeFileSync2(filePath, `${importStatement}
546
+ ${source}`, "utf8");
547
+ return;
548
+ }
549
+ const withoutDuplicateImports = source.replace(allImportPattern, "");
550
+ writeFileSync2(filePath, `${importStatement}
551
+ ${withoutDuplicateImports}`, "utf8");
552
+ }
553
+ function ensureDirectory(directoryPath) {
554
+ mkdirSync2(directoryPath, { recursive: true });
555
+ }
556
+ function toCssRelativeImport(fromFile, toFile) {
557
+ return normalizeImportPath(path4.relative(path4.dirname(fromFile), toFile));
558
+ }
559
+ function toCodeRelativeImport(fromFile, toFile) {
560
+ return normalizeImportPath(path4.relative(path4.dirname(fromFile), toFile));
561
+ }
562
+ function normalizeImportPath(value) {
563
+ const normalized = value.split(path4.sep).join("/");
564
+ if (normalized.startsWith(".")) {
565
+ return normalized;
566
+ }
567
+ return `./${normalized}`;
568
+ }
569
+ function normalizeForGlob(value) {
570
+ return value.split(path4.sep).join("/");
571
+ }
572
+ function escapeForRegExp2(value) {
573
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
184
574
  }
185
575
 
186
576
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uiscore/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "CLI for installing UIScore registry components into projects.",
5
5
  "type": "module",
6
6
  "bin": {