@dword-design/eslint-plugin-import-alias
Version:
An ESLint plugin that enforces the use of import aliases. Also supports autofixing.
88 lines (87 loc) • 4.04 kB
JavaScript
import pathLib from 'node:path';
import { loadOptions } from '@babel/core';
import defu from '@dword-design/defu';
import { resolvePath as defaultResolvePath } from 'babel-plugin-module-resolver';
const isParentImport = path => /^(\.\/)?\.\.\//.test(path);
const findMatchingAlias = (sourcePath, currentFile, options) => {
const resolvePath = options.resolvePath || defaultResolvePath;
const absoluteSourcePath = pathLib.resolve(pathLib.dirname(currentFile), sourcePath);
for (const aliasName of Object.keys(options.alias)) {
const path = pathLib.resolve(pathLib.dirname(currentFile), resolvePath(`${aliasName}/`, currentFile, options));
if (absoluteSourcePath.startsWith(path)) {
return { name: aliasName, path };
}
}
};
export default {
create: context => {
const currentFile = context.getFilename();
const folder = pathLib.dirname(currentFile);
// can't check a non-file
if (currentFile === '<text>')
return {};
const optionsFromRule = context.options[0] ?? {};
const babelConfig = (loadOptions({
filename: currentFile,
...optionsFromRule.babelOptions,
}) || {});
const optionsFromPlugin = babelConfig?.plugins?.find(_ => _.key === 'module-resolver')?.options ??
{};
const options = defu(optionsFromRule, optionsFromPlugin, {
alias: [],
cwd: context.cwd,
});
if (options.alias.length === 0) {
throw new Error('No alias configured. You have to define aliases by either passing them to the babel-plugin-module-resolver plugin in your Babel config, or directly to the prefer-alias rule.');
}
const resolvePath = options.resolvePath || defaultResolvePath;
return {
ImportDeclaration: node => {
const sourcePath = node.source.value;
const hasAlias = Object.keys(options.alias).some(alias => sourcePath.startsWith(`${alias}/`));
// relative parent
if (isParentImport(sourcePath)) {
const matchingAlias = findMatchingAlias(sourcePath, currentFile, options);
if (!matchingAlias) {
return;
}
const absoluteImportPath = pathLib.resolve(folder, sourcePath);
const rewrittenImport = `${matchingAlias.name}/${pathLib
.relative(matchingAlias.path, absoluteImportPath)
.replaceAll('\\', '/')}`;
return context.report({
fix: fixer => fixer.replaceTextRange([node.source.range[0] + 1, node.source.range[1] - 1], rewrittenImport),
message: `Unexpected parent import '${sourcePath}'. Use '${rewrittenImport}' instead`,
node,
});
}
const importWithoutAlias = resolvePath(sourcePath, currentFile, options);
if (!isParentImport(importWithoutAlias) &&
hasAlias &&
!options.aliasForSubpaths) {
return context.report({
fix: fixer => fixer.replaceTextRange([node.source.range[0] + 1, node.source.range[1] - 1], importWithoutAlias),
message: `Unexpected subpath import via alias '${sourcePath}'. Use '${importWithoutAlias}' instead`,
node,
});
}
return;
},
};
},
meta: {
fixable: true,
schema: [
{
additionalProperties: false,
properties: {
alias: { type: 'object' },
aliasForSubpaths: { default: false, type: 'boolean' },
babelOptions: { type: 'object' },
},
type: 'object',
},
],
type: 'suggestion',
},
};