@dvukovic/style-guide 0.18.1 → 0.19.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/dist/stylelint/rules/no-unused-selectors/no-unused-selectors.d.ts +3 -0
- package/dist/stylelint/rules/no-unused-selectors/no-unused-selectors.types.d.ts +13 -0
- package/dist/stylelint/rules/no-unused-selectors/no-unused-selectors.utils.d.ts +26 -0
- package/package.json +10 -5
- package/src/stylelint/configs/core.js +3 -3
- package/src/stylelint/rules/no-unused-selectors/fixtures/basic/Component.tsx +5 -0
- package/src/stylelint/rules/no-unused-selectors/fixtures/bracket-access/Dynamic.tsx +5 -0
- package/src/stylelint/rules/no-unused-selectors/fixtures/class-name-attribute/Static.tsx +3 -0
- package/src/stylelint/rules/no-unused-selectors/fixtures/compound-selectors/Layout.tsx +9 -0
- package/src/stylelint/rules/no-unused-selectors/fixtures/cva-pattern/Card.tsx +6 -0
- package/src/stylelint/rules/no-unused-selectors/fixtures/cva-pattern/Card.variants.ts +12 -0
- package/src/stylelint/rules/no-unused-selectors/fixtures/id-attribute/Panel.tsx +3 -0
- package/src/stylelint/rules/no-unused-selectors/fixtures/multi-document/Button.tsx +6 -0
- package/src/stylelint/rules/no-unused-selectors/fixtures/multi-document/Button.variants.ts +12 -0
- package/src/stylelint/rules/no-unused-selectors/fixtures/pseudo-classes/Link.tsx +5 -0
- package/src/stylelint/rules/no-unused-selectors/fixtures/utility-libraries/Widget.tsx +5 -0
- package/src/stylelint/rules/no-unused-selectors/no-unused-selectors.js +125 -0
- package/src/stylelint/rules/no-unused-selectors/no-unused-selectors.types.ts +15 -0
- package/src/stylelint/rules/no-unused-selectors/no-unused-selectors.utils.js +528 -0
- package/dist/stylelint/plugins/no-unused-selectors.d.ts +0 -3
- package/src/stylelint/plugins/no-unused-selectors.js +0 -30
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type TemplateVariablesType = {
|
|
2
|
+
cssBaseName: string;
|
|
3
|
+
cssDirectory: string;
|
|
4
|
+
cssDirectoryName: string;
|
|
5
|
+
cssName: string;
|
|
6
|
+
};
|
|
7
|
+
export type ResolvedDocumentType = {
|
|
8
|
+
content: string;
|
|
9
|
+
path: string;
|
|
10
|
+
};
|
|
11
|
+
export type SecondaryOptionsType = {
|
|
12
|
+
documents?: string[];
|
|
13
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const DEFAULT_DOCUMENT_TEMPLATES: string[];
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} sourceContent
|
|
4
|
+
* @param {string} filePath
|
|
5
|
+
* @returns {Set<string>}
|
|
6
|
+
*/
|
|
7
|
+
export function extractClassReferences(sourceContent: string, filePath: string): Set<string>;
|
|
8
|
+
/**
|
|
9
|
+
* @param {string} resolvedSelector
|
|
10
|
+
* @returns {{ type: "class" | "id"; value: string } | null}
|
|
11
|
+
*/
|
|
12
|
+
export function extractSimpleSelector(resolvedSelector: string): {
|
|
13
|
+
type: "class" | "id";
|
|
14
|
+
value: string;
|
|
15
|
+
} | null;
|
|
16
|
+
/**
|
|
17
|
+
* @param {string[]} documentPaths
|
|
18
|
+
* @returns {import("./no-unused-selectors.types.ts").ResolvedDocumentType[]}
|
|
19
|
+
*/
|
|
20
|
+
export function resolveAllDocuments(documentPaths: string[]): import("./no-unused-selectors.types.ts").ResolvedDocumentType[];
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} cssPath
|
|
23
|
+
* @param {string[]} templates
|
|
24
|
+
* @returns {string[]}
|
|
25
|
+
*/
|
|
26
|
+
export function resolveDocumentPaths(cssPath: string, templates: string[]): string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dvukovic/style-guide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "My own style guide",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"@stylistic/eslint-plugin": "5.7.1",
|
|
69
69
|
"@tanstack/eslint-plugin-query": "5.91.4",
|
|
70
70
|
"@typescript-eslint/parser": "8.53.1",
|
|
71
|
-
"@vitest/eslint-plugin": "1.6.
|
|
71
|
+
"@vitest/eslint-plugin": "1.6.9",
|
|
72
72
|
"eslint-plugin-baseline-js": "0.4.2",
|
|
73
73
|
"eslint-plugin-import-x": "4.16.1",
|
|
74
74
|
"eslint-plugin-jest": "29.12.1",
|
|
@@ -91,6 +91,8 @@
|
|
|
91
91
|
"eslint-plugin-unused-imports": "4.3.0",
|
|
92
92
|
"globals": "17.1.0",
|
|
93
93
|
"jsonc-eslint-parser": "2.4.2",
|
|
94
|
+
"postcss-resolve-nested-selector": "0.1.6",
|
|
95
|
+
"postcss-selector-parser": "7.1.1",
|
|
94
96
|
"prettier-plugin-embed": "0.5.1",
|
|
95
97
|
"prettier-plugin-jsdoc": "1.8.0",
|
|
96
98
|
"prettier-plugin-packagejson": "3.0.0",
|
|
@@ -99,7 +101,6 @@
|
|
|
99
101
|
"prettier-plugin-sql": "0.19.2",
|
|
100
102
|
"prettier-plugin-tailwindcss": "0.7.2",
|
|
101
103
|
"prettier-plugin-toml": "2.0.6",
|
|
102
|
-
"stylelint-no-unused-selectors": "1.0.40",
|
|
103
104
|
"stylelint-order": "7.0.1",
|
|
104
105
|
"typescript-eslint": "8.53.1"
|
|
105
106
|
},
|
|
@@ -123,7 +124,8 @@
|
|
|
123
124
|
"eslint": "^9",
|
|
124
125
|
"knip": "5",
|
|
125
126
|
"prettier": "3",
|
|
126
|
-
"stylelint": "16"
|
|
127
|
+
"stylelint": "16",
|
|
128
|
+
"typescript": "5"
|
|
127
129
|
},
|
|
128
130
|
"peerDependenciesMeta": {
|
|
129
131
|
"cspell": {
|
|
@@ -140,9 +142,12 @@
|
|
|
140
142
|
},
|
|
141
143
|
"stylelint": {
|
|
142
144
|
"optional": true
|
|
145
|
+
},
|
|
146
|
+
"typescript": {
|
|
147
|
+
"optional": true
|
|
143
148
|
}
|
|
144
149
|
},
|
|
145
|
-
"packageManager": "yarn@4.
|
|
150
|
+
"packageManager": "yarn@4.13.0",
|
|
146
151
|
"engines": {
|
|
147
152
|
"node": ">=24.0.0"
|
|
148
153
|
},
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import stylelintNoUnusedSelectors from "../plugins/no-unused-selectors.js"
|
|
2
1
|
import stylelintOrder from "../plugins/order.js"
|
|
3
2
|
import stylelint from "../plugins/stylelint.js"
|
|
3
|
+
import noUnusedSelectors from "../rules/no-unused-selectors/no-unused-selectors.js"
|
|
4
4
|
|
|
5
5
|
/** @type {import("stylelint").Config} */
|
|
6
6
|
const config = {
|
|
7
7
|
allowEmptyInput: true,
|
|
8
8
|
defaultSeverity: "error",
|
|
9
|
-
plugins: ["stylelint-order",
|
|
9
|
+
plugins: ["stylelint-order", noUnusedSelectors],
|
|
10
10
|
reportDescriptionlessDisables: true,
|
|
11
11
|
reportInvalidScopeDisables: true,
|
|
12
12
|
reportNeedlessDisables: true,
|
|
@@ -14,7 +14,7 @@ const config = {
|
|
|
14
14
|
rules: {
|
|
15
15
|
...stylelint.rules,
|
|
16
16
|
...stylelintOrder.rules,
|
|
17
|
-
|
|
17
|
+
"dvukovic/no-unused-selectors": true,
|
|
18
18
|
},
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import resolveNestedSelector from "postcss-resolve-nested-selector"
|
|
2
|
+
import stylelint from "stylelint"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_DOCUMENT_TEMPLATES,
|
|
6
|
+
extractClassReferences,
|
|
7
|
+
extractSimpleSelector,
|
|
8
|
+
resolveAllDocuments,
|
|
9
|
+
resolveDocumentPaths,
|
|
10
|
+
} from "./no-unused-selectors.utils.js"
|
|
11
|
+
|
|
12
|
+
const ruleName = "dvukovic/no-unused-selectors"
|
|
13
|
+
|
|
14
|
+
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
15
|
+
rejected: (selector) => {
|
|
16
|
+
return `Selector "${selector}" is not used in any document`
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const meta = {
|
|
21
|
+
fixable: false,
|
|
22
|
+
url: "https://github.com/vuki656/style-guide",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isInsideKeyframes(ruleNode) {
|
|
26
|
+
const parent = ruleNode.parent
|
|
27
|
+
|
|
28
|
+
if (parent?.type !== "atrule") {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return parent.name === "keyframes"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @type {import("stylelint").Rule<
|
|
37
|
+
* boolean,
|
|
38
|
+
* import("./no-unused-selectors.types.ts").SecondaryOptionsType
|
|
39
|
+
* >}
|
|
40
|
+
*/
|
|
41
|
+
const ruleFunction = (primaryOption, secondaryOptions) => {
|
|
42
|
+
return (root, result) => {
|
|
43
|
+
const validOptions = stylelint.utils.validateOptions(
|
|
44
|
+
result,
|
|
45
|
+
ruleName,
|
|
46
|
+
{
|
|
47
|
+
actual: primaryOption,
|
|
48
|
+
possible: [true, false],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
actual: secondaryOptions,
|
|
52
|
+
optional: true,
|
|
53
|
+
possible: {
|
|
54
|
+
documents: [
|
|
55
|
+
(value) => {
|
|
56
|
+
return typeof value === "string"
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if (!validOptions || !primaryOption) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const cssFilePath = root.source?.input?.file
|
|
68
|
+
|
|
69
|
+
if (!cssFilePath) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const templates = secondaryOptions?.documents ?? DEFAULT_DOCUMENT_TEMPLATES
|
|
74
|
+
const documentPaths = resolveDocumentPaths(cssFilePath, templates)
|
|
75
|
+
const documents = resolveAllDocuments(documentPaths)
|
|
76
|
+
|
|
77
|
+
if (documents.length === 0) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const allReferences = new Set()
|
|
82
|
+
|
|
83
|
+
for (const document of documents) {
|
|
84
|
+
const documentReferences = extractClassReferences(document.content, document.path)
|
|
85
|
+
|
|
86
|
+
for (const reference of documentReferences) {
|
|
87
|
+
allReferences.add(reference)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
root.walkRules((ruleNode) => {
|
|
92
|
+
if (isInsideKeyframes(ruleNode)) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const resolvedSelectors = resolveNestedSelector(ruleNode.selector, ruleNode)
|
|
97
|
+
|
|
98
|
+
for (const resolvedSelector of resolvedSelectors) {
|
|
99
|
+
const simpleSelector = extractSimpleSelector(resolvedSelector)
|
|
100
|
+
|
|
101
|
+
if (!simpleSelector) {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (allReferences.has(simpleSelector.value)) {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
stylelint.utils.report({
|
|
110
|
+
message: messages.rejected(simpleSelector.value),
|
|
111
|
+
node: ruleNode,
|
|
112
|
+
result,
|
|
113
|
+
ruleName,
|
|
114
|
+
word: ruleNode.selector,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ruleFunction.ruleName = ruleName
|
|
122
|
+
ruleFunction.messages = messages
|
|
123
|
+
ruleFunction.meta = meta
|
|
124
|
+
|
|
125
|
+
export default stylelint.createPlugin(ruleName, ruleFunction)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type TemplateVariablesType = {
|
|
2
|
+
cssBaseName: string
|
|
3
|
+
cssDirectory: string
|
|
4
|
+
cssDirectoryName: string
|
|
5
|
+
cssName: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ResolvedDocumentType = {
|
|
9
|
+
content: string
|
|
10
|
+
path: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type SecondaryOptionsType = {
|
|
14
|
+
documents?: string[]
|
|
15
|
+
}
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import { createRequire } from "node:module"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
import selectorParser from "postcss-selector-parser"
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url)
|
|
8
|
+
|
|
9
|
+
let typescriptModule = null
|
|
10
|
+
|
|
11
|
+
function getTypeScript() {
|
|
12
|
+
if (typescriptModule) {
|
|
13
|
+
return typescriptModule
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
typescriptModule = require("typescript")
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return typescriptModule
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const NON_STRUCTURAL_PSEUDO_CLASSES = new Set([
|
|
26
|
+
"active",
|
|
27
|
+
"any-link",
|
|
28
|
+
"autofill",
|
|
29
|
+
"checked",
|
|
30
|
+
"default",
|
|
31
|
+
"defined",
|
|
32
|
+
"disabled",
|
|
33
|
+
"enabled",
|
|
34
|
+
"focus",
|
|
35
|
+
"focus-visible",
|
|
36
|
+
"focus-within",
|
|
37
|
+
"fullscreen",
|
|
38
|
+
"hover",
|
|
39
|
+
"in-range",
|
|
40
|
+
"indeterminate",
|
|
41
|
+
"invalid",
|
|
42
|
+
"link",
|
|
43
|
+
"modal",
|
|
44
|
+
"optional",
|
|
45
|
+
"out-of-range",
|
|
46
|
+
"placeholder-shown",
|
|
47
|
+
"read-only",
|
|
48
|
+
"read-write",
|
|
49
|
+
"required",
|
|
50
|
+
"target",
|
|
51
|
+
"valid",
|
|
52
|
+
"visited",
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
const NON_STRUCTURAL_PSEUDO_ELEMENTS = new Set([
|
|
56
|
+
"after",
|
|
57
|
+
"backdrop",
|
|
58
|
+
"before",
|
|
59
|
+
"cue",
|
|
60
|
+
"file-selector-button",
|
|
61
|
+
"first-letter",
|
|
62
|
+
"first-line",
|
|
63
|
+
"marker",
|
|
64
|
+
"placeholder",
|
|
65
|
+
"selection",
|
|
66
|
+
])
|
|
67
|
+
|
|
68
|
+
const UTILITY_LIBRARY_IMPORTS = new Set(["class-variance-authority", "classnames", "clsx"])
|
|
69
|
+
|
|
70
|
+
const UTILITY_FUNCTION_NAMES = new Set(["cn", "classnames", "clsx", "cva", "cx"])
|
|
71
|
+
|
|
72
|
+
const DEFAULT_DOCUMENT_TEMPLATES = [
|
|
73
|
+
"{cssDirectory}/{cssBaseName}.variants.ts",
|
|
74
|
+
"{cssDirectory}/{cssBaseName}.variants.tsx",
|
|
75
|
+
"{cssDirectory}/{cssBaseName}.tsx",
|
|
76
|
+
"{cssDirectory}/{cssBaseName}.jsx",
|
|
77
|
+
"{cssDirectory}/{cssDirectoryName}.tsx",
|
|
78
|
+
"{cssDirectory}/{cssDirectoryName}.jsx",
|
|
79
|
+
"{cssDirectory}/index.tsx",
|
|
80
|
+
"{cssDirectory}/index.jsx",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
function addClassReference(references, name) {
|
|
84
|
+
references.add(`.${name}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function addClassReferencesFromString(references, text) {
|
|
88
|
+
const classNames = text.split(/\s+/).filter(Boolean)
|
|
89
|
+
|
|
90
|
+
for (const className of classNames) {
|
|
91
|
+
addClassReference(references, className)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getCallArguments(callExpression) {
|
|
96
|
+
return [...callExpression.arguments] // eslint-disable-line baseline-js/use-baseline -- TS AST node property, not Function.arguments
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function collectCssModuleImports(node, cssModuleIdentifiers) {
|
|
100
|
+
const ts = typescriptModule
|
|
101
|
+
|
|
102
|
+
if (!ts.isImportDeclaration(node)) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const moduleSpecifier = node.moduleSpecifier
|
|
107
|
+
|
|
108
|
+
if (!ts.isStringLiteral(moduleSpecifier)) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!moduleSpecifier.text.endsWith(".module.css")) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const importClause = node.importClause
|
|
117
|
+
|
|
118
|
+
if (!importClause?.name) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
cssModuleIdentifiers.add(importClause.name.text)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function collectUtilityImports(node, utilityFunctionIdentifiers) {
|
|
126
|
+
const ts = typescriptModule
|
|
127
|
+
|
|
128
|
+
if (!ts.isImportDeclaration(node)) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const moduleSpecifier = node.moduleSpecifier
|
|
133
|
+
|
|
134
|
+
if (!ts.isStringLiteral(moduleSpecifier)) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const source = moduleSpecifier.text
|
|
139
|
+
const isKnownLibrary = UTILITY_LIBRARY_IMPORTS.has(source)
|
|
140
|
+
const importClause = node.importClause
|
|
141
|
+
|
|
142
|
+
if (!importClause) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (isKnownLibrary && importClause.name) {
|
|
147
|
+
utilityFunctionIdentifiers.add(importClause.name.text)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const namedBindings = importClause.namedBindings
|
|
151
|
+
|
|
152
|
+
if (!namedBindings || !ts.isNamedImports(namedBindings)) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const specifier of namedBindings.elements) {
|
|
157
|
+
const localName = specifier.name.text
|
|
158
|
+
const importedName = specifier.propertyName?.text ?? localName
|
|
159
|
+
|
|
160
|
+
if (isKnownLibrary && UTILITY_FUNCTION_NAMES.has(importedName)) {
|
|
161
|
+
utilityFunctionIdentifiers.add(localName)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!isKnownLibrary && UTILITY_FUNCTION_NAMES.has(localName)) {
|
|
165
|
+
utilityFunctionIdentifiers.add(localName)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function collectPropertyAccessReferences(node, context) {
|
|
171
|
+
const ts = typescriptModule
|
|
172
|
+
|
|
173
|
+
if (!ts.isPropertyAccessExpression(node)) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const expression = node.expression
|
|
178
|
+
|
|
179
|
+
if (!ts.isIdentifier(expression)) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!context.cssModuleIdentifiers.has(expression.text)) {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
addClassReference(context.references, node.name.text)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function collectBracketAccessReferences(node, context) {
|
|
191
|
+
const ts = typescriptModule
|
|
192
|
+
|
|
193
|
+
if (!ts.isElementAccessExpression(node)) {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const expression = node.expression
|
|
198
|
+
|
|
199
|
+
if (!ts.isIdentifier(expression)) {
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!context.cssModuleIdentifiers.has(expression.text)) {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const argument = node.argumentExpression
|
|
208
|
+
|
|
209
|
+
if (!ts.isStringLiteral(argument)) {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
addClassReference(context.references, argument.text)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function collectClassNameAttributes(node, references) {
|
|
217
|
+
const ts = typescriptModule
|
|
218
|
+
|
|
219
|
+
if (!ts.isJsxAttribute(node)) {
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (node.name.text !== "className") {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const initializer = node.initializer
|
|
228
|
+
|
|
229
|
+
if (!initializer || !ts.isStringLiteral(initializer)) {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
addClassReferencesFromString(references, initializer.text)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function collectIdAttributes(node, references) {
|
|
237
|
+
const ts = typescriptModule
|
|
238
|
+
|
|
239
|
+
if (!ts.isJsxAttribute(node)) {
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (node.name.text !== "id") {
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const initializer = node.initializer
|
|
248
|
+
|
|
249
|
+
if (!initializer || !ts.isStringLiteral(initializer)) {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
references.add(`#${initializer.text}`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function collectUtilityCallReferences(node, context) {
|
|
257
|
+
const ts = typescriptModule
|
|
258
|
+
|
|
259
|
+
if (!ts.isCallExpression(node)) {
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const callee = node.expression
|
|
264
|
+
let functionName = null
|
|
265
|
+
|
|
266
|
+
if (ts.isIdentifier(callee)) {
|
|
267
|
+
functionName = callee.text
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!functionName || !context.utilityFunctionIdentifiers.has(functionName)) {
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// eslint-disable-next-line no-use-before-define -- mutual recursion with extractStringLiteralsFromNodes
|
|
275
|
+
extractStringLiteralsFromNodes(getCallArguments(node), context.references)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function extractObjectPropertyReferences(property, references) {
|
|
279
|
+
const ts = typescriptModule
|
|
280
|
+
|
|
281
|
+
if (!ts.isPropertyAssignment(property)) {
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const key = property.name
|
|
286
|
+
|
|
287
|
+
if (ts.isStringLiteral(key)) {
|
|
288
|
+
addClassReferencesFromString(references, key.text)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (ts.isIdentifier(key)) {
|
|
292
|
+
addClassReference(references, key.text)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// eslint-disable-next-line no-use-before-define -- mutual recursion with extractStringLiteralsFromNodes
|
|
296
|
+
extractStringLiteralsFromNodes([property.initializer], references)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function extractStringLiteralsFromNodes(nodes, references) {
|
|
300
|
+
const ts = typescriptModule
|
|
301
|
+
|
|
302
|
+
for (const node of nodes) {
|
|
303
|
+
if (ts.isStringLiteral(node)) {
|
|
304
|
+
addClassReferencesFromString(references, node.text)
|
|
305
|
+
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
310
|
+
for (const property of node.properties) {
|
|
311
|
+
extractObjectPropertyReferences(property, references)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
318
|
+
extractStringLiteralsFromNodes([...node.elements], references)
|
|
319
|
+
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (ts.isCallExpression(node)) {
|
|
324
|
+
extractStringLiteralsFromNodes(getCallArguments(node), references)
|
|
325
|
+
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (ts.isConditionalExpression(node)) {
|
|
330
|
+
extractStringLiteralsFromNodes([node.whenTrue, node.whenFalse], references)
|
|
331
|
+
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (ts.isTemplateExpression(node)) {
|
|
336
|
+
const spanExpressions = node.templateSpans.map((span) => {
|
|
337
|
+
return span.expression
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
extractStringLiteralsFromNodes(spanExpressions, references)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isSimpleSelector(selectorRoot) {
|
|
346
|
+
for (const selector of selectorRoot.nodes) {
|
|
347
|
+
const significantNodes = selector.nodes.filter((node) => {
|
|
348
|
+
return node.type !== "comment" && node.type !== "combinator"
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
if (significantNodes.length !== 1) {
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function stripNonStructuralPseudos(selectorRoot) {
|
|
360
|
+
selectorRoot.walk((node) => {
|
|
361
|
+
if (node.type !== "pseudo") {
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const pseudoName = node.value.replace(/^::?/, "")
|
|
366
|
+
|
|
367
|
+
if (node.value.startsWith("::")) {
|
|
368
|
+
if (NON_STRUCTURAL_PSEUDO_ELEMENTS.has(pseudoName)) {
|
|
369
|
+
node.remove()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (NON_STRUCTURAL_PSEUDO_CLASSES.has(pseudoName)) {
|
|
376
|
+
node.remove()
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
return selectorRoot
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* @param {string} cssPath
|
|
385
|
+
* @returns {import("./no-unused-selectors.types.ts").TemplateVariablesType}
|
|
386
|
+
*/
|
|
387
|
+
function resolveTemplateVariables(cssPath) {
|
|
388
|
+
const parsed = path.parse(cssPath)
|
|
389
|
+
const cssDirectory = parsed.dir
|
|
390
|
+
const cssName = parsed.name
|
|
391
|
+
const cssBaseName = cssName.replace(/\.module$/, "")
|
|
392
|
+
const cssDirectoryName = path.basename(cssDirectory)
|
|
393
|
+
|
|
394
|
+
return { cssBaseName, cssDirectory, cssDirectoryName, cssName }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* @param {string} cssPath
|
|
399
|
+
* @param {string[]} templates
|
|
400
|
+
* @returns {string[]}
|
|
401
|
+
*/
|
|
402
|
+
function resolveDocumentPaths(cssPath, templates) {
|
|
403
|
+
const variables = resolveTemplateVariables(cssPath)
|
|
404
|
+
|
|
405
|
+
return templates.map((template) => {
|
|
406
|
+
return template
|
|
407
|
+
.replaceAll("{cssDirectory}", variables.cssDirectory)
|
|
408
|
+
.replaceAll("{cssName}", variables.cssName)
|
|
409
|
+
.replaceAll("{cssBaseName}", variables.cssBaseName)
|
|
410
|
+
.replaceAll("{cssDirectoryName}", variables.cssDirectoryName)
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* @param {string[]} documentPaths
|
|
416
|
+
* @returns {import("./no-unused-selectors.types.ts").ResolvedDocumentType[]}
|
|
417
|
+
*/
|
|
418
|
+
function resolveAllDocuments(documentPaths) {
|
|
419
|
+
const documents = []
|
|
420
|
+
|
|
421
|
+
for (const documentPath of documentPaths) {
|
|
422
|
+
try {
|
|
423
|
+
const content = fs.readFileSync(documentPath, "utf8")
|
|
424
|
+
|
|
425
|
+
documents.push({ content, path: documentPath })
|
|
426
|
+
} catch {
|
|
427
|
+
// File doesn't exist, skip
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return documents
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* @param {string} sourceContent
|
|
436
|
+
* @param {string} filePath
|
|
437
|
+
* @returns {Set<string>}
|
|
438
|
+
*/
|
|
439
|
+
function extractClassReferences(sourceContent, filePath) {
|
|
440
|
+
/** @type {Set<string>} */
|
|
441
|
+
const references = new Set()
|
|
442
|
+
|
|
443
|
+
const ts = getTypeScript()
|
|
444
|
+
|
|
445
|
+
if (!ts) {
|
|
446
|
+
return references
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const sourceFile = ts.createSourceFile(
|
|
450
|
+
filePath,
|
|
451
|
+
sourceContent,
|
|
452
|
+
ts.ScriptTarget.Latest,
|
|
453
|
+
true,
|
|
454
|
+
filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
const cssModuleIdentifiers = new Set()
|
|
458
|
+
const utilityFunctionIdentifiers = new Set()
|
|
459
|
+
const context = { cssModuleIdentifiers, references, utilityFunctionIdentifiers }
|
|
460
|
+
|
|
461
|
+
ts.forEachChild(sourceFile, function visit(node) {
|
|
462
|
+
collectCssModuleImports(node, cssModuleIdentifiers)
|
|
463
|
+
collectUtilityImports(node, utilityFunctionIdentifiers)
|
|
464
|
+
collectPropertyAccessReferences(node, context)
|
|
465
|
+
collectBracketAccessReferences(node, context)
|
|
466
|
+
collectClassNameAttributes(node, references)
|
|
467
|
+
collectIdAttributes(node, references)
|
|
468
|
+
collectUtilityCallReferences(node, context)
|
|
469
|
+
|
|
470
|
+
ts.forEachChild(node, visit)
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
return references
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* @param {string} resolvedSelector
|
|
478
|
+
* @returns {{ type: "class" | "id"; value: string } | null}
|
|
479
|
+
*/
|
|
480
|
+
function extractSimpleSelector(resolvedSelector) {
|
|
481
|
+
let result = null
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
selectorParser((selectors) => {
|
|
485
|
+
const stripped = stripNonStructuralPseudos(selectors)
|
|
486
|
+
|
|
487
|
+
if (!isSimpleSelector(stripped)) {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const firstSelector = stripped.nodes[0]
|
|
492
|
+
|
|
493
|
+
if (!firstSelector) {
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const significantNode = firstSelector.nodes.find((node) => {
|
|
498
|
+
return node.type !== "comment" && node.type !== "combinator"
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
if (!significantNode) {
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (significantNode.type === "class") {
|
|
506
|
+
result = { type: "class", value: `.${significantNode.value}` }
|
|
507
|
+
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (significantNode.type === "id") {
|
|
512
|
+
result = { type: "id", value: `#${significantNode.value}` }
|
|
513
|
+
}
|
|
514
|
+
}).processSync(resolvedSelector)
|
|
515
|
+
} catch {
|
|
516
|
+
return null
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return result
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export {
|
|
523
|
+
DEFAULT_DOCUMENT_TEMPLATES,
|
|
524
|
+
extractClassReferences,
|
|
525
|
+
extractSimpleSelector,
|
|
526
|
+
resolveAllDocuments,
|
|
527
|
+
resolveDocumentPaths,
|
|
528
|
+
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/** @type {import("stylelint").Config} */
|
|
2
|
-
const plugin = {
|
|
3
|
-
rules: {
|
|
4
|
-
"plugin/no-unused-selectors": [
|
|
5
|
-
true,
|
|
6
|
-
{
|
|
7
|
-
resolve: {
|
|
8
|
-
documents: [
|
|
9
|
-
"{cssDir}/{cssName}.variants.ts",
|
|
10
|
-
"{cssDir}/{cssName}.variants.tsx",
|
|
11
|
-
"{cssDir}/{cssName}.tsx",
|
|
12
|
-
"{cssDir}/{cssName}.jsx",
|
|
13
|
-
"{cssDir}/{cssName}.html",
|
|
14
|
-
"{cssDir}/{cssName}.htm",
|
|
15
|
-
"{cssDir}/{cssDirName}.tsx",
|
|
16
|
-
"{cssDir}/{cssDirName}.jsx",
|
|
17
|
-
"{cssDir}/{cssDirName}.html",
|
|
18
|
-
"{cssDir}/{cssDirName}.htm",
|
|
19
|
-
"{cssDir}/index.tsx",
|
|
20
|
-
"{cssDir}/index.jsx",
|
|
21
|
-
"{cssDir}/index.html",
|
|
22
|
-
"{cssDir}/index.htm",
|
|
23
|
-
],
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default plugin
|