I have reorganized my project in NodeJS/Typescript using workspaces and project references. One of the workspaces (sub-project) is a NextJS application, and the recommended tsconfig.json
includes the following configuration property:
"noEmit": true
However, this option is not compatible with project references in Typescript. I am facing this situation where NextJS needs noEmit: true
because it handles the code transpilation and bundling internally, but Typescript only allows noEmit: false
when I run npx tsc --build
from the root directory.
This question does not require my tsconfig.json
or any other setup files because the source of this issue is that specific option. Is there a way to add a NextJS application to a composite project? Are there some sample projects?. The ideal answer would treat both CommonJS and ESmodule types, but I am more interested in the CommonJS module type.
I searched among the NextJS example projects but nothing. I found a similar GitHub issue, but unfortunately, it did not receive the deserved attention. Following this link from that topic, I found a paragraph about NextJS setup, but the article is 3 years old and the proposed solution is deprecated.
@JaredSmith correctly pointed out that a NextJS project probably does not need tsc
to emit JS code. The solution is to separately compile the other dependencies and let NextJS take care of the app build. This answer wants to be a template for the configuration of a composite project that includes NextJS.
Assuming you have just 2 projects (workspaces): api
and next-app
. next-app
depends on the api
project. This is the output of tree -L 1
:
.
├── jest.config.base.ts # see later
├── jest.config.ts # see later
├── node_modules/
├── package.json
├── package-lock.json
├── packages/
├── tsconfig.base.json # see later
└── tsconfig.json
The package.json
file contains the workspaces' folder:
{
"workspaces": [
"packages/api",
"packages/next-app"
],
"name": "...",
...
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"typescript": "^5.5.3"
},
"dependencies": {
"eslint": "^8.57.0"
}
}
dependencies
and devDependencies
in the root package.json
are available for all workspaces. packages/api/package.json
and packages/next-app/package.json
are regular package.json
.
To configure typescript with composite projects, first create a tsconfig.json
in the root directory:
{
"references": [
{
"path": "packages/api"
},
{
"path": "packages/next-app"
}
],
"include": [
"packages/*"
],
"exclude": [
"packages/next-app"
]
}
references
should contain the path of folders or tsconfig
files included in the composite project. Notice that the next-app
project is excluded.
Then create another tsconfig.base.json
file with the common configurations for all your projects. It is imported in packages/api/tsconfig.json
and packages/next-app/tsconfig.json
:
{
"compilerOptions": {
"incremental": true,
"composite": true,
// The following properties can be customized
"target": "ES6",
"allowJs": true,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
The composite: true
property is mandatory.
api
referenced projectpackages/api/tsconfig.json
:
{
"extends": "../../tsconfig.base.json",
"include": [
"src/**/*.ts"
],
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
We need to import tsconfig.base.json
located in the root folder, then we customize the tsc
compiler options.
next-app
referenced projectpackages/next-app/tsconfig.json
:
{
"extends": "../../tsconfig.base.json",
"references": [
{
"path": "../api"
}
],
"include": [
"src/**/*",
"next.config.mjs",
".next/types/**/*.ts"
],
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
},
"strict": false,
"noEmit": true,
"isolatedModules": true
},
"exclude": [
"node_modules"
]
}
NextJS typically requires a more complex configuration. The additional property is references
, which points to the dependency folder. (Again, through the extends
property we import the root tsconfig.json
).
To build the entire project I have prepared a simple ./Makefile
:
SERVER_PORT ?= 3001
.PHONY: compile test start
compile:
npx tsc --build
cd packages/next-app && npx next lint
start: compile
cd packages/next-app && npx next dev -p $(SERVER_PORT)
test: compile
npx jest
Run make start
to compile the project and start the NextJS server.
The logic behind Jest configuration is similar to the tsconfig.json
one. In the root folder, create jest.config.js
:
import type { Config } from 'jest';
const config: Config = {
projects: [
'<rootDir>/packages/api',
'<rootDir>/packages/next-app',
]
};
export default config;
Where projects
contains the sub-projects' folders. Then create a jest.config.base.ts
with the common configurations:
import type { Config } from 'jest';
const config: Config = {
testEnvironment: "node",
transform: {
"^.+.tsx?$": ["ts-jest", {}],
}
};
export default config;
Each sub-project referenced in the projects
property must contain a jest.config.ts
file in its root folder, but can extend the base one:
import type { Config } from 'jest';
import baseConfig from '../../jest.config.base'
const config: Config = {
...(baseConfig as Config),
// other properties
};
export default config;
With the previous Makefile
, run make test
and tests from all the specified projects are executed. The only issue I found is related to the working directory with the fs
module: the base path is .
instead of ./packages/my-prj
. I solved it by concatenating the current file directory:
import * as path from 'path'
path.resolve(__dirname, './relative-path')
I honestly do not know whether this is a bug, but the solution I found is just a workaround and dangerous in case you change the project structure. I believe the alternative of running npx jest
inside each sub-project remains valid.