mirror of
https://github.com/paperclipai/paperclip
synced 2026-04-25 17:25:15 +02:00
Fix worktree provisioning and relinking
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -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<string, string> {
|
||||
|
||||
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<string, unknown> | undefined),
|
||||
...(serverPackageJson.devDependencies as Record<string, unknown> | undefined),
|
||||
...(packageJson.dependencies as Record<string, unknown> | undefined),
|
||||
...(packageJson.devDependencies as Record<string, unknown> | 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<void>((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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -208,35 +208,10 @@ function findServerWorkspaceLinkMismatches(rootDir: string): WorkspaceLinkMismat
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
async function runCommand(command: string, args: string[], cwd: string) {
|
||||
await new Promise<void>((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<void>;
|
||||
runCommand?: (command: string, args: string[], cwd: string) => Promise<void>;
|
||||
},
|
||||
) {
|
||||
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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user