TuBrief
Subscribed Channels
Videos
Community

Pitfalls and Solutions Moving a pnpm Monorepo to Nub

TuBrief Editorial
July 27, 2026
0
Computing/Software

Written with AI assistance from the source video. The video is the authority.

English한국어Español中文हिन्दीFrançaisPortuguêsDeutschРусскийالعربيةBahasa Indonesia日本語

Related Video

There's a NEW Package Manager!? (Bun Alternative)8:29

There's a NEW Package Manager!? (Bun Alternative)

Better Stack

More from the community

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

September 13, 2026

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

September 13, 2026

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

September 13, 2026

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

September 13, 2026

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

September 12, 2026

Apple Won the AI Race

September 12, 2026

Comments (0)

Log in to leave a comment

No posts yet

© 2026 . All rights reserved.

TuBrief
Subscribed Channels
Videos
Community
Log in

Pitfalls and Solutions Moving a pnpm Monorepo to Nub

The Node.js ecosystem is exhausted. Build environments layered with tsx, dotenv-cli, nvm, and pnpm are heavy, and scripts break whenever you update even a single piece. When Nub—an integrated toolkit written in Rust—appeared promising to bundle this package mess into a single binary, I was honestly both welcomed and skeptical.

When I actually migrated our production pnpm-workspace monorepo to Nub, it turned out to be quite attractive. However, nothing ever goes completely smoothly. We immediately ran into real-world hurdles, including dependency conflicts, 24-hour security lock policies, and CI cache separation issues.

Fixing the Dependency Tree During Workspace Migration

Nub's package engine, aube, immediately recognizes the existing pnpm-workspace.yaml and the workspaces field in package.json. True to the explanation that it is byte-for-byte compatible with the pnpm-lock.yaml v9 schema, it natively utilizes the existing virtual Store structure (node_modules/.store/).

The problem lies with legacy tools that implicitly assume a hoisted node_modules structure. Running them right after migration throws a MODULE_NOT_FOUND error and crashes. To get your service up and running immediately, you should start by flattening the package tree with the --node-linker hoisted option.

On a warm installation with caching applied, the default GVS mode takes 346 ms. Hoisted mode takes 1461 ms—more than 4 times slower—but it is still more than twice as fast as pnpm v10+'s 3453 ms. It is much easier to first secure compatibility with Hoisted mode and then switch to default GVS mode as you remove legacy tool dependencies.

`bash
#!/usr/bin/env bash
set -euo pipefail

echo "==> [1/4] Checking Nub binary"
if ! command -v nub &> /dev/null; then
echo "Error: Nub is missing. Run 'npm i -g @nubjs/nub' first."
exit 1
fi

echo "==> [2/4] Checking pnpm-lock.yaml schema"
if [ -f "pnpm-lock.yaml" ]; then
nub pm use nub
fi

echo "==> [3/4] Specifying execution engine in package.json"
node -e '
const fs = require("fs");
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
pkg.devEngines = pkg.devEngines || {};
pkg.devEngines.packageManager = {
name: "nub",
version: "^0.4.0",
onFail: "warn"
};
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n");
'

echo "==> [4/4] Generating Lockfile"
nub install --frozen-lockfile=false

`

Converting pnpm-lock.yaml to nub.lock using this script can save you over 2 hours a week that used to be wasted fixing broken scripts every time.

24-Hour Release Age Restrictions and Emergency Hotfixes

Nub has pretty strict security policies. It blocks lifecycle scripts by default and throws ERR_NUB_TRUST_DOWNGRADE if an npm Provenance signature is missing. The most confusing part is the minimumReleaseAge option. It blocks the installation of package versions published less than 24 hours ago, treating them as pending security verification.

While it is a great shield during normal times, it turns into a wall of tears when a zero-day vulnerability hits and you need to deploy a patch version released an hour ago immediately. In this case, you must explicitly list the allowed build script packages in the root package.json and bypass it using commands.

`json
{
"name": "@org/monorepo-root",
"private": true,
"allowBuilds": {
"esbuild": true,
"sharp": true,
"@fast-cve/patch-pkg": true
}
}

`

Here is the workflow to bypass and apply a blocked hotfix package:

`bash

1. Manually approve and add hotfix package younger than 24 hours

nub add --allow-build=@fast-cve/patch-pkg @fast-cve/patch-pkg@1.0.1-hotfix

2. Batch-approve build scripts waiting in the queue

nub approve-builds

`

Running nub approve-builds unblocks the list and proceeds with the build immediately. Packages registered as malware (MAL-*) in the OSV database cannot be bypassed even with this command, so in those cases, you must find a higher alternative version.

Separating Cache Layers in GitHub Actions and Docker

To boost CI/CD speed, you need to know Nub's cache paths. Node binaries go into ~/.cache/nub/node/<version>/, while the package CAS (Content-Addressable Store) is split between ~/.cache/nub/ and node_modules/.store/.

In Docker multi-stage builds, you should use BuildKit's cache mounts to completely isolate the installation layer to benefit from caching.

`dockerfile
FROM ghcr.io/nubjs/nub:latest AS base
WORKDIR /app

FROM base AS dependencies
COPY package.json nub.lock pnpm-workspace.yaml ./
COPY packages/core/package.json ./packages/core/
COPY packages/api/package.json ./packages/api/

Reuse repository cache with BuildKit cache mount

RUN --mount=type=cache,target=/root/.cache/nub
nub ci --prefer-offline

FROM dependencies AS builder
COPY . .
RUN nub run build --filter=@org/api

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/packages/api/dist ./dist
COPY --from=builder /app/node_modules ./node_modules

EXPOSE 3000
CMD ["node", "dist/index.js"]

`

In GitHub Actions, use nubjs/setup-nub@v0 instead of actions/setup-node.

`yaml
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

  - uses: nubjs/setup-nub@v0
    with:
      cache: true

  - run: nub ci
  - run: nub -r run build
  - run: nub -r run test

`

The speed of nub ci combined with setup-nub and caching is around 346 ms. Compared to the existing pnpm (3453 ms), package installation time in the pipeline drops significantly, cutting total CI build time in half. Seeing the cloud infrastructure bill at the end of the month definitely puts you in a good mood.

Standardizing Team Development Environments with a Shell Wrapper

Nub includes a built-in oxc-based in-memory transpiler, running TypeScript code directly without tsx or ts-node. It also reads .env files automatically. As the Node.js process bootstrap overhead that popped up every time when using pnpm run (442.7 ms) disappears, script execution time for nub run drops to 14.7 ms. The difference is quite noticeable.

To prevent Node.js version fragmentation across team members, simply place a .node-version file at the root.

`bash
echo "22.15.0" > .node-version
nub src/index.ts

`

On Node 22.15.0 and above, synchronous module.registerHooks() completely eliminates cold start latency. Setting up an automation script is the cleanest way to standardize the environment for new teammates without lengthy explanations.

`bash
#!/usr/bin/env bash
set -euo pipefail

echo "==> Starting development environment setup"

if ! command -v nub &> /dev/null; then
if command -v brew &> /dev/null; then
brew install nubjs/tap/nub
else
npm install -g --ignore-scripts=false @nubjs/nub
fi
fi

Automatically route pnpm and npm commands to the Nub engine

nub pm shim
nub node install
nub install

if [ ! -f ".env.local" ] && [ -f ".env.example" ]; then
cp .env.example .env.local
fi

echo "==> Setup complete. Run with 'nub run dev'."

`

Once you run nub pm shim, even if you habitually type pnpm install or npm run, it will be intercepted and handled by the Nub runner. On top of reducing CLI execution overhead from pnpm exec (191 ms) to nubx (11 ms), you can save at least 2 hours a week debugging issues like "It works on my machine."