@master/eslint-plugin-css 2.0.0-rc.74 → 2.0.0-rc.76
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 +18 -2
- package/dist/configs/stylesheet.d.ts +6 -0
- package/dist/configs/stylesheet.js +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/rules/no-conflicting-classes.js +4 -49
- package/dist/rules/no-invalid-classes.js +7 -13
- package/dist/rules/no-unapproved-raw-values.js +7 -16
- package/dist/rules/prefer-canonical-classes.js +50 -41
- package/dist/rules/sort-classes.js +12 -14
- package/dist/stylesheet-parser.d.ts +31 -0
- package/dist/stylesheet-parser.js +33 -0
- package/dist/utils/report-lint-diagnostics.d.ts +16 -0
- package/dist/utils/report-lint-diagnostics.js +46 -0
- package/dist/utils/resolve-compose-directive-class-nodes.d.ts +11 -0
- package/dist/utils/resolve-compose-directive-class-nodes.js +157 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -60,6 +60,8 @@ npm install -D @master/eslint-plugin-css
|
|
|
60
60
|
|
|
61
61
|
The recommended config is also available through `@master/eslint-config-css`.
|
|
62
62
|
|
|
63
|
+
In Visual Studio Code, install the VS Code ESLint extension to surface these diagnostics and run sort-on-save. The Master CSS extension provides completion, hover, colors, highlighting, directive diagnostics, manifest diagnostics, and directive formatting; it does not duplicate class policy warnings by default.
|
|
64
|
+
|
|
63
65
|
## Usage
|
|
64
66
|
|
|
65
67
|
Use ESLint flat configuration:
|
|
@@ -83,7 +85,7 @@ export default [
|
|
|
83
85
|
parser: tsParser
|
|
84
86
|
}
|
|
85
87
|
},
|
|
86
|
-
css,
|
|
88
|
+
...css,
|
|
87
89
|
{
|
|
88
90
|
rules: {
|
|
89
91
|
'@master/css/no-invalid-classes': ['error', {
|
|
@@ -154,6 +156,18 @@ export default [
|
|
|
154
156
|
|
|
155
157
|
For example, this rule can fix `text-align:center` to `text-center`, `font:16px` to `font:md`, `margin:md` to `m:md`, `m:1rem|1.5rem` to `m:md|lg`, `fg:var(--color-red-60)` to `fg:red-60`, `w:md h:md` to `size:md`, `mt:md mb:md` to `my:md`, and `block@dark@sm` to `block@sm@dark`.
|
|
156
158
|
|
|
159
|
+
The same canonicalization applies to unquoted `@compose` class lists. In CSS, declaration-like classes are moved to native declarations and suffixes are moved to `@variant`, `@dark`, or `@light` blocks when the rewrite is safe.
|
|
160
|
+
|
|
161
|
+
Standalone CSS files are included by default when using `@master/eslint-config-css`:
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
import css from '@master/eslint-config-css'
|
|
165
|
+
|
|
166
|
+
export default [...css]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Direct plugin users can add `css.configs.stylesheet` from `@master/eslint-plugin-css` alongside the recommended config.
|
|
170
|
+
|
|
157
171
|
You can disable specific canonicalization families:
|
|
158
172
|
|
|
159
173
|
```js
|
|
@@ -167,7 +181,9 @@ export default [
|
|
|
167
181
|
preferVariableReferences: true,
|
|
168
182
|
preferMultiValueTokens: true,
|
|
169
183
|
preferCompositionUtilities: true,
|
|
170
|
-
preferConditionOrder: true
|
|
184
|
+
preferConditionOrder: true,
|
|
185
|
+
preferNativeDeclarationsInCompose: true,
|
|
186
|
+
preferVariantBlocksInCompose: true
|
|
171
187
|
}]
|
|
172
188
|
}
|
|
173
189
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import plugin from "../plugin.js";
|
|
2
|
+
import stylesheet_parser_default from "../stylesheet-parser.js";
|
|
3
|
+
//#region src/configs/stylesheet.ts
|
|
4
|
+
var stylesheet_default = {
|
|
5
|
+
files: ["**/*.{css,scss,less}"],
|
|
6
|
+
plugins: { "@master/css": plugin },
|
|
7
|
+
languageOptions: { parser: stylesheet_parser_default },
|
|
8
|
+
rules: { "@master/css/prefer-canonical-classes": "warn" }
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { stylesheet_default as default };
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -3,13 +3,15 @@ import plugin from "./plugin.js";
|
|
|
3
3
|
import base_default from "./configs/base.js";
|
|
4
4
|
import recommended_default from "./configs/recommended.js";
|
|
5
5
|
import legacy_default from "./configs/legacy.js";
|
|
6
|
+
import stylesheet_default from "./configs/stylesheet.js";
|
|
6
7
|
//#region src/index.ts
|
|
7
8
|
const masterCSSPlugin = {
|
|
8
9
|
...plugin,
|
|
9
10
|
configs: {
|
|
10
11
|
legacy: legacy_default,
|
|
11
12
|
base: base_default,
|
|
12
|
-
recommended: recommended_default
|
|
13
|
+
recommended: recommended_default,
|
|
14
|
+
stylesheet: stylesheet_default
|
|
13
15
|
},
|
|
14
16
|
settings
|
|
15
17
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import create_rule_default from "../create-rule.js";
|
|
2
2
|
import defineVisitors from "../utils/define-visitors.js";
|
|
3
3
|
import resolveContext from "../utils/resolve-context.js";
|
|
4
|
-
import
|
|
4
|
+
import reportLintDiagnostics from "../utils/report-lint-diagnostics.js";
|
|
5
|
+
import { createConflictingClassesReport } from "@master/css-lint";
|
|
5
6
|
//#region src/rules/no-conflicting-classes.ts
|
|
6
7
|
var no_conflicting_classes_default = create_rule_default({
|
|
7
8
|
name: "no-conflicting-classes",
|
|
@@ -21,54 +22,8 @@ var no_conflicting_classes_default = create_rule_default({
|
|
|
21
22
|
return defineVisitors({
|
|
22
23
|
context,
|
|
23
24
|
settings
|
|
24
|
-
}, (
|
|
25
|
-
|
|
26
|
-
if (conflicts.length) {
|
|
27
|
-
const classNamesToRemove = conflicts.map(({ className }) => className);
|
|
28
|
-
const keptClassNames = [...new Set(conflicts.flatMap(({ conflicts }) => conflicts))];
|
|
29
|
-
const formatClassName = (className) => `"${className}"`;
|
|
30
|
-
const formatClassList = (classNames) => {
|
|
31
|
-
if (classNames.length <= 2) return classNames.map(formatClassName).join(" and ");
|
|
32
|
-
return `${classNames.slice(0, -1).map(formatClassName).join(", ")}, and ${formatClassName(classNames[classNames.length - 1])}`;
|
|
33
|
-
};
|
|
34
|
-
const fixedRaw = removeClassNamesFromClassList(raw, classNamesToRemove, { unescape });
|
|
35
|
-
const firstClassNode = classNodes.find((node) => node.value === classNamesToRemove[0]);
|
|
36
|
-
context.report({
|
|
37
|
-
loc: firstClassNode.loc,
|
|
38
|
-
messageId: "collisionClass",
|
|
39
|
-
data: { message: `${formatClassList(classNamesToRemove)} ${classNamesToRemove.length === 1 ? "is" : "are"} overridden by ${formatClassList(keptClassNames)}.` },
|
|
40
|
-
fix: function(fixer) {
|
|
41
|
-
return fixer.replaceTextRange([start, end], fixedRaw);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const partialConflicts = findPartialClassConflicts(classValues, css);
|
|
47
|
-
if (!partialConflicts.length) return;
|
|
48
|
-
let fixedRaw = raw;
|
|
49
|
-
for (const conflict of partialConflicts) fixedRaw = replaceClassNameInClassList(fixedRaw, conflict.className, conflict.replacement, { unescape });
|
|
50
|
-
const classNodeQueues = /* @__PURE__ */ new Map();
|
|
51
|
-
for (const classNode of classNodes) {
|
|
52
|
-
const queue = classNodeQueues.get(classNode.value);
|
|
53
|
-
if (queue) queue.push(classNode);
|
|
54
|
-
else classNodeQueues.set(classNode.value, [classNode]);
|
|
55
|
-
}
|
|
56
|
-
for (const conflict of partialConflicts) {
|
|
57
|
-
const classNode = classNodeQueues.get(conflict.className)?.shift();
|
|
58
|
-
if (!classNode) continue;
|
|
59
|
-
context.report({
|
|
60
|
-
loc: classNode.loc,
|
|
61
|
-
messageId: "partialCollisionClass",
|
|
62
|
-
data: {
|
|
63
|
-
actual: conflict.className,
|
|
64
|
-
replacement: conflict.replacement,
|
|
65
|
-
conflict: conflict.conflict
|
|
66
|
-
},
|
|
67
|
-
fix(fixer) {
|
|
68
|
-
return fixer.replaceTextRange([start, end], fixedRaw);
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
25
|
+
}, (node, resolved) => {
|
|
26
|
+
reportLintDiagnostics(context, node, resolved, createConflictingClassesReport(resolved.raw, css, { unescape: resolved.unescape }).diagnostics);
|
|
72
27
|
});
|
|
73
28
|
}
|
|
74
29
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import create_rule_default from "../create-rule.js";
|
|
2
2
|
import defineVisitors from "../utils/define-visitors.js";
|
|
3
3
|
import resolveContext from "../utils/resolve-context.js";
|
|
4
|
+
import reportLintDiagnostics from "../utils/report-lint-diagnostics.js";
|
|
4
5
|
import { noInvalidClassesOptionsSchema } from "../settings-schema.js";
|
|
5
|
-
import {
|
|
6
|
+
import { createInvalidClassesReport } from "@master/css-lint";
|
|
6
7
|
//#region src/rules/no-invalid-classes.ts
|
|
7
8
|
var no_invalid_classes_default = create_rule_default({
|
|
8
9
|
name: "no-invalid-classes",
|
|
@@ -22,18 +23,11 @@ var no_invalid_classes_default = create_rule_default({
|
|
|
22
23
|
return defineVisitors({
|
|
23
24
|
context,
|
|
24
25
|
settings
|
|
25
|
-
}, (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
for (const issue of issues) context.report({
|
|
32
|
-
loc: node.loc,
|
|
33
|
-
messageId: issue.kind === "invalid" ? "invalidClass" : "disallowUnknownClass",
|
|
34
|
-
data: { message: issue.message }
|
|
35
|
-
});
|
|
36
|
-
}
|
|
26
|
+
}, (node, resolved) => {
|
|
27
|
+
reportLintDiagnostics(context, node, resolved, createInvalidClassesReport(resolved.raw, css, {
|
|
28
|
+
unescape: resolved.unescape,
|
|
29
|
+
disallowUnknownClass: options.disallowUnknownClass
|
|
30
|
+
}).diagnostics);
|
|
37
31
|
});
|
|
38
32
|
}
|
|
39
33
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import create_rule_default from "../create-rule.js";
|
|
2
2
|
import defineVisitors from "../utils/define-visitors.js";
|
|
3
3
|
import resolveContext from "../utils/resolve-context.js";
|
|
4
|
-
import
|
|
4
|
+
import reportLintDiagnostics from "../utils/report-lint-diagnostics.js";
|
|
5
|
+
import { createUnapprovedRawValueClassesReport } from "@master/css-lint";
|
|
5
6
|
//#region src/rules/no-unapproved-raw-values.ts
|
|
6
7
|
var no_unapproved_raw_values_default = create_rule_default({
|
|
7
8
|
name: "no-unapproved-raw-values",
|
|
@@ -39,21 +40,11 @@ var no_unapproved_raw_values_default = create_rule_default({
|
|
|
39
40
|
return defineVisitors({
|
|
40
41
|
context,
|
|
41
42
|
settings
|
|
42
|
-
}, (node,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
context.report({
|
|
48
|
-
node,
|
|
49
|
-
loc: classNode.loc,
|
|
50
|
-
messageId: "unapprovedRawValue",
|
|
51
|
-
data: {
|
|
52
|
-
className: issue.className,
|
|
53
|
-
value: issue.value
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
43
|
+
}, (node, resolved) => {
|
|
44
|
+
reportLintDiagnostics(context, node, resolved, createUnapprovedRawValueClassesReport(resolved.raw, css, {
|
|
45
|
+
...options,
|
|
46
|
+
unescape: resolved.unescape
|
|
47
|
+
}).diagnostics);
|
|
57
48
|
});
|
|
58
49
|
}
|
|
59
50
|
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import create_rule_default from "../create-rule.js";
|
|
2
2
|
import defineVisitors from "../utils/define-visitors.js";
|
|
3
3
|
import resolveContext from "../utils/resolve-context.js";
|
|
4
|
-
import
|
|
4
|
+
import reportLintDiagnostics from "../utils/report-lint-diagnostics.js";
|
|
5
|
+
import resolveComposeDirectiveClassNodes from "../utils/resolve-compose-directive-class-nodes.js";
|
|
6
|
+
import { createCanonicalClassesReport, createCanonicalComposeDirectiveReport, defaultCanonicalClassNameOptions } from "@master/css-lint";
|
|
5
7
|
//#region src/rules/prefer-canonical-classes.ts
|
|
6
8
|
var prefer_canonical_classes_default = create_rule_default({
|
|
7
9
|
name: "prefer-canonical-classes",
|
|
@@ -19,7 +21,9 @@ var prefer_canonical_classes_default = create_rule_default({
|
|
|
19
21
|
preferVariableReferences: { type: "boolean" },
|
|
20
22
|
preferMultiValueTokens: { type: "boolean" },
|
|
21
23
|
preferCompositionUtilities: { type: "boolean" },
|
|
22
|
-
preferConditionOrder: { type: "boolean" }
|
|
24
|
+
preferConditionOrder: { type: "boolean" },
|
|
25
|
+
preferNativeDeclarationsInCompose: { type: "boolean" },
|
|
26
|
+
preferVariantBlocksInCompose: { type: "boolean" }
|
|
23
27
|
},
|
|
24
28
|
additionalProperties: false
|
|
25
29
|
}]
|
|
@@ -31,49 +35,54 @@ var prefer_canonical_classes_default = create_rule_default({
|
|
|
31
35
|
...defaultCanonicalClassNameOptions,
|
|
32
36
|
...context.options[0] || {}
|
|
33
37
|
};
|
|
34
|
-
|
|
38
|
+
const reportCanonicalClassList = (node, { raw, start, end, unescape, classNodes, classValues }) => {
|
|
39
|
+
reportLintDiagnostics(context, node, {
|
|
40
|
+
raw,
|
|
41
|
+
start,
|
|
42
|
+
end,
|
|
43
|
+
unescape,
|
|
44
|
+
classNodes,
|
|
45
|
+
classValues,
|
|
46
|
+
nodes: [],
|
|
47
|
+
value: raw
|
|
48
|
+
}, createCanonicalClassesReport(raw, css, {
|
|
49
|
+
...options,
|
|
50
|
+
unescape
|
|
51
|
+
}).diagnostics);
|
|
52
|
+
};
|
|
53
|
+
const reportCanonicalComposeDirective = (node, classNode) => {
|
|
54
|
+
reportLintDiagnostics(context, node, classNode, createCanonicalComposeDirectiveReport(classNode.raw, css, options).diagnostics, { getFix(_, fix) {
|
|
55
|
+
if (fix.scope !== "directive") return {
|
|
56
|
+
range: [classNode.start + fix.range.start, classNode.start + fix.range.end],
|
|
57
|
+
text: fix.text
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
range: [classNode.directiveStart, classNode.directiveEnd],
|
|
61
|
+
text: formatComposeDirectiveReplacement(context.sourceCode.getText(), classNode.directiveStart, fix.text)
|
|
62
|
+
};
|
|
63
|
+
} });
|
|
64
|
+
};
|
|
65
|
+
const visitors = defineVisitors({
|
|
35
66
|
context,
|
|
36
67
|
settings
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
if (!firstClassNode) continue;
|
|
45
|
-
fixedRaw = replaceClassGroupInClassList(fixedRaw, suggestion.classNames, suggestion.recommended, { unescape });
|
|
46
|
-
reports.push({
|
|
47
|
-
loc: firstClassNode.loc,
|
|
48
|
-
actual: suggestion.classNames.join(" "),
|
|
49
|
-
recommended: suggestion.recommended
|
|
50
|
-
});
|
|
68
|
+
}, reportCanonicalClassList);
|
|
69
|
+
const visitProgram = visitors.Program;
|
|
70
|
+
return {
|
|
71
|
+
...visitors,
|
|
72
|
+
Program(node) {
|
|
73
|
+
if (typeof visitProgram === "function") visitProgram(node);
|
|
74
|
+
for (const classNode of resolveComposeDirectiveClassNodes(context)) reportCanonicalComposeDirective(node, classNode);
|
|
51
75
|
}
|
|
52
|
-
|
|
53
|
-
if (coveredClassNames.has(classNode.value)) continue;
|
|
54
|
-
const recommended = suggestCanonicalClassName(classNode.value, css, options);
|
|
55
|
-
if (!recommended) continue;
|
|
56
|
-
fixedRaw = replaceClassNameInClassList(fixedRaw, classNode.value, recommended, { unescape });
|
|
57
|
-
reports.push({
|
|
58
|
-
loc: classNode.loc,
|
|
59
|
-
actual: classNode.value,
|
|
60
|
-
recommended
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
for (const report of reports) context.report({
|
|
64
|
-
node,
|
|
65
|
-
loc: report.loc,
|
|
66
|
-
messageId: "preferClass",
|
|
67
|
-
data: {
|
|
68
|
-
actual: report.actual,
|
|
69
|
-
recommended: report.recommended
|
|
70
|
-
},
|
|
71
|
-
fix(fixer) {
|
|
72
|
-
return fixer.replaceTextRange([start, end], fixedRaw);
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
+
};
|
|
76
77
|
}
|
|
77
78
|
});
|
|
79
|
+
function getLinePrefix(source, index) {
|
|
80
|
+
return source.slice(source.lastIndexOf("\n", index - 1) + 1, index);
|
|
81
|
+
}
|
|
82
|
+
function formatComposeDirectiveReplacement(source, index, replacement) {
|
|
83
|
+
const linePrefix = getLinePrefix(source, index);
|
|
84
|
+
if (/\S/.test(linePrefix)) return replacement.replace(/\n\s*/g, " ");
|
|
85
|
+
return replacement.replace(/\n/g, `\n${linePrefix}`);
|
|
86
|
+
}
|
|
78
87
|
//#endregion
|
|
79
88
|
export { prefer_canonical_classes_default as default };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import create_rule_default from "../create-rule.js";
|
|
2
2
|
import defineVisitors from "../utils/define-visitors.js";
|
|
3
3
|
import resolveContext from "../utils/resolve-context.js";
|
|
4
|
-
import
|
|
4
|
+
import reportLintDiagnostics from "../utils/report-lint-diagnostics.js";
|
|
5
|
+
import { createSortClassesReport } from "@master/css-lint";
|
|
5
6
|
//#region src/rules/sort-classes.ts
|
|
6
7
|
var sort_classes_default = create_rule_default({
|
|
7
8
|
name: "sort-classes",
|
|
@@ -15,24 +16,21 @@ var sort_classes_default = create_rule_default({
|
|
|
15
16
|
defaultOptions: [],
|
|
16
17
|
create: function(context) {
|
|
17
18
|
const { settings, css } = resolveContext(context);
|
|
18
|
-
const { sourceCode } = context;
|
|
19
19
|
return defineVisitors({
|
|
20
20
|
context,
|
|
21
21
|
settings
|
|
22
22
|
}, (node, { raw, start, end, nodes, unescape }) => {
|
|
23
23
|
if (nodes.length <= 1) return;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
});
|
|
24
|
+
reportLintDiagnostics(context, node, {
|
|
25
|
+
raw,
|
|
26
|
+
start,
|
|
27
|
+
end,
|
|
28
|
+
nodes,
|
|
29
|
+
unescape,
|
|
30
|
+
value: raw,
|
|
31
|
+
classNodes: [],
|
|
32
|
+
classValues: []
|
|
33
|
+
}, createSortClassesReport(raw, css, { unescape }).diagnostics);
|
|
36
34
|
});
|
|
37
35
|
}
|
|
38
36
|
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/stylesheet-parser.d.ts
|
|
2
|
+
declare const _default: {
|
|
3
|
+
meta: {
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
parseForESLint(source: string): {
|
|
7
|
+
ast: {
|
|
8
|
+
type: string;
|
|
9
|
+
body: any[];
|
|
10
|
+
comments: any[];
|
|
11
|
+
tokens: any[];
|
|
12
|
+
sourceType: string;
|
|
13
|
+
range: number[];
|
|
14
|
+
loc: {
|
|
15
|
+
start: {
|
|
16
|
+
line: number;
|
|
17
|
+
column: number;
|
|
18
|
+
};
|
|
19
|
+
end: {
|
|
20
|
+
line: number;
|
|
21
|
+
column: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
visitorKeys: {
|
|
26
|
+
Program: any[];
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { _default as default };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/stylesheet-parser.ts
|
|
2
|
+
function getLocFromIndex(source, index) {
|
|
3
|
+
const lines = source.slice(0, index).split(/\r\n|[\n\r]/);
|
|
4
|
+
return {
|
|
5
|
+
line: lines.length,
|
|
6
|
+
column: lines[lines.length - 1].length
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
var stylesheet_parser_default = {
|
|
10
|
+
meta: { name: "@master/eslint-plugin-css/stylesheet-parser" },
|
|
11
|
+
parseForESLint(source) {
|
|
12
|
+
return {
|
|
13
|
+
ast: {
|
|
14
|
+
type: "Program",
|
|
15
|
+
body: [],
|
|
16
|
+
comments: [],
|
|
17
|
+
tokens: [],
|
|
18
|
+
sourceType: "module",
|
|
19
|
+
range: [0, source.length],
|
|
20
|
+
loc: {
|
|
21
|
+
start: {
|
|
22
|
+
line: 1,
|
|
23
|
+
column: 0
|
|
24
|
+
},
|
|
25
|
+
end: getLocFromIndex(source, source.length)
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
visitorKeys: { Program: [] }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { stylesheet_parser_default as default };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import { MasterCSSLintDiagnostic, MasterCSSLintFix } from "@master/css-lint";
|
|
3
|
+
import { RuleContext } from "@typescript-eslint/utils/ts-eslint";
|
|
4
|
+
import { ResolvedClassNode } from "./resolve-class-node.js";
|
|
5
|
+
|
|
6
|
+
//#region src/utils/report-lint-diagnostics.d.ts
|
|
7
|
+
type FixRange = [number, number];
|
|
8
|
+
interface ReportLintDiagnosticsOptions {
|
|
9
|
+
getFix?: (diagnostic: MasterCSSLintDiagnostic, fix: MasterCSSLintFix) => {
|
|
10
|
+
range: FixRange;
|
|
11
|
+
text: string;
|
|
12
|
+
} | undefined;
|
|
13
|
+
}
|
|
14
|
+
declare function reportLintDiagnostics(context: RuleContext<any, any[]>, node: TSESTree.Node, resolved: ResolvedClassNode, diagnostics: MasterCSSLintDiagnostic[], options?: ReportLintDiagnosticsOptions): void;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { reportLintDiagnostics as default };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//#region src/utils/report-lint-diagnostics.ts
|
|
2
|
+
const messageIdByCode = {
|
|
3
|
+
"invalid-class-order": "invalidClassOrder",
|
|
4
|
+
"invalid-class": "invalidClass",
|
|
5
|
+
"unknown-class": "disallowUnknownClass",
|
|
6
|
+
"conflicting-class": "collisionClass",
|
|
7
|
+
"partially-conflicting-class": "partialCollisionClass",
|
|
8
|
+
"prefer-canonical-class": "preferClass",
|
|
9
|
+
"prefer-native-declaration": "preferClass",
|
|
10
|
+
"prefer-variant-block": "preferClass",
|
|
11
|
+
"unapproved-raw-value": "unapprovedRawValue"
|
|
12
|
+
};
|
|
13
|
+
function stringifyData(data = {}) {
|
|
14
|
+
return Object.fromEntries(Object.entries(data).filter(([, value]) => value !== void 0).map(([key, value]) => [key, Array.isArray(value) ? value.join(", ") : String(value)]));
|
|
15
|
+
}
|
|
16
|
+
function defaultFix(resolved, fix) {
|
|
17
|
+
if (fix.scope === "directive") return;
|
|
18
|
+
return {
|
|
19
|
+
range: [resolved.start + fix.range.start, resolved.start + fix.range.end],
|
|
20
|
+
text: fix.text
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function reportLintDiagnostics(context, node, resolved, diagnostics, options = {}) {
|
|
24
|
+
const { sourceCode } = context;
|
|
25
|
+
for (const diagnostic of diagnostics) {
|
|
26
|
+
const start = resolved.start + diagnostic.range.start;
|
|
27
|
+
const end = resolved.start + diagnostic.range.end;
|
|
28
|
+
const messageId = messageIdByCode[diagnostic.code];
|
|
29
|
+
const fix = diagnostic.fix ? options.getFix?.(diagnostic, diagnostic.fix) || defaultFix(resolved, diagnostic.fix) : void 0;
|
|
30
|
+
const descriptor = {
|
|
31
|
+
node,
|
|
32
|
+
loc: {
|
|
33
|
+
start: sourceCode.getLocFromIndex(start),
|
|
34
|
+
end: sourceCode.getLocFromIndex(end)
|
|
35
|
+
},
|
|
36
|
+
...messageId ? {
|
|
37
|
+
messageId,
|
|
38
|
+
data: stringifyData(diagnostic.data || { message: diagnostic.message })
|
|
39
|
+
} : { message: diagnostic.message }
|
|
40
|
+
};
|
|
41
|
+
if (fix) descriptor.fix = (fixer) => fixer.replaceTextRange(fix.range, fix.text);
|
|
42
|
+
context.report(descriptor);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { reportLintDiagnostics as default };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RuleContext } from "@typescript-eslint/utils/ts-eslint";
|
|
2
|
+
import { ResolvedClassNode } from "./resolve-class-node.js";
|
|
3
|
+
|
|
4
|
+
//#region src/utils/resolve-compose-directive-class-nodes.d.ts
|
|
5
|
+
interface ResolvedComposeDirectiveClassNode extends ResolvedClassNode {
|
|
6
|
+
directiveStart: number;
|
|
7
|
+
directiveEnd: number;
|
|
8
|
+
}
|
|
9
|
+
declare function resolveComposeDirectiveClassNodes(context: RuleContext<any, any[]>): ResolvedComposeDirectiveClassNode[];
|
|
10
|
+
//#endregion
|
|
11
|
+
export { ResolvedComposeDirectiveClassNode, resolveComposeDirectiveClassNodes as default };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { collectCSSDirectiveRanges, parseMasterCSSClassList } from "@master/css-lexer";
|
|
2
|
+
//#region src/utils/resolve-compose-directive-class-nodes.ts
|
|
3
|
+
const CSS_LIKE_FILE_RE = /\.(?:css|scss|sass|less|pcss|postcss)$/i;
|
|
4
|
+
const CSS_LINE_COMMENT_FILE_RE = /\.(?:scss|sass|less)$/i;
|
|
5
|
+
const STYLE_CONTAINER_FILE_RE = /\.(?:vue|svelte|astro|html|htm|mdx)$/i;
|
|
6
|
+
const STYLE_LINE_COMMENT_RE = /\blang\s*=\s*(?:"(?:scss|sass|less)"|'(?:scss|sass|less)'|(?:scss|sass|less)\b)/i;
|
|
7
|
+
function getFilename(context) {
|
|
8
|
+
return (context.filename || context.getFilename?.() || "").split(/[?#]/)[0];
|
|
9
|
+
}
|
|
10
|
+
function isWhitespace(char) {
|
|
11
|
+
return char !== void 0 && /\s/.test(char);
|
|
12
|
+
}
|
|
13
|
+
function findHTMLTagEnd(source, start) {
|
|
14
|
+
let quote = "";
|
|
15
|
+
for (let index = start; index < source.length; index++) {
|
|
16
|
+
const char = source[index];
|
|
17
|
+
if (quote) {
|
|
18
|
+
if (char === quote) quote = "";
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (char === "\"" || char === "'") {
|
|
22
|
+
quote = char;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (char === ">") return index + 1;
|
|
26
|
+
}
|
|
27
|
+
return -1;
|
|
28
|
+
}
|
|
29
|
+
function collectStyleContentRanges(source) {
|
|
30
|
+
const ranges = [];
|
|
31
|
+
const openStylePattern = /<style\b/gi;
|
|
32
|
+
let match;
|
|
33
|
+
while (match = openStylePattern.exec(source)) {
|
|
34
|
+
const openStart = match.index;
|
|
35
|
+
const openEnd = findHTMLTagEnd(source, openStart);
|
|
36
|
+
if (openEnd === -1) break;
|
|
37
|
+
const openTag = source.slice(openStart, openEnd);
|
|
38
|
+
const closeMatch = /<\/style\s*>/i.exec(source.slice(openEnd));
|
|
39
|
+
const closeStart = closeMatch ? openEnd + closeMatch.index : source.length;
|
|
40
|
+
ranges.push({
|
|
41
|
+
start: openEnd,
|
|
42
|
+
end: closeStart,
|
|
43
|
+
lineComments: STYLE_LINE_COMMENT_RE.test(openTag)
|
|
44
|
+
});
|
|
45
|
+
openStylePattern.lastIndex = closeMatch ? closeStart + closeMatch[0].length : source.length;
|
|
46
|
+
}
|
|
47
|
+
return ranges;
|
|
48
|
+
}
|
|
49
|
+
function collectLineCommentRanges(source, start, end) {
|
|
50
|
+
const ranges = [];
|
|
51
|
+
let quote = "";
|
|
52
|
+
let blockComment = false;
|
|
53
|
+
for (let index = start; index < end; index++) {
|
|
54
|
+
const char = source[index];
|
|
55
|
+
const next = source[index + 1];
|
|
56
|
+
if (blockComment) {
|
|
57
|
+
if (char === "*" && next === "/") {
|
|
58
|
+
blockComment = false;
|
|
59
|
+
index++;
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (quote) {
|
|
64
|
+
if (char === "\\") index++;
|
|
65
|
+
else if (char === quote) quote = "";
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (char === "/" && next === "*") {
|
|
69
|
+
blockComment = true;
|
|
70
|
+
index++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (char === "\"" || char === "'") {
|
|
74
|
+
quote = char;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (char === "/" && next === "/") {
|
|
78
|
+
const lineEnd = source.indexOf("\n", index + 2);
|
|
79
|
+
const commentEnd = lineEnd === -1 ? end : Math.min(lineEnd, end);
|
|
80
|
+
ranges.push({
|
|
81
|
+
start: index,
|
|
82
|
+
end: commentEnd
|
|
83
|
+
});
|
|
84
|
+
index = commentEnd;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return ranges;
|
|
88
|
+
}
|
|
89
|
+
function overlapsRange(start, end, ranges) {
|
|
90
|
+
return ranges.some((range) => start < range.end && end > range.start);
|
|
91
|
+
}
|
|
92
|
+
function collectCandidateRanges(source, filename) {
|
|
93
|
+
if (CSS_LIKE_FILE_RE.test(filename)) return [{
|
|
94
|
+
start: 0,
|
|
95
|
+
end: source.length,
|
|
96
|
+
lineComments: CSS_LINE_COMMENT_FILE_RE.test(filename)
|
|
97
|
+
}];
|
|
98
|
+
if (STYLE_CONTAINER_FILE_RE.test(filename)) return collectStyleContentRanges(source);
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
function resolveComposeDirectiveClassNode(source, sourceCode, start, end) {
|
|
102
|
+
while (start < end && isWhitespace(source[start])) start++;
|
|
103
|
+
while (end > start && isWhitespace(source[end - 1])) end--;
|
|
104
|
+
if (start >= end) return;
|
|
105
|
+
const raw = source.slice(start, end);
|
|
106
|
+
const nodes = parseMasterCSSClassList(raw, { preserveSpaces: true }).map((item) => {
|
|
107
|
+
const startOffset = start + item.start;
|
|
108
|
+
const endOffset = start + item.end;
|
|
109
|
+
return {
|
|
110
|
+
type: item.type,
|
|
111
|
+
value: item.token,
|
|
112
|
+
raw: item.raw,
|
|
113
|
+
range: [startOffset, endOffset],
|
|
114
|
+
loc: {
|
|
115
|
+
start: sourceCode.getLocFromIndex(startOffset),
|
|
116
|
+
end: sourceCode.getLocFromIndex(endOffset)
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
const classNodes = nodes.filter((node) => node.type === "class");
|
|
121
|
+
if (!classNodes.length) return;
|
|
122
|
+
return {
|
|
123
|
+
nodes,
|
|
124
|
+
start,
|
|
125
|
+
end,
|
|
126
|
+
raw,
|
|
127
|
+
value: raw,
|
|
128
|
+
unescape: false,
|
|
129
|
+
classNodes,
|
|
130
|
+
classValues: classNodes.map((node) => node.value)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function resolveComposeDirectiveClassNodes(context) {
|
|
134
|
+
const filename = getFilename(context);
|
|
135
|
+
const source = context.sourceCode.getText();
|
|
136
|
+
const nodes = [];
|
|
137
|
+
for (const range of collectCandidateRanges(source, filename)) {
|
|
138
|
+
const lineCommentRanges = range.lineComments ? collectLineCommentRanges(source, range.start, range.end) : [];
|
|
139
|
+
const rangeSource = source.slice(range.start, range.end);
|
|
140
|
+
for (const directive of collectCSSDirectiveRanges(rangeSource)) {
|
|
141
|
+
if (directive.name !== "compose") continue;
|
|
142
|
+
if (directive.blockRange || directive.quotedStringRanges.length) continue;
|
|
143
|
+
if (overlapsRange(range.start + directive.keywordRange.start, range.start + directive.keywordRange.end, lineCommentRanges)) continue;
|
|
144
|
+
const classNode = resolveComposeDirectiveClassNode(source, context.sourceCode, range.start + directive.preludeRange.start, range.start + directive.preludeRange.end);
|
|
145
|
+
if (!classNode) continue;
|
|
146
|
+
if (overlapsRange(classNode.start, classNode.end, lineCommentRanges)) continue;
|
|
147
|
+
nodes.push({
|
|
148
|
+
...classNode,
|
|
149
|
+
directiveStart: range.start + directive.start,
|
|
150
|
+
directiveEnd: range.start + (directive.semicolonRange?.end ?? directive.end)
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return nodes;
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
export { resolveComposeDirectiveClassNodes as default };
|
package/package.json
CHANGED
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@typescript-eslint/utils": "^8.61.1",
|
|
49
49
|
"typescript-eslint": "^8.61.1",
|
|
50
|
-
"@master/css": "^2.0.0-rc.
|
|
51
|
-
"@master/css-lexer": "^2.0.0-rc.
|
|
52
|
-
"@master/css-lint": "^2.0.0-rc.
|
|
53
|
-
"@master/css-project": "^2.0.0-rc.
|
|
54
|
-
"@master/css-preset": "^2.0.0-rc.
|
|
55
|
-
"@master/css-validator": "^2.0.0-rc.
|
|
50
|
+
"@master/css": "^2.0.0-rc.76",
|
|
51
|
+
"@master/css-lexer": "^2.0.0-rc.76",
|
|
52
|
+
"@master/css-lint": "^2.0.0-rc.76",
|
|
53
|
+
"@master/css-project": "^2.0.0-rc.76",
|
|
54
|
+
"@master/css-preset": "^2.0.0-rc.76",
|
|
55
|
+
"@master/css-validator": "^2.0.0-rc.76"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@angular-eslint/template-parser": "^21.4.0",
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"svelte-eslint-parser": "^1.8.0",
|
|
66
66
|
"ts-node": "^10.9.2",
|
|
67
67
|
"vue-eslint-parser": "^10.4.1",
|
|
68
|
-
"@master/css-schema": "^2.0.0-rc.
|
|
68
|
+
"@master/css-schema": "^2.0.0-rc.76"
|
|
69
69
|
},
|
|
70
|
-
"version": "2.0.0-rc.
|
|
70
|
+
"version": "2.0.0-rc.76",
|
|
71
71
|
"scripts": {
|
|
72
72
|
"build": "tsdown --config ../../shared/tsdown-package.config.ts",
|
|
73
73
|
"dev": "tsdown --config ../../shared/tsdown-package.config.ts --watch",
|