Plugin Marketplace API

Summary: Extracted plugin marketplace API to a separate file and updated it to load full plugin manifests.

Reviewed By: passy

Differential Revision: D25181759

fbshipit-source-id: a63f9ce16249ccc170df148cef5c209fdc6d4d6d
This commit is contained in:
Anton Nikolaev
2020-12-15 09:28:58 -08:00
committed by Facebook GitHub Bot
parent 658b3e8a91
commit 5b26f36672
11 changed files with 152 additions and 58 deletions

View File

@@ -7,7 +7,7 @@
* @format
*/
export default interface PluginDetails {
export interface PluginDetails {
dir: string;
name: string;
specVersion: number;
@@ -22,9 +22,19 @@ export default interface PluginDetails {
icon?: string;
description?: string;
category?: string;
engines?: {
[name: string]: string;
};
bugs?: {
email?: string;
url?: string;
};
flipperSDKVersion?: string;
}
export interface DownloadablePluginDetails extends PluginDetails {
downloadUrl: string;
lastUpdated: Date;
}
export default PluginDetails;

View File

@@ -9,7 +9,7 @@
import fs from 'fs-extra';
import path from 'path';
import getPluginDetails from '../getPluginDetails';
import {getPluginDetailsFromDir} 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 getPluginDetails(pluginPath);
const details = await getPluginDetailsFromDir(pluginPath);
details.dir = normalizePath(details.dir);
details.entry = normalizePath(details.entry);
expect(details).toMatchInlineSnapshot(`
@@ -69,7 +69,7 @@ test('getPluginDetailsV2', async () => {
};
jest.mock('fs-extra', () => jest.fn());
fs.readJson = jest.fn().mockImplementation(() => pluginV2);
const details = await getPluginDetails(pluginPath);
const details = await getPluginDetailsFromDir(pluginPath);
details.dir = normalizePath(details.dir);
details.entry = normalizePath(details.entry);
expect(details).toMatchInlineSnapshot(`
@@ -107,7 +107,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 getPluginDetails(pluginPath);
const details = await getPluginDetailsFromDir(pluginPath);
details.dir = normalizePath(details.dir);
details.entry = normalizePath(details.entry);
expect(details).toMatchInlineSnapshot(`
@@ -144,7 +144,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 getPluginDetails(pluginPath);
const details = await getPluginDetailsFromDir(pluginPath);
details.dir = normalizePath(details.dir);
details.entry = normalizePath(details.entry);
expect(details).toMatchInlineSnapshot(`
@@ -184,7 +184,7 @@ test('flipper-plugin-version is parsed', async () => {
};
jest.mock('fs-extra', () => jest.fn());
fs.readJson = jest.fn().mockImplementation(() => pluginV2);
const details = await getPluginDetails(pluginPath);
const details = await getPluginDetailsFromDir(pluginPath);
details.dir = normalizePath(details.dir);
details.entry = normalizePath(details.entry);
expect(details).toMatchInlineSnapshot(`

View File

@@ -15,7 +15,7 @@ import {
pluginInstallationDir,
} from './pluginPaths';
import PluginDetails from './PluginDetails';
import getPluginDetails from './getPluginDetails';
import {getPluginDetailsFromDir} from './getPluginDetails';
import pmap from 'p-map';
import {notNull} from './typeUtils';
@@ -40,7 +40,7 @@ async function getFullyInstalledPlugins(): Promise<PluginDetails[]> {
return undefined;
}
try {
return await getPluginDetails(pluginDir);
return await getPluginDetailsFromDir(pluginDir);
} catch (e) {
console.error(`Failed to load plugin from ${pluginDir}`, e);
return undefined;
@@ -71,7 +71,7 @@ async function getPendingInstallationPlugins(): Promise<PluginDetails[]> {
return undefined;
}
try {
return await getPluginDetails(pluginDir);
return await getPluginDetailsFromDir(pluginDir);
} catch (e) {
console.error(`Failed to load plugin from ${pluginDir}`, e);
return undefined;

View File

@@ -9,15 +9,10 @@
import fs from 'fs-extra';
import path from 'path';
import PluginDetails from './PluginDetails';
import {pluginCacheDir} from './pluginPaths';
import {PluginDetails} from './PluginDetails';
import {getPluginInstallationDir, pluginCacheDir} from './pluginPaths';
export default async function (
pluginDir: string,
packageJson?: any,
): Promise<PluginDetails> {
packageJson =
packageJson || (await fs.readJson(path.join(pluginDir, 'package.json')));
export async function getPluginDetails(pluginDir: string, packageJson: any) {
const specVersion =
packageJson.$schema &&
packageJson.$schema ===
@@ -34,6 +29,31 @@ export default async function (
}
}
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 getPluginDetailsFromPackageJson(packageJson: any) {
const pluginDir = getPluginInstallationDir(packageJson.name);
return await getPluginDetails(pluginDir, packageJson);
}
export async function getDownloadablePluginDetails(
packageJson: any,
downloadUrl: string,
lastUpdated: Date,
) {
const details = await getPluginDetailsFromPackageJson(packageJson);
return {
...details,
downloadUrl,
lastUpdated,
};
}
// Plugins packaged using V1 are distributed as sources and compiled in run-time.
async function getPluginDetailsV1(
pluginDir: string,

View File

@@ -12,8 +12,8 @@ import {getInstalledPlugins} from './getInstalledPlugins';
import semver from 'semver';
import {getNpmHostedPlugins, NpmPackageDescriptor} from './getNpmHostedPlugins';
import NpmApi from 'npm-api';
import getPluginDetails from './getPluginDetails';
import {getPluginInstallationDir} from './pluginInstaller';
import {getPluginDetails} from './getPluginDetails';
import {getPluginInstallationDir} from './pluginPaths';
import pmap from 'p-map';
import {notNull} from './typeUtils';
const npmApi = new NpmApi();

View File

@@ -7,10 +7,10 @@
* @format
*/
export {default as PluginDetails} from './PluginDetails';
export {default as getPluginDetails} from './getPluginDetails';
export * from './PluginDetails';
export * from './getPluginDetails';
export * from './pluginInstaller';
export * from './getInstalledPlugins';
export * from './getUpdatablePlugins';
export * from './getSourcePlugins';
export {getPluginSourceFolders} from './pluginPaths';
export * from './pluginPaths';

View File

@@ -16,10 +16,14 @@ import decompressTargz from 'decompress-targz';
import decompressUnzip from 'decompress-unzip';
import tmp from 'tmp';
import PluginDetails from './PluginDetails';
import getPluginDetails from './getPluginDetails';
import {getPluginDetailsFromDir} from './getPluginDetails';
import {
getPluginInstallationDir,
getPluginPendingInstallationDir,
getPluginPendingInstallationsDir,
pluginInstallationDir,
pluginPendingInstallationDir,
getPluginDirNameFromPackageName,
} from './pluginPaths';
import semver from 'semver';
@@ -29,35 +33,10 @@ function providePluginManagerNoDependencies(): PM {
return new PM({ignoredDependencies: [/.*/]});
}
function getPluginPendingInstallationDir(
name: string,
version: string,
): string {
return path.join(getPluginPendingInstallationsDir(name), version);
}
function getPluginPendingInstallationsDir(name: string): string {
return path.join(
pluginPendingInstallationDir,
replaceInvalidPathSegmentCharacters(name),
);
}
export function getPluginInstallationDir(name: string): string {
return path.join(
pluginInstallationDir,
replaceInvalidPathSegmentCharacters(name),
);
}
function replaceInvalidPathSegmentCharacters(name: string) {
return name.replace('/', '__');
}
async function installPluginFromTempDir(
sourceDir: string,
): Promise<PluginDetails> {
const pluginDetails = await getPluginDetails(sourceDir);
const pluginDetails = await getPluginDetailsFromDir(sourceDir);
const {name, version} = pluginDetails;
const backupDir = path.join(await getTmpDir(), `${name}-${version}`);
const installationsDir = getPluginPendingInstallationsDir(name);
@@ -93,7 +72,7 @@ async function installPluginFromTempDir(
}
throw err;
}
return await getPluginDetails(destinationDir);
return await getPluginDetailsFromDir(destinationDir);
}
async function getPluginRootDir(dir: string) {
@@ -121,7 +100,7 @@ export async function getInstalledPlugin(
if (!(await fs.pathExists(dir))) {
return null;
}
return await getPluginDetails(dir);
return await getPluginDetailsFromDir(dir);
}
export async function isPluginPendingInstallation(
@@ -140,7 +119,7 @@ export async function installPluginFromNpm(name: string) {
await plugManNoDep.install(name);
const pluginTempDir = path.join(
tmpDir,
replaceInvalidPathSegmentCharacters(name),
getPluginDirNameFromPackageName(name),
);
await installPluginFromTempDir(pluginTempDir);
} finally {

View File

@@ -12,7 +12,7 @@ import {homedir} from 'os';
import fs from 'fs-extra';
import expandTilde from 'expand-tilde';
export const flipperDataDir = path.join(homedir(), '.flipper');
const flipperDataDir = path.join(homedir(), '.flipper');
export const pluginInstallationDir = path.join(flipperDataDir, 'thirdparty');
@@ -42,3 +42,28 @@ export async function getPluginSourceFolders(): Promise<string[]> {
pluginFolders.push(path.resolve(__dirname, '..', '..', 'plugins', 'fb'));
return pluginFolders.map(expandTilde).filter(fs.existsSync);
}
export function getPluginPendingInstallationDir(
name: string,
version: string,
): string {
return path.join(getPluginPendingInstallationsDir(name), version);
}
export function getPluginPendingInstallationsDir(name: string): string {
return path.join(
pluginPendingInstallationDir,
getPluginDirNameFromPackageName(name),
);
}
export function getPluginInstallationDir(name: string): string {
return path.join(
pluginInstallationDir,
getPluginDirNameFromPackageName(name),
);
}
export function getPluginDirNameFromPackageName(name: string) {
return name.replace('/', '__');
}