This document describes how to create a similar monorepository by using just a NPM workspaces feature.
Create a new directory that will be a root for a new monorepository. In this article it's supposed that root directory is just-monorepo
.
mkdir just-monorepo
Switch to created directory and initialize a new git repository then:
cd just-monorepo
git init
Initialize a new NPM package that will be a root package:
npm init --yes
Enable workspaces in previously created package so it become a root meta-package for real sub-packages. To do so, clean up created root package.json
and make it look like this:
{
"name": "just-monorepo",
"version": "0.0.0",
"private": true,
"workspaces": [
"packages/*"
]
}
Here "workspaces"
property lists directories of actual sub-packages. See details in NPM docs.
There is no "common" or "best" packages structure. Sometimes it's better to use flat structure of just packages/*
directory while sometimes it can be split up to apps/*
, libs/*
, <etc>/*
and so on. In order to provide simple example this monorepo will use flat option.
Create the following structure:
just-monorepo/ — root package: just-monorepo
├── packages/
│ ├── client/
│ │ └── package.json — sub-package: @just-monorepo/client
│ ├── server/
│ │ └── package.json — sub-package: @just-monorepo/server
│ ├── types/
│ │ └── package.json — sub-package: @just-monorepo/types
│ └── utils/
│ └── package.json — sub-package: @just-monorepo/utils
└── package.json
Each sub-package is just a subdirectory with its own package.json
:
{
"name": "@just-monorepo/<sub-package-name>",
"version": "0.0.0",
"author": "...",
"license": "...",
"homepage": "...",
"bugs": {
"url": "..."
},
"repository": {
"type": "git",
"url": "git+https://example.org/just-monorepo.git",
"directory": "packages/<sub-package-name>"
}
}
Where <sub-package-name>
is one of packages
subdirectories names, e.g:
"name": "@just-monorepo/client"
Now it's ready: blank monorepo with 4 sub-packages is created.
Types and data contracts are essential for large realworld projects. For JavaScript projects one can use TypeScript which can be simply integrated by installing its package first.
In root directory:
npm install --save-dev typescript
To compile TypeScript create a tsconfig.json
configuration, for each sub-package and for root package too:
just-monorepo/
├── packages/
│ ├── client/
│ │ ├── package.json
+ │ │ └── tsconfig.json ← Sub-package TypeScript configuration
│ ├── server/
│ │ ├── package.json
+ │ │ └── tsconfig.json ← Sub-package TypeScript configuration
│ ├── types/
│ │ ├── package.json
+ │ │ └── tsconfig.json ← Sub-package TypeScript configuration
│ └── utils/
│ ├── package.json
+ │ └── tsconfig.json ← Sub-package TypeScript configuration
+ ├── tsconfig.json ← Root TypeScript configuration
└── package.json
Root tsconfig.json
will contain configuration base:
{
"compilerOptions": {
"experimentalDecorators": true,
"module": "commonjs",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"allowJs": true,
"target": "ES2020",
"noImplicitAny": true,
"declaration": true,
"emitDecoratorMetadata": true,
"moduleResolution": "node",
"sourceMap": true,
"skipLibCheck": true
},
"paths": {
"*": ["node_modules/*"]
}
}
While sub-packages tsconfig.json
files will extend the base and contain just paths per each sub-package:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
"outDir": "dist"
},
"files": ["src/index.ts"]
}
Then package.json
of each sub-package should now contain following properties:
- build script (
"scripts.build"
) - path working files (
"files"
) - path to types definitions (
"types"
) - path to entry point (
"main"
)
@@ -11,5 +11,13 @@
"type": "git",
"url": "git+https://example.org/just-monorepo.git",
"directory": "packages/<sub-package-name>"
- }
+ },
+ "scripts": {
+ "build": "tsc"
+ },
+ "files": [
+ "dist"
+ ],
+ "types": "dist/index.d.ts",
+ "main": "dist/index.js"
}
These values above are the same for all the 4 sub-packages for now.
Each sub-package should now have an entry point file src/index.ts
. It could be empty for now:
just-monorepo/
├── packages/
│ ├── client/
│ │ ├── src/
+ │ │ │ └── index.ts ← Entry point file
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── server/
│ │ ├── src/
+ │ │ │ └── index.ts ← Entry point file
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── types/
│ │ ├── src
+ │ │ │ └── index.ts ← Entry point file
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── utils/
│ ├── src
+ │ │ └── index.ts ← Entry point file
│ ├── package.json
│ └── tsconfig.json
├── tsconfig.json
└── package.json
Also, root package.json
should now have global building scripts:
@@ -4,5 +4,15 @@
"private": true,
"workspaces": [
"packages/*"
- ]
+ ],
+ "scripts": {
+ "build:client": "npm run build --prefix packages/client",
+ "build:server": "npm run build --prefix packages/server",
+ "build:types": "npm run build --prefix packages/types",
+ "build:utils": "npm run build --prefix packages/utils",
+ "build": "npm run build:types && npm run build:utils && npm run build:server && npm run build:client"
+ },
+ "devDependencies": {
+ "typescript": "..."
+ }
Note a building order: @just-monorepo/types
should be built first since all the packages needs it and so on.
Now it's done: monorepository supports TypeScript.
One can now build any package by running:
npm run build:<package>
For instance:
npm run build:types
Or build them all at once:
npm run build
For server sub-packages (like @just-monorepo/server
) there should be some server infrastructure, like Express for instance.
Server dependencies (Express) are potentially may be utilized by multiple server sub-packages so install them in root package. In root directory:
npm install cors express
Then development packages:
npm install --save-dev @types/cors @types/express node-dev ts-node
One note for server sub-packages configuration: to use local sub-package dependencies use this in root tsconfig.json
:
@@ -15,5 +15,8 @@
},
"paths": {
"*": ["node_modules/*"]
+ },
+ "ts-node": {
+ "transpileOnly": true
}
}
Next, setup watch and start scripts in package.json
of @just-monorepo/server
:
@@ -13,7 +13,9 @@
"directory": "packages/server"
},
"scripts": {
- "build": "tsc"
+ "build": "tsc",
+ "watch": "node-dev --notify=false src/index.ts",
+ "start": "node ."
},
"files": [
"dist"
And for root package.json
too:
@@ -10,9 +10,19 @@
"build:server": "npm run build --prefix packages/server",
"build:types": "npm run build --prefix packages/types",
"build:utils": "npm run build --prefix packages/utils",
- "build": "npm run build:types && npm run build:utils && npm run build:server && npm run build:client"
+ "build": "npm run build:types && npm run build:utils && npm run build:server && npm run build:client",
+ "watch:server": "npm run watch --prefix packages/server",
+ "start:server": "npm run start --prefix packages/server"
},
"devDependencies": {
+ "@types/cors": "...",
+ "@types/express": "...",
+ "node-dev": "...",
+ "ts-node": "...",
"typescript": "..."
+ },
+ "dependencies": {
+ "cors": "...",
+ "express": "..."
}
}
Now it's done: monorepository has global Express support for any server sub-package.
Also, @just-monorepo/server
is set up and ready. It can be built by build
command:
npm run build:server
Then it can be launched by start
command:
npm run start
While developing it can be started in non-production live-reload mode:
npm run watch:server
For client sub-packages (like @just-monorepo/client
) there should be some client bundling infrastructure, like Webpack bundling for instance.
Client dependencies (Webpack and plugins) are potentially may be utilized by multiple sub-packages so install them in root package. They are all goes as development dependencies:
npm install --save-dev clean-webpack-plugin copy-webpack-plugin css-loader file-loader html-webpack-plugin mini-css-extract-plugin postcss-csso postcss-import postcss-loader ts-loader webpack webpack-cli webpack-dev-server webpack-merge
Next, setup watch and build scripts in package.json
of @just-monorepo/client
:
@@ -13,7 +13,8 @@
"directory": "packages/client"
},
"scripts": {
- "build": "tsc",
+ "build": "webpack --config-name build",
+ "watch": "webpack serve --config-name watch"
},
"files": [
"dist"
And for root package.json
too:
@@ -12,14 +12,29 @@
"build:utils": "npm run build --prefix packages/utils",
"build": "npm run build:types && npm run build:utils && npm run build:server && npm run build:client",
"watch:server": "npm run watch --prefix packages/server",
+ "watch:client": "npm run watch --prefix packages/client",
"start:server": "npm run start --prefix packages/server"
},
"devDependencies": {
"@types/cors": "...",
"@types/express": "...",
+ "clean-webpack-plugin": "...",
+ "copy-webpack-plugin": "...",
+ "css-loader": "...",
+ "file-loader": "...",
+ "html-webpack-plugin": "...",
+ "mini-css-extract-plugin": "...",
"node-dev": "...",
+ "postcss-csso": "...",
+ "postcss-import": "...",
+ "postcss-loader": "...",
+ "ts-loader": "...",
"ts-node": "...",
- "typescript": "..."
+ "typescript": "...",
+ "webpack": "...",
+ "webpack-cli": "...",
+ "webpack-dev-server": "...",
+ "webpack-merge": "..."
},
"dependencies": {
"cors": "^2.8.5",
Setup Webpack configuration next. Like e.g. TypeScript its configuration should be split up to base and local ones with specific paths (for sub-package). They are available here:
/webpack.config.js
— base Webpack configuration/packages/client/webpack.config.js
— local (per package) Webpack configuration- Note: if there will be another client package it should have its own
webpack.config.js
Now it's done. A monorepository has global Webpack support for any client sub-package.
Also, @just-monorepo/client
is set up and ready. It can be built by build
command:
npm run build:client
While developing it can be started in non-production live-reload mode:
npm run watch:client
@just-monorepo/types
is types and data contracts definitions sub-package. It will be consumed by other sub-packages, so it should be populated first.
Create some data contract definition that will be used later. In vehicles/vehicle.dto.ts
:
export interface OrderDto {
vehicleId: number;
fullName: string;
contacts: string;
}
And so on, as in actual example code.
Now types sub-package is done. Build it by using command from root directory:
npm run build:types
@just-monorepo/utils
is generic utility functions sub-package which will be used both by client and server applications. Among others this sub-package provides validation utilities for OrderDto
structure validation.
Since OrderDto
interface is defined and exported in @just-monorepo/types
sub-package it should be integrated into @just-monorepo/utils
as a development dependency first.
Types sub-package is never published so it can be added only manually by editing package.json
of @just-monorepo/utils
:
@@ -19,5 +19,8 @@
"dist"
],
"types": "dist/index.d.ts",
- "main": "dist/index.js"
+ "main": "dist/index.js",
+ "devDependencies": {
+ "@just-monorepo/types": "file:../types/dist"
+ }
}
Now link it by running install
from root directory:
npm install
After install
ends OrderDto
can be imported in @just-monorepo/utils
code, e.g. in validate/is-valid-order.ts
:
import { OrderDto } from '@just-monorepo/types'; // ← Here
import { isNumber } from './is-number';
import { isFullString } from './is-full-string';
export const isValidOrder = (order: OrderDto): boolean =>
isInteger(order.vehicleId) &&
isFullString(order.fullName) &&
isFullString(order.contacts);
Note some caveats:
- Every time when
@just-monorepo/types
code is changed it should be re-built to be available in other sub-packages. Same does for every dependency sub-package. - For Visual Studio Code users: use
Developer: Reload Window
command if local sub-packages dependency is not recognized (i.e. in TypeScriptimport
s)
Now utilities sub-package is done. Build it by using command from root directory:
npm run build:utils
@just-monorepo/server
is a server package. It will act like example of API server for client front-end applications.
Add local types and utilities sub-packages as dependencies, like in a section before:
@@ -21,5 +21,11 @@
"dist"
],
"types": "dist/index.d.ts",
- "main": "dist/index.js"
+ "main": "dist/index.js",
+ "dependencies": {
+ "@just-monorepo/utils": "file:../utils/dist"
+ },
+ "devDependencies": {
+ "@just-monorepo/types": "file:../types/dist"
+ }
}
Then link them by running install
from root directory:
npm install
Now any exported code from @just-monorepo/types
and @just-monorepo/utils
is available in @just-monorepo/server
and can be used like in src/orders/handlers.ts
:
import { Request, Response } from 'express';
import { OrderDto } from '@just-monorepo/types'; // ← Here
import { times, validate } from '@just-monorepo/utils'; // ← And there
export const createOrder = async (
request: Request,
response: Response
): Promise<void> => {
// <...>
};
Now @just-monorepo/server
is populated.
@just-monorepo/client
is a client package. It's an example of front-end application for end users and will be built with React and Webpack.
Webpack was installed and set up as global dependency already since it's potentially may be used by multiple frontend applications. Unlike Webpack, assume that React will be used only in @just-monorepo/client
(another client packages may be built with Angular, Vue.js or other technologies).
First install React packages:
npm install --workspace=packages/client react react-dom
Then install development dependencies:
npm install --workspace=packages/client --save-dev @types/react @types/react-dom
Like with server package, setup local dependencies manually by modifying client package.json
`:
@@ -22,10 +22,12 @@
"types": "dist/index.d.ts",
"main": "dist/index.js",
"dependencies": {
+ "@just-monorepo/utils": "../utils/dist",
"react": "...",
"react-dom": "..."
},
"devDependencies": {
+ "@just-monorepo/types": "../types/dist",
"@types/react": "...",
"@types/react-dom": "..."
}
And link them:
npm install
Due to React typed JSX (TSX files) integration local tsconfig.json
should now have jsx
option along with new TSX entry point ("src/index.tsx"
):
@@ -3,7 +3,8 @@
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
- "outDir": "dist"
+ "outDir": "dist",
+ "jsx": "react"
},
- "files": ["src/index.ts"]
+ "files": ["src/index.tsx"]
}
Now @just-monorepo/client
is populated.
Integrated documentation to TypeScript code can be easily added by using JSDoc syntax (see more). Just add special entry to annotate a code block, i.e. in @just-monorepo/utils
code, validate/is-valid-order.ts
:
/**
* Returns true if Order is valid, i.e:
*
* - Has a `vehicleId` property which is valid integer number
* - Has a `fullName` property which is valid non-empty string
* - Has a `contacts` property which is valid non-empty string
* @param order
*/
export const isValidOrder = (order: OrderDto): boolean =>
isInteger(order.vehicleId) &&
isFullString(order.fullName) &&
isFullString(order.contacts);
After build
command this annotation will be available in external packages, i.e. in @just-monorepo/server
which utilizes @just-monorepo/utils
.