@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.
Files changed (21) hide show
  1. package/dist/stylelint/rules/no-unused-selectors/no-unused-selectors.d.ts +3 -0
  2. package/dist/stylelint/rules/no-unused-selectors/no-unused-selectors.types.d.ts +13 -0
  3. package/dist/stylelint/rules/no-unused-selectors/no-unused-selectors.utils.d.ts +26 -0
  4. package/package.json +10 -5
  5. package/src/stylelint/configs/core.js +3 -3
  6. package/src/stylelint/rules/no-unused-selectors/fixtures/basic/Component.tsx +5 -0
  7. package/src/stylelint/rules/no-unused-selectors/fixtures/bracket-access/Dynamic.tsx +5 -0
  8. package/src/stylelint/rules/no-unused-selectors/fixtures/class-name-attribute/Static.tsx +3 -0
  9. package/src/stylelint/rules/no-unused-selectors/fixtures/compound-selectors/Layout.tsx +9 -0
  10. package/src/stylelint/rules/no-unused-selectors/fixtures/cva-pattern/Card.tsx +6 -0
  11. package/src/stylelint/rules/no-unused-selectors/fixtures/cva-pattern/Card.variants.ts +12 -0
  12. package/src/stylelint/rules/no-unused-selectors/fixtures/id-attribute/Panel.tsx +3 -0
  13. package/src/stylelint/rules/no-unused-selectors/fixtures/multi-document/Button.tsx +6 -0
  14. package/src/stylelint/rules/no-unused-selectors/fixtures/multi-document/Button.variants.ts +12 -0
  15. package/src/stylelint/rules/no-unused-selectors/fixtures/pseudo-classes/Link.tsx +5 -0
  16. package/src/stylelint/rules/no-unused-selectors/fixtures/utility-libraries/Widget.tsx +5 -0
  17. package/src/stylelint/rules/no-unused-selectors/no-unused-selectors.js +125 -0
  18. package/src/stylelint/rules/no-unused-selectors/no-unused-selectors.types.ts +15 -0
  19. package/src/stylelint/rules/no-unused-selectors/no-unused-selectors.utils.js +528 -0
  20. package/dist/stylelint/plugins/no-unused-selectors.d.ts +0 -3
  21. package/src/stylelint/plugins/no-unused-selectors.js +0 -30
@@ -0,0 +1,3 @@
1
+ declare const _default: stylelint.Plugin;
2
+ export default _default;
3
+ import stylelint from "stylelint";
@@ -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.18.1",
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.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.12.0",
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", "stylelint-no-unused-selectors"],
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
- ...stylelintNoUnusedSelectors.rules,
17
+ "dvukovic/no-unused-selectors": true,
18
18
  },
19
19
  }
20
20
 
@@ -0,0 +1,5 @@
1
+ import styles from "./Component.module.css"
2
+
3
+ export function Component() {
4
+ return <div className={styles.used}>Hello</div>
5
+ }
@@ -0,0 +1,5 @@
1
+ import styles from "./Dynamic.module.css"
2
+
3
+ export function Dynamic() {
4
+ return <div className={styles["bracket-class"]}>Hello</div>
5
+ }
@@ -0,0 +1,3 @@
1
+ export function Static() {
2
+ return <div className="foo bar">Hello</div>
3
+ }
@@ -0,0 +1,9 @@
1
+ import styles from "./Layout.module.css"
2
+
3
+ export function Layout() {
4
+ return (
5
+ <div className={styles.parent}>
6
+ <div className={styles.child}>Content</div>
7
+ </div>
8
+ )
9
+ }
@@ -0,0 +1,6 @@
1
+ import styles from "./Card.module.css"
2
+ import { cardVariants } from "./Card.variants"
3
+
4
+ export function Card({ size }) {
5
+ return <div className={cardVariants({ size })}>{styles.header}</div>
6
+ }
@@ -0,0 +1,12 @@
1
+ import { cva } from "class-variance-authority"
2
+
3
+ import styles from "./Card.module.css"
4
+
5
+ export const cardVariants = cva(styles.cardBase, {
6
+ variants: {
7
+ size: {
8
+ large: styles.cardLarge,
9
+ small: styles.cardSmall,
10
+ },
11
+ },
12
+ })
@@ -0,0 +1,3 @@
1
+ export function Panel() {
2
+ return <div id="panel">Hello</div>
3
+ }
@@ -0,0 +1,6 @@
1
+ import styles from "./Button.module.css"
2
+ import { buttonVariants } from "./Button.variants"
3
+
4
+ export function Button({ variant }) {
5
+ return <button className={buttonVariants({ variant })}>{styles.base}</button>
6
+ }
@@ -0,0 +1,12 @@
1
+ import { cva } from "class-variance-authority"
2
+
3
+ import styles from "./Button.module.css"
4
+
5
+ export const buttonVariants = cva(styles.variant, {
6
+ variants: {
7
+ size: {
8
+ large: styles.large,
9
+ small: styles.small,
10
+ },
11
+ },
12
+ })
@@ -0,0 +1,5 @@
1
+ import styles from "./Link.module.css"
2
+
3
+ export function Link() {
4
+ return <a className={styles.link}>Click me</a>
5
+ }
@@ -0,0 +1,5 @@
1
+ import clsx from "clsx"
2
+
3
+ export function Widget({ active }) {
4
+ return <div className={clsx("foo", "bar", { baz: active })}>Hello</div>
5
+ }
@@ -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,3 +0,0 @@
1
- export default plugin;
2
- /** @type {import("stylelint").Config} */
3
- declare const plugin: import("stylelint").Config;
@@ -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