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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { a as DesignSystemModel, x as ResolvedConfig } from "./types-DJtccu3A.js";
|
|
2
|
+
import { MessagePort } from "node:worker_threads";
|
|
3
|
+
//#region src/eslint/worker.d.ts
|
|
4
|
+
export interface LoadRequest {
|
|
5
|
+
/** ESLint's working directory: where `check` would run from. */
|
|
6
|
+
cwd: string;
|
|
7
|
+
/** The file being linted, absolute. */
|
|
8
|
+
file: string;
|
|
9
|
+
/** `settings.onsystem` in the ESLint config: `--root` and `--config`, relative to `cwd`. */
|
|
10
|
+
root?: string | undefined;
|
|
11
|
+
config?: string | undefined;
|
|
12
|
+
/** The version of each design system the plugin already holds, by project root. */
|
|
13
|
+
known: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
export type LoadResponse = {
|
|
16
|
+
/** Why the file is not checked, as `check .` would leave it out. */
|
|
17
|
+
skip: string;
|
|
18
|
+
} | {
|
|
19
|
+
root: string;
|
|
20
|
+
version: string;
|
|
21
|
+
/** Left out when the plugin already holds this version. */
|
|
22
|
+
data?: {
|
|
23
|
+
config: ResolvedConfig;
|
|
24
|
+
model: DesignSystemModel;
|
|
25
|
+
};
|
|
26
|
+
} | {
|
|
27
|
+
error: string;
|
|
28
|
+
};
|
|
29
|
+
export interface Call {
|
|
30
|
+
request: LoadRequest;
|
|
31
|
+
/** Set to 1, and notified, once the response is on `port`. */
|
|
32
|
+
signal: Int32Array;
|
|
33
|
+
port: MessagePort;
|
|
34
|
+
}
|
|
35
|
+
export declare function load(request: LoadRequest): Promise<LoadResponse>;
|
|
36
|
+
//#endregion
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { B as DEFAULT_TEST_FOLDERS, i as componentFiles, it as relativePath, r as buildModel, rt as matchesGlob, z as DEFAULT_TEST_EXCLUDE } from "./design-system-CnEv5sIL.js";
|
|
2
|
+
import { r as loadTarget } from "./workspace-DzWQ4L8R.js";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { parentPort } from "node:worker_threads";
|
|
6
|
+
//#region src/eslint/worker.ts
|
|
7
|
+
/**
|
|
8
|
+
* The ESLint plugin's loader, in a worker thread. ESLint rules run synchronously, and finding a
|
|
9
|
+
* project's design system does not (config files written in code, globs, the extraction
|
|
10
|
+
* cache), so the plugin asks this worker and waits for the answer (see `bridge.ts`).
|
|
11
|
+
*/
|
|
12
|
+
/** How long a loaded design system is trusted before its files are looked at again. */
|
|
13
|
+
const FRESH_MS = 2e3;
|
|
14
|
+
const targets = /* @__PURE__ */ new Map();
|
|
15
|
+
/** The default test and story patterns: the recommended config ignores those files itself. */
|
|
16
|
+
const TEST_PATTERNS = /* @__PURE__ */ new Set([DEFAULT_TEST_EXCLUDE, DEFAULT_TEST_FOLDERS]);
|
|
17
|
+
const projects = /* @__PURE__ */ new Map();
|
|
18
|
+
function realPath(file) {
|
|
19
|
+
try {
|
|
20
|
+
return fs.realpathSync.native(file);
|
|
21
|
+
} catch {
|
|
22
|
+
return file;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function targetFor({ cwd, root, config }) {
|
|
26
|
+
const key = JSON.stringify([
|
|
27
|
+
cwd,
|
|
28
|
+
root,
|
|
29
|
+
config
|
|
30
|
+
]);
|
|
31
|
+
const cached = targets.get(key);
|
|
32
|
+
if (cached && Date.now() - cached.at < FRESH_MS) return cached.target;
|
|
33
|
+
const target = loadTarget({
|
|
34
|
+
cwd,
|
|
35
|
+
root: root === void 0 ? void 0 : path.resolve(cwd, root),
|
|
36
|
+
config: config === void 0 ? void 0 : path.resolve(cwd, config)
|
|
37
|
+
});
|
|
38
|
+
targets.set(key, {
|
|
39
|
+
at: Date.now(),
|
|
40
|
+
target
|
|
41
|
+
});
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
async function load(request) {
|
|
45
|
+
const target = await targetFor(request);
|
|
46
|
+
const config = target.workspace ? (await target.workspace.projectFor(request.file)).config : target.config;
|
|
47
|
+
let project = projects.get(config.root);
|
|
48
|
+
if (!project || Date.now() - project.at >= FRESH_MS) {
|
|
49
|
+
const { model, key } = await buildModel(config, { cache: true });
|
|
50
|
+
const own = new Set((await componentFiles(config)).map(realPath));
|
|
51
|
+
project = {
|
|
52
|
+
at: Date.now(),
|
|
53
|
+
version: key ?? String(Date.now()),
|
|
54
|
+
config,
|
|
55
|
+
model,
|
|
56
|
+
own
|
|
57
|
+
};
|
|
58
|
+
projects.set(config.root, project);
|
|
59
|
+
}
|
|
60
|
+
const relative = relativePath(config.root, request.file);
|
|
61
|
+
const excluded = config.exclude.find((pattern) => !TEST_PATTERNS.has(pattern) && matchesGlob(relative, pattern));
|
|
62
|
+
if (excluded) return { skip: `excluded by "${excluded}"` };
|
|
63
|
+
if (!config.includeDesignSystem && project.own.has(realPath(request.file))) return { skip: "one of the design system’s own component files" };
|
|
64
|
+
const { version } = project;
|
|
65
|
+
return {
|
|
66
|
+
root: config.root,
|
|
67
|
+
version,
|
|
68
|
+
...request.known[config.root] === version ? {} : { data: {
|
|
69
|
+
config: project.config,
|
|
70
|
+
model: project.model
|
|
71
|
+
} }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
parentPort?.on("message", ({ request, signal, port }) => {
|
|
75
|
+
const reply = (response) => {
|
|
76
|
+
port.postMessage(response);
|
|
77
|
+
Atomics.store(signal, 0, 1);
|
|
78
|
+
Atomics.notify(signal, 0);
|
|
79
|
+
};
|
|
80
|
+
load({
|
|
81
|
+
...request,
|
|
82
|
+
file: path.resolve(request.cwd, request.file)
|
|
83
|
+
}).then(reply, (error) => {
|
|
84
|
+
reply({ error: error.message });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
//#endregion
|
|
88
|
+
export { load };
|
package/dist/eslint.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Linter, Rule } from "eslint";
|
|
2
|
+
//#region src/eslint/index.d.ts
|
|
3
|
+
/** `settings.onsystem`: where the project and its config are, when finding them from ESLint's cwd is not right. */
|
|
4
|
+
export interface Settings {
|
|
5
|
+
/** The project root, relative to ESLint's working directory (`check --root`). */
|
|
6
|
+
root?: string;
|
|
7
|
+
/** The config file, relative to ESLint's working directory (`check --config`). */
|
|
8
|
+
config?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const plugin: {
|
|
11
|
+
meta: {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
};
|
|
15
|
+
rules: {
|
|
16
|
+
[k: string]: Rule.RuleModule;
|
|
17
|
+
};
|
|
18
|
+
configs: {
|
|
19
|
+
recommended: Linter.Config;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { plugin as default };
|
package/dist/eslint.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { B as DEFAULT_TEST_FOLDERS, R as DEFAULT_SEVERITY, T as RULES, _ as VERSION, g as NAME, it as relativePath, t as DesignSystem, z as DEFAULT_TEST_EXCLUDE } from "./design-system-CnEv5sIL.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MessageChannel, Worker, receiveMessageOnPort } from "node:worker_threads";
|
|
4
|
+
//#region src/eslint/bridge.ts
|
|
5
|
+
/**
|
|
6
|
+
* Calls the loader worker (`worker.ts`) synchronously: posts the request, blocks on a shared
|
|
7
|
+
* flag until the worker has answered, and takes the answer off the port. The worker is
|
|
8
|
+
* started on first use and does not keep ESLint's process alive.
|
|
9
|
+
*/
|
|
10
|
+
/** Long enough for a cold extraction of a large design system. */
|
|
11
|
+
const TIMEOUT_MS = 12e4;
|
|
12
|
+
let worker;
|
|
13
|
+
function start() {
|
|
14
|
+
if (worker) return worker;
|
|
15
|
+
worker = new Worker(new URL("./eslint-worker.js", import.meta.url));
|
|
16
|
+
worker.unref();
|
|
17
|
+
worker.on("error", () => {
|
|
18
|
+
worker = void 0;
|
|
19
|
+
});
|
|
20
|
+
worker.on("exit", () => {
|
|
21
|
+
worker = void 0;
|
|
22
|
+
});
|
|
23
|
+
return worker;
|
|
24
|
+
}
|
|
25
|
+
function loadSync(request) {
|
|
26
|
+
const { port1, port2 } = new MessageChannel();
|
|
27
|
+
const signal = new Int32Array(new SharedArrayBuffer(4));
|
|
28
|
+
try {
|
|
29
|
+
const call = {
|
|
30
|
+
request,
|
|
31
|
+
signal,
|
|
32
|
+
port: port2
|
|
33
|
+
};
|
|
34
|
+
start().postMessage(call, [port2]);
|
|
35
|
+
if (Atomics.wait(signal, 0, 0, TIMEOUT_MS) === "timed-out") return { error: `the design system did not load within ${TIMEOUT_MS / 1e3} seconds` };
|
|
36
|
+
const message = receiveMessageOnPort(port1);
|
|
37
|
+
return message ? message.message : { error: "the loader sent no answer" };
|
|
38
|
+
} finally {
|
|
39
|
+
port1.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/eslint/index.ts
|
|
44
|
+
/**
|
|
45
|
+
* onsystem's rules as an ESLint plugin (ESLint 9 and 10, flat config):
|
|
46
|
+
*
|
|
47
|
+
* import onsystem from 'onsystem/eslint';
|
|
48
|
+
* export default [...yourConfig, onsystem.configs.recommended];
|
|
49
|
+
*
|
|
50
|
+
* Each file is checked as `onsystem check` checks it, against the design system of its project
|
|
51
|
+
* (the nearest folder with a config, a components.json or a package.json, found from ESLint's
|
|
52
|
+
* working directory), with the project's config: its `rules` and `overrides` turn rules off
|
|
53
|
+
* per file, `// onsystem-disable-*` comments suppress findings, and the files `check .` leaves
|
|
54
|
+
* out (the config's `exclude`, the design system's own components) are not checked. ESLint's
|
|
55
|
+
* config sets each rule's severity.
|
|
56
|
+
*/
|
|
57
|
+
const RULES_PAGE = "https://onsystem.vercel.app/rules";
|
|
58
|
+
/** Design systems by project root, at the version the loader last reported. */
|
|
59
|
+
const designSystems = /* @__PURE__ */ new Map();
|
|
60
|
+
/**
|
|
61
|
+
* One check per file per lint pass, shared by every rule of the pass: they see the same
|
|
62
|
+
* SourceCode, and a new pass (another run, a fix pass, an editor's next keystroke) gets a new
|
|
63
|
+
* one, so the design system's freshness is asked about again. `reported` says that a file that
|
|
64
|
+
* could not be checked has been said so in this pass.
|
|
65
|
+
*/
|
|
66
|
+
const passes = /* @__PURE__ */ new WeakMap();
|
|
67
|
+
function passFor(context, file, settings) {
|
|
68
|
+
const { sourceCode } = context;
|
|
69
|
+
let pass = passes.get(sourceCode);
|
|
70
|
+
if (!pass) {
|
|
71
|
+
pass = {
|
|
72
|
+
outcome: run(context.cwd, file, sourceCode.text, settings),
|
|
73
|
+
reported: false
|
|
74
|
+
};
|
|
75
|
+
passes.set(sourceCode, pass);
|
|
76
|
+
}
|
|
77
|
+
return pass;
|
|
78
|
+
}
|
|
79
|
+
function run(cwd, file, text, settings) {
|
|
80
|
+
const response = loadSync({
|
|
81
|
+
cwd,
|
|
82
|
+
file,
|
|
83
|
+
known: Object.fromEntries([...designSystems].map(([root, { version }]) => [root, version])),
|
|
84
|
+
root: settings.root,
|
|
85
|
+
config: settings.config
|
|
86
|
+
});
|
|
87
|
+
if ("error" in response) return { error: response.error };
|
|
88
|
+
if ("skip" in response) return { skipped: true };
|
|
89
|
+
let entry = designSystems.get(response.root);
|
|
90
|
+
if (response.data) {
|
|
91
|
+
const { model, config } = response.data;
|
|
92
|
+
entry = {
|
|
93
|
+
version: response.version,
|
|
94
|
+
ds: new DesignSystem(model, config)
|
|
95
|
+
};
|
|
96
|
+
designSystems.set(response.root, entry);
|
|
97
|
+
}
|
|
98
|
+
if (!entry) return { error: `no design system loaded for ${response.root}` };
|
|
99
|
+
const result = entry.ds.check(text, relativePath(entry.ds.root, file));
|
|
100
|
+
return result.skipped ? { skipped: true } : { result };
|
|
101
|
+
}
|
|
102
|
+
function createRule(id, description) {
|
|
103
|
+
return {
|
|
104
|
+
meta: {
|
|
105
|
+
type: "problem",
|
|
106
|
+
docs: {
|
|
107
|
+
description,
|
|
108
|
+
url: `${RULES_PAGE}#${id}`
|
|
109
|
+
},
|
|
110
|
+
fixable: "code",
|
|
111
|
+
schema: []
|
|
112
|
+
},
|
|
113
|
+
create(context) {
|
|
114
|
+
const file = path.resolve(context.cwd, context.filename);
|
|
115
|
+
const settings = context.settings.onsystem ?? {};
|
|
116
|
+
return { "Program:exit"(node) {
|
|
117
|
+
const pass = passFor(context, file, settings);
|
|
118
|
+
const { outcome } = pass;
|
|
119
|
+
if ("skipped" in outcome) return;
|
|
120
|
+
if ("error" in outcome) {
|
|
121
|
+
if (pass.reported) return;
|
|
122
|
+
pass.reported = true;
|
|
123
|
+
context.report({
|
|
124
|
+
node,
|
|
125
|
+
message: `onsystem could not check this file: ${outcome.error}`
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
for (const d of outcome.result.diagnostics) {
|
|
130
|
+
if (d.ruleId !== id) continue;
|
|
131
|
+
const edits = d.fix;
|
|
132
|
+
context.report({
|
|
133
|
+
loc: {
|
|
134
|
+
start: {
|
|
135
|
+
line: d.line,
|
|
136
|
+
column: d.column - 1
|
|
137
|
+
},
|
|
138
|
+
end: {
|
|
139
|
+
line: d.endLine,
|
|
140
|
+
column: d.endColumn - 1
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
message: d.message,
|
|
144
|
+
...edits?.length ? { fix: (fixer) => edits.map((e) => fixer.replaceTextRange(e.range, e.text)) } : {}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
} };
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const rules = Object.fromEntries(RULES.map((rule) => [rule.id, createRule(rule.id, rule.description)]));
|
|
152
|
+
const plugin = {
|
|
153
|
+
meta: {
|
|
154
|
+
name: NAME,
|
|
155
|
+
version: VERSION
|
|
156
|
+
},
|
|
157
|
+
rules,
|
|
158
|
+
configs: {}
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Every rule at its default severity, for `.tsx` and `.jsx` files other than tests and stories,
|
|
162
|
+
* which `check` also leaves out unless asked (`--include-tests`).
|
|
163
|
+
*/
|
|
164
|
+
plugin.configs.recommended = {
|
|
165
|
+
name: "onsystem/recommended",
|
|
166
|
+
files: ["**/*.{tsx,jsx}"],
|
|
167
|
+
ignores: [DEFAULT_TEST_EXCLUDE, DEFAULT_TEST_FOLDERS],
|
|
168
|
+
plugins: { onsystem: plugin },
|
|
169
|
+
rules: Object.fromEntries(RULES.map((rule) => [`onsystem/${rule.id}`, DEFAULT_SEVERITY[rule.id]]))
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
172
|
+
export { plugin as default };
|