move plugin management from ui-core to server-core
Summary: Follow up of D32665064, this diff moves all plugin management logic from flipper-ui to flipper-server. Things like downloading, installing, querying new plugins. Loading plugins is handled separately in the next diff. Reviewed By: nikoant Differential Revision: D32666537 fbshipit-source-id: 9786b82987f00180bb26200e38735b334dc4d5c3
This commit is contained in:
committed by
Facebook GitHub Bot
parent
f9b72ac69e
commit
64747dc417
@@ -98,4 +98,91 @@ export interface DownloadablePluginDetails extends ConcretePluginDetails {
|
||||
lastUpdated: Date;
|
||||
}
|
||||
|
||||
export default PluginDetails;
|
||||
export type UpdateResult =
|
||||
| {kind: 'not-installed'; version: string}
|
||||
| {kind: 'up-to-date'}
|
||||
| {kind: 'error'; error: Error}
|
||||
| {kind: 'update-available'; version: string};
|
||||
|
||||
export type UpdatablePlugin = {
|
||||
updateStatus: UpdateResult;
|
||||
};
|
||||
|
||||
export type UpdatablePluginDetails = InstalledPluginDetails & UpdatablePlugin;
|
||||
|
||||
export function getPluginDetails(packageJson: any): PluginDetails {
|
||||
const specVersion =
|
||||
packageJson.$schema &&
|
||||
packageJson.$schema ===
|
||||
'https://fbflipper.com/schemas/plugin-package/v2.json'
|
||||
? 2
|
||||
: 1;
|
||||
switch (specVersion) {
|
||||
case 1:
|
||||
return getPluginDetailsV1(packageJson);
|
||||
case 2:
|
||||
return getPluginDetailsV2(packageJson);
|
||||
default:
|
||||
throw new Error(`Unknown plugin format version: ${specVersion}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Plugins packaged using V1 are distributed as sources and compiled in run-time.
|
||||
function getPluginDetailsV1(packageJson: any): PluginDetails {
|
||||
return {
|
||||
specVersion: 1,
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
main: 'dist/bundle.js',
|
||||
source: packageJson.main,
|
||||
id: packageJson.name,
|
||||
gatekeeper: packageJson.gatekeeper,
|
||||
icon: packageJson.icon,
|
||||
title: packageJson.title || packageJson.name,
|
||||
description: packageJson.description,
|
||||
category: packageJson.category,
|
||||
bugs: packageJson.bugs,
|
||||
flipperSDKVersion: packageJson?.peerDependencies?.['flipper-plugin'],
|
||||
pluginType: packageJson?.pluginType,
|
||||
supportedDevices: packageJson?.supportedDevices,
|
||||
supportedApps: packageJson?.supportedApps,
|
||||
engines: packageJson.engines,
|
||||
};
|
||||
}
|
||||
|
||||
// Plugins packaged using V2 are pre-bundled, so compilation in run-time is not required for them.
|
||||
function getPluginDetailsV2(packageJson: any): PluginDetails {
|
||||
return {
|
||||
specVersion: 2,
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
main: packageJson.main,
|
||||
source: packageJson.flipperBundlerEntry,
|
||||
id: packageJson.id || packageJson.name,
|
||||
gatekeeper: packageJson.gatekeeper,
|
||||
icon: packageJson.icon,
|
||||
title:
|
||||
packageJson.title || packageJson.id || getTitleFromName(packageJson.name),
|
||||
description: packageJson.description,
|
||||
category: packageJson.category,
|
||||
bugs: packageJson.bugs,
|
||||
flipperSDKVersion: packageJson?.peerDependencies?.['flipper-plugin'],
|
||||
pluginType: packageJson?.pluginType,
|
||||
supportedDevices: packageJson?.supportedDevices,
|
||||
supportedApps: packageJson?.supportedApps,
|
||||
engines: packageJson.engines,
|
||||
publishedDocs: packageJson.publishedDocs,
|
||||
};
|
||||
}
|
||||
|
||||
function getTitleFromName(name: string): string {
|
||||
const prefix = 'flipper-plugin-';
|
||||
if (name.startsWith(prefix)) {
|
||||
return name.substr(prefix.length);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
export function isPluginJson(packageJson: any): boolean {
|
||||
return packageJson?.keywords?.includes('flipper-plugin');
|
||||
}
|
||||
|
||||
@@ -7,7 +7,15 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {DeviceSpec, DeviceType, OS as PluginOS} from './PluginDetails';
|
||||
import {
|
||||
BundledPluginDetails,
|
||||
DeviceSpec,
|
||||
DeviceType,
|
||||
DownloadablePluginDetails,
|
||||
InstalledPluginDetails,
|
||||
OS as PluginOS,
|
||||
UpdatablePluginDetails,
|
||||
} from './PluginDetails';
|
||||
import {LauncherSettings, ProcessConfig, Settings} from './settings';
|
||||
|
||||
// In the future, this file would deserve it's own package, as it doesn't really relate to plugins.
|
||||
@@ -152,6 +160,20 @@ export type FlipperServerCommands = {
|
||||
'keychain-write': (service: string, token: string) => Promise<void>;
|
||||
'keychain-read': (service: string) => Promise<string>;
|
||||
'keychain-unset': (service: string) => Promise<void>;
|
||||
'plugins-load-dynamic-plugins': () => Promise<InstalledPluginDetails[]>;
|
||||
'plugins-get-bundled-plugins': () => Promise<BundledPluginDetails[]>;
|
||||
'plugins-get-installed-plugins': () => Promise<InstalledPluginDetails[]>;
|
||||
'plugins-get-updatable-plugins': (
|
||||
query: string | undefined,
|
||||
) => Promise<UpdatablePluginDetails[]>;
|
||||
'plugin-start-download': (
|
||||
plugin: DownloadablePluginDetails,
|
||||
) => Promise<InstalledPluginDetails>;
|
||||
'plugins-install-from-npm': (name: string) => Promise<InstalledPluginDetails>;
|
||||
'plugins-install-from-file': (
|
||||
path: string,
|
||||
) => Promise<InstalledPluginDetails>;
|
||||
'plugins-remove-plugins': (names: string[]) => Promise<void>;
|
||||
};
|
||||
|
||||
export type ENVIRONMENT_VARIABLES =
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
"bugs": "https://github.com/facebook/flipper/issues",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"axios": "^0.22.0",
|
||||
"JSONStream": "^1.3.1",
|
||||
"adbkit": "^2.11.1",
|
||||
"adbkit-logcat": "^2.0.1",
|
||||
"archiver": "^5.3.0",
|
||||
"async-mutex": "^0.3.2",
|
||||
"flipper-plugin-lib": "0.0.0",
|
||||
"flipper-common": "0.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"invariant": "^2.2.4",
|
||||
|
||||
@@ -34,6 +34,7 @@ import {setFlipperServerConfig} from './FlipperServerConfig';
|
||||
import {saveSettings} from './utils/settings';
|
||||
import {saveLauncherSettings} from './utils/launcherSettings';
|
||||
import {KeytarManager} from './utils/keytar';
|
||||
import {PluginManager} from './plugins/PluginManager';
|
||||
|
||||
/**
|
||||
* FlipperServer takes care of all incoming device & client connections.
|
||||
@@ -53,6 +54,7 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
android: AndroidDeviceManager;
|
||||
ios: IOSDeviceManager;
|
||||
keytarManager: KeytarManager;
|
||||
pluginManager: PluginManager;
|
||||
|
||||
constructor(
|
||||
public config: FlipperServerConfig,
|
||||
@@ -64,6 +66,9 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
this.android = new AndroidDeviceManager(this);
|
||||
this.ios = new IOSDeviceManager(this);
|
||||
this.keytarManager = new KeytarManager(keytarModule);
|
||||
// TODO: given flipper-dump, it might make more sense to have the plugin command
|
||||
// handled by moved to flipper-server & app, but let's keep things simple for now
|
||||
this.pluginManager = new PluginManager();
|
||||
|
||||
server.addListener('error', (err) => {
|
||||
this.emit('server-error', err);
|
||||
@@ -122,6 +127,7 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
|
||||
try {
|
||||
await this.server.init();
|
||||
await this.pluginManager.start();
|
||||
await this.startDeviceListeners();
|
||||
this.setServerState('started');
|
||||
} catch (e) {
|
||||
@@ -247,6 +253,21 @@ export class FlipperServerImpl implements FlipperServer {
|
||||
'keychain-write': (service, password) =>
|
||||
this.keytarManager.writeKeychain(service, password),
|
||||
'keychain-unset': (service) => this.keytarManager.unsetKeychain(service),
|
||||
'plugins-load-dynamic-plugins': () =>
|
||||
this.pluginManager.loadDynamicPlugins(),
|
||||
'plugins-get-bundled-plugins': () => this.pluginManager.getBundledPlugins(),
|
||||
'plugins-get-installed-plugins': () =>
|
||||
this.pluginManager.getInstalledPlugins(),
|
||||
'plugins-remove-plugins': (plugins) =>
|
||||
this.pluginManager.removePlugins(plugins),
|
||||
'plugin-start-download': (details) =>
|
||||
this.pluginManager.downloadPlugin(details),
|
||||
'plugins-get-updatable-plugins': (query) =>
|
||||
this.pluginManager.getUpdatablePlugins(query),
|
||||
'plugins-install-from-file': (path) =>
|
||||
this.pluginManager.installPluginFromFile(path),
|
||||
'plugins-install-from-npm': (name) =>
|
||||
this.pluginManager.installPluginFromNpm(name),
|
||||
};
|
||||
|
||||
registerDevice(device: ServerDevice) {
|
||||
|
||||
10
desktop/flipper-server-core/src/fb-stubs/constants.tsx
Normal file
10
desktop/flipper-server-core/src/fb-stubs/constants.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
export const isFBBuild: boolean = false;
|
||||
147
desktop/flipper-server-core/src/plugins/PluginManager.tsx
Normal file
147
desktop/flipper-server-core/src/plugins/PluginManager.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import tmp from 'tmp';
|
||||
import {promisify} from 'util';
|
||||
import {default as axios} from 'axios';
|
||||
import {
|
||||
BundledPluginDetails,
|
||||
DownloadablePluginDetails,
|
||||
InstalledPluginDetails,
|
||||
} from 'flipper-common';
|
||||
import {getStaticPath} from '../utils/pathUtils';
|
||||
import {loadDynamicPlugins} from './loadDynamicPlugins';
|
||||
import {
|
||||
cleanupOldInstalledPluginVersions,
|
||||
getInstalledPluginDetails,
|
||||
getInstalledPlugins,
|
||||
getPluginVersionInstallationDir,
|
||||
installPluginFromFile,
|
||||
removePlugins,
|
||||
getUpdatablePlugins,
|
||||
getInstalledPlugin,
|
||||
installPluginFromNpm,
|
||||
} from 'flipper-plugin-lib';
|
||||
|
||||
const maxInstalledPluginVersionsToKeep = 2;
|
||||
|
||||
// Adapter which forces node.js implementation for axios instead of browser implementation
|
||||
// used by default in Electron. Node.js implementation is better, because it
|
||||
// supports streams which can be used for direct downloading to disk.
|
||||
const axiosHttpAdapter = require('axios/lib/adapters/http'); // eslint-disable-line import/no-commonjs
|
||||
|
||||
const getTempDirName = promisify(tmp.dir) as (
|
||||
options?: tmp.DirOptions,
|
||||
) => Promise<string>;
|
||||
|
||||
export class PluginManager {
|
||||
async start() {
|
||||
// This needn't happen immediately and is (light) I/O work.
|
||||
(window.requestIdleCallback || setImmediate)(() => {
|
||||
cleanupOldInstalledPluginVersions(maxInstalledPluginVersionsToKeep).catch(
|
||||
(err) =>
|
||||
console.error('Failed to clean up old installed plugins:', err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadDynamicPlugins = loadDynamicPlugins;
|
||||
getInstalledPlugins = getInstalledPlugins;
|
||||
removePlugins = removePlugins;
|
||||
getUpdatablePlugins = getUpdatablePlugins;
|
||||
getInstalledPlugin = getInstalledPlugin;
|
||||
installPluginFromFile = installPluginFromFile;
|
||||
installPluginFromNpm = installPluginFromNpm;
|
||||
|
||||
async getBundledPlugins(): Promise<Array<BundledPluginDetails>> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return [];
|
||||
}
|
||||
// defaultPlugins that are included in the Flipper distributive.
|
||||
// List of default bundled plugins is written at build time to defaultPlugins/bundled.json.
|
||||
const pluginPath = getStaticPath(
|
||||
path.join('defaultPlugins', 'bundled.json'),
|
||||
{asarUnpacked: true},
|
||||
);
|
||||
let bundledPlugins: Array<BundledPluginDetails> = [];
|
||||
try {
|
||||
bundledPlugins = await fs.readJson(pluginPath);
|
||||
} catch (e) {
|
||||
console.error('Failed to load list of bundled plugins', e);
|
||||
}
|
||||
return bundledPlugins;
|
||||
}
|
||||
|
||||
async downloadPlugin(
|
||||
plugin: DownloadablePluginDetails,
|
||||
): Promise<InstalledPluginDetails> {
|
||||
const {name, title, version, downloadUrl} = plugin;
|
||||
const installationDir = getPluginVersionInstallationDir(name, version);
|
||||
console.log(
|
||||
`Downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`,
|
||||
);
|
||||
const tmpDir = await getTempDirName();
|
||||
const tmpFile = path.join(tmpDir, `${name}-${version}.tgz`);
|
||||
try {
|
||||
const cancelationSource = axios.CancelToken.source();
|
||||
if (await fs.pathExists(installationDir)) {
|
||||
console.log(
|
||||
`Using existing files instead of downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}"`,
|
||||
);
|
||||
return await getInstalledPluginDetails(installationDir);
|
||||
} else {
|
||||
await fs.ensureDir(tmpDir);
|
||||
let percentCompleted = 0;
|
||||
const response = await axios.get(plugin.downloadUrl, {
|
||||
adapter: axiosHttpAdapter,
|
||||
cancelToken: cancelationSource.token,
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
},
|
||||
onDownloadProgress: async (progressEvent) => {
|
||||
const newPercentCompleted = !progressEvent.total
|
||||
? 0
|
||||
: Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
if (newPercentCompleted - percentCompleted >= 20) {
|
||||
percentCompleted = newPercentCompleted;
|
||||
console.log(
|
||||
`Downloading plugin "${title}" v${version} from "${downloadUrl}": ${percentCompleted}% completed (${progressEvent.loaded} from ${progressEvent.total})`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (response.headers['content-type'] !== 'application/octet-stream') {
|
||||
throw new Error(
|
||||
`It looks like you are not on VPN/Lighthouse. Unexpected content type received: ${response.headers['content-type']}.`,
|
||||
);
|
||||
}
|
||||
const responseStream = response.data as fs.ReadStream;
|
||||
const writeStream = responseStream.pipe(
|
||||
fs.createWriteStream(tmpFile, {autoClose: true}),
|
||||
);
|
||||
await new Promise((resolve, reject) =>
|
||||
writeStream.once('finish', resolve).once('error', reject),
|
||||
);
|
||||
return await installPluginFromFile(tmpFile);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to download plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
await fs.remove(tmpDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,7 @@ import {getStaticPath} from '../utils/pathUtils';
|
||||
|
||||
// Load "dynamic" plugins, e.g. those which are either pre-installed (default), installed or loaded from sources (for development).
|
||||
// This opposed to "bundled" plugins which are included into Flipper bundle.
|
||||
export default async function loadDynamicPlugins(): Promise<
|
||||
InstalledPluginDetails[]
|
||||
> {
|
||||
export async function loadDynamicPlugins(): Promise<InstalledPluginDetails[]> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return [];
|
||||
}
|
||||
40
desktop/flipper-server-core/src/utils/pathUtils.tsx
Normal file
40
desktop/flipper-server-core/src/utils/pathUtils.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
// We use sync access once per startup.
|
||||
/* eslint-disable node/no-sync */
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import {getFlipperServerConfig} from '../FlipperServerConfig';
|
||||
import {isFBBuild} from '../fb-stubs/constants';
|
||||
|
||||
export function getStaticPath(
|
||||
relativePath: string = '.',
|
||||
{asarUnpacked}: {asarUnpacked: boolean} = {asarUnpacked: false},
|
||||
) {
|
||||
const staticDir = getFlipperServerConfig().paths.staticPath;
|
||||
const absolutePath = path.resolve(staticDir, relativePath);
|
||||
// Unfortunately, path.resolve, fs.pathExists, fs.read etc do not automatically work with asarUnpacked files.
|
||||
// All these functions still look for files in "app.asar" even if they are unpacked.
|
||||
// Looks like automatic resolving for asarUnpacked files only work for "child_process" module.
|
||||
// So we're using a hack here to actually look to "app.asar.unpacked" dir instead of app.asar package.
|
||||
return asarUnpacked
|
||||
? absolutePath.replace('app.asar', 'app.asar.unpacked')
|
||||
: absolutePath;
|
||||
}
|
||||
|
||||
export function getChangelogPath() {
|
||||
const changelogPath = getStaticPath(isFBBuild ? 'facebook' : '.');
|
||||
if (fs.existsSync(changelogPath)) {
|
||||
return changelogPath;
|
||||
} else {
|
||||
throw new Error('Changelog path path does not exist: ' + changelogPath);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@
|
||||
"references": [
|
||||
{
|
||||
"path": "../flipper-common"
|
||||
},
|
||||
{
|
||||
"path": "../plugin-lib"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
"flipper-common": "0.0.0",
|
||||
"flipper-doctor": "0.0.0",
|
||||
"flipper-plugin": "0.0.0",
|
||||
"flipper-plugin-lib": "0.0.0",
|
||||
"flipper-ui-core": "0.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"immer": "^9.0.6",
|
||||
|
||||
@@ -14,23 +14,18 @@ import {
|
||||
reportPlatformFailures,
|
||||
reportUsage,
|
||||
InstalledPluginDetails,
|
||||
UpdateResult,
|
||||
UpdatablePluginDetails,
|
||||
} from 'flipper-common';
|
||||
import reloadFlipper from '../../utils/reloadFlipper';
|
||||
import {registerInstalledPlugins} from '../../reducers/plugins';
|
||||
import {
|
||||
UpdateResult,
|
||||
getInstalledPlugins,
|
||||
getUpdatablePlugins,
|
||||
removePlugin,
|
||||
UpdatablePluginDetails,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {installPluginFromNpm} from 'flipper-plugin-lib';
|
||||
import {State as AppState} from '../../reducers';
|
||||
import {connect} from 'react-redux';
|
||||
import {Dispatch, Action} from 'redux';
|
||||
import PluginPackageInstaller from './PluginPackageInstaller';
|
||||
import {Toolbar} from 'flipper-plugin';
|
||||
import {Alert, Button, Input, Tooltip, Typography} from 'antd';
|
||||
import {getRenderHostInstance} from '../../RenderHost';
|
||||
|
||||
const {Text, Link} = Typography;
|
||||
|
||||
@@ -327,8 +322,33 @@ export default connect<PropsFromState, DispatchFromProps, OwnProps, AppState>(
|
||||
}),
|
||||
(dispatch: Dispatch<Action<any>>) => ({
|
||||
refreshInstalledPlugins: async () => {
|
||||
const plugins = await getInstalledPlugins();
|
||||
const plugins = await await getRenderHostInstance().flipperServer!.exec(
|
||||
'plugins-get-installed-plugins',
|
||||
);
|
||||
dispatch(registerInstalledPlugins(plugins));
|
||||
},
|
||||
}),
|
||||
)(PluginInstaller);
|
||||
|
||||
async function installPluginFromNpm(
|
||||
name: string,
|
||||
): Promise<InstalledPluginDetails> {
|
||||
return await getRenderHostInstance().flipperServer!.exec(
|
||||
'plugins-install-from-npm',
|
||||
name,
|
||||
);
|
||||
}
|
||||
|
||||
async function removePlugin(name: string) {
|
||||
return await getRenderHostInstance().flipperServer!.exec(
|
||||
'plugins-remove-plugins',
|
||||
[name],
|
||||
);
|
||||
}
|
||||
|
||||
async function getUpdatablePlugins(query: string | undefined) {
|
||||
return await getRenderHostInstance().flipperServer!.exec(
|
||||
'plugins-get-updatable-plugins',
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
} from '../../ui';
|
||||
import styled from '@emotion/styled';
|
||||
import React, {useState} from 'react';
|
||||
import {installPluginFromFile} from 'flipper-plugin-lib';
|
||||
import {Toolbar, FileSelector} from 'flipper-plugin';
|
||||
import {getRenderHostInstance} from '../../RenderHost';
|
||||
|
||||
const CenteredGlyph = styled(Glyph)({
|
||||
margin: 'auto',
|
||||
@@ -51,7 +51,10 @@ export default function PluginPackageInstaller({
|
||||
setError(undefined);
|
||||
setInProgress(true);
|
||||
try {
|
||||
await installPluginFromFile(path);
|
||||
await getRenderHostInstance().flipperServer!.exec(
|
||||
'plugins-install-from-file',
|
||||
path,
|
||||
);
|
||||
await onInstall();
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
|
||||
@@ -7,19 +7,22 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
jest.mock('flipper-plugin-lib');
|
||||
|
||||
import {default as PluginInstaller} from '../PluginInstaller';
|
||||
import React from 'react';
|
||||
import {render, waitFor} from '@testing-library/react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {Provider} from 'react-redux';
|
||||
import type {PluginDetails} from 'flipper-common';
|
||||
import {getUpdatablePlugins, UpdatablePluginDetails} from 'flipper-plugin-lib';
|
||||
import type {PluginDetails, UpdatablePluginDetails} from 'flipper-common';
|
||||
import {Store} from '../../../reducers';
|
||||
import {mocked} from 'ts-jest/utils';
|
||||
import {getRenderHostInstance} from '../../../RenderHost';
|
||||
|
||||
const getUpdatablePluginsMock = mocked(getUpdatablePlugins);
|
||||
let getUpdatablePluginsMock: jest.Mock<any, any>;
|
||||
|
||||
beforeEach(() => {
|
||||
// flipperServer get resets before each test, no need to do so explicitly
|
||||
getUpdatablePluginsMock = getRenderHostInstance().flipperServer!.exec =
|
||||
jest.fn();
|
||||
});
|
||||
|
||||
function getStore(installedPlugins: PluginDetails[] = []): Store {
|
||||
return configureStore([])({
|
||||
@@ -70,10 +73,6 @@ const samplePluginDetails2: UpdatablePluginDetails = {
|
||||
|
||||
const SEARCH_RESULTS = [samplePluginDetails1, samplePluginDetails2];
|
||||
|
||||
afterEach(() => {
|
||||
getUpdatablePluginsMock.mockClear();
|
||||
});
|
||||
|
||||
test('load PluginInstaller list', async () => {
|
||||
getUpdatablePluginsMock.mockReturnValue(Promise.resolve(SEARCH_RESULTS));
|
||||
const component = (
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
jest.mock('../../../../app/src/defaultPlugins');
|
||||
jest.mock('../../utils/loadDynamicPlugins');
|
||||
import dispatcher, {
|
||||
getDynamicPlugins,
|
||||
checkDisabled,
|
||||
@@ -23,11 +22,9 @@ import {getLogger} from 'flipper-common';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import TestPlugin from './TestPlugin';
|
||||
import {_SandyPluginDefinition} from 'flipper-plugin';
|
||||
import {mocked} from 'ts-jest/utils';
|
||||
import loadDynamicPlugins from '../../utils/loadDynamicPlugins';
|
||||
import {getRenderHostInstance} from '../../RenderHost';
|
||||
|
||||
const loadDynamicPluginsMock = mocked(loadDynamicPlugins);
|
||||
let loadDynamicPluginsMock: jest.Mock;
|
||||
|
||||
const mockStore = configureStore<State, {}>([])(
|
||||
createRootReducer()(undefined, {type: 'INIT'}),
|
||||
@@ -56,13 +53,11 @@ const sampleBundledPluginDetails: BundledPluginDetails = {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
loadDynamicPluginsMock = getRenderHostInstance().flipperServer.exec =
|
||||
jest.fn();
|
||||
loadDynamicPluginsMock.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
loadDynamicPluginsMock.mockClear();
|
||||
});
|
||||
|
||||
test('dispatcher dispatches REGISTER_PLUGINS', async () => {
|
||||
await dispatcher(mockStore, logger);
|
||||
const actions = mockStore.getActions();
|
||||
@@ -70,7 +65,6 @@ test('dispatcher dispatches REGISTER_PLUGINS', async () => {
|
||||
});
|
||||
|
||||
test('getDynamicPlugins returns empty array on errors', async () => {
|
||||
const loadDynamicPluginsMock = mocked(loadDynamicPlugins);
|
||||
loadDynamicPluginsMock.mockRejectedValue(new Error('ooops'));
|
||||
const res = await getDynamicPlugins();
|
||||
expect(res).toEqual([]);
|
||||
|
||||
@@ -7,15 +7,7 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {
|
||||
getInstalledPluginDetails,
|
||||
getPluginVersionInstallationDir,
|
||||
installPluginFromFile,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {
|
||||
InstalledPluginDetails,
|
||||
DownloadablePluginDetails,
|
||||
} from 'flipper-common';
|
||||
import {DownloadablePluginDetails} from 'flipper-common';
|
||||
import {State, Store} from '../reducers/index';
|
||||
import {
|
||||
PluginDownloadStatus,
|
||||
@@ -23,25 +15,12 @@ import {
|
||||
pluginDownloadFinished,
|
||||
} from '../reducers/pluginDownloads';
|
||||
import {sideEffect} from '../utils/sideEffect';
|
||||
import {default as axios} from 'axios';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import tmp from 'tmp';
|
||||
import {promisify} from 'util';
|
||||
import {reportPlatformFailures, reportUsage} from 'flipper-common';
|
||||
import {loadPlugin} from '../reducers/pluginManager';
|
||||
import {showErrorNotification} from '../utils/notifications';
|
||||
import {pluginInstalled} from '../reducers/plugins';
|
||||
import {getAllClients} from '../reducers/connections';
|
||||
|
||||
// Adapter which forces node.js implementation for axios instead of browser implementation
|
||||
// used by default in Electron. Node.js implementation is better, because it
|
||||
// supports streams which can be used for direct downloading to disk.
|
||||
const axiosHttpAdapter = require('axios/lib/adapters/http'); // eslint-disable-line import/no-commonjs
|
||||
|
||||
const getTempDirName = promisify(tmp.dir) as (
|
||||
options?: tmp.DirOptions,
|
||||
) => Promise<string>;
|
||||
import {getRenderHostInstance} from '../RenderHost';
|
||||
|
||||
export default (store: Store) => {
|
||||
sideEffect(
|
||||
@@ -79,61 +58,18 @@ async function handlePluginDownload(
|
||||
startedByUser: boolean,
|
||||
store: Store,
|
||||
) {
|
||||
const {title, version, downloadUrl} = plugin;
|
||||
const dispatch = store.dispatch;
|
||||
const {name, title, version, downloadUrl} = plugin;
|
||||
const installationDir = getPluginVersionInstallationDir(name, version);
|
||||
console.log(
|
||||
`Downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`,
|
||||
`Downloading plugin "${title}" v${version} from "${downloadUrl}".`,
|
||||
);
|
||||
const tmpDir = await getTempDirName();
|
||||
const tmpFile = path.join(tmpDir, `${name}-${version}.tgz`);
|
||||
let installedPlugin: InstalledPluginDetails | undefined;
|
||||
try {
|
||||
const cancelationSource = axios.CancelToken.source();
|
||||
dispatch(pluginDownloadStarted({plugin, cancel: cancelationSource.cancel}));
|
||||
if (await fs.pathExists(installationDir)) {
|
||||
console.log(
|
||||
`Using existing files instead of downloading plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}"`,
|
||||
dispatch(pluginDownloadStarted({plugin}));
|
||||
const installedPlugin = await getRenderHostInstance().flipperServer!.exec(
|
||||
'plugin-start-download',
|
||||
plugin,
|
||||
);
|
||||
installedPlugin = await getInstalledPluginDetails(installationDir);
|
||||
} else {
|
||||
await fs.ensureDir(tmpDir);
|
||||
let percentCompleted = 0;
|
||||
const response = await axios.get(plugin.downloadUrl, {
|
||||
adapter: axiosHttpAdapter,
|
||||
cancelToken: cancelationSource.token,
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
},
|
||||
onDownloadProgress: async (progressEvent) => {
|
||||
const newPercentCompleted = !progressEvent.total
|
||||
? 0
|
||||
: Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
if (newPercentCompleted - percentCompleted >= 20) {
|
||||
percentCompleted = newPercentCompleted;
|
||||
console.log(
|
||||
`Downloading plugin "${title}" v${version} from "${downloadUrl}": ${percentCompleted}% completed (${progressEvent.loaded} from ${progressEvent.total})`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (response.headers['content-type'] !== 'application/octet-stream') {
|
||||
throw new Error(
|
||||
`It looks like you are not on VPN/Lighthouse. Unexpected content type received: ${response.headers['content-type']}.`,
|
||||
);
|
||||
}
|
||||
const responseStream = response.data as fs.ReadStream;
|
||||
const writeStream = responseStream.pipe(
|
||||
fs.createWriteStream(tmpFile, {autoClose: true}),
|
||||
);
|
||||
await new Promise((resolve, reject) =>
|
||||
writeStream.once('finish', resolve).once('error', reject),
|
||||
);
|
||||
installedPlugin = await installPluginFromFile(tmpFile);
|
||||
dispatch(pluginInstalled(installedPlugin));
|
||||
}
|
||||
if (pluginIsDisabledForAllConnectedClients(store.getState(), plugin)) {
|
||||
dispatch(
|
||||
loadPlugin({
|
||||
@@ -144,11 +80,11 @@ async function handlePluginDownload(
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`,
|
||||
`Successfully downloaded and installed plugin "${title}" v${version} from "${downloadUrl}".`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to download plugin "${title}" v${version} from "${downloadUrl}" to "${installationDir}".`,
|
||||
`Failed to download plugin "${title}" v${version} from "${downloadUrl}".`,
|
||||
error,
|
||||
);
|
||||
if (startedByUser) {
|
||||
@@ -160,7 +96,6 @@ async function handlePluginDownload(
|
||||
throw error;
|
||||
} finally {
|
||||
dispatch(pluginDownloadFinished({plugin}));
|
||||
await fs.remove(tmpDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,6 @@ import {
|
||||
SwitchPluginActionPayload,
|
||||
PluginCommand,
|
||||
} from '../reducers/pluginManager';
|
||||
import {
|
||||
getInstalledPlugins,
|
||||
cleanupOldInstalledPluginVersions,
|
||||
removePlugins,
|
||||
} from 'flipper-plugin-lib';
|
||||
import {sideEffect} from '../utils/sideEffect';
|
||||
import {requirePlugin} from './plugins';
|
||||
import {showErrorNotification} from '../utils/notifications';
|
||||
@@ -49,13 +44,18 @@ import {
|
||||
defaultEnabledBackgroundPlugins,
|
||||
} from '../utils/pluginUtils';
|
||||
import {getPluginKey} from '../utils/pluginKey';
|
||||
|
||||
const maxInstalledPluginVersionsToKeep = 2;
|
||||
import {getRenderHostInstance} from '../RenderHost';
|
||||
|
||||
async function refreshInstalledPlugins(store: Store) {
|
||||
await removePlugins(store.getState().plugins.uninstalledPluginNames.values());
|
||||
await cleanupOldInstalledPluginVersions(maxInstalledPluginVersionsToKeep);
|
||||
const plugins = await getInstalledPlugins();
|
||||
const flipperServer = getRenderHostInstance().flipperServer;
|
||||
if (!flipperServer) {
|
||||
throw new Error('Flipper Server not ready');
|
||||
}
|
||||
await flipperServer.exec(
|
||||
'plugins-remove-plugins',
|
||||
Array.from(store.getState().plugins.uninstalledPluginNames.values()),
|
||||
);
|
||||
const plugins = await flipperServer.exec('plugins-get-installed-plugins');
|
||||
return store.dispatch(registerInstalledPlugins(plugins));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import type {Store} from '../reducers/index';
|
||||
import type {Logger} from 'flipper-common';
|
||||
import type {InstalledPluginDetails, Logger} from 'flipper-common';
|
||||
import {PluginDefinition} from '../plugin';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
@@ -25,8 +25,6 @@ import {
|
||||
pluginsInitialized,
|
||||
} from '../reducers/plugins';
|
||||
import {FlipperBasePlugin} from '../plugin';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {notNull} from '../utils/typeUtils';
|
||||
import {
|
||||
ActivatablePluginDetails,
|
||||
@@ -36,7 +34,6 @@ import {
|
||||
import {tryCatchReportPluginFailures, reportUsage} from 'flipper-common';
|
||||
import * as FlipperPluginSDK from 'flipper-plugin';
|
||||
import {_SandyPluginDefinition} from 'flipper-plugin';
|
||||
import loadDynamicPlugins from '../utils/loadDynamicPlugins';
|
||||
import * as Immer from 'immer';
|
||||
import * as antd from 'antd';
|
||||
import * as emotion_styled from '@emotion/styled';
|
||||
@@ -47,7 +44,6 @@ import * as crc32 from 'crc32';
|
||||
import {isDevicePluginDefinition} from '../utils/pluginUtils';
|
||||
import isPluginCompatible from '../utils/isPluginCompatible';
|
||||
import isPluginVersionMoreRecent from '../utils/isPluginVersionMoreRecent';
|
||||
import {getStaticPath} from '../utils/pathUtils';
|
||||
import {createSandyPluginWrapper} from '../utils/createSandyPluginWrapper';
|
||||
import {getRenderHostInstance} from '../RenderHost';
|
||||
let defaultPluginsIndex: any = null;
|
||||
@@ -152,25 +148,23 @@ async function getBundledPlugins(): Promise<Array<BundledPluginDetails>> {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
// defaultPlugins that are included in the Flipper distributive.
|
||||
// List of default bundled plugins is written at build time to defaultPlugins/bundled.json.
|
||||
const pluginPath = getStaticPath(
|
||||
path.join('defaultPlugins', 'bundled.json'),
|
||||
{asarUnpacked: true},
|
||||
return await getRenderHostInstance().flipperServer!.exec(
|
||||
'plugins-get-bundled-plugins',
|
||||
);
|
||||
let bundledPlugins: Array<BundledPluginDetails> = [];
|
||||
try {
|
||||
bundledPlugins = await fs.readJson(pluginPath);
|
||||
} catch (e) {
|
||||
console.error('Failed to load list of bundled plugins', e);
|
||||
return [];
|
||||
}
|
||||
|
||||
return bundledPlugins;
|
||||
}
|
||||
|
||||
export async function getDynamicPlugins() {
|
||||
export async function getDynamicPlugins(): Promise<InstalledPluginDetails[]> {
|
||||
try {
|
||||
return await loadDynamicPlugins();
|
||||
return await getRenderHostInstance().flipperServer!.exec(
|
||||
'plugins-load-dynamic-plugins',
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to load dynamic plugins', e);
|
||||
return [];
|
||||
|
||||
@@ -8,10 +8,8 @@
|
||||
*/
|
||||
|
||||
import {DownloadablePluginDetails} from 'flipper-common';
|
||||
import {getPluginVersionInstallationDir} from 'flipper-plugin-lib';
|
||||
import {Actions} from '.';
|
||||
import produce from 'immer';
|
||||
import {Canceler} from 'axios';
|
||||
|
||||
export enum PluginDownloadStatus {
|
||||
QUEUED = 'Queued',
|
||||
@@ -24,7 +22,7 @@ export type DownloadablePluginState = {
|
||||
startedByUser: boolean;
|
||||
} & (
|
||||
| {status: PluginDownloadStatus.QUEUED}
|
||||
| {status: PluginDownloadStatus.STARTED; cancel: Canceler}
|
||||
| {status: PluginDownloadStatus.STARTED}
|
||||
);
|
||||
|
||||
// We use plugin installation path as key as it is unique for each plugin version.
|
||||
@@ -42,7 +40,6 @@ export type PluginDownloadStarted = {
|
||||
type: 'PLUGIN_DOWNLOAD_STARTED';
|
||||
payload: {
|
||||
plugin: DownloadablePluginDetails;
|
||||
cancel: Canceler;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -67,10 +64,7 @@ export default function reducer(
|
||||
switch (action.type) {
|
||||
case 'PLUGIN_DOWNLOAD_START': {
|
||||
const {plugin, startedByUser} = action.payload;
|
||||
const installationDir = getPluginVersionInstallationDir(
|
||||
plugin.name,
|
||||
plugin.version,
|
||||
);
|
||||
const installationDir = getDownloadKey(plugin.name, plugin.version);
|
||||
const downloadState = state[installationDir];
|
||||
if (downloadState) {
|
||||
// If download is already in progress - re-use the existing state.
|
||||
@@ -90,11 +84,8 @@ export default function reducer(
|
||||
});
|
||||
}
|
||||
case 'PLUGIN_DOWNLOAD_STARTED': {
|
||||
const {plugin, cancel} = action.payload;
|
||||
const installationDir = getPluginVersionInstallationDir(
|
||||
plugin.name,
|
||||
plugin.version,
|
||||
);
|
||||
const {plugin} = action.payload;
|
||||
const installationDir = getDownloadKey(plugin.name, plugin.version);
|
||||
const downloadState = state[installationDir];
|
||||
if (downloadState?.status !== PluginDownloadStatus.QUEUED) {
|
||||
console.warn(
|
||||
@@ -107,16 +98,12 @@ export default function reducer(
|
||||
status: PluginDownloadStatus.STARTED,
|
||||
plugin,
|
||||
startedByUser: downloadState.startedByUser,
|
||||
cancel,
|
||||
};
|
||||
});
|
||||
}
|
||||
case 'PLUGIN_DOWNLOAD_FINISHED': {
|
||||
const {plugin} = action.payload;
|
||||
const installationDir = getPluginVersionInstallationDir(
|
||||
plugin.name,
|
||||
plugin.version,
|
||||
);
|
||||
const installationDir = getDownloadKey(plugin.name, plugin.version);
|
||||
return produce(state, (draft) => {
|
||||
delete draft[installationDir];
|
||||
});
|
||||
@@ -136,9 +123,12 @@ export const startPluginDownload = (payload: {
|
||||
|
||||
export const pluginDownloadStarted = (payload: {
|
||||
plugin: DownloadablePluginDetails;
|
||||
cancel: Canceler;
|
||||
}): Action => ({type: 'PLUGIN_DOWNLOAD_STARTED', payload});
|
||||
|
||||
export const pluginDownloadFinished = (payload: {
|
||||
plugin: DownloadablePluginDetails;
|
||||
}): Action => ({type: 'PLUGIN_DOWNLOAD_FINISHED', payload});
|
||||
|
||||
function getDownloadKey(name: string, version: string) {
|
||||
return name.replace('/', '__') + '@' + version;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ import fs from 'fs';
|
||||
import config from '../fb-stubs/config';
|
||||
import {getRenderHostInstance} from '../RenderHost';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function getStaticPath(
|
||||
relativePath: string = '.',
|
||||
{asarUnpacked}: {asarUnpacked: boolean} = {asarUnpacked: false},
|
||||
@@ -31,6 +34,9 @@ export function getStaticPath(
|
||||
: absolutePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function getChangelogPath() {
|
||||
const changelogPath = getStaticPath(config.isFBBuild ? 'facebook' : '.');
|
||||
if (fs.existsSync(changelogPath)) {
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
{
|
||||
"path": "../flipper-plugin"
|
||||
},
|
||||
{
|
||||
"path": "../plugin-lib"
|
||||
},
|
||||
{
|
||||
"path": "../test-utils"
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {
|
||||
DownloadablePluginDetails,
|
||||
getPluginDetails,
|
||||
InstalledPluginDetails,
|
||||
PluginDetails,
|
||||
isPluginJson,
|
||||
} from 'flipper-common';
|
||||
import {pluginCacheDir} from './pluginPaths';
|
||||
|
||||
@@ -26,10 +26,6 @@ export async function readPluginPackageJson(dir: string): Promise<any> {
|
||||
}
|
||||
}
|
||||
|
||||
export function isPluginJson(packageJson: any): boolean {
|
||||
return packageJson?.keywords?.includes('flipper-plugin');
|
||||
}
|
||||
|
||||
export async function isPluginDir(dir: string): Promise<boolean> {
|
||||
const packageJsonPath = path.join(dir, 'package.json');
|
||||
const json = (await fs.pathExists(packageJsonPath))
|
||||
@@ -40,23 +36,6 @@ export async function isPluginDir(dir: string): Promise<boolean> {
|
||||
return isPluginJson(json);
|
||||
}
|
||||
|
||||
export function getPluginDetails(packageJson: any): PluginDetails {
|
||||
const specVersion =
|
||||
packageJson.$schema &&
|
||||
packageJson.$schema ===
|
||||
'https://fbflipper.com/schemas/plugin-package/v2.json'
|
||||
? 2
|
||||
: 1;
|
||||
switch (specVersion) {
|
||||
case 1:
|
||||
return getPluginDetailsV1(packageJson);
|
||||
case 2:
|
||||
return getPluginDetailsV2(packageJson);
|
||||
default:
|
||||
throw new Error(`Unknown plugin format version: ${specVersion}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getInstalledPluginDetails(
|
||||
dir: string,
|
||||
packageJson?: any,
|
||||
@@ -92,74 +71,3 @@ export async function getInstalledPluginDetails(
|
||||
entry,
|
||||
};
|
||||
}
|
||||
|
||||
export function getDownloadablePluginDetails(
|
||||
packageJson: any,
|
||||
downloadUrl: string,
|
||||
lastUpdated: Date,
|
||||
): 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.
|
||||
function getPluginDetailsV1(packageJson: any): PluginDetails {
|
||||
return {
|
||||
specVersion: 1,
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
main: 'dist/bundle.js',
|
||||
source: packageJson.main,
|
||||
id: packageJson.name,
|
||||
gatekeeper: packageJson.gatekeeper,
|
||||
icon: packageJson.icon,
|
||||
title: packageJson.title || packageJson.name,
|
||||
description: packageJson.description,
|
||||
category: packageJson.category,
|
||||
bugs: packageJson.bugs,
|
||||
flipperSDKVersion: packageJson?.peerDependencies?.['flipper-plugin'],
|
||||
pluginType: packageJson?.pluginType,
|
||||
supportedDevices: packageJson?.supportedDevices,
|
||||
supportedApps: packageJson?.supportedApps,
|
||||
engines: packageJson.engines,
|
||||
};
|
||||
}
|
||||
|
||||
// Plugins packaged using V2 are pre-bundled, so compilation in run-time is not required for them.
|
||||
function getPluginDetailsV2(packageJson: any): PluginDetails {
|
||||
return {
|
||||
specVersion: 2,
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
main: packageJson.main,
|
||||
source: packageJson.flipperBundlerEntry,
|
||||
id: packageJson.id || packageJson.name,
|
||||
gatekeeper: packageJson.gatekeeper,
|
||||
icon: packageJson.icon,
|
||||
title:
|
||||
packageJson.title || packageJson.id || getTitleFromName(packageJson.name),
|
||||
description: packageJson.description,
|
||||
category: packageJson.category,
|
||||
bugs: packageJson.bugs,
|
||||
flipperSDKVersion: packageJson?.peerDependencies?.['flipper-plugin'],
|
||||
pluginType: packageJson?.pluginType,
|
||||
supportedDevices: packageJson?.supportedDevices,
|
||||
supportedApps: packageJson?.supportedApps,
|
||||
engines: packageJson.engines,
|
||||
publishedDocs: packageJson.publishedDocs,
|
||||
};
|
||||
}
|
||||
|
||||
function getTitleFromName(name: string): string {
|
||||
const prefix = 'flipper-plugin-';
|
||||
if (name.startsWith(prefix)) {
|
||||
return name.substr(prefix.length);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -7,29 +7,21 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {InstalledPluginDetails} from 'flipper-common';
|
||||
import {
|
||||
UpdatablePluginDetails,
|
||||
UpdateResult,
|
||||
getPluginDetails,
|
||||
} from 'flipper-common';
|
||||
import {getInstalledPlugins} from './pluginInstaller';
|
||||
import semver from 'semver';
|
||||
import {getNpmHostedPlugins, NpmPackageDescriptor} from './getNpmHostedPlugins';
|
||||
import NpmApi from 'npm-api';
|
||||
import {getInstalledPluginDetails, getPluginDetails} from './getPluginDetails';
|
||||
import {getInstalledPluginDetails} from './getPluginDetails';
|
||||
import {getPluginVersionInstallationDir} from './pluginPaths';
|
||||
import pmap from 'p-map';
|
||||
import {notNull} from './typeUtils';
|
||||
const npmApi = new NpmApi();
|
||||
|
||||
export type UpdateResult =
|
||||
| {kind: 'not-installed'; version: string}
|
||||
| {kind: 'up-to-date'}
|
||||
| {kind: 'error'; error: Error}
|
||||
| {kind: 'update-available'; version: string};
|
||||
|
||||
export type UpdatablePlugin = {
|
||||
updateStatus: UpdateResult;
|
||||
};
|
||||
|
||||
export type UpdatablePluginDetails = InstalledPluginDetails & UpdatablePlugin;
|
||||
|
||||
export async function getUpdatablePlugins(
|
||||
query?: string,
|
||||
): Promise<UpdatablePluginDetails[]> {
|
||||
|
||||
@@ -109,7 +109,7 @@ export async function installPluginFromNpm(name: string) {
|
||||
tmpDir,
|
||||
getPluginDirNameFromPackageName(name),
|
||||
);
|
||||
await installPluginFromTempDir(pluginTempDir);
|
||||
return await installPluginFromTempDir(pluginTempDir);
|
||||
} finally {
|
||||
await fs.remove(tmpDir);
|
||||
}
|
||||
@@ -137,9 +137,7 @@ export async function removePlugin(name: string): Promise<void> {
|
||||
await fs.remove(getPluginInstallationDir(name));
|
||||
}
|
||||
|
||||
export async function removePlugins(
|
||||
names: IterableIterator<string>,
|
||||
): Promise<void> {
|
||||
export async function removePlugins(names: Array<string>): Promise<void> {
|
||||
await pmap(names, (name) => removePlugin(name));
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {EOL} from 'os';
|
||||
import pmap from 'p-map';
|
||||
import {rootDir} from './paths';
|
||||
import yargs from 'yargs';
|
||||
import {isPluginJson} from 'flipper-plugin-lib';
|
||||
import {isPluginJson} from 'flipper-common';
|
||||
|
||||
const argv = yargs
|
||||
.usage('yarn tsc-plugins [args]')
|
||||
|
||||
@@ -15,7 +15,7 @@ import globImport from 'glob';
|
||||
import pfilter from 'p-filter';
|
||||
import pmap from 'p-map';
|
||||
import {execSync} from 'child_process';
|
||||
import {isPluginJson} from 'flipper-plugin-lib';
|
||||
import {isPluginJson} from 'flipper-common';
|
||||
const glob = promisify(globImport);
|
||||
|
||||
export interface Package {
|
||||
|
||||
Reference in New Issue
Block a user