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:
Anton Nikolaev
2020-04-20 06:01:08 -07:00
committed by Facebook GitHub Bot
parent eb34b2f6e3
commit ca2d04a5da
22 changed files with 329 additions and 163 deletions

View File

@@ -82,12 +82,6 @@
"yargs": "^15.3.1", "yargs": "^15.3.1",
"yazl": "^2.5.1" "yazl": "^2.5.1"
}, },
"greenkeeper": {
"ignore": [
"tmp",
"flipper-doctor"
]
},
"optionalDependencies": { "optionalDependencies": {
"7zip-bin-mac": "^1.0.1" "7zip-bin-mac": "^1.0.1"
} }

View File

@@ -143,14 +143,7 @@ class PluginDebugger extends Component<Props> {
getRows(): Array<TableBodyRow> { getRows(): Array<TableBodyRow> {
const rows: Array<TableBodyRow> = []; const rows: Array<TableBodyRow> = [];
// bundled plugins are loaded from the defaultPlugins directory within const externalPluginPath = (p: any) => p.entry || 'Native Plugin';
// Flipper's package.
const externalPluginPath = (p: any) =>
p.out
? p.out.startsWith('./defaultPlugins/')
? null
: p.entry
: 'Native Plugin';
this.props.gatekeepedPlugins.forEach((plugin) => this.props.gatekeepedPlugins.forEach((plugin) =>
rows.push( rows.push(

View File

@@ -69,14 +69,14 @@ test('checkDisabled', () => {
expect( expect(
disabled({ disabled({
name: 'other Name', name: 'other Name',
out: './test/index.js', entry: './test/index.js',
}), }),
).toBeTruthy(); ).toBeTruthy();
expect( expect(
disabled({ disabled({
name: disabledPlugin, name: disabledPlugin,
out: './test/index.js', entry: './test/index.js',
}), }),
).toBeFalsy(); ).toBeFalsy();
}); });
@@ -85,7 +85,7 @@ test('checkGK for plugin without GK', () => {
expect( expect(
checkGK([])({ checkGK([])({
name: 'pluginID', name: 'pluginID',
out: './test/index.js', entry: './test/index.js',
}), }),
).toBeTruthy(); ).toBeTruthy();
}); });
@@ -95,7 +95,7 @@ test('checkGK for passing plugin', () => {
checkGK([])({ checkGK([])({
name: 'pluginID', name: 'pluginID',
gatekeeper: TEST_PASSING_GK, gatekeeper: TEST_PASSING_GK,
out: './test/index.js', entry: './test/index.js',
}), }),
).toBeTruthy(); ).toBeTruthy();
}); });
@@ -106,7 +106,7 @@ test('checkGK for failing plugin', () => {
const plugins = checkGK(gatekeepedPlugins)({ const plugins = checkGK(gatekeepedPlugins)({
name, name,
gatekeeper: TEST_FAILING_GK, gatekeeper: TEST_FAILING_GK,
out: './test/index.js', entry: './test/index.js',
}); });
expect(plugins).toBeFalsy(); expect(plugins).toBeFalsy();
@@ -117,7 +117,7 @@ test('requirePlugin returns null for invalid requires', () => {
const requireFn = requirePlugin([], require); const requireFn = requirePlugin([], require);
const plugin = requireFn({ const plugin = requireFn({
name: 'pluginID', name: 'pluginID',
out: 'this/path/does not/exist', entry: 'this/path/does not/exist',
}); });
expect(plugin).toBeNull(); expect(plugin).toBeNull();
@@ -128,7 +128,7 @@ test('requirePlugin loads plugin', () => {
const requireFn = requirePlugin([], require); const requireFn = requirePlugin([], require);
const plugin = requireFn({ const plugin = requireFn({
name, name,
out: path.join(__dirname, 'TestPlugin'), entry: path.join(__dirname, 'TestPlugin'),
}); });
expect(plugin!.prototype).toBeInstanceOf(FlipperPlugin); expect(plugin!.prototype).toBeInstanceOf(FlipperPlugin);
expect(plugin!.id).toBe(TestPlugin.id); expect(plugin!.id).toBe(TestPlugin.id);

View File

@@ -96,15 +96,15 @@ function getBundledPlugins(): Array<PluginDefinition> {
} }
return bundledPlugins return bundledPlugins
.filter((plugin) => notNull(plugin.out)) .filter((plugin) => notNull(plugin.entry))
.map( .map(
(plugin) => (plugin) =>
({ ({
...plugin, ...plugin,
out: path.join(pluginPath, plugin.out!), entry: path.resolve(pluginPath, plugin.entry!),
} as PluginDefinition), } as PluginDefinition),
) )
.concat(bundledPlugins.filter((plugin) => !plugin.out)); .concat(bundledPlugins.filter((plugin) => !plugin.entry));
} }
export function getDynamicPlugins() { export function getDynamicPlugins() {
@@ -155,8 +155,8 @@ export const requirePlugin = (
pluginDefinition: PluginDefinition, pluginDefinition: PluginDefinition,
): typeof FlipperPlugin | typeof FlipperDevicePlugin | null => { ): typeof FlipperPlugin | typeof FlipperDevicePlugin | null => {
try { try {
let plugin = pluginDefinition.out let plugin = pluginDefinition.entry
? reqFn(pluginDefinition.out) ? reqFn(pluginDefinition.entry)
: defaultPluginsIndex[pluginDefinition.name]; : defaultPluginsIndex[pluginDefinition.name];
if (plugin.default) { if (plugin.default) {
plugin = plugin.default; plugin = plugin.default;

View File

@@ -4,7 +4,7 @@
"description": "Babel transformer for Flipper plugins", "description": "Babel transformer for Flipper plugins",
"repository": "facebook/flipper", "repository": "facebook/flipper",
"main": "lib/index.js", "main": "lib/index.js",
"flipper:source": "src", "flipperBundlerEntry": "src",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",
"license": "MIT", "license": "MIT",
"bugs": "https://github.com/facebook/flipper/issues", "bugs": "https://github.com/facebook/flipper/issues",

View File

@@ -3,7 +3,7 @@
"version": "0.37.0", "version": "0.37.0",
"description": "Utility for checking for issues with a flipper installation", "description": "Utility for checking for issues with a flipper installation",
"main": "lib/index.js", "main": "lib/index.js",
"flipper:source": "src", "flipperBundlerEntry": "src",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {

View File

@@ -4,7 +4,7 @@
"description": "Library for building and publishing Flipper plugins", "description": "Library for building and publishing Flipper plugins",
"repository": "facebook/flipper", "repository": "facebook/flipper",
"main": "lib/index.js", "main": "lib/index.js",
"flipper:source": "src", "flipperBundlerEntry": "src",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",
"license": "MIT", "license": "MIT",
"bugs": "https://github.com/facebook/flipper/issues", "bugs": "https://github.com/facebook/flipper/issues",

View 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;
};
}

View 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",
}
`);
});

View 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,
};
}

View File

@@ -9,3 +9,5 @@
export {default as runBuild} from './runBuild'; export {default as runBuild} from './runBuild';
export {default as getWatchFolders} from './getWatchFolders'; export {default as getWatchFolders} from './getWatchFolders';
export {default as PluginDetails} from './PluginDetails';
export {default as getPluginDetails} from './getPluginDetails';

View File

@@ -4,7 +4,7 @@
"description": "Utility for building and publishing Flipper plugins", "description": "Utility for building and publishing Flipper plugins",
"repository": "facebook/flipper", "repository": "facebook/flipper",
"main": "lib/index.js", "main": "lib/index.js",
"flipper:source": "src", "flipperBundlerEntry": "src",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",
"license": "MIT", "license": "MIT",
"bin": { "bin": {

View File

@@ -14,7 +14,7 @@ import * as inquirer from 'inquirer';
import * as path from 'path'; import * as path from 'path';
import * as yarn from '../utils/yarn'; import * as yarn from '../utils/yarn';
import cli from 'cli-ux'; 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> { async function deriveOutputFileName(inputDirectory: string): Promise<string> {
const packageJson = await readJSON(path.join(inputDirectory, 'package.json')); const packageJson = await readJSON(path.join(inputDirectory, 'package.json'));
@@ -100,22 +100,14 @@ export default class Bundle extends Command {
await yarn.install(inputDirectory); await yarn.install(inputDirectory);
cli.action.stop(); cli.action.stop();
cli.action.start('Reading package.json'); cli.action.start('Reading plugin details');
const packageJson = await readJSON( const plugin = await getPluginDetails(inputDirectory);
path.join(inputDirectory, 'package.json'), const out = path.resolve(inputDirectory, plugin.main);
); cli.action.stop(`done. Source: ${plugin.source}. Main: ${plugin.main}.`);
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(`Compiling`); cli.action.start(`Compiling`);
await ensureDir(path.dirname(out)); await ensureDir(path.dirname(out));
await runBuild(inputDirectory, entry, out); await runBuild(inputDirectory, plugin.source, out);
cli.action.stop(); cli.action.stop();
cli.action.start(`Packing to ${outputFile}`); cli.action.start(`Packing to ${outputFile}`);

View File

@@ -122,6 +122,7 @@ export const NetworkRouteContext = createContext<NetworkRouteManager>(
); );
export default class extends FlipperPlugin<State, any, PersistedState> { export default class extends FlipperPlugin<State, any, PersistedState> {
static id = 'Network';
static keyboardActions: Array<DefaultKeyboardAction> = ['clear']; static keyboardActions: Array<DefaultKeyboardAction> = ['clear'];
static subscribed = []; static subscribed = [];
static defaultPersistedState = { static defaultPersistedState = {

View File

@@ -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", "version": "1.0.0",
"main": "index.tsx",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [
"flipper-plugin" "flipper-plugin"
], ],
"bugs": {
"email": "oncall+flipper@xmail.facebook.com",
"url": "https://fb.workplace.com/groups/flippersupport/"
},
"dependencies": { "dependencies": {
"@types/pako": "^1.0.1",
"lodash": "^4.17.11", "lodash": "^4.17.11",
"pako": "^1.0.11", "pako": "^1.0.11",
"xml-beautifier": "^0.4.0" "xml-beautifier": "^0.4.0"
}, },
"icon": "internet", "devDependencies": {
"title": "Network", "@types/pako": "^1.0.1"
"bugs": {
"email": "oncall+flipper@xmail.facebook.com",
"url": "https://fb.workplace.com/groups/flippersupport/"
} }
} }

View File

@@ -33,16 +33,13 @@ export function die(err: Error) {
export async function generatePluginEntryPoints() { export async function generatePluginEntryPoints() {
console.log('⚙️ Generating plugin entry points...'); console.log('⚙️ Generating plugin entry points...');
const pluginEntryPoints = await getPlugins(); const plugins = await getPlugins();
if (await fs.pathExists(defaultPluginsIndexDir)) { if (await fs.pathExists(defaultPluginsIndexDir)) {
await fs.remove(defaultPluginsIndexDir); await fs.remove(defaultPluginsIndexDir);
} }
await fs.mkdirp(defaultPluginsIndexDir); await fs.mkdirp(defaultPluginsIndexDir);
await fs.writeJSON( await fs.writeJSON(path.join(defaultPluginsIndexDir, 'index.json'), plugins);
path.join(defaultPluginsIndexDir, 'index.json'), const pluginRequres = plugins
pluginEntryPoints.map((plugin) => plugin.manifest),
);
const pluginRequres = pluginEntryPoints
.map((x) => ` '${x.name}': require('${x.name}')`) .map((x) => ` '${x.name}': require('${x.name}')`)
.join(',\n'); .join(',\n');
const generatedIndex = ` const generatedIndex = `
@@ -76,7 +73,7 @@ async function compile(
), ),
}, },
resolver: { resolver: {
resolverMainFields: ['flipper:source', 'module', 'main'], resolverMainFields: ['flipperBundlerEntry', 'module', 'main'],
blacklistRE: /\.native\.js$/, blacklistRE: /\.native\.js$/,
sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json', 'mjs'], sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json', 'mjs'],
}, },
@@ -151,7 +148,7 @@ export async function compileMain() {
}, },
resolver: { resolver: {
sourceExts: ['tsx', 'ts', 'js'], sourceExts: ['tsx', 'ts', 'js'],
resolverMainFields: ['flipper:source', 'module', 'main'], resolverMainFields: ['flipperBundlerEntry', 'module', 'main'],
blacklistRE: /\.native\.js$/, blacklistRE: /\.native\.js$/,
}, },
}); });

View File

@@ -99,7 +99,7 @@ async function startMetroServer(app: Express, server: http.Server) {
}, },
resolver: { resolver: {
...baseConfig.resolver, ...baseConfig.resolver,
resolverMainFields: ['flipper:source', 'module', 'main'], resolverMainFields: ['flipperBundlerEntry', 'module', 'main'],
blacklistRE: /\.native\.js$/, blacklistRE: /\.native\.js$/,
resolveRequest: (context: any, moduleName: string, platform: string) => { resolveRequest: (context: any, moduleName: string, platform: string) => {
if (moduleName.startsWith('./localhost:3000')) { if (moduleName.startsWith('./localhost:3000')) {

View File

@@ -14,8 +14,8 @@ import util from 'util';
import recursiveReaddir from 'recursive-readdir'; import recursiveReaddir from 'recursive-readdir';
import pMap from 'p-map'; import pMap from 'p-map';
import {homedir} from 'os'; import {homedir} from 'os';
import {getWatchFolders} from 'flipper-pkg-lib'; import {getWatchFolders, PluginDetails} from 'flipper-pkg-lib';
import {default as getPlugins, PluginManifest, PluginInfo} from './getPlugins'; import getPlugins from './getPlugins';
import startWatchPlugins from './startWatchPlugins'; import startWatchPlugins from './startWatchPlugins';
const HOME_DIR = homedir(); const HOME_DIR = homedir();
@@ -35,13 +35,13 @@ export type CompileOptions = {
recompileOnChanges: boolean; recompileOnChanges: boolean;
}; };
export type CompiledPluginInfo = PluginManifest & {out: string}; export type CompiledPluginDetails = PluginDetails & {entry: string};
export default async function ( export default async function (
reloadCallback: (() => void) | null, reloadCallback: (() => void) | null,
pluginCache: string, pluginCache: string,
options: CompileOptions = DEFAULT_COMPILE_OPTIONS, options: CompileOptions = DEFAULT_COMPILE_OPTIONS,
): Promise<CompiledPluginInfo[]> { ): Promise<CompiledPluginDetails[]> {
if (process.env.FLIPPER_FAST_REFRESH) { if (process.env.FLIPPER_FAST_REFRESH) {
console.log( console.log(
'🥫 Skipping loading of third-party plugins because Fast Refresh is enabled', '🥫 Skipping loading of third-party plugins because Fast Refresh is enabled',
@@ -74,12 +74,12 @@ export default async function (
const compiledDynamicPlugins = (await compilations).filter( const compiledDynamicPlugins = (await compilations).filter(
(c) => c !== null, (c) => c !== null,
) as CompiledPluginInfo[]; ) as CompiledPluginDetails[];
console.log('✅ Compiled all plugins.'); console.log('✅ Compiled all plugins.');
return compiledDynamicPlugins; return compiledDynamicPlugins;
} }
async function startWatchChanges( async function startWatchChanges(
plugins: PluginInfo[], plugins: PluginDetails[],
reloadCallback: (() => void) | null, reloadCallback: (() => void) | null,
pluginCache: string, pluginCache: string,
options: CompileOptions = DEFAULT_COMPILE_OPTIONS, options: CompileOptions = DEFAULT_COMPILE_OPTIONS,
@@ -88,7 +88,7 @@ async function startWatchChanges(
// no hot reloading for plugins in .flipper folder. This is to prevent // no hot reloading for plugins in .flipper folder. This is to prevent
// Flipper from reloading, while we are doing changes on thirdparty plugins. // Flipper from reloading, while we are doing changes on thirdparty plugins.
.filter( .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}); const watchOptions = Object.assign({}, options, {force: true});
await startWatchPlugins(filteredPlugins, (plugin) => await startWatchPlugins(filteredPlugins, (plugin) =>
@@ -142,30 +142,32 @@ async function getMetroDir() {
return __dirname; return __dirname;
} }
async function compilePlugin( async function compilePlugin(
pluginInfo: PluginInfo, pluginDetails: PluginDetails,
pluginCache: string, pluginCache: string,
{force, failSilently}: CompileOptions, {force, failSilently}: CompileOptions,
): Promise<CompiledPluginInfo | null> { ): Promise<CompiledPluginDetails | null> {
const {rootDir, manifest, entry, name} = pluginInfo; const {dir, specVersion, version, main, source, name} = pluginDetails;
const bundleMain = manifest.bundleMain ?? path.join('dist', 'index.js');
const bundlePath = path.join(rootDir, bundleMain);
const dev = process.env.NODE_ENV !== 'production'; const dev = process.env.NODE_ENV !== 'production';
if (await fs.pathExists(bundlePath)) { if (specVersion > 1) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
const out = path.join(rootDir, bundleMain); const entry = path.join(dir, main);
console.log(`🥫 Using pre-built version of ${name}: ${out}...`); if (await fs.pathExists(entry)) {
return Object.assign({}, pluginInfo.manifest, {out}); console.log(`🥫 Using pre-built version of ${name}: ${entry}...`);
return Object.assign({}, pluginDetails, {entry});
} else { } else {
const out = path.join( console.error(
pluginCache, `❌ Plugin ${name} is ignored, because its entry point not found: ${entry}.`,
`${name}@${manifest.version || '0.0.0'}.js`,
); );
const result = Object.assign({}, pluginInfo.manifest, {out}); return null;
const rootDirCtime = await mostRecentlyChanged(rootDir); }
} else {
const entry = path.join(pluginCache, `${name}@${version || '0.0.0'}.js`);
const result = Object.assign({}, pluginDetails, {entry});
const rootDirCtime = await mostRecentlyChanged(dir);
if ( if (
!force && !force &&
(await fs.pathExists(out)) && (await fs.pathExists(entry)) &&
rootDirCtime < (await fs.lstat(out)).ctime rootDirCtime < (await fs.lstat(entry)).ctime
) { ) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`🥫 Using cached version of ${name}...`); console.log(`🥫 Using cached version of ${name}...`);
@@ -177,9 +179,9 @@ async function compilePlugin(
await Metro.runBuild( await Metro.runBuild(
{ {
reporter: {update: () => {}}, reporter: {update: () => {}},
projectRoot: rootDir, projectRoot: dir,
watchFolders: [metroDir || (await metroDirPromise)].concat( watchFolders: [metroDir || (await metroDirPromise)].concat(
await getWatchFolders(rootDir), await getWatchFolders(dir),
), ),
serializer: { serializer: {
getRunModuleStatement: (moduleID: string) => getRunModuleStatement: (moduleID: string) =>
@@ -197,8 +199,8 @@ async function compilePlugin(
}, },
}, },
{ {
entry: entry.replace(rootDir, '.'), entry: source,
out, out: entry,
dev, dev,
sourceMap: true, sourceMap: true,
minify: false, minify: false,

View File

@@ -11,82 +11,95 @@ import path from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import expandTilde from 'expand-tilde'; import expandTilde from 'expand-tilde';
import getPluginFolders from './getPluginFolders'; import getPluginFolders from './getPluginFolders';
import {PluginDetails, getPluginDetails} from 'flipper-pkg-lib';
import pmap from 'p-map';
import pfilter from 'p-filter';
export type PluginManifest = { export default async function getPlugins(
version: string; includeThirdparty: boolean = false,
name: string; ): Promise<PluginDetails[]> {
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) {
const pluginFolders = await getPluginFolders(includeThirdparty); const pluginFolders = await getPluginFolders(includeThirdparty);
const entryPoints: {[key: string]: PluginInfo} = {}; const entryPoints: {[key: string]: PluginDetails} = {};
pluginFolders.forEach((additionalPath) => { const additionalPlugins = await pmap(pluginFolders, (path) =>
const additionalPlugins = entryPointForPluginFolder(additionalPath); entryPointForPluginFolder(path),
Object.keys(additionalPlugins).forEach((key) => { );
entryPoints[key] = additionalPlugins[key]; for (const p of additionalPlugins) {
}); Object.keys(p).forEach((key) => {
entryPoints[key] = p[key];
}); });
}
return Object.values(entryPoints); return Object.values(entryPoints);
} }
function entryPointForPluginFolder(pluginPath: string) { async function entryPointForPluginFolder(
pluginPath = expandTilde(pluginPath); pluginsDir: string,
if (!fs.existsSync(pluginPath)) { ): Promise<{[key: string]: PluginDetails}> {
pluginsDir = expandTilde(pluginsDir);
if (!fs.existsSync(pluginsDir)) {
return {}; return {};
} }
return fs return await fs
.readdirSync(pluginPath) .readdir(pluginsDir)
.filter((name) => fs.lstatSync(path.join(pluginPath, name)).isDirectory()) .then((entries) =>
.filter(Boolean) entries.map((name) => ({
.map((name) => { dir: path.join(pluginsDir, name),
let packageJSON; manifestPath: path.join(pluginsDir, name, 'package.json'),
})),
)
.then((entries) =>
pfilter(entries, ({manifestPath}) => fs.pathExists(manifestPath)),
)
.then((packages) =>
pmap(packages, async ({manifestPath, dir}) => {
try { try {
packageJSON = fs const manifest = await fs.readJson(manifestPath);
.readFileSync(path.join(pluginPath, name, 'package.json')) return {
.toString(); dir,
} catch (e) {} manifest,
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),
}; };
return plugin;
} catch (e) { } catch (e) {
console.error( 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); console.error(e);
return null; 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; 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!; acc[cv!.name] = cv!;
return acc; return acc;
}, {}); }, {}),
);
}
function notNull<T>(x: T | null | undefined): x is T {
return x !== null && x !== undefined;
} }

View File

@@ -16,7 +16,8 @@
"metro": "^0.59.0", "metro": "^0.59.0",
"mkdirp": "^1.0.0", "mkdirp": "^1.0.0",
"p-map": "^4.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", "uuid": "^7.0.1",
"xdg-basedir": "^4.0.0", "xdg-basedir": "^4.0.0",
"yargs": "^15.3.1", "yargs": "^15.3.1",

View File

@@ -9,18 +9,18 @@
import path from 'path'; import path from 'path';
import Watchman from './watchman'; import Watchman from './watchman';
import {PluginInfo} from './getPlugins'; import {PluginDetails} from 'flipper-pkg-lib';
export default async function startWatchPlugins( export default async function startWatchPlugins(
plugins: PluginInfo[], plugins: PluginDetails[],
compilePlugin: (plugin: PluginInfo) => void | Promise<void>, compilePlugin: (plugin: PluginDetails) => void | Promise<void>,
) { ) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('🕵️‍ Watching for plugin changes'); console.log('🕵️‍ Watching for plugin changes');
const delayedCompilation: {[key: string]: NodeJS.Timeout | null} = {}; const delayedCompilation: {[key: string]: NodeJS.Timeout | null} = {};
const kCompilationDelayMillis = 1000; const kCompilationDelayMillis = 1000;
const onPluginChanged = (plugin: PluginInfo) => { const onPluginChanged = (plugin: PluginDetails) => {
if (!delayedCompilation[plugin.name]) { if (!delayedCompilation[plugin.name]) {
delayedCompilation[plugin.name] = setTimeout(() => { delayedCompilation[plugin.name] = setTimeout(() => {
delayedCompilation[plugin.name] = null; delayedCompilation[plugin.name] = null;
@@ -41,14 +41,14 @@ export default async function startWatchPlugins(
} }
async function startWatchingPluginsUsingWatchman( async function startWatchingPluginsUsingWatchman(
plugins: PluginInfo[], plugins: PluginDetails[],
onPluginChanged: (plugin: PluginInfo) => void, onPluginChanged: (plugin: PluginDetails) => void,
) { ) {
// Initializing a watchman for each folder containing plugins // Initializing a watchman for each folder containing plugins
const watchmanRootMap: {[key: string]: Watchman} = {}; const watchmanRootMap: {[key: string]: Watchman} = {};
await Promise.all( await Promise.all(
plugins.map(async (plugin) => { plugins.map(async (plugin) => {
const watchmanRoot = path.resolve(plugin.rootDir, '..'); const watchmanRoot = path.resolve(plugin.dir, '..');
if (!watchmanRootMap[watchmanRoot]) { if (!watchmanRootMap[watchmanRoot]) {
watchmanRootMap[watchmanRoot] = new Watchman(watchmanRoot); watchmanRootMap[watchmanRoot] = new Watchman(watchmanRoot);
await watchmanRootMap[watchmanRoot].initialize(); await watchmanRootMap[watchmanRoot].initialize();
@@ -58,10 +58,10 @@ async function startWatchingPluginsUsingWatchman(
// Start watching plugins using the initialized watchmans // Start watching plugins using the initialized watchmans
await Promise.all( await Promise.all(
plugins.map(async (plugin) => { plugins.map(async (plugin) => {
const watchmanRoot = path.resolve(plugin.rootDir, '..'); const watchmanRoot = path.resolve(plugin.dir, '..');
const watchman = watchmanRootMap[watchmanRoot]; const watchman = watchmanRootMap[watchmanRoot];
await watchman.startWatchFiles( await watchman.startWatchFiles(
path.relative(watchmanRoot, plugin.rootDir), path.relative(watchmanRoot, plugin.dir),
() => onPluginChanged(plugin), () => onPluginChanged(plugin),
{ {
excludes: ['**/__tests__/**/*', '**/node_modules/**/*', '**/.*'], excludes: ['**/__tests__/**/*', '**/node_modules/**/*', '**/.*'],

View File

@@ -9997,7 +9997,7 @@ recharts@1.7.1:
recharts-scale "^0.4.2" recharts-scale "^0.4.2"
reduce-css-calc "^1.3.0" 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" version "2.2.2"
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==