diff --git a/package-lock.json b/package-lock.json index a9f400c6..4a177a57 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..1624f84c --- /dev/null +++ b/recipes/kleur-to-util-styletext/README.md @@ -0,0 +1,39 @@ +# 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')); +``` + +## 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 + +- 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/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..0c6366ce --- /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 ./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": { + "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..360c52bb --- /dev/null +++ b/recipes/kleur-to-util-styletext/src/workflow.ts @@ -0,0 +1,530 @@ +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'; + +const kleurBinding = 'kleur'; +const kleurColorsBinding = 'kleur/colors'; + +type StyleCall = { + styles: string[]; + 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[] = []; + 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) { + if (statement.is('expression_statement')) { + edits.push(...createDynamicImportReplacementEdits(statement)); + continue; + } + + const importReplacement = createImportReplacement(statement); + if (importReplacement) { + edits.push(statement.replace(importReplacement)); + } + } + } + + if (!edits.length) return 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; +} + +/** + * Returns whether a kleur method needs manual migration. + */ +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) => isUnsupportedMethod(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 (statementKind === '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 (statementKind === '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; +} + +/** + * Transforms calls that use the default or namespace kleur binding. + */ +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; + + const unsupportedMethods = getUnsupportedMethods(styles); + + if (unsupportedMethods.length) { + for (const method of unsupportedMethods) { + warnOnUnsupportedMethod(method, rootNode, call); + } + continue; + } + + const textArg = getFirstCallArgument(call); + if (!textArg) continue; + + edits.push(call.replace(createStyleTextReplacement(styles, textArg))); + } +} + +/** + * Transforms calls imported from kleur/colors named exports. + */ +function processKleurColorsImports( + rootNode: SgNode, + destructuredNames: Array<{ imported: string; local: string }>, + edits: Edit[], +): void { + if (!destructuredNames.length) return; + + const unsupportedNames = destructuredNames.filter( + (name) => isUnsupportedMethod(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)), + ); + } +} + +/** + * Extracts a kleur style chain from a member expression. + */ +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?.is('property_identifier')) { + return false; + } + + const objKind = obj.kind(); + const propName = mapStyle(prop.text()); + + if (objKind === 'identifier' && obj.text() === binding) { + styles.push(propName); + return true; + } + + if (objKind === 'member_expression' && traverse(obj)) { + styles.push(propName); + return true; + } + + if (objKind === '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; +} + +/** + * Extracts a kleur/colors call and nested style calls. + */ +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(), + }; +} + +/** + * Skips nested kleur/colors calls so only the outer call is replaced. + */ +function hasKleurColorsCallAncestor( + call: SgNode, + styleImports: Map, +): boolean { + let parentNode = call.parent(); + + while (parentNode) { + if ( + parentNode.is('call_expression') && + extractKleurColorsCall(parentNode, styleImports) + ) { + return true; + } + + parentNode = parentNode.parent(); + } + + return false; +} + +/** + * Detects kleur.enabled assignments that need manual migration. + */ +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; +} + +/** + * 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); + + if (args.length === 0) return 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'); + + if (!args) return []; + + return args.children().filter((child) => { + const excluded = [',', '(', ')']; + return !excluded.includes(child.kind()); + }); +} + +/** + * 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})`; + } + + const stylesArray = `[${styles.map((style) => `"${style}"`).join(', ')}]`; + + return `styleText(${stylesArray}, ${textArg})`; +} + +/** + * 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 (statementKind === 'variable_declarator') { + if (statement.field('value')?.kind() === 'await_expression') { + return `{ styleText } = await import("node:util")`; + } + + return `{ styleText } = require("node:util")`; + } + + 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, + 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/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/tests/workflow/basic/expected.js b/recipes/kleur-to-util-styletext/tests/workflow/basic/expected.js new file mode 100644 index 00000000..ab65054c --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/basic/expected.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/workflow/basic/input.js b/recipes/kleur-to-util-styletext/tests/workflow/basic/input.js new file mode 100644 index 00000000..3bc10720 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/basic/input.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/workflow/chained/expected.js b/recipes/kleur-to-util-styletext/tests/workflow/chained/expected.js new file mode 100644 index 00000000..d5c12f76 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/chained/expected.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/workflow/chained/input.js b/recipes/kleur-to-util-styletext/tests/workflow/chained/input.js new file mode 100644 index 00000000..be2a3feb --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/chained/input.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/workflow/colors-import/expected.js b/recipes/kleur-to-util-styletext/tests/workflow/colors-import/expected.js new file mode 100644 index 00000000..c809eec6 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/colors-import/expected.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/workflow/colors-import/input.js b/recipes/kleur-to-util-styletext/tests/workflow/colors-import/input.js new file mode 100644 index 00000000..469833a2 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/colors-import/input.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/workflow/commonjs-require-colors/expected.js b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require-colors/expected.js new file mode 100644 index 00000000..8b5d0dde --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require-colors/expected.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/workflow/commonjs-require-colors/input.js b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require-colors/input.js new file mode 100644 index 00000000..805f2dfc --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require-colors/input.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/workflow/commonjs-require/expected.js b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require/expected.js new file mode 100644 index 00000000..05ea8600 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require/expected.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/workflow/commonjs-require/input.js b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require/input.js new file mode 100644 index 00000000..8287066d --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/commonjs-require/input.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/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/workflow/namespace-import/expected.js b/recipes/kleur-to-util-styletext/tests/workflow/namespace-import/expected.js new file mode 100644 index 00000000..414d3772 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/namespace-import/expected.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/workflow/namespace-import/input.js b/recipes/kleur-to-util-styletext/tests/workflow/namespace-import/input.js new file mode 100644 index 00000000..9abd2c96 --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/namespace-import/input.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/workflow/unsupported-features/expected.js b/recipes/kleur-to-util-styletext/tests/workflow/unsupported-features/expected.js new file mode 100644 index 00000000..3ee83f5a --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/unsupported-features/expected.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/workflow/unsupported-features/input.js b/recipes/kleur-to-util-styletext/tests/workflow/unsupported-features/input.js new file mode 100644 index 00000000..3ee83f5a --- /dev/null +++ b/recipes/kleur-to-util-styletext/tests/workflow/unsupported-features/input.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/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