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:
committed by
Facebook GitHub Bot
parent
efb82e80b5
commit
5383017299
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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}.`);
|
||||
|
||||
|
||||
@@ -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.`,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}, {}),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = `
|
||||
|
||||
Reference in New Issue
Block a user