Skip to content

Commit be66a6d

Browse files
authored
Merge pull request #46 from vitonsky/26-add-option-to-provide-a-path-to-tsconfigjsconfig
feat: Add option to provide a path to tsconfig/jsconfig
2 parents 0f948fe + 7cb0326 commit be66a6d

File tree

7 files changed

+159
-70
lines changed

7 files changed

+159
-70
lines changed

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
"lang": "en_US",
3838
"skipWords": [
3939
"tsconfig",
40-
"jsconfig"
40+
"jsconfig",
41+
"qux"
4142
],
4243
// Check if word contains numbers
4344
"skipIfMatch": [

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,12 @@ and this code will be invalid
5050

5151
import foo from './foo';
5252
import barZ from './bar/x/y/z';
53-
```
53+
```
54+
55+
# Options
56+
57+
## configFilePath
58+
59+
Provide path to json file with a compiler config.
60+
61+
When not set, used `tsconfig.json` from root directory if exists or `jsconfig.json` if not.

src/rules/alias.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,46 @@ tester.run('paths-alias', rule, {
7373
},
7474
],
7575
});
76+
77+
[
78+
'testFiles/custom-tsconfig-with-foo-and-qux.json',
79+
'./testFiles/custom-tsconfig-with-foo-and-qux.json',
80+
path.resolve('testFiles/custom-tsconfig-with-foo-and-qux.json'),
81+
].forEach((configFilePath) => {
82+
const options = [
83+
{
84+
configFilePath,
85+
},
86+
];
87+
88+
tester.run(`paths-alias rule with configFile option "${configFilePath}"`, rule, {
89+
valid: [
90+
{
91+
name: 'relative import from bar are possible, because used another config',
92+
filename: path.resolve('./src/index.ts'),
93+
options,
94+
code: `import bar from './bar/index';`,
95+
},
96+
{
97+
name: 'import from @foo',
98+
filename: path.resolve('./src/index.ts'),
99+
code: `import foo from '@foo';`,
100+
},
101+
{
102+
name: 'import from @qux',
103+
filename: path.resolve('./src/index.ts'),
104+
code: `import qux from '@qux';`,
105+
},
106+
],
107+
invalid: [
108+
{
109+
name: 'relative import from alias must be fixed',
110+
filename: path.resolve('./src/index.ts'),
111+
options,
112+
code: `import z from './foo/x/y/z';`,
113+
output: `import z from '@foo/x/y/z';`,
114+
errors: ['Update import to @foo/x/y/z'],
115+
},
116+
],
117+
});
118+
});

src/rules/alias.ts

Lines changed: 54 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
'use strict';
2-
3-
import { parse as parseJsonWithComments } from 'comment-json';
41
import { Rule } from 'eslint';
52
import fs from 'fs';
63
import path from 'path';
74

5+
import { CompilerOptions } from '../types';
6+
import { getCompilerConfigFromFile } from '../utils/getCompilerConfigFromFile';
7+
88
function findDirWithFile(filename: string) {
99
let dir = path.resolve(filename);
1010

@@ -20,68 +20,48 @@ function findDirWithFile(filename: string) {
2020
}
2121

2222
function findAlias(
23+
compilerOptions: CompilerOptions,
2324
baseDir: string,
2425
importPath: string,
2526
filePath: string,
2627
ignoredPaths: string[] = [],
2728
) {
28-
const isTsconfigExists = fs.existsSync(path.join(baseDir, 'tsconfig.json'));
29-
const isJsconfigExists = fs.existsSync(path.join(baseDir, 'jsconfig.json'));
30-
31-
const configFile = isTsconfigExists
32-
? 'tsconfig.json'
33-
: isJsconfigExists
34-
? 'jsconfig.json'
35-
: null;
36-
37-
if (configFile) {
38-
const tsconfig = parseJsonWithComments(
39-
fs.readFileSync(path.join(baseDir, configFile)).toString('utf8'),
40-
);
41-
42-
const paths: Record<string, string[]> =
43-
(tsconfig as any)?.compilerOptions?.paths ?? {};
44-
for (const [alias, aliasPaths] of Object.entries(paths)) {
45-
// TODO: support full featured glob patterns instead of trivial cases like `@utils/*` and `src/utils/*`
46-
const matchedPath = aliasPaths.find((dirPath) => {
47-
// Remove last asterisk
48-
const dirPathBase = path
49-
.join(baseDir, dirPath)
50-
.split('/')
51-
.slice(0, -1)
52-
.join('/');
53-
54-
if (filePath.startsWith(dirPathBase)) return false;
55-
if (
56-
ignoredPaths.some((ignoredPath) =>
57-
ignoredPath.startsWith(dirPathBase),
58-
)
59-
)
60-
return false;
61-
62-
return importPath.startsWith(dirPathBase);
63-
});
64-
65-
if (!matchedPath) continue;
66-
67-
// Split import path
68-
// Remove basedir and slash in start
69-
const slicedImportPath = importPath
70-
.slice(baseDir.length + 1)
71-
.slice(path.dirname(matchedPath).length + 1);
72-
73-
// Remove asterisk from end of alias
74-
const replacedPathSegments = path
75-
.join(path.dirname(alias), slicedImportPath)
76-
.split('/');
77-
78-
// Add index in path
79-
return (
80-
replacedPathSegments.length === 1
81-
? [...replacedPathSegments, 'index']
82-
: replacedPathSegments
83-
).join('/');
84-
}
29+
for (const [alias, aliasPaths] of Object.entries(compilerOptions.paths)) {
30+
// TODO: support full featured glob patterns instead of trivial cases like `@utils/*` and `src/utils/*`
31+
const matchedPath = aliasPaths.find((dirPath) => {
32+
// Remove last asterisk
33+
const dirPathBase = path
34+
.join(baseDir, dirPath)
35+
.split('/')
36+
.slice(0, -1)
37+
.join('/');
38+
39+
if (filePath.startsWith(dirPathBase)) return false;
40+
if (ignoredPaths.some((ignoredPath) => ignoredPath.startsWith(dirPathBase)))
41+
return false;
42+
43+
return importPath.startsWith(dirPathBase);
44+
});
45+
46+
if (!matchedPath) continue;
47+
48+
// Split import path
49+
// Remove basedir and slash in start
50+
const slicedImportPath = importPath
51+
.slice(baseDir.length + 1)
52+
.slice(path.dirname(matchedPath).length + 1);
53+
54+
// Remove asterisk from end of alias
55+
const replacedPathSegments = path
56+
.join(path.dirname(alias), slicedImportPath)
57+
.split('/');
58+
59+
// Add index in path
60+
return (
61+
replacedPathSegments.length === 1
62+
? [...replacedPathSegments, 'index']
63+
: replacedPathSegments
64+
).join('/');
8565
}
8666

8767
return null;
@@ -95,28 +75,34 @@ const rule: Rule.RuleModule = {
9575
},
9676
create(context) {
9777
const baseDir = findDirWithFile('package.json');
98-
9978
if (!baseDir) throw new Error("Can't find base dir");
10079

80+
const [{ ignoredPaths = [], configFilePath = null } = {}] = context.options as [
81+
{ ignoredPaths: string[]; configFilePath?: string },
82+
];
83+
84+
const compilerOptions = getCompilerConfigFromFile(
85+
baseDir,
86+
configFilePath ?? undefined,
87+
);
88+
if (!compilerOptions) throw new Error('Compiler options did not found');
89+
10190
return {
10291
ImportDeclaration(node) {
103-
const [{ ignoredPaths = [] } = {}] = context.options as [
104-
{ ignoredPaths: string[] },
105-
];
106-
107-
const source = node.source.value;
108-
if (typeof source === 'string' && source.startsWith('.')) {
92+
const importPath = node.source.value;
93+
if (typeof importPath === 'string' && importPath.startsWith('.')) {
10994
const filename = context.getFilename();
11095

11196
const resolvedIgnoredPaths = ignoredPaths.map((ignoredPath) =>
11297
path.normalize(path.join(path.dirname(filename), ignoredPath)),
11398
);
11499

115100
const absolutePath = path.normalize(
116-
path.join(path.dirname(filename), source),
101+
path.join(path.dirname(filename), importPath),
117102
);
118103

119104
const replacement = findAlias(
105+
compilerOptions,
120106
baseDir,
121107
absolutePath,
122108
filename,

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type CompilerOptions = {
2+
baseUrl?: string;
3+
paths: Record<string, string[]>;
4+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { parse as parseJsonWithComments } from 'comment-json';
2+
import fs from 'fs';
3+
import path from 'path';
4+
5+
import { CompilerOptions } from '../types';
6+
7+
export function getCompilerConfigFromFile(
8+
baseDir: string,
9+
configFilePath?: string,
10+
): CompilerOptions | null {
11+
if (!configFilePath) {
12+
// Looking for a config file
13+
for (const filename of ['tsconfig.json', 'jsconfig.json']) {
14+
const resolvedPath = path.resolve(path.join(baseDir, filename));
15+
const isFileExists = fs.existsSync(resolvedPath);
16+
if (isFileExists) {
17+
configFilePath = resolvedPath;
18+
break;
19+
}
20+
}
21+
22+
if (!configFilePath) return null;
23+
}
24+
25+
const tsconfig = parseJsonWithComments(
26+
fs.readFileSync(path.resolve(configFilePath)).toString('utf8'),
27+
);
28+
29+
// TODO: validate options
30+
const { baseUrl, paths = {} } = (tsconfig as any)?.compilerOptions ?? {};
31+
32+
return {
33+
baseUrl,
34+
paths,
35+
};
36+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
// Test comment, that must not break a parsing of config file
4+
// See more info in https://github.com/vitonsky/eslint-plugin-paths/issues/37#issuecomment-2052542343
5+
"baseUrl": ".",
6+
"paths": {
7+
"@foo/*": ["src/foo/*"],
8+
"@qux/*": ["src/qux/*"]
9+
}
10+
}
11+
}

0 commit comments

Comments
 (0)