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

@@ -0,0 +1,60 @@
/**
* 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 {DownloadablePluginDetails} from 'flipper-plugin-lib';
export function createMockDownloadablePluginDetails(
params: {
id?: string;
name?: string;
version?: string;
title?: string;
flipperEngineVersion?: string;
downloadUrl?: string;
gatekeeper?: string;
lastUpdated?: Date;
} = {},
): DownloadablePluginDetails {
const {id, version, title, flipperEngineVersion, gatekeeper, lastUpdated} = {
id: 'test',
version: '3.0.1',
flipperEngineVersion: '0.46.0',
lastUpdated: new Date(1591226525 * 1000),
...params,
};
const lowercasedID = id.toLowerCase();
const name = params.name || `flipper-plugin-${lowercasedID}`;
const details: DownloadablePluginDetails = {
name: name || `flipper-plugin-${lowercasedID}`,
id: id,
bugs: {
email: 'bugs@localhost',
url: 'bugs.localhost',
},
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,
title: title ?? id,
version: version,
downloadUrl: `http://localhost/${lowercasedID}/${version}`,
lastUpdated: lastUpdated,
};
return details;
}

View File

@@ -12,7 +12,7 @@ import {args} from '@oclif/parser';
import fs from 'fs-extra';
import path from 'path';
import {runBuild} from 'flipper-pkg-lib';
import {getPluginDetails} from 'flipper-plugin-lib';
import {getPluginDetailsFromDir} 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 getPluginDetails(inputDirectory);
const plugin = await getPluginDetailsFromDir(inputDirectory);
const out = path.resolve(inputDirectory, plugin.main);
await fs.ensureDir(path.dirname(out));

View File

@@ -16,7 +16,7 @@ import * as path from 'path';
import * as yarn from '../utils/yarn';
import cli from 'cli-ux';
import {runBuild} from 'flipper-pkg-lib';
import {getPluginDetails} from 'flipper-plugin-lib';
import {getPluginDetailsFromDir} 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 getPluginDetails(inputDirectory);
const plugin = await getPluginDetailsFromDir(inputDirectory);
const out = path.resolve(inputDirectory, plugin.main);
cli.action.stop(`done. Source: ${plugin.source}. Main: ${plugin.main}.`);

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('/', '__');
}