From 587f428cf82ba088549cac38fed78b910eca822a Mon Sep 17 00:00:00 2001 From: Lorenzo Blasa Date: Thu, 27 Oct 2022 22:50:30 -0700 Subject: [PATCH] Allow plugins to use css Summary: Flipper plugins fail when importing css from third-party dependencies. This diff tries to fix that. Effectively, the plugin can import the css and export it when is bundled. When we load the plugin, we check if there's a css file for it. If there's one, we return it and try to use it. Reviewed By: aigoncharov Differential Revision: D40758178 fbshipit-source-id: e53afffcc481504905d5eeb1aea1f9114ee2a86b --- .../app/src/electron/initializeElectron.tsx | 20 ++++++++++++-- desktop/flipper-common/src/server-types.tsx | 6 ++++- .../flipper-frontend-core/src/RenderHost.tsx | 2 +- .../src/plugin/SandyPluginDefinition.tsx | 5 +++- .../src/plugin/PluginRenderer.tsx | 9 +++++++ .../src/HeadlessPluginInitializer.tsx | 6 ++--- .../src/initializeRenderHost.tsx | 16 +++++++----- .../src/plugins/PluginManager.tsx | 26 ++++++++++++++++--- .../src/initializeRenderHost.tsx | 13 +++++----- .../createMockFlipperWithPlugin.node.tsx.snap | 1 + .../src/dispatcher/__tests__/plugins.node.tsx | 1 + .../src/dispatcher/plugins.tsx | 13 +++++++--- desktop/plugins/public/package.json | 3 ++- desktop/plugins/public/ui-debugger/index.tsx | 1 + desktop/scripts/jest-setup-after.tsx | 2 +- 15 files changed, 94 insertions(+), 30 deletions(-) diff --git a/desktop/app/src/electron/initializeElectron.tsx b/desktop/app/src/electron/initializeElectron.tsx index f7798a152..24e740b3a 100644 --- a/desktop/app/src/electron/initializeElectron.tsx +++ b/desktop/app/src/electron/initializeElectron.tsx @@ -189,8 +189,24 @@ export async function initializeElectron( return flipperServerConfig.gatekeepers[gatekeeper] ?? false; }, flipperServer, - async requirePlugin(path) { - return electronRequire(path); + async requirePlugin(path): Promise<{plugin: any; css?: string}> { + const plugin = electronRequire(path); + /** + * Check if the plugin includes a bundled css. If so, + * load its content too. + */ + const idx = path.lastIndexOf('.'); + const cssPath = path.substring(0, idx < 0 ? path.length : idx) + '.css'; + try { + await fs.promises.access(cssPath); + + const buffer = await fs.promises.readFile(cssPath, {encoding: 'utf-8'}); + const css = buffer.toString(); + + return {plugin, css}; + } catch (e) {} + + return {plugin}; }, getStaticResourceUrl(relativePath): string { return ( diff --git a/desktop/flipper-common/src/server-types.tsx b/desktop/flipper-common/src/server-types.tsx index 7ce8e0d07..301aa8704 100644 --- a/desktop/flipper-common/src/server-types.tsx +++ b/desktop/flipper-common/src/server-types.tsx @@ -176,6 +176,10 @@ export interface DeviceDebugData { data: (DeviceDebugFile | DeviceDebugCommand)[]; } +export interface PluginSource { + js: string; + css?: string; +} export type FlipperServerCommands = { 'get-server-state': () => Promise<{ state: FlipperServerState; @@ -275,7 +279,7 @@ export type FlipperServerCommands = { 'plugin-start-download': ( plugin: DownloadablePluginDetails, ) => Promise; - 'plugin-source': (path: string) => Promise; + 'plugin-source': (path: string) => Promise; 'plugins-install-from-marketplace': ( name: string, ) => Promise; diff --git a/desktop/flipper-frontend-core/src/RenderHost.tsx b/desktop/flipper-frontend-core/src/RenderHost.tsx index 0dbc1313f..168bdccb1 100644 --- a/desktop/flipper-frontend-core/src/RenderHost.tsx +++ b/desktop/flipper-frontend-core/src/RenderHost.tsx @@ -142,7 +142,7 @@ export interface RenderHost { GK(gatekeeper: string): boolean; flipperServer: FlipperServer; serverConfig: FlipperServerConfig; - requirePlugin(path: string): Promise; + requirePlugin(path: string): Promise<{plugin: any; css?: string}>; getStaticResourceUrl(relativePath: string): string; // given the requested icon and proposed public url of the icon, rewrite it to a local icon if needed getLocalIconUrl?(icon: Icon, publicUrl: string): string; diff --git a/desktop/flipper-plugin-core/src/plugin/SandyPluginDefinition.tsx b/desktop/flipper-plugin-core/src/plugin/SandyPluginDefinition.tsx index d1c064504..b792b4181 100644 --- a/desktop/flipper-plugin-core/src/plugin/SandyPluginDefinition.tsx +++ b/desktop/flipper-plugin-core/src/plugin/SandyPluginDefinition.tsx @@ -55,15 +55,18 @@ export type FlipperPluginModule< export class SandyPluginDefinition { id: string; module: FlipperPluginModule | FlipperDevicePluginModule; + css?: string; details: ActivatablePluginDetails; isDevicePlugin: boolean; constructor( details: ActivatablePluginDetails, module: FlipperPluginModule | FlipperDevicePluginModule, + css?: string, ); - constructor(details: ActivatablePluginDetails, module: any) { + constructor(details: ActivatablePluginDetails, module: any, css?: string) { this.id = details.id; + this.css = css; this.details = details; if ( details.pluginType === 'device' || diff --git a/desktop/flipper-plugin/src/plugin/PluginRenderer.tsx b/desktop/flipper-plugin/src/plugin/PluginRenderer.tsx index 9008dfc3f..55324ce66 100644 --- a/desktop/flipper-plugin/src/plugin/PluginRenderer.tsx +++ b/desktop/flipper-plugin/src/plugin/PluginRenderer.tsx @@ -28,9 +28,18 @@ export const SandyPluginRenderer = memo(({plugin}: Props) => { throw new Error('Expected plugin, got ' + plugin); } useEffect(() => { + const style = document.createElement('style'); + if (plugin.definition.css) { + style.innerText = plugin.definition.css; + document.head.appendChild(style); + } + plugin.activate(); return () => { plugin.deactivate(); + if (plugin.definition.css) { + document.head.removeChild(style); + } }; }, [plugin]); diff --git a/desktop/flipper-server-companion/src/HeadlessPluginInitializer.tsx b/desktop/flipper-server-companion/src/HeadlessPluginInitializer.tsx index c0fa6dbe1..170ac6a69 100644 --- a/desktop/flipper-server-companion/src/HeadlessPluginInitializer.tsx +++ b/desktop/flipper-server-companion/src/HeadlessPluginInitializer.tsx @@ -26,15 +26,15 @@ export class HeadlessPluginInitializer extends AbstractPluginInitializer { protected async requirePluginImpl( pluginDetails: ActivatablePluginDetails, ): Promise<_SandyPluginDefinition> { - const plugin = await getRenderHostInstance().requirePlugin( + const requiredPlugin = await getRenderHostInstance().requirePlugin( pluginDetails.entry, ); - if (!plugin) { + if (!requiredPlugin || !requiredPlugin.plugin) { throw new Error( `Failed to obtain plugin source for: ${pluginDetails.name}`, ); } - return new _SandyPluginDefinition(pluginDetails, plugin); + return new _SandyPluginDefinition(pluginDetails, requiredPlugin.plugin); } protected async filterAllLocalVersions( diff --git a/desktop/flipper-server-companion/src/initializeRenderHost.tsx b/desktop/flipper-server-companion/src/initializeRenderHost.tsx index 53d50dfe7..0243004c0 100644 --- a/desktop/flipper-server-companion/src/initializeRenderHost.tsx +++ b/desktop/flipper-server-companion/src/initializeRenderHost.tsx @@ -31,7 +31,7 @@ export function initializeRenderHost( async exportFileBinary() { throw new Error('Not implemented'); }, - openLink(url: string) { + openLink(_url: string) { throw new Error('Not implemented'); }, hasFocus() { @@ -54,22 +54,24 @@ export function initializeRenderHost( return flipperServerConfig.gatekeepers[gatekeeper] ?? false; }, flipperServer, - async requirePlugin(path) { - let source = await flipperServer.exec('plugin-source', path); + async requirePlugin(path): Promise<{plugin: any; css?: string}> { + const source = await flipperServer.exec('plugin-source', path); + + let js = source.js; // append source url (to make sure a file entry shows up in the debugger) - source += `\n//# sourceURL=file://${path}`; + js += `\n//# sourceURL=file://${path}`; // and source map url (to get source code if available) - source += `\n//# sourceMappingURL=file://${path.replace(/.js$/, '.map')}`; + js += `\n//# sourceMappingURL=file://${path.replace(/.js$/, '.map')}`; // Plugins are compiled as typical CJS modules, referring to the global // 'module', which we'll make available by loading the source into a closure that captures 'module'. // Note that we use 'eval', and not 'new Function', because the latter will cause the source maps // to be off by two lines (as the function declaration uses two lines in the generated source) // eslint-disable-next-line no-eval - const cjsLoader = eval('(module) => {' + source + '\n}'); + const cjsLoader = eval('(module) => {' + js + '\n}'); const theModule = {exports: {}}; cjsLoader(theModule); - return theModule.exports; + return {plugin: theModule.exports}; }, getStaticResourceUrl(path): string { // the 'static' folder is mounted as static middleware in Express at the root diff --git a/desktop/flipper-server-core/src/plugins/PluginManager.tsx b/desktop/flipper-server-core/src/plugins/PluginManager.tsx index 06a20d2a8..ac12b5ba5 100644 --- a/desktop/flipper-server-core/src/plugins/PluginManager.tsx +++ b/desktop/flipper-server-core/src/plugins/PluginManager.tsx @@ -17,9 +17,10 @@ import { ExecuteMessage, FlipperServerForServerAddOn, InstalledPluginDetails, + PluginSource, ServerAddOnStartDetails, } from 'flipper-common'; -import {getStaticPath} from '../utils/pathUtils'; + import {loadDynamicPlugins} from './loadDynamicPlugins'; import { cleanupOldInstalledPluginVersions, @@ -72,8 +73,27 @@ export class PluginManager { installPluginFromFile = installPluginFromFile; installPluginFromNpm = installPluginFromNpm; - async loadSource(path: string) { - return await fs.readFile(path, 'utf8'); + async loadSource(path: string): Promise { + const js = await fs.readFile(path, 'utf8'); + + /** + * Check if the plugin includes a bundled css. If so, + * load its content too. + */ + let css = undefined; + const idx = path.lastIndexOf('.'); + const cssPath = path.substring(0, idx < 0 ? path.length : idx) + '.css'; + try { + await fs.promises.access(cssPath); + + const buffer = await fs.promises.readFile(cssPath, {encoding: 'utf-8'}); + css = buffer.toString(); + } catch (e) {} + + return { + js, + css, + }; } async loadMarketplacePlugins() { diff --git a/desktop/flipper-ui-browser/src/initializeRenderHost.tsx b/desktop/flipper-ui-browser/src/initializeRenderHost.tsx index 4aeb4168b..c5b917e24 100644 --- a/desktop/flipper-ui-browser/src/initializeRenderHost.tsx +++ b/desktop/flipper-ui-browser/src/initializeRenderHost.tsx @@ -80,14 +80,15 @@ export function initializeRenderHost( return flipperServerConfig.gatekeepers[gatekeeper] ?? false; }, flipperServer, - async requirePlugin(path) { - let source = await flipperServer.exec('plugin-source', path); + async requirePlugin(path): Promise<{plugin: any; css?: string}> { + const source = await flipperServer.exec('plugin-source', path); + let js = source.js; // append source url (to make sure a file entry shows up in the debugger) - source += `\n//# sourceURL=file://${path}`; + js += `\n//# sourceURL=file://${path}`; if (isProduction()) { // and source map url (to get source code if available) - source += `\n//# sourceMappingURL=file://${path}.map`; + js += `\n//# sourceMappingURL=file://${path}.map`; } // Plugins are compiled as typical CJS modules, referring to the global @@ -95,10 +96,10 @@ export function initializeRenderHost( // Note that we use 'eval', and not 'new Function', because the latter will cause the source maps // to be off by two lines (as the function declaration uses two lines in the generated source) // eslint-disable-next-line no-eval - const cjsLoader = eval('(module) => {' + source + '\n}'); + const cjsLoader = eval('(module) => {' + js + '\n}'); const theModule = {exports: {}}; cjsLoader(theModule); - return theModule.exports; + return {plugin: theModule.exports, css: source.css}; }, getStaticResourceUrl(path): string { // the 'static' folder is mounted as static middleware in Express at the root diff --git a/desktop/flipper-ui-core/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap b/desktop/flipper-ui-core/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap index 339514437..f24efee2a 100644 --- a/desktop/flipper-ui-core/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap +++ b/desktop/flipper-ui-core/src/__tests__/__snapshots__/createMockFlipperWithPlugin.node.tsx.snap @@ -57,6 +57,7 @@ exports[`can create a Fake flipper with legacy wrapper 2`] = ` Object { "clientPlugins": Map { "TestPlugin" => SandyPluginDefinition { + "css": undefined, "details": Object { "dir": "/Users/mock/.flipper/thirdparty/flipper-plugin-sample1", "entry": "./test/index.js", diff --git a/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx b/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx index cd7fde7f5..2884ad1da 100644 --- a/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/__tests__/plugins.node.tsx @@ -79,6 +79,7 @@ test('requirePluginInternal loads plugin', async () => { expect(plugin).not.toBeNull(); expect(Object.keys(plugin as any)).toEqual([ 'id', + 'css', 'details', 'isDevicePlugin', 'module', diff --git a/desktop/flipper-ui-core/src/dispatcher/plugins.tsx b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx index 852594ad5..e0a010c7a 100644 --- a/desktop/flipper-ui-core/src/dispatcher/plugins.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx @@ -131,19 +131,24 @@ export const requirePlugin = (pluginDetails: ActivatablePluginDetails) => export const requirePluginInternal = async ( pluginDetails: ActivatablePluginDetails, ): Promise => { - let plugin = await getRenderHostInstance().requirePlugin( + const requiredPlugin = await getRenderHostInstance().requirePlugin( (pluginDetails as InstalledPluginDetails).entry, ); - if (!plugin) { + if (!requiredPlugin || !requiredPlugin.plugin) { throw new Error( `Failed to obtain plugin source for: ${pluginDetails.name}`, ); } if (isSandyPlugin(pluginDetails)) { // Sandy plugin - return new _SandyPluginDefinition(pluginDetails, plugin); + return new _SandyPluginDefinition( + pluginDetails, + requiredPlugin.plugin, + requiredPlugin.css, + ); } else { - // classic plugin + // Classic plugin + let plugin = requiredPlugin.plugin; if (plugin.default) { plugin = plugin.default; } diff --git a/desktop/plugins/public/package.json b/desktop/plugins/public/package.json index e6473ecc8..133ae4999 100644 --- a/desktop/plugins/public/package.json +++ b/desktop/plugins/public/package.json @@ -7,7 +7,8 @@ "*" ], "nohoist": [ - "flipper-plugin-kaios-big-allocations/**" + "flipper-plugin-kaios-big-allocations/**", + "flipper-plugin-ui-debugger/**" ] }, "bugs": { diff --git a/desktop/plugins/public/ui-debugger/index.tsx b/desktop/plugins/public/ui-debugger/index.tsx index 2bfa99e32..72f3356ea 100644 --- a/desktop/plugins/public/ui-debugger/index.tsx +++ b/desktop/plugins/public/ui-debugger/index.tsx @@ -9,6 +9,7 @@ import {PluginClient, createState, createDataSource} from 'flipper-plugin'; import {Events, Id, PerfStatsEvent, Snapshot, TreeState, UINode} from './types'; +import './node_modules/react-complex-tree/lib/style.css'; export function plugin(client: PluginClient) { const rootId = createState(undefined); diff --git a/desktop/scripts/jest-setup-after.tsx b/desktop/scripts/jest-setup-after.tsx index cdd708565..313e9ce92 100644 --- a/desktop/scripts/jest-setup-after.tsx +++ b/desktop/scripts/jest-setup-after.tsx @@ -190,7 +190,7 @@ function createStubRenderHost(): RenderHost { }, flipperServer: TestUtils.createFlipperServerMock(), async requirePlugin(path: string) { - return require(path); + return {plugin: require(path)}; }, getStaticResourceUrl(relativePath): string { return 'file://' + resolve(rootPath, 'static', relativePath);