Skip to content

Commit 6a0cddf

Browse files
authored
feat: transform command (#7)
* initial draft * Merge branch 'main' into transform-command * somoe changes * add logic files * write new content * remove --print * remove allowUnknownOption
1 parent ef07a8d commit 6a0cddf

File tree

5 files changed

+138
-3
lines changed

5 files changed

+138
-3
lines changed

commands/transform.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { writeFile } from 'node:fs/promises'
2+
import { bold } from 'picocolors'
3+
import prompts from 'prompts'
4+
import { TRANSFORM_OPTIONS } from '../config'
5+
import { getAllFiles, getContent } from '../utils/file'
6+
7+
export function onCancel() {
8+
process.exit(1)
9+
}
10+
11+
// biome-ignore lint/suspicious/noExplicitAny: 'Any' is used because options can be anything.
12+
export async function transform(codemodName: string, source: string, options: any): Promise<void> {
13+
let codemodSelected = codemodName
14+
let sourceSelected = source
15+
16+
const { dry } = options
17+
18+
let existCodemod = TRANSFORM_OPTIONS.find(({ value }) => value === codemodSelected)
19+
20+
if (!codemodSelected || (codemodSelected && !existCodemod)) {
21+
const res = await prompts(
22+
{
23+
type: 'select',
24+
name: 'transformer',
25+
message: 'Which codemod would you like to apply?',
26+
choices: TRANSFORM_OPTIONS.map(({ description, value, version }) => {
27+
return {
28+
title: `(${bold(`v${version}`)}) ${value}`,
29+
description,
30+
value,
31+
}
32+
}),
33+
},
34+
{ onCancel },
35+
)
36+
37+
codemodSelected = res.transformer
38+
existCodemod = TRANSFORM_OPTIONS.find(({ value }) => value === codemodSelected)
39+
}
40+
41+
if (!sourceSelected) {
42+
const res = await prompts(
43+
{
44+
type: 'text',
45+
name: 'path',
46+
message: 'Which files or directories should the codemods be applied to?',
47+
initial: '.',
48+
},
49+
{ onCancel },
50+
)
51+
52+
sourceSelected = res.path
53+
}
54+
55+
const files = await getAllFiles(sourceSelected)
56+
57+
for (const file of files) {
58+
const content = await getContent(file)
59+
60+
if (existCodemod) {
61+
const newContent = existCodemod.codemod({ path: file.toString(), source: content }, options)
62+
63+
if (!dry) {
64+
await writeFile(file.toString(), newContent)
65+
}
66+
}
67+
}
68+
}

config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import magicRedirect from './transforms/magic-redirect'
2+
import pluralizedMethods from './transforms/pluralized-methods'
3+
4+
export const TRANSFORM_OPTIONS = [
5+
{
6+
description: 'Transform the deprecated magic string "back"',
7+
value: 'magic-redirect',
8+
version: '5.0.0',
9+
codemod: magicRedirect,
10+
},
11+
{
12+
description: 'Transform the methods to their pluralized versions',
13+
value: 'pluralized-methods',
14+
version: '5.0.0',
15+
codemod: pluralizedMethods,
16+
},
17+
//{ description: 'Transform the deprecated signatures in Express v4', value: 'signature-deprecated', version: '5.0.0' },
18+
]

index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
#!/usr/bin/env node
22

33
import { Command } from 'commander'
4+
import { transform } from './commands/transform'
45
import packageJson from './package.json'
56

67
const program = new Command(packageJson.name)
8+
.version(packageJson.version, '-v, --version', `Output the current version of ${packageJson.name}.`)
79
.description(packageJson.description)
810
.argument('[codemod]', 'Codemod slug to run')
911
.argument('[source]', 'Path to source files or directory to transform including glob patterns.')
10-
.usage('[codemod] [source]')
12+
.helpOption('-h, --help', 'Display this help message.')
13+
.option('-d, --dry', 'Dry run (no changes are made to files)')
14+
.usage('[codemod] [source] [options]')
15+
.action(transform)
16+
// Why this option is necessary is explained here: https://github.com/tj/commander.js/pull/1427
17+
.enablePositionalOptions()
18+
19+
program.parse(process.argv)

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,21 @@
1818
},
1919
"dependencies": {
2020
"commander": "^12.1.0",
21-
"jscodeshift": "^17.0.0"
21+
"fast-glob": "^3.3.2",
22+
"jscodeshift": "^17.0.0",
23+
"picocolors": "1.1.1",
24+
"prompts": "2.4.2"
2225
},
2326
"devDependencies": {
2427
"@biomejs/biome": "1.9.4",
2528
"@types/jest": "29.5.14",
2629
"@types/jscodeshift": "^0.12.0",
2730
"@types/node": "^22.8.1",
31+
"@types/prompts": "2.4.9",
2832
"@vercel/ncc": "0.38.2",
2933
"jest": "29.7.0",
3034
"ts-jest": "29.2.5",
31-
"typescript": "5.1.6"
35+
"typescript": "5.6.3"
3236
},
3337
"engines": {
3438
"node": ">=18"

utils/file.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { PathLike } from 'node:fs'
2+
import { readFile, stat } from 'node:fs/promises'
3+
import { join, resolve } from 'node:path'
4+
import { async as glob } from 'fast-glob'
5+
6+
export async function isDirectory(path: PathLike): Promise<boolean> {
7+
try {
8+
const metadata = await stat(path)
9+
return metadata.isDirectory()
10+
} catch (err) {
11+
return false
12+
}
13+
}
14+
15+
export async function getAllFiles(path: PathLike, arrayOfFiles: PathLike[] = []): Promise<PathLike[]> {
16+
if (await isDirectory(path)) {
17+
const files = await glob('**/*.{js,ts}', {
18+
cwd: path.toString(),
19+
dot: true,
20+
ignore: ['node_modules', 'dist', 'build'],
21+
markDirectories: true,
22+
})
23+
24+
for (const file of files) {
25+
arrayOfFiles.push(resolve(join(path.toString(), file)))
26+
}
27+
} else {
28+
arrayOfFiles.push(resolve(path.toString()))
29+
}
30+
31+
return arrayOfFiles
32+
}
33+
34+
export async function getContent(path: PathLike): Promise<string> {
35+
return readFile(path, 'utf-8')
36+
}

0 commit comments

Comments
 (0)