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 +28 -21
- package/c3b-lint.js +26 -31
- package/check/report.js +42 -6
- package/config/eslint.config.js +5 -1
- package/config/project-settings.js +9 -8
- package/package.json +1 -1
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. **
|
|
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
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
|
-
|
|
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
|
|
|
@@ -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
|
-
//
|
|
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
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/config/eslint.config.js
CHANGED
|
@@ -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"]
|
|
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
|
|
30
|
-
// c3b-lint runs itself (ESLint and Stylelint are handed the globs directly)
|
|
31
|
-
export function ignoredPaths() {
|
|
32
|
-
|
|
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(
|
|
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.
|
|
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",
|