mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Add support for building using Metro (#44464)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44464 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] Reviewed By: cipolleschi Differential Revision: D57067039 fbshipit-source-id: fdbe891657d826535cb779a4d1b71cfd13921684
This commit is contained in:
committed by
Facebook GitHub Bot
parent
6b56fb0d0b
commit
2fd7733137
@@ -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",
|
||||
|
||||
@@ -13,59 +13,222 @@ 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 {
|
||||
return execa('node', [
|
||||
getNodePackagePath(path.join('metro', 'src', 'cli.js')),
|
||||
...args,
|
||||
]);
|
||||
const metroPath = getNodePackagePath(path.join('metro', 'src', 'cli.js'));
|
||||
log(`🚇 ${metroPath} ${args.join(' ')} `);
|
||||
return execa('node', [metroPath, ...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),
|
||||
),
|
||||
};
|
||||
|
||||
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 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 it 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;
|
||||
});
|
||||
|
||||
return bundle;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user