Fails to resolve with mulit-TS monorepos
Opened this issue · 0 comments
derolf commented
We have a monorepo with multiple TypeScript packages. It breaks in the following scenario:
Package A
with module A/foo.ts
: import "B/bar"
Package B
with module B/bar.ts
: import "mappedPath/baz"
Run ts-node A/foo.ts
.
When B/bar.ts
is compiled, the TransformationContext
has CompilerOptions
from A
, but NOT from B
. So all the mappings from B/tsconfig.json
are ignored.
Here's a workaround version that "solves" the issues for me. I am basically loading the nearest tsConfig.json
and then using your ImportPathsResolver
and some other coped stuff to make it work.
// heavily inspired by https://github.com/zerkalica/zerollup/tree/master/packages/ts-transform-paths
//
// +: works for multi-package typescript projects
// -: only rewrites "import" declarations
import { existsSync, readFileSync } from "fs";
import {
ModuleResolutionHost,
Program,
SourceFile,
TransformerFactory,
TransformationContext,
findConfigFile,
Node,
visitEachChild,
isImportDeclaration,
CompilerOptions,
isStringLiteral,
} from "typescript";
import { ImportPathsResolver } from "@zerollup/ts-helpers";
import { dirname, join, resolve } from "path";
const tsParts = [".ts", ".d.ts", ".tsx", "/index.ts", "/index.tsx", "/index.d.ts", ""];
declare module "typescript" {
interface TransformationContext {
getEmitHost?(): ModuleResolutionHost;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Program extends ModuleResolutionHost {}
}
const resolvers: Record<string, ReturnType<typeof createResolver>> = {};
function createResolver(tsConfigFilename: string) {
try {
const compilerOptions = JSON.parse(readFileSync(tsConfigFilename, "utf8")).compilerOptions as CompilerOptions;
compilerOptions.baseUrl = resolve(dirname(tsConfigFilename), compilerOptions.baseUrl ?? "");
return new ImportPathsResolver(compilerOptions);
} catch {
throw new Error("Invalid tsconfig.json " + tsConfigFilename);
}
}
export default function (program: Program): TransformerFactory<SourceFile> {
return (transformationContext: TransformationContext) => {
const emitHost = transformationContext.getEmitHost ? transformationContext.getEmitHost() : undefined;
function fileExists(file: string) {
if (program?.fileExists) return program.fileExists(file);
if (emitHost?.fileExists) return emitHost.fileExists(file);
throw "no fileExists";
}
return (sourceFile: SourceFile) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const tsConfigFilename = findConfigFile(sourceFile.fileName, existsSync)!;
const resolver = (resolvers[tsConfigFilename] ??= createResolver(tsConfigFilename));
function resolveImport(oldImport: string, currentDir: string): string | undefined {
const newImports = resolver.getImportSuggestions(oldImport, currentDir);
if (!newImports) return;
for (const newImport of newImports) {
const newImportPath = join(currentDir, newImport);
for (const part of tsParts) {
if (fileExists(`${newImportPath}${part}`)) return newImport;
}
}
}
function visitor(node: Node): Node {
if (isImportDeclaration(node) && isStringLiteral(node.moduleSpecifier)) {
const module = node.moduleSpecifier.text;
const newImport = resolveImport(module, dirname(sourceFile.fileName));
if (newImport) {
const newSpec = transformationContext.factory.createStringLiteral(newImport);
node = transformationContext.factory.updateImportDeclaration(node, node.decorators, node.modifiers, node.importClause, newSpec);
}
}
return visitEachChild(node, visitor, transformationContext);
}
return visitEachChild(sourceFile, visitor, transformationContext);
};
};
}