#!/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/g, "ReturnType", ); } // 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`);