eslint-plugin-restrict-replace-import 1.4.0 → 2.0.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 +34 -3
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +354 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +372 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +36 -22
- package/docs/rules/restrict-import.md +0 -166
- package/eslint.config.mjs +0 -43
- package/lib/index.js +0 -18
- package/lib/rules/restrict-import.js +0 -420
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
# Prevent the Import of a Specific Package (`restrict-replace-import/restrict-import`)
|
|
2
|
-
|
|
3
|
-
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
|
|
4
|
-
|
|
5
|
-
<!-- end auto-generated rule header -->
|
|
6
|
-
|
|
7
|
-
This rule aims to prevent the import of a specific package and optionally replace it with an alternative package.
|
|
8
|
-
|
|
9
|
-
## Features
|
|
10
|
-
|
|
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
|
|
17
|
-
|
|
18
|
-
## Options
|
|
19
|
-
|
|
20
|
-
<!-- begin auto-generated rule options list -->
|
|
21
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
```json
|
|
51
|
-
{
|
|
52
|
-
"rules": {
|
|
53
|
-
"restrict-replace-import/restrict-import": [
|
|
54
|
-
"error",
|
|
55
|
-
[{
|
|
56
|
-
"target": "react",
|
|
57
|
-
"replacement": "preact"
|
|
58
|
-
}]
|
|
59
|
-
]
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
```js
|
|
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
|
-
```
|
|
87
|
-
|
|
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
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### Partial String Replacements
|
|
95
|
-
|
|
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
|
-
```
|
|
113
|
-
|
|
114
|
-
```js
|
|
115
|
-
// Before
|
|
116
|
-
import { useState } from 'with-partial-replacements'
|
|
117
|
-
|
|
118
|
-
// After
|
|
119
|
-
import { useState } from 'successfully-replaced'
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
### Restricting Specific Named Imports
|
|
123
|
-
|
|
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
|
-
```
|
|
138
|
-
|
|
139
|
-
This configuration handles various scenarios:
|
|
140
|
-
|
|
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';
|
|
166
|
-
```
|
package/eslint.config.mjs
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview ESLint Plugin for Restricting Import
|
|
3
|
-
* @author shiwoo.park
|
|
4
|
-
*/
|
|
5
|
-
'use strict'
|
|
6
|
-
|
|
7
|
-
//------------------------------------------------------------------------------
|
|
8
|
-
// Requirements
|
|
9
|
-
//------------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
const requireIndex = require('requireindex')
|
|
12
|
-
|
|
13
|
-
//------------------------------------------------------------------------------
|
|
14
|
-
// Plugin Definition
|
|
15
|
-
//------------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
// import all rules in lib/rules
|
|
18
|
-
module.exports.rules = requireIndex(__dirname + '/rules')
|
|
@@ -1,420 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Prevent the Import of a Specific Package
|
|
3
|
-
* @author shiwoo.park
|
|
4
|
-
*/
|
|
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
|
-
}
|
|
293
|
-
|
|
294
|
-
/** @type {import('eslint').Rule.RuleModule} */
|
|
295
|
-
module.exports = {
|
|
296
|
-
meta: {
|
|
297
|
-
type: 'problem',
|
|
298
|
-
docs: {
|
|
299
|
-
description: 'Prevent the Import of a Specific Package',
|
|
300
|
-
recommended: false,
|
|
301
|
-
url: 'https://github.com/custardcream98/eslint-plugin-restrict-replace-import/blob/main/docs/rules/restrict-import.md',
|
|
302
|
-
},
|
|
303
|
-
fixable: 'code',
|
|
304
|
-
|
|
305
|
-
messages: {
|
|
306
|
-
ImportRestriction: '`{{ name }}` is restricted from being used.',
|
|
307
|
-
ImportRestrictionWithReplacement:
|
|
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}}'.",
|
|
312
|
-
},
|
|
313
|
-
|
|
314
|
-
schema: {
|
|
315
|
-
type: 'array',
|
|
316
|
-
maxLength: 1,
|
|
317
|
-
minLength: 1,
|
|
318
|
-
items: {
|
|
319
|
-
type: 'array',
|
|
320
|
-
items: {
|
|
321
|
-
oneOf: [
|
|
322
|
-
{
|
|
323
|
-
type: 'string',
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
type: 'object',
|
|
327
|
-
properties: {
|
|
328
|
-
target: {
|
|
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.',
|
|
337
|
-
},
|
|
338
|
-
replacement: {
|
|
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.',
|
|
350
|
-
},
|
|
351
|
-
},
|
|
352
|
-
required: ['target'],
|
|
353
|
-
additionalProperties: false,
|
|
354
|
-
},
|
|
355
|
-
],
|
|
356
|
-
},
|
|
357
|
-
},
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
|
-
|
|
361
|
-
create(context) {
|
|
362
|
-
const restrictedPackages = createRestrictedPackagesMap(context.options[0])
|
|
363
|
-
|
|
364
|
-
return {
|
|
365
|
-
ImportDeclaration(node) {
|
|
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)
|
|
373
|
-
|
|
374
|
-
if (!checkerResult) return
|
|
375
|
-
|
|
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
|
|
393
|
-
}
|
|
394
|
-
|
|
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
|
-
})
|
|
417
|
-
},
|
|
418
|
-
}
|
|
419
|
-
},
|
|
420
|
-
}
|