onsystem 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ import ts from "typescript";
7
7
  import os from "node:os";
8
8
  import postcss from "postcss";
9
9
  import { clampChroma, colorsNamed, converter, differenceEuclidean, formatHex, parse, wcagContrast } from "culori";
10
- import { createHash } from "node:crypto";
10
+ import { createHash, randomUUID } from "node:crypto";
11
11
  import fsp from "node:fs/promises";
12
12
  import { LineCounter, isMap, isScalar, parseDocument, stringify } from "yaml";
13
13
  import { stemmer } from "stemmer";
@@ -122,6 +122,74 @@ function isProjectFile(file) {
122
122
  return !/[\\/]node_modules[\\/]/.test(file);
123
123
  }
124
124
  //#endregion
125
+ //#region src/util/strings.ts
126
+ /** `alert-dialog` / `alert_dialog` / `alertDialog` → `AlertDialog`. */
127
+ function pascalCase(input) {
128
+ return input.replace(/\.[^.]+$/, "").split(/[^a-zA-Z0-9]+|(?<=[a-z0-9])(?=[A-Z])/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
129
+ }
130
+ /** `AlertDialog` → `["Alert", "Dialog"]`, `isHTMLSafe` → `["is", "HTML", "Safe"]`. */
131
+ function splitWords(input) {
132
+ return input.split(/[^a-zA-Z0-9]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/).filter(Boolean);
133
+ }
134
+ function isPascalCase(name) {
135
+ return /^[A-Z][A-Za-z0-9]*$/.test(name);
136
+ }
137
+ /** Optimal string alignment distance (Levenshtein plus adjacent transpositions). */
138
+ function editDistance(a, b) {
139
+ if (a === b) return 0;
140
+ if (!a.length) return b.length;
141
+ if (!b.length) return a.length;
142
+ const rows = a.length + 1;
143
+ const cols = b.length + 1;
144
+ const d = new Array(rows * cols).fill(0);
145
+ const at = (i, j) => d[i * cols + j] ?? 0;
146
+ for (let i = 0; i < rows; i++) d[i * cols] = i;
147
+ for (let j = 0; j < cols; j++) d[j] = j;
148
+ for (let i = 1; i < rows; i++) for (let j = 1; j < cols; j++) {
149
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
150
+ let v = Math.min(at(i - 1, j) + 1, at(i, j - 1) + 1, at(i - 1, j - 1) + cost);
151
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) v = Math.min(v, at(i - 2, j - 2) + 1);
152
+ d[i * cols + j] = v;
153
+ }
154
+ return at(a.length, b.length);
155
+ }
156
+ /**
157
+ * The candidate closest to `input` by case-insensitive edit distance, if it is
158
+ * close enough to be a plausible typo (at most a third of the length, min 1).
159
+ */
160
+ function closest(input, candidates, maxRatio = 1 / 3) {
161
+ const needle = input.toLowerCase();
162
+ const limit = Math.max(1, Math.floor(needle.length * maxRatio));
163
+ let best;
164
+ let bestScore = Infinity;
165
+ for (const candidate of candidates) {
166
+ const gap = Math.abs(candidate.length - needle.length);
167
+ if (gap > limit || gap >= bestScore) continue;
168
+ const score = editDistance(needle, candidate.toLowerCase());
169
+ if (score < bestScore) {
170
+ best = candidate;
171
+ bestScore = score;
172
+ }
173
+ }
174
+ return bestScore <= limit ? best : void 0;
175
+ }
176
+ function truncate(text, max) {
177
+ return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
178
+ }
179
+ /**
180
+ * `text` as a JSON string, with U+0085, U+2028 and U+2029 escaped too, so it stays on one line
181
+ * (JSON escapes the other line breaks).
182
+ */
183
+ function json(text) {
184
+ return JSON.stringify(text).replace(/[\u0085\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
185
+ }
186
+ function plural$1(count, word, pluralWord = `${word}s`) {
187
+ return `${count} ${count === 1 ? word : pluralWord}`;
188
+ }
189
+ function unique(items) {
190
+ return [...new Set(items)];
191
+ }
192
+ //#endregion
125
193
  //#region src/util/paths.ts
126
194
  function toPosix(p) {
127
195
  return p.split(path.sep).join("/");
@@ -271,6 +339,14 @@ function splitTopLevel$2(text) {
271
339
  function escapeRegExp(text) {
272
340
  return text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
273
341
  }
342
+ /**
343
+ * A file, folder or import path, as written when it holds only the characters of a path (letters,
344
+ * digits and `_ @ $ . + ~ / \ : ( ) [ ] { } # % , = ! -`, as in `app/(site)/[id]/page.tsx`);
345
+ * otherwise, as for a path that holds a space or a line break (git allows one), JSON-quoted.
346
+ */
347
+ function pathOf(file) {
348
+ return /^[\p{L}\p{N}_@$.+~/\\:()[\]{}#%,=!-]+$/u.test(file) ? file : json(file);
349
+ }
274
350
  //#endregion
275
351
  //#region src/package-source.ts
276
352
  /**
@@ -567,67 +643,6 @@ function importedStylesheets(files, resolve) {
567
643
  return out;
568
644
  }
569
645
  //#endregion
570
- //#region src/util/strings.ts
571
- /** `alert-dialog` / `alert_dialog` / `alertDialog` → `AlertDialog`. */
572
- function pascalCase(input) {
573
- return input.replace(/\.[^.]+$/, "").split(/[^a-zA-Z0-9]+|(?<=[a-z0-9])(?=[A-Z])/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
574
- }
575
- /** `AlertDialog` → `["Alert", "Dialog"]`, `isHTMLSafe` → `["is", "HTML", "Safe"]`. */
576
- function splitWords(input) {
577
- return input.split(/[^a-zA-Z0-9]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/).filter(Boolean);
578
- }
579
- function isPascalCase(name) {
580
- return /^[A-Z][A-Za-z0-9]*$/.test(name);
581
- }
582
- /** Optimal string alignment distance (Levenshtein plus adjacent transpositions). */
583
- function editDistance(a, b) {
584
- if (a === b) return 0;
585
- if (!a.length) return b.length;
586
- if (!b.length) return a.length;
587
- const rows = a.length + 1;
588
- const cols = b.length + 1;
589
- const d = new Array(rows * cols).fill(0);
590
- const at = (i, j) => d[i * cols + j] ?? 0;
591
- for (let i = 0; i < rows; i++) d[i * cols] = i;
592
- for (let j = 0; j < cols; j++) d[j] = j;
593
- for (let i = 1; i < rows; i++) for (let j = 1; j < cols; j++) {
594
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
595
- let v = Math.min(at(i - 1, j) + 1, at(i, j - 1) + 1, at(i - 1, j - 1) + cost);
596
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) v = Math.min(v, at(i - 2, j - 2) + 1);
597
- d[i * cols + j] = v;
598
- }
599
- return at(a.length, b.length);
600
- }
601
- /**
602
- * The candidate closest to `input` by case-insensitive edit distance, if it is
603
- * close enough to be a plausible typo (at most a third of the length, min 1).
604
- */
605
- function closest(input, candidates, maxRatio = 1 / 3) {
606
- const needle = input.toLowerCase();
607
- const limit = Math.max(1, Math.floor(needle.length * maxRatio));
608
- let best;
609
- let bestScore = Infinity;
610
- for (const candidate of candidates) {
611
- const gap = Math.abs(candidate.length - needle.length);
612
- if (gap > limit || gap >= bestScore) continue;
613
- const score = editDistance(needle, candidate.toLowerCase());
614
- if (score < bestScore) {
615
- best = candidate;
616
- bestScore = score;
617
- }
618
- }
619
- return bestScore <= limit ? best : void 0;
620
- }
621
- function truncate(text, max) {
622
- return text.length <= max ? text : `${text.slice(0, max - 1).trimEnd()}…`;
623
- }
624
- function plural$1(count, word, pluralWord = `${word}s`) {
625
- return `${count} ${count === 1 ? word : pluralWord}`;
626
- }
627
- function unique(items) {
628
- return [...new Set(items)];
629
- }
630
- //#endregion
631
646
  //#region src/detect.ts
632
647
  const ignore = () => void 0;
633
648
  const COMPONENT_FILE = /\.[jt]sx$/;
@@ -1588,11 +1603,11 @@ const PACKAGE_FIELD = "onsystem";
1588
1603
  function declaredConfig(dir, pkg = readJson$1(path.join(dir, "package.json"))) {
1589
1604
  const value = pkg?.[PACKAGE_FIELD];
1590
1605
  if (value === void 0) return void 0;
1591
- const field = `"${PACKAGE_FIELD}" in ${path.join(dir, "package.json")}`;
1606
+ const field = `"${PACKAGE_FIELD}" in ${pathOf(path.join(dir, "package.json"))}`;
1592
1607
  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".` };
1593
1608
  const file = path.resolve(dir, value);
1594
- if (!isInside(dir, file) || file === path.resolve(dir)) return { problem: `${field} names ${value}, which is outside the package.` };
1595
- if (!isFile(file)) return { problem: `${field} names ${value}, which does not exist.` };
1609
+ if (!isInside(dir, file) || file === path.resolve(dir)) return { problem: `${field} names ${JSON.stringify(value)}, which is outside the package.` };
1610
+ if (!isFile(file)) return { problem: `${field} names ${JSON.stringify(value)}, which does not exist.` };
1596
1611
  return { file };
1597
1612
  }
1598
1613
  /**
@@ -3530,7 +3545,7 @@ function unknownKeys(what, known) {
3530
3545
  if (issue.code !== "unrecognized_keys") return void 0;
3531
3546
  return issue.keys.map((key) => {
3532
3547
  const guess = closest(key, known, .5);
3533
- return `Unknown ${what} "${key}". ${guess ? `Did you mean "${guess}"?` : `Expected one of: ${known.join(", ")}.`}`;
3548
+ return `Unknown ${what} ${JSON.stringify(key)}. ${guess ? `Did you mean "${guess}"?` : `Expected one of: ${known.join(", ")}.`}`;
3534
3549
  }).join("\n");
3535
3550
  };
3536
3551
  }
@@ -3544,7 +3559,7 @@ const ruleSettingSchema = z.union([
3544
3559
  if (typeof issue.input !== "string") return void 0;
3545
3560
  const input = issue.input;
3546
3561
  const guess = SEVERITIES.find((s) => input && (s.startsWith(input) || input.startsWith(s)));
3547
- return `Invalid severity "${input}": use "off", "warn" or "error"${guess ? ` (did you mean "${guess}"?)` : ""}.`;
3562
+ return `Invalid severity ${JSON.stringify(input)}: use "off", "warn" or "error"${guess ? ` (did you mean "${guess}"?)` : ""}.`;
3548
3563
  } });
3549
3564
  const globsSchema = z.union([z.string(), z.array(z.string())]);
3550
3565
  const rulesSchema = z.partialRecord(z.enum(RULE_IDS), ruleSettingSchema, { error: unknownKeys("rule", RULE_IDS) });
@@ -3562,7 +3577,7 @@ const librariesSchema = z.union([z.literal(false), z.array(z.enum(LIBRARY_IDS))]
3562
3577
  const unknown = Array.isArray(issue.input) ? issue.input.find((id) => !LIBRARY_IDS.includes(id)) : void 0;
3563
3578
  if (typeof unknown === "string") {
3564
3579
  const guess = closest(unknown, LIBRARY_IDS, .5);
3565
- return `Unknown library "${unknown}". ${guess ? `Did you mean "${guess}"?` : `Expected one of: ${LIBRARY_IDS.join(", ")}.`}`;
3580
+ return `Unknown library ${JSON.stringify(unknown)}. ${guess ? `Did you mean "${guess}"?` : `Expected one of: ${LIBRARY_IDS.join(", ")}.`}`;
3566
3581
  }
3567
3582
  return `"libraries" is false, to read no library, or a list of the ones to read: ${JSON.stringify(LIBRARY_IDS)}.`;
3568
3583
  } });
@@ -3675,9 +3690,12 @@ async function loadConfigWith(options, context = {}) {
3675
3690
  lookup ??= findDesignSystemPackage(root, finder(root));
3676
3691
  if (!lookup?.problems.length) declared = lookup?.found;
3677
3692
  else if (!configFile) throw declarationError(lookup, root);
3678
- else warnings.push(`${lookup.problems.map((p) => `${p.name}: ${p.problem}`).join("\n")}\nSo ${configFile} is read alone, without the config a design-system package declares. Fix the "${PACKAGE_FIELD}" field in that package, or set "extends": false in ${path.basename(configFile)}.`);
3693
+ else warnings.push(`${problemLines(lookup.problems)}\nSo ${configFile} is read alone, without the config a design-system package declares. Fix the "${PACKAGE_FIELD}" field in that package, or set "extends": false in ${path.basename(configFile)}.`);
3694
+ }
3695
+ for (const { name, problem } of lookup?.skipped ?? []) {
3696
+ const quoted = json(name);
3697
+ warnings.push(`The onsystem config of ${quoted} was skipped: ${problem} ${quoted} is installed in node_modules, so this project is read as if it declared none.`);
3679
3698
  }
3680
- 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.`);
3681
3699
  const pkg = declared ? await readConfig(declared.config, options) : void 0;
3682
3700
  const base = options.base ? path.resolve(cwd, options.base) : root;
3683
3701
  const rebase = (from) => (p) => from === root ? p : toPosix(path.relative(root, path.resolve(from, slashGlob(p)))) || ".";
@@ -3693,7 +3711,7 @@ async function loadConfigWith(options, context = {}) {
3693
3711
  const outside = [];
3694
3712
  const within = (field) => (p) => {
3695
3713
  if (!declared || staysIn(declared.dir, declared.boundary, slashGlob(p))) return true;
3696
- outside.push(`${field} ${p}`);
3714
+ outside.push(`${field} ${json(p)}`);
3697
3715
  return false;
3698
3716
  };
3699
3717
  const pathOf = (t) => typeof t === "string" ? t : t.path;
@@ -3704,7 +3722,7 @@ async function loadConfigWith(options, context = {}) {
3704
3722
  const packageComponents = pkg?.components === void 0 ? void 0 : toArray(pkg.components).filter(within("components")).map(fromPackage);
3705
3723
  const packageTokens = pkg?.tokens === void 0 ? void 0 : toArray(pkg.tokens).filter((t) => within("tokens")(pathOf(t))).map(tokensFrom(fromPackage));
3706
3724
  const packageDocs = pkg?.docs === void 0 ? void 0 : toArray(pkg.docs).filter(within("docs")).map(fromPackage);
3707
- 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(", ")}.`);
3725
+ if (declared && outside.length) warnings.push(`The onsystem config of ${json(declared.name)} names paths outside ${declared.installed ? "the package" : "the workspace"}, which were ignored: ${outside.join(", ")}.`);
3708
3726
  const inherits = pkg !== void 0 && config.components === void 0;
3709
3727
  const configComponents = config.components === void 0 ? packageComponents : toArray(config.components).map(fromBase);
3710
3728
  const configTokens = config.tokens === void 0 ? inherits ? packageTokens : void 0 : toArray(config.tokens).map(tokensFrom(fromBase));
@@ -3811,9 +3829,17 @@ function ownDeclaredConfig(dir) {
3811
3829
  if (declared && "problem" in declared) throw new ConfigError(declared.problem);
3812
3830
  return declared?.file;
3813
3831
  }
3832
+ /**
3833
+ * Each declaration that cannot be read, on a line of its own. The package's name is the
3834
+ * repository's text (any string can name a dependency in package.json), so it is JSON-quoted:
3835
+ * a line break or a sentence in it stays inside the quotes, on that line.
3836
+ */
3837
+ function problemLines(problems) {
3838
+ return problems.map((p) => `${json(p.name)}: ${p.problem}`).join("\n");
3839
+ }
3814
3840
  /** The error for workspace packages whose declaration cannot be read: their team can fix them. */
3815
3841
  function declarationError(lookup, dir) {
3816
- 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.`);
3842
+ return new ConfigError(`${problemLines(lookup.problems)}\nFix the "${PACKAGE_FIELD}" field in that package, or give ${dir} a config of its own (onsystem.config.json), which is read instead.`);
3817
3843
  }
3818
3844
  /**
3819
3845
  * Whether a path or glob in a dependency's config, relative to its folder `dir`, stays in
@@ -4935,6 +4961,8 @@ function dedent(code) {
4935
4961
  }
4936
4962
  //#endregion
4937
4963
  //#region src/libraries/declarations.ts
4964
+ /** A component's name: a capitalised identifier (`Button`, `ButtonBase`, `Unstable_Grid2`). */
4965
+ const COMPONENT_NAME = /^[A-Z][\w$]*$/;
4938
4966
  /** Packages contributing more props than this are summarised, as in source extraction. */
4939
4967
  const MAX_LISTED_INHERITED = 40;
4940
4968
  /**
@@ -5006,9 +5034,9 @@ function extractLibrary(options) {
5006
5034
  const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
5007
5035
  const isDefault = exported.getName() === "default";
5008
5036
  const name = isDefault && subpath && declaration ? ts.getNameOfDeclaration(declaration)?.getText() ?? "default" : exported.getName();
5009
- if (isDefault && !/^[A-Z]/.test(name)) continue;
5037
+ if (isDefault && !COMPONENT_NAME.test(name)) continue;
5010
5038
  exportNames.add(name);
5011
- if (!/^[A-Z]/.test(name)) continue;
5039
+ if (!COMPONENT_NAME.test(name)) continue;
5012
5040
  const known = bySymbol.get(symbol);
5013
5041
  if (known) {
5014
5042
  if (known.name !== name && !known.aliases.includes(name)) known.aliases.push(name);
@@ -10434,7 +10462,7 @@ function maskReferencedGradientIds(elements) {
10434
10462
  for (const element of elements) if (element.tag === "mask") refsFrom(element.node);
10435
10463
  return ids;
10436
10464
  }
10437
- const noHardcodedColor = {
10465
+ const noHardcodedColor = shortQuotes({
10438
10466
  id: "no-hardcoded-color",
10439
10467
  description: "Colors must come from design tokens: no hex/rgb/oklch literals, arbitrary Tailwind colors or default-palette classes.",
10440
10468
  run(context) {
@@ -10645,7 +10673,31 @@ const noHardcodedColor = {
10645
10673
  }
10646
10674
  reportChartTokens(context, allow);
10647
10675
  }
10648
- };
10676
+ });
10677
+ /** The longest code a message quotes. */
10678
+ const QUOTED_CODE = 120;
10679
+ /**
10680
+ * The rule, with each `code` span in its messages cut to QUOTED_CODE characters. A message quotes
10681
+ * the class, its fix and the value whole, so one class with 33,000 `hover:` variants made a
10682
+ * message of 400,000 characters for the agent to read.
10683
+ */
10684
+ function shortQuotes(rule) {
10685
+ const cut = (span, code) => code.length > QUOTED_CODE ? `\`${truncate(code, QUOTED_CODE)}\`` : span;
10686
+ return {
10687
+ ...rule,
10688
+ run(context) {
10689
+ rule.run({
10690
+ ...context,
10691
+ report(report) {
10692
+ context.report({
10693
+ ...report,
10694
+ message: report.message.replace(/`([^`]+)`/g, cut)
10695
+ });
10696
+ }
10697
+ });
10698
+ }
10699
+ };
10700
+ }
10649
10701
  /** Packages that draw charts: Recharts, Chart.js, Nivo, visx, Victory, Tremor, MUI X Charts, D3. */
10650
10702
  const CHART_LIBRARY = /chart|^@nivo\/|^@visx\/|^victory|^@tremor\/|^d3(?:-|$)/i;
10651
10703
  /** Roles of an element that is a drawing: a chart drawn by hand is an image. */
@@ -11234,25 +11286,69 @@ function setsColor(utility, prefix) {
11234
11286
  */
11235
11287
  function keepStatesApart(list) {
11236
11288
  const after = (listed) => listed.report?.fix?.[0]?.text ?? listed.value;
11289
+ const order = new Map(list.map((listed, i) => [listed, i]));
11290
+ const parsed = /* @__PURE__ */ new Map();
11291
+ const parse = (text) => {
11292
+ let utility = parsed.get(text);
11293
+ if (!utility) {
11294
+ utility = parseUtility(text);
11295
+ parsed.set(text, utility);
11296
+ }
11297
+ return utility;
11298
+ };
11299
+ const byText = /* @__PURE__ */ new Map();
11300
+ const byUtility = /* @__PURE__ */ new Map();
11301
+ const utilityKey = (text) => `${parse(text).base}\0${parse(text).modifier}`;
11302
+ const place = (listed) => {
11303
+ const text = after(listed);
11304
+ const same = byText.get(text) ?? [];
11305
+ byText.set(text, same);
11306
+ const index = order.get(listed) ?? 0;
11307
+ same.splice(same.findLastIndex((other) => (order.get(other) ?? 0) < index) + 1, 0, listed);
11308
+ const texts = byUtility.get(utilityKey(text)) ?? /* @__PURE__ */ new Set();
11309
+ byUtility.set(utilityKey(text), texts.add(text));
11310
+ };
11311
+ const unplace = (listed) => {
11312
+ const text = after(listed);
11313
+ const same = byText.get(text) ?? [];
11314
+ same.splice(same.indexOf(listed), 1);
11315
+ if (!same.length) byUtility.get(utilityKey(text))?.delete(text);
11316
+ };
11317
+ for (const listed of list) place(listed);
11237
11318
  for (const pass of ["state", "base"]) for (const listed of list) {
11238
11319
  const { report } = listed;
11239
11320
  if (!report?.fix?.[0]) continue;
11240
- for (const other of list) {
11241
- if (other === listed || oneColor(listed.color, other.color)) continue;
11242
- const [stateListed, baseListed] = pass === "state" ? [listed, other] : [other, listed];
11243
- const state = parseUtility(after(stateListed));
11244
- const base = parseUtility(after(baseListed));
11321
+ const text = after(listed);
11322
+ let found;
11323
+ for (const otherText of byUtility.get(utilityKey(text)) ?? []) {
11324
+ if (otherText === text) continue;
11325
+ const [state, base] = pass === "state" ? [parse(text), parse(otherText)] : [parse(otherText), parse(text)];
11245
11326
  const conditions = conditionsBetween(state, base);
11246
- if (!conditions || hasDarkBase(list, stateListed, baseListed, state, base, after)) continue;
11247
- const had = pass === "base" ? `\`${other.value}\` is in the class list too` : after(other) === other.value ? `The class list already has \`${other.value}\`` : `\`${other.value}\` becomes \`${after(other)}\` too`;
11248
- const apart = state.modifier ? "such as with another opacity modifier" : `such as with \`${withBase({
11249
- ...state,
11250
- modifier: "/90"
11251
- }, state.base)}\``;
11252
- report.message += ` ${had}, so with this fix \`${conditions}\` would change nothing: it is not applied automatically. Keep the two apart, ${apart}.`;
11253
- report.fix = void 0;
11254
- break;
11327
+ if (!conditions) continue;
11328
+ for (const other of byText.get(otherText) ?? []) {
11329
+ if (found && (order.get(other) ?? 0) > (order.get(found.other) ?? 0)) break;
11330
+ if (oneColor(listed.color, other.color)) continue;
11331
+ const [stateListed, baseListed] = pass === "state" ? [listed, other] : [other, listed];
11332
+ if (hasDarkBase(list, stateListed, baseListed, state, base, after)) continue;
11333
+ found = {
11334
+ other,
11335
+ state,
11336
+ conditions
11337
+ };
11338
+ break;
11339
+ }
11255
11340
  }
11341
+ if (!found) continue;
11342
+ const { other, state, conditions } = found;
11343
+ const had = pass === "base" ? `\`${other.value}\` is in the class list too` : after(other) === other.value ? `The class list already has \`${other.value}\`` : `\`${other.value}\` becomes \`${after(other)}\` too`;
11344
+ const apart = state.modifier ? "such as with another opacity modifier" : `such as with \`${withBase({
11345
+ ...state,
11346
+ modifier: "/90"
11347
+ }, state.base)}\``;
11348
+ report.message += ` ${had}, so with this fix \`${conditions}\` would change nothing: it is not applied automatically. Keep the two apart, ${apart}.`;
11349
+ unplace(listed);
11350
+ report.fix = void 0;
11351
+ place(listed);
11256
11352
  }
11257
11353
  }
11258
11354
  /**
@@ -11432,7 +11528,8 @@ function scopes(context, element, classes) {
11432
11528
  if (name && !/^Sidebar(?:Provider|Inset)$/.test(name)) tags.push(name);
11433
11529
  }
11434
11530
  const { tokens, components } = context.target;
11435
- const found = scopesIn(tokens.scopedFamilies, context.file, classes, ...tags);
11531
+ const found = scopesIn(tokens.scopedFamilies, context.file, ...tags);
11532
+ if (classes) for (const family of classScopes(context, classes)) found.add(family);
11436
11533
  const files = /* @__PURE__ */ new Set([context.file]);
11437
11534
  for (const tag of tags) {
11438
11535
  const file = components.get(tag)?.source.file;
@@ -11444,6 +11541,25 @@ function scopes(context, element, classes) {
11444
11541
  }
11445
11542
  return found;
11446
11543
  }
11544
+ /** The scoped families each class string of a check names, by its text. */
11545
+ const classScopeCache = /* @__PURE__ */ new WeakMap();
11546
+ /**
11547
+ * The scoped families a class string names, read once per string and check: each color in it
11548
+ * asks, and reading the whole string again for each made a list of thousands quadratic.
11549
+ */
11550
+ function classScopes(context, classes) {
11551
+ let cache = classScopeCache.get(context.analysis);
11552
+ if (!cache) {
11553
+ cache = /* @__PURE__ */ new Map();
11554
+ classScopeCache.set(context.analysis, cache);
11555
+ }
11556
+ let found = cache.get(classes);
11557
+ if (!found) {
11558
+ found = scopesIn(context.target.tokens.scopedFamilies, classes);
11559
+ cache.set(classes, found);
11560
+ }
11561
+ return found;
11562
+ }
11447
11563
  /** The variant prop of the element's component that already sets `prefix-*` classes, if any. */
11448
11564
  function variantProp(context, element, prefix) {
11449
11565
  const resolution = context.resolve(element);
@@ -11466,7 +11582,7 @@ function numberValue(init) {
11466
11582
  if (ts.isPrefixUnaryExpression(init) && ts.isNumericLiteral(init.operand) && (init.operator === ts.SyntaxKind.MinusToken || init.operator === ts.SyntaxKind.PlusToken)) return `${init.operator === ts.SyntaxKind.MinusToken ? "-" : ""}${init.operand.text}`;
11467
11583
  }
11468
11584
  function lengthRule(id, description, spec) {
11469
- return {
11585
+ return shortQuotes({
11470
11586
  id,
11471
11587
  description,
11472
11588
  run(context) {
@@ -11538,7 +11654,7 @@ function lengthRule(id, description, spec) {
11538
11654
  }
11539
11655
  }
11540
11656
  }
11541
- };
11657
+ });
11542
11658
  }
11543
11659
  /**
11544
11660
  * A value on the scale whose token code cannot name, with neither a Tailwind class nor a
@@ -12021,7 +12137,7 @@ function isObject(value) {
12021
12137
  var name = "onsystem";
12022
12138
  //#endregion
12023
12139
  //#region src/version.ts
12024
- const VERSION = "0.10.0";
12140
+ const VERSION = "0.10.1";
12025
12141
  const NAME = name;
12026
12142
  //#endregion
12027
12143
  //#region src/lint/sarif.ts
@@ -12244,7 +12360,7 @@ function checkSource(code, target, options) {
12244
12360
  try {
12245
12361
  return checkParsed(code, target, options, file);
12246
12362
  } catch (error) {
12247
- if (!isStackOverflow(error)) throw error;
12363
+ if (!isStackOverflow$1(error)) throw error;
12248
12364
  return {
12249
12365
  file,
12250
12366
  diagnostics: [{
@@ -12262,7 +12378,7 @@ function checkSource(code, target, options) {
12262
12378
  };
12263
12379
  }
12264
12380
  }
12265
- function isStackOverflow(error) {
12381
+ function isStackOverflow$1(error) {
12266
12382
  return error instanceof RangeError && /call stack/i.test(error.message);
12267
12383
  }
12268
12384
  function checkParsed(code, target, options, file) {
@@ -12386,7 +12502,7 @@ function uncheckedNotice(results) {
12386
12502
  const list = [...names];
12387
12503
  const shown = list.slice(0, 4).map((n) => `<${n}>`);
12388
12504
  const more = list.length > shown.length ? ` and ${list.length - shown.length} more` : "";
12389
- const from = [...modules].slice(0, 3).map((m) => `"${m}"`);
12505
+ const from = [...modules].slice(0, 3).map((m) => JSON.stringify(m));
12390
12506
  const moreModules = modules.size > from.length ? ` and ${modules.size - from.length} more` : "";
12391
12507
  const where = results.length > 1 ? ` (in ${files} ${files === 1 ? "file" : "files"})` : "";
12392
12508
  return `${shown.join(", ")}${more} ${list.length === 1 ? "comes" : "come"} from ${from.join(", ")}${moreModules}, which the design-system model does not include, so ${list.length === 1 ? "it was" : "they were"} not checked${where}. Extraction looks incomplete: \`inspect\` lists what was found; add the missing files to "components" in the config.`;
@@ -13103,6 +13219,38 @@ function repositoryOf(dir) {
13103
13219
  if (current === path.dirname(current)) return void 0;
13104
13220
  }
13105
13221
  }
13222
+ /**
13223
+ * `extractComponents`, without the component files nested too deeply for TypeScript's recursive
13224
+ * parser (thousands of levels), each named in a warning: one such file would otherwise fail the
13225
+ * whole design system with a RangeError that names no file.
13226
+ */
13227
+ function extractReadable(options) {
13228
+ try {
13229
+ return extractComponents(options);
13230
+ } catch (error) {
13231
+ if (!isStackOverflow(error)) throw error;
13232
+ const tooDeep = new Set(options.files.filter(overflowsParser));
13233
+ if (!tooDeep.size) throw error;
13234
+ const extracted = extractComponents({
13235
+ ...options,
13236
+ files: options.files.filter((file) => !tooDeep.has(file))
13237
+ });
13238
+ for (const file of tooDeep) extracted.warnings.push(`${relativePath(options.root, file)}: nested too deeply to read (thousands of levels); skipped.`);
13239
+ return extracted;
13240
+ }
13241
+ }
13242
+ /** Whether parsing `file` on its own runs out of stack. */
13243
+ function overflowsParser(file) {
13244
+ try {
13245
+ ts.createSourceFile(file, fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest);
13246
+ return false;
13247
+ } catch (error) {
13248
+ return isStackOverflow(error);
13249
+ }
13250
+ }
13251
+ function isStackOverflow(error) {
13252
+ return error instanceof RangeError && /call stack/i.test(error.message);
13253
+ }
13106
13254
  /** What `inspect` and the server's log say about files that lead outside the project. */
13107
13255
  function outsideWarning(root, files) {
13108
13256
  const shown = files.map((file) => relativePath(root, file)).sort();
@@ -13250,11 +13398,54 @@ async function cacheKey(config, files, dependencies, stats = new FileStats(confi
13250
13398
  stats: await stats.of(all)
13251
13399
  })).digest("hex");
13252
13400
  }
13401
+ /**
13402
+ * Where the model is cached: the project's `node_modules/.cache`, else (`temp`) a folder of the
13403
+ * user's own in the system temp folder, `onsystem-<uid>`.
13404
+ */
13253
13405
  function cacheFile(root) {
13254
13406
  const nodeModules = path.join(root, "node_modules");
13255
- if (fs.existsSync(nodeModules)) return path.join(nodeModules, ".cache", "onsystem", "model.json");
13407
+ if (fs.existsSync(nodeModules)) return {
13408
+ file: path.join(nodeModules, ".cache", "onsystem", "model.json"),
13409
+ temp: false
13410
+ };
13256
13411
  const id = createHash("sha256").update(root).digest("hex").slice(0, 16);
13257
- return path.join(os.tmpdir(), "onsystem", `${id}.json`);
13412
+ const user = process.getuid ? `-${process.getuid()}` : "";
13413
+ return {
13414
+ file: path.join(os.tmpdir(), `onsystem${user}`, `${id}.json`),
13415
+ temp: true
13416
+ };
13417
+ }
13418
+ /**
13419
+ * Why the cache folder in the system temp folder cannot be trusted, or undefined when it can: it
13420
+ * must be a folder, not a link, that the user owns and no one else can write to. On a multi-user
13421
+ * host the temp folder is shared, and a model planted in it would reach the agent. With `create`,
13422
+ * it is created (or closed) for the user first. Windows gives each user a temp folder of their own.
13423
+ */
13424
+ async function untrustedCacheFolder(folder, create) {
13425
+ if (!process.getuid) return void 0;
13426
+ if (create) await fsp.mkdir(folder, {
13427
+ recursive: true,
13428
+ mode: 448
13429
+ });
13430
+ const stat = await fsp.lstat(folder);
13431
+ if (!stat.isDirectory()) return `${folder} is not a folder`;
13432
+ if (stat.uid !== process.getuid()) return `${folder} belongs to another user`;
13433
+ if (!create && stat.mode & 18) return `other users can write to ${folder}`;
13434
+ if (create && stat.mode & 63) await fsp.chmod(folder, 448);
13435
+ }
13436
+ /**
13437
+ * Writes `text` beside `file`, then renames it into place: a run reading it never sees half. With
13438
+ * `own`, only the user can read it (the temp folder's cache); without, it has the default mode, so
13439
+ * a cache in `node_modules` built by one user (a Docker build as root) is still read by another.
13440
+ */
13441
+ async function writeWhole(file, text, own) {
13442
+ const partial = `${file}.${process.pid}-${randomUUID()}.tmp`;
13443
+ try {
13444
+ await fsp.writeFile(partial, text, own ? { mode: 384 } : {});
13445
+ await fsp.rename(partial, file);
13446
+ } finally {
13447
+ await fsp.rm(partial, { force: true });
13448
+ }
13258
13449
  }
13259
13450
  /**
13260
13451
  * Fingerprint of the files a config tracks (paths, sizes, mtimes), plus the
@@ -13268,10 +13459,10 @@ async function buildModel(config, options = {}) {
13268
13459
  const started = performance.now();
13269
13460
  const files = await resolveFiles(config);
13270
13461
  const useCache = options.cache !== false;
13271
- const cachePath = cacheFile(config.root);
13462
+ const { file: cachePath, temp } = cacheFile(config.root);
13272
13463
  let cached;
13273
13464
  if (useCache && !options.oldProgram) try {
13274
- cached = JSON.parse(await fsp.readFile(cachePath, "utf8"));
13465
+ if (!(temp && await untrustedCacheFolder(path.dirname(cachePath), false))) cached = JSON.parse(await fsp.readFile(cachePath, "utf8"));
13275
13466
  } catch {}
13276
13467
  const known = Array.isArray(cached?.dependencies) ? cached.dependencies : options.oldProgram ? projectFiles(options.oldProgram) : [];
13277
13468
  const stats = new FileStats(config.root);
@@ -13285,7 +13476,7 @@ async function buildModel(config, options = {}) {
13285
13476
  dependencies: cached.dependencies
13286
13477
  };
13287
13478
  }
13288
- const extracted = extractComponents({
13479
+ const extracted = extractReadable({
13289
13480
  root: config.root,
13290
13481
  files: files.components,
13291
13482
  tsconfig: config.tsconfig,
@@ -13347,12 +13538,16 @@ async function buildModel(config, options = {}) {
13347
13538
  ]);
13348
13539
  const key = await cacheKey(config, files, dependencies, stats);
13349
13540
  if (useCache) try {
13350
- await fsp.mkdir(path.dirname(cachePath), { recursive: true });
13351
- await fsp.writeFile(cachePath, JSON.stringify({
13541
+ const folder = path.dirname(cachePath);
13542
+ let untrusted;
13543
+ if (temp) untrusted = await untrustedCacheFolder(folder, true);
13544
+ else await fsp.mkdir(folder, { recursive: true });
13545
+ if (untrusted) logger.warn(`not caching the model: ${untrusted}. Install the project's dependencies, so the cache goes in node_modules, or set TMPDIR to a folder of your own.`);
13546
+ else await writeWhole(cachePath, JSON.stringify({
13352
13547
  key,
13353
13548
  dependencies,
13354
13549
  model
13355
- }));
13550
+ }), temp);
13356
13551
  } catch (error) {
13357
13552
  logger.warn(`could not write cache ${cachePath}: ${error.message}`);
13358
13553
  }
@@ -13535,4 +13730,4 @@ var DesignSystemHost = class {
13535
13730
  }
13536
13731
  };
13537
13732
  //#endregion
13538
- export { DEFAULT_TOKENS as $, unrecordedRules as A, isInside as At, statusKinds as B, countBaseline as C, workspacePackageDirs as Ct, pruneBaseline as D, truncate as Dt, onLines as E, plural$1 as Et, loadTokens as F, readProjectConfig as Ft, parseDoc as G, parseDesignMd as H, mergeTokens as I, ConfigError as J, extractComponents as K, toDesignMd as L, writeBaseline as M, relativePath as Mt, RULES as N, toPosix as Nt, readBaseline as O, unique as Ot, TokenIndex as P, defaultProjectConfig as Pt, DEFAULT_TEST_FOLDERS as Q, toDtcg as R, applyBaseline as S, isWorkspaceRoot as St, goneFiles as T, closest as Tt, parseCssTokens as U, parseDtcgTokens as V, attachDocs as W, DEFAULT_SEVERITY as X, DEFAULT_DOCS as Y, DEFAULT_TEST_EXCLUDE as Z, formatDiagnostics as _, PackageFinder as _t, fingerprint as a, excludedBy as at, BASELINE_FILE as b, findDesignSystemPackage as bt, silentLogger as c, loadConfigWith as ct, descriptionOf as d, findVariantDefinitions as dt, FALLBACK_TOKENS as et, searchComponents as f, LIBRARIES as ft, escapeData as g, PACKAGE_FIELD as gt, uncheckedNotice as h, notReadYet as ht, componentFiles as i, defineConfig as it, updateBaseline as j, matchesGlob as jt, rulesRun as k, SCRIPT_FILE$2 as kt, stderrLogger as l, rulesFor as lt, checkSource as m, libraryTitle as mt, DesignSystemHost as n, RULE_IDS as nt, loadDesignSystem as o, hasOwnConfig as ot, applyFixes as p, LIBRARY_IDS as pt, CONFIG_FILES as q, buildModel as r, configJsonSchema as rt, tokenFiles as s, loadConfig as st, DesignSystem as t, LEGACY_CONFIG_FILES as tt, buildSearchIndex as u, formatPx as ut, NAME as v, declaredConfig as vt, findMoves as w, workspacePatterns as wt, LEGACY_BASELINE_FILE as x, isDesignSystemName as xt, VERSION as y, detectProject as yt, TOKEN_CATEGORIES as z };
13733
+ export { DEFAULT_TOKENS as $, unrecordedRules as A, toPosix as At, statusKinds as B, countBaseline as C, workspacePackageDirs as Ct, pruneBaseline as D, matchesGlob as Dt, onLines as E, isInside as Et, loadTokens as F, unique as Ft, parseDoc as G, parseDesignMd as H, mergeTokens as I, defaultProjectConfig as It, ConfigError as J, extractComponents as K, toDesignMd as L, readProjectConfig as Lt, writeBaseline as M, json as Mt, RULES as N, plural$1 as Nt, readBaseline as O, pathOf as Ot, TokenIndex as P, truncate as Pt, DEFAULT_TEST_FOLDERS as Q, toDtcg as R, applyBaseline as S, isWorkspaceRoot as St, goneFiles as T, SCRIPT_FILE$2 as Tt, parseCssTokens as U, parseDtcgTokens as V, attachDocs as W, DEFAULT_SEVERITY as X, DEFAULT_DOCS as Y, DEFAULT_TEST_EXCLUDE as Z, formatDiagnostics as _, PackageFinder as _t, fingerprint as a, excludedBy as at, BASELINE_FILE as b, findDesignSystemPackage as bt, silentLogger as c, loadConfigWith as ct, descriptionOf as d, findVariantDefinitions as dt, FALLBACK_TOKENS as et, searchComponents as f, LIBRARIES as ft, escapeData as g, PACKAGE_FIELD as gt, uncheckedNotice as h, notReadYet as ht, componentFiles as i, defineConfig as it, updateBaseline as j, closest as jt, rulesRun as k, relativePath as kt, stderrLogger as l, rulesFor as lt, checkSource as m, libraryTitle as mt, DesignSystemHost as n, RULE_IDS as nt, loadDesignSystem as o, hasOwnConfig as ot, applyFixes as p, LIBRARY_IDS as pt, CONFIG_FILES as q, buildModel as r, configJsonSchema as rt, tokenFiles as s, loadConfig as st, DesignSystem as t, LEGACY_CONFIG_FILES as tt, buildSearchIndex as u, formatPx as ut, NAME as v, declaredConfig as vt, findMoves as w, workspacePatterns as wt, LEGACY_BASELINE_FILE as x, isDesignSystemName as xt, VERSION as y, detectProject as yt, TOKEN_CATEGORIES as z };
@@ -1,5 +1,5 @@
1
- import { Mt as relativePath, at as excludedBy, i as componentFiles, r as buildModel } from "./design-system-D53Jxybo.js";
2
- import { r as loadTarget } from "./workspace-DAa_08uB.js";
1
+ import { at as excludedBy, i as componentFiles, kt as relativePath, r as buildModel } from "./design-system-CPH4C7-1.js";
2
+ import { r as loadTarget } from "./workspace-BINqxi7p.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { parentPort } from "node:worker_threads";
package/dist/eslint.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Mt as relativePath, N as RULES, Q as DEFAULT_TEST_FOLDERS, X as DEFAULT_SEVERITY, Z as DEFAULT_TEST_EXCLUDE, t as DesignSystem, v as NAME, y as VERSION } from "./design-system-D53Jxybo.js";
1
+ import { N as RULES, Q as DEFAULT_TEST_FOLDERS, X as DEFAULT_SEVERITY, Z as DEFAULT_TEST_EXCLUDE, kt as relativePath, t as DesignSystem, v as NAME, y as VERSION } from "./design-system-CPH4C7-1.js";
2
2
  import path from "node:path";
3
3
  import { MessageChannel, Worker, receiveMessageOnPort } from "node:worker_threads";
4
4
  //#region src/eslint/bridge.ts
package/dist/index.d.ts CHANGED
@@ -826,7 +826,7 @@ interface CreateServerOptions {
826
826
  getProjects?: (target?: string) => Promise<ProjectSet | undefined>;
827
827
  }
828
828
  /** Builds the MCP server: five tools, two resources and one prompt. Transport-agnostic. */
829
- export declare function createServer({ getDesignSystem, getProjects }: CreateServerOptions): McpServer;
829
+ export declare function createServer(options: CreateServerOptions): McpServer;
830
830
  //#endregion
831
831
  //#region src/server/stdio.d.ts
832
832
  interface ServeOptions extends LoadConfigOptions {