c3b-lint 0.0.14 → 0.0.16

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
@@ -1,35 +1,35 @@
1
1
  # c3b-lint
2
2
 
3
3
  Lints a co:lab project against the rules in the [llm-instructions](https://codeberg.org/colab47/llm-instructions)
4
- markdown that a tool can check, with one command and one config to install. It runs:
4
+ markdown that a tool can check, with one command and one config to install. It runs, in this order:
5
5
 
6
- 1. **ESLint** on every `.js` file — classic browser scripts under `src/` (IIFE required, no top-level declarations,
7
- no `console.log`), ES modules everywhere else — with `eslint:recommended` plus the rules from `frontend.md`:
8
- braces on every `if`, `const`/`let` never `var`, `===`, a blank line before `return`, 500 lines max, and a
9
- `replace()`/`replaceAll()` whose replacement comes from data must pass a function (a string one expands `$&`).
10
- 2. **Stylelint** on `src/**/*.css` with `stylelint-config-recommended` plus the rules from `frontend.md`: no
11
- `!important`, max 3 compound selectors, kebab-case ids/classes with `is-*` states and `--` variants, `:root` only
12
- in `tokens.css` (and `reset.css`), token names `--{category}-{role}`, `rem` font sizes, `em` letter-spacing,
13
- never `dvh`, no styling off `data-*` (warning).
14
- 3. **Token hygiene** across all the CSS, which no per-file linter can do: a literal that duplicates a token's value
15
- (`#2b4b6f` where `--color-primary` is that), a literal colour, font-family or spacing value used in two or more
16
- rules (that is a token by definition), and a token nothing uses (CSS `var()`, or JS/HTML by name).
17
- 4. **HTML** on every page under `src/` (partials are skipped): doctype, `lang`, charset, viewport without
6
+ 1. **Project structure**: singular directory names, lowercase kebab-case asset names (and no camera exports), and,
7
+ when `src/` holds pages, CSS only in `src/style/` + `style/page/` + `style/animation/`, JS only in `src/script/`
8
+ (an experiment under `src/experiment/<name>/` keeps its own beside its `index.html`), fonts only in
9
+ `src/asset/font/` as woff2, file names starting with a letter. A directory whose plural-looking name is a proper
10
+ name (a client's brand) is listed in `package.json` under `"c3b-lint": { "singularDirectories": [...] }`.
11
+ 2. **HTML** on every page under `src/` (partials are skipped): doctype, `lang`, charset, viewport without
18
12
  `user-scalable=no`, the required head set (title, description, absolute canonical, `og:*` with an absolute
19
13
  `og:image`, `twitter:card`, one SVG favicon; a `noindex` page is excused the canonical and social tags), no external
20
- stylesheets (webfonts are self-hosted), stylesheet order (`reset`, `tokens`, `main`, …, page file last),
14
+ stylesheets (webfonts are self-hosted; a warning, not an error), stylesheet order (`reset`, `tokens`, `main`, …, page file last),
21
15
  the skip-link rule (`<a id="skip-link" href="#main">` first in `<body>` when there is a header or nav, absent
22
16
  otherwise, landing on `<main id="main">` — or on the id `<main>` already has), one `<h1>`, `alt` on every image, no inline handlers, no `<style>` or `style=""`
23
17
  (except a JS-driven custom property), relative paths only (a Barba site's links outside its container — header, menu — stay root-relative), kebab-case ids and classes, no duplicate ids,
24
18
  `integrity` on CDN scripts.
25
- 5. **Project structure**: singular directory names, lowercase kebab-case asset names (and no camera exports), and,
26
- when `src/` holds pages, CSS only in `src/style/` + `style/page/` + `style/animation/`, JS only in `src/script/`
27
- (an experiment under `src/experiment/<name>/` keeps its own beside its `index.html`), fonts only in
28
- `src/asset/font/` as woff2, file names starting with a letter. A directory whose plural-looking name is a proper
29
- name (a client's brand) is listed in `package.json` under `"c3b-lint": { "singularDirectories": [...] }`.
30
- 6. The **500-line limit** on `src/**/*.{css,html}` (ESLint reports the JS); inline `<svg>` art is not counted.
31
- 7. That the project's **`.editorconfig`** is the shared one — it carries the JetBrains `ij_*` code-style keys, so
19
+ 3. **Token hygiene** across all the CSS, which no per-file linter can do: a literal that duplicates a token's value
20
+ (`#2b4b6f` where `--color-primary` is that), a literal colour, font-family or spacing value used in two or more
21
+ rules (that is a token by definition), and a token nothing uses (CSS `var()`, or JS/HTML by name).
22
+ 4. That the project's **`.editorconfig`** is the shared one — it carries the JetBrains `ij_*` code-style keys, so
32
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.
25
+ 6. **ESLint** on every `.js` file — classic browser scripts under `src/` (IIFE required, no top-level declarations,
26
+ no `console.log`), ES modules everywhere else — with `eslint:recommended` plus the rules from `frontend.md`:
27
+ braces on every `if`, `const`/`let` never `var`, `===`, a blank line before `return`, 500 lines max, and a
28
+ `replace()`/`replaceAll()` whose replacement comes from data must pass a function (a string one expands `$&`).
29
+ 7. **Stylelint** on `src/**/*.css` with `stylelint-config-recommended` plus the rules from `frontend.md`: no
30
+ `!important`, max 3 compound selectors, kebab-case ids/classes with `is-*` states and `--` variants, `:root` only
31
+ in `tokens.css` (and `reset.css`), token names `--{category}-{role}`, `rem` font sizes, `em` letter-spacing,
32
+ never `dvh`, no styling off `data-*` (warning).
33
33
 
34
34
  Formatting itself (indent, quotes, spacing) is not linted: the `.editorconfig` and the IDE own it.
35
35
 
@@ -97,6 +97,10 @@ c3b-lint --help
97
97
 
98
98
  The version is printed first, so you can tell which install is running.
99
99
 
100
+ Findings print in sections, in the order they run — `--- c3b-lint ---` (checks 1–5), `--- eslint ---`,
101
+ `--- stylelint ---` — each grouped under the file's absolute path the way ESLint prints, with a `✖ N problems` total.
102
+ A section with no findings is not printed. The timings follow, then the error count.
103
+
100
104
  ## Not covered
101
105
 
102
106
  Rules that need judgement stay with review: whether two selectors are structurally disjoint, flex vs grid, whether
package/c3b-lint.js CHANGED
@@ -114,26 +114,19 @@ function writeBaseline() {
114
114
 
115
115
  // --- the checks -----------------------------------------------------------
116
116
 
117
- // Each check returns the number of errors it found, after printing them.
117
+ // c3b-lint's own checks add their findings to the shared report; ESLint and Stylelint print their
118
+ // own and return the number of errors they found.
118
119
 
119
- function checkEditorconfig() {
120
+ function checkEditorconfig(report) {
120
121
  if (!existsSync(EDITORCONFIG)) {
121
- console.log(` error .editorconfig is missing — run "${ name } --init"`);
122
-
123
- return 1;
124
- }
125
- if (readFileSync(EDITORCONFIG, 'utf8') !== BASELINE['.editorconfig']) {
126
- console.log(` error .editorconfig differs from the shared one — run "${ name } --init"`);
127
-
128
- return 1;
122
+ report.error('.editorconfig', `is missing — run "${ name } --init"`);
123
+ } else if (readFileSync(EDITORCONFIG, 'utf8') !== BASELINE['.editorconfig']) {
124
+ report.error('.editorconfig', `differs from the shared one — run "${ name } --init"`);
129
125
  }
130
-
131
- return 0;
132
126
  }
133
127
 
134
128
  // general.md: no hand-written file past 500 lines. ESLint reports JS; this covers CSS and HTML.
135
- function checkFileLengths() {
136
- let errors = 0;
129
+ function checkFileLengths(report) {
137
130
  const ignored = ignoredPaths();
138
131
  const files = globSync('src/**/*.{css,html}').map(toPosix).filter((file) => !file.includes('/vendor/') && !ignored.has(file));
139
132
  for (const file of files.sort()) {
@@ -141,12 +134,9 @@ function checkFileLengths() {
141
134
  const content = readFileSync(file, 'utf8').replace(/<svg[\s\S]*?<\/svg>/g, '<svg/>');
142
135
  const lines = content.split('\n').length - (content.endsWith('\n') ? 1 : 0);
143
136
  if (lines > MAX_LINES) {
144
- console.log(` error ${ toPosix(file) } is ${ lines } lines (limit ${ MAX_LINES }) — split it by responsibility`);
145
- errors++;
137
+ report.error(file, `is ${ lines } lines (limit ${ MAX_LINES }) — split it by responsibility`);
146
138
  }
147
139
  }
148
-
149
- return errors;
150
140
  }
151
141
 
152
142
  async function runEslint() {
@@ -165,10 +155,7 @@ async function runEslint() {
165
155
  await ESLint.outputFixes(results);
166
156
  }
167
157
  const formatter = await eslint.loadFormatter('stylish');
168
- const output = await formatter.format(results);
169
- if (output.trim()) {
170
- console.log(output);
171
- }
158
+ printSection('eslint', await formatter.format(results));
172
159
 
173
160
  return results.reduce((sum, result) => sum + result.errorCount, 0);
174
161
  }
@@ -187,16 +174,22 @@ async function runStylelint() {
187
174
  fix,
188
175
  formatter: 'string'
189
176
  });
190
- const output = result.report ?? result.output;
191
- if (output.trim()) {
192
- console.log(output);
193
- }
177
+ printSection('stylelint', result.report ?? result.output);
194
178
 
195
179
  return result.results.reduce((sum, file) => sum + file.warnings.filter((w) => w.severity === 'error').length, 0);
196
180
  }
197
181
 
198
182
  // --- run ------------------------------------------------------------------
199
183
 
184
+ // The checks run in a fixed order: c3b-lint's own, then ESLint, then Stylelint. Each tool's findings
185
+ // print under its name, so it's clear where one ends and the next begins.
186
+ function printSection(title, output) {
187
+ if (output.trim()) {
188
+ console.log(`\n--- ${ title } ---`);
189
+ console.log(output.trimEnd());
190
+ }
191
+ }
192
+
200
193
  async function main() {
201
194
  // --init only writes: it runs from postinstall, where a lint failure must not fail npm install
202
195
  if (init) {
@@ -222,13 +215,14 @@ async function main() {
222
215
  await timed('structure', () => checkStructure(report));
223
216
  await timed('html', () => checkHtml(report));
224
217
  await timed('tokens', () => checkCssTokens(report));
218
+ await timed('editorconfig', () => checkEditorconfig(report));
219
+ await timed('lengths', () => checkFileLengths(report));
220
+ printSection(name, report.format());
225
221
  let errors = report.errors;
226
- errors += await timed('editorconfig', () => checkEditorconfig());
227
- errors += await timed('lengths', () => checkFileLengths());
228
222
  errors += await timed('eslint', () => runEslint());
229
223
  errors += await timed('stylelint', () => runStylelint());
230
224
 
231
- console.log(timings.join('\n'));
225
+ console.log(`\n${ timings.join('\n') }`);
232
226
  console.log(` ${ 'total'.padEnd(13) }${ Math.round(performance.now() - started) }ms`);
233
227
  if (errors > 0) {
234
228
  fail(`${ errors } error(s)`);
package/check/html.js CHANGED
@@ -128,9 +128,11 @@ function checkPage(report, file, source) {
128
128
  // --- stylesheet link order: reset, tokens, main, components, page file last
129
129
  const allSheets = headEls.filter((el) => el.name === 'link' && attr(el, 'rel') === 'stylesheet')
130
130
  .map((el) => ({ el, href: attr(el, 'href') ?? '' }));
131
- // frontend.md: webfonts are self-hosted woff2 in src/asset/font/; no stylesheet comes from another origin
131
+ // frontend.md: webfonts are self-hosted woff2 in src/asset/font/; no stylesheet comes from another origin.
132
+ // A warning, not an error: a licensed font can come with a stylesheet that has to load from the foundry
133
+ // (Monotype's tracking CSS), and a font still waiting on its licence shouldn't block every commit.
132
134
  for (const sheet of allSheets.filter((s) => /^(https?:)?\/\//.test(s.href))) {
133
- report.error(at(sheet.el), `external stylesheet ${ sheet.href } — self-host it (fonts as woff2 in src/asset/font/)`);
135
+ report.warn(at(sheet.el), `external stylesheet ${ sheet.href } — self-host it (fonts as woff2 in src/asset/font/)`);
134
136
  }
135
137
  const sheets = allSheets.filter((s) => !/^(https?:)?\/\//.test(s.href));
136
138
  const names = sheets.map((s) => basename(s.href.split('?')[0]));
package/check/report.js CHANGED
@@ -1,18 +1,54 @@
1
- // One line per finding, the way every co:lab tool prints: a padded level, then the file and
2
- // message. Returns a counter so each check can hand back how many errors it printed.
1
+ import { resolve } from 'path';
2
+
3
+ // Findings are collected, then printed grouped under each file's absolute path the way ESLint's
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.
3
6
  export function createReport() {
7
+ const byFile = new Map();
4
8
  let errors = 0;
5
9
  let warnings = 0;
6
10
 
11
+ // Checks pass "file" or "file:line"
12
+ function add(where, level, message) {
13
+ const match = where.match(/^(.*):(\d+)$/);
14
+ const file = match ? match[1] : where;
15
+ const line = match ? match[2] : '';
16
+ if (!byFile.has(file)) {
17
+ byFile.set(file, []);
18
+ }
19
+ byFile.get(file).push({ line, level, message });
20
+ }
21
+
7
22
  return {
8
- error(file, message) {
9
- console.log(` error ${ file }: ${ message }`);
23
+ error(where, message) {
24
+ add(where, 'error', message);
10
25
  errors++;
11
26
  },
12
- warn(file, message) {
13
- console.log(` warn ${ file }: ${ message }`);
27
+ warn(where, message) {
28
+ add(where, 'warning', message);
14
29
  warnings++;
15
30
  },
31
+ // The whole report as one string, empty when nothing was found
32
+ format() {
33
+ let output = '';
34
+ for (const file of [...byFile.keys()].sort()) {
35
+ const findings = byFile.get(file).sort((a, b) => Number(a.line) - Number(b.line));
36
+ const lineWidth = Math.max(...findings.map((finding) => finding.line.length));
37
+ const levelWidth = Math.max(...findings.map((finding) => finding.level.length));
38
+ // resolve() drops a directory's trailing slash; it is kept so a directory reads as one
39
+ output += `\n${ resolve(file) }${ file.endsWith('/') ? '/' : '' }\n`;
40
+ for (const { line, level, message } of findings) {
41
+ output += ` ${ line.padStart(lineWidth) } ${ level.padEnd(levelWidth) } ${ message }\n`;
42
+ }
43
+ }
44
+ if (byFile.size > 0) {
45
+ const total = errors + warnings;
46
+ const plural = (count, word) => `${ count } ${ word }${ count === 1 ? '' : 's' }`;
47
+ output += `\n✖ ${ plural(total, 'problem') } (${ plural(errors, 'error') }, ${ plural(warnings, 'warning') })\n`;
48
+ }
49
+
50
+ return output;
51
+ },
16
52
  get errors() {
17
53
  return errors;
18
54
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c3b-lint",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
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",