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

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

Closed
nakamorichi opened this issue Apr 30, 2017 · 92 comments
Closed

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

nakamorichi opened this issue Apr 30, 2017 · 92 comments
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript

Comments

@nakamorichi
Copy link

nakamorichi commented Apr 30, 2017

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 baseUrl and paths options and adding a new --rewriteAbsolute option?

Related

#5039
#12954

@aluanhaddad
Copy link
Contributor

aluanhaddad commented May 5, 2017

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
Copy link
Contributor

ikokostya commented May 5, 2017

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
Copy link
Author

@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
Copy link
Author

@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
Copy link
Contributor

ikokostya commented May 6, 2017

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
Copy link
Contributor

aluanhaddad commented May 7, 2017

@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
Copy link
Author

@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
Copy link
Contributor

@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 RyanCavanaugh added Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds labels May 9, 2017
@RyanCavanaugh
Copy link
Member

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
Copy link
Author

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

morlay commented May 10, 2017

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: #11441)

@azarus
Copy link

azarus commented Jun 4, 2017

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
Copy link
Author

@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
Copy link
Author

nakamorichi commented Jun 9, 2017

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
Copy link

azarus commented Jun 9, 2017

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
Copy link

kayjtea commented Aug 3, 2017

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
Copy link

vakhtang commented Sep 6, 2017

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
Copy link

0x80 commented Jan 6, 2018

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

azarus commented Jan 6, 2018 via email

@0x80
Copy link

0x80 commented Jan 6, 2018

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

azarus commented Jan 6, 2018

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

duffman commented Jan 14, 2018

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
Copy link

0x80 commented Jan 14, 2018

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

@stereobooster
Copy link

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.

@RacerDelux
Copy link

Another issue is when you use typescript with the msbuild typescript package.
I don't have NPM, I don't use NPM build pipeline, I don't use webpack.
I just want the output javascript files to append the base url (as an option).
What is the argument against this setting in compilerOptions?:
"appendBaseUrl" : true or false

This would allow us to use relative paths just fine.
import { Something } from "global.js";
Would become:
import { Something } from "./global.js";
With these options in tsconfig:
"compilerOptions": { "baseUrl": "./", "appendBaseUrl": true }

@loopingz
Copy link

loopingz commented Jun 8, 2022

So I find this issue interesting annoying,

I do understand the concept of not modifying the JS but.:

When I have a target with es5, you transpile every await/async, import etc to the promise/require, the generated code does not look at all like my .ts files.
Yet on this one to just add the extension at the end of the import, seems to violate the typescript principle.

I must admit I am confused

NathanVss added a commit to openfun/cunningham that referenced this issue Dec 30, 2022
Relatives import are quite ugly and reduces readability, but
hard to say that Typescript handles it very well for compilation
time, but nothing for runtime. That's why I had to add tsc-alias
to the build script. Please see this issue for more details.
microsoft/TypeScript#15479
Furthermore, some configuration was needed for Jest to work well.
NathanVss added a commit to openfun/cunningham that referenced this issue Jan 5, 2023
Relatives import are quite ugly and reduces readability, but
hard to say that Typescript handles it very well for compilation
time, but nothing for runtime. That's why I had to add tsc-alias
to the build script. Please see this issue for more details.
microsoft/TypeScript#15479
Furthermore, some configuration was needed for Jest to work well.
NathanVss added a commit to openfun/cunningham that referenced this issue Jan 5, 2023
Relatives import are quite ugly and reduces readability, but
hard to say that Typescript handles it very well for compilation
time, but nothing for runtime. That's why I had to add tsc-alias
to the build script. Please see this issue for more details.
microsoft/TypeScript#15479
Furthermore, some configuration was needed for Jest to work well.
NathanVss added a commit to openfun/cunningham that referenced this issue Jan 11, 2023
Relatives import are quite ugly and reduces readability, but
hard to say that Typescript handles it very well for compilation
time, but nothing for runtime. That's why I had to add tsc-alias
to the build script. Please see this issue for more details.
microsoft/TypeScript#15479
Furthermore, some configuration was needed for Jest to work well.
@ArcticLampyrid
Copy link

For anyone who may concern this:
I've found a custom transformer: LeDDGroup/typescript-transform-paths
We can load it with ts-loader or awesome-typescript-loader by wrting such config:

const createTsTransformPaths = require('typescript-transform-paths').default;
{
    test: /\.tsx?$/,
    use: "ts-loader",
    use: {
        loader: "ts-loader",
        options: {
            getCustomTransformers: (program) => ({
                before: [createTsTransformPaths(program, {})],
                afterDeclarations: [createTsTransformPaths(program, {
                    afterDeclarations: true
                })]
            })
        }
    },
    exclude: /node_modules/
}

Desplandis added a commit to EIG6-Geocommuns/itowns that referenced this issue Feb 28, 2023
Workaround microsoft/TypeScript#15479 to generate definitions with
relative imports. Use `ts-patch` to patch the `tsc` binary.
micnil added a commit to micnil/vscode-diff that referenced this issue Sep 19, 2023
Cant make it work after building otherwise:
microsoft/TypeScript#15479
@joecarl
Copy link

joecarl commented Oct 27, 2023

Design goal number 7 says

Preserve runtime behavior of all JavaScript code.

When you write the JavaScript line

var n = 10;

TypeScript says "This is JavaScript code" and will always emit

var n = 10;

When you write JavaScript code, TypeScript says that you wrote JavaScript code, and the behavior of this is preserved.

I'm quoting this comment here because I come from this other issue

I know the issue discussed here is not exactly about what I'm going to say but here it goes:

Look at this typical import statement:

import MyClass from '../utils/MyClass'

Is that javascript code? Well, yes it is. But the problem here is that each language will interpret this line differently!

And when you transpile code shouldn't it be in a way so the transpiled code is interpreted exactly in the same way the original code was?

It's not about changing JavaScript semantics but rather aligning TypeScript's behavior with JavaScript to ensure consistent interpretation.

I don't know, maybe the followingTypescript dev team principle should be reconsidered.

We are not going to implement features, even under a commandline flag, that imply changing JS semantics by rewriting it during emit

@RyanCavanaugh
Copy link
Member

It's not about changing JavaScript semantics but rather aligning TypeScript's behavior with JavaScript to ensure consistent interpretation.

This is exactly why we don't modify the paths - so that it's never a potential source of disagreement between you, TypeScript, and the runtime.

The new module documentation covers this in exhaustive detail and I'd recommend reading it.

I don't know, maybe the followingTypescript dev team principle should be reconsidered.

The problem here is not that we haven't thought about it over the hundreds of comments we've posted and thousands of comments we've read.

@RacerDelux
Copy link

@RyanCavanaugh If your goal is to stick to the JS standards, then please do that.
This means you should by default assume any import is referring to an es6 module, not a node module, and treat it as such.
I get that when typescript came out we didn't have modules outside of node, but we do now.

As it stands, by not wanting to disagree with the runtime, you mean "do not disagree with the node runtime".

We NEED the ability to tell the compiler we are running in a node or es environment, as the way imports are handled in those two environments are fundamentally different.

gregsexton added a commit to gregsexton/skdb that referenced this issue Jan 26, 2024
These are now correctly produced and work. But the path stuff does
still need to be stripped. It is not supported and requires downstream
projects to also define the paths. Refer:

https://stackoverflow.com/a/52501384
microsoft/TypeScript#15479
gregsexton added a commit to gregsexton/skdb that referenced this issue Jan 29, 2024
These are now correctly produced and work. But the path stuff does
still need to be stripped. It is not supported and requires downstream
projects to also define the paths. Refer:

https://stackoverflow.com/a/52501384
microsoft/TypeScript#15479
4leite added a commit to Tohuhono/Oberon that referenced this issue May 9, 2024
* revert to relative paths for libraries rather than introduce more build steps microsoft/TypeScript#15479

* clean up packages
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests