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 +25 -21
- package/c3b-lint.js +23 -29
- package/check/html.js +4 -2
- package/check/report.js +42 -6
- package/package.json +1 -1
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. **
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
(
|
|
28
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
2
|
-
|
|
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(
|
|
9
|
-
|
|
23
|
+
error(where, message) {
|
|
24
|
+
add(where, 'error', message);
|
|
10
25
|
errors++;
|
|
11
26
|
},
|
|
12
|
-
warn(
|
|
13
|
-
|
|
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.
|
|
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",
|