From 5383017299e3ed5bec08b710fd8ae4125b4992eb Mon Sep 17 00:00:00 2001 From: Anton Nikolaev Date: Tue, 15 Dec 2020 09:28:58 -0800 Subject: [PATCH] Separate interfaces for installed, bundled and downloadable plugins Summary: I've re-designed interfaces describing plugins as I found that mental overhead working with them became too expensive because of slightly flawed design. However this cascaded changes in many files so you can see how extensively these interfaces used in our codebase. Before this change we had one interface PluginDetails which described three different entities: 1) plugins installed on the disk 2) plugins bundled into Flipper 3) plugins available on Marketplace. It's hard to use this "general" PluginDetails interface because of this as you always need to think about all three use cases everywhere. After this change we have 3 separate interfaces: InstalledPluginDetails, BundledPluginDetails and DownloadablePluginDetails and things became much type-safer now. Reviewed By: mweststrate Differential Revision: D25530383 fbshipit-source-id: b93593916a980c04e36dc6ffa168797645a0ff9c --- .../chrome/plugin-manager/PluginDebugger.tsx | 2 +- .../__tests__/PluginInstaller.node.tsx | 6 +- .../src/dispatcher/__tests__/plugins.node.tsx | 116 ++++++++++++------ .../app/src/dispatcher/pluginDownloads.tsx | 30 +++-- desktop/app/src/dispatcher/plugins.tsx | 49 ++++---- desktop/app/src/plugin.tsx | 7 +- desktop/app/src/plugins/TableNativePlugin.tsx | 9 +- .../reducers/__tests__/pluginManager.node.tsx | 7 +- .../src/reducers/__tests__/plugins.node.tsx | 2 +- desktop/app/src/reducers/pluginDownloads.tsx | 31 +++-- desktop/app/src/reducers/pluginManager.tsx | 10 +- .../sandy-chrome/appinspect/PluginList.tsx | 2 +- desktop/app/src/utils/loadDynamicPlugins.tsx | 6 +- desktop/app/src/utils/testUtils.tsx | 5 +- .../src/plugin/SandyPluginDefinition.tsx | 12 +- .../src/test-utils/test-utils.tsx | 9 +- desktop/pkg/src/commands/bundle.ts | 4 +- desktop/pkg/src/commands/pack.ts | 4 +- desktop/pkg/src/utils/runMigrate.ts | 4 +- desktop/plugin-lib/src/PluginDetails.ts | 34 ++++- .../src/__tests__/getPluginDetails.node.ts | 27 ++-- .../src/__tests__/getUpdatablePlugins.node.ts | 10 +- desktop/plugin-lib/src/getPluginDetails.ts | 78 ++++++------ desktop/plugin-lib/src/getSourcePlugins.ts | 13 +- desktop/plugin-lib/src/getUpdatablePlugins.ts | 13 +- desktop/plugin-lib/src/pluginInstaller.ts | 22 ++-- desktop/scripts/build-utils.ts | 31 +++-- 27 files changed, 327 insertions(+), 216 deletions(-) diff --git a/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx b/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx index 0a7e8aec1..8c8e5b6d6 100644 --- a/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx +++ b/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx @@ -139,7 +139,7 @@ class PluginDebugger extends Component { getRows(): Array { const rows: Array = []; - const externalPluginPath = (p: any) => (p.isDefault ? 'bundled' : p.entry); + const externalPluginPath = (p: any) => (p.isBundled ? 'bundled' : p.entry); this.props.gatekeepedPlugins.forEach((plugin) => rows.push( 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 c296c629e..80c414db3 100644 --- a/desktop/app/src/chrome/plugin-manager/__tests__/PluginInstaller.node.tsx +++ b/desktop/app/src/chrome/plugin-manager/__tests__/PluginInstaller.node.tsx @@ -39,7 +39,8 @@ const samplePluginDetails1: UpdatablePluginDetails = { id: 'Hello', title: 'Hello', description: 'World?', - isDefault: false, + isBundled: false, + isActivatable: true, updateStatus: { kind: 'not-installed', version: '0.1.0', @@ -57,7 +58,8 @@ const samplePluginDetails2: UpdatablePluginDetails = { id: 'World', title: 'World', description: 'Hello?', - isDefault: false, + isBundled: false, + isActivatable: true, updateStatus: { kind: 'not-installed', version: '0.2.0', diff --git a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx index bc2b2bf3a..cfdd57dc3 100644 --- a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx +++ b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx @@ -16,7 +16,7 @@ import dispatcher, { createRequirePluginFunction, filterNewestVersionOfEachPlugin, } from '../plugins'; -import {PluginDetails} from 'flipper-plugin-lib'; +import {BundledPluginDetails, InstalledPluginDetails} from 'flipper-plugin-lib'; import path from 'path'; import {remote} from 'electron'; import {FlipperPlugin} from '../../plugin'; @@ -37,17 +37,23 @@ const mockStore = configureStore([])( ); const logger = getInstance(); -const samplePluginDetails: PluginDetails = { +const sampleInstalledPluginDetails: InstalledPluginDetails = { 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, + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample', + entry: 'this/path/does not/exist', + isBundled: false, + isActivatable: true, +}; + +const sampleBundledPluginDetails: BundledPluginDetails = { + ...sampleInstalledPluginDetails, + isBundled: true, }; beforeEach(() => { @@ -80,18 +86,16 @@ test('checkDisabled', () => { expect( disabled({ - ...samplePluginDetails, + ...sampleBundledPluginDetails, name: 'other Name', - entry: './test/index.js', version: '1.0.0', }), ).toBeTruthy(); expect( disabled({ - ...samplePluginDetails, + ...sampleBundledPluginDetails, name: disabledPlugin, - entry: './test/index.js', version: '1.0.0', }), ).toBeFalsy(); @@ -100,9 +104,8 @@ test('checkDisabled', () => { test('checkGK for plugin without GK', () => { expect( checkGK([])({ - ...samplePluginDetails, + ...sampleBundledPluginDetails, name: 'pluginID', - entry: './test/index.js', version: '1.0.0', }), ).toBeTruthy(); @@ -111,23 +114,21 @@ test('checkGK for plugin without GK', () => { test('checkGK for passing plugin', () => { expect( checkGK([])({ - ...samplePluginDetails, + ...sampleBundledPluginDetails, name: 'pluginID', gatekeeper: TEST_PASSING_GK, - entry: './test/index.js', version: '1.0.0', }), ).toBeTruthy(); }); test('checkGK for failing plugin', () => { - const gatekeepedPlugins: PluginDetails[] = []; + const gatekeepedPlugins: InstalledPluginDetails[] = []; const name = 'pluginID'; const plugins = checkGK(gatekeepedPlugins)({ - ...samplePluginDetails, + ...sampleBundledPluginDetails, name, gatekeeper: TEST_FAILING_GK, - entry: './test/index.js', version: '1.0.0', }); @@ -138,8 +139,9 @@ test('checkGK for failing plugin', () => { test('requirePlugin returns null for invalid requires', () => { const requireFn = createRequirePluginFunction([], require); const plugin = requireFn({ - ...samplePluginDetails, + ...sampleInstalledPluginDetails, name: 'pluginID', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample', entry: 'this/path/does not/exist', version: '1.0.0', }); @@ -151,8 +153,9 @@ test('requirePlugin loads plugin', () => { const name = 'pluginID'; const requireFn = createRequirePluginFunction([], require); const plugin = requireFn({ - ...samplePluginDetails, + ...sampleInstalledPluginDetails, name, + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample', entry: path.join(__dirname, 'TestPlugin'), version: '1.0.0', }); @@ -162,21 +165,33 @@ test('requirePlugin loads plugin', () => { }); test('newest version of each plugin is used', () => { - const bundledPlugins: PluginDetails[] = [ - {...samplePluginDetails, name: 'flipper-plugin-test1', version: '0.1.0'}, + const bundledPlugins: BundledPluginDetails[] = [ { - ...samplePluginDetails, + ...sampleBundledPluginDetails, + name: 'flipper-plugin-test1', + version: '0.1.0', + }, + { + ...sampleBundledPluginDetails, name: 'flipper-plugin-test2', version: '0.1.0-alpha.201', }, ]; - const installedPlugins: PluginDetails[] = [ + const installedPlugins: InstalledPluginDetails[] = [ { - ...samplePluginDetails, + ...sampleInstalledPluginDetails, name: 'flipper-plugin-test2', version: '0.1.0-alpha.21', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-test2', + entry: './test/index.js', + }, + { + ...sampleInstalledPluginDetails, + name: 'flipper-plugin-test1', + version: '0.10.0', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-test1', + entry: './test/index.js', }, - {...samplePluginDetails, name: 'flipper-plugin-test1', version: '0.10.0'}, ]; const filteredPlugins = filterNewestVersionOfEachPlugin( bundledPlugins, @@ -184,12 +199,14 @@ test('newest version of each plugin is used', () => { ); expect(filteredPlugins).toHaveLength(2); expect(filteredPlugins).toContainEqual({ - ...samplePluginDetails, + ...sampleInstalledPluginDetails, name: 'flipper-plugin-test1', version: '0.10.0', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-test1', + entry: './test/index.js', }); expect(filteredPlugins).toContainEqual({ - ...samplePluginDetails, + ...sampleBundledPluginDetails, name: 'flipper-plugin-test2', version: '0.1.0-alpha.201', }); @@ -198,21 +215,33 @@ test('newest version of each plugin is used', () => { test('bundled versions are used when env var FLIPPER_DISABLE_PLUGIN_AUTO_UPDATE is set even if newer versions are installed', () => { process.env.FLIPPER_DISABLE_PLUGIN_AUTO_UPDATE = 'true'; try { - const bundledPlugins: PluginDetails[] = [ - {...samplePluginDetails, name: 'flipper-plugin-test1', version: '0.1.0'}, + const bundledPlugins: BundledPluginDetails[] = [ { - ...samplePluginDetails, + ...sampleBundledPluginDetails, + name: 'flipper-plugin-test1', + version: '0.1.0', + }, + { + ...sampleBundledPluginDetails, name: 'flipper-plugin-test2', version: '0.1.0-alpha.21', }, ]; - const installedPlugins: PluginDetails[] = [ + const installedPlugins: InstalledPluginDetails[] = [ { - ...samplePluginDetails, + ...sampleInstalledPluginDetails, name: 'flipper-plugin-test2', version: '0.1.0-alpha.201', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-test2', + entry: './test/index.js', + }, + { + ...sampleInstalledPluginDetails, + name: 'flipper-plugin-test1', + version: '0.10.0', + dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-test1', + entry: './test/index.js', }, - {...samplePluginDetails, name: 'flipper-plugin-test1', version: '0.10.0'}, ]; const filteredPlugins = filterNewestVersionOfEachPlugin( bundledPlugins, @@ -220,12 +249,12 @@ test('bundled versions are used when env var FLIPPER_DISABLE_PLUGIN_AUTO_UPDATE ); expect(filteredPlugins).toHaveLength(2); expect(filteredPlugins).toContainEqual({ - ...samplePluginDetails, + ...sampleBundledPluginDetails, name: 'flipper-plugin-test1', version: '0.1.0', }); expect(filteredPlugins).toContainEqual({ - ...samplePluginDetails, + ...sampleBundledPluginDetails, name: 'flipper-plugin-test2', version: '0.1.0-alpha.21', }); @@ -238,8 +267,12 @@ test('requirePlugin loads valid Sandy plugin', () => { const name = 'pluginID'; const requireFn = createRequirePluginFunction([], require); const plugin = requireFn({ - ...samplePluginDetails, + ...sampleInstalledPluginDetails, name, + dir: path.join( + __dirname, + '../../../../flipper-plugin/src/__tests__/TestPlugin', + ), entry: path.join( __dirname, '../../../../flipper-plugin/src/__tests__/TestPlugin', @@ -253,7 +286,7 @@ test('requirePlugin loads valid Sandy plugin', () => { expect(plugin.details).toMatchObject({ flipperSDKVersion: '0.0.0', id: 'Sample', - isDefault: false, + isBundled: false, main: 'dist/bundle.js', name: 'pluginID', source: 'src/index.js', @@ -272,9 +305,10 @@ test('requirePlugin errors on invalid Sandy plugin', () => { const failedPlugins: any[] = []; const requireFn = createRequirePluginFunction(failedPlugins, require); requireFn({ - ...samplePluginDetails, + ...sampleInstalledPluginDetails, name, // Intentionally the wrong file: + dir: __dirname, entry: path.join(__dirname, 'TestPlugin'), version: '1.0.0', flipperSDKVersion: '0.0.0', @@ -288,8 +322,12 @@ test('requirePlugin loads valid Sandy Device plugin', () => { const name = 'pluginID'; const requireFn = createRequirePluginFunction([], require); const plugin = requireFn({ - ...samplePluginDetails, + ...sampleInstalledPluginDetails, name, + dir: path.join( + __dirname, + '../../../../flipper-plugin/src/__tests__/DeviceTestPlugin', + ), entry: path.join( __dirname, '../../../../flipper-plugin/src/__tests__/DeviceTestPlugin', @@ -303,7 +341,7 @@ test('requirePlugin loads valid Sandy Device plugin', () => { expect(plugin.details).toMatchObject({ flipperSDKVersion: '0.0.0', id: 'Sample', - isDefault: false, + isBundled: false, main: 'dist/bundle.js', name: 'pluginID', source: 'src/index.js', diff --git a/desktop/app/src/dispatcher/pluginDownloads.tsx b/desktop/app/src/dispatcher/pluginDownloads.tsx index 2526871b5..16f52533a 100644 --- a/desktop/app/src/dispatcher/pluginDownloads.tsx +++ b/desktop/app/src/dispatcher/pluginDownloads.tsx @@ -9,6 +9,8 @@ import { DownloadablePluginDetails, + getInstalledPluginDetails, + getPluginVersionInstallationDir, installPluginFromFile, } from 'flipper-plugin-lib'; import {Store} from '../reducers/index'; @@ -62,23 +64,24 @@ async function handlePluginDownload( store: Store, ) { const dispatch = store.dispatch; - const {name, title, version, downloadUrl, dir} = plugin; + const {name, title, version, downloadUrl} = plugin; + const installationDir = getPluginVersionInstallationDir(name, version); console.log( - `Downloading plugin "${title}" v${version} from "${downloadUrl}" to "${dir}".`, + `Downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`, ); - const targetDir = await getTempDirName(); - const targetFile = path.join(targetDir, `${name}-${version}.tgz`); + const tmpDir = await getTempDirName(); + const tmpFile = path.join(tmpDir, `${name}-${version}.tgz`); try { const cancellationSource = axios.CancelToken.source(); dispatch( pluginDownloadStarted({plugin, cancel: cancellationSource.cancel}), ); - if (await fs.pathExists(dir)) { + if (await fs.pathExists(installationDir)) { console.log( - `Using existing files instead of downloading plugin "${title}" v${version} from "${downloadUrl}" to "${dir}"`, + `Using existing files instead of downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}"`, ); } else { - await fs.ensureDir(targetDir); + await fs.ensureDir(tmpDir); let percentCompleted = 0; const response = await axios.get(plugin.downloadUrl, { adapter: axiosHttpAdapter, @@ -103,15 +106,16 @@ async function handlePluginDownload( } const responseStream = response.data as fs.ReadStream; const writeStream = responseStream.pipe( - fs.createWriteStream(targetFile, {autoClose: true}), + fs.createWriteStream(tmpFile, {autoClose: true}), ); await new Promise((resolve, reject) => writeStream.once('finish', resolve).once('error', reject), ); - await installPluginFromFile(targetFile); + await installPluginFromFile(tmpFile); } + const installedPlugin = await getInstalledPluginDetails(installationDir); if (!store.getState().plugins.clientPlugins.has(plugin.id)) { - const pluginDefinition = requirePlugin(plugin); + const pluginDefinition = requirePlugin(installedPlugin); dispatch( registerPluginUpdate({ plugin: pluginDefinition, @@ -120,11 +124,11 @@ async function handlePluginDownload( ); } console.log( - `Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}" to "${dir}".`, + `Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`, ); } catch (error) { console.error( - `Failed to download plugin "${title}" v${version} from "${downloadUrl}" to "${dir}".`, + `Failed to download plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`, error, ); if (startedByUser) { @@ -144,6 +148,6 @@ async function handlePluginDownload( } } finally { dispatch(pluginDownloadFinished({plugin})); - await fs.remove(targetDir); + await fs.remove(tmpDir); } } diff --git a/desktop/app/src/dispatcher/plugins.tsx b/desktop/app/src/dispatcher/plugins.tsx index b648dea26..abfc08d58 100644 --- a/desktop/app/src/dispatcher/plugins.tsx +++ b/desktop/app/src/dispatcher/plugins.tsx @@ -29,7 +29,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'; +import { + ActivatablePluginDetails, + BundledPluginDetails, + InstalledPluginDetails, +} from 'flipper-plugin-lib'; import {tryCatchReportPluginFailures, reportUsage} from '../utils/metrics'; import * as FlipperPluginSDK from 'flipper-plugin'; import {_SandyPluginDefinition} from 'flipper-plugin'; @@ -51,9 +55,9 @@ export default async (store: Store, logger: Logger) => { globalObject.FlipperPlugin = FlipperPluginSDK; globalObject.Immer = Immer; - const gatekeepedPlugins: Array = []; - const disabledPlugins: Array = []; - const failedPlugins: Array<[PluginDetails, string]> = []; + const gatekeepedPlugins: Array = []; + const disabledPlugins: Array = []; + const failedPlugins: Array<[ActivatablePluginDetails, string]> = []; defaultPluginsIndex = getDefaultPluginsIndex(); @@ -89,7 +93,7 @@ export default async (store: Store, logger: Logger) => { ); }; -function reportVersion(pluginDetails: PluginDetails) { +function reportVersion(pluginDetails: ActivatablePluginDetails) { reportUsage( 'plugin:version', { @@ -101,10 +105,10 @@ function reportVersion(pluginDetails: PluginDetails) { } export function filterNewestVersionOfEachPlugin( - bundledPlugins: PluginDetails[], - dynamicPlugins: PluginDetails[], -): PluginDetails[] { - const pluginByName: {[key: string]: PluginDetails} = {}; + bundledPlugins: BundledPluginDetails[], + dynamicPlugins: InstalledPluginDetails[], +): ActivatablePluginDetails[] { + const pluginByName: {[key: string]: ActivatablePluginDetails} = {}; for (const plugin of bundledPlugins) { pluginByName[plugin.name] = plugin; } @@ -120,7 +124,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 = @@ -129,7 +133,7 @@ function getBundledPlugins(): Array { ? path.join(__dirname, 'defaultPlugins') : './defaultPlugins/index.json'); - let bundledPlugins: Array = []; + let bundledPlugins: Array = []; try { bundledPlugins = global.electronRequire(pluginPath); } catch (e) { @@ -148,8 +152,8 @@ export async function getDynamicPlugins() { } } -export const checkGK = (gatekeepedPlugins: Array) => ( - plugin: PluginDetails, +export const checkGK = (gatekeepedPlugins: Array) => ( + plugin: ActivatablePluginDetails, ): boolean => { if (!plugin.gatekeeper) { return true; @@ -161,7 +165,9 @@ export const checkGK = (gatekeepedPlugins: Array) => ( return result; }; -export const checkDisabled = (disabledPlugins: Array) => { +export const checkDisabled = ( + disabledPlugins: Array, +) => { const enabledList = process.env.FLIPPER_ENABLED_PLUGINS ? new Set(process.env.FLIPPER_ENABLED_PLUGINS.split(',')) : null; @@ -171,7 +177,7 @@ export const checkDisabled = (disabledPlugins: Array) => { } catch (e) { console.error(e); } - return (plugin: PluginDetails): boolean => { + return (plugin: ActivatablePluginDetails): boolean => { if (disabledList.has(plugin.name)) { disabledPlugins.push(plugin); return false; @@ -192,10 +198,10 @@ export const checkDisabled = (disabledPlugins: Array) => { }; export const createRequirePluginFunction = ( - failedPlugins: Array<[PluginDetails, string]>, + failedPlugins: Array<[ActivatablePluginDetails, string]>, reqFn: Function = global.electronRequire, ) => { - return (pluginDetails: PluginDetails): PluginDefinition | null => { + return (pluginDetails: ActivatablePluginDetails): PluginDefinition | null => { try { return requirePlugin(pluginDetails, reqFn); } catch (e) { @@ -207,7 +213,7 @@ export const createRequirePluginFunction = ( }; export const requirePlugin = ( - pluginDetails: PluginDetails, + pluginDetails: ActivatablePluginDetails, reqFn: Function = global.electronRequire, ): PluginDefinition => { return tryCatchReportPluginFailures( @@ -218,10 +224,10 @@ export const requirePlugin = ( }; const requirePluginInternal = ( - pluginDetails: PluginDetails, + pluginDetails: ActivatablePluginDetails, reqFn: Function = global.electronRequire, ): PluginDefinition => { - let plugin = pluginDetails.isDefault + let plugin = pluginDetails.isBundled ? defaultPluginsIndex[pluginDetails.name] : reqFn(pluginDetails.entry); if (pluginDetails.flipperSDKVersion) { @@ -248,7 +254,8 @@ const requirePluginInternal = ( // set values from package.json as static variables on class Object.keys(pluginDetails).forEach((key) => { if (key !== 'name' && key !== 'id') { - plugin[key] = plugin[key] || pluginDetails[key as keyof PluginDetails]; + plugin[key] = + plugin[key] || pluginDetails[key as keyof ActivatablePluginDetails]; } }); } diff --git a/desktop/app/src/plugin.tsx b/desktop/app/src/plugin.tsx index 4495e459a..7088d3e29 100644 --- a/desktop/app/src/plugin.tsx +++ b/desktop/app/src/plugin.tsx @@ -18,7 +18,7 @@ import {Idler} from './utils/Idler'; import {StaticView} from './reducers/connections'; import {State as ReduxState} from './reducers'; import {DEFAULT_MAX_QUEUE_SIZE} from './reducers/pluginMessageQueue'; -import {PluginDetails} from 'flipper-plugin-lib'; +import {ActivatablePluginDetails} from 'flipper-plugin-lib'; import {Settings} from './reducers/settings'; import {_SandyPluginDefinition} from 'flipper-plugin'; @@ -122,9 +122,8 @@ export abstract class FlipperBasePlugin< static version: string = ''; static icon: string | null = null; static gatekeeper: string | null = null; - static entry: string | null = null; - static isDefault: boolean; - static details: PluginDetails; + static isBundled: boolean; + static details: ActivatablePluginDetails; static keyboardActions: KeyboardActions | null; static screenshot: string | null; static defaultPersistedState: any; diff --git a/desktop/app/src/plugins/TableNativePlugin.tsx b/desktop/app/src/plugins/TableNativePlugin.tsx index 1ac9d0d73..e531dfb2f 100644 --- a/desktop/app/src/plugins/TableNativePlugin.tsx +++ b/desktop/app/src/plugins/TableNativePlugin.tsx @@ -34,7 +34,7 @@ import createPaste from '../fb-stubs/createPaste'; import {ReactNode} from 'react'; import React from 'react'; import {KeyboardActions} from '../MenuBar'; -import {PluginDetails} from 'flipper-plugin-lib'; +import {BundledPluginDetails} from 'flipper-plugin-lib'; type ID = string; @@ -256,7 +256,7 @@ export default function createTableNativePlugin(id: string, title: string) { static id = id || ''; static title = title || ''; - static details: PluginDetails = { + static details: BundledPluginDetails = { id, title, icon: 'apps', @@ -264,11 +264,10 @@ export default function createTableNativePlugin(id: string, title: string) { // all hmm... specVersion: 1, version: 'auto', - dir: '', source: '', main: '', - entry: '', - isDefault: true, + isBundled: true, + isActivatable: true, }; static defaultPersistedState: PersistedState = { diff --git a/desktop/app/src/reducers/__tests__/pluginManager.node.tsx b/desktop/app/src/reducers/__tests__/pluginManager.node.tsx index 5e1e9017f..bd2778650 100644 --- a/desktop/app/src/reducers/__tests__/pluginManager.node.tsx +++ b/desktop/app/src/reducers/__tests__/pluginManager.node.tsx @@ -8,7 +8,7 @@ */ import {default as reducer, registerInstalledPlugins} from '../pluginManager'; -import {PluginDetails} from 'flipper-plugin-lib'; +import {InstalledPluginDetails} from 'flipper-plugin-lib'; test('reduce empty registerInstalledPlugins', () => { const result = reducer(undefined, registerInstalledPlugins([])); @@ -22,12 +22,13 @@ const EXAMPLE_PLUGIN = { dir: '/plugins/test', specVersion: 2, source: 'src/index.ts', - isDefault: false, + isBundled: false, + isActivatable: true, main: 'lib/index.js', title: 'test', id: 'test', entry: '/plugins/test/lib/index.js', -} as PluginDetails; +} as InstalledPluginDetails; test('reduce registerInstalledPlugins, clear again', () => { const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN])); diff --git a/desktop/app/src/reducers/__tests__/plugins.node.tsx b/desktop/app/src/reducers/__tests__/plugins.node.tsx index f16d15914..0d48b4652 100644 --- a/desktop/app/src/reducers/__tests__/plugins.node.tsx +++ b/desktop/app/src/reducers/__tests__/plugins.node.tsx @@ -83,7 +83,7 @@ test('add gatekeeped plugin', () => { dir: '/plugins/test', specVersion: 2, source: 'src/index.ts', - isDefault: false, + isBundled: false, main: 'lib/index.js', title: 'test', id: 'test', diff --git a/desktop/app/src/reducers/pluginDownloads.tsx b/desktop/app/src/reducers/pluginDownloads.tsx index 150f1b84d..83e32adee 100644 --- a/desktop/app/src/reducers/pluginDownloads.tsx +++ b/desktop/app/src/reducers/pluginDownloads.tsx @@ -7,7 +7,10 @@ * @format */ -import {DownloadablePluginDetails} from 'flipper-plugin-lib'; +import { + DownloadablePluginDetails, + getPluginVersionInstallationDir, +} from 'flipper-plugin-lib'; import {Actions} from '.'; import produce from 'immer'; import {Canceler} from 'axios'; @@ -66,18 +69,22 @@ export default function reducer( switch (action.type) { case 'PLUGIN_DOWNLOAD_START': { const {plugin, startedByUser} = action.payload; - const downloadState = state[plugin.dir]; + const installationDir = getPluginVersionInstallationDir( + plugin.name, + plugin.version, + ); + const downloadState = state[installationDir]; if (downloadState) { // If download is already in progress - re-use the existing state. return produce(state, (draft) => { - draft[plugin.dir] = { + draft[installationDir] = { ...downloadState, startedByUser: startedByUser || downloadState.startedByUser, }; }); } return produce(state, (draft) => { - draft[plugin.dir] = { + draft[installationDir] = { plugin, startedByUser: startedByUser, status: PluginDownloadStatus.QUEUED, @@ -86,15 +93,19 @@ export default function reducer( } case 'PLUGIN_DOWNLOAD_STARTED': { const {plugin, cancel} = action.payload; - const downloadState = state[plugin.dir]; + const installationDir = getPluginVersionInstallationDir( + plugin.name, + plugin.version, + ); + const downloadState = state[installationDir]; if (downloadState?.status !== PluginDownloadStatus.QUEUED) { console.warn( - `Invalid state transition PLUGIN_DOWNLOAD_STARTED in status ${downloadState?.status} for download to directory ${plugin.dir}.`, + `Invalid state transition PLUGIN_DOWNLOAD_STARTED in status ${downloadState?.status} for download to directory ${installationDir}.`, ); return state; } return produce(state, (draft) => { - draft[plugin.dir] = { + draft[installationDir] = { status: PluginDownloadStatus.STARTED, plugin, startedByUser: downloadState.startedByUser, @@ -104,8 +115,12 @@ export default function reducer( } case 'PLUGIN_DOWNLOAD_FINISHED': { const {plugin} = action.payload; + const installationDir = getPluginVersionInstallationDir( + plugin.name, + plugin.version, + ); return produce(state, (draft) => { - delete draft[plugin.dir]; + delete draft[installationDir]; }); } default: diff --git a/desktop/app/src/reducers/pluginManager.tsx b/desktop/app/src/reducers/pluginManager.tsx index c08106386..7d3664b88 100644 --- a/desktop/app/src/reducers/pluginManager.tsx +++ b/desktop/app/src/reducers/pluginManager.tsx @@ -8,19 +8,19 @@ */ import {Actions} from './'; -import {PluginDetails} from 'flipper-plugin-lib'; +import {InstalledPluginDetails} from 'flipper-plugin-lib'; import {PluginDefinition} from '../plugin'; import {produce} from 'immer'; export type State = { - installedPlugins: PluginDetails[]; + installedPlugins: InstalledPluginDetails[]; uninstalledPlugins: Set; }; export type Action = | { type: 'REGISTER_INSTALLED_PLUGINS'; - payload: PluginDetails[]; + payload: InstalledPluginDetails[]; } | { // Implemented by rootReducer in `store.tsx` @@ -48,7 +48,9 @@ export default function reducer( } } -export const registerInstalledPlugins = (payload: PluginDetails[]): Action => ({ +export const registerInstalledPlugins = ( + payload: InstalledPluginDetails[], +): Action => ({ type: 'REGISTER_INSTALLED_PLUGINS', payload, }); diff --git a/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx b/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx index f01a0eb68..32c1a5f48 100644 --- a/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx +++ b/desktop/app/src/sandy-chrome/appinspect/PluginList.tsx @@ -229,7 +229,7 @@ export const PluginList = memo(function PluginList({ tooltip={getPluginTooltip(plugin.details)} actions={ <> - {!plugin.details.isDefault && ( + {!plugin.details.isBundled && ( { +export default async function loadDynamicPlugins(): Promise< + InstalledPluginDetails[] +> { if (process.env.FLIPPER_FAST_REFRESH) { console.log( '❌ Skipping loading of dynamic plugins because Fast Refresh is enabled. Fast Refresh only works with bundled plugins.', diff --git a/desktop/app/src/utils/testUtils.tsx b/desktop/app/src/utils/testUtils.tsx index 7fdc3de20..24058fbda 100644 --- a/desktop/app/src/utils/testUtils.tsx +++ b/desktop/app/src/utils/testUtils.tsx @@ -39,15 +39,12 @@ export function createMockDownloadablePluginDetails( }, category: 'tools', description: 'Description of Test Plugin', - dir: `/Users/mock/.flipper/thirdparty/${name}`, - entry: `/Users/mock/.flipper/thirdparty/${name}/dist/bundle.js`, flipperSDKVersion: flipperEngineVersion, engines: { flipper: flipperEngineVersion, }, gatekeeper: gatekeeper ?? `GK_${lowercasedID}`, icon: 'internet', - isDefault: false, main: 'dist/bundle.js', source: 'src/index.tsx', specVersion: 2, @@ -55,6 +52,8 @@ export function createMockDownloadablePluginDetails( version: version, downloadUrl: `http://localhost/${lowercasedID}/${version}`, lastUpdated: lastUpdated, + isBundled: false, + isActivatable: false, }; return details; } diff --git a/desktop/flipper-plugin/src/plugin/SandyPluginDefinition.tsx b/desktop/flipper-plugin/src/plugin/SandyPluginDefinition.tsx index 798226c1f..bfb4cb41e 100644 --- a/desktop/flipper-plugin/src/plugin/SandyPluginDefinition.tsx +++ b/desktop/flipper-plugin/src/plugin/SandyPluginDefinition.tsx @@ -7,7 +7,7 @@ * @format */ -import {PluginDetails} from 'flipper-plugin-lib'; +import {ActivatablePluginDetails} from 'flipper-plugin-lib'; import {PluginFactory, FlipperPluginComponent} from './Plugin'; import {DevicePluginPredicate, DevicePluginFactory} from './DevicePlugin'; @@ -42,7 +42,7 @@ export type FlipperPluginModule> = { export class SandyPluginDefinition { id: string; module: FlipperPluginModule | FlipperDevicePluginModule; - details: PluginDetails; + details: ActivatablePluginDetails; isDevicePlugin: boolean; // TODO: Implement T68683476 @@ -58,10 +58,10 @@ export class SandyPluginDefinition { | undefined = undefined; constructor( - details: PluginDetails, + details: ActivatablePluginDetails, module: FlipperPluginModule | FlipperDevicePluginModule, ); - constructor(details: PluginDetails, module: any) { + constructor(details: ActivatablePluginDetails, module: any) { this.id = details.id; this.details = details; if (module.supportsDevice) { @@ -123,8 +123,8 @@ export class SandyPluginDefinition { return this.details.version; } - get isDefault() { - return this.details.isDefault; + get isBundled() { + return this.details.isBundled; } get keyboardActions() { diff --git a/desktop/flipper-plugin/src/test-utils/test-utils.tsx b/desktop/flipper-plugin/src/test-utils/test-utils.tsx index e783d133e..bf98db906 100644 --- a/desktop/flipper-plugin/src/test-utils/test-utils.tsx +++ b/desktop/flipper-plugin/src/test-utils/test-utils.tsx @@ -14,7 +14,7 @@ import { act as testingLibAct, } from '@testing-library/react'; import {queries} from '@testing-library/dom'; -import {PluginDetails} from 'flipper-plugin-lib'; +import {InstalledPluginDetails} from 'flipper-plugin-lib'; import { RealFlipperClient, @@ -383,15 +383,16 @@ function createBasePluginResult( } export function createMockPluginDetails( - details?: Partial, -): PluginDetails { + details?: Partial, +): InstalledPluginDetails { return { id: 'TestPlugin', dir: '', name: 'TestPlugin', specVersion: 0, entry: '', - isDefault: false, + isBundled: false, + isActivatable: true, main: '', source: '', title: 'Testing Plugin', diff --git a/desktop/pkg/src/commands/bundle.ts b/desktop/pkg/src/commands/bundle.ts index 6b01692ec..db9b9c493 100644 --- a/desktop/pkg/src/commands/bundle.ts +++ b/desktop/pkg/src/commands/bundle.ts @@ -12,7 +12,7 @@ import {args} from '@oclif/parser'; import fs from 'fs-extra'; import path from 'path'; import {runBuild} from 'flipper-pkg-lib'; -import {getPluginDetailsFromDir} from 'flipper-plugin-lib'; +import {getInstalledPluginDetails} from 'flipper-plugin-lib'; export default class Bundle extends Command { public static description = 'transpiles and bundles plugin'; @@ -55,7 +55,7 @@ export default class Bundle extends Command { `package.json is not found in plugin source directory ${inputDirectory}.`, ); } - const plugin = await getPluginDetailsFromDir(inputDirectory); + const plugin = await getInstalledPluginDetails(inputDirectory); const out = path.resolve(inputDirectory, plugin.main); await fs.ensureDir(path.dirname(out)); diff --git a/desktop/pkg/src/commands/pack.ts b/desktop/pkg/src/commands/pack.ts index 1219c5985..0ac8598ec 100644 --- a/desktop/pkg/src/commands/pack.ts +++ b/desktop/pkg/src/commands/pack.ts @@ -16,7 +16,7 @@ import * as path from 'path'; import * as yarn from '../utils/yarn'; import cli from 'cli-ux'; import {runBuild} from 'flipper-pkg-lib'; -import {getPluginDetailsFromDir} from 'flipper-plugin-lib'; +import {getInstalledPluginDetails} from 'flipper-plugin-lib'; async function deriveOutputFileName(inputDirectory: string): Promise { const packageJson = await readJSON(path.join(inputDirectory, 'package.json')); @@ -116,7 +116,7 @@ export default class Pack extends Command { cli.action.stop(); cli.action.start('Reading plugin details'); - const plugin = await getPluginDetailsFromDir(inputDirectory); + const plugin = await getInstalledPluginDetails(inputDirectory); const out = path.resolve(inputDirectory, plugin.main); cli.action.stop(`done. Source: ${plugin.source}. Main: ${plugin.main}.`); diff --git a/desktop/pkg/src/utils/runMigrate.ts b/desktop/pkg/src/utils/runMigrate.ts index bd0624d0d..d7bbf495d 100644 --- a/desktop/pkg/src/utils/runMigrate.ts +++ b/desktop/pkg/src/utils/runMigrate.ts @@ -9,7 +9,7 @@ import path from 'path'; import fs from 'fs-extra'; -import {getPluginDetails} from 'flipper-plugin-lib'; +import {getInstalledPluginDetails} from 'flipper-plugin-lib'; import {kebabCase} from 'lodash'; export default async function ( @@ -36,7 +36,7 @@ export default async function ( console.log(`⚙️ Migrating Flipper plugin package in ${dir}`); const packageJsonString = (await fs.readFile(packageJsonPath)).toString(); const packageJson = JSON.parse(packageJsonString); - const pluginDetails = await getPluginDetails(dir, packageJson); + const pluginDetails = await getInstalledPluginDetails(dir, packageJson); if (pluginDetails.specVersion === 2) { console.log( `✅ Plugin is already defined according to the latest specification version.`, diff --git a/desktop/plugin-lib/src/PluginDetails.ts b/desktop/plugin-lib/src/PluginDetails.ts index 94f504736..50b85f056 100644 --- a/desktop/plugin-lib/src/PluginDetails.ts +++ b/desktop/plugin-lib/src/PluginDetails.ts @@ -8,15 +8,12 @@ */ export interface PluginDetails { - dir: string; name: string; specVersion: number; version: string; source: string; main: string; id: string; - isDefault: boolean; - entry: string; gatekeeper?: string; title: string; icon?: string; @@ -32,7 +29,36 @@ export interface PluginDetails { flipperSDKVersion?: string; } -export interface DownloadablePluginDetails extends PluginDetails { +export interface ConcretePluginDetails extends PluginDetails { + // Determines whether the plugin is a part of the Flipper JS bundle. + isBundled: boolean; + // Determines whether the plugin is physically available for activation in Flipper. + isActivatable: boolean; +} + +// Describes plugin which is a part of the Flipper JS bundle. +export interface BundledPluginDetails extends ConcretePluginDetails { + isBundled: true; + isActivatable: true; +} + +// Describes plugin installed on the disk. +export interface InstalledPluginDetails extends ConcretePluginDetails { + isBundled: false; + isActivatable: true; + dir: string; + entry: string; +} + +// Describes plugin physically available for activation in Flipper. +export type ActivatablePluginDetails = + | BundledPluginDetails + | InstalledPluginDetails; + +// Describes plugin available for downloading. Until downloaded to the disk it is not available for activation in Flipper. +export interface DownloadablePluginDetails extends ConcretePluginDetails { + isActivatable: false; + isBundled: false; downloadUrl: string; lastUpdated: Date; } diff --git a/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts b/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts index c0a8f3243..65974fd96 100644 --- a/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts +++ b/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts @@ -9,7 +9,7 @@ import fs from 'fs-extra'; import path from 'path'; -import {getPluginDetailsFromDir} from '../getPluginDetails'; +import {getInstalledPluginDetails} from '../getPluginDetails'; import {pluginInstallationDir} from '../pluginPaths'; import {normalizePath} from 'flipper-test-utils'; @@ -31,7 +31,7 @@ test('getPluginDetailsV1', async () => { }; jest.mock('fs-extra', () => jest.fn()); fs.readJson = jest.fn().mockImplementation(() => pluginV1); - const details = await getPluginDetailsFromDir(pluginPath); + const details = await getInstalledPluginDetails(pluginPath); details.dir = normalizePath(details.dir); details.entry = normalizePath(details.entry); expect(details).toMatchInlineSnapshot(` @@ -45,7 +45,8 @@ test('getPluginDetailsV1', async () => { "gatekeeper": "GK_flipper_plugin_test", "icon": undefined, "id": "flipper-plugin-test", - "isDefault": false, + "isActivatable": true, + "isBundled": false, "main": "dist/bundle.js", "name": "flipper-plugin-test", "source": "src/index.tsx", @@ -69,7 +70,7 @@ test('getPluginDetailsV2', async () => { }; jest.mock('fs-extra', () => jest.fn()); fs.readJson = jest.fn().mockImplementation(() => pluginV2); - const details = await getPluginDetailsFromDir(pluginPath); + const details = await getInstalledPluginDetails(pluginPath); details.dir = normalizePath(details.dir); details.entry = normalizePath(details.entry); expect(details).toMatchInlineSnapshot(` @@ -83,7 +84,8 @@ test('getPluginDetailsV2', async () => { "gatekeeper": "GK_flipper_plugin_test", "icon": undefined, "id": "flipper-plugin-test", - "isDefault": false, + "isActivatable": true, + "isBundled": false, "main": "dist/bundle.js", "name": "flipper-plugin-test", "source": "src/index.tsx", @@ -107,7 +109,7 @@ 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 getPluginDetailsFromDir(pluginPath); + const details = await getInstalledPluginDetails(pluginPath); details.dir = normalizePath(details.dir); details.entry = normalizePath(details.entry); expect(details).toMatchInlineSnapshot(` @@ -121,7 +123,8 @@ test('id used as title if the latter omited', async () => { "gatekeeper": "GK_flipper_plugin_test", "icon": undefined, "id": "test", - "isDefault": false, + "isActivatable": true, + "isBundled": false, "main": "dist/bundle.js", "name": "flipper-plugin-test", "source": "src/index.tsx", @@ -144,7 +147,7 @@ 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 getPluginDetailsFromDir(pluginPath); + const details = await getInstalledPluginDetails(pluginPath); details.dir = normalizePath(details.dir); details.entry = normalizePath(details.entry); expect(details).toMatchInlineSnapshot(` @@ -158,7 +161,8 @@ test('name without "flipper-plugin-" prefix is used as title if the latter omite "gatekeeper": "GK_flipper_plugin_test", "icon": undefined, "id": "flipper-plugin-test", - "isDefault": false, + "isActivatable": true, + "isBundled": false, "main": "dist/bundle.js", "name": "flipper-plugin-test", "source": "src/index.tsx", @@ -184,7 +188,7 @@ test('flipper-plugin-version is parsed', async () => { }; jest.mock('fs-extra', () => jest.fn()); fs.readJson = jest.fn().mockImplementation(() => pluginV2); - const details = await getPluginDetailsFromDir(pluginPath); + const details = await getInstalledPluginDetails(pluginPath); details.dir = normalizePath(details.dir); details.entry = normalizePath(details.entry); expect(details).toMatchInlineSnapshot(` @@ -198,7 +202,8 @@ test('flipper-plugin-version is parsed', async () => { "gatekeeper": "GK_flipper_plugin_test", "icon": undefined, "id": "flipper-plugin-test", - "isDefault": false, + "isActivatable": true, + "isBundled": false, "main": "dist/bundle.js", "name": "flipper-plugin-test", "source": "src/index.tsx", diff --git a/desktop/plugin-lib/src/__tests__/getUpdatablePlugins.node.ts b/desktop/plugin-lib/src/__tests__/getUpdatablePlugins.node.ts index 38485c5c1..2f31e0ca0 100644 --- a/desktop/plugin-lib/src/__tests__/getUpdatablePlugins.node.ts +++ b/desktop/plugin-lib/src/__tests__/getUpdatablePlugins.node.ts @@ -18,7 +18,7 @@ import { import {getInstalledPlugins} from '../pluginInstaller'; import {mocked} from 'ts-jest/utils'; import type {Package} from 'npm-api'; -import PluginDetails from '../PluginDetails'; +import {InstalledPluginDetails} from '../PluginDetails'; jest.mock('npm-api', () => { return jest.fn().mockImplementation(() => { @@ -54,7 +54,7 @@ jest.mock('npm-api', () => { }); }); -const installedPlugins: PluginDetails[] = [ +const installedPlugins: InstalledPluginDetails[] = [ { name: 'flipper-plugin-hello', entry: './test/index.js', @@ -66,7 +66,8 @@ const installedPlugins: PluginDetails[] = [ id: 'Hello', title: 'Hello', description: 'World?', - isDefault: false, + isBundled: false, + isActivatable: true, }, { name: 'flipper-plugin-world', @@ -79,7 +80,8 @@ const installedPlugins: PluginDetails[] = [ id: 'World', title: 'World', description: 'Hello?', - isDefault: false, + isBundled: false, + isActivatable: true, }, ]; diff --git a/desktop/plugin-lib/src/getPluginDetails.ts b/desktop/plugin-lib/src/getPluginDetails.ts index fdc827226..7d46821cc 100644 --- a/desktop/plugin-lib/src/getPluginDetails.ts +++ b/desktop/plugin-lib/src/getPluginDetails.ts @@ -9,10 +9,14 @@ import fs from 'fs-extra'; import path from 'path'; -import {PluginDetails} from './PluginDetails'; -import {getPluginVersionInstallationDir, pluginCacheDir} from './pluginPaths'; +import { + DownloadablePluginDetails, + InstalledPluginDetails, + PluginDetails, +} from './PluginDetails'; +import {pluginCacheDir} from './pluginPaths'; -export async function getPluginDetails(pluginDir: string, packageJson: any) { +export function getPluginDetails(packageJson: any): PluginDetails { const specVersion = packageJson.$schema && packageJson.$schema === @@ -21,60 +25,61 @@ export async function getPluginDetails(pluginDir: string, packageJson: any) { : 1; switch (specVersion) { case 1: - return await getPluginDetailsV1(pluginDir, packageJson); + return getPluginDetailsV1(packageJson); case 2: - return await getPluginDetailsV2(pluginDir, packageJson); + return getPluginDetailsV2(packageJson); default: throw new Error(`Unknown plugin format version: ${specVersion}`); } } -export async function getPluginDetailsFromDir( - pluginDir: string, -): Promise { - const packageJson = await fs.readJson(path.join(pluginDir, 'package.json')); - return await getPluginDetails(pluginDir, packageJson); +export async function getInstalledPluginDetails( + dir: string, + packageJson?: any, +): Promise { + packageJson = + packageJson ?? (await fs.readJson(path.join(dir, 'package.json'))); + const pluginDetails = getPluginDetails(packageJson); + const entry = + pluginDetails.specVersion === 1 + ? path.resolve( + pluginCacheDir, + `${packageJson.name}@${packageJson.version || '0.0.0'}.js`, + ) + : path.resolve(dir, packageJson.main); + return { + ...pluginDetails, + isBundled: false, + isActivatable: true, + dir, + entry, + }; } -export async function getPluginDetailsFromPackageJson(packageJson: any) { - const pluginDir = getPluginVersionInstallationDir( - packageJson.name, - packageJson.version, - ); - return await getPluginDetails(pluginDir, packageJson); -} - -export async function getDownloadablePluginDetails( +export function getDownloadablePluginDetails( packageJson: any, downloadUrl: string, lastUpdated: Date, -) { - const details = await getPluginDetailsFromPackageJson(packageJson); +): 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. -async function getPluginDetailsV1( - pluginDir: string, - packageJson: any, -): Promise { +function getPluginDetailsV1(packageJson: any): PluginDetails { return { specVersion: 1, - dir: pluginDir, 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, @@ -86,19 +91,13 @@ async function getPluginDetailsV1( } // Plugins packaged using V2 are pre-bundled, so compilation in run-time is not required for them. -async function getPluginDetailsV2( - pluginDir: string, - packageJson: any, -): Promise { +function getPluginDetailsV2(packageJson: any): PluginDetails { return { specVersion: 2, - dir: pluginDir, 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, @@ -111,9 +110,10 @@ async function getPluginDetailsV2( }; } -function getTitleFromName(name: string) { +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/getSourcePlugins.ts b/desktop/plugin-lib/src/getSourcePlugins.ts index d925cf2cd..9434185c8 100644 --- a/desktop/plugin-lib/src/getSourcePlugins.ts +++ b/desktop/plugin-lib/src/getSourcePlugins.ts @@ -11,16 +11,17 @@ import path from 'path'; import fs from 'fs-extra'; import expandTilde from 'expand-tilde'; import {getPluginSourceFolders} from './pluginPaths'; -import {PluginDetails, getPluginDetails} from 'flipper-plugin-lib'; import pmap from 'p-map'; import pfilter from 'p-filter'; import {satisfies} from 'semver'; +import {getInstalledPluginDetails} from './getPluginDetails'; +import {InstalledPluginDetails} from './PluginDetails'; const flipperVersion = require('../package.json').version; -export async function getSourcePlugins(): Promise { +export async function getSourcePlugins(): Promise { const pluginFolders = await getPluginSourceFolders(); - const entryPoints: {[key: string]: PluginDetails} = {}; + const entryPoints: {[key: string]: InstalledPluginDetails} = {}; const additionalPlugins = await pmap(pluginFolders, (path) => entryPointForPluginFolder(path), ); @@ -47,7 +48,7 @@ export async function getSourcePlugins(): Promise { } async function entryPointForPluginFolder( pluginsDir: string, -): Promise<{[key: string]: PluginDetails}> { +): Promise<{[key: string]: InstalledPluginDetails}> { pluginsDir = expandTilde(pluginsDir); if (!fs.existsSync(pluginsDir)) { return {}; @@ -96,7 +97,7 @@ async function entryPointForPluginFolder( .then((packages) => pmap(packages, async ({manifest, dir}) => { try { - const details = await getPluginDetails(dir, manifest); + const details = await getInstalledPluginDetails(dir, manifest); if ( details.flipperSDKVersion && !satisfies(flipperVersion, details.flipperSDKVersion) @@ -117,7 +118,7 @@ async function entryPointForPluginFolder( ) .then((plugins) => plugins.filter(notNull)) .then((plugins) => - plugins.reduce<{[key: string]: PluginDetails}>((acc, cv) => { + plugins.reduce<{[key: string]: InstalledPluginDetails}>((acc, cv) => { acc[cv!.name] = cv!; return acc; }, {}), diff --git a/desktop/plugin-lib/src/getUpdatablePlugins.ts b/desktop/plugin-lib/src/getUpdatablePlugins.ts index 021d0339f..c78539952 100644 --- a/desktop/plugin-lib/src/getUpdatablePlugins.ts +++ b/desktop/plugin-lib/src/getUpdatablePlugins.ts @@ -7,15 +7,12 @@ * @format */ -import PluginDetails from './PluginDetails'; +import {InstalledPluginDetails} from './PluginDetails'; import {getInstalledPlugins} from './pluginInstaller'; import semver from 'semver'; import {getNpmHostedPlugins, NpmPackageDescriptor} from './getNpmHostedPlugins'; import NpmApi from 'npm-api'; -import { - getPluginDetails, - getPluginDetailsFromPackageJson, -} from './getPluginDetails'; +import {getInstalledPluginDetails, getPluginDetails} from './getPluginDetails'; import {getPluginVersionInstallationDir} from './pluginPaths'; import pmap from 'p-map'; import {notNull} from './typeUtils'; @@ -31,7 +28,7 @@ export type UpdatablePlugin = { updateStatus: UpdateResult; }; -export type UpdatablePluginDetails = PluginDetails & UpdatablePlugin; +export type UpdatablePluginDetails = InstalledPluginDetails & UpdatablePlugin; export async function getUpdatablePlugins( query?: string, @@ -51,7 +48,7 @@ export async function getUpdatablePlugins( semver.lt(installedPlugin.version, npmPackageDescriptor.version) ) { const pkg = await npmApi.repo(npmPackageDescriptor.name).package(); - const npmPluginDetails = await getPluginDetails( + const npmPluginDetails = await getInstalledPluginDetails( getPluginVersionInstallationDir( npmPackageDescriptor.name, npmPackageDescriptor.version, @@ -91,7 +88,7 @@ export async function getUpdatablePlugins( async (notInstalledPlugin) => { try { const pkg = await npmApi.repo(notInstalledPlugin.name).package(); - const npmPluginDetails = await getPluginDetailsFromPackageJson(pkg); + const npmPluginDetails = getPluginDetails(pkg); if (npmPluginDetails.specVersion === 1) { return null; } diff --git a/desktop/plugin-lib/src/pluginInstaller.ts b/desktop/plugin-lib/src/pluginInstaller.ts index 3b83adf62..49daa678e 100644 --- a/desktop/plugin-lib/src/pluginInstaller.ts +++ b/desktop/plugin-lib/src/pluginInstaller.ts @@ -15,8 +15,8 @@ import decompress from 'decompress'; import decompressTargz from 'decompress-targz'; import decompressUnzip from 'decompress-unzip'; import tmp from 'tmp'; -import PluginDetails from './PluginDetails'; -import {getPluginDetailsFromDir} from './getPluginDetails'; +import {InstalledPluginDetails} from './PluginDetails'; +import {getInstalledPluginDetails} from './getPluginDetails'; import { getPluginVersionInstallationDir, getPluginDirNameFromPackageName, @@ -37,8 +37,8 @@ function providePluginManagerNoDependencies(): PM { async function installPluginFromTempDir( sourceDir: string, -): Promise { - const pluginDetails = await getPluginDetailsFromDir(sourceDir); +): Promise { + const pluginDetails = await getInstalledPluginDetails(sourceDir); const {name, version} = pluginDetails; const backupDir = path.join(await getTmpDir(), `${name}-${version}`); const destinationDir = getPluginVersionInstallationDir(name, version); @@ -63,7 +63,7 @@ async function installPluginFromTempDir( } throw err; } - return await getPluginDetailsFromDir(destinationDir); + return await getInstalledPluginDetails(destinationDir); } async function getPluginRootDir(dir: string) { @@ -87,12 +87,12 @@ async function getPluginRootDir(dir: string) { export async function getInstalledPlugin( name: string, version: string, -): Promise { +): Promise { const dir = getPluginVersionInstallationDir(name, version); if (!(await fs.pathExists(dir))) { return null; } - return await getPluginDetailsFromDir(dir); + return await getInstalledPluginDetails(dir); } export async function installPluginFromNpm(name: string) { @@ -114,7 +114,7 @@ export async function installPluginFromNpm(name: string) { export async function installPluginFromFile( packagePath: string, -): Promise { +): Promise { const tmpDir = await getTmpDir(); try { const files = await decompress(packagePath, tmpDir, { @@ -140,14 +140,14 @@ export async function removePlugins( await pmap(names, (name) => removePlugin(name)); } -export async function getInstalledPlugins(): Promise { +export async function getInstalledPlugins(): Promise { const versionDirs = await getInstalledPluginVersionDirs(); return pmap( versionDirs .filter(([_, versionDirs]) => versionDirs.length > 0) .map(([_, versionDirs]) => versionDirs[0]), (latestVersionDir) => - getPluginDetailsFromDir(latestVersionDir).catch((err) => { + getInstalledPluginDetails(latestVersionDir).catch((err) => { console.error(`Failed to load plugin from ${latestVersionDir}`, err); return null; }), @@ -187,7 +187,7 @@ export async function moveInstalledPluginsFromLegacyDir() { ) .then((dirs) => pmap(dirs, (dir) => - getPluginDetailsFromDir(dir).catch(async (err) => { + getInstalledPluginDetails(dir).catch(async (err) => { console.error( `Failed to load plugin from ${dir} on moving legacy plugins. Removing it.`, err, diff --git a/desktop/scripts/build-utils.ts b/desktop/scripts/build-utils.ts index 9a823764c..88a189987 100644 --- a/desktop/scripts/build-utils.ts +++ b/desktop/scripts/build-utils.ts @@ -14,7 +14,11 @@ import fs from 'fs-extra'; import {spawn} from 'promisify-child-process'; import {getWatchFolders} from 'flipper-pkg-lib'; import getAppWatchFolders from './get-app-watch-folders'; -import {getSourcePlugins, getPluginSourceFolders} from 'flipper-plugin-lib'; +import { + getSourcePlugins, + getPluginSourceFolders, + BundledPluginDetails, +} from 'flipper-plugin-lib'; import { appDir, staticDir, @@ -33,19 +37,26 @@ 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; - plugin.version = plugin.version === '0.0.0' ? version : plugin.version; - plugin.flipperSDKVersion = - plugin.flipperSDKVersion === '0.0.0' ? version : plugin.flipperSDKVersion; - } + const sourcePlugins = await getSourcePlugins(); + const bundledPlugins = sourcePlugins.map( + (p) => + ({ + ...p, + isBundled: true, + version: p.version === '0.0.0' ? version : p.version, + flipperSDKVersion: + p.flipperSDKVersion === '0.0.0' ? version : p.flipperSDKVersion, + } as BundledPluginDetails), + ); if (await fs.pathExists(defaultPluginsIndexDir)) { await fs.remove(defaultPluginsIndexDir); } await fs.mkdirp(defaultPluginsIndexDir); - await fs.writeJSON(path.join(defaultPluginsIndexDir, 'index.json'), plugins); - const pluginRequres = plugins + await fs.writeJSON( + path.join(defaultPluginsIndexDir, 'index.json'), + bundledPlugins, + ); + const pluginRequres = bundledPlugins .map((x) => ` '${x.name}': require('${x.name}')`) .join(',\n'); const generatedIndex = `