Skip to content

refactor: replaces picocolors with node:util's styleText #346

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/wild-boats-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clack/prompts": minor
"@clack/core": minor
---

Replaces `picocolors` with Node.js built-in `styleText`
24 changes: 12 additions & 12 deletions examples/basic/autocomplete-multiselect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as p from '@clack/prompts';
import color from 'picocolors';
import { styleText } from 'node:util';

/**
* Example demonstrating the integrated autocomplete multiselect component
Expand All @@ -9,17 +9,17 @@ import color from 'picocolors';
async function main() {
console.clear();

p.intro(`${color.bgCyan(color.black(' Integrated Autocomplete Multiselect Example '))}`);
p.intro(`${styleText('bgCyan', styleText('black', ' Integrated Autocomplete Multiselect Example '))}`);

p.note(
`
${color.cyan('Filter and select multiple items in a single interface:')}
- ${color.yellow('Type')} to filter the list in real-time
- Use ${color.yellow('up/down arrows')} to navigate with improved stability
- Press ${color.yellow('Space')} to select/deselect the highlighted item ${color.green('(multiple selections allowed)')}
- Use ${color.yellow('Backspace')} to modify your filter text when searching for different options
- Press ${color.yellow('Enter')} when done selecting all items
- Press ${color.yellow('Ctrl+C')} to cancel
${styleText('cyan', 'Filter and select multiple items in a single interface:')}
- ${styleText('yellow', 'Type')} to filter the list in real-time
- Use ${styleText('yellow', 'up/down arrows')} to navigate with improved stability
- Press ${styleText('yellow', 'Space')} to select/deselect the highlighted item ${styleText('green', '(multiple selections allowed)')}
- Use ${styleText('yellow', 'Backspace')} to modify your filter text when searching for different options
- Press ${styleText('yellow', 'Enter')} when done selecting all items
- Press ${styleText('yellow', 'Ctrl+C')} to cancel
`,
'Instructions'
);
Expand Down Expand Up @@ -79,7 +79,7 @@ ${color.cyan('Filter and select multiple items in a single interface:')}

// Display selected frameworks with detailed information
p.note(
`You selected ${color.green(selectedFrameworks.length)} frameworks:`,
`You selected ${styleText('green', `${selectedFrameworks.length}`)} frameworks:`,
'Selection Complete'
);

Expand All @@ -88,13 +88,13 @@ ${color.cyan('Filter and select multiple items in a single interface:')}
.map((value) => {
const framework = frameworks.find((f) => f.value === value);
return framework
? `${color.cyan(framework.label)} ${color.dim(`- ${framework.hint}`)}`
? `${styleText('cyan', framework.label)} ${styleText('dim', `- ${framework.hint}`)}`
: value;
})
.join('\n');

p.log.message(selectedDetails);
p.outro(`Successfully selected ${color.green(selectedFrameworks.length)} frameworks.`);
p.outro(`Successfully selected ${styleText('green', `${selectedFrameworks.length}`)} frameworks.`);
}

main().catch(console.error);
22 changes: 14 additions & 8 deletions examples/basic/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import * as p from '@clack/prompts';
import color from 'picocolors';
import { styleText } from 'node:util';

async function main() {
console.clear();

p.intro(`${color.bgCyan(color.black(' Autocomplete Example '))}`);
p.intro(`${styleText('bgCyan', styleText('black', ' Autocomplete Example '))}`);

p.note(
`
${color.cyan('This example demonstrates the type-ahead autocomplete feature:')}
- ${color.yellow('Type')} to filter the list in real-time
- Use ${color.yellow('up/down arrows')} to navigate the filtered results
- Press ${color.yellow('Enter')} to select the highlighted option
- Press ${color.yellow('Ctrl+C')} to cancel
${styleText('cyan', 'This example demonstrates the type-ahead autocomplete feature:')}
- ${styleText('yellow', 'Type')} to filter the list in real-time
- Use ${styleText('yellow', 'up/down arrows')} to navigate the filtered results
- Press ${styleText('yellow', 'Enter')} to select the highlighted option
- Press ${styleText('yellow', 'Ctrl+C')} to cancel
`,
'Instructions'
);
Expand Down Expand Up @@ -53,7 +53,13 @@ ${color.cyan('This example demonstrates the type-ahead autocomplete feature:')}
}

const selected = countries.find((c) => c.value === result);
p.outro(`You selected: ${color.cyan(selected?.label)} (${color.yellow(selected?.hint)})`);

if (!selected) {
p.outro('No country selected.');
process.exit(1);
}

p.outro(`You selected: ${styleText('cyan', selected?.label)} (${styleText('yellow', selected?.hint)})`);
}

main().catch(console.error);
4 changes: 2 additions & 2 deletions examples/basic/default-value.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as p from '@clack/prompts';
import color from 'picocolors';
import { styleText } from 'node:util';

async function main() {
const defaultPath = 'my-project';
Expand All @@ -24,7 +24,7 @@ async function main() {
process.exit(0);
}

p.outro(`Let's bootstrap the project in ${color.cyan(result)}`);
p.outro(`Let's bootstrap the project in ${styleText('cyan', result)}`);
}

main().catch(console.error);
6 changes: 3 additions & 3 deletions examples/basic/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { setTimeout } from 'node:timers/promises';
import * as p from '@clack/prompts';
import color from 'picocolors';
import { styleText } from 'node:util';

async function main() {
console.clear();
Expand All @@ -16,7 +16,7 @@ async function main() {
},
});

p.intro(`${color.bgCyan(color.black(' create-app '))}`);
p.intro(`${styleText('bgCyan', styleText('black', ' create-app '))}`);

const project = await p.group(
{
Expand Down Expand Up @@ -87,7 +87,7 @@ async function main() {

p.note(nextSteps, 'Next steps.');

p.outro(`Problems? ${color.underline(color.cyan('https://example.com/issues'))}`);
p.outro(`Problems? ${styleText('underline', styleText('cyan', 'https://example.com/issues'))}`);
}

main().catch(console.error);
9 changes: 7 additions & 2 deletions examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,23 @@
"type": "module",
"dependencies": {
"@clack/prompts": "workspace:*",
"picocolors": "^1.0.0",
"jiti": "^1.17.0"
},
"scripts": {
"autocomplete": "jiti ./autocomplete.ts",
"autocomplete-multiselect": "jiti ./autocomplete-multiselect.ts",
"default-value": "jiti ./default-value.ts",
"spinner-cancel": "jiti ./spinner-cancel.ts",
"spinner-cancel-advanced": "jiti ./spinner-cancel-advanced.ts",
"start": "jiti ./index.ts",
"stream": "jiti ./stream.ts",
"progress": "jiti ./progress.ts",
"spinner": "jiti ./spinner.ts",
"path": "jiti ./path.ts",
"spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts",
"spinner-timer": "jiti ./spinner-timer.ts",
"task-log": "jiti ./task-log.ts"
"task-log": "jiti ./task-log.ts",
"text-validation": "jiti ./text-validation.ts"
},
"devDependencies": {
"cross-env": "^7.0.3"
Expand Down
2 changes: 1 addition & 1 deletion examples/basic/spinner-ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as p from '@clack/prompts';
const s = p.spinner();
let progress = 0;
let counter = 0;
let loop: NodeJS.Timer;
let loop: NodeJS.Timeout;

p.intro('Running spinner in CI environment');
s.start('spinner.start');
Expand Down
6 changes: 3 additions & 3 deletions examples/basic/stream.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { setTimeout } from 'node:timers/promises';
import * as p from '@clack/prompts';
import color from 'picocolors';
import { styleText } from 'node:util';

async function main() {
console.clear();

await setTimeout(1000);

p.intro(`${color.bgCyan(color.black(' create-app '))}`);
p.intro(`${styleText('bgCyan', styleText('black', ' create-app '))}`);

await p.stream.step(
(async function* () {
Expand All @@ -25,7 +25,7 @@ async function main() {
})()
);

p.outro(`Problems? ${color.underline(color.cyan('https://example.com/issues'))}`);
p.outro(`Problems? ${styleText('underline', styleText('cyan', 'https://example.com/issues'))}`);
}

const lorem = [
Expand Down
2 changes: 2 additions & 0 deletions examples/basic/text-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ async function main() {
message: 'Enter your name (letters and spaces only)',
initialValue: 'John123', // Invalid initial value with numbers
validate: (value) => {
if (!value) return 'Name is required';
if (!/^[a-zA-Z\s]+$/.test(value)) return 'Name can only contain letters and spaces';
return undefined;
},
Expand All @@ -25,6 +26,7 @@ async function main() {
message: 'Enter another name (letters and spaces only)',
initialValue: 'John Doe', // Valid initial value
validate: (value) => {
if (!value) return 'Name is required';
if (!/^[a-zA-Z\s]+$/.test(value)) return 'Name can only contain letters and spaces';
return undefined;
},
Expand Down
14 changes: 7 additions & 7 deletions examples/changesets/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { setTimeout } from 'node:timers/promises';
import * as p from '@clack/prompts';
import color from 'picocolors';
import { styleText } from 'node:util';

function onCancel() {
p.cancel('Operation cancelled.');
Expand All @@ -12,7 +12,7 @@ async function main() {

await setTimeout(1000);

p.intro(`${color.bgCyan(color.black(' changesets '))}`);
p.intro(`${styleText('bgCyan', styleText('black', ' changesets '))}`);

const changeset = await p.group(
{
Expand All @@ -35,7 +35,7 @@ async function main() {
major: ({ results }) => {
const packages = results.packages ?? [];
return p.multiselect({
message: `Which packages should have a ${color.red('major')} bump?`,
message: `Which packages should have a ${styleText('red', 'major')} bump?`,
options: packages.map((value) => ({ value })),
required: false,
});
Expand All @@ -46,7 +46,7 @@ async function main() {
const possiblePackages = packages.filter((pkg) => !major.includes(pkg));
if (possiblePackages.length === 0) return;
return p.multiselect({
message: `Which packages should have a ${color.yellow('minor')} bump?`,
message: `Which packages should have a ${styleText('yellow', 'minor')} bump?`,
options: possiblePackages.map((value) => ({ value })),
required: false,
});
Expand All @@ -59,9 +59,9 @@ async function main() {
(pkg) => !major.includes(pkg) && !minor.includes(pkg)
);
if (possiblePackages.length === 0) return;
const note = possiblePackages.join(color.dim(', '));
const note = possiblePackages.join(styleText('dim', ', '));

p.log.step(`These packages will have a ${color.green('patch')} bump.\n${color.dim(note)}`);
p.log.step(`These packages will have a ${styleText('green', 'patch')} bump.\n${styleText('dim', note)}`);
return possiblePackages;
},
},
Expand All @@ -79,7 +79,7 @@ async function main() {
return onCancel();
}

p.outro(`Changeset added! ${color.underline(color.cyan('.changeset/orange-crabs-sing.md'))}`);
p.outro(`Changeset added! ${styleText('underline', styleText('cyan', '.changeset/orange-crabs-sing.md'))}`);
}

main().catch(console.error);
3 changes: 1 addition & 2 deletions examples/changesets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
"type": "module",
"dependencies": {
"jiti": "^1.17.0",
"@clack/prompts": "workspace:*",
"picocolors": "^1.0.0"
"@clack/prompts": "workspace:*"
},
"scripts": {
"start": "jiti ./index.ts"
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@changesets/cli": "^2.26.2",
"@types/node": "^18.16.0",
"@types/node": "^20.12.0",
"jsr": "^0.13.4",
"knip": "^5.50.4",
"typescript": "^5.8.3",
"unbuild": "^2.0.0",
"jsr": "^0.13.4"
"unbuild": "^2.0.0"
},
"packageManager": "pnpm@9.14.2",
"packageManager": "pnpm@10.12.1",
"volta": {
"node": "20.18.1"
}
Expand Down
1 change: 0 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"test": "vitest run"
},
"dependencies": {
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
},
"devDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/prompts/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Key } from 'node:readline';
import color from 'picocolors';
import { styleText } from 'node:util';
import Prompt, { type PromptOptions } from './prompt.js';

interface OptionLike {
Expand Down Expand Up @@ -71,14 +71,14 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<

get userInputWithCursor() {
if (!this.userInput) {
return color.inverse(color.hidden('_'));
return styleText(['inverse', 'hidden'], '_');
}
if (this._cursor >= this.userInput.length) {
return `${this.userInput}█`;
}
const s1 = this.userInput.slice(0, this._cursor);
const [s2, ...s3] = this.userInput.slice(this._cursor);
return `${s1}${color.inverse(s2)}${s3.join('')}`;
return `${s1}${styleText(['inverse'], s2)}${s3.join('')}`;
}

get options(): T[] {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/prompts/password.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import color from 'picocolors';
import { styleText } from 'node:util';
import Prompt, { type PromptOptions } from './prompt.js';

interface PasswordOptions extends PromptOptions<string, PasswordPrompt> {
Expand All @@ -18,12 +18,12 @@ export default class PasswordPrompt extends Prompt<string> {
}
const userInput = this.userInput;
if (this.cursor >= userInput.length) {
return `${this.masked}${color.inverse(color.hidden('_'))}`;
return `${this.masked}${styleText(['inverse', 'hidden'], '_')}`;
}
const masked = this.masked;
const s1 = masked.slice(0, this.cursor);
const s2 = masked.slice(this.cursor);
return `${s1}${color.inverse(s2[0])}${s2.slice(1)}`;
return `${s1}${styleText(['inverse'], s2[0])}${s2.slice(1)}`;
}
constructor({ mask, ...opts }: PasswordOptions) {
super(opts);
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/prompts/text.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import color from 'picocolors';
import { styleText } from 'node:util';
import Prompt, { type PromptOptions } from './prompt.js';

interface TextOptions extends PromptOptions<string, TextPrompt> {
Expand All @@ -17,7 +17,7 @@ export default class TextPrompt extends Prompt<string> {
}
const s1 = userInput.slice(0, this.cursor);
const [s2, ...s3] = userInput.slice(this.cursor);
return `${s1}${color.inverse(s2)}${s3.join('')}`;
return `${s1}${styleText(['inverse'], s2)}${s3.join('')}`;
}
get cursor() {
return this._cursor;
Expand Down
1 change: 0 additions & 1 deletion packages/core/test/prompts/confirm.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import color from 'picocolors';
import { cursor } from 'sisteransi';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { default as ConfirmPrompt } from '../../src/prompts/confirm.js';
Expand Down
Loading