Skip to content

Commit c373d76

Browse files
committed
feat: Rewrote resolution strategy + various improvements (see notes)
- Several improvements were made for speed and efficiency. - Now accommodating for new TS empty baseURL provision (closes #109) - Pre-checking necessity before overwriting paths (closes #110) - Rewrote core resolution methodology to: - Properly handle implicit indexes (closes #106) - Properly handle implicit sub-package indexes set via package.json 'main' #108) - Not follow symlinks (#107) - Resolve from output path as opposed to SourceFile path (#103)
1 parent 2699abd commit c373d76

File tree

22 files changed

+378
-141
lines changed

22 files changed

+378
-141
lines changed

src/transformer.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
import {} from "ts-expose-internals";
33
import path from "path";
44
import ts from "typescript";
5-
import { cast, getImplicitExtensions } from "./utils";
5+
import { cast } from "./utils";
66
import { TsTransformPathsConfig, TsTransformPathsContext, TypeScriptThree, VisitorContext } from "./types";
77
import { nodeVisitor } from "./visitor";
88
import { createHarmonyFactory } from "./utils/harmony-factory";
99
import { Minimatch } from "minimatch";
10+
import { createParsedCommandLineForProgram } from "./utils/ts-helpers";
1011

1112
/* ****************************************************************************************************************** *
1213
* Transformer
@@ -20,27 +21,31 @@ export default function transformer(
2021
if (!tsInstance) tsInstance = ts;
2122

2223
const compilerOptions = program.getCompilerOptions();
23-
const implicitExtensions = getImplicitExtensions(compilerOptions);
2424
const rootDirs = compilerOptions.rootDirs?.filter(path.isAbsolute);
2525

2626
return (transformationContext: ts.TransformationContext) => {
27+
const pathsBasePath = compilerOptions.pathsBasePath ?? compilerOptions.baseUrl;
28+
29+
if (!pathsBasePath || !compilerOptions.paths) return (sourceFile: ts.SourceFile) => sourceFile;
30+
2731
const tsTransformPathsContext: TsTransformPathsContext = {
2832
compilerOptions,
2933
config,
3034
elisionMap: new Map(),
3135
tsFactory: transformationContext.factory,
32-
implicitExtensions,
3336
program,
3437
rootDirs,
3538
transformationContext,
3639
tsInstance,
40+
pathsBasePath,
41+
getCanonicalFileName: tsInstance.createGetCanonicalFileName(tsInstance.sys.useCaseSensitiveFileNames),
3742
tsThreeInstance: cast<TypeScriptThree>(tsInstance),
3843
excludeMatchers: config.exclude?.map((globPattern) => new Minimatch(globPattern, { matchBase: true })),
44+
parsedCommandLine: createParsedCommandLineForProgram(tsInstance, program),
45+
outputFileNamesCache: new Map(),
3946
};
4047

4148
return (sourceFile: ts.SourceFile) => {
42-
if (!compilerOptions.baseUrl || !compilerOptions.paths) return sourceFile;
43-
4449
const visitorContext: VisitorContext = {
4550
...tsTransformPathsContext,
4651
sourceFile,

src/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import tsThree from "./declarations/typescript3";
2-
import ts, { CompilerOptions } from "typescript";
2+
import ts, { CompilerOptions, GetCanonicalFileName, ParsedCommandLine } from "typescript";
33
import { PluginConfig } from "ts-patch";
44
import { HarmonyFactory } from "./utils/harmony-factory";
55
import { IMinimatch } from "minimatch";
@@ -42,12 +42,15 @@ export interface TsTransformPathsContext {
4242
readonly tsFactory?: ts.NodeFactory;
4343
readonly program: ts.Program | tsThree.Program;
4444
readonly config: TsTransformPathsConfig;
45-
readonly implicitExtensions: readonly string[];
4645
readonly compilerOptions: CompilerOptions;
4746
readonly elisionMap: Map<ts.SourceFile, Map<ImportOrExportDeclaration, ImportOrExportDeclaration>>;
4847
readonly transformationContext: ts.TransformationContext;
4948
readonly rootDirs?: string[];
5049
readonly excludeMatchers: IMinimatch[] | undefined;
50+
readonly parsedCommandLine: ParsedCommandLine;
51+
readonly outputFileNamesCache: Map<string, string>;
52+
readonly pathsBasePath: string;
53+
readonly getCanonicalFileName: GetCanonicalFileName;
5154
}
5255

5356
export interface VisitorContext extends TsTransformPathsContext {

src/utils/general-utils.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import ts from "typescript";
21
import url from "url";
32
import path from "path";
43

@@ -7,21 +6,9 @@ import path from "path";
76
* ****************************************************************************************************************** */
87

98
export const isURL = (s: string): boolean => !!s && (!!url.parse(s).host || !!url.parse(s).hostname);
10-
export const isBaseDir = (base: string, dir: string) => path.relative(base, dir)?.[0] !== ".";
119
export const cast = <T>(v: any): T => v;
12-
13-
/**
14-
* @returns Array of implicit extensions, given CompilerOptions
15-
*/
16-
export function getImplicitExtensions(options: ts.CompilerOptions) {
17-
let res: string[] = [".ts", ".d.ts"];
18-
19-
let { allowJs, jsx } = options;
20-
const allowJsx = !!jsx && (<any>jsx !== ts.JsxEmit.None);
21-
22-
allowJs && res.push(".js", ".cjs", ".mjs");
23-
allowJsx && res.push(".tsx");
24-
allowJs && allowJsx && res.push(".jsx");
25-
26-
return res;
27-
}
10+
export const isBaseDir = (baseDir: string, testDir: string): boolean => {
11+
const relative = path.relative(baseDir, testDir);
12+
return relative ? !relative.startsWith("..") && !path.isAbsolute(relative) : true;
13+
};
14+
export const maybeAddRelativeLocalPrefix = (p: string) => (p[0] === "." ? p : `./${p}`);

src/utils/resolve-module-name.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { VisitorContext } from "../types";
2+
import { isBaseDir, isURL, maybeAddRelativeLocalPrefix } from "./general-utils";
3+
import * as path from "path";
4+
import { removeFileExtension, removeSuffix, ResolvedModuleFull } from "typescript";
5+
import { getOutputFile } from "./ts-helpers";
6+
7+
/* ****************************************************************************************************************** */
8+
// region: Types
9+
/* ****************************************************************************************************************** */
10+
11+
export interface ResolvedModule {
12+
/**
13+
* Absolute path to resolved module
14+
*/
15+
resolvedPath: string | undefined;
16+
/**
17+
* Output path
18+
*/
19+
outputPath: string;
20+
/**
21+
* Resolved to URL
22+
*/
23+
isURL: boolean;
24+
}
25+
26+
enum IndexType {
27+
NonIndex,
28+
Explicit,
29+
Implicit,
30+
ImplicitPackage,
31+
}
32+
33+
// endregion
34+
35+
/* ****************************************************************************************************************** */
36+
// region: Helpers
37+
/* ****************************************************************************************************************** */
38+
39+
function getPathDetail(moduleName: string, resolvedModule: ResolvedModuleFull) {
40+
let resolvedFileName = resolvedModule.originalPath ?? resolvedModule.resolvedFileName;
41+
const implicitPackageIndex = resolvedModule.packageId?.subModuleName;
42+
43+
const resolvedDir = implicitPackageIndex
44+
? removeSuffix(resolvedFileName, `/${implicitPackageIndex}`)
45+
: path.dirname(resolvedFileName);
46+
const resolvedBaseName = implicitPackageIndex ? void 0 : path.basename(resolvedFileName);
47+
const resolvedBaseNameNoExtension = resolvedBaseName && removeFileExtension(resolvedBaseName);
48+
const resolvedExtName = resolvedBaseName && path.extname(resolvedFileName);
49+
50+
let baseName = !implicitPackageIndex ? path.basename(moduleName) : void 0;
51+
let baseNameNoExtension = baseName && removeFileExtension(baseName);
52+
let extName = baseName && path.extname(moduleName);
53+
54+
// Account for possible false extensions. Example scenario:
55+
// moduleName = './file.accounting'
56+
// resolvedBaseName = 'file.accounting.ts'
57+
// ('accounting' would be considered the extension)
58+
if (resolvedBaseNameNoExtension && baseName && resolvedBaseNameNoExtension === baseName) {
59+
baseNameNoExtension = baseName;
60+
extName = void 0;
61+
}
62+
63+
// prettier-ignore
64+
const indexType =
65+
implicitPackageIndex ? IndexType.ImplicitPackage :
66+
baseNameNoExtension === 'index' && resolvedBaseNameNoExtension === 'index' ? IndexType.Explicit :
67+
baseNameNoExtension !== 'index' && resolvedBaseNameNoExtension === 'index' ? IndexType.Implicit :
68+
IndexType.NonIndex;
69+
70+
if (indexType === IndexType.Implicit) {
71+
baseName = void 0;
72+
baseNameNoExtension = void 0;
73+
extName = void 0;
74+
}
75+
76+
return {
77+
baseName,
78+
baseNameNoExtension,
79+
extName,
80+
resolvedBaseName,
81+
resolvedBaseNameNoExtension,
82+
resolvedExtName,
83+
resolvedDir,
84+
indexType,
85+
implicitPackageIndex,
86+
resolvedFileName,
87+
};
88+
}
89+
90+
// endregion
91+
92+
/* ****************************************************************************************************************** */
93+
// region: Utils
94+
/* ****************************************************************************************************************** */
95+
96+
/**
97+
* Resolve a module name
98+
*/
99+
export function resolveModuleName(context: VisitorContext, moduleName: string): ResolvedModule | undefined {
100+
const { tsInstance, compilerOptions, sourceFile, config, rootDirs } = context;
101+
102+
// Attempt to resolve with TS Compiler API
103+
const { resolvedModule, failedLookupLocations } = tsInstance.resolveModuleName(
104+
moduleName,
105+
sourceFile.fileName,
106+
compilerOptions,
107+
tsInstance.sys
108+
);
109+
110+
// Handle non-resolvable module
111+
if (!resolvedModule) {
112+
const maybeURL = failedLookupLocations[0];
113+
if (!isURL(maybeURL)) return void 0;
114+
return {
115+
isURL: true,
116+
resolvedPath: void 0,
117+
outputPath: maybeURL,
118+
};
119+
}
120+
121+
const {
122+
indexType,
123+
resolvedBaseNameNoExtension,
124+
resolvedFileName,
125+
implicitPackageIndex,
126+
extName,
127+
resolvedDir,
128+
} = getPathDetail(moduleName, resolvedModule);
129+
130+
/* Determine output filename */
131+
let outputBaseName = resolvedBaseNameNoExtension ?? "";
132+
133+
if (indexType === IndexType.Implicit) outputBaseName = outputBaseName.replace(/(\/index$)|(^index$)/, "");
134+
if (outputBaseName && extName) outputBaseName = `${outputBaseName}${extName}`;
135+
136+
/* Determine output dir */
137+
let srcFileOutputDir = path.dirname(getOutputFile(context, sourceFile.fileName));
138+
let moduleFileOutputDir = implicitPackageIndex ? resolvedDir : path.dirname(getOutputFile(context, resolvedFileName));
139+
140+
// Handle rootDirs remapping
141+
if (config.useRootDirs && rootDirs) {
142+
let fileRootDir = "";
143+
let moduleRootDir = "";
144+
for (const rootDir of rootDirs) {
145+
if (isBaseDir(rootDir, moduleFileOutputDir) && rootDir.length > moduleRootDir.length) moduleRootDir = rootDir;
146+
if (isBaseDir(rootDir, srcFileOutputDir) && rootDir.length > fileRootDir.length) fileRootDir = rootDir;
147+
}
148+
149+
/* Remove base dirs to make relative to root */
150+
if (fileRootDir && moduleRootDir) {
151+
srcFileOutputDir = path.relative(fileRootDir, srcFileOutputDir);
152+
moduleFileOutputDir = path.relative(moduleRootDir, moduleFileOutputDir);
153+
}
154+
}
155+
156+
const outputDir = path.relative(srcFileOutputDir, moduleFileOutputDir);
157+
158+
/* Compose final output path */
159+
const outputPath = maybeAddRelativeLocalPrefix(tsInstance.normalizePath(path.join(outputDir, outputBaseName)));
160+
161+
return { isURL: false, outputPath, resolvedPath: resolvedFileName };
162+
}
163+
164+
// endregion

0 commit comments

Comments
 (0)