From bfedc867367316b78b841e8b9c238c5b8397c9e0 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 6 Oct 2026 09:47:56 +0300 Subject: [PATCH 1/2] Keep a launch-argument jsLocation when the packager answers /status late packagerServerHostPort probes the location it resolves and, when the probe does not answer in time, replaces it with a guessed host. That recovery suits a location saved in the defaults, which can be stale. A location passed as a launch argument is the launcher's instruction for this process, so it is used as given. --- .../React/Base/RCTBundleURLProvider.mm | 10 +++- .../RCTBundleURLProviderTests.m | 47 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/react-native/React/Base/RCTBundleURLProvider.mm b/packages/react-native/React/Base/RCTBundleURLProvider.mm index 1cc42d71d3bc..dc434d565655 100644 --- a/packages/react-native/React/Base/RCTBundleURLProvider.mm +++ b/packages/react-native/React/Base/RCTBundleURLProvider.mm @@ -267,7 +267,10 @@ - (NSString *)packagerServerHostPort NSString *location = [self jsLocation]; #if RCT_DEV_MENU NSString *scheme = [self packagerScheme]; - if ([location length] && ![RCTBundleURLProvider isPackagerRunning:location scheme:scheme]) { + // A location saved in the defaults can be stale, so one whose packager does not answer gives way to a guessed host. + // A location passed as a launch argument is this launch's instruction, so it is used however late /status answers. + if ([location length] && ![self _isJsLocationLaunchArgument] && + ![RCTBundleURLProvider isPackagerRunning:location scheme:scheme]) { location = nil; } #endif @@ -605,4 +608,9 @@ - (void)_setDefaults [[NSUserDefaults standardUserDefaults] registerDefaults:[self defaults]]; } +- (BOOL)_isJsLocationLaunchArgument +{ + return [[NSUserDefaults standardUserDefaults] volatileDomainForName:NSArgumentDomain][kRCTJsLocationKey] != nil; +} + @end diff --git a/packages/rn-tester/RNTesterUnitTests/RCTBundleURLProviderTests.m b/packages/rn-tester/RNTesterUnitTests/RCTBundleURLProviderTests.m index 3a2b33db0375..3ba46e05940a 100644 --- a/packages/rn-tester/RNTesterUnitTests/RCTBundleURLProviderTests.m +++ b/packages/rn-tester/RNTesterUnitTests/RCTBundleURLProviderTests.m @@ -43,6 +43,17 @@ RCTPlatformName]]; } +static NSURL *ipPortBundleURL(void) +{ + return [NSURL + URLWithString: + [NSString + stringWithFormat: + @"http://192.168.1.1:8099/%@.bundle?platform=%@&dev=true&lazy=true&minify=false&inlineSourceMap=false&modulesOnly=false&runModule=true&excludeSource=true&sourcePaths=url-server&app=com.apple.dt.xctest.tool", + testFile, + RCTPlatformName]]; +} + @implementation NSBundle (RCTBundleURLProviderTests) - (NSURL *)RCT_URLForResource:(NSString *)name withExtension:(NSString *)ext @@ -59,7 +70,9 @@ - (NSURL *)RCT_URLForResource:(NSString *)name withExtension:(NSString *)ext @interface RCTBundleURLProviderTests : XCTestCase @end -@implementation RCTBundleURLProviderTests +@implementation RCTBundleURLProviderTests { + NSDictionary *_launchArguments; +} - (void)setUp { @@ -67,16 +80,25 @@ - (void)setUp RCTSwapInstanceMethods( [NSBundle class], @selector(URLForResource:withExtension:), @selector(RCT_URLForResource:withExtension:)); + _launchArguments = [[NSUserDefaults standardUserDefaults] volatileDomainForName:NSArgumentDomain]; } - (void)tearDown { + [self replaceLaunchArguments:_launchArguments]; RCTSwapInstanceMethods( [NSBundle class], @selector(URLForResource:withExtension:), @selector(RCT_URLForResource:withExtension:)); [super tearDown]; } +- (void)replaceLaunchArguments:(NSDictionary *)launchArguments +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults removeVolatileDomainForName:NSArgumentDomain]; + [defaults setVolatileDomain:launchArguments forName:NSArgumentDomain]; +} + - (void)testBundleURL { RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings]; @@ -109,4 +131,27 @@ - (void)testIPURL XCTAssertEqualObjects(URL, ipBundleURL()); } +- (void)testLaunchArgumentLocationIsKeptWhenPackagerDoesNotAnswer +{ + id classMock = OCMClassMock([RCTBundleURLProvider class]); + [[[classMock stub] andReturnValue:@NO] isPackagerRunning:[OCMArg any] scheme:[OCMArg any]]; + NSMutableDictionary *launchArguments = [_launchArguments mutableCopy]; + launchArguments[@"RCT_jsLocation"] = @"192.168.1.1:8099"; + [self replaceLaunchArguments:launchArguments]; + RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings]; + settings.jsLocation = nil; + NSURL *URL = [settings jsBundleURLForBundleRoot:testFile]; + XCTAssertEqualObjects(URL, ipPortBundleURL()); +} + +- (void)testSavedLocationGivesWayWhenPackagerDoesNotAnswer +{ + id classMock = OCMClassMock([RCTBundleURLProvider class]); + [[[classMock stub] andReturnValue:@NO] isPackagerRunning:[OCMArg any] scheme:[OCMArg any]]; + RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings]; + settings.jsLocation = @"192.168.1.1:8099"; + NSURL *URL = [settings jsBundleURLForBundleRoot:testFile]; + XCTAssertEqualObjects(URL, mainBundleURL()); +} + @end From 1323449bd380c2eb9aa0a76eb81759bd3fab6b6a Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 6 Oct 2026 20:14:51 +0300 Subject: [PATCH 2/2] Read the launch argument through a shared RCTLaunchArgumentForKey RCTUtils gains RCTLaunchArgumentForKey, which reads a key from the NSUserDefaults argument domain, and RCTBundleURLProvider asks it for kRCTJsLocationKey instead of reading the domain itself. --- packages/react-native/React/Base/RCTBundleURLProvider.mm | 8 ++------ packages/react-native/React/Base/RCTUtils.h | 4 ++++ packages/react-native/React/Base/RCTUtils.mm | 5 +++++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/react-native/React/Base/RCTBundleURLProvider.mm b/packages/react-native/React/Base/RCTBundleURLProvider.mm index dc434d565655..4fa598abeac4 100644 --- a/packages/react-native/React/Base/RCTBundleURLProvider.mm +++ b/packages/react-native/React/Base/RCTBundleURLProvider.mm @@ -12,6 +12,7 @@ #import "RCTDefines.h" #import "RCTDevSupportHttpHeaders.h" #import "RCTLog.h" +#import "RCTUtils.h" #import @@ -269,7 +270,7 @@ - (NSString *)packagerServerHostPort NSString *scheme = [self packagerScheme]; // A location saved in the defaults can be stale, so one whose packager does not answer gives way to a guessed host. // A location passed as a launch argument is this launch's instruction, so it is used however late /status answers. - if ([location length] && ![self _isJsLocationLaunchArgument] && + if ([location length] && RCTLaunchArgumentForKey(kRCTJsLocationKey) == nil && ![RCTBundleURLProvider isPackagerRunning:location scheme:scheme]) { location = nil; } @@ -608,9 +609,4 @@ - (void)_setDefaults [[NSUserDefaults standardUserDefaults] registerDefaults:[self defaults]]; } -- (BOOL)_isJsLocationLaunchArgument -{ - return [[NSUserDefaults standardUserDefaults] volatileDomainForName:NSArgumentDomain][kRCTJsLocationKey] != nil; -} - @end diff --git a/packages/react-native/React/Base/RCTUtils.h b/packages/react-native/React/Base/RCTUtils.h index abc8a851a5d8..302cded58b38 100644 --- a/packages/react-native/React/Base/RCTUtils.h +++ b/packages/react-native/React/Base/RCTUtils.h @@ -86,6 +86,10 @@ RCT_EXTERN BOOL RCTRunningInTestEnvironment(void); // Returns YES if React is running in an iOS App Extension RCT_EXTERN BOOL RCTRunningInAppExtension(void); +// Returns the value passed for a user defaults key as a launch argument (-key value), +// leaving out any value the app saved for it +RCT_EXTERN id __nullable RCTLaunchArgumentForKey(NSString *key); + // Returns the shared UIApplication instance, or nil if running in an App Extension RCT_EXTERN UIApplication *__nullable RCTSharedApplication(void); diff --git a/packages/react-native/React/Base/RCTUtils.mm b/packages/react-native/React/Base/RCTUtils.mm index 21ddfb6ff1f2..ac6f6fbb76bb 100644 --- a/packages/react-native/React/Base/RCTUtils.mm +++ b/packages/react-native/React/Base/RCTUtils.mm @@ -601,6 +601,11 @@ BOOL RCTRunningInAppExtension(void) return [[[[NSBundle mainBundle] bundlePath] pathExtension] isEqualToString:@"appex"]; } +id __nullable RCTLaunchArgumentForKey(NSString *key) +{ + return [[NSUserDefaults standardUserDefaults] volatileDomainForName:NSArgumentDomain][key]; +} + UIApplication *__nullable RCTSharedApplication(void) { if (RCTRunningInAppExtension()) {