Skip to content

Commit 9eb7348

Browse files
committed
feat: 🎸 use eslint-import-resolver logic to resolve
1 parent feed9e0 commit 9eb7348

File tree

6 files changed

+272
-165
lines changed

6 files changed

+272
-165
lines changed

README.md

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
- Provide import patterns that restrict imports of certain files based on location.
4949
- Ensure imports meet the expected guidelines within your repo.
5050
- Adapted from VSCode's rule `code-import-patterns`.
51+
- Works with configured `paths` from your `tsconfig.json`
5152
- Provide custom eslint messaging for each pattern if needed.
5253
- Useful in monorepos and most Typescript projects which utilize incremental builds.
5354

@@ -59,10 +60,6 @@
5960
module.exports = {
6061
root: true,
6162
parser: '@typescript-eslint/parser',
62-
parserOptions: {
63-
project: './tsconfig.lint.json',
64-
sourceType: 'module',
65-
},
6663
rules: {
6764
'ts-import/patterns': [
6865
'error',
@@ -95,13 +92,10 @@ module.exports = {
9592
},
9693
],
9794
},
98-
plugins: ['ts-import'],
99-
settings: {
100-
'import/resolver': {
101-
typescript: {
102-
directory: 'tsconfig.lint.json',
103-
},
104-
},
105-
},
95+
plugins: ['ts-import']
10696
}
10797
```
98+
99+
## Special Thanks
100+
101+
- While originally utilizing custom logic, we since borrowed the resolving method used by [eslint-import-resolver-typescript](https://github.com/alexgorbatchev/eslint-import-resolver-typescript).

package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,26 @@
3939
},
4040
"dependencies": {
4141
"@zerollup/ts-helpers": "^1.7.18",
42+
"debug": "^4.1.1",
43+
"fast-glob": "^3.2.4",
44+
"is-glob": "^4.0.1",
4245
"minimatch": "^3.0.4",
46+
"resolve": "^1.17.0",
47+
"tsconfig-paths": "^3.9.0",
4348
"tslib": "^2.0.0"
4449
},
4550
"devDependencies": {
4651
"@semantic-release/changelog": "^5.0.1",
4752
"@semantic-release/git": "^9.0.0",
4853
"@semantic-release/npm": "^7.0.5",
54+
"@types/debug": "^4.1.5",
4955
"@types/eslint": "^7.2.0",
56+
"@types/is-glob": "^4.0.1",
57+
"@types/minimatch": "^3.0.3",
58+
"@types/resolve": "^1.17.1",
5059
"@typescript-eslint/eslint-plugin": "^3.3.0",
51-
"@typescript-eslint/parser": "^3.3.0",
5260
"@typescript-eslint/experimental-utils": "^3.3.0",
61+
"@typescript-eslint/parser": "^3.3.0",
5362
"commitizen": "^4.1.2",
5463
"eslint": "^7.2.0",
5564
"eslint-config-airbnb-base": "^14.2.0",

src/configPaths.ts

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/* eslint-disable no-nested-ternary */
2+
import path from 'path';
3+
4+
import {
5+
ConfigLoaderSuccessResult,
6+
createMatchPath,
7+
loadConfig,
8+
ConfigLoaderResult,
9+
} from 'tsconfig-paths';
10+
import { sync as globSync } from 'fast-glob';
11+
import isGlob from 'is-glob';
12+
import { isCore, sync } from 'resolve';
13+
import debug from 'debug';
14+
15+
const log = debug('eslint-plugin-ts-import');
16+
17+
const extensions = ['.ts', '.tsx', '.d.ts'].concat(
18+
Object.keys(require.extensions),
19+
'.jsx',
20+
);
21+
22+
export const interfaceVersion = 2;
23+
24+
export interface TsResolverOptions {
25+
alwaysTryTypes?: boolean;
26+
directory?: string | string[];
27+
}
28+
29+
/**
30+
* @param {string} source the module to resolve; i.e './some-module'
31+
* @param {string} file the importing file's full path; i.e. '/usr/local/bin/file.js'
32+
*/
33+
export function resolve(
34+
source: string,
35+
file: string,
36+
options: TsResolverOptions | null,
37+
): {
38+
found: boolean;
39+
path?: string | null;
40+
} {
41+
// eslint-disable-next-line no-param-reassign
42+
options = options || {};
43+
44+
log('looking for:', source);
45+
46+
// don't worry about core node modules
47+
if (isCore(source)) {
48+
log('matched core:', source);
49+
50+
return {
51+
found: true,
52+
path: null,
53+
};
54+
}
55+
56+
initMappers(options);
57+
const mappedPath = getMappedPath(source, file);
58+
if (mappedPath) {
59+
log('matched ts path:', mappedPath);
60+
}
61+
62+
// note that even if we map the path, we still need to do a final resolve
63+
let foundNodePath: string | null | undefined;
64+
try {
65+
foundNodePath = sync(mappedPath || source, {
66+
extensions,
67+
basedir: path.dirname(path.resolve(file)),
68+
packageFilter,
69+
});
70+
} catch (err) {
71+
foundNodePath = null;
72+
}
73+
74+
// naive attempt at @types/* resolution,
75+
// if path is neither absolute nor relative
76+
if (
77+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
78+
(/\.jsx?$/.test(foundNodePath!) ||
79+
(options.alwaysTryTypes && !foundNodePath)) &&
80+
!/^@types[/\\]/.test(source) &&
81+
!path.isAbsolute(source) &&
82+
!source.startsWith('.')
83+
) {
84+
const definitelyTyped = resolve(
85+
`@types${path.sep}${mangleScopedPackage(source)}`,
86+
file,
87+
options,
88+
);
89+
if (definitelyTyped.found) {
90+
return definitelyTyped;
91+
}
92+
}
93+
94+
if (foundNodePath) {
95+
log('matched node path:', foundNodePath);
96+
97+
return {
98+
found: true,
99+
path: foundNodePath,
100+
};
101+
}
102+
103+
log("didn't find ", source);
104+
105+
return {
106+
found: false,
107+
};
108+
}
109+
110+
function packageFilter(pkg: Record<string, string>) {
111+
// eslint-disable-next-line no-param-reassign
112+
pkg.main =
113+
pkg.types || pkg.typings || pkg.module || pkg['jsnext:main'] || pkg.main;
114+
return pkg;
115+
}
116+
117+
let mappersBuildForOptions: TsResolverOptions;
118+
let mappers:
119+
| Array<(source: string, file: string) => string | undefined>
120+
| undefined;
121+
122+
/**
123+
* @param {string} source the module to resolve; i.e './some-module'
124+
* @param {string} file the importing file's full path; i.e. '/usr/local/bin/file.js'
125+
* @returns The mapped path of the module or undefined
126+
*/
127+
function getMappedPath(source: string, file: string) {
128+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
129+
const paths = mappers!
130+
.map((mapper) => mapper(source, file))
131+
.filter((filePath) => !!filePath);
132+
133+
if (paths.length > 1) {
134+
log('found multiple matching ts paths:', paths);
135+
}
136+
137+
return paths[0];
138+
}
139+
140+
function initMappers(options: TsResolverOptions) {
141+
if (mappers && mappersBuildForOptions === options) {
142+
return;
143+
}
144+
145+
const isArrayOfStrings = (array?: string | string[]) =>
146+
Array.isArray(array) && array.every((o) => typeof o === 'string');
147+
148+
const configPaths =
149+
typeof options.directory === 'string'
150+
? [options.directory]
151+
: isArrayOfStrings(options.directory)
152+
? options.directory
153+
: [process.cwd()];
154+
155+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
156+
mappers = configPaths!
157+
// turn glob patterns into paths
158+
.reduce<string[]>(
159+
(paths, fpath) => paths.concat(isGlob(fpath) ? globSync(fpath) : fpath),
160+
[],
161+
)
162+
.map(loadConfig)
163+
.filter(isConfigLoaderSuccessResult)
164+
.map((configLoaderResult) => {
165+
const matchPath = createMatchPath(
166+
configLoaderResult.absoluteBaseUrl,
167+
configLoaderResult.paths,
168+
);
169+
170+
return (source: string, file: string) => {
171+
// exclude files that are not part of the config base url
172+
if (!file.includes(configLoaderResult.absoluteBaseUrl)) {
173+
return undefined;
174+
}
175+
176+
// look for files based on setup tsconfig "paths"
177+
return matchPath(source, undefined, undefined, extensions);
178+
};
179+
});
180+
181+
mappersBuildForOptions = options;
182+
}
183+
184+
function isConfigLoaderSuccessResult(
185+
configLoaderResult: ConfigLoaderResult,
186+
): configLoaderResult is ConfigLoaderSuccessResult {
187+
if (configLoaderResult.resultType !== 'success') {
188+
// this can happen if the user has problems with their tsconfig
189+
// or if it's valid, but they don't have baseUrl set
190+
log('failed to init tsconfig-paths:', configLoaderResult.message);
191+
return false;
192+
}
193+
return true;
194+
}
195+
196+
/**
197+
* For a scoped package, we must look in `@types/foo__bar` instead of `@types/@foo/bar`.
198+
*/
199+
function mangleScopedPackage(moduleName: string) {
200+
if (moduleName.startsWith('@')) {
201+
const replaceSlash = moduleName.replace(path.sep, '__');
202+
if (replaceSlash !== moduleName) {
203+
return replaceSlash.slice(1); // Take off the "@"
204+
}
205+
}
206+
return moduleName;
207+
}

src/importHelpers.ts

Lines changed: 0 additions & 118 deletions
This file was deleted.

0 commit comments

Comments
 (0)