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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
ff282630be
commit
587f428cf8
@@ -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 (
|
||||
|
||||
@@ -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<InstalledPluginDetails>;
|
||||
'plugin-source': (path: string) => Promise<string>;
|
||||
'plugin-source': (path: string) => Promise<PluginSource>;
|
||||
'plugins-install-from-marketplace': (
|
||||
name: string,
|
||||
) => Promise<InstalledPluginDetails>;
|
||||
|
||||
@@ -142,7 +142,7 @@ export interface RenderHost {
|
||||
GK(gatekeeper: string): boolean;
|
||||
flipperServer: FlipperServer;
|
||||
serverConfig: FlipperServerConfig;
|
||||
requirePlugin(path: string): Promise<any>;
|
||||
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;
|
||||
|
||||
@@ -55,15 +55,18 @@ export type FlipperPluginModule<
|
||||
export class SandyPluginDefinition {
|
||||
id: string;
|
||||
module: FlipperPluginModule<any> | FlipperDevicePluginModule;
|
||||
css?: string;
|
||||
details: ActivatablePluginDetails;
|
||||
isDevicePlugin: boolean;
|
||||
|
||||
constructor(
|
||||
details: ActivatablePluginDetails,
|
||||
module: FlipperPluginModule<any> | 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' ||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PluginSource> {
|
||||
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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -131,19 +131,24 @@ export const requirePlugin = (pluginDetails: ActivatablePluginDetails) =>
|
||||
export const requirePluginInternal = async (
|
||||
pluginDetails: ActivatablePluginDetails,
|
||||
): Promise<PluginDefinition> => {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"*"
|
||||
],
|
||||
"nohoist": [
|
||||
"flipper-plugin-kaios-big-allocations/**"
|
||||
"flipper-plugin-kaios-big-allocations/**",
|
||||
"flipper-plugin-ui-debugger/**"
|
||||
]
|
||||
},
|
||||
"bugs": {
|
||||
|
||||
@@ -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<Events>) {
|
||||
const rootId = createState<Id | undefined>(undefined);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user