@doubleaxe/eslint-plugin-module-path-fixer
v1.0.3
Published
ESLint plugin for the standardized transformation of module specifiers. Enforces consistency between aliased and relative path formats and manages mandatory or prohibited file extensions across ESM and CommonJS modules.
Downloads
77
Maintainers
Readme
@doubleaxe/eslint-plugin-module-path-fixer
ESLint plugin for deterministic module specifier normalization.
The plugin resolves real import targets and applies autofixes only when target resolution remains semantically equivalent. It is designed to work with:
import ... from 'x'import type ... from 'x'export ... from 'x'export type ... from 'x'import('x')require('x')import x = require('x')- Ignores unresolved and non-static specifiers.
Where path is one of:
- relative paths (
./,../) - absolute paths
tsconfig.json/jsconfig.jsonpath aliasespackage.json#imports- manual alias maps
The plugin ships two autofixable rules:
prefer-alias-or-relativeextensions
Installation
Requires eslint 9.x
npm install @doubleaxe/eslint-plugin-module-path-fixer -D
pnpm add @doubleaxe/eslint-plugin-module-path-fixer -DGlobal Settings
Use settings['module-path-fixer']:
type ModulePathFixerSettings = {
alias?: Array<{
baseUrl: string;
paths: Record<string, string[]>;
}>;
extensionAlias?: Record<string, string>;
extensions?: string[];
indexDirSlash?: 'always' | 'never';
resolveCacheTtl?: number;
usePackageJson?: boolean | string | string[];
useTsConfig?: boolean | string | string[];
};alias- global alias list, in tsconfig format, it is used to resolve aliases in addition totsconfig.jsonandpackage.jsonextensionAlias- maps source extensions to emitted import extensions, also used by file resolver to map extensions for resolution, default is{ '.ts': '.js', '.tsx': '.jsx', '.mts': '.mjs', '.cts': '.cjs' }extensions- which extensions to resolve, default is['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json', '.vue']indexDirSlash- controls whether directory-style specifiers keep a trailing slash when a target resolves as an index directory, default is'never'resolveCacheTtl- resolver cache TTL in milliseconds, default is10000usePackageJson- defaults totrue; also accepts a file name or a list of file names; defaultpackage.json; accepts absolute, relative paths and file namesuseTsConfig- defaults totrue; also accepts a file name or a list of file names; default[tsconfig.json,jsconfig.json]accepts absolute, relative paths and file names
Rule: prefer-alias-or-relative
Normalizes imports between alias and relative forms after resolving the target and normalizing the specifier path. Before any alias decision, the rule normalizes relative inputs with POSIX path normalization. Automatically fixes not normalized paths. May also choose shorter alias if many aliases exists for specific path.
The rule works in two directions:
- Relative specifiers may be rewritten to aliases when the resolved file has a matching alias.
- Alias specifiers may be rewritten to the shortest stable relative form or to shorter alias.
Options (all optional)
type PreferAliasOrRelativeRuleOptions = {
alias?: Array<{
baseUrl: string;
paths: Record<string, string[]>;
}>;
preferAlias?: {
maxChildFolderSegments?: number;
maxParentSegments?: number;
optimization?: 'none' | 'shorter' | 'shorterEqual';
useTotalParentSegments?: 'always' | 'never';
};
};Defaults:
aliasdefaults to globalsettings['module-path-fixer'].aliaspreferAlias.optimizationdefaults to'shorterEqual'preferAlias.maxChildFolderSegmentsdefaults to-1preferAlias.maxParentSegmentsdefaults to1preferAlias.useTotalParentSegmentsdefaults tonever
Behavior is based on comparison of depths. Depths is number of directory path segments (between /) not including file name.
- Alias - name of alias
@/is also segment - Child paths - leading dot
./is also segment - Parent folders - both
../and directory segments counted separately.
Options explained:
preferAlias.optimization: 'none'uses only depth based algorithmpreferAlias.optimization: 'shorter'prefers an alias when it has fewer path segments than the relative form; if no shorter alias found, uses depth based algorithmpreferAlias.optimization: 'shorterEqual'prefers an alias when it is shorter or equal in segment count; if no shorter alias found, uses depth based algorithmpreferAlias.maxParentSegments < 0disables alias conversion for parent-relative imports such as../x, all parent imports become relativepreferAlias.maxParentSegments = 0always allows alias conversion for parent-relative imports when a safe alias existspreferAlias.maxParentSegments > 0allows alias conversion only when the parent-relative path depth> maxParentSegmentspreferAlias.maxChildFolderSegments < 0disables alias conversion for child-relative imports such as./x, all child imports become relativepreferAlias.maxChildFolderSegments = 0always allows alias conversion for child-relative imports when a safe alias existspreferAlias.maxChildFolderSegments > 0allows alias conversion only when the child-relative folder depth> maxChildFolderSegments, including leading dot./preferAlias.useTotalParentSegmentsifneveronly parent segments../are taken into account for depth calculation of parent folders, ifalwaystotal number of parent segments are used
With default options:
- alias is always used if direct alias is found which is shorter or equal in segments to relative path
- otherwise child folder imports are always relative
- otherwise parent folder imports are relative only for direct siblings (one
../segment) and use alias for more (../../)
Usage Examples
See unit tests for more examples
Default behavior, optimization: 'shorterEqual':
// before
import { tool } from '../utils/tool';
// after
import { tool } from '@app/utils/tool';Normalized relative path is handled before alias lookup:
// before
import { qq } from '../utils/../components/tool';
// after
import { qq } from '@app/components/tool';Normalized relative path is always done:
// before
import { qq } from './utils/..//component';
// after
import { qq } from './component';Alias to relative conversion:
// before
import { tool } from '@app/utils/tool';
// after
import { tool } from '../utils/tool';Package imports alias to relative conversion:
// before
import mod from '#core';
// after
import mod from '../core';Rule: extensions
Enforces extension and index style for resolvable imports.
Options (all optional)
type ExtensionsRuleOptions = {
alias?: Array<{
baseUrl: string;
paths: Record<string, string[]>;
}>;
extension?: 'always' | 'never' | ['always' | 'never'] | ['always' | 'never', { except?: string[] }];
index?: 'always' | 'never';
};Defaults:
aliasdefaults to globalsettings['module-path-fixer'].aliasextensiondefaults to['always', { 'except': ['.cjs', '.cts', '.js', '.mjs', '.mts', '.ts'] }]indexdefaults to'never'
Behavior:
extension: 'always': enforces explicit extension in import specifier.extension: 'never': removes explicit extension when safe.extension: ['always']: same asextension: 'always'.extension: ['always', { except: ['.json'] }]: enforces explicit extensions by default, except for.json.extension: ['never', { except: ['.json'] }]: removes extensions by default, except for.json.index: 'always': enforces explicit.../indexform for directory index targets.index: 'never': enforces directory form without/indexwhen safe.- Extension rewriting uses the global
settings['module-path-fixer'].extensionAliasmap.
Usage Examples
extension: 'always', index: 'never'
// before
import { helper } from '../utils/helper';
// after
import { helper } from '../utils/helper.js';extension: ['always', { except: ['.json'] }], index: 'never'
// before
import schema from './schema.json';
import { helper } from '../utils/helper';
// after
import schema from './schema';
import { helper } from '../utils/helper.js';extension: 'never', index: 'never'
// before
import mod from '../core/index.ts';
// after
import mod from '../core';// before
import { tool } from '@app/utils/tool.ts';
// after (extension: 'never')
import { tool } from '@app/utils/tool';extension: 'always', index: 'always'
// before
import mod from '../core';
// after
import mod from '../core/index.js';extension: 'never', index: 'always'
// before
import mod from '../core';
// after
import mod from '../core/index';Flat Config Example
import modulePathFixer from '@doubleaxe/eslint-plugin-module-path-fixer';
export default [
{
plugins: {
'module-path-fixer': modulePathFixer,
},
settings: {
'module-path-fixer': {
// Global resolver settings
extensions: ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json'],
indexDirSlash: 'never',
resolveCacheTtl: 10000,
useTsConfig: true,
usePackageJson: true,
extensionAlias: {
'.ts': '.js',
'.tsx': '.jsx',
'.mts': '.mjs',
'.cts': '.cjs',
},
alias: [
{
baseUrl: './src',
paths: {
'@app/*': ['*'],
},
},
],
},
},
rules: {
'module-path-fixer/prefer-alias-or-relative': [
'error',
{
preferAlias: {
optimization: 'shorterEqual',
maxParentSegments: 1,
maxChildFolderSegments: -1,
useTotalParentSegments: false,
},
alias: [
{
baseUrl: '.',
paths: {
'@feature/*': ['src/feature/*'],
},
},
],
},
],
'module-path-fixer/extensions': [
'error',
{
extension: ['always', { except: ['.json'] }],
index: 'never',
alias: [
{
baseUrl: '.',
paths: {
'@shared/*': ['src/shared/*'],
},
},
],
},
],
},
},
];