Skip to content

Wait for bursts of external writes before writing the Terminal status - #2170

Merged
vzaidman merged 3 commits into
react:mainfrom
vzaidman:vzaidman/terminal-external-write-bursts
Nov 6, 2026
Merged

vzaidman merged 3 commits into
react:mainfrom
vzaidman:vzaidman/terminal-external-write-bursts

Conversation

@vzaidman

@vzaidman vzaidman commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

When output is written outside of Terminal (e.g. console.log) in a burst, a status update can land between the burst's lines. In a TTY it can't be erased there, so copies of the status are left scattered in the output.

  • Before writing the status, Terminal waits until no output is written outside of it for 333ms (EXTERNAL_WRITES_QUIET_MS), in interactive and non-interactive terminals, for at most 2000ms (EXTERNAL_WRITES_MAX_WAIT_MS), so sustained output doesn't hide the status. Outside output is detected through the stream's bytesWritten, counted separately from Terminal's own writes. Without outside output, the status is written synchronously. flush() skips the wait.
  • Status writes wait on a delay promise set after each write: 33ms in a TTY, 3500ms otherwise. flush() ends the delay early.
  • Status delay timers are unreferenced, so they don't keep the process alive. An ongoing status update is flushed on beforeExit, so the latest status is still written and waitForUpdates() resolves.
  • lodash.throttle is removed from metro-core and metro:
    • Terminal limits non-TTY status lines through the same status delay.
    • TerminalReporter updates bundle progress state on each bundle_transform_progressed event, and updates the status at most every 100ms (PROGRESS_STATUS_INTERVAL_MS). The bundle_transform_progressed_throttled event type is removed from TerminalReportableEvent. TerminalReporter subclasses that handled it should handle bundle_transform_progressed instead, and their _log overrides now run on every progress event (e.g. Expo CLI's MetroTerminalReporter).
  • Terminal.log() writes an erased status again below the line in the same write, so the status stays on the screen, unless it is cleared. A console.log right after a Terminal.log() leaves a copy of the restored status.
  • The status is erased with ANSI escape sequences written through Terminal's own write, so every write tracks its completion and the bytes written after it. node:readline is no longer used.
  • isUpdating and hasPendingUpdate are removed: an update is ongoing while #updatePromise is set, and pending while the written status differs from the latest one.

The performance impact was measured with the benchmark in #2171, which builds a generated app and supports a base patch applied to every variant (results below).

Changelog: [Breaking] Terminal no longer prints the status in the middle of bursts of output written outside of it, and bundle_transform_progressed_throttled is removed from TerminalReportableEvent; TerminalReporter subclasses should handle bundle_transform_progressed instead

Test plan

  • New test "writes the status after a burst of external writes" fails before this change, with a 33ms quiet period, and without non-TTY outside output detection, and passes after.
  • New test "flush does not wait for the next status update" fails when flush() doesn't end the status delay.
  • New test "flushes the pending status before the process exits" fails without the beforeExit listener.
  • New test "writes the status during sustained external writes" fails without the 2000ms cap.
  • New test "doesn't count Terminal's own logs as external writes" fails when the wait compares the raw byte count.
  • New TerminalReporter test "updates the bundle transform progress status at most every 100ms" fails without the limit.
  • New test "keeps the status on the screen when logging" fails without restoring the status in log(). "keeps logs in order with output written outside of Terminal" expects the restored status copy before the outside line.
  • Terminal-test.js uses fake timers.
  • yarn jest packages/metro-core packages/metro/src/lib
  • yarn flow check, yarn eslint packages/metro-core/src packages/metro/src/lib, yarn verify-api-snapshots

Benchmark

node scripts/benchmark-patches/benchmark-patches.js bundling_speed --runs=10, with a base.patch that makes metro build log 3 lines per transformed module (30,000 lines per build, interleaved with progress updates), and this PR's Terminal.js and TerminalReporter.js changes as the variant patch. Measured on e7abf93, with this PR's files as of afd65a3, before the 100ms progress limit and the status restore in log(), which were measured in this review report:

Bundle Variant Wall (s) Wall Δ CPU (s) CPU Δ Size (bytes) Same as unpatched
app-prod unpatched 8.4 ± 1.1 69.0 ± 4.9 600,199 yes
app-prod terminal-external-write-bursts 5.6 ± 0.2 -33.41% (t -8.1) 67.0 ± 2.8 -2.89% (t -1.1) 600,199 yes
app-dev unpatched 8.2 ± 0.6 54.9 ± 4.1 1,447,772 yes
app-dev terminal-external-write-bursts 5.1 ± 0.4 -38.25% (t -14.1) 55.2 ± 3.2 +0.62% (t 0.2) 1,447,772 yes

Values are mean ± sample sd over 10 runs per variant. t is Welch's t vs unpatched.

The saving is a roughly flat 2-3s per build, not faster bundling: the process no longer waits for status timers before exiting (on main, the trailing lodash.throttle timer, up to 3.5s after the last status line in non-interactive output). The generated app builds in about 5s, so the percentages are large. Longer builds, like real apps, save about the same seconds, which is a much smaller share of their wall time. CPU time doesn't change significantly, and bundles are byte-identical.

base.patch
diff --git a/packages/metro/src/commands/build.js b/packages/metro/src/commands/build.js
index af858666c..ee2f45492 100644
--- a/packages/metro/src/commands/build.js
+++ b/packages/metro/src/commands/build.js
@@ -133,6 +133,11 @@ export default (): CommandModule => ({
         transformedFileCount: number,
         totalFileCount: number,
       ): void => {
+        for (let i = 0; i < 3; i++) {
+          console.log(
+            `transformed ${transformedFileCount}/${totalFileCount} (${i})`,
+          );
+        }
         updateReporter.update({
           buildID: '$',
           type: 'bundle_transform_progressed',

Benchmark on an internal app

bundling_speed as on main, which takes an app as a target (5 runs per variant), on an internal app, built cold with Metro from source, with DEBUG=Metro:*, a patch that logs 3 lines per transformed module in the main process, and output piped. Its Metro didn't include #2162 yet, so main-terminal adds #2162, and terminal-external-write-bursts adds #2162 and #2170: #2170's effect is the difference between the two.

Bundle Variant Wall (s) Wall Δ CPU (s) CPU Δ Same as unpatched
internal-app-prod unpatched 107.6 ± 1.9 1376.7 ± 6.2 yes
internal-app-prod main-terminal 108.2 ± 2.9 +0.58% (t 0.4) 1385.1 ± 13.4 +0.61% (t 1.3) yes
internal-app-prod terminal-external-write-bursts 107.7 ± 1.1 +0.14% (t 0.2) 1385.5 ± 9.8 +0.64% (t 1.7) yes
internal-app-dev unpatched 93.5 ± 2.4 1089.8 ± 14.4 yes
internal-app-dev main-terminal 93.5 ± 1.3 -0.03% (t -0.0) 1087.3 ± 10.3 -0.23% (t -0.3) yes
internal-app-dev terminal-external-write-bursts 94.1 ± 2.3 +0.63% (t 0.4) 1090.6 ± 9.1 +0.08% (t 0.1) yes

Values are mean ± sample sd. t is Welch's t vs unpatched; at 5 runs, |t| < 2.3 is not significant (p > 0.05).

No wall time or CPU change is significant, and bundles are byte-identical. This app's Meta internal build command already flushes its terminal when the build finishes, so there's no exit wait to save; the burst handling adds no measurable cost on a large app under heavy logging.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 4, 2026
@vzaidman
vzaidman force-pushed the vzaidman/terminal-external-write-bursts branch 2 times, most recently from 5b00d8a to 286024f Compare November 4, 2026 20:16
@vzaidman
vzaidman marked this pull request as ready for review November 4, 2026 20:21
@vzaidman
vzaidman force-pushed the vzaidman/terminal-external-write-bursts branch from 286024f to bd0c69d Compare November 4, 2026 20:22
@vzaidman
vzaidman requested a review from robhogan November 4, 2026 20:23
@vzaidman
vzaidman requested a review from huntie November 4, 2026 20:23
@vzaidman

vzaidman commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

@robhogan how did you benchmark #2162? Could you please benchmark this as well? Maybe even add it to the benchmarking script?

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Metro review: Looks good

Risk: low · Commit: afd65a3 · 13 commands run

The PR makes Terminal hold status writes until outside output has been quiet for 333ms, capped at 2000ms, replaces lodash.throttle with an unreferenced status delay flushed on beforeExit, and removes the throttled reporter event. No blocking or should-fix issues: real-process runs on head and base confirm the status ordering and exit behavior, and a cold metro build exits about 3.3s sooner, matching the flat saving the PR describes. TerminalReporter now renders on every progress event, which costs about 31µs more per event in a simulated TTY; that is small but is the one cost to watch. package.json and yarn.lock change, so this needs a Meta import.

No findings.

Verified
  • ✅ yarn jest --no-watchman packages/metro-core packages/metro/src/lib — 24 suites, 249 tests. The 'worker failed to exit gracefully' notice also appears on base for packages/metro-core.
  • ✅ yarn jest --no-watchman packages/metro/src packages/metro-config — 78 suites, 1111 passed, 1 skipped, including the integration tests.
  • ✅ yarn eslint packages/metro-core/src packages/metro/src/lib
  • ✅ yarn verify-api-snapshots — 18 snapshots verified.
  • ⚠️ timeout 280 yarn flow check — Did not run: timeout is not installed on this machine. Flow was not checked; see not_run.
  • ✅ sh bench/run-scenarios.sh — Piped stdout, head vs base. Pending status is written on natural exit, waitForUpdates resolves with no other handles, and process.exit behaves the same. Under output every 100ms, head holds the first status about 2s, then writes it.
  • ✅ yarn jest --no-watchman packages/metro-core/src/__tests__/Terminal-test.js -t "sustained|own logs" (raw byte count in #getExternalBytesWritten, deadline check off) (failed as expected) — The own-logs test fails in both TTY modes, which backs the fix for b4814541. The sustained test still passed because Math.min also bounds the wait, so this mutant didn't remove the cap.
  • ✅ yarn jest --no-watchman packages/metro-core/src/__tests__/Terminal-test.js -t "sustained" (deadline check and Math.min cap both removed) (failed as expected) — Fails in both TTY modes, which backs the fix for 5a6c4169.
  • ✅ yarn jest --no-watchman packages/metro-core/src/__tests__/Terminal-test.js (mutations reverted) — 37 tests. git diff --stat on Terminal.js is empty, so the file matches head.
  • ✅ node bench/build-bench.js
  • ✅ node bench/progress-bench.js
  • ✅ node bench/tty-bench.js
  • ✅ node bench/chunk-bench.js — chunkString in a tight loop: 0.33µs with the RegExp built per call, 0.22µs cached. The RegExp compile doesn't explain the TTY per-event cost, so I'm not suggesting that change.

Benchmarks (base vs this PR)

Benchmark Base This PR Change
Cold metro build wall time, non-TTY 4295ms (4266-4363) 962ms (944-990) -3.3s (4.5x faster)
Progress event CPU, non-TTY stream, batched 0.03µs/event (0.03-0.03) 3.16µs/event (3.10-3.28) about 100x, +3.1µs per event
Progress event CPU, simulated TTY, spaced events 1.77µs/event (1.17-2.38) 33.11µs/event (30.50-42.94) about 19x, +31µs per event
Method
  • Cold metro build wall time, non-TTY: Integration fixture TestBundle.js (few modules), --reset-cache, stdout piped, 10 rounds per side, alternating ABAB in separate processes, range shown. The saving is the exit wait, so the ratio is specific to this tiny build. bench/build-bench.js
  • Progress event CPU, non-TTY stream, batched: 20,000 bundle_transform_progressed events in batches of 50 per macrotask, no-op Writable, one process loading both checkouts, 12 rounds ABAB, range shown. bench/progress-bench.js
  • Progress event CPU, simulated TTY, spaced events: Fake tty.WriteStream, 100 columns, 3 events every 3ms or so for 3s, one process, 10 rounds ABAB, range shown. Stream writes per 3s: head 179, base 91. No real terminal rendering included. bench/tty-bench.js

Not run: Flow: not checked; timeout is missing and the retry was denied (no approval surface), so it rests on the PR's claim. Real TTY: running under script was denied, so only mock and fake tty.WriteStream streams were used. Windows: no machine available, so ANSI erase and pipes are unverified. The PR's benchmark table wasn't reproduced (needs GNU time and a generated app), so its 10k-module CPU numbers are unchecked. Warm-cache builds of large apps, where the per-event cost weighs most, weren't measured.

Automated review by Claude. Advisory only: it never approves or blocks, and a maintainer makes the call.

Comment thread packages/metro-core/src/Terminal.js Resolved
Comment thread packages/metro-core/src/Terminal.js Resolved
Summary:
When output was written outside of `Terminal` (e.g. `console.log`) in a burst, a status update could land between the burst's lines. In a TTY it can't be erased there, so copies of the status were left scattered in the output.

- Before writing the status, `Terminal` waits until no output is written outside of it for 333ms (`EXTERNAL_WRITES_QUIET_MS`), in interactive and non-interactive terminals. Outside output is detected through the stream's `bytesWritten`. Without outside output, the status is written synchronously. `flush()` skips the wait.
- Status writes wait on a delay promise set after each write: 33ms in a TTY, 3500ms otherwise. `flush()` ends the delay early.
- Status delay timers are unreferenced, so they don't keep the process alive. An ongoing status update is flushed on `beforeExit`, so the latest status is still written and `waitForUpdates()` resolves.
- `lodash.throttle` is removed from `metro-core` and `metro`:
  - `Terminal` limits non-TTY status lines through the same status delay.
  - `TerminalReporter` updates bundle progress on each `bundle_transform_progressed` event; `Terminal` limits the redraws. The `bundle_transform_progressed_throttled` event type is removed from `TerminalReportableEvent`.
- The status is erased with ANSI escape sequences written through `Terminal`'s own write, so every write tracks its completion and the bytes written after it. `node:readline` is no longer used.
- `isUpdating` and `hasPendingUpdate` are removed: an update is ongoing while `#updatePromise` is set, and pending while the written status differs from the latest one.

`metro build` no longer waits for status timers before exiting: on a generated app of 5,000 modules whose build logs 3 lines per transformed module, wall time drops by 28% in prod and 37% in dev (about 2-3s, the trailing `lodash.throttle` timer on `main`), with no significant CPU change and byte-identical bundles.

Changelog: [Breaking] Terminal no longer prints the status in the middle of bursts of output written outside of it, and `bundle_transform_progressed_throttled` is removed from `TerminalReportableEvent`

Test Plan:
- New test "writes the status after a burst of external writes" fails before this change, with a 33ms quiet period, and without non-TTY outside output detection, and passes after.
- New test "flush does not wait for the next status update" fails when `flush()` doesn't end the status delay.
- New test "flushes the pending status before the process exits" fails without the `beforeExit` listener.
- New `TerminalReporter` test "shows the latest bundle transform progress" fails with the throttled reporter.
- `Terminal-test.js` uses fake timers.
- `yarn jest packages/metro-core packages/metro/src/lib`
- `yarn flow check`, `yarn eslint packages/metro-core/src packages/metro/src/lib`, `yarn verify-api-snapshots`
- `node scripts/benchmark-patches/benchmark-patches.js bundling_speed --runs=10`
…minal's own writes

- The wait for external writes to stop is capped at 2000ms (`EXTERNAL_WRITES_MAX_WAIT_MS`), so sustained external output doesn't hide the status.
- Bytes written outside of `Terminal` are counted separately from its own writes, so `Terminal.log()` calls don't extend the wait once it started.
@vzaidman
vzaidman force-pushed the vzaidman/terminal-external-write-bursts branch from 74c4905 to afd65a3 Compare November 5, 2026 06:24
robhogan added a commit to robhogan/reports that referenced this pull request Nov 5, 2026

@robhogan robhogan left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a closer look at this, I'm a fan of removing the dependency and the dead wait at the end of bundling, but there are a couple of regressions that need fixing - flickering and too much main thread use:

  • Every terminal.log line erases the status, and it only reappears after a 333ms pause.
  • It renders the status on every transform progress event, vs previous throttling limiting renders. I measured that at 11% slower warm builds (worse on slow terminals)

But both are fairly easily fixed with two changes: limit progress renders to one per 100ms and have log() redraw the status in the same write. That eliminates flickering and makes main thread load negligible.

Full report, with recommendations and benchmark methodology: https://robhogan.github.io/reports/metro-2170-terminal-status/

@robhogan

robhogan commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

FWIW, I disagree with the review bot about this being a breaking change. Reporter events have never been considered part of the semver contract (IMO). We can land this in a minor release once it's not regressing.

…s in Terminal.log()

- `TerminalReporter` updates the status on `bundle_transform_progressed` at most every 100ms (`PROGRESS_STATUS_INTERVAL_MS`). `_log` and `_updateState` still run on every event.
- `Terminal.log()` writes an erased status again below the line in the same write, so the status stays on the screen, unless it is cleared.
@vzaidman
vzaidman requested a review from robhogan November 6, 2026 16:37
@vzaidman
vzaidman merged commit 0315757 into react:main Nov 6, 2026
16 checks passed
@vzaidman
vzaidman deleted the vzaidman/terminal-external-write-bursts branch November 6, 2026 17:18
robhogan added a commit that referenced this pull request Nov 6, 2026
Summary:
Publish Metro 0.88.0 from `main`.

Changelog: [Internal]

Test plan:
```
node scripts/updateVersion.js 0.88.0
sl diff --stat
```

Only the 17 `packages/*/package.json` manifests change, and every `0.87.3` reference is replaced. Flow, ESLint, Prettier, TypeScript, API snapshots, Markdown lint and Jest pass on this commit.

Notable breakage in third parties, for the notes or a heads-up before React Native adopts this:

 - `@rnx-kit/metro-config` 2.2.6 adds `path.dirname(resolver.emptyModulePath)` as a watch folder when there are none, which is now `metro:runtime/modules`, so the dev server never serves a bundle for a single-package app.
 - `@tamagui/metro-plugin` 3.x refuses `metro-resolver` outside 0.84-0.87, and then imports the removed `metro-resolver/private/createDefaultContext`.
 - `@rnx-kit/metro-resolver-symlinks` calls the removed `context.redirectModulePath`.

End to end (AI-driven), with the packed tarballs against 0.87.3 from npm, in a React Native 0.89.0-rc.0 template app with a few popular dependencies: dev and release bundles, source maps and assets for iOS and Android are byte-identical, except where #2174 changes resolution. `rxjs/operators` now resolves to the same ESM build as `rxjs`, dropping 190 duplicate modules, and `require('nanoid/non-secure')` gets nanoid's CJS build. With #2186, only the module system and `asyncRequire` differ besides those. Release builds reach the first screen on a Galaxy A16 and the iOS simulator. The dev server passes the same checks on both, with and without Watchman (edits, added and deleted files, HMR, `/symbolicate`, assets and lazy `import()`), and Fast Refresh works on the simulator.

The end-to-end checks above were on 9de42bbed3b2, before #2170, #2157, #2158 and #1990 landed. I'm re-running them now and will update this.

Draft release notes (from the 19 commits in v0.87.3...main - internal-only changes omitted):

```
 - **[Breaking]**: Resolver: Read `"exports"` from the package root, and apply `"browser"` redirections to the file it resolves to, by default. Set `resolver.unstable_resolveExportsFromPackageRoot` and `resolver.unstable_applyBrowserSpecAfterExports` to `false` for the previous behavior (#2174 by @robhogan)
 - **[Breaking]**: Default `transformer.asyncRequireModulePath` and `resolver.emptyModulePath` to `metro:runtime/...`. A custom `resolver.resolveRequest` must delegate `metro:` specifiers it doesn't handle to `context.resolveRequest`, and code that reads either option as a file path must resolve it instead (#2180 by @robhogan)
 - **[Breaking]**: Resolve the module system via `metro:runtime/polyfills/require`. A custom `resolver.resolveRequest` must delegate `metro:` specifiers it doesn't handle to `context.resolveRequest` (#2184 by @robhogan)
 - **[Breaking]**: `transformer.globalPrefix` now also prefixes `__r`, `__c`, `__registerSegment` and `__accept`, and the default run statement. Code that calls these by name must apply the prefix (#2186 by @robhogan)
 - **[Breaking]**: Remove the deprecated `redirectModulePath` from `ResolutionContext`. A custom resolver should drop any call to it and delegate to `context.resolveRequest` or `resolve`, which already apply `"browser"` redirections and return `{type: 'empty'}` for `false` (#2181 by @robhogan)
 - **[Breaking]**: Remove the deprecated `doesFileExist` from `ResolutionContext`, and the `DoesFileExist` type from `metro-resolver` (use `fileSystemLookup`) (#2181 by @robhogan)
 - **[Breaking]**: `ResolutionContext.isESMImport` is required (#2182 by @robhogan)
 - **[Breaking]**: Async bundle and HMR module URLs keep the module's file extension (`foo.js.bundle`) and carry the bundle's `platform`, fixing lazy loading and HMR source maps for files whose extension isn't in `sourceExts` (e.g. `.cjs`, `.mjs`) or that share a name with a sibling (e.g. `foo.json` next to `foo.js`). Old URLs still work (#1990 by @dasjideepak)
 - **[Breaking]**: Babel transformers can no longer return a `functionMap`. Apply the `plugins` passed by Metro and return the resulting Babel `metadata` to keep function names in stack traces (#2157 by @robhogan)
 - **[Breaking]**: `/symbolicate` no longer reads `req.rawBody`, and fails if a middleware has already read the request body (#2158 by @robhogan)
 - **[Breaking]**: Remove the `bundle_transform_progressed_throttled` event from `TerminalReportableEvent`. `TerminalReporter` subclasses should handle `bundle_transform_progressed`, which their `_log` now receives for every progress update (#2170 by @vzaidman)
 - **[Breaking]**: Source and JSON files containing the reserved string `_$$_METRO_`, including in comments and string literals, now fail to transform (#2155 by @robhogan)
 - **[Breaking]**: Remove the deprecated, always-`true` `hot` option from `getTransformOptions` (#2159 by @robhogan)
 - **[Feature]**: Custom serializers receive the reserved string as `options.reservedString` (#2155 by @robhogan)
 - **[Fix]**: Don't print the terminal status in the middle of bursts of output written outside of it, and don't wait for a pending status update before exiting (#2170 by @vzaidman)
 - **[Fix]**: Fall back to file-based resolution, instead of throwing a `TypeError`, when a package's `"exports"` field is not a string, array or object (#2187 by @kwy404)
 - **[Fix]**: Fix relative symlinks on a different Windows drive than the project root (#2161 by @ramya-2196)
 - **[Performance]**: ~5-10% faster resolution (#2181 by @robhogan)

> NOTE: Experimental features are not covered by semver and can change at any time.

 - **[Experimental]**: Remove `unstable_incrementalResolution` from `ResolutionContext`, and `metro-resolver/private/createDefaultContext` (#2181 by @robhogan)

 **Full Changelog**: v0.87.3...v0.88.0
```
@robhogan robhogan mentioned this pull request Nov 6, 2026
robhogan added a commit that referenced this pull request Nov 6, 2026
Summary:
Publish Metro 0.88.0 from `main`.

Changelog: [Internal]

Test plan:
```
node scripts/updateVersion.js 0.88.0
sl diff --stat
```

Only the 17 `packages/*/package.json` manifests change, and every `0.87.3` reference is replaced. Flow, ESLint, Prettier, TypeScript, API snapshots, Markdown lint and Jest pass on this commit.

Notable breakage in third parties, for the notes or a heads-up before React Native adopts this:

 - `@rnx-kit/metro-config` 2.2.6 adds `path.dirname(resolver.emptyModulePath)` as a watch folder when there are none, which is now `metro:runtime/modules`, so the dev server never serves a bundle for a single-package app.
 - `@tamagui/metro-plugin` 3.x refuses `metro-resolver` outside 0.84-0.87, and then imports the removed `metro-resolver/private/createDefaultContext`.
 - `@rnx-kit/metro-resolver-symlinks` calls the removed `context.redirectModulePath`.

End to end (AI-driven), with the packed tarballs against 0.87.3 from npm, in a React Native 0.89.0-rc.0 template app with a few popular dependencies: dev and release bundles, source maps and assets for iOS and Android are byte-identical, except where #2174 changes resolution. `rxjs/operators` now resolves to the same ESM build as `rxjs`, dropping 190 duplicate modules, and `require('nanoid/non-secure')` gets nanoid's CJS build. With #2186, only the module system and `asyncRequire` differ besides those. Release builds reach the first screen on a Galaxy A16 and the iOS simulator. The dev server passes the same checks on both, with and without Watchman (edits, added and deleted files, HMR, `/symbolicate`, assets and lazy `import()`), and Fast Refresh works on the simulator.

The end-to-end checks above were on 9de42bbed3b2, before #2170, #2157, #2158 and #1990 landed. I'm re-running them now and will update this.

Draft release notes (from the 19 commits in v0.87.3...main - internal-only changes omitted):

```
 - **[Breaking]**: Resolver: Read `"exports"` from the package root, and apply `"browser"` redirections to the file it resolves to, by default. Set `resolver.unstable_resolveExportsFromPackageRoot` and `resolver.unstable_applyBrowserSpecAfterExports` to `false` for the previous behavior (#2174 by @robhogan)
 - **[Breaking]**: Default `transformer.asyncRequireModulePath` and `resolver.emptyModulePath` to `metro:runtime/...`. A custom `resolver.resolveRequest` must delegate `metro:` specifiers it doesn't handle to `context.resolveRequest`, and code that reads either option as a file path must resolve it instead (#2180 by @robhogan)
 - **[Breaking]**: Resolve the module system via `metro:runtime/polyfills/require`. A custom `resolver.resolveRequest` must delegate `metro:` specifiers it doesn't handle to `context.resolveRequest` (#2184 by @robhogan)
 - **[Breaking]**: `transformer.globalPrefix` now also prefixes `__r`, `__c`, `__registerSegment` and `__accept`, and the default run statement. Code that calls these by name must apply the prefix (#2186 by @robhogan)
 - **[Breaking]**: Remove the deprecated `redirectModulePath` from `ResolutionContext`. A custom resolver should drop any call to it and delegate to `context.resolveRequest` or `resolve`, which already apply `"browser"` redirections and return `{type: 'empty'}` for `false` (#2181 by @robhogan)
 - **[Breaking]**: Remove the deprecated `doesFileExist` from `ResolutionContext`, and the `DoesFileExist` type from `metro-resolver` (use `fileSystemLookup`) (#2181 by @robhogan)
 - **[Breaking]**: `ResolutionContext.isESMImport` is required (#2182 by @robhogan)
 - **[Breaking]**: Async bundle and HMR module URLs keep the module's file extension (`foo.js.bundle`) and carry the bundle's `platform`, fixing lazy loading and HMR source maps for files whose extension isn't in `sourceExts` (e.g. `.cjs`, `.mjs`) or that share a name with a sibling (e.g. `foo.json` next to `foo.js`). Old URLs still work (#1990 by @dasjideepak)
 - **[Breaking]**: Babel transformers can no longer return a `functionMap`. Apply the `plugins` passed by Metro and return the resulting Babel `metadata` to keep function names in stack traces (#2157 by @robhogan)
 - **[Breaking]**: `/symbolicate` no longer reads `req.rawBody`, and fails if a middleware has already read the request body (#2158 by @robhogan)
 - **[Breaking]**: Remove the `bundle_transform_progressed_throttled` event from `TerminalReportableEvent`. `TerminalReporter` subclasses should handle `bundle_transform_progressed`, which their `_log` now receives for every progress update (#2170 by @vzaidman)
 - **[Breaking]**: Source and JSON files containing the reserved string `_$$_METRO_`, including in comments and string literals, now fail to transform (#2155 by @robhogan)
 - **[Breaking]**: Remove the deprecated, always-`true` `hot` option from `getTransformOptions` (#2159 by @robhogan)
 - **[Feature]**: Custom serializers receive the reserved string as `options.reservedString` (#2155 by @robhogan)
 - **[Fix]**: Don't print the terminal status in the middle of bursts of output written outside of it, and don't wait for a pending status update before exiting (#2170 by @vzaidman)
 - **[Fix]**: Fall back to file-based resolution, instead of throwing a `TypeError`, when a package's `"exports"` field is not a string, array or object (#2187 by @kwy404)
 - **[Fix]**: Fix relative symlinks on a different Windows drive than the project root (#2161 by @ramya-2196)
 - **[Performance]**: ~5-10% faster resolution (#2181 by @robhogan)

> NOTE: Experimental features are not covered by semver and can change at any time.

 - **[Experimental]**: Remove `unstable_incrementalResolution` from `ResolutionContext`, and `metro-resolver/private/createDefaultContext` (#2181 by @robhogan)

 **Full Changelog**: v0.87.3...v0.88.0
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. needs-meta-import

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants