onsystem 0.5.0 → 0.7.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.
@@ -1,4 +1,4 @@
1
- import { a as DesignSystemModel, x as ResolvedConfig } from "./types-DJtccu3A.js";
1
+ import { h as DesignSystemModel, i as ResolvedConfig } from "./config-1XSTYI44.js";
2
2
  import { MessagePort } from "node:worker_threads";
3
3
  //#region src/eslint/worker.d.ts
4
4
  export interface LoadRequest {
@@ -1,5 +1,5 @@
1
- import { B as DEFAULT_TEST_FOLDERS, i as componentFiles, it as relativePath, r as buildModel, rt as matchesGlob, z as DEFAULT_TEST_EXCLUDE } from "./design-system-CnEv5sIL.js";
2
- import { r as loadTarget } from "./workspace-DzWQ4L8R.js";
1
+ import { Ct as relativePath, K as DEFAULT_TEST_EXCLUDE, St as matchesGlob, i as componentFiles, q as DEFAULT_TEST_FOLDERS, r as buildModel } from "./design-system-C4bhqLLw.js";
2
+ import { r as loadTarget } from "./workspace-Z0tZf_Dv.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { parentPort } from "node:worker_threads";
@@ -9,6 +9,8 @@ import { parentPort } from "node:worker_threads";
9
9
  * project's design system does not (config files written in code, globs, the extraction
10
10
  * cache), so the plugin asks this worker and waits for the answer (see `bridge.ts`).
11
11
  */
12
+ /** What the configs said (a design-system package's skipped, or its paths ignored), each said once. */
13
+ const warned = /* @__PURE__ */ new Set();
12
14
  /** How long a loaded design system is trusted before its files are looked at again. */
13
15
  const FRESH_MS = 2e3;
14
16
  const targets = /* @__PURE__ */ new Map();
@@ -44,6 +46,11 @@ async function targetFor({ cwd, root, config }) {
44
46
  async function load(request) {
45
47
  const target = await targetFor(request);
46
48
  const config = target.workspace ? (await target.workspace.projectFor(request.file)).config : target.config;
49
+ for (const text of config.warnings ?? []) {
50
+ if (warned.has(text)) continue;
51
+ warned.add(text);
52
+ process.stderr.write(`onsystem: ${text}\n`);
53
+ }
47
54
  let project = projects.get(config.root);
48
55
  if (!project || Date.now() - project.at >= FRESH_MS) {
49
56
  const { model, key } = await buildModel(config, { cache: true });
package/dist/eslint.js CHANGED
@@ -1,4 +1,4 @@
1
- import { B as DEFAULT_TEST_FOLDERS, R as DEFAULT_SEVERITY, T as RULES, _ as VERSION, g as NAME, it as relativePath, t as DesignSystem, z as DEFAULT_TEST_EXCLUDE } from "./design-system-CnEv5sIL.js";
1
+ import { Ct as relativePath, G as DEFAULT_SEVERITY, K as DEFAULT_TEST_EXCLUDE, _ as NAME, k as RULES, q as DEFAULT_TEST_FOLDERS, t as DesignSystem, v as VERSION } from "./design-system-C4bhqLLw.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
@@ -96,8 +96,63 @@ function run(cwd, file, text, settings) {
96
96
  designSystems.set(response.root, entry);
97
97
  }
98
98
  if (!entry) return { error: `no design system loaded for ${response.root}` };
99
- const result = entry.ds.check(text, relativePath(entry.ds.root, file));
100
- return result.skipped ? { skipped: true } : { result };
99
+ const { ds } = entry;
100
+ const relative = relativePath(ds.root, file);
101
+ const result = ds.check(text, relative);
102
+ if (result.skipped) return { skipped: true };
103
+ return {
104
+ result,
105
+ fromSystem: (specifier) => ds.lint.isDesignSystemImport(specifier, relative)
106
+ };
107
+ }
108
+ /**
109
+ * The name an edit gives a JSX tag when that name does not mean the component there: `Button`
110
+ * for `<button>` → `<Button>` in a file that does not import it, imports it as a type only, or
111
+ * imports another `Button` from outside the design system. Applied, such a rename leaves a file
112
+ * that does not compile.
113
+ */
114
+ function unboundRename(sourceCode, edit, fromSystem) {
115
+ if (!edit.text.includes(".") && /^[a-z]/.test(edit.text)) return;
116
+ const [start, end] = edit.range;
117
+ let node = sourceCode.getNodeByRangeIndex(start);
118
+ while (node && (node.range[0] !== start || node.range[1] !== end)) node = node.parent ?? null;
119
+ const tag = node?.parent;
120
+ if (!node || !tag || !["JSXOpeningElement", "JSXClosingElement"].includes(tag.type)) return;
121
+ if (tag.name !== node) return;
122
+ const name = edit.text.split(".")[0] ?? edit.text;
123
+ for (let scope = sourceCode.getScope(node); scope; scope = scope.upper) {
124
+ const values = scope.set.get(name)?.defs.filter((def) => !typeOnly(def)) ?? [];
125
+ if (!values.length) continue;
126
+ return values.some((def) => isComponent(def, fromSystem)) ? void 0 : edit.text;
127
+ }
128
+ return edit.text;
129
+ }
130
+ /**
131
+ * Whether a value binding can be the component a rename names: an import from the design
132
+ * system, or a function, class, variable or parameter of the file. Not an import of the same
133
+ * name from elsewhere (`Table<TData>` from @tanstack/react-table without `type`, an app's own
134
+ * `Label`), nor an enum or a namespace.
135
+ */
136
+ function isComponent(def, fromSystem) {
137
+ if (def.type === "ImportBinding") {
138
+ const source = def.parent?.source?.value;
139
+ return typeof source === "string" && fromSystem(source);
140
+ }
141
+ return [
142
+ "FunctionName",
143
+ "ClassName",
144
+ "Variable",
145
+ "Parameter"
146
+ ].includes(def.type);
147
+ }
148
+ /**
149
+ * A binding that is only a type: a type alias, interface or type parameter (typescript-eslint's
150
+ * `Type` definition), or `import type { Button }` and `import { type Button }` (its `importKind`).
151
+ */
152
+ function typeOnly(def) {
153
+ if (def.type === "Type") return true;
154
+ const kind = (node) => node?.importKind;
155
+ return def.type === "ImportBinding" && (kind(def.node) === "type" || kind(def.parent) === "type");
101
156
  }
102
157
  function createRule(id, description) {
103
158
  return {
@@ -108,6 +163,7 @@ function createRule(id, description) {
108
163
  url: `${RULES_PAGE}#${id}`
109
164
  },
110
165
  fixable: "code",
166
+ hasSuggestions: true,
111
167
  schema: []
112
168
  },
113
169
  create(context) {
@@ -128,7 +184,9 @@ function createRule(id, description) {
128
184
  }
129
185
  for (const d of outcome.result.diagnostics) {
130
186
  if (d.ruleId !== id) continue;
131
- const edits = d.fix;
187
+ const edits = d.fix ?? [];
188
+ const fix = (fixer) => edits.map((e) => fixer.replaceTextRange(e.range, e.text));
189
+ const unbound = edits.map((e) => unboundRename(context.sourceCode, e, outcome.fromSystem)).find((name) => name !== void 0);
132
190
  context.report({
133
191
  loc: {
134
192
  start: {
@@ -141,7 +199,10 @@ function createRule(id, description) {
141
199
  }
142
200
  },
143
201
  message: d.message,
144
- ...edits?.length ? { fix: (fixer) => edits.map((e) => fixer.replaceTextRange(e.range, e.text)) } : {}
202
+ ...!edits.length ? {} : unbound ? { suggest: [{
203
+ desc: `Rename to <${unbound}>, then import it`,
204
+ fix
205
+ }] } : { fix }
145
206
  });
146
207
  }
147
208
  } };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as RuleId, D as loadConfig, E as defineConfig, O as ImportMapping, S as ResolvedRule, T as configJsonSchema, _ as VariantInfo, a as DesignSystemModel, b as RULE_IDS, c as ExampleInfo, d as Severity, f as SourceLocation, g as TokenCategory, h as Token, i as CompoundVariantInfo, l as InheritedProps, m as TextEdit, n as ComponentDocs, o as Diagnostic, p as TOKEN_CATEGORIES, r as ComponentInfo, s as DocSection, t as CheckResult, u as PropInfo, v as Config, w as RuleOptions, x as ResolvedConfig, y as LoadConfigOptions } from "./types-DJtccu3A.js";
1
+ import { C as TOKEN_CATEGORIES, D as VariantInfo, E as TokenCategory, O as ImportMapping, S as SourceLocation, T as Token, _ as DocSection, a as ResolvedRule, b as PropInfo, c as configJsonSchema, d as CheckResult, f as ComponentDocs, g as Diagnostic, h as DesignSystemModel, i as ResolvedConfig, l as defineConfig, m as CompoundVariantInfo, n as LoadConfigOptions, o as RuleId, p as ComponentInfo, r as RULE_IDS, s as RuleOptions, t as Config, u as loadConfig, v as ExampleInfo, w as TextEdit, x as Severity, y as InheritedProps } from "./config-1XSTYI44.js";
2
2
  import ts from "typescript";
3
3
  import { Oklch } from "culori";
4
4
  import { McpServer, Transport } from "@modelcontextprotocol/server";
@@ -115,6 +115,12 @@ export declare function parseCssTokens(css: string, file: string): {
115
115
  warnings: string[];
116
116
  };
117
117
  //#endregion
118
+ //#region src/tokens/design-md.d.ts
119
+ export declare function parseDesignMd(text: string, file: string): {
120
+ tokens: Token[];
121
+ warnings: string[];
122
+ };
123
+ //#endregion
118
124
  //#region src/tokens/dtcg.d.ts
119
125
  interface DtcgOptions {
120
126
  /** Prefix for generated custom property names: `ds` → `--ds-color-primary`. */
@@ -125,6 +131,34 @@ export declare function parseDtcgTokens(text: string, file: string, options?: Dt
125
131
  warnings: string[];
126
132
  };
127
133
  //#endregion
134
+ //#region src/tokens/export.d.ts
135
+ /**
136
+ * Writes the tokens onsystem found as a DTCG 2025.10 file or a DESIGN.md. Both are grouped by
137
+ * category; reading the DTCG file back gives the same tokens, with the same names.
138
+ */
139
+ interface Exported {
140
+ text: string;
141
+ /** How many tokens were written. */
142
+ count: number;
143
+ /** Tokens left out, and why. */
144
+ warnings: string[];
145
+ }
146
+ /**
147
+ * The tokens as a DTCG 2025.10 file: a group per category, colors as color objects in their
148
+ * own color space, lengths as `{ value, unit }`, aliases as references, and what DTCG has no
149
+ * field for (the token's name, the custom property, the Tailwind theme variable, dark and
150
+ * other modes) under `$extensions["com.github.dgesteves.onsystem"]`.
151
+ */
152
+ export declare function toDtcg(tokens: readonly Token[]): Exported;
153
+ /**
154
+ * The tokens as a DESIGN.md: front matter with the colors, typography, rounded corners and
155
+ * spacing in the spec's schema, then a heading per category with a table of its tokens, their
156
+ * values in each mode and how code uses them.
157
+ */
158
+ export declare function toDesignMd(tokens: readonly Token[], options: {
159
+ name: string;
160
+ }): Exported;
161
+ //#endregion
128
162
  //#region src/tokens/index.d.ts
129
163
  interface TokenSource {
130
164
  file: string;
@@ -180,8 +214,19 @@ interface ColorSuggestion {
180
214
  reason?: 'far' | 'hue' | 'role' | 'status' | 'scoped' | undefined;
181
215
  /** With reason `status`: how far apart the hues are, in degrees. */
182
216
  hueOff?: number | undefined;
217
+ /**
218
+ * With reason `role`: a token that is close and of the same hue but made for
219
+ * another role (`content-muted`, a text color, for a border), when it is not the nearest.
220
+ */
221
+ otherRole?: Nearest<ColorCandidate> | undefined;
183
222
  /** Other tokens with the nearest one's value. */
184
223
  sameValue: Token[];
224
+ /**
225
+ * With a match that is no status color: a close status color of the color's hue that is
226
+ * about as near (within STATUS_MARGIN), so the color may mean that status (a red error
227
+ * message next to a rose brand), and the match is no sure fix.
228
+ */
229
+ closeStatus?: Nearest<ColorCandidate> | undefined;
185
230
  }
186
231
  interface NearestLength extends Nearest<LengthCandidate> {
187
232
  /** A radius so far above the scale that it means "fully rounded" (`rounded-full`). */
@@ -261,6 +306,8 @@ declare class LintTarget {
261
306
  private readonly moduleDirs;
262
307
  /** The files extraction read, when the model says. */
263
308
  private readonly sources?;
309
+ /** Published libraries the model read from type declarations: `@mui/material`. */
310
+ readonly libraries: Set<string>;
264
311
  /**
265
312
  * `resolver` finds the module an import points at and what it exports
266
313
  * (`@calcom/ui/components/icon` → `components/icon/index.ts`), so a name from
@@ -275,6 +322,14 @@ declare class LintTarget {
275
322
  * directory itself (a barrel: `../components/ui` and its index).
276
323
  */
277
324
  isDesignSystemImport(specifier: string, fromFile?: string): boolean;
325
+ /**
326
+ * Whether the component rules judge the components imported from `specifier`: it points at
327
+ * the design system, and the model has components to judge them by. A model with none (an
328
+ * app whose design-system package publishes only compiled `dist/`) still matches the
329
+ * configured `importPath`, but the component rules do not run there, as `check` says, so
330
+ * what comes from it is not called invented.
331
+ */
332
+ checksImport(specifier: string, fromFile?: string): boolean;
278
333
  /**
279
334
  * Whether `name`, imported from the design system, may be a real export the
280
335
  * model left out rather than an invented one: its module exists outside the
@@ -316,6 +371,10 @@ declare class LintTarget {
316
371
  /** The component a module exports as `default`, for `import Anything from "..."`. */
317
372
  defaultExport(specifier: string, fromFile?: string): ComponentInfo | undefined;
318
373
  private resolveRelative;
374
+ /** The library an import specifier points into (`@mui/material/Button`), if any. */
375
+ libraryOf(specifier: string): string | undefined;
376
+ /** The library's own component of that name: `Button` from `@mui/material`, not a wrapper. */
377
+ libraryComponent(name: string, library: string): ComponentInfo | undefined;
319
378
  /** Component names for "did you mean" suggestions. */
320
379
  names(): string[];
321
380
  }
@@ -480,6 +539,12 @@ export declare class DesignSystem {
480
539
  readonly config: ResolvedConfig;
481
540
  readonly lint: LintTarget;
482
541
  private readonly searchIndex;
542
+ /**
543
+ * The components the tools offer: all of them, except a library's (MUI's) where the project
544
+ * has its own of that name, a wrapper the team means to be used. The rules still check code
545
+ * that imports the library's directly against the library's.
546
+ */
547
+ private readonly offered;
483
548
  constructor(model: DesignSystemModel, config: ResolvedConfig);
484
549
  get root(): string;
485
550
  get components(): ComponentInfo[];
@@ -498,6 +563,14 @@ export declare class DesignSystem {
498
563
  }): Token[];
499
564
  /** Tokens a component's classes and CSS variables reference, e.g. `bg-primary` → `primary`. */
500
565
  relatedTokens(component: ComponentInfo): Token[];
566
+ /**
567
+ * Whether `check --require-design-system` (and the GitHub Action, where it is on by default)
568
+ * finds a design system to check against: components and color tokens, or components from a
569
+ * published library (MUI). A library is the design system whatever its theme: when the theme's
570
+ * colors cannot be read (a palette from Sass variables), the check still runs and `notice()`
571
+ * says that no-hardcoded-color did not.
572
+ */
573
+ hasDesignSystem(): boolean;
501
574
  /**
502
575
  * What the rules could not check, when it matters: without components the
503
576
  * component rules find nothing, and without color tokens neither does
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { A as parseDtcgTokens, D as loadTokens, E as TokenIndex, G as loadConfig, H as RULE_IDS, M as attachDocs, N as parseDoc, O as mergeTokens, P as extractComponents, T as RULES, U as configJsonSchema, W as defineConfig, _ as VERSION, a as fingerprint, d as searchComponents, f as applyFixes, h as formatDiagnostics, j as parseCssTokens, n as DesignSystemHost, o as loadDesignSystem, p as checkSource, q as findVariantDefinitions, r as buildModel, t as DesignSystem } from "./design-system-CnEv5sIL.js";
2
- import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-DjfkiDMV.js";
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, parseDoc, parseDtcgTokens, searchComponents, serveStdio };
1
+ import { $ as loadConfig, A as TokenIndex, B as parseDoc, I as parseDtcgTokens, L as parseDesignMd, M as mergeTokens, N as toDesignMd, P as toDtcg, R as parseCssTokens, V as extractComponents, X as configJsonSchema, Y as RULE_IDS, Z as defineConfig, a as fingerprint, d as searchComponents, f as applyFixes, g as formatDiagnostics, j as loadTokens, k as RULES, n as DesignSystemHost, o as loadDesignSystem, p as checkSource, r as buildModel, rt as findVariantDefinitions, t as DesignSystem, v as VERSION, z as attachDocs } from "./design-system-C4bhqLLw.js";
2
+ import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-BojAQ6JP.js";
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,25 +1,216 @@
1
- import { $ as plural, F as CONFIG_FILES, G as loadConfig, Q as closest, V as LEGACY_CONFIG_FILES, _ as VERSION, d as searchComponents, et as truncate, g as NAME, it as relativePath, k as formatPx, m as uncheckedNotice, n as DesignSystemHost, nt as isInside, tt as unique, u as buildSearchIndex } from "./design-system-CnEv5sIL.js";
2
- import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-DzWQ4L8R.js";
1
+ import { $ as loadConfig, Ct as relativePath, F as TOKEN_CATEGORIES, G as DEFAULT_SEVERITY, H as CONFIG_FILES, J as LEGACY_CONFIG_FILES, Tt as defaultProjectConfig, Y as RULE_IDS, _ as NAME, _t as closest, at as LIBRARY_IDS, bt as unique, d as searchComponents, dt as detectProject, ft as findDesignSystemPackage, it as LIBRARIES, m as uncheckedNotice, n as DesignSystemHost, nt as formatPx, ot as libraryTitle, st as notReadYet, u as buildSearchIndex, v as VERSION, vt as plural, xt as isInside, yt as truncate } from "./design-system-C4bhqLLw.js";
2
+ import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-Z0tZf_Dv.js";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import * as z from "zod";
7
- import fs$1 from "node:fs/promises";
7
+ import fsp from "node:fs/promises";
8
8
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
9
9
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
10
- //#region src/types.ts
10
+ //#region src/explain.ts
11
11
  /**
12
- * The design-system model the server exposes. Everything here is plain JSON so
13
- * it can be cached on disk and returned as MCP structured content.
12
+ * What `inspect --explain` says about one project: every candidate zero config looked
13
+ * at and why each was taken or not, the config it resolved to, tokens whose value did
14
+ * not resolve, and components without props, with the likely reason.
14
15
  */
15
- const TOKEN_CATEGORIES = [
16
- "color",
17
- "spacing",
18
- "radius",
19
- "typography",
20
- "shadow",
21
- "other"
22
- ];
16
+ function explainProject(ds, cwd, indent = "") {
17
+ const { config, model } = ds;
18
+ const lines = [];
19
+ const out = (line = "") => lines.push(line ? `${indent}${line}` : "");
20
+ out("How the design system was found");
21
+ for (const step of packageSteps(config, cwd)) out(` ${(step.accepted ? "found" : "no").padEnd(6)} ${step.candidate}: ${step.reason}`);
22
+ if (config.configured) out(` components come from ${config.configFile ? display(cwd, config.configFile) : "--components"}, so zero config did not look`);
23
+ else {
24
+ const steps = [];
25
+ detectProject(config.root, config.tsconfig, steps, config.designSystemPackage);
26
+ for (const step of steps) out(` ${(step.accepted ? "found" : "no").padEnd(6)} ${step.candidate}: ${step.reason}`);
27
+ if (!config.detected) out(` So the defaults apply: ${config.components.join(", ")}${model.stats.files.components ? "" : ", which match nothing"}.`);
28
+ }
29
+ const named = (library) => `${library.package}${library.version ? ` ${library.version}` : ""}`;
30
+ for (const library of config.libraries ?? []) {
31
+ const count = model.components.filter((c) => c.library === library.package).length;
32
+ out(library.dir ? ` ${"found".padEnd(6)} ${named(library)}: a dependency, ${count} components from its type declarations in ${display(cwd, library.dir)}` : ` ${"no".padEnd(6)} ${named(library)}: a dependency, but not installed`);
33
+ }
34
+ for (const library of config.librariesOff ?? []) out(` ${"no".padEnd(6)} ${named(library)}: a dependency, ${librariesLeftOut(config, cwd)}`);
35
+ for (const library of config.librariesUnread ?? []) out(` ${"no".padEnd(6)} ${named(library)}: ${notReadYet()}`);
36
+ out();
37
+ out("Resolved config");
38
+ const field = (name, value) => out(` ${name.padEnd(12)} ${value}`);
39
+ field("root", config.root);
40
+ field("config", config.configFile ? `${display(cwd, config.configFile)}${config.designSystemPackage ? ` (declared by ${config.designSystemPackage.name})` : ""}` : "(none)");
41
+ field("components", listOf$3(config.components) || "(none)");
42
+ field("exclude", listOf$3(config.exclude));
43
+ field("tokens", listOf$3(config.tokens.map((t) => `${t.path}${t.prefix ? ` (prefix ${t.prefix})` : ""}${t.fallback ? " (when nothing else is found)" : ""}`)));
44
+ field("docs", listOf$3(config.docs));
45
+ const projectConfig = defaultProjectConfig(config.root);
46
+ field("tsconfig", config.tsconfig ?? (projectConfig ? display(config.root, projectConfig) : "(none)"));
47
+ field("importPath", importPaths(ds));
48
+ field("libraries", config.libraryIds === void 0 ? `every library onsystem reads (${LIBRARY_IDS.join(", ")})` : config.libraryIds !== false && config.libraryIds.length ? config.libraryIds.join(", ") : `none ("libraries": ${JSON.stringify(config.libraryIds)})`);
49
+ if (config.tailwindConfig) field("tailwind", display(config.root, config.tailwindConfig));
50
+ if (config.designSystems) field("primary", config.designSystems.map((d, i) => `${i ? "then " : ""}${d.source} (${d.imports} imports)`).join("; "));
51
+ const changed = RULE_IDS.filter((id) => config.rules[id].severity !== DEFAULT_SEVERITY[id]);
52
+ field("rules", `${RULE_IDS.map((id) => `${id} ${config.rules[id].severity}`).join(", ")}${changed.length ? ` (changed: ${changed.join(", ")})` : ""}`);
53
+ if (config.overrides?.length) for (const override of config.overrides) field("override", `${override.files.join(", ")}: ${Object.entries(override.rules).map(([id, rule]) => `${id} ${rule.severity}`).join(", ")}`);
54
+ if (config.base) field("relative to", `${config.base} (a config at the workspace root)`);
55
+ out();
56
+ out("Tokens");
57
+ const found = ds.tokens;
58
+ const files = model.stats.files.tokens;
59
+ const themed = found.filter((t) => t.origin === "library");
60
+ out(` ${found.length - themed.length} tokens from ${files} ${files === 1 ? "file" : "files"}${files ? "" : `: no file matches ${config.tokens.map((t) => t.path).join(", ")}`}`);
61
+ if (themed.length) {
62
+ const sources = [...new Set(themed.map((t) => t.source.file))];
63
+ out(` ${themed.length} tokens from a library's theme in ${sources.join(", ")}`);
64
+ }
65
+ const defined = new Set(found.map((t) => t.cssVar).filter((v) => Boolean(v)));
66
+ const missing = (value) => [...value.matchAll(/var\(\s*(--[\w-]+)/g)].map((m) => m[1] ?? "").filter((v) => !defined.has(v));
67
+ const unresolved = found.filter((t) => missing(t.value).length);
68
+ if (unresolved.length) out(` ${unresolved.length} unresolved, reading a custom property no token file defines: ${unresolved.slice(0, 8).map((t) => `${t.cssVar ?? t.name}: ${t.value}`).join(", ")}${unresolved.length > 8 ? `, and ${unresolved.length - 8} more` : ""}`);
69
+ else if (found.length) out(" every value resolves");
70
+ for (const warning of model.warnings.filter((w) => /alias|token/i.test(w))) out(` ${warning}`);
71
+ out();
72
+ out("Props");
73
+ const empty = ds.roots().filter((c) => c.openProps && !c.props.length && !c.inherits.length);
74
+ const open = ds.components.filter((c) => c.openProps);
75
+ if (!open.length) out(` every component's props resolved (${ds.components.length} components)`);
76
+ else {
77
+ const installed = nodeModulesFor(config.root);
78
+ const names = (list) => `${list.slice(0, 6).map((c) => c.name).join(", ")}${list.length > 6 ? ` and ${list.length - 6} more` : ""}`;
79
+ if (empty.length) out(` ${empty.length} ${empty.length === 1 ? "component has" : "components have"} 0 props: ${names(empty)}.`);
80
+ out(` ${open.length} ${open.length === 1 ? "component has" : "components have"} props that did not fully resolve${empty.length === open.length ? "" : `: ${names(open)}`}, so no-unknown-prop skips ${open.length === 1 ? "it" : "them"}.`);
81
+ out(installed ? ` Dependencies are installed (${display(cwd, installed)}): a package without types, a type error, or a props type that depends on a type argument leaves them open.` : ` The dependencies are not installed: no node_modules in ${config.root} or a folder above it. Their props come from the packages they wrap (Radix, React Aria), so install them (npm install, pnpm install) and run again.`);
82
+ }
83
+ return lines;
84
+ }
85
+ /** Why a library the project depends on is not read: `left out by "libraries" in onsystem.config.json`. */
86
+ function librariesLeftOut(config, cwd) {
87
+ return `left out by "libraries" in ${config.configFile ? display(cwd, config.configFile) : "the config"}`;
88
+ }
89
+ /** The first few, then how many more: a package's exports can name hundreds of files. */
90
+ function listOf$3(items, max = 8) {
91
+ const shown = items.slice(0, max).join(", ");
92
+ return items.length > max ? `${shown}, and ${items.length - max} more` : shown;
93
+ }
94
+ /** How components are suggested to be imported. */
95
+ function importPaths(ds) {
96
+ if (ds.config.importPath) return `${ds.config.importPath} (from the config)`;
97
+ const counts = /* @__PURE__ */ new Map();
98
+ for (const c of ds.roots()) counts.set(c.importPath, (counts.get(c.importPath) ?? 0) + 1);
99
+ const top = [...counts].sort((a, b) => b[1] - a[1]).slice(0, 3);
100
+ const from = ds.config.imports?.length ? "package exports" : "tsconfig paths or relative paths";
101
+ return top.length ? `inferred from ${from}: ${top.map(([p, n]) => `${p} (${n})`).join(", ")}${counts.size > 3 ? `, and ${counts.size - 3} more` : ""}` : `inferred from ${from}`;
102
+ }
103
+ /** The nearest node_modules at or above `dir`, if any. */
104
+ function nodeModulesFor(dir) {
105
+ for (let current = path.resolve(dir);; current = path.dirname(current)) {
106
+ const candidate = path.join(current, "node_modules");
107
+ if (fs.existsSync(candidate)) return candidate;
108
+ if (current === path.dirname(current)) return void 0;
109
+ }
110
+ }
111
+ function display(cwd, file) {
112
+ const rel = relativePath(cwd, file);
113
+ return rel && !rel.startsWith("..") ? rel : file;
114
+ }
115
+ /**
116
+ * Whether a dependency's declared config was taken on, for a project without a config of its
117
+ * own: the package it came from and the others that declare one, or that none does.
118
+ */
119
+ function packageSteps(config, cwd) {
120
+ const declared = config.designSystemPackage;
121
+ if (config.configFile && !declared) return [];
122
+ const lookup = findDesignSystemPackage(config.root);
123
+ if (!lookup) return [];
124
+ const candidate = "a design-system package's config";
125
+ const skipped = lookup.skipped.length ? `; ${lookup.skipped.map((p) => p.name).join(", ")} ${lookup.skipped.length === 1 ? "declares one" : "declare one"} that cannot be read, skipped since installed` : "";
126
+ if (!declared) return [{
127
+ candidate,
128
+ accepted: false,
129
+ reason: `no dependency declares one ("onsystem" in its package.json) among ${lookup.dependencies} ${lookup.dependencies === 1 ? "dependency" : "dependencies"}${skipped}`
130
+ }];
131
+ return [{
132
+ candidate,
133
+ accepted: true,
134
+ reason: `${declared.name} (${declared.installed ? "installed" : "a workspace package"}) declares ${display(cwd, declared.config)}, which this project takes on, with its paths from ${display(cwd, declared.dir)}${lookup.others.length ? `; ${lookup.others.join(", ")} also declare one, not read: one config is taken on, from the package the code imports most` : ""}${skipped}`
135
+ }];
136
+ }
137
+ //#endregion
138
+ //#region src/not-read.ts
139
+ const LIMITS_DOCS = "https://onsystem.vercel.app/docs/how-it-works#limits";
140
+ /**
141
+ * What `check` and `check_ui` could not read, said after the results as the notice for missing
142
+ * components and tokens is, so a clean result is not mistaken for a checked one: a component
143
+ * library the project depends on that onsystem does not read yet, and dependencies that are not
144
+ * installed. Projects with the same gap share one notice that names them.
145
+ */
146
+ function notReadNotices(projects) {
147
+ return [...unreadLibraries(projects), missingDependencies(projects)].filter((notice) => notice !== void 0);
148
+ }
149
+ /**
150
+ * A component library onsystem does not read yet (Chakra UI, Mantine), in a project without
151
+ * components of its own, whose code is then checked by the accessibility rule alone. One notice
152
+ * per set of libraries.
153
+ */
154
+ function unreadLibraries(projects) {
155
+ const groups = /* @__PURE__ */ new Map();
156
+ for (const { ds, dir } of projects) {
157
+ const packages = (ds.config.librariesUnread ?? []).map((library) => library.package);
158
+ if (!packages.length || ds.components.length) continue;
159
+ const key = packages.join(" ");
160
+ const group = groups.get(key) ?? {
161
+ packages,
162
+ dirs: []
163
+ };
164
+ if (dir !== void 0) group.dirs.push(dir);
165
+ groups.set(key, group);
166
+ }
167
+ return [...groups.values()].map(({ packages, dirs }) => {
168
+ const one = packages.length === 1;
169
+ const names = listOf$2(packages.map((name) => `${libraryTitle(name)} (${name})`), packages.length);
170
+ const of = dirs.length ? ` of ${listOf$2(dirs)}` : "";
171
+ return `${names} ${one ? "is a dependency" : "are dependencies"}${of}, but onsystem does not read ${one ? "it" : "them"} yet: of the published component libraries, it reads only ${LIBRARIES.map((l) => l.title).join(", ")} so far. Wrap ${one ? "its" : "their"} components in your own and point "components" in the config at those. See ${LIMITS_DOCS}`;
172
+ });
173
+ }
174
+ /**
175
+ * Dependencies that are not installed: no `node_modules` at or above a project that declares
176
+ * some, so the props of components that come from them (Radix, React's own) do not resolve and
177
+ * `no-unknown-prop` skips those components. For plain `inspect` too, on stderr.
178
+ */
179
+ function missingDependencies(projects) {
180
+ const dirs = [];
181
+ let skipped = 0;
182
+ for (const { ds, dir } of projects) {
183
+ if (ds.config.rules["no-unknown-prop"].severity === "off") continue;
184
+ const open = ds.components.filter((c) => c.openProps).length;
185
+ if (!open || nodeModulesFor(ds.config.root) || !declaresDependencies(ds.config.root)) continue;
186
+ skipped += open;
187
+ if (dir !== void 0) dirs.push(dir);
188
+ }
189
+ if (!skipped) return void 0;
190
+ return `The dependencies${dirs.length ? ` of ${listOf$2(dirs)}` : ""} are not installed (no node_modules): no-unknown-prop skips ${plural(skipped, "component")} whose props come from them. Install them (npm install, pnpm install) and run again.`;
191
+ }
192
+ /** Whether `root`'s package.json lists dependencies, devDependencies or peerDependencies. */
193
+ function declaresDependencies(root) {
194
+ try {
195
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
196
+ return [
197
+ "dependencies",
198
+ "devDependencies",
199
+ "peerDependencies"
200
+ ].some((field) => {
201
+ const value = pkg[field];
202
+ return Boolean(value && typeof value === "object" && Object.keys(value).length);
203
+ });
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+ /** `apps/web`, `apps/web and packages/ui`, `a, b, c and 4 more`. */
209
+ function listOf$2(items, max = 3) {
210
+ if (items.length <= 1) return items[0] ?? "";
211
+ if (items.length <= max) return `${items.slice(0, -1).join(", ")} and ${items.at(-1) ?? ""}`;
212
+ return `${items.slice(0, max).join(", ")} and ${items.length - max} more`;
213
+ }
23
214
  //#endregion
24
215
  //#region src/server/render.ts
25
216
  /**
@@ -44,15 +235,21 @@ function renderComponentList(ds, project) {
44
235
  if (!roots.length) return `No components found${project ? ` in ${project}` : ""}. Check the "components" globs in the config (root: ${ds.root}).`;
45
236
  const parts = ds.components.length - roots.length;
46
237
  const lines = [`${plural(roots.length, "component")}${parts ? ` (+${plural(parts, "part")})` : ""}${project ? ` in ${project}` : ""}. Call get_component before using one.`, ""];
47
- for (const component of roots) lines.push(...listItem(component));
238
+ const own = roots.filter((c) => !c.library);
239
+ for (const component of own) lines.push(...listItem(component));
240
+ for (const library of new Set(roots.flatMap((c) => c.library ? [c.library] : []))) {
241
+ if (own.length) lines.push("");
242
+ lines.push(`From ${library} (import { Name } from "${library}"):`, "");
243
+ for (const component of roots.filter((c) => c.library === library)) lines.push(...listItem(component, "", false));
244
+ }
48
245
  return lines.join("\n");
49
246
  }
50
- function listItem(component, note = "") {
247
+ function listItem(component, note = "", withImport = true) {
51
248
  const element = component.element ? ` <${component.element}>` : "";
52
249
  const lines = [`${component.name}${element}${note} — ${firstSentence(component.description) || "No description."}`];
53
250
  for (const variant of variantSummary(component)) lines.push(` ${variant}`);
54
251
  if (component.subcomponents.length) lines.push(` parts: ${component.subcomponents.join(", ")}`);
55
- lines.push(` ${importStatement(component)}`);
252
+ if (withImport) lines.push(` ${importStatement(component)}`);
56
253
  return lines;
57
254
  }
58
255
  /** `apps/web`, `apps/web and packages/ui`, `a, b, c and 4 more`. */
@@ -191,6 +388,10 @@ function renderComponent(ds, component, origin) {
191
388
  if (component.variants.length) {
192
389
  lines.push("", "## Variants");
193
390
  for (const variant of component.variants) {
391
+ if (!Object.values(variant.classes).some(Boolean)) {
392
+ lines.push(`${variant.name}: ${variant.values.join(" | ")}${variant.default ? ` (default "${variant.default}")` : ""}`);
393
+ continue;
394
+ }
194
395
  lines.push(`${variant.name}${variant.default ? ` (default "${variant.default}")` : ""}`);
195
396
  const width = Math.max(...variant.values.map((v) => v.length));
196
397
  for (const value of variant.values) lines.push(` ${value.padEnd(width)} ${variant.classes[value] ?? ""}`.trimEnd());
@@ -818,7 +1019,7 @@ function createServer({ getDesignSystem, getProjects }) {
818
1019
  names: z.array(z.string()),
819
1020
  modules: z.array(z.string())
820
1021
  }).optional().describe("Components imported from modules the model does not include, not checked."),
821
- notice: z.string().optional().describe("Set when no components or no color tokens were found, so rules did not run, or when components came from modules the model does not include.")
1022
+ notice: z.string().optional().describe("Set when no components or no color tokens were found, so rules did not run, when components came from modules the model does not include, when the project depends on a component library onsystem does not read yet, or when its dependencies are not installed.")
822
1023
  }),
823
1024
  annotations: READ_ONLY
824
1025
  }, async ({ code, path: filePath, filename, limit }) => {
@@ -848,16 +1049,16 @@ function createServer({ getDesignSystem, getProjects }) {
848
1049
  const outside = errorResult(`${filePath} is outside the project root (${base}).`);
849
1050
  if (!isInside(base, absolute)) return outside;
850
1051
  try {
851
- const [root, real] = await Promise.all([fs$1.realpath(base), fs$1.realpath(absolute)]);
1052
+ const [root, real] = await Promise.all([fsp.realpath(base), fsp.realpath(absolute)]);
852
1053
  if (!isInside(root, real)) return outside;
853
- if ((await fs$1.stat(real)).size > MAX_CODE) return {
1054
+ if ((await fsp.stat(real)).size > MAX_CODE) return {
854
1055
  isError: true,
855
1056
  content: [{
856
1057
  type: "text",
857
1058
  text: `${filePath} is too large to check.`
858
1059
  }]
859
1060
  };
860
- source = await fs$1.readFile(real, "utf8");
1061
+ source = await fsp.readFile(real, "utf8");
861
1062
  } catch {
862
1063
  return {
863
1064
  isError: true,
@@ -876,7 +1077,11 @@ function createServer({ getDesignSystem, getProjects }) {
876
1077
  file: shown
877
1078
  };
878
1079
  const capped = capDiagnostics(result, limit);
879
- const notice = result.skipped ? void 0 : [ds.notice(), uncheckedNotice([result])].filter(Boolean).join("\n\n") || void 0;
1080
+ const notice = result.skipped ? void 0 : [
1081
+ ds.notice(),
1082
+ ...notReadNotices([{ ds }]),
1083
+ uncheckedNotice([result])
1084
+ ].filter(Boolean).join("\n\n") || void 0;
880
1085
  return {
881
1086
  content: [{
882
1087
  type: "text",
@@ -1035,7 +1240,7 @@ var Catalog = class {
1035
1240
  byKey.set(key, {
1036
1241
  component: rebase(component, root, file),
1037
1242
  ds,
1038
- package: owner(file),
1243
+ package: component.library ?? owner(file),
1039
1244
  projects: [dir],
1040
1245
  ambiguous: false
1041
1246
  });
@@ -1265,6 +1470,7 @@ function serveStdio$1(options) {
1265
1470
  };
1266
1471
  const { config, workspace } = await loadTarget(configOptions);
1267
1472
  for (const text of config.deprecations ?? []) logger.warn(text);
1473
+ for (const text of (workspace ? workspace.warnings : config.warnings) ?? []) logger.warn(text);
1268
1474
  if (workspace) {
1269
1475
  const projects = new WorkspaceProjects(workspace, {
1270
1476
  cache: options.cache,
@@ -1396,4 +1602,4 @@ async function clientRoot(server) {
1396
1602
  }
1397
1603
  }
1398
1604
  //#endregion
1399
- export { INSTRUCTIONS as n, createServer as r, serveStdio$1 as t };
1605
+ export { notReadNotices as a, missingDependencies as i, INSTRUCTIONS as n, explainProject as o, createServer as r, librariesLeftOut as s, serveStdio$1 as t };