Tricky TypeScript Errors after pnpm migration

Migrating from npm to pnpm exposed a TypeScript project scope mismatch that looked like a missing package but was really about where files lived.

Read time is about 9 minutes

Alexander Garcia is an effective JavaScript Engineer who crafts stunning web experiences.

Alexander Garcia is a meticulous Web Architect who creates scalable, maintainable web solutions.

Alexander Garcia is a passionate Software Consultant who develops extendable, fault-tolerant code.

Alexander Garcia is a detail-oriented Web Developer who builds user-friendly websites.

Alexander Garcia is a passionate Lead Software Engineer who builds user-friendly experiences.

Alexander Garcia is a trailblazing UI Engineer who develops pixel-perfect code and design.

I migrated this site from npm to pnpm. It went smoothly — until VSCode started underlining imports in red and telling me it couldn't find @rsbuild/core.

The package was installed. pnpm install completed without errors. The build ran fine. But the editor was upset, and that kind of silent mismatch bothers me.

Here's what was actually going on and how I fixed it.

The Setup

This site uses Rsbuild as its build tool. The project has a few files that live outside of src/ because they're part of the build configuration, not the application:

  • rsbuild.config.ts — the main build config
  • plugins/sitemap-plugin.ts — a custom Rsbuild plugin I wrote for auto-generating the sitemap
  • src/lib/recma-read-time.ts — a recma plugin for calculating blog post read time

The project had two TypeScript config files:

  • tsconfig.json — for the application code in src/
  • tsconfig.node.json — for Node.js build tooling

Before the migration, this all worked fine. After switching to pnpm, VSCode flagged two files with the same error:

Cannot find module '@rsbuild/core' or its corresponding type declarations.

The files in question: plugins/sitemap-plugin.ts and rsbuild.config.ts.

What I Tried First

The obvious culprit was tsconfig.node.json. Build-time files like Rsbuild configs need to be covered by the Node-flavored TypeScript config — not the browser-targeted one. So I added plugins/ to its include array:

// tsconfig.node.json { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "strict": false, "types": ["node"] }, "include": ["rsbuild.config.ts", "plugins"] }

That fixed the error in plugins/sitemap-plugin.ts however my rsbuild.config.ts file was still complaining and showing red.

The Second Attempt

rsbuild.config.ts was already listed in the include array of tsconfig.node.json. So why was it still complaining?

The issue was that tsconfig.json didn't know tsconfig.node.json existed. They were two completely independent configs. VSCode was resolving rsbuild.config.ts through the main tsconfig.json, which had no idea about Node types or Rsbuild's package.

The fix was to wire them together using TypeScript project references:

// tsconfig.json { "references": [{ "path": "./tsconfig.node.json" }], "compilerOptions": { ... }, "include": ["src"] }

This basically tells TypeScript

"Hey when you encounter a file that belongs to tsconfig.node.json, use that config not this one!"

The linting error for the @rsbuild/core import in rsbuild.config.ts cleared but a new import linting error appeared now instead.

The Real Problem

Adding the reference exposed something I hadn't noticed before. rsbuild.config.ts imports recma-read-time.ts:

// rsbuild.config.ts import { recmaReadTime } from "./src/lib/recma-read-time";

That file lived in src/lib/ — which is covered by tsconfig.json, not tsconfig.node.json. So now TypeScript was flagging @rsbuild/core inside recma-read-time.ts for the same reason it had flagged the other files: It had the wrong project scope.

I could have tried to include src/lib/recma-read-time.ts in tsconfig.node.json. However that didn't feel quite right. The src/ directory is application code. Pulling individual files out of it into a separate config would be a mess to maintain.

The cleaner answer was to ask: why is this file in src/lib/ at all? especially since it only run during build time.

recma-read-time.ts is a custom Recma plugin — a build-time transform. It runs during the MDX compilation step in Rsbuild. It's never imported by any application code. The only file that imports it is rsbuild.config.ts.

It didn't need to be in src/lib/ why don't we just move it to plugins/?.

The Fix

Moving the file was the right call (below) then we updated the import in rsbuild.config.ts

// src/lib/recma-read-time.ts → plugins/recma-read-time.ts // Before import { recmaReadTime } from "./src/lib/recma-read-time"; // After import { recmaReadTime } from "./plugins/recma-read-time";

All three errors cleared. No more red underlines. The TypeScript configs are accurate, the file is in the right place, and the build continues to work.

The Mental Model

Here's the thing that made this confusing: the error message said "cannot find module." That sounds like a missing package. You check node_modules, you check pnpm install, everything looks fine.

But the error wasn't about a missing package. It was about TypeScript not knowing which config to use when resolving that file — and therefore not having access to the right module resolution context.

When you have separate TypeScript configs for application code and build tooling, the boundary between them matters. Build-time files need to live where the build-time config can find them. If a file is only ever used by your build config, it's a build-time file — regardless of where it happened to be sitting on disk.

npm was silently tolerating the mismatch. pnpm's stricter module resolution made it visible. Shout out to the devs at pnpm

A note on pnpm and hoisting

One thing worth flagging if you're doing a similar migration: pnpm uses symlinked node_modules by default, which means packages that rely on hoisted dependencies can break in ways they didn't with npm. If you use packages with implicit peer dependencies, you might need to add an .npmrc file:

// .npmrc shamefully-hoist=true

In my case I didn't need it for the @squoosh/lib package, but it's good to know before you spend time debugging a build script failure that has nothing to do with TypeScript.

The Final State

After the fix, both configs are clean and accurate:

// tsconfig.node.json — covers build tooling { "compilerOptions": { "composite": true, "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "strict": false, "types": ["node"] }, "include": ["rsbuild.config.ts", "plugins"] } // tsconfig.json — covers application code, references build config { "compilerOptions": { ... }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] }
/** plugins/ sitemap-plugin.ts ← custom Rsbuild plugin recma-read-time.ts ← moved here from src/lib/ rsbuild.config.ts */

If you're migrating to pnpm and start seeing TypeScript import errors that don't make sense given your installed packages — check your project boundaries first, not your lockfile.

Related Posts