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;
|
return flipperServerConfig.gatekeepers[gatekeeper] ?? false;
|
||||||
},
|
},
|
||||||
flipperServer,
|
flipperServer,
|
||||||
async requirePlugin(path) {
|
async requirePlugin(path): Promise<{plugin: any; css?: string}> {
|
||||||
return electronRequire(path);
|
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 {
|
getStaticResourceUrl(relativePath): string {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ export interface DeviceDebugData {
|
|||||||
data: (DeviceDebugFile | DeviceDebugCommand)[];
|
data: (DeviceDebugFile | DeviceDebugCommand)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PluginSource {
|
||||||
|
js: string;
|
||||||
|
css?: string;
|
||||||
|
}
|
||||||
export type FlipperServerCommands = {
|
export type FlipperServerCommands = {
|
||||||
'get-server-state': () => Promise<{
|
'get-server-state': () => Promise<{
|
||||||
state: FlipperServerState;
|
state: FlipperServerState;
|
||||||
@@ -275,7 +279,7 @@ export type FlipperServerCommands = {
|
|||||||
'plugin-start-download': (
|
'plugin-start-download': (
|
||||||
plugin: DownloadablePluginDetails,
|
plugin: DownloadablePluginDetails,
|
||||||
) => Promise<InstalledPluginDetails>;
|
) => Promise<InstalledPluginDetails>;
|
||||||
'plugin-source': (path: string) => Promise<string>;
|
'plugin-source': (path: string) => Promise<PluginSource>;
|
||||||
'plugins-install-from-marketplace': (
|
'plugins-install-from-marketplace': (
|
||||||
name: string,
|
name: string,
|
||||||
) => Promise<InstalledPluginDetails>;
|
) => Promise<InstalledPluginDetails>;
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export interface RenderHost {
|
|||||||
GK(gatekeeper: string): boolean;
|
GK(gatekeeper: string): boolean;
|
||||||
flipperServer: FlipperServer;
|
flipperServer: FlipperServer;
|
||||||
serverConfig: FlipperServerConfig;
|
serverConfig: FlipperServerConfig;
|
||||||
requirePlugin(path: string): Promise<any>;
|
requirePlugin(path: string): Promise<{plugin: any; css?: string}>;
|
||||||
getStaticResourceUrl(relativePath: string): string;
|
getStaticResourceUrl(relativePath: string): string;
|
||||||
// given the requested icon and proposed public url of the icon, rewrite it to a local icon if needed
|
// 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;
|
getLocalIconUrl?(icon: Icon, publicUrl: string): string;
|
||||||
|
|||||||
@@ -55,15 +55,18 @@ export type FlipperPluginModule<
|
|||||||
export class SandyPluginDefinition {
|
export class SandyPluginDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
module: FlipperPluginModule<any> | FlipperDevicePluginModule;
|
module: FlipperPluginModule<any> | FlipperDevicePluginModule;
|
||||||
|
css?: string;
|
||||||
details: ActivatablePluginDetails;
|
details: ActivatablePluginDetails;
|
||||||
isDevicePlugin: boolean;
|
isDevicePlugin: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
details: ActivatablePluginDetails,
|
details: ActivatablePluginDetails,
|
||||||
module: FlipperPluginModule<any> | FlipperDevicePluginModule,
|
module: FlipperPluginModule<any> | FlipperDevicePluginModule,
|
||||||
|
css?: string,
|
||||||
);
|
);
|
||||||
constructor(details: ActivatablePluginDetails, module: any) {
|
constructor(details: ActivatablePluginDetails, module: any, css?: string) {
|
||||||
this.id = details.id;
|
this.id = details.id;
|
||||||
|
this.css = css;
|
||||||
this.details = details;
|
this.details = details;
|
||||||
if (
|
if (
|
||||||
details.pluginType === 'device' ||
|
details.pluginType === 'device' ||
|
||||||
|
|||||||
@@ -28,9 +28,18 @@ export const SandyPluginRenderer = memo(({plugin}: Props) => {
|
|||||||
throw new Error('Expected plugin, got ' + plugin);
|
throw new Error('Expected plugin, got ' + plugin);
|
||||||
}
|
}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
if (plugin.definition.css) {
|
||||||
|
style.innerText = plugin.definition.css;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
plugin.activate();
|
plugin.activate();
|
||||||
return () => {
|
return () => {
|
||||||
plugin.deactivate();
|
plugin.deactivate();
|
||||||
|
if (plugin.definition.css) {
|
||||||
|
document.head.removeChild(style);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [plugin]);
|
}, [plugin]);
|
||||||
|
|
||||||
|
|||||||
@@ -26,15 +26,15 @@ export class HeadlessPluginInitializer extends AbstractPluginInitializer {
|
|||||||
protected async requirePluginImpl(
|
protected async requirePluginImpl(
|
||||||
pluginDetails: ActivatablePluginDetails,
|
pluginDetails: ActivatablePluginDetails,
|
||||||
): Promise<_SandyPluginDefinition> {
|
): Promise<_SandyPluginDefinition> {
|
||||||
const plugin = await getRenderHostInstance().requirePlugin(
|
const requiredPlugin = await getRenderHostInstance().requirePlugin(
|
||||||
pluginDetails.entry,
|
pluginDetails.entry,
|
||||||
);
|
);
|
||||||
if (!plugin) {
|
if (!requiredPlugin || !requiredPlugin.plugin) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to obtain plugin source for: ${pluginDetails.name}`,
|
`Failed to obtain plugin source for: ${pluginDetails.name}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return new _SandyPluginDefinition(pluginDetails, plugin);
|
return new _SandyPluginDefinition(pluginDetails, requiredPlugin.plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async filterAllLocalVersions(
|
protected async filterAllLocalVersions(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function initializeRenderHost(
|
|||||||
async exportFileBinary() {
|
async exportFileBinary() {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
},
|
},
|
||||||
openLink(url: string) {
|
openLink(_url: string) {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
},
|
},
|
||||||
hasFocus() {
|
hasFocus() {
|
||||||
@@ -54,22 +54,24 @@ export function initializeRenderHost(
|
|||||||
return flipperServerConfig.gatekeepers[gatekeeper] ?? false;
|
return flipperServerConfig.gatekeepers[gatekeeper] ?? false;
|
||||||
},
|
},
|
||||||
flipperServer,
|
flipperServer,
|
||||||
async requirePlugin(path) {
|
async requirePlugin(path): Promise<{plugin: any; css?: string}> {
|
||||||
let source = await flipperServer.exec('plugin-source', path);
|
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)
|
// 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)
|
// 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
|
// 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'.
|
// '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
|
// 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)
|
// to be off by two lines (as the function declaration uses two lines in the generated source)
|
||||||
// eslint-disable-next-line no-eval
|
// eslint-disable-next-line no-eval
|
||||||
const cjsLoader = eval('(module) => {' + source + '\n}');
|
const cjsLoader = eval('(module) => {' + js + '\n}');
|
||||||
const theModule = {exports: {}};
|
const theModule = {exports: {}};
|
||||||
cjsLoader(theModule);
|
cjsLoader(theModule);
|
||||||
return theModule.exports;
|
return {plugin: theModule.exports};
|
||||||
},
|
},
|
||||||
getStaticResourceUrl(path): string {
|
getStaticResourceUrl(path): string {
|
||||||
// the 'static' folder is mounted as static middleware in Express at the root
|
// the 'static' folder is mounted as static middleware in Express at the root
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import {
|
|||||||
ExecuteMessage,
|
ExecuteMessage,
|
||||||
FlipperServerForServerAddOn,
|
FlipperServerForServerAddOn,
|
||||||
InstalledPluginDetails,
|
InstalledPluginDetails,
|
||||||
|
PluginSource,
|
||||||
ServerAddOnStartDetails,
|
ServerAddOnStartDetails,
|
||||||
} from 'flipper-common';
|
} from 'flipper-common';
|
||||||
import {getStaticPath} from '../utils/pathUtils';
|
|
||||||
import {loadDynamicPlugins} from './loadDynamicPlugins';
|
import {loadDynamicPlugins} from './loadDynamicPlugins';
|
||||||
import {
|
import {
|
||||||
cleanupOldInstalledPluginVersions,
|
cleanupOldInstalledPluginVersions,
|
||||||
@@ -72,8 +73,27 @@ export class PluginManager {
|
|||||||
installPluginFromFile = installPluginFromFile;
|
installPluginFromFile = installPluginFromFile;
|
||||||
installPluginFromNpm = installPluginFromNpm;
|
installPluginFromNpm = installPluginFromNpm;
|
||||||
|
|
||||||
async loadSource(path: string) {
|
async loadSource(path: string): Promise<PluginSource> {
|
||||||
return await fs.readFile(path, 'utf8');
|
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() {
|
async loadMarketplacePlugins() {
|
||||||
|
|||||||
@@ -80,14 +80,15 @@ export function initializeRenderHost(
|
|||||||
return flipperServerConfig.gatekeepers[gatekeeper] ?? false;
|
return flipperServerConfig.gatekeepers[gatekeeper] ?? false;
|
||||||
},
|
},
|
||||||
flipperServer,
|
flipperServer,
|
||||||
async requirePlugin(path) {
|
async requirePlugin(path): Promise<{plugin: any; css?: string}> {
|
||||||
let source = await flipperServer.exec('plugin-source', path);
|
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)
|
// 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()) {
|
if (isProduction()) {
|
||||||
// and source map url (to get source code if available)
|
// 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
|
// 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
|
// 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)
|
// to be off by two lines (as the function declaration uses two lines in the generated source)
|
||||||
// eslint-disable-next-line no-eval
|
// eslint-disable-next-line no-eval
|
||||||
const cjsLoader = eval('(module) => {' + source + '\n}');
|
const cjsLoader = eval('(module) => {' + js + '\n}');
|
||||||
const theModule = {exports: {}};
|
const theModule = {exports: {}};
|
||||||
cjsLoader(theModule);
|
cjsLoader(theModule);
|
||||||
return theModule.exports;
|
return {plugin: theModule.exports, css: source.css};
|
||||||
},
|
},
|
||||||
getStaticResourceUrl(path): string {
|
getStaticResourceUrl(path): string {
|
||||||
// the 'static' folder is mounted as static middleware in Express at the root
|
// 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 {
|
Object {
|
||||||
"clientPlugins": Map {
|
"clientPlugins": Map {
|
||||||
"TestPlugin" => SandyPluginDefinition {
|
"TestPlugin" => SandyPluginDefinition {
|
||||||
|
"css": undefined,
|
||||||
"details": Object {
|
"details": Object {
|
||||||
"dir": "/Users/mock/.flipper/thirdparty/flipper-plugin-sample1",
|
"dir": "/Users/mock/.flipper/thirdparty/flipper-plugin-sample1",
|
||||||
"entry": "./test/index.js",
|
"entry": "./test/index.js",
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ test('requirePluginInternal loads plugin', async () => {
|
|||||||
expect(plugin).not.toBeNull();
|
expect(plugin).not.toBeNull();
|
||||||
expect(Object.keys(plugin as any)).toEqual([
|
expect(Object.keys(plugin as any)).toEqual([
|
||||||
'id',
|
'id',
|
||||||
|
'css',
|
||||||
'details',
|
'details',
|
||||||
'isDevicePlugin',
|
'isDevicePlugin',
|
||||||
'module',
|
'module',
|
||||||
|
|||||||
@@ -131,19 +131,24 @@ export const requirePlugin = (pluginDetails: ActivatablePluginDetails) =>
|
|||||||
export const requirePluginInternal = async (
|
export const requirePluginInternal = async (
|
||||||
pluginDetails: ActivatablePluginDetails,
|
pluginDetails: ActivatablePluginDetails,
|
||||||
): Promise<PluginDefinition> => {
|
): Promise<PluginDefinition> => {
|
||||||
let plugin = await getRenderHostInstance().requirePlugin(
|
const requiredPlugin = await getRenderHostInstance().requirePlugin(
|
||||||
(pluginDetails as InstalledPluginDetails).entry,
|
(pluginDetails as InstalledPluginDetails).entry,
|
||||||
);
|
);
|
||||||
if (!plugin) {
|
if (!requiredPlugin || !requiredPlugin.plugin) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to obtain plugin source for: ${pluginDetails.name}`,
|
`Failed to obtain plugin source for: ${pluginDetails.name}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isSandyPlugin(pluginDetails)) {
|
if (isSandyPlugin(pluginDetails)) {
|
||||||
// Sandy plugin
|
// Sandy plugin
|
||||||
return new _SandyPluginDefinition(pluginDetails, plugin);
|
return new _SandyPluginDefinition(
|
||||||
|
pluginDetails,
|
||||||
|
requiredPlugin.plugin,
|
||||||
|
requiredPlugin.css,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// classic plugin
|
// Classic plugin
|
||||||
|
let plugin = requiredPlugin.plugin;
|
||||||
if (plugin.default) {
|
if (plugin.default) {
|
||||||
plugin = plugin.default;
|
plugin = plugin.default;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"*"
|
"*"
|
||||||
],
|
],
|
||||||
"nohoist": [
|
"nohoist": [
|
||||||
"flipper-plugin-kaios-big-allocations/**"
|
"flipper-plugin-kaios-big-allocations/**",
|
||||||
|
"flipper-plugin-ui-debugger/**"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import {PluginClient, createState, createDataSource} from 'flipper-plugin';
|
import {PluginClient, createState, createDataSource} from 'flipper-plugin';
|
||||||
import {Events, Id, PerfStatsEvent, Snapshot, TreeState, UINode} from './types';
|
import {Events, Id, PerfStatsEvent, Snapshot, TreeState, UINode} from './types';
|
||||||
|
import './node_modules/react-complex-tree/lib/style.css';
|
||||||
|
|
||||||
export function plugin(client: PluginClient<Events>) {
|
export function plugin(client: PluginClient<Events>) {
|
||||||
const rootId = createState<Id | undefined>(undefined);
|
const rootId = createState<Id | undefined>(undefined);
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ function createStubRenderHost(): RenderHost {
|
|||||||
},
|
},
|
||||||
flipperServer: TestUtils.createFlipperServerMock(),
|
flipperServer: TestUtils.createFlipperServerMock(),
|
||||||
async requirePlugin(path: string) {
|
async requirePlugin(path: string) {
|
||||||
return require(path);
|
return {plugin: require(path)};
|
||||||
},
|
},
|
||||||
getStaticResourceUrl(relativePath): string {
|
getStaticResourceUrl(relativePath): string {
|
||||||
return 'file://' + resolve(rootPath, 'static', relativePath);
|
return 'file://' + resolve(rootPath, 'static', relativePath);
|
||||||
|
|||||||
Reference in New Issue
Block a user