Skip to content

Offer option to resolve module path maps in emitted code #26722

Closed

Description

Search Terms

paths
tsconfig.json
compilerOptions
ts-node
node
cannot find module

Suggestion

Per #10866, module path maps are not resolved in emitted code. This means that if you attempt to run the emitted code with node, it will be unable to resolve those paths and the server will not start.

Current solutions:

  1. use browserify or webpack to bundle the output -- this is an unfortunate solution given that it'd be the only reason for me to bring in one of these tools, as I've managed to build and deploy apps using just the typescript compiler. This blog post from the TypeScript team even strongly recommends using a TypeScript toolchain:

https://blogs.msdn.microsoft.com/typescript/2018/08/27/typescript-and-babel-7/

For that reason, we feel tsc and the tools around the compiler pipeline will still give the most integrated and consistent experience for most projects.

  1. use a tool like module-alias. This is a bit gross as it requires duplicating the path maps, and inserting code at the root of your server.

Proposed solution:

Offer a compilerOption, something like resolvePaths, that resolves aliased paths to their relative paths.

Use Cases

So that I can continue to meaningfully use the paths property in tsconfig.json.

Examples

Please see the example in #10866.

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)

Activity

  1. DanielRosenwasser commented on Aug 28, 2018

    @DanielRosenwasser
    Member

    I think you want #16577.

  2. DanielRosenwasser commented on Aug 28, 2018

    @DanielRosenwasser
    Member

    Just to elaborate a bit here, we haven't moved on that issue given that there are so many moving parts in the modules space that anything we do would be irrelevant or deprecated in 2 years.

    For ts-node, can you use tsconfig-paths, as per this documentation?

  3. dannycochran commented on Aug 29, 2018

    @dannycochran
    Author

    Hey Daniel, thanks for the quick response.

    I don't think this is a duplicate of #16577 -- this issue deals with aliases not being resolved to their relative paths on build. I should have been clearer in the description rather than linking to an old bug.

    E.g, given this directory structure:

    tsconfig.json
    common
      - helper.ts
    server
      - index.ts
    

    Where tsconfig.json maps common to @common, like this:

    compilerOptions: {
      ...
      "paths": {
        "@common/*": [
          "./common/*"
        ]
      }
    }

    And this code in server/index.ts:

    # server/index.ts
    import { foo } from '@common/helper';

    When TSC outputs this, it keeps the alias rather than resolve it to the relative path. And node doesn't know how to resolve this.

    Unfortunately, I don't think you can use tsconfig-paths when running node against JavaScript files. I use tsconfig-paths to run my server in development, e.g;

    node -r ts-node/register -r tsconfig-paths server/index.ts

    But this does not work:

    node -r tsconfig-paths server-build/index.js

    My request is that the TypeScript compiler offer an option to resolve the aliases to relative paths so that node can properly run the outputted code. Otherwise, the "paths" feature in tsconfig.json is somewhat unhelpful, as it requires bringing in additional tools to reconcile the output.

  4. deejayy commented on Sep 13, 2018

    @deejayy

    Check out this gist:
    https://gist.github.com/deejayy/aa5f0cde76dc29f6b4127e60e74be2f2
    It resolves the paths from tsconfig.json. Dirty hack, but works.

  5. icopp commented on Sep 13, 2018

    @icopp

    A simple use case that tsconfig-paths doesn't really help with:

    • I have package @companyName/some-shared-frontend-stuff that's written in Typescript
    • I want to use path aliases internally in @companyName/some-shared-frontend-stuff
    • I want to use that package in @companyName/site-number-1 that's written in JS with Webpack compiling
    • I don't want @companyName/site-number-1 to know or care about the internals of @companyName/some-shared-frontend-stuff and just use plain imports (e.g. import { WhateverComponent } from '...')

    Right now, the only way to do this is to have a Babel compile step happen with @companyName/some-shared-frontend-stuff, which complicates dev work pretty painfully because I can't just use tsc -b --watch with npm link and instead I have to have a separate file watcher that re-does Babel builds when things change.

  6. dannycochran commented on Sep 13, 2018

    @dannycochran
    Author

    Daniel Rosenwasser (@DanielRosenwasser) provided my previous comment makes sense, can we remove the duplicate flag or discuss further?

  7. dannycochran commented on Sep 28, 2018

    @dannycochran
    Author

    Daniel Rosenwasser (@DanielRosenwasser) pinging again. Any chance this could go on the roadmap for 3.2?

  8. joonhocho commented on Oct 8, 2018

    @joonhocho

    Until officially supported, I've created a solution that does not require any dependencies nor babel. It also fixes .d.ts files (currently only if they are in the same directory as output. declarationDir support can be added too. PRs are welcomed!).
    https://github.com/joonhocho/tscpaths

  9. dannycochran commented on Nov 8, 2018

    @dannycochran
    Author

    Joon Ho Cho (@joonhocho) there are existing third party solutions, the idea is to avoid them entirely.

    Daniel Rosenwasser (@DanielRosenwasser) pinging again -- this is still marked as a duplicate and it shouldn't be. can we re-evaluate?

  10. alexandertrefz commented on Nov 30, 2018

    @alexandertrefz

    Daniel Rosenwasser (@DanielRosenwasser) please reevaluate this issue. The proposed solution is simple, and has nothing to do with the reasons you mentioned for rejection - this doesn’t align with any 3rd party tool, it just makes the TS internal paths option work like the vast majority of people expect. Please consider this.

    Every week an issue is opened about this, and quite a few have very long and very strong comment sections that all echo the same 2 things:

    1. This is incredibly unintuitive which wastes a ton of time and turns users away from the language because they get a "successful" compile that yields code that can't be run

    2. The only thing people want is a simple string replace at compile time that turns the paths path back into normal paths. That this is achievable is shown by tools like rollup - which is doing JUST THAT, without any confusion or problems - it just works. Like it should out of the box in tsc

  11. dannycochran commented on Dec 1, 2018

    @dannycochran
    Author

    Andy (Andrewkraft) (@Andy-MS) could I get your eyes on this issue? I think Daniel errantly resolved this as a duplicate a while ago, but per my explanation above (#26722 (comment)), it's not.

    I think given the reactions to this FR and the original issue (#10866), it would be a well received addition to TS.

  12. dannycochran commented on Dec 4, 2018

    @dannycochran
    Author

    Andy (Andrewkraft) (@Andy-MS) ah my mistake I read through the issue more and understand how this is a dupe. Thanks for taking a look.

  13. 62 remaining items

  14. t83714 commented on Nov 30, 2019

    @t83714

    Here are some questions I have about this feature:

    • What happens when an import resolves to a .d.ts file? Should these paths be rewritten too?

      • Should it be possible that importing a/b and a/c would write a/b but not a/c as a result?
    • Does this apply to all imports that start with a non-relative name, or just those where path mapping was invoked?

      • Does a path mapping that only changes the case of a path qualify under the above?
    • What if I only want to rewrite some of my paths?

    • Should this treat symlinks as opaque, or transparent?

    • Does this apply when an import resolves to an ambiently-declared module, but that import path also has an applicable path mapping?

      • If "yes", this means adding a .d.ts file to your compilation could break the runtime output of your program. This is unprecedented.
      • If "no", then how does a user predict whether a given import will be rewritten?
    • Should paths in emitted .d.ts files get rewritten? Why or why not?

      • What about reference directives?
    • What happens if an import resolves to a .ts file in node_modules? Should TS write out ../../node_modules/foo/index.ts ?

      • If not, by what logic is this excluded?
    • What happens under --allowJs when an import resolves to a .js file?

    Again though, I still consider this a non-starter feature, because the statement

    import a from "b";

    is an ECMAScript statement, and per design goal 7, when it doesn't need to be downleveled, we do not change it. This is no different from saying TS should cause a runtime error when a divide-by-zero occurs: We don't change the semantics of JS code.

    Ryan Cavanaugh (@RyanCavanaugh) I think what people ask for could be a rather simple solution and has nothing to do with the existing compile-time module resolve process. I think what people want is:

    • Only apply this feature to the current compile result (i.e. after all existing compile logic is complete and just before emitting the result)
    • Works as the simple string replacement without involving any complicated path resolution processes.
      e.g. for the config below:
    {
      "compilerOptions": {
        "pathAlias": {
          "a/b/c" : "../x/y/z",
          "@e/d/f/": "../../e/d/f",
          "../../u/v/w/": "../dist/"
        }
      }
    }

    The compliler should complete all existing compilation logic and do the followings before emitting the result:

    • check if any imported module names in the compile result meet the following criteria:
      • fully match or start with the string "a/b/c/" or fully match string "a/b/c"
        • If so, replace the string a/b/c in the module name with "../x/y/z" in the compile result that is about to emit (without validating or resolve the path ../x/y/z).
        • e.g. require("a/b/c/moduleA") becomes require("../x/y/z/moduleA")
        • require("a/b/c") becomes require("../x/y/z")
        • import xxx from "a/b/c/moduleA"; becomes import xxx from "../x/y/z/moduleA";
      • fully match or start with the string "@e/d/f/" => replace with "../../e/d/f"
        • e.g. require("@e/d/f/moduleB") becomes require("../../e/d/f/moduleB")
        • import xxx from "@e/d/f/moduleB"; becomes import xxx from "../../e/d/f/moduleB";
      • fully match or start with the string "../../u/v/w/" => replace with "../dist/"
        • e.g. require("../../u/v/w/moduleC") becomes require("../dist/moduleC")
        • import xxx from "../../u/v/w/moduleC"; becomes import xxx from "../dist/moduleC";

    Moreover:

    • this module name rewritten logic should not only apply to .js but also .d.ts files that are about to emit.
    • this feature should work independently. i.e. whether or not existing path mapping feature or project references feature is on or off doesn't impact the on/off of this feature as this feature works as the last step of the compilation process just before emitting.
    • this feature should apply to any /// <reference types="xxx/xxx/xx" /> as well

    I think the simple logic above should avoid most of the issues you mentioned in your post as this module name rewritten logic is triggered by string match and processed by string replacement without any module resolution process involved.

  15. t83714 commented on Nov 30, 2019

    @t83714

    It also achieves what we ask for using babel & babel-plugin-module-resolver (just in case anyone looks for alternative solution):

    • install @babel/cli & babel-plugin-module-resolver
      • yarn add --dev @babel/cli babel-plugin-module-resolver
    • Create .babelrc file at the project root with the following content:
    {
        "compact": false,
        "retainLines": true,
        "minified": false,
        "inputSourceMap": false,
        "sourceMaps": false,
        "plugins": [
            [
                "module-resolver",
                {
                    "root": ["./dist"],
                    "alias": {
                        "abc/src": "@a/bc/dist"
                    }
                }
            ]
        ]
    }
    • Build your project by: yarn tsc -b && babel dist -d dist
      • This assumes you output all build files to dist directory.
      • It will, firstly, compile your project using typescript compiler tsc
      • Then, pass all compile result in dist directory through babel and replace the module name and output back to the same dist directory

    Unfortunately, babel won't touch *.d.ts files in the dist directory.

    It will be good that this feature can be implemented by tsc or make it possible to create a plugin of tsc to do so.

  16. tconroy commented on Jan 7, 2020

    @tconroy

    Curious if anyone has found a solution for rewriting import alias in *.d.ts files? We're using babel to build TS to JS which resolves the alias correctly, but it's broken in *.d.ts.

  17. wintercounter commented on Jan 7, 2020

    @wintercounter

    Yes, https://www.npmjs.com/package/tscpaths
    UPDATE: Sorry, linked wrong package first.

  18. tconroy commented on Jan 8, 2020

    @tconroy

    Viktor Vincze (@wintercounter) Hm. Doesn't seem to work if your tsconfig.json extends from another config. In my project, we extend from a dependency config and tscpaths blows up.

  19. wintercounter commented on Jan 8, 2020

    @wintercounter
  20. shlomiassaf commented on Jan 16, 2020

    @shlomiassaf

    After reading a heavy portion of this thread I can say that everyone is right, from their perspective.

    What I (again. I) think the problem in this discussion is?

    TS users come from a working mode of, hey, i'm running my node app with TS in development (ts-node/webpack/whatever) and it works!!

    Now, when I build it does not!

    While (I guess) TS authors look at it differently, TS is just a platform to take on form of source code and output another form of source code and having it run as intended.

    This came to bloom mainly because of the gaining popularity of monorepos and how easy it is to build libraries that way, alongside a working application.

    Now, there are 2 scenarios:

    1. The app and libraries are both compiled to a DIST folder and run from it
    2. The app is compiled to a DIST folder, the libraries are compiled but published to a package repository (e.g NPM)

    With (1), users expect TS to take those paths mappings and convert them to relative paths so they will work cause they are not in node_modules....

    With (2) user's will not expect that to happen because it will cause the imports to point to nothing.

    I understand TS's team take on this, it is something that will work with (1) but not (2) and that's enough to rule it out.

    Maybe i'm wrong, but it seems to me it's one of the reasons for the mixup

  21. eyedean commented on Jan 21, 2020

    @eyedean

    After reading a heavy portion of this thread I can say that everyone is right, from their perspective.

    What I (again. I) think the problem in this discussion is?

    TS users come from a working mode of, hey, i'm running my node app with TS in development (ts-node/webpack/whatever) and it works!!

    Now, when I build it does not!

    While (I guess) TS authors look at it differently, TS is just a platform to take on form of source code and output another form of source code and having it run as intended.

    This came to bloom mainly because of the gaining popularity of monorepos and how easy it is to build libraries that way, alongside a working application.

    Now, there are 2 scenarios:

    1. The app and libraries are both compiled to a DIST folder and run from it
    
    2. The app is compiled to a DIST folder, the libraries are compiled but published to a package repository (e.g NPM)
    

    With (1), users expect TS to take those paths mappings and convert them to relative paths so they will work cause they are not in node_modules....

    With (2) user's will not expect that to happen because it will cause the imports to point to nothing.

    I understand TS's team take on this, it is something that will work with (1) but not (2) and that's enough to rule it out.

    Maybe i'm wrong, but it seems to me it's one of the reasons for the mixup

    Optional flag = Developer will have the option to enable it for #1 of your case; which is the majority of the cases as can be seen by the number of upvotes here.

  22. winseros commented on Jan 31, 2020

    @winseros

    If path maps are not resolved into emitted code, then what are they for?

  23. DanielRosenwasser commented on Jan 31, 2020

    @DanielRosenwasser
    Member

    Nikita Kobzev (@winseros) this heavily downvoted comment I wrote a few months ago answers that question. #26722 (comment)

  24. added
    DeclinedThe issue was declined as something which matches the TypeScript vision
    and removed
    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this feature
    on Feb 1, 2020
  25. RyanCavanaugh commented on Feb 1, 2020

    @RyanCavanaugh
    Member

    We took this to several design meetings and discussed this to death, and we're sticking with the "JS is JS" design philosophy that has driven TypeScript since its origination.

    I'm going to lock this because it's a long thread and our typical experience is that these threads go in circles when it takes more than a few minutes to read the thread. As usual, I'd implore anyone to actually read all 81 comments here, as we ourselves have done multiple times, before filing new issues related to the matter.

  26. locked as resolved and limited conversation to collaborators on Feb 1, 2020
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

    DeclinedThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions