feat: add removable reference feature vertical slice
This commit is contained in:
+185
-45
@@ -1,78 +1,218 @@
|
||||
import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
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/sample-removal");
|
||||
const sampleRoot = path.resolve("src/sample/contract-fixture");
|
||||
const sourceRoot = path.resolve("src");
|
||||
const fixtureRoot = path.resolve(".tmp/reference-feature-removal");
|
||||
const pnpmCli = /** @type {string} */ (process.env.npm_execpath);
|
||||
const featureSource = "src/features/reference-feature";
|
||||
const featureTests = "tests/features/reference-feature";
|
||||
const copyTargets = [
|
||||
"src",
|
||||
"tests",
|
||||
"scripts",
|
||||
"config",
|
||||
"public",
|
||||
"index.html",
|
||||
"package.json",
|
||||
"tsconfig.base.json",
|
||||
"tsconfig.json",
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.node.json",
|
||||
"tsconfig.test.json",
|
||||
"vite.config.js",
|
||||
"vitest.config.js",
|
||||
"playwright.config.js",
|
||||
"eslint.config.js",
|
||||
".dependency-cruiser.cjs",
|
||||
];
|
||||
|
||||
const emptyContracts = `import { PLATFORM_ROUTE_RUNTIME_CONTRACT } from "../contracts/route-runtime-contract.js";
|
||||
import { PLATFORM_ROUTE_REGISTRY } from "../contracts/routes.js";
|
||||
|
||||
export const INSTALLED_FEATURE_CONTRACTS =
|
||||
/** @type {readonly unknown[]} */ (Object.freeze([]));
|
||||
export const ROUTE_REGISTRY = PLATFORM_ROUTE_REGISTRY;
|
||||
export const ROUTE_RUNTIME_CONTRACT = PLATFORM_ROUTE_RUNTIME_CONTRACT;
|
||||
export const API_OPERATIONS = Object.freeze({});
|
||||
export const QUERY_REGISTRY = Object.freeze({});
|
||||
export const NAVIGATION_ROUTES = Object.freeze(
|
||||
Object.values(ROUTE_REGISTRY)
|
||||
.filter((definition) => definition.navigationOrder !== null)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
/** @type {number} */ (left.navigationOrder) -
|
||||
/** @type {number} */ (right.navigationOrder),
|
||||
),
|
||||
);
|
||||
/** @param {string} routeId */
|
||||
export function getRoute(routeId) {
|
||||
const registry =
|
||||
/** @type {Readonly<Record<string, import("../contracts/routes.js").RouteDefinition>>} */ (
|
||||
ROUTE_REGISTRY
|
||||
);
|
||||
const selected = registry[routeId];
|
||||
if (!selected) throw new Error(\`Unregistered route: \${routeId}\`);
|
||||
return selected;
|
||||
}
|
||||
/** @param {string} routeId */
|
||||
export function routePath(routeId) {
|
||||
return getRoute(routeId).path;
|
||||
}
|
||||
`;
|
||||
|
||||
const emptyRuntimes = `import { PLATFORM_ROUTE_CODECS } from "../presentation/routes/platform-route-codecs.js";
|
||||
import { PLATFORM_ROUTE_RUNTIME } from "../presentation/routes/route-runtime.js";
|
||||
|
||||
export const ROUTE_CODECS = PLATFORM_ROUTE_CODECS;
|
||||
export const ROUTE_RUNTIME = PLATFORM_ROUTE_RUNTIME;
|
||||
`;
|
||||
|
||||
const emptyAdapters = `type FeatureContext = Readonly<{
|
||||
createHttpClient(contract: Readonly<Record<string, unknown>>): unknown;
|
||||
}>;
|
||||
export function createInstalledFeatureInputs(_context: FeatureContext) {
|
||||
void _context;
|
||||
return Object.freeze({});
|
||||
}
|
||||
`;
|
||||
|
||||
/** @param {string} directory @returns {Promise<string[]>} */
|
||||
async function sourceFiles(directory) {
|
||||
async function filesBelow(directory) {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const nested = /** @type {string[][]} */ (await Promise.all(
|
||||
const groups = await Promise.all(
|
||||
entries.map((entry) => {
|
||||
const target = path.join(directory, entry.name);
|
||||
return entry.isDirectory() ? sourceFiles(target) : [target];
|
||||
return entry.isDirectory() ? filesBelow(target) : [target];
|
||||
}),
|
||||
));
|
||||
return nested.flat();
|
||||
);
|
||||
return groups.flat();
|
||||
}
|
||||
|
||||
/** @param {string} script @param {string[]} [extra] */
|
||||
function runPnpm(script, extra = []) {
|
||||
const result = spawnSync(process.execPath, [pnpmCli, script, ...extra], {
|
||||
cwd: fixtureRoot,
|
||||
stdio: "inherit",
|
||||
});
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
const incomingImports = [];
|
||||
for (const sourceFile of await sourceFiles(sourceRoot)) {
|
||||
if (sourceFile.startsWith(sampleRoot)) continue;
|
||||
const content = await readFile(sourceFile, "utf8");
|
||||
if (/from\s+["'][^"']*sample\/contract-fixture/.test(content)) {
|
||||
incomingImports.push(path.relative(".", sourceFile));
|
||||
await rm(path.join(fixtureRoot, featureSource), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
await rm(path.join(fixtureRoot, featureTests), {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
await writeFile(
|
||||
path.join(fixtureRoot, "src/features/installed-feature-contracts.js"),
|
||||
emptyContracts,
|
||||
);
|
||||
await writeFile(
|
||||
path.join(fixtureRoot, "src/features/installed-feature-runtimes.tsx"),
|
||||
emptyRuntimes,
|
||||
);
|
||||
await writeFile(
|
||||
path.join(fixtureRoot, "src/features/installed-feature-adapters.ts"),
|
||||
emptyAdapters,
|
||||
);
|
||||
|
||||
/** @type {string[]} */
|
||||
const residue = [];
|
||||
for (const root of ["src", "tests"]) {
|
||||
for (const file of await filesBelow(path.join(fixtureRoot, root))) {
|
||||
const relative = path.relative(fixtureRoot, file);
|
||||
const content = await readFile(file, "utf8");
|
||||
if (
|
||||
/REFERENCE_RESOURCE|reference-feature|reference-resource/i.test(
|
||||
`${relative}\n${content}`,
|
||||
)
|
||||
) {
|
||||
residue.push(relative);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let buildStatus = 1;
|
||||
if (incomingImports.length === 0) {
|
||||
await cp("src", path.join(fixtureRoot, "src"), {
|
||||
recursive: true,
|
||||
filter: (source) => !source.startsWith(sampleRoot),
|
||||
});
|
||||
await cp("public", path.join(fixtureRoot, "public"), { recursive: true });
|
||||
await cp("index.html", path.join(fixtureRoot, "index.html"));
|
||||
await cp("vite.config.js", path.join(fixtureRoot, "vite.config.js"));
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
pnpmCli,
|
||||
"exec",
|
||||
"vite",
|
||||
"build",
|
||||
fixtureRoot,
|
||||
"--outDir",
|
||||
path.join(fixtureRoot, "dist"),
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
buildStatus = result.status ?? 1;
|
||||
const checks = [
|
||||
["typecheck", runPnpm("check:types")],
|
||||
["architecture", runPnpm("check:architecture")],
|
||||
["registry", runPnpm("check:registries")],
|
||||
["unit-integration", runPnpm("test:all")],
|
||||
[
|
||||
"home-smoke",
|
||||
runPnpm("exec", [
|
||||
"vitest",
|
||||
"run",
|
||||
"tests/component/router.test.jsx",
|
||||
"--reporter=default",
|
||||
]),
|
||||
],
|
||||
["build", runPnpm("build")],
|
||||
];
|
||||
/** @type {string[]} */
|
||||
const builtResidue = [];
|
||||
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 (
|
||||
/REFERENCE_RESOURCE|reference-feature|reference-resource/i.test(content)
|
||||
) {
|
||||
builtResidue.push(path.relative(fixtureRoot, file));
|
||||
}
|
||||
}
|
||||
const routeCatalog = await import(
|
||||
`${new URL(
|
||||
"../src/features/installed-feature-contracts.js",
|
||||
`file://${fixtureRoot}/scripts/`,
|
||||
).href}?removed=${Date.now()}`
|
||||
);
|
||||
const routeIds = Object.keys(routeCatalog.ROUTE_REGISTRY);
|
||||
const routeAbsent = routeIds.every((routeId) => !routeId.startsWith("REFERENCE_"));
|
||||
checks.push(["route-absent", routeAbsent]);
|
||||
checks.push(["fixture-id-residue", residue.length === 0]);
|
||||
checks.push(["built-fixture-id-residue", builtResidue.length === 0]);
|
||||
|
||||
const passed = checks.every(([, result]) => result);
|
||||
await mkdir("artifacts/tests", { recursive: true });
|
||||
const passed = incomingImports.length === 0 && buildStatus === 0;
|
||||
await writeFile(
|
||||
"artifacts/tests/sample-removal.xml",
|
||||
`<?xml version="1.0" encoding="UTF-8"?>\n` +
|
||||
`<testsuite name="sample-removal" tests="2" failures="${passed ? 0 : 1}">` +
|
||||
`<testcase name="no-product-import"/>` +
|
||||
`<testcase name="production-build">${passed ? "" : "<failure/>"}</testcase>` +
|
||||
`<testsuite name="reference-feature-removal" tests="${checks.length}" failures="${passed ? 0 : 1}">` +
|
||||
checks
|
||||
.map(
|
||||
([name, result]) =>
|
||||
`<testcase name="${name}">${result ? "" : `<failure>${[...residue, ...builtResidue].join(", ")}</failure>`}</testcase>`,
|
||||
)
|
||||
.join("") +
|
||||
`</testsuite>\n`,
|
||||
);
|
||||
await rm(fixtureRoot, { recursive: true, force: true });
|
||||
|
||||
if (!passed) {
|
||||
const failures = checks
|
||||
.filter(([, result]) => !result)
|
||||
.map(([name]) => name);
|
||||
process.stderr.write(
|
||||
`Sample removal failed. Incoming imports: ${incomingImports.join(", ")}\n`,
|
||||
`Reference feature removal failed: ${failures.join(", ")}; residue: ${[...residue, ...builtResidue].join(", ")}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write("Sample removal smoke: PASS\n");
|
||||
process.stdout.write(
|
||||
`Reference feature removal: PASS (${checks.length} checks, no fixture IDs)\n`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user