singularity-forge/scripts/migrate-to-vitest-all.mjs
Mikael Hugo 1de5d5456a chore: complete vitest migration for remaining packages and API calls
- Convert remaining node:test → vitest imports in packages/* and studio/*
- Fix mock.callCount() → mock.callCount property access for vitest compat
- Fix mock.calls[N].arguments → mock.calls[N] for vitest compat
- Update tsconfig.extensions.json to exclude test files from tsc
- Harden migrate-to-vitest-all.mjs regex for single quotes and optional semicolons
2026-05-02 04:46:11 +02:00

134 lines
3.9 KiB
JavaScript

#!/usr/bin/env node
/**
* Migrate ALL test files from node:test to vitest.
*
* Scans src/, packages/, web/, studio/, and scripts/.
* Changes:
* 1. Replace `from "node:test"` → `from 'vitest'` in all imports
* 2. Files using mock.fn(): replace `mock.fn` → `vi.fn` and add `vi` to imports
* 3. Files using mock.timers: migrate to vi fake timers API
*/
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const ROOTS = [
join(process.cwd(), "src"),
join(process.cwd(), "packages"),
join(process.cwd(), "web"),
join(process.cwd(), "studio"),
join(process.cwd(), "scripts"),
];
function collectTestFiles(dirs) {
const results = [];
for (const dir of dirs) {
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...collectTestFiles([full]));
} else if (
(entry.name.endsWith(".test.ts") || entry.name.endsWith(".test.mjs")) &&
!entry.name.endsWith(".d.ts")
) {
results.push(full);
}
}
} catch {
// Directory may not exist
}
}
return results;
}
function migrateImport(content, { hasMockFn, hasMockTimers }) {
// Case 1: import test from "node:test"; (or single quotes, optional semicolon)
content = content.replace(
/^import test from ["']node:test["'];?$/gm,
"import { test } from 'vitest';",
);
// Case 2: import { ... } from "node:test" (and variants with default import)
content = content.replace(
/import\s+(test,)?\s*\{\s*([^}]+)\}\s+from\s+["']node:test["'];?/g,
(match, hasDefault, named) => {
const namedList = named
.split(",")
.map((s) => s.trim())
.filter((s) => s !== "mock" && s !== "");
const extra = [];
if (hasMockFn || hasMockTimers) {
extra.push("vi");
}
const allNamed = [...extra, ...namedList.filter((s) => s !== "test")];
const defaultImport = hasDefault ? "test, " : "";
const namedStr = allNamed.join(", ");
return `import { ${defaultImport}${namedStr} } from 'vitest';`;
},
);
return content;
}
const files = collectTestFiles(ROOTS);
console.log(`Found ${files.length} test files`);
let updated = 0;
let errors = 0;
for (const file of files) {
try {
const content = readFileSync(file, "utf-8");
if (!content.includes('from "node:test"') && !content.includes("from 'node:test'")) continue;
const hasMockFn = content.includes("mock.fn");
const hasMockTimers = content.includes("mock.timers");
let newContent = migrateImport(content, { hasMockFn, hasMockTimers });
// Migrate mock.fn → vi.fn
if (hasMockFn) {
newContent = newContent.replace(/\bmock\.fn\b/g, "vi.fn");
newContent = newContent.replace(
/ReturnType<typeof mock\.fn>/g,
"ReturnType<typeof vi.fn>",
);
}
// Migrate mock.timers → vi fake timers
if (hasMockTimers) {
newContent = newContent.replace(
/\bmock\.timers\.enable\(\)/g,
"vi.useFakeTimers()",
);
newContent = newContent.replace(
/\bmock\.timers\.tick\(([^)]+)\)/g,
"vi.advanceTimersByTime($1)",
);
newContent = newContent.replace(
/\bmock\.timers\.reset\(\)/g,
"vi.useRealTimers()",
);
}
if (newContent !== content) {
writeFileSync(file, newContent, "utf-8");
updated++;
const rel = file.replace(process.cwd() + "/", "");
const tags = [];
if (hasMockFn) tags.push("mock");
if (hasMockTimers) tags.push("timers");
const tagStr = tags.length ? ` (${tags.join(", ")})` : "";
console.log(` Migrated${tagStr}: ${rel}`);
}
} catch (err) {
console.error(`Error: ${file}: ${err.message}`);
errors++;
}
}
console.log(`Updated ${updated} files, ${errors} errors`);