eslint-plugin-restrict-replace-import 1.2.1 → 1.4.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 +90 -9
- package/docs/rules/restrict-import.md +131 -42
- package/eslint.config.mjs +43 -0
- package/lib/index.js +3 -7
- package/lib/rules/restrict-import.js +371 -94
- package/package.json +11 -6
- package/.eslintrc.js +0 -19
package/README.md
CHANGED
|
@@ -20,11 +20,11 @@ npm install eslint-plugin-restrict-replace-import --save-dev
|
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
23
|
-
Add `restrict-import` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
|
|
23
|
+
Add `restrict-replace-import` to the plugins section of your `.eslintrc` configuration file. You can omit the `eslint-plugin-` prefix:
|
|
24
24
|
|
|
25
25
|
```json
|
|
26
26
|
{
|
|
27
|
-
"plugins": ["restrict-import"]
|
|
27
|
+
"plugins": ["restrict-replace-import"]
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -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
|
```
|
|
@@ -82,14 +79,98 @@ You can use RegExp for package name:
|
|
|
82
79
|
}
|
|
83
80
|
```
|
|
84
81
|
|
|
82
|
+
Is it possible as well to perform multiple partial replacements by setting and Object in the `replacement` property:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"rules": {
|
|
87
|
+
"restrict-replace-import/restrict-import": [
|
|
88
|
+
"error",
|
|
89
|
+
[
|
|
90
|
+
{
|
|
91
|
+
"target": "with-partial-.*",
|
|
92
|
+
"replacement": {
|
|
93
|
+
"par(regExp)?tial-": "successfully-",
|
|
94
|
+
"repla(regExp)?cements": "replaced",
|
|
95
|
+
"with-": ""
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Given that rule configuration it will perform the following replacement:
|
|
105
|
+
|
|
106
|
+
Input:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import { useState } from 'with-partial-replacements'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Output:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
import { useState } from 'successfully-replaced'
|
|
116
|
+
```
|
|
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"
|
|
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
|
+
```
|
|
165
|
+
|
|
85
166
|
## Rules
|
|
86
167
|
|
|
87
168
|
<!-- begin auto-generated rules list -->
|
|
88
169
|
|
|
89
170
|
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
|
|
90
171
|
|
|
91
|
-
| Name | Description | 🔧
|
|
92
|
-
| :----------------------------------------------- | :--------------------------------------- |
|
|
93
|
-
| [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 | 🔧 |
|
|
94
175
|
|
|
95
176
|
<!-- end auto-generated rules list -->
|
|
@@ -1,77 +1,166 @@
|
|
|
1
|
-
# Prevent the Import of a Specific Package (`restrict-import/restrict-import`)
|
|
1
|
+
# Prevent the Import of a Specific Package (`restrict-replace-import/restrict-import`)
|
|
2
2
|
|
|
3
3
|
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
|
|
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
|
|
18
33
|
|
|
19
34
|
```json
|
|
20
35
|
{
|
|
21
36
|
"rules": {
|
|
22
|
-
"restrict-import/restrict-import": [
|
|
23
|
-
"error",
|
|
24
|
-
[
|
|
25
|
-
{
|
|
26
|
-
"target": "test-package",
|
|
27
|
-
"replacement": "replacement-package"
|
|
28
|
-
},
|
|
29
|
-
"another-package"
|
|
30
|
-
"with(?:-regex)?-support"
|
|
31
|
-
]
|
|
32
|
-
]
|
|
37
|
+
"restrict-replace-import/restrict-import": ["error", ["lodash"]]
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
```
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
This will prevent imports from `lodash`:
|
|
39
43
|
```js
|
|
40
|
-
|
|
44
|
+
// ❌ Error: `lodash` is restricted from being used
|
|
45
|
+
import _ from 'lodash'
|
|
46
|
+
```
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
### Replacing with Alternative Package
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"rules": {
|
|
53
|
+
"restrict-replace-import/restrict-import": [
|
|
54
|
+
"error",
|
|
55
|
+
[{
|
|
56
|
+
"target": "react",
|
|
57
|
+
"replacement": "preact"
|
|
58
|
+
}]
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
46
62
|
```
|
|
47
63
|
|
|
48
|
-
Examples of **correct** code for this rule with options above:
|
|
49
|
-
|
|
50
64
|
```js
|
|
51
|
-
|
|
65
|
+
// Before
|
|
66
|
+
import { useState } from 'react'
|
|
67
|
+
|
|
68
|
+
// After
|
|
69
|
+
import { useState } from 'preact'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Using RegExp for Package Names
|
|
73
|
+
|
|
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"
|
|
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,49 +2,354 @@
|
|
|
2
2
|
* @fileoverview Prevent the Import of a Specific Package
|
|
3
3
|
* @author shiwoo.park
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
'use strict'
|
|
6
|
+
|
|
7
|
+
const createRestrictedPackagesMap = (options) => {
|
|
8
|
+
/**
|
|
9
|
+
* @type {Map<RegExp, { replacement: string | { [key: string]: string } | null, namedImports: string[] | null }>}
|
|
10
|
+
*/
|
|
11
|
+
const map = new Map()
|
|
12
|
+
options.forEach((config) => {
|
|
13
|
+
if (typeof config === 'string') {
|
|
14
|
+
map.set(new RegExp(`^${config}$`), {
|
|
15
|
+
replacement: null,
|
|
16
|
+
namedImports: null,
|
|
17
|
+
})
|
|
18
|
+
} else {
|
|
19
|
+
map.set(new RegExp(`^${config.target}$`), {
|
|
20
|
+
replacement: config.replacement || null,
|
|
21
|
+
namedImports: config.namedImports || null,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
return map
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} importSource
|
|
30
|
+
* @param {string[]} namedImports
|
|
31
|
+
* @param {Map<RegExp, { replacement: string | { [key: string]: string } | null, namedImports: string[] | null }>} restrictedPackages
|
|
32
|
+
*/
|
|
33
|
+
const checkIsRestrictedImport = (importSource, namedImports, restrictedPackages) => {
|
|
34
|
+
for (const [pattern, restrictedPackageOptions] of restrictedPackages) {
|
|
35
|
+
if (pattern.test(importSource)) {
|
|
36
|
+
if (!restrictedPackageOptions.namedImports?.length) {
|
|
37
|
+
return {
|
|
38
|
+
type: 'module',
|
|
39
|
+
pattern,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const restrictedImportedName = restrictedPackageOptions.namedImports.find((namedImport) =>
|
|
44
|
+
namedImports.includes(namedImport),
|
|
45
|
+
)
|
|
46
|
+
if (restrictedImportedName) {
|
|
47
|
+
return {
|
|
48
|
+
type: 'importedName',
|
|
49
|
+
pattern,
|
|
50
|
+
restrictedImportedName,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Strip the beginning and ending of RegExp pattern (e.g. ^pattern$ -> pattern)
|
|
60
|
+
* @param {string} regExpPatternSource
|
|
61
|
+
*/
|
|
62
|
+
const getPatternDisplayName = (regExpPatternSource) => regExpPatternSource.slice(1, -1)
|
|
63
|
+
|
|
64
|
+
const getQuoteStyle = (target) => (target?.includes("'") ? "'" : '"')
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Format a list of import specifiers as a string
|
|
68
|
+
* @param {Array<{imported: string, local: string}>} specifiers
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
const formatSpecifiers = (specifiers) => {
|
|
72
|
+
return specifiers.map((s) => (s.imported === s.local ? s.imported : `${s.imported} as ${s.local}`)).join(', ')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Creates the text for a new import statement
|
|
77
|
+
* @param {Object} options
|
|
78
|
+
* @param {Array} options.specifiers - The import specifiers
|
|
79
|
+
* @param {string} options.source - The import source
|
|
80
|
+
* @param {string} options.quote - The quote style
|
|
81
|
+
* @param {string} options.semicolon - The semicolon (if any)
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
const createImportText = ({ specifiers, source, quote, semicolon = '' }) => {
|
|
85
|
+
const defaultSpecifier = specifiers.find((s) => s.type === 'ImportDefaultSpecifier')
|
|
86
|
+
const namespaceSpecifier = specifiers.find((s) => s.type === 'ImportNamespaceSpecifier')
|
|
87
|
+
const namedSpecifiers = specifiers.filter((s) => s.type === 'ImportSpecifier')
|
|
88
|
+
|
|
89
|
+
if (namespaceSpecifier) {
|
|
90
|
+
return `import * as ${namespaceSpecifier.local.name} from ${quote}${source}${quote}${semicolon}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (defaultSpecifier) {
|
|
94
|
+
if (namedSpecifiers.length === 0) {
|
|
95
|
+
return `import ${defaultSpecifier.local.name} from ${quote}${source}${quote}${semicolon}`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const namedText = namedSpecifiers
|
|
99
|
+
.map((s) => (s.imported.name === s.local.name ? s.imported.name : `${s.imported.name} as ${s.local.name}`))
|
|
100
|
+
.join(', ')
|
|
101
|
+
|
|
102
|
+
return `import ${defaultSpecifier.local.name}, { ${namedText} } from ${quote}${source}${quote}${semicolon}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (namedSpecifiers.length > 0) {
|
|
106
|
+
const namedText = namedSpecifiers
|
|
107
|
+
.map((s) => (s.imported.name === s.local.name ? s.imported.name : `${s.imported.name} as ${s.local.name}`))
|
|
108
|
+
.join(', ')
|
|
109
|
+
|
|
110
|
+
return `import { ${namedText} } from ${quote}${source}${quote}${semicolon}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return `import ${quote}${source}${quote}${semicolon}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
118
|
+
* @param {import('estree').ImportDeclaration} node
|
|
119
|
+
* @param {string[]} restrictedNames
|
|
120
|
+
* @param {string} replacement
|
|
121
|
+
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
122
|
+
*/
|
|
123
|
+
const createNamedImportReplacer = (context, node, restrictedNames, replacement) => {
|
|
124
|
+
return (fixer) => {
|
|
125
|
+
if (!replacement) return null
|
|
126
|
+
|
|
127
|
+
const quote = getQuoteStyle(node.source.raw)
|
|
128
|
+
const semicolon = node.source.raw.endsWith(';') || node.source.value.endsWith(';') ? ';' : ''
|
|
129
|
+
const fixes = []
|
|
130
|
+
|
|
131
|
+
// Find restricted specifiers to move
|
|
132
|
+
const restrictedSpecifiers = node.specifiers.filter(
|
|
133
|
+
(specifier) => specifier.type === 'ImportSpecifier' && restrictedNames.includes(specifier.imported.name),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if (restrictedSpecifiers.length === 0) {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Format the restricted specifiers for moving
|
|
141
|
+
const specifiersToMove = restrictedSpecifiers.map((specifier) => ({
|
|
142
|
+
imported: specifier.imported.name,
|
|
143
|
+
local: specifier.local.name,
|
|
144
|
+
}))
|
|
145
|
+
|
|
146
|
+
// Handle the original import
|
|
147
|
+
const remainingSpecifiers = node.specifiers.filter(
|
|
148
|
+
(specifier) => specifier.type !== 'ImportSpecifier' || !restrictedNames.includes(specifier.imported.name),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// Remove or update the original import
|
|
152
|
+
if (remainingSpecifiers.length === 0) {
|
|
153
|
+
fixes.push(fixer.remove(node))
|
|
154
|
+
} else if (remainingSpecifiers.length < node.specifiers.length) {
|
|
155
|
+
const newImportText = createImportText({
|
|
156
|
+
specifiers: remainingSpecifiers,
|
|
157
|
+
source: node.source.value,
|
|
158
|
+
quote,
|
|
159
|
+
semicolon,
|
|
160
|
+
})
|
|
161
|
+
fixes.push(fixer.replaceText(node, newImportText))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Add imports to the replacement module
|
|
165
|
+
const { sourceCode } = context
|
|
166
|
+
const allImports = sourceCode.ast.body.filter(
|
|
167
|
+
(node) => node.type === 'ImportDeclaration' && node.source.type === 'Literal',
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
const existingReplacementImport = allImports.find((importNode) => importNode.source.value === replacement)
|
|
171
|
+
|
|
172
|
+
if (existingReplacementImport) {
|
|
173
|
+
fixes.push(
|
|
174
|
+
...updateExistingImport(
|
|
175
|
+
fixer,
|
|
176
|
+
sourceCode,
|
|
177
|
+
existingReplacementImport,
|
|
178
|
+
specifiersToMove,
|
|
179
|
+
quote,
|
|
180
|
+
semicolon,
|
|
181
|
+
replacement,
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
} else {
|
|
185
|
+
// Create a new import for the replacement
|
|
186
|
+
const newSpecifiersText = formatSpecifiers(specifiersToMove)
|
|
187
|
+
const newImport = `import { ${newSpecifiersText} } from ${quote}${replacement}${quote}${semicolon}`
|
|
188
|
+
fixes.push(fixer.insertTextBefore(node, newImport + '\n'))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return fixes
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Updates an existing import with new specifiers
|
|
197
|
+
* @param {import('eslint').Rule.RuleFixer} fixer
|
|
198
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
199
|
+
* @param {import('estree').ImportDeclaration} existingImport
|
|
200
|
+
* @param {Array<{imported: string, local: string}>} specifiersToAdd
|
|
201
|
+
* @param {string} quote
|
|
202
|
+
* @param {string} semicolon
|
|
203
|
+
* @param {string} replacement
|
|
204
|
+
* @returns {Array<import('eslint').Rule.Fix>}
|
|
205
|
+
*/
|
|
206
|
+
const updateExistingImport = (fixer, sourceCode, existingImport, specifiersToAdd, quote, semicolon, replacement) => {
|
|
207
|
+
const fixes = []
|
|
208
|
+
const existingNamedSpecifiers = existingImport.specifiers
|
|
209
|
+
.filter((s) => s.type === 'ImportSpecifier')
|
|
210
|
+
.map((s) => s.imported.name)
|
|
211
|
+
|
|
212
|
+
const newSpecifiersToAdd = specifiersToAdd.filter((s) => !existingNamedSpecifiers.includes(s.imported))
|
|
213
|
+
|
|
214
|
+
if (newSpecifiersToAdd.length === 0) {
|
|
215
|
+
return fixes
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const existingText = sourceCode.getText(existingImport)
|
|
219
|
+
const namedSpecifiers = existingImport.specifiers.filter((s) => s.type === 'ImportSpecifier')
|
|
220
|
+
|
|
221
|
+
if (namedSpecifiers.length > 0) {
|
|
222
|
+
// Add new specifiers to existing named imports
|
|
223
|
+
const existingSpecifiersMatch = existingText.match(/import\s*(?:[^{]*,\s*)?{([^}]*)}/)
|
|
224
|
+
if (existingSpecifiersMatch) {
|
|
225
|
+
const existingSpecifiersText = existingSpecifiersMatch[1].trim()
|
|
226
|
+
const newSpecifierText = formatSpecifiers(newSpecifiersToAdd)
|
|
227
|
+
const combinedSpecifiers = `${existingSpecifiersText}, ${newSpecifierText}`
|
|
228
|
+
const newImportText = existingText.replace(/\{[^}]*\}/, `{ ${combinedSpecifiers} }`)
|
|
229
|
+
fixes.push(fixer.replaceText(existingImport, newImportText))
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// Handle imports with default but no named imports
|
|
233
|
+
const defaultSpecifier = existingImport.specifiers.find((s) => s.type === 'ImportDefaultSpecifier')
|
|
234
|
+
if (defaultSpecifier) {
|
|
235
|
+
const defaultName = defaultSpecifier.local.name
|
|
236
|
+
const newSpecifiersText = formatSpecifiers(newSpecifiersToAdd)
|
|
237
|
+
const newText = `import ${defaultName}, { ${newSpecifiersText} } from ${quote}${replacement}${quote}${semicolon}`
|
|
238
|
+
fixes.push(fixer.replaceText(existingImport, newText))
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return fixes
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {import('estree').ImportDeclaration['source']} sourceNode
|
|
247
|
+
* @param {string} replacement
|
|
248
|
+
* @param {'"' | "'"} quote
|
|
249
|
+
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
250
|
+
*/
|
|
251
|
+
const createStringReplacer = (sourceNode, replacement, quote) => {
|
|
252
|
+
return (fixer) => fixer.replaceText(sourceNode, `${quote}${replacement}${quote}`)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @param {import('estree').ImportDeclaration['source']} sourceNode
|
|
257
|
+
* @param {string | { [key: string]: string }} replacementPatterns
|
|
258
|
+
* @param {'"' | "'"} quote
|
|
259
|
+
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
260
|
+
*/
|
|
261
|
+
const createPatternReplacer = (sourceNode, replacementPatterns, quote) => {
|
|
262
|
+
return (fixer) => {
|
|
263
|
+
let result = sourceNode.value
|
|
264
|
+
|
|
265
|
+
if (typeof replacementPatterns === 'string') {
|
|
266
|
+
return createStringReplacer(sourceNode, replacementPatterns, quote)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const [pattern, replacement] of Object.entries(replacementPatterns)) {
|
|
270
|
+
const regex = new RegExp(pattern, 'g')
|
|
271
|
+
result = result.replace(regex, replacement)
|
|
272
|
+
}
|
|
273
|
+
return fixer.replaceText(sourceNode, `${quote}${result}${quote}`)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {import('estree').ImportDeclaration} node
|
|
279
|
+
* @param {string | { [key: string]: string }} replacement
|
|
280
|
+
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
281
|
+
*/
|
|
282
|
+
const createModuleReplacer = (node, replacement) => {
|
|
283
|
+
if (!replacement) return null
|
|
284
|
+
|
|
285
|
+
const quote = getQuoteStyle(node.source.raw)
|
|
286
|
+
|
|
287
|
+
if (typeof replacement === 'string') {
|
|
288
|
+
return createStringReplacer(node.source, replacement, quote)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return createPatternReplacer(node.source, replacement, quote)
|
|
292
|
+
}
|
|
6
293
|
|
|
7
294
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
8
295
|
module.exports = {
|
|
9
296
|
meta: {
|
|
10
|
-
type:
|
|
297
|
+
type: 'problem',
|
|
11
298
|
docs: {
|
|
12
|
-
description:
|
|
13
|
-
"Prevent the Import of a Specific Package",
|
|
299
|
+
description: 'Prevent the Import of a Specific Package',
|
|
14
300
|
recommended: false,
|
|
15
|
-
url:
|
|
301
|
+
url: 'https://github.com/custardcream98/eslint-plugin-restrict-replace-import/blob/main/docs/rules/restrict-import.md',
|
|
16
302
|
},
|
|
17
|
-
fixable:
|
|
303
|
+
fixable: 'code',
|
|
18
304
|
|
|
19
305
|
messages: {
|
|
20
|
-
ImportRestriction:
|
|
21
|
-
"`{{ name }}` is restricted from being used.",
|
|
306
|
+
ImportRestriction: '`{{ name }}` is restricted from being used.',
|
|
22
307
|
ImportRestrictionWithReplacement:
|
|
23
|
-
|
|
308
|
+
'`{{ name }}` is restricted from being used. Replace it with `{{ replacement }}`.',
|
|
309
|
+
ImportedNameRestriction: "Import of '{{importedName}}' from '{{name}}' is restricted",
|
|
310
|
+
ImportedNameRestrictionWithReplacement:
|
|
311
|
+
"Import of '{{importedName}}' from '{{name}}' is restricted. Replace it with '{{replacement}}'.",
|
|
24
312
|
},
|
|
25
313
|
|
|
26
314
|
schema: {
|
|
27
|
-
type:
|
|
315
|
+
type: 'array',
|
|
28
316
|
maxLength: 1,
|
|
29
317
|
minLength: 1,
|
|
30
318
|
items: {
|
|
31
|
-
type:
|
|
319
|
+
type: 'array',
|
|
32
320
|
items: {
|
|
33
321
|
oneOf: [
|
|
34
322
|
{
|
|
35
|
-
type:
|
|
323
|
+
type: 'string',
|
|
36
324
|
},
|
|
37
325
|
{
|
|
38
|
-
type:
|
|
326
|
+
type: 'object',
|
|
39
327
|
properties: {
|
|
40
328
|
target: {
|
|
41
|
-
type:
|
|
329
|
+
type: 'string',
|
|
330
|
+
description: 'The target of the import to be restricted',
|
|
331
|
+
},
|
|
332
|
+
namedImports: {
|
|
333
|
+
type: 'array',
|
|
334
|
+
items: { type: 'string' },
|
|
335
|
+
description:
|
|
336
|
+
'The named imports to be restricted. If not provided, all named imports will be restricted.',
|
|
42
337
|
},
|
|
43
338
|
replacement: {
|
|
44
|
-
|
|
339
|
+
oneOf: [
|
|
340
|
+
{ type: 'string' },
|
|
341
|
+
{
|
|
342
|
+
type: 'object',
|
|
343
|
+
patternProperties: {
|
|
344
|
+
'.*': { type: 'string' },
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
description:
|
|
349
|
+
'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.',
|
|
45
350
|
},
|
|
46
351
|
},
|
|
47
|
-
required: [
|
|
352
|
+
required: ['target'],
|
|
48
353
|
additionalProperties: false,
|
|
49
354
|
},
|
|
50
355
|
],
|
|
@@ -54,90 +359,62 @@ module.exports = {
|
|
|
54
359
|
},
|
|
55
360
|
|
|
56
361
|
create(context) {
|
|
57
|
-
const restrictedPackages =
|
|
58
|
-
const restrictedPackagesOption = context.options[0];
|
|
59
|
-
|
|
60
|
-
restrictedPackagesOption.forEach((packageName) => {
|
|
61
|
-
if (typeof packageName === "string") {
|
|
62
|
-
restrictedPackages.set(
|
|
63
|
-
new RegExp(`^${packageName}$`),
|
|
64
|
-
null
|
|
65
|
-
);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
restrictedPackages.set(
|
|
70
|
-
new RegExp(`^${packageName.target}$`),
|
|
71
|
-
packageName.replacement
|
|
72
|
-
? packageName.replacement
|
|
73
|
-
: null
|
|
74
|
-
);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const checkRestricted = (importSource) => {
|
|
78
|
-
const restrictedRegExp = Array.from(
|
|
79
|
-
restrictedPackages.keys()
|
|
80
|
-
).find((packageName) => {
|
|
81
|
-
return packageName.test(importSource);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
isRestricted: !!restrictedRegExp,
|
|
86
|
-
restrictedRegExp,
|
|
87
|
-
};
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const getReplacement = (restrictedRegExp) => {
|
|
91
|
-
const replacement = restrictedPackages.get(
|
|
92
|
-
restrictedRegExp
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
return replacement;
|
|
96
|
-
};
|
|
362
|
+
const restrictedPackages = createRestrictedPackagesMap(context.options[0])
|
|
97
363
|
|
|
98
364
|
return {
|
|
99
365
|
ImportDeclaration(node) {
|
|
100
|
-
|
|
101
|
-
|
|
366
|
+
if (node.source.type !== 'Literal') return
|
|
367
|
+
|
|
368
|
+
const importSource = node.source.value
|
|
369
|
+
const namedImports = node.specifiers
|
|
370
|
+
.filter((specifier) => specifier.type === 'ImportSpecifier')
|
|
371
|
+
.map((specifier) => specifier.imported.name)
|
|
372
|
+
const checkerResult = checkIsRestrictedImport(importSource, namedImports, restrictedPackages)
|
|
102
373
|
|
|
103
|
-
|
|
104
|
-
checkRestricted(importSource);
|
|
374
|
+
if (!checkerResult) return
|
|
105
375
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
) {
|
|
110
|
-
|
|
376
|
+
const restrictedPackageOptions = restrictedPackages.get(checkerResult.pattern)
|
|
377
|
+
const patternName = getPatternDisplayName(checkerResult.pattern.source)
|
|
378
|
+
|
|
379
|
+
if (checkerResult.type === 'module') {
|
|
380
|
+
context.report({
|
|
381
|
+
node,
|
|
382
|
+
messageId:
|
|
383
|
+
typeof restrictedPackageOptions.replacement === 'string'
|
|
384
|
+
? 'ImportRestrictionWithReplacement'
|
|
385
|
+
: 'ImportRestriction',
|
|
386
|
+
data: {
|
|
387
|
+
name: patternName,
|
|
388
|
+
replacement: restrictedPackageOptions.replacement,
|
|
389
|
+
},
|
|
390
|
+
fix: createModuleReplacer(node, restrictedPackageOptions.replacement),
|
|
391
|
+
})
|
|
392
|
+
return
|
|
111
393
|
}
|
|
112
394
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
node.source,
|
|
136
|
-
`${quote}${replacement}${quote}`
|
|
137
|
-
);
|
|
138
|
-
},
|
|
139
|
-
});
|
|
395
|
+
const restrictedImports = restrictedPackageOptions.namedImports.filter((name) => namedImports.includes(name))
|
|
396
|
+
|
|
397
|
+
restrictedImports.forEach((restrictedImportedName) => {
|
|
398
|
+
context.report({
|
|
399
|
+
node,
|
|
400
|
+
messageId:
|
|
401
|
+
typeof restrictedPackageOptions.replacement === 'string'
|
|
402
|
+
? 'ImportedNameRestrictionWithReplacement'
|
|
403
|
+
: 'ImportedNameRestriction',
|
|
404
|
+
data: {
|
|
405
|
+
importedName: restrictedImportedName,
|
|
406
|
+
name: importSource,
|
|
407
|
+
replacement: restrictedPackageOptions.replacement,
|
|
408
|
+
},
|
|
409
|
+
fix: createNamedImportReplacer(
|
|
410
|
+
context,
|
|
411
|
+
node,
|
|
412
|
+
restrictedPackageOptions.namedImports,
|
|
413
|
+
restrictedPackageOptions.replacement,
|
|
414
|
+
),
|
|
415
|
+
})
|
|
416
|
+
})
|
|
140
417
|
},
|
|
141
|
-
}
|
|
418
|
+
}
|
|
142
419
|
},
|
|
143
|
-
}
|
|
420
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-restrict-replace-import",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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
|
+
"publish": "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
|
-
};
|