Skip to content

Commit 34e038b

Browse files
csandmanclaude
andcommitted
chore(codemod): Replace deprecated deps with native alternatives
- Remove `execa`, `inquirer`, `meow`, `is-git-clean` and all transitive deprecated deps (cross-spawn-async, etc.) - Add `commander` (CLI parsing with auto-generated --help) and `prompts` (lightweight interactive prompts) - Inline git clean check using node:child_process execFileSync instead of is-git-clean - Replace execaSync with native spawnSync for jscodeshift invocation - Add --parser=tsx flag to jscodeshift so TSX files are parsed correctly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b3c4c04 commit 34e038b

4 files changed

Lines changed: 466 additions & 991 deletions

File tree

codemod/bin/cli.ts

Lines changed: 91 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44
*
55
* @see {@link https://github.com/vercel/next.js/blob/dc9f30c/packages/next-codemod/bin/cli.ts}
66
*/
7-
import { execaSync } from "execa";
7+
import { Command } from "commander";
88
import { globbySync } from "globby";
9-
import inquirer, { type DistinctQuestion } from "inquirer";
10-
import isGitClean from "is-git-clean";
11-
import meow, { type AnyFlags } from "meow";
9+
import { execFileSync, spawnSync } from "node:child_process";
1210
import { createRequire } from "node:module";
1311
import path from "node:path";
1412
import { fileURLToPath } from "node:url";
1513
import pc from "picocolors";
14+
import prompts from "prompts";
1615

1716
const { yellow } = pc;
1817

@@ -27,7 +26,11 @@ export function checkGitStatus(force: boolean) {
2726
let clean = false;
2827
let errorMessage = "Unable to determine if git directory is clean";
2928
try {
30-
clean = isGitClean.sync(process.cwd());
29+
const stdout = execFileSync("git", ["status", "--porcelain"], {
30+
cwd: process.cwd(),
31+
encoding: "utf8",
32+
});
33+
clean = stdout.trim().length === 0;
3134
errorMessage = "Git directory is not clean";
3235
} catch (err: unknown) {
3336
const stderr =
@@ -63,7 +66,7 @@ interface RunTransformOptions {
6366
dry?: boolean;
6467
print?: boolean;
6568
runInBand?: boolean;
66-
jscodeshift?: readonly string[];
69+
jscodeshift?: string[];
6770
}
6871

6972
export function runTransform({
@@ -89,6 +92,7 @@ export function runTransform({
8992
}
9093

9194
args.push("--verbose=2");
95+
args.push("--parser=tsx");
9296

9397
args.push("--ignore-pattern=**/node_modules/**");
9498
args.push("--ignore-pattern=**/.next/**");
@@ -105,19 +109,19 @@ export function runTransform({
105109

106110
console.log(`Executing command: jscodeshift ${args.join(" ")}`);
107111

108-
const result = execaSync(jscodeshiftExecutable, args, {
109-
stdio: "inherit",
110-
stripFinalNewline: false,
111-
});
112+
const result = spawnSync(jscodeshiftExecutable, args, { stdio: "inherit" });
112113

113-
if (result.failed) {
114-
throw new Error(`jscodeshift exited with code ${result.exitCode}`);
114+
if (result.error) {
115+
throw result.error;
116+
}
117+
if (result.status !== 0) {
118+
throw new Error(`jscodeshift exited with code ${result.status}`);
115119
}
116120
}
117121

118-
const TRANSFORMER_INQUIRER_CHOICES = [
122+
const TRANSFORMER_CHOICES = [
119123
{
120-
name: "v5: Remove or replace deprecated props",
124+
title: "v5: Remove or replace deprecated props",
121125
value: "v5",
122126
},
123127
];
@@ -131,101 +135,92 @@ function expandFilePathsIfNeeded(filesBeforeExpansion: string[]) {
131135
: filesBeforeExpansion;
132136
}
133137

134-
const flagsSchema = {
135-
force: { type: "boolean" },
136-
dry: { type: "boolean" },
137-
print: { type: "boolean" },
138-
runInBand: { type: "boolean" },
139-
jscodeshift: { type: "string", isMultiple: true },
140-
help: { type: "boolean", shortFlag: "h" },
141-
} as const satisfies AnyFlags;
142-
143-
interface PromptAnswers {
144-
files?: string;
145-
transformer?: string;
138+
interface ProgramOptions {
139+
force?: boolean;
140+
dry?: boolean;
141+
print?: boolean;
142+
runInBand?: boolean;
143+
jscodeshift?: string[];
146144
}
147145

148-
export function run() {
149-
const cli = meow({
150-
importMeta: import.meta,
151-
description: "Codemods for updating chakra-react-select in applications.",
152-
help: `
153-
Usage
154-
$ npx crs-codemod <transform> <path> <...options>
155-
transform One of the choices from https://github.com/vercel/next.js/tree/canary/packages/next-codemod
156-
path Files or directory to transform. Can be a glob like pages/**.js
157-
Options
158-
--force Bypass Git safety checks and forcibly run codemods
159-
--dry Dry run (no changes are made to files)
160-
--print Print transformed files to your terminal
161-
--jscodeshift (Advanced) Pass options directly to jscodeshift
162-
`,
163-
flags: flagsSchema,
164-
});
165-
166-
if (!cli.flags.dry) {
167-
checkGitStatus(!!cli.flags.force);
146+
export async function run() {
147+
const program = new Command()
148+
.description("Codemods for updating chakra-react-select in applications.")
149+
.argument("[transform]", "codemod transform to apply")
150+
.argument("[path]", "files or directory to transform (supports globs)")
151+
.option("--force", "bypass Git safety checks and forcibly run codemods")
152+
.option("--dry", "dry run (no changes are made to files)")
153+
.option("--print", "print transformed files to your terminal")
154+
.option(
155+
"--run-in-band",
156+
"run in a single process (slower but useful for debugging)"
157+
)
158+
.option(
159+
"--jscodeshift <options...>",
160+
"(advanced) pass options directly to jscodeshift"
161+
)
162+
.parse(process.argv);
163+
164+
const opts = program.opts<ProgramOptions>();
165+
const [inputTransform, inputPath] = program.args;
166+
167+
if (!opts.dry) {
168+
checkGitStatus(!!opts.force);
168169
}
169170

170171
if (
171-
cli.input[0] &&
172-
!TRANSFORMER_INQUIRER_CHOICES.find((x) => x.value === cli.input[0])
172+
inputTransform &&
173+
!TRANSFORMER_CHOICES.find((x) => x.value === inputTransform)
173174
) {
174175
console.error("Invalid transform choice, pick one of:");
175-
console.error(
176-
TRANSFORMER_INQUIRER_CHOICES.map((x) => `- ${x.value}`).join("\n")
177-
);
176+
console.error(TRANSFORMER_CHOICES.map((x) => `- ${x.value}`).join("\n"));
178177
process.exit(1);
179178
}
180179

181-
const questions: DistinctQuestion<PromptAnswers>[] = [
182-
{
183-
type: "input",
184-
name: "files",
185-
message: "On which files or directory should the codemods be applied?",
186-
when: !cli.input[1],
187-
default: ".",
188-
filter: (files: string) => files.trim(),
189-
},
190-
{
191-
type: "select",
192-
name: "transformer",
193-
message: "Which transform would you like to apply?",
194-
when: !cli.input[0],
195-
pageSize: TRANSFORMER_INQUIRER_CHOICES.length,
196-
choices: TRANSFORMER_INQUIRER_CHOICES,
197-
},
198-
];
199-
200-
inquirer.prompt<PromptAnswers>(questions).then((answers) => {
201-
const { files, transformer } = answers;
202-
203-
const filesBeforeExpansion = cli.input[1] || files;
204-
if (!filesBeforeExpansion) {
205-
console.log("No files or directory provided.");
206-
return null;
207-
}
180+
const answers = await prompts(
181+
[
182+
{
183+
type: inputPath ? null : "text",
184+
name: "files",
185+
message: "On which files or directory should the codemods be applied?",
186+
initial: ".",
187+
format: (val: string) => val.trim(),
188+
},
189+
{
190+
type: inputTransform ? null : "select",
191+
name: "transformer",
192+
message: "Which transform would you like to apply?",
193+
choices: TRANSFORMER_CHOICES,
194+
},
195+
],
196+
{ onCancel: () => process.exit(1) }
197+
);
208198

209-
const filesExpanded = expandFilePathsIfNeeded([filesBeforeExpansion]);
199+
const filesBeforeExpansion = inputPath ?? answers.files;
200+
if (!filesBeforeExpansion) {
201+
console.log("No files or directory provided.");
202+
return;
203+
}
210204

211-
const selectedTransformer = cli.input[0] || transformer;
212-
if (!selectedTransformer) {
213-
console.log("No transformer selected.");
214-
return null;
215-
}
205+
const selectedTransformer = inputTransform ?? answers.transformer;
206+
if (!selectedTransformer) {
207+
console.log("No transformer selected.");
208+
return;
209+
}
216210

217-
if (!filesExpanded.length) {
218-
console.log(`No files found matching ${filesBeforeExpansion}`);
219-
return null;
220-
}
211+
const filesExpanded = expandFilePathsIfNeeded([filesBeforeExpansion]);
221212

222-
return runTransform({
223-
files: filesExpanded,
224-
transformer: selectedTransformer,
225-
dry: cli.flags.dry,
226-
print: cli.flags.print,
227-
runInBand: cli.flags.runInBand,
228-
jscodeshift: cli.flags.jscodeshift,
229-
});
213+
if (!filesExpanded.length) {
214+
console.log(`No files found matching ${filesBeforeExpansion}`);
215+
return;
216+
}
217+
218+
runTransform({
219+
files: filesExpanded,
220+
transformer: selectedTransformer,
221+
dry: opts.dry,
222+
print: opts.print,
223+
runInBand: opts.runInBand,
224+
jscodeshift: opts.jscodeshift,
230225
});
231226
}

codemod/bin/crs-codemod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88
*/
99
import { run } from "./cli.js";
1010

11-
run();
11+
run().catch(console.error);

codemod/package.json

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,18 @@
2525
"prepublishOnly": "pnpm build"
2626
},
2727
"dependencies": {
28-
"execa": "^9.6.1",
28+
"commander": "^14.0.3",
2929
"globby": "^16.2.0",
30-
"inquirer": "^13.4.3",
31-
"is-git-clean": "^1.1.0",
3230
"jscodeshift": "^17.3.0",
33-
"meow": "^14.1.0",
34-
"picocolors": "^1.1.1"
31+
"picocolors": "^1.1.1",
32+
"prompts": "^2.4.2"
3533
},
3634
"devDependencies": {
37-
"@types/is-git-clean": "^1.1.2",
3835
"@types/jscodeshift": "^17.3.0",
3936
"@types/node": "^24.12.4",
37+
"@types/prompts": "^2.4.9",
4038
"rimraf": "^6.1.3",
4139
"typescript": "^6.0.3",
42-
"vitest": "^4.1.6"
40+
"vitest": "^4.1.7"
4341
}
4442
}

0 commit comments

Comments
 (0)