Skip to content

[0.84] Picks for 0.84.7 - #2070

Draft
robhogan wants to merge 45 commits into
0.84.xfrom
0.84/picks-for-0.84.7
Draft

robhogan wants to merge 45 commits into
0.84.xfrom
0.84/picks-for-0.84.7

Conversation

@robhogan

@robhogan robhogan commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Picks the non-breaking changes from main since 0.84.4 onto 0.84.x, and bumps to 0.84.7. The main motivation is #1892 (with its prerequisite #1917), so that React Native 0.86 (on metro@^0.84.3) gets working assets outside projectRoot. Everything else here is a fix, perf improvement or additive feature that applies to 0.84.x without its breaking neighbours.

Commits are in main order, one per original commit, each with a (cherry picked from commit ...) trailer. Please don't squash. Where a pick needed adapting to 0.84.x (no node: specifiers, no getDefaultAsyncDependencyPath extraction, checked-in .d.ts), the commit message says so, and generated .d.ts changes are folded into the commit that caused them.

This also picks #1769, which stops building and publishing integration_tests, __fixtures__ etc. Without it, yarn build fails on the export * as fixture added by #1768.

Left out:

Note that #1892 changes where release builds put assets outside projectRoot (eg assets/[metro-watchFolders]/1/... on iOS, metrowatchfolders_1_... on Android). React Native derives both the copy destination and the runtime lookup from httpServerLocation, so they move together, but it's an observable change for a patch.

Changelog:

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

Test plan

Every commit was checked out and verified on its own (Flow, .d.ts sync, lint and prettier on the touched files, and Jest --findRelatedTests), so the branch bisects cleanly. Full Jest, Flow, tsc and lint pass at the tip.

The watcher integration tests are timing-sensitive under heavy parallel load, on 0.84.x too - running --findRelatedTests for FallbackWatcher.js six times locally, 0.84.x failed 2/6 (NativeWatcher cases) and this branch 3/6 (including the new #1907 test for a directory created while watching). metro-file-map's watcher tests on their own passed 8/8 at the tip.

#1892 was tested end-to-end on main (see its test plan), and the picked version is unchanged apart from its tests.

🤖 Generated with Claude Code

kitten and others added 11 commits October 15, 2026 07:14
Summary:
This was added to `loadConfig.js` but is missing in other places. While it's arguably not useful in many cases in a "standard" Metro setup with Metro's own packages, it'd be nice to have this behave consistently even with transpiled ESM output (or with require-ESM in Node).

Specifically this affects:
- metro-file-map plugin workers that are loaded with `require`
- metro-file-map's main worker loading
- metro's asset plugins
- metro-transform-worker's `getMinifier` implementation
- metro-transform-worker's `babelTransformerPath` loading

This is probably specifically interesting for `babelTransformerPath` which is often overridden by third-party setups.

Changelog: [Fix] Unwrap ES modules when requiring metro-file-map workers, asset plugins, the minifier, or the babel transformer

Pull Request resolved: #1675

Test Plan: - n/a: This will likely be best left untested, but is a consistent implementation that matches ESM->CJS semantics, also used in `loadConfig.js`

Reviewed By: javache

Differential Revision: D102510513

Pulled By: robhogan

fbshipit-source-id: 863b53901d90e06853f96fa04b2b129c85b316e8

(cherry picked from commit 0e9d7aa)
…1712)

Summary:
Pull Request resolved: #1712

babel/plugin-transform-modules-systemjs versions >= 7.12.0 and <= 7.29.3 are affected by CVE-2026-44728 (GHSA-fv7c-fp4j-7gwp), a HIGH severity vulnerability. The metro website resolves this package at 7.25.9 via babel/preset-env from Docusaurus. This bumps the package to ^7.29.4, the first patched version.

Reviewed By: robhogan

Differential Revision: D104687111

fbshipit-source-id: 19ec44324488a79fb078c2939f8c05573c0c8695

(cherry picked from commit 25bad99)
Summary:
Related PR for `expo/expo` fork: expo/expo#45648
Supersedes #1696

Cross-device path normals are intended to be relative paths from the rootDir. If the rootDir is at `C:\project` then a cross-device normal path at `D:\temp` is represented as `..\..\D:\temp`. This assumption is broken in several places, like `normalToAbsolute` printing, leading to `C:\D:\` paths that are invalid.

Changelog: [Fix] Fix Windows path handling for cross-drive paths

Pull Request resolved: #1711

Test Plan:
- Unit tests were adjusted to demonstrate all failing cases
- **Note:** Invalid unit tests have been removed for paths that rise above the filesystem root (they're assumed to be impossible, and their output doesn't have to be deterministic/correct)

Reviewed By: robhogan

Differential Revision: D104700077

Pulled By: CalixTang

fbshipit-source-id: e77baa4e66495b50ef41b260cec60bcc832d4a6a

(cherry picked from commit 6e6b52c)
…n rather than calls to redirectModulePath

Summary:
Currently, `metro-resolver` tests assert on and mock calls to `redirectModulePath`, which since [5e96d17](5e96d17) (v0.81.0) is a fixed implementation detail, rather than a user-configurable context method.

Refactor tests to assert on observable behaviour by creating mock `package.json`s, which makes these tests more robust to upcoming internal optimisations.

Changelog: [Internal]

___

Differential Revision: D102514698

fbshipit-source-id: 05c47b37ea07d2d42128b20c4d15148a8f35a634

(cherry picked from commit 922ee8b)
…ft closest package scope to root resolver for reuse

Summary:
On the bare-specifier hot path, replace the top-level `redirectModulePath(context, specifier)` wrapper call with a direct `getPackageForModule(originModulePath)` + `matchSubpathFromMainFields(...)`.

Perf: `resolve()` has already excluded relative/absolute specifiers by this point, so the `redirectModulePath` wrapper was re-deriving facts the caller already knew — an extra call frame, a redundant `path.isAbsolute()` and `.startsWith('.')`, and an unconditional `isRelativeImport()/path.isAbsolute()` on the result that now only runs on the rare actual-redirect case. The two genuinely costly operations (the closest-package lookup and the main-fields match) are unchanged, so this is a small saving — ~1.5% resolver-only CPU (see test plan).

More importantly, this surfaces the closest `package.json` to the origin (`closestPackageToOrigin`) in the root resolver, where it can be reused by follow-up diffs rather than being re-derived inside `redirectModulePath`.

In particular, we'll reuse closest package scope to implement `PACKAGE_SELF_RESOLVE` at negligible cost.

Changelog: Internal

Reviewed By: GijsWeterings, huntie

Differential Revision: D102515190

fbshipit-source-id: acc962617e38e1c56b85196ed71a5b010abe2c17

(cherry picked from commit 7529585)
…s, improve overall resolver perf ~5%

Summary:
`matchSubpathFromMainFields` is called on every resolution attempt (per source-extension via `redirectModulePath`, and per package entry point), and on every call it allocated a `.map()` array, a `.filter()` array, and an `Object.assign({}, ...spread)` - even in the overwhelmingly common case where the package declares no object-valued main field (no "browser" map) and the function returns `null`.

Instead, rebuild the merged replacement map with a single reverse loop over `mainFields` (preserving the prior "earlier mainFields win on key conflict" semantics) such that nothing is allocated (`null` is still returned) unless and until an object-valued field is actually found. The list of subpath variants is likewise only built in that rare matched case.

This improves the perf of `matchSubpathFromMainFields` by ~20% and ~5% on resolutions overall, based on Meta's product graph.

```
 - **[Performance]**: Refactor to reduce allocs for a ~5% faster resolver

Reviewed By: huntie

Differential Revision: D107024445

fbshipit-source-id: cec2261dc588590eb12bb413a7bbb6fc6869401c

(cherry picked from commit 9341463)
…ecifier -> absolute path

Summary:
While doing some refactoring here I noticed some nonsensical handling of the case where a browser-spec `package.json` tries to redirect a bare specifier to an absolute path, eg:

```
// /project/package.json
{
  "browser": {
    "foo-pkg": "/var/foo"
  }
}
```

This currently causes the resolver to redirect to `/project/var/foo` - *joining* the absolute path onto the end of the origin-relative path.

This is surely never the desired behaviour, and is outside the [`browser` field spec](https://github.com/defunctzombie/package-browser-field-spec#replace-specific-files---advanced).

Rather than "fix" this and support something new in an increasingly outdated spec (`browser` redirects have been effectively replaced by `exports`/`imports` maps), here we just replace what was broken with a graceful, actionable rejection.

Although this changes observable resolver behaviour, I think it's safe to treat this as non-breaking because the previous behaviour was so unexpected.

```
 - **[Fix]**: Resolver: Gracefully reject bare->absolute redirects rather than failing on a spurious path
```

Reviewed By: huntie

Differential Revision: D102568988

fbshipit-source-id: 5bf8d6c3f5a26e0b3ea2fd3f49a3dd9fa4113663

(cherry picked from commit e1f4d61)
… nested/first-party package.jsons (#1720)

Summary:
Pull Request resolved: #1720

The [browser spec](https://github.com/defunctzombie/package-browser-field-spec) redirect for bare specifiers (`"browser": {"foo-pkg": "./shims/foo.js"}`) was previously rebased against a path derived by slicing `originModulePath` after the last `node_modules/` segment. That happens to be correct only in the (common) case that the closest `package.json` is the one at the root of a containing `node_modules` package. However:

- It's legal for an NPM package to contain nested `package.json` files and those may contain redirects - in which case their targets are relative to themselves, not to the package root `package.json`.

- For origin modules that aren't located inside a `node_modules` tree (e.g. project-local source), `lastIndexOf('node_modules/')` returns `-1`, producing a malformed root path and either failed resolutions or surprising successes.

This diff replaces that derivation with `closestPackageToOrigin.rootPath`, which we already look up at the top of `resolve`. Behaviour is identical in the already-working cases, and corrected otherwise. Matches webpack's `AliasFieldPlugin`, which rebases against `request.descriptionFileRoot`.

Changelog:
```
 - **[Fix]**: Fix browser-spec redirects of bare specifier to relative path, where `package.json` is not directly under `node_modules/<pkg>/`
````

Reviewed By: huntie

Differential Revision: D105485945

fbshipit-source-id: 0b0b0b985294024d64305a15720ecc9007658d16

(cherry picked from commit 385a946)
…#1721)

Summary:
Pull Request resolved: #1721

Add support to Metro's resolver for [self-referencing](https://nodejs.org/api/packages.html#self-referencing-a-package-using-its-name). A package can import itself by name, resolving via its own `exports` field. Per the [Node ESM resolution algorithm](https://nodejs.org/api/esm.html#resolution-algorithm-specification) (6.5 `PACKAGE_SELF_RESOLVE`), if the origin module lives in a package whose `name` matches the bare specifier and which declares `exports`, the request is routed through `PACKAGE_EXPORTS_RESOLVE` against that package, in preference to hierarchical `node_modules` lookup.

(In particular, this allows `react-native`'s own source to `import {Platform} from 'react-native'`, which *may* be the approach we take to fix broken platform inlining)

Gated behind the existing `unstable_enablePackageExports` flag (enabled by default). When on, the resolver attempts self-resolve before hierarchical lookup; on `PackagePathNotExportedError` it follows Metro's existing nonstrict policy of warning and falling through to legacy resolution, consistent with how non-self bare specifiers behave today. This nonstrict fallback will be removed in future.

Matches webpack's `SelfReferencePlugin` and Node's own behaviour.

```
 - **[Feature]**: Resolver: support self-referencing packages ([`PACKAGE_SELF_RESOLVE`](https://nodejs.org/api/esm.html#resolution-algorithm-specification))
```

Reviewed By: huntie, motiz88

Differential Revision: D105485943

fbshipit-source-id: 257090425208ef418d9b8c9cc3968d9d1df839cf

(cherry picked from commit 06f1de1)
Summary:
`HttpStore.#withRetries(fn, endpoint)` is called for both endpoints — `get()` passes `#getEndpoint`, `set()` passes `#setEndpoint` — and it correctly consults the passed `endpoint` for the `maxAttempts === 1` short-circuit. However, the `backOff` options hardcoded `this.#getEndpoint` for `numOfAttempts`, `retryStatuses`, and `retryNetworkErrors`.

As a result, when an `HttpStore` is constructed with the documented split `{ getOptions, setOptions }` form, all `set()`/PUT retries silently use the **read** endpoint's retry configuration — `setOptions.maxAttempts`, `setOptions.retryStatuses`, and `setOptions.retryNetworkErrors` are ignored. There's also an internal inconsistency: the `maxAttempts === 1` early-return reads the (set) endpoint, but `numOfAttempts` then read `#getEndpoint.maxAttempts`.

The fix uses the passed-in `endpoint` consistently.

Pull Request resolved: #1751

Test Plan:
The existing retry tests only use the single-options form via `get()`, where `#getEndpoint === #setEndpoint`, so they can't catch this. Added a regression test that builds the store with the split form (reads: `maxAttempts: 1`; writes: `maxAttempts: 2, retryStatuses: [503]`) and asserts `set()` retries a 503:

- Before: `set()` uses the read config (no retry) and throws the 503 → test fails.
- After: `set()` retries per `setOptions` and resolves → test passes.

```
yarn jest packages/metro-cache/src/stores/__tests__/HttpStore-test.js
# Tests: 19 passed   (full metro-cache package: 45 passed)
```

Reviewed By: Abbondanzo

Differential Revision: D109932333

Pulled By: robhogan

fbshipit-source-id: d7cdf9f1f128a611424df001ff885093682ed1ab

(cherry picked from commit 22fa6dd)
…1754)

Summary:
When `mergeConfig(base, ...configs)` encounters a config that resolves to a `Promise` (e.g. an async `metro.config.js` / a config function returning a promise) and at least one more config follows it, it hands off to `mergeConfigAsync` with two defects:

1. **Missing spread.** `mergeConfigAsync(baseConfig, ...reversedConfigs)` takes a rest parameter, but the call passed `reversedConfigs.toReversed()` as a single array argument. The remaining configs end up wrapped one level too deep, so they're merged as a bogus object (producing junk keys like `"0"`) instead of being applied as configs.
2. **Dropped accumulator.** `currentConfig` (the base merged with every config to the *left* of the async one) was discarded — the unresolved promise was passed as the base, so all prior merges were lost.

The fix spreads the remaining configs into the rest parameter and folds `currentConfig` into the resolved async config so both the accumulated state and the trailing overrides are preserved.

Pull Request resolved: #1754

Test Plan:
The existing `mergeConfig` tests only cover sync configs, so this path was uncovered. Added a regression test (an async config function followed by a trailing override) asserting the base, the async config, and the trailing override all survive:

```
yarn jest packages/metro-config/src/__tests__/mergeConfig-test.js
# Tests: 19 passed  (full metro-config package: 40 passed)
```
- Before: `result.server.port` is `undefined` (base dropped); trailing override mangled.
- After: all three configs merge correctly.

Reviewed By: huntie, robhogan

Differential Revision: D110207664

Pulled By: rozele

fbshipit-source-id: 804041a89c05a08aee0823e91cf22f4539e927ca

(cherry picked from commit 9dbe0cc)
@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 Oct 15, 2026
robhogan and others added 18 commits October 15, 2026 07:55
Summary:
Pull Request resolved: #1769

The build script previously only excluded `__tests__`, and had an incorrect comment that `IGNORE_PATTERNS` was for passthrough, whereas it does actually exclude files from the build.

This expands to an array of patterns and covers other non-prod files.

Changelog: Internal

Reviewed By: huntie

Differential Revision: D111232823

fbshipit-source-id: 0e994803d2f5b036e4208686d28775a780ea442f

Adapted for 0.84.x: Uses micromatch, as 0.84.x's build script predates the switch to path.matchesGlob.

(cherry picked from commit bbdf032)
Summary:
Pull Request resolved: #1770

X-link: react/react-native#57488

The generated Flow libdef for `babel/types` declared `duplicates` as a required parameter on `getBindingIdentifiers` and `getOuterBindingIdentifiers`.

It is actually optional, both at runtime (only read as a truthy flag, defaulting to the non-duplicate map) and in the upstream TS types:

https://github.com/babel/babel/blob/7.x/packages/babel-types/src/retrievers/getBindingIdentifiers.ts#L17-L36

This prevented legitimate single-argument use later in this stack. Separating it for export to RN + Metro.

Changelog: [Internal]

Reviewed By: GijsWeterings

Differential Revision: D111056985

fbshipit-source-id: f3f07617e2fb010b08e83d7d012800a884f5bd15

(cherry picked from commit 25e322a)
Summary:
Pull Request resolved: #1768

Add static import/export plugin (`experimentalImportSupport`) support for namespace re-exports, i.e. `export * as Name from "module"`.

This was added to Expo's fork in expo/expo#38055, (h/t krystofwoldrich).

Despite this being perfectly valid syntax it currently fails to transform under `experimantalImportSupport`, with:

```
Cannot read properties of undefined (reading 'name')
    at <..>/metro-transform-plugins/src/import-export-plugin.js:289:67
```

(On access of `local.name`, because this type of export has no `local` binding)

## Spec

ECMA-262 includes namespace re-export syntax in the ExportDeclaration grammar: https://tc39.es/ecma262/#prod-ExportDeclaration

## Implementation

Metro's existing `experimentalImportSupport` transform already has namespace import helper plumbing via `importAll`. This change reuses that helper and then emits an explicit export assignment for the namespace object, keeping the change isolated to the static transform path.

```
 - **[Experimental]** Support `export * as Name from "module"` under `experimentalImportSupport`
```

Reviewed By: huntie

Differential Revision: D111013905

fbshipit-source-id: 101c89427b04263e641c769f4ed4c6641a18af09

(cherry picked from commit 59a1072)
…est spread (#1772)

Summary:
Pull Request resolved: #1772

Fixes #1460

Fixes two related bugs in Metro's `experimentalImportSupport`.

## Exports with inline relabelling

```js
export const { A: B } = {};
```

**Currently transforms to**

```js
const { A: B } = {};
exports.A = A;
```

Which is invalid: `A` is a reference error.

**After this diff**

```js
const { A: B } = {};
exports.A = B;
```

## Exports with spreads

Currently both of these fail to transform:

**Object spread**
```js
export const {A, ...others} = {A: 'A', B: 'B', C: 'C'};
```

Plugin throws `TypeError: Cannot read properties of undefined (reading 'name')` when attempting to iterate over the object for key: val pairs.

**Array spread**
```js
export const [A, ...others] = ['A', 'B', 'C'];
```

Plugin throws `Property name expected type of string but got undefined` after extracting an `undefined` name and then attempting to use it at as prop of `exports`.

**After this diff**

```js
const {A, ...others} = {A: 'A', B: 'B', C: 'C'};
exports.A = A;
exports.others = others;
```
and

```js
const [A, ...others] = ['A', 'B', 'C'];
exports.A = A;
exports.others = others;
```

## Spec
Almost goes without saying in this case - exported variable declarations should expose the declaration's bound names. ECMA-262 defines those names through Static Semantics: `BoundNames`, including `BindingPattern` forms: https://tc39.es/ecma262/#sec-static-semantics-boundnames

## Implementation

Metro had naive `ObjectPattern`/`ArrayPattern` extraction that read object keys or array elements directly.

This fixes the same issues Expo did in [#38058](expo/expo#38058) and [#38118](expo/expo#38118), but uses Babel's own `getBindingIdentifiers` for a simpler implementation.

## Changelog
```
 - **[Experimental]**: experimentalImportSupport: Fix exports renamed by destructuring, and support exports of rest spreads.
```

Reviewed By: GijsWeterings, huntie

Differential Revision: D111013897

fbshipit-source-id: 6f0b0e135f5014006d2d1b0851ae26903e21355b

(cherry picked from commit 0dc5030)
…it exports, fix precedence of `export default` > `export * from` (#1773)

Summary:
Pull Request resolved: #1773

Fixes a bug in `experimentalImportSupport` where `export * from 'foo'` would incorrectly overwrite an `export default`, if `'foo'` also has a default export.

# Example
## Input source

```js
// foo.js  (the star source)
export default 'star default';
export const overridden = 'star named';
export const sourceOnly = 'source only';
```

```js
// entry.js
export * from 'foo';
export const overridden = 'explicit named';
export default 'explicit default';
```

Per the ES spec, importing from `entry.js` should give `overridden === 'explicit named'`, `default === 'explicit default'`, and `sourceOnly === 'source only'`. Explicit exports take precedence over names supplied via `export *`, regardless of the position of export declarations.

## Before

```js
'use strict';

Object.defineProperty(exports, '__esModule', {value: true});

var _overridden = 'explicit named';
var _default = 'explicit default';

exports.overridden = _overridden;    // 'explicit named'
exports.default = _default;          // 'explicit default'

var _foo = require('foo');
for (var _key in _foo) {             // star copy runs LAST...
  exports[_key] = _foo[_key];        // ...clobbers the explicit exports
}
```

Runtime result (wrong):

```js
exports.overridden // 'star named'     ❌ should be 'explicit named'
exports.default    // 'star default'   ❌ should be 'explicit default'
exports.sourceOnly // 'source only'    ✅
```

## After

```js
'use strict';

Object.defineProperty(exports, '__esModule', {value: true});

var _overridden = 'explicit named';
var _default = 'explicit default';

var _foo = require('foo');
for (var _key in _foo) {             // star copy runs FIRST
  exports[_key] = _foo[_key];
}

exports.overridden = _overridden;    // explicit exports now win
exports.default = _default;
```

Runtime result (spec-correct):

```js
exports.overridden // 'explicit named'    ✅
exports.default    // 'explicit default'  ✅
exports.sourceOnly // 'source only'       ✅
```

# Spec
[ECMA-262 Source Text Module Record](https://tc39.es/ecma262/#sec-source-text-module-records-resolveexport-exportname-resolve-set) `ResolveExport` checks local `ExportEntries` and indirect `ExportEntries` before considering `StarExportEntries`, so explicit exports take precedence over names supplied through `export *`.

# Provenance
This is part of the change made in Expo's fork in [#38111](expo/expo#38111) (though that's mainly concerned with introducing live bindings), so this is "in the wild" already for Expo users.

# Implementation
This is just a straightforward ordering swap of the emitted assignments.

# Changelog
```
 - **[Experimental]**: `experimentalImportSupport` - fix precedence of `export default` over `export * from`.
```

Reviewed By: huntie

Differential Revision: D111013892

fbshipit-source-id: 937f7aabdbe6760425072153f2093fe4e45a37e3

(cherry picked from commit e6b154f)
Summary:
Pull Request resolved: #1774

In ESM, static imports (including `export * from`) must be evaluated before the current module's body.

Metro's `import-export-plugin` currently evaluates named re-exports at their declared position within the body (by turning them into a CJS `require` at that posiiton), and `export *` after the body - both are incorrect.

Move static re-export helper requires and export-star copies into the import prelude instead of leaving them at the original export declaration location or after the module body.

## `inlineRequires`

Under `inlineRequires`, which runs after this plugin, evaluation of imported modules may be deferred back to where they were before this diff - contrary to the spec but by design for startup performance.

## Spec

- Source Text Module Records collect imported and re-exported module specifiers as requested modules: https://tc39.es/ecma262/#sec-source-text-module-records
- ECMA-262 `InnerModuleEvaluation` evaluates requested modules before executing the current module body: https://tc39.es/ecma262/#sec-innermoduleevaluation

## Expo

Along with the previous diff, this is another change introduced in Expo last year in [#38111](expo/expo#38111), on by default for Expo apps.

## Changelog

```javascript
  - **[Experimental]**: `experimentalImportSupport`: Move `export from` imports to be evaluated at module prelude, rather than within/after module body
```

Reviewed By: huntie

Differential Revision: D111013896

fbshipit-source-id: 069438ab66a206471fc9f948efc924ce881c8d6d

(cherry picked from commit 27a5adc)
…ng the default export (#1777)

Summary:
Pull request resolved: #1777

Fix a spec conformance bug in `export * from 'foo'` under `experimentalImportSupport` (`import-export-plugin`).

Currently, Metro re-exports all exports including the source module's default export - the default export should be excluded.

## Spec
https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-getexportednames `8.d.i`

```
  8. For each ExportEntry Record exportEntry of module.[[StarExportEntries]], do
     a. Assert: exportEntry.[[ModuleRequest]] is not null.
     b. Let requestedModule be GetImportedModule(module, exportEntry.[[ModuleRequest]]).
     c. Let starNames be requestedModule.GetExportedNames(exportStarSet).
     d. For each element name of starNames, do
->      i. If name is not "default", then
           1. If exportedNames does not contain name, then
              a. Append name to exportedNames.
```

Node.js, Babel's `transform-modules-commonjs`, and browsers all conform to this spec.

## Fix
Trivially `continue` through a key named "default" in our `exportAllTemplate`.

NOTE: `export * as ns from 'foo'` is unaffected: it goes through the `ExportNamespaceSpecifier` / `importAll` path, and per spec the namespace object still exposes the source's default.

## Possible alternatives + benchmarks

Compiled release HBC under SH trunk (`hermesc -O -emit-binary`) and run in the `hermes` VM.
 - One "op" = one full re-export of a module with 20 named exports + `default` (where noted).
 - Throughput = median of 9 runs × 250k ops.
 - Bytecode = marginal HBC size of the isolated `f(exports, REQUIRED)` body vs an empty-body baseline.

| Form | Input | Throughput (ops/s) | stddev | rel | HBC bytes (Δ) |
|---|---|--:|--:|--:|--:|
| `for-in`, no `default` guard (re-exports default) | 20 + `default` | 99,602 | 1,041 | 0.95× | +96 |
| `for-in` + `if (KEY === "default") continue;` (current) | 20 + `default` | 104,778 | 1,025 | 1.00× | +126 |
| `for-in` + `if (KEY !== "default") { ... }` | 20 + `default` | 104,515 | 879 | 1.00× | +127 |
| object spread + `delete default` | 20 + `default` | 91,709 | 583 | 0.88× | +148 |
| `Object.keys` + indexed for-loop | 20 + `default` | 90,351 | 672 | 0.86× | +206 |
| `Object.getOwnPropertyNames` + for-loop | 20 + `default` | 89,831 | 680 | 0.86× | +224 |
| `for-in` + `hasOwnProperty` guard | 20 + `default` | 78,939 | 862 | 0.75× | +239 |
| `Object.keys().forEach` | 20 + `default` | 73,746 | 346 | 0.70× | +273 |
| `Object.assign` + `delete default` | 20 + `default` | 61,395 | 567 | 0.59× | +139 |
| `for-in` + `default` guard (guard is pure overhead) | 20, **no** `default` | 104,080 | 538 | 0.99× | +126 |
| `for-in`, no guard | 20, **no** `default` | 104,866 | 634 | 1.00× | +96 |

**Conclusion:** both `for-in` guard styles are fastest and smallest of all
semantically-equivalent forms (differing by ~3% throughput / 1 byte — within
noise).

- The `default` guard's runtime effect flips with the input:
  - **default present:** guard is **+5.2%** faster (104,778 vs 99,602) — skipping the
    `default` property write outweighs the per-key comparisons.
  - **default absent:** guard is **−0.7%** slower (104,080 vs 104,866) — pure overhead,
    nothing to skip.
  - The guard is order of magnitudes cheaper than prop assignment, adding it has negligible effect.
 - Adding the guard adds 5 bytes of HBC per occurrence (plus one-time 16 bytes for the "default" string table entry - which is there in a real app anyway).

## Changelog
```
 - **[Experimental]** experimentalImportSupport: Fix `export * from` incorrectly re-exporting default exports
```

Reviewed By: huntie

Differential Revision: D111910097

fbshipit-source-id: 5c93c35fa8fd17470bed14caae9f95347fccf66d

(cherry picked from commit 11ad083)
Summary:
I need to double-check this every time, so use some clearer labelling in opaque types + names + comments, where it's API neutral.

Changelog: Internal

Reviewed By: huntie

Differential Revision: D114616012

fbshipit-source-id: a615e4467ba249f5a815d527457cd88407884bd9

(cherry picked from commit 42a6502)
Summary:
Fast Refresh's dependency walk (`metroHotUpdateModule` in the require polyfill) assumes every module reachable through `inverseDependencies` is already present in the client module registry as an (uninitialised) `ModuleDefinition`.

This is true for single-segment "plain" bundles, but doesn't hold for lazily defined segments. Currently these are only used in non-`dev` builds so Fast Refresh doesn't need to care about them, but in the spirit of bringing dev and prod as close together as possible, and unforking serialiser implementations in the process, we want to expose segmented bundles in dev.

Fortunately it's quite easy - the pieces we need (`definingSegmentByModuleID`, `moduleDefinersBySegmentID`) are already in the runtime. We only need to define (note, not initialise/evalaute - we're not running user code) them, just in time.

This extra step - `ensureModuleRegistered` - is entirely contained within the Fast Refresh pass so there's no prod impact at all, and it's a no-op for plain (eager) bundles.

Changelog:
```
 - **[Fix]** Fix Fast Refresh hitting undefined modules when using lazy (segmented/switch) bundles in dev
```

Reviewed By: vzaidman

Differential Revision: D113665912

fbshipit-source-id: c648c0457d33b468ae73402b1a5c85b333bd2eaa

(cherry picked from commit c70d0ae)
…fault

Summary:
`ci-info` was a direct runtime dependency of `metro` with exactly one use: choosing the default value of `watch` in `createFileMap`. Replace it with a local `isCIEnv()` helper reading `process.env.CI`, and drop the dependency along with its now-unused Flow libdef.

This also fixes an opt-out bug. Metro pinned `ci-info@^2.0.0`, whose `isCI` is a plain truthiness check:

```
exports.isCI = !!(env.CI || env.CONTINUOUS_INTEGRATION || env.BUILD_NUMBER || env.RUN_ID || exports.name || false)
```

so `CI=false` and `CI=0` were both treated as "in CI" — the opposite of what someone setting them intends. `ci-info@3` later added an `env.CI !== 'false'` bypass for precisely this reason. `isCIEnv()` adopts the v3 semantics (and additionally honours `CI=0`, another conventional falsey spelling) without taking a major upgrade of a dependency we no longer want.

The tradeoff is narrower CI detection: we no longer infer CI from `CONTINUOUS_INTEGRATION`, `BUILD_NUMBER`, `RUN_ID`, or ci-info's `vendors.json` list, so a CI that does not export `CI` (Jenkins, TeamCity) now defaults to `watch: true`. That is acceptable here because:

- every mainstream CI (GitHub Actions, CircleCI, GitLab CI, Buildkite, Travis) sets `CI=true` — that is the convention this variable exists for;
- this is only a *default*. `watch` is an explicit option on `runServer` / `DependencyGraph` / `createFileMap`, and `runBuild` already passes `watch: false`;
- the cost of a miss is a filesystem watcher we did not need in a `metro serve` process, not incorrect output.

`ci-info@3` remains in the lockfiles as a transitive dev dependency of Jest.

Reviewed By: vzaidman

Differential Revision: D116225836

fbshipit-source-id: f8d1a7a6cfc98face0ad99255ad63556a7e27f2e

(cherry picked from commit 61e4592)
…lt on via `@react-native/metro-babel-transformer`)

Summary:
X-link: react/react-native#57973

`react-native/babel-preset` inlines `Platform.OS` and `Platform.select(...)` whenever it is handed a `platform` (since yesterday - react/react-native#57848 ), but this is over-zealous - `platform` is a pre-existing transform option whose intent is to set the transform target. In some cases (eg, to allow `Platform` mocking from tests), we don't want `Platform` inlining, even though `platform` may already be passed for other reasons.

Gate the inlining behind a separate `inlinePlatform` opt-in, resolved as `options.inlinePlatform ?? babel.caller(...) ?? false`, mirroring how `platform` and `unstable_transformProfile` are already resolved. Metro already models this as a distinct transform option, so both Babel transformers pass it straight through. The caller channel covers the case where the preset is named in a `babel.config.js` and so receives no preset options at all.

Bundling is unaffected: Metro sets `inlinePlatform` on every transform, so `Platform` continues to be inlined through `react-native/metro-babel-transformer`.

Changelog:
[General][Changed] - `Platform.OS` and `Platform.select(...)` inlining in `react-native/babel-preset` now requires the `inlinePlatform` option in addition to `platform`

Reviewed By: GijsWeterings

Differential Revision: D116281656

fbshipit-source-id: 0230cd74f38661862396391da62cf81b34773b85

(cherry picked from commit 6bbe095)
Summary:
WARNING: Generated by Autopilot (alpha) — review carefully, verify the underlying claim before accepting.
Agent: React Native Oncall Tests Fixer | Trajectory: https://www.internalfb.com/intern/devai/devmate/inspector/7753ac91-3b3f-43d3-a8aa-d9e336e50d0d/ | SC job: https://www.internalfb.com/intern/sandcastle/instance/67553994997036426/

 ---

The `react_native` Metro remote-cache warmer (`//tools/skycastle/workflows2/react_native/fb-metro-cache-warmer.sky:main`, test issue 237151770) has been failing at a 25-46% rate. The failing shard's log shows the actual cause once, at the top, followed by a long tail of consequences:

```
Uncaught exception: Error: read ECONNRESET
    at TLSWrap.onStreamRead (node:internal/stream_base_commons:216:20)
Reporting error to the React Native team.
...
38 of 112 failures exceeds tolerance of 10%. Failing job.
FetchError: request to http://localhost:37907/fb-metro-cache-stats failed, reason: connect ECONNREFUSED ::1:37907
```

`HttpStore.#setOnce` attaches `req.on('timeout')` but — unlike `#getOnce` — never attaches `req.on('error')`. A Node `ClientRequest` that emits `'error'` with no listener throws it as an uncaught exception. Because the event is emitted asynchronously, it escapes the `new Promise` executor entirely, so `reject` is never called: `Cache.set`'s try/catch cannot see it and it lands in the CLI's `uncaughtException` handler, which calls `process.exit(1)`.

That kills the Metro server the warmer spawned. Every bundle still queued then fails with `connect ECONNREFUSED` against the now-dead local server, which is why a single socket reset produces dozens of bundle failures and trips the 10% tolerance. The `ECONNREFUSED` tail is the symptom, not the cause — it accounts for 114 of the 177 recent failed runs.

The reset itself is expected and benign: writes go to Manifold over TLS through a `keepAlive` agent with `maxSockets: 64`, so a pooled socket being reaped server-side races the next PUT. `setOptions` already specifies `retryNetworkErrors: true, maxAttempts: 3` for exactly this, but that retry path was dead for socket errors — a failure that never rejects can never be retried.

Attach `req.on('error')` in `#setOnce`, mirroring `#getOnce`, rejecting with a `NetworkError` so `#withRetries` can retry it. Also attach `gzip.on('error')`: `gzip.pipe(req)` does not forward errors, so a destroyed request mid-upload can leave the upload stream emitting unhandled `'error'` too.

This also restores the warmer's documented intent — `fill-http-cache` already treats `cache_write_error` as non-fatal ("Cache warming is best-effort") — which the uncaught path was defeating, since the error never became a `cache_write_error` event.

Reviewed By: javache

Differential Revision: D116631955

fbshipit-source-id: b80baceb2b812d5f0b885175299233c73a5cd637

Adapted for 0.84.x: Tests mock `http`, not `node:http`.

(cherry picked from commit fad3b88)
Summary:
Metro replaces `Platform.OS` with a string literal unless it is the direct left side of an assignment. Update/delete expressions, destructuring assignment targets, and direct or nested `for...in` / `for...of` targets can therefore be changed semantically or make Babel abort with an invalid literal assignment target.

Walk the member expression's ancestors and preserve it whenever the containing expression is the left side of an assignment or loop, or the argument of an update/delete operation. The React Native Babel preset has a parallel transform; companion [React Native PR #58252](react/react-native#58252) applies the same guard there.

Changelog:
```
 - **[Fix]**: Preserve `Platform.OS` write targets during constant inlining
```

Pull Request resolved: #1890

Test Plan:
- Added one combined regression for update/delete, array/object destructuring assignments, and direct/nested `for...in` and `for...of` targets.
- Exact baseline aborts on the direct `for...of` target; the fixed suite preserves all eight forms.
- All `metro-transform-plugins` suites pass: 6/6 suites, 160/160 tests, 41 snapshots.
- Fresh full Flow check reports 0 errors.
- Full monorepo build passes for all 17 packages.
- Targeted ESLint, Prettier, and `git diff --check` pass.

No public API or intended read-transform behavior changes.

Reviewed By: GijsWeterings

Differential Revision: D118440964

Pulled By: vzaidman

fbshipit-source-id: eb4a934c1c8d469618af290356377555bcca6733

(cherry picked from commit b15a67b)
)

Summary:
Metro's dev server sets `Content-Type` with `mime.lookup()`, both for `/assets/` and for source files under `/[metro-project]/`. That returns a bare type like `text/html` with no charset, so clients fall back to their own default - #859 is a `WebView` rendering a UTF-8 `.html` asset as mojibake.

`mime-types` has `contentType()` for this. It appends `; charset=utf-8` where a charset applies and is otherwise identical, including returning `false` for an unrecognised extension, so binary assets are unaffected.

Fixes: #859

Changelog:
```
 - **[Fix]**: Include `charset=utf-8` in the `Content-Type` of text assets and source files served by the dev server
```

Pull Request resolved: #1888

Test Plan:
```
yarn jest packages/metro/src/Server
yarn flow check
```

New cases for a text asset, a source file and a binary file. The charset assertions fail on `main`.

Reviewed By: GijsWeterings

Differential Revision: D118437775

Pulled By: vzaidman

fbshipit-source-id: 57a7c5809fa8638f33025b96a4aa1c90f8795b12

(cherry picked from commit eeb7934)
Summary:
Metro statically replaces `Platform.select({...})` for the target platform. Its property scan currently stops at the first matching key, while JavaScript object-literal evaluation keeps the last duplicate definition. For example, `Platform.select({ios: 1, ios: 2})` runs as `2` but Metro compiles it to `1`.

Scan the already-validated static properties from the end and return the first reverse match. This preserves O(n) behavior, avoids an unnecessary nullable temporary, and keeps compiled output consistent with runtime object semantics. The React Native Babel preset contains a parallel transform; companion [React Native PR #58249](react/react-native#58249) applies the same rule there.

Changelog:
```
 - **[Fix]**: Inline the last duplicate key from static `Platform.select` object literals
```

Pull Request resolved: #1889

Test Plan:
- Added a focused `metro-transform-plugins` regression; it fails on pristine main with `1` and passes with `2` after the fix.
- All package suites pass: 6/6 suites, 160/160 tests, 41 snapshots.
- Full Flow check reports no errors.
- Full monorepo build passes for all 17 packages.
- Targeted ESLint, Prettier, and `git diff --check` pass.

No behavior changes for object literals without duplicate static keys.

Reviewed By: GijsWeterings

Differential Revision: D118499357

Pulled By: vzaidman

fbshipit-source-id: 1036c78826c4e7d12a94baa8b8eeb1a04f9f8f60

(cherry picked from commit c7bd299)
…ce RSS+heap (#1906)

Summary:
`FallbackWatcher`'s `recReaddir` is the only consumer of [`walker`](https://www.npmjs.com/package/walker) (unmaintained since 2021, three deps) in Metro. `walker` schedules an `lstat` for every entry the moment the parent directory is read, so a crawl has as many in-flight `fs` requests. Those requests queue against a libuv thread pool of 4, so the concurrency buys nothing and the queued request state is pure memory overhead. Note - I *don't* believe this is responsible for `EMFILE: Too many open files`, as libuv's limits should prevent that.

This diff replaces it with a plain `async` `recReaddir` - a depth-first stack drained by a pool of at most 32 concurrent visits. The syscalls are identical, one `lstat` per entry and one `readdir` per directory, and so are the filters: `ignored` is still evaluated against a directory before it's descended into or reported. `endCallback` goes away in favour of awaiting, and `normalizeProxy` also goes because `path.join` already normalises.

The win32 settling delay is preserved and moves up into `startWatching`, which is the only place it was used.

Crawl of this repo's `node_modules`, 33,394 entries (3,160 directories, 30,054 files, 180 symlinks), median of 5 runs, macOS 25.6 on APFS, node 22.13.1:

| variant | wall (ms) | RSS (MB) | peak heap (MB) | max in-flight `readdir` |
|---|---|---|---|---|
| `walker` | 107.2 | 102.8 | 21.7 | 818 |
| bounded, 16 | 107.4 | 66.3 | 11.4 | 16 |
| bounded, 32 | 103.9 | 67.5 | 12.8 | 32 |
| bounded, 64 | 100.9 | 71.8 | 14.5 | 64 |
| bounded, 256 | 97.2 | 85.4 | 20.2 | 239 |

Both report the same 3,160/30,054/180 and issue the same number of syscalls. Wall time is constrained by the libuv pool concurrency, so 32 is chosen for the memory: **-34% RSS and -41% peak heap** against `walker`.

Changelog: Internal

Pull Request resolved: #1906

Test Plan:
- `watchers/__tests__/integration-test.js` on all supported OSes, which drives `FallbackWatcher` against a real temporary tree - new/changed/deleted files, symlinks to a file, to a directory and to a non-existent target, pre-existing files, a directory moved in from outside the watch root, a directory moved out, and deletion of a directory with files under it.

```
yarn jest packages/metro-file-map
yarn jest packages/metro/src/DeltaBundler/__tests__/resolver-test.js
yarn flow check
yarn lint
```

Reviewed By: GijsWeterings

Differential Revision: D119076649

Pulled By: vzaidman

fbshipit-source-id: f85fd675e37d8c4360d8ee2fef8e48cfdb952e81

Adapted for 0.84.x: Node.js builtins imported without the `node:` scheme.

(cherry picked from commit 3173de1)
…nux + Windows) watcher (#1907)

Summary:
`FallbackWatcher` starts an `fs.watch` on each directory from `recReaddir`'s `dirCallback`, and the crawl calls that after `readdir` has returned.

Anything written into a directory *between the two* appears in neither the listing nor the watch, so it stays invisible until the next full crawl. There's no recovery on Linux or Windows.

This is expo/expo#48950 - `npx expo install` against a running dev server leaves the new modules unresolvable until restart, which is a lot more painful than it sounds now that agents routinely run Metro in a VM. brentvatne diagnosed it in expo/expo#49363, with a fix in Expo's fork that we can hopefully replace with this one.

Now that we own the crawl (#1906), the fix is very simply to call `dirCallback` before `readdir` instead of after, which also moves it inside `recReaddir`'s existing `try`.

Separate issues not fixed here, both pre-existing and both covered by expo/expo#49363 :

- An `fs.watch` that emits `error` is never removed from `#watched`. Node emits no `close` after `error`, so the path can never be re-watched, and `#stopWatching` waits on a `close` that will not arrive.
- On win32, `fs.watch` can report an event with no filename. `#detectChangedFile` drops it when `#dirRegistry[dir]` is empty, which is exactly the state a newly watched directory is in.

Changelog:
```
 - **[Fix]**: `FallbackWatcher` no longer misses files written to a directory while it is being crawled
```

Pull Request resolved: #1907

Test Plan:
```
yarn jest packages/metro-file-map
yarn flow check
yarn lint
```

New `watchers/__tests__/FallbackWatcher-test.js` asserts the ordering directly, that `fs.watch` precedes `readdir` for the same directory, on the initial crawl and on a directory created while watching, plus that a directory whose `fs.watch` throws is skipped without failing the crawl. All three fail on the parent commit and pass here.

The race can't be asserted behaviourally on macOS, because FSEvents delivers with a latency window and a watch started immediately after a write still reports it:

```
$ node -e "fs.writeFileSync(d+'/raced.js', ''); fs.watch(d, (e, f) => console.log(e, f))"
rename raced.js
```

That's why the issue is Linux/Windows only, and why the assertion is on the ordering rather than on a missed event. Metro has no Windows coverage for this backend, so the win32 path is unexercised either way.

Reviewed By: motiz88, javache

Differential Revision: D119187785

Pulled By: vzaidman

fbshipit-source-id: 5b69c83db65fa546a7f4ead5e4fb75b112cad9b9

(cherry picked from commit f63b251)
…#1917)

Summary:
## Context

When Metro sees `require('./imgs/logo.png')` in userland source it synthesises a JS module representing `./imgs/logo.png` that encodes includes everything needed to _find_ the asset, roughly:

```
{name: "logo", ext: "png", scales: [1, 2], hash: "abc123", httpServerLocation: "/assets/imgs"}
```

In a dev server build, the client rebuilds a URL from this information and asks Metro for the file:

```
http://localhost:8081/assets/imgs/logo@2x.png?platform=ios&hash=abc123
```

In a "release" build (more correctly - any CLI-driven build not served from the dev server, but this is typically used for (and only for) release builds), typically, frameworks use Metro's `saveAssets`, which copies each file to a **path derived from `httpServerLocation`** (even though no http server is used) at build time:

```
iOS:      /assets/imgs  ->  assets/imgs/logo.png, assets/imgs/logo@2x.png
Android:  /assets/imgs  ->  drawable-mdpi/imgs_logo.png, drawable-xhdpi/imgs_logo.png
```

and at runtime `AssetSourceResolver` derives the lookup path from `httpServerLocation`, by the same rules. **Both halves have to agree.**

`httpServerLocation` is computed twice, from two different roots. The transformer computes it per module relative to `projectRoot` and bakes it into the bundle - that's what the client uses to find an asset at runtime, in both dev and prod. The `getAssets` serialiser (for prod) computes it again over the whole graph relative to `unstable_serverRoot`, and that's what `saveAssets` uses to place files for `--assets-dest`.

So if `unstable_serverRoot` is set, `Server.getAssets()` and `Server.build(<...>, {withAssets: true})` result in asset paths that do not match the JS bundle. Eg, for `unstable_serverRoot` set to a workspace root, and `projectRoot` set to the subdir `app`, the release build writes (conceptually) `assets/app/assets/logo.png` but the runtime looks for `assets/assets/logo.png`.

Dev is unaffected, because there both halves of the lookup come from the transformer's copy.

This dates to the original `unstable_serverRoot` change, which applied the server root broadly across `Server` and took this call site with it while both readers stayed on `projectRoot`. It was obviously never really tested - `unstable_serverRoot` is/was used at Meta but the release assets pipeline via Metro-Buck is completely different.

The main motivation for this now is *not* to make it easier to use `unstable_serverRoot`, which probably should not exist, but to make asset path calculation consistent, allowing us to build on that to **fix assets outside `projectRoot`** - that's the follow up #1892.

Changelog:
```
 - **[Experimental]**: Fix mismatched asset paths in (release) builds under `unstable_serverRoot`, make them always relative to `projectRoot`.
```

## Expo

No immediate impact. Expo is the main setter of `unstable_serverRoot` - `expo/metro-config` points it at the workspace root. Expo already applies exactly this fix in our fork of the assets path - expo/expo#25312, since 2023.

So this fix in core removes one of the reasons Expo's fork has to exist.

## Community CLI

No impact - `unstable_serverRoot` (thankfully) remains completely unmentioned in RN or RNC/CLI.

Pull Request resolved: #1917

Test Plan:
### Unit

New test pins `_getAssetsFromDependencies` to `projectRoot` with `unstable_serverRoot` set to `/`. Restoring `_getServerRootDir()` fails it:

```console
$ yarn jest packages/metro/src/Server
  ✕ anchors asset URLs on projectRoot, not unstable_serverRoot
Tests: 1 failed, 83 passed, 84 total
```

### End to end

Built a React Native 0.87.1 app in a Yarn workspace, with an asset directory peer to the app root and `server.unstable_serverRoot` pointed at the workspace root:

```text
workspace/
├── media/          peer-logo.png, peer-logo@2x.png, peer-logo.ios.png, peer-logo@2x.ios.png
└── app/            projectRoot; watchFolders: [workspaceRoot]
    └── assets/     in-project.png, in-project@2x.png, in-project.ios.png, in-project@2x.ios.png
```

`react-native bundle --platform ios --dev false --entry-file app/index.js --assets-dest …` on `main` produces the four-way mismatch shown under "The problem" above: the JS bundle carries `/assets/assets` and `/assets/../media` from the transformer, while the copied files land under `assets/app/assets/` and `assets/media/` from `getAssets`.

Running `Metro.runBuild` with `assets: true` against the same config, at each revision, and passing the resulting `AssetData` through React Native 0.87.1's own `getAssetDestPathIOS` / `getAssetDestPathAndroid` / `filterPlatformAssetScales` - the three functions `saveAssets` uses to implement `--assets-dest`:

```text
                       getAssets says        destination
main                   /assets/app/assets    assets/app/assets/in-project.png
                       /assets/media         assets/media/peer-logo.png
this PR                /assets/assets        assets/assets/in-project.png
                       /assets/../media      assets/_media/peer-logo.png
```

Comparing each against what the runtime derives from the bundle's copy:

```text
in-project   main      iOS MISMATCH   Android MISMATCH
             this PR   iOS MATCH      Android MATCH
peer-logo    main      iOS MISMATCH   Android MISMATCH
             this PR   iOS MATCH      Android MATCH
```

The peer asset matches without the `[metro-watchFolders]` change stacked on top, because `getAssetDestPathIOS` and `scaledAssetURLNearBundle` apply the same `../` to `_` rewrite independently. An ugly filename, but a working image.

The "this PR" destinations are computed from the verified `getAssets` output fed through React Native's real helpers, rather than from a `react-native bundle` run against a patched Metro.

Against a running dev server, the URLs `main` produces do not resolve at the endpoint meant to serve them, and this PR's do:

```console
$ curl -o /dev/null -w '%{http_code}' '/assets/app/assets/in-project.png?platform=ios'   # main
404
$ curl -o /dev/null -w '%{http_code}' '/assets/media/peer-logo.png?platform=ios'         # main
404
$ curl -o /dev/null -w '%{http_code}' '/assets/assets/in-project.png?platform=ios'       # this PR
200
$ curl -o /dev/null -w '%{http_code}' '/assets/../media/peer-logo.png?platform=ios'      # this PR
200
```

### Checks

Run on this commit alone, not on the stack:

```console
$ yarn jest packages/metro/src
Test Suites: 61 passed, 61 total
Tests:       1 skipped, 869 passed, 870 total

$ yarn flow check
$ yarn typecheck-ts
$ yarn verify-api-snapshots
$ yarn lint
```

Reviewed By: GijsWeterings

Differential Revision: D120129779

Pulled By: vzaidman

fbshipit-source-id: 19f1dbc5e9be35f89e4a034b6e43b2ee143bf736

(cherry picked from commit 37c767e)
Boulea7 and others added 16 commits October 15, 2026 07:55
* Propagate transformer initialization errors

* Type Bundler-test with Flow and ESM imports

Adds `@flow strict-local` to the new `Bundler-test.js` and switches its `require` calls to ESM imports, matching neighboring tests such as `HmrServer-test.js`.

Changelog: [Internal]

---------

Co-authored-by: Rob Hogan <2590098+robhogan@users.noreply.github.com>

(cherry picked from commit 1f83bfc)
…formOptions (#1757)

* Fix RAM bundle passing the deps function (not its result) to getTransformOptions

getRamBundleInfo gives custom getTransformOptions a callback to look up a
module's transitive dependencies, but it called `Array.from(getDependencies)`
on the function itself instead of `Array.from(getDependencies(x))`. Since
the closure has arity 1, Array.from treated it as array-like and returned
[undefined], so any getTransformOptions relying on this callback received
garbage regardless of the file queried.

Invoke getDependencies(x). Existing tests never exercised the callback,
which masked this.

Adds a regression test that calls the callback and asserts the real
transitive deps (fails before / passes after).

* Fix Flow error in the new getRamBundleInfo regression test

`resolvedDeps` was declared without a type or initialiser, so spreading it fails `flow check` with "possibly uninitialized variable is incompatible with $Iterable".

Annotate it and assert it was assigned before spreading, which also gives a clearer failure if `getTransformOptions` is never called with a dependency lookup.

Changelog: [Internal]

* Remove stale $FlowFixMe above the getTransformOptions dependency callback

The suppression dates to Flow v0.99 in 2019 and no longer suppresses anything: `flow check` reports no error on this line with it removed, either with the fix in this stack or with the original `Array.from(getDependencies)`.

Worth noting Flow did not catch this bug in either form, for the same reason it was silent at runtime - `Array.from` accepts array-likes, and a function satisfies that via `.length`.

Changelog: [Internal]

* Apply Prettier formatting to the regression test

The guard added in the previous commit exceeded the print width, failing `prettier --check` and so `yarn lint`.

Changelog: [Internal]

* Share base options across the getRamBundleInfo tests

Each test repeated the full serializer options object. Extract it into `baseOptions` so each test overrides only what it exercises.

---------

Co-authored-by: Rob Hogan <2590098+robhogan@users.noreply.github.com>

(cherry picked from commit bd4534d)
…flaky integration tests (#1974)

Adapted for 0.84.x: Resolved a conflict with the `Promise<void>` annotation added in 0.84.6.

(cherry picked from commit 88f95b0)
Adapted for 0.84.x: Kept the `Promise<void>` return annotation added in 0.84.6.

(cherry picked from commit 8751591)
`MappingsConsumer` decoded the VLQ of every mapping twice - once through its cache and once more with the result discarded - since the consumer was added in ccd508c. This removes the second call.

`composeSourceMaps` with a bundle map and its Hermes map is 13.6% faster (95% CI 13.3-13.8%; 2,088ms -> 1,801ms on our benchmark app*), with byte-identical output. Composition memory is within 0.2% (-1MB*).

| | Time (median) | vs previous | vs `main` | Composition memory* | vs previous | vs `main` | Peak RSS |
|---|---|---|---|---|---|---|---|
| `main` | 2,088ms (2,082 to 2,094) | | | 1,070MB (1,070 to 1,070) | | | 1,664MB |
| This diff | 1,801ms (1,794 to 1,806) | -13.6% (-13.8 to -13.3) | -13.6% (-13.8 to -13.3) | 1,069MB (1,069 to 1,069) | -0.1% (-0.2 to -0.1) | -0.1% (-0.2 to -0.1) | 1,663MB |

## Changelog

```
 - **[Performance]**: Don't decode source map segments twice in `metro-source-map`'s `Consumer`
```

## Test plan

```
yarn jest packages/metro-source-map packages/metro-symbolicate
yarn flow check
```

\* Benchmark: Mattermost Mobile 2.45.0 (React Native 0.83.9, Metro 0.83.7), iOS release bundle, unminified: 7,086 sources, 52.6MB bundle, 82.4MB flat source map. Composed with its `hermesc -O -output-source-map` map (hermes-compiler 0.14.1; 10.7MB, 1.84M segments). Timings are `composeSourceMaps` alone, excluding JSON parsing, over 105 rounds on Node 22.13.1 on an M5 Pro; each round runs `main`, every diff in this stack and a memory baseline, in a shuffled order. Figures are medians with 95% bootstrap confidence intervals - for changes, of the per-round paired ratio. Composition memory is peak RSS less that of the same process parsing both maps without composing them (594MB).

(cherry picked from commit 5e08337)
* skip errors that occurred on virtual lines

* Update ModuleResolution.js

* Add regression test for resolution errors past the end of the source

This fix has had no test since the PR was opened, which is what the review asked for in 2024. Adds one at the integration level, in `build-errors-test.js`, so it exercises the real transform and resolution path rather than constructing the error directly.

`injectImportPastEndOfFileTransformer.js` wraps `@react-native/metro-babel-transformer` and, for one fixture, appends `import './does-not-exist';` parsed with `startLine` one line past the end of the source — which is what a plugin that generates and appends code does. The dependency Metro collects then reports a location with no counterpart in the file on disk, so building the code frame for the resolution error walks off the end of `lines`.

Without the guard, the build rejects with `TypeError: Cannot read properties of undefined (reading 'length')` in place of the resolution error. With it, the error survives and the code frame degrades to the lines that do exist.

The assertion is deliberately not a snapshot: the resolver's list of candidate paths varies between versions, and what matters here is only that the resolution error survives.

Changelog: [Internal]
---------

Co-authored-by: Rob Hogan <2590098+robhogan@users.noreply.github.com>

(cherry picked from commit 5d6eaea)
Summary:
With `includeAsyncPaths`, Metro writes a server-relative URL for each async dependency into the `paths` argument of `__d(...)`, which the client uses to fetch the split bundle. `getDefaultAsyncDependencyPath` builds that URL with `path.relative` and `path.join`, and never converts separators, so on Windows a dependency at `C:\root\src\bar.js` under server root `C:\root` produces:

```
/src\bar.bundle?modulesOnly=true&runModule=false
```

This is a URL, and Metro's own parsing of bundle URLs treats the pathname as posix (`split('/')` in `parseBundleOptionsFromBundleRequestUrl`), so it should be `/src/bar.bundle?...` on every platform. This passes the joined path through `normalizePathSeparatorsToPosix`, already used in the same file for module debug names. No change on posix.

Changelog: [Fix] Fix backslashes in async bundle paths on Windows (`includeAsyncPaths`)

Test Plan:
Removes `js-test.js` from the Windows skip list. Its three async `paths` tests already assert forward-slash URLs from Windows-style inputs.

Before this change, on Windows CI:

```
  ✕ includes the paths of async dependencies when requested
  - "paths":{"1":"/../bar.bundle?param1=true&param2=1234&modulesOnly=true&runModule=false"}
  + "paths":{"1":"/..\\bar.bundle?param1=true&param2=1234&modulesOnly=true&runModule=false"}
  ✕ async bundle paths override modulesOnly and runModule
  ✕ passes a paths object (not the id array) for async dependencies
```

After, `yarn jest packages/metro/src/DeltaBundler` passes, and Windows CI on this PR is green for `js-test.js`.

Adapted for 0.84.x: Hand-ported, since 0.84.x builds async bundle paths inline rather than in `getDefaultAsyncDependencyPath`.

(cherry picked from commit d52ee79)
Adapted for 0.84.x: Tests use 0.84.x conventions (no `node:` mocks).

(cherry picked from commit 5793c17)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@robhogan
robhogan force-pushed the 0.84/picks-for-0.84.7 branch from 36768b6 to 2d7aa4e Compare October 15, 2026 06:57

This branch has not been deployed

No deployments
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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants