Set up a monorepo using pnpm workspace

Ukpai Chukwuemeka - Sep 5 - - Dev Community

In the previous article, we discussed how a monorepo structure could be package-centric or app-centric, depending on your project needs. Well, in this particular article, we will set up a monorepo using pnpm and take the app-centric route.

Image description

What's Pnpm? 

Pnpm is a new generation of package management tools; it works just like NPM and yarn but offers some advantages.

## install pnpm
npm install -g pnpm # install pnpm globally
Enter fullscreen mode Exit fullscreen mode

Create a new directory or clone an existing repository

mkdir monorepo # creates a new folder

or

git clone repository-link # clones a git repository (note: replace the link with repository URL)`
Enter fullscreen mode Exit fullscreen mode

Let's change our current directory to the recently created directory or cloned repo.

cd monorepo # change directory
Enter fullscreen mode Exit fullscreen mode

Next, we are going to initialize the package manager with pnpm.

pnpm init
Enter fullscreen mode Exit fullscreen mode

Optional: Initialize git if the directory wasn't cloned from GitHub or GitLab.


git init # initializes git
Enter fullscreen mode Exit fullscreen mode
touch .gitignore
Enter fullscreen mode Exit fullscreen mode

Add node_modules and common build output folders to exclude pushing them to your repository.

# .gitignore  

node_modules  
dist  
build
.env
Enter fullscreen mode Exit fullscreen mode

Once all this is done, you can open your folder in your favorite code editor. If you use Visual Studio Code, you can open up the folder by using the command below

code . # opens up the folder in vs code
Enter fullscreen mode Exit fullscreen mode

code . didn't work fix here

Set up the workspace

To set up a workspace, create a new file pnpm-workspace.yaml

touch pnpm-workspace.yaml # creates a new file 
Enter fullscreen mode Exit fullscreen mode

Add the configuration below to the workspace file:

# pnpm-workspace.yaml

packages:
 - 'apps/*'
 - 'packages/*'
Enter fullscreen mode Exit fullscreen mode

This configuration tells pnpm that your workspace includes all directories in the apps and packages folders.


Create the apps and packages directory

mkdir -p apps packages
Enter fullscreen mode Exit fullscreen mode

We will break down our apps into two apps/website and apps/api.

**apps/website **The website directory, which we will configure using NextJS. To configure this, we need to change our current working directory to apps.

cd apps

pnpm create next-app website
Enter fullscreen mode Exit fullscreen mode

apps/api The api directory, which we will configure using NestJs.

# monorepo/apps (current working directory)

nest new api --package-manager=pnpm # creates a new NestJS project
Enter fullscreen mode Exit fullscreen mode

Create a Shared UI Library

Great job! You have got your apps set up. Now, let's take things to the next level by creating a shared UI library. This is where the magic of monorepos starts to shine.

packages/library The library directory will contain our shared UI components.

# monorepo 

cd packages 
Enter fullscreen mode Exit fullscreen mode
mkdir library && cd library && pnpm init
Enter fullscreen mode Exit fullscreen mode
# monorepo/packages/library/package.json 
{
  "private": true,  
  "name": "library",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
Enter fullscreen mode Exit fullscreen mode

Note we declare it as private by setting privateproperty to true; this is because we don't want to publish it to NPM or somewhere else, but rather just reference and use it locally within our workspace.

In our library folder, we will install the following: React as a dependency and TypeScript as a development dependency.

# monorepo

pnpm add --filter library react  
pnpm add --filter library typescript -D
Enter fullscreen mode Exit fullscreen mode

The --filter library in the installation command, tells pnpm to install these NPM packages locally to the library package.

To keep things simple, we will make use of the TypeScript compiler to compile our package. We could have a more comprehensive arrangement for bundling numerous files together, using something like Rollup or Vite.

To use the TypeScript compiler, we are going to create a tsconfig.json file at the root of our library directory and add the configuration below.

# monorepo/packages/library/tsconfig.json 
{  
  "compilerOptions": {  
    "jsx": "react-jsx",  
    "allowJs": true,  
    "esModuleInterop": true,  
    "allowSyntheticDefaultImports": true,  
    "module": "commonjs",  
    "outDir": "./dist"  
  },  
  "include": ["."],  
  "exclude": ["dist", "node_modules", "**/*.spec.ts"]  
}
Enter fullscreen mode Exit fullscreen mode

Next, we will update the package.json file main property to match the output directory specified in tsconfig.json and also add a build script.

# monorepo/packages/library/package.json 
{
  "private": true,
  "name": "library",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  "scripts": {
    "build": "rm -rf dist && tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18"
  },
  "devDependencies": {
    "typescript": "^5.5.4"
  }
}
Enter fullscreen mode Exit fullscreen mode

Creating our first shared library UI component

Let us create a simple select component first Create the file in our library directory.

# monorepo/packages/library 

touch Select.tsx
# monorepo/packages/library/Select.tsx 

function CustomSelect(props: any) {  
   return <select onChange={(e) => props.onChange(e)}>
            {props.children}
          </select>;  
}

export default CustomSelect;
Enter fullscreen mode Exit fullscreen mode

We also want to have a public API where we export components to be used outside of our library package. To achieve this, we will create an index.tsx file and export the Select component we just created.

# monorepo/packages/library 

touch index.tsx
# monorepo/packages/library/index.tsx 

export * from './Select';
Enter fullscreen mode Exit fullscreen mode

Building our library

pnpm --filter library build
Enter fullscreen mode Exit fullscreen mode

Consuming our library package from the NextJs app

Congratulations! You just created your own shared UI library component. To use this library inside our NextJS app in the apps directory, you can add it using pnpm or manually.

pnpm add library --filter website --workspace
Enter fullscreen mode Exit fullscreen mode

This adds the library package as a website dependency

{
  "name": "website",
  "version": "0.1.0",
  "private": true,
  ...,
  "dependencies": {
    "library": "workspace:*",
    ...
  },
  "devDependencies": {
    ...  
  }
}
Enter fullscreen mode Exit fullscreen mode

We are almost done. Let us update the library dependency value to "workspace:*". This means we want to resolve the package locally rather than from a registry like NPM, and * means we want to always use the latest version.

Now to use our Select Component, all we have to do is import it.

Start up the development server to see the component in action.

pnpm --filter website dev
Enter fullscreen mode Exit fullscreen mode

Setting up scripts for the monorepo

In our root package.json file, we are going to add some scripts to make our workflow easier.

# monorepo/package.json

"scripts": {
 "dev": "pnpm run - parallel - filter \"./apps/**\" dev",
 "build": "pnpm run - recursive - filter \"./apps/**\" build",
 "test": "pnpm run - recursive - filter \"./apps/**\" test",
 "lint": "pnpm run - recursive - filter \"./apps/**\" lint"
}
Enter fullscreen mode Exit fullscreen mode

Congratulations! We did it. This process shows how to set up a monorepo using pnpm. For a real project, I would recommend using any of the monorepo tools available; I mentioned a few in my previous post. They offer some advantages over pnpm.

GitHub Repository

. . . . . . . . . .