diff --git a/package-lock.json b/package-lock.json index 0388fb23..bd620faf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1476,6 +1476,10 @@ "resolved": "recipes/buffer-atob-btoa", "link": true }, + "node_modules/@nodejs/chalk-to-util-styletext": { + "resolved": "recipes/chalk-to-util-styletext", + "link": true + }, "node_modules/@nodejs/codemod-utils": { "resolved": "utils", "link": true @@ -4284,6 +4288,17 @@ "@codemod.com/jssg-types": "^1.3.0" } }, + "recipes/chalk-to-util-styletext": { + "name": "@nodejs/chalk-to-util-styletext", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + } + }, "recipes/correct-ts-specifiers": { "name": "@nodejs/correct-ts-specifiers", "version": "1.0.0", diff --git a/recipes/chalk-to-util-styletext/README.md b/recipes/chalk-to-util-styletext/README.md new file mode 100644 index 00000000..e0fc812d --- /dev/null +++ b/recipes/chalk-to-util-styletext/README.md @@ -0,0 +1,56 @@ +# Chalk to util.styleText + +This recipe migrates from the external `chalk` package to Node.js built-in `util.styleText` API. It transforms chalk method calls to use the native Node.js styling functionality. + +## Examples + +```diff +- import chalk from 'chalk'; ++ import { styleText } from 'node:util'; +- console.log(chalk.red('Error message')); ++ console.log(styleText('red', 'Error message')); +- console.log(chalk.green('Success message')); ++ console.log(styleText('green', 'Success message')); +- console.log(chalk.blue('Info message')); ++ console.log(styleText('blue', 'Info message')); +``` + +```diff +- import chalk from 'chalk'; ++ import { styleText } from 'node:util'; +- console.log(chalk.red.bold('Important error')); ++ console.log(styleText(['red', 'bold'], 'Important error')); +- console.log(chalk.green.underline('Success with emphasis')); ++ console.log(styleText(['green', 'underline'], 'Success with emphasis')); +``` + +```diff +- const chalk = require('chalk'); ++ const { styleText } = require('node:util'); +- const red = chalk.red; ++ const red = (text) => styleText('red', text); +- const boldBlue = chalk.blue.bold; ++ const boldBlue = (text) => styleText(['blue', 'bold'], text); +- console.log(red('Error')); ++ console.log(red('Error')); +- console.log(boldBlue('Info')); ++ console.log(boldBlue('Info')); +``` + +## Usage + +Run this codemod with: + +```sh +npx codemod nodejs/chalk-to-util-styletext +``` + +## Compatibility + +- **Removes chalk dependency** from package.json automatically +- **Supports most chalk methods**: colors, background colors, and text modifiers +- **Unsupported methods**: `hex()`, `rgb()`, `ansi256()`, `bgAnsi256()`, `visible()` (warnings will be shown) + +## Limitations + +- **Complex conditional expressions** in some contexts may need manual review diff --git a/recipes/chalk-to-util-styletext/codemod.yaml b/recipes/chalk-to-util-styletext/codemod.yaml new file mode 100644 index 00000000..1aed15ba --- /dev/null +++ b/recipes/chalk-to-util-styletext/codemod.yaml @@ -0,0 +1,24 @@ +schema_version: "1.0" +name: "@nodejs/chalk-to-util-styletext" +version: 1.0.0 +capabilities: + - fs + - child_process +description: Migrate from the chalk package to Node.js's built-in util.styleText API +author: Richie McColl +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + +registry: + access: public + visibility: public diff --git a/recipes/chalk-to-util-styletext/package.json b/recipes/chalk-to-util-styletext/package.json new file mode 100644 index 00000000..39b91392 --- /dev/null +++ b/recipes/chalk-to-util-styletext/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/chalk-to-util-styletext", + "version": "1.0.0", + "description": "Migrate from the chalk package to Node.js's built-in util.styleText API", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/chalk-to-util-styletext", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Richie McColl", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/chalk-to-util-styletext/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/chalk-to-util-styletext/src/remove-dependencies.ts b/recipes/chalk-to-util-styletext/src/remove-dependencies.ts new file mode 100644 index 00000000..0591fbcf --- /dev/null +++ b/recipes/chalk-to-util-styletext/src/remove-dependencies.ts @@ -0,0 +1,8 @@ +import removeDependencies from '@nodejs/codemod-utils/remove-dependencies'; + +/** + * Remove chalk and @types/chalk dependencies from package.json + */ +export default function removeChalkDependencies(): string | null { + return removeDependencies(['chalk', '@types/chalk']); +} diff --git a/recipes/chalk-to-util-styletext/src/workflow.ts b/recipes/chalk-to-util-styletext/src/workflow.ts new file mode 100644 index 00000000..ba8f44f2 --- /dev/null +++ b/recipes/chalk-to-util-styletext/src/workflow.ts @@ -0,0 +1,566 @@ +import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { + getNodeImportCalls, + getNodeImportStatements, +} from '@nodejs/codemod-utils/ast-grep/import-statement'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; + +/** + * Transform function that converts chalk method calls to Node.js util.styleText calls. + * + * Examples: + * - chalk.red("text") → styleText("red", "text") + * - chalk.red.bold("text") → styleText(["red", "bold"], "text") + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + const chalkBinding = 'chalk'; + + // This actually catches `node:chalk` import but we don't care as it shouldn't append + const statements = [ + ...getNodeImportStatements(root, chalkBinding), + ...getNodeRequireCalls(root, chalkBinding), + ...getNodeImportCalls(root, chalkBinding), + ]; + + // If there aren't any imports then we don't process the file + if (!statements.length) return null; + + for (const statement of statements) { + const initialEditCount = edits.length; + + // Check if we're dealing with a destructured import/require first + const destructuredNames = getDestructuredNames(statement); + + if (destructuredNames.length > 0) { + // Handle destructured imports + // const { red } = require('chalk') or import { red } from 'chalk' + processDestructuredImports(rootNode, destructuredNames, edits); + + // TODO - Handle special instances like chalkStderr + } else { + // Handle default imports + // const chalk = require('chalk') or import chalk from 'chalk' + const binding = resolveBindingPath(statement, '$'); + + if (binding) { + processDefaultImports(rootNode, binding, edits); + } + } + + // Track if any transformations occurred for this statement + if (edits.length > initialEditCount) { + const importReplacement = createImportReplacement(statement); + + if (importReplacement) { + edits.push(statement.replace(importReplacement)); + } + } + } + + if (!edits.length) return null; + + return rootNode.commitEdits(edits); +} + +// Compatibility mapping for chalk properties that differ in util.styleText +const COMPAT_MAP: Record = { + overline: 'overlined', +}; + +// Chalk methods that are supported by util.styleText +const SUPPORTED_METHODS = new Set([ + // Foreground colors + 'black', + 'red', + 'green', + 'yellow', + 'blue', + 'magenta', + 'cyan', + 'white', + 'gray', + 'grey', + 'blackBright', + 'redBright', + 'greenBright', + 'yellowBright', + 'blueBright', + 'magentaBright', + 'cyanBright', + 'whiteBright', + // Background colors + 'bgBlack', + 'bgRed', + 'bgGreen', + 'bgYellow', + 'bgBlue', + 'bgMagenta', + 'bgCyan', + 'bgWhite', + 'bgGray', + 'bgGrey', + 'bgBlackBright', + 'bgRedBright', + 'bgGreenBright', + 'bgYellowBright', + 'bgBlueBright', + 'bgMagentaBright', + 'bgCyanBright', + 'bgWhiteBright', + // Modifiers + 'reset', + 'bold', + 'italic', + 'underline', + 'strikethrough', + 'hidden', + 'dim', + 'overlined', + 'blink', + 'inverse', + 'doubleunderline', + 'framed', +]); + +/** + * Check if a method name is supported by util.styleText + */ +function isSupportedMethod(method: string): boolean { + return SUPPORTED_METHODS.has(method); +} + +/** + * Check if a style chain contains any unsupported methods + */ +function hasUnsupportedMethods(styles: string[]): boolean { + return styles.some((style) => !SUPPORTED_METHODS.has(style)); +} + +/** + * Extract destructured import names from a statement + * Returns an array of {imported, local} objects for each destructured import + */ +function getDestructuredNames( + statement: SgNode, +): Array<{ imported: string; local: string }> { + const names: Array<{ imported: string; local: string }> = []; + + // Handle ESM imports: import { red, blue as foo } from 'chalk' + if (statement.kind() === 'import_statement') { + const namedImports = statement.find({ + rule: { kind: 'named_imports' }, + }); + + if (namedImports) { + const importSpecifiers = namedImports.findAll({ + rule: { kind: 'import_specifier' }, + }); + + for (const specifier of importSpecifiers) { + const importedName = specifier.field('name'); + const alias = specifier.field('alias'); + + if (importedName) { + const imported = importedName.text(); + const local = alias ? alias.text() : imported; + + names.push({ imported, local }); + } + } + } + } + // Handle CommonJS requires: const { red, blue: foo } = require('chalk') + // Handle dynamic imports: const { red, blue: foo } = await import('chalk') + else if (statement.kind() === 'variable_declarator') { + const nameField = statement.field('name'); + + if (nameField && nameField.kind() === 'object_pattern') { + const properties = nameField.findAll({ + rule: { + any: [ + { kind: 'shorthand_property_identifier_pattern' }, + { kind: 'pair_pattern' }, + ], + }, + }); + + for (const prop of properties) { + if (prop.kind() === 'shorthand_property_identifier_pattern') { + // { red } - shorthand + const name = prop.text(); + + names.push({ imported: name, local: name }); + } else if (prop.kind() === 'pair_pattern') { + // { red: foo } - with alias + const key = prop.field('key'); + const value = prop.field('value'); + + if (key && value) { + const imported = key.text(); + const local = value.text(); + + names.push({ imported, local }); + } + } + } + } + } + + return names; +} + +/** + * Extract the first text argument from a function call + */ +function getFirstCallArgument(call: SgNode): string | null { + const args = call.field('arguments'); + + if (!args) return null; + + const argsList = args.children().filter((c) => { + const excluded = [',', '(', ')']; + return !excluded.includes(c.kind()); + }); + + if (argsList.length === 0) return null; + + return argsList[0].text(); +} + +/** + * Generate a styleText replacement for a single style + */ +function createStyleTextReplacement( + styleMethod: string, + textArg: string, +): string { + return `styleText("${styleMethod}", ${textArg})`; +} + +/** + * Generate a styleText replacement for multiple styles + */ +function createMultiStyleTextReplacement( + styles: string[], + textArg: string, +): string { + if (styles.length === 1) { + return createStyleTextReplacement(styles[0], textArg); + } + + const stylesArray = `[${styles.map((s) => `"${s}"`).join(', ')}]`; + + return `styleText(${stylesArray}, ${textArg})`; +} + +/** + * Process destructured imports and transform direct function calls + */ +function processDestructuredImports( + rootNode: SgNode, + destructuredNames: Array<{ imported: string; local: string }>, + edits: Edit[], +): void { + for (const name of destructuredNames) { + const directCalls = rootNode.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'identifier', + pattern: name.local, + }, + }, + }); + + for (const call of directCalls) { + if (!isSupportedMethod(name.imported)) { + warnOnUnsupportedMethod(name.imported, rootNode, call); + continue; + } + + const textArg = getFirstCallArgument(call); + + if (!textArg) continue; + + const styleMethod = COMPAT_MAP[name.imported] || name.imported; + const replacement = createStyleTextReplacement(styleMethod, textArg); + + edits.push(call.replace(replacement)); + } + } +} + +/** + * Process default imports and transform member expression calls + */ +function processDefaultImports( + rootNode: SgNode, + binding: string, + edits: Edit[], +): void { + const chalkCalls = rootNode.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'member_expression', + has: { + field: 'object', + any: [ + // Direct chalk calls + { + kind: 'identifier', + pattern: binding, + }, + // Chained chalk calls + { + kind: 'member_expression', + any: [ + { + has: { + field: 'object', + kind: 'identifier', + pattern: binding, // chalk.method1.method2 + }, + }, + { + has: { + field: 'object', + kind: 'member_expression', + has: { + field: 'object', + kind: 'identifier', + pattern: binding, // chalk.method1.method2.method3 + }, + }, + }, + ], + }, + ], + }, + }, + }, + }); + + for (const call of chalkCalls) { + const functionExpr = call.field('function'); + + if (!functionExpr) continue; + + const styles = extractChalkStyles(functionExpr, binding); + + if (styles.length === 0) continue; + + if (hasUnsupportedMethods(styles)) { + for (const style of styles) { + if (!SUPPORTED_METHODS.has(style)) { + warnOnUnsupportedMethod(style, rootNode, call); + } + } + continue; + } + + const textArg = getFirstCallArgument(call); + + if (!textArg) continue; + + const replacement = createMultiStyleTextReplacement(styles, textArg); + + edits.push(call.replace(replacement)); + } + + // Handle method assignments + // const red = chalk.red; → const red = (text) => styleText("red", text); + const methodAssignments = rootNode.findAll({ + rule: { + kind: 'variable_declarator', + has: { + field: 'value', + any: [{ kind: 'member_expression' }, { kind: 'ternary_expression' }], + }, + }, + }); + + for (const assignment of methodAssignments) { + const valueExpr = assignment.field('value'); + if (!valueExpr) continue; + + const nameField = assignment.field('name'); + if (!nameField) continue; + + const variableName = nameField.text(); + + if (valueExpr.kind() === 'member_expression') { + // Direct assignment: const red = chalk.red; + const replacement = createMemberExpressionAssignment( + valueExpr, + variableName, + binding, + ); + if (replacement) { + edits.push(assignment.replace(replacement)); + } + } else if (valueExpr.kind() === 'ternary_expression') { + // Conditional assignment: const c = b ? chalk.bold : chalk.underline; + const replacement = createTernaryExpressionAssignment( + valueExpr, + variableName, + binding, + ); + if (replacement) { + edits.push(assignment.replace(replacement)); + } + } + } +} + +/** + * Replace import/require statement with node:util import + */ +function createImportReplacement(statement: SgNode): string { + if (statement.kind() === 'import_statement') { + return `import { styleText } from "node:util";`; + } + + if (statement.kind() === 'variable_declarator') { + // Handle dynamic ESM import + if (statement.field('value')?.kind() === 'await_expression') { + return `{ styleText } = await import("node:util")`; + } + // Handle CommonJS require + return `{ styleText } = require("node:util")`; + } + + return ''; +} + +/** + * Traverses a member expression node to extract chained chalk styles. + * and returns a list of styles in the order they were called. + */ +function extractChalkStyles(node: SgNode, chalkBinding: string): string[] { + const styles: string[] = []; + + function traverse(node: SgNode): boolean { + const obj = node.field('object'); + const prop = node.field('property'); + + if (obj && prop && prop.kind() === 'property_identifier') { + const propName = prop.text(); + + if (obj.kind() === 'identifier' && obj.text() === chalkBinding) { + // Base case: chalk.method + styles.push(COMPAT_MAP[propName] || propName); + + return true; + } + + if (obj.kind() === 'member_expression' && traverse(obj)) { + // Recursive case: chain.method + styles.push(COMPAT_MAP[propName] || propName); + + return true; + } + } + + return false; + } + + traverse(node); + + return styles; +} + +/** + * Create a wrapper function for a chalk member expression assignment + */ +function createMemberExpressionAssignment( + valueExpr: SgNode, + variableName: string, + binding: string, +): string | null { + const styles = extractChalkStyles(valueExpr, binding); + + if (styles.length === 0) { + return null; + } + + if (hasUnsupportedMethods(styles)) { + return null; + } + + const styleTextCall = createMultiStyleTextReplacement(styles, 'text'); + const wrapperFunction = `(text) => ${styleTextCall}`; + + return `${variableName} = ${wrapperFunction}`; +} + +/** + * Create wrapper functions for a ternary expression assignment with chalk expressions + */ +function createTernaryExpressionAssignment( + valueExpr: SgNode, + variableName: string, + binding: string, +): string | null { + const condition = valueExpr.field('condition'); + const consequent = valueExpr.field('consequence'); + const alternative = valueExpr.field('alternative'); + + if (!condition || !consequent || !alternative) { + return null; + } + + // Extract styles from both sides if they are member expressions + if ( + consequent.kind() !== 'member_expression' && + alternative.kind() !== 'member_expression' + ) { + return null; + } + + const consequentStyles = extractChalkStyles(consequent, binding); + const alternativeStyles = extractChalkStyles(alternative, binding); + + // Only transform if both sides are chalk expressions + if (consequentStyles.length === 0 || alternativeStyles.length === 0) { + return null; + } + + if (hasUnsupportedMethods([...consequentStyles, ...alternativeStyles])) { + return null; + } + + const consequentCall = createMultiStyleTextReplacement( + consequentStyles, + 'text', + ); + const alternativeCall = createMultiStyleTextReplacement( + alternativeStyles, + 'text', + ); + const conditionText = condition.text(); + + return `${variableName} = ${conditionText} ? (text) => ${consequentCall} : (text) => ${alternativeCall}`; +} + +/** + * Utility to warn the user about unsupported chalk methods. + */ +function warnOnUnsupportedMethod( + method: string, + rootNode: SgNode, + node: SgNode, +) { + const filename = rootNode.getRoot().filename(); + const { start } = node.range(); + + console.warn( + `${filename}:${start.line}:${start.column}: uses chalk method '${method}' that does not have any equivalent in util.styleText please review this line`, + ); +} diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_advanced_modifiers.js b/recipes/chalk-to-util-styletext/tests/expected/tests_advanced_modifiers.js new file mode 100644 index 00000000..1fb5dcf8 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_advanced_modifiers.js @@ -0,0 +1,15 @@ +import { styleText } from "node:util"; + +// Some of these have limited terminal support - may not work in all environments +console.log(styleText("bold", "Bold text")); +console.log(styleText("blink", "Blinking text")); +console.log(styleText("dim", "Dimmed text")); +console.log(styleText("doubleunderline", "Double underlined")); +console.log(styleText("framed", "Framed text")); +console.log(styleText("italic", "Italic text")); +console.log(styleText("inverse", "Inverted colors")); +console.log(styleText("hidden", "Hidden text")); +console.log(styleText("overlined", "Overlined text")); +console.log(styleText("reset", "Reset text")); +console.log(styleText("strikethrough", "Strikethrough text")); +console.log(styleText("underline", "Underlined text")); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_background.js b/recipes/chalk-to-util-styletext/tests/expected/tests_background.js new file mode 100644 index 00000000..899b4c29 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_background.js @@ -0,0 +1,5 @@ +import { styleText } from "node:util"; + +console.log(styleText(["bgRed", "white"], "Error on red background")); +console.log(styleText(["bgGreen", "black"], "Success on green background")); +console.log(styleText(["bgBlue", "whiteBright"], "Info on blue background")); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_basic.js b/recipes/chalk-to-util-styletext/tests/expected/tests_basic.js new file mode 100644 index 00000000..8bffa6fa --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_basic.js @@ -0,0 +1,5 @@ +import { styleText } from "node:util"; + +console.log(styleText("red", "Error message")); +console.log(styleText("green", "Success message")); +console.log(styleText("blue", "Info message")); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_chained.js b/recipes/chalk-to-util-styletext/tests/expected/tests_chained.js new file mode 100644 index 00000000..a63ba0fe --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_chained.js @@ -0,0 +1,5 @@ +import { styleText } from "node:util"; + +console.log(styleText(["red", "bold"], "Error: Operation failed")); +console.log(styleText(["green", "underline"], "Success: All tests passed")); +console.log(styleText(["yellow", "bgBlack"], "Warning: Deprecated API usage")); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_commonjs_require.js b/recipes/chalk-to-util-styletext/tests/expected/tests_commonjs_require.js new file mode 100644 index 00000000..f2c2e6a3 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_commonjs_require.js @@ -0,0 +1,7 @@ +const { styleText } = require("node:util"); + +const error = styleText("red", "Error"); +const warning = styleText("yellow", "Warning"); +const info = styleText("blue", "Info"); + +console.log(error, warning, info); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_commonjs_require_destructure_reassign.js b/recipes/chalk-to-util-styletext/tests/expected/tests_commonjs_require_destructure_reassign.js new file mode 100644 index 00000000..999364b7 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_commonjs_require_destructure_reassign.js @@ -0,0 +1,6 @@ +const { styleText } = require("node:util"); + +const error = styleText("red", "Error"); +const warning = styleText("yellow", "Warning message"); + +console.log(error, warning); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_commonjs_require_named.js b/recipes/chalk-to-util-styletext/tests/expected/tests_commonjs_require_named.js new file mode 100644 index 00000000..999364b7 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_commonjs_require_named.js @@ -0,0 +1,6 @@ +const { styleText } = require("node:util"); + +const error = styleText("red", "Error"); +const warning = styleText("yellow", "Warning message"); + +console.log(error, warning); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_complex_chaining.js b/recipes/chalk-to-util-styletext/tests/expected/tests_complex_chaining.js new file mode 100644 index 00000000..c6666e9e --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_complex_chaining.js @@ -0,0 +1,6 @@ +const { styleText } = require("node:util"); + +const b = true; +const c = b ? (text) => styleText("bold", text) : (text) => styleText("underline", text); + +console.log(c("Conditional style")); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_dynamic_import.js b/recipes/chalk-to-util-styletext/tests/expected/tests_dynamic_import.js new file mode 100644 index 00000000..24960903 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_dynamic_import.js @@ -0,0 +1,9 @@ +// CommonJS style dynamic import +async function testCommonJS() { + const { styleText } = await import("node:util"); + console.log(styleText("bgBlue", "This is a message")); +} + +// ESM style dynamic import +const { styleText } = await import("node:util"); +console.log(styleText("bgRed", "This is a message")); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_dynamic_import_custom_variable.js b/recipes/chalk-to-util-styletext/tests/expected/tests_dynamic_import_custom_variable.js new file mode 100644 index 00000000..0b782a11 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_dynamic_import_custom_variable.js @@ -0,0 +1,6 @@ +const { styleText } = await import("node:util"); + +const error = styleText("red", "Error"); +const warning = styleText("yellow", "Warning message"); + +console.log(error, warning); \ No newline at end of file diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_dynamic_import_destructured.js b/recipes/chalk-to-util-styletext/tests/expected/tests_dynamic_import_destructured.js new file mode 100644 index 00000000..03894ae4 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_dynamic_import_destructured.js @@ -0,0 +1,6 @@ +const { styleText } = await import("node:util"); + +const error = styleText("red", "Error"); +const warning = styleText("yellow", "Warning message"); + +console.log(error, warning); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_esm_import_all.js b/recipes/chalk-to-util-styletext/tests/expected/tests_esm_import_all.js new file mode 100644 index 00000000..a2656ff0 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_esm_import_all.js @@ -0,0 +1,12 @@ +import { styleText } from "node:util"; + +function logError(message) { + console.log(styleText(["red", "bold"], `ERROR: ${message}`)); +} + +function logSuccess(message) { + console.log(styleText("green", `SUCCESS: ${message}`)); +} + +logError("Something went wrong"); +logSuccess("Operation completed"); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_esm_import_named.js b/recipes/chalk-to-util-styletext/tests/expected/tests_esm_import_named.js new file mode 100644 index 00000000..bca36b1d --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_esm_import_named.js @@ -0,0 +1,6 @@ +import { styleText } from "node:util"; + +const error = styleText("red", "Error"); +const warning = styleText("yellow", "Warning message"); + +console.log(error, warning); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_method_assignments.js b/recipes/chalk-to-util-styletext/tests/expected/tests_method_assignments.js new file mode 100644 index 00000000..a503fb25 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_method_assignments.js @@ -0,0 +1,28 @@ +const { styleText } = require("node:util"); + +// Method assignments to variables +const red = (text) => styleText("red", text); +const green = (text) => styleText("green", text); +const yellow = (text) => styleText("yellow", text); + +// Using assigned methods +console.log(red("This is red")); +console.log(green("This is green")); + +// Method assignments with chaining +const bold = (text) => styleText("bold", text); +const underline = (text) => styleText("underline", text); +console.log(bold("Bold text")); + +// Complex assignments +const redBold = (text) => styleText(["red", "bold"], text); +console.log(redBold("Red and bold")); + +// Assignment in different scopes +function setupColors() { + const blue = (text) => styleText("blue", text); + return blue; +} + +const blueFunc = setupColors(); +console.log(blueFunc("Blue text")); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_mixed_import.js b/recipes/chalk-to-util-styletext/tests/expected/tests_mixed_import.js new file mode 100644 index 00000000..b3d6b286 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_mixed_import.js @@ -0,0 +1,13 @@ +import { styleText } from "node:util"; +import { otherFunction } from "./utils"; + +function logError(message) { + console.log(styleText(["red", "bold"], `ERROR: ${message}`)); +} + +function logSuccess(message) { + console.log(styleText("green", `SUCCESS: ${message}`)); +} + +logError("Something went wrong"); +logSuccess("Operation completed"); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_modifiers.js b/recipes/chalk-to-util-styletext/tests/expected/tests_modifiers.js new file mode 100644 index 00000000..e48b1dd1 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_modifiers.js @@ -0,0 +1,5 @@ +import { styleText } from "node:util"; + +console.log(styleText(["bold", "italic", "underline"], "Important announcement")); +console.log(styleText(["dim", "strikethrough"], "Deprecated feature")); +console.log(styleText("inverse", "Inverted colors")); diff --git a/recipes/chalk-to-util-styletext/tests/expected/tests_unsupported_features.js b/recipes/chalk-to-util-styletext/tests/expected/tests_unsupported_features.js new file mode 100644 index 00000000..388dc44c --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/expected/tests_unsupported_features.js @@ -0,0 +1,34 @@ +// Test file for unsupported chalk features +const { styleText } = require("node:util"); + +// 1. chalk.hex - should be left unchanged (unsupported) +console.log(chalk.hex('#DEADED').bold('Custom hex color')); +console.log(chalk.hex('#FF5733')('Another hex color')); + +// 2. chalk.rgb - should be left unchanged (unsupported) +console.log(chalk.rgb(255, 136, 0).bold('RGB orange color')); +console.log(chalk.rgb(100, 200, 50)('RGB green color')); + +// 3. chalk.ansi256 - should be left unchanged (unsupported) +console.log(chalk.ansi256(194)('ANSI 256 color')); +console.log(chalk.bgAnsi256(45).white('Background ANSI color')); + +// 4. new Chalk() - should be left unchanged (unsupported) +const customChalk = new chalk.Chalk({ level: 2 }); +console.log(customChalk.red('Custom chalk instance')); + +// 5. chalk.level - property access should be ignored +if (chalk.level > 0) { + console.log(styleText("green", 'Colors are supported')); +} + +// 6. chalk.visible - should handle visibility logic +console.log(chalk.visible('This text may not be visible')); +console.log(chalk.red.visible('Red text that may not be visible')); + +// Mixed cases that should still work +console.log(styleText("red", 'This should be transformed')); +console.log(styleText(["bold", "blue"], 'This should also be transformed')); + +// Edge case: method chaining with unsupported methods +console.log(chalk.red.hex('#FF0000')('Mixed supported and unsupported')); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_advanced_modifiers.js b/recipes/chalk-to-util-styletext/tests/input/tests_advanced_modifiers.js new file mode 100644 index 00000000..e46cd3fd --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_advanced_modifiers.js @@ -0,0 +1,15 @@ +import chalk from "chalk"; + +// Some of these have limited terminal support - may not work in all environments +console.log(chalk.bold("Bold text")); +console.log(chalk.blink("Blinking text")); +console.log(chalk.dim("Dimmed text")); +console.log(chalk.doubleunderline("Double underlined")); +console.log(chalk.framed("Framed text")); +console.log(chalk.italic("Italic text")); +console.log(chalk.inverse("Inverted colors")); +console.log(chalk.hidden("Hidden text")); +console.log(chalk.overline("Overlined text")); +console.log(chalk.reset("Reset text")); +console.log(chalk.strikethrough("Strikethrough text")); +console.log(chalk.underline("Underlined text")); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_background.js b/recipes/chalk-to-util-styletext/tests/input/tests_background.js new file mode 100644 index 00000000..78a0f203 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_background.js @@ -0,0 +1,5 @@ +import chalk from "chalk"; + +console.log(chalk.bgRed.white("Error on red background")); +console.log(chalk.bgGreen.black("Success on green background")); +console.log(chalk.bgBlue.whiteBright("Info on blue background")); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_basic.js b/recipes/chalk-to-util-styletext/tests/input/tests_basic.js new file mode 100644 index 00000000..d229d3bd --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_basic.js @@ -0,0 +1,5 @@ +import chalk from "chalk"; + +console.log(chalk.red("Error message")); +console.log(chalk.green("Success message")); +console.log(chalk.blue("Info message")); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_chained.js b/recipes/chalk-to-util-styletext/tests/input/tests_chained.js new file mode 100644 index 00000000..23d7af98 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_chained.js @@ -0,0 +1,5 @@ +import chalk from "chalk"; + +console.log(chalk.red.bold("Error: Operation failed")); +console.log(chalk.green.underline("Success: All tests passed")); +console.log(chalk.yellow.bgBlack("Warning: Deprecated API usage")); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_commonjs_require.js b/recipes/chalk-to-util-styletext/tests/input/tests_commonjs_require.js new file mode 100644 index 00000000..d0b1385d --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_commonjs_require.js @@ -0,0 +1,7 @@ +const chalk = require("chalk"); + +const error = chalk.red("Error"); +const warning = chalk.yellow("Warning"); +const info = chalk.blue("Info"); + +console.log(error, warning, info); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_commonjs_require_destructure_reassign.js b/recipes/chalk-to-util-styletext/tests/input/tests_commonjs_require_destructure_reassign.js new file mode 100644 index 00000000..f00e1d11 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_commonjs_require_destructure_reassign.js @@ -0,0 +1,6 @@ +const { red: foo, yellow: bar } = require("chalk"); + +const error = foo("Error"); +const warning = bar("Warning message"); + +console.log(error, warning); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_commonjs_require_named.js b/recipes/chalk-to-util-styletext/tests/input/tests_commonjs_require_named.js new file mode 100644 index 00000000..220a23c0 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_commonjs_require_named.js @@ -0,0 +1,6 @@ +const { red, yellow } = require("chalk"); + +const error = red("Error"); +const warning = yellow("Warning message"); + +console.log(error, warning); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_complex_chaining.js b/recipes/chalk-to-util-styletext/tests/input/tests_complex_chaining.js new file mode 100644 index 00000000..baa63ac5 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_complex_chaining.js @@ -0,0 +1,6 @@ +const chalk = require("chalk"); + +const b = true; +const c = b ? chalk.bold : chalk.underline; + +console.log(c("Conditional style")); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_dynamic_import.js b/recipes/chalk-to-util-styletext/tests/input/tests_dynamic_import.js new file mode 100644 index 00000000..f1385d98 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_dynamic_import.js @@ -0,0 +1,9 @@ +// CommonJS style dynamic import +async function testCommonJS() { + const chalk = await import("chalk"); + console.log(chalk.bgBlue("This is a message")); +} + +// ESM style dynamic import +const chalk = await import("chalk"); +console.log(chalk.bgRed("This is a message")); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_dynamic_import_custom_variable.js b/recipes/chalk-to-util-styletext/tests/input/tests_dynamic_import_custom_variable.js new file mode 100644 index 00000000..0febd420 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_dynamic_import_custom_variable.js @@ -0,0 +1,6 @@ +const foo = await import('chalk'); + +const error = foo.red("Error"); +const warning = foo.yellow("Warning message"); + +console.log(error, warning); \ No newline at end of file diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_dynamic_import_destructured.js b/recipes/chalk-to-util-styletext/tests/input/tests_dynamic_import_destructured.js new file mode 100644 index 00000000..06df0456 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_dynamic_import_destructured.js @@ -0,0 +1,6 @@ +const { red, yellow } = await import('chalk'); + +const error = red("Error"); +const warning = yellow("Warning message"); + +console.log(error, warning); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_esm_import_all.js b/recipes/chalk-to-util-styletext/tests/input/tests_esm_import_all.js new file mode 100644 index 00000000..7db5df21 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_esm_import_all.js @@ -0,0 +1,12 @@ +import * as c from "chalk"; + +function logError(message) { + console.log(c.red.bold(`ERROR: ${message}`)); +} + +function logSuccess(message) { + console.log(c.green(`SUCCESS: ${message}`)); +} + +logError("Something went wrong"); +logSuccess("Operation completed"); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_esm_import_named.js b/recipes/chalk-to-util-styletext/tests/input/tests_esm_import_named.js new file mode 100644 index 00000000..ce101fe3 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_esm_import_named.js @@ -0,0 +1,6 @@ +import { red, yellow as y } from "chalk"; + +const error = red("Error"); +const warning = y("Warning message"); + +console.log(error, warning); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_method_assignments.js b/recipes/chalk-to-util-styletext/tests/input/tests_method_assignments.js new file mode 100644 index 00000000..d2fe1a25 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_method_assignments.js @@ -0,0 +1,28 @@ +const chalk = require("chalk"); + +// Method assignments to variables +const red = chalk.red; +const green = chalk.green; +const yellow = chalk.yellow; + +// Using assigned methods +console.log(red("This is red")); +console.log(green("This is green")); + +// Method assignments with chaining +const bold = chalk.bold; +const underline = chalk.underline; +console.log(bold("Bold text")); + +// Complex assignments +const redBold = chalk.red.bold; +console.log(redBold("Red and bold")); + +// Assignment in different scopes +function setupColors() { + const blue = chalk.blue; + return blue; +} + +const blueFunc = setupColors(); +console.log(blueFunc("Blue text")); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_mixed_import.js b/recipes/chalk-to-util-styletext/tests/input/tests_mixed_import.js new file mode 100644 index 00000000..1742ed81 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_mixed_import.js @@ -0,0 +1,13 @@ +import chalk from "chalk"; +import { otherFunction } from "./utils"; + +function logError(message) { + console.log(chalk.red.bold(`ERROR: ${message}`)); +} + +function logSuccess(message) { + console.log(chalk.green(`SUCCESS: ${message}`)); +} + +logError("Something went wrong"); +logSuccess("Operation completed"); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_modifiers.js b/recipes/chalk-to-util-styletext/tests/input/tests_modifiers.js new file mode 100644 index 00000000..85008599 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_modifiers.js @@ -0,0 +1,5 @@ +import chalk from "chalk"; + +console.log(chalk.bold.italic.underline("Important announcement")); +console.log(chalk.dim.strikethrough("Deprecated feature")); +console.log(chalk.inverse("Inverted colors")); diff --git a/recipes/chalk-to-util-styletext/tests/input/tests_unsupported_features.js b/recipes/chalk-to-util-styletext/tests/input/tests_unsupported_features.js new file mode 100644 index 00000000..c0d4d602 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_unsupported_features.js @@ -0,0 +1,34 @@ +// Test file for unsupported chalk features +const chalk = require('chalk'); + +// 1. chalk.hex - should be left unchanged (unsupported) +console.log(chalk.hex('#DEADED').bold('Custom hex color')); +console.log(chalk.hex('#FF5733')('Another hex color')); + +// 2. chalk.rgb - should be left unchanged (unsupported) +console.log(chalk.rgb(255, 136, 0).bold('RGB orange color')); +console.log(chalk.rgb(100, 200, 50)('RGB green color')); + +// 3. chalk.ansi256 - should be left unchanged (unsupported) +console.log(chalk.ansi256(194)('ANSI 256 color')); +console.log(chalk.bgAnsi256(45).white('Background ANSI color')); + +// 4. new Chalk() - should be left unchanged (unsupported) +const customChalk = new chalk.Chalk({ level: 2 }); +console.log(customChalk.red('Custom chalk instance')); + +// 5. chalk.level - property access should be ignored +if (chalk.level > 0) { + console.log(chalk.green('Colors are supported')); +} + +// 6. chalk.visible - should handle visibility logic +console.log(chalk.visible('This text may not be visible')); +console.log(chalk.red.visible('Red text that may not be visible')); + +// Mixed cases that should still work +console.log(chalk.red('This should be transformed')); +console.log(chalk.bold.blue('This should also be transformed')); + +// Edge case: method chaining with unsupported methods +console.log(chalk.red.hex('#FF0000')('Mixed supported and unsupported')); diff --git a/recipes/chalk-to-util-styletext/workflow.yaml b/recipes/chalk-to-util-styletext/workflow.yaml new file mode 100644 index 00000000..fa2c7ad0 --- /dev/null +++ b/recipes/chalk-to-util-styletext/workflow.yaml @@ -0,0 +1,42 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: Migrate from the chalk package to Node.js's built-in util.styleText API + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.cts" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript + + - id: remove-dependencies + name: Remove chalk dependency + type: automatic + steps: + - name: Detect package manager and remove chalk dependency + js-ast-grep: + js_file: src/remove-dependencies.ts + base_path: . + include: + - "**/package.json" + exclude: + - "**/node_modules/**" + language: typescript + capabilities: + - child_process + - fs diff --git a/utils/src/remove-dependencies.test.ts b/utils/src/remove-dependencies.test.ts new file mode 100644 index 00000000..5f2dc7c7 --- /dev/null +++ b/utils/src/remove-dependencies.test.ts @@ -0,0 +1,164 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, writeFile, rm } from 'node:fs/promises'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { describe, it } from 'node:test'; + +import removeDependencies from './remove-dependencies.ts'; + +describe('removeDependencies', () => { + it('should return null when no dependencies are specified', () => { + const result = removeDependencies(); + assert.strictEqual(result, null); + }); + + it('should return null when empty array is provided', () => { + const result = removeDependencies([]); + assert.strictEqual(result, null); + }); + + it('should return null when package.json does not exist in current directory', async () => { + const tempDir = await mkdtemp(join(tmpdir(), 'remove-deps-test-')); + const originalCwd = process.cwd(); + + try { + process.chdir(tempDir); + const result = removeDependencies('chalk'); + assert.strictEqual(result, null); + } finally { + process.chdir(originalCwd); + await rm(tempDir, { recursive: true }); + } + }); + + it('should handle package.json with no dependencies sections', async () => { + const tempDir = await mkdtemp(join(tmpdir(), 'remove-deps-test-')); + const originalCwd = process.cwd(); + + const packageJsonContent = { + name: 'test-package', + version: '1.0.0', + }; + + try { + process.chdir(tempDir); + await writeFile( + 'package.json', + JSON.stringify(packageJsonContent, null, 2), + ); + + const result = removeDependencies('chalk'); + + assert.strictEqual(result, null); + } finally { + process.chdir(originalCwd); + await rm(tempDir, { recursive: true }); + } + }); + + it('should remove a single dependency from dependencies', async () => { + const tempDir = await mkdtemp(join(tmpdir(), 'remove-deps-test-')); + const originalCwd = process.cwd(); + + const packageJsonContent = { + name: 'test-package', + version: '1.0.0', + dependencies: { + chalk: '^4.0.0', + lodash: '^4.17.21', + }, + }; + + try { + process.chdir(tempDir); + await writeFile( + 'package.json', + JSON.stringify(packageJsonContent, null, 2), + ); + + removeDependencies('chalk'); + + const updatedContent = readFileSync('package.json', 'utf-8'); + const updatedPackageJson = JSON.parse(updatedContent); + + assert.strictEqual(updatedPackageJson.dependencies.chalk, undefined); + assert.strictEqual(updatedPackageJson.dependencies.lodash, '^4.17.21'); + } finally { + process.chdir(originalCwd); + await rm(tempDir, { recursive: true }); + } + }); + + it('should remove multiple dependencies from both dependencies and devDependencies', async () => { + const tempDir = await mkdtemp(join(tmpdir(), 'remove-deps-test-')); + const originalCwd = process.cwd(); + + const packageJsonContent = { + name: 'test-package', + version: '1.0.0', + dependencies: { + chalk: '^4.0.0', + lodash: '^4.17.21', + }, + devDependencies: { + jest: '^29.0.0', + typescript: '^5.0.0', + chalk: '^4.0.0', + }, + }; + + try { + process.chdir(tempDir); + await writeFile( + 'package.json', + JSON.stringify(packageJsonContent, null, 2), + ); + + removeDependencies(['chalk', 'jest']); + + const updatedContent = readFileSync('package.json', 'utf-8'); + const updatedPackageJson = JSON.parse(updatedContent); + + assert.strictEqual(updatedPackageJson.dependencies.chalk, undefined); + assert.strictEqual(updatedPackageJson.dependencies.lodash, '^4.17.21'); + assert.strictEqual(updatedPackageJson.devDependencies.chalk, undefined); + assert.strictEqual(updatedPackageJson.devDependencies.jest, undefined); + assert.strictEqual( + updatedPackageJson.devDependencies.typescript, + '^5.0.0', + ); + } finally { + process.chdir(originalCwd); + await rm(tempDir, { recursive: true }); + } + }); + + it('should return null when no specified dependencies are found', async () => { + const tempDir = await mkdtemp(join(tmpdir(), 'remove-deps-test-')); + const originalCwd = process.cwd(); + + const packageJsonContent = { + name: 'test-package', + version: '1.0.0', + dependencies: { + lodash: '^4.17.21', + }, + }; + + try { + process.chdir(tempDir); + await writeFile( + 'package.json', + JSON.stringify(packageJsonContent, null, 2), + ); + + const result = removeDependencies(['chalk', 'jest']); + + assert.strictEqual(result, null); + } finally { + process.chdir(originalCwd); + await rm(tempDir, { recursive: true }); + } + }); +}); diff --git a/utils/src/remove-dependencies.ts b/utils/src/remove-dependencies.ts new file mode 100644 index 00000000..602b2747 --- /dev/null +++ b/utils/src/remove-dependencies.ts @@ -0,0 +1,114 @@ +import { execSync } from 'node:child_process'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; + +/** + * Remove specified dependencies from package.json and run appropriate package manager install + */ +export default function removeDependencies( + dependenciesToRemove?: string | string[], +): string | null { + const packageJsonPath = 'package.json'; + + if (!dependenciesToRemove) { + console.log('No dependencies specified for removal'); + return null; + } + + if (!existsSync(packageJsonPath)) { + console.log('No package.json found, skipping dependency removal'); + return null; + } + + try { + const depsToRemove = Array.isArray(dependenciesToRemove) + ? dependenciesToRemove + : [dependenciesToRemove]; + + const packageJsonContent = readFileSync(packageJsonPath, 'utf-8'); + const packageJson = JSON.parse(packageJsonContent); + + let modified = false; + + if (packageJson.dependencies) { + for (const dep of depsToRemove) { + if (packageJson.dependencies[dep]) { + delete packageJson.dependencies[dep]; + modified = true; + console.log(`Removed ${dep} from dependencies`); + } + } + } + + if (packageJson.devDependencies) { + for (const dep of depsToRemove) { + if (packageJson.devDependencies[dep]) { + delete packageJson.devDependencies[dep]; + modified = true; + console.log(`Removed ${dep} from devDependencies`); + } + } + } + + if (!modified) { + console.log( + `No specified dependencies (${depsToRemove.join(', ')}) found in package.json`, + ); + return null; + } + + const updatedContent = JSON.stringify(packageJson, null, 2); + writeFileSync(packageJsonPath, updatedContent, 'utf-8'); + console.log('Updated package.json'); + + const packageManager = detectPackageManager(); + runPackageManagerInstall(packageManager); + + return updatedContent; + } catch (error) { + console.error('Error removing dependencies:', error); + return null; + } +} + +/** + * Detect which package manager is being used based on lock files. Defaults to npm. + */ +function detectPackageManager(): 'npm' | 'yarn' | 'pnpm' { + if (existsSync('pnpm-lock.yaml')) { + return 'pnpm'; + } + + if (existsSync('yarn.lock')) { + return 'yarn'; + } + + return 'npm'; +} + +/** + * Run the appropriate package manager install command + */ +function runPackageManagerInstall( + packageManager: 'npm' | 'yarn' | 'pnpm', +): void { + try { + console.log(`Running ${packageManager} install to update dependencies...`); + + switch (packageManager) { + case 'npm': + execSync('npm install', { stdio: 'inherit' }); + break; + case 'yarn': + execSync('yarn install', { stdio: 'inherit' }); + break; + case 'pnpm': + execSync('pnpm install', { stdio: 'inherit' }); + break; + } + + console.log(`Successfully updated dependencies with ${packageManager}`); + } catch (error) { + console.error(`Error running ${packageManager} install:`, error); + // Don't throw - dependency removal was successful, install failure shouldn't break the codemod + } +}