c3b-lint 0.0.17 → 0.0.20
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 +8 -0
- package/c3b-lint.js +50 -6
- package/check/report.js +34 -9
- package/config/eslint.config.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,6 +104,14 @@ Findings print in sections, in the order they run — `--- c3b-lint ---` (checks
|
|
|
104
104
|
`--- stylelint ---` — each grouped under the file's absolute path the way ESLint prints, with a `✖ N problems` total.
|
|
105
105
|
A section with no findings is not printed. The timings follow, then the error count.
|
|
106
106
|
|
|
107
|
+
With `C3B_LINT_REPORT=<path>` in the environment, every finding of the run is also written to that file as JSON,
|
|
108
|
+
passed or failed: `{ version, errors, warnings, findings }`, each finding
|
|
109
|
+
`{ check, file, line, column, severity, rule, message, fixable }` — `check` is `structure`, `html`, `tokens`,
|
|
110
|
+
`editorconfig`, `lengths`, `eslint` or `stylelint`, `file` is relative to the project root, `rule` and `fixable`
|
|
111
|
+
come from ESLint and Stylelint (null and false for c3b-lint's own checks). It's for a program reading the run —
|
|
112
|
+
colab-desktop's Output summary. An environment variable rather than a flag: a caller that sets it for every project
|
|
113
|
+
can't know which c3b-lint each one pins, and an older version ignores it where it would refuse a flag.
|
|
114
|
+
|
|
107
115
|
## Not covered
|
|
108
116
|
|
|
109
117
|
Rules that need judgement stay with review: whether two selectors are structurally disjoint, flex vs grid, whether
|
package/c3b-lint.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync, globSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
-
import { dirname, join, resolve, sep } from 'path';
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from 'path';
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
5
|
import { ESLint } from 'eslint';
|
|
6
6
|
import stylelint from 'stylelint';
|
|
@@ -83,6 +83,12 @@ for (const arg of args) {
|
|
|
83
83
|
|
|
84
84
|
const fix = args.includes('--fix');
|
|
85
85
|
const init = args.includes('--init');
|
|
86
|
+
// Where to also write every finding as JSON, for a program reading the run (colab-desktop's Output
|
|
87
|
+
// summary). An environment variable rather than a flag: a caller that sets it for every project can't
|
|
88
|
+
// know which c3b-lint each one pins, and an older version ignores it where it would refuse a flag.
|
|
89
|
+
const REPORT_FILE = process.env.C3B_LINT_REPORT;
|
|
90
|
+
// ESLint's and Stylelint's findings, in the report's shape, as they come in
|
|
91
|
+
const toolFindings = [];
|
|
86
92
|
|
|
87
93
|
const toPosix = (p) => p.split(sep).join('/');
|
|
88
94
|
|
|
@@ -155,6 +161,16 @@ async function runEslint() {
|
|
|
155
161
|
if (fix) {
|
|
156
162
|
await ESLint.outputFixes(results);
|
|
157
163
|
}
|
|
164
|
+
results.forEach((result) => result.messages.forEach((message) => toolFindings.push({
|
|
165
|
+
check: 'eslint',
|
|
166
|
+
file: toPosix(relative(process.cwd(), result.filePath)),
|
|
167
|
+
line: message.line ?? null,
|
|
168
|
+
column: message.column ?? null,
|
|
169
|
+
severity: message.severity === 2 ? 'error' : 'warning',
|
|
170
|
+
rule: message.ruleId ?? null,
|
|
171
|
+
message: message.message,
|
|
172
|
+
fixable: Boolean(message.fix)
|
|
173
|
+
})));
|
|
158
174
|
const formatter = await eslint.loadFormatter('stylish');
|
|
159
175
|
printSection('eslint', await formatter.format(results));
|
|
160
176
|
|
|
@@ -176,10 +192,35 @@ async function runStylelint() {
|
|
|
176
192
|
formatter: 'string'
|
|
177
193
|
});
|
|
178
194
|
printSection('stylelint', result.report ?? result.output);
|
|
195
|
+
result.results.forEach((file) => file.warnings.forEach((warning) => toolFindings.push({
|
|
196
|
+
check: 'stylelint',
|
|
197
|
+
file: toPosix(relative(process.cwd(), file.source)),
|
|
198
|
+
line: warning.line ?? null,
|
|
199
|
+
column: warning.column ?? null,
|
|
200
|
+
severity: warning.severity === 'error' ? 'error' : 'warning',
|
|
201
|
+
rule: warning.rule ?? null,
|
|
202
|
+
// Stylelint ends each message with its rule in brackets; the report has the rule apart
|
|
203
|
+
message: warning.text.replace(/\s*\([^()]+\)$/, ''),
|
|
204
|
+
fixable: false
|
|
205
|
+
})));
|
|
179
206
|
|
|
180
207
|
return result.results.reduce((sum, file) => sum + file.warnings.filter((w) => w.severity === 'error').length, 0);
|
|
181
208
|
}
|
|
182
209
|
|
|
210
|
+
// --- the JSON report --------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
// Every finding of the run, whether it failed or not: { version, errors, warnings, findings }, the
|
|
213
|
+
// files relative to the project root. Written whole, once the checks are done.
|
|
214
|
+
function writeReport(path, findings) {
|
|
215
|
+
const count = (severity) => findings.filter((finding) => finding.severity === severity).length;
|
|
216
|
+
const body = { version, errors: count('error'), warnings: count('warning'), findings };
|
|
217
|
+
try {
|
|
218
|
+
writeFileSync(resolve(path), `${ JSON.stringify(body, null, 2) }\n`);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
fail(`could not write the report to ${ path }: ${ error.message }`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
183
224
|
// --- run ------------------------------------------------------------------
|
|
184
225
|
|
|
185
226
|
// The checks run in a fixed order: c3b-lint's own, then ESLint, then Stylelint. Each tool's findings
|
|
@@ -213,11 +254,11 @@ async function main() {
|
|
|
213
254
|
};
|
|
214
255
|
|
|
215
256
|
const report = createReport();
|
|
216
|
-
await timed('structure', () => checkStructure(report));
|
|
217
|
-
await timed('html', () => checkHtml(report));
|
|
218
|
-
await timed('tokens', () => checkCssTokens(report));
|
|
219
|
-
await timed('editorconfig', () => checkEditorconfig(report));
|
|
220
|
-
await timed('lengths', () => checkFileLengths(report));
|
|
257
|
+
await timed('structure', () => checkStructure(report.as('structure')));
|
|
258
|
+
await timed('html', () => checkHtml(report.as('html')));
|
|
259
|
+
await timed('tokens', () => checkCssTokens(report.as('tokens')));
|
|
260
|
+
await timed('editorconfig', () => checkEditorconfig(report.as('editorconfig')));
|
|
261
|
+
await timed('lengths', () => checkFileLengths(report.as('lengths')));
|
|
221
262
|
printSection(name, report.format());
|
|
222
263
|
let errors = report.errors;
|
|
223
264
|
errors += await timed('eslint', () => runEslint());
|
|
@@ -225,6 +266,9 @@ async function main() {
|
|
|
225
266
|
|
|
226
267
|
console.log(`\n${ timings.join('\n') }`);
|
|
227
268
|
console.log(` ${ 'total'.padEnd(13) }${ Math.round(performance.now() - started) }ms`);
|
|
269
|
+
if (REPORT_FILE) {
|
|
270
|
+
writeReport(REPORT_FILE, [...report.findings(), ...toolFindings]);
|
|
271
|
+
}
|
|
228
272
|
if (errors > 0) {
|
|
229
273
|
fail(`${ errors } error(s)`);
|
|
230
274
|
}
|
package/check/report.js
CHANGED
|
@@ -2,31 +2,56 @@ import { resolve } from 'path';
|
|
|
2
2
|
|
|
3
3
|
// Findings are collected, then printed grouped under each file's absolute path the way ESLint's
|
|
4
4
|
// stylish formatter prints, so the c3b-lint and ESLint sections of a run read the same. Returns a
|
|
5
|
-
// counter so main() knows how many errors were found.
|
|
5
|
+
// counter so main() knows how many errors were found. Each finding also keeps the check it came
|
|
6
|
+
// from (as(check) hands a check its own reporter), for the JSON report (C3B_LINT_REPORT).
|
|
6
7
|
export function createReport() {
|
|
7
8
|
const byFile = new Map();
|
|
8
9
|
let errors = 0;
|
|
9
10
|
let warnings = 0;
|
|
10
11
|
|
|
11
12
|
// Checks pass "file" or "file:line"
|
|
12
|
-
function add(where, level, message) {
|
|
13
|
+
function add(where, level, message, check) {
|
|
13
14
|
const match = where.match(/^(.*):(\d+)$/);
|
|
14
15
|
const file = match ? match[1] : where;
|
|
15
16
|
const line = match ? match[2] : '';
|
|
16
17
|
if (!byFile.has(file)) {
|
|
17
18
|
byFile.set(file, []);
|
|
18
19
|
}
|
|
19
|
-
byFile.get(file).push({ line, level, message });
|
|
20
|
+
byFile.get(file).push({ line, level, message, check });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function error(where, message, check = null) {
|
|
24
|
+
add(where, 'error', message, check);
|
|
25
|
+
errors++;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function warn(where, message, check = null) {
|
|
29
|
+
add(where, 'warning', message, check);
|
|
30
|
+
warnings++;
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
return {
|
|
23
|
-
error
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
error,
|
|
35
|
+
warn,
|
|
36
|
+
// the reporter one check is handed: the same report, its findings marked as that check's
|
|
37
|
+
as(check) {
|
|
38
|
+
return {
|
|
39
|
+
error: (where, message) => error(where, message, check),
|
|
40
|
+
warn: (where, message) => warn(where, message, check)
|
|
41
|
+
};
|
|
26
42
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
// every finding, for the JSON report: { check, file, line, column, severity, rule, message, fixable }
|
|
44
|
+
findings() {
|
|
45
|
+
return [...byFile.entries()].flatMap(([file, found]) => found.map((finding) => ({
|
|
46
|
+
check: finding.check,
|
|
47
|
+
file,
|
|
48
|
+
line: finding.line ? Number(finding.line) : null,
|
|
49
|
+
column: null,
|
|
50
|
+
severity: finding.level,
|
|
51
|
+
rule: null,
|
|
52
|
+
message: finding.message,
|
|
53
|
+
fixable: false
|
|
54
|
+
})));
|
|
30
55
|
},
|
|
31
56
|
// The whole report as one string, empty when nothing was found
|
|
32
57
|
format() {
|
package/config/eslint.config.js
CHANGED
|
@@ -130,8 +130,13 @@ export const librarySrc = {
|
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
-
// the "ignoreLength" files: everything but max-lines still applies (ESLint refuses an empty files list)
|
|
134
|
-
|
|
133
|
+
// the "ignoreLength" files: everything but max-lines still applies (ESLint refuses an empty files list).
|
|
134
|
+
// Each glob is ANDed with the JS extensions (a nested array): a `files` pattern also adds what it matches
|
|
135
|
+
// to what ESLint lints, so a bare "src/index.html" here had ESLint parse the page as JavaScript.
|
|
136
|
+
const JS_FILES = '**/*.{js,mjs,cjs}';
|
|
137
|
+
const lengthExempt = settings.ignoreLength.length
|
|
138
|
+
? [{ files: settings.ignoreLength.map((glob) => [glob, JS_FILES]), rules: { 'max-lines': 'off' } }]
|
|
139
|
+
: [];
|
|
135
140
|
|
|
136
141
|
export default [
|
|
137
142
|
{ ignores: ['node_modules/**', 'dist/**', 'c10g/**', 'src/**/vendor/**', '**/*.min.js', ...settings.ignore] },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c3b-lint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "Lints a co:lab project against the rules a tool can check: ESLint, Stylelint, the 500-line limit and the shared .editorconfig; --init writes the config files.",
|
|
5
5
|
"license": "GPL-3.0",
|
|
6
6
|
"type": "module",
|