From 094c5bdfdd62de823b6393598ded0a3185a271e0 Mon Sep 17 00:00:00 2001 From: Andrey Goncharov Date: Thu, 15 Sep 2022 10:02:19 -0700 Subject: [PATCH] Provide external modules to plugins Summary: esbuild references external modules via `require`. We wrap `require` to point the references to built-in modules to global variables Reviewed By: lblasa Differential Revision: D39311893 fbshipit-source-id: a99480161c082f4095d78c22271f114532f32c16 --- desktop/flipper-common/src/index.tsx | 1 + .../src/plugin-external-modules.tsx | 38 +++++++++++++++++++ desktop/flipper-server-companion/src/init.tsx | 4 +- .../src/dispatcher/plugins.tsx | 19 ++++++++++ desktop/pkg-lib/src/runBuild.tsx | 2 +- 5 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 desktop/flipper-common/src/plugin-external-modules.tsx diff --git a/desktop/flipper-common/src/index.tsx b/desktop/flipper-common/src/index.tsx index b28ae7fb9..3d33a2d98 100644 --- a/desktop/flipper-common/src/index.tsx +++ b/desktop/flipper-common/src/index.tsx @@ -23,6 +23,7 @@ export { export * from './server-types'; export * from './companion-types'; export * from './ServerAddOn'; +export * from './plugin-external-modules'; export {sleep} from './utils/sleep'; export {timeout} from './utils/timeout'; export {isTest} from './utils/isTest'; diff --git a/desktop/flipper-common/src/plugin-external-modules.tsx b/desktop/flipper-common/src/plugin-external-modules.tsx new file mode 100644 index 000000000..a337ff844 --- /dev/null +++ b/desktop/flipper-common/src/plugin-external-modules.tsx @@ -0,0 +1,38 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +// This list should match `dispatcher/plugins.tsx` and `builtInModules` in `desktop/.eslintrc.js` +export const pluginExternalModules = { + flipper: 'Flipper', + 'flipper-plugin': 'FlipperPlugin', + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOMClient', + 'react-is': 'ReactIs', + antd: 'antd', + immer: 'Immer', + '@emotion/styled': 'emotion_styled', + '@ant-design/icons': 'antdesign_icons', +}; + +export const wrapRequire = any>(require: T): T => + new Proxy(require, { + apply(target, thisArg, argumentsList) { + const moduleName = argumentsList[0]; + const replacementName = ( + pluginExternalModules as Record + )[moduleName]; + + if (replacementName && replacementName in globalThis) { + return (globalThis as any)[replacementName]; + } + + return Reflect.apply(target, thisArg, argumentsList); + }, + }); diff --git a/desktop/flipper-server-companion/src/init.tsx b/desktop/flipper-server-companion/src/init.tsx index c4e829929..7bc422548 100644 --- a/desktop/flipper-server-companion/src/init.tsx +++ b/desktop/flipper-server-companion/src/init.tsx @@ -7,7 +7,7 @@ * @format */ -import {FlipperServer, getLogger} from 'flipper-common'; +import {FlipperServer, getLogger, wrapRequire} from 'flipper-common'; import {getRenderHostInstance, setGlobalObject} from 'flipper-frontend-core'; import * as FlipperPluginSDK from 'flipper-plugin-core'; import * as Immer from 'immer'; @@ -18,6 +18,7 @@ import * as React from './globalsReplacements/fakeReact'; import * as ReactDOM from './globalsReplacements/fakeReactDOM'; import {styled} from './globalsReplacements/fakeEmotionStyled'; import * as legacyExports from './globalsReplacements/fakeLegacyExports'; +import Module from 'module'; export interface FlipperServerCompanionEnv { pluginInitializer: HeadlessPluginInitializer; @@ -39,6 +40,7 @@ export const initCompanionEnv = async ( emotion_styled: {default: styled}, antdesign_icons: {}, }); + Module.prototype.require = wrapRequire(Module.prototype.require); const flipperServerConfig = await flipperServer.exec('get-config'); initializeRenderHost(flipperServer, flipperServerConfig); diff --git a/desktop/flipper-ui-core/src/dispatcher/plugins.tsx b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx index efa0db5d2..30e9debb8 100644 --- a/desktop/flipper-ui-core/src/dispatcher/plugins.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx @@ -12,6 +12,7 @@ import { InstalledPluginDetails, Logger, MarketplacePluginDetails, + wrapRequire, } from 'flipper-common'; import {PluginDefinition} from '../plugin'; import React from 'react'; @@ -100,6 +101,10 @@ class UIPluginInitializer extends AbstractPluginInitializer { } } +declare module globalThis { + let require: any; +} + let uiPluginInitializer: UIPluginInitializer; export default async (store: Store, _logger: Logger) => { setGlobalObject({ @@ -114,6 +119,20 @@ export default async (store: Store, _logger: Logger) => { emotion_styled, antdesign_icons, }); + // Whenever we bundle plugins, we assume that they are going to share some modules - React, React-DOM, ant design and etc. + // It allows us to decrease the bundle size and not to create separate React roots for every plugin + // To tell a plugin that a module is going to be provided externally, we add the module to the list of externals (see https://esbuild.github.io/api/#external). + // As a result, esbuild does not bundle hte contents of the module. Instead, it wraps the module name with `require(...)`. + // `require` does not exist ion the browser environment, so we substitute it here to feed the plugin our global module. + globalThis.require = wrapRequire( + // globalThis.require might exist in the electron build + globalThis.require ?? + ((module: string) => { + throw new Error( + `Dynamic require is not supported in browser envs. Tried to require: ${module}`, + ); + }), + ); uiPluginInitializer = new UIPluginInitializer(store); await uiPluginInitializer.init(); diff --git a/desktop/pkg-lib/src/runBuild.tsx b/desktop/pkg-lib/src/runBuild.tsx index 35bb6adee..4640fa512 100644 --- a/desktop/pkg-lib/src/runBuild.tsx +++ b/desktop/pkg-lib/src/runBuild.tsx @@ -29,8 +29,8 @@ async function runBuild({pluginDir, entry, out, dev, node}: RunBuildConfig) { format: 'cjs', // This list should match `dispatcher/plugins.tsx` and `builtInModules` in `desktop/.eslintrc.js` external: [ - 'flipper-plugin', 'flipper', + 'flipper-plugin', 'react', 'react-dom', 'react-dom/client',