Summary: Some plugins import from shared directories. These directories are not plugins themselves, therefore the current plugin root searching mechanism does nto work for them. To support plugin reloading for this scenario, we start re-building all plugins if we fail to find a plugin root. Reviewed By: lblasa Differential Revision: D39693820 fbshipit-source-id: 33dd7de4121bd5665a39b0ea96adce4603dc7df0
445 lines
13 KiB
TypeScript
445 lines
13 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
*/
|
|
|
|
// @ts-ignore
|
|
import Metro from 'metro';
|
|
// provided by Metro
|
|
// @ts-ignore
|
|
// eslint-disable-next-line
|
|
import MetroResolver from 'metro-resolver';
|
|
import tmp from 'tmp';
|
|
import path from 'path';
|
|
import fs from 'fs-extra';
|
|
import {spawn, exec} from 'promisify-child-process';
|
|
import {
|
|
buildDefaultPlugins,
|
|
getDefaultPlugins,
|
|
getWatchFolders,
|
|
stripSourceMapComment,
|
|
} from 'flipper-pkg-lib';
|
|
import getAppWatchFolders from './get-app-watch-folders';
|
|
import {getPluginSourceFolders} from 'flipper-plugin-lib';
|
|
import {
|
|
appDir,
|
|
staticDir,
|
|
defaultPluginsDir,
|
|
babelTransformationsDir,
|
|
serverDir,
|
|
rootDir,
|
|
browserUiDir,
|
|
} from './paths';
|
|
import pFilter from 'p-filter';
|
|
import child from 'child_process';
|
|
|
|
const dev = process.env.NODE_ENV !== 'production';
|
|
|
|
export function die(err: Error) {
|
|
console.error('Script termnated.', err);
|
|
process.exit(1);
|
|
}
|
|
|
|
export async function prepareDefaultPlugins(isInsidersBuild: boolean = false) {
|
|
console.log(
|
|
`⚙️ Preparing default plugins (isInsidersBuild=${isInsidersBuild})...`,
|
|
);
|
|
await fs.emptyDir(defaultPluginsDir);
|
|
const forcedDefaultPluginsDir = process.env.FLIPPER_DEFAULT_PLUGINS_DIR;
|
|
if (forcedDefaultPluginsDir) {
|
|
// Used for internal builds. Sandcastle downloads plugins from the marketplace to preserve their versions if they are not updated.
|
|
console.log(
|
|
`⚙️ Copying the provided default plugins dir "${forcedDefaultPluginsDir}"...`,
|
|
);
|
|
await fs.copy(forcedDefaultPluginsDir, defaultPluginsDir, {
|
|
recursive: true,
|
|
overwrite: true,
|
|
dereference: true,
|
|
});
|
|
console.log('✅ Copied the provided default plugins dir.');
|
|
} else {
|
|
const defaultPlugins = await getDefaultPlugins(isInsidersBuild);
|
|
await buildDefaultPlugins(defaultPlugins, dev);
|
|
}
|
|
console.log('✅ Prepared default plugins.');
|
|
}
|
|
|
|
const minifierConfig = {
|
|
minifierPath: require.resolve('metro-minify-terser'),
|
|
minifierConfig: {
|
|
// see: https://www.npmjs.com/package/terser
|
|
keep_fnames: true,
|
|
module: true,
|
|
warnings: true,
|
|
mangle: false,
|
|
compress: false,
|
|
},
|
|
};
|
|
|
|
async function compile(
|
|
buildFolder: string,
|
|
projectRoot: string,
|
|
watchFolders: string[],
|
|
entry: string,
|
|
) {
|
|
const out = path.join(buildFolder, 'bundle.js');
|
|
await Metro.runBuild(
|
|
{
|
|
reporter: {update: () => {}},
|
|
projectRoot,
|
|
watchFolders,
|
|
serializer: {},
|
|
transformer: {
|
|
babelTransformerPath: path.join(
|
|
babelTransformationsDir,
|
|
'transform-app',
|
|
),
|
|
...minifierConfig,
|
|
},
|
|
resolver: {
|
|
resolverMainFields: ['flipperBundlerEntry', 'module', 'main'],
|
|
blacklistRE: /\.native\.js$/,
|
|
sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json', 'mjs', 'cjs'],
|
|
},
|
|
},
|
|
{
|
|
dev,
|
|
minify: !dev,
|
|
resetCache: !dev,
|
|
sourceMap: true,
|
|
sourceMapUrl: dev ? 'bundle.map' : undefined,
|
|
inlineSourceMap: false,
|
|
entry,
|
|
out,
|
|
},
|
|
);
|
|
if (!dev) {
|
|
stripSourceMapComment(out);
|
|
}
|
|
}
|
|
|
|
export async function compileRenderer(buildFolder: string) {
|
|
console.log(`⚙️ Compiling renderer bundle...`);
|
|
const watchFolders = [
|
|
...(await getAppWatchFolders()),
|
|
...(await getPluginSourceFolders()),
|
|
];
|
|
try {
|
|
await compile(
|
|
buildFolder,
|
|
appDir,
|
|
watchFolders,
|
|
path.join(appDir, 'src', 'init.tsx'),
|
|
);
|
|
console.log('✅ Compiled renderer bundle.');
|
|
} catch (err) {
|
|
die(err);
|
|
}
|
|
}
|
|
|
|
export async function moveSourceMaps(
|
|
buildFolder: string,
|
|
sourceMapFolder: string | undefined,
|
|
) {
|
|
console.log(`⚙️ Moving source maps...`);
|
|
const mainBundleMap = path.join(buildFolder, 'bundle.map');
|
|
const rendererBundleMap = path.join(staticDir, 'main.bundle.map');
|
|
if (sourceMapFolder) {
|
|
await fs.ensureDir(sourceMapFolder);
|
|
await fs.move(mainBundleMap, path.join(sourceMapFolder, 'bundle.map'), {
|
|
overwrite: true,
|
|
});
|
|
await fs.move(
|
|
rendererBundleMap,
|
|
path.join(sourceMapFolder, 'main.bundle.map'),
|
|
{overwrite: true},
|
|
);
|
|
console.log(`✅ Moved to ${sourceMapFolder}.`);
|
|
} else {
|
|
// If we don't move them out of the build folders, they'll get included in the ASAR
|
|
// which we don't want.
|
|
console.log(`⏭ Removing source maps.`);
|
|
await fs.remove(mainBundleMap);
|
|
await fs.remove(rendererBundleMap);
|
|
}
|
|
}
|
|
|
|
export async function moveServerSourceMaps(
|
|
buildFolder: string,
|
|
sourceMapFolder: string | undefined,
|
|
) {
|
|
console.log(`⚙️ Moving server source maps...`);
|
|
const rendererBundleMap = path.join(buildFolder, 'static', 'bundle.map');
|
|
if (sourceMapFolder) {
|
|
await fs.ensureDir(sourceMapFolder);
|
|
// TODO: Remove me
|
|
// Create an empty file not satisfy Sandcastle. Remove it once Sandcastle no longer requires the file
|
|
await fs.writeFile(path.join(sourceMapFolder, 'bundle.map'), '{}');
|
|
await fs.move(
|
|
rendererBundleMap,
|
|
path.join(sourceMapFolder, 'main.bundle.map'),
|
|
{overwrite: true},
|
|
);
|
|
console.log(`✅ Moved to ${sourceMapFolder}.`);
|
|
} else {
|
|
// Removing so we don't bundle them up as part of the release.
|
|
console.log(`⏭ Removing source maps.`);
|
|
await fs.remove(rendererBundleMap);
|
|
}
|
|
}
|
|
|
|
export async function compileMain() {
|
|
const out = path.join(staticDir, 'main.bundle.js');
|
|
process.env.FLIPPER_ELECTRON_VERSION =
|
|
require('electron/package.json').version;
|
|
console.log('⚙️ Compiling main bundle...');
|
|
try {
|
|
const config = Object.assign({}, await Metro.loadConfig(), {
|
|
reporter: {update: () => {}},
|
|
projectRoot: staticDir,
|
|
watchFolders: await getWatchFolders(staticDir),
|
|
transformer: {
|
|
babelTransformerPath: path.join(
|
|
babelTransformationsDir,
|
|
'transform-main',
|
|
),
|
|
...minifierConfig,
|
|
},
|
|
resolver: {
|
|
sourceExts: ['tsx', 'ts', 'js'],
|
|
resolverMainFields: ['flipperBundlerEntry', 'module', 'main'],
|
|
blacklistRE: /\.native\.js$/,
|
|
},
|
|
});
|
|
await Metro.runBuild(config, {
|
|
platform: 'web',
|
|
entry: path.join(staticDir, 'main.tsx'),
|
|
out,
|
|
dev,
|
|
minify: !dev,
|
|
sourceMap: true,
|
|
sourceMapUrl: dev ? 'main.bundle.map' : undefined,
|
|
inlineSourceMap: false,
|
|
resetCache: !dev,
|
|
});
|
|
console.log('✅ Compiled main bundle.');
|
|
if (!dev) {
|
|
stripSourceMapComment(out);
|
|
}
|
|
} catch (err) {
|
|
die(err);
|
|
}
|
|
}
|
|
export function buildFolder(
|
|
prefix: string = 'flipper-build-',
|
|
): Promise<string> {
|
|
// eslint-disable-next-line no-console
|
|
console.log('Creating build directory');
|
|
return new Promise<string>((resolve, reject) => {
|
|
tmp.dir({prefix}, (err, buildFolder) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
resolve(buildFolder);
|
|
}
|
|
});
|
|
}).catch((e) => {
|
|
die(e);
|
|
return '';
|
|
});
|
|
}
|
|
export function getVersionNumber(buildNumber?: number) {
|
|
// eslint-disable-next-line flipper/no-relative-imports-across-packages
|
|
let {version} = require('../package.json');
|
|
if (buildNumber) {
|
|
// Unique build number is passed as --version parameter from Sandcastle
|
|
version = [...version.split('.').slice(0, 2), buildNumber].join('.');
|
|
}
|
|
return version;
|
|
}
|
|
|
|
// Asynchronously determine current mercurial revision as string or `null` in case of any error.
|
|
export function genMercurialRevision(): Promise<string | null> {
|
|
return spawn('hg', ['log', '-r', '.', '-T', '{node}'], {encoding: 'utf8'})
|
|
.then(
|
|
(res) =>
|
|
(res &&
|
|
(typeof res.stdout === 'string'
|
|
? res.stdout
|
|
: res.stdout?.toString())) ||
|
|
null,
|
|
)
|
|
.catch(() => null);
|
|
}
|
|
|
|
export async function compileServerMain(dev: boolean) {
|
|
console.log('⚙️ Compiling server sources...');
|
|
await exec(`cd ${serverDir} && yarn build`);
|
|
console.log('✅ Compiled server sources.');
|
|
}
|
|
|
|
// TODO: needed?
|
|
const uiSourceDirs = [
|
|
'flipper-ui-browser',
|
|
'flipper-ui-core',
|
|
'flipper-plugin',
|
|
'flipper-common',
|
|
];
|
|
|
|
export async function buildBrowserBundle(outDir: string, dev: boolean) {
|
|
console.log('⚙️ Compiling browser bundle...');
|
|
const out = path.join(outDir, 'bundle.js');
|
|
|
|
const electronRequires = path.join(
|
|
babelTransformationsDir,
|
|
'electron-requires',
|
|
);
|
|
const stubModules = new Set<string>(require(electronRequires).BUILTINS);
|
|
if (!stubModules.size) {
|
|
throw new Error('Failed to load list of Node builtins');
|
|
}
|
|
|
|
const watchFolders = await dedupeFolders([
|
|
...(
|
|
await Promise.all(
|
|
uiSourceDirs.map((dir) => getWatchFolders(path.resolve(rootDir, dir))),
|
|
)
|
|
).flat(),
|
|
...(await getPluginSourceFolders()),
|
|
]);
|
|
|
|
const baseConfig = await Metro.loadConfig();
|
|
const config = Object.assign({}, baseConfig, {
|
|
projectRoot: browserUiDir,
|
|
watchFolders,
|
|
transformer: {
|
|
...baseConfig.transformer,
|
|
babelTransformerPath: path.join(
|
|
babelTransformationsDir,
|
|
'transform-browser',
|
|
),
|
|
...(!dev ? minifierConfig : undefined),
|
|
},
|
|
resolver: {
|
|
...baseConfig.resolver,
|
|
resolverMainFields: ['flipperBundlerEntry', 'browser', 'module', 'main'],
|
|
blacklistRE: [/\.native\.js$/],
|
|
sourceExts: ['js', 'jsx', 'ts', 'tsx', 'json', 'mjs', 'cjs'],
|
|
resolveRequest(context: any, moduleName: string, ...rest: any[]) {
|
|
assertSaneImport(context, moduleName);
|
|
// flipper is special cased, for plugins that we bundle,
|
|
// we want to resolve `import from 'flipper'` to 'flipper-ui-core', which
|
|
// defines all the deprecated exports
|
|
if (moduleName === 'flipper') {
|
|
return MetroResolver.resolve(context, 'flipper-ui-core', ...rest);
|
|
}
|
|
// stubbed modules are modules that don't make sense outside a Node / Electron context,
|
|
// like fs, child_process etc etc.
|
|
// UI / plugins using these features should use the corresponding RenderHost api's instead
|
|
// Ideally we'd fail hard on those, but not all plugins are properly converted yet, and some
|
|
// libraries try to require them for feature detection (e.g. jsbase64)
|
|
if (stubModules.has(moduleName)) {
|
|
console.warn(
|
|
`Found a reference to built-in module '${moduleName}', which will be stubbed out. Referer: ${context.originModulePath}`,
|
|
);
|
|
return {
|
|
type: 'empty',
|
|
};
|
|
}
|
|
return defaultResolve(context, moduleName, ...rest);
|
|
},
|
|
},
|
|
});
|
|
await Metro.runBuild(config, {
|
|
platform: 'web',
|
|
entry: path.join(browserUiDir, 'src', 'index.tsx'),
|
|
out,
|
|
dev,
|
|
minify: !dev,
|
|
sourceMap: true,
|
|
sourceMapUrl: dev ? 'index.map' : undefined,
|
|
inlineSourceMap: false,
|
|
});
|
|
console.log('✅ Compiled browser bundle.');
|
|
}
|
|
|
|
async function dedupeFolders(paths: string[]): Promise<string[]> {
|
|
return pFilter(
|
|
paths.filter((value, index, self) => self.indexOf(value) === index),
|
|
(f) => fs.pathExists(f),
|
|
);
|
|
}
|
|
|
|
export function sleep(ms: number) {
|
|
return new Promise((r) => setTimeout(r, ms));
|
|
}
|
|
|
|
let proc: child.ChildProcess | undefined;
|
|
|
|
export async function launchServer(
|
|
startBundler: boolean,
|
|
open: boolean,
|
|
tcp: boolean,
|
|
) {
|
|
if (proc) {
|
|
console.log('⚙️ Killing old flipper-server...');
|
|
proc.kill(9);
|
|
}
|
|
console.log('⚙️ Launching flipper-server...');
|
|
proc = child.spawn(
|
|
'node',
|
|
[
|
|
'--inspect=9229',
|
|
`../flipper-server/server.js`,
|
|
startBundler ? `--bundler` : `--no-bundler`,
|
|
open ? `--open` : `--no-open`,
|
|
tcp ? `--tcp` : `--no-tcp`,
|
|
],
|
|
{
|
|
cwd: serverDir,
|
|
env: {
|
|
...process.env,
|
|
},
|
|
stdio: 'inherit',
|
|
},
|
|
);
|
|
}
|
|
|
|
function assertSaneImport(context: any, moduleName: string) {
|
|
// This function checks that we aren't accidentally bundling up something huge we don't want to
|
|
// bundle up
|
|
if (
|
|
moduleName.startsWith('jest') ||
|
|
(moduleName.startsWith('metro') &&
|
|
!moduleName.startsWith('metro-runtime')) ||
|
|
moduleName === 'Metro' ||
|
|
(moduleName.startsWith('babel') &&
|
|
!moduleName.startsWith('babel-runtime')) ||
|
|
moduleName.startsWith('typescript') ||
|
|
moduleName.startsWith('electron') ||
|
|
moduleName.startsWith('@testing-library')
|
|
) {
|
|
console.error(
|
|
`Found a reference to module '${moduleName}', which should not be imported / required. Referer: ${context.originModulePath}`,
|
|
);
|
|
// throwing errors doesn't really stop Metro :-/
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function defaultResolve(...rest: any[]) {
|
|
const [context, moduleName] = rest;
|
|
return MetroResolver.resolve(
|
|
{
|
|
...context,
|
|
resolveRequest: null,
|
|
},
|
|
moduleName,
|
|
...rest,
|
|
);
|
|
}
|