diff --git a/local-cli/bundle/buildBundle.js b/local-cli/bundle/buildBundle.js index a2ed17929..60a88411a 100644 --- a/local-cli/bundle/buildBundle.js +++ b/local-cli/bundle/buildBundle.js @@ -10,6 +10,7 @@ const log = require('../util/log').out('bundle'); const Server = require('../../packager/react-packager/src/Server'); +const TerminalReporter = require('../../packager/react-packager/src/lib/TerminalReporter'); const outputBundle = require('./output/bundle'); const path = require('path'); @@ -57,6 +58,7 @@ function buildBundle(args, config, output = outputBundle, packagerInstance) { extraNodeModules: config.extraNodeModules, resetCache: args.resetCache, watch: false, + reporter: new TerminalReporter(), }; packagerInstance = new Server(options); diff --git a/local-cli/server/util/attachHMRServer.js b/local-cli/server/util/attachHMRServer.js index c4cb29e4c..229d91e31 100644 --- a/local-cli/server/util/attachHMRServer.js +++ b/local-cli/server/util/attachHMRServer.js @@ -11,7 +11,6 @@ const querystring = require('querystring'); const url = require('url'); -const {createEntry, print} = require('../../../packager/react-packager/src/Logger'); const {getInverseDependencies} = require('../../../packager/react-packager/src/node-haste'); const blacklist = [ @@ -114,9 +113,7 @@ function attachHMRServer({httpServer, path, packagerServer}) { path: path, }); - print(createEntry(`HMR Server listening on ${path}`)); wss.on('connection', ws => { - print(createEntry('HMR Client connected')); const params = querystring.parse(url.parse(ws.upgradeReq.url).query); getDependencies(params.platform, params.bundleEntry) @@ -140,7 +137,6 @@ function attachHMRServer({httpServer, path, packagerServer}) { if (!client) { return; } - print(createEntry('HMR Server detected file change')); const blacklisted = blacklist.find(blacklistedPath => filename.indexOf(blacklistedPath) !== -1 @@ -297,7 +293,6 @@ function attachHMRServer({httpServer, path, packagerServer}) { return; } - print(createEntry('HMR Server sending update to client')); client.ws.send(update); }); diff --git a/packager/react-packager/index.js b/packager/react-packager/index.js index 172018df2..056508dbe 100644 --- a/packager/react-packager/index.js +++ b/packager/react-packager/index.js @@ -57,12 +57,22 @@ function createServer(options) { options = Object.assign({}, options); 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'); return new Server(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; return createServer(options); } diff --git a/packager/react-packager/src/Bundler/index.js b/packager/react-packager/src/Bundler/index.js index e8499574c..a9e8c9275 100644 --- a/packager/react-packager/src/Bundler/index.js +++ b/packager/react-packager/src/Bundler/index.js @@ -39,6 +39,7 @@ import type AssetServer from '../AssetServer'; import type Module from '../node-haste/Module'; import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse'; import type {Options as TransformOptions} from '../JSTransformer/worker/worker'; +import type {Reporter} from '../lib/reporting'; export type GetTransformOptions = ( string, @@ -54,7 +55,6 @@ const { createActionStartEntry, createActionEndEntry, log, - print, } = require('../Logger'); const validateOpts = declareOpts({ @@ -113,6 +113,9 @@ const validateOpts = declareOpts({ type: 'boolean', default: false, }, + reporter: { + type: 'object', + }, }); const assetPropertyBlacklist = new Set([ @@ -122,21 +125,22 @@ const assetPropertyBlacklist = new Set([ ]); type Options = { - projectRoots: Array, + allowBundleUpdates: boolean, + assetExts: Array, + assetServer: AssetServer, blacklistRE: RegExp, - moduleFormat: string, - polyfillModuleNames: Array, cacheVersion: string, + extraNodeModules: {}, + getTransformOptions?: GetTransformOptions<*>, + moduleFormat: string, + platforms: Array, + polyfillModuleNames: Array, + projectRoots: Array, + reporter: Reporter, resetCache: boolean, transformModulePath: string, - getTransformOptions?: GetTransformOptions<*>, - extraNodeModules: {}, - assetExts: Array, - platforms: Array, - watch: boolean, - assetServer: AssetServer, transformTimeoutInterval: ?number, - allowBundleUpdates: boolean, + watch: boolean, }; class Bundler { @@ -204,20 +208,21 @@ class Bundler { blacklistRE: opts.blacklistRE, cache: this._cache, extraNodeModules: opts.extraNodeModules, - watch: opts.watch, minifyCode: this._transformer.minify, moduleFormat: opts.moduleFormat, platforms: opts.platforms, polyfillModuleNames: opts.polyfillModuleNames, projectRoots: opts.projectRoots, + reporter: options.reporter, resetCache: opts.resetCache, + transformCacheKey, transformCode: (module, code, transformCodeOptions) => this._transformer.transformFile( module.path, code, transformCodeOptions, ), - transformCacheKey, + watch: opts.watch, }); this._projectRoots = opts.projectRoots; @@ -401,11 +406,11 @@ class Bundler { onProgress = noop, }: *) { const transformingFilesLogEntry = - print(log(createActionStartEntry({ + log(createActionStartEntry({ action_name: 'Transforming files', entry_point: entryFile, environment: dev ? 'dev' : 'prod', - }))); + })); const modulesByName = Object.create(null); @@ -425,7 +430,7 @@ class Bundler { return Promise.resolve(resolutionResponse).then(response => { bundle.setRamGroups(response.transformOptions.transform.ramGroups); - print(log(createActionEndEntry(transformingFilesLogEntry))); + log(createActionEndEntry(transformingFilesLogEntry)); onResolutionResponse(response); // get entry file complete path (`entryFile` is relative to roots) diff --git a/packager/react-packager/src/Logger/__tests__/Logger-test.js b/packager/react-packager/src/Logger/__tests__/Logger-test.js index b4b067b15..96d10c3cf 100644 --- a/packager/react-packager/src/Logger/__tests__/Logger-test.js +++ b/packager/react-packager/src/Logger/__tests__/Logger-test.js @@ -17,7 +17,6 @@ const { createEntry, createActionStartEntry, createActionEndEntry, - enablePrinting, } = require('../'); describe('Logger', () => { @@ -29,7 +28,6 @@ describe('Logger', () => { afterEach(() => { console.log = originalConsoleLog; - enablePrinting(); }); it('creates simple log entries', () => { diff --git a/packager/react-packager/src/Logger/index.js b/packager/react-packager/src/Logger/index.js index e93f15571..cb7ea774f 100644 --- a/packager/react-packager/src/Logger/index.js +++ b/packager/react-packager/src/Logger/index.js @@ -7,14 +7,12 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @flow - * */ + 'use strict'; -const chalk = require('chalk'); const os = require('os'); const pkgjson = require('../../../package.json'); -const terminal = require('../lib/terminal'); const {EventEmitter} = require('events'); @@ -24,17 +22,6 @@ import type { LogEntry, } 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 eventEmitter = new EventEmitter(); @@ -93,68 +80,10 @@ function log(logEntry: LogEntry): LogEntry { return logEntry; } -function print( - logEntry: LogEntry, - printFields?: Array = [], -): 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}] ${logEntryLabel}`); - break; - case 'end': - logEntryString = chalk.dim(`[${timeStamp}] ${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 = { on, createEntry, createActionStartEntry, createActionEndEntry, log, - print, - enablePrinting, - disablePrinting, }; diff --git a/packager/react-packager/src/Resolver/index.js b/packager/react-packager/src/Resolver/index.js index 2300ee989..0283498da 100644 --- a/packager/react-packager/src/Resolver/index.js +++ b/packager/react-packager/src/Resolver/index.js @@ -21,6 +21,7 @@ import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionRes import type Module from '../node-haste/Module'; import type {SourceMap} from '../lib/SourceMap'; import type {Options as TransformOptions} from '../JSTransformer/worker/worker'; +import type {Reporter} from '../lib/reporting'; const validateOpts = declareOpts({ projectRoots: { @@ -71,6 +72,9 @@ const validateOpts = declareOpts({ type: 'boolean', default: false, }, + reporter: { + type: 'object', + }, }); const getDependenciesValidateOpts = declareOpts({ @@ -99,30 +103,34 @@ class Resolver { Promise<{code: string, map: SourceMap}>; _polyfillModuleNames: Array; - constructor(options: {resetCache: boolean}) { + constructor(options: { + reporter: Reporter, + resetCache: boolean, + }) { const opts = validateOpts(options); this._depGraph = new DependencyGraph({ - roots: opts.projectRoots, + assetDependencies: ['react-native/Libraries/Image/AssetRegistry'], assetExts: opts.assetExts, + cache: opts.cache, + extraNodeModules: opts.extraNodeModules, ignoreFilePath: function(filepath) { return filepath.indexOf('__tests__') !== -1 || (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: { cacheTransformResults: true, 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; diff --git a/packager/react-packager/src/Server/__tests__/Server-test.js b/packager/react-packager/src/Server/__tests__/Server-test.js index a81f56b3f..7da51533b 100644 --- a/packager/react-packager/src/Server/__tests__/Server-test.js +++ b/packager/react-packager/src/Server/__tests__/Server-test.js @@ -39,7 +39,8 @@ describe('processRequest', () => { projectRoots: ['root'], blacklistRE: null, cacheVersion: null, - polyfillModuleNames: null + polyfillModuleNames: null, + reporter: require('../../lib/reporting').nullReporter, }; const makeRequest = (reqHandler, requrl, reqOptions) => new Promise(resolve => diff --git a/packager/react-packager/src/Server/index.js b/packager/react-packager/src/Server/index.js index 956986d22..4e27d9fc4 100644 --- a/packager/react-packager/src/Server/index.js +++ b/packager/react-packager/src/Server/index.js @@ -22,7 +22,6 @@ const defaults = require('../../../defaults'); const mime = require('mime-types'); const path = require('path'); const terminal = require('../lib/terminal'); -const throttle = require('lodash/throttle'); const url = require('url'); const debug = require('debug')('ReactNativePackager:Server'); @@ -32,12 +31,12 @@ import type {Stats} from 'fs'; import type {IncomingMessage, ServerResponse} from 'http'; import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse'; import type Bundle from '../Bundler/Bundle'; +import type {Reporter} from '../lib/reporting'; const { createActionStartEntry, createActionEndEntry, log, - print, } = require('../Logger'); function debounceAndBatch(fn, delay) { @@ -109,6 +108,9 @@ const validateOpts = declareOpts({ type: 'boolean', default: false, }, + reporter: { + type: 'object', + }, }); const bundleOpts = declareOpts({ @@ -223,12 +225,17 @@ class Server { _bundler: Bundler; _debouncedFileChangeHandler: (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 processFileChange = ({type, filePath, stat}) => this.onFileChange(type, filePath, stat); + this._reporter = options.reporter; this._projectRoots = opts.projectRoots; this._bundles = Object.create(null); this._changeWatchers = []; @@ -243,6 +250,7 @@ class Server { bundlerOpts.assetServer = this._assetServer; bundlerOpts.allowBundleUpdates = options.watch; bundlerOpts.watch = options.watch; + bundlerOpts.reporter = options.reporter; this._bundler = new Bundler(bundlerOpts); // 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 processingAssetRequestLogEntry = - print(log(createActionStartEntry({ + log(createActionStartEntry({ action_name: 'Processing asset request', asset: assetPath[1], - })), ['asset']); + })); /* $FlowFixMe: query may be empty for invalid URLs */ this._assetServer.get(assetPath[1], urlObj.query.platform) @@ -530,7 +538,7 @@ class Server { } res.end(this._rangeRequestMiddleware(req, res, data, assetPath)); process.nextTick(() => { - print(log(createActionEndEntry(processingAssetRequestLogEntry)), ['asset']); + log(createActionEndEntry(processingAssetRequestLogEntry)); }); }, error => { @@ -565,10 +573,10 @@ class Server { if (outdated.size) { const updatingExistingBundleLogEntry = - print(log(createActionStartEntry({ + log(createActionStartEntry({ action_name: 'Updating existing bundle', outdated_modules: outdated.size, - })), ['outdated_modules']); + })); debug('Attempt to update existing bundle'); @@ -632,10 +640,7 @@ class Server { bundle.invalidateSource(); - print( - log(createActionEndEntry(updatingExistingBundleLogEntry)), - ['outdated_modules'], - ); + log(createActionEndEntry(updatingExistingBundleLogEntry)); debug('Successfully updated existing bundle'); return bundle; @@ -689,21 +694,32 @@ class Server { } const options = this._getOptionsFromUrl(req.url); + this._reporter.update({ + type: 'bundle_requested', + entryFilePath: options.entryFile, + }); const requestingBundleLogEntry = - print(log(createActionStartEntry({ + log(createActionStartEntry({ action_name: 'Requesting bundle', bundle_url: req.url, entry_point: options.entryFile, - })), ['bundle_url']); + })); - let updateTTYProgressMessage = () => {}; - if (process.stdout.isTTY && !this._opts.silent) { - updateTTYProgressMessage = startTTYProgressMessage(); + let reportProgress = () => {}; + if (!this._opts.silent) { + reportProgress = (transformedFileCount, totalFileCount) => { + this._reporter.update({ + type: 'bundle_transform_progressed', + entryFilePath: options.entryFile, + transformedFileCount, + totalFileCount, + }); + }; } const mres = MultipartResponse.wrap(req, res); options.onProgress = (done, total) => { - updateTTYProgressMessage(done, total); + reportProgress(done, total); mres.writeChunk({'Content-Type': 'application/json'}, JSON.stringify({done, total})); }; @@ -711,6 +727,10 @@ class Server { const building = this._useCachedOrUpdateOrCreateBundle(options); building.then( p => { + this._reporter.update({ + type: 'bundle_built', + entryFilePath: options.entryFile, + }); if (requestType === 'bundle') { debug('Generating source code'); const bundleSource = p.getSource({ @@ -731,7 +751,7 @@ class Server { mres.end(bundleSource); } debug('Finished response'); - print(log(createActionEndEntry(requestingBundleLogEntry)), ['bundle_url']); + log(createActionEndEntry(requestingBundleLogEntry)); } else if (requestType === 'map') { let sourceMap = p.getSourceMap({ minify: options.minify, @@ -744,12 +764,12 @@ class Server { mres.setHeader('Content-Type', 'application/json'); mres.end(sourceMap); - print(log(createActionEndEntry(requestingBundleLogEntry)), ['bundle_url']); + log(createActionEndEntry(requestingBundleLogEntry)); } else if (requestType === 'assets') { const assetsList = JSON.stringify(p.getAssets()); mres.setHeader('Content-Type', 'application/json'); mres.end(assetsList); - print(log(createActionEndEntry(requestingBundleLogEntry)), ['bundle_url']); + log(createActionEndEntry(requestingBundleLogEntry)); } }, error => this._handleError(mres, this.optionsHash(options), error) @@ -762,7 +782,7 @@ class Server { _symbolicate(req: IncomingMessage, res: ServerResponse) { const symbolicatingLogEntry = - print(log(createActionStartEntry('Symbolicating'))); + log(createActionStartEntry('Symbolicating')); /* $FlowFixMe: where is `rowBody` defined? Is it added by * the `connect` framework? */ @@ -815,7 +835,7 @@ class Server { stack => { res.end(JSON.stringify({stack: stack})); process.nextTick(() => { - print(log(createActionEndEntry(symbolicatingLogEntry))); + log(createActionEndEntry(symbolicatingLogEntry)); }); }, 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, set: Set): boolean { return array.length === set.size && array.every(set.has, set); } diff --git a/packager/react-packager/src/lib/TerminalReporter.js b/packager/react-packager/src/lib/TerminalReporter.js new file mode 100644 index 000000000..a720729a1 --- /dev/null +++ b/packager/react-packager/src/lib/TerminalReporter.js @@ -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; + + _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; diff --git a/packager/react-packager/src/lib/reporting.js b/packager/react-packager/src/lib/reporting.js new file mode 100644 index 000000000..62152febb --- /dev/null +++ b/packager/react-packager/src/lib/reporting.js @@ -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): 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, +}; diff --git a/packager/react-packager/src/lib/terminal.js b/packager/react-packager/src/lib/terminal.js index 8ab19ef6e..facc56f28 100644 --- a/packager/react-packager/src/lib/terminal.js +++ b/packager/react-packager/src/lib/terminal.js @@ -126,9 +126,9 @@ class GlobalTerminal extends Terminal { Terminal: Class; 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). */ - super(process.stderr); + super(process.stdout); this.Terminal = Terminal; } diff --git a/packager/react-packager/src/node-haste/Module.js b/packager/react-packager/src/node-haste/Module.js index 2f914585a..729c9b2d7 100644 --- a/packager/react-packager/src/node-haste/Module.js +++ b/packager/react-packager/src/node-haste/Module.js @@ -14,20 +14,19 @@ const GlobalTransformCache = require('../lib/GlobalTransformCache'); const TransformCache = require('../lib/TransformCache'); -const chalk = require('chalk'); const crypto = require('crypto'); const docblock = require('./DependencyGraph/docblock'); const fs = require('fs'); const invariant = require('invariant'); const isAbsolutePath = require('absolute-path'); const jsonStableStringify = require('json-stable-stringify'); -const terminal = require('../lib/terminal'); const {join: joinPath, relative: relativePath, extname} = require('path'); import type {TransformedCode, Options as TransformOptions} from '../JSTransformer/worker/worker'; import type {SourceMap} from '../lib/SourceMap'; import type {ReadTransformProps} from '../lib/TransformCache'; +import type {Reporter} from '../lib/reporting'; import type Cache from './Cache'; import type DependencyGraphHelpers from './DependencyGraph/DependencyGraphHelpers'; import type ModuleCache from './ModuleCache'; @@ -65,6 +64,7 @@ export type ConstructorArgs = { transformCacheKey: ?string, depGraphHelpers: DependencyGraphHelpers, options: Options, + reporter: Reporter, }; class Module { @@ -78,6 +78,7 @@ class Module { _transformCacheKey: ?string; _depGraphHelpers: DependencyGraphHelpers; _options: Options; + _reporter: Reporter; _docBlock: Promise<{id?: string, moduleDocBlock: {[key: string]: mixed}}>; _readSourceCodePromise: Promise; @@ -93,6 +94,7 @@ class Module { transformCode, transformCacheKey, depGraphHelpers, + reporter, options, }: ConstructorArgs) { if (!isAbsolutePath(file)) { @@ -112,6 +114,7 @@ class Module { ); this._depGraphHelpers = depGraphHelpers; this._options = options || {}; + this._reporter = reporter; this._readPromises = new Map(); } @@ -276,25 +279,25 @@ class Module { } globalCache.fetch(cacheProps, (globalCacheError, globalCachedResult) => { if (globalCacheError != null && Module._globalCacheRetries > 0) { - terminal.log(chalk.red( - 'Warning: the global cache failed with error:', - )); - terminal.log(chalk.red(globalCacheError.stack)); + this._reporter.update({ + type: 'global_cache_error', + error: globalCacheError, + }); Module._globalCacheRetries--; if (Module._globalCacheRetries <= 0) { - terminal.log(chalk.red( - 'No more retries, the global cache will be disabled for the ' + - 'remainder of the transformation.', - )); + this._reporter.update({ + type: 'global_cache_disabled', + reason: 'too_many_errors', + }); } } if (globalCachedResult == null) { --Module._globalCacheMaxMisses; if (Module._globalCacheMaxMisses === 0) { - terminal.log( - 'warning: global cache is now disabled because it ' + - 'has been missing too many consecutive keys.', - ); + this._reporter.update({ + type: 'global_cache_disabled', + reason: 'too_many_misses', + }); } this._transformAndStoreCodeGlobally(cacheProps, globalCache, callback); return; diff --git a/packager/react-packager/src/node-haste/ModuleCache.js b/packager/react-packager/src/node-haste/ModuleCache.js index 141024dd0..94358a3c4 100644 --- a/packager/react-packager/src/node-haste/ModuleCache.js +++ b/packager/react-packager/src/node-haste/ModuleCache.js @@ -16,6 +16,7 @@ const Module = require('./Module'); const Package = require('./Package'); const Polyfill = require('./Polyfill'); +import type {Reporter} from '../lib/reporting'; import type Cache from './Cache'; import type DependencyGraphHelpers from './DependencyGraph/DependencyGraphHelpers'; import type { @@ -38,6 +39,7 @@ class ModuleCache { _platforms: mixed; _transformCacheKey: string; _transformCode: TransformCode; + _reporter: Reporter; constructor({ assetDependencies, @@ -48,6 +50,7 @@ class ModuleCache { moduleOptions, transformCacheKey, transformCode, + reporter, }: { assetDependencies: mixed, cache: Cache, @@ -56,6 +59,7 @@ class ModuleCache { moduleOptions: ModuleOptions, transformCacheKey: string, transformCode: TransformCode, + reporter: Reporter, }, platforms: mixed) { this._assetDependencies = assetDependencies; this._getClosestPackage = getClosestPackage; @@ -68,6 +72,7 @@ class ModuleCache { this._platforms = platforms; this._transformCacheKey = transformCacheKey; this._transformCode = transformCode; + this._reporter = reporter; } getModule(filePath: string) { @@ -80,6 +85,7 @@ class ModuleCache { transformCacheKey: this._transformCacheKey, depGraphHelpers: this._depGraphHelpers, options: this._moduleOptions, + reporter: this._reporter, }); } return this._moduleCache[filePath]; diff --git a/packager/react-packager/src/node-haste/__tests__/DependencyGraph-test.js b/packager/react-packager/src/node-haste/__tests__/DependencyGraph-test.js index da91d0925..f19830316 100644 --- a/packager/react-packager/src/node-haste/__tests__/DependencyGraph-test.js +++ b/packager/react-packager/src/node-haste/__tests__/DependencyGraph-test.js @@ -127,6 +127,7 @@ describe('DependencyGraph', function() { }); }, transformCacheKey, + reporter: require('../../lib/reporting').nullReporter, }; }); diff --git a/packager/react-packager/src/node-haste/index.js b/packager/react-packager/src/node-haste/index.js index a8b64d103..be128dee3 100644 --- a/packager/react-packager/src/node-haste/index.js +++ b/packager/react-packager/src/node-haste/index.js @@ -35,10 +35,10 @@ const { createActionEndEntry, createActionStartEntry, log, - print, } = require('../Logger'); import type {Options as TransformOptions} from '../JSTransformer/worker/worker'; +import type {Reporter} from '../lib/reporting'; import type { Options as ModuleOptions, TransformCode, @@ -76,6 +76,7 @@ class DependencyGraph { _hasteMapError: ?Error; _helpers: DependencyGraphHelpers; _moduleCache: ModuleCache; + _reporter: Reporter; _loading: Promise; @@ -100,6 +101,7 @@ class DependencyGraph { transformCode, useWatchman, watch, + reporter, }: { assetDependencies: mixed, assetExts: Array, @@ -121,6 +123,7 @@ class DependencyGraph { transformCode: TransformCode, useWatchman?: ?boolean, watch: boolean, + reporter: Reporter, }) { this._opts = { assetExts: assetExts || [], @@ -145,6 +148,7 @@ class DependencyGraph { watch: !!watch, }; + this._reporter = reporter; this._cache = cache; this._assetDependencies = assetDependencies; this._helpers = new DependencyGraphHelpers(this._opts); @@ -174,7 +178,8 @@ class DependencyGraph { }); 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._hasteFS = hasteFS; const hasteFSFiles = hasteFS.getAllFiles(); @@ -186,6 +191,7 @@ class DependencyGraph { depGraphHelpers: this._helpers, assetDependencies: this._assetDependencies, moduleOptions: this._opts.moduleOptions, + reporter: this._reporter, getClosestPackage: filePath => { let {dir, root} = path.parse(filePath); do { @@ -216,12 +222,13 @@ class DependencyGraph { }); const buildingHasteMapLogEntry = - print(log(createActionStartEntry('Building Haste Map'))); + log(createActionStartEntry('Building Haste Map')); return this._hasteMap.build().then( map => { - print(log(createActionEndEntry(buildingHasteMapLogEntry))); - print(log(createActionEndEntry(initializingPackagerLogEntry))); + log(createActionEndEntry(buildingHasteMapLogEntry)); + log(createActionEndEntry(initializingPackagerLogEntry)); + this._reporter.update({type: 'dep_graph_loaded'}); return map; }, err => {