Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add upgrade command #8

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 2 additions & 20 deletions commands/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ import { run as jscodeshift } from 'jscodeshift/src/Runner'
import { bold } from 'picocolors'
import prompts from 'prompts'
import { TRANSFORM_OPTIONS } from '../config'

export function onCancel() {
console.info('> Cancelled process. Program will stop now without any actions. \n')
process.exit(1)
}
import { onCancel, promptSource } from '../utils/share'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this share filename needs adjustment. It does not tell what can be found inside. From what I can see you collect there utility methods for commands

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I gave it that name without thinking too much, I just wanted to separate it


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

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

const selectSource = async (): Promise<string> => {
const res = await prompts(
{
type: 'text',
name: 'path',
message: 'Which files or directories should the codemods be applied to?',
initial: '.',
},
{ onCancel },
)

return res.path
}

export async function transform(codemodName?: string, source?: string, options?: Record<string, unknown>) {
const existCodemod = TRANSFORM_OPTIONS.find(({ value }) => value === codemodName)
const codemodSelected = !codemodName || (codemodName && !existCodemod) ? await selectCodemod() : codemodName
Expand All @@ -55,7 +37,7 @@ export async function transform(codemodName?: string, source?: string, options?:
process.exit(1)
}

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

if (!sourceSelected) {
console.info('> Source path for project is not selected. Exits the program. \n')
Expand Down
109 changes: 109 additions & 0 deletions commands/upgrade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { readFile } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import type { Options } from 'jscodeshift'
import { run as jscodeshift } from 'jscodeshift/src/Runner'
import prompts from 'prompts'
import { coerce, compare } from 'semver'
import { TRANSFORM_OPTIONS } from '../config'
import { onCancel, promptSource } from '../utils/share'

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

export async function upgrade(source?: string) {
const sourceSelected = source || (await promptSource('Which directory should the codemods be applied to?'))

if (!sourceSelected) {
console.info('> Source path for project is not selected. Exits the program. \n')
process.exit(1)
}

let packageJson = ''

try {
const packageJsonPath = resolve(sourceSelected || '', 'package.json')
packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'))
} catch (err) {
if (err.code === 'ENOENT') {
console.info('> No package.json found in the selected directory. \n')
process.exit(1)
} else {
console.error(err.message)
}
}

const codemods = suggestCodemods(packageJson)

if (codemods.length === 0) {
console.info('> No codemods are suggested for this project. \n')

return
}

const { codemodsSelected } = await prompts(
{
type: 'multiselect',
name: 'codemodsSelected',
message: `The following 'codemods' are recommended for your upgrade. Select the ones to apply.`,
choices: codemods.map(({ description, value, version }) => {
return {
title: `(v${version}) ${value}`,
description,
value,
selected: true,
}
}),
},
{ onCancel },
)

const args: Options = {
dry: false,
babel: false,
silent: true,
ignorePattern: '**/node_modules/**',
extensions: 'cts,mts,ts,js,mjs,cjs',
}

const results = {
ok: 0,
skipped: 0,
failed: 0,
unmodified: 0,
}

for (const codemod of codemodsSelected) {
const transformerPath = require.resolve(`${transformerDirectory}/${codemod}.js`)

console.log(`> Applying codemod: ${codemod}`)
const { ok, skip, error, nochange } = await jscodeshift(transformerPath, [resolve(sourceSelected)], args)

results.ok += ok
results.skipped += skip
results.failed += error
results.unmodified += nochange
}

console.log('\n> Summary of the upgrade')
console.log(`> ${results.ok} codemods were applied successfully`)
console.log(`> ${results.skipped} codemods were skipped`)
console.log(`> ${results.failed} codemods failed`)
console.log(`> ${results.unmodified} codemods were skipped because they didn't change anything`)
}

function suggestCodemods(packageJson) {
const { dependencies } = packageJson

if (dependencies?.express == null) {
console.info('> No express dependency found in package.json. \n')

process.exit(0)
}

const expressVersion = coerce(dependencies.express)?.version ?? '4.0.0'

const codemodsSuggested = TRANSFORM_OPTIONS.filter((a) => {
return compare(a.version, expressVersion) > 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we apply our codemods only if the project is on v4?

according to docs:

compare(v1, v2): Return 0 if v1 == v2, or 1 if v1 is greater, or -1 if v2 is greater.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is more thought out so that in the future more codemods can be added as Express evolves, and not limit us to version 4

})

return codemodsSuggested
}
7 changes: 7 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { Command } from 'commander'
import { transform } from './commands/transform'
import { upgrade } from './commands/upgrade'
import packageJson from './package.json'

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

program
.command('upgrade')
.description('Upgrade your express server to the latest version.')
.argument('[source]', 'Path to source files or directory to transform.')
.action(upgrade)

program.parse(process.argv)
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@
"fast-glob": "^3.3.2",
"jscodeshift": "^17.1.1",
"picocolors": "^1.1.1",
"prompts": "^2.4.2"
"prompts": "^2.4.2",
"semver": "^7.6.3"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@types/jest": "29.5.14",
"@types/jscodeshift": "^0.12.0",
"@types/node": "^22.8.1",
"@types/prompts": "2.4.9",
"@types/semver": "^7.5.8",
"jest": "29.7.0",
"ts-jest": "29.2.5",
"typescript": "5.6.3"
Expand Down
20 changes: 20 additions & 0 deletions utils/share.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import prompts from 'prompts'

export function onCancel() {
console.info('> Cancelled process. Program will stop now without any actions. \n')
process.exit(1)
}

export const promptSource = async (message: string): Promise<string> => {
const res = await prompts(
{
type: 'text',
name: 'path',
message,
initial: '.',
},
{ onCancel },
)

return res.path
}
Loading