onsystem 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,6 +30,8 @@ Using the GitHub Action? `uses: dgesteves/onsystem@v0` checks the lines a pull r
30
30
 
31
31
  **Website and docs:** [onsystem.vercel.app](https://onsystem.vercel.app), with a [playground](https://onsystem.vercel.app/playground) that checks your own code.
32
32
 
33
+ **Feedback wanted:** say where it breaks in the [launch feedback thread](https://github.com/dgesteves/onsystem/discussions/227), see what's next in the [roadmap](https://github.com/dgesteves/onsystem/blob/main/ROADMAP.md), or pick up a [good first issue](https://github.com/dgesteves/onsystem/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22good%20first%20issue%22). If onsystem is useful to you, a star helps other people find it.
34
+
33
35
  <p align="center">
34
36
  <a href="https://onsystem.vercel.app"><img src=".github/assets/demo.webp" width="100%" alt="On the website, an agent drafts a settings card, check_ui finds 11 problems with a fix each and the agent applies them. Then a real Claude Code session with the plugin, where Claude looks the components up before writing and check_ui passes. Last, the benchmark."></a>
35
37
  </p>
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as LEGACY_CONFIG_FILES, A as unrecordedRules, At as relativePath, B as statusKinds, C as countBaseline, D as pruneBaseline, Dt as SCRIPT_FILE, E as onLines, Et as unique, J as ConfigError, L as toDesignMd, M as writeBaseline, Nt as readProjectConfig, O as readBaseline, Ot as isInside, R as toDtcg, S as applyBaseline, T as goneFiles, Y as DEFAULT_DOCS, _ as formatDiagnostics, _t as detectProject, b as BASELINE_FILE, c as silentLogger, et as RULE_IDS, ft as libraryTitle, g as escapeData, gt as declaredConfig, h as uncheckedNotice, i as componentFiles, j as updateBaseline, jt as toPosix, k as rulesRun, kt as matchesGlob, l as stderrLogger, mt as PACKAGE_FIELD, o as loadDesignSystem, pt as notReadYet, q as CONFIG_FILES, rt as excludedBy, s as tokenFiles, st as rulesFor, v as NAME, w as findMoves, wt as plural, x as LEGACY_BASELINE_FILE, y as VERSION, yt as isDesignSystemName } from "./design-system-Bykei5W0.js";
3
- import { a as notReadNotices, i as missingDependencies, o as explainProject, s as librariesLeftOut, t as serveStdio } from "./stdio-B7u6dqLd.js";
4
- import { r as loadTarget } from "./workspace-Dp1q8qZ8.js";
2
+ import { $ as LEGACY_CONFIG_FILES, A as unrecordedRules, At as relativePath, B as statusKinds, C as countBaseline, D as pruneBaseline, Dt as SCRIPT_FILE, E as onLines, Et as unique, J as ConfigError, L as toDesignMd, M as writeBaseline, Nt as readProjectConfig, O as readBaseline, Ot as isInside, R as toDtcg, S as applyBaseline, T as goneFiles, Y as DEFAULT_DOCS, _ as formatDiagnostics, _t as detectProject, b as BASELINE_FILE, c as silentLogger, et as RULE_IDS, ft as libraryTitle, g as escapeData, gt as declaredConfig, h as uncheckedNotice, i as componentFiles, j as updateBaseline, jt as toPosix, k as rulesRun, kt as matchesGlob, l as stderrLogger, mt as PACKAGE_FIELD, o as loadDesignSystem, pt as notReadYet, q as CONFIG_FILES, rt as excludedBy, s as tokenFiles, st as rulesFor, v as NAME, w as findMoves, wt as plural, x as LEGACY_BASELINE_FILE, y as VERSION, yt as isDesignSystemName } from "./design-system-DZm2aP1z.js";
3
+ import { a as notReadNotices, i as missingDependencies, o as explainProject, s as librariesLeftOut, t as serveStdio } from "./stdio-BJvjEnKW.js";
4
+ import { r as loadTarget } from "./workspace-Dqu3nkYM.js";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { escapePath, glob } from "tinyglobby";
@@ -1060,7 +1060,9 @@ async function check(patterns, configOptions, values, io) {
1060
1060
  const warnings = results.reduce((n, r) => n + r.warningCount, 0);
1061
1061
  if (errors > 0) return 1;
1062
1062
  if (maxWarnings !== void 0 && warnings > maxWarnings) {
1063
- if (format === "pretty") io.stderr(`Too many warnings (${warnings}, max ${maxWarnings}).`);
1063
+ const message = `Too many warnings (${warnings}, max ${maxWarnings}).`;
1064
+ if (format === "github") io.stdout(`::error title=${NAME}::${message}`);
1065
+ else io.stderr(message);
1064
1066
  return 1;
1065
1067
  }
1066
1068
  return 0;
@@ -5,10 +5,10 @@ import * as z from "zod";
5
5
  import { escapePath, glob, globSync } from "tinyglobby";
6
6
  import ts from "typescript";
7
7
  import os from "node:os";
8
+ import postcss from "postcss";
8
9
  import { clampChroma, colorsNamed, converter, differenceEuclidean, formatHex, parse, wcagContrast } from "culori";
9
10
  import { createHash } from "node:crypto";
10
11
  import fsp from "node:fs/promises";
11
- import postcss from "postcss";
12
12
  import { LineCounter, isMap, isScalar, parseDocument, stringify } from "yaml";
13
13
  import { stemmer } from "stemmer";
14
14
  //#region src/extract/program.ts
@@ -142,7 +142,7 @@ function slashGlob(pattern) {
142
142
  * `.mjs` and `.cjs` forms, in any case. CSS, Markdown or JSON parsed as TSX gives hundreds of
143
143
  * syntax errors (209 for a shadcn/ui `app/globals.css`).
144
144
  */
145
- const SCRIPT_FILE$1 = /\.(?:[jt]sx?|[cm][jt]s)$/i;
145
+ const SCRIPT_FILE$2 = /\.(?:[jt]sx?|[cm][jt]s)$/i;
146
146
  /** Path relative to `root`, always with forward slashes. */
147
147
  function relativePath(root, file) {
148
148
  return toPosix(path.relative(root, file));
@@ -514,6 +514,59 @@ function isFile$1(file) {
514
514
  }
515
515
  }
516
516
  //#endregion
517
+ //#region src/tokens/imports.ts
518
+ /** Enough stylesheets for a theme split over files, without reading a whole design system. */
519
+ const MAX_IMPORTED = 50;
520
+ /**
521
+ * The specifiers a stylesheet `@import`s: `@import "./theme.css" layer(base)`,
522
+ * `@import url(theme.css)`, `@import "@acme/theme"`. Empty for CSS that does not parse.
523
+ */
524
+ function cssImports(css) {
525
+ let root;
526
+ try {
527
+ root = postcss.parse(css);
528
+ } catch {
529
+ return [];
530
+ }
531
+ const out = [];
532
+ for (const node of root.nodes) {
533
+ if (node.type !== "atrule" || node.name !== "import") continue;
534
+ const match = /^\s*(?:url\(\s*)?(?:"([^"]+)"|'([^']+)'|([^\s"')]+)\s*\))/.exec(node.params);
535
+ const specifier = match?.[1] ?? match?.[2] ?? match?.[3];
536
+ if (specifier) out.push(specifier);
537
+ }
538
+ return out;
539
+ }
540
+ /**
541
+ * The stylesheets `files` import, and the ones those import in turn, each once, in the order
542
+ * the cascade ranks them, for a reader that keeps the first declaration of a variable. CSS
543
+ * puts an imported stylesheet where its `@import` is, so a later import overrides an earlier
544
+ * one, and the importing stylesheet's own rules override both: `@import "./base.css";
545
+ * @import "./brand.css";` lists brand.css, then what brand.css imports, then base.css. The
546
+ * files themselves are not repeated, and come before all of these.
547
+ */
548
+ function importedStylesheets(files, resolve) {
549
+ const seen = new Set(files);
550
+ const out = [];
551
+ const visit = (file) => {
552
+ let text;
553
+ try {
554
+ text = fs.readFileSync(file, "utf8");
555
+ } catch {
556
+ return;
557
+ }
558
+ for (const specifier of cssImports(text).reverse()) {
559
+ const target = resolve(specifier, file);
560
+ if (!target || seen.has(target) || out.length >= MAX_IMPORTED) continue;
561
+ seen.add(target);
562
+ out.push(target);
563
+ visit(target);
564
+ }
565
+ };
566
+ for (const file of files) visit(file);
567
+ return out;
568
+ }
569
+ //#endregion
517
570
  //#region src/util/strings.ts
518
571
  /** `alert-dialog` / `alert_dialog` / `alertDialog` → `AlertDialog`. */
519
572
  function pascalCase(input) {
@@ -579,7 +632,7 @@ function unique(items) {
579
632
  const ignore = () => void 0;
580
633
  const COMPONENT_FILE = /\.[jt]sx$/;
581
634
  /** A module a barrel can lead to: TypeScript or JavaScript, not a declaration file. */
582
- const SCRIPT_FILE = /(?<!\.d)\.[cm]?[jt]sx?$/;
635
+ const SCRIPT_FILE$1 = /(?<!\.d)\.[cm]?[jt]sx?$/;
583
636
  const STYLESHEETS = [
584
637
  "src/styles/globals.css",
585
638
  "src/globals.css",
@@ -588,6 +641,18 @@ const STYLESHEETS = [
588
641
  "src/index.css"
589
642
  ];
590
643
  /**
644
+ * An app's own stylesheets, where zero config looks for them without components.json (the
645
+ * stylesheets of `DEFAULT_TOKENS` in config.ts).
646
+ */
647
+ const APP_STYLESHEETS = [
648
+ "app/globals.css",
649
+ "src/app/globals.css",
650
+ "styles/globals.css",
651
+ "src/styles/globals.css",
652
+ "src/index.css",
653
+ "app/app.css"
654
+ ];
655
+ /**
591
656
  * Finds the design system without a config:
592
657
  *
593
658
  * 1. `components.json` (shadcn/ui): the `ui` alias, resolved through tsconfig
@@ -892,15 +957,28 @@ function fromComponentsJson(root, dir, json, project, packages, note = ignore) {
892
957
  }
893
958
  const tailwind = asRecord(json.tailwind);
894
959
  const css = typeof tailwind.css === "string" && tailwind.css ? path.resolve(dir, tailwind.css) : void 0;
960
+ const named = css && fs.existsSync(css) ? [css] : [];
895
961
  return {
896
962
  components: [`${escapePath(relativePath(root, uiDir) || ".")}/**/*.{tsx,jsx}`],
897
- tokens: css && fs.existsSync(css) ? [escapePath(relativePath(root, css))] : [],
963
+ tokens: (dir === root ? withAppStylesheets(root, named) : named).map((f) => escapePath(relativePath(root, f))),
898
964
  imports,
899
965
  dirs: [uiDir],
900
966
  source: `${ui} → ${relativePath(root, uiDir) || "."}`,
901
967
  ...ownerPackage(root, uiDir, ui)
902
968
  };
903
969
  }
970
+ /**
971
+ * The stylesheets an app's tokens come from when its components.json names a stylesheet in
972
+ * another package, as cal.com's names coss/ui's: the app's own stylesheets first, which hold
973
+ * its own tokens, then the named one, unless they import it, so that the cascade places it.
974
+ */
975
+ function withAppStylesheets(root, named) {
976
+ if (named.every((file) => isInside(root, file))) return named;
977
+ const own = APP_STYLESHEETS.map((f) => path.join(root, f)).filter(isFile);
978
+ if (!own.length) return named;
979
+ const imported = new Set(importedStylesheets(own, stylesheetResolver(root)));
980
+ return [...own, ...named.filter((file) => !imported.has(file))];
981
+ }
904
982
  /** How a shadcn registry is named in components.json: `@` and a name, so none leads out of the folder. */
905
983
  const REGISTRY_NAMESPACE = /^@[a-z0-9][\w-]*$/i;
906
984
  /**
@@ -1056,7 +1134,7 @@ function fromPackage(root, dir, pkg, note = ignore, installed = false) {
1056
1134
  for (const mapping of mappings) {
1057
1135
  const file = path.resolve(root, mapping.target);
1058
1136
  if (installed && isCompiledExport(dir, file)) continue;
1059
- if (mapping.target.includes("*") || !SCRIPT_FILE.test(file) || !isFile(file)) {
1137
+ if (mapping.target.includes("*") || !SCRIPT_FILE$1.test(file) || !isFile(file)) {
1060
1138
  add(mapping, COMPONENT_FILE.test(mapping.target));
1061
1139
  continue;
1062
1140
  }
@@ -1346,7 +1424,7 @@ function presetResolver(root) {
1346
1424
  if (file || specifier.startsWith(".")) return file;
1347
1425
  const name = packageName(specifier);
1348
1426
  const dir = name ? packages.find(name) : void 0;
1349
- return name && dir ? exportedFile(dir, `.${specifier.slice(name.length)}`, (f) => SCRIPT_FILE.test(f)) : void 0;
1427
+ return name && dir ? exportedFile(dir, `.${specifier.slice(name.length)}`, (f) => SCRIPT_FILE$1.test(f)) : void 0;
1350
1428
  };
1351
1429
  }
1352
1430
  /**
@@ -2009,7 +2087,7 @@ const KEYWORDS = /* @__PURE__ */ new Set([
2009
2087
  * `/ alpha` allowed): how Tailwind v3 projects, and shadcn/ui before v4, store
2010
2088
  * colors, to be used as `hsl(var(--border))`.
2011
2089
  */
2012
- const BARE_HSL = /^(-?\d*\.?\d+)(deg)?(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*\/\s*(\d*\.?\d+%?))?$/;
2090
+ const BARE_HSL = /^(-?(?:\d+(?:\.\d+)?|\.\d+))(deg)?(?:\s*,\s*|\s+)(\d+(?:\.\d+)?|\.\d+)%(?:\s*,\s*|\s+)(\d+(?:\.\d+)?|\.\d+)%(?:\s*\/\s*((?:\d+(?:\.\d+)?|\.\d+)%?))?$/;
2013
2091
  function isBareHsl(value) {
2014
2092
  return BARE_HSL.test(value.trim());
2015
2093
  }
@@ -2019,7 +2097,7 @@ function isBareHsl(value) {
2019
2097
  * Three numbers alone are no sure color, so they count as one only in a token already
2020
2098
  * known to be a color: one the Tailwind config reads with `rgb()`.
2021
2099
  */
2022
- const BARE_RGB = /^(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*\/\s*(\d*\.?\d+%?))?$/;
2100
+ const BARE_RGB = /^(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*\/\s*((?:\d+(?:\.\d+)?|\.\d+)%?))?$/;
2023
2101
  function isBareRgb(value) {
2024
2102
  const match = BARE_RGB.exec(value.trim());
2025
2103
  return match !== null && match.slice(1, 4).every((channel) => Number(channel) <= 255);
@@ -2757,7 +2835,7 @@ function classText(node) {
2757
2835
  visit(node);
2758
2836
  return parts.join(" ").replace(/\s+/g, " ").trim();
2759
2837
  }
2760
- const LENGTH = /^(-?(?:\d+\.?\d*|\.\d+))(px|rem|em)?$/i;
2838
+ const LENGTH = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(px|rem|em)?$/i;
2761
2839
  /** `"0.5rem"` → 8, `"12px"` → 12, `"4"` → 4. Returns undefined for anything else. */
2762
2840
  function lengthToPx(value) {
2763
2841
  if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
@@ -2785,13 +2863,27 @@ function evaluateLength(value, resolve, depth = 0) {
2785
2863
  function expandVars(value, resolve, depth) {
2786
2864
  let out = "";
2787
2865
  let last = 0;
2788
- for (const match of value.matchAll(/var\(\s*(--[\w-]+)\s*(?:,\s*([^)]+))?\)/g)) {
2789
- const [whole, name = "", fallback] = match;
2866
+ for (let at = value.indexOf("var(", last); at !== -1; at = value.indexOf("var(", last)) {
2867
+ let level = 0;
2868
+ let comma = -1;
2869
+ let end = -1;
2870
+ for (let i = at + 3; i < value.length; i++) {
2871
+ const c = value[i];
2872
+ if (c === "(") level++;
2873
+ else if (c === ")" && --level === 0) {
2874
+ end = i;
2875
+ break;
2876
+ } else if (c === "," && level === 1 && comma === -1) comma = i;
2877
+ }
2878
+ if (end === -1) return void 0;
2879
+ const name = value.slice(at + 4, comma === -1 ? end : comma).trim();
2880
+ if (!/^--[\w-]+$/.test(name)) return void 0;
2881
+ const fallback = comma === -1 ? void 0 : value.slice(comma + 1, end);
2790
2882
  const resolved = resolve(name) ?? fallback;
2791
2883
  const px = resolved === void 0 ? void 0 : evaluateLength(resolved, resolve, depth + 1);
2792
2884
  if (px === void 0) return void 0;
2793
- out += `${value.slice(last, match.index)}${px}px`;
2794
- last = match.index + whole.length;
2885
+ out += `${value.slice(last, at)}${px}px`;
2886
+ last = end + 1;
2795
2887
  }
2796
2888
  return out + value.slice(last);
2797
2889
  }
@@ -2917,6 +3009,10 @@ function utilities(namespace, key) {
2917
3009
  //#region src/tokens/tailwind-config.ts
2918
3010
  /** A preset chain is a few files deep; anything beyond is not a Tailwind config. */
2919
3011
  const MAX_FILES = 12;
3012
+ /** A config or preset module, which is read for its theme. */
3013
+ const SCRIPT_FILE = /\.[cm]?[jt]s$/;
3014
+ /** Tailwind's own modules, such as `tailwindcss/defaultTheme`, which hold its defaults. */
3015
+ const TAILWIND_MODULE = /^tailwindcss(?:\/|$)/;
2920
3016
  /**
2921
3017
  * Reads which custom property backs which color class from a Tailwind v3
2922
3018
  * config, without running it: every `colors` object in the file
@@ -2946,7 +3042,7 @@ function readTailwindColors(file, resolve) {
2946
3042
  }
2947
3043
  const specifier = importedSpecifier(node);
2948
3044
  const target = specifier === void 0 ? void 0 : resolve(specifier, current);
2949
- if (target && /\.[cm]?[jt]s$/.test(target)) queue.push(target);
3045
+ if (target && SCRIPT_FILE.test(target)) queue.push(target);
2950
3046
  ts.forEachChild(node, visit);
2951
3047
  };
2952
3048
  visit(source);
@@ -2978,13 +3074,47 @@ function collect(object, keys, colors, rgb) {
2978
3074
  }
2979
3075
  }
2980
3076
  /**
3077
+ * Tailwind v3's default `borderRadius` scale, which `theme.extend.borderRadius` adds to and
3078
+ * `theme.borderRadius` replaces. `none` (0) is left out, as the rule skips zero, and `full`
3079
+ * is the token index's own `rounded-full`.
3080
+ */
3081
+ const TAILWIND_V3_RADIUS = {
3082
+ sm: "0.125rem",
3083
+ DEFAULT: "0.25rem",
3084
+ md: "0.375rem",
3085
+ lg: "0.5rem",
3086
+ xl: "0.75rem",
3087
+ "2xl": "1rem",
3088
+ "3xl": "1.5rem"
3089
+ };
3090
+ /** borderRadius entries by key, and which of them are Tailwind's default scale rather than the config's. */
3091
+ var RadiusEntries = class extends Map {
3092
+ defaults = /* @__PURE__ */ new Set();
3093
+ };
3094
+ /**
2981
3095
  * Reads `borderRadius` entries from a Tailwind v3 config without running it:
2982
3096
  * every `borderRadius` object in the file, then in the project modules it
2983
- * imports, such as a shared preset. The config's own entries win.
3097
+ * imports, such as a shared preset. The config's own entries win. Tailwind's
3098
+ * default scale follows, for the keys none of them sets, unless the first
3099
+ * `theme.borderRadius` outside `extend` replaces it without spreading it back
3100
+ * in (`...defaultTheme.borderRadius`), a `theme` is a whole module or spreads
3101
+ * one (`theme: require("./theme")`, `theme: { ...require("./theme") }`), other
3102
+ * than Tailwind's own, `presets: []` drops Tailwind's default config
3103
+ * and no `theme.borderRadius` spreads the scale back in, or the scale comes from
3104
+ * somewhere this cannot read. That is a module that is no project file (one
3105
+ * installed from npm, other than Tailwind's own) that the config takes as a
3106
+ * preset, a theme, or the config itself (`module.exports = require("x")`,
3107
+ * `{ ...require("x"), content }`), or spreads into a theme, its `extend` or a
3108
+ * `borderRadius`; a `presets` that is no array; or a `theme.borderRadius` or
3109
+ * `theme.extend.borderRadius` that is no object (`borderRadius: radius`,
3110
+ * `borderRadius: ({ theme }) => …`) or spreads anything but Tailwind's scale.
2984
3111
  */
2985
3112
  function readTailwindRadii(file, resolve) {
2986
- const radii = /* @__PURE__ */ new Map();
3113
+ const radii = new RadiusEntries();
2987
3114
  const files = [];
3115
+ let keepsDefaults;
3116
+ let unread = false;
3117
+ let noDefaultConfig = false;
2988
3118
  const queue = [file];
2989
3119
  while (queue.length && files.length < MAX_FILES) {
2990
3120
  const current = queue.shift() ?? "";
@@ -2997,18 +3127,58 @@ function readTailwindRadii(file, resolve) {
2997
3127
  }
2998
3128
  files.push(current);
2999
3129
  const source = ts.createSourceFile(current, text, ts.ScriptTarget.Latest, true, scriptKind(current));
3130
+ const bindings = importBindings(source);
3131
+ const external = (specifier) => {
3132
+ if (specifier === void 0 || TAILWIND_MODULE.test(specifier)) return false;
3133
+ const target = resolve(specifier, current);
3134
+ return !(target && SCRIPT_FILE.test(target));
3135
+ };
3136
+ const fromPackage = (value) => external(moduleOf(value, bindings));
3137
+ const fromTailwind = (value) => TAILWIND_MODULE.test(moduleOf(value, bindings) ?? "");
3000
3138
  const visit = (node) => {
3001
- if (ts.isPropertyAssignment(node) && propertyName(node.name) === "borderRadius" && ts.isObjectLiteralExpression(node.initializer)) {
3002
- collectRadius(node.initializer, radii);
3003
- return;
3139
+ if ((ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) && propertyName(node.name) === "borderRadius") {
3140
+ const value = ts.isPropertyAssignment(node) ? node.initializer : void 0;
3141
+ const holder = themeSection(node.parent);
3142
+ if (value && ts.isObjectLiteralExpression(value)) {
3143
+ collectRadius(value, radii);
3144
+ let spreads = false;
3145
+ for (const property of value.properties) {
3146
+ if (!ts.isSpreadAssignment(property)) continue;
3147
+ if (fromTailwind(property.expression)) spreads = true;
3148
+ else unread = true;
3149
+ }
3150
+ if (holder === "theme") keepsDefaults ??= spreads;
3151
+ return;
3152
+ }
3153
+ if (holder === "theme") keepsDefaults ??= false;
3154
+ else if (holder === "extend") unread = true;
3155
+ }
3156
+ if ((ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) && propertyName(node.name) === "presets") {
3157
+ const value = ts.isPropertyAssignment(node) ? node.initializer : void 0;
3158
+ if (value && ts.isArrayLiteralExpression(value)) {
3159
+ if (!value.elements.length) noDefaultConfig = true;
3160
+ for (const element of value.elements) if (fromPackage(ts.isSpreadElement(element) ? element.expression : element)) unread = true;
3161
+ } else unread = true;
3162
+ }
3163
+ if ((ts.isShorthandPropertyAssignment(node) || ts.isPropertyAssignment(node) && !ts.isObjectLiteralExpression(node.initializer)) && propertyName(node.name) === "theme" && !ts.isPropertyAssignment(node.parent.parent)) {
3164
+ keepsDefaults ??= false;
3165
+ if (fromPackage(ts.isPropertyAssignment(node) ? node.initializer : node.name)) unread = true;
3004
3166
  }
3167
+ if (ts.isSpreadAssignment(node) && fromPackage(node.expression) && (themeSection(node.parent) !== void 0 || !ts.isPropertyAssignment(node.parent.parent) && !themeValueOnly(node.parent, source))) unread = true;
3168
+ if (ts.isSpreadAssignment(node) && themeSection(node.parent) === "theme" && !fromTailwind(node.expression)) keepsDefaults ??= false;
3169
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && isModuleExports(node.left) && fromPackage(node.right) || ts.isExportAssignment(node) && fromPackage(node.expression) || ts.isExportDeclaration(node) && !node.isTypeOnly && node.exportClause !== void 0 && ts.isNamedExports(node.exportClause) && node.exportClause.elements.some((element) => element.name.text === "default") && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && external(node.moduleSpecifier.text)) unread = true;
3005
3170
  const specifier = importedSpecifier(node);
3006
3171
  const target = specifier === void 0 ? void 0 : resolve(specifier, current);
3007
- if (target && /\.[cm]?[jt]s$/.test(target)) queue.push(target);
3172
+ if (target && SCRIPT_FILE.test(target)) queue.push(target);
3008
3173
  ts.forEachChild(node, visit);
3009
3174
  };
3010
3175
  visit(source);
3011
3176
  }
3177
+ if ((noDefaultConfig ? keepsDefaults === true : keepsDefaults !== false) && !unread) for (const [key, value] of Object.entries(TAILWIND_V3_RADIUS)) {
3178
+ if (radii.has(key)) continue;
3179
+ radii.set(key, value);
3180
+ radii.defaults.add(key);
3181
+ }
3012
3182
  return {
3013
3183
  radii,
3014
3184
  files
@@ -3028,6 +3198,10 @@ function collectRadius(object, radii) {
3028
3198
  radii.set(key, raw);
3029
3199
  }
3030
3200
  }
3201
+ /** `module.exports`, as in `module.exports = require("@acme/tailwind-config")`. */
3202
+ function isModuleExports(node) {
3203
+ return ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "module" && node.name.text === "exports";
3204
+ }
3031
3205
  /**
3032
3206
  * Gives radius tokens their Tailwind v3 class names: each config entry is
3033
3207
  * evaluated (its `var()` against radius token values, so
@@ -3036,15 +3210,29 @@ function collectRadius(object, radii) {
3036
3210
  * token add a synthetic one, so the scale step exists as a candidate
3037
3211
  * (mirrors the spacing-steps synthesis); without it the literal would keep
3038
3212
  * matching the nearest unrelated token. Config entries win over presets.
3213
+ *
3214
+ * The steps of Tailwind's default scale ({@link RadiusEntries.defaults}) only add
3215
+ * a token for a size that has none, named after nothing the project names:
3216
+ * `rounded-xl` is 12px in a shadcn/ui v3 project, whose config extends the scale
3217
+ * with `lg`, `md` and `sm`. They add none when a config step has no size (its
3218
+ * `var(--radius)` is in no stylesheet read), as they would make a scale with
3219
+ * holes, nor when the tokens are a Tailwind v4 theme, whose own scale is read from
3220
+ * its stylesheet: Tailwind v4's defaults, or a token of an `@theme` variable outside
3221
+ * the colors (only v4 reads `@theme`; a v3 config names colors too).
3039
3222
  */
3040
3223
  function applyTailwindRadii(tokens, radii, configFile = "tailwind.config") {
3041
3224
  const byVar = /* @__PURE__ */ new Map();
3042
3225
  for (const token of tokens) if (token.category === "radius" && token.cssVar) byVar.set(token.cssVar, token.value);
3043
3226
  const resolve = (name) => byVar.get(name);
3227
+ const defaults = radii instanceof RadiusEntries ? radii.defaults : /* @__PURE__ */ new Set();
3228
+ const v4 = tokens.some((t) => t.origin === "tailwind-default" || t.category !== "color" && t.tailwind !== void 0);
3044
3229
  const byPx = /* @__PURE__ */ new Map();
3230
+ let unsized = false;
3045
3231
  for (const [key, raw] of radii) {
3232
+ if (defaults.has(key)) continue;
3046
3233
  const px = evaluateLength(raw, resolve);
3047
- if (px !== void 0 && !byPx.has(px)) byPx.set(px, key);
3234
+ if (px === void 0) unsized = true;
3235
+ else if (!byPx.has(px)) byPx.set(px, key);
3048
3236
  }
3049
3237
  const havePx = /* @__PURE__ */ new Set();
3050
3238
  for (const token of tokens) {
@@ -3076,6 +3264,123 @@ function applyTailwindRadii(tokens, radii, configFile = "tailwind.config") {
3076
3264
  tokens.push(token);
3077
3265
  havePx.add(px);
3078
3266
  }
3267
+ if (unsized || v4) return;
3268
+ const names = new Set(tokens.map((t) => t.name));
3269
+ for (const key of defaults) {
3270
+ const value = radii.get(key) ?? "";
3271
+ const px = evaluateLength(value, resolve);
3272
+ const name = `radius-${key}`;
3273
+ if (px === void 0 || havePx.has(px) || names.has(name)) continue;
3274
+ const token = {
3275
+ name,
3276
+ category: "radius",
3277
+ value,
3278
+ tailwind: key,
3279
+ usage: usageFor({
3280
+ category: "radius",
3281
+ tailwind: key
3282
+ }, "--radius"),
3283
+ description: "Tailwind's default radius scale.",
3284
+ origin: "tailwind-default",
3285
+ source: {
3286
+ file: configFile,
3287
+ line: 1
3288
+ }
3289
+ };
3290
+ tokens.push(token);
3291
+ havePx.add(px);
3292
+ }
3293
+ }
3294
+ /**
3295
+ * Whether an object is the value of a variable that its file uses only as a theme value of a
3296
+ * key other than `extend` and `borderRadius`, such as `const brand = { ...blue, ...blueDark }`
3297
+ * with `theme: { extend: { colors: brand } }`. What it spreads is then no config, theme or
3298
+ * radius scale. A variable that is exported, or used in any other way, may be one.
3299
+ */
3300
+ function themeValueOnly(object, source) {
3301
+ let value = object;
3302
+ while (ts.isParenthesizedExpression(value.parent) || ts.isAsExpression(value.parent) || ts.isSatisfiesExpression(value.parent)) value = value.parent;
3303
+ const declaration = value.parent;
3304
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer !== value || !ts.isIdentifier(declaration.name) || !ts.isVariableStatement(declaration.parent.parent) || declaration.parent.parent.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return false;
3305
+ const name = declaration.name;
3306
+ let only = true;
3307
+ const visit = (node) => {
3308
+ if (!only) return;
3309
+ const parent = node.parent;
3310
+ const key = parent !== void 0 && (ts.isPropertyAssignment(parent) || ts.isPropertyAccessExpression(parent)) && parent.name === node;
3311
+ if (ts.isIdentifier(node) && node !== name && node.text === name.text && !key) only = isThemeValue(node);
3312
+ ts.forEachChild(node, visit);
3313
+ };
3314
+ visit(source);
3315
+ return only;
3316
+ }
3317
+ /**
3318
+ * Whether a name is a theme value, under a key other than `extend` and `borderRadius`:
3319
+ * `colors: brand`, or `colors: { brand }`.
3320
+ */
3321
+ function isThemeValue(reference) {
3322
+ const parent = reference.parent;
3323
+ let property;
3324
+ if (ts.isPropertyAssignment(parent) && parent.initializer === reference) property = parent;
3325
+ else if (ts.isShorthandPropertyAssignment(parent)) property = parent;
3326
+ else return false;
3327
+ for (;;) {
3328
+ const key = propertyName(property.name);
3329
+ const object = property.parent;
3330
+ if (themeSection(object) !== void 0) return key !== void 0 && key !== "extend" && key !== "borderRadius";
3331
+ const holder = object.parent;
3332
+ if (!ts.isPropertyAssignment(holder) || holder.initializer !== object) return false;
3333
+ property = holder;
3334
+ }
3335
+ }
3336
+ /** `theme` or `extend` for a property of `theme: {…}` or of `theme.extend: {…}`; else undefined. */
3337
+ function themeSection(object) {
3338
+ const holder = object.parent;
3339
+ if (!ts.isObjectLiteralExpression(object) || !ts.isPropertyAssignment(holder)) return void 0;
3340
+ const key = propertyName(holder.name);
3341
+ if (key === "theme") return "theme";
3342
+ return key === "extend" && themeSection(holder.parent) === "theme" ? "extend" : void 0;
3343
+ }
3344
+ /**
3345
+ * The names a module binds to what it imports or requires, by specifier: `acme` for
3346
+ * `import acme from "x"`, `import { acme } from "x"`, `const acme = require("x")` or
3347
+ * `const { default: acme } = require("x")`.
3348
+ */
3349
+ function importBindings(source) {
3350
+ const bindings = /* @__PURE__ */ new Map();
3351
+ const bind = (name, specifier) => {
3352
+ if (ts.isIdentifier(name)) {
3353
+ bindings.set(name.text, specifier);
3354
+ return;
3355
+ }
3356
+ for (const element of name.elements) if (!ts.isOmittedExpression(element)) bind(element.name, specifier);
3357
+ };
3358
+ for (const statement of source.statements) if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
3359
+ const specifier = statement.moduleSpecifier.text;
3360
+ const clause = statement.importClause;
3361
+ if (clause?.name) bind(clause.name, specifier);
3362
+ const named = clause?.namedBindings;
3363
+ if (named && ts.isNamespaceImport(named)) bind(named.name, specifier);
3364
+ else if (named) for (const element of named.elements) bind(element.name, specifier);
3365
+ } else if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) {
3366
+ const specifier = declaration.initializer && moduleOf(declaration.initializer, bindings);
3367
+ if (specifier !== void 0) bind(declaration.name, specifier);
3368
+ }
3369
+ return bindings;
3370
+ }
3371
+ /**
3372
+ * The module an expression takes its value from: `"x"` for `require("x")`, `import("x")`, a
3373
+ * name bound to either ({@link importBindings}), a call of any of these
3374
+ * (`createPreset({…})`, `require("x")()`) or a property of one (`require("x").default`).
3375
+ */
3376
+ function moduleOf(node, bindings) {
3377
+ let current = node;
3378
+ for (;;) if (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current) || ts.isNonNullExpression(current) || ts.isAwaitExpression(current) || ts.isPropertyAccessExpression(current)) current = current.expression;
3379
+ else if (ts.isCallExpression(current)) {
3380
+ const specifier = importedSpecifier(current);
3381
+ if (specifier !== void 0) return specifier;
3382
+ current = current.expression;
3383
+ } else return ts.isIdentifier(current) ? bindings.get(current.text) : void 0;
3079
3384
  }
3080
3385
  /** The specifier of `require("x")`, `import x from "x"`, `export { default } from "x"` or `import("x")`. */
3081
3386
  function importedSpecifier(node) {
@@ -6193,7 +6498,7 @@ function cssSheetTokens(sheets) {
6193
6498
  const category = isSpacingBase ? "spacing" : namespace?.[1] ?? "other";
6194
6499
  const tailwindKey = isSpacingBase ? "" : variable.name.slice(namespace?.[0].length ?? 0);
6195
6500
  const tailwindNamespace = isSpacingBase ? "--spacing" : namespace?.[0].slice(0, -1) ?? "";
6196
- const alias = /^var\(\s*(--[\w-]+)\s*\)$/.exec(variable.value)?.[1];
6501
+ const alias = themeAlias(variable.value, base, lookup);
6197
6502
  const target = alias ? base.get(alias) : void 0;
6198
6503
  if (alias && target) {
6199
6504
  consumed.add(alias);
@@ -6214,7 +6519,7 @@ function cssSheetTokens(sheets) {
6214
6519
  add(finalize({
6215
6520
  name: variable.name.slice(2),
6216
6521
  category,
6217
- value: variable.value,
6522
+ value: resolveValue(variable.value, lookup),
6218
6523
  modes: modeValues(variable.name, modes, lookup),
6219
6524
  cssVar: variable.inline ? void 0 : variable.name,
6220
6525
  tailwind: tailwindKey,
@@ -6345,6 +6650,22 @@ function categorize$1(name, value) {
6345
6650
  function varAlias(value) {
6346
6651
  return /^var\(\s*(--[\w-]+)\s*\)$/.exec(value.trim())?.[1];
6347
6652
  }
6653
+ /**
6654
+ * The base variable an `@theme` variable stands for: `--x` for `var(--x)`, and for
6655
+ * `hsl(var(--x))` or `rgb(var(--x))` when `--x` holds bare HSL or RGB channels, as
6656
+ * shadcn/ui's Tailwind v4 codemod leaves a v3 theme (`--color-primary: hsl(var(--primary))`).
6657
+ * The token is then `--x`, whose usage already wraps the channels (`hsl(var(--primary))`).
6658
+ */
6659
+ function themeAlias(value, base, lookup) {
6660
+ const plain = varAlias(value);
6661
+ if (plain) return plain;
6662
+ const match = /^(hsl|rgb)a?\(\s*var\(\s*(--[\w-]+)\s*\)\s*(?:\/\s*<alpha-value>\s*)?\)$/.exec(value.trim());
6663
+ const name = match?.[2];
6664
+ const target = name ? base.get(name) : void 0;
6665
+ if (!match || !name || !target) return void 0;
6666
+ const channels = resolveValue(target.value, lookup);
6667
+ return (match[1] === "hsl" ? isBareHsl(channels) : isBareRgb(channels)) ? name : void 0;
6668
+ }
6348
6669
  /** Follows plain `var(--x)` aliases so the token shows its concrete value. */
6349
6670
  function resolveValue(value, lookup, depth = 0) {
6350
6671
  const alias = /^var\(\s*(--[\w-]+)\s*\)$/.exec(value)?.[1];
@@ -7210,7 +7531,7 @@ function toDtcg(tokens) {
7210
7531
  const paths = assignPaths(tokens, warnings);
7211
7532
  const lookup = varLookup(tokens);
7212
7533
  const byName = new Map(tokens.map((t) => [t.name, t]));
7213
- const tree = {};
7534
+ const tree = Object.create(null);
7214
7535
  let count = 0;
7215
7536
  for (const category of TOKEN_CATEGORIES) for (const token of tokens) {
7216
7537
  const path = token.category === category ? paths.get(token) : void 0;
@@ -7292,7 +7613,7 @@ function insert(tree, path, node) {
7292
7613
  const next = group[key];
7293
7614
  if (isObject$1(next)) group = next;
7294
7615
  else {
7295
- const created = {};
7616
+ const created = Object.create(null);
7296
7617
  group[key] = created;
7297
7618
  group = created;
7298
7619
  }
@@ -7524,7 +7845,7 @@ function colorObject(css) {
7524
7845
  else if (spec.space === "srgb" && components.every((c) => typeof c === "number" && Math.abs(c * 255 - Math.round(c * 255)) < .001)) out.hex = formatHex(color);
7525
7846
  return out;
7526
7847
  }
7527
- const PLAIN_LENGTH = /^(-?(?:\d+\.?\d*|\.\d+))(px|rem)$/;
7848
+ const PLAIN_LENGTH = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(px|rem)$/;
7528
7849
  /** `0.5rem` → `{ value: 0.5, unit: "rem" }`; a `calc()` of tokens → its px value. */
7529
7850
  function dimensionValue(value, lookup) {
7530
7851
  const plain = plainDimension(value);
@@ -7554,8 +7875,8 @@ function plainDimension(value) {
7554
7875
  unit: match[2] ?? "px"
7555
7876
  } : void 0;
7556
7877
  }
7557
- const COMPOSITE = /^[\w-]+\s*:\s*[^;]+(?:;\s*[\w-]+\s*:\s*[^;]+)*;?$/;
7558
- const NUMBER = /^-?(?:\d+\.?\d*|\.\d+)$/;
7878
+ const COMPOSITE = /^[\w-]+\s*:[^;]+(?:;\s*[\w-]+\s*:[^;]+)*;?$/;
7879
+ const NUMBER = /^-?(?:\d+(?:\.\d*)?|\.\d+)$/;
7559
7880
  /** What a typography token sets, from its name and Tailwind class (`font-sans`, `text-sm`). */
7560
7881
  function typographyKind(token) {
7561
7882
  const value = token.value.trim();
@@ -7815,7 +8136,7 @@ function frontMatterValue(token, key, lookup) {
7815
8136
  }
7816
8137
  }
7817
8138
  /** A DESIGN.md dimension: px, em or rem. */
7818
- const DESIGN_DIMENSION = /^-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)$/;
8139
+ const DESIGN_DIMENSION = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:px|rem|em)$/;
7819
8140
  /**
7820
8141
  * A typography token as a DESIGN.md typography level: `{ fontFamily: "Inter" }` for
7821
8142
  * `--font-sans`, every property of a composite. A `var()` is no font name (`next/font` sets
@@ -11677,7 +11998,7 @@ function isObject(value) {
11677
11998
  var name = "onsystem";
11678
11999
  //#endregion
11679
12000
  //#region src/version.ts
11680
- const VERSION = "0.9.0";
12001
+ const VERSION = "0.9.1";
11681
12002
  const NAME = name;
11682
12003
  //#endregion
11683
12004
  //#region src/lint/sarif.ts
@@ -12505,55 +12826,6 @@ function searchComponents(index, query, limit = 5) {
12505
12826
  }));
12506
12827
  }
12507
12828
  //#endregion
12508
- //#region src/tokens/imports.ts
12509
- /** Enough stylesheets for a theme split over files, without reading a whole design system. */
12510
- const MAX_IMPORTED = 50;
12511
- /**
12512
- * The specifiers a stylesheet `@import`s: `@import "./theme.css" layer(base)`,
12513
- * `@import url(theme.css)`, `@import "@acme/theme"`. Empty for CSS that does not parse.
12514
- */
12515
- function cssImports(css) {
12516
- let root;
12517
- try {
12518
- root = postcss.parse(css);
12519
- } catch {
12520
- return [];
12521
- }
12522
- const out = [];
12523
- for (const node of root.nodes) {
12524
- if (node.type !== "atrule" || node.name !== "import") continue;
12525
- const match = /^\s*(?:url\(\s*)?(?:"([^"]+)"|'([^']+)'|([^\s"')]+)\s*\))/.exec(node.params);
12526
- const specifier = match?.[1] ?? match?.[2] ?? match?.[3];
12527
- if (specifier) out.push(specifier);
12528
- }
12529
- return out;
12530
- }
12531
- /**
12532
- * The stylesheets `files` import, and the ones those import in turn, each once and in the
12533
- * order they are imported. The files themselves are not repeated.
12534
- */
12535
- function importedStylesheets(files, resolve) {
12536
- const seen = new Set(files);
12537
- const out = [];
12538
- const visit = (file) => {
12539
- let text;
12540
- try {
12541
- text = fs.readFileSync(file, "utf8");
12542
- } catch {
12543
- return;
12544
- }
12545
- for (const specifier of cssImports(text)) {
12546
- const target = resolve(specifier, file);
12547
- if (!target || seen.has(target) || out.length >= MAX_IMPORTED) continue;
12548
- seen.add(target);
12549
- out.push(target);
12550
- visit(target);
12551
- }
12552
- };
12553
- for (const file of files) visit(file);
12554
- return out;
12555
- }
12556
- //#endregion
12557
12829
  //#region src/util/log.ts
12558
12830
  const stderrLogger = {
12559
12831
  info: (message) => process.stderr.write(`[onsystem] ${message}\n`),
@@ -13239,4 +13511,4 @@ var DesignSystemHost = class {
13239
13511
  }
13240
13512
  };
13241
13513
  //#endregion
13242
- export { LEGACY_CONFIG_FILES as $, unrecordedRules as A, relativePath as At, statusKinds as B, countBaseline as C, closest as Ct, pruneBaseline as D, SCRIPT_FILE$1 as Dt, onLines as E, unique as Et, loadTokens as F, parseDoc as G, parseDesignMd as H, mergeTokens as I, ConfigError as J, extractComponents as K, toDesignMd as L, writeBaseline as M, defaultProjectConfig as Mt, RULES as N, readProjectConfig as Nt, readBaseline as O, isInside as Ot, TokenIndex as P, DEFAULT_TEST_FOLDERS as Q, toDtcg as R, applyBaseline as S, workspacePatterns as St, goneFiles as T, truncate as Tt, parseCssTokens as U, parseDtcgTokens as V, attachDocs as W, DEFAULT_SEVERITY as X, DEFAULT_DOCS as Y, DEFAULT_TEST_EXCLUDE as Z, formatDiagnostics as _, detectProject as _t, fingerprint as a, loadConfig as at, BASELINE_FILE as b, isWorkspaceRoot as bt, silentLogger as c, formatPx as ct, descriptionOf as d, LIBRARY_IDS as dt, RULE_IDS as et, searchComponents as f, libraryTitle as ft, escapeData as g, declaredConfig as gt, uncheckedNotice as h, PackageFinder as ht, componentFiles as i, hasOwnConfig as it, updateBaseline as j, toPosix as jt, rulesRun as k, matchesGlob as kt, stderrLogger as l, findVariantDefinitions as lt, checkSource as m, PACKAGE_FIELD as mt, DesignSystemHost as n, defineConfig as nt, loadDesignSystem as o, loadConfigWith as ot, applyFixes as p, notReadYet as pt, CONFIG_FILES as q, buildModel as r, excludedBy as rt, tokenFiles as s, rulesFor as st, DesignSystem as t, configJsonSchema as tt, buildSearchIndex as u, LIBRARIES as ut, NAME as v, findDesignSystemPackage as vt, findMoves as w, plural$1 as wt, LEGACY_BASELINE_FILE as x, workspacePackageDirs as xt, VERSION as y, isDesignSystemName as yt, TOKEN_CATEGORIES as z };
13514
+ export { LEGACY_CONFIG_FILES as $, unrecordedRules as A, relativePath as At, statusKinds as B, countBaseline as C, closest as Ct, pruneBaseline as D, SCRIPT_FILE$2 as Dt, onLines as E, unique as Et, loadTokens as F, parseDoc as G, parseDesignMd as H, mergeTokens as I, ConfigError as J, extractComponents as K, toDesignMd as L, writeBaseline as M, defaultProjectConfig as Mt, RULES as N, readProjectConfig as Nt, readBaseline as O, isInside as Ot, TokenIndex as P, DEFAULT_TEST_FOLDERS as Q, toDtcg as R, applyBaseline as S, workspacePatterns as St, goneFiles as T, truncate as Tt, parseCssTokens as U, parseDtcgTokens as V, attachDocs as W, DEFAULT_SEVERITY as X, DEFAULT_DOCS as Y, DEFAULT_TEST_EXCLUDE as Z, formatDiagnostics as _, detectProject as _t, fingerprint as a, loadConfig as at, BASELINE_FILE as b, isWorkspaceRoot as bt, silentLogger as c, formatPx as ct, descriptionOf as d, LIBRARY_IDS as dt, RULE_IDS as et, searchComponents as f, libraryTitle as ft, escapeData as g, declaredConfig as gt, uncheckedNotice as h, PackageFinder as ht, componentFiles as i, hasOwnConfig as it, updateBaseline as j, toPosix as jt, rulesRun as k, matchesGlob as kt, stderrLogger as l, findVariantDefinitions as lt, checkSource as m, PACKAGE_FIELD as mt, DesignSystemHost as n, defineConfig as nt, loadDesignSystem as o, loadConfigWith as ot, applyFixes as p, notReadYet as pt, CONFIG_FILES as q, buildModel as r, excludedBy as rt, tokenFiles as s, rulesFor as st, DesignSystem as t, configJsonSchema as tt, buildSearchIndex as u, LIBRARIES as ut, NAME as v, findDesignSystemPackage as vt, findMoves as w, plural$1 as wt, LEGACY_BASELINE_FILE as x, workspacePackageDirs as xt, VERSION as y, isDesignSystemName as yt, TOKEN_CATEGORIES as z };
@@ -1,5 +1,5 @@
1
- import { At as relativePath, i as componentFiles, r as buildModel, rt as excludedBy } from "./design-system-Bykei5W0.js";
2
- import { r as loadTarget } from "./workspace-Dp1q8qZ8.js";
1
+ import { At as relativePath, i as componentFiles, r as buildModel, rt as excludedBy } from "./design-system-DZm2aP1z.js";
2
+ import { r as loadTarget } from "./workspace-Dqu3nkYM.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 { At 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-Bykei5W0.js";
1
+ import { At 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-DZm2aP1z.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.js CHANGED
@@ -1,3 +1,3 @@
1
- import { F as loadTokens, G as parseDoc, H as parseDesignMd, I as mergeTokens, K as extractComponents, L as toDesignMd, N as RULES, P as TokenIndex, R as toDtcg, U as parseCssTokens, V as parseDtcgTokens, W as attachDocs, _ as formatDiagnostics, a as fingerprint, at as loadConfig, et as RULE_IDS, f as searchComponents, lt as findVariantDefinitions, m as checkSource, n as DesignSystemHost, nt as defineConfig, o as loadDesignSystem, p as applyFixes, r as buildModel, t as DesignSystem, tt as configJsonSchema, y as VERSION } from "./design-system-Bykei5W0.js";
2
- import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-B7u6dqLd.js";
1
+ import { F as loadTokens, G as parseDoc, H as parseDesignMd, I as mergeTokens, K as extractComponents, L as toDesignMd, N as RULES, P as TokenIndex, R as toDtcg, U as parseCssTokens, V as parseDtcgTokens, W as attachDocs, _ as formatDiagnostics, a as fingerprint, at as loadConfig, et as RULE_IDS, f as searchComponents, lt as findVariantDefinitions, m as checkSource, n as DesignSystemHost, nt as defineConfig, o as loadDesignSystem, p as applyFixes, r as buildModel, t as DesignSystem, tt as configJsonSchema, y as VERSION } from "./design-system-DZm2aP1z.js";
2
+ import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-BJvjEnKW.js";
3
3
  export { DesignSystem, DesignSystemHost, INSTRUCTIONS, RULES, RULE_IDS, TokenIndex, VERSION, applyFixes, attachDocs, buildModel, checkSource, configJsonSchema, createServer, defineConfig, extractComponents, findVariantDefinitions, fingerprint, formatDiagnostics, loadConfig, loadDesignSystem, loadTokens, mergeTokens, parseCssTokens, parseDesignMd, parseDoc, parseDtcgTokens, searchComponents, serveStdio, toDesignMd, toDtcg };
@@ -1,5 +1,5 @@
1
- import { $ as LEGACY_CONFIG_FILES, At as relativePath, Ct as closest, Dt as SCRIPT_FILE, Et as unique, Mt as defaultProjectConfig, Ot as isInside, Tt as truncate, X as DEFAULT_SEVERITY, _t as detectProject, at as loadConfig, ct as formatPx, d as descriptionOf, dt as LIBRARY_IDS, et as RULE_IDS, f as searchComponents, ft as libraryTitle, h as uncheckedNotice, n as DesignSystemHost, pt as notReadYet, q as CONFIG_FILES, rt as excludedBy, u as buildSearchIndex, ut as LIBRARIES, v as NAME, vt as findDesignSystemPackage, wt as plural, y as VERSION, z as TOKEN_CATEGORIES } from "./design-system-Bykei5W0.js";
2
- import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-Dp1q8qZ8.js";
1
+ import { $ as LEGACY_CONFIG_FILES, At as relativePath, Ct as closest, Dt as SCRIPT_FILE, Et as unique, Mt as defaultProjectConfig, Ot as isInside, Tt as truncate, X as DEFAULT_SEVERITY, _t as detectProject, at as loadConfig, ct as formatPx, d as descriptionOf, dt as LIBRARY_IDS, et as RULE_IDS, f as searchComponents, ft as libraryTitle, h as uncheckedNotice, n as DesignSystemHost, pt as notReadYet, q as CONFIG_FILES, rt as excludedBy, u as buildSearchIndex, ut as LIBRARIES, v as NAME, vt as findDesignSystemPackage, wt as plural, y as VERSION, z as TOKEN_CATEGORIES } from "./design-system-DZm2aP1z.js";
2
+ import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-Dqu3nkYM.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -327,11 +327,16 @@ function listTail(listed) {
327
327
  if (listed.omitted) lines.push("", `${listed.omitted.toLocaleString("en-US")} more not shown: pass a higher \`limit\`, or a \`query\`.`);
328
328
  return lines;
329
329
  }
330
+ /** The shared explanation when a project has no design-system components to show. */
331
+ function noComponentsMessage(root, project, monorepo = false) {
332
+ if (monorepo) return `No design-system components found in any project of this monorepo (root: ${root}). Check the "components" globs in the config. Run \`onsystem inspect\` there to see what was looked at.`;
333
+ return `No design-system components found${project ? ` in ${project}` : ""}. Check the "components" globs in the config (root: ${root}).`;
334
+ }
330
335
  /** `project`: the folder of a workspace root's project the list is for. */
331
336
  function renderComponentList(ds, project, filter = {}) {
332
337
  const roots = ds.roots();
333
338
  const where = project ? ` in ${project}` : "";
334
- if (!roots.length) return `No components found${where}. Check the "components" globs in the config (root: ${ds.root}).`;
339
+ if (!roots.length) return noComponentsMessage(ds.root, project);
335
340
  const listed = listComponents(roots, (c) => c, filter);
336
341
  if (!listed.matched) return `No components match ${filterText(filter)}${where} (${roots.length} in all). Call list_components without filters to see them.`;
337
342
  const parts = ds.components.length - roots.length;
@@ -374,7 +379,7 @@ const PICK_PROJECT = "This is a monorepo root: pass `path`, the file you are edi
374
379
  /** Every project's components at a workspace root, grouped by the package they come from. */
375
380
  function renderCatalogList(catalog, filter = {}) {
376
381
  const roots = catalog.roots();
377
- if (!roots.length) return `No components found in any project of this monorepo (root: ${catalog.root}). Run \`onsystem inspect\` there to see what was looked at.`;
382
+ if (!roots.length) return noComponentsMessage(catalog.root, void 0, true);
378
383
  const listed = listComponents(roots, (e) => e.component, filter, (e) => e.package);
379
384
  if (!listed.matched) return `No components match ${filterText(filter)} in any project (${roots.length} in all). Call list_components without filters to see them.`;
380
385
  const packages = new Set(roots.map((e) => e.package));
@@ -1086,6 +1091,7 @@ function createServer({ getDesignSystem, getProjects }) {
1086
1091
  const matches = catalog.find(name);
1087
1092
  const [entry] = matches;
1088
1093
  if (!entry) {
1094
+ if (!catalog.roots().length) return errorResult(noComponentsMessage(catalog.root, void 0, true));
1089
1095
  const guess = catalog.suggest(name);
1090
1096
  const names = [...new Set(catalog.roots().map((e) => e.component.name))];
1091
1097
  return errorResult(`No component named "${name}" in any project.` + (guess ? ` Did you mean "${guess}"?` : "") + ` Available: ${names.join(", ")}.`);
@@ -1109,6 +1115,13 @@ function createServer({ getDesignSystem, getProjects }) {
1109
1115
  const { ds } = found;
1110
1116
  const component = ds.getComponent(name);
1111
1117
  if (!component) {
1118
+ if (!ds.roots().length) return {
1119
+ isError: true,
1120
+ content: [{
1121
+ type: "text",
1122
+ text: noComponentsMessage(ds.root, found.project?.dir)
1123
+ }]
1124
+ };
1112
1125
  const guess = ds.suggestComponent(name);
1113
1126
  return {
1114
1127
  isError: true,
@@ -1,4 +1,4 @@
1
- import { $ as LEGACY_CONFIG_FILES, At as relativePath, Ot as isInside, St as workspacePatterns, at as loadConfig, bt as isWorkspaceRoot, ht as PackageFinder, i as componentFiles, it as hasOwnConfig, ot as loadConfigWith, q as CONFIG_FILES, vt as findDesignSystemPackage, xt as workspacePackageDirs } from "./design-system-Bykei5W0.js";
1
+ import { $ as LEGACY_CONFIG_FILES, At as relativePath, Ot as isInside, St as workspacePatterns, at as loadConfig, bt as isWorkspaceRoot, ht as PackageFinder, i as componentFiles, it as hasOwnConfig, ot as loadConfigWith, q as CONFIG_FILES, vt as findDesignSystemPackage, xt as workspacePackageDirs } from "./design-system-DZm2aP1z.js";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onsystem",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "mcpName": "io.github.dgesteves/onsystem",
5
5
  "description": "Keeps coding agents on your design system: catches palette colors, native elements and APIs your system lacks, at every edit and pull request, with the fix. Set it up once in your design-system package; every app in your monorepo gets it.",
6
6
  "keywords": [