Typescript: Using baseUrl and paths on node

Created on 20 Jun 2016  路  19Comments  路  Source: microsoft/TypeScript

I notice the feature is for requirejs and systemjs. And as this comment said, the emitted js code is unchanged by the configuration:
https://github.com/Microsoft/TypeScript/issues/5039#issuecomment-206451221

So the primary usage is to resolve packages in various locations (e.g. jspm_packages) for transpilation.

I have the following interesting use case. It worked for me so far as I was using jspm, i.e. the actual module loading happens on the browser. However it fails when I try to do the same using ts-node (fail because it runs on node and the reason above).

Is there a way to support this?

Here is the example:

// tsconfig.json
{
  "compileOptions": {
    "baseUrl": ".",
    "paths": {
      "my-package": [ "src/" ]  // i.e. mapping the package name to the source folder
    }
  }
}

// test/sometest.ts
import abc from 'my-package';
...

i.e., using paths so tests can reference the source as if it is a regular module.
馃尫

Reference issue on ts-node: https://github.com/TypeStrong/ts-node/issues/138

External

Most helpful comment

Typescript should give an option to allow "directory expansion" for path alias defined in "tsconfig.json" and used in module importation. it would be nice to have something like:

{
    "compilerOptions": {
          "expandImport":true,
       "paths": {
            "@fooPath/*":["./path/to/manyfoos/*"]
            }
     }
}

TS Code:
import {Foo} from "@fooPath/MyFoo"

Compilation:
const Foo = require("path/to/manyfoos/MyFoo");

All 19 comments

this is already supported in TS today. if you run tsc it should resolve your module. i would say this is specific to how ts-node works.

Yes. Tsc compiles successfully. But as you mentioned in the past, there is no change(path rewrite) on the emitted js files. If I run the test js file through node, would result in the same error

But as you mentioned in the past, there is no change(path rewrite) on the emitted js files. If I run the test js file through node, would result in the same error

But that is the purpose of these settings, to allow module loaders than can remap modules to remap modules. The CommonJS loader in NodeJS was not designed as a module loader that is configurable in this way, because the base assumption is that you would have access to a file system with a very deterministic resolution pattern for resolving modules.

I personally would not want TypeScript to re-write MIDs, but giving the flexibility to resolve modules in the way my loader will resolve modules at run-time, which TypeScript 2.0 does perfectly.

Especially with CJS under NodeJS, because the default loader is not configurable in that sense, any remapping by TypeScript would require making invalid assumptions about the run-time environment and likely result in code that does not work at run-time and certainly not a distributable package that could be consumed by anyone using npm.

Because of the assumptions made by the CJS loader in NodeJS we distribute our packages in UMD format and recommend people use an AMD loader that easily allows remapping, even when running under NodeJS. But knowing that there is a "cowpath" to CJS we include an index.js in the root of our packages which loaders a transpiled main.ts (which is the "default" for AMD) which exports the public API of the package. We use dts-generator to create a .d.ts where the main module for the package gets exported as the package name, therefore providing the types when the package is required. It does mean that those loading only under CJS are limited to the main module, while those using AMD or another configurable loader can reference each module individually.

But that is the purpose of these settings

Fully understand that is how it should be used, just that I found this use case interesting because it creates a mental disconnect between what's appear to be working on the IDE but failed on runtime.

One option is to add a mapPathForNode flags that will create a separate js file that does the remapping under node (I'm not excel in node so I don't know if this is possible).
Just brainstorming.

To make sure I deliver my message correctly, I don't want to suggest a feature that doesn't have a real use case.

In this case, it is like "I abuse the system and complain it doesn't work". Fully aware of that. Just want to open the discussion to see if this would lead to some interesting possibilities.

馃尫

One option is to add a mapPathForNode flags that will create a separate js file that does the remapping under node (I'm not excel in node so I don't know if this is possible).

That seem clearly the domain for tooling outside of the scope of TypeScript. Especially considering the design goals:

3) Impose no runtime overhead on emitted programs.
7) Preserve runtime behavior of all JavaScript code.

And anti-goal:

5) Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.

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 existsCache = {d:0}; delete existsCache.d
  const baseUrl = path.dirname(process['mainModule'].filename)
  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)
  }
})()

@papercuptech This is brilliant. I modified your code and it worked like a charm in my isomorphic react project.

(function () {
  const baseUrl = path.join(cwd, tsConfig.compilerOptions.baseUrl);
  const moduleProto = Object.getPrototypeOf(module);
  const origRequire = moduleProto.require;
  const pathKey = {};
  for (const [key, value] of Object.entries(tsConfig.compilerOptions.paths)) {
    pathKey[key.slice(0, -2)] = value[0].slice(0, -2);
  }
  moduleProto.require = function (request) {
    if (request[0] === '@') {
      const pathName = request.match(/(@.*?)\//)[1];
      const relativeToBase = path.relative(path.dirname(this.id), baseUrl);
      existsPath = relativeToBase + '/' + pathKey[pathName] +  request.slice(pathName.length);
      return origRequire.call(this, existsPath || request)
    }
    return origRequire.call(this, request);
  };
})();

Here I'm assuming that the tsconfig.json is like

    "paths": {
      "@pages/*": ["./ui/pages/*"],
      "@components/*": ["./ui/components/*"],
    },

where the keys in the paths start with @. If someone needs it to be more general, modifications could be made further.

@papercuptech So many people on the internet want this. Would be a neat idea to implement this into a stable package possibly.

Maybe you'd be interested in https://github.com/jonaskello/tsconfig-paths?

Maybe you'd be interested in https://github.com/jonaskello/tsconfig-paths?

Yes. Thank you.

Maybe you'd be interested in https://github.com/jonaskello/tsconfig-paths?

The problem with this is, that tsconfig-paths or require hook and tsconfig.json also have to be used in production, where it is preferable to have just TS build output results. I think TS compiler here could really help to get rid of excessive tooling overhead and double mapping configuration. It could be made configurable and of course optional.

Here is a link to another issue about it https://github.com/Microsoft/TypeScript/issues/18951

Re-writing module names is really quite dangerous and not something TypeScript should involve itself with. Providing the right tooling to make what is written work like it would at run-time under different loaders is the most logical solution. TypeScript is not a module loader. While the default loader with NodeJS does not support remapping, there are many loaders and bundlers that run under NodeJS that provide a lot of flexibility and configuration. We should tools what they are designed for.

Probably, it would be good to have open plugin API for the compiler (there is one not stable yet) and convenient way of applying plugins to the standard compilation process (there is none as I'm aware of), something like babel has. This would allow solving such tasks.

Typescript should give an option to allow "directory expansion" for path alias defined in "tsconfig.json" and used in module importation. it would be nice to have something like:

{
    "compilerOptions": {
          "expandImport":true,
       "paths": {
            "@fooPath/*":["./path/to/manyfoos/*"]
            }
     }
}

TS Code:
import {Foo} from "@fooPath/MyFoo"

Compilation:
const Foo = require("path/to/manyfoos/MyFoo");

Oh wow I just got burned by this - thought it was a shortcut for easy readability.

Is there an open request for similar functionality to paths but like shortcuts instead?

@arogozine you could instead try to use one of the following three strategies:

1) Using Webpack to compile your project, webpack already expands your path alias, but only for web app (html, css, js, images):
https://webpack.js.org/guides/typescript/

2) Using a typescript alias path loader for node.js projects:
https://www.npmjs.com/package/@momothepug/tsmodule-alias

3) Avoid using path alias and instead define index.ts for each module you have like in:
https://github.com/palantir/tslint/blob/master/src/index.ts

You can define an "index.ts" inside your "module" or "submodule directory", then you should "export" many "alias import" as you want, for instance, if you have an "AnimalModule" with an structure like:

AnimalModule/
------DogModule/
------------dogs/
------------------------kind1/dog1.ts
------------------------kind2/dog2.ts
------------------------kind3/dog3.ts
------CatModule/
------------cats/
------------------------kind1/cat1.ts
------------------------kind2/cat2.ts
------------------------kind3/cat3.ts
------IguanaModule/
------------iguanas/
------------------------kind1/iguana1.ts
------------------------kind2/iguana2.ts
------------------------kind3/iguana3.ts

// DogModule/index.ts:

import * as dog1 from "./dogs/kind1/dog1.ts";
import * as dog2 from "./dogs/kind2/dog2.ts";
import * as dog3 from "./dogs/kind3/dog3.ts";
export {dog1, dog2, dog3};

// CatModule/index.ts:

import * as cat1 from "./cats/kind1/cat1.ts";
import * as cat2 from "./cats/kind2/cat2.ts";
import * as cat3 from "./cats/kind3/cat3.ts";
export {cat1, cat2, cat3};

// IguanaModule/index.ts:

import * as iguana1 from "./iguanas/kind1/iguana1.ts";
import * as iguana2 from "./iguanas/kind2/iguana2.ts";
import * as iguana3 from "./iguanas/kind3/iguana3.ts";
export {iguana1, iguana2, iguana3};

Then you can use any module defined before like this:
// MyApp.ts

import {iguana1} from "./AnimalModule/IguanaModule"
import {cat3} from "./AnimalModule/CatModule"
import {dog2} from "./AnimalModule/DogModule"

And Also you can now define submodules declarations inside a parent module like the following example:

// AnimalModule/index.ts:

import * as dogs from "./DogModule";
import * as cats from "./CatModule";
import * as iguanas from "./IguanaModule";
export {dogs, cats, iguanas};

Using AnimalModule definition like this:
// MyApp.ts

import {iguanas, cats, dogs} from "./AnimalModule"
// iguanas.iguana1
// cats.cat3
// dogs.dog2

UPDATE:

As @unional said, it's better to re-export:

export * from './cats/kind1/cat1.ts'
export * from './cats/kind2/cat2.ts'
export * from './cats/kind3/cat3.ts'

As a side note, it would be better to not rely on namespacing. i.e.

// instead of
import * as cat1 from "./cats/kind1/cat1.ts";
import * as cat2 from "./cats/kind2/cat2.ts";
import * as cat3 from "./cats/kind3/cat3.ts";
export {cat1, cat2, cat3};

// just re-export
export * from './cats/kind1/cat1.ts'
export * from './cats/kind2/cat2.ts'
export * from './cats/kind3/cat3.ts'

Tree shaking and namespacing doesn't work well together.

@unional awesome! It's a better way, thanks.

Was this page helpful?
0 / 5 - 0 ratings