c3b-lint 0.0.1 → 0.0.2
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 +30 -9
- package/c3b-lint.js +4 -0
- package/check/css-tokens.js +7 -2
- package/check/html.js +6 -0
- package/config/eslint.config.js +5 -1
- package/config/stylelint.config.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,8 +10,21 @@ markdown that a tool can check, with one command and one config to install. It r
|
|
|
10
10
|
`!important`, max 3 compound selectors, kebab-case ids/classes with `is-*` states and `--` variants, `:root` only
|
|
11
11
|
in `tokens.css` (and `reset.css`), token names `--{category}-{role}`, `rem` font sizes, `em` letter-spacing,
|
|
12
12
|
never `dvh`, no styling off `data-*` (warning).
|
|
13
|
-
3.
|
|
14
|
-
|
|
13
|
+
3. **Token hygiene** across all the CSS, which no per-file linter can do: a literal that duplicates a token's value
|
|
14
|
+
(`#2b4b6f` where `--color-primary` is that), a literal colour, font-family or spacing value used in two or more
|
|
15
|
+
rules (that is a token by definition), and a token nothing uses (CSS `var()`, or JS/HTML by name).
|
|
16
|
+
4. **HTML** on every page under `src/` (partials are skipped): doctype, `lang`, charset, viewport without
|
|
17
|
+
`user-scalable=no`, the required head set (title, description, absolute canonical, `og:*` with an absolute
|
|
18
|
+
`og:image`, `twitter:card`, one SVG favicon; a `noindex` page is excused the canonical and social tags), stylesheet order (`reset`, `tokens`, `main`, …, page file last),
|
|
19
|
+
the skip-link rule (`<a id="skip-link" href="#main">` first in `<body>` when there is a header or nav, absent
|
|
20
|
+
otherwise, `<main id="main">`), one `<h1>`, `alt` on every image, no inline handlers, no `<style>` or `style=""`
|
|
21
|
+
(except a JS-driven custom property), relative paths only, kebab-case ids and classes, no duplicate ids,
|
|
22
|
+
`integrity` on CDN scripts.
|
|
23
|
+
5. **Project structure**: singular directory names, lowercase kebab-case asset names (and no camera exports), CSS
|
|
24
|
+
only in `src/style/` + `style/page/` + `style/animation/`, JS only in `src/script/`, fonts only in
|
|
25
|
+
`src/asset/font/` as woff2, file names starting with a letter.
|
|
26
|
+
6. The **500-line limit** on `src/**/*.{css,html}` (ESLint reports the JS).
|
|
27
|
+
7. That the project's **`.editorconfig`** is the shared one — it carries the JetBrains `ij_*` code-style keys, so
|
|
15
28
|
WebStorm and IntelliJ format the same way without committing `.idea/`.
|
|
16
29
|
|
|
17
30
|
Formatting itself (indent, quotes, spacing) is not linted: the `.editorconfig` and the IDE own it.
|
|
@@ -23,27 +36,33 @@ npm install --save-dev c3b-lint
|
|
|
23
36
|
npx c3b-lint --init
|
|
24
37
|
```
|
|
25
38
|
|
|
26
|
-
`--init` writes
|
|
39
|
+
`--init` writes four files at the project root, all to commit:
|
|
27
40
|
|
|
28
41
|
- `.editorconfig` — the shared one (a copy; editors need a real file).
|
|
29
42
|
- `eslint.config.js` and `stylelint.config.js` — one line each, re-exporting this package's configs, so WebStorm
|
|
30
43
|
shows the same errors inline as the CLI reports. Bumping `c3b-lint` changes the rules everywhere.
|
|
44
|
+
- `.githooks/pre-commit`, and `git config core.hooksPath .githooks` — the lint runs before every commit, so a
|
|
45
|
+
commit that fails it never lands, whoever makes it. The hook lives in the repo, so after a clone one `--init`
|
|
46
|
+
(or `npx c3b-lint --init` in `postinstall`) wires it up. If `node_modules` is missing it says so and lets the
|
|
47
|
+
commit through; the test gate catches it later.
|
|
31
48
|
|
|
32
49
|
Then in `package.json`:
|
|
33
50
|
|
|
34
51
|
```json
|
|
52
|
+
"postinstall": "c3b-lint --init",
|
|
35
53
|
"lint": "c3b-lint",
|
|
36
54
|
"pretest": "colab-partial --check && c3b-lint"
|
|
37
55
|
```
|
|
38
56
|
|
|
39
|
-
|
|
57
|
+
`postinstall` keeps the four files current and wires the hook on every clone with no extra step (`--init` only
|
|
58
|
+
writes, it never fails an install); `pretest` makes a lint error fail `npm test` and anything that runs it.
|
|
40
59
|
|
|
41
60
|
## Usage
|
|
42
61
|
|
|
43
62
|
```
|
|
44
63
|
c3b-lint # lint; exits 1 on any error
|
|
45
|
-
c3b-lint --fix # apply the fixes ESLint and Stylelint consider safe (var -> const, braces, …)
|
|
46
|
-
c3b-lint --init # (re)write .editorconfig, eslint.config.js, stylelint.config.js
|
|
64
|
+
c3b-lint --fix # apply the fixes ESLint and Stylelint consider safe (var -> const, braces, …); reformat in the IDE after
|
|
65
|
+
c3b-lint --init # (re)write .editorconfig, eslint.config.js, stylelint.config.js, the pre-commit hook
|
|
47
66
|
c3b-lint --help
|
|
48
67
|
```
|
|
49
68
|
|
|
@@ -51,7 +70,9 @@ The version is printed first, so you can tell which install is running.
|
|
|
51
70
|
|
|
52
71
|
## Not covered
|
|
53
72
|
|
|
54
|
-
Rules that need judgement stay with review:
|
|
55
|
-
|
|
56
|
-
reason.
|
|
73
|
+
Rules that need judgement stay with review: whether two selectors are structurally disjoint, flex vs grid, whether
|
|
74
|
+
a one-off literal should be a token anyway, where a component style belongs. `no-descending-specificity` is off for
|
|
75
|
+
the same reason. The checks that need a browser (console errors, broken links and images, accessibility, overflow)
|
|
76
|
+
stay in [c10g](https://codeberg.org/colab47/c10g); its SEO and duplicate-id suites now overlap with the HTML check
|
|
77
|
+
here. Java projects use [colab-checkstyle](https://codeberg.org/colab47/colab-checkstyle) and the same
|
|
57
78
|
`.editorconfig`.
|
package/c3b-lint.js
CHANGED
|
@@ -176,9 +176,13 @@ async function runStylelint() {
|
|
|
176
176
|
// --- run ------------------------------------------------------------------
|
|
177
177
|
|
|
178
178
|
async function main() {
|
|
179
|
+
// --init only writes: it runs from postinstall, where a lint failure must not fail npm install
|
|
179
180
|
if (init) {
|
|
180
181
|
writeBaseline();
|
|
181
182
|
installHook((what) => console.log(` write ${ what }`));
|
|
183
|
+
console.log('Baseline written. Run c3b-lint to lint.');
|
|
184
|
+
|
|
185
|
+
return;
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
const report = createReport();
|
package/check/css-tokens.js
CHANGED
|
@@ -75,11 +75,16 @@ export function checkCssTokens(report) {
|
|
|
75
75
|
if (file === TOKENS || decl.parent.type === 'atrule' && decl.parent.name === 'font-face') {
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
// the steps of one @keyframes are one rule: the same colour in 20%, 40% and 60% is one animation
|
|
79
|
+
const keyframes = decl.parent.parent?.type === 'atrule' && decl.parent.parent.name === 'keyframes';
|
|
80
|
+
const where = { file, line: decl.source.start.line, rule: keyframes ? decl.parent.parent : decl.parent };
|
|
79
81
|
const value = decl.value.replace(/url\([^)]*\)/g, '');
|
|
80
82
|
|
|
83
|
+
// A colour or font literal duplicates a token wherever it appears; a length only does on a
|
|
84
|
+
// spacing property (a 24px icon is not --spacing-lg, a 16px font size is not --spacing-md)
|
|
81
85
|
const exact = normalise(value);
|
|
82
|
-
|
|
86
|
+
const lengthLike = /^-?\d/.test(exact);
|
|
87
|
+
if (byValue.has(exact) && !decl.prop.startsWith('--') && (!lengthLike || SPACING_PROPERTIES.test(decl.prop))) {
|
|
83
88
|
report.error(`${ file }:${ where.line }`, `${ decl.value } duplicates ${ byValue.get(exact) } — use var(${ byValue.get(exact) })`);
|
|
84
89
|
}
|
|
85
90
|
|
package/check/html.js
CHANGED
|
@@ -81,7 +81,13 @@ function checkPage(report, file, source) {
|
|
|
81
81
|
if (!headEls.some((el) => el.name === 'title')) {
|
|
82
82
|
report.error(file, 'missing <title>');
|
|
83
83
|
}
|
|
84
|
+
// an internal tool (noindex) is never shared or indexed, so the social and canonical tags are
|
|
85
|
+
// not required there; the description and favicon still are
|
|
86
|
+
const noindex = headEls.some((el) => el.name === 'meta' && attr(el, 'name') === 'robots' && /noindex/.test(attr(el, 'content') ?? ''));
|
|
84
87
|
for (const [label, matches] of REQUIRED_META) {
|
|
88
|
+
if (noindex && /canonical|og:|twitter:/.test(label)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
85
91
|
if (!headEls.some(matches)) {
|
|
86
92
|
report.error(file, `missing ${ label }`);
|
|
87
93
|
}
|
package/config/eslint.config.js
CHANGED
|
@@ -15,10 +15,14 @@ const shared = {
|
|
|
15
15
|
'max-lines': ['error', { max: MAX_LINES, skipBlankLines: false, skipComments: false }],
|
|
16
16
|
// frontend.md: brackets on every if; const by default, let when reassigned, never var; blank line before return
|
|
17
17
|
'curly': ['error', 'all'],
|
|
18
|
+
// and the block's body on its own line: `if (x) {y();}` is what curly's fix produces on its own
|
|
19
|
+
'brace-style': ['error', '1tbs', { allowSingleLine: false }],
|
|
18
20
|
'no-var': 'error',
|
|
19
21
|
'prefer-const': 'error',
|
|
20
22
|
'eqeqeq': ['error', 'always'],
|
|
21
23
|
'padding-line-between-statements': ['error', { blankLine: 'always', prev: '*', next: 'return' }],
|
|
24
|
+
// an unused catch binding is fine: `catch (error) { // reason }` is how we say the failure is expected
|
|
25
|
+
'no-unused-vars': ['error', { caughtErrors: 'none' }],
|
|
22
26
|
};
|
|
23
27
|
|
|
24
28
|
export const browser = {
|
|
@@ -26,7 +30,7 @@ export const browser = {
|
|
|
26
30
|
languageOptions: {
|
|
27
31
|
ecmaVersion: 2024,
|
|
28
32
|
sourceType: 'script',
|
|
29
|
-
globals: { ...globals.browser, App: 'writable' },
|
|
33
|
+
globals: { ...globals.browser, App: 'writable', ColabClient: 'readonly' },
|
|
30
34
|
},
|
|
31
35
|
rules: {
|
|
32
36
|
...js.configs.recommended.rules,
|
|
@@ -47,6 +47,12 @@ export default {
|
|
|
47
47
|
files: ['src/style/tokens.css', 'src/style/reset.css'],
|
|
48
48
|
rules: { 'selector-disallowed-list': null },
|
|
49
49
|
},
|
|
50
|
+
{
|
|
51
|
+
// a theme switch (:root[data-theme="light"] { --color-bg: … }) is the one place styling
|
|
52
|
+
// hangs off a data attribute: JS sets it, and only tokens change
|
|
53
|
+
files: ['src/style/tokens.css'],
|
|
54
|
+
rules: { 'selector-attribute-name-disallowed-list': null },
|
|
55
|
+
},
|
|
50
56
|
{
|
|
51
57
|
files: ['src/style/tokens.css'],
|
|
52
58
|
rules: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c3b-lint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
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",
|