microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.9k stars 12.47k forks source link

[FEATURE] absolute->relative module path transformation #15479

Closed nakamorichi closed 7 years ago

nakamorichi commented 7 years ago

Problem

tsc does not support transforming absolute module paths into relative paths for modules located outside node_modules. In client side apps, this is often not an issue because people tend to use Webpack or similar tool for transformations and bundling, but for TypeScript apps targeting Node.js, this is an issue because there is usually no need for complex transformations and bundling, and adding additional build steps and tools on top of tsc only for path transformation is cumbersome.

Example input (es2015 style):

import { myModule } from 'myModuleRoot/a/b/my_module';

Example output (CommonJS style):

const myModule = require('./a/b/my_module');

My personal opinion is that relative module paths (import { ... } from '../../xxx/yyy';) are an abomination and make it difficult to figure out and change application structure. The possibility of using absolute paths would also be a major benefit of using TypeScript for Node.js apps.

Solution

Compiler options for tsc similar to Webpack's resolve.modules.

Could this be achieved for example with existing baseUrland paths options and adding a new --rewriteAbsolute option?

Related

https://github.com/Microsoft/TypeScript/issues/5039 https://github.com/Microsoft/TypeScript/issues/12954

aluanhaddad commented 7 years ago

My personal opinion is that relative module paths (import { ... } from '../../xxx/yyy';) are an abomination and make it difficult to figure out and change application structure.

While I heartily agree with your sentiment, I think rewriting such imports would open up a can of worms that would ultimately break or complicate an insanely large number of tools and workflows that rely on the emitted JavaScript using the same module specifiers. Even under a flag, I think it will introduce a lot of complexity.

I hate relative paths that go up, it is just awful for maintainability, but my two cents is that this needs to be done on the NodeJS side, not the transpiler side. Of course it's extremely unlikely that will ever happen...

ikokostya commented 7 years ago

This issue can be solved if add node_modules directory with symlink to src directory:

project_root/
  node_modules/  <-- external modules here
  src/
     node_modules/  <-- keep this folder in git
        src -> ../src  <-- symlink to src
     a/
        b/
           c.ts
     d.ts
  tsconfig.json

In this case you can use the following import in c.ts:

import * as d from 'src/d';

instead

import * as d from '../../d';

All will work in typescript and commonjs. You just need to add exclude field in tsconfig.json:

{
    "exclude": [
        "src/node_modules"
    ]
}
nakamorichi commented 7 years ago

@aluanhaddad I've been using path rewrite (Webpack) on client-side since I began writing React apps. How would it cause problems on the server side if it doesn't cause problems on the client side? Editors already support setting module roots, and linters (ESLint, TSLint) seem to be fine also.

To the maintainers of TypeScript: Please add support for custom module roots so that we can get rid of the awful and unmaintainable import paths.

nakamorichi commented 7 years ago

@ikokostya Many tools have special treatment for node_modules, and it is always treated as a folder for external code. Putting app code into node_modules may solve the import path problem, but introduces potential other problems. It is also ugly solution that shouldn't, in my opinion, be recommended to anyone.

ikokostya commented 7 years ago

Many tools have special treatment for node_modules, and it is always treated as a folder for external code.

Could you provide example of such tools? In my example all external code are placed in node_modules in project_root.

Putting app code into node_modules may solve the import path problem, but introduces potential other problems.

Which problems?

It is also ugly solution that shouldn't, in my opinion, be recommended to anyone.

Maybe you should read this

And why it's ugly? It uses standard way for loading from node_modules.

aluanhaddad commented 7 years ago

@Kitanotori perhaps I misunderstood your suggestion, TypeScript supports this feature with the --baseUrl flag. My point was that NodeJS doesn't support it and that TypeScript should not attempt to provide the future on top of NodeJS by rewriting paths in output code.

@ikokostya

Could you provide example of such tools?

There are too many to count but TypeScript is definitely an example of such a tool.

I think putting application code in a node_modules folder is a very ill-advised hack.

nakamorichi commented 7 years ago

@aluanhaddad --baseUrl setting enables to transpile the app, but what's the point of being able to transpile successfully if you can't execute it? ts-node does not seem to support --baseUrl, so I think it is inaccurate to say that TypeScript supports custom absolute paths.

@ikokostya Thanks for the links. It seems that setting NODE_PATH in the startup script is the best way currently. I think I will go with that approach.

aluanhaddad commented 7 years ago

@Kitanotori

@aluanhaddad --baseUrl setting enables to transpile the app, but what's the point of being able to transpile successfully if you can't execute it? ts-node does not seem to support --baseUrl, so I think it is inaccurate to say that TypeScript supports custom absolute paths.

The point is that it does execute perfectly in environments that support that. RequireJS, SystemJS, and even Webpack support setting a base URL.

What I'm trying to say is that the issue is on the NodeJS side. TypeScript provides base URL configuration to integrate with and take advantage of those other tools.

RyanCavanaugh commented 7 years ago

Our general take on this is that you should write the import path that works at runtime, and set your TS flags to satisfy the compiler's module resolution step, rather than writing the import that works out-of-the-box for TS and then trying to have some other step "fix" the paths to what works at runtime.

We have about a billion flags that affect module resolutions already (baseUrl, path mapping, rootDir, outDir, etc). Trying to rewrite import paths "correctly" under all these schemes would be an endless nightmare.

nakamorichi commented 7 years ago

@RyanCavanaugh Sorry to hear that. If Webpack team was able to deliver such feature, I thought TypeScript team could also - considering that TypeScript even has major corporate backing. It's a shame that people are forced to add another build step on top of tsc for such a widely needed feature.

morlay commented 7 years ago

Still hope TypeScript could support this. Or Just make it easy to create a plugin, so we can build something like babel-plugin-module-resolver to make it work. (ref: https://github.com/Microsoft/TypeScript/issues/11441)

azarus commented 7 years ago

So anyone got any solution or a really nice workaround without babel to make this happen ?

Edit I've ended up with a custom transform script using gulp & tsify & gulp-typescript https://gist.github.com/azarus/f369ee2ab0283ba0793b0ccf0e9ec590 Browserify & Gulp samples included. So anyone got any solution or a really nice workaround without babel to make this happen ?

nakamorichi commented 7 years ago

@azarus I went with the solution of adding this to my top level file:

import * as AppModulePath from 'app-module-path';
AppModulePath.addPath(__dirname);

The downside is that if the loading order differs from expected, the app crashes. Better solution would be to have a compiler plugin for converting absolute paths to relative paths. However, I'm not sure if TypeScript has any kind of plugin feature.

nakamorichi commented 7 years ago

I created a post-build script for converting absolute paths to relative (doesn't yet have way to set the module root paths, but one can easily implement such): https://gist.github.com/Kitanotori/86c906b2c4032d4426b750d232a4133b

I was thinking of having the module roots being set in package.json via moduleRoots array containing path strings relative to package.json. I wonder what kind of risks there are in this kind of approach?

azarus commented 7 years ago

I've ended up with my own post build script too, https://gist.github.com/azarus/f369ee2ab0283ba0793b0ccf0e9ec590 Browserify & Gulp samples included.

It acutally uses the tsconfig.json paths and baseUrl setup :) I am gonna make a npm package from this during the weekend, i just haven't had time to properly test the script.

kayjtea commented 7 years ago

I have this in my tsconfig:

{
  "compilerOptions": {
    ...
    "module": "commonjs",
    "moduleResolution": "node",
    "rootDir": "src",
    "baseUrl": "src",
    "paths": {
      "~/*": ["*"]
    }
  },

I can't remember if the other options above are necessary, but "paths" will allow this:

import {fundManagersApi} from "~/core/api/fund-managers.api";

The core directory is at src/core.

And then I use the npm package "module-alias" and do:

require('module-alias').addAlias("~", __dirname + "/src");

At the top of my top level file.

Webstorm understands the paths option and will auto-import correctly even.

vakhtang commented 7 years ago

I use tsconfig-paths to handle this during runtime. I'm not aware of any performance implications or issues otherwise. The one objection I could see is monkey-patching Node's module resolution strategy.

0x80 commented 6 years ago

@kayjtea I don't think module and moduleResolution are related. Here's a slightly simpeler version that seems to work:

    "baseUrl": "./",
    "paths": {
      "~/*": [
        "src/*"
      ]
    },

Personally, I prefer writing @src, and for this you can use:

 "baseUrl": "./",
    "paths": {
      "@src/*": [
        "src/*"
      ]
    },

In VSCode I don't seem to have to do anything with my top-level files or npm module-alias.

--- edit --- Ah damn it. I see now that the paths don't work at runtime work if you don't do more then make the compiler and VSCode happy.

azarus commented 6 years ago

Well i solved this sort of problem by splitting my apllication into npm modules and linking them during development. Symlinks are generated and i find it much easier to solve the relative path hell.

However theres still a great need to properly support importing modules by absolute/root path!

Any other solution just seems to be band-aid for a pain that needs pain killer.

On Jan 6, 2018 22:21, "Thijs Koerselman" notifications@github.com wrote:

@kayjtea https://github.com/kayjtea I don't think module and moduleResolution are related. Here's a slightly simpeler version that seems to work:

"baseUrl": "./",
"paths": {
  "~/*": [
    "src/*"
  ]
},

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/15479#issuecomment-355753605, or mute the thread https://github.com/notifications/unsubscribe-auth/AFlNc1RXUW0Hx4D4MF-UMQVMQQ06CouCks5tH49bgaJpZM4NMg4- .

0x80 commented 6 years ago

@azarus I think I might have to go that route too.

I have two repo's that are also depending on roughly the same set of helpers and services. And I am getting tired of copying code between the two.

azarus commented 6 years ago

@0x80 i ran into a similar problem when had to share model definitions between 5 different services. I solved this by creating a shared folder that everyone were able to access if needed. Setting up an NPM link is also easy and less hassle than you would think. But i am sad that proper solution is still not in place.

duffman commented 6 years ago

Use tspath (https://www.npmjs.com/package/tspath) run it in the project folder after compile, no config needed, it uses the tsconfig to figure things out (a recent version of node is needed)

0x80 commented 6 years ago

@duffman Thanks for the tip! I'll check it out 👍

stereobooster commented 6 years ago

In case you want to configure create-react-app project add:

{
  "compilerOptions": {
    //... other react-scripts-ts options
    "paths": {
      "src/*": ["*"]
    },
    "baseUrl": "src"
  },

Is equivalent to NODE_PATH=./ in .env in standard c-r-a project.

mustafaekim commented 6 years ago

I think that this feature request should be re-considered since es6 modules are now on browsers and browsers do not know what to do with non-relative paths. Somehow the non-relative paths should be replaced with relative paths.

azarus commented 6 years ago

I agree. But If i recal this feature request was declined because "its out of the scope of the project" "use something else"... "we already have a solution in place" (and that solution sucks btw).... and many other nonsense reasons. Would be much easier for everyone if ts would translate project absolute paths to relative paths. If the reasoning "many developers need this out of the box" is aint enough.. why is typescript still in development then?

edufschmidt commented 6 years ago

I have found module-alias or tsmodule-alias to be useful in these situations. It does, however, make sense to me to support absolute to relative path mapping in Typescript.

fis-cz commented 6 years ago

If they don't want to implement its another reason to have TSC plugin system so we can integrate such functionality on our own directly to TSC build process with possibility to use tsconfig, ast, file lists, whatever is tsc using internally without need of another bunch of external, post processing tools.

JakobJingleheimer commented 6 years ago

@RyanCavanaugh I'm not sure how you can consider it "out of scope" to fix a half-baked feature that:

That seems to meet even the most forgiving definition of "broken".

Typescript already (potentially) outputs significantly different code than its input, so a tiny bit more does not seem incongruent with existing functionality.

It's a bit ridiculous to expect adding webpack solely to work around this obviously undesirable behaviour: You claim that the purpose of paths is for satisfying the compiler; but then what is the value of this compiler check/error at all? Nothing, because making the compiler happy results only in a happy compiler but broken output, and producing unbroken output is its main job.

this needs to be done on the NodeJS side, not the transpiler side. Of course it's extremely unlikely that will ever happen...

Shirking responsibility to the VM as @aluanhaddad suggests is not a solution; especially when the suggester already knows that is not a reasonable expectation (because it's not the VM's responsibility to resolve bundle issues).

This should be re-classified as a bug instead of a feature request: Asking for a feature to not break is not a new feature or an enhancement. Otherwise, you might as well add a warning:

paths ⚠️ Using this feature on its own will break your project. It handles only half the job (and not the useful half); it must be paired with a bundler like webpack to handle the missing half.

In terms of complexity, is this not a find and replace that is aware of values (baseUrl, outDir, etc) that TypeScript is already using (and then dropping on the floor)? Monkey-patching that (with some kind of post-processor) would be a significantly more difficult implementation than doing it right originally.

clayne11 commented 6 years ago

This definitely needs to be re-opened and roadmapped. It's causing no end of hell for my team and I.

rafsawicki commented 6 years ago

I'm all in for at least improving documentation about lack of a runtime support for this option. Using paths for anything other than .d.ts files is a great way to waste time being confused why the code is happily compiling, but not working.

cruhl commented 6 years ago

The fact that this isn't documented resulted in a massive time waste for me.

MRazvan commented 6 years ago

I agreee with @jshado1 at around 90%. I don't think it should be a note, I think it should be fixed. It's not even specific to something. It's a functionality to help developers manage large projects. It's like adding syntactic sugar in C#, but generated code is not valid unless you use something else to fix the generated output.

It should either be removed and leave us with nothing, or fix it so it generates valid code regardless of other tools.

It's not even specific to node, there should be a flag transforming relative paths to absolute paths. It's a functionality for helping developers manage large projects.

The funny thing is typescript has flags specific for 'react' so it is already are doing something for third party library. A flag to generate valid builds when using aliases is "out of scope" even if it's a generic functionality.

What happens if in 3 year's there is a cool new tool that we will use and it does not support relative paths. What happens if we don't want to use a another tool. What happens if webpack for some reason drops relative paths.

marcos-diaz commented 6 years ago

Please stop using excuses as "It's a Node problem" or "TSC is not a build system, use Webpack", because this is indeed a Typescript problem that is annoying plenty of people, and TSC is indeed a build system, now there is even a --build parameter in 3.x.

It is a can of worms? Yes It is a must to fix? Also yes

eaardal commented 6 years ago

My first impression of TypeScript is banging my head against this wall for 2 days. Not fun 😩

clayne11 commented 6 years ago

This seriously needs to be fixed.

With our current set up we're using a babel-plugin-module-resolver on our shared library code after our Typescript build completes in order to fix the module paths. Unfortunately, Babel doesn't compile type files, so we have to use relative paths anytime we reference a type in our library which is leading to horrible paths like ../../../../../ui/type.d.ts.

ramesaliyev commented 6 years ago

Well, if you dont want us to use absolute paths, then you should not introduce baseUrl config, because its not make any sense if my application wont run in real world when i use absolute paths. And having a config named baseUrl is really misleading. Its wasting developers time to finding out what the problem is.

So your general take on this is simply wrong.

Raiondesu commented 6 years ago

Well, there are already more than 5 issues with 100+ responses voting to fix this with at least 1 new comment per-week. Seems like TypeScript devs simply avoid the problem pretending it does not exist.

I think this may never be resolved until somebody with a PR comes and solves this "Too Complex" and "Out of Scope" problem for them.

That being said, the possible PR also has to be accepted for this to be fixed though. Which does not seem to me like much of a possibility given the conservative mindset of maintainers in regards to this problem.

P.S. For anyone who got here looking for a solution (like I did), tsconfig-paths seems to be the most convenient hack to make this work without adding webpack or babel to your dependencies.

Raiondesu commented 6 years ago

Also, as @MRazvan stated:

The funny thing is typescript has flags specific for 'react' so it is already are doing something for third party library.

@RyanCavanaugh, this line alone breaks any argument about this issue being "Out of Scope" as TypeScript already does waaay more than its "scope" should allow it to do.

Current list of such things includes but not limited to:

So, whadda we do with all that?..

If resolving relative paths is "Out of Scope" then things like building other projects, compiling single-front-end-framework-specific syntax and making exceptions for single (even though quite popular) frameworks should be not only "Out of Scope" but even out of consideration for being implemented.

Raiondesu commented 6 years ago

Speaking of this:

rather than writing the import that works out-of-the-box for TS and then trying to have some other step "fix" the paths to what works at runtime

  1. Custom import paths do not "work" for TS out-of-the-box. Current way of making this work in TS is "some other step to fix the paths" because they didn't work in TS-compile-time. So, the "paths" options is already a hack. Though, it's a hack that only cares about TypeScript being happy and not anyone else. That being said, by making custom paths render into es-compliant relative paths would not be a breaking change too, since all the other transpiling tools like webpack or rollup do not care about relative paths being relative anyway.
  2. Replacement of custom paths with TS-resolved relative paths can be done at the same step as the TS resolves them due to TS compiler being "aware" of the paths in question right at the time the paths need to be changed with all variables and flags in mind. Not everything in later cycles of software development has to be the "some other step to fix" that is duct-taped to the code somewhere.

That being said, I would gladly propose a PR myself, as I study the code at this exact time. But the lack of reliable documentation and messy code structure make this process extremely painful, so I have no idea of how long it can take me to propose any reliable enough solution.

grrowl commented 6 years ago

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

joonhocho commented 6 years ago

I've created https://github.com/joonhocho/tscpaths and have been using it for my projects. It replaces absolute paths to relative paths in compile time. (tsconfig-paths does it in run-time). You can simply add it to your build script after tsc and that's it. This is not a mature project, so it may not work for your if your setup is complicated, but I've been using it fine for my setups without any problems. PRs are welcome!

alecdwm commented 6 years ago

Thanks to @joonhocho's module I was at last able to use typescript with nexe, which bundles a node project into a single executable file. In such a setup, runtime modifications to the path were ineffective.

joonhocho commented 6 years ago

@alecdwm glad I helped!

longlho commented 5 years ago

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

kvendrik commented 5 years ago

Just to throw this out there we've had the same problem which drove me to create this package yesterday https://github.com/kvendrik/ts-absolute-paths-transformer. @longlho's solution looks great as well so whichever works best for your project. 🙂

realmhamdy commented 5 years ago

For those interested, here's an SO answer on how to use webpack in your nodejs project to enable absolute imports.

disclaimer: my answer.

houd1ni commented 5 years ago

Typescript generates typings, webpack does packing. Typings has imports with aliases. They are deeply in crap.

brian428 commented 5 years ago

It's very frustrating that this remains such a huge problem. The argument that TS shouldn't be responsible for fixing path aliases is baffling. What's the point of allowing path aliases if the compiled output is broken as a result of using them? Please consider adding a flag to allow alias fixing, or provide a simple plugin hook so that folks who need this have some option to deal with it (like ttypescript does).

craigsumner commented 5 years ago

After struggling with this for a while, and finding the best solutions I could for our projects, I wrote up my notes. I'll duplicate them here, in case anyone can correct my mistakes, or benefit from the summary.

Module Resolution and Import Paths

TypeScript supports relative and non-relative import paths.

See: https://www.typescriptlang.org/docs/handbook/module-resolution.html

Non-relative paths work immediately for dependencies from external packages. But without further configuration, only relative paths work when importing from files in the local project. Primarily due to upward navigations, relative paths quickly become difficult to understand and maintain, once the code is organized into directories more than one level deep.

import { Banana } from '../../../model/fruit/Banana';

TypeScript's baseurl / paths feature partially addresses this problem.

See: https://www.typescriptlang.org/docs/handbook/module-resolution.html#base-url

A simple configuration of baseurl and paths can allow the above import to be written without the upward navigations.

import { Banana } from 'src/model/fruit/Banana';

This is part of a fine solution, but creates new problems downstream. While the TypeScript compiler is capable of finding the dependencies using baseurl and paths, and completing the compilation successfully, the compiler produces JavaScript files which contain the original import paths as written in the TypeScript source. Typical consumers of those JavaScript files, including Node.js, browsers, and critically, other projects referencing the library, will fail to resolve those imports, because those consumers are unaware of the original baseurl and paths scheme.

This is, by assertion of the TypeScript team, not a bug or pending enhancement in TypeScript. The TypeScript team states clearly that adjusting the paths in the output JavaScript to reflect the compilation context is out of scope. Instead, post-compilation consumers must understand the original paths. In practice, that means either writing ugly relative paths, or re-interpreting baseurl and paths, or their equivalents, post-compilation.

See: https://github.com/Microsoft/TypeScript/issues/15479#issuecomment-300240856

Many solutions to this problem have been published. Here are the few I've selected to use.

First: tsconfig-paths

https://www.npmjs.com/package/tsconfig-paths

This hijacks the module lookup calls in Node.js and fixes the paths on the fly at runtime. It's heavily used, and works well for in-memory tasks, like test frameworks. But it doesn't help browser runtimes or other projects referencing the library, because it doesn't write out adjusted JavaScript files to be consumed later.

Second: tsconfig-paths-webpack-plugin

https://www.npmjs.com/package/tsconfig-paths-webpack-plugin

This helps webpack resolve imports. It helps solve the problem for browser consumers.

Third: @zerollup/ts-transform-paths

https://www.npmjs.com/package/@zerollup/ts-transform-paths

This rewrites the import paths to working relative paths in the output JavaScript during TypeScript's compilation process. This helps solve the problem for other projects referencing the library. Like tsconfig-paths, this can be used with ts-node to get test frameworks to run, so they're partially redundant for that purpose. You'll find npm scripts illustrating both approaches in these projects.

Here are a handful of other options I found, but ultimately rejected in preference for the above:

https://www.npmjs.com/package/ts-transformer-imports

https://www.npmjs.com/package/ts-absolute-paths-transformer

https://www.npmjs.com/package/tscpaths

https://www.npmjs.com/package/ts-transform-import-path-rewrite

https://www.npmjs.com/package/tspath

https://www.npmjs.com/package/ts-transform-paths

https://www.npmjs.com/package/babel-plugin-module-resolver

Other references:

https://github.com/Microsoft/TypeScript/issues/15479

https://github.com/Microsoft/TypeScript/issues/23701

https://stackoverflow.com/questions/53647638/simple-absolute-import-causing-cannot-find-module-error/53691493#53691493