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.
- package/README.md +41 -17
- package/dist/cli.js +476 -126
- package/dist/{types-DJtccu3A.d.ts → config-1XSTYI44.d.ts} +271 -167
- package/dist/{design-system-CnEv5sIL.js → design-system-C4bhqLLw.js} +4426 -679
- package/dist/eslint-worker.d.ts +1 -1
- package/dist/eslint-worker.js +9 -2
- package/dist/eslint.js +66 -5
- package/dist/index.d.ts +74 -1
- package/dist/index.js +3 -3
- package/dist/{stdio-DjfkiDMV.js → stdio-BojAQ6JP.js} +230 -24
- package/dist/{workspace-DzWQ4L8R.js → workspace-Z0tZf_Dv.js} +44 -10
- package/package.json +11 -10
- package/schema.json +19 -1
package/dist/eslint-worker.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
@@ -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 {
|
|
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
|
|
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 {
|
|
2
|
-
import { n as INSTRUCTIONS, r as createServer, t as serveStdio } from "./stdio-
|
|
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
|
|
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";
|
|
6
6
|
import * as z from "zod";
|
|
7
|
-
import
|
|
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/
|
|
10
|
+
//#region src/explain.ts
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
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
|
-
|
|
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,
|
|
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([
|
|
1052
|
+
const [root, real] = await Promise.all([fsp.realpath(base), fsp.realpath(absolute)]);
|
|
852
1053
|
if (!isInside(root, real)) return outside;
|
|
853
|
-
if ((await
|
|
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
|
|
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 : [
|
|
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 };
|