microsoft/TypeScript

Module path maps are not resolved in emitted code

mika-fischer opened this issue · 76 comments

TypeScript Version: 2.0.2

Code

tsconfig.json

{
    "compilerOptions": {
        "target": "ES6",
        "module": "commonjs",
        "baseUrl": ".",
        "paths": {
            "foo/*": ["*"]
        }
    }
}

app.ts

import {Foo} from 'foo/utils';
console.log(Foo);

utils.ts

export const Foo = 'Foo';

Expected behavior:

% ./node_modules/.bin/tsc && node app.js
Foo

Actual behavior:

% ./node_modules/.bin/tsc && node app.js
module.js:457
    throw err;
    ^

Error: Cannot find module 'foo/utils'
    at Function.Module._resolveFilename (module.js:455:15)
    at Function.Module._load (module.js:403:25)
    at Module.require (module.js:483:17)
    at require (internal/module.js:20:19)
    at Object.<anonymous> (/home/mfischer/src/videmo/tsc-test/app.js:2:17)
    at Module._compile (module.js:556:32)
    at Object.Module._extensions..js (module.js:565:10)
    at Module.load (module.js:473:32)
    at tryModuleLoad (module.js:432:12)
    at Function.Module._load (module.js:424:3)

app.js

"use strict";
const utils_1 = require('foo/utils');
console.log(utils_1.Foo);

Typescript is finding the right module, but in the emitted code, the module path is left as-is instead of applying the path aliases from tsconfig.json. Obviously node has no idea where to find the module. I would have expected typescript to resolve the module path and replace it with something that node can resolve.

If this behavior is intended, then how can the path maps be used to solve the relative-import-hell in conjunction with node?

Do you use some other bundling tool like browserify or webpack on the generated output? or do you expect this to run directly on node?

If this behavior is intended, then how can the path maps be used to solve the relative-import-hell in conjunction with node?

Well and to add context, "paths" is designed for use with loaders that allow remapping, unlike the Node.js require(). The intended behaviour is to allow TypeScript to resolve type information for various module IDs used by various loaders, not to rewrite module IDs. Basically it doesn't do what you thought it did. Nor should it in my opinion, it should only have the capability to mirror the resolution strategies of loaders.

@mhegazy I expected it to work directly with node. It's for a backend application. Is @kitsonk correct in stating that this is working as intended and typescript will not rewrite module paths?

yes, this was the intent - to mitigate the mismatch between runtime and design time experience when module (as it is written by user) can be resolved in runtime but failed to be found by the compiler. At this point compiler always assumes that module id provided by the user is correct and never tries to change it.

similar response #9910 (comment)

All right, thanks. It might be useful to document this better in order to prevent more people from being confused. I now use https://www.npmjs.com/package/module-alias to make it work with node.

Appreciating TS's position, here's a simple solution to the 90% use case for those of us using node, but wanting the convenience of using baseUrl relative require() calls without any fuss.

This solution hooks node's require() call, and resolves requests using the dirname of "main" to mimic baseUrl. It therefore assumes the baseUrl compiler option was also set to the same directory where the source "main.ts" was located.

To use, paste this tiny chunk of code at the top of your "main.ts".

import * as path from 'path'
import * as fs from 'fs'
(function() {
  const CH_PERIOD = 46
  const baseUrl = path.dirname(process['mainModule'].filename)
  const existsCache = {d:0}; delete existsCache.d
  const moduleProto = Object.getPrototypeOf(module)
  const origRequire = moduleProto.require
  moduleProto.require = function(request) {
    let existsPath = existsCache[request]
    if(existsPath === undefined) {
      existsPath = ''
      if(!path.isAbsolute(request) && request.charCodeAt(0) !== CH_PERIOD) {
        const ext = path.extname(request)
        const basedRequest = path.join(baseUrl, ext ? request : request + '.js')
        if(fs.existsSync(basedRequest)) existsPath = basedRequest
        else {
          const basedIndexRequest = path.join(baseUrl, request, 'index.js')
          existsPath = fs.existsSync(basedIndexRequest) ? basedIndexRequest : ''
        }
      }
      existsCache[request] = existsPath
    }
    return origRequire.call(this, existsPath || request)
  }
})()

If you're going to use the module-alias package that mika-fischer mentioned, note that the paths you register to the package shouldn't end in /, and the paths are relative to the path where package.json is (it may be obvious but it's good to get it clear),

So if you have this in your tsconfig file:

"outDir": "./dist",
"baseUrl": ".",
"paths": {
  "foo/*": ["./src"]
}

You have to register this in your package.json:

"_moduleAliases": {
  "foo": "dist"
}

Well and to add context, "paths" is designed for use with loaders that allow remapping, unlike the Node.js require(). The intended behaviour is to allow TypeScript to resolve type information for various module IDs used by various loaders, not to rewrite module IDs. Basically it doesn't do what you thought it did. Nor should it in my opinion, it should only have the capability to mirror the resolution strategies of loaders.

Got here after wasting some time trying to set it up in a big project.
If this behaviour is not going to change the least you could do is to update the documentation before closing this issue.
The official documentation https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping%20docs does not say a thing about having to use "loaders that allow remapping" whatsoever.

install and run "tspath" in your project folder... https://www.npmjs.com/package/tspath

Also you can try "momothepug/tsmodule-alias" to alias path resolution:

https://www.npmjs.com/package/@momothepug/tsmodule-alias

I too managed to get this working with module-alias, with the downside that I have to keep track of my aliases both within tsconfig.json and package.json. Has anyone found a simpler solution?

The solution pointed out by @mattyclarkson also works, but I couldn't find a way of using it side-by-side with ts-node. Any ideas?

Thanks @DanyelMorales, this is really handy.

Can someone tell me what the point of this feature is if the pathnames emitted are actually incorrect? That is to say, if the TypeScript compiler is happy with it, but the end result isn't runnable - what's the use case for this feature?

How should one do relative path mapping for things that are NOT modules, i.e. not imports?

In a node script that reads a certain directory relative to the source file I used to do:

const starterDir = path.resolve(__dirname, './templates/starter')

since typescript compiles the script and writes it to another directory (based on config), __dirname will no longer lead to the above path resolving. What would be a solution to this?

How should one do relative path mapping for things that are NOT modules, i.e. not imports?

That is really "how do I use Node.js and TypeScript question" and much better asked in Gitter.im or StackOverflow.

I love TypeScript but this is insanity.

I don't get it. Even with me knowing little about the TS codebase, this shouldn't be hard to implement. I've just started using a shared project with client and server... why does TS present the paths functionality in the first place, then makes me jump through hoops to actually use it? Why does TS assume that I want to use a bundler/loader with every project I make? I'm trying to use TS to simplify my projects, not tack on more tooling libraries to compensate for a 90% implemented TS feature.

+1!

webpack may help me to resolve path maps (or other tool likes babel-plugin-module-resolver) but not declarations (.d.ts).

All paths in declaration are not resolved. And declarations seem unusable.

Ran into this issue as well. Seemed logical that the emitted code would include path mappings. Resorted to module-alias. But +1 for Typescript to optionally provide this functionality.

Can this not just be added as a compiler option? Clearly it's a popular request. Knowing nothing of the workings of the compiler, shouldn't implementing it be super simple? Why force us to jump through hoops elsewhere when it can be so easily solved directly with the TypeScript compiler, where it makes the most sense?

You can compile absolute and path-based TypeScript imports to relative Javascript files using ts-transformer-imports and ttypescript

I've created a compile-time solution where you can continue using tsc. https://github.com/joonhocho/tscpaths

#15479 (comment)

I have just been struck with this issue when trying to get vue-cli to output d.ts files where there are lots of import {foo} from "@/some/folder/foo" and the d.ts files outputted dont resolve the alias.

From general searching and looking over this thread and others it looks like the knee jerk reaction is "this isnt a problem for TS to solve" but I would implore the team to change that stance as currently if you use custom path alias (completely valid and recommended approach for complex projects) you simply cannot use generate d.ts files (without a custom 3rd party build process) so I would say that the typescript compiler should also resolve these alias' in the declaration file process too.

The compilers job is to output valid javascript AND d.ts files for your typescript files, and it simply doesn't work in this valid scenario (using path alias' in tsconfig files).

I'm a little confused on this issue. It has been closed and labelled 'Working as Intended'. Am I to understand Typescript is intended to output invalid source? Seems odd.

Beggars can't be choosers, but using Typescript avoids many frustrating aspects of vanilla JS and I count relative imports ('../../../../../utils/parser') to be one of them. It would be amazing if Typescript could clean these up!

@codeitcody Seems like it. It's dumb that it would output something that simply doesn't work without some third-party package but that's the reality.

o8e commented

Well, isn't this a nice problem to fall upon.

It does seem very counterproductive to have a feature that essentially renders your app unusable without jumping through hoops (i.e installing more dependencies to workaround the issue).

dwjft commented

This issue has been around for 2 years and has no official word on whether it's going to be implemented or not.

Considering this is required and basic functionality to use one of the best features of TypeScript in your Node.js projects, it's pretty awful how it is being treated.

@mhegazy Can you or someone else let us know if now two years later maybe the TypeScript team would possibly take a look at this again and reconsider?

webpack may help me to resolve path maps (or other tool likes babel-plugin-module-resolver) but not declarations (.d.ts).

All paths in declaration are not resolved. And declarations seem unusable.

Is there a way to achieve this? I have a custom react components library and I got this error when trying to use paths for an alias. I do the 2 bundles with rollup and the types with --emitDeclarationOnly

I can not use module-alias because it says:

WARNING: This module should not be used in other npm modules since it modifies the default require behavior!

Please help to upvote this post on Reddit: https://www.reddit.com/r/typescript/comments/a07jlr/path_maps_cannot_be_resolved_by_tsc_works_as/
Don't know why it needs this huge discussion here. It should be easy to solve this bug. An option in tsconfig and everyone can decide if he want's the current behavior (for whatever reason) or a working approach.

We had the same problem at Dropbox and open-sourced this transformer https://github.com/dropbox/ts-transform-import-path-rewrite

I have had the same experience multiple times now, I expect the path alias to be resolved but I keep forgetting that I have to install module-alias, update package.json and import it in the main file. Would be awesome if this was handledin the compile step by Typescript.

Ouch. This is a real blow to TypeScript. Where is the sense in this?

p.s Can we not just comment +1

Welcome to the club @def14nt. We're a happy group of dreamers, starry-eyed as we gaze into a better future, patiently awaiting the day when TypeScript will implement the simple and sensible compiler option to make our lives easier. When that day finally is upon us, be sure to check the sky for the portly pink bodies of the pigs as they majestically fly away into the sunset on their newfound wings.

Lol, I'll just install an npm extension for something the TypeScript team could add support for. Maybe they should stop adding more and more enhancements and add features that are this basic.

@mika-fischer
How to use https://www.npmjs.com/package/module-alias so that eslint stop to warn about 'Unresolved path '@root/bla/bla' (in JS files)? And how to enable autocompletion for such paths in WebStorm and VS Code?

For me, autocompleting imports works in VSCode by default in typescript projects.

@bobmoff Yes, seems like everything is good for import TS files from TS files.
But autocompletion and navigation for `require('@root/bla/bla') from TS files does not work.

I wish to translate my JS project to TS, and thought that I can rename js files to ts one by one.
Now I realize that it is so hard and unsupported neither by ts nor by IDEs, and I should rename all my js files to ts at once.

Because if I rename just one JS file to TS, all relative paths in build directory became broken (probably I should use "allowJs: true", but I have one project with 2 Gigabytes of JS files, it is so weird to compile them to the build dir %)).
Ok, I trying to solve this by module-aliases, and my IDE's navigation and autocompletion stop to work and eslint warns about 100500 'Unresolved paths'. I am already a bit hate TS, seems like migration for big JS projects is not so easy as TS-marketing people say. It seems like migration JS backend projects to golang is more simple :)

I'm successfully using tscpaths as a workaround.

Me too. I really recommend tscpaths . It works as it supposed to.

My simple workaround:

node -r ts-node/register/transpile-only -r tsconfig-paths/register index.js

Or with the pm2 process.yml

apps:
  - name: 'my-app'
    script: './dist/index.js'
    instances: 'max'
    exec_mode: 'cluster'
    out_file : '/dev/null'
    error_file : '/dev/null'
    node_args: ['--require=ts-node/register/transpile-only', '--require=tsconfig-paths/register']
    env_production:
      NODE_ENV: production

Just stumbled upon this, sometimes TypeScript can be a real pain in the ass.

@duffman

The aliases are interpreted by the TypeScript compiler, it compiles exactly
as it should, it should most definitely not poke around in the resulting javascript, if
you want to use aliases you will have to solve this yourselves!

Highly disagree. Unless you were joking, I really couldn't tell.

TypeScript should compile code that works without needing a third-party tool to complete the job that was only partially implemented into the native compiler.

The TypeScript compiler does exactly what it should do!

If this thread was full of people requesting that tsc also do minification or some such, I'd agree with you. However it isn't. It's people requesting that the compiler generate executable code. I really don't think that's too much to ask from a compiler, do you?

This is a feature that can be utilized if you manage the relative paths, take responsibility for them like Angular does with WebPack or as I do with all my TypeScript projects with TSPath!

This is a feature which makes the compiler output broken code, code that could be working if they only wrote 1 line of code for properly resolving those paths.

The fact that TS needs an external bundler just so that the outputted code can be executed is plain ridiculous.

And it is generating executable code if you don´t use features which is
unsupported by JavaScript Engines,

I've always understood that TypeScript was supposed to compile to JavaScript. If you're telling me that certain features are not supported by JavaScript engines then why exactly are they there?

would you blame the C++ compiler if your application dynamic link libraries and that the program won´t run on a machine doesn't have these installed?

No, but I would blame it if it let me link to other C++ code that didn't actually exist with no compiler error or warning.

Look, I see your point. I do. But part of the compilers job is to sanity check things one last time. At best code that compiles but doesn't run is inconsistent behavior and when I first read into this issue the documentation seemed to suggest behavior that clearly isn't there

This feature has been added in order to SUPPORT LOADERS not
the other way around, read up
on Path Mapping in the official documentation, so again, you are using it
wrong!

https://austinknight.com/wp-content/uploads/2015/04/DesignVSUX.jpeg

@duffman Can't you see that people here just want this feature??? You are telling everyone that he/she is to stupid to understand how this "feature" should be used. OK - you can look at this that way but who knows - maybe it's the other way around...

By the way, my opinion is following:

As aliases are builtin in compiler and compilation of project with them is OK. It may make users think, that it works way it's suggesting (and this issue is pretty much good proof I'm right). It even seems illogical - why something works in "official" editor (vscode - especially when using "auto import" feature, vscode uses aliased path), why copiler also works OK, when resulting code is not working? Saying "js engine don't support it" makes me wanting to ask even more - wasn't TS meant as a thing to mitigate some of JS "problems"?

I would expect one of two solutions of this:

  1. Correctly overriding imports with aliases
  2. Showing a warning

Saying "it's correct behavior" is, I think, wrong. It's not. TS is not assembly language, not even a C/C++.

. TS is not assembly language, not even a C/C++.

I really don´t understand what you are trying to point out by establishing that TS is not C++, most of us are well aware of that I think!

We have also established that alias/path mapping is used in production all over the world, so naturally, VS Code should support that, but it´s still not an argument for MS to craft the compiler to suit your setup!

What I´m having a hard time understanding is why you keep at it, the compiler works as it´s supposed to work, again, read the docs which clearly states what the feature is for!

I mean you can set up a working TS development environment with path aliases in like 2 minutes, if you don´t want to use WebPack, you can use TSPath to resolve all paths in all js files in 1 second, add it to the package.json as a run script and you don´t even have to think about it, the problem does not exist, the compiler stays the way it was meant to function and you can carry on happy hurray!?

Or if it is so important to you that the actual compiler does this for you, then I suggest you fork the compiler and implement it yourself, maybe it would be a big hit or maybe people are happy the way they are since they have set up their environments to support aliases.

Summoning the top five TypeScript contributors: @ahejlsberg @andy-ms @DanielRosenwasser @sandersn @sheetalkamat

Could the TypeScript team reconsider this issue? I think this thread offers some useful discussion on both viewpoints and given its recent popularity and the amount of time that's passed it should be looked at again.

The status of this issue left me no other choice but to demote TS to type checker duty only.
Babel now has a decent support for TS syntax and together with babel-plugin-module-resolver does the job of emitting working code for this use case just fine.
The only downside is duplicating bits of configuration from tsconfig.json as Babel does not care about TS configs. But it is an acceptable price for working absolute paths in node projects and as a bonus I get the whole ecosystem with brilliant things like babel macros.

This is the minimum setup I got working as a drop in replacement for tsc compiler:

  • npm install --save-dev @babel/cli @babel/core @babel/preset-env @babel/preset-typescript babel-plugin-module-resolver @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
  • in package.json:
    tsc -> tsc && babel ./src --out-dir ./dist --extensions ".ts,.js"
  • in tsconfig.json:
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@mynamespace/*": ["src/*"]
    },
    "outDir": "./dist",
    "noEmit": true, <--
    "allowJs": true,
    "target": "ES2017",
    "module": "CommonJS",
    "lib": [
      "ES2017"
    ]
  },
  "include": [
    "./src/**/*"
  ]
}
  • in .babelrc:
{
  "presets": [
    "@babel/preset-typescript",
    ["@babel/preset-env", {
      "targets": {
        "node": true
      }
    }]
  ],
  "plugins": [
    ["module-resolver", {
      "root": ["./src"],
      "alias": {
        "@mynamespace": "./src"
      },
      "extensions": [".js", ".ts"]
    }],
    "@babel/plugin-proposal-class-properties",
    "@babel/plugin-proposal-object-rest-spread"   
  ]
}

I just want to use typescript with absolute path, but seems i have to config webpack or babel or something, it's too hard to achieve this simple feature, it should be easier 😞

Leaving this here because an actual documented use case for the current behavior of paths was not mentioned in this thread: @types/ packages do not backport features with regards to semver. They do however include updated types for older APIs that I can use. E.g. I'm using history@2 when history@3 is the latest.

"paths": {
    "history": [ "history/v2" ]
}

The compiler would need an extra option to differentiate between type aliases and "code" aliases. If we change the behavior to actually emit the path aliases then we need to add the ability to the compiler to find the correct types version.

This is not an argument against the proposed behavior. I'd rather have emitted aliases and just™ working solution for versioning of types. Just wanted to provide some context why changing this might not be as trivial as people think it is.

cruhl commented

For the second time during a company-wide TS workshop, I've had to explain this inane and embarrassing behavior...

Seriously!

What kind of language compiler, especially one whose primary sales pitch is more correctness for JavaScript, produces broken code as a "feature"?!?!

cruhl commented

I'm sorry to have sounded so frustrated in my comment, but the community's views here are seemingly being ignored and downplayed repeatedly.

cruhl commented

Just look at how many times this has been referenced... What a waste of time and attention for so many people.

I understand your frustration, but lots of people feeling a behaviour is correct, doesn't mean it is the right thing to do.

TypeScript rewriting module identifiers is a slippery slippery slope. What has been expressed multiple times in this thread is that TypeScript is configurable to model the behaviour of other module resolvers and other build tools, not replace or implement them.

Just because TypeScript can be configured to resolve modules in flexible ways doesn't not mean that TypeScript emits "broken code". Certain loaders and bundlers that mirror this configuration would work just fine.

If we were to be critical of anything, we could blame the team for naming the option something that might look like it fixes a problem it was never intended to fix, though the documentation for the option makes it clear that it does not change the emit.

Because a particular tool doesn't solve a problem you have doesn't always mean it is that tools fault. It maybe you are just don't have all the tools you need to solve your problem.

dwjft commented

@kitsonk everything you just said is way off the mark.

The issue is that TS will operate one way during development / testing, and another after compilation has completed.

If TS wants to be a module resolver, it needs to choose a pattern and stick to it. If I run something with ts-node it should operate exactly as if I compiled some TS and ran it with node.

It does not, and that is the problem.

Maybe module maps will alleviate the frustration in the future. But our position here along with the technical problems we want to solve are pretty explicit - path mapping reflects the behavior of an external resolution scheme (e.g. path mapping in AMD and System.js, aliases in Webpack and other bundlers). It doesn't imply we will change your paths.

I don't think any recent discussion has been constructive recently, nor do I foresee any future changes here, so I'm going to lock this issue.