diff --git a/.changeset/big-wolves-begin.md b/.changeset/big-wolves-begin.md new file mode 100644 index 000000000..319680071 --- /dev/null +++ b/.changeset/big-wolves-begin.md @@ -0,0 +1,5 @@ +--- +"@rnx-kit/config": patch +--- + +Add `HermesOptions` for controlling Hermes bytecode output diff --git a/.changeset/nervous-yaks-hide.md b/.changeset/nervous-yaks-hide.md new file mode 100644 index 000000000..daebf17aa --- /dev/null +++ b/.changeset/nervous-yaks-hide.md @@ -0,0 +1,5 @@ +--- +"@rnx-kit/cli": patch +--- + +Allow Hermes to be run post-bundle diff --git a/packages/cli/README.md b/packages/cli/README.md index 542d87841..61dcc1c2e 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -75,8 +75,9 @@ command-line, they are explicitly set to default values. | Parameter | Default Value | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | -| entryFile | "index.js" | -| bundleOutput | "index.<`platform`>.bundle" (Windows, Android), or "index.<`platform`>.jsbundle" (iOS, macOS) | +| entryFile | `"index.js"` | +| bundleOutput | `"index..bundle"` (Windows, Android) or `"index..jsbundle"` (iOS, macOS) | +| hermes | `false` | | treeShake | `false` | | plugins | `["@rnx-kit/metro-plugin-cyclic-dependencies-detector", "@rnx-kit/metro-plugin-duplicates-checker", "@rnx-kit/metro-plugin-typescript"]` | diff --git a/packages/cli/src/bundle.ts b/packages/cli/src/bundle.ts index 48f0223ba..690035ec9 100644 --- a/packages/cli/src/bundle.ts +++ b/packages/cli/src/bundle.ts @@ -1,6 +1,7 @@ import type { Config as CLIConfig } from "@react-native-community/cli-types"; import { loadMetroConfig } from "@rnx-kit/metro-service"; import { commonBundleCommandOptions } from "./bundle/cliOptions"; +import { emitBytecode } from "./bundle/hermes"; import { getCliPlatformBundleConfigs } from "./bundle/kit-config"; import { metroBundle } from "./bundle/metro"; import { @@ -28,6 +29,7 @@ export async function rnxBundle( applyBundleConfigOverrides(cliOptions, bundleConfigs, [ ...overridableCommonBundleOptions, + "hermes", "treeShake", ]); @@ -38,6 +40,15 @@ export async function rnxBundle( cliOptions.dev, cliOptions.minify ); + + const { bundleOutput, hermes, sourcemapOutput } = bundleConfig; + if (hermes) { + emitBytecode( + bundleOutput, + sourcemapOutput, + hermes === true ? {} : hermes + ); + } } } diff --git a/packages/cli/src/bundle/hermes.ts b/packages/cli/src/bundle/hermes.ts new file mode 100644 index 000000000..923bc8d2a --- /dev/null +++ b/packages/cli/src/bundle/hermes.ts @@ -0,0 +1,104 @@ +import type { HermesOptions } from "@rnx-kit/config"; +import { error, info } from "@rnx-kit/console"; +import { findPackageDependencyDir } from "@rnx-kit/tools-node/package"; +import { spawnSync } from "child_process"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +function hermesBinaryInDir(hermesc: string): string | null { + switch (os.platform()) { + case "darwin": + return path.join(hermesc, "osx-bin", "hermesc"); + case "linux": + return path.join(hermesc, "linux64-bin", "hermesc"); + case "win32": + return path.join(hermesc, "win64-bin", "hermesc.exe"); + default: + return null; + } +} + +function findHermesBinary() { + const locations = [ + () => { + const rnPath = findPackageDependencyDir("react-native"); + if (!rnPath) { + throw new Error("Cannot find module 'react-native'"); + } + return path.join(rnPath, "sdks", "hermesc"); + }, + () => findPackageDependencyDir("hermes-engine"), + ]; + + for (const getLocation of locations) { + const location = getLocation(); + if (location) { + const hermesc = hermesBinaryInDir(location); + if (hermesc && fs.existsSync(hermesc)) { + return hermesc; + } + } + } + + return null; +} + +function getOutput(args: string[]): string | null { + const length = args.length; + for (let i = 0; i < length; ++i) { + const flag = args[i]; + if (flag === "-out") { + return args[i + 1]; + } else if (flag.startsWith("-out=")) { + return flag.substring(5); + } + } + return null; +} + +function isSourceMapFlag(flag: string): boolean { + return flag === "-source-map" || flag.startsWith("-source-map="); +} + +export function emitBytecode( + input: string, + sourcemap: string | undefined, + options: HermesOptions +): void { + const cmd = options.command || findHermesBinary(); + if (!cmd) { + error("No Hermes compiler was found"); + return; + } + + const args = [ + "-emit-binary", + // If Hermes can't detect the width of the terminal, it will set the limit + // to "unlimited". Since we might be passing a minified bundle to Hermes, + // limit output width to avoid issues when it outputs diagnostics. See: + // - https://github.com/microsoft/rnx-kit/issues/2416 + // - https://github.com/microsoft/rnx-kit/issues/2419 + // - https://github.com/microsoft/rnx-kit/issues/2424 + "-max-diagnostic-width=80", + ...(options.flags ?? ["-O", "-output-source-map", "-w"]), + ]; + + let output = getOutput(args); + if (!output) { + output = input + ".hbc"; + args.push("-out", output); + } + + if (sourcemap && !args.some(isSourceMapFlag)) { + args.push("-source-map", sourcemap); + } + + args.push(input); + + info("Emitting bytecode to:", output); + const result = spawnSync(cmd, args, { stdio: "inherit" }); + if (result.status !== 0) { + throw result.error; + } +} diff --git a/packages/cli/src/bundle/overrides.ts b/packages/cli/src/bundle/overrides.ts index e9c2537d1..a6690fd12 100644 --- a/packages/cli/src/bundle/overrides.ts +++ b/packages/cli/src/bundle/overrides.ts @@ -14,6 +14,7 @@ type BundleConfigOverrides = Partial< | "treeShake" | "unstableTransformProfile" | "indexedRamBundle" + | "hermes" > >; diff --git a/packages/config/src/bundleConfig.ts b/packages/config/src/bundleConfig.ts index d3104c8a7..354099882 100644 --- a/packages/config/src/bundleConfig.ts +++ b/packages/config/src/bundleConfig.ts @@ -4,6 +4,20 @@ import type { Options as EsbuildOptions } from "@rnx-kit/metro-serializer-esbuil import type { AllPlatforms } from "@rnx-kit/tools-react-native/platform"; import type { OutputOptions } from "metro/src/shared/types"; +export type HermesOptions = { + /** + * Path to `hermesc` binary. By default, `cli` will try to find it in + * `node_modules`. + */ + command?: string; + + /** + * List of arguments passed to `hermesc`. By default, this is + * `["-O", "-output-source-map", "-w"]`. + */ + flags?: string[]; +}; + export type TypeScriptValidationOptions = { /** * Controls whether an error is thrown when type-validation fails. @@ -105,6 +119,13 @@ export type BundleParameters = BundlerPlugins & { */ treeShake?: boolean | EsbuildOptions; + /** + * Whether to run the Hermes compiler on the output bundle. + * + * Only applies to `rnx-bundle` command. + */ + hermes?: boolean | HermesOptions; + /** * List of plugins to add to the bundling process. * diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 614c19d16..da377262a 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -2,6 +2,7 @@ export type { BundleConfig, BundleParameters, BundlerPlugins, + HermesOptions, TypeScriptValidationOptions, } from "./bundleConfig";