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.
Background
In a project we sometimes rewrite external modules and want to use the rewritten version uniformly. For example, we replaced the
react-router Linkcomponent 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
