nestjs / typescript-starter

Nest framework TypeScript starter :coffee:
https://nestjs.com
1.86k stars 1.05k forks source link

Absolute import paths cannot be used in production #74

Closed dislick closed 5 years ago

dislick commented 5 years ago

Issue

Absolute import paths like import { foo } from 'src/utils/foo' work great with ts-node, but fail when running npm run start:prod.

Error

internal/modules/cjs/loader.js:583
    throw err;
    ^

Error: Cannot find module 'src/utils/foo'
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:581:15)
    at Function.Module._resolveFilename (/Users/patrick/r/typescript-starter/node_modules/tsconfig-paths/lib/register.js:75:40)
    at Function.Module._load (internal/modules/cjs/loader.js:507:25)
    at Module.require (internal/modules/cjs/loader.js:637:17)
    at require (internal/modules/cjs/helpers.js:22:18)
    at Object.<anonymous> (/Users/patrick/r/typescript-starter/dist/main.js:13:15)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)

Here is a fork of typescript-starter with minimal changes to reproduce the issue. Compare changes.

The start:prod command is already changed to include the tsconfig-paths/register module, from

{
  "start:prod": "node dist/main.js"
}

to

{
  "start:prod": "node -r tsconfig-paths/register dist/main.js"
}

unfortunately without any effect.

dantman commented 5 years ago

Is this actually a supported way of running NestJS?

If it is, in addition to production it also does not appear to work in tests.

dislick commented 5 years ago

If it isn't supported we should remove tsconfig-paths from the starter repo. I was not even 100% aware I was using such a feature because of auto-imports in VSCode.

dantman commented 5 years ago

Also remove baseUrl from the tsconfig. I found this issue because I was going to open an issue asking for baseUrl to be removed because it causes vscode to make src/ imports. Which apparently is an intended feature of baseUrl. But not supported by the jest config to my knowledge (I'm pretty certain that import failures in jest for src/ imports is how I found they were being inserted).

kamilmysliwiec commented 5 years ago

You should never use such absolute imports src/utils/foo in your app because eventually, your code will very likely end up in a different directory (for example, dist). However, baseUrl is required in order to enable tsconfig paths which basic setup is shipped together with the starter project. Nonetheless, we don't force anybody to use them, even though it's a recommended way (it's always up to you).

dantman commented 5 years ago

However, baseUrl is required in order to enable tsconfig paths which basic setup is shipped together with the starter project. Nonetheless, we don't force anybody to use them, even though it's a recommended way (it's always up to you).

Is there any way tsconfig paths can be enabled without baseUrl? Or can we at least warn new users of the side effects.

Because baseUrl explicitly enables those src/ absolute imports. And as a result development tools output those absolute paths because tsconfig has told them you want them to be output.

kamilmysliwiec commented 5 years ago

@dantman Unfortunately, no. I have been struggling with the same issue as well and this is actually unbearable in the long run. I hope that IDEs will provide better integration with TS options soon.

dantman commented 5 years ago

@kamilmysliwiec Since you're having this issue to (in vscode I presume), could you try setting "javascript.preferences.importModuleSpecifier": "relative" and see if it does anything.

kamilmysliwiec commented 5 years ago

Thanks @dantman. However, I believe that it will disable typescript-paths feature which, on the other hand, are very useful.

dantman commented 5 years ago

@kamilmysliwiec Can you confirm if that is the case, I don't have any typescript-paths to test. If it is then I'll try to get the other bug reopened.

muyu66 commented 5 years ago

try create a index.js in root path

require("ts-node/register"); require("./src/main");

then, node -r tsconfig-paths/register index.js

korniychuk commented 5 years ago

The problem is that we don't have src directory in the dist after compilation.

This is the reason why imports like import ... from 'src/...' don't work in case compiling via tsc(when you making a build) or tsc-watch (when you running the app in dev mode).

There are several solutions:

  1. Add rootDir to your tsconfig.json to avoid omitting src directory during compilation. 1.1. tsconfig.json:

      {
        "compilerOptions": {
          "baseUrl": "./",
        }
      }

    1.2 Add src to scripts in the package.json:

      "start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node dist/src/main.js\"",

    Notice: imports like require('../ormconfig.json') from the root of project will not work. It looks like not a problem because these imports is a bad practice because in this case dist loses independence.

  2. Provide mapping without src in dist natively. 2.1. Add NODE_PATH=dist prefix to your scripts in package.json.

      "start": "ts-node -r tsconfig-paths/register src/main.ts",
      "start:dev": "NODE_PATH=dist tsc-watch -p tsconfig.build.json --onSuccess \"node dist/main.js\"",
      "start:prod": "NODE_PATH=dist node dist/main.js",

    Notice: don't add NODE_PATH to ts-node ... commands. 2.2. Replace "baseUrl": "./", with "baseUrl": "./src", in the your tsconfig.json 2.3. When you import a file from the root omit src/. Example: import { ... } from 'utils/foo'; instead of src/utils/foo.

  3. Provide mapping without src in dist using module-alias package. 3.1. Install the package. npm i --save module-alias or yarn add module-alias. 3.2. Add next code to the end of package.json

    "_moduleAliases": {
      "@app": "./dist"

    }

    3.3. Add paths to compileOptions in the tsconfig.json

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

    3.4. The package will break execution via ts-node. Therefore we need to udpate start command in the package.json

    "start": "IS_TS_NODE=true ts-node -r tsconfig-paths/register src/main.ts",

    3.5. Add next code to the top of src/main.ts file:

    if (!process.env.IS_TS_NODE) { // tslint:disable-next-line:no-var-requires require('module-alias/register'); }

    3.6. Write your imports like import { ... } from '@app/utils/foo'; instead of src/utils/foo.

    Notice: You can define multiple aliases ;-) For example, @utils for src/utils

PS: I prefer the third solution.

Warning: Previously proposed solution is a bad solution!

try create a index.js in root path require("ts-node/register"); require("./src/main"); then, node -r tsconfig-paths/register index.js

Because this is the same to ts-node -r tsconfig-paths/register src/main.ts, but via a hack. In this case, we use ts-node to compile TS to JS on the fly. This is good for development reasons, however fatal for production because of broken performance.

Notes: tsconfig-paths issues:

AndrewShwets commented 4 years ago

@korniychuk Thank you so much! Third solution is awesome.

korniychuk commented 4 years ago

The problem is that we don't have src directory in the dist after compilation. ...

  1. Provide mapping without src in dist using module-alias package. 3.1. Install the package. npm i --save module-alias or yarn add module-alias. 3.2. Add next code to the end of package.json

    "_moduleAliases": {
      "@app": "./dist"
    }

    3.3. Add paths to compileOptions in the tsconfig.json

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

    3.4. The package will break execution via ts-node. Therefore we need to udpate start command in the package.json

    "start": "IS_TS_NODE=true ts-node -r tsconfig-paths/register src/main.ts",

    3.5. Add next code to the top of src/main.ts file:

    if (!process.env.IS_TS_NODE) {
     // tslint:disable-next-line:no-var-requires
     require('module-alias/register');
    }

    3.6. Write your imports like import { ... } from '@app/utils/foo'; instead of src/utils/foo. Notice: You can define multiple aliases ;-) For example, @utils for src/utils

PS: I prefer the third solution.

The third solution can be improved. We can:

To do this we need to write a simple script for jest and module-alias to load paths directly from tsconfig.json.

The full solution you can find in this fork of the original starter: https://github.com/korniychuk/nestjs-starter

microcipcip commented 4 years ago

I've finally got it working without having to specify multiple aliases, and it works in production, here are the steps for my future self:

  1. Set the following parameters to tsconfig.json

    {
    "compilerOptions": {
    //...other
    "outDir": "./dist",
    "baseUrl": ".",
    "paths": {
      "@src/*": ["src/*"]
    },
    "esModuleInterop": true
    }
    }
  2. Install module-alias:

    npm i module-alias
    npm i -D @types/module-alias
  3. Create the file aliases.ts on ./src/config/aliases.ts with this code:

    
    import moduleAlias from 'module-alias';
    import path from 'path';

const rootPath = path.resolve(__dirname, '..', '..', 'dist'); moduleAlias.addAliases({ '@src': rootPath, });


4. Import the newly created file on `./src/main.ts`, note that this has to be the FIRST import:

import './config/aliases'; import { NestFactory } from '@nestjs/core'; // etc


5. For the jest config (untested):

/ eslint-disable / const { pathsToModuleNameMapper } = require('ts-jest/utils'); const { compilerOptions } = require('./tsconfig');

module.exports = { preset: 'ts-jest', rootDir: '.', testEnvironment: 'node', moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/', }), };



Done! Basically the idea here is that with `module-alias` we can change the import url on runtime, so we can effectively resolve for a different path from `dev` mode to `prod` mode.

Now we can import with absolute paths, for example with `@src/auth/auth.repository.ts`
pxmage commented 4 years ago

@microcipcip Thanks! You just saved my ass XD.

felipegouveiae commented 4 years ago

@kamilmysliwiec Since you're having this issue to (in vscode I presume), could you try setting "javascript.preferences.importModuleSpecifier": "relative" and see if it does anything.

This one worked for me perfectly.

ninthsun91 commented 1 year ago

@microcipcip Thanks, this finally worked. But I still don't get it why some Nestjs projects work fine without any external library installed such as module_alias, and some Nestjs projects fail to resolve alias path.

microcipcip commented 1 year ago

@microcipcip Thanks, this finally worked. But I still don't get it why some Nestjs projects work fine without any external library installed such as module_alias, and some Nestjs projects fail to resolve alias path.

If for some projects works it may be that they have solved it with a slightly different configuration like point 2.0 of this answer.