mirror of https://github.com/status-im/metro.git
packager: Terminal abstraction to manage TTYs
Reviewed By: cpojer Differential Revision: D4293146 fbshipit-source-id: 66e943b026197d293b5a518b4f97a0bced8d11bb
This commit is contained in:
parent
4130afcf19
commit
9919940807
|
@ -14,6 +14,7 @@
|
||||||
const chalk = require('chalk');
|
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');
|
||||||
|
|
||||||
|
@ -134,7 +135,7 @@ function print(
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-console-disallow
|
// eslint-disable-next-line no-console-disallow
|
||||||
console.log(logEntryString);
|
terminal.log(logEntryString);
|
||||||
|
|
||||||
return logEntry;
|
return logEntry;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,14 @@ const AssetServer = require('../AssetServer');
|
||||||
const getPlatformExtension = require('../node-haste').getPlatformExtension;
|
const getPlatformExtension = require('../node-haste').getPlatformExtension;
|
||||||
const Bundler = require('../Bundler');
|
const Bundler = require('../Bundler');
|
||||||
const MultipartResponse = require('./MultipartResponse');
|
const MultipartResponse = require('./MultipartResponse');
|
||||||
const ProgressBar = require('progress');
|
|
||||||
const SourceMapConsumer = require('source-map').SourceMapConsumer;
|
const SourceMapConsumer = require('source-map').SourceMapConsumer;
|
||||||
|
|
||||||
const declareOpts = require('../lib/declareOpts');
|
const declareOpts = require('../lib/declareOpts');
|
||||||
const defaults = require('../../../defaults');
|
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 throttle = require('lodash/throttle');
|
||||||
const url = require('url');
|
const url = require('url');
|
||||||
|
|
||||||
const debug = require('debug')('ReactNativePackager:Server');
|
const debug = require('debug')('ReactNativePackager:Server');
|
||||||
|
@ -454,7 +455,7 @@ class Server {
|
||||||
e => {
|
e => {
|
||||||
res.writeHead(500);
|
res.writeHead(500);
|
||||||
res.end('Internal Error');
|
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 {
|
} else {
|
||||||
|
@ -697,13 +698,15 @@ class Server {
|
||||||
|
|
||||||
let consoleProgress = () => {};
|
let consoleProgress = () => {};
|
||||||
if (process.stdout.isTTY && !this._opts.silent) {
|
if (process.stdout.isTTY && !this._opts.silent) {
|
||||||
const bar = new ProgressBar('transformed :current/:total (:percent)', {
|
const onProgress = (doneCount, totalCount) => {
|
||||||
complete: '=',
|
const format = 'transformed %s/%s (%s%)';
|
||||||
incomplete: ' ',
|
const percent = Math.floor(100 * doneCount / totalCount);
|
||||||
width: 40,
|
terminal.status(format, doneCount, totalCount, percent);
|
||||||
total: 1,
|
if (doneCount === totalCount) {
|
||||||
});
|
terminal.persistStatus();
|
||||||
consoleProgress = debouncedTick(bar);
|
}
|
||||||
|
};
|
||||||
|
consoleProgress = throttle(onProgress, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mres = MultipartResponse.wrap(req, res);
|
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);
|
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;
|
module.exports = Server;
|
||||||
|
|
|
@ -22,6 +22,7 @@ const jsonStableStringify = require('json-stable-stringify');
|
||||||
const mkdirp = require('mkdirp');
|
const mkdirp = require('mkdirp');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const rimraf = require('rimraf');
|
const rimraf = require('rimraf');
|
||||||
|
const terminal = require('../lib/terminal');
|
||||||
const toFixedHex = require('./toFixedHex');
|
const toFixedHex = require('./toFixedHex');
|
||||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||||
|
|
||||||
|
@ -189,18 +190,18 @@ const GARBAGE_COLLECTOR = new (class GarbageCollector {
|
||||||
try {
|
try {
|
||||||
this._collectSync();
|
this._collectSync();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error.stack);
|
terminal.log(error.stack);
|
||||||
console.error(
|
terminal.log(
|
||||||
'Error: Cleaning up the cache folder failed. Continuing anyway.',
|
'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();
|
this._lastCollected = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
_resetCache() {
|
_resetCache() {
|
||||||
rimraf.sync(getCacheDirPath());
|
rimraf.sync(getCacheDirPath());
|
||||||
console.log('Warning: The transform cache was reset.');
|
terminal.log('Warning: The transform cache was reset.');
|
||||||
this._cacheWasReset = true;
|
this._cacheWasReset = true;
|
||||||
this._lastCollected = Date.now();
|
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 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');
|
||||||
|
|
||||||
|
@ -266,13 +267,13 @@ class Module {
|
||||||
}
|
}
|
||||||
globalCache.fetch(cacheProps, (globalCacheError, globalCachedResult) => {
|
globalCache.fetch(cacheProps, (globalCacheError, globalCachedResult) => {
|
||||||
if (globalCacheError != null && Module._globalCacheRetries > 0) {
|
if (globalCacheError != null && Module._globalCacheRetries > 0) {
|
||||||
console.log(chalk.red(
|
terminal.log(chalk.red(
|
||||||
'\nWarning: the global cache failed with error:',
|
'Warning: the global cache failed with error:',
|
||||||
));
|
));
|
||||||
console.log(chalk.red(globalCacheError.stack));
|
terminal.log(chalk.red(globalCacheError.stack));
|
||||||
Module._globalCacheRetries--;
|
Module._globalCacheRetries--;
|
||||||
if (Module._globalCacheRetries <= 0) {
|
if (Module._globalCacheRetries <= 0) {
|
||||||
console.log(chalk.red(
|
terminal.log(chalk.red(
|
||||||
'No more retries, the global cache will be disabled for the ' +
|
'No more retries, the global cache will be disabled for the ' +
|
||||||
'remainder of the transformation.',
|
'remainder of the transformation.',
|
||||||
));
|
));
|
||||||
|
|
Loading…
Reference in New Issue