c3b-lint 0.0.5 → 0.0.7
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 +4 -2
- package/check/css-tokens.js +5 -2
- package/check/html.js +4 -2
- package/check/structure.js +23 -9
- package/config/eslint.config.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,8 +21,10 @@ markdown that a tool can check, with one command and one config to install. It r
|
|
|
21
21
|
(except a JS-driven custom property), relative paths only, kebab-case ids and classes, no duplicate ids,
|
|
22
22
|
`integrity` on CDN scripts.
|
|
23
23
|
5. **Project structure**: singular directory names, lowercase kebab-case asset names (and no camera exports), and,
|
|
24
|
-
when `src/` holds pages, CSS only in `src/style/` + `style/page/` + `style/animation/`, JS only in `src/script
|
|
25
|
-
|
|
24
|
+
when `src/` holds pages, CSS only in `src/style/` + `style/page/` + `style/animation/`, JS only in `src/script/`
|
|
25
|
+
(an experiment under `src/experiment/<name>/` keeps its own beside its `index.html`), fonts only in
|
|
26
|
+
`src/asset/font/` as woff2, file names starting with a letter. A directory whose plural-looking name is a proper
|
|
27
|
+
name (a client's brand) is listed in `package.json` under `"c3b-lint": { "singularDirectories": [...] }`.
|
|
26
28
|
6. The **500-line limit** on `src/**/*.{css,html}` (ESLint reports the JS).
|
|
27
29
|
7. That the project's **`.editorconfig`** is the shared one — it carries the JetBrains `ij_*` code-style keys, so
|
|
28
30
|
WebStorm and IntelliJ format the same way without committing `.idea/`.
|
package/check/css-tokens.js
CHANGED
|
@@ -84,7 +84,9 @@ export function checkCssTokens(report) {
|
|
|
84
84
|
// spacing property (a 24px icon is not --spacing-lg, a 16px font size is not --spacing-md)
|
|
85
85
|
const exact = normalise(value);
|
|
86
86
|
const lengthLike = /^-?\d/.test(exact);
|
|
87
|
-
|
|
87
|
+
// zero and keywords are not "the token's value" even when a token happens to hold them
|
|
88
|
+
const trivial = /^(0|0px|none|auto|inherit|initial|transparent|currentcolor)$/.test(exact);
|
|
89
|
+
if (byValue.has(exact) && !trivial && !decl.prop.startsWith('--') && (!lengthLike || SPACING_PROPERTIES.test(decl.prop))) {
|
|
88
90
|
report.error(`${ file }:${ where.line }`, `${ decl.value } duplicates ${ byValue.get(exact) } — use var(${ byValue.get(exact) })`);
|
|
89
91
|
}
|
|
90
92
|
|
|
@@ -96,7 +98,8 @@ export function checkCssTokens(report) {
|
|
|
96
98
|
add(colours, colour, where);
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
|
-
|
|
101
|
+
// a keyword (inherit, initial, unset, revert) is not a font literal
|
|
102
|
+
if (decl.prop === 'font-family' && !/var\(/.test(value) && !/^(inherit|initial|unset|revert|revert-layer)$/.test(exact)) {
|
|
100
103
|
add(fonts, exact, where);
|
|
101
104
|
}
|
|
102
105
|
if (SPACING_PROPERTIES.test(decl.prop) && !/var\(|calc\(|%|auto|^0$/.test(value)) {
|
package/check/html.js
CHANGED
|
@@ -118,9 +118,11 @@ function checkPage(report, file, source) {
|
|
|
118
118
|
if (names.slice(0, expectedStart.length).join() !== expectedStart.join()) {
|
|
119
119
|
report.error(at(sheets[0].el), `stylesheets start with ${ STYLE_ORDER.join(', ') } in that order`);
|
|
120
120
|
}
|
|
121
|
+
// page stylesheets (style/page/*.css, one or more) come after every shared one
|
|
121
122
|
const pageIndex = sheets.findIndex((s) => /\/page\//.test(s.href));
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
const sharedAfter = sheets.findIndex((s, i) => i > pageIndex && pageIndex !== -1 && !/\/page\//.test(s.href));
|
|
124
|
+
if (sharedAfter !== -1) {
|
|
125
|
+
report.error(at(sheets[sharedAfter].el), 'the page stylesheets are linked last');
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
// --- skip link and main
|
package/check/structure.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, globSync, readdirSync, statSync } from 'fs';
|
|
1
|
+
import { existsSync, globSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
2
2
|
import { basename, dirname, sep } from 'path';
|
|
3
3
|
|
|
4
4
|
// The project-structure rules from general.md and frontend.md: singular directory names, kebab-case
|
|
@@ -8,13 +8,27 @@ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'c10g', '.llm-rules',
|
|
|
8
8
|
// Words that end in s but are not plurals, or that are plural by convention outside our control
|
|
9
9
|
const SINGULAR_ENDING_IN_S = new Set(['css', 'js', 'canvas', 'status', 'news', 'gl', 'lens', 'bus', 'analysis', 'ios', 'macos', 'assets']);
|
|
10
10
|
const ASSET_NAME = /^[a-z0-9]+(-[a-z0-9]+)*(\.[a-z0-9]+)+$/;
|
|
11
|
-
const CSS_HOMES = [/^src\/style\/[a-z0-9-]+\.css$/, /^src\/style\/page\/[a-z][a-z0-9
|
|
12
|
-
|
|
11
|
+
const CSS_HOMES = [/^src\/style\/[a-z0-9-]+\.css$/, /^src\/style\/page\/[a-z][a-z0-9.-]*\.css$/, /^src\/style\/animation\/[a-z][a-z0-9.-]*\.css$/];
|
|
12
|
+
// an experiment (src/experiment/<name>/) is self-contained: its own JS and CSS sit beside its index.html
|
|
13
|
+
const JS_HOMES = [/^src\/script\/([a-z0-9-]+\/)*[a-z][a-z0-9.-]*\.js$/, /^src\/gl\//, /^src\/experiment\//];
|
|
14
|
+
const CSS_HOMES_EXTRA = [/^src\/experiment\//];
|
|
13
15
|
|
|
14
16
|
const toPosix = (p) => p.split(sep).join('/');
|
|
15
17
|
|
|
18
|
+
// A project names the plural-looking directories that are proper names (a client's brand, an
|
|
19
|
+
// experiment's title) in package.json: "c3b-lint": { "singularDirectories": ["mannequin-films"] }
|
|
20
|
+
function readAllowedDirectories() {
|
|
21
|
+
try {
|
|
22
|
+
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
|
23
|
+
|
|
24
|
+
return new Set(pkg['c3b-lint']?.singularDirectories ?? []);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return new Set();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
export function checkStructure(report) {
|
|
17
|
-
checkDirectoryNames(report, '.');
|
|
31
|
+
checkDirectoryNames(report, '.', readAllowedDirectories());
|
|
18
32
|
checkAssetNames(report);
|
|
19
33
|
checkFileHomes(report);
|
|
20
34
|
}
|
|
@@ -22,9 +36,9 @@ export function checkStructure(report) {
|
|
|
22
36
|
// Every directory, root included, is singular: component/ not components/. "assets" is in the
|
|
23
37
|
// exception list only because it is a common name for a dir that should be asset/ — it is reported
|
|
24
38
|
// separately below, with the rename.
|
|
25
|
-
function checkDirectoryNames(report, dir) {
|
|
39
|
+
function checkDirectoryNames(report, dir, allowed) {
|
|
26
40
|
for (const entry of readdirSync(dir)) {
|
|
27
|
-
if (SKIP_DIRS.has(entry) || entry.startsWith('.')) {
|
|
41
|
+
if (SKIP_DIRS.has(entry) || entry.startsWith('.') || allowed.has(entry)) {
|
|
28
42
|
continue;
|
|
29
43
|
}
|
|
30
44
|
const path = dir === '.' ? entry : `${ dir }/${ entry }`;
|
|
@@ -34,9 +48,9 @@ function checkDirectoryNames(report, dir) {
|
|
|
34
48
|
if (entry === 'assets' || entry === 'images' || entry === 'scripts' || entry === 'styles') {
|
|
35
49
|
report.error(path + '/', `directories are singular — rename to ${ entry.slice(0, -1) }/`);
|
|
36
50
|
} else if (/[a-z]s$/.test(entry) && !/ss$/.test(entry) && !SINGULAR_ENDING_IN_S.has(entry)) {
|
|
37
|
-
report.error(path + '/', `directories are singular — rename to ${ entry.slice(0, -1) }/ (
|
|
51
|
+
report.error(path + '/', `directories are singular — rename to ${ entry.slice(0, -1) }/ (a proper name goes in package.json "c3b-lint".singularDirectories)`);
|
|
38
52
|
}
|
|
39
|
-
checkDirectoryNames(report, path);
|
|
53
|
+
checkDirectoryNames(report, path, allowed);
|
|
40
54
|
}
|
|
41
55
|
}
|
|
42
56
|
|
|
@@ -73,7 +87,7 @@ function checkFileHomes(report) {
|
|
|
73
87
|
if (/^\d/.test(basename(file))) {
|
|
74
88
|
report.error(file, 'file names start with a letter, never a digit');
|
|
75
89
|
}
|
|
76
|
-
if (file.endsWith('.css') && !CSS_HOMES.some((home) => home.test(file))) {
|
|
90
|
+
if (file.endsWith('.css') && ![...CSS_HOMES, ...CSS_HOMES_EXTRA].some((home) => home.test(file))) {
|
|
77
91
|
report.error(file, 'CSS lives in src/style/ (shared), src/style/page/ (one per page) or src/style/animation/');
|
|
78
92
|
} else if (file.endsWith('.js') && !JS_HOMES.some((home) => home.test(file))) {
|
|
79
93
|
report.error(file, 'JS lives in src/script/');
|
package/config/eslint.config.js
CHANGED
|
@@ -30,7 +30,7 @@ export const browser = {
|
|
|
30
30
|
languageOptions: {
|
|
31
31
|
ecmaVersion: 2024,
|
|
32
32
|
sourceType: 'script',
|
|
33
|
-
globals: { ...globals.browser, App: 'writable', ColabClient: 'readonly' },
|
|
33
|
+
globals: { ...globals.browser, HTMLCanvasElement: 'readonly', App: 'writable', ColabClient: 'readonly' },
|
|
34
34
|
},
|
|
35
35
|
rules: {
|
|
36
36
|
...js.configs.recommended.rules,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c3b-lint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
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",
|