diff --git a/ui/desktop/app/routes/index.js b/ui/desktop/app/routes/index.js index c8728d037..22fa97d16 100644 --- a/ui/desktop/app/routes/index.js +++ b/ui/desktop/app/routes/index.js @@ -1,8 +1,23 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class IndexRoute extends Route { + // =services + + @service ipc; + // =methods + /** + * Invokes an IPC method and returns the result. + * @return {Promise} + */ + model() { + return this.ipc.invoke('getFoobars', { + id: '1' + }); + } + /** * Redirects to scopes route for further processing. */ diff --git a/ui/desktop/app/services/ipc.js b/ui/desktop/app/services/ipc.js new file mode 100644 index 000000000..d7f5d8819 --- /dev/null +++ b/ui/desktop/app/services/ipc.js @@ -0,0 +1,150 @@ +import Service from '@ember/service'; +import { getOwner } from '@ember/application'; +import { Promise } from 'rsvp'; +import config from '../config/environment'; + +/** + * An IPC request is a promise-like object that issues a request via + * `window.postMessage` and resolves to a response received via a response + * port. IPC requests are used by an Electron renderer process to communicate + * with the main process when the renderer is fully isolated. + * + * Some of the design goals of this "safe IPC" are to respect the confines + * of process isolation and security, without using node integration, while + * retaining the ability to run the Ember application within a browser + * (sans main process) by using only APIs available in a standard + * (non-Electron) browser. This is achieved by posting messages to the + * renderer's `window.postMessage` interface. Thus, the renderer requires no + * knowledge of the main process or the Electron context. In fact a main + * process and Electron context are not even required. + * + * In an Electron context, the `preload.js` script sets up a `window.postMessge` + * listener which acts as proxy layer between the main and renderer processes. + * It waits for incoming requests from the renderer and forwards them to main. + * Responses from the main process are forwarded back via a response port + * (sent with the original request). + * + * Local development in a non-Electron browser context or test context should + * mock up IPC requests. See the ipc service test for an example of mocking. + * + * TODO: add utilities for quickly mocking IPC responses (like Mirage) + * + */ +export class IPCRequest { + + // =attributes + + /** + * @type {?MessageChannel} + * @private + */ + #channel = null; + + /** + * @type {?Promise} + * @private + */ + #promise = null; + + // =methods + + /** + * Posts a message to the window object, along with a MessagePort + * with which to receive a response. An internal promise is generated which + * resolves to the response. + * @param {string} method + * @param {object} payload + * @param {Window} window - A reference to the window object + * @param {?string} origin + */ + constructor(method, payload, window, origin=config.ipc.defaultOrigin) { + this.method = method; + this.payload = payload; + this.origin = origin; + this.#channel = new MessageChannel(); + this.#promise = new Promise((resolve/*, reject*/) => { + this.#channel.port1.onmessage = (event) => { + this.close(); + resolve(event.data); + } + }); + window.postMessage({ + method: this.method, + payload: this.payload + }, origin, [ this.#channel.port2 ]); + } + + /** + * Closes the message ports and drops the channel reference. + * This is called upon receiving a response for cleanup purposes. + */ + close() { + this.#channel.port1.close(); + this.#channel.port2.close(); + this.#channel = null; + } + + /** + * Proxies to the underlying promise. + */ + then() { + return this.#promise.then(...arguments); + } + + /** + * Proxies to the underlying promise. + */ + catch() { + return this.#promise.catch(...arguments); + } + + /** + * Proxies to the underlying promise. + */ + finally() { + return this.#promise.finally(...arguments); + } + +} + +/** + * A simple factory for generating IPCRequests in Ember. + * @example + * + * class MyClass { + * @service ipc; + * + * makeRequest { + * return this.ipc.invoke('getFoobars', { hello: 'world' }); + * } + * } + */ +export default class IpcService extends Service { + + // =attributes + + /** + * Looks up the window object indirectly. + * @type {Window} + */ + get window() { + // The Ember way of accessing globals... + const document = getOwner(this).lookup('service:-document').documentElement; + // defaultView === window, but without using globals directly + return document.parentNode.defaultView; + } + + // =methods + + /** + * Creates a new IPC request and returns it. + * @param {string} method + * @param {object} payload + * @param {object} origin + * @return {IPCRequest} + */ + invoke(method, payload, origin) { + return new IPCRequest(method, payload, this.window, origin); + } + +} diff --git a/ui/desktop/app/templates/index.hbs b/ui/desktop/app/templates/index.hbs index e2147cab0..f3994d28f 100644 --- a/ui/desktop/app/templates/index.hbs +++ b/ui/desktop/app/templates/index.hbs @@ -1 +1,2 @@ -{{outlet}} \ No newline at end of file +{{outlet}} +{{@model.message}} diff --git a/ui/desktop/config/environment.js b/ui/desktop/config/environment.js index dbd5328b7..4b485b1e4 100644 --- a/ui/desktop/config/environment.js +++ b/ui/desktop/config/environment.js @@ -31,6 +31,12 @@ module.exports = function (environment) { namespace: 'v1', }, + ipc: { + defaultOrigin: process.env.EMBER_CLI_ELECTRON + ? 'serve://boundary' + : 'http://localhost:4200' + }, + appName: APP_NAME, }; diff --git a/ui/desktop/electron-app/src/handle-file-urls.js b/ui/desktop/electron-app/src/handle-file-urls.js deleted file mode 100644 index f6eeb1707..000000000 --- a/ui/desktop/electron-app/src/handle-file-urls.js +++ /dev/null @@ -1,42 +0,0 @@ -const { fileURLToPath } = require('url'); -const path = require('path'); -const fs = require('fs'); -const { promisify } = require('util'); - -const access = promisify(fs.access); - -// -// Patch asset loading -- Ember apps use absolute paths to reference their -// assets, e.g. ``. When the current URL is a `file:` -// URL, that ends up resolving to the absolute filesystem path `/images/foo.jpg` -// rather than being relative to the root of the Ember app. So, we intercept -// `file:` URL request and look to see if they point to an asset when -// interpreted as being relative to the root of the Ember app. If so, we return -// that path, and if not we leave them as-is, as their absolute path. -// -async function getAssetPath(emberAppDir, url) { - let urlPath = fileURLToPath(url); - // Get the root of the path -- should be '/' on MacOS or something like - // 'C:\' on Windows - let { root } = path.parse(urlPath); - // Get the relative path from the root to the full path - let relPath = path.relative(root, urlPath); - // Join the relative path with the Ember app directory - let appPath = path.join(emberAppDir, relPath); - try { - await access(appPath); - return appPath; - } catch (e) { - return urlPath; - } -} - -module.exports = function handleFileURLs(emberAppDir) { - const { protocol } = require('electron'); - - protocol.interceptFileProtocol('file', async ({ url }, callback) => { - callback(await getAssetPath(emberAppDir, url)); - }); -}; - -module.exports.getAssetPath = getAssetPath; diff --git a/ui/desktop/electron-app/src/handlers.js b/ui/desktop/electron-app/src/handlers.js new file mode 100644 index 000000000..f5bcf175b --- /dev/null +++ b/ui/desktop/electron-app/src/handlers.js @@ -0,0 +1,17 @@ +const { ipcMain } = require('electron'); +const isDev = require('electron-is-dev'); + +const log = (method, request) => { + if (isDev) console.log(`[ipc-main](${method}): `, request); +} + + +/** + * A demonstration method `getFoobars` which returns an object with a message. + */ +ipcMain.handle('getFoobars', async (event, request) => { + log('getFoobars', request); + return { + message: 'this is a response from an ipcMain handler' + }; +}); diff --git a/ui/desktop/electron-app/src/index.js b/ui/desktop/electron-app/src/index.js index 0a55aca15..c4a1a50e0 100644 --- a/ui/desktop/electron-app/src/index.js +++ b/ui/desktop/electron-app/src/index.js @@ -1,25 +1,28 @@ /* eslint-disable no-console */ -const { default: installExtension, EMBER_INSPECTOR } = require('electron-devtools-installer'); -const { pathToFileURL } = require('url'); -const { app, BrowserWindow } = require('electron'); const path = require('path'); -const isDev = require('electron-is-dev'); -const handleFileUrls = require('./handle-file-urls'); +const { default: installExtension, EMBER_INSPECTOR } = require('electron-devtools-installer'); +const { session, app, protocol, BrowserWindow, ipcMain } = require('electron'); +require('./handlers.js'); +const isDev = require('electron-is-dev'); + +// Register the custom file protocol +const emberAppProtocol = 'serve'; +const emberAppName = 'boundary'; +const emberAppURL = `${emberAppProtocol}://${emberAppName}`; const emberAppDir = path.resolve(__dirname, '..', 'ember-dist'); -const emberAppURL = pathToFileURL(path.join(emberAppDir, 'index.html')).toString(); +const preloadPath = path.resolve(__dirname, 'preload.js'); + +protocol.registerSchemesAsPrivileged([{ + scheme: emberAppProtocol, + privileges: { + secure: true, + standard: true + } +}]); let mainWindow = null; -// Uncomment the lines below to enable Electron's crash reporter -// For more information, see http://electron.atom.io/docs/api/crash-reporter/ -// electron.crashReporter.start({ -// productName: 'YourName', -// companyName: 'YourCompany', -// submitURL: 'https://your-domain.com/url-to-submit', -// autoSubmit: true -// }); - app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); @@ -27,6 +30,23 @@ app.on('window-all-closed', () => { }); app.on('ready', async () => { + + // Register custom protocol + const partition = emberAppName; + const ses = session.fromPartition(partition); + + ses.protocol.registerFileProtocol(emberAppProtocol, (request, callback) => { + const isDir = request.url.endsWith('/'); + const absolutePath = request.url.substr(emberAppURL.length); + const normalizedPath = isDir + ? path.normalize(`${emberAppDir}/index.html`) + : path.normalize(`${emberAppDir}${absolutePath}`); + + if (isDev) console.log('[serving]', request.url); + + callback({ path: normalizedPath }); + }); + if (isDev) { try { require('devtron').install(); @@ -40,11 +60,19 @@ app.on('ready', async () => { } } - await handleFileUrls(emberAppDir); - mainWindow = new BrowserWindow({ - width: 800, - height: 600, + width: 1024, + height: 768, + webPreferences: { + partition, + sandbox: true, + webSecurity: true, + contextIsolation: true, + nodeIntegration: false, + enableRemoteModule: false, + allowRunningInsecureContent: false, + preload: preloadPath + } }); // If you want to open up dev tools programmatically, call diff --git a/ui/desktop/electron-app/src/preload.js b/ui/desktop/electron-app/src/preload.js new file mode 100644 index 000000000..8c9757b0d --- /dev/null +++ b/ui/desktop/electron-app/src/preload.js @@ -0,0 +1,24 @@ +const { ipcRenderer } = require('electron'); +const emberAppOrigin = 'serve://boundary'; + +process.once('loaded', () => { + + /** + * Ember-land has no access to the renderer or node modules, and thus + * cannot call into the main process or make invocations directly. + * In order to communicate with the main process, the Ember app uses + * the `postMessage` interface to send messages to its browser window. + * These messages include message ports with which to receive responses from + * the main process. + * + * This preload script simply wires up the forwarding of + * messages received on the window and responses received from main. + */ + window.addEventListener('message', async function (event) { + if (event.origin !== emberAppOrigin) return; + const response = + await ipcRenderer.invoke(event.data.method, event.data.payload); + event.ports[0].postMessage(response); + }); + +}); diff --git a/ui/desktop/package.json b/ui/desktop/package.json index ee6ec558d..793d4a132 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -78,7 +78,8 @@ "prettier": "^2.1.2", "qunit-dom": "^1.2.0", "rose": "*", - "sass": "^1.29.0" + "sass": "^1.29.0", + "sinon": "^9.2.1" }, "engines": { "node": "10.* || >= 12" diff --git a/ui/desktop/tests/unit/services/ipc-test.js b/ui/desktop/tests/unit/services/ipc-test.js new file mode 100644 index 000000000..1de9e29b0 --- /dev/null +++ b/ui/desktop/tests/unit/services/ipc-test.js @@ -0,0 +1,57 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { IPCRequest } from 'desktop/services/ipc'; +import sinon from 'sinon'; + +module('Unit | Service | ipc', function(hooks) { + setupTest(hooks); + + hooks.beforeEach(() => { + // IPC requests talk to the main process indirectly via window.postMessage + // proxy layer (mediated by preload.js). + // But we don't want to send these for real in tests. + sinon.stub(window, 'postMessage'); + }); + + hooks.afterEach(() => { + sinon.restore(); + }); + + test('it creates new IPCRequest objects', function(assert) { + assert.expect(1); + const service = this.owner.lookup('service:ipc'); + const request = service.invoke('hello', 'world', 'http://localhost:4200'); + assert.equal(request.constructor, IPCRequest); + }); + + test('IPCRequest objects post messages on their window objects', function(assert) { + assert.expect(2); + const myWindow = { + postMessage(request) { + assert.equal(request.method, 'hello'); + assert.equal(request.payload, 'world'); + } + }; + const request = new IPCRequest('hello', 'world', myWindow); + }); + + test('IPCRequest objects resolve to the value posted via a response message port', async function(assert) { + assert.expect(1); + const myWindow = { + postMessage(request, origin, [ port ]) { + // Post back a message on the response port + port.postMessage({ + foo: 'bar', + test: 42 + }); + } + }; + const request = new IPCRequest('hello', 'world', myWindow); + const response = await request; + assert.deepEqual(response, { + foo: 'bar', + test: 42 + }); + }); + +}); diff --git a/yarn.lock b/yarn.lock index d410d9114..9bd2efbf7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6341,9 +6341,9 @@ caseless@~0.11.0: integrity sha1-cVuW6phBWTzDMGeSP17GDr2k99c= ccount@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" - integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== + version "1.0.5" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" + integrity sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw== chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" @@ -6420,7 +6420,7 @@ cheerio@^1.0.0-rc.2: lodash "^4.15.0" parse5 "^3.0.1" -"chokidar@>=2.0.0 <4.0.0", chokidar@^3.3.0, chokidar@^3.4.1: +"chokidar@>=2.0.0 <4.0.0": version "3.4.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.3.tgz#c1df38231448e45ca4ac588e6c79573ba6a57d5b" integrity sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ== @@ -6454,6 +6454,21 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.3.0, chokidar@^3.4.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + chownr@^1.1.1, chownr@^1.1.2, chownr@^1.1.3, chownr@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -11880,7 +11895,14 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.7.1, hosted-git-info@^2.8.8: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== -hosted-git-info@^3.0.2, hosted-git-info@^3.0.6: +hosted-git-info@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.5.tgz#bea87905ef7317442e8df3087faa3c842397df03" + integrity sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ== + dependencies: + lru-cache "^6.0.0" + +hosted-git-info@^3.0.6: version "3.0.7" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.7.tgz#a30727385ea85acfcee94e0aad9e368c792e036c" integrity sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ== @@ -12530,7 +12552,12 @@ is-buffer@^2.0.0: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.4, is-callable@^1.2.2: +is-callable@^1.1.4: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" + integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== + +is-callable@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== @@ -15433,7 +15460,7 @@ npm-package-arg@^7.0.0: semver "^5.6.0" validate-npm-package-name "^3.0.0" -npm-package-arg@^8.0.0, npm-package-arg@^8.0.1: +npm-package-arg@^8.0.0: version "8.1.0" resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.0.tgz#b5f6319418c3246a1c38e1a8fbaa06231bc5308f" integrity sha512-/ep6QDxBkm9HvOhOg0heitSd7JHA1U7y1qhhlRlteYYAi9Pdb/ZV7FW5aHpkrpM8+P+4p/jjR8zCyKPBMBjSig== @@ -15442,6 +15469,15 @@ npm-package-arg@^8.0.0, npm-package-arg@^8.0.1: semver "^7.0.0" validate-npm-package-name "^3.0.0" +npm-package-arg@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.0.1.tgz#9d76f8d7667b2373ffda60bb801a27ef71e3e270" + integrity sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ== + dependencies: + hosted-git-info "^3.0.2" + semver "^7.0.0" + validate-npm-package-name "^3.0.0" + npm-packlist@^1.1.12, npm-packlist@^1.1.6, npm-packlist@^1.4.6, npm-packlist@^1.4.8: version "1.4.8" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" @@ -15863,7 +15899,7 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -open@^7.0.0, open@^7.0.2, open@^7.0.3, open@^7.2.0: +open@^7.0.0, open@^7.2.0: version "7.3.0" resolved "https://registry.yarnpkg.com/open/-/open-7.3.0.tgz#45461fdee46444f3645b6e14eb3ca94b82e1be69" integrity sha512-mgLwQIx2F/ye9SmbrUkurZCnkoXyXyu9EbHtJZrICjVAJfyMArdHp3KkixGdZx1ZHFPNIwl0DDM1dFFqXbTLZw== @@ -15871,6 +15907,22 @@ open@^7.0.0, open@^7.0.2, open@^7.0.3, open@^7.2.0: is-docker "^2.0.0" is-wsl "^2.1.1" +open@^7.0.2: + version "7.2.1" + resolved "https://registry.yarnpkg.com/open/-/open-7.2.1.tgz#07b0ade11a43f2a8ce718480bdf3d7563a095195" + integrity sha512-xbYCJib4spUdmcs0g/2mK1nKo/jO2T7INClWd/beL7PFkXRWgr8B23ssDHX/USPn2M2IjDR5UdpYs6I67SnTSA== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + +open@^7.0.3: + version "7.0.4" + resolved "https://registry.yarnpkg.com/open/-/open-7.0.4.tgz#c28a9d315e5c98340bf979fdcb2e58664aa10d83" + integrity sha512-brSA+/yq+b08Hsr4c8fsEW2CRzk1BmfN3SAK/5VCHQ9bdoZJ4qa/+AfR0xHjlbbZUyPkUHs1b8x1RqdyZdkVqQ== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + opener@^1.5.1: version "1.5.2" resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" @@ -17382,7 +17434,7 @@ read-package-json-fast@^1.0.3: json-parse-even-better-errors "^2.3.0" npm-normalize-package-bin "^1.0.1" -"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.13, read-package-json@^2.1.1: +"read-package-json@1 || 2", read-package-json@^2.0.13, read-package-json@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.2.tgz#6992b2b66c7177259feb8eaac73c3acd28b9222a" integrity sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA== @@ -17392,6 +17444,18 @@ read-package-json-fast@^1.0.3: normalize-package-data "^2.0.0" npm-normalize-package-bin "^1.0.0" +read-package-json@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.1.tgz#16aa66c59e7d4dad6288f179dd9295fd59bb98f1" + integrity sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A== + dependencies: + glob "^7.1.1" + json-parse-better-errors "^1.0.1" + normalize-package-data "^2.0.0" + npm-normalize-package-bin "^1.0.0" + optionalDependencies: + graceful-fs "^4.1.2" + read-package-tree@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" @@ -17512,6 +17576,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + readdirp@~3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" @@ -18600,7 +18671,7 @@ single-line-log@^1.1.2: dependencies: string-width "^1.0.1" -sinon@^9.1.0: +sinon@^9.1.0, sinon@^9.2.1: version "9.2.1" resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.1.tgz#64cc88beac718557055bd8caa526b34a2231be6d" integrity sha512-naPfsamB5KEE1aiioaoqJ6MEhdUs/2vtI5w1hPAXX/UwvoPjXcwh1m5HiKx0HGgKR8lQSoFIgY5jM6KK8VrS9w== @@ -20543,13 +20614,6 @@ util-extend@^1.0.1: resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" integrity sha1-p8IW0mdUUWljeztu3GypEZ4v+T8= -util-promisify@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/util-promisify/-/util-promisify-2.1.0.tgz#3c2236476c4d32c5ff3c47002add7c13b9a82a53" - integrity sha1-PCI2R2xNMsX/PEcAKt18E7moKlM= - dependencies: - object.getownpropertydescriptors "^2.0.3" - util.promisify@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"