Setting up a pnpm workspace monorepo
This lesson preview is part of the Bundling and Automation in Monorepos course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to Bundling and Automation in Monorepos, plus 90+ \newline books, guides and courses with the \newline Pro subscription.

[00:00 - 00:13] We're finally about to get into the monorepo part of this monorepo course. We have our package.json, we have a package manager setup, we have a node version setup, and that's about it.
[00:14 - 00:38] We don't have anything else set up in this folder, so we're starting from scratch. The first thing we need to do is to configure this as a monorepo instead of a normal repository. To do this, we're going to create a file called pnpm-workspace.yaml. This is a configuration file for pnpm that specifies how we're going to lay out our monorepo.
[00:39 - 00:51] Under the "packages" key, we can specify a list of globs. Each glob defines the folders that pnpm is going to check for workspace packages and make them part of our monorepo.
[00:52 - 01:15] Initially, we're only going to add one glob that's going to be all folders under a directory called apps. Later on in the course, we will talk more about adding shared configuration and code, but for now, all we're going to do is we're going to use this apps folder to co-locate our user-facing applications.
[01:16 - 01:40] I'm going to create the apps folder now cd into it, and then I'm going to create-next-app version 14 with TypeScript, ESLint, Tailwind, and the new App Router. I'm going to add the newly created app to git, so we can look at diffs later as we change some files, and then I'm going to cd into its folder.
[01:41 - 01:50] Let's look at package.json. It comes pre-populated with some scripts that we are going to base all our apps around.
[01:51 - 02:05] We're going to make sure that all our apps have the "dev", "build", "start", and "lint" scripts by the end. And we're going to add one more that's going to be called "type-check" and it's going to run TypeScript type checking from the command line.
[02:06 - 02:15] Let's just quickly check that it works... and it does. So we're going to now verify the dev, build, and start scripts.
[02:16 - 02:27] As you can see they all work as expected. And we can move on to the next app we're going to create.
[02:28 - 02:37] I'm going to go back to the apps folder and then create a Vite app. I'm going to add it to git, same as we did with our Next.js app.
[02:38 - 02:45] So we can later look at what changes we made. Let's open up package.json and look at the list of scripts.
[02:46 - 02:59] As you can see, these are slightly different compared to our Next.js app. What I'm going to change is I'm going to rename the "preview" script to "start" because that's what it most closely matches with Next.js.
[03:00 - 03:15] And I'm also going to add our "type-check" script. Then we're going to run through all the commands and verify that they work as expected. And as you can see, they do.
[03:16 - 03:38] Now I want to pull up "git diff" and we're going to see something slightly unexpected. There are two new untracked files that were added to the workspace when we ran the build script. They are "tsconfig.app.tsbuildinfo" and "tsconfig.node.tsbuildinfo".
[03:39 - 03:54] We're going to examine these files further, but that's going to be later in the course. For now, it's enough to know that "*.tsbuildinfo" files can be safely discarded, so we're just going to mark them for git ignore and continue from there.
[03:55 - 04:06] I'm going to go back to the app folder and then create my last app. For this one, we won't use a template, but instead, I'm going to manually create all files myself.
[04:07 - 04:23] It's going to be a server app using "express". So I'm going to call it "server-express." I'm going to create a package.json file that just has a "name", "version", and "private" fields set - those are enough for now.
[04:24 - 04:56] And then I'm going to add a production dependency on "express@^4" and then devDependencies of the TypeScript types of express TypeScript itself and "tsup", which is going to be our bundler. We're going to create a minimal tsconfig.json that only sets "compilerOptions" to "strict: true", and "esModuleInterlop: true".
[04:57 - 05:05] We will be further discussing TypeScript configuration later in the course. For now suffice to say that this is necessary for express to work.
[05:06 - 05:30] I'm going to create a "src" folder, and then I'm going to edit in my index.ts file. Quickly walking through it, we import express, then we have a function called "createServer" that creates the express app. It has a single route that returns a JSON with status of running.
[05:31 - 06:13] Afterwards, we have our main function that can optionally take a port from the environment, or it will default to 5001, and then it will create the express server instance. Start listening on our designated port and write to the console when it's ready. We're going to add the "dist" folder to our .gitignore, because that's where tsup is going to compile our TypeScript. And we're going to go into package.json and create the same scripts that we created everywhere else. The "build" script directly invokes tsup with our source file.
[06:14 - 06:29] The "start" script then runs node on the output, and the "dev" script basically does the same, but with a watcher, and then for every change, it automatically runs node. Finally, we're going to add our "type-check" script.
[06:30 - 07:01] And this is enough for us to run our small server. We can build it. And when we try to start it, it fails. I made a small mistake the first time around, and I accidentally tried to start "dist/index.ts" instead of "dist/index.js" with node. So let me fix that, and then it runs correctly and we can verify it. So now that that runs I want to go back to the monorepo root folder.
[07:03 - 07:10] And I want to look at the folder structure that we currently have. I've omitted all the source files.
[07:11 - 07:20] And instead I'm just showing you the config files that we have. Right now, we have three applications that are co-located.
[07:21 - 07:43] They don't share any code and they don't share any configuration. They are independent. Over the next few modules of this course, we're going to see how we can share code between applications how we can share configuration and how we can even make one application dependent on parts of another.
[07:44 - 08:01] But for now, one of the immediate benefits we get from co-locating our three applications is that we can run scripts over all three of them at the same time. The "pnpm run" command has a "-r" or a "--recursive" option.
[08:02 - 08:47] Using that, for example, we can run typechecking on all our applications at the same time and verify that they are all working and passing type correctness. We're going to make use of this to build shortcut scripts in our monorepo root package.json, that will invoke all the scripts in our apps recursively. So now when I'm in the root of the monorepo, I can say "pnpm run dev" and that's going to run the "dev" scripts in all three of our applications, allowing us to do simultaneous development of all of them, if we imagined that the two front end applications were consuming the server application in some way.
[08:48 - 08:57] There is an important shortcut to be aware of when running pnpm scripts. I think everyone knows that you can omit the run part.
[08:58 - 09:12] You can just say "pnpm type-check" instead of "pnpm run type-check". But in a monorepo, there is an option called "--workspace-root" or "-w" for short that allows you to do "pnpm -w type:check".
[09:13 - 09:35] This runs the "type:check" command from the monorepo root and this works even when you're not in the monorepo root. So for example, I can go to the front end Next.js app and I can still do "pnpm -w type:check" and it's still going to run the monorepo root "type:check".
[09:36 - 09:45] But if I omit the "-w" and just do "pnpm type:check", it's going to run the local script for "type:check".