diff --git a/desktop/.eslintignore b/desktop/.eslintignore index 7ab311a1c..3e4eb255a 100644 --- a/desktop/.eslintignore +++ b/desktop/.eslintignore @@ -12,3 +12,5 @@ website/build react-native/ReactNativeFlipperExample scripts/generate-changelog.js static/index.js +static/defaultPlugins/index.json +app/src/defaultPlugins/index.tsx diff --git a/desktop/.gitignore b/desktop/.gitignore index 6cf8928fa..384077f5f 100644 --- a/desktop/.gitignore +++ b/desktop/.gitignore @@ -1,3 +1,5 @@ lib/ node_modules/ *.tsbuildinfo +/static/defaultPlugins/index.json +/app/src/defaultPlugins/index.tsx diff --git a/desktop/app/src/defaultPlugins/__mocks__/index.tsx b/desktop/app/src/defaultPlugins/__mocks__/index.tsx new file mode 100644 index 000000000..167d9e8a9 --- /dev/null +++ b/desktop/app/src/defaultPlugins/__mocks__/index.tsx @@ -0,0 +1,10 @@ +/** + * 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 {} as any; diff --git a/desktop/app/src/dispatcher/__tests__/TestPlugin.js b/desktop/app/src/dispatcher/__tests__/TestPlugin.tsx similarity index 85% rename from desktop/app/src/dispatcher/__tests__/TestPlugin.js rename to desktop/app/src/dispatcher/__tests__/TestPlugin.tsx index eaa21f5a5..a155e5e74 100644 --- a/desktop/app/src/dispatcher/__tests__/TestPlugin.js +++ b/desktop/app/src/dispatcher/__tests__/TestPlugin.tsx @@ -9,7 +9,7 @@ import {FlipperPlugin} from 'flipper'; -export default class extends FlipperPlugin { +export default class extends FlipperPlugin { static id = 'Static ID'; } diff --git a/desktop/app/src/dispatcher/__tests__/plugins.node.js b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx similarity index 76% rename from desktop/app/src/dispatcher/__tests__/plugins.node.js rename to desktop/app/src/dispatcher/__tests__/plugins.node.tsx index cd538c866..349f8d4ef 100644 --- a/desktop/app/src/dispatcher/__tests__/plugins.node.js +++ b/desktop/app/src/dispatcher/__tests__/plugins.node.tsx @@ -7,23 +7,28 @@ * @format */ +jest.mock('../../defaultPlugins/index'); + import dispatcher, { getDynamicPlugins, checkDisabled, checkGK, requirePlugin, -} from '../plugins.tsx'; +} from '../plugins'; import path from 'path'; import {ipcRenderer, remote} from 'electron'; import {FlipperPlugin} from 'flipper'; -import reducers from '../../reducers/index.tsx'; -import {init as initLogger} from '../../fb-stubs/Logger.tsx'; +import reducers, {State} from '../../reducers/index'; +import {init as initLogger} from '../../fb-stubs/Logger'; import configureStore from 'redux-mock-store'; -import {TEST_PASSING_GK, TEST_FAILING_GK} from '../../fb-stubs/GK.tsx'; +import {TEST_PASSING_GK, TEST_FAILING_GK} from '../../fb-stubs/GK'; import TestPlugin from './TestPlugin'; -import {resetConfigForTesting} from '../../utils/processConfig.tsx'; +import {resetConfigForTesting} from '../../utils/processConfig'; +import {PluginDefinition} from '../../reducers/pluginManager'; -const mockStore = configureStore([])(reducers(undefined, {type: 'INIT'})); +const mockStore = configureStore([])( + reducers(undefined, {type: 'INIT'}), +); const logger = initLogger(mockStore); beforeEach(() => { @@ -37,18 +42,20 @@ test('dispatcher dispatches REGISTER_PLUGINS', () => { }); test('getDynamicPlugins returns empty array on errors', () => { - ipcRenderer.sendSync = jest.fn(); - ipcRenderer.sendSync.mockImplementation(() => { + const sendSyncMock = jest.fn(); + sendSyncMock.mockImplementation(() => { throw new Error('ooops'); }); + ipcRenderer.sendSync = sendSyncMock; const res = getDynamicPlugins(); expect(res).toEqual([]); }); test('getDynamicPlugins from main process via ipc', () => { const plugins = [{name: 'test'}]; - ipcRenderer.sendSync = jest.fn(); - ipcRenderer.sendSync.mockReturnValue(plugins); + const sendSyncMock = jest.fn(); + sendSyncMock.mockReturnValue(plugins); + ipcRenderer.sendSync = sendSyncMock; const res = getDynamicPlugins(); expect(res).toEqual(plugins); }); @@ -94,7 +101,7 @@ test('checkGK for passing plugin', () => { }); test('checkGK for failing plugin', () => { - const gatekeepedPlugins = []; + const gatekeepedPlugins: PluginDefinition[] = []; const name = 'pluginID'; const plugins = checkGK(gatekeepedPlugins)({ name, @@ -118,14 +125,11 @@ test('requirePlugin returns null for invalid requires', () => { test('requirePlugin loads plugin', () => { const name = 'pluginID'; - const homepage = 'https://fb.workplace.com/groups/flippersupport/'; const requireFn = requirePlugin([], require); const plugin = requireFn({ name, - homepage, - out: path.join(__dirname, 'TestPlugin.js'), + out: path.join(__dirname, 'TestPlugin'), }); - expect(plugin.prototype).toBeInstanceOf(FlipperPlugin); - expect(plugin.homepage).toBe(homepage); - expect(plugin.id).toBe(TestPlugin.id); + 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 124c13528..54d11080f 100644 --- a/desktop/app/src/dispatcher/plugins.tsx +++ b/desktop/app/src/dispatcher/plugins.tsx @@ -30,6 +30,9 @@ import isProduction from '../utils/isProduction'; import {notNull} from '../utils/typeUtils'; import {sideEffect} from '../utils/sideEffect'; +// eslint-disable-next-line import/no-unresolved +import {default as defaultPluginsIndex} from '../defaultPlugins/index'; + export type PluginDefinition = { id?: string; name: string; @@ -77,31 +80,31 @@ export default (store: Store, _logger: Logger) => { }; function getBundledPlugins(): Array { - if (!isProduction() || process.env.FLIPPER_NO_EMBEDDED_PLUGINS) { - // Plugins are only bundled in production builds - return []; - } - // DefaultPlugins that are included in the bundle. // List of defaultPlugins is written at build time const pluginPath = - process.env.BUNDLED_PLUGIN_PATH || path.join(__dirname, 'defaultPlugins'); + process.env.BUNDLED_PLUGIN_PATH || + (isProduction() + ? path.join(__dirname, 'defaultPlugins') + : './defaultPlugins/index.json'); let bundledPlugins: Array = []; try { - bundledPlugins = global.electronRequire( - path.join(pluginPath, 'index.json'), - ); + bundledPlugins = global.electronRequire(pluginPath); } catch (e) { console.error(e); } return bundledPlugins .filter((plugin) => notNull(plugin.out)) - .map((plugin) => ({ - ...plugin, - out: path.join(pluginPath, plugin.out!), - })); + .map( + (plugin) => + ({ + ...plugin, + out: path.join(pluginPath, plugin.out!), + } as PluginDefinition), + ) + .concat(bundledPlugins.filter((plugin) => !plugin.out)); } export function getDynamicPlugins() { @@ -152,7 +155,9 @@ export const requirePlugin = ( pluginDefinition: PluginDefinition, ): typeof FlipperPlugin | typeof FlipperDevicePlugin | null => { try { - let plugin = reqFn(pluginDefinition.out); + let plugin = pluginDefinition.out + ? reqFn(pluginDefinition.out) + : defaultPluginsIndex[pluginDefinition.name]; if (plugin.default) { plugin = plugin.default; } diff --git a/desktop/app/src/utils/__tests__/processConfig.node.js b/desktop/app/src/utils/__tests__/processConfig.node.js index 40fb22db3..7512ac2f6 100644 --- a/desktop/app/src/utils/__tests__/processConfig.node.js +++ b/desktop/app/src/utils/__tests__/processConfig.node.js @@ -16,7 +16,6 @@ afterEach(() => { test('config is decoded from env', () => { process.env.CONFIG = JSON.stringify({ disabledPlugins: ['pluginA', 'pluginB', 'pluginC'], - pluginPaths: ['/a/path', 'b/path'], lastWindowPosition: {x: 4, y: 8, width: 15, height: 16}, launcherMsg: 'wubba lubba dub dub', updaterEnabled: false, @@ -26,7 +25,6 @@ test('config is decoded from env', () => { expect(config()).toEqual({ disabledPlugins: new Set(['pluginA', 'pluginB', 'pluginC']), - pluginPaths: ['/a/path', 'b/path'], lastWindowPosition: {x: 4, y: 8, width: 15, height: 16}, launcherMsg: 'wubba lubba dub dub', updaterEnabled: false, @@ -40,7 +38,6 @@ test('config is decoded from env with defaults', () => { expect(config()).toEqual({ disabledPlugins: new Set([]), - pluginPaths: [], lastWindowPosition: undefined, launcherMsg: undefined, updaterEnabled: false, diff --git a/desktop/app/src/utils/processConfig.tsx b/desktop/app/src/utils/processConfig.tsx index a3d0d6e9c..ea7599132 100644 --- a/desktop/app/src/utils/processConfig.tsx +++ b/desktop/app/src/utils/processConfig.tsx @@ -11,7 +11,6 @@ import {remote} from 'electron'; export type ProcessConfig = { disabledPlugins: Set; - pluginPaths: Array; lastWindowPosition: { x: number; y: number; @@ -33,7 +32,6 @@ export default function config(): ProcessConfig { ); configObj = { disabledPlugins: new Set(json.disabledPlugins || []), - pluginPaths: json.pluginPaths || [], lastWindowPosition: json.lastWindowPosition, launcherMsg: json.launcherMsg, // TODO(T64836070): The built-in updater is disabled as we don't have a strategy for signing prod builds right now. diff --git a/desktop/babel-transformer/src/transform.ts b/desktop/babel-transformer/src/transform.ts index 0e17c962c..1871f060d 100644 --- a/desktop/babel-transformer/src/transform.ts +++ b/desktop/babel-transformer/src/transform.ts @@ -28,9 +28,15 @@ export default function transform({ presets = presets ?? [require('@babel/preset-react')]; plugins = plugins ?? []; const isTypeScript = filename.endsWith('.tsx') || filename.endsWith('.ts'); + const commonJs = [ + require('@babel/plugin-transform-modules-commonjs'), + { + strictMode: false, + }, + ]; if (!isTypeScript) { plugins.unshift( - require('@babel/plugin-transform-modules-commonjs'), + commonJs, require('@babel/plugin-proposal-object-rest-spread'), require('@babel/plugin-proposal-class-properties'), require('@babel/plugin-transform-flow-strip-types'), @@ -42,7 +48,7 @@ export default function transform({ plugins.unshift( require('@babel/plugin-transform-typescript'), require('@babel/plugin-proposal-class-properties'), - require('@babel/plugin-transform-modules-commonjs'), + commonJs, require('@babel/plugin-proposal-optional-chaining'), require('@babel/plugin-proposal-nullish-coalescing-operator'), ); diff --git a/desktop/package.json b/desktop/package.json index 6af947761..6d16f8dea 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -189,7 +189,7 @@ }, "scripts": { "preinstall": "node scripts/prepare-watchman-config.js && yarn config set ignore-engines", - "postinstall": "patch-package && ./ts-node scripts/yarn-install-fb-plugins.ts", + "postinstall": "patch-package && ./ts-node scripts/yarn-install-fb-plugins.ts && ./ts-node scripts/generate-plugin-entry-points.ts", "rm-dist": "rimraf ../dist", "rm-modules": "rimraf **/*/node_modules node_modules", "rm-temp": "rimraf $TMPDIR/jest* $TMPDIR/react-native-packager*", @@ -208,7 +208,7 @@ "build:dev": "cross-env NODE_ENV=development ./ts-node scripts/build-release.ts $@", "prebuild-headless": "yarn build:babel-transformer", "build-headless": "cross-env NODE_ENV=production ./ts-node scripts/build-headless.ts $@", - "open-dist": "open ../dist/mac/Flipper.app --args --launcher=false", + "open-dist": "open ../dist/mac/Flipper.app --args --launcher=false --inspect=9229", "fix": "eslint . --fix --ext .js,.ts,.tsx", "test": "yarn build:babel-transformer && jest --env=jest-environment-jsdom-sixteen --testPathPattern=\"node\\.(js|ts|tsx)$\" --no-cache", "test:debug": "yarn build:babel-transformer && node --inspect node_modules/.bin/jest --runInBand --env=jest-environment-jsdom-sixteen", diff --git a/desktop/scripts/build-headless.ts b/desktop/scripts/build-headless.ts index ef3fe2a5d..26522438f 100644 --- a/desktop/scripts/build-headless.ts +++ b/desktop/scripts/build-headless.ts @@ -15,15 +15,13 @@ const {exec: createBinary} = require('pkg'); import { buildFolder, compileHeadless, - compileDefaultPlugins, getVersionNumber, genMercurialRevision, + generatePluginEntryPoints, } from './build-utils'; import isFB from './isFB'; import {distDir} from './paths'; -const PLUGINS_FOLDER_NAME = 'plugins'; - function preludeBundle( dir: string, versionNumber: string, @@ -52,15 +50,6 @@ async function createZip(buildDir: string, distDir: string, targets: string[]) { zip.addFile(path.join(buildDir, binary), binary); }); - // add plugins - const pluginDir = path.join(buildDir, PLUGINS_FOLDER_NAME); - fs.readdirSync(pluginDir).forEach((file) => { - zip.addFile( - path.join(pluginDir, file), - path.join(PLUGINS_FOLDER_NAME, file), - ); - }); - // write zip file zip.outputStream .pipe(fs.createWriteStream(path.join(distDir, 'Flipper-headless.zip'))) @@ -94,22 +83,15 @@ async function createZip(buildDir: string, distDir: string, targets: string[]) { // platformPostfix is automatically added by pkg platformPostfix = ''; } - // Compiling all plugins takes a long time. Use this flag for quicker - // developement iteration by not including any plugins. - const skipPlugins = process.argv.indexOf('--no-plugins') > -1; - process.env.FLIPPER_HEADLESS = 'true'; const buildDir = await buildFolder(); // eslint-disable-next-line no-console console.log('Created build directory', buildDir); + await generatePluginEntryPoints(); await compileHeadless(buildDir); const versionNumber = getVersionNumber(); const buildRevision = await genMercurialRevision(); await preludeBundle(buildDir, versionNumber, buildRevision); - await compileDefaultPlugins( - path.join(buildDir, PLUGINS_FOLDER_NAME), - skipPlugins, - ); await createBinary([ path.join(buildDir, 'bundle.js'), '--output', diff --git a/desktop/scripts/build-release.ts b/desktop/scripts/build-release.ts index 8499c2721..fa4624fe7 100755 --- a/desktop/scripts/build-release.ts +++ b/desktop/scripts/build-release.ts @@ -16,9 +16,9 @@ import { compileRenderer, compileMain, die, - compileDefaultPlugins, getVersionNumber, genMercurialRevision, + generatePluginEntryPoints, } from './build-utils'; import fetch from 'node-fetch'; import {getIcons, buildLocalIconPath, getIconURL} from '../app/src/utils/icons'; @@ -126,7 +126,9 @@ async function buildDist(buildFolder: string) { } async function copyStaticFolder(buildFolder: string) { + console.log(`⚙️ Copying static package with dependencies...`); await copyPackageWithDependencies(staticDir, buildFolder); + console.log('✅ Copied static package with dependencies.'); } function downloadIcons(buildFolder: string) { @@ -184,11 +186,9 @@ function downloadIcons(buildFolder: string) { console.log('Created build directory', dir); await compileMain(); + await generatePluginEntryPoints(); await copyStaticFolder(dir); await downloadIcons(dir); - if (!process.argv.includes('--no-embedded-plugins')) { - await compileDefaultPlugins(path.join(dir, 'defaultPlugins')); - } await compileRenderer(dir); const versionNumber = getVersionNumber(); const hgRevision = await genMercurialRevision(); diff --git a/desktop/scripts/build-utils.ts b/desktop/scripts/build-utils.ts index b002731ad..7233efb81 100644 --- a/desktop/scripts/build-utils.ts +++ b/desktop/scripts/build-utils.ts @@ -8,64 +8,53 @@ */ import Metro from 'metro'; -import compilePlugins from '../static/compilePlugins'; -import util from 'util'; import tmp from 'tmp'; import path from 'path'; import fs from 'fs-extra'; import {spawn} from 'promisify-child-process'; -import recursiveReaddir from 'recursive-readdir'; import getWatchFolders from '../static/getWatchFolders'; import getAppWatchFolders from './get-app-watch-folders'; +import getPlugins from '../static/getPlugins'; import { appDir, staticDir, - pluginsDir, + defaultPluginsIndexDir, headlessDir, babelTransformationsDir, } from './paths'; +import getPluginFolders from '../static/getPluginFolders'; const dev = process.env.NODE_ENV !== 'production'; -async function mostRecentlyChanged( - dir: string, - ignores: string[], -): Promise { - const files = await util.promisify( - recursiveReaddir, - )(dir, ignores); - return files - .map((f) => fs.lstatSync(f).ctime) - .reduce((a, b) => (a > b ? a : b), new Date(0)); -} - export function die(err: Error) { console.error(err.stack); process.exit(1); } -export function compileDefaultPlugins( - defaultPluginDir: string, - skipAll: boolean = false, -) { - return compilePlugins( - null, - skipAll ? [] : [pluginsDir, path.join(pluginsDir, 'fb')], - defaultPluginDir, - {force: true, failSilently: false, recompileOnChanges: false}, - ) - .then((defaultPlugins) => - fs.writeFileSync( - path.join(defaultPluginDir, 'index.json'), - JSON.stringify( - defaultPlugins.map(({entry, rootDir, out, ...plugin}) => ({ - ...plugin, - out: path.parse(out).base, - })), - ), - ), - ) - .catch(die); +export async function generatePluginEntryPoints() { + console.log('⚙️ Generating plugin entry points...'); + const pluginEntryPoints = 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 + .map((x) => ` '${x.name}': require('${x.name}')`) + .join(',\n'); + const generatedIndex = ` + // THIS FILE IS AUTO-GENERATED by function "generatePluginEntryPoints" in "build-utils.ts". + export default {\n${pluginRequres}\n} as any + `; + await fs.ensureDir(path.join(appDir, 'src', 'defaultPlugins')); + await fs.writeFile( + path.join(appDir, 'src', 'defaultPlugins', 'index.tsx'), + generatedIndex, + ); + console.log('✅ Generated plugin entry points.'); } async function compile( @@ -89,6 +78,7 @@ async function compile( resolver: { resolverMainFields: ['flipper:source', 'module', 'main'], blacklistRE: /\.native\.js$/, + sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json', 'mjs'], }, }, { @@ -108,6 +98,7 @@ export async function compileHeadless(buildFolder: string) { headlessDir, ...(await getWatchFolders(staticDir)), ...(await getAppWatchFolders()), + ...(await getPluginFolders()), ] .filter((value, index, self) => self.indexOf(value) === index) .filter(fs.pathExistsSync); @@ -126,7 +117,10 @@ export async function compileHeadless(buildFolder: string) { export async function compileRenderer(buildFolder: string) { console.log(`⚙️ Compiling renderer bundle...`); - const watchFolders = await getAppWatchFolders(); + const watchFolders = [ + ...(await getAppWatchFolders()), + ...(await getPluginFolders()), + ]; try { await compile( buildFolder, @@ -143,15 +137,6 @@ export async function compileRenderer(buildFolder: string) { export async function compileMain() { const out = path.join(staticDir, 'main.bundle.js'); process.env.FLIPPER_ELECTRON_VERSION = require('electron/package.json').version; - // check if main needs to be compiled - if (await fs.pathExists(out)) { - const staticDirCtime = await mostRecentlyChanged(staticDir, ['*.bundle.*']); - const bundleCtime = (await fs.lstat(out)).ctime; - if (staticDirCtime < bundleCtime) { - console.log(`🥫 Using cached version of main bundle...`); - return; - } - } console.log('⚙️ Compiling main bundle...'); try { const config = Object.assign({}, await Metro.loadConfig(), { diff --git a/desktop/scripts/generate-plugin-entry-points.ts b/desktop/scripts/generate-plugin-entry-points.ts new file mode 100644 index 000000000..6da52acf7 --- /dev/null +++ b/desktop/scripts/generate-plugin-entry-points.ts @@ -0,0 +1,15 @@ +/** + * 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 {generatePluginEntryPoints} from './build-utils'; + +generatePluginEntryPoints().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/desktop/scripts/paths.ts b/desktop/scripts/paths.ts index 9f58aed9d..ce9505985 100644 --- a/desktop/scripts/paths.ts +++ b/desktop/scripts/paths.ts @@ -12,6 +12,7 @@ import path from 'path'; export const rootDir = path.resolve(__dirname, '..'); export const appDir = path.join(rootDir, 'app'); export const staticDir = path.join(rootDir, 'static'); +export const defaultPluginsIndexDir = path.join(staticDir, 'defaultPlugins'); export const pluginsDir = path.join(rootDir, 'plugins'); export const headlessDir = path.join(rootDir, 'headless'); export const distDir = path.resolve(rootDir, '..', 'dist'); diff --git a/desktop/scripts/start-dev-server.ts b/desktop/scripts/start-dev-server.ts index e26144233..dcda0e507 100644 --- a/desktop/scripts/start-dev-server.ts +++ b/desktop/scripts/start-dev-server.ts @@ -18,13 +18,16 @@ import chalk from 'chalk'; import http from 'http'; import path from 'path'; import fs from 'fs-extra'; -import {compileMain} from './build-utils'; +import {compileMain, generatePluginEntryPoints} from './build-utils'; import Watchman from '../static/watchman'; import Metro from 'metro'; import MetroResolver from 'metro-resolver'; -import getAppWatchFolders from './get-app-watch-folders'; import {staticDir, appDir, babelTransformationsDir} from './paths'; import isFB from './isFB'; +import getAppWatchFolders from './get-app-watch-folders'; +import getPlugins from '../static/getPlugins'; +import getPluginFolders from '../static/getPluginFolders'; +import startWatchPlugins from '../static/startWatchPlugins'; const ansiToHtmlConverter = new AnsiToHtmlConverter(); @@ -81,7 +84,9 @@ function launchElectron({ } async function startMetroServer(app: Express) { - const watchFolders = await getAppWatchFolders(); + const watchFolders = (await getAppWatchFolders()).concat( + await getPluginFolders(), + ); const metroBundlerServer = await Metro.runMetro({ projectRoot: appDir, watchFolders, @@ -101,6 +106,7 @@ async function startMetroServer(app: Express) { platform, ); }, + sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json', 'mjs'], }, watch: true, }); @@ -170,7 +176,7 @@ async function addWebsocket(server: http.Server) { } }); - // refresh the app on changes to the src folder + // refresh the app on changes // this can be removed once metroServer notifies us about file changes try { const watchman = new Watchman(path.resolve(__dirname, '..')); @@ -188,6 +194,10 @@ async function addWebsocket(server: http.Server) { ), ), ); + const plugins = await getPlugins(); + await startWatchPlugins(plugins, () => { + io.emit('refresh'); + }); } catch (err) { console.error( 'Failed to start watching for changes using Watchman, continue without hot reloading', @@ -258,6 +268,7 @@ function outputScreen(socket?: socketIo.Server) { await startMetroServer(app); outputScreen(socket); await compileMain(); + await generatePluginEntryPoints(); shutdownElectron = launchElectron({ devServerURL: `http://localhost:${port}`, bundleURL: `http://localhost:${port}/src/init.bundle`, diff --git a/desktop/static/compilePlugins.ts b/desktop/static/compilePlugins.ts index 1fb6a8f7c..2765cc989 100644 --- a/desktop/static/compilePlugins.ts +++ b/desktop/static/compilePlugins.ts @@ -15,12 +15,8 @@ import recursiveReaddir from 'recursive-readdir'; import pMap from 'p-map'; import {homedir} from 'os'; import getWatchFolders from './getWatchFolders'; -import { - default as getPluginEntryPoints, - PluginManifest, - PluginInfo, -} from './getPluginEntryPoints'; -import watchPlugins from './watchPlugins'; +import {default as getPlugins, PluginManifest, PluginInfo} from './getPlugins'; +import startWatchPlugins from './startWatchPlugins'; const HOME_DIR = homedir(); @@ -43,46 +39,53 @@ export type CompiledPluginInfo = PluginManifest & {out: string}; export default async function ( reloadCallback: (() => void) | null, - pluginPaths: string[], pluginCache: string, options: CompileOptions = DEFAULT_COMPILE_OPTIONS, ) { options = Object.assign({}, DEFAULT_COMPILE_OPTIONS, options); - const plugins = getPluginEntryPoints(pluginPaths); - if (!(await fs.pathExists(pluginCache))) { - await fs.mkdir(pluginCache); - } + const defaultPlugins = ( + await fs.readJson(path.join(__dirname, 'defaultPlugins', 'index.json')) + ).map((p: any) => p.name) as string[]; + const dynamicPlugins = (await getPlugins(true)).filter( + (p) => !defaultPlugins.includes(p.name), + ); + await fs.ensureDir(pluginCache); if (options.recompileOnChanges) { - await startWatchChanges(plugins, reloadCallback, pluginCache, options); + await startWatchChanges( + dynamicPlugins, + reloadCallback, + pluginCache, + options, + ); } const compilations = pMap( - Object.values(plugins), + dynamicPlugins, (plugin) => { return compilePlugin(plugin, pluginCache, options); }, {concurrency: 4}, ); - const dynamicPlugins = (await compilations).filter( + const compiledDynamicPlugins = (await compilations).filter( (c) => c !== null, ) as CompiledPluginInfo[]; console.log('✅ Compiled all plugins.'); - return dynamicPlugins; + return compiledDynamicPlugins; } async function startWatchChanges( - plugins: {[key: string]: PluginInfo}, + plugins: PluginInfo[], reloadCallback: (() => void) | null, pluginCache: string, options: CompileOptions = DEFAULT_COMPILE_OPTIONS, ) { - const filteredPlugins = Object.values(plugins) + const filteredPlugins = plugins // 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')), ); - const watchOptions = Object.assign(options, {force: true}); - await watchPlugins(filteredPlugins, (plugin) => + const watchOptions = Object.assign({}, options, {force: true}); + await startWatchPlugins(filteredPlugins, (plugin) => compilePlugin(plugin, pluginCache, watchOptions).then( reloadCallback ?? (() => {}), ), diff --git a/desktop/static/getPluginFolders.ts b/desktop/static/getPluginFolders.ts new file mode 100644 index 000000000..8d33fad33 --- /dev/null +++ b/desktop/static/getPluginFolders.ts @@ -0,0 +1,32 @@ +/** + * 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 path from 'path'; +import fs from 'fs-extra'; +import expandTilde from 'expand-tilde'; +import {homedir} from 'os'; + +export default async function getPluginFolders( + includeThirdparty: boolean = false, +) { + const pluginFolders: string[] = []; + if (includeThirdparty) { + pluginFolders.push(path.join(homedir(), '.flipper', 'thirdparty')); + } + const flipperConfigPath = path.join(homedir(), '.flipper', 'config.json'); + if (await fs.pathExists(flipperConfigPath)) { + const config = await fs.readJson(flipperConfigPath); + if (config.pluginPaths) { + pluginFolders.push(...config.pluginPaths); + } + } + pluginFolders.push(path.resolve(__dirname, '..', 'plugins')); + pluginFolders.push(path.resolve(__dirname, '..', 'plugins', 'fb')); + return pluginFolders.map(expandTilde).filter(fs.existsSync); +} diff --git a/desktop/static/getPluginEntryPoints.ts b/desktop/static/getPlugins.ts similarity index 83% rename from desktop/static/getPluginEntryPoints.ts rename to desktop/static/getPlugins.ts index ac0a246a6..48860ea71 100644 --- a/desktop/static/getPluginEntryPoints.ts +++ b/desktop/static/getPlugins.ts @@ -10,9 +10,7 @@ import path from 'path'; import fs from 'fs-extra'; import expandTilde from 'expand-tilde'; -import {homedir} from 'os'; - -const HOME_DIR = homedir(); +import getPluginFolders from './getPluginFolders'; export type PluginManifest = { version: string; @@ -29,19 +27,16 @@ export type PluginInfo = { manifest: PluginManifest; }; -export default function getPluginEntryPoints(additionalPaths: string[] = []) { - const defaultPluginPath = path.join(HOME_DIR, '.flipper', 'node_modules'); - const entryPoints = entryPointForPluginFolder(defaultPluginPath); - if (typeof additionalPaths === 'string') { - additionalPaths = [additionalPaths]; - } - additionalPaths.forEach((additionalPath) => { +export default async function getPlugins(includeThirdparty: boolean = false) { + 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]; }); }); - return entryPoints; + return Object.values(entryPoints); } function entryPointForPluginFolder(pluginPath: string) { pluginPath = expandTilde(pluginPath); @@ -62,6 +57,9 @@ function entryPointForPluginFolder(pluginPath: string) { 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"`, diff --git a/desktop/static/main.ts b/desktop/static/main.ts index 574e4a0f5..cb187e876 100644 --- a/desktop/static/main.ts +++ b/desktop/static/main.ts @@ -27,7 +27,6 @@ import compilePlugins from './compilePlugins'; import setup from './setup'; import isFB from './fb-stubs/isFB'; import delegateToLauncher from './launcher'; -import expandTilde from 'expand-tilde'; import yargs from 'yargs'; const VERSION: string = (global as any).__VERSION__; @@ -90,25 +89,7 @@ if (isFB && process.env.FLIPPER_FB === undefined) { process.env.FLIPPER_FB = 'true'; } -const skipLoadingEmbeddedPlugins = process.env.FLIPPER_NO_EMBEDDED_PLUGINS; - -const pluginPaths = (config.pluginPaths ?? []) - .concat([ - path.join(configPath, '..', 'thirdparty'), - ...(skipLoadingEmbeddedPlugins - ? [] - : [ - path.join(__dirname, '..', 'plugins'), - path.join(__dirname, '..', 'plugins', 'fb'), - ]), - ]) - .map(expandTilde) - .filter(fs.existsSync); - -process.env.CONFIG = JSON.stringify({ - ...config, - pluginPaths, -}); +process.env.CONFIG = JSON.stringify(config); // possible reference to main app window let win: BrowserWindow; @@ -124,15 +105,11 @@ setInterval(() => { } }, 60 * 1000); -compilePlugins( - () => { - if (win) { - win.reload(); - } - }, - pluginPaths, - path.join(flipperDir, 'plugins'), -).then((dynamicPlugins) => { +compilePlugins(() => { + if (win) { + win.reload(); + } +}, path.join(flipperDir, 'plugins')).then((dynamicPlugins) => { ipcMain.on('get-dynamic-plugins', (event) => { event.returnValue = dynamicPlugins; }); diff --git a/desktop/static/watchPlugins.ts b/desktop/static/startWatchPlugins.ts similarity index 95% rename from desktop/static/watchPlugins.ts rename to desktop/static/startWatchPlugins.ts index 6d63abf7e..edb072f05 100644 --- a/desktop/static/watchPlugins.ts +++ b/desktop/static/startWatchPlugins.ts @@ -9,9 +9,9 @@ import path from 'path'; import Watchman from './watchman'; -import {PluginInfo} from './getPluginEntryPoints'; +import {PluginInfo} from './getPlugins'; -export default async function watchPlugins( +export default async function startWatchPlugins( plugins: PluginInfo[], compilePlugin: (plugin: PluginInfo) => void | Promise, ) {