Summary
When a project is created with the Metro bundler, the scaffold does not generate a metro.config.js. Without that file, the dev server cannot build a bundle. Every bundle request returns 404 with Unable to resolve module ./index, even though index.js is present in the project root.
Creating the file by hand fixes the problem, and nothing else needs to change.
For comparison, creating a project with the Re.Pack bundler does generate an rspack.config.mjs, and that project bundles correctly on the first try.
Environment
| Package |
Version |
rock, @rock-js/plugin-metro |
0.15.1 |
react-native |
0.86.0 |
@react-native/metro-config |
0.86.0 |
metro (dev server) |
0.85.0 |
| Node |
24.16.0 |
| OS |
macOS, arm64 |
Steps to reproduce
- Create a project with the Metro bundler, so that
rock.config.mjs uses bundler: pluginMetro(). The project root has no metro.config.js.
- Run
npx rock start --reset-cache.
- Request a bundle:
curl -o /dev/null -w '%{http_code}' 'http://localhost:8081/index.bundle?platform=ios&dev=true'
The server responds with 404 and this body:
{
"type": "UnableToResolveError",
"originModulePath": "<project>/.",
"targetModuleName": "./index",
"message": "Unable to resolve module ./index from <project>/.: None of these files exist:\n * index(.ios.js|.native.js|.js|.ios.jsx|.native.jsx|.jsx|...)"
}
The app shows the same message as a red screen on the simulator. The native build, install and launch all succeed, so the failure looks like a problem with the entry file rather than with the configuration.
index.js exists in the project root and has not been modified. Building through the Metro API directly gives a clearer message:
Failed to get the SHA-1 for: <project>/index.js
1) The file is not watched. Ensure it is under the configured `projectRoot` or `watchFolders`.
- Add a standard configuration file and change nothing else:
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
module.exports = mergeConfig(getDefaultConfig(__dirname), {});
- Restart the dev server. The same request now returns 200 and the app runs.
The lockfile and node_modules were identical in both cases, and no .watchmanconfig was added. I also ruled out a stale Metro cache, an incorrect watchman root, symlinks in the project path, and duplicate metro versions in the dependency tree. Both 0.84.6 and 0.85.0 are installed in the working case as well, so that is not the cause.
Possible cause
Running without a metro.config.js looks like it is meant to be supported, since loadMetroConfig.ts logs "No metro.config.js found, using default configuration". However, the configuration returned in that case seems to be missing the plugin's own defaults.
From packages/plugin-metro/src/lib/start/loadMetroConfig.ts on main:
105: const defaultConfig = RNMetroConfig.getDefaultConfig(ctx.root);
...
114: RNMetroConfig.setFrameworkDefaults(
115: getCommunityCliDefaultConfig(ctx, defaultConfig),
116: );
...
122: if (projectConfig.isEmpty) {
126: return defaultConfig;
defaultConfig is created on line 105, before setFrameworkDefaults() is called on line 114. setFrameworkDefaults() affects later calls to getDefaultConfig(), so it does not modify the object that already exists. As a result, the configuration returned on line 126 never receives the values from getCommunityCliDefaultConfig, such as resolver.platforms and serializer.getModulesRunBeforeMainModule.
The branch for React Native below 0.81 does merge those values:
129: const overrideConfig = getCommunityCliDefaultConfig(ctx, defaultConfig);
130: return mergeConfig(defaultConfig, overrideConfig);
This also explains why adding a metro.config.js helps. In that case the project's own call to getDefaultConfig(__dirname) runs after setFrameworkDefaults(), so the defaults are applied.
Suggested fix
Generate a metro.config.js when a project is created with the Metro bundler, in the same way that rspack.config.mjs is generated for Re.Pack. The only metro.config.js I could find in this repository is under packages/platform-harmony/template.
It would also help to fix the projectConfig.isEmpty branch, either by calling RNMetroConfig.getDefaultConfig(ctx.root) again after setFrameworkDefaults(), or by merging the overrides in the same way as the branch below it. Otherwise anyone who removes the file will hit the same error.
Notes
- Switching the project to
bundler: pluginRepack() was a working workaround, because that path does not use this code.
- I confirmed that the bundle fails without the configuration file and succeeds with it, and I read the code above. I did not verify which specific missing default causes the resolution to fail.
Summary
When a project is created with the Metro bundler, the scaffold does not generate a
metro.config.js. Without that file, the dev server cannot build a bundle. Every bundle request returns 404 withUnable to resolve module ./index, even thoughindex.jsis present in the project root.Creating the file by hand fixes the problem, and nothing else needs to change.
For comparison, creating a project with the Re.Pack bundler does generate an
rspack.config.mjs, and that project bundles correctly on the first try.Environment
rock,@rock-js/plugin-metroreact-native@react-native/metro-configmetro(dev server)Steps to reproduce
rock.config.mjsusesbundler: pluginMetro(). The project root has nometro.config.js.npx rock start --reset-cache.The server responds with 404 and this body:
{ "type": "UnableToResolveError", "originModulePath": "<project>/.", "targetModuleName": "./index", "message": "Unable to resolve module ./index from <project>/.: None of these files exist:\n * index(.ios.js|.native.js|.js|.ios.jsx|.native.jsx|.jsx|...)" }The app shows the same message as a red screen on the simulator. The native build, install and launch all succeed, so the failure looks like a problem with the entry file rather than with the configuration.
index.jsexists in the project root and has not been modified. Building through the Metro API directly gives a clearer message:The lockfile and
node_moduleswere identical in both cases, and no.watchmanconfigwas added. I also ruled out a stale Metro cache, an incorrect watchman root, symlinks in the project path, and duplicatemetroversions in the dependency tree. Both 0.84.6 and 0.85.0 are installed in the working case as well, so that is not the cause.Possible cause
Running without a
metro.config.jslooks like it is meant to be supported, sinceloadMetroConfig.tslogs "No metro.config.js found, using default configuration". However, the configuration returned in that case seems to be missing the plugin's own defaults.From
packages/plugin-metro/src/lib/start/loadMetroConfig.tsonmain:defaultConfigis created on line 105, beforesetFrameworkDefaults()is called on line 114.setFrameworkDefaults()affects later calls togetDefaultConfig(), so it does not modify the object that already exists. As a result, the configuration returned on line 126 never receives the values fromgetCommunityCliDefaultConfig, such asresolver.platformsandserializer.getModulesRunBeforeMainModule.The branch for React Native below 0.81 does merge those values:
This also explains why adding a
metro.config.jshelps. In that case the project's own call togetDefaultConfig(__dirname)runs aftersetFrameworkDefaults(), so the defaults are applied.Suggested fix
Generate a
metro.config.jswhen a project is created with the Metro bundler, in the same way thatrspack.config.mjsis generated for Re.Pack. The onlymetro.config.jsI could find in this repository is underpackages/platform-harmony/template.It would also help to fix the
projectConfig.isEmptybranch, either by callingRNMetroConfig.getDefaultConfig(ctx.root)again aftersetFrameworkDefaults(), or by merging the overrides in the same way as the branch below it. Otherwise anyone who removes the file will hit the same error.Notes
bundler: pluginRepack()was a working workaround, because that path does not use this code.