diff --git a/package-lock.json b/package-lock.json index 34bf48db..0d921646 100644 --- a/package-lock.json +++ b/package-lock.json @@ -429,6 +429,10 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@nodejs/ansi-colors-to-styletext": { + "resolved": "recipes/ansi-colors-to-styletext", + "link": true + }, "node_modules/@nodejs/axios-to-whatwg-fetch": { "resolved": "recipes/axios-to-whatwg-fetch", "link": true @@ -987,6 +991,17 @@ "dev": true, "license": "MIT" }, + "recipes/ansi-colors-to-styletext": { + "name": "@nodejs/ansi-colors-to-styletext", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.6.3" + } + }, "recipes/axios-to-whatwg-fetch": { "name": "@nodejs/axios-to-whatwg-fetch", "dependencies": { diff --git a/recipes/ansi-colors-to-styletext/README.md b/recipes/ansi-colors-to-styletext/README.md new file mode 100644 index 00000000..962930b5 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/README.md @@ -0,0 +1,60 @@ +# ansi-colors to util.styleText + +This recipe migrates from the external `ansi-colors` package to Node.js's built-in `util.styleText` API. It transforms ansi-colors method calls to use the native Node.js styling functionality. + +## Usage + +Run this codemod with: + +```sh +npx codemod @nodejs/ansi-colors-to-styletext +``` + +## Examples + +```diff +- import ac from 'ansi-colors'; ++ import { styleText, stripVTControlCharacters } from 'node:util'; + +- console.log(ac.red('Error message')); ++ console.log(styleText('red', 'Error message')); + +- console.log(ac.green('Success message')); ++ console.log(styleText('green', 'Success message')); + +- console.log(ac.unstyle(ac.bold.blue('Info message'))); ++ console.log(stripVTControlCharacters(styleText(['bold', 'blue'], 'Info message'))); +``` + +```diff +- const ac = require('ansi-colors'); ++ const { styleText } = require('node:util'); + +- console.log(ac.bold.red('Critical error')); ++ console.log(styleText(['bold', 'red'], 'Critical error')); +``` + +```diff +- const { red, blue } = require('ansi-colors'); ++ const { styleText } = require('node:util'); + +- console.log(red('Error')); ++ console.log(styleText('red', 'Error')); + +- console.log(blue('Info')); ++ console.log(styleText('blue', 'Info')); +``` + +## Compatibility + +- **Removes ansi-colors dependency** from package.json automatically +- **Supports all ansi-colors methods**: colors, background colors, text modifiers, and chained styles +- **Unsupported methods**: `enabled`, `visible`, `unstyle`, `alias`, `theme`, `create` (warnings will be shown) + +## Limitations + +- **Runtime toggles** like `ac.enabled = false` require manual intervention +- **Custom themes and aliases** need to be rewritten as plain objects +- **Dynamic imports with `.then()`** are not transformed and require manual migration + + diff --git a/recipes/ansi-colors-to-styletext/codemod.yaml b/recipes/ansi-colors-to-styletext/codemod.yaml new file mode 100644 index 00000000..c21d49af --- /dev/null +++ b/recipes/ansi-colors-to-styletext/codemod.yaml @@ -0,0 +1,28 @@ +schema_version: "1.0" +name: "@nodejs/ansi-colors-to-styletext" +version: 1.0.0 +capabilities: + - fs + - child_process +description: "Migrate from ansi-colors package to Node.js util.styleText API" +author: Shamya Haria +license: MIT +workflow: workflow.yaml +category: migration +repository: https://github.com/nodejs/userland-migrations + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - nodejs + - ansi-colors + - styletext + +registry: + access: public + visibility: public diff --git a/recipes/ansi-colors-to-styletext/package.json b/recipes/ansi-colors-to-styletext/package.json new file mode 100644 index 00000000..43991d9d --- /dev/null +++ b/recipes/ansi-colors-to-styletext/package.json @@ -0,0 +1,26 @@ +{ + "name": "@nodejs/ansi-colors-to-styletext", + "version": "1.0.0", + "description": "Migrate from ansi-colors package to Node.js util.styleText API", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./tests && npx codemod jssg test -l json ./src/remove-dependencies.ts ./tests/remove-dependencies --allow-child-process --allow-fs --strictness cst", + "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/ansi-colors-to-styletext", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Shamya Haria", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/ansi-colors-to-styletext/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.6.3" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/ansi-colors-to-styletext/src/remove-dependencies.ts b/recipes/ansi-colors-to-styletext/src/remove-dependencies.ts new file mode 100644 index 00000000..73f42041 --- /dev/null +++ b/recipes/ansi-colors-to-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(['ansi-colors', '@types/ansi-colors'], { + packageJsonPath: root.filename(), + runInstall: false, + persistFileWrite: false, + }); +}; + +export default transform; diff --git a/recipes/ansi-colors-to-styletext/src/workflow.ts b/recipes/ansi-colors-to-styletext/src/workflow.ts new file mode 100644 index 00000000..6987f9d4 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/src/workflow.ts @@ -0,0 +1,753 @@ +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 ANSI_COLORS_BINDING = 'ansi-colors'; + +const COMPATIBILITY_MAP = { + gray: 'blackBright', + grey: 'blackBright', +}; + +// Shared ansi-colors and util.styleText formats; gray/grey are normalized above. +const SUPPORTED_STYLES = new Set([ + 'reset', + 'bold', + 'dim', + 'italic', + 'underline', + 'inverse', + 'hidden', + 'strikethrough', + 'black', + 'red', + 'green', + 'yellow', + 'blue', + 'magenta', + 'cyan', + 'white', + 'blackBright', + 'redBright', + 'greenBright', + 'yellowBright', + 'blueBright', + 'magentaBright', + 'cyanBright', + 'whiteBright', + 'bgBlack', + 'bgRed', + 'bgGreen', + 'bgYellow', + 'bgBlue', + 'bgMagenta', + 'bgCyan', + 'bgWhite', + 'bgBlackBright', + 'bgRedBright', + 'bgGreenBright', + 'bgYellowBright', + 'bgBlueBright', + 'bgMagentaBright', + 'bgCyanBright', + 'bgWhiteBright', +]); + +const API_REPLACEMENTS = { + unstyle: 'stripVTControlCharacters', +}; + +const UNSUPPORTED_API_WARNINGS = { + enabled: `util.styleText has no equivalent runtime instance flag. Map this configuration to environment variables instead: set process.env.NO_COLOR='1' or NODE_DISABLE_COLORS='1' before application initialization.`, + visible: `util.styleText lacks a visual toggling mechanism and will always return a string wrapper. Please guard the call site explicitly: const out = visible ? styleText('red', msg) : '';`, + stripColor: `util.styleText does not expose an ANSI text stripper. Replace with a native regex str.replace(/\\x1b\\[[0-9;]*m/g, '') or install a zero-dependency package like strip-ansi.`, + hasAnsi: `util.styleText does not expose an ANSI text stripper. Replace with a native regex str.replace(/\\x1b\\[[0-9;]*m/g, '') or install a zero-dependency package like strip-ansi.`, + hasColor: `util.styleText does not expose an ANSI text stripper. Replace with a native regex str.replace(/\\x1b\\[[0-9;]*m/g, '') or install a zero-dependency package like strip-ansi.`, + alias: `util.styleText is stateless and does not maintain a style or theme registry. Migrate global configurations to dedicated structural objects mapping keys to arrow functions (e.g., const theme = { error: (m) => styleText(['bold', 'red'], m) }).`, + theme: `util.styleText is stateless and does not maintain a style or theme registry. Migrate global configurations to dedicated structural objects mapping keys to arrow functions (e.g., const theme = { error: (m) => styleText(['bold', 'red'], m) }).`, + create: `util.styleText is stateless and does not maintain a style or theme registry. Migrate global configurations to dedicated structural objects mapping keys to arrow functions (e.g., const theme = { error: (m) => styleText(['bold', 'red'], m) }).`, + define: `util.styleText is stateless and does not maintain a style or theme registry. Migrate global configurations to dedicated structural objects mapping keys to arrow functions (e.g., const theme = { error: (m) => styleText(['bold', 'red'], m) }).`, +}; + +const UNSUPPORTED_APIS = Object.keys(UNSUPPORTED_API_WARNINGS); + +type RequiredApi = 'styleText' | 'stripVTControlCharacters'; +type BindingMatcher = (node: SgNode) => boolean; + +/** + * Main codemod entry point. + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + const requiredApis = new Set(); + const statements = getModuleDependencies(root, ANSI_COLORS_BINDING); + + if (!statements.length) return null; + + for (const statement of statements) { + const initialEditCount = edits.length; + const destructuredNames = getDestructuredNames(statement); + const isFromStatement: BindingMatcher = (node) => { + const definition = node.definition({ resolveExternal: false }); + return definition !== null && + definition.root.filename() === root.filename() && + rangeContains(statement.range(), definition.node.range()); + }; + + if (destructuredNames.length > 0) { + processDestructuredImports( + rootNode, + destructuredNames, + isFromStatement, + edits, + requiredApis, + ); + } else { + const name = getDefaultBinding(statement); + + if (name) { + const binding: BindingMatcher = (node) => + node.text() === name && isFromStatement(node); + checkUnsupportedApis(rootNode, binding, root); + + processDefaultImports( + rootNode, + binding, + edits, + requiredApis, + ); + } + } + + if (edits.length > initialEditCount) { + const importReplacement = createImportReplacement( + statement, + requiredApis, + ); + + if (importReplacement) { + edits.push(statement.replace(importReplacement)); + } + } + } + + if (!edits.length) return null; + + return rootNode.commitEdits(edits); +} + +/** + * Builds the replacement import line based on whether the original was + * ESM, CJS, or dynamic. + */ +function createImportReplacement( + statement: SgNode, + requiredApis: Set, +): string { + const imports = [...requiredApis].join(', '); + + if (!imports) return ''; + + const kind = statement.kind(); + + if (kind === 'import_statement') { + return `import { ${imports} } from 'node:util';`; + } + + if (kind === 'variable_declarator') { + if (statement.field('value')?.kind() === 'await_expression') { + return `{ ${imports} } = await import('node:util')`; + } + + return `{ ${imports} } = require('node:util')`; + } + + return ''; +} + +/** + * Resolves the local binding name for default and namespace imports. + */ +function getDefaultBinding(statement: SgNode): string | null { + const kind = statement.kind(); + + if (kind === 'import_statement') { + const defaultImport = statement.find({ + rule: { + kind: 'identifier', + inside: { + kind: 'import_clause', + not: { + any: [ + { has: { kind: 'named_imports' } }, + { has: { kind: 'namespace_import' } }, + ], + }, + }, + }, + }); + + if (defaultImport) return defaultImport.text(); + + const namespaceImport = statement.find({ + rule: { + kind: 'identifier', + inside: { kind: 'namespace_import' }, + }, + }); + + return namespaceImport?.text() ?? null; + } + + if (kind === 'variable_declarator') { + const nameField = statement.field('name'); + + if (nameField?.kind() === 'identifier') { + return nameField.text(); + } + } + + return null; +} + +/** + * Collects named import bindings from ESM and CJS destructured statements. + */ +function getDestructuredNames( + statement: SgNode, +): Array<{ imported: string; local: string }> { + const names: Array<{ imported: string; local: string }> = []; + const kind = statement.kind(); + + if (kind === 'import_statement') { + const namedImports = statement.find({ + rule: { kind: 'named_imports' }, + }); + + if (namedImports) { + for (const specifier of namedImports.findAll({ + rule: { kind: 'import_specifier' }, + })) { + const importedName = specifier.field('name'); + const alias = specifier.field('alias'); + + if (importedName) { + const imported = importedName.text(); + const local = alias ? alias.text() : imported; + const mappedImported = + COMPATIBILITY_MAP[ + imported as keyof typeof COMPATIBILITY_MAP + ]; + + names.push({ + imported: mappedImported ?? imported, + local, + }); + } + } + } + } else if (kind === 'variable_declarator') { + const nameField = statement.field('name'); + + if (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(); + const mappedImported = + COMPATIBILITY_MAP[ + name as keyof typeof COMPATIBILITY_MAP + ]; + + names.push({ + imported: mappedImported ?? name, + local: name, + }); + } else if (prop.kind() === 'pair_pattern') { + const key = prop.field('key'); + const value = prop.field('value'); + + if (key && value) { + const imported = key.text(); + const mappedImported = + COMPATIBILITY_MAP[ + imported as keyof typeof COMPATIBILITY_MAP + ]; + + names.push({ + imported: mappedImported ?? imported, + local: value.text(), + }); + } + } + } + } + } + + return names; +} + +/** + * Walks a member expression chain and returns the ordered style names, + * or null if the chain doesn't start from the expected binding. + */ +function extractChainedStyles( + node: SgNode, + binding: BindingMatcher, +): string[] | null { + const objectNode = node.field('object'); + const propertyNode = node.field('property'); + + if ( + !objectNode || + !propertyNode || + propertyNode.kind() !== 'property_identifier' + ) { + return null; + } + + const propertyName = propertyNode.text(); + + if ( + UNSUPPORTED_APIS.includes(propertyName) || + propertyName in API_REPLACEMENTS + ) { + return null; + } + + const normalizedName = + COMPATIBILITY_MAP[ + propertyName as keyof typeof COMPATIBILITY_MAP + ] ?? propertyName; + + if (!SUPPORTED_STYLES.has(normalizedName)) return null; + + if (objectNode.kind() === 'identifier') { + if (!binding(objectNode)) return null; + + return [normalizedName]; + } + + if (objectNode.kind() === 'member_expression') { + const nested = extractChainedStyles(objectNode, binding); + + if (!nested) return null; + + return [...nested, normalizedName]; + } + + return null; +} + +/** + * Emits targeted warnings for ansi-colors APIs with no util.styleText equivalent. + */ +function checkUnsupportedApis( + rootNode: SgNode, + binding: BindingMatcher, + root: SgRoot, +): void { + const memberExpressions = rootNode.findAll({ + rule: { kind: 'member_expression' }, + }); + + for (const memberExpr of memberExpressions) { + const objectNode = memberExpr.field('object'); + const propertyNode = memberExpr.field('property'); + + if (!objectNode || !propertyNode) continue; + if (!binding(objectNode)) continue; + if (propertyNode.kind() !== 'property_identifier') continue; + + const propertyName = propertyNode.text(); + + // `unstyle` has a native Node.js equivalent, so it should not + // produce an unsupported API warning. + if (propertyName in API_REPLACEMENTS) continue; + + if (!UNSUPPORTED_APIS.includes(propertyName)) continue; + + const filename = root.filename(); + const { start } = memberExpr.range(); + const message = + UNSUPPORTED_API_WARNINGS[ + propertyName as keyof typeof UNSUPPORTED_API_WARNINGS + ]; + + console.warn( + `${filename}:${start.line}:${start.column}: ${message}`, + ); + } +} + +/** + * Transforms calls from destructured bindings. + * + * red('text') becomes: + * styleText('red', 'text') + * + * unstyle('text') becomes: + * stripVTControlCharacters('text') + */ +function processDestructuredImports( + rootNode: SgNode, + destructuredNames: Array<{ imported: string; local: string }>, + isFromStatement: BindingMatcher, + edits: Edit[], + requiredApis: Set, +): void { + // Replacing this import would also remove unsupported bindings. Leave it intact. + if (destructuredNames.some(({ imported }) => + !SUPPORTED_STYLES.has(imported) && !Object.hasOwn(API_REPLACEMENTS, imported) + )) return; + + for (const { local, imported } of destructuredNames) { + const replacement = API_REPLACEMENTS[ + imported as keyof typeof API_REPLACEMENTS + ]; + + const calls = rootNode.findAll({ + rule: { + kind: 'call_expression', + pattern: `${local}($$$ARGS)`, + }, + }); + + for (const call of calls) { + const functionNode = call.field('function'); + const openingParen = call.field('arguments')?.child(0); + + if (!functionNode || openingParen?.text() !== '(') continue; + if (!isFromStatement(functionNode)) continue; + + if (replacement) { + requiredApis.add(replacement as RequiredApi); + edits.push(functionNode.replace(replacement)); + } else { + requiredApis.add('styleText'); + // Leave argument expressions intact so nested calls have disjoint edits. + edits.push(functionNode.replace('styleText')); + edits.push(openingParen.replace(`('${imported}', `)); + } + } + } +} + +/** + * Represents the transformation that can be applied to an ansi-colors + * call expression. + */ +type CallTransformation = { + replacement: string; + requiredApis: RequiredApi[]; +}; + +/** + * Returns the transformation for a call expression if it is an + * ansi-colors call belonging to the provided binding. + * + * Examples: + * + * colors.unstyle(value) + * -> + * stripVTControlCharacters(value) + * + * colors.bold.red(value) + * -> + * styleText(['bold', 'red'], value) + */ +function getCallTransformation( + call: SgNode, + binding: BindingMatcher, +): CallTransformation | null { + const functionNode = call.field('function'); + + if (functionNode?.kind() !== 'member_expression') { + return null; + } + + const propertyNode = functionNode.field('property'); + const objectNode = functionNode.field('object'); + + if ( + propertyNode?.kind() === 'property_identifier' && + propertyNode.text() in API_REPLACEMENTS && + objectNode?.kind() === 'identifier' && + binding(objectNode) + ) { + const replacement = + API_REPLACEMENTS[ + propertyNode.text() as keyof typeof API_REPLACEMENTS + ]; + + return { + replacement, + requiredApis: [replacement as RequiredApi], + }; + } + + const styles = extractChainedStyles( + functionNode, + binding, + ); + + if (!styles?.length) return null; + + const styleTextArgument = + styles.length === 1 + ? `'${styles[0]}'` + : `[${styles.map(style => `'${style}'`).join(', ')}]`; + + return { + replacement: `styleText(${styleTextArgument}`, + requiredApis: ['styleText'], + }; +} + +/** + * Compares two AST positions. + */ +function positionBeforeOrEqual( + a: { line: number; column: number }, + b: { line: number; column: number }, +): boolean { + return ( + a.line < b.line || + (a.line === b.line && a.column <= b.column) + ); +} + +/** + * Returns true when `outer` completely contains `inner`. + */ +function rangeContains( + outer: ReturnType['range']>, + inner: ReturnType['range']>, +): boolean { + return ( + positionBeforeOrEqual(outer.start, inner.start) && + positionBeforeOrEqual(inner.end, outer.end) + ); +} + +/** + * Returns only the outermost relevant calls from a collection of calls. + * + * This prevents overlapping edits such as: + * + * colors.unstyle(colors.bold('hello')) + * + * from producing separate edits for both calls. + * + * The outer call is edited once, and its nested calls are rendered + * recursively. + */ +function getOutermostCalls( + calls: SgNode[], +): SgNode[] { + return calls.filter(call => { + const callRange = call.range(); + + return !calls.some(other => { + if (other === call) return false; + + const otherRange = other.range(); + + if (!rangeContains(otherRange, callRange)) { + return false; + } + + // Equal ranges are not considered containment. + return ( + otherRange.start.line !== callRange.start.line || + otherRange.start.column !== callRange.start.column || + otherRange.end.line !== callRange.end.line || + otherRange.end.column !== callRange.end.column + ); + }); + }); +} + +/** + * Returns relevant nested calls that are not themselves contained by + * another relevant nested call. + * + * For: + * + * colors.unstyle( + * colors.bold( + * colors.blue('hello') + * ) + * ) + * + * the first level returned here is `colors.bold(...)`. + * `colors.blue(...)` is handled recursively by that call. + */ +function getOutermostNestedCalls( + args: SgNode, + binding: BindingMatcher, +): SgNode[] { + const candidates = args.findAll({ + rule: { kind: 'call_expression' }, + }).filter(call => { + return getCallTransformation(call, binding) !== null; + }); + + return getOutermostCalls(candidates); +} + +/** + * Recursively transforms ansi-colors calls nested inside an argument list. + * + * This is the important part for cases such as: + * + * colors.unstyle(colors.bold.blue('\u001b[34mhello\u001b[39m')) + * + * which becomes: + * + * stripVTControlCharacters(styleText(['bold', 'blue'], '\u001b[34mhello\u001b[39m')) + * + * The ANSI escape sequences are intentionally preserved here because + * `stripVTControlCharacters` is the outer operation that removes them. + */ +function transformNestedArguments( + args: SgNode, + binding: BindingMatcher, + requiredApis: Set, +): string { + const nestedCalls = getOutermostNestedCalls(args, binding); + const edits = nestedCalls.map(call => + call.replace(transformCallExpression(call, binding, requiredApis)), + ); + + // Apply AST ranges instead of searching for matching text in strings or comments. + return args.commitEdits(edits).slice(1, -1); +} + +/** + * Recursively transforms one ansi-colors call expression. + * + * This function does not create an AST edit itself. It renders the + * transformed call as a string so an outer call can incorporate the + * transformed result into its own replacement. + */ +function transformCallExpression( + call: SgNode, + binding: BindingMatcher, + requiredApis: Set, +): string { + const transformation = getCallTransformation( + call, + binding, + ); + + if (!transformation) { + return call.text(); + } + + for (const api of transformation.requiredApis) { + requiredApis.add(api); + } + + const args = call.field('arguments'); + + if (!args) { + return call.text(); + } + + const transformedArgs = transformNestedArguments( + args, + binding, + requiredApis, + ); + + if ( + transformation.replacement === + 'stripVTControlCharacters' + ) { + return `stripVTControlCharacters(${transformedArgs})`; + } + + return `${transformation.replacement}, ${transformedArgs})`; +} + +/** + * Transforms chained member calls, including nested ansi-colors calls. + * + * ac.bold.red('text') becomes: + * styleText(['bold', 'red'], 'text') + * + * ac.unstyle('text') becomes: + * stripVTControlCharacters('text') + * + * Nested calls are transformed recursively: + * + * ac.unstyle(ac.bold.red('text')) + * + * becomes: + * stripVTControlCharacters(styleText(['bold', 'red'], 'text')) + */ +function processDefaultImports( + rootNode: SgNode, + binding: BindingMatcher, + edits: Edit[], + requiredApis: Set, +): void { + // Keep the original binding when any property chain needs manual migration. + for (const member of rootNode.findAll({ rule: { kind: 'member_expression' } })) { + let base = member; + while (base.kind() === 'member_expression') base = base.field('object'); + if (base.kind() !== 'identifier' || !binding(base)) continue; + if (extractChainedStyles(member, binding)) continue; + if ( + member.field('object')?.kind() === 'identifier' && + Object.hasOwn(API_REPLACEMENTS, member.field('property')?.text() ?? '') + ) continue; + return; + } + + const calls = rootNode + .findAll({ + rule: { + kind: 'call_expression', + has: { + field: 'function', + kind: 'member_expression', + }, + }, + }) + .filter(call => { + return getCallTransformation(call, binding) !== null; + }); + + /** + * Only edit the outermost relevant call. Nested calls are rendered + * recursively as part of the outer replacement, which avoids + * overlapping edits. + */ + const outermostCalls = getOutermostCalls(calls); + + for (const call of outermostCalls) { + const replacement = transformCallExpression( + call, + binding, + requiredApis, + ); + + if (replacement === call.text()) continue; + + edits.push(call.replace(replacement)); + } +} diff --git a/recipes/ansi-colors-to-styletext/tests/basic-color/expected.js b/recipes/ansi-colors-to-styletext/tests/basic-color/expected.js new file mode 100644 index 00000000..45cb524e --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/basic-color/expected.js @@ -0,0 +1,3 @@ +const { styleText } = require('node:util'); + +console.log(styleText('red', 'Error message')); diff --git a/recipes/ansi-colors-to-styletext/tests/basic-color/input.js b/recipes/ansi-colors-to-styletext/tests/basic-color/input.js new file mode 100644 index 00000000..50f48cf6 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/basic-color/input.js @@ -0,0 +1,3 @@ +const ansi = require('ansi-colors'); + +console.log(ansi.red('Error message')); diff --git a/recipes/ansi-colors-to-styletext/tests/chained-styles/expected.js b/recipes/ansi-colors-to-styletext/tests/chained-styles/expected.js new file mode 100644 index 00000000..5cb97e89 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/chained-styles/expected.js @@ -0,0 +1,4 @@ +const { styleText } = require('node:util'); + +console.log(styleText(['bold', 'red'], 'Critical error')); +console.log(styleText(['bgBlue', 'white', 'bold'], 'HEADER')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/chained-styles/input.js b/recipes/ansi-colors-to-styletext/tests/chained-styles/input.js new file mode 100644 index 00000000..f7a8061b --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/chained-styles/input.js @@ -0,0 +1,4 @@ +const ac = require('ansi-colors'); + +console.log(ac.bold.red('Critical error')); +console.log(ac.bgBlue.white.bold('HEADER')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/commonjs-destructured/expected.js b/recipes/ansi-colors-to-styletext/tests/commonjs-destructured/expected.js new file mode 100644 index 00000000..3b36aa2f --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/commonjs-destructured/expected.js @@ -0,0 +1,5 @@ +const { styleText } = require('node:util'); + +console.log(styleText('red', 'Error')); +console.log(styleText('blue', 'Info')); +console.log(styleText('bold', 'Important')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/commonjs-destructured/input.js b/recipes/ansi-colors-to-styletext/tests/commonjs-destructured/input.js new file mode 100644 index 00000000..d6dcf16a --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/commonjs-destructured/input.js @@ -0,0 +1,5 @@ +const { red, blue, bold } = require('ansi-colors'); + +console.log(red('Error')); +console.log(blue('Info')); +console.log(bold('Important')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/dynamic-import-await/expected.js b/recipes/ansi-colors-to-styletext/tests/dynamic-import-await/expected.js new file mode 100644 index 00000000..aacd4619 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/dynamic-import-await/expected.js @@ -0,0 +1,4 @@ +const { styleText } = await import('node:util'); + +console.log(styleText('red', 'Error')); +console.log(styleText(['bold', 'green'], 'Success')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/dynamic-import-await/input.js b/recipes/ansi-colors-to-styletext/tests/dynamic-import-await/input.js new file mode 100644 index 00000000..5b0b431a --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/dynamic-import-await/input.js @@ -0,0 +1,4 @@ +const ac = await import('ansi-colors'); + +console.log(ac.red('Error')); +console.log(ac.bold.green('Success')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/dynamic-import-then/expected.js b/recipes/ansi-colors-to-styletext/tests/dynamic-import-then/expected.js new file mode 100644 index 00000000..5896a512 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/dynamic-import-then/expected.js @@ -0,0 +1,4 @@ +import('ansi-colors').then((ac) => { + console.log(ac.red('Error')); + console.log(ac.bold.green('Success')); +}); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/dynamic-import-then/input.js b/recipes/ansi-colors-to-styletext/tests/dynamic-import-then/input.js new file mode 100644 index 00000000..5896a512 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/dynamic-import-then/input.js @@ -0,0 +1,4 @@ +import('ansi-colors').then((ac) => { + console.log(ac.red('Error')); + console.log(ac.bold.green('Success')); +}); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/esm-default-import/expected.js b/recipes/ansi-colors-to-styletext/tests/esm-default-import/expected.js new file mode 100644 index 00000000..3b597123 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/esm-default-import/expected.js @@ -0,0 +1,4 @@ +import { styleText } from 'node:util'; + +console.log(styleText('red', 'Error')); +console.log(styleText(['bold', 'green'], 'Success')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/esm-default-import/input.js b/recipes/ansi-colors-to-styletext/tests/esm-default-import/input.js new file mode 100644 index 00000000..838d2868 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/esm-default-import/input.js @@ -0,0 +1,4 @@ +import ac from 'ansi-colors'; + +console.log(ac.red('Error')); +console.log(ac.bold.green('Success')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/gray-alias/expected.js b/recipes/ansi-colors-to-styletext/tests/gray-alias/expected.js new file mode 100644 index 00000000..7d5059bf --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/gray-alias/expected.js @@ -0,0 +1,4 @@ +const { styleText } = require('node:util'); + +console.log(styleText('blackBright', 'Hint text')); +console.log(styleText('blackBright', 'Another hint')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/gray-alias/input.js b/recipes/ansi-colors-to-styletext/tests/gray-alias/input.js new file mode 100644 index 00000000..d7a49025 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/gray-alias/input.js @@ -0,0 +1,4 @@ +const ac = require('ansi-colors'); + +console.log(ac.gray('Hint text')); +console.log(ac.grey('Another hint')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/literal-and-comment/expected.js b/recipes/ansi-colors-to-styletext/tests/literal-and-comment/expected.js new file mode 100644 index 00000000..2fc48052 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/literal-and-comment/expected.js @@ -0,0 +1,2 @@ +import { stripVTControlCharacters, styleText } from 'node:util'; +stripVTControlCharacters("ac.red('x')" + /* ac.red('x') */ styleText('red', 'x')); diff --git a/recipes/ansi-colors-to-styletext/tests/literal-and-comment/input.js b/recipes/ansi-colors-to-styletext/tests/literal-and-comment/input.js new file mode 100644 index 00000000..f55536fb --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/literal-and-comment/input.js @@ -0,0 +1,2 @@ +import ac from 'ansi-colors'; +ac.unstyle("ac.red('x')" + /* ac.red('x') */ ac.red('x')); diff --git a/recipes/ansi-colors-to-styletext/tests/mixed-chained-destructured/expected.js b/recipes/ansi-colors-to-styletext/tests/mixed-chained-destructured/expected.js new file mode 100644 index 00000000..5a87241b --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/mixed-chained-destructured/expected.js @@ -0,0 +1,5 @@ +import { styleText } from 'node:util'; +import { styleText } from 'node:util'; + +console.log(styleText(['bold', 'blue'], 'Header')); +console.log(styleText('red', 'Error')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/mixed-chained-destructured/input.js b/recipes/ansi-colors-to-styletext/tests/mixed-chained-destructured/input.js new file mode 100644 index 00000000..b2071b8d --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/mixed-chained-destructured/input.js @@ -0,0 +1,5 @@ +import ac from 'ansi-colors'; +import { red } from 'ansi-colors'; + +console.log(ac.bold.blue('Header')); +console.log(red('Error')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/multiple-uses/expected.js b/recipes/ansi-colors-to-styletext/tests/multiple-uses/expected.js new file mode 100644 index 00000000..8774adc7 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/multiple-uses/expected.js @@ -0,0 +1,5 @@ +const { styleText } = require('node:util'); + +console.log(styleText('red', 'Error')); +console.log(styleText('green', 'Success')); +console.log(styleText('blue', 'Info')); diff --git a/recipes/ansi-colors-to-styletext/tests/multiple-uses/input.js b/recipes/ansi-colors-to-styletext/tests/multiple-uses/input.js new file mode 100644 index 00000000..b290699b --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/multiple-uses/input.js @@ -0,0 +1,5 @@ +const ansi = require('ansi-colors'); + +console.log(ansi.red('Error')); +console.log(ansi.green('Success')); +console.log(ansi.blue('Info')); diff --git a/recipes/ansi-colors-to-styletext/tests/nested-named/expected.js b/recipes/ansi-colors-to-styletext/tests/nested-named/expected.js new file mode 100644 index 00000000..8ebd094f --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/nested-named/expected.js @@ -0,0 +1,2 @@ +import { styleText, stripVTControlCharacters } from 'node:util'; +stripVTControlCharacters(styleText('red', 'x')); diff --git a/recipes/ansi-colors-to-styletext/tests/nested-named/input.js b/recipes/ansi-colors-to-styletext/tests/nested-named/input.js new file mode 100644 index 00000000..64bdab36 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/nested-named/input.js @@ -0,0 +1,2 @@ +import { red, unstyle } from 'ansi-colors'; +unstyle(red('x')); diff --git a/recipes/ansi-colors-to-styletext/tests/no-match/expected.js b/recipes/ansi-colors-to-styletext/tests/no-match/expected.js new file mode 100644 index 00000000..271b3129 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/no-match/expected.js @@ -0,0 +1,3 @@ +const chalk = require('chalk'); + +console.log(chalk.red('text')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/no-match/input.js b/recipes/ansi-colors-to-styletext/tests/no-match/input.js new file mode 100644 index 00000000..271b3129 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/no-match/input.js @@ -0,0 +1,3 @@ +const chalk = require('chalk'); + +console.log(chalk.red('text')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/remove-dependencies/remove-ansi-colors/expected.json b/recipes/ansi-colors-to-styletext/tests/remove-dependencies/remove-ansi-colors/expected.json new file mode 100644 index 00000000..33d25829 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/remove-dependencies/remove-ansi-colors/expected.json @@ -0,0 +1,10 @@ +{ + "name": "fixture", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2" + }, + "devDependencies": { + "typescript": "^5.6.0" + } +} \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/remove-dependencies/remove-ansi-colors/input.json b/recipes/ansi-colors-to-styletext/tests/remove-dependencies/remove-ansi-colors/input.json new file mode 100644 index 00000000..490b36b8 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/remove-dependencies/remove-ansi-colors/input.json @@ -0,0 +1,12 @@ +{ + "name": "fixture", + "version": "1.0.0", + "dependencies": { + "ansi-colors": "^4.1.3", + "express": "^4.18.2" + }, + "devDependencies": { + "@types/ansi-colors": "^1.0.0", + "typescript": "^5.6.0" + } +} \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/reusable-functions/expected.js b/recipes/ansi-colors-to-styletext/tests/reusable-functions/expected.js new file mode 100644 index 00000000..c005ca59 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/reusable-functions/expected.js @@ -0,0 +1,3 @@ +const { styleText } = require('node:util'); +const errorStyle = (msg) => styleText(['bold', 'red'], msg); +const status = level === 'error' ? styleText(['bold', 'red'], 'boom') : styleText('yellow', 'slow'); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/reusable-functions/input.js b/recipes/ansi-colors-to-styletext/tests/reusable-functions/input.js new file mode 100644 index 00000000..cbea1ae5 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/reusable-functions/input.js @@ -0,0 +1,3 @@ +const ac = require('ansi-colors'); +const errorStyle = (msg) => ac.bold.red(msg); +const status = level === 'error' ? ac.bold.red('boom') : ac.yellow('slow'); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/shadowed-arrow/expected.js b/recipes/ansi-colors-to-styletext/tests/shadowed-arrow/expected.js new file mode 100644 index 00000000..3604ced7 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/shadowed-arrow/expected.js @@ -0,0 +1,3 @@ +import { styleText } from 'node:util'; +const render = ac => ac.red('local'); +styleText('red', 'imported'); diff --git a/recipes/ansi-colors-to-styletext/tests/shadowed-arrow/input.js b/recipes/ansi-colors-to-styletext/tests/shadowed-arrow/input.js new file mode 100644 index 00000000..d245de6d --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/shadowed-arrow/input.js @@ -0,0 +1,3 @@ +import ac from 'ansi-colors'; +const render = ac => ac.red('local'); +ac.red('imported'); diff --git a/recipes/ansi-colors-to-styletext/tests/shadowed-default/expected.js b/recipes/ansi-colors-to-styletext/tests/shadowed-default/expected.js new file mode 100644 index 00000000..265e8dc6 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/shadowed-default/expected.js @@ -0,0 +1,3 @@ +import { styleText } from 'node:util'; +function paint(ac) { return ac.red('local'); } +styleText('blue', 'imported'); diff --git a/recipes/ansi-colors-to-styletext/tests/shadowed-default/input.js b/recipes/ansi-colors-to-styletext/tests/shadowed-default/input.js new file mode 100644 index 00000000..a762c1ea --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/shadowed-default/input.js @@ -0,0 +1,3 @@ +import ac from 'ansi-colors'; +function paint(ac) { return ac.red('local'); } +ac.blue('imported'); diff --git a/recipes/ansi-colors-to-styletext/tests/shadowed-local/expected.js b/recipes/ansi-colors-to-styletext/tests/shadowed-local/expected.js new file mode 100644 index 00000000..1358e4ad --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/shadowed-local/expected.js @@ -0,0 +1,5 @@ +const { styleText } = require('node:util'); +function render() { ac.red('local'); var ac = custom; } +{ const { colors: ac } = other; ac.blue('local'); } +try {} catch (ac) { ac.green('local'); } +styleText('red', 'imported'); diff --git a/recipes/ansi-colors-to-styletext/tests/shadowed-local/input.js b/recipes/ansi-colors-to-styletext/tests/shadowed-local/input.js new file mode 100644 index 00000000..f8c97ea6 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/shadowed-local/input.js @@ -0,0 +1,5 @@ +const ac = require('ansi-colors'); +function render() { ac.red('local'); var ac = custom; } +{ const { colors: ac } = other; ac.blue('local'); } +try {} catch (ac) { ac.green('local'); } +ac.red('imported'); diff --git a/recipes/ansi-colors-to-styletext/tests/shadowed-named/expected.js b/recipes/ansi-colors-to-styletext/tests/shadowed-named/expected.js new file mode 100644 index 00000000..03d316f5 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/shadowed-named/expected.js @@ -0,0 +1,3 @@ +import { styleText } from 'node:util'; +const render = ({ paint }) => paint('local'); +styleText('red', 'imported'); diff --git a/recipes/ansi-colors-to-styletext/tests/shadowed-named/input.js b/recipes/ansi-colors-to-styletext/tests/shadowed-named/input.js new file mode 100644 index 00000000..99384942 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/shadowed-named/input.js @@ -0,0 +1,3 @@ +import { red as paint } from 'ansi-colors'; +const render = ({ paint }) => paint('local'); +paint('imported'); diff --git a/recipes/ansi-colors-to-styletext/tests/shadowed-nested/expected.js b/recipes/ansi-colors-to-styletext/tests/shadowed-nested/expected.js new file mode 100644 index 00000000..cdd856ea --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/shadowed-nested/expected.js @@ -0,0 +1,3 @@ +import { styleText } from 'node:util'; +styleText('red', ((ac) => ac.blue('local'))(custom)); +function f(ac) { return ac.noop('local'); } diff --git a/recipes/ansi-colors-to-styletext/tests/shadowed-nested/input.js b/recipes/ansi-colors-to-styletext/tests/shadowed-nested/input.js new file mode 100644 index 00000000..faf8425c --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/shadowed-nested/input.js @@ -0,0 +1,3 @@ +import ac from 'ansi-colors'; +ac.red(((ac) => ac.blue('local'))(custom)); +function f(ac) { return ac.noop('local'); } diff --git a/recipes/ansi-colors-to-styletext/tests/string-concatenation/expected.js b/recipes/ansi-colors-to-styletext/tests/string-concatenation/expected.js new file mode 100644 index 00000000..288996b7 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/string-concatenation/expected.js @@ -0,0 +1,4 @@ +const { styleText } = require('node:util'); + +console.log('Hello, ' + styleText('green', 'World') + '!'); +console.log(styleText(['bgRedBright', 'white'], ' ERR ') + ' ' + styleText('red', 'File not found')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/string-concatenation/input.js b/recipes/ansi-colors-to-styletext/tests/string-concatenation/input.js new file mode 100644 index 00000000..3641b676 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/string-concatenation/input.js @@ -0,0 +1,4 @@ +const ac = require('ansi-colors'); + +console.log('Hello, ' + ac.green('World') + '!'); +console.log(ac.bgRedBright.white(' ERR ') + ' ' + ac.red('File not found')); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/supported-named-aliases/expected.js b/recipes/ansi-colors-to-styletext/tests/supported-named-aliases/expected.js new file mode 100644 index 00000000..2fcad48f --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/supported-named-aliases/expected.js @@ -0,0 +1,2 @@ +const { styleText, stripVTControlCharacters } = require('node:util'); +stripVTControlCharacters(styleText('bgRedBright', styleText('blackBright', 'text'))); diff --git a/recipes/ansi-colors-to-styletext/tests/supported-named-aliases/input.js b/recipes/ansi-colors-to-styletext/tests/supported-named-aliases/input.js new file mode 100644 index 00000000..cde9a3b2 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/supported-named-aliases/input.js @@ -0,0 +1,2 @@ +const { gray: muted, bgRedBright: highlight, unstyle: strip } = require('ansi-colors'); +strip(highlight(muted('text'))); diff --git a/recipes/ansi-colors-to-styletext/tests/supported-property-aliases/expected.js b/recipes/ansi-colors-to-styletext/tests/supported-property-aliases/expected.js new file mode 100644 index 00000000..e03d6a80 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/supported-property-aliases/expected.js @@ -0,0 +1,3 @@ +import { stripVTControlCharacters, styleText } from 'node:util'; +stripVTControlCharacters(styleText(['bold', 'blackBright'], 'text')); +styleText('bgRedBright', 'text'); diff --git a/recipes/ansi-colors-to-styletext/tests/supported-property-aliases/input.js b/recipes/ansi-colors-to-styletext/tests/supported-property-aliases/input.js new file mode 100644 index 00000000..29c7cad7 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/supported-property-aliases/input.js @@ -0,0 +1,3 @@ +import ac from 'ansi-colors'; +ac.unstyle(ac.bold.gray('text')); +ac.bgRedBright('text'); diff --git a/recipes/ansi-colors-to-styletext/tests/template-literals/expected.js b/recipes/ansi-colors-to-styletext/tests/template-literals/expected.js new file mode 100644 index 00000000..8c4cfdea --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/template-literals/expected.js @@ -0,0 +1,6 @@ +const { styleText } = require('node:util'); +const file = 'server.js'; +const line = '42'; + +console.log(`${styleText(['bold', 'red'], '[ERR]')} ${styleText('dim', file)}:${styleText('dim', line)}`); +console.log(`Multi-badge: ${styleText(['bgRed', 'white'], ' ERR ')} ${styleText(['bgGreen', 'black'], ' OK ')}`); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/template-literals/input.js b/recipes/ansi-colors-to-styletext/tests/template-literals/input.js new file mode 100644 index 00000000..59771b8f --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/template-literals/input.js @@ -0,0 +1,6 @@ +const ac = require('ansi-colors'); +const file = 'server.js'; +const line = '42'; + +console.log(`${ac.bold.red('[ERR]')} ${ac.dim(file)}:${ac.dim(line)}`); +console.log(`Multi-badge: ${ac.bgRed.white(' ERR ')} ${ac.bgGreen.black(' OK ')}`); \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/unshadowed-default-expression/expected.js b/recipes/ansi-colors-to-styletext/tests/unshadowed-default-expression/expected.js new file mode 100644 index 00000000..d1e4eaad --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unshadowed-default-expression/expected.js @@ -0,0 +1,4 @@ +import { styleText } from 'node:util'; +function render(color = styleText('red', 'default')) { return color; } +const { ac: other } = settings; +styleText('blue', 'imported'); diff --git a/recipes/ansi-colors-to-styletext/tests/unshadowed-default-expression/input.js b/recipes/ansi-colors-to-styletext/tests/unshadowed-default-expression/input.js new file mode 100644 index 00000000..ddfb4563 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unshadowed-default-expression/input.js @@ -0,0 +1,4 @@ +import ac from 'ansi-colors'; +function render(color = ac.red('default')) { return color; } +const { ac: other } = settings; +ac.blue('imported'); diff --git a/recipes/ansi-colors-to-styletext/tests/unstyle-bis/expected.js b/recipes/ansi-colors-to-styletext/tests/unstyle-bis/expected.js new file mode 100644 index 00000000..cd9af2df --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unstyle-bis/expected.js @@ -0,0 +1,15 @@ +import { stripVTControlCharacters, styleText } from 'node:util'; + +const foo = stripVTControlCharacters(styleText(['bold', 'blue'], 'hello')); + +/** + * @param {string} text + * @returns {string} + */ +const restyle = (text) => { + const stripped = stripVTControlCharacters(text); + + return styleText(['bold', 'blue'], stripped); +} + +console.log(foo); diff --git a/recipes/ansi-colors-to-styletext/tests/unstyle-bis/input.js b/recipes/ansi-colors-to-styletext/tests/unstyle-bis/input.js new file mode 100644 index 00000000..99100c24 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unstyle-bis/input.js @@ -0,0 +1,15 @@ +import colors from 'ansi-colors'; + +const foo = colors.unstyle(colors.bold.blue('hello')); + +/** + * @param {string} text + * @returns {string} + */ +const restyle = (text) => { + const stripped = colors.unstyle(text); + + return colors.bold.blue(stripped); +} + +console.log(foo); diff --git a/recipes/ansi-colors-to-styletext/tests/unstyle-cjs/expected.js b/recipes/ansi-colors-to-styletext/tests/unstyle-cjs/expected.js new file mode 100644 index 00000000..2560db7f --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unstyle-cjs/expected.js @@ -0,0 +1,5 @@ +const { stripVTControlCharacters } = require('node:util'); + +const foo = stripVTControlCharacters('\u001b[31mhello\u001b[39m'); + +console.log(foo); diff --git a/recipes/ansi-colors-to-styletext/tests/unstyle-cjs/input.js b/recipes/ansi-colors-to-styletext/tests/unstyle-cjs/input.js new file mode 100644 index 00000000..3d3b88a3 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unstyle-cjs/input.js @@ -0,0 +1,5 @@ +const ac = require('ansi-colors'); + +const foo = ac.unstyle('\u001b[31mhello\u001b[39m'); + +console.log(foo); diff --git a/recipes/ansi-colors-to-styletext/tests/unstyle/expected.js b/recipes/ansi-colors-to-styletext/tests/unstyle/expected.js new file mode 100644 index 00000000..7868a3e4 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unstyle/expected.js @@ -0,0 +1,5 @@ +import { stripVTControlCharacters } from 'node:util'; + +const foo = stripVTControlCharacters('\u001b[34mhello\u001b[39m'); + +console.log(foo); diff --git a/recipes/ansi-colors-to-styletext/tests/unstyle/input.js b/recipes/ansi-colors-to-styletext/tests/unstyle/input.js new file mode 100644 index 00000000..e7bf2692 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unstyle/input.js @@ -0,0 +1,5 @@ +import colors from 'ansi-colors'; + +const foo = colors.unstyle('\u001b[34mhello\u001b[39m'); + +console.log(foo); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-api/expected.js b/recipes/ansi-colors-to-styletext/tests/unsupported-api/expected.js new file mode 100644 index 00000000..996ceee2 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-api/expected.js @@ -0,0 +1,2 @@ +const ac = require('ansi-colors'); +ac.enabled = false; \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-api/input.js b/recipes/ansi-colors-to-styletext/tests/unsupported-api/input.js new file mode 100644 index 00000000..996ceee2 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-api/input.js @@ -0,0 +1,2 @@ +const ac = require('ansi-colors'); +ac.enabled = false; \ No newline at end of file diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-named-cjs/expected.js b/recipes/ansi-colors-to-styletext/tests/unsupported-named-cjs/expected.js new file mode 100644 index 00000000..c0ee59b7 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-named-cjs/expected.js @@ -0,0 +1,2 @@ +const { hasColor } = require('ansi-colors'); +hasColor('text'); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-named-cjs/input.js b/recipes/ansi-colors-to-styletext/tests/unsupported-named-cjs/input.js new file mode 100644 index 00000000..c0ee59b7 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-named-cjs/input.js @@ -0,0 +1,2 @@ +const { hasColor } = require('ansi-colors'); +hasColor('text'); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-named-dynamic/expected.js b/recipes/ansi-colors-to-styletext/tests/unsupported-named-dynamic/expected.js new file mode 100644 index 00000000..9c9cd8b2 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-named-dynamic/expected.js @@ -0,0 +1,3 @@ +const { red, noop: plain } = await import('ansi-colors'); +red('text'); +plain('text'); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-named-dynamic/input.js b/recipes/ansi-colors-to-styletext/tests/unsupported-named-dynamic/input.js new file mode 100644 index 00000000..9c9cd8b2 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-named-dynamic/input.js @@ -0,0 +1,3 @@ +const { red, noop: plain } = await import('ansi-colors'); +red('text'); +plain('text'); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-named-esm/expected.js b/recipes/ansi-colors-to-styletext/tests/unsupported-named-esm/expected.js new file mode 100644 index 00000000..5cd2f8fc --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-named-esm/expected.js @@ -0,0 +1,3 @@ +import { red, hasColor as containsColor } from 'ansi-colors'; +red('text'); +containsColor('text'); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-named-esm/input.js b/recipes/ansi-colors-to-styletext/tests/unsupported-named-esm/input.js new file mode 100644 index 00000000..5cd2f8fc --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-named-esm/input.js @@ -0,0 +1,3 @@ +import { red, hasColor as containsColor } from 'ansi-colors'; +red('text'); +containsColor('text'); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-property-cjs/expected.js b/recipes/ansi-colors-to-styletext/tests/unsupported-property-cjs/expected.js new file mode 100644 index 00000000..24d855a1 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-property-cjs/expected.js @@ -0,0 +1,2 @@ +const ac = require('ansi-colors'); +ac.noop('text'); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-property-cjs/input.js b/recipes/ansi-colors-to-styletext/tests/unsupported-property-cjs/input.js new file mode 100644 index 00000000..24d855a1 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-property-cjs/input.js @@ -0,0 +1,2 @@ +const ac = require('ansi-colors'); +ac.noop('text'); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-property-dynamic/expected.js b/recipes/ansi-colors-to-styletext/tests/unsupported-property-dynamic/expected.js new file mode 100644 index 00000000..cbc40f75 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-property-dynamic/expected.js @@ -0,0 +1,3 @@ +const ac = await import('ansi-colors'); +ac.red('text'); +ac.clear('text'); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-property-dynamic/input.js b/recipes/ansi-colors-to-styletext/tests/unsupported-property-dynamic/input.js new file mode 100644 index 00000000..cbc40f75 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-property-dynamic/input.js @@ -0,0 +1,3 @@ +const ac = await import('ansi-colors'); +ac.red('text'); +ac.clear('text'); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-property-mixed/expected.js b/recipes/ansi-colors-to-styletext/tests/unsupported-property-mixed/expected.js new file mode 100644 index 00000000..744c4f21 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-property-mixed/expected.js @@ -0,0 +1,3 @@ +import ac from 'ansi-colors'; +ac.red('text'); +ac.unstyle(ac.bold.noop.red('text')); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-property-mixed/input.js b/recipes/ansi-colors-to-styletext/tests/unsupported-property-mixed/input.js new file mode 100644 index 00000000..744c4f21 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-property-mixed/input.js @@ -0,0 +1,3 @@ +import ac from 'ansi-colors'; +ac.red('text'); +ac.unstyle(ac.bold.noop.red('text')); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-warnings/expected.js b/recipes/ansi-colors-to-styletext/tests/unsupported-warnings/expected.js new file mode 100644 index 00000000..8fa02523 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-warnings/expected.js @@ -0,0 +1,7 @@ +const ac = require('ansi-colors'); +ac.enabled = false; +ac.visible = false; +ac.alias('error', ac.bold.red); +ac.theme({ error: ac.bold.red }); + +console.log(ac.red('text')); diff --git a/recipes/ansi-colors-to-styletext/tests/unsupported-warnings/input.js b/recipes/ansi-colors-to-styletext/tests/unsupported-warnings/input.js new file mode 100644 index 00000000..8fa02523 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/unsupported-warnings/input.js @@ -0,0 +1,7 @@ +const ac = require('ansi-colors'); +ac.enabled = false; +ac.visible = false; +ac.alias('error', ac.bold.red); +ac.theme({ error: ac.bold.red }); + +console.log(ac.red('text')); diff --git a/recipes/ansi-colors-to-styletext/tests/with-import/expected.js b/recipes/ansi-colors-to-styletext/tests/with-import/expected.js new file mode 100644 index 00000000..7e778034 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/with-import/expected.js @@ -0,0 +1,5 @@ +import { styleText } from 'node:util'; + +const error = styleText('red', 'Error'); +const success = styleText('green', 'Success'); +const important = styleText('bold', 'Important'); diff --git a/recipes/ansi-colors-to-styletext/tests/with-import/input.js b/recipes/ansi-colors-to-styletext/tests/with-import/input.js new file mode 100644 index 00000000..e75e039a --- /dev/null +++ b/recipes/ansi-colors-to-styletext/tests/with-import/input.js @@ -0,0 +1,5 @@ +import { red, green, bold } from 'ansi-colors'; + +const error = red('Error'); +const success = green('Success'); +const important = bold('Important'); diff --git a/recipes/ansi-colors-to-styletext/workflow.yaml b/recipes/ansi-colors-to-styletext/workflow.yaml new file mode 100644 index 00000000..10f01be3 --- /dev/null +++ b/recipes/ansi-colors-to-styletext/workflow.yaml @@ -0,0 +1,43 @@ +# 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 ansi-colors to Node.js 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 + semantic_analysis: file + + - id: remove-dependencies + name: Remove ansi-colors dependency + type: automatic + steps: + - name: Detect package manager and remove ansi-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 \ No newline at end of file