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

Pull Request Overview

  • Opened on September 5, 2026
  • Status Open
  • Commit count 3 with first commit September 5, 2026

Total Delta

30 Total Diff Delta

Open Days

Open 10 weekdays

Test Delta

0 Diff Delta in Test Files
Breakdown by Phase

How long has this pull request spent in each phase of its lifecycle?

Fraction of total time Business days Phase
 
0.0 days Authoring 1 commit before pull request opened for review
 
0.0 days Awaiting first review
 
10.0 days Revising work with 2 commits in response to 2 reviews that left 7 comments

Total time for pull request still awaiting merge (longer than repo's target): 10.0 business days

Author avatar

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

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

Comments Threads Pending Resolution

ljharb reviewed on September 5, 2026
ljharb left a comment

### 🟑 Changes recommended

PowerShell discovery failures lose actionable diagnostics or bypass health-check failure handling.

*Once you've addressed the issues Copilot identified, you can request another Copilot review.*

Review details

### Suppressed comments (1)

**packages/@react-native-windows/cli/src/commands/healthCheck/healthChecks.ts:82**
* `findPowerShell()` can throw when the tool is absent, but this lookup occurs before the callback's failure-handling `try`. In that case `runAutomaticFix` rejects without calling `loader.fail()` or reporting manual-installation guidance, unlike all `execa` failures below. Include discovery in the handled path and fail the loader cleanly when no executable can be found.
```
powershell ??= findPowerShell();
```

- **Files reviewed:** 3/3 changed files
- **Comments generated:** 1
- **Review effort level:** Balanced

packages/@react-native-windows/cli/src/utils/commandWithProgress.ts
58
  errorCategory: CodedErrorType,
59
  useAppxCompatibility = false,
60
) {
61
  try {
61 62
    powershell ??= findPowerShell();
Unspecified Git Hub Copilot Model
ljharb reviewed on September 5, 2026
ljharb left a comment

### 🟑 Changes recommended

PowerShell lookup failures bypass existing command and health-check error handling.

*Once you've addressed the issues Copilot identified, you can request another Copilot review.*

Pull request overview

Defers PowerShell discovery to avoid failures when loading RNW configuration without Windows tooling.

**Changes:**
- Lazily caches PowerShell paths during command and health-check execution.
- Adds a prerelease change record.

File summaries

| File | Description |
| ---- | ----------- |
| `commandWithProgress.ts` | Defers PowerShell discovery until command execution. |
| `healthChecks.ts` | Defers discovery until diagnostics or repair. |
| Change JSON | Records the CLI prerelease change. |

Review details

- **Files reviewed:** 3/3 changed files
- **Comments generated:** 2
- **Review effort level:** Balanced

---

πŸ’‘ Add a `code-review` agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

packages/@react-native-windows/cli/src/commands/healthCheck/healthChecks.ts
79
            };
80
          },
81
          runAutomaticFix: async ({loader, logManualInstallation}) => {
82
            powershell ??= findPowerShell();

Resolved Comment Threads

ljharb reviewed on September 5, 2026
ljharb left a comment

### 🟑 Changes recommended

PowerShell lookup failures bypass existing command and health-check error handling.

*Once you've addressed the issues Copilot identified, you can request another Copilot review.*

Pull request overview

Defers PowerShell discovery to avoid failures when loading RNW configuration without Windows tooling.

**Changes:**
- Lazily caches PowerShell paths during command and health-check execution.
- Adds a prerelease change record.

File summaries

| File | Description |
| ---- | ----------- |
| `commandWithProgress.ts` | Defers PowerShell discovery until command execution. |
| `healthChecks.ts` | Defers discovery until diagnostics or repair. |
| Change JSON | Records the CLI prerelease change. |

Review details

- **Files reviewed:** 3/3 changed files
- **Comments generated:** 2
- **Review effort level:** Balanced

---

πŸ’‘ Add a `code-review` agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

packages/@react-native-windows/cli/src/utils/commandWithProgress.ts
Outdated
58
  errorCategory: CodedErrorType,
59
  useAppxCompatibility = false,
60
) {
61
  powershell ??= findPowerShell();
62
  try {