diff --git a/recipes/kleur-to-util-styletext/README.md b/recipes/kleur-to-util-styletext/README.md new file mode 100644 index 00000000..c02a26c5 --- /dev/null +++ b/recipes/kleur-to-util-styletext/README.md @@ -0,0 +1,57 @@ +# kleur to util.styleText + +This recipe migrates from the external `kleur` package to Node.js built-in `util.styleText` API. It transforms kleur method calls to use the native Node.js styling functionality. + +## Examples + +```diff +- import kleur from 'kleur'; ++ import { styleText } from 'node:util'; +- console.log(kleur.red('Error message')); ++ console.log(styleText('red', 'Error message')); +- console.log(kleur.green('Success message')); ++ console.log(styleText('green', 'Success message')); +- console.log(kleur.blue('Info message')); ++ console.log(styleText('blue', 'Info message')); +``` + +```diff +- import kleur from 'kleur'; ++ import { styleText } from 'node:util'; +- console.log(kleur.red.bold('Important error')); ++ console.log(styleText(['red', 'bold'], 'Important error')); +- console.log(kleur.green.underline('Success with emphasis')); ++ console.log(styleText(['green', 'underline'], 'Success with emphasis')); +``` + +```diff +- const kleur = require('kleur'); ++ const { styleText } = require('node:util'); +- const red = kleur.red; ++ const red = (text) => styleText('red', text); +- const boldBlue = kleur.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/kleur-to-util-styletext +``` + +## Compatibility + +- **Removes kleur dependency** from package.json automatically +- **Supports kleur's full API**: every modifier, color, and background color kleur ships has a direct `util.styleText` equivalent, so no kleur method is left unsupported. + +## Limitations + +- **Complex conditional expressions** in some contexts may need manual review +- kleur exposes a secondary `kleur/colors` entry point (non-chainable individual color functions). This codemod targets the default `kleur` import/require only. +- **Direct calls chaining more than 4 styles** (e.g. `kleur.a.b.c.d.e("text")`) are left unchanged rather than transformed. Assigning the chain to a variable first (`const x = kleur.a.b.c.d.e; x("text")`) is transformed regardless of depth. diff --git a/recipes/kleur-to-util-styletext/codemod.yaml b/recipes/kleur-to-util-styletext/codemod.yaml new file mode 100644 index 00000000..e6d7e5c5 --- /dev/null +++ b/recipes/kleur-to-util-styletext/codemod.yaml @@ -0,0 +1,26 @@ +schema_version: "1.0" +name: "@nodejs/kleur-to-util-styletext" +version: 1.0.0 +capabilities: + - fs + - child_process +description: Migrate from the kleur package to Node.js's built-in util.styleText API +author: Node.js contributors +license: MIT +workflow: workflow.yaml +category: migration +repository: https://github.com/nodejs/userland-migrations + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - nodejs + +registry: + access: public + visibility: public diff --git a/recipes/kleur-to-util-styletext/package.json b/recipes/kleur-to-util-styletext/package.json new file mode 100644 index 00000000..74a42087 --- /dev/null +++ b/recipes/kleur-to-util-styletext/package.json @@ -0,0 +1,26 @@ +{ + "name": "@nodejs/kleur-to-util-styletext", + "version": "1.0.0", + "description": "Migrate from the kleur package to Node.js's built-in util.styleText API", + "type": "module", + "scripts": { + "test": "node --run test:workflow && node --run test:remove-dependencies", + "test:workflow": "npx codemod jssg test -l typescript ./src/workflow.ts ./", + "test:remove-dependencies": "npx codemod jssg test -l json ./src/remove-dependencies.ts ./tests/remove-dependencies --allow-child-process --allow-fs --strictness cst" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/kleur-to-util-styletext", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Node.js contributors", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/kleur-to-util-styletext/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.6.2" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/kleur-to-util-styletext/src/remove-dependencies.ts b/recipes/kleur-to-util-styletext/src/remove-dependencies.ts new file mode 100644 index 00000000..f3561939 --- /dev/null +++ b/recipes/kleur-to-util-styletext/src/remove-dependencies.ts @@ -0,0 +1,13 @@ +import type { Transform } from '@codemod.com/jssg-types/main'; +import type Json from '@codemod.com/jssg-types/langs/json'; +import removeDependencies from '@nodejs/codemod-utils/remove-dependencies'; + +const transform: Transform = async (root) => { + return removeDependencies(['kleur'], { + packageJsonPath: root.filename(), + runInstall: false, + persistFileWrite: false, + }); +}; + +export default transform; diff --git a/recipes/kleur-to-util-styletext/src/workflow.ts b/recipes/kleur-to-util-styletext/src/workflow.ts new file mode 100644 index 00000000..085fe1d5 --- /dev/null +++ b/recipes/kleur-to-util-styletext/src/workflow.ts @@ -0,0 +1,534 @@ +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'; +import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies'; + +const kleurBinding = 'kleur'; + +/** + * Transform function that converts kleur method calls to Node.js util.styleText calls. + * + * Examples: + * - kleur.red("text") → styleText("red", "text") + * - kleur.red.bold("text") → styleText(["red", "bold"], "text") + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + + // This actually catches `node:kleur` import but we don't care as it shouldn't append + const statements = getModuleDependencies(root, kleurBinding); + + // 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('kleur') or import { red } from 'kleur' + processDestructuredImports(rootNode, destructuredNames, edits); + } else { + // Handle default imports + // const kleur = require('kleur') or import kleur from 'kleur' + 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); +} + +// kleur methods that are supported by util.styleText +// (kleur's full API - see https://github.com/lukeed/kleur/blob/master/colors.mjs - +// every one of these already has a direct util.styleText equivalent, unlike chalk +// which also has hex()/rgb()/ansi256()/visible() with no styleText equivalent) +const SUPPORTED_METHODS = new Set([ + // Modifiers + 'reset', + 'bold', + 'dim', + 'italic', + 'underline', + 'inverse', + 'hidden', + 'strikethrough', + // Colors + 'black', + 'red', + 'green', + 'yellow', + 'blue', + 'magenta', + 'cyan', + 'white', + 'gray', + 'grey', + // Background colors + 'bgBlack', + 'bgRed', + 'bgGreen', + 'bgYellow', + 'bgBlue', + 'bgMagenta', + 'bgCyan', + 'bgWhite', +]); + +/** + * 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 'kleur' + 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('kleur') + // Handle dynamic imports: const { red, blue: foo } = await import('kleur') + 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 calls = rootNode.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + any: [ + { + kind: 'identifier', + pattern: name.local, + }, + { + kind: 'member_expression', + has: { + field: 'object', + any: [ + { + kind: 'identifier', + pattern: name.local, + }, + { + kind: 'member_expression', + has: { + field: 'object', + kind: 'identifier', + pattern: name.local, + }, + }, + ], + }, + }, + ], + }, + }, + }); + + for (const call of calls) { + const functionExpr = call.field('function'); + if (!functionExpr) continue; + + if (functionExpr.kind() !== 'identifier') continue; + + if (!isSupportedMethod(name.imported)) { + warnOnUnsupportedMethod(name.imported, rootNode, call); + continue; + } + + const textArg = getFirstCallArgument(call); + + if (!textArg) continue; + + const replacement = createStyleTextReplacement(name.imported, textArg); + + edits.push(call.replace(replacement)); + } + } +} + +/** + * Process default imports and transform member expression calls + */ +function processDefaultImports( + rootNode: SgNode, + binding: string, + edits: Edit[], +): void { + const kleurCalls = rootNode.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'member_expression', + has: { + field: 'object', + any: [ + // Direct kleur calls + { kind: 'identifier', pattern: binding }, + + // Chained kleur calls + { + kind: 'member_expression', + has: { field: 'object', kind: 'identifier', pattern: binding }, + }, + + // kleur.method1.method2() + { + kind: 'member_expression', + has: { + field: 'object', + kind: 'member_expression', + has: { field: 'object', kind: 'identifier', pattern: binding }, + }, + }, + + // kleur.method1.method2.method3() + { + kind: 'member_expression', + has: { + field: 'object', + kind: 'member_expression', + has: { + field: 'object', + kind: 'member_expression', + has: { field: 'object', kind: 'identifier', pattern: binding }, + }, + }, + }, + ], + }, + }, + }, + }); + + for (const call of kleurCalls) { + const functionExpr = call.field('function'); + + if (!functionExpr) continue; + + const styles = extractKleurStyles(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 = kleur.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 = kleur.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 ? kleur.bold : kleur.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 kleur styles. + */ +function extractKleurStyles(node: SgNode, kleurBinding: 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() === kleurBinding) { + // Base case: kleur.method + styles.push(propName); + return true; + } + + if (obj.kind() === 'member_expression' && traverse(obj)) { + // Recursive case: chain.method + styles.push(propName); + return true; + } + } + + return false; + } + + traverse(node); + + return styles; +} + +/** + * Create a wrapper function for a kleur member expression assignment + */ +function createMemberExpressionAssignment( + valueExpr: SgNode, + variableName: string, + binding: string, +): string | null { + const styles = extractKleurStyles(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 kleur 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 = extractKleurStyles(consequent, binding); + const alternativeStyles = extractKleurStyles(alternative, binding); + + // Only transform if both sides are kleur 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 kleur 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 kleur method '${method}' that does not have any equivalent in util.styleText please review this line`, + ); +} diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_advanced_modifiers.js b/recipes/kleur-to-util-styletext/tests/expected/tests_advanced_modifiers.js new file mode 100644 index 00000000..b4a36c86 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/expected/tests_advanced_modifiers.js @@ -0,0 +1,11 @@ +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("dim", "Dimmed text")); +console.log(styleText("italic", "Italic text")); +console.log(styleText("inverse", "Inverted colors")); +console.log(styleText("hidden", "Hidden text")); +console.log(styleText("reset", "Reset text")); +console.log(styleText("strikethrough", "Strikethrough text")); +console.log(styleText("underline", "Underlined text")); diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_background.js b/recipes/kleur-to-util-styletext/tests/expected/tests_background.js new file mode 100644 index 00000000..7056fc4a --- /dev/null +++ b/recipes/kleur-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", "gray"], "Info on blue background")); diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_basic.js b/recipes/kleur-to-util-styletext/tests/expected/tests_basic.js new file mode 100644 index 00000000..8bffa6fa --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_chained.js b/recipes/kleur-to-util-styletext/tests/expected/tests_chained.js new file mode 100644 index 00000000..a63ba0fe --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_commonjs_require.js b/recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require.js new file mode 100644 index 00000000..f2c2e6a3 --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_commonjs_require_destructure_reassign.js b/recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require_destructure_reassign.js new file mode 100644 index 00000000..999364b7 --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_commonjs_require_named.js b/recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require_named.js new file mode 100644 index 00000000..999364b7 --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_complex_chaining.js b/recipes/kleur-to-util-styletext/tests/expected/tests_complex_chaining.js new file mode 100644 index 00000000..c6666e9e --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_dynamic_import.js b/recipes/kleur-to-util-styletext/tests/expected/tests_dynamic_import.js new file mode 100644 index 00000000..24960903 --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_dynamic_import_custom_variable.js b/recipes/kleur-to-util-styletext/tests/expected/tests_dynamic_import_custom_variable.js new file mode 100644 index 00000000..0b782a11 --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_dynamic_import_destructured.js b/recipes/kleur-to-util-styletext/tests/expected/tests_dynamic_import_destructured.js new file mode 100644 index 00000000..03894ae4 --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_esm_import_all.js b/recipes/kleur-to-util-styletext/tests/expected/tests_esm_import_all.js new file mode 100644 index 00000000..a2656ff0 --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_esm_import_named.js b/recipes/kleur-to-util-styletext/tests/expected/tests_esm_import_named.js new file mode 100644 index 00000000..bca36b1d --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_four_level_chain.js b/recipes/kleur-to-util-styletext/tests/expected/tests_four_level_chain.js new file mode 100644 index 00000000..14fc58e8 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/expected/tests_four_level_chain.js @@ -0,0 +1,3 @@ +import { styleText } from "node:util"; + +console.log(styleText(["bold", "italic", "underline", "strikethrough"], "Four levels chained")); diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_method_assignments.js b/recipes/kleur-to-util-styletext/tests/expected/tests_method_assignments.js new file mode 100644 index 00000000..a503fb25 --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_mixed_import.js b/recipes/kleur-to-util-styletext/tests/expected/tests_mixed_import.js new file mode 100644 index 00000000..b3d6b286 --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_modifiers.js b/recipes/kleur-to-util-styletext/tests/expected/tests_modifiers.js new file mode 100644 index 00000000..e48b1dd1 --- /dev/null +++ b/recipes/kleur-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/kleur-to-util-styletext/tests/expected/tests_unsupported_features.js b/recipes/kleur-to-util-styletext/tests/expected/tests_unsupported_features.js new file mode 100644 index 00000000..9c95622c --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/expected/tests_unsupported_features.js @@ -0,0 +1,19 @@ +// Test file for unrecognized kleur method names. +// kleur's real API has no method without a util.styleText equivalent, but this +// guards against typos or a future kleur release adding something new. +const { styleText } = require("node:util"); + +// 1. An unrecognized method - should be left unchanged (unsupported) +console.log(kleur.someFutureMethod('Not a real kleur method')); + +// 2. kleur.enabled - property access should be ignored (not a call) +if (kleur.enabled) { + console.log(styleText("green", 'Colors are supported')); +} + +// 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 an unsupported method +console.log(kleur.red.someFutureMethod('Mixed supported and unsupported')); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_advanced_modifiers.js b/recipes/kleur-to-util-styletext/tests/input/tests_advanced_modifiers.js new file mode 100644 index 00000000..21430f2d --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_advanced_modifiers.js @@ -0,0 +1,11 @@ +import kleur from "kleur"; + +// Some of these have limited terminal support - may not work in all environments +console.log(kleur.bold("Bold text")); +console.log(kleur.dim("Dimmed text")); +console.log(kleur.italic("Italic text")); +console.log(kleur.inverse("Inverted colors")); +console.log(kleur.hidden("Hidden text")); +console.log(kleur.reset("Reset text")); +console.log(kleur.strikethrough("Strikethrough text")); +console.log(kleur.underline("Underlined text")); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_background.js b/recipes/kleur-to-util-styletext/tests/input/tests_background.js new file mode 100644 index 00000000..0932f932 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_background.js @@ -0,0 +1,5 @@ +import kleur from "kleur"; + +console.log(kleur.bgRed.white("Error on red background")); +console.log(kleur.bgGreen.black("Success on green background")); +console.log(kleur.bgBlue.gray("Info on blue background")); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_basic.js b/recipes/kleur-to-util-styletext/tests/input/tests_basic.js new file mode 100644 index 00000000..3e18455c --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_basic.js @@ -0,0 +1,5 @@ +import kleur from "kleur"; + +console.log(kleur.red("Error message")); +console.log(kleur.green("Success message")); +console.log(kleur.blue("Info message")); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_chained.js b/recipes/kleur-to-util-styletext/tests/input/tests_chained.js new file mode 100644 index 00000000..402978a1 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_chained.js @@ -0,0 +1,5 @@ +import kleur from "kleur"; + +console.log(kleur.red.bold("Error: Operation failed")); +console.log(kleur.green.underline("Success: All tests passed")); +console.log(kleur.yellow.bgBlack("Warning: Deprecated API usage")); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require.js b/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require.js new file mode 100644 index 00000000..9eab30c9 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require.js @@ -0,0 +1,7 @@ +const kleur = require("kleur"); + +const error = kleur.red("Error"); +const warning = kleur.yellow("Warning"); +const info = kleur.blue("Info"); + +console.log(error, warning, info); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_destructure_reassign.js b/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_destructure_reassign.js new file mode 100644 index 00000000..376ad46d --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_destructure_reassign.js @@ -0,0 +1,6 @@ +const { red: foo, yellow: bar } = require("kleur"); + +const error = foo("Error"); +const warning = bar("Warning message"); + +console.log(error, warning); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_named.js b/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_named.js new file mode 100644 index 00000000..87f63ce7 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_named.js @@ -0,0 +1,6 @@ +const { red, yellow } = require("kleur"); + +const error = red("Error"); +const warning = yellow("Warning message"); + +console.log(error, warning); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_complex_chaining.js b/recipes/kleur-to-util-styletext/tests/input/tests_complex_chaining.js new file mode 100644 index 00000000..28d2b52c --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_complex_chaining.js @@ -0,0 +1,6 @@ +const kleur = require("kleur"); + +const b = true; +const c = b ? kleur.bold : kleur.underline; + +console.log(c("Conditional style")); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_dynamic_import.js b/recipes/kleur-to-util-styletext/tests/input/tests_dynamic_import.js new file mode 100644 index 00000000..d8b69e4c --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_dynamic_import.js @@ -0,0 +1,9 @@ +// CommonJS style dynamic import +async function testCommonJS() { + const kleur = await import("kleur"); + console.log(kleur.bgBlue("This is a message")); +} + +// ESM style dynamic import +const kleur = await import("kleur"); +console.log(kleur.bgRed("This is a message")); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_dynamic_import_custom_variable.js b/recipes/kleur-to-util-styletext/tests/input/tests_dynamic_import_custom_variable.js new file mode 100644 index 00000000..9485b6a8 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_dynamic_import_custom_variable.js @@ -0,0 +1,6 @@ +const foo = await import('kleur'); + +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/kleur-to-util-styletext/tests/input/tests_dynamic_import_destructured.js b/recipes/kleur-to-util-styletext/tests/input/tests_dynamic_import_destructured.js new file mode 100644 index 00000000..e70559f6 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_dynamic_import_destructured.js @@ -0,0 +1,6 @@ +const { red, yellow } = await import('kleur'); + +const error = red("Error"); +const warning = yellow("Warning message"); + +console.log(error, warning); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_esm_import_all.js b/recipes/kleur-to-util-styletext/tests/input/tests_esm_import_all.js new file mode 100644 index 00000000..d5047eb1 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_esm_import_all.js @@ -0,0 +1,12 @@ +import * as c from "kleur"; + +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/kleur-to-util-styletext/tests/input/tests_esm_import_named.js b/recipes/kleur-to-util-styletext/tests/input/tests_esm_import_named.js new file mode 100644 index 00000000..523c1960 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_esm_import_named.js @@ -0,0 +1,6 @@ +import { red, yellow as y } from "kleur"; + +const error = red("Error"); +const warning = y("Warning message"); + +console.log(error, warning); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_four_level_chain.js b/recipes/kleur-to-util-styletext/tests/input/tests_four_level_chain.js new file mode 100644 index 00000000..201cc96b --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_four_level_chain.js @@ -0,0 +1,3 @@ +import kleur from "kleur"; + +console.log(kleur.bold.italic.underline.strikethrough("Four levels chained")); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_method_assignments.js b/recipes/kleur-to-util-styletext/tests/input/tests_method_assignments.js new file mode 100644 index 00000000..cb2b039a --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_method_assignments.js @@ -0,0 +1,28 @@ +const kleur = require("kleur"); + +// Method assignments to variables +const red = kleur.red; +const green = kleur.green; +const yellow = kleur.yellow; + +// Using assigned methods +console.log(red("This is red")); +console.log(green("This is green")); + +// Method assignments with chaining +const bold = kleur.bold; +const underline = kleur.underline; +console.log(bold("Bold text")); + +// Complex assignments +const redBold = kleur.red.bold; +console.log(redBold("Red and bold")); + +// Assignment in different scopes +function setupColors() { + const blue = kleur.blue; + return blue; +} + +const blueFunc = setupColors(); +console.log(blueFunc("Blue text")); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_mixed_import.js b/recipes/kleur-to-util-styletext/tests/input/tests_mixed_import.js new file mode 100644 index 00000000..c843252f --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_mixed_import.js @@ -0,0 +1,13 @@ +import kleur from "kleur"; +import { otherFunction } from "./utils"; + +function logError(message) { + console.log(kleur.red.bold(`ERROR: ${message}`)); +} + +function logSuccess(message) { + console.log(kleur.green(`SUCCESS: ${message}`)); +} + +logError("Something went wrong"); +logSuccess("Operation completed"); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_modifiers.js b/recipes/kleur-to-util-styletext/tests/input/tests_modifiers.js new file mode 100644 index 00000000..258ffa52 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_modifiers.js @@ -0,0 +1,5 @@ +import kleur from "kleur"; + +console.log(kleur.bold.italic.underline("Important announcement")); +console.log(kleur.dim.strikethrough("Deprecated feature")); +console.log(kleur.inverse("Inverted colors")); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_unsupported_features.js b/recipes/kleur-to-util-styletext/tests/input/tests_unsupported_features.js new file mode 100644 index 00000000..bf71c6e1 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_unsupported_features.js @@ -0,0 +1,19 @@ +// Test file for unrecognized kleur method names. +// kleur's real API has no method without a util.styleText equivalent, but this +// guards against typos or a future kleur release adding something new. +const kleur = require('kleur'); + +// 1. An unrecognized method - should be left unchanged (unsupported) +console.log(kleur.someFutureMethod('Not a real kleur method')); + +// 2. kleur.enabled - property access should be ignored (not a call) +if (kleur.enabled) { + console.log(kleur.green('Colors are supported')); +} + +// Mixed cases that should still work +console.log(kleur.red('This should be transformed')); +console.log(kleur.bold.blue('This should also be transformed')); + +// Edge case: method chaining with an unsupported method +console.log(kleur.red.someFutureMethod('Mixed supported and unsupported')); diff --git a/recipes/kleur-to-util-styletext/tests/remove-dependencies/remove-kleur/expected.json b/recipes/kleur-to-util-styletext/tests/remove-dependencies/remove-kleur/expected.json new file mode 100644 index 00000000..c8ac19c5 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/remove-dependencies/remove-kleur/expected.json @@ -0,0 +1,10 @@ +{ + "name": "fixture", + "version": "1.0.0", + "dependencies": { + "chalk": "^5.3.0" + }, + "devDependencies": { + "typescript": "^5.6.0" + } +} diff --git a/recipes/kleur-to-util-styletext/tests/remove-dependencies/remove-kleur/input.json b/recipes/kleur-to-util-styletext/tests/remove-dependencies/remove-kleur/input.json new file mode 100644 index 00000000..56ed351f --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/remove-dependencies/remove-kleur/input.json @@ -0,0 +1,11 @@ +{ + "name": "fixture", + "version": "1.0.0", + "dependencies": { + "kleur": "^4.1.5", + "chalk": "^5.3.0" + }, + "devDependencies": { + "typescript": "^5.6.0" + } +} diff --git a/recipes/kleur-to-util-styletext/workflow.yaml b/recipes/kleur-to-util-styletext/workflow.yaml new file mode 100644 index 00000000..10487ac4 --- /dev/null +++ b/recipes/kleur-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 kleur 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 kleur dependency + type: automatic + steps: + - name: Detect package manager and remove kleur dependency + js-ast-grep: + js_file: src/remove-dependencies.ts + base_path: . + include: + - "**/package.json" + exclude: + - "**/node_modules/**" + language: typescript + capabilities: + - child_process + - fs