From c436997840207e392e689b181f463669d0116e94 Mon Sep 17 00:00:00 2001 From: Jeremy Apthorp Date: Fri, 31 May 2019 10:25:19 -0700 Subject: [PATCH] feat: add ipcRenderer.invoke() (#18449) --- atom/browser/api/atom_api_web_contents.cc | 8 ++ atom/browser/api/atom_api_web_contents.h | 3 + atom/browser/api/event.cc | 2 +- atom/browser/api/event.h | 5 +- atom/common/api/api.mojom | 6 + atom/renderer/api/atom_api_renderer_ipc.cc | 74 ++++++++++++- docs/api/ipc-main.md | 56 +++++++++- docs/api/ipc-renderer.md | 33 +++++- docs/api/structures/ipc-main-event.md | 1 - docs/api/structures/ipc-main-invoke-event.md | 4 + filenames.auto.gni | 1 + lib/browser/api/ipc-main.ts | 38 ++++++- lib/browser/api/web-contents.js | 13 +++ lib/renderer/api/ipc-renderer.js | 7 ++ package.json | 2 +- spec-main/api-ipc-spec.ts | 109 +++++++++++++++++++ yarn.lock | 47 +++++++- 17 files changed, 389 insertions(+), 20 deletions(-) create mode 100644 docs/api/structures/ipc-main-invoke-event.md create mode 100644 spec-main/api-ipc-spec.ts diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index 4fe0ef992e..763a1e00f5 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -924,6 +924,14 @@ void WebContents::Message(bool internal, internal, channel, std::move(arguments)); } +void WebContents::Invoke(const std::string& channel, + base::Value arguments, + InvokeCallback callback) { + // webContents.emit('-ipc-invoke', new Event(), channel, arguments); + EmitWithSender("-ipc-invoke", bindings_.dispatch_context(), + std::move(callback), channel, std::move(arguments)); +} + void WebContents::MessageSync(bool internal, const std::string& channel, base::Value arguments, diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index 8cfbfdc1e3..1aea7f1d03 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -492,6 +492,9 @@ class WebContents : public mate::TrackableObject, void Message(bool internal, const std::string& channel, base::Value arguments) override; + void Invoke(const std::string& channel, + base::Value arguments, + InvokeCallback callback) override; void MessageSync(bool internal, const std::string& channel, base::Value arguments, diff --git a/atom/browser/api/event.cc b/atom/browser/api/event.cc index 7890f9c50f..c8909a24d2 100644 --- a/atom/browser/api/event.cc +++ b/atom/browser/api/event.cc @@ -58,7 +58,7 @@ void Event::PreventDefault(v8::Isolate* isolate) { .Check(); } -bool Event::SendReply(const base::ListValue& result) { +bool Event::SendReply(const base::Value& result) { if (!callback_ || sender_ == nullptr) return false; diff --git a/atom/browser/api/event.h b/atom/browser/api/event.h index 35c241ea3e..ac71a1e12b 100644 --- a/atom/browser/api/event.h +++ b/atom/browser/api/event.h @@ -32,8 +32,9 @@ class Event : public Wrappable, public content::WebContentsObserver { // event.PreventDefault(). void PreventDefault(v8::Isolate* isolate); - // event.sendReply(array), used for replying synchronous message. - bool SendReply(const base::ListValue& result); + // event.sendReply(value), used for replying to synchronous messages and + // `invoke` calls. + bool SendReply(const base::Value& result); protected: explicit Event(v8::Isolate* isolate); diff --git a/atom/common/api/api.mojom b/atom/common/api/api.mojom index abd87fe6a8..ae5b665fcf 100644 --- a/atom/common/api/api.mojom +++ b/atom/common/api/api.mojom @@ -21,6 +21,12 @@ interface ElectronBrowser { string channel, mojo_base.mojom.ListValue arguments); + // Emits an event on |channel| from the ipcMain JavaScript object in the main + // process, and returns the response. + Invoke( + string channel, + mojo_base.mojom.ListValue arguments) => (mojo_base.mojom.Value result); + // Emits an event on |channel| from the ipcMain JavaScript object in the main // process, and waits synchronously for a response. // diff --git a/atom/renderer/api/atom_api_renderer_ipc.cc b/atom/renderer/api/atom_api_renderer_ipc.cc index 295ead588c..dac609df85 100644 --- a/atom/renderer/api/atom_api_renderer_ipc.cc +++ b/atom/renderer/api/atom_api_renderer_ipc.cc @@ -8,6 +8,7 @@ #include "atom/common/native_mate_converters/value_converter.h" #include "atom/common/node_bindings.h" #include "atom/common/node_includes.h" +#include "atom/common/promise_util.h" #include "base/task/post_task.h" #include "base/values.h" #include "content/public/renderer/render_frame.h" @@ -41,6 +42,8 @@ class IPCRenderer : public mate::Wrappable { DCHECK(render_frame); render_frame->GetRemoteInterfaces()->GetInterface( mojo::MakeRequest(&electron_browser_ptr_)); + render_frame->GetRemoteInterfaces()->GetInterface( + mojo::MakeRequest(&electron_browser_sync_ptr_)); } static void BuildPrototype(v8::Isolate* isolate, v8::Local prototype) { @@ -49,7 +52,8 @@ class IPCRenderer : public mate::Wrappable { .SetMethod("send", &IPCRenderer::Send) .SetMethod("sendSync", &IPCRenderer::SendSync) .SetMethod("sendTo", &IPCRenderer::SendTo) - .SetMethod("sendToHost", &IPCRenderer::SendToHost); + .SetMethod("sendToHost", &IPCRenderer::SendToHost) + .SetMethod("invoke", &IPCRenderer::Invoke); } static mate::Handle Create(v8::Isolate* isolate) { return mate::CreateHandle(isolate, new IPCRenderer(isolate)); @@ -62,6 +66,20 @@ class IPCRenderer : public mate::Wrappable { electron_browser_ptr_->Message(internal, channel, arguments.Clone()); } + v8::Local Invoke(mate::Arguments* args, + const std::string& channel, + const base::Value& arguments) { + atom::util::Promise p(args->isolate()); + auto handle = p.GetHandle(); + electron_browser_ptr_->Invoke( + channel, arguments.Clone(), + base::BindOnce( + [](atom::util::Promise p, base::Value value) { p.Resolve(value); }, + std::move(p))); + + return handle; + } + void SendTo(mate::Arguments* args, bool internal, bool send_to_all, @@ -82,6 +100,52 @@ class IPCRenderer : public mate::Wrappable { bool internal, const std::string& channel, const base::ListValue& arguments) { + // We aren't using a true synchronous mojo call here. We're calling an + // asynchronous method and blocking on the result. The reason we're doing + // this is a little complicated, so buckle up. + // + // Mojo has a concept of synchronous calls. However, synchronous calls are + // dangerous. In particular, it's quite possible for two processes to call + // synchronous methods on each other and cause a deadlock. Mojo has a + // mechanism to avoid this kind of deadlock: if a process is waiting on the + // result of a synchronous call, and it receives an incoming call for a + // synchronous method, it will process that request immediately, even + // though it's currently blocking. However, if it receives an incoming + // request for an _asynchronous_ method, that can't cause a deadlock, so it + // stashes the request on a queue to be processed once the synchronous + // thing it's waiting on returns. + // + // This behavior is useful for preventing deadlocks, but it is inconvenient + // here because it can result in messages being reordered. If the main + // process is awaiting the result of a synchronous call (which it does only + // very rarely, since it's bad to block the main process), and we send + // first an asynchronous message to the main process, followed by a + // synchronous message, then the main process will process the synchronous + // one first. + // + // It turns out, Electron has some dependency on message ordering, + // especially during window shutdown, and getting messages out of order can + // result in, for example, remote objects disappearing unexpectedly. To + // avoid these issues and guarantee consistent message ordering, we send + // all messages to the main process as asynchronous messages. This causes + // them to always be queued and processed in the same order they were + // received, even if they were received while the main process was waiting + // on a synchronous call. + // + // However, in the calling process, we still need to block on the result, + // because the caller is expecting a result synchronously. So we do a bit + // of a trick: we pass the Mojo handle over to a new thread, send the + // asynchronous message from that thread, and then block on the result. + // It's important that we pass the handle over to the new thread, because + // that allows Mojo to process incoming messages (most importantly, the + // response to our request) on the new thread. If we didn't pass it to a + // new thread, and instead sent the call from the main thread, we would + // never receive a response because Mojo wouldn't be able to run its + // message handling code, because the main thread would be tied up blocking + // on the WaitableEvent. + // + // Phew. If you got this far, here's a gold star: ⭐️ + base::Value result; // A task is posted to a separate thread to execute the request so that @@ -96,7 +160,7 @@ class IPCRenderer : public mate::Wrappable { // We unbind the interface from this thread to pass it over to the worker // thread temporarily. This requires that no callbacks be pending for this // interface. - auto interface_info = electron_browser_ptr_.PassInterface(); + auto interface_info = electron_browser_sync_ptr_.PassInterface(); task_runner->PostTask( FROM_HERE, base::BindOnce(&IPCRenderer::SendMessageSyncOnWorkerThread, base::Unretained(&interface_info), @@ -104,7 +168,7 @@ class IPCRenderer : public mate::Wrappable { base::Unretained(&result), internal, channel, base::Unretained(&arguments))); response_received_event.Wait(); - electron_browser_ptr_.Bind(std::move(interface_info)); + electron_browser_sync_ptr_.Bind(std::move(interface_info)); return result; } @@ -135,6 +199,10 @@ class IPCRenderer : public mate::Wrappable { } atom::mojom::ElectronBrowserPtr electron_browser_ptr_; + + // We execute all synchronous calls on a separate mojo pipe, because + // of the way that we block on the result of synchronous calls. + atom::mojom::ElectronBrowserPtr electron_browser_sync_ptr_; }; void Initialize(v8::Local exports, diff --git a/docs/api/ipc-main.md b/docs/api/ipc-main.md index 006fd9bcb8..c82f717671 100644 --- a/docs/api/ipc-main.md +++ b/docs/api/ipc-main.md @@ -88,7 +88,61 @@ Removes the specified `listener` from the listener array for the specified Removes listeners of the specified `channel`. -## Event object +### `ipcMain.handle(channel, listener)` + +* `channel` String +* `listener` Function | Function + * `event` IpcMainInvokeEvent + * `...args` any[] + +Adds a handler for an `invoke`able IPC. This handler will be called whenever a +renderer calls `ipcRenderer.invoke(channel, ...args)`. + +If `listener` returns a Promise, the eventual result of the promise will be +returned as a reply to the remote caller. Otherwise, the return value of the +listener will be used as the value of the reply. + +```js +// Main process +ipcMain.handle('my-invokable-ipc', async (event, ...args) => { + const result = await somePromise(...args) + return result +}) + +// Renderer process +async () => { + const result = await ipcRenderer.invoke('my-invokable-ipc', arg1, arg2) + // ... +} +``` + +The `event` that is passed as the first argument to the handler is the same as +that passed to a regular event listener. It includes information about which +WebContents is the source of the invoke request. + +### `ipcMain.handleOnce(channel, listener)` + +* `channel` String +* `listener` Function | Function + * `event` IpcMainInvokeEvent + * `...args` any[] + +Handles a single `invoke`able IPC message, then removes the listener. See +`ipcMain.handle(channel, listener)`. + +### `ipcMain.removeHandler(channel)` + +* `channel` String + +Removes any handler for `channel`, if present. + +## IpcMainEvent object The documentation for the `event` object passed to the `callback` can be found in the [`ipc-main-event`](structures/ipc-main-event.md) structure docs. + +## IpcMainInvokeEvent object + +The documentation for the `event` object passed to `handle` callbacks can be +found in the [`ipc-main-invoke-event`](structures/ipc-main-invoke-event.md) +structure docs. diff --git a/docs/api/ipc-renderer.md b/docs/api/ipc-renderer.md index 9ea0dd7274..dea228ec59 100644 --- a/docs/api/ipc-renderer.md +++ b/docs/api/ipc-renderer.md @@ -57,10 +57,39 @@ Removes all listeners, or those of the specified `channel`. * `...args` any[] Send a message to the main process asynchronously via `channel`, you can also -send arbitrary arguments. Arguments will be serialized in JSON internally and +send arbitrary arguments. Arguments will be serialized as JSON internally and hence no functions or prototype chain will be included. -The main process handles it by listening for `channel` with [`ipcMain`](ipc-main.md) module. +The main process handles it by listening for `channel` with the +[`ipcMain`](ipc-main.md) module. + +### `ipcRenderer.invoke(channel[, arg1][, arg2][, ...])` + +* `channel` String +* `...args` any[] + +Returns `Promise` - Resolves with the response from the main process. + +Send a message to the main process asynchronously via `channel` and expect an +asynchronous result. Arguments will be serialized as JSON internally and +hence no functions or prototype chain will be included. + +The main process should listen for `channel` with +[`ipcMain.handle()`](ipc-main.md#ipcmainhandlechannel-listener). + +For example: +```javascript +// Renderer process +ipcRenderer.invoke('some-name', someArgument).then((result) => { + // ... +}) + +// Main process +ipcMain.handle('some-name', async (event, someArgument) => { + const result = await doSomeWork(someArgument) + return result +}) +``` ### `ipcRenderer.sendSync(channel[, arg1][, arg2][, ...])` diff --git a/docs/api/structures/ipc-main-event.md b/docs/api/structures/ipc-main-event.md index 4a072c94f9..02212e8732 100644 --- a/docs/api/structures/ipc-main-event.md +++ b/docs/api/structures/ipc-main-event.md @@ -5,4 +5,3 @@ * `sender` WebContents - Returns the `webContents` that sent the message * `reply` Function - A function that will send an IPC message to the renderer frame that sent the original message that you are currently handling. You should use this method to "reply" to the sent message in order to guaruntee the reply will go to the correct process and frame. * `...args` any[] -IpcRenderer diff --git a/docs/api/structures/ipc-main-invoke-event.md b/docs/api/structures/ipc-main-invoke-event.md new file mode 100644 index 0000000000..235b219c2d --- /dev/null +++ b/docs/api/structures/ipc-main-invoke-event.md @@ -0,0 +1,4 @@ +# IpcMainInvokeEvent Object extends `Event` + +* `frameId` Integer - The ID of the renderer frame that sent this message +* `sender` WebContents - Returns the `webContents` that sent the message diff --git a/filenames.auto.gni b/filenames.auto.gni index 2e4a19d69c..d2d4bd5a95 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -78,6 +78,7 @@ auto_filenames = { "docs/api/structures/gpu-feature-status.md", "docs/api/structures/io-counters.md", "docs/api/structures/ipc-main-event.md", + "docs/api/structures/ipc-main-invoke-event.md", "docs/api/structures/ipc-renderer-event.md", "docs/api/structures/jump-list-category.md", "docs/api/structures/jump-list-item.md", diff --git a/lib/browser/api/ipc-main.ts b/lib/browser/api/ipc-main.ts index 8229f257af..d97e916149 100644 --- a/lib/browser/api/ipc-main.ts +++ b/lib/browser/api/ipc-main.ts @@ -1,8 +1,40 @@ import { EventEmitter } from 'events' +import { IpcMainInvokeEvent } from 'electron' -const emitter = new EventEmitter() +class IpcMain extends EventEmitter { + private _invokeHandlers: Map void> = new Map(); + + handle: Electron.IpcMain['handle'] = (method, fn) => { + if (this._invokeHandlers.has(method)) { + throw new Error(`Attempted to register a second handler for '${method}'`) + } + if (typeof fn !== 'function') { + throw new Error(`Expected handler to be a function, but found type '${typeof fn}'`) + } + this._invokeHandlers.set(method, async (e, ...args) => { + try { + (e as any)._reply(await Promise.resolve(fn(e, ...args))) + } catch (err) { + (e as any)._throw(err) + } + }) + } + + handleOnce: Electron.IpcMain['handleOnce'] = (method, fn) => { + this.handle(method, (e, ...args) => { + this.removeHandler(method) + return fn(e, ...args) + }) + } + + removeHandler (method: string) { + this._invokeHandlers.delete(method) + } +} + +const ipcMain = new IpcMain() // Do not throw exception when channel name is "error". -emitter.on('error', () => {}) +ipcMain.on('error', () => {}) -export default emitter +export default ipcMain diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index 0e515961ce..7cd3e6a5f2 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -326,6 +326,19 @@ WebContents.prototype._init = function () { } }) + this.on('-ipc-invoke', function (event, channel, args) { + event._reply = (result) => event.sendReply({ result }) + event._throw = (error) => { + console.error(`Error occurred in handler for '${channel}':`, error) + event.sendReply({ error: error.toString() }) + } + if (ipcMain._invokeHandlers.has(channel)) { + ipcMain._invokeHandlers.get(channel)(event, ...args) + } else { + event._throw(`No handler registered for '${channel}'`) + } + }) + this.on('-ipc-message-sync', function (event, internal, channel, args) { addReturnValueToEvent(event) if (internal) { diff --git a/lib/renderer/api/ipc-renderer.js b/lib/renderer/api/ipc-renderer.js index 8705882d73..64b2ce4ef1 100644 --- a/lib/renderer/api/ipc-renderer.js +++ b/lib/renderer/api/ipc-renderer.js @@ -27,4 +27,11 @@ ipcRenderer.sendToAll = function (webContentsId, channel, ...args) { return ipc.sendTo(internal, true, webContentsId, channel, args) } +ipcRenderer.invoke = function (channel, ...args) { + return ipc.invoke(channel, args).then(({ error, result }) => { + if (error) { throw new Error(`Error invoking remote method '${channel}': ${error}`) } + return result + }) +} + module.exports = ipcRenderer diff --git a/package.json b/package.json index 66a3cf5e16..dfcb011345 100644 --- a/package.json +++ b/package.json @@ -136,4 +136,4 @@ "git add filenames.auto.gni" ] } -} \ No newline at end of file +} diff --git a/spec-main/api-ipc-spec.ts b/spec-main/api-ipc-spec.ts new file mode 100644 index 0000000000..d004f4d069 --- /dev/null +++ b/spec-main/api-ipc-spec.ts @@ -0,0 +1,109 @@ +import * as chai from 'chai' +import * as chaiAsPromised from 'chai-as-promised' +import { BrowserWindow, ipcMain, IpcMainInvokeEvent } from 'electron' + +const { expect } = chai + +chai.use(chaiAsPromised) + +describe('ipc module', () => { + describe('invoke', () => { + let w = (null as unknown as BrowserWindow); + + before(async () => { + w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }) + await w.loadURL('about:blank') + }) + after(async () => { + w.destroy() + }) + + async function rendererInvoke(...args: any[]) { + const {ipcRenderer} = require('electron') + try { + const result = await ipcRenderer.invoke('test', ...args) + ipcRenderer.send('result', {result}) + } catch (e) { + ipcRenderer.send('result', {error: e.message}) + } + } + + it('receives a response from a synchronous handler', async () => { + ipcMain.handleOnce('test', (e: IpcMainInvokeEvent, arg: number) => { + expect(arg).to.equal(123) + return 3 + }) + const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { + expect(arg).to.deep.equal({result: 3}) + resolve() + })) + await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`) + await done + }) + + it('receives a response from an asynchronous handler', async () => { + ipcMain.handleOnce('test', async (e: IpcMainInvokeEvent, arg: number) => { + expect(arg).to.equal(123) + await new Promise(resolve => setImmediate(resolve)) + return 3 + }) + const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { + expect(arg).to.deep.equal({result: 3}) + resolve() + })) + await w.webContents.executeJavaScript(`(${rendererInvoke})(123)`) + await done + }) + + it('receives an error from a synchronous handler', async () => { + ipcMain.handleOnce('test', () => { + throw new Error('some error') + }) + const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { + expect(arg.error).to.match(/some error/) + resolve() + })) + await w.webContents.executeJavaScript(`(${rendererInvoke})()`) + await done + }) + + it('receives an error from an asynchronous handler', async () => { + ipcMain.handleOnce('test', async () => { + await new Promise(resolve => setImmediate(resolve)) + throw new Error('some error') + }) + const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { + expect(arg.error).to.match(/some error/) + resolve() + })) + await w.webContents.executeJavaScript(`(${rendererInvoke})()`) + await done + }) + + it('throws an error if no handler is registered', async () => { + const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { + expect(arg.error).to.match(/No handler registered/) + resolve() + })) + await w.webContents.executeJavaScript(`(${rendererInvoke})()`) + await done + }) + + it('throws an error when invoking a handler that was removed', async () => { + ipcMain.handle('test', () => {}) + ipcMain.removeHandler('test') + const done = new Promise(resolve => ipcMain.once('result', (e, arg) => { + expect(arg.error).to.match(/No handler registered/) + resolve() + })) + await w.webContents.executeJavaScript(`(${rendererInvoke})()`) + await done + }) + + it('forbids multiple handlers', async () => { + ipcMain.handle('test', () => {}) + expect(() => { ipcMain.handle('test', () => {}) }).to.throw(/second handler/) + ipcMain.removeHandler('test') + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 464dacf69e..bd1c00ddb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,8 +23,9 @@ regenerator-runtime "^0.12.0" "@electron/docs-parser@^0.1.1": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@electron/docs-parser/-/docs-parser-0.1.5.tgz#e6c05ed200b4c155986fb0f46178dede71a5e078" + version "0.1.6" + resolved "https://registry.yarnpkg.com/@electron/docs-parser/-/docs-parser-0.1.6.tgz#7ed86586e0ebc4a5c63e7c112264357adc61d334" + integrity sha512-WAV0xHx1HIflqvb6G01LLnpS9n3VzNF0vyfxYhbP3Ev2p+m8CODc2+9pCdzCmH457UYi8GsX/jnB23djTCxt7Q== dependencies: "@types/markdown-it" "^0.0.7" chai "^4.2.0" @@ -156,10 +157,12 @@ "@types/linkify-it@*": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806" + integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw== "@types/markdown-it@^0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39" + integrity sha512-WyL6pa76ollQFQNEaLVa41ZUUvDvPY+qAUmlsphnrpL6I9p1m868b26FyeoOmo7X3/Ta/S9WKXcEYXUSHnxoVQ== dependencies: "@types/linkify-it" "*" @@ -319,6 +322,7 @@ ansi-regex@^3.0.0: ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== ansi-styles@^2.2.1: version "2.2.1" @@ -327,6 +331,7 @@ ansi-styles@^2.2.1: ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" @@ -359,6 +364,7 @@ are-we-there-yet@~1.1.2: argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" @@ -469,6 +475,7 @@ assert@^1.4.0: assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== assign-symbols@^1.0.0: version "1.0.0" @@ -876,6 +883,7 @@ ccount@^1.0.0: chai@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" + integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== dependencies: assertion-error "^1.1.0" check-error "^1.0.2" @@ -897,6 +905,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" @@ -929,6 +938,7 @@ chardet@^0.7.0: check-error@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= check-for-leaks@^1.0.2: version "1.2.0" @@ -1010,6 +1020,7 @@ cli-cursor@^1.0.2: cli-cursor@^2.0.0, cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= dependencies: restore-cursor "^2.0.0" @@ -1020,6 +1031,7 @@ cli-spinners@^0.1.2: cli-spinners@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.1.0.tgz#22c34b4d51f573240885b201efda4e4ec9fff3c7" + integrity sha512-8B00fJOEh1HPrx4fo5eW16XmE1PcL1tGpGrxy63CXGP9nHdPBN63X75hA1zhvQuhVztJWLqV58Roj2qlNM7cAA== cli-truncate@^0.2.1: version "0.2.1" @@ -1035,6 +1047,7 @@ cli-width@^2.0.0: clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= co@3.1.0: version "3.1.0" @@ -1058,12 +1071,14 @@ collection-visit@^1.0.0: color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= colors@^1.1.2: version "1.3.3" @@ -1291,6 +1306,7 @@ dedent@^0.7.0: deep-eql@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== dependencies: type-detect "^4.0.0" @@ -1309,6 +1325,7 @@ deepmerge@3.2.0: defaults@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= dependencies: clone "^1.0.2" @@ -1554,6 +1571,7 @@ ensure-posix-path@^1.0.0: entities@~1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" @@ -2262,6 +2280,7 @@ get-assigned-identifiers@^1.2.0: get-func-name@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= get-own-enumerable-property-symbols@^3.0.0: version "3.0.0" @@ -2416,6 +2435,7 @@ has-flag@^2.0.0: has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= has-symbols@^1.0.0: version "1.0.0" @@ -3117,6 +3137,7 @@ levn@^0.3.0, levn@~0.3.0: linkify-it@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.1.0.tgz#c4caf38a6cd7ac2212ef3c7d2bde30a91561f9db" + integrity sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg== dependencies: uc.micro "^1.0.1" @@ -3247,6 +3268,7 @@ locate-path@^3.0.0: lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= lodash.flatten@^4.4.0: version "4.4.0" @@ -3289,6 +3311,7 @@ log-symbols@^1.0.2: log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== dependencies: chalk "^2.0.1" @@ -3354,6 +3377,7 @@ markdown-extensions@^1.1.0: markdown-it@^8.4.2: version "8.4.2" resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54" + integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ== dependencies: argparse "^1.0.7" entities "~1.1.1" @@ -3410,6 +3434,7 @@ mdast-util-to-string@^1.0.2: mdurl@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= media-typer@0.3.0: version "0.3.0" @@ -3502,6 +3527,7 @@ mime@1.4.1: mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" @@ -3806,6 +3832,7 @@ onetime@^1.0.0: onetime@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= dependencies: mimic-fn "^1.0.0" @@ -3838,6 +3865,7 @@ ora@^0.2.3: ora@^3.0.0, ora@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" + integrity sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg== dependencies: chalk "^2.4.2" cli-cursor "^2.1.0" @@ -3993,6 +4021,7 @@ parse-json@^4.0.0: parse-ms@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" + integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== parseurl@~1.3.2: version "1.3.3" @@ -4067,6 +4096,7 @@ path-type@^3.0.0: pathval@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= pbkdf2@^3.0.3: version "3.0.17" @@ -4198,6 +4228,7 @@ pretty-bytes@^1.0.2: pretty-ms@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-5.0.0.tgz#6133a8f55804b208e4728f6aa7bf01085e951e24" + integrity sha512-94VRYjL9k33RzfKiGokPBPpsmloBYSf5Ri+Pq19zlsEcUKFob+admeXr5eFDRuPjFmEOcjJvPGdillYOJyvZ7Q== dependencies: parse-ms "^2.1.0" @@ -5069,6 +5100,7 @@ restore-cursor@^1.0.1: restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= dependencies: onetime "^2.0.0" signal-exit "^3.0.2" @@ -5260,6 +5292,7 @@ shx@^0.3.2: signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= simple-concat@^1.0.0: version "1.0.0" @@ -5399,6 +5432,7 @@ split-string@^3.0.1, split-string@^3.0.2: sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= sshpk@^1.7.0: version "1.16.1" @@ -5581,6 +5615,7 @@ strip-ansi@^4.0.0: strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: ansi-regex "^4.1.0" @@ -5604,10 +5639,6 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -strip-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" - strip-json-comments@^2.0.0, strip-json-comments@^2.0.1, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -5637,6 +5668,7 @@ supports-color@^4.1.0: supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" @@ -5898,6 +5930,7 @@ type-check@~0.3.2: type-detect@^4.0.0, type-detect@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-fest@^0.4.1: version "0.4.1" @@ -5926,6 +5959,7 @@ typescript@~3.3.3333: uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== umd@^3.0.0: version "3.0.3" @@ -6207,6 +6241,7 @@ walk-sync@^0.3.2: wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= dependencies: defaults "^1.0.3"