diff --git a/desktop/flipper-server-core/src/server/startServer.tsx b/desktop/flipper-server-core/src/server/startServer.tsx index b0770e02f..ac46574ee 100644 --- a/desktop/flipper-server-core/src/server/startServer.tsx +++ b/desktop/flipper-server-core/src/server/startServer.tsx @@ -177,6 +177,13 @@ async function startHTTPServer( 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)); const server = http.createServer(app); diff --git a/desktop/flipper-ui-browser/src/initializeRenderHost.tsx b/desktop/flipper-ui-browser/src/initializeRenderHost.tsx index 00ab059e6..9c0f9d043 100644 --- a/desktop/flipper-ui-browser/src/initializeRenderHost.tsx +++ b/desktop/flipper-ui-browser/src/initializeRenderHost.tsx @@ -177,25 +177,19 @@ export function initializeRenderHost( }, flipperServer, 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); - - 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 - const cjsLoader = eval('(module) => {' + js + '\n}'); - const theModule = {exports: {}}; - cjsLoader(theModule); - return {plugin: theModule.exports, css: source.css}; + const importRes = await eval(importStr); + + return {plugin: importRes, 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/dispatcher/plugins.tsx b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx index 49364b099..a2cde1028 100644 --- a/desktop/flipper-ui-core/src/dispatcher/plugins.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/plugins.tsx @@ -122,7 +122,7 @@ export default async (store: Store, _logger: Logger) => { FlipperPlugin, Immer, antd, - emotion_styled, + emotion_styled: emotion_styled.default, emotion_css, antdesign_icons, ReactJsxRuntime, diff --git a/desktop/pkg-lib/src/runBuild.tsx b/desktop/pkg-lib/src/runBuild.tsx index ac07f17f4..5cc7024f1 100644 --- a/desktop/pkg-lib/src/runBuild.tsx +++ b/desktop/pkg-lib/src/runBuild.tsx @@ -28,6 +28,53 @@ const resolveFbStubsToFbPlugin: Plugin = { }, }; +const globalFallbackPlugin: Plugin = { + name: 'global-fallback', + setup(build) { + const pluginExternalModules: Record = { + 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 { pluginDir: string; entry: string; @@ -52,7 +99,7 @@ async function runBuild({ bundle: true, outfile: out, platform: node ? 'node' : 'browser', - format: 'cjs', + format: 'esm', // This list should match `dispatcher/plugins.tsx` and `builtInModules` in `desktop/.eslintrc.js` external: [ 'flipper', @@ -73,7 +120,10 @@ async function runBuild({ ], sourcemap: dev ? 'inline' : 'external', minify: !dev, - plugins: intern ? [resolveFbStubsToFbPlugin] : undefined, + plugins: [ + ...(intern ? [resolveFbStubsToFbPlugin] : []), + globalFallbackPlugin, + ], loader: { '.ttf': 'dataurl', },