eslint-plugin-restrict-replace-import 1.3.0 → 1.5.0
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 +55 -8
- package/docs/rules/restrict-import.md +129 -40
- package/eslint.config.mjs +43 -0
- package/lib/index.js +3 -7
- package/lib/rules/restrict-import.js +493 -112
- package/package.json +11 -6
- package/.eslintrc.js +0 -19
package/README.md
CHANGED
|
@@ -33,10 +33,7 @@ Then configure the rules you want to use under the rules section.
|
|
|
33
33
|
```json
|
|
34
34
|
{
|
|
35
35
|
"rules": {
|
|
36
|
-
"restrict-replace-import/restrict-import": [
|
|
37
|
-
"error",
|
|
38
|
-
["restricted-package1", "restricted-package2"]
|
|
39
|
-
]
|
|
36
|
+
"restrict-replace-import/restrict-import": ["error", ["restricted-package1", "restricted-package2"]]
|
|
40
37
|
}
|
|
41
38
|
}
|
|
42
39
|
```
|
|
@@ -95,7 +92,7 @@ Is it possible as well to perform multiple partial replacements by setting and O
|
|
|
95
92
|
"replacement": {
|
|
96
93
|
"par(regExp)?tial-": "successfully-",
|
|
97
94
|
"repla(regExp)?cements": "replaced",
|
|
98
|
-
"with-": ""
|
|
95
|
+
"with-": ""
|
|
99
96
|
}
|
|
100
97
|
}
|
|
101
98
|
]
|
|
@@ -103,18 +100,68 @@ Is it possible as well to perform multiple partial replacements by setting and O
|
|
|
103
100
|
}
|
|
104
101
|
}
|
|
105
102
|
```
|
|
103
|
+
|
|
106
104
|
Given that rule configuration it will perform the following replacement:
|
|
107
105
|
|
|
108
106
|
Input:
|
|
107
|
+
|
|
109
108
|
```js
|
|
110
109
|
import { useState } from 'with-partial-replacements'
|
|
111
110
|
```
|
|
112
111
|
|
|
113
112
|
Output:
|
|
113
|
+
|
|
114
114
|
```js
|
|
115
115
|
import { useState } from 'successfully-replaced'
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
You can also restrict specific named imports from a package while allowing others:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"rules": {
|
|
123
|
+
"restrict-replace-import/restrict-import": [
|
|
124
|
+
"error",
|
|
125
|
+
[
|
|
126
|
+
{
|
|
127
|
+
"target": "restricted-module",
|
|
128
|
+
"namedImports": ["restrictedImport", "alsoRestricted"],
|
|
129
|
+
"replacement": "replacement-module" // Object is not supported yet
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This configuration will:
|
|
138
|
+
- Allow: `import { allowedImport } from 'restricted-module'`
|
|
139
|
+
- Replace: `import { restrictedImport } from 'restricted-module'` → `import { restrictedImport } from 'replacement-module'`
|
|
140
|
+
|
|
141
|
+
The rule handles various import scenarios:
|
|
142
|
+
- Named imports with aliases: `import { restrictedImport as aliasName }`
|
|
143
|
+
- Mixed allowed/restricted imports: Will split into separate import statements
|
|
144
|
+
- Default exports with named imports
|
|
145
|
+
- Multiple restricted named imports
|
|
146
|
+
- Merging with existing imports from replacement module
|
|
147
|
+
|
|
148
|
+
Examples of complex scenarios:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
// Before
|
|
152
|
+
import { existingImport } from 'replacement-module';
|
|
153
|
+
import { restrictedImport } from 'restricted-module';
|
|
154
|
+
|
|
155
|
+
// After
|
|
156
|
+
import { existingImport, restrictedImport } from 'replacement-module';
|
|
157
|
+
|
|
158
|
+
// Before
|
|
159
|
+
import defaultExport, { restrictedImport, allowed } from 'restricted-module';
|
|
160
|
+
|
|
161
|
+
// After
|
|
162
|
+
import { restrictedImport } from 'replacement-module'
|
|
163
|
+
import defaultExport, { allowed } from 'restricted-module'
|
|
164
|
+
```
|
|
118
165
|
|
|
119
166
|
## Rules
|
|
120
167
|
|
|
@@ -122,8 +169,8 @@ import { useState } from 'successfully-replaced'
|
|
|
122
169
|
|
|
123
170
|
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
|
|
124
171
|
|
|
125
|
-
| Name | Description | 🔧
|
|
126
|
-
| :----------------------------------------------- | :--------------------------------------- |
|
|
127
|
-
| [restrict-import](docs/rules/restrict-import.md) | Prevent the Import of a Specific Package | 🔧
|
|
172
|
+
| Name | Description | 🔧 |
|
|
173
|
+
| :----------------------------------------------- | :--------------------------------------- | :- |
|
|
174
|
+
| [restrict-import](docs/rules/restrict-import.md) | Prevent the Import of a Specific Package | 🔧 |
|
|
128
175
|
|
|
129
176
|
<!-- end auto-generated rules list -->
|
|
@@ -4,74 +4,163 @@
|
|
|
4
4
|
|
|
5
5
|
<!-- end auto-generated rule header -->
|
|
6
6
|
|
|
7
|
-
This rule aims to prevent the import of a specific package.
|
|
7
|
+
This rule aims to prevent the import of a specific package and optionally replace it with an alternative package.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Features
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
- Restrict imports from specific packages
|
|
12
|
+
- Replace restricted imports with alternative packages
|
|
13
|
+
- Support for RegExp patterns in package names
|
|
14
|
+
- Restrict specific named imports while allowing others
|
|
15
|
+
- Partial string replacements using RegExp
|
|
16
|
+
- Automatic merging with existing imports from replacement modules
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
## Options
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
<!-- begin auto-generated rule options list -->
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
| Name | Description | Type | Required |
|
|
23
|
+
| :------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------- |
|
|
24
|
+
| `namedImports` | The named imports to be restricted. If not provided, all named imports will be restricted. | String[] | |
|
|
25
|
+
| `replacement` | The replacement for the import. If a string is provided, it will be used as the replacement for all imports. If an object is provided, the keys will be used as the pattern and the values will be used as the replacement. | | |
|
|
26
|
+
| `target` | The target of the import to be restricted | String | Yes |
|
|
27
|
+
|
|
28
|
+
<!-- end auto-generated rule options list -->
|
|
29
|
+
|
|
30
|
+
## Usage Examples
|
|
31
|
+
|
|
32
|
+
### Basic Usage - Restricting Packages
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"rules": {
|
|
37
|
+
"restrict-replace-import/restrict-import": ["error", ["lodash"]]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This will prevent imports from `lodash`:
|
|
43
|
+
```js
|
|
44
|
+
// ❌ Error: `lodash` is restricted from being used
|
|
45
|
+
import _ from 'lodash'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Replacing with Alternative Package
|
|
18
49
|
|
|
19
50
|
```json
|
|
20
51
|
{
|
|
21
52
|
"rules": {
|
|
22
53
|
"restrict-replace-import/restrict-import": [
|
|
23
|
-
"error",
|
|
24
|
-
[
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
"another-package"
|
|
30
|
-
"with(?:-regex)?-support"
|
|
31
|
-
]
|
|
54
|
+
"error",
|
|
55
|
+
[{
|
|
56
|
+
"target": "react",
|
|
57
|
+
"replacement": "preact"
|
|
58
|
+
}]
|
|
32
59
|
]
|
|
33
60
|
}
|
|
34
61
|
}
|
|
35
62
|
```
|
|
36
63
|
|
|
37
|
-
Examples of **incorrect** code for this rule with options above:
|
|
38
|
-
|
|
39
64
|
```js
|
|
40
|
-
|
|
65
|
+
// Before
|
|
66
|
+
import { useState } from 'react'
|
|
41
67
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
import withRegexSupport from "with-regex-support";
|
|
45
|
-
import withSupport from "with-support";
|
|
68
|
+
// After
|
|
69
|
+
import { useState } from 'preact'
|
|
46
70
|
```
|
|
47
71
|
|
|
48
|
-
|
|
72
|
+
### Using RegExp for Package Names
|
|
49
73
|
|
|
50
|
-
```
|
|
51
|
-
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"rules": {
|
|
77
|
+
"restrict-replace-import/restrict-import": [
|
|
78
|
+
"error",
|
|
79
|
+
[{
|
|
80
|
+
"target": "with(?:-regex)?-support",
|
|
81
|
+
"replacement": "with-support-replacement"
|
|
82
|
+
}]
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
52
87
|
|
|
53
|
-
|
|
88
|
+
This will match and replace both:
|
|
89
|
+
```js
|
|
90
|
+
import { something } from 'with-regex-support' // will be replaced
|
|
91
|
+
import { something } from 'with-support' // will be replaced
|
|
54
92
|
```
|
|
55
93
|
|
|
56
|
-
###
|
|
94
|
+
### Partial String Replacements
|
|
57
95
|
|
|
58
|
-
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"rules": {
|
|
99
|
+
"restrict-replace-import/restrict-import": [
|
|
100
|
+
"error",
|
|
101
|
+
[{
|
|
102
|
+
"target": "with-partial-replacements",
|
|
103
|
+
"replacement": {
|
|
104
|
+
"par(regExp)?tial-": "successfully-",
|
|
105
|
+
"repla(regExp)?cements": "replaced",
|
|
106
|
+
"with-": ""
|
|
107
|
+
}
|
|
108
|
+
}]
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
59
113
|
|
|
60
|
-
|
|
114
|
+
```js
|
|
115
|
+
// Before
|
|
116
|
+
import { useState } from 'with-partial-replacements'
|
|
61
117
|
|
|
62
|
-
|
|
118
|
+
// After
|
|
119
|
+
import { useState } from 'successfully-replaced'
|
|
120
|
+
```
|
|
63
121
|
|
|
64
|
-
|
|
122
|
+
### Restricting Specific Named Imports
|
|
65
123
|
|
|
66
|
-
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"rules": {
|
|
127
|
+
"restrict-replace-import/restrict-import": [
|
|
128
|
+
"error",
|
|
129
|
+
[{
|
|
130
|
+
"target": "restricted-module",
|
|
131
|
+
"namedImports": ["restrictedImport", "alsoRestricted"],
|
|
132
|
+
"replacement": "replacement-module" // Object is not supported yet
|
|
133
|
+
}]
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
67
138
|
|
|
68
|
-
|
|
139
|
+
This configuration handles various scenarios:
|
|
69
140
|
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
141
|
+
```js
|
|
142
|
+
// ✅ Allowed - import not in restricted list
|
|
143
|
+
import { allowedImport } from 'restricted-module'
|
|
144
|
+
|
|
145
|
+
// ❌ Will be replaced
|
|
146
|
+
import { restrictedImport } from 'restricted-module'
|
|
147
|
+
// ↓ Becomes
|
|
148
|
+
import { restrictedImport } from 'replacement-module'
|
|
149
|
+
|
|
150
|
+
// Mixed imports are split
|
|
151
|
+
import { restrictedImport, allowed } from 'restricted-module'
|
|
152
|
+
// ↓ Becomes
|
|
153
|
+
import { restrictedImport } from 'replacement-module'
|
|
154
|
+
import { allowed } from 'restricted-module'
|
|
155
|
+
|
|
156
|
+
// Handles aliases
|
|
157
|
+
import { restrictedImport as aliasName } from 'restricted-module'
|
|
158
|
+
// ↓ Becomes
|
|
159
|
+
import { restrictedImport as aliasName } from 'replacement-module'
|
|
160
|
+
|
|
161
|
+
// Merges with existing imports
|
|
162
|
+
import { existingImport } from 'replacement-module';
|
|
163
|
+
import { restrictedImport } from 'restricted-module';
|
|
164
|
+
// ↓ Becomes
|
|
165
|
+
import { existingImport, restrictedImport } from 'replacement-module';
|
|
77
166
|
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
import eslint from '@eslint/js'
|
|
4
|
+
import eslintPluginEslintPlugin from 'eslint-plugin-eslint-plugin'
|
|
5
|
+
import eslintPluginNode from 'eslint-plugin-node'
|
|
6
|
+
import { fixupPluginRules } from '@eslint/compat'
|
|
7
|
+
import globals from 'globals'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @type {import('eslint').Linter.Config[]}
|
|
11
|
+
*/
|
|
12
|
+
export default [
|
|
13
|
+
eslint.configs.recommended,
|
|
14
|
+
{
|
|
15
|
+
languageOptions: {
|
|
16
|
+
ecmaVersion: 2023,
|
|
17
|
+
sourceType: 'commonjs',
|
|
18
|
+
globals: {
|
|
19
|
+
...globals.node,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
files: ['**/*.js'],
|
|
23
|
+
rules: {
|
|
24
|
+
...eslintPluginEslintPlugin.configs.recommended.rules,
|
|
25
|
+
...eslintPluginNode.configs.recommended.rules,
|
|
26
|
+
},
|
|
27
|
+
plugins: {
|
|
28
|
+
'eslint-plugin': eslintPluginEslintPlugin,
|
|
29
|
+
node: fixupPluginRules(eslintPluginNode),
|
|
30
|
+
},
|
|
31
|
+
linterOptions: {
|
|
32
|
+
reportUnusedDisableDirectives: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
files: ['tests/**/*.js'],
|
|
37
|
+
languageOptions: {
|
|
38
|
+
globals: {
|
|
39
|
+
mocha: 'readonly',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
]
|
package/lib/index.js
CHANGED
|
@@ -2,21 +2,17 @@
|
|
|
2
2
|
* @fileoverview ESLint Plugin for Restricting Import
|
|
3
3
|
* @author shiwoo.park
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
'use strict'
|
|
6
6
|
|
|
7
7
|
//------------------------------------------------------------------------------
|
|
8
8
|
// Requirements
|
|
9
9
|
//------------------------------------------------------------------------------
|
|
10
10
|
|
|
11
|
-
const requireIndex = require(
|
|
11
|
+
const requireIndex = require('requireindex')
|
|
12
12
|
|
|
13
13
|
//------------------------------------------------------------------------------
|
|
14
14
|
// Plugin Definition
|
|
15
15
|
//------------------------------------------------------------------------------
|
|
16
16
|
|
|
17
|
-
|
|
18
17
|
// import all rules in lib/rules
|
|
19
|
-
module.exports.rules = requireIndex(__dirname +
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
module.exports.rules = requireIndex(__dirname + '/rules')
|
|
@@ -2,57 +2,459 @@
|
|
|
2
2
|
* @fileoverview Prevent the Import of a Specific Package
|
|
3
3
|
* @author shiwoo.park
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
'use strict'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {string | {[key: string]: string;}} Replacement
|
|
9
|
+
* @typedef {{replacement: Replacement | null; namedImports: string[] | null;}} ImportRestrictionOptions
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const createRestrictedPackagesMap = (options) => {
|
|
13
|
+
/**
|
|
14
|
+
* @type {Map<RegExp, ImportRestrictionOptions>}
|
|
15
|
+
*/
|
|
16
|
+
const map = new Map()
|
|
17
|
+
options.forEach((config) => {
|
|
18
|
+
if (typeof config === 'string') {
|
|
19
|
+
map.set(new RegExp(`^${config}$`), {
|
|
20
|
+
replacement: null,
|
|
21
|
+
namedImports: null,
|
|
22
|
+
})
|
|
23
|
+
} else {
|
|
24
|
+
map.set(new RegExp(`^${config.target}$`), {
|
|
25
|
+
replacement: config.replacement || null,
|
|
26
|
+
namedImports: config.namedImports || null,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
return map
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} importSource
|
|
35
|
+
* @param {string[]} namedImports
|
|
36
|
+
* @param {Map<RegExp, ImportRestrictionOptions>} restrictedPackages
|
|
37
|
+
*/
|
|
38
|
+
const checkIsRestrictedImport = (importSource, namedImports, restrictedPackages) => {
|
|
39
|
+
for (const [pattern, restrictedPackageOptions] of restrictedPackages) {
|
|
40
|
+
if (pattern.test(importSource)) {
|
|
41
|
+
if (!restrictedPackageOptions.namedImports?.length) {
|
|
42
|
+
return {
|
|
43
|
+
type: 'module',
|
|
44
|
+
pattern,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const restrictedImportedName = restrictedPackageOptions.namedImports.find((namedImport) =>
|
|
49
|
+
namedImports.includes(namedImport),
|
|
50
|
+
)
|
|
51
|
+
if (restrictedImportedName) {
|
|
52
|
+
return {
|
|
53
|
+
type: 'importedName',
|
|
54
|
+
pattern,
|
|
55
|
+
restrictedImportedName,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Strip the beginning and ending of RegExp pattern (e.g. ^pattern$ -> pattern)
|
|
65
|
+
* @param {string} regExpPatternSource
|
|
66
|
+
*/
|
|
67
|
+
const getPatternDisplayName = (regExpPatternSource) => regExpPatternSource.slice(1, -1)
|
|
68
|
+
|
|
69
|
+
const getQuoteStyle = (target) => (target?.includes("'") ? "'" : '"')
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format a list of import specifiers as a string
|
|
73
|
+
* @param {Array<{imported: string, local: string}>} specifiers
|
|
74
|
+
* @returns {string}
|
|
75
|
+
*/
|
|
76
|
+
const formatSpecifiers = (specifiers) => {
|
|
77
|
+
return specifiers.map((s) => (s.imported === s.local ? s.imported : `${s.imported} as ${s.local}`)).join(', ')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates the text for a new import statement
|
|
82
|
+
* @param {Object} options
|
|
83
|
+
* @param {Array} options.specifiers - The import specifiers
|
|
84
|
+
* @param {string} options.source - The import source
|
|
85
|
+
* @param {string} options.quote - The quote style
|
|
86
|
+
* @param {string} options.semicolon - The semicolon (if any)
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
const createImportText = ({ specifiers, source, quote, semicolon = '' }) => {
|
|
90
|
+
const defaultSpecifier = specifiers.find((s) => s.type === 'ImportDefaultSpecifier')
|
|
91
|
+
const namespaceSpecifier = specifiers.find((s) => s.type === 'ImportNamespaceSpecifier')
|
|
92
|
+
const namedSpecifiers = specifiers.filter((s) => s.type === 'ImportSpecifier')
|
|
93
|
+
|
|
94
|
+
if (namespaceSpecifier) {
|
|
95
|
+
return `import * as ${namespaceSpecifier.local.name} from ${quote}${source}${quote}${semicolon}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (defaultSpecifier) {
|
|
99
|
+
if (namedSpecifiers.length === 0) {
|
|
100
|
+
return `import ${defaultSpecifier.local.name} from ${quote}${source}${quote}${semicolon}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const namedText = namedSpecifiers
|
|
104
|
+
.map((s) => (s.imported.name === s.local.name ? s.imported.name : `${s.imported.name} as ${s.local.name}`))
|
|
105
|
+
.join(', ')
|
|
106
|
+
|
|
107
|
+
return `import ${defaultSpecifier.local.name}, { ${namedText} } from ${quote}${source}${quote}${semicolon}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (namedSpecifiers.length > 0) {
|
|
111
|
+
const namedText = namedSpecifiers
|
|
112
|
+
.map((s) => (s.imported.name === s.local.name ? s.imported.name : `${s.imported.name} as ${s.local.name}`))
|
|
113
|
+
.join(', ')
|
|
114
|
+
|
|
115
|
+
return `import { ${namedText} } from ${quote}${source}${quote}${semicolon}`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return `import ${quote}${source}${quote}${semicolon}`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
123
|
+
* @param {import('estree').ImportDeclaration} node
|
|
124
|
+
* @param {string[]} restrictedNames
|
|
125
|
+
* @param {string} replacement
|
|
126
|
+
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
127
|
+
* @deprecated Function no longer used as each restriction is now handled individually
|
|
128
|
+
*/
|
|
129
|
+
// eslint-disable-next-line no-unused-vars
|
|
130
|
+
const createNamedImportReplacer = (context, node, restrictedNames, replacement) => {
|
|
131
|
+
return (fixer) => {
|
|
132
|
+
if (!replacement) return null
|
|
133
|
+
|
|
134
|
+
const quote = getQuoteStyle(node.source.raw)
|
|
135
|
+
const semicolon = node.source.raw.endsWith(';') || node.source.value.endsWith(';') ? ';' : ''
|
|
136
|
+
const fixes = []
|
|
137
|
+
|
|
138
|
+
// Find restricted specifiers to move
|
|
139
|
+
const restrictedSpecifiers = node.specifiers.filter(
|
|
140
|
+
(specifier) => specifier.type === 'ImportSpecifier' && restrictedNames.includes(specifier.imported.name),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if (restrictedSpecifiers.length === 0) {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Format the restricted specifiers for moving
|
|
148
|
+
const specifiersToMove = restrictedSpecifiers.map((specifier) => ({
|
|
149
|
+
imported: specifier.imported.name,
|
|
150
|
+
local: specifier.local.name,
|
|
151
|
+
}))
|
|
152
|
+
|
|
153
|
+
// Handle the original import
|
|
154
|
+
const remainingSpecifiers = node.specifiers.filter(
|
|
155
|
+
(specifier) => specifier.type !== 'ImportSpecifier' || !restrictedNames.includes(specifier.imported.name),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// Remove or update the original import
|
|
159
|
+
if (remainingSpecifiers.length === 0) {
|
|
160
|
+
fixes.push(fixer.remove(node))
|
|
161
|
+
} else if (remainingSpecifiers.length < node.specifiers.length) {
|
|
162
|
+
const newImportText = createImportText({
|
|
163
|
+
specifiers: remainingSpecifiers,
|
|
164
|
+
source: node.source.value,
|
|
165
|
+
quote,
|
|
166
|
+
semicolon,
|
|
167
|
+
})
|
|
168
|
+
fixes.push(fixer.replaceText(node, newImportText))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Add imports to the replacement module
|
|
172
|
+
const { sourceCode } = context
|
|
173
|
+
const allImports = sourceCode.ast.body.filter(
|
|
174
|
+
(node) => node.type === 'ImportDeclaration' && node.source.type === 'Literal',
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const existingReplacementImport = allImports.find((importNode) => importNode.source.value === replacement)
|
|
178
|
+
|
|
179
|
+
if (existingReplacementImport) {
|
|
180
|
+
fixes.push(
|
|
181
|
+
...updateExistingImport(
|
|
182
|
+
fixer,
|
|
183
|
+
sourceCode,
|
|
184
|
+
existingReplacementImport,
|
|
185
|
+
specifiersToMove,
|
|
186
|
+
quote,
|
|
187
|
+
semicolon,
|
|
188
|
+
replacement,
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
} else {
|
|
192
|
+
// Create a new import for the replacement
|
|
193
|
+
const newSpecifiersText = formatSpecifiers(specifiersToMove)
|
|
194
|
+
const newImport = `import { ${newSpecifiersText} } from ${quote}${replacement}${quote}${semicolon}`
|
|
195
|
+
fixes.push(fixer.insertTextBefore(node, newImport + '\n'))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return fixes
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Updates an existing import with new specifiers
|
|
204
|
+
* @param {import('eslint').Rule.RuleFixer} fixer
|
|
205
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
206
|
+
* @param {import('estree').ImportDeclaration} existingImport
|
|
207
|
+
* @param {Array<{imported: string, local: string}>} specifiersToAdd
|
|
208
|
+
* @param {string} quote
|
|
209
|
+
* @param {string} semicolon
|
|
210
|
+
* @param {string} replacement
|
|
211
|
+
* @returns {Array<import('eslint').Rule.Fix>}
|
|
212
|
+
*/
|
|
213
|
+
const updateExistingImport = (fixer, sourceCode, existingImport, specifiersToAdd, quote, semicolon, replacement) => {
|
|
214
|
+
const fixes = []
|
|
215
|
+
const existingNamedSpecifiers = existingImport.specifiers
|
|
216
|
+
.filter((s) => s.type === 'ImportSpecifier')
|
|
217
|
+
.map((s) => s.imported.name)
|
|
218
|
+
|
|
219
|
+
const newSpecifiersToAdd = specifiersToAdd.filter((s) => !existingNamedSpecifiers.includes(s.imported))
|
|
220
|
+
|
|
221
|
+
if (newSpecifiersToAdd.length === 0) {
|
|
222
|
+
return fixes
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const existingText = sourceCode.getText(existingImport)
|
|
226
|
+
const namedSpecifiers = existingImport.specifiers.filter((s) => s.type === 'ImportSpecifier')
|
|
227
|
+
|
|
228
|
+
if (namedSpecifiers.length > 0) {
|
|
229
|
+
// Add new specifiers to existing named imports
|
|
230
|
+
const existingSpecifiersMatch = existingText.match(/import\s*(?:[^{]*,\s*)?{([^}]*)}/)
|
|
231
|
+
if (existingSpecifiersMatch) {
|
|
232
|
+
const existingSpecifiersText = existingSpecifiersMatch[1].trim()
|
|
233
|
+
const newSpecifierText = formatSpecifiers(newSpecifiersToAdd)
|
|
234
|
+
const combinedSpecifiers = `${existingSpecifiersText}, ${newSpecifierText}`
|
|
235
|
+
const newImportText = existingText.replace(/\{[^}]*\}/, `{ ${combinedSpecifiers} }`)
|
|
236
|
+
fixes.push(fixer.replaceText(existingImport, newImportText))
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// Handle imports with default but no named imports
|
|
240
|
+
const defaultSpecifier = existingImport.specifiers.find((s) => s.type === 'ImportDefaultSpecifier')
|
|
241
|
+
if (defaultSpecifier) {
|
|
242
|
+
const defaultName = defaultSpecifier.local.name
|
|
243
|
+
const newSpecifiersText = formatSpecifiers(newSpecifiersToAdd)
|
|
244
|
+
const newText = `import ${defaultName}, { ${newSpecifiersText} } from ${quote}${replacement}${quote}${semicolon}`
|
|
245
|
+
fixes.push(fixer.replaceText(existingImport, newText))
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return fixes
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* @param {import('estree').ImportDeclaration['source']} sourceNode
|
|
254
|
+
* @param {string} replacement
|
|
255
|
+
* @param {'"' | "'"} quote
|
|
256
|
+
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
257
|
+
*/
|
|
258
|
+
const createStringReplacer = (sourceNode, replacement, quote) => {
|
|
259
|
+
return (fixer) => fixer.replaceText(sourceNode, `${quote}${replacement}${quote}`)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {import('estree').ImportDeclaration['source']} sourceNode
|
|
264
|
+
* @param {Replacement} replacementPatterns
|
|
265
|
+
* @param {'"' | "'"} quote
|
|
266
|
+
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
267
|
+
*/
|
|
268
|
+
const createPatternReplacer = (sourceNode, replacementPatterns, quote) => {
|
|
269
|
+
return (fixer) => {
|
|
270
|
+
let result = sourceNode.value
|
|
271
|
+
|
|
272
|
+
if (typeof replacementPatterns === 'string') {
|
|
273
|
+
return createStringReplacer(sourceNode, replacementPatterns, quote)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const [pattern, replacement] of Object.entries(replacementPatterns)) {
|
|
277
|
+
const regex = new RegExp(pattern, 'g')
|
|
278
|
+
result = result.replace(regex, replacement)
|
|
279
|
+
}
|
|
280
|
+
return fixer.replaceText(sourceNode, `${quote}${result}${quote}`)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {import('estree').ImportDeclaration} node
|
|
286
|
+
* @param {Replacement} replacement
|
|
287
|
+
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
288
|
+
*/
|
|
289
|
+
const createModuleReplacer = (node, replacement) => {
|
|
290
|
+
if (!replacement) return null
|
|
291
|
+
|
|
292
|
+
const quote = getQuoteStyle(node.source.raw)
|
|
293
|
+
|
|
294
|
+
if (typeof replacement === 'string') {
|
|
295
|
+
return createStringReplacer(node.source, replacement, quote)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return createPatternReplacer(node.source, replacement, quote)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
303
|
+
* @param {import('estree').ImportDeclaration} node
|
|
304
|
+
* @param {Array<{importName: string, replacement: string}>} importRestrictions
|
|
305
|
+
* @returns {(fixer: import('eslint').Rule.RuleFixer) => any}
|
|
306
|
+
*/
|
|
307
|
+
const createMultiNamedImportReplacer = (context, node, importRestrictions) => {
|
|
308
|
+
return (fixer) => {
|
|
309
|
+
if (!importRestrictions.length) return null
|
|
310
|
+
|
|
311
|
+
const quote = getQuoteStyle(node.source.raw)
|
|
312
|
+
const semicolon = node.source.raw.endsWith(';') || node.source.value.endsWith(';') ? ';' : ''
|
|
313
|
+
const fixes = []
|
|
314
|
+
|
|
315
|
+
const allRestrictedNames = importRestrictions.map((r) => r.importName)
|
|
316
|
+
|
|
317
|
+
// Group imports by replacement
|
|
318
|
+
const groupedByReplacement = importRestrictions.reduce((acc, restriction) => {
|
|
319
|
+
if (!restriction.replacement) return acc
|
|
320
|
+
|
|
321
|
+
if (!acc[restriction.replacement]) {
|
|
322
|
+
acc[restriction.replacement] = []
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
acc[restriction.replacement].push(restriction.importName)
|
|
326
|
+
|
|
327
|
+
return acc
|
|
328
|
+
}, {})
|
|
329
|
+
|
|
330
|
+
// Find non-restricted specifiers from the original import
|
|
331
|
+
const remainingSpecifiers = node.specifiers.filter(
|
|
332
|
+
(specifier) => specifier.type !== 'ImportSpecifier' || !allRestrictedNames.includes(specifier.imported.name),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
// Update or remove the original import
|
|
336
|
+
if (remainingSpecifiers.length === 0) {
|
|
337
|
+
fixes.push(fixer.remove(node))
|
|
338
|
+
} else {
|
|
339
|
+
const newImportText = createImportText({
|
|
340
|
+
specifiers: remainingSpecifiers,
|
|
341
|
+
source: node.source.value,
|
|
342
|
+
quote,
|
|
343
|
+
semicolon,
|
|
344
|
+
})
|
|
345
|
+
fixes.push(fixer.replaceText(node, newImportText))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Create new imports for each replacement module
|
|
349
|
+
const { sourceCode } = context
|
|
350
|
+
const allImports = sourceCode.ast.body.filter(
|
|
351
|
+
(node) => node.type === 'ImportDeclaration' && node.source.type === 'Literal',
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
// Process each replacement module
|
|
355
|
+
Object.entries(groupedByReplacement).forEach(([replacement, restrictedNames]) => {
|
|
356
|
+
// Find specifiers to move
|
|
357
|
+
const specifiersToMove = restrictedNames
|
|
358
|
+
.map((name) => {
|
|
359
|
+
const specifier = node.specifiers.find((s) => s.type === 'ImportSpecifier' && s.imported.name === name)
|
|
360
|
+
return specifier
|
|
361
|
+
? {
|
|
362
|
+
imported: specifier.imported.name,
|
|
363
|
+
local: specifier.local.name,
|
|
364
|
+
}
|
|
365
|
+
: null
|
|
366
|
+
})
|
|
367
|
+
.filter(Boolean)
|
|
368
|
+
|
|
369
|
+
if (specifiersToMove.length === 0) return
|
|
370
|
+
|
|
371
|
+
// Find existing import for the same replacement module
|
|
372
|
+
const existingReplacementImport = allImports.find((importNode) => importNode.source.value === replacement)
|
|
373
|
+
|
|
374
|
+
if (existingReplacementImport) {
|
|
375
|
+
// Add to existing import
|
|
376
|
+
fixes.push(
|
|
377
|
+
...updateExistingImport(
|
|
378
|
+
fixer,
|
|
379
|
+
sourceCode,
|
|
380
|
+
existingReplacementImport,
|
|
381
|
+
specifiersToMove,
|
|
382
|
+
quote,
|
|
383
|
+
semicolon,
|
|
384
|
+
replacement,
|
|
385
|
+
),
|
|
386
|
+
)
|
|
387
|
+
} else {
|
|
388
|
+
// Create new import
|
|
389
|
+
const newSpecifiersText = formatSpecifiers(specifiersToMove)
|
|
390
|
+
const newImport = `import { ${newSpecifiersText} } from ${quote}${replacement}${quote}${semicolon}`
|
|
391
|
+
fixes.push(fixer.insertTextBefore(node, newImport + '\n'))
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
return fixes
|
|
396
|
+
}
|
|
397
|
+
}
|
|
6
398
|
|
|
7
399
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
8
400
|
module.exports = {
|
|
9
401
|
meta: {
|
|
10
|
-
type:
|
|
402
|
+
type: 'problem',
|
|
11
403
|
docs: {
|
|
12
|
-
description:
|
|
13
|
-
"Prevent the Import of a Specific Package",
|
|
404
|
+
description: 'Prevent the Import of a Specific Package',
|
|
14
405
|
recommended: false,
|
|
15
|
-
url:
|
|
406
|
+
url: 'https://github.com/custardcream98/eslint-plugin-restrict-replace-import/blob/main/docs/rules/restrict-import.md',
|
|
16
407
|
},
|
|
17
|
-
fixable:
|
|
408
|
+
fixable: 'code',
|
|
18
409
|
|
|
19
410
|
messages: {
|
|
20
|
-
ImportRestriction:
|
|
21
|
-
"`{{ name }}` is restricted from being used.",
|
|
411
|
+
ImportRestriction: '`{{ name }}` is restricted from being used.',
|
|
22
412
|
ImportRestrictionWithReplacement:
|
|
23
|
-
|
|
413
|
+
'`{{ name }}` is restricted from being used. Replace it with `{{ replacement }}`.',
|
|
414
|
+
ImportedNameRestriction: "Import of '{{importedName}}' from '{{name}}' is restricted",
|
|
415
|
+
ImportedNameRestrictionWithReplacement:
|
|
416
|
+
"Import of '{{importedName}}' from '{{name}}' is restricted. Replace it with '{{replacement}}'.",
|
|
24
417
|
},
|
|
25
418
|
|
|
26
419
|
schema: {
|
|
27
|
-
type:
|
|
420
|
+
type: 'array',
|
|
28
421
|
maxLength: 1,
|
|
29
422
|
minLength: 1,
|
|
30
423
|
items: {
|
|
31
|
-
type:
|
|
424
|
+
type: 'array',
|
|
32
425
|
items: {
|
|
33
426
|
oneOf: [
|
|
34
427
|
{
|
|
35
|
-
type:
|
|
428
|
+
type: 'string',
|
|
36
429
|
},
|
|
37
430
|
{
|
|
38
|
-
type:
|
|
431
|
+
type: 'object',
|
|
39
432
|
properties: {
|
|
40
433
|
target: {
|
|
41
|
-
type:
|
|
434
|
+
type: 'string',
|
|
435
|
+
description: 'The target of the import to be restricted',
|
|
436
|
+
},
|
|
437
|
+
namedImports: {
|
|
438
|
+
type: 'array',
|
|
439
|
+
items: { type: 'string' },
|
|
440
|
+
description:
|
|
441
|
+
'The named imports to be restricted. If not provided, all named imports will be restricted.',
|
|
42
442
|
},
|
|
43
443
|
replacement: {
|
|
44
444
|
oneOf: [
|
|
45
|
-
{ type:
|
|
46
|
-
{
|
|
47
|
-
type:
|
|
445
|
+
{ type: 'string' },
|
|
446
|
+
{
|
|
447
|
+
type: 'object',
|
|
48
448
|
patternProperties: {
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
]
|
|
449
|
+
'.*': { type: 'string' },
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
description:
|
|
454
|
+
'The replacement for the import. If a string is provided, it will be used as the replacement for all imports. If an object is provided, the keys will be used as the pattern and the values will be used as the replacement.',
|
|
53
455
|
},
|
|
54
456
|
},
|
|
55
|
-
required: [
|
|
457
|
+
required: ['target'],
|
|
56
458
|
additionalProperties: false,
|
|
57
459
|
},
|
|
58
460
|
],
|
|
@@ -62,106 +464,85 @@ module.exports = {
|
|
|
62
464
|
},
|
|
63
465
|
|
|
64
466
|
create(context) {
|
|
65
|
-
const restrictedPackages =
|
|
66
|
-
const restrictedPackagesOption = context.options[0];
|
|
67
|
-
|
|
68
|
-
restrictedPackagesOption.forEach((packageName) => {
|
|
69
|
-
if (typeof packageName === "string") {
|
|
70
|
-
restrictedPackages.set(
|
|
71
|
-
new RegExp(`^${packageName}$`),
|
|
72
|
-
null
|
|
73
|
-
);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
restrictedPackages.set(
|
|
78
|
-
new RegExp(`^${packageName.target}$`),
|
|
79
|
-
packageName.replacement
|
|
80
|
-
? packageName.replacement
|
|
81
|
-
: null
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const checkRestricted = (importSource) => {
|
|
86
|
-
const restrictedRegExp = Array.from(
|
|
87
|
-
restrictedPackages.keys()
|
|
88
|
-
).find((packageName) => {
|
|
89
|
-
return packageName.test(importSource);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
isRestricted: !!restrictedRegExp,
|
|
94
|
-
restrictedRegExp,
|
|
95
|
-
};
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const getReplacement = (restrictedRegExp) => {
|
|
99
|
-
const replacement = restrictedPackages.get(
|
|
100
|
-
restrictedRegExp
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
return replacement;
|
|
104
|
-
};
|
|
467
|
+
const restrictedPackages = createRestrictedPackagesMap(context.options[0])
|
|
105
468
|
|
|
106
469
|
return {
|
|
107
470
|
ImportDeclaration(node) {
|
|
108
|
-
|
|
109
|
-
|
|
471
|
+
if (node.source.type !== 'Literal') return
|
|
472
|
+
|
|
473
|
+
const importSource = node.source.value
|
|
474
|
+
const namedImports = node.specifiers
|
|
475
|
+
.filter((specifier) => specifier.type === 'ImportSpecifier')
|
|
476
|
+
.map((specifier) => specifier.imported.name)
|
|
477
|
+
const checkerResult = checkIsRestrictedImport(importSource, namedImports, restrictedPackages)
|
|
478
|
+
|
|
479
|
+
if (!checkerResult) return
|
|
110
480
|
|
|
111
|
-
const
|
|
112
|
-
|
|
481
|
+
const restrictedPackageOptions = restrictedPackages.get(checkerResult.pattern)
|
|
482
|
+
const patternName = getPatternDisplayName(checkerResult.pattern.source)
|
|
113
483
|
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
484
|
+
if (checkerResult.type === 'module') {
|
|
485
|
+
context.report({
|
|
486
|
+
node,
|
|
487
|
+
messageId:
|
|
488
|
+
typeof restrictedPackageOptions.replacement === 'string'
|
|
489
|
+
? 'ImportRestrictionWithReplacement'
|
|
490
|
+
: 'ImportRestriction',
|
|
491
|
+
data: {
|
|
492
|
+
name: patternName,
|
|
493
|
+
replacement: restrictedPackageOptions.replacement,
|
|
494
|
+
},
|
|
495
|
+
fix: createModuleReplacer(node, restrictedPackageOptions.replacement),
|
|
496
|
+
})
|
|
497
|
+
return
|
|
119
498
|
}
|
|
120
499
|
|
|
121
|
-
|
|
122
|
-
restrictedRegExp
|
|
123
|
-
);
|
|
124
|
-
const quote = node.source.raw.includes("'")
|
|
125
|
-
? "'"
|
|
126
|
-
: '"';
|
|
127
|
-
|
|
128
|
-
context.report({
|
|
129
|
-
node,
|
|
130
|
-
messageId: typeof replacement === "string"
|
|
131
|
-
? "ImportRestrictionWithReplacement"
|
|
132
|
-
: "ImportRestriction",
|
|
133
|
-
data: {
|
|
134
|
-
name: restrictedRegExp.toString().slice(2, -2),
|
|
135
|
-
replacement,
|
|
136
|
-
},
|
|
137
|
-
fix: (fixer) => {
|
|
138
|
-
if (!replacement) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
500
|
+
// Find potential rules and replacement mappings for multiple restricted named imports
|
|
141
501
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
);
|
|
147
|
-
}
|
|
502
|
+
/**
|
|
503
|
+
* @type {{importName: string, replacement: string | null, pattern: RegExp}[]}
|
|
504
|
+
*/
|
|
505
|
+
const importRestrictions = []
|
|
148
506
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
507
|
+
// Check each named import for restrictions
|
|
508
|
+
namedImports.forEach((importName) => {
|
|
509
|
+
for (const [pattern, options] of restrictedPackages.entries()) {
|
|
510
|
+
if (
|
|
511
|
+
pattern.test(importSource) &&
|
|
512
|
+
options.namedImports &&
|
|
513
|
+
options.namedImports.includes(importName) &&
|
|
514
|
+
// TODO: handle options.replacement as an object
|
|
515
|
+
(typeof options.replacement === 'string' || options.replacement === null)
|
|
516
|
+
) {
|
|
517
|
+
importRestrictions.push({
|
|
518
|
+
importName,
|
|
519
|
+
replacement: options.replacement,
|
|
520
|
+
pattern,
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
break // Only use the first matching restriction for an import
|
|
159
524
|
}
|
|
525
|
+
}
|
|
526
|
+
})
|
|
160
527
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
528
|
+
if (importRestrictions.length === 0) {
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Report separate errors for each restricted import
|
|
533
|
+
importRestrictions.forEach((restriction) => {
|
|
534
|
+
context.report({
|
|
535
|
+
node,
|
|
536
|
+
messageId: restriction.replacement ? 'ImportedNameRestrictionWithReplacement' : 'ImportedNameRestriction',
|
|
537
|
+
data: {
|
|
538
|
+
importedName: restriction.importName,
|
|
539
|
+
name: importSource,
|
|
540
|
+
replacement: restriction.replacement,
|
|
541
|
+
},
|
|
542
|
+
fix: restriction.replacement ? createMultiNamedImportReplacer(context, node, importRestrictions) : null,
|
|
543
|
+
})
|
|
544
|
+
})
|
|
164
545
|
},
|
|
165
|
-
}
|
|
546
|
+
}
|
|
166
547
|
},
|
|
167
|
-
}
|
|
548
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-restrict-replace-import",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "ESLint Plugin for Restricting and Replacing Import",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -20,22 +20,27 @@
|
|
|
20
20
|
"main": "./lib/index.js",
|
|
21
21
|
"exports": "./lib/index.js",
|
|
22
22
|
"scripts": {
|
|
23
|
+
"format": "prettier --write .",
|
|
23
24
|
"lint": "npm-run-all \"lint:*\"",
|
|
24
25
|
"lint:eslint-docs": "npm-run-all \"update:eslint-docs -- --check\"",
|
|
25
26
|
"lint:js": "eslint .",
|
|
26
27
|
"test": "mocha tests --recursive",
|
|
27
|
-
"update:eslint-docs": "eslint-doc-generator"
|
|
28
|
+
"update:eslint-docs": "eslint-doc-generator",
|
|
29
|
+
"release": "npm run lint && npm run test && npm publish"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
30
32
|
"requireindex": "^1.2.0"
|
|
31
33
|
},
|
|
32
34
|
"devDependencies": {
|
|
33
|
-
"eslint": "^
|
|
34
|
-
"
|
|
35
|
-
"eslint
|
|
35
|
+
"@eslint/compat": "^1.2.7",
|
|
36
|
+
"@types/estree": "^1.0.6",
|
|
37
|
+
"eslint": "^9.22.0",
|
|
38
|
+
"eslint-doc-generator": "^2.1.0",
|
|
39
|
+
"eslint-plugin-eslint-plugin": "^6.4.0",
|
|
36
40
|
"eslint-plugin-node": "^11.1.0",
|
|
37
41
|
"mocha": "^10.0.0",
|
|
38
|
-
"npm-run-all": "^4.1.5"
|
|
42
|
+
"npm-run-all": "^4.1.5",
|
|
43
|
+
"prettier": "^3.5.3"
|
|
39
44
|
},
|
|
40
45
|
"engines": {
|
|
41
46
|
"node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
|
package/.eslintrc.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
module.exports = {
|
|
4
|
-
root: true,
|
|
5
|
-
extends: [
|
|
6
|
-
"eslint:recommended",
|
|
7
|
-
"plugin:eslint-plugin/recommended",
|
|
8
|
-
"plugin:node/recommended",
|
|
9
|
-
],
|
|
10
|
-
env: {
|
|
11
|
-
node: true,
|
|
12
|
-
},
|
|
13
|
-
overrides: [
|
|
14
|
-
{
|
|
15
|
-
files: ["tests/**/*.js"],
|
|
16
|
-
env: { mocha: true },
|
|
17
|
-
},
|
|
18
|
-
],
|
|
19
|
-
};
|