Skip to content

Commit

Permalink
fix: resolve paths from tsconfig that defined it (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber authored Sep 22, 2023
1 parent 785457b commit 4b5f839
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 39 deletions.
30 changes: 15 additions & 15 deletions src/parse-tsconfig/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { TsConfigJson, TsConfigJsonResolved } from '../types.js';
import { normalizePath } from '../utils/normalize-path.js';
import { readJsonc } from '../utils/read-jsonc.js';
import { realpath } from '../utils/fs-cached.js';
import { implicitBaseUrlSymbol } from '../utils/symbols.js';
import { resolveExtendsPath } from './resolve-extends-path.js';

const resolveExtends = (
Expand Down Expand Up @@ -34,20 +35,6 @@ const resolveExtends = (

const { compilerOptions } = extendsConfig;
if (compilerOptions) {
const { baseUrl = '.', paths } = compilerOptions;
if (paths) {
for (const key in paths) {
if (Array.isArray(paths[key])) {
paths[key] = paths[key].map(
p => normalizePath(path.relative(
fromDirectoryPath,
path.join(extendsDirectoryPath, baseUrl, p),
)),
);
}
}
}

const resolvePaths = ['baseUrl', 'outDir'] as const;
for (const property of resolvePaths) {
const unresolvedPath = compilerOptions[property];
Expand Down Expand Up @@ -119,6 +106,20 @@ const _parseTsconfig = (
}

const directoryPath = path.dirname(realTsconfigPath);

if (config.compilerOptions) {
const { compilerOptions } = config;
if (
compilerOptions.paths
&& !compilerOptions.baseUrl
) {
type WithImplicitBaseUrl = TsConfigJson.CompilerOptions & {
[implicitBaseUrlSymbol]: string;
};
(compilerOptions as WithImplicitBaseUrl)[implicitBaseUrlSymbol] = directoryPath;
}
}

if (config.extends) {
const extendsPathList = (
Array.isArray(config.extends)
Expand Down Expand Up @@ -157,7 +158,6 @@ const _parseTsconfig = (

if (config.compilerOptions) {
const { compilerOptions } = config;

const normalizedPaths = [
'baseUrl',
'rootDir',
Expand Down
9 changes: 7 additions & 2 deletions src/paths-matcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'path';
import slash from 'slash';
import type { TsConfigResult } from '../types.js';
import { isRelativePathPattern } from '../utils/is-relative-path-pattern.js';
import { implicitBaseUrlSymbol } from '../utils/symbols.js';
import {
assertStarCount,
parsePattern,
Expand All @@ -21,7 +22,7 @@ const parsePaths = (
substitutions: substitutions!.map((substitution) => {
assertStarCount(
substitution,
`Substitution '${substitution}' in pattern '${pattern}' can have at most one '*' character.`,
`Substitution '${substitution}' in pattern '${pattern}' can have at most one '*' character.`,
);

if (!baseUrl && !isRelativePathPattern.test(substitution)) {
Expand All @@ -45,13 +46,17 @@ export const createPathsMatcher = (
}

const { baseUrl, paths } = tsconfig.config.compilerOptions;
const implicitBaseUrl = (
implicitBaseUrlSymbol in tsconfig.config.compilerOptions
&& (tsconfig.config.compilerOptions[implicitBaseUrlSymbol] as string)
);
if (!baseUrl && !paths) {
return null;
}

const resolvedBaseUrl = path.resolve(
path.dirname(tsconfig.path),
baseUrl || '.',
baseUrl || implicitBaseUrl || '.',
);

const pathEntries = (
Expand Down
12 changes: 12 additions & 0 deletions src/utils/symbols.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* When a tsconfig extends another file with relative `paths` entries and the final tsconfig
* doesn't have a `baseUrl` set, the relative paths are resolved relative to the tsconfig that
* defined the `paths`
*
* However, this is impossible to compute from a flattened tsconfig, because we no longer know
* the path of the tsconfig that defined the `paths` entry.
*
* This is why we store the implicit baseUrl in the flattened tsconfig, so that the pathsMatcher
* can use it to resolve relative paths.
*/
export const implicitBaseUrlSymbol = Symbol('implicitBaseUrl');
144 changes: 122 additions & 22 deletions tests/specs/create-paths-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { getTsconfig, createPathsMatcher } from '#get-tsconfig';
export default testSuite(({ describe }) => {
describe('paths', ({ describe, test }) => {
describe('error cases', ({ test }) => {
test('no baseUrl or paths', async () => {
test('no baseUrl or paths should be fine', async () => {
const fixture = await createFixture({
'tsconfig.json': createTsconfigJson({
compilerOptions: {},
Expand All @@ -26,7 +26,7 @@ export default testSuite(({ describe }) => {
await fixture.rm();
});

test('no baseUrl & non-relative paths', async () => {
test('no baseUrl nor relative paths', async () => {
const fixture = await createFixture({
'tsconfig.json': createTsconfigJson({
compilerOptions: {
Expand Down Expand Up @@ -54,6 +54,40 @@ export default testSuite(({ describe }) => {
await fixture.rm();
});

test('no baseUrl nor relative paths in extends', async () => {
const fixture = await createFixture({
'some-dir2/tsconfig.json': createTsconfigJson({
compilerOptions: {
paths: {
'@': ['src'],
},
},
}),
'some-dir/tsconfig.json': createTsconfigJson({
extends: '../some-dir2/tsconfig.json',
}),
'tsconfig.json': createTsconfigJson({
extends: './some-dir/tsconfig.json',
}),
});

let throwsError = false;
const errorMessage = 'Non-relative paths are not allowed when \'baseUrl\' is not set. Did you forget a leading \'./\'?';
try {
await getTscResolution('@', fixture.path);
} catch (error) {
throwsError = true;
expect((error as any).stdout).toMatch(errorMessage);
}
expect(throwsError).toBe(true);

const tsconfig = getTsconfig(fixture.path);
expect(tsconfig).not.toBeNull();
expect(() => createPathsMatcher(tsconfig!)).toThrow(errorMessage);

await fixture.rm();
});

test('multiple * in pattern', async () => {
const fixture = await createFixture({
'tsconfig.json': createTsconfigJson({
Expand Down Expand Up @@ -472,36 +506,102 @@ export default testSuite(({ describe }) => {
await fixture.rm();
});

test('extended config should resolve relative to self', async () => {
const fixture = await createFixture({
tsconfigs: {
describe('extends w/ no baseUrl', ({ test }) => {
test('extended config should resolve relative to self', async () => {
const fixture = await createFixture({
tsconfigs: {
'tsconfig.json': createTsconfigJson({
compilerOptions: {
paths: {
'@': [
'./file',
],
},
},
}),
},
'tsconfig.json': createTsconfigJson({
extends: './tsconfigs/tsconfig.json',
}),
});

const tsconfig = getTsconfig(fixture.path);
expect(tsconfig).not.toBeNull();

const matcher = createPathsMatcher(tsconfig!)!;
expect(tsconfig).not.toBeNull();

const resolvedAttempts = await getTscResolution('@', fixture.path);
expect(matcher('@')).toStrictEqual([
resolvedAttempts[0].filePath.slice(0, -3),
]);

await fixture.rm();
});

test('extended config should implicitly resolve paths from self', async () => {
const fixture = await createFixture({
tsconfigs: {
'tsconfig.json': createTsconfigJson({
compilerOptions: {
paths: {
'@': [
'./file',
],
},
},
}),
},
'tsconfig.json': createTsconfigJson({
extends: './tsconfigs/tsconfig.json',
}),
});

const tsconfig = getTsconfig(fixture.path);
expect(tsconfig).not.toBeNull();

const matcher = createPathsMatcher(tsconfig!)!;
expect(tsconfig).not.toBeNull();

const resolvedAttempts = await getTscResolution('@', fixture.path);
expect(matcher('@')).toStrictEqual([
resolvedAttempts[0].filePath.slice(0, -3),
]);

await fixture.rm();
});

test('extended config should implicitly resolve paths from self - complex', async () => {
const fixture = await createFixture({
'file.ts': '',
'some-dir2/tsconfig.json': createTsconfigJson({
compilerOptions: {
paths: {
'#imports': [
'./imports',
],
'@': ['./a'],
},
},
}),
},
'tsconfig.json': createTsconfigJson({
extends: './tsconfigs/tsconfig.json',
}),
});
'some-dir/tsconfig.json': createTsconfigJson({
extends: '../some-dir2/tsconfig.json',
}),
'tsconfig.json': createTsconfigJson({
extends: './some-dir/tsconfig.json',
}),
});

const tsconfig = getTsconfig(fixture.path);
expect(tsconfig).not.toBeNull();
const tsconfig = getTsconfig(fixture.path);
expect(tsconfig).not.toBeNull();

const matcher = createPathsMatcher(tsconfig!)!;
expect(tsconfig).not.toBeNull();
const matcher = createPathsMatcher(tsconfig!)!;
expect(tsconfig).not.toBeNull();

const resolvedAttempts = await getTscResolution('#imports', fixture.path);
expect(matcher('#imports')).toStrictEqual([
resolvedAttempts[0].filePath.slice(0, -3),
]);
const resolvedAttempts = await getTscResolution('@', fixture.path);
expect(matcher('@')).toStrictEqual([
resolvedAttempts[0].filePath.slice(0, -3),
]);

await fixture.rm();
await fixture.rm();
});
});
});
});
25 changes: 25 additions & 0 deletions tests/specs/parse-tsconfig/extends/merges.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,31 @@ export default testSuite(({ describe }) => {

await fixture.rm();
});

test('resolves parent baseUrl & paths', async () => {
const fixture = await createFixture({
'project/tsconfig.json': createTsconfigJson({
compilerOptions: {
baseUrl: '.',
paths: {
'@/*': ['src/*'],
},
},
}),
'tsconfig.json': createTsconfigJson({
extends: './project/tsconfig.json',
}),
'a.ts': '',
});

const expectedTsconfig = await getTscTsconfig(fixture.path);
delete expectedTsconfig.files;

const tsconfig = parseTsconfig(path.join(fixture.path, 'tsconfig.json'));
expect(tsconfig).toStrictEqual(expectedTsconfig);

await fixture.rm();
});
});

test('nested extends', async () => {
Expand Down

0 comments on commit 4b5f839

Please sign in to comment.