From 49525adc730f44aeb26606a2c006e05308600eef Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Tue, 16 May 2023 12:30:58 +0200 Subject: [PATCH] feat(cli): allow Hermes to be run post-bundle --- .changeset/big-wolves-begin.md | 5 ++ .changeset/nervous-yaks-hide.md | 5 ++ packages/cli/README.md | 5 +- packages/cli/src/bundle.ts | 7 +++ packages/cli/src/bundle/hermes.ts | 85 ++++++++++++++++++++++++++++ packages/cli/src/bundle/overrides.ts | 1 + packages/config/src/bundleConfig.ts | 21 +++++++ packages/config/src/index.ts | 1 + 8 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 .changeset/big-wolves-begin.md create mode 100644 .changeset/nervous-yaks-hide.md create mode 100644 packages/cli/src/bundle/hermes.ts diff --git a/.changeset/big-wolves-begin.md b/.changeset/big-wolves-begin.md new file mode 100644 index 0000000000..3196800714 --- /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 0000000000..daebf17aae --- /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 542d878416..61dcc1c2ef 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 48f0223ba0..5b34355948 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,11 @@ 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 0000000000..5ef295e1fb --- /dev/null +++ b/packages/cli/src/bundle/hermes.ts @@ -0,0 +1,85 @@ +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; + } +} + +const findHermesBinary = (() => { + let bin: string; + return () => { + if (!bin) { + 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)) { + bin = hermesc; + break; + } + } + } + } + return bin; + }; +})(); + +export function emitBytecode( + input: string, + sourcemap: string | undefined, + options: HermesOptions +): void { + const cmd = options.command || findHermesBinary(); + if (!cmd || !fs.existsSync(cmd)) { + error("No Hermes compiler was found"); + return; + } + + const output = input + ".hbc"; + + info("Emitting bytecode to:", output); + const args = [ + ...(options.flags ?? ["-O", "-output-source-map", "-w"]), + ...(sourcemap ? ["-source-map", sourcemap] : []), + "-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", + "-out", + output, + input, + ]; + 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 e9c2537d11..a6690fd125 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 d3104c8a79..354099882b 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 614c19d163..da377262a6 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";