diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffbcf22..1d15384 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: 'lts/*' - persist-credentials: false - name: Install dependencies run: npm install --ignore-scripts --only=dev @@ -39,11 +38,11 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest] + os: [ubuntu-latest, windows-latest, macos-latest] node-version: [18, 19, 20, 21, 22, 23] # Node.js release schedule: https://nodejs.org/en/about/releases/ - name: Node.js ${{ matrix.node-version }} + name: Node.js ${{ matrix.node-version }} - ${{matrix.os}} runs-on: ${{ matrix.os }} steps: diff --git a/.gitignore b/.gitignore index 755450a..952607d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ node_modules coverage # Build Outputs -dist +build # Debug npm-debug.log* diff --git a/commands/__test__/transform.spec.ts b/commands/__test__/transform.spec.ts new file mode 100644 index 0000000..7866cb3 --- /dev/null +++ b/commands/__test__/transform.spec.ts @@ -0,0 +1,112 @@ +import { join } from 'node:path' +import { run } from 'jscodeshift/src/Runner' +import prompts from 'prompts' +import { transform } from '../transform' + +jest.mock('jscodeshift/src/Runner', () => ({ + run: jest.fn(), +})) + +describe('interactive mode', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('runs without codemodName and source params provided', async () => { + const spyOnConsole = jest.spyOn(console, 'log').mockImplementation() + + prompts.inject(['magic-redirect']) + prompts.inject(['./transforms/__testfixtures__']) + + await transform(undefined, undefined, { dry: true, silent: true }) + + expect(spyOnConsole).not.toHaveBeenCalled() + expect(run).toHaveBeenCalledTimes(1) + expect(run).toHaveBeenCalledWith( + join(__dirname, '../../', 'transforms/magic-redirect.js'), + ['./transforms/__testfixtures__'], + { + babel: false, + dry: true, + extensions: 'cts,mts,ts,js,mjs,cjs', + ignorePattern: '**/node_modules/**', + silent: true, + verbose: 0, + }, + ) + }) + + it('runs properly on incorrect user input', async () => { + const spyOnConsole = jest.spyOn(console, 'log').mockImplementation() + + prompts.inject(['magic-redirect']) + + await transform('bad-codemod', './transforms/__testfixtures__', { + dry: true, + silent: true, + }) + + expect(spyOnConsole).not.toHaveBeenCalled() + expect(run).toHaveBeenCalledTimes(1) + expect(run).toHaveBeenCalledWith( + join(__dirname, '../../', 'transforms/magic-redirect.js'), + ['./transforms/__testfixtures__'], + { + babel: false, + dry: true, + extensions: 'cts,mts,ts,js,mjs,cjs', + ignorePattern: '**/node_modules/**', + silent: true, + verbose: 0, + }, + ) + }) + + it('runs with codemodName and without source param provided', async () => { + const spyOnConsole = jest.spyOn(console, 'log').mockImplementation() + + prompts.inject(['__testfixtures__']) + + await transform('magic-redirect', undefined, { + dry: true, + silent: true, + }) + + expect(spyOnConsole).not.toHaveBeenCalled() + expect(run).toHaveBeenCalledTimes(1) + expect(run).toHaveBeenCalledWith(join(__dirname, '../../', 'transforms/magic-redirect.js'), ['__testfixtures__'], { + babel: false, + dry: true, + extensions: 'cts,mts,ts,js,mjs,cjs', + ignorePattern: '**/node_modules/**', + silent: true, + verbose: 0, + }) + }) +}) + +describe('Non-Interactive Mode', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('Transforms code with codemodName and source params provided', async () => { + const spyOnConsole = jest.spyOn(console, 'log').mockImplementation() + + await transform('magic-redirect', '__testfixtures__', { + dry: true, + silent: true, + }) + + expect(spyOnConsole).not.toHaveBeenCalled() + expect(run).toHaveBeenCalledTimes(1) + expect(run).toHaveBeenCalledWith(join(__dirname, '../../', 'transforms/magic-redirect.js'), ['__testfixtures__'], { + babel: false, + dry: true, + extensions: 'cts,mts,ts,js,mjs,cjs', + ignorePattern: '**/node_modules/**', + silent: true, + verbose: 0, + }) + }) +}) diff --git a/commands/transform.ts b/commands/transform.ts index 076f96a..9ac5659 100644 --- a/commands/transform.ts +++ b/commands/transform.ts @@ -1,25 +1,22 @@ import { join } from 'node:path' -import execa from 'execa' -import { bold, green } from 'picocolors' +import type { Options } from 'jscodeshift' +import { run as jscodeshift } from 'jscodeshift/src/Runner' +import { bold } from 'picocolors' import prompts from 'prompts' import { TRANSFORM_OPTIONS } from '../config' -import { getAllFiles } from '../utils/file' export function onCancel() { + console.info('> Cancelled process. Program will stop now without any actions. \n') process.exit(1) } -const jscodeshiftExecutable = require.resolve('.bin/jscodeshift') const transformerDirectory = join(__dirname, '../', 'transforms') -// biome-ignore lint/suspicious/noExplicitAny: 'Any' is used because options can be anything. -export async function transform(codemodName: string, source: string, options: any): Promise { +export async function transform(codemodName?: string, source?: string, options?: Record) { let codemodSelected = codemodName let sourceSelected = source - const { dry, print, verbose } = options - - let existCodemod = TRANSFORM_OPTIONS.find(({ value }) => value === codemodSelected) + const existCodemod = TRANSFORM_OPTIONS.find(({ value }) => value === codemodSelected) if (!codemodSelected || (codemodSelected && !existCodemod)) { const res = await prompts( @@ -39,7 +36,6 @@ export async function transform(codemodName: string, source: string, options: an ) codemodSelected = res.transformer - existCodemod = TRANSFORM_OPTIONS.find(({ value }) => value === codemodSelected) } if (!sourceSelected) { @@ -56,37 +52,20 @@ export async function transform(codemodName: string, source: string, options: an sourceSelected = res.path } - const transformerPath = join(transformerDirectory, `${codemodSelected}.js`) - - const args: string[] = [] - - if (dry) { - args.push('--dry') + if (!codemodSelected) { + console.info('> Codemod is not selected. Exist the program. \n') + process.exit(1) } - if (print) { - args.push('--print') - } + const transformerPath = join(transformerDirectory, `${codemodSelected}.js`) - if (verbose) { - args.push('--verbose=2') + const args: Options = { + ...options, + verbose: options?.verbose ? 2 : 0, + babel: false, + ignorePattern: '**/node_modules/**', + extensions: 'cts,mts,ts,js,mjs,cjs', } - args.push('--no-babel') - args.push('--ignore-pattern=**/node_modules/**') - args.push('--extensions=cts,mts,ts,js,mjs,cjs') - - const files = await getAllFiles(sourceSelected) - - args.push('--transform', transformerPath, ...files.map((file) => file.toString())) - - console.log(`Executing command: ${green('jscodeshift')} ${args.join(' ')}`) - - const jscodeshiftProcess = execa(jscodeshiftExecutable, args, { - // include ANSI color codes - env: process.stdout.isTTY ? { FORCE_COLOR: 'true' } : {}, - }) - - jscodeshiftProcess.stdout?.pipe(process.stdout) - jscodeshiftProcess.stderr?.pipe(process.stderr) + await jscodeshift(transformerPath, [sourceSelected || ''], args) } diff --git a/config.ts b/config.ts index e7838e9..89e079b 100644 --- a/config.ts +++ b/config.ts @@ -14,4 +14,24 @@ export const TRANSFORM_OPTIONS = [ value: 'v4-deprecated-signatures', version: '5.0.0', }, + { + description: 'Reverse param order for "redirect" method', + value: 'redirect', + version: '5.0.0', + }, + { + description: 'Change request.param() to dedicated methods', + value: 'req-param', + version: '5.0.0', + }, + { + description: 'Convert method name "sendfile" to "sendFile"', + value: 'send-file', + version: '5.0.0', + }, + { + description: 'Convert method name "del" to "delete"', + value: 'full-name-delete', + version: '5.0.0', + }, ] diff --git a/index.ts b/index.ts index 22a9472..f5293ae 100644 --- a/index.ts +++ b/index.ts @@ -16,6 +16,7 @@ const program = new Command(packageJson.name) .option('-d, --dry', 'Dry run (no changes are made to files)') .option('-p, --print', 'Print transformed files to stdout') .option('--verbose', 'Show more information about the transform process') + .option('--silent', "Don't print anything to stdout") .usage('[codemod] [source] [options]') .action(transform) // Why this option is necessary is explained here: https://github.com/tj/commander.js/pull/1427 diff --git a/jest.config.js b/jest.config.js index f8ce674..d1f1374 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,4 +3,5 @@ module.exports = { transform: { '\\.ts$': ['ts-jest', { isolatedModules: true }], }, + modulePathIgnorePatterns: ['/build/'], } diff --git a/package.json b/package.json index 922e1d1..4f2e9cf 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,11 @@ "private": true, "version": "0.0.1", "description": "Codemods for updating express servers.", - "main": "index.js", + "main": "build/index.js", "contributors": ["Sebastian Beltran ", "Filip Kudla "], "license": "MIT", - "bin": "./index.js", - "files": ["transforms/*.js", "commands/*.js", "utils/*.js", "config.js", "index.js"], + "bin": "build/index.js", + "files": ["build/transforms/*.js", "build/commands/*.js", "build/utils/*.js", "build/config.js", "build/index.js"], "scripts": { "dev": "tsc -d -w -p tsconfig.json", "build": "tsc -d -p tsconfig.json", @@ -18,7 +18,6 @@ }, "dependencies": { "commander": "^12.1.0", - "execa": "^5.1.1", "fast-glob": "^3.3.2", "jscodeshift": "^17.1.1", "picocolors": "^1.1.1", diff --git a/tsconfig.json b/tsconfig.json index 59bebb0..60c33c5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,8 +7,9 @@ "downlevelIteration": true, "preserveWatchOutput": true, "resolveJsonModule": true, - "strictNullChecks": true + "strictNullChecks": true, + "outDir": "build" }, "include": ["**/*.ts"], - "exclude": ["node_modules", "transforms/__testfixtures__/**"] + "exclude": ["node_modules", "build", "transforms/__testfixtures__/**", "__test__", "**/*.spec.ts"] }