packager: Terminal abstraction to manage TTYs
Reviewed By: cpojer Differential Revision: D4293146 fbshipit-source-id: 66e943b026197d293b5a518b4f97a0bced8d11bb
This commit is contained in:
parent
f8f70d2275
commit
26ed94c0fd
|
@ -167,7 +167,6 @@
|
|||
"opn": "^3.0.2",
|
||||
"optimist": "^0.6.1",
|
||||
"plist": "^1.2.0",
|
||||
"progress": "^1.1.8",
|
||||
"promise": "^7.1.1",
|
||||
"react-clone-referenced-element": "^1.0.1",
|
||||
"react-timer-mixin": "^0.13.2",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
const chalk = require('chalk');
|
||||
const os = require('os');
|
||||
const pkgjson = require('../../../package.json');
|
||||
const terminal = require('../lib/terminal');
|
||||
|
||||
const {EventEmitter} = require('events');
|
||||
|
||||
|
@ -134,7 +135,7 @@ function print(
|
|||
}
|
||||
|
||||
// eslint-disable-next-line no-console-disallow
|
||||
console.log(logEntryString);
|
||||
terminal.log(logEntryString);
|
||||
|
||||
return logEntry;
|
||||
}
|
||||
|
|
|
@ -15,13 +15,14 @@ const AssetServer = require('../AssetServer');
|
|||
const getPlatformExtension = require('../node-haste').getPlatformExtension;
|
||||
const Bundler = require('../Bundler');
|
||||
const MultipartResponse = require('./MultipartResponse');
|
||||
const ProgressBar = require('progress');
|
||||
const SourceMapConsumer = require('source-map').SourceMapConsumer;
|
||||
|
||||
const declareOpts = require('../lib/declareOpts');
|
||||
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');
|
||||
|
@ -454,7 +455,7 @@ class Server {
|
|||
e => {
|
||||
res.writeHead(500);
|
||||
res.end('Internal Error');
|
||||
console.log(e.stack); // eslint-disable-line no-console-disallow
|
||||
terminal.log(e.stack); // eslint-disable-line no-console-disallow
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
@ -697,13 +698,15 @@ class Server {
|
|||
|
||||
let consoleProgress = () => {};
|
||||
if (process.stdout.isTTY && !this._opts.silent) {
|
||||
const bar = new ProgressBar('transformed :current/:total (:percent)', {
|
||||
complete: '=',
|
||||
incomplete: ' ',
|
||||
width: 40,
|
||||
total: 1,
|
||||
});
|
||||
consoleProgress = debouncedTick(bar);
|
||||
const onProgress = (doneCount, totalCount) => {
|
||||
const format = 'transformed %s/%s (%s%)';
|
||||
const percent = Math.floor(100 * doneCount / totalCount);
|
||||
terminal.status(format, doneCount, totalCount, percent);
|
||||
if (doneCount === totalCount) {
|
||||
terminal.persistStatus();
|
||||
}
|
||||
};
|
||||
consoleProgress = throttle(onProgress, 200);
|
||||
}
|
||||
|
||||
const mres = MultipartResponse.wrap(req, res);
|
||||
|
@ -959,23 +962,4 @@ function contentsEqual(array: Array<mixed>, set: Set<mixed>): boolean {
|
|||
return array.length === set.size && array.every(set.has, set);
|
||||
}
|
||||
|
||||
function debouncedTick(progressBar) {
|
||||
let n = 0;
|
||||
let start, total;
|
||||
|
||||
return (_, t) => {
|
||||
total = t;
|
||||
n += 1;
|
||||
if (start) {
|
||||
if (progressBar.curr + n >= total || Date.now() - start > 200) {
|
||||
progressBar.total = total;
|
||||
progressBar.tick(n);
|
||||
start = n = 0;
|
||||
}
|
||||
} else {
|
||||
start = Date.now();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = Server;
|
||||
|
|
|
@ -22,6 +22,7 @@ const jsonStableStringify = require('json-stable-stringify');
|
|||
const mkdirp = require('mkdirp');
|
||||
const path = require('path');
|
||||
const rimraf = require('rimraf');
|
||||
const terminal = require('../lib/terminal');
|
||||
const toFixedHex = require('./toFixedHex');
|
||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||
|
||||
|
@ -189,18 +190,18 @@ const GARBAGE_COLLECTOR = new (class GarbageCollector {
|
|||
try {
|
||||
this._collectSync();
|
||||
} catch (error) {
|
||||
console.error(error.stack);
|
||||
console.error(
|
||||
terminal.log(error.stack);
|
||||
terminal.log(
|
||||
'Error: Cleaning up the cache folder failed. Continuing anyway.',
|
||||
);
|
||||
console.error('The cache folder is: %s', getCacheDirPath());
|
||||
terminal.log('The cache folder is: %s', getCacheDirPath());
|
||||
}
|
||||
this._lastCollected = Date.now();
|
||||
}
|
||||
|
||||
_resetCache() {
|
||||
rimraf.sync(getCacheDirPath());
|
||||
console.log('Warning: The transform cache was reset.');
|
||||
terminal.log('Warning: The transform cache was reset.');
|
||||
this._cacheWasReset = true;
|
||||
this._lastCollected = Date.now();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
jest.dontMock('../terminal');
|
||||
|
||||
jest.mock('readline', () => ({
|
||||
moveCursor: (stream, dx, dy) => {
|
||||
const {cursor, columns} = stream;
|
||||
stream.cursor = Math.max(cursor - cursor % columns, cursor + dx) + dy * columns;
|
||||
},
|
||||
clearLine: (stream, dir) => {
|
||||
if (dir !== 0) {throw new Error('unsupported');}
|
||||
const {cursor, columns} = stream;
|
||||
const curLine = cursor - cursor % columns;
|
||||
const nextLine = curLine + columns;
|
||||
for (var i = curLine; i < nextLine; ++i) {
|
||||
stream.buffer[i] = ' ';
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('terminal', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
function prepare(isTTY) {
|
||||
const {Terminal} = require('../terminal');
|
||||
const lines = 10;
|
||||
const columns = 10;
|
||||
const stream = Object.create(
|
||||
isTTY ? require('tty').WriteStream.prototype : require('net').Socket,
|
||||
);
|
||||
Object.assign(stream, {
|
||||
cursor: 0,
|
||||
buffer: ' '.repeat(columns * lines).split(''),
|
||||
columns,
|
||||
lines,
|
||||
write(str) {
|
||||
for (let i = 0; i < str.length; ++i) {
|
||||
if (str[i] === '\n') {
|
||||
this.cursor = this.cursor - (this.cursor % columns) + columns;
|
||||
} else {
|
||||
this.buffer[this.cursor] = str[i];
|
||||
++this.cursor;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return {stream, terminal: new Terminal(stream)};
|
||||
}
|
||||
|
||||
it('is not printing status to non-interactive terminal', () => {
|
||||
const {stream, terminal} = prepare(false);
|
||||
terminal.log('foo %s', 'smth');
|
||||
terminal.status('status');
|
||||
terminal.log('bar');
|
||||
expect(stream.buffer.join('').trim())
|
||||
.toEqual('foo smth bar');
|
||||
});
|
||||
|
||||
it('updates status when logging, single line', () => {
|
||||
const {stream, terminal} = prepare(true);
|
||||
terminal.log('foo');
|
||||
terminal.status('status');
|
||||
terminal.status('status2');
|
||||
terminal.log('bar');
|
||||
expect(stream.buffer.join('').trim())
|
||||
.toEqual('foo bar status2');
|
||||
});
|
||||
|
||||
it('updates status when logging, multi-line', () => {
|
||||
const {stream, terminal} = prepare(true);
|
||||
terminal.log('foo');
|
||||
terminal.status('status\nanother');
|
||||
terminal.log('bar');
|
||||
expect(stream.buffer.join('').trim())
|
||||
.toEqual('foo bar status another');
|
||||
});
|
||||
|
||||
it('persists status', () => {
|
||||
const {stream, terminal} = prepare(true);
|
||||
terminal.log('foo');
|
||||
terminal.status('status');
|
||||
terminal.persistStatus();
|
||||
terminal.log('bar');
|
||||
expect(stream.buffer.join('').trim())
|
||||
.toEqual('foo status bar');
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,137 @@
|
|||
/**
|
||||
* 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 readline = require('readline');
|
||||
const tty = require('tty');
|
||||
const util = require('util');
|
||||
|
||||
/**
|
||||
* Clear some text that was previously printed on an interactive stream,
|
||||
* without trailing newline character (so we have to move back to the
|
||||
* beginning of the line).
|
||||
*/
|
||||
function clearStringBackwards(stream: tty.WriteStream, str: string): void {
|
||||
readline.moveCursor(stream, -stream.columns, 0);
|
||||
readline.clearLine(stream, 0);
|
||||
let lineCount = (str.match(/\n/g) || []).length;
|
||||
while (lineCount > 0) {
|
||||
readline.moveCursor(stream, 0, -1);
|
||||
readline.clearLine(stream, 0);
|
||||
--lineCount;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't just print things to the console, sometimes we also want to show
|
||||
* and update progress. This utility just ensures the output stays neat: no
|
||||
* missing newlines, no mangled log lines.
|
||||
*
|
||||
* const terminal = Terminal.default;
|
||||
* terminal.status('Updating... 38%');
|
||||
* terminal.log('warning: Something happened.');
|
||||
* terminal.status('Updating, done.');
|
||||
* terminal.persistStatus();
|
||||
*
|
||||
* The final output:
|
||||
*
|
||||
* warning: Something happened.
|
||||
* Updating, done.
|
||||
*
|
||||
* Without the status feature, we may get a mangled output:
|
||||
*
|
||||
* Updating... 38%warning: Something happened.
|
||||
* Updating, done.
|
||||
*
|
||||
* This is meant to be user-readable and TTY-oriented. We use stdout by default
|
||||
* because it's more about status information than diagnostics/errors (stderr).
|
||||
*
|
||||
* Do not add any higher-level functionality in this class such as "warning" and
|
||||
* "error" printers, as it is not meant for formatting/reporting. It has the
|
||||
* single responsibility of handling status messages.
|
||||
*/
|
||||
class Terminal {
|
||||
|
||||
_statusStr: string;
|
||||
_stream: net$Socket;
|
||||
|
||||
constructor(stream: net$Socket) {
|
||||
this._stream = stream;
|
||||
this._statusStr = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as status() without the formatting capabilities. We just clear and
|
||||
* rewrite with the new status. If the stream is non-interactive we still
|
||||
* keep track of the string so that `persistStatus` works.
|
||||
*/
|
||||
_setStatus(str: string): string {
|
||||
const {_statusStr, _stream} = this;
|
||||
if (_statusStr !== str && _stream instanceof tty.WriteStream) {
|
||||
clearStringBackwards(_stream, _statusStr);
|
||||
_stream.write(str);
|
||||
}
|
||||
this._statusStr = str;
|
||||
return _statusStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows some text that is meant to be overriden later. Return the previous
|
||||
* status that was shown and is no more. Calling `status()` with no argument
|
||||
* removes the status altogether. The status is never shown in a
|
||||
* non-interactive terminal: for example, if the output is redirected to a
|
||||
* file, then we don't care too much about having a progress bar.
|
||||
*/
|
||||
status(format: string, ...args: Array<mixed>): string {
|
||||
return this._setStatus(util.format(format, ...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to `console.log`, except it moves the status/progress text out of
|
||||
* the way correctly. In non-interactive terminals this is the same as
|
||||
* `console.log`.
|
||||
*/
|
||||
log(format: string, ...args: Array<mixed>): void {
|
||||
const oldStatus = this._setStatus('');
|
||||
this._stream.write(util.format(format, ...args) + '\n');
|
||||
this._setStatus(oldStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the current status and start from scratch. This is useful if the last
|
||||
* status was the last one of a series of updates.
|
||||
*/
|
||||
persistStatus(): void {
|
||||
return this.log(this.status(''));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* On the same pattern as node.js `console` module, we export the stdout-based
|
||||
* terminal at the top-level, but provide access to the Terminal class as a
|
||||
* field (so it can be used, for instance, with stderr).
|
||||
*/
|
||||
class GlobalTerminal extends Terminal {
|
||||
|
||||
Terminal: Class<Terminal>;
|
||||
|
||||
constructor() {
|
||||
/* $FlowFixMe: Flow is wrong, Node.js docs specify that process.stderr is an
|
||||
* instance of a net.Socket (a local socket, not network). */
|
||||
super(process.stderr);
|
||||
this.Terminal = Terminal;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = new GlobalTerminal();
|
|
@ -21,6 +21,7 @@ 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');
|
||||
|
||||
|
@ -266,13 +267,13 @@ class Module {
|
|||
}
|
||||
globalCache.fetch(cacheProps, (globalCacheError, globalCachedResult) => {
|
||||
if (globalCacheError != null && Module._globalCacheRetries > 0) {
|
||||
console.log(chalk.red(
|
||||
'\nWarning: the global cache failed with error:',
|
||||
terminal.log(chalk.red(
|
||||
'Warning: the global cache failed with error:',
|
||||
));
|
||||
console.log(chalk.red(globalCacheError.stack));
|
||||
terminal.log(chalk.red(globalCacheError.stack));
|
||||
Module._globalCacheRetries--;
|
||||
if (Module._globalCacheRetries <= 0) {
|
||||
console.log(chalk.red(
|
||||
terminal.log(chalk.red(
|
||||
'No more retries, the global cache will be disabled for the ' +
|
||||
'remainder of the transformation.',
|
||||
));
|
||||
|
|
Loading…
Reference in New Issue