Why does TypeScript compiler convert top-level CommonJS paths to relative paths?

Topics: General
Aug 7, 2013 at 7:55 PM
Hi,

I'm currently trying to start converting our codebase from JS to TS and there is one thing about typescript modules which makes it inconvenient. In our code we use CommonJS-style imports with top-level paths (i.e. all paths are relative to the top-level project directory). However tsc converts all top-level path to relative paths which renders them incorrect. For example consider the following listing:
# cat a/a.ts
import b = require("b/b");

export class A {
  public foo() : b.B {
    return new b.B();
  }
}
# cat b/b.ts
export class B {
  public foo(): void {
  }
}
# tsc -m commonjs a/a.ts
# cat a/a.js
var b = require("./b/b");

var A = (function () {
    function A() {
    }
    A.prototype.foo = function () {
        return new b.B();
    };
    return A;
})();
exports.A = A;
You can see here that require("b/b") was converted to require("./b/b"). There is a snippet of code in the TypeScript compiler that specifically does that (in emitter.ts):
modPath = isAmbient ? modPath : (!isRelative(stripQuotes(modPath)) ? quoteStr("./" + stripQuotes(modPath)) : modPath);
this.writeToOutput("require(" + modPath + ")");
What I don't understand is why this happens? And how can I get around it?

Thanks.
Coordinator
Aug 7, 2013 at 11:12 PM
From the Node.js docs:

"A module prefixed with '/' is an absolute path to the file. For example, require('/home/marco/foo.js') will load the file at /home/marco/foo.js.
A module prefixed with './' is relative to the file calling require(). That is, circle.js must be in the same directory as foo.js for require('./circle') to find it.
Without a leading '/' or './' to indicate a file, the module is either a "core module" or is loaded from a node_modules folder. "

I suspect we prepend the ./ because we don't yet support creating core modules or loading from the node_modules folder.
Aug 8, 2013 at 10:23 AM
I see, thanks. Node also has support for a NODE_PATH environment variable, which is sometimes useful, for example in my case. Anyway, isn't the current behavior of tsc broken? I mean, when it loads a/a for compilation, it correctly resolves the reference to b/b and typechecks. But the code that it emits is not going to work - blindly adding "./" to all require paths is only going to break them. Why won't it leave require paths as they are, and delegate it to a programmer to correctly set up NODE_PATH and directory structure for the output files?
Coordinator
Aug 8, 2013 at 4:40 PM
It assumes, I believe, that "a/a" and "./a/a" are the same thing since it doesn't support the other kind of lookup. When it looks for matching .ts or .d.ts, it's going to assume that's a relative path rather than one that is a core module or something in the node_modules folder. You should generally just use the format it supports, which is the "./a/a" rather than the "a/a" format.
Aug 8, 2013 at 4:52 PM
But this is exactly my point - "a/a" and "./a/a" are not the same thing for tsc. It correctly resolves the former relative to the project root. If you instead use "./a/a" it will complain that the file does not exists, which is also correct. What is wrong, is the generated JavaScript file, and it is inconsistent with the behavior of tsc when it resolves the requirements.

For that matter, the typescript specification also suggests that "a/a" and "./a/a" should be treated differently:
  • External module names may be “relative” or “top-level”. An external module name is “relative” if
    the first term is “.” or “..”.
  • Top-level names are resolved off the conceptual module name space root.
Coordinator
Aug 8, 2013 at 4:55 PM
Ah, sorry, I think I understand now. Sounds like a bug. I'll copy this over to a work item.
Coordinator
Aug 8, 2013 at 4:55 PM
This discussion has been copied to a work item. Click here to go to the work item and continue the discussion.