@avi2dg/checks 0.47.0 → 0.48.0
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/CHANGELOG.md +8 -0
- package/README.md +19 -0
- package/dist/presets/stylelint.d.ts +4 -1
- package/dist/presets/stylelint.js +38 -2
- package/docs/configs/native-settings.md +2 -2
- package/docs/gates/checks-frontend-syntax.md +8 -1
- package/package.json +2 -1
- package/src/quality/presets/stylelint.ts +53 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Every release of `@avi2dg/checks`, newest first, written by the release from its conventional commits.
|
|
4
4
|
|
|
5
|
+
## 0.48.0
|
|
6
|
+
|
|
7
|
+
Released 2026-10-19.
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **quality:** add opt-in BEM, flat selector and page :has stylelint options [#179](https://github.com/avi2d/checks/pull/179)
|
|
12
|
+
|
|
5
13
|
## 0.47.0
|
|
6
14
|
|
|
7
15
|
Released 2026-10-19.
|
package/README.md
CHANGED
|
@@ -223,6 +223,25 @@ To move a repository to a newer release of the kit:
|
|
|
223
223
|
The repository's lockfile pins the kit, so a repository moves only when it runs these steps.
|
|
224
224
|
[CHANGELOG.md](CHANGELOG.md), shipped in the package, lists what each release changed.
|
|
225
225
|
|
|
226
|
+
## Turn on the CSS families a repository needs
|
|
227
|
+
|
|
228
|
+
A repository that declares `frontend-syntax.json` writes `stylelint.config.ts` with the families it needs:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
import { defineConfig } from "@avi2dg/checks/stylelint";
|
|
232
|
+
|
|
233
|
+
export default defineConfig({
|
|
234
|
+
baseline: "widely",
|
|
235
|
+
tokens: ["src/styles/tokens.css"],
|
|
236
|
+
bem: true,
|
|
237
|
+
flatSelectors: true,
|
|
238
|
+
noPageHas: true,
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
[checks-frontend-syntax](docs/gates/checks-frontend-syntax.md#what-it-reads) says what each key takes and which family it turns on.
|
|
243
|
+
`noPageHas` sees a `:has` nested under a page selector only together with `flatSelectors`, which refuses the nesting.
|
|
244
|
+
|
|
226
245
|
## Where things are
|
|
227
246
|
|
|
228
247
|
Every path is relative to the installed package, `node_modules/@avi2dg/checks/`.
|
|
@@ -4,7 +4,10 @@ export type BaselineTarget = "widely" | "newly" | number;
|
|
|
4
4
|
export type ChecksStylelintConfig = Omit<Config, "extends" | "plugins"> & {
|
|
5
5
|
readonly baseline: BaselineTarget;
|
|
6
6
|
readonly tokens: Globs | false;
|
|
7
|
+
readonly bem?: boolean;
|
|
8
|
+
readonly flatSelectors?: boolean;
|
|
9
|
+
readonly noPageHas?: boolean;
|
|
7
10
|
};
|
|
8
11
|
export declare const MARKUP: readonly ["**/*.html", "**/*.astro", "**/*.vue", "**/*.svelte"];
|
|
9
|
-
export declare function defineConfig({ baseline, tokens, rules, overrides, ...rest }: ChecksStylelintConfig): Config;
|
|
12
|
+
export declare function defineConfig({ baseline, tokens, bem, flatSelectors, noPageHas, rules, overrides, ...rest }: ChecksStylelintConfig): Config;
|
|
10
13
|
export {};
|
|
@@ -20,6 +20,35 @@ var LANGUAGE_FREE_SELECTORS = {
|
|
|
20
20
|
"selector-disallowed-list": ["/[^\\x00-\\x7F]/"]
|
|
21
21
|
};
|
|
22
22
|
var COLOUR_FUNCTIONS = ["rgb", "rgba", "hsl", "hsla", "hwb", "lab", "lch", "oklab", "oklch", "color"];
|
|
23
|
+
var BEM_NAMES = {
|
|
24
|
+
"selector-class-pattern": "^[a-z][a-z0-9]*(__[a-z0-9]+(-[a-z0-9]+)*)?(--[a-z0-9]+(-[a-z0-9]+)*)?$"
|
|
25
|
+
};
|
|
26
|
+
var POSITIONAL_PSEUDO_CLASSES = [
|
|
27
|
+
"first-child",
|
|
28
|
+
"last-child",
|
|
29
|
+
"only-child",
|
|
30
|
+
"nth-child",
|
|
31
|
+
"nth-last-child",
|
|
32
|
+
"first-of-type",
|
|
33
|
+
"last-of-type",
|
|
34
|
+
"only-of-type",
|
|
35
|
+
"nth-of-type",
|
|
36
|
+
"nth-last-of-type"
|
|
37
|
+
];
|
|
38
|
+
var FLAT_SELECTORS = {
|
|
39
|
+
"selector-max-type": 0,
|
|
40
|
+
"selector-max-id": 0,
|
|
41
|
+
"selector-max-universal": 0,
|
|
42
|
+
"selector-max-compound-selectors": 2,
|
|
43
|
+
"max-nesting-depth": 0,
|
|
44
|
+
"selector-max-attribute": 0,
|
|
45
|
+
"selector-pseudo-class-disallowed-list": POSITIONAL_PSEUDO_CLASSES
|
|
46
|
+
};
|
|
47
|
+
var PAGE_HAS = "/^(?=[\\s\\S]*:has\\()(?=[\\s\\S]*(?:(?:^|[\\s>+~,([])(?:html|body|main)|:root)(?![\\w-]))[\\s\\S]*$/i";
|
|
48
|
+
var PSEUDO_ARGUMENT = "(?:[^()]|\\([^()]*\\))*";
|
|
49
|
+
var PSEUDO_CLASS = `::?[\\w-]+(?:\\(${PSEUDO_ARGUMENT}\\))?`;
|
|
50
|
+
var HAS_IN_PSEUDO_CLASS = `(?::has\\(${PSEUDO_ARGUMENT}\\)|::?[\\w-]+\\(${PSEUDO_ARGUMENT}:has\\([^()]*\\)${PSEUDO_ARGUMENT}\\))`;
|
|
51
|
+
var CLASSLESS_HAS = `/(?:^|[\\s>+~,])(?:${PSEUDO_CLASS})*${HAS_IN_PSEUDO_CLASS}(?:${PSEUDO_CLASS})*(?=[\\s>+~,]|$)/i`;
|
|
23
52
|
var DESIGN_TOKENS = {
|
|
24
53
|
"color-no-hex": true,
|
|
25
54
|
"color-named": "never",
|
|
@@ -61,16 +90,23 @@ function designTokens(files) {
|
|
|
61
90
|
const off = Object.fromEntries(Object.keys(DESIGN_TOKENS).map((rule) => [rule, null]));
|
|
62
91
|
return { rules: DESIGN_TOKENS, overrides: [{ files: [...files], rules: off }] };
|
|
63
92
|
}
|
|
64
|
-
function
|
|
93
|
+
function disallowedSelectors(noPageHas) {
|
|
94
|
+
if (noPageHas === true)
|
|
95
|
+
return { "selector-disallowed-list": [[...LANGUAGE_FREE_SELECTORS["selector-disallowed-list"], PAGE_HAS, CLASSLESS_HAS], { splitList: true }] };
|
|
96
|
+
return LANGUAGE_FREE_SELECTORS;
|
|
97
|
+
}
|
|
98
|
+
function defineConfig({ baseline, tokens, bem, flatSelectors, noPageHas, rules, overrides = [], ...rest }) {
|
|
65
99
|
const available = baselineTarget(baseline);
|
|
66
100
|
const tokenRules = designTokens(tokenFiles(tokens));
|
|
67
101
|
const merged = {
|
|
68
102
|
...CONVENTION_CHANGES,
|
|
69
103
|
...SELECTOR_COMPLEXITY,
|
|
70
104
|
...COMMENT_REFUSAL,
|
|
71
|
-
...
|
|
105
|
+
...disallowedSelectors(noPageHas),
|
|
72
106
|
[BASELINE_RULE]: [true, { available }],
|
|
73
107
|
...tokenRules.rules,
|
|
108
|
+
...bem === true ? BEM_NAMES : null,
|
|
109
|
+
...flatSelectors === true ? FLAT_SELECTORS : null,
|
|
74
110
|
...rules
|
|
75
111
|
};
|
|
76
112
|
return {
|
|
@@ -17,7 +17,7 @@ A consuming repository puts each setting in the file its tool reads, and no mani
|
|
|
17
17
|
| `exports-baseline.json` | The existing unused exports and types, per file, kind and name | `checks-exports` |
|
|
18
18
|
| `.jscpd.json` | The `path` and `ignore` globs of the files repetition is measured in | jscpd and `checks-repetition` |
|
|
19
19
|
| `knip.config.ts` | The `entry` globs Knip traces unreferenced files from | Knip, `checks-unused` and `checks-exports` |
|
|
20
|
-
| `stylelint.config.ts` | The Baseline target, the token files, and any stylelint rule the repository sets over the kit's families | `checks-frontend-syntax` |
|
|
20
|
+
| `stylelint.config.ts` | The Baseline target, the token files, the CSS families the repository turns on, and any stylelint rule the repository sets over the kit's families | `checks-frontend-syntax` |
|
|
21
21
|
| `dependency-cruiser.config.ts` | Entry points nothing imports, paths that may import dev dependencies, and import boundaries, when the kit's defaults do not fit | `checks-imports` |
|
|
22
22
|
| `tsconfig.json` | Compiler options, and the Effect language service plugin whose `overrides` and kit severities `checks-effect-scope` writes, keeping every other key | TypeScript and Effect language service |
|
|
23
23
|
| `package.json` | `scripts` with the `checks-vendor` arguments in `prepare`, `author` and `contributors` | Bun, `checks-commit-identity` and `checks-vendor` |
|
|
@@ -41,7 +41,7 @@ The builder throws when the tool loads such a config anyway.
|
|
|
41
41
|
| `@avi2dg/checks/oxlint` | `effect` | `extends`, `plugins`, `jsPlugins` and `categories` |
|
|
42
42
|
| `@avi2dg/checks/knip` | `entry`, where `[]` means the package scripts and tests name every entry | `include` |
|
|
43
43
|
| `@avi2dg/checks/dependency-cruiser` | nothing | `extends` |
|
|
44
|
-
| `@avi2dg/checks/stylelint` | `baseline` and `tokens`, where `false` leaves the design token rules off | `extends` and `plugins` |
|
|
44
|
+
| `@avi2dg/checks/stylelint` | `baseline` and `tokens`, where `false` leaves the design token rules off, with `bem`, `flatSelectors` and `noPageHas` turning on their families | `extends` and `plugins` |
|
|
45
45
|
|
|
46
46
|
A repository whose sources are Effect programs writes `oxlint.config.ts`:
|
|
47
47
|
|
|
@@ -41,12 +41,16 @@ A control that needs selection off sets it on its own selector, such as `.drag-h
|
|
|
41
41
|
| G5 Baseline target | `plugin/use-baseline` from `stylelint-plugin-use-baseline`, which judges each property, value, function, at-rule, selector and unit against the Baseline data, and passes a property inside an `@supports` block. It passes `@property` at every target, since a browser without `@property` treats the declaration as an unregistered custom property | on, at the target `baseline` sets |
|
|
42
42
|
| G6 comments refused | `comment-word-disallowed-list` over `/^[\s\S]*$/`, which refuses every comment, including an empty `/* */` | on |
|
|
43
43
|
| G7 language-free selectors | `selector-disallowed-list` over `/[^\x00-\x7F]/`, which refuses any selector holding a non-ASCII character, in attribute values, class names and ids alike | on |
|
|
44
|
+
| G8 BEM names | `selector-class-pattern` over one lowercase block with kebab `__element` and `--modifier` parts | off until `bem` is `true` |
|
|
45
|
+
| G9 class-only flat selectors | `selector-max-type` at 0, `selector-max-id` at 0, `selector-max-universal` at 0, `selector-max-compound-selectors` at 2, `max-nesting-depth` at 0, `selector-max-attribute` at 0, and `selector-pseudo-class-disallowed-list` over the positional pseudo-classes, from `:first-child` to `:nth-last-of-type` | off until `flatSelectors` is `true` |
|
|
46
|
+
| G10 page-wide has refused | `selector-disallowed-list` with `splitList` over a pattern that refuses a `:has` beside `html`, `body`, `main` or `:root` in one selector of a list, across line breaks in the selector, and one that refuses a `:has` in a compound of pseudo-classes alone, such as `:has(.hero)`, `:scope:has(.hero)` or `:not(.card):has(.hero)`, whose subject can be the root. A nested `:has` escapes it unless G9 refuses the nesting | off until `noPageHas` is `true` |
|
|
44
47
|
|
|
45
|
-
The builder changes
|
|
48
|
+
The builder changes four of the shared config's rules:
|
|
46
49
|
|
|
47
50
|
- `no-descending-specificity` is off, since it compares only the last compound selector and has no access to the DOM.
|
|
48
51
|
It reports components that never match one element.
|
|
49
52
|
- `property-no-vendor-prefix` keeps reporting without a fix, and passes `-webkit-user-select`, since its fix deletes the prefix Safari still needs.
|
|
53
|
+
- `selector-class-pattern` holds BEM names while `bem` is `true`, since the shared kebab case refuses `__` and `--`.
|
|
50
54
|
- In an `.html` or `.astro` file, `declaration-block-single-line-max-declarations` is off, since a `style` attribute is one line.
|
|
51
55
|
|
|
52
56
|
The comment rule refuses every comment but a sourcemap `/*# */` one, which stylelint passes to keep the map.
|
|
@@ -108,6 +112,9 @@ The builder throws when the gate loads such a config anyway.
|
|
|
108
112
|
| --- | --- |
|
|
109
113
|
| `baseline` | `"widely"`, `"newly"` or a year such as `2024`, which allows what became Baseline newly available in that year or earlier |
|
|
110
114
|
| `tokens` | the globs of the files that declare the design tokens, which turns G4 on in every other file, or `false` |
|
|
115
|
+
| `bem` | `true` to turn G8 on, left out to leave it off |
|
|
116
|
+
| `flatSelectors` | `true` to turn G9 on, left out to leave it off |
|
|
117
|
+
| `noPageHas` | `true` to turn G10 on, left out to leave it off |
|
|
111
118
|
|
|
112
119
|
`rules`, `overrides`, `ignoreFiles` and the other keys mean what they mean in stylelint's own docs, and land after the kit's.
|
|
113
120
|
An accepted exception lives there rather than in a comment, such as a property the Baseline rule passes:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -184,6 +184,7 @@
|
|
|
184
184
|
"lint": "oxlint && bun src/core/lint.ts",
|
|
185
185
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
186
186
|
"test": "bun src/testing/test.ts",
|
|
187
|
+
"test:fast": "bun scripts/test-fast.ts",
|
|
187
188
|
"test:unit": "bun src/testing/test.ts --tier=unit",
|
|
188
189
|
"test:e2e": "bun src/testing/test.ts --tier=e2e --parallel=4",
|
|
189
190
|
"mutate": "bunx stryker run",
|
|
@@ -8,6 +8,9 @@ export type BaselineTarget = "widely" | "newly" | number;
|
|
|
8
8
|
export type ChecksStylelintConfig = Omit<Config, "extends" | "plugins"> & {
|
|
9
9
|
readonly baseline: BaselineTarget;
|
|
10
10
|
readonly tokens: Globs | false;
|
|
11
|
+
readonly bem?: boolean;
|
|
12
|
+
readonly flatSelectors?: boolean;
|
|
13
|
+
readonly noPageHas?: boolean;
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
type Rules = NonNullable<Config["rules"]>;
|
|
@@ -43,6 +46,47 @@ const LANGUAGE_FREE_SELECTORS = {
|
|
|
43
46
|
|
|
44
47
|
const COLOUR_FUNCTIONS = ["rgb", "rgba", "hsl", "hsla", "hwb", "lab", "lch", "oklab", "oklch", "color"] as const;
|
|
45
48
|
|
|
49
|
+
// selector-class-pattern builds a RegExp from the string itself, so slashes would match literal slashes.
|
|
50
|
+
const BEM_NAMES = {
|
|
51
|
+
"selector-class-pattern": "^[a-z][a-z0-9]*(__[a-z0-9]+(-[a-z0-9]+)*)?(--[a-z0-9]+(-[a-z0-9]+)*)?$",
|
|
52
|
+
} as const satisfies Rules;
|
|
53
|
+
|
|
54
|
+
const POSITIONAL_PSEUDO_CLASSES = [
|
|
55
|
+
"first-child",
|
|
56
|
+
"last-child",
|
|
57
|
+
"only-child",
|
|
58
|
+
"nth-child",
|
|
59
|
+
"nth-last-child",
|
|
60
|
+
"first-of-type",
|
|
61
|
+
"last-of-type",
|
|
62
|
+
"only-of-type",
|
|
63
|
+
"nth-of-type",
|
|
64
|
+
"nth-last-of-type",
|
|
65
|
+
] as const;
|
|
66
|
+
|
|
67
|
+
const FLAT_SELECTORS = {
|
|
68
|
+
"selector-max-type": 0,
|
|
69
|
+
"selector-max-id": 0,
|
|
70
|
+
"selector-max-universal": 0,
|
|
71
|
+
"selector-max-compound-selectors": 2,
|
|
72
|
+
"max-nesting-depth": 0,
|
|
73
|
+
"selector-max-attribute": 0,
|
|
74
|
+
"selector-pseudo-class-disallowed-list": POSITIONAL_PSEUDO_CLASSES,
|
|
75
|
+
} as const satisfies Rules;
|
|
76
|
+
|
|
77
|
+
// The ban needs both a page element and :has( in one selector, in either order, and a class such as .block-body holds neither.
|
|
78
|
+
// The rule tests the raw selector, newlines included, so [\s\S] stands where . would stop at a line break.
|
|
79
|
+
const PAGE_HAS = "/^(?=[\\s\\S]*:has\\()(?=[\\s\\S]*(?:(?:^|[\\s>+~,([])(?:html|body|main)|:root)(?![\\w-]))[\\s\\S]*$/i";
|
|
80
|
+
|
|
81
|
+
const PSEUDO_ARGUMENT = "(?:[^()]|\\([^()]*\\))*";
|
|
82
|
+
|
|
83
|
+
const PSEUDO_CLASS = `::?[\\w-]+(?:\\(${PSEUDO_ARGUMENT}\\))?`;
|
|
84
|
+
|
|
85
|
+
const HAS_IN_PSEUDO_CLASS = `(?::has\\(${PSEUDO_ARGUMENT}\\)|::?[\\w-]+\\(${PSEUDO_ARGUMENT}:has\\([^()]*\\)${PSEUDO_ARGUMENT}\\))`;
|
|
86
|
+
|
|
87
|
+
// A compound of pseudo-classes alone, :scope and :not(.card) among them, can match the root element, so its :has() reads page state.
|
|
88
|
+
const CLASSLESS_HAS = `/(?:^|[\\s>+~,])(?:${PSEUDO_CLASS})*${HAS_IN_PSEUDO_CLASS}(?:${PSEUDO_CLASS})*(?=[\\s>+~,]|$)/i`;
|
|
89
|
+
|
|
46
90
|
const DESIGN_TOKENS = {
|
|
47
91
|
"color-no-hex": true,
|
|
48
92
|
"color-named": "never",
|
|
@@ -87,17 +131,24 @@ function designTokens(files: readonly string[]): Required<Pick<Config, "rules" |
|
|
|
87
131
|
return { rules: DESIGN_TOKENS, overrides: [{ files: [...files], rules: off }] };
|
|
88
132
|
}
|
|
89
133
|
|
|
134
|
+
function disallowedSelectors(noPageHas: boolean | undefined): Rules {
|
|
135
|
+
if (noPageHas === true) return { "selector-disallowed-list": [[...LANGUAGE_FREE_SELECTORS["selector-disallowed-list"], PAGE_HAS, CLASSLESS_HAS], { splitList: true }] };
|
|
136
|
+
return LANGUAGE_FREE_SELECTORS;
|
|
137
|
+
}
|
|
138
|
+
|
|
90
139
|
// The types refuse a missing or malformed baseline or tokens, and the guards repeat it for a config no typecheck reads.
|
|
91
|
-
export function defineConfig({ baseline, tokens, rules, overrides = [], ...rest }: ChecksStylelintConfig): Config {
|
|
140
|
+
export function defineConfig({ baseline, tokens, bem, flatSelectors, noPageHas, rules, overrides = [], ...rest }: ChecksStylelintConfig): Config {
|
|
92
141
|
const available = baselineTarget(baseline);
|
|
93
142
|
const tokenRules = designTokens(tokenFiles(tokens));
|
|
94
143
|
const merged: Rules = {
|
|
95
144
|
...CONVENTION_CHANGES,
|
|
96
145
|
...SELECTOR_COMPLEXITY,
|
|
97
146
|
...COMMENT_REFUSAL,
|
|
98
|
-
...
|
|
147
|
+
...disallowedSelectors(noPageHas),
|
|
99
148
|
[BASELINE_RULE]: [true, { available }],
|
|
100
149
|
...tokenRules.rules,
|
|
150
|
+
...(bem === true ? BEM_NAMES : null),
|
|
151
|
+
...(flatSelectors === true ? FLAT_SELECTORS : null),
|
|
101
152
|
...rules,
|
|
102
153
|
};
|
|
103
154
|
return {
|