211 lines
7.4 KiB
TypeScript
211 lines
7.4 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Migrates every packages/<plugin>/<pkg>/vitest.config.ts to use
|
|
* `createTestConfig` from `@suaveplan/testing/config`. Also:
|
|
*
|
|
* - Adds `@suaveplan/testing` as a workspace devDependency where missing.
|
|
* - Removes the 81 per-package `tests/setup/polyfills.ts` files.
|
|
* - Heuristically detects async-heavy packages (those whose existing
|
|
* config sets `testTimeout: 30000` or higher) and preserves the timeout.
|
|
* - Heuristically detects DOM packages (env: happy-dom) and preserves
|
|
* `environment` + sets `fileParallelism: false`.
|
|
*
|
|
* Run:
|
|
* bun scripts/migrate-vitest-configs.ts # apply
|
|
* bun scripts/migrate-vitest-configs.ts --dry # preview
|
|
*/
|
|
|
|
import {
|
|
existsSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
rmSync,
|
|
statSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const repoRoot = "/mnt/s/web/genesis";
|
|
const packagesRoot = join(repoRoot, "packages");
|
|
const TESTING_PKG = "@suaveplan/testing";
|
|
const TESTING_OWN_PATH = join(packagesRoot, "dev", "testing");
|
|
|
|
const dry = process.argv.includes("--dry");
|
|
|
|
interface Detected {
|
|
timeout: number;
|
|
hookTimeout?: number;
|
|
environment: "node" | "happy-dom" | "jsdom";
|
|
fileParallelism: boolean;
|
|
additionalSetupFiles: string[];
|
|
additionalIncludes: string[];
|
|
additionalCoverageExcludes: string[];
|
|
}
|
|
|
|
function detect(content: string): Detected {
|
|
const out: Detected = {
|
|
timeout: 5000,
|
|
environment: "node",
|
|
fileParallelism: true,
|
|
additionalSetupFiles: [],
|
|
additionalIncludes: [],
|
|
additionalCoverageExcludes: [],
|
|
};
|
|
const tm = content.match(/testTimeout:\s*(\d+)/);
|
|
if (tm?.[1] !== undefined) out.timeout = Number(tm[1]);
|
|
const hm = content.match(/hookTimeout:\s*(\d+)/);
|
|
if (hm?.[1] !== undefined) out.hookTimeout = Number(hm[1]);
|
|
if (/environment:\s*"happy-dom"/.test(content))
|
|
out.environment = "happy-dom";
|
|
else if (/environment:\s*"jsdom"/.test(content)) out.environment = "jsdom";
|
|
if (/fileParallelism:\s*false/.test(content)) out.fileParallelism = false;
|
|
const setupMatch = content.match(/setupFiles:\s*\[([^\]]*)\]/s);
|
|
if (setupMatch?.[1]) {
|
|
const entries = setupMatch[1]
|
|
.split(",")
|
|
.map((s) => s.trim().replace(/["']/g, ""))
|
|
.filter(Boolean)
|
|
.filter(
|
|
(p) =>
|
|
!p.endsWith("polyfills.ts") && !p.endsWith("polyfills.js")
|
|
);
|
|
out.additionalSetupFiles = entries;
|
|
}
|
|
const incMatch = content.match(/include:\s*\[([^\]]*)\]/s);
|
|
if (incMatch?.[1]) {
|
|
const entries = incMatch[1]
|
|
.split(",")
|
|
.map((s) => s.trim().replace(/["']/g, ""))
|
|
.filter(Boolean)
|
|
.filter(
|
|
(p) => p !== "src/**/*.test.ts" && p !== "tests/**/*.test.ts"
|
|
);
|
|
out.additionalIncludes = entries;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function buildConfig(d: Detected): string {
|
|
const opts: string[] = [];
|
|
if (d.environment !== "node") opts.push(`environment: "${d.environment}"`);
|
|
if (d.timeout !== 5000) opts.push(`testTimeout: ${d.timeout}`);
|
|
if (d.hookTimeout !== undefined && d.hookTimeout !== d.timeout)
|
|
opts.push(`hookTimeout: ${d.hookTimeout}`);
|
|
if (d.fileParallelism === false) opts.push("fileParallelism: false");
|
|
if (d.additionalSetupFiles.length > 0) {
|
|
opts.push(
|
|
`additionalSetupFiles: [${d.additionalSetupFiles.map((s) => `"${s}"`).join(", ")}]`
|
|
);
|
|
}
|
|
if (d.additionalIncludes.length > 0) {
|
|
opts.push(
|
|
`additionalIncludes: [${d.additionalIncludes.map((s) => `"${s}"`).join(", ")}]`
|
|
);
|
|
}
|
|
|
|
const optsBlock = opts.length === 0 ? "" : `{\n\t${opts.join(",\n\t")},\n}`;
|
|
const call =
|
|
optsBlock === ""
|
|
? "createTestConfig()"
|
|
: `createTestConfig(${optsBlock})`;
|
|
|
|
return `import { createTestConfig } from "@suaveplan/testing/config";\n\nexport default ${call};\n`;
|
|
}
|
|
|
|
function ensureTestingDep(pkgJsonPath: string): boolean {
|
|
const raw = readFileSync(pkgJsonPath, "utf8");
|
|
const pj = JSON.parse(raw);
|
|
const dev = pj.devDependencies ?? (pj.devDependencies = {});
|
|
if (dev[TESTING_PKG] !== undefined) return false;
|
|
dev[TESTING_PKG] = "workspace:^";
|
|
const sorted: Record<string, string> = {};
|
|
for (const k of Object.keys(dev).sort()) sorted[k] = dev[k];
|
|
pj.devDependencies = sorted;
|
|
if (!dry) writeFileSync(pkgJsonPath, `${JSON.stringify(pj, null, 2)}\n`);
|
|
return true;
|
|
}
|
|
|
|
function migrateOne(pkgDir: string): {
|
|
configChanged: boolean;
|
|
depAdded: boolean;
|
|
polyfillRemoved: boolean;
|
|
} {
|
|
const result = {
|
|
configChanged: false,
|
|
depAdded: false,
|
|
polyfillRemoved: false,
|
|
};
|
|
const cfgPath = join(pkgDir, "vitest.config.ts");
|
|
const pkgJsonPath = join(pkgDir, "package.json");
|
|
if (!existsSync(cfgPath) || !existsSync(pkgJsonPath)) return result;
|
|
if (pkgDir === TESTING_OWN_PATH) return result; // do not migrate the testing package itself
|
|
|
|
const original = readFileSync(cfgPath, "utf8");
|
|
const detected = detect(original);
|
|
const replacement = buildConfig(detected);
|
|
if (original.trim() !== replacement.trim()) {
|
|
if (!dry) writeFileSync(cfgPath, replacement);
|
|
result.configChanged = true;
|
|
}
|
|
|
|
if (ensureTestingDep(pkgJsonPath)) result.depAdded = true;
|
|
|
|
const polyfillPath = join(pkgDir, "tests", "setup", "polyfills.ts");
|
|
if (existsSync(polyfillPath)) {
|
|
if (!dry) rmSync(polyfillPath, { force: true });
|
|
result.polyfillRemoved = true;
|
|
}
|
|
const setupDir = join(pkgDir, "tests", "setup");
|
|
if (existsSync(setupDir) && readdirSync(setupDir).length === 0 && !dry) {
|
|
try {
|
|
rmSync(setupDir, { recursive: true });
|
|
} catch {}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function main(): void {
|
|
const counts = {
|
|
configChanged: 0,
|
|
depAdded: 0,
|
|
polyfillRemoved: 0,
|
|
scanned: 0,
|
|
};
|
|
const skipped: string[] = [];
|
|
|
|
for (const plugin of readdirSync(packagesRoot)) {
|
|
const pluginPath = join(packagesRoot, plugin);
|
|
if (!statSync(pluginPath).isDirectory()) continue;
|
|
for (const pkg of readdirSync(pluginPath)) {
|
|
const pkgPath = join(pluginPath, pkg);
|
|
if (!statSync(pkgPath).isDirectory()) continue;
|
|
const cfgPath = join(pkgPath, "vitest.config.ts");
|
|
if (!existsSync(cfgPath)) {
|
|
skipped.push(`${plugin}/${pkg} — no vitest.config.ts`);
|
|
continue;
|
|
}
|
|
counts.scanned += 1;
|
|
const r = migrateOne(pkgPath);
|
|
if (r.configChanged) counts.configChanged += 1;
|
|
if (r.depAdded) counts.depAdded += 1;
|
|
if (r.polyfillRemoved) counts.polyfillRemoved += 1;
|
|
}
|
|
}
|
|
|
|
console.log(`Mode: ${dry ? "dry-run" : "execute"}`);
|
|
console.log(`Scanned vitest configs: ${counts.scanned}`);
|
|
console.log(`Config rewrites: ${counts.configChanged}`);
|
|
console.log(`Deps added: ${counts.depAdded}`);
|
|
console.log(`Polyfill files removed: ${counts.polyfillRemoved}`);
|
|
if (skipped.length > 0) {
|
|
console.log(`\nSkipped (${skipped.length}):`);
|
|
for (const s of skipped.slice(0, 10)) console.log(` ${s}`);
|
|
if (skipped.length > 10)
|
|
console.log(` ...and ${skipped.length - 10} more`);
|
|
}
|
|
console.log(dry ? "\n(no changes written)" : "\n✓ migration applied");
|
|
}
|
|
|
|
main();
|