onsystem 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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) {
@@ -3263,12 +3568,13 @@ const librariesSchema = z.union([z.literal(false), z.array(z.enum(LIBRARY_IDS))]
3263
3568
  } });
3264
3569
  const configShape = {
3265
3570
  $schema: z.string().optional(),
3571
+ extends: z.boolean({ error: (issue) => `"extends" is true or false, not ${JSON.stringify(issue.input)}: the design-system package is found from the dependencies, the one whose package.json declares its config ("onsystem"). Leave "extends" out to build on it, or set it to false to read this config alone.` }).optional().describe("Whether this config builds on the one a design-system package declares (\"onsystem\" in the package.json of a dependency, found from the dependencies): its rules apply over the package's, and, unless it sets components of its own, what it leaves out comes from the package's (components with their importPath, docs, elements, tokens and libraries). Default: true. false reads this config alone."),
3266
3572
  components: globsSchema.optional().describe("Globs for component source files, relative to the root."),
3267
3573
  exclude: z.array(z.string()).optional().describe("Globs for files to leave out, relative to the root: `check` in folders and globs, the ESLint plugin, the Claude Code hook and check_ui leave them out, and finding components skips them. A folder (`app/legacy`) covers everything in it. Adds to the defaults (node_modules, .d.ts files, tests and stories); a pattern that starts with `!` takes out the default it names, such as `!**/*.d.ts`."),
3268
3574
  tokens: z.union([tokenSourceSchema, z.array(tokenSourceSchema)]).optional().describe("Token files: W3C DTCG JSON (*.json), CSS custom properties (*.css) or a DESIGN.md's front matter (*.md)."),
3269
3575
  docs: globsSchema.optional().describe("Globs for per-component Markdown/MDX docs."),
3270
3576
  tsconfig: z.string().optional().describe("tsconfig used to resolve types and path aliases."),
3271
- importPath: z.string().optional().describe("Package name components are imported from, e.g. \"@acme/ui\", or a pattern for packages imported by path: \"@acme/ui/{path}\" is @acme/ui/primitives/button for primitives/button.tsx in the package. Default: inferred from package exports and tsconfig paths."),
3577
+ importPath: z.string().optional().describe("Package name components are imported from, e.g. \"@acme/ui\", or a pattern for packages imported by path: \"@acme/ui/{path}\" is @acme/ui/primitives/button for primitives/button.tsx in the package. An installed dependency it names exactly, whose component sources the project does not read, is read from its type declarations (.d.ts). Default: inferred from package exports and tsconfig paths."),
3272
3578
  elements: z.record(z.string(), z.string()).optional().describe("Extra native element → component mappings, e.g. { \"a\": \"Link\" }."),
3273
3579
  libraries: librariesSchema.optional().describe(`Published component libraries to read from their type declarations when the project depends on them. Omitted: every one onsystem reads (${LIBRARY_IDS.join(", ")}). false: none. A list: only those, e.g. ["mui"].`),
3274
3580
  includeDesignSystem: z.boolean().optional().describe("check: lint the design system's own component files too. Off by default: they implement the scale and primitives the rules enforce."),
@@ -3348,9 +3654,11 @@ async function loadConfigWith(options, context = {}) {
3348
3654
  let packages = context.packages?.root === searchDir ? context.packages : void 0;
3349
3655
  const finder = (dir) => packages?.root === dir ? packages : packages = new PackageFinder(dir);
3350
3656
  let lookup = context.lookup;
3657
+ /**
3658
+ * The project's own config: a config file in the folder searched, `--config`, or a workspace
3659
+ * root's that a project in it takes on.
3660
+ */
3351
3661
  let configFile;
3352
- /** The design-system package whose config this project takes on, having none of its own. */
3353
- let declared;
3354
3662
  if (options.config) configFile = path.resolve(cwd, options.config);
3355
3663
  else {
3356
3664
  const find = (names) => names.map((name) => path.join(searchDir, name)).find((file) => fs.existsSync(file));
@@ -3358,120 +3666,131 @@ async function loadConfigWith(options, context = {}) {
3358
3666
  const legacy = find(LEGACY_CONFIG_FILES);
3359
3667
  if (legacy) deprecations.push(legacyConfigNotice(legacy, configFile));
3360
3668
  configFile ??= legacy ?? ownDeclaredConfig(searchDir);
3361
- if (!configFile) {
3362
- lookup ??= findDesignSystemPackage(searchDir, finder(searchDir));
3363
- if (lookup?.problems.length) throw declarationError(lookup, searchDir);
3364
- declared = lookup?.found;
3365
- configFile = declared?.config;
3366
- }
3367
3669
  }
3368
- 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.`);
3369
- let raw = {};
3370
- if (configFile) {
3371
- if (!fs.existsSync(configFile)) throw new ConfigError(`Config file not found: ${configFile}`);
3372
- if (options.jsonConfigOnly && !configFile.endsWith(".json")) throw new ConfigError(`${configFile} was not run: the server runs a JavaScript or TypeScript config only in a project it was started in (its working directory, --root or the client's roots), not in one found from the path a tool call names. Start the server in the project, or use onsystem.config.json.`);
3373
- raw = await readConfigFile(configFile);
3374
- }
3375
- const parsed = configSchema.safeParse(raw);
3376
- if (!parsed.success) throw new ConfigError(`Invalid config in ${configFile ?? "(defaults)"}:\n${z.prettifyError(parsed.error)}`);
3377
- const config = parsed.data;
3670
+ const config = configFile ? await readConfig(configFile, options) : {};
3378
3671
  const root = options.root ? path.resolve(cwd, options.root) : options.config && configFile ? path.dirname(configFile) : searchDir;
3379
3672
  if (!isDirectory(root)) throw new ConfigError(`Project root not found: ${root}`);
3380
- const base = declared ? declared.dir : options.base ? path.resolve(cwd, options.base) : root;
3381
- const inherited = base !== root;
3382
- const fromBase = (p) => inherited ? toPosix(path.relative(root, path.resolve(base, slashGlob(p)))) || "." : p;
3673
+ let declared;
3674
+ if (config.extends !== false) {
3675
+ lookup ??= findDesignSystemPackage(root, finder(root));
3676
+ if (!lookup?.problems.length) declared = lookup?.found;
3677
+ 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)}.`);
3679
+ }
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
+ const pkg = declared ? await readConfig(declared.config, options) : void 0;
3682
+ const base = options.base ? path.resolve(cwd, options.base) : root;
3683
+ const rebase = (from) => (p) => from === root ? p : toPosix(path.relative(root, path.resolve(from, slashGlob(p)))) || ".";
3684
+ const fromBase = rebase(base);
3685
+ const fromPackage = rebase(declared?.dir ?? root);
3383
3686
  const toProject = toPosix(path.relative(base, root));
3384
3687
  const excludeFromBase = (e) => {
3385
- if (!inherited || e.startsWith("**/")) return e;
3386
- if (declared) return fromBase(e);
3688
+ if (base === root || e.startsWith("**/")) return e;
3387
3689
  if (e === toProject) return "**";
3388
3690
  return e.startsWith(`${toProject}/`) ? e.slice(toProject.length + 1) : void 0;
3389
3691
  };
3692
+ const excludeFromPackage = (e) => e.startsWith("**/") ? e : fromPackage(e);
3390
3693
  const outside = [];
3391
3694
  const within = (field) => (p) => {
3392
3695
  if (!declared || staysIn(declared.dir, declared.boundary, slashGlob(p))) return true;
3393
3696
  outside.push(`${field} ${p}`);
3394
3697
  return false;
3395
3698
  };
3396
- const configComponents = config.components === void 0 ? void 0 : toArray(config.components).filter(within("components"));
3397
- const configTokens = config.tokens === void 0 ? void 0 : toArray(config.tokens).filter((t) => within("tokens")(typeof t === "string" ? t : t.path));
3398
- const configDocs = config.docs === void 0 ? void 0 : toArray(config.docs).filter(within("docs"));
3699
+ const pathOf = (t) => typeof t === "string" ? t : t.path;
3700
+ const tokensFrom = (move) => (t) => typeof t === "string" ? { path: move(t) } : {
3701
+ ...t,
3702
+ path: move(t.path)
3703
+ };
3704
+ const packageComponents = pkg?.components === void 0 ? void 0 : toArray(pkg.components).filter(within("components")).map(fromPackage);
3705
+ const packageTokens = pkg?.tokens === void 0 ? void 0 : toArray(pkg.tokens).filter((t) => within("tokens")(pathOf(t))).map(tokensFrom(fromPackage));
3706
+ const packageDocs = pkg?.docs === void 0 ? void 0 : toArray(pkg.docs).filter(within("docs")).map(fromPackage);
3399
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(", ")}.`);
3708
+ const inherits = pkg !== void 0 && config.components === void 0;
3709
+ const configComponents = config.components === void 0 ? packageComponents : toArray(config.components).map(fromBase);
3710
+ const configTokens = config.tokens === void 0 ? inherits ? packageTokens : void 0 : toArray(config.tokens).map(tokensFrom(fromBase));
3711
+ const configDocs = config.docs === void 0 ? inherits ? packageDocs : void 0 : toArray(config.docs).map(fromBase);
3712
+ const importPath = config.importPath ?? (inherits ? pkg.importPath : void 0);
3713
+ const librariesSetting = config.libraries ?? (inherits ? pkg.libraries : void 0);
3400
3714
  const rules = {};
3401
3715
  for (const id of RULE_IDS) {
3402
- const setting = config.rules?.[id];
3716
+ const setting = config.rules?.[id] ?? pkg?.rules?.[id];
3403
3717
  rules[id] = setting === void 0 ? {
3404
3718
  severity: DEFAULT_SEVERITY[id],
3405
3719
  options: {}
3406
3720
  } : resolveRule(setting);
3407
3721
  }
3408
- const overrides = (declared ? [] : config.overrides ?? []).map((override) => ({
3722
+ const overrides = (config.overrides ?? []).map((override) => ({
3409
3723
  files: toArray(override.files).map(slashGlob),
3410
3724
  rules: Object.fromEntries(Object.entries(override.rules).map(([id, setting]) => [id, resolveRule(setting)]))
3411
3725
  }));
3412
- const tsconfig = config.tsconfig === void 0 || declared ? void 0 : fromBase(config.tsconfig.replaceAll("\\", "/"));
3726
+ const tsconfig = config.tsconfig === void 0 ? void 0 : fromBase(config.tsconfig.replaceAll("\\", "/"));
3413
3727
  if (tsconfig !== void 0 && !fs.existsSync(path.resolve(root, tsconfig))) throw new ConfigError(`tsconfig not found: ${path.resolve(root, tsconfig)} (from "tsconfig" in ${configFile ?? "the config"})`);
3414
- const componentsSet = Boolean(options.components?.length) || config.components !== void 0;
3728
+ const componentsSet = Boolean(options.components?.length) || configComponents !== void 0;
3415
3729
  const detection = componentsSet ? void 0 : detectProject(root, tsconfig, void 0, declared, finder(root));
3416
3730
  const explicitTokens = options.tokens?.length ? {
3417
3731
  paths: options.tokens,
3418
3732
  from: "--tokens"
3419
- } : config.tokens !== void 0 && !declared ? {
3420
- paths: toArray(config.tokens).map((t) => fromBase(typeof t === "string" ? t : t.path)),
3733
+ } : config.tokens !== void 0 ? {
3734
+ paths: toArray(config.tokens).map((t) => fromBase(pathOf(t))),
3421
3735
  from: `"tokens" in ${configFile ?? "the config"}`
3422
3736
  } : void 0;
3423
3737
  for (const file of explicitTokens?.paths ?? []) {
3424
3738
  const absolute = path.resolve(root, slashGlob(file));
3425
3739
  if (!/[*?[\]{}()!]/.test(file) && !fs.existsSync(absolute)) throw new ConfigError(`Token file not found: ${absolute} (from ${explicitTokens?.from})`);
3426
3740
  }
3427
- const components = (options.components?.length ? options.components : configComponents !== void 0 ? configComponents.map(fromBase) : detection ? [...detection.withDefaults ? DEFAULT_COMPONENTS : [], ...detection.components] : DEFAULT_COMPONENTS).map(slashGlob);
3428
- const libraries = detectLibraries(root, config.libraries, {
3429
- importPath: config.importPath,
3741
+ const components = (options.components?.length ? options.components : configComponents ?? (detection ? [...detection.withDefaults ? DEFAULT_COMPONENTS : [], ...detection.components] : DEFAULT_COMPONENTS)).map(slashGlob);
3742
+ const libraries = detectLibraries(root, librariesSetting, {
3743
+ importPath,
3430
3744
  components
3431
3745
  });
3432
- const tokenSources = options.tokens?.length ? options.tokens.map((p) => ({ path: p })) : configTokens !== void 0 ? configTokens.map((t) => typeof t === "string" ? { path: fromBase(t) } : {
3433
- ...t,
3434
- path: fromBase(t.path)
3435
- }) : [...(detection?.tokens.length ? [...detection.tokens, ...DEFAULT_TOKENS.filter((t) => t.endsWith(".json"))] : DEFAULT_TOKENS).map((p) => ({ path: p })), ...FALLBACK_TOKENS.map((p) => ({
3746
+ const tokenSources = options.tokens?.length ? options.tokens.map((p) => ({ path: p })) : configTokens ?? [...(detection?.tokens.length ? [...detection.tokens, ...DEFAULT_TOKENS.filter((t) => t.endsWith(".json"))] : DEFAULT_TOKENS).map((p) => ({ path: p })), ...FALLBACK_TOKENS.map((p) => ({
3436
3747
  path: p,
3437
3748
  fallback: true
3438
3749
  }))];
3439
3750
  const tidy = (e) => slashGlob(e).replace(/^(?:\.\/)+/, "").replace(/(.)\/+$/, "$1");
3440
- const listed = config.exclude ?? [];
3441
- const dropped = new Set(listed.filter((e) => e.startsWith("!")).map((e) => tidy(e.slice(1))));
3442
- for (const pattern of dropped) {
3443
- if (DEFAULT_EXCLUDE.includes(pattern)) continue;
3444
- warnings.push(`"!${pattern}" in "exclude" takes out none of the defaults, which are ${DEFAULT_EXCLUDE.map((d) => `"${d}"`).join(", ")}: a pattern that starts with "!" names one of them.`);
3445
- }
3446
- const exclude = [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDE.filter((d) => !dropped.has(d)), ...listed.filter((e) => !e.startsWith("!")).map(tidy)])].map(excludeFromBase).filter((e) => e !== void 0);
3751
+ const excludeOf = (listed, move) => {
3752
+ const dropped = new Set(listed.filter((e) => e.startsWith("!")).map((e) => tidy(e.slice(1))));
3753
+ for (const pattern of dropped) {
3754
+ if (DEFAULT_EXCLUDE.includes(pattern)) continue;
3755
+ warnings.push(`"!${pattern}" in "exclude" takes out none of the defaults, which are ${DEFAULT_EXCLUDE.map((d) => `"${d}"`).join(", ")}: a pattern that starts with "!" names one of them.`);
3756
+ }
3757
+ return [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDE.filter((d) => !dropped.has(d)), ...listed.filter((e) => !e.startsWith("!")).map(tidy)])].map(move).filter((e) => e !== void 0);
3758
+ };
3759
+ const exclude = excludeOf(config.exclude ?? [], excludeFromBase);
3760
+ const packageExclude = pkg?.exclude ? excludeOf(pkg.exclude, excludeFromPackage) : void 0;
3447
3761
  return {
3448
3762
  root,
3449
- configFile,
3763
+ configFile: configFile ?? declared?.config,
3450
3764
  components,
3451
- exclude: declared && config.exclude ? [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDE, ...exclude])] : exclude,
3452
- ...declared && config.exclude ? { designSystemExclude: exclude } : {},
3765
+ exclude: packageExclude ? [.../* @__PURE__ */ new Set([...exclude, ...packageExclude.filter((e) => !DEFAULT_EXCLUDE.includes(e))])] : exclude,
3766
+ ...packageExclude && inherits ? { designSystemExclude: packageExclude } : {},
3453
3767
  tokens: tokenSources.map((t) => ({
3454
3768
  ...t,
3455
3769
  path: slashGlob(t.path)
3456
3770
  })),
3457
3771
  ...!options.tokens?.length && configTokens === void 0 ? { tokensDetected: true } : {},
3458
- docs: (options.docs?.length ? options.docs : configDocs === void 0 ? [.../* @__PURE__ */ new Set([...DEFAULT_DOCS, ...detection?.docs ?? []])] : configDocs.map(fromBase)).map(slashGlob),
3772
+ docs: (options.docs?.length ? options.docs : configDocs ?? [.../* @__PURE__ */ new Set([...DEFAULT_DOCS, ...detection?.docs ?? []])]).map(slashGlob),
3459
3773
  tsconfig,
3460
- importPath: config.importPath,
3461
- imports: detection?.imports ?? (declared && componentsSet ? packageImports(root, declared.dir) : []),
3774
+ importPath,
3775
+ imports: detection?.imports ?? (declared && inherits && componentsSet ? packageImports(root, declared.dir) : []),
3462
3776
  detected: detection?.source,
3463
3777
  ...detection?.designSystems ? { designSystems: detection.designSystems } : {},
3464
3778
  tailwindConfig: findTailwindConfig(root),
3465
3779
  libraries: libraries.read,
3466
- libraryIds: config.libraries,
3780
+ libraryIds: librariesSetting,
3467
3781
  librariesOff: libraries.off,
3468
3782
  librariesUnread: libraries.unread,
3469
- elements: config.elements ?? {},
3470
- includeDesignSystem: options.includeDesignSystem ?? (declared ? void 0 : config.includeDesignSystem) ?? false,
3783
+ elements: {
3784
+ ...inherits ? pkg.elements : {},
3785
+ ...config.elements
3786
+ },
3787
+ includeDesignSystem: options.includeDesignSystem ?? config.includeDesignSystem ?? false,
3471
3788
  rules,
3472
3789
  overrides,
3473
- ...inherited && !declared ? { base } : {},
3790
+ ...base !== root ? { base } : {},
3474
3791
  ...declared ? { designSystemPackage: declared } : {},
3792
+ ...inherits ? { inherits: true } : {},
3793
+ ...config.extends === false ? { extendsOff: true } : {},
3475
3794
  configured: componentsSet,
3476
3795
  ...deprecations.length ? { deprecations } : {},
3477
3796
  ...warnings.length ? { warnings } : {}
@@ -3480,7 +3799,7 @@ async function loadConfigWith(options, context = {}) {
3480
3799
  /**
3481
3800
  * Whether `dir` has a config of its own: a config file under its current or old name, or one its
3482
3801
  * package.json names in its `onsystem` field. Without one, a project takes on its design-system
3483
- * package's config, else the workspace root's.
3802
+ * package's config, and, from a workspace root, the root's over it.
3484
3803
  */
3485
3804
  function hasOwnConfig(dir) {
3486
3805
  if ([...CONFIG_FILES, ...LEGACY_CONFIG_FILES].some((name) => fs.existsSync(path.join(dir, name)))) return true;
@@ -3558,6 +3877,14 @@ function nodeRunsTypeScript(version = process.versions.node) {
3558
3877
  const [major = 0, minor = 0] = version.replace(/^v/, "").split(".").map(Number);
3559
3878
  return major > 23 || major === 23 && minor >= 6 || major === 22 && minor >= 18;
3560
3879
  }
3880
+ /** A config file, read and validated: a JSON one only with `jsonConfigOnly`. */
3881
+ async function readConfig(file, options) {
3882
+ if (!fs.existsSync(file)) throw new ConfigError(`Config file not found: ${file}`);
3883
+ if (options.jsonConfigOnly && !file.endsWith(".json")) throw new ConfigError(`${file} was not run: the server runs a JavaScript or TypeScript config only in a project it was started in (its working directory, --root or the client's roots), not in one found from the path a tool call names. Start the server in the project, or use onsystem.config.json.`);
3884
+ const parsed = configSchema.safeParse(await readConfigFile(file));
3885
+ if (!parsed.success) throw new ConfigError(`Invalid config in ${file}:\n${z.prettifyError(parsed.error)}`);
3886
+ return parsed.data;
3887
+ }
3561
3888
  async function readConfigFile(file) {
3562
3889
  if (/\.[cm]?ts$/.test(file) && !nodeRunsTypeScript()) throw new ConfigError(`${file} is TypeScript, which Node.js runs from 22.18; this is ${process.version}. Use onsystem.config.json or .mjs, or Node.js 22.18 or later.`);
3563
3890
  if (file.endsWith(".json")) try {
@@ -6193,7 +6520,7 @@ function cssSheetTokens(sheets) {
6193
6520
  const category = isSpacingBase ? "spacing" : namespace?.[1] ?? "other";
6194
6521
  const tailwindKey = isSpacingBase ? "" : variable.name.slice(namespace?.[0].length ?? 0);
6195
6522
  const tailwindNamespace = isSpacingBase ? "--spacing" : namespace?.[0].slice(0, -1) ?? "";
6196
- const alias = /^var\(\s*(--[\w-]+)\s*\)$/.exec(variable.value)?.[1];
6523
+ const alias = themeAlias(variable.value, base, lookup);
6197
6524
  const target = alias ? base.get(alias) : void 0;
6198
6525
  if (alias && target) {
6199
6526
  consumed.add(alias);
@@ -6214,7 +6541,7 @@ function cssSheetTokens(sheets) {
6214
6541
  add(finalize({
6215
6542
  name: variable.name.slice(2),
6216
6543
  category,
6217
- value: variable.value,
6544
+ value: resolveValue(variable.value, lookup),
6218
6545
  modes: modeValues(variable.name, modes, lookup),
6219
6546
  cssVar: variable.inline ? void 0 : variable.name,
6220
6547
  tailwind: tailwindKey,
@@ -6345,6 +6672,22 @@ function categorize$1(name, value) {
6345
6672
  function varAlias(value) {
6346
6673
  return /^var\(\s*(--[\w-]+)\s*\)$/.exec(value.trim())?.[1];
6347
6674
  }
6675
+ /**
6676
+ * The base variable an `@theme` variable stands for: `--x` for `var(--x)`, and for
6677
+ * `hsl(var(--x))` or `rgb(var(--x))` when `--x` holds bare HSL or RGB channels, as
6678
+ * shadcn/ui's Tailwind v4 codemod leaves a v3 theme (`--color-primary: hsl(var(--primary))`).
6679
+ * The token is then `--x`, whose usage already wraps the channels (`hsl(var(--primary))`).
6680
+ */
6681
+ function themeAlias(value, base, lookup) {
6682
+ const plain = varAlias(value);
6683
+ if (plain) return plain;
6684
+ const match = /^(hsl|rgb)a?\(\s*var\(\s*(--[\w-]+)\s*\)\s*(?:\/\s*<alpha-value>\s*)?\)$/.exec(value.trim());
6685
+ const name = match?.[2];
6686
+ const target = name ? base.get(name) : void 0;
6687
+ if (!match || !name || !target) return void 0;
6688
+ const channels = resolveValue(target.value, lookup);
6689
+ return (match[1] === "hsl" ? isBareHsl(channels) : isBareRgb(channels)) ? name : void 0;
6690
+ }
6348
6691
  /** Follows plain `var(--x)` aliases so the token shows its concrete value. */
6349
6692
  function resolveValue(value, lookup, depth = 0) {
6350
6693
  const alias = /^var\(\s*(--[\w-]+)\s*\)$/.exec(value)?.[1];
@@ -7210,7 +7553,7 @@ function toDtcg(tokens) {
7210
7553
  const paths = assignPaths(tokens, warnings);
7211
7554
  const lookup = varLookup(tokens);
7212
7555
  const byName = new Map(tokens.map((t) => [t.name, t]));
7213
- const tree = {};
7556
+ const tree = Object.create(null);
7214
7557
  let count = 0;
7215
7558
  for (const category of TOKEN_CATEGORIES) for (const token of tokens) {
7216
7559
  const path = token.category === category ? paths.get(token) : void 0;
@@ -7292,7 +7635,7 @@ function insert(tree, path, node) {
7292
7635
  const next = group[key];
7293
7636
  if (isObject$1(next)) group = next;
7294
7637
  else {
7295
- const created = {};
7638
+ const created = Object.create(null);
7296
7639
  group[key] = created;
7297
7640
  group = created;
7298
7641
  }
@@ -7524,7 +7867,7 @@ function colorObject(css) {
7524
7867
  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
7868
  return out;
7526
7869
  }
7527
- const PLAIN_LENGTH = /^(-?(?:\d+\.?\d*|\.\d+))(px|rem)$/;
7870
+ const PLAIN_LENGTH = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(px|rem)$/;
7528
7871
  /** `0.5rem` → `{ value: 0.5, unit: "rem" }`; a `calc()` of tokens → its px value. */
7529
7872
  function dimensionValue(value, lookup) {
7530
7873
  const plain = plainDimension(value);
@@ -7554,8 +7897,8 @@ function plainDimension(value) {
7554
7897
  unit: match[2] ?? "px"
7555
7898
  } : void 0;
7556
7899
  }
7557
- const COMPOSITE = /^[\w-]+\s*:\s*[^;]+(?:;\s*[\w-]+\s*:\s*[^;]+)*;?$/;
7558
- const NUMBER = /^-?(?:\d+\.?\d*|\.\d+)$/;
7900
+ const COMPOSITE = /^[\w-]+\s*:[^;]+(?:;\s*[\w-]+\s*:[^;]+)*;?$/;
7901
+ const NUMBER = /^-?(?:\d+(?:\.\d*)?|\.\d+)$/;
7559
7902
  /** What a typography token sets, from its name and Tailwind class (`font-sans`, `text-sm`). */
7560
7903
  function typographyKind(token) {
7561
7904
  const value = token.value.trim();
@@ -7815,7 +8158,7 @@ function frontMatterValue(token, key, lookup) {
7815
8158
  }
7816
8159
  }
7817
8160
  /** A DESIGN.md dimension: px, em or rem. */
7818
- const DESIGN_DIMENSION = /^-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)$/;
8161
+ const DESIGN_DIMENSION = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:px|rem|em)$/;
7819
8162
  /**
7820
8163
  * A typography token as a DESIGN.md typography level: `{ fontFamily: "Inter" }` for
7821
8164
  * `--font-sans`, every property of a composite. A `var()` is no font name (`next/font` sets
@@ -7858,7 +8201,8 @@ function table(tokens, keys) {
7858
8201
  use(token),
7859
8202
  ...described ? [token.description ?? ""] : []
7860
8203
  ]);
7861
- const line = (cells) => `| ${cells.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`;
8204
+ const oneLine = (text) => text.split(/\r\n?|\n/).map((part) => part.trim()).filter(Boolean).join(" ");
8205
+ const line = (cells) => `| ${cells.map((c) => oneLine(c).replace(/\|/g, "\\|")).join(" | ")} |`;
7862
8206
  return [
7863
8207
  line(header),
7864
8208
  line(header.map(() => "---")),
@@ -11677,7 +12021,7 @@ function isObject(value) {
11677
12021
  var name = "onsystem";
11678
12022
  //#endregion
11679
12023
  //#region src/version.ts
11680
- const VERSION = "0.9.0";
12024
+ const VERSION = "0.10.0";
11681
12025
  const NAME = name;
11682
12026
  //#endregion
11683
12027
  //#region src/lint/sarif.ts
@@ -12505,55 +12849,6 @@ function searchComponents(index, query, limit = 5) {
12505
12849
  }));
12506
12850
  }
12507
12851
  //#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
12852
  //#region src/util/log.ts
12558
12853
  const stderrLogger = {
12559
12854
  info: (message) => process.stderr.write(`[onsystem] ${message}\n`),
@@ -13134,15 +13429,16 @@ var DesignSystemHost = class {
13134
13429
  this.watchConfigFiles();
13135
13430
  }
13136
13431
  /**
13137
- * Watches the config file, the tsconfig and the tsconfigs it extends through
13138
- * their directories: editors save by renaming a new file over the old one,
13139
- * which ends a watch on the file. Called again after each build, so a new
13140
- * `extends` target is picked up.
13432
+ * Watches the config file, the config a design-system package declares, which a project with
13433
+ * a config of its own reads under that one, the tsconfig and the tsconfigs it extends through
13434
+ * their directories: editors save by renaming a new file over the old one, which ends a watch
13435
+ * on the file. Called again after each build, so a new `extends` target is picked up.
13141
13436
  */
13142
13437
  watchConfigFiles() {
13143
13438
  const { root, configFile, tsconfig, designSystemPackage } = this.config;
13144
13439
  const files = [
13145
13440
  configFile,
13441
+ designSystemPackage?.config,
13146
13442
  designSystemPackage && path.join(designSystemPackage.dir, "package.json"),
13147
13443
  ...DETECTION_FILES.map((f) => path.join(root, f)),
13148
13444
  ...tsconfig ? [path.resolve(root, tsconfig)] : PROJECT_CONFIG_FILES.map((f) => path.join(root, f)),
@@ -13200,7 +13496,7 @@ var DesignSystemHost = class {
13200
13496
  const { root, configFile, designSystemPackage, warnings } = this.config;
13201
13497
  const load = this.options.loadConfig ?? (() => loadConfig({
13202
13498
  root,
13203
- config: designSystemPackage ? void 0 : configFile
13499
+ config: configFile === designSystemPackage?.config ? void 0 : configFile
13204
13500
  }));
13205
13501
  try {
13206
13502
  this.config = await load();
@@ -13239,4 +13535,4 @@ var DesignSystemHost = class {
13239
13535
  }
13240
13536
  };
13241
13537
  //#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 };
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 };