Absolute import paths cannot be used in production
dislick opened this issue ยท 18 comments
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.
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.
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.
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).
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).
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.
@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.
@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.
Thanks @dantman. However, I believe that it will disable typescript-paths
feature which, on the other hand, are very useful.
@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.
try create a index.js in root path
require("ts-node/register"); require("./src/main");
then, node -r tsconfig-paths/register index.js
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:
-
Add
rootDir
to yourtsconfig.json
to avoid omittingsrc
directory during compilation.
1.1.tsconfig.json
:{ "compilerOptions": { "baseUrl": "./", } }
1.2 Add
src
toscripts
in thepackage.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 casedist
loses independence. -
Provide mapping without
src
indist
natively.
2.1. AddNODE_PATH=dist
prefix to your scripts inpackage.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
tots-node ...
commands.
2.2. Replace"baseUrl": "./",
with"baseUrl": "./src",
in the yourtsconfig.json
2.3. When you import a file from the root omitsrc/
.
Example:import { ... } from 'utils/foo';
instead ofsrc/utils/foo
. -
Provide mapping without
src
indist
usingmodule-alias
package.
3.1. Install the package.npm i --save module-alias
oryarn add module-alias
.
3.2. Add next code to the end ofpackage.json
"_moduleAliases": { "@app": "./dist" }
3.3. Add
paths
tocompileOptions
in thetsconfig.json
"paths": { "@app/*": ["./src/*"] },
3.4. The package will break execution via
ts-node
. Therefore we need to udpatestart
command in thepackage.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 ofsrc/utils/foo
.Notice: You can define multiple aliases ;-) For example,
@utils
forsrc/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:
@korniychuk Thank you so much! Third solution is awesome.
The problem is that we don't have
src
directory in thedist
after compilation.
...
3. Provide mapping withoutsrc
indist
usingmodule-alias
package.
3.1. Install the package.npm i --save module-alias
oryarn add module-alias
.
3.2. Add next code to the end ofpackage.json
"_moduleAliases": { "@app": "./dist" }
3.3. Add
paths
tocompileOptions
in thetsconfig.json
"paths": { "@app/*": ["./src/*"] },
3.4. The package will break execution via
ts-node
. Therefore we need to udpatestart
command in thepackage.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 ofsrc/utils/foo
.
Notice: You can define multiple aliases ;-) For example,@utils
forsrc/utils
PS: I prefer the third solution.
The third solution can be improved.
We can:
- avoid
tsconfig-paths/register
dependency - avoid aliases duplication in 3 places (
tsconfig.json
,jest
config,module-alias
config). - avoid
IS_TS_NODE
variable
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
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:
- Set the following parameters to
tsconfig.json
{
"compilerOptions": {
//...other
"outDir": "./dist",
"baseUrl": ".",
"paths": {
"@src/*": ["src/*"]
},
"esModuleInterop": true
}
}
- Install
module-alias
:
npm i module-alias
npm i -D @types/module-alias
- 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,
});
- 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
- 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: '<rootDir>/',
}),
};
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
@microcipcip Thanks! You just saved my ass XD.
@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.
@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 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.