eslint-plugin-restrict-replace-import 1.4.0 → 1.5.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
@@ -126,7 +126,7 @@ You can also restrict specific named imports from a package while allowing other
126
126
  {
127
127
  "target": "restricted-module",
128
128
  "namedImports": ["restrictedImport", "alsoRestricted"],
129
- "replacement": "replacement-module"
129
+ "replacement": "replacement-module" // Object is not supported yet
130
130
  }
131
131
  ]
132
132
  ]
@@ -129,7 +129,7 @@ import { useState } from 'successfully-replaced'
129
129
  [{
130
130
  "target": "restricted-module",
131
131
  "namedImports": ["restrictedImport", "alsoRestricted"],
132
- "replacement": "replacement-module"
132
+ "replacement": "replacement-module" // Object is not supported yet
133
133
  }]
134
134
  ]
135
135
  }
@@ -4,9 +4,14 @@
4
4
  */
5
5
  'use strict'
6
6
 
7
+ /**
8
+ * @typedef {string | {[key: string]: string;}} Replacement
9
+ * @typedef {{replacement: Replacement | null; namedImports: string[] | null;}} ImportRestrictionOptions
10
+ */
11
+
7
12
  const createRestrictedPackagesMap = (options) => {
8
13
  /**
9
- * @type {Map<RegExp, { replacement: string | { [key: string]: string } | null, namedImports: string[] | null }>}
14
+ * @type {Map<RegExp, ImportRestrictionOptions>}
10
15
  */
11
16
  const map = new Map()
12
17
  options.forEach((config) => {
@@ -28,7 +33,7 @@ const createRestrictedPackagesMap = (options) => {
28
33
  /**
29
34
  * @param {string} importSource
30
35
  * @param {string[]} namedImports
31
- * @param {Map<RegExp, { replacement: string | { [key: string]: string } | null, namedImports: string[] | null }>} restrictedPackages
36
+ * @param {Map<RegExp, ImportRestrictionOptions>} restrictedPackages
32
37
  */
33
38
  const checkIsRestrictedImport = (importSource, namedImports, restrictedPackages) => {
34
39
  for (const [pattern, restrictedPackageOptions] of restrictedPackages) {
@@ -119,7 +124,9 @@ const createImportText = ({ specifiers, source, quote, semicolon = '' }) => {
119
124
  * @param {string[]} restrictedNames
120
125
  * @param {string} replacement
121
126
  * @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
127
+ * @deprecated Function no longer used as each restriction is now handled individually
122
128
  */
129
+ // eslint-disable-next-line no-unused-vars
123
130
  const createNamedImportReplacer = (context, node, restrictedNames, replacement) => {
124
131
  return (fixer) => {
125
132
  if (!replacement) return null
@@ -254,7 +261,7 @@ const createStringReplacer = (sourceNode, replacement, quote) => {
254
261
 
255
262
  /**
256
263
  * @param {import('estree').ImportDeclaration['source']} sourceNode
257
- * @param {string | { [key: string]: string }} replacementPatterns
264
+ * @param {Replacement} replacementPatterns
258
265
  * @param {'"' | "'"} quote
259
266
  * @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
260
267
  */
@@ -276,7 +283,7 @@ const createPatternReplacer = (sourceNode, replacementPatterns, quote) => {
276
283
 
277
284
  /**
278
285
  * @param {import('estree').ImportDeclaration} node
279
- * @param {string | { [key: string]: string }} replacement
286
+ * @param {Replacement} replacement
280
287
  * @returns {(fixer: import('eslint').Rule.RuleFixer) => void}
281
288
  */
282
289
  const createModuleReplacer = (node, replacement) => {
@@ -291,6 +298,104 @@ const createModuleReplacer = (node, replacement) => {
291
298
  return createPatternReplacer(node.source, replacement, quote)
292
299
  }
293
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
+
294
399
  /** @type {import('eslint').Rule.RuleModule} */
295
400
  module.exports = {
296
401
  meta: {
@@ -392,26 +497,49 @@ module.exports = {
392
497
  return
393
498
  }
394
499
 
395
- const restrictedImports = restrictedPackageOptions.namedImports.filter((name) => namedImports.includes(name))
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
+ })
396
527
 
397
- restrictedImports.forEach((restrictedImportedName) => {
528
+ if (importRestrictions.length === 0) {
529
+ return
530
+ }
531
+
532
+ // Report separate errors for each restricted import
533
+ importRestrictions.forEach((restriction) => {
398
534
  context.report({
399
535
  node,
400
- messageId:
401
- typeof restrictedPackageOptions.replacement === 'string'
402
- ? 'ImportedNameRestrictionWithReplacement'
403
- : 'ImportedNameRestriction',
536
+ messageId: restriction.replacement ? 'ImportedNameRestrictionWithReplacement' : 'ImportedNameRestriction',
404
537
  data: {
405
- importedName: restrictedImportedName,
538
+ importedName: restriction.importName,
406
539
  name: importSource,
407
- replacement: restrictedPackageOptions.replacement,
540
+ replacement: restriction.replacement,
408
541
  },
409
- fix: createNamedImportReplacer(
410
- context,
411
- node,
412
- restrictedPackageOptions.namedImports,
413
- restrictedPackageOptions.replacement,
414
- ),
542
+ fix: restriction.replacement ? createMultiNamedImportReplacer(context, node, importRestrictions) : null,
415
543
  })
416
544
  })
417
545
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-restrict-replace-import",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "ESLint Plugin for Restricting and Replacing Import",
5
5
  "keywords": [
6
6
  "eslint",
@@ -26,7 +26,7 @@
26
26
  "lint:js": "eslint .",
27
27
  "test": "mocha tests --recursive",
28
28
  "update:eslint-docs": "eslint-doc-generator",
29
- "publish": "npm run lint && npm run test && npm publish"
29
+ "release": "npm run lint && npm run test && npm publish"
30
30
  },
31
31
  "dependencies": {
32
32
  "requireindex": "^1.2.0"