@dword-design/eslint-plugin-import-alias 6.0.3 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,7 +85,9 @@ $ yarn add @dword-design/eslint-plugin-import-alias
85
85
 
86
86
  Add the plugin to your ESLint config:
87
87
 
88
- ```js
88
+ ```ts
89
+ // eslint.config.ts
90
+
89
91
  import { defineConfig } from 'eslint/config';
90
92
  import importAlias from '@dword-design/import-alias';
91
93
 
@@ -94,9 +96,37 @@ export default defineConfig([
94
96
  ]);
95
97
  ```
96
98
 
97
- Alright, now you have to tell the plugin which aliases to use. In the simplest case, you are already using [babel-plugin-module-resolver](https://www.npmjs.com/package/babel-plugin-module-resolver) for your aliases. Your babel config would look something like this:
99
+ Options can be passed by setting them in the `prefer-alias` rule:
100
+
101
+ ```ts
102
+ // eslint.config.ts
103
+
104
+ import { defineConfig } from 'eslint/config';
105
+ import importAlias from '@dword-design/import-alias';
106
+
107
+ export default defineConfig([
108
+ importAlias.configs.recommended,
109
+ {
110
+ rules: {
111
+ '@dword-design/import-alias/prefer-alias': ['error', /* options */],
112
+ },
113
+ },
114
+ ]);
115
+ ```
116
+
117
+ Now you have multiple ways to tell the plugin about aliases.
118
+
119
+ ### `tsconfig.json` `paths` setting
120
+
121
+ If you are a TypeScript user and you have aliases configured in your `tsconfig.json` via the `paths` setting, they will automatically be loaded. You can disable this behavior by setting `shouldReadTsConfig` to `false` in the plugin options.
122
+
123
+ ### [babel-plugin-module-resolver](https://www.npmjs.com/package/babel-plugin-module-resolver)
124
+
125
+ If you are already using [babel-plugin-module-resolver](https://www.npmjs.com/package/babel-plugin-module-resolver), the plugin will load the Babel config and extract the `alias` and `resolvePath` options. You can disable this behavior by setting `shouldReadBabelConfig` to `false` in the plugin options.
98
126
 
99
127
  ```json
128
+ // .babelrc.json
129
+
100
130
  {
101
131
  "plugins": {
102
132
  ["module-resolver", {
@@ -108,32 +138,54 @@ Alright, now you have to tell the plugin which aliases to use. In the simplest c
108
138
  }
109
139
  ```
110
140
 
111
- In this case lucky you, you don't have to do anything else. The plugin should work out of the box.
141
+ ### Plugin `alias` option
112
142
 
113
- If you have a special project setup that does not have a babel config in the project path, you can still use the plugin by passing the aliases directly to the rule. In this case you define the rule additionally in the `rules` section:
143
+ You can also just pass the aliases to the plugin as an option.
114
144
 
115
- ```json
116
- "rules": {
117
- "@dword-design/import-alias/prefer-alias": [
118
- "error",
119
- {
120
- "alias": {
121
- "@": "./src",
122
- "@components": "./src/components"
123
- }
124
- }
125
- ]
126
- }
145
+ ```ts
146
+ // eslint.config.ts
147
+
148
+ import { defineConfig } from 'eslint/config';
149
+ import importAlias from '@dword-design/import-alias';
150
+
151
+ export default defineConfig([
152
+ importAlias.configs.recommended,
153
+ {
154
+ rules: {
155
+ '@dword-design/import-alias/prefer-alias': [
156
+ 'error',
157
+ {
158
+ 'alias': {
159
+ '@': './src',
160
+ '@components': './src/components',
161
+ },
162
+ },
163
+ ],
164
+ },
165
+ },
166
+ ]);
127
167
  ```
128
168
 
169
+ ## Alias resolution
170
+
129
171
  By default, the plugin will convert parent paths to aliases (like `../model/foo`), but will keep subpath imports relative. You can change that to also convert subpaths to aliased imports by passing the `aliasForSubpaths` option to the rule like so:
130
172
 
131
- ```json
132
- "rules": {
133
- "@dword-design/import-alias/prefer-alias": ["error", { "aliasForSubpaths": true }]
173
+ ```ts
174
+ rules: {
175
+ '@dword-design/import-alias/prefer-alias': ['error', { aliasForSubpaths: true }],
176
+ }
177
+ ```
178
+
179
+ Also, inner alias paths are preferred to outer ones. Example:
180
+
181
+ ```ts
182
+ rules: {
183
+ '@dword-design/import-alias/prefer-alias': ['error', { alias: { '@': './app', '@@': '.' }],
134
184
  }
135
185
  ```
136
186
 
187
+ If an import resolves to a file insode `app`, `@` will be preferred over `@@` although both aliases match. This is convenient for the use case where you have a lot of aliases for top-level folders like `components`, `utils` etc where you usually want those instead of a generic root alias. If you have other use cases, please let me know.
188
+
137
189
  <!-- LICENSE/ -->
138
190
  ## Contribute
139
191
 
@@ -0,0 +1,18 @@
1
+ declare module 'babel-plugin-module-resolver' {
2
+ export function resolvePath(
3
+ sourcePath: string,
4
+ currentFile: string,
5
+ options: { alias?: Record<string, string>; cwd?: string },
6
+ ): string;
7
+ }
8
+ declare module '@babel/core' {
9
+ export type BabelPlugin = { key: string; options?: Record<string, unknown> };
10
+
11
+ export interface BabelConfig {
12
+ plugins?: BabelPlugin[];
13
+ }
14
+
15
+ export function loadOptions(
16
+ options?: Record<string, unknown>,
17
+ ): BabelConfig | null;
18
+ }
@@ -0,0 +1,18 @@
1
+ declare const _default: {
2
+ configs: {
3
+ recommended: {
4
+ plugins: {
5
+ '@dword-design/import-alias': {
6
+ rules: {
7
+ 'prefer-alias': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"parentImport" | "subpathImport", [import("./rules/prefer-alias").OptionsInput], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
8
+ };
9
+ };
10
+ };
11
+ rules: {
12
+ '@dword-design/import-alias/prefer-alias': string;
13
+ };
14
+ };
15
+ };
16
+ };
17
+ export default _default;
18
+ export { type OptionsInput } from './rules/prefer-alias';
package/dist/index.js CHANGED
@@ -1,10 +1,18 @@
1
- import preferAlias from './rules/prefer-alias.js';
2
- const plugin = { rules: { 'prefer-alias': preferAlias } };
3
- export default {
4
- configs: {
5
- recommended: {
6
- plugins: { '@dword-design/import-alias': plugin },
7
- rules: { '@dword-design/import-alias/prefer-alias': 'error' },
8
- },
9
- },
1
+ import preferAlias from "./rules/prefer-alias.js";
2
+ const plugin = {
3
+ rules: {
4
+ "prefer-alias": preferAlias
5
+ }
10
6
  };
7
+ export default {
8
+ configs: {
9
+ recommended: {
10
+ plugins: {
11
+ "@dword-design/import-alias": plugin
12
+ },
13
+ rules: {
14
+ "@dword-design/import-alias/prefer-alias": "error"
15
+ }
16
+ }
17
+ }
18
+ };
@@ -0,0 +1,21 @@
1
+ import { loadOptions } from '@babel/core';
2
+ import { ESLintUtils } from '@typescript-eslint/utils';
3
+ export interface BabelPluginModuleResolverOptions {
4
+ alias?: Record<string, string>;
5
+ cwd?: string;
6
+ resolvePath?: (sourcePath: string, currentFile: string, options: Pick<BabelPluginModuleResolverOptions, 'alias' | 'cwd'>) => string;
7
+ }
8
+ export interface Options {
9
+ alias: Record<string, string>;
10
+ aliasForSubpaths: boolean;
11
+ shouldReadTsConfig: boolean;
12
+ shouldReadBabelConfig: boolean;
13
+ resolvePath: (sourcePath: string, currentFile: string, options: Pick<BabelPluginModuleResolverOptions, 'alias' | 'cwd'>) => string;
14
+ cwd: string;
15
+ }
16
+ type BabelOptions = Exclude<Parameters<typeof loadOptions>[0], undefined>;
17
+ export type OptionsInput = Partial<Options> & {
18
+ babelOptions?: BabelOptions;
19
+ };
20
+ declare const _default: ESLintUtils.RuleModule<"parentImport" | "subpathImport", [OptionsInput], unknown, ESLintUtils.RuleListener>;
21
+ export default _default;
@@ -1,87 +1,164 @@
1
- import pathLib from 'node:path';
2
- import { loadOptions } from '@babel/core';
3
- import defu from '@dword-design/defu';
4
- import { resolvePath as defaultResolvePath } from 'babel-plugin-module-resolver';
1
+ import pathLib from "node:path";
2
+ import { loadOptions } from "@babel/core";
3
+ import defaults from "@dword-design/defaults";
4
+ import { ESLintUtils } from "@typescript-eslint/utils";
5
+ import { resolvePath as defaultResolvePath } from "babel-plugin-module-resolver";
6
+ import { omit, orderBy, pick } from "lodash-es";
7
+ const ts = await import("typescript").then(module => module.default).catch(() => null);
8
+ const loadTsConfigPaths = (currentFile, cwd) => {
9
+ if (!ts) {
10
+ return {};
11
+ }
12
+ const configPath = ts.findConfigFile(pathLib.dirname(currentFile), ts.sys.fileExists, "tsconfig.json");
13
+ if (!configPath) {
14
+ return {};
15
+ }
16
+ const configText = ts.sys.readFile(configPath);
17
+ if (!configText) {
18
+ return {};
19
+ }
20
+ const result = ts.parseConfigFileTextToJson(configPath, configText);
21
+ if (!result.config) {
22
+ return {};
23
+ }
24
+ const parsedConfig = ts.parseJsonConfigFileContent(result.config, ts.sys, pathLib.dirname(configPath), void 0, configPath);
25
+ const {
26
+ baseUrl,
27
+ paths
28
+ } = parsedConfig.options;
29
+ if (!paths) {
30
+ return {};
31
+ }
32
+ const aliases = {};
33
+ const basePath = baseUrl ? pathLib.resolve(pathLib.dirname(configPath), baseUrl) : pathLib.dirname(configPath);
34
+ return Object.fromEntries(Object.entries(paths).map(([key, values]) => {
35
+ const aliasKey = key.replace(/\/\*$/, "");
36
+ const absoluteAliasPath = pathLib.resolve(basePath, values[0].replace(/\/\*$/, ""));
37
+ const relativeAliasPath = pathLib.relative(cwd, absoluteAliasPath);
38
+ return [aliasKey, `./${relativeAliasPath}`];
39
+ }));
40
+ return aliases;
41
+ };
42
+ const createRule = ESLintUtils.RuleCreator(() => "");
5
43
  const isParentImport = path => /^(\.\/)?\.\.\//.test(path);
6
44
  const findMatchingAlias = (sourcePath, currentFile, options) => {
7
- const resolvePath = options.resolvePath || defaultResolvePath;
8
- const absoluteSourcePath = pathLib.resolve(pathLib.dirname(currentFile), sourcePath);
9
- for (const aliasName of Object.keys(options.alias)) {
10
- const path = pathLib.resolve(pathLib.dirname(currentFile), resolvePath(`${aliasName}/`, currentFile, options));
11
- if (absoluteSourcePath.startsWith(path)) {
12
- return { name: aliasName, path };
13
- }
45
+ const absoluteSourcePath = pathLib.resolve(pathLib.dirname(currentFile), sourcePath);
46
+ const matches = Object.keys(options.alias).map(aliasName => {
47
+ const path = pathLib.resolve(pathLib.dirname(currentFile), options.resolvePath(`${aliasName}/`, currentFile, pick(options, ["alias", "cwd"])));
48
+ if (absoluteSourcePath.startsWith(path)) {
49
+ return {
50
+ name: aliasName,
51
+ path,
52
+ segmentCount: path.split(pathLib.sep).length
53
+ };
14
54
  }
55
+ return null;
56
+ }).filter(match => !!match);
57
+ const sortedMatches = orderBy(matches, ["segmentCount"], ["desc"]);
58
+ return sortedMatches?.[0] ?? null;
15
59
  };
16
- export default {
17
- create: context => {
18
- const currentFile = context.getFilename();
19
- const folder = pathLib.dirname(currentFile);
20
- // can't check a non-file
21
- if (currentFile === '<text>')
22
- return {};
23
- const optionsFromRule = context.options[0] ?? {};
24
- const babelConfig = (loadOptions({
25
- filename: currentFile,
26
- ...optionsFromRule.babelOptions,
27
- }) || {});
28
- const optionsFromPlugin = babelConfig?.plugins?.find(_ => _.key === 'module-resolver')?.options ??
29
- {};
30
- const options = defu(optionsFromRule, optionsFromPlugin, {
31
- alias: [],
32
- cwd: context.cwd,
33
- });
34
- if (options.alias.length === 0) {
35
- 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.');
60
+ export default createRule({
61
+ create: context => {
62
+ const currentFile = context.getFilename();
63
+ const folder = pathLib.dirname(currentFile);
64
+ if (currentFile === "<text>") return {};
65
+ const optionsFromRule = defaults(context.options[0] ?? {}, {
66
+ babelOptions: {},
67
+ shouldReadBabelConfig: true,
68
+ shouldReadTsConfig: true
69
+ });
70
+ const optionsFromBabelPlugin = optionsFromRule.shouldReadBabelConfig ? (() => {
71
+ const babelConfig = loadOptions({
72
+ filename: currentFile,
73
+ ...optionsFromRule.babelOptions
74
+ });
75
+ const babelPlugin = babelConfig?.plugins?.find?.(iteratedPlugin => iteratedPlugin.key === "module-resolver") ?? null;
76
+ const babelPluginOptions = babelPlugin?.options ?? {};
77
+ return pick(babelPluginOptions, ["alias", "resolvePath"]);
78
+ })() : {};
79
+ const options = defaults(omit(optionsFromRule, ["babelOptions"]), {
80
+ alias: optionsFromRule.shouldReadTsConfig ? loadTsConfigPaths(currentFile, context.cwd) : {}
81
+ }, optionsFromBabelPlugin, {
82
+ alias: {},
83
+ aliasForSubpaths: false,
84
+ cwd: context.cwd,
85
+ resolvePath: defaultResolvePath
86
+ });
87
+ if (Object.keys(options.alias).length === 0) {
88
+ 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, defining them in your tsconfig.json paths, or passing them directly to the prefer-alias rule.");
89
+ }
90
+ return {
91
+ ImportDeclaration: node => {
92
+ const sourcePath = node.source.value;
93
+ const hasAlias = Object.keys(options.alias).some(alias => sourcePath.startsWith(`${alias}/`));
94
+ if (isParentImport(sourcePath)) {
95
+ const matchingAlias = findMatchingAlias(sourcePath, currentFile, options);
96
+ if (!matchingAlias) {
97
+ return;
98
+ }
99
+ const absoluteImportPath = pathLib.resolve(folder, sourcePath);
100
+ const rewrittenImport = `${matchingAlias.name}/${pathLib.relative(matchingAlias.path, absoluteImportPath).replaceAll("\\", "/")}`;
101
+ return context.report({
102
+ data: {
103
+ rewrittenImport,
104
+ sourcePath
105
+ },
106
+ fix: fixer => fixer.replaceTextRange([node.source.range[0] + 1, node.source.range[1] - 1], rewrittenImport),
107
+ messageId: "parentImport",
108
+ node
109
+ });
36
110
  }
37
- const resolvePath = options.resolvePath || defaultResolvePath;
38
- return {
39
- ImportDeclaration: node => {
40
- const sourcePath = node.source.value;
41
- const hasAlias = Object.keys(options.alias).some(alias => sourcePath.startsWith(`${alias}/`));
42
- // relative parent
43
- if (isParentImport(sourcePath)) {
44
- const matchingAlias = findMatchingAlias(sourcePath, currentFile, options);
45
- if (!matchingAlias) {
46
- return;
47
- }
48
- const absoluteImportPath = pathLib.resolve(folder, sourcePath);
49
- const rewrittenImport = `${matchingAlias.name}/${pathLib
50
- .relative(matchingAlias.path, absoluteImportPath)
51
- .replaceAll('\\', '/')}`;
52
- return context.report({
53
- fix: fixer => fixer.replaceTextRange([node.source.range[0] + 1, node.source.range[1] - 1], rewrittenImport),
54
- message: `Unexpected parent import '${sourcePath}'. Use '${rewrittenImport}' instead`,
55
- node,
56
- });
57
- }
58
- const importWithoutAlias = resolvePath(sourcePath, currentFile, options);
59
- if (!isParentImport(importWithoutAlias) &&
60
- hasAlias &&
61
- !options.aliasForSubpaths) {
62
- return context.report({
63
- fix: fixer => fixer.replaceTextRange([node.source.range[0] + 1, node.source.range[1] - 1], importWithoutAlias),
64
- message: `Unexpected subpath import via alias '${sourcePath}'. Use '${importWithoutAlias}' instead`,
65
- node,
66
- });
67
- }
68
- return;
111
+ const importWithoutAlias = options.resolvePath(sourcePath, currentFile, options);
112
+ if (!isParentImport(importWithoutAlias) && hasAlias && !options.aliasForSubpaths) {
113
+ return context.report({
114
+ data: {
115
+ rewrittenImport: importWithoutAlias,
116
+ sourcePath
69
117
  },
70
- };
118
+ fix: fixer => fixer.replaceTextRange([node.source.range[0] + 1, node.source.range[1] - 1], importWithoutAlias),
119
+ messageId: "subpathImport",
120
+ node
121
+ });
122
+ }
123
+ return;
124
+ }
125
+ };
126
+ },
127
+ defaultOptions: [{}],
128
+ meta: {
129
+ docs: {
130
+ description: "Enforce usage of import aliases over relative parent imports"
71
131
  },
72
- meta: {
73
- fixable: true,
74
- schema: [
75
- {
76
- additionalProperties: false,
77
- properties: {
78
- alias: { type: 'object' },
79
- aliasForSubpaths: { default: false, type: 'boolean' },
80
- babelOptions: { type: 'object' },
81
- },
82
- type: 'object',
83
- },
84
- ],
85
- type: 'suggestion',
132
+ fixable: "code",
133
+ messages: {
134
+ parentImport: "Unexpected parent import '{{sourcePath}}'. Use '{{rewrittenImport}}' instead",
135
+ subpathImport: "Unexpected subpath import via alias '{{sourcePath}}'. Use '{{rewrittenImport}}' instead"
86
136
  },
87
- };
137
+ schema: [{
138
+ additionalProperties: false,
139
+ properties: {
140
+ alias: {
141
+ type: "object"
142
+ },
143
+ aliasForSubpaths: {
144
+ default: false,
145
+ type: "boolean"
146
+ },
147
+ babelOptions: {
148
+ type: "object"
149
+ },
150
+ shouldReadBabelConfig: {
151
+ default: true,
152
+ type: "boolean"
153
+ },
154
+ shouldReadTsConfig: {
155
+ default: true,
156
+ type: "boolean"
157
+ }
158
+ },
159
+ type: "object"
160
+ }],
161
+ type: "suggestion"
162
+ },
163
+ name: "prefer-alias"
164
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dword-design/eslint-plugin-import-alias",
3
- "version": "6.0.3",
3
+ "version": "8.0.0",
4
4
  "description": "An ESLint plugin that enforces the use of import aliases. Also supports autofixing.",
5
5
  "keywords": [
6
6
  "alias",
@@ -28,7 +28,12 @@
28
28
  "license": "MIT",
29
29
  "author": "Sebastian Landwehr <info@sebastianlandwehr.com>",
30
30
  "type": "module",
31
- "exports": "./dist/index.js",
31
+ "exports": {
32
+ ".": {
33
+ "default": "./dist/index.js",
34
+ "types": "./dist/index.d.ts"
35
+ }
36
+ },
32
37
  "main": "dist/index.js",
33
38
  "files": [
34
39
  "dist"
@@ -41,26 +46,40 @@
41
46
  "lint": "base lint",
42
47
  "prepare": "base prepare",
43
48
  "prepublishOnly": "base prepublishOnly",
44
- "test": "base test"
49
+ "test": "base test",
50
+ "typecheck": "base typecheck",
51
+ "verify": "base verify"
45
52
  },
46
53
  "dependencies": {
47
- "@babel/core": "^7.27.4",
48
- "@dword-design/defu": "^1.0.0",
54
+ "@babel/core": "^7.28.5",
55
+ "@dword-design/defaults": "^1.1.1",
56
+ "@types/babel__core": "^7.20.5",
57
+ "@types/lodash-es": "^4.17.12",
58
+ "@typescript-eslint/utils": "^8.50.1",
49
59
  "babel-plugin-module-resolver": "^5.0.2"
50
60
  },
51
61
  "devDependencies": {
52
- "@dword-design/base": "^13.0.1",
53
- "@playwright/test": "^1.52.0",
54
- "depcheck-package-name": "^3.0.1",
55
- "endent": "npm:@dword-design/endent@^1.4.1",
56
- "eslint": "^9.28.0",
57
- "lodash-es": "^4.17.21",
58
- "output-files": "^2.0.32",
59
- "typescript-eslint": "^8.33.1"
62
+ "@dword-design/base": "^16.1.7",
63
+ "@playwright/test": "^1.57.0",
64
+ "depcheck-package-name": "^5.0.0",
65
+ "endent": "npm:@dword-design/endent@^1.4.7",
66
+ "eslint": "^9.39.2",
67
+ "lodash-es": "^4.17.22",
68
+ "output-files": "^3.0.0",
69
+ "typescript": "^5.0.0",
70
+ "typescript-eslint": "^8.50.0"
71
+ },
72
+ "peerDependencies": {
73
+ "typescript": ">=5.0.0"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "typescript": {
77
+ "optional": true
78
+ }
60
79
  },
61
80
  "packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912",
62
81
  "engines": {
63
- "node": ">=18"
82
+ "node": ">=22"
64
83
  },
65
84
  "publishConfig": {
66
85
  "access": "public"