diff --git a/packages/react-native/React/Base/RCTBundleURLProvider.mm b/packages/react-native/React/Base/RCTBundleURLProvider.mm index 1cc42d71d3bc..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 @@ -267,7 +268,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] && RCTLaunchArgumentForKey(kRCTJsLocationKey) == nil && + ![RCTBundleURLProvider isPackagerRunning:location scheme:scheme]) { location = nil; } #endif 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()) { 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