@plumeria/eslint-plugin 16.1.1 → 16.2.1
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 +20 -0
- package/bin/oxlint.js +88 -10
- package/dist/index.js +3 -0
- package/dist/rules/validate-pseudos.d.ts +4 -0
- package/dist/rules/validate-pseudos.js +247 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ The `plugin:@plumeria/recommended` config enables the following:
|
|
|
19
19
|
- `@plumeria/sort-properties`: **warn**
|
|
20
20
|
- `@plumeria/format-properties`: **warn**
|
|
21
21
|
- `@plumeria/validate-values`: **warn**
|
|
22
|
+
- `@plumeria/validate-pseudos`: **error**
|
|
22
23
|
|
|
23
24
|
```js
|
|
24
25
|
import plumeria from '@plumeria/eslint-plugin';
|
|
@@ -78,6 +79,10 @@ Automatically format for consistency and maintainability.
|
|
|
78
79
|
|
|
79
80
|
Validates CSS property values for correctness. Only standard CSS properties are checked; properties with string literal keys (e.g., computed or dynamic property names) are not validated.
|
|
80
81
|
|
|
82
|
+
### validate-pseudos
|
|
83
|
+
|
|
84
|
+
Validates CSS pseudo-classes and pseudo-elements inside `css.create()`. It checks for typos and structural correctness and supports validation of computed keys when TypeScript is available.
|
|
85
|
+
|
|
81
86
|
## CLI (plumerialint)
|
|
82
87
|
|
|
83
88
|
This package provides a CLI command, `plumerialint`, as a convenient way
|
|
@@ -112,4 +117,19 @@ Example usage in `package.json`:
|
|
|
112
117
|
}
|
|
113
118
|
```
|
|
114
119
|
|
|
120
|
+
### Aborting Builds on Lint Errors (Parallel Pipeline)
|
|
121
|
+
|
|
122
|
+
You can run `plumerialint` in parallel with your build command (e.g. `next build` or `vite build`) using the `--` separator:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"scripts": {
|
|
127
|
+
"build": "plumerialint -- next build"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
If `plumerialint` detects any styling errors or warnings, it will print the diagnostics, kill the build process immediately, and exit with a non-zero code. This avoids compiling when styling validation fails.
|
|
133
|
+
|
|
115
134
|
**Note:** `oxlint` is required as `plumerialint` uses it internally.
|
|
135
|
+
|
package/bin/oxlint.js
CHANGED
|
@@ -3,15 +3,26 @@
|
|
|
3
3
|
const { spawn } = require('child_process');
|
|
4
4
|
const process = require('process');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const
|
|
6
|
+
const oxlintConfig = path.join(__dirname, '..', 'oxlint.json');
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const doubleDashIndex = process.argv.indexOf('--');
|
|
9
|
+
let oxlintExtraArgs = [];
|
|
10
|
+
let buildCommand = null;
|
|
11
|
+
let buildArgs = [];
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
if (doubleDashIndex !== -1) {
|
|
14
|
+
oxlintExtraArgs = process.argv.slice(2, doubleDashIndex);
|
|
15
|
+
if (doubleDashIndex + 1 < process.argv.length) {
|
|
16
|
+
buildCommand = process.argv[doubleDashIndex + 1];
|
|
17
|
+
buildArgs = process.argv.slice(doubleDashIndex + 2);
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
oxlintExtraArgs = process.argv.slice(2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const oxlintArgs = ['-c', oxlintConfig, '--deny-warnings', ...oxlintExtraArgs];
|
|
13
24
|
|
|
14
|
-
|
|
25
|
+
function handleOxlintError(err) {
|
|
15
26
|
if (err.code === 'ENOENT') {
|
|
16
27
|
console.error('\n✖ oxlint is not installed.');
|
|
17
28
|
console.error('➡︎ plumerialint uses oxlint.');
|
|
@@ -21,8 +32,75 @@ child.on('error', (err) => {
|
|
|
21
32
|
console.error('Error running oxlint:', err.message);
|
|
22
33
|
process.exit(1);
|
|
23
34
|
}
|
|
24
|
-
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!buildCommand) {
|
|
38
|
+
const child = spawn('oxlint', oxlintArgs, { stdio: 'inherit' });
|
|
39
|
+
child.on('error', handleOxlintError);
|
|
40
|
+
child.on('close', (code) => {
|
|
41
|
+
process.exit(code || 0);
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
let oxlintExited = false;
|
|
45
|
+
let oxlintCode = null;
|
|
46
|
+
let buildExited = false;
|
|
47
|
+
let buildCode = null;
|
|
48
|
+
let aborted = false;
|
|
49
|
+
|
|
50
|
+
const oxlintChild = spawn('oxlint', oxlintArgs, { stdio: 'inherit' });
|
|
51
|
+
const buildChild = spawn(buildCommand, buildArgs, {
|
|
52
|
+
stdio: 'inherit',
|
|
53
|
+
shell: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function abort(exitCode, failedSource) {
|
|
57
|
+
if (aborted) return;
|
|
58
|
+
aborted = true;
|
|
59
|
+
|
|
60
|
+
if (failedSource === 'lint') {
|
|
61
|
+
console.error(`\n✖ [plumerialint] Linting failed. Aborting build...`);
|
|
62
|
+
try {
|
|
63
|
+
buildChild.kill();
|
|
64
|
+
} catch (e) {}
|
|
65
|
+
process.exit(exitCode || 1);
|
|
66
|
+
} else if (failedSource === 'build') {
|
|
67
|
+
console.error(`\n✖ [plumerialint] Build failed. Aborting lint...`);
|
|
68
|
+
try {
|
|
69
|
+
oxlintChild.kill();
|
|
70
|
+
} catch (e) {}
|
|
71
|
+
process.exit(exitCode || 1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
oxlintChild.on('error', (err) => {
|
|
76
|
+
handleOxlintError(err);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
buildChild.on('error', (err) => {
|
|
80
|
+
console.error(
|
|
81
|
+
`Error running build command "${buildCommand}":`,
|
|
82
|
+
err.message,
|
|
83
|
+
);
|
|
84
|
+
abort(1, 'build');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
oxlintChild.on('close', (code) => {
|
|
88
|
+
oxlintExited = true;
|
|
89
|
+
oxlintCode = code;
|
|
90
|
+
if (code !== 0) {
|
|
91
|
+
abort(code, 'lint');
|
|
92
|
+
} else if (buildExited) {
|
|
93
|
+
process.exit(buildCode || 0);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
25
96
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
97
|
+
buildChild.on('close', (code) => {
|
|
98
|
+
buildExited = true;
|
|
99
|
+
buildCode = code;
|
|
100
|
+
if (code !== 0) {
|
|
101
|
+
abort(code, 'build');
|
|
102
|
+
} else if (oxlintExited) {
|
|
103
|
+
process.exit(oxlintCode || 0);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const no_unused_keys_1 = require("./rules/no-unused-keys");
|
|
|
11
11
|
const sort_properties_1 = require("./rules/sort-properties");
|
|
12
12
|
const format_properties_1 = require("./rules/format-properties");
|
|
13
13
|
const validate_values_1 = require("./rules/validate-values");
|
|
14
|
+
const validate_pseudos_1 = require("./rules/validate-pseudos");
|
|
14
15
|
const rules = {
|
|
15
16
|
'style-name-requires-import': style_name_requires_import_1.styleNameRequiresImport,
|
|
16
17
|
'no-combinator': no_combinator_1.noCombinator,
|
|
@@ -24,6 +25,7 @@ const rules = {
|
|
|
24
25
|
'sort-properties': sort_properties_1.sortProperties,
|
|
25
26
|
'format-properties': format_properties_1.formatProperties,
|
|
26
27
|
'validate-values': validate_values_1.validateValues,
|
|
28
|
+
'validate-pseudos': validate_pseudos_1.validatePseudos,
|
|
27
29
|
};
|
|
28
30
|
const configs = {
|
|
29
31
|
recommended: {
|
|
@@ -45,6 +47,7 @@ const configs = {
|
|
|
45
47
|
'@plumeria/sort-properties': 'warn',
|
|
46
48
|
'@plumeria/format-properties': 'warn',
|
|
47
49
|
'@plumeria/validate-values': 'warn',
|
|
50
|
+
'@plumeria/validate-pseudos': 'error',
|
|
48
51
|
},
|
|
49
52
|
},
|
|
50
53
|
};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validatePseudos = void 0;
|
|
4
|
+
exports.splitChainedPseudos = splitChainedPseudos;
|
|
5
|
+
exports.isValidPseudo = isValidPseudo;
|
|
6
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
7
|
+
const VALID_STATIC_PSEUDOS = new Set([
|
|
8
|
+
':hover',
|
|
9
|
+
':active',
|
|
10
|
+
':focus',
|
|
11
|
+
':focus-visible',
|
|
12
|
+
':focus-within',
|
|
13
|
+
':checked',
|
|
14
|
+
':disabled',
|
|
15
|
+
':enabled',
|
|
16
|
+
':required',
|
|
17
|
+
':optional',
|
|
18
|
+
':valid',
|
|
19
|
+
':invalid',
|
|
20
|
+
':in-range',
|
|
21
|
+
':out-of-range',
|
|
22
|
+
':read-only',
|
|
23
|
+
':read-write',
|
|
24
|
+
':placeholder-shown',
|
|
25
|
+
':indeterminate',
|
|
26
|
+
':default',
|
|
27
|
+
':autofill',
|
|
28
|
+
':first-child',
|
|
29
|
+
':last-child',
|
|
30
|
+
':only-child',
|
|
31
|
+
':first-of-type',
|
|
32
|
+
':last-of-type',
|
|
33
|
+
':only-of-type',
|
|
34
|
+
':empty',
|
|
35
|
+
':link',
|
|
36
|
+
':visited',
|
|
37
|
+
':any-link',
|
|
38
|
+
':root',
|
|
39
|
+
':target',
|
|
40
|
+
':fullscreen',
|
|
41
|
+
':modal',
|
|
42
|
+
':open',
|
|
43
|
+
':defined',
|
|
44
|
+
':popover-open',
|
|
45
|
+
':host',
|
|
46
|
+
'::before',
|
|
47
|
+
'::after',
|
|
48
|
+
'::first-letter',
|
|
49
|
+
'::first-line',
|
|
50
|
+
'::selection',
|
|
51
|
+
'::placeholder',
|
|
52
|
+
'::marker',
|
|
53
|
+
'::cue',
|
|
54
|
+
'::backdrop',
|
|
55
|
+
'::spelling-error',
|
|
56
|
+
'::grammar-error',
|
|
57
|
+
'::view-transition',
|
|
58
|
+
'::file-selector-button',
|
|
59
|
+
'::details-content',
|
|
60
|
+
'::target-text',
|
|
61
|
+
':before',
|
|
62
|
+
':after',
|
|
63
|
+
':first-letter',
|
|
64
|
+
':first-line',
|
|
65
|
+
':placeholder',
|
|
66
|
+
]);
|
|
67
|
+
const FUNCTIONAL_PREFIXES = [
|
|
68
|
+
':nth-child(',
|
|
69
|
+
':nth-last-child(',
|
|
70
|
+
':nth-of-type(',
|
|
71
|
+
':nth-last-of-type(',
|
|
72
|
+
':not(',
|
|
73
|
+
':is(',
|
|
74
|
+
':where(',
|
|
75
|
+
':has(',
|
|
76
|
+
':lang(',
|
|
77
|
+
':dir(',
|
|
78
|
+
':host(',
|
|
79
|
+
':host-context(',
|
|
80
|
+
':state(',
|
|
81
|
+
'::cue(',
|
|
82
|
+
'::part(',
|
|
83
|
+
'::slotted(',
|
|
84
|
+
'::view-transition-group(',
|
|
85
|
+
'::view-transition-image-pair(',
|
|
86
|
+
'::view-transition-old(',
|
|
87
|
+
'::view-transition-new(',
|
|
88
|
+
'::highlight(',
|
|
89
|
+
];
|
|
90
|
+
function splitChainedPseudos(selector) {
|
|
91
|
+
const parts = [];
|
|
92
|
+
let current = '';
|
|
93
|
+
let parenDepth = 0;
|
|
94
|
+
for (let i = 0; i < selector.length; i++) {
|
|
95
|
+
const char = selector[i];
|
|
96
|
+
if (char === '(') {
|
|
97
|
+
parenDepth++;
|
|
98
|
+
current += char;
|
|
99
|
+
}
|
|
100
|
+
else if (char === ')') {
|
|
101
|
+
parenDepth--;
|
|
102
|
+
current += char;
|
|
103
|
+
}
|
|
104
|
+
else if (char === ':' && parenDepth === 0) {
|
|
105
|
+
if (current.length > 0 && current !== ':' && current !== '::') {
|
|
106
|
+
parts.push(current);
|
|
107
|
+
current = ':';
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
current += ':';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
current += char;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (current.length > 0) {
|
|
118
|
+
parts.push(current);
|
|
119
|
+
}
|
|
120
|
+
return parts;
|
|
121
|
+
}
|
|
122
|
+
function isValidPseudo(selector) {
|
|
123
|
+
if (VALID_STATIC_PSEUDOS.has(selector)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
for (const prefix of FUNCTIONAL_PREFIXES) {
|
|
127
|
+
if (selector.startsWith(prefix) && selector.endsWith(')')) {
|
|
128
|
+
const content = selector.slice(prefix.length, -1).trim();
|
|
129
|
+
if (content.length > 0) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
exports.validatePseudos = {
|
|
137
|
+
meta: {
|
|
138
|
+
type: 'problem',
|
|
139
|
+
docs: {
|
|
140
|
+
description: 'Validate CSS pseudo-classes and pseudo-elements inside css.create.',
|
|
141
|
+
},
|
|
142
|
+
messages: {
|
|
143
|
+
invalidPseudo: 'Invalid pseudo-class or pseudo-element: "{{selector}}".',
|
|
144
|
+
},
|
|
145
|
+
schema: [],
|
|
146
|
+
},
|
|
147
|
+
create(context) {
|
|
148
|
+
const plumeriaAliases = {};
|
|
149
|
+
const parserServices = context.sourceCode.parserServices;
|
|
150
|
+
const checker = parserServices?.program?.getTypeChecker();
|
|
151
|
+
function getSelectorString(node) {
|
|
152
|
+
if (node.type === utils_1.TSESTree.AST_NODE_TYPES.Literal &&
|
|
153
|
+
typeof node.value === 'string') {
|
|
154
|
+
return node.value;
|
|
155
|
+
}
|
|
156
|
+
if (node.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier &&
|
|
157
|
+
!(node.parent?.type === utils_1.TSESTree.AST_NODE_TYPES.Property &&
|
|
158
|
+
node.parent.computed)) {
|
|
159
|
+
return node.name;
|
|
160
|
+
}
|
|
161
|
+
if (checker && parserServices?.esTreeNodeToTSNodeMap) {
|
|
162
|
+
try {
|
|
163
|
+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
164
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
165
|
+
if (type.isStringLiteral()) {
|
|
166
|
+
return type.value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
function checkProperties(node) {
|
|
175
|
+
for (const prop of node.properties) {
|
|
176
|
+
if (prop.type !== utils_1.TSESTree.AST_NODE_TYPES.Property)
|
|
177
|
+
continue;
|
|
178
|
+
const selectorString = getSelectorString(prop.key);
|
|
179
|
+
if (selectorString !== null && selectorString.startsWith(':')) {
|
|
180
|
+
const parts = splitChainedPseudos(selectorString);
|
|
181
|
+
for (const part of parts) {
|
|
182
|
+
if (!isValidPseudo(part)) {
|
|
183
|
+
context.report({
|
|
184
|
+
node: prop.key,
|
|
185
|
+
messageId: 'invalidPseudo',
|
|
186
|
+
data: {
|
|
187
|
+
selector: selectorString,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (prop.value.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
195
|
+
checkProperties(prop.value);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
ImportDeclaration(node) {
|
|
201
|
+
if (node.source.value === '@plumeria/core') {
|
|
202
|
+
node.specifiers.forEach((specifier) => {
|
|
203
|
+
if (specifier.type ===
|
|
204
|
+
utils_1.TSESTree.AST_NODE_TYPES.ImportNamespaceSpecifier ||
|
|
205
|
+
specifier.type === utils_1.TSESTree.AST_NODE_TYPES.ImportDefaultSpecifier) {
|
|
206
|
+
plumeriaAliases[specifier.local.name] = 'NAMESPACE';
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const importedName = specifier.imported.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier
|
|
210
|
+
? specifier.imported.name
|
|
211
|
+
: String(specifier.imported.value);
|
|
212
|
+
plumeriaAliases[specifier.local.name] = importedName;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
CallExpression(node) {
|
|
218
|
+
let isCssCreate = false;
|
|
219
|
+
if (node.callee.type === utils_1.TSESTree.AST_NODE_TYPES.MemberExpression) {
|
|
220
|
+
if (node.callee.object.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier &&
|
|
221
|
+
plumeriaAliases[node.callee.object.name] === 'NAMESPACE') {
|
|
222
|
+
const propertyName = node.callee.property.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier
|
|
223
|
+
? node.callee.property.name
|
|
224
|
+
: null;
|
|
225
|
+
if (propertyName === 'create')
|
|
226
|
+
isCssCreate = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (node.callee.type === utils_1.TSESTree.AST_NODE_TYPES.Identifier) {
|
|
230
|
+
const alias = plumeriaAliases[node.callee.name];
|
|
231
|
+
if (alias === 'create')
|
|
232
|
+
isCssCreate = true;
|
|
233
|
+
}
|
|
234
|
+
if (isCssCreate &&
|
|
235
|
+
node.arguments[0]?.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
236
|
+
const styleObj = node.arguments[0];
|
|
237
|
+
styleObj.properties.forEach((prop) => {
|
|
238
|
+
if (prop.type === utils_1.TSESTree.AST_NODE_TYPES.Property &&
|
|
239
|
+
prop.value.type === utils_1.TSESTree.AST_NODE_TYPES.ObjectExpression) {
|
|
240
|
+
checkProperties(prop.value);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
},
|
|
247
|
+
};
|