onsystem 0.8.0 → 0.9.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.
@@ -5,10 +5,10 @@ import * as z from "zod";
5
5
  import { escapePath, glob, globSync } from "tinyglobby";
6
6
  import ts from "typescript";
7
7
  import os from "node:os";
8
+ import postcss from "postcss";
8
9
  import { clampChroma, colorsNamed, converter, differenceEuclidean, formatHex, parse, wcagContrast } from "culori";
9
10
  import { createHash } from "node:crypto";
10
11
  import fsp from "node:fs/promises";
11
- import postcss from "postcss";
12
12
  import { LineCounter, isMap, isScalar, parseDocument, stringify } from "yaml";
13
13
  import { stemmer } from "stemmer";
14
14
  //#region src/extract/program.ts
@@ -142,7 +142,7 @@ function slashGlob(pattern) {
142
142
  * `.mjs` and `.cjs` forms, in any case. CSS, Markdown or JSON parsed as TSX gives hundreds of
143
143
  * syntax errors (209 for a shadcn/ui `app/globals.css`).
144
144
  */
145
- const SCRIPT_FILE$1 = /\.(?:[jt]sx?|[cm][jt]s)$/i;
145
+ const SCRIPT_FILE$2 = /\.(?:[jt]sx?|[cm][jt]s)$/i;
146
146
  /** Path relative to `root`, always with forward slashes. */
147
147
  function relativePath(root, file) {
148
148
  return toPosix(path.relative(root, file));
@@ -514,6 +514,59 @@ function isFile$1(file) {
514
514
  }
515
515
  }
516
516
  //#endregion
517
+ //#region src/tokens/imports.ts
518
+ /** Enough stylesheets for a theme split over files, without reading a whole design system. */
519
+ const MAX_IMPORTED = 50;
520
+ /**
521
+ * The specifiers a stylesheet `@import`s: `@import "./theme.css" layer(base)`,
522
+ * `@import url(theme.css)`, `@import "@acme/theme"`. Empty for CSS that does not parse.
523
+ */
524
+ function cssImports(css) {
525
+ let root;
526
+ try {
527
+ root = postcss.parse(css);
528
+ } catch {
529
+ return [];
530
+ }
531
+ const out = [];
532
+ for (const node of root.nodes) {
533
+ if (node.type !== "atrule" || node.name !== "import") continue;
534
+ const match = /^\s*(?:url\(\s*)?(?:"([^"]+)"|'([^']+)'|([^\s"')]+)\s*\))/.exec(node.params);
535
+ const specifier = match?.[1] ?? match?.[2] ?? match?.[3];
536
+ if (specifier) out.push(specifier);
537
+ }
538
+ return out;
539
+ }
540
+ /**
541
+ * The stylesheets `files` import, and the ones those import in turn, each once, in the order
542
+ * the cascade ranks them, for a reader that keeps the first declaration of a variable. CSS
543
+ * puts an imported stylesheet where its `@import` is, so a later import overrides an earlier
544
+ * one, and the importing stylesheet's own rules override both: `@import "./base.css";
545
+ * @import "./brand.css";` lists brand.css, then what brand.css imports, then base.css. The
546
+ * files themselves are not repeated, and come before all of these.
547
+ */
548
+ function importedStylesheets(files, resolve) {
549
+ const seen = new Set(files);
550
+ const out = [];
551
+ const visit = (file) => {
552
+ let text;
553
+ try {
554
+ text = fs.readFileSync(file, "utf8");
555
+ } catch {
556
+ return;
557
+ }
558
+ for (const specifier of cssImports(text).reverse()) {
559
+ const target = resolve(specifier, file);
560
+ if (!target || seen.has(target) || out.length >= MAX_IMPORTED) continue;
561
+ seen.add(target);
562
+ out.push(target);
563
+ visit(target);
564
+ }
565
+ };
566
+ for (const file of files) visit(file);
567
+ return out;
568
+ }
569
+ //#endregion
517
570
  //#region src/util/strings.ts
518
571
  /** `alert-dialog` / `alert_dialog` / `alertDialog` → `AlertDialog`. */
519
572
  function pascalCase(input) {
@@ -579,7 +632,7 @@ function unique(items) {
579
632
  const ignore = () => void 0;
580
633
  const COMPONENT_FILE = /\.[jt]sx$/;
581
634
  /** A module a barrel can lead to: TypeScript or JavaScript, not a declaration file. */
582
- const SCRIPT_FILE = /(?<!\.d)\.[cm]?[jt]sx?$/;
635
+ const SCRIPT_FILE$1 = /(?<!\.d)\.[cm]?[jt]sx?$/;
583
636
  const STYLESHEETS = [
584
637
  "src/styles/globals.css",
585
638
  "src/globals.css",
@@ -588,6 +641,18 @@ const STYLESHEETS = [
588
641
  "src/index.css"
589
642
  ];
590
643
  /**
644
+ * An app's own stylesheets, where zero config looks for them without components.json (the
645
+ * stylesheets of `DEFAULT_TOKENS` in config.ts).
646
+ */
647
+ const APP_STYLESHEETS = [
648
+ "app/globals.css",
649
+ "src/app/globals.css",
650
+ "styles/globals.css",
651
+ "src/styles/globals.css",
652
+ "src/index.css",
653
+ "app/app.css"
654
+ ];
655
+ /**
591
656
  * Finds the design system without a config:
592
657
  *
593
658
  * 1. `components.json` (shadcn/ui): the `ui` alias, resolved through tsconfig
@@ -660,6 +725,15 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
660
725
  const names = unique([...declared ? [declared.name] : [], ...dependencies.filter(isDesignSystemName)]);
661
726
  if (!names.length) step("workspace design-system packages", false, `no dependency is named like one (@acme/ui, acme-ui, @acme/design-system) among ${plural(dependencies.length, "dependency", "dependencies")}`);
662
727
  let imported;
728
+ let declarations;
729
+ /** What the trace adds for an installed package whose sources are not read. */
730
+ const declarationsNote = (name, dir, pkg) => {
731
+ declarations ??= new Set(declarationsPackages(root, packages).map((p) => p.name));
732
+ if (declarations.has(name)) return "; its components are read from its type declarations instead";
733
+ if (!declaresTypes(dir, pkg)) return "";
734
+ const scope = scopeOf$1(name);
735
+ return `; its type declarations are read when it declares its onsystem config${scope ? `, when the app is in its npm scope (${scope}/…)` : ""} or when the app's config sets "importPath" to "${name}"`;
736
+ };
663
737
  for (const name of names) {
664
738
  const own = name === declared?.name ? declared : void 0;
665
739
  const workspaceDir = own ? void 0 : packages.find(name);
@@ -687,7 +761,10 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
687
761
  prefixes: [name]
688
762
  });
689
763
  step(candidate, true, `${relativePath(root, dir)}, ${countFiles(root, result)}`);
690
- } else step(candidate, false, result ? "its components match no file" : unique(reasons).join("; ") || "nothing to read");
764
+ } else {
765
+ const reason = result ? "its components match no file" : unique(reasons).join("; ") || "nothing to read";
766
+ step(candidate, false, kind === "installed" ? `${reason}${declarationsNote(name, dir, pkg)}` : reason);
767
+ }
691
768
  }
692
769
  const registries = shadcn && project && fromShadcn && !withDefaults ? fromRegistries(root, shadcn, fromShadcn, found, project, step) : [];
693
770
  if (found.length) {
@@ -880,15 +957,28 @@ function fromComponentsJson(root, dir, json, project, packages, note = ignore) {
880
957
  }
881
958
  const tailwind = asRecord(json.tailwind);
882
959
  const css = typeof tailwind.css === "string" && tailwind.css ? path.resolve(dir, tailwind.css) : void 0;
960
+ const named = css && fs.existsSync(css) ? [css] : [];
883
961
  return {
884
962
  components: [`${escapePath(relativePath(root, uiDir) || ".")}/**/*.{tsx,jsx}`],
885
- tokens: css && fs.existsSync(css) ? [escapePath(relativePath(root, css))] : [],
963
+ tokens: (dir === root ? withAppStylesheets(root, named) : named).map((f) => escapePath(relativePath(root, f))),
886
964
  imports,
887
965
  dirs: [uiDir],
888
966
  source: `${ui} → ${relativePath(root, uiDir) || "."}`,
889
967
  ...ownerPackage(root, uiDir, ui)
890
968
  };
891
969
  }
970
+ /**
971
+ * The stylesheets an app's tokens come from when its components.json names a stylesheet in
972
+ * another package, as cal.com's names coss/ui's: the app's own stylesheets first, which hold
973
+ * its own tokens, then the named one, unless they import it, so that the cascade places it.
974
+ */
975
+ function withAppStylesheets(root, named) {
976
+ if (named.every((file) => isInside(root, file))) return named;
977
+ const own = APP_STYLESHEETS.map((f) => path.join(root, f)).filter(isFile);
978
+ if (!own.length) return named;
979
+ const imported = new Set(importedStylesheets(own, stylesheetResolver(root)));
980
+ return [...own, ...named.filter((file) => !imported.has(file))];
981
+ }
892
982
  /** How a shadcn registry is named in components.json: `@` and a name, so none leads out of the folder. */
893
983
  const REGISTRY_NAMESPACE = /^@[a-z0-9][\w-]*$/i;
894
984
  /**
@@ -1044,7 +1134,7 @@ function fromPackage(root, dir, pkg, note = ignore, installed = false) {
1044
1134
  for (const mapping of mappings) {
1045
1135
  const file = path.resolve(root, mapping.target);
1046
1136
  if (installed && isCompiledExport(dir, file)) continue;
1047
- if (mapping.target.includes("*") || !SCRIPT_FILE.test(file) || !isFile(file)) {
1137
+ if (mapping.target.includes("*") || !SCRIPT_FILE$1.test(file) || !isFile(file)) {
1048
1138
  add(mapping, COMPONENT_FILE.test(mapping.target));
1049
1139
  continue;
1050
1140
  }
@@ -1334,7 +1424,7 @@ function presetResolver(root) {
1334
1424
  if (file || specifier.startsWith(".")) return file;
1335
1425
  const name = packageName(specifier);
1336
1426
  const dir = name ? packages.find(name) : void 0;
1337
- return name && dir ? exportedFile(dir, `.${specifier.slice(name.length)}`, (f) => SCRIPT_FILE.test(f)) : void 0;
1427
+ return name && dir ? exportedFile(dir, `.${specifier.slice(name.length)}`, (f) => SCRIPT_FILE$1.test(f)) : void 0;
1338
1428
  };
1339
1429
  }
1340
1430
  /**
@@ -1342,15 +1432,21 @@ function presetResolver(root) {
1342
1432
  * (`./theme.css`, or `theme.css` as plain CSS reads it), or a stylesheet of a workspace
1343
1433
  * package (`@acme/theme/colors.css`, or `@acme/theme` through its `exports` or its `style`
1344
1434
  * field). An installed package resolves only when zero config reads it as a design system
1345
- * ({@link installedDesignSystem}), and a stylesheet in it imports others of that package alone.
1346
- * In an installed package, a stylesheet in a build folder (`dist/index.css`) is compiled, and
1347
- * resolves to nothing. Other installed packages (`tailwindcss`) and URLs resolve to nothing.
1435
+ * ({@link installedDesignSystem}) or reads its type declarations ({@link declarationsPackages}:
1436
+ * by its config, its scope or the config's `importPath`), and a stylesheet in it imports others
1437
+ * of that package alone. In an installed package, a stylesheet in a build folder
1438
+ * (`dist/index.css`) is compiled, and resolves to nothing. Other installed packages
1439
+ * (`tailwindcss`) and URLs resolve to nothing.
1348
1440
  */
1349
- function stylesheetResolver(root) {
1441
+ function stylesheetResolver(root, { importPath } = {}) {
1350
1442
  const packages = new PackageFinder(root);
1351
1443
  const designSystems = /* @__PURE__ */ new Map();
1444
+ let declarations;
1352
1445
  const designSystem = (name) => {
1353
- if (!designSystems.has(name)) designSystems.set(name, installedDesignSystem(root, name, packages));
1446
+ if (!designSystems.has(name)) {
1447
+ declarations ??= new Set(declarationsPackages(root, packages, { importPath }).map((p) => p.name));
1448
+ designSystems.set(name, installedDesignSystem(root, name, packages) ?? (declarations.has(name) ? packages.installed(name) : void 0));
1449
+ }
1354
1450
  return designSystems.get(name);
1355
1451
  };
1356
1452
  const stylesheet = (file, within) => [file, `${file}.css`].find((f) => f.endsWith(".css") && (within === void 0 || isInside(within, f) && !isBuildOutput(within, f)) && !/(?:^|[\\/])node_modules[\\/]/.test(within === void 0 ? f : path.relative(within, f)) && isFile(f));
@@ -1566,6 +1662,87 @@ function packageImports(root, dir) {
1566
1662
  return pkg ? fromPackage(root, dir, pkg)?.imports ?? [] : [];
1567
1663
  }
1568
1664
  /**
1665
+ * The dependencies of `root` (in `dependencies` or `devDependencies`) whose components are read
1666
+ * from their type declarations: design-system packages installed from a registry that name
1667
+ * `.d.ts` files, and whose component sources the root does not read (`options.components`
1668
+ * matches no file in them), as when they publish only compiled code. A design-system package
1669
+ * is one that declares its onsystem config (set up with `init`); one named like a design system
1670
+ * in the root's own npm scope (`@acme/ui` for `@acme/web`); or the dependency the root's config
1671
+ * names as its `importPath`, whatever its name or scope (`@dub/ui` in an unscoped app). A kit
1672
+ * from another scope named like one (`@sanity/ui`, `@react-email/components`) is not the app's
1673
+ * design system unless the config says so, and is not read. Workspace packages are read from
1674
+ * their sources. None at a workspace root, whose projects each find their own.
1675
+ */
1676
+ function declarationsPackages(root, packages = new PackageFinder(root), options = {}) {
1677
+ const own = readJson$1(path.join(root, "package.json"));
1678
+ if (!own || isWorkspaceRoot(root)) return [];
1679
+ const scope = scopeOf$1(own.name);
1680
+ const found = [];
1681
+ let sources;
1682
+ for (const name of Object.keys({
1683
+ ...asRecord(own.dependencies),
1684
+ ...asRecord(own.devDependencies)
1685
+ })) {
1686
+ const located = packages.locate(name);
1687
+ if (!located) continue;
1688
+ const declaration = located.json["onsystem"] === void 0 ? void 0 : declaredConfig(packages.place(located.dir).dir, located.json);
1689
+ const by = declaration !== void 0 && "file" in declaration ? "config" : scope !== void 0 && scopeOf$1(name) === scope && isDesignSystemName(name) ? "scope" : name === options.importPath ? "importPath" : void 0;
1690
+ if (!by) continue;
1691
+ const installed = packages.installed(name);
1692
+ const pkg = installed ? cachedManifest(path.join(installed, "package.json")) : void 0;
1693
+ if (!installed || !pkg || !declaresTypes(installed, pkg)) continue;
1694
+ sources ??= componentSources(root, options.components ?? []);
1695
+ if (sources.some((file) => isInside(realPath(installed), file))) continue;
1696
+ const version = typeof pkg.version === "string" ? pkg.version : void 0;
1697
+ found.push({
1698
+ name,
1699
+ dir: path.resolve(located.dir),
1700
+ version,
1701
+ by
1702
+ });
1703
+ }
1704
+ return found;
1705
+ }
1706
+ /** `@acme` for `@acme/web`; undefined for an unscoped name. */
1707
+ function scopeOf$1(name) {
1708
+ return typeof name === "string" && /^@[^/]+\/./.test(name) ? name.split("/")[0] : void 0;
1709
+ }
1710
+ /** Whether a package names type declarations: `types`, a `types` export condition, or `index.d.ts`. */
1711
+ function declaresTypes(dir, pkg) {
1712
+ if (typeof pkg.types === "string" || typeof pkg.typings === "string") return true;
1713
+ const hasTypes = (value) => Array.isArray(value) ? value.some(hasTypes) : value !== null && typeof value === "object" && Object.entries(value).some(([key, nested]) => key === "types" || hasTypes(nested));
1714
+ return hasTypes(pkg.exports) || isFile(path.join(dir, "index.d.ts"));
1715
+ }
1716
+ /** Files that hold no component to read: declarations, tests and stories, as `exclude` has them. */
1717
+ const NOT_COMPONENT_SOURCES = [
1718
+ "**/*.d.ts",
1719
+ "**/*.{test,spec,stories}.{ts,tsx,js,jsx}",
1720
+ "**/__{tests,stories,mocks}__/**"
1721
+ ];
1722
+ /**
1723
+ * The real paths of the component files in `node_modules` that `patterns` (relative to `root`)
1724
+ * match: the sources of an installed package that the root reads, through a design-system
1725
+ * package's config or zero config. Tests and stories are not components.
1726
+ */
1727
+ function componentSources(root, patterns) {
1728
+ const named = patterns.filter((p) => p.includes("node_modules"));
1729
+ if (!named.length) return [];
1730
+ return globSync(named, {
1731
+ cwd: root,
1732
+ absolute: true,
1733
+ expandDirectories: false,
1734
+ ignore: NOT_COMPONENT_SOURCES
1735
+ }).map(realPath);
1736
+ }
1737
+ /** A path with its links resolved, or as it is when it cannot be. */
1738
+ function realPath(file) {
1739
+ try {
1740
+ return fs.realpathSync(file);
1741
+ } catch {
1742
+ return path.resolve(file);
1743
+ }
1744
+ }
1745
+ /**
1569
1746
  * Dependencies' package.json files, parsed, by path: read again only when one's size or mtime
1570
1747
  * changes, so a config loaded again (the ESLint plugin's and the MCP server's reloads) does not
1571
1748
  * read every dependency again.
@@ -1910,7 +2087,7 @@ const KEYWORDS = /* @__PURE__ */ new Set([
1910
2087
  * `/ alpha` allowed): how Tailwind v3 projects, and shadcn/ui before v4, store
1911
2088
  * colors, to be used as `hsl(var(--border))`.
1912
2089
  */
1913
- const BARE_HSL = /^(-?\d*\.?\d+)(deg)?(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*\/\s*(\d*\.?\d+%?))?$/;
2090
+ const BARE_HSL = /^(-?(?:\d+(?:\.\d+)?|\.\d+))(deg)?(?:\s*,\s*|\s+)(\d+(?:\.\d+)?|\.\d+)%(?:\s*,\s*|\s+)(\d+(?:\.\d+)?|\.\d+)%(?:\s*\/\s*((?:\d+(?:\.\d+)?|\.\d+)%?))?$/;
1914
2091
  function isBareHsl(value) {
1915
2092
  return BARE_HSL.test(value.trim());
1916
2093
  }
@@ -1920,7 +2097,7 @@ function isBareHsl(value) {
1920
2097
  * Three numbers alone are no sure color, so they count as one only in a token already
1921
2098
  * known to be a color: one the Tailwind config reads with `rgb()`.
1922
2099
  */
1923
- const BARE_RGB = /^(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*\/\s*(\d*\.?\d+%?))?$/;
2100
+ const BARE_RGB = /^(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*\/\s*((?:\d+(?:\.\d+)?|\.\d+)%?))?$/;
1924
2101
  function isBareRgb(value) {
1925
2102
  const match = BARE_RGB.exec(value.trim());
1926
2103
  return match !== null && match.slice(1, 4).every((channel) => Number(channel) <= 255);
@@ -2432,9 +2609,23 @@ const UNREAD_LIBRARIES = [
2432
2609
  function libraryAdapter(name) {
2433
2610
  return LIBRARIES.find((library) => library.package === name);
2434
2611
  }
2435
- /** A library's name for messages, read or not: `MUI`, `Chakra UI`. */
2612
+ /** What a design-system package read from its type declarations is called in messages. */
2613
+ const PACKAGE_TITLE = "Design-system package";
2614
+ /** A library's name for messages, read or not: `MUI`, `Chakra UI`, `Design-system package`. */
2436
2615
  function libraryTitle(name) {
2437
- return [...LIBRARIES, ...UNREAD_LIBRARIES].find((l) => l.package === name)?.title ?? name;
2616
+ return [...LIBRARIES, ...UNREAD_LIBRARIES].find((l) => l.package === name)?.title ?? PACKAGE_TITLE;
2617
+ }
2618
+ /**
2619
+ * The adapter for a design-system package read from its type declarations (`@acme/ui`): its
2620
+ * components and their props, with no theme to read, and the app's augmentations of its modules.
2621
+ */
2622
+ function packageAdapter(name) {
2623
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2624
+ return {
2625
+ package: name,
2626
+ title: PACKAGE_TITLE,
2627
+ augments: new RegExp(`declare\\s+module\\s+['"]${escaped}(?:/[\\w./-]*)?['"]`)
2628
+ };
2438
2629
  }
2439
2630
  /** Why a library in `UNREAD_LIBRARIES` is not read: `a dependency onsystem does not read yet (…)`. */
2440
2631
  function notReadYet() {
@@ -2446,8 +2637,15 @@ function notReadYet() {
2446
2637
  * folder above it, as a monorepo hoists it. Nothing in the package is run. `ids` is the config's
2447
2638
  * `libraries`: undefined reads every library onsystem supports, false none, a list those named;
2448
2639
  * the dependencies it leaves out are `off`. Those in `UNREAD_LIBRARIES` are `unread`.
2449
- */
2450
- function detectLibraries(root, ids) {
2640
+ *
2641
+ * The design-system packages installed without their component sources that the app depends
2642
+ * on ({@link declarationsPackages}) come after them, read from their type declarations unless
2643
+ * `libraries` is false. A list names libraries, not packages, so it leaves them in. `options`
2644
+ * is what the config says of them: its `importPath`, which makes the dependency it names one,
2645
+ * and the component globs that read a package from its sources instead. A library onsystem
2646
+ * knows is never read as one, whatever `importPath` says.
2647
+ */
2648
+ function detectLibraries(root, ids, options = {}) {
2451
2649
  const pkg = readJson(path.join(root, "package.json"));
2452
2650
  if (!pkg) return {
2453
2651
  read: [],
@@ -2476,9 +2674,16 @@ function detectLibraries(root, ids) {
2476
2674
  source: sourceOf(library.package)
2477
2675
  }));
2478
2676
  const wanted = (id) => ids === void 0 || ids !== false && ids.includes(id);
2677
+ const known = new Set([...LIBRARIES, ...UNREAD_LIBRARIES].map((library) => library.package));
2678
+ const packages = declarationsPackages(root, void 0, options).filter((p) => !known.has(p.name)).map((p) => ({
2679
+ package: p.name,
2680
+ dir: p.dir,
2681
+ version: p.version,
2682
+ designSystem: true
2683
+ }));
2479
2684
  return {
2480
- read: found.filter((f) => wanted(f.id)).map((f) => f.source),
2481
- off: found.filter((f) => !wanted(f.id)).map((f) => f.source),
2685
+ read: [...found.filter((f) => wanted(f.id)).map((f) => f.source), ...ids === false ? [] : packages],
2686
+ off: [...found.filter((f) => !wanted(f.id)).map((f) => f.source), ...ids === false ? packages : []],
2482
2687
  unread: UNREAD_LIBRARIES.filter((library) => dependencies.has(library.package)).map((library) => sourceOf(library.package))
2483
2688
  };
2484
2689
  }
@@ -2630,7 +2835,7 @@ function classText(node) {
2630
2835
  visit(node);
2631
2836
  return parts.join(" ").replace(/\s+/g, " ").trim();
2632
2837
  }
2633
- const LENGTH = /^(-?(?:\d+\.?\d*|\.\d+))(px|rem|em)?$/i;
2838
+ const LENGTH = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(px|rem|em)?$/i;
2634
2839
  /** `"0.5rem"` → 8, `"12px"` → 12, `"4"` → 4. Returns undefined for anything else. */
2635
2840
  function lengthToPx(value) {
2636
2841
  if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
@@ -2658,13 +2863,27 @@ function evaluateLength(value, resolve, depth = 0) {
2658
2863
  function expandVars(value, resolve, depth) {
2659
2864
  let out = "";
2660
2865
  let last = 0;
2661
- for (const match of value.matchAll(/var\(\s*(--[\w-]+)\s*(?:,\s*([^)]+))?\)/g)) {
2662
- const [whole, name = "", fallback] = match;
2866
+ for (let at = value.indexOf("var(", last); at !== -1; at = value.indexOf("var(", last)) {
2867
+ let level = 0;
2868
+ let comma = -1;
2869
+ let end = -1;
2870
+ for (let i = at + 3; i < value.length; i++) {
2871
+ const c = value[i];
2872
+ if (c === "(") level++;
2873
+ else if (c === ")" && --level === 0) {
2874
+ end = i;
2875
+ break;
2876
+ } else if (c === "," && level === 1 && comma === -1) comma = i;
2877
+ }
2878
+ if (end === -1) return void 0;
2879
+ const name = value.slice(at + 4, comma === -1 ? end : comma).trim();
2880
+ if (!/^--[\w-]+$/.test(name)) return void 0;
2881
+ const fallback = comma === -1 ? void 0 : value.slice(comma + 1, end);
2663
2882
  const resolved = resolve(name) ?? fallback;
2664
2883
  const px = resolved === void 0 ? void 0 : evaluateLength(resolved, resolve, depth + 1);
2665
2884
  if (px === void 0) return void 0;
2666
- out += `${value.slice(last, match.index)}${px}px`;
2667
- last = match.index + whole.length;
2885
+ out += `${value.slice(last, at)}${px}px`;
2886
+ last = end + 1;
2668
2887
  }
2669
2888
  return out + value.slice(last);
2670
2889
  }
@@ -2790,6 +3009,10 @@ function utilities(namespace, key) {
2790
3009
  //#region src/tokens/tailwind-config.ts
2791
3010
  /** A preset chain is a few files deep; anything beyond is not a Tailwind config. */
2792
3011
  const MAX_FILES = 12;
3012
+ /** A config or preset module, which is read for its theme. */
3013
+ const SCRIPT_FILE = /\.[cm]?[jt]s$/;
3014
+ /** Tailwind's own modules, such as `tailwindcss/defaultTheme`, which hold its defaults. */
3015
+ const TAILWIND_MODULE = /^tailwindcss(?:\/|$)/;
2793
3016
  /**
2794
3017
  * Reads which custom property backs which color class from a Tailwind v3
2795
3018
  * config, without running it: every `colors` object in the file
@@ -2819,7 +3042,7 @@ function readTailwindColors(file, resolve) {
2819
3042
  }
2820
3043
  const specifier = importedSpecifier(node);
2821
3044
  const target = specifier === void 0 ? void 0 : resolve(specifier, current);
2822
- if (target && /\.[cm]?[jt]s$/.test(target)) queue.push(target);
3045
+ if (target && SCRIPT_FILE.test(target)) queue.push(target);
2823
3046
  ts.forEachChild(node, visit);
2824
3047
  };
2825
3048
  visit(source);
@@ -2851,13 +3074,47 @@ function collect(object, keys, colors, rgb) {
2851
3074
  }
2852
3075
  }
2853
3076
  /**
3077
+ * Tailwind v3's default `borderRadius` scale, which `theme.extend.borderRadius` adds to and
3078
+ * `theme.borderRadius` replaces. `none` (0) is left out, as the rule skips zero, and `full`
3079
+ * is the token index's own `rounded-full`.
3080
+ */
3081
+ const TAILWIND_V3_RADIUS = {
3082
+ sm: "0.125rem",
3083
+ DEFAULT: "0.25rem",
3084
+ md: "0.375rem",
3085
+ lg: "0.5rem",
3086
+ xl: "0.75rem",
3087
+ "2xl": "1rem",
3088
+ "3xl": "1.5rem"
3089
+ };
3090
+ /** borderRadius entries by key, and which of them are Tailwind's default scale rather than the config's. */
3091
+ var RadiusEntries = class extends Map {
3092
+ defaults = /* @__PURE__ */ new Set();
3093
+ };
3094
+ /**
2854
3095
  * Reads `borderRadius` entries from a Tailwind v3 config without running it:
2855
3096
  * every `borderRadius` object in the file, then in the project modules it
2856
- * imports, such as a shared preset. The config's own entries win.
3097
+ * imports, such as a shared preset. The config's own entries win. Tailwind's
3098
+ * default scale follows, for the keys none of them sets, unless the first
3099
+ * `theme.borderRadius` outside `extend` replaces it without spreading it back
3100
+ * in (`...defaultTheme.borderRadius`), a `theme` is a whole module or spreads
3101
+ * one (`theme: require("./theme")`, `theme: { ...require("./theme") }`), other
3102
+ * than Tailwind's own, `presets: []` drops Tailwind's default config
3103
+ * and no `theme.borderRadius` spreads the scale back in, or the scale comes from
3104
+ * somewhere this cannot read. That is a module that is no project file (one
3105
+ * installed from npm, other than Tailwind's own) that the config takes as a
3106
+ * preset, a theme, or the config itself (`module.exports = require("x")`,
3107
+ * `{ ...require("x"), content }`), or spreads into a theme, its `extend` or a
3108
+ * `borderRadius`; a `presets` that is no array; or a `theme.borderRadius` or
3109
+ * `theme.extend.borderRadius` that is no object (`borderRadius: radius`,
3110
+ * `borderRadius: ({ theme }) => …`) or spreads anything but Tailwind's scale.
2857
3111
  */
2858
3112
  function readTailwindRadii(file, resolve) {
2859
- const radii = /* @__PURE__ */ new Map();
3113
+ const radii = new RadiusEntries();
2860
3114
  const files = [];
3115
+ let keepsDefaults;
3116
+ let unread = false;
3117
+ let noDefaultConfig = false;
2861
3118
  const queue = [file];
2862
3119
  while (queue.length && files.length < MAX_FILES) {
2863
3120
  const current = queue.shift() ?? "";
@@ -2870,18 +3127,58 @@ function readTailwindRadii(file, resolve) {
2870
3127
  }
2871
3128
  files.push(current);
2872
3129
  const source = ts.createSourceFile(current, text, ts.ScriptTarget.Latest, true, scriptKind(current));
3130
+ const bindings = importBindings(source);
3131
+ const external = (specifier) => {
3132
+ if (specifier === void 0 || TAILWIND_MODULE.test(specifier)) return false;
3133
+ const target = resolve(specifier, current);
3134
+ return !(target && SCRIPT_FILE.test(target));
3135
+ };
3136
+ const fromPackage = (value) => external(moduleOf(value, bindings));
3137
+ const fromTailwind = (value) => TAILWIND_MODULE.test(moduleOf(value, bindings) ?? "");
2873
3138
  const visit = (node) => {
2874
- if (ts.isPropertyAssignment(node) && propertyName(node.name) === "borderRadius" && ts.isObjectLiteralExpression(node.initializer)) {
2875
- collectRadius(node.initializer, radii);
2876
- return;
3139
+ if ((ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) && propertyName(node.name) === "borderRadius") {
3140
+ const value = ts.isPropertyAssignment(node) ? node.initializer : void 0;
3141
+ const holder = themeSection(node.parent);
3142
+ if (value && ts.isObjectLiteralExpression(value)) {
3143
+ collectRadius(value, radii);
3144
+ let spreads = false;
3145
+ for (const property of value.properties) {
3146
+ if (!ts.isSpreadAssignment(property)) continue;
3147
+ if (fromTailwind(property.expression)) spreads = true;
3148
+ else unread = true;
3149
+ }
3150
+ if (holder === "theme") keepsDefaults ??= spreads;
3151
+ return;
3152
+ }
3153
+ if (holder === "theme") keepsDefaults ??= false;
3154
+ else if (holder === "extend") unread = true;
2877
3155
  }
3156
+ if ((ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) && propertyName(node.name) === "presets") {
3157
+ const value = ts.isPropertyAssignment(node) ? node.initializer : void 0;
3158
+ if (value && ts.isArrayLiteralExpression(value)) {
3159
+ if (!value.elements.length) noDefaultConfig = true;
3160
+ for (const element of value.elements) if (fromPackage(ts.isSpreadElement(element) ? element.expression : element)) unread = true;
3161
+ } else unread = true;
3162
+ }
3163
+ if ((ts.isShorthandPropertyAssignment(node) || ts.isPropertyAssignment(node) && !ts.isObjectLiteralExpression(node.initializer)) && propertyName(node.name) === "theme" && !ts.isPropertyAssignment(node.parent.parent)) {
3164
+ keepsDefaults ??= false;
3165
+ if (fromPackage(ts.isPropertyAssignment(node) ? node.initializer : node.name)) unread = true;
3166
+ }
3167
+ if (ts.isSpreadAssignment(node) && fromPackage(node.expression) && (themeSection(node.parent) !== void 0 || !ts.isPropertyAssignment(node.parent.parent) && !themeValueOnly(node.parent, source))) unread = true;
3168
+ if (ts.isSpreadAssignment(node) && themeSection(node.parent) === "theme" && !fromTailwind(node.expression)) keepsDefaults ??= false;
3169
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && isModuleExports(node.left) && fromPackage(node.right) || ts.isExportAssignment(node) && fromPackage(node.expression) || ts.isExportDeclaration(node) && !node.isTypeOnly && node.exportClause !== void 0 && ts.isNamedExports(node.exportClause) && node.exportClause.elements.some((element) => element.name.text === "default") && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && external(node.moduleSpecifier.text)) unread = true;
2878
3170
  const specifier = importedSpecifier(node);
2879
3171
  const target = specifier === void 0 ? void 0 : resolve(specifier, current);
2880
- if (target && /\.[cm]?[jt]s$/.test(target)) queue.push(target);
3172
+ if (target && SCRIPT_FILE.test(target)) queue.push(target);
2881
3173
  ts.forEachChild(node, visit);
2882
3174
  };
2883
3175
  visit(source);
2884
3176
  }
3177
+ if ((noDefaultConfig ? keepsDefaults === true : keepsDefaults !== false) && !unread) for (const [key, value] of Object.entries(TAILWIND_V3_RADIUS)) {
3178
+ if (radii.has(key)) continue;
3179
+ radii.set(key, value);
3180
+ radii.defaults.add(key);
3181
+ }
2885
3182
  return {
2886
3183
  radii,
2887
3184
  files
@@ -2901,6 +3198,10 @@ function collectRadius(object, radii) {
2901
3198
  radii.set(key, raw);
2902
3199
  }
2903
3200
  }
3201
+ /** `module.exports`, as in `module.exports = require("@acme/tailwind-config")`. */
3202
+ function isModuleExports(node) {
3203
+ return ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "module" && node.name.text === "exports";
3204
+ }
2904
3205
  /**
2905
3206
  * Gives radius tokens their Tailwind v3 class names: each config entry is
2906
3207
  * evaluated (its `var()` against radius token values, so
@@ -2909,15 +3210,29 @@ function collectRadius(object, radii) {
2909
3210
  * token add a synthetic one, so the scale step exists as a candidate
2910
3211
  * (mirrors the spacing-steps synthesis); without it the literal would keep
2911
3212
  * matching the nearest unrelated token. Config entries win over presets.
3213
+ *
3214
+ * The steps of Tailwind's default scale ({@link RadiusEntries.defaults}) only add
3215
+ * a token for a size that has none, named after nothing the project names:
3216
+ * `rounded-xl` is 12px in a shadcn/ui v3 project, whose config extends the scale
3217
+ * with `lg`, `md` and `sm`. They add none when a config step has no size (its
3218
+ * `var(--radius)` is in no stylesheet read), as they would make a scale with
3219
+ * holes, nor when the tokens are a Tailwind v4 theme, whose own scale is read from
3220
+ * its stylesheet: Tailwind v4's defaults, or a token of an `@theme` variable outside
3221
+ * the colors (only v4 reads `@theme`; a v3 config names colors too).
2912
3222
  */
2913
3223
  function applyTailwindRadii(tokens, radii, configFile = "tailwind.config") {
2914
3224
  const byVar = /* @__PURE__ */ new Map();
2915
3225
  for (const token of tokens) if (token.category === "radius" && token.cssVar) byVar.set(token.cssVar, token.value);
2916
3226
  const resolve = (name) => byVar.get(name);
3227
+ const defaults = radii instanceof RadiusEntries ? radii.defaults : /* @__PURE__ */ new Set();
3228
+ const v4 = tokens.some((t) => t.origin === "tailwind-default" || t.category !== "color" && t.tailwind !== void 0);
2917
3229
  const byPx = /* @__PURE__ */ new Map();
3230
+ let unsized = false;
2918
3231
  for (const [key, raw] of radii) {
3232
+ if (defaults.has(key)) continue;
2919
3233
  const px = evaluateLength(raw, resolve);
2920
- if (px !== void 0 && !byPx.has(px)) byPx.set(px, key);
3234
+ if (px === void 0) unsized = true;
3235
+ else if (!byPx.has(px)) byPx.set(px, key);
2921
3236
  }
2922
3237
  const havePx = /* @__PURE__ */ new Set();
2923
3238
  for (const token of tokens) {
@@ -2949,6 +3264,123 @@ function applyTailwindRadii(tokens, radii, configFile = "tailwind.config") {
2949
3264
  tokens.push(token);
2950
3265
  havePx.add(px);
2951
3266
  }
3267
+ if (unsized || v4) return;
3268
+ const names = new Set(tokens.map((t) => t.name));
3269
+ for (const key of defaults) {
3270
+ const value = radii.get(key) ?? "";
3271
+ const px = evaluateLength(value, resolve);
3272
+ const name = `radius-${key}`;
3273
+ if (px === void 0 || havePx.has(px) || names.has(name)) continue;
3274
+ const token = {
3275
+ name,
3276
+ category: "radius",
3277
+ value,
3278
+ tailwind: key,
3279
+ usage: usageFor({
3280
+ category: "radius",
3281
+ tailwind: key
3282
+ }, "--radius"),
3283
+ description: "Tailwind's default radius scale.",
3284
+ origin: "tailwind-default",
3285
+ source: {
3286
+ file: configFile,
3287
+ line: 1
3288
+ }
3289
+ };
3290
+ tokens.push(token);
3291
+ havePx.add(px);
3292
+ }
3293
+ }
3294
+ /**
3295
+ * Whether an object is the value of a variable that its file uses only as a theme value of a
3296
+ * key other than `extend` and `borderRadius`, such as `const brand = { ...blue, ...blueDark }`
3297
+ * with `theme: { extend: { colors: brand } }`. What it spreads is then no config, theme or
3298
+ * radius scale. A variable that is exported, or used in any other way, may be one.
3299
+ */
3300
+ function themeValueOnly(object, source) {
3301
+ let value = object;
3302
+ while (ts.isParenthesizedExpression(value.parent) || ts.isAsExpression(value.parent) || ts.isSatisfiesExpression(value.parent)) value = value.parent;
3303
+ const declaration = value.parent;
3304
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer !== value || !ts.isIdentifier(declaration.name) || !ts.isVariableStatement(declaration.parent.parent) || declaration.parent.parent.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return false;
3305
+ const name = declaration.name;
3306
+ let only = true;
3307
+ const visit = (node) => {
3308
+ if (!only) return;
3309
+ const parent = node.parent;
3310
+ const key = parent !== void 0 && (ts.isPropertyAssignment(parent) || ts.isPropertyAccessExpression(parent)) && parent.name === node;
3311
+ if (ts.isIdentifier(node) && node !== name && node.text === name.text && !key) only = isThemeValue(node);
3312
+ ts.forEachChild(node, visit);
3313
+ };
3314
+ visit(source);
3315
+ return only;
3316
+ }
3317
+ /**
3318
+ * Whether a name is a theme value, under a key other than `extend` and `borderRadius`:
3319
+ * `colors: brand`, or `colors: { brand }`.
3320
+ */
3321
+ function isThemeValue(reference) {
3322
+ const parent = reference.parent;
3323
+ let property;
3324
+ if (ts.isPropertyAssignment(parent) && parent.initializer === reference) property = parent;
3325
+ else if (ts.isShorthandPropertyAssignment(parent)) property = parent;
3326
+ else return false;
3327
+ for (;;) {
3328
+ const key = propertyName(property.name);
3329
+ const object = property.parent;
3330
+ if (themeSection(object) !== void 0) return key !== void 0 && key !== "extend" && key !== "borderRadius";
3331
+ const holder = object.parent;
3332
+ if (!ts.isPropertyAssignment(holder) || holder.initializer !== object) return false;
3333
+ property = holder;
3334
+ }
3335
+ }
3336
+ /** `theme` or `extend` for a property of `theme: {…}` or of `theme.extend: {…}`; else undefined. */
3337
+ function themeSection(object) {
3338
+ const holder = object.parent;
3339
+ if (!ts.isObjectLiteralExpression(object) || !ts.isPropertyAssignment(holder)) return void 0;
3340
+ const key = propertyName(holder.name);
3341
+ if (key === "theme") return "theme";
3342
+ return key === "extend" && themeSection(holder.parent) === "theme" ? "extend" : void 0;
3343
+ }
3344
+ /**
3345
+ * The names a module binds to what it imports or requires, by specifier: `acme` for
3346
+ * `import acme from "x"`, `import { acme } from "x"`, `const acme = require("x")` or
3347
+ * `const { default: acme } = require("x")`.
3348
+ */
3349
+ function importBindings(source) {
3350
+ const bindings = /* @__PURE__ */ new Map();
3351
+ const bind = (name, specifier) => {
3352
+ if (ts.isIdentifier(name)) {
3353
+ bindings.set(name.text, specifier);
3354
+ return;
3355
+ }
3356
+ for (const element of name.elements) if (!ts.isOmittedExpression(element)) bind(element.name, specifier);
3357
+ };
3358
+ for (const statement of source.statements) if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
3359
+ const specifier = statement.moduleSpecifier.text;
3360
+ const clause = statement.importClause;
3361
+ if (clause?.name) bind(clause.name, specifier);
3362
+ const named = clause?.namedBindings;
3363
+ if (named && ts.isNamespaceImport(named)) bind(named.name, specifier);
3364
+ else if (named) for (const element of named.elements) bind(element.name, specifier);
3365
+ } else if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) {
3366
+ const specifier = declaration.initializer && moduleOf(declaration.initializer, bindings);
3367
+ if (specifier !== void 0) bind(declaration.name, specifier);
3368
+ }
3369
+ return bindings;
3370
+ }
3371
+ /**
3372
+ * The module an expression takes its value from: `"x"` for `require("x")`, `import("x")`, a
3373
+ * name bound to either ({@link importBindings}), a call of any of these
3374
+ * (`createPreset({…})`, `require("x")()`) or a property of one (`require("x").default`).
3375
+ */
3376
+ function moduleOf(node, bindings) {
3377
+ let current = node;
3378
+ for (;;) if (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current) || ts.isNonNullExpression(current) || ts.isAwaitExpression(current) || ts.isPropertyAccessExpression(current)) current = current.expression;
3379
+ else if (ts.isCallExpression(current)) {
3380
+ const specifier = importedSpecifier(current);
3381
+ if (specifier !== void 0) return specifier;
3382
+ current = current.expression;
3383
+ } else return ts.isIdentifier(current) ? bindings.get(current.text) : void 0;
2952
3384
  }
2953
3385
  /** The specifier of `require("x")`, `import x from "x"`, `export { default } from "x"` or `import("x")`. */
2954
3386
  function importedSpecifier(node) {
@@ -3297,7 +3729,11 @@ async function loadConfigWith(options, context = {}) {
3297
3729
  const absolute = path.resolve(root, slashGlob(file));
3298
3730
  if (!/[*?[\]{}()!]/.test(file) && !fs.existsSync(absolute)) throw new ConfigError(`Token file not found: ${absolute} (from ${explicitTokens?.from})`);
3299
3731
  }
3300
- const libraries = detectLibraries(root, config.libraries);
3732
+ const components = (options.components?.length ? options.components : configComponents !== void 0 ? configComponents.map(fromBase) : detection ? [...detection.withDefaults ? DEFAULT_COMPONENTS : [], ...detection.components] : DEFAULT_COMPONENTS).map(slashGlob);
3733
+ const libraries = detectLibraries(root, config.libraries, {
3734
+ importPath: config.importPath,
3735
+ components
3736
+ });
3301
3737
  const tokenSources = options.tokens?.length ? options.tokens.map((p) => ({ path: p })) : configTokens !== void 0 ? configTokens.map((t) => typeof t === "string" ? { path: fromBase(t) } : {
3302
3738
  ...t,
3303
3739
  path: fromBase(t.path)
@@ -3316,7 +3752,7 @@ async function loadConfigWith(options, context = {}) {
3316
3752
  return {
3317
3753
  root,
3318
3754
  configFile,
3319
- components: (options.components?.length ? options.components : configComponents !== void 0 ? configComponents.map(fromBase) : detection ? [...detection.withDefaults ? DEFAULT_COMPONENTS : [], ...detection.components] : DEFAULT_COMPONENTS).map(slashGlob),
3755
+ components,
3320
3756
  exclude: declared && config.exclude ? [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDE, ...exclude])] : exclude,
3321
3757
  ...declared && config.exclude ? { designSystemExclude: exclude } : {},
3322
3758
  tokens: tokenSources.map((t) => ({
@@ -4481,9 +4917,9 @@ function dedent(code) {
4481
4917
  const MAX_LISTED_INHERITED = 40;
4482
4918
  /**
4483
4919
  * Reads a published library's components from its type declarations with the TypeScript
4484
- * checker: every value its entry exports under a capitalised name whose type takes props
4485
- * (function and class components, `forwardRef` and MUI's `OverridableComponent`), with the
4486
- * props of each call signature, their string-literal values (custom ones the app declares by
4920
+ * checker: every value its entry exports under a capitalised name whose type takes props and
4921
+ * renders (function and class components, `forwardRef` and MUI's `OverridableComponent`), with
4922
+ * the props of each call signature, their string-literal values (custom ones the app declares by
4487
4923
  * module augmentation included), JSDoc, the element it renders, and the module that
4488
4924
  * default-exports it. A signature generic over the element to render (MUI's `component`) marks
4489
4925
  * that prop as polymorphic instead of adding its props.
@@ -4505,16 +4941,27 @@ function extractLibrary(options) {
4505
4941
  return empty(`${adapter.package}: ${dir} could not be read`);
4506
4942
  }
4507
4943
  const resolve = (specifier) => ts.resolveModuleName(specifier, path.join(root, "__onsystem__.tsx"), options.project.options, ts.sys).resolvedModule?.resolvedFileName;
4508
- const entry = resolve(adapter.package) ?? path.join(real, typesEntry(real));
4509
- if (!fs.existsSync(entry) || !/\.d\.[cm]?ts$/.test(entry)) return empty(`${adapter.package}: no type declarations at ${relativePath(root, entry)}`);
4944
+ const main = resolve(adapter.package) ?? path.join(real, typesEntry(real));
4945
+ const entries = isDeclarations(main) ? [{
4946
+ specifier: adapter.package,
4947
+ file: main
4948
+ }] : subpathEntries(real, adapter.package, resolve);
4949
+ if (!entries.length) return empty(`${adapter.package}: no type declarations at ${relativePath(root, main)}`);
4510
4950
  const program = ts.createProgram({
4511
- rootNames: [entry, ...options.augmentations],
4512
- options: options.project.options
4951
+ rootNames: [...entries.map((e) => e.file), ...options.augmentations],
4952
+ options: options.project.options,
4953
+ host: declarationsHost(options.project.options)
4513
4954
  });
4514
4955
  const checker = program.getTypeChecker();
4515
- const entryFile = program.getSourceFile(entry);
4516
- const moduleSymbol = entryFile ? checker.getSymbolAtLocation(entryFile) : void 0;
4517
- if (!moduleSymbol) return empty(`${adapter.package}: ${entry} has no exports`);
4956
+ const modules = entries.flatMap((e) => {
4957
+ const file = program.getSourceFile(e.file);
4958
+ const symbol = file ? checker.getSymbolAtLocation(file) : void 0;
4959
+ return symbol ? [{
4960
+ ...e,
4961
+ symbol
4962
+ }] : [];
4963
+ });
4964
+ if (!modules.length) return empty(`${adapter.package}: ${entries[0]?.file ?? main} has no exports`);
4518
4965
  const shown = (file) => {
4519
4966
  const absolute = path.resolve(file);
4520
4967
  return relativePath(root, isInside(real, absolute) ? path.join(dir, path.relative(real, absolute)) : absolute);
@@ -4523,38 +4970,45 @@ function extractLibrary(options) {
4523
4970
  checker,
4524
4971
  adapter,
4525
4972
  propSets: /* @__PURE__ */ new Map(),
4526
- scope: adapter.package.startsWith("@") ? `${adapter.package.split("/")[0] ?? ""}/` : adapter.package
4973
+ scope: adapter.package.startsWith("@") ? `${adapter.package.split("/")[0] ?? ""}/` : adapter.package,
4974
+ icons: /* @__PURE__ */ new Set()
4527
4975
  };
4528
4976
  const exportNames = /* @__PURE__ */ new Set();
4529
4977
  const bySymbol = /* @__PURE__ */ new Map();
4530
4978
  const components = [];
4531
- for (const exported of checker.getExportsOfModule(moduleSymbol)) {
4532
- const name = exported.getName();
4533
- const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
4534
- if (!(symbol.flags & ts.SymbolFlags.Value)) continue;
4535
- exportNames.add(name);
4536
- if (!/^[A-Z]/.test(name)) continue;
4537
- const known = bySymbol.get(symbol);
4538
- if (known) {
4539
- known.aliases.push(name);
4540
- continue;
4979
+ for (const module of modules) {
4980
+ const subpath = module.specifier !== adapter.package;
4981
+ for (const exported of checker.getExportsOfModule(module.symbol)) {
4982
+ const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
4983
+ if (!(symbol.flags & ts.SymbolFlags.Value)) continue;
4984
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
4985
+ const isDefault = exported.getName() === "default";
4986
+ const name = isDefault && subpath && declaration ? ts.getNameOfDeclaration(declaration)?.getText() ?? "default" : exported.getName();
4987
+ if (isDefault && !/^[A-Z]/.test(name)) continue;
4988
+ exportNames.add(name);
4989
+ if (!/^[A-Z]/.test(name)) continue;
4990
+ const known = bySymbol.get(symbol);
4991
+ if (known) {
4992
+ if (known.name !== name && !known.aliases.includes(name)) known.aliases.push(name);
4993
+ continue;
4994
+ }
4995
+ if (!declaration) continue;
4996
+ const component = buildComponent(context, name, symbol);
4997
+ if (!component) continue;
4998
+ const file = declaration.getSourceFile();
4999
+ const line = file.getLineAndCharacterOfPosition(declaration.getStart(file)).line + 1;
5000
+ component.source = {
5001
+ file: shown(file.fileName),
5002
+ line
5003
+ };
5004
+ if (subpath) component.importPath = module.specifier;
5005
+ const defaultImport = isDefault ? module.specifier : defaultModule(program, checker, resolve, adapter.package, file, symbol);
5006
+ if (defaultImport) component.defaultImport = defaultImport;
5007
+ bySymbol.set(symbol, component);
5008
+ components.push(component);
4541
5009
  }
4542
- const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
4543
- if (!declaration) continue;
4544
- const component = buildComponent(context, name, symbol);
4545
- if (!component) continue;
4546
- const file = declaration.getSourceFile();
4547
- const line = file.getLineAndCharacterOfPosition(declaration.getStart(file)).line + 1;
4548
- component.source = {
4549
- file: shown(file.fileName),
4550
- line
4551
- };
4552
- const defaultImport = defaultModule(program, checker, resolve, adapter.package, file, symbol);
4553
- if (defaultImport) component.defaultImport = defaultImport;
4554
- bySymbol.set(symbol, component);
4555
- components.push(component);
4556
5010
  }
4557
- linkSlots(components);
5011
+ linkSlots(components, context.icons);
4558
5012
  return {
4559
5013
  components,
4560
5014
  propSets: Object.fromEntries(context.propSets),
@@ -4563,6 +5017,29 @@ function extractLibrary(options) {
4563
5017
  dependencies: unique(projectFiles(program))
4564
5018
  };
4565
5019
  }
5020
+ /**
5021
+ * A compiler host that resolves what the app's own resolution leaves unresolved in a package's
5022
+ * declarations by path, as Node.js's old resolution does. A bundler emits declarations that
5023
+ * import a dependency's file by its path (`class-variance-authority/dist/types`, for the
5024
+ * `ClassProp` behind every `cva()` variant), which that dependency's `exports` do not name: with
5025
+ * `"moduleResolution": "bundler"` the import is `any`, and a component's variants and props go
5026
+ * with it. The app's own files resolve as they do for tsc.
5027
+ */
5028
+ function declarationsHost(options) {
5029
+ const host = ts.createCompilerHost(options);
5030
+ const cache = ts.createModuleResolutionCache(host.getCurrentDirectory(), (file) => host.getCanonicalFileName(file), options);
5031
+ const byPath = {
5032
+ ...options,
5033
+ moduleResolution: ts.ModuleResolutionKind.Node10
5034
+ };
5035
+ host.resolveModuleNameLiterals = (literals, containingFile, redirected, compilerOptions, file) => literals.map((literal) => {
5036
+ const resolved = ts.resolveModuleName(literal.text, containingFile, compilerOptions, host, cache, redirected, ts.getModeForUsageLocation(file, literal, compilerOptions));
5037
+ if (resolved.resolvedModule || !/[\\/]node_modules[\\/].*\.d\.[cm]?ts$/.test(containingFile)) return resolved;
5038
+ const fallback = ts.resolveModuleName(literal.text, containingFile, byPath, host);
5039
+ return fallback.resolvedModule && /\.d\.[cm]?ts$/.test(fallback.resolvedModule.resolvedFileName) ? fallback : resolved;
5040
+ });
5041
+ return host;
5042
+ }
4566
5043
  /** The declaration entry of an installed package: its `types`, else `index.d.ts`. */
4567
5044
  function typesEntry(dir) {
4568
5045
  try {
@@ -4572,9 +5049,106 @@ function typesEntry(dir) {
4572
5049
  } catch {}
4573
5050
  return "index.d.ts";
4574
5051
  }
4575
- /** Call and construct signatures that take props, across a union (`React.ComponentType`). */
4576
- function propSignatures(type) {
4577
- return (type.isUnion() ? type.types : [type]).flatMap((member) => [...member.getCallSignatures(), ...member.getConstructSignatures()]).filter((signature) => signature.getParameters().length >= 1);
5052
+ /** A type declarations file on disk: `.d.ts`, `.d.mts` or `.d.cts`. */
5053
+ function isDeclarations(file) {
5054
+ return /\.d\.[cm]?ts$/.test(file) && fs.existsSync(file);
5055
+ }
5056
+ /** Packages exporting more subpaths than this are read from the first ones, sorted. */
5057
+ const MAX_SUBPATHS = 500;
5058
+ /**
5059
+ * The declarations of each subpath a package without an entry of its own exports (`./button`,
5060
+ * `./card`, or the files a `./*` pattern maps), with the specifier apps import it by
5061
+ * (`@acme/ui/button`): as the app's own imports resolve it, else through the `types` condition
5062
+ * its `exports` give it. Subpaths that name no declarations (`./theme.css`) are left out.
5063
+ */
5064
+ function subpathEntries(dir, name, resolve) {
5065
+ let exports;
5066
+ try {
5067
+ exports = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).exports;
5068
+ } catch {
5069
+ return [];
5070
+ }
5071
+ if (!exports || typeof exports !== "object" || Array.isArray(exports)) return [];
5072
+ const found = /* @__PURE__ */ new Map();
5073
+ const add = (subpath, target) => {
5074
+ const specifier = `${name}/${subpath}`;
5075
+ const file = resolve(specifier) ?? (target ? path.resolve(dir, target) : void 0);
5076
+ if (file && isInside(dir, path.resolve(file)) && isDeclarations(file)) found.set(specifier, file);
5077
+ };
5078
+ for (const [key, value] of Object.entries(exports)) {
5079
+ if (!key.startsWith("./") || key === "./package.json") continue;
5080
+ const target = typesTarget(value);
5081
+ const star = key.indexOf("*");
5082
+ if (star === -1) {
5083
+ add(key.slice(2), target);
5084
+ continue;
5085
+ }
5086
+ const at = target?.indexOf("*") ?? -1;
5087
+ if (!target || at === -1) continue;
5088
+ const prefix = path.posix.normalize(target.slice(0, at)).replace(/^\.\//, "");
5089
+ const suffix = target.slice(at + 1);
5090
+ const folder = prefix.slice(0, prefix.lastIndexOf("/") + 1);
5091
+ const files = globSync(`${escapePath(folder)}**/*${escapePath(suffix)}`, {
5092
+ cwd: dir,
5093
+ ignore: ["**/node_modules/**"],
5094
+ expandDirectories: false
5095
+ });
5096
+ for (const file of files.sort()) {
5097
+ if (!file.startsWith(prefix) || !file.endsWith(suffix)) continue;
5098
+ const middle = file.slice(prefix.length, file.length - suffix.length);
5099
+ add(`${key.slice(2, star)}${middle}${key.slice(star + 1)}`, path.join(dir, file));
5100
+ }
5101
+ }
5102
+ return [...found].sort(([a], [b]) => a.localeCompare(b)).slice(0, MAX_SUBPATHS).map(([specifier, file]) => ({
5103
+ specifier,
5104
+ file
5105
+ }));
5106
+ }
5107
+ /** The first `types` target an `exports` value gives, through nested conditions. */
5108
+ function typesTarget(value) {
5109
+ if (Array.isArray(value)) {
5110
+ for (const item of value) {
5111
+ const found = typesTarget(item);
5112
+ if (found) return found;
5113
+ }
5114
+ return;
5115
+ }
5116
+ if (!value || typeof value !== "object") return void 0;
5117
+ for (const [condition, nested] of Object.entries(value)) {
5118
+ if (condition === "types" && typeof nested === "string") return nested;
5119
+ const found = typesTarget(nested);
5120
+ if (found) return found;
5121
+ }
5122
+ }
5123
+ /**
5124
+ * Call and construct signatures that take props and render, across a union
5125
+ * (`React.ComponentType`): a call that returns an element, and a class whose instances have
5126
+ * `render`. A capitalised function that returns anything else (`Format(value): string`) is no
5127
+ * component.
5128
+ */
5129
+ function propSignatures(checker, type) {
5130
+ return (type.isUnion() ? type.types : [type]).flatMap((member) => [...member.getCallSignatures().filter((s) => rendersElement(checker, checker.getReturnTypeOfSignature(s))), ...member.getConstructSignatures().filter((s) => checker.getPropertyOfType(checker.getReturnTypeOfSignature(s), "render") !== void 0)]).filter((signature) => signature.getParameters().length >= 1);
5131
+ }
5132
+ /**
5133
+ * Whether a call's return type is what a component returns: a React element (`JSX.Element`,
5134
+ * `ReactElement`), a `ReactNode` that can be one, a promise of one (a server component), or
5135
+ * nothing (`null`). `any`, where React's types do not resolve, counts.
5136
+ */
5137
+ function rendersElement(checker, type) {
5138
+ if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Null)) return true;
5139
+ const members = (type.isUnion() ? type.types : [type]).filter((member) => !(member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)));
5140
+ if (!members.length) return true;
5141
+ return members.some((member) => {
5142
+ if (member.flags & ts.TypeFlags.Any) return true;
5143
+ if (checker.getPropertyOfType(member, "props") !== void 0 && checker.getPropertyOfType(member, "type") !== void 0) return true;
5144
+ const awaited = checker.getPropertyOfType(member, "then") ? checker.getAwaitedType(member) : void 0;
5145
+ return awaited !== void 0 && awaited !== member && rendersElement(checker, awaited);
5146
+ });
5147
+ }
5148
+ /** A conditional type the checker cannot resolve yet, or a union or intersection with one. */
5149
+ function hasConditional(type) {
5150
+ if (type.flags & ts.TypeFlags.Conditional) return true;
5151
+ return type.isUnionOrIntersection() && type.types.some((member) => hasConditional(member));
4578
5152
  }
4579
5153
  /** A React context (`ButtonGroupContext`): callable in React 19's types, but not a component to offer. */
4580
5154
  function isContext(checker, type) {
@@ -4603,7 +5177,7 @@ function buildComponent(context, name, symbol) {
4603
5177
  const { checker, adapter } = context;
4604
5178
  const type = checker.getTypeOfSymbol(symbol);
4605
5179
  if (isContext(checker, type)) return void 0;
4606
- const signatures = propSignatures(type);
5180
+ const signatures = propSignatures(checker, type);
4607
5181
  if (!signatures.length) return void 0;
4608
5182
  let polymorphic;
4609
5183
  const plain = [];
@@ -4624,6 +5198,7 @@ function buildComponent(context, name, symbol) {
4624
5198
  openProps = true;
4625
5199
  continue;
4626
5200
  }
5201
+ if (signature.getTypeParameters()?.length && hasConditional(propsType)) openProps = true;
4627
5202
  for (const part of propsType.isUnion() ? propsType.types : [propsType]) {
4628
5203
  members++;
4629
5204
  if (checker.getIndexInfosOfType(part).length) openProps = true;
@@ -4650,7 +5225,8 @@ function buildComponent(context, name, symbol) {
4650
5225
  const byOrigin = /* @__PURE__ */ new Map();
4651
5226
  for (const [propName, { origin }] of entries) byOrigin.set(origin, [...byOrigin.get(origin) ?? [], propName]);
4652
5227
  const summarised = new Set([...byOrigin].filter(([origin, names]) => !own(origin) && (origin === "react" || names.length > MAX_LISTED_INHERITED)).map(([origin]) => origin));
4653
- const element = renderedElement(checker, used.at(-1));
5228
+ const rendered = elementText(checker, used.at(-1));
5229
+ const element = renderedElement(rendered);
4654
5230
  const props = [];
4655
5231
  const variants = [];
4656
5232
  for (const [propName, entry] of entries) {
@@ -4704,6 +5280,7 @@ function buildComponent(context, name, symbol) {
4704
5280
  };
4705
5281
  if (polymorphic) info.polymorphic = polymorphic.name;
4706
5282
  if (element) info.element = element;
5283
+ if (rendered && /\bSVG\w*Element\b/.test(rendered)) context.icons.add(info);
4707
5284
  const jsdoc = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
4708
5285
  const description = jsdoc ? adapter.describe?.(jsdoc, name) ?? jsdoc : void 0;
4709
5286
  if (description) info.description = description;
@@ -4805,15 +5382,23 @@ function literalValues$1(checker, type, depth = 0) {
4805
5382
  }
4806
5383
  return values.length ? unique(values) : void 0;
4807
5384
  }
4808
- /** The native element behind a signature, from the type of the `ref` its props take. */
4809
- function renderedElement(checker, signature) {
5385
+ /** The native element an {@link elementText} names: `button` for `Ref<HTMLButtonElement>`. */
5386
+ function renderedElement(text) {
5387
+ const element = text ? /\bHTML(\w+?)Element\b/.exec(text)?.[1] : void 0;
5388
+ return element ? ELEMENT_INTERFACES[element] : void 0;
5389
+ }
5390
+ /**
5391
+ * What names the native element behind a signature, as text: the type of the `ref` its props
5392
+ * take, or without one, of the DOM event handler they take (`HTMLAttributes<HTMLSpanElement>`
5393
+ * gives `onFocus: FocusEventHandler<HTMLSpanElement>`), as a function component's props declare
5394
+ * it. An icon's names an SVG element (`FocusEventHandler<SVGSVGElement>`).
5395
+ */
5396
+ function elementText(checker, signature) {
4810
5397
  const param = signature?.getParameters()[0];
4811
5398
  if (!param) return void 0;
4812
- const ref = checker.getPropertyOfType(checker.getTypeOfSymbol(param), "ref");
4813
- if (!ref) return void 0;
4814
- const text = checker.typeToString(checker.getTypeOfSymbol(ref));
4815
- const element = /\bHTML(\w+?)Element\b/.exec(text)?.[1];
4816
- return element ? ELEMENT_INTERFACES[element] : void 0;
5399
+ const props = checker.getTypeOfSymbol(param);
5400
+ const typed = checker.getPropertyOfType(props, "ref") ?? checker.getPropertyOfType(props, "onFocus");
5401
+ return typed ? checker.typeToString(checker.getTypeOfSymbol(typed)) : void 0;
4817
5402
  }
4818
5403
  /**
4819
5404
  * The folder module that default-exports a component (`@mui/material/Button` for
@@ -4831,16 +5416,36 @@ function defaultModule(program, checker, resolve, name, file, symbol) {
4831
5416
  }
4832
5417
  /**
4833
5418
  * Styled slots a library exports next to the component they belong to (`SliderThumb` in
4834
- * `Slider.d.ts`) become its parts, so lists show the component and its parts under it.
4835
- */
4836
- function linkSlots(components) {
5419
+ * `Slider.d.ts`, `CardHeader` and `CardContent` next to `Card` in a bundle's one `index.d.ts`)
5420
+ * become its parts, so lists show the component and its parts under it. A name that only goes
5421
+ * on in lower case (`Tabs` next to `Tab`) is another component, and so is a lone container
5422
+ * (`CheckboxGroup` next to `Checkbox`), as for source components. Icons are no parts and have
5423
+ * none (`UserCheck` next to `User`). In a file that declares several components of their own,
5424
+ * as a bundle does, a component with one such name next to it is no family
5425
+ * (`ButtonTooltip` next to `Button`): only two or more are its parts.
5426
+ */
5427
+ function linkSlots(components, icons) {
4837
5428
  const byFile = /* @__PURE__ */ new Map();
4838
- for (const component of components) byFile.set(component.source.file, [...byFile.get(component.source.file) ?? [], component]);
4839
- for (const group of byFile.values()) for (const component of group) {
4840
- const owner = group.filter((other) => other !== component && component.name.length > other.name.length && component.name.startsWith(other.name)).sort((a, b) => b.name.length - a.name.length)[0];
4841
- if (!owner) continue;
4842
- component.parent = owner.name;
4843
- owner.subcomponents.push(component.name);
5429
+ for (const component of components) {
5430
+ if (icons.has(component)) continue;
5431
+ byFile.set(component.source.file, [...byFile.get(component.source.file) ?? [], component]);
5432
+ }
5433
+ for (const group of byFile.values()) {
5434
+ const parts = /* @__PURE__ */ new Map();
5435
+ for (const component of group) {
5436
+ const owner = group.filter((other) => other !== component && component.name.length > other.name.length && component.name.startsWith(other.name) && /^[A-Z]/.test(component.name.charAt(other.name.length))).sort((a, b) => b.name.length - a.name.length)[0];
5437
+ if (owner) parts.set(owner, [...parts.get(owner) ?? [], component]);
5438
+ }
5439
+ const owned = new Set([...parts.values()].flat());
5440
+ const bundle = group.filter((component) => !owned.has(component)).length > 1;
5441
+ for (const [owner, family] of parts) {
5442
+ if (family.every((part) => /^(?:Group|List)$/.test(part.name.slice(owner.name.length)))) continue;
5443
+ if (bundle && family.length < 2) continue;
5444
+ for (const part of family) {
5445
+ part.parent = owner.name;
5446
+ owner.subcomponents.push(part.name);
5447
+ }
5448
+ }
4844
5449
  }
4845
5450
  }
4846
5451
  //#endregion
@@ -4861,7 +5466,8 @@ const NOT_SOURCES = [
4861
5466
  /**
4862
5467
  * The components and tokens of the libraries `config` found (`config.libraries`): each one's
4863
5468
  * components from its type declarations, read together with the app's files that augment them,
4864
- * and the tokens of the theme the app creates for it.
5469
+ * and the tokens of the theme the app creates for it. A design-system package installed without
5470
+ * its sources is read the same way, with no theme.
4865
5471
  */
4866
5472
  async function loadLibraries(config) {
4867
5473
  const loaded = {
@@ -4873,7 +5479,7 @@ async function loadLibraries(config) {
4873
5479
  dependencies: []
4874
5480
  };
4875
5481
  const libraries = (config.libraries ?? []).flatMap((source) => {
4876
- const adapter = libraryAdapter(source.package);
5482
+ const adapter = libraryAdapter(source.package) ?? (source.designSystem ? packageAdapter(source.package) : void 0);
4877
5483
  return adapter ? [{
4878
5484
  source,
4879
5485
  adapter
@@ -5892,7 +6498,7 @@ function cssSheetTokens(sheets) {
5892
6498
  const category = isSpacingBase ? "spacing" : namespace?.[1] ?? "other";
5893
6499
  const tailwindKey = isSpacingBase ? "" : variable.name.slice(namespace?.[0].length ?? 0);
5894
6500
  const tailwindNamespace = isSpacingBase ? "--spacing" : namespace?.[0].slice(0, -1) ?? "";
5895
- const alias = /^var\(\s*(--[\w-]+)\s*\)$/.exec(variable.value)?.[1];
6501
+ const alias = themeAlias(variable.value, base, lookup);
5896
6502
  const target = alias ? base.get(alias) : void 0;
5897
6503
  if (alias && target) {
5898
6504
  consumed.add(alias);
@@ -5913,7 +6519,7 @@ function cssSheetTokens(sheets) {
5913
6519
  add(finalize({
5914
6520
  name: variable.name.slice(2),
5915
6521
  category,
5916
- value: variable.value,
6522
+ value: resolveValue(variable.value, lookup),
5917
6523
  modes: modeValues(variable.name, modes, lookup),
5918
6524
  cssVar: variable.inline ? void 0 : variable.name,
5919
6525
  tailwind: tailwindKey,
@@ -6044,6 +6650,22 @@ function categorize$1(name, value) {
6044
6650
  function varAlias(value) {
6045
6651
  return /^var\(\s*(--[\w-]+)\s*\)$/.exec(value.trim())?.[1];
6046
6652
  }
6653
+ /**
6654
+ * The base variable an `@theme` variable stands for: `--x` for `var(--x)`, and for
6655
+ * `hsl(var(--x))` or `rgb(var(--x))` when `--x` holds bare HSL or RGB channels, as
6656
+ * shadcn/ui's Tailwind v4 codemod leaves a v3 theme (`--color-primary: hsl(var(--primary))`).
6657
+ * The token is then `--x`, whose usage already wraps the channels (`hsl(var(--primary))`).
6658
+ */
6659
+ function themeAlias(value, base, lookup) {
6660
+ const plain = varAlias(value);
6661
+ if (plain) return plain;
6662
+ const match = /^(hsl|rgb)a?\(\s*var\(\s*(--[\w-]+)\s*\)\s*(?:\/\s*<alpha-value>\s*)?\)$/.exec(value.trim());
6663
+ const name = match?.[2];
6664
+ const target = name ? base.get(name) : void 0;
6665
+ if (!match || !name || !target) return void 0;
6666
+ const channels = resolveValue(target.value, lookup);
6667
+ return (match[1] === "hsl" ? isBareHsl(channels) : isBareRgb(channels)) ? name : void 0;
6668
+ }
6047
6669
  /** Follows plain `var(--x)` aliases so the token shows its concrete value. */
6048
6670
  function resolveValue(value, lookup, depth = 0) {
6049
6671
  const alias = /^var\(\s*(--[\w-]+)\s*\)$/.exec(value)?.[1];
@@ -6665,24 +7287,33 @@ const CORE_FAMILIES = /* @__PURE__ */ new Set([
6665
7287
  "overlay",
6666
7288
  "link"
6667
7289
  ]);
6668
- /** Name segments of status colors: what happened, rather than how it looks. */
6669
- const STATUS_WORDS = /* @__PURE__ */ new Set([
6670
- "destructive",
6671
- "danger",
6672
- "error",
6673
- "critical",
6674
- "negative",
6675
- "invalid",
6676
- "warning",
6677
- "warn",
6678
- "caution",
6679
- "success",
6680
- "positive",
6681
- "valid",
6682
- "info",
6683
- "information",
6684
- "notice"
6685
- ]);
7290
+ /** Name segments of status colors, by the status they show: what happened, rather than how it looks. */
7291
+ const STATUS_KINDS = {
7292
+ destructive: [
7293
+ "destructive",
7294
+ "danger",
7295
+ "error",
7296
+ "critical",
7297
+ "negative",
7298
+ "invalid"
7299
+ ],
7300
+ warning: [
7301
+ "warning",
7302
+ "warn",
7303
+ "caution"
7304
+ ],
7305
+ success: [
7306
+ "success",
7307
+ "positive",
7308
+ "valid"
7309
+ ],
7310
+ info: [
7311
+ "info",
7312
+ "information",
7313
+ "notice"
7314
+ ]
7315
+ };
7316
+ const STATUS_WORDS = new Set(Object.values(STATUS_KINDS).flat());
6686
7317
  /** The family a color token belongs to: the first segment of its name (`sidebar`, `sh`, `rainbow`). */
6687
7318
  function tokenFamily(token) {
6688
7319
  return tokenKey(token).split("-")[0] ?? "";
@@ -6725,6 +7356,19 @@ function scopesIn(families, ...texts) {
6725
7356
  function isStatusToken(token) {
6726
7357
  return roleKey(token).split("-").some((segment) => STATUS_WORDS.has(segment));
6727
7358
  }
7359
+ /**
7360
+ * The statuses a theme has a color for: `success` for `--success`, Dub's `content-success` or a
7361
+ * DTCG `color.feedback.positive`, `destructive` for shadcn/ui's `--destructive` or `error`.
7362
+ */
7363
+ function statusKinds(tokens) {
7364
+ const kinds = /* @__PURE__ */ new Set();
7365
+ for (const token of tokens) {
7366
+ if (token.category !== "color") continue;
7367
+ const segments = roleKey(token).split("-");
7368
+ for (const [kind, words] of Object.entries(STATUS_KINDS)) if (segments.some((segment) => words.includes(segment))) kinds.add(kind);
7369
+ }
7370
+ return kinds;
7371
+ }
6728
7372
  /** `primary` for `--color-primary`, `text-muted` for the DTCG token `color.text.muted`. */
6729
7373
  function tokenKey(token) {
6730
7374
  return (token.tailwind || token.name).toLowerCase().replace(/[.\s_]+/g, "-").replace(/^colou?rs?-/, "");
@@ -6887,7 +7531,7 @@ function toDtcg(tokens) {
6887
7531
  const paths = assignPaths(tokens, warnings);
6888
7532
  const lookup = varLookup(tokens);
6889
7533
  const byName = new Map(tokens.map((t) => [t.name, t]));
6890
- const tree = {};
7534
+ const tree = Object.create(null);
6891
7535
  let count = 0;
6892
7536
  for (const category of TOKEN_CATEGORIES) for (const token of tokens) {
6893
7537
  const path = token.category === category ? paths.get(token) : void 0;
@@ -6969,7 +7613,7 @@ function insert(tree, path, node) {
6969
7613
  const next = group[key];
6970
7614
  if (isObject$1(next)) group = next;
6971
7615
  else {
6972
- const created = {};
7616
+ const created = Object.create(null);
6973
7617
  group[key] = created;
6974
7618
  group = created;
6975
7619
  }
@@ -7201,7 +7845,7 @@ function colorObject(css) {
7201
7845
  else if (spec.space === "srgb" && components.every((c) => typeof c === "number" && Math.abs(c * 255 - Math.round(c * 255)) < .001)) out.hex = formatHex(color);
7202
7846
  return out;
7203
7847
  }
7204
- const PLAIN_LENGTH = /^(-?(?:\d+\.?\d*|\.\d+))(px|rem)$/;
7848
+ const PLAIN_LENGTH = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(px|rem)$/;
7205
7849
  /** `0.5rem` → `{ value: 0.5, unit: "rem" }`; a `calc()` of tokens → its px value. */
7206
7850
  function dimensionValue(value, lookup) {
7207
7851
  const plain = plainDimension(value);
@@ -7231,8 +7875,8 @@ function plainDimension(value) {
7231
7875
  unit: match[2] ?? "px"
7232
7876
  } : void 0;
7233
7877
  }
7234
- const COMPOSITE = /^[\w-]+\s*:\s*[^;]+(?:;\s*[\w-]+\s*:\s*[^;]+)*;?$/;
7235
- const NUMBER = /^-?(?:\d+\.?\d*|\.\d+)$/;
7878
+ const COMPOSITE = /^[\w-]+\s*:[^;]+(?:;\s*[\w-]+\s*:[^;]+)*;?$/;
7879
+ const NUMBER = /^-?(?:\d+(?:\.\d*)?|\.\d+)$/;
7236
7880
  /** What a typography token sets, from its name and Tailwind class (`font-sans`, `text-sm`). */
7237
7881
  function typographyKind(token) {
7238
7882
  const value = token.value.trim();
@@ -7492,7 +8136,7 @@ function frontMatterValue(token, key, lookup) {
7492
8136
  }
7493
8137
  }
7494
8138
  /** A DESIGN.md dimension: px, em or rem. */
7495
- const DESIGN_DIMENSION = /^-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)$/;
8139
+ const DESIGN_DIMENSION = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:px|rem|em)$/;
7496
8140
  /**
7497
8141
  * A typography token as a DESIGN.md typography level: `{ fontFamily: "Inter" }` for
7498
8142
  * `--font-sans`, every property of a composite. A `var()` is no font name (`next/font` sets
@@ -9557,6 +10201,35 @@ function labelledByParent(context, element) {
9557
10201
  return !ts.isJsxElement(value) && !ts.isJsxSelfClosingElement(value) && !ts.isJsxFragment(value);
9558
10202
  }
9559
10203
  /**
10204
+ * What an `id` or `htmlFor` holds, to compare the two: a string as itself (`"dark-mode"`, `{"x"}`),
10205
+ * an expression as written (`{id}`, `` {`${id}-switch`} ``) together with the function it is in,
10206
+ * since two components in one file can each have their own `id`. Undefined when it holds nothing.
10207
+ */
10208
+ function idValue(context, attribute) {
10209
+ const init = attribute?.initializer;
10210
+ if (!init) return void 0;
10211
+ const value = ts.isJsxExpression(init) ? init.expression : init;
10212
+ if (!value) return void 0;
10213
+ if (ts.isStringLiteralLike(value)) return value.text.trim() ? `"${value.text}"` : void 0;
10214
+ const scope = ts.findAncestor(value, ts.isFunctionLike);
10215
+ return `${String(scope?.pos ?? -1)}:${value.getText(context.sourceFile)}`;
10216
+ }
10217
+ /**
10218
+ * Whether a label with content names the button through its `id`: `<label htmlFor={id}>Dark
10219
+ * mode</label>` next to `<button id={id} role="switch">`, or the design system's `<Label>`. A
10220
+ * button is a labelable element, so the label's text is its accessible name.
10221
+ */
10222
+ function labelledByFor(context, element) {
10223
+ const id = idValue(context, findAttribute(element, "id"));
10224
+ if (id === void 0) return false;
10225
+ return context.analysis.elements.some((label) => {
10226
+ if (label === element) return false;
10227
+ if (idValue(context, findAttribute(label, "htmlFor") ?? findAttribute(label, "for")) !== id) return false;
10228
+ const content = classify(label.children, context, []);
10229
+ return content === "text" || content === "unknown";
10230
+ });
10231
+ }
10232
+ /**
9560
10233
  * The element whose `render` prop this one is, as in Base UI's
9561
10234
  * `<Dialog.Close render={<Button size="icon" />}><XIcon /></Dialog.Close>`:
9562
10235
  * the rendered button takes the host's children and attributes. Also through
@@ -9662,6 +10335,7 @@ const iconButtonAccessibleName = {
9662
10335
  if (host && (isButton(context, host) || host.hasSpread)) continue;
9663
10336
  if (hasLabel(element.attributes) || host && hasLabel(host.attributes)) continue;
9664
10337
  if (labelledByParent(context, element)) continue;
10338
+ if (labelledByFor(context, element) || host && labelledByFor(context, host)) continue;
9665
10339
  const icons = [];
9666
10340
  const children = host ? [...host.children, ...element.children] : element.children;
9667
10341
  const content = host && !children.length ? "unknown" : classify(children, context, icons);
@@ -9946,8 +10620,63 @@ const noHardcodedColor = {
9946
10620
  }
9947
10621
  }
9948
10622
  }
10623
+ reportChartTokens(context, allow);
9949
10624
  }
9950
10625
  };
10626
+ /** Packages that draw charts: Recharts, Chart.js, Nivo, visx, Victory, Tremor, MUI X Charts, D3. */
10627
+ const CHART_LIBRARY = /chart|^@nivo\/|^@visx\/|^victory|^@tremor\/|^d3(?:-|$)/i;
10628
+ /** Roles of an element that is a drawing: a chart drawn by hand is an image. */
10629
+ const DRAWING_ROLE = /^(?:img|figure|graphics-[a-z]+)$/;
10630
+ const CHART_TOKEN = "is a chart color, and this is not a chart: it changes with the chart palette, and it does not say what a status or brand color says. For a status color, add a token for it to the theme, such as `--success`; otherwise use the semantic token that fits.";
10631
+ /**
10632
+ * Warns on a chart token outside a chart: `text-chart-2` on a green "Active" label in a theme
10633
+ * with no success color. The class is a token, so the color rule would call it clean, but it
10634
+ * says nothing a status or brand color says, and it changes with the chart palette (a gray or a
10635
+ * blue in some shadcn/ui themes). A file that draws a chart is in scope throughout, legend dots
10636
+ * and all: one that imports a chart library, renders a tag such as `<ChartContainer>` or
10637
+ * `<BarChart>`, declares a name such as `chartData`, or is named for charts. So is an element
10638
+ * in a drawing (`<svg>`, `role="img"`), as a chart drawn by hand is. When the design system's
10639
+ * own components use chart tokens outside a chart, they are a palette it shares: nothing is
10640
+ * reported.
10641
+ */
10642
+ function reportChartTokens(context, allow) {
10643
+ const { tokens } = context.target;
10644
+ if (!tokens.scopedFamilies.has("chart")) return;
10645
+ const chart = /* @__PURE__ */ new Set(["chart"]);
10646
+ if ([...tokens.familyFiles.get("chart") ?? []].some((file) => !scopesIn(chart, file).size)) return;
10647
+ const { elements, imports, declared } = context.analysis;
10648
+ if (scopesIn(chart, ...[
10649
+ context.file,
10650
+ ...elements.map((e) => e.tag),
10651
+ ...declared
10652
+ ]).size) return;
10653
+ if ([...imports.values()].some((binding) => CHART_LIBRARY.test(binding.source))) return;
10654
+ for (const classString of context.analysis.classStrings) {
10655
+ const { element } = classString;
10656
+ if (element && inDrawing(element)) continue;
10657
+ for (const cls of splitClasses(classString.text, classString.start)) {
10658
+ const key = COLOR_UTILITY.exec(parseUtility(cls.value).base)?.[1];
10659
+ if (!key?.startsWith("chart-") || !tokens.colorKeys.has(key)) continue;
10660
+ if (allow.has(cls.value.toLowerCase())) continue;
10661
+ context.report({
10662
+ start: cls.start,
10663
+ end: cls.end,
10664
+ message: `\`${cls.value}\` ${CHART_TOKEN}`,
10665
+ severity: "warning"
10666
+ });
10667
+ }
10668
+ }
10669
+ }
10670
+ /** Whether the element is, or is in, a drawing: an `<svg>`, a `<figure>` or a `role="img"`. */
10671
+ function inDrawing(element) {
10672
+ return ts.findAncestor(element.node, (node) => {
10673
+ if (!ts.isJsxElement(node) && !ts.isJsxSelfClosingElement(node)) return false;
10674
+ const opening = ts.isJsxElement(node) ? node.openingElement : node;
10675
+ const tag = opening.tagName.getText(element.node.getSourceFile());
10676
+ if (tag === "svg" || tag === "figure") return true;
10677
+ return opening.attributes.properties.some((attribute) => ts.isJsxAttribute(attribute) && attributeName(attribute) === "role" && literalValues(attribute).some((value) => DRAWING_ROLE.test(value.text)));
10678
+ }) !== void 0;
10679
+ }
9951
10680
  function reportLiteralColors(context, literal, element, options) {
9952
10681
  const start = literal.getStart(context.sourceFile) + 1;
9953
10682
  const text = context.text.slice(start, literal.end - 1);
@@ -10984,28 +11713,36 @@ function readBaseline(file) {
10984
11713
  rules: parsed.rules ? new Set(parsed.rules) : void 0
10985
11714
  };
10986
11715
  }
11716
+ /** Whether `diagnostic` is on one of the `lines`, such as those a pull request changed. */
11717
+ function onLines(lines, diagnostic) {
11718
+ return lines?.some(([start, end]) => start <= diagnostic.endLine && end >= diagnostic.line) ?? false;
11719
+ }
10987
11720
  /**
10988
11721
  * Removes the findings the baseline accepts for `file`. Within a file and
10989
11722
  * rule, each entry accepts that many findings with the same source text, in
10990
- * order; any beyond the count are new. Entries of `disabled` rules are not
10991
- * counted as fixed: the rule is off, not satisfied.
11723
+ * order; any beyond the count are new. With `changed`, the lines a pull request
11724
+ * changed, findings on other lines are accepted first: a copy of a baselined
11725
+ * line inserted above it is the new one, not the line that was there. Entries
11726
+ * of `disabled` rules are not counted as fixed: the rule is off, not satisfied.
10992
11727
  */
10993
- function applyBaseline(baseline, file, result, disabled = /* @__PURE__ */ new Set()) {
11728
+ function applyBaseline(baseline, file, result, disabled = /* @__PURE__ */ new Set(), changed) {
10994
11729
  const remaining = /* @__PURE__ */ new Map();
10995
11730
  for (const [rule, sources] of baseline.get(file) ?? []) {
10996
11731
  if (disabled.has(rule)) continue;
10997
11732
  for (const [source, count] of sources) remaining.set(`${rule}\0${source}`, count);
10998
11733
  }
10999
- const diagnostics = [];
11000
- let baselined = 0;
11001
- for (const diagnostic of result.diagnostics) {
11734
+ const accepted = /* @__PURE__ */ new Set();
11735
+ for (const later of [false, true]) for (const diagnostic of result.diagnostics) {
11736
+ if (onLines(changed, diagnostic) !== later) continue;
11002
11737
  const id = `${diagnostic.ruleId}\0${findingKey(diagnostic)}`;
11003
11738
  const left = remaining.get(id) ?? 0;
11004
11739
  if (left > 0) {
11005
11740
  remaining.set(id, left - 1);
11006
- baselined++;
11007
- } else diagnostics.push(diagnostic);
11741
+ accepted.add(diagnostic);
11742
+ }
11008
11743
  }
11744
+ const diagnostics = result.diagnostics.filter((d) => !accepted.has(d));
11745
+ const baselined = accepted.size;
11009
11746
  let fixed = 0;
11010
11747
  for (const count of remaining.values()) fixed += count;
11011
11748
  return {
@@ -11020,15 +11757,116 @@ function applyBaseline(baseline, file, result, disabled = /* @__PURE__ */ new Se
11020
11757
  };
11021
11758
  }
11022
11759
  /**
11760
+ * Whether the key `file`, relative to the real `root`, still names a file by that spelling. On a
11761
+ * disk that ignores case (macOS, Windows), `Billing.tsx` still opens after a rename to
11762
+ * `billing.tsx`, but `check` keys the file by its new spelling, so the old key is gone, as on
11763
+ * Linux.
11764
+ */
11765
+ function existsAs(root, file) {
11766
+ try {
11767
+ return relativePath(root, fs.realpathSync.native(path.resolve(root, file))).normalize("NFC") === file;
11768
+ } catch {
11769
+ return false;
11770
+ }
11771
+ }
11772
+ /**
11773
+ * The files with baseline entries that no longer exist under `root`, deleted or renamed, among
11774
+ * those `include` takes. `checked` are files a run read, which exist.
11775
+ */
11776
+ function goneFiles(entries, root, checked, include = () => true) {
11777
+ return [...entries.keys()].filter((file) => !checked.has(file) && include(file) && !existsAs(root, file));
11778
+ }
11779
+ /**
11780
+ * Renamed and moved files (`git mv`, a folder reorganised): each file of `unrecorded` (file → its
11781
+ * findings), which has no entries of its own, paired with a file of `gone` whose entries match at
11782
+ * least half of its findings and half of those entries match its findings, as git counts a
11783
+ * rename from 50% similarity. Each file of `gone` pairs with one file at most, so a copy of a
11784
+ * file is new however many copies there are. Entries of rules `disabled` for the new file are
11785
+ * left out, as `applyBaseline` leaves them out. Returns new file → old file.
11786
+ *
11787
+ * Best match first: the file with most matching findings that are not `fresh`, then the one with
11788
+ * most matching findings, then the pair with fewest findings and entries the other does not
11789
+ * match. With `fresh`, the findings on the lines a pull request changed, a file the pull request
11790
+ * did not touch keeps the entries it took over in an earlier move, and a copy the pull request
11791
+ * adds is the new one, as `applyBaseline` spends entries on the other lines first.
11792
+ * Without it, as in a plain `check`, ties go to a file that kept the old one's name (a folder
11793
+ * moved), then to one in the old one's folder (a file renamed), then to path order.
11794
+ */
11795
+ function findMoves(entries, unrecorded, gone, disabled = () => /* @__PURE__ */ new Set(), fresh = () => false) {
11796
+ const recorded = /* @__PURE__ */ new Map();
11797
+ for (const file of gone) for (const [rule, sources] of entries.get(file) ?? []) for (const [source, count] of sources) {
11798
+ const id = `${rule}\0${source}`;
11799
+ const list = recorded.get(id) ?? [];
11800
+ list.push({
11801
+ file,
11802
+ count
11803
+ });
11804
+ recorded.set(id, list);
11805
+ }
11806
+ const pairs = [];
11807
+ for (const [to, diagnostics] of unrecorded) {
11808
+ if (entries.has(to) || !diagnostics.length) continue;
11809
+ const found = /* @__PURE__ */ new Map();
11810
+ for (const diagnostic of diagnostics) {
11811
+ const id = `${diagnostic.ruleId}\0${findingKey(diagnostic)}`;
11812
+ const n = found.get(id) ?? {
11813
+ all: 0,
11814
+ old: 0
11815
+ };
11816
+ n.all += 1;
11817
+ if (!fresh(to, diagnostic)) n.old += 1;
11818
+ found.set(id, n);
11819
+ }
11820
+ const matched = /* @__PURE__ */ new Map();
11821
+ for (const [id, { all, old }] of found) for (const { file, count } of recorded.get(id) ?? []) {
11822
+ const m = matched.get(file) ?? {
11823
+ matched: 0,
11824
+ kept: 0
11825
+ };
11826
+ m.matched += Math.min(all, count);
11827
+ m.kept += Math.min(old, count);
11828
+ matched.set(file, m);
11829
+ }
11830
+ const off = disabled(to);
11831
+ for (const [from, { matched: n, kept }] of matched) {
11832
+ let size = 0;
11833
+ for (const [rule, sources] of entries.get(from) ?? []) {
11834
+ if (off.has(rule)) continue;
11835
+ for (const count of sources.values()) size += count;
11836
+ }
11837
+ const unmatched = diagnostics.length + size - 2 * n;
11838
+ if (n * 2 >= diagnostics.length && n * 2 >= size) pairs.push({
11839
+ to,
11840
+ from,
11841
+ matched: n,
11842
+ kept,
11843
+ unmatched
11844
+ });
11845
+ }
11846
+ }
11847
+ const sameName = (pair) => path.posix.basename(pair.to) === path.posix.basename(pair.from) ? 0 : 1;
11848
+ const sameFolder = (pair) => path.posix.dirname(pair.to) === path.posix.dirname(pair.from) ? 0 : 1;
11849
+ const order = (a, b) => a < b ? -1 : a > b ? 1 : 0;
11850
+ pairs.sort((a, b) => b.kept - a.kept || b.matched - a.matched || a.unmatched - b.unmatched || sameName(a) - sameName(b) || sameFolder(a) - sameFolder(b) || order(a.to, b.to) || order(a.from, b.from));
11851
+ const moves = /* @__PURE__ */ new Map();
11852
+ const taken = /* @__PURE__ */ new Set();
11853
+ for (const { to, from } of pairs) {
11854
+ if (moves.has(to) || taken.has(from)) continue;
11855
+ moves.set(to, from);
11856
+ taken.add(from);
11857
+ }
11858
+ return moves;
11859
+ }
11860
+ /**
11023
11861
  * The baseline after a run over `checked` (file → its findings): those files
11024
11862
  * get exactly their current findings, plus their previous entries for
11025
11863
  * `disabled` rules; other files keep their entries while they still exist
11026
- * under `root`.
11864
+ * under `root` by the same spelling.
11027
11865
  */
11028
11866
  function updateBaseline(previous, checked, root, disabled = /* @__PURE__ */ new Set()) {
11029
11867
  const disabledIn = typeof disabled === "function" ? disabled : () => disabled;
11030
11868
  const next = /* @__PURE__ */ new Map();
11031
- for (const [file, rules] of previous ?? []) if (!checked.has(file) && fs.existsSync(path.resolve(root, file))) next.set(file, rules);
11869
+ for (const [file, rules] of previous ?? []) if (!checked.has(file) && existsAs(root, file)) next.set(file, rules);
11032
11870
  for (const [file, diagnostics] of checked) {
11033
11871
  const rules = /* @__PURE__ */ new Map();
11034
11872
  const off = disabledIn(file);
@@ -11063,14 +11901,21 @@ function rulesRun(previous, next, checked, ran, wholeRoot) {
11063
11901
  * The baseline without the entries a run over `checked` (file → its findings) shows no longer
11064
11902
  * occur: a checked file keeps at most as many of each finding as it still has, plus its entries
11065
11903
  * for `disabled` rules, and files the run did not check keep theirs while they still exist under
11066
- * `root`. Nothing is added, so a new finding stays new, unlike with `updateBaseline`.
11904
+ * `root` by the same spelling. Nothing is added, so a new finding stays new, unlike with
11905
+ * `updateBaseline`. A file of `moves` (new file → old file, from `findMoves`) takes the old
11906
+ * file's entries as its own.
11067
11907
  */
11068
- function pruneBaseline(previous, checked, root, disabled) {
11908
+ function pruneBaseline(previous, checked, root, disabled, moves = /* @__PURE__ */ new Map()) {
11909
+ const files = new Map(previous);
11910
+ for (const [to, from] of moves) {
11911
+ const rules = previous.get(from);
11912
+ if (rules && !previous.has(to)) files.set(to, rules);
11913
+ }
11069
11914
  const next = /* @__PURE__ */ new Map();
11070
- for (const [file, rules] of previous) {
11915
+ for (const [file, rules] of files) {
11071
11916
  const diagnostics = checked.get(file);
11072
11917
  if (!diagnostics) {
11073
- if (fs.existsSync(path.resolve(root, file))) next.set(file, rules);
11918
+ if (existsAs(root, file)) next.set(file, rules);
11074
11919
  continue;
11075
11920
  }
11076
11921
  const found = /* @__PURE__ */ new Map();
@@ -11153,7 +11998,7 @@ function isObject(value) {
11153
11998
  var name = "onsystem";
11154
11999
  //#endregion
11155
12000
  //#region src/version.ts
11156
- const VERSION = "0.8.0";
12001
+ const VERSION = "0.9.1";
11157
12002
  const NAME = name;
11158
12003
  //#endregion
11159
12004
  //#region src/lint/sarif.ts
@@ -11837,6 +12682,89 @@ const WEIGHTS = {
11837
12682
  props: 1,
11838
12683
  examples: .5
11839
12684
  };
12685
+ /**
12686
+ * What the components of shadcn/ui's registry are for, by the name its file exports them
12687
+ * under (`Badge` in `badge.tsx`). Its files have no JSDoc, so without these a stock shadcn/ui
12688
+ * app lists every component as undescribed and search finds them by name alone.
12689
+ */
12690
+ const SHADCN_DESCRIPTIONS = {
12691
+ Accordion: "Stacked sections whose headings expand and collapse their content.",
12692
+ Alert: "A callout that draws attention to a message, with an optional title and icon.",
12693
+ AlertDialog: "A modal dialog that asks the user to confirm an action, such as a deletion, before it happens.",
12694
+ AspectRatio: "Keeps its content, such as an image or a video, at a fixed width-to-height ratio.",
12695
+ Avatar: "A user's picture, with a fallback such as initials while it loads or when it is missing.",
12696
+ Badge: "A small label for a status, a count or a category.",
12697
+ Breadcrumb: "The path to the current page, as a trail of links.",
12698
+ Button: "Triggers an action, such as submitting a form or opening a dialog.",
12699
+ ButtonGroup: "Joins related buttons into one bar.",
12700
+ Calendar: "A month grid to pick a date or a range of dates.",
12701
+ Card: "A bordered surface that groups related content, with a header, content and footer.",
12702
+ Carousel: "Shows a row of slides one at a time, with previous and next buttons.",
12703
+ Checkbox: "A box the user ticks to turn an option on or off.",
12704
+ Collapsible: "Shows and hides a panel of content from a trigger.",
12705
+ Combobox: "A text input with a list of options that narrows as the user types.",
12706
+ Command: "A searchable list of commands or options, as in a command palette.",
12707
+ ContextMenu: "A menu of actions that opens on right-click.",
12708
+ Dialog: "A modal window over the page, for a task or a message that needs the user first.",
12709
+ Drawer: "A panel that slides in from an edge of the screen, often from the bottom on phones.",
12710
+ DropdownMenu: "A menu of actions or options that opens from a button.",
12711
+ Empty: "An empty state: what to show when there is nothing yet, with an action to start.",
12712
+ Field: "A form field: its label, control, description and error message together.",
12713
+ HoverCard: "A card of preview content that opens while the pointer rests on a link.",
12714
+ Input: "A single-line text field.",
12715
+ InputGroup: "An input with icons, text or buttons inside its border.",
12716
+ InputOTP: "A row of one-character slots for a one-time code.",
12717
+ Item: "A row of content for lists: media, a title, a description and actions.",
12718
+ Kbd: "Shows a keyboard key or shortcut.",
12719
+ Label: "The text label of a form control.",
12720
+ Menubar: "A horizontal bar of menus, as in a desktop app.",
12721
+ NativeSelect: "A styled native <select> element.",
12722
+ NavigationMenu: "A site's main navigation links, with panels that open from them.",
12723
+ Pagination: "Links to move between pages of results, with previous and next.",
12724
+ Popover: "Rich content in a floating panel that opens from a button.",
12725
+ Progress: "A bar that shows how far a task has got.",
12726
+ RadioGroup: "A set of options of which the user picks exactly one.",
12727
+ ScrollArea: "A scrollable region with styled scrollbars.",
12728
+ Select: "Lets the user pick one value from a list that opens from a button.",
12729
+ Separator: "A thin line between content, horizontal or vertical.",
12730
+ Sheet: "A panel that slides in over the page from an edge of the screen, for secondary content.",
12731
+ Sidebar: "The app's side navigation: a collapsible panel of grouped menu items.",
12732
+ Skeleton: "A placeholder shape shown while content loads.",
12733
+ Slider: "Picks a number, or a range, by dragging along a track.",
12734
+ Spinner: "An animated indicator that something is loading.",
12735
+ Switch: "A toggle that turns a setting on or off at once.",
12736
+ Table: "Rows and columns of data.",
12737
+ Tabs: "Switches between panels of content, one visible at a time.",
12738
+ Textarea: "A multi-line text field.",
12739
+ Toggle: "A button that stays pressed or not, such as Bold in a toolbar.",
12740
+ ToggleGroup: "A set of toggle buttons, of which one or several can be on.",
12741
+ Tooltip: "A short hint that appears while the pointer rests on, or focus is in, an element."
12742
+ };
12743
+ /**
12744
+ * What shadcn/ui's component of this name is for, when `component` is laid out as `npx shadcn
12745
+ * add` writes it: exported under the registry's name from the file named for it, in a `ui`
12746
+ * folder (`Badge` from `components/ui/badge.tsx`, components.json's default `ui` alias). A
12747
+ * component of the same name elsewhere (`Sheet` in a design system's `src/sheet.tsx`) is not
12748
+ * shadcn/ui's, and may be something else entirely.
12749
+ */
12750
+ function shadcnPurpose(component) {
12751
+ if (component.library || component.parent) return void 0;
12752
+ if (!Object.hasOwn(SHADCN_DESCRIPTIONS, component.name)) return void 0;
12753
+ const [folder, file = ""] = component.source.file.split(/[\\/]/).slice(-2);
12754
+ if (folder !== "ui") return void 0;
12755
+ const kebab = splitWords(component.name).join("-").toLowerCase();
12756
+ return file.replace(/\.[jt]sx?$/, "") === kebab ? SHADCN_DESCRIPTIONS[component.name] : void 0;
12757
+ }
12758
+ /**
12759
+ * The component's own description or, for a shadcn/ui component without one (`Badge` in
12760
+ * `components/ui/badge.tsx`, no JSDoc), what shadcn/ui's component of that name is for, said
12761
+ * as shadcn/ui's: `shadcn/ui's Badge: A small label for a status, a count or a category.`
12762
+ */
12763
+ function descriptionOf(component) {
12764
+ if (component.description) return component.description;
12765
+ const purpose = shadcnPurpose(component);
12766
+ return purpose && `shadcn/ui's ${component.name}: ${purpose}`;
12767
+ }
11840
12768
  function buildSearchIndex(components) {
11841
12769
  const index = new Bm25Index();
11842
12770
  for (const component of components) index.add(component, [
@@ -11845,7 +12773,7 @@ function buildSearchIndex(components) {
11845
12773
  ...component.aliases,
11846
12774
  component.parent ?? ""
11847
12775
  ].join(" ")), WEIGHTS.name],
11848
- [tokenize(component.description ?? ""), WEIGHTS.description],
12776
+ [tokenize(component.description ?? shadcnPurpose(component) ?? ""), WEIGHTS.description],
11849
12777
  [tokenize(component.variants.flatMap((v) => [v.name, ...v.values]).join(" ")), WEIGHTS.variants],
11850
12778
  [tokenize((component.docs?.sections ?? []).map((s) => `${s.heading} ${s.body}`).join(" ")), WEIGHTS.docs],
11851
12779
  [tokenize(component.props.map((p) => `${p.name} ${p.description ?? ""}`).join(" ")), WEIGHTS.props],
@@ -11853,61 +12781,49 @@ function buildSearchIndex(components) {
11853
12781
  ]);
11854
12782
  return index;
11855
12783
  }
11856
- function searchComponents(index, query, limit = 5) {
11857
- return index.search(expandQuery(query), limit).map((hit) => ({
11858
- component: hit.item,
11859
- score: Math.round(hit.score * 100) / 100,
11860
- matched: hit.terms
11861
- }));
11862
- }
11863
- //#endregion
11864
- //#region src/tokens/imports.ts
11865
- /** Enough stylesheets for a theme split over files, without reading a whole design system. */
11866
- const MAX_IMPORTED = 50;
11867
- /**
11868
- * The specifiers a stylesheet `@import`s: `@import "./theme.css" layer(base)`,
11869
- * `@import url(theme.css)`, `@import "@acme/theme"`. Empty for CSS that does not parse.
11870
- */
11871
- function cssImports(css) {
11872
- let root;
11873
- try {
11874
- root = postcss.parse(css);
11875
- } catch {
11876
- return [];
11877
- }
11878
- const out = [];
11879
- for (const node of root.nodes) {
11880
- if (node.type !== "atrule" || node.name !== "import") continue;
11881
- const match = /^\s*(?:url\(\s*)?(?:"([^"]+)"|'([^']+)'|([^\s"')]+)\s*\))/.exec(node.params);
11882
- const specifier = match?.[1] ?? match?.[2] ?? match?.[3];
11883
- if (specifier) out.push(specifier);
12784
+ /** The query's words, each with the terms that find it: `modal` is found by modal and dialog. */
12785
+ function queryWords(query) {
12786
+ const words = /* @__PURE__ */ new Map();
12787
+ for (const word of splitWords(query)) {
12788
+ const stems = tokenize(word);
12789
+ const key = stems.join(" ");
12790
+ if (stems.length && !words.has(key)) words.set(key, {
12791
+ stems,
12792
+ terms: [...expandQuery(word).keys()]
12793
+ });
11884
12794
  }
11885
- return out;
12795
+ return [...words.values()];
11886
12796
  }
11887
12797
  /**
11888
- * The stylesheets `files` import, and the ones those import in turn, each once and in the
11889
- * order they are imported. The files themselves are not repeated.
12798
+ * Components for `query`, best first. A hit matches clearly when a word of the query is in
12799
+ * its name (`button` for Button) or when it has more than half of the query's words, each
12800
+ * itself or through a synonym (`modal` through `dialog`). The hits that do not are left out
12801
+ * when any does, and otherwise come back marked `weak`: "a dropdown menu to pick a country",
12802
+ * in a design system with no select, finds only a card's action slot that mentions a menu,
12803
+ * which is not an answer to give as the best match.
11890
12804
  */
11891
- function importedStylesheets(files, resolve) {
11892
- const seen = new Set(files);
11893
- const out = [];
11894
- const visit = (file) => {
11895
- let text;
11896
- try {
11897
- text = fs.readFileSync(file, "utf8");
11898
- } catch {
11899
- return;
11900
- }
11901
- for (const specifier of cssImports(text)) {
11902
- const target = resolve(specifier, file);
11903
- if (!target || seen.has(target) || out.length >= MAX_IMPORTED) continue;
11904
- seen.add(target);
11905
- out.push(target);
11906
- visit(target);
11907
- }
11908
- };
11909
- for (const file of files) visit(file);
11910
- return out;
12805
+ function searchComponents(index, query, limit = 5) {
12806
+ const words = queryWords(query);
12807
+ const hits = index.search(expandQuery(query), index.size).map((hit) => {
12808
+ const matched = new Set(hit.terms);
12809
+ const covered = words.filter((word) => word.terms.some((term) => matched.has(term))).length;
12810
+ const names = new Set(tokenize([hit.item.name, ...hit.item.aliases].join(" ")));
12811
+ const clear = covered * 2 > words.length || words.some((word) => word.stems.some((s) => names.has(s)));
12812
+ return {
12813
+ found: {
12814
+ component: hit.item,
12815
+ score: Math.round(hit.score * 100) / 100,
12816
+ matched: hit.terms
12817
+ },
12818
+ clear
12819
+ };
12820
+ });
12821
+ const clear = hits.filter((hit) => hit.clear);
12822
+ if (clear.length) return clear.slice(0, limit).map((hit) => hit.found);
12823
+ return hits.slice(0, limit).map((hit) => ({
12824
+ ...hit.found,
12825
+ weak: true
12826
+ }));
11911
12827
  }
11912
12828
  //#endregion
11913
12829
  //#region src/util/log.ts
@@ -12255,7 +13171,7 @@ async function resolveFiles(config) {
12255
13171
  * `@dub/tailwind-config` and its `themes.css`), with what they import. May repeat a file.
12256
13172
  */
12257
13173
  function linkedStylesheets(config, found) {
12258
- const resolve = stylesheetResolver(config.root);
13174
+ const resolve = stylesheetResolver(config.root, { importPath: config.importPath });
12259
13175
  const preset = config.tailwindConfig ? presetStylesheets(readTailwindColors(config.tailwindConfig, presetResolver(config.root)), config.root) : [];
12260
13176
  return [
12261
13177
  ...importedStylesheets(found.filter((f) => f.endsWith(".css")), resolve),
@@ -12595,4 +13511,4 @@ var DesignSystemHost = class {
12595
13511
  }
12596
13512
  };
12597
13513
  //#endregion
12598
- export { hasOwnConfig as $, TokenIndex as A, parseDoc as B, pruneBaseline as C, isInside as Ct, updateBaseline as D, defaultProjectConfig as Dt, unrecordedRules as E, toPosix as Et, TOKEN_CATEGORIES as F, DEFAULT_SEVERITY as G, CONFIG_FILES as H, parseDtcgTokens as I, LEGACY_CONFIG_FILES as J, DEFAULT_TEST_EXCLUDE as K, parseDesignMd as L, mergeTokens as M, toDesignMd as N, writeBaseline as O, readProjectConfig as Ot, toDtcg as P, excludedBy as Q, parseCssTokens as R, countBaseline as S, SCRIPT_FILE$1 as St, rulesRun as T, relativePath as Tt, ConfigError as U, extractComponents as V, DEFAULT_DOCS as W, configJsonSchema as X, RULE_IDS as Y, defineConfig as Z, NAME as _, workspacePatterns as _t, fingerprint as a, LIBRARIES as at, LEGACY_BASELINE_FILE as b, truncate as bt, silentLogger as c, notReadYet as ct, searchComponents as d, declaredConfig as dt, loadConfig as et, applyFixes as f, detectProject as ft, formatDiagnostics as g, workspacePackageDirs as gt, escapeData as h, isWorkspaceRoot as ht, componentFiles as i, findVariantDefinitions as it, loadTokens as j, RULES as k, stderrLogger as l, PACKAGE_FIELD as lt, uncheckedNotice as m, isDesignSystemName as mt, DesignSystemHost as n, rulesFor as nt, loadDesignSystem as o, LIBRARY_IDS as ot, checkSource as p, findDesignSystemPackage as pt, DEFAULT_TEST_FOLDERS as q, buildModel as r, formatPx as rt, tokenFiles as s, libraryTitle as st, DesignSystem as t, loadConfigWith as tt, buildSearchIndex as u, PackageFinder as ut, VERSION as v, closest as vt, readBaseline as w, matchesGlob as wt, applyBaseline as x, unique as xt, BASELINE_FILE as y, plural$1 as yt, attachDocs as z };
13514
+ export { LEGACY_CONFIG_FILES as $, unrecordedRules as A, relativePath as At, statusKinds as B, countBaseline as C, closest as Ct, pruneBaseline as D, SCRIPT_FILE$2 as Dt, onLines as E, unique as Et, loadTokens as F, parseDoc as G, parseDesignMd as H, mergeTokens as I, ConfigError as J, extractComponents as K, toDesignMd as L, writeBaseline as M, defaultProjectConfig as Mt, RULES as N, readProjectConfig as Nt, readBaseline as O, isInside as Ot, TokenIndex as P, DEFAULT_TEST_FOLDERS as Q, toDtcg as R, applyBaseline as S, workspacePatterns as St, goneFiles as T, truncate as Tt, parseCssTokens as U, parseDtcgTokens as V, attachDocs as W, DEFAULT_SEVERITY as X, DEFAULT_DOCS as Y, DEFAULT_TEST_EXCLUDE as Z, formatDiagnostics as _, detectProject as _t, fingerprint as a, loadConfig as at, BASELINE_FILE as b, isWorkspaceRoot as bt, silentLogger as c, formatPx as ct, descriptionOf as d, LIBRARY_IDS as dt, RULE_IDS as et, searchComponents as f, libraryTitle as ft, escapeData as g, declaredConfig as gt, uncheckedNotice as h, PackageFinder as ht, componentFiles as i, hasOwnConfig as it, updateBaseline as j, toPosix as jt, rulesRun as k, matchesGlob as kt, stderrLogger as l, findVariantDefinitions as lt, checkSource as m, PACKAGE_FIELD as mt, DesignSystemHost as n, defineConfig as nt, loadDesignSystem as o, loadConfigWith as ot, applyFixes as p, notReadYet as pt, CONFIG_FILES as q, buildModel as r, excludedBy as rt, tokenFiles as s, rulesFor as st, DesignSystem as t, configJsonSchema as tt, buildSearchIndex as u, LIBRARIES as ut, NAME as v, findDesignSystemPackage as vt, findMoves as w, plural$1 as wt, LEGACY_BASELINE_FILE as x, workspacePackageDirs as xt, VERSION as y, isDesignSystemName as yt, TOKEN_CATEGORIES as z };