Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fails to resolve with mulit-TS monorepos #36

Open
derolf opened this issue Sep 8, 2021 · 0 comments
Open

Fails to resolve with mulit-TS monorepos #36

derolf opened this issue Sep 8, 2021 · 0 comments

Comments

@derolf
Copy link

derolf commented Sep 8, 2021

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);
    };
  };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant