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 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, like a desktop app's one renderer page.
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
- const content = readFileSync(file, 'utf8').replace(/<svg[\s\S]*?<\/svg>/g, '<svg/>');
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(where, message) {
24
- add(where, 'error', message);
25
- errors++;
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
- warn(where, message) {
28
- add(where, 'warning', message);
29
- warnings++;
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.18",
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",