In previous article, we already learnt what monorepo
is. In this article, we will create a monorepo
project using PnPM
workspaces.
PnPM
is a pacakge management just like npm
and yarn
. It is an alternative tool to those ones.
First, we need to install PnPM
if you haven't installed it before. Using this url for more detail.
With PnPM
installed, we can initilize a new project using this command:
pnpm init
We will have a package.json
file in root folder. The content will be like this:
{ "name": "monorepo", "version": "1.0.0", "description": "", "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
Next, create a file called pnpm-workspace.yaml
at root folder. This file is a configuration to define where to put all projects we have. Open it and add those lines:
packages: - "libs/**" - "workspaces/**"
The config tells PnPM
that we will have two places to store our projects. All projects should be put inside those two folders.
After that, install TypeScript to root package:
pnpm -w add -D typescript
All the installed packages at root package will apply for all the mono projects
Create tsconfig.json
at root folder:
{ "compilerOptions": { "allowJs": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "experimentalDecorators": true, "forceConsistentCasingInFileNames": true, "importHelpers": true, "isolatedModules": true, "module": "commonjs", "moduleResolution": "node", "noUncheckedIndexedAccess": true, "removeComments": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": false, "strict": true, "target": "esnext" }, "exclude": [ "**/node_modules/*", "**/dist/*" ] }
This file acts like global tsconfig for all projects. Every tsconfig of mono project should inherit from this file.
Access to the libs
folder, create a folder called commons
, then run init command to init the project. We will package.json
like this:
{ "name": "@libs/commons", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
Create tsconfig.json
inside libs/commons
:
{ "extends": "../../tsconfig.json", "compilerOptions": { "baseUrl": "src", "outDir": "lib" }, "include": [ "src" ], "exclude": [ "lib", "node_modules" ] }
Similar to the previous step, access to workspaces
folder and create folder called major-app
. Init and create tsconfig
file:
{ "name": "major-app", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
{ "extends": "../../tsconfig.json", "compilerOptions": { "baseUrl": "src", "outDir": "dist" }, "include": [ "src" ] }
First, create a method for commons lib.
interface ICombineOptionsProps { joinWith: string; } function compact(source: (string | undefined)[]): string[] { return source.filter((s) => !!s) as string[]; } export function combine( opts: ICombineOptionsProps | string | undefined = '', ...params: (string | undefined)[] ): string { let options: ICombineOptionsProps = { joinWith: ' ' }; if (typeof opts === 'object') { options = opts; } else { params = [opts, ...params]; } const { joinWith } = options; return compact(params).join(joinWith); }
Change the package.json
to export the method for other project:
1{ 2 "name": "@libs/commons", 3 "version": "1.0.0", 4 "description": "", 5 "main": "index.js", 6 "main": "lib/index.js", 7 "types": "lib/index.d.ts", 8 "scripts": { 9 "test": "echo \"Error: no test specified\" && exit 1" 10 "build": "tsc" 11 }, 12 "keywords": [], 13 "author": "", 14 "license": "ISC" 15}
Next, we will build the commons project:
pnpm --filter=@libs/commons build
After build, install the @libs/commons
to major-app
:
pnpm --filter=major-app add @libs/commons
Update: for
PnPM
version 9 (9.0.6), it changes how it will fetch package. You should do the step below. Check here for more info.
pnpm --filter=major-app add @libs/commons@workspace:
Now the content of package.json
of major-app
will change similar to:
1{ 2 "name": "major-app", 3 "version": "1.0.0", 4 "description": "", 5 "main": "index.js", 6 "scripts": { 7 "test": "echo \"Error: no test specified\" && exit 1" 8 }, 9 "keywords": [], 10 "author": "", 11 "license": "ISC", 12 "dependencies": { 13 "@libs/commons": "workspace:^" 14 } 15}
You will receive a difference content if you use an older
PnPM
(below 8)
1{ 2 "name": "major-app", 3 "version": "1.0.0", 4 "description": "", 5 "main": "index.js", 6 "scripts": { 7 "test": "echo \"Error: no test specified\" && exit 1" 8 }, 9 "keywords": [], 10 "author": "", 11 "license": "ISC", 12 "dependencies": { 13 "@libs/commons": "workspace:^1.0.0" 14 } 15}
You should change the version like the below code to make sure the major-app
always uses the latest version of commons:
1{ 2 "name": "major-app", 3 "version": "1.0.0", 4 "description": "", 5 "main": "index.js", 6 "scripts": { 7 "test": "echo \"Error: no test specified\" && exit 1" 8 }, 9 "keywords": [], 10 "author": "", 11 "license": "ISC", 12 "dependencies": { 13 "@libs/commons": "workspace:^1.0.0" 14 "@libs/commons": "workspace:*" 15 } 16}
Finally, create a file and use the code inside major-app
import { combine } from '@libs/commons'; const combinedString = combine({ joinWith: ' - ' }, 'Alpha', 'de Lucifer'); console.log(combinedString); // will print Alpha - de Lucifer
In this article, we explore how to setup a monorepo using PnPM
. Within this comprehensive guide, we not only cover the essential steps to establish a monorepo architecture but also reveal the invaluable technique of code sharing between the various projects within the monorepo.