diff --git a/__docs__/README.md b/__docs__/README.md index bc778b473053..4ff46942ed26 100644 --- a/__docs__/README.md +++ b/__docs__/README.md @@ -78,6 +78,7 @@ TODO: Explain the different components of React Native at a high level. - UI - Events - Mounting + - [Bundler Connection](../packages/react-native/React/DevSupport/__docs__/README.md) - Build system - Android - iOS diff --git a/packages/react-native/React/DevSupport/__docs__/BuildModes.md b/packages/react-native/React/DevSupport/__docs__/BuildModes.md new file mode 100644 index 000000000000..69d59803f03f --- /dev/null +++ b/packages/react-native/React/DevSupport/__docs__/BuildModes.md @@ -0,0 +1,80 @@ +# Build Modes + +[🏠 Home](../../../../../__docs__/README.md) + +Part of [Bundler Connection (Apple platforms)](./README.md). What each build +configuration and compile flag enables. + +| Build | Flags | JavaScript comes from | Dev Menu, reload, Fast Refresh, DevTools | +| ---------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------- | +| Debug, simulator | `RCT_DEV=1`, `RCT_DEV_MENU=1` (from `DEBUG`) | Metro on `localhost`. No embedded bundle unless `FORCE_BUNDLING`. | Yes | +| Debug, device | `RCT_DEV=1`, `RCT_DEV_MENU=1` (from `DEBUG`) | Metro at the development machine's IP; the embedded development bundle without it. | Yes | +| Release | `RCT_DEV=0`, `RCT_DEV_MENU=0` | The embedded production bundle (Hermes bytecode with Hermes). | No | +| Profiling | `RCT_DEV=0`, `REACT_NATIVE_DEBUGGER_MODE_PROD` (β†’ `RCT_REMOTE_PROFILE=1`, `RCT_ENABLE_INSPECTOR=1`) | The embedded production bundle. | React Native DevTools only | + +`RCT_DEV` and `RCT_DEV_MENU` are separate flags so that a build can enable the +Dev Menu independently of the rest of the development tooling. By default +`RCT_DEV_MENU` follows `RCT_DEV`, which follows `DEBUG`; only the builds above +have names. Other combinations are custom builds that define the macros +explicitly: + +| Combination | Flags | Result | +| ------------------------------------ | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Dev Menu without development tooling | `RCT_DEV=0`, `RCT_DEV_MENU=1` | Dev Menu with Reload and Configure Bundler, and the loading banner. Metro is found only through an explicit `RCT_jsLocation`, checked with `/status`. Fast Refresh works when the bundle comes from Metro, because the `/hot` connection is opened from JavaScript and `RCT_DEV` does not gate it. No `/message` connection, DevTools or Cmd+R. | +| Development tooling without Dev Menu | `RCT_DEV=1`, `RCT_DEV_MENU=0`, `RCT_PACKAGER_LOADING_FUNCTIONALITY=1` | Metro loading with host guessing, the `/message` connection, Fast Refresh and DevTools, without the Dev Menu or loading banner. Fast Refresh stays in its saved state (on by default), since the Dev Menu toggle is unavailable. A saved `RCT_jsLocation` is used without checking. `RCT_PACKAGER_LOADING_FUNCTIONALITY` keeps the `/status` checks that host guessing relies on. | + +`scripts/react-native-xcode.sh` produces this during the Xcode build: + +- Debug, device: writes `ip.txt` (unless `SKIP_BUNDLING_METRO_IP`) and embeds a + development bundle (`--dev true`). +- Debug, simulator: embeds nothing unless `FORCE_BUNDLING` is set. +- Release: embeds a production bundle (`--dev false`). +- `SKIP_BUNDLING` skips bundling in any configuration. `ENTRY_FILE`, + `BUNDLE_NAME`, `BUNDLE_CONFIG` and `BUNDLE_COMMAND` customize the bundle + command. + +> **Example: RNTester "Build JS Bundle" phase** +> (`packages/rn-tester/RNTesterPods.xcodeproj`) +> +> ```sh +> export PROJECT_ROOT="$SRCROOT" +> export ENTRY_FILE="$SRCROOT/js/RNTesterApp.ios.js" +> export SOURCEMAP_FILE=../sourcemap.ios.map +> # export FORCE_BUNDLING=true +> +> WITH_ENVIRONMENT="../react-native/scripts/xcode/with-environment.sh" +> REACT_NATIVE_XCODE="../react-native/scripts/react-native-xcode.sh" +> +> /bin/sh -c "$WITH_ENVIRONMENT $REACT_NATIVE_XCODE" +> ``` + +> **Example: HelloWorld build command** (`private/helloworld/cli.flow.js`) +> +> `build ios` passes `SKIP_BUNDLING: 'true'` to `xcodebuild`, so +> `react-native-xcode.sh` does not bundle. A separate `bundle ios` command +> writes `main.jsbundle` into the built app's resources. + +In the native code, the behavior is controlled by flags in +`React/Base/RCTDefines.h`: + +- `RCT_DEV`: `DEBUG`. +- `RCT_DEV_MENU`: `RCT_DEV`. +- `RCT_REMOTE_PROFILE`: `REACT_NATIVE_DEBUGGER_MODE_PROD` if defined (profiling + builds), otherwise `RCT_DEV`. +- `RCT_ENABLE_INSPECTOR`: `RCT_DEV || RCT_REMOTE_PROFILE`. +- `RCT_PACKAGER_LOADING_FUNCTIONALITY`: opt-in, undefined by default. + +| Behavior | Compiled in when | +| ---------------------------------------------------------------------- | ------------------------------------------------------ | +| `/status` checks | `RCT_DEV_MENU` or `RCT_PACKAGER_LOADING_FUNCTIONALITY` | +| Dropping a saved `RCT_jsLocation` where Metro does not answer | `RCT_DEV_MENU` | +| Finding Metro through `ip.txt` or `localhost` | `RCT_DEV` | +| `/message` connection, Cmd+R | `RCT_DEV` | +| `RCTDevSettings` (Fast Refresh setup, Metro commands); stubs otherwise | `RCT_DEV_MENU` or `RCT_REMOTE_PROFILE` | +| Dev Menu, `devMenu` command, loading banner | `RCT_DEV_MENU` | +| DevTools connection, synchronous loading of plain-text file bundles | `RCT_ENABLE_INSPECTOR` | + +Without these flags, `packagerServerHostPort` still returns an explicitly set +`RCT_jsLocation`, unchecked. Apps normally do not call `RCTBundleURLProvider` in +Release, as in the HelloWorld example in +[Bundle URL](./BundleURL.md#choosing-the-bundle). diff --git a/packages/react-native/React/DevSupport/__docs__/BundleLoading.md b/packages/react-native/React/DevSupport/__docs__/BundleLoading.md new file mode 100644 index 000000000000..1ddf586763a1 --- /dev/null +++ b/packages/react-native/React/DevSupport/__docs__/BundleLoading.md @@ -0,0 +1,52 @@ +# Downloading and Running the Bundle + +[🏠 Home](../../../../../__docs__/README.md) + +Part of [Bundler Connection (Apple platforms)](./README.md). How the bundle is +fetched from Metro or read from the app, and what happens when it loads or +fails. + +While loading, the app shows a "Loading from Metro…" banner at the top of the +screen, followed by Metro's bundling progress and then the download progress. If +loading fails, a red error screen (RedBox) shows the reason. When loading from a +file in a Debug build, the banner says "Connect to Metro to develop JavaScript." + +`-[RCTHost start]` takes the URL from the app's provider and creates an +`RCTInstance` (`RCTInstance.mm`, next to `RCTHost.mm`). Once the JavaScript +runtime is ready, `RCTInstance` calls `_loadJSBundle:` with +`bundleManager.bundleURL`: + +1. Creates the `DevSettings` module first. That opens the `/message` connection, + so the `r` command in the Metro terminal still reloads the app when the + download fails. +2. Shows the banner (`RCTDevLoadingView`, `React/CoreModules/`). +3. Calls `-[RCTHost loadBundleAtURL:…]`, which uses the host delegate's loader + if it has one, otherwise `RCTJavaScriptLoader` (`React/Base/`). +4. On error: posts `RCTJavaScriptDidFailToLoadNotification`, shows the RedBox, + and calls `RCTFatal`. +5. On success: runs the bundle, posts `RCTInstanceDidLoadBundle` and + `RCTJavaScriptDidLoadNotification`, and connects Fast Refresh. + +`RCTJavaScriptLoader` handles the two kinds of URL: + +- File (embedded bundle): loaded synchronously when `RCT_ENABLE_INSPECTOR` is + set, otherwise on a background queue. +- Metro: `RCTMultipartDataTask` asks for a streamed response + (`Accept: multipart/mixed`). Metro first sends JSON progress parts (`status`, + `done`, `total`) for the banner, then the bundle itself. Download progress is + reported for `application/javascript` and + `application/x-metro-bytecode-bundle` parts. For the bundle part: + - `X-Http-Status` overrides the HTTP status. A non-200 status becomes an error + built from the response body, which is how Metro's build errors (such as a + syntax error) reach the RedBox. + - `Content-Type` must be `application/javascript` or `text/javascript`. For a + JSON body, its `message` becomes the error. + - `Content-Location`, if present, becomes the source URL used in stack traces. + - The bundle is saved to `NSTemporaryDirectory()/ReactNativeDevBundle.js` and + memory-mapped. The file URL is stored as + `bundleManager.downloadedBundleFileURL`. + - Network errors become "Could not connect to development server", with the + URL. + + If Metro does not answer with a multipart response, the bundle is downloaded + as a single body. diff --git a/packages/react-native/React/DevSupport/__docs__/BundleURL.md b/packages/react-native/React/DevSupport/__docs__/BundleURL.md new file mode 100644 index 000000000000..3b23ddded089 --- /dev/null +++ b/packages/react-native/React/DevSupport/__docs__/BundleURL.md @@ -0,0 +1,207 @@ +# Bundle URL + +[🏠 Home](../../../../../__docs__/README.md) + +Part of [Bundler Connection (Apple platforms)](./README.md). How the app decides +where its JavaScript comes from, how to point it at a specific Metro, and where +the current URL is kept. + +## Choosing the bundle + +The app tells React Native where its JavaScript comes from by implementing +`bundleURL` in the delegate of its `RCTReactNativeFactory`, usually the app's +`AppDelegate` or `SceneDelegate`. The factory passes that method to the +`RCTRootViewFactory` it creates (as the `bundleURLBlock` of +`RCTRootViewFactoryConfiguration`), which hands it to `RCTHost` as the provider +of the bundle URL. Typically, Debug builds ask `RCTBundleURLProvider` for a +Metro URL and Release builds use the embedded bundle. + +> **Example: HelloWorld** +> (`private/helloworld/ios/HelloWorld/SceneDelegate.swift`) +> +> ```swift +> override func bundleURL() -> URL? { +> #if DEBUG +> RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") +> #else +> Bundle.main.url(forResource: "main", withExtension: "jsbundle") +> #endif +> } +> ``` + +The app names only the entry point (`index` for `index.js`). +`RCTBundleURLProvider` looks for a running Metro and turns the entry point into +a full bundle URL. + +> **Example:** entry point `index` on an iPhone connected to Metro at +> `192.168.1.5`: +> +> ```text +> http://192.168.1.5:8081/index.bundle?platform=ios&dev=true&lazy=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&app=com.example.app +> ``` +> +> Without a running Metro, the same entry point resolves to +> `file:///.../main.jsbundle` inside the app. + +If Metro is not running, `jsBundleURLForBundleRoot:` returns the bundle embedded +in the app instead. React Native looks for it as `main.jsbundle`, the name the +build uses by default. If there is no embedded bundle either, loading fails with +"No script URL provided". + +> **Example: RNTester** (`packages/rn-tester/RNTester/SceneDelegate.mm`) +> +> RNTester relies on this fallback instead of an `#if DEBUG` branch. It always +> passes the path of its entry file (`js/RNTesterApp.ios`) to +> `RCTBundleURLProvider`. When no Metro answers, as in Release builds, React +> Native returns the embedded `main.jsbundle` instead of a Metro URL. +> +> ```objc +> static NSString *const kBundlePath = @"js/RNTesterApp.ios"; +> +> - (NSURL *)bundleURL +> { +> return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:kBundlePath]; +> } +> ``` + +The Metro URL is not fixed for the lifetime of the app. React Native calls +`bundleURL` again on every reload, so the result changes when Metro starts or +stops, or when its address is reconfigured (see +[Reloading and switching Metro](./Reloading.md)). + +## Pointing the app at Metro + +| To | Use | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Change the default port | The `RCT_METRO_PORT` build setting (8081 by default). | +| Run on a physical device | No configuration required: Debug device builds record the development machine's IP in `ip.txt` (`scripts/react-native-xcode.sh`). | +| Use a specific Metro for one launch | The launch argument `-RCT_jsLocation host:port`. It is used without checking that Metro answers. | +| Change Metro from inside the app | Dev Menu β†’ Configure Bundler, or `RCTBundleURLProvider.jsLocation`. Saved across launches. | +| Use HTTPS | `RCTBundleURLProvider.packagerScheme`, or `-RCT_packager_scheme https`. | +| Ask Metro for a different kind of bundle | `enableDev`, `enableMinification`, `inlineSourceMap` (defaults `YES`, `NO`, `NO`). | +| Always use a fixed Metro or bundle file | `RCTReactNativeFactory.bundleConfiguration` (`RCTBundleConfiguration`). Overrides `bundleURL`. | +| Load the bundle with custom code | `RCTHostDelegate loadBundleAtURL:onProgress:onComplete:`. | +| Modify the bundle download request | `RCTSetCustomMultipartDataTaskRequestInterceptor`. | +| Send extra HTTP headers to Metro | `RCTDevSupportHttpHeaders`. Applies to status checks, `/message` and DevTools, not the bundle download. | +| Never contact Metro (e.g. in tests) | `RCTBundleURLProviderAllowPackagerServerAccess(NO)`, called before React Native initializes. `RCTBundleURLProvider` then skips Metro, so bundle URLs resolve to the embedded bundle. | + +All `RCT_*` settings of `RCTBundleURLProvider` are stored in `NSUserDefaults`, +so each can also be passed as a launch argument. + +> **Example: Metro port** (`packages/metro-config/src/index.flow.js`) +> +> The default Metro configuration reads the same variable, so setting +> `RCT_METRO_PORT` for both the Xcode build and Metro moves both sides to the +> same port: +> +> ```js +> server: { +> port: Number(process.env.RCT_METRO_PORT) || 8081, +> }, +> ``` + +## How the app finds Metro + +In the simulator, Metro is reachable on `localhost`. On a device, the app uses +the development machine's IP recorded by the build in `ip.txt`. An explicitly +configured address (launch argument or Configure Bundler) takes priority. This +resolution runs again on every reload and whenever the `/message` connection +checks for an address change, so the selected Metro can change while the app +runs. + +`-[RCTBundleURLProvider packagerServerHostPort]` (`React/Base/`) decides: + +1. A `RCT_jsLocation` launch argument is used as is. A saved `RCT_jsLocation` is + used only if `GET /status` answers `packager-status:running`. +2. Otherwise the host from `ip.txt`, or `localhost`, if `/status` answers. +3. Otherwise no Metro, and the app falls back to the embedded bundle. + +A host without a port gets `RCT_METRO_PORT`. The `/status` check blocks the +calling thread for up to 6 s (plus 2 s grace); `isPackagerRunningAsync:` is the +non-blocking variant. + +> **Example:** on a Debug iOS simulator with no saved address, the provider +> sends `GET http://localhost:8081/status`. If the body is +> `packager-status:running`, the bundle URL is +> `http://localhost:8081/index.bundle?platform=ios&...`; otherwise it is +> `file:///.../main.jsbundle`. On a physical device, the address in `ip.txt` +> (for example `192.168.1.5`) is checked instead of `localhost`. + +> **Example: RNTester unit tests** +> (`packages/rn-tester/RNTesterUnitTests/RCTBundleURLProviderTests.m`) +> +> `testLaunchArgumentLocationIsKeptWhenPackagerDoesNotAnswer` and +> `testSavedLocationGivesWayWhenPackagerDoesNotAnswer` cover step 1. With Metro +> not answering, a `RCT_jsLocation` launch argument still produces a Metro URL, +> while the same address saved with `jsLocation` falls back to the embedded +> bundle: +> +> ```objc +> launchArguments[@"RCT_jsLocation"] = @"192.168.1.1:8099"; +> [self replaceLaunchArguments:launchArguments]; +> settings.jsLocation = nil; +> NSURL *URL = [settings jsBundleURLForBundleRoot:testFile]; +> XCTAssertEqualObjects(URL, ipPortBundleURL()); +> ``` + +## What the bundle request asks for + +The bundle URL is `:///.bundle`, where `` is the +entry file (`index` for `index.js`). Its query parameters tell Metro what to +build: + +- `platform=ios`: resolve `.ios.js` files. The value comes from + `RCTPlatformName` and is `ios` on every Apple platform, including tvOS and + macOS. +- `dev` and `lazy` (both follow `enableDev`): development mode, with warnings + and dev tools, and lazy bundling. +- `minify`, `inlineSourceMap`. +- `modulesOnly=false&runModule=true` for the main bundle; + `modulesOnly=true&runModule=false` for split bundles + (`jsBundleURLForSplitBundleRoot:`). +- `app`: the app's bundle identifier. +- `excludeSource=true&sourcePaths=url-server` when React Native DevTools is + enabled, so the source map points at sources on Metro instead of embedding + them. +- Anything added through `additionalOptions` or the `RCTBundleConfiguration` + `packagerOptionsUpdater`. + +Native resources resolve against the same Metro through +`resourceURLForResourceRoot:resourceName:resourceExtension:offlineBundle:`. + +## Where the current URL lives + +The bundle URL is used more than once while the app runs: to download the +bundle, to connect Fast Refresh and React Native DevTools, to open DevTools from +the Dev Menu, and by JavaScript, which sees it as `scriptURL` (exported by +`RCTSourceCode`). Because the URL can change (see [Reloading](./Reloading.md)), +native code reads it from `RCTBundleManager` (`React/Base/`) instead of keeping +a copy. The manager works in one of two mutually exclusive modes. + +### Fixed configuration + +An `RCTBundleConfiguration` set on `RCTReactNativeFactory.bundleConfiguration` +fixes the URL up front. It holds either a bundle file (`bundleFilePath`, which +must be a file URL) or a Metro address (`packagerServerScheme`, +`packagerServerHost` and `bundlePath`, plus a `packagerOptionsUpdater` for extra +query parameters). While it yields a URL, every read returns that URL; the app's +`bundleURL`, reloads and Dev Menu changes do not affect what is loaded. + +> **Example:** an app that ships its own bundle file sets +> `[[RCTBundleConfiguration alloc] initWithBundleFilePath:fileURL]`. React +> Native always loads `fileURL`, never contacts Metro for the bundle, and +> disables the "Loading from Metro…" banner. + +### Dynamic URL + +With the default configuration, `RCTHost` stores the app's provider block and +the current URL, and `RCTBundleManager` exposes them: + +- Getter: the host's current URL. +- Setter: `-[RCTHost _setBundleURL:]`. The new URL is used from the next load; + see [Reloading](./Reloading.md) for how long it lasts. +- `resetBundleURL`: sets the URL to whatever the app's provider returns. + +`_setBundleURL:` also publishes the URL with `RCTReloadCommandSetBundleURL`; it +is attached to every reload notification +(`RCTTriggerReloadCommandNotification`). diff --git a/packages/react-native/React/DevSupport/__docs__/DevToolsConnection.md b/packages/react-native/React/DevSupport/__docs__/DevToolsConnection.md new file mode 100644 index 000000000000..714c65636c33 --- /dev/null +++ b/packages/react-native/React/DevSupport/__docs__/DevToolsConnection.md @@ -0,0 +1,48 @@ +# React Native DevTools (`/inspector/device`) + +[🏠 Home](../../../../../__docs__/README.md) + +Part of [Bundler Connection (Apple platforms)](./README.md). How the app +registers with Metro so React Native DevTools can attach. + +The app registers itself with Metro as a debuggable device, so React Native +DevTools can list it and attach to it. Dev Menu β†’ Open DevTools asks Metro to +open DevTools for this device. While the connection is down, that item reads +"Connect to Metro to debug JavaScript". + +`-[RCTDevSettings initialize]` calls +`+[RCTInspectorDevServerHelper connectWithBundleURL:]` (`React/DevSupport/`), +which opens a WebSocket to: + +```text +:///inspector/device?name=&app=&device=&profiling= +``` + +- The host and scheme come from the bundle URL. The port is the bundle URL's + port, otherwise the `RCT_METRO_PORT` environment variable, otherwise omitted + for `https`, otherwise 8081. +- The device ID is a SHA-256 of the vendor identifier, the bundle identifier and + the debugger mode, so the same app on the same device keeps its ID across + installs. +- Connections are kept in a static map keyed by URL and reused while connected. + `initialize` runs for every new instance, so after a reload that changed the + Metro address, a new connection is opened to the new address. +- Open DevTools (`openDebugger`) sends `POST /open-debugger?device=`. + +> **Example:** an iPhone app with bundle ID `com.example.app` loaded from +> `localhost:8081` connects to +> `http://localhost:8081/inspector/device?name=iPhone&app=com.example.app&device=&profiling=false`, +> and Open DevTools sends +> `POST http://localhost:8081/open-debugger?device=`. + +> [!TIP] +> +> Commands such as reload and debugging travel on different WebSockets +> (`/message` and `/inspector/device`), which are compiled in by different flags +> and don't have to be enabled together. For example, a profiling build has +> React Native DevTools but no `/message` connection, and a build with +> `RCT_DEV_MENU` but without `RCT_DEV` has the Dev Menu's Reload but neither +> connection (see [Build modes](./BuildModes.md)). + +The protocol on this socket is described in +[InspectorPackagerConnection](../../../ReactCommon/jsinspector-modern/__docs__/InspectorPackagerConnection.md). diff --git a/packages/react-native/React/DevSupport/__docs__/FastRefresh.md b/packages/react-native/React/DevSupport/__docs__/FastRefresh.md new file mode 100644 index 000000000000..9a805706f5a5 --- /dev/null +++ b/packages/react-native/React/DevSupport/__docs__/FastRefresh.md @@ -0,0 +1,25 @@ +# Fast Refresh (`/hot`) + +[🏠 Home](../../../../../__docs__/README.md) + +Part of [Bundler Connection (Apple platforms)](./README.md). How Fast Refresh +connects to Metro after the bundle loads. + +When a source file is saved, Metro pushes the changed modules over a WebSocket +to `/hot`, and Fast Refresh applies them without reloading the app. + +After a successful load from Metro, +`-[RCTDevSettings setupHMRClientWithBundleURL:]` calls the JavaScript +`HMRClient.setup(platform, bundlePath, host, port, isEnabled, scheme)` +(`Libraries/Utilities/HMRClient.js`), which opens `:///hot` +through `metro-runtime`. Unlike `/message`, this connection always goes to the +Metro the bundle actually came from. It is set up again after every load, so it +follows a Metro address change after a reload. + +- Dev Menu β†’ Enable/Disable Fast Refresh calls `setHotLoadingEnabled:`. That + saves `hotLoadingEnabled` (on by default) and calls `HMRClient.enable()` / + `HMRClient.disable()`. +- Bundles that JavaScript loads later (`loadBundleFromServer`) are registered + with `HMRClient.registerBundle`, so their modules refresh too. Native code + registers its additional bundles with + `setupHMRClientWithAdditionalBundleURL:`. diff --git a/packages/react-native/React/DevSupport/__docs__/JavaScriptDevTools.md b/packages/react-native/React/DevSupport/__docs__/JavaScriptDevTools.md new file mode 100644 index 000000000000..6384c0aa86d5 --- /dev/null +++ b/packages/react-native/React/DevSupport/__docs__/JavaScriptDevTools.md @@ -0,0 +1,61 @@ +# JavaScript Dev Tools That Use Metro + +[🏠 Home](../../../../../__docs__/README.md) + +Part of [Bundler Connection (Apple platforms)](./README.md). LogBox +symbolication, open in editor, links, split bundles and images. + +LogBox stack traces, "open in editor", and images in a Debug build also talk to +Metro. They find it with `getDevServer()` +(`Libraries/Core/Devtools/getDevServer.js`), which takes the origin of +`scriptURL` and falls back to `http://localhost:8081/`. The origin is cached for +the lifetime of the JavaScript instance; a reload picks up a new Metro address. +Its users: + +- `symbolicateStackTrace`: maps stack traces back to source files + (`POST /symbolicate`). +- `openFileInEditor`: opens a stack frame in the editor + (`POST /open-stack-frame`). +- `openURLInBrowser`: opens a link on the development machine + (`POST /open-url`). +- `loadBundleFromServer`: loads split bundles from Metro. +- `resolveAssetSource`: serves images from Metro when the bundle came from + Metro, otherwise from the app bundle. + +> **Example: who uses `symbolicateStackTrace`.** A component throws a +> `TypeError` while the app runs a bundle from Metro at `localhost:8081`. +> +> 1. LogBox records the error with its raw stack. Each frame points into +> `http://localhost:8081/index.bundle?...` at a line and column of the +> generated bundle. +> 2. LogBox (`Libraries/LogBox/Data/LogBoxData.js`) asks the log to symbolicate +> itself (`LogBoxLog.symbolicate`), which calls +> `LogBoxSymbolication.symbolicate`, which calls +> `symbolicateStackTrace(stack, extraData)`. For a fatal error this happens +> immediately, and LogBox waits up to 1 s for the result before opening the +> error. Warnings and other logs are symbolicated when their notification +> appears or the LogBox inspector opens them. +> 3. `symbolicateStackTrace` sends `POST http://localhost:8081/symbolicate` with +> `{stack, extraData}`. Metro maps each frame through the source map (for +> example to `src/screens/Profile.js:42:17`) and returns a `codeFrame` with +> the surrounding source lines. +> 4. LogBox shows the mapped stack and code frame. Results are cached per stack +> in `LogBoxSymbolication.js`. +> +> If the bundle was not loaded from Metro, `symbolicateStackTrace` throws +> ("Bundle was not loaded from Metro.") instead of sending the request. + +> **Example:** `` in a component at +> the project root, with `img/logo@2x.png` and `img/logo@3x.png`, on a 3x +> iPhone. +> +> - Bundle loaded from Metro at `localhost:8081`: the image loads from +> `http://localhost:8081/assets/img/logo@3x.png?platform=ios&hash=`. +> - Bundle loaded from the embedded `file:///.../MyApp.app/main.jsbundle`: the +> image loads from `file:///.../MyApp.app/assets/img/logo@3x.png`, which +> `react-native-xcode.sh` copied into the app (`--assets-dest`) when it built +> the bundle. +> +> `AssetSourceResolver` (`Libraries/Image/AssetSourceResolver.js`) builds both +> URLs; `pickScale` (`Libraries/Image/AssetUtils.js`) picks the scale suffix for +> the device's pixel ratio. diff --git a/packages/react-native/React/DevSupport/__docs__/PackagerConnection.md b/packages/react-native/React/DevSupport/__docs__/PackagerConnection.md new file mode 100644 index 000000000000..3f4437a9ef72 --- /dev/null +++ b/packages/react-native/React/DevSupport/__docs__/PackagerConnection.md @@ -0,0 +1,48 @@ +# Commands from Metro (`/message`) + +[🏠 Home](../../../../../__docs__/README.md) + +Part of [Bundler Connection (Apple platforms)](./README.md). The WebSocket that +carries reload and Dev Menu commands from Metro. + +In the Metro terminal, `r` reloads the app and `d` opens the Dev Menu. Metro +sends these commands over a WebSocket that the app keeps open to `/message`. +`RCTPackagerConnection` (`React/DevSupport/`) is that connection. + +- Each `RCTDevSettings` instance (`React/CoreModules/`) owns one and opens it in + `initialize`. Every reload creates a new `RCTDevSettings`, and with it a new + connection. +- URL: `://:/message?role=ios` (`role` is `ios` on every + Apple platform, like `platform` in the bundle URL). The host and scheme come + from `bundleConfig.getPackagerServerHost` / `getPackagerServerScheme`: the + `RCTBundleConfiguration` values if set, otherwise `RCTBundleURLProvider` + (`packagerServerHostPort`, `packagerScheme`). Missing parts default to + `localhost`, `http` and `RCT_METRO_PORT`. The host is resolved separately from + the bundle URL, so an app whose bundle URL does not come from + `RCTBundleURLProvider` still connects to the provider's host. +- If Metro restarts, `RCTReconnectingWebSocket` (`Libraries/WebSocket/`, on + SocketRocket) reconnects 2 s after the socket closes or fails. It stops + retrying when the connection is refused; the next reload creates a new + connection. +- The connection follows a new Metro address when + `RCTBundleURLProviderUpdatedNotification` (posted by every + `RCTBundleURLProvider` setter) or `RCTTriggerReloadCommandNotification` + arrives and the `host:port` or scheme changed. These observers run on the main + queue, and resolving the host may perform a synchronous `/status` check. +- Messages are JSON with `version: 2`, `method`, `params` and an optional `id` + (`RCTPackagerClient`): + - Without `id`: a notification, delivered to every handler for `method`. + - With `id`: a request, delivered to the last registered handler, which + replies through `RCTPackagerClientResponder` with + `{version, id, result | error}`. A request without a handler gets an error + reply. +- `RCTDevSettings` handles `reload` (calls `RCTTriggerReloadCommandListeners`) + and `devMenu` (shows the Dev Menu). Native code can add its own commands with + `RCTDevSettings` `addNotificationHandler:` / `addRequestHandler:`, and run + code once connected with `RCTPackagerConnection` `addConnectedHandler:`. + +> **Example:** with Metro on `localhost:8081`, the connection URL is +> `http://localhost:8081/message?role=ios`. SocketRocket opens it as a +> WebSocket. Pressing `r` in the Metro terminal sends a message with +> `"version": 2` and `"method": "reload"` and no `id`, so every `reload` handler +> runs. diff --git a/packages/react-native/React/DevSupport/__docs__/README.md b/packages/react-native/React/DevSupport/__docs__/README.md new file mode 100644 index 000000000000..a6bc5f4de9b4 --- /dev/null +++ b/packages/react-native/React/DevSupport/__docs__/README.md @@ -0,0 +1,135 @@ +# Bundler Connection (Apple platforms) + +[🏠 Home](../../../../../__docs__/README.md) + +When a React Native app on an Apple platform (iOS, tvOS, etc.) is launched, the +native side calculates where to load the JavaScript from: the bundler (Metro) +running on the development machine, or a bundle embedded in the app. In a Debug +build, the app then stays connected to Metro, which enables reloading, Fast +Refresh, opening the Dev Menu from the terminal, and debugging with React Native +DevTools. In a Release build, the app runs the embedded bundle and does not +contact Metro (see [Build modes](./BuildModes.md)). + +The app talks to Metro through two kinds of channels: + +1. **HTTP**: one-shot requests to check whether Metro is running, download the + bundle, and open React Native DevTools. +2. **WebSocket**: long-lived connections for Metro's commands (`/message`), Fast + Refresh (`/hot`), and React Native DevTools (`/inspector/device`). + +The details are split into the documents below. File paths are relative to +`packages/react-native/`; paths in examples are relative to the repository root. + +| Document | Covers | +| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Bundle URL](./BundleURL.md) | How the app chooses where its JavaScript comes from, how to point it at a specific Metro, how Metro is found, what the bundle request asks for, and where the current URL is kept. | +| [Downloading and Running the Bundle](./BundleLoading.md) | Fetching the bundle from Metro or reading it from the app, the loading banner, and load errors. | +| [Reloading and Switching Metro](./Reloading.md) | What a reload restarts, what triggers it, how the URL changes between loads, and Configure Bundler. | +| [Commands from Metro (`/message`)](./PackagerConnection.md) | The WebSocket for reload and Dev Menu commands and its message protocol. | +| [Fast Refresh (`/hot`)](./FastRefresh.md) | How Fast Refresh connects to Metro. | +| [React Native DevTools (`/inspector/device`)](./DevToolsConnection.md) | How the app registers with Metro for debugging. | +| [JavaScript Dev Tools That Use Metro](./JavaScriptDevTools.md) | LogBox symbolication, open in editor, links, split bundles and images. | +| [Build Modes](./BuildModes.md) | What each build configuration and compile flag enables. | + +## πŸš€ Usage + +The app chooses where its JavaScript comes from by implementing `bundleURL` in +the delegate of its `RCTReactNativeFactory`. Typically, Debug builds ask +`RCTBundleURLProvider` for a Metro URL and Release builds use the embedded +`main.jsbundle`. [Bundle URL](./BundleURL.md) describes this, the ways to point +the app at a specific Metro, and how the URL can change. + +## πŸ“ Design + +All bundle loading goes through `RCTHost` +(`ReactCommon/react/runtime/platform/ios/ReactCommon/RCTHost.mm`). When the app +starts, or reloads, React Native: + +1. Connects to Metro for terminal commands and React Native DevTools. +2. Downloads the bundle while showing "Loading from Metro…" with progress. +3. Runs the bundle, which renders the app, and connects Fast Refresh. + +```text +App delegate ── bundleURL (provider block) ──► RCTHost ── RCTBundleManager.bundleURL + β”‚ + RCTInstance._loadJSBundle + β”‚ + 1. moduleForName:"DevSettings" ◄────────── + RCTDevSettings.initialize β”‚ + β”œβ”€ RCTPackagerConnection ──────── WS /message ───────────► Metro (receives reload and devMenu commands) + └─ RCTInspectorDevServerHelper ── WS /inspector/device ──► Metro (connects React Native DevTools) + β”‚ + 2. RCTHost.loadBundleAtURL ◄────────────── + └─ RCTJavaScriptLoader ──────────── GET /.bundle ────► Metro (downloads the bundle) + β”‚ + 3. _loadJSBundle onComplete β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”œβ”€ _loadScriptFromSource (runs the bundle) + β”‚ └─ ReactInstance::loadScript (runs the app's JavaScript) + └─ setupHMRClientWithBundleURL + └─ HMRClient.setup ───────────── WS /hot ───────────────► Metro (applies hot updates) +``` + +Each step is described in its own document: +[Downloading and Running the Bundle](./BundleLoading.md), +[Commands from Metro](./PackagerConnection.md), +[React Native DevTools](./DevToolsConnection.md) and +[Fast Refresh](./FastRefresh.md). + +### Endpoints + +| Endpoint | Client | Purpose | +| -------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GET /status` | `RCTBundleURLProvider` | Is Metro running | +| `GET /.bundle?…` | `RCTJavaScriptLoader` | Bundle download | +| `GET //.` | `RCTBundleURLProvider` resource URLs | Native resources | +| WS `/message?role=ios` | `RCTPackagerConnection` | Reload, Dev Menu, custom | +| WS `/hot` | `HMRClient.js` | Fast Refresh | +| WS `/inspector/device?…` | `RCTCxxInspectorPackagerConnection` | React Native DevTools | +| `POST /open-debugger` | `RCTInspectorDevServerHelper` | Open React Native DevTools | +| `POST /symbolicate` | `symbolicateStackTrace.js` (used by LogBox) | Sends a JavaScript stack trace (`{stack, extraData}`); Metro maps each frame from the bundle back to the original source file, line and column using the source map | +| `POST /open-stack-frame` | `openFileInEditor.js` (used when a LogBox stack frame or code frame is tapped) | Sends `{file, lineNumber}`; Metro opens that file at that line in the editor on the development machine | +| `POST /open-url` | `openURLInBrowser.js` | Sends `{url}`; Metro opens the URL in the browser on the development machine | + +## πŸ”— Relationship with other systems + +### Part of + +- React Native developer tooling on Apple platforms. + +### Part of this + +- `React/Base/RCTBundleURLProvider`: finds Metro, builds bundle and resource + URLs, stores the settings. +- `React/Base/RCTBundleManager`: `RCTBundleConfiguration` and the current bundle + URL shared with native modules. +- `React/Base/RCTJavaScriptLoader`, `RCTMultipartDataTask`, + `RCTMultipartStreamReader`: download and file loading. +- `React/Base/RCTReloadCommand`: reload listeners and the reload notification. +- `React/Base/RCTDevSupportHttpHeaders`: extra headers for Metro requests. +- `React/DevSupport/RCTPackagerConnection`, `RCTPackagerClient`, + `Libraries/WebSocket/RCTReconnectingWebSocket`: the `/message` connection. +- `React/DevSupport/RCTInspectorDevServerHelper`: the DevTools connection and + `/open-debugger`. +- `React/CoreModules/RCTDevSettings`: owns the `/message` connection, starts + Fast Refresh and the DevTools connection. +- `React/CoreModules/RCTDevMenu`: Reload and Configure Bundler. +- `React/CoreModules/RCTDevLoadingView`: the loading banner. +- `Libraries/Utilities/HMRClient.js`, `Libraries/Core/Devtools/getDevServer.js`: + the JavaScript side. +- `scripts/react-native-xcode.sh`: the embedded bundle and `ip.txt`. + +### Used by this + +- Metro and `@react-native/dev-middleware`: serve the endpoints above. See the + [Inspector proxy protocol](../../../../dev-middleware/src/inspector-proxy/__docs__/README.md). +- `jsinspector-modern`: the DevTools socket protocol and flags (`InspectorFlags` + adds the DevTools query parameters). +- SocketRocket: WebSocket client. +- `metro-runtime` `HMRClient`: the `/hot` protocol. + +### Uses this + +- `RCTHost` / `RCTInstance`: get the bundle URL, load the bundle, reload. +- `RCTReactNativeFactory` / `RCTRootViewFactory`: pass the app's bundle URL + provider and `RCTBundleConfiguration` to `RCTHost`. +- JavaScript dev tools: LogBox symbolication, open in editor, image loading. diff --git a/packages/react-native/React/DevSupport/__docs__/Reloading.md b/packages/react-native/React/DevSupport/__docs__/Reloading.md new file mode 100644 index 000000000000..7f360383e2e1 --- /dev/null +++ b/packages/react-native/React/DevSupport/__docs__/Reloading.md @@ -0,0 +1,55 @@ +# Reloading and Switching Metro + +[🏠 Home](../../../../../__docs__/README.md) + +Part of [Bundler Connection (Apple platforms)](./README.md). What a reload does, +what triggers it, and how the bundle URL can change between loads. + +A reload discards the JavaScript instance, asks the app for the bundle URL +again, and loads it from scratch. The native app process keeps running: +`RCTHost` invalidates the JavaScript runtime, creates a new `RCTInstance`, and +restarts the React Native surfaces (root views) already on screen. Reloads are +triggered by: + +- Cmd+R in the simulator and Mac Catalyst. +- Dev Menu β†’ Reload. +- `r` in the Metro terminal (the `reload` message on `/message`). +- `DevSettings.reload()` in JavaScript. +- React Native DevTools. + +These go through `RCTTriggerReloadCommandListeners` +(`React/Base/RCTReloadCommand`) to `-[RCTHost didReceiveReloadCommand]`. React +Native DevTools calls that method directly. + +On reload, `RCTHost` passes the provider's URL to `_setBundleURL:` and creates a +new `RCTInstance`. `_setBundleURL:` replaces the current URL only when the URL +it receives differs from the previous one it received, whether that came from +the provider or from the setter. As a result: + +- Each reload looks for Metro again. An app that started from the embedded + bundle switches to Metro once Metro is running, and back to the embedded + bundle when Metro is gone. +- A URL set with `bundleManager.bundleURL = url` lasts until the provider + returns a different URL. The Dev Menu reloads right after setting a custom + entry point, so the provider's URL replaces that entry point. The host and + port survive because they are saved in `RCT_jsLocation`, which the provider + reads. +- A URL from `RCTBundleConfiguration` overrides both, so neither the provider + nor the setter changes what is loaded. + +Dev Menu β†’ Configure Bundler (`React/CoreModules/RCTDevMenu`) asks for a host, a +port (default `RCT_METRO_PORT`) and an entry point: + +- Apply Changes: saves `host:port` as `jsLocation`. Then it sets + `bundleManager.bundleURL` for the entry point, or calls `resetBundleURL` if + the entry point is empty, and reloads. +- Reset to Default, or Apply Changes with an empty host and port: calls + `resetToDefaults`, sets `bundleManager.bundleURL` to the embedded bundle + (`file:///.../main.jsbundle`), and reloads. `resetToDefaults` restores only + `RCT_enableDev` and `RCT_enableMinification`; a saved `RCT_jsLocation` is + kept. + +> **Example:** entering host `192.168.1.5`, port `8081` and entry point `index` +> saves `192.168.1.5:8081` as `jsLocation`, reconnects the `/message` connection +> to that address, and, if Metro answers there, reloads from +> `http://192.168.1.5:8081/index.bundle?platform=ios&...`.