Files
react-native/packager/react-packager/src/lib/TerminalReporter.js
Jean Lauliac 04fdf40c61 packager: TerminalReporter: remove underline/emoji
Reviewed By: cpojer

Differential Revision: D4357225

fbshipit-source-id: 05d6b4fd9b79616a68e5fa3c4700aeac2f3552a9
2016-12-21 05:58:32 -08:00

245 lines
7.2 KiB
JavaScript

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const path = require('path');
const reporting = require('./reporting');
const terminal = require('./terminal');
const throttle = require('lodash/throttle');
const util = require('util');
import type {ReportableEvent, GlobalCacheDisabledReason} from './reporting';
const DEP_GRAPH_MESSAGE = 'Loading dependency graph';
const GLOBAL_CACHE_DISABLED_MESSAGE_FORMAT =
'The global cache is now disabled because %s';
type BundleProgress = {
transformedFileCount: number,
totalFileCount: number,
ratio: number,
};
const DARK_BLOCK_CHAR = '\u2593';
const LIGHT_BLOCK_CHAR = '\u2591';
function getProgressBar(ratio: number, length: number) {
const blockCount = Math.floor(ratio * length);
return (
DARK_BLOCK_CHAR.repeat(blockCount) +
LIGHT_BLOCK_CHAR.repeat(length - blockCount)
);
}
type TerminalReportableEvent = ReportableEvent | {
type: 'bundle_transform_progressed_throttled',
entryFilePath: string,
transformedFileCount: number,
totalFileCount: number,
};
/**
* We try to print useful information to the terminal for interactive builds.
* This implements the `Reporter` interface from the './reporting' module.
*/
class TerminalReporter {
/**
* The bundle builds for which we are actively maintaining the status on the
* terminal, ie. showing a progress bar. There can be several bundles being
* built at the same time.
*/
_activeBundles: Map<string, BundleProgress>;
_dependencyGraphHasLoaded: boolean;
_scheduleUpdateBundleProgress: (data: {
entryFilePath: string,
transformedFileCount: number,
totalFileCount: number,
}) => void;
constructor() {
this._dependencyGraphHasLoaded = false;
this._activeBundles = new Map();
this._scheduleUpdateBundleProgress = throttle((data) => {
this.update({...data, type: 'bundle_transform_progressed_throttled'});
}, 200);
}
/**
* Return a message looking like this:
*
* Transforming files |#### | 34.2% (324/945)...
*
*/
_getFileTransformMessage(
{totalFileCount, transformedFileCount, ratio}: BundleProgress,
build: 'in_progress' | 'done',
): string {
if (build === 'done' && totalFileCount === 0) {
return 'All files are already up-to-date.';
}
return util.format(
'Transforming files %s%s% (%s/%s)%s',
build === 'done' ? '' : getProgressBar(ratio, 30) + ' ',
(100 * ratio).toFixed(1),
transformedFileCount,
totalFileCount,
build === 'done' ? ', done.' : '...',
);
}
/**
* Construct a message that represent the progress of a single bundle build.
*/
_getBundleStatusMessage(
entryFilePath: string,
progress: BundleProgress,
build: 'in_progress' | 'done',
): string {
const localPath = path.relative('.', entryFilePath);
return [
`Bundling \`${localPath}\``,
' ' + this._getFileTransformMessage(progress, build),
].join('\n');
}
_logCacheDisabled(reason: GlobalCacheDisabledReason): void {
const format = GLOBAL_CACHE_DISABLED_MESSAGE_FORMAT;
switch (reason) {
case 'too_many_errors':
reporting.logWarning(terminal, format, 'it has been failing too many times.');
break;
case 'too_many_misses':
reporting.logWarning(terminal, format, 'it has been missing too many consecutive keys.');
break;
}
}
/**
* This function is only concerned with logging and should not do state
* or terminal status updates.
*/
_log(event: TerminalReportableEvent): void {
switch (event.type) {
case 'bundle_built':
const progress = this._activeBundles.get(event.entryFilePath);
if (progress != null) {
terminal.log(
this._getBundleStatusMessage(event.entryFilePath, progress, 'done'),
);
}
break;
case 'dep_graph_loaded':
terminal.log(`${DEP_GRAPH_MESSAGE}, done.`);
break;
case 'global_cache_error':
reporting.logWarning(terminal, 'The global cache failed: %s', event.error.stack);
break;
case 'global_cache_disabled':
this._logCacheDisabled(event.reason);
break;
}
}
/**
* We use Math.pow(ratio, 2) to as a conservative measure of progress because
* we know the `totalCount` is going to progressively increase as well. We
* also prevent the ratio from going backwards.
*/
_updateBundleProgress(
{entryFilePath, transformedFileCount, totalFileCount}: {
entryFilePath: string,
transformedFileCount: number,
totalFileCount: number,
},
) {
const currentProgress = this._activeBundles.get(entryFilePath);
if (currentProgress == null) {
return;
}
const rawRatio = transformedFileCount / totalFileCount;
const conservativeRatio = Math.pow(rawRatio, 2);
const ratio = Math.max(conservativeRatio, currentProgress.ratio);
Object.assign(currentProgress, {
ratio,
transformedFileCount,
totalFileCount,
});
}
/**
* This function is exclusively concerned with updating the internal state.
* No logging or status updates should be done at this point.
*/
_updateState(event: TerminalReportableEvent): void {
switch (event.type) {
case 'bundle_requested':
this._activeBundles.set(event.entryFilePath, {
transformedFileCount: 0,
totalFileCount: 0,
ratio: 0,
});
break;
case 'bundle_transform_progressed':
this._scheduleUpdateBundleProgress(event);
break;
case 'bundle_transform_progressed_throttled':
this._updateBundleProgress(event);
break;
case 'bundle_built':
this._activeBundles.delete(event.entryFilePath);
break;
case 'dep_graph_loading':
this._dependencyGraphHasLoaded = false;
break;
case 'dep_graph_loaded':
this._dependencyGraphHasLoaded = true;
break;
}
}
_getDepGraphStatusMessage(): ?string {
if (!this._dependencyGraphHasLoaded) {
return `${DEP_GRAPH_MESSAGE}...`;
}
return null;
}
/**
* Return a status message that is always consistent with the current state
* of the application. Having this single function ensures we don't have
* different callsites overriding each other status messages.
*/
_getStatusMessage(): string {
return [
this._getDepGraphStatusMessage(),
].concat(Array.from(this._activeBundles.entries()).map(
([entryFilePath, progress]) =>
this._getBundleStatusMessage(entryFilePath, progress, 'in_progress'),
)).filter(str => str != null).join('\n');
}
/**
* Everything that happens goes through the same 3 steps. This makes the
* output more reliable and consistent, because no matter what additional.
*/
update(event: TerminalReportableEvent) {
this._log(event);
this._updateState(event);
terminal.status(this._getStatusMessage());
}
}
module.exports = TerminalReporter;