[PR #121] [CLOSED] fix(deploy): resolve node_modules by walking up to monorepo root #329

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

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/121
Author: @harrisrobin
Created: 2/26/2026
Status: Closed

Base: mainHead: fix/monorepo-node-modules-resolution


📝 Commits (4)

  • dd656fe chore: add .worktrees/ to .gitignore
  • bf3a90e fix(deploy): detect bun.lock text format and walk up to monorepo root
  • a5875a1 fix(routing): pass exclude as function to node:fs/promises.glob
  • cb6c314 fix(deploy): resolve node_modules by walking up to monorepo root

📊 Changes

6 files changed (+198 additions, -27 deletions)

View changed files

📝 .gitignore (+3 -0)
📝 packages/vinext/src/deploy.ts (+13 -15)
📝 packages/vinext/src/routing/app-router.ts (+7 -3)
📝 packages/vinext/src/routing/pages-router.ts (+7 -1)
📝 packages/vinext/src/utils/project.ts (+67 -8)
📝 tests/deploy.test.ts (+101 -0)

📄 Description

Problem

vinext deploy fails in monorepos with an ENOENT crash when the wrangler binary is hoisted to the workspace root rather than installed in the app's own node_modules:

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

Three separate places hardcode path.join(root, "node_modules", ...) and fail to find hoisted packages:

  1. detectProjecthasCloudflarePlugin, hasRscPlugin, hasWrangler all check the app-level node_modules only, producing false negatives for all three in a monorepo.
  2. getMissingDepshasMdxRollup has the same issue.
  3. runWranglerDeploy — constructs the wrangler binary path from the app root, crashing with ENOENT when the binary is at the workspace root.

The false negatives in (1) and (2) also cause vinext to try to re-install packages that are already present, adding unnecessary install steps.

Fix

Add findInNodeModules(start, subPath) to utils/project.ts — it walks up parent directories until it finds node_modules/{subPath}, returning the absolute path or null. This mirrors the same approach already used for lock file detection in detectPackageManager.

All four callsites are updated to use it:

// Before
const hasWrangler = fs.existsSync(path.join(root, "node_modules", ".bin", "wrangler"));
const wranglerBin = path.join(root, "node_modules", ".bin", "wrangler");

// After
const hasWrangler = findInNodeModules(root, ".bin/wrangler") !== null;
const wranglerBin = findInNodeModules(root, ".bin/wrangler") ?? path.join(root, "node_modules", ".bin", "wrangler");

Tests

5 new unit tests for 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

pnpm run typecheck and pnpm run lint pass clean.

/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/121 **Author:** [@harrisrobin](https://github.com/harrisrobin) **Created:** 2/26/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix/monorepo-node-modules-resolution` --- ### 📝 Commits (4) - [`dd656fe`](https://github.com/cloudflare/vinext/commit/dd656fee5b07e59eb887614a43dd9069fae227f3) chore: add .worktrees/ to .gitignore - [`bf3a90e`](https://github.com/cloudflare/vinext/commit/bf3a90e2a9fcc488f00008c1a7943113172bb9df) fix(deploy): detect bun.lock text format and walk up to monorepo root - [`a5875a1`](https://github.com/cloudflare/vinext/commit/a5875a1c1f020e1e6dd3bc72afd7e4fd4b2dfff4) fix(routing): pass exclude as function to node:fs/promises.glob - [`cb6c314`](https://github.com/cloudflare/vinext/commit/cb6c3145cee82b364a5af9078d2f111823af6c66) fix(deploy): resolve node_modules by walking up to monorepo root ### 📊 Changes **6 files changed** (+198 additions, -27 deletions) <details> <summary>View changed files</summary> 📝 `.gitignore` (+3 -0) 📝 `packages/vinext/src/deploy.ts` (+13 -15) 📝 `packages/vinext/src/routing/app-router.ts` (+7 -3) 📝 `packages/vinext/src/routing/pages-router.ts` (+7 -1) 📝 `packages/vinext/src/utils/project.ts` (+67 -8) 📝 `tests/deploy.test.ts` (+101 -0) </details> ### 📄 Description ## Problem `vinext deploy` fails in monorepos with an ENOENT crash when the wrangler binary is hoisted to the workspace root rather than installed in the app's own `node_modules`: ``` Error: spawnSync .../apps/web-next/node_modules/.bin/wrangler ENOENT at runWranglerDeploy (dist/deploy.js:622:20) ``` Three separate places hardcode `path.join(root, "node_modules", ...)` and fail to find hoisted packages: 1. **`detectProject`** — `hasCloudflarePlugin`, `hasRscPlugin`, `hasWrangler` all check the app-level `node_modules` only, producing false negatives for all three in a monorepo. 2. **`getMissingDeps`** — `hasMdxRollup` has the same issue. 3. **`runWranglerDeploy`** — constructs the wrangler binary path from the app root, crashing with ENOENT when the binary is at the workspace root. The false negatives in (1) and (2) also cause vinext to try to re-install packages that are already present, adding unnecessary install steps. ## Fix Add `findInNodeModules(start, subPath)` to `utils/project.ts` — it walks up parent directories until it finds `node_modules/{subPath}`, returning the absolute path or `null`. This mirrors the same approach already used for lock file detection in `detectPackageManager`. All four callsites are updated to use it: ```ts // Before const hasWrangler = fs.existsSync(path.join(root, "node_modules", ".bin", "wrangler")); const wranglerBin = path.join(root, "node_modules", ".bin", "wrangler"); // After const hasWrangler = findInNodeModules(root, ".bin/wrangler") !== null; const wranglerBin = findInNodeModules(root, ".bin/wrangler") ?? path.join(root, "node_modules", ".bin", "wrangler"); ``` ## Tests 5 new unit tests for `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 ``` `pnpm run typecheck` and `pnpm run lint` pass clean. /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:14 +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#329
No description provided.