fix: reuse cloud web deps across worktrees
This commit is contained in:
@@ -28,6 +28,7 @@ vendor/
|
||||
|
||||
# Node package managers, for future web work
|
||||
node_modules/
|
||||
node_modules
|
||||
.pnpm-store/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
||||
+3
-2
@@ -23,8 +23,9 @@
|
||||
"deploy:desired-state:plan": "node scripts/run-bun.mjs scripts/deploy-desired-state-plan.mjs --pretty",
|
||||
"deploy:desired-state:check": "node scripts/run-bun.mjs scripts/deploy-desired-state-plan.mjs --check",
|
||||
"cli:client": "node scripts/run-bun.mjs tools/hwlab-cli/bin/hwlab-cli.ts client",
|
||||
"web:check": "cd web/hwlab-cloud-web && bun run check",
|
||||
"web:build": "(cd web/hwlab-cloud-web && bun run build)",
|
||||
"worktree:deps": "node scripts/worktree-deps.mjs --web",
|
||||
"web:check": "node scripts/worktree-deps.mjs --web && cd web/hwlab-cloud-web && bun run check",
|
||||
"web:build": "node scripts/worktree-deps.mjs --web && cd web/hwlab-cloud-web && bun run build",
|
||||
"artifact-catalog:preview-blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref HEAD --blocked --no-write",
|
||||
"lane:expand": "node scripts/hwlab-lane-expand.mjs",
|
||||
"gitops:render": "node scripts/run-bun.mjs scripts/gitops-render.mjs",
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const webRelativePath = path.join("web", "hwlab-cloud-web");
|
||||
const sharedWebDepsName = "hwlab-cloud-web-deps";
|
||||
const requiredWebPackages = ["vite", "vue", "@vitejs/plugin-vue", "vue-tsc", "vitest"];
|
||||
const requiredWebBins = [path.join(".bin", "vite"), path.join(".bin", "vue-tsc")];
|
||||
const allowedArgs = new Set(["--web", "--json", "--quiet", "--help"]);
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const argSet = new Set(args);
|
||||
|
||||
try {
|
||||
for (const arg of args) {
|
||||
if (!allowedArgs.has(arg)) fail(`Unknown argument: ${arg}`);
|
||||
}
|
||||
|
||||
if (argSet.has("--help")) {
|
||||
process.stdout.write(`Usage: node scripts/worktree-deps.mjs [--web] [--json] [--quiet]\n\nPrepares shared, ignored dependencies for HWLAB git worktrees.\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const repoRoot = findRepoRoot(process.cwd());
|
||||
const result = ensureWebDeps(repoRoot);
|
||||
writeResult(result);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (argSet.has("--json")) process.stdout.write(`${JSON.stringify({ ok: false, error: message })}\n`);
|
||||
else process.stderr.write(`hwlab worktree deps: ${message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function ensureWebDeps(repoRoot) {
|
||||
const webRoot = path.join(repoRoot, webRelativePath);
|
||||
const packagePath = path.join(webRoot, "package.json");
|
||||
const lockPath = path.join(webRoot, "bun.lock");
|
||||
if (!fs.existsSync(packagePath)) fail(`Missing ${path.relative(repoRoot, packagePath)}`);
|
||||
if (!fs.existsSync(lockPath)) fail(`Missing ${path.relative(repoRoot, lockPath)}`);
|
||||
|
||||
const fixedRoot = fixedRepoRoot(repoRoot);
|
||||
const sharedRoot = path.join(fixedRoot, ".worktree", sharedWebDepsName);
|
||||
const sharedNodeModules = path.join(sharedRoot, "node_modules");
|
||||
const nodeModulesPath = path.join(webRoot, "node_modules");
|
||||
const existing = tryLstat(nodeModulesPath);
|
||||
|
||||
if (hasRequiredWebDeps(nodeModulesPath)) {
|
||||
return {
|
||||
ok: true,
|
||||
mode: existing?.isSymbolicLink() ? "shared-symlink" : "local-existing",
|
||||
repoRoot,
|
||||
fixedRoot,
|
||||
webRoot,
|
||||
sharedRoot,
|
||||
nodeModulesPath,
|
||||
installRan: false,
|
||||
linked: existing?.isSymbolicLink() ?? false
|
||||
};
|
||||
}
|
||||
|
||||
if (existing && !existing.isSymbolicLink()) {
|
||||
if (!existing.isDirectory()) fail(`${path.relative(repoRoot, nodeModulesPath)} exists but is not a directory or symlink`);
|
||||
runBunInstall(webRoot);
|
||||
if (!hasRequiredWebDeps(nodeModulesPath)) fail("Local web dependency install completed but required packages are still missing");
|
||||
return {
|
||||
ok: true,
|
||||
mode: "local-install",
|
||||
repoRoot,
|
||||
fixedRoot,
|
||||
webRoot,
|
||||
sharedRoot,
|
||||
nodeModulesPath,
|
||||
installRan: true,
|
||||
linked: false
|
||||
};
|
||||
}
|
||||
|
||||
const shared = ensureSharedWebDeps(webRoot, sharedRoot, sharedNodeModules);
|
||||
linkNodeModules(nodeModulesPath, sharedNodeModules);
|
||||
if (!hasRequiredWebDeps(nodeModulesPath)) fail("Shared web dependency link completed but required packages are still missing");
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
mode: "shared-symlink",
|
||||
repoRoot,
|
||||
fixedRoot,
|
||||
webRoot,
|
||||
sharedRoot,
|
||||
nodeModulesPath,
|
||||
installRan: shared.installRan,
|
||||
linked: true
|
||||
};
|
||||
}
|
||||
|
||||
function ensureSharedWebDeps(webRoot, sharedRoot, sharedNodeModules) {
|
||||
fs.mkdirSync(sharedRoot, { recursive: true });
|
||||
const packageChanged = copyIfDifferent(path.join(webRoot, "package.json"), path.join(sharedRoot, "package.json"));
|
||||
const lockChanged = copyIfDifferent(path.join(webRoot, "bun.lock"), path.join(sharedRoot, "bun.lock"));
|
||||
const needsInstall = packageChanged || lockChanged || !hasRequiredWebDeps(sharedNodeModules);
|
||||
if (needsInstall) runBunInstall(sharedRoot);
|
||||
if (!hasRequiredWebDeps(sharedNodeModules)) fail(`Shared dependency install is incomplete at ${sharedNodeModules}`);
|
||||
return { installRan: needsInstall };
|
||||
}
|
||||
|
||||
function linkNodeModules(nodeModulesPath, sharedNodeModules) {
|
||||
const existing = tryLstat(nodeModulesPath);
|
||||
if (existing?.isSymbolicLink()) fs.unlinkSync(nodeModulesPath);
|
||||
else if (existing) fail(`${nodeModulesPath} already exists and is not a symlink`);
|
||||
const relativeTarget = path.relative(path.dirname(nodeModulesPath), sharedNodeModules) || sharedNodeModules;
|
||||
fs.symlinkSync(relativeTarget, nodeModulesPath, "dir");
|
||||
}
|
||||
|
||||
function runBunInstall(cwd) {
|
||||
const result = spawnSync("bun", ["install", "--frozen-lockfile"], { cwd, encoding: "utf8", stdio: ["ignore", "inherit", "inherit"] });
|
||||
if (result.status !== 0) fail(`bun install --frozen-lockfile failed in ${cwd} with exit ${result.status ?? "unknown"}`);
|
||||
}
|
||||
|
||||
function hasRequiredWebDeps(nodeModulesPath) {
|
||||
return requiredWebPackages.every((name) => fs.existsSync(path.join(nodeModulesPath, name))) && requiredWebBins.every((name) => fs.existsSync(path.join(nodeModulesPath, name)));
|
||||
}
|
||||
|
||||
function copyIfDifferent(source, target) {
|
||||
if (fs.existsSync(target) && fs.readFileSync(source).equals(fs.readFileSync(target))) return false;
|
||||
fs.mkdirSync(path.dirname(target), { recursive: true });
|
||||
fs.copyFileSync(source, target);
|
||||
return true;
|
||||
}
|
||||
|
||||
function findRepoRoot(startDir) {
|
||||
const gitResult = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: startDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
||||
if (gitResult.status === 0 && gitResult.stdout.trim()) return path.resolve(gitResult.stdout.trim());
|
||||
|
||||
let current = path.resolve(startDir);
|
||||
while (true) {
|
||||
if (fs.existsSync(path.join(current, "package.json")) && fs.existsSync(path.join(current, webRelativePath, "package.json"))) return current;
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
fail(`Unable to find HWLAB repo root from ${startDir}`);
|
||||
}
|
||||
|
||||
function fixedRepoRoot(repoRoot) {
|
||||
const marker = `${path.sep}.worktree${path.sep}`;
|
||||
const index = repoRoot.indexOf(marker);
|
||||
return index >= 0 ? repoRoot.slice(0, index) : repoRoot;
|
||||
}
|
||||
|
||||
function tryLstat(filePath) {
|
||||
try {
|
||||
return fs.lstatSync(filePath);
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function writeResult(result) {
|
||||
if (argSet.has("--json")) {
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
return;
|
||||
}
|
||||
if (argSet.has("--quiet")) return;
|
||||
const install = result.installRan ? "install=ran" : "install=skipped";
|
||||
const link = result.linked ? `node_modules -> ${path.relative(result.webRoot, path.join(result.sharedRoot, "node_modules"))}` : "node_modules=local";
|
||||
process.stdout.write(`hwlab worktree deps: web ready (${result.mode}, ${install}, ${link})\n`);
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
@@ -167,11 +167,26 @@ function ensureWebDeps(rootDir: string): void {
|
||||
const lockPath = path.join(rootDir, "bun.lock");
|
||||
const nodeModulesPath = path.join(rootDir, "node_modules");
|
||||
if (!fs.existsSync(lockPath)) return;
|
||||
if (fs.existsSync(path.join(nodeModulesPath, "vite")) && fs.existsSync(path.join(nodeModulesPath, "vue")) && fs.existsSync(path.join(nodeModulesPath, "@vitejs/plugin-vue"))) return;
|
||||
if (hasRequiredWebDeps(nodeModulesPath)) return;
|
||||
|
||||
const repoRoot = path.resolve(rootDir, "..", "..");
|
||||
const worktreeDepsHelper = path.join(repoRoot, "scripts", "worktree-deps.mjs");
|
||||
if (fs.existsSync(worktreeDepsHelper)) {
|
||||
const helperResult = spawnSync("node", [worktreeDepsHelper, "--web", "--quiet"], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "inherit", "inherit"] });
|
||||
if (helperResult.status !== 0) throw new Error(`worktree web dependency setup exited with code ${helperResult.status ?? "unknown"}`);
|
||||
if (hasRequiredWebDeps(nodeModulesPath)) return;
|
||||
throw new Error("worktree web dependency setup completed but required packages are still missing");
|
||||
}
|
||||
|
||||
const result = spawnSync("bun", ["install", "--frozen-lockfile"], { cwd: rootDir, encoding: "utf8", stdio: ["ignore", "inherit", "inherit"] });
|
||||
if (result.status !== 0) throw new Error(`bun install exited with code ${result.status ?? "unknown"}`);
|
||||
}
|
||||
|
||||
function hasRequiredWebDeps(nodeModulesPath: string): boolean {
|
||||
return ["vite", "vue", "@vitejs/plugin-vue", "vue-tsc", "vitest"].every((name) => fs.existsSync(path.join(nodeModulesPath, name))) &&
|
||||
[path.join(".bin", "vite"), path.join(".bin", "vue-tsc")].every((name) => fs.existsSync(path.join(nodeModulesPath, name)));
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const args = process.argv.slice(2);
|
||||
const rootIndex = args.indexOf("--root");
|
||||
|
||||
Reference in New Issue
Block a user