@plumeria/eslint-plugin 16.1.1 → 16.2.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/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
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,4 @@
1
+ import { Rule } from 'eslint';
2
+ export declare function splitChainedPseudos(selector: string): string[];
3
+ export declare function isValidPseudo(selector: string): boolean;
4
+ export declare const validatePseudos: Rule.RuleModule;
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeria/eslint-plugin",
3
- "version": "16.1.1",
3
+ "version": "16.2.0",
4
4
  "description": "Plumeria ESLint plugin",
5
5
  "author": "Refirst 11",
6
6
  "license": "MIT",