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 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. The **500-line limit** on `src/**/*.{css,html}` (ESLint reports the JS).
14
- 4. That the project's **`.editorconfig`** is the shared one — it carries the JetBrains `ij_*` code-style keys, so
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 three files at the project root, all to commit:
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
- so a lint error fails `npm test` and anything that runs it (`predeploy`).
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: when a literal should become a token, whether two selectors are
55
- structurally disjoint, flex vs grid, where a page style belongs. `no-descending-specificity` is off for the same
56
- reason. Java projects use [colab-checkstyle](https://codeberg.org/colab47/colab-checkstyle) and the same
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();
@@ -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
- const where = { file, line: decl.source.start.line, rule: decl.parent };
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
- if (byValue.has(exact) && !decl.prop.startsWith('--')) {
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
  }
@@ -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.1",
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",