diff --git a/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx b/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx index 703bae5bb..09553cc3b 100644 --- a/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx +++ b/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx @@ -7,7 +7,7 @@ * @format */ -import {PluginDefinition} from '../../dispatcher/plugins'; +import {PluginDetails} from 'flipper-plugin-lib'; import Client from '../../Client'; import {TableBodyRow} from '../../ui/components/table/types'; import React, {Component, Fragment} from 'react'; @@ -46,9 +46,9 @@ const Lamp = (props: {on: boolean}) => ( ); type StateFromProps = { - gatekeepedPlugins: Array; - disabledPlugins: Array; - failedPlugins: Array<[PluginDefinition, string]>; + gatekeepedPlugins: Array; + disabledPlugins: Array; + failedPlugins: Array<[PluginDetails, string]>; clients: Array; selectedDevice: string | null | undefined; devicePlugins: Array; @@ -101,7 +101,7 @@ class PluginDebugger extends Component { loaded: boolean, status: string, GKname: string | null | undefined, - pluginPath: string | null | undefined, + pluginPath: string, ): TableBodyRow { return { key: name.toLowerCase(), @@ -123,12 +123,10 @@ class PluginDebugger extends Component { value: this.getSupportedClients(name), }, source: { - value: pluginPath ? ( + value: ( {pluginPath} - ) : ( - bundled ), }, }, @@ -149,7 +147,7 @@ class PluginDebugger extends Component { getRows(): Array { const rows: Array = []; - const externalPluginPath = (p: any) => p.entry || 'Native Plugin'; + const externalPluginPath = (p: any) => (p.isDefault ? 'bundled' : p.entry); this.props.gatekeepedPlugins.forEach((plugin) => rows.push( diff --git a/desktop/app/src/chrome/plugin-manager/PluginInstaller.tsx b/desktop/app/src/chrome/plugin-manager/PluginInstaller.tsx index a836aa219..8725c3a9b 100644 --- a/desktop/app/src/chrome/plugin-manager/PluginInstaller.tsx +++ b/desktop/app/src/chrome/plugin-manager/PluginInstaller.tsx @@ -29,18 +29,16 @@ import React, {useCallback, useState, useMemo, useEffect} from 'react'; import {List} from 'immutable'; import {SearchIndex} from 'algoliasearch'; import {SearchResponse} from '@algolia/client-search'; -import path from 'path'; -import fs from 'fs-extra'; import {reportPlatformFailures, reportUsage} from '../../utils/metrics'; import restartFlipper from '../../utils/restartFlipper'; +import {registerInstalledPlugins} from '../../reducers/pluginManager'; import { - PluginMap, - PluginDefinition, - registerInstalledPlugins, -} from '../../reducers/pluginManager'; -import { - PLUGIN_DIR, readInstalledPlugins, + removePlugin, + PluginMap, + PluginDetails, +} from 'flipper-plugin-lib'; +import { provideSearchIndex, findPluginUpdates as _findPluginUpdates, UpdateResult, @@ -126,11 +124,11 @@ type UpdatablePlugin = { updateStatus: UpdateResult; }; -type UpdatablePluginDefinition = PluginDefinition & UpdatablePlugin; +type UpdatablePluginDefinition = PluginDetails & UpdatablePlugin; // exported for testing export function annotatePluginsWithUpdates( - installedPlugins: Map, + installedPlugins: PluginMap, updates: Map, ): Map { const annotated: Array<[string, UpdatablePluginDefinition]> = Array.from( @@ -211,7 +209,7 @@ const AlignedGlyph = styled(Glyph)({ marginTop: 6, }); -function liftUpdatable(val: PluginDefinition): UpdatablePluginDefinition { +function liftUpdatable(val: PluginDetails): UpdatablePluginDefinition { return { ...val, updateStatus: {kind: 'up-to-date'}, @@ -272,7 +270,7 @@ function InstallButton(props: { catchError('Remove', async () => { reportUsage(`${TAG}:remove`, undefined, props.name); setAction({kind: 'Waiting'}); - await fs.remove(path.join(PLUGIN_DIR, props.name)); + await removePlugin(props.name); props.onInstall(); setAction({kind: 'Install'}); }), @@ -336,7 +334,7 @@ function useNPMSearch( query: string, setQuery: (query: string) => void, searchClientFactory: () => SearchIndex, - installedPlugins: Map, + installedPlugins: PluginMap, onInstall: () => Promise, findPluginUpdates: ( currentPlugins: PluginMap, @@ -403,11 +401,11 @@ function useNPMSearch( (async () => { let cancelled = false; const {hits} = await reportPlatformFailures( - index.search('', { + index.search('', { query, filters: 'keywords:flipper-plugin', hitsPerPage: 20, - }) as Promise>, + }) as Promise>, `${TAG}:queryIndex`, ); if (cancelled) { diff --git a/desktop/app/src/chrome/plugin-manager/__tests__/PluginInstaller.node.tsx b/desktop/app/src/chrome/plugin-manager/__tests__/PluginInstaller.node.tsx index 2d4977c14..bf4bc1a3e 100644 --- a/desktop/app/src/chrome/plugin-manager/__tests__/PluginInstaller.node.tsx +++ b/desktop/app/src/chrome/plugin-manager/__tests__/PluginInstaller.node.tsx @@ -9,15 +9,24 @@ import {annotatePluginsWithUpdates} from '../PluginInstaller'; import {UpdateResult} from '../../../utils/pluginManager'; +import {PluginDetails} from 'flipper-plugin-lib'; test('annotatePluginsWithUpdates', async () => { - const installedPlugins = new Map([ + const installedPlugins = new Map([ [ 'example', { name: 'example', version: '0.1.0', description: 'Gaze into the death crystal', + dir: '/plugins/example', + specVersion: 2, + source: 'src/index.ts', + isDefault: false, + main: 'lib/index.js', + title: 'Example', + id: 'Example', + entry: '/plugins/example/lib/index.js', }, ], [ @@ -26,6 +35,14 @@ test('annotatePluginsWithUpdates', async () => { name: 'ricksybusiness', version: '1.0.0', description: 'Rick Die Rickpeat', + dir: '/plugins/example', + specVersion: 2, + source: 'src/index.ts', + isDefault: false, + main: 'lib/index.js', + title: 'ricksybusiness', + id: 'ricksybusiness', + entry: '/plugins/ricksybusiness/lib/index.js', }, ], ]); @@ -37,7 +54,15 @@ test('annotatePluginsWithUpdates', async () => { Map { "example" => Object { "description": "Gaze into the death crystal", + "dir": "/plugins/example", + "entry": "/plugins/example/lib/index.js", + "id": "Example", + "isDefault": false, + "main": "lib/index.js", "name": "example", + "source": "src/index.ts", + "specVersion": 2, + "title": "Example", "updateStatus": Object { "kind": "update-available", "version": "1.1.0", @@ -46,7 +71,15 @@ test('annotatePluginsWithUpdates', async () => { }, "ricksybusiness" => Object { "description": "Rick Die Rickpeat", + "dir": "/plugins/example", + "entry": "/plugins/ricksybusiness/lib/index.js", + "id": "ricksybusiness", + "isDefault": false, + "main": "lib/index.js", "name": "ricksybusiness", + "source": "src/index.ts", + "specVersion": 2, + "title": "ricksybusiness", "updateStatus": Object { "kind": "up-to-date", }, diff --git a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx index 0c1fe78cd..da58a0811 100644 --- a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx +++ b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx @@ -10,13 +10,13 @@ jest.mock('../../defaultPlugins'); import dispatcher, { - PluginDefinition, getDynamicPlugins, checkDisabled, checkGK, requirePlugin, filterNewestVersionOfEachPlugin, } from '../plugins'; +import {PluginDetails} from 'flipper-plugin-lib'; import path from 'path'; import {ipcRenderer, remote} from 'electron'; import {FlipperPlugin} from 'flipper'; @@ -32,6 +32,19 @@ const mockStore = configureStore([])( ); const logger = initLogger(mockStore); +const samplePluginDetails: PluginDetails = { + name: 'other Name', + entry: './test/index.js', + version: '1.0.0', + specVersion: 2, + main: 'dist/bundle.js', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample', + source: 'src/index.js', + id: 'Sample', + title: 'Sample', + isDefault: false, +}; + beforeEach(() => { resetConfigForTesting(); }); @@ -69,6 +82,7 @@ test('checkDisabled', () => { expect( disabled({ + ...samplePluginDetails, name: 'other Name', entry: './test/index.js', version: '1.0.0', @@ -77,6 +91,7 @@ test('checkDisabled', () => { expect( disabled({ + ...samplePluginDetails, name: disabledPlugin, entry: './test/index.js', version: '1.0.0', @@ -87,6 +102,7 @@ test('checkDisabled', () => { test('checkGK for plugin without GK', () => { expect( checkGK([])({ + ...samplePluginDetails, name: 'pluginID', entry: './test/index.js', version: '1.0.0', @@ -97,6 +113,7 @@ test('checkGK for plugin without GK', () => { test('checkGK for passing plugin', () => { expect( checkGK([])({ + ...samplePluginDetails, name: 'pluginID', gatekeeper: TEST_PASSING_GK, entry: './test/index.js', @@ -106,9 +123,10 @@ test('checkGK for passing plugin', () => { }); test('checkGK for failing plugin', () => { - const gatekeepedPlugins: PluginDefinition[] = []; + const gatekeepedPlugins: PluginDetails[] = []; const name = 'pluginID'; const plugins = checkGK(gatekeepedPlugins)({ + ...samplePluginDetails, name, gatekeeper: TEST_FAILING_GK, entry: './test/index.js', @@ -122,6 +140,7 @@ test('checkGK for failing plugin', () => { test('requirePlugin returns null for invalid requires', () => { const requireFn = requirePlugin([], {}, require); const plugin = requireFn({ + ...samplePluginDetails, name: 'pluginID', entry: 'this/path/does not/exist', version: '1.0.0', @@ -134,6 +153,7 @@ test('requirePlugin loads plugin', () => { const name = 'pluginID'; const requireFn = requirePlugin([], {}, require); const plugin = requireFn({ + ...samplePluginDetails, name, entry: path.join(__dirname, 'TestPlugin'), version: '1.0.0', @@ -143,19 +163,29 @@ test('requirePlugin loads plugin', () => { }); test('newest version of each plugin is taken', () => { - const plugins: PluginDefinition[] = [ - {name: 'flipper-plugin-test1', version: '0.1.0'}, - {name: 'flipper-plugin-test2', version: '0.1.0-alpha.201'}, - {name: 'flipper-plugin-test2', version: '0.1.0-alpha.21'}, - {name: 'flipper-plugin-test1', version: '0.10.0'}, + const plugins: PluginDetails[] = [ + {...samplePluginDetails, name: 'flipper-plugin-test1', version: '0.1.0'}, + { + ...samplePluginDetails, + name: 'flipper-plugin-test2', + version: '0.1.0-alpha.201', + }, + { + ...samplePluginDetails, + name: 'flipper-plugin-test2', + version: '0.1.0-alpha.21', + }, + {...samplePluginDetails, name: 'flipper-plugin-test1', version: '0.10.0'}, ]; const filteredPlugins = filterNewestVersionOfEachPlugin(plugins); expect(filteredPlugins).toHaveLength(2); expect(filteredPlugins).toContainEqual({ + ...samplePluginDetails, name: 'flipper-plugin-test1', version: '0.10.0', }); expect(filteredPlugins).toContainEqual({ + ...samplePluginDetails, name: 'flipper-plugin-test2', version: '0.1.0-alpha.201', }); diff --git a/desktop/app/src/dispatcher/pluginManager.tsx b/desktop/app/src/dispatcher/pluginManager.tsx index 982080d10..64af9cafb 100644 --- a/desktop/app/src/dispatcher/pluginManager.tsx +++ b/desktop/app/src/dispatcher/pluginManager.tsx @@ -10,7 +10,7 @@ import {Store} from '../reducers/index'; import {Logger} from '../fb-interfaces/Logger'; import {registerInstalledPlugins} from '../reducers/pluginManager'; -import {readInstalledPlugins} from '../utils/pluginManager'; +import {readInstalledPlugins} from 'flipper-plugin-lib'; function refreshInstalledPlugins(store: Store) { readInstalledPlugins().then((plugins) => diff --git a/desktop/app/src/dispatcher/plugins.tsx b/desktop/app/src/dispatcher/plugins.tsx index f0d3c2f0e..bbea21b8f 100644 --- a/desktop/app/src/dispatcher/plugins.tsx +++ b/desktop/app/src/dispatcher/plugins.tsx @@ -30,19 +30,11 @@ import isProduction from '../utils/isProduction'; import {notNull} from '../utils/typeUtils'; import {sideEffect} from '../utils/sideEffect'; import semver from 'semver'; +import {PluginDetails} from 'flipper-plugin-lib'; // eslint-disable-next-line import/no-unresolved import getPluginIndex from '../utils/getDefaultPluginsIndex'; -export type PluginDefinition = { - id?: string; - name: string; - out?: string; - gatekeeper?: string; - entry?: string; - version: string; -}; - export default (store: Store, logger: Logger) => { // expose Flipper and exact globally for dynamically loaded plugins const globalObject: any = typeof window === 'undefined' ? global : window; @@ -51,9 +43,9 @@ export default (store: Store, logger: Logger) => { globalObject.Flipper = Flipper; globalObject.adbkit = adbkit; - const gatekeepedPlugins: Array = []; - const disabledPlugins: Array = []; - const failedPlugins: Array<[PluginDefinition, string]> = []; + const gatekeepedPlugins: Array = []; + const disabledPlugins: Array = []; + const failedPlugins: Array<[PluginDetails, string]> = []; const defaultPluginsIndex = getPluginIndex(); @@ -88,9 +80,9 @@ export default (store: Store, logger: Logger) => { }; export function filterNewestVersionOfEachPlugin( - plugins: PluginDefinition[], -): PluginDefinition[] { - const pluginByName: {[key: string]: PluginDefinition} = {}; + plugins: PluginDetails[], +): PluginDetails[] { + const pluginByName: {[key: string]: PluginDetails} = {}; for (const plugin of plugins) { if ( !pluginByName[plugin.name] || @@ -102,7 +94,7 @@ export function filterNewestVersionOfEachPlugin( return Object.values(pluginByName); } -function getBundledPlugins(): Array { +function getBundledPlugins(): Array { // DefaultPlugins that are included in the bundle. // List of defaultPlugins is written at build time const pluginPath = @@ -111,27 +103,18 @@ function getBundledPlugins(): Array { ? path.join(__dirname, 'defaultPlugins') : './defaultPlugins/index.json'); - let bundledPlugins: Array = []; + let bundledPlugins: Array = []; try { bundledPlugins = global.electronRequire(pluginPath); } catch (e) { console.error(e); } - return bundledPlugins - .filter((plugin) => notNull(plugin.entry)) - .map( - (plugin) => - ({ - ...plugin, - entry: path.resolve(pluginPath, plugin.entry!), - } as PluginDefinition), - ) - .concat(bundledPlugins.filter((plugin) => !plugin.entry)); + return bundledPlugins; } export function getDynamicPlugins() { - let dynamicPlugins: Array = []; + let dynamicPlugins: Array = []; try { dynamicPlugins = ipcRenderer.sendSync('get-dynamic-plugins'); } catch (e) { @@ -140,8 +123,8 @@ export function getDynamicPlugins() { return dynamicPlugins; } -export const checkGK = (gatekeepedPlugins: Array) => ( - plugin: PluginDefinition, +export const checkGK = (gatekeepedPlugins: Array) => ( + plugin: PluginDetails, ): boolean => { if (!plugin.gatekeeper) { return true; @@ -153,8 +136,8 @@ export const checkGK = (gatekeepedPlugins: Array) => ( return result; }; -export const checkDisabled = (disabledPlugins: Array) => ( - plugin: PluginDefinition, +export const checkDisabled = (disabledPlugins: Array) => ( + plugin: PluginDetails, ): boolean => { let disabledList: Set = new Set(); try { @@ -171,17 +154,17 @@ export const checkDisabled = (disabledPlugins: Array) => ( }; export const requirePlugin = ( - failedPlugins: Array<[PluginDefinition, string]>, + failedPlugins: Array<[PluginDetails, string]>, defaultPluginsIndex: any, reqFn: Function = global.electronRequire, ) => { return ( - pluginDefinition: PluginDefinition, + pluginDetails: PluginDetails, ): typeof FlipperPlugin | typeof FlipperDevicePlugin | null => { try { - let plugin = pluginDefinition.entry - ? reqFn(pluginDefinition.entry) - : defaultPluginsIndex[pluginDefinition.name]; + let plugin = pluginDetails.isDefault + ? defaultPluginsIndex[pluginDetails.name] + : reqFn(pluginDetails.entry); if (plugin.default) { plugin = plugin.default; } @@ -189,21 +172,21 @@ export const requirePlugin = ( throw new Error(`Plugin ${plugin.name} is not a FlipperBasePlugin`); } - plugin.id = plugin.id || pluginDefinition.id; - plugin.packageName = pluginDefinition.name; + plugin.id = plugin.id || pluginDetails.id; + plugin.packageName = pluginDetails.name; // set values from package.json as static variables on class - Object.keys(pluginDefinition).forEach((key) => { + Object.keys(pluginDetails).forEach((key) => { if (key !== 'name' && key !== 'id') { plugin[key] = - plugin[key] || pluginDefinition[key as keyof PluginDefinition]; + plugin[key] || pluginDetails[key as keyof PluginDetails]; } }); return plugin; } catch (e) { - failedPlugins.push([pluginDefinition, e.message]); - console.error(pluginDefinition, e); + failedPlugins.push([pluginDetails, e.message]); + console.error(pluginDetails, e); return null; } }; diff --git a/desktop/app/src/plugin.tsx b/desktop/app/src/plugin.tsx index aed8bcaa5..7bc68468b 100644 --- a/desktop/app/src/plugin.tsx +++ b/desktop/app/src/plugin.tsx @@ -101,6 +101,7 @@ export abstract class FlipperBasePlugin< static icon: string | null = null; static gatekeeper: string | null = null; static entry: string | null = null; + static isDefault: boolean; static bugs: { email?: string; url?: string; diff --git a/desktop/app/src/reducers/__tests__/pluginManager.node.tsx b/desktop/app/src/reducers/__tests__/pluginManager.node.tsx index 096b0fd27..0d3dfc6cf 100644 --- a/desktop/app/src/reducers/__tests__/pluginManager.node.tsx +++ b/desktop/app/src/reducers/__tests__/pluginManager.node.tsx @@ -18,6 +18,14 @@ const EXAMPLE_PLUGIN = { name: 'test', version: '0.1', description: 'my test plugin', + dir: '/plugins/test', + specVersion: 2, + source: 'src/index.ts', + isDefault: false, + main: 'lib/index.js', + title: 'test', + id: 'test', + entry: '/plugins/test/lib/index.js', }; test('reduce registerInstalledPlugins, clear again', () => { diff --git a/desktop/app/src/reducers/__tests__/plugins.node.tsx b/desktop/app/src/reducers/__tests__/plugins.node.tsx index 5250a7abe..f69dc21d7 100644 --- a/desktop/app/src/reducers/__tests__/plugins.node.tsx +++ b/desktop/app/src/reducers/__tests__/plugins.node.tsx @@ -72,7 +72,21 @@ test('do not add plugin twice', () => { }); test('add gatekeeped plugin', () => { - const gatekeepedPlugins = [{name: 'plugin', out: 'out.js', version: '1.0.0'}]; + const gatekeepedPlugins = [ + { + name: 'plugin', + out: 'out.js', + version: '1.0.0', + dir: '/plugins/test', + specVersion: 2, + source: 'src/index.ts', + isDefault: false, + main: 'lib/index.js', + title: 'test', + id: 'test', + entry: '/plugins/test/lib/index.js', + }, + ]; const res = reducer( { devicePlugins: new Map(), diff --git a/desktop/app/src/reducers/pluginManager.tsx b/desktop/app/src/reducers/pluginManager.tsx index 9ca20ca8e..74857b501 100644 --- a/desktop/app/src/reducers/pluginManager.tsx +++ b/desktop/app/src/reducers/pluginManager.tsx @@ -8,14 +8,7 @@ */ import {Actions} from './'; - -export type PluginDefinition = { - name: string; - version: string; - description: string; -}; - -export type PluginMap = Map; +import {PluginMap} from 'flipper-plugin-lib'; export type State = { installedPlugins: PluginMap; diff --git a/desktop/app/src/reducers/plugins.tsx b/desktop/app/src/reducers/plugins.tsx index 6b5a13afa..fe29f8065 100644 --- a/desktop/app/src/reducers/plugins.tsx +++ b/desktop/app/src/reducers/plugins.tsx @@ -8,16 +8,16 @@ */ import {FlipperPlugin, FlipperDevicePlugin} from '../plugin'; -import {PluginDefinition} from '../dispatcher/plugins'; +import {PluginDetails} from 'flipper-plugin-lib'; import {Actions} from '.'; import produce from 'immer'; export type State = { devicePlugins: Map; clientPlugins: Map; - gatekeepedPlugins: Array; - disabledPlugins: Array; - failedPlugins: Array<[PluginDefinition, string]>; + gatekeepedPlugins: Array; + disabledPlugins: Array; + failedPlugins: Array<[PluginDetails, string]>; selectedPlugins: Array; }; @@ -32,15 +32,15 @@ export type Action = | RegisterPluginAction | { type: 'GATEKEEPED_PLUGINS'; - payload: Array; + payload: Array; } | { type: 'DISABLED_PLUGINS'; - payload: Array; + payload: Array; } | { type: 'FAILED_PLUGINS'; - payload: Array<[PluginDefinition, string]>; + payload: Array<[PluginDetails, string]>; } | { type: 'SELECTED_PLUGINS'; @@ -113,21 +113,19 @@ export const registerPlugins = (payload: Array): Action => ({ }); export const addGatekeepedPlugins = ( - payload: Array, + payload: Array, ): Action => ({ type: 'GATEKEEPED_PLUGINS', payload, }); -export const addDisabledPlugins = ( - payload: Array, -): Action => ({ +export const addDisabledPlugins = (payload: Array): Action => ({ type: 'DISABLED_PLUGINS', payload, }); export const addFailedPlugins = ( - payload: Array<[PluginDefinition, string]>, + payload: Array<[PluginDetails, string]>, ): Action => ({ type: 'FAILED_PLUGINS', payload, diff --git a/desktop/app/src/utils/__tests__/pluginUtils.node.js b/desktop/app/src/utils/__tests__/pluginUtils.node.js index 8b4eca5b7..c9178acf7 100644 --- a/desktop/app/src/utils/__tests__/pluginUtils.node.js +++ b/desktop/app/src/utils/__tests__/pluginUtils.node.js @@ -13,7 +13,7 @@ import { } from '../pluginUtils.tsx'; import type {State as PluginsState} from '../../reducers/plugins.tsx'; import type {State as PluginStatesState} from '../../reducers/pluginStates.tsx'; -import type {PluginDefinition} from '../../dispatcher/plugins.tsx'; +import type {PluginDetails} from 'flipper-plugin-lib'; import type {State as PluginMessageQueueState} from '../../reducers/pluginStates.tsx'; import {FlipperBasePlugin} from 'flipper'; import type {ReduxState} from '../../reducers/index.tsx'; @@ -60,9 +60,9 @@ function createMockFlipperPluginWithNoPersistedState(id: string) { } function mockPluginState( - gatekeepedPlugins: Array, - disabledPlugins: Array, - failedPlugins: Array<[PluginDefinition, string]>, + gatekeepedPlugins: Array, + disabledPlugins: Array, + failedPlugins: Array<[PluginDetails, string]>, ): PluginsState { return { devicePlugins: new Map([ @@ -92,7 +92,7 @@ function mockPluginState( }; } -function mockPluginDefinition(name: string): PluginDefinition { +function mockPluginDefinition(name: string): PluginDetails { return { name, out: 'out', diff --git a/desktop/app/src/utils/pluginManager.tsx b/desktop/app/src/utils/pluginManager.tsx index 0f468c561..f40b13bc9 100644 --- a/desktop/app/src/utils/pluginManager.tsx +++ b/desktop/app/src/utils/pluginManager.tsx @@ -8,9 +8,8 @@ */ import path from 'path'; -import fs from 'fs-extra'; import {homedir} from 'os'; -import {PluginMap, PluginDefinition} from '../reducers/pluginManager'; +import {PluginMap, PluginDetails} from 'flipper-plugin-lib'; import {default as algoliasearch, SearchIndex} from 'algoliasearch'; import NpmApi, {Package} from 'npm-api'; import semver from 'semver'; @@ -26,36 +25,6 @@ export function provideSearchIndex(): SearchIndex { return client.initIndex('npm-search'); } -export async function readInstalledPlugins(): Promise { - const pluginDirExists = await fs.pathExists(PLUGIN_DIR); - - if (!pluginDirExists) { - return new Map(); - } - const dirs = await fs.readdir(PLUGIN_DIR); - const plugins = await Promise.all<[string, PluginDefinition]>( - dirs.map( - (name) => - new Promise(async (resolve, reject) => { - if (!(await fs.lstat(path.join(PLUGIN_DIR, name))).isDirectory()) { - return resolve(undefined); - } - - const packageJSON = await fs.readFile( - path.join(PLUGIN_DIR, name, 'package.json'), - ); - - try { - resolve([name, JSON.parse(packageJSON.toString())]); - } catch (e) { - reject(e); - } - }), - ), - ); - return new Map(plugins.filter(Boolean)); -} - export type UpdateResult = | {kind: 'up-to-date'} | {kind: 'error'; error: Error} @@ -68,9 +37,7 @@ export async function findPluginUpdates( return Promise.all( Array.from(currentPlugins.values()).map( - async ( - currentPlugin: PluginDefinition, - ): Promise<[string, UpdateResult]> => + async (currentPlugin: PluginDetails): Promise<[string, UpdateResult]> => npm .repo(currentPlugin.name) .package() diff --git a/desktop/app/src/utils/pluginUtils.tsx b/desktop/app/src/utils/pluginUtils.tsx index 83a628ac4..899d6a955 100644 --- a/desktop/app/src/utils/pluginUtils.tsx +++ b/desktop/app/src/utils/pluginUtils.tsx @@ -11,7 +11,7 @@ import {FlipperDevicePlugin, FlipperPlugin, FlipperBasePlugin} from '../plugin'; import {State as PluginStatesState} from '../reducers/pluginStates'; import {State as PluginsState} from '../reducers/plugins'; import {State as PluginMessageQueueState} from '../reducers/pluginMessageQueue'; -import {PluginDefinition} from '../dispatcher/plugins'; +import {PluginDetails} from 'flipper-plugin-lib'; import {deconstructPluginKey, deconstructClientId} from './clientUtils'; type Client = import('../Client').default; @@ -188,16 +188,16 @@ export function getPersistentPlugins(plugins: PluginsState): Array { typeof FlipperDevicePlugin | typeof FlipperPlugin > = pluginsClassMap(plugins); - const arr: Array = plugins.disabledPlugins.concat( + const arr: Array = plugins.disabledPlugins.concat( plugins.gatekeepedPlugins, ); - arr.forEach((plugin: PluginDefinition) => { + arr.forEach((plugin: PluginDetails) => { if (pluginsMap.has(plugin.name)) { pluginsMap.delete(plugin.name); } }); - plugins.failedPlugins.forEach((plugin: [PluginDefinition, string]) => { + plugins.failedPlugins.forEach((plugin: [PluginDetails, string]) => { if (plugin[0] && plugin[0].name && pluginsMap.has(plugin[0].name)) { pluginsMap.delete(plugin[0].name); } diff --git a/desktop/plugin-lib/src/PluginDetails.ts b/desktop/plugin-lib/src/PluginDetails.ts index 9d2c951c5..d513dce45 100644 --- a/desktop/plugin-lib/src/PluginDetails.ts +++ b/desktop/plugin-lib/src/PluginDetails.ts @@ -15,9 +15,11 @@ export default interface PluginDetails { source: string; main: string; id: string; + isDefault: boolean; + entry: string; gatekeeper?: string; - icon?: string; title: string; + icon?: string; description?: string; category?: string; bugs?: { diff --git a/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts b/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts index 5f422091a..3b6d49208 100644 --- a/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts +++ b/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts @@ -8,7 +8,16 @@ */ import fs from 'fs-extra'; +import path from 'path'; import getPluginDetails from '../getPluginDetails'; +import {pluginInstallationDir} from '../pluginPaths'; + +jest.mock('../pluginPaths', () => ({ + pluginInstallationDir: '/Users/mock/.flipper/thirdparty', + pluginCacheDir: '/Users/mock/.flipper/plugins', +})); + +const pluginPath = path.join(pluginInstallationDir, 'flipper-plugin-test'); test('getPluginDetailsV1', async () => { const pluginV1 = { @@ -21,16 +30,18 @@ test('getPluginDetailsV1', async () => { }; jest.mock('fs-extra', () => jest.fn()); fs.readJson = jest.fn().mockImplementation(() => pluginV1); - const details = await getPluginDetails('./plugins/flipper-plugin-test'); + const details = await getPluginDetails(pluginPath); expect(details).toMatchInlineSnapshot(` Object { "bugs": undefined, "category": undefined, "description": "Description of Test Plugin", - "dir": "./plugins/flipper-plugin-test", + "dir": "/Users/mock/.flipper/thirdparty/flipper-plugin-test", + "entry": "/Users/mock/.flipper/plugins/flipper-plugin-test@2.0.0.js", "gatekeeper": "GK_flipper_plugin_test", "icon": undefined, "id": "flipper-plugin-test", + "isDefault": false, "main": "dist/bundle.js", "name": "flipper-plugin-test", "source": "src/index.tsx", @@ -54,16 +65,18 @@ test('getPluginDetailsV2', async () => { }; jest.mock('fs-extra', () => jest.fn()); fs.readJson = jest.fn().mockImplementation(() => pluginV2); - const details = await getPluginDetails('./plugins/flipper-plugin-test'); + const details = await getPluginDetails(pluginPath); expect(details).toMatchInlineSnapshot(` Object { "bugs": undefined, "category": undefined, "description": "Description of Test Plugin", - "dir": "./plugins/flipper-plugin-test", + "dir": "/Users/mock/.flipper/thirdparty/flipper-plugin-test", + "entry": "/Users/mock/.flipper/thirdparty/flipper-plugin-test/dist/bundle.js", "gatekeeper": "GK_flipper_plugin_test", "icon": undefined, "id": "flipper-plugin-test", + "isDefault": false, "main": "dist/bundle.js", "name": "flipper-plugin-test", "source": "src/index.tsx", @@ -87,16 +100,18 @@ test('id used as title if the latter omited', async () => { }; jest.mock('fs-extra', () => jest.fn()); fs.readJson = jest.fn().mockImplementation(() => pluginV2); - const details = await getPluginDetails('./plugins/flipper-plugin-test'); + const details = await getPluginDetails(pluginPath); expect(details).toMatchInlineSnapshot(` Object { "bugs": undefined, "category": undefined, "description": "Description of Test Plugin", - "dir": "./plugins/flipper-plugin-test", + "dir": "/Users/mock/.flipper/thirdparty/flipper-plugin-test", + "entry": "/Users/mock/.flipper/thirdparty/flipper-plugin-test/dist/bundle.js", "gatekeeper": "GK_flipper_plugin_test", "icon": undefined, "id": "test", + "isDefault": false, "main": "dist/bundle.js", "name": "flipper-plugin-test", "source": "src/index.tsx", @@ -119,16 +134,18 @@ test('name without "flipper-plugin-" prefix is used as title if the latter omite }; jest.mock('fs-extra', () => jest.fn()); fs.readJson = jest.fn().mockImplementation(() => pluginV2); - const details = await getPluginDetails('./plugins/flipper-plugin-test'); + const details = await getPluginDetails(pluginPath); expect(details).toMatchInlineSnapshot(` Object { "bugs": undefined, "category": undefined, "description": "Description of Test Plugin", - "dir": "./plugins/flipper-plugin-test", + "dir": "/Users/mock/.flipper/thirdparty/flipper-plugin-test", + "entry": "/Users/mock/.flipper/thirdparty/flipper-plugin-test/dist/bundle.js", "gatekeeper": "GK_flipper_plugin_test", "icon": undefined, "id": "flipper-plugin-test", + "isDefault": false, "main": "dist/bundle.js", "name": "flipper-plugin-test", "source": "src/index.tsx", diff --git a/desktop/plugin-lib/src/getPluginDetails.ts b/desktop/plugin-lib/src/getPluginDetails.ts index d577bf33f..d36a3d79a 100644 --- a/desktop/plugin-lib/src/getPluginDetails.ts +++ b/desktop/plugin-lib/src/getPluginDetails.ts @@ -10,6 +10,7 @@ import fs from 'fs-extra'; import path from 'path'; import PluginDetails from './PluginDetails'; +import {pluginCacheDir} from './pluginPaths'; export default async function ( pluginDir: string, @@ -44,8 +45,13 @@ async function getPluginDetailsV1( name: packageJson.name, version: packageJson.version, main: 'dist/bundle.js', + entry: path.join( + pluginCacheDir, + `${packageJson.name}@${packageJson.version || '0.0.0'}.js`, + ), source: packageJson.main, id: packageJson.name, + isDefault: false, gatekeeper: packageJson.gatekeeper, icon: packageJson.icon, title: packageJson.title || packageJson.name, @@ -66,7 +72,9 @@ async function getPluginDetailsV2( name: packageJson.name, version: packageJson.version, main: packageJson.main, + entry: path.resolve(pluginDir, packageJson.main), source: packageJson.flipperBundlerEntry, + isDefault: false, id: packageJson.id || packageJson.name, gatekeeper: packageJson.gatekeeper, icon: packageJson.icon, diff --git a/desktop/plugin-lib/src/pluginInstaller.tsx b/desktop/plugin-lib/src/pluginInstaller.ts similarity index 74% rename from desktop/plugin-lib/src/pluginInstaller.tsx rename to desktop/plugin-lib/src/pluginInstaller.ts index 9ce3508a2..99a4cfee3 100644 --- a/desktop/plugin-lib/src/pluginInstaller.tsx +++ b/desktop/plugin-lib/src/pluginInstaller.ts @@ -10,17 +10,19 @@ import path from 'path'; import fs from 'fs-extra'; import {promisify} from 'util'; -import {homedir} from 'os'; import {PluginManager as PM} from 'live-plugin-manager'; import decompress from 'decompress'; import decompressTargz from 'decompress-targz'; import decompressUnzip from 'decompress-unzip'; import tmp from 'tmp'; +import PluginDetails from './PluginDetails'; +import getPluginDetails from './getPluginDetails'; +import {pluginInstallationDir} from './pluginPaths'; + +export type PluginMap = Map; const getTmpDir = promisify(tmp.dir) as () => Promise; -export const PLUGIN_DIR = path.join(homedir(), '.flipper', 'thirdparty'); - function providePluginManager(): PM { return new PM({ ignoredDependencies: [/^flipper$/, /^react$/, /^react-dom$/, /^@types\//], @@ -38,10 +40,10 @@ async function installPluginFromTempDir(pluginDir: string) { ); const name = packageJSON.name; - await fs.ensureDir(PLUGIN_DIR); + await fs.ensureDir(pluginInstallationDir); // create empty watchman config (required by metro's file watcher) - await fs.writeFile(path.join(PLUGIN_DIR, '.watchmanconfig'), '{}'); - const destinationDir = path.join(PLUGIN_DIR, name); + await fs.writeFile(path.join(pluginInstallationDir, '.watchmanconfig'), '{}'); + const destinationDir = path.join(pluginInstallationDir, name); // Clean up existing destination files. await fs.remove(destinationDir); await fs.ensureDir(destinationDir); @@ -118,3 +120,32 @@ export async function installPluginFromFile(packagePath: string) { } } } + +export async function readInstalledPlugins(): Promise { + const pluginDirExists = await fs.pathExists(pluginInstallationDir); + if (!pluginDirExists) { + return new Map(); + } + const dirs = await fs.readdir(pluginInstallationDir); + const plugins = await Promise.all<[string, PluginDetails]>( + dirs.map( + (name) => + new Promise(async (resolve, reject) => { + const pluginDir = path.join(pluginInstallationDir, name); + if (!(await fs.lstat(pluginDir)).isDirectory()) { + return resolve(undefined); + } + try { + resolve([name, await getPluginDetails(pluginDir)]); + } catch (e) { + reject(e); + } + }), + ), + ); + return new Map(plugins.filter(Boolean)); +} + +export async function removePlugin(name: string): Promise { + await fs.remove(path.join(pluginInstallationDir, name)); +} diff --git a/desktop/plugin-lib/src/pluginPaths.ts b/desktop/plugin-lib/src/pluginPaths.ts new file mode 100644 index 000000000..359f0eabc --- /dev/null +++ b/desktop/plugin-lib/src/pluginPaths.ts @@ -0,0 +1,17 @@ +/** + * 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 path from 'path'; +import {homedir} from 'os'; + +export const flipperDataDir = path.join(homedir(), '.flipper'); + +export const pluginInstallationDir = path.join(flipperDataDir, 'thirdparty'); + +export const pluginCacheDir = path.join(flipperDataDir, 'plugins'); diff --git a/desktop/scripts/build-utils.ts b/desktop/scripts/build-utils.ts index 333e1b54b..499ee0e38 100644 --- a/desktop/scripts/build-utils.ts +++ b/desktop/scripts/build-utils.ts @@ -34,6 +34,9 @@ export function die(err: Error) { export async function generatePluginEntryPoints() { console.log('⚙️ Generating plugin entry points...'); const plugins = await getSourcePlugins(); + for (const plugin of plugins) { + plugin.isDefault = true; + } if (await fs.pathExists(defaultPluginsIndexDir)) { await fs.remove(defaultPluginsIndexDir); } diff --git a/desktop/static/compilePlugins.ts b/desktop/static/compilePlugins.ts index 9810aea90..d83a19dc6 100644 --- a/desktop/static/compilePlugins.ts +++ b/desktop/static/compilePlugins.ts @@ -33,13 +33,11 @@ export type CompileOptions = { recompileOnChanges: boolean; }; -export type CompiledPluginDetails = PluginDetails & {entry: string}; - export default async function ( reloadCallback: (() => void) | null, pluginCache: string, options: CompileOptions = DEFAULT_COMPILE_OPTIONS, -): Promise { +): Promise { if (process.env.FLIPPER_FAST_REFRESH) { console.log( '🥫 Skipping loading of installed plugins because Fast Refresh is enabled', @@ -76,7 +74,7 @@ export default async function ( const compiledDynamicPlugins = (await compilations).filter( (c) => c !== null, - ) as CompiledPluginDetails[]; + ) as PluginDetails[]; console.log('✅ Compiled all plugins.'); return compiledDynamicPlugins; } @@ -109,14 +107,13 @@ async function compilePlugin( pluginDetails: PluginDetails, pluginCache: string, {force, failSilently}: CompileOptions, -): Promise { - const {dir, specVersion, version, main, source, name} = pluginDetails; +): Promise { + const {dir, specVersion, version, entry, source, name} = pluginDetails; if (specVersion > 1) { // eslint-disable-next-line no-console - const entry = path.join(dir, main); if (await fs.pathExists(entry)) { console.log(`🥫 Using pre-built version of ${name}: ${entry}...`); - return Object.assign({}, pluginDetails, {entry}); + return pluginDetails; } else { console.error( `❌ Plugin ${name} is ignored, because its entry point not found: ${entry}.`, @@ -125,7 +122,6 @@ async function compilePlugin( } } else { const entry = path.join(pluginCache, `${name}@${version || '0.0.0'}.js`); - const result = Object.assign({}, pluginDetails, {entry}); const rootDirCtime = await mostRecentlyChanged(dir); if ( !force && @@ -134,7 +130,7 @@ async function compilePlugin( ) { // eslint-disable-next-line no-console console.log(`🥫 Using cached version of ${name}...`); - return result; + return pluginDetails; } else { // eslint-disable-line no-console console.log(`⚙️ Compiling ${name}...`); @@ -151,7 +147,7 @@ async function compilePlugin( throw e; } } - return result; + return pluginDetails; } } }