@friendsoftheweb/eslint-plugin 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # @friendsoftheweb/eslint-plugin
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ yarn add -D @friendsoftheweb/eslint-plugin
7
+ ```
8
+
9
+ ## Example Configurations
10
+
11
+ ## Basic Example
12
+
13
+ ```javascript
14
+ import friendsOfTheWeb from '@friendsoftheweb/eslint-plugin';
15
+ import { defineConfig } from 'eslint/config';
16
+ import tseslint from 'typescript-eslint';
17
+
18
+ export default defineConfig([
19
+ { ignores: ['.yarn/**/*'] },
20
+ {
21
+ files: ['**/*.{js,jsx,ts,tsx}'],
22
+ extends: [
23
+ tseslint.configs.recommended,
24
+ friendsOfTheWeb.configs['flat/recommended'],
25
+ ],
26
+ },
27
+ ]);
28
+ ```
29
+
30
+ ## Example with React
31
+
32
+ ```javascript
33
+ import friendsOfTheWeb from '@friendsoftheweb/eslint-plugin';
34
+ import react from 'eslint-plugin-react';
35
+ import reactCompiler from 'eslint-plugin-react-compiler';
36
+ import reactHooks from 'eslint-plugin-react-hooks';
37
+ import { defineConfig } from 'eslint/config';
38
+ import tseslint from 'typescript-eslint';
39
+
40
+ export default defineConfig([
41
+ { ignores: ['.yarn/**/*'] },
42
+ {
43
+ files: ['**/*.{js,jsx,ts,tsx}'],
44
+ extends: [
45
+ tseslint.configs.recommended,
46
+ react.configs.flat.recommended,
47
+ react.configs.flat['jsx-runtime'],
48
+ reactHooks.configs.flat.recommended,
49
+ reactCompiler.configs.recommended,
50
+ friendsOfTheWeb.configs['flat/recommended'],
51
+ ],
52
+ settings: {
53
+ react: {
54
+ version: 'detect',
55
+ },
56
+ },
57
+ },
58
+ ]);
59
+ ```
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ var cssModuleNameMatches = require('./rules/css-module-name-matches.js');
4
+ var cssModuleClassExists = require('./rules/css-module-class-exists.js');
5
+
6
+ /** @type {import('eslint').ESLint.Plugin} */
7
+ const plugin = {
8
+ meta: {
9
+ name: '@friendsoftheweb/eslint-plugin',
10
+ version: '0.0.1',
11
+ },
12
+ configs: {},
13
+ rules: {
14
+ 'css-module-name-matches': cssModuleNameMatches,
15
+ 'css-module-class-exists': cssModuleClassExists,
16
+ },
17
+ };
18
+
19
+ Object.assign(plugin.configs, {
20
+ // flat config format
21
+ 'flat/recommended': [
22
+ {
23
+ plugins: {
24
+ friendsoftheweb: plugin,
25
+ },
26
+ rules: {
27
+ 'friendsoftheweb/css-module-name-matches': 'error',
28
+ 'friendsoftheweb/css-module-class-exists': 'error',
29
+ },
30
+ },
31
+ ],
32
+
33
+ // eslintrc format
34
+ recommended: {
35
+ plugins: { friendsoftheweb: plugin },
36
+ rules: {
37
+ 'friendsoftheweb/css-module-name-matches': 'error',
38
+ 'friendsoftheweb/css-module-class-exists': 'error',
39
+ },
40
+ },
41
+ });
42
+
43
+ module.exports = plugin;
44
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/index.mjs"],"sourcesContent":["import cssModuleNameMatchesRule from './rules/css-module-name-matches.mjs';\nimport cssModuleClassExistsRule from './rules/css-module-class-exists.mjs';\n\n/** @type {import('eslint').ESLint.Plugin} */\nconst plugin = {\n meta: {\n name: '@friendsoftheweb/eslint-plugin',\n version: '0.0.1',\n },\n configs: {},\n rules: {\n 'css-module-name-matches': cssModuleNameMatchesRule,\n 'css-module-class-exists': cssModuleClassExistsRule,\n },\n};\n\nObject.assign(plugin.configs, {\n // flat config format\n 'flat/recommended': [\n {\n plugins: {\n friendsoftheweb: plugin,\n },\n rules: {\n 'friendsoftheweb/css-module-name-matches': 'error',\n 'friendsoftheweb/css-module-class-exists': 'error',\n },\n },\n ],\n\n // eslintrc format\n recommended: {\n plugins: { friendsoftheweb: plugin },\n rules: {\n 'friendsoftheweb/css-module-name-matches': 'error',\n 'friendsoftheweb/css-module-class-exists': 'error',\n },\n },\n});\n\nexport default plugin;\n"],"names":["cssModuleNameMatchesRule","cssModuleClassExistsRule"],"mappings":";;;;;AAGA;AACK,MAAC,MAAM,GAAG;AACf,EAAE,IAAI,EAAE;AACR,IAAI,IAAI,EAAE,gCAAgC;AAC1C,IAAI,OAAO,EAAE,OAAO;AACpB,GAAG;AACH,EAAE,OAAO,EAAE,EAAE;AACb,EAAE,KAAK,EAAE;AACT,IAAI,yBAAyB,EAAEA,oBAAwB;AACvD,IAAI,yBAAyB,EAAEC,oBAAwB;AACvD,GAAG;AACH;;AAEA,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE;AAC9B;AACA,EAAE,kBAAkB,EAAE;AACtB,IAAI;AACJ,MAAM,OAAO,EAAE;AACf,QAAQ,eAAe,EAAE,MAAM;AAC/B,OAAO;AACP,MAAM,KAAK,EAAE;AACb,QAAQ,yCAAyC,EAAE,OAAO;AAC1D,QAAQ,yCAAyC,EAAE,OAAO;AAC1D,OAAO;AACP,KAAK;AACL,GAAG;;AAEH;AACA,EAAE,WAAW,EAAE;AACf,IAAI,OAAO,EAAE,EAAE,eAAe,EAAE,MAAM,EAAE;AACxC,IAAI,KAAK,EAAE;AACX,MAAM,yCAAyC,EAAE,OAAO;AACxD,MAAM,yCAAyC,EAAE,OAAO;AACxD,KAAK;AACL,GAAG;AACH,CAAC,CAAC;;;;"}
@@ -0,0 +1,215 @@
1
+ 'use strict';
2
+
3
+ var path = require('node:path');
4
+ var fs = require('node:fs');
5
+ var postcss = require('postcss');
6
+ var selectorParser = require('postcss-selector-parser');
7
+
8
+ /** @type {import('eslint').JSRuleDefinition} */
9
+ var cssModuleClassExistsRule = {
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ description:
14
+ 'enforce that class names used from an imported CSS module exist in the module file',
15
+ },
16
+ schema: [],
17
+ messages: {
18
+ relativePath:
19
+ 'CSS module import "{{importPath}}" should be a relative path',
20
+ defaultImport:
21
+ 'CSS module import "{{importPath}}" should have a default import',
22
+ onlyDefaultImport:
23
+ 'CSS module import "{{importPath}}" should have only a default import',
24
+ fileDoesNotExist:
25
+ 'CSS module file "{{absoluteImportPath}}" does not exist',
26
+ classDoesNotExist:
27
+ 'Class `.{{className}}` does not exist in the CSS module imported as `{{objectName}}`',
28
+ },
29
+ },
30
+ create(context) {
31
+ const classNames = {};
32
+
33
+ return {
34
+ ImportDeclaration(node) {
35
+ if (
36
+ typeof node.source.value !== 'string' ||
37
+ !node.source.value.endsWith('.module.css')
38
+ ) {
39
+ return;
40
+ }
41
+
42
+ const importPath = node.source.value;
43
+
44
+ if (!(importPath.startsWith('./') || importPath.startsWith('../'))) {
45
+ context.report({
46
+ node,
47
+ messageId: 'relativePath',
48
+ data: { importPath },
49
+ });
50
+
51
+ return;
52
+ }
53
+
54
+ if (node.specifiers.length === 0) {
55
+ context.report({
56
+ node,
57
+ messageId: 'defaultImport',
58
+ data: { importPath },
59
+ });
60
+
61
+ return;
62
+ }
63
+
64
+ if (node.specifiers.length > 1) {
65
+ context.report({
66
+ node,
67
+ messageId: 'onlyDefaultImport',
68
+ data: { importPath },
69
+ });
70
+
71
+ return;
72
+ }
73
+
74
+ const defaultImportSpecifier = node.specifiers.find(
75
+ (specifier) => specifier.type === 'ImportDefaultSpecifier',
76
+ );
77
+
78
+ if (defaultImportSpecifier == null) {
79
+ context.report({
80
+ node,
81
+ messageId: 'onlyDefaultImport',
82
+ data: { importPath },
83
+ });
84
+
85
+ return;
86
+ }
87
+
88
+ const dirname = path.dirname(context.physicalFilename);
89
+ const absoluteImportPath = path.resolve(dirname, importPath);
90
+
91
+ if (!fs.existsSync(absoluteImportPath)) {
92
+ context.report({
93
+ node,
94
+ messageId: 'fileDoesNotExist',
95
+ data: { absoluteImportPath },
96
+ });
97
+
98
+ return;
99
+ }
100
+
101
+ const importName = defaultImportSpecifier.local.name;
102
+
103
+ classNames[importName] = new Set();
104
+
105
+ const cssModuleContent = fs.readFileSync(absoluteImportPath, 'utf8');
106
+ const root = postcss.parse(cssModuleContent);
107
+
108
+ for (const node of root.nodes) {
109
+ if (node.type === 'rule') {
110
+ selectorParser(function transform(selectors) {
111
+ selectors.walkClasses((classNode) => {
112
+ classNames[importName].add(classNode.value);
113
+ });
114
+ }).processSync(node.selector);
115
+ } else if (
116
+ node.type === 'atrule' &&
117
+ (node.name === 'media' ||
118
+ node.name === 'container' ||
119
+ node.name === 'layer')
120
+ ) {
121
+ for (const childNode of node.nodes) {
122
+ if (childNode.type !== 'rule') {
123
+ continue;
124
+ }
125
+
126
+ selectorParser(function transform(selectors) {
127
+ selectors.walkClasses((classNode) => {
128
+ classNames[importName].add(classNode.value);
129
+ });
130
+ }).processSync(childNode.selector);
131
+ }
132
+ }
133
+ }
134
+ },
135
+ MemberExpression(node) {
136
+ if (node.object.type !== 'Identifier') {
137
+ return;
138
+ }
139
+
140
+ if (classNames[node.object.name] == null) {
141
+ return;
142
+ }
143
+
144
+ const objectName = node.object.name;
145
+
146
+ if (node.property.type === 'Identifier') {
147
+ const className = node.property.name;
148
+
149
+ if (!classNames[objectName].has(className)) {
150
+ context.report({
151
+ node,
152
+ messageId: 'classDoesNotExist',
153
+ data: { className, objectName },
154
+ });
155
+ }
156
+
157
+ return;
158
+ }
159
+
160
+ if (
161
+ node.property.type === 'Literal' &&
162
+ typeof node.property.value === 'string'
163
+ ) {
164
+ const className = node.property.value;
165
+
166
+ if (!classNames[objectName].has(className)) {
167
+ context.report({
168
+ node,
169
+ messageId: 'classDoesNotExist',
170
+ data: { className, objectName },
171
+ });
172
+ }
173
+ }
174
+ },
175
+ VariableDeclarator(node) {
176
+ if (node.id.type !== 'ObjectPattern') {
177
+ return;
178
+ }
179
+
180
+ if (node.init?.type !== 'Identifier') {
181
+ return;
182
+ }
183
+
184
+ const objectName = node.init.name;
185
+
186
+ if (classNames[objectName] == null) {
187
+ return;
188
+ }
189
+
190
+ for (const property of node.id.properties) {
191
+ if (property.type !== 'Property') {
192
+ continue;
193
+ }
194
+
195
+ if (property.key.type !== 'Identifier') {
196
+ continue;
197
+ }
198
+
199
+ const className = property.key.name;
200
+
201
+ if (!classNames[objectName].has(className)) {
202
+ context.report({
203
+ node: property,
204
+ messageId: 'classDoesNotExist',
205
+ data: { className, objectName },
206
+ });
207
+ }
208
+ }
209
+ },
210
+ };
211
+ },
212
+ };
213
+
214
+ module.exports = cssModuleClassExistsRule;
215
+ //# sourceMappingURL=css-module-class-exists.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"css-module-class-exists.js","sources":["../../../src/rules/css-module-class-exists.mjs"],"sourcesContent":["import path from 'node:path';\nimport fs from 'node:fs';\n\nimport { parse } from 'postcss';\nimport selectorParser from 'postcss-selector-parser';\n\n/** @type {import('eslint').JSRuleDefinition} */\nexport default {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'enforce that class names used from an imported CSS module exist in the module file',\n },\n schema: [],\n messages: {\n relativePath:\n 'CSS module import \"{{importPath}}\" should be a relative path',\n defaultImport:\n 'CSS module import \"{{importPath}}\" should have a default import',\n onlyDefaultImport:\n 'CSS module import \"{{importPath}}\" should have only a default import',\n fileDoesNotExist:\n 'CSS module file \"{{absoluteImportPath}}\" does not exist',\n classDoesNotExist:\n 'Class `.{{className}}` does not exist in the CSS module imported as `{{objectName}}`',\n },\n },\n create(context) {\n const classNames = {};\n\n return {\n ImportDeclaration(node) {\n if (\n typeof node.source.value !== 'string' ||\n !node.source.value.endsWith('.module.css')\n ) {\n return;\n }\n\n const importPath = node.source.value;\n\n if (!(importPath.startsWith('./') || importPath.startsWith('../'))) {\n context.report({\n node,\n messageId: 'relativePath',\n data: { importPath },\n });\n\n return;\n }\n\n if (node.specifiers.length === 0) {\n context.report({\n node,\n messageId: 'defaultImport',\n data: { importPath },\n });\n\n return;\n }\n\n if (node.specifiers.length > 1) {\n context.report({\n node,\n messageId: 'onlyDefaultImport',\n data: { importPath },\n });\n\n return;\n }\n\n const defaultImportSpecifier = node.specifiers.find(\n (specifier) => specifier.type === 'ImportDefaultSpecifier',\n );\n\n if (defaultImportSpecifier == null) {\n context.report({\n node,\n messageId: 'onlyDefaultImport',\n data: { importPath },\n });\n\n return;\n }\n\n const dirname = path.dirname(context.physicalFilename);\n const absoluteImportPath = path.resolve(dirname, importPath);\n\n if (!fs.existsSync(absoluteImportPath)) {\n context.report({\n node,\n messageId: 'fileDoesNotExist',\n data: { absoluteImportPath },\n });\n\n return;\n }\n\n const importName = defaultImportSpecifier.local.name;\n\n classNames[importName] = new Set();\n\n const cssModuleContent = fs.readFileSync(absoluteImportPath, 'utf8');\n const root = parse(cssModuleContent);\n\n for (const node of root.nodes) {\n if (node.type === 'rule') {\n selectorParser(function transform(selectors) {\n selectors.walkClasses((classNode) => {\n classNames[importName].add(classNode.value);\n });\n }).processSync(node.selector);\n } else if (\n node.type === 'atrule' &&\n (node.name === 'media' ||\n node.name === 'container' ||\n node.name === 'layer')\n ) {\n for (const childNode of node.nodes) {\n if (childNode.type !== 'rule') {\n continue;\n }\n\n selectorParser(function transform(selectors) {\n selectors.walkClasses((classNode) => {\n classNames[importName].add(classNode.value);\n });\n }).processSync(childNode.selector);\n }\n }\n }\n },\n MemberExpression(node) {\n if (node.object.type !== 'Identifier') {\n return;\n }\n\n if (classNames[node.object.name] == null) {\n return;\n }\n\n const objectName = node.object.name;\n\n if (node.property.type === 'Identifier') {\n const className = node.property.name;\n\n if (!classNames[objectName].has(className)) {\n context.report({\n node,\n messageId: 'classDoesNotExist',\n data: { className, objectName },\n });\n }\n\n return;\n }\n\n if (\n node.property.type === 'Literal' &&\n typeof node.property.value === 'string'\n ) {\n const className = node.property.value;\n\n if (!classNames[objectName].has(className)) {\n context.report({\n node,\n messageId: 'classDoesNotExist',\n data: { className, objectName },\n });\n }\n }\n },\n VariableDeclarator(node) {\n if (node.id.type !== 'ObjectPattern') {\n return;\n }\n\n if (node.init?.type !== 'Identifier') {\n return;\n }\n\n const objectName = node.init.name;\n\n if (classNames[objectName] == null) {\n return;\n }\n\n for (const property of node.id.properties) {\n if (property.type !== 'Property') {\n continue;\n }\n\n if (property.key.type !== 'Identifier') {\n continue;\n }\n\n const className = property.key.name;\n\n if (!classNames[objectName].has(className)) {\n context.report({\n node: property,\n messageId: 'classDoesNotExist',\n data: { className, objectName },\n });\n }\n }\n },\n };\n },\n};\n"],"names":["parse"],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ var path = require('node:path');
4
+ require('node:fs');
5
+
6
+ /** @type {import('eslint').JSRuleDefinition} */
7
+ var cssModuleNameMatchesRule = {
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description:
12
+ "enforce that a CSS module's filename matches the filename of the importing file",
13
+ },
14
+ schema: [],
15
+ messages: {
16
+ filenameMismatch:
17
+ 'CSS module filename "{{cssModuleFilename}}" does not match the current filename "{{filename}}"',
18
+ },
19
+ },
20
+ create(context) {
21
+ return {
22
+ ImportDeclaration(node) {
23
+ if (
24
+ typeof node.source.value !== 'string' ||
25
+ !node.source.value.endsWith('.module.css')
26
+ ) {
27
+ return;
28
+ }
29
+
30
+ const filename = path.basename(
31
+ context.filename,
32
+ path.extname(context.filename),
33
+ );
34
+
35
+ const cssModulePath = node.source.value;
36
+ const cssModuleFilename = path.basename(cssModulePath, '.module.css');
37
+
38
+ if (cssModuleFilename !== filename) {
39
+ context.report({
40
+ node,
41
+ messageId: 'filenameMismatch',
42
+ data: {
43
+ cssModuleFilename,
44
+ filename,
45
+ },
46
+ });
47
+ }
48
+ },
49
+ };
50
+ },
51
+ };
52
+
53
+ module.exports = cssModuleNameMatchesRule;
54
+ //# sourceMappingURL=css-module-name-matches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"css-module-name-matches.js","sources":["../../../src/rules/css-module-name-matches.mjs"],"sourcesContent":["import path from 'node:path';\nimport fs from 'node:fs';\n\n/** @type {import('eslint').JSRuleDefinition} */\nexport default {\n meta: {\n type: 'problem',\n docs: {\n description:\n \"enforce that a CSS module's filename matches the filename of the importing file\",\n },\n schema: [],\n messages: {\n filenameMismatch:\n 'CSS module filename \"{{cssModuleFilename}}\" does not match the current filename \"{{filename}}\"',\n },\n },\n create(context) {\n return {\n ImportDeclaration(node) {\n if (\n typeof node.source.value !== 'string' ||\n !node.source.value.endsWith('.module.css')\n ) {\n return;\n }\n\n const filename = path.basename(\n context.filename,\n path.extname(context.filename),\n );\n\n const cssModulePath = node.source.value;\n const cssModuleFilename = path.basename(cssModulePath, '.module.css');\n\n if (cssModuleFilename !== filename) {\n context.report({\n node,\n messageId: 'filenameMismatch',\n data: {\n cssModuleFilename,\n filename,\n },\n });\n }\n },\n };\n },\n};\n"],"names":[],"mappings":";;;;;AAGA;AACA,+BAAe;AACf,EAAE,IAAI,EAAE;AACR,IAAI,IAAI,EAAE,SAAS;AACnB,IAAI,IAAI,EAAE;AACV,MAAM,WAAW;AACjB,QAAQ,iFAAiF;AACzF,KAAK;AACL,IAAI,MAAM,EAAE,EAAE;AACd,IAAI,QAAQ,EAAE;AACd,MAAM,gBAAgB;AACtB,QAAQ,gGAAgG;AACxG,KAAK;AACL,GAAG;AACH,EAAE,MAAM,CAAC,OAAO,EAAE;AAClB,IAAI,OAAO;AACX,MAAM,iBAAiB,CAAC,IAAI,EAAE;AAC9B,QAAQ;AACR,UAAU,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,QAAQ;AAC/C,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,aAAa;AACnD,UAAU;AACV,UAAU;AACV;;AAEA,QAAQ,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ;AACtC,UAAU,OAAO,CAAC,QAAQ;AAC1B,UAAU,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;AACxC,SAAS;;AAET,QAAQ,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK;AAC/C,QAAQ,MAAM,iBAAiB,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;;AAE7E,QAAQ,IAAI,iBAAiB,KAAK,QAAQ,EAAE;AAC5C,UAAU,OAAO,CAAC,MAAM,CAAC;AACzB,YAAY,IAAI;AAChB,YAAY,SAAS,EAAE,kBAAkB;AACzC,YAAY,IAAI,EAAE;AAClB,cAAc,iBAAiB;AAC/B,cAAc,QAAQ;AACtB,aAAa;AACb,WAAW,CAAC;AACZ;AACA,OAAO;AACP,KAAK;AACL,GAAG;AACH,CAAC;;;;"}
@@ -0,0 +1,42 @@
1
+ import cssModuleNameMatchesRule from './rules/css-module-name-matches.js';
2
+ import cssModuleClassExistsRule from './rules/css-module-class-exists.js';
3
+
4
+ /** @type {import('eslint').ESLint.Plugin} */
5
+ const plugin = {
6
+ meta: {
7
+ name: '@friendsoftheweb/eslint-plugin',
8
+ version: '0.0.1',
9
+ },
10
+ configs: {},
11
+ rules: {
12
+ 'css-module-name-matches': cssModuleNameMatchesRule,
13
+ 'css-module-class-exists': cssModuleClassExistsRule,
14
+ },
15
+ };
16
+
17
+ Object.assign(plugin.configs, {
18
+ // flat config format
19
+ 'flat/recommended': [
20
+ {
21
+ plugins: {
22
+ friendsoftheweb: plugin,
23
+ },
24
+ rules: {
25
+ 'friendsoftheweb/css-module-name-matches': 'error',
26
+ 'friendsoftheweb/css-module-class-exists': 'error',
27
+ },
28
+ },
29
+ ],
30
+
31
+ // eslintrc format
32
+ recommended: {
33
+ plugins: { friendsoftheweb: plugin },
34
+ rules: {
35
+ 'friendsoftheweb/css-module-name-matches': 'error',
36
+ 'friendsoftheweb/css-module-class-exists': 'error',
37
+ },
38
+ },
39
+ });
40
+
41
+ export { plugin as default };
42
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/index.mjs"],"sourcesContent":["import cssModuleNameMatchesRule from './rules/css-module-name-matches.mjs';\nimport cssModuleClassExistsRule from './rules/css-module-class-exists.mjs';\n\n/** @type {import('eslint').ESLint.Plugin} */\nconst plugin = {\n meta: {\n name: '@friendsoftheweb/eslint-plugin',\n version: '0.0.1',\n },\n configs: {},\n rules: {\n 'css-module-name-matches': cssModuleNameMatchesRule,\n 'css-module-class-exists': cssModuleClassExistsRule,\n },\n};\n\nObject.assign(plugin.configs, {\n // flat config format\n 'flat/recommended': [\n {\n plugins: {\n friendsoftheweb: plugin,\n },\n rules: {\n 'friendsoftheweb/css-module-name-matches': 'error',\n 'friendsoftheweb/css-module-class-exists': 'error',\n },\n },\n ],\n\n // eslintrc format\n recommended: {\n plugins: { friendsoftheweb: plugin },\n rules: {\n 'friendsoftheweb/css-module-name-matches': 'error',\n 'friendsoftheweb/css-module-class-exists': 'error',\n },\n },\n});\n\nexport default plugin;\n"],"names":[],"mappings":";;;AAGA;AACK,MAAC,MAAM,GAAG;AACf,EAAE,IAAI,EAAE;AACR,IAAI,IAAI,EAAE,gCAAgC;AAC1C,IAAI,OAAO,EAAE,OAAO;AACpB,GAAG;AACH,EAAE,OAAO,EAAE,EAAE;AACb,EAAE,KAAK,EAAE;AACT,IAAI,yBAAyB,EAAE,wBAAwB;AACvD,IAAI,yBAAyB,EAAE,wBAAwB;AACvD,GAAG;AACH;;AAEA,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE;AAC9B;AACA,EAAE,kBAAkB,EAAE;AACtB,IAAI;AACJ,MAAM,OAAO,EAAE;AACf,QAAQ,eAAe,EAAE,MAAM;AAC/B,OAAO;AACP,MAAM,KAAK,EAAE;AACb,QAAQ,yCAAyC,EAAE,OAAO;AAC1D,QAAQ,yCAAyC,EAAE,OAAO;AAC1D,OAAO;AACP,KAAK;AACL,GAAG;;AAEH;AACA,EAAE,WAAW,EAAE;AACf,IAAI,OAAO,EAAE,EAAE,eAAe,EAAE,MAAM,EAAE;AACxC,IAAI,KAAK,EAAE;AACX,MAAM,yCAAyC,EAAE,OAAO;AACxD,MAAM,yCAAyC,EAAE,OAAO;AACxD,KAAK;AACL,GAAG;AACH,CAAC,CAAC;;;;"}
@@ -0,0 +1,213 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { parse } from 'postcss';
4
+ import selectorParser from 'postcss-selector-parser';
5
+
6
+ /** @type {import('eslint').JSRuleDefinition} */
7
+ var cssModuleClassExistsRule = {
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description:
12
+ 'enforce that class names used from an imported CSS module exist in the module file',
13
+ },
14
+ schema: [],
15
+ messages: {
16
+ relativePath:
17
+ 'CSS module import "{{importPath}}" should be a relative path',
18
+ defaultImport:
19
+ 'CSS module import "{{importPath}}" should have a default import',
20
+ onlyDefaultImport:
21
+ 'CSS module import "{{importPath}}" should have only a default import',
22
+ fileDoesNotExist:
23
+ 'CSS module file "{{absoluteImportPath}}" does not exist',
24
+ classDoesNotExist:
25
+ 'Class `.{{className}}` does not exist in the CSS module imported as `{{objectName}}`',
26
+ },
27
+ },
28
+ create(context) {
29
+ const classNames = {};
30
+
31
+ return {
32
+ ImportDeclaration(node) {
33
+ if (
34
+ typeof node.source.value !== 'string' ||
35
+ !node.source.value.endsWith('.module.css')
36
+ ) {
37
+ return;
38
+ }
39
+
40
+ const importPath = node.source.value;
41
+
42
+ if (!(importPath.startsWith('./') || importPath.startsWith('../'))) {
43
+ context.report({
44
+ node,
45
+ messageId: 'relativePath',
46
+ data: { importPath },
47
+ });
48
+
49
+ return;
50
+ }
51
+
52
+ if (node.specifiers.length === 0) {
53
+ context.report({
54
+ node,
55
+ messageId: 'defaultImport',
56
+ data: { importPath },
57
+ });
58
+
59
+ return;
60
+ }
61
+
62
+ if (node.specifiers.length > 1) {
63
+ context.report({
64
+ node,
65
+ messageId: 'onlyDefaultImport',
66
+ data: { importPath },
67
+ });
68
+
69
+ return;
70
+ }
71
+
72
+ const defaultImportSpecifier = node.specifiers.find(
73
+ (specifier) => specifier.type === 'ImportDefaultSpecifier',
74
+ );
75
+
76
+ if (defaultImportSpecifier == null) {
77
+ context.report({
78
+ node,
79
+ messageId: 'onlyDefaultImport',
80
+ data: { importPath },
81
+ });
82
+
83
+ return;
84
+ }
85
+
86
+ const dirname = path.dirname(context.physicalFilename);
87
+ const absoluteImportPath = path.resolve(dirname, importPath);
88
+
89
+ if (!fs.existsSync(absoluteImportPath)) {
90
+ context.report({
91
+ node,
92
+ messageId: 'fileDoesNotExist',
93
+ data: { absoluteImportPath },
94
+ });
95
+
96
+ return;
97
+ }
98
+
99
+ const importName = defaultImportSpecifier.local.name;
100
+
101
+ classNames[importName] = new Set();
102
+
103
+ const cssModuleContent = fs.readFileSync(absoluteImportPath, 'utf8');
104
+ const root = parse(cssModuleContent);
105
+
106
+ for (const node of root.nodes) {
107
+ if (node.type === 'rule') {
108
+ selectorParser(function transform(selectors) {
109
+ selectors.walkClasses((classNode) => {
110
+ classNames[importName].add(classNode.value);
111
+ });
112
+ }).processSync(node.selector);
113
+ } else if (
114
+ node.type === 'atrule' &&
115
+ (node.name === 'media' ||
116
+ node.name === 'container' ||
117
+ node.name === 'layer')
118
+ ) {
119
+ for (const childNode of node.nodes) {
120
+ if (childNode.type !== 'rule') {
121
+ continue;
122
+ }
123
+
124
+ selectorParser(function transform(selectors) {
125
+ selectors.walkClasses((classNode) => {
126
+ classNames[importName].add(classNode.value);
127
+ });
128
+ }).processSync(childNode.selector);
129
+ }
130
+ }
131
+ }
132
+ },
133
+ MemberExpression(node) {
134
+ if (node.object.type !== 'Identifier') {
135
+ return;
136
+ }
137
+
138
+ if (classNames[node.object.name] == null) {
139
+ return;
140
+ }
141
+
142
+ const objectName = node.object.name;
143
+
144
+ if (node.property.type === 'Identifier') {
145
+ const className = node.property.name;
146
+
147
+ if (!classNames[objectName].has(className)) {
148
+ context.report({
149
+ node,
150
+ messageId: 'classDoesNotExist',
151
+ data: { className, objectName },
152
+ });
153
+ }
154
+
155
+ return;
156
+ }
157
+
158
+ if (
159
+ node.property.type === 'Literal' &&
160
+ typeof node.property.value === 'string'
161
+ ) {
162
+ const className = node.property.value;
163
+
164
+ if (!classNames[objectName].has(className)) {
165
+ context.report({
166
+ node,
167
+ messageId: 'classDoesNotExist',
168
+ data: { className, objectName },
169
+ });
170
+ }
171
+ }
172
+ },
173
+ VariableDeclarator(node) {
174
+ if (node.id.type !== 'ObjectPattern') {
175
+ return;
176
+ }
177
+
178
+ if (node.init?.type !== 'Identifier') {
179
+ return;
180
+ }
181
+
182
+ const objectName = node.init.name;
183
+
184
+ if (classNames[objectName] == null) {
185
+ return;
186
+ }
187
+
188
+ for (const property of node.id.properties) {
189
+ if (property.type !== 'Property') {
190
+ continue;
191
+ }
192
+
193
+ if (property.key.type !== 'Identifier') {
194
+ continue;
195
+ }
196
+
197
+ const className = property.key.name;
198
+
199
+ if (!classNames[objectName].has(className)) {
200
+ context.report({
201
+ node: property,
202
+ messageId: 'classDoesNotExist',
203
+ data: { className, objectName },
204
+ });
205
+ }
206
+ }
207
+ },
208
+ };
209
+ },
210
+ };
211
+
212
+ export { cssModuleClassExistsRule as default };
213
+ //# sourceMappingURL=css-module-class-exists.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"css-module-class-exists.js","sources":["../../../src/rules/css-module-class-exists.mjs"],"sourcesContent":["import path from 'node:path';\nimport fs from 'node:fs';\n\nimport { parse } from 'postcss';\nimport selectorParser from 'postcss-selector-parser';\n\n/** @type {import('eslint').JSRuleDefinition} */\nexport default {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'enforce that class names used from an imported CSS module exist in the module file',\n },\n schema: [],\n messages: {\n relativePath:\n 'CSS module import \"{{importPath}}\" should be a relative path',\n defaultImport:\n 'CSS module import \"{{importPath}}\" should have a default import',\n onlyDefaultImport:\n 'CSS module import \"{{importPath}}\" should have only a default import',\n fileDoesNotExist:\n 'CSS module file \"{{absoluteImportPath}}\" does not exist',\n classDoesNotExist:\n 'Class `.{{className}}` does not exist in the CSS module imported as `{{objectName}}`',\n },\n },\n create(context) {\n const classNames = {};\n\n return {\n ImportDeclaration(node) {\n if (\n typeof node.source.value !== 'string' ||\n !node.source.value.endsWith('.module.css')\n ) {\n return;\n }\n\n const importPath = node.source.value;\n\n if (!(importPath.startsWith('./') || importPath.startsWith('../'))) {\n context.report({\n node,\n messageId: 'relativePath',\n data: { importPath },\n });\n\n return;\n }\n\n if (node.specifiers.length === 0) {\n context.report({\n node,\n messageId: 'defaultImport',\n data: { importPath },\n });\n\n return;\n }\n\n if (node.specifiers.length > 1) {\n context.report({\n node,\n messageId: 'onlyDefaultImport',\n data: { importPath },\n });\n\n return;\n }\n\n const defaultImportSpecifier = node.specifiers.find(\n (specifier) => specifier.type === 'ImportDefaultSpecifier',\n );\n\n if (defaultImportSpecifier == null) {\n context.report({\n node,\n messageId: 'onlyDefaultImport',\n data: { importPath },\n });\n\n return;\n }\n\n const dirname = path.dirname(context.physicalFilename);\n const absoluteImportPath = path.resolve(dirname, importPath);\n\n if (!fs.existsSync(absoluteImportPath)) {\n context.report({\n node,\n messageId: 'fileDoesNotExist',\n data: { absoluteImportPath },\n });\n\n return;\n }\n\n const importName = defaultImportSpecifier.local.name;\n\n classNames[importName] = new Set();\n\n const cssModuleContent = fs.readFileSync(absoluteImportPath, 'utf8');\n const root = parse(cssModuleContent);\n\n for (const node of root.nodes) {\n if (node.type === 'rule') {\n selectorParser(function transform(selectors) {\n selectors.walkClasses((classNode) => {\n classNames[importName].add(classNode.value);\n });\n }).processSync(node.selector);\n } else if (\n node.type === 'atrule' &&\n (node.name === 'media' ||\n node.name === 'container' ||\n node.name === 'layer')\n ) {\n for (const childNode of node.nodes) {\n if (childNode.type !== 'rule') {\n continue;\n }\n\n selectorParser(function transform(selectors) {\n selectors.walkClasses((classNode) => {\n classNames[importName].add(classNode.value);\n });\n }).processSync(childNode.selector);\n }\n }\n }\n },\n MemberExpression(node) {\n if (node.object.type !== 'Identifier') {\n return;\n }\n\n if (classNames[node.object.name] == null) {\n return;\n }\n\n const objectName = node.object.name;\n\n if (node.property.type === 'Identifier') {\n const className = node.property.name;\n\n if (!classNames[objectName].has(className)) {\n context.report({\n node,\n messageId: 'classDoesNotExist',\n data: { className, objectName },\n });\n }\n\n return;\n }\n\n if (\n node.property.type === 'Literal' &&\n typeof node.property.value === 'string'\n ) {\n const className = node.property.value;\n\n if (!classNames[objectName].has(className)) {\n context.report({\n node,\n messageId: 'classDoesNotExist',\n data: { className, objectName },\n });\n }\n }\n },\n VariableDeclarator(node) {\n if (node.id.type !== 'ObjectPattern') {\n return;\n }\n\n if (node.init?.type !== 'Identifier') {\n return;\n }\n\n const objectName = node.init.name;\n\n if (classNames[objectName] == null) {\n return;\n }\n\n for (const property of node.id.properties) {\n if (property.type !== 'Property') {\n continue;\n }\n\n if (property.key.type !== 'Identifier') {\n continue;\n }\n\n const className = property.key.name;\n\n if (!classNames[objectName].has(className)) {\n context.report({\n node: property,\n messageId: 'classDoesNotExist',\n data: { className, objectName },\n });\n }\n }\n },\n };\n },\n};\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import path from 'node:path';
2
+ import 'node:fs';
3
+
4
+ /** @type {import('eslint').JSRuleDefinition} */
5
+ var cssModuleNameMatchesRule = {
6
+ meta: {
7
+ type: 'problem',
8
+ docs: {
9
+ description:
10
+ "enforce that a CSS module's filename matches the filename of the importing file",
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ filenameMismatch:
15
+ 'CSS module filename "{{cssModuleFilename}}" does not match the current filename "{{filename}}"',
16
+ },
17
+ },
18
+ create(context) {
19
+ return {
20
+ ImportDeclaration(node) {
21
+ if (
22
+ typeof node.source.value !== 'string' ||
23
+ !node.source.value.endsWith('.module.css')
24
+ ) {
25
+ return;
26
+ }
27
+
28
+ const filename = path.basename(
29
+ context.filename,
30
+ path.extname(context.filename),
31
+ );
32
+
33
+ const cssModulePath = node.source.value;
34
+ const cssModuleFilename = path.basename(cssModulePath, '.module.css');
35
+
36
+ if (cssModuleFilename !== filename) {
37
+ context.report({
38
+ node,
39
+ messageId: 'filenameMismatch',
40
+ data: {
41
+ cssModuleFilename,
42
+ filename,
43
+ },
44
+ });
45
+ }
46
+ },
47
+ };
48
+ },
49
+ };
50
+
51
+ export { cssModuleNameMatchesRule as default };
52
+ //# sourceMappingURL=css-module-name-matches.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"css-module-name-matches.js","sources":["../../../src/rules/css-module-name-matches.mjs"],"sourcesContent":["import path from 'node:path';\nimport fs from 'node:fs';\n\n/** @type {import('eslint').JSRuleDefinition} */\nexport default {\n meta: {\n type: 'problem',\n docs: {\n description:\n \"enforce that a CSS module's filename matches the filename of the importing file\",\n },\n schema: [],\n messages: {\n filenameMismatch:\n 'CSS module filename \"{{cssModuleFilename}}\" does not match the current filename \"{{filename}}\"',\n },\n },\n create(context) {\n return {\n ImportDeclaration(node) {\n if (\n typeof node.source.value !== 'string' ||\n !node.source.value.endsWith('.module.css')\n ) {\n return;\n }\n\n const filename = path.basename(\n context.filename,\n path.extname(context.filename),\n );\n\n const cssModulePath = node.source.value;\n const cssModuleFilename = path.basename(cssModulePath, '.module.css');\n\n if (cssModuleFilename !== filename) {\n context.report({\n node,\n messageId: 'filenameMismatch',\n data: {\n cssModuleFilename,\n filename,\n },\n });\n }\n },\n };\n },\n};\n"],"names":[],"mappings":";;;AAGA;AACA,+BAAe;AACf,EAAE,IAAI,EAAE;AACR,IAAI,IAAI,EAAE,SAAS;AACnB,IAAI,IAAI,EAAE;AACV,MAAM,WAAW;AACjB,QAAQ,iFAAiF;AACzF,KAAK;AACL,IAAI,MAAM,EAAE,EAAE;AACd,IAAI,QAAQ,EAAE;AACd,MAAM,gBAAgB;AACtB,QAAQ,gGAAgG;AACxG,KAAK;AACL,GAAG;AACH,EAAE,MAAM,CAAC,OAAO,EAAE;AAClB,IAAI,OAAO;AACX,MAAM,iBAAiB,CAAC,IAAI,EAAE;AAC9B,QAAQ;AACR,UAAU,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,QAAQ;AAC/C,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,aAAa;AACnD,UAAU;AACV,UAAU;AACV;;AAEA,QAAQ,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ;AACtC,UAAU,OAAO,CAAC,QAAQ;AAC1B,UAAU,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;AACxC,SAAS;;AAET,QAAQ,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK;AAC/C,QAAQ,MAAM,iBAAiB,GAAG,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;;AAE7E,QAAQ,IAAI,iBAAiB,KAAK,QAAQ,EAAE;AAC5C,UAAU,OAAO,CAAC,MAAM,CAAC;AACzB,YAAY,IAAI;AAChB,YAAY,SAAS,EAAE,kBAAkB;AACzC,YAAY,IAAI,EAAE;AAClB,cAAc,iBAAiB;AAC/B,cAAc,QAAQ;AACtB,aAAa;AACb,WAAW,CAAC;AACZ;AACA,OAAO;AACP,KAAK;AACL,GAAG;AACH,CAAC;;;;"}
@@ -0,0 +1,3 @@
1
+ export default plugin;
2
+ /** @type {import('eslint').ESLint.Plugin} */
3
+ declare const plugin: import("eslint").ESLint.Plugin;
@@ -0,0 +1,28 @@
1
+ declare const _default: {
2
+ meta: {
3
+ type: "problem";
4
+ docs: {
5
+ description: string;
6
+ };
7
+ schema: any[];
8
+ messages: {
9
+ relativePath: string;
10
+ defaultImport: string;
11
+ onlyDefaultImport: string;
12
+ fileDoesNotExist: string;
13
+ classDoesNotExist: string;
14
+ };
15
+ };
16
+ create(context: import("@eslint/core", { with: { "resolution-mode": "require" } }).RuleContext<{
17
+ LangOptions: import("eslint").Linter.LanguageOptions;
18
+ Code: import("eslint").SourceCode;
19
+ RuleOptions: unknown[];
20
+ Node: import("eslint").JSSyntaxElement;
21
+ MessageIds: string;
22
+ }>): {
23
+ ImportDeclaration(node: import("estree").ImportDeclaration & import("eslint").Rule.NodeParentExtension): void;
24
+ MemberExpression(node: import("estree").MemberExpression & import("eslint").Rule.NodeParentExtension): void;
25
+ VariableDeclarator(node: import("estree").VariableDeclarator & import("eslint").Rule.NodeParentExtension): void;
26
+ };
27
+ };
28
+ export default _default;
@@ -0,0 +1,22 @@
1
+ declare const _default: {
2
+ meta: {
3
+ type: "problem";
4
+ docs: {
5
+ description: string;
6
+ };
7
+ schema: any[];
8
+ messages: {
9
+ filenameMismatch: string;
10
+ };
11
+ };
12
+ create(context: import("@eslint/core", { with: { "resolution-mode": "require" } }).RuleContext<{
13
+ LangOptions: import("eslint").Linter.LanguageOptions;
14
+ Code: import("eslint").SourceCode;
15
+ RuleOptions: unknown[];
16
+ Node: import("eslint").JSSyntaxElement;
17
+ MessageIds: string;
18
+ }>): {
19
+ ImportDeclaration(node: import("estree").ImportDeclaration & import("eslint").Rule.NodeParentExtension): void;
20
+ };
21
+ };
22
+ export default _default;
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@friendsoftheweb/eslint-plugin",
3
+ "repository": {
4
+ "url": "git+https://github.com/friendsoftheweb/eslint-plugin.git"
5
+ },
6
+ "version": "0.0.1",
7
+ "license": "MIT",
8
+ "packageManager": "yarn@4.12.0",
9
+ "main": "./dist/cjs/index.js",
10
+ "module": "./dist/esm/index.js",
11
+ "types": "./dist/types/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/types/index.d.ts",
15
+ "node": "./dist/cjs/index.js",
16
+ "require": "./dist/cjs/index.js",
17
+ "default": "./dist/esm/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "test": "node test.mjs",
25
+ "types:check": "tsc --noEmit",
26
+ "lint": "eslint",
27
+ "format:check": "prettier --check .",
28
+ "build": "rm -rf dist && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript && tsc -p tsconfig.build.json"
29
+ },
30
+ "devDependencies": {
31
+ "@rollup/plugin-typescript": "^12.1.2",
32
+ "@types/node": "^22.15.31",
33
+ "eslint": "^9.28.0",
34
+ "eslint-plugin-eslint-plugin": "^6.4.0",
35
+ "prettier": "^3.5.3",
36
+ "rollup": "^4.43.0",
37
+ "sinon": "^21.0.0",
38
+ "tslib": "^2.8.1",
39
+ "typescript": "^5.8.3"
40
+ },
41
+ "dependencies": {
42
+ "postcss": "^8.5.5",
43
+ "postcss-selector-parser": "^7.1.0"
44
+ },
45
+ "peerDependencies": {
46
+ "eslint": ">=9.0.0"
47
+ }
48
+ }