@dword-design/eslint-plugin-import-alias 7.0.0 → 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
 
package/dist/index.d.ts CHANGED
@@ -4,9 +4,7 @@ declare const _default: {
4
4
  plugins: {
5
5
  '@dword-design/import-alias': {
6
6
  rules: {
7
- 'prefer-alias': import("@typescript-eslint/utils/dist/ts-eslint").RuleModule<"parentImport" | "subpathImport", [Partial<import("./rules/prefer-alias").Options> & {
8
- babelOptions?: Record<string, unknown>;
9
- }], unknown, import("@typescript-eslint/utils/dist/ts-eslint").RuleListener>;
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>;
10
8
  };
11
9
  };
12
10
  };
@@ -17,3 +15,4 @@ declare const _default: {
17
15
  };
18
16
  };
19
17
  export default _default;
18
+ export { type OptionsInput } from './rules/prefer-alias';
@@ -1,3 +1,4 @@
1
+ import { loadOptions } from '@babel/core';
1
2
  import { ESLintUtils } from '@typescript-eslint/utils';
2
3
  export interface BabelPluginModuleResolverOptions {
3
4
  alias?: Record<string, string>;
@@ -7,11 +8,14 @@ export interface BabelPluginModuleResolverOptions {
7
8
  export interface Options {
8
9
  alias: Record<string, string>;
9
10
  aliasForSubpaths: boolean;
11
+ shouldReadTsConfig: boolean;
12
+ shouldReadBabelConfig: boolean;
10
13
  resolvePath: (sourcePath: string, currentFile: string, options: Pick<BabelPluginModuleResolverOptions, 'alias' | 'cwd'>) => string;
11
14
  cwd: string;
12
15
  }
13
- type OptionsInput = Partial<Options> & {
14
- babelOptions?: Record<string, unknown>;
16
+ type BabelOptions = Exclude<Parameters<typeof loadOptions>[0], undefined>;
17
+ export type OptionsInput = Partial<Options> & {
18
+ babelOptions?: BabelOptions;
15
19
  };
16
20
  declare const _default: ESLintUtils.RuleModule<"parentImport" | "subpathImport", [OptionsInput], unknown, ESLintUtils.RuleListener>;
17
21
  export default _default;
@@ -3,20 +3,59 @@ import { loadOptions } from "@babel/core";
3
3
  import defaults from "@dword-design/defaults";
4
4
  import { ESLintUtils } from "@typescript-eslint/utils";
5
5
  import { resolvePath as defaultResolvePath } from "babel-plugin-module-resolver";
6
- import { omit, pick } from "lodash-es";
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
+ };
7
42
  const createRule = ESLintUtils.RuleCreator(() => "");
8
43
  const isParentImport = path => /^(\.\/)?\.\.\//.test(path);
9
44
  const findMatchingAlias = (sourcePath, currentFile, options) => {
10
45
  const absoluteSourcePath = pathLib.resolve(pathLib.dirname(currentFile), sourcePath);
11
- for (const aliasName of Object.keys(options.alias)) {
46
+ const matches = Object.keys(options.alias).map(aliasName => {
12
47
  const path = pathLib.resolve(pathLib.dirname(currentFile), options.resolvePath(`${aliasName}/`, currentFile, pick(options, ["alias", "cwd"])));
13
48
  if (absoluteSourcePath.startsWith(path)) {
14
49
  return {
15
50
  name: aliasName,
16
- path
51
+ path,
52
+ segmentCount: path.split(pathLib.sep).length
17
53
  };
18
54
  }
19
- }
55
+ return null;
56
+ }).filter(match => !!match);
57
+ const sortedMatches = orderBy(matches, ["segmentCount"], ["desc"]);
58
+ return sortedMatches?.[0] ?? null;
20
59
  };
21
60
  export default createRule({
22
61
  create: context => {
@@ -24,23 +63,29 @@ export default createRule({
24
63
  const folder = pathLib.dirname(currentFile);
25
64
  if (currentFile === "<text>") return {};
26
65
  const optionsFromRule = defaults(context.options[0] ?? {}, {
27
- babelOptions: {}
28
- });
29
- const babelConfig = loadOptions({
30
- filename: currentFile,
31
- ...optionsFromRule.babelOptions
66
+ babelOptions: {},
67
+ shouldReadBabelConfig: true,
68
+ shouldReadTsConfig: true
32
69
  });
33
- const babelPlugin = babelConfig?.plugins?.find?.(iteratedPlugin => iteratedPlugin.key === "module-resolver") ?? null;
34
- const babelPluginOptions = babelPlugin?.options ?? {};
35
- const optionsFromPlugin = pick(babelPluginOptions, ["alias", "resolvePath"]);
36
- const options = defaults(omit(optionsFromRule, ["babelOptions"]), optionsFromPlugin, {
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, {
37
82
  alias: {},
38
83
  aliasForSubpaths: false,
39
84
  cwd: context.cwd,
40
85
  resolvePath: defaultResolvePath
41
86
  });
42
87
  if (Object.keys(options.alias).length === 0) {
43
- 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.");
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.");
44
89
  }
45
90
  return {
46
91
  ImportDeclaration: node => {
@@ -101,6 +146,14 @@ export default createRule({
101
146
  },
102
147
  babelOptions: {
103
148
  type: "object"
149
+ },
150
+ shouldReadBabelConfig: {
151
+ default: true,
152
+ type: "boolean"
153
+ },
154
+ shouldReadTsConfig: {
155
+ default: true,
156
+ type: "boolean"
104
157
  }
105
158
  },
106
159
  type: "object"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dword-design/eslint-plugin-import-alias",
3
- "version": "7.0.0",
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",
@@ -59,15 +59,24 @@
59
59
  "babel-plugin-module-resolver": "^5.0.2"
60
60
  },
61
61
  "devDependencies": {
62
- "@dword-design/base": "^16.1.5",
62
+ "@dword-design/base": "^16.1.7",
63
63
  "@playwright/test": "^1.57.0",
64
64
  "depcheck-package-name": "^5.0.0",
65
65
  "endent": "npm:@dword-design/endent@^1.4.7",
66
66
  "eslint": "^9.39.2",
67
67
  "lodash-es": "^4.17.22",
68
68
  "output-files": "^3.0.0",
69
+ "typescript": "^5.0.0",
69
70
  "typescript-eslint": "^8.50.0"
70
71
  },
72
+ "peerDependencies": {
73
+ "typescript": ">=5.0.0"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "typescript": {
77
+ "optional": true
78
+ }
79
+ },
71
80
  "packageManager": "pnpm@10.11.1+sha512.e519b9f7639869dc8d5c3c5dfef73b3f091094b0a006d7317353c72b124e80e1afd429732e28705ad6bfa1ee879c1fce46c128ccebd3192101f43dd67c667912",
72
81
  "engines": {
73
82
  "node": ">=22"