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
This commit is contained in:
Anton Nikolaev
2020-12-15 09:28:58 -08:00
committed by Facebook GitHub Bot
parent efb82e80b5
commit 5383017299
27 changed files with 327 additions and 216 deletions

View File

@@ -139,7 +139,7 @@ class PluginDebugger extends Component<Props> {
getRows(): Array<TableBodyRow> {
const rows: Array<TableBodyRow> = [];
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(

View File

@@ -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',

View File

@@ -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<State, {}>([])(
);
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',

View File

@@ -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);
}
}

View File

@@ -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<PluginDetails> = [];
const disabledPlugins: Array<PluginDetails> = [];
const failedPlugins: Array<[PluginDetails, string]> = [];
const gatekeepedPlugins: Array<ActivatablePluginDetails> = [];
const disabledPlugins: Array<ActivatablePluginDetails> = [];
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<PluginDetails> {
function getBundledPlugins(): Array<BundledPluginDetails> {
// DefaultPlugins that are included in the bundle.
// List of defaultPlugins is written at build time
const pluginPath =
@@ -129,7 +133,7 @@ function getBundledPlugins(): Array<PluginDetails> {
? path.join(__dirname, 'defaultPlugins')
: './defaultPlugins/index.json');
let bundledPlugins: Array<PluginDetails> = [];
let bundledPlugins: Array<BundledPluginDetails> = [];
try {
bundledPlugins = global.electronRequire(pluginPath);
} catch (e) {
@@ -148,8 +152,8 @@ export async function getDynamicPlugins() {
}
}
export const checkGK = (gatekeepedPlugins: Array<PluginDetails>) => (
plugin: PluginDetails,
export const checkGK = (gatekeepedPlugins: Array<ActivatablePluginDetails>) => (
plugin: ActivatablePluginDetails,
): boolean => {
if (!plugin.gatekeeper) {
return true;
@@ -161,7 +165,9 @@ export const checkGK = (gatekeepedPlugins: Array<PluginDetails>) => (
return result;
};
export const checkDisabled = (disabledPlugins: Array<PluginDetails>) => {
export const checkDisabled = (
disabledPlugins: Array<ActivatablePluginDetails>,
) => {
const enabledList = process.env.FLIPPER_ENABLED_PLUGINS
? new Set<string>(process.env.FLIPPER_ENABLED_PLUGINS.split(','))
: null;
@@ -171,7 +177,7 @@ export const checkDisabled = (disabledPlugins: Array<PluginDetails>) => {
} 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<PluginDetails>) => {
};
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];
}
});
}

View File

@@ -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;

View File

@@ -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 = {

View File

@@ -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]));

View File

@@ -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',

View File

@@ -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:

View File

@@ -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<string>;
};
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,
});

View File

@@ -229,7 +229,7 @@ export const PluginList = memo(function PluginList({
tooltip={getPluginTooltip(plugin.details)}
actions={
<>
{!plugin.details.isDefault && (
{!plugin.details.isBundled && (
<ActionButton
id={plugin.id}
title="Uninstall plugin"

View File

@@ -10,16 +10,18 @@
import path from 'path';
import fs from 'fs-extra';
import {
PluginDetails,
getSourcePlugins,
getInstalledPlugins,
moveInstalledPluginsFromLegacyDir,
InstalledPluginDetails,
} from 'flipper-plugin-lib';
import {getStaticPath} from '../utils/pathUtils';
// Load "dynamic" plugins, e.g. those which are either installed or loaded from sources for development purposes.
// This opposed to "default" plugins which are included into Flipper bundle.
export default async function loadDynamicPlugins(): Promise<PluginDetails[]> {
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.',

View File

@@ -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;
}

View File

@@ -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<Factory extends PluginFactory<any, any>> = {
export class SandyPluginDefinition {
id: string;
module: FlipperPluginModule<any> | 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<any> | 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() {

View File

@@ -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>,
): PluginDetails {
details?: Partial<InstalledPluginDetails>,
): InstalledPluginDetails {
return {
id: 'TestPlugin',
dir: '',
name: 'TestPlugin',
specVersion: 0,
entry: '',
isDefault: false,
isBundled: false,
isActivatable: true,
main: '',
source: '',
title: 'Testing Plugin',

View File

@@ -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));

View File

@@ -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<string> {
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}.`);

View File

@@ -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.`,

View File

@@ -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;
}

View File

@@ -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",

View File

@@ -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,
},
];

View File

@@ -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<PluginDetails> {
const packageJson = await fs.readJson(path.join(pluginDir, 'package.json'));
return await getPluginDetails(pluginDir, packageJson);
export async function getInstalledPluginDetails(
dir: string,
packageJson?: any,
): Promise<InstalledPluginDetails> {
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<PluginDetails> {
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<PluginDetails> {
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;
}

View File

@@ -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<PluginDetails[]> {
export async function getSourcePlugins(): Promise<InstalledPluginDetails[]> {
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<PluginDetails[]> {
}
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;
}, {}),

View File

@@ -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;
}

View File

@@ -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<PluginDetails> {
const pluginDetails = await getPluginDetailsFromDir(sourceDir);
): Promise<InstalledPluginDetails> {
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<PluginDetails | null> {
): Promise<InstalledPluginDetails | null> {
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<PluginDetails> {
): Promise<InstalledPluginDetails> {
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<PluginDetails[]> {
export async function getInstalledPlugins(): Promise<InstalledPluginDetails[]> {
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,

View File

@@ -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 = `