I'm building a full-stack web application in a monorepo managed by NX, the back-end server is a NodeJS/Express monolith written in typescript (this app was created using nx g @nx/node:application app
).
The structure of the monorepo is as follows
root/
apps/
api/
config/
config.ts // This builds a config object from env vars, etc...
index.ts // index file exporting config object
src/
routes/..
resources/..
app.ts
init.ts
main.ts // entry point
tsconfig.app.json // extends ./tsconfig.json
tsconfig.json // extends ../../tsconfig.base.json
project.json
package.json
tsconfig.base.json
For local development I'm using the generated nx serve app
command, compilation is successful however when node tries to run the compiled Javascript I'm greeted with this error:
Error: Directory import 'dist/apps/api/apps/api/config' is not supported resolving ES modules imported from dist/apps/api/apps/api/src/main.js
at new NodeError (node:internal/errors:406:5)
at finalizeResolution (node:internal/modules/esm/resolve:227:11)
at moduleResolve (node:internal/modules/esm/resolve:845:10)
at defaultResolve (node:internal/modules/esm/resolve:1043:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:383:12)
at ModuleLoader.resolve (node:internal/modules/esm/loader:352:25)
at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:228:38)
at ModuleLoader.import (node:internal/modules/esm/loader:315:34)
at importModuleDynamically (node:internal/modules/cjs/loader:1164:37)
at importModuleDynamicallyWrapper (node:internal/vm/module:431:21)
I've not seen this error in past typescript apps I've written. So I created a reference app, in the same monorepo, generated with nx g @nx/node:application reference
to see if the same error would appear. I structured it like this:
root/
apps/
reference/
foo/
foo.ts
index.ts
src/
main.ts
// the default generated tsconfig's from nx
Code:
reference/foo/foo.ts
export const foo = () => console.log('Foo');
reference/foo/index.ts
export * from './foo';
reference/src/main.ts
import * as Foo from '../foo';
Foo.foo();
I've also tried refactoring the import to target the file directly: import { config } from '../config';
-> import { config } from '../config/config';
. But I end up with this error:
Error: Cannot find module 'dist/apps/api/apps/api/config/config' imported from dist/apps/api/apps/api/src/main.js
at new NodeError (node:internal/errors:406:5)
at finalizeResolution (node:internal/modules/esm/resolve:233:11)
at moduleResolve (node:internal/modules/esm/resolve:845:10)
at defaultResolve (node:internal/modules/esm/resolve:1043:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:383:12)
at ModuleLoader.resolve (node:internal/modules/esm/loader:352:25)
at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:228:38)
at ModuleLoader.import (node:internal/modules/esm/loader:315:34)
at importModuleDynamically (node:internal/modules/cjs/loader:1164:37)
at importModuleDynamicallyWrapper (node:internal/vm/module:431:21)
The reference app built and ran without error. So my guess is that something is wrong with my TypeScript configuration in my api app. I can't find anything online short of implementing babel to transpile my compiled JS - I shouldn't have to do this as reference works fine. I've included the config for api below.
tsconfig.base.json
{
"compileOnSave": false,
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"allowJs": true,
"allowUnusedLabels": false,
"alwaysStrict": true,
"baseUrl": ".",
"declaration": false,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"importHelpers": true,
"importsNotUsedAsValues": "remove",
"incremental": true,
"isolatedModules": true,
"noImplicitOverride": false,
"noImplicitReturns": false,
"noUnusedLocals": true,
"strict": true,
"rootDir": ".",
"sourceMap": true,
"moduleResolution": "Node",
"target": "ES2020",
"module": "esnext",
"lib": [
"es2020",
"dom"
],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"resolveJsonModule": true
},
"exclude": [
"node_modules",
"tmp"
]
}
apps/api/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"compilerOptions": {
"esModuleInterop": true
}
}
apps/api/tsconfig.app.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": [
"node"
]
},
"exclude": [
"jest.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts"
],
"include": [
"src/**/*.ts",
"config/**/*.ts"
]
}
It's worth mentioning that main.ts
imports config
asynchronously in an initialization function: const { config } = await import('../config');
. In the compiled javascript tsc keeps it as:
const { config } = await import("../config");
Whereas for the reference app, the import is converted to:
var foo = __toESM(require("../foo"));
Any help would be incredibly appreciated!