Files

173 lines
6.6 KiB
JavaScript

#!/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", "@playwright/test"];
const requiredWebBins = [path.join(".bin", "vite"), path.join(".bin", "vue-tsc"), path.join(".bin", "playwright")];
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);
}