Repository: un-ts/eslint-plugin-import-x
Pull request #100 (2024-07-01 14:39:13)

feat: disallow import chunkname w/ webpack eager mode
Backport https://github.com/import-js/eslint-plugin-import/pull/3004

changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: 246ad3326281536423816714c29b82d1db7febd8

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                   | Type  |
| ---------------------- | ----- |
| eslint-plugin-import-x | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/SukkaW/eslint-plugin-import-x/new/backport-webpack-eager?filename=.changeset/tender-lamps-talk.md&value=---%0A%22eslint-plugin-import-x%22%3A%20patch%0A---%0A%0Afeat%3A%20disallow%20import%20chunkname%20w%2F%20webpack%20eager%20mode%0A)


codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/un-ts/eslint-plugin-import-x/pr/100/builds/518974) or the icon next to each commit SHA.

Patch:
diff --git a/.changeset/bright-taxis-smell.md b/.changeset/bright-taxis-smell.md
new file mode 100644
index 00000000..be8ed24e
--- /dev/null
+++ b/.changeset/bright-taxis-smell.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": minor
+---
+
+Allow incorrect file path in extraneous deps check
diff --git a/.changeset/grumpy-games-shake.md b/.changeset/grumpy-games-shake.md
new file mode 100644
index 00000000..e87ea98d
--- /dev/null
+++ b/.changeset/grumpy-games-shake.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+feat: webpack comment regex support `webpackFetchPriority`
diff --git a/.changeset/sixty-avocados-stare.md b/.changeset/sixty-avocados-stare.md
new file mode 100644
index 00000000..e722d569
--- /dev/null
+++ b/.changeset/sixty-avocados-stare.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-import-x": patch
+---
+
+Allow empty chunk name when webpackMode: 'eager' is set; add suggestions to remove name in eager mode
diff --git a/README.md b/README.md
index 0112056e..d3967509 100644
--- a/README.md
+++ b/README.md
@@ -79,7 +79,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
 | Name                                                                             | Description                                                                 | 💼  | ⚠️    | 🚫  | 🔧  | 💡  | ❌  |
 | :------------------------------------------------------------------------------- | :-------------------------------------------------------------------------- | :-- | :---- | :-- | :-- | :-- | :-- |
 | [consistent-type-specifier-style](docs/rules/consistent-type-specifier-style.md) | Enforce or ban the use of inline type-only markers for named imports.       |     |       |     | 🔧  |     |     |
-| [dynamic-import-chunkname](docs/rules/dynamic-import-chunkname.md)               | Enforce a leading comment with the webpackChunkName for dynamic imports.    |     |       |     |     |     |     |
+| [dynamic-import-chunkname](docs/rules/dynamic-import-chunkname.md)               | Enforce a leading comment with the webpackChunkName for dynamic imports.    |     |       |     |     | 💡  |     |
 | [exports-last](docs/rules/exports-last.md)                                       | Ensure all exports appear after other statements.                           |     |       |     |     |     |     |
 | [extensions](docs/rules/extensions.md)                                           | Ensure consistent use of file extension within the import path.             |     |       |     |     |     |     |
 | [first](docs/rules/first.md)                                                     | Ensure all imports appear before other statements.                          |     |       |     | 🔧  |     |     |
diff --git a/docs/rules/dynamic-import-chunkname.md b/docs/rules/dynamic-import-chunkname.md
index 401938d2..71b98be2 100644
--- a/docs/rules/dynamic-import-chunkname.md
+++ b/docs/rules/dynamic-import-chunkname.md
@@ -1,5 +1,7 @@
 # import-x/dynamic-import-chunkname
 
+💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
+
 <!-- end auto-generated rule header -->
 
 This rule reports any dynamic imports without a webpackChunkName specified in a leading block comment in the proper format.
@@ -56,6 +58,13 @@ import(
   // webpackChunkName: "someModule"
   'someModule'
 )
+
+// chunk names are disallowed when eager mode is set
+import(
+  /* webpackMode: "eager" */
+  /* webpackChunkName: "someModule" */
+  'someModule'
+)
 ```
 
 ### valid
diff --git a/src/rules/dynamic-import-chunkname.ts b/src/rules/dynamic-import-chunkname.ts
index d394ee78..bbdedb9b 100644
--- a/src/rules/dynamic-import-chunkname.ts
+++ b/src/rules/dynamic-import-chunkname.ts
@@ -1,6 +1,7 @@
 import vm from 'node:vm'
 
 import type { TSESTree } from '@typescript-eslint/utils'
+import type { RuleFixer } from '@typescript-eslint/utils/dist/ts-eslint'
 
 import { createRule } from '../utils'
 
@@ -16,6 +17,9 @@ type MessageId =
   | 'paddedSpaces'
   | 'webpackComment'
   | 'chunknameFormat'
+  | 'webpackEagerModeNoChunkName'
+  | 'webpackRemoveEagerMode'
+  | 'webpackRemoveChunkName'
 
 export = createRule<[Options?], MessageId>({
   name: 'dynamic-import-chunkname',
@@ -26,6 +30,7 @@ export = createRule<[Options?], MessageId>({
       description:
         'Enforce a leading comment with the webpackChunkName for dynamic imports.',
     },
+    hasSuggestions: true,
     schema: [
       {
         type: 'object',
@@ -56,7 +61,11 @@ export = createRule<[Options?], MessageId>({
       webpackComment:
         'dynamic imports require a "webpack" comment with valid syntax',
       chunknameFormat:
-        'dynamic imports require a leading comment in the form /*{{format}}*/',
+        'dynamic imports require a leading comment in the form /* {{format}} */',
+      webpackEagerModeNoChunkName:
+        'dynamic imports using eager mode do not need a webpackChunkName',
+      webpackRemoveEagerMode: 'Remove webpackMode',
+      webpackRemoveChunkName: 'Remove webpackChunkName',
     },
   },
   defaultOptions: [],
@@ -69,9 +78,12 @@ export = createRule<[Options?], MessageId>({
 
     const paddedCommentRegex = /^ (\S[\S\s]+\S) $/
     const commentStyleRegex =
-      /^( ((webpackChunkName: .+)|((webpackPrefetch|webpackPreload): (true|false|-?\d+))|(webpackIgnore: (true|false))|((webpackInclude|webpackExclude): \/.*\/)|(webpackMode: ["'](lazy|lazy-once|eager|weak)["'])|(webpackExports: (["']\w+["']|\[(["']\w+["'], *)+(["']\w+["']*)]))),?)+ $/
-    const chunkSubstrFormat = ` webpackChunkName: ["']${webpackChunknameFormat}["'],? `
+      /^( (((webpackChunkName|webpackFetchPriority): .+)|((webpackPrefetch|webpackPreload): (true|false|-?\d+))|(webpackIgnore: (true|false))|((webpackInclude|webpackExclude): \/.+\/)|(webpackMode: ["'](lazy|lazy-once|eager|weak)["'])|(webpackExports: (["']\w+["']|\[(["']\w+["'], *)+(["']\w+["']*)]))),?)+ $/
+
+    const chunkSubstrFormat = `webpackChunkName: ["']${webpackChunknameFormat}["'],?`
     const chunkSubstrRegex = new RegExp(chunkSubstrFormat)
+    const eagerModeFormat = `webpackMode: ["']eager["'],?`
+    const eagerModeRegex = new RegExp(eagerModeFormat)
 
     function run(node: TSESTree.Node, arg: TSESTree.Node) {
       const { sourceCode } = context
@@ -86,6 +98,7 @@ export = createRule<[Options?], MessageId>({
       }
 
       let isChunknamePresent = false
+      let isEagerModePresent = false
 
       for (const comment of leadingComments) {
         if (comment.type !== 'Block') {
@@ -123,12 +136,83 @@ export = createRule<[Options?], MessageId>({
           return
         }
 
+        if (eagerModeRegex.test(comment.value)) {
+          isEagerModePresent = true
+        }
+
         if (chunkSubstrRegex.test(comment.value)) {
           isChunknamePresent = true
         }
       }
 
-      if (!isChunknamePresent && !allowEmpty) {
+      const removeCommentsAndLeadingSpaces = (
+        fixer: RuleFixer,
+        comment: TSESTree.Comment,
+      ) => {
+        const leftToken = sourceCode.getTokenBefore(comment)
+        const leftComments = sourceCode.getCommentsBefore(comment)
+        if (leftToken) {
+          if (leftComments.length > 0) {
+            return fixer.removeRange([
+              Math.max(
+                leftToken.range[1],
+                leftComments[leftComments.length - 1].range[1],
+              ),
+              comment.range[1],
+            ])
+          }
+          return fixer.removeRange([leftToken.range[1], comment.range[1]])
+        }
+        return fixer.remove(comment)
+      }
+
+      if (isChunknamePresent && isEagerModePresent) {
+        context.report({
+          node,
+          messageId: 'webpackEagerModeNoChunkName',
+          suggest: [
+            {
+              messageId: 'webpackRemoveChunkName',
+              fix(fixer) {
+                for (const comment of leadingComments) {
+                  if (chunkSubstrRegex.test(comment.value)) {
+                    const replacement = comment.value
+                      .replace(chunkSubstrRegex, '')
+                      .trim()
+                      .replace(/,$/, '')
+
+                    return replacement === ''
+                      ? removeCommentsAndLeadingSpaces(fixer, comment)
+                      : fixer.replaceText(comment, `/* ${replacement} */`)
+                  }
+                }
+
+                return null
+              },
+            },
+            {
+              messageId: 'webpackRemoveEagerMode',
+              fix(fixer) {
+                for (const comment of leadingComments) {
+                  if (eagerModeRegex.test(comment.value)) {
+                    const replacement = comment.value
+                      .replace(eagerModeRegex, '')
+                      .trim()
+                      .replace(/,$/, '')
+                    return replacement === ''
+                      ? removeCommentsAndLeadingSpaces(fixer, comment)
+                      : fixer.replaceText(comment, `/* ${replacement} */`)
+                  }
+                }
+
+                return null
+              },
+            },
+          ],
+        })
+      }
+
+      if (!isChunknamePresent && !allowEmpty && !isEagerModePresent) {
         context.report({
           node,
           messageId: 'chunknameFormat',
diff --git a/src/rules/no-extraneous-dependencies.ts b/src/rules/no-extraneous-dependencies.ts
index 9d9c167c..f91324ba 100644
--- a/src/rules/no-extraneous-dependencies.ts
+++ b/src/rules/no-extraneous-dependencies.ts
@@ -55,8 +55,11 @@ function extractDepFields(pkg: PackageJson) {
 
 function getPackageDepFields(packageJsonPath: string, throwAtRead: boolean) {
   if (!depFieldCache.has(packageJsonPath)) {
-    const depFields = extractDepFields(readJSON(packageJsonPath, throwAtRead)!)
-    depFieldCache.set(packageJsonPath, depFields)
+    const packageJson = readJSON<PackageJson>(packageJsonPath, throwAtRead)
+    if (packageJson) {
+      const depFields = extractDepFields(packageJson)
+      depFieldCache.set(packageJsonPath, depFields)
+    }
   }
   return depFieldCache.get(packageJsonPath)
 }
@@ -83,10 +86,15 @@ function getDependencies(context: RuleContext, packageDir?: string | string[]) {
       // use rule config to find package.json
       for (const dir of paths) {
         const packageJsonPath = path.resolve(dir, 'package.json')
-        const packageContent_ = getPackageDepFields(packageJsonPath, true)!
-        for (const depsKey of Object.keys(packageContent)) {
-          const key = depsKey as keyof PackageDeps
-          Object.assign(packageContent[key], packageContent_[key])
+        const packageContent_ = getPackageDepFields(
+          packageJsonPath,
+          paths.length === 1,
+        )
+        if (packageContent_) {
+          for (const depsKey of Object.keys(packageContent)) {
+            const key = depsKey as keyof PackageDeps
+            Object.assign(packageContent[key], packageContent_[key])
+          }
         }
       }
     } else {


Test patch:
diff --git a/test/rules/dynamic-import-chunkname.spec.ts b/test/rules/dynamic-import-chunkname.spec.ts
index 61b7bafb..0a5d14e3 100644
--- a/test/rules/dynamic-import-chunkname.spec.ts
+++ b/test/rules/dynamic-import-chunkname.spec.ts
@@ -39,7 +39,7 @@ const parser = parsers.BABEL
 const pickyChunkNameFormatError = {
   messageId: 'chunknameFormat',
   data: {
-    format: ` webpackChunkName: ["']${pickyCommentFormat}["'],? `,
+    format: `webpackChunkName: ["']${pickyCommentFormat}["'],?`,
   },
 } as const
 
@@ -366,15 +366,6 @@ ruleTester.run('dynamic-import-chunkname', rule, {
       options,
       parser,
     },
-    {
-      code: `import(
-        /* webpackChunkName: "someModule" */
-        /* webpackMode: "eager" */
-        'someModule'
-      )`,
-      options,
-      parser,
-    },
     {
       code: `import(
         /* webpackChunkName: "someModule" */
@@ -426,7 +417,7 @@ ruleTester.run('dynamic-import-chunkname', rule, {
         /* webpackPrefetch: true */
         /* webpackPreload: true */
         /* webpackIgnore: false */
-        /* webpackMode: "eager" */
+        /* webpackMode: "lazy" */
         /* webpackExports: ["default", "named"] */
         'someModule'
       )`,
@@ -1220,15 +1211,6 @@ describe('TypeScript', () => {
         options,
         parser: typescriptParser,
       },
-      {
-        code: `import(
-            /* webpackChunkName: "someModule" */
-            /* webpackMode: "eager" */
-            'someModule'
-          )`,
-        options,
-        parser: typescriptParser,
-      },
       {
         code: `import(
             /* webpackChunkName: "someModule" */
@@ -1280,7 +1262,7 @@ describe('TypeScript', () => {
             /* webpackPrefetch: true */
             /* webpackPreload: true */
             /* webpackIgnore: false */
-            /* webpackMode: "eager" */
+            /* webpackMode: "lazy" */
             /* webpackExports: ["default", "named"] */
             'someModule'
           )`,
@@ -1707,6 +1689,104 @@ describe('TypeScript', () => {
           },
         ],
       },
+      {
+        code: `import(
+          /* webpackChunkName: "someModule" */
+          /* webpackMode: "eager" */
+          'someModule'
+        )`,
+        options,
+        parser,
+        output: null,
+        errors: [
+          {
+            messageId: 'webpackEagerModeNoChunkName',
+            type: nodeType,
+            suggestions: [
+              {
+                messageId: 'webpackRemoveChunkName',
+                output: `import(
+          /* webpackMode: "eager" */
+          'someModule'
+        )`,
+              },
+              {
+                messageId: 'webpackRemoveEagerMode',
+                output: `import(
+          /* webpackChunkName: "someModule" */
+          'someModule'
+        )`,
+              },
+            ],
+          },
+        ],
+      },
+      {
+        code: `import(
+          /* webpackChunkName: "someModule", webpackPrefetch: true */
+          /* webpackMode: "eager" */
+          'someModule'
+        )`,
+        options,
+        parser,
+        output: null,
+        errors: [
+          {
+            messageId: 'webpackEagerModeNoChunkName',
+            type: nodeType,
+            suggestions: [
+              {
+                messageId: 'webpackRemoveChunkName',
+                output: `import(
+          /* webpackPrefetch: true */
+          /* webpackMode: "eager" */
+          'someModule'
+        )`,
+              },
+              {
+                messageId: 'webpackRemoveEagerMode',
+                output: `import(
+          /* webpackChunkName: "someModule", webpackPrefetch: true */
+          'someModule'
+        )`,
+              },
+            ],
+          },
+        ],
+      },
+      {
+        code: `import(
+          /* webpackChunkName: "someModule" */
+          /* webpackMode: "eager", webpackPrefetch: true */
+          'someModule'
+        )`,
+        options,
+        parser,
+        output: null,
+        errors: [
+          {
+            messageId: 'webpackEagerModeNoChunkName',
+            type: nodeType,
+            suggestions: [
+              {
+                messageId: 'webpackRemoveChunkName',
+                output: `import(
+          /* webpackMode: "eager", webpackPrefetch: true */
+          'someModule'
+        )`,
+              },
+              {
+                messageId: 'webpackRemoveEagerMode',
+                output: `import(
+          /* webpackChunkName: "someModule" */
+          /* webpackPrefetch: true */
+          'someModule'
+        )`,
+              },
+            ],
+          },
+        ],
+      },
     ],
   })
 })
diff --git a/test/rules/no-extraneous-dependencies.spec.ts b/test/rules/no-extraneous-dependencies.spec.ts
index 98299b48..610005b9 100644
--- a/test/rules/no-extraneous-dependencies.spec.ts
+++ b/test/rules/no-extraneous-dependencies.spec.ts
@@ -48,6 +48,7 @@ const packageDirBundledDepsAsObject = testFilePath(
 const packageDirBundledDepsRaceCondition = testFilePath(
   'bundled-dependencies/race-condition',
 )
+const emptyPackageDir = testFilePath('empty-folder')
 
 ruleTester.run('no-extraneous-dependencies', rule, {
   valid: [
@@ -130,6 +131,14 @@ ruleTester.run('no-extraneous-dependencies', rule, {
       code: 'import leftpad from "left-pad";',
       options: [{ packageDir: packageDirMonoRepoRoot }],
     }),
+    test({
+      code: 'import leftpad from "left-pad";',
+      options: [{ packageDir: [emptyPackageDir, packageDirMonoRepoRoot] }],
+    }),
+    test({
+      code: 'import leftpad from "left-pad";',
+      options: [{ packageDir: [packageDirMonoRepoRoot, emptyPackageDir] }],
+    }),
     test({
       code: 'import react from "react";',
       options: [