ESM plugin bundles

Summary:
This change will allow us to display correct stack traces in flipper UI as well as send them to scuba.

Currently correct stack traces are only displayed in the console and we do not have access to them.

Reviewed By: ivanmisuno

Differential Revision: D50015827

fbshipit-source-id: 2a60315dd5c06b2635ce0414f612ff1fdca0e489
This commit is contained in:
Anton Kastritskiy
2023-10-10 03:37:21 -07:00
committed by Facebook GitHub Bot
parent 4554e27eca
commit c4a1c90a1e
4 changed files with 71 additions and 20 deletions

View File

@@ -177,6 +177,13 @@ async function startHTTPServer(
res.end('flipper-ok'); res.end('flipper-ok');
}); });
// In dev plugins are served with paths from their location on disk
if (process.env.NODE_ENV !== 'production') {
app.use(
express.static(config.staticPath.split('/').slice(0, -1).join('/')),
);
}
app.use(express.static(config.staticPath)); app.use(express.static(config.staticPath));
const server = http.createServer(app); const server = http.createServer(app);

View File

@@ -177,25 +177,19 @@ export function initializeRenderHost(
}, },
flipperServer, flipperServer,
async requirePlugin(path): Promise<{plugin: any; css?: string}> { async requirePlugin(path): Promise<{plugin: any; css?: string}> {
/** path to bundle.js from project root */
const staticPath = path.includes('/static/')
? path.split('/static/', 2).pop()
: path.split('/desktop/', 2).pop();
// This is a string as server side is transpiled by typescript.
// Typescript transpiles dynamic import calls to `require` in the browser bundle
// We want to explicilty use dynamic import here
const importStr = `import('/${staticPath}?ts=${Date.now()}')`;
const 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)
js += `\n//# sourceURL=file://${path}`;
if (isProduction()) {
// and source map url (to get source code if available)
js += `\n//# sourceMappingURL=file://${path}.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 // eslint-disable-next-line no-eval
const cjsLoader = eval('(module) => {' + js + '\n}'); const importRes = await eval(importStr);
const theModule = {exports: {}};
cjsLoader(theModule); return {plugin: importRes, css: source.css};
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

View File

@@ -122,7 +122,7 @@ export default async (store: Store, _logger: Logger) => {
FlipperPlugin, FlipperPlugin,
Immer, Immer,
antd, antd,
emotion_styled, emotion_styled: emotion_styled.default,
emotion_css, emotion_css,
antdesign_icons, antdesign_icons,
ReactJsxRuntime, ReactJsxRuntime,

View File

@@ -28,6 +28,53 @@ const resolveFbStubsToFbPlugin: Plugin = {
}, },
}; };
const globalFallbackPlugin: Plugin = {
name: 'global-fallback',
setup(build) {
const pluginExternalModules: Record<string, string> = {
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',
'@emotion/css': 'emotion_css',
'@ant-design/icons': 'antdesign_icons',
'react/jsx-runtime': 'ReactJsxRuntime',
};
// Filter and modify the resolution of certain paths
build.onResolve(
{
filter: new RegExp(
`^(${Object.keys(pluginExternalModules).join('|')})$`,
),
},
(args) => {
return {
path: args.path,
namespace: 'global-fallback',
};
},
);
// Load the module using the global fallback
build.onLoad({filter: /.*/, namespace: 'global-fallback'}, (args) => {
const globalName = pluginExternalModules[args.path];
return {
// contents: `export default window["${globalName}"];`,
contents: `const mod = window["${globalName}"];
for (const key in mod) {
exports[key] = mod[key];
}`,
};
});
},
};
interface RunBuildConfig { interface RunBuildConfig {
pluginDir: string; pluginDir: string;
entry: string; entry: string;
@@ -52,7 +99,7 @@ async function runBuild({
bundle: true, bundle: true,
outfile: out, outfile: out,
platform: node ? 'node' : 'browser', platform: node ? 'node' : 'browser',
format: 'cjs', format: 'esm',
// This list should match `dispatcher/plugins.tsx` and `builtInModules` in `desktop/.eslintrc.js` // This list should match `dispatcher/plugins.tsx` and `builtInModules` in `desktop/.eslintrc.js`
external: [ external: [
'flipper', 'flipper',
@@ -73,7 +120,10 @@ async function runBuild({
], ],
sourcemap: dev ? 'inline' : 'external', sourcemap: dev ? 'inline' : 'external',
minify: !dev, minify: !dev,
plugins: intern ? [resolveFbStubsToFbPlugin] : undefined, plugins: [
...(intern ? [resolveFbStubsToFbPlugin] : []),
globalFallbackPlugin,
],
loader: { loader: {
'.ttf': 'dataurl', '.ttf': 'dataurl',
}, },