diff --git a/examples/basic/log.ts b/examples/basic/log.ts new file mode 100644 index 00000000..be6207d3 --- /dev/null +++ b/examples/basic/log.ts @@ -0,0 +1,25 @@ +import { setTimeout } from 'node:timers/promises'; +import * as p from '@clack/prompts'; +import color from 'picocolors'; + +async function main() { + console.clear(); + + await setTimeout(1000); + + p.intro(`${color.bgCyan(color.black(' create-app '))}`); + + await p.log.message('Entering directory "src"'); + await p.log.info('No files to update'); + await p.log.warn('Directory is empty, skipping'); + await p.log.warning('Directory is empty, skipping'); + await p.log.error('Permission denied on file src/secret.js'); + await p.log.success('Installation complete'); + await p.log.step('Check files'); + await p.log.step('Line 1\nLine 2'); + await p.log.step(['Line 1 (array)', 'Line 2 (array)']); + + p.outro(`Problems? ${color.underline(color.cyan('https://example.com/issues'))}`); +} + +main().catch(console.error); diff --git a/examples/basic/package.json b/examples/basic/package.json index e0f6b143..1cc692ac 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -9,6 +9,7 @@ "jiti": "^1.17.0" }, "scripts": { + "log": "jiti ./log.ts", "start": "jiti ./index.ts", "stream": "jiti ./stream.ts", "progress": "jiti ./progress.ts", diff --git a/examples/basic/stream.ts b/examples/basic/stream.ts index c87ad5bf..cd47f216 100644 --- a/examples/basic/stream.ts +++ b/examples/basic/stream.ts @@ -9,7 +9,7 @@ async function main() { p.intro(`${color.bgCyan(color.black(' create-app '))}`); - await p.stream.step( + await p.log.step( (async function* () { for (const line of lorem) { for (const word of line.split(' ')) { diff --git a/packages/prompts/package.json b/packages/prompts/package.json index 0571615f..c9da222e 100644 --- a/packages/prompts/package.json +++ b/packages/prompts/package.json @@ -51,6 +51,7 @@ }, "dependencies": { "@clack/core": "workspace:*", + "@macfja/ansi": "^1.0.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" }, diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 6adc59f9..18bfc009 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -14,7 +14,6 @@ export * from './progress-bar.js'; export * from './select-key.js'; export * from './select.js'; export * from './spinner.js'; -export * from './stream.js'; export * from './task.js'; export * from './task-log.js'; export * from './text.js'; diff --git a/packages/prompts/src/log.ts b/packages/prompts/src/log.ts index ff4e00cd..e5653385 100644 --- a/packages/prompts/src/log.ts +++ b/packages/prompts/src/log.ts @@ -1,4 +1,7 @@ +import { getColumns } from '@clack/core'; +import { wrap } from '@macfja/ansi'; import color from 'picocolors'; +import { cursor, erase } from 'sisteransi'; import { type CommonOptions, S_BAR, @@ -13,57 +16,70 @@ export interface LogMessageOptions extends CommonOptions { symbol?: string; spacing?: number; secondarySymbol?: string; + removeLeadingSpace?: boolean; } export const log = { - message: ( - message: string | string[] = [], + message: async ( + message: string | Iterable | AsyncIterable = [], { symbol = color.gray(S_BAR), + spacing = 0, secondarySymbol = color.gray(S_BAR), output = process.stdout, - spacing = 1, + removeLeadingSpace = true, }: LogMessageOptions = {} ) => { - const parts: string[] = []; - for (let i = 0; i < spacing; i++) { - parts.push(`${secondarySymbol}`); - } - const messageParts = Array.isArray(message) ? message : message.split('\n'); - if (messageParts.length > 0) { - const [firstLine, ...lines] = messageParts; - if (firstLine.length > 0) { - parts.push(`${symbol} ${firstLine}`); - } else { - parts.push(symbol); + output.write(`${color.gray(S_BAR)}\n`); + let first = true; + let lastLine = ''; + const iterable = typeof message === 'string' ? [message] : message; + const isAsync = Symbol.asyncIterator in iterable; + for await (let chunk of iterable) { + const width = getColumns(output); + if (first) { + lastLine = `${symbol} `; + chunk = '\n'.repeat(spacing) + chunk; + first = false; } - for (const ln of lines) { - if (ln.length > 0) { - parts.push(`${secondarySymbol} ${ln}`); - } else { - parts.push(secondarySymbol); - } + const newLineRE = removeLeadingSpace ? /\n */g : /\n/g; + const lines = + lastLine.substring(0, 3) + + wrap(`${lastLine.substring(3)}${chunk}`, width).replace( + newLineRE, + `\n${secondarySymbol} ` + ); + output?.write(cursor.move(-999, 0) + erase.lines(1)); + output?.write(lines); + lastLine = lines.substring(Math.max(0, lines.lastIndexOf('\n') + 1)); + if (!isAsync) { + lastLine = `${secondarySymbol} `; + output?.write('\n'); } } - output.write(`${parts.join('\n')}\n`); + if (isAsync) { + output.write('\n'); + } }, - info: (message: string, opts?: LogMessageOptions) => { - log.message(message, { ...opts, symbol: color.blue(S_INFO) }); + info: async (message: string, opts?: LogMessageOptions) => { + await log.message(message, { ...opts, symbol: color.blue(S_INFO) }); }, - success: (message: string, opts?: LogMessageOptions) => { - log.message(message, { ...opts, symbol: color.green(S_SUCCESS) }); + success: async (message: string, opts?: LogMessageOptions) => { + await log.message(message, { ...opts, symbol: color.green(S_SUCCESS) }); }, - step: (message: string, opts?: LogMessageOptions) => { - log.message(message, { ...opts, symbol: color.green(S_STEP_SUBMIT) }); + step: async (message: string, opts?: LogMessageOptions) => { + await log.message(message, { ...opts, symbol: color.green(S_STEP_SUBMIT) }); }, - warn: (message: string, opts?: LogMessageOptions) => { - log.message(message, { ...opts, symbol: color.yellow(S_WARN) }); + warn: async (message: string, opts?: LogMessageOptions) => { + await log.message(message, { ...opts, symbol: color.yellow(S_WARN) }); }, /** alias for `log.warn()`. */ - warning: (message: string, opts?: LogMessageOptions) => { - log.warn(message, opts); + warning: async (message: string, opts?: LogMessageOptions) => { + await log.warn(message, opts); }, - error: (message: string, opts?: LogMessageOptions) => { - log.message(message, { ...opts, symbol: color.red(S_ERROR) }); + error: async (message: string, opts?: LogMessageOptions) => { + await log.message(message, { ...opts, symbol: color.red(S_ERROR) }); }, }; + +export const stream = log; diff --git a/packages/prompts/src/stream.ts b/packages/prompts/src/stream.ts deleted file mode 100644 index 19e79c11..00000000 --- a/packages/prompts/src/stream.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { stripVTControlCharacters as strip } from 'node:util'; -import color from 'picocolors'; -import { S_BAR, S_ERROR, S_INFO, S_STEP_SUBMIT, S_SUCCESS, S_WARN } from './common.js'; -import type { LogMessageOptions } from './log.js'; - -const prefix = `${color.gray(S_BAR)} `; - -// TODO (43081j): this currently doesn't support custom `output` writables -// because we rely on `columns` existing (i.e. `process.stdout.columns). -// -// If we want to support `output` being passed in, we will need to use -// a condition like `if (output insance Writable)` to check if it has columns -export const stream = { - message: async ( - iterable: Iterable | AsyncIterable, - { symbol = color.gray(S_BAR) }: LogMessageOptions = {} - ) => { - process.stdout.write(`${color.gray(S_BAR)}\n${symbol} `); - let lineWidth = 3; - for await (let chunk of iterable) { - chunk = chunk.replace(/\n/g, `\n${prefix}`); - if (chunk.includes('\n')) { - lineWidth = 3 + strip(chunk.slice(chunk.lastIndexOf('\n'))).length; - } - const chunkLen = strip(chunk).length; - if (lineWidth + chunkLen < process.stdout.columns) { - lineWidth += chunkLen; - process.stdout.write(chunk); - } else { - process.stdout.write(`\n${prefix}${chunk.trimStart()}`); - lineWidth = 3 + strip(chunk.trimStart()).length; - } - } - process.stdout.write('\n'); - }, - info: (iterable: Iterable | AsyncIterable) => { - return stream.message(iterable, { symbol: color.blue(S_INFO) }); - }, - success: (iterable: Iterable | AsyncIterable) => { - return stream.message(iterable, { symbol: color.green(S_SUCCESS) }); - }, - step: (iterable: Iterable | AsyncIterable) => { - return stream.message(iterable, { symbol: color.green(S_STEP_SUBMIT) }); - }, - warn: (iterable: Iterable | AsyncIterable) => { - return stream.message(iterable, { symbol: color.yellow(S_WARN) }); - }, - /** alias for `log.warn()`. */ - warning: (iterable: Iterable | AsyncIterable) => { - return stream.warn(iterable); - }, - error: (iterable: Iterable | AsyncIterable) => { - return stream.message(iterable, { symbol: color.red(S_ERROR) }); - }, -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2eb8527e..356e5c2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,6 +79,9 @@ importers: '@clack/core': specifier: workspace:* version: link:../core + '@macfja/ansi': + specifier: ^1.0.0 + version: 1.0.0 picocolors: specifier: ^1.0.0 version: 1.0.0 @@ -716,6 +719,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@macfja/ansi@1.0.0': + resolution: {integrity: sha512-fcshx5NS+PFmIZL2UiA+kowuli0LZYxV+gVnzZowvQTEh/3QVOvs19VcsyZG55RfVJMFz7cbwwTaN5yrpvk5tQ==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -2245,6 +2251,10 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.escape@2.0.1: + resolution: {integrity: sha512-JItRb4rmyTzmERBkAf6J87LjDPy/RscIwmaJQ3gsFlAzrmZbZU8LwBw5IydFZXW9hqpgbPlGbMhtpqtuAhMgtg==} + engines: {node: '>= 0.4'} + regexp.prototype.flags@1.5.3: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} @@ -2692,6 +2702,10 @@ packages: engines: {node: '>=8'} hasBin: true + wordwrapjs@5.1.0: + resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==} + engines: {node: '>=12.17'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -3271,6 +3285,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@macfja/ansi@1.0.0': + dependencies: + regexp.escape: 2.0.1 + wordwrapjs: 5.1.0 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.26.0 @@ -4857,6 +4876,15 @@ snapshots: regenerator-runtime@0.14.1: {} + regexp.escape@2.0.1: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + for-each: 0.3.3 + safe-regex-test: 1.0.3 + regexp.prototype.flags@1.5.3: dependencies: call-bind: 1.0.7 @@ -5382,6 +5410,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wordwrapjs@5.1.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0