Skip to content

Commit

Permalink
feat(paths): createPathsMatcher (#5)
Browse files Browse the repository at this point in the history
privatenumber authored Jun 10, 2022
1 parent 48c2df8 commit b27dd16
Showing 12 changed files with 424 additions and 13 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -56,7 +56,7 @@ type TsconfigResult = {
* The resolved tsconfig.json file
*/
config: TsConfigJsonResolved
} | null
}
```
#### searchPath
@@ -73,6 +73,24 @@ Default: `tsconfig.json`
The file name of the TypeScript config file.
### createPathsMatcher(tsconfig: TsconfigResult)
Given a tsconfig with [`compilerOptions.paths`](https://www.typescriptlang.org/tsconfig#paths) defined, it returns a matcher function.
```ts
import getTsconfig from 'get-tsconfig'
import { createPathsMatcher } from 'get-tsconfig/paths'

const tsconfig = getTsconfig()
const pathsMatcher = createPathsMatcher(tsconfig)
```

The matcher function accepts an [import specifier (the path to resolve)](https://nodejs.org/api/esm.html#terminology), checks it against `compilerOptions.paths`, and returns an array of possible paths to check:
```ts
function pathsMatcher(specifier: string): string[]
```

This function only returns possible paths and doesn't actually do any resolution. This helps increase compatibility wtih file/build systems which usually have their own resolvers.

## FAQ

13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -23,9 +23,16 @@
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
"./paths": {
"require": "./dist/paths/index.js",
"import": "./dist/paths/index.mjs",
"types": "./dist/paths/index.d.ts"
}
},
"scripts": {
"lint": "eslint .",
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import type { TsConfigResult } from './types';
function getTsconfig(
searchPath = process.cwd(),
configName = 'tsconfig.json',
): TsConfigResult {
): TsConfigResult | null {
const configFile = findConfigFile(searchPath, configName);

if (!configFile) {
107 changes: 107 additions & 0 deletions src/paths/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import path from 'path';
import type { TsConfigResult } from '../types';
import {
assertStarCount,
isRelativePathPattern,
parsePattern,
isPatternMatch,
} from './utils';
import type { StarPattern, PathEntry } from './types';

function parsePaths(
paths: Partial<Record<string, string[]>>,
baseUrl: string | undefined,
absoluteBaseUrl: string,
) {
return Object.entries(paths).map(([pattern, substitutions]) => {
assertStarCount(pattern, `Pattern '${pattern}' can have at most one '*' character.`);

return {
pattern: parsePattern(pattern),
substitutions: substitutions!.map((substitution) => {
assertStarCount(
substitution,
`Substitution '${substitution}' in pattern '${pattern}' can have at most one '*' character.`,
);

if (!substitution.startsWith('./') && !baseUrl) {
throw new Error('Non-relative paths are not allowed when \'baseUrl\' is not set. Did you forget a leading \'./\'?');
}

return path.join(absoluteBaseUrl, substitution);
}),
} as PathEntry<string | StarPattern>;
});
}

/**
* Reference:
* https://github.com/microsoft/TypeScript/blob/3ccbe804f850f40d228d3c875be952d94d39aa1d/src/compiler/moduleNameResolver.ts#L2465
*/
export function createPathsMatcher(
tsconfig: TsConfigResult,
) {
if (!tsconfig.config.compilerOptions) {
return null;
}

const { baseUrl, paths } = tsconfig.config.compilerOptions;
if (!baseUrl && !paths) {
return null;
}

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

const pathEntries = paths ? parsePaths(paths, baseUrl, resolvedBaseUrl) : [];

return function pathsMatcher(specifier: string) {
if (isRelativePathPattern.test(specifier)) {
return [];
}

const patternPathEntries: PathEntry<StarPattern>[] = [];

for (const pathEntry of pathEntries) {
if (pathEntry.pattern === specifier) {
return pathEntry.substitutions;
}

if (typeof pathEntry.pattern !== 'string') {
patternPathEntries.push(pathEntry as PathEntry<StarPattern>);
}
}

let matchedValue: PathEntry<StarPattern> | undefined;
let longestMatchPrefixLength = -1;

for (const pathEntry of patternPathEntries) {
if (
isPatternMatch(pathEntry.pattern, specifier)
&& pathEntry.pattern.prefix.length > longestMatchPrefixLength
) {
longestMatchPrefixLength = pathEntry.pattern.prefix.length;
matchedValue = pathEntry;
}
}

if (!matchedValue) {
return (
baseUrl
? [path.join(resolvedBaseUrl, specifier)]
: []
);
}

const matchedPath = specifier.slice(
matchedValue.pattern.prefix.length,
specifier.length - matchedValue.pattern.suffix.length,
);

return matchedValue.substitutions.map(
substitution => substitution.replace('*', matchedPath),
);
};
}
9 changes: 9 additions & 0 deletions src/paths/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type StarPattern = {
prefix: string;
suffix: string;
};

export type PathEntry<T extends string | StarPattern> = {
pattern: T;
substitutions: string[];
};
32 changes: 32 additions & 0 deletions src/paths/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { StarPattern } from './types';

export const isRelativePathPattern = /^\.{1,2}\//;

const starPattern = /\*/g;

export const assertStarCount = (
pattern: string,
errorMessage: string,
) => {
const starCount = pattern.match(starPattern);
if (starCount && starCount.length > 1) {
throw new Error(errorMessage);
}
};

export function parsePattern(pattern: string) {
if (pattern.includes('*')) {
const [prefix, suffix] = pattern.split('*');
return { prefix, suffix } as StarPattern;
}

return pattern;
}

export const isPatternMatch = (
{ prefix, suffix }: StarPattern,
candidate: string,
) => (
candidate.startsWith(prefix)
&& candidate.endsWith(suffix)
);
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -14,4 +14,4 @@ export type TsConfigResult = {
* The resolved tsconfig.json file
*/
config: TsConfigJsonResolved;
} | null;
};
10 changes: 4 additions & 6 deletions tests/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { describe } from 'manten';
import specErrors from './specs/errors.spec';
import specFindsConfig from './specs/finds-config.spec';
import specExtends from './specs/extends.spec';

describe('get-tsconfig', ({ runTestSuite }) => {
runTestSuite(specErrors);
runTestSuite(specFindsConfig);
runTestSuite(specExtends);
runTestSuite(import('./specs/errors.spec'));
runTestSuite(import('./specs/finds-config.spec'));
runTestSuite(import('./specs/extends.spec'));
runTestSuite(import('./specs/paths.spec'));
});
1 change: 0 additions & 1 deletion tests/specs/extends.spec.ts
Original file line number Diff line number Diff line change
@@ -314,7 +314,6 @@ export default testSuite(({ describe }) => {
delete expectedTsconfig.files;

const tsconfig = getTsconfig(fixture.path);

expect(tsconfig!.config).toStrictEqual(expectedTsconfig);

await fixture.cleanup();
1 change: 1 addition & 0 deletions tests/specs/finds-config.spec.ts
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ export default testSuite(({ describe }) => {
compilerOptions: {
moduleResolution: 'node',
isolatedModules: true,
module: 'NodeNext',
esModuleInterop: true,
declaration: true,
outDir: 'dist',
Loading

0 comments on commit b27dd16

Please sign in to comment.