From 64747dc4171364047be77421f892df3141febb6e Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Wed, 8 Dec 2021 04:25:28 -0800 Subject: [PATCH] move plugin management from ui-core to server-core Summary: Follow up of D32665064, this diff moves all plugin management logic from flipper-ui to flipper-server. Things like downloading, installing, querying new plugins. Loading plugins is handled separately in the next diff. Reviewed By: nikoant Differential Revision: D32666537 fbshipit-source-id: 9786b82987f00180bb26200e38735b334dc4d5c3 --- desktop/flipper-common/src/PluginDetails.ts | 89 ++++++++++- desktop/flipper-common/src/server-types.tsx | 24 ++- desktop/flipper-server-core/package.json | 2 + .../src/FlipperServerImpl.tsx | 21 +++ .../src/fb-stubs/constants.tsx | 10 ++ .../src/plugins/PluginManager.tsx | 147 ++++++++++++++++++ .../src/plugins}/loadDynamicPlugins.tsx | 4 +- .../src/utils/pathUtils.tsx | 40 +++++ desktop/flipper-server-core/tsconfig.json | 3 + desktop/flipper-ui-core/package.json | 1 - .../chrome/plugin-manager/PluginInstaller.tsx | 38 +++-- .../plugin-manager/PluginPackageInstaller.tsx | 7 +- .../__tests__/PluginInstaller.node.tsx | 19 ++- .../src/dispatcher/__tests__/plugins.node.tsx | 12 +- .../src/dispatcher/pluginDownloads.tsx | 89 ++--------- .../src/dispatcher/pluginManager.tsx | 20 +-- .../src/dispatcher/plugins.tsx | 28 ++-- .../src/reducers/pluginDownloads.tsx | 28 ++-- .../flipper-ui-core/src/utils/pathUtils.tsx | 6 + desktop/flipper-ui-core/tsconfig.json | 3 - desktop/plugin-lib/src/getPluginDetails.ts | 96 +----------- desktop/plugin-lib/src/getUpdatablePlugins.ts | 20 +-- desktop/plugin-lib/src/pluginInstaller.ts | 6 +- desktop/scripts/tsc-plugins.ts | 2 +- desktop/scripts/workspaces.ts | 2 +- 25 files changed, 441 insertions(+), 276 deletions(-) create mode 100644 desktop/flipper-server-core/src/fb-stubs/constants.tsx create mode 100644 desktop/flipper-server-core/src/plugins/PluginManager.tsx rename desktop/{flipper-ui-core/src/utils => flipper-server-core/src/plugins}/loadDynamicPlugins.tsx (96%) create mode 100644 desktop/flipper-server-core/src/utils/pathUtils.tsx diff --git a/desktop/flipper-common/src/PluginDetails.ts b/desktop/flipper-common/src/PluginDetails.ts index 7b33bcac3..6adf9c51b 100644 --- a/desktop/flipper-common/src/PluginDetails.ts +++ b/desktop/flipper-common/src/PluginDetails.ts @@ -98,4 +98,91 @@ export interface DownloadablePluginDetails extends ConcretePluginDetails { lastUpdated: Date; } -export default PluginDetails; +export type UpdateResult = + | {kind: 'not-installed'; version: string} + | {kind: 'up-to-date'} + | {kind: 'error'; error: Error} + | {kind: 'update-available'; version: string}; + +export type UpdatablePlugin = { + updateStatus: UpdateResult; +}; + +export type UpdatablePluginDetails = InstalledPluginDetails & UpdatablePlugin; + +export function getPluginDetails(packageJson: any): PluginDetails { + const specVersion = + packageJson.$schema && + packageJson.$schema === + 'https://fbflipper.com/schemas/plugin-package/v2.json' + ? 2 + : 1; + switch (specVersion) { + case 1: + return getPluginDetailsV1(packageJson); + case 2: + return getPluginDetailsV2(packageJson); + default: + throw new Error(`Unknown plugin format version: ${specVersion}`); + } +} + +// Plugins packaged using V1 are distributed as sources and compiled in run-time. +function getPluginDetailsV1(packageJson: any): PluginDetails { + return { + specVersion: 1, + name: packageJson.name, + version: packageJson.version, + main: 'dist/bundle.js', + source: packageJson.main, + id: packageJson.name, + gatekeeper: packageJson.gatekeeper, + icon: packageJson.icon, + title: packageJson.title || packageJson.name, + description: packageJson.description, + category: packageJson.category, + bugs: packageJson.bugs, + flipperSDKVersion: packageJson?.peerDependencies?.['flipper-plugin'], + pluginType: packageJson?.pluginType, + supportedDevices: packageJson?.supportedDevices, + supportedApps: packageJson?.supportedApps, + engines: packageJson.engines, + }; +} + +// Plugins packaged using V2 are pre-bundled, so compilation in run-time is not required for them. +function getPluginDetailsV2(packageJson: any): PluginDetails { + return { + specVersion: 2, + name: packageJson.name, + version: packageJson.version, + main: packageJson.main, + source: packageJson.flipperBundlerEntry, + id: packageJson.id || packageJson.name, + gatekeeper: packageJson.gatekeeper, + icon: packageJson.icon, + title: + packageJson.title || packageJson.id || getTitleFromName(packageJson.name), + description: packageJson.description, + category: packageJson.category, + bugs: packageJson.bugs, + flipperSDKVersion: packageJson?.peerDependencies?.['flipper-plugin'], + pluginType: packageJson?.pluginType, + supportedDevices: packageJson?.supportedDevices, + supportedApps: packageJson?.supportedApps, + engines: packageJson.engines, + publishedDocs: packageJson.publishedDocs, + }; +} + +function getTitleFromName(name: string): string { + const prefix = 'flipper-plugin-'; + if (name.startsWith(prefix)) { + return name.substr(prefix.length); + } + return name; +} + +export function isPluginJson(packageJson: any): boolean { + return packageJson?.keywords?.includes('flipper-plugin'); +} diff --git a/desktop/flipper-common/src/server-types.tsx b/desktop/flipper-common/src/server-types.tsx index e76a30f59..4b8b4a57e 100644 --- a/desktop/flipper-common/src/server-types.tsx +++ b/desktop/flipper-common/src/server-types.tsx @@ -7,7 +7,15 @@ * @format */ -import {DeviceSpec, DeviceType, OS as PluginOS} from './PluginDetails'; +import { + BundledPluginDetails, + DeviceSpec, + DeviceType, + DownloadablePluginDetails, + InstalledPluginDetails, + OS as PluginOS, + UpdatablePluginDetails, +} from './PluginDetails'; import {LauncherSettings, ProcessConfig, Settings} from './settings'; // In the future, this file would deserve it's own package, as it doesn't really relate to plugins. @@ -152,6 +160,20 @@ export type FlipperServerCommands = { 'keychain-write': (service: string, token: string) => Promise; 'keychain-read': (service: string) => Promise; 'keychain-unset': (service: string) => Promise; + 'plugins-load-dynamic-plugins': () => Promise; + 'plugins-get-bundled-plugins': () => Promise; + 'plugins-get-installed-plugins': () => Promise; + 'plugins-get-updatable-plugins': ( + query: string | undefined, + ) => Promise; + 'plugin-start-download': ( + plugin: DownloadablePluginDetails, + ) => Promise; + 'plugins-install-from-npm': (name: string) => Promise; + 'plugins-install-from-file': ( + path: string, + ) => Promise; + 'plugins-remove-plugins': (names: string[]) => Promise; }; export type ENVIRONMENT_VARIABLES = diff --git a/desktop/flipper-server-core/package.json b/desktop/flipper-server-core/package.json index 2764a6561..d37e723a9 100644 --- a/desktop/flipper-server-core/package.json +++ b/desktop/flipper-server-core/package.json @@ -11,11 +11,13 @@ "bugs": "https://github.com/facebook/flipper/issues", "dependencies": { "@iarna/toml": "^2.2.5", + "axios": "^0.22.0", "JSONStream": "^1.3.1", "adbkit": "^2.11.1", "adbkit-logcat": "^2.0.1", "archiver": "^5.3.0", "async-mutex": "^0.3.2", + "flipper-plugin-lib": "0.0.0", "flipper-common": "0.0.0", "fs-extra": "^10.0.0", "invariant": "^2.2.4", diff --git a/desktop/flipper-server-core/src/FlipperServerImpl.tsx b/desktop/flipper-server-core/src/FlipperServerImpl.tsx index f5f4d79df..49e49cf77 100644 --- a/desktop/flipper-server-core/src/FlipperServerImpl.tsx +++ b/desktop/flipper-server-core/src/FlipperServerImpl.tsx @@ -34,6 +34,7 @@ import {setFlipperServerConfig} from './FlipperServerConfig'; import {saveSettings} from './utils/settings'; import {saveLauncherSettings} from './utils/launcherSettings'; import {KeytarManager} from './utils/keytar'; +import {PluginManager} from './plugins/PluginManager'; /** * FlipperServer takes care of all incoming device & client connections. @@ -53,6 +54,7 @@ export class FlipperServerImpl implements FlipperServer { android: AndroidDeviceManager; ios: IOSDeviceManager; keytarManager: KeytarManager; + pluginManager: PluginManager; constructor( public config: FlipperServerConfig, @@ -64,6 +66,9 @@ export class FlipperServerImpl implements FlipperServer { this.android = new AndroidDeviceManager(this); this.ios = new IOSDeviceManager(this); this.keytarManager = new KeytarManager(keytarModule); + // TODO: given flipper-dump, it might make more sense to have the plugin command + // handled by moved to flipper-server & app, but let's keep things simple for now + this.pluginManager = new PluginManager(); server.addListener('error', (err) => { this.emit('server-error', err); @@ -122,6 +127,7 @@ export class FlipperServerImpl implements FlipperServer { try { await this.server.init(); + await this.pluginManager.start(); await this.startDeviceListeners(); this.setServerState('started'); } catch (e) { @@ -247,6 +253,21 @@ export class FlipperServerImpl implements FlipperServer { 'keychain-write': (service, password) => this.keytarManager.writeKeychain(service, password), 'keychain-unset': (service) => this.keytarManager.unsetKeychain(service), + 'plugins-load-dynamic-plugins': () => + this.pluginManager.loadDynamicPlugins(), + 'plugins-get-bundled-plugins': () => this.pluginManager.getBundledPlugins(), + 'plugins-get-installed-plugins': () => + this.pluginManager.getInstalledPlugins(), + 'plugins-remove-plugins': (plugins) => + this.pluginManager.removePlugins(plugins), + 'plugin-start-download': (details) => + this.pluginManager.downloadPlugin(details), + 'plugins-get-updatable-plugins': (query) => + this.pluginManager.getUpdatablePlugins(query), + 'plugins-install-from-file': (path) => + this.pluginManager.installPluginFromFile(path), + 'plugins-install-from-npm': (name) => + this.pluginManager.installPluginFromNpm(name), }; registerDevice(device: ServerDevice) { diff --git a/desktop/flipper-server-core/src/fb-stubs/constants.tsx b/desktop/flipper-server-core/src/fb-stubs/constants.tsx new file mode 100644 index 000000000..1d093647f --- /dev/null +++ b/desktop/flipper-server-core/src/fb-stubs/constants.tsx @@ -0,0 +1,10 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +export const isFBBuild: boolean = false; diff --git a/desktop/flipper-server-core/src/plugins/PluginManager.tsx b/desktop/flipper-server-core/src/plugins/PluginManager.tsx new file mode 100644 index 000000000..c7a8d6dcf --- /dev/null +++ b/desktop/flipper-server-core/src/plugins/PluginManager.tsx @@ -0,0 +1,147 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import fs from 'fs-extra'; +import path from 'path'; +import tmp from 'tmp'; +import {promisify} from 'util'; +import {default as axios} from 'axios'; +import { + BundledPluginDetails, + DownloadablePluginDetails, + InstalledPluginDetails, +} from 'flipper-common'; +import {getStaticPath} from '../utils/pathUtils'; +import {loadDynamicPlugins} from './loadDynamicPlugins'; +import { + cleanupOldInstalledPluginVersions, + getInstalledPluginDetails, + getInstalledPlugins, + getPluginVersionInstallationDir, + installPluginFromFile, + removePlugins, + getUpdatablePlugins, + getInstalledPlugin, + installPluginFromNpm, +} from 'flipper-plugin-lib'; + +const maxInstalledPluginVersionsToKeep = 2; + +// Adapter which forces node.js implementation for axios instead of browser implementation +// used by default in Electron. Node.js implementation is better, because it +// supports streams which can be used for direct downloading to disk. +const axiosHttpAdapter = require('axios/lib/adapters/http'); // eslint-disable-line import/no-commonjs + +const getTempDirName = promisify(tmp.dir) as ( + options?: tmp.DirOptions, +) => Promise; + +export class PluginManager { + async start() { + // This needn't happen immediately and is (light) I/O work. + (window.requestIdleCallback || setImmediate)(() => { + cleanupOldInstalledPluginVersions(maxInstalledPluginVersionsToKeep).catch( + (err) => + console.error('Failed to clean up old installed plugins:', err), + ); + }); + } + + loadDynamicPlugins = loadDynamicPlugins; + getInstalledPlugins = getInstalledPlugins; + removePlugins = removePlugins; + getUpdatablePlugins = getUpdatablePlugins; + getInstalledPlugin = getInstalledPlugin; + installPluginFromFile = installPluginFromFile; + installPluginFromNpm = installPluginFromNpm; + + async getBundledPlugins(): Promise> { + if (process.env.NODE_ENV === 'test') { + return []; + } + // defaultPlugins that are included in the Flipper distributive. + // List of default bundled plugins is written at build time to defaultPlugins/bundled.json. + const pluginPath = getStaticPath( + path.join('defaultPlugins', 'bundled.json'), + {asarUnpacked: true}, + ); + let bundledPlugins: Array = []; + try { + bundledPlugins = await fs.readJson(pluginPath); + } catch (e) { + console.error('Failed to load list of bundled plugins', e); + } + return bundledPlugins; + } + + async downloadPlugin( + plugin: DownloadablePluginDetails, + ): Promise { + const {name, title, version, downloadUrl} = plugin; + const installationDir = getPluginVersionInstallationDir(name, version); + console.log( + `Downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`, + ); + const tmpDir = await getTempDirName(); + const tmpFile = path.join(tmpDir, `${name}-${version}.tgz`); + try { + const cancelationSource = axios.CancelToken.source(); + if (await fs.pathExists(installationDir)) { + console.log( + `Using existing files instead of downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}"`, + ); + return await getInstalledPluginDetails(installationDir); + } else { + await fs.ensureDir(tmpDir); + let percentCompleted = 0; + const response = await axios.get(plugin.downloadUrl, { + adapter: axiosHttpAdapter, + cancelToken: cancelationSource.token, + responseType: 'stream', + headers: { + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-Mode': 'navigate', + }, + onDownloadProgress: async (progressEvent) => { + const newPercentCompleted = !progressEvent.total + ? 0 + : Math.round((progressEvent.loaded * 100) / progressEvent.total); + if (newPercentCompleted - percentCompleted >= 20) { + percentCompleted = newPercentCompleted; + console.log( + `Downloading plugin "${title}" v${version} from "${downloadUrl}": ${percentCompleted}% completed (${progressEvent.loaded} from ${progressEvent.total})`, + ); + } + }, + }); + if (response.headers['content-type'] !== 'application/octet-stream') { + throw new Error( + `It looks like you are not on VPN/Lighthouse. Unexpected content type received: ${response.headers['content-type']}.`, + ); + } + const responseStream = response.data as fs.ReadStream; + const writeStream = responseStream.pipe( + fs.createWriteStream(tmpFile, {autoClose: true}), + ); + await new Promise((resolve, reject) => + writeStream.once('finish', resolve).once('error', reject), + ); + return await installPluginFromFile(tmpFile); + } + } catch (error) { + console.error( + `Failed to download plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`, + error, + ); + throw error; + } finally { + await fs.remove(tmpDir); + } + } +} diff --git a/desktop/flipper-ui-core/src/utils/loadDynamicPlugins.tsx b/desktop/flipper-server-core/src/plugins/loadDynamicPlugins.tsx similarity index 96% rename from desktop/flipper-ui-core/src/utils/loadDynamicPlugins.tsx rename to desktop/flipper-server-core/src/plugins/loadDynamicPlugins.tsx index 8a0b21026..290eb3d7a 100644 --- a/desktop/flipper-ui-core/src/utils/loadDynamicPlugins.tsx +++ b/desktop/flipper-server-core/src/plugins/loadDynamicPlugins.tsx @@ -20,9 +20,7 @@ import {getStaticPath} from '../utils/pathUtils'; // Load "dynamic" plugins, e.g. those which are either pre-installed (default), installed or loaded from sources (for development). // This opposed to "bundled" plugins which are included into Flipper bundle. -export default async function loadDynamicPlugins(): Promise< - InstalledPluginDetails[] -> { +export async function loadDynamicPlugins(): Promise { if (process.env.NODE_ENV === 'test') { return []; } diff --git a/desktop/flipper-server-core/src/utils/pathUtils.tsx b/desktop/flipper-server-core/src/utils/pathUtils.tsx new file mode 100644 index 000000000..b8cdcf33b --- /dev/null +++ b/desktop/flipper-server-core/src/utils/pathUtils.tsx @@ -0,0 +1,40 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +// We use sync access once per startup. +/* eslint-disable node/no-sync */ + +import path from 'path'; +import fs from 'fs'; +import {getFlipperServerConfig} from '../FlipperServerConfig'; +import {isFBBuild} from '../fb-stubs/constants'; + +export function getStaticPath( + relativePath: string = '.', + {asarUnpacked}: {asarUnpacked: boolean} = {asarUnpacked: false}, +) { + const staticDir = getFlipperServerConfig().paths.staticPath; + const absolutePath = path.resolve(staticDir, relativePath); + // Unfortunately, path.resolve, fs.pathExists, fs.read etc do not automatically work with asarUnpacked files. + // All these functions still look for files in "app.asar" even if they are unpacked. + // Looks like automatic resolving for asarUnpacked files only work for "child_process" module. + // So we're using a hack here to actually look to "app.asar.unpacked" dir instead of app.asar package. + return asarUnpacked + ? absolutePath.replace('app.asar', 'app.asar.unpacked') + : absolutePath; +} + +export function getChangelogPath() { + const changelogPath = getStaticPath(isFBBuild ? 'facebook' : '.'); + if (fs.existsSync(changelogPath)) { + return changelogPath; + } else { + throw new Error('Changelog path path does not exist: ' + changelogPath); + } +} diff --git a/desktop/flipper-server-core/tsconfig.json b/desktop/flipper-server-core/tsconfig.json index 4bb952b9f..8468e4285 100644 --- a/desktop/flipper-server-core/tsconfig.json +++ b/desktop/flipper-server-core/tsconfig.json @@ -7,6 +7,9 @@ "references": [ { "path": "../flipper-common" + }, + { + "path": "../plugin-lib" } ] } diff --git a/desktop/flipper-ui-core/package.json b/desktop/flipper-ui-core/package.json index a940ca78d..974b40eac 100644 --- a/desktop/flipper-ui-core/package.json +++ b/desktop/flipper-ui-core/package.json @@ -33,7 +33,6 @@ "flipper-common": "0.0.0", "flipper-doctor": "0.0.0", "flipper-plugin": "0.0.0", - "flipper-plugin-lib": "0.0.0", "flipper-ui-core": "0.0.0", "fs-extra": "^10.0.0", "immer": "^9.0.6", diff --git a/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginInstaller.tsx b/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginInstaller.tsx index cfed76ef6..a371254c9 100644 --- a/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginInstaller.tsx +++ b/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginInstaller.tsx @@ -14,23 +14,18 @@ import { reportPlatformFailures, reportUsage, InstalledPluginDetails, + UpdateResult, + UpdatablePluginDetails, } from 'flipper-common'; import reloadFlipper from '../../utils/reloadFlipper'; import {registerInstalledPlugins} from '../../reducers/plugins'; -import { - UpdateResult, - getInstalledPlugins, - getUpdatablePlugins, - removePlugin, - UpdatablePluginDetails, -} from 'flipper-plugin-lib'; -import {installPluginFromNpm} from 'flipper-plugin-lib'; import {State as AppState} from '../../reducers'; import {connect} from 'react-redux'; import {Dispatch, Action} from 'redux'; import PluginPackageInstaller from './PluginPackageInstaller'; import {Toolbar} from 'flipper-plugin'; import {Alert, Button, Input, Tooltip, Typography} from 'antd'; +import {getRenderHostInstance} from '../../RenderHost'; const {Text, Link} = Typography; @@ -327,8 +322,33 @@ export default connect( }), (dispatch: Dispatch>) => ({ refreshInstalledPlugins: async () => { - const plugins = await getInstalledPlugins(); + const plugins = await await getRenderHostInstance().flipperServer!.exec( + 'plugins-get-installed-plugins', + ); dispatch(registerInstalledPlugins(plugins)); }, }), )(PluginInstaller); + +async function installPluginFromNpm( + name: string, +): Promise { + return await getRenderHostInstance().flipperServer!.exec( + 'plugins-install-from-npm', + name, + ); +} + +async function removePlugin(name: string) { + return await getRenderHostInstance().flipperServer!.exec( + 'plugins-remove-plugins', + [name], + ); +} + +async function getUpdatablePlugins(query: string | undefined) { + return await getRenderHostInstance().flipperServer!.exec( + 'plugins-get-updatable-plugins', + query, + ); +} diff --git a/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginPackageInstaller.tsx b/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginPackageInstaller.tsx index d1823c91a..1be26bd0c 100644 --- a/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginPackageInstaller.tsx +++ b/desktop/flipper-ui-core/src/chrome/plugin-manager/PluginPackageInstaller.tsx @@ -17,8 +17,8 @@ import { } from '../../ui'; import styled from '@emotion/styled'; import React, {useState} from 'react'; -import {installPluginFromFile} from 'flipper-plugin-lib'; import {Toolbar, FileSelector} from 'flipper-plugin'; +import {getRenderHostInstance} from '../../RenderHost'; const CenteredGlyph = styled(Glyph)({ margin: 'auto', @@ -51,7 +51,10 @@ export default function PluginPackageInstaller({ setError(undefined); setInProgress(true); try { - await installPluginFromFile(path); + await getRenderHostInstance().flipperServer!.exec( + 'plugins-install-from-file', + path, + ); await onInstall(); } catch (e) { setError(e); diff --git a/desktop/flipper-ui-core/src/chrome/plugin-manager/__tests__/PluginInstaller.node.tsx b/desktop/flipper-ui-core/src/chrome/plugin-manager/__tests__/PluginInstaller.node.tsx index 5595ba2d9..b816040fb 100644 --- a/desktop/flipper-ui-core/src/chrome/plugin-manager/__tests__/PluginInstaller.node.tsx +++ b/desktop/flipper-ui-core/src/chrome/plugin-manager/__tests__/PluginInstaller.node.tsx @@ -7,19 +7,22 @@ * @format */ -jest.mock('flipper-plugin-lib'); - import {default as PluginInstaller} from '../PluginInstaller'; import React from 'react'; import {render, waitFor} from '@testing-library/react'; import configureStore from 'redux-mock-store'; import {Provider} from 'react-redux'; -import type {PluginDetails} from 'flipper-common'; -import {getUpdatablePlugins, UpdatablePluginDetails} from 'flipper-plugin-lib'; +import type {PluginDetails, UpdatablePluginDetails} from 'flipper-common'; import {Store} from '../../../reducers'; -import {mocked} from 'ts-jest/utils'; +import {getRenderHostInstance} from '../../../RenderHost'; -const getUpdatablePluginsMock = mocked(getUpdatablePlugins); +let getUpdatablePluginsMock: jest.Mock; + +beforeEach(() => { + // flipperServer get resets before each test, no need to do so explicitly + getUpdatablePluginsMock = getRenderHostInstance().flipperServer!.exec = + jest.fn(); +}); function getStore(installedPlugins: PluginDetails[] = []): Store { return configureStore([])({ @@ -70,10 +73,6 @@ const samplePluginDetails2: UpdatablePluginDetails = { const SEARCH_RESULTS = [samplePluginDetails1, samplePluginDetails2]; -afterEach(() => { - getUpdatablePluginsMock.mockClear(); -}); - test('load PluginInstaller list', async () => { getUpdatablePluginsMock.mockReturnValue(Promise.resolve(SEARCH_RESULTS)); const component = ( diff --git a/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx b/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx index 5eceaac02..afd999b92 100644 --- a/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx @@ -8,7 +8,6 @@ */ jest.mock('../../../../app/src/defaultPlugins'); -jest.mock('../../utils/loadDynamicPlugins'); import dispatcher, { getDynamicPlugins, checkDisabled, @@ -23,11 +22,9 @@ import {getLogger} from 'flipper-common'; import configureStore from 'redux-mock-store'; import TestPlugin from './TestPlugin'; import {_SandyPluginDefinition} from 'flipper-plugin'; -import {mocked} from 'ts-jest/utils'; -import loadDynamicPlugins from '../../utils/loadDynamicPlugins'; import {getRenderHostInstance} from '../../RenderHost'; -const loadDynamicPluginsMock = mocked(loadDynamicPlugins); +let loadDynamicPluginsMock: jest.Mock; const mockStore = configureStore([])( createRootReducer()(undefined, {type: 'INIT'}), @@ -56,13 +53,11 @@ const sampleBundledPluginDetails: BundledPluginDetails = { }; beforeEach(() => { + loadDynamicPluginsMock = getRenderHostInstance().flipperServer.exec = + jest.fn(); loadDynamicPluginsMock.mockResolvedValue([]); }); -afterEach(() => { - loadDynamicPluginsMock.mockClear(); -}); - test('dispatcher dispatches REGISTER_PLUGINS', async () => { await dispatcher(mockStore, logger); const actions = mockStore.getActions(); @@ -70,7 +65,6 @@ test('dispatcher dispatches REGISTER_PLUGINS', async () => { }); test('getDynamicPlugins returns empty array on errors', async () => { - const loadDynamicPluginsMock = mocked(loadDynamicPlugins); loadDynamicPluginsMock.mockRejectedValue(new Error('ooops')); const res = await getDynamicPlugins(); expect(res).toEqual([]); diff --git a/desktop/flipper-ui-core/src/dispatcher/pluginDownloads.tsx b/desktop/flipper-ui-core/src/dispatcher/pluginDownloads.tsx index 2a0eb5186..62329f80d 100644 --- a/desktop/flipper-ui-core/src/dispatcher/pluginDownloads.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/pluginDownloads.tsx @@ -7,15 +7,7 @@ * @format */ -import { - getInstalledPluginDetails, - getPluginVersionInstallationDir, - installPluginFromFile, -} from 'flipper-plugin-lib'; -import { - InstalledPluginDetails, - DownloadablePluginDetails, -} from 'flipper-common'; +import {DownloadablePluginDetails} from 'flipper-common'; import {State, Store} from '../reducers/index'; import { PluginDownloadStatus, @@ -23,25 +15,12 @@ import { pluginDownloadFinished, } from '../reducers/pluginDownloads'; import {sideEffect} from '../utils/sideEffect'; -import {default as axios} from 'axios'; -import fs from 'fs-extra'; -import path from 'path'; -import tmp from 'tmp'; -import {promisify} from 'util'; import {reportPlatformFailures, reportUsage} from 'flipper-common'; import {loadPlugin} from '../reducers/pluginManager'; import {showErrorNotification} from '../utils/notifications'; import {pluginInstalled} from '../reducers/plugins'; import {getAllClients} from '../reducers/connections'; - -// Adapter which forces node.js implementation for axios instead of browser implementation -// used by default in Electron. Node.js implementation is better, because it -// supports streams which can be used for direct downloading to disk. -const axiosHttpAdapter = require('axios/lib/adapters/http'); // eslint-disable-line import/no-commonjs - -const getTempDirName = promisify(tmp.dir) as ( - options?: tmp.DirOptions, -) => Promise; +import {getRenderHostInstance} from '../RenderHost'; export default (store: Store) => { sideEffect( @@ -79,61 +58,18 @@ async function handlePluginDownload( startedByUser: boolean, store: Store, ) { + const {title, version, downloadUrl} = plugin; const dispatch = store.dispatch; - const {name, title, version, downloadUrl} = plugin; - const installationDir = getPluginVersionInstallationDir(name, version); console.log( - `Downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`, + `Downloading plugin "${title}" v${version} from "${downloadUrl}".`, ); - const tmpDir = await getTempDirName(); - const tmpFile = path.join(tmpDir, `${name}-${version}.tgz`); - let installedPlugin: InstalledPluginDetails | undefined; try { - const cancelationSource = axios.CancelToken.source(); - dispatch(pluginDownloadStarted({plugin, cancel: cancelationSource.cancel})); - if (await fs.pathExists(installationDir)) { - console.log( - `Using existing files instead of downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}"`, - ); - installedPlugin = await getInstalledPluginDetails(installationDir); - } else { - await fs.ensureDir(tmpDir); - let percentCompleted = 0; - const response = await axios.get(plugin.downloadUrl, { - adapter: axiosHttpAdapter, - cancelToken: cancelationSource.token, - responseType: 'stream', - headers: { - 'Sec-Fetch-Site': 'none', - 'Sec-Fetch-Mode': 'navigate', - }, - onDownloadProgress: async (progressEvent) => { - const newPercentCompleted = !progressEvent.total - ? 0 - : Math.round((progressEvent.loaded * 100) / progressEvent.total); - if (newPercentCompleted - percentCompleted >= 20) { - percentCompleted = newPercentCompleted; - console.log( - `Downloading plugin "${title}" v${version} from "${downloadUrl}": ${percentCompleted}% completed (${progressEvent.loaded} from ${progressEvent.total})`, - ); - } - }, - }); - if (response.headers['content-type'] !== 'application/octet-stream') { - throw new Error( - `It looks like you are not on VPN/Lighthouse. Unexpected content type received: ${response.headers['content-type']}.`, - ); - } - const responseStream = response.data as fs.ReadStream; - const writeStream = responseStream.pipe( - fs.createWriteStream(tmpFile, {autoClose: true}), - ); - await new Promise((resolve, reject) => - writeStream.once('finish', resolve).once('error', reject), - ); - installedPlugin = await installPluginFromFile(tmpFile); - dispatch(pluginInstalled(installedPlugin)); - } + dispatch(pluginDownloadStarted({plugin})); + const installedPlugin = await getRenderHostInstance().flipperServer!.exec( + 'plugin-start-download', + plugin, + ); + dispatch(pluginInstalled(installedPlugin)); if (pluginIsDisabledForAllConnectedClients(store.getState(), plugin)) { dispatch( loadPlugin({ @@ -144,11 +80,11 @@ async function handlePluginDownload( ); } console.log( - `Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`, + `Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}".`, ); } catch (error) { console.error( - `Failed to download plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`, + `Failed to download plugin "${title}" v${version} from "${downloadUrl}".`, error, ); if (startedByUser) { @@ -160,7 +96,6 @@ async function handlePluginDownload( throw error; } finally { dispatch(pluginDownloadFinished({plugin})); - await fs.remove(tmpDir); } } diff --git a/desktop/flipper-ui-core/src/dispatcher/pluginManager.tsx b/desktop/flipper-ui-core/src/dispatcher/pluginManager.tsx index 697b9b9a6..67bdcc749 100644 --- a/desktop/flipper-ui-core/src/dispatcher/pluginManager.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/pluginManager.tsx @@ -17,11 +17,6 @@ import { SwitchPluginActionPayload, PluginCommand, } from '../reducers/pluginManager'; -import { - getInstalledPlugins, - cleanupOldInstalledPluginVersions, - removePlugins, -} from 'flipper-plugin-lib'; import {sideEffect} from '../utils/sideEffect'; import {requirePlugin} from './plugins'; import {showErrorNotification} from '../utils/notifications'; @@ -49,13 +44,18 @@ import { defaultEnabledBackgroundPlugins, } from '../utils/pluginUtils'; import {getPluginKey} from '../utils/pluginKey'; - -const maxInstalledPluginVersionsToKeep = 2; +import {getRenderHostInstance} from '../RenderHost'; async function refreshInstalledPlugins(store: Store) { - await removePlugins(store.getState().plugins.uninstalledPluginNames.values()); - await cleanupOldInstalledPluginVersions(maxInstalledPluginVersionsToKeep); - const plugins = await getInstalledPlugins(); + const flipperServer = getRenderHostInstance().flipperServer; + if (!flipperServer) { + throw new Error('Flipper Server not ready'); + } + await flipperServer.exec( + 'plugins-remove-plugins', + Array.from(store.getState().plugins.uninstalledPluginNames.values()), + ); + const plugins = await flipperServer.exec('plugins-get-installed-plugins'); return store.dispatch(registerInstalledPlugins(plugins)); } diff --git a/desktop/flipper-ui-core/src/dispatcher/plugins.tsx b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx index 2b1b7133e..43311dd84 100644 --- a/desktop/flipper-ui-core/src/dispatcher/plugins.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx @@ -8,7 +8,7 @@ */ import type {Store} from '../reducers/index'; -import type {Logger} from 'flipper-common'; +import type {InstalledPluginDetails, Logger} from 'flipper-common'; import {PluginDefinition} from '../plugin'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -25,8 +25,6 @@ import { pluginsInitialized, } from '../reducers/plugins'; import {FlipperBasePlugin} from '../plugin'; -import fs from 'fs-extra'; -import path from 'path'; import {notNull} from '../utils/typeUtils'; import { ActivatablePluginDetails, @@ -36,7 +34,6 @@ import { import {tryCatchReportPluginFailures, reportUsage} from 'flipper-common'; import * as FlipperPluginSDK from 'flipper-plugin'; import {_SandyPluginDefinition} from 'flipper-plugin'; -import loadDynamicPlugins from '../utils/loadDynamicPlugins'; import * as Immer from 'immer'; import * as antd from 'antd'; import * as emotion_styled from '@emotion/styled'; @@ -47,7 +44,6 @@ import * as crc32 from 'crc32'; import {isDevicePluginDefinition} from '../utils/pluginUtils'; import isPluginCompatible from '../utils/isPluginCompatible'; import isPluginVersionMoreRecent from '../utils/isPluginVersionMoreRecent'; -import {getStaticPath} from '../utils/pathUtils'; import {createSandyPluginWrapper} from '../utils/createSandyPluginWrapper'; import {getRenderHostInstance} from '../RenderHost'; let defaultPluginsIndex: any = null; @@ -152,25 +148,23 @@ async function getBundledPlugins(): Promise> { if (process.env.NODE_ENV === 'test') { return []; } - // defaultPlugins that are included in the Flipper distributive. - // List of default bundled plugins is written at build time to defaultPlugins/bundled.json. - const pluginPath = getStaticPath( - path.join('defaultPlugins', 'bundled.json'), - {asarUnpacked: true}, - ); - let bundledPlugins: Array = []; try { - bundledPlugins = await fs.readJson(pluginPath); + // defaultPlugins that are included in the Flipper distributive. + // List of default bundled plugins is written at build time to defaultPlugins/bundled.json. + return await getRenderHostInstance().flipperServer!.exec( + 'plugins-get-bundled-plugins', + ); } catch (e) { console.error('Failed to load list of bundled plugins', e); + return []; } - - return bundledPlugins; } -export async function getDynamicPlugins() { +export async function getDynamicPlugins(): Promise { try { - return await loadDynamicPlugins(); + return await getRenderHostInstance().flipperServer!.exec( + 'plugins-load-dynamic-plugins', + ); } catch (e) { console.error('Failed to load dynamic plugins', e); return []; diff --git a/desktop/flipper-ui-core/src/reducers/pluginDownloads.tsx b/desktop/flipper-ui-core/src/reducers/pluginDownloads.tsx index d593ddae6..d29efed7d 100644 --- a/desktop/flipper-ui-core/src/reducers/pluginDownloads.tsx +++ b/desktop/flipper-ui-core/src/reducers/pluginDownloads.tsx @@ -8,10 +8,8 @@ */ import {DownloadablePluginDetails} from 'flipper-common'; -import {getPluginVersionInstallationDir} from 'flipper-plugin-lib'; import {Actions} from '.'; import produce from 'immer'; -import {Canceler} from 'axios'; export enum PluginDownloadStatus { QUEUED = 'Queued', @@ -24,7 +22,7 @@ export type DownloadablePluginState = { startedByUser: boolean; } & ( | {status: PluginDownloadStatus.QUEUED} - | {status: PluginDownloadStatus.STARTED; cancel: Canceler} + | {status: PluginDownloadStatus.STARTED} ); // We use plugin installation path as key as it is unique for each plugin version. @@ -42,7 +40,6 @@ export type PluginDownloadStarted = { type: 'PLUGIN_DOWNLOAD_STARTED'; payload: { plugin: DownloadablePluginDetails; - cancel: Canceler; }; }; @@ -67,10 +64,7 @@ export default function reducer( switch (action.type) { case 'PLUGIN_DOWNLOAD_START': { const {plugin, startedByUser} = action.payload; - const installationDir = getPluginVersionInstallationDir( - plugin.name, - plugin.version, - ); + const installationDir = getDownloadKey(plugin.name, plugin.version); const downloadState = state[installationDir]; if (downloadState) { // If download is already in progress - re-use the existing state. @@ -90,11 +84,8 @@ export default function reducer( }); } case 'PLUGIN_DOWNLOAD_STARTED': { - const {plugin, cancel} = action.payload; - const installationDir = getPluginVersionInstallationDir( - plugin.name, - plugin.version, - ); + const {plugin} = action.payload; + const installationDir = getDownloadKey(plugin.name, plugin.version); const downloadState = state[installationDir]; if (downloadState?.status !== PluginDownloadStatus.QUEUED) { console.warn( @@ -107,16 +98,12 @@ export default function reducer( status: PluginDownloadStatus.STARTED, plugin, startedByUser: downloadState.startedByUser, - cancel, }; }); } case 'PLUGIN_DOWNLOAD_FINISHED': { const {plugin} = action.payload; - const installationDir = getPluginVersionInstallationDir( - plugin.name, - plugin.version, - ); + const installationDir = getDownloadKey(plugin.name, plugin.version); return produce(state, (draft) => { delete draft[installationDir]; }); @@ -136,9 +123,12 @@ export const startPluginDownload = (payload: { export const pluginDownloadStarted = (payload: { plugin: DownloadablePluginDetails; - cancel: Canceler; }): Action => ({type: 'PLUGIN_DOWNLOAD_STARTED', payload}); export const pluginDownloadFinished = (payload: { plugin: DownloadablePluginDetails; }): Action => ({type: 'PLUGIN_DOWNLOAD_FINISHED', payload}); + +function getDownloadKey(name: string, version: string) { + return name.replace('/', '__') + '@' + version; +} diff --git a/desktop/flipper-ui-core/src/utils/pathUtils.tsx b/desktop/flipper-ui-core/src/utils/pathUtils.tsx index c122fcf06..2be659450 100644 --- a/desktop/flipper-ui-core/src/utils/pathUtils.tsx +++ b/desktop/flipper-ui-core/src/utils/pathUtils.tsx @@ -16,6 +16,9 @@ import fs from 'fs'; import config from '../fb-stubs/config'; import {getRenderHostInstance} from '../RenderHost'; +/** + * @deprecated + */ export function getStaticPath( relativePath: string = '.', {asarUnpacked}: {asarUnpacked: boolean} = {asarUnpacked: false}, @@ -31,6 +34,9 @@ export function getStaticPath( : absolutePath; } +/** + * @deprecated + */ export function getChangelogPath() { const changelogPath = getStaticPath(config.isFBBuild ? 'facebook' : '.'); if (fs.existsSync(changelogPath)) { diff --git a/desktop/flipper-ui-core/tsconfig.json b/desktop/flipper-ui-core/tsconfig.json index ef90c5401..00bb977a9 100644 --- a/desktop/flipper-ui-core/tsconfig.json +++ b/desktop/flipper-ui-core/tsconfig.json @@ -16,9 +16,6 @@ { "path": "../flipper-plugin" }, - { - "path": "../plugin-lib" - }, { "path": "../test-utils" } diff --git a/desktop/plugin-lib/src/getPluginDetails.ts b/desktop/plugin-lib/src/getPluginDetails.ts index 20d5a99bf..f1cf83348 100644 --- a/desktop/plugin-lib/src/getPluginDetails.ts +++ b/desktop/plugin-lib/src/getPluginDetails.ts @@ -10,9 +10,9 @@ import fs from 'fs-extra'; import path from 'path'; import { - DownloadablePluginDetails, + getPluginDetails, InstalledPluginDetails, - PluginDetails, + isPluginJson, } from 'flipper-common'; import {pluginCacheDir} from './pluginPaths'; @@ -26,10 +26,6 @@ export async function readPluginPackageJson(dir: string): Promise { } } -export function isPluginJson(packageJson: any): boolean { - return packageJson?.keywords?.includes('flipper-plugin'); -} - export async function isPluginDir(dir: string): Promise { const packageJsonPath = path.join(dir, 'package.json'); const json = (await fs.pathExists(packageJsonPath)) @@ -40,23 +36,6 @@ export async function isPluginDir(dir: string): Promise { return isPluginJson(json); } -export function getPluginDetails(packageJson: any): PluginDetails { - const specVersion = - packageJson.$schema && - packageJson.$schema === - 'https://fbflipper.com/schemas/plugin-package/v2.json' - ? 2 - : 1; - switch (specVersion) { - case 1: - return getPluginDetailsV1(packageJson); - case 2: - return getPluginDetailsV2(packageJson); - default: - throw new Error(`Unknown plugin format version: ${specVersion}`); - } -} - export async function getInstalledPluginDetails( dir: string, packageJson?: any, @@ -92,74 +71,3 @@ export async function getInstalledPluginDetails( entry, }; } - -export function getDownloadablePluginDetails( - packageJson: any, - downloadUrl: string, - lastUpdated: Date, -): DownloadablePluginDetails { - const details = getPluginDetails(packageJson); - return { - ...details, - isBundled: false, - isActivatable: false, - downloadUrl, - lastUpdated, - }; -} - -// Plugins packaged using V1 are distributed as sources and compiled in run-time. -function getPluginDetailsV1(packageJson: any): PluginDetails { - return { - specVersion: 1, - name: packageJson.name, - version: packageJson.version, - main: 'dist/bundle.js', - source: packageJson.main, - id: packageJson.name, - gatekeeper: packageJson.gatekeeper, - icon: packageJson.icon, - title: packageJson.title || packageJson.name, - description: packageJson.description, - category: packageJson.category, - bugs: packageJson.bugs, - flipperSDKVersion: packageJson?.peerDependencies?.['flipper-plugin'], - pluginType: packageJson?.pluginType, - supportedDevices: packageJson?.supportedDevices, - supportedApps: packageJson?.supportedApps, - engines: packageJson.engines, - }; -} - -// Plugins packaged using V2 are pre-bundled, so compilation in run-time is not required for them. -function getPluginDetailsV2(packageJson: any): PluginDetails { - return { - specVersion: 2, - name: packageJson.name, - version: packageJson.version, - main: packageJson.main, - source: packageJson.flipperBundlerEntry, - id: packageJson.id || packageJson.name, - gatekeeper: packageJson.gatekeeper, - icon: packageJson.icon, - title: - packageJson.title || packageJson.id || getTitleFromName(packageJson.name), - description: packageJson.description, - category: packageJson.category, - bugs: packageJson.bugs, - flipperSDKVersion: packageJson?.peerDependencies?.['flipper-plugin'], - pluginType: packageJson?.pluginType, - supportedDevices: packageJson?.supportedDevices, - supportedApps: packageJson?.supportedApps, - engines: packageJson.engines, - publishedDocs: packageJson.publishedDocs, - }; -} - -function getTitleFromName(name: string): string { - const prefix = 'flipper-plugin-'; - if (name.startsWith(prefix)) { - return name.substr(prefix.length); - } - return name; -} diff --git a/desktop/plugin-lib/src/getUpdatablePlugins.ts b/desktop/plugin-lib/src/getUpdatablePlugins.ts index 9b0123dca..164975889 100644 --- a/desktop/plugin-lib/src/getUpdatablePlugins.ts +++ b/desktop/plugin-lib/src/getUpdatablePlugins.ts @@ -7,29 +7,21 @@ * @format */ -import {InstalledPluginDetails} from 'flipper-common'; +import { + UpdatablePluginDetails, + UpdateResult, + getPluginDetails, +} from 'flipper-common'; import {getInstalledPlugins} from './pluginInstaller'; import semver from 'semver'; import {getNpmHostedPlugins, NpmPackageDescriptor} from './getNpmHostedPlugins'; import NpmApi from 'npm-api'; -import {getInstalledPluginDetails, getPluginDetails} from './getPluginDetails'; +import {getInstalledPluginDetails} from './getPluginDetails'; import {getPluginVersionInstallationDir} from './pluginPaths'; import pmap from 'p-map'; import {notNull} from './typeUtils'; const npmApi = new NpmApi(); -export type UpdateResult = - | {kind: 'not-installed'; version: string} - | {kind: 'up-to-date'} - | {kind: 'error'; error: Error} - | {kind: 'update-available'; version: string}; - -export type UpdatablePlugin = { - updateStatus: UpdateResult; -}; - -export type UpdatablePluginDetails = InstalledPluginDetails & UpdatablePlugin; - export async function getUpdatablePlugins( query?: string, ): Promise { diff --git a/desktop/plugin-lib/src/pluginInstaller.ts b/desktop/plugin-lib/src/pluginInstaller.ts index c977b6b61..9d43f5786 100644 --- a/desktop/plugin-lib/src/pluginInstaller.ts +++ b/desktop/plugin-lib/src/pluginInstaller.ts @@ -109,7 +109,7 @@ export async function installPluginFromNpm(name: string) { tmpDir, getPluginDirNameFromPackageName(name), ); - await installPluginFromTempDir(pluginTempDir); + return await installPluginFromTempDir(pluginTempDir); } finally { await fs.remove(tmpDir); } @@ -137,9 +137,7 @@ export async function removePlugin(name: string): Promise { await fs.remove(getPluginInstallationDir(name)); } -export async function removePlugins( - names: IterableIterator, -): Promise { +export async function removePlugins(names: Array): Promise { await pmap(names, (name) => removePlugin(name)); } diff --git a/desktop/scripts/tsc-plugins.ts b/desktop/scripts/tsc-plugins.ts index 776c62494..98d3ae400 100644 --- a/desktop/scripts/tsc-plugins.ts +++ b/desktop/scripts/tsc-plugins.ts @@ -16,7 +16,7 @@ import {EOL} from 'os'; import pmap from 'p-map'; import {rootDir} from './paths'; import yargs from 'yargs'; -import {isPluginJson} from 'flipper-plugin-lib'; +import {isPluginJson} from 'flipper-common'; const argv = yargs .usage('yarn tsc-plugins [args]') diff --git a/desktop/scripts/workspaces.ts b/desktop/scripts/workspaces.ts index 78c087159..427260965 100644 --- a/desktop/scripts/workspaces.ts +++ b/desktop/scripts/workspaces.ts @@ -15,7 +15,7 @@ import globImport from 'glob'; import pfilter from 'p-filter'; import pmap from 'p-map'; import {execSync} from 'child_process'; -import {isPluginJson} from 'flipper-plugin-lib'; +import {isPluginJson} from 'flipper-common'; const glob = promisify(globImport); export interface Package {