zerkalica/zerollup

Fails to resolve with mulit-TS monorepos

Opened this issue · 0 comments

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);
    };
  };
}