forked from facebook/react-native
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
packager: Terminal abstraction to manage TTYs
Reviewed By: cpojer Differential Revision: D4293146 fbshipit-source-id: 66e943b026197d293b5a518b4f97a0bced8d11bb
- Loading branch information
Jean Lauliac
authored and
Facebook Github Bot
committed
Dec 14, 2016
1 parent
f8f70d2
commit 26ed94c
Showing
7 changed files
with
261 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
packager/react-packager/src/lib/__tests__/terminal-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
}); | ||
|
||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters