Make the Metro config's PowerShell path lookup lazy again, to stop spamming errors on macOS #16430

Open
shirakaba opened 8:15am on September 5, 2026 wants to merge 30 Ξ” into microsoft/react-native-windows main from
codex/defer-powershell-discovery
Reviewing
8 fewer changed lines
(50% less) vs GitHub
This saves about 87.5 hours per year vs conventional diff tools
shirakaba authored
of work during September 5
Diff Delta:
30
About 17 Diff Delta/hour
Classified as:  Bug Fix, General

shirakaba's Description of Work

I didn't file an issue for this yet, so I'll begin by introducing the problem before describing the solution that this PR implements.

Problem

When you try to run rnc-cli start on a Mac, in a project with react-native-windows installed, using @rnx-kit/metro-config as your Metro config, it eagerly looks for .NET and Powershell, naturally fails to find them, and makes a lot of noise:

./node_modules/.bin/rnc-cli start 

/bin/sh: dotnet.exe: command not found
/bin/sh: where: command not found
/bin/sh: dotnet.exe: command not found
/bin/sh: where: command not found
/Users/jamie/Documents/git/expo-desktop/packages/expo-desktop/MyApp4/node_modules/react-native-windows/react-native.config.js: /bin/sh: dotnet.exe: command not found
/bin/sh: where: command not found
/Users/jamie/Documents/git/expo-desktop/packages/expo-desktop/MyApp4/node_modules/@react-native-windows/find-dotnet-tools/lib-commonjs/findDotnetTools.js:62
throw new Error('Unable to find pwsh.exe. It should have been made available by `yarn install`.');
^

Error: Unable to find pwsh.exe. It should have been made available by `yarn install`.
at findPowerShell (/Users/jamie/Documents/git/expo-desktop/packages/expo-desktop/MyApp4/node_modules/@react-native-windows/find-dotnet-tools/lib-commonjs/findDotnetTools.js:62:11)
at Object.<anonymous> (/Users/jamie/Documents/git/expo-desktop/packages/expo-desktop/MyApp4/node_modules/@react-native-windows/cli/lib-commonjs/utils/commandWithProgress.js:45:59)
at Module._compile (node:internal/modules/cjs/loader:1812:14)
at Object..js (node:internal/modules/cjs/loader:1943:10)
at Module.load (node:internal/modules/cjs/loader:1533:32)
at Module._load (node:internal/modules/cjs/loader:1335:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
at Module.require (node:internal/modules/cjs/loader:1556:12)
at require (node:internal/modules/helpers:152:16)
at Object.<anonymous> (/Users/jamie/Documents/git/expo-desktop/packages/expo-desktop/MyApp4/node_modules/@react-native-windows/cli/lib-commonjs/commands/autolinkWindows/autolinkWindows.js:47:31)

Node.js v24.14.0
● Validation Warning:

Unknown option "watcher.unstable_workerThreads" with value false was found.
This is probably a typing mistake. Fixing it will remove this message.

Welcome to React Native v0.81
Starting dev server on http://localhost:8081

This eager search happens when @rnx-kit/metro-config discovers platforms from the dependencies at https://github.com/microsoft/rnx-kit/blob/08312a2fec3231e27a613754baa394ce2b41086e/packages/tools-react-native/src/context.ts#L147. It ends up evaluating react-native-windows/react-native.config.js, which calls findPowerShell().

(CC @tido64 – I think it's reasonable to fix it in react-native-windows, but tagging just in case it'd be preferable to fix it on the rnx-kit side).

History

There was no such problem in [email protected]. The regression was introduced when https://github.com/microsoft/react-native-windows/pull/16075 was backported to 0.81.28 in https://github.com/microsoft/react-native-windows/pull/16235, with changes such as this one, to line 50 of packages/@react-native-windows/cli/src/utils/commandWithProgress.ts:

- export const powershell = `${process.env.SystemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;

+ const powershell = findPowerShell();

See how instances that used to be static strings instead become eager file system lookup, causing side-effects.

Solution

This PR makes it so that we resolve and cache the path to PowerShell only when actually about to execute a PowerShell command. i.e. it changes it from eager to lazy.

Reproduction

Repro repository, based on Expo Desktop:

# (1) Set up the project:

git clone --branch codex/reproduce https://github.com/shirakaba/rnw-powershell-discovery-repro.git
cd rnw-powershell-discovery-repro
bun install --frozen-lockfile

# (2a) Trigger `expo start` (equivalent to `rnc-cli start`):
node --run start

# (2b) Or, more minimally, just import the metro config:
node -e "require('./metro.config.js')"

# (3) Compare two branches (run `bun install` post-checkout):
# - main: problem reproduces.
# - codex/fix: problem fixed, by patching RNW.

In my case, I used Node.js 24 and Bun 1.3.11.

See the codex/fix branch, which applies this proposed change as a patch.

Backporting

While this PR is opened against main, please consider backporting to 0.81-stable so that I can release Expo Desktop in best condition.

Testing

Astra initially wrote a whole test suite for this. I think they're a bit over the top, though, so I've pushed an extra commit to drop them. If the reviewer wants, I can restore them just by reverting that commit.


  • ~~10 Jest tests pass: import/serialization on simulated macOS and Windows, deferred lookup, successful caching, retry after failure, and diagnostic/repair execution. The import regressions fail against the original source on both simulated hosts.~~

  • ~~CLI TypeScript build and ESLint pass; changed files formatted with Prettier.~~

  • ~~Repro CI on real macOS and Windows: baseline confirms the bug; patched branch passes. Checks forbid PowerShell discovery, require empty stderr after patching, and verify both platforms remain available.~~

Changelog

Yes: Defer PowerShell discovery until a command or health check needs it, allowing CLI configuration to load without Windows build tools.

###### Microsoft Reviewers: Open in CodeFlow

3 total changed files
(1 file ignored)
Loading changes...
Loading changes...
You’ve reached the end of the pull request
Please mind the merge button