Skip to content

Commit 0202152

Browse files
authored
feat: add upgrade command (#8)
* feat: add upgrade command * test: support more tests * test: enhance upgrade command tests for console output
1 parent 7d5992f commit 0202152

File tree

5 files changed

+162
-20
lines changed

5 files changed

+162
-20
lines changed

commands/__test__/upgrade.spec.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { run } from 'jscodeshift/src/Runner'
2+
import prompts from 'prompts'
3+
import { upgrade } from '../upgrade'
4+
5+
jest.mock('jscodeshift/src/Runner', () => ({
6+
run: jest.fn(),
7+
}))
8+
9+
describe('interactive mode', () => {
10+
beforeEach(() => {
11+
jest.clearAllMocks()
12+
})
13+
14+
it('runs without source params provided and select is true', async () => {
15+
const spyOnConsole = jest.spyOn(console, 'log').mockImplementation()
16+
17+
prompts.inject([['magic-redirect', 'req-param']])
18+
19+
await upgrade('__testfixtures__', { select: true })
20+
21+
expect(spyOnConsole).toHaveBeenCalled()
22+
expect(spyOnConsole).toHaveBeenCalledTimes(3)
23+
expect(run).toHaveBeenCalledTimes(2)
24+
})
25+
26+
it('runs with source params provided and select is true', async () => {
27+
const spyOnConsole = jest.spyOn(console, 'log').mockImplementation()
28+
29+
prompts.inject([['magic-redirect', 'req-param']])
30+
31+
await upgrade('__testfixtures__', { select: true })
32+
33+
expect(spyOnConsole).toHaveBeenCalled()
34+
expect(spyOnConsole).toHaveBeenCalledTimes(3)
35+
expect(run).toHaveBeenCalledTimes(2)
36+
})
37+
38+
it('runs without source params provided and select is undefined', async () => {
39+
const spyOnConsole = jest.spyOn(console, 'log').mockImplementation()
40+
41+
prompts.inject(['__testfixtures__'])
42+
43+
await upgrade(undefined)
44+
45+
expect(spyOnConsole).toHaveBeenCalled()
46+
expect(spyOnConsole).toHaveBeenCalledTimes(5)
47+
expect(run).toHaveBeenCalledTimes(4)
48+
})
49+
})
50+
51+
describe('Non-Interactive Mode', () => {
52+
beforeEach(() => {
53+
jest.clearAllMocks()
54+
})
55+
56+
it('Transforms code with source params provided', async () => {
57+
const spyOnConsole = jest.spyOn(console, 'log').mockImplementation()
58+
59+
await upgrade('__testfixtures__')
60+
61+
expect(spyOnConsole).toHaveBeenCalledTimes(5)
62+
expect(spyOnConsole).toHaveBeenCalledWith('> Applying codemod: magic-redirect')
63+
expect(spyOnConsole).toHaveBeenCalledWith('> Applying codemod: pluralized-methods')
64+
expect(spyOnConsole).toHaveBeenCalledWith('> Applying codemod: req-param')
65+
expect(spyOnConsole).toHaveBeenCalledWith('> Applying codemod: v4-deprecated-signatures')
66+
expect(spyOnConsole).toHaveBeenLastCalledWith('\n> All codemods have been applied successfully. \n')
67+
expect(run).toHaveBeenCalledTimes(4)
68+
})
69+
})

commands/transform.ts

+2-20
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import { run as jscodeshift } from 'jscodeshift/src/Runner'
44
import { bold } from 'picocolors'
55
import prompts from 'prompts'
66
import { TRANSFORM_OPTIONS } from '../config'
7-
8-
export function onCancel() {
9-
console.info('> Cancelled process. Program will stop now without any actions. \n')
10-
process.exit(1)
11-
}
7+
import { onCancel, promptSource } from '../utils/prompts'
128

139
const transformerDirectory = join(__dirname, '../', 'transforms')
1410

@@ -32,20 +28,6 @@ const selectCodemod = async (): Promise<string> => {
3228
return res.transformer
3329
}
3430

35-
const selectSource = async (): Promise<string> => {
36-
const res = await prompts(
37-
{
38-
type: 'text',
39-
name: 'path',
40-
message: 'Which files or directories should the codemods be applied to?',
41-
initial: '.',
42-
},
43-
{ onCancel },
44-
)
45-
46-
return res.path
47-
}
48-
4931
export async function transform(codemodName?: string, source?: string, options?: Record<string, unknown>) {
5032
const existCodemod = TRANSFORM_OPTIONS.find(({ value }) => value === codemodName)
5133
const codemodSelected = !codemodName || (codemodName && !existCodemod) ? await selectCodemod() : codemodName
@@ -55,7 +37,7 @@ export async function transform(codemodName?: string, source?: string, options?:
5537
process.exit(1)
5638
}
5739

58-
const sourceSelected = source || (await selectSource())
40+
const sourceSelected = source || (await promptSource('Which files or directories should the codemods be applied to?'))
5941

6042
if (!sourceSelected) {
6143
console.info('> Source path for project is not selected. Exits the program. \n')

commands/upgrade.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { join, resolve } from 'node:path'
2+
import type { Options } from 'jscodeshift'
3+
import { run as jscodeshift } from 'jscodeshift/src/Runner'
4+
import prompts from 'prompts'
5+
import { TRANSFORM_OPTIONS } from '../config'
6+
import { onCancel, promptSource } from '../utils/prompts'
7+
8+
const transformerDirectory = join(__dirname, '../', 'transforms')
9+
10+
export async function upgrade(source?: string, options?: Record<string, unknown>) {
11+
const sourceSelected = source || (await promptSource('Which directory should the codemods be applied to?'))
12+
13+
if (!sourceSelected) {
14+
console.info('> Source path for project is not selected. Exits the program. \n')
15+
process.exit(1)
16+
}
17+
let codemods: string[] = []
18+
19+
if (options?.select) {
20+
const { codemodsSelected } = await prompts(
21+
{
22+
type: 'multiselect',
23+
name: 'codemodsSelected',
24+
message: `The following 'codemods' are recommended for your upgrade. Select the ones to apply.`,
25+
choices: TRANSFORM_OPTIONS.map(({ description, value, version }) => {
26+
return {
27+
title: `(v${version}) ${value}`,
28+
description,
29+
value,
30+
selected: true,
31+
}
32+
}),
33+
},
34+
{ onCancel },
35+
)
36+
codemods = codemodsSelected
37+
} else {
38+
codemods = TRANSFORM_OPTIONS.map(({ value }) => value)
39+
}
40+
41+
const args: Options = {
42+
dry: false,
43+
babel: false,
44+
silent: true,
45+
ignorePattern: '**/node_modules/**',
46+
extensions: 'cts,mts,ts,js,mjs,cjs',
47+
}
48+
49+
for (const codemod of codemods) {
50+
const transformerPath = join(transformerDirectory, `${codemod}.js`)
51+
52+
console.log(`> Applying codemod: ${codemod}`)
53+
54+
try {
55+
await jscodeshift(transformerPath, [resolve(sourceSelected)], args)
56+
} catch (error) {
57+
console.error(`> Error applying codemod: ${codemod}`)
58+
console.error(error)
59+
}
60+
}
61+
62+
console.log('\n> All codemods have been applied successfully. \n')
63+
}

index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { Command } from 'commander'
77
import { transform } from './commands/transform'
8+
import { upgrade } from './commands/upgrade'
89
import packageJson from './package.json'
910

1011
const program = new Command(packageJson.name)
@@ -22,4 +23,11 @@ const program = new Command(packageJson.name)
2223
// Why this option is necessary is explained here: https://github.com/tj/commander.js/pull/1427
2324
.enablePositionalOptions()
2425

26+
program
27+
.command('upgrade')
28+
.description('Upgrade your express server to the latest version.')
29+
.argument('[source]', 'Path to source files or directory to transform.')
30+
.option('--select', 'Select which codemods to apply (Show a list of available codemods)')
31+
.action(upgrade)
32+
2533
program.parse(process.argv)

utils/prompts.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import prompts from 'prompts'
2+
3+
export function onCancel() {
4+
console.info('> Cancelled process. Program will stop now without any actions. \n')
5+
process.exit(1)
6+
}
7+
8+
export const promptSource = async (message: string): Promise<string> => {
9+
const res = await prompts(
10+
{
11+
type: 'text',
12+
name: 'path',
13+
message,
14+
initial: '.',
15+
},
16+
{ onCancel },
17+
)
18+
19+
return res.path
20+
}

0 commit comments

Comments
 (0)