diff --git a/CHANGELOG.md b/CHANGELOG.md index a6f60fb932f5..7380b3635467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### Chore & Maintenance +- `[@jest-reporters]` Move helper functions from `utils.ts` into separate files ([#12782](https://github.com/facebook/jest/pull/12782)) + ### Performance ## 28.0.3 diff --git a/packages/jest-reporters/__typetests__/jest-reporters.test.ts b/packages/jest-reporters/__typetests__/jest-reporters.test.ts new file mode 100644 index 000000000000..ba538ed4bf5b --- /dev/null +++ b/packages/jest-reporters/__typetests__/jest-reporters.test.ts @@ -0,0 +1,97 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {expectError, expectType} from 'tsd-lite'; +import {utils} from '@jest/reporters'; +import type { + AggregatedResult, + Config, + SnapshotSummary, + SummaryOptions, + TestResult, +} from '@jest/reporters'; + +declare const aggregatedResults: AggregatedResult; +declare const globalConfig: Config.GlobalConfig; +declare const projectConfig: Config.ProjectConfig; +declare const snapshot: TestResult['snapshot']; +declare const snapshotSummary: SnapshotSummary; +declare const summaryOptions: SummaryOptions; +declare const testResult: TestResult; + +// utils.formatTestPath() + +expectType(utils.formatTestPath(globalConfig, 'some/path')); +expectType(utils.formatTestPath(projectConfig, 'some/path')); +expectError(utils.formatTestPath()); +expectError(utils.formatTestPath({}, 'some/path')); +expectError(utils.formatTestPath(globalConfig, 123)); +expectError(utils.formatTestPath(projectConfig, 123)); + +// utils.getResultHeader() + +expectType( + utils.getResultHeader(testResult, globalConfig, projectConfig), +); +expectType(utils.getResultHeader(testResult, globalConfig)); +expectError(utils.getResultHeader()); +expectError(utils.getResultHeader({}, globalConfig)); +expectError(utils.getResultHeader({}, globalConfig, projectConfig)); +expectError(utils.getResultHeader(testResult, {})); +expectError(utils.getResultHeader(testResult, globalConfig, {})); + +// utils.getSnapshotStatus() + +expectType>(utils.getSnapshotStatus(snapshot, true)); +expectError(utils.getSnapshotStatus()); +expectError(utils.getSnapshotStatus({}, true)); +expectError(utils.getSnapshotStatus(snapshot, 123)); + +// utils.getSnapshotSummary() + +expectType>( + utils.getSnapshotSummary(snapshotSummary, globalConfig, 'press `u`'), +); +expectError(utils.getSnapshotSummary()); +expectError(utils.getSnapshotSummary({}, globalConfig, 'press `u`')); +expectError(utils.getSnapshotSummary(snapshotSummary, {}, 'press `u`')); +expectError(utils.getSnapshotSummary(snapshotSummary, globalConfig, true)); + +// utils.getSummary() + +expectType(utils.getSummary(aggregatedResults, summaryOptions)); +expectType(utils.getSummary(aggregatedResults)); +expectError(utils.getSummary()); +expectError(utils.getSummary({})); +expectError(utils.getSummary(aggregatedResults, true)); + +// utils.printDisplayName() + +expectType(utils.printDisplayName(projectConfig)); +expectError(utils.printDisplayName()); +expectError(utils.printDisplayName({})); + +// utils.relativePath() + +expectType<{basename: string; dirname: string}>( + utils.relativePath(globalConfig, 'some/path'), +); +expectType<{basename: string; dirname: string}>( + utils.relativePath(projectConfig, 'some/path'), +); +expectError(utils.relativePath()); +expectError(utils.relativePath({}, 'some/path')); +expectError(utils.relativePath(projectConfig, true)); + +// utils.trimAndFormatPath() + +expectType(utils.trimAndFormatPath(2, globalConfig, 'some/path', 4)); +expectError(utils.trimAndFormatPath()); +expectError(utils.trimAndFormatPath(true, globalConfig, 'some/path', 4)); +expectError(utils.trimAndFormatPath(2, {}, 'some/path', 4)); +expectError(utils.trimAndFormatPath(2, globalConfig, true, 4)); +expectError(utils.trimAndFormatPath(2, globalConfig, 'some/path', '4')); diff --git a/packages/jest-reporters/__typetests__/tsconfig.json b/packages/jest-reporters/__typetests__/tsconfig.json new file mode 100644 index 000000000000..165ba1343021 --- /dev/null +++ b/packages/jest-reporters/__typetests__/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "composite": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "skipLibCheck": true, + + "types": [] + }, + "include": ["./**/*"] +} diff --git a/packages/jest-reporters/package.json b/packages/jest-reporters/package.json index bb3da3f57f8a..433bda4df97c 100644 --- a/packages/jest-reporters/package.json +++ b/packages/jest-reporters/package.json @@ -33,11 +33,13 @@ "jest-worker": "^28.0.2", "slash": "^3.0.0", "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", "terminal-link": "^2.0.0", "v8-to-istanbul": "^9.0.0" }, "devDependencies": { "@jest/test-utils": "^28.0.2", + "@tsd/typescript": "~4.6.2", "@types/exit": "^0.1.30", "@types/glob": "^7.1.1", "@types/graceful-fs": "^4.1.3", @@ -49,7 +51,7 @@ "@types/node-notifier": "^8.0.0", "jest-resolve": "^28.0.3", "mock-fs": "^5.1.2", - "strip-ansi": "^6.0.0" + "tsd-lite": "^0.5.1" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" diff --git a/packages/jest-reporters/src/Status.ts b/packages/jest-reporters/src/Status.ts index e357b0d9ddc0..5236e14373f5 100644 --- a/packages/jest-reporters/src/Status.ts +++ b/packages/jest-reporters/src/Status.ts @@ -14,13 +14,11 @@ import type { TestResult, } from '@jest/test-result'; import type {Config} from '@jest/types'; +import getSummary from './getSummary'; +import printDisplayName from './printDisplayName'; +import trimAndFormatPath from './trimAndFormatPath'; import type {ReporterOnStartOptions} from './types'; -import { - getSummary, - printDisplayName, - trimAndFormatPath, - wrapAnsiString, -} from './utils'; +import wrapAnsiString from './wrapAnsiString'; const RUNNING_TEXT = ' RUNS '; const RUNNING = `${chalk.reset.inverse.yellow.bold(RUNNING_TEXT)} `; diff --git a/packages/jest-reporters/src/SummaryReporter.ts b/packages/jest-reporters/src/SummaryReporter.ts index cef51dda1b36..5103d3f52fcc 100644 --- a/packages/jest-reporters/src/SummaryReporter.ts +++ b/packages/jest-reporters/src/SummaryReporter.ts @@ -16,8 +16,8 @@ import {testPathPatternToRegExp} from 'jest-util'; import BaseReporter from './BaseReporter'; import getResultHeader from './getResultHeader'; import getSnapshotSummary from './getSnapshotSummary'; +import getSummary from './getSummary'; import type {ReporterOnStartOptions} from './types'; -import {getSummary} from './utils'; const TEST_SUMMARY_THRESHOLD = 20; diff --git a/packages/jest-reporters/src/__tests__/utils.test.ts b/packages/jest-reporters/src/__tests__/utils.test.ts index 855ba9222fc6..f832fa52b1ad 100644 --- a/packages/jest-reporters/src/__tests__/utils.test.ts +++ b/packages/jest-reporters/src/__tests__/utils.test.ts @@ -9,7 +9,9 @@ import * as path from 'path'; import chalk = require('chalk'); import stripAnsi = require('strip-ansi'); import {makeProjectConfig} from '@jest/test-utils'; -import {printDisplayName, trimAndFormatPath, wrapAnsiString} from '../utils'; +import printDisplayName from '../printDisplayName'; +import trimAndFormatPath from '../trimAndFormatPath'; +import wrapAnsiString from '../wrapAnsiString'; describe('wrapAnsiString()', () => { it('wraps a long string containing ansi chars', () => { diff --git a/packages/jest-reporters/src/formatTestPath.ts b/packages/jest-reporters/src/formatTestPath.ts new file mode 100644 index 000000000000..2970eda4a17b --- /dev/null +++ b/packages/jest-reporters/src/formatTestPath.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as path from 'path'; +import chalk = require('chalk'); +import slash = require('slash'); +import type {Config} from '@jest/types'; +import relativePath from './relativePath'; + +export default function formatTestPath( + config: Config.GlobalConfig | Config.ProjectConfig, + testPath: string, +): string { + const {dirname, basename} = relativePath(config, testPath); + return slash(chalk.dim(dirname + path.sep) + chalk.bold(basename)); +} diff --git a/packages/jest-reporters/src/getResultHeader.ts b/packages/jest-reporters/src/getResultHeader.ts index b3626bd03c36..8f74f761fb0d 100644 --- a/packages/jest-reporters/src/getResultHeader.ts +++ b/packages/jest-reporters/src/getResultHeader.ts @@ -10,7 +10,8 @@ import terminalLink = require('terminal-link'); import type {TestResult} from '@jest/test-result'; import type {Config} from '@jest/types'; import {formatTime} from 'jest-util'; -import {formatTestPath, printDisplayName} from './utils'; +import formatTestPath from './formatTestPath'; +import printDisplayName from './printDisplayName'; const LONG_TEST_COLOR = chalk.reset.bold.bgRed; // Explicitly reset for these messages since they can get written out in the diff --git a/packages/jest-reporters/src/getSnapshotSummary.ts b/packages/jest-reporters/src/getSnapshotSummary.ts index e4bac9395b40..cf3e12a0e08b 100644 --- a/packages/jest-reporters/src/getSnapshotSummary.ts +++ b/packages/jest-reporters/src/getSnapshotSummary.ts @@ -9,7 +9,7 @@ import chalk = require('chalk'); import type {SnapshotSummary} from '@jest/test-result'; import type {Config} from '@jest/types'; import {pluralize} from 'jest-util'; -import {formatTestPath} from './utils'; +import formatTestPath from './formatTestPath'; const ARROW = ' \u203A '; const DOWN_ARROW = ' \u21B3 '; diff --git a/packages/jest-reporters/src/utils.ts b/packages/jest-reporters/src/getSummary.ts similarity index 57% rename from packages/jest-reporters/src/utils.ts rename to packages/jest-reporters/src/getSummary.ts index 158baaf9e6cd..dbb42c8d0411 100644 --- a/packages/jest-reporters/src/utils.ts +++ b/packages/jest-reporters/src/getSummary.ts @@ -5,96 +5,16 @@ * LICENSE file in the root directory of this source tree. */ -import * as path from 'path'; import chalk = require('chalk'); -import slash = require('slash'); import type {AggregatedResult, Test, TestCaseResult} from '@jest/test-result'; -import type {Config} from '@jest/types'; import {formatTime, pluralize} from 'jest-util'; import type {SummaryOptions} from './types'; -const PROGRESS_BAR_WIDTH = 40; +export const PROGRESS_BAR_WIDTH = 40; -export const printDisplayName = (config: Config.ProjectConfig): string => { - const {displayName} = config; - const white = chalk.reset.inverse.white; - if (!displayName) { - return ''; - } - - const {name, color} = displayName; - const chosenColor = chalk.reset.inverse[color] - ? chalk.reset.inverse[color] - : white; - return chalk.supportsColor ? chosenColor(` ${name} `) : name; -}; - -export const trimAndFormatPath = ( - pad: number, - config: Config.ProjectConfig | Config.GlobalConfig, - testPath: string, - columns: number, -): string => { - const maxLength = columns - pad; - const relative = relativePath(config, testPath); - const {basename} = relative; - let {dirname} = relative; - - // length is ok - if ((dirname + path.sep + basename).length <= maxLength) { - return slash(chalk.dim(dirname + path.sep) + chalk.bold(basename)); - } - - // we can fit trimmed dirname and full basename - const basenameLength = basename.length; - if (basenameLength + 4 < maxLength) { - const dirnameLength = maxLength - 4 - basenameLength; - dirname = `...${dirname.slice( - dirname.length - dirnameLength, - dirname.length, - )}`; - return slash(chalk.dim(dirname + path.sep) + chalk.bold(basename)); - } - - if (basenameLength + 4 === maxLength) { - return slash(chalk.dim(`...${path.sep}`) + chalk.bold(basename)); - } - - // can't fit dirname, but can fit trimmed basename - return slash( - chalk.bold( - `...${basename.slice(basename.length - maxLength - 4, basename.length)}`, - ), - ); -}; - -export const formatTestPath = ( - config: Config.GlobalConfig | Config.ProjectConfig, - testPath: string, -): string => { - const {dirname, basename} = relativePath(config, testPath); - return slash(chalk.dim(dirname + path.sep) + chalk.bold(basename)); -}; - -export const relativePath = ( - config: Config.GlobalConfig | Config.ProjectConfig, - testPath: string, -): {basename: string; dirname: string} => { - // this function can be called with ProjectConfigs or GlobalConfigs. GlobalConfigs - // do not have config.cwd, only config.rootDir. Try using config.cwd, fallback - // to config.rootDir. (Also, some unit just use config.rootDir, which is ok) - testPath = path.relative( - (config as Config.ProjectConfig).cwd || config.rootDir, - testPath, - ); - const dirname = path.dirname(testPath); - const basename = path.basename(testPath); - return {basename, dirname}; -}; - -const getValuesCurrentTestCases = ( +function getValuesCurrentTestCases( currentTestCases: Array<{test: Test; testCaseResult: TestCaseResult}> = [], -) => { +) { let numFailingTests = 0; let numPassingTests = 0; let numPendingTests = 0; @@ -129,12 +49,40 @@ const getValuesCurrentTestCases = ( numTodoTests, numTotalTests, }; -}; +} + +function renderTime(runTime: number, estimatedTime: number, width: number) { + // If we are more than one second over the estimated time, highlight it. + const renderedTime = + estimatedTime && runTime >= estimatedTime + 1 + ? chalk.bold.yellow(formatTime(runTime, 0)) + : formatTime(runTime, 0); + let time = `${chalk.bold('Time:')} ${renderedTime}`; + if (runTime < estimatedTime) { + time += `, estimated ${formatTime(estimatedTime, 0)}`; + } + + // Only show a progress bar if the test run is actually going to take + // some time. + if (estimatedTime > 2 && runTime < estimatedTime && width) { + const availableWidth = Math.min(PROGRESS_BAR_WIDTH, width); + const length = Math.min( + Math.floor((runTime / estimatedTime) * availableWidth), + availableWidth, + ); + if (availableWidth >= 2) { + time += `\n${chalk.green('█').repeat(length)}${chalk + .white('█') + .repeat(availableWidth - length)}`; + } + } + return time; +} -export const getSummary = ( +export default function getSummary( aggregatedResults: AggregatedResult, options?: SummaryOptions, -): string => { +): string { let runTime = (Date.now() - aggregatedResults.startTime) / 1000; if (options && options.roundTime) { runTime = Math.floor(runTime); @@ -236,98 +184,4 @@ export const getSummary = ( const time = renderTime(runTime, estimatedTime, width); return [suites, tests, snapshots, time].join('\n'); -}; - -const renderTime = (runTime: number, estimatedTime: number, width: number) => { - // If we are more than one second over the estimated time, highlight it. - const renderedTime = - estimatedTime && runTime >= estimatedTime + 1 - ? chalk.bold.yellow(formatTime(runTime, 0)) - : formatTime(runTime, 0); - let time = `${chalk.bold('Time:')} ${renderedTime}`; - if (runTime < estimatedTime) { - time += `, estimated ${formatTime(estimatedTime, 0)}`; - } - - // Only show a progress bar if the test run is actually going to take - // some time. - if (estimatedTime > 2 && runTime < estimatedTime && width) { - const availableWidth = Math.min(PROGRESS_BAR_WIDTH, width); - const length = Math.min( - Math.floor((runTime / estimatedTime) * availableWidth), - availableWidth, - ); - if (availableWidth >= 2) { - time += `\n${chalk.green('█').repeat(length)}${chalk - .white('█') - .repeat(availableWidth - length)}`; - } - } - return time; -}; - -// word-wrap a string that contains ANSI escape sequences. -// ANSI escape sequences do not add to the string length. -export const wrapAnsiString = ( - string: string, - terminalWidth: number, -): string => { - if (terminalWidth === 0) { - // if the terminal width is zero, don't bother word-wrapping - return string; - } - - const ANSI_REGEXP = /[\u001b\u009b]\[\d{1,2}m/gu; - const tokens = []; - let lastIndex = 0; - let match; - - while ((match = ANSI_REGEXP.exec(string))) { - const ansi = match[0]; - const index = match['index']; - if (index != lastIndex) { - tokens.push(['string', string.slice(lastIndex, index)]); - } - tokens.push(['ansi', ansi]); - lastIndex = index + ansi.length; - } - - if (lastIndex != string.length - 1) { - tokens.push(['string', string.slice(lastIndex, string.length)]); - } - - let lastLineLength = 0; - - return tokens - .reduce( - (lines, [kind, token]) => { - if (kind === 'string') { - if (lastLineLength + token.length > terminalWidth) { - while (token.length) { - const chunk = token.slice(0, terminalWidth - lastLineLength); - const remaining = token.slice( - terminalWidth - lastLineLength, - token.length, - ); - lines[lines.length - 1] += chunk; - lastLineLength += chunk.length; - token = remaining; - if (token.length) { - lines.push(''); - lastLineLength = 0; - } - } - } else { - lines[lines.length - 1] += token; - lastLineLength += token.length; - } - } else { - lines[lines.length - 1] += token; - } - - return lines; - }, - [''], - ) - .join('\n'); -}; +} diff --git a/packages/jest-reporters/src/index.ts b/packages/jest-reporters/src/index.ts index f536c362a7f8..61f3cde17739 100644 --- a/packages/jest-reporters/src/index.ts +++ b/packages/jest-reporters/src/index.ts @@ -5,16 +5,14 @@ * LICENSE file in the root directory of this source tree. */ +import formatTestPath from './formatTestPath'; import getResultHeader from './getResultHeader'; import getSnapshotStatus from './getSnapshotStatus'; import getSnapshotSummary from './getSnapshotSummary'; -import { - formatTestPath, - getSummary, - printDisplayName, - relativePath, - trimAndFormatPath, -} from './utils'; +import getSummary from './getSummary'; +import printDisplayName from './printDisplayName'; +import relativePath from './relativePath'; +import trimAndFormatPath from './trimAndFormatPath'; export type { AggregatedResult, @@ -28,10 +26,10 @@ export type {Config} from '@jest/types'; export {default as BaseReporter} from './BaseReporter'; export {default as CoverageReporter} from './CoverageReporter'; export {default as DefaultReporter} from './DefaultReporter'; +export {default as GitHubActionsReporter} from './GitHubActionsReporter'; export {default as NotifyReporter} from './NotifyReporter'; export {default as SummaryReporter} from './SummaryReporter'; export {default as VerboseReporter} from './VerboseReporter'; -export {default as GitHubActionsReporter} from './GitHubActionsReporter'; export type { Reporter, ReporterOnStartOptions, diff --git a/packages/jest-reporters/src/printDisplayName.ts b/packages/jest-reporters/src/printDisplayName.ts new file mode 100644 index 000000000000..f03be5ddd094 --- /dev/null +++ b/packages/jest-reporters/src/printDisplayName.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import chalk = require('chalk'); +import type {Config} from '@jest/types'; + +export default function printDisplayName(config: Config.ProjectConfig): string { + const {displayName} = config; + const white = chalk.reset.inverse.white; + if (!displayName) { + return ''; + } + + const {name, color} = displayName; + const chosenColor = chalk.reset.inverse[color] + ? chalk.reset.inverse[color] + : white; + return chalk.supportsColor ? chosenColor(` ${name} `) : name; +} diff --git a/packages/jest-reporters/src/relativePath.ts b/packages/jest-reporters/src/relativePath.ts new file mode 100644 index 000000000000..38ec11f2356f --- /dev/null +++ b/packages/jest-reporters/src/relativePath.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as path from 'path'; +import type {Config} from '@jest/types'; + +export default function relativePath( + config: Config.GlobalConfig | Config.ProjectConfig, + testPath: string, +): {basename: string; dirname: string} { + // this function can be called with ProjectConfigs or GlobalConfigs. GlobalConfigs + // do not have config.cwd, only config.rootDir. Try using config.cwd, fallback + // to config.rootDir. (Also, some unit just use config.rootDir, which is ok) + testPath = path.relative( + (config as Config.ProjectConfig).cwd || config.rootDir, + testPath, + ); + const dirname = path.dirname(testPath); + const basename = path.basename(testPath); + return {basename, dirname}; +} diff --git a/packages/jest-reporters/src/trimAndFormatPath.ts b/packages/jest-reporters/src/trimAndFormatPath.ts new file mode 100644 index 000000000000..c8a4198abd94 --- /dev/null +++ b/packages/jest-reporters/src/trimAndFormatPath.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as path from 'path'; +import chalk = require('chalk'); +import slash = require('slash'); +import type {Config} from '@jest/types'; +import relativePath from './relativePath'; + +export default function trimAndFormatPath( + pad: number, + config: Config.ProjectConfig | Config.GlobalConfig, + testPath: string, + columns: number, +): string { + const maxLength = columns - pad; + const relative = relativePath(config, testPath); + const {basename} = relative; + let {dirname} = relative; + + // length is ok + if ((dirname + path.sep + basename).length <= maxLength) { + return slash(chalk.dim(dirname + path.sep) + chalk.bold(basename)); + } + + // we can fit trimmed dirname and full basename + const basenameLength = basename.length; + if (basenameLength + 4 < maxLength) { + const dirnameLength = maxLength - 4 - basenameLength; + dirname = `...${dirname.slice( + dirname.length - dirnameLength, + dirname.length, + )}`; + return slash(chalk.dim(dirname + path.sep) + chalk.bold(basename)); + } + + if (basenameLength + 4 === maxLength) { + return slash(chalk.dim(`...${path.sep}`) + chalk.bold(basename)); + } + + // can't fit dirname, but can fit trimmed basename + return slash( + chalk.bold( + `...${basename.slice(basename.length - maxLength - 4, basename.length)}`, + ), + ); +} diff --git a/packages/jest-reporters/src/wrapAnsiString.ts b/packages/jest-reporters/src/wrapAnsiString.ts new file mode 100644 index 000000000000..6a6db4d3da55 --- /dev/null +++ b/packages/jest-reporters/src/wrapAnsiString.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// word-wrap a string that contains ANSI escape sequences. +// ANSI escape sequences do not add to the string length. +export default function wrapAnsiString( + string: string, + terminalWidth: number, +): string { + if (terminalWidth === 0) { + // if the terminal width is zero, don't bother word-wrapping + return string; + } + + const ANSI_REGEXP = /[\u001b\u009b]\[\d{1,2}m/gu; + const tokens = []; + let lastIndex = 0; + let match; + + while ((match = ANSI_REGEXP.exec(string))) { + const ansi = match[0]; + const index = match['index']; + if (index != lastIndex) { + tokens.push(['string', string.slice(lastIndex, index)]); + } + tokens.push(['ansi', ansi]); + lastIndex = index + ansi.length; + } + + if (lastIndex != string.length - 1) { + tokens.push(['string', string.slice(lastIndex, string.length)]); + } + + let lastLineLength = 0; + + return tokens + .reduce( + (lines, [kind, token]) => { + if (kind === 'string') { + if (lastLineLength + token.length > terminalWidth) { + while (token.length) { + const chunk = token.slice(0, terminalWidth - lastLineLength); + const remaining = token.slice( + terminalWidth - lastLineLength, + token.length, + ); + lines[lines.length - 1] += chunk; + lastLineLength += chunk.length; + token = remaining; + if (token.length) { + lines.push(''); + lastLineLength = 0; + } + } + } else { + lines[lines.length - 1] += token; + lastLineLength += token.length; + } + } else { + lines[lines.length - 1] += token; + } + + return lines; + }, + [''], + ) + .join('\n'); +} diff --git a/yarn.lock b/yarn.lock index 001344e5fbce..5b8d2d5bfabf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2745,6 +2745,7 @@ __metadata: "@jest/transform": ^28.0.3 "@jest/types": ^28.0.2 "@jridgewell/trace-mapping": ^0.3.7 + "@tsd/typescript": ~4.6.2 "@types/exit": ^0.1.30 "@types/glob": ^7.1.1 "@types/graceful-fs": ^4.1.3 @@ -2773,6 +2774,7 @@ __metadata: string-length: ^4.0.1 strip-ansi: ^6.0.0 terminal-link: ^2.0.0 + tsd-lite: ^0.5.1 v8-to-istanbul: ^9.0.0 peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0