onsystem 0.4.0 → 0.5.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 +18 -14
- package/dist/cli.js +42 -152
- package/dist/{stdio-BcJth14k.js → design-system-CnEv5sIL.js} +311 -1531
- package/dist/eslint-worker.d.ts +36 -0
- package/dist/eslint-worker.js +88 -0
- package/dist/eslint.d.ts +23 -0
- package/dist/eslint.js +172 -0
- package/dist/index.d.ts +30 -387
- package/dist/index.js +2 -1
- package/dist/stdio-DjfkiDMV.js +1399 -0
- package/dist/types-DJtccu3A.d.ts +386 -0
- package/dist/workspace-DzWQ4L8R.js +193 -0
- package/package.json +20 -1
package/README.md
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
**Keeps coding agents on your design system:** it knows your real components, props, variants and tokens, catches the moment an agent invents one and has it fix it, and the same check gates your PRs. Local, zero config, works alongside [@shadcn/lint](https://github.com/shadcn-ui/lint).
|
|
4
4
|
|
|
5
|
-
Built for design-system and platform teams with a React design-system package; a shadcn/ui app is the zero-config demo. Formerly `@dgesteves/design-system-mcp` ([what changed](https://
|
|
5
|
+
Built for design-system and platform teams with a React design-system package; a shadcn/ui app is the zero-config demo. Formerly `@dgesteves/design-system-mcp` ([what changed](https://onsystem.vercel.app/docs/migrating)).
|
|
6
6
|
|
|
7
7
|
[](https://github.com/dgesteves/onsystem/actions/workflows/ci.yml)
|
|
8
8
|
[](https://www.npmjs.com/package/onsystem)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
|
|
11
|
-
**Website and docs:** [
|
|
11
|
+
**Website and docs:** [onsystem.vercel.app](https://onsystem.vercel.app), with a [playground](https://onsystem.vercel.app/playground) that checks your own code.
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
|
-
<a href="https://
|
|
14
|
+
<a href="https://onsystem.vercel.app"><img src=".github/assets/demo.webp" width="100%" alt="On the website, an agent drafts a settings card, check_ui finds 11 problems with a fix each and the agent applies them. Then a real Claude Code session with the plugin, where Claude looks the components up before writing and check_ui passes. Last, the benchmark: clean components go from 6 to 10 out of 10 with Claude Haiku 4.5 and from 8 to 10 with Claude Opus 5."></a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
-
<sub>On the <a href="https://
|
|
17
|
+
<sub>On the <a href="https://onsystem.vercel.app">website</a>, an agent drafts a settings card, <code>check_ui</code> finds 11 problems with a fix each, and the agent applies them. Then a real Claude Code session with the plugin: Claude looks the components up before writing, and <code>check_ui</code> passes. Last, the benchmark.</sub>
|
|
18
18
|
|
|
19
19
|
## The problem
|
|
20
20
|
|
|
@@ -35,11 +35,11 @@ onsystem reads your components, `cva` and `tv` variants, parts and tokens from s
|
|
|
35
35
|
| `no-hardcoded-spacing` | warn | Arbitrary padding, margin and gap (`p-[13px]` → `p-3`) |
|
|
36
36
|
| `no-hardcoded-radius` | warn | Arbitrary radius (`rounded-[7px]` → `rounded-sm`) |
|
|
37
37
|
|
|
38
|
-
**How often it is wrong.** The [corpus](corpus) job runs `check` on 10 public repositories pinned by commit (Documenso, Dub, Cal.com, midday and others, the four monorepos also from their root: 21 runs, 4,731 findings) and compares the findings with hand labels. In a random sample of 126 findings, 5 are false positives (4.0%) and 55 are debatable; 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, so those three rules have 1 finding in the corpus and none in the sample; their precision comes from the test suite. The [rules reference](https://
|
|
38
|
+
**How often it is wrong.** The [corpus](corpus) job runs `check` on 10 public repositories pinned by commit (Documenso, Dub, Cal.com, midday and others, the four monorepos also from their root: 21 runs, 4,731 findings) and compares the findings with hand labels. In a random sample of 126 findings, 5 are false positives (4.0%) and 55 are debatable; 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, so those three rules have 1 finding in the corpus and none in the sample; their precision comes from the test suite. 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
39
|
|
|
40
40
|
## Quickstart
|
|
41
41
|
|
|
42
|
-
Run it from the app's folder or from the monorepo root, with Node.js 20.19 or later. `npx onsystem inspect` shows what it found, and `--explain` why; at a [monorepo root](https://
|
|
42
|
+
Run it from the app's folder or from the monorepo root, with Node.js 20.19 or later. `npx onsystem inspect` shows what it found, and `--explain` why; at a [monorepo root](https://onsystem.vercel.app/docs/configuration#monorepo-roots), every app and design-system package, each file checked by its own project's design system. `npx onsystem init` writes the config from it and offers a baseline.
|
|
43
43
|
|
|
44
44
|
**Claude Code.** The plugin brings the hook, the MCP server and a skill:
|
|
45
45
|
|
|
@@ -48,11 +48,13 @@ Run it from the app's folder or from the monorepo root, with Node.js 20.19 or la
|
|
|
48
48
|
/plugin install onsystem@dgesteves
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
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://
|
|
51
|
+
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
52
|
|
|
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://
|
|
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.
|
|
54
54
|
|
|
55
|
-
**
|
|
55
|
+
**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
|
+
|
|
57
|
+
**Any MCP client.** Run `npx -y onsystem` as a stdio server. There are [configs for Claude Desktop, Codex CLI, GitHub Copilot CLI, Windsurf, JetBrains IDEs, Zed, Gemini CLI and Grok Build](https://onsystem.vercel.app/docs/clients#other-clients).
|
|
56
58
|
|
|
57
59
|
**CI.** Install it as a dev dependency, accept the findings the codebase already has once, and fail pull requests only on new ones:
|
|
58
60
|
|
|
@@ -65,7 +67,9 @@ npx onsystem check . --update-baseline # writes onsystem.baseline.json: commit
|
|
|
65
67
|
- run: npx onsystem check . --format github --require-design-system
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
Findings show up as annotations on the pull request, and `--require-design-system` fails the job when the design system is no longer found. [CI and baselines](https://
|
|
70
|
+
Findings show up as annotations on the pull request, and `--require-design-system` fails the job when the design system is no longer found. [CI and baselines](https://onsystem.vercel.app/docs/ci) has the whole workflow. The GitHub Action, `uses: dgesteves/onsystem@v0`, annotates only the lines a pull request changed: [make it a required check on agent pull requests](https://onsystem.vercel.app/docs/required-check).
|
|
71
|
+
|
|
72
|
+
**ESLint.** The same rules for ESLint 9 and 10: add `onsystem.configs.recommended` from `onsystem/eslint` to `eslint.config.mjs` ([ESLint plugin](https://onsystem.vercel.app/docs/eslint), with @shadcn/lint).
|
|
69
73
|
|
|
70
74
|
## Works with @shadcn/lint
|
|
71
75
|
|
|
@@ -84,11 +88,11 @@ It is small: one repository, Claude models only, 40 runs (one per task, model an
|
|
|
84
88
|
|
|
85
89
|
## Docs
|
|
86
90
|
|
|
87
|
-
On the [website](https://
|
|
91
|
+
On the [website](https://onsystem.vercel.app/docs), and as Markdown in [docs/](docs):
|
|
88
92
|
|
|
89
|
-
- [Quickstart](https://
|
|
90
|
-
- [Configuration](https://
|
|
91
|
-
- [How it works](https://
|
|
93
|
+
- [Quickstart](https://onsystem.vercel.app/docs), [Claude Code plugin](https://onsystem.vercel.app/docs/plugin), [CI and baselines](https://onsystem.vercel.app/docs/ci) and [Set up your agent](https://onsystem.vercel.app/docs/clients), with the config for each client
|
|
94
|
+
- [Configuration](https://onsystem.vercel.app/docs/configuration), [Rules](https://onsystem.vercel.app/rules), [MCP tools](https://onsystem.vercel.app/docs/tools) and [Troubleshooting](https://onsystem.vercel.app/docs/troubleshooting)
|
|
95
|
+
- [How it works](https://onsystem.vercel.app/docs/how-it-works), [FAQ](https://onsystem.vercel.app/docs/faq) and [Migrating from design-system-mcp](https://onsystem.vercel.app/docs/migrating)
|
|
92
96
|
|
|
93
97
|
[Contributing](CONTRIBUTING.md), [security](SECURITY.md) and the [changelog](CHANGELOG.md) are in the repository.
|
|
94
98
|
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { $ as plural, C as updateBaseline, F as CONFIG_FILES, H as RULE_IDS, I as ConfigError, J as detectProject, K as rulesFor, L as DEFAULT_DOCS, R as DEFAULT_SEVERITY, S as readBaseline, V as LEGACY_CONFIG_FILES, _ as VERSION, at as toPosix, b as applyBaseline, c as silentLogger, g as NAME, h as formatDiagnostics, i as componentFiles, it as relativePath, l as stderrLogger, m as uncheckedNotice, nt as isInside, o as loadDesignSystem, rt as matchesGlob, s as tokenFiles, tt as unique, v as BASELINE_FILE, w as writeBaseline, x as countBaseline, y as LEGACY_BASELINE_FILE } from "./design-system-CnEv5sIL.js";
|
|
3
|
+
import { t as serveStdio } from "./stdio-DjfkiDMV.js";
|
|
4
|
+
import { r as loadTarget } from "./workspace-DzWQ4L8R.js";
|
|
3
5
|
import fs from "node:fs";
|
|
4
6
|
import path from "node:path";
|
|
5
7
|
import { escapePath, glob } from "tinyglobby";
|
|
@@ -7,148 +9,6 @@ import fs$1 from "node:fs/promises";
|
|
|
7
9
|
import { spawnSync } from "node:child_process";
|
|
8
10
|
import readline from "node:readline/promises";
|
|
9
11
|
import { parseArgs } from "node:util";
|
|
10
|
-
//#region src/baseline.ts
|
|
11
|
-
const BASELINE_FILE = "onsystem.baseline.json";
|
|
12
|
-
/** The baseline's name from before the rename to onsystem: still read when the new one is absent. */
|
|
13
|
-
const LEGACY_BASELINE_FILE = "design-system-mcp.baseline.json";
|
|
14
|
-
/**
|
|
15
|
-
* A finding's identity within its file and rule: the offending text with
|
|
16
|
-
* whitespace collapsed, and without the quotes of a string literal, so a
|
|
17
|
-
* formatter switching quote style does not make every finding look new.
|
|
18
|
-
*/
|
|
19
|
-
function key(diagnostic) {
|
|
20
|
-
const text = diagnostic.source.replace(/\s+/g, " ").trim();
|
|
21
|
-
return (/^(['"`])(.*)\1$/.exec(text)?.[2] ?? text).slice(0, 200);
|
|
22
|
-
}
|
|
23
|
-
/** The baseline's entries, or undefined when the file does not exist. */
|
|
24
|
-
function readBaseline(file) {
|
|
25
|
-
let text;
|
|
26
|
-
try {
|
|
27
|
-
text = fs.readFileSync(file, "utf8");
|
|
28
|
-
} catch (error) {
|
|
29
|
-
if (error.code === "ENOENT") return void 0;
|
|
30
|
-
throw new ConfigError(`Could not read baseline ${file}: ${error.message}`);
|
|
31
|
-
}
|
|
32
|
-
const invalid = (why) => new ConfigError(`${file} is not a valid onsystem baseline (${why}). Fix it, or delete it and run check --update-baseline.`);
|
|
33
|
-
let parsed;
|
|
34
|
-
try {
|
|
35
|
-
parsed = JSON.parse(text);
|
|
36
|
-
} catch (error) {
|
|
37
|
-
throw invalid(error.message);
|
|
38
|
-
}
|
|
39
|
-
if (!isObject(parsed) || parsed.version !== 1) throw invalid("expected \"version\": 1");
|
|
40
|
-
if (!isObject(parsed.files)) throw invalid("\"files\" must be an object");
|
|
41
|
-
const entries = /* @__PURE__ */ new Map();
|
|
42
|
-
for (const [name, rules] of Object.entries(parsed.files)) {
|
|
43
|
-
if (!isObject(rules)) throw invalid(`"${name}" must map rules to findings`);
|
|
44
|
-
const byRule = /* @__PURE__ */ new Map();
|
|
45
|
-
for (const [rule, sources] of Object.entries(rules)) {
|
|
46
|
-
if (!isObject(sources)) throw invalid(`"${name}" › "${rule}" must map findings to counts`);
|
|
47
|
-
const counts = /* @__PURE__ */ new Map();
|
|
48
|
-
for (const [source, count] of Object.entries(sources)) {
|
|
49
|
-
if (!Number.isInteger(count) || count < 1) throw invalid(`"${name}" › "${rule}" › "${source}" must be a positive whole number`);
|
|
50
|
-
counts.set(source, count);
|
|
51
|
-
}
|
|
52
|
-
byRule.set(rule, counts);
|
|
53
|
-
}
|
|
54
|
-
entries.set(name, byRule);
|
|
55
|
-
}
|
|
56
|
-
return entries;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Removes the findings the baseline accepts for `file`. Within a file and
|
|
60
|
-
* rule, each entry accepts that many findings with the same source text, in
|
|
61
|
-
* order; any beyond the count are new. Entries of `disabled` rules are not
|
|
62
|
-
* counted as fixed: the rule is off, not satisfied.
|
|
63
|
-
*/
|
|
64
|
-
function applyBaseline(baseline, file, result, disabled = /* @__PURE__ */ new Set()) {
|
|
65
|
-
const remaining = /* @__PURE__ */ new Map();
|
|
66
|
-
for (const [rule, sources] of baseline.get(file) ?? []) {
|
|
67
|
-
if (disabled.has(rule)) continue;
|
|
68
|
-
for (const [source, count] of sources) remaining.set(`${rule}\0${source}`, count);
|
|
69
|
-
}
|
|
70
|
-
const diagnostics = [];
|
|
71
|
-
let baselined = 0;
|
|
72
|
-
for (const diagnostic of result.diagnostics) {
|
|
73
|
-
const id = `${diagnostic.ruleId}\0${key(diagnostic)}`;
|
|
74
|
-
const left = remaining.get(id) ?? 0;
|
|
75
|
-
if (left > 0) {
|
|
76
|
-
remaining.set(id, left - 1);
|
|
77
|
-
baselined++;
|
|
78
|
-
} else diagnostics.push(diagnostic);
|
|
79
|
-
}
|
|
80
|
-
let fixed = 0;
|
|
81
|
-
for (const count of remaining.values()) fixed += count;
|
|
82
|
-
return {
|
|
83
|
-
result: {
|
|
84
|
-
...result,
|
|
85
|
-
diagnostics,
|
|
86
|
-
errorCount: diagnostics.filter((d) => d.severity === "error").length,
|
|
87
|
-
warningCount: diagnostics.filter((d) => d.severity === "warning").length
|
|
88
|
-
},
|
|
89
|
-
baselined,
|
|
90
|
-
fixed
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* The baseline after a run over `checked` (file → its findings): those files
|
|
95
|
-
* get exactly their current findings, plus their previous entries for
|
|
96
|
-
* `disabled` rules; other files keep their entries while they still exist
|
|
97
|
-
* under `root`.
|
|
98
|
-
*/
|
|
99
|
-
function updateBaseline(previous, checked, root, disabled = /* @__PURE__ */ new Set()) {
|
|
100
|
-
const disabledIn = typeof disabled === "function" ? disabled : () => disabled;
|
|
101
|
-
const next = /* @__PURE__ */ new Map();
|
|
102
|
-
for (const [file, rules] of previous ?? []) if (!checked.has(file) && fs.existsSync(path.resolve(root, file))) next.set(file, rules);
|
|
103
|
-
for (const [file, diagnostics] of checked) {
|
|
104
|
-
const rules = /* @__PURE__ */ new Map();
|
|
105
|
-
const off = disabledIn(file);
|
|
106
|
-
for (const [rule, sources] of previous?.get(file) ?? []) if (off.has(rule)) rules.set(rule, sources);
|
|
107
|
-
for (const diagnostic of diagnostics) {
|
|
108
|
-
let sources = rules.get(diagnostic.ruleId);
|
|
109
|
-
if (!sources) rules.set(diagnostic.ruleId, sources = /* @__PURE__ */ new Map());
|
|
110
|
-
const id = key(diagnostic);
|
|
111
|
-
sources.set(id, (sources.get(id) ?? 0) + 1);
|
|
112
|
-
}
|
|
113
|
-
if (rules.size) next.set(file, rules);
|
|
114
|
-
}
|
|
115
|
-
return next;
|
|
116
|
-
}
|
|
117
|
-
function countBaseline(entries) {
|
|
118
|
-
let findings = 0;
|
|
119
|
-
for (const rules of entries.values()) for (const sources of rules.values()) for (const count of sources.values()) findings += count;
|
|
120
|
-
return {
|
|
121
|
-
findings,
|
|
122
|
-
files: entries.size
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
/** Writes the baseline sorted at every level, so it diffs cleanly in review. */
|
|
126
|
-
function writeBaseline(file, entries) {
|
|
127
|
-
const sorted = (map) => [...map].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
128
|
-
const files = {};
|
|
129
|
-
for (const [name, rules] of sorted(entries)) {
|
|
130
|
-
const byRule = {};
|
|
131
|
-
for (const [rule, sources] of sorted(rules)) byRule[rule] = Object.fromEntries(sorted(sources));
|
|
132
|
-
Object.defineProperty(files, name, {
|
|
133
|
-
value: byRule,
|
|
134
|
-
enumerable: true,
|
|
135
|
-
writable: true
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
const baseline = {
|
|
139
|
-
version: 1,
|
|
140
|
-
files
|
|
141
|
-
};
|
|
142
|
-
try {
|
|
143
|
-
fs.writeFileSync(file, `${JSON.stringify(baseline, null, 2)}\n`);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
throw new ConfigError(`Could not write baseline ${file}: ${error.message}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
function isObject(value) {
|
|
149
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
150
|
-
}
|
|
151
|
-
//#endregion
|
|
152
12
|
//#region src/explain.ts
|
|
153
13
|
/**
|
|
154
14
|
* What `inspect --explain` says about one project: every candidate zero config looked
|
|
@@ -362,7 +222,7 @@ function count$1(n, word) {
|
|
|
362
222
|
}
|
|
363
223
|
//#endregion
|
|
364
224
|
//#region src/cli.ts
|
|
365
|
-
const DOCS = "https://
|
|
225
|
+
const DOCS = "https://onsystem.vercel.app/docs";
|
|
366
226
|
const HELP = `${NAME} ${VERSION}
|
|
367
227
|
|
|
368
228
|
Keeps coding agents on your React design system: checks UI code against your
|
|
@@ -392,7 +252,7 @@ Options
|
|
|
392
252
|
--docs <glob> Component docs (Markdown/MDX), repeatable
|
|
393
253
|
--no-cache Ignore the on-disk extraction cache
|
|
394
254
|
--no-watch serve: do not reload when files change
|
|
395
|
-
--format <format> check: pretty | json | github (default: pretty)
|
|
255
|
+
--format <format> check: pretty | json | github | sarif (default: pretty)
|
|
396
256
|
--max-warnings <n> check: exit 1 when there are more than n warnings
|
|
397
257
|
--update-baseline check: record the current findings as accepted, and exit 0
|
|
398
258
|
--baseline <file> check: baseline file (default: ${BASELINE_FILE} in the root,
|
|
@@ -614,9 +474,10 @@ async function check(patterns, configOptions, values, io) {
|
|
|
614
474
|
if (![
|
|
615
475
|
"pretty",
|
|
616
476
|
"json",
|
|
617
|
-
"github"
|
|
477
|
+
"github",
|
|
478
|
+
"sarif"
|
|
618
479
|
].includes(format)) {
|
|
619
|
-
io.stderr(`check: unknown --format "${values.format}" (pretty, json, github)`);
|
|
480
|
+
io.stderr(`check: unknown --format "${values.format}" (pretty, json, github, sarif)`);
|
|
620
481
|
return 2;
|
|
621
482
|
}
|
|
622
483
|
const maxWarnings = values["max-warnings"] === void 0 ? void 0 : Number(values["max-warnings"]);
|
|
@@ -630,11 +491,10 @@ async function check(patterns, configOptions, values, io) {
|
|
|
630
491
|
if (typeof selection === "number") return selection;
|
|
631
492
|
const { files, notes } = selection;
|
|
632
493
|
if (!files.length) {
|
|
633
|
-
if (format === "
|
|
634
|
-
else if (format === "pretty") {
|
|
494
|
+
if (format === "pretty") {
|
|
635
495
|
const hints = [selection.skipped ? `Pass --include-design-system to check ${selection.skipped === 1 ? "it" : "them"}.` : "", selection.testsLeftOut ? "Pass --include-tests to check tests and stories." : ""].filter(Boolean);
|
|
636
496
|
io.stdout(`Nothing to check: ${notes.join("; ")}. ${hints.join(" ")}`.trimEnd());
|
|
637
|
-
}
|
|
497
|
+
} else printNoResults(format, io);
|
|
638
498
|
return 0;
|
|
639
499
|
}
|
|
640
500
|
const update = values["update-baseline"];
|
|
@@ -693,6 +553,7 @@ async function check(patterns, configOptions, values, io) {
|
|
|
693
553
|
}
|
|
694
554
|
const output = formatDiagnostics(format === "json" ? results : results.filter((r) => !r.skipped), format, {
|
|
695
555
|
color: io.color,
|
|
556
|
+
uri: format === "sarif" || format === "github" ? repositoryPath(io.cwd, io.env ?? process.env) : void 0,
|
|
696
557
|
baselined: baseline ? baselined : void 0,
|
|
697
558
|
notes,
|
|
698
559
|
fixedHint: fixed ? `${count(fixed, "baseline finding")} no longer ${fixed === 1 ? "occurs" : "occur"}: run \`check ${patterns.map(shellQuote).join(" ")} --update-baseline\` to drop ${fixed === 1 ? "it" : "them"}.` : void 0
|
|
@@ -713,6 +574,35 @@ async function check(patterns, configOptions, values, io) {
|
|
|
713
574
|
}
|
|
714
575
|
return 0;
|
|
715
576
|
}
|
|
577
|
+
/** What `json` and `sarif` print when there is nothing to report on: `[]`, or a log without results. */
|
|
578
|
+
function printNoResults(format, io) {
|
|
579
|
+
if (format === "json") io.stdout("[]");
|
|
580
|
+
else if (format === "sarif") io.stdout(formatDiagnostics([], "sarif"));
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Maps a path relative to the working directory to one relative to the repository root, which
|
|
584
|
+
* GitHub resolves workflow command annotations (`--format github`) and code scanning locations
|
|
585
|
+
* (`--format sarif`) against, so `check` run from `apps/web` still reports
|
|
586
|
+
* `apps/web/app/page.tsx`. The root is GITHUB_WORKSPACE or the git top-level, whichever is the
|
|
587
|
+
* innermost folder holding the file: the git top-level outside Actions, GITHUB_WORKSPACE where
|
|
588
|
+
* git is missing or refuses the checkout, and the repository checked out in a folder of the
|
|
589
|
+
* workspace (`actions/checkout` with `path:`). A file under neither keeps its path relative to
|
|
590
|
+
* the working directory.
|
|
591
|
+
*/
|
|
592
|
+
function repositoryPath(cwd, env) {
|
|
593
|
+
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
594
|
+
cwd,
|
|
595
|
+
encoding: "utf8",
|
|
596
|
+
windowsHide: true
|
|
597
|
+
});
|
|
598
|
+
const roots = [result.status === 0 && typeof result.stdout === "string" ? result.stdout.trim() : "", env.GITHUB_WORKSPACE ?? ""].filter(Boolean).map((dir) => realPath(path.resolve(cwd, dir))).sort((a, b) => b.length - a.length);
|
|
599
|
+
if (!roots.length) return toPosix;
|
|
600
|
+
return (file) => {
|
|
601
|
+
const absolute = realPath(path.resolve(cwd, file));
|
|
602
|
+
const root = roots.find((dir) => isInside(dir, absolute));
|
|
603
|
+
return root ? relativePath(root, absolute) : toPosix(file);
|
|
604
|
+
};
|
|
605
|
+
}
|
|
716
606
|
/** Splits the patterns into files named outright and globs (a folder: every TSX/JSX file under it). */
|
|
717
607
|
function expandPatterns(patterns, cwd) {
|
|
718
608
|
const named = [];
|
|
@@ -766,7 +656,7 @@ async function selectInProject(config, patterns, values, format, io) {
|
|
|
766
656
|
return 2;
|
|
767
657
|
}
|
|
768
658
|
if (values["quiet-without-design-system"] && !config.configFile && !ds.components.length) {
|
|
769
|
-
|
|
659
|
+
printNoResults(format, io);
|
|
770
660
|
return 0;
|
|
771
661
|
}
|
|
772
662
|
const notice = ds.notice();
|
|
@@ -851,7 +741,7 @@ async function selectInWorkspace(workspace, patterns, values, format, io) {
|
|
|
851
741
|
if (values["quiet-without-design-system"]) {
|
|
852
742
|
files = files.filter((file) => Boolean(project(file).config.configFile) || designSystemOf(file).components.length > 0);
|
|
853
743
|
if (!files.length) {
|
|
854
|
-
|
|
744
|
+
printNoResults(format, io);
|
|
855
745
|
return 0;
|
|
856
746
|
}
|
|
857
747
|
}
|