onsystem 0.5.0 → 0.6.0

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.
@@ -4,11 +4,12 @@ import { pathToFileURL } from "node:url";
4
4
  import * as z from "zod";
5
5
  import { escapePath, glob, globSync } from "tinyglobby";
6
6
  import ts from "typescript";
7
- import { clampChroma, colorsNamed, converter, differenceEuclidean, parse } from "culori";
7
+ import { clampChroma, colorsNamed, converter, differenceEuclidean, formatHex, parse } from "culori";
8
8
  import { createHash } from "node:crypto";
9
- import fs$1 from "node:fs/promises";
9
+ import fsp from "node:fs/promises";
10
10
  import os from "node:os";
11
11
  import postcss from "postcss";
12
+ import { LineCounter, isMap, isScalar, parseDocument, stringify } from "yaml";
12
13
  import { stemmer } from "stemmer";
13
14
  //#region src/extract/program.ts
14
15
  const DEFAULT_OPTIONS = {
@@ -32,7 +33,8 @@ function readProjectConfig(root, tsconfig) {
32
33
  options: { ...DEFAULT_OPTIONS },
33
34
  pathsBase: root,
34
35
  paths: {},
35
- configFiles: []
36
+ configFiles: [],
37
+ fileNames: []
36
38
  };
37
39
  const configFiles = [configFile];
38
40
  const read = ts.readConfigFile(configFile, (file) => ts.sys.readFile(file));
@@ -41,7 +43,8 @@ function readProjectConfig(root, tsconfig) {
41
43
  pathsBase: root,
42
44
  paths: {},
43
45
  configFile,
44
- configFiles
46
+ configFiles,
47
+ fileNames: []
45
48
  };
46
49
  const host = {
47
50
  useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
@@ -69,7 +72,8 @@ function readProjectConfig(root, tsconfig) {
69
72
  pathsBase: options.baseUrl ?? (typeof options.pathsBasePath === "string" ? options.pathsBasePath : path.dirname(configFile)),
70
73
  paths: options.paths ?? {},
71
74
  configFile,
72
- configFiles
75
+ configFiles,
76
+ fileNames: parsed.fileNames
73
77
  };
74
78
  }
75
79
  function createProgram(files, config, oldProgram) {
@@ -166,7 +170,7 @@ function globSource(pattern) {
166
170
  out += "\\{";
167
171
  continue;
168
172
  }
169
- const options = splitTopLevel$1(pattern.slice(i + 1, end));
173
+ const options = splitTopLevel$2(pattern.slice(i + 1, end));
170
174
  out += `(?:${options.map(globSource).join("|")})`;
171
175
  i = end;
172
176
  } else out += escapeRegExp(c);
@@ -180,7 +184,7 @@ function closingBrace(pattern, open) {
180
184
  else if (pattern[i] === "}" && --depth === 0) return i;
181
185
  return -1;
182
186
  }
183
- function splitTopLevel$1(text) {
187
+ function splitTopLevel$2(text) {
184
188
  const parts = [];
185
189
  let depth = 0;
186
190
  let current = "";
@@ -235,13 +239,17 @@ const SOURCE_EXTENSIONS = [
235
239
  const BUILD_DIR = /^(?:dist|build|lib|out|esm|cjs|es|module|types)\//;
236
240
  /** `index.mjs`, `index.d.ts`, `button.cjs`: an output file, by its extension. */
237
241
  const OUTPUT_EXTENSION = /(?:\.d)?\.[cm]?[jt]sx?$/;
238
- /** `base` as a file, with an extension added, or as a directory's index. */
239
- function moduleFile(base, extensions = MODULE_EXTENSIONS) {
242
+ /**
243
+ * `base` as a file, with an extension added, or as a directory's index, never in an installed
244
+ * package: in `node_modules`, or with `within` (a package's own folder, which may be installed
245
+ * itself), in a `node_modules` below it.
246
+ */
247
+ function moduleFile(base, extensions = MODULE_EXTENSIONS, within) {
240
248
  return [
241
249
  base,
242
250
  ...extensions.map((ext) => `${base}${ext}`),
243
251
  ...extensions.map((ext) => path.join(base, `index${ext}`))
244
- ].find((file) => isFile$1(file) && !/[\\/]node_modules[\\/]/.test(file));
252
+ ].find((file) => isFile$1(file) && !/(?:^|[\\/])node_modules[\\/]/.test(within ? path.relative(within, file) : file));
245
253
  }
246
254
  const BUILD_CONFIGS = [
247
255
  "tsup",
@@ -361,7 +369,7 @@ function sourceTarget(dir, key, target, entries) {
361
369
  ];
362
370
  for (const candidate of candidates) {
363
371
  if (candidate === void 0) continue;
364
- const file = moduleFile(path.join(dir, candidate), SOURCE_EXTENSIONS);
372
+ const file = moduleFile(path.join(dir, candidate), SOURCE_EXTENSIONS, dir);
365
373
  if (file && isInside(dir, file)) return toPosix(path.relative(dir, file));
366
374
  }
367
375
  }
@@ -389,7 +397,7 @@ function exportedFiles(file, packageDir) {
389
397
  if (!barrel) found.push(current);
390
398
  for (const specifier of from) {
391
399
  if (!specifier.startsWith(".")) continue;
392
- const next = moduleFile(path.resolve(path.dirname(current), specifier));
400
+ const next = moduleFile(path.resolve(path.dirname(current), specifier), void 0, packageDir);
393
401
  if (next && isInside(packageDir, next)) visit(next, depth + 1);
394
402
  }
395
403
  };
@@ -533,16 +541,19 @@ const STYLESHEETS = [
533
541
  * With more than one design system, the one the root's code imports most comes first
534
542
  * (`designSystems`). A candidate whose globs match no file is skipped. Returns undefined
535
543
  * when nothing applies, so the shadcn defaults stay in force.
544
+ *
545
+ * `declared` is the dependency whose config the root takes on ({@link findDesignSystemPackage}):
546
+ * it is read first among the dependencies in step 3, whatever its name, and also when it is
547
+ * installed from a registry rather than a workspace package.
536
548
  */
537
- function detectProject(root, tsconfig, trace) {
549
+ function detectProject(root, tsconfig, trace, declared, packages = new PackageFinder(root)) {
538
550
  const step = (candidate, accepted, reason) => trace?.push({
539
551
  candidate,
540
552
  accepted,
541
553
  reason
542
554
  });
543
- const packages = new PackageFinder(root);
544
555
  const found = [];
545
- const shadcn = readJson(path.join(root, "components.json"));
556
+ const shadcn = readJson$1(path.join(root, "components.json"));
546
557
  let why = "no ui or components alias";
547
558
  const fromShadcn = shadcn ? fromComponentsJson(root, root, shadcn, readProjectConfig(root, tsconfig), packages, (r) => {
548
559
  why = r;
@@ -558,7 +569,7 @@ function detectProject(root, tsconfig, trace) {
558
569
  step("components.json", true, `ui: ${fromShadcn.source}, ${countFiles(root, fromShadcn)}`);
559
570
  }
560
571
  const withDefaults = !found.length;
561
- const own = readJson(path.join(root, "package.json"));
572
+ const own = readJson$1(path.join(root, "package.json"));
562
573
  const ownName = typeof own?.name === "string" ? own.name : "the root";
563
574
  if (!found.length) {
564
575
  let reason = "no package.json";
@@ -575,17 +586,19 @@ function detectProject(root, tsconfig, trace) {
575
586
  ...asRecord(own?.dependencies),
576
587
  ...asRecord(own?.devDependencies)
577
588
  });
578
- const names = dependencies.filter(isDesignSystemName);
589
+ const names = unique([...declared ? [declared.name] : [], ...dependencies.filter(isDesignSystemName)]);
579
590
  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")}`);
580
591
  let imported;
581
592
  for (const name of names) {
582
- const candidate = `workspace package ${name}`;
593
+ const own = name === declared?.name ? declared : void 0;
594
+ const kind = own?.installed ? "installed" : "workspace";
595
+ const candidate = `${kind} package ${name}`;
583
596
  if (found.some((f) => f.package === name)) {
584
597
  step(candidate, false, "not added twice: components.json points into it");
585
598
  continue;
586
599
  }
587
- const dir = packages.find(name);
588
- const pkg = dir ? readJson(path.join(dir, "package.json")) : void 0;
600
+ const dir = own ? own.dir : packages.find(name);
601
+ const pkg = dir ? readJson$1(path.join(dir, "package.json")) : void 0;
589
602
  if (!dir || !pkg) {
590
603
  step(candidate, false, "not a workspace package: an installed package is compiled, and only sources are read");
591
604
  continue;
@@ -596,7 +609,7 @@ function detectProject(root, tsconfig, trace) {
596
609
  if (result && matchesAny(root, result)) {
597
610
  found.push({
598
611
  ...result,
599
- source: `workspace package ${result.source}`,
612
+ source: `${kind} package ${result.source}`,
600
613
  package: name,
601
614
  prefixes: [name]
602
615
  });
@@ -638,12 +651,23 @@ function detectProject(root, tsconfig, trace) {
638
651
  }
639
652
  /** `12 component files`, for the trace. */
640
653
  function countFiles(root, found) {
641
- const files = globSync(found.components, {
654
+ return plural(componentGlob(root, found.components).length, "component file");
655
+ }
656
+ /**
657
+ * The files `patterns` match, outside `node_modules` unless a pattern points into it: the
658
+ * sources of a design-system package installed there.
659
+ */
660
+ function componentGlob(root, patterns) {
661
+ const named = patterns.filter((p) => p.includes("node_modules"));
662
+ const rest = patterns.filter((p) => !p.includes("node_modules"));
663
+ const options = {
642
664
  cwd: root,
643
- ignore: ["**/node_modules/**"],
644
665
  expandDirectories: false
645
- }).length;
646
- return plural(files, "component file");
666
+ };
667
+ return [...rest.length ? globSync(rest, {
668
+ ...options,
669
+ ignore: ["**/node_modules/**"]
670
+ }) : [], ...named.length ? globSync(named, options) : []];
647
671
  }
648
672
  function plural(n, one, many = `${one}s`) {
649
673
  return `${n.toLocaleString("en-US")} ${n === 1 ? one : many}`;
@@ -737,11 +761,7 @@ function detection(root, found, withDefaults, source) {
737
761
  };
738
762
  }
739
763
  function matchesAny(root, found) {
740
- return globSync(found.components, {
741
- cwd: root,
742
- ignore: ["**/node_modules/**"],
743
- expandDirectories: false
744
- }).length > 0;
764
+ return componentGlob(root, found.components).length > 0;
745
765
  }
746
766
  /** `components.json` read from `dir` (the root, or a workspace package's directory). */
747
767
  function fromComponentsJson(root, dir, json, project, packages, note = ignore) {
@@ -756,7 +776,7 @@ function fromComponentsJson(root, dir, json, project, packages, note = ignore) {
756
776
  if (!uiDir) {
757
777
  const name = packageName(ui);
758
778
  const pkgDir = name ? packages.find(name) : void 0;
759
- const pkg = pkgDir ? readJson(path.join(pkgDir, "package.json")) : void 0;
779
+ const pkg = pkgDir ? readJson$1(path.join(pkgDir, "package.json")) : void 0;
760
780
  if (!name || !pkgDir || !pkg) {
761
781
  note(`the ui alias ${ui} resolves through neither tsconfig paths nor a workspace package`);
762
782
  return;
@@ -786,7 +806,7 @@ function fromComponentsJson(root, dir, json, project, packages, note = ignore) {
786
806
  */
787
807
  function ownerPackage(root, dir, alias) {
788
808
  for (let current = dir;; current = path.dirname(current)) {
789
- const name = readJson(path.join(current, "package.json"))?.name;
809
+ const name = readJson$1(path.join(current, "package.json"))?.name;
790
810
  if (typeof name === "string") return path.resolve(current) === path.resolve(root) ? { prefixes: [alias] } : {
791
811
  package: name,
792
812
  prefixes: [alias, name]
@@ -800,7 +820,7 @@ function ownerPackage(root, dir, alias) {
800
820
  * the package name, not through the package's internal `@/` alias.
801
821
  */
802
822
  function fromPackageComponentsJson(root, dir, pkg, packages, note = ignore) {
803
- const json = readJson(path.join(dir, "components.json"));
823
+ const json = readJson$1(path.join(dir, "components.json"));
804
824
  if (!json) return void 0;
805
825
  const found = fromComponentsJson(root, dir, json, readProjectConfig(dir), packages, (r) => {
806
826
  note(`its components.json: ${r}`);
@@ -876,7 +896,7 @@ function fromImports(root, dir, name, subpaths, note = ignore) {
876
896
  const stylesheets = [];
877
897
  const seen = /* @__PURE__ */ new Set();
878
898
  for (const subpath of [...subpaths ?? []].sort()) {
879
- const file = moduleFile(path.join(dir, subpath));
899
+ const file = moduleFile(path.join(dir, subpath), void 0, dir);
880
900
  if (!file || seen.has(file)) continue;
881
901
  seen.add(file);
882
902
  const specifier = `${name}/${subpath}`;
@@ -1065,7 +1085,7 @@ function moduleResolver(root) {
1065
1085
  if (!name || !dir) return void 0;
1066
1086
  const subpath = specifier.slice(name.length + 1);
1067
1087
  if (subpath) return moduleFile(path.join(dir, subpath));
1068
- const main = readJson(path.join(dir, "package.json"))?.main;
1088
+ const main = readJson$1(path.join(dir, "package.json"))?.main;
1069
1089
  return moduleFile(path.resolve(dir, typeof main === "string" ? main : "index"));
1070
1090
  };
1071
1091
  }
@@ -1086,7 +1106,7 @@ function workspaceModule(root) {
1086
1106
  if (!name || !dir) return void 0;
1087
1107
  const source = moduleFile(path.join(dir, "src", specifier.slice(name.length + 1) || "index"));
1088
1108
  if (source) return { file: source };
1089
- const exports = readJson(path.join(dir, "package.json"))?.exports;
1109
+ const exports = readJson$1(path.join(dir, "package.json"))?.exports;
1090
1110
  if (!exports || typeof exports !== "object" || Array.isArray(exports)) return void 0;
1091
1111
  const subpath = `.${specifier.slice(name.length)}`;
1092
1112
  return Object.keys(exports).some((key) => {
@@ -1096,18 +1116,130 @@ function workspaceModule(root) {
1096
1116
  }) ? {} : void 0;
1097
1117
  };
1098
1118
  }
1119
+ /** The package.json field a design-system package names its onsystem config with. */
1120
+ const PACKAGE_FIELD = "onsystem";
1121
+ /**
1122
+ * The config the package.json in `dir` declares: its `onsystem` field, a JSON file in the
1123
+ * package (`"./onsystem.config.json"`). Undefined without the field; a problem when the field
1124
+ * names something that cannot be read. Only JSON: a config in code would run a dependency's code.
1125
+ */
1126
+ function declaredConfig(dir, pkg = readJson$1(path.join(dir, "package.json"))) {
1127
+ const value = pkg?.[PACKAGE_FIELD];
1128
+ if (value === void 0) return void 0;
1129
+ const field = `"${PACKAGE_FIELD}" in ${path.join(dir, "package.json")}`;
1130
+ if (typeof value !== "string" || !value.endsWith(".json")) return { problem: `${field} is ${JSON.stringify(value)}: it names a JSON config file in the package, such as "./onsystem.config.json".` };
1131
+ const file = path.resolve(dir, value);
1132
+ if (!isInside(dir, file) || file === path.resolve(dir)) return { problem: `${field} names ${value}, which is outside the package.` };
1133
+ if (!isFile(file)) return { problem: `${field} names ${value}, which does not exist.` };
1134
+ return { file };
1135
+ }
1136
+ /**
1137
+ * The dependency of `root` (in `dependencies` or `devDependencies`) whose package.json declares
1138
+ * its onsystem config, as a workspace package or installed from a registry: the design system
1139
+ * the root uses, set up in its own package. With several, the one the root's code imports
1140
+ * most, then the first listed; `others` names the rest. A declaration that cannot be read is a
1141
+ * problem in a workspace package, which its team owns, and is skipped in an installed one, so a
1142
+ * published package cannot stop the apps that use it. Undefined at a workspace root, whose
1143
+ * projects each find their own, and without a package.json.
1144
+ *
1145
+ * Only the root's own dependencies are looked at, each where Node.js resolves it
1146
+ * ({@link PackageFinder.locate}). `packages` is the load's finder, shared with detection.
1147
+ */
1148
+ function findDesignSystemPackage(root, packages = new PackageFinder(root)) {
1149
+ const own = readJson$1(path.join(root, "package.json"));
1150
+ if (!own || isWorkspaceRoot(root)) return void 0;
1151
+ const names = Object.keys({
1152
+ ...asRecord(own.dependencies),
1153
+ ...asRecord(own.devDependencies)
1154
+ });
1155
+ const declaring = [];
1156
+ const problems = [];
1157
+ const skipped = [];
1158
+ for (const name of names) {
1159
+ const located = packages.locate(name);
1160
+ if (located?.json["onsystem"] === void 0) continue;
1161
+ const resolved = packages.place(located.dir);
1162
+ const declared = declaredConfig(resolved.dir, located.json);
1163
+ if (!declared) continue;
1164
+ if ("problem" in declared) (resolved.installed ? skipped : problems).push({
1165
+ name,
1166
+ problem: declared.problem
1167
+ });
1168
+ else declaring.push({
1169
+ name,
1170
+ dir: resolved.dir,
1171
+ config: declared.file,
1172
+ installed: resolved.installed,
1173
+ boundary: resolved.boundary
1174
+ });
1175
+ }
1176
+ let ranked = declaring;
1177
+ if (declaring.length > 1) {
1178
+ const counts = importCounts(root, declaring.map((p) => [p.name]));
1179
+ ranked = declaring.map((p, i) => ({
1180
+ p,
1181
+ imports: counts[i] ?? 0,
1182
+ i
1183
+ })).sort((a, b) => b.imports - a.imports || a.i - b.i).map((r) => r.p);
1184
+ }
1185
+ const [found, ...others] = ranked;
1186
+ return {
1187
+ found,
1188
+ others: others.map((p) => p.name),
1189
+ problems,
1190
+ skipped,
1191
+ dependencies: names.length
1192
+ };
1193
+ }
1194
+ /**
1195
+ * Import specifiers for the component files a package's `exports` lead to, through barrels and
1196
+ * from `dist/` back to the sources: what apps import its components by.
1197
+ */
1198
+ function packageImports(root, dir) {
1199
+ const pkg = readJson$1(path.join(dir, "package.json"));
1200
+ return pkg ? fromPackage(root, dir, pkg)?.imports ?? [] : [];
1201
+ }
1202
+ /**
1203
+ * Dependencies' package.json files, parsed, by path: read again only when one's size or mtime
1204
+ * changes, so a config loaded again (the ESLint plugin's and the MCP server's reloads) does not
1205
+ * read every dependency again.
1206
+ */
1207
+ const manifests = /* @__PURE__ */ new Map();
1208
+ function cachedManifest(file) {
1209
+ const stamp = fileStamp(file);
1210
+ const cached = manifests.get(file);
1211
+ if (cached?.stamp === stamp) return cached.json;
1212
+ const json = readJson$1(file);
1213
+ manifests.set(file, {
1214
+ stamp,
1215
+ json
1216
+ });
1217
+ return json;
1218
+ }
1219
+ /** A file's or folder's size and mtime, or `-` when it is missing. */
1220
+ function fileStamp(file) {
1221
+ const stats = fs.statSync(file, { throwIfNoEntry: false });
1222
+ return stats ? `${String(stats.mtimeMs)}:${String(stats.size)}` : "-";
1223
+ }
1099
1224
  /** `@acme/ui`, `@acme/ui-kit`, `@acme/design-system`, `acme-ui`; not `@acme/email-components`. */
1100
1225
  function isDesignSystemName(name) {
1101
1226
  const base = name.slice(name.lastIndexOf("/") + 1);
1102
1227
  return /^(?:ui|ui-kit|uikit|ui-react|react-ui|design-system|ds|components|primitives)$/.test(base) || /-(?:ui|ui-kit|design-system)$/.test(base);
1103
1228
  }
1104
1229
  /**
1105
- * Locates workspace packages by name: through `node_modules` links up to the
1106
- * workspace root, then by scanning the workspace's package globs.
1230
+ * Locates packages by name for one load: a project's config, its design-system package and
1231
+ * its detection share one. Workspace packages are found through `node_modules` links up to the
1232
+ * workspace root, then by the workspace's package globs ({@link find}); any dependency as
1233
+ * Node.js resolves it ({@link locate}).
1107
1234
  */
1108
1235
  var PackageFinder = class {
1109
1236
  root;
1110
1237
  byName;
1238
+ /** The packages the package manager links: those its workspace globs list, without Nx's. */
1239
+ linkable;
1240
+ nodeModules;
1241
+ realRoot;
1242
+ workspace;
1111
1243
  constructor(root) {
1112
1244
  this.root = root;
1113
1245
  }
@@ -1123,6 +1255,61 @@ var PackageFinder = class {
1123
1255
  this.byName ??= scanWorkspace(this.root);
1124
1256
  return this.byName.get(name);
1125
1257
  }
1258
+ /**
1259
+ * Where `name` resolves for code in the root, as Node.js finds it: in the nearest
1260
+ * `node_modules` at or above it that has it; else the workspace package of that name, in a
1261
+ * checkout that is not installed (only then is the workspace scanned). With its package.json.
1262
+ */
1263
+ locate(name) {
1264
+ for (const nodeModules of this.nodeModulesDirs()) {
1265
+ const dir = path.join(nodeModules, ...name.split("/"));
1266
+ const json = cachedManifest(path.join(dir, "package.json"));
1267
+ if (json) return {
1268
+ dir,
1269
+ json
1270
+ };
1271
+ }
1272
+ this.linkable ??= scanWorkspace(this.root, false);
1273
+ const dir = this.linkable.get(name);
1274
+ const json = dir ? cachedManifest(path.join(dir, "package.json")) : void 0;
1275
+ return dir && json ? {
1276
+ dir,
1277
+ json
1278
+ } : void 0;
1279
+ }
1280
+ /**
1281
+ * A located package at its real folder, expressed from the root as given: `installed` when
1282
+ * that is in a `node_modules`, rather than a workspace package linked there, and the
1283
+ * `boundary` its config's paths must stay in.
1284
+ */
1285
+ place(located) {
1286
+ this.realRoot ??= fs.realpathSync(this.root);
1287
+ const real = path.relative(this.realRoot, fs.realpathSync(located));
1288
+ const dir = path.resolve(this.root, real);
1289
+ const installed = /(?:^|[\\/])node_modules[\\/]/.test(real);
1290
+ return {
1291
+ dir,
1292
+ installed,
1293
+ boundary: installed ? dir : this.boundaryOf(dir)
1294
+ };
1295
+ }
1296
+ /** The `node_modules` folders at and above the root, nearest first, looked up once. */
1297
+ nodeModulesDirs() {
1298
+ if (!this.nodeModules) {
1299
+ this.nodeModules = [];
1300
+ for (let dir = path.resolve(this.root);; dir = path.dirname(dir)) {
1301
+ const candidate = path.join(dir, "node_modules");
1302
+ if (isDirectory$1(candidate)) this.nodeModules.push(candidate);
1303
+ if (dir === path.dirname(dir)) break;
1304
+ }
1305
+ }
1306
+ return this.nodeModules;
1307
+ }
1308
+ /** The workspace root when `dir` is in it, else `dir` itself. */
1309
+ boundaryOf(dir) {
1310
+ if (this.workspace === void 0) this.workspace = workspaceRootOf(this.root) ?? null;
1311
+ return this.workspace !== null && isInside(this.workspace, dir) ? this.workspace : dir;
1312
+ }
1126
1313
  };
1127
1314
  /**
1128
1315
  * A monorepo root: a pnpm-workspace.yaml, `workspaces` in package.json (npm, Yarn, Bun),
@@ -1132,28 +1319,65 @@ var PackageFinder = class {
1132
1319
  function isWorkspaceRoot(dir) {
1133
1320
  if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) return true;
1134
1321
  if (fs.existsSync(path.join(dir, "nx.json"))) return true;
1135
- const pkg = readJson(path.join(dir, "package.json"));
1322
+ const pkg = readJson$1(path.join(dir, "package.json"));
1136
1323
  if (pkg !== void 0 && pkg.workspaces !== void 0) return true;
1137
- return Array.isArray(readJson(path.join(dir, "lerna.json"))?.packages);
1324
+ return Array.isArray(readJson$1(path.join(dir, "lerna.json"))?.packages);
1138
1325
  }
1139
- /** Package name → directory for every package the workspace declares. */
1140
- function scanWorkspace(start) {
1141
- const packages = /* @__PURE__ */ new Map();
1142
- let workspaceRoot;
1326
+ /** The nearest workspace root at or above `start`. */
1327
+ function workspaceRootOf(start) {
1143
1328
  for (let dir = start;; dir = path.dirname(dir)) {
1144
- if (isWorkspaceRoot(dir)) {
1145
- workspaceRoot = dir;
1146
- break;
1147
- }
1148
- if (dir === path.dirname(dir)) break;
1329
+ if (isWorkspaceRoot(dir)) return dir;
1330
+ if (dir === path.dirname(dir)) return void 0;
1149
1331
  }
1150
- if (!workspaceRoot) return packages;
1151
- for (const dir of workspacePackageDirs(workspaceRoot)) {
1152
- const name = readJson(path.join(dir, "package.json"))?.name;
1332
+ }
1333
+ /** What a workspace root declares its packages in, and Nx's marker. */
1334
+ const WORKSPACE_MANIFESTS = [
1335
+ "pnpm-workspace.yaml",
1336
+ "package.json",
1337
+ "lerna.json",
1338
+ "nx.json"
1339
+ ];
1340
+ /**
1341
+ * Workspace maps by workspace root (with Nx's projects or not), kept for the next load in the
1342
+ * same process (a monorepo root's projects, the ESLint plugin's and the MCP server's reloads)
1343
+ * while the files they were read from keep their size and mtime: the root's manifests, each
1344
+ * package's package.json, the folders the packages are in and the folders the package globs
1345
+ * start from. A package added to one of those folders, or a manifest edited, makes the next
1346
+ * load read the workspace again.
1347
+ */
1348
+ const workspaceMaps = /* @__PURE__ */ new Map();
1349
+ /**
1350
+ * Package name → directory for every package the workspace declares, with Nx's projects
1351
+ * unless `nx` is false.
1352
+ */
1353
+ function scanWorkspace(start, nx = true) {
1354
+ const workspaceRoot = workspaceRootOf(start);
1355
+ if (!workspaceRoot) return /* @__PURE__ */ new Map();
1356
+ const key = `${workspaceRoot}\0${String(nx)}`;
1357
+ const cached = workspaceMaps.get(key);
1358
+ if (cached && stampOf(cached.watched) === cached.stamp) return cached.packages;
1359
+ const before = [...WORKSPACE_MANIFESTS.map((name) => path.join(workspaceRoot, name)), ...workspacePatterns(workspaceRoot).include.map((p) => path.resolve(workspaceRoot, globBase(`${p}/package.json`)))];
1360
+ const stamps = new Map(before.map((file) => [file, fileStamp(file)]));
1361
+ const packages = /* @__PURE__ */ new Map();
1362
+ const dirs = workspacePackageDirs(workspaceRoot, nx);
1363
+ for (const dir of dirs) {
1364
+ const manifest = path.join(dir, "package.json");
1365
+ stamps.set(manifest, fileStamp(manifest));
1366
+ if (!stamps.has(path.dirname(dir))) stamps.set(path.dirname(dir), fileStamp(path.dirname(dir)));
1367
+ const name = readJson$1(manifest)?.name;
1153
1368
  if (typeof name === "string" && !packages.has(name)) packages.set(name, dir);
1154
1369
  }
1370
+ const watched = [...stamps.keys()];
1371
+ workspaceMaps.set(key, {
1372
+ stamp: watched.map((file) => stamps.get(file)).join("|"),
1373
+ watched,
1374
+ packages
1375
+ });
1155
1376
  return packages;
1156
1377
  }
1378
+ function stampOf(files) {
1379
+ return files.map(fileStamp).join("|");
1380
+ }
1157
1381
  /**
1158
1382
  * `packages` from pnpm-workspace.yaml, `workspaces` from package.json (npm, Yarn, Bun), or
1159
1383
  * `packages` from lerna.json, in that order of precedence.
@@ -1166,10 +1390,10 @@ function workspacePatterns(dir) {
1166
1390
  });
1167
1391
  const yaml = path.join(dir, "pnpm-workspace.yaml");
1168
1392
  if (fs.existsSync(yaml)) return split(pnpmPackages(fs.readFileSync(yaml, "utf8")), "pnpm-workspace.yaml");
1169
- const workspaces = readJson(path.join(dir, "package.json"))?.workspaces;
1393
+ const workspaces = readJson$1(path.join(dir, "package.json"))?.workspaces;
1170
1394
  const list = Array.isArray(workspaces) ? workspaces : asRecord(workspaces).packages;
1171
1395
  if (Array.isArray(list)) return split(list.filter((p) => typeof p === "string"), "package.json workspaces");
1172
- const lerna = readJson(path.join(dir, "lerna.json"))?.packages;
1396
+ const lerna = readJson$1(path.join(dir, "lerna.json"))?.packages;
1173
1397
  if (Array.isArray(lerna)) return split(lerna.filter((p) => typeof p === "string"), "lerna.json");
1174
1398
  return {
1175
1399
  include: [],
@@ -1184,7 +1408,7 @@ const NOT_PACKAGES = ["**/node_modules/**", "**/.git/**"];
1184
1408
  * package globs match that hold a package.json, and with an nx.json, every folder with an
1185
1409
  * Nx project.json.
1186
1410
  */
1187
- function workspacePackageDirs(root) {
1411
+ function workspacePackageDirs(root, nx = true) {
1188
1412
  const { include, exclude } = workspacePatterns(root);
1189
1413
  const ignore = [...NOT_PACKAGES, ...exclude];
1190
1414
  const dirs = include.length ? globSync(include.map((p) => `${p}/package.json`), {
@@ -1192,7 +1416,7 @@ function workspacePackageDirs(root) {
1192
1416
  absolute: true,
1193
1417
  ignore
1194
1418
  }).map((manifest) => path.dirname(manifest)) : [];
1195
- if (fs.existsSync(path.join(root, "nx.json"))) {
1419
+ if (nx && fs.existsSync(path.join(root, "nx.json"))) {
1196
1420
  const projects = globSync("**/project.json", {
1197
1421
  cwd: root,
1198
1422
  absolute: true,
@@ -1229,7 +1453,7 @@ function pnpmPackages(text) {
1229
1453
  }
1230
1454
  return items.filter(Boolean);
1231
1455
  }
1232
- function readJson(file) {
1456
+ function readJson$1(file) {
1233
1457
  try {
1234
1458
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
1235
1459
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -1255,6 +1479,532 @@ function isDirectory$1(dir) {
1255
1479
  }
1256
1480
  }
1257
1481
  //#endregion
1482
+ //#region src/tokens/color.ts
1483
+ const toOklch$1 = converter("oklch");
1484
+ /**
1485
+ * Euclidean distance in OKLCH with hue handled as an angle, which is ΔE in
1486
+ * OKLab: perceptually uniform, so "nearest token" means nearest to the eye.
1487
+ */
1488
+ const okDistance = differenceEuclidean("oklch");
1489
+ const KEYWORDS = /* @__PURE__ */ new Set([
1490
+ "transparent",
1491
+ "currentcolor",
1492
+ "inherit",
1493
+ "initial",
1494
+ "unset",
1495
+ "revert",
1496
+ "revert-layer",
1497
+ "none"
1498
+ ]);
1499
+ /**
1500
+ * HSL channels without the function, `214.3 31.8% 91.4%` (commas, `deg` and
1501
+ * `/ alpha` allowed): how Tailwind v3 projects, and shadcn/ui before v4, store
1502
+ * colors, to be used as `hsl(var(--border))`.
1503
+ */
1504
+ const BARE_HSL = /^(-?\d*\.?\d+)(deg)?(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*\/\s*(\d*\.?\d+%?))?$/;
1505
+ function isBareHsl(value) {
1506
+ return BARE_HSL.test(value.trim());
1507
+ }
1508
+ /**
1509
+ * Parses any CSS color (hex, rgb(), hsl(), oklch(), named, ...) into OKLCH.
1510
+ * With `bareHsl`, for token values, HSL channels count too: in a stylesheet
1511
+ * they can only be a color, while in a class or style they are not one.
1512
+ */
1513
+ function parseColor(value, options = {}) {
1514
+ const text = value.trim();
1515
+ if (!text || KEYWORDS.has(text.toLowerCase()) || text.includes("var(")) return void 0;
1516
+ let parsed = parse(text);
1517
+ const hsl = !parsed && options.bareHsl ? BARE_HSL.exec(text) : null;
1518
+ if (hsl) {
1519
+ const [, h, deg = "", s, l, alpha] = hsl;
1520
+ parsed = parse(`hsl(${h}${deg} ${s}% ${l}%${alpha ? ` / ${alpha}` : ""})`);
1521
+ }
1522
+ return parsed ? toOklch$1(parsed) : void 0;
1523
+ }
1524
+ function colorDistance(a, b) {
1525
+ return okDistance(a, b);
1526
+ }
1527
+ function isNamedColor(value) {
1528
+ return Object.hasOwn(colorsNamed, value.toLowerCase());
1529
+ }
1530
+ /** Color literals inside arbitrary CSS text: hex (not `url(#id)` references) and color functions. */
1531
+ const COLOR_LITERAL = /(?<!url\(\s*["']?)#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![0-9a-zA-Z_-])|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([^()]*(?:\([^()]*\)[^()]*)*\)/g;
1532
+ /** Finds hex and functional color literals in `text` (named colors are not matched). */
1533
+ function findColorLiterals(text) {
1534
+ const matches = [];
1535
+ for (const match of text.matchAll(COLOR_LITERAL)) {
1536
+ const color = parseColor(match[0]);
1537
+ if (color) matches.push({
1538
+ text: match[0],
1539
+ index: match.index,
1540
+ color
1541
+ });
1542
+ }
1543
+ return matches;
1544
+ }
1545
+ /**
1546
+ * Chroma from which a color reads as a hue at any lightness. Tailwind's grays,
1547
+ * slate to taupe, stay below 0.046; its colors pass 0.05 by shade 200.
1548
+ */
1549
+ const TINT_CHROMA = .05;
1550
+ /**
1551
+ * Pale colors have little chroma to give, so below TINT_CHROMA a light color's
1552
+ * hue is told by its chroma relative to the most sRGB allows at that lightness
1553
+ * and hue. `#fef2f2` (red-50) has 0.013 of a possible 0.014; Tailwind's light
1554
+ * grays reach at most 0.44 of theirs (slate-100), its palest hues start at
1555
+ * 0.64 (teal-50). Near black the measure says little (slate-950 and shadcn's
1556
+ * v3 dark background would read as blue), so it applies from TINT_LIGHTNESS up.
1557
+ */
1558
+ const TINT_RELATIVE = .55;
1559
+ const TINT_LIGHTNESS = .7;
1560
+ /** Below this chroma the hue angle is rounding noise. */
1561
+ const MIN_CHROMA = .005;
1562
+ const maxChromas = /* @__PURE__ */ new Map();
1563
+ /** The highest chroma sRGB can show at this lightness and hue. */
1564
+ function maxChroma(l, h) {
1565
+ const key = `${l.toFixed(3)}:${h.toFixed(1)}`;
1566
+ let max = maxChromas.get(key);
1567
+ if (max === void 0) {
1568
+ max = clampChroma({
1569
+ mode: "oklch",
1570
+ l: Math.min(1, Math.max(0, l)),
1571
+ c: .4,
1572
+ h
1573
+ }, "oklch").c;
1574
+ maxChromas.set(key, max);
1575
+ }
1576
+ return max;
1577
+ }
1578
+ /** Whether a color reads as a hue rather than a gray, however pale or dark it is. */
1579
+ function isTinted(color) {
1580
+ if (color.c >= TINT_CHROMA) return true;
1581
+ if (color.c < MIN_CHROMA || color.h === void 0 || color.l < TINT_LIGHTNESS) return false;
1582
+ const max = maxChroma(color.l, color.h);
1583
+ return max > 0 && color.c / max >= TINT_RELATIVE;
1584
+ }
1585
+ /** The angle between two hues, 0–180°; 0 when either is a gray. */
1586
+ function hueDistance(a, b) {
1587
+ if (a.h === void 0 || b.h === void 0) return 0;
1588
+ const diff = Math.abs(a.h - b.h) % 360;
1589
+ return Math.min(diff, 360 - diff);
1590
+ }
1591
+ /**
1592
+ * Whether `token` can stand in for `color` without changing what it is: both
1593
+ * grays, or both hues within `tolerance`. ΔE alone does not say it: a pale
1594
+ * yellow is 0.07 from a light gray. `tinted` overrides the test for the source,
1595
+ * so a pale `bg-sky-50` counts as blue whatever its chroma.
1596
+ */
1597
+ function sameHue(color, token, tinted = isTinted(color), tolerance = 15) {
1598
+ if (tinted !== isTinted(token)) return false;
1599
+ return !tinted || hueDistance(color, token) <= tolerance;
1600
+ }
1601
+ /** Formats a ΔE value the way it reads best in a diagnostic. */
1602
+ function formatDeltaE(deltaE) {
1603
+ return deltaE < 5e-4 ? "exact match" : `ΔE ${deltaE.toFixed(3)}`;
1604
+ }
1605
+ //#endregion
1606
+ //#region src/libraries/mui.ts
1607
+ /** The palette groups MUI's components take as a `color` value (`<Button color="primary">`). */
1608
+ const ROLES = /* @__PURE__ */ new Set([
1609
+ "primary",
1610
+ "secondary",
1611
+ "error",
1612
+ "warning",
1613
+ "info",
1614
+ "success"
1615
+ ]);
1616
+ /** Palette keys that are settings, not colors. */
1617
+ const SETTINGS = /* @__PURE__ */ new Set([
1618
+ "mode",
1619
+ "contrastThreshold",
1620
+ "tonalOffset",
1621
+ "augmentColor",
1622
+ "getContrastText"
1623
+ ]);
1624
+ /** Functions that build an MUI theme from an options object. */
1625
+ const THEME_CALLS = /^(?:createTheme|extendTheme|experimental_extendTheme|createMuiTheme)$/;
1626
+ /**
1627
+ * MUI (`@mui/material`): components from its type declarations, and tokens from the theme the
1628
+ * app creates with `createTheme`, where its values are written out.
1629
+ */
1630
+ const mui = {
1631
+ id: "mui",
1632
+ package: "@mui/material",
1633
+ title: "MUI",
1634
+ augments: /declare\s+module\s+['"]@mui\/(?:material|system|types)(?:\/[\w./-]*)?['"]/,
1635
+ describe: describeMui,
1636
+ annotate(component) {
1637
+ if (component.name === "Tooltip") component.labelsChild = {
1638
+ prop: "title",
1639
+ unless: "describeChild"
1640
+ };
1641
+ },
1642
+ theme: {
1643
+ matches: /\b(?:createTheme|extendTheme|experimental_extendTheme|createMuiTheme)\s*\(|\bmain\s*:|\bvariants\s*:/,
1644
+ read: readThemes
1645
+ }
1646
+ };
1647
+ /** The keys of a palette color group: `teal: { light, main, dark, contrastText }`. */
1648
+ const SHADES = /^(?:light|main|dark|contrastText|lighter|darker|A?\d{2,3})$/;
1649
+ /**
1650
+ * MUI's JSDoc lists demos and API pages rather than describing the component: keep any prose,
1651
+ * then the demo titles and the API page, which also feed search.
1652
+ */
1653
+ function describeMui(jsdoc) {
1654
+ const [prose = "", rest = ""] = jsdoc.split(/^Demos:\s*$/m);
1655
+ const demos = [...(rest.split(/^API:\s*$/m)[0] ?? "").matchAll(/^- \[([^\]]+)\]\(/gm)].map((m) => m[1]);
1656
+ const api = /\[[^\]]+ API\]\((https:[^)\s]+)\)/.exec(rest)?.[1];
1657
+ const parts = [
1658
+ prose.trim(),
1659
+ demos.length ? `Demos: ${demos.join(", ")}.` : "",
1660
+ api ? `API: ${api}` : ""
1661
+ ].filter(Boolean);
1662
+ return parts.length ? parts.join("\n\n") : void 0;
1663
+ }
1664
+ /** Tests, stories, mocks and fixtures: themes there are not the app's. */
1665
+ const NOT_THE_APP = /(?:^|\/)[^/]*(?:test|spec|stor(?:y|ies)|mock|fixture)[^/]*(?:\/|$)/i;
1666
+ /**
1667
+ * Tokens from the options objects passed to `createTheme` (or `extendTheme`) in `files`:
1668
+ * palette colors (`primary.main`, `background.paper`, `divider`) with their dark values from
1669
+ * `colorSchemes.dark`, and the spacing unit, border radius and font family. Values written as
1670
+ * literals are read, through constants of the same file or imported from another of the app's
1671
+ * files by a relative path; anything computed is left out. Nothing runs. Tests, stories and
1672
+ * their helpers are skipped: a theme there is not the app's.
1673
+ */
1674
+ function readThemes(files, root) {
1675
+ const tokens = /* @__PURE__ */ new Map();
1676
+ const values = [];
1677
+ const scopes = new Scopes(root);
1678
+ for (const { file, text } of files) {
1679
+ const rel = relativePath(root, file);
1680
+ if (NOT_THE_APP.test(rel)) continue;
1681
+ const scope = scopes.of(file, text);
1682
+ if (!scope) continue;
1683
+ const visit = (node) => {
1684
+ if (ts.isCallExpression(node) && THEME_CALLS.test(calleeName(node.expression) ?? "")) {
1685
+ const [first] = node.arguments;
1686
+ const literal = object(first ? scopes.evaluate(first, scope, 0) : void 0);
1687
+ if (literal) {
1688
+ const line = scope.source.getLineAndCharacterOfPosition(node.getStart(scope.source)).line + 1;
1689
+ for (const token of themeTokens(literal, {
1690
+ file: rel,
1691
+ line
1692
+ })) if (!tokens.has(token.name)) tokens.set(token.name, token);
1693
+ }
1694
+ }
1695
+ if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.initializer)) values.push(...addedValues(node.name, node.initializer));
1696
+ ts.forEachChild(node, visit);
1697
+ };
1698
+ visit(scope.source);
1699
+ }
1700
+ return {
1701
+ tokens: [...tokens.values()],
1702
+ warnings: [],
1703
+ values,
1704
+ files: scopes.read()
1705
+ };
1706
+ }
1707
+ /**
1708
+ * Values a theme adds, whether or not its options can be read: a palette group
1709
+ * (`teal: { main: …, dark: … }`) makes `teal` a color of every component's `color`, and a
1710
+ * component's `variants: [{ props: { variant: "dashed" } }]` (under `MuiButton`) a value of
1711
+ * that prop. Over-reading only allows more; it never reports anything.
1712
+ */
1713
+ function addedValues(name, value) {
1714
+ const key = keyOf(name);
1715
+ if (key === void 0) return [];
1716
+ const keys = value.properties.map((p) => p.name ? keyOf(p.name) : void 0);
1717
+ if (keys.includes("main") && keys.every((k) => k !== void 0 && SHADES.test(k))) return [{
1718
+ prop: "color",
1719
+ value: key,
1720
+ alongside: "primary"
1721
+ }];
1722
+ const component = /^Mui([A-Z]\w*)$/.exec(key)?.[1];
1723
+ if (!component) return [];
1724
+ const found = [];
1725
+ const visit = (node) => {
1726
+ if (ts.isPropertyAssignment(node) && keyOf(node.name) === "props" && ts.isObjectLiteralExpression(node.initializer)) {
1727
+ for (const prop of node.initializer.properties) {
1728
+ if (!ts.isPropertyAssignment(prop) || !ts.isStringLiteralLike(prop.initializer)) continue;
1729
+ const propName = keyOf(prop.name);
1730
+ if (propName) found.push({
1731
+ component,
1732
+ prop: propName,
1733
+ value: prop.initializer.text
1734
+ });
1735
+ }
1736
+ return;
1737
+ }
1738
+ ts.forEachChild(node, visit);
1739
+ };
1740
+ visit(value);
1741
+ return found;
1742
+ }
1743
+ function calleeName(expression) {
1744
+ if (ts.isIdentifier(expression)) return expression.text;
1745
+ if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
1746
+ }
1747
+ const SCRIPT_EXTENSIONS = [
1748
+ ".ts",
1749
+ ".tsx",
1750
+ ".js",
1751
+ ".jsx",
1752
+ ".mjs",
1753
+ ".cjs",
1754
+ ".mts",
1755
+ ".cts"
1756
+ ];
1757
+ /** The app's files a theme reads from, each parsed once. */
1758
+ var Scopes = class {
1759
+ root;
1760
+ scopes = /* @__PURE__ */ new Map();
1761
+ constructor(root) {
1762
+ this.root = root;
1763
+ }
1764
+ /** The files read beyond those passed in: the modules a theme imports. */
1765
+ read() {
1766
+ return [...this.scopes.keys()];
1767
+ }
1768
+ of(file, text) {
1769
+ const known = this.scopes.get(file);
1770
+ if (known !== void 0) return known ?? void 0;
1771
+ let scope;
1772
+ try {
1773
+ scope = scopeOf(file, ts.createSourceFile(file, text ?? fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, scriptKind$1(file)));
1774
+ } catch {
1775
+ scope = null;
1776
+ }
1777
+ this.scopes.set(file, scope);
1778
+ return scope ?? void 0;
1779
+ }
1780
+ /** A literal value, following constants and relative imports; undefined for anything computed. */
1781
+ evaluate(node, scope, depth) {
1782
+ if (depth > 24) return void 0;
1783
+ const next = (n, s = scope) => this.evaluate(n, s, depth + 1);
1784
+ if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node)) return next(node.expression);
1785
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
1786
+ if (ts.isNumericLiteral(node)) return Number(node.text);
1787
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
1788
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
1789
+ if (ts.isIdentifier(node)) return this.binding(node.text, scope, depth);
1790
+ if (ts.isPropertyAccessExpression(node)) return member(next(node.expression), node.name.text);
1791
+ if (ts.isElementAccessExpression(node)) {
1792
+ const key = next(node.argumentExpression);
1793
+ return typeof key === "string" || typeof key === "number" ? member(next(node.expression), String(key)) : void 0;
1794
+ }
1795
+ if (ts.isArrayLiteralExpression(node)) return node.elements.map((element) => next(element));
1796
+ if (ts.isObjectLiteralExpression(node)) {
1797
+ const result = {};
1798
+ for (const property of node.properties) if (ts.isPropertyAssignment(property)) {
1799
+ const key = keyOf(property.name);
1800
+ if (key !== void 0) result[key] = next(property.initializer);
1801
+ } else if (ts.isShorthandPropertyAssignment(property)) result[property.name.text] = next(property.name);
1802
+ else if (ts.isSpreadAssignment(property)) {
1803
+ const spread = object(next(property.expression));
1804
+ if (spread) Object.assign(result, spread);
1805
+ }
1806
+ return result;
1807
+ }
1808
+ }
1809
+ /** The value of a name in a scope: one of its constants, or what it imports by that name. */
1810
+ binding(name, scope, depth) {
1811
+ const constant = scope.constants.get(name);
1812
+ if (constant) return this.evaluate(constant, scope, depth + 1);
1813
+ const imported = scope.imports.get(name);
1814
+ if (!imported) return void 0;
1815
+ const target = this.resolve(imported.specifier, scope.file);
1816
+ const module = target ? this.of(target) : void 0;
1817
+ const local = module?.exports.get(imported.name);
1818
+ return module && local ? this.binding(local, module, depth + 1) : void 0;
1819
+ }
1820
+ /** A relative import's file, among the app's own files. */
1821
+ resolve(specifier, from) {
1822
+ if (!specifier.startsWith(".")) return void 0;
1823
+ const base = path.resolve(path.dirname(from), specifier);
1824
+ if (!isInside(this.root, base) || /[\\/]node_modules[\\/]/.test(base)) return void 0;
1825
+ const stem = base.replace(/\.[cm]?js$/, "");
1826
+ for (const candidate of [
1827
+ base,
1828
+ ...SCRIPT_EXTENSIONS.map((ext) => `${stem}${ext}`),
1829
+ ...SCRIPT_EXTENSIONS.map((ext) => path.join(base, `index${ext}`))
1830
+ ]) if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
1831
+ }
1832
+ };
1833
+ function scriptKind$1(file) {
1834
+ if (/\.tsx$/.test(file)) return ts.ScriptKind.TSX;
1835
+ if (/\.jsx$/.test(file)) return ts.ScriptKind.JSX;
1836
+ if (/\.[cm]?js$/.test(file)) return ts.ScriptKind.JS;
1837
+ return ts.ScriptKind.TS;
1838
+ }
1839
+ function scopeOf(file, source) {
1840
+ const scope = {
1841
+ file,
1842
+ source,
1843
+ constants: /* @__PURE__ */ new Map(),
1844
+ imports: /* @__PURE__ */ new Map(),
1845
+ exports: /* @__PURE__ */ new Map()
1846
+ };
1847
+ for (const statement of source.statements) if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
1848
+ const specifier = statement.moduleSpecifier.text;
1849
+ const clause = statement.importClause;
1850
+ if (clause?.name) scope.imports.set(clause.name.text, {
1851
+ specifier,
1852
+ name: "default"
1853
+ });
1854
+ const bindings = clause?.namedBindings;
1855
+ if (bindings && ts.isNamedImports(bindings)) for (const element of bindings.elements) scope.imports.set(element.name.text, {
1856
+ specifier,
1857
+ name: (element.propertyName ?? element.name).text
1858
+ });
1859
+ } else if (ts.isVariableStatement(statement)) {
1860
+ const list = statement.declarationList;
1861
+ if (!(list.flags & ts.NodeFlags.Const)) continue;
1862
+ const exported = statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
1863
+ for (const declaration of list.declarations) {
1864
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
1865
+ scope.constants.set(declaration.name.text, declaration.initializer);
1866
+ if (exported) scope.exports.set(declaration.name.text, declaration.name.text);
1867
+ }
1868
+ } else if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier) {
1869
+ const clause = statement.exportClause;
1870
+ if (clause && ts.isNamedExports(clause)) for (const element of clause.elements) scope.exports.set(element.name.text, (element.propertyName ?? element.name).text);
1871
+ } else if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
1872
+ if (ts.isIdentifier(statement.expression)) scope.exports.set("default", statement.expression.text);
1873
+ else {
1874
+ scope.constants.set("default", statement.expression);
1875
+ scope.exports.set("default", "default");
1876
+ }
1877
+ }
1878
+ return scope;
1879
+ }
1880
+ /** A property of an object read so far (`neonBlue[400]`, `palette.primary`). */
1881
+ function member(value, key) {
1882
+ if (Array.isArray(value)) return /^\d+$/.test(key) ? value[Number(key)] : void 0;
1883
+ return object(value)?.[key];
1884
+ }
1885
+ function keyOf(name) {
1886
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
1887
+ }
1888
+ function object(value) {
1889
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1890
+ }
1891
+ /** The colors of a palette, by path: `primary.main`, `background.paper`, `divider`. */
1892
+ function paletteColors(palette) {
1893
+ const colors = /* @__PURE__ */ new Map();
1894
+ for (const [key, value] of Object.entries(palette ?? {})) {
1895
+ if (SETTINGS.has(key)) continue;
1896
+ if (typeof value === "string") {
1897
+ if (parseColor(value)) colors.set(key, value);
1898
+ continue;
1899
+ }
1900
+ for (const [shade, color] of Object.entries(object(value) ?? {})) if (typeof color === "string" && parseColor(color)) colors.set(`${key}.${shade}`, color);
1901
+ }
1902
+ return colors;
1903
+ }
1904
+ function themeTokens(options, source) {
1905
+ const tokens = [];
1906
+ const schemes = object(options.colorSchemes);
1907
+ const light = paletteColors(object(object(schemes?.light)?.palette) ?? object(options.palette));
1908
+ const dark = paletteColors(object(object(schemes?.dark)?.palette));
1909
+ const variables = options.cssVariables !== void 0 && options.cssVariables !== false;
1910
+ const add = (name, category, value, usage) => tokens.push({
1911
+ name,
1912
+ category,
1913
+ value,
1914
+ usage,
1915
+ source,
1916
+ description: "From the MUI theme."
1917
+ });
1918
+ for (const name of /* @__PURE__ */ new Set([...light.keys(), ...dark.keys()])) {
1919
+ const value = light.get(name) ?? dark.get(name) ?? "";
1920
+ const [group = name, shade] = name.split(".");
1921
+ const key = group === "background" ? "bgcolor" : group === "divider" ? "borderColor" : "color";
1922
+ add(name, "color", value, [
1923
+ ...ROLES.has(group) && shade === "main" ? [`color="${group}"`] : [],
1924
+ `sx={{ ${key}: '${name}' }}`,
1925
+ `theme.palette.${name}`
1926
+ ]);
1927
+ const token = tokens.at(-1);
1928
+ if (!token) continue;
1929
+ if (variables) token.cssVar = `--mui-palette-${name.replace(/\./g, "-")}`;
1930
+ const darkValue = dark.get(name);
1931
+ if (darkValue !== void 0 && light.has(name) && darkValue !== value) token.modes = { dark: darkValue };
1932
+ }
1933
+ const spacing = options.spacing;
1934
+ if (typeof spacing === "number" || typeof spacing === "string") add("spacing", "spacing", typeof spacing === "number" ? `${spacing}px` : spacing, ["theme.spacing(1)", "sx={{ p: 1 }}"]);
1935
+ const radius = object(options.shape)?.borderRadius;
1936
+ if (typeof radius === "number" || typeof radius === "string") add("shape.borderRadius", "radius", typeof radius === "number" ? `${radius}px` : radius, ["theme.shape.borderRadius", "sx={{ borderRadius: 1 }}"]);
1937
+ const family = object(options.typography)?.fontFamily;
1938
+ if (typeof family === "string") add("typography.fontFamily", "typography", family, ["theme.typography.fontFamily"]);
1939
+ for (const token of tokens) token.origin = "library";
1940
+ return tokens;
1941
+ }
1942
+ //#endregion
1943
+ //#region src/libraries/index.ts
1944
+ /** The ids of the libraries onsystem can read, for the config's `libraries` list. */
1945
+ const LIBRARY_IDS = ["mui"];
1946
+ /** The libraries onsystem can read, by package. */
1947
+ const LIBRARIES = [mui];
1948
+ function libraryAdapter(name) {
1949
+ return LIBRARIES.find((library) => library.package === name);
1950
+ }
1951
+ /**
1952
+ * The libraries `root`'s package.json depends on (dependencies, devDependencies or
1953
+ * peerDependencies), each found where Node.js would find it: in `node_modules` at the root or a
1954
+ * folder above it, as a monorepo hoists it. Nothing in the package is run. `ids` is the config's
1955
+ * `libraries`: undefined reads every library onsystem supports, false none, a list those named;
1956
+ * the dependencies it leaves out are `off`.
1957
+ */
1958
+ function detectLibraries(root, ids) {
1959
+ const pkg = readJson(path.join(root, "package.json"));
1960
+ if (!pkg) return {
1961
+ read: [],
1962
+ off: []
1963
+ };
1964
+ const dependencies = new Set([
1965
+ "dependencies",
1966
+ "devDependencies",
1967
+ "peerDependencies"
1968
+ ].flatMap((field) => {
1969
+ const value = pkg[field];
1970
+ return value && typeof value === "object" ? Object.keys(value) : [];
1971
+ }));
1972
+ const found = LIBRARIES.filter((library) => dependencies.has(library.package)).map((library) => {
1973
+ const dir = installedPackage(root, library.package);
1974
+ const version = dir ? readJson(path.join(dir, "package.json"))?.version : void 0;
1975
+ const source = {
1976
+ package: library.package,
1977
+ dir,
1978
+ version: typeof version === "string" ? version : void 0
1979
+ };
1980
+ return {
1981
+ id: library.id,
1982
+ source
1983
+ };
1984
+ });
1985
+ const wanted = (id) => ids === void 0 || ids !== false && ids.includes(id);
1986
+ return {
1987
+ read: found.filter((f) => wanted(f.id)).map((f) => f.source),
1988
+ off: found.filter((f) => !wanted(f.id)).map((f) => f.source)
1989
+ };
1990
+ }
1991
+ /** The folder `name` is installed in for code in `root`, looking up through `node_modules` folders. */
1992
+ function installedPackage(root, name) {
1993
+ for (let dir = path.resolve(root);; dir = path.dirname(dir)) {
1994
+ const candidate = path.join(dir, "node_modules", ...name.split("/"));
1995
+ if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
1996
+ if (dir === path.dirname(dir)) return void 0;
1997
+ }
1998
+ }
1999
+ function readJson(file) {
2000
+ try {
2001
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
2002
+ return value && typeof value === "object" ? value : void 0;
2003
+ } catch {
2004
+ return;
2005
+ }
2006
+ }
2007
+ //#endregion
1258
2008
  //#region src/extract/cva.ts
1259
2009
  const FACTORIES = /* @__PURE__ */ new Set(["cva", "tv"]);
1260
2010
  /** Finds every `const x = cva(base, { variants, defaultVariants })` in a file. */
@@ -1386,129 +2136,102 @@ function classText(node) {
1386
2136
  visit(node);
1387
2137
  return parts.join(" ").replace(/\s+/g, " ").trim();
1388
2138
  }
1389
- //#endregion
1390
- //#region src/tokens/color.ts
1391
- const toOklch = converter("oklch");
1392
- /**
1393
- * Euclidean distance in OKLCH with hue handled as an angle, which is ΔE in
1394
- * OKLab: perceptually uniform, so "nearest token" means nearest to the eye.
1395
- */
1396
- const okDistance = differenceEuclidean("oklch");
1397
- const KEYWORDS = /* @__PURE__ */ new Set([
1398
- "transparent",
1399
- "currentcolor",
1400
- "inherit",
1401
- "initial",
1402
- "unset",
1403
- "revert",
1404
- "revert-layer",
1405
- "none"
1406
- ]);
1407
- /**
1408
- * HSL channels without the function, `214.3 31.8% 91.4%` (commas, `deg` and
1409
- * `/ alpha` allowed): how Tailwind v3 projects, and shadcn/ui before v4, store
1410
- * colors, to be used as `hsl(var(--border))`.
1411
- */
1412
- const BARE_HSL = /^(-?\d*\.?\d+)(deg)?(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*\/\s*(\d*\.?\d+%?))?$/;
1413
- function isBareHsl(value) {
1414
- return BARE_HSL.test(value.trim());
1415
- }
1416
- /**
1417
- * Parses any CSS color (hex, rgb(), hsl(), oklch(), named, ...) into OKLCH.
1418
- * With `bareHsl`, for token values, HSL channels count too: in a stylesheet
1419
- * they can only be a color, while in a class or style they are not one.
1420
- */
1421
- function parseColor(value, options = {}) {
1422
- const text = value.trim();
1423
- if (!text || KEYWORDS.has(text.toLowerCase()) || text.includes("var(")) return void 0;
1424
- let parsed = parse(text);
1425
- const hsl = !parsed && options.bareHsl ? BARE_HSL.exec(text) : null;
1426
- if (hsl) {
1427
- const [, h, deg = "", s, l, alpha] = hsl;
1428
- parsed = parse(`hsl(${h}${deg} ${s}% ${l}%${alpha ? ` / ${alpha}` : ""})`);
1429
- }
1430
- return parsed ? toOklch(parsed) : void 0;
1431
- }
1432
- function colorDistance(a, b) {
1433
- return okDistance(a, b);
1434
- }
1435
- function isNamedColor(value) {
1436
- return Object.hasOwn(colorsNamed, value.toLowerCase());
2139
+ const LENGTH = /^(-?(?:\d+\.?\d*|\.\d+))(px|rem|em)?$/i;
2140
+ /** `"0.5rem"` → 8, `"12px"` → 12, `"4"` → 4. Returns undefined for anything else. */
2141
+ function lengthToPx(value) {
2142
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
2143
+ const match = LENGTH.exec(value.trim());
2144
+ if (!match) return void 0;
2145
+ const n = Number(match[1]);
2146
+ return (match[2] ?? "px").toLowerCase() === "px" ? n : n * 16;
1437
2147
  }
1438
- /** Color literals inside arbitrary CSS text: hex (not `url(#id)` references) and color functions. */
1439
- const COLOR_LITERAL = /(?<!url\(\s*["']?)#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![0-9a-zA-Z_-])|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([^()]*(?:\([^()]*\)[^()]*)*\)/g;
1440
- /** Finds hex and functional color literals in `text` (named colors are not matched). */
1441
- function findColorLiterals(text) {
1442
- const matches = [];
1443
- for (const match of text.matchAll(COLOR_LITERAL)) {
1444
- const color = parseColor(match[0]);
1445
- if (color) matches.push({
1446
- text: match[0],
1447
- index: match.index,
1448
- color
1449
- });
1450
- }
1451
- return matches;
2148
+ function formatPx(px) {
2149
+ return `${Number(px.toFixed(2))}px`;
1452
2150
  }
1453
2151
  /**
1454
- * Chroma from which a color reads as a hue at any lightness. Tailwind's grays,
1455
- * slate to taupe, stay below 0.046; its colors pass 0.05 by shade 200.
1456
- */
1457
- const TINT_CHROMA = .05;
1458
- /**
1459
- * Pale colors have little chroma to give, so below TINT_CHROMA a light color's
1460
- * hue is told by its chroma relative to the most sRGB allows at that lightness
1461
- * and hue. `#fef2f2` (red-50) has 0.013 of a possible 0.014; Tailwind's light
1462
- * grays reach at most 0.44 of theirs (slate-100), its palest hues start at
1463
- * 0.64 (teal-50). Near black the measure says little (slate-950 and shadcn's
1464
- * v3 dark background would read as blue), so it applies from TINT_LIGHTNESS up.
2152
+ * Evaluates a length that may use `var()` and `calc()` to px:
2153
+ * `calc(var(--radius) - 4px)` with `--radius: 0.625rem` → 6.
1465
2154
  */
1466
- const TINT_RELATIVE = .55;
1467
- const TINT_LIGHTNESS = .7;
1468
- /** Below this chroma the hue angle is rounding noise. */
1469
- const MIN_CHROMA = .005;
1470
- const maxChromas = /* @__PURE__ */ new Map();
1471
- /** The highest chroma sRGB can show at this lightness and hue. */
1472
- function maxChroma(l, h) {
1473
- const key = `${l.toFixed(3)}:${h.toFixed(1)}`;
1474
- let max = maxChromas.get(key);
1475
- if (max === void 0) {
1476
- max = clampChroma({
1477
- mode: "oklch",
1478
- l: Math.min(1, Math.max(0, l)),
1479
- c: .4,
1480
- h
1481
- }, "oklch").c;
1482
- maxChromas.set(key, max);
1483
- }
1484
- return max;
1485
- }
1486
- /** Whether a color reads as a hue rather than a gray, however pale or dark it is. */
1487
- function isTinted(color) {
1488
- if (color.c >= TINT_CHROMA) return true;
1489
- if (color.c < MIN_CHROMA || color.h === void 0 || color.l < TINT_LIGHTNESS) return false;
1490
- const max = maxChroma(color.l, color.h);
1491
- return max > 0 && color.c / max >= TINT_RELATIVE;
2155
+ function evaluateLength(value, resolve, depth = 0) {
2156
+ if (depth > 16) return void 0;
2157
+ const expanded = expandVars(value.trim(), resolve, depth);
2158
+ if (expanded === void 0) return void 0;
2159
+ const direct = lengthToPx(expanded);
2160
+ if (direct !== void 0) return direct;
2161
+ const calc = /^calc\((.*)\)$/is.exec(expanded);
2162
+ return calc?.[1] !== void 0 ? evaluateCalc(calc[1]) : void 0;
1492
2163
  }
1493
- /** The angle between two hues, 0–180°; 0 when either is a gray. */
1494
- function hueDistance(a, b) {
1495
- if (a.h === void 0 || b.h === void 0) return 0;
1496
- const diff = Math.abs(a.h - b.h) % 360;
1497
- return Math.min(diff, 360 - diff);
2164
+ function expandVars(value, resolve, depth) {
2165
+ let out = "";
2166
+ let last = 0;
2167
+ for (const match of value.matchAll(/var\(\s*(--[\w-]+)\s*(?:,\s*([^)]+))?\)/g)) {
2168
+ const [whole, name = "", fallback] = match;
2169
+ const resolved = resolve(name) ?? fallback;
2170
+ const px = resolved === void 0 ? void 0 : evaluateLength(resolved, resolve, depth + 1);
2171
+ if (px === void 0) return void 0;
2172
+ out += `${value.slice(last, match.index)}${px}px`;
2173
+ last = match.index + whole.length;
2174
+ }
2175
+ return out + value.slice(last);
1498
2176
  }
2177
+ const CALC_TOKEN = /-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)?|[()+\-*/]|calc/gi;
1499
2178
  /**
1500
- * Whether `token` can stand in for `color` without changing what it is: both
1501
- * grays, or both hues within `tolerance`. ΔE alone does not say it: a pale
1502
- * yellow is 0.07 from a light gray. `tinted` overrides the test for the source,
1503
- * so a pale `bg-sky-50` counts as blue whatever its chroma.
1504
- */
1505
- function sameHue(color, token, tinted = isTinted(color), tolerance = 15) {
1506
- if (tinted !== isTinted(token)) return false;
1507
- return !tinted || hueDistance(color, token) <= tolerance;
1508
- }
1509
- /** Formats a ΔE value the way it reads best in a diagnostic. */
1510
- function formatDeltaE(deltaE) {
1511
- return deltaE < 5e-4 ? "exact match" : `ΔE ${deltaE.toFixed(3)}`;
2179
+ * Tiny recursive-descent evaluator for `+ - * /` over px/rem/em/unitless
2180
+ * numbers. Anything else (`%`, `vw`, `ch`, functions) has no fixed px value,
2181
+ * so the whole expression is undefined.
2182
+ */
2183
+ function evaluateCalc(expression) {
2184
+ if (expression.replace(CALC_TOKEN, "").trim()) return void 0;
2185
+ const tokens = expression.match(CALC_TOKEN);
2186
+ if (!tokens) return void 0;
2187
+ let i = 0;
2188
+ const primary = () => {
2189
+ const token = tokens[i++];
2190
+ if (token === void 0) return void 0;
2191
+ if (token.toLowerCase() === "calc") return primary();
2192
+ if (token === "(") {
2193
+ const v = sum();
2194
+ i++;
2195
+ return v;
2196
+ }
2197
+ const px = lengthToPx(token);
2198
+ if (px === void 0) return void 0;
2199
+ return {
2200
+ n: px,
2201
+ length: /[a-z]$/i.test(token)
2202
+ };
2203
+ };
2204
+ const product = () => {
2205
+ let left = primary();
2206
+ while (left && (tokens[i] === "*" || tokens[i] === "/")) {
2207
+ const op = tokens[i++];
2208
+ const right = primary();
2209
+ if (!right) return void 0;
2210
+ left = op === "*" ? {
2211
+ n: left.n * right.n,
2212
+ length: left.length || right.length
2213
+ } : {
2214
+ n: left.n / right.n,
2215
+ length: left.length
2216
+ };
2217
+ }
2218
+ return left;
2219
+ };
2220
+ const sum = () => {
2221
+ let left = product();
2222
+ while (left && (tokens[i] === "+" || tokens[i] === "-")) {
2223
+ const op = tokens[i++];
2224
+ const right = product();
2225
+ if (!right) return void 0;
2226
+ left = {
2227
+ n: op === "+" ? left.n + right.n : left.n - right.n,
2228
+ length: true
2229
+ };
2230
+ }
2231
+ return left;
2232
+ };
2233
+ const result = sum();
2234
+ return result && Number.isFinite(result.n) ? result.n : void 0;
1512
2235
  }
1513
2236
  //#endregion
1514
2237
  //#region src/tokens/usage.ts
@@ -1553,7 +2276,7 @@ function utilities(namespace, key) {
1553
2276
  "gap-2",
1554
2277
  "m-1"
1555
2278
  ];
1556
- case "--radius": return [`rounded-${key}`];
2279
+ case "--radius": return [key === "DEFAULT" ? "rounded" : `rounded-${key}`];
1557
2280
  case "--font": return [`font-${key}`];
1558
2281
  case "--font-weight": return [`font-${key}`];
1559
2282
  case "--text": return [`text-${key}`];
@@ -1626,6 +2349,106 @@ function collect(object, keys, colors) {
1626
2349
  if (variable && name && !colors.has(variable)) colors.set(variable, name);
1627
2350
  }
1628
2351
  }
2352
+ /**
2353
+ * Reads `borderRadius` entries from a Tailwind v3 config without running it:
2354
+ * every `borderRadius` object in the file, then in the project modules it
2355
+ * imports, such as a shared preset. The config's own entries win.
2356
+ */
2357
+ function readTailwindRadii(file, resolve) {
2358
+ const radii = /* @__PURE__ */ new Map();
2359
+ const files = [];
2360
+ const queue = [file];
2361
+ while (queue.length && files.length < MAX_FILES) {
2362
+ const current = queue.shift() ?? "";
2363
+ if (files.includes(current)) continue;
2364
+ let text;
2365
+ try {
2366
+ text = fs.readFileSync(current, "utf8");
2367
+ } catch {
2368
+ continue;
2369
+ }
2370
+ files.push(current);
2371
+ const source = ts.createSourceFile(current, text, ts.ScriptTarget.Latest, true, scriptKind(current));
2372
+ const visit = (node) => {
2373
+ if (ts.isPropertyAssignment(node) && propertyName(node.name) === "borderRadius" && ts.isObjectLiteralExpression(node.initializer)) {
2374
+ collectRadius(node.initializer, radii);
2375
+ return;
2376
+ }
2377
+ const specifier = importedSpecifier(node);
2378
+ const target = specifier === void 0 ? void 0 : resolve(specifier, current);
2379
+ if (target && /\.[cm]?[jt]s$/.test(target)) queue.push(target);
2380
+ ts.forEachChild(node, visit);
2381
+ };
2382
+ visit(source);
2383
+ }
2384
+ return {
2385
+ radii,
2386
+ files
2387
+ };
2388
+ }
2389
+ /** `sm: "calc(var(--radius) - 4px)"` → `sm` → raw string; first seen wins. */
2390
+ function collectRadius(object, radii) {
2391
+ for (const property of object.properties) {
2392
+ if (!ts.isPropertyAssignment(property)) continue;
2393
+ const key = propertyName(property.name);
2394
+ if (key === void 0) continue;
2395
+ const value = property.initializer;
2396
+ let raw;
2397
+ if (ts.isStringLiteralLike(value)) raw = value.text;
2398
+ else if (ts.isNumericLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) raw = value.text;
2399
+ if (raw === void 0 || radii.has(key)) continue;
2400
+ radii.set(key, raw);
2401
+ }
2402
+ }
2403
+ /**
2404
+ * Gives radius tokens their Tailwind v3 class names: each config entry is
2405
+ * evaluated (its `var()` against radius token values, so
2406
+ * `calc(var(--radius) - 4px)` resolves with the theme), then radius tokens
2407
+ * whose px matches exactly take the entry key. Entries with no matching
2408
+ * token add a synthetic one, so the scale step exists as a candidate
2409
+ * (mirrors the spacing-steps synthesis); without it the literal would keep
2410
+ * matching the nearest unrelated token. Config entries win over presets.
2411
+ */
2412
+ function applyTailwindRadii(tokens, radii, configFile = "tailwind.config") {
2413
+ const byVar = /* @__PURE__ */ new Map();
2414
+ for (const token of tokens) if (token.category === "radius" && token.cssVar) byVar.set(token.cssVar, token.value);
2415
+ const resolve = (name) => byVar.get(name);
2416
+ const byPx = /* @__PURE__ */ new Map();
2417
+ for (const [key, raw] of radii) {
2418
+ const px = evaluateLength(raw, resolve);
2419
+ if (px !== void 0 && !byPx.has(px)) byPx.set(px, key);
2420
+ }
2421
+ const havePx = /* @__PURE__ */ new Set();
2422
+ for (const token of tokens) {
2423
+ if (token.category !== "radius") continue;
2424
+ const px = evaluateLength(token.value, resolve);
2425
+ if (px === void 0) continue;
2426
+ havePx.add(px);
2427
+ const key = byPx.get(px);
2428
+ if (key === void 0) continue;
2429
+ token.tailwind = key;
2430
+ token.usage = usageFor(token, "--radius");
2431
+ }
2432
+ for (const [px, key] of byPx) {
2433
+ if (havePx.has(px)) continue;
2434
+ const token = {
2435
+ name: `radius-${key}`,
2436
+ category: "radius",
2437
+ value: `${px}px`,
2438
+ tailwind: key,
2439
+ usage: usageFor({
2440
+ category: "radius",
2441
+ tailwind: key
2442
+ }, "--radius"),
2443
+ source: {
2444
+ file: configFile,
2445
+ line: 1
2446
+ }
2447
+ };
2448
+ tokens.push(token);
2449
+ havePx.add(px);
2450
+ }
2451
+ }
1629
2452
  /** The specifier of `require("x")`, `import x from "x"` or `import("x")`. */
1630
2453
  function importedSpecifier(node) {
1631
2454
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) return node.moduleSpecifier.text;
@@ -1754,15 +2577,25 @@ const tokenSourceSchema = z.union([z.string(), z.object({
1754
2577
  path: z.string().describe("File or glob."),
1755
2578
  prefix: z.string().optional().describe("DTCG only: custom property prefix, e.g. \"acme\" for --acme-color-primary.")
1756
2579
  }).strict()]);
2580
+ /** `false`, or a list of library ids; an unknown id is named with the closest known one. */
2581
+ const librariesSchema = z.union([z.literal(false), z.array(z.enum(LIBRARY_IDS))], { error: (issue) => {
2582
+ const unknown = Array.isArray(issue.input) ? issue.input.find((id) => !LIBRARY_IDS.includes(id)) : void 0;
2583
+ if (typeof unknown === "string") {
2584
+ const guess = closest(unknown, LIBRARY_IDS, .5);
2585
+ return `Unknown library "${unknown}". ${guess ? `Did you mean "${guess}"?` : `Expected one of: ${LIBRARY_IDS.join(", ")}.`}`;
2586
+ }
2587
+ return `"libraries" is false, to read no library, or a list of the ones to read: ${JSON.stringify(LIBRARY_IDS)}.`;
2588
+ } });
1757
2589
  const configShape = {
1758
2590
  $schema: z.string().optional(),
1759
2591
  components: globsSchema.optional().describe("Globs for component source files, relative to the root."),
1760
2592
  exclude: z.array(z.string()).optional().describe("Globs to ignore when finding components and when `check` expands folders and globs (stories, tests). Replaces the defaults; node_modules is skipped either way unless a pattern names it."),
1761
- tokens: z.union([tokenSourceSchema, z.array(tokenSourceSchema)]).optional().describe("Token files: W3C DTCG JSON (*.json) or CSS custom properties (*.css)."),
2593
+ tokens: z.union([tokenSourceSchema, z.array(tokenSourceSchema)]).optional().describe("Token files: W3C DTCG JSON (*.json), CSS custom properties (*.css) or a DESIGN.md's front matter (*.md)."),
1762
2594
  docs: globsSchema.optional().describe("Globs for per-component Markdown/MDX docs."),
1763
2595
  tsconfig: z.string().optional().describe("tsconfig used to resolve types and path aliases."),
1764
2596
  importPath: z.string().optional().describe("Package name components are imported from, e.g. \"@acme/ui\", or a pattern for packages imported by path: \"@acme/ui/{path}\" is @acme/ui/primitives/button for primitives/button.tsx in the package. Default: inferred from package exports and tsconfig paths."),
1765
2597
  elements: z.record(z.string(), z.string()).optional().describe("Extra native element → component mappings, e.g. { \"a\": \"Link\" }."),
2598
+ libraries: librariesSchema.optional().describe(`Published component libraries to read from their type declarations when the project depends on them. Omitted: every one onsystem reads (${LIBRARY_IDS.join(", ")}). false: none. A list: only those, e.g. ["mui"].`),
1766
2599
  includeDesignSystem: z.boolean().optional().describe("check: lint the design system's own component files too. Off by default: they implement the scale and primitives the rules enforce."),
1767
2600
  rules: rulesSchema.optional().describe("Rule severities and options: \"off\" | \"warn\" | \"error\" | [severity, { allow }]."),
1768
2601
  overrides: z.array(overrideSchema).optional().describe("Rules for some files: [{ \"files\": [\"app/legacy/**\"], \"rules\": { \"no-hardcoded-color\": \"warn\" } }]. Later entries win.")
@@ -1805,6 +2638,11 @@ const DEFAULT_TOKENS = [
1805
2638
  "app/app.css",
1806
2639
  "**/*.tokens.json"
1807
2640
  ];
2641
+ /**
2642
+ * Zero config reads these only when none of the token files above exist: the tokens in a
2643
+ * DESIGN.md's front matter, which describes a design system to agents.
2644
+ */
2645
+ const FALLBACK_TOKENS = ["DESIGN.md"];
1808
2646
  const DEFAULT_DOCS = [
1809
2647
  "docs/components/**/*.{md,mdx}",
1810
2648
  "components/ui/**/*.{md,mdx}",
@@ -1824,18 +2662,35 @@ var ConfigError = class extends Error {
1824
2662
  name = "ConfigError";
1825
2663
  };
1826
2664
  async function loadConfig(options = {}) {
2665
+ return loadConfigWith(options);
2666
+ }
2667
+ /** {@link loadConfig}, with what the caller has already found in the folder searched. */
2668
+ async function loadConfigWith(options, context = {}) {
1827
2669
  const cwd = path.resolve(options.cwd ?? process.cwd());
1828
2670
  const searchDir = options.root ? path.resolve(cwd, options.root) : cwd;
1829
2671
  const deprecations = [];
2672
+ const warnings = [];
2673
+ let packages = context.packages?.root === searchDir ? context.packages : void 0;
2674
+ const finder = (dir) => packages?.root === dir ? packages : packages = new PackageFinder(dir);
2675
+ let lookup = context.lookup;
1830
2676
  let configFile;
2677
+ /** The design-system package whose config this project takes on, having none of its own. */
2678
+ let declared;
1831
2679
  if (options.config) configFile = path.resolve(cwd, options.config);
1832
2680
  else {
1833
2681
  const find = (names) => names.map((name) => path.join(searchDir, name)).find((file) => fs.existsSync(file));
1834
2682
  configFile = find(CONFIG_FILES);
1835
2683
  const legacy = find(LEGACY_CONFIG_FILES);
1836
2684
  if (legacy) deprecations.push(legacyConfigNotice(legacy, configFile));
1837
- configFile ??= legacy;
2685
+ configFile ??= legacy ?? ownDeclaredConfig(searchDir);
2686
+ if (!configFile) {
2687
+ lookup ??= findDesignSystemPackage(searchDir, finder(searchDir));
2688
+ if (lookup?.problems.length) throw declarationError(lookup, searchDir);
2689
+ declared = lookup?.found;
2690
+ configFile = declared?.config;
2691
+ }
1838
2692
  }
2693
+ for (const { name, problem } of lookup?.skipped ?? []) warnings.push(`${name}'s onsystem config was skipped: ${problem} ${name} is installed in node_modules, so this project is read as if it declared none.`);
1839
2694
  let raw = {};
1840
2695
  if (configFile) {
1841
2696
  if (!fs.existsSync(configFile)) throw new ConfigError(`Config file not found: ${configFile}`);
@@ -1845,16 +2700,27 @@ async function loadConfig(options = {}) {
1845
2700
  const parsed = configSchema.safeParse(raw);
1846
2701
  if (!parsed.success) throw new ConfigError(`Invalid config in ${configFile ?? "(defaults)"}:\n${z.prettifyError(parsed.error)}`);
1847
2702
  const config = parsed.data;
1848
- const root = options.root ? path.resolve(cwd, options.root) : configFile ? path.dirname(configFile) : cwd;
2703
+ const root = options.root ? path.resolve(cwd, options.root) : options.config && configFile ? path.dirname(configFile) : searchDir;
1849
2704
  if (!isDirectory(root)) throw new ConfigError(`Project root not found: ${root}`);
1850
- const base = options.base ? path.resolve(cwd, options.base) : root;
2705
+ const base = declared ? declared.dir : options.base ? path.resolve(cwd, options.base) : root;
1851
2706
  const inherited = base !== root;
1852
2707
  const fromBase = (p) => inherited ? toPosix(path.relative(root, path.resolve(base, slashGlob(p)))) || "." : p;
1853
2708
  const toProject = toPosix(path.relative(base, root));
1854
2709
  const excludeFromBase = (e) => {
1855
2710
  if (!inherited || e.startsWith("**/")) return e;
2711
+ if (declared) return fromBase(e);
1856
2712
  return e.startsWith(`${toProject}/`) ? e.slice(toProject.length + 1) : void 0;
1857
2713
  };
2714
+ const outside = [];
2715
+ const within = (field) => (p) => {
2716
+ if (!declared || staysIn(declared.dir, declared.boundary, slashGlob(p))) return true;
2717
+ outside.push(`${field} ${p}`);
2718
+ return false;
2719
+ };
2720
+ const configComponents = config.components === void 0 ? void 0 : toArray(config.components).filter(within("components"));
2721
+ const configTokens = config.tokens === void 0 ? void 0 : toArray(config.tokens).filter((t) => within("tokens")(typeof t === "string" ? t : t.path));
2722
+ const configDocs = config.docs === void 0 ? void 0 : toArray(config.docs).filter(within("docs"));
2723
+ if (declared && outside.length) warnings.push(`${declared.name}'s onsystem config names paths outside ${declared.installed ? "the package" : "the workspace"}, which were ignored: ${outside.join(", ")}.`);
1858
2724
  const rules = {};
1859
2725
  for (const id of RULE_IDS) {
1860
2726
  const setting = config.rules?.[id];
@@ -1863,18 +2729,18 @@ async function loadConfig(options = {}) {
1863
2729
  options: {}
1864
2730
  } : resolveRule(setting);
1865
2731
  }
1866
- const overrides = (config.overrides ?? []).map((override) => ({
2732
+ const overrides = (declared ? [] : config.overrides ?? []).map((override) => ({
1867
2733
  files: toArray(override.files).map(slashGlob),
1868
2734
  rules: Object.fromEntries(Object.entries(override.rules).map(([id, setting]) => [id, resolveRule(setting)]))
1869
2735
  }));
1870
- const tsconfig = config.tsconfig === void 0 ? void 0 : fromBase(config.tsconfig.replaceAll("\\", "/"));
2736
+ const tsconfig = config.tsconfig === void 0 || declared ? void 0 : fromBase(config.tsconfig.replaceAll("\\", "/"));
1871
2737
  if (tsconfig !== void 0 && !fs.existsSync(path.resolve(root, tsconfig))) throw new ConfigError(`tsconfig not found: ${path.resolve(root, tsconfig)} (from "tsconfig" in ${configFile ?? "the config"})`);
1872
2738
  const componentsSet = Boolean(options.components?.length) || config.components !== void 0;
1873
- const detection = componentsSet ? void 0 : detectProject(root, tsconfig);
2739
+ const detection = componentsSet ? void 0 : detectProject(root, tsconfig, void 0, declared, finder(root));
1874
2740
  const explicitTokens = options.tokens?.length ? {
1875
2741
  paths: options.tokens,
1876
2742
  from: "--tokens"
1877
- } : config.tokens !== void 0 ? {
2743
+ } : config.tokens !== void 0 && !declared ? {
1878
2744
  paths: toArray(config.tokens).map((t) => fromBase(typeof t === "string" ? t : t.path)),
1879
2745
  from: `"tokens" in ${configFile ?? "the config"}`
1880
2746
  } : void 0;
@@ -1882,35 +2748,77 @@ async function loadConfig(options = {}) {
1882
2748
  const absolute = path.resolve(root, slashGlob(file));
1883
2749
  if (!/[*?[\]{}()!]/.test(file) && !fs.existsSync(absolute)) throw new ConfigError(`Token file not found: ${absolute} (from ${explicitTokens?.from})`);
1884
2750
  }
1885
- const tokenSources = options.tokens?.length ? options.tokens.map((p) => ({ path: p })) : config.tokens !== void 0 ? toArray(config.tokens).map((t) => typeof t === "string" ? { path: fromBase(t) } : {
2751
+ const libraries = detectLibraries(root, config.libraries);
2752
+ const tokenSources = options.tokens?.length ? options.tokens.map((p) => ({ path: p })) : configTokens !== void 0 ? configTokens.map((t) => typeof t === "string" ? { path: fromBase(t) } : {
1886
2753
  ...t,
1887
2754
  path: fromBase(t.path)
1888
- }) : (detection?.tokens.length ? [...detection.tokens, ...DEFAULT_TOKENS.filter((t) => t.endsWith(".json"))] : DEFAULT_TOKENS).map((p) => ({ path: p }));
2755
+ }) : [...(detection?.tokens.length ? [...detection.tokens, ...DEFAULT_TOKENS.filter((t) => t.endsWith(".json"))] : DEFAULT_TOKENS).map((p) => ({ path: p })), ...FALLBACK_TOKENS.map((p) => ({
2756
+ path: p,
2757
+ fallback: true
2758
+ }))];
2759
+ const exclude = (config.exclude ?? DEFAULT_EXCLUDE).map(slashGlob).map(excludeFromBase).filter((e) => e !== void 0);
1889
2760
  return {
1890
2761
  root,
1891
2762
  configFile,
1892
- components: (options.components?.length ? options.components : config.components !== void 0 ? toArray(config.components) : detection ? [...detection.withDefaults ? DEFAULT_COMPONENTS : [], ...detection.components] : DEFAULT_COMPONENTS).map(slashGlob),
1893
- exclude: (config.exclude ?? DEFAULT_EXCLUDE).map(slashGlob).map(excludeFromBase).filter((e) => e !== void 0),
2763
+ components: (options.components?.length ? options.components : configComponents !== void 0 ? configComponents.map(fromBase) : detection ? [...detection.withDefaults ? DEFAULT_COMPONENTS : [], ...detection.components] : DEFAULT_COMPONENTS).map(slashGlob),
2764
+ exclude: declared && config.exclude ? [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDE, ...exclude])] : exclude,
2765
+ ...declared && config.exclude ? { designSystemExclude: exclude } : {},
1894
2766
  tokens: tokenSources.map((t) => ({
1895
2767
  ...t,
1896
2768
  path: slashGlob(t.path)
1897
2769
  })),
1898
- docs: (options.docs?.length ? options.docs : config.docs === void 0 ? [...DEFAULT_DOCS, ...detection?.docs ?? []] : toArray(config.docs).map(fromBase)).map(slashGlob),
2770
+ docs: (options.docs?.length ? options.docs : configDocs === void 0 ? [.../* @__PURE__ */ new Set([...DEFAULT_DOCS, ...detection?.docs ?? []])] : configDocs.map(fromBase)).map(slashGlob),
1899
2771
  tsconfig,
1900
2772
  importPath: config.importPath,
1901
- imports: detection?.imports ?? [],
2773
+ imports: detection?.imports ?? (declared && componentsSet ? packageImports(root, declared.dir) : []),
1902
2774
  detected: detection?.source,
1903
2775
  ...detection?.designSystems ? { designSystems: detection.designSystems } : {},
1904
2776
  tailwindConfig: findTailwindConfig(root),
2777
+ libraries: libraries.read,
2778
+ libraryIds: config.libraries,
2779
+ librariesOff: libraries.off,
1905
2780
  elements: config.elements ?? {},
1906
- includeDesignSystem: options.includeDesignSystem ?? config.includeDesignSystem ?? false,
2781
+ includeDesignSystem: options.includeDesignSystem ?? (declared ? void 0 : config.includeDesignSystem) ?? false,
1907
2782
  rules,
1908
2783
  overrides,
1909
- ...inherited ? { base } : {},
2784
+ ...inherited && !declared ? { base } : {},
2785
+ ...declared ? { designSystemPackage: declared } : {},
1910
2786
  configured: componentsSet,
1911
- ...deprecations.length ? { deprecations } : {}
2787
+ ...deprecations.length ? { deprecations } : {},
2788
+ ...warnings.length ? { warnings } : {}
1912
2789
  };
1913
2790
  }
2791
+ /**
2792
+ * Whether `dir` has a config of its own: a config file under its current or old name, or one its
2793
+ * package.json names in its `onsystem` field. Without one, a project takes on its design-system
2794
+ * package's config, else the workspace root's.
2795
+ */
2796
+ function hasOwnConfig(dir) {
2797
+ if ([...CONFIG_FILES, ...LEGACY_CONFIG_FILES].some((name) => fs.existsSync(path.join(dir, name)))) return true;
2798
+ return declaredConfig(dir) !== void 0;
2799
+ }
2800
+ /** The config `dir`'s own package.json names in its `onsystem` field, as a design-system package does. */
2801
+ function ownDeclaredConfig(dir) {
2802
+ const declared = declaredConfig(dir);
2803
+ if (declared && "problem" in declared) throw new ConfigError(declared.problem);
2804
+ return declared?.file;
2805
+ }
2806
+ /** The error for workspace packages whose declaration cannot be read: their team can fix them. */
2807
+ function declarationError(lookup, dir) {
2808
+ return new ConfigError(`${lookup.problems.map((p) => `${p.name}: ${p.problem}`).join("\n")}\nFix the "${PACKAGE_FIELD}" field in that package, or give ${dir} a config of its own (onsystem.config.json), which is read instead.`);
2809
+ }
2810
+ /**
2811
+ * Whether a path or glob in a dependency's config, relative to its folder `dir`, stays in
2812
+ * `boundary`: its static part resolves inside it, and no `..` hides among its wildcards and
2813
+ * braces (`{src,../..}/**`), which could reach anywhere.
2814
+ */
2815
+ function staysIn(dir, boundary, pattern) {
2816
+ const base = globBase(pattern);
2817
+ const parts = pattern.split("/");
2818
+ const fixed = base === "." ? parts[0] === "." ? 1 : 0 : base.split("/").length;
2819
+ if (parts.slice(fixed).some((part) => part.includes(".."))) return false;
2820
+ return isInside(boundary, path.resolve(dir, base));
2821
+ }
1914
2822
  /** What to say about a config file under its old name, with or without a new one next to it. */
1915
2823
  function legacyConfigNotice(legacy, current) {
1916
2824
  const name = path.basename(legacy);
@@ -2016,7 +2924,7 @@ function extractComponents(options) {
2016
2924
  if (seen.has(c.declaration)) return false;
2017
2925
  seen.add(c.declaration);
2018
2926
  return true;
2019
- }).map((candidate) => buildComponent(context, candidate));
2927
+ }).map((candidate) => buildComponent$1(context, candidate));
2020
2928
  linkComposition(fileComponents);
2021
2929
  components.push(...fileComponents);
2022
2930
  }
@@ -2202,7 +3110,7 @@ function declarationName(declaration) {
2202
3110
  if ((ts.isFunctionDeclaration(declaration) || ts.isClassDeclaration(declaration)) && declaration.name) return declaration.name.text;
2203
3111
  if (ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name)) return declaration.name.text;
2204
3112
  }
2205
- function buildComponent(context, candidate) {
3113
+ function buildComponent$1(context, candidate) {
2206
3114
  const { checker, sourceFile } = context;
2207
3115
  const param = candidate.fn?.parameters[0];
2208
3116
  const typeNode = candidate.propsTypeNode ?? param?.type;
@@ -2382,11 +3290,11 @@ function extractProps(context, candidate, typeNode, param) {
2382
3290
  }
2383
3291
  const byOrigin = /* @__PURE__ */ new Map();
2384
3292
  for (const [name, { origin }] of own) byOrigin.set(origin, [...byOrigin.get(origin) ?? [], name]);
2385
- const summarised = new Set([...byOrigin].filter(([origin, names]) => origin === "react" || origin !== "own" && names.length > MAX_LISTED_INHERITED).map(([origin]) => origin));
3293
+ const summarised = new Set([...byOrigin].filter(([origin, names]) => origin === "react" || origin !== "own" && names.length > MAX_LISTED_INHERITED$1).map(([origin]) => origin));
2386
3294
  const props = [];
2387
3295
  for (const [name, { symbol, requiredIn, origin }] of own) {
2388
3296
  if (summarised.has(origin)) continue;
2389
- props.push(describeProp(context, name, symbol, location, requiredIn === types.length, origin));
3297
+ props.push(describeProp$1(context, name, symbol, location, requiredIn === types.length, origin));
2390
3298
  }
2391
3299
  const inherits = [];
2392
3300
  for (const origin of summarised) {
@@ -2410,7 +3318,7 @@ function extractProps(context, candidate, typeNode, param) {
2410
3318
  };
2411
3319
  }
2412
3320
  /** Packages contributing more props than this are summarised instead of listed. */
2413
- const MAX_LISTED_INHERITED = 40;
3321
+ const MAX_LISTED_INHERITED$1 = 40;
2414
3322
  /** Flattens intersections and local aliases into the types they are built from. */
2415
3323
  function constituents(context, typeNode, depth = 0) {
2416
3324
  const { checker } = context;
@@ -2466,7 +3374,7 @@ function originOf(symbol) {
2466
3374
  if (pkg === "@types/react" || pkg === "typescript") return "react";
2467
3375
  return pkg;
2468
3376
  }
2469
- function describeProp(context, name, symbol, location, required, origin) {
3377
+ function describeProp$1(context, name, symbol, location, required, origin) {
2470
3378
  const { checker } = context;
2471
3379
  const declared = checker.getTypeOfSymbolAtLocation(symbol, location);
2472
3380
  const type = checker.getNonNullableType(declared);
@@ -2476,7 +3384,7 @@ function describeProp(context, name, symbol, location, required, origin) {
2476
3384
  required,
2477
3385
  kind: "prop"
2478
3386
  };
2479
- const values = literalValues$1(type);
3387
+ const values = literalValues$2(type);
2480
3388
  if (values) prop.values = values;
2481
3389
  const description = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
2482
3390
  if (description) prop.description = description;
@@ -2499,7 +3407,7 @@ function unwrapParens(text) {
2499
3407
  }
2500
3408
  return text.slice(1, -1);
2501
3409
  }
2502
- function literalValues$1(type) {
3410
+ function literalValues$2(type) {
2503
3411
  const members = type.isUnion() ? type.types : [type];
2504
3412
  if (!members.every((t) => t.isStringLiteral())) return void 0;
2505
3413
  return members.map((t) => t.value);
@@ -2639,10 +3547,10 @@ function inferElement(context, candidate, typeNode) {
2639
3547
  const ref = candidate.refTypeNode?.getText();
2640
3548
  const refElement = ref ? /^HTML(\w+)Element$/.exec(ref)?.[1] : void 0;
2641
3549
  if (refElement) return ELEMENT_INTERFACES[refElement];
2642
- return candidate.fn ? renderedElement(candidate.fn) : void 0;
3550
+ return candidate.fn ? renderedElement$1(candidate.fn) : void 0;
2643
3551
  }
2644
3552
  /** The intrinsic element returned by the render function, following `const Comp = asChild ? Slot : "button"`. */
2645
- function renderedElement(fn) {
3553
+ function renderedElement$1(fn) {
2646
3554
  let found;
2647
3555
  const stringAssignments = /* @__PURE__ */ new Map();
2648
3556
  const visit = (node) => {
@@ -2927,10 +3835,491 @@ function unquote(value) {
2927
3835
  function stringValue(value) {
2928
3836
  return typeof value === "string" && value ? value : void 0;
2929
3837
  }
2930
- function dedent(code) {
2931
- const lines = code.replace(/^\n+|\s+$/g, "").split("\n");
2932
- const indent = Math.min(...lines.filter((l) => l.trim()).map((l) => /^\s*/.exec(l)?.[0].length ?? 0));
2933
- return lines.map((l) => l.slice(Number.isFinite(indent) ? indent : 0)).join("\n");
3838
+ function dedent(code) {
3839
+ const lines = code.replace(/^\n+|\s+$/g, "").split("\n");
3840
+ const indent = Math.min(...lines.filter((l) => l.trim()).map((l) => /^\s*/.exec(l)?.[0].length ?? 0));
3841
+ return lines.map((l) => l.slice(Number.isFinite(indent) ? indent : 0)).join("\n");
3842
+ }
3843
+ //#endregion
3844
+ //#region src/libraries/declarations.ts
3845
+ /** Packages contributing more props than this are summarised, as in source extraction. */
3846
+ const MAX_LISTED_INHERITED = 40;
3847
+ /**
3848
+ * Reads a published library's components from its type declarations with the TypeScript
3849
+ * checker: every value its entry exports under a capitalised name whose type takes props
3850
+ * (function and class components, `forwardRef` and MUI's `OverridableComponent`), with the
3851
+ * props of each call signature, their string-literal values (custom ones the app declares by
3852
+ * module augmentation included), JSDoc, the element it renders, and the module that
3853
+ * default-exports it. A signature generic over the element to render (MUI's `component`) marks
3854
+ * that prop as polymorphic instead of adding its props.
3855
+ */
3856
+ function extractLibrary(options) {
3857
+ const { root, adapter, dir } = options;
3858
+ const warnings = [];
3859
+ const empty = (warning) => ({
3860
+ components: [],
3861
+ propSets: {},
3862
+ exports: [],
3863
+ warnings: [warning],
3864
+ dependencies: []
3865
+ });
3866
+ let real;
3867
+ try {
3868
+ real = fs.realpathSync.native(dir);
3869
+ } catch {
3870
+ return empty(`${adapter.package}: ${dir} could not be read`);
3871
+ }
3872
+ const resolve = (specifier) => ts.resolveModuleName(specifier, path.join(root, "__onsystem__.tsx"), options.project.options, ts.sys).resolvedModule?.resolvedFileName;
3873
+ const entry = resolve(adapter.package) ?? path.join(real, typesEntry(real));
3874
+ if (!fs.existsSync(entry) || !/\.d\.[cm]?ts$/.test(entry)) return empty(`${adapter.package}: no type declarations at ${relativePath(root, entry)}`);
3875
+ const program = ts.createProgram({
3876
+ rootNames: [entry, ...options.augmentations],
3877
+ options: options.project.options
3878
+ });
3879
+ const checker = program.getTypeChecker();
3880
+ const entryFile = program.getSourceFile(entry);
3881
+ const moduleSymbol = entryFile ? checker.getSymbolAtLocation(entryFile) : void 0;
3882
+ if (!moduleSymbol) return empty(`${adapter.package}: ${entry} has no exports`);
3883
+ const shown = (file) => {
3884
+ const absolute = path.resolve(file);
3885
+ return relativePath(root, isInside(real, absolute) ? path.join(dir, path.relative(real, absolute)) : absolute);
3886
+ };
3887
+ const context = {
3888
+ checker,
3889
+ adapter,
3890
+ propSets: /* @__PURE__ */ new Map(),
3891
+ scope: adapter.package.startsWith("@") ? `${adapter.package.split("/")[0] ?? ""}/` : adapter.package
3892
+ };
3893
+ const exportNames = /* @__PURE__ */ new Set();
3894
+ const bySymbol = /* @__PURE__ */ new Map();
3895
+ const components = [];
3896
+ for (const exported of checker.getExportsOfModule(moduleSymbol)) {
3897
+ const name = exported.getName();
3898
+ const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
3899
+ if (!(symbol.flags & ts.SymbolFlags.Value)) continue;
3900
+ exportNames.add(name);
3901
+ if (!/^[A-Z]/.test(name)) continue;
3902
+ const known = bySymbol.get(symbol);
3903
+ if (known) {
3904
+ known.aliases.push(name);
3905
+ continue;
3906
+ }
3907
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
3908
+ if (!declaration) continue;
3909
+ const component = buildComponent(context, name, symbol);
3910
+ if (!component) continue;
3911
+ const file = declaration.getSourceFile();
3912
+ const line = file.getLineAndCharacterOfPosition(declaration.getStart(file)).line + 1;
3913
+ component.source = {
3914
+ file: shown(file.fileName),
3915
+ line
3916
+ };
3917
+ const defaultImport = defaultModule(program, checker, resolve, adapter.package, file, symbol);
3918
+ if (defaultImport) component.defaultImport = defaultImport;
3919
+ bySymbol.set(symbol, component);
3920
+ components.push(component);
3921
+ }
3922
+ linkSlots(components);
3923
+ return {
3924
+ components,
3925
+ propSets: Object.fromEntries(context.propSets),
3926
+ exports: [...exportNames].sort(),
3927
+ warnings,
3928
+ dependencies: unique(projectFiles(program))
3929
+ };
3930
+ }
3931
+ /** The declaration entry of an installed package: its `types`, else `index.d.ts`. */
3932
+ function typesEntry(dir) {
3933
+ try {
3934
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
3935
+ const types = typeof pkg.types === "string" ? pkg.types : pkg.typings;
3936
+ if (typeof types === "string" && types.endsWith(".d.ts")) return types;
3937
+ } catch {}
3938
+ return "index.d.ts";
3939
+ }
3940
+ /** Call and construct signatures that take props, across a union (`React.ComponentType`). */
3941
+ function propSignatures(type) {
3942
+ return (type.isUnion() ? type.types : [type]).flatMap((member) => [...member.getCallSignatures(), ...member.getConstructSignatures()]).filter((signature) => signature.getParameters().length >= 1);
3943
+ }
3944
+ /** A React context (`ButtonGroupContext`): callable in React 19's types, but not a component to offer. */
3945
+ function isContext(checker, type) {
3946
+ return checker.getPropertyOfType(type, "Provider") !== void 0 && checker.getPropertyOfType(type, "Consumer") !== void 0;
3947
+ }
3948
+ /**
3949
+ * The prop a generic signature renders in the component's place (`component: RootComponent`
3950
+ * where `RootComponent extends React.ElementType`), which then takes that element's props.
3951
+ */
3952
+ function polymorphicProp(checker, signature) {
3953
+ const parameters = signature.getTypeParameters();
3954
+ if (!parameters?.length) return void 0;
3955
+ const props = signature.getParameters()[0];
3956
+ if (!props) return void 0;
3957
+ for (const prop of checker.getPropertiesOfType(checker.getTypeOfSymbol(props))) {
3958
+ const type = checker.getNonNullableType(checker.getTypeOfSymbol(prop));
3959
+ if (!(type.flags & ts.TypeFlags.TypeParameter)) continue;
3960
+ const declaration = type.symbol.declarations?.find(ts.isTypeParameterDeclaration);
3961
+ if (parameters.includes(type) && declaration?.constraint && /\bElementType\b/.test(declaration.constraint.getText())) return {
3962
+ name: prop.getName(),
3963
+ symbol: prop
3964
+ };
3965
+ }
3966
+ }
3967
+ function buildComponent(context, name, symbol) {
3968
+ const { checker, adapter } = context;
3969
+ const type = checker.getTypeOfSymbol(symbol);
3970
+ if (isContext(checker, type)) return void 0;
3971
+ const signatures = propSignatures(type);
3972
+ if (!signatures.length) return void 0;
3973
+ let polymorphic;
3974
+ const plain = [];
3975
+ for (const signature of signatures) {
3976
+ const prop = polymorphicProp(checker, signature);
3977
+ if (prop) polymorphic ??= prop;
3978
+ else plain.push(signature);
3979
+ }
3980
+ const used = plain.length ? plain : signatures;
3981
+ const entries = /* @__PURE__ */ new Map();
3982
+ let members = 0;
3983
+ let openProps = false;
3984
+ for (const signature of used) {
3985
+ const param = signature.getParameters()[0];
3986
+ if (!param) continue;
3987
+ const propsType = checker.getTypeOfSymbol(param);
3988
+ if (propsType.flags & ts.TypeFlags.Any) {
3989
+ openProps = true;
3990
+ continue;
3991
+ }
3992
+ for (const part of propsType.isUnion() ? propsType.types : [propsType]) {
3993
+ members++;
3994
+ if (checker.getIndexInfosOfType(part).length) openProps = true;
3995
+ for (const prop of checker.getPropertiesOfType(part)) {
3996
+ const propName = prop.getName();
3997
+ const entry = entries.get(propName) ?? {
3998
+ symbols: [],
3999
+ required: 0,
4000
+ origin: originOf(prop)
4001
+ };
4002
+ entry.symbols.push(prop);
4003
+ if (!(prop.flags & ts.SymbolFlags.Optional)) entry.required++;
4004
+ entries.set(propName, entry);
4005
+ }
4006
+ }
4007
+ }
4008
+ if (!members && !openProps) return void 0;
4009
+ if (polymorphic && !entries.has(polymorphic.name)) entries.set(polymorphic.name, {
4010
+ symbols: [polymorphic.symbol],
4011
+ required: 0,
4012
+ origin: originOf(polymorphic.symbol)
4013
+ });
4014
+ const own = (origin) => origin === adapter.package || origin.startsWith(context.scope) || origin === "own";
4015
+ const byOrigin = /* @__PURE__ */ new Map();
4016
+ for (const [propName, { origin }] of entries) byOrigin.set(origin, [...byOrigin.get(origin) ?? [], propName]);
4017
+ const summarised = new Set([...byOrigin].filter(([origin, names]) => !own(origin) && (origin === "react" || names.length > MAX_LISTED_INHERITED)).map(([origin]) => origin));
4018
+ const element = renderedElement(checker, used.at(-1));
4019
+ const props = [];
4020
+ const variants = [];
4021
+ for (const [propName, entry] of entries) {
4022
+ if (summarised.has(entry.origin)) continue;
4023
+ const prop = describeProp(checker, propName, entry, entry.required === members);
4024
+ if (propName === polymorphic?.name) prop.type = "React.ElementType";
4025
+ if (!own(entry.origin)) prop.description = [prop.description, `From ${entry.origin}.`].filter(Boolean).join(" ");
4026
+ if (prop.values && prop.values.length > 1) {
4027
+ prop.kind = "variant";
4028
+ const fallback = prop.default?.replace(/^(['"])(.*)\1$/, "$2");
4029
+ variants.push({
4030
+ name: propName,
4031
+ values: [...prop.values],
4032
+ ...fallback !== void 0 && prop.values.includes(fallback) ? { default: fallback } : {},
4033
+ classes: {}
4034
+ });
4035
+ }
4036
+ props.push(prop);
4037
+ }
4038
+ props.sort((a, b) => Number(b.kind === "variant") - Number(a.kind === "variant"));
4039
+ const inherits = [];
4040
+ for (const origin of summarised) {
4041
+ const names = (byOrigin.get(origin) ?? []).sort();
4042
+ const deprecated = names.filter((propName) => entries.get(propName)?.symbols[0]?.getJsDocTags(checker).some((tag) => tag.name === "deprecated"));
4043
+ inherits.push({
4044
+ from: origin === "react" ? element ? `React.ComponentPropsWithRef<"${element}">` : "React DOM attributes" : origin,
4045
+ count: names.length,
4046
+ set: registerPropSet(context.propSets, names),
4047
+ ...deprecated.length ? { deprecated } : {}
4048
+ });
4049
+ }
4050
+ const info = {
4051
+ name,
4052
+ aliases: [],
4053
+ importPath: adapter.package,
4054
+ exportName: name,
4055
+ source: {
4056
+ file: "",
4057
+ line: 0
4058
+ },
4059
+ subcomponents: [],
4060
+ props,
4061
+ inherits,
4062
+ openProps,
4063
+ variants,
4064
+ compoundVariants: [],
4065
+ examples: [],
4066
+ classNames: [],
4067
+ cssVars: [],
4068
+ library: adapter.package
4069
+ };
4070
+ if (polymorphic) info.polymorphic = polymorphic.name;
4071
+ if (element) info.element = element;
4072
+ const jsdoc = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
4073
+ const description = jsdoc ? adapter.describe?.(jsdoc, name) ?? jsdoc : void 0;
4074
+ if (description) info.description = description;
4075
+ for (const tag of symbol.getJsDocTags(checker)) {
4076
+ if (tag.name !== "deprecated") continue;
4077
+ info.deprecated = ts.displayPartsToString(tag.text).trim() || true;
4078
+ }
4079
+ adapter.annotate?.(info);
4080
+ return info;
4081
+ }
4082
+ function describeProp(checker, name, entry, required) {
4083
+ const [first] = entry.symbols;
4084
+ const declared = first ? checker.getTypeOfSymbol(first) : void 0;
4085
+ const printed = declared ? checker.typeToString(declared, void 0, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope).replace(/^(?:(?:undefined|null) \| )+|(?: \| (?:undefined|null))+$/g, "") : "unknown";
4086
+ let values = [];
4087
+ for (const symbol of entry.symbols) {
4088
+ const found = literalValues$1(checker, checker.getTypeOfSymbol(symbol));
4089
+ if (!found) {
4090
+ values = void 0;
4091
+ break;
4092
+ }
4093
+ values.push(...found);
4094
+ }
4095
+ const ordered = values?.length ? declarationOrder(unique(values), entry.symbols, checker) : void 0;
4096
+ const prop = {
4097
+ name,
4098
+ type: ordered ? ordered.map((v) => JSON.stringify(v)).join(" | ") : truncate(unwrapParens(printed) || "unknown", 200),
4099
+ required,
4100
+ kind: "prop"
4101
+ };
4102
+ if (ordered) prop.values = ordered;
4103
+ const documented = entry.symbols.find((s) => s.getDocumentationComment(checker).length);
4104
+ const description = documented ? ts.displayPartsToString(documented.getDocumentationComment(checker)).trim() : "";
4105
+ if (description) prop.description = description;
4106
+ for (const tag of (documented ?? first)?.getJsDocTags(checker) ?? []) {
4107
+ const text = ts.displayPartsToString(tag.text).trim();
4108
+ if ((tag.name === "default" || tag.name === "defaultValue") && text) prop.default = text;
4109
+ if (tag.name === "deprecated") prop.deprecated = text || true;
4110
+ }
4111
+ return prop;
4112
+ }
4113
+ /**
4114
+ * Values in the order the declaration writes them (`'small' | 'medium' | 'large'`), which the
4115
+ * checker does not keep; values it does not write (an app's augmentation adds them) go last.
4116
+ */
4117
+ function declarationOrder(values, symbols, checker) {
4118
+ const text = symbols.map((s) => {
4119
+ const declaration = s.valueDeclaration ?? s.declarations?.[0];
4120
+ return declaration && (ts.isPropertySignature(declaration) || ts.isPropertyDeclaration(declaration)) && declaration.type ? typeText(checker, declaration.type, 0) : "";
4121
+ }).join("\n");
4122
+ const at = (value) => {
4123
+ const index = Math.max(text.indexOf(`'${value}'`), text.indexOf(`"${value}"`));
4124
+ return index === -1 ? Number.MAX_SAFE_INTEGER : index;
4125
+ };
4126
+ return values.map((value, i) => ({
4127
+ value,
4128
+ at: at(value),
4129
+ i
4130
+ })).sort((a, b) => a.at - b.at || a.i - b.i).map((entry) => entry.value);
4131
+ }
4132
+ /**
4133
+ * A type as written, with the type parameters and aliases it names written out too
4134
+ * (`Variant` → `Variant extends TextFieldVariants` → `'outlined' | 'standard' | 'filled'`).
4135
+ */
4136
+ function typeText(checker, node, depth) {
4137
+ let text = node.getText();
4138
+ if (depth > 2) return text;
4139
+ const visit = (child) => {
4140
+ if (ts.isTypeReferenceNode(child)) {
4141
+ const symbol = checker.getSymbolAtLocation(child.typeName);
4142
+ for (const declaration of symbol?.declarations ?? []) {
4143
+ const referenced = ts.isTypeParameterDeclaration(declaration) ? declaration.constraint : ts.isTypeAliasDeclaration(declaration) ? declaration.type : void 0;
4144
+ if (referenced) text += `\n${typeText(checker, referenced, depth + 1)}`;
4145
+ }
4146
+ }
4147
+ ts.forEachChild(child, visit);
4148
+ };
4149
+ visit(node);
4150
+ return text;
4151
+ }
4152
+ /**
4153
+ * The string literals a type allows, without `undefined` and `null`: a type parameter's
4154
+ * constraint (`variant?: Variant` where `Variant extends "outlined" | "filled"`). Undefined
4155
+ * when any member is not a string literal (`string & {}` leaves the set open).
4156
+ */
4157
+ function literalValues$1(checker, type, depth = 0) {
4158
+ if (depth > 4) return void 0;
4159
+ const values = [];
4160
+ for (const member of type.isUnion() ? type.types : [type]) {
4161
+ if (member.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) continue;
4162
+ if (member.isStringLiteral()) {
4163
+ values.push(member.value);
4164
+ continue;
4165
+ }
4166
+ const constraint = member.flags & ts.TypeFlags.TypeParameter ? checker.getBaseConstraintOfType(member) : void 0;
4167
+ const inner = constraint && constraint !== member ? literalValues$1(checker, constraint, depth + 1) : void 0;
4168
+ if (!inner) return void 0;
4169
+ values.push(...inner);
4170
+ }
4171
+ return values.length ? unique(values) : void 0;
4172
+ }
4173
+ /** The native element behind a signature, from the type of the `ref` its props take. */
4174
+ function renderedElement(checker, signature) {
4175
+ const param = signature?.getParameters()[0];
4176
+ if (!param) return void 0;
4177
+ const ref = checker.getPropertyOfType(checker.getTypeOfSymbol(param), "ref");
4178
+ if (!ref) return void 0;
4179
+ const text = checker.typeToString(checker.getTypeOfSymbol(ref));
4180
+ const element = /\bHTML(\w+?)Element\b/.exec(text)?.[1];
4181
+ return element ? ELEMENT_INTERFACES[element] : void 0;
4182
+ }
4183
+ /**
4184
+ * The folder module that default-exports a component (`@mui/material/Button` for
4185
+ * `Button/Button.d.ts`), when the package has one for it.
4186
+ */
4187
+ function defaultModule(program, checker, resolve, name, file, symbol) {
4188
+ const folder = path.basename(path.dirname(file.fileName));
4189
+ if (!/^[A-Za-z_][\w-]*$/.test(folder)) return void 0;
4190
+ const resolved = resolve(`${name}/${folder}`);
4191
+ const index = resolved ? program.getSourceFile(resolved) : void 0;
4192
+ const module = index ? checker.getSymbolAtLocation(index) : void 0;
4193
+ const exported = module ? checker.getExportsOfModule(module).find((e) => e.getName() === "default") : void 0;
4194
+ if (!exported) return void 0;
4195
+ return (exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported) === symbol ? `${name}/${folder}` : void 0;
4196
+ }
4197
+ /**
4198
+ * Styled slots a library exports next to the component they belong to (`SliderThumb` in
4199
+ * `Slider.d.ts`) become its parts, so lists show the component and its parts under it.
4200
+ */
4201
+ function linkSlots(components) {
4202
+ const byFile = /* @__PURE__ */ new Map();
4203
+ for (const component of components) byFile.set(component.source.file, [...byFile.get(component.source.file) ?? [], component]);
4204
+ for (const group of byFile.values()) for (const component of group) {
4205
+ 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];
4206
+ if (!owner) continue;
4207
+ component.parent = owner.name;
4208
+ owner.subcomponents.push(component.name);
4209
+ }
4210
+ }
4211
+ //#endregion
4212
+ //#region src/libraries/load.ts
4213
+ /** App sources read for module augmentations and themes, at most. */
4214
+ const MAX_SOURCES = 5e3;
4215
+ const SOURCE_FILE = /\.(?:[cm]?[jt]sx?)$/;
4216
+ /** Without a tsconfig, where an app's sources are not. */
4217
+ const NOT_SOURCES = [
4218
+ "**/node_modules/**",
4219
+ "**/dist/**",
4220
+ "**/build/**",
4221
+ "**/out/**",
4222
+ "**/.next/**",
4223
+ "**/coverage/**",
4224
+ "**/public/**"
4225
+ ];
4226
+ /**
4227
+ * The components and tokens of the libraries `config` found (`config.libraries`): each one's
4228
+ * components from its type declarations, read together with the app's files that augment them,
4229
+ * and the tokens of the theme the app creates for it.
4230
+ */
4231
+ async function loadLibraries(config) {
4232
+ const loaded = {
4233
+ components: [],
4234
+ propSets: {},
4235
+ exports: [],
4236
+ tokens: [],
4237
+ warnings: [],
4238
+ dependencies: []
4239
+ };
4240
+ const libraries = (config.libraries ?? []).flatMap((source) => {
4241
+ const adapter = libraryAdapter(source.package);
4242
+ return adapter ? [{
4243
+ source,
4244
+ adapter
4245
+ }] : [];
4246
+ });
4247
+ if (!libraries.length) return loaded;
4248
+ const project = readProjectConfig(config.root, config.tsconfig);
4249
+ const patterns = libraries.flatMap(({ adapter }) => [adapter.augments, ...adapter.theme ? [adapter.theme.matches] : []]);
4250
+ const sources = await appSources(config.root, project, patterns);
4251
+ for (const { source, adapter } of libraries) {
4252
+ if (!source.dir) {
4253
+ loaded.warnings.push(`${adapter.title} (${source.package}) is a dependency but is not installed, so its components are not known: install the dependencies.`);
4254
+ continue;
4255
+ }
4256
+ const augmentations = sources.filter((s) => adapter.augments.test(s.text)).map((s) => s.file);
4257
+ const extracted = extractLibrary({
4258
+ root: config.root,
4259
+ adapter,
4260
+ dir: source.dir,
4261
+ project,
4262
+ augmentations
4263
+ });
4264
+ if (adapter.theme) {
4265
+ const { matches } = adapter.theme;
4266
+ const themes = sources.filter((s) => matches.test(s.text));
4267
+ const read = adapter.theme.read(themes, config.root);
4268
+ addValues(extracted.components, read.values);
4269
+ loaded.tokens.push(...read.tokens);
4270
+ loaded.warnings.push(...read.warnings);
4271
+ loaded.dependencies.push(...themes.map((s) => s.file), ...read.files ?? []);
4272
+ }
4273
+ loaded.components.push(...extracted.components);
4274
+ Object.assign(loaded.propSets, extracted.propSets);
4275
+ loaded.exports.push(...extracted.exports);
4276
+ loaded.warnings.push(...extracted.warnings);
4277
+ loaded.dependencies.push(...extracted.dependencies, ...augmentations);
4278
+ }
4279
+ loaded.exports = unique(loaded.exports).sort();
4280
+ loaded.dependencies = unique(loaded.dependencies);
4281
+ return loaded;
4282
+ }
4283
+ /** Adds the values the app's theme gives props (`teal`, `dashed`) to the props that take values. */
4284
+ function addValues(components, values) {
4285
+ for (const added of values) for (const component of components) {
4286
+ if (added.component !== void 0 && component.name !== added.component) continue;
4287
+ const prop = component.props.find((p) => p.name === added.prop);
4288
+ if (!prop?.values || prop.values.includes(added.value)) continue;
4289
+ if (added.alongside !== void 0 && !prop.values.includes(added.alongside)) continue;
4290
+ prop.values.push(added.value);
4291
+ prop.type = prop.values.map((v) => JSON.stringify(v)).join(" | ");
4292
+ const variant = component.variants.find((v) => v.name === added.prop);
4293
+ if (variant && !variant.values.includes(added.value)) variant.values.push(added.value);
4294
+ }
4295
+ }
4296
+ /**
4297
+ * The app's source files whose text one of `patterns` matches, with that text: the files its
4298
+ * tsconfig includes, or without one the scripts under the root, at most `MAX_SOURCES`.
4299
+ */
4300
+ async function appSources(root, project, patterns) {
4301
+ const included = project.fileNames.filter((file) => SOURCE_FILE.test(file) && !/[\\/]node_modules[\\/]/.test(file));
4302
+ const files = (included.length ? included : await glob("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", {
4303
+ cwd: root,
4304
+ absolute: true,
4305
+ ignore: NOT_SOURCES
4306
+ })).sort().slice(0, MAX_SOURCES);
4307
+ const found = [];
4308
+ for (let i = 0; i < files.length; i += 64) {
4309
+ const batch = await Promise.all(files.slice(i, i + 64).map(async (file) => {
4310
+ try {
4311
+ const text = await fsp.readFile(file, "utf8");
4312
+ return patterns.some((pattern) => pattern.test(text)) ? {
4313
+ file,
4314
+ text
4315
+ } : void 0;
4316
+ } catch {
4317
+ return;
4318
+ }
4319
+ }));
4320
+ for (const entry of batch) if (entry) found.push(entry);
4321
+ }
4322
+ return found;
2934
4323
  }
2935
4324
  //#endregion
2936
4325
  //#region src/lint/analyze.ts
@@ -3196,6 +4585,8 @@ function resolveElement(element, analysis, target, file) {
3196
4585
  if (binding?.imported === "*" && rest.length) {
3197
4586
  if (!target.isDesignSystemImport(binding.source, file)) return { kind: "external" };
3198
4587
  const name = rest.join(".");
4588
+ const library = target.libraryOf(binding.source);
4589
+ if (library) return fromLibrary(target, library, name, binding.source, rest.length > 1);
3199
4590
  const component = target.components.get(name);
3200
4591
  if (component) return {
3201
4592
  kind: "component",
@@ -3211,7 +4602,14 @@ function resolveElement(element, analysis, target, file) {
3211
4602
  }
3212
4603
  let owner;
3213
4604
  if (binding && analysis.declared.has(head)) return { kind: "external" };
3214
- if (binding) {
4605
+ const library = binding ? target.libraryOf(binding.source) : void 0;
4606
+ if (binding && library) {
4607
+ const imported = binding.imported === "default" ? target.defaultExport(binding.source, file)?.name : binding.imported;
4608
+ if (imported === void 0) return { kind: "external" };
4609
+ const resolved = fromLibrary(target, library, imported, binding.source, rest.length > 0);
4610
+ if (resolved.kind !== "component") return resolved;
4611
+ owner = resolved.component;
4612
+ } else if (binding) {
3215
4613
  if (!target.isDesignSystemImport(binding.source, file)) return { kind: "external" };
3216
4614
  const imported = binding.imported === "default" ? target.defaultExport(binding.source, file)?.name ?? head : binding.imported;
3217
4615
  owner = target.componentFor(imported, binding.source, file);
@@ -3246,6 +4644,26 @@ function resolveElement(element, analysis, target, file) {
3246
4644
  member: rest.join(".")
3247
4645
  };
3248
4646
  }
4647
+ /**
4648
+ * `name` from a library (`@mui/material`, or a module of it such as `@mui/material/Button`): its
4649
+ * own component of that name, never a wrapper the project has. A name the package exports that
4650
+ * is no component (a context, a styled slot it does not offer), or one imported from one of its
4651
+ * modules rather than the package itself, is not judged; one the package does not export at all
4652
+ * is invented.
4653
+ */
4654
+ function fromLibrary(target, library, name, source, member) {
4655
+ const component = target.libraryComponent(name, library);
4656
+ if (component) return {
4657
+ kind: "component",
4658
+ component
4659
+ };
4660
+ if (source !== library || target.exports.has(name) || member) return { kind: "external" };
4661
+ return {
4662
+ kind: "missing-export",
4663
+ name,
4664
+ source
4665
+ };
4666
+ }
3249
4667
  //#endregion
3250
4668
  //#region src/lint/tailwind-palette.ts
3251
4669
  /** Tailwind's default color palette: name → shade → OKLCH value. */
@@ -3606,6 +5024,23 @@ const NAMESPACES = [
3606
5024
  ["--font-", "typography"],
3607
5025
  ["--text-", "typography"]
3608
5026
  ];
5027
+ /**
5028
+ * A Tailwind v4 theme variable split into its namespace and key: `--color-primary` →
5029
+ * `--color` and `primary`, `--spacing` → `--spacing` and `''` (the spacing unit).
5030
+ */
5031
+ function themeVariable(name) {
5032
+ if (name === "--spacing") return {
5033
+ namespace: "--spacing",
5034
+ key: "",
5035
+ category: "spacing"
5036
+ };
5037
+ const found = NAMESPACES.find(([prefix]) => name.startsWith(prefix) && name.length > prefix.length);
5038
+ return found && {
5039
+ namespace: found[0].slice(0, -1),
5040
+ key: name.slice(found[0].length),
5041
+ category: found[1]
5042
+ };
5043
+ }
3609
5044
  const TAILWIND_DEFAULT_SPACING = "0.25rem";
3610
5045
  /** Tailwind v4's default `--radius-*` scale; a theme overrides keys one by one. */
3611
5046
  const TAILWIND_DEFAULT_RADIUS = {
@@ -3695,6 +5130,12 @@ function cssSheetTokens(sheets) {
3695
5130
  const lookup = (name) => base.get(name)?.value ?? theme.get(name)?.value ?? tailwindColor(name);
3696
5131
  const tokens = [];
3697
5132
  const consumed = /* @__PURE__ */ new Set();
5133
+ const aliases = /* @__PURE__ */ new Map();
5134
+ const add = (token, raw) => {
5135
+ tokens.push(token);
5136
+ const alias = varAlias(raw);
5137
+ if (alias) aliases.set(token, alias);
5138
+ };
3698
5139
  for (const variable of theme.values()) {
3699
5140
  const namespace = NAMESPACES.find(([prefix]) => variable.name.startsWith(prefix));
3700
5141
  const isSpacingBase = variable.name === "--spacing";
@@ -3707,7 +5148,7 @@ function cssSheetTokens(sheets) {
3707
5148
  const target = alias ? base.get(alias) : void 0;
3708
5149
  if (alias && target) {
3709
5150
  consumed.add(alias);
3710
- tokens.push(finalize({
5151
+ add(finalize({
3711
5152
  name: alias.slice(2),
3712
5153
  category,
3713
5154
  value: resolveValue(target.value, lookup),
@@ -3718,10 +5159,10 @@ function cssSheetTokens(sheets) {
3718
5159
  description: target.comment ?? variable.comment,
3719
5160
  line: target.line,
3720
5161
  file: target.file
3721
- }));
5162
+ }), target.value);
3722
5163
  continue;
3723
5164
  }
3724
- tokens.push(finalize({
5165
+ add(finalize({
3725
5166
  name: variable.name.slice(2),
3726
5167
  category,
3727
5168
  value: variable.value,
@@ -3732,11 +5173,11 @@ function cssSheetTokens(sheets) {
3732
5173
  description: variable.comment,
3733
5174
  line: variable.line,
3734
5175
  file: variable.file
3735
- }));
5176
+ }), variable.value);
3736
5177
  }
3737
5178
  for (const variable of base.values()) {
3738
5179
  if (consumed.has(variable.name)) continue;
3739
- tokens.push(finalize({
5180
+ add(finalize({
3740
5181
  name: variable.name.slice(2),
3741
5182
  category: categorize$1(variable.name, resolveValue(variable.value, lookup)),
3742
5183
  value: resolveValue(variable.value, lookup),
@@ -3745,7 +5186,12 @@ function cssSheetTokens(sheets) {
3745
5186
  description: variable.comment,
3746
5187
  line: variable.line,
3747
5188
  file: variable.file
3748
- }));
5189
+ }), variable.value);
5190
+ }
5191
+ const byVar = new Map(tokens.map((t) => [t.cssVar ?? `--${t.name}`, t]));
5192
+ for (const [token, name] of aliases) {
5193
+ const target = byVar.get(name);
5194
+ if (target && target !== token) token.aliasOf = target.name;
3749
5195
  }
3750
5196
  const cleared = (namespace) => resets.has("*") || resets.has(namespace);
3751
5197
  if (tailwind && !cleared("spacing") && !theme.has("--spacing") && !base.has("--spacing")) tokens.push({
@@ -3846,6 +5292,10 @@ function categorize$1(name, value) {
3846
5292
  if (parseColor(value, { bareHsl: true })) return "color";
3847
5293
  return "other";
3848
5294
  }
5295
+ /** `--x` for a value that is exactly `var(--x)`. */
5296
+ function varAlias(value) {
5297
+ return /^var\(\s*(--[\w-]+)\s*\)$/.exec(value.trim())?.[1];
5298
+ }
3849
5299
  /** Follows plain `var(--x)` aliases so the token shows its concrete value. */
3850
5300
  function resolveValue(value, lookup, depth = 0) {
3851
5301
  const alias = /^var\(\s*(--[\w-]+)\s*\)$/.exec(value)?.[1];
@@ -3924,7 +5374,150 @@ function trails(comment, before) {
3924
5374
  return line !== void 0 && comment.source?.start?.line === line;
3925
5375
  }
3926
5376
  //#endregion
5377
+ //#region src/tokens/design-md.ts
5378
+ /**
5379
+ * Reads the tokens in a DESIGN.md's YAML front matter (https://github.com/google-labs-code/design.md):
5380
+ * `colors`, `typography`, `rounded` and `spacing`. Tokens are named by their path
5381
+ * (`colors.primary`), which is how `{colors.primary}` references them. `components` holds
5382
+ * per-component styling rather than tokens to choose from, and the Markdown body is prose, so
5383
+ * neither is read. A DESIGN.md says nothing about how code reads its tokens, so they have no
5384
+ * custom property or Tailwind class: a finding names the nearest token without a fix.
5385
+ */
5386
+ /** Front matter section → category, in the spec's order. */
5387
+ const SECTIONS$1 = [
5388
+ ["colors", "color"],
5389
+ ["typography", "typography"],
5390
+ ["rounded", "radius"],
5391
+ ["spacing", "spacing"]
5392
+ ];
5393
+ /** A DESIGN.md dimension: a number with px, em or rem, or a bare 0. */
5394
+ const DIMENSION = /^-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)$|^0$/;
5395
+ function parseDesignMd(text, file) {
5396
+ const front = frontMatter(text);
5397
+ if (front === void 0) return {
5398
+ tokens: [],
5399
+ warnings: [`${file}: no YAML front matter (between --- lines at the top)`]
5400
+ };
5401
+ const lineCounter = new LineCounter();
5402
+ const doc = parseDocument(front.yaml, { lineCounter });
5403
+ const [error] = doc.errors;
5404
+ if (error) return {
5405
+ tokens: [],
5406
+ warnings: [`${file}: invalid YAML front matter (${error.message.split("\n")[0] ?? ""})`]
5407
+ };
5408
+ if (!isMap(doc.contents)) return {
5409
+ tokens: [],
5410
+ warnings: []
5411
+ };
5412
+ const warnings = [];
5413
+ const raw = [];
5414
+ const lineAt = (offset) => offset === void 0 ? 1 : lineCounter.linePos(offset).line + front.line;
5415
+ const collect = (map, path, category) => {
5416
+ for (const pair of map.items) {
5417
+ const key = isScalar(pair.key) ? scalar(pair.key.value) : void 0;
5418
+ if (key === void 0) continue;
5419
+ const at = [...path, key];
5420
+ const line = lineAt(isScalar(pair.key) ? pair.key.range?.[0] : void 0);
5421
+ const node = pair.value;
5422
+ if (isMap(node)) {
5423
+ if (category === "typography") {
5424
+ const value = Object.fromEntries(node.items.flatMap((p) => {
5425
+ const property = isScalar(p.key) ? scalar(p.key.value) : void 0;
5426
+ const value = isScalar(p.value) ? scalar(p.value.value) : void 0;
5427
+ return property === void 0 || value === void 0 ? [] : [[property, value]];
5428
+ }));
5429
+ raw.push({
5430
+ path: at,
5431
+ category,
5432
+ value,
5433
+ line
5434
+ });
5435
+ } else collect(node, at, category);
5436
+ continue;
5437
+ }
5438
+ const value = isScalar(node) ? scalar(node.value) : void 0;
5439
+ if (value === void 0) {
5440
+ warnings.push(`${file}: ${at.join(".")} has no value${category === "color" ? " (a # starts a YAML comment: quote hex colors)" : ""}`);
5441
+ continue;
5442
+ }
5443
+ const length = category === "spacing" || category === "radius";
5444
+ raw.push({
5445
+ path: at,
5446
+ category: length && !DIMENSION.test(value) && !isReference(value) ? "other" : category,
5447
+ value,
5448
+ line
5449
+ });
5450
+ }
5451
+ };
5452
+ for (const [section, category] of SECTIONS$1) {
5453
+ const node = doc.contents.get(section, true);
5454
+ if (isMap(node)) collect(node, [section], category);
5455
+ }
5456
+ const byPath = new Map(raw.map((t) => [t.path.join("."), t]));
5457
+ const resolve = (value, seen) => {
5458
+ const alias = reference(value);
5459
+ if (alias === void 0) return value;
5460
+ const target = byPath.get(alias);
5461
+ if (!target || typeof target.value !== "string") {
5462
+ warnings.push(`${file}: unresolved reference {${alias}}`);
5463
+ return value;
5464
+ }
5465
+ if (seen.has(alias)) {
5466
+ warnings.push(`${file}: circular reference {${alias}}`);
5467
+ return value;
5468
+ }
5469
+ return resolve(target.value, /* @__PURE__ */ new Set([...seen, alias]));
5470
+ };
5471
+ return {
5472
+ tokens: raw.map((t) => {
5473
+ const name = t.path.join(".");
5474
+ const value = typeof t.value === "string" ? resolve(t.value, /* @__PURE__ */ new Set([name])) : Object.entries(t.value).map(([k, v]) => `${k}: ${resolve(v, /* @__PURE__ */ new Set([name]))}`).join("; ");
5475
+ const token = {
5476
+ name,
5477
+ category: t.category,
5478
+ value,
5479
+ usage: [],
5480
+ source: {
5481
+ file,
5482
+ line: t.line
5483
+ }
5484
+ };
5485
+ const alias = typeof t.value === "string" ? reference(t.value) : void 0;
5486
+ if (alias && byPath.has(alias)) token.aliasOf = alias;
5487
+ return token;
5488
+ }),
5489
+ warnings
5490
+ };
5491
+ }
5492
+ /** The YAML between a first line of exactly `---` and the next such line, and the line it starts on. */
5493
+ function frontMatter(text) {
5494
+ const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
5495
+ if (lines[0]?.trimEnd() !== "---") return void 0;
5496
+ const end = lines.findIndex((line, i) => i > 0 && line.trimEnd() === "---");
5497
+ if (end === -1) return void 0;
5498
+ return {
5499
+ yaml: lines.slice(1, end).join("\n"),
5500
+ line: 1
5501
+ };
5502
+ }
5503
+ /** A YAML scalar as text: strings, numbers and booleans; null (an empty value) is none. */
5504
+ function scalar(value) {
5505
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : void 0;
5506
+ }
5507
+ function reference(value) {
5508
+ return /^\{([^{}]+)\}$/.exec(value.trim())?.[1];
5509
+ }
5510
+ function isReference(value) {
5511
+ return reference(value) !== void 0;
5512
+ }
5513
+ //#endregion
3927
5514
  //#region src/tokens/dtcg.ts
5515
+ /**
5516
+ * The `$extensions` key under which onsystem keeps what DTCG has no field for: the token's
5517
+ * own name, the custom property code uses, the Tailwind theme variable and the values in
5518
+ * other modes.
5519
+ */
5520
+ const ONSYSTEM_EXTENSION = "com.github.dgesteves.onsystem";
3928
5521
  function parseDtcgTokens(text, file, options = {}) {
3929
5522
  const warnings = [];
3930
5523
  let json;
@@ -3936,13 +5529,16 @@ function parseDtcgTokens(text, file, options = {}) {
3936
5529
  warnings: [`${file}: invalid JSON (${error.message})`]
3937
5530
  };
3938
5531
  }
3939
- if (!isObject$1(json)) return {
5532
+ if (!isObject$2(json)) return {
3940
5533
  tokens: [],
3941
5534
  warnings: [`${file}: expected a JSON object`]
3942
5535
  };
3943
5536
  const lines = lineIndex(text);
5537
+ const origins = /* @__PURE__ */ new WeakMap();
5538
+ recordPaths(json, [], origins);
5539
+ const tree = extendGroups(json, (message) => warnings.push(`${file}: ${message}`));
3944
5540
  const raw = [];
3945
- walk(json, [], void 0, raw, lines);
5541
+ walk(tree, [], void 0, raw, (node, path) => lines(origins.get(node) ?? path));
3946
5542
  const byPath = new Map(raw.map((t) => [t.path.join("."), t]));
3947
5543
  const resolve = (value, seen) => {
3948
5544
  if (typeof value === "string") {
@@ -3962,7 +5558,7 @@ function parseDtcgTokens(text, file, options = {}) {
3962
5558
  return value;
3963
5559
  }
3964
5560
  if (Array.isArray(value)) return value.map((v) => resolve(v, seen));
3965
- if (isObject$1(value)) {
5561
+ if (isObject$2(value)) {
3966
5562
  const ref = value.$ref;
3967
5563
  if (typeof ref === "string" && Object.keys(value).length === 1) {
3968
5564
  const pointer = ref.replace(/^#\//, "").split("/").filter((p) => p !== "$value");
@@ -3982,61 +5578,189 @@ function parseDtcgTokens(text, file, options = {}) {
3982
5578
  const target = byPath.get(alias);
3983
5579
  return target ? typeOf(target, seen) : void 0;
3984
5580
  };
5581
+ const nameOf = /* @__PURE__ */ new Map();
5582
+ const taken = new Set(raw.filter((t) => !t.own?.name).map((t) => t.path.join(".")));
5583
+ for (const t of raw) {
5584
+ const path = t.path.join(".");
5585
+ const own = t.own?.name;
5586
+ const name = own !== void 0 && !taken.has(own) ? own : path;
5587
+ taken.add(name);
5588
+ nameOf.set(path, name);
5589
+ }
3985
5590
  return {
3986
5591
  tokens: raw.map((t) => {
3987
- const name = t.path.join(".");
3988
- const aliasOf = aliasTarget(t.value);
5592
+ const path = t.path.join(".");
5593
+ const name = nameOf.get(path) ?? path;
5594
+ const alias = aliasTarget(t.value);
5595
+ const aliasOf = alias === void 0 ? void 0 : nameOf.get(alias) ?? alias;
3989
5596
  const type = typeOf(t);
3990
- const value = formatValue(resolve(t.value, /* @__PURE__ */ new Set([name])), type);
5597
+ const format = (value) => {
5598
+ const resolved = resolve(value, /* @__PURE__ */ new Set([path]));
5599
+ return t.own?.channels === "hsl" && hslChannels(resolved) || formatValue(resolved, type);
5600
+ };
5601
+ const segments = [options.prefix, ...t.path.filter((p) => p !== "$root")].filter(Boolean);
5602
+ const generated = segments.length ? `--${segments.join("-").replace(/[^\w-]+/g, "-")}` : void 0;
5603
+ const theme = t.own?.tailwind ? themeVariable(t.own.tailwind) : void 0;
3991
5604
  const token = {
3992
5605
  name,
3993
5606
  category: categorize(type, t.path),
3994
- value,
3995
- cssVar: `--${[options.prefix, ...t.path].filter(Boolean).join("-").replace(/[^\w-]+/g, "-")}`,
5607
+ value: format(t.value),
3996
5608
  usage: [],
3997
5609
  source: {
3998
5610
  file,
3999
5611
  line: t.line
4000
5612
  }
4001
5613
  };
5614
+ const cssVar = t.own ? t.own.cssVar : generated;
5615
+ if (cssVar) token.cssVar = cssVar;
5616
+ if (theme) token.tailwind = theme.key;
4002
5617
  if (aliasOf) token.aliasOf = aliasOf;
4003
5618
  if (t.description) token.description = t.description;
4004
5619
  if (t.deprecated) token.deprecated = t.deprecated;
4005
- if (t.modes) token.modes = Object.fromEntries(Object.entries(t.modes).map(([mode, v]) => [mode, formatValue(resolve(v, /* @__PURE__ */ new Set([name])), type)]));
4006
- token.usage = usageFor(token);
5620
+ if (t.modes) token.modes = Object.fromEntries(Object.entries(t.modes).map(([mode, v]) => [mode, format(v)]));
5621
+ token.usage = usageFor(token, theme?.namespace);
4007
5622
  return token;
4008
5623
  }),
4009
5624
  warnings
4010
5625
  };
4011
5626
  }
4012
- function walk(node, path, inheritedType, out, lines) {
5627
+ function walk(node, path, inheritedType, out, lineOf) {
4013
5628
  const type = typeof node.$type === "string" ? node.$type : inheritedType;
4014
5629
  if ("$value" in node) {
4015
5630
  const token = {
4016
5631
  path,
4017
5632
  value: node.$value ?? null,
4018
- line: lines(path)
5633
+ line: lineOf(node, path)
4019
5634
  };
4020
5635
  if (type) token.type = type;
4021
5636
  if (typeof node.$description === "string") token.description = node.$description;
4022
5637
  if (node.$deprecated === true) token.deprecated = true;
4023
5638
  else if (typeof node.$deprecated === "string") token.deprecated = node.$deprecated;
4024
- const modes = modesOf(node.$extensions);
5639
+ const extension = onsystemExtension(node.$extensions);
5640
+ const modes = extension?.modes ?? modesOf(node.$extensions);
4025
5641
  if (modes) token.modes = modes;
5642
+ if (extension) {
5643
+ const { modes: _modes, ...own } = extension;
5644
+ token.own = own;
5645
+ }
4026
5646
  out.push(token);
4027
5647
  return;
4028
5648
  }
4029
5649
  for (const [key, child] of Object.entries(node)) {
4030
- if (key.startsWith("$") || !isObject$1(child)) continue;
4031
- walk(child, [...path, key], type, out, lines);
5650
+ if (key.startsWith("$") && key !== "$root" || !isObject$2(child)) continue;
5651
+ walk(child, [...path, key], type, out, lineOf);
5652
+ }
5653
+ }
5654
+ /** Where each object of the file is, by path. */
5655
+ function recordPaths(node, path, into) {
5656
+ into.set(node, path);
5657
+ if ("$value" in node) return;
5658
+ for (const [key, child] of Object.entries(node)) {
5659
+ if (key.startsWith("$") && key !== "$root" || !isObject$2(child)) continue;
5660
+ recordPaths(child, [...path, key], into);
5661
+ }
5662
+ }
5663
+ /**
5664
+ * Resolves group extension: a group with `$extends` (or JSON Schema's `$ref`, its equivalent)
5665
+ * takes on every token and property of the group it names, and its own win where both define
5666
+ * one, group by group (a token is replaced whole). A `$ref` on an object without `$value`
5667
+ * that points at a token makes it an alias of that token. Unresolvable and circular
5668
+ * references, and `$extends` that names a token, are reported and left out.
5669
+ */
5670
+ function extendGroups(root, warn) {
5671
+ const done = /* @__PURE__ */ new Map();
5672
+ const active = /* @__PURE__ */ new Set();
5673
+ const expand = (node, path) => {
5674
+ if ("$value" in node) return node;
5675
+ const key = JSON.stringify(path);
5676
+ const cached = done.get(key);
5677
+ if (cached) return cached;
5678
+ active.add(key);
5679
+ const extendsRef = typeof node.$extends === "string" ? node.$extends : void 0;
5680
+ const ref = extendsRef ?? (typeof node.$ref === "string" ? node.$ref : void 0);
5681
+ const label = extendsRef === void 0 ? "$ref" : "$extends";
5682
+ let out = {};
5683
+ let result;
5684
+ if (ref !== void 0) {
5685
+ const target = referencePath(ref);
5686
+ const found = target && lookup(root, target);
5687
+ if (!target || !found) warn(`unresolved ${label} ${ref}`);
5688
+ else if ("$value" in found) {
5689
+ if (extendsRef !== void 0) warn(`$extends ${ref} names a token, not a group`);
5690
+ else {
5691
+ const { $ref: _ref, ...rest } = node;
5692
+ result = {
5693
+ ...rest,
5694
+ $value: `{${target.join(".")}}`
5695
+ };
5696
+ }
5697
+ } else if (active.has(JSON.stringify(target))) warn(`circular ${label} ${ref}`);
5698
+ else out = { ...expand(found, target) };
5699
+ }
5700
+ if (!result) {
5701
+ for (const [k, v] of Object.entries(node)) {
5702
+ if (k === "$extends" || k === "$ref" && extendsRef === void 0) continue;
5703
+ if (isObject$2(v) && (!k.startsWith("$") || k === "$root")) {
5704
+ const child = expand(v, [...path, k]);
5705
+ const inherited = out[k];
5706
+ out[k] = isGroup(inherited) && isGroup(child) ? mergeGroups(inherited, child) : child;
5707
+ } else out[k] = v;
5708
+ }
5709
+ result = out;
5710
+ }
5711
+ active.delete(key);
5712
+ done.set(key, result);
5713
+ return result;
5714
+ };
5715
+ return expand(root, []);
5716
+ }
5717
+ /** `base` with `own` over it: tokens and properties replaced whole, groups merged. */
5718
+ function mergeGroups(base, own) {
5719
+ const out = { ...base };
5720
+ for (const [key, value] of Object.entries(own)) {
5721
+ const inherited = out[key];
5722
+ out[key] = (!key.startsWith("$") || key === "$root") && isGroup(inherited) && isGroup(value) ? mergeGroups(inherited, value) : value;
4032
5723
  }
5724
+ return out;
5725
+ }
5726
+ /** `{button}` or `#/button` → `["button"]`; a pointer's trailing `$value` is the token itself. */
5727
+ function referencePath(ref) {
5728
+ const curly = /^\{([^{}]+)\}$/.exec(ref.trim())?.[1];
5729
+ if (curly) return curly.split(".");
5730
+ if (!ref.startsWith("#/")) return void 0;
5731
+ const parts = ref.slice(2).split("/").map((p) => p.replaceAll("~1", "/").replaceAll("~0", "~"));
5732
+ if (parts.at(-1) === "$value") parts.pop();
5733
+ return parts.length ? parts : void 0;
5734
+ }
5735
+ function lookup(root, path) {
5736
+ let node = root;
5737
+ for (const key of path) {
5738
+ if (!isObject$2(node)) return void 0;
5739
+ node = node[key];
5740
+ }
5741
+ return isObject$2(node) ? node : void 0;
5742
+ }
5743
+ function isGroup(value) {
5744
+ return isObject$2(value) && !("$value" in value);
5745
+ }
5746
+ /** The fields onsystem reads from its own `$extensions` entry, when they are well formed. */
5747
+ function onsystemExtension(extensions) {
5748
+ const own = isObject$2(extensions) ? extensions[ONSYSTEM_EXTENSION] : void 0;
5749
+ if (!isObject$2(own)) return void 0;
5750
+ const out = {};
5751
+ if (typeof own.name === "string" && /^[^\s{}]+$/.test(own.name)) out.name = own.name;
5752
+ if (typeof own.cssVar === "string" && /^--[\w-]+$/.test(own.cssVar)) out.cssVar = own.cssVar;
5753
+ if (typeof own.tailwind === "string" && themeVariable(own.tailwind)) out.tailwind = own.tailwind;
5754
+ if (own.channels === "hsl") out.channels = "hsl";
5755
+ if (isObject$2(own.modes)) out.modes = own.modes;
5756
+ return out;
4033
5757
  }
4034
5758
  /** Modes are not standardised yet; accept the common `$extensions.mode(s)` shape. */
4035
5759
  function modesOf(extensions) {
4036
- if (!isObject$1(extensions)) return void 0;
5760
+ if (!isObject$2(extensions)) return void 0;
4037
5761
  for (const key of ["modes", "mode"]) {
4038
5762
  const value = extensions[key];
4039
- if (isObject$1(value)) return value;
5763
+ if (isObject$2(value)) return value;
4040
5764
  }
4041
5765
  }
4042
5766
  function categorize(type, path) {
@@ -4060,6 +5784,13 @@ function categorize(type, path) {
4060
5784
  return "other";
4061
5785
  }
4062
5786
  }
5787
+ /** `214.3 31.8% 91.4%`: an HSL color as the bare channels a Tailwind v3 custom property holds. */
5788
+ function hslChannels(value) {
5789
+ if (!isObject$2(value) || value.colorSpace !== "hsl" || !Array.isArray(value.components)) return;
5790
+ const [h, s, l] = value.components.map((c) => formatValue(c, void 0));
5791
+ if (h === void 0 || s === void 0 || l === void 0) return void 0;
5792
+ return `${h} ${s}% ${l}%${typeof value.alpha === "number" && value.alpha < 1 ? ` / ${value.alpha}` : ""}`;
5793
+ }
4063
5794
  function formatValue(value, type) {
4064
5795
  if (typeof value === "string") return value;
4065
5796
  if (typeof value === "number" || typeof value === "boolean") return String(value);
@@ -4074,6 +5805,7 @@ function formatValue(value, type) {
4074
5805
  if (type === "shadow") {
4075
5806
  const s = value;
4076
5807
  return [
5808
+ s.inset === true ? "inset" : void 0,
4077
5809
  s.offsetX,
4078
5810
  s.offsetY,
4079
5811
  s.blur,
@@ -4083,7 +5815,7 @@ function formatValue(value, type) {
4083
5815
  }
4084
5816
  return Object.entries(value).map(([k, v]) => `${k}: ${formatValue(v, void 0)}`).join("; ");
4085
5817
  }
4086
- const COLOR_SPACES = {
5818
+ const COLOR_SPACES$1 = {
4087
5819
  srgb: ([r, g, b]) => `rgb(${pct255(r)} ${pct255(g)} ${pct255(b)}`,
4088
5820
  hsl: ([h, s, l]) => `hsl(${h} ${s}% ${l}%`,
4089
5821
  hwb: ([h, w, b]) => `hwb(${h} ${w}% ${b}%`,
@@ -4097,14 +5829,14 @@ function formatColor(value) {
4097
5829
  if (typeof value.hex === "string" && alpha === void 0) return value.hex;
4098
5830
  const space = typeof value.colorSpace === "string" ? value.colorSpace : "srgb";
4099
5831
  const components = Array.isArray(value.components) ? value.components.map((c) => formatValue(c, void 0)) : [];
4100
- const make = COLOR_SPACES[space];
5832
+ const make = COLOR_SPACES$1[space];
4101
5833
  return `${make ? make(components) : `color(${space} ${components.join(" ")}`}${alpha === void 0 ? "" : ` / ${alpha}`})`;
4102
5834
  }
4103
5835
  function pct255(component) {
4104
5836
  if (component === void 0 || component === "none") return "none";
4105
5837
  return String(Math.round(Number(component) * 255));
4106
5838
  }
4107
- function isObject$1(value) {
5839
+ function isObject$2(value) {
4108
5840
  return typeof value === "object" && value !== null && !Array.isArray(value);
4109
5841
  }
4110
5842
  /**
@@ -4212,7 +5944,7 @@ function scopedFamilies(tokens, filesUsing) {
4212
5944
  const family = tokenFamily(token);
4213
5945
  if (!family || scoped.has(family)) continue;
4214
5946
  if (NAMED_SCOPES.has(family)) scoped.add(family);
4215
- else if (filesUsing && !CORE_FAMILIES.has(family) && !STATUS_WORDS.has(family) && (filesUsing.get(family)?.size ?? 0) <= 1) scoped.add(family);
5947
+ else if (filesUsing && token.usage.length > 0 && !CORE_FAMILIES.has(family) && !STATUS_WORDS.has(family) && (filesUsing.get(family)?.size ?? 0) <= 1) scoped.add(family);
4216
5948
  }
4217
5949
  return scoped;
4218
5950
  }
@@ -4270,165 +6002,774 @@ const KIND_WORDS = [
4270
6002
  ]]
4271
6003
  ];
4272
6004
  /**
4273
- * The name that says what a token is for: a sidebar's own palette mirrors the
4274
- * core one, so `sidebar-accent` is an `accent` and `sidebar-foreground` a
4275
- * `foreground`. shadcn/ui's sidebar surface is `--sidebar` (v4) or
4276
- * `--sidebar-background` (v3).
6005
+ * The name that says what a token is for: a sidebar's own palette mirrors the
6006
+ * core one, so `sidebar-accent` is an `accent` and `sidebar-foreground` a
6007
+ * `foreground`. shadcn/ui's sidebar surface is `--sidebar` (v4) or
6008
+ * `--sidebar-background` (v3).
6009
+ */
6010
+ function roleKey(token) {
6011
+ const key = tokenKey(token);
6012
+ return key === "sidebar" ? "background" : key.replace(/^sidebar-/, "");
6013
+ }
6014
+ /** What a token is for, from its name: shadcn/ui's conventions and common DTCG names. */
6015
+ function kindOf(key) {
6016
+ const segments = key.split("-");
6017
+ for (const [kind, words] of KIND_WORDS) if (segments.some((s) => words.includes(s))) return kind;
6018
+ return "other";
6019
+ }
6020
+ /** Tokens made for each role, in the order to prefer them when their values tie. */
6021
+ const PREFERRED = {
6022
+ text: ["foreground", "muted-foreground"],
6023
+ surface: [
6024
+ "background",
6025
+ "muted",
6026
+ "secondary",
6027
+ "accent",
6028
+ "card",
6029
+ "popover"
6030
+ ],
6031
+ line: [
6032
+ "border",
6033
+ "input",
6034
+ "ring"
6035
+ ]
6036
+ };
6037
+ /** Plain text colors, as opposed to the text that goes on one surface (`card-foreground`). */
6038
+ const GENERIC_TEXT = /^(?:muted-foreground|(?:foreground|text|fg)(?:-(?:default|base|primary|secondary|muted|subtle))?)$/;
6039
+ function roleFit(token, role) {
6040
+ if (!role) return {
6041
+ tier: 1,
6042
+ rank: 0
6043
+ };
6044
+ const key = roleKey(token);
6045
+ const index = PREFERRED[role].indexOf(key);
6046
+ const rank = index === -1 ? PREFERRED[role].length : index;
6047
+ const kind = kindOf(key);
6048
+ switch (role) {
6049
+ case "text":
6050
+ if (kind === "foreground") return {
6051
+ tier: GENERIC_TEXT.test(key) ? 0 : 1,
6052
+ rank
6053
+ };
6054
+ return {
6055
+ tier: kind === "other" ? 1 : 2,
6056
+ rank
6057
+ };
6058
+ case "surface": return {
6059
+ tier: kind === "surface" ? 0 : kind === "foreground" ? 2 : 1,
6060
+ rank
6061
+ };
6062
+ case "line": return {
6063
+ tier: kind === "line" ? 0 : kind === "other" ? 1 : 2,
6064
+ rank
6065
+ };
6066
+ }
6067
+ }
6068
+ //#endregion
6069
+ //#region src/types.ts
6070
+ /**
6071
+ * The design-system model the server exposes. Everything here is plain JSON so
6072
+ * it can be cached on disk and returned as MCP structured content.
6073
+ */
6074
+ const TOKEN_CATEGORIES = [
6075
+ "color",
6076
+ "spacing",
6077
+ "radius",
6078
+ "typography",
6079
+ "shadow",
6080
+ "other"
6081
+ ];
6082
+ //#endregion
6083
+ //#region src/tokens/export.ts
6084
+ /** Group names a DTCG or DESIGN.md token path may already start with, per category. */
6085
+ const GROUP_NAMES = {
6086
+ color: [
6087
+ "color",
6088
+ "colors",
6089
+ "colour",
6090
+ "colours"
6091
+ ],
6092
+ spacing: ["spacing", "space"],
6093
+ radius: [
6094
+ "radius",
6095
+ "radii",
6096
+ "rounded"
6097
+ ],
6098
+ typography: [
6099
+ "typography",
6100
+ "font",
6101
+ "fonts"
6102
+ ],
6103
+ shadow: [
6104
+ "shadow",
6105
+ "shadows",
6106
+ "elevation"
6107
+ ],
6108
+ other: []
6109
+ };
6110
+ /**
6111
+ * The tokens as a DTCG 2025.10 file: a group per category, colors as color objects in their
6112
+ * own color space, lengths as `{ value, unit }`, aliases as references, and what DTCG has no
6113
+ * field for (the token's name, the custom property, the Tailwind theme variable, dark and
6114
+ * other modes) under `$extensions["com.github.dgesteves.onsystem"]`.
6115
+ */
6116
+ function toDtcg(tokens) {
6117
+ const warnings = [];
6118
+ const paths = assignPaths(tokens, warnings);
6119
+ const lookup = varLookup(tokens);
6120
+ const byName = new Map(tokens.map((t) => [t.name, t]));
6121
+ const tree = {};
6122
+ let count = 0;
6123
+ for (const category of TOKEN_CATEGORIES) for (const token of tokens) {
6124
+ const path = token.category === category ? paths.get(token) : void 0;
6125
+ if (!path) continue;
6126
+ const own = typed(token.value, token, lookup);
6127
+ const target = token.aliasOf ? byName.get(token.aliasOf) : void 0;
6128
+ const targetPath = target ? paths.get(target) : void 0;
6129
+ const node = {};
6130
+ const type = target && targetPath ? typed(target.value, target, lookup).type : own.type;
6131
+ if (type) node.$type = type;
6132
+ node.$value = targetPath ? `{${targetPath.join(".")}}` : own.value;
6133
+ if (token.description) node.$description = token.description;
6134
+ if (token.deprecated) node.$deprecated = token.deprecated;
6135
+ const extension = {};
6136
+ if (token.name !== path.join(".")) extension.name = token.name;
6137
+ if (token.cssVar) extension.cssVar = token.cssVar;
6138
+ const tailwind = tailwindVariable(token);
6139
+ if (tailwind) extension.tailwind = tailwind;
6140
+ if (own.channels) extension.channels = "hsl";
6141
+ if (token.modes && Object.keys(token.modes).length) extension.modes = Object.fromEntries(Object.entries(token.modes).map(([mode, value]) => [mode, typed(value, token, lookup).value]));
6142
+ if (Object.keys(extension).length) node.$extensions = { [ONSYSTEM_EXTENSION]: extension };
6143
+ insert(tree, path, node);
6144
+ count++;
6145
+ }
6146
+ return {
6147
+ text: `${JSON.stringify(tree, null, 2).replace(/\[\n\s*([^[\]{}]*?)\n\s*\]/g, (_, items) => `[${items.split(/,\n\s*/).join(", ")}]`)}\n`,
6148
+ count,
6149
+ warnings
6150
+ };
6151
+ }
6152
+ /**
6153
+ * Each token's path: its category, then its name without that category (`primary` →
6154
+ * `color.primary`, `radius-md` → `radius.md`, `color.brand.500` as it is). The spacing unit,
6155
+ * named like its group, is the group's `$root`; so is a token whose path is also a group.
6156
+ */
6157
+ function assignPaths(tokens, warnings) {
6158
+ const out = /* @__PURE__ */ new Map();
6159
+ const taken = /* @__PURE__ */ new Set();
6160
+ const key = (path) => JSON.stringify(path);
6161
+ for (const token of tokens) {
6162
+ const path = [[token.category, ...leaf(token)], [token.category, ...token.name.split(".")]].find((p) => !taken.has(key(p)));
6163
+ if (!path) {
6164
+ warnings.push(`${token.name} is left out: another token has the same path`);
6165
+ continue;
6166
+ }
6167
+ taken.add(key(path));
6168
+ out.set(token, path);
6169
+ }
6170
+ const groups = /* @__PURE__ */ new Set();
6171
+ for (const path of out.values()) for (let i = 1; i < path.length; i++) groups.add(key(path.slice(0, i)));
6172
+ for (const [token, path] of out) {
6173
+ if (!groups.has(key(path))) continue;
6174
+ const root = [...path, "$root"];
6175
+ if (taken.has(key(root))) {
6176
+ warnings.push(`${token.name} is left out: another token has the same path`);
6177
+ out.delete(token);
6178
+ continue;
6179
+ }
6180
+ taken.add(key(root));
6181
+ out.set(token, root);
6182
+ }
6183
+ return out;
6184
+ }
6185
+ /** A token's path within its category group. */
6186
+ function leaf(token) {
6187
+ const group = token.category;
6188
+ if (token.name.includes(".")) {
6189
+ const parts = token.name.split(".");
6190
+ const [first = ""] = parts;
6191
+ return parts.length > 1 && GROUP_NAMES[group].includes(first.toLowerCase()) ? parts.slice(1) : parts;
6192
+ }
6193
+ if (token.name.startsWith(`${group}-`)) return [token.name.slice(group.length + 1)];
6194
+ if (token.name !== group) return [token.name];
6195
+ return [token.tailwind || "$root"];
6196
+ }
6197
+ function insert(tree, path, node) {
6198
+ let group = tree;
6199
+ for (const key of path.slice(0, -1)) {
6200
+ const next = group[key];
6201
+ if (isObject$1(next)) group = next;
6202
+ else {
6203
+ const created = {};
6204
+ group[key] = created;
6205
+ group = created;
6206
+ }
6207
+ }
6208
+ group[path.at(-1) ?? ""] = node;
6209
+ }
6210
+ /**
6211
+ * The Tailwind theme variable a token is exposed through: `--color-primary` for `bg-primary`,
6212
+ * `--spacing` for the spacing unit. Tailwind v3 colors come from its config, and read the same.
6213
+ */
6214
+ function tailwindVariable(token) {
6215
+ const key = token.tailwind;
6216
+ if (key === void 0) return void 0;
6217
+ const utility = token.usage.find((u) => !u.includes("(")) ?? "";
6218
+ switch (token.category) {
6219
+ case "color": return `--color-${key}`;
6220
+ case "spacing": return key ? `--spacing-${key}` : "--spacing";
6221
+ case "radius": return `--radius-${key}`;
6222
+ case "shadow": return `--${/^(inset-shadow|drop-shadow|text-shadow|shadow)-/.exec(utility)?.[1] ?? "shadow"}-${key}`;
6223
+ case "typography": {
6224
+ const kind = typographyKind(token);
6225
+ if (kind === "fontWeight") return `--font-weight-${key}`;
6226
+ if (kind === "fontFamily") return `--font-${key}`;
6227
+ if (kind === "lineHeight") return `--leading-${key}`;
6228
+ if (kind === "letterSpacing") return `--tracking-${key}`;
6229
+ if (kind === "fontSize") return `--text-${key}`;
6230
+ return;
6231
+ }
6232
+ default: return;
6233
+ }
6234
+ }
6235
+ /** Custom property → value, for lengths that read another token (`calc(var(--radius) - 4px)`). */
6236
+ function varLookup(tokens) {
6237
+ const values = /* @__PURE__ */ new Map();
6238
+ for (const token of tokens) values.set(token.cssVar ?? `--${token.name}`, token.value);
6239
+ return (name) => values.get(name);
6240
+ }
6241
+ /**
6242
+ * A CSS value in DTCG form. Values DTCG has no type for (`color-mix()`, `var()` without a
6243
+ * token, lengths in `em` or `%`) stay strings without `$type`, which onsystem reads back as is.
6244
+ */
6245
+ function typed(value, token, lookup) {
6246
+ switch (token.category) {
6247
+ case "color": return colorValue(value);
6248
+ case "spacing":
6249
+ case "radius": return dimensionValue(value, lookup) ?? { value };
6250
+ case "typography": return typographyValue(value, token) ?? { value };
6251
+ case "shadow": {
6252
+ const shadow = shadowValue(value);
6253
+ return shadow ? {
6254
+ type: "shadow",
6255
+ value: shadow
6256
+ } : { value };
6257
+ }
6258
+ default: return { value };
6259
+ }
6260
+ }
6261
+ function colorValue(value) {
6262
+ if (!parseColor(value, { bareHsl: true })) return { value };
6263
+ const bare = isBareHsl(value);
6264
+ const color = colorObject(bare ? `hsl(${value.replace(/,/g, " ")})` : value);
6265
+ if (!color) return { value };
6266
+ return bare ? {
6267
+ type: "color",
6268
+ value: color,
6269
+ channels: true
6270
+ } : {
6271
+ type: "color",
6272
+ value: color
6273
+ };
6274
+ }
6275
+ /** culori mode → DTCG color space, its channels, and the factor from culori's range to DTCG's. */
6276
+ const COLOR_SPACES = {
6277
+ rgb: {
6278
+ space: "srgb",
6279
+ channels: [
6280
+ "r",
6281
+ "g",
6282
+ "b"
6283
+ ]
6284
+ },
6285
+ lrgb: {
6286
+ space: "srgb-linear",
6287
+ channels: [
6288
+ "r",
6289
+ "g",
6290
+ "b"
6291
+ ]
6292
+ },
6293
+ hsl: {
6294
+ space: "hsl",
6295
+ channels: [
6296
+ "h",
6297
+ "s",
6298
+ "l"
6299
+ ],
6300
+ scale: [
6301
+ 1,
6302
+ 100,
6303
+ 100
6304
+ ]
6305
+ },
6306
+ hwb: {
6307
+ space: "hwb",
6308
+ channels: [
6309
+ "h",
6310
+ "w",
6311
+ "b"
6312
+ ],
6313
+ scale: [
6314
+ 1,
6315
+ 100,
6316
+ 100
6317
+ ]
6318
+ },
6319
+ lab: {
6320
+ space: "lab",
6321
+ channels: [
6322
+ "l",
6323
+ "a",
6324
+ "b"
6325
+ ]
6326
+ },
6327
+ lch: {
6328
+ space: "lch",
6329
+ channels: [
6330
+ "l",
6331
+ "c",
6332
+ "h"
6333
+ ]
6334
+ },
6335
+ oklab: {
6336
+ space: "oklab",
6337
+ channels: [
6338
+ "l",
6339
+ "a",
6340
+ "b"
6341
+ ]
6342
+ },
6343
+ oklch: {
6344
+ space: "oklch",
6345
+ channels: [
6346
+ "l",
6347
+ "c",
6348
+ "h"
6349
+ ]
6350
+ },
6351
+ p3: {
6352
+ space: "display-p3",
6353
+ channels: [
6354
+ "r",
6355
+ "g",
6356
+ "b"
6357
+ ]
6358
+ },
6359
+ a98: {
6360
+ space: "a98-rgb",
6361
+ channels: [
6362
+ "r",
6363
+ "g",
6364
+ "b"
6365
+ ]
6366
+ },
6367
+ prophoto: {
6368
+ space: "prophoto-rgb",
6369
+ channels: [
6370
+ "r",
6371
+ "g",
6372
+ "b"
6373
+ ]
6374
+ },
6375
+ rec2020: {
6376
+ space: "rec2020",
6377
+ channels: [
6378
+ "r",
6379
+ "g",
6380
+ "b"
6381
+ ]
6382
+ },
6383
+ xyz65: {
6384
+ space: "xyz-d65",
6385
+ channels: [
6386
+ "x",
6387
+ "y",
6388
+ "z"
6389
+ ]
6390
+ },
6391
+ xyz50: {
6392
+ space: "xyz-d50",
6393
+ channels: [
6394
+ "x",
6395
+ "y",
6396
+ "z"
6397
+ ]
6398
+ }
6399
+ };
6400
+ const toOklch = converter("oklch");
6401
+ /**
6402
+ * A CSS color as a DTCG color object, in the color space it is written in (others in OKLCH).
6403
+ * An 8-bit sRGB color also gets its `hex`.
4277
6404
  */
4278
- function roleKey(token) {
4279
- const key = tokenKey(token);
4280
- return key === "sidebar" ? "background" : key.replace(/^sidebar-/, "");
6405
+ function colorObject(css) {
6406
+ const parsed = parse(css);
6407
+ if (!parsed) return void 0;
6408
+ const known = COLOR_SPACES[parsed.mode];
6409
+ const color = known ? parsed : toOklch(parsed);
6410
+ const spec = known ?? {
6411
+ space: "oklch",
6412
+ channels: [
6413
+ "l",
6414
+ "c",
6415
+ "h"
6416
+ ]
6417
+ };
6418
+ const channels = color;
6419
+ const components = spec.channels.map((channel, i) => {
6420
+ const v = channels[channel];
6421
+ return v === void 0 || Number.isNaN(v) ? "none" : round(v * (spec.scale?.[i] ?? 1));
6422
+ });
6423
+ const out = {
6424
+ colorSpace: spec.space,
6425
+ components
6426
+ };
6427
+ const alpha = color.alpha;
6428
+ if (alpha !== void 0 && alpha < 1) out.alpha = round(alpha);
6429
+ else if (spec.space === "srgb" && components.every((c) => typeof c === "number" && Math.abs(c * 255 - Math.round(c * 255)) < .001)) out.hex = formatHex(color);
6430
+ return out;
4281
6431
  }
4282
- /** What a token is for, from its name: shadcn/ui's conventions and common DTCG names. */
4283
- function kindOf(key) {
4284
- const segments = key.split("-");
4285
- for (const [kind, words] of KIND_WORDS) if (segments.some((s) => words.includes(s))) return kind;
4286
- return "other";
6432
+ const PLAIN_LENGTH = /^(-?(?:\d+\.?\d*|\.\d+))(px|rem)$/;
6433
+ /** `0.5rem` → `{ value: 0.5, unit: "rem" }`; a `calc()` of tokens → its px value. */
6434
+ function dimensionValue(value, lookup) {
6435
+ const plain = plainDimension(value);
6436
+ if (plain) return {
6437
+ type: "dimension",
6438
+ value: plain
6439
+ };
6440
+ const px = evaluateLength(value, lookup);
6441
+ if (px === void 0 || !Number.isFinite(px)) return void 0;
6442
+ return {
6443
+ type: "dimension",
6444
+ value: {
6445
+ value: round(px),
6446
+ unit: "px"
6447
+ }
6448
+ };
4287
6449
  }
4288
- /** Tokens made for each role, in the order to prefer them when their values tie. */
4289
- const PREFERRED = {
4290
- text: ["foreground", "muted-foreground"],
4291
- surface: [
4292
- "background",
4293
- "muted",
4294
- "secondary",
4295
- "accent",
4296
- "card",
4297
- "popover"
4298
- ],
4299
- line: [
4300
- "border",
4301
- "input",
4302
- "ring"
4303
- ]
4304
- };
4305
- /** Plain text colors, as opposed to the text that goes on one surface (`card-foreground`). */
4306
- const GENERIC_TEXT = /^(?:muted-foreground|(?:foreground|text|fg)(?:-(?:default|base|primary|secondary|muted|subtle))?)$/;
4307
- function roleFit(token, role) {
4308
- if (!role) return {
4309
- tier: 1,
4310
- rank: 0
6450
+ function plainDimension(value) {
6451
+ const text = value.trim();
6452
+ if (text === "0") return {
6453
+ value: 0,
6454
+ unit: "px"
4311
6455
  };
4312
- const key = roleKey(token);
4313
- const index = PREFERRED[role].indexOf(key);
4314
- const rank = index === -1 ? PREFERRED[role].length : index;
4315
- const kind = kindOf(key);
4316
- switch (role) {
4317
- case "text":
4318
- if (kind === "foreground") return {
4319
- tier: GENERIC_TEXT.test(key) ? 0 : 1,
4320
- rank
6456
+ const match = PLAIN_LENGTH.exec(text);
6457
+ return match ? {
6458
+ value: Number(match[1]),
6459
+ unit: match[2] ?? "px"
6460
+ } : void 0;
6461
+ }
6462
+ const COMPOSITE = /^[\w-]+\s*:\s*[^;]+(?:;\s*[\w-]+\s*:\s*[^;]+)*;?$/;
6463
+ const NUMBER = /^-?(?:\d+\.?\d*|\.\d+)$/;
6464
+ /** What a typography token sets, from its name and Tailwind class (`font-sans`, `text-sm`). */
6465
+ function typographyKind(token) {
6466
+ const value = token.value.trim();
6467
+ if (COMPOSITE.test(value)) return "composite";
6468
+ const utility = token.usage.find((u) => !u.includes("(")) ?? "";
6469
+ const name = `${token.name} ${utility}`.toLowerCase();
6470
+ if (/weight/.test(name) || utility.startsWith("font-") && /^\d+$/.test(value)) return "fontWeight";
6471
+ if (/leading|line-?height/.test(name)) return "lineHeight";
6472
+ if (/tracking|letter-?spacing/.test(name)) return "letterSpacing";
6473
+ if (/(?:^|[\s.-])text-|size/.test(name)) return "fontSize";
6474
+ if (/font|family/.test(name)) return "fontFamily";
6475
+ }
6476
+ function typographyValue(value, token) {
6477
+ const text = value.trim();
6478
+ switch (typographyKind(token)) {
6479
+ case "fontFamily": return {
6480
+ type: "fontFamily",
6481
+ value: fontFamilies(text)
6482
+ };
6483
+ case "fontWeight": {
6484
+ const n = weight(text);
6485
+ return n === void 0 ? void 0 : {
6486
+ type: "fontWeight",
6487
+ value: n
4321
6488
  };
4322
- return {
4323
- tier: kind === "other" ? 1 : 2,
4324
- rank
6489
+ }
6490
+ case "lineHeight": {
6491
+ if (NUMBER.test(text)) return {
6492
+ type: "number",
6493
+ value: Number(text)
4325
6494
  };
4326
- case "surface": return {
4327
- tier: kind === "surface" ? 0 : kind === "foreground" ? 2 : 1,
4328
- rank
4329
- };
4330
- case "line": return {
4331
- tier: kind === "line" ? 0 : kind === "other" ? 1 : 2,
4332
- rank
6495
+ const length = plainDimension(text);
6496
+ return length && {
6497
+ type: "dimension",
6498
+ value: length
6499
+ };
6500
+ }
6501
+ case "fontSize":
6502
+ case "letterSpacing": {
6503
+ const length = plainDimension(text);
6504
+ return length && {
6505
+ type: "dimension",
6506
+ value: length
6507
+ };
6508
+ }
6509
+ case "composite": return {
6510
+ type: "typography",
6511
+ value: composite(text)
4333
6512
  };
6513
+ default: return;
4334
6514
  }
4335
6515
  }
4336
- const LENGTH = /^(-?(?:\d+\.?\d*|\.\d+))(px|rem|em)?$/i;
4337
- /** `"0.5rem"` → 8, `"12px"` → 12, `"4"` → 4. Returns undefined for anything else. */
4338
- function lengthToPx(value) {
4339
- if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
4340
- const match = LENGTH.exec(value.trim());
4341
- if (!match) return void 0;
4342
- const n = Number(match[1]);
4343
- return (match[2] ?? "px").toLowerCase() === "px" ? n : n * 16;
6516
+ /** `fontFamily: Inter; fontSize: 16px` → `{ fontFamily: "Inter", fontSize: { value: 16, unit: "px" } }`. */
6517
+ function composite(text) {
6518
+ const out = {};
6519
+ for (const [key, value] of pairs(text)) if (key === "fontFamily") out[key] = fontFamilies(value);
6520
+ else if (key === "fontWeight") out[key] = weight(value) ?? value;
6521
+ else if (key === "lineHeight" && NUMBER.test(value)) out[key] = Number(value);
6522
+ else out[key] = plainDimension(value) ?? value;
6523
+ return out;
4344
6524
  }
4345
- function formatPx(px) {
4346
- return `${Number(px.toFixed(2))}px`;
6525
+ /** `fontFamily: Inter; fontSize: 16px` → `[["fontFamily", "Inter"], ["fontSize", "16px"]]`. */
6526
+ function pairs(text) {
6527
+ return text.split(";").flatMap((part) => {
6528
+ const colon = part.indexOf(":");
6529
+ const key = part.slice(0, colon).trim();
6530
+ const value = part.slice(colon + 1).trim();
6531
+ return colon > 0 && key && value ? [[key, value]] : [];
6532
+ });
4347
6533
  }
4348
- /**
4349
- * Evaluates a length that may use `var()` and `calc()` to px:
4350
- * `calc(var(--radius) - 4px)` with `--radius: 0.625rem` → 6.
4351
- */
4352
- function evaluateLength(value, resolve, depth = 0) {
4353
- if (depth > 16) return void 0;
4354
- const expanded = expandVars(value.trim(), resolve, depth);
4355
- if (expanded === void 0) return void 0;
4356
- const direct = lengthToPx(expanded);
4357
- if (direct !== void 0) return direct;
4358
- const calc = /^calc\((.*)\)$/is.exec(expanded);
4359
- return calc?.[1] !== void 0 ? evaluateCalc(calc[1]) : void 0;
6534
+ function weight(value) {
6535
+ const n = Number(value);
6536
+ return /^\d+$/.test(value) && n >= 1 && n <= 1e3 ? n : void 0;
6537
+ }
6538
+ /** `"Geist", ui-sans-serif` → `["Geist", "ui-sans-serif"]`; one family, or a `var()`, stays a string. */
6539
+ function fontFamilies(value) {
6540
+ if (value.includes("var(")) return value;
6541
+ const families = splitTopLevel$1(value, ",").map((f) => f.trim().replace(/^(["'])(.*)\1$/, "$2")).filter(Boolean);
6542
+ return families.length === 1 ? families[0] ?? value : families;
6543
+ }
6544
+ /** `0 1px 2px 0 rgb(0 0 0 / 0.05)` → a DTCG shadow object; several layers → an array of them. */
6545
+ function shadowValue(value) {
6546
+ const layers = [];
6547
+ for (const layer of splitTopLevel$1(value, ",")) {
6548
+ let inset = false;
6549
+ let color;
6550
+ const lengths = [];
6551
+ for (const part of splitTopLevel$1(layer.trim(), " ")) {
6552
+ if (!part) continue;
6553
+ const length = plainDimension(part);
6554
+ if (part === "inset") inset = true;
6555
+ else if (length) lengths.push(length);
6556
+ else if (!color && parseColor(part)) color = colorObject(part);
6557
+ else return void 0;
6558
+ }
6559
+ const [offsetX, offsetY, blur = zero(), spread = zero()] = lengths;
6560
+ if (!color || !offsetX || !offsetY || lengths.length > 4) return void 0;
6561
+ layers.push({
6562
+ color,
6563
+ offsetX,
6564
+ offsetY,
6565
+ blur,
6566
+ spread,
6567
+ ...inset ? { inset: true } : {}
6568
+ });
6569
+ }
6570
+ if (!layers.length) return void 0;
6571
+ return layers.length === 1 ? layers[0] ?? null : layers;
4360
6572
  }
4361
- function expandVars(value, resolve, depth) {
4362
- let out = "";
4363
- let last = 0;
4364
- for (const match of value.matchAll(/var\(\s*(--[\w-]+)\s*(?:,\s*([^)]+))?\)/g)) {
4365
- const [whole, name = "", fallback] = match;
4366
- const resolved = resolve(name) ?? fallback;
4367
- const px = resolved === void 0 ? void 0 : evaluateLength(resolved, resolve, depth + 1);
4368
- if (px === void 0) return void 0;
4369
- out += `${value.slice(last, match.index)}${px}px`;
4370
- last = match.index + whole.length;
6573
+ function zero() {
6574
+ return {
6575
+ value: 0,
6576
+ unit: "px"
6577
+ };
6578
+ }
6579
+ /** Splits on `separator` outside parentheses and quotes. */
6580
+ function splitTopLevel$1(text, separator) {
6581
+ const parts = [];
6582
+ let depth = 0;
6583
+ let quote;
6584
+ let start = 0;
6585
+ for (let i = 0; i < text.length; i++) {
6586
+ const ch = text[i];
6587
+ if (quote) {
6588
+ if (ch === quote) quote = void 0;
6589
+ } else if (ch === "\"" || ch === "'") quote = ch;
6590
+ else if (ch === "(") depth++;
6591
+ else if (ch === ")") depth--;
6592
+ else if (depth === 0 && (separator === " " ? /\s/.test(ch ?? "") : ch === separator)) {
6593
+ parts.push(text.slice(start, i));
6594
+ start = i + 1;
6595
+ }
4371
6596
  }
4372
- return out + value.slice(last);
6597
+ parts.push(text.slice(start));
6598
+ return parts;
4373
6599
  }
4374
- const CALC_TOKEN = /-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)?|[()+\-*/]|calc/gi;
6600
+ function round(n) {
6601
+ return Number(n.toFixed(6));
6602
+ }
6603
+ function isObject$1(value) {
6604
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6605
+ }
6606
+ /** DESIGN.md sections in the spec's order, each with the front matter key it fills. */
6607
+ const SECTIONS = [
6608
+ {
6609
+ category: "color",
6610
+ heading: "Colors",
6611
+ key: "colors",
6612
+ intro: "Use these colors instead of hex, rgb() or Tailwind palette values."
6613
+ },
6614
+ {
6615
+ category: "typography",
6616
+ heading: "Typography",
6617
+ key: "typography",
6618
+ intro: "The font families, sizes and weights of the design system."
6619
+ },
6620
+ {
6621
+ category: "spacing",
6622
+ heading: "Layout",
6623
+ key: "spacing",
6624
+ intro: "Use these spacing steps instead of arbitrary lengths."
6625
+ },
6626
+ {
6627
+ category: "shadow",
6628
+ heading: "Elevation & Depth",
6629
+ intro: "The shadows of the design system."
6630
+ },
6631
+ {
6632
+ category: "radius",
6633
+ heading: "Shapes",
6634
+ key: "rounded",
6635
+ intro: "Use these corner radii instead of arbitrary values."
6636
+ },
6637
+ {
6638
+ category: "other",
6639
+ heading: "Other tokens",
6640
+ intro: "Tokens that are not colors, typography, spacing, shadows or radii."
6641
+ }
6642
+ ];
6643
+ /** The front matter keys, in the order of the spec's schema. */
6644
+ const FRONT_MATTER_ORDER = [
6645
+ "colors",
6646
+ "typography",
6647
+ "rounded",
6648
+ "spacing"
6649
+ ];
4375
6650
  /**
4376
- * Tiny recursive-descent evaluator for `+ - * /` over px/rem/em/unitless
4377
- * numbers. Anything else (`%`, `vw`, `ch`, functions) has no fixed px value,
4378
- * so the whole expression is undefined.
6651
+ * The tokens as a DESIGN.md: front matter with the colors, typography, rounded corners and
6652
+ * spacing in the spec's schema, then a heading per category with a table of its tokens, their
6653
+ * values in each mode and how code uses them.
4379
6654
  */
4380
- function evaluateCalc(expression) {
4381
- if (expression.replace(CALC_TOKEN, "").trim()) return void 0;
4382
- const tokens = expression.match(CALC_TOKEN);
4383
- if (!tokens) return void 0;
4384
- let i = 0;
4385
- const primary = () => {
4386
- const token = tokens[i++];
4387
- if (token === void 0) return void 0;
4388
- if (token.toLowerCase() === "calc") return primary();
4389
- if (token === "(") {
4390
- const v = sum();
4391
- i++;
4392
- return v;
4393
- }
4394
- const px = lengthToPx(token);
4395
- if (px === void 0) return void 0;
4396
- return {
4397
- n: px,
4398
- length: /[a-z]$/i.test(token)
4399
- };
6655
+ function toDesignMd(tokens, options) {
6656
+ const warnings = [];
6657
+ const lookup = varLookup(tokens);
6658
+ const keys = designKeys(tokens);
6659
+ const front = {
6660
+ version: "alpha",
6661
+ name: options.name
4400
6662
  };
4401
- const product = () => {
4402
- let left = primary();
4403
- while (left && (tokens[i] === "*" || tokens[i] === "/")) {
4404
- const op = tokens[i++];
4405
- const right = primary();
4406
- if (!right) return void 0;
4407
- left = op === "*" ? {
4408
- n: left.n * right.n,
4409
- length: left.length || right.length
4410
- } : {
4411
- n: left.n / right.n,
4412
- length: left.length
4413
- };
6663
+ const sections = {};
6664
+ const body = [`# ${options.name}`];
6665
+ const byName = new Map(tokens.map((t) => [t.name, t]));
6666
+ for (const section of SECTIONS) {
6667
+ const list = tokens.filter((t) => t.category === section.category);
6668
+ if (!list.length) continue;
6669
+ if (section.key) {
6670
+ const values = {};
6671
+ for (const token of list) {
6672
+ const key = keys.get(token);
6673
+ if (key === void 0) continue;
6674
+ const target = token.aliasOf ? byName.get(token.aliasOf) : void 0;
6675
+ const targetKey = target?.category === token.category && frontMatterValue(target, section.key, lookup) !== void 0 ? keys.get(target) : void 0;
6676
+ const value = targetKey === void 0 ? frontMatterValue(token, section.key, lookup) : `{${section.key}.${targetKey}}`;
6677
+ if (value === void 0) continue;
6678
+ if (key in values) {
6679
+ warnings.push(`${token.name} is left out of the front matter: another token has its key`);
6680
+ continue;
6681
+ }
6682
+ values[key] = value;
6683
+ }
6684
+ if (Object.keys(values).length) sections[section.key] = values;
4414
6685
  }
4415
- return left;
6686
+ body.push("", `## ${section.heading}`, "", section.intro, "", ...table(list, keys));
6687
+ }
6688
+ for (const key of FRONT_MATTER_ORDER) if (sections[key]) front[key] = sections[key];
6689
+ return {
6690
+ text: `---\n${stringify(front, { lineWidth: 0 })}---\n\n${body.join("\n")}\n`,
6691
+ count: tokens.length,
6692
+ warnings
4416
6693
  };
4417
- const sum = () => {
4418
- let left = product();
4419
- while (left && (tokens[i] === "+" || tokens[i] === "-")) {
4420
- const op = tokens[i++];
4421
- const right = product();
4422
- if (!right) return void 0;
4423
- left = {
4424
- n: op === "+" ? left.n + right.n : left.n - right.n,
4425
- length: true
4426
- };
6694
+ }
6695
+ /** Each token's key in its section: its DTCG path without the group, joined with `-`. */
6696
+ function designKeys(tokens) {
6697
+ const out = /* @__PURE__ */ new Map();
6698
+ for (const token of tokens) {
6699
+ const parts = leaf(token).filter((p) => p !== "$root");
6700
+ out.set(token, parts.join("-") || "base");
6701
+ }
6702
+ return out;
6703
+ }
6704
+ function frontMatterValue(token, key, lookup) {
6705
+ const value = token.value.trim();
6706
+ switch (key) {
6707
+ case "colors":
6708
+ if (!parseColor(value, { bareHsl: true })) return void 0;
6709
+ return isBareHsl(value) ? `hsl(${value})` : value;
6710
+ case "rounded":
6711
+ case "spacing": {
6712
+ if (DESIGN_DIMENSION.test(value) || value === "0") return value;
6713
+ const px = evaluateLength(value, lookup);
6714
+ return px === void 0 || !Number.isFinite(px) ? void 0 : `${round(px)}px`;
4427
6715
  }
4428
- return left;
4429
- };
4430
- const result = sum();
4431
- return result && Number.isFinite(result.n) ? result.n : void 0;
6716
+ case "typography": return designTypography(token);
6717
+ }
6718
+ }
6719
+ /** A DESIGN.md dimension: px, em or rem. */
6720
+ const DESIGN_DIMENSION = /^-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)$/;
6721
+ /**
6722
+ * A typography token as a DESIGN.md typography level: `{ fontFamily: "Inter" }` for
6723
+ * `--font-sans`, every property of a composite. A `var()` is no font name (`next/font` sets
6724
+ * it at runtime), and a value the schema has no form for is left out.
6725
+ */
6726
+ function designTypography(token) {
6727
+ const value = token.value.trim();
6728
+ const kind = typographyKind(token);
6729
+ const entries = kind === "composite" ? pairs(value) : kind ? [[kind, value]] : [];
6730
+ const out = {};
6731
+ for (const [property, v] of entries) if (property === "fontFamily") {
6732
+ if (!v.includes("var(")) out[property] = v;
6733
+ } else if (property === "fontWeight") {
6734
+ const n = weight(v);
6735
+ if (n !== void 0) out[property] = n;
6736
+ } else if (property === "lineHeight") {
6737
+ if (NUMBER.test(v)) out[property] = Number(v);
6738
+ else if (DESIGN_DIMENSION.test(v)) out[property] = v;
6739
+ } else if (property === "fontSize" || property === "letterSpacing") {
6740
+ if (DESIGN_DIMENSION.test(v)) out[property] = v;
6741
+ } else out[property] = v;
6742
+ return Object.keys(out).length ? out : void 0;
6743
+ }
6744
+ /** A Markdown table of tokens: key, value, a column per mode, how code uses it, and the description. */
6745
+ function table(tokens, keys) {
6746
+ const modes = [...new Set(tokens.flatMap((t) => Object.keys(t.modes ?? {})))];
6747
+ const described = tokens.some((t) => t.description);
6748
+ const header = [
6749
+ "Token",
6750
+ "Value",
6751
+ ...modes.map((m) => m.charAt(0).toUpperCase() + m.slice(1)),
6752
+ "Use",
6753
+ ...described ? ["Description"] : []
6754
+ ];
6755
+ const code = (text) => text ? `\`${text}\`` : "";
6756
+ const rows = tokens.map((token) => [
6757
+ code(keys.get(token) ?? token.name),
6758
+ code(token.value),
6759
+ ...modes.map((m) => code(token.modes?.[m])),
6760
+ use(token),
6761
+ ...described ? [token.description ?? ""] : []
6762
+ ]);
6763
+ const line = (cells) => `| ${cells.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`;
6764
+ return [
6765
+ line(header),
6766
+ line(header.map(() => "---")),
6767
+ ...rows.map(line)
6768
+ ];
6769
+ }
6770
+ /** `bg-primary`, `var(--primary)`: the first utility class and the CSS reference. */
6771
+ function use(token) {
6772
+ return [token.usage.find((u) => !u.includes("(")), cssReference(token)].filter((u) => Boolean(u)).map((u) => `\`${u}\``).join(", ");
4432
6773
  }
4433
6774
  //#endregion
4434
6775
  //#region src/tokens/index.ts
@@ -4441,7 +6782,7 @@ async function loadTokens(root, sources) {
4441
6782
  const rel = relativePath(root, source.file);
4442
6783
  let text;
4443
6784
  try {
4444
- text = await fs$1.readFile(source.file, "utf8");
6785
+ text = await fsp.readFile(source.file, "utf8");
4445
6786
  } catch {
4446
6787
  warnings.push(`${rel}: token file not found`);
4447
6788
  continue;
@@ -4453,6 +6794,10 @@ async function loadTokens(root, sources) {
4453
6794
  const result = parseDtcgTokens(text, rel, options);
4454
6795
  all.push(...result.tokens);
4455
6796
  warnings.push(...result.warnings);
6797
+ } else if (ext === ".md" || ext === ".markdown") {
6798
+ const result = parseDesignMd(text, rel);
6799
+ all.push(...result.tokens);
6800
+ warnings.push(...result.warnings);
4456
6801
  } else {
4457
6802
  sheets.push(readCssSheet(text, rel));
4458
6803
  sheetsAt ??= all.length;
@@ -4561,7 +6906,7 @@ var TokenIndex = class {
4561
6906
  ...colorModes(token)
4562
6907
  });
4563
6908
  if (token.tailwind) this.colorKeys.add(token.tailwind);
4564
- } else if (token.category === "spacing" || token.category === "radius") {
6909
+ } else if ((token.category === "spacing" || token.category === "radius") && token.origin !== "library") {
4565
6910
  const px = evaluateLength(token.value, resolve);
4566
6911
  if (px === void 0) continue;
4567
6912
  if (token.category === "spacing" && token.tailwind === "") {
@@ -5142,6 +7487,8 @@ var LintTarget = class {
5142
7487
  moduleDirs = /* @__PURE__ */ new Set();
5143
7488
  /** The files extraction read, when the model says. */
5144
7489
  sources;
7490
+ /** Published libraries the model read from type declarations: `@mui/material`. */
7491
+ libraries;
5145
7492
  /**
5146
7493
  * `resolver` finds the module an import points at and what it exports
5147
7494
  * (`@calcom/ui/components/icon` → `components/icon/index.ts`), so a name from
@@ -5153,13 +7500,14 @@ var LintTarget = class {
5153
7500
  this.config = config;
5154
7501
  this.resolver = resolver;
5155
7502
  const rank = designSystemRank(config.designSystems);
7503
+ this.libraries = new Set(model.components.flatMap((c) => c.library ? [c.library] : []));
5156
7504
  const byRank = [...model.components].sort((a, b) => rank(b) - rank(a));
5157
7505
  for (const component of byRank) for (const name of [component.name, ...component.aliases]) {
5158
7506
  this.components.set(name, component);
5159
7507
  this.named.set(name, [...this.named.get(name) ?? [], component]);
5160
7508
  }
5161
7509
  this.tokens = new TokenIndex(model.tokens, { filesUsing: model.components.length ? familyUsage(model) : void 0 });
5162
- this.componentFiles = new Set(model.components.map((c) => c.source.file));
7510
+ this.componentFiles = new Set(model.components.filter((c) => !c.library).map((c) => c.source.file));
5163
7511
  this.exports = new Set(model.exports);
5164
7512
  for (const component of model.components) {
5165
7513
  if (component.parent) continue;
@@ -5181,7 +7529,7 @@ var LintTarget = class {
5181
7529
  }
5182
7530
  this.importPaths = new Set(model.components.map((c) => c.importPath));
5183
7531
  const pattern = config.importPath?.indexOf("{path}") ?? -1;
5184
- this.importPrefixes = config.importPath ? [pattern === -1 ? `${config.importPath}/` : config.importPath.slice(0, pattern)] : unique([...this.importPaths].filter((p) => !p.startsWith(".")).map((p) => isPackageName(p) ? `${p}/` : p.slice(0, p.lastIndexOf("/") + 1)).filter(Boolean));
7532
+ this.importPrefixes = unique([...config.importPath ? [pattern === -1 ? `${config.importPath}/` : config.importPath.slice(0, pattern)] : [...this.importPaths].filter((p) => !p.startsWith(".")).map((p) => isPackageName(p) ? `${p}/` : p.slice(0, p.lastIndexOf("/") + 1)).filter(Boolean), ...[...this.libraries].map((library) => `${library}/`)]);
5185
7533
  for (const file of this.componentFiles) {
5186
7534
  for (const module of modulePaths(file)) this.componentModules.add(module);
5187
7535
  this.moduleDirs.add(path.posix.dirname(file));
@@ -5306,6 +7654,8 @@ var LintTarget = class {
5306
7654
  /** The component a module exports as `default`, for `import Anything from "..."`. */
5307
7655
  defaultExport(specifier, fromFile = "snippet.tsx") {
5308
7656
  const resolved = specifier.startsWith(".") ? this.resolveRelative(specifier, fromFile) : void 0;
7657
+ const fromLibrary = this.model.components.filter((c) => c.defaultImport === specifier);
7658
+ if (fromLibrary.length === 1) return fromLibrary[0];
5309
7659
  const matches = this.model.components.filter((c) => c.exportName === "default" && (resolved === void 0 ? (!this.config.importPath || this.config.importPath.includes("{path}")) && c.importPath === specifier : modulePaths(c.source.file).includes(resolved)));
5310
7660
  return matches.length === 1 ? matches[0] : void 0;
5311
7661
  }
@@ -5313,6 +7663,14 @@ var LintTarget = class {
5313
7663
  const from = path.isAbsolute(fromFile) ? path.relative(this.model.root, fromFile) : fromFile;
5314
7664
  return path.posix.join(path.posix.dirname(toPosix(from)), stripExtension(specifier));
5315
7665
  }
7666
+ /** The library an import specifier points into (`@mui/material/Button`), if any. */
7667
+ libraryOf(specifier) {
7668
+ for (const library of this.libraries) if (specifier === library || specifier.startsWith(`${library}/`)) return library;
7669
+ }
7670
+ /** The library's own component of that name: `Button` from `@mui/material`, not a wrapper. */
7671
+ libraryComponent(name, library) {
7672
+ return (this.named.get(name) ?? []).find((c) => c.library === library);
7673
+ }
5316
7674
  /** Component names for "did you mean" suggestions. */
5317
7675
  names() {
5318
7676
  return [...this.components.keys()];
@@ -5350,12 +7708,14 @@ function familyUsage(model) {
5350
7708
  }
5351
7709
  /**
5352
7710
  * A component's place among the design systems an app uses: 0 for the most imported, and
5353
- * so on; with one design system, 0 for all.
7711
+ * so on; with one design system, 0 for all. A library's components (MUI) come after them all.
5354
7712
  */
5355
7713
  function designSystemRank(sources) {
5356
- if (!sources || sources.length < 2) return () => 0;
7714
+ const last = (sources?.length ?? 1) + 1;
7715
+ if (!sources || sources.length < 2) return (component) => component.library ? last : 0;
5357
7716
  const ranks = /* @__PURE__ */ new Map();
5358
7717
  return (component) => {
7718
+ if (component.library) return last;
5359
7719
  const file = component.source.file;
5360
7720
  let rank = ranks.get(file);
5361
7721
  if (rank === void 0) {
@@ -5694,6 +8054,7 @@ const noUnknownProp = {
5694
8054
  const resolution = context.resolve(element);
5695
8055
  if (resolution.kind !== "component" || resolution.component.openProps) continue;
5696
8056
  const component = resolution.component;
8057
+ if (component.polymorphic && findAttribute(element, component.polymorphic)) continue;
5697
8058
  const own = component.props.map((p) => p.name);
5698
8059
  const known = new Set(own);
5699
8060
  for (const inherited of component.inherits) for (const name of propSets[inherited.set] ?? []) known.add(name);
@@ -6015,6 +8376,38 @@ function hasLabel(attributes) {
6015
8376
  });
6016
8377
  }
6017
8378
  /**
8379
+ * Whether the element's parent is a component that gives its child an accessible name, as MUI's
8380
+ * `<Tooltip title="Delete">` does with a string title (not a JSX one, and not with
8381
+ * `describeChild`, which makes it a description).
8382
+ */
8383
+ function labelledByParent(context, element) {
8384
+ let node = element.node.parent;
8385
+ while (ts.isParenthesizedExpression(node) || ts.isJsxExpression(node) || ts.isConditionalExpression(node) && node.condition !== element.node || ts.isBinaryExpression(node) && [
8386
+ ts.SyntaxKind.AmpersandAmpersandToken,
8387
+ ts.SyntaxKind.BarBarToken,
8388
+ ts.SyntaxKind.QuestionQuestionToken
8389
+ ].includes(node.operatorToken.kind)) node = node.parent;
8390
+ if (!ts.isJsxElement(node)) return false;
8391
+ const parent = context.analysis.elements.find((e) => e.node === node);
8392
+ if (!parent) return false;
8393
+ const resolution = context.resolve(parent);
8394
+ const labels = resolution.kind === "component" ? resolution.component.labelsChild : void 0;
8395
+ if (!labels) return false;
8396
+ const unless = labels.unless ? findAttribute(parent, labels.unless) : void 0;
8397
+ if (unless) {
8398
+ const init = unless.initializer;
8399
+ if ((init && ts.isJsxExpression(init) ? init.expression : init)?.kind !== ts.SyntaxKind.FalseKeyword) return false;
8400
+ }
8401
+ const label = findAttribute(parent, labels.prop);
8402
+ if (!label?.initializer) return false;
8403
+ if (ts.isStringLiteral(label.initializer)) return label.initializer.text.trim() !== "";
8404
+ const value = ts.isJsxExpression(label.initializer) ? label.initializer.expression : void 0;
8405
+ if (!value) return false;
8406
+ const literals = literalValues(label);
8407
+ if (literals.length) return literals.some((l) => l.text.trim() !== "");
8408
+ return !ts.isJsxElement(value) && !ts.isJsxSelfClosingElement(value) && !ts.isJsxFragment(value);
8409
+ }
8410
+ /**
6018
8411
  * The element whose `render` prop this one is, as in Base UI's
6019
8412
  * `<Dialog.Close render={<Button size="icon" />}><XIcon /></Dialog.Close>`:
6020
8413
  * the rendered button takes the host's children and attributes. Also through
@@ -6075,6 +8468,7 @@ const iconButtonAccessibleName = {
6075
8468
  if (host === "object") continue;
6076
8469
  if (host && (isButton(context, host) || host.hasSpread)) continue;
6077
8470
  if (hasLabel(element.attributes) || host && hasLabel(host.attributes)) continue;
8471
+ if (labelledByParent(context, element)) continue;
6078
8472
  const icons = [];
6079
8473
  const children = host ? [...host.children, ...element.children] : element.children;
6080
8474
  const content = host && !children.length ? "unknown" : classify(children, context, icons);
@@ -6259,6 +8653,7 @@ function reportColor(context, input) {
6259
8653
  replacement = cls ? `className="${cls}"` : cssVar;
6260
8654
  advice = cls ? `\`${cls}\` in className` : void 0;
6261
8655
  }
8656
+ if (!cls && !cssVar && token.usage.length) advice = token.usage.map((usage) => `\`${usage}\``).join(" or ");
6262
8657
  const same = sameValue.length ? `, same value as ${sameValue.map((t) => t.name).join(", ")}` : "";
6263
8658
  const inMode = mode && token.modes?.[mode] !== void 0 ? ` in ${mode} mode` : "";
6264
8659
  const found = `${token.name}${inMode} (${formatDeltaE(nearest.distance)}${same})`;
@@ -6362,7 +8757,7 @@ function lengthRule(id, description, spec) {
6362
8757
  const negative = utility.negative !== px < 0;
6363
8758
  if (negative && !NEGATIVE_PREFIXES.has(utility.prefix)) continue;
6364
8759
  const replacement = lengthReplacement(context, spec.category, px, utility.prefix);
6365
- if (!replacement) continue;
8760
+ if (!replacement || unnamed(replacement)) continue;
6366
8761
  const fixed = withBase({
6367
8762
  ...utility,
6368
8763
  negative
@@ -6395,6 +8790,7 @@ function lengthRule(id, description, spec) {
6395
8790
  const negative = px < 0;
6396
8791
  if (negative && !NEGATIVE_PREFIXES.has(prefix)) continue;
6397
8792
  const replacement = values.length === 1 ? lengthReplacement(context, spec.category, px, prefix) : void 0;
8793
+ if (replacement && unnamed(replacement)) continue;
6398
8794
  const cls = replacement && `${negative ? "-" : ""}${replacement.cls}`;
6399
8795
  const cssVar = replacement?.cssVar && (negative ? `calc(${replacement.cssVar} * -1)` : replacement.cssVar);
6400
8796
  const shown = number !== void 0 ? `${name}: ${raw}` : `${name}: "${raw}"`;
@@ -6415,6 +8811,13 @@ function lengthRule(id, description, spec) {
6415
8811
  }
6416
8812
  };
6417
8813
  }
8814
+ /**
8815
+ * A value on the scale whose token code cannot name, with neither a Tailwind class nor a
8816
+ * custom property (a DESIGN.md's `spacing.md`): there is nothing better to write.
8817
+ */
8818
+ function unnamed(replacement) {
8819
+ return replacement.exact && !replacement.tailwind && !replacement.cssVar;
8820
+ }
6418
8821
  /** The nearest step is "far" when it is off by more than half the value and more than 4px. */
6419
8822
  const FAR_RATIO = .5;
6420
8823
  const FAR_PX = 4;
@@ -6693,7 +9096,7 @@ function isObject(value) {
6693
9096
  var name = "onsystem";
6694
9097
  //#endregion
6695
9098
  //#region src/version.ts
6696
- const VERSION = "0.5.0";
9099
+ const VERSION = "0.6.0";
6697
9100
  const NAME = name;
6698
9101
  //#endregion
6699
9102
  //#region src/lint/sarif.ts
@@ -7379,17 +9782,25 @@ var DesignSystem = class {
7379
9782
  config;
7380
9783
  lint;
7381
9784
  searchIndex;
9785
+ /**
9786
+ * The components the tools offer: all of them, except a library's (MUI's) where the project
9787
+ * has its own of that name, a wrapper the team means to be used. The rules still check code
9788
+ * that imports the library's directly against the library's.
9789
+ */
9790
+ offered;
7382
9791
  constructor(model, config) {
7383
9792
  this.model = model;
7384
9793
  this.config = config;
7385
9794
  this.lint = new LintTarget(model, config, new ModuleResolver(config));
7386
- this.searchIndex = buildSearchIndex(model.components);
9795
+ const own = new Set(model.components.filter((c) => !c.library).flatMap((c) => [c.name, ...c.aliases]));
9796
+ this.offered = model.components.filter((c) => !c.library || !(own.has(c.name) || c.parent !== void 0 && own.has(c.parent)));
9797
+ this.searchIndex = buildSearchIndex(this.offered);
7387
9798
  }
7388
9799
  get root() {
7389
9800
  return this.model.root;
7390
9801
  }
7391
9802
  get components() {
7392
- return this.model.components;
9803
+ return this.offered;
7393
9804
  }
7394
9805
  get tokens() {
7395
9806
  return this.model.tokens;
@@ -7399,7 +9810,7 @@ var DesignSystem = class {
7399
9810
  }
7400
9811
  /** Top-level components, i.e. not parts of another component. */
7401
9812
  roots() {
7402
- return this.model.components.filter((c) => !c.parent);
9813
+ return this.offered.filter((c) => !c.parent);
7403
9814
  }
7404
9815
  /** Looks a component up by name, alias, `Card.Header`/`CardHeader` or case-insensitively. */
7405
9816
  getComponent(name) {
@@ -7407,7 +9818,7 @@ var DesignSystem = class {
7407
9818
  if (exact) return exact;
7408
9819
  const key = (n) => n.replace(/\./g, "").toLowerCase();
7409
9820
  const wanted = key(name);
7410
- return this.model.components.find((c) => key(c.name) === wanted || c.aliases.some((a) => key(a) === wanted));
9821
+ return this.offered.find((c) => key(c.name) === wanted || c.aliases.some((a) => key(a) === wanted));
7411
9822
  }
7412
9823
  suggestComponent(name) {
7413
9824
  return closest(name, this.lint.names(), .5);
@@ -7438,6 +9849,16 @@ var DesignSystem = class {
7438
9849
  return [...found.values()];
7439
9850
  }
7440
9851
  /**
9852
+ * Whether `check --require-design-system` (and the GitHub Action, where it is on by default)
9853
+ * finds a design system to check against: components and color tokens, or components from a
9854
+ * published library (MUI). A library is the design system whatever its theme: when the theme's
9855
+ * colors cannot be read (a palette from Sass variables), the check still runs and `notice()`
9856
+ * says that no-hardcoded-color did not.
9857
+ */
9858
+ hasDesignSystem() {
9859
+ return !this.notice() || this.model.components.some((c) => c.library !== void 0);
9860
+ }
9861
+ /**
7441
9862
  * What the rules could not check, when it matters: without components the
7442
9863
  * component rules find nothing, and without color tokens neither does
7443
9864
  * no-hardcoded-color, so a clean result says little. Undefined when both exist.
@@ -7476,17 +9897,18 @@ async function find(config, patterns) {
7476
9897
  absolute: true,
7477
9898
  dot: false
7478
9899
  };
9900
+ const exclude = config.designSystemExclude ?? config.exclude;
7479
9901
  const named = patterns.filter((p) => p.includes("node_modules"));
7480
9902
  const outside = patterns.filter((p) => !p.includes("node_modules") && p.startsWith("../"));
7481
9903
  const rest = patterns.filter((p) => !p.includes("node_modules") && !p.startsWith("../"));
7482
9904
  return unique((await Promise.all([
7483
9905
  rest.length ? glob(rest, {
7484
9906
  ...options,
7485
- ignore: unique(["**/node_modules/**", ...config.exclude])
9907
+ ignore: unique(["**/node_modules/**", ...exclude])
7486
9908
  }) : [],
7487
9909
  named.length ? glob(named, {
7488
9910
  ...options,
7489
- ignore: config.exclude.filter((e) => !e.includes("node_modules"))
9911
+ ignore: exclude.filter((e) => !/^(?:\*\*\/)?node_modules(?:\/\*\*)?\/?$/.test(e))
7490
9912
  }) : [],
7491
9913
  ...outside.map((pattern) => {
7492
9914
  const base = globBase(pattern);
@@ -7496,8 +9918,8 @@ async function find(config, patterns) {
7496
9918
  cwd: path.resolve(config.root, base),
7497
9919
  ignore: unique([
7498
9920
  "**/node_modules/**",
7499
- ...config.exclude.filter((e) => e.startsWith("**/")),
7500
- ...config.exclude.filter((e) => e.startsWith(prefix)).map((e) => e.slice(prefix.length))
9921
+ ...exclude.filter((e) => e.startsWith("**/")),
9922
+ ...exclude.filter((e) => e.startsWith(prefix)).map((e) => e.slice(prefix.length))
7501
9923
  ])
7502
9924
  });
7503
9925
  })
@@ -7516,14 +9938,18 @@ async function resolveFiles(config) {
7516
9938
  const docs = await find(config, config.docs);
7517
9939
  const tokens = [];
7518
9940
  const seen = /* @__PURE__ */ new Set();
7519
- for (const source of config.tokens) for (const file of await find(config, [source.path])) {
7520
- if (seen.has(file)) continue;
7521
- seen.add(file);
7522
- tokens.push({
7523
- file,
7524
- prefix: source.prefix
7525
- });
7526
- }
9941
+ const add = async (sources) => {
9942
+ for (const source of sources) for (const file of await find(config, [source.path])) {
9943
+ if (seen.has(file)) continue;
9944
+ seen.add(file);
9945
+ tokens.push({
9946
+ file,
9947
+ prefix: source.prefix
9948
+ });
9949
+ }
9950
+ };
9951
+ await add(config.tokens.filter((t) => !t.fallback));
9952
+ if (!tokens.length) await add(config.tokens.filter((t) => t.fallback));
7527
9953
  return {
7528
9954
  components,
7529
9955
  tokens,
@@ -7541,7 +9967,7 @@ var FileStats = class {
7541
9967
  return Promise.all(files.map((file) => {
7542
9968
  let stat = this.stats.get(file);
7543
9969
  if (!stat) {
7544
- stat = fs$1.stat(file).then((s) => `${relativePath(this.root, file)}:${s.size}:${Math.round(s.mtimeMs)}`, () => `${file}:missing`);
9970
+ stat = fsp.stat(file).then((s) => `${relativePath(this.root, file)}:${s.size}:${Math.round(s.mtimeMs)}`, () => `${file}:missing`);
7545
9971
  this.stats.set(file, stat);
7546
9972
  }
7547
9973
  return stat;
@@ -7597,7 +10023,7 @@ async function buildModel(config, options = {}) {
7597
10023
  const cachePath = cacheFile(config.root);
7598
10024
  let cached;
7599
10025
  if (useCache && !options.oldProgram) try {
7600
- cached = JSON.parse(await fs$1.readFile(cachePath, "utf8"));
10026
+ cached = JSON.parse(await fsp.readFile(cachePath, "utf8"));
7601
10027
  } catch {}
7602
10028
  const known = Array.isArray(cached?.dependencies) ? cached.dependencies : options.oldProgram ? projectFiles(options.oldProgram) : [];
7603
10029
  const stats = new FileStats(config.root);
@@ -7622,24 +10048,36 @@ async function buildModel(config, options = {}) {
7622
10048
  const docs = [];
7623
10049
  const warnings = [...extracted.warnings];
7624
10050
  for (const file of files.docs) try {
7625
- docs.push(parseDoc(await fs$1.readFile(file, "utf8"), relativePath(config.root, file)));
10051
+ docs.push(parseDoc(await fsp.readFile(file, "utf8"), relativePath(config.root, file)));
7626
10052
  } catch (error) {
7627
10053
  warnings.push(`${relativePath(config.root, file)}: ${error.message}`);
7628
10054
  }
7629
10055
  warnings.push(...attachDocs(extracted.components, docs));
7630
10056
  const tokens = await loadTokens(config.root, files.tokens);
7631
10057
  warnings.push(...tokens.warnings);
10058
+ const libraries = await loadLibraries(config);
10059
+ warnings.push(...libraries.warnings);
10060
+ tokens.tokens.push(...libraries.tokens);
7632
10061
  const tailwind = config.tailwindConfig ? readTailwindColors(config.tailwindConfig, moduleResolver(config.root)) : void 0;
7633
10062
  if (tailwind) applyTailwindColors(tokens.tokens, tailwind.colors);
7634
- if (!files.components.length) warnings.push(`No component files matched ${config.components.join(", ")} under ${config.root}`);
7635
- if (!files.tokens.length) warnings.push("No token files found; token rules are disabled.");
10063
+ let radiiFiles = [];
10064
+ if (config.tailwindConfig) {
10065
+ const radii = readTailwindRadii(config.tailwindConfig, moduleResolver(config.root));
10066
+ applyTailwindRadii(tokens.tokens, radii.radii, config.tailwindConfig);
10067
+ radiiFiles = radii.files;
10068
+ }
10069
+ if (!files.components.length && !libraries.components.length) warnings.push(`No component files matched ${config.components.join(", ")} under ${config.root}`);
10070
+ if (!files.tokens.length && !libraries.tokens.length) warnings.push("No token files found; token rules are disabled.");
7636
10071
  const model = {
7637
10072
  version: 1,
7638
10073
  root: config.root,
7639
- components: extracted.components,
10074
+ components: [...extracted.components, ...libraries.components],
7640
10075
  tokens: tokens.tokens,
7641
- propSets: extracted.propSets,
7642
- exports: extracted.exports,
10076
+ propSets: {
10077
+ ...extracted.propSets,
10078
+ ...libraries.propSets
10079
+ },
10080
+ exports: unique([...extracted.exports, ...libraries.exports]).sort(),
7643
10081
  warnings,
7644
10082
  sources: files.components.map((file) => relativePath(config.root, file)),
7645
10083
  stats: {
@@ -7652,11 +10090,16 @@ async function buildModel(config, options = {}) {
7652
10090
  fromCache: false
7653
10091
  }
7654
10092
  };
7655
- const dependencies = unique([...extracted.dependencies, ...tailwind?.files ?? []]);
10093
+ const dependencies = unique([
10094
+ ...extracted.dependencies,
10095
+ ...tailwind?.files ?? [],
10096
+ ...radiiFiles,
10097
+ ...libraries.dependencies
10098
+ ]);
7656
10099
  const key = await cacheKey(config, files, dependencies, stats);
7657
10100
  if (useCache) try {
7658
- await fs$1.mkdir(path.dirname(cachePath), { recursive: true });
7659
- await fs$1.writeFile(cachePath, JSON.stringify({
10101
+ await fsp.mkdir(path.dirname(cachePath), { recursive: true });
10102
+ await fsp.writeFile(cachePath, JSON.stringify({
7660
10103
  key,
7661
10104
  dependencies,
7662
10105
  model
@@ -7743,9 +10186,10 @@ var DesignSystemHost = class {
7743
10186
  * `extends` target is picked up.
7744
10187
  */
7745
10188
  watchConfigFiles() {
7746
- const { root, configFile, tsconfig } = this.config;
10189
+ const { root, configFile, tsconfig, designSystemPackage } = this.config;
7747
10190
  const files = [
7748
10191
  configFile,
10192
+ designSystemPackage && path.join(designSystemPackage.dir, "package.json"),
7749
10193
  ...DETECTION_FILES.map((f) => path.join(root, f)),
7750
10194
  path.resolve(root, tsconfig ?? "tsconfig.json"),
7751
10195
  ...readProjectConfig(root, tsconfig).configFiles.filter(isProjectFile)
@@ -7762,8 +10206,8 @@ var DesignSystemHost = class {
7762
10206
  }
7763
10207
  }
7764
10208
  changed(file) {
7765
- const { root, configFile, detected } = this.config;
7766
- if (file === void 0 || file === configFile || path.dirname(file) === root && DETECTION_FILES.includes(path.basename(file)) || detected !== void 0 && this.watchedFiles.has(file)) this.configChanged = true;
10209
+ const { root, configFile, detected, designSystemPackage } = this.config;
10210
+ if (file === void 0 || file === configFile || designSystemPackage !== void 0 && this.watchedFiles.has(file) || path.dirname(file) === root && DETECTION_FILES.includes(path.basename(file)) || detected !== void 0 && this.watchedFiles.has(file)) this.configChanged = true;
7767
10211
  this.schedule();
7768
10212
  }
7769
10213
  close() {
@@ -7799,10 +10243,10 @@ var DesignSystemHost = class {
7799
10243
  }
7800
10244
  /** Re-reads the config, re-watches what it points at and rebuilds. */
7801
10245
  async reloadConfig() {
7802
- const { root, configFile } = this.config;
10246
+ const { root, configFile, designSystemPackage, warnings } = this.config;
7803
10247
  const load = this.options.loadConfig ?? (() => loadConfig({
7804
10248
  root,
7805
- config: configFile
10249
+ config: designSystemPackage ? void 0 : configFile
7806
10250
  }));
7807
10251
  try {
7808
10252
  this.config = await load();
@@ -7810,6 +10254,7 @@ var DesignSystemHost = class {
7810
10254
  (this.options.logger ?? silentLogger).error(`config reload failed: ${error.message}`);
7811
10255
  return;
7812
10256
  }
10257
+ for (const text of this.config.warnings ?? []) if (!warnings?.includes(text)) (this.options.logger ?? silentLogger).warn(text);
7813
10258
  if (this.watchers.length) {
7814
10259
  this.close();
7815
10260
  this.watch();
@@ -7831,11 +10276,13 @@ var DesignSystemHost = class {
7831
10276
  this.dependencies = dependencies ?? [];
7832
10277
  if (this.watchers.length) this.watchConfigFiles();
7833
10278
  const { stats } = model;
7834
- logger.info(`${incremental ? "reloaded" : "loaded"} ${model.components.length} components, ${model.tokens.length} tokens from ${relativePath(process.cwd(), config.root) || "."} in ${stats.durationMs}ms${stats.fromCache ? " (cache)" : ""}`);
10279
+ const roots = ds.roots().length;
10280
+ const parts = ds.components.length - roots;
10281
+ logger.info(`${incremental ? "reloaded" : "loaded"} ${plural$1(roots, "component")}${parts ? ` (+${plural$1(parts, "part")})` : ""}, ${plural$1(model.tokens.length, "token")} from ${relativePath(process.cwd(), config.root) || "."} in ${stats.durationMs}ms${stats.fromCache ? " (cache)" : ""}`);
7835
10282
  for (const warning of model.warnings) logger.warn(warning);
7836
10283
  if (incremental) for (const listener of this.listeners) listener(ds);
7837
10284
  return ds;
7838
10285
  }
7839
10286
  };
7840
10287
  //#endregion
7841
- export { plural$1 as $, parseDtcgTokens as A, DEFAULT_TEST_FOLDERS as B, updateBaseline as C, loadTokens as D, TokenIndex as E, CONFIG_FILES as F, loadConfig as G, RULE_IDS as H, ConfigError as I, detectProject as J, rulesFor as K, DEFAULT_DOCS as L, attachDocs as M, parseDoc as N, mergeTokens as O, extractComponents as P, closest as Q, DEFAULT_SEVERITY as R, readBaseline as S, RULES as T, configJsonSchema as U, LEGACY_CONFIG_FILES as V, defineConfig as W, workspacePackageDirs as X, isWorkspaceRoot as Y, workspacePatterns as Z, VERSION as _, fingerprint as a, toPosix as at, applyBaseline as b, silentLogger as c, searchComponents as d, truncate as et, applyFixes as f, NAME as g, formatDiagnostics as h, componentFiles as i, relativePath as it, parseCssTokens as j, formatPx as k, stderrLogger as l, uncheckedNotice as m, DesignSystemHost as n, isInside as nt, loadDesignSystem as o, checkSource as p, findVariantDefinitions as q, buildModel as r, matchesGlob as rt, tokenFiles as s, DesignSystem as t, unique as tt, buildSearchIndex as u, BASELINE_FILE as v, writeBaseline as w, countBaseline as x, LEGACY_BASELINE_FILE as y, DEFAULT_TEST_EXCLUDE as z };
10288
+ export { findVariantDefinitions as $, toDtcg as A, DEFAULT_DOCS as B, updateBaseline as C, loadTokens as D, TokenIndex as E, attachDocs as F, RULE_IDS as G, DEFAULT_TEST_EXCLUDE as H, parseDoc as I, hasOwnConfig as J, configJsonSchema as K, extractComponents as L, parseDtcgTokens as M, parseDesignMd as N, mergeTokens as O, parseCssTokens as P, formatPx as Q, CONFIG_FILES as R, readBaseline as S, RULES as T, DEFAULT_TEST_FOLDERS as U, DEFAULT_SEVERITY as V, LEGACY_CONFIG_FILES as W, loadConfigWith as X, loadConfig as Y, rulesFor as Z, VERSION as _, relativePath as _t, fingerprint as a, detectProject as at, applyBaseline as b, silentLogger as c, isWorkspaceRoot as ct, searchComponents as d, closest as dt, LIBRARY_IDS as et, applyFixes as f, plural$1 as ft, NAME as g, matchesGlob as gt, formatDiagnostics as h, isInside as ht, componentFiles as i, declaredConfig as it, TOKEN_CATEGORIES as j, toDesignMd as k, stderrLogger as l, workspacePackageDirs as lt, uncheckedNotice as m, unique as mt, DesignSystemHost as n, PACKAGE_FIELD as nt, loadDesignSystem as o, findDesignSystemPackage as ot, checkSource as p, truncate as pt, defineConfig as q, buildModel as r, PackageFinder as rt, tokenFiles as s, isDesignSystemName as st, DesignSystem as t, libraryAdapter as tt, buildSearchIndex as u, workspacePatterns as ut, BASELINE_FILE as v, toPosix as vt, writeBaseline as w, countBaseline as x, LEGACY_BASELINE_FILE as y, ConfigError as z };