When you have serveral highly coupled projects which you want to organize them together, you can consider a monorepo.
Yarn (1.x) provide the workspace feature to help you organize monorepo project.
Yarn workspace has serveral advantages like:
- Hoist same dependecies to top level to avoid duplicate install.
- Upgrade dependencies is much more easier.
- Easy to run a same script for all projects.
A typical monorepo is a backend api and frontend SPA project.
This demo show only the steps to setup the shared
part and the backend
part.
The source code can find in https://github.com/t7yang/ts-yarn-workspace-demo
File Structure
.
├── backend
│ ├── package.json
│ ├── src
│ │ └── index.ts
├── shared
│ ├── package.json
│ ├── src
│ │ ├── index.ts
├── package.json
How to
Setup yarn workspace in package.json
Yarn workspace must set private
to true
. In workspaces
field you either explicitly list the path of projects or put all projects under a directory (said packages
) then write as packages/*
in workspaces
field.
{
"private": true,
"workspaces": [
"shared",
"backend"
]
}
Add shortcut for projects (optional)
You don't need cd
into each project to run a script, simply run yarn workspace 'workspacename' 'scriptname'
in project's root.
A shortcut can make this easier:
...
"scripts": {
"backend": "yarn workspace backend",
"be": "yarn workspace backend",
"shared": "yarn workspace shared"
}
Then you can run script in more shorter way, the scripts below is equivalance.
$ yarn workspace backend start
$ yarn backend start
$ yarn be start
Add TypeScript dependency
Just like the scripts above, adding dependency to a workspace is:
$ yarn workspace shared add -D typescript
$ yarn workspace backend add -D typescript
Run yarn install
again in root project, yarn will hoist the same dependencies among sub projects to the top level node_modules
.
Setting tsconfig.json
A share tsconfig.json
can place in the root project:
{
"compilerOptions": {
/* Basic Options */
"target": "es5",
"module": "commonjs",
"lib": ["ESNext"],
/* Strict Type-Checking Options */
"strict": true,
/* Module Resolution Options */
"moduleResolution": "node",
"esModuleInterop": true,
/* Advanced Options */
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
}
}
For the shared
sub project, composite: true
field is required, other fields is optional but highly recommended, especially outDir
and rootDir
.
{
"compilerOptions": {
/* Basic Options */
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
Due the transpiled code of shared
sub project is dist
, point the main
and types
fields in package.json
to dist
.
{
...
"main": "dist/index.js",
"types": "dist/index.d.ts",
...
}
For the backend
sub project, references
field is required. The path
field should point to the folder contain tsconfig.json
or the tsconfig.json
itself (here we point to the folder for short).
{
"compilerOptions": {
/* Basic Options */
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"references": [{ "path": "../shared" }]
}
Start Coding
After the routine setup, now we can start coding. Add some code in shared/src
, don't forget to export them to shared/src/index.ts
// shared/src/user.ts
export interface User {
name: string;
age: number;
}
export const createUser = (name: string, age: number): User => ({ name, age });
export const showUser = (user: User) => console.log(`${user.name} is ${user.age} years old.`);
// shared/src/index.ts
export * from './user';
Import the code from shared
in backend
files, if everything is setup correctly, vscode will not complain and recognize the imports properly.
// backend/src/index.ts
import { createUser, showUser, User } from 'shared';
const user: User = createUser('t7yang', 18);
showUser(user);
Add Script and Run
Finally, add the build
and start
script for backend
.
{
...
"scripts": {
"build": "tsc --build",
"start": "yarn build && node dist/index.js"
},
...
}
Run yarn backend start
in root project, the backend
project will transpile and run correctly.
$ yarn backend start
yarn run v1.22.4
$ yarn workspace backend start
$ yarn build && node dist/index.js
$ tsc --build
t7yang is 18 years old.
You may curious why we don't have to build shared
before we build backend
.
tsc
is smart enough to transpile the project which referenced by backend
and transpile them if necessary before transpile itself.
Summary
By combining TypeScript project references and Yarn workpsace, we can setup a monorepo fast and easy.
If you need more powerful monorepo features, consider to use lerna or Yarn v2 Workspace.
References:
- Yarn Workspace
- TypeScript Project References
Top comments (16)
Thanks for the nice article. Wondering if you also have experience of implementing hot reload with TS composite approach? For the context, I am particularly interested at NextJS hot reload when shared module (aka package or workspace) updated. Thanks in advance.
I'm not working with NextJS before, but I don't think they going to support this.
github.com/vercel/next.js/issues/8708
Great article. Can you please help me with this error:
Could not find a declaration file for module '@my-organisation/common'
. I followed your article to the teeth but can't seem to find a solution to this issue.You can check the node_modules folder first to make sure
@my-organisation/common
existed. Somehow after yarn install / update some packages just "disappear". If so, you can do ayarn install --force
to force yarn install again.In my case I checked an exist inside the
node_modules
, but without thedist
folder.shared package tsconfig.json:
and shared package.json:
Do you know why this can happen? Thanks!
@t7yang I updated your example from typescript 3.8.3 to 4.9.5 and is not working either. Do you know how to fix it for TypeScript v.4.9.5?
have you run the
build
cmd? you need to build at least once to generate thedist
folder. the final solution is run thetsc
in yourshared
folder.Thanks!
For those that get an issue with
Cannot find module "shared" or its corresponding type declarations
when it is not yet built when you try to build the backend... I had this in my base config:"lib": ["es6", "es2015"],
. Changing it to"lib": ["ESNext"],
fixed the issue.This is my go-to, quick guide to setting up TypeScript in a monorepo. Thank you
Do you have insights on how to dockerize / deploy such a monorepo? How would you setup the docker files?
You can reference to this article xfor.medium.com/yarn-workspaces-an...
Great read man! Might also be worth mentioning here that projectReferences are a thing - helps speed up building too.
Sounds pro! Thanks. In the following article, I consider the same topic, but from the aliases issue perspective: webman.pro/blog/how-to-setup-types....
Thanks for the article! Wondering why you didn't need to add the "shared" package as a dependency in the package.json of "backend"?
That because the "node module resolution rule".
node will keep finding upward until found or failed.