Skip to content

fix(cli): resolve react-native script subpaths through the exports map - #57709

Closed
chrfalch wants to merge 3 commits into
mainfrom
chrfalch/fix-cli-plugin-subpath-resolution
Closed

fix(cli): resolve react-native script subpaths through the exports map#57709
chrfalch wants to merge 3 commits into
mainfrom
chrfalch/fix-cli-plugin-subpath-resolution

Conversation

@chrfalch

@chrfalch chrfalch commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Summary:

Two CLI commands are broken on main right now. Both fail the moment you run them:

$ npx react-native spm add
error Cannot find module '.../node_modules/react-native/scripts/setup-apple-spm'

$ npx react-native codegen
error Cannot find module '.../node_modules/react-native/scripts/codegen/generate-artifacts-executor'

What happened. #57652 moved these two command wrappers into community-cli-plugin. Each one loads a helper script out of the react-native package by name:

require.resolve('react-native/scripts/setup-apple-spm');

Anything starting with react-native/ is looked up through the exports list in that package's package.json. This lookup is strict — it takes the path exactly as written. It will not add a missing .js, and it will not find the index.js inside a folder. Node's older lookup style did both of those automatically, and both wrappers were relying on it:

what the wrapper asked for what's actually there needed
scripts/setup-apple-spm a file named setup-apple-spm.js .js on the end
scripts/codegen/generate-artifacts-executor a folder containing index.js /index.js on the end

The fix. Write both paths out in full. That's a one-line change in each wrapper; nothing else about the commands changes.

This was found by the new SwiftPM iOS CI job in #57659, where every build failed on the spm command.

Changelog:

[GENERAL] [FIXED] - Fix react-native spm and react-native codegen failing with "Cannot find module"

Test Plan:

Added packages/community-cli-plugin/src/commands/__tests__/reactNativeSubpathResolution-test.js. It reads the command wrapper files, picks out every react-native/... path they reference, and checks each one can actually be loaded. That covers both commands today and any wrapper added later.

Why the test runs node in a subprocess: Jest redirects react-native to the source folder, which skips the exports list altogether. A test that does the lookup inside Jest therefore passes even while the real command is broken — I wrote that version first and it went green against the broken code. Running a separate node process is what makes the test meaningful.

Test fails before the fix, passes after:

# before
✕ codegen.js resolves react-native/scripts/codegen/generate-artifacts-executor
✕ spm.js resolves react-native/scripts/setup-apple-spm
Tests: 2 failed, 1 passed, 3 total

# after
✓ codegen.js resolves react-native/scripts/codegen/generate-artifacts-executor/index.js
✓ spm.js resolves react-native/scripts/setup-apple-spm.js
Tests: 3 passed, 3 total

Same result checking the paths directly, outside Jest:

node -e "require.resolve('react-native/scripts/setup-apple-spm')"                                # fails
node -e "require.resolve('react-native/scripts/setup-apple-spm.js')"                             # works
node -e "require.resolve('react-native/scripts/codegen/generate-artifacts-executor')"            # fails
node -e "require.resolve('react-native/scripts/codegen/generate-artifacts-executor/index.js')"   # works

flow focus-check, ESLint and Prettier are clean on the changed files.

The `spm` and `codegen` command wrappers reach into the react-native
package with require.resolve('react-native/...'). Those specifiers go
through react-native's `exports` map, which — unlike legacy CJS
resolution — neither appends extensions nor falls back to a directory's
index.js. Both specifiers relied on exactly that, so each threw
MODULE_NOT_FOUND as soon as the command ran:

  npx react-native spm ...  -> Cannot find module
      '.../node_modules/react-native/scripts/setup-apple-spm'
  npx react-native codegen  -> same, for
      '.../scripts/codegen/generate-artifacts-executor' (a directory)

Point them at the real files: `setup-apple-spm.js` and
`generate-artifacts-executor/index.js`.

Adds a regression test that scans the command wrappers for
react-native/* specifiers and resolves each one in a real node process —
Jest's moduleNameMapper remaps react-native to the source tree and
bypasses the exports map, so an in-band resolve passes even when
production fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@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 Jul 28, 2026
readdirSync's withFileTypes overload types Dirent.name as
`string | Buffer`, which flow-check rejected at three call sites. Use the
plain readdirSync overload (Array<string>) with a statSync isFile check,
and switch the requires to the node: protocol per the lint rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Jul 28, 2026
@chrfalch

Copy link
Copy Markdown
Collaborator Author

Verified end-to-end on a clean main worktree (54656471a54) — both commands fail today, and the two-line change repairs both.

Before (main, unmodified):

$ npx react-native spm download --artifacts /tmp/probe      # from private/helloworld/ios
error Cannot find module '.../node_modules/react-native/scripts/setup-apple-spm'
    at Object.func (.../packages/community-cli-plugin/src/commands/spm.js:109:35)
exit 1

$ npx react-native codegen --path . --platform ios --outputPath /tmp/out   # from private/helloworld
error Cannot find module '.../node_modules/react-native/scripts/codegen/generate-artifacts-executor'
    at Object.func (.../packages/community-cli-plugin/src/commands/codegen.js:49:47)
exit 1

After (same worktree, only the two specifier lines changed):

$ npx react-native spm download --artifacts /tmp/probe
[setup-apple-spm] Running SPM download in: .../private/helloworld/ios
[setup-apple-spm] React Native version: 1000.0.0
[download-spm-artifacts] Resolved nightly: 0.88.0-nightly-20260728-...
[download-spm-artifacts] Fetching snapshot metadata: https://central.sonatype.com/...

Gets past module loading into real artifact downloading (I stopped it with a 180s timeout rather than waiting for the full fetch).

$ npx react-native codegen --path . --platform ios --outputPath /tmp/out
exit 0

Runs to completion, writing real artifacts (RCTThirdPartyComponentsProvider, RCTModuleProviders, ReactAppDependencyProvider, Package.swift).

No Cannot find module in either command's output afterwards.

Worth noting npx react-native codegen has no CI coverage at all — no workflow invokes it, and Jest's moduleNameMapper bypasses the exports lookup — so nothing currently in the pipeline would catch this.

@chrfalch
chrfalch requested a review from huntie July 28, 2026 09:04
Drop the helper functions and the long explanatory comment; the scan and
the subprocess check (the part Jest's moduleNameMapper would otherwise
defeat) collapse into two statements. 70 lines -> 43.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@meta-codesync

meta-codesync Bot commented Jul 28, 2026

Copy link
Copy Markdown

@fabriziocucci has imported this pull request. If you are a Meta employee, you can view this in D113895593.

@fabriziocucci

Copy link
Copy Markdown
Contributor

Superseded by #57718.

meta-codesync Bot pushed a commit that referenced this pull request Jul 28, 2026
Summary:
Pull Request resolved: #57718

**Context**

Replaces #57709, which identified a real bug in 0.87 from the combination of:

- #57276
- #57652

```
$ npx react-native spm add
error Cannot find module '.../node_modules/react-native/scripts/setup-apple-spm'

$ npx react-native codegen
error Cannot find module '.../node_modules/react-native/scripts/codegen/generate-artifacts-executor'
```

**This diff**

- Fix — and exclusively switch to — extensionless imports rather than requiring `.js`.
- Update in-repo consumers.

The previous single mapping is now an extension-aware mapping:
- `./scripts/*` now appends `.js`, so `react-native/scripts/foo` resolves to `foo.js`.
- `.sh` and `.rb` stay reachable via explicit `./scripts/*.sh` and `./scripts/*.rb` passthrough patterns.

**Impact**

- Explicit `react-native/scripts/*.js` specifiers no longer resolve, so JavaScript paths must be imported without an extension.
- Files under `scripts/` with extensions other than `.js`, `.sh`, or `.rb` are no longer exposed through `./scripts/*`.
    - These have no open source consumers.

Changelog:
[General][Fixed] - (RC4 only, drop for main changelog): `react-native/scripts/*` imports once again expand `.js` extensions
[General][Breaking] - Extensionless `react-native/scripts/*` imports are now **mandated**; explicit `.js` import specifiers are rejected.

Reviewed By: rubennorte

Differential Revision: D113898792

fbshipit-source-id: d72f60be2c08ab97871e336645856c9029e74ae2
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. p: Expo Partner: Expo Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants