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:
committed by
Facebook GitHub Bot
parent
658b3e8a91
commit
5b26f36672
60
desktop/app/src/utils/testUtils.tsx
Normal file
60
desktop/app/src/utils/testUtils.tsx
Normal 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;
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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}.`);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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('/', '__');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user