Bundle headless plugins

Summary: Current temporary limitations: all headless plugins are bundled with Flipper.

Reviewed By: antonk52

Differential Revision: D36098168

fbshipit-source-id: c58abe41776157258a5c39a80a5c1a33cf3f42c5
This commit is contained in:
Andrey Goncharov
2022-05-10 05:13:24 -07:00
committed by Facebook GitHub Bot
parent 1f2f799772
commit a6d7f98cfd
10 changed files with 84 additions and 11 deletions

View File

@@ -18,10 +18,16 @@ const babelOptions = {
filename: 'index.js', filename: 'index.js',
}; };
const babelOptionsPlugin = {
...babelOptions,
root: 'plugins/randomPlugin/',
filename: 'plugins/randomPlugin/index.js',
};
test('transform react requires to global object', () => { test('transform react requires to global object', () => {
const src = 'require("react")'; const src = 'require("react")';
const ast = parse(src); const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions)!.ast; const transformed = transformFromAstSync(ast, src, babelOptionsPlugin)!.ast;
const {code} = generate(transformed!); const {code} = generate(transformed!);
expect(code).toBe('global.React;'); expect(code).toBe('global.React;');
}); });
@@ -29,7 +35,7 @@ test('transform react requires to global object', () => {
test('transform react-dom requires to global object', () => { test('transform react-dom requires to global object', () => {
const src = 'require("react-dom")'; const src = 'require("react-dom")';
const ast = parse(src); const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions)!.ast; const transformed = transformFromAstSync(ast, src, babelOptionsPlugin)!.ast;
const {code} = generate(transformed!); const {code} = generate(transformed!);
expect(code).toBe('global.ReactDOM;'); expect(code).toBe('global.ReactDOM;');
}); });
@@ -37,7 +43,7 @@ test('transform react-dom requires to global object', () => {
test('transform react-dom/client requires to global object', () => { test('transform react-dom/client requires to global object', () => {
const src = 'require("react-dom/client")'; const src = 'require("react-dom/client")';
const ast = parse(src); const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions)!.ast; const transformed = transformFromAstSync(ast, src, babelOptionsPlugin)!.ast;
const {code} = generate(transformed!); const {code} = generate(transformed!);
expect(code).toBe('global.ReactDOMClient;'); expect(code).toBe('global.ReactDOMClient;');
}); });
@@ -45,19 +51,35 @@ test('transform react-dom/client requires to global object', () => {
test('transform flipper requires to global object', () => { test('transform flipper requires to global object', () => {
const src = 'require("flipper")'; const src = 'require("flipper")';
const ast = parse(src); const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions)!.ast; const transformed = transformFromAstSync(ast, src, babelOptionsPlugin)!.ast;
const {code} = generate(transformed!); const {code} = generate(transformed!);
expect(code).toBe('global.Flipper;'); expect(code).toBe('global.Flipper;');
}); });
test('do NOT transform flipper requires outside of plugins folder', () => {
const src = 'require("flipper");';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions)!.ast;
const {code} = generate(transformed!);
expect(code).toBe('require("flipper");');
});
test('transform React identifier to global.React', () => { test('transform React identifier to global.React', () => {
const src = 'React;'; const src = 'React;';
const ast = parse(src); const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions)!.ast; const transformed = transformFromAstSync(ast, src, babelOptionsPlugin)!.ast;
const {code} = generate(transformed!); const {code} = generate(transformed!);
expect(code).toBe('global.React;'); expect(code).toBe('global.React;');
}); });
test('do NOT transform React identifier to global.React outside of plugins folder', () => {
const src = 'React;';
const ast = parse(src);
const transformed = transformFromAstSync(ast, src, babelOptions)!.ast;
const {code} = generate(transformed!);
expect(code).toBe('React;');
});
test('do NOT transform local React namespace import to global.React', () => { test('do NOT transform local React namespace import to global.React', () => {
const src = `import * as React from 'react';`; const src = `import * as React from 'react';`;
const ast = parse(src, {sourceType: 'module'}); const ast = parse(src, {sourceType: 'module'});
@@ -70,7 +92,7 @@ test('throw error when requiring outside the plugin', () => {
const src = 'require("../test.js")'; const src = 'require("../test.js")';
const ast = parse(src); const ast = parse(src);
expect(() => { expect(() => {
transformFromAstSync(ast, src, babelOptions); transformFromAstSync(ast, src, babelOptionsPlugin);
}).toThrow(); }).toThrow();
}); });

View File

@@ -16,9 +16,16 @@ import {
tryReplaceGlobalReactUsage, tryReplaceGlobalReactUsage,
} from './replace-flipper-requires'; } from './replace-flipper-requires';
const sourceRootDir = resolve(__dirname, '../..');
const pluginRootDir = resolve(__dirname, '../../plugin');
// do not apply this transform for these paths // do not apply this transform for these paths
const EXCLUDE_PATHS = ['relay-devtools/DevtoolsUI']; const EXCLUDE_PATHS = ['relay-devtools/DevtoolsUI'];
function isExcludedPath(path: string) { function isExcludedPath(path: string) {
// Replace requires and React for plugins, but not for the Flipper core code which can access bundled React and other Flipper packages
if (path.startsWith(sourceRootDir) && !path.startsWith(pluginRootDir)) {
return true;
}
for (const epath of EXCLUDE_PATHS) { for (const epath of EXCLUDE_PATHS) {
if (path.indexOf(epath) > -1) { if (path.indexOf(epath) > -1) {
return true; return true;

View File

@@ -51,6 +51,7 @@ export function tryReplaceGlobalReactUsage(path: NodePath<Identifier>) {
if ( if (
path.node.name === 'React' && path.node.name === 'React' &&
(path.parentPath.node as any).id !== path.node && (path.parentPath.node as any).id !== path.node &&
path.parent.type !== 'ObjectProperty' &&
!isReactImportIdentifier(path) !isReactImportIdentifier(path)
) { ) {
path.replaceWith(identifier('global.React')); path.replaceWith(identifier('global.React'));

View File

@@ -11,6 +11,7 @@ import {default as doTransform} from './transform';
import {default as getCacheKey} from './get-cache-key'; import {default as getCacheKey} from './get-cache-key';
const presets = [ const presets = [
'@babel/preset-react',
[ [
'@babel/preset-env', '@babel/preset-env',
{ {

View File

@@ -11,6 +11,7 @@ import {default as doTransform} from './transform';
import {default as getCacheKey} from './get-cache-key'; import {default as getCacheKey} from './get-cache-key';
const presets = [ const presets = [
'@babel/preset-react',
[ [
'@babel/preset-env', '@babel/preset-env',
{ {

View File

@@ -16,6 +16,7 @@ import {
genMercurialRevision, genMercurialRevision,
getVersionNumber, getVersionNumber,
prepareDefaultPlugins, prepareDefaultPlugins,
prepareHeadlessPlugins,
} from './build-utils'; } from './build-utils';
import {defaultPluginsDir, distDir, serverDir, staticDir} from './paths'; import {defaultPluginsDir, distDir, serverDir, staticDir} from './paths';
import isFB from './isFB'; import isFB from './isFB';
@@ -330,6 +331,7 @@ async function buildServerRelease() {
await compileServerMain(false); await compileServerMain(false);
await prepareDefaultPlugins(argv.channel === 'insiders'); await prepareDefaultPlugins(argv.channel === 'insiders');
await prepareHeadlessPlugins();
await copyStaticResources(dir, versionNumber); await copyStaticResources(dir, versionNumber);
await downloadIcons(path.join(dir, 'static')); await downloadIcons(path.join(dir, 'static'));
await buildBrowserBundle(path.join(dir, 'static'), false); await buildBrowserBundle(path.join(dir, 'static'), false);

View File

@@ -37,6 +37,7 @@ import {
rootDir, rootDir,
browserUiDir, browserUiDir,
serverCoreDir, serverCoreDir,
serverCompanionDir,
} from './paths'; } from './paths';
import pFilter from 'p-filter'; import pFilter from 'p-filter';
import child from 'child_process'; import child from 'child_process';
@@ -110,6 +111,14 @@ export async function prepareDefaultPlugins(isInsidersBuild: boolean = false) {
console.log('✅ Prepared default plugins.'); console.log('✅ Prepared default plugins.');
} }
export async function prepareHeadlessPlugins() {
console.log(`⚙️ Preparing headless plugins...`);
const sourcePlugins = await getSourcePlugins();
const headlessPlugins = sourcePlugins.filter((p) => p.headless);
await generateHeadlessPluginEntryPoints(headlessPlugins);
console.log('✅ Prepared headless plugins.');
}
function getGeneratedIndex(pluginRequires: string) { function getGeneratedIndex(pluginRequires: string) {
return ` return `
/* eslint-disable */ /* eslint-disable */
@@ -191,6 +200,28 @@ async function generateDefaultPluginEntryPoints(
console.log('✅ Generated bundled plugin entry points.'); console.log('✅ Generated bundled plugin entry points.');
} }
async function generateHeadlessPluginEntryPoints(
headlessPlugins: InstalledPluginDetails[],
) {
console.log(
`⚙️ Generating entry points for ${headlessPlugins.length} headless plugins...`,
);
const headlessRequires = headlessPlugins
.map(
(x) =>
` '${x.name}': tryRequire('${x.name}', () => require('${x.name}'))`,
)
.join(',\n');
const generatedIndexHeadless = getGeneratedIndex(headlessRequires);
await fs.ensureDir(path.join(serverCompanionDir, 'src', 'defaultPlugins'));
await fs.writeFile(
path.join(serverCompanionDir, 'src', 'defaultPlugins', 'index.tsx'),
generatedIndexHeadless,
);
console.log('✅ Generated headless plugin entry points.');
}
async function buildDefaultPlugins(defaultPlugins: InstalledPluginDetails[]) { async function buildDefaultPlugins(defaultPlugins: InstalledPluginDetails[]) {
if (process.env.FLIPPER_NO_REBUILD_PLUGINS) { if (process.env.FLIPPER_NO_REBUILD_PLUGINS) {
console.log( console.log(

View File

@@ -9,9 +9,11 @@
/* eslint-disable flipper/no-console-error-without-context */ /* eslint-disable flipper/no-console-error-without-context */
import {prepareDefaultPlugins} from './build-utils'; import {prepareDefaultPlugins, prepareHeadlessPlugins} from './build-utils';
prepareDefaultPlugins().catch((err) => { Promise.all([prepareDefaultPlugins(), prepareHeadlessPlugins()]).catch(
console.error(err); (err) => {
process.exit(1); console.error(err);
}); process.exit(1);
},
);

View File

@@ -15,6 +15,10 @@ export const browserUiDir = path.join(rootDir, 'flipper-ui-browser');
export const staticDir = path.join(rootDir, 'static'); export const staticDir = path.join(rootDir, 'static');
export const serverDir = path.join(rootDir, 'flipper-server'); export const serverDir = path.join(rootDir, 'flipper-server');
export const serverCoreDir = path.join(rootDir, 'flipper-server-core'); export const serverCoreDir = path.join(rootDir, 'flipper-server-core');
export const serverCompanionDir = path.join(
rootDir,
'flipper-server-companion',
);
export const defaultPluginsDir = path.join(staticDir, 'defaultPlugins'); export const defaultPluginsDir = path.join(staticDir, 'defaultPlugins');
export const pluginsDir = path.join(rootDir, 'plugins'); export const pluginsDir = path.join(rootDir, 'plugins');
export const fbPluginsDir = path.join(pluginsDir, 'fb'); export const fbPluginsDir = path.join(pluginsDir, 'fb');

View File

@@ -14,6 +14,7 @@ import {
compileServerMain, compileServerMain,
launchServer, launchServer,
prepareDefaultPlugins, prepareDefaultPlugins,
prepareHeadlessPlugins,
} from './build-utils'; } from './build-utils';
import Watchman from './watchman'; import Watchman from './watchman';
import isFB from './isFB'; import isFB from './isFB';
@@ -185,6 +186,7 @@ async function startWatchChanges() {
await prepareDefaultPlugins( await prepareDefaultPlugins(
process.env.FLIPPER_RELEASE_CHANNEL === 'insiders', process.env.FLIPPER_RELEASE_CHANNEL === 'insiders',
); );
await prepareHeadlessPlugins();
// watch // watch
await startWatchChanges(); await startWatchChanges();