123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import {
|
|
cp,
|
|
mkdir,
|
|
readFile,
|
|
readdir,
|
|
rm,
|
|
symlink,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
const fixtureRoot = path.resolve(".tmp/optional-recipe-removal");
|
|
const pnpmCli = requireEnvironment("npm_execpath");
|
|
const copyTargets = [
|
|
"src",
|
|
"tests",
|
|
"recipes",
|
|
"scripts",
|
|
"config",
|
|
"public",
|
|
".storybook",
|
|
"index.html",
|
|
"package.json",
|
|
"tsconfig.base.json",
|
|
"tsconfig.json",
|
|
"tsconfig.app.json",
|
|
"tsconfig.node.json",
|
|
"tsconfig.test.json",
|
|
"tsconfig.recipes.json",
|
|
"vite.config.ts",
|
|
"vitest.config.ts",
|
|
"playwright.config.ts",
|
|
"playwright.dev.config.ts",
|
|
"playwright.storybook.config.ts",
|
|
"playwright.visual.config.ts",
|
|
"eslint.config.ts",
|
|
".dependency-cruiser.json",
|
|
];
|
|
|
|
function requireEnvironment(name: string): string {
|
|
const value = process.env[name];
|
|
if (!value) {
|
|
throw new Error(`${name} is required to run removal verification`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function runPnpm(script: string): boolean {
|
|
return (
|
|
spawnSync(process.execPath, [pnpmCli, script], {
|
|
cwd: fixtureRoot,
|
|
stdio: "inherit",
|
|
}).status === 0
|
|
);
|
|
}
|
|
|
|
async function filesBelow(directory: string): Promise<string[]> {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
const groups = await Promise.all(
|
|
entries.map((entry) => {
|
|
const target = path.join(directory, entry.name);
|
|
return entry.isDirectory() ? filesBelow(target) : [target];
|
|
}),
|
|
);
|
|
return groups.flat();
|
|
}
|
|
|
|
await rm(fixtureRoot, { recursive: true, force: true });
|
|
await mkdir(fixtureRoot, { recursive: true });
|
|
for (const target of copyTargets) {
|
|
await cp(target, path.join(fixtureRoot, target), { recursive: true });
|
|
}
|
|
await symlink(path.resolve("node_modules"), path.join(fixtureRoot, "node_modules"), "dir");
|
|
await rm(path.join(fixtureRoot, "recipes"), { recursive: true, force: true });
|
|
await rm(path.join(fixtureRoot, "tests/recipes"), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
|
|
const checks: Array<[string, boolean]> = [
|
|
["typecheck", runPnpm("check:types")],
|
|
["architecture", runPnpm("check:architecture")],
|
|
["test", runPnpm("test:all")],
|
|
["build", runPnpm("build")],
|
|
];
|
|
const residue: string[] = [];
|
|
for (const file of await filesBelow(path.join(fixtureRoot, "dist"))) {
|
|
if (!/\.(?:js|css|html|json)$/.test(file)) continue;
|
|
const content = await readFile(file, "utf8");
|
|
if (content.includes("frontend-optional-recipe-must-not-reach-production")) {
|
|
residue.push(path.relative(fixtureRoot, file));
|
|
}
|
|
}
|
|
checks.push(["bundle-residue", residue.length === 0]);
|
|
const passed = checks.every(([, result]) => result);
|
|
await mkdir("artifacts/tests", { recursive: true });
|
|
await writeFile(
|
|
"artifacts/tests/optional-recipe-removal.xml",
|
|
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
`<testsuite name="optional-recipe-removal" tests="${checks.length}" failures="${passed ? 0 : 1}">` +
|
|
checks
|
|
.map(
|
|
([name, result]) =>
|
|
`<testcase name="${name}">${result ? "" : `<failure>${residue.join(", ")}</failure>`}</testcase>`,
|
|
)
|
|
.join("") +
|
|
`</testsuite>\n`,
|
|
);
|
|
await rm(fixtureRoot, { recursive: true, force: true });
|
|
if (!passed) {
|
|
process.stderr.write(
|
|
`Optional recipe removal failed: ${checks
|
|
.filter(([, result]) => !result)
|
|
.map(([name]) => name)
|
|
.join(", ")}\n`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
process.stdout.write(
|
|
`Optional recipe removal: PASS (${checks.length} base checks)\n`,
|
|
);
|