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

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

Closed
3 of 4 tasks
dannycochran opened this issue Aug 28, 2018 · 82 comments
Closed
3 of 4 tasks

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

dannycochran opened this issue Aug 28, 2018 · 82 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@dannycochran
Copy link

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)
@DanielRosenwasser DanielRosenwasser added the Duplicate An existing issue was already created label Aug 28, 2018
@DanielRosenwasser
Copy link
Member

I think you want #16577.

@DanielRosenwasser
Copy link
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?

@dannycochran
Copy link
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.

@deejayy
Copy link

deejayy commented Sep 13, 2018

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

@icopp
Copy link

icopp commented Sep 13, 2018

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.

@dannycochran
Copy link
Author

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

@dannycochran
Copy link
Author

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

@joonhocho
Copy link

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

@dannycochran
Copy link
Author

@joonhocho there are existing third party solutions, the idea is to avoid them entirely.

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

@alexandertrefz
Copy link

alexandertrefz commented Nov 30, 2018

@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

@dannycochran
Copy link
Author

@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.

@ghost
Copy link

ghost commented Dec 3, 2018

I think it's unlikely that we would change the module specifier you wrote when emitting -- #16577 is an existing issue that asks for that kind of functionality, and if we ever did add that then maybe we would take a look at this too.

@dannycochran
Copy link
Author

@andy-ms ah my mistake I read through the issue more and understand how this is a dupe. Thanks for taking a look.

@eyedean
Copy link

eyedean commented Jun 13, 2019

It's so different from 16577, and more important!

While this issue and #16577 look similar in terms of "customizing emitted code on module paths", I think there is a subtle difference between the two and I'd like to ask for reopening this issue as its own.

Let's say I have the following import in my typescript code

import helper from "@utils/helper";`

and in tsconfig.json I have set:

"paths":  {
   "@utils/*": [ "src/interal/tools/utils/*" ],
}

The problem with this one is, there is an actual piece of information, (rather than just a .js extension like #16577), that is getting lost. It's because tsc just emits the same path as following:

const helper_1 = __importDefault(require("@utils/helper"))

and the piece of information that @util is actually pointing out to somewhere specific in my codebase (i.e. src/internal/tools/utils) is swallowed by the compiler! Unlike the .js extension issue, there is no way on earth that one can infere this mapping and fix this code, only by looking at the output of the compiler.


Exesting Third Party Solutions

To solve this issue, I need to either re-enter the same mapping manually for the consumption of my JS Loader via something like module-alias (funny, I'm already doing it once for ESLint!) which requires having multiple entries in the codebase for the same concept, a terrible practice; or I should use a tool like tsconfig-paths that reads tsconfig.json and does what compiler is failing to do!

It's interesting to me that for this very problem, tsconfig-paths has got to 500K downloads/week! In the mean time, all the tickets asking for this simple functionality have been closed here: #16640, #18951, #18972, #19453, and so on! (PS. Just found #10866 from years ago with tons of emojis!)


Proposal

It's really simple.

tsconfig.json already has 89 flags. Let's add the 90th one as --resolveMappedPaths (or something similar) which, at the time of compile, will replace any mapped path (coming from the "key" side of the property in the paths object) to its destination ("value" side of the same property!)

So my tsconfig.json would be:

   "compilerOptions": {
      "module": "commonjs",
      "esModuleInterop": true,
      "target": "es6",
      "noImplicitAny": true,
      "moduleResolution": "node",
      resolveMappedPaths: "true"
      "..."

It can also be a property on an overloaded per-path-basis config, e.g.:

"paths":  {
   "@utils/*": { 
       "resolveOnCompile": true,
       "target": [ "src/interal/tools/utils/*" ]
   },
}

@nuno-tomas
Copy link

+1

@yang4515
Copy link

+10086

@joseluisq
Copy link

joseluisq commented Jul 8, 2019

For production apps after a tsc build.
I have created a TS Paths replacer in Go which is pretty fast and that addresses the custom paths issue.
After it's applied you only need to run your app node main.js.

@MikeMitterer
Copy link

@joseluisq Thanks! But @DanielRosenwasser I don't get it why we don't get a compiler flag to resolve the path. Just to make the path-feature work we have to usw webpack or another bundler. It does not work with tsc.
You say "it's our main goal not to change the generated JS code" but path-maps with tsc breaks the code!!!!
I have two options - Use webpack for this problem to solve or stay with those ugly, long, import path statements...

@DanielRosenwasser
Copy link
Member

You say "it's our main goal not to change the generated JS code" but path-maps with tsc breaks the code!!!!

Path mappings are meant to reflect the behavior of whatever is actually resolving .js files. The idea is not to break your code, but to reflect the behavior of something like webpack's resolve.alias or AMD's paths field.

I don't get it why we don't get a compiler flag

Because a compiler flag doesn't actually relieve us of the burden of the complexity of a feature - it just makes it harder for us to think about.

@MikeMitterer
Copy link

MikeMitterer commented Aug 1, 2019

@DanielRosenwasser Thanks for your answer!

the burden of the complexity of a feature
For this particular case??? Seems not sooo complex to me.

My main problem is that I really like what tsc gives me - multiple output files! Every TS-file produces a JS counterpart. That's cool. Readable, understandable and nice. That's what I use for all of my libs. I use webpack for the main application - OK.

is not to break your code
But this is what tsc produces without the help of WebPack - shouldn't tsc produce valid AND runnable code just out of the box???

I can choose between two options.

  • TS-Path-Mappings / WebPack for my libs and loosing the multi-file-output-mode (you know what I mean)
  • No TS-Path-Mappings but accept this ugly 'import ../../../whatever' style
    Both options are just a pain in the ass... (AND it destroys the great TS user experience)

[Update]
Hmmm. I found this plugin for ttypescript - it seems to work but I still think this should be part of the official tsc-compiler.
If you don't want to support the mentioned compiler flag, another way to open TS/tsc would be a better support for transformers e.g. like ttypescript . I understand that you want to be as close as possible to JS but on the other hand at least a bit more flexibility would really help

@icopp
Copy link

icopp commented Aug 2, 2019

but to reflect the behavior of something like webpack's resolve.alias

But when you use that in webpack, the resulting build actually works. When you use path mapping in TS and use the TS compiler, the resulting build doesn't work.

@wintercounter
Copy link

+1 for this, same issue here. We're using TSC only for type-check and generating d.ts files. The rest is Babel. In Babel we're resolving our aliases fine, but the generated d.ts files will still have the alieses in the copiled code. I also don't understand why this issue have to be closed.

@regniblod
Copy link

+1

@belaarany
Copy link

+1.....

@MikeMitterer
Copy link

@wintercounter >I also don't understand why this issue have to be closed.
Nobody does! Is this really OpenSource????
Microsoft blocks this feature in a very dictatorial manner

@elmcrest
Copy link

Also wondering - this is really making a theoretically nice feature (having nice imports) completely useless.
I mean I don't have any Idea how hard this is to implement but from a naive though it's just s/@alias/realPathFromTSConfig ... no?

@ark120202
Copy link

Wouldn't it also conceptually conflict with import maps?


Here are some questions I have about this feature:

Another one I'd like to add is handling of dynamic import syntax. Should they be transformed as well? If yes, that might present a very surprising behavior:

import('path'); // works
const path = 'path';
import(path); // fails

If no, that might be against the specification, since both static and dynamic imports use the same abstract operation for specifier resolution.

@kitsonk
Copy link
Contributor

kitsonk commented Sep 20, 2019

Wouldn't it also conceptually conflict with import maps?

As the paths feature stands now, it reasonably models import maps. It is how the feature of SystemJS and AMD was standardised in a browser context. It doesn't conflict conceptually any more because of it, but it underlines that TypeScript doesn't want to get into the business of re-writing module specifiers, because that is effectively a runtime only concern.

@wintercounter
Copy link

wintercounter commented Sep 20, 2019 via email

@tiagonapoli
Copy link

+1

@mrmckeb
Copy link

mrmckeb commented Sep 29, 2019

I think this is really important, as TypeScript declarations for libraries are essentially broken if you use absolute paths. We then need to manually fix them, or use a tool to go through and correct paths.

If my library internally imports ./src/folder as folder (which is the same as ./folder), this works fine. I can use Rollup, Webpack, etc. to update the paths as appropriate.

But I can't do the same for d.ts files. The result is that TypeScript (in a user's project) thinks my library is importing a package called folder and fails.

For the JavaScript itself, I understand the argument for not changing output.

@georgyfarniev
Copy link

I spend all day trying to figure out what wrong with my setup and why paths aren’t resolved in outputted js files. Makes this feature almost useless and very confusing, since it processes NOT WORKING output for NodeJS. Please add some option to resolve paths that tsc already knows internally and bring the end to this struggles. It wouldn’t break anything if it will require option to enable resolution.

@mrobrian
Copy link

I also ended up here after searching for why my emitted code doesn't have the paths I expected. After reading through this thread, I think I agree with what seems to be the unpopular position of not changing the paths in the output. There's just too much to consider, and I don't see how tsc could possibly account for every variation of build process out there, not to mention things like automated tests, etc.

What I would suggest to the tsc devs is to make the documentation a little clearer here: https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping

Right now, that section could be interpreted to mean that emitted code will have the paths resolved, but really the setting just makes it so that we can use other tools for module resolution and allow TypeScript to understand what those imports mean. The documentation not being clear about that seems to have led to some confusion and expectations among a lot of users, myself included.

@eyedean
Copy link

eyedean commented Oct 31, 2019

There's just too much to consider, and I don't see how tsc could possibly account for every variation of build process out there, not to mention things like automated tests, etc.

@mrobrian what is suggested here multiple times (and has received many upvotes and support from other developers) is to add an optional flag that supports the simple absolute paths to relative conversation in the output, just as it's already doing in the compile time.

The two bold words here are:

  • Optional, so it wouldn't break anything. Only people who want it will use it as an experimental flag.
  • Simple, as simple as what https://github.com/dividab/tsconfig-paths does. I understand we can make a beast out of this to support 100% of the use cases; but just providing the simple 20% of it will satisfy 80% or more of the needs.

Thanks.

@tonitrnel
Copy link

+1

@MikeMitterer
Copy link

Just gave up and change 72 file in one of my packages. Moved back from module path to relative path imports.
IT LOOKS SOOOO UGLY - All those ../ It makes me wanna puke!
It is tilting at windmills - I'm broken. MS you won - Congrats! 😢

@t83714
Copy link

t83714 commented Nov 30, 2019

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.

@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.

@t83714
Copy link

t83714 commented Nov 30, 2019

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.

@tconroy
Copy link

tconroy commented Jan 7, 2020

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.

@wintercounter
Copy link

wintercounter commented Jan 7, 2020

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

@tconroy
Copy link

tconroy commented Jan 8, 2020

@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.

@wintercounter
Copy link

wintercounter commented Jan 8, 2020 via email

@shlomiassaf
Copy link

shlomiassaf commented Jan 16, 2020

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

@eyedean
Copy link

eyedean commented Jan 21, 2020

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.

@winseros
Copy link

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

@DanielRosenwasser
Copy link
Member

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

@RyanCavanaugh RyanCavanaugh added Declined The issue was declined as something which matches the TypeScript vision and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Feb 1, 2020
@RyanCavanaugh
Copy link
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.

@microsoft microsoft locked as resolved and limited conversation to collaborators Feb 1, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests