[PR #122] [MERGED] fix(deploy): monorepo-aware lock file and node_modules resolution #328

Closed
opened 2026-05-06 12:39:13 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/122
Author: @harrisrobin
Created: 2/26/2026
Status: Merged
Merged: 3/7/2026
Merged by: @southpolesteve

Base: mainHead: fix/monorepo-deploy-support


📝 Commits (2)

  • 1fba5ae fix(deploy): monorepo-aware lock file and node_modules resolution
  • c638b86 fix(test): clear npm_config_user_agent in npm fallback test

📊 Changes

3 files changed (+185 additions, -30 deletions)

View changed files

📝 packages/vinext/src/deploy.ts (+13 -15)
📝 packages/vinext/src/utils/project.ts (+68 -15)
📝 tests/deploy.test.ts (+104 -0)

📄 Description

Problem

vinext deploy fails in monorepos in two distinct but related ways, both caused by the same assumption: that the project's lock file and node_modules are always in the app directory rather than potentially hoisted to a workspace root.

1. Wrong package manager detected → npm error Invalid Version:

detectPackageManager only checked for bun.lockb (legacy binary format), missing bun.lock (text format, introduced in Bun v1.0). It also only searched the immediate project directory, so any monorepo where the lock file lives at the workspace root fell through to npm install -D, producing:

npm error Invalid Version:
Error: Command failed: npm install -D @cloudflare/vite-plugin@latest ...
    at installDeps (dist/deploy.js:534:5)

2. Wrangler binary not found → ENOENT

detectProject checked hasCloudflarePlugin, hasRscPlugin, and hasWrangler only in {root}/node_modules, producing false negatives for all three when packages are hoisted. runWranglerDeploy hardcoded the same path, crashing with:

Error: spawnSync .../apps/web-next/node_modules/.bin/wrangler ENOENT
    at runWranglerDeploy (dist/deploy.js:622:20)

Solution

Both problems share the same root fix: walk up ancestor directories until the relevant file is found, the same way npm/pnpm/yarn themselves resolve workspace roots.

Rather than two separate ad-hoc loops, this PR introduces a single shared primitive:

// utils/project.ts
function walkUpUntil<T>(start: string, check: (dir: string) => T | null): T | null

Both features are then built on top of it:

// Lock file detection — now also recognises bun.lock (Bun v1.0+ text format)
detectPackageManager(root)     // uses walkUpUntil + checkLockFiles
detectPackageManagerName(root) // uses walkUpUntil + checkLockFiles

// node_modules resolution
findInNodeModules(start, subPath) // walks up to find node_modules/{subPath}

findInNodeModules replaces all four hardcoded fs.existsSync(path.join(root, "node_modules", ...)) calls in deploy.ts, and provides the resolved binary path to runWranglerDeploy.

Tests

12 new unit tests:

detectPackageManager
  ✓ detects pnpm from pnpm-lock.yaml
  ✓ detects yarn from yarn.lock
  ✓ detects bun from bun.lock (text format, Bun v1.0+)
  ✓ detects bun from bun.lockb (legacy binary format)
  ✓ falls back to npm when no lock file is found
  ✓ walks up to parent directory to find lock file (monorepo root)
  ✓ prefers the closest lock file when both child and parent have one

findInNodeModules
  ✓ finds a package in the immediate node_modules
  ✓ finds a binary in node_modules/.bin
  ✓ returns null when not found anywhere
  ✓ walks up to find package in monorepo root node_modules
  ✓ prefers the closest node_modules when both app and root have the package

Supersedes #119 and #121 (closing those).

/bonk


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/122 **Author:** [@harrisrobin](https://github.com/harrisrobin) **Created:** 2/26/2026 **Status:** ✅ Merged **Merged:** 3/7/2026 **Merged by:** [@southpolesteve](https://github.com/southpolesteve) **Base:** `main` ← **Head:** `fix/monorepo-deploy-support` --- ### 📝 Commits (2) - [`1fba5ae`](https://github.com/cloudflare/vinext/commit/1fba5ae1eaace854a82721a1d485614f129d0271) fix(deploy): monorepo-aware lock file and node_modules resolution - [`c638b86`](https://github.com/cloudflare/vinext/commit/c638b86a5f30ae55d0657d8df53fedf686479868) fix(test): clear npm_config_user_agent in npm fallback test ### 📊 Changes **3 files changed** (+185 additions, -30 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/deploy.ts` (+13 -15) 📝 `packages/vinext/src/utils/project.ts` (+68 -15) 📝 `tests/deploy.test.ts` (+104 -0) </details> ### 📄 Description ## Problem `vinext deploy` fails in monorepos in two distinct but related ways, both caused by the same assumption: that the project's lock file and `node_modules` are always in the app directory rather than potentially hoisted to a workspace root. ### 1. Wrong package manager detected → `npm error Invalid Version:` `detectPackageManager` only checked for `bun.lockb` (legacy binary format), missing `bun.lock` (text format, introduced in Bun v1.0). It also only searched the immediate project directory, so any monorepo where the lock file lives at the workspace root fell through to `npm install -D`, producing: ``` npm error Invalid Version: Error: Command failed: npm install -D @cloudflare/vite-plugin@latest ... at installDeps (dist/deploy.js:534:5) ``` ### 2. Wrangler binary not found → `ENOENT` `detectProject` checked `hasCloudflarePlugin`, `hasRscPlugin`, and `hasWrangler` only in `{root}/node_modules`, producing false negatives for all three when packages are hoisted. `runWranglerDeploy` hardcoded the same path, crashing with: ``` Error: spawnSync .../apps/web-next/node_modules/.bin/wrangler ENOENT at runWranglerDeploy (dist/deploy.js:622:20) ``` ## Solution Both problems share the same root fix: walk up ancestor directories until the relevant file is found, the same way npm/pnpm/yarn themselves resolve workspace roots. Rather than two separate ad-hoc loops, this PR introduces a single shared primitive: ```ts // utils/project.ts function walkUpUntil<T>(start: string, check: (dir: string) => T | null): T | null ``` Both features are then built on top of it: ```ts // Lock file detection — now also recognises bun.lock (Bun v1.0+ text format) detectPackageManager(root) // uses walkUpUntil + checkLockFiles detectPackageManagerName(root) // uses walkUpUntil + checkLockFiles // node_modules resolution findInNodeModules(start, subPath) // walks up to find node_modules/{subPath} ``` `findInNodeModules` replaces all four hardcoded `fs.existsSync(path.join(root, "node_modules", ...))` calls in `deploy.ts`, and provides the resolved binary path to `runWranglerDeploy`. ## Tests 12 new unit tests: ``` detectPackageManager ✓ detects pnpm from pnpm-lock.yaml ✓ detects yarn from yarn.lock ✓ detects bun from bun.lock (text format, Bun v1.0+) ✓ detects bun from bun.lockb (legacy binary format) ✓ falls back to npm when no lock file is found ✓ walks up to parent directory to find lock file (monorepo root) ✓ prefers the closest lock file when both child and parent have one findInNodeModules ✓ finds a package in the immediate node_modules ✓ finds a binary in node_modules/.bin ✓ returns null when not found anywhere ✓ walks up to find package in monorepo root node_modules ✓ prefers the closest node_modules when both app and root have the package ``` Supersedes #119 and #121 (closing those). /bonk --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 12:39:13 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#328
No description provided.