Initial Commit
This commit is contained in:
210
scripts/migrate-vitest-configs.ts
Normal file
210
scripts/migrate-vitest-configs.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user