diff --git a/e2e/commands/reset.e2e.ts b/e2e/commands/reset.e2e.ts index 44c589f8f6aa..e3a08b15bc54 100644 --- a/e2e/commands/reset.e2e.ts +++ b/e2e/commands/reset.e2e.ts @@ -135,7 +135,7 @@ describe('bit reset command', function () { untagOutput = helper.command.resetAll(); }); it('should display a descriptive successful message', () => { - expect(untagOutput).to.have.string('2 component(s) were reset'); + expect(untagOutput).to.have.string('2 component(s) reset successfully'); }); it('should remove only local components from the model', () => { const output = helper.command.listLocalScope(); @@ -151,7 +151,7 @@ describe('bit reset command', function () { untagOutput = helper.command.resetAll('--head'); }); it('should display a descriptive successful message', () => { - expect(untagOutput).to.have.string('3 component(s) were reset'); + expect(untagOutput).to.have.string('3 component(s) reset successfully'); }); it('should remove only the specified version from the model', () => { const output = helper.command.listLocalScope(); @@ -195,7 +195,7 @@ describe('bit reset command', function () { untagOutput = helper.command.reset('utils/is-type', undefined, '--force'); }); it('should untag successfully', () => { - expect(untagOutput).to.have.string('1 component(s) were reset'); + expect(untagOutput).to.have.string('1 component(s) reset successfully'); }); }); describe('after exporting the component and tagging the scope', () => { @@ -236,7 +236,7 @@ describe('bit reset command', function () { untagOutput = helper.command.reset('utils/is-string'); }); it('should untag successfully the dependent', () => { - expect(untagOutput).to.have.string('1 component(s) were reset'); + expect(untagOutput).to.have.string('1 component(s) reset successfully'); expect(untagOutput).to.have.string('utils/is-string'); }); it('should leave the dependency intact', () => { @@ -263,7 +263,7 @@ describe('bit reset command', function () { output = helper.command.reset('utils/is-string'); }); it('should untag successfully', () => { - expect(output).to.have.string('1 component(s) were reset'); + expect(output).to.have.string('1 component(s) reset successfully'); expect(output).to.have.string('utils/is-string'); }); }); diff --git a/e2e/flows/id-with-wildcard.e2e.2.ts b/e2e/flows/id-with-wildcard.e2e.2.ts index 169989db9d17..15465a82466f 100644 --- a/e2e/flows/id-with-wildcard.e2e.2.ts +++ b/e2e/flows/id-with-wildcard.e2e.2.ts @@ -203,7 +203,7 @@ describe('component id with wildcard', function () { output = helper.command.reset('"**/*/is/*"'); }); it('should indicate the untagged components', () => { - expect(output).to.have.string('2 component(s) were reset'); + expect(output).to.have.string('2 component(s) reset successfully'); expect(output).to.have.string('utils/is/string'); expect(output).to.have.string('utils/is/type'); }); diff --git a/e2e/harmony/checkout-harmony.e2e.ts b/e2e/harmony/checkout-harmony.e2e.ts index 89afa46c7d67..dd5c2c4ca330 100644 --- a/e2e/harmony/checkout-harmony.e2e.ts +++ b/e2e/harmony/checkout-harmony.e2e.ts @@ -750,7 +750,7 @@ describe('bit checkout command', function () { expect(list).to.have.lengthOf(3); }); it('should show the new components in the output', () => { - expect(output).to.have.string('successfully imported the following new components from the defaultScope'); + expect(output).to.have.string('new components from scope'); expect(output).to.have.string('comp2'); expect(output).to.have.string('comp3'); }); diff --git a/e2e/harmony/delete.e2e.ts b/e2e/harmony/delete.e2e.ts index 13f00b4c36e7..2d6633d671e3 100644 --- a/e2e/harmony/delete.e2e.ts +++ b/e2e/harmony/delete.e2e.ts @@ -359,7 +359,7 @@ describe('bit delete command', function () { output = helper.command.resetAll(); }); it('should reset the deleted component', () => { - expect(output).to.have.string('2 component(s) were reset'); + expect(output).to.have.string('2 component(s) reset successfully'); }); it('should revert the .bitmap entry of the deleted component as it was before', () => { const bitmap = helper.bitMap.read(); diff --git a/e2e/harmony/lanes/lane-snapping.e2e.ts b/e2e/harmony/lanes/lane-snapping.e2e.ts index e5857ce6bdc5..21d4145f193a 100644 --- a/e2e/harmony/lanes/lane-snapping.e2e.ts +++ b/e2e/harmony/lanes/lane-snapping.e2e.ts @@ -188,7 +188,7 @@ describe('bit lane snapping and tagging', function () { output = helper.command.resetAll(); }); it('should untag successfully', () => { - expect(output).to.have.string('1 component(s) were reset'); + expect(output).to.have.string('1 component(s) reset successfully'); }); it('should change the component to be new', () => { const status = helper.command.statusJson(); diff --git a/e2e/harmony/lanes/merge-lanes-remote.e2e.ts b/e2e/harmony/lanes/merge-lanes-remote.e2e.ts index a22d8b2f5705..3c75f65164a1 100644 --- a/e2e/harmony/lanes/merge-lanes-remote.e2e.ts +++ b/e2e/harmony/lanes/merge-lanes-remote.e2e.ts @@ -75,7 +75,7 @@ describe('merge lanes - remote lane operations', function () { mergeOutput = helper.command.mergeLane(`${helper.scopes.remote}/dev`, `--workspace --verbose`); }); it('should indicate that the components were not merge because they are not in the workspace', () => { - expect(mergeOutput).to.have.string('merge skipped for the following component(s)'); + expect(mergeOutput).to.have.string('merge skipped'); expect(mergeOutput).to.have.string('not in the workspace'); }); it('bitmap should not save any component', () => { @@ -137,12 +137,12 @@ describe('merge lanes - remote lane operations', function () { it('bit lane --merged should not show the lane as it was not merged into main yet', () => { const merged = helper.command.listLanes('--merged'); expect(merged).to.not.have.string('dev'); - expect(merged).to.have.string('None of the lanes is merged'); + expect(merged).to.have.string('none of the lanes is merged'); }); it('bit lane --unmerged should show the lane', () => { const merged = helper.command.listLanes('--not-merged'); expect(merged).to.have.string('dev'); - expect(merged).to.not.have.string('All lanes are merged'); + expect(merged).to.not.have.string('all lanes are merged'); }); }); }); diff --git a/e2e/harmony/lanes/switch-lanes.e2e.ts b/e2e/harmony/lanes/switch-lanes.e2e.ts index b5ed667fcd5c..06124a15f8ce 100644 --- a/e2e/harmony/lanes/switch-lanes.e2e.ts +++ b/e2e/harmony/lanes/switch-lanes.e2e.ts @@ -418,7 +418,7 @@ describe('bit lane command', function () { }); it('should switch the component on the lane only', () => { expect(switchOutput).to.have.string('switched 1 components'); - expect(switchOutput).to.have.string('skipped legitimately for 1 component(s)'); + expect(switchOutput).to.have.string('switch skipped for 1 component(s)'); }); }); }); diff --git a/e2e/harmony/snap.e2e.2.ts b/e2e/harmony/snap.e2e.2.ts index 508bf123a798..8206553abc38 100644 --- a/e2e/harmony/snap.e2e.2.ts +++ b/e2e/harmony/snap.e2e.2.ts @@ -324,7 +324,7 @@ describe('bit snap command', function () { resolveOutput = helper.command.merge('bar/foo --resolve'); }); it('should resolve successfully', () => { - expect(resolveOutput).to.have.string('successfully resolved component'); + expect(resolveOutput).to.have.string('resolved components'); }); it('bit status should not show the component as during merge state', () => { const status = helper.command.statusJson(); @@ -481,7 +481,7 @@ describe('bit snap command', function () { resolveOutput = helper.command.merge('bar/foo --resolve'); }); it('should resolve the conflicts successfully', () => { - expect(resolveOutput).to.have.string('successfully resolved component'); + expect(resolveOutput).to.have.string('resolved components'); }); it('bit status should not show the component as if it has conflicts', () => { const status = helper.command.statusJson(); @@ -509,7 +509,7 @@ describe('bit snap command', function () { abortOutput = helper.command.merge('bar/foo --abort'); }); it('should abort the merge successfully', () => { - expect(abortOutput).to.have.string('successfully aborted the merge'); + expect(abortOutput).to.have.string('merge aborted'); }); it('bit status should show the same state as before the merge', () => { const status = helper.command.statusJson(); diff --git a/scopes/component/checkout/checkout-cmd.ts b/scopes/component/checkout/checkout-cmd.ts index 21847ab8bff1..add657a932ff 100644 --- a/scopes/component/checkout/checkout-cmd.ts +++ b/scopes/component/checkout/checkout-cmd.ts @@ -1,6 +1,14 @@ import chalk from 'chalk'; import type { Command, CommandOptions } from '@teambit/cli'; -import { compact } from 'lodash'; +import { + formatTitle, + formatSection, + formatItem, + formatSuccessSummary, + formatHint, + warnSymbol, + joinSections, +} from '@teambit/cli'; import type { ApplyVersionResults, MergeStrategy } from '@teambit/component.modules.merge-helper'; import { applyVersionReport, @@ -189,51 +197,47 @@ export function checkoutOutput( const getNotCheckedOutOutput = () => { if (!notCheckedOutComponents.length) return ''; if (!verbose && all) { - return chalk.green( - `checkout was not needed for ${chalk.bold( - notCheckedOutComponents.length.toString() - )} components (use --verbose to get more details)\n` + return formatHint( + `checkout was not needed for ${notCheckedOutComponents.length} components (use --verbose to get more details)` ); } - const title = 'checkout was not required for the following component(s)'; - const body = notCheckedOutComponents - .map((failedComponent) => `${failedComponent.id.toString()} - ${failedComponent.unchangedMessage}`) - .join('\n'); - return `${chalk.underline(title)}\n${body}`; + const items = notCheckedOutComponents.map((failedComponent) => + formatItem(`${failedComponent.id.toString()} - ${failedComponent.unchangedMessage}`) + ); + return formatSection('checkout skipped', '', items); }; const getWsConfigUpdateLogs = () => { const logs = workspaceConfigUpdateResult?.logs; if (!logs || !logs.length) return ''; const logsStr = logs.join('\n'); - return `${chalk.underline('verbose logs of workspace config update')}\n${logsStr}`; + return `${formatTitle('verbose logs of workspace config update')}\n${logsStr}`; }; const getConflictSummary = () => { if (!components || !components.length || !leftUnresolvedConflicts) return ''; - const title = `files with conflicts summary\n`; - const suggestion = `\n\nfix the conflicts above manually and then run "bit install". -once ready, snap/tag the components to persist the changes`; + const title = formatTitle(`${warnSymbol} files with conflicts summary`); const conflictSummary = conflictSummaryReport(components); - return chalk.underline(title) + conflictSummary.conflictStr + chalk.yellow(suggestion); + const suggestion = formatHint( + `fix the conflicts above manually and then run "bit install".\nonce ready, snap/tag the components to persist the changes` + ); + return `${title}\n${conflictSummary.conflictStr}\n\n${suggestion}`; }; const getSuccessfulOutput = () => { if (!components || !components.length) return ''; - const newLine = '\n'; const switchedOrReverted = revert ? 'reverted' : 'switched'; if (components.length === 1) { const component = components[0]; const componentName = reset ? component.id.toString() : component.id.toStringWithoutVersion(); - if (reset) return `successfully reset ${chalk.bold(componentName)}\n`; + if (reset) return formatSuccessSummary(`successfully reset ${chalk.bold(componentName)}`); const title = alternativeTitle || `successfully ${switchedOrReverted} ${chalk.bold(componentName)} to version ${chalk.bold( head || latest ? component.id.version : version )}`; - return chalk.bold(title) + newLine + applyVersionReport(components, false); + return [formatSuccessSummary(title), applyVersionReport(components, false)].filter(Boolean).join('\n'); } if (reset) { - const title = 'successfully reset the following components\n\n'; - const body = components.map((component) => chalk.bold(component.id.toString())).join('\n'); - return chalk.underline(title) + body; + const items = components.map((component) => formatItem(component.id.toString())); + return formatSection('reset components', '', items); } const getVerOutput = () => { if (head) return 'their head version'; @@ -245,26 +249,24 @@ once ready, snap/tag the components to persist the changes`; const title = alternativeTitle || `successfully ${switchedOrReverted} ${components.length} components to ${versionOutput}`; const showVersion = head || reset; - return chalk.bold(title) + newLine + applyVersionReport(components, true, showVersion); + return [formatSuccessSummary(title), applyVersionReport(components, true, showVersion)].filter(Boolean).join('\n'); }; const getNewOnLaneOutput = () => { if (!newFromLane?.length) return ''; - const title = newFromLaneAdded - ? `successfully added the following components from the lane` - : `the following components exist on the lane but were not added to the workspace. omit --workspace-only flag to add them`; - const body = newFromLane.join('\n'); - return `${chalk.underline(title)}\n${body}`; + const title = newFromLaneAdded ? 'new components from lane' : 'new components on lane (not added)'; + const desc = newFromLaneAdded ? '' : 'omit --workspace-only flag to add them'; + const items = newFromLane.map((c) => formatItem(c.toString())); + return formatSection(title, desc, items); }; const getNewFromScopeOutput = () => { if (!newFromScope?.length) return ''; - const title = `successfully imported the following new components from the defaultScope`; - const body = newFromScope.join('\n'); - return `${chalk.underline(title)}\n${body}`; + const items = newFromScope.map((c) => formatItem(c.toString())); + return formatSection('new components from scope', '', items); }; const getSummary = () => { const checkedOut = components?.length || 0; const notCheckedOutLegitimately = notCheckedOutComponents.length; - const title = chalk.bold.underline('Summary'); + const title = formatTitle('Checkout Summary'); const checkedOutStr = `\nTotal Changed: ${chalk.bold(checkedOut.toString())}`; const unchangedLegitimatelyStr = `\nTotal Unchanged: ${chalk.bold(notCheckedOutLegitimately.toString())}`; const newOnLaneNum = newFromLane?.length || 0; @@ -280,7 +282,7 @@ once ready, snap/tag the components to persist the changes`; return title + checkedOutStr + unchangedLegitimatelyStr + newOnLaneStr + newFromScopeStr; }; - return compact([ + return joinSections([ getWsConfigUpdateLogs(), getNotCheckedOutOutput(), getSuccessfulOutput(), @@ -293,5 +295,5 @@ once ready, snap/tag the components to persist the changes`; getSummary(), installationErrorOutput(installationError), compilationErrorOutput(compilationError), - ]).join('\n\n'); + ]); } diff --git a/scopes/component/component-log/log-cmd.ts b/scopes/component/component-log/log-cmd.ts index bb9b6a15d528..4fb04d8e7a47 100644 --- a/scopes/component/component-log/log-cmd.ts +++ b/scopes/component/component-log/log-cmd.ts @@ -1,6 +1,7 @@ import c from 'chalk'; import Table from 'cli-table'; import type { Command, CommandOptions } from '@teambit/cli'; +import { warnSymbol, errorSymbol } from '@teambit/cli'; import type { LegacyComponentLog } from '@teambit/legacy-component-log'; import type { ComponentLogMain, LogOpts } from './component-log.main.runtime'; @@ -93,11 +94,14 @@ export function paintAuthor(email: string | null | undefined, username: string | function paintLog(log: LegacyComponentLog): string { const { message, date, tag, hash, username, email, deleted, deprecated } = log; - const deletedStr = deleted ? c.red(' [deleted]') : ''; - const deprecatedStr = !deleted && deprecated ? c.yellow(' [deprecated]') : ''; - const title = tag ? `tag ${tag} (${hash})${deletedStr}${deprecatedStr}\n` : `snap ${hash}\n`; + const deletedStr = deleted ? ` ${c.red(`${errorSymbol} deleted`)}` : ''; + const deprecatedStr = !deleted && deprecated ? ` ${c.yellow(`${warnSymbol} deprecated`)}` : ''; + const title = tag ? `tag ${tag} (${hash})` : `snap ${hash}`; return ( c.yellow(title) + + deletedStr + + deprecatedStr + + '\n' + paintAuthor(email, username) + (date ? c.white(`date: ${date}\n`) : '') + (message ? c.white(`\n ${message}\n`) : '') diff --git a/scopes/component/merging/merge-cmd.ts b/scopes/component/merging/merge-cmd.ts index c9c56b04db98..ecf0b95a924c 100644 --- a/scopes/component/merging/merge-cmd.ts +++ b/scopes/component/merging/merge-cmd.ts @@ -1,7 +1,14 @@ import chalk from 'chalk'; import type { Command, CommandOptions } from '@teambit/cli'; -import { warnSymbol, formatTitle, formatHint, joinSections } from '@teambit/cli'; -import { compact } from 'lodash'; +import { + warnSymbol, + errorSymbol, + formatTitle, + formatSection, + formatItem, + formatHint, + joinSections, +} from '@teambit/cli'; import { COMPONENT_PATTERN_HELP, AUTO_SNAPPED_MSG, @@ -128,14 +135,12 @@ for lane-to-lane merging, use 'bit lane merge' instead.`; skipDependencyInstallation ); if (resolvedComponents) { - const title = formatTitle('successfully resolved component(s)'); - const componentsStr = resolvedComponents.map((c) => c.id.toStringWithoutVersion()).join('\n'); - return `${title}\n${chalk.green(componentsStr)}`; + const items = resolvedComponents.map((c) => formatItem(c.id.toStringWithoutVersion())); + return formatSection('resolved components', '', items); } if (abortedComponents) { - const title = formatTitle('successfully aborted the merge of the following component(s)'); - const componentsStr = abortedComponents.map((c) => c.id.toStringWithoutVersion()).join('\n'); - return `${title}\n${chalk.green(componentsStr)}`; + const items = abortedComponents.map((c) => formatItem(c.id.toStringWithoutVersion())); + return formatSection('merge aborted', '', items); } return mergeReport({ @@ -196,31 +201,22 @@ export function mergeReport({ const getSnapsOutput = () => { if (mergeSnapError) { - return `${chalk.bold( + return `${formatTitle(`${errorSymbol} snap error`)}\n${chalk.red( 'snapping merged components failed with the following error, please fix the issues and snap manually' - )} -${mergeSnapError.message} -`; + )}\n${mergeSnapError.message}`; } if (!mergeSnapResults || !mergeSnapResults.snappedComponents) return ''; const { snappedComponents, autoSnappedResults } = mergeSnapResults; - const outputComponents = (comps) => { - return comps - .map((component) => { - let componentOutput = ` > ${component.id.toString()}`; - const autoTag = autoSnappedResults.filter((result) => result.triggeredBy.searchWithoutVersion(component.id)); - if (autoTag.length) { - const autoTagComp = autoTag.map((a) => a.component.id.toString()); - componentOutput += `\n ${AUTO_SNAPPED_MSG}: ${autoTagComp.join(', ')}`; - } - return componentOutput; - }) - .join('\n'); - }; - - return `${formatTitle( - `merge-snapped components (${snappedComponents.length})` - )}\n${chalk.dim('components snapped as a result of the merge')}\n${outputComponents(snappedComponents)}`; + const items = snappedComponents.map((component) => { + let line = formatItem(component.id.toString()); + const autoTag = autoSnappedResults.filter((result) => result.triggeredBy.searchWithoutVersion(component.id)); + if (autoTag.length) { + const autoTagComp = autoTag.map((a) => a.component.id.toString()); + line += `\n ${AUTO_SNAPPED_MSG}: ${autoTagComp.join(', ')}`; + } + return line; + }); + return formatSection('merge-snapped components', 'components snapped as a result of the merge', items); }; const getFailureOutput = () => { @@ -232,31 +228,24 @@ ${mergeSnapError.message} const parts: string[] = []; if (skippedRemoved.length) { - const removedTitle = chalk.yellow(`\nmerge skipped for ${skippedRemoved.length} soft-removed component(s):`); - const removedBody = skippedRemoved.map((fc) => `${chalk.bold(fc.id.toString())}`).join('\n'); - const removedHint = chalk.yellow(`(use "bit recover " to restore, then re-run the merge)`); - parts.push(`${removedTitle}\n${removedBody}\n${removedHint}`); + const items = skippedRemoved.map((fc) => formatItem(chalk.bold(fc.id.toString()), warnSymbol)); + const section = formatSection('merge skipped - soft-removed', '', items); + const hint = formatHint('use "bit recover " to restore, then re-run the merge'); + parts.push(`${section}\n${hint}`); } if (otherComponents.length) { - const title = '\nmerge skipped for the following component(s)'; - const body = compact( - otherComponents.map((failedComponent) => { - // all failures here are "unchangedLegitimately". otherwise, it would have been thrown as an error - if (!verbose) return null; - return `${chalk.bold(failedComponent.id.toString())} - ${chalk.white(failedComponent.unchangedMessage)}`; - }) - ).join('\n'); - if (!body) { - parts.push( - `${chalk.bold(`\nmerge skipped legitimately for ${otherComponents.length} component(s)`)}\n(use --verbose to list them next time)` - ); + if (!verbose) { + parts.push(formatHint(`merge skipped for ${otherComponents.length} component(s) (use --verbose to list them)`)); } else { - parts.push(`${formatTitle(title)}\n${body}`); + const items = otherComponents.map((failedComponent) => + formatItem(`${chalk.bold(failedComponent.id.toString())} - ${failedComponent.unchangedMessage}`) + ); + parts.push(formatSection('merge skipped', '', items)); } } - return parts.join('\n'); + return parts.join('\n\n'); }; const getSummary = () => { @@ -268,7 +257,7 @@ ${mergeSnapError.message} const comps = componentsWithConflicts ? `${componentsWithConflicts} components` : ''; const ws = workspaceConfigUpdateResult?.workspaceDepsConflicts ? 'workspace.jsonc file' : ''; const mergeConfig = configMergeWithConflicts.length ? `${MergeConfigFilename} file` : ''; - return compact([comps, ws, mergeConfig]).join(', '); + return [comps, ws, mergeConfig].filter(Boolean).join(', '); }; const title = formatTitle('Merge Summary'); diff --git a/scopes/component/snapping/reset-cmd.ts b/scopes/component/snapping/reset-cmd.ts index 15c6f1838402..16bc0fc329a8 100644 --- a/scopes/component/snapping/reset-cmd.ts +++ b/scopes/component/snapping/reset-cmd.ts @@ -2,6 +2,7 @@ import { BitError } from '@teambit/bit-error'; import chalk from 'chalk'; import yesno from 'yesno'; import type { Command, CommandOptions } from '@teambit/cli'; +import { formatSection, formatItem, formatSuccessSummary, joinSections } from '@teambit/cli'; import { COMPONENT_PATTERN_HELP } from '@teambit/legacy.constants'; import type { SnappingMain } from './snapping.main.runtime'; @@ -49,7 +50,11 @@ useful for undoing mistakes before exporting. exported versions cannot be reset. ) { if (neverExported) { const compIds = await this.snapping.resetNeverExported(); - return chalk.green(`successfully reset the following never-exported components:\n${compIds.join('\n')}`); + const items = compIds.map((id) => formatItem(id.toString())); + return joinSections([ + formatSection('reset never-exported components', '', items), + formatSuccessSummary(`successfully reset ${compIds.length} never-exported component(s)`), + ]); } if (soft && head) { throw new BitError('please specify either --soft or --head flag, not both'); @@ -59,12 +64,15 @@ useful for undoing mistakes before exporting. exported versions cannot be reset. await this.promptForResetAll(); } const { results, isSoftUntag } = await this.snapping.reset(pattern, head, force, soft); - const titleSuffix = isSoftUntag ? 'soft-untagged (are not candidates for tagging any more)' : 'reset'; - const title = chalk.green(`${results.length} component(s) were ${titleSuffix}:\n`); - const components = results.map((result) => { - return `${chalk.cyan(result.id.toStringWithoutVersion())}. version(s): ${result.versions.join(', ')}`; - }); - return title + components.join('\n'); + const titleSuffix = isSoftUntag ? 'soft-untagged' : 'reset'; + const description = isSoftUntag ? 'soft-untagged versions are no longer candidates for tagging' : ''; + const items = results.map((result) => + formatItem(`${chalk.cyan(result.id.toStringWithoutVersion())} - version(s): ${result.versions.join(', ')}`) + ); + return joinSections([ + formatSection(`${titleSuffix} components`, description, items), + formatSuccessSummary(`${results.length} component(s) ${titleSuffix} successfully`), + ]); } private async promptForResetAll() { diff --git a/scopes/defender/tester/test.cmd.ts b/scopes/defender/tester/test.cmd.ts index 6c3d7e3452e8..686f6b959a85 100644 --- a/scopes/defender/tester/test.cmd.ts +++ b/scopes/defender/tester/test.cmd.ts @@ -1,4 +1,5 @@ import type { Command, CommandOptions, GenericObject } from '@teambit/cli'; +import { formatHint, formatSuccessSummary } from '@teambit/cli'; import chalk from 'chalk'; import type { Logger } from '@teambit/logger'; import type { Workspace } from '@teambit/workspace'; @@ -99,9 +100,9 @@ supports watch mode, coverage reporting, and debug mode for development workflow true ); if (!components.length) { - const data = chalk.bold(`no components found to test. -use "--unmodified" flag to test all components or specify the ids to test. -otherwise, only new and modified components will be tested`); + const data = formatHint( + `no components found to test.\nuse "--unmodified" flag to test all components or specify the ids to test.\notherwise, only new and modified components will be tested` + ); return { code: 0, data, @@ -143,7 +144,10 @@ otherwise, only new and modified components will be tested`); const { seconds } = timer.stop(); if (watch) return ''; - const data = `tests has been completed in ${chalk.cyan(seconds.toString())} seconds.`; + const data = + code === 0 + ? formatSuccessSummary(`tests completed in ${seconds} seconds`) + : formatHint(`tests completed in ${seconds} seconds`); return { code, data, diff --git a/scopes/lanes/lanes/lane.cmd.ts b/scopes/lanes/lanes/lane.cmd.ts index ac1a3f598754..39992bbfcd68 100644 --- a/scopes/lanes/lanes/lane.cmd.ts +++ b/scopes/lanes/lanes/lane.cmd.ts @@ -7,6 +7,15 @@ import { checkoutOutput } from '@teambit/checkout'; import type { Workspace } from '@teambit/workspace'; import { OutsideWorkspaceError } from '@teambit/workspace'; import type { Command, CommandOptions } from '@teambit/cli'; +import { + formatTitle, + formatSection, + formatItem, + formatSuccessSummary, + formatHint, + warnSymbol, + joinSections, +} from '@teambit/cli'; import type { LaneData } from '@teambit/legacy.scope'; import { serializeLaneData } from '@teambit/legacy.scope'; import { BitError } from '@teambit/bit-error'; @@ -61,68 +70,65 @@ export class LaneListCmd implements Command { }); if (merged) { const mergedLanes = lanes.filter((l) => l.isMerged); - if (!mergedLanes.length) return chalk.green('None of the lanes is merged'); - return chalk.green(mergedLanes.map((m) => m.name).join('\n')); + if (!mergedLanes.length) return formatHint('none of the lanes is merged'); + const items = mergedLanes.map((m) => formatItem(m.name)); + return formatSection('merged lanes', '', items); } if (notMerged) { const unmergedLanes = lanes.filter((l) => !l.isMerged); - if (!unmergedLanes.length) return chalk.green('All lanes are merged'); - return chalk.green(unmergedLanes.map((m) => m.name).join('\n')); + if (!unmergedLanes.length) return formatHint('all lanes are merged'); + const items = unmergedLanes.map((m) => formatItem(m.name)); + return formatSection('unmerged lanes', '', items); } const currentLane = this.lanes.getCurrentLaneId() || this.lanes.getDefaultLaneId(); const laneDataOfCurrentLane = currentLane ? lanes.find((l) => currentLane.isEqual(l.id)) : undefined; const currentAlias = laneDataOfCurrentLane ? laneDataOfCurrentLane.alias : undefined; const currentLaneReadmeComponentStr = outputReadmeComponent(laneDataOfCurrentLane?.readmeComponent); - let currentLaneStr = remote - ? '' - : `current lane - ${chalk.green.green(laneIdStr(currentLane, currentAlias))}${currentLaneReadmeComponentStr}`; - if (details) { - const currentLaneComponents = laneDataOfCurrentLane ? outputComponents(laneDataOfCurrentLane.components) : ''; - if (currentLaneStr) { - currentLaneStr += `\n${currentLaneComponents}`; + const getCurrentLaneOutput = () => { + if (remote) return ''; + const currentLaneLabel = `current lane - ${chalk.green(laneIdStr(currentLane, currentAlias))}${currentLaneReadmeComponentStr}`; + if (details && laneDataOfCurrentLane) { + return `${currentLaneLabel}\n${outputComponents(laneDataOfCurrentLane.components)}`; } - } + return currentLaneLabel; + }; - const availableLanes = lanes - .filter((l) => !currentLane.isEqual(l.id)) - .map((laneData) => { - const readmeComponentStr = outputReadmeComponent(laneData.readmeComponent); - if (details) { - const laneTitle = `> ${chalk.bold(laneIdStr(laneData.id, laneData.alias))}\n`; + const getAvailableLanesOutput = () => { + const otherLanes = lanes.filter((l) => !currentLane.isEqual(l.id)); + if (!otherLanes.length) return ''; + if (details) { + const items = otherLanes.map((laneData) => { + const readmeComponentStr = outputReadmeComponent(laneData.readmeComponent); + const laneTitle = `> ${chalk.bold(laneIdStr(laneData.id, laneData.alias))}`; const components = outputComponents(laneData.components); - return laneTitle + readmeComponentStr.concat('\n') + components; - } - return ` > ${chalk.green(laneIdStr(laneData.id, laneData.alias))} (${ - laneData.components.length - } components)${readmeComponentStr}`; - }) - .join('\n'); - - const outputFooter = () => { - let footer = '\n'; + return `${laneTitle}${readmeComponentStr}\n${components}`; + }); + return items.join('\n\n'); + } + const items = otherLanes.map((laneData) => { + const readmeComponentStr = outputReadmeComponent(laneData.readmeComponent); + return formatItem( + `${chalk.green(laneIdStr(laneData.id, laneData.alias))} (${laneData.components.length} components)${readmeComponentStr}` + ); + }); + return formatSection('available lanes', '', items); + }; + + const getFooter = () => { + const hints: string[] = []; if (details) { - footer += 'You can use --merged and --not-merged to see which of the lanes is fully merged.'; + hints.push('use --merged and --not-merged to see which of the lanes is fully merged'); } else { - footer += - "to get more info on all lanes in local scope use 'bit lane list --details', or 'bit lane show ' for a specific lane."; + hints.push("use 'bit lane list --details' or 'bit lane show ' for more info"); } - if (!remote && this.workspace) - footer += `\nswitch lanes using 'bit switch '. create lanes using 'bit lane create '.`; - - return footer; + if (!remote && this.workspace) { + hints.push("switch lanes using 'bit switch '. create lanes using 'bit lane create '"); + } + return formatHint(hints.join('\n')); }; - return outputCurrentLane() + outputAvailableLanes() + outputFooter(); - - function outputCurrentLane() { - return currentLaneStr ? `${currentLaneStr}\n` : ''; - } - - function outputAvailableLanes() { - if (!availableLanes) return ''; - return remote ? `${availableLanes}\n` : `\nAvailable lanes:\n${availableLanes}\n`; - } + return joinSections([getCurrentLaneOutput(), getAvailableLanesOutput(), getFooter()]); } async json(args, laneOptions: LaneOptions) { const { remote, merged = false, notMerged = false } = laneOptions; @@ -200,13 +206,14 @@ export class LaneShowCmd implements Command { const onlyLane = lanes[0]; const laneId = onlyLane.id; const laneIdStr = laneId.isDefault() ? DEFAULT_LANE : laneId.toString(); - const title = `showing information for ${chalk.bold(laneIdStr)}\n`; - const author = onlyLane.log ? `author: ${onlyLane.log?.username || 'N/A'} <${onlyLane.log?.email || 'N/A'}>\n` : ''; - const date = onlyLane.log?.date ? `created: ${new Date(parseInt(onlyLane.log.date)).toLocaleString()}\n` : ''; + const title = formatTitle(laneIdStr); + const author = onlyLane.log ? `author: ${onlyLane.log?.username || 'N/A'} <${onlyLane.log?.email || 'N/A'}>` : ''; + const date = onlyLane.log?.date ? `created: ${new Date(parseInt(onlyLane.log.date)).toLocaleString()}` : ''; const link = laneId.isDefault() ? '' - : `link: https://${DEFAULT_CLOUD_DOMAIN}/${laneId.scope.replace('.', '/')}/~lane/${laneId.name}\n`; - return title + author + date + link + outputComponents(onlyLane.components); + : `link: https://${DEFAULT_CLOUD_DOMAIN}/${laneId.scope.replace('.', '/')}/~lane/${laneId.name}`; + const meta = [author, date, link].filter(Boolean).join('\n'); + return joinSections([title, meta, outputComponents(onlyLane.components)]); } private async geLanesData([name]: [string], laneOptions: LaneOptions) { @@ -279,13 +286,12 @@ a lane created from another lane contains all the components of the original lan : `the default-scope ${chalk.bold( result.laneId.scope )}. you can change the lane's scope, before it is exported, with the "bit lane change-scope" command`; - const title = chalk.green( - `successfully added and checked out to the new lane ${chalk.bold(result.alias || result.laneId.name)} - ${currentLane ? chalk.yellow(`\nnote - your new lane will be based on lane ${currentLane.name}`) : ''} - ` + const summary = formatSuccessSummary( + `successfully created and checked out to lane ${chalk.bold(result.alias || result.laneId.name)}` ); - const remoteScopeOutput = `this lane will be exported to ${remoteScopeOrDefaultScope}`; - return `${title}\n${remoteScopeOutput}`; + const note = currentLane ? formatHint(`note - your new lane will be based on lane ${currentLane.name}`) : ''; + const scopeInfo = formatHint(`this lane will be exported to ${remoteScopeOrDefaultScope}`); + return joinSections([summary, note, scopeInfo]); } } @@ -800,15 +806,14 @@ export class LaneRemoveReadmeCmd implements Command { } function outputComponents(components: LaneData['components']): string { - const componentsTitle = `\t${chalk.bold(`components (${components.length})`)}\n`; - const componentsStr = components.map((c) => `\t ${c.id.toString()} - ${c.head}`).join('\n'); - return componentsTitle + componentsStr; + const items = components.map((c) => formatItem(`${c.id.toString()} - ${c.head}`)); + return formatSection('components', '', items); } function outputReadmeComponent(component: LaneData['readmeComponent']): string { if (!component) return ''; - return `\n\t${`${chalk.yellow('readme component')}\n\t ${component.id} - ${ - component.head || - `(unsnapped)\n\t("use bit snap ${component.id.fullName}" to snap the readme component on the lane before exporting)` - }`}\n`; + const head = component.head || '(unsnapped)'; + const hint = component.head ? '' : ` - use "bit snap ${component.id.fullName}" to snap before exporting`; + const prefix = component.head ? ' ' : ` ${warnSymbol} `; + return `${prefix}readme: ${component.id} - ${head}${hint}`; } diff --git a/scopes/lanes/lanes/switch.cmd.ts b/scopes/lanes/lanes/switch.cmd.ts index cf37dc907bb4..7638379866fb 100644 --- a/scopes/lanes/lanes/switch.cmd.ts +++ b/scopes/lanes/lanes/switch.cmd.ts @@ -1,5 +1,4 @@ import chalk from 'chalk'; -import { compact } from 'lodash'; import type { MergeStrategy } from '@teambit/component.modules.merge-helper'; import { applyVersionReport, @@ -7,6 +6,7 @@ import { compilationErrorOutput, } from '@teambit/component.modules.merge-helper'; import type { Command, CommandOptions } from '@teambit/cli'; +import { formatItem, formatSection, formatSuccessSummary, formatHint, joinSections } from '@teambit/cli'; import { COMPONENT_PATTERN_HELP } from '@teambit/legacy.constants'; import type { LanesMain } from './lanes.main.runtime'; @@ -106,37 +106,33 @@ ${COMPONENT_PATTERN_HELP}`, } const getFailureOutput = () => { if (!failedComponents || !failedComponents.length) return ''; - const title = '\nswitch skipped for the following component(s)'; - const body = compact( - failedComponents.map((failedComponent) => { - // all failures here are "unchangedLegitimately". otherwise, it would have been thrown as an error - if (!verbose) return null; - return `${chalk.bold(failedComponent.id.toString())} - ${chalk.white(failedComponent.unchangedMessage)}`; - }) - ).join('\n'); - if (!body) { - return `${chalk.bold(`\nswitch skipped legitimately for ${failedComponents.length} component(s)`)} - (use --verbose to list them next time)`; + if (!verbose) { + return formatHint(`switch skipped for ${failedComponents.length} component(s) (use --verbose to list them)`); } - return `${chalk.underline(title)}\n${body}`; + const items = failedComponents.map((failedComponent) => + formatItem(`${chalk.bold(failedComponent.id.toString())} - ${failedComponent.unchangedMessage}`) + ); + return formatSection('switch skipped', '', items); }; const getSuccessfulOutput = () => { - const laneSwitched = chalk.green(`\nsuccessfully set "${chalk.bold(lane)}" as the active lane`); - if (!components || !components.length) return `No components have been changed.${laneSwitched}`; - const title = `successfully switched ${components.length} components to the head of lane ${lane}\n`; - return chalk.bold(title) + applyVersionReport(components, true, false) + laneSwitched; + const laneSwitched = formatSuccessSummary(`successfully set "${chalk.bold(lane)}" as the active lane`); + if (!components || !components.length) return `No components have been changed.\n${laneSwitched}`; + const title = `successfully switched ${components.length} components to the head of lane ${lane}`; + return [formatSuccessSummary(title), applyVersionReport(components, true, false), laneSwitched] + .filter(Boolean) + .join('\n'); }; const getGitBranchWarningOutput = () => { - return gitBranchWarning ? chalk.yellow(`Warning: ${gitBranchWarning}`) : null; + return gitBranchWarning ? chalk.yellow(`Warning: ${gitBranchWarning}`) : ''; }; - return compact([ + return joinSections([ getFailureOutput(), getSuccessfulOutput(), getGitBranchWarningOutput(), installationErrorOutput(installationError), compilationErrorOutput(compilationError), - ]).join('\n\n'); + ]); } } diff --git a/scopes/lanes/merge-lanes/merge-lane.cmd.ts b/scopes/lanes/merge-lanes/merge-lane.cmd.ts index 4630b284f5f2..9569cdd22978 100644 --- a/scopes/lanes/merge-lanes/merge-lane.cmd.ts +++ b/scopes/lanes/merge-lanes/merge-lane.cmd.ts @@ -1,5 +1,6 @@ import chalk from 'chalk'; import type { Command, CommandOptions } from '@teambit/cli'; +import { joinSections } from '@teambit/cli'; import type { MergeStrategy } from '@teambit/component.modules.merge-helper'; import { mergeReport } from '@teambit/merging'; import { COMPONENT_PATTERN_HELP, CFG_FORCE_LOCAL_BUILD } from '@teambit/legacy.constants'; @@ -219,11 +220,11 @@ Component pattern format: ${COMPONENT_PATTERN_HELP}`, }); const mergeResult = mergeReport({ ...mergeResults, configMergeResults, verbose }); - const deleteOutput = `\n${deleteResults.localResult ? removeTemplate(deleteResults.localResult, false) : ''}${( - deleteResults.remoteResult || [] - ).map((item) => removeTemplate(item, true))}${ - (deleteResults.readmeResult && chalk.yellow(deleteResults.readmeResult)) || '' - }\n`; - return mergeResult + deleteOutput; + const localDeleteOutput = deleteResults.localResult ? removeTemplate(deleteResults.localResult, false) : ''; + const remoteDeleteOutput = joinSections( + (deleteResults.remoteResult || []).map((item) => removeTemplate(item, true)) + ); + const readmeOutput = deleteResults.readmeResult ? chalk.yellow(deleteResults.readmeResult) : ''; + return joinSections([mergeResult, localDeleteOutput, remoteDeleteOutput, readmeOutput]); } }