@kinetixui/cli 0.15.0 → 0.16.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.
Files changed (2) hide show
  1. package/dist/index.js +241 -44
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import pc8 from "picocolors";
5
+ import pc9 from "picocolors";
6
6
 
7
7
  // src/commands/add.ts
8
8
  import { existsSync as existsSync4 } from "fs";
@@ -91,6 +91,14 @@ function assertComponentName(name) {
91
91
  }
92
92
  return name;
93
93
  }
94
+ function assertThemeName(name) {
95
+ if (typeof name !== "string" || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(name)) {
96
+ throw new Error(
97
+ `Invalid theme name ${JSON.stringify(name)} \u2014 expected letters, digits and dashes.`
98
+ );
99
+ }
100
+ return name;
101
+ }
94
102
  function assertRegistryUrl(url) {
95
103
  let parsed;
96
104
  try {
@@ -248,6 +256,13 @@ async function doctor() {
248
256
  levels.push(report("warn", `"${alias}" alias points to ${path5.relative(cwd, dir) || "."}, which doesn't exist yet.`));
249
257
  }
250
258
  }
259
+ const utilsBase = resolveAliasDir(cwd, config, "utils");
260
+ const utilsFile = [".ts", ".tsx", ".js", ".jsx"].map((ext) => `${utilsBase}${ext}`).find((f) => existsSync5(f));
261
+ if (utilsFile) {
262
+ levels.push(report("pass", `"utils" alias resolves to ${path5.relative(cwd, utilsFile)}`));
263
+ } else {
264
+ levels.push(report("warn", `"utils" alias points to ${path5.relative(cwd, utilsBase)}, which doesn't exist yet.`));
265
+ }
251
266
  const cssPath2 = path5.join(cwd, config.srcDir ? "src" : "", config.tailwind.css);
252
267
  if (existsSync5(cssPath2)) {
253
268
  levels.push(report("pass", `Tailwind CSS file found at ${path5.relative(cwd, cssPath2)}`));
@@ -381,7 +396,12 @@ async function inspect(name, options) {
381
396
  console.log();
382
397
  console.log(`${pc4.dim("Type")} ${item.type}`);
383
398
  const cwd = process.cwd();
384
- const config = await readConfig(cwd);
399
+ let config = null;
400
+ try {
401
+ config = await readConfig(cwd);
402
+ } catch {
403
+ console.log(`${pc4.dim("Installed")} ${pc4.yellow("? (kinetixui.json exists but isn't valid JSON)")}`);
404
+ }
385
405
  if (config) {
386
406
  const uiDir = resolveAliasDir(cwd, config, "ui");
387
407
  const installed = item.files.some(
@@ -412,24 +432,191 @@ async function inspect(name, options) {
412
432
  console.log(`Add it: ${pc4.cyan(`npx @kinetixui/cli add ${item.name}`)}`);
413
433
  }
414
434
 
415
- // src/commands/list.ts
435
+ // src/commands/lint.ts
436
+ import { existsSync as existsSync8 } from "fs";
437
+ import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
438
+ import path8 from "path";
416
439
  import pc5 from "picocolors";
440
+
441
+ // src/lib/lint.ts
442
+ function stripComments(text) {
443
+ const noBlocks = text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
444
+ return noBlocks.replace(/(^|[^:])\/\/.*$/gm, (_m, pre) => pre);
445
+ }
446
+ var HEX = "(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})";
447
+ var COLOR_UTILITY_PREFIXES = [
448
+ "bg",
449
+ "text",
450
+ "border",
451
+ "fill",
452
+ "stroke",
453
+ "ring",
454
+ "from",
455
+ "via",
456
+ "to",
457
+ "outline",
458
+ "decoration",
459
+ "caret",
460
+ "accent",
461
+ "shadow",
462
+ "divide"
463
+ ];
464
+ var COLOR_PATTERNS = [
465
+ // bg-[#1a2b3c], text-[#fff], ring-[#1a2b3c80] …
466
+ new RegExp(`\\b(?:${COLOR_UTILITY_PREFIXES.join("|")})-\\[#${HEX}\\]`, "g"),
467
+ // style={{ color: "#1a2b3c" }} / a plain CSS declaration `color: #1a2b3c;`
468
+ new RegExp(`:\\s*["']?#${HEX}["']?`, "g")
469
+ ];
470
+ var SPACING_UTILITY_PATTERN = "-?(?:(?:p|m)[trblxy]?|gap(?:-[xy])?|space-[xy]|inset(?:-[xy])?|top|right|bottom|left)";
471
+ var SPACING_VALUE = "-?[\\d.]+(?:px|rem|em|%)?";
472
+ var SPACING_PATTERNS = [
473
+ new RegExp(`\\b(?:${SPACING_UTILITY_PATTERN})-\\[${SPACING_VALUE}\\]`, "g")
474
+ ];
475
+ function lineOf(text, index) {
476
+ let line = 1;
477
+ for (let i = 0; i < index; i++) if (text.charCodeAt(i) === 10) line++;
478
+ return line;
479
+ }
480
+ function snippetAround(text, index, length) {
481
+ const lineStart = text.lastIndexOf("\n", index) + 1;
482
+ let lineEnd = text.indexOf("\n", index);
483
+ if (lineEnd === -1) lineEnd = text.length;
484
+ return text.slice(lineStart, lineEnd).trim().slice(0, 120) || text.slice(index, index + length);
485
+ }
486
+ function lintText(file, rawText) {
487
+ const text = stripComments(rawText);
488
+ const violations = [];
489
+ const seen = /* @__PURE__ */ new Set();
490
+ const scan = (patterns, kind) => {
491
+ for (const pattern of patterns) {
492
+ pattern.lastIndex = 0;
493
+ let match;
494
+ while (match = pattern.exec(text)) {
495
+ const line = lineOf(text, match.index);
496
+ const key = `${kind}:${line}:${match.index}`;
497
+ if (!seen.has(key)) {
498
+ seen.add(key);
499
+ violations.push({ file, line, kind, snippet: snippetAround(text, match.index, match[0].length) });
500
+ }
501
+ if (match[0].length === 0) pattern.lastIndex++;
502
+ }
503
+ }
504
+ };
505
+ scan(COLOR_PATTERNS, "color");
506
+ scan(SPACING_PATTERNS, "spacing");
507
+ violations.sort((a, b) => a.line - b.line);
508
+ return violations;
509
+ }
510
+ var LINT_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js", ".css"];
511
+
512
+ // src/commands/lint.ts
513
+ var SUGGESTION = {
514
+ color: "consider a semantic token utility instead (e.g. bg-primary, text-foreground)",
515
+ spacing: "consider a token-backed spacing utility instead (e.g. p-4, gap-2)"
516
+ };
517
+ function dedupeNestedDirs(dirs) {
518
+ const sorted = [...new Set(dirs)].sort((a, b) => a.length - b.length);
519
+ const kept = [];
520
+ for (const dir of sorted) {
521
+ if (!kept.some((k) => dir === k || dir.startsWith(k + path8.sep))) kept.push(dir);
522
+ }
523
+ return kept;
524
+ }
525
+ async function collectFiles(dir) {
526
+ if (!existsSync8(dir)) return [];
527
+ const files = [];
528
+ const walk = async (current) => {
529
+ const entries = await readdir2(current, { withFileTypes: true });
530
+ for (const entry of entries) {
531
+ const full = path8.join(current, entry.name);
532
+ if (entry.isDirectory()) {
533
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
534
+ await walk(full);
535
+ } else if (entry.isFile() && LINT_EXTENSIONS.includes(path8.extname(entry.name))) {
536
+ files.push(full);
537
+ }
538
+ }
539
+ };
540
+ await walk(dir);
541
+ return files;
542
+ }
543
+ async function lint(targetArg, options) {
544
+ const cwd = process.cwd();
545
+ let dirs;
546
+ if (targetArg) {
547
+ dirs = [path8.resolve(cwd, targetArg)];
548
+ } else {
549
+ if (!existsSync8(path8.join(cwd, CONFIG_FILE))) {
550
+ throw new Error(`No ${CONFIG_FILE} found \u2014 run "kinetixui init" first, or pass a path: kinetixui lint <path>`);
551
+ }
552
+ const config = await readConfig(cwd);
553
+ if (!config) throw new Error(`${CONFIG_FILE} exists but isn't valid JSON.`);
554
+ dirs = dedupeNestedDirs([resolveAliasDir(cwd, config, "components"), resolveAliasDir(cwd, config, "ui")]);
555
+ }
556
+ const seenFiles = /* @__PURE__ */ new Set();
557
+ const files = [];
558
+ for (const dir of dirs) {
559
+ for (const file of await collectFiles(dir)) {
560
+ if (!seenFiles.has(file)) {
561
+ seenFiles.add(file);
562
+ files.push(file);
563
+ }
564
+ }
565
+ }
566
+ if (files.length === 0) {
567
+ console.log(pc5.dim(`No ${LINT_EXTENSIONS.join("/")} files found in ${dirs.map((d) => path8.relative(cwd, d) || ".").join(", ")}.`));
568
+ return;
569
+ }
570
+ let totalViolations = 0;
571
+ let filesWithViolations = 0;
572
+ for (const file of files) {
573
+ const text = await readFile2(file, "utf8");
574
+ const violations = lintText(file, text);
575
+ if (violations.length === 0) continue;
576
+ filesWithViolations++;
577
+ const rel = path8.relative(cwd, file);
578
+ for (const v of violations) {
579
+ totalViolations++;
580
+ console.log(
581
+ `${pc5.yellow("!")} ${pc5.bold(`${rel}:${v.line}`)} \u2014 hardcoded ${v.kind}: ${pc5.dim(v.snippet)}`
582
+ );
583
+ console.log(` ${pc5.dim(SUGGESTION[v.kind])}`);
584
+ }
585
+ }
586
+ console.log();
587
+ if (totalViolations === 0) {
588
+ console.log(pc5.green("\u2714"), `${files.length} file${files.length === 1 ? "" : "s"} scanned, no hardcoded colors or spacing found.`);
589
+ } else {
590
+ console.log(
591
+ `${totalViolations} violation${totalViolations === 1 ? "" : "s"} in ${filesWithViolations} of ${files.length} file${files.length === 1 ? "" : "s"} scanned.`
592
+ );
593
+ console.log(
594
+ pc5.dim(
595
+ "Checks unknown/deprecated tokens, accessibility violations and cross-platform inconsistencies aren't covered yet \u2014 see https://kinetixui.com/docs/cli#lint."
596
+ )
597
+ );
598
+ if (options.fail) process.exitCode = 1;
599
+ }
600
+ }
601
+
602
+ // src/commands/list.ts
603
+ import pc6 from "picocolors";
417
604
  async function list(options) {
418
605
  const items = await fetchRegistryIndex(options.registry);
419
606
  const components = items.filter((item) => item.type === "registry:ui").sort((a, b) => a.name.localeCompare(b.name));
420
607
  console.log(`${components.length} component(s):`);
421
608
  console.log();
422
609
  for (const item of components) {
423
- const description = item.description ? pc5.dim(` \u2014 ${item.description}`) : "";
424
- console.log(` ${pc5.cyan(item.name)}${description}`);
610
+ const description = item.description ? pc6.dim(` \u2014 ${item.description}`) : "";
611
+ console.log(` ${pc6.cyan(item.name)}${description}`);
425
612
  }
426
613
  console.log();
427
- console.log(`Add one: ${pc5.cyan("npx @kinetixui/cli add <name>")}`);
428
- console.log(`Add everything: ${pc5.cyan("npx @kinetixui/cli add --all")}`);
614
+ console.log(`Add one: ${pc6.cyan("npx @kinetixui/cli add <name>")}`);
615
+ console.log(`Add everything: ${pc6.cyan("npx @kinetixui/cli add --all")}`);
429
616
  }
430
617
 
431
618
  // src/commands/parity.ts
432
- import pc6 from "picocolors";
619
+ import pc7 from "picocolors";
433
620
  var PLATFORMS = ["React", "SwiftUI", "Compose", "Flutter"];
434
621
  var PLATFORM_ABBR = {
435
622
  React: "RE",
@@ -447,11 +634,11 @@ async function parity(names, options) {
447
634
  }) : components;
448
635
  const nameWidth = Math.max(9, ...rows.map((r) => r.name.length));
449
636
  const header = `${"COMPONENT".padEnd(nameWidth)} ${PLATFORMS.map((p) => PLATFORM_ABBR[p]).join(" ")} STATUS`;
450
- console.log(pc6.dim(header));
637
+ console.log(pc7.dim(header));
451
638
  for (const item of rows) {
452
639
  const on = new Set(item.platforms ?? PLATFORMS);
453
- const marks = PLATFORMS.map((p) => on.has(p) ? pc6.green("\u2713 ") : pc6.dim("\u2014 "));
454
- const status = item.status ? pc6.yellow(item.status) : "";
640
+ const marks = PLATFORMS.map((p) => on.has(p) ? pc7.green("\u2713 ") : pc7.dim("\u2014 "));
641
+ const status = item.status ? pc7.yellow(item.status) : "";
455
642
  console.log(`${item.name.padEnd(nameWidth)} ${marks.join(" ")} ${status}`);
456
643
  }
457
644
  console.log();
@@ -459,10 +646,10 @@ async function parity(names, options) {
459
646
  }
460
647
 
461
648
  // src/commands/theme.ts
462
- import { existsSync as existsSync8 } from "fs";
463
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
464
- import path8 from "path";
465
- import pc7 from "picocolors";
649
+ import { existsSync as existsSync9 } from "fs";
650
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
651
+ import path9 from "path";
652
+ import pc8 from "picocolors";
466
653
 
467
654
  // src/lib/color-math.ts
468
655
  function hexToRgb(hex) {
@@ -652,57 +839,59 @@ function scaffoldThemeFile(name) {
652
839
  // src/commands/theme.ts
653
840
  var THEME_DIR = "kinetixui-themes";
654
841
  function csvPath(cwd, name) {
655
- return path8.join(cwd, THEME_DIR, `${name}.csv`);
842
+ return path9.join(cwd, THEME_DIR, `${name}.csv`);
656
843
  }
657
844
  function cssPath(cwd, name) {
658
- return path8.join(cwd, THEME_DIR, `${name}.css`);
845
+ return path9.join(cwd, THEME_DIR, `${name}.css`);
659
846
  }
660
847
  async function themeCreate(name, options) {
848
+ assertThemeName(name);
661
849
  const cwd = process.cwd();
662
850
  const file = csvPath(cwd, name);
663
- if (existsSync8(file) && !options.force) {
664
- throw new Error(`${path8.relative(cwd, file)} already exists. Pass --force to overwrite it.`);
851
+ if (existsSync9(file) && !options.force) {
852
+ throw new Error(`${path9.relative(cwd, file)} already exists. Pass --force to overwrite it.`);
665
853
  }
666
- await mkdir2(path8.dirname(file), { recursive: true });
854
+ await mkdir2(path9.dirname(file), { recursive: true });
667
855
  await writeFile3(file, scaffoldThemeFile(name), "utf8");
668
- console.log(pc7.green("\u2714"), `Wrote ${path8.relative(cwd, file)}`);
856
+ console.log(pc8.green("\u2714"), `Wrote ${path9.relative(cwd, file)}`);
669
857
  console.log(`Uncomment and edit the tokens you want to override, then run:`);
670
- console.log(pc7.dim(` kinetixui theme build ${name}`));
858
+ console.log(pc8.dim(` kinetixui theme build ${name}`));
671
859
  }
672
860
  async function themeBuild(name, options) {
861
+ assertThemeName(name);
673
862
  const cwd = process.cwd();
674
863
  const src = csvPath(cwd, name);
675
- if (!existsSync8(src)) {
676
- throw new Error(`${path8.relative(cwd, src)} doesn't exist. Run "kinetixui theme create ${name}" first.`);
864
+ if (!existsSync9(src)) {
865
+ throw new Error(`${path9.relative(cwd, src)} doesn't exist. Run "kinetixui theme create ${name}" first.`);
677
866
  }
678
- const text = await readFile2(src, "utf8");
867
+ const text = await readFile3(src, "utf8");
679
868
  const { values: parsed, errors, unknownTokens } = parseThemeFile(text);
680
- for (const err of errors) console.log(pc7.yellow("!"), err);
681
- for (const unknown of unknownTokens) console.log(pc7.yellow("!"), `"${unknown}" isn't a known token \u2014 skipped.`);
869
+ for (const err of errors) console.log(pc8.yellow("!"), err);
870
+ for (const unknown of unknownTokens) console.log(pc8.yellow("!"), `"${unknown}" isn't a known token \u2014 skipped.`);
682
871
  const values = deriveForegrounds(parsed);
683
872
  const setCount = Object.keys(values).length;
684
873
  if (setCount === 0) {
685
- throw new Error(`No tokens set in ${path8.relative(cwd, src)} \u2014 nothing to build. Uncomment at least one line.`);
874
+ throw new Error(`No tokens set in ${path9.relative(cwd, src)} \u2014 nothing to build. Uncomment at least one line.`);
686
875
  }
687
876
  const dest = cssPath(cwd, name);
688
877
  await writeFile3(dest, toCssBlock(values), "utf8");
689
- console.log(pc7.green("\u2714"), `Wrote ${path8.relative(cwd, dest)} (${setCount} token${setCount === 1 ? "" : "s"})`);
690
- console.log(`Paste it after ${pc7.dim('@import "@kinetixui/tokens/css";')} in your global CSS.`);
878
+ console.log(pc8.green("\u2714"), `Wrote ${path9.relative(cwd, dest)} (${setCount} token${setCount === 1 ? "" : "s"})`);
879
+ console.log(`Paste it after ${pc8.dim('@import "@kinetixui/tokens/css";')} in your global CSS.`);
691
880
  console.log();
692
- console.log(pc7.dim("WCAG AA contrast (4.5:1)"));
881
+ console.log(pc8.dim("WCAG AA contrast (4.5:1)"));
693
882
  const results = checkContrast(values);
694
883
  let failed = 0;
695
884
  for (const { pair, ratio, pass } of results) {
696
- const mark = pass ? pc7.green("\u2714") : pc7.red("\u2716");
885
+ const mark = pass ? pc8.green("\u2714") : pc8.red("\u2716");
697
886
  if (!pass) failed++;
698
- console.log(`${mark} ${pair[0]} / ${pair[1]}`, pc7.dim(`${ratio.toFixed(2)}:1`));
887
+ console.log(`${mark} ${pair[0]} / ${pair[1]}`, pc8.dim(`${ratio.toFixed(2)}:1`));
699
888
  }
700
889
  if (results.length === 0) {
701
- console.log(pc7.dim("(no checkable pairs \u2014 set at least one base + foreground, or just a base to auto-derive one)"));
890
+ console.log(pc8.dim("(no checkable pairs \u2014 set at least one base + foreground, or just a base to auto-derive one)"));
702
891
  }
703
892
  console.log();
704
893
  console.log("Native (SwiftUI/Compose/Flutter) theme output isn't built yet \u2014 this compiles to CSS only.");
705
- console.log(pc7.dim("See https://kinetixui.com/docs/cli#theme for the current scope."));
894
+ console.log(pc8.dim("See https://kinetixui.com/docs/cli#theme for the current scope."));
706
895
  if (failed > 0 && options.failOnContrast) {
707
896
  process.exitCode = 1;
708
897
  }
@@ -711,7 +900,7 @@ async function themeBuild(name, options) {
711
900
  // package.json
712
901
  var package_default = {
713
902
  name: "@kinetixui/cli",
714
- version: "0.15.0",
903
+ version: "0.16.1",
715
904
  description: "Add KinetixUI components and tokens to your project.",
716
905
  type: "module",
717
906
  license: "MIT",
@@ -755,7 +944,7 @@ program.command("init").description("Set up kinetixui.json and pull in the token
755
944
  try {
756
945
  await init({ registry: opts.registry, yes: opts.yes });
757
946
  } catch (err) {
758
- console.error(pc8.red("\u2716"), err.message);
947
+ console.error(pc9.red("\u2716"), err.message);
759
948
  process.exitCode = 1;
760
949
  }
761
950
  });
@@ -763,7 +952,7 @@ program.command("add").description("Add one or more components to your project."
763
952
  try {
764
953
  await add(components, { registry: opts.registry, overwrite: opts.overwrite, all: opts.all });
765
954
  } catch (err) {
766
- console.error(pc8.red("\u2716"), err.message);
955
+ console.error(pc9.red("\u2716"), err.message);
767
956
  process.exitCode = 1;
768
957
  }
769
958
  });
@@ -771,7 +960,7 @@ program.command("list").description("List every component available in the regis
771
960
  try {
772
961
  await list({ registry: opts.registry });
773
962
  } catch (err) {
774
- console.error(pc8.red("\u2716"), err.message);
963
+ console.error(pc9.red("\u2716"), err.message);
775
964
  process.exitCode = 1;
776
965
  }
777
966
  });
@@ -779,7 +968,7 @@ program.command("inspect").description("Show everything the registry knows about
779
968
  try {
780
969
  await inspect(name, { registry: opts.registry });
781
970
  } catch (err) {
782
- console.error(pc8.red("\u2716"), err.message);
971
+ console.error(pc9.red("\u2716"), err.message);
783
972
  process.exitCode = 1;
784
973
  }
785
974
  });
@@ -787,7 +976,15 @@ program.command("parity").description("Show which platforms carry each component
787
976
  try {
788
977
  await parity(components, { registry: opts.registry });
789
978
  } catch (err) {
790
- console.error(pc8.red("\u2716"), err.message);
979
+ console.error(pc9.red("\u2716"), err.message);
980
+ process.exitCode = 1;
981
+ }
982
+ });
983
+ program.command("lint").description("Scan for hardcoded colors and spacing that should be semantic tokens.").argument("[path]", "directory to scan \u2014 defaults to the components/ui aliases from kinetixui.json").option("--no-fail", "exit 0 even if violations are found").action(async (target, opts) => {
984
+ try {
985
+ await lint(target, { fail: opts.fail });
986
+ } catch (err) {
987
+ console.error(pc9.red("\u2716"), err.message);
791
988
  process.exitCode = 1;
792
989
  }
793
990
  });
@@ -795,7 +992,7 @@ program.command("doctor").description("Check kinetixui.json, its aliases, and th
795
992
  try {
796
993
  await doctor();
797
994
  } catch (err) {
798
- console.error(pc8.red("\u2716"), err.message);
995
+ console.error(pc9.red("\u2716"), err.message);
799
996
  process.exitCode = 1;
800
997
  }
801
998
  });
@@ -804,7 +1001,7 @@ theme.command("create").description("Scaffold kinetixui-themes/<name>.csv with e
804
1001
  try {
805
1002
  await themeCreate(name, { force: opts.force });
806
1003
  } catch (err) {
807
- console.error(pc8.red("\u2716"), err.message);
1004
+ console.error(pc9.red("\u2716"), err.message);
808
1005
  process.exitCode = 1;
809
1006
  }
810
1007
  });
@@ -812,7 +1009,7 @@ theme.command("build").description("Compile kinetixui-themes/<name>.csv to CSS a
812
1009
  try {
813
1010
  await themeBuild(name, { failOnContrast: opts.failOnContrast });
814
1011
  } catch (err) {
815
- console.error(pc8.red("\u2716"), err.message);
1012
+ console.error(pc9.red("\u2716"), err.message);
816
1013
  process.exitCode = 1;
817
1014
  }
818
1015
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kinetixui/cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Add KinetixUI components and tokens to your project.",
5
5
  "type": "module",
6
6
  "license": "MIT",