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

Cannot use relative import paths with RequireJS #1567

Closed
eschwartz opened this issue Dec 26, 2014 · 8 comments
Closed

Cannot use relative import paths with RequireJS #1567

eschwartz opened this issue Dec 26, 2014 · 8 comments

Comments

@eschwartz
Copy link

I am trying to convert a RequireJS project over to TypeScript, and I'm running into a problem with relative AMD module paths, where generated TypeScript files are not playing nicely with RequireJS.

TL;DR: RequireJS does not support using relative paths from more than one location. TypeScript only supports relative import paths.

Here's what I've got going on:

app/
- MyClass.ts
- MyClass.js
- AnotherClass.ts
- AnotherClass.ts
tests/
- MyClassTest.ts
- MyClassTest.js
// app/MyClass.ts
class MyClass = {
  // ...
}
export = MyClass;

// app/MyClass.js
define(["require", "exports"], function (require, exports) {
    var MyClass = (function () {
        function MyClass() {
        }

        // ...       

        return MyClass;
    })();
    return MyClass;
});

// app/AnotherClass.ts
import MyClass = ('./MyClass')
class AnotherClass = {
  // Do some stuff that uses MyClass...
}
export = AnotherClass;

// app/AnotherClass.js
define(["require", "exports", "./MyClass"], function (require, exports, MyClass) {
    var AnotherClass = (function () {
        function AnotherClass() {
        }

         // Do some stuff that uses MyClass ...

        return AnotherClass;
    })();
    return AnotherClass;
});

// tests/MyClassTest.ts
import MyClass = require('../app/MyClass');
/// some tests...

// tests/MyClassTest.js
define(["require", "exports", "../app/MyClass"], function (require, exports, MyClass) {
    // some tests...
});

When I run tests/MyClassTest.js I get a RequireJS timeout error `Load timeout for module 'app/MyClass'.

From what I can find, RequireJS does not support using relative paths in this way. @JBurke recommends to always use a paths config to specify aliases or root paths, so that all modules can be referenced with the exact same id. My experience with RequireJS confirms this -- I've had the same issue if I reference a module with inconsistent casing, if I use two different path aliases to reference the same module, or if I mix relative paths with aliased module IDs.

I've seen some discussion related to this (#293). But I'm wondering if anyone has found a way to work around this issue. I can't imagine I'm the only one trying to use TypeScript with RequireJS.

@WorldMaker
Copy link

The current means to handling this is to use string-named modules in definition files:

module "amdmodulename" {
  import stuff = require('./relative/path/amdmodule');
  export = stuff;
}

Once something like that appears in a definition file you should be able to just require('amdmodulename'). (You can even use forward slashes in these module names and take advantage of deeper path structures.)

Many (albeit not all) of the definitions on DefinitelyTyped already have the most common AMD module name ready to import. For the ones that do not and for your own files it is relatively easy to add a modules.d.ts file in your project and build up module definitions like the above.

Proposals like #293 and similar would make this a bit easier, but maintaining these AMD module path definition files is reasonably simple.

There's also one other trick here, primarily used for require extensions:

/// <amd-dependency path="text!my/template.html"/>
var mytemplate: string = require('text!my/template.html');

That amd-dependency path has to match the require path. What this is doing is falling back from TypeScript's require system to require.js' "CommonJS fallback pattern" and as you can see any type information available to this file is embedded in the var declaration.

@eschwartz
Copy link
Author

Ok, that's kind of what I ended up doing.

The problem is that I'm trying to convert over a RequireJS project with hundreds of files. I ended up writing a build script which automatically generates a definition file with all of the AMD module ids.

For posterity, the definition file need to look like this:

declare module "my/amd/module" {
  import MyModule = require('path/to/my/amd/module');

  export = MyModule;
}

Note that you cannot use relative module paths:

declare module "my/amd/module" {
  import MyModule = require('./relative/path/to/my/amd/module');
   // --> error TS2439: Import declaration in an ambient external module declaration cannot reference external module through relative external module name.

  export = MyModule;
}

@benjamin-hg
Copy link

@eschwartz: yes, you cannot use relative module paths, but in some cases relative and 'absolute' paths look the same!
Here is an example:

app/
- MyClass.ts
- MyClass.js
- AnotherClass.ts
- AnotherClass.ts
- module-aliases.d.ts
tests/
- MyClassTest.ts
- MyClassTest.js

Let's say, we've configured our AMD loader so there are two packages for the app and test: 'appPackage' and 'testPackage'. (The loader will look for required module IDs starting with that package names and resolve the rest of the module ID relative to the package folder)

module-aliases.d.ts:

declare module "appPackage/MyClass" {
  import imp = require('MyClass'); // this behaves like a relative path, but tsc eats it as 'not relative'.
  export = imp;
}

tests/MyClassTest.ts:

/// <reference path="../app/module-aliases.d.ts" /> // that's the relative part, must match at compile time. 
import MyClass = require('appPackage/MyClass'); // resolves at compile time and at run time
// some tests...

This module-aliases.d.ts file could be generated with some assumptions about the file structure. I've done this for a larger project. It works quite well with Dojo module loader.

@eschwartz
Copy link
Author

Thanks, @WorldMaker @benjamin-hg.

This module-aliases.d.ts file could be generated with some assumptions about the file structure.

This is what I ended up doing on my project. I created a little grunt task, which generates a definitions file using a RequireJS style paths config. So far, it's doing it's job. The only real pain is that my IDE (PHPStorm) does not seem to follow the trail all the way through to the original module, which is a real pain.

@benjamin-hg
Copy link

Thanks for the quote, @eschwartz, but I'm not WorldMaker ;-)
BTW, I'm using WebStorm and I can navigate through my aliases ... it is jumping to the alias module of course, from there I reach the actual module -- so I need to click twice.

@eschwartz
Copy link
Author

Oops, sorry!

You expect me to click twice?! 😏

I really which JetBrains would open up their language plugins, so you could extend the behavior of built-in languages. Of course, then I have to learn how to write IntelliJIDEA plugins...

@electricessence
Copy link

Uhg. Any fix for this?
I'm finding it frustrating that I'm following a solid pattern of app vs test and I can't get it to work right.
But if I bundle them in a common folder, it works? :(

@bomzj
Copy link

bomzj commented Mar 15, 2017

https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html
Maybe declaring ///<amd-module name="Relative/Path/To/ModuleName"/> at the top of the *.ts file will help.

@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants