diff --git a/desktop/app/package.json b/desktop/app/package.json index 09b30f60a..55a6d0538 100644 --- a/desktop/app/package.json +++ b/desktop/app/package.json @@ -82,12 +82,6 @@ "yargs": "^15.3.1", "yazl": "^2.5.1" }, - "greenkeeper": { - "ignore": [ - "tmp", - "flipper-doctor" - ] - }, "optionalDependencies": { "7zip-bin-mac": "^1.0.1" } diff --git a/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx b/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx index 292fbf0ab..948ba5d45 100644 --- a/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx +++ b/desktop/app/src/chrome/plugin-manager/PluginDebugger.tsx @@ -143,14 +143,7 @@ class PluginDebugger extends Component { getRows(): Array { const rows: Array = []; - // 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( diff --git a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx index 349f8d4ef..c2ee8ff9c 100644 --- a/desktop/app/src/dispatcher/__tests__/plugins.node.tsx +++ b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx @@ -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); diff --git a/desktop/app/src/dispatcher/plugins.tsx b/desktop/app/src/dispatcher/plugins.tsx index 54d11080f..5c2260500 100644 --- a/desktop/app/src/dispatcher/plugins.tsx +++ b/desktop/app/src/dispatcher/plugins.tsx @@ -96,15 +96,15 @@ function getBundledPlugins(): Array { } 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; diff --git a/desktop/babel-transformer/package.json b/desktop/babel-transformer/package.json index 32e03c1e2..d98b20313 100644 --- a/desktop/babel-transformer/package.json +++ b/desktop/babel-transformer/package.json @@ -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", diff --git a/desktop/doctor/package.json b/desktop/doctor/package.json index 3fc715a0f..88fc26758 100644 --- a/desktop/doctor/package.json +++ b/desktop/doctor/package.json @@ -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": { diff --git a/desktop/pkg-lib/package.json b/desktop/pkg-lib/package.json index 4d5a02796..9a49b7d1b 100644 --- a/desktop/pkg-lib/package.json +++ b/desktop/pkg-lib/package.json @@ -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", diff --git a/desktop/pkg-lib/src/PluginDetails.ts b/desktop/pkg-lib/src/PluginDetails.ts new file mode 100644 index 000000000..ec3e3c0a3 --- /dev/null +++ b/desktop/pkg-lib/src/PluginDetails.ts @@ -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; + }; +} diff --git a/desktop/pkg-lib/src/__tests__/getPluginDetails.node.ts b/desktop/pkg-lib/src/__tests__/getPluginDetails.node.ts new file mode 100644 index 000000000..dd5088389 --- /dev/null +++ b/desktop/pkg-lib/src/__tests__/getPluginDetails.node.ts @@ -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", + } + `); +}); diff --git a/desktop/pkg-lib/src/getPluginDetails.ts b/desktop/pkg-lib/src/getPluginDetails.ts new file mode 100644 index 000000000..ee2532687 --- /dev/null +++ b/desktop/pkg-lib/src/getPluginDetails.ts @@ -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 { + 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 { + 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 { + 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, + }; +} diff --git a/desktop/pkg-lib/src/index.ts b/desktop/pkg-lib/src/index.ts index e5ab0987d..87d8b505a 100644 --- a/desktop/pkg-lib/src/index.ts +++ b/desktop/pkg-lib/src/index.ts @@ -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'; diff --git a/desktop/pkg/package.json b/desktop/pkg/package.json index 5c8df3eca..368ad5b8d 100644 --- a/desktop/pkg/package.json +++ b/desktop/pkg/package.json @@ -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": { diff --git a/desktop/pkg/src/commands/bundle.ts b/desktop/pkg/src/commands/bundle.ts index 0f8195a42..dd1ee11b9 100644 --- a/desktop/pkg/src/commands/bundle.ts +++ b/desktop/pkg/src/commands/bundle.ts @@ -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 { 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}`); diff --git a/desktop/plugins/network/index.tsx b/desktop/plugins/network/index.tsx index b71b347a3..4e831fe4a 100644 --- a/desktop/plugins/network/index.tsx +++ b/desktop/plugins/network/index.tsx @@ -122,6 +122,7 @@ export const NetworkRouteContext = createContext( ); export default class extends FlipperPlugin { + static id = 'Network'; static keyboardActions: Array = ['clear']; static subscribed = []; static defaultPersistedState = { diff --git a/desktop/plugins/network/package.json b/desktop/plugins/network/package.json index dbcb0021d..b795baba3 100644 --- a/desktop/plugins/network/package.json +++ b/desktop/plugins/network/package.json @@ -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" } } diff --git a/desktop/scripts/build-utils.ts b/desktop/scripts/build-utils.ts index 4ffc28ccb..34d1edd9e 100644 --- a/desktop/scripts/build-utils.ts +++ b/desktop/scripts/build-utils.ts @@ -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$/, }, }); diff --git a/desktop/scripts/start-dev-server.ts b/desktop/scripts/start-dev-server.ts index 245cb29d4..99b7dee81 100644 --- a/desktop/scripts/start-dev-server.ts +++ b/desktop/scripts/start-dev-server.ts @@ -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')) { diff --git a/desktop/static/compilePlugins.ts b/desktop/static/compilePlugins.ts index 98de73274..6b5a08502 100644 --- a/desktop/static/compilePlugins.ts +++ b/desktop/static/compilePlugins.ts @@ -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 { +): Promise { 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 { - const {rootDir, manifest, entry, name} = pluginInfo; - const bundleMain = manifest.bundleMain ?? path.join('dist', 'index.js'); - const bundlePath = path.join(rootDir, bundleMain); +): Promise { + 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 { + console.error( + `❌ Plugin ${name} is ignored, because its entry point not found: ${entry}.`, + ); + return null; + } } else { - const out = path.join( - pluginCache, - `${name}@${manifest.version || '0.0.0'}.js`, - ); - const result = Object.assign({}, pluginInfo.manifest, {out}); - const rootDirCtime = await mostRecentlyChanged(rootDir); + 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, diff --git a/desktop/static/getPlugins.ts b/desktop/static/getPlugins.ts index 48860ea71..90347b561 100644 --- a/desktop/static/getPlugins.ts +++ b/desktop/static/getPlugins.ts @@ -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 { 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; - try { - packageJSON = fs - .readFileSync(path.join(pluginPath, name, 'package.json')) - .toString(); - } catch (e) {} - if (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 { - 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; } - } - return null; - }) - .filter(Boolean) - .reduce<{[key: string]: PluginInfo}>((acc, cv) => { - acc[cv!.name] = cv!; - return acc; - }, {}); + }), + ) + .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; + } + }), + ) + .then((plugins) => plugins.filter(notNull)) + .then((plugins) => + plugins.reduce<{[key: string]: PluginDetails}>((acc, cv) => { + acc[cv!.name] = cv!; + return acc; + }, {}), + ); +} + +function notNull(x: T | null | undefined): x is T { + return x !== null && x !== undefined; } diff --git a/desktop/static/package.json b/desktop/static/package.json index 1270cb2ae..9c4db712b 100644 --- a/desktop/static/package.json +++ b/desktop/static/package.json @@ -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", diff --git a/desktop/static/startWatchPlugins.ts b/desktop/static/startWatchPlugins.ts index edb072f05..f09ffca39 100644 --- a/desktop/static/startWatchPlugins.ts +++ b/desktop/static/startWatchPlugins.ts @@ -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, + plugins: PluginDetails[], + compilePlugin: (plugin: PluginDetails) => void | Promise, ) { // 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/**/*', '**/.*'], diff --git a/desktop/yarn.lock b/desktop/yarn.lock index c4d5c189e..d40fae899 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -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==