Keep multiple installed versions of each plugin
Summary:
This diff changes directory structure for installed plugins to allow installation of multiple versions simultaneously, e.g. to to allow downloading new plugin version while user is still using the previous one, and to have possibility of fast rollback to the previous installed if necessary. The new folder for installed plugins is located in `~/.flipper/installed-plugins` and has the following structure:
flipper-plugin-reactotron
1.0.0
...
package.json
1.0.1
...
package.json
flipper-plugin-network
0.67.1
...
package.json
0.67.2
...
package.json
The tricky part here is that we also need to migrate already installed plugins from the old folder `~/.flipper/thirdparty` to the new folder and maintain the new structure for them.
Another tricky part is that we need to periodically cleanup old versions. For now we will just keep 2 versions of each plugin. Cleanup is performed in background right after Flipper startup.
Reviewed By: mweststrate
Differential Revision: D25393474
fbshipit-source-id: 26617ac26114148f797cc3d6765a42242edc205e
This commit is contained in:
committed by
Facebook GitHub Bot
parent
9c5f59e109
commit
02d695cb28
@@ -36,7 +36,7 @@ import {
|
||||
getUpdatablePlugins,
|
||||
removePlugin,
|
||||
UpdatablePluginDetails,
|
||||
InstalledPluginDetails,
|
||||
PluginDetails,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {installPluginFromNpm} from 'flipper-plugin-lib';
|
||||
import {State as AppState} from '../../reducers';
|
||||
@@ -92,7 +92,7 @@ const RestartBar = styled(FlexColumn)({
|
||||
});
|
||||
|
||||
type PropsFromState = {
|
||||
installedPlugins: InstalledPluginDetails[];
|
||||
installedPlugins: PluginDetails[];
|
||||
};
|
||||
|
||||
type DispatchFromProps = {
|
||||
@@ -289,7 +289,7 @@ function InstallButton(props: {
|
||||
function useNPMSearch(
|
||||
query: string,
|
||||
onInstall: () => void,
|
||||
installedPlugins: InstalledPluginDetails[],
|
||||
installedPlugins: PluginDetails[],
|
||||
): TableRows_immutable {
|
||||
useEffect(() => {
|
||||
reportUsage(`${TAG}:open`);
|
||||
|
||||
@@ -14,14 +14,14 @@ import React from 'react';
|
||||
import {render, waitForElement} from '@testing-library/react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {Provider} from 'react-redux';
|
||||
import type {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||
import type {PluginDetails} from 'flipper-plugin-lib';
|
||||
import {getUpdatablePlugins, UpdatablePluginDetails} from 'flipper-plugin-lib';
|
||||
import {Store} from '../../../reducers';
|
||||
import {mocked} from 'ts-jest/utils';
|
||||
|
||||
const getUpdatablePluginsMock = mocked(getUpdatablePlugins);
|
||||
|
||||
function getStore(installedPlugins: InstalledPluginDetails[] = []): Store {
|
||||
function getStore(installedPlugins: PluginDetails[] = []): Store {
|
||||
return configureStore([])({
|
||||
application: {sessionId: 'mysession'},
|
||||
pluginManager: {installedPlugins},
|
||||
@@ -95,9 +95,7 @@ test('load PluginInstaller list with one plugin installed', async () => {
|
||||
samplePluginDetails2,
|
||||
]),
|
||||
);
|
||||
const store = getStore([
|
||||
{...samplePluginDetails1, installationStatus: 'installed'},
|
||||
]);
|
||||
const store = getStore([samplePluginDetails1]);
|
||||
const component = (
|
||||
<Provider store={store}>
|
||||
<PluginInstaller
|
||||
|
||||
@@ -10,12 +10,17 @@
|
||||
import {Store} from '../reducers/index';
|
||||
import {Logger} from '../fb-interfaces/Logger';
|
||||
import {registerInstalledPlugins} from '../reducers/pluginManager';
|
||||
import {getInstalledPlugins} from 'flipper-plugin-lib';
|
||||
import {
|
||||
getInstalledPlugins,
|
||||
cleanupOldInstalledPluginVersions,
|
||||
} from 'flipper-plugin-lib';
|
||||
|
||||
const maxInstalledPluginVersionsToKeep = 2;
|
||||
|
||||
function refreshInstalledPlugins(store: Store) {
|
||||
getInstalledPlugins().then((plugins) =>
|
||||
store.dispatch(registerInstalledPlugins(plugins)),
|
||||
);
|
||||
cleanupOldInstalledPluginVersions(maxInstalledPluginVersionsToKeep)
|
||||
.then(() => getInstalledPlugins())
|
||||
.then((plugins) => store.dispatch(registerInstalledPlugins(plugins)));
|
||||
}
|
||||
|
||||
export default (store: Store, _logger: Logger) => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import {default as reducer, registerInstalledPlugins} from '../pluginManager';
|
||||
import {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||
import {PluginDetails} from 'flipper-plugin-lib';
|
||||
|
||||
test('reduce empty registerInstalledPlugins', () => {
|
||||
const result = reducer(undefined, registerInstalledPlugins([]));
|
||||
@@ -27,8 +27,7 @@ const EXAMPLE_PLUGIN = {
|
||||
title: 'test',
|
||||
id: 'test',
|
||||
entry: '/plugins/test/lib/index.js',
|
||||
installationStatus: 'installed',
|
||||
} as InstalledPluginDetails;
|
||||
} as PluginDetails;
|
||||
|
||||
test('reduce registerInstalledPlugins, clear again', () => {
|
||||
const result = reducer(undefined, registerInstalledPlugins([EXAMPLE_PLUGIN]));
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
*/
|
||||
|
||||
import {Actions} from './';
|
||||
import {InstalledPluginDetails} from 'flipper-plugin-lib';
|
||||
import {PluginDetails} from 'flipper-plugin-lib';
|
||||
|
||||
export type State = {
|
||||
installedPlugins: InstalledPluginDetails[];
|
||||
installedPlugins: PluginDetails[];
|
||||
};
|
||||
|
||||
export type Action = {
|
||||
type: 'REGISTER_INSTALLED_PLUGINS';
|
||||
payload: InstalledPluginDetails[];
|
||||
payload: PluginDetails[];
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
@@ -37,9 +37,7 @@ export default function reducer(
|
||||
}
|
||||
}
|
||||
|
||||
export const registerInstalledPlugins = (
|
||||
payload: InstalledPluginDetails[],
|
||||
): Action => ({
|
||||
export const registerInstalledPlugins = (payload: PluginDetails[]): Action => ({
|
||||
type: 'REGISTER_INSTALLED_PLUGINS',
|
||||
payload,
|
||||
});
|
||||
|
||||
@@ -9,20 +9,16 @@
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import pMap from 'p-map';
|
||||
import {
|
||||
PluginDetails,
|
||||
getSourcePlugins,
|
||||
getInstalledPlugins,
|
||||
finishPendingPluginInstallations,
|
||||
moveInstalledPluginsFromLegacyDir,
|
||||
} from 'flipper-plugin-lib';
|
||||
import os from 'os';
|
||||
import {getStaticPath} from '../utils/pathUtils';
|
||||
|
||||
const pluginCache = path.join(os.homedir(), '.flipper', 'plugins');
|
||||
|
||||
// Load "dynamic" plugins, e.g. those which are either installed or loaded from sources for development purposes.
|
||||
// This opposed to "static" plugins which are already included into Flipper bundle.
|
||||
// This opposed to "default" plugins which are included into Flipper bundle.
|
||||
export default async function loadDynamicPlugins(): Promise<PluginDetails[]> {
|
||||
if (process.env.FLIPPER_FAST_REFRESH) {
|
||||
console.log(
|
||||
@@ -30,63 +26,38 @@ export default async function loadDynamicPlugins(): Promise<PluginDetails[]> {
|
||||
);
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
await finishPendingPluginInstallations();
|
||||
} catch (err) {
|
||||
console.error('❌ Failed to finish pending installations', err);
|
||||
}
|
||||
await moveInstalledPluginsFromLegacyDir().catch((ex) =>
|
||||
console.error(
|
||||
'Eror while migrating installed plugins from legacy folder',
|
||||
ex,
|
||||
),
|
||||
);
|
||||
const staticPath = getStaticPath();
|
||||
const defaultPlugins = new Set<string>(
|
||||
(
|
||||
await fs.readJson(path.join(staticPath, 'defaultPlugins', 'index.json'))
|
||||
).map((p: any) => p.name) as string[],
|
||||
);
|
||||
const dynamicPlugins = [
|
||||
...(await getInstalledPlugins()),
|
||||
...(await getSourcePlugins()).filter((p) => !defaultPlugins.has(p.name)),
|
||||
];
|
||||
await fs.ensureDir(pluginCache);
|
||||
const compilations = pMap(
|
||||
dynamicPlugins,
|
||||
(plugin) => {
|
||||
return loadPlugin(plugin);
|
||||
},
|
||||
{concurrency: 4},
|
||||
const [installedPlugins, unfilteredSourcePlugins] = await Promise.all([
|
||||
getInstalledPlugins(),
|
||||
getSourcePlugins(),
|
||||
]);
|
||||
const sourcePlugins = unfilteredSourcePlugins.filter(
|
||||
(p) => !defaultPlugins.has(p.name),
|
||||
);
|
||||
const compiledDynamicPlugins = (await compilations).filter(
|
||||
(c) => c !== null,
|
||||
) as PluginDetails[];
|
||||
console.log(
|
||||
`✅ Loaded ${dynamicPlugins.length} dynamic plugins: ${dynamicPlugins
|
||||
.map((x) => x.title)
|
||||
.join(', ')}.`,
|
||||
);
|
||||
return compiledDynamicPlugins;
|
||||
}
|
||||
async function loadPlugin(
|
||||
pluginDetails: PluginDetails,
|
||||
): Promise<PluginDetails | null> {
|
||||
const {specVersion, version, entry, name} = pluginDetails;
|
||||
if (specVersion > 1) {
|
||||
if (await fs.pathExists(entry)) {
|
||||
return pluginDetails;
|
||||
} else {
|
||||
console.error(
|
||||
`❌ Plugin ${name} is ignored, because its entry point not found: ${entry}.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// Try to load cached version of legacy plugin
|
||||
const entry = path.join(pluginCache, `${name}@${version || '0.0.0'}.js`);
|
||||
if (await fs.pathExists(entry)) {
|
||||
console.log(`🥫 Using cached version of legacy plugin ${name}...`);
|
||||
return pluginDetails;
|
||||
} else {
|
||||
console.error(
|
||||
`❌ Plugin ${name} is ignored, because it is defined by the unsupported spec v1 and could not be compiled.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (installedPlugins.length > 0) {
|
||||
console.log(
|
||||
`✅ Loaded ${
|
||||
installedPlugins.length
|
||||
} installed plugins: ${installedPlugins.map((x) => x.title).join(', ')}.`,
|
||||
);
|
||||
}
|
||||
if (sourcePlugins.length > 0) {
|
||||
console.log(
|
||||
`✅ Loaded ${sourcePlugins.length} source plugins: ${sourcePlugins
|
||||
.map((x) => x.title)
|
||||
.join(', ')}.`,
|
||||
);
|
||||
}
|
||||
return [...installedPlugins, ...sourcePlugins];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user