eslint-plugin-restrict-replace-import 1.3.0 → 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 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"
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
- With additional configuration, this rule can also suggest an alternative package to import instead.
9
+ ## Features
10
10
 
11
- If the alternative package is specified, auto-fixing will replace the import statement with the alternative package.
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
- You can use RegExp to match multiple packages.
18
+ ## Options
14
19
 
15
- ## Rule Details
20
+ <!-- begin auto-generated rule options list -->
16
21
 
17
- Example configuration:
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
- "target": "test-package",
27
- "replacement": "replacement-package"
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
- import testPackage from "test-package";
65
+ // Before
66
+ import { useState } from 'react'
41
67
 
42
- import anotherPackage from "another-package";
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
- Examples of **correct** code for this rule with options above:
72
+ ### Using RegExp for Package Names
49
73
 
50
- ```js
51
- import testPackage from "replacement-package";
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
- import theOtherPackage from "the-other-package";
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
- ### Options
94
+ ### Partial String Replacements
57
95
 
58
- This rule takes a single argument, an array of strings or objects.
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
- Each string or object represents a package that should be restricted.
114
+ ```js
115
+ // Before
116
+ import { useState } from 'with-partial-replacements'
61
117
 
62
- If the array element is a string, it represents the name of the package that should be restricted.
118
+ // After
119
+ import { useState } from 'successfully-replaced'
120
+ ```
63
121
 
64
- If the array element is an object, it represents the name of the package that should be restricted and the alternative package that should be suggested instead.
122
+ ### Restricting Specific Named Imports
65
123
 
66
- The alternative package is optional.
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
- Scheme:
139
+ This configuration handles various scenarios:
69
140
 
70
- ```ts
71
- type Restriction =
72
- | string
73
- | {
74
- target: string;
75
- replacement?: string;
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
- "use strict";
5
+ 'use strict'
6
6
 
7
7
  //------------------------------------------------------------------------------
8
8
  // Requirements
9
9
  //------------------------------------------------------------------------------
10
10
 
11
- const requireIndex = require("requireindex");
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 + "/rules");
20
-
21
-
22
-
18
+ module.exports.rules = requireIndex(__dirname + '/rules')
@@ -2,57 +2,354 @@
2
2
  * @fileoverview Prevent the Import of a Specific Package
3
3
  * @author shiwoo.park
4
4
  */
5
- "use strict";
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: "problem",
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: "https://github.com/custardcream98/eslint-plugin-restrict-replace-import/blob/main/docs/rules/restrict-import.md",
301
+ url: 'https://github.com/custardcream98/eslint-plugin-restrict-replace-import/blob/main/docs/rules/restrict-import.md',
16
302
  },
17
- fixable: "code",
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
- "`{{ name }}` is restricted from being used. Replace it with `{{ replacement }}`.",
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: "array",
315
+ type: 'array',
28
316
  maxLength: 1,
29
317
  minLength: 1,
30
318
  items: {
31
- type: "array",
319
+ type: 'array',
32
320
  items: {
33
321
  oneOf: [
34
322
  {
35
- type: "string",
323
+ type: 'string',
36
324
  },
37
325
  {
38
- type: "object",
326
+ type: 'object',
39
327
  properties: {
40
328
  target: {
41
- type: "string",
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: [
45
- { type: "string" },
46
- {
47
- type: "object",
340
+ { type: 'string' },
341
+ {
342
+ type: 'object',
48
343
  patternProperties: {
49
- ".*": { type: "string" }
50
- }
51
- }
52
- ]
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.',
53
350
  },
54
351
  },
55
- required: ["target"],
352
+ required: ['target'],
56
353
  additionalProperties: false,
57
354
  },
58
355
  ],
@@ -62,106 +359,62 @@ module.exports = {
62
359
  },
63
360
 
64
361
  create(context) {
65
- const restrictedPackages = new Map();
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
- };
362
+ const restrictedPackages = createRestrictedPackagesMap(context.options[0])
105
363
 
106
364
  return {
107
365
  ImportDeclaration(node) {
108
- const importSource = node.source.value;
109
- const importSourceType = node.source.type;
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)
110
373
 
111
- const { isRestricted, restrictedRegExp } =
112
- checkRestricted(importSource);
374
+ if (!checkerResult) return
113
375
 
114
- if (
115
- !isRestricted ||
116
- importSourceType !== "Literal"
117
- ) {
118
- return;
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
119
393
  }
120
394
 
121
- const replacement = getReplacement(
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
- }
141
-
142
- if (typeof replacement === "string") {
143
- return fixer.replaceText(
144
- node.source,
145
- `${quote}${replacement}${quote}`
146
- );
147
- }
148
-
149
- if (typeof replacement === "object") {
150
- let partiallyReplaced = node.source.value;
151
- for (const [key, value] of Object.entries(replacement)) {
152
- const regex = new RegExp(key, 'g');
153
- partiallyReplaced = partiallyReplaced.replace(regex, value);
154
- }
155
- return fixer.replaceText(
156
- node.source,
157
- `${quote}${partiallyReplaced}${quote}`
158
- );
159
- }
160
-
161
- return null;
162
- },
163
- });
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
+ })
164
417
  },
165
- };
418
+ }
166
419
  },
167
- };
420
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-restrict-replace-import",
3
- "version": "1.3.0",
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": "^8.19.0",
34
- "eslint-doc-generator": "^1.0.0",
35
- "eslint-plugin-eslint-plugin": "^5.0.0",
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
- };