Skip to content

Fails to resolve with mulit-TS monorepos #36

Description

@derolf

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions