onsystem 0.6.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.
- package/README.md +28 -21
- package/dist/cli.js +238 -157
- package/dist/{config-lbfW91qB.d.ts → config-1XSTYI44.d.ts} +8 -0
- package/dist/{design-system-A3pRX3Ka.js → design-system-C4bhqLLw.js} +1582 -282
- package/dist/eslint-worker.d.ts +1 -1
- package/dist/eslint-worker.js +2 -2
- package/dist/eslint.js +66 -5
- package/dist/index.d.ts +20 -1
- package/dist/index.js +2 -2
- package/dist/{stdio-CEChNFLR.js → stdio-BojAQ6JP.js} +214 -5
- package/dist/{workspace-CHvUwgz9.js → workspace-Z0tZf_Dv.js} +1 -1
- package/package.json +9 -9
package/dist/eslint-worker.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h as DesignSystemModel, i as ResolvedConfig } from "./config-
|
|
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 {
|
package/dist/eslint-worker.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as loadTarget } from "./workspace-
|
|
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";
|
package/dist/eslint.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
100
|
-
|
|
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
|
-
|
|
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 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-
|
|
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";
|
|
@@ -214,8 +214,19 @@ interface ColorSuggestion {
|
|
|
214
214
|
reason?: 'far' | 'hue' | 'role' | 'status' | 'scoped' | undefined;
|
|
215
215
|
/** With reason `status`: how far apart the hues are, in degrees. */
|
|
216
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;
|
|
217
222
|
/** Other tokens with the nearest one's value. */
|
|
218
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;
|
|
219
230
|
}
|
|
220
231
|
interface NearestLength extends Nearest<LengthCandidate> {
|
|
221
232
|
/** A radius so far above the scale that it means "fully rounded" (`rounded-full`). */
|
|
@@ -311,6 +322,14 @@ declare class LintTarget {
|
|
|
311
322
|
* directory itself (a barrel: `../components/ui` and its index).
|
|
312
323
|
*/
|
|
313
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;
|
|
314
333
|
/**
|
|
315
334
|
* Whether `name`, imported from the design system, may be a real export the
|
|
316
335
|
* model left out rather than an invented one: its module exists outside the
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-
|
|
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
3
|
export { DesignSystem, DesignSystemHost, INSTRUCTIONS, RULES, RULE_IDS, TokenIndex, VERSION, applyFixes, attachDocs, buildModel, checkSource, configJsonSchema, createServer, defineConfig, extractComponents, findVariantDefinitions, fingerprint, formatDiagnostics, loadConfig, loadDesignSystem, loadTokens, mergeTokens, parseCssTokens, parseDesignMd, parseDoc, parseDtcgTokens, searchComponents, serveStdio, toDesignMd, toDtcg };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as hasProjectMarker, r as loadTarget, t as findProjectRoot } from "./workspace-
|
|
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";
|
|
@@ -7,6 +7,211 @@ import * as z from "zod";
|
|
|
7
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/explain.ts
|
|
11
|
+
/**
|
|
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.
|
|
15
|
+
*/
|
|
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
|
+
}
|
|
214
|
+
//#endregion
|
|
10
215
|
//#region src/server/render.ts
|
|
11
216
|
/**
|
|
12
217
|
* Tool results are read by models, so they are rendered as compact Markdown:
|
|
@@ -814,7 +1019,7 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
814
1019
|
names: z.array(z.string()),
|
|
815
1020
|
modules: z.array(z.string())
|
|
816
1021
|
}).optional().describe("Components imported from modules the model does not include, not checked."),
|
|
817
|
-
notice: z.string().optional().describe("Set when no components or no color tokens were found, so rules did not run,
|
|
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.")
|
|
818
1023
|
}),
|
|
819
1024
|
annotations: READ_ONLY
|
|
820
1025
|
}, async ({ code, path: filePath, filename, limit }) => {
|
|
@@ -872,7 +1077,11 @@ function createServer({ getDesignSystem, getProjects }) {
|
|
|
872
1077
|
file: shown
|
|
873
1078
|
};
|
|
874
1079
|
const capped = capDiagnostics(result, limit);
|
|
875
|
-
const notice = result.skipped ? 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;
|
|
876
1085
|
return {
|
|
877
1086
|
content: [{
|
|
878
1087
|
type: "text",
|
|
@@ -1393,4 +1602,4 @@ async function clientRoot(server) {
|
|
|
1393
1602
|
}
|
|
1394
1603
|
}
|
|
1395
1604
|
//#endregion
|
|
1396
|
-
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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as loadConfig, Ct as relativePath, H as CONFIG_FILES, J as LEGACY_CONFIG_FILES, Q as hasOwnConfig, et as loadConfigWith, ft as findDesignSystemPackage, gt as workspacePatterns, ht as workspacePackageDirs, i as componentFiles, lt as PackageFinder, mt as isWorkspaceRoot, xt as isInside } from "./design-system-C4bhqLLw.js";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import os from "node:os";
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onsystem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"mcpName": "io.github.dgesteves/onsystem",
|
|
5
|
-
"description": "Keeps coding agents on your design system:
|
|
5
|
+
"description": "Keeps coding agents on your design system: catches palette colors, native elements and APIs your system lacks, at every edit and pull request, with the fix. Set it up once in your design-system package; every app in your monorepo gets it.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"design-system",
|
|
8
8
|
"design-system-lint",
|
|
@@ -69,10 +69,10 @@
|
|
|
69
69
|
"tinyglobby": "^0.2.17",
|
|
70
70
|
"typescript": "~6.0.3",
|
|
71
71
|
"yaml": "^2.9.1",
|
|
72
|
-
"zod": "^4.6.
|
|
72
|
+
"zod": "^4.6.6"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
|
-
"eslint": ">=
|
|
75
|
+
"eslint": ">=8.57.0"
|
|
76
76
|
},
|
|
77
77
|
"peerDependenciesMeta": {
|
|
78
78
|
"eslint": {
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"@modelcontextprotocol/client": "^2.3.1",
|
|
87
87
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
88
88
|
"@types/culori": "^4.0.1",
|
|
89
|
-
"@types/node": "^24.19.
|
|
89
|
+
"@types/node": "^24.19.2",
|
|
90
90
|
"ajv": "^8.20.0",
|
|
91
91
|
"ajv-draft-04": "^1.0.0",
|
|
92
92
|
"ajv-formats": "^3.0.1",
|
|
@@ -96,11 +96,11 @@
|
|
|
96
96
|
"geist": "^1.7.2",
|
|
97
97
|
"globals": "^17.13.0",
|
|
98
98
|
"opentype.js": "^2.0.0",
|
|
99
|
-
"prettier": "^3.9.
|
|
99
|
+
"prettier": "^3.9.10",
|
|
100
100
|
"publint": "^0.3.25",
|
|
101
|
-
"react-aria-components": "^1.
|
|
101
|
+
"react-aria-components": "^1.22.1",
|
|
102
102
|
"tailwind-variants": "^3.3.1",
|
|
103
|
-
"tsdown": "^0.23.
|
|
103
|
+
"tsdown": "^0.23.1",
|
|
104
104
|
"typescript-eslint": "^8.71.1",
|
|
105
105
|
"vitest": "^5.0.3"
|
|
106
106
|
},
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"test": "vitest run",
|
|
115
115
|
"test:watch": "vitest",
|
|
116
116
|
"smoke": "node scripts/smoke.mjs",
|
|
117
|
-
"pack-test": "node scripts/pack-test.mjs",
|
|
117
|
+
"pack-test": "node scripts/pack-test.mjs && node scripts/pack-test-eslint8.mjs",
|
|
118
118
|
"assets": "node scripts/build-assets.mjs",
|
|
119
119
|
"palette": "node scripts/build-palette.mjs",
|
|
120
120
|
"sync-plugins": "node scripts/sync-plugins.mjs",
|