From 5b26f366725718b31f45db8b74e6076c88b9d427 Mon Sep 17 00:00:00 2001 From: Anton Nikolaev Date: Tue, 15 Dec 2020 09:28:58 -0800 Subject: [PATCH] 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 --- desktop/app/src/utils/testUtils.tsx | 60 +++++++++++++++++++ desktop/pkg/src/commands/bundle.ts | 4 +- desktop/pkg/src/commands/pack.ts | 4 +- desktop/plugin-lib/src/PluginDetails.ts | 12 +++- .../src/__tests__/getPluginDetails.node.ts | 12 ++-- desktop/plugin-lib/src/getInstalledPlugins.ts | 6 +- desktop/plugin-lib/src/getPluginDetails.ts | 36 ++++++++--- desktop/plugin-lib/src/getUpdatablePlugins.ts | 4 +- desktop/plugin-lib/src/index.ts | 6 +- desktop/plugin-lib/src/pluginInstaller.ts | 39 +++--------- desktop/plugin-lib/src/pluginPaths.ts | 27 ++++++++- 11 files changed, 152 insertions(+), 58 deletions(-) create mode 100644 desktop/app/src/utils/testUtils.tsx diff --git a/desktop/app/src/utils/testUtils.tsx b/desktop/app/src/utils/testUtils.tsx new file mode 100644 index 000000000..7fdc3de20 --- /dev/null +++ b/desktop/app/src/utils/testUtils.tsx @@ -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; +} diff --git a/desktop/pkg/src/commands/bundle.ts b/desktop/pkg/src/commands/bundle.ts index 6454fabcd..6b01692ec 100644 --- a/desktop/pkg/src/commands/bundle.ts +++ b/desktop/pkg/src/commands/bundle.ts @@ -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)); diff --git a/desktop/pkg/src/commands/pack.ts b/desktop/pkg/src/commands/pack.ts index 220230321..1219c5985 100644 --- a/desktop/pkg/src/commands/pack.ts +++ b/desktop/pkg/src/commands/pack.ts @@ -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 { 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}.`); diff --git a/desktop/plugin-lib/src/PluginDetails.ts b/desktop/plugin-lib/src/PluginDetails.ts index 285a00a37..94f504736 100644 --- a/desktop/plugin-lib/src/PluginDetails.ts +++ b/desktop/plugin-lib/src/PluginDetails.ts @@ -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; diff --git a/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts b/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts index a2f18dc20..c0a8f3243 100644 --- a/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts +++ b/desktop/plugin-lib/src/__tests__/getPluginDetails.node.ts @@ -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(` diff --git a/desktop/plugin-lib/src/getInstalledPlugins.ts b/desktop/plugin-lib/src/getInstalledPlugins.ts index 16e517969..55a872145 100644 --- a/desktop/plugin-lib/src/getInstalledPlugins.ts +++ b/desktop/plugin-lib/src/getInstalledPlugins.ts @@ -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 { 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 { return undefined; } try { - return await getPluginDetails(pluginDir); + return await getPluginDetailsFromDir(pluginDir); } catch (e) { console.error(`Failed to load plugin from ${pluginDir}`, e); return undefined; diff --git a/desktop/plugin-lib/src/getPluginDetails.ts b/desktop/plugin-lib/src/getPluginDetails.ts index f91859411..ff11cce1d 100644 --- a/desktop/plugin-lib/src/getPluginDetails.ts +++ b/desktop/plugin-lib/src/getPluginDetails.ts @@ -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 { - 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 { + 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, diff --git a/desktop/plugin-lib/src/getUpdatablePlugins.ts b/desktop/plugin-lib/src/getUpdatablePlugins.ts index b9dc0b5ec..708fa6d4e 100644 --- a/desktop/plugin-lib/src/getUpdatablePlugins.ts +++ b/desktop/plugin-lib/src/getUpdatablePlugins.ts @@ -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(); diff --git a/desktop/plugin-lib/src/index.ts b/desktop/plugin-lib/src/index.ts index d10b1517d..78731618d 100644 --- a/desktop/plugin-lib/src/index.ts +++ b/desktop/plugin-lib/src/index.ts @@ -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'; diff --git a/desktop/plugin-lib/src/pluginInstaller.ts b/desktop/plugin-lib/src/pluginInstaller.ts index bfbeea6dc..a2c148837 100644 --- a/desktop/plugin-lib/src/pluginInstaller.ts +++ b/desktop/plugin-lib/src/pluginInstaller.ts @@ -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 { - 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 { diff --git a/desktop/plugin-lib/src/pluginPaths.ts b/desktop/plugin-lib/src/pluginPaths.ts index 2801e37fb..a7eac5c44 100644 --- a/desktop/plugin-lib/src/pluginPaths.ts +++ b/desktop/plugin-lib/src/pluginPaths.ts @@ -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 { 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('/', '__'); +}