From 15ce85f7f80abe5df95ceac8a8f7a3ad838a7ca4 Mon Sep 17 00:00:00 2001 From: aaron3312 Date: Sun, 1 Mar 2026 03:41:26 -0600 Subject: [PATCH] fix(cli): resolve bin path from cwd to support monorepo workspace hoisting When using npm/bun/yarn workspaces, packages are hoisted to a parent node_modules. The previous require.resolve('.bin/') resolved relative to the CLI module location, which fails when the CLI is installed in a different node_modules subtree than the project binaries. resolveBin() now walks up from process.cwd() searching each node_modules/.bin/ until the binary is found, falling back to the original require.resolve behavior for backward compatibility. On Windows, both .exe and .cmd extensions are checked. Fixes #7314 --- .changeset/eleven-needles-teach.md | 7 +++ .../commands/runner/projectScripts.test.ts | 49 ++++++++++++++++++- .../cli/src/commands/runner/projectScripts.ts | 22 +++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 .changeset/eleven-needles-teach.md diff --git a/.changeset/eleven-needles-teach.md b/.changeset/eleven-needles-teach.md new file mode 100644 index 0000000000000..4eaa911126387 --- /dev/null +++ b/.changeset/eleven-needles-teach.md @@ -0,0 +1,7 @@ +--- +"@refinedev/cli": patch +--- + +fix: resolve bin path from cwd to support monorepo workspace hoisting + +`resolveBin()` now walks up from `process.cwd()` to find `node_modules/.bin/`, fixing `MODULE_NOT_FOUND` errors when running `refine dev` in npm/bun/yarn workspaces where packages are hoisted to a parent `node_modules`. diff --git a/packages/cli/src/commands/runner/projectScripts.test.ts b/packages/cli/src/commands/runner/projectScripts.test.ts index 2625eb6b5f73e..ce1454bb25fab 100644 --- a/packages/cli/src/commands/runner/projectScripts.test.ts +++ b/packages/cli/src/commands/runner/projectScripts.test.ts @@ -1,4 +1,6 @@ -import { vi } from "vitest"; +import { vi, afterEach, describe, test, expect } from "vitest"; +import path from "path"; +import fs from "fs"; import { projectScripts } from "./projectScripts"; import { ProjectTypes } from "@definitions/projectTypes"; @@ -462,3 +464,48 @@ describe("UNKNOWN project type", () => { }); }); }); + +describe("resolveBin (workspace support)", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test("should find binary in cwd node_modules/.bin (standard setup)", () => { + const mockCwd = "/project"; + vi.spyOn(process, "cwd").mockReturnValue(mockCwd); + vi.spyOn(fs, "existsSync").mockImplementation((p) => { + return p === path.join(mockCwd, "node_modules", ".bin", "vite"); + }); + + expect(projectScripts[ProjectTypes.VITE].getBin()).toBe( + path.join(mockCwd, "node_modules", ".bin", "vite"), + ); + }); + + test("should find binary in parent node_modules/.bin (monorepo workspace setup)", () => { + const mockCwd = "/project/apps/admin"; + const workspaceRoot = "/project"; + vi.spyOn(process, "cwd").mockReturnValue(mockCwd); + vi.spyOn(fs, "existsSync").mockImplementation((p) => { + // Binary is NOT in the app's local node_modules, but IS in the workspace root + return p === path.join(workspaceRoot, "node_modules", ".bin", "vite"); + }); + + expect(projectScripts[ProjectTypes.VITE].getBin()).toBe( + path.join(workspaceRoot, "node_modules", ".bin", "vite"), + ); + }); + + test("should find binary for next.js in parent node_modules/.bin (monorepo workspace setup)", () => { + const mockCwd = "/project/apps/admin"; + const workspaceRoot = "/project"; + vi.spyOn(process, "cwd").mockReturnValue(mockCwd); + vi.spyOn(fs, "existsSync").mockImplementation((p) => { + return p === path.join(workspaceRoot, "node_modules", ".bin", "next"); + }); + + expect(projectScripts[ProjectTypes.NEXTJS].getBin()).toBe( + path.join(workspaceRoot, "node_modules", ".bin", "next"), + ); + }); +}); diff --git a/packages/cli/src/commands/runner/projectScripts.ts b/packages/cli/src/commands/runner/projectScripts.ts index 65a4b8779a2d9..e49eb9e3e1dd0 100644 --- a/packages/cli/src/commands/runner/projectScripts.ts +++ b/packages/cli/src/commands/runner/projectScripts.ts @@ -1,6 +1,28 @@ +import path from "path"; +import fs from "fs"; import { ProjectTypes } from "@definitions/projectTypes"; function resolveBin(name: string) { + // Walk up from the current working directory to find node_modules/.bin/${name}. + // This handles monorepo/workspace setups where packages may be hoisted to a + // parent directory's node_modules. + let dir = process.cwd(); + while (true) { + if (process.platform === "win32") { + const exePath = path.join(dir, "node_modules", ".bin", `${name}.exe`); + if (fs.existsSync(exePath)) return exePath; + const cmdPath = path.join(dir, "node_modules", ".bin", `${name}.cmd`); + if (fs.existsSync(cmdPath)) return cmdPath; + } else { + const binPath = path.join(dir, "node_modules", ".bin", name); + if (fs.existsSync(binPath)) return binPath; + } + const parent = path.dirname(dir); + if (parent === dir) break; // reached filesystem root + dir = parent; + } + + // Fall back to require.resolve for backward compatibility if (process.platform === "win32") { try { return require.resolve(`.bin/${name}.exe`);