Skip to content

Commit

Permalink
Add support for building using Metro (#44464)
Browse files Browse the repository at this point in the history
Summary:

Adds `app` to allow building and serving your React Native app in a similar structure to the boostrap and build tasks.  This is the more comprehensive followup to D57067040.

Changelog: [Internal]

Differential Revision: D57067039
  • Loading branch information
blakef authored and facebook-github-bot committed May 7, 2024
1 parent 6e82471 commit 01df30f
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 34 deletions.
2 changes: 1 addition & 1 deletion packages/core-cli-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.75.0-main",
"description": "React Native CLI library for Frameworks to build on",
"license": "MIT",
"main": "./src/index.js",
"main": "./src/index.flow.js",
"repository": {
"type": "git",
"url": "git+https://github.com/facebook/react-native.git",
Expand Down
225 changes: 196 additions & 29 deletions packages/core-cli-utils/src/private/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,59 +13,226 @@ import type {Task} from './types';
import type {ExecaPromise} from 'execa';

import {task} from './utils';
import debug from 'debug';
import execa from 'execa';
import fs from 'fs';
import path from 'path';

type AppOptions = {
const log = debug('core-cli-utils');

type BundlerOptions = {
// Metro's config: https://metrobundler.dev/docs/configuration/
config?: string,
// Typically index.{ios,android}.js
entryFile: string,
+platform: 'ios' | 'android' | string,
dev: boolean,
// Metro built main bundle
outputJsBundle: string,
minify: boolean,
optimize: boolean,
// Generate a source map file
outputSourceMap: string,
// Where to pass the final bundle. Typically this is the App's resource
// folder, however this is app specific. React Native will need to know where
// this is to bootstrap your application. See:
// - Android: https://reactnative.dev/docs/integration-with-existing-apps?language=kotlin#creating-a-release-build-in-android-studio
// - iOS: https://reactnative.dev/docs/integration-with-existing-apps?language=swift#2-event-handler
outputBundle: string,
cwd: string,

target: 'hermes' | 'jsc',
hermes?: HermesConfig,

...Bundler,
};

type BundleOptions =
| {mode: 'bundle', ...AppOptions}
| {mode: 'watch', callback?: (metro: ExecaPromise) => void, ...AppOptions};
type HermesConfig = {
// iOS: Pods/hermes-engine/destroot/bin/hermesc
hermesc: string,
};

type BundlerWatch = {
+mode: 'watch',
callback?: (metro: ExecaPromise) => void,
};

type BundlerBuild = {
+mode: 'bundle',
};

type Bundler = BundlerWatch | BundlerBuild;

const FIRST = 1,
SECOND = 2;
SECOND = 2,
THIRD = 3,
FOURTH = 4;

function getNodePackagePath(packageName: string): string {
// $FlowFixMe[prop-missing] type definition is incomplete
// $FlowIgnore[prop-missing] type definition is incomplete
return require.resolve(packageName, {cwd: [process.cwd(), ...module.paths]});
}

function metro(...args: $ReadOnlyArray<string>): ExecaPromise {
log(
`🚇 ${getNodePackagePath(path.join('metro', 'src', 'cli.js'))} ${args.join(' ')} `,
);
return execa('node', [
getNodePackagePath(path.join('metro', 'src', 'cli.js')),
...args,
]);
}

const noMetro = new Error('Metro is not available');

export const tasks = {
bundle: (
options: BundleOptions,
options: BundlerOptions,
...args: $ReadOnlyArray<string>
): {
validate: Task<void>,
run: Task<ExecaPromise>,
} => ({
/* eslint-disable sort-keys */
validate: task(FIRST, 'Check if Metro is available', () => {
try {
require('metro');
} catch {
throw noMetro;
): Bundle => {
const steps: Bundle = {
/* eslint-disable sort-keys */
validate: task(FIRST, 'Check if Metro is available', () => {
try {
require('metro');
} catch {
throw new Error('Metro is not available');
}
}),
javascript: task(SECOND, 'Metro watching for changes', () =>
metro('serve', ...args),
),
};

return options.mode === 'bundle'
? Object.assign(steps, bundleApp(options, ...args))
: steps;
},
};

type Bundle = {
validate?: Task<void>,
javascript: Task<ExecaPromise>,
sourcemap?: Task<void>,
validateHermesc?: Task<ExecaPromise>,
convert?: Task<ExecaPromise>,
compose?: Task<ExecaPromise>,
};

const bundleApp = (
options: BundlerOptions,
...metroArgs: $ReadOnlyArray<string>
) => {
if (options.outputJsBundle === options.outputBundle) {
throw new Error('outputJsBundle and outputBundle cannot be the same.');
}
// When using Hermes, Metro should generate the JS bundle to an intermediate file
// to then be converted to bytecode in the outputBundle. Otherwise just write to
// the outputBundle directly.
let output =
options.target === 'hermes' ? options.outputJsBundle : options.outputBundle;

// TODO: Fix this by not using Metro CLI, which appends a .js extension
if (output === options.outputJsBundle && !output.endsWith('.js')) {
log(
`Appending .js to outputBundle (because metro cli does if it's missing): ${output}`,
);
output += '.js';
}

const isSourceMaps = options.outputSourceMap != null;
const bundle: Bundle = {
javascript: task(SECOND, 'Metro generating an .jsbundle', () => {
const args = [
'--platform',
options.platform,
'--dev',
options.dev ? 'true' : 'false',
'--reset-cache',
'--out',
output,
];
if (options.target === 'hermes' && !options.dev) {
// Hermes doesn't require JS minification
args.push('--minify', 'false');
} else {
args.push('--minify', options.minify ? 'true' : 'false');
}
if (isSourceMaps) {
args.push('--source-map');
}
return metro('build', options.entryFile, ...args, ...metroArgs);
}),
};

if (options.target === 'jsc') {
return bundle;
}

// $FlowIgnore[incompatible-use] We know it's a Hermes config
const hermesc: string = options.hermes.hermesc;

/*
* Hermes only tasks:
*/
let composeSourceMaps;
if (isSourceMaps) {
bundle.sourcemap = task(
FIRST,
'Check if SourceMap script available',
() => {
composeSourceMaps = getNodePackagePath(
'react-native/scripts/compose-source-maps.js',
);
},
);
}

bundle.validateHermesc = task(FIRST, 'Check if Hermesc is available', () =>
execa(hermesc, ['--version']),
);

bundle.convert = task(
THIRD,
'Hermesc converting .jsbundle bytecode',
() => {
const args = [
'-emit-binary',
'-max-diagnostic-width=80',
options.dev === true ? '-Og' : '-O',
];
if (isSourceMaps) {
args.push('-output-source-map');
}
args.push(`-out=${options.outputBundle}`, output);
return execa(hermesc, args, {cwd: options.cwd});
},
);

bundle.compose = task(FOURTH, 'Compose Hermes and Metro source maps', () => {
if (composeSourceMaps == null) {
throw new Error(
'Unable to find the compose-source-map.js script in react-native',
);
}
const metroSourceMap = output.replace(/(\.js)?$/, '.map');
const hermesSourceMap = options.outputBundle + '.map';
const compose = execa(
'node',
[
composeSourceMaps,
metroSourceMap,
hermesSourceMap,
`-o ${options.outputSourceMap}`,
],
{
cwd: options.cwd,
},
);
compose.finally(() => {
fs.rmSync(metroSourceMap, {force: true});
fs.rmSync(hermesSourceMap, {force: true});
});
return compose;
});

run:
options.mode === 'bundle'
? task(SECOND, 'Metro generating an .jsbundle', () =>
metro('bundle', ...args),
)
: task(SECOND, 'Metro watching for changes', () => {
const proc = metro('serve', ...args);
return proc;
}),
}),
return bundle;
};
9 changes: 6 additions & 3 deletions packages/core-cli-utils/src/private/apple.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ type AppleBuildOptions = {
};

type AppleBootstrapOption = {
// Enabled by default
hermes?: boolean | string,
// Configure what type of VM and how we'd like it built
// - true: build Hermes from source
// - false: use JSC
// - 'Debug' | 'Release': download a prebuilt copy of Hermes
hermes: boolean,
newArchitecture: boolean,
...AppleOptions,
};
Expand Down Expand Up @@ -84,7 +87,7 @@ export const tasks = {
if (options.hermes != null) {
switch (typeof options.hermes) {
case 'string':
env.HERMES = options.hermes;
env.HERMES = '1';
break;
case 'boolean':
env.HERMES = options.hermes ? '1' : '0';
Expand Down
3 changes: 2 additions & 1 deletion packages/helloworld/cli.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,9 @@ bootstrap
.action(async (_, options: {newArchitecture: boolean}) => {
await run(
apple.bootstrap({
newArchitecture: options.newArchitecture,
cwd: cwd.ios,
hermes: true,
newArchitecture: options.newArchitecture,
}),
);
});
Expand Down

0 comments on commit 01df30f

Please sign in to comment.