From 29b0fc72c2026d97200ffd2a372e4e5a6c75c3a5 Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:16:59 +0200 Subject: [PATCH 1/6] Add kleur to styleText migration recipe --- package-lock.json | 15 + recipes/kleur-to-util-styletext/README.md | 47 ++ recipes/kleur-to-util-styletext/codemod.yaml | 26 + recipes/kleur-to-util-styletext/package.json | 26 + .../src/remove-dependencies.ts | 13 + .../kleur-to-util-styletext/src/workflow.ts | 448 ++++++++++++++++++ .../tests/expected/tests_basic.js | 8 + .../tests/expected/tests_chained.js | 8 + .../tests/expected/tests_colors_import.js | 5 + .../tests/expected/tests_commonjs_require.js | 4 + .../expected/tests_commonjs_require_colors.js | 5 + .../tests/expected/tests_namespace_import.js | 4 + .../expected/tests_unsupported_features.js | 5 + .../tests/input/tests_basic.js | 8 + .../tests/input/tests_chained.js | 8 + .../tests/input/tests_colors_import.js | 5 + .../tests/input/tests_commonjs_require.js | 4 + .../input/tests_commonjs_require_colors.js | 5 + .../tests/input/tests_namespace_import.js | 4 + .../tests/input/tests_unsupported_features.js | 5 + .../remove-kleur/expected.json | 10 + .../remove-kleur/input.json | 11 + recipes/kleur-to-util-styletext/workflow.yaml | 42 ++ 23 files changed, 716 insertions(+) create mode 100644 recipes/kleur-to-util-styletext/README.md create mode 100644 recipes/kleur-to-util-styletext/codemod.yaml create mode 100644 recipes/kleur-to-util-styletext/package.json create mode 100644 recipes/kleur-to-util-styletext/src/remove-dependencies.ts create mode 100644 recipes/kleur-to-util-styletext/src/workflow.ts create mode 100644 recipes/kleur-to-util-styletext/tests/expected/tests_basic.js create mode 100644 recipes/kleur-to-util-styletext/tests/expected/tests_chained.js create mode 100644 recipes/kleur-to-util-styletext/tests/expected/tests_colors_import.js create mode 100644 recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require.js create mode 100644 recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require_colors.js create mode 100644 recipes/kleur-to-util-styletext/tests/expected/tests_namespace_import.js create mode 100644 recipes/kleur-to-util-styletext/tests/expected/tests_unsupported_features.js create mode 100644 recipes/kleur-to-util-styletext/tests/input/tests_basic.js create mode 100644 recipes/kleur-to-util-styletext/tests/input/tests_chained.js create mode 100644 recipes/kleur-to-util-styletext/tests/input/tests_colors_import.js create mode 100644 recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require.js create mode 100644 recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_colors.js create mode 100644 recipes/kleur-to-util-styletext/tests/input/tests_namespace_import.js create mode 100644 recipes/kleur-to-util-styletext/tests/input/tests_unsupported_features.js create mode 100644 recipes/kleur-to-util-styletext/tests/remove-dependencies/remove-kleur/expected.json create mode 100644 recipes/kleur-to-util-styletext/tests/remove-dependencies/remove-kleur/input.json create mode 100644 recipes/kleur-to-util-styletext/workflow.yaml diff --git a/package-lock.json b/package-lock.json index cb54b876..11929353 100644 --- a/package-lock.json +++ b/package-lock.json @@ -476,6 +476,10 @@ "resolved": "recipes/import-assertions-to-attributes", "link": true }, + "node_modules/@nodejs/kleur-to-util-styletext": { + "resolved": "recipes/kleur-to-util-styletext", + "link": true + }, "node_modules/@nodejs/mock-module-exports": { "resolved": "recipes/mock-module-exports", "link": true @@ -891,6 +895,17 @@ "@codemod.com/jssg-types": "^1.6.1" } }, + "recipes/kleur-to-util-styletext": { + "name": "@nodejs/kleur-to-util-styletext", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.6.1" + } + }, "recipes/mock-module-exports": { "name": "@nodejs/mock-module-exports", "version": "1.0.0", diff --git a/recipes/kleur-to-util-styletext/README.md b/recipes/kleur-to-util-styletext/README.md new file mode 100644 index 00000000..1fc699eb --- /dev/null +++ b/recipes/kleur-to-util-styletext/README.md @@ -0,0 +1,47 @@ +# Kleur to util.styleText + +This recipe migrates from the external `kleur` package to Node.js built-in `util.styleText` API. It transforms `kleur` style calls and `kleur/colors` named style functions to the native Node.js styling functionality. + +## Examples + +```diff +- import kleur from 'kleur'; ++ import { styleText } from 'node:util'; +- console.log(kleur.red('Error')); ++ console.log(styleText('red', 'Error')); +- console.log(kleur.bold().red('Failure')); ++ console.log(styleText(['bold', 'red'], 'Failure')); +``` + +```diff +- import { green, dim } from 'kleur/colors'; ++ import { styleText } from 'node:util'; +- console.log(green('OK') + ' ' + dim(name)); ++ console.log(styleText('green', 'OK') + ' ' + styleText('dim', name)); +``` + +```diff +- import { bgRed, white } from 'kleur/colors'; ++ import { styleText } from 'node:util'; +- console.log(bgRed(white('FAIL'))); ++ console.log(styleText(['bgRed', 'white'], 'FAIL')); +``` + +## Usage + +Run this codemod with: + +```sh +npx codemod nodejs/kleur-to-util-styletext +``` + +## Compatibility + +- Removes the `kleur` dependency from package.json automatically +- Supports default, namespace, and CommonJS `kleur` imports +- Supports named imports and destructured requires from `kleur/colors` +- Leaves unsupported APIs such as `kleur.enabled` and `$` from `kleur/colors` unchanged + +## Limitations + +- Unsupported `kleur` APIs that do not map to `util.styleText` require manual migration diff --git a/recipes/kleur-to-util-styletext/codemod.yaml b/recipes/kleur-to-util-styletext/codemod.yaml new file mode 100644 index 00000000..820d047f --- /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: Herrtian +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..ffa81fe5 --- /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": "Herrtian", + "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.1" + }, + "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..bcf36043 --- /dev/null +++ b/recipes/kleur-to-util-styletext/src/workflow.ts @@ -0,0 +1,448 @@ +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'; +const kleurColorsBinding = 'kleur/colors'; + +type StyleCall = { + styles: string[]; + textArg: string; +}; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + const statements = [ + ...getModuleDependencies(root, kleurBinding).map((statement) => ({ + moduleName: kleurBinding, + statement, + })), + ...getModuleDependencies(root, kleurColorsBinding).map((statement) => ({ + moduleName: kleurColorsBinding, + statement, + })), + ]; + + if (!statements.length) return null; + + for (const { moduleName, statement } of statements) { + const initialEditCount = edits.length; + + if (moduleName === kleurColorsBinding) { + const destructuredNames = getDestructuredNames(statement); + processKleurColorsImports(rootNode, destructuredNames, edits); + } else { + const binding = resolveBindingPath(statement, '$'); + + if (binding && !hasUnsupportedEnabledUsage(rootNode, binding)) { + processKleurImports(rootNode, binding, edits); + } + } + + if (edits.length > initialEditCount) { + edits.push(statement.replace(createImportReplacement(statement))); + } + } + + if (!edits.length) return null; + + return rootNode.commitEdits(edits); +} + +const COMPAT_MAP: Record = { + overline: 'overlined', +}; + +const SUPPORTED_METHODS = new Set([ + 'black', + 'red', + 'green', + 'yellow', + 'blue', + 'magenta', + 'cyan', + 'white', + 'gray', + 'grey', + 'blackBright', + 'redBright', + 'greenBright', + 'yellowBright', + 'blueBright', + 'magentaBright', + 'cyanBright', + 'whiteBright', + 'bgBlack', + 'bgRed', + 'bgGreen', + 'bgYellow', + 'bgBlue', + 'bgMagenta', + 'bgCyan', + 'bgWhite', + 'bgGray', + 'bgGrey', + 'bgBlackBright', + 'bgRedBright', + 'bgGreenBright', + 'bgYellowBright', + 'bgBlueBright', + 'bgMagentaBright', + 'bgCyanBright', + 'bgWhiteBright', + 'reset', + 'bold', + 'italic', + 'underline', + 'strikethrough', + 'hidden', + 'dim', + 'overlined', + 'blink', + 'inverse', + 'doubleunderline', + 'framed', +]); + +function mapStyle(style: string): string { + return COMPAT_MAP[style] || style; +} + +function isSupportedMethod(method: string): boolean { + return SUPPORTED_METHODS.has(mapStyle(method)); +} + +function hasUnsupportedMethods(styles: string[]): boolean { + return styles.some((style) => !SUPPORTED_METHODS.has(style)); +} + +function getDestructuredNames( + statement: SgNode, +): Array<{ imported: string; local: string }> { + const names: Array<{ imported: string; local: string }> = []; + + 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 }); + } + } + } + } 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') { + const name = prop.text(); + + names.push({ imported: name, local: name }); + } else if (prop.kind() === 'pair_pattern') { + const key = prop.field('key'); + const value = prop.field('value'); + + if (key && value) { + names.push({ imported: key.text(), local: value.text() }); + } + } + } + } + } + + return names; +} + +function processKleurImports( + rootNode: SgNode, + binding: string, + edits: Edit[], +): void { + const calls = rootNode.findAll({ + rule: { kind: 'call_expression' }, + }); + + for (const call of calls) { + const functionExpr = call.field('function'); + if (!functionExpr) continue; + + const styles = extractKleurStyles(functionExpr, binding); + if (!styles || 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; + + edits.push(call.replace(createStyleTextReplacement(styles, textArg))); + } +} + +function processKleurColorsImports( + rootNode: SgNode, + destructuredNames: Array<{ imported: string; local: string }>, + edits: Edit[], +): void { + if (!destructuredNames.length) return; + + const unsupportedNames = destructuredNames.filter( + (name) => !isSupportedMethod(name.imported), + ); + + if (unsupportedNames.length) { + for (const name of unsupportedNames) { + warnOnUnsupportedMethod(name.imported, rootNode, rootNode); + } + return; + } + + const styleImports = new Map(); + + for (const name of destructuredNames) { + styleImports.set(name.local, mapStyle(name.imported)); + } + + const calls = rootNode.findAll({ + rule: { kind: 'call_expression' }, + }); + + for (const call of calls) { + if (hasKleurColorsCallAncestor(call, styleImports)) continue; + + const parsed = extractKleurColorsCall(call, styleImports); + if (!parsed) continue; + + edits.push( + call.replace(createStyleTextReplacement(parsed.styles, parsed.textArg)), + ); + } +} + +function extractKleurStyles( + node: SgNode, + binding: string, +): string[] | null { + const styles: string[] = []; + + function traverse(activeNode: SgNode): boolean { + if (activeNode.kind() !== 'member_expression') return false; + + const obj = activeNode.field('object'); + const prop = activeNode.field('property'); + + if (!obj || !prop || prop.kind() !== 'property_identifier') { + return false; + } + + const propName = mapStyle(prop.text()); + + if (obj.kind() === 'identifier' && obj.text() === binding) { + styles.push(propName); + return true; + } + + if (obj.kind() === 'member_expression' && traverse(obj)) { + styles.push(propName); + return true; + } + + if (obj.kind() === 'call_expression') { + const chainedArgs = getCallArguments(obj); + const chainedFunction = obj.field('function'); + + if ( + chainedArgs.length === 0 && + chainedFunction && + traverse(chainedFunction) + ) { + styles.push(propName); + return true; + } + } + + return false; + } + + return traverse(node) ? styles : null; +} + +function extractKleurColorsCall( + call: SgNode, + styleImports: Map, +): StyleCall | null { + const functionExpr = call.field('function'); + + if (!functionExpr || functionExpr.kind() !== 'identifier') { + return null; + } + + const style = styleImports.get(functionExpr.text()); + if (!style) return null; + + const firstArg = getFirstCallArgumentNode(call); + if (!firstArg) return null; + + if (firstArg.kind() === 'call_expression') { + const nested = extractKleurColorsCall(firstArg, styleImports); + + if (nested) { + return { + styles: [style, ...nested.styles], + textArg: nested.textArg, + }; + } + } + + return { + styles: [style], + textArg: firstArg.text(), + }; +} + +function hasKleurColorsCallAncestor( + call: SgNode, + styleImports: Map, +): boolean { + let parentNode = call.parent(); + + while (parentNode) { + if ( + parentNode.kind() === 'call_expression' && + extractKleurColorsCall(parentNode, styleImports) + ) { + return true; + } + + parentNode = parentNode.parent(); + } + + return false; +} + +function hasUnsupportedEnabledUsage( + rootNode: SgNode, + binding: string, +): boolean { + const enabledUsage = rootNode.find({ + rule: { + kind: 'member_expression', + all: [ + { + has: { + field: 'object', + kind: 'identifier', + pattern: binding, + }, + }, + { + has: { + field: 'property', + kind: 'property_identifier', + pattern: 'enabled', + }, + }, + ], + }, + }); + + if (enabledUsage) { + warnOnUnsupportedMethod('enabled', rootNode, enabledUsage); + return true; + } + + return false; +} + +function getFirstCallArgument(call: SgNode): string | null { + return getFirstCallArgumentNode(call)?.text() || null; +} + +function getFirstCallArgumentNode(call: SgNode): SgNode | null { + const args = getCallArguments(call); + + if (args.length === 0) return null; + + return args[0] || null; +} + +function getCallArguments(call: SgNode): SgNode[] { + const args = call.field('arguments'); + + if (!args) return []; + + return args.children().filter((child) => { + const excluded = [',', '(', ')']; + return !excluded.includes(child.kind()); + }); +} + +function createStyleTextReplacement(styles: string[], textArg: string): string { + if (styles.length === 1) { + return `styleText("${styles[0]}", ${textArg})`; + } + + const stylesArray = `[${styles.map((style) => `"${style}"`).join(', ')}]`; + + return `styleText(${stylesArray}, ${textArg})`; +} + +function createImportReplacement(statement: SgNode): string { + if (statement.kind() === 'import_statement') { + return `import { styleText } from "node:util";`; + } + + if (statement.kind() === 'variable_declarator') { + if (statement.field('value')?.kind() === 'await_expression') { + return `{ styleText } = await import("node:util")`; + } + + return `{ styleText } = require("node:util")`; + } + + return ''; +} + +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_basic.js b/recipes/kleur-to-util-styletext/tests/expected/tests_basic.js new file mode 100644 index 00000000..ab65054c --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/expected/tests_basic.js @@ -0,0 +1,8 @@ +import { styleText } from "node:util"; + +console.log(styleText("red", "Error")); +console.log(styleText("green", "Success")); +console.log(styleText("bold", "Important")); +console.log(styleText("bgRed", "Alert")); +console.log(`${styleText("blue", "[INFO]")} ${styleText("green", user)}`); +console.log("Status: " + styleText(["bold", "green"], "OK")); 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..d5c12f76 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/expected/tests_chained.js @@ -0,0 +1,8 @@ +import { styleText } from "node:util"; + +console.log(styleText(["bold", "red"], "Failure")); +console.log(styleText(["bold", "italic", "red"], "Failure")); +console.log(styleText(["bold", "italic", "underline", "red"], "Failure")); + +const errorStyle = (msg) => styleText(["bold", "red"], msg); +const badge = styleText(["bgRed", "white"], " ERROR "); diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_colors_import.js b/recipes/kleur-to-util-styletext/tests/expected/tests_colors_import.js new file mode 100644 index 00000000..c809eec6 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/expected/tests_colors_import.js @@ -0,0 +1,5 @@ +import { styleText } from "node:util"; + +console.log(styleText("red", "Error")); +console.log(styleText("green", "OK") + " " + styleText("dim", name)); +console.log(styleText(["bgRed", "white"], "FAIL")); 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..05ea8600 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require.js @@ -0,0 +1,4 @@ +const { styleText } = require("node:util"); + +console.log(styleText("red", "Error")); +console.log(styleText(["bold", "yellow"], "Warning")); diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require_colors.js b/recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require_colors.js new file mode 100644 index 00000000..8b5d0dde --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require_colors.js @@ -0,0 +1,5 @@ +const { styleText } = require("node:util"); + +console.log(styleText("red", "Error")); +console.log(styleText("green", "OK")); +console.log(styleText(["bgRed", "white"], "FAIL")); diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_namespace_import.js b/recipes/kleur-to-util-styletext/tests/expected/tests_namespace_import.js new file mode 100644 index 00000000..414d3772 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/expected/tests_namespace_import.js @@ -0,0 +1,4 @@ +import { styleText } from "node:util"; + +console.log(styleText("red", "Error")); +console.log(styleText(["bold", "cyan"], "Info")); 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..3ee83f5a --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/expected/tests_unsupported_features.js @@ -0,0 +1,5 @@ +import kleur from "kleur"; +import { $ } from "kleur/colors"; + +kleur.enabled = false; +console.log($.enabled); 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..3bc10720 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_basic.js @@ -0,0 +1,8 @@ +import kleur from "kleur"; + +console.log(kleur.red("Error")); +console.log(kleur.green("Success")); +console.log(kleur.bold("Important")); +console.log(kleur.bgRed("Alert")); +console.log(`${kleur.blue("[INFO]")} ${kleur.green(user)}`); +console.log("Status: " + kleur.bold().green("OK")); 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..be2a3feb --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_chained.js @@ -0,0 +1,8 @@ +import kleur from "kleur"; + +console.log(kleur.bold().red("Failure")); +console.log(kleur.bold().italic().red("Failure")); +console.log(kleur.bold().italic().underline().red("Failure")); + +const errorStyle = (msg) => kleur.bold().red(msg); +const badge = kleur.bgRed().white(" ERROR "); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_colors_import.js b/recipes/kleur-to-util-styletext/tests/input/tests_colors_import.js new file mode 100644 index 00000000..469833a2 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_colors_import.js @@ -0,0 +1,5 @@ +import { red, green, dim as muted, bgRed, white } from "kleur/colors"; + +console.log(red("Error")); +console.log(green("OK") + " " + muted(name)); +console.log(bgRed(white("FAIL"))); 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..8287066d --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require.js @@ -0,0 +1,4 @@ +const kleur = require("kleur"); + +console.log(kleur.red("Error")); +console.log(kleur.bold().yellow("Warning")); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_colors.js b/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_colors.js new file mode 100644 index 00000000..805f2dfc --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_colors.js @@ -0,0 +1,5 @@ +const { red, green: success, bgRed, white } = require("kleur/colors"); + +console.log(red("Error")); +console.log(success("OK")); +console.log(bgRed(white("FAIL"))); diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_namespace_import.js b/recipes/kleur-to-util-styletext/tests/input/tests_namespace_import.js new file mode 100644 index 00000000..9abd2c96 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_namespace_import.js @@ -0,0 +1,4 @@ +import * as kleur from "kleur"; + +console.log(kleur.red("Error")); +console.log(kleur.bold().cyan("Info")); 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..3ee83f5a --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/input/tests_unsupported_features.js @@ -0,0 +1,5 @@ +import kleur from "kleur"; +import { $ } from "kleur/colors"; + +kleur.enabled = false; +console.log($.enabled); 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..4660c4a9 --- /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": { + "chalk": "^5.3.0", + "kleur": "^4.1.5" + }, + "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 From 9e64c47c9e15b75a66050948e4d5d9bec7fe89c1 Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Sat, 6 Jun 2026 19:02:47 +0200 Subject: [PATCH 2/6] fix(kleur): address review feedback --- recipes/kleur-to-util-styletext/README.md | 10 +- recipes/kleur-to-util-styletext/package.json | 2 +- .../kleur-to-util-styletext/src/workflow.ts | 172 ++++++++++++++++-- .../basic/expected.js} | 0 .../basic/input.js} | 0 .../chained/expected.js} | 0 .../chained/input.js} | 0 .../colors-import/expected.js} | 0 .../colors-import/input.js} | 0 .../commonjs-require-colors/expected.js} | 0 .../commonjs-require-colors/input.js} | 0 .../commonjs-require/expected.js} | 0 .../commonjs-require/input.js} | 0 .../workflow/dynamic-import-await/expected.js | 4 + .../workflow/dynamic-import-await/input.js | 4 + .../workflow/dynamic-import-then/expected.js | 4 + .../workflow/dynamic-import-then/input.js | 4 + .../workflow/esm-alias-import/expected.js | 4 + .../tests/workflow/esm-alias-import/input.js | 4 + .../namespace-import/expected.js} | 0 .../namespace-import/input.js} | 0 .../unsupported-features/expected.js} | 0 .../unsupported-features/input.js} | 0 23 files changed, 179 insertions(+), 29 deletions(-) rename recipes/kleur-to-util-styletext/tests/{expected/tests_basic.js => workflow/basic/expected.js} (100%) rename recipes/kleur-to-util-styletext/tests/{input/tests_basic.js => workflow/basic/input.js} (100%) rename recipes/kleur-to-util-styletext/tests/{expected/tests_chained.js => workflow/chained/expected.js} (100%) rename recipes/kleur-to-util-styletext/tests/{input/tests_chained.js => workflow/chained/input.js} (100%) rename recipes/kleur-to-util-styletext/tests/{expected/tests_colors_import.js => workflow/colors-import/expected.js} (100%) rename recipes/kleur-to-util-styletext/tests/{input/tests_colors_import.js => workflow/colors-import/input.js} (100%) rename recipes/kleur-to-util-styletext/tests/{expected/tests_commonjs_require_colors.js => workflow/commonjs-require-colors/expected.js} (100%) rename recipes/kleur-to-util-styletext/tests/{input/tests_commonjs_require_colors.js => workflow/commonjs-require-colors/input.js} (100%) rename recipes/kleur-to-util-styletext/tests/{expected/tests_commonjs_require.js => workflow/commonjs-require/expected.js} (100%) rename recipes/kleur-to-util-styletext/tests/{input/tests_commonjs_require.js => workflow/commonjs-require/input.js} (100%) create mode 100644 recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-await/expected.js create mode 100644 recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-await/input.js create mode 100644 recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-then/expected.js create mode 100644 recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-then/input.js create mode 100644 recipes/kleur-to-util-styletext/tests/workflow/esm-alias-import/expected.js create mode 100644 recipes/kleur-to-util-styletext/tests/workflow/esm-alias-import/input.js rename recipes/kleur-to-util-styletext/tests/{expected/tests_namespace_import.js => workflow/namespace-import/expected.js} (100%) rename recipes/kleur-to-util-styletext/tests/{input/tests_namespace_import.js => workflow/namespace-import/input.js} (100%) rename recipes/kleur-to-util-styletext/tests/{expected/tests_unsupported_features.js => workflow/unsupported-features/expected.js} (100%) rename recipes/kleur-to-util-styletext/tests/{input/tests_unsupported_features.js => workflow/unsupported-features/input.js} (100%) diff --git a/recipes/kleur-to-util-styletext/README.md b/recipes/kleur-to-util-styletext/README.md index 1fc699eb..1624f84c 100644 --- a/recipes/kleur-to-util-styletext/README.md +++ b/recipes/kleur-to-util-styletext/README.md @@ -27,14 +27,6 @@ This recipe migrates from the external `kleur` package to Node.js built-in `util + console.log(styleText(['bgRed', 'white'], 'FAIL')); ``` -## Usage - -Run this codemod with: - -```sh -npx codemod nodejs/kleur-to-util-styletext -``` - ## Compatibility - Removes the `kleur` dependency from package.json automatically @@ -44,4 +36,4 @@ npx codemod nodejs/kleur-to-util-styletext ## Limitations -- Unsupported `kleur` APIs that do not map to `util.styleText` require manual migration +- Manual migration is required for `kleur.enabled`, `$` from `kleur/colors`, and other APIs that are not direct style functions diff --git a/recipes/kleur-to-util-styletext/package.json b/recipes/kleur-to-util-styletext/package.json index ffa81fe5..0c6366ce 100644 --- a/recipes/kleur-to-util-styletext/package.json +++ b/recipes/kleur-to-util-styletext/package.json @@ -5,7 +5,7 @@ "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:workflow": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests/workflow", "test:remove-dependencies": "npx codemod jssg test -l json ./src/remove-dependencies.ts ./tests/remove-dependencies --allow-child-process --allow-fs --strictness cst" }, "repository": { diff --git a/recipes/kleur-to-util-styletext/src/workflow.ts b/recipes/kleur-to-util-styletext/src/workflow.ts index bcf36043..72453cce 100644 --- a/recipes/kleur-to-util-styletext/src/workflow.ts +++ b/recipes/kleur-to-util-styletext/src/workflow.ts @@ -11,6 +11,12 @@ type StyleCall = { textArg: string; }; +/** + * Converts kleur and kleur/colors style calls to Node.js util.styleText calls. + * + * Handles default, namespace, CommonJS, awaited dynamic imports, and + * kleur/colors named style imports. + */ export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; @@ -42,7 +48,15 @@ export default function transform(root: SgRoot): string | null { } if (edits.length > initialEditCount) { - edits.push(statement.replace(createImportReplacement(statement))); + if (statement.is('expression_statement')) { + edits.push(...createDynamicImportReplacementEdits(statement)); + continue; + } + + const importReplacement = createImportReplacement(statement); + if (importReplacement) { + edits.push(statement.replace(importReplacement)); + } } } @@ -55,6 +69,9 @@ const COMPAT_MAP: Record = { overline: 'overlined', }; +/** + * Kleur style methods that can be mapped to util.styleText. + */ const SUPPORTED_METHODS = new Set([ 'black', 'red', @@ -106,24 +123,37 @@ const SUPPORTED_METHODS = new Set([ 'framed', ]); +/** + * Maps kleur method names that have a different util.styleText spelling. + */ function mapStyle(style: string): string { return COMPAT_MAP[style] || style; } +/** + * Returns whether a kleur method maps to a util.styleText style. + */ function isSupportedMethod(method: string): boolean { return SUPPORTED_METHODS.has(mapStyle(method)); } -function hasUnsupportedMethods(styles: string[]): boolean { - return styles.some((style) => !SUPPORTED_METHODS.has(style)); +/** + * Returns the style methods that should be left for manual migration. + */ +function getUnsupportedMethods(styles: string[]): string[] { + return styles.filter((style) => !isSupportedMethod(style)); } +/** + * Extracts named imports from import statements and destructured requires. + */ function getDestructuredNames( statement: SgNode, ): Array<{ imported: string; local: string }> { const names: Array<{ imported: string; local: string }> = []; + const statementKind = statement.kind(); - if (statement.kind() === 'import_statement') { + if (statementKind === 'import_statement') { const namedImports = statement.find({ rule: { kind: 'named_imports' }, }); @@ -145,7 +175,7 @@ function getDestructuredNames( } } } - } else if (statement.kind() === 'variable_declarator') { + } else if (statementKind === 'variable_declarator') { const nameField = statement.field('name'); if (nameField && nameField.kind() === 'object_pattern') { @@ -178,6 +208,9 @@ function getDestructuredNames( return names; } +/** + * Transforms calls that use the default or namespace kleur binding. + */ function processKleurImports( rootNode: SgNode, binding: string, @@ -194,11 +227,11 @@ function processKleurImports( const styles = extractKleurStyles(functionExpr, binding); if (!styles || styles.length === 0) continue; - if (hasUnsupportedMethods(styles)) { - for (const style of styles) { - if (!SUPPORTED_METHODS.has(style)) { - warnOnUnsupportedMethod(style, rootNode, call); - } + const unsupportedMethods = getUnsupportedMethods(styles); + + if (unsupportedMethods.length) { + for (const method of unsupportedMethods) { + warnOnUnsupportedMethod(method, rootNode, call); } continue; } @@ -210,6 +243,9 @@ function processKleurImports( } } +/** + * Transforms calls imported from kleur/colors named exports. + */ function processKleurColorsImports( rootNode: SgNode, destructuredNames: Array<{ imported: string; local: string }>, @@ -250,6 +286,9 @@ function processKleurColorsImports( } } +/** + * Extracts a kleur style chain from a member expression. + */ function extractKleurStyles( node: SgNode, binding: string, @@ -262,23 +301,24 @@ function extractKleurStyles( const obj = activeNode.field('object'); const prop = activeNode.field('property'); - if (!obj || !prop || prop.kind() !== 'property_identifier') { + if (!obj || !prop?.is('property_identifier')) { return false; } + const objKind = obj.kind(); const propName = mapStyle(prop.text()); - if (obj.kind() === 'identifier' && obj.text() === binding) { + if (objKind === 'identifier' && obj.text() === binding) { styles.push(propName); return true; } - if (obj.kind() === 'member_expression' && traverse(obj)) { + if (objKind === 'member_expression' && traverse(obj)) { styles.push(propName); return true; } - if (obj.kind() === 'call_expression') { + if (objKind === 'call_expression') { const chainedArgs = getCallArguments(obj); const chainedFunction = obj.field('function'); @@ -298,6 +338,9 @@ function extractKleurStyles( return traverse(node) ? styles : null; } +/** + * Extracts a kleur/colors call and nested style calls. + */ function extractKleurColorsCall( call: SgNode, styleImports: Map, @@ -331,6 +374,9 @@ function extractKleurColorsCall( }; } +/** + * Skips nested kleur/colors calls so only the outer call is replaced. + */ function hasKleurColorsCallAncestor( call: SgNode, styleImports: Map, @@ -339,7 +385,7 @@ function hasKleurColorsCallAncestor( while (parentNode) { if ( - parentNode.kind() === 'call_expression' && + parentNode.is('call_expression') && extractKleurColorsCall(parentNode, styleImports) ) { return true; @@ -351,6 +397,9 @@ function hasKleurColorsCallAncestor( return false; } +/** + * Detects kleur.enabled assignments that need manual migration. + */ function hasUnsupportedEnabledUsage( rootNode: SgNode, binding: string, @@ -385,10 +434,16 @@ function hasUnsupportedEnabledUsage( return false; } +/** + * Returns the first argument text from a call expression. + */ function getFirstCallArgument(call: SgNode): string | null { return getFirstCallArgumentNode(call)?.text() || null; } +/** + * Returns the first argument node from a call expression. + */ function getFirstCallArgumentNode(call: SgNode): SgNode | null { const args = getCallArguments(call); @@ -397,6 +452,9 @@ function getFirstCallArgumentNode(call: SgNode): SgNode | null { return args[0] || null; } +/** + * Returns the non-punctuation argument nodes from a call expression. + */ function getCallArguments(call: SgNode): SgNode[] { const args = call.field('arguments'); @@ -408,6 +466,9 @@ function getCallArguments(call: SgNode): SgNode[] { }); } +/** + * Builds a util.styleText call for one or more styles. + */ function createStyleTextReplacement(styles: string[], textArg: string): string { if (styles.length === 1) { return `styleText("${styles[0]}", ${textArg})`; @@ -418,12 +479,17 @@ function createStyleTextReplacement(styles: string[], textArg: string): string { return `styleText(${stylesArray}, ${textArg})`; } -function createImportReplacement(statement: SgNode): string { - if (statement.kind() === 'import_statement') { +/** + * Replaces kleur imports with node:util styleText imports. + */ +function createImportReplacement(statement: SgNode): string | null { + const statementKind = statement.kind(); + + if (statementKind === 'import_statement') { return `import { styleText } from "node:util";`; } - if (statement.kind() === 'variable_declarator') { + if (statementKind === 'variable_declarator') { if (statement.field('value')?.kind() === 'await_expression') { return `{ styleText } = await import("node:util")`; } @@ -431,9 +497,77 @@ function createImportReplacement(statement: SgNode): string { return `{ styleText } = require("node:util")`; } - return ''; + return null; +} + +/** + * Builds edits for import("kleur").then((kleur) => ...) callback imports. + */ +function createDynamicImportReplacementEdits(statement: SgNode): Edit[] { + const importCall = statement.find({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'import', + }, + }, + }); + + const callback = + statement.find({ + rule: { kind: 'arrow_function' }, + }) || + statement.find({ + rule: { kind: 'function_expression' }, + }); + + const edits: Edit[] = []; + + if (importCall) { + edits.push(importCall.replace('import("node:util")')); + } + + if (!callback) return edits; + + const args = callback.field('parameter') || callback.field('parameters'); + + if (args) { + if (args.is('formal_parameters')) { + const firstArg = args.child(1); + + if (firstArg) { + edits.push(firstArg.replace('{ styleText }')); + } + + return edits; + } + + edits.push(args.replace('({ styleText })')); + + return edits; + } + + const firstFormalArg = callback.find({ + rule: { + kind: 'identifier', + inside: { + kind: 'formal_parameters', + stopBy: 'end', + }, + }, + }); + + if (firstFormalArg) { + edits.push(firstFormalArg.replace('{ styleText }')); + } + + return edits; } +/** + * Emits a location-aware warning for unsupported kleur methods. + */ function warnOnUnsupportedMethod( method: string, rootNode: SgNode, diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_basic.js b/recipes/kleur-to-util-styletext/tests/workflow/basic/expected.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/expected/tests_basic.js rename to recipes/kleur-to-util-styletext/tests/workflow/basic/expected.js diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_basic.js b/recipes/kleur-to-util-styletext/tests/workflow/basic/input.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/input/tests_basic.js rename to recipes/kleur-to-util-styletext/tests/workflow/basic/input.js diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_chained.js b/recipes/kleur-to-util-styletext/tests/workflow/chained/expected.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/expected/tests_chained.js rename to recipes/kleur-to-util-styletext/tests/workflow/chained/expected.js diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_chained.js b/recipes/kleur-to-util-styletext/tests/workflow/chained/input.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/input/tests_chained.js rename to recipes/kleur-to-util-styletext/tests/workflow/chained/input.js diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_colors_import.js b/recipes/kleur-to-util-styletext/tests/workflow/colors-import/expected.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/expected/tests_colors_import.js rename to recipes/kleur-to-util-styletext/tests/workflow/colors-import/expected.js diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_colors_import.js b/recipes/kleur-to-util-styletext/tests/workflow/colors-import/input.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/input/tests_colors_import.js rename to recipes/kleur-to-util-styletext/tests/workflow/colors-import/input.js diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require_colors.js b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require-colors/expected.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require_colors.js rename to recipes/kleur-to-util-styletext/tests/workflow/commonjs-require-colors/expected.js diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_colors.js b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require-colors/input.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require_colors.js rename to recipes/kleur-to-util-styletext/tests/workflow/commonjs-require-colors/input.js diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require.js b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require/expected.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/expected/tests_commonjs_require.js rename to recipes/kleur-to-util-styletext/tests/workflow/commonjs-require/expected.js diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require.js b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require/input.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/input/tests_commonjs_require.js rename to recipes/kleur-to-util-styletext/tests/workflow/commonjs-require/input.js diff --git a/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-await/expected.js b/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-await/expected.js new file mode 100644 index 00000000..dea8f097 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-await/expected.js @@ -0,0 +1,4 @@ +const { styleText } = await import("node:util"); + +console.log(styleText("red", "Error")); +console.log(styleText(["bold", "cyan"], "Info")); diff --git a/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-await/input.js b/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-await/input.js new file mode 100644 index 00000000..ee636d93 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-await/input.js @@ -0,0 +1,4 @@ +const kleur = await import("kleur"); + +console.log(kleur.red("Error")); +console.log(kleur.bold().cyan("Info")); diff --git a/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-then/expected.js b/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-then/expected.js new file mode 100644 index 00000000..16a71f9e --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-then/expected.js @@ -0,0 +1,4 @@ +import("node:util").then(({ styleText }) => { + console.log(styleText("red", "Error")); + console.log(styleText(["bold", "green"], "OK")); +}); diff --git a/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-then/input.js b/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-then/input.js new file mode 100644 index 00000000..a54e8670 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/dynamic-import-then/input.js @@ -0,0 +1,4 @@ +import("kleur").then((kleur) => { + console.log(kleur.red("Error")); + console.log(kleur.bold().green("OK")); +}); diff --git a/recipes/kleur-to-util-styletext/tests/workflow/esm-alias-import/expected.js b/recipes/kleur-to-util-styletext/tests/workflow/esm-alias-import/expected.js new file mode 100644 index 00000000..0ce51fb1 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/esm-alias-import/expected.js @@ -0,0 +1,4 @@ +import { styleText } from "node:util"; + +console.log(styleText("red", "Error")); +console.log(styleText(["bgBlue", "white"], "Info")); diff --git a/recipes/kleur-to-util-styletext/tests/workflow/esm-alias-import/input.js b/recipes/kleur-to-util-styletext/tests/workflow/esm-alias-import/input.js new file mode 100644 index 00000000..0b9aa8d1 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/esm-alias-import/input.js @@ -0,0 +1,4 @@ +import { red as danger, bgBlue as panel, white } from "kleur/colors"; + +console.log(danger("Error")); +console.log(panel(white("Info"))); diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_namespace_import.js b/recipes/kleur-to-util-styletext/tests/workflow/namespace-import/expected.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/expected/tests_namespace_import.js rename to recipes/kleur-to-util-styletext/tests/workflow/namespace-import/expected.js diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_namespace_import.js b/recipes/kleur-to-util-styletext/tests/workflow/namespace-import/input.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/input/tests_namespace_import.js rename to recipes/kleur-to-util-styletext/tests/workflow/namespace-import/input.js diff --git a/recipes/kleur-to-util-styletext/tests/expected/tests_unsupported_features.js b/recipes/kleur-to-util-styletext/tests/workflow/unsupported-features/expected.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/expected/tests_unsupported_features.js rename to recipes/kleur-to-util-styletext/tests/workflow/unsupported-features/expected.js diff --git a/recipes/kleur-to-util-styletext/tests/input/tests_unsupported_features.js b/recipes/kleur-to-util-styletext/tests/workflow/unsupported-features/input.js similarity index 100% rename from recipes/kleur-to-util-styletext/tests/input/tests_unsupported_features.js rename to recipes/kleur-to-util-styletext/tests/workflow/unsupported-features/input.js From 0c8e8c2dc5bda06102669ea2eacc23d15be5f670 Mon Sep 17 00:00:00 2001 From: Herrtian <70463940+Herrtian@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:45:37 +0200 Subject: [PATCH 3/6] fix(kleur): address remaining review comments --- .../kleur-to-util-styletext/src/workflow.ts | 66 ++----------------- 1 file changed, 7 insertions(+), 59 deletions(-) diff --git a/recipes/kleur-to-util-styletext/src/workflow.ts b/recipes/kleur-to-util-styletext/src/workflow.ts index 72453cce..360c52bb 100644 --- a/recipes/kleur-to-util-styletext/src/workflow.ts +++ b/recipes/kleur-to-util-styletext/src/workflow.ts @@ -1,7 +1,7 @@ import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; +import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies'; 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'; const kleurColorsBinding = 'kleur/colors'; @@ -69,59 +69,7 @@ const COMPAT_MAP: Record = { overline: 'overlined', }; -/** - * Kleur style methods that can be mapped to util.styleText. - */ -const SUPPORTED_METHODS = new Set([ - 'black', - 'red', - 'green', - 'yellow', - 'blue', - 'magenta', - 'cyan', - 'white', - 'gray', - 'grey', - 'blackBright', - 'redBright', - 'greenBright', - 'yellowBright', - 'blueBright', - 'magentaBright', - 'cyanBright', - 'whiteBright', - 'bgBlack', - 'bgRed', - 'bgGreen', - 'bgYellow', - 'bgBlue', - 'bgMagenta', - 'bgCyan', - 'bgWhite', - 'bgGray', - 'bgGrey', - 'bgBlackBright', - 'bgRedBright', - 'bgGreenBright', - 'bgYellowBright', - 'bgBlueBright', - 'bgMagentaBright', - 'bgCyanBright', - 'bgWhiteBright', - 'reset', - 'bold', - 'italic', - 'underline', - 'strikethrough', - 'hidden', - 'dim', - 'overlined', - 'blink', - 'inverse', - 'doubleunderline', - 'framed', -]); +const UNSUPPORTED_METHODS = new Set(['$', 'enabled']); /** * Maps kleur method names that have a different util.styleText spelling. @@ -131,17 +79,17 @@ function mapStyle(style: string): string { } /** - * Returns whether a kleur method maps to a util.styleText style. + * Returns whether a kleur method needs manual migration. */ -function isSupportedMethod(method: string): boolean { - return SUPPORTED_METHODS.has(mapStyle(method)); +function isUnsupportedMethod(method: string): boolean { + return UNSUPPORTED_METHODS.has(method); } /** * Returns the style methods that should be left for manual migration. */ function getUnsupportedMethods(styles: string[]): string[] { - return styles.filter((style) => !isSupportedMethod(style)); + return styles.filter((style) => isUnsupportedMethod(style)); } /** @@ -254,7 +202,7 @@ function processKleurColorsImports( if (!destructuredNames.length) return; const unsupportedNames = destructuredNames.filter( - (name) => !isSupportedMethod(name.imported), + (name) => isUnsupportedMethod(name.imported), ); if (unsupportedNames.length) { From b9073868596e0c9cb6b15ba0b2d3c69a514a74f0 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:53:53 +0200 Subject: [PATCH 4/6] applied suggestion --- package-lock.json | 2 +- recipes/kleur-to-util-styletext/package.json | 2 +- .../kleur-to-util-styletext/src/workflow.ts | 22 +++++++------------ 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index f199084f..e0338ee8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1011,7 +1011,7 @@ "@nodejs/codemod-utils": "*" }, "devDependencies": { - "@codemod.com/jssg-types": "^1.6.1" + "@codemod.com/jssg-types": "^1.6.3" } }, "recipes/mocha-to-node-test-runner": { diff --git a/recipes/kleur-to-util-styletext/package.json b/recipes/kleur-to-util-styletext/package.json index 0c6366ce..9efb863d 100644 --- a/recipes/kleur-to-util-styletext/package.json +++ b/recipes/kleur-to-util-styletext/package.json @@ -18,7 +18,7 @@ "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.1" + "@codemod.com/jssg-types": "^1.6.3" }, "dependencies": { "@nodejs/codemod-utils": "*" diff --git a/recipes/kleur-to-util-styletext/src/workflow.ts b/recipes/kleur-to-util-styletext/src/workflow.ts index 360c52bb..95b3a31b 100644 --- a/recipes/kleur-to-util-styletext/src/workflow.ts +++ b/recipes/kleur-to-util-styletext/src/workflow.ts @@ -20,6 +20,7 @@ type StyleCall = { export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; + const statements = [ ...getModuleDependencies(root, kleurBinding).map((statement) => ({ moduleName: kleurBinding, @@ -65,24 +66,17 @@ export default function transform(root: SgRoot): string | null { return rootNode.commitEdits(edits); } -const COMPAT_MAP: Record = { - overline: 'overlined', -}; - -const UNSUPPORTED_METHODS = new Set(['$', 'enabled']); - -/** - * Maps kleur method names that have a different util.styleText spelling. - */ function mapStyle(style: string): string { - return COMPAT_MAP[style] || style; + if (style === 'overline') return 'overlined' + + return style } -/** - * Returns whether a kleur method needs manual migration. - */ + function isUnsupportedMethod(method: string): boolean { - return UNSUPPORTED_METHODS.has(method); + if(method === '$' || method === 'enabled') return true + + return false } /** From 32e438e1fce6e85f4b1d903d87c303b00ada7fe8 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:56:35 +0200 Subject: [PATCH 5/6] Update package-lock.json --- package-lock.json | 174 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/package-lock.json b/package-lock.json index e0338ee8..a34b6425 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,8 @@ }, "node_modules/@ast-grep/lang-bash": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@ast-grep/lang-bash/-/lang-bash-0.0.8.tgz", + "integrity": "sha512-qFykSsOdBv35I1rtt2PstJ4luzV9J1YCiFZBgySI8UPOh+TFnmpdlq9CrryNOj0Q7f6zZhc1OIau+/mUSs6HAg==", "dev": true, "hasInstallScript": true, "license": "ISC", @@ -38,6 +40,8 @@ }, "node_modules/@ast-grep/lang-json": { "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@ast-grep/lang-json/-/lang-json-0.0.7.tgz", + "integrity": "sha512-8wiH+B4Rih8/vHsUB4DOTXnQHMxfWhaBygUEZ036kfr1T9WFvx5E7QYd2WX2Wm1nFCzUmWbIfsoE+cR2G9SJlA==", "dev": true, "hasInstallScript": true, "license": "ISC", @@ -55,6 +59,8 @@ }, "node_modules/@ast-grep/napi": { "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@ast-grep/napi/-/napi-0.45.0.tgz", + "integrity": "sha512-5dQqAVZeHfdQsXjH9FCn+A8a1tb+GVa3Asdq6cKP4l7O0KqH/gmWLswvSaPx7twWfpUwHtGpX7L9frPEAqfNng==", "dev": true, "license": "MIT", "engines": { @@ -74,6 +80,8 @@ }, "node_modules/@ast-grep/napi-darwin-arm64": { "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-darwin-arm64/-/napi-darwin-arm64-0.45.0.tgz", + "integrity": "sha512-yRf7pj+KFgCLw7mA39MjwrTu9zUnz7Kp7f/y8NrZ8Z7b3nI+eBQRvqwdG85J3nR8IJGXLbGBtCBLkrmYtAxWpQ==", "cpu": [ "arm64" ], @@ -87,13 +95,165 @@ "node": ">= 10" } }, + "node_modules/@ast-grep/napi-darwin-x64": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-darwin-x64/-/napi-darwin-x64-0.45.0.tgz", + "integrity": "sha512-rS4PI6qbi828AuvnxT5Yh7rVsy8mAGiN5ruLdi99Po8Ll3v+01x3OXEC/px/816/72WFCXGWacaZ17otowgA1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-linux-arm64-gnu": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-linux-arm64-gnu/-/napi-linux-arm64-gnu-0.45.0.tgz", + "integrity": "sha512-RD0B8fwENAVfQ54+M+l4h0GtIWReefuNXohzSeu9QkKm12vsui9LLNkS5bBdYko/riW5WSeGwgToGmVLxaJBTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-linux-arm64-musl": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-linux-arm64-musl/-/napi-linux-arm64-musl-0.45.0.tgz", + "integrity": "sha512-Q4GCYojuHVM3hfLOZ9DaOQmhMTUEHYmGhkWavPQyYZGO0/cc0DqSqOqewBPpP/sM9Q2X5ZYAo0HM9nsoklEm/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-linux-x64-gnu": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-linux-x64-gnu/-/napi-linux-x64-gnu-0.45.0.tgz", + "integrity": "sha512-yRrrvPaC0xt/kSRCrZctHwaosgEKGjSsU68yb4xqToC8Ue0TdLm0VuHjaCevzonRvO602NwKyyzYXtpFLdMzfw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-linux-x64-musl": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-linux-x64-musl/-/napi-linux-x64-musl-0.45.0.tgz", + "integrity": "sha512-Z80/+rdrk0xxGwbYkw14/xZJ6G1nA+6R5xqiWjq4UzoNs+azQ5+zOQAuD2m/hj+asbZxcA/KD+SXVaWX0+QWgA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-win32-arm64-msvc": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-win32-arm64-msvc/-/napi-win32-arm64-msvc-0.45.0.tgz", + "integrity": "sha512-gfuDq5TEEZGjV0cpj9queABM0E2y/WSwloE38Og1xdHPYmxQNLaCLayfqvtBzgG911NEVoD8+DCeEAYLcgByng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-win32-ia32-msvc": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-win32-ia32-msvc/-/napi-win32-ia32-msvc-0.45.0.tgz", + "integrity": "sha512-BzysPG/tobpa4YBQ7VHPvvxPth6o3ct4LUS8A9hG0rvYdMGi5DKZya9nxSgpY/8yuxQsi9VSNAzWnCamnKWReg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ast-grep/napi-win32-x64-msvc": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@ast-grep/napi-win32-x64-msvc/-/napi-win32-x64-msvc-0.45.0.tgz", + "integrity": "sha512-sBAdQUZHrZL4JECMPWtz+aFF7nK0vUG4xQ68w8UGkqKbA//8JYtddpTtpJ1hM9nYfZupZN/saprkIMWjp7cqbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@ast-grep/setup-lang": { "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@ast-grep/setup-lang/-/setup-lang-0.0.6.tgz", + "integrity": "sha512-aSYZNF5nGQMxnwiA3Lcc8zi0AtpGlug47ADgqCgP/2n7p922FbnW7vo1rbW4kKjW72B/3mDdN7uvYidv8JnPnw==", "dev": true, "license": "ISC" }, "node_modules/@biomejs/biome": { "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.6.tgz", + "integrity": "sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -119,6 +279,8 @@ }, "node_modules/@biomejs/cli-darwin-arm64": { "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw==", "cpu": [ "arm64" ], @@ -265,6 +427,8 @@ }, "node_modules/@codemod.com/jssg-types": { "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@codemod.com/jssg-types/-/jssg-types-1.6.3.tgz", + "integrity": "sha512-b5829ixO5TdGL5xg5YcL9YfiLC3WwFZU+8Zx78NpsHlJ1iySbqfA0g0psqNXv8ss/r7hdpy2KiBXm3GE6bN5bQ==", "dev": true, "license": "Apache-2.0" }, @@ -422,6 +586,8 @@ }, "node_modules/@types/node": { "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", "dev": true, "license": "MIT", "dependencies": { @@ -447,6 +613,8 @@ }, "node_modules/@typescript/typescript-darwin-arm64": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", "cpu": [ "arm64" ], @@ -768,6 +936,8 @@ }, "node_modules/dedent": { "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -780,6 +950,8 @@ }, "node_modules/typescript": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -813,6 +985,8 @@ }, "node_modules/undici-types": { "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, From 3f3e55725a4d5e5c4443b3fcdafdbb1a1f0aa3d4 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:01:59 +0200 Subject: [PATCH 6/6] Update README.md --- recipes/kleur-to-util-styletext/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/recipes/kleur-to-util-styletext/README.md b/recipes/kleur-to-util-styletext/README.md index 1624f84c..e511aeb3 100644 --- a/recipes/kleur-to-util-styletext/README.md +++ b/recipes/kleur-to-util-styletext/README.md @@ -37,3 +37,5 @@ This recipe migrates from the external `kleur` package to Node.js built-in `util ## Limitations - Manual migration is required for `kleur.enabled`, `$` from `kleur/colors`, and other APIs that are not direct style functions + +