c3b-lint 0.0.18 → 0.0.21
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 +13 -2
- package/c3b-lint.js +59 -8
- package/check/report.js +34 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,9 @@ markdown that a tool can check, with one command and one config to install. It r
|
|
|
21
21
|
rules (that is a token by definition), and a token nothing uses (CSS `var()`, or JS/HTML by name).
|
|
22
22
|
4. That the project's **`.editorconfig`** is the shared one — it carries the JetBrains `ij_*` code-style keys, so
|
|
23
23
|
WebStorm and IntelliJ format the same way without committing `.idea/`.
|
|
24
|
-
5. The **500-line limit** on `src/**/*.{css,html}` (ESLint reports the JS); inline `<svg>` art is not counted
|
|
24
|
+
5. The **500-line limit** on `src/**/*.{css,html}` (ESLint reports the JS); inline `<svg>` art is not counted, nor is
|
|
25
|
+
a [colab-partial](https://codeberg.org/colab47/colab-partial) region whose partial is in `src/partial/` (one line
|
|
26
|
+
each: the partial is counted on its own).
|
|
25
27
|
6. **ESLint** on every `.js` file — classic browser scripts under `src/` (IIFE required, no top-level declarations,
|
|
26
28
|
no `console.log`), ES modules everywhere else — with `eslint:recommended` plus the rules from `frontend.md`:
|
|
27
29
|
braces on every `if`, `const`/`let` never `var`, `===`, a blank line before `return`, 500 lines max, and a
|
|
@@ -85,7 +87,8 @@ A project's own settings go in its `package.json`, under `"c3b-lint"`:
|
|
|
85
87
|
its own and never reads the HTML, so without them every use is "not defined". Read-only, `src/` scripts only.
|
|
86
88
|
- `singularDirectories` — plural-looking directory names that are proper names (a client's brand).
|
|
87
89
|
- `ignoreLength` — globs exempt from the 500-line limit and nothing else: the HTML, token, structure, ESLint and
|
|
88
|
-
Stylelint checks still run on them. For a file that can't be split
|
|
90
|
+
Stylelint checks still run on them. For a file that can't be split. A page made long by the partials it carries
|
|
91
|
+
needs none: their regions count as one line each.
|
|
89
92
|
|
|
90
93
|
The ESLint and Stylelint configs read these too, so WebStorm reports what the CLI does.
|
|
91
94
|
|
|
@@ -104,6 +107,14 @@ Findings print in sections, in the order they run — `--- c3b-lint ---` (checks
|
|
|
104
107
|
`--- stylelint ---` — each grouped under the file's absolute path the way ESLint prints, with a `✖ N problems` total.
|
|
105
108
|
A section with no findings is not printed. The timings follow, then the error count.
|
|
106
109
|
|
|
110
|
+
With `C3B_LINT_REPORT=<path>` in the environment, every finding of the run is also written to that file as JSON,
|
|
111
|
+
passed or failed: `{ version, errors, warnings, findings }`, each finding
|
|
112
|
+
`{ check, file, line, column, severity, rule, message, fixable }` — `check` is `structure`, `html`, `tokens`,
|
|
113
|
+
`editorconfig`, `lengths`, `eslint` or `stylelint`, `file` is relative to the project root, `rule` and `fixable`
|
|
114
|
+
come from ESLint and Stylelint (null and false for c3b-lint's own checks). It's for a program reading the run —
|
|
115
|
+
colab-desktop's Output summary. An environment variable rather than a flag: a caller that sets it for every project
|
|
116
|
+
can't know which c3b-lint each one pins, and an older version ignores it where it would refuse a flag.
|
|
117
|
+
|
|
107
118
|
## Not covered
|
|
108
119
|
|
|
109
120
|
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
|
|
|
@@ -125,14 +131,21 @@ function checkEditorconfig(report) {
|
|
|
125
131
|
}
|
|
126
132
|
}
|
|
127
133
|
|
|
134
|
+
// A colab-partial region: the opening marker's line, the copy, the closing marker's line naming the same partial
|
|
135
|
+
const PARTIAL_REGION = /^[ \t]*<!-- partial ([\w./-]+) -->[ \t]*\r?\n[\s\S]*?^[ \t]*<!-- \/partial \1 -->/gm;
|
|
136
|
+
|
|
128
137
|
// general.md: no hand-written file past 500 lines. ESLint reports JS; this covers CSS and HTML.
|
|
129
138
|
function checkFileLengths(report) {
|
|
130
139
|
const ignored = ignoredPaths();
|
|
131
140
|
const exempt = ignoredPaths(readSettings().ignoreLength);
|
|
132
141
|
const files = globSync('src/**/*.{css,html}').map(toPosix).filter((file) => !file.includes('/vendor/') && !ignored.has(file) && !exempt.has(file));
|
|
133
142
|
for (const file of files.sort()) {
|
|
134
|
-
// inline <svg> is vector art, not hand-written markup: its lines are not counted
|
|
135
|
-
|
|
143
|
+
// inline <svg> is vector art, not hand-written markup: its lines are not counted. Nor is a partial's
|
|
144
|
+
// copy (colab-partial writes it from src/partial/, which is counted on its own): one line, as long as
|
|
145
|
+
// that partial is there, so markers can't hide markup written nowhere else
|
|
146
|
+
const content = readFileSync(file, 'utf8')
|
|
147
|
+
.replace(/<svg[\s\S]*?<\/svg>/g, '<svg/>')
|
|
148
|
+
.replace(PARTIAL_REGION, (region, name) => (existsSync(join('src', 'partial', name)) ? `<!-- partial ${ name } -->` : region));
|
|
136
149
|
const lines = content.split('\n').length - (content.endsWith('\n') ? 1 : 0);
|
|
137
150
|
if (lines > MAX_LINES) {
|
|
138
151
|
report.error(file, `is ${ lines } lines (limit ${ MAX_LINES }) — split it by responsibility`);
|
|
@@ -155,6 +168,16 @@ async function runEslint() {
|
|
|
155
168
|
if (fix) {
|
|
156
169
|
await ESLint.outputFixes(results);
|
|
157
170
|
}
|
|
171
|
+
results.forEach((result) => result.messages.forEach((message) => toolFindings.push({
|
|
172
|
+
check: 'eslint',
|
|
173
|
+
file: toPosix(relative(process.cwd(), result.filePath)),
|
|
174
|
+
line: message.line ?? null,
|
|
175
|
+
column: message.column ?? null,
|
|
176
|
+
severity: message.severity === 2 ? 'error' : 'warning',
|
|
177
|
+
rule: message.ruleId ?? null,
|
|
178
|
+
message: message.message,
|
|
179
|
+
fixable: Boolean(message.fix)
|
|
180
|
+
})));
|
|
158
181
|
const formatter = await eslint.loadFormatter('stylish');
|
|
159
182
|
printSection('eslint', await formatter.format(results));
|
|
160
183
|
|
|
@@ -176,10 +199,35 @@ async function runStylelint() {
|
|
|
176
199
|
formatter: 'string'
|
|
177
200
|
});
|
|
178
201
|
printSection('stylelint', result.report ?? result.output);
|
|
202
|
+
result.results.forEach((file) => file.warnings.forEach((warning) => toolFindings.push({
|
|
203
|
+
check: 'stylelint',
|
|
204
|
+
file: toPosix(relative(process.cwd(), file.source)),
|
|
205
|
+
line: warning.line ?? null,
|
|
206
|
+
column: warning.column ?? null,
|
|
207
|
+
severity: warning.severity === 'error' ? 'error' : 'warning',
|
|
208
|
+
rule: warning.rule ?? null,
|
|
209
|
+
// Stylelint ends each message with its rule in brackets; the report has the rule apart
|
|
210
|
+
message: warning.text.replace(/\s*\([^()]+\)$/, ''),
|
|
211
|
+
fixable: false
|
|
212
|
+
})));
|
|
179
213
|
|
|
180
214
|
return result.results.reduce((sum, file) => sum + file.warnings.filter((w) => w.severity === 'error').length, 0);
|
|
181
215
|
}
|
|
182
216
|
|
|
217
|
+
// --- the JSON report --------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
// Every finding of the run, whether it failed or not: { version, errors, warnings, findings }, the
|
|
220
|
+
// files relative to the project root. Written whole, once the checks are done.
|
|
221
|
+
function writeReport(path, findings) {
|
|
222
|
+
const count = (severity) => findings.filter((finding) => finding.severity === severity).length;
|
|
223
|
+
const body = { version, errors: count('error'), warnings: count('warning'), findings };
|
|
224
|
+
try {
|
|
225
|
+
writeFileSync(resolve(path), `${ JSON.stringify(body, null, 2) }\n`);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
fail(`could not write the report to ${ path }: ${ error.message }`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
183
231
|
// --- run ------------------------------------------------------------------
|
|
184
232
|
|
|
185
233
|
// The checks run in a fixed order: c3b-lint's own, then ESLint, then Stylelint. Each tool's findings
|
|
@@ -213,11 +261,11 @@ async function main() {
|
|
|
213
261
|
};
|
|
214
262
|
|
|
215
263
|
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));
|
|
264
|
+
await timed('structure', () => checkStructure(report.as('structure')));
|
|
265
|
+
await timed('html', () => checkHtml(report.as('html')));
|
|
266
|
+
await timed('tokens', () => checkCssTokens(report.as('tokens')));
|
|
267
|
+
await timed('editorconfig', () => checkEditorconfig(report.as('editorconfig')));
|
|
268
|
+
await timed('lengths', () => checkFileLengths(report.as('lengths')));
|
|
221
269
|
printSection(name, report.format());
|
|
222
270
|
let errors = report.errors;
|
|
223
271
|
errors += await timed('eslint', () => runEslint());
|
|
@@ -225,6 +273,9 @@ async function main() {
|
|
|
225
273
|
|
|
226
274
|
console.log(`\n${ timings.join('\n') }`);
|
|
227
275
|
console.log(` ${ 'total'.padEnd(13) }${ Math.round(performance.now() - started) }ms`);
|
|
276
|
+
if (REPORT_FILE) {
|
|
277
|
+
writeReport(REPORT_FILE, [...report.findings(), ...toolFindings]);
|
|
278
|
+
}
|
|
228
279
|
if (errors > 0) {
|
|
229
280
|
fail(`${ errors } error(s)`);
|
|
230
281
|
}
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c3b-lint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
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",
|