eslint-plugin-restrict-replace-import 1.5.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 +35 -4
- 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 -548
|
@@ -1,548 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Prevent the Import of a Specific Package
|
|
3
|
-
* @author shiwoo.park
|
|
4
|
-
*/
|
|
5
|
-
'use strict'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @typedef {string | {[key: string]: string;}} Replacement
|
|
9
|
-
* @typedef {{replacement: Replacement | null; namedImports: string[] | null;}} ImportRestrictionOptions
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const createRestrictedPackagesMap = (options) => {
|
|
13
|
-
/**
|
|
14
|
-
* @type {Map<RegExp, ImportRestrictionOptions>}
|
|
15
|
-
*/
|
|
16
|
-
const map = new Map()
|
|
17
|
-
options.forEach((config) => {
|
|
18
|
-
if (typeof config === 'string') {
|
|
19
|
-
map.set(new RegExp(`^${config}$`), {
|
|
20
|
-
replacement: null,
|
|
21
|
-
namedImports: null,
|
|
22
|
-
})
|
|
23
|
-
} else {
|
|
24
|
-
map.set(new RegExp(`^${config.target}$`), {
|
|
25
|
-
replacement: config.replacement || null,
|
|
26
|
-
namedImports: config.namedImports || null,
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
return map
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* @param {string} importSource
|
|
35
|
-
* @param {string[]} namedImports
|
|
36
|
-
* @param {Map<RegExp, ImportRestrictionOptions>} restrictedPackages
|
|
37
|
-
*/
|
|
38
|
-
const checkIsRestrictedImport = (importSource, namedImports, restrictedPackages) => {
|
|
39
|
-
for (const [pattern, restrictedPackageOptions] of restrictedPackages) {
|
|
40
|
-
if (pattern.test(importSource)) {
|
|
41
|
-
if (!restrictedPackageOptions.namedImports?.length) {
|
|
42
|
-
return {
|
|
43
|
-
type: 'module',
|
|
44
|
-
pattern,
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const restrictedImportedName = restrictedPackageOptions.namedImports.find((namedImport) =>
|
|
49
|
-
namedImports.includes(namedImport),
|
|
50
|
-
)
|
|
51
|
-
if (restrictedImportedName) {
|
|
52
|
-
return {
|
|
53
|
-
type: 'importedName',
|
|
54
|
-
pattern,
|
|
55
|
-
restrictedImportedName,
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return null
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Strip the beginning and ending of RegExp pattern (e.g. ^pattern$ -> pattern)
|
|
65
|
-
* @param {string} regExpPatternSource
|
|
66
|
-
*/
|
|
67
|
-
const getPatternDisplayName = (regExpPatternSource) => regExpPatternSource.slice(1, -1)
|
|
68
|
-
|
|
69
|
-
const getQuoteStyle = (target) => (target?.includes("'") ? "'" : '"')
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Format a list of import specifiers as a string
|
|
73
|
-
* @param {Array<{imported: string, local: string}>} specifiers
|
|
74
|
-
* @returns {string}
|
|
75
|
-
*/
|
|
76
|
-
const formatSpecifiers = (specifiers) => {
|
|
77
|
-
return specifiers.map((s) => (s.imported === s.local ? s.imported : `${s.imported} as ${s.local}`)).join(', ')
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Creates the text for a new import statement
|
|
82
|
-
* @param {Object} options
|
|
83
|
-
* @param {Array} options.specifiers - The import specifiers
|
|
84
|
-
* @param {string} options.source - The import source
|
|
85
|
-
* @param {string} options.quote - The quote style
|
|
86
|
-
* @param {string} options.semicolon - The semicolon (if any)
|
|
87
|
-
* @returns {string}
|
|
88
|
-
*/
|
|
89
|
-
const createImportText = ({ specifiers, source, quote, semicolon = '' }) => {
|
|
90
|
-
const defaultSpecifier = specifiers.find((s) => s.type === 'ImportDefaultSpecifier')
|
|
91
|
-
const namespaceSpecifier = specifiers.find((s) => s.type === 'ImportNamespaceSpecifier')
|
|
92
|
-
const namedSpecifiers = specifiers.filter((s) => s.type === 'ImportSpecifier')
|
|
93
|
-
|
|
94
|
-
if (namespaceSpecifier) {
|
|
95
|
-
return `import * as ${namespaceSpecifier.local.name} from ${quote}${source}${quote}${semicolon}`
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (defaultSpecifier) {
|
|
99
|
-
if (namedSpecifiers.length === 0) {
|
|
100
|
-
return `import ${defaultSpecifier.local.name} from ${quote}${source}${quote}${semicolon}`
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const namedText = namedSpecifiers
|
|
104
|
-
.map((s) => (s.imported.name === s.local.name ? s.imported.name : `${s.imported.name} as ${s.local.name}`))
|
|
105
|
-
.join(', ')
|
|
106
|
-
|
|
107
|
-
return `import ${defaultSpecifier.local.name}, { ${namedText} } from ${quote}${source}${quote}${semicolon}`
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (namedSpecifiers.length > 0) {
|
|
111
|
-
const namedText = namedSpecifiers
|
|
112
|
-
.map((s) => (s.imported.name === s.local.name ? s.imported.name : `${s.imported.name} as ${s.local.name}`))
|
|
113
|
-
.join(', ')
|
|
114
|
-
|
|
115
|
-
return `import { ${namedText} } from ${quote}${source}${quote}${semicolon}`
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return `import ${quote}${source}${quote}${semicolon}`
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* @param {import('eslint').Rule.RuleContext} context
|
|
123
|
-
* @param {import('estree').ImportDeclaration} node
|
|
124
|
-
* @param {string[]} restrictedNames
|
|
125
|
-
* @param {string} replacement
|
|
126
|
-
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
127
|
-
* @deprecated Function no longer used as each restriction is now handled individually
|
|
128
|
-
*/
|
|
129
|
-
// eslint-disable-next-line no-unused-vars
|
|
130
|
-
const createNamedImportReplacer = (context, node, restrictedNames, replacement) => {
|
|
131
|
-
return (fixer) => {
|
|
132
|
-
if (!replacement) return null
|
|
133
|
-
|
|
134
|
-
const quote = getQuoteStyle(node.source.raw)
|
|
135
|
-
const semicolon = node.source.raw.endsWith(';') || node.source.value.endsWith(';') ? ';' : ''
|
|
136
|
-
const fixes = []
|
|
137
|
-
|
|
138
|
-
// Find restricted specifiers to move
|
|
139
|
-
const restrictedSpecifiers = node.specifiers.filter(
|
|
140
|
-
(specifier) => specifier.type === 'ImportSpecifier' && restrictedNames.includes(specifier.imported.name),
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
if (restrictedSpecifiers.length === 0) {
|
|
144
|
-
return null
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Format the restricted specifiers for moving
|
|
148
|
-
const specifiersToMove = restrictedSpecifiers.map((specifier) => ({
|
|
149
|
-
imported: specifier.imported.name,
|
|
150
|
-
local: specifier.local.name,
|
|
151
|
-
}))
|
|
152
|
-
|
|
153
|
-
// Handle the original import
|
|
154
|
-
const remainingSpecifiers = node.specifiers.filter(
|
|
155
|
-
(specifier) => specifier.type !== 'ImportSpecifier' || !restrictedNames.includes(specifier.imported.name),
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
// Remove or update the original import
|
|
159
|
-
if (remainingSpecifiers.length === 0) {
|
|
160
|
-
fixes.push(fixer.remove(node))
|
|
161
|
-
} else if (remainingSpecifiers.length < node.specifiers.length) {
|
|
162
|
-
const newImportText = createImportText({
|
|
163
|
-
specifiers: remainingSpecifiers,
|
|
164
|
-
source: node.source.value,
|
|
165
|
-
quote,
|
|
166
|
-
semicolon,
|
|
167
|
-
})
|
|
168
|
-
fixes.push(fixer.replaceText(node, newImportText))
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Add imports to the replacement module
|
|
172
|
-
const { sourceCode } = context
|
|
173
|
-
const allImports = sourceCode.ast.body.filter(
|
|
174
|
-
(node) => node.type === 'ImportDeclaration' && node.source.type === 'Literal',
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
const existingReplacementImport = allImports.find((importNode) => importNode.source.value === replacement)
|
|
178
|
-
|
|
179
|
-
if (existingReplacementImport) {
|
|
180
|
-
fixes.push(
|
|
181
|
-
...updateExistingImport(
|
|
182
|
-
fixer,
|
|
183
|
-
sourceCode,
|
|
184
|
-
existingReplacementImport,
|
|
185
|
-
specifiersToMove,
|
|
186
|
-
quote,
|
|
187
|
-
semicolon,
|
|
188
|
-
replacement,
|
|
189
|
-
),
|
|
190
|
-
)
|
|
191
|
-
} else {
|
|
192
|
-
// Create a new import for the replacement
|
|
193
|
-
const newSpecifiersText = formatSpecifiers(specifiersToMove)
|
|
194
|
-
const newImport = `import { ${newSpecifiersText} } from ${quote}${replacement}${quote}${semicolon}`
|
|
195
|
-
fixes.push(fixer.insertTextBefore(node, newImport + '\n'))
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return fixes
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Updates an existing import with new specifiers
|
|
204
|
-
* @param {import('eslint').Rule.RuleFixer} fixer
|
|
205
|
-
* @param {import('eslint').SourceCode} sourceCode
|
|
206
|
-
* @param {import('estree').ImportDeclaration} existingImport
|
|
207
|
-
* @param {Array<{imported: string, local: string}>} specifiersToAdd
|
|
208
|
-
* @param {string} quote
|
|
209
|
-
* @param {string} semicolon
|
|
210
|
-
* @param {string} replacement
|
|
211
|
-
* @returns {Array<import('eslint').Rule.Fix>}
|
|
212
|
-
*/
|
|
213
|
-
const updateExistingImport = (fixer, sourceCode, existingImport, specifiersToAdd, quote, semicolon, replacement) => {
|
|
214
|
-
const fixes = []
|
|
215
|
-
const existingNamedSpecifiers = existingImport.specifiers
|
|
216
|
-
.filter((s) => s.type === 'ImportSpecifier')
|
|
217
|
-
.map((s) => s.imported.name)
|
|
218
|
-
|
|
219
|
-
const newSpecifiersToAdd = specifiersToAdd.filter((s) => !existingNamedSpecifiers.includes(s.imported))
|
|
220
|
-
|
|
221
|
-
if (newSpecifiersToAdd.length === 0) {
|
|
222
|
-
return fixes
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const existingText = sourceCode.getText(existingImport)
|
|
226
|
-
const namedSpecifiers = existingImport.specifiers.filter((s) => s.type === 'ImportSpecifier')
|
|
227
|
-
|
|
228
|
-
if (namedSpecifiers.length > 0) {
|
|
229
|
-
// Add new specifiers to existing named imports
|
|
230
|
-
const existingSpecifiersMatch = existingText.match(/import\s*(?:[^{]*,\s*)?{([^}]*)}/)
|
|
231
|
-
if (existingSpecifiersMatch) {
|
|
232
|
-
const existingSpecifiersText = existingSpecifiersMatch[1].trim()
|
|
233
|
-
const newSpecifierText = formatSpecifiers(newSpecifiersToAdd)
|
|
234
|
-
const combinedSpecifiers = `${existingSpecifiersText}, ${newSpecifierText}`
|
|
235
|
-
const newImportText = existingText.replace(/\{[^}]*\}/, `{ ${combinedSpecifiers} }`)
|
|
236
|
-
fixes.push(fixer.replaceText(existingImport, newImportText))
|
|
237
|
-
}
|
|
238
|
-
} else {
|
|
239
|
-
// Handle imports with default but no named imports
|
|
240
|
-
const defaultSpecifier = existingImport.specifiers.find((s) => s.type === 'ImportDefaultSpecifier')
|
|
241
|
-
if (defaultSpecifier) {
|
|
242
|
-
const defaultName = defaultSpecifier.local.name
|
|
243
|
-
const newSpecifiersText = formatSpecifiers(newSpecifiersToAdd)
|
|
244
|
-
const newText = `import ${defaultName}, { ${newSpecifiersText} } from ${quote}${replacement}${quote}${semicolon}`
|
|
245
|
-
fixes.push(fixer.replaceText(existingImport, newText))
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return fixes
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* @param {import('estree').ImportDeclaration['source']} sourceNode
|
|
254
|
-
* @param {string} replacement
|
|
255
|
-
* @param {'"' | "'"} quote
|
|
256
|
-
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
257
|
-
*/
|
|
258
|
-
const createStringReplacer = (sourceNode, replacement, quote) => {
|
|
259
|
-
return (fixer) => fixer.replaceText(sourceNode, `${quote}${replacement}${quote}`)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* @param {import('estree').ImportDeclaration['source']} sourceNode
|
|
264
|
-
* @param {Replacement} replacementPatterns
|
|
265
|
-
* @param {'"' | "'"} quote
|
|
266
|
-
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
267
|
-
*/
|
|
268
|
-
const createPatternReplacer = (sourceNode, replacementPatterns, quote) => {
|
|
269
|
-
return (fixer) => {
|
|
270
|
-
let result = sourceNode.value
|
|
271
|
-
|
|
272
|
-
if (typeof replacementPatterns === 'string') {
|
|
273
|
-
return createStringReplacer(sourceNode, replacementPatterns, quote)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
for (const [pattern, replacement] of Object.entries(replacementPatterns)) {
|
|
277
|
-
const regex = new RegExp(pattern, 'g')
|
|
278
|
-
result = result.replace(regex, replacement)
|
|
279
|
-
}
|
|
280
|
-
return fixer.replaceText(sourceNode, `${quote}${result}${quote}`)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* @param {import('estree').ImportDeclaration} node
|
|
286
|
-
* @param {Replacement} replacement
|
|
287
|
-
* @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
|
|
288
|
-
*/
|
|
289
|
-
const createModuleReplacer = (node, replacement) => {
|
|
290
|
-
if (!replacement) return null
|
|
291
|
-
|
|
292
|
-
const quote = getQuoteStyle(node.source.raw)
|
|
293
|
-
|
|
294
|
-
if (typeof replacement === 'string') {
|
|
295
|
-
return createStringReplacer(node.source, replacement, quote)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return createPatternReplacer(node.source, replacement, quote)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* @param {import('eslint').Rule.RuleContext} context
|
|
303
|
-
* @param {import('estree').ImportDeclaration} node
|
|
304
|
-
* @param {Array<{importName: string, replacement: string}>} importRestrictions
|
|
305
|
-
* @returns {(fixer: import('eslint').Rule.RuleFixer) => any}
|
|
306
|
-
*/
|
|
307
|
-
const createMultiNamedImportReplacer = (context, node, importRestrictions) => {
|
|
308
|
-
return (fixer) => {
|
|
309
|
-
if (!importRestrictions.length) return null
|
|
310
|
-
|
|
311
|
-
const quote = getQuoteStyle(node.source.raw)
|
|
312
|
-
const semicolon = node.source.raw.endsWith(';') || node.source.value.endsWith(';') ? ';' : ''
|
|
313
|
-
const fixes = []
|
|
314
|
-
|
|
315
|
-
const allRestrictedNames = importRestrictions.map((r) => r.importName)
|
|
316
|
-
|
|
317
|
-
// Group imports by replacement
|
|
318
|
-
const groupedByReplacement = importRestrictions.reduce((acc, restriction) => {
|
|
319
|
-
if (!restriction.replacement) return acc
|
|
320
|
-
|
|
321
|
-
if (!acc[restriction.replacement]) {
|
|
322
|
-
acc[restriction.replacement] = []
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
acc[restriction.replacement].push(restriction.importName)
|
|
326
|
-
|
|
327
|
-
return acc
|
|
328
|
-
}, {})
|
|
329
|
-
|
|
330
|
-
// Find non-restricted specifiers from the original import
|
|
331
|
-
const remainingSpecifiers = node.specifiers.filter(
|
|
332
|
-
(specifier) => specifier.type !== 'ImportSpecifier' || !allRestrictedNames.includes(specifier.imported.name),
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
// Update or remove the original import
|
|
336
|
-
if (remainingSpecifiers.length === 0) {
|
|
337
|
-
fixes.push(fixer.remove(node))
|
|
338
|
-
} else {
|
|
339
|
-
const newImportText = createImportText({
|
|
340
|
-
specifiers: remainingSpecifiers,
|
|
341
|
-
source: node.source.value,
|
|
342
|
-
quote,
|
|
343
|
-
semicolon,
|
|
344
|
-
})
|
|
345
|
-
fixes.push(fixer.replaceText(node, newImportText))
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Create new imports for each replacement module
|
|
349
|
-
const { sourceCode } = context
|
|
350
|
-
const allImports = sourceCode.ast.body.filter(
|
|
351
|
-
(node) => node.type === 'ImportDeclaration' && node.source.type === 'Literal',
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
// Process each replacement module
|
|
355
|
-
Object.entries(groupedByReplacement).forEach(([replacement, restrictedNames]) => {
|
|
356
|
-
// Find specifiers to move
|
|
357
|
-
const specifiersToMove = restrictedNames
|
|
358
|
-
.map((name) => {
|
|
359
|
-
const specifier = node.specifiers.find((s) => s.type === 'ImportSpecifier' && s.imported.name === name)
|
|
360
|
-
return specifier
|
|
361
|
-
? {
|
|
362
|
-
imported: specifier.imported.name,
|
|
363
|
-
local: specifier.local.name,
|
|
364
|
-
}
|
|
365
|
-
: null
|
|
366
|
-
})
|
|
367
|
-
.filter(Boolean)
|
|
368
|
-
|
|
369
|
-
if (specifiersToMove.length === 0) return
|
|
370
|
-
|
|
371
|
-
// Find existing import for the same replacement module
|
|
372
|
-
const existingReplacementImport = allImports.find((importNode) => importNode.source.value === replacement)
|
|
373
|
-
|
|
374
|
-
if (existingReplacementImport) {
|
|
375
|
-
// Add to existing import
|
|
376
|
-
fixes.push(
|
|
377
|
-
...updateExistingImport(
|
|
378
|
-
fixer,
|
|
379
|
-
sourceCode,
|
|
380
|
-
existingReplacementImport,
|
|
381
|
-
specifiersToMove,
|
|
382
|
-
quote,
|
|
383
|
-
semicolon,
|
|
384
|
-
replacement,
|
|
385
|
-
),
|
|
386
|
-
)
|
|
387
|
-
} else {
|
|
388
|
-
// Create new import
|
|
389
|
-
const newSpecifiersText = formatSpecifiers(specifiersToMove)
|
|
390
|
-
const newImport = `import { ${newSpecifiersText} } from ${quote}${replacement}${quote}${semicolon}`
|
|
391
|
-
fixes.push(fixer.insertTextBefore(node, newImport + '\n'))
|
|
392
|
-
}
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
return fixes
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/** @type {import('eslint').Rule.RuleModule} */
|
|
400
|
-
module.exports = {
|
|
401
|
-
meta: {
|
|
402
|
-
type: 'problem',
|
|
403
|
-
docs: {
|
|
404
|
-
description: 'Prevent the Import of a Specific Package',
|
|
405
|
-
recommended: false,
|
|
406
|
-
url: 'https://github.com/custardcream98/eslint-plugin-restrict-replace-import/blob/main/docs/rules/restrict-import.md',
|
|
407
|
-
},
|
|
408
|
-
fixable: 'code',
|
|
409
|
-
|
|
410
|
-
messages: {
|
|
411
|
-
ImportRestriction: '`{{ name }}` is restricted from being used.',
|
|
412
|
-
ImportRestrictionWithReplacement:
|
|
413
|
-
'`{{ name }}` is restricted from being used. Replace it with `{{ replacement }}`.',
|
|
414
|
-
ImportedNameRestriction: "Import of '{{importedName}}' from '{{name}}' is restricted",
|
|
415
|
-
ImportedNameRestrictionWithReplacement:
|
|
416
|
-
"Import of '{{importedName}}' from '{{name}}' is restricted. Replace it with '{{replacement}}'.",
|
|
417
|
-
},
|
|
418
|
-
|
|
419
|
-
schema: {
|
|
420
|
-
type: 'array',
|
|
421
|
-
maxLength: 1,
|
|
422
|
-
minLength: 1,
|
|
423
|
-
items: {
|
|
424
|
-
type: 'array',
|
|
425
|
-
items: {
|
|
426
|
-
oneOf: [
|
|
427
|
-
{
|
|
428
|
-
type: 'string',
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
type: 'object',
|
|
432
|
-
properties: {
|
|
433
|
-
target: {
|
|
434
|
-
type: 'string',
|
|
435
|
-
description: 'The target of the import to be restricted',
|
|
436
|
-
},
|
|
437
|
-
namedImports: {
|
|
438
|
-
type: 'array',
|
|
439
|
-
items: { type: 'string' },
|
|
440
|
-
description:
|
|
441
|
-
'The named imports to be restricted. If not provided, all named imports will be restricted.',
|
|
442
|
-
},
|
|
443
|
-
replacement: {
|
|
444
|
-
oneOf: [
|
|
445
|
-
{ type: 'string' },
|
|
446
|
-
{
|
|
447
|
-
type: 'object',
|
|
448
|
-
patternProperties: {
|
|
449
|
-
'.*': { type: 'string' },
|
|
450
|
-
},
|
|
451
|
-
},
|
|
452
|
-
],
|
|
453
|
-
description:
|
|
454
|
-
'The replacement for the import. If a string is provided, it will be used as the replacement for all imports. If an object is provided, the keys will be used as the pattern and the values will be used as the replacement.',
|
|
455
|
-
},
|
|
456
|
-
},
|
|
457
|
-
required: ['target'],
|
|
458
|
-
additionalProperties: false,
|
|
459
|
-
},
|
|
460
|
-
],
|
|
461
|
-
},
|
|
462
|
-
},
|
|
463
|
-
},
|
|
464
|
-
},
|
|
465
|
-
|
|
466
|
-
create(context) {
|
|
467
|
-
const restrictedPackages = createRestrictedPackagesMap(context.options[0])
|
|
468
|
-
|
|
469
|
-
return {
|
|
470
|
-
ImportDeclaration(node) {
|
|
471
|
-
if (node.source.type !== 'Literal') return
|
|
472
|
-
|
|
473
|
-
const importSource = node.source.value
|
|
474
|
-
const namedImports = node.specifiers
|
|
475
|
-
.filter((specifier) => specifier.type === 'ImportSpecifier')
|
|
476
|
-
.map((specifier) => specifier.imported.name)
|
|
477
|
-
const checkerResult = checkIsRestrictedImport(importSource, namedImports, restrictedPackages)
|
|
478
|
-
|
|
479
|
-
if (!checkerResult) return
|
|
480
|
-
|
|
481
|
-
const restrictedPackageOptions = restrictedPackages.get(checkerResult.pattern)
|
|
482
|
-
const patternName = getPatternDisplayName(checkerResult.pattern.source)
|
|
483
|
-
|
|
484
|
-
if (checkerResult.type === 'module') {
|
|
485
|
-
context.report({
|
|
486
|
-
node,
|
|
487
|
-
messageId:
|
|
488
|
-
typeof restrictedPackageOptions.replacement === 'string'
|
|
489
|
-
? 'ImportRestrictionWithReplacement'
|
|
490
|
-
: 'ImportRestriction',
|
|
491
|
-
data: {
|
|
492
|
-
name: patternName,
|
|
493
|
-
replacement: restrictedPackageOptions.replacement,
|
|
494
|
-
},
|
|
495
|
-
fix: createModuleReplacer(node, restrictedPackageOptions.replacement),
|
|
496
|
-
})
|
|
497
|
-
return
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Find potential rules and replacement mappings for multiple restricted named imports
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* @type {{importName: string, replacement: string | null, pattern: RegExp}[]}
|
|
504
|
-
*/
|
|
505
|
-
const importRestrictions = []
|
|
506
|
-
|
|
507
|
-
// Check each named import for restrictions
|
|
508
|
-
namedImports.forEach((importName) => {
|
|
509
|
-
for (const [pattern, options] of restrictedPackages.entries()) {
|
|
510
|
-
if (
|
|
511
|
-
pattern.test(importSource) &&
|
|
512
|
-
options.namedImports &&
|
|
513
|
-
options.namedImports.includes(importName) &&
|
|
514
|
-
// TODO: handle options.replacement as an object
|
|
515
|
-
(typeof options.replacement === 'string' || options.replacement === null)
|
|
516
|
-
) {
|
|
517
|
-
importRestrictions.push({
|
|
518
|
-
importName,
|
|
519
|
-
replacement: options.replacement,
|
|
520
|
-
pattern,
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
break // Only use the first matching restriction for an import
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
})
|
|
527
|
-
|
|
528
|
-
if (importRestrictions.length === 0) {
|
|
529
|
-
return
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Report separate errors for each restricted import
|
|
533
|
-
importRestrictions.forEach((restriction) => {
|
|
534
|
-
context.report({
|
|
535
|
-
node,
|
|
536
|
-
messageId: restriction.replacement ? 'ImportedNameRestrictionWithReplacement' : 'ImportedNameRestriction',
|
|
537
|
-
data: {
|
|
538
|
-
importedName: restriction.importName,
|
|
539
|
-
name: importSource,
|
|
540
|
-
replacement: restriction.replacement,
|
|
541
|
-
},
|
|
542
|
-
fix: restriction.replacement ? createMultiNamedImportReplacer(context, node, importRestrictions) : null,
|
|
543
|
-
})
|
|
544
|
-
})
|
|
545
|
-
},
|
|
546
|
-
}
|
|
547
|
-
},
|
|
548
|
-
}
|