Skip to content

Commit

Permalink
feat: Rewrote resolution strategy + various improvements (see notes)
Browse files Browse the repository at this point in the history
- 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)
  • Loading branch information
nonara authored and Ron S committed Jun 16, 2021
1 parent 5000930 commit ed1df79
Show file tree
Hide file tree
Showing 22 changed files with 378 additions and 141 deletions.
15 changes: 10 additions & 5 deletions src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import {} from "ts-expose-internals";
import path from "path";
import ts from "typescript";
import { cast, getImplicitExtensions } from "./utils";
import { cast } from "./utils";
import { TsTransformPathsConfig, TsTransformPathsContext, TypeScriptThree, VisitorContext } from "./types";
import { nodeVisitor } from "./visitor";
import { createHarmonyFactory } from "./utils/harmony-factory";
import { Minimatch } from "minimatch";
import { createParsedCommandLineForProgram } from "./utils/ts-helpers";

/* ****************************************************************************************************************** *
* Transformer
Expand All @@ -20,27 +21,31 @@ export default function transformer(
if (!tsInstance) tsInstance = ts;

const compilerOptions = program.getCompilerOptions();
const implicitExtensions = getImplicitExtensions(compilerOptions);
const rootDirs = compilerOptions.rootDirs?.filter(path.isAbsolute);

return (transformationContext: ts.TransformationContext) => {
const pathsBasePath = compilerOptions.pathsBasePath ?? compilerOptions.baseUrl;

if (!pathsBasePath || !compilerOptions.paths) return (sourceFile: ts.SourceFile) => sourceFile;

const tsTransformPathsContext: TsTransformPathsContext = {
compilerOptions,
config,
elisionMap: new Map(),
tsFactory: transformationContext.factory,
implicitExtensions,
program,
rootDirs,
transformationContext,
tsInstance,
pathsBasePath,
getCanonicalFileName: tsInstance.createGetCanonicalFileName(tsInstance.sys.useCaseSensitiveFileNames),
tsThreeInstance: cast<TypeScriptThree>(tsInstance),
excludeMatchers: config.exclude?.map((globPattern) => new Minimatch(globPattern, { matchBase: true })),
parsedCommandLine: createParsedCommandLineForProgram(tsInstance, program),
outputFileNamesCache: new Map(),
};

return (sourceFile: ts.SourceFile) => {
if (!compilerOptions.baseUrl || !compilerOptions.paths) return sourceFile;

const visitorContext: VisitorContext = {
...tsTransformPathsContext,
sourceFile,
Expand Down
7 changes: 5 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import tsThree from "./declarations/typescript3";
import ts, { CompilerOptions } from "typescript";
import ts, { CompilerOptions, GetCanonicalFileName, ParsedCommandLine } from "typescript";
import { PluginConfig } from "ts-patch";
import { HarmonyFactory } from "./utils/harmony-factory";
import { IMinimatch } from "minimatch";
Expand Down Expand Up @@ -42,12 +42,15 @@ export interface TsTransformPathsContext {
readonly tsFactory?: ts.NodeFactory;
readonly program: ts.Program | tsThree.Program;
readonly config: TsTransformPathsConfig;
readonly implicitExtensions: readonly string[];
readonly compilerOptions: CompilerOptions;
readonly elisionMap: Map<ts.SourceFile, Map<ImportOrExportDeclaration, ImportOrExportDeclaration>>;
readonly transformationContext: ts.TransformationContext;
readonly rootDirs?: string[];
readonly excludeMatchers: IMinimatch[] | undefined;
readonly parsedCommandLine: ParsedCommandLine;
readonly outputFileNamesCache: Map<string, string>;
readonly pathsBasePath: string;
readonly getCanonicalFileName: GetCanonicalFileName;
}

export interface VisitorContext extends TsTransformPathsContext {
Expand Down
23 changes: 5 additions & 18 deletions src/utils/general-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import ts from "typescript";
import url from "url";
import path from "path";

Expand All @@ -7,21 +6,9 @@ import path from "path";
* ****************************************************************************************************************** */

export const isURL = (s: string): boolean => !!s && (!!url.parse(s).host || !!url.parse(s).hostname);
export const isBaseDir = (base: string, dir: string) => path.relative(base, dir)?.[0] !== ".";
export const cast = <T>(v: any): T => v;

/**
* @returns Array of implicit extensions, given CompilerOptions
*/
export function getImplicitExtensions(options: ts.CompilerOptions) {
let res: string[] = [".ts", ".d.ts"];

let { allowJs, jsx } = options;
const allowJsx = !!jsx && (<any>jsx !== ts.JsxEmit.None);

allowJs && res.push(".js", ".cjs", ".mjs");
allowJsx && res.push(".tsx");
allowJs && allowJsx && res.push(".jsx");

return res;
}
export const isBaseDir = (baseDir: string, testDir: string): boolean => {
const relative = path.relative(baseDir, testDir);
return relative ? !relative.startsWith("..") && !path.isAbsolute(relative) : true;
};
export const maybeAddRelativeLocalPrefix = (p: string) => (p[0] === "." ? p : `./${p}`);
164 changes: 164 additions & 0 deletions src/utils/resolve-module-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { VisitorContext } from "../types";
import { isBaseDir, isURL, maybeAddRelativeLocalPrefix } from "./general-utils";
import * as path from "path";
import { removeFileExtension, removeSuffix, ResolvedModuleFull } from "typescript";
import { getOutputFile } from "./ts-helpers";

/* ****************************************************************************************************************** */
// region: Types
/* ****************************************************************************************************************** */

export interface ResolvedModule {
/**
* Absolute path to resolved module
*/
resolvedPath: string | undefined;
/**
* Output path
*/
outputPath: string;
/**
* Resolved to URL
*/
isURL: boolean;
}

enum IndexType {
NonIndex,
Explicit,
Implicit,
ImplicitPackage,
}

// endregion

/* ****************************************************************************************************************** */
// region: Helpers
/* ****************************************************************************************************************** */

function getPathDetail(moduleName: string, resolvedModule: ResolvedModuleFull) {
let resolvedFileName = resolvedModule.originalPath ?? resolvedModule.resolvedFileName;
const implicitPackageIndex = resolvedModule.packageId?.subModuleName;

const resolvedDir = implicitPackageIndex
? removeSuffix(resolvedFileName, `/${implicitPackageIndex}`)
: path.dirname(resolvedFileName);
const resolvedBaseName = implicitPackageIndex ? void 0 : path.basename(resolvedFileName);
const resolvedBaseNameNoExtension = resolvedBaseName && removeFileExtension(resolvedBaseName);
const resolvedExtName = resolvedBaseName && path.extname(resolvedFileName);

let baseName = !implicitPackageIndex ? path.basename(moduleName) : void 0;
let baseNameNoExtension = baseName && removeFileExtension(baseName);
let extName = baseName && path.extname(moduleName);

// Account for possible false extensions. Example scenario:
// moduleName = './file.accounting'
// resolvedBaseName = 'file.accounting.ts'
// ('accounting' would be considered the extension)
if (resolvedBaseNameNoExtension && baseName && resolvedBaseNameNoExtension === baseName) {
baseNameNoExtension = baseName;
extName = void 0;
}

// prettier-ignore
const indexType =
implicitPackageIndex ? IndexType.ImplicitPackage :
baseNameNoExtension === 'index' && resolvedBaseNameNoExtension === 'index' ? IndexType.Explicit :
baseNameNoExtension !== 'index' && resolvedBaseNameNoExtension === 'index' ? IndexType.Implicit :
IndexType.NonIndex;

if (indexType === IndexType.Implicit) {
baseName = void 0;
baseNameNoExtension = void 0;
extName = void 0;
}

return {
baseName,
baseNameNoExtension,
extName,
resolvedBaseName,
resolvedBaseNameNoExtension,
resolvedExtName,
resolvedDir,
indexType,
implicitPackageIndex,
resolvedFileName,
};
}

// endregion

/* ****************************************************************************************************************** */
// region: Utils
/* ****************************************************************************************************************** */

/**
* Resolve a module name
*/
export function resolveModuleName(context: VisitorContext, moduleName: string): ResolvedModule | undefined {
const { tsInstance, compilerOptions, sourceFile, config, rootDirs } = context;

// Attempt to resolve with TS Compiler API
const { resolvedModule, failedLookupLocations } = tsInstance.resolveModuleName(
moduleName,
sourceFile.fileName,
compilerOptions,
tsInstance.sys
);

// Handle non-resolvable module
if (!resolvedModule) {
const maybeURL = failedLookupLocations[0];
if (!isURL(maybeURL)) return void 0;
return {
isURL: true,
resolvedPath: void 0,
outputPath: maybeURL,
};
}

const {
indexType,
resolvedBaseNameNoExtension,
resolvedFileName,
implicitPackageIndex,
extName,
resolvedDir,
} = getPathDetail(moduleName, resolvedModule);

/* Determine output filename */
let outputBaseName = resolvedBaseNameNoExtension ?? "";

if (indexType === IndexType.Implicit) outputBaseName = outputBaseName.replace(/(\/index$)|(^index$)/, "");
if (outputBaseName && extName) outputBaseName = `${outputBaseName}${extName}`;

/* Determine output dir */
let srcFileOutputDir = path.dirname(getOutputFile(context, sourceFile.fileName));
let moduleFileOutputDir = implicitPackageIndex ? resolvedDir : path.dirname(getOutputFile(context, resolvedFileName));

// Handle rootDirs remapping
if (config.useRootDirs && rootDirs) {
let fileRootDir = "";
let moduleRootDir = "";
for (const rootDir of rootDirs) {
if (isBaseDir(rootDir, moduleFileOutputDir) && rootDir.length > moduleRootDir.length) moduleRootDir = rootDir;
if (isBaseDir(rootDir, srcFileOutputDir) && rootDir.length > fileRootDir.length) fileRootDir = rootDir;
}

/* Remove base dirs to make relative to root */
if (fileRootDir && moduleRootDir) {
srcFileOutputDir = path.relative(fileRootDir, srcFileOutputDir);
moduleFileOutputDir = path.relative(moduleRootDir, moduleFileOutputDir);
}
}

const outputDir = path.relative(srcFileOutputDir, moduleFileOutputDir);

/* Compose final output path */
const outputPath = maybeAddRelativeLocalPrefix(tsInstance.normalizePath(path.join(outputDir, outputBaseName)));

return { isURL: false, outputPath, resolvedPath: resolvedFileName };
}

// endregion
Loading

0 comments on commit ed1df79

Please sign in to comment.