diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4c96662..07de961 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -65,14 +65,14 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Build - run: pnpm run build + - name: Run lint + run: pnpm run lint - name: Run tests run: pnpm test - - name: Run lint - run: pnpm run lint + - name: Build + run: pnpm run build - name: Publish to NPM env: diff --git a/README.md b/README.md index 101c466..d2ba7e0 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Automated code transformations for Suites projects. Built on [jscodeshift](https ## Usage ```bash -npx @suites/codemod [options] +npx @suites/codemod [options] ``` **Example:** @@ -18,9 +18,9 @@ npx @suites/codemod [options] npx @suites/codemod automock/2/to-suites-v3 src/**/*.spec.ts ``` -Run with `--dry-run` to preview changes without modifying files. +Run with `--dry` or `-d` to preview changes without modifying files. -## Available Transforms +## Available Codemods - **`automock/2/to-suites-v3`** - Migrate test files from Automock v2 to Suites v3 testing framework @@ -62,30 +62,40 @@ describe('UserService', () => { }); ``` -## CLI Options +## CLI Reference + +### Arguments + +| Argument | Description | Default | +|----------|-------------|---------| +| `codemod` | Codemod slug to run. See available transforms below. | - | +| `source` | Path to source files or directory to transform including glob patterns. | `.` | + +### Options | Option | Description | Default | |--------|-------------|---------| -| `-d, --dry-run` | Preview changes without writing files | `false` | -| `-f, --force` | Bypass git safety checks | `false` | -| `-p, --parser ` | Parser: `tsx`, `ts`, `babel` | `tsx` | -| `-e, --extensions ` | File extensions to transform | `.ts,.tsx` | -| `-i, --ignore ` | Ignore file patterns (comma-separated) | - | -| `--print` | Print output to stdout | `false` | -| `-v, --verbose` | Show detailed logs | `false` | -| `--skip-validation` | Skip validation checks | `false` | -| `--list-transforms` | List all available transforms | - | - -**More examples:** +| `-v, --version` | Output the current version | - | +| `-d, --dry` | Dry run (no changes are made to files) | `false` | +| `-f, --force` | Bypass Git safety checks and forcibly run codemods | `false` | +| `-p, --print` | Print transformed files to stdout, useful for development | `false` | +| `--verbose` | Show more information about the transform process | `false` | +| `--parser ` | Parser to use: `tsx`, `ts`, `babel` | `tsx` | +| `-h, --help` | Display help message | - | + +**Examples:** ```bash -# Preview changes -npx @suites/codemod automock/2/to-suites-v3 src --dry-run +# Preview changes (dry run) +npx @suites/codemod automock/2/to-suites-v3 src --dry + +# Print output to stdout +npx @suites/codemod automock/2/to-suites-v3 src/file.ts -p -# Ignore certain files -npx @suites/codemod automock/2/to-suites-v3 src --ignore "**/*.integration.ts" +# Verbose output +npx @suites/codemod automock/2/to-suites-v3 src --verbose -# List all transforms -npx @suites/codemod --list-transforms +# Use different parser +npx @suites/codemod automock/2/to-suites-v3 src --parser babel ``` ## Transform Details @@ -132,7 +142,7 @@ Built-in validation ensures: - Commit your changes or use `--force` to bypass **"No files found"** -- Check your path and file extensions: `--extensions .spec.ts,.test.ts` +- Check your source path and ensure it contains `.ts` or `.tsx` files **Parser errors** - Try the babel parser: `--parser babel` @@ -140,7 +150,7 @@ Built-in validation ensures: **Validation failed** - Run with `--verbose` for detailed logs - Review validation errors in the output -- Use `--skip-validation` to bypass (not recommended) +- Fix the issues reported by validators For more help, see [troubleshooting guide](https://github.com/suites-dev/codemod/issues) or open an issue. @@ -235,52 +245,6 @@ test/ fixtures/ # Test fixtures (before/after) ``` -## Local Development - -### Running Locally - -From within the codemod repository: - -```bash -# Build first -pnpm build - -# Run on a target repository -node dist/cli.js automock/2/to-suites-v3 /path/to/repo --dry-run - -# Run on test fixtures -node dist/cli.js automock/2/to-suites-v3 fixtures/simple-final --dry-run - -# Verbose output for debugging -node dist/cli.js automock/2/to-suites-v3 /path/to/repo --dry-run --verbose -``` - -### Using npm link for Testing - -```bash -# In the codemod repo -npm link - -# Now use it anywhere like npx -codemod automock/2/to-suites-v3 /path/to/repo --dry-run - -# Unlink when done -npm unlink -g @suites/codemod -``` - -### Running Tests - -```bash -# All tests -pnpm test - -# Specific test file -pnpm test path/to/test.spec.ts - -# With coverage -pnpm test --coverage -``` - ## License MIT (c) [Omer Morad](https://github.com/omermorad) diff --git a/fixtures/simple-final/input.ts b/fixtures/simple-final/input.ts index a8399e9..9c3250b 100644 --- a/fixtures/simple-final/input.ts +++ b/fixtures/simple-final/input.ts @@ -1,12 +1,12 @@ -import { TestBed } from '@automock/jest'; +import { TestBed } from '@suites/unit'; describe('UserService', () => { let service: UserService; - beforeAll(() => { - const { unit } = TestBed.create(UserService) + beforeAll(async () => { + const { unit } = await TestBed.solitary(UserService) .mock(UserRepository) - .using({ + .final({ findById: (id: string) => Promise.resolve({ id, name: 'Test' }) }) .compile(); diff --git a/src/cli.ts b/src/cli.ts index 46cf0f5..ba9d9ed 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,43 +13,30 @@ const program = new Command(); program .name('@suites/codemod') .description('Code transformation tool for the Suites testing framework') - .version('0.1.0') + .version('0.1.0', '-v, --version', 'Output the current version') + .helpOption('-h, --help', 'Display help message') .argument( - '[transform]', - 'Transform to apply (e.g., automock/2/to-suites-v3)' + '[codemod]', + 'Codemod slug to run. See "https://github.com/suites-dev/codemod".' ) - .argument('[path]', 'Path to transform (file or directory)', '.') - .option('-d, --dry-run', 'Preview changes without writing files', false) - .option('-f, --force', 'Bypass git safety checks', false) - .option('-p, --parser ', 'Parser to use (tsx, ts, babel)', 'tsx') - .option('-e, --extensions ', 'File extensions to transform', '.ts,.tsx') - .option('-i, --ignore ', 'Ignore file patterns (comma-separated)') - .option('--print', 'Print transformed output to stdout', false) - .option('-v, --verbose', 'Show detailed transformation logs', false) - .option('--skip-validation', 'Skip post-transformation validation checks', false) - .option('--list-transforms', 'List all available transforms', false) + .argument('[source]', 'Path to source files or directory to transform including glob patterns.', '.') + .option('-d, --dry', 'Dry run (no changes are made to files)', false) + .option('-f, --force', 'Bypass Git safety checks and forcibly run codemods', false) + .option('-p, --print', 'Print transformed files to stdout, useful for development', false) + .option('--verbose', 'Show more information about the transform process', false) + .option('--parser ', 'Parser to use (tsx, ts, babel)', 'tsx') .action( async ( - transformArg: string | undefined, - pathArg: string | undefined, - options: CliOptions & { listTransforms?: boolean } + codemodArg: string | undefined, + sourceArg: string | undefined, + options: CliOptions ) => { const logger = createLogger(options.verbose); - // Handle --list-transforms - if (options.listTransforms) { - console.log('Available transforms:\n'); - AVAILABLE_TRANSFORMS.forEach((t) => { - console.log(` ${t.name}`); - console.log(` ${t.description}\n`); - }); - return; - } - - // Validate transform is provided - if (!transformArg) { - logger.error('Transform argument required.'); - logger.info('\nAvailable transforms:'); + // Validate codemod is provided + if (!codemodArg) { + logger.error('Codemod argument required.'); + logger.info('\nAvailable codemods:'); AVAILABLE_TRANSFORMS.forEach((t) => { console.log(` ${t.name}`); console.log(` ${t.description}\n`); @@ -59,37 +46,41 @@ program process.exit(1); } - const transformName = transformArg; - const targetPath = pathArg || '.'; + const codemodName = codemodArg; + const sourcePath = sourceArg || '.'; - const transformInfo = getTransform(transformName); + const transformInfo = getTransform(codemodName); if (!transformInfo) { - logger.error(`Unknown transform: ${transformName}`); - logger.error('Run with --list-transforms to see available transforms'); + logger.error(`Unknown codemod: ${codemodName}`); + logger.info('\nAvailable codemods:'); + AVAILABLE_TRANSFORMS.forEach((t) => { + console.log(` ${t.name}`); + console.log(` ${t.description}\n`); + }); process.exit(1); } try { - // Git safety check (unless in dry-run or force mode) - if (!options.dryRun && !options.force) { + // Git safety check (unless in dry or force mode) + if (!options.dry && !options.force) { checkGitStatus(logger); } // Show header with dynamic transform name logger.section(`🔄 Suites Codemod - ${transformInfo.description}`); - if (options.dryRun) { - logger.info('Running in dry-run mode (no files will be modified)'); + if (options.dry) { + logger.info('Running in dry mode (no changes are made to files)'); } - if (options.force && !options.dryRun) { + if (options.force && !options.dry) { logger.warn('Bypassing git safety checks (--force enabled)'); } logger.newline(); // Run the transformation - await runTransform(targetPath, transformInfo, options, logger); + await runTransform(sourcePath, transformInfo, options, logger); } catch (error) { logger.newline(); logger.error('Transformation failed:'); diff --git a/src/runner.ts b/src/runner.ts index f8a6c31..46e75d3 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -30,8 +30,12 @@ export async function runTransform( logger.startSpinner('Discovering files..'); const fileProcessor = createFileProcessor({ - extensions: options.extensions.split(',').map((ext) => ext.trim()), - ignorePatterns: options.ignore?.split(',').map((pattern) => pattern.trim()) || [], + extensions: ['.ts', '.tsx'], + ignorePatterns: [ + '**/node_modules/**', + '**/dist/**', + '**/*.d.ts', + ], }); const allFiles = await fileProcessor.discoverFiles(targetPath); @@ -154,7 +158,6 @@ async function transformFile( // Apply transformation const transformOutput = applyTransform(source, { - skipValidation: options.skipValidation, parser: options.parser, }); @@ -167,19 +170,17 @@ async function transformFile( result.transformed = true; // Collect validation errors and warnings - if (!options.skipValidation) { - transformOutput.validation.errors.forEach((err: ValidationError) => { - result.errors.push(`${err.rule}: ${err.message}`); - }); - - transformOutput.validation.warnings.forEach((warn: ValidationError) => { - result.warnings.push(`${warn.rule}: ${warn.message}`); - }); - - transformOutput.validation.criticalErrors.forEach((err: ValidationError) => { - result.errors.push(`[CRITICAL] ${err.rule}: ${err.message}`); - }); - } + transformOutput.validation.errors.forEach((err: ValidationError) => { + result.errors.push(`${err.rule}: ${err.message}`); + }); + + transformOutput.validation.warnings.forEach((warn: ValidationError) => { + result.warnings.push(`${warn.rule}: ${warn.message}`); + }); + + transformOutput.validation.criticalErrors.forEach((err: ValidationError) => { + result.errors.push(`[CRITICAL] ${err.rule}: ${err.message}`); + }); // Handle --print flag (output to stdout instead of writing) if (options.print) { @@ -189,21 +190,21 @@ async function transformFile( console.log(transformOutput.code); logger.info('='.repeat(60)); result.changes.push('Printed to stdout'); - } else if (!options.dryRun) { + } else if (!options.dry) { // Write transformed file await fs.writeFile(filePath, transformOutput.code, 'utf-8'); result.changes.push('File updated'); - logger.success(` ✓ ${path.relative(process.cwd(), filePath)}`); + logger.success(` ${path.relative(process.cwd(), filePath)}`); } else { // Dry run - just report what would change - result.changes.push('Would be updated (dry-run)'); - logger.info(` ~ ${path.relative(process.cwd(), filePath)} (dry-run)`); + result.changes.push('Would be updated (dry)'); + logger.info(` ~ ${path.relative(process.cwd(), filePath)} (dry)`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; result.errors.push(errorMessage); - logger.error(` ✗ ${path.relative(process.cwd(), filePath)}: ${errorMessage}`); + logger.error(` ${path.relative(process.cwd(), filePath)}: ${errorMessage}`); } return result; diff --git a/src/types.ts b/src/types.ts index 474b5e7..9496a72 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,14 +4,11 @@ import type { Collection, JSCodeshift } from 'jscodeshift'; * CLI options */ export interface CliOptions { - dryRun: boolean; + dry: boolean; force: boolean; parser: string; - extensions: string; - ignore?: string; print: boolean; verbose: boolean; - skipValidation: boolean; } /** diff --git a/src/utils/git-safety.ts b/src/utils/git-safety.ts index e6022f4..243330b 100644 --- a/src/utils/git-safety.ts +++ b/src/utils/git-safety.ts @@ -19,13 +19,13 @@ export function checkGitStatus(logger: Logger): void { clean = true; } else if (err && err.code === 'MODULE_NOT_FOUND') { // is-git-clean not installed - warn and allow - logger.warn('⚠️ Git safety check skipped (is-git-clean not installed)'); + logger.warn('Git safety check skipped (is-git-clean not installed)'); clean = true; } } if (!clean) { - logger.error('❌ Git working directory is not clean'); + logger.error('Git working directory is not clean'); logger.info('Please commit or stash your changes before running the codemod.'); logger.info('This ensures you can easily revert changes if needed.'); logger.newline();