packager: make output simpler, more legible

Reviewed By: cpojer

Differential Revision: D4339417

fbshipit-source-id: f174ee11bc220de5e8da1d8227e9a9ceb5319e8d
This commit is contained in:
Jean Lauliac 2016-12-19 10:50:20 -08:00 committed by Facebook Github Bot
parent c92ad5f6ae
commit ede04abf8f
16 changed files with 474 additions and 188 deletions

View File

@ -10,6 +10,7 @@
const log = require('../util/log').out('bundle'); const log = require('../util/log').out('bundle');
const Server = require('../../packager/react-packager/src/Server'); const Server = require('../../packager/react-packager/src/Server');
const TerminalReporter = require('../../packager/react-packager/src/lib/TerminalReporter');
const outputBundle = require('./output/bundle'); const outputBundle = require('./output/bundle');
const path = require('path'); const path = require('path');
@ -57,6 +58,7 @@ function buildBundle(args, config, output = outputBundle, packagerInstance) {
extraNodeModules: config.extraNodeModules, extraNodeModules: config.extraNodeModules,
resetCache: args.resetCache, resetCache: args.resetCache,
watch: false, watch: false,
reporter: new TerminalReporter(),
}; };
packagerInstance = new Server(options); packagerInstance = new Server(options);

View File

@ -11,7 +11,6 @@
const querystring = require('querystring'); const querystring = require('querystring');
const url = require('url'); const url = require('url');
const {createEntry, print} = require('../../../packager/react-packager/src/Logger');
const {getInverseDependencies} = require('../../../packager/react-packager/src/node-haste'); const {getInverseDependencies} = require('../../../packager/react-packager/src/node-haste');
const blacklist = [ const blacklist = [
@ -114,9 +113,7 @@ function attachHMRServer({httpServer, path, packagerServer}) {
path: path, path: path,
}); });
print(createEntry(`HMR Server listening on ${path}`));
wss.on('connection', ws => { wss.on('connection', ws => {
print(createEntry('HMR Client connected'));
const params = querystring.parse(url.parse(ws.upgradeReq.url).query); const params = querystring.parse(url.parse(ws.upgradeReq.url).query);
getDependencies(params.platform, params.bundleEntry) getDependencies(params.platform, params.bundleEntry)
@ -140,7 +137,6 @@ function attachHMRServer({httpServer, path, packagerServer}) {
if (!client) { if (!client) {
return; return;
} }
print(createEntry('HMR Server detected file change'));
const blacklisted = blacklist.find(blacklistedPath => const blacklisted = blacklist.find(blacklistedPath =>
filename.indexOf(blacklistedPath) !== -1 filename.indexOf(blacklistedPath) !== -1
@ -297,7 +293,6 @@ function attachHMRServer({httpServer, path, packagerServer}) {
return; return;
} }
print(createEntry('HMR Server sending update to client'));
client.ws.send(update); client.ws.send(update);
}); });

View File

@ -57,12 +57,22 @@ function createServer(options) {
options = Object.assign({}, options); options = Object.assign({}, options);
delete options.verbose; delete options.verbose;
if (options.reporter == null) {
// It's unsound to set-up the reporter here, but this allows backward
// compatibility.
var TerminalReporter = require('./src/lib/TerminalReporter');
options.reporter = new TerminalReporter();
}
var Server = require('./src/Server'); var Server = require('./src/Server');
return new Server(options); return new Server(options);
} }
function createNonPersistentServer(options) { function createNonPersistentServer(options) {
Logger.disablePrinting(); if (options.reporter == null) {
// It's unsound to set-up the reporter here, but this allows backward
// compatibility.
options.reporter = require('./src/lib/reporting').nullReporter;
}
options.watch = !options.nonPersistent; options.watch = !options.nonPersistent;
return createServer(options); return createServer(options);
} }

View File

@ -39,6 +39,7 @@ import type AssetServer from '../AssetServer';
import type Module from '../node-haste/Module'; import type Module from '../node-haste/Module';
import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse'; import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse';
import type {Options as TransformOptions} from '../JSTransformer/worker/worker'; import type {Options as TransformOptions} from '../JSTransformer/worker/worker';
import type {Reporter} from '../lib/reporting';
export type GetTransformOptions<T> = ( export type GetTransformOptions<T> = (
string, string,
@ -54,7 +55,6 @@ const {
createActionStartEntry, createActionStartEntry,
createActionEndEntry, createActionEndEntry,
log, log,
print,
} = require('../Logger'); } = require('../Logger');
const validateOpts = declareOpts({ const validateOpts = declareOpts({
@ -113,6 +113,9 @@ const validateOpts = declareOpts({
type: 'boolean', type: 'boolean',
default: false, default: false,
}, },
reporter: {
type: 'object',
},
}); });
const assetPropertyBlacklist = new Set([ const assetPropertyBlacklist = new Set([
@ -122,21 +125,22 @@ const assetPropertyBlacklist = new Set([
]); ]);
type Options = { type Options = {
projectRoots: Array<string>, allowBundleUpdates: boolean,
assetExts: Array<string>,
assetServer: AssetServer,
blacklistRE: RegExp, blacklistRE: RegExp,
moduleFormat: string,
polyfillModuleNames: Array<string>,
cacheVersion: string, cacheVersion: string,
extraNodeModules: {},
getTransformOptions?: GetTransformOptions<*>,
moduleFormat: string,
platforms: Array<string>,
polyfillModuleNames: Array<string>,
projectRoots: Array<string>,
reporter: Reporter,
resetCache: boolean, resetCache: boolean,
transformModulePath: string, transformModulePath: string,
getTransformOptions?: GetTransformOptions<*>,
extraNodeModules: {},
assetExts: Array<string>,
platforms: Array<string>,
watch: boolean,
assetServer: AssetServer,
transformTimeoutInterval: ?number, transformTimeoutInterval: ?number,
allowBundleUpdates: boolean, watch: boolean,
}; };
class Bundler { class Bundler {
@ -204,20 +208,21 @@ class Bundler {
blacklistRE: opts.blacklistRE, blacklistRE: opts.blacklistRE,
cache: this._cache, cache: this._cache,
extraNodeModules: opts.extraNodeModules, extraNodeModules: opts.extraNodeModules,
watch: opts.watch,
minifyCode: this._transformer.minify, minifyCode: this._transformer.minify,
moduleFormat: opts.moduleFormat, moduleFormat: opts.moduleFormat,
platforms: opts.platforms, platforms: opts.platforms,
polyfillModuleNames: opts.polyfillModuleNames, polyfillModuleNames: opts.polyfillModuleNames,
projectRoots: opts.projectRoots, projectRoots: opts.projectRoots,
reporter: options.reporter,
resetCache: opts.resetCache, resetCache: opts.resetCache,
transformCacheKey,
transformCode: transformCode:
(module, code, transformCodeOptions) => this._transformer.transformFile( (module, code, transformCodeOptions) => this._transformer.transformFile(
module.path, module.path,
code, code,
transformCodeOptions, transformCodeOptions,
), ),
transformCacheKey, watch: opts.watch,
}); });
this._projectRoots = opts.projectRoots; this._projectRoots = opts.projectRoots;
@ -401,11 +406,11 @@ class Bundler {
onProgress = noop, onProgress = noop,
}: *) { }: *) {
const transformingFilesLogEntry = const transformingFilesLogEntry =
print(log(createActionStartEntry({ log(createActionStartEntry({
action_name: 'Transforming files', action_name: 'Transforming files',
entry_point: entryFile, entry_point: entryFile,
environment: dev ? 'dev' : 'prod', environment: dev ? 'dev' : 'prod',
}))); }));
const modulesByName = Object.create(null); const modulesByName = Object.create(null);
@ -425,7 +430,7 @@ class Bundler {
return Promise.resolve(resolutionResponse).then(response => { return Promise.resolve(resolutionResponse).then(response => {
bundle.setRamGroups(response.transformOptions.transform.ramGroups); bundle.setRamGroups(response.transformOptions.transform.ramGroups);
print(log(createActionEndEntry(transformingFilesLogEntry))); log(createActionEndEntry(transformingFilesLogEntry));
onResolutionResponse(response); onResolutionResponse(response);
// get entry file complete path (`entryFile` is relative to roots) // get entry file complete path (`entryFile` is relative to roots)

View File

@ -17,7 +17,6 @@ const {
createEntry, createEntry,
createActionStartEntry, createActionStartEntry,
createActionEndEntry, createActionEndEntry,
enablePrinting,
} = require('../'); } = require('../');
describe('Logger', () => { describe('Logger', () => {
@ -29,7 +28,6 @@ describe('Logger', () => {
afterEach(() => { afterEach(() => {
console.log = originalConsoleLog; console.log = originalConsoleLog;
enablePrinting();
}); });
it('creates simple log entries', () => { it('creates simple log entries', () => {

View File

@ -7,14 +7,12 @@
* of patent rights can be found in the PATENTS file in the same directory. * of patent rights can be found in the PATENTS file in the same directory.
* *
* @flow * @flow
*
*/ */
'use strict'; 'use strict';
const chalk = require('chalk');
const os = require('os'); const os = require('os');
const pkgjson = require('../../../package.json'); const pkgjson = require('../../../package.json');
const terminal = require('../lib/terminal');
const {EventEmitter} = require('events'); const {EventEmitter} = require('events');
@ -24,17 +22,6 @@ import type {
LogEntry, LogEntry,
} from './Types'; } from './Types';
const DATE_LOCALE_OPTIONS = {
day: '2-digit',
hour12: false,
hour: '2-digit',
minute: '2-digit',
month: '2-digit',
second: '2-digit',
year: 'numeric',
};
let PRINT_LOG_ENTRIES = true;
const log_session = `${os.hostname()}-${Date.now()}`; const log_session = `${os.hostname()}-${Date.now()}`;
const eventEmitter = new EventEmitter(); const eventEmitter = new EventEmitter();
@ -93,68 +80,10 @@ function log(logEntry: LogEntry): LogEntry {
return logEntry; return logEntry;
} }
function print(
logEntry: LogEntry,
printFields?: Array<string> = [],
): LogEntry {
if (!PRINT_LOG_ENTRIES) {
return logEntry;
}
const {
log_entry_label: logEntryLabel,
action_phase: actionPhase,
duration_ms: duration,
} = logEntry;
const timeStamp = new Date().toLocaleString(undefined, DATE_LOCALE_OPTIONS);
let logEntryString;
switch (actionPhase) {
case 'start':
logEntryString = chalk.dim(`[${timeStamp}] <START> ${logEntryLabel}`);
break;
case 'end':
logEntryString = chalk.dim(`[${timeStamp}] <END> ${logEntryLabel}`) +
chalk.cyan(` (${+duration}ms)`);
break;
default:
logEntryString = chalk.dim(`[${timeStamp}] ${logEntryLabel}`);
break;
}
if (printFields.length) {
const indent = ' '.repeat(timeStamp.length + 11);
for (const field of printFields) {
const value = logEntry[field];
if (value === undefined) {
continue;
}
logEntryString += chalk.dim(`\n${indent}${field}: ${value.toString()}`);
}
}
// eslint-disable-next-line no-console-disallow
terminal.log(logEntryString);
return logEntry;
}
function enablePrinting(): void {
PRINT_LOG_ENTRIES = true;
}
function disablePrinting(): void {
PRINT_LOG_ENTRIES = false;
}
module.exports = { module.exports = {
on, on,
createEntry, createEntry,
createActionStartEntry, createActionStartEntry,
createActionEndEntry, createActionEndEntry,
log, log,
print,
enablePrinting,
disablePrinting,
}; };

View File

@ -21,6 +21,7 @@ import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionRes
import type Module from '../node-haste/Module'; import type Module from '../node-haste/Module';
import type {SourceMap} from '../lib/SourceMap'; import type {SourceMap} from '../lib/SourceMap';
import type {Options as TransformOptions} from '../JSTransformer/worker/worker'; import type {Options as TransformOptions} from '../JSTransformer/worker/worker';
import type {Reporter} from '../lib/reporting';
const validateOpts = declareOpts({ const validateOpts = declareOpts({
projectRoots: { projectRoots: {
@ -71,6 +72,9 @@ const validateOpts = declareOpts({
type: 'boolean', type: 'boolean',
default: false, default: false,
}, },
reporter: {
type: 'object',
},
}); });
const getDependenciesValidateOpts = declareOpts({ const getDependenciesValidateOpts = declareOpts({
@ -99,30 +103,34 @@ class Resolver {
Promise<{code: string, map: SourceMap}>; Promise<{code: string, map: SourceMap}>;
_polyfillModuleNames: Array<string>; _polyfillModuleNames: Array<string>;
constructor(options: {resetCache: boolean}) { constructor(options: {
reporter: Reporter,
resetCache: boolean,
}) {
const opts = validateOpts(options); const opts = validateOpts(options);
this._depGraph = new DependencyGraph({ this._depGraph = new DependencyGraph({
roots: opts.projectRoots, assetDependencies: ['react-native/Libraries/Image/AssetRegistry'],
assetExts: opts.assetExts, assetExts: opts.assetExts,
cache: opts.cache,
extraNodeModules: opts.extraNodeModules,
ignoreFilePath: function(filepath) { ignoreFilePath: function(filepath) {
return filepath.indexOf('__tests__') !== -1 || return filepath.indexOf('__tests__') !== -1 ||
(opts.blacklistRE && opts.blacklistRE.test(filepath)); (opts.blacklistRE && opts.blacklistRE.test(filepath));
}, },
providesModuleNodeModules: defaults.providesModuleNodeModules,
platforms: opts.platforms,
preferNativePlatform: true,
watch: opts.watch,
cache: opts.cache,
transformCode: opts.transformCode,
transformCacheKey: opts.transformCacheKey,
extraNodeModules: opts.extraNodeModules,
assetDependencies: ['react-native/Libraries/Image/AssetRegistry'],
resetCache: options.resetCache,
moduleOptions: { moduleOptions: {
cacheTransformResults: true, cacheTransformResults: true,
resetCache: options.resetCache, resetCache: options.resetCache,
}, },
platforms: opts.platforms,
preferNativePlatform: true,
providesModuleNodeModules: defaults.providesModuleNodeModules,
reporter: options.reporter,
resetCache: options.resetCache,
roots: opts.projectRoots,
transformCacheKey: opts.transformCacheKey,
transformCode: opts.transformCode,
watch: opts.watch,
}); });
this._minifyCode = opts.minifyCode; this._minifyCode = opts.minifyCode;

View File

@ -39,7 +39,8 @@ describe('processRequest', () => {
projectRoots: ['root'], projectRoots: ['root'],
blacklistRE: null, blacklistRE: null,
cacheVersion: null, cacheVersion: null,
polyfillModuleNames: null polyfillModuleNames: null,
reporter: require('../../lib/reporting').nullReporter,
}; };
const makeRequest = (reqHandler, requrl, reqOptions) => new Promise(resolve => const makeRequest = (reqHandler, requrl, reqOptions) => new Promise(resolve =>

View File

@ -22,7 +22,6 @@ const defaults = require('../../../defaults');
const mime = require('mime-types'); const mime = require('mime-types');
const path = require('path'); const path = require('path');
const terminal = require('../lib/terminal'); const terminal = require('../lib/terminal');
const throttle = require('lodash/throttle');
const url = require('url'); const url = require('url');
const debug = require('debug')('ReactNativePackager:Server'); const debug = require('debug')('ReactNativePackager:Server');
@ -32,12 +31,12 @@ import type {Stats} from 'fs';
import type {IncomingMessage, ServerResponse} from 'http'; import type {IncomingMessage, ServerResponse} from 'http';
import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse'; import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse';
import type Bundle from '../Bundler/Bundle'; import type Bundle from '../Bundler/Bundle';
import type {Reporter} from '../lib/reporting';
const { const {
createActionStartEntry, createActionStartEntry,
createActionEndEntry, createActionEndEntry,
log, log,
print,
} = require('../Logger'); } = require('../Logger');
function debounceAndBatch(fn, delay) { function debounceAndBatch(fn, delay) {
@ -109,6 +108,9 @@ const validateOpts = declareOpts({
type: 'boolean', type: 'boolean',
default: false, default: false,
}, },
reporter: {
type: 'object',
},
}); });
const bundleOpts = declareOpts({ const bundleOpts = declareOpts({
@ -223,12 +225,17 @@ class Server {
_bundler: Bundler; _bundler: Bundler;
_debouncedFileChangeHandler: (filePath: string) => mixed; _debouncedFileChangeHandler: (filePath: string) => mixed;
_hmrFileChangeListener: (type: string, filePath: string) => mixed; _hmrFileChangeListener: (type: string, filePath: string) => mixed;
_reporter: Reporter;
constructor(options: {watch?: boolean}) { constructor(options: {
reporter: Reporter,
watch?: boolean,
}) {
const opts = this._opts = validateOpts(options); const opts = this._opts = validateOpts(options);
const processFileChange = const processFileChange =
({type, filePath, stat}) => this.onFileChange(type, filePath, stat); ({type, filePath, stat}) => this.onFileChange(type, filePath, stat);
this._reporter = options.reporter;
this._projectRoots = opts.projectRoots; this._projectRoots = opts.projectRoots;
this._bundles = Object.create(null); this._bundles = Object.create(null);
this._changeWatchers = []; this._changeWatchers = [];
@ -243,6 +250,7 @@ class Server {
bundlerOpts.assetServer = this._assetServer; bundlerOpts.assetServer = this._assetServer;
bundlerOpts.allowBundleUpdates = options.watch; bundlerOpts.allowBundleUpdates = options.watch;
bundlerOpts.watch = options.watch; bundlerOpts.watch = options.watch;
bundlerOpts.reporter = options.reporter;
this._bundler = new Bundler(bundlerOpts); this._bundler = new Bundler(bundlerOpts);
// changes to the haste map can affect resolution of files in the bundle // changes to the haste map can affect resolution of files in the bundle
@ -514,10 +522,10 @@ class Server {
const assetPath: string = urlObj.pathname.match(/^\/assets\/(.+)$/); const assetPath: string = urlObj.pathname.match(/^\/assets\/(.+)$/);
const processingAssetRequestLogEntry = const processingAssetRequestLogEntry =
print(log(createActionStartEntry({ log(createActionStartEntry({
action_name: 'Processing asset request', action_name: 'Processing asset request',
asset: assetPath[1], asset: assetPath[1],
})), ['asset']); }));
/* $FlowFixMe: query may be empty for invalid URLs */ /* $FlowFixMe: query may be empty for invalid URLs */
this._assetServer.get(assetPath[1], urlObj.query.platform) this._assetServer.get(assetPath[1], urlObj.query.platform)
@ -530,7 +538,7 @@ class Server {
} }
res.end(this._rangeRequestMiddleware(req, res, data, assetPath)); res.end(this._rangeRequestMiddleware(req, res, data, assetPath));
process.nextTick(() => { process.nextTick(() => {
print(log(createActionEndEntry(processingAssetRequestLogEntry)), ['asset']); log(createActionEndEntry(processingAssetRequestLogEntry));
}); });
}, },
error => { error => {
@ -565,10 +573,10 @@ class Server {
if (outdated.size) { if (outdated.size) {
const updatingExistingBundleLogEntry = const updatingExistingBundleLogEntry =
print(log(createActionStartEntry({ log(createActionStartEntry({
action_name: 'Updating existing bundle', action_name: 'Updating existing bundle',
outdated_modules: outdated.size, outdated_modules: outdated.size,
})), ['outdated_modules']); }));
debug('Attempt to update existing bundle'); debug('Attempt to update existing bundle');
@ -632,10 +640,7 @@ class Server {
bundle.invalidateSource(); bundle.invalidateSource();
print( log(createActionEndEntry(updatingExistingBundleLogEntry));
log(createActionEndEntry(updatingExistingBundleLogEntry)),
['outdated_modules'],
);
debug('Successfully updated existing bundle'); debug('Successfully updated existing bundle');
return bundle; return bundle;
@ -689,21 +694,32 @@ class Server {
} }
const options = this._getOptionsFromUrl(req.url); const options = this._getOptionsFromUrl(req.url);
this._reporter.update({
type: 'bundle_requested',
entryFilePath: options.entryFile,
});
const requestingBundleLogEntry = const requestingBundleLogEntry =
print(log(createActionStartEntry({ log(createActionStartEntry({
action_name: 'Requesting bundle', action_name: 'Requesting bundle',
bundle_url: req.url, bundle_url: req.url,
entry_point: options.entryFile, entry_point: options.entryFile,
})), ['bundle_url']); }));
let updateTTYProgressMessage = () => {}; let reportProgress = () => {};
if (process.stdout.isTTY && !this._opts.silent) { if (!this._opts.silent) {
updateTTYProgressMessage = startTTYProgressMessage(); reportProgress = (transformedFileCount, totalFileCount) => {
this._reporter.update({
type: 'bundle_transform_progressed',
entryFilePath: options.entryFile,
transformedFileCount,
totalFileCount,
});
};
} }
const mres = MultipartResponse.wrap(req, res); const mres = MultipartResponse.wrap(req, res);
options.onProgress = (done, total) => { options.onProgress = (done, total) => {
updateTTYProgressMessage(done, total); reportProgress(done, total);
mres.writeChunk({'Content-Type': 'application/json'}, JSON.stringify({done, total})); mres.writeChunk({'Content-Type': 'application/json'}, JSON.stringify({done, total}));
}; };
@ -711,6 +727,10 @@ class Server {
const building = this._useCachedOrUpdateOrCreateBundle(options); const building = this._useCachedOrUpdateOrCreateBundle(options);
building.then( building.then(
p => { p => {
this._reporter.update({
type: 'bundle_built',
entryFilePath: options.entryFile,
});
if (requestType === 'bundle') { if (requestType === 'bundle') {
debug('Generating source code'); debug('Generating source code');
const bundleSource = p.getSource({ const bundleSource = p.getSource({
@ -731,7 +751,7 @@ class Server {
mres.end(bundleSource); mres.end(bundleSource);
} }
debug('Finished response'); debug('Finished response');
print(log(createActionEndEntry(requestingBundleLogEntry)), ['bundle_url']); log(createActionEndEntry(requestingBundleLogEntry));
} else if (requestType === 'map') { } else if (requestType === 'map') {
let sourceMap = p.getSourceMap({ let sourceMap = p.getSourceMap({
minify: options.minify, minify: options.minify,
@ -744,12 +764,12 @@ class Server {
mres.setHeader('Content-Type', 'application/json'); mres.setHeader('Content-Type', 'application/json');
mres.end(sourceMap); mres.end(sourceMap);
print(log(createActionEndEntry(requestingBundleLogEntry)), ['bundle_url']); log(createActionEndEntry(requestingBundleLogEntry));
} else if (requestType === 'assets') { } else if (requestType === 'assets') {
const assetsList = JSON.stringify(p.getAssets()); const assetsList = JSON.stringify(p.getAssets());
mres.setHeader('Content-Type', 'application/json'); mres.setHeader('Content-Type', 'application/json');
mres.end(assetsList); mres.end(assetsList);
print(log(createActionEndEntry(requestingBundleLogEntry)), ['bundle_url']); log(createActionEndEntry(requestingBundleLogEntry));
} }
}, },
error => this._handleError(mres, this.optionsHash(options), error) error => this._handleError(mres, this.optionsHash(options), error)
@ -762,7 +782,7 @@ class Server {
_symbolicate(req: IncomingMessage, res: ServerResponse) { _symbolicate(req: IncomingMessage, res: ServerResponse) {
const symbolicatingLogEntry = const symbolicatingLogEntry =
print(log(createActionStartEntry('Symbolicating'))); log(createActionStartEntry('Symbolicating'));
/* $FlowFixMe: where is `rowBody` defined? Is it added by /* $FlowFixMe: where is `rowBody` defined? Is it added by
* the `connect` framework? */ * the `connect` framework? */
@ -815,7 +835,7 @@ class Server {
stack => { stack => {
res.end(JSON.stringify({stack: stack})); res.end(JSON.stringify({stack: stack}));
process.nextTick(() => { process.nextTick(() => {
print(log(createActionEndEntry(symbolicatingLogEntry))); log(createActionEndEntry(symbolicatingLogEntry));
}); });
}, },
error => { error => {
@ -950,41 +970,6 @@ class Server {
} }
} }
function getProgressBar(ratio: number, length: number) {
const blockCount = Math.floor(ratio * length);
return (
'\u2593'.repeat(blockCount) +
'\u2591'.repeat(length - blockCount)
);
}
/**
* 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.
*/
function startTTYProgressMessage(
): (doneCount: number, totalCount: number) => void {
let currentRatio = 0;
const updateMessage = (doneCount, totalCount) => {
const isDone = doneCount === totalCount;
const conservativeRatio = Math.pow(doneCount / totalCount, 2);
currentRatio = Math.max(conservativeRatio, currentRatio);
terminal.status(
'Transforming files %s%s% (%s/%s)%s',
isDone ? '' : getProgressBar(currentRatio, 20) + ' ',
(100 * currentRatio).toFixed(1),
doneCount,
totalCount,
isDone ? ', done.' : '...',
);
if (isDone) {
terminal.persistStatus();
}
};
return throttle(updateMessage, 200);
}
function contentsEqual(array: Array<mixed>, set: Set<mixed>): boolean { function contentsEqual(array: Array<mixed>, set: Set<mixed>): boolean {
return array.length === set.size && array.every(set.has, set); return array.length === set.size && array.every(set.has, set);
} }

View File

@ -0,0 +1,246 @@
/**
* 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 chalk = require('chalk');
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';
const PACKAGE_EMOJI = '\uD83D\uDCE6';
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 [
chalk.underline(`Bundling ${PACKAGE_EMOJI} \`${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;

View File

@ -0,0 +1,90 @@
/**
* 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 chalk = require('chalk');
const util = require('util');
import type {Terminal} from './terminal';
export type GlobalCacheDisabledReason = 'too_many_errors' | 'too_many_misses';
/**
* A tagged union of all the actions that may happen and we may want to
* report to the tool user.
*/
export type ReportableEvent = {
type: 'dep_graph_loading',
} | {
type: 'dep_graph_loaded',
} | {
type: 'bundle_requested',
entryFilePath: string,
} | {
type: 'bundle_transform_progressed',
entryFilePath: string,
transformedFileCount: number,
totalFileCount: number,
} | {
type: 'bundle_built',
entryFilePath: string,
} | {
type: 'global_cache_error',
error: Error,
} | {
type: 'global_cache_disabled',
reason: GlobalCacheDisabledReason,
};
/**
* Code across the application takes a reporter as an option and calls the
* update whenever one of the ReportableEvent happens. Code does not directly
* write to the standard output, because a build would be:
*
* 1. ad-hoc, embedded into another tool, in which case we do not want to
* pollute that tool's own output. The tool is free to present the
* warnings/progress we generate any way they want, by specifing a custom
* reporter.
* 2. run as a background process from another tool, in which case we want
* to expose updates in a way that is easily machine-readable, for example
* a JSON-stream. We don't want to pollute it with textual messages.
*
* We centralize terminal reporting into a single place because we want the
* output to be robust and consistent. The most common reporter is
* TerminalReporter, that should be the only place in the application should
* access the `terminal` module (nor the `console`).
*/
export type Reporter = {
update(event: ReportableEvent): void,
};
/**
* A standard way to log a warning to the terminal. This should not be called
* from some arbitrary packager logic, only from the reporters. Instead of
* calling this, add a new type of ReportableEvent instead, and implement a
* proper handler in the reporter(s).
*/
function logWarning(terminal: Terminal, format: string, ...args: Array<mixed>): void {
const str = util.format(format, ...args);
terminal.log('%s: %s', chalk.yellow('warning'), str);
}
/**
* A reporter that does nothing. Errors and warnings will be swallowed, that
* is generally not what you want.
*/
const nullReporter: Reporter = {update() {}};
module.exports = {
logWarning,
nullReporter,
};

View File

@ -126,9 +126,9 @@ class GlobalTerminal extends Terminal {
Terminal: Class<Terminal>; Terminal: Class<Terminal>;
constructor() { constructor() {
/* $FlowFixMe: Flow is wrong, Node.js docs specify that process.stderr is an /* $FlowFixMe: Flow is wrong, Node.js docs specify that process.stdout is an
* instance of a net.Socket (a local socket, not network). */ * instance of a net.Socket (a local socket, not network). */
super(process.stderr); super(process.stdout);
this.Terminal = Terminal; this.Terminal = Terminal;
} }

View File

@ -14,20 +14,19 @@
const GlobalTransformCache = require('../lib/GlobalTransformCache'); const GlobalTransformCache = require('../lib/GlobalTransformCache');
const TransformCache = require('../lib/TransformCache'); const TransformCache = require('../lib/TransformCache');
const chalk = require('chalk');
const crypto = require('crypto'); const crypto = require('crypto');
const docblock = require('./DependencyGraph/docblock'); const docblock = require('./DependencyGraph/docblock');
const fs = require('fs'); const fs = require('fs');
const invariant = require('invariant'); const invariant = require('invariant');
const isAbsolutePath = require('absolute-path'); const isAbsolutePath = require('absolute-path');
const jsonStableStringify = require('json-stable-stringify'); const jsonStableStringify = require('json-stable-stringify');
const terminal = require('../lib/terminal');
const {join: joinPath, relative: relativePath, extname} = require('path'); const {join: joinPath, relative: relativePath, extname} = require('path');
import type {TransformedCode, Options as TransformOptions} from '../JSTransformer/worker/worker'; import type {TransformedCode, Options as TransformOptions} from '../JSTransformer/worker/worker';
import type {SourceMap} from '../lib/SourceMap'; import type {SourceMap} from '../lib/SourceMap';
import type {ReadTransformProps} from '../lib/TransformCache'; import type {ReadTransformProps} from '../lib/TransformCache';
import type {Reporter} from '../lib/reporting';
import type Cache from './Cache'; import type Cache from './Cache';
import type DependencyGraphHelpers from './DependencyGraph/DependencyGraphHelpers'; import type DependencyGraphHelpers from './DependencyGraph/DependencyGraphHelpers';
import type ModuleCache from './ModuleCache'; import type ModuleCache from './ModuleCache';
@ -65,6 +64,7 @@ export type ConstructorArgs = {
transformCacheKey: ?string, transformCacheKey: ?string,
depGraphHelpers: DependencyGraphHelpers, depGraphHelpers: DependencyGraphHelpers,
options: Options, options: Options,
reporter: Reporter,
}; };
class Module { class Module {
@ -78,6 +78,7 @@ class Module {
_transformCacheKey: ?string; _transformCacheKey: ?string;
_depGraphHelpers: DependencyGraphHelpers; _depGraphHelpers: DependencyGraphHelpers;
_options: Options; _options: Options;
_reporter: Reporter;
_docBlock: Promise<{id?: string, moduleDocBlock: {[key: string]: mixed}}>; _docBlock: Promise<{id?: string, moduleDocBlock: {[key: string]: mixed}}>;
_readSourceCodePromise: Promise<string>; _readSourceCodePromise: Promise<string>;
@ -93,6 +94,7 @@ class Module {
transformCode, transformCode,
transformCacheKey, transformCacheKey,
depGraphHelpers, depGraphHelpers,
reporter,
options, options,
}: ConstructorArgs) { }: ConstructorArgs) {
if (!isAbsolutePath(file)) { if (!isAbsolutePath(file)) {
@ -112,6 +114,7 @@ class Module {
); );
this._depGraphHelpers = depGraphHelpers; this._depGraphHelpers = depGraphHelpers;
this._options = options || {}; this._options = options || {};
this._reporter = reporter;
this._readPromises = new Map(); this._readPromises = new Map();
} }
@ -276,25 +279,25 @@ class Module {
} }
globalCache.fetch(cacheProps, (globalCacheError, globalCachedResult) => { globalCache.fetch(cacheProps, (globalCacheError, globalCachedResult) => {
if (globalCacheError != null && Module._globalCacheRetries > 0) { if (globalCacheError != null && Module._globalCacheRetries > 0) {
terminal.log(chalk.red( this._reporter.update({
'Warning: the global cache failed with error:', type: 'global_cache_error',
)); error: globalCacheError,
terminal.log(chalk.red(globalCacheError.stack)); });
Module._globalCacheRetries--; Module._globalCacheRetries--;
if (Module._globalCacheRetries <= 0) { if (Module._globalCacheRetries <= 0) {
terminal.log(chalk.red( this._reporter.update({
'No more retries, the global cache will be disabled for the ' + type: 'global_cache_disabled',
'remainder of the transformation.', reason: 'too_many_errors',
)); });
} }
} }
if (globalCachedResult == null) { if (globalCachedResult == null) {
--Module._globalCacheMaxMisses; --Module._globalCacheMaxMisses;
if (Module._globalCacheMaxMisses === 0) { if (Module._globalCacheMaxMisses === 0) {
terminal.log( this._reporter.update({
'warning: global cache is now disabled because it ' + type: 'global_cache_disabled',
'has been missing too many consecutive keys.', reason: 'too_many_misses',
); });
} }
this._transformAndStoreCodeGlobally(cacheProps, globalCache, callback); this._transformAndStoreCodeGlobally(cacheProps, globalCache, callback);
return; return;

View File

@ -16,6 +16,7 @@ const Module = require('./Module');
const Package = require('./Package'); const Package = require('./Package');
const Polyfill = require('./Polyfill'); const Polyfill = require('./Polyfill');
import type {Reporter} from '../lib/reporting';
import type Cache from './Cache'; import type Cache from './Cache';
import type DependencyGraphHelpers from './DependencyGraph/DependencyGraphHelpers'; import type DependencyGraphHelpers from './DependencyGraph/DependencyGraphHelpers';
import type { import type {
@ -38,6 +39,7 @@ class ModuleCache {
_platforms: mixed; _platforms: mixed;
_transformCacheKey: string; _transformCacheKey: string;
_transformCode: TransformCode; _transformCode: TransformCode;
_reporter: Reporter;
constructor({ constructor({
assetDependencies, assetDependencies,
@ -48,6 +50,7 @@ class ModuleCache {
moduleOptions, moduleOptions,
transformCacheKey, transformCacheKey,
transformCode, transformCode,
reporter,
}: { }: {
assetDependencies: mixed, assetDependencies: mixed,
cache: Cache, cache: Cache,
@ -56,6 +59,7 @@ class ModuleCache {
moduleOptions: ModuleOptions, moduleOptions: ModuleOptions,
transformCacheKey: string, transformCacheKey: string,
transformCode: TransformCode, transformCode: TransformCode,
reporter: Reporter,
}, platforms: mixed) { }, platforms: mixed) {
this._assetDependencies = assetDependencies; this._assetDependencies = assetDependencies;
this._getClosestPackage = getClosestPackage; this._getClosestPackage = getClosestPackage;
@ -68,6 +72,7 @@ class ModuleCache {
this._platforms = platforms; this._platforms = platforms;
this._transformCacheKey = transformCacheKey; this._transformCacheKey = transformCacheKey;
this._transformCode = transformCode; this._transformCode = transformCode;
this._reporter = reporter;
} }
getModule(filePath: string) { getModule(filePath: string) {
@ -80,6 +85,7 @@ class ModuleCache {
transformCacheKey: this._transformCacheKey, transformCacheKey: this._transformCacheKey,
depGraphHelpers: this._depGraphHelpers, depGraphHelpers: this._depGraphHelpers,
options: this._moduleOptions, options: this._moduleOptions,
reporter: this._reporter,
}); });
} }
return this._moduleCache[filePath]; return this._moduleCache[filePath];

View File

@ -127,6 +127,7 @@ describe('DependencyGraph', function() {
}); });
}, },
transformCacheKey, transformCacheKey,
reporter: require('../../lib/reporting').nullReporter,
}; };
}); });

View File

@ -35,10 +35,10 @@ const {
createActionEndEntry, createActionEndEntry,
createActionStartEntry, createActionStartEntry,
log, log,
print,
} = require('../Logger'); } = require('../Logger');
import type {Options as TransformOptions} from '../JSTransformer/worker/worker'; import type {Options as TransformOptions} from '../JSTransformer/worker/worker';
import type {Reporter} from '../lib/reporting';
import type { import type {
Options as ModuleOptions, Options as ModuleOptions,
TransformCode, TransformCode,
@ -76,6 +76,7 @@ class DependencyGraph {
_hasteMapError: ?Error; _hasteMapError: ?Error;
_helpers: DependencyGraphHelpers; _helpers: DependencyGraphHelpers;
_moduleCache: ModuleCache; _moduleCache: ModuleCache;
_reporter: Reporter;
_loading: Promise<mixed>; _loading: Promise<mixed>;
@ -100,6 +101,7 @@ class DependencyGraph {
transformCode, transformCode,
useWatchman, useWatchman,
watch, watch,
reporter,
}: { }: {
assetDependencies: mixed, assetDependencies: mixed,
assetExts: Array<string>, assetExts: Array<string>,
@ -121,6 +123,7 @@ class DependencyGraph {
transformCode: TransformCode, transformCode: TransformCode,
useWatchman?: ?boolean, useWatchman?: ?boolean,
watch: boolean, watch: boolean,
reporter: Reporter,
}) { }) {
this._opts = { this._opts = {
assetExts: assetExts || [], assetExts: assetExts || [],
@ -145,6 +148,7 @@ class DependencyGraph {
watch: !!watch, watch: !!watch,
}; };
this._reporter = reporter;
this._cache = cache; this._cache = cache;
this._assetDependencies = assetDependencies; this._assetDependencies = assetDependencies;
this._helpers = new DependencyGraphHelpers(this._opts); this._helpers = new DependencyGraphHelpers(this._opts);
@ -174,7 +178,8 @@ class DependencyGraph {
}); });
const initializingPackagerLogEntry = const initializingPackagerLogEntry =
print(log(createActionStartEntry('Initializing Packager'))); log(createActionStartEntry('Initializing Packager'));
this._reporter.update({type: 'dep_graph_loading'});
this._loading = this._haste.build().then(({hasteFS}) => { this._loading = this._haste.build().then(({hasteFS}) => {
this._hasteFS = hasteFS; this._hasteFS = hasteFS;
const hasteFSFiles = hasteFS.getAllFiles(); const hasteFSFiles = hasteFS.getAllFiles();
@ -186,6 +191,7 @@ class DependencyGraph {
depGraphHelpers: this._helpers, depGraphHelpers: this._helpers,
assetDependencies: this._assetDependencies, assetDependencies: this._assetDependencies,
moduleOptions: this._opts.moduleOptions, moduleOptions: this._opts.moduleOptions,
reporter: this._reporter,
getClosestPackage: filePath => { getClosestPackage: filePath => {
let {dir, root} = path.parse(filePath); let {dir, root} = path.parse(filePath);
do { do {
@ -216,12 +222,13 @@ class DependencyGraph {
}); });
const buildingHasteMapLogEntry = const buildingHasteMapLogEntry =
print(log(createActionStartEntry('Building Haste Map'))); log(createActionStartEntry('Building Haste Map'));
return this._hasteMap.build().then( return this._hasteMap.build().then(
map => { map => {
print(log(createActionEndEntry(buildingHasteMapLogEntry))); log(createActionEndEntry(buildingHasteMapLogEntry));
print(log(createActionEndEntry(initializingPackagerLogEntry))); log(createActionEndEntry(initializingPackagerLogEntry));
this._reporter.update({type: 'dep_graph_loaded'});
return map; return map;
}, },
err => { err => {