c3b-lint 0.0.15 → 0.0.17

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,20 +1,14 @@
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
14
  stylesheets (webfonts are self-hosted; a warning, not an error), stylesheet order (`reset`, `tokens`, `main`, …, page file last),
@@ -22,14 +16,20 @@ markdown that a tool can check, with one command and one config to install. It r
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
 
@@ -74,7 +74,8 @@ A project's own settings go in its `package.json`, under `"c3b-lint"`:
74
74
  "c3b-lint": {
75
75
  "ignore": ["src/experiment/**"],
76
76
  "globals": ["THREE", "gsap", "ScrollTrigger"],
77
- "singularDirectories": ["mannequin-films"]
77
+ "singularDirectories": ["mannequin-films"],
78
+ "ignoreLength": ["src/index.html"]
78
79
  }
79
80
  ```
80
81
 
@@ -83,6 +84,8 @@ A project's own settings go in its `package.json`, under `"c3b-lint"`:
83
84
  - `globals` — the names a page's CDN `<script>` tags define (`THREE`, `gsap`, `Lenis`, …). ESLint lints each file on
84
85
  its own and never reads the HTML, so without them every use is "not defined". Read-only, `src/` scripts only.
85
86
  - `singularDirectories` — plural-looking directory names that are proper names (a client's brand).
87
+ - `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.
86
89
 
87
90
  The ESLint and Stylelint configs read these too, so WebStorm reports what the CLI does.
88
91
 
@@ -97,6 +100,10 @@ c3b-lint --help
97
100
 
98
101
  The version is printed first, so you can tell which install is running.
99
102
 
103
+ Findings print in sections, in the order they run — `--- c3b-lint ---` (checks 1–5), `--- eslint ---`,
104
+ `--- stylelint ---` — each grouped under the file's absolute path the way ESLint prints, with a `✖ N problems` total.
105
+ A section with no findings is not printed. The timings follow, then the error count.
106
+
100
107
  ## Not covered
101
108
 
102
109
  Rules that need judgement stay with review: whether two selectors are structurally disjoint, flex vs grid, whether
package/c3b-lint.js CHANGED
@@ -9,7 +9,7 @@ import { checkCssTokens } from './check/css-tokens.js';
9
9
  import { checkHtml } from './check/html.js';
10
10
  import { checkStructure } from './check/structure.js';
11
11
  import { installHook } from './check/hook.js';
12
- import { ignoredPaths } from './config/project-settings.js';
12
+ import { ignoredPaths, readSettings } from './config/project-settings.js';
13
13
 
14
14
  // Printed first so it's obvious which installed version is running.
15
15
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -114,39 +114,30 @@ 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
- const files = globSync('src/**/*.{css,html}').map(toPosix).filter((file) => !file.includes('/vendor/') && !ignored.has(file));
131
+ const exempt = ignoredPaths(readSettings().ignoreLength);
132
+ const files = globSync('src/**/*.{css,html}').map(toPosix).filter((file) => !file.includes('/vendor/') && !ignored.has(file) && !exempt.has(file));
139
133
  for (const file of files.sort()) {
140
134
  // inline <svg> is vector art, not hand-written markup: its lines are not counted
141
135
  const content = readFileSync(file, 'utf8').replace(/<svg[\s\S]*?<\/svg>/g, '<svg/>');
142
136
  const lines = content.split('\n').length - (content.endsWith('\n') ? 1 : 0);
143
137
  if (lines > MAX_LINES) {
144
- console.log(` error ${ toPosix(file) } is ${ lines } lines (limit ${ MAX_LINES }) — split it by responsibility`);
145
- errors++;
138
+ report.error(file, `is ${ lines } lines (limit ${ MAX_LINES }) — split it by responsibility`);
146
139
  }
147
140
  }
148
-
149
- return errors;
150
141
  }
151
142
 
152
143
  async function runEslint() {
@@ -165,10 +156,7 @@ async function runEslint() {
165
156
  await ESLint.outputFixes(results);
166
157
  }
167
158
  const formatter = await eslint.loadFormatter('stylish');
168
- const output = await formatter.format(results);
169
- if (output.trim()) {
170
- console.log(output);
171
- }
159
+ printSection('eslint', await formatter.format(results));
172
160
 
173
161
  return results.reduce((sum, result) => sum + result.errorCount, 0);
174
162
  }
@@ -187,16 +175,22 @@ async function runStylelint() {
187
175
  fix,
188
176
  formatter: 'string'
189
177
  });
190
- const output = result.report ?? result.output;
191
- if (output.trim()) {
192
- console.log(output);
193
- }
178
+ printSection('stylelint', result.report ?? result.output);
194
179
 
195
180
  return result.results.reduce((sum, file) => sum + file.warnings.filter((w) => w.severity === 'error').length, 0);
196
181
  }
197
182
 
198
183
  // --- run ------------------------------------------------------------------
199
184
 
185
+ // The checks run in a fixed order: c3b-lint's own, then ESLint, then Stylelint. Each tool's findings
186
+ // print under its name, so it's clear where one ends and the next begins.
187
+ function printSection(title, output) {
188
+ if (output.trim()) {
189
+ console.log(`\n--- ${ title } ---`);
190
+ console.log(output.trimEnd());
191
+ }
192
+ }
193
+
200
194
  async function main() {
201
195
  // --init only writes: it runs from postinstall, where a lint failure must not fail npm install
202
196
  if (init) {
@@ -222,13 +216,14 @@ async function main() {
222
216
  await timed('structure', () => checkStructure(report));
223
217
  await timed('html', () => checkHtml(report));
224
218
  await timed('tokens', () => checkCssTokens(report));
219
+ await timed('editorconfig', () => checkEditorconfig(report));
220
+ await timed('lengths', () => checkFileLengths(report));
221
+ printSection(name, report.format());
225
222
  let errors = report.errors;
226
- errors += await timed('editorconfig', () => checkEditorconfig());
227
- errors += await timed('lengths', () => checkFileLengths());
228
223
  errors += await timed('eslint', () => runEslint());
229
224
  errors += await timed('stylelint', () => runStylelint());
230
225
 
231
- console.log(timings.join('\n'));
226
+ console.log(`\n${ timings.join('\n') }`);
232
227
  console.log(` ${ 'total'.padEnd(13) }${ Math.round(performance.now() - started) }ms`);
233
228
  if (errors > 0) {
234
229
  fail(`${ errors } error(s)`);
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
  },
@@ -130,8 +130,12 @@ 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
+ const lengthExempt = settings.ignoreLength.length ? [{ files: settings.ignoreLength, rules: { 'max-lines': 'off' } }] : [];
135
+
133
136
  export default [
134
137
  { ignores: ['node_modules/**', 'dist/**', 'c10g/**', 'src/**/vendor/**', '**/*.min.js', ...settings.ignore] },
135
138
  browser,
136
- node
139
+ node,
140
+ ...lengthExempt
137
141
  ];
@@ -7,7 +7,8 @@ import { resolve, sep } from 'path';
7
7
  // "c3b-lint": {
8
8
  // "ignore": ["src/experiment/**"], globs no check looks at: ESLint, Stylelint and c3b-lint's own
9
9
  // "globals": ["THREE", "gsap"], names the pages' CDN <script> tags define, read-only in src/ scripts
10
- // "singularDirectories": ["mannequin-films"] plural-looking directory names that are proper names
10
+ // "singularDirectories": ["mannequin-films"], plural-looking directory names that are proper names
11
+ // "ignoreLength": ["src/index.html"] globs exempt from the 500-line limit only; every other check still runs
11
12
  // }
12
13
  //
13
14
  // The ESLint and Stylelint configs read it too, so the editor reports what the CLI does.
@@ -22,17 +23,17 @@ export function readSettings() {
22
23
  return {
23
24
  ignore: settings.ignore ?? [],
24
25
  globals: settings.globals ?? [],
25
- singularDirectories: settings.singularDirectories ?? []
26
+ singularDirectories: settings.singularDirectories ?? [],
27
+ ignoreLength: settings.ignoreLength ?? []
26
28
  };
27
29
  }
28
30
 
29
- // Every file and directory the "ignore" globs match, as posix paths from the project root, for the checks
30
- // c3b-lint runs itself (ESLint and Stylelint are handed the globs directly)
31
- export function ignoredPaths() {
32
- const { ignore } = readSettings();
33
- if (ignore.length === 0) {
31
+ // Every file and directory the "ignore" globs (or the globs given) match, as posix paths from the project
32
+ // root, for the checks c3b-lint runs itself (ESLint and Stylelint are handed the globs directly)
33
+ export function ignoredPaths(globs = readSettings().ignore) {
34
+ if (globs.length === 0) {
34
35
  return new Set();
35
36
  }
36
37
 
37
- return new Set(globSync(ignore).map((path) => path.split(sep).join('/')));
38
+ return new Set(globSync(globs).map((path) => path.split(sep).join('/')));
38
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c3b-lint",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
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",