@chacki/eslint-plugin-require-extensions 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/.eslintrc.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": ["plugin:@chacki/require-extensions/recommended"],
3
+ "parserOptions": {
4
+ "sourceType": "module",
5
+ "ecmaVersion": 2020
6
+ },
7
+ "ignorePatterns": ["/index.js"]
8
+ }
@@ -0,0 +1 @@
1
+ package-lock.json
package/.prettierrc ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "bracketSpacing": true,
5
+ "trailingComma": "all",
6
+ "printWidth": 80,
7
+ "tabWidth": 2,
8
+ "singleAttributePerLine": false,
9
+ "useTabs": false,
10
+ "endOfLine": "auto",
11
+ "arrowParens": "always"
12
+ }
package/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # @chacki/eslint-plugin-require-extensions
2
+
3
+ Custom ESLint rules to enforce `.js` extensions in ESM imports (also converts .ts/.tsx to .js).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install --save-dev @chacki/eslint-plugin-require-extensions
package/index.js ADDED
@@ -0,0 +1,136 @@
1
+ const { existsSync, lstatSync } = require('fs');
2
+ const { dirname, resolve } = require('path');
3
+
4
+ module.exports = {
5
+ rules: {
6
+ 'require-extensions': {
7
+ meta: {
8
+ type: 'problem',
9
+ docs: {
10
+ description:
11
+ 'Относительные импорты/экспорты должны заканчиваться на .js (или .ts→.js/.tsx→.js)',
12
+ category: 'ESM',
13
+ recommended: true,
14
+ },
15
+ fixable: 'code',
16
+ schema: [
17
+ {
18
+ type: 'object',
19
+ properties: {
20
+ extensions: {
21
+ type: 'array',
22
+ items: { type: 'string' },
23
+ default: ['.js'],
24
+ },
25
+ extMapping: {
26
+ type: 'object',
27
+ additionalProperties: { type: 'string' },
28
+ default: { '.ts': '.js', '.tsx': '.js' },
29
+ },
30
+ },
31
+ additionalProperties: false,
32
+ },
33
+ ],
34
+ },
35
+ create(context) {
36
+ const opts = context.options[0] || {};
37
+ const extensions = opts.extensions || ['.js'];
38
+ const extMapping = opts.extMapping || { '.ts': '.js', '.tsx': '.js' };
39
+
40
+ function checkNode(node) {
41
+ const source = node.source && node.source.value;
42
+ if (!source) return;
43
+ const raw = source.replace(/\?.*$/, ''); // убираем query
44
+ if (!raw.startsWith('.') || extensions.some((e) => raw.endsWith(e))) {
45
+ return;
46
+ }
47
+
48
+ const absPath = resolve(dirname(context.getFilename()), raw);
49
+ for (const ext of extensions) {
50
+ if (existsSync(absPath + ext)) {
51
+ // найден .js-файл
52
+ return context.report({
53
+ node: node.source,
54
+ message: 'Relative import/ export must end with {{ext}}',
55
+ data: { ext },
56
+ fix(fixer) {
57
+ return fixer.replaceText(node.source, `'${raw + ext}'`);
58
+ },
59
+ });
60
+ }
61
+ }
62
+ for (const [from, to] of Object.entries(extMapping)) {
63
+ if (raw.endsWith(from)) {
64
+ const without = raw.slice(0, -from.length);
65
+ const target = without + to;
66
+ if (existsSync(resolve(dirname(context.getFilename()), target))) {
67
+ return context.report({
68
+ node: node.source,
69
+ message: 'Change extension from {{from}} to {{to}}',
70
+ data: { from, to },
71
+ fix(fixer) {
72
+ return fixer.replaceText(node.source, `'${target}'`);
73
+ },
74
+ });
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ return {
81
+ ImportDeclaration: checkNode,
82
+ ExportNamedDeclaration: checkNode,
83
+ ExportAllDeclaration: checkNode,
84
+ };
85
+ },
86
+ },
87
+
88
+ 'require-index': {
89
+ meta: {
90
+ type: 'problem',
91
+ docs: {
92
+ description: 'Directory imports must end with /index.js',
93
+ category: 'ESM',
94
+ recommended: true,
95
+ },
96
+ fixable: 'code',
97
+ schema: [],
98
+ },
99
+ create(context) {
100
+ function checkNode(node) {
101
+ const source = node.source && node.source.value;
102
+ if (!source) return;
103
+ const raw = source.replace(/\?.*$/, '');
104
+ if (!raw.startsWith('.')) return;
105
+
106
+ const absPath = resolve(dirname(context.getFilename()), raw);
107
+ if (existsSync(absPath) && lstatSync(absPath).isDirectory()) {
108
+ return context.report({
109
+ node: node.source,
110
+ message: 'Directory imports must end with /index.js',
111
+ fix(fixer) {
112
+ return fixer.replaceText(node.source, `'${raw}/index.js'`);
113
+ },
114
+ });
115
+ }
116
+ }
117
+
118
+ return {
119
+ ImportDeclaration: checkNode,
120
+ ExportNamedDeclaration: checkNode,
121
+ ExportAllDeclaration: checkNode,
122
+ };
123
+ },
124
+ },
125
+ },
126
+
127
+ configs: {
128
+ recommended: {
129
+ plugins: ['@chacki/require-extensions'],
130
+ rules: {
131
+ '@chacki/require-extensions/require-extensions': 'error',
132
+ '@chacki/require-extensions/require-index': 'error',
133
+ },
134
+ },
135
+ },
136
+ };
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@chacki/eslint-plugin-require-extensions",
3
+ "version": "0.0.1",
4
+ "main": "./index.js",
5
+ "author": "Chacki",
6
+ "type": "commonjs",
7
+ "license": "ISC",
8
+ "keywords": ["eslint", "eslintplugin", "eslint-plugin", "esm", "extensions"],
9
+ "scripts": {
10
+ "fmt": "prettier --write '{*,**/*}.{ts,tsx,js,jsx,json}'",
11
+ "test": "eslint . --report-unused-disable-directives"
12
+ },
13
+ "exports": {
14
+ "require": "./index.js"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "peerDependencies": {
20
+ "eslint": "*"
21
+ },
22
+ "devDependencies": {
23
+ "eslint": "^8.57.1",
24
+ "prettier": "^3.5.3"
25
+ }
26
+ }