diff --git a/package-lock.json b/package-lock.json index b73899e5..668e5c4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -415,6 +415,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1457,6 +1458,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 @@ -1551,6 +1556,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -2057,6 +2063,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001733", "electron-to-chromium": "^1.5.199", @@ -4260,6 +4267,17 @@ "@codemod.com/jssg-types": "^1.0.9" } }, + "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..4a7436cb --- /dev/null +++ b/recipes/chalk-to-util-styletext/README.md @@ -0,0 +1,3 @@ +# chalk-to-util-styletext + +TODO diff --git a/recipes/chalk-to-util-styletext/codemod.yaml b/recipes/chalk-to-util-styletext/codemod.yaml new file mode 100644 index 00000000..71619a69 --- /dev/null +++ b/recipes/chalk-to-util-styletext/codemod.yaml @@ -0,0 +1,21 @@ +schema_version: "1.0" +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 +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/workflow.ts b/recipes/chalk-to-util-styletext/src/workflow.ts new file mode 100644 index 00000000..eb1bbcec --- /dev/null +++ b/recipes/chalk-to-util-styletext/src/workflow.ts @@ -0,0 +1,364 @@ +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 not supported by util.styleText +const UNSUPPORTED_METHODS = new Set(["hex", "rgb", "ansi256", "bgAnsi256", "visible"]); + +/** + * Check if a method name is supported by util.styleText + */ +function isSupportedMethod(method: string): boolean { + return !UNSUPPORTED_METHODS.has(method); +} + +/** + * Check if a style chain contains any unsupported methods + */ +function hasUnsupportedMethods(styles: string[]): boolean { + return styles.some((style) => UNSUPPORTED_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", + regex: `^${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: { + any: [ + // Pattern 1: single method calls: chalk.method(text) + { pattern: `${binding}.$METHOD($TEXT)` }, + // Pattern 2: chained method calls: chalk.method1.method2(text)); + { + kind: "call_expression", + has: { + field: "function", + kind: "member_expression", + }, + }, + ], + }, + }); + + for (const call of chalkCalls) { + const methodMatch = call.getMatch("METHOD"); + const textMatch = call.getMatch("TEXT"); + + if (methodMatch && textMatch) { + // Pattern 1: chalk.method(text) → styleText("method", text) + const method = methodMatch.text(); + + if (!isSupportedMethod(method)) { + warnOnUnsupportedMethod(method, rootNode, call); + continue; + } + + const text = textMatch.text(); + const styleMethod = COMPAT_MAP[method] || method; + const replacement = createStyleTextReplacement(styleMethod, text); + + edits.push(call.replace(replacement)); + } else { + // Pattern 2: chalk.method1.method2(text) → styleText(["method1", "method2"], text) + const functionExpr = call.field("function"); + + if (!functionExpr) continue; + + const styles = extractChalkStyles(functionExpr, binding); + + if (styles.length === 0) continue; + + if (hasUnsupportedMethods(styles)) continue; + + const textArg = getFirstCallArgument(call); + + if (!textArg) continue; + + const replacement = createMultiStyleTextReplacement(styles, textArg); + + edits.push(call.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; +} + +/** + * 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_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_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_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_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_named.js b/recipes/chalk-to-util-styletext/tests/input/tests_esm_import_named.js new file mode 100644 index 00000000..10d8b283 --- /dev/null +++ b/recipes/chalk-to-util-styletext/tests/input/tests_esm_import_named.js @@ -0,0 +1,6 @@ +import { red, yellow } from '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_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/tsconfig.json b/recipes/chalk-to-util-styletext/tsconfig.json new file mode 100644 index 00000000..8981fe3f --- /dev/null +++ b/recipes/chalk-to-util-styletext/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "allowJs": true, + "alwaysStrict": true, + "baseUrl": "./", + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "lib": ["ESNext", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noImplicitThis": true, + "removeComments": true, + "strict": true, + "stripInternal": true, + "target": "esnext" + }, + "include": ["./"], + "exclude": ["tests/**"] +} diff --git a/recipes/chalk-to-util-styletext/workflow.yaml b/recipes/chalk-to-util-styletext/workflow.yaml new file mode 100644 index 00000000..a76208c2 --- /dev/null +++ b/recipes/chalk-to-util-styletext/workflow.yaml @@ -0,0 +1,25 @@ +# 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