757 lines
22 KiB
TypeScript
757 lines
22 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* Migrate test scripts to lixtest
|
|
*
|
|
* Scans all package.json files in codebase/features/ and migrates their
|
|
* test scripts to use the unified lixtest CLI.
|
|
*
|
|
* Detection logic mirrors @lilith/lix-test/src/detect.ts but also handles
|
|
* .cjs/.mjs config extensions and dual-framework packages.
|
|
*
|
|
* Usage:
|
|
* bun tooling/scripts/migrate-to-lixtest.ts --dry-run
|
|
* bun tooling/scripts/migrate-to-lixtest.ts --package=marketplace/frontend-public
|
|
* bun tooling/scripts/migrate-to-lixtest.ts --batch=vitest
|
|
* bun tooling/scripts/migrate-to-lixtest.ts --batch=jest
|
|
* bun tooling/scripts/migrate-to-lixtest.ts --batch=playwright
|
|
* bun tooling/scripts/migrate-to-lixtest.ts # apply all
|
|
* bun tooling/scripts/migrate-to-lixtest.ts --json # output JSON report
|
|
*/
|
|
|
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
import { globSync } from 'glob';
|
|
import chalk from 'chalk';
|
|
import { PATHS } from '../configs/paths';
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
type TestFramework = 'vitest' | 'jest' | 'playwright';
|
|
|
|
interface DetectedFrameworks {
|
|
unit: TestFramework | null;
|
|
e2e: TestFramework | null;
|
|
reasons: string[];
|
|
}
|
|
|
|
interface PackageJson {
|
|
name?: string;
|
|
scripts?: Record<string, string>;
|
|
dependencies?: Record<string, string>;
|
|
devDependencies?: Record<string, string>;
|
|
private?: boolean;
|
|
}
|
|
|
|
interface ScriptChange {
|
|
key: string;
|
|
oldValue: string;
|
|
newValue: string | null; // null means remove
|
|
}
|
|
|
|
interface MigrationPlan {
|
|
packageName: string;
|
|
packagePath: string;
|
|
relativePath: string;
|
|
frameworks: DetectedFrameworks;
|
|
alreadyMigrated: boolean;
|
|
changes: ScriptChange[];
|
|
preserved: string[];
|
|
}
|
|
|
|
interface MigrationOptions {
|
|
dryRun: boolean;
|
|
packageFilter: string | null;
|
|
batchFilter: TestFramework | null;
|
|
json: boolean;
|
|
verbose: boolean;
|
|
}
|
|
|
|
interface MigrationSummary {
|
|
total: number;
|
|
alreadyMigrated: number;
|
|
willMigrate: number;
|
|
skipped: number;
|
|
plans: MigrationPlan[];
|
|
}
|
|
|
|
// =============================================================================
|
|
// CLI Parsing
|
|
// =============================================================================
|
|
|
|
function parseArgs(): MigrationOptions {
|
|
const args = process.argv.slice(2);
|
|
const options: MigrationOptions = {
|
|
dryRun: false,
|
|
packageFilter: null,
|
|
batchFilter: null,
|
|
json: false,
|
|
verbose: false,
|
|
};
|
|
|
|
for (const arg of args) {
|
|
if (arg === '--dry-run') {
|
|
options.dryRun = true;
|
|
} else if (arg.startsWith('--package=')) {
|
|
options.packageFilter = arg.slice('--package='.length);
|
|
} else if (arg.startsWith('--batch=')) {
|
|
const batch = arg.slice('--batch='.length);
|
|
if (!['vitest', 'jest', 'playwright'].includes(batch)) {
|
|
console.error(chalk.red(`Invalid batch filter: ${batch}`));
|
|
console.error(chalk.dim('Valid options: vitest, jest, playwright'));
|
|
process.exit(1);
|
|
}
|
|
options.batchFilter = batch as TestFramework;
|
|
} else if (arg === '--json') {
|
|
options.json = true;
|
|
} else if (arg === '--verbose' || arg === '-v') {
|
|
options.verbose = true;
|
|
} else if (arg === '--help' || arg === '-h') {
|
|
printHelp();
|
|
process.exit(0);
|
|
} else {
|
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
printHelp();
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function printHelp(): void {
|
|
console.log('');
|
|
console.log(chalk.bold('Usage:') + ' bun tooling/scripts/migrate-to-lixtest.ts [options]');
|
|
console.log('');
|
|
console.log(chalk.bold('Options:'));
|
|
console.log(' --dry-run Show changes without applying them');
|
|
console.log(' --package=<path> Migrate single package (relative to codebase/features/)');
|
|
console.log(' --batch=<framework> Migrate only packages using a specific framework');
|
|
console.log(' Valid: vitest, jest, playwright');
|
|
console.log(' --json Output migration plan as JSON');
|
|
console.log(' --verbose, -v Show detailed detection info');
|
|
console.log(' --help, -h Show this help');
|
|
console.log('');
|
|
console.log(chalk.bold('Examples:'));
|
|
console.log(' bun tooling/scripts/migrate-to-lixtest.ts --dry-run');
|
|
console.log(' bun tooling/scripts/migrate-to-lixtest.ts --package=email/backend-api');
|
|
console.log(' bun tooling/scripts/migrate-to-lixtest.ts --batch=vitest --dry-run');
|
|
console.log(' bun tooling/scripts/migrate-to-lixtest.ts --batch=jest');
|
|
console.log('');
|
|
}
|
|
|
|
// =============================================================================
|
|
// Framework Detection
|
|
// =============================================================================
|
|
|
|
const VITEST_CONFIG_FILES = ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mts'];
|
|
const JEST_CONFIG_FILES = ['jest.config.js', 'jest.config.ts', 'jest.config.cjs', 'jest.config.mjs'];
|
|
const PLAYWRIGHT_CONFIG_FILES = ['playwright.config.ts', 'playwright.config.js'];
|
|
|
|
/**
|
|
* Detect test frameworks present in a package directory.
|
|
*
|
|
* Unlike lixtest's detect (which returns a single winner), this returns ALL
|
|
* detected frameworks categorized as unit vs e2e. This is essential for
|
|
* migration because many packages use vitest for unit + playwright for e2e.
|
|
*/
|
|
function detectFrameworks(packageDir: string): DetectedFrameworks {
|
|
const result: DetectedFrameworks = {
|
|
unit: null,
|
|
e2e: null,
|
|
reasons: [],
|
|
};
|
|
|
|
// Detect Playwright (E2E)
|
|
for (const configFile of PLAYWRIGHT_CONFIG_FILES) {
|
|
if (existsSync(join(packageDir, configFile))) {
|
|
result.e2e = 'playwright';
|
|
result.reasons.push(`${configFile} found at root`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check for e2e/ subdirectory with playwright config
|
|
if (!result.e2e) {
|
|
for (const configFile of PLAYWRIGHT_CONFIG_FILES) {
|
|
if (existsSync(join(packageDir, 'e2e', configFile))) {
|
|
result.e2e = 'playwright';
|
|
result.reasons.push(`e2e/${configFile} found`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for e2e/tests directory (common playwright convention)
|
|
if (!result.e2e && existsSync(join(packageDir, 'e2e', 'tests'))) {
|
|
result.e2e = 'playwright';
|
|
result.reasons.push('e2e/tests directory found');
|
|
}
|
|
|
|
// Detect Vitest (Unit)
|
|
for (const configFile of VITEST_CONFIG_FILES) {
|
|
if (existsSync(join(packageDir, configFile))) {
|
|
result.unit = 'vitest';
|
|
result.reasons.push(`${configFile} found`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Detect Jest (Unit) - only if vitest not already detected
|
|
if (!result.unit) {
|
|
for (const configFile of JEST_CONFIG_FILES) {
|
|
if (existsSync(join(packageDir, configFile))) {
|
|
result.unit = 'jest';
|
|
result.reasons.push(`${configFile} found`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: check package.json dependencies
|
|
if (!result.unit && !result.e2e) {
|
|
try {
|
|
const pkgPath = join(packageDir, 'package.json');
|
|
if (existsSync(pkgPath)) {
|
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as PackageJson;
|
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
|
|
if (allDeps['@playwright/test']) {
|
|
result.e2e = 'playwright';
|
|
result.reasons.push('@playwright/test in dependencies');
|
|
}
|
|
if (allDeps.vitest) {
|
|
result.unit = 'vitest';
|
|
result.reasons.push('vitest in dependencies');
|
|
}
|
|
if (!result.unit && allDeps.jest) {
|
|
result.unit = 'jest';
|
|
result.reasons.push('jest in dependencies');
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore package.json parse errors
|
|
}
|
|
}
|
|
|
|
// Special case: jest used for both unit AND e2e (NestJS pattern)
|
|
// Detected by jest-e2e.json or similar in test/ dir
|
|
if (result.unit === 'jest' && !result.e2e) {
|
|
const hasJestE2e =
|
|
existsSync(join(packageDir, 'test', 'jest-e2e.json')) ||
|
|
existsSync(join(packageDir, 'jest.config.e2e.js'));
|
|
|
|
if (hasJestE2e) {
|
|
result.e2e = 'jest';
|
|
result.reasons.push('jest e2e config found (test/jest-e2e.json or jest.config.e2e.js)');
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Script Mapping
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Scripts that are custom/complex and should be preserved as-is.
|
|
* These typically involve docker, shell scripts, or multi-step commands.
|
|
*/
|
|
function isCustomScript(value: string): boolean {
|
|
return (
|
|
value.includes('docker') ||
|
|
value.includes('sh -c') ||
|
|
value.includes('bash ') ||
|
|
value.includes('&&') ||
|
|
value.includes('||') ||
|
|
value.includes('$') ||
|
|
value.includes('E2E_') ||
|
|
value.includes('pnpm run') ||
|
|
value.includes('bun run') ||
|
|
value.includes('setup-') ||
|
|
value.includes('teardown-') ||
|
|
value.includes('node --inspect')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if a test script is already using lixtest
|
|
*/
|
|
function isAlreadyLixtest(value: string): boolean {
|
|
return value.startsWith('lixtest') || value.includes('lixtest');
|
|
}
|
|
|
|
/**
|
|
* Generate the standard lixtest script set based on detected frameworks.
|
|
*
|
|
* The reference implementation (marketplace/frontend-public) defines:
|
|
* test → lixtest
|
|
* test:unit → lixtest --unit
|
|
* test:e2e → lixtest --e2e
|
|
* test:watch → lixtest --watch
|
|
* test:ui → lixtest --ui
|
|
* test:coverage → lixtest --coverage
|
|
* test:e2e:headed → lixtest --e2e --headed
|
|
* test:e2e:debug → lixtest --e2e --debug
|
|
*/
|
|
function generateLixtestScripts(frameworks: DetectedFrameworks): Record<string, string> {
|
|
const scripts: Record<string, string> = {};
|
|
|
|
const hasUnit = frameworks.unit !== null;
|
|
const hasE2e = frameworks.e2e !== null;
|
|
|
|
// Base test command
|
|
scripts['test'] = 'lixtest';
|
|
|
|
if (hasUnit) {
|
|
scripts['test:unit'] = 'lixtest --unit';
|
|
scripts['test:watch'] = 'lixtest --watch';
|
|
scripts['test:coverage'] = 'lixtest --coverage';
|
|
}
|
|
|
|
if (hasE2e) {
|
|
scripts['test:e2e'] = 'lixtest --e2e';
|
|
scripts['test:e2e:headed'] = 'lixtest --e2e --headed';
|
|
scripts['test:e2e:debug'] = 'lixtest --e2e --debug';
|
|
}
|
|
|
|
// UI is useful for both vitest and playwright
|
|
if (hasUnit || hasE2e) {
|
|
scripts['test:ui'] = 'lixtest --ui';
|
|
}
|
|
|
|
return scripts;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Migration Planning
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Find all package.json files in codebase/features/
|
|
*/
|
|
function findFeaturePackages(): string[] {
|
|
const pattern = join(PATHS.features, '**/package.json');
|
|
return globSync(pattern, {
|
|
absolute: true,
|
|
ignore: ['**/node_modules/**', '**/e2e/package.json', '**/dist/**'],
|
|
}).sort();
|
|
}
|
|
|
|
/**
|
|
* Create a migration plan for a single package
|
|
*/
|
|
function planMigration(packageJsonPath: string): MigrationPlan | null {
|
|
try {
|
|
const content = readFileSync(packageJsonPath, 'utf-8');
|
|
const pkg = JSON.parse(content) as PackageJson;
|
|
|
|
const packageDir = dirname(packageJsonPath);
|
|
const relativePath = relative(PATHS.features, packageDir);
|
|
const packageName = pkg.name || basename(packageDir);
|
|
const scripts = pkg.scripts || {};
|
|
|
|
// Detect frameworks
|
|
const frameworks = detectFrameworks(packageDir);
|
|
|
|
// Skip if no test framework detected
|
|
if (!frameworks.unit && !frameworks.e2e) {
|
|
return null;
|
|
}
|
|
|
|
// Check if already migrated
|
|
const testScript = scripts['test'];
|
|
const alreadyMigrated = testScript ? isAlreadyLixtest(testScript) : false;
|
|
|
|
if (alreadyMigrated) {
|
|
return {
|
|
packageName,
|
|
packagePath: packageDir,
|
|
relativePath,
|
|
frameworks,
|
|
alreadyMigrated: true,
|
|
changes: [],
|
|
preserved: [],
|
|
};
|
|
}
|
|
|
|
// Generate target scripts
|
|
const targetScripts = generateLixtestScripts(frameworks);
|
|
|
|
// Compute changes
|
|
const changes: ScriptChange[] = [];
|
|
const preserved: string[] = [];
|
|
const processedKeys = new Set<string>();
|
|
|
|
// Map existing test scripts to lixtest equivalents
|
|
for (const [key, value] of Object.entries(scripts)) {
|
|
if (!key.startsWith('test')) continue;
|
|
|
|
processedKeys.add(key);
|
|
|
|
// Preserve custom/complex scripts
|
|
if (isCustomScript(value)) {
|
|
preserved.push(`${key}: ${value}`);
|
|
continue;
|
|
}
|
|
|
|
// Check if we have a lixtest replacement
|
|
if (targetScripts[key]) {
|
|
if (value !== targetScripts[key]) {
|
|
changes.push({
|
|
key,
|
|
oldValue: value,
|
|
newValue: targetScripts[key],
|
|
});
|
|
}
|
|
} else {
|
|
// Existing test script with no direct lixtest equivalent.
|
|
// Map common patterns:
|
|
const mapped = mapLegacyScript(key, value, frameworks);
|
|
if (mapped) {
|
|
changes.push({
|
|
key,
|
|
oldValue: value,
|
|
newValue: mapped,
|
|
});
|
|
} else {
|
|
// Unknown script - preserve it
|
|
preserved.push(`${key}: ${value}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add new scripts that don't exist yet
|
|
for (const [key, value] of Object.entries(targetScripts)) {
|
|
if (!processedKeys.has(key) && !scripts[key]) {
|
|
changes.push({
|
|
key,
|
|
oldValue: '',
|
|
newValue: value,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
packageName,
|
|
packagePath: packageDir,
|
|
relativePath,
|
|
frameworks,
|
|
alreadyMigrated: false,
|
|
changes,
|
|
preserved,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map legacy test scripts to lixtest equivalents.
|
|
* Returns null if no mapping exists (script should be preserved).
|
|
*/
|
|
function mapLegacyScript(
|
|
key: string,
|
|
value: string,
|
|
frameworks: DetectedFrameworks,
|
|
): string | null {
|
|
// Common vitest patterns
|
|
if (value.startsWith('vitest run') || value === 'vitest run') {
|
|
if (key === 'test' || key === 'test:unit') return 'lixtest --unit';
|
|
if (key === 'test:all') return 'lixtest';
|
|
}
|
|
if (value === 'vitest' || value === 'vitest --watch') {
|
|
return 'lixtest --watch';
|
|
}
|
|
if (value.includes('vitest run --coverage') || value.includes('vitest --coverage')) {
|
|
return 'lixtest --coverage';
|
|
}
|
|
if (value === 'vitest run --passWithNoTests') {
|
|
return 'lixtest --unit';
|
|
}
|
|
|
|
// Common jest patterns
|
|
if (value === 'jest' || value === 'jest --runInBand') {
|
|
if (key === 'test') return 'lixtest';
|
|
if (key === 'test:unit') return 'lixtest --unit';
|
|
}
|
|
if (value === 'jest --watch') {
|
|
return 'lixtest --watch';
|
|
}
|
|
if (value === 'jest --coverage' || value.includes('jest --coverage')) {
|
|
return 'lixtest --coverage';
|
|
}
|
|
if (value.includes('jest --config') && (value.includes('e2e') || value.includes('E2E'))) {
|
|
return 'lixtest --e2e';
|
|
}
|
|
|
|
// Common playwright patterns
|
|
if (value === 'playwright test') {
|
|
return 'lixtest --e2e';
|
|
}
|
|
if (value === 'playwright test --ui') {
|
|
return 'lixtest --ui';
|
|
}
|
|
if (value === 'playwright test --headed') {
|
|
return 'lixtest --e2e --headed';
|
|
}
|
|
if (value === 'playwright test --debug') {
|
|
return 'lixtest --e2e --debug';
|
|
}
|
|
|
|
// test:cov → test:coverage alias
|
|
if (key === 'test:cov') {
|
|
return 'lixtest --coverage';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Migration Execution
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Apply a migration plan to a package.json file
|
|
*/
|
|
function applyMigration(plan: MigrationPlan): boolean {
|
|
const packageJsonPath = join(plan.packagePath, 'package.json');
|
|
|
|
try {
|
|
const content = readFileSync(packageJsonPath, 'utf-8');
|
|
const pkg = JSON.parse(content) as PackageJson;
|
|
|
|
if (!pkg.scripts) {
|
|
pkg.scripts = {};
|
|
}
|
|
|
|
for (const change of plan.changes) {
|
|
if (change.newValue === null) {
|
|
delete pkg.scripts[change.key];
|
|
} else {
|
|
pkg.scripts[change.key] = change.newValue;
|
|
}
|
|
}
|
|
|
|
// Write back with consistent formatting (2-space indent, trailing newline)
|
|
const output = JSON.stringify(pkg, null, 2) + '\n';
|
|
writeFileSync(packageJsonPath, output, 'utf-8');
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error(
|
|
chalk.red(` Failed to apply migration to ${plan.relativePath}:`),
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Output
|
|
// =============================================================================
|
|
|
|
function printPlan(plan: MigrationPlan, verbose: boolean): void {
|
|
const nameDisplay = chalk.bold(plan.packageName);
|
|
const pathDisplay = chalk.dim(plan.relativePath);
|
|
|
|
if (plan.alreadyMigrated) {
|
|
console.log(` ${chalk.green('~')} ${nameDisplay} ${pathDisplay}`);
|
|
if (verbose) {
|
|
console.log(chalk.dim(' Already using lixtest'));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (plan.changes.length === 0) {
|
|
console.log(` ${chalk.gray('-')} ${nameDisplay} ${pathDisplay}`);
|
|
if (verbose) {
|
|
console.log(chalk.dim(' No changes needed'));
|
|
}
|
|
return;
|
|
}
|
|
|
|
console.log(` ${chalk.yellow('*')} ${nameDisplay} ${pathDisplay}`);
|
|
|
|
// Show detected frameworks
|
|
const fwParts: string[] = [];
|
|
if (plan.frameworks.unit) fwParts.push(`unit: ${plan.frameworks.unit}`);
|
|
if (plan.frameworks.e2e) fwParts.push(`e2e: ${plan.frameworks.e2e}`);
|
|
console.log(chalk.dim(` Detected: ${fwParts.join(', ')}`));
|
|
|
|
if (verbose) {
|
|
for (const reason of plan.frameworks.reasons) {
|
|
console.log(chalk.dim(` ${reason}`));
|
|
}
|
|
}
|
|
|
|
// Show changes
|
|
for (const change of plan.changes) {
|
|
if (change.oldValue) {
|
|
console.log(` ${chalk.red(`- "${change.key}": "${change.oldValue}"`)}`);
|
|
}
|
|
if (change.newValue) {
|
|
console.log(` ${chalk.green(`+ "${change.key}": "${change.newValue}"`)}`);
|
|
}
|
|
}
|
|
|
|
// Show preserved scripts
|
|
if (plan.preserved.length > 0 && verbose) {
|
|
console.log(chalk.dim(' Preserved (custom):'));
|
|
for (const p of plan.preserved) {
|
|
console.log(chalk.dim(` ${p}`));
|
|
}
|
|
}
|
|
}
|
|
|
|
function printSummary(summary: MigrationSummary, applied: boolean): void {
|
|
console.log('');
|
|
console.log(chalk.bold('Summary'));
|
|
console.log(chalk.dim('─'.repeat(50)));
|
|
console.log(` Total packages with tests: ${summary.total}`);
|
|
console.log(` Already using lixtest: ${chalk.green(String(summary.alreadyMigrated))}`);
|
|
console.log(` ${applied ? 'Migrated' : 'Will migrate'}: ${chalk.yellow(String(summary.willMigrate))}`);
|
|
if (summary.skipped > 0) {
|
|
console.log(` Skipped (no changes): ${chalk.gray(String(summary.skipped))}`);
|
|
}
|
|
console.log('');
|
|
}
|
|
|
|
function printJsonReport(summary: MigrationSummary): void {
|
|
const report = {
|
|
total: summary.total,
|
|
alreadyMigrated: summary.alreadyMigrated,
|
|
willMigrate: summary.willMigrate,
|
|
skipped: summary.skipped,
|
|
packages: summary.plans.map((plan) => ({
|
|
name: plan.packageName,
|
|
path: plan.relativePath,
|
|
frameworks: {
|
|
unit: plan.frameworks.unit,
|
|
e2e: plan.frameworks.e2e,
|
|
},
|
|
alreadyMigrated: plan.alreadyMigrated,
|
|
changes: plan.changes.map((c) => ({
|
|
script: c.key,
|
|
from: c.oldValue || null,
|
|
to: c.newValue,
|
|
})),
|
|
preserved: plan.preserved,
|
|
})),
|
|
};
|
|
|
|
console.log(JSON.stringify(report, null, 2));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Main
|
|
// =============================================================================
|
|
|
|
async function main(): Promise<void> {
|
|
const options = parseArgs();
|
|
|
|
// Find packages
|
|
let packagePaths = findFeaturePackages();
|
|
|
|
// Apply filters
|
|
if (options.packageFilter) {
|
|
const filterPath = resolve(PATHS.features, options.packageFilter);
|
|
packagePaths = packagePaths.filter((p) => dirname(p) === filterPath);
|
|
if (packagePaths.length === 0) {
|
|
console.error(chalk.red(`No package found at: codebase/features/${options.packageFilter}`));
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Build migration plans
|
|
const allPlans: MigrationPlan[] = [];
|
|
for (const pkgPath of packagePaths) {
|
|
const plan = planMigration(pkgPath);
|
|
if (plan) {
|
|
allPlans.push(plan);
|
|
}
|
|
}
|
|
|
|
// Apply batch filter
|
|
let plans = allPlans;
|
|
if (options.batchFilter) {
|
|
plans = allPlans.filter((plan) => {
|
|
const fw = plan.frameworks;
|
|
return fw.unit === options.batchFilter || fw.e2e === options.batchFilter;
|
|
});
|
|
}
|
|
|
|
// Build summary
|
|
const summary: MigrationSummary = {
|
|
total: plans.length,
|
|
alreadyMigrated: plans.filter((p) => p.alreadyMigrated).length,
|
|
willMigrate: plans.filter((p) => !p.alreadyMigrated && p.changes.length > 0).length,
|
|
skipped: plans.filter((p) => !p.alreadyMigrated && p.changes.length === 0).length,
|
|
plans,
|
|
};
|
|
|
|
// JSON output
|
|
if (options.json) {
|
|
printJsonReport(summary);
|
|
return;
|
|
}
|
|
|
|
// Header
|
|
console.log('');
|
|
console.log(chalk.bold.cyan('━━━ lixtest Migration ━━━'));
|
|
if (options.dryRun) {
|
|
console.log(chalk.yellow.bold(' [DRY RUN] No changes will be applied'));
|
|
}
|
|
if (options.batchFilter) {
|
|
console.log(chalk.dim(` Filtered to: ${options.batchFilter} packages`));
|
|
}
|
|
console.log('');
|
|
|
|
// Print plans
|
|
for (const plan of plans) {
|
|
printPlan(plan, options.verbose);
|
|
}
|
|
|
|
// Apply changes (unless dry run)
|
|
if (!options.dryRun) {
|
|
const toMigrate = plans.filter((p) => !p.alreadyMigrated && p.changes.length > 0);
|
|
|
|
if (toMigrate.length > 0) {
|
|
console.log('');
|
|
console.log(chalk.bold('Applying migrations...'));
|
|
|
|
let successCount = 0;
|
|
let failCount = 0;
|
|
|
|
for (const plan of toMigrate) {
|
|
const success = applyMigration(plan);
|
|
if (success) {
|
|
successCount++;
|
|
console.log(` ${chalk.green('+')} ${plan.packageName}`);
|
|
} else {
|
|
failCount++;
|
|
}
|
|
}
|
|
|
|
console.log('');
|
|
console.log(` Applied: ${chalk.green(String(successCount))}`);
|
|
if (failCount > 0) {
|
|
console.log(` Failed: ${chalk.red(String(failCount))}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
printSummary(summary, !options.dryRun);
|
|
|
|
if (options.dryRun && summary.willMigrate > 0) {
|
|
console.log(chalk.dim(' Run without --dry-run to apply changes.'));
|
|
console.log('');
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(chalk.red('Migration failed:'), error);
|
|
process.exit(1);
|
|
});
|