diff --git a/scripts/ensure-workspace-package-links.ts b/scripts/ensure-workspace-package-links.ts index 430ba5897b..8ff86b7188 100644 --- a/scripts/ensure-workspace-package-links.ts +++ b/scripts/ensure-workspace-package-links.ts @@ -1,10 +1,11 @@ #!/usr/bin/env -S node --import tsx -import { spawn } from "node:child_process"; +import fs from "node:fs/promises"; import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs"; import path from "node:path"; import { repoRoot } from "./dev-service-profile.ts"; type WorkspaceLinkMismatch = { + workspaceDir: string; packageName: string; expectedPath: string; actualPath: string | null; @@ -44,11 +45,11 @@ function discoverWorkspacePackagePaths(rootDir: string): Map { const workspacePackagePaths = discoverWorkspacePackagePaths(repoRoot); -function findServerWorkspaceLinkMismatches(): WorkspaceLinkMismatch[] { - const serverPackageJson = readJsonFile(path.join(repoRoot, "server", "package.json")); +function findWorkspaceLinkMismatches(workspaceDir: string): WorkspaceLinkMismatch[] { + const packageJson = readJsonFile(path.join(repoRoot, workspaceDir, "package.json")); const dependencies = { - ...(serverPackageJson.dependencies as Record | undefined), - ...(serverPackageJson.devDependencies as Record | undefined), + ...(packageJson.dependencies as Record | undefined), + ...(packageJson.devDependencies as Record | undefined), }; const mismatches: WorkspaceLinkMismatch[] = []; @@ -58,11 +59,12 @@ function findServerWorkspaceLinkMismatches(): WorkspaceLinkMismatch[] { const expectedPath = workspacePackagePaths.get(packageName); if (!expectedPath) continue; - const linkPath = path.join(repoRoot, "server", "node_modules", ...packageName.split("/")); + const linkPath = path.join(repoRoot, workspaceDir, "node_modules", ...packageName.split("/")); const actualPath = existsSync(linkPath) ? path.resolve(realpathSync(linkPath)) : null; if (actualPath === path.resolve(expectedPath)) continue; mismatches.push({ + workspaceDir, packageName, expectedPath: path.resolve(expectedPath), actualPath, @@ -72,53 +74,32 @@ function findServerWorkspaceLinkMismatches(): WorkspaceLinkMismatch[] { return mismatches; } -function runCommand(command: string, args: string[], cwd: string) { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - cwd, - env: process.env, - stdio: "inherit", - }); - - child.on("error", reject); - child.on("exit", (code, signal) => { - if (code === 0) { - resolve(); - return; - } - reject( - new Error( - `${command} ${args.join(" ")} failed with ${signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`}`, - ), - ); - }); - }); -} - -async function ensureServerWorkspaceLinksCurrent() { - const mismatches = findServerWorkspaceLinkMismatches(); +async function ensureWorkspaceLinksCurrent(workspaceDir: string) { + const mismatches = findWorkspaceLinkMismatches(workspaceDir); if (mismatches.length === 0) return; - console.log("[paperclip] detected stale workspace package links for server; relinking dependencies..."); + console.log(`[paperclip] detected stale workspace package links for ${workspaceDir}; relinking dependencies...`); for (const mismatch of mismatches) { console.log( `[paperclip] ${mismatch.packageName}: ${mismatch.actualPath ?? "missing"} -> ${mismatch.expectedPath}`, ); } - const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; - await runCommand( - pnpmBin, - ["install", "--force", "--config.confirmModulesPurge=false"], - repoRoot, - ); + for (const mismatch of mismatches) { + const linkPath = path.join(repoRoot, mismatch.workspaceDir, "node_modules", ...mismatch.packageName.split("/")); + await fs.mkdir(path.dirname(linkPath), { recursive: true }); + await fs.rm(linkPath, { recursive: true, force: true }); + await fs.symlink(mismatch.expectedPath, linkPath); + } - const remainingMismatches = findServerWorkspaceLinkMismatches(); + const remainingMismatches = findWorkspaceLinkMismatches(workspaceDir); if (remainingMismatches.length === 0) return; throw new Error( - `Workspace relink did not repair all server package links: ${remainingMismatches.map((item) => item.packageName).join(", ")}`, + `Workspace relink did not repair all ${workspaceDir} package links: ${remainingMismatches.map((item) => item.packageName).join(", ")}`, ); } -await ensureServerWorkspaceLinksCurrent(); +for (const workspaceDir of ["server", "ui"]) { + await ensureWorkspaceLinksCurrent(workspaceDir); +} diff --git a/scripts/provision-worktree.sh b/scripts/provision-worktree.sh index 861a60370a..bee0048a5f 100644 --- a/scripts/provision-worktree.sh +++ b/scripts/provision-worktree.sh @@ -361,7 +361,7 @@ if [[ -f "$worktree_cwd/package.json" && -f "$worktree_cwd/pnpm-lock.yaml" ]]; t done < <(list_base_node_modules_paths) if [[ "$needs_install" -eq 1 ]]; then - backup_suffix=".paperclip-backup-$BASHPID" + backup_suffix=".paperclip-backup-${BASHPID:-$$}" moved_symlink_paths=() while IFS= read -r relative_path; do diff --git a/server/src/__tests__/workspace-runtime.test.ts b/server/src/__tests__/workspace-runtime.test.ts index 0d6211c104..9d52356864 100644 --- a/server/src/__tests__/workspace-runtime.test.ts +++ b/server/src/__tests__/workspace-runtime.test.ts @@ -223,21 +223,7 @@ describe("ensureServerWorkspaceLinksCurrent", () => { ); await fs.symlink(stalePackageDir, path.join(serverNodeModulesScopeDir, "db")); - const commands: Array<{ command: string; args: string[]; cwd: string }> = []; - await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server"), { - runCommand: async (command, args, cwd) => { - commands.push({ command, args, cwd }); - await fs.rm(path.join(serverNodeModulesScopeDir, "db"), { force: true }); - await fs.symlink(expectedPackageDir, path.join(serverNodeModulesScopeDir, "db")); - }, - }); - - expect(commands).toHaveLength(1); - expect(commands[0]).toMatchObject({ - command: process.platform === "win32" ? "pnpm.cmd" : "pnpm", - args: ["install", "--force", "--config.confirmModulesPurge=false"], - cwd: repoRoot, - }); + await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server")); expect(await fs.realpath(path.join(serverNodeModulesScopeDir, "db"))).toBe(await fs.realpath(expectedPackageDir)); }); @@ -267,14 +253,7 @@ describe("ensureServerWorkspaceLinksCurrent", () => { ); await fs.symlink(expectedPackageDir, path.join(serverNodeModulesScopeDir, "db")); - let invoked = false; - await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server"), { - runCommand: async () => { - invoked = true; - }, - }); - - expect(invoked).toBe(false); + await ensureServerWorkspaceLinksCurrent(path.join(repoRoot, "server")); }); }); diff --git a/server/src/services/workspace-runtime.ts b/server/src/services/workspace-runtime.ts index e9e97548e8..4137a43fc9 100644 --- a/server/src/services/workspace-runtime.ts +++ b/server/src/services/workspace-runtime.ts @@ -208,35 +208,10 @@ function findServerWorkspaceLinkMismatches(rootDir: string): WorkspaceLinkMismat return mismatches; } -async function runCommand(command: string, args: string[], cwd: string) { - await new Promise((resolve, reject) => { - const child = spawn(command, args, { - cwd, - env: process.env, - stdio: "ignore", - shell: process.platform === "win32", - }); - - child.on("error", reject); - child.on("exit", (code, signal) => { - if (code === 0) { - resolve(); - return; - } - reject( - new Error( - `${command} ${args.join(" ")} failed with ${signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`}`, - ), - ); - }); - }); -} - export async function ensureServerWorkspaceLinksCurrent( startCwd: string, opts?: { onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise; - runCommand?: (command: string, args: string[], cwd: string) => Promise; }, ) { const workspaceRoot = findWorkspaceRoot(startCwd); @@ -255,12 +230,12 @@ export async function ensureServerWorkspaceLinksCurrent( } } - const pnpmBin = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; - await (opts?.runCommand ?? runCommand)( - pnpmBin, - ["install", "--force", "--config.confirmModulesPurge=false"], - workspaceRoot, - ); + for (const mismatch of mismatches) { + const linkPath = path.join(workspaceRoot, "server", "node_modules", ...mismatch.packageName.split("/")); + await fs.mkdir(path.dirname(linkPath), { recursive: true }); + await fs.rm(linkPath, { recursive: true, force: true }); + await fs.symlink(mismatch.expectedPath, linkPath); + } const remainingMismatches = findServerWorkspaceLinkMismatches(workspaceRoot); if (remainingMismatches.length === 0) return; diff --git a/ui/package.json b/ui/package.json index d344f67aea..094b716219 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,10 +14,11 @@ }, "type": "module", "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", + "preflight:workspace-links": "tsx ../scripts/ensure-workspace-package-links.ts", + "dev": "pnpm run preflight:workspace-links && vite", + "build": "pnpm run preflight:workspace-links && tsc -b && vite build", "preview": "vite preview", - "typecheck": "tsc -b", + "typecheck": "pnpm run preflight:workspace-links && tsc -b", "clean": "rm -rf dist tsconfig.tsbuildinfo", "prepack": "rm -f package.dev.json && cp package.json package.dev.json && node ../scripts/generate-ui-package-json.mjs", "postpack": "if [ -f package.dev.json ]; then mv package.dev.json package.json; fi"