Files
flipper/desktop/scripts/build-utils.tsx
Anton Kastritskiy f2ef26cd9a bump fs-extra types
Summary: `recursive` was dropped in when [dropping](https://github.com/jprichardson/node-fs-extra/issues/886) node v10

Reviewed By: aigoncharov

Differential Revision: D48780242

fbshipit-source-id: 29349590a7f14da85fe8df28b20d9b418e7a8b1d
2023-09-05 07:17:06 -07:00

443 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';
import isFB from './isFB';
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, {
overwrite: true,
dereference: true,
});
console.log('✅ Copied the provided default plugins dir.');
} else {
const defaultPlugins = await getDefaultPlugins(isInsidersBuild);
await buildDefaultPlugins(
defaultPlugins,
dev,
isFB && !process.env.FLIPPER_FORCE_PUBLIC_BUILD,
);
}
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) {
await 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 Promise.all([fs.remove(mainBundleMap), 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) {
await 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) {
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`,
],
{
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,
);
}