From 3cf97dd446c4fd1249257dea9f41b0b00afe429e Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Fri, 22 Dec 2017 00:19:59 +0200 Subject: [PATCH] refactor: move reporter for client to separate file --- src/index.ts | 357 +-------------------------------------- src/mergeOutputs.ts | 76 +++++++++ src/reporterForClient.ts | 292 ++++++++++++++++++++++++++++++++ 3 files changed, 372 insertions(+), 353 deletions(-) create mode 100644 src/mergeOutputs.ts create mode 100644 src/reporterForClient.ts diff --git a/src/index.ts b/src/index.ts index 98fd996275..367491108f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,36 +1,11 @@ -import chalk from 'chalk' import {EventEmitter} from 'events' import logUpdate = require('log-update') -import os = require('os') -import prettyBytes = require('pretty-bytes') import R = require('ramda') -import semver = require('semver') -import { - DeprecationLog, - InstallCheckLog, - LifecycleLog, - Log, - ProgressLog, - RegistryLog, -} from 'supi' +import {Log} from 'supi' import xs, {Stream} from 'xstream' -import dropRepeats from 'xstream/extra/dropRepeats' -import flattenConcurrently from 'xstream/extra/flattenConcurrently' import fromEvent from 'xstream/extra/fromEvent' -import getPkgsDiff, { - PackageDiff, - propertyByDependencyType, -} from './pkgsDiff' -import reportError from './reportError' - -const EOL = os.EOL -const BIG_TARBALL_SIZE = 1024 * 1024 * 5 // 5 MB - -const addedSign = chalk.green('+') -const removedSign = chalk.red('-') -const linkSign = chalk.magentaBright('#') -const hlValue = chalk.blue -const hlPkgId = chalk['whiteBright'] +import mergeOutputs from './mergeOutputs' +import reporterForClient from './reporterForClient' export default function ( streamParser: object, @@ -51,331 +26,7 @@ export function toOutput$ ( const isRecursive = cmd === 'recursive' const obs = fromEvent(streamParser as EventEmitter, 'data') const log$ = xs.fromObservable(obs) - const outputs: Array>> = [] - - const progressLog$ = log$ - .filter((log) => log.name === 'pnpm:progress') as Stream - - const resolutionDone$ = isRecursive - ? xs.never() - : log$ - .filter((log) => log.name === 'pnpm:stage' && log.message === 'resolution_done') - .mapTo(true) - .take(1) - .startWith(false) - - const resolvingContentLog$ = progressLog$ - .filter((log) => log.status === 'resolving_content') - .fold(R.inc, 0) - .drop(1) - .endWhen(resolutionDone$.last()) - - const fedtchedLog$ = progressLog$ - .filter((log) => log.status === 'fetched') - .fold(R.inc, 0) - - const foundInStoreLog$ = progressLog$ - .filter((log) => log.status === 'found_in_store') - .fold(R.inc, 0) - - if (!isRecursive) { - const alreadyUpToDate$ = xs.of( - resolvingContentLog$ - .take(1) - .mapTo(false) - .startWith(true) - .last() - .filter(R.equals(true)) - .mapTo({ - fixed: false, - msg: 'Already up-to-date', - }), - ) - - outputs.push(alreadyUpToDate$) - } - - const progressSummaryOutput$ = xs.of( - xs.combine( - resolvingContentLog$, - fedtchedLog$, - foundInStoreLog$, - isRecursive ? xs.of(false) : resolutionDone$, - ) - .map( - R.apply((resolving, fetched, foundInStore: number, resolutionDone) => { - const msg = `Resolving: total ${hlValue(resolving.toString())}, reused ${hlValue(foundInStore.toString())}, downloaded ${hlValue(fetched.toString())}` - if (resolving === foundInStore + fetched && resolutionDone) { - return { - fixed: false, - msg: `${msg}, done`, - } - } - return { - fixed: true, - msg, - } - }), - ), - ) - - outputs.push(progressSummaryOutput$) - - const tarballsProgressOutput$ = progressLog$ - .filter((log) => log.status === 'fetching_started' && - typeof log.size === 'number' && log.size >= BIG_TARBALL_SIZE) - .map((startedLog) => { - const size = prettyBytes(startedLog['size']) - return progressLog$ - .filter((log) => log.status === 'fetching_progress' && log.pkgId === startedLog['pkgId']) - .map((log) => log['downloaded']) - .startWith(0) - .map((downloadedRaw) => { - const done = startedLog['size'] === downloadedRaw - const downloaded = prettyBytes(downloadedRaw) - return { - fixed: !done, - msg: `Downloading ${hlPkgId(startedLog['pkgId'])}: ${hlValue(downloaded)}/${hlValue(size)}${done ? ', done' : ''}`, - } - }) - }) - - outputs.push(tarballsProgressOutput$) - - if (!isRecursive) { - const deprecationLog$ = log$ - .filter((log) => log.name === 'pnpm:deprecation') as Stream - - const pkgsDiff$ = getPkgsDiff(log$, deprecationLog$) - - const summaryLog$ = log$ - .filter((log) => log.name === 'pnpm:summary') - .take(1) - - const summaryOutput$ = xs.combine( - pkgsDiff$, - summaryLog$, - ) - .map(R.apply((pkgsDiff) => { - let msg = '' - for (const depType of ['prod', 'optional', 'dev']) { - const diffs = R.values(pkgsDiff[depType]) - if (diffs.length) { - msg += EOL - msg += chalk.blue(`${propertyByDependencyType[depType]}:`) - msg += EOL - msg += printDiffs(diffs) - msg += EOL - } - } - return {msg} - })) - .take(1) - .map(xs.of) - - outputs.push(summaryOutput$) - - const deprecationOutput$ = deprecationLog$ - // print warnings only about deprecated packages from the root - .filter((log) => log.depth === 0) - .map((log) => { - return { - msg: formatWarn(`${chalk.red('deprecated')} ${log.pkgName}@${log.pkgVersion}: ${log.deprecated}`), - } - }) - .map(xs.of) - - outputs.push(deprecationOutput$) - } - - const lifecycleMessages: {[pkgId: string]: string} = {} - const lifecycleOutput$ = xs.of( - log$ - .filter((log) => log.name === 'pnpm:lifecycle') - .map((log: LifecycleLog) => { - const key = `${log.script}:${log.pkgId}` - lifecycleMessages[key] = formatLifecycle(log) - return R.values(lifecycleMessages).join(EOL) - }) - .map((msg) => ({msg})), - ) - - outputs.push(lifecycleOutput$) - - if (!isRecursive) { - const installCheckOutput$ = log$ - .filter((log) => log.name === 'pnpm:install-check') - .map(formatInstallCheck) - .filter(Boolean) - .map((msg) => ({msg})) - .map(xs.of) as Stream> - - outputs.push(installCheckOutput$) - - const registryOutput$ = log$ - .filter((log) => log.name === 'pnpm:registry' && log.level === 'warn') - .map((log: RegistryLog) => ({msg: formatWarn(log.message)})) - .map(xs.of) - - outputs.push(registryOutput$) - - const miscOutput$ = log$ - .filter((log) => log.name as string === 'pnpm' || !isRecursive && log.name as string === 'pnpm:link') - .map((obj) => { - if (obj.level === 'debug') return - if (obj.level === 'warn') { - return formatWarn(obj['message']) - } - if (obj.level === 'error') { - return reportError(obj) - } - return obj['message'] - }) - .map((msg) => ({msg})) - .map(xs.of) - - outputs.push(miscOutput$) - } + const outputs: Array>> = reporterForClient(log$, isRecursive) return mergeOutputs(outputs) } - -function mergeOutputs (outputs: Array>>): Stream { - let blockNo = 0 - let fixedBlockNo = 0 - let started = false - return flattenConcurrently( - (xs.merge.apply(xs, outputs) as xs>) - .map((log: Stream<{msg: string, fixed: boolean}>) => { - let currentBlockNo = -1 - let currentFixedBlockNo = -1 - let calculated = false - let fixedCalculated = false - return log - .map((msg) => { - if (msg['fixed']) { - if (!fixedCalculated) { - fixedCalculated = true - currentFixedBlockNo = fixedBlockNo++ - } - return { - blockNo: currentFixedBlockNo, - fixed: true, - msg: msg.msg, - } - } - if (!calculated) { - calculated = true - currentBlockNo = blockNo++ - } - return { - blockNo: currentBlockNo, - fixed: false, - msg: typeof msg === 'string' ? msg : msg.msg, - prevFixedBlockNo: currentFixedBlockNo, - } - }) - }), - ) - .fold((acc, log) => { - if (log.fixed === true) { - acc.fixedBlocks[log.blockNo] = log.msg - } else { - delete acc.fixedBlocks[log['prevFixedBlockNo']] - acc.blocks[log.blockNo] = log.msg - } - return acc - }, {fixedBlocks: [], blocks: []} as {fixedBlocks: string[], blocks: string[]}) - .map((sections) => { - const fixedBlocks = sections.fixedBlocks.filter(Boolean) - const nonFixedPart = sections.blocks.filter(Boolean).join(EOL) - if (!fixedBlocks.length) { - return nonFixedPart - } - const fixedPart = fixedBlocks.join(EOL) - if (!nonFixedPart) { - return fixedPart - } - return chalk.dim(nonFixedPart) + EOL + fixedPart - }) - .filter((msg) => { - if (started) { - return true - } - if (msg === '') return false - started = true - return true - }) - .compose(dropRepeats()) -} - -function printDiffs (pkgsDiff: PackageDiff[]) { - // Sorts by alphabet then by removed/added - // + ava 0.10.0 - // - chalk 1.0.0 - // + chalk 2.0.0 - pkgsDiff.sort((a, b) => (a.name.localeCompare(b.name) * 10 + (Number(!b.added) - Number(!a.added)))) - const msg = pkgsDiff.map((pkg) => { - let result = pkg.added - ? addedSign - : pkg.linked - ? linkSign - : removedSign - if (!pkg.realName || pkg.name === pkg.realName) { - result += ` ${pkg.name}` - } else { - result += ` ${pkg.name} <- ${pkg.realName}` - } - if (pkg.version) { - result += ` ${chalk.grey(pkg.version)}` - if (pkg.latest && semver.lt(pkg.version, pkg.latest)) { - result += ` ${chalk.grey(`(${pkg.latest} is available)`)}` - } - } - if (pkg.deprecated) { - result += ` ${chalk.red('deprecated')}` - } - if (pkg.linked) { - result += ` ${chalk.magentaBright('linked from')} ${chalk.grey(pkg.from || '???')}` - } - return result - }).join(EOL) - return msg -} - -function formatLifecycle (logObj: LifecycleLog) { - const prefix = `Running ${hlValue(logObj.script)} for ${hlPkgId(logObj.pkgId)}` - if (logObj['exitCode'] === 0) { - return `${prefix}, done` - } - const line = formatLine(logObj) - if (logObj.level === 'error') { - return `${prefix}! ${line}` - } - return `${prefix}: ${line}` -} - -function formatLine (logObj: LifecycleLog) { - if (typeof logObj['exitCode'] === 'number') return chalk.red(`Exited with ${logObj['exitCode']}`) - - const color = logObj.level === 'error' ? chalk.red : chalk.gray - return color(logObj['line']) -} - -function formatInstallCheck (logObj: InstallCheckLog) { - switch (logObj.code) { - case 'EBADPLATFORM': - return formatWarn(`Unsupported system. Skipping dependency ${logObj.pkgId}`) - case 'ENOTSUP': - return logObj.toString() - default: - return - } -} - -function formatWarn (message: string) { - // The \u2009 is the "thin space" unicode character - // It is used instead of ' ' because chalk (as of version 2.1.0) - // trims whitespace at the beginning - return `${chalk.bgYellow.black('\u2009WARN\u2009')} ${message}` -} diff --git a/src/mergeOutputs.ts b/src/mergeOutputs.ts new file mode 100644 index 0000000000..98b6cd1042 --- /dev/null +++ b/src/mergeOutputs.ts @@ -0,0 +1,76 @@ +import chalk from 'chalk' +import os = require('os') +import xs, {Stream} from 'xstream' +import dropRepeats from 'xstream/extra/dropRepeats' +import flattenConcurrently from 'xstream/extra/flattenConcurrently' + +const EOL = os.EOL + +export default function mergeOutputs (outputs: Array>>): Stream { + let blockNo = 0 + let fixedBlockNo = 0 + let started = false + return flattenConcurrently( + (xs.merge.apply(xs, outputs) as xs>) + .map((log: Stream<{msg: string, fixed: boolean}>) => { + let currentBlockNo = -1 + let currentFixedBlockNo = -1 + let calculated = false + let fixedCalculated = false + return log + .map((msg) => { + if (msg['fixed']) { + if (!fixedCalculated) { + fixedCalculated = true + currentFixedBlockNo = fixedBlockNo++ + } + return { + blockNo: currentFixedBlockNo, + fixed: true, + msg: msg.msg, + } + } + if (!calculated) { + calculated = true + currentBlockNo = blockNo++ + } + return { + blockNo: currentBlockNo, + fixed: false, + msg: typeof msg === 'string' ? msg : msg.msg, + prevFixedBlockNo: currentFixedBlockNo, + } + }) + }), + ) + .fold((acc, log) => { + if (log.fixed === true) { + acc.fixedBlocks[log.blockNo] = log.msg + } else { + delete acc.fixedBlocks[log['prevFixedBlockNo']] + acc.blocks[log.blockNo] = log.msg + } + return acc + }, {fixedBlocks: [], blocks: []} as {fixedBlocks: string[], blocks: string[]}) + .map((sections) => { + const fixedBlocks = sections.fixedBlocks.filter(Boolean) + const nonFixedPart = sections.blocks.filter(Boolean).join(EOL) + if (!fixedBlocks.length) { + return nonFixedPart + } + const fixedPart = fixedBlocks.join(EOL) + if (!nonFixedPart) { + return fixedPart + } + return chalk.dim(nonFixedPart) + EOL + fixedPart + }) + .filter((msg) => { + if (started) { + return true + } + if (msg === '') return false + started = true + return true + }) + .compose(dropRepeats()) +} diff --git a/src/reporterForClient.ts b/src/reporterForClient.ts new file mode 100644 index 0000000000..1a544d4624 --- /dev/null +++ b/src/reporterForClient.ts @@ -0,0 +1,292 @@ +import chalk from 'chalk' +import os = require('os') +import prettyBytes = require('pretty-bytes') +import R = require('ramda') +import semver = require('semver') +import { + DeprecationLog, + InstallCheckLog, + LifecycleLog, + Log, + ProgressLog, + RegistryLog, +} from 'supi' +import xs, {Stream} from 'xstream' +import getPkgsDiff, { + PackageDiff, + propertyByDependencyType, +} from './pkgsDiff' +import reportError from './reportError' + +const EOL = os.EOL +const BIG_TARBALL_SIZE = 1024 * 1024 * 5 // 5 MB + +const addedSign = chalk.green('+') +const removedSign = chalk.red('-') +const linkSign = chalk.magentaBright('#') +const hlValue = chalk.blue +const hlPkgId = chalk['whiteBright'] + +export default function ( + log$: xs, + isRecursive: boolean, +): Array>> { + const outputs: Array>> = [] + + const progressLog$ = log$ + .filter((log) => log.name === 'pnpm:progress') as Stream + + const resolutionDone$ = isRecursive + ? xs.never() + : log$ + .filter((log) => log.name === 'pnpm:stage' && log.message === 'resolution_done') + .mapTo(true) + .take(1) + .startWith(false) + + const resolvingContentLog$ = progressLog$ + .filter((log) => log.status === 'resolving_content') + .fold(R.inc, 0) + .drop(1) + .endWhen(resolutionDone$.last()) + + const fedtchedLog$ = progressLog$ + .filter((log) => log.status === 'fetched') + .fold(R.inc, 0) + + const foundInStoreLog$ = progressLog$ + .filter((log) => log.status === 'found_in_store') + .fold(R.inc, 0) + + if (!isRecursive) { + const alreadyUpToDate$ = xs.of( + resolvingContentLog$ + .take(1) + .mapTo(false) + .startWith(true) + .last() + .filter(R.equals(true)) + .mapTo({ + fixed: false, + msg: 'Already up-to-date', + }), + ) + + outputs.push(alreadyUpToDate$) + } + + const progressSummaryOutput$ = xs.of( + xs.combine( + resolvingContentLog$, + fedtchedLog$, + foundInStoreLog$, + isRecursive ? xs.of(false) : resolutionDone$, + ) + .map( + R.apply((resolving, fetched, foundInStore: number, resolutionDone) => { + const msg = `Resolving: total ${hlValue(resolving.toString())}, reused ${hlValue(foundInStore.toString())}, downloaded ${hlValue(fetched.toString())}` + if (resolving === foundInStore + fetched && resolutionDone) { + return { + fixed: false, + msg: `${msg}, done`, + } + } + return { + fixed: true, + msg, + } + }), + ), + ) + + outputs.push(progressSummaryOutput$) + + const tarballsProgressOutput$ = progressLog$ + .filter((log) => log.status === 'fetching_started' && + typeof log.size === 'number' && log.size >= BIG_TARBALL_SIZE) + .map((startedLog) => { + const size = prettyBytes(startedLog['size']) + return progressLog$ + .filter((log) => log.status === 'fetching_progress' && log.pkgId === startedLog['pkgId']) + .map((log) => log['downloaded']) + .startWith(0) + .map((downloadedRaw) => { + const done = startedLog['size'] === downloadedRaw + const downloaded = prettyBytes(downloadedRaw) + return { + fixed: !done, + msg: `Downloading ${hlPkgId(startedLog['pkgId'])}: ${hlValue(downloaded)}/${hlValue(size)}${done ? ', done' : ''}`, + } + }) + }) + + outputs.push(tarballsProgressOutput$) + + if (!isRecursive) { + const deprecationLog$ = log$ + .filter((log) => log.name === 'pnpm:deprecation') as Stream + + const pkgsDiff$ = getPkgsDiff(log$, deprecationLog$) + + const summaryLog$ = log$ + .filter((log) => log.name === 'pnpm:summary') + .take(1) + + const summaryOutput$ = xs.combine( + pkgsDiff$, + summaryLog$, + ) + .map(R.apply((pkgsDiff) => { + let msg = '' + for (const depType of ['prod', 'optional', 'dev']) { + const diffs = R.values(pkgsDiff[depType]) + if (diffs.length) { + msg += EOL + msg += chalk.blue(`${propertyByDependencyType[depType]}:`) + msg += EOL + msg += printDiffs(diffs) + msg += EOL + } + } + return {msg} + })) + .take(1) + .map(xs.of) + + outputs.push(summaryOutput$) + + const deprecationOutput$ = deprecationLog$ + // print warnings only about deprecated packages from the root + .filter((log) => log.depth === 0) + .map((log) => { + return { + msg: formatWarn(`${chalk.red('deprecated')} ${log.pkgName}@${log.pkgVersion}: ${log.deprecated}`), + } + }) + .map(xs.of) + + outputs.push(deprecationOutput$) + } + + const lifecycleMessages: {[pkgId: string]: string} = {} + const lifecycleOutput$ = xs.of( + log$ + .filter((log) => log.name === 'pnpm:lifecycle') + .map((log: LifecycleLog) => { + const key = `${log.script}:${log.pkgId}` + lifecycleMessages[key] = formatLifecycle(log) + return R.values(lifecycleMessages).join(EOL) + }) + .map((msg) => ({msg})), + ) + + outputs.push(lifecycleOutput$) + + if (!isRecursive) { + const installCheckOutput$ = log$ + .filter((log) => log.name === 'pnpm:install-check') + .map(formatInstallCheck) + .filter(Boolean) + .map((msg) => ({msg})) + .map(xs.of) as Stream> + + outputs.push(installCheckOutput$) + + const registryOutput$ = log$ + .filter((log) => log.name === 'pnpm:registry' && log.level === 'warn') + .map((log: RegistryLog) => ({msg: formatWarn(log.message)})) + .map(xs.of) + + outputs.push(registryOutput$) + + const miscOutput$ = log$ + .filter((log) => log.name as string === 'pnpm' || !isRecursive && log.name as string === 'pnpm:link') + .map((obj) => { + if (obj.level === 'debug') return + if (obj.level === 'warn') { + return formatWarn(obj['message']) + } + if (obj.level === 'error') { + return reportError(obj) + } + return obj['message'] + }) + .map((msg) => ({msg})) + .map(xs.of) + + outputs.push(miscOutput$) + } + + return outputs +} + +function printDiffs (pkgsDiff: PackageDiff[]) { + // Sorts by alphabet then by removed/added + // + ava 0.10.0 + // - chalk 1.0.0 + // + chalk 2.0.0 + pkgsDiff.sort((a, b) => (a.name.localeCompare(b.name) * 10 + (Number(!b.added) - Number(!a.added)))) + const msg = pkgsDiff.map((pkg) => { + let result = pkg.added + ? addedSign + : pkg.linked + ? linkSign + : removedSign + if (!pkg.realName || pkg.name === pkg.realName) { + result += ` ${pkg.name}` + } else { + result += ` ${pkg.name} <- ${pkg.realName}` + } + if (pkg.version) { + result += ` ${chalk.grey(pkg.version)}` + if (pkg.latest && semver.lt(pkg.version, pkg.latest)) { + result += ` ${chalk.grey(`(${pkg.latest} is available)`)}` + } + } + if (pkg.deprecated) { + result += ` ${chalk.red('deprecated')}` + } + if (pkg.linked) { + result += ` ${chalk.magentaBright('linked from')} ${chalk.grey(pkg.from || '???')}` + } + return result + }).join(EOL) + return msg +} + +function formatLifecycle (logObj: LifecycleLog) { + const prefix = `Running ${hlValue(logObj.script)} for ${hlPkgId(logObj.pkgId)}` + if (logObj['exitCode'] === 0) { + return `${prefix}, done` + } + const line = formatLine(logObj) + if (logObj.level === 'error') { + return `${prefix}! ${line}` + } + return `${prefix}: ${line}` +} + +function formatLine (logObj: LifecycleLog) { + if (typeof logObj['exitCode'] === 'number') return chalk.red(`Exited with ${logObj['exitCode']}`) + + const color = logObj.level === 'error' ? chalk.red : chalk.gray + return color(logObj['line']) +} + +function formatInstallCheck (logObj: InstallCheckLog) { + switch (logObj.code) { + case 'EBADPLATFORM': + return formatWarn(`Unsupported system. Skipping dependency ${logObj.pkgId}`) + case 'ENOTSUP': + return logObj.toString() + default: + return + } +} + +function formatWarn (message: string) { + // The \u2009 is the "thin space" unicode character + // It is used instead of ' ' because chalk (as of version 2.1.0) + // trims whitespace at the beginning + return `${chalk.bgYellow.black('\u2009WARN\u2009')} ${message}` +}