From a28bbb0822cac825678e2b6ed275452f5f8ec6a3 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Mon, 5 Oct 2026 12:57:42 +0100 Subject: [PATCH] Fix HmrServer crash when entry point uses virtual URL prefix *Recreating #1709 / [D104223071](https://www.internalfb.com/diff/D104223071) (@motiz88) for GH first without a diff attachment* D102004228 added support for serving bundles from `Server` with the handling `[metro-watchFolders]//...` or `[metro-project]/...` virtual URL prefixes, but did not add the same support in `HmrServer`. Here, `HmrServer` constructs its own `RootUrlMap` to match `Server`'s behaviour exactly. Changelog: * **[Fix]:** Prevent HMR crash on `[metro-watchFolders]` or `[metro-project]` URLs Test plan: See [D104223071](https://www.internalfb.com/diff/D104223071) --- packages/metro/src/HmrServer.js | 30 +++++----- .../metro/src/__tests__/HmrServer-test.js | 55 +++++++++++++++++++ 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/packages/metro/src/HmrServer.js b/packages/metro/src/HmrServer.js index 9396b632f6..c803f0a34c 100644 --- a/packages/metro/src/HmrServer.js +++ b/packages/metro/src/HmrServer.js @@ -28,6 +28,7 @@ import debounceAsyncQueue from './lib/debounceAsyncQueue'; import formatBundlingError from './lib/formatBundlingError'; import getGraphId from './lib/getGraphId'; import parseBundleOptionsFromBundleRequestUrl from './lib/parseBundleOptionsFromBundleRequestUrl'; +import RootUrlMap from './lib/RootUrlMap'; import splitBundleOptions from './lib/splitBundleOptions'; import * as transformHelpers from './lib/transformHelpers'; import debugModule from 'debug'; @@ -71,6 +72,7 @@ export default class HmrServer { _bundler: IncrementalBundler; _createModuleId: (path: string) => number; _clientGroups: Map; + _rootUrlMap: RootUrlMap; constructor( bundler: IncrementalBundler, @@ -81,6 +83,7 @@ export default class HmrServer { this._bundler = bundler; this._createModuleId = createModuleId; this._clientGroups = new Map(); + this._rootUrlMap = new RootUrlMap(config); } onClientConnect: ( @@ -121,19 +124,19 @@ export default class HmrServer { transformOptions.platform, resolverOptions, ); - const resolvedEntryFilePath = resolutionFn( - (this._config.server.unstable_serverRoot ?? this._config.projectRoot) + - '/.', - { - name: entryFile, - data: { - key: entryFile, - asyncType: null, - isESMImport: false, - locs: [], - }, + const absolutePath = + this._rootUrlMap.filePathOfUrlDecodedPathname(entryFile); + const rootDir = absolutePath != null ? '/' : this._rootUrlMap.serverRootDir; + const resolvedEntryFile = absolutePath ?? entryFile; + const resolvedEntryFilePath = resolutionFn(rootDir + '/.', { + name: resolvedEntryFile, + data: { + key: resolvedEntryFile, + asyncType: null, + isESMImport: false, + locs: [], }, - ).filePath; + }).filePath; const graphId = getGraphId(resolvedEntryFilePath, transformOptions, { resolverOptions, shallow: graphOptions.shallow, @@ -379,8 +382,7 @@ export default class HmrServer { createModuleId: this._createModuleId, includeAsyncPaths: group.graphOptions.lazy, projectRoot: this._config.projectRoot, - serverRoot: - this._config.server.unstable_serverRoot ?? this._config.projectRoot, + serverRoot: this._rootUrlMap.serverRootDir, }); logger?.point('serialize_end'); diff --git a/packages/metro/src/__tests__/HmrServer-test.js b/packages/metro/src/__tests__/HmrServer-test.js index 15a2e1d266..70d23172cb 100644 --- a/packages/metro/src/__tests__/HmrServer-test.js +++ b/packages/metro/src/__tests__/HmrServer-test.js @@ -131,6 +131,7 @@ describe('HmrServer', () => { unstable_allowRequireContext: false, }, resolver: {platforms: []}, + watchFolders: [p('/external/node_modules')], server: { rewriteRequestUrl(requrl) { const rewritten = requrl.replace( @@ -650,6 +651,60 @@ describe('HmrServer', () => { ]); }); + test('should resolve [metro-watchFolders] prefix in entry point', async () => { + await connect( + '/hot?bundleEntry=./[metro-watchFolders]/0/expo-router/entry.js&platform=ios', + ); + + expect(getRevisionByGraphIdMock).toBeCalledWith( + getGraphId( + p('/external/node_modules/expo-router/entry.js'), + { + customTransformOptions: {}, + dev: true, + minify: false, + platform: 'ios', + type: 'module', + unstable_transformProfile: 'default', + }, + { + shallow: false, + lazy: false, + unstable_allowRequireContext: false, + resolverOptions: { + dev: true, + }, + }, + ), + ); + }); + + test('should resolve [metro-project] prefix in entry point', async () => { + await connect('/hot?bundleEntry=./[metro-project]/src/App.js&platform=ios'); + + expect(getRevisionByGraphIdMock).toBeCalledWith( + getGraphId( + p('/root/src/App.js'), + { + customTransformOptions: {}, + dev: true, + minify: false, + platform: 'ios', + type: 'module', + unstable_transformProfile: 'default', + }, + { + shallow: false, + lazy: false, + unstable_allowRequireContext: false, + resolverOptions: { + dev: true, + }, + }, + ), + ); + }); + test('should return error messages when there is a transform error', async () => { jest.useRealTimers(); const sendMessage = jest.fn();