/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import Metro from 'metro'; import compilePlugins from '../static/compilePlugins'; import util from 'util'; import tmp from 'tmp'; import path from 'path'; import fs from 'fs-extra'; import {spawn} from 'promisify-child-process'; import recursiveReaddir from 'recursive-readdir'; import getWatchFolders from '../static/get-watch-folders'; import getAppWatchFolders from './get-app-watch-folders'; import { appDir, staticDir, pluginsDir, headlessDir, babelTransformationsDir, } from './paths'; const dev = process.env.NODE_ENV !== 'production'; async function mostRecentlyChanged( dir: string, ignores: string[], ): Promise { const files = await util.promisify( recursiveReaddir, )(dir, ignores); return files .map((f) => fs.lstatSync(f).ctime) .reduce((a, b) => (a > b ? a : b), new Date(0)); } export function die(err: Error) { console.error(err.stack); process.exit(1); } export function compileDefaultPlugins( defaultPluginDir: string, skipAll: boolean = false, ) { return compilePlugins( null, skipAll ? [] : [pluginsDir, path.join(pluginsDir, 'fb')], defaultPluginDir, {force: true, failSilently: false, recompileOnChanges: false}, ) .then((defaultPlugins) => fs.writeFileSync( path.join(defaultPluginDir, 'index.json'), JSON.stringify( defaultPlugins.map(({entry, rootDir, out, ...plugin}) => ({ ...plugin, out: path.parse(out).base, })), ), ), ) .catch(die); } async function compile( buildFolder: string, projectRoot: string, watchFolders: string[], entry: string, ) { await Metro.runBuild( { reporter: {update: () => {}}, projectRoot, watchFolders, serializer: {}, transformer: { babelTransformerPath: path.join( babelTransformationsDir, 'transform-app', ), }, resolver: { resolverMainFields: ['flipper:source', 'module', 'main'], blacklistRE: /\.native\.js$/, }, }, { dev, minify: false, resetCache: !dev, sourceMap: true, entry, out: path.join(buildFolder, 'bundle.js'), }, ); } export async function compileHeadless(buildFolder: string) { console.log(`⚙️ Compiling headless bundle...`); const watchFolders = [ headlessDir, ...(await getWatchFolders(staticDir)), ...(await getAppWatchFolders()), ] .filter((value, index, self) => self.indexOf(value) === index) .filter(fs.pathExistsSync); try { await compile( buildFolder, headlessDir, watchFolders, path.join(headlessDir, 'index.tsx'), ); console.log('✅ Compiled headless bundle.'); } catch (err) { die(err); } } export async function compileRenderer(buildFolder: string) { console.log(`⚙️ Compiling renderer bundle...`); const watchFolders = await getAppWatchFolders(); try { await compile( buildFolder, appDir, watchFolders, path.join(appDir, 'src', 'init.tsx'), ); console.log('✅ Compiled renderer bundle.'); } catch (err) { die(err); } } export async function compileMain() { const out = path.join(staticDir, 'main.bundle.js'); process.env.FLIPPER_ELECTRON_VERSION = require('electron/package.json').version; // check if main needs to be compiled if (await fs.pathExists(out)) { const staticDirCtime = await mostRecentlyChanged(staticDir, ['*.bundle.*']); const bundleCtime = (await fs.lstat(out)).ctime; if (staticDirCtime < bundleCtime) { console.log(`🥫 Using cached version of main bundle...`); return; } } 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', ), }, resolver: { sourceExts: ['tsx', 'ts', 'js'], resolverMainFields: ['flipper:source', 'module', 'main'], blacklistRE: /\.native\.js$/, }, }); await Metro.runBuild(config, { platform: 'web', entry: path.join(staticDir, 'main.ts'), out, dev, minify: false, sourceMap: true, resetCache: !dev, }); console.log('✅ Compiled main bundle.'); } catch (err) { die(err); } } export function buildFolder(): Promise { // eslint-disable-next-line no-console console.log('Creating build directory'); return new Promise((resolve, reject) => { tmp.dir({prefix: 'flipper-build-'}, (err, buildFolder) => { if (err) { reject(err); } else { resolve(buildFolder); } }); }).catch((e) => { die(e); return ''; }); } export function getVersionNumber() { let {version} = require('../package.json'); const buildNumber = process.argv.join(' ').match(/--version=(\d+)/); if (buildNumber && buildNumber.length > 0) { version = [...version.split('.').slice(0, 2), buildNumber[1]].join('.'); } return version; } // Asynchronously determine current mercurial revision as string or `null` in case of any error. export function genMercurialRevision(): Promise { return spawn('hg', ['log', '-r', '.', '-T', '{node}'], {encoding: 'utf8'}) .then( (res) => (res && (typeof res.stdout === 'string' ? res.stdout : res.stdout?.toString())) || null, ) .catch(() => null); }