onsystem 0.5.0 → 0.6.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 +21 -4
- package/dist/cli.js +302 -33
- package/dist/{types-DJtccu3A.d.ts → config-lbfW91qB.d.ts} +263 -167
- package/dist/{design-system-CnEv5sIL.js → design-system-A3pRX3Ka.js} +2861 -414
- package/dist/eslint-worker.d.ts +1 -1
- package/dist/eslint-worker.js +9 -2
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +3 -3
- package/dist/{stdio-DjfkiDMV.js → stdio-CEChNFLR.js} +21 -24
- package/dist/{workspace-DzWQ4L8R.js → workspace-CHvUwgz9.js} +44 -10
- package/package.json +3 -2
- package/schema.json +19 -1
package/README.md
CHANGED
|
@@ -6,8 +6,15 @@ Built for design-system and platform teams with a React design-system package; a
|
|
|
6
6
|
|
|
7
7
|
[](https://github.com/dgesteves/onsystem/actions/workflows/ci.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/onsystem)
|
|
9
|
+
[](https://www.npmjs.com/package/onsystem)
|
|
10
|
+
[](https://registry.modelcontextprotocol.io/?q=io.github.dgesteves%2Fonsystem)
|
|
11
|
+
[](https://cursor.directory/plugins/design-system-mcp)
|
|
9
12
|
[](LICENSE)
|
|
10
13
|
|
|
14
|
+
[](https://cursor.com/en/install-mcp?name=onsystem&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm9uc3lzdGVtIiwiLS1yb290IiwiJHt3b3Jrc3BhY2VGb2xkZXJ9Il19)
|
|
15
|
+
[](https://insiders.vscode.dev/redirect/mcp/install?name=onsystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22onsystem%22%2C%22--root%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D)
|
|
16
|
+
[](https://onsystem.vercel.app/docs/plugin)
|
|
17
|
+
|
|
11
18
|
**Website and docs:** [onsystem.vercel.app](https://onsystem.vercel.app), with a [playground](https://onsystem.vercel.app/playground) that checks your own code.
|
|
12
19
|
|
|
13
20
|
<p align="center">
|
|
@@ -22,7 +29,7 @@ Coding agents write UI from their training data, not from your design system. As
|
|
|
22
29
|
|
|
23
30
|
## What it checks
|
|
24
31
|
|
|
25
|
-
onsystem reads your components, `cva` and `tv` variants, parts and tokens from source with the TypeScript compiler, and checks `.tsx` and `.jsx` against them. Every finding has a rule id, a location and the fix.
|
|
32
|
+
onsystem reads your components, `cva` and `tv` variants, parts and tokens from source with the TypeScript compiler, and checks `.tsx` and `.jsx` against them. Every finding has a rule id, a location and the fix. Tokens come from your stylesheets, [DTCG 2025.10 files or a DESIGN.md](https://onsystem.vercel.app/docs/tokens), and `onsystem export` writes them out as either.
|
|
26
33
|
|
|
27
34
|
| Rule | Default | Catches |
|
|
28
35
|
| -------------------------------- | ------- | ------------------------------------------------------------------------------- |
|
|
@@ -35,11 +42,21 @@ onsystem reads your components, `cva` and `tv` variants, parts and tokens from s
|
|
|
35
42
|
| `no-hardcoded-spacing` | warn | Arbitrary padding, margin and gap (`p-[13px]` → `p-3`) |
|
|
36
43
|
| `no-hardcoded-radius` | warn | Arbitrary radius (`rounded-[7px]` → `rounded-sm`) |
|
|
37
44
|
|
|
38
|
-
**How often it is wrong.** The [corpus](corpus) job runs `check` on
|
|
45
|
+
**How often it is wrong.** The [corpus](corpus) job runs `check` on 12 public repositories pinned by commit (Documenso, Dub, Cal.com, midday, Flowise and others, the four monorepos also from their root, and Dub's app also with a config and with `@dub/ui` set up by `init`: 24 runs, 4,076 distinct findings) and compares the findings with hand labels. In a random sample of 149 findings, 88 are clearly right (59.1%), 56 are debatable and 5 are false positives (3.4%); weighted by each run's and rule's share of all findings, about 1.0% are false positives. Shipped code rarely invents components, props or variants: those three rules have 56 findings in the corpus, 55 of them in Flowise, an MUI app that passes props and values MUI's components do not take, and the 10 in the sample are all right. The [rules reference](https://onsystem.vercel.app/rules) shows each rule on an example and how to [suppress a finding](https://onsystem.vercel.app/rules#suppressing-findings).
|
|
39
46
|
|
|
40
47
|
## Quickstart
|
|
41
48
|
|
|
42
|
-
Run it from the app's folder or from the monorepo root, with Node.js 20.19 or later
|
|
49
|
+
Run it from the app's folder or from the monorepo root, with Node.js 20.19 or later:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npx -y onsystem inspect # what it found; --explain says why
|
|
53
|
+
npx -y onsystem check . # the same check the agent and CI run
|
|
54
|
+
npx -y onsystem init # write the config, and offer a baseline
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
At a [monorepo root](https://onsystem.vercel.app/docs/configuration#monorepo-roots), `inspect` lists every app and design-system package, and `check` checks each file by its own project's design system. In a [design-system package](https://onsystem.vercel.app/docs/design-system-packages), `init` sets it up for every app that depends on it, workspace or npm, with no config in the apps.
|
|
58
|
+
|
|
59
|
+
In a new create-next-app, the template page's hardcoded colors already make the first `check .` fail; `init` offers to record them as a baseline, so only new findings fail.
|
|
43
60
|
|
|
44
61
|
**Claude Code.** The plugin brings the hook, the MCP server and a skill:
|
|
45
62
|
|
|
@@ -50,7 +67,7 @@ Run it from the app's folder or from the monorepo root, with Node.js 20.19 or la
|
|
|
50
67
|
|
|
51
68
|
After each Write or Edit of a `.tsx` or `.jsx` file, the hook runs `onsystem check` on it. Errors go back to Claude as a blocking error with the fix, so Claude corrects the file before it moves on; warnings go as a note. It stays quiet in projects without a design system. [What it runs and touches](https://onsystem.vercel.app/docs/plugin).
|
|
52
69
|
|
|
53
|
-
**Cursor or VS Code.** [](https://cursor.com/en/install-mcp?name=onsystem&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm9uc3lzdGVtIiwiLS1yb290IiwiJHt3b3Jrc3BhY2VGb2xkZXJ9Il19) [](https://insiders.vscode.dev/redirect/mcp/install?name=onsystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22onsystem%22%5D%7D), or commit the [config](https://onsystem.vercel.app/docs/clients#cursor-and-vs-code). The agent gets the tools and runs `check_ui` on what it writes.
|
|
70
|
+
**Cursor or VS Code.** [](https://cursor.com/en/install-mcp?name=onsystem&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIm9uc3lzdGVtIiwiLS1yb290IiwiJHt3b3Jrc3BhY2VGb2xkZXJ9Il19) [](https://insiders.vscode.dev/redirect/mcp/install?name=onsystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22onsystem%22%2C%22--root%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D), or commit the [config](https://onsystem.vercel.app/docs/clients#cursor-and-vs-code). The agent gets the tools and runs `check_ui` on what it writes.
|
|
54
71
|
|
|
55
72
|
**Other agents.** `npx skills add dgesteves/onsystem` adds the skill to any agent with Agent Skills, and there are [plugins for Codex, GitHub Copilot CLI, VS Code and Cursor](https://onsystem.vercel.app/docs/clients#plugins-for-other-agents).
|
|
56
73
|
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { t as serveStdio } from "./stdio-
|
|
4
|
-
import { r as loadTarget } from "./workspace-
|
|
2
|
+
import { A as toDtcg, B as DEFAULT_DOCS, C as updateBaseline, G as RULE_IDS, R as CONFIG_FILES, S as readBaseline, V as DEFAULT_SEVERITY, W as LEGACY_CONFIG_FILES, Z as rulesFor, _ as VERSION, _t as relativePath, at as detectProject, b as applyBaseline, c as silentLogger, et as LIBRARY_IDS, ft as plural, g as NAME, gt as matchesGlob, h as formatDiagnostics, ht as isInside, i as componentFiles, it as declaredConfig, k as toDesignMd, l as stderrLogger, m as uncheckedNotice, mt as unique, nt as PACKAGE_FIELD, o as loadDesignSystem, ot as findDesignSystemPackage, s as tokenFiles, st as isDesignSystemName, tt as libraryAdapter, v as BASELINE_FILE, vt as toPosix, w as writeBaseline, x as countBaseline, y as LEGACY_BASELINE_FILE, z as ConfigError } from "./design-system-A3pRX3Ka.js";
|
|
3
|
+
import { t as serveStdio } from "./stdio-CEChNFLR.js";
|
|
4
|
+
import { r as loadTarget } from "./workspace-CHvUwgz9.js";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { escapePath, glob } from "tinyglobby";
|
|
8
|
-
import
|
|
8
|
+
import fsp from "node:fs/promises";
|
|
9
9
|
import { spawnSync } from "node:child_process";
|
|
10
10
|
import readline from "node:readline/promises";
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
@@ -20,24 +20,32 @@ function explainProject(ds, cwd, indent = "") {
|
|
|
20
20
|
const lines = [];
|
|
21
21
|
const out = (line = "") => lines.push(line ? `${indent}${line}` : "");
|
|
22
22
|
out("How the design system was found");
|
|
23
|
+
for (const step of packageSteps(config, cwd)) out(` ${(step.accepted ? "found" : "no").padEnd(6)} ${step.candidate}: ${step.reason}`);
|
|
23
24
|
if (config.configured) out(` components come from ${config.configFile ? display(cwd, config.configFile) : "--components"}, so zero config did not look`);
|
|
24
25
|
else {
|
|
25
26
|
const steps = [];
|
|
26
|
-
detectProject(config.root, config.tsconfig, steps);
|
|
27
|
+
detectProject(config.root, config.tsconfig, steps, config.designSystemPackage);
|
|
27
28
|
for (const step of steps) out(` ${(step.accepted ? "found" : "no").padEnd(6)} ${step.candidate}: ${step.reason}`);
|
|
28
29
|
if (!config.detected) out(` So the defaults apply: ${config.components.join(", ")}${model.stats.files.components ? "" : ", which match nothing"}.`);
|
|
29
30
|
}
|
|
31
|
+
const named = (library) => `${library.package}${library.version ? ` ${library.version}` : ""}`;
|
|
32
|
+
for (const library of config.libraries ?? []) {
|
|
33
|
+
const count = model.components.filter((c) => c.library === library.package).length;
|
|
34
|
+
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`);
|
|
35
|
+
}
|
|
36
|
+
for (const library of config.librariesOff ?? []) out(` ${"no".padEnd(6)} ${named(library)}: a dependency, ${librariesLeftOut(config, cwd)}`);
|
|
30
37
|
out();
|
|
31
38
|
out("Resolved config");
|
|
32
39
|
const field = (name, value) => out(` ${name.padEnd(12)} ${value}`);
|
|
33
40
|
field("root", config.root);
|
|
34
|
-
field("config", config.configFile ? display(cwd, config.configFile) : "(none)");
|
|
41
|
+
field("config", config.configFile ? `${display(cwd, config.configFile)}${config.designSystemPackage ? ` (declared by ${config.designSystemPackage.name})` : ""}` : "(none)");
|
|
35
42
|
field("components", listOf$1(config.components) || "(none)");
|
|
36
43
|
field("exclude", listOf$1(config.exclude));
|
|
37
|
-
field("tokens", listOf$1(config.tokens.map((t) => `${t.path}${t.prefix ? ` (prefix ${t.prefix})` : ""}`)));
|
|
44
|
+
field("tokens", listOf$1(config.tokens.map((t) => `${t.path}${t.prefix ? ` (prefix ${t.prefix})` : ""}${t.fallback ? " (when nothing else is found)" : ""}`)));
|
|
38
45
|
field("docs", listOf$1(config.docs));
|
|
39
46
|
field("tsconfig", config.tsconfig ?? (fs.existsSync(path.join(config.root, "tsconfig.json")) ? "tsconfig.json" : "(none)"));
|
|
40
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)})`);
|
|
41
49
|
if (config.tailwindConfig) field("tailwind", display(config.root, config.tailwindConfig));
|
|
42
50
|
if (config.designSystems) field("primary", config.designSystems.map((d, i) => `${i ? "then " : ""}${d.source} (${d.imports} imports)`).join("; "));
|
|
43
51
|
const changed = RULE_IDS.filter((id) => config.rules[id].severity !== DEFAULT_SEVERITY[id]);
|
|
@@ -48,7 +56,12 @@ function explainProject(ds, cwd, indent = "") {
|
|
|
48
56
|
out("Tokens");
|
|
49
57
|
const found = ds.tokens;
|
|
50
58
|
const files = model.stats.files.tokens;
|
|
51
|
-
|
|
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
|
+
}
|
|
52
65
|
const defined = new Set(found.map((t) => t.cssVar).filter((v) => Boolean(v)));
|
|
53
66
|
const missing = (value) => [...value.matchAll(/var\(\s*(--[\w-]+)/g)].map((m) => m[1] ?? "").filter((v) => !defined.has(v));
|
|
54
67
|
const unresolved = found.filter((t) => missing(t.value).length);
|
|
@@ -69,6 +82,10 @@ function explainProject(ds, cwd, indent = "") {
|
|
|
69
82
|
}
|
|
70
83
|
return lines;
|
|
71
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
|
+
}
|
|
72
89
|
/** The first few, then how many more: a package's exports can name hundreds of files. */
|
|
73
90
|
function listOf$1(items, max = 8) {
|
|
74
91
|
const shown = items.slice(0, max).join(", ");
|
|
@@ -95,15 +112,46 @@ function display(cwd, file) {
|
|
|
95
112
|
const rel = relativePath(cwd, file);
|
|
96
113
|
return rel && !rel.startsWith("..") ? rel : file;
|
|
97
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
|
+
}
|
|
98
137
|
//#endregion
|
|
99
138
|
//#region src/init.ts
|
|
100
139
|
const SCHEMA = "https://unpkg.com/onsystem/schema.json";
|
|
101
140
|
/** More detected globs than this are files, not folders: zero config keeps them. */
|
|
102
141
|
const MAX_GLOBS = 6;
|
|
142
|
+
/** What a design-system package's apps are told about it, next to its config. */
|
|
143
|
+
const GUIDE_FILE = "ONSYSTEM.md";
|
|
103
144
|
/**
|
|
104
145
|
* Writes `onsystem.config.json` in the root from what zero config finds there, or from
|
|
105
146
|
* the answers to up to three questions when it finds nothing, then checks the project
|
|
106
147
|
* once without failing and offers to record what it found as the baseline.
|
|
148
|
+
*
|
|
149
|
+
* In a design-system package (one zero config reads through its own `exports`, or named like
|
|
150
|
+
* one, as apps look for it), it also declares the config in package.json
|
|
151
|
+
* (`"onsystem": "./onsystem.config.json"`, and in `files` when the package lists them), so
|
|
152
|
+
* every app that depends on the package reads it, and writes ONSYSTEM.md, which tells those
|
|
153
|
+
* apps' teams how to add the agent tools and the CI check. In an app that takes on such a
|
|
154
|
+
* package's config, it writes no config of its own unless --force says to.
|
|
107
155
|
*/
|
|
108
156
|
async function init(configOptions, options, io, runCheck) {
|
|
109
157
|
const { config, workspace } = await loadTarget({
|
|
@@ -111,16 +159,23 @@ async function init(configOptions, options, io, runCheck) {
|
|
|
111
159
|
config: void 0
|
|
112
160
|
});
|
|
113
161
|
const root = config.root;
|
|
114
|
-
const
|
|
162
|
+
const declared = declaredConfig(root);
|
|
163
|
+
const existing = [...CONFIG_FILES, ...LEGACY_CONFIG_FILES].map((name) => path.join(root, name)).find((file) => fs.existsSync(file)) ?? (declared && "file" in declared ? declared.file : void 0);
|
|
164
|
+
const manifest = readManifest(root);
|
|
165
|
+
const name = typeof manifest?.json.name === "string" ? manifest.json.name : void 0;
|
|
166
|
+
const designSystem = !workspace && !config.designSystemPackage && name !== void 0 && (config.detected?.startsWith("package.json exports of") === true || isDesignSystemName(name));
|
|
115
167
|
if (existing && !options.force) {
|
|
116
|
-
|
|
168
|
+
const share = designSystem && !declared && existing.endsWith(".json") ? ` To share it with the apps that depend on ${name}, add "${PACKAGE_FIELD}": "./${path.basename(existing)}" to package.json, and to "files" when it lists them (https://onsystem.vercel.app/docs/design-system-packages).` : "";
|
|
169
|
+
io.stderr(`init: ${relativePath(io.cwd, existing) || existing} already exists. Run \`${NAME} inspect --explain\` to see what it resolves to, or pass --force to write a new onsystem.config.json.${share}`);
|
|
117
170
|
return 2;
|
|
118
171
|
}
|
|
119
172
|
const ask = options.yes ? void 0 : io.prompt;
|
|
120
173
|
const file = { $schema: SCHEMA };
|
|
121
174
|
const notes = [];
|
|
122
175
|
const relative = (abs) => relativePath(root, abs);
|
|
123
|
-
|
|
176
|
+
const inherited = options.force ? void 0 : config.designSystemPackage;
|
|
177
|
+
if (inherited) notes.push(`This project takes on the config ${inherited.name} declares in its package.json (${relativePath(io.cwd, inherited.config) || inherited.config}), so it needs none of its own; nothing was written. Pass --force to write one, which is read instead.`);
|
|
178
|
+
else if (configOptions.components?.length) {
|
|
124
179
|
file.components = configOptions.components;
|
|
125
180
|
if (configOptions.tokens?.length) file.tokens = configOptions.tokens;
|
|
126
181
|
if (configOptions.docs?.length) file.docs = configOptions.docs;
|
|
@@ -138,6 +193,16 @@ async function init(configOptions, options, io, runCheck) {
|
|
|
138
193
|
if (docs.length) file.docs = [...DEFAULT_DOCS, ...docs];
|
|
139
194
|
}
|
|
140
195
|
if (tokens.length) file.tokens = tokens;
|
|
196
|
+
} else if ((await componentFiles(config)).length) {
|
|
197
|
+
const matching = [];
|
|
198
|
+
for (const glob of config.components) if ((await componentFiles({
|
|
199
|
+
...config,
|
|
200
|
+
components: [glob]
|
|
201
|
+
})).length) matching.push(glob);
|
|
202
|
+
notes.push(`Found the design system in the default folders: ${matching.join(", ")}.`);
|
|
203
|
+
file.components = matching;
|
|
204
|
+
const tokens = (await tokenFiles(config)).map(relative);
|
|
205
|
+
if (tokens.length) file.tokens = tokens;
|
|
141
206
|
} else {
|
|
142
207
|
if (!ask) {
|
|
143
208
|
io.stderr(`init: zero config found no design system in ${root}. Pass --components (and --tokens) to say where it is, or run init in a terminal to answer a few questions. \`${NAME} inspect --explain\` shows what it looked at.`);
|
|
@@ -167,10 +232,19 @@ async function init(configOptions, options, io, runCheck) {
|
|
|
167
232
|
const importPath = (await ask("How do apps import them? A package name such as @acme/ui, or leave it empty for tsconfig paths: ")).trim();
|
|
168
233
|
if (importPath) file.importPath = importPath;
|
|
169
234
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
235
|
+
const written = [];
|
|
236
|
+
if (!inherited) {
|
|
237
|
+
const out = path.join(root, CONFIG_FILE);
|
|
238
|
+
fs.writeFileSync(out, `${JSON.stringify(file, null, 2)}\n`);
|
|
239
|
+
written.push(CONFIG_FILE);
|
|
240
|
+
}
|
|
241
|
+
if (designSystem && manifest && name) {
|
|
242
|
+
written.push(...declarePackage(root, manifest, name, options.force, notes));
|
|
243
|
+
const shipped = await unshipped(config, manifest.json);
|
|
244
|
+
if (shipped) notes.push(shipped);
|
|
245
|
+
}
|
|
172
246
|
for (const note of notes) io.stdout(note);
|
|
173
|
-
io.stdout(`Wrote ${relativePath(io.cwd,
|
|
247
|
+
if (written.length) io.stdout(`Wrote ${written.map((f) => relativePath(io.cwd, path.join(root, f)) || f).join(", ")}.`);
|
|
174
248
|
const checkIo = {
|
|
175
249
|
...io,
|
|
176
250
|
cwd: root
|
|
@@ -202,9 +276,11 @@ async function init(configOptions, options, io, runCheck) {
|
|
|
202
276
|
io.stdout(total ? `Dry run of \`${NAME} check .\`: ${count$1(errors, "error")} and ${count$1(warnings, "warning")} in ${flagged} of ${count$1(results.length, "file")}. By rule: ${[...byRule].sort((a, b) => b[1] - a[1]).map(([rule, n]) => `${rule} ${n.toLocaleString("en-US")}`).join(", ")}.` : `Dry run of \`${NAME} check .\`: no problems in ${count$1(results.length, "file")}.`);
|
|
203
277
|
for (const line of dry.stderr.split("\n").filter(Boolean)) io.stdout(line);
|
|
204
278
|
const hasBaseline = [BASELINE_FILE, LEGACY_BASELINE_FILE].some((name) => fs.existsSync(path.join(root, name)));
|
|
279
|
+
let baselineWritten = false;
|
|
205
280
|
if (total && !hasBaseline) {
|
|
206
|
-
const answer = options.yes ? "y" : ask ? (await ask(`Record these ${total.toLocaleString("en-US")} findings in ${BASELINE_FILE}, so check fails only on new ones? [Y/n] `)).trim().toLowerCase() :
|
|
207
|
-
if (answer ===
|
|
281
|
+
const answer = options.yes ? "y" : ask ? (await ask(`Record these ${total.toLocaleString("en-US")} findings in ${BASELINE_FILE}, so check fails only on new ones? [Y/n] `)).trim().toLowerCase() : void 0;
|
|
282
|
+
if (answer === void 0) io.stdout(`No terminal to ask in, so the baseline question was skipped and no baseline was written; \`${NAME} check . --update-baseline\` records one.`);
|
|
283
|
+
else if (answer === "" || answer.startsWith("y")) {
|
|
208
284
|
const recorded = await runCheck([
|
|
209
285
|
"check",
|
|
210
286
|
".",
|
|
@@ -212,11 +288,128 @@ async function init(configOptions, options, io, runCheck) {
|
|
|
212
288
|
...options.cache ? [] : ["--no-cache"]
|
|
213
289
|
], checkIo);
|
|
214
290
|
if (recorded.stdout) io.stdout(recorded.stdout);
|
|
291
|
+
baselineWritten = fs.existsSync(path.join(root, BASELINE_FILE));
|
|
215
292
|
} else io.stdout(`No baseline written; \`${NAME} check . --update-baseline\` records one later.`);
|
|
216
293
|
}
|
|
217
|
-
|
|
294
|
+
const commit = [...written, ...baselineWritten ? [BASELINE_FILE] : []];
|
|
295
|
+
const files = commit.length > 1 ? `${commit.slice(0, -1).join(", ")} and ${commit.at(-1) ?? ""}` : commit[0];
|
|
296
|
+
io.stdout(designSystem && name ? `Next: commit ${files}. An app that depends on ${name} now finds it with no config of its own: run \`npx ${NAME} inspect\` in one to see it, and give its team ${GUIDE_FILE}.` : `Next: ${files ? `commit ${files}, and ` : ""}run \`npx ${NAME} check . --format github --require-design-system\` in CI.`);
|
|
218
297
|
return 0;
|
|
219
298
|
}
|
|
299
|
+
const CONFIG_FILE = "onsystem.config.json";
|
|
300
|
+
function readManifest(root) {
|
|
301
|
+
const file = path.join(root, "package.json");
|
|
302
|
+
try {
|
|
303
|
+
const text = fs.readFileSync(file, "utf8");
|
|
304
|
+
const json = JSON.parse(text);
|
|
305
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) return void 0;
|
|
306
|
+
return {
|
|
307
|
+
file,
|
|
308
|
+
json,
|
|
309
|
+
indent: /^([ \t]+)"/m.exec(text)?.[1] ?? " ",
|
|
310
|
+
newline: text.endsWith("\n")
|
|
311
|
+
};
|
|
312
|
+
} catch {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Declares the config in the package's package.json, `"onsystem": "./onsystem.config.json"`,
|
|
318
|
+
* and adds it to `files` when the package lists the files it publishes, so apps that install it
|
|
319
|
+
* get it too. Writes ONSYSTEM.md for the apps' teams unless one exists. Returns what it wrote.
|
|
320
|
+
*/
|
|
321
|
+
function declarePackage(root, manifest, name, force, notes) {
|
|
322
|
+
const written = [];
|
|
323
|
+
const { json } = manifest;
|
|
324
|
+
const value = `./${CONFIG_FILE}`;
|
|
325
|
+
let changed = false;
|
|
326
|
+
if (json["onsystem"] !== value) {
|
|
327
|
+
json[PACKAGE_FIELD] = value;
|
|
328
|
+
changed = true;
|
|
329
|
+
}
|
|
330
|
+
const files = Array.isArray(json.files) ? json.files : void 0;
|
|
331
|
+
if (files && !ships(files, CONFIG_FILE)) {
|
|
332
|
+
files.push(CONFIG_FILE);
|
|
333
|
+
changed = true;
|
|
334
|
+
}
|
|
335
|
+
if (changed) {
|
|
336
|
+
fs.writeFileSync(manifest.file, `${JSON.stringify(json, null, manifest.indent)}${manifest.newline ? "\n" : ""}`);
|
|
337
|
+
written.push("package.json");
|
|
338
|
+
}
|
|
339
|
+
notes.push(`This is a design-system package: package.json declares its config ("${PACKAGE_FIELD}": "${value}"${files ? ", also added to \"files\"" : ""}), which every app that depends on ${name} reads when it has no config of its own.`);
|
|
340
|
+
const guide = path.join(root, GUIDE_FILE);
|
|
341
|
+
if (force || !fs.existsSync(guide)) {
|
|
342
|
+
fs.writeFileSync(guide, consumerGuide(name));
|
|
343
|
+
written.push(GUIDE_FILE);
|
|
344
|
+
} else notes.push(`${GUIDE_FILE} already exists; it was left as it is (--force replaces it).`);
|
|
345
|
+
return written;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* What apps that install the package from a registry will not find: the component and token
|
|
349
|
+
* files its `files` list leaves out of the published package. Undefined for a private package,
|
|
350
|
+
* one without a `files` list (npm then ships everything), or when nothing is left out.
|
|
351
|
+
*/
|
|
352
|
+
async function unshipped(config, json) {
|
|
353
|
+
if (json.private === true || !Array.isArray(json.files)) return void 0;
|
|
354
|
+
const files = json.files;
|
|
355
|
+
const missing = [...await componentFiles(config), ...await tokenFiles(config)].map((f) => relativePath(config.root, f)).filter((f) => !f.startsWith("../")).filter((f) => !ships(files, f));
|
|
356
|
+
if (!missing.length) return void 0;
|
|
357
|
+
const folders = [...new Set(missing.map((f) => f.includes("/") ? `${f.slice(0, f.indexOf("/"))}/` : f))];
|
|
358
|
+
return `Apps that install ${typeof json.name === "string" ? json.name : "the package"} from a registry get only what "files" in package.json publishes, which leaves out ${folders.join(", ")} (${count$1(missing.length, "component or token file")}): they will not find those. Add ${folders.length === 1 ? "it" : "them"} to "files" for them; workspace apps read the sources either way.`;
|
|
359
|
+
}
|
|
360
|
+
/** Whether npm's `files` list publishes `file`, a path relative to the package. */
|
|
361
|
+
function ships(files, file) {
|
|
362
|
+
return files.some((entry) => {
|
|
363
|
+
if (typeof entry !== "string") return false;
|
|
364
|
+
const pattern = entry.replace(/^\.?\//, "").replace(/\/+$/, "");
|
|
365
|
+
if (!pattern || pattern.startsWith("!")) return false;
|
|
366
|
+
return file === pattern || file.startsWith(`${pattern}/`) || matchesGlob(file, pattern) || matchesGlob(file, `${pattern}/**`);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/** ONSYSTEM.md: how an app that uses the package gets the design system to its agents and CI. */
|
|
370
|
+
function consumerGuide(name) {
|
|
371
|
+
return `${[
|
|
372
|
+
`# onsystem in apps that use ${name}`,
|
|
373
|
+
"",
|
|
374
|
+
`${name} declares its [onsystem](https://onsystem.vercel.app) config in its package.json: \`"${PACKAGE_FIELD}": "./${CONFIG_FILE}"\`. An app that depends on ${name}, as a workspace package or installed from npm, reads that config when it has none of its own, and its UI is checked against ${name}'s components, props, variants and tokens. To see what an app finds, run this in its folder:`,
|
|
375
|
+
"",
|
|
376
|
+
"```sh",
|
|
377
|
+
`npx -y ${NAME} inspect`,
|
|
378
|
+
"```",
|
|
379
|
+
"",
|
|
380
|
+
`An app with an ${CONFIG_FILE} of its own reads that instead.`,
|
|
381
|
+
"",
|
|
382
|
+
"## Coding agents",
|
|
383
|
+
"",
|
|
384
|
+
"Claude Code: the plugin checks every UI file the agent writes and hands the errors back to fix, and adds the MCP server and a skill.",
|
|
385
|
+
"",
|
|
386
|
+
"```text",
|
|
387
|
+
"/plugin marketplace add dgesteves/onsystem",
|
|
388
|
+
"/plugin install onsystem@dgesteves",
|
|
389
|
+
"```",
|
|
390
|
+
"",
|
|
391
|
+
`Other agents: \`npx skills add dgesteves/onsystem\` adds the skill, and \`npx -y ${NAME}\` started in the app is the MCP server. [Set up your agent](https://onsystem.vercel.app/docs/clients) has the config for each client.`,
|
|
392
|
+
"",
|
|
393
|
+
"## CI",
|
|
394
|
+
"",
|
|
395
|
+
"In the app, record the findings it already has once, and commit the baseline, so pull requests fail only on new ones:",
|
|
396
|
+
"",
|
|
397
|
+
"```sh",
|
|
398
|
+
`npm install --save-dev ${NAME}`,
|
|
399
|
+
`npx ${NAME} check . --update-baseline`,
|
|
400
|
+
"```",
|
|
401
|
+
"",
|
|
402
|
+
"```yaml",
|
|
403
|
+
`- run: npx ${NAME} check . --format github --require-design-system`,
|
|
404
|
+
"```",
|
|
405
|
+
"",
|
|
406
|
+
"[CI and baselines](https://onsystem.vercel.app/docs/ci) has the GitHub Action and the rest.",
|
|
407
|
+
"",
|
|
408
|
+
"## Changing the config",
|
|
409
|
+
"",
|
|
410
|
+
`${CONFIG_FILE} in ${name} sets what every app gets: where the components, tokens and docs are, and the rules' severities. Its \`tsconfig\`, \`overrides\` and \`includeDesignSystem\` apply to ${name}'s own files only. [Design-system packages](https://onsystem.vercel.app/docs/design-system-packages) has the details.`
|
|
411
|
+
].join("\n")}\n`;
|
|
412
|
+
}
|
|
220
413
|
function count$1(n, word) {
|
|
221
414
|
return `${n.toLocaleString("en-US")} ${n === 1 ? word : `${word}s`}`;
|
|
222
415
|
}
|
|
@@ -234,6 +427,9 @@ Usage
|
|
|
234
427
|
onsystem check <paths...> Lint files, folders or globs with the check_ui rules
|
|
235
428
|
(for CI)
|
|
236
429
|
onsystem inspect Print what was extracted from the project
|
|
430
|
+
onsystem export --format dtcg|design-md
|
|
431
|
+
Print the tokens it found as a DTCG 2025.10 file or a
|
|
432
|
+
DESIGN.md (--out <file> writes them to a file)
|
|
237
433
|
onsystem init Write onsystem.config.json from what was found, run a
|
|
238
434
|
dry-run check and offer a baseline
|
|
239
435
|
onsystem help Show this help
|
|
@@ -252,7 +448,9 @@ Options
|
|
|
252
448
|
--docs <glob> Component docs (Markdown/MDX), repeatable
|
|
253
449
|
--no-cache Ignore the on-disk extraction cache
|
|
254
450
|
--no-watch serve: do not reload when files change
|
|
255
|
-
--format <format> check: pretty | json | github | sarif (default: pretty)
|
|
451
|
+
--format <format> check: pretty | json | github | sarif (default: pretty);
|
|
452
|
+
export: dtcg | design-md
|
|
453
|
+
--out <file> export: write to this file instead of stdout
|
|
256
454
|
--max-warnings <n> check: exit 1 when there are more than n warnings
|
|
257
455
|
--update-baseline check: record the current findings as accepted, and exit 0
|
|
258
456
|
--baseline <file> check: baseline file (default: ${BASELINE_FILE} in the root,
|
|
@@ -292,7 +490,8 @@ const defaultIo = {
|
|
|
292
490
|
stdout: (text) => process.stdout.write(`${text}\n`),
|
|
293
491
|
stderr: (text) => process.stderr.write(`${text}\n`),
|
|
294
492
|
color: process.stdout.isTTY && !process.env.NO_COLOR,
|
|
295
|
-
prompt: process.stdin.isTTY && process.stdout.isTTY ? askInTerminal : void 0
|
|
493
|
+
prompt: process.stdin.isTTY && process.stdout.isTTY ? askInTerminal : void 0,
|
|
494
|
+
stdinIsTTY: process.stdin.isTTY
|
|
296
495
|
};
|
|
297
496
|
/** One question on the terminal, answered with a line. */
|
|
298
497
|
async function askInTerminal(question) {
|
|
@@ -337,10 +536,8 @@ async function main(argv, io = defaultIo) {
|
|
|
337
536
|
type: "boolean",
|
|
338
537
|
default: true
|
|
339
538
|
},
|
|
340
|
-
format: {
|
|
341
|
-
|
|
342
|
-
default: "pretty"
|
|
343
|
-
},
|
|
539
|
+
format: { type: "string" },
|
|
540
|
+
out: { type: "string" },
|
|
344
541
|
"max-warnings": { type: "string" },
|
|
345
542
|
baseline: { type: "string" },
|
|
346
543
|
"update-baseline": {
|
|
@@ -422,15 +619,18 @@ async function main(argv, io = defaultIo) {
|
|
|
422
619
|
try {
|
|
423
620
|
switch (command) {
|
|
424
621
|
case "serve":
|
|
622
|
+
if (io.stdinIsTTY) io.stderr(`${NAME} is an MCP server: it is waiting for an agent on stdin. Add it to your agent (${DOCS}/clients), or run \`npx -y ${NAME} inspect\` or \`npx -y ${NAME} check .\` here. Ctrl+C quits.`);
|
|
425
623
|
await serveStdio({
|
|
426
624
|
...configOptions,
|
|
427
625
|
cache: values.cache,
|
|
428
626
|
watch: values.watch,
|
|
429
|
-
logger: stderrLogger
|
|
627
|
+
logger: stderrLogger,
|
|
628
|
+
...io.transport ? { transport: io.transport } : {}
|
|
430
629
|
});
|
|
431
630
|
return 0;
|
|
432
631
|
case "check": return await check(rest, configOptions, values, io);
|
|
433
632
|
case "inspect": return await inspect(configOptions, values, io);
|
|
633
|
+
case "export": return await exportTokens(configOptions, values, io);
|
|
434
634
|
case "init": return await init(configOptions, {
|
|
435
635
|
yes: values.yes,
|
|
436
636
|
force: values.force,
|
|
@@ -470,14 +670,14 @@ async function check(patterns, configOptions, values, io) {
|
|
|
470
670
|
io.stderr(`check: pass files, folders or globs, e.g. ${NAME} check .`);
|
|
471
671
|
return 2;
|
|
472
672
|
}
|
|
473
|
-
const format = values.format;
|
|
673
|
+
const format = values.format ?? "pretty";
|
|
474
674
|
if (![
|
|
475
675
|
"pretty",
|
|
476
676
|
"json",
|
|
477
677
|
"github",
|
|
478
678
|
"sarif"
|
|
479
679
|
].includes(format)) {
|
|
480
|
-
io.stderr(`check: unknown --format "${
|
|
680
|
+
io.stderr(`check: unknown --format "${format}" (pretty, json, github, sarif)`);
|
|
481
681
|
return 2;
|
|
482
682
|
}
|
|
483
683
|
const maxWarnings = values["max-warnings"] === void 0 ? void 0 : Number(values["max-warnings"]);
|
|
@@ -487,6 +687,7 @@ async function check(patterns, configOptions, values, io) {
|
|
|
487
687
|
}
|
|
488
688
|
const target = await loadTarget(configOptions);
|
|
489
689
|
warnDeprecated(target.config.deprecations, format, io);
|
|
690
|
+
warnDeprecated(configWarnings(target), format, io);
|
|
490
691
|
const selection = target.workspace ? await selectInWorkspace(target.workspace, patterns, values, format, io) : await selectInProject(target.config, patterns, values, format, io);
|
|
491
692
|
if (typeof selection === "number") return selection;
|
|
492
693
|
const { files, notes } = selection;
|
|
@@ -518,7 +719,7 @@ async function check(patterns, configOptions, values, io) {
|
|
|
518
719
|
let baselined = 0;
|
|
519
720
|
let fixed = 0;
|
|
520
721
|
for (const file of files) {
|
|
521
|
-
const code = await
|
|
722
|
+
const code = await fsp.readFile(file, "utf8");
|
|
522
723
|
const ds = selection.designSystemOf(file);
|
|
523
724
|
let result = ds.check(code, relativePath(ds.root, file));
|
|
524
725
|
const key = baselineKey(file);
|
|
@@ -660,7 +861,7 @@ async function selectInProject(config, patterns, values, format, io) {
|
|
|
660
861
|
return 0;
|
|
661
862
|
}
|
|
662
863
|
const notice = ds.notice();
|
|
663
|
-
if (notice && values["require-design-system"]) {
|
|
864
|
+
if (notice && values["require-design-system"] && !ds.hasDesignSystem()) {
|
|
664
865
|
io.stderr(`check --require-design-system: ${notice}`);
|
|
665
866
|
return 2;
|
|
666
867
|
}
|
|
@@ -747,7 +948,7 @@ async function selectInWorkspace(workspace, patterns, values, format, io) {
|
|
|
747
948
|
}
|
|
748
949
|
if (values["require-design-system"]) {
|
|
749
950
|
let complete = false;
|
|
750
|
-
for (const p of workspace.projects) if (
|
|
951
|
+
for (const p of workspace.projects) if ((await load(p)).hasDesignSystem()) {
|
|
751
952
|
complete = true;
|
|
752
953
|
break;
|
|
753
954
|
}
|
|
@@ -799,6 +1000,7 @@ async function inspect(configOptions, values, io) {
|
|
|
799
1000
|
const target = await loadTarget(configOptions);
|
|
800
1001
|
const { config } = target;
|
|
801
1002
|
warnDeprecated(config.deprecations, "pretty", io);
|
|
1003
|
+
warnDeprecated(configWarnings(target), "pretty", io);
|
|
802
1004
|
if (target.workspace) return inspectWorkspace(target.workspace, values, io);
|
|
803
1005
|
const ds = await loadDesignSystem(config, {
|
|
804
1006
|
cache: values.cache,
|
|
@@ -811,12 +1013,13 @@ async function inspect(configOptions, values, io) {
|
|
|
811
1013
|
}
|
|
812
1014
|
const lines = [
|
|
813
1015
|
`root ${model.root}`,
|
|
814
|
-
`config ${config.configFile ? displayPath(io.cwd, config.configFile) : "(defaults)"}`,
|
|
1016
|
+
`config ${config.configFile ? `${displayPath(io.cwd, config.configFile)}${declaredBy(config)}` : "(defaults)"}`,
|
|
815
1017
|
...config.detected ? [`detected ${config.detected}`] : [],
|
|
1018
|
+
...describeLibraries(config, model, io.cwd).map((line) => `library ${line}`),
|
|
816
1019
|
`files ${plural(model.stats.files.components, "component file")}, ${plural(model.stats.files.tokens, "token file")}, ${plural(model.stats.files.docs, "doc")}`,
|
|
817
1020
|
`time ${model.stats.durationMs}ms${model.stats.fromCache ? " (cache)" : ""}`,
|
|
818
1021
|
"",
|
|
819
|
-
`Components (${ds.roots().length} + ${
|
|
1022
|
+
`Components (${ds.roots().length} + ${ds.components.length - ds.roots().length} parts)`
|
|
820
1023
|
];
|
|
821
1024
|
for (const c of ds.roots()) {
|
|
822
1025
|
const variants = c.variants.map((v) => `${v.name}[${v.values.length}]`).join(" ");
|
|
@@ -840,6 +1043,64 @@ async function inspect(configOptions, values, io) {
|
|
|
840
1043
|
io.stdout(lines.join("\n"));
|
|
841
1044
|
return 0;
|
|
842
1045
|
}
|
|
1046
|
+
/**
|
|
1047
|
+
* `MUI (@mui/material 9.4.0): 149 components from its type declarations`, per library the
|
|
1048
|
+
* project depends on, including those the config's `libraries` leaves out.
|
|
1049
|
+
*/
|
|
1050
|
+
function describeLibraries(config, model, cwd) {
|
|
1051
|
+
const name = (library) => `${libraryAdapter(library.package)?.title ?? library.package} (${library.package}${library.version ? ` ${library.version}` : ""})`;
|
|
1052
|
+
return [...(config.libraries ?? []).map((library) => {
|
|
1053
|
+
if (!library.dir) return `${name(library)}: a dependency, but not installed`;
|
|
1054
|
+
const count = model.components.filter((c) => c.library === library.package).length;
|
|
1055
|
+
return `${name(library)}: ${plural(count, "component")} from its type declarations`;
|
|
1056
|
+
}), ...(config.librariesOff ?? []).map((library) => `${name(library)}: ${librariesLeftOut(config, cwd)}`)];
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* `export --format dtcg|design-md`: the project's tokens as a DTCG 2025.10 file or a
|
|
1060
|
+
* DESIGN.md, on stdout or in the `--out` file.
|
|
1061
|
+
*/
|
|
1062
|
+
async function exportTokens(configOptions, values, io) {
|
|
1063
|
+
const format = values.format;
|
|
1064
|
+
if (format !== "dtcg" && format !== "design-md") {
|
|
1065
|
+
io.stderr(`export: pass --format dtcg or --format design-md${format === void 0 ? "" : ` (not "${format}")`}`);
|
|
1066
|
+
return 2;
|
|
1067
|
+
}
|
|
1068
|
+
const target = await loadTarget(configOptions);
|
|
1069
|
+
warnDeprecated(target.config.deprecations, "pretty", io);
|
|
1070
|
+
warnDeprecated(configWarnings(target), "pretty", io);
|
|
1071
|
+
if (target.workspace) {
|
|
1072
|
+
const dirs = target.workspace.projects.map((p) => p.dir);
|
|
1073
|
+
io.stderr(`export: ${target.workspace.root} is a monorepo root, and each project has its own tokens. Pass --root with one of them${dirs.length ? ` (${listOf(dirs)})` : ""}.`);
|
|
1074
|
+
return 2;
|
|
1075
|
+
}
|
|
1076
|
+
const ds = await loadDesignSystem(target.config, {
|
|
1077
|
+
cache: values.cache,
|
|
1078
|
+
logger: silentLogger
|
|
1079
|
+
});
|
|
1080
|
+
if (!ds.tokens.length) {
|
|
1081
|
+
io.stderr(`export: no design tokens found in ${ds.root}. \`${NAME} inspect --explain\` shows where it looked.`);
|
|
1082
|
+
return 2;
|
|
1083
|
+
}
|
|
1084
|
+
const result = format === "dtcg" ? toDtcg(ds.tokens) : toDesignMd(ds.tokens, { name: projectName(ds.root) });
|
|
1085
|
+
for (const warning of result.warnings) io.stderr(`export: ${warning}`);
|
|
1086
|
+
if (values.out === void 0) {
|
|
1087
|
+
io.stdout(result.text.trimEnd());
|
|
1088
|
+
return 0;
|
|
1089
|
+
}
|
|
1090
|
+
const file = path.resolve(io.cwd, values.out);
|
|
1091
|
+
await fsp.mkdir(path.dirname(file), { recursive: true });
|
|
1092
|
+
await fsp.writeFile(file, result.text);
|
|
1093
|
+
io.stdout(`Wrote ${plural(result.count, "token")} to ${displayPath(io.cwd, file)} (${format === "dtcg" ? "DTCG 2025.10" : "DESIGN.md"})`);
|
|
1094
|
+
return 0;
|
|
1095
|
+
}
|
|
1096
|
+
/** The package name in the root's package.json, else the folder's name. */
|
|
1097
|
+
function projectName(root) {
|
|
1098
|
+
try {
|
|
1099
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
1100
|
+
if (typeof pkg.name === "string" && pkg.name) return pkg.name;
|
|
1101
|
+
} catch {}
|
|
1102
|
+
return path.basename(root);
|
|
1103
|
+
}
|
|
843
1104
|
function tokenCounts(model) {
|
|
844
1105
|
const counts = /* @__PURE__ */ new Map();
|
|
845
1106
|
for (const t of model.tokens) counts.set(t.category, (counts.get(t.category) ?? 0) + 1);
|
|
@@ -880,8 +1141,8 @@ async function inspectWorkspace(workspace, values, io) {
|
|
|
880
1141
|
const { model } = ds;
|
|
881
1142
|
const roots = ds.roots();
|
|
882
1143
|
const tokens = [...tokenCounts(model)].map(([category, n]) => `${category} ${n}`);
|
|
883
|
-
const own = project.config.configFile && project.config.configFile !== workspace.configFile ? displayPath(workspace.root, project.config.configFile) : void 0;
|
|
884
|
-
lines.push("", `${project.dir}${project.name ? ` (${project.name})` : ""}`, ...own ? [` config ${own}`] : [], ...project.config.detected ? [` detected ${project.config.detected}`] : [], ` files ${plural(model.stats.files.components, "component file")}, ${plural(model.stats.files.tokens, "token file")}, ${plural(model.stats.files.docs, "doc")}`, ` components ${roots.length} + ${
|
|
1144
|
+
const own = project.config.configFile && project.config.configFile !== workspace.configFile ? `${displayPath(workspace.root, project.config.configFile)}${declaredBy(project.config)}` : void 0;
|
|
1145
|
+
lines.push("", `${project.dir}${project.name ? ` (${project.name})` : ""}`, ...own ? [` config ${own}`] : [], ...project.config.detected ? [` detected ${project.config.detected}`] : [], ...describeLibraries(project.config, model, io.cwd).map((line) => ` library ${line}`), ` files ${plural(model.stats.files.components, "component file")}, ${plural(model.stats.files.tokens, "token file")}, ${plural(model.stats.files.docs, "doc")}`, ` components ${roots.length} + ${ds.components.length - roots.length} parts${roots.length ? `: ${listNames(roots.map((c) => c.name))}` : ""}`, ` tokens ${tokens.length ? tokens.join(", ") : "none"}`, ...model.warnings.length ? [` warnings ${model.warnings.length}: ${model.warnings[0] ?? ""}`] : []);
|
|
885
1146
|
}
|
|
886
1147
|
if (!loaded.length) lines.push("", `No workspace package has a design system that zero config finds. See ${DOCS}/troubleshooting`);
|
|
887
1148
|
if (workspace.others.length) lines.push("", `Without a design system (${workspace.others.length}): ${listNames(workspace.others)}`);
|
|
@@ -903,6 +1164,10 @@ function explainWorkspace(workspace, loaded, cwd) {
|
|
|
903
1164
|
for (const { project, ds } of loaded) lines.push("", `${project.dir}${project.name ? ` (${project.name})` : ""}`, ...explainProject(ds, cwd, " "));
|
|
904
1165
|
return lines;
|
|
905
1166
|
}
|
|
1167
|
+
/** ` (declared by @acme/ui)` after a config a design-system package declares; empty otherwise. */
|
|
1168
|
+
function declaredBy(config) {
|
|
1169
|
+
return config.designSystemPackage ? ` (declared by ${config.designSystemPackage.name})` : "";
|
|
1170
|
+
}
|
|
906
1171
|
/** `A, B, C` up to a dozen names, then `and 40 more`. */
|
|
907
1172
|
function listNames(names, max = 12) {
|
|
908
1173
|
const shown = names.slice(0, max).join(", ");
|
|
@@ -912,6 +1177,10 @@ function listNames(names, max = 12) {
|
|
|
912
1177
|
* Says once what a file's old name means: on stderr, or as a workflow warning with
|
|
913
1178
|
* `--format github` so it shows on the run. Never on stdout otherwise, which JSON output owns.
|
|
914
1179
|
*/
|
|
1180
|
+
/** What the configs read have to say: the project's, or every package's at a monorepo root. */
|
|
1181
|
+
function configWarnings(target) {
|
|
1182
|
+
return target.workspace ? target.workspace.warnings : target.config.warnings;
|
|
1183
|
+
}
|
|
915
1184
|
function warnDeprecated(messages, format, io) {
|
|
916
1185
|
for (const text of messages ?? []) if (format === "github") io.stdout(`::warning title=${NAME}::${text}`);
|
|
917
1186
|
else io.stderr(text);
|