@uiscore/cli 0.1.5 → 0.1.7

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 +429 -11
  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,8 +71,18 @@ 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
85
+ var UISCORE_FONT_IMPORT = '@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700&display=swap");';
76
86
  async function runAddCommand(options) {
77
87
  const config = loadConfig(options.cwd);
78
88
  const url = config.registryUrl.replace("{name}", options.name);
@@ -124,7 +134,7 @@ async function runAddCommand(options) {
124
134
  for (const filePath of writtenFiles) {
125
135
  info(`- ${filePath}`);
126
136
  }
127
- info(`Import your generated styles once if needed: ${config.stylesPath}`);
137
+ info(`UIScore styles file: ${config.stylesPath}`);
128
138
  }
129
139
  function writeCssVars({
130
140
  cwd,
@@ -138,19 +148,30 @@ function writeCssVars({
138
148
  const markerEnd = "/* uiscore:tokens:end */";
139
149
  const nextBlock = buildCssVarBlock(cssVars, markerStart, markerEnd);
140
150
  if (!existing) {
141
- writeFileSync(outputPath, nextBlock, "utf8");
151
+ writeFileSync(outputPath, `${UISCORE_FONT_IMPORT}
152
+
153
+ ${nextBlock}`, "utf8");
142
154
  return;
143
155
  }
144
- if (existing.includes(markerStart) && existing.includes(markerEnd)) {
145
- const updated = existing.replace(
156
+ const withFontImport = existing.includes(UISCORE_FONT_IMPORT) ? existing : `${UISCORE_FONT_IMPORT}
157
+
158
+ ${existing.trimStart()}`;
159
+ if (withFontImport.includes(markerStart) && withFontImport.includes(markerEnd)) {
160
+ const existingCssVars = parseCssVarsFromBlock(withFontImport, markerStart, markerEnd);
161
+ const mergedCssVars = {
162
+ ...existingCssVars,
163
+ ...cssVars
164
+ };
165
+ const mergedBlock = buildCssVarBlock(mergedCssVars, markerStart, markerEnd);
166
+ const updated = withFontImport.replace(
146
167
  new RegExp(`${escapeForRegExp(markerStart)}[\\s\\S]*?${escapeForRegExp(markerEnd)}`),
147
- nextBlock.trimEnd()
168
+ mergedBlock.trimEnd()
148
169
  );
149
170
  writeFileSync(outputPath, `${updated.trimEnd()}
150
171
  `, "utf8");
151
172
  return;
152
173
  }
153
- writeFileSync(outputPath, `${existing.trimEnd()}
174
+ writeFileSync(outputPath, `${withFontImport.trimEnd()}
154
175
 
155
176
  ${nextBlock}`, "utf8");
156
177
  }
@@ -163,24 +184,421 @@ ${vars}
163
184
  ${markerEnd}
164
185
  `;
165
186
  }
187
+ function parseCssVarsFromBlock(source, markerStart, markerEnd) {
188
+ const blockMatch = source.match(
189
+ new RegExp(
190
+ `${escapeForRegExp(markerStart)}([\\s\\S]*?)${escapeForRegExp(markerEnd)}`
191
+ )
192
+ );
193
+ if (!blockMatch) {
194
+ return {};
195
+ }
196
+ const cssVars = {};
197
+ const matches = blockMatch[1].matchAll(/--([a-z0-9-]+)\s*:\s*([^;]+);/gi);
198
+ for (const match of matches) {
199
+ cssVars[match[1]] = match[2].trim();
200
+ }
201
+ return cssVars;
202
+ }
166
203
  function escapeForRegExp(value) {
167
204
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
168
205
  }
169
206
 
170
207
  // src/commands/init.ts
171
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
208
+ import {
209
+ existsSync as existsSync4,
210
+ mkdirSync as mkdirSync2,
211
+ readFileSync as readFileSync3,
212
+ writeFileSync as writeFileSync2
213
+ } from "fs";
214
+ import { spawnSync as spawnSync2 } from "child_process";
172
215
  import path4 from "path";
173
216
  function runInitCommand(cwd) {
174
217
  const configPath = getConfigPath(cwd);
218
+ const packageJsonPath = path4.join(cwd, "package.json");
219
+ if (!existsSync4(packageJsonPath)) {
220
+ throw new Error(
221
+ "No package.json found in the current directory. Run `uiscore init` inside a project root."
222
+ );
223
+ }
175
224
  if (existsSync4(configPath)) {
176
225
  warn(`Config already exists: ${configPath}`);
226
+ } else {
227
+ mkdirSync2(path4.dirname(configPath), { recursive: true });
228
+ writeFileSync2(
229
+ configPath,
230
+ `${JSON.stringify(DEFAULT_CONFIG, null, 2)}
231
+ `,
232
+ "utf8"
233
+ );
234
+ info(`Created ${configPath}`);
235
+ }
236
+ const config = loadConfig(cwd);
237
+ const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf8"));
238
+ const packageManager = detectPackageManager(cwd);
239
+ ensureTailwindDependencies({
240
+ cwd,
241
+ packageManager,
242
+ packageJson
243
+ });
244
+ const environment = detectEnvironment(cwd);
245
+ ensureTailwindConfig({
246
+ cwd,
247
+ isEsmProject: packageJson.type === "module",
248
+ sourceRoot: config.sourceRoot
249
+ });
250
+ ensurePostcssConfig({
251
+ cwd,
252
+ isEsmProject: packageJson.type === "module"
253
+ });
254
+ ensureDirectory(path4.join(cwd, config.sourceRoot, "components"));
255
+ ensureDirectory(path4.join(cwd, config.sourceRoot, "lib"));
256
+ ensureDirectory(path4.join(cwd, path4.dirname(config.stylesPath)));
257
+ ensureUiscoreStylesFile({
258
+ cwd,
259
+ stylesPath: config.stylesPath
260
+ });
261
+ const globalCssPath = ensureGlobalCssFile({
262
+ cwd,
263
+ environment
264
+ });
265
+ ensureGlobalCssContent({
266
+ globalCssPath,
267
+ stylesPath: path4.join(cwd, config.stylesPath)
268
+ });
269
+ ensureEntryImportsGlobalCss({
270
+ cwd,
271
+ environment,
272
+ globalCssPath
273
+ });
274
+ info("UIScore infrastructure initialized.");
275
+ info("You can now run `uiscore add button`.");
276
+ }
277
+ function ensureTailwindDependencies({
278
+ cwd,
279
+ packageManager,
280
+ packageJson
281
+ }) {
282
+ const requiredDependencies = [
283
+ "tailwindcss@^3.4.17",
284
+ "postcss@^8.5.8",
285
+ "autoprefixer@^10.4.27"
286
+ ];
287
+ const installedDependencies = {
288
+ ...packageJson.dependencies,
289
+ ...packageJson.devDependencies
290
+ };
291
+ const missingDependencies = requiredDependencies.filter((dependency) => {
292
+ const [name] = dependency.split("@", 1);
293
+ return !installedDependencies[name];
294
+ });
295
+ if (!missingDependencies.length) {
177
296
  return;
178
297
  }
179
- mkdirSync2(path4.dirname(configPath), { recursive: true });
180
- writeFileSync2(configPath, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}
181
- `, "utf8");
298
+ info(`Installing infrastructure dependencies with ${packageManager}...`);
299
+ const result = spawnSync2(
300
+ packageManager,
301
+ devDependencyInstallArgs(packageManager, requiredDependencies),
302
+ {
303
+ cwd,
304
+ stdio: "inherit",
305
+ shell: process.platform === "win32"
306
+ }
307
+ );
308
+ if (result.status !== 0) {
309
+ throw new Error("Failed to install Tailwind infrastructure dependencies.");
310
+ }
311
+ }
312
+ function detectEnvironment(cwd) {
313
+ const nextLayoutFiles = [
314
+ path4.join(cwd, "src", "app", "layout.tsx"),
315
+ path4.join(cwd, "src", "app", "layout.jsx"),
316
+ path4.join(cwd, "app", "layout.tsx"),
317
+ path4.join(cwd, "app", "layout.jsx")
318
+ ];
319
+ for (const layoutPath of nextLayoutFiles) {
320
+ if (existsSync4(layoutPath)) {
321
+ return {
322
+ type: "next",
323
+ layoutPath
324
+ };
325
+ }
326
+ }
327
+ const viteMainFiles = [
328
+ path4.join(cwd, "src", "main.tsx"),
329
+ path4.join(cwd, "src", "main.jsx"),
330
+ path4.join(cwd, "src", "main.ts"),
331
+ path4.join(cwd, "src", "main.js")
332
+ ];
333
+ for (const mainPath of viteMainFiles) {
334
+ if (existsSync4(mainPath)) {
335
+ return {
336
+ type: "vite",
337
+ mainPath
338
+ };
339
+ }
340
+ }
341
+ return {
342
+ type: "generic"
343
+ };
344
+ }
345
+ function ensureTailwindConfig({
346
+ cwd,
347
+ isEsmProject,
348
+ sourceRoot
349
+ }) {
350
+ const configPath = path4.join(cwd, "tailwind.config.js");
351
+ const contentPatterns = [
352
+ "./index.html",
353
+ "./src/**/*.{js,ts,jsx,tsx,mdx}",
354
+ "./app/**/*.{js,ts,jsx,tsx,mdx}",
355
+ "./pages/**/*.{js,ts,jsx,tsx,mdx}",
356
+ "./components/**/*.{js,ts,jsx,tsx,mdx}",
357
+ `./${normalizeForGlob(sourceRoot)}/**/*.{js,ts,jsx,tsx,mdx}`
358
+ ];
359
+ const nextConfigSource = buildTailwindConfigSource({
360
+ isEsmProject,
361
+ contentPatterns
362
+ });
363
+ if (!existsSync4(configPath)) {
364
+ writeFileSync2(configPath, nextConfigSource, "utf8");
365
+ info(`Created ${configPath}`);
366
+ return;
367
+ }
368
+ const current = readFileSync3(configPath, "utf8");
369
+ if (contentPatterns.every((pattern) => current.includes(pattern))) {
370
+ return;
371
+ }
372
+ if (!current.includes("content")) {
373
+ warn(`Tailwind config exists but has no content field: ${configPath}`);
374
+ return;
375
+ }
376
+ const markerPattern = /content\s*:\s*\[([\s\S]*?)\]/m;
377
+ const match = current.match(markerPattern);
378
+ if (!match) {
379
+ warn(`Could not safely update Tailwind content globs in ${configPath}`);
380
+ return;
381
+ }
382
+ const existingBlock = match[1];
383
+ const missingPatterns = contentPatterns.filter(
384
+ (pattern) => !existingBlock.includes(pattern)
385
+ );
386
+ if (!missingPatterns.length) {
387
+ return;
388
+ }
389
+ const insertion = missingPatterns.map((pattern) => ` "${pattern}",`).join("\n");
390
+ const replacement = `content: [
391
+ ${existingBlock.trimEnd()}
392
+ ${insertion}
393
+ ]`;
394
+ const updated = current.replace(markerPattern, replacement);
395
+ writeFileSync2(configPath, updated, "utf8");
396
+ info(`Updated Tailwind content globs in ${configPath}`);
397
+ }
398
+ function buildTailwindConfigSource({
399
+ isEsmProject,
400
+ contentPatterns
401
+ }) {
402
+ const lines = contentPatterns.map((pattern) => ` "${pattern}",`).join("\n");
403
+ if (isEsmProject) {
404
+ return `/** @type {import("tailwindcss").Config} */
405
+ export default {
406
+ content: [
407
+ ${lines}
408
+ ],
409
+ theme: {
410
+ extend: {},
411
+ },
412
+ plugins: [],
413
+ };
414
+ `;
415
+ }
416
+ return `/** @type {import("tailwindcss").Config} */
417
+ module.exports = {
418
+ content: [
419
+ ${lines}
420
+ ],
421
+ theme: {
422
+ extend: {},
423
+ },
424
+ plugins: [],
425
+ };
426
+ `;
427
+ }
428
+ function ensurePostcssConfig({
429
+ cwd,
430
+ isEsmProject
431
+ }) {
432
+ const configPath = path4.join(cwd, "postcss.config.js");
433
+ if (existsSync4(configPath)) {
434
+ return;
435
+ }
436
+ const content = isEsmProject ? `export default {
437
+ plugins: {
438
+ tailwindcss: {},
439
+ autoprefixer: {},
440
+ },
441
+ };
442
+ ` : `module.exports = {
443
+ plugins: {
444
+ tailwindcss: {},
445
+ autoprefixer: {},
446
+ },
447
+ };
448
+ `;
449
+ writeFileSync2(configPath, content, "utf8");
182
450
  info(`Created ${configPath}`);
183
- info("Update registryUrl before running `uiscore add` if needed.");
451
+ }
452
+ function ensureUiscoreStylesFile({
453
+ cwd,
454
+ stylesPath
455
+ }) {
456
+ const outputPath = path4.join(cwd, stylesPath);
457
+ const fontImport = '@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700&display=swap");';
458
+ const markerStart = "/* uiscore:tokens:start */";
459
+ const markerEnd = "/* uiscore:tokens:end */";
460
+ if (!existsSync4(outputPath)) {
461
+ writeFileSync2(
462
+ outputPath,
463
+ `${fontImport}
464
+
465
+ ${markerStart}
466
+ :root {
467
+ }
468
+ ${markerEnd}
469
+ `,
470
+ "utf8"
471
+ );
472
+ info(`Created ${outputPath}`);
473
+ return;
474
+ }
475
+ let current = readFileSync3(outputPath, "utf8");
476
+ if (!current.includes(fontImport)) {
477
+ current = `${fontImport}
478
+
479
+ ${current.trimStart()}`;
480
+ }
481
+ if (!current.includes(markerStart) || !current.includes(markerEnd)) {
482
+ current = `${current.trimEnd()}
483
+
484
+ ${markerStart}
485
+ :root {
486
+ }
487
+ ${markerEnd}
488
+ `;
489
+ }
490
+ writeFileSync2(outputPath, `${current.trimEnd()}
491
+ `, "utf8");
492
+ }
493
+ function ensureGlobalCssFile({
494
+ cwd,
495
+ environment
496
+ }) {
497
+ const candidates = environment.type === "next" ? [path4.join(cwd, "src", "app", "globals.css"), path4.join(cwd, "app", "globals.css")] : [path4.join(cwd, "src", "index.css")];
498
+ for (const candidate of candidates) {
499
+ if (existsSync4(candidate)) {
500
+ return candidate;
501
+ }
502
+ }
503
+ const fallbackPath = environment.type === "next" ? candidates.find((candidate) => candidate.includes(path4.join("src", "app"))) ?? candidates[0] : path4.join(cwd, "src", "index.css");
504
+ ensureDirectory(path4.dirname(fallbackPath));
505
+ writeFileSync2(fallbackPath, "", "utf8");
506
+ info(`Created ${fallbackPath}`);
507
+ return fallbackPath;
508
+ }
509
+ function ensureGlobalCssContent({
510
+ globalCssPath,
511
+ stylesPath
512
+ }) {
513
+ const stylesImportPath = toCssRelativeImport(globalCssPath, stylesPath);
514
+ const stylesBlock = `/* uiscore:styles:start */
515
+ @import "${stylesImportPath}";
516
+ /* uiscore:styles:end */`;
517
+ const tailwindBlock = `/* uiscore:tailwind:start */
518
+ @tailwind base;
519
+ @tailwind components;
520
+ @tailwind utilities;
521
+ /* uiscore:tailwind:end */`;
522
+ const current = existsSync4(globalCssPath) ? readFileSync3(globalCssPath, "utf8") : "";
523
+ const withoutManagedBlocks = current.replace(
524
+ /\/\* uiscore:styles:start \*\/[\s\S]*?\/\* uiscore:styles:end \*\/\s*/gm,
525
+ ""
526
+ ).replace(
527
+ /\/\* uiscore:tailwind:start \*\/[\s\S]*?\/\* uiscore:tailwind:end \*\/\s*/gm,
528
+ ""
529
+ ).trimStart();
530
+ const updated = `${stylesBlock}
531
+
532
+ ${tailwindBlock}${withoutManagedBlocks ? `
533
+
534
+ ${withoutManagedBlocks}` : ""}`;
535
+ writeFileSync2(globalCssPath, `${updated.trimEnd()}
536
+ `, "utf8");
537
+ }
538
+ function ensureEntryImportsGlobalCss({
539
+ cwd,
540
+ environment,
541
+ globalCssPath
542
+ }) {
543
+ if (environment.type === "vite") {
544
+ ensureImportInCodeFile({
545
+ filePath: environment.mainPath,
546
+ importPath: toCodeRelativeImport(environment.mainPath, globalCssPath)
547
+ });
548
+ return;
549
+ }
550
+ if (environment.type === "next") {
551
+ ensureImportInCodeFile({
552
+ filePath: environment.layoutPath,
553
+ importPath: toCodeRelativeImport(environment.layoutPath, globalCssPath)
554
+ });
555
+ }
556
+ }
557
+ function ensureImportInCodeFile({
558
+ filePath,
559
+ importPath
560
+ }) {
561
+ const source = readFileSync3(filePath, "utf8");
562
+ const normalizedImport = normalizeImportPath(importPath);
563
+ const importStatement = `import "${normalizedImport}";`;
564
+ const importPattern = new RegExp(
565
+ `^\\s*import\\s+["']${escapeForRegExp2(normalizedImport)}["'];?\\s*$`,
566
+ "m"
567
+ );
568
+ const allImportPattern = new RegExp(
569
+ `^\\s*import\\s+["']${escapeForRegExp2(normalizedImport)}["'];?\\s*$\\n?`,
570
+ "gm"
571
+ );
572
+ if (!importPattern.test(source)) {
573
+ writeFileSync2(filePath, `${importStatement}
574
+ ${source}`, "utf8");
575
+ return;
576
+ }
577
+ const withoutDuplicateImports = source.replace(allImportPattern, "");
578
+ writeFileSync2(filePath, `${importStatement}
579
+ ${withoutDuplicateImports}`, "utf8");
580
+ }
581
+ function ensureDirectory(directoryPath) {
582
+ mkdirSync2(directoryPath, { recursive: true });
583
+ }
584
+ function toCssRelativeImport(fromFile, toFile) {
585
+ return normalizeImportPath(path4.relative(path4.dirname(fromFile), toFile));
586
+ }
587
+ function toCodeRelativeImport(fromFile, toFile) {
588
+ return normalizeImportPath(path4.relative(path4.dirname(fromFile), toFile));
589
+ }
590
+ function normalizeImportPath(value) {
591
+ const normalized = value.split(path4.sep).join("/");
592
+ if (normalized.startsWith(".")) {
593
+ return normalized;
594
+ }
595
+ return `./${normalized}`;
596
+ }
597
+ function normalizeForGlob(value) {
598
+ return value.split(path4.sep).join("/");
599
+ }
600
+ function escapeForRegExp2(value) {
601
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
184
602
  }
185
603
 
186
604
  // 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.7",
4
4
  "description": "CLI for installing UIScore registry components into projects.",
5
5
  "type": "module",
6
6
  "bin": {