diff --git a/package-lock.json b/package-lock.json index a3ac6e9a..49d224df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -424,6 +424,10 @@ "resolved": "utils", "link": true }, + "node_modules/@nodejs/colors-to-util-styletext": { + "resolved": "recipes/colors-to-util-styletext", + "link": true + }, "node_modules/@nodejs/create-require-from-path": { "resolved": "recipes/create-require-from-path", "link": true @@ -750,6 +754,17 @@ "@codemod.com/jssg-types": "^1.6.1" } }, + "recipes/colors-to-util-styletext": { + "name": "@nodejs/colors-to-util-styletext", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.6.0" + } + }, "recipes/create-require-from-path": { "name": "@nodejs/create-require-from-path", "version": "1.1.2", diff --git a/recipes/colors-to-util-styletext/README.md b/recipes/colors-to-util-styletext/README.md new file mode 100644 index 00000000..0dfd3a90 --- /dev/null +++ b/recipes/colors-to-util-styletext/README.md @@ -0,0 +1,33 @@ +# Colors to util.styleText + +This recipe migrates compatible `colors` package usage to Node.js built-in `util.styleText`. + +## Examples + +```diff +- const colors = require('colors'); ++ const { styleText } = require('node:util'); +- console.log('Error message'.red); ++ console.log(styleText('red', 'Error message')); +``` + +```diff +- import colors from 'colors'; ++ import { styleText } from 'node:util'; +- console.log('Success'.green.bold); ++ console.log(styleText(['green', 'bold'], 'Success')); +``` + +```diff +- const colors = require('colors/safe'); ++ const { styleText } = require('node:util'); +- console.log(colors.green('Success message')); ++ console.log(styleText('green', 'Success message')); +``` + +## Compatibility + +- Removes the `colors` dependency from package.json automatically. +- Supports string prototype colors and modifiers, including chained styles. +- Supports `colors/safe` namespace calls. +- Unsupported extras such as `rainbow`, `zebra`, `america`, `trap`, and `random` are left unchanged and reported for manual review. diff --git a/recipes/colors-to-util-styletext/codemod.yaml b/recipes/colors-to-util-styletext/codemod.yaml new file mode 100644 index 00000000..ef0889f4 --- /dev/null +++ b/recipes/colors-to-util-styletext/codemod.yaml @@ -0,0 +1,26 @@ +schema_version: "1.0" +name: "@nodejs/colors-to-util-styletext" +version: 1.0.0 +capabilities: + - fs + - child_process +description: Migrate from the colors 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: + - nodejs + - colors + - styleText + +registry: + access: public + visibility: public diff --git a/recipes/colors-to-util-styletext/package.json b/recipes/colors-to-util-styletext/package.json new file mode 100644 index 00000000..55ad416f --- /dev/null +++ b/recipes/colors-to-util-styletext/package.json @@ -0,0 +1,26 @@ +{ + "name": "@nodejs/colors-to-util-styletext", + "version": "1.0.0", + "description": "Migrate from the colors 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", + "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/colors-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/colors-to-util-styletext/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.6.0" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/colors-to-util-styletext/src/remove-dependencies.ts b/recipes/colors-to-util-styletext/src/remove-dependencies.ts new file mode 100644 index 00000000..581114b0 --- /dev/null +++ b/recipes/colors-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(['colors', '@types/colors'], { + packageJsonPath: root.filename(), + runInstall: false, + persistFileWrite: false, + }); +}; + +export default transform; diff --git a/recipes/colors-to-util-styletext/src/workflow.ts b/recipes/colors-to-util-styletext/src/workflow.ts new file mode 100644 index 00000000..5013da97 --- /dev/null +++ b/recipes/colors-to-util-styletext/src/workflow.ts @@ -0,0 +1,642 @@ +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'; +import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; + +const colorsModule = 'colors'; +const safeColorsModule = 'colors/safe'; + +const COMPAT_MAP: Record = { + bgGrey: 'bgGray', + grey: 'gray', +}; + +const SUPPORTED_STYLES = 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', + 'dim', + 'italic', + 'underline', + 'inverse', + 'hidden', + 'strikethrough', +]); + +const UNSUPPORTED_EXTRAS = new Set([ + 'america', + 'rainbow', + 'random', + 'trap', + 'zebra', +]); + +/** Converts supported colors imports and usages to util.styleText. */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + + const colorsStatements = getModuleDependencies(root, colorsModule); + const safeColorsStatements = getModuleDependencies(root, safeColorsModule); + const safeThenCalls = getSafeDynamicImportThenCalls(rootNode); + + if ( + !colorsStatements.length && + !safeColorsStatements.length && + !safeThenCalls.length + ) { + return null; + } + + for (const statement of safeColorsStatements) { + processStatement(rootNode, statement, edits, true); + } + + for (const thenCall of safeThenCalls) { + processDynamicImportThenCall(thenCall, edits); + } + + for (const statement of colorsStatements) { + processStatement(rootNode, statement, edits, false); + } + + if (!edits.length) return null; + + return rootNode.commitEdits(edits); +} + +/** Rewrites one colors dependency statement and its related usages. */ +function processStatement( + rootNode: SgNode, + statement: SgNode, + edits: Edit[], + isSafeImport: boolean, +): void { + const editCountBeforeStatement = edits.length; + const destructuredNames = getDestructuredNames(statement); + const blockedPrototypeBases = new Set(); + + if (destructuredNames.length > 0) { + processDestructuredSafeCalls(rootNode, destructuredNames, edits); + } else { + const binding = resolveOptionalBinding(statement); + + if (binding) { + blockedPrototypeBases.add(binding); + processSafeNamespaceCalls(rootNode, binding, edits); + } + } + + if (!isSafeImport) { + processPrototypeStyles(rootNode, edits, blockedPrototypeBases); + } + + if (edits.length > editCountBeforeStatement) { + const importReplacement = createImportReplacement(statement); + + if (importReplacement) { + edits.push(statement.replace(importReplacement)); + } + } else if ( + isSafeImport && + ['import_statement', 'variable_declarator'].includes(statement.kind()) + ) { + edits.push(removeUnusedSafeImport(statement)); + } +} + +/** Returns the local binding for namespace imports/requires when one exists. */ +function resolveOptionalBinding(statement: SgNode): string | undefined { + if ( + statement.is('import_statement') && + !statement.find({ rule: { kind: 'import_clause' } }) + ) { + return undefined; + } + + return resolveBindingPath(statement, '$'); +} + +/** Collects imported colors names from destructured imports and requires. */ +function getDestructuredNames( + statement: SgNode, +): Array<{ imported: string; local: string }> { + const names: Array<{ imported: string; local: string }> = []; + const statementKind = statement.kind(); + + switch (statementKind) { + case 'import_statement': { + const namedImports = statement.find({ + rule: { kind: 'named_imports' }, + }); + + if (!namedImports) break; + + 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(); + names.push({ imported, local: alias ? alias.text() : imported }); + } + } + break; + } + case 'variable_declarator': { + const nameField = statement.field('name'); + + if (nameField?.kind() !== 'object_pattern') break; + + const properties = nameField.findAll({ + rule: { + any: [ + { kind: 'shorthand_property_identifier_pattern' }, + { kind: 'pair_pattern' }, + ], + }, + }); + + for (const prop of properties) { + const propKind = prop.kind(); + + switch (propKind) { + case 'shorthand_property_identifier_pattern': { + const name = prop.text(); + + names.push({ imported: name, local: name }); + break; + } + case 'pair_pattern': { + const key = prop.field('key'); + const value = prop.field('value'); + + if (key && value) { + names.push({ imported: key.text(), local: value.text() }); + } + break; + } + } + } + break; + } + } + + return names; +} + +/** Rewrites calls that use destructured colors/safe helpers. */ +function processDestructuredSafeCalls( + rootNode: SgNode, + destructuredNames: Array<{ imported: string; local: string }>, + edits: Edit[], +): void { + for (const { imported, local } of destructuredNames) { + const calls = rootNode.findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + any: [ + { kind: 'identifier', pattern: local }, + { + kind: 'member_expression', + has: { + field: 'object', + any: [ + { kind: 'identifier', pattern: local }, + { + kind: 'member_expression', + has: { + field: 'object', + kind: 'identifier', + pattern: local, + }, + }, + ], + }, + }, + ], + }, + }, + }); + + for (const call of calls) { + const functionExpr = call.field('function'); + if (!functionExpr) continue; + + const styles = + functionExpr.kind() === 'identifier' + ? [normalizeStyle(imported)] + : extractNamespaceStyles( + functionExpr, + local, + normalizeStyle(imported), + ); + + replaceSafeCall(rootNode, call, styles, edits); + } + } +} + +/** Rewrites colors/safe usages inside dynamic import .then callbacks. */ +function processDynamicImportThenCall( + thenCall: SgNode, + edits: Edit[], +): void { + const callback = getCallArguments(thenCall)[0]; + + if (!callback) return; + + const destructuredParam = callback.find({ + rule: { kind: 'object_pattern' }, + }); + + if (!destructuredParam) return; + + const destructuredNames = getNamesFromObjectPattern(destructuredParam); + const editCountBeforeCall = edits.length; + + processDestructuredSafeCalls(callback, destructuredNames, edits); + + if (edits.length > editCountBeforeCall) { + const dynamicImport = thenCall.field('function')?.field('object'); + + if (dynamicImport) { + edits.push(dynamicImport.replace('import("node:util")')); + } + edits.push(destructuredParam.replace('{ styleText }')); + } +} + +/** Rewrites calls accessed through a colors/safe namespace binding. */ +function processSafeNamespaceCalls( + 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?.kind() !== 'member_expression') continue; + + const styles = extractNamespaceStyles(functionExpr, binding); + replaceSafeCall(rootNode, call, styles, edits); + } +} + +/** Replaces a safe colors call with util.styleText when it can be mapped. */ +function replaceSafeCall( + rootNode: SgNode, + call: SgNode, + styles: string[], + edits: Edit[], +): void { + if (!styles.length) return; + + if (hasUnsupportedStyles(styles)) { + warnOnUnsupportedStyle(styles, rootNode, call); + return; + } + + const textArg = getFirstCallArgument(call); + + if (!textArg) return; + + edits.push(call.replace(createStyleTextReplacement(styles, textArg))); +} + +/** Rewrites colors prototype style chains such as "text".green. */ +function processPrototypeStyles( + rootNode: SgNode, + edits: Edit[], + blockedPrototypeBases: Set, +): void { + const memberExpressions = rootNode.findAll({ + rule: { kind: 'member_expression' }, + }); + + for (const memberExpression of memberExpressions) { + if ( + isNestedStyleChain(memberExpression) || + isCallFunction(memberExpression) + ) { + continue; + } + + const prototypeStyle = extractPrototypeStyles(memberExpression); + + if (!prototypeStyle || blockedPrototypeBases.has(prototypeStyle.text)) { + continue; + } + + if (hasUnsupportedStyles(prototypeStyle.styles)) { + warnOnUnsupportedStyle(prototypeStyle.styles, rootNode, memberExpression); + continue; + } + + edits.push( + memberExpression.replace( + createStyleTextReplacement(prototypeStyle.styles, prototypeStyle.text), + ), + ); + } +} + +/** Reads the style chain from a safe namespace call. */ +function extractNamespaceStyles( + node: SgNode, + binding: string, + initialStyle?: string, +): string[] { + const styles = initialStyle ? [initialStyle] : []; + + /** Walks a member chain until it reaches the expected namespace binding. */ + function traverse(current: SgNode): boolean { + const object = current.field('object'); + const property = current.field('property'); + + if (!object || property?.kind() !== 'property_identifier') return false; + + const propertyName = normalizeStyle(property.text()); + const objectKind = object.kind(); + + if (objectKind === 'identifier' && object.text() === binding) { + styles.push(propertyName); + return true; + } + + if (objectKind === 'member_expression' && traverse(object)) { + styles.push(propertyName); + return true; + } + + return false; + } + + traverse(node); + + return styles; +} + +/** Reads the style chain from a prototype access expression. */ +function extractPrototypeStyles( + node: SgNode, +): { text: string; styles: string[] } | null { + const property = node.field('property'); + const object = node.field('object'); + + if (!object || property?.kind() !== 'property_identifier') return null; + + const style = normalizeStyle(property.text()); + + if (!isSupportedOrKnownUnsupported(style)) return null; + + if (object.is('member_expression')) { + const nested = extractPrototypeStyles(object); + + if (!nested) return null; + + return { text: nested.text, styles: [...nested.styles, style] }; + } + + if (!isSupportedPrototypeBase(object)) return null; + + return { text: object.text(), styles: [style] }; +} + +/** Checks whether a node can safely be used as the text argument. */ +function isSupportedPrototypeBase(node: SgNode): boolean { + return [ + 'identifier', + 'parenthesized_expression', + 'string', + 'template_string', + ].includes(node.kind()); +} + +/** Normalizes colors aliases to util.styleText names. */ +function normalizeStyle(style: string): string { + return COMPAT_MAP[style] || style; +} + +/** Keeps traversal limited to colors styles that are known to the recipe. */ +function isSupportedOrKnownUnsupported(style: string): boolean { + return SUPPORTED_STYLES.has(style) || UNSUPPORTED_EXTRAS.has(style); +} + +/** Detects styles that colors supports but util.styleText does not. */ +function hasUnsupportedStyles(styles: string[]): boolean { + return styles.some((style) => !SUPPORTED_STYLES.has(style)); +} + +/** Returns the first real argument for a colors/safe call. */ +function getFirstCallArgument(call: SgNode): string | null { + return getCallArguments(call)[0]?.text() ?? null; +} + +/** Returns named call arguments. */ +function getCallArguments(call: SgNode): Array> { + const args = call.field('arguments'); + + if (!args) return []; + + return args.children().filter((child) => child.isNamed()); +} + +/** 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})`; + } + + return `styleText([${styles.map((style) => `"${style}"`).join(', ')}], ${textArg})`; +} + +/** Builds the matching util.styleText import or require replacement. */ +function createImportReplacement(statement: SgNode): string { + const statementKind = statement.kind(); + + switch (statementKind) { + case 'import_statement': + return 'import { styleText } from "node:util";'; + case 'variable_declarator': { + if (statement.field('value')?.kind() === 'await_expression') { + return '{ styleText } = await import("node:util")'; + } + + return '{ styleText } = require("node:util")'; + } + } + + return ''; +} + +/** Finds `import("colors/safe").then(...)` calls. */ +function getSafeDynamicImportThenCalls(rootNode: SgNode): Array> { + return rootNode + .findAll({ + rule: { kind: 'call_expression' }, + }) + .filter((call) => { + const functionExpr = call.field('function'); + + if (functionExpr?.kind() !== 'member_expression') return false; + + const object = functionExpr.field('object'); + const property = functionExpr.field('property'); + + return property?.text() === 'then' && isSafeDynamicImport(object); + }); +} + +/** Checks for `import("colors/safe")`. */ +function isSafeDynamicImport(node: SgNode | null): boolean { + if (node?.kind() !== 'call_expression') return false; + + const functionExpr = node.field('function'); + const moduleName = getFirstCallArgument(node); + + return ( + functionExpr?.text() === 'import' && + (moduleName === '"colors/safe"' || moduleName === "'colors/safe'") + ); +} + +/** Removes an unused colors/safe dependency statement. */ +function removeUnusedSafeImport(statement: SgNode): Edit { + const parent = statement.parent(); + + if ( + parent && + ['lexical_declaration', 'variable_declaration'].includes(parent.kind()) + ) { + return parent.replace(''); + } + + return statement.replace(''); +} + +/** Returns destructured names from an object pattern node. */ +function getNamesFromObjectPattern( + objectPattern: SgNode, +): Array<{ imported: string; local: string }> { + const names: Array<{ imported: string; local: string }> = []; + const properties = objectPattern.findAll({ + rule: { + any: [ + { kind: 'shorthand_property_identifier_pattern' }, + { kind: 'pair_pattern' }, + ], + }, + }); + + for (const prop of properties) { + const propKind = prop.kind(); + + switch (propKind) { + case 'shorthand_property_identifier_pattern': { + const name = prop.text(); + + names.push({ imported: name, local: name }); + break; + } + case 'pair_pattern': { + const key = prop.field('key'); + const value = prop.field('value'); + + if (key && value) { + names.push({ imported: key.text(), local: value.text() }); + } + break; + } + } + } + + return names; +} + +/** Skips intermediate members so only the full style chain is replaced. */ +function isNestedStyleChain(node: SgNode): boolean { + const parent = node.parent(); + + if (parent?.kind() !== 'member_expression') return false; + + const object = parent.field('object'); + const property = parent.field('property'); + + return ( + object?.text() === node.text() && + property?.kind() === 'property_identifier' && + isSupportedOrKnownUnsupported(normalizeStyle(property.text())) + ); +} + +/** Skips member expressions that are already part of a call expression. */ +function isCallFunction(node: SgNode): boolean { + const parent = node.parent(); + + if (parent?.kind() !== 'call_expression') return false; + + return parent.field('function')?.text() === node.text(); +} + +/** Emits a review warning for colors styles that cannot be mapped. */ +function warnOnUnsupportedStyle( + styles: string[], + rootNode: SgNode, + node: SgNode, +): void { + const filename = rootNode.getRoot().filename(); + const { start } = node.range(); + const unsupported = styles.filter((style) => !SUPPORTED_STYLES.has(style)); + + for (const style of unsupported) { + console.warn( + `${filename}:${start.line}:${start.column}: uses colors style '${style}' that does not have any equivalent in util.styleText; please review this line`, + ); + } +} diff --git a/recipes/colors-to-util-styletext/tests/basic-property/expected.js b/recipes/colors-to-util-styletext/tests/basic-property/expected.js new file mode 100644 index 00000000..10711813 --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/basic-property/expected.js @@ -0,0 +1,3 @@ +const { styleText } = require("node:util"); + +console.log(styleText("red", "Error message")); diff --git a/recipes/colors-to-util-styletext/tests/basic-property/input.js b/recipes/colors-to-util-styletext/tests/basic-property/input.js new file mode 100644 index 00000000..84ffb60a --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/basic-property/input.js @@ -0,0 +1,3 @@ +const colors = require("colors"); + +console.log("Error message".red); diff --git a/recipes/colors-to-util-styletext/tests/chained-property/expected.mjs b/recipes/colors-to-util-styletext/tests/chained-property/expected.mjs new file mode 100644 index 00000000..6ee4176f --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/chained-property/expected.mjs @@ -0,0 +1,3 @@ +import { styleText } from "node:util"; + +console.log(styleText(["green", "bold"], "Success message")); diff --git a/recipes/colors-to-util-styletext/tests/chained-property/input.mjs b/recipes/colors-to-util-styletext/tests/chained-property/input.mjs new file mode 100644 index 00000000..97be8c8f --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/chained-property/input.mjs @@ -0,0 +1,3 @@ +import colors from "colors"; + +console.log("Success message".green.bold); diff --git a/recipes/colors-to-util-styletext/tests/concat-and-template/expected.js b/recipes/colors-to-util-styletext/tests/concat-and-template/expected.js new file mode 100644 index 00000000..9934acb5 --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/concat-and-template/expected.js @@ -0,0 +1,6 @@ +const { styleText } = require("node:util"); + +const name = "World"; +const action = "ready"; +console.log("Hello, " + styleText("green", name) + "!"); +console.log(`${styleText("blue", "[INFO]")} User ${styleText("green", name)} is ${styleText("yellow", action)}`); diff --git a/recipes/colors-to-util-styletext/tests/concat-and-template/input.js b/recipes/colors-to-util-styletext/tests/concat-and-template/input.js new file mode 100644 index 00000000..a1f9edac --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/concat-and-template/input.js @@ -0,0 +1,6 @@ +const colors = require("colors"); + +const name = "World"; +const action = "ready"; +console.log("Hello, " + name.green + "!"); +console.log(`${"[INFO]".blue} User ${name.green} is ${action.yellow}`); diff --git a/recipes/colors-to-util-styletext/tests/destructured-safe/expected.js b/recipes/colors-to-util-styletext/tests/destructured-safe/expected.js new file mode 100644 index 00000000..617bc05c --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/destructured-safe/expected.js @@ -0,0 +1,4 @@ +const { styleText } = require("node:util"); + +console.log(styleText("red", "Error message")); +console.log(styleText("green", "Success message")); diff --git a/recipes/colors-to-util-styletext/tests/destructured-safe/input.js b/recipes/colors-to-util-styletext/tests/destructured-safe/input.js new file mode 100644 index 00000000..b927409f --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/destructured-safe/input.js @@ -0,0 +1,4 @@ +const { red, green: success } = require("colors/safe"); + +console.log(red("Error message")); +console.log(success("Success message")); diff --git a/recipes/colors-to-util-styletext/tests/remove-dependencies/remove-colors/expected.json b/recipes/colors-to-util-styletext/tests/remove-dependencies/remove-colors/expected.json new file mode 100644 index 00000000..46578268 --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/remove-dependencies/remove-colors/expected.json @@ -0,0 +1,10 @@ +{ + "name": "fixture", + "version": "1.0.0", + "dependencies": { + "kleur": "^4.1.5" + }, + "devDependencies": { + "typescript": "^5.6.0" + } +} diff --git a/recipes/colors-to-util-styletext/tests/remove-dependencies/remove-colors/input.json b/recipes/colors-to-util-styletext/tests/remove-dependencies/remove-colors/input.json new file mode 100644 index 00000000..33682f2c --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/remove-dependencies/remove-colors/input.json @@ -0,0 +1,12 @@ +{ + "name": "fixture", + "version": "1.0.0", + "dependencies": { + "colors": "^1.4.0", + "kleur": "^4.1.5" + }, + "devDependencies": { + "@types/colors": "^1.2.1", + "typescript": "^5.6.0" + } +} diff --git a/recipes/colors-to-util-styletext/tests/safe-chained-esm/expected.mjs b/recipes/colors-to-util-styletext/tests/safe-chained-esm/expected.mjs new file mode 100644 index 00000000..6ee4176f --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/safe-chained-esm/expected.mjs @@ -0,0 +1,3 @@ +import { styleText } from "node:util"; + +console.log(styleText(["green", "bold"], "Success message")); diff --git a/recipes/colors-to-util-styletext/tests/safe-chained-esm/input.mjs b/recipes/colors-to-util-styletext/tests/safe-chained-esm/input.mjs new file mode 100644 index 00000000..ff00f655 --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/safe-chained-esm/input.mjs @@ -0,0 +1,3 @@ +import colors from "colors/safe"; + +console.log(colors.green.bold("Success message")); diff --git a/recipes/colors-to-util-styletext/tests/safe-commonjs/expected.js b/recipes/colors-to-util-styletext/tests/safe-commonjs/expected.js new file mode 100644 index 00000000..72ec84d5 --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/safe-commonjs/expected.js @@ -0,0 +1,3 @@ +const { styleText } = require("node:util"); + +console.log(styleText("green", "Success message")); diff --git a/recipes/colors-to-util-styletext/tests/safe-commonjs/input.js b/recipes/colors-to-util-styletext/tests/safe-commonjs/input.js new file mode 100644 index 00000000..131f221b --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/safe-commonjs/input.js @@ -0,0 +1,3 @@ +const colors = require("colors/safe"); + +console.log(colors.green("Success message")); diff --git a/recipes/colors-to-util-styletext/tests/safe-dynamic-import-await/expected.mjs b/recipes/colors-to-util-styletext/tests/safe-dynamic-import-await/expected.mjs new file mode 100644 index 00000000..4a457864 --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/safe-dynamic-import-await/expected.mjs @@ -0,0 +1,4 @@ +const { styleText } = await import("node:util"); + +console.log(styleText("red", "Error message")); +console.log(styleText("green", "Success message")); diff --git a/recipes/colors-to-util-styletext/tests/safe-dynamic-import-await/input.mjs b/recipes/colors-to-util-styletext/tests/safe-dynamic-import-await/input.mjs new file mode 100644 index 00000000..ab50b3a9 --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/safe-dynamic-import-await/input.mjs @@ -0,0 +1,4 @@ +const { red, green: success } = await import("colors/safe"); + +console.log(red("Error message")); +console.log(success("Success message")); diff --git a/recipes/colors-to-util-styletext/tests/safe-dynamic-import-then/expected.mjs b/recipes/colors-to-util-styletext/tests/safe-dynamic-import-then/expected.mjs new file mode 100644 index 00000000..7464a125 --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/safe-dynamic-import-then/expected.mjs @@ -0,0 +1,3 @@ +import("node:util").then(({ styleText }) => { + console.log(styleText("green", "Success message")); +}); diff --git a/recipes/colors-to-util-styletext/tests/safe-dynamic-import-then/input.mjs b/recipes/colors-to-util-styletext/tests/safe-dynamic-import-then/input.mjs new file mode 100644 index 00000000..aacabd1c --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/safe-dynamic-import-then/input.mjs @@ -0,0 +1,3 @@ +import("colors/safe").then(({ green }) => { + console.log(green("Success message")); +}); diff --git a/recipes/colors-to-util-styletext/tests/safe-unrelated-call/expected.js b/recipes/colors-to-util-styletext/tests/safe-unrelated-call/expected.js new file mode 100644 index 00000000..3f207a1c --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/safe-unrelated-call/expected.js @@ -0,0 +1,3 @@ + + +console.log("plain message"); diff --git a/recipes/colors-to-util-styletext/tests/safe-unrelated-call/input.js b/recipes/colors-to-util-styletext/tests/safe-unrelated-call/input.js new file mode 100644 index 00000000..b2f68913 --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/safe-unrelated-call/input.js @@ -0,0 +1,3 @@ +const colors = require("colors/safe"); + +console.log("plain message"); diff --git a/recipes/colors-to-util-styletext/tests/side-effect-import/expected.mjs b/recipes/colors-to-util-styletext/tests/side-effect-import/expected.mjs new file mode 100644 index 00000000..df48c12f --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/side-effect-import/expected.mjs @@ -0,0 +1,4 @@ +import { styleText } from "node:util"; + +console.log(styleText("red", "Error message")); +console.log(styleText(["yellow", "bold"], "Warning message")); diff --git a/recipes/colors-to-util-styletext/tests/side-effect-import/input.mjs b/recipes/colors-to-util-styletext/tests/side-effect-import/input.mjs new file mode 100644 index 00000000..74ce239f --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/side-effect-import/input.mjs @@ -0,0 +1,4 @@ +import "colors"; + +console.log("Error message".red); +console.log("Warning message".yellow.bold); diff --git a/recipes/colors-to-util-styletext/tests/unsupported-extra/expected.js b/recipes/colors-to-util-styletext/tests/unsupported-extra/expected.js new file mode 100644 index 00000000..04daea23 --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/unsupported-extra/expected.js @@ -0,0 +1,3 @@ +const colors = require("colors"); + +console.log("Party".rainbow); diff --git a/recipes/colors-to-util-styletext/tests/unsupported-extra/input.js b/recipes/colors-to-util-styletext/tests/unsupported-extra/input.js new file mode 100644 index 00000000..04daea23 --- /dev/null +++ b/recipes/colors-to-util-styletext/tests/unsupported-extra/input.js @@ -0,0 +1,3 @@ +const colors = require("colors"); + +console.log("Party".rainbow); diff --git a/recipes/colors-to-util-styletext/workflow.yaml b/recipes/colors-to-util-styletext/workflow.yaml new file mode 100644 index 00000000..76b00334 --- /dev/null +++ b/recipes/colors-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 colors 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 colors dependency + type: automatic + steps: + - name: Detect package manager and remove colors dependency + js-ast-grep: + js_file: src/remove-dependencies.ts + base_path: . + include: + - "**/package.json" + exclude: + - "**/node_modules/**" + language: typescript + capabilities: + - child_process + - fs