build: remove stale backend distribution files
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { rm } from "node:fs/promises";
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
||||
const backendRoot = resolve(scriptDirectory, "..");
|
||||
const target = resolve(backendRoot, "dist");
|
||||
|
||||
if (dirname(target) !== backendRoot || basename(target) !== "dist") {
|
||||
throw new Error(`Refusing to clean non-dist target: ${target}`);
|
||||
}
|
||||
|
||||
await rm(target, { recursive: true, force: true });
|
||||
@@ -0,0 +1,147 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import {
|
||||
access, cp, lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const backendRoot = fileURLToPath(new URL("../", import.meta.url));
|
||||
const ownedRoots = [];
|
||||
|
||||
function packageBuildInvocation(platform = process.platform, environment = process.env) {
|
||||
if (platform === "win32") {
|
||||
const comspec = environment.ComSpec ?? environment.COMSPEC;
|
||||
if (!comspec) throw new Error("ComSpec is required to run npm on Windows.");
|
||||
return { executable: comspec, args: ["/d", "/s", "/c", "npm.cmd run build"] };
|
||||
}
|
||||
return { executable: "npm", args: ["run", "build"] };
|
||||
}
|
||||
|
||||
async function isMissing(path) {
|
||||
try {
|
||||
await access(path);
|
||||
return false;
|
||||
} catch (error) {
|
||||
if (error?.code === "ENOENT") return true;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createOwnedRoot(prefix) {
|
||||
const root = await mkdtemp(join(tmpdir(), prefix));
|
||||
ownedRoots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
async function copyCleaner(fixtureRoot) {
|
||||
await mkdir(join(fixtureRoot, "scripts"), { recursive: true });
|
||||
const cleaner = join(fixtureRoot, "scripts", "clean-dist.mjs");
|
||||
await cp(join(backendRoot, "scripts", "clean-dist.mjs"), cleaner);
|
||||
return cleaner;
|
||||
}
|
||||
|
||||
async function createBackendFixture() {
|
||||
const fixtureRoot = await createOwnedRoot("thoth-backend-clean-dist-");
|
||||
await Promise.all([
|
||||
cp(join(backendRoot, "package.json"), join(fixtureRoot, "package.json")),
|
||||
cp(join(backendRoot, "tsconfig.json"), join(fixtureRoot, "tsconfig.json")),
|
||||
cp(join(backendRoot, "src"), join(fixtureRoot, "src"), { recursive: true }),
|
||||
copyCleaner(fixtureRoot),
|
||||
]);
|
||||
const dependencyRoot = join(backendRoot, "node_modules");
|
||||
const dependencyEntry = await lstat(dependencyRoot);
|
||||
if (!dependencyEntry.isDirectory() || dependencyEntry.isSymbolicLink()) {
|
||||
throw new Error("Backend node_modules must be a real directory.");
|
||||
}
|
||||
await symlink(
|
||||
dependencyRoot,
|
||||
join(fixtureRoot, "node_modules"),
|
||||
process.platform === "win32" ? "junction" : "dir",
|
||||
);
|
||||
return fixtureRoot;
|
||||
}
|
||||
|
||||
async function removeOwnedRoot(root) {
|
||||
for (const childName of ["node_modules", "dist"]) {
|
||||
const child = join(root, childName);
|
||||
try {
|
||||
const entry = await lstat(child);
|
||||
if (entry.isSymbolicLink()) {
|
||||
await rm(child, { recursive: true, force: true });
|
||||
} else if (childName === "node_modules") {
|
||||
throw new Error(`Refusing to clean fixture with a non-link node_modules: ${root}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.code !== "ENOENT") throw error;
|
||||
}
|
||||
}
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
test.afterEach(async () => {
|
||||
for (const root of ownedRoots.splice(0)) await removeOwnedRoot(root);
|
||||
});
|
||||
|
||||
test("Windows package builds use ComSpec instead of executing npm.cmd directly", () => {
|
||||
assert.deepEqual(
|
||||
packageBuildInvocation("win32", { ComSpec: "C:\\Windows\\System32\\cmd.exe" }),
|
||||
{
|
||||
executable: "C:\\Windows\\System32\\cmd.exe",
|
||||
args: ["/d", "/s", "/c", "npm.cmd run build"],
|
||||
},
|
||||
);
|
||||
assert.throws(() => packageBuildInvocation("win32", {}), /ComSpec is required/);
|
||||
});
|
||||
|
||||
test("cleaner is idempotent and removes a dist link without following it", async () => {
|
||||
const fixtureRoot = await createOwnedRoot("thoth-backend-cleaner-");
|
||||
const cleaner = await copyCleaner(fixtureRoot);
|
||||
const fixtureDist = join(fixtureRoot, "dist");
|
||||
|
||||
await execFileAsync(process.execPath, [cleaner], { cwd: fixtureRoot });
|
||||
assert.equal(await isMissing(fixtureDist), true);
|
||||
await execFileAsync(process.execPath, [cleaner], { cwd: fixtureRoot });
|
||||
assert.equal(await isMissing(fixtureDist), true);
|
||||
|
||||
const outsideRoot = await createOwnedRoot("thoth-backend-cleaner-outside-");
|
||||
const outsideSentinel = join(outsideRoot, "sentinel.txt");
|
||||
await writeFile(outsideSentinel, "outside-owned-data\n", "utf8");
|
||||
await symlink(outsideRoot, fixtureDist, process.platform === "win32" ? "junction" : "dir");
|
||||
|
||||
await execFileAsync(process.execPath, [cleaner], { cwd: fixtureRoot });
|
||||
|
||||
assert.equal(await isMissing(fixtureDist), true, "dist link survived cleaner");
|
||||
assert.equal(await readFile(outsideSentinel, "utf8"), "outside-owned-data\n");
|
||||
});
|
||||
|
||||
test("package build replaces the complete backend distribution in an owned fixture", async () => {
|
||||
const fixtureRoot = await createBackendFixture();
|
||||
const copiedPackage = JSON.parse(await readFile(join(fixtureRoot, "package.json"), "utf8"));
|
||||
assert.equal(copiedPackage.scripts.prebuild, "node scripts/clean-dist.mjs");
|
||||
|
||||
const workspacesDist = join(fixtureRoot, "dist", "workspaces");
|
||||
const staleModules = [
|
||||
"stale-build-sentinel.js",
|
||||
"migrate-legacy.js",
|
||||
"migrate-v2-qdrant.js",
|
||||
].map((name) => join(workspacesDist, name));
|
||||
await mkdir(workspacesDist, { recursive: true });
|
||||
await Promise.all(staleModules.map((path) => writeFile(path, "export const stale = true;\n", "utf8")));
|
||||
|
||||
const { executable, args } = packageBuildInvocation();
|
||||
await execFileAsync(executable, args, { cwd: fixtureRoot });
|
||||
|
||||
for (const path of staleModules) {
|
||||
assert.equal(await isMissing(path), true, `stale module survived the package build: ${path}`);
|
||||
}
|
||||
assert.equal(
|
||||
await isMissing(join(fixtureRoot, "dist", "server.js")),
|
||||
false,
|
||||
"server output was not compiled",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user