Skip to main content

tsconfig in monorepo

Since we have packages that are to be built and compiled stand-alone, we need a way to separate out what is used during build, and what is used while coding. If you look in any package's package.json, you will notice:

{
"main": "dist/index",
"types": "dist/index"
}

The package points to the build directory, and this means that if we don't build a package while developing, we won't get the updated changes in another package that is using it. This can be annoying. To alleviate this, we will have a setup where during the build processes, each package is treated separately, but during development, the typescript compiler will play nicely and refer to a projects source (src) directory. This is achieved by having two separate tsconfigs:

  1. tsconfig.build.json: in the root, this is a typical TS config and will be used as a base for all other configs.

  2. tsconfig.json: the "normal" tsconfig.json in the root will contain the settings required to enable seamless navigation in the IDE during development.

The configs need to be separate because the settings required for code navigation need to be turned off when building.

tsconfig.json is the config that will be picked up by an IDE by default and its role is to map absolute package names back to their source code in the monorepo:

{
"extends": "./tsconfig.build.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@chaine/*": ["packages/*/src"]
},
"noEmit": true
}
}

Read this blog post for details on the setup.

Now for each package we can configure another tsconfig.json that extends the root one, which will make the IDE think it is just one stand-alone typescript project to play nice during development:

{
"extends": "../../tsconfig.json",

"compilerOptions": {}
}

Each package will also have a tsconfig.build.json that will be used for publishing. This config differs from the main one by:

  • omitting the compilerOptions.paths settings so that the TypeScript compiler will look in node_modules instead of in the monorepo,
  • declaring an outDir that needs to be relative to each package.
{
"extends": "../../tsconfig.build.json",

"compilerOptions": {
"outDir": "./dist"
},

"include": ["src/**/*"]
}

Unfortunately, the include and outDir options can’t be hoisted in the root config because they are resolved relatively to the config they’re in.

One important thing in the package.json of each package is:

{
"name": "@chaine/fortress",
"version": "1.0.0",
"main": "dist/index",
"types": "dist/index",
"files": ["dist"],
"scripts": {
"build": "npm run clean && npm run compile",
"clean": "rm -rf ./dist",
"compile": "tsc -p tsconfig.build.json"
},
"dependencies": {
"@chaine/core": "^1.0.0"
},
"devDependencies": {
"typescript": "~3.4.5"
}
}
  • specifying the dependencies between monorepo packages using dependencies or devDependencies and
  • telling the tsc compiler to use our tsconfig.build.json config via the -p or --project flag.

One requisite (and downside) is that we have to declare the same dependencies between packages in both package.json and tsconfig.build.json for every package:

{
"extends": "../../tsconfig.build.json",

"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true,
"noEmit": false,
"declaration": false,
"declarationMap": false
},
"references": [
{
"path": "../core/tsconfig.build.json"
}
],
"include": ["src/**/*"]
}

Deployable Services / Webpack

If a package is being deployed and bundled using webpack, it needs to be slightly modified/

tsconfig.json

{
"extends": "../../tsconfig.json"
}

tsconfig.build.json

{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"declaration": false,
"declarationMap": false
},
"references": [
{
"path": "../keychaine/tsconfig.build.json"
}
],
"include": ["src/**/*", "src/custom.d.ts", "public/**/*"]
}

webpack.browser.config.js

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')

module.exports:{

...
module: {
rules: [
...
{
test: /\.ts(x?)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: babelOptions
},
{
loader: 'ts-loader',
options: {
configFile: isDevelopmentMode ? 'tsconfig.json' : 'tsconfig.build.json',
compilerOptions: {
noEmit: false
}
}
}
]
},
...
]
...
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
fallback: {
crypto: false
},
plugins: [new TsconfigPathsPlugin()]
},
}

webpack.server.config.js or webpack.config.js

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')

module.exports:{

...
module: {
rules: [
...
{
test: /\.ts(x?)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
options: babelOptions
},
{
loader: 'ts-loader',
options: {
configFile: isDevelopmentMode ? 'tsconfig.json' : 'tsconfig.build.json',
compilerOptions: {
noEmit: false
}
}
}
]
},
...
]
...
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
fallback: {
crypto: false
},
plugins: [new TsconfigPathsPlugin()]
},
}