c3b-lint 0.0.9 → 0.0.10

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
@@ -15,7 +15,8 @@ markdown that a tool can check, with one command and one config to install. It r
15
15
  rules (that is a token by definition), and a token nothing uses (CSS `var()`, or JS/HTML by name).
16
16
  4. **HTML** on every page under `src/` (partials are skipped): doctype, `lang`, charset, viewport without
17
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), no external stylesheets (webfonts are self-hosted), stylesheet order (`reset`, `tokens`, `main`, …, page file last),
18
+ `og:image`, `twitter:card`, one SVG favicon; a `noindex` page is excused the canonical and social tags), no external
19
+ stylesheets (webfonts are self-hosted), stylesheet order (`reset`, `tokens`, `main`, …, page file last),
19
20
  the skip-link rule (`<a id="skip-link" href="#main">` first in `<body>` when there is a header or nav, absent
20
21
  otherwise, `<main id="main">`), one `<h1>`, `alt` on every image, no inline handlers, no `<style>` or `style=""`
21
22
  (except a JS-driven custom property), relative paths only, kebab-case ids and classes, no duplicate ids,
@@ -46,7 +47,8 @@ npx c3b-lint --init
46
47
 
47
48
  - `.editorconfig` — the shared one (a copy; editors need a real file).
48
49
  - `eslint.config.js` and `stylelint.config.js` — one line each, re-exporting this package's configs, so WebStorm
49
- shows the same errors inline as the CLI reports. Bumping `c3b-lint` changes the rules everywhere.
50
+ shows the same errors inline as the CLI reports. Bumping `c3b-lint` changes the rules everywhere. Written only
51
+ when missing: a project that has extended its config keeps it.
50
52
  - `.githooks/pre-commit`, and `git config core.hooksPath .githooks` — the lint runs before every commit, so a
51
53
  commit that fails it never lands, whoever makes it. The hook lives in the repo, so after a clone one `--init`
52
54
  (or `npx c3b-lint --init` in `postinstall`) wires it up. If `node_modules` is missing it says so and lets the
package/c3b-lint.js CHANGED
@@ -37,6 +37,7 @@ function configFor(file) {
37
37
 
38
38
  return join(CONFIG_DIR, file);
39
39
  }
40
+
40
41
  const MAX_LINES = 500;
41
42
 
42
43
  // Files --init writes at the project root. The .editorconfig is a copy (it has to be a real
@@ -44,14 +45,14 @@ const MAX_LINES = 500;
44
45
  // c3b-lint changes the rules everywhere without touching projects.
45
46
  const BASELINE = {
46
47
  '.editorconfig': readFileSync(join(CONFIG_DIR, 'editorconfig'), 'utf8'),
47
- 'eslint.config.js': "export { default } from 'c3b-lint/config/eslint.config.js';\n",
48
- 'stylelint.config.js': "export { default } from 'c3b-lint/config/stylelint.config.js';\n",
48
+ 'eslint.config.js': 'export { default } from \'c3b-lint/config/eslint.config.js\';\n',
49
+ 'stylelint.config.js': 'export { default } from \'c3b-lint/config/stylelint.config.js\';\n'
49
50
  };
50
51
 
51
52
  const FLAGS = {
52
53
  '--fix': 'apply the ESLint and Stylelint fixes that are safe to apply automatically',
53
54
  '--init': 'write .editorconfig, eslint.config.js, stylelint.config.js and the pre-commit hook',
54
- '--help': 'print this help',
55
+ '--help': 'print this help'
55
56
  };
56
57
 
57
58
  function usage() {
@@ -96,6 +97,11 @@ function writeBaseline() {
96
97
  if (existsSync(path) && readFileSync(path, 'utf8') === content) {
97
98
  continue;
98
99
  }
100
+ // a lint config the project has extended (c10g's librarySrc, colab-client's exceptions) is its own:
101
+ // only the .editorconfig is kept in step with the shared one
102
+ if (file.endsWith('.js') && existsSync(path)) {
103
+ continue;
104
+ }
99
105
  // a project keeping a .mjs config (not "type": "module") has made its choice
100
106
  if (file.endsWith('.js') && existsSync(path.replace(/\.js$/, '.mjs'))) {
101
107
  continue;
@@ -177,7 +183,7 @@ async function runStylelint() {
177
183
  config,
178
184
  configBasedir: process.cwd(),
179
185
  fix,
180
- formatter: 'string',
186
+ formatter: 'string'
181
187
  });
182
188
  const output = result.report ?? result.output;
183
189
  if (output.trim()) {
@@ -199,11 +205,29 @@ async function main() {
199
205
  return;
200
206
  }
201
207
 
208
+ const started = performance.now();
209
+ const timings = [];
210
+ // Runs one check and records how long it took, so a slow run says where the time went
211
+ const timed = async (name, check) => {
212
+ const before = performance.now();
213
+ const result = await check();
214
+ timings.push(` ${ name.padEnd(13) }${ Math.round(performance.now() - before) }ms`);
215
+
216
+ return result;
217
+ };
218
+
202
219
  const report = createReport();
203
- checkStructure(report);
204
- checkHtml(report);
205
- checkCssTokens(report);
206
- const errors = checkEditorconfig() + checkFileLengths() + report.errors + await runEslint() + await runStylelint();
220
+ await timed('structure', () => checkStructure(report));
221
+ await timed('html', () => checkHtml(report));
222
+ await timed('tokens', () => checkCssTokens(report));
223
+ let errors = report.errors;
224
+ errors += await timed('editorconfig', () => checkEditorconfig());
225
+ errors += await timed('lengths', () => checkFileLengths());
226
+ errors += await timed('eslint', () => runEslint());
227
+ errors += await timed('stylelint', () => runStylelint());
228
+
229
+ console.log(timings.join('\n'));
230
+ console.log(` ${ 'total'.padEnd(13) }${ Math.round(performance.now() - started) }ms`);
207
231
  if (errors > 0) {
208
232
  fail(`${ errors } error(s)`);
209
233
  }
package/check/hook.js CHANGED
@@ -26,7 +26,10 @@ export function installHook(log) {
26
26
 
27
27
  if (existsSync('.git')) {
28
28
  try {
29
- const current = execFileSync('git', ['config', '--get', 'core.hooksPath'], { encoding: 'utf8', stdio: 'pipe' }).trim();
29
+ const current = execFileSync('git', ['config', '--get', 'core.hooksPath'], {
30
+ encoding: 'utf8',
31
+ stdio: 'pipe'
32
+ }).trim();
30
33
  if (current === '.githooks') {
31
34
  return;
32
35
  }
package/check/html.js CHANGED
@@ -18,7 +18,7 @@ const REQUIRED_META = [
18
18
  ['og:url', (el) => el.name === 'meta' && attr(el, 'property') === 'og:url'],
19
19
  ['og:image', (el) => el.name === 'meta' && attr(el, 'property') === 'og:image'],
20
20
  ['twitter:card', (el) => el.name === 'meta' && attr(el, 'name') === 'twitter:card'],
21
- ['favicon link[rel=icon]', (el) => el.name === 'link' && attr(el, 'rel') === 'icon'],
21
+ ['favicon link[rel=icon]', (el) => el.name === 'link' && attr(el, 'rel') === 'icon']
22
22
  ];
23
23
 
24
24
  const toPosix = (p) => p.split(sep).join('/');
@@ -44,7 +44,7 @@ const children = (el) => (el.childNodes ?? []).filter(isElement);
44
44
 
45
45
  export function checkHtml(report) {
46
46
  const files = globSync('src/**/*.html').map(toPosix)
47
- .filter((f) => !f.startsWith('src/partial/') && !f.includes('/vendor/')).sort();
47
+ .filter((f) => !f.startsWith('src/partial/') && !f.includes('/vendor/')).sort();
48
48
  for (const file of files) {
49
49
  checkPage(report, file, readFileSync(file, 'utf8'));
50
50
  }
@@ -112,7 +112,7 @@ function checkPage(report, file, source) {
112
112
 
113
113
  // --- stylesheet link order: reset, tokens, main, components, page file last
114
114
  const allSheets = headEls.filter((el) => el.name === 'link' && attr(el, 'rel') === 'stylesheet')
115
- .map((el) => ({ el, href: attr(el, 'href') ?? '' }));
115
+ .map((el) => ({ el, href: attr(el, 'href') ?? '' }));
116
116
  // frontend.md: webfonts are self-hosted woff2 in src/asset/font/; no stylesheet comes from another origin
117
117
  for (const sheet of allSheets.filter((s) => /^(https?:)?\/\//.test(s.href))) {
118
118
  report.error(at(sheet.el), `external stylesheet ${ sheet.href } — self-host it (fonts as woff2 in src/asset/font/)`);
package/check/report.js CHANGED
@@ -18,6 +18,6 @@ export function createReport() {
18
18
  },
19
19
  get warnings() {
20
20
  return warnings;
21
- },
21
+ }
22
22
  };
23
23
  }
@@ -31,6 +31,9 @@ ij_javascript_align_multiline_parameters = false
31
31
  ij_javascript_align_multiline_chained_methods = false
32
32
  ij_javascript_blank_lines_after_imports = 1
33
33
  ij_javascript_keep_blank_lines_in_code = 1
34
+ ij_javascript_enforce_trailing_comma = remove
35
+ ij_javascript_spaces_within_array_initializer_brackets = false
36
+ ij_javascript_space_before_function_left_parenth = false
34
37
 
35
38
  [*.java]
36
39
  ij_java_if_brace_force = always
@@ -23,6 +23,27 @@ const shared = {
23
23
  'padding-line-between-statements': ['error', { blankLine: 'always', prev: '*', next: 'return' }],
24
24
  // an unused catch binding is fine: `catch (error) { // reason }` is how we say the failure is expected
25
25
  'no-unused-vars': ['error', { caughtErrors: 'none' }],
26
+
27
+ // Formatting, matching the shared .editorconfig so the CLI enforces what WebStorm formats, and
28
+ // --fix can do it without an IDE. (ESLint 9 still ships these rules deprecated; when it drops
29
+ // them in 10, they move to @stylistic/eslint-plugin under the same names.)
30
+ // MemberExpression: 2 is the editorconfig's continuation indent of 8 for a chained call on its own line
31
+ 'indent': ['error', 4, { SwitchCase: 1, MemberExpression: 2 }],
32
+ 'quotes': ['error', 'single', { avoidEscape: true, allowTemplateLiterals: false }],
33
+ 'semi': ['error', 'always'],
34
+ 'template-curly-spacing': ['error', 'always'],
35
+ 'object-curly-spacing': ['error', 'always'],
36
+ 'array-bracket-spacing': ['error', 'never'],
37
+ 'comma-dangle': ['error', 'never'],
38
+ 'arrow-parens': ['error', 'always'],
39
+ 'space-before-function-paren': ['error', { anonymous: 'always', named: 'never', asyncArrow: 'always' }],
40
+ 'keyword-spacing': 'error',
41
+ 'space-infix-ops': 'error',
42
+ 'comma-spacing': 'error',
43
+ 'key-spacing': 'error',
44
+ 'no-trailing-spaces': 'error',
45
+ 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 0 }],
46
+ 'eol-last': 'error'
26
47
  };
27
48
 
28
49
  export const browser = {
@@ -30,7 +51,7 @@ export const browser = {
30
51
  languageOptions: {
31
52
  ecmaVersion: 2024,
32
53
  sourceType: 'script',
33
- globals: { ...globals.browser, HTMLCanvasElement: 'readonly', App: 'writable', ColabClient: 'readonly' },
54
+ globals: { ...globals.browser, HTMLCanvasElement: 'readonly', App: 'writable', ColabClient: 'readonly' }
34
55
  },
35
56
  rules: {
36
57
  ...js.configs.recommended.rules,
@@ -41,26 +62,26 @@ export const browser = {
41
62
  'error',
42
63
  {
43
64
  selector: 'Program > VariableDeclaration',
44
- message: 'Classic scripts share one global scope: wrap the file in an IIFE and attach its API to window.App.',
65
+ message: 'Classic scripts share one global scope: wrap the file in an IIFE and attach its API to window.App.'
45
66
  },
46
67
  {
47
68
  selector: 'Program > FunctionDeclaration',
48
- message: 'Classic scripts share one global scope: wrap the file in an IIFE and attach its API to window.App.',
49
- },
69
+ message: 'Classic scripts share one global scope: wrap the file in an IIFE and attach its API to window.App.'
70
+ }
50
71
  ],
51
72
  'no-restricted-properties': [
52
73
  'warn',
53
74
  {
54
75
  object: 'document',
55
76
  property: 'getElementsByClassName',
56
- message: 'Use getElementById for the landmark, then querySelectorAll scoped off it.',
57
- },
77
+ message: 'Use getElementById for the landmark, then querySelectorAll scoped off it.'
78
+ }
58
79
  ],
59
80
  'no-restricted-globals': [
60
81
  'warn',
61
- { name: 'setInterval', message: 'Use IntersectionObserver or requestAnimationFrame, not polling.' },
62
- ],
63
- },
82
+ { name: 'setInterval', message: 'Use IntersectionObserver or requestAnimationFrame, not polling.' }
83
+ ]
84
+ }
64
85
  };
65
86
 
66
87
  export const node = {
@@ -69,12 +90,12 @@ export const node = {
69
90
  languageOptions: {
70
91
  ecmaVersion: 2024,
71
92
  sourceType: 'module',
72
- globals: globals.node,
93
+ globals: globals.node
73
94
  },
74
95
  rules: {
75
96
  ...js.configs.recommended.rules,
76
- ...shared,
77
- },
97
+ ...shared
98
+ }
78
99
  };
79
100
 
80
101
  // For a Node library that keeps its modules in src/ (c10g): the Node rules there instead of the
@@ -89,12 +110,12 @@ export const librarySrc = {
89
110
  'no-console': 'off',
90
111
  'no-restricted-syntax': 'off',
91
112
  'no-restricted-properties': 'off',
92
- 'no-restricted-globals': 'off',
93
- },
113
+ 'no-restricted-globals': 'off'
114
+ }
94
115
  };
95
116
 
96
117
  export default [
97
118
  { ignores: ['node_modules/**', 'dist/**', 'c10g/**', 'src/**/vendor/**', '**/*.min.js'] },
98
119
  browser,
99
- node,
120
+ node
100
121
  ];
@@ -23,7 +23,7 @@ export default {
23
23
  'selector-id-pattern': [KEBAB, { message: 'ids are kebab-case' }],
24
24
  'selector-class-pattern': [CLASS, {
25
25
  resolveNestedSelectors: true,
26
- message: 'classes are kebab-case; state classes are is-*; a variant of a shared class uses --',
26
+ message: 'classes are kebab-case; state classes are is-*; a variant of a shared class uses --'
27
27
  }],
28
28
  // custom properties are kebab-case everywhere; the stricter token shape applies in tokens.css below
29
29
  'custom-property-pattern': ['^[a-z0-9]+(-[a-z0-9]+)*$', { message: 'custom properties are kebab-case' }],
@@ -33,33 +33,33 @@ export default {
33
33
  'unit-disallowed-list': [['dvh'], { message: 'dvh follows the phone toolbar; use vh, or svh for bottom-anchored content' }],
34
34
  'declaration-property-unit-allowed-list': {
35
35
  'font-size': ['rem', 'vw', '%'],
36
- 'letter-spacing': ['em'],
36
+ 'letter-spacing': ['em']
37
37
  },
38
38
  // frontend.md: don't style off data attributes; they exist for JS
39
39
  'selector-attribute-name-disallowed-list': [['/^data-/'], {
40
40
  severity: 'warning',
41
- message: 'data-* attributes are for JS; style with an id, class or structural selector',
42
- }],
41
+ message: 'data-* attributes are for JS; style with an id, class or structural selector'
42
+ }]
43
43
  },
44
44
  // Override globs are relative to the project root: c3b-lint sets configBasedir to it.
45
45
  overrides: [
46
46
  {
47
47
  files: ['src/style/tokens.css', 'src/style/reset.css'],
48
- rules: { 'selector-disallowed-list': null },
48
+ rules: { 'selector-disallowed-list': null }
49
49
  },
50
50
  {
51
51
  // a theme switch (:root[data-theme="light"] { --color-bg: … }) is the one place styling
52
52
  // hangs off a data attribute: JS sets it, and only tokens change
53
53
  files: ['src/style/tokens.css'],
54
- rules: { 'selector-attribute-name-disallowed-list': null },
54
+ rules: { 'selector-attribute-name-disallowed-list': null }
55
55
  },
56
56
  {
57
57
  files: ['src/style/tokens.css'],
58
58
  rules: {
59
59
  'custom-property-pattern': ['^[a-z][a-z0-9]*-[a-z0-9]+(-[a-z0-9]+)*$', {
60
- message: 'tokens are --{category}-{role}[-{variant}]: --color-text-muted, not --fg',
61
- }],
62
- },
63
- },
64
- ],
60
+ message: 'tokens are --{category}-{role}[-{variant}]: --color-text-muted, not --fg'
61
+ }]
62
+ }
63
+ }
64
+ ]
65
65
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c3b-lint",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
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",