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.
@@ -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
- }