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`);