Versioning for plugin format
Summary: Added versioning for plugin format. The first version is where "main" points to source code entry and plugins are bundled by Flipper in run-time on loading them. The second version is where "main" points to the already existing bundle and Flipper just loads it without bundling. The plugins of version 2 must be bundled using "flipper-pkg" tool before publishing. Changelog: Support new packaging format for plugins. Reviewed By: mweststrate Differential Revision: D21074173 fbshipit-source-id: 7b70250e48e5bd5d359c96149fb5b14e67783c4d
This commit is contained in:
committed by
Facebook GitHub Bot
parent
eb34b2f6e3
commit
ca2d04a5da
@@ -82,12 +82,6 @@
|
||||
"yargs": "^15.3.1",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
"greenkeeper": {
|
||||
"ignore": [
|
||||
"tmp",
|
||||
"flipper-doctor"
|
||||
]
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"7zip-bin-mac": "^1.0.1"
|
||||
}
|
||||
|
||||
@@ -143,14 +143,7 @@ class PluginDebugger extends Component<Props> {
|
||||
getRows(): Array<TableBodyRow> {
|
||||
const rows: Array<TableBodyRow> = [];
|
||||
|
||||
// bundled plugins are loaded from the defaultPlugins directory within
|
||||
// Flipper's package.
|
||||
const externalPluginPath = (p: any) =>
|
||||
p.out
|
||||
? p.out.startsWith('./defaultPlugins/')
|
||||
? null
|
||||
: p.entry
|
||||
: 'Native Plugin';
|
||||
const externalPluginPath = (p: any) => p.entry || 'Native Plugin';
|
||||
|
||||
this.props.gatekeepedPlugins.forEach((plugin) =>
|
||||
rows.push(
|
||||
|
||||
@@ -69,14 +69,14 @@ test('checkDisabled', () => {
|
||||
expect(
|
||||
disabled({
|
||||
name: 'other Name',
|
||||
out: './test/index.js',
|
||||
entry: './test/index.js',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
disabled({
|
||||
name: disabledPlugin,
|
||||
out: './test/index.js',
|
||||
entry: './test/index.js',
|
||||
}),
|
||||
).toBeFalsy();
|
||||
});
|
||||
@@ -85,7 +85,7 @@ test('checkGK for plugin without GK', () => {
|
||||
expect(
|
||||
checkGK([])({
|
||||
name: 'pluginID',
|
||||
out: './test/index.js',
|
||||
entry: './test/index.js',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
});
|
||||
@@ -95,7 +95,7 @@ test('checkGK for passing plugin', () => {
|
||||
checkGK([])({
|
||||
name: 'pluginID',
|
||||
gatekeeper: TEST_PASSING_GK,
|
||||
out: './test/index.js',
|
||||
entry: './test/index.js',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
});
|
||||
@@ -106,7 +106,7 @@ test('checkGK for failing plugin', () => {
|
||||
const plugins = checkGK(gatekeepedPlugins)({
|
||||
name,
|
||||
gatekeeper: TEST_FAILING_GK,
|
||||
out: './test/index.js',
|
||||
entry: './test/index.js',
|
||||
});
|
||||
|
||||
expect(plugins).toBeFalsy();
|
||||
@@ -117,7 +117,7 @@ test('requirePlugin returns null for invalid requires', () => {
|
||||
const requireFn = requirePlugin([], require);
|
||||
const plugin = requireFn({
|
||||
name: 'pluginID',
|
||||
out: 'this/path/does not/exist',
|
||||
entry: 'this/path/does not/exist',
|
||||
});
|
||||
|
||||
expect(plugin).toBeNull();
|
||||
@@ -128,7 +128,7 @@ test('requirePlugin loads plugin', () => {
|
||||
const requireFn = requirePlugin([], require);
|
||||
const plugin = requireFn({
|
||||
name,
|
||||
out: path.join(__dirname, 'TestPlugin'),
|
||||
entry: path.join(__dirname, 'TestPlugin'),
|
||||
});
|
||||
expect(plugin!.prototype).toBeInstanceOf(FlipperPlugin);
|
||||
expect(plugin!.id).toBe(TestPlugin.id);
|
||||
|
||||
@@ -96,15 +96,15 @@ function getBundledPlugins(): Array<PluginDefinition> {
|
||||
}
|
||||
|
||||
return bundledPlugins
|
||||
.filter((plugin) => notNull(plugin.out))
|
||||
.filter((plugin) => notNull(plugin.entry))
|
||||
.map(
|
||||
(plugin) =>
|
||||
({
|
||||
...plugin,
|
||||
out: path.join(pluginPath, plugin.out!),
|
||||
entry: path.resolve(pluginPath, plugin.entry!),
|
||||
} as PluginDefinition),
|
||||
)
|
||||
.concat(bundledPlugins.filter((plugin) => !plugin.out));
|
||||
.concat(bundledPlugins.filter((plugin) => !plugin.entry));
|
||||
}
|
||||
|
||||
export function getDynamicPlugins() {
|
||||
@@ -155,8 +155,8 @@ export const requirePlugin = (
|
||||
pluginDefinition: PluginDefinition,
|
||||
): typeof FlipperPlugin | typeof FlipperDevicePlugin | null => {
|
||||
try {
|
||||
let plugin = pluginDefinition.out
|
||||
? reqFn(pluginDefinition.out)
|
||||
let plugin = pluginDefinition.entry
|
||||
? reqFn(pluginDefinition.entry)
|
||||
: defaultPluginsIndex[pluginDefinition.name];
|
||||
if (plugin.default) {
|
||||
plugin = plugin.default;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Babel transformer for Flipper plugins",
|
||||
"repository": "facebook/flipper",
|
||||
"main": "lib/index.js",
|
||||
"flipper:source": "src",
|
||||
"flipperBundlerEntry": "src",
|
||||
"types": "lib/index.d.ts",
|
||||
"license": "MIT",
|
||||
"bugs": "https://github.com/facebook/flipper/issues",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.37.0",
|
||||
"description": "Utility for checking for issues with a flipper installation",
|
||||
"main": "lib/index.js",
|
||||
"flipper:source": "src",
|
||||
"flipperBundlerEntry": "src",
|
||||
"types": "lib/index.d.ts",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Library for building and publishing Flipper plugins",
|
||||
"repository": "facebook/flipper",
|
||||
"main": "lib/index.js",
|
||||
"flipper:source": "src",
|
||||
"flipperBundlerEntry": "src",
|
||||
"types": "lib/index.d.ts",
|
||||
"license": "MIT",
|
||||
"bugs": "https://github.com/facebook/flipper/issues",
|
||||
|
||||
25
desktop/pkg-lib/src/PluginDetails.ts
Normal file
25
desktop/pkg-lib/src/PluginDetails.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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 default interface PluginDetails {
|
||||
dir: string;
|
||||
name: string;
|
||||
specVersion: number;
|
||||
version: string;
|
||||
source: string;
|
||||
main: string;
|
||||
gatekeeper?: string;
|
||||
icon?: string;
|
||||
title?: string;
|
||||
category?: string;
|
||||
bugs?: {
|
||||
email?: string;
|
||||
url?: string;
|
||||
};
|
||||
}
|
||||
70
desktop/pkg-lib/src/__tests__/getPluginDetails.node.ts
Normal file
70
desktop/pkg-lib/src/__tests__/getPluginDetails.node.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 {mocked} from 'ts-jest/utils';
|
||||
import getPluginDetails from '../getPluginDetails';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
const fsMock = mocked(fs, true);
|
||||
|
||||
test('getPluginDetailsV1', async () => {
|
||||
const pluginV1 = {
|
||||
name: 'flipper-plugin-test',
|
||||
version: '2.0.0',
|
||||
title: 'Test Plugin',
|
||||
main: 'src/index.tsx',
|
||||
gatekeeper: 'GK_flipper_plugin_test',
|
||||
};
|
||||
fsMock.readJson.mockImplementation(() => pluginV1);
|
||||
const details = await getPluginDetails('./plugins/flipper-plugin-test');
|
||||
expect(details).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"bugs": undefined,
|
||||
"category": undefined,
|
||||
"dir": "./plugins/flipper-plugin-test",
|
||||
"gatekeeper": "GK_flipper_plugin_test",
|
||||
"icon": undefined,
|
||||
"main": "dist/index.js",
|
||||
"name": "flipper-plugin-test",
|
||||
"source": "src/index.tsx",
|
||||
"specVersion": 1,
|
||||
"title": "Test Plugin",
|
||||
"version": "2.0.0",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('getPluginDetailsV2', async () => {
|
||||
const pluginV2 = {
|
||||
specVersion: 2,
|
||||
name: 'flipper-plugin-test',
|
||||
version: '3.0.1',
|
||||
main: 'dist/bundle.js',
|
||||
flipperBundlerEntry: 'src/index.tsx',
|
||||
gatekeeper: 'GK_flipper_plugin_test',
|
||||
};
|
||||
fsMock.readJson.mockImplementation(() => pluginV2);
|
||||
const details = await getPluginDetails('./plugins/flipper-plugin-test');
|
||||
expect(details).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"bugs": undefined,
|
||||
"category": undefined,
|
||||
"dir": "./plugins/flipper-plugin-test",
|
||||
"gatekeeper": "GK_flipper_plugin_test",
|
||||
"icon": undefined,
|
||||
"main": "dist/bundle.js",
|
||||
"name": "flipper-plugin-test",
|
||||
"source": "src/index.tsx",
|
||||
"specVersion": 2,
|
||||
"title": undefined,
|
||||
"version": "3.0.1",
|
||||
}
|
||||
`);
|
||||
});
|
||||
71
desktop/pkg-lib/src/getPluginDetails.ts
Normal file
71
desktop/pkg-lib/src/getPluginDetails.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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 PluginDetails from './PluginDetails';
|
||||
|
||||
export default async function (
|
||||
pluginDir: string,
|
||||
packageJson?: any,
|
||||
): Promise<PluginDetails> {
|
||||
packageJson =
|
||||
packageJson || (await fs.readJson(path.join(pluginDir, 'package.json')));
|
||||
const specVersion = !packageJson.specVersion
|
||||
? 1
|
||||
: (packageJson.specVersion as number);
|
||||
switch (specVersion) {
|
||||
case 1:
|
||||
return await getPluginDetailsV1(pluginDir, packageJson);
|
||||
case 2:
|
||||
return await getPluginDetailsV2(pluginDir, packageJson);
|
||||
default:
|
||||
throw new Error(`Unknown plugin format version: ${specVersion}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Plugins packaged using V1 are distributed as sources and compiled in run-time.
|
||||
async function getPluginDetailsV1(
|
||||
pluginDir: string,
|
||||
packageJson: any,
|
||||
): Promise<PluginDetails> {
|
||||
return {
|
||||
specVersion: 1,
|
||||
dir: pluginDir,
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
main: path.join('dist', 'index.js'),
|
||||
source: packageJson.main,
|
||||
gatekeeper: packageJson.gatekeeper,
|
||||
icon: packageJson.icon,
|
||||
title: packageJson.title,
|
||||
category: packageJson.category,
|
||||
bugs: packageJson.bugs,
|
||||
};
|
||||
}
|
||||
|
||||
// Plugins packaged using V2 are pre-bundled, so compilation in run-time is not required for them.
|
||||
async function getPluginDetailsV2(
|
||||
pluginDir: string,
|
||||
packageJson: any,
|
||||
): Promise<PluginDetails> {
|
||||
return {
|
||||
specVersion: 2,
|
||||
dir: pluginDir,
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
main: packageJson.main,
|
||||
source: packageJson.flipperBundlerEntry,
|
||||
gatekeeper: packageJson.gatekeeper,
|
||||
icon: packageJson.icon,
|
||||
title: packageJson.displayName || packageJson.title,
|
||||
category: packageJson.category,
|
||||
bugs: packageJson.bugs,
|
||||
};
|
||||
}
|
||||
@@ -9,3 +9,5 @@
|
||||
|
||||
export {default as runBuild} from './runBuild';
|
||||
export {default as getWatchFolders} from './getWatchFolders';
|
||||
export {default as PluginDetails} from './PluginDetails';
|
||||
export {default as getPluginDetails} from './getPluginDetails';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Utility for building and publishing Flipper plugins",
|
||||
"repository": "facebook/flipper",
|
||||
"main": "lib/index.js",
|
||||
"flipper:source": "src",
|
||||
"flipperBundlerEntry": "src",
|
||||
"types": "lib/index.d.ts",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
||||
@@ -14,7 +14,7 @@ import * as inquirer from 'inquirer';
|
||||
import * as path from 'path';
|
||||
import * as yarn from '../utils/yarn';
|
||||
import cli from 'cli-ux';
|
||||
import {runBuild} from 'flipper-pkg-lib';
|
||||
import {runBuild, getPluginDetails} from 'flipper-pkg-lib';
|
||||
|
||||
async function deriveOutputFileName(inputDirectory: string): Promise<string> {
|
||||
const packageJson = await readJSON(path.join(inputDirectory, 'package.json'));
|
||||
@@ -100,22 +100,14 @@ export default class Bundle extends Command {
|
||||
await yarn.install(inputDirectory);
|
||||
cli.action.stop();
|
||||
|
||||
cli.action.start('Reading package.json');
|
||||
const packageJson = await readJSON(
|
||||
path.join(inputDirectory, 'package.json'),
|
||||
);
|
||||
const entry =
|
||||
packageJson.main ??
|
||||
((await pathExists(path.join(inputDirectory, 'index.tsx')))
|
||||
? 'index.tsx'
|
||||
: 'index.jsx');
|
||||
const bundleMain = packageJson.bundleMain ?? path.join('dist', 'index.js');
|
||||
const out = path.resolve(inputDirectory, bundleMain);
|
||||
cli.action.stop(`done. Entry: ${entry}. Bundle main: ${bundleMain}.`);
|
||||
cli.action.start('Reading plugin details');
|
||||
const plugin = await getPluginDetails(inputDirectory);
|
||||
const out = path.resolve(inputDirectory, plugin.main);
|
||||
cli.action.stop(`done. Source: ${plugin.source}. Main: ${plugin.main}.`);
|
||||
|
||||
cli.action.start(`Compiling`);
|
||||
await ensureDir(path.dirname(out));
|
||||
await runBuild(inputDirectory, entry, out);
|
||||
await runBuild(inputDirectory, plugin.source, out);
|
||||
cli.action.stop();
|
||||
|
||||
cli.action.start(`Packing to ${outputFile}`);
|
||||
|
||||
@@ -122,6 +122,7 @@ export const NetworkRouteContext = createContext<NetworkRouteManager>(
|
||||
);
|
||||
|
||||
export default class extends FlipperPlugin<State, any, PersistedState> {
|
||||
static id = 'Network';
|
||||
static keyboardActions: Array<DefaultKeyboardAction> = ['clear'];
|
||||
static subscribed = [];
|
||||
static defaultPersistedState = {
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
{
|
||||
"name": "Network",
|
||||
"name": "flipper-plugin-network",
|
||||
"specVersion": 2,
|
||||
"flipperBundlerEntry": "index.tsx",
|
||||
"main": "dist/index.js",
|
||||
"title": "Network",
|
||||
"description": "Use the Network inspector to inspect outgoing network traffic in your apps.",
|
||||
"icon": "internet",
|
||||
"version": "1.0.0",
|
||||
"main": "index.tsx",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"flipper-plugin"
|
||||
],
|
||||
"bugs": {
|
||||
"email": "oncall+flipper@xmail.facebook.com",
|
||||
"url": "https://fb.workplace.com/groups/flippersupport/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/pako": "^1.0.1",
|
||||
"lodash": "^4.17.11",
|
||||
"pako": "^1.0.11",
|
||||
"xml-beautifier": "^0.4.0"
|
||||
},
|
||||
"icon": "internet",
|
||||
"title": "Network",
|
||||
"bugs": {
|
||||
"email": "oncall+flipper@xmail.facebook.com",
|
||||
"url": "https://fb.workplace.com/groups/flippersupport/"
|
||||
"devDependencies": {
|
||||
"@types/pako": "^1.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,16 +33,13 @@ export function die(err: Error) {
|
||||
|
||||
export async function generatePluginEntryPoints() {
|
||||
console.log('⚙️ Generating plugin entry points...');
|
||||
const pluginEntryPoints = await getPlugins();
|
||||
const plugins = await getPlugins();
|
||||
if (await fs.pathExists(defaultPluginsIndexDir)) {
|
||||
await fs.remove(defaultPluginsIndexDir);
|
||||
}
|
||||
await fs.mkdirp(defaultPluginsIndexDir);
|
||||
await fs.writeJSON(
|
||||
path.join(defaultPluginsIndexDir, 'index.json'),
|
||||
pluginEntryPoints.map((plugin) => plugin.manifest),
|
||||
);
|
||||
const pluginRequres = pluginEntryPoints
|
||||
await fs.writeJSON(path.join(defaultPluginsIndexDir, 'index.json'), plugins);
|
||||
const pluginRequres = plugins
|
||||
.map((x) => ` '${x.name}': require('${x.name}')`)
|
||||
.join(',\n');
|
||||
const generatedIndex = `
|
||||
@@ -76,7 +73,7 @@ async function compile(
|
||||
),
|
||||
},
|
||||
resolver: {
|
||||
resolverMainFields: ['flipper:source', 'module', 'main'],
|
||||
resolverMainFields: ['flipperBundlerEntry', 'module', 'main'],
|
||||
blacklistRE: /\.native\.js$/,
|
||||
sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json', 'mjs'],
|
||||
},
|
||||
@@ -151,7 +148,7 @@ export async function compileMain() {
|
||||
},
|
||||
resolver: {
|
||||
sourceExts: ['tsx', 'ts', 'js'],
|
||||
resolverMainFields: ['flipper:source', 'module', 'main'],
|
||||
resolverMainFields: ['flipperBundlerEntry', 'module', 'main'],
|
||||
blacklistRE: /\.native\.js$/,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ async function startMetroServer(app: Express, server: http.Server) {
|
||||
},
|
||||
resolver: {
|
||||
...baseConfig.resolver,
|
||||
resolverMainFields: ['flipper:source', 'module', 'main'],
|
||||
resolverMainFields: ['flipperBundlerEntry', 'module', 'main'],
|
||||
blacklistRE: /\.native\.js$/,
|
||||
resolveRequest: (context: any, moduleName: string, platform: string) => {
|
||||
if (moduleName.startsWith('./localhost:3000')) {
|
||||
|
||||
@@ -14,8 +14,8 @@ import util from 'util';
|
||||
import recursiveReaddir from 'recursive-readdir';
|
||||
import pMap from 'p-map';
|
||||
import {homedir} from 'os';
|
||||
import {getWatchFolders} from 'flipper-pkg-lib';
|
||||
import {default as getPlugins, PluginManifest, PluginInfo} from './getPlugins';
|
||||
import {getWatchFolders, PluginDetails} from 'flipper-pkg-lib';
|
||||
import getPlugins from './getPlugins';
|
||||
import startWatchPlugins from './startWatchPlugins';
|
||||
|
||||
const HOME_DIR = homedir();
|
||||
@@ -35,13 +35,13 @@ export type CompileOptions = {
|
||||
recompileOnChanges: boolean;
|
||||
};
|
||||
|
||||
export type CompiledPluginInfo = PluginManifest & {out: string};
|
||||
export type CompiledPluginDetails = PluginDetails & {entry: string};
|
||||
|
||||
export default async function (
|
||||
reloadCallback: (() => void) | null,
|
||||
pluginCache: string,
|
||||
options: CompileOptions = DEFAULT_COMPILE_OPTIONS,
|
||||
): Promise<CompiledPluginInfo[]> {
|
||||
): Promise<CompiledPluginDetails[]> {
|
||||
if (process.env.FLIPPER_FAST_REFRESH) {
|
||||
console.log(
|
||||
'🥫 Skipping loading of third-party plugins because Fast Refresh is enabled',
|
||||
@@ -74,12 +74,12 @@ export default async function (
|
||||
|
||||
const compiledDynamicPlugins = (await compilations).filter(
|
||||
(c) => c !== null,
|
||||
) as CompiledPluginInfo[];
|
||||
) as CompiledPluginDetails[];
|
||||
console.log('✅ Compiled all plugins.');
|
||||
return compiledDynamicPlugins;
|
||||
}
|
||||
async function startWatchChanges(
|
||||
plugins: PluginInfo[],
|
||||
plugins: PluginDetails[],
|
||||
reloadCallback: (() => void) | null,
|
||||
pluginCache: string,
|
||||
options: CompileOptions = DEFAULT_COMPILE_OPTIONS,
|
||||
@@ -88,7 +88,7 @@ async function startWatchChanges(
|
||||
// no hot reloading for plugins in .flipper folder. This is to prevent
|
||||
// Flipper from reloading, while we are doing changes on thirdparty plugins.
|
||||
.filter(
|
||||
(plugin) => !plugin.rootDir.startsWith(path.join(HOME_DIR, '.flipper')),
|
||||
(plugin) => !plugin.dir.startsWith(path.join(HOME_DIR, '.flipper')),
|
||||
);
|
||||
const watchOptions = Object.assign({}, options, {force: true});
|
||||
await startWatchPlugins(filteredPlugins, (plugin) =>
|
||||
@@ -142,30 +142,32 @@ async function getMetroDir() {
|
||||
return __dirname;
|
||||
}
|
||||
async function compilePlugin(
|
||||
pluginInfo: PluginInfo,
|
||||
pluginDetails: PluginDetails,
|
||||
pluginCache: string,
|
||||
{force, failSilently}: CompileOptions,
|
||||
): Promise<CompiledPluginInfo | null> {
|
||||
const {rootDir, manifest, entry, name} = pluginInfo;
|
||||
const bundleMain = manifest.bundleMain ?? path.join('dist', 'index.js');
|
||||
const bundlePath = path.join(rootDir, bundleMain);
|
||||
): Promise<CompiledPluginDetails | null> {
|
||||
const {dir, specVersion, version, main, source, name} = pluginDetails;
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
if (await fs.pathExists(bundlePath)) {
|
||||
if (specVersion > 1) {
|
||||
// eslint-disable-next-line no-console
|
||||
const out = path.join(rootDir, bundleMain);
|
||||
console.log(`🥫 Using pre-built version of ${name}: ${out}...`);
|
||||
return Object.assign({}, pluginInfo.manifest, {out});
|
||||
const entry = path.join(dir, main);
|
||||
if (await fs.pathExists(entry)) {
|
||||
console.log(`🥫 Using pre-built version of ${name}: ${entry}...`);
|
||||
return Object.assign({}, pluginDetails, {entry});
|
||||
} else {
|
||||
const out = path.join(
|
||||
pluginCache,
|
||||
`${name}@${manifest.version || '0.0.0'}.js`,
|
||||
console.error(
|
||||
`❌ Plugin ${name} is ignored, because its entry point not found: ${entry}.`,
|
||||
);
|
||||
const result = Object.assign({}, pluginInfo.manifest, {out});
|
||||
const rootDirCtime = await mostRecentlyChanged(rootDir);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
const entry = path.join(pluginCache, `${name}@${version || '0.0.0'}.js`);
|
||||
const result = Object.assign({}, pluginDetails, {entry});
|
||||
const rootDirCtime = await mostRecentlyChanged(dir);
|
||||
if (
|
||||
!force &&
|
||||
(await fs.pathExists(out)) &&
|
||||
rootDirCtime < (await fs.lstat(out)).ctime
|
||||
(await fs.pathExists(entry)) &&
|
||||
rootDirCtime < (await fs.lstat(entry)).ctime
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`🥫 Using cached version of ${name}...`);
|
||||
@@ -177,9 +179,9 @@ async function compilePlugin(
|
||||
await Metro.runBuild(
|
||||
{
|
||||
reporter: {update: () => {}},
|
||||
projectRoot: rootDir,
|
||||
projectRoot: dir,
|
||||
watchFolders: [metroDir || (await metroDirPromise)].concat(
|
||||
await getWatchFolders(rootDir),
|
||||
await getWatchFolders(dir),
|
||||
),
|
||||
serializer: {
|
||||
getRunModuleStatement: (moduleID: string) =>
|
||||
@@ -197,8 +199,8 @@ async function compilePlugin(
|
||||
},
|
||||
},
|
||||
{
|
||||
entry: entry.replace(rootDir, '.'),
|
||||
out,
|
||||
entry: source,
|
||||
out: entry,
|
||||
dev,
|
||||
sourceMap: true,
|
||||
minify: false,
|
||||
|
||||
@@ -11,82 +11,95 @@ import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import expandTilde from 'expand-tilde';
|
||||
import getPluginFolders from './getPluginFolders';
|
||||
import {PluginDetails, getPluginDetails} from 'flipper-pkg-lib';
|
||||
import pmap from 'p-map';
|
||||
import pfilter from 'p-filter';
|
||||
|
||||
export type PluginManifest = {
|
||||
version: string;
|
||||
name: string;
|
||||
main?: string;
|
||||
bundleMain?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type PluginInfo = {
|
||||
rootDir: string;
|
||||
name: string;
|
||||
entry: string;
|
||||
manifest: PluginManifest;
|
||||
};
|
||||
|
||||
export default async function getPlugins(includeThirdparty: boolean = false) {
|
||||
export default async function getPlugins(
|
||||
includeThirdparty: boolean = false,
|
||||
): Promise<PluginDetails[]> {
|
||||
const pluginFolders = await getPluginFolders(includeThirdparty);
|
||||
const entryPoints: {[key: string]: PluginInfo} = {};
|
||||
pluginFolders.forEach((additionalPath) => {
|
||||
const additionalPlugins = entryPointForPluginFolder(additionalPath);
|
||||
Object.keys(additionalPlugins).forEach((key) => {
|
||||
entryPoints[key] = additionalPlugins[key];
|
||||
});
|
||||
const entryPoints: {[key: string]: PluginDetails} = {};
|
||||
const additionalPlugins = await pmap(pluginFolders, (path) =>
|
||||
entryPointForPluginFolder(path),
|
||||
);
|
||||
for (const p of additionalPlugins) {
|
||||
Object.keys(p).forEach((key) => {
|
||||
entryPoints[key] = p[key];
|
||||
});
|
||||
}
|
||||
return Object.values(entryPoints);
|
||||
}
|
||||
function entryPointForPluginFolder(pluginPath: string) {
|
||||
pluginPath = expandTilde(pluginPath);
|
||||
if (!fs.existsSync(pluginPath)) {
|
||||
async function entryPointForPluginFolder(
|
||||
pluginsDir: string,
|
||||
): Promise<{[key: string]: PluginDetails}> {
|
||||
pluginsDir = expandTilde(pluginsDir);
|
||||
if (!fs.existsSync(pluginsDir)) {
|
||||
return {};
|
||||
}
|
||||
return fs
|
||||
.readdirSync(pluginPath)
|
||||
.filter((name) => fs.lstatSync(path.join(pluginPath, name)).isDirectory())
|
||||
.filter(Boolean)
|
||||
.map((name) => {
|
||||
let packageJSON;
|
||||
return await fs
|
||||
.readdir(pluginsDir)
|
||||
.then((entries) =>
|
||||
entries.map((name) => ({
|
||||
dir: path.join(pluginsDir, name),
|
||||
manifestPath: path.join(pluginsDir, name, 'package.json'),
|
||||
})),
|
||||
)
|
||||
.then((entries) =>
|
||||
pfilter(entries, ({manifestPath}) => fs.pathExists(manifestPath)),
|
||||
)
|
||||
.then((packages) =>
|
||||
pmap(packages, async ({manifestPath, dir}) => {
|
||||
try {
|
||||
packageJSON = fs
|
||||
.readFileSync(path.join(pluginPath, name, 'package.json'))
|
||||
.toString();
|
||||
} catch (e) {}
|
||||
if (packageJSON) {
|
||||
try {
|
||||
const json = JSON.parse(packageJSON);
|
||||
if (json.workspaces) {
|
||||
return;
|
||||
}
|
||||
if (!json.keywords || !json.keywords.includes('flipper-plugin')) {
|
||||
console.log(
|
||||
`Skipping package "${json.name}" as its "keywords" field does not contain tag "flipper-plugin"`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const pkg = json as PluginManifest;
|
||||
const plugin: PluginInfo = {
|
||||
manifest: pkg,
|
||||
name: pkg.name,
|
||||
entry: path.join(pluginPath, name, pkg.main || 'index.js'),
|
||||
rootDir: path.join(pluginPath, name),
|
||||
const manifest = await fs.readJson(manifestPath);
|
||||
return {
|
||||
dir,
|
||||
manifest,
|
||||
};
|
||||
return plugin;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Could not load plugin "${pluginPath}", because package.json is invalid.`,
|
||||
`Could not load plugin from "${dir}", because package.json is invalid.`,
|
||||
);
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then((packages) => packages.filter(notNull))
|
||||
.then((packages) => packages.filter(({manifest}) => !manifest.workspaces))
|
||||
.then((packages) =>
|
||||
packages.filter(({manifest: {keywords, name}}) => {
|
||||
if (!keywords || !keywords.includes('flipper-plugin')) {
|
||||
console.log(
|
||||
`Skipping package "${name}" as its "keywords" field does not contain tag "flipper-plugin"`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
)
|
||||
.then((packages) =>
|
||||
pmap(packages, async ({manifest, dir}) => {
|
||||
try {
|
||||
return await getPluginDetails(dir, manifest);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`Could not load plugin from "${dir}", because package.json is invalid.`,
|
||||
);
|
||||
console.error(e);
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.reduce<{[key: string]: PluginInfo}>((acc, cv) => {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then((plugins) => plugins.filter(notNull))
|
||||
.then((plugins) =>
|
||||
plugins.reduce<{[key: string]: PluginDetails}>((acc, cv) => {
|
||||
acc[cv!.name] = cv!;
|
||||
return acc;
|
||||
}, {});
|
||||
}, {}),
|
||||
);
|
||||
}
|
||||
|
||||
function notNull<T>(x: T | null | undefined): x is T {
|
||||
return x !== null && x !== undefined;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"metro": "^0.59.0",
|
||||
"mkdirp": "^1.0.0",
|
||||
"p-map": "^4.0.0",
|
||||
"recursive-readdir": "2.2.2",
|
||||
"p-filter": "^2.1.0",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"uuid": "^7.0.1",
|
||||
"xdg-basedir": "^4.0.0",
|
||||
"yargs": "^15.3.1",
|
||||
|
||||
@@ -9,18 +9,18 @@
|
||||
|
||||
import path from 'path';
|
||||
import Watchman from './watchman';
|
||||
import {PluginInfo} from './getPlugins';
|
||||
import {PluginDetails} from 'flipper-pkg-lib';
|
||||
|
||||
export default async function startWatchPlugins(
|
||||
plugins: PluginInfo[],
|
||||
compilePlugin: (plugin: PluginInfo) => void | Promise<void>,
|
||||
plugins: PluginDetails[],
|
||||
compilePlugin: (plugin: PluginDetails) => void | Promise<void>,
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('🕵️ Watching for plugin changes');
|
||||
|
||||
const delayedCompilation: {[key: string]: NodeJS.Timeout | null} = {};
|
||||
const kCompilationDelayMillis = 1000;
|
||||
const onPluginChanged = (plugin: PluginInfo) => {
|
||||
const onPluginChanged = (plugin: PluginDetails) => {
|
||||
if (!delayedCompilation[plugin.name]) {
|
||||
delayedCompilation[plugin.name] = setTimeout(() => {
|
||||
delayedCompilation[plugin.name] = null;
|
||||
@@ -41,14 +41,14 @@ export default async function startWatchPlugins(
|
||||
}
|
||||
|
||||
async function startWatchingPluginsUsingWatchman(
|
||||
plugins: PluginInfo[],
|
||||
onPluginChanged: (plugin: PluginInfo) => void,
|
||||
plugins: PluginDetails[],
|
||||
onPluginChanged: (plugin: PluginDetails) => void,
|
||||
) {
|
||||
// Initializing a watchman for each folder containing plugins
|
||||
const watchmanRootMap: {[key: string]: Watchman} = {};
|
||||
await Promise.all(
|
||||
plugins.map(async (plugin) => {
|
||||
const watchmanRoot = path.resolve(plugin.rootDir, '..');
|
||||
const watchmanRoot = path.resolve(plugin.dir, '..');
|
||||
if (!watchmanRootMap[watchmanRoot]) {
|
||||
watchmanRootMap[watchmanRoot] = new Watchman(watchmanRoot);
|
||||
await watchmanRootMap[watchmanRoot].initialize();
|
||||
@@ -58,10 +58,10 @@ async function startWatchingPluginsUsingWatchman(
|
||||
// Start watching plugins using the initialized watchmans
|
||||
await Promise.all(
|
||||
plugins.map(async (plugin) => {
|
||||
const watchmanRoot = path.resolve(plugin.rootDir, '..');
|
||||
const watchmanRoot = path.resolve(plugin.dir, '..');
|
||||
const watchman = watchmanRootMap[watchmanRoot];
|
||||
await watchman.startWatchFiles(
|
||||
path.relative(watchmanRoot, plugin.rootDir),
|
||||
path.relative(watchmanRoot, plugin.dir),
|
||||
() => onPluginChanged(plugin),
|
||||
{
|
||||
excludes: ['**/__tests__/**/*', '**/node_modules/**/*', '**/.*'],
|
||||
|
||||
@@ -9997,7 +9997,7 @@ recharts@1.7.1:
|
||||
recharts-scale "^0.4.2"
|
||||
reduce-css-calc "^1.3.0"
|
||||
|
||||
recursive-readdir@2.2.2, recursive-readdir@^2.2.2:
|
||||
recursive-readdir@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
|
||||
integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==
|
||||
|
||||
Reference in New Issue
Block a user