Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4fac653
feat(chalk-to-util-styletext): add workflow with basic test
richiemccoll Oct 30, 2025
21781d6
feat(chalk-to-util-styletext): sync lockfile
richiemccoll Oct 30, 2025
6944457
feat(chalk-to-util-styletext): add commonjs require case
richiemccoll Oct 30, 2025
e628155
Merge branch 'main' into feat/chalk-styletext-migration
richiemccoll Oct 31, 2025
eaf5469
feat(chalk-to-util-styletext): add bg color case
richiemccoll Oct 31, 2025
8b29e26
feat(chalk-to-util-styletext): add chained case
richiemccoll Oct 31, 2025
0c2b44a
feat(chalk-to-util-styletext): add advanced modifiers case
richiemccoll Oct 31, 2025
54b3bb9
feat(chalk-to-util-styletext): add more modifiers to test case
richiemccoll Oct 31, 2025
90d0648
feat(chalk-to-util-styletext): add modifiers test case
richiemccoll Oct 31, 2025
1bf1736
feat(chalk-to-util-styletext): add mixed imports test case
richiemccoll Oct 31, 2025
6a14179
feat(chalk-to-util-styletext): add dynamic imports test case
richiemccoll Oct 31, 2025
cefb46c
Merge branch 'main' into feat/chalk-styletext-migration
richiemccoll Oct 31, 2025
324fb50
feat(chalk-to-util-styletext): refactoring
richiemccoll Oct 31, 2025
fa51707
feat(chalk-to-util-styletext): code review comments
richiemccoll Nov 1, 2025
27a0ad2
Merge branch 'main' into feat/chalk-styletext-migration
richiemccoll Nov 3, 2025
0482005
feat(chalk-to-util-styletext): code review comments
richiemccoll Nov 3, 2025
8c762aa
Merge branch 'feat/chalk-styletext-migration' of github.com:richiemcc…
richiemccoll Nov 3, 2025
f2a5a8b
feat(chalk-to-util-styletext): handle different import cases
richiemccoll Nov 4, 2025
09e8eaf
feat(chalk-to-util-styletext): add unsupported features test
richiemccoll Nov 4, 2025
412f1ee
Merge branch 'main' into feat/chalk-styletext-migration
richiemccoll Nov 5, 2025
34c97cd
feat(chalk-to-util-styletext): warn on unsupported method
richiemccoll Nov 6, 2025
6d5433d
Merge branch 'feat/chalk-styletext-migration' of github.com:richiemcc…
richiemccoll Nov 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions recipes/chalk-to-util-styletext/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# chalk-to-util-styletext

TODO
21 changes: 21 additions & 0 deletions recipes/chalk-to-util-styletext/codemod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
schema_version: "1.0"
name: "@nodejs/chalk-to-util-styletext"
version: 1.0.0
description: Migrate from the chalk package to Node.js's built-in util.styleText API
author: Richie McColl
license: MIT
workflow: workflow.yaml
category: migration

targets:
languages:
- javascript
- typescript

keywords:
- transformation
- migration

registry:
access: public
visibility: public
24 changes: 24 additions & 0 deletions recipes/chalk-to-util-styletext/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@nodejs/chalk-to-util-styletext",
"version": "1.0.0",
"description": "Migrate from the chalk package to Node.js's built-in util.styleText API",
"type": "module",
"scripts": {
"test": "npx codemod jssg test -l typescript ./src/workflow.ts ./"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nodejs/userland-migrations.git",
"directory": "recipes/chalk-to-util-styletext",
"bugs": "https://github.com/nodejs/userland-migrations/issues"
},
"author": "Richie McColl",
"license": "MIT",
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/chalk-to-util-styletext/README.md",
"devDependencies": {
"@codemod.com/jssg-types": "^1.0.9"
},
"dependencies": {
"@nodejs/codemod-utils": "*"
}
}
166 changes: 166 additions & 0 deletions recipes/chalk-to-util-styletext/src/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call";
import {
getNodeImportCalls,
getNodeImportStatements,
} from "@nodejs/codemod-utils/ast-grep/import-statement";
import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path";
import type { Edit, Range, SgNode, SgRoot } from "@codemod.com/jssg-types/main";
import type Js from "@codemod.com/jssg-types/langs/javascript";

/**
* Transform function that converts chalk method calls to Node.js util.styleText calls.
*
* Examples:
* - chalk.red("text") → styleText("red", "text")
* - chalk.red.bold("text") → styleText(["red", "bold"], "text")
*/
export default function transform(root: SgRoot<Js>): string | null {
const rootNode = root.root();
const edits: Edit[] = [];
const chalkBinding = "chalk";

// This actually catches `node:chalk` import but we don't care as it shouldn't append
const statements = [
...getNodeImportStatements(root, chalkBinding),
...getNodeRequireCalls(root, chalkBinding),
...getNodeImportCalls(root, chalkBinding),
];

// If there aren't any imports then we don't process the file
if (!statements.length) return null;

for (const statement of statements) {
const binding = resolveBindingPath(statement, "$");

if (!binding) continue;

const chalkCalls = rootNode.findAll({
rule: {
any: [
// Pattern 1: single method calls: chalk.method(text)
{ pattern: `${binding}.$METHOD($TEXT)` },
// Pattern 2: chained method calls: chalk.method1.method2(text));
{
kind: "call_expression",
has: {
field: "function",
kind: "member_expression",
},
},
],
},
});

for (const call of chalkCalls) {
const methodMatch = call.getMatch("METHOD");
const textMatch = call.getMatch("TEXT");

if (methodMatch && textMatch) {
// Pattern 1: chalk.method(text) → styleText("method", text)
const method = methodMatch.text();
const text = textMatch.text();
const styleMethod = COMPAT_MAP[method] || method;
const replacement = `styleText("${styleMethod}", ${text})`;

edits.push(call.replace(replacement));
} else {
// Pattern 2: chalk.method1.method2(text) → styleText(["method1", "method2"], text)
const functionExpr = call.field("function");

if (!functionExpr) continue;

const styles = extractChalkStyles(functionExpr, binding);

if (styles.length === 0) continue;

const args = call.field("arguments");

if (!args) continue;

const argsList = args.children().filter((c) => {
const excluded = [",", "(", ")"];
return !excluded.includes(c.kind());
});

if (argsList.length === 0) continue;

const textArg = argsList[0].text();

if (styles.length === 1) {
const replacement = `styleText("${styles[0]}", ${textArg})`;

edits.push(call.replace(replacement));
} else {
const stylesArray = `[${styles.map((s) => `"${s}"`).join(", ")}]`;
const replacement = `styleText(${stylesArray}, ${textArg})`;

edits.push(call.replace(replacement));
}
}
}

if (edits.length > 0) {
// Update the import or require statements if any calls were transformed
if (statement.kind() === "import_statement") {
// Replace entire import statement
edits.push(statement.replace(`import { styleText } from "node:util";`));
} else if (statement.kind() === "variable_declarator") {
// Handle dynamic ESM import
if (statement.field("value")?.kind() === "await_expression") {
edits.push(statement.replace(`{ styleText } = await import("node:util")`));
} else {
// Handle CommonJS require
edits.push(statement.replace(`{ styleText } = require("node:util")`));
}
}
}
}

if (!edits.length) return null;

const sourceCode = rootNode.commitEdits(edits);

return sourceCode;
}

// Compatibility mapping for chalk properties that differ in util.styleText
const COMPAT_MAP: Record<string, string> = {
overline: "overlined",
};

/**
* Traverses a member expression node to extract chained chalk styles.
* and returns a list of styles in the order they were called.
*/
function extractChalkStyles(node: SgNode<Js>, chalkBinding: string): string[] {
const styles: string[] = [];

function traverse(node: SgNode<Js>): boolean {
const obj = node.field("object");
const prop = node.field("property");

if (obj && prop && prop.kind() === "property_identifier") {
const propName = prop.text();

if (obj.kind() === "identifier" && obj.text() === chalkBinding) {
// Base case: chalk.method
styles.push(COMPAT_MAP[propName] || propName);

return true;
}

if (obj.kind() === "member_expression" && traverse(obj)) {
// Recursive case: chain.method
styles.push(COMPAT_MAP[propName] || propName);

return true;
}
}

return false;
}

traverse(node);

return styles;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { styleText } from "node:util";

// Some of these have limited terminal support - may not work in all environments
console.log(styleText("bold", "Bold text"));
console.log(styleText("blink", "Blinking text"));
console.log(styleText("dim", "Dimmed text"));
console.log(styleText("doubleunderline", "Double underlined"));
console.log(styleText("framed", "Framed text"));
console.log(styleText("italic", "Italic text"));
console.log(styleText("inverse", "Inverted colors"));
console.log(styleText("hidden", "Hidden text"));
console.log(styleText("overlined", "Overlined text"));
console.log(styleText("reset", "Reset text"));
console.log(styleText("strikethrough", "Strikethrough text"));
console.log(styleText("underline", "Underlined text"));
5 changes: 5 additions & 0 deletions recipes/chalk-to-util-styletext/tests/expected/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { styleText } from "node:util";

console.log(styleText(["bgRed", "white"], "Error on red background"));
console.log(styleText(["bgGreen", "black"], "Success on green background"));
console.log(styleText(["bgBlue", "whiteBright"], "Info on blue background"));
5 changes: 5 additions & 0 deletions recipes/chalk-to-util-styletext/tests/expected/basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { styleText } from "node:util";

console.log(styleText("red", "Error message"));
console.log(styleText("green", "Success message"));
console.log(styleText("blue", "Info message"));
5 changes: 5 additions & 0 deletions recipes/chalk-to-util-styletext/tests/expected/chained.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { styleText } from "node:util";

console.log(styleText(["red", "bold"], "Error: Operation failed"));
console.log(styleText(["green", "underline"], "Success: All tests passed"));
console.log(styleText(["yellow", "bgBlack"], "Warning: Deprecated API usage"));
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { styleText } = require("node:util");

const error = styleText("red", "Error");
const warning = styleText("yellow", "Warning");
const info = styleText("blue", "Info");

console.log(error, warning, info);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// CommonJS style dynamic import
async function testCommonJS() {
const { styleText } = await import("node:util");
console.log(styleText("bgBlue", "This is a message"));
}

// ESM style dynamic import
const { styleText } = await import("node:util");
console.log(styleText("bgRed", "This is a message"));
13 changes: 13 additions & 0 deletions recipes/chalk-to-util-styletext/tests/expected/mixed-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { styleText } from "node:util";
import { otherFunction } from "./utils";

function logError(message) {
console.log(styleText(["red", "bold"], `ERROR: ${message}`));
}

function logSuccess(message) {
console.log(styleText("green", `SUCCESS: ${message}`));
}

logError("Something went wrong");
logSuccess("Operation completed");
5 changes: 5 additions & 0 deletions recipes/chalk-to-util-styletext/tests/expected/modifiers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { styleText } from "node:util";

console.log(styleText(["bold", "italic", "underline"], "Important announcement"));
console.log(styleText(["dim", "strikethrough"], "Deprecated feature"));
console.log(styleText("inverse", "Inverted colors"));
15 changes: 15 additions & 0 deletions recipes/chalk-to-util-styletext/tests/input/advanced-modifiers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import chalk from "chalk";

// Some of these have limited terminal support - may not work in all environments
console.log(chalk.bold("Bold text"));
console.log(chalk.blink("Blinking text"));
console.log(chalk.dim("Dimmed text"));
console.log(chalk.doubleunderline("Double underlined"));
console.log(chalk.framed("Framed text"));
console.log(chalk.italic("Italic text"));
console.log(chalk.inverse("Inverted colors"));
console.log(chalk.hidden("Hidden text"));
console.log(chalk.overline("Overlined text"));
console.log(chalk.reset("Reset text"));
console.log(chalk.strikethrough("Strikethrough text"));
console.log(chalk.underline("Underlined text"));
5 changes: 5 additions & 0 deletions recipes/chalk-to-util-styletext/tests/input/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import chalk from "chalk";

console.log(chalk.bgRed.white("Error on red background"));
console.log(chalk.bgGreen.black("Success on green background"));
console.log(chalk.bgBlue.whiteBright("Info on blue background"));
5 changes: 5 additions & 0 deletions recipes/chalk-to-util-styletext/tests/input/basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import chalk from "chalk";

console.log(chalk.red("Error message"));
console.log(chalk.green("Success message"));
console.log(chalk.blue("Info message"));
5 changes: 5 additions & 0 deletions recipes/chalk-to-util-styletext/tests/input/chained.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import chalk from "chalk";

console.log(chalk.red.bold("Error: Operation failed"));
console.log(chalk.green.underline("Success: All tests passed"));
console.log(chalk.yellow.bgBlack("Warning: Deprecated API usage"));
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const chalk = require("chalk");

const error = chalk.red("Error");
const warning = chalk.yellow("Warning");
const info = chalk.blue("Info");

console.log(error, warning, info);
9 changes: 9 additions & 0 deletions recipes/chalk-to-util-styletext/tests/input/dynamic-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// CommonJS style dynamic import
async function testCommonJS() {
const chalk = await import("chalk");
console.log(chalk.bgBlue("This is a message"));
}

// ESM style dynamic import
const chalk = await import("chalk");
console.log(chalk.bgRed("This is a message"));
Loading
Loading