How to Build a Custom ESLint Rule to Rewrite Import Paths

This tutorial explains why and how to create a custom ESLint rule that automatically replaces specified import paths, using AST traversal, rule configuration, and fixers, with a complete example including project scaffolding, implementation details, and unit testing.

Huolala Tech
Huolala Tech
Huolala Tech
How to Build a Custom ESLint Rule to Rewrite Import Paths

Background

In a project we sometimes rewrite external modules and want to use the rewritten version uniformly. For example, we replaced the

react-router
Link

component with a custom implementation.

// before
import { Link, useModel } from 'umi';

// after
import { useModel } from 'umi';
import { Link } from '@/utils/router';

If ESLint could check and automatically replace a module’s import path, we could enforce a unified path. Since ESLint does not provide such a rule out‑of‑the‑box, we need to develop a custom rule.

ESLint Rule Definition

We define a rule configuration that specifies the target module, the replacement path, and the names to be rewritten.

{
  "rules": {
    "import-path-plus/no-internal-modules": [
      "warn",
      {
        "target": "umi",
        "replace": "@/utils/router",
        "names": ["Link", "useHistory"]
      }
    ]
  }
}

The rule will replace the specified target imports for the listed names with the replace path.

Implementation

How ESLint Works

ESLint reads its configuration, parses code into an AST using a parser (default espree), traverses the tree, and invokes visitor functions for specific node types.

We focus on ImportDeclaration nodes, checking their source. If the source matches the configured target, we examine the node’s ImportSpecifier children and report an error when a specifier name appears in names.

Create Project

We use generator-eslint to scaffold a rule project, then edit the rule file under /lib/rules.

module.exports = {
  meta: {
    type: 'problem',
    fixable: 'code',
  },

  create(context) {
    return {
      ImportDeclaration: (node) => {},
    };
  },
};

The rule consists of a meta object describing the rule and a create function that returns visitor methods.

Specific Implementation

When an ImportDeclaration matches the target, we iterate over its specifiers. If a specifier name is listed in names, we report it and provide a fix that removes the original import and inserts a new one from the replace path.

const visitor = {
  ImportDeclaration: (node) => {
    context.options.forEach((option) => {
      const regexp = minimatch.makeRe(option.target);
      if (!regexp.test(node.source.value)) return;

      node.specifiers.forEach((spec) => {
        if (spec.type !== 'ImportSpecifier') return;
        if (option.names.includes(spec.imported.name)) {
          reportModule(spec, node, option);
        }
      });
    });
  },
};

The fix uses fixer.removeRange to delete the original specifier (handling trailing commas) and fixer.insertTextAfter to add the new import statement.

const sourceCode = context.getSourceCode();

const reportModule = (spec, node, option) => {
  const moduleName =
    spec.imported.name == spec.local.name
      ? spec.imported.name
      : `${spec.imported.name} as ${spec.local.name}`;

  const afterToken = sourceCode.getTokenAfter(spec);

  context.report({
    node: spec.imported,
    messageId: 'replace-module',
    fix(fixer) {
      return [
        fixer.removeRange([
          spec.range[0],
          afterToken.value === ',' ? afterToken.range[1] : spec.range[1],
        ]),
        fixer.insertTextAfter(
          node,
          `
import { ${moduleName} } from '${option.replace}';`
        ),
      ];
    },
  });
};

Unit Tests

We write tests using ESLint’s RuleTester, separating valid and invalid cases. Invalid cases trigger the rule and verify the auto‑fixed output.

const options = [
  {
    target: 'umi',
    replace: '@/utils/router',
    names: ['Link', 'useHistory'],
  },
];

ruleTester.run('no-internal-modules', rule, {
  valid: [
    { code: "import { useRequest } from 'umi'", options },
    { code: "import { useRequest, useModel } from 'umi'", options },
  ],

  invalid: [
    {
      code: "import { Link, useModel } from 'umi';",
      errors: [{ messageId: 'replace-module', data: { name: 'Link', replace: '@/utils/router' } }],
      options,
      output: "import {  useModel } from 'umi';
import { Link } from '@/utils/router';",
    },
  ],
});

After tests pass, we publish the package to npm for other developers to use.

Conclusion

This article introduces ESLint’s core concepts and demonstrates how to write a custom rule that rewrites import paths, helping developers enforce consistent module usage.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptASTESLintlintingcustom ruleimport path
Huolala Tech
Written by

Huolala Tech

Technology reshapes logistics

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.