Skip to content

Commit b8d061a

Browse files
authored
Merge pull request #32 from domharries/inherited-aliases
feat: inherited import aliases in tsconfig
2 parents 68f9792 + fbf260d commit b8d061a

File tree

3 files changed

+135
-14
lines changed

3 files changed

+135
-14
lines changed

src/spec/resolveAliasedFilepath.spec.ts

+65
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {existsSync} from 'fs';
22
import {lilconfigSync} from 'lilconfig';
33
import {type Mock, describe, expect, it, vi} from 'vitest';
44
import {resolveAliasedImport} from '../utils/resolveAliasedImport';
5+
import {resolveJson5File} from '../utils/resolveJson5File';
56

67
vi.mock('lilconfig', async () => {
78
const actual: typeof import('lilconfig') =
@@ -12,6 +13,12 @@ vi.mock('lilconfig', async () => {
1213
};
1314
});
1415

16+
vi.mock('../utils/resolveJson5File', async () => {
17+
return {
18+
resolveJson5File: vi.fn(),
19+
};
20+
});
21+
1522
vi.mock('fs', async () => {
1623
const actual: typeof import('fs') = await vi.importActual('fs');
1724
const existsSync = vi.fn();
@@ -175,4 +182,62 @@ describe('utils: resolveAliasedFilepath', () => {
175182

176183
expect(result).toEqual(expected);
177184
});
185+
186+
it('searches for paths in parent configs when extends is set', () => {
187+
(lilconfigSync as Mock).mockReturnValueOnce({
188+
search: () => ({
189+
config: {
190+
compilerOptions: {},
191+
extends: '../tsconfig.base.json',
192+
},
193+
filepath: '/root/module/tsconfig.json',
194+
}),
195+
});
196+
(existsSync as Mock).mockReturnValue(true);
197+
(resolveJson5File as Mock).mockReturnValueOnce({
198+
config: {
199+
compilerOptions: {
200+
baseUrl: './',
201+
paths: {
202+
'@other/*': ['./other/*'],
203+
},
204+
},
205+
},
206+
filepath: '/root/tsconfig.base.json',
207+
});
208+
const result = resolveAliasedImport({
209+
location: '',
210+
importFilepath: '@other/file.css',
211+
});
212+
const expected = '/root/other/file.css';
213+
214+
expect(result).toEqual(expected);
215+
});
216+
217+
it('handles infinite extends loops', () => {
218+
(lilconfigSync as Mock).mockReturnValueOnce({
219+
search: () => ({
220+
config: {
221+
compilerOptions: {},
222+
extends: '../tsconfig.base.json',
223+
},
224+
filepath: '/root/module/tsconfig.json',
225+
}),
226+
});
227+
(existsSync as Mock).mockReturnValue(true);
228+
(resolveJson5File as Mock).mockReturnValue({
229+
config: {
230+
compilerOptions: {},
231+
extends: './tsconfig.base.json',
232+
},
233+
filepath: '/root/tsconfig.base.json',
234+
});
235+
const result = resolveAliasedImport({
236+
location: '',
237+
importFilepath: '@bar/file.css',
238+
});
239+
const expected = null;
240+
241+
expect(result).toEqual(expected);
242+
});
178243
});

src/utils/resolveAliasedImport.ts

+34-14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'path';
33
import JSON5 from 'json5';
44

55
import {lilconfigSync} from 'lilconfig';
6+
import {resolveJson5File} from './resolveJson5File';
67

78
const validate = {
89
string: (x: unknown): x is string => typeof x === 'string',
@@ -54,22 +55,46 @@ export const resolveAliasedImport = ({
5455
'.json': (_, content) => JSON5.parse(content),
5556
},
5657
});
57-
const config = searcher.search(location);
58+
let config = searcher.search(location);
5859

5960
if (config == null) {
6061
return null;
6162
}
6263

63-
const paths: unknown = config.config?.compilerOptions?.paths;
64+
let configLocation = path.dirname(config.filepath);
6465

65-
const potentialBaseUrl: unknown = config.config?.compilerOptions?.baseUrl;
66+
let paths: unknown = config.config?.compilerOptions?.paths;
67+
let pathsBase = configLocation;
6668

67-
const configLocation = path.dirname(config.filepath);
69+
let potentialBaseUrl: unknown = config.config?.compilerOptions?.baseUrl;
70+
let baseUrl = validate.string(potentialBaseUrl)
71+
? path.resolve(configLocation, potentialBaseUrl)
72+
: null;
73+
74+
let depth = 0;
75+
while ((!paths || !baseUrl) && config.config?.extends && depth++ < 10) {
76+
config = resolveJson5File({
77+
path: config.config.extends,
78+
base: configLocation,
79+
});
80+
if (config == null) {
81+
return null;
82+
}
83+
configLocation = path.dirname(config.filepath);
84+
if (!paths && config.config?.compilerOptions?.paths) {
85+
paths = config.config.compilerOptions.paths;
86+
pathsBase = configLocation;
87+
}
88+
if (!baseUrl && config.config?.compilerOptions?.baseUrl) {
89+
potentialBaseUrl = config.config.compilerOptions.baseUrl;
90+
baseUrl = validate.string(potentialBaseUrl)
91+
? path.resolve(configLocation, potentialBaseUrl)
92+
: null;
93+
}
94+
}
6895

6996
if (validate.tsconfigPaths(paths)) {
70-
const baseUrl = validate.string(potentialBaseUrl)
71-
? potentialBaseUrl
72-
: '.';
97+
baseUrl = baseUrl || pathsBase;
7398

7499
for (const alias in paths) {
75100
const aliasRe = new RegExp(alias.replace('*', '(.+)'), '');
@@ -80,7 +105,6 @@ export const resolveAliasedImport = ({
80105

81106
for (const potentialAliasLocation of paths[alias]) {
82107
const resolvedFileLocation = path.resolve(
83-
configLocation,
84108
baseUrl,
85109
potentialAliasLocation
86110
// "./utils/*" -> "./utils/style.module.css"
@@ -100,12 +124,8 @@ export const resolveAliasedImport = ({
100124
// so here we only try and resolve the file if baseUrl is explcitly set and valid
101125
// i.e. if no baseUrl is provided
102126
// then no imports relative to baseUrl on its own are allowed, only relative to paths
103-
if (validate.string(potentialBaseUrl)) {
104-
const resolvedFileLocation = path.resolve(
105-
configLocation,
106-
potentialBaseUrl,
107-
importFilepath,
108-
);
127+
if (baseUrl) {
128+
const resolvedFileLocation = path.resolve(baseUrl, importFilepath);
109129

110130
if (fs.existsSync(resolvedFileLocation)) {
111131
return resolvedFileLocation;

src/utils/resolveJson5File.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import fs from 'fs';
2+
import JSON5 from 'json5';
3+
4+
import type {LilconfigResult} from 'lilconfig';
5+
6+
/**
7+
* Attempts to resolve the path to a json5 file using node.js resolution rules
8+
*
9+
* returns null if file could not be resolved, or if JSON5 parsing fails
10+
* @see https://www.typescriptlang.org/tsconfig/#extends
11+
*/
12+
export const resolveJson5File = ({
13+
path,
14+
base,
15+
}: {
16+
/**
17+
* path to the json5 file
18+
* @example "../tsconfig.json"
19+
*/
20+
path: string;
21+
/**
22+
* directory where the file with import is located
23+
* @example "/Users/foo/project/components"
24+
*/
25+
base: string;
26+
}): LilconfigResult => {
27+
try {
28+
const filepath = require.resolve(path, {paths: [base]});
29+
const content = fs.readFileSync(filepath, 'utf8');
30+
const isEmpty = content.trim() === '';
31+
const config = isEmpty ? {} : JSON5.parse(content);
32+
return {filepath, isEmpty, config};
33+
} catch (e) {
34+
return null;
35+
}
36+
};

0 commit comments

Comments
 (0)