Skip to content

Commit 6a33b04

Browse files
motiz88meta-codesync[bot]
authored andcommitted
Attempt to clear RedBox by automatically reloading on Metro file change
Summary: While RedBox is displayed, open a native WebSocket to Metro's `/hot` endpoint. On file change, automatically reload — bridging the gap for bundle loading errors where the JS HMR client is unavailable. Changelog: [Internal] Reviewed By: robhogan Differential Revision: D98350597
1 parent 2bad126 commit 6a33b04

6 files changed

Lines changed: 196 additions & 1 deletion

File tree

packages/react-native/React/CoreModules/RCTRedBox+Internal.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,10 @@
3333

3434
@end
3535

36+
@protocol RCTRedBox2Controlling <RCTRedBoxControlling>
37+
38+
@property (nonatomic, strong, nullable) NSURL *bundleURL;
39+
40+
@end
41+
3642
#endif

packages/react-native/React/CoreModules/RCTRedBox.mm

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,14 @@ - (void)updateErrorMessage:(NSString *)message
165165
[self showErrorMessage:message withParsedStack:stack isUpdate:YES errorCookie:errorCookie];
166166
}
167167

168+
- (id<RCTRedBox2Controlling>)_redBox2Controller
169+
{
170+
if ([_controller conformsToProtocol:@protocol(RCTRedBox2Controlling)]) {
171+
return (id<RCTRedBox2Controlling>)_controller;
172+
}
173+
return nil;
174+
}
175+
168176
- (void)showErrorMessage:(NSString *)message
169177
withParsedStack:(NSArray<RCTJSStackFrame *> *)stack
170178
isUpdate:(BOOL)isUpdate
@@ -195,6 +203,7 @@ - (void)showErrorMessage:(NSString *)message
195203
}
196204
self->_controller.actionDelegate = self;
197205
}
206+
[self _redBox2Controller].bundleURL = self->_overrideBundleURL ?: self->_bundleManager.bundleURL;
198207
[self->_controller showErrorMessage:errorInfo.errorMessage
199208
withStack:errorInfo.stack
200209
isUpdate:isUpdate

packages/react-native/React/CoreModules/RCTRedBox2Controller+Internal.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
using RCTRedBox2ButtonPressHandler = void (^)(void);
1515

16-
@interface RCTRedBox2Controller : UIViewController <RCTRedBoxControlling, UITableViewDelegate, UITableViewDataSource>
16+
@interface RCTRedBox2Controller : UIViewController <RCTRedBox2Controlling, UITableViewDelegate, UITableViewDataSource>
1717

1818
@property (nonatomic, weak) id<RCTRedBoxControllerActionDelegate> actionDelegate;
1919

@@ -25,6 +25,9 @@ using RCTRedBox2ButtonPressHandler = void (^)(void);
2525
isUpdate:(BOOL)isUpdate
2626
errorCookie:(int)errorCookie;
2727

28+
/// The bundle URL used by the app, for the native HMR connection.
29+
@property (nonatomic, strong, nullable) NSURL *bundleURL;
30+
2831
- (void)dismiss;
2932
@end
3033

packages/react-native/React/CoreModules/RCTRedBox2Controller.mm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#import "RCTRedBox2AnsiParser+Internal.h"
1818
#import "RCTRedBox2ErrorParser+Internal.h"
19+
#import "RCTRedBoxHMRClient+Internal.h"
1920

2021
// @lint-ignore-every CLANGTIDY clang-diagnostic-switch-default
2122
// NOTE: clang-diagnostic-switch-default conflicts with clang-diagnostic-switch-enum
@@ -64,6 +65,7 @@ @implementation RCTRedBox2Controller {
6465
NSInteger _autoRetryCountdown;
6566
UIButton *_reloadButton;
6667
NSString *_reloadBaseText;
68+
RCTRedBoxHMRClient *_hmrClient;
6769
}
6870

6971
- (instancetype)initWithCustomButtonTitles:(NSArray<NSString *> *)customButtonTitles
@@ -297,6 +299,7 @@ - (void)showErrorMessage:(NSString *)message
297299
animated:NO];
298300

299301
[self startAutoRetryIfApplicable];
302+
[self _startHMRClient];
300303
}
301304
}
302305

@@ -308,6 +311,7 @@ - (void)dismiss
308311

309312
- (void)reload
310313
{
314+
[self _stopHMRClient];
311315
[self stopAutoRetry];
312316
if (_actionDelegate != nil) {
313317
[_actionDelegate reloadFromRedBoxController:self];
@@ -318,6 +322,28 @@ - (void)reload
318322
}
319323
}
320324

325+
#pragma mark - Native HMR Connection
326+
327+
- (void)_startHMRClient
328+
{
329+
[self _stopHMRClient];
330+
if (!_bundleURL) {
331+
return;
332+
}
333+
__weak __typeof(self) weakSelf = self;
334+
_hmrClient = [[RCTRedBoxHMRClient alloc] initWithBundleURL:_bundleURL
335+
onFileChange:^{
336+
[weakSelf reload];
337+
}];
338+
[_hmrClient start];
339+
}
340+
341+
- (void)_stopHMRClient
342+
{
343+
[_hmrClient stop];
344+
_hmrClient = nil;
345+
}
346+
321347
#pragma mark - Auto-Retry
322348

323349
- (void)startAutoRetryIfApplicable
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <Foundation/Foundation.h>
9+
10+
#import <React/RCTDefines.h>
11+
12+
#if RCT_DEV_MENU
13+
14+
/**
15+
* Minimal native HMR client that connects to Metro's /hot WebSocket endpoint
16+
* while RedBox 2.0 is displayed. When Metro detects a file change
17+
* (update-start), this client triggers a reload so the user's fix is picked up
18+
* automatically — even when the JS runtime has no active HMR connection.
19+
*/
20+
@interface RCTRedBoxHMRClient : NSObject <NSURLSessionWebSocketDelegate>
21+
- (instancetype)initWithBundleURL:(NSURL *)bundleURL onFileChange:(void (^)(void))onFileChange;
22+
- (void)start;
23+
- (void)stop;
24+
@end
25+
26+
#endif
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import "RCTRedBoxHMRClient+Internal.h"
9+
10+
#if RCT_DEV_MENU
11+
12+
@implementation RCTRedBoxHMRClient {
13+
NSURL *_bundleURL;
14+
NSURLSessionWebSocketTask *_webSocketTask;
15+
NSURLSession *_session;
16+
void (^_onFileChange)(void);
17+
BOOL _stopped;
18+
}
19+
20+
- (instancetype)initWithBundleURL:(NSURL *)bundleURL onFileChange:(void (^)(void))onFileChange
21+
{
22+
if (self = [super init]) {
23+
_bundleURL = bundleURL;
24+
_onFileChange = [onFileChange copy];
25+
}
26+
return self;
27+
}
28+
29+
- (void)start
30+
{
31+
if (![_bundleURL.scheme hasPrefix:@"http"]) {
32+
return;
33+
}
34+
35+
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:_bundleURL resolvingAgainstBaseURL:NO];
36+
components.scheme = [_bundleURL.scheme isEqualToString:@"https"] ? @"wss" : @"ws";
37+
components.path = @"/hot";
38+
components.query = nil;
39+
components.fragment = nil;
40+
NSURL *wsURL = components.URL;
41+
if (!wsURL) {
42+
return;
43+
}
44+
45+
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
46+
delegate:self
47+
delegateQueue:nil];
48+
_webSocketTask = [_session webSocketTaskWithURL:wsURL];
49+
[_webSocketTask resume];
50+
}
51+
52+
- (void)stop
53+
{
54+
_stopped = YES;
55+
_onFileChange = nil;
56+
[_webSocketTask cancel];
57+
_webSocketTask = nil;
58+
[_session invalidateAndCancel];
59+
_session = nil;
60+
}
61+
62+
- (void)URLSession:(__unused NSURLSession *)session
63+
webSocketTask:(__unused NSURLSessionWebSocketTask *)webSocketTask
64+
didOpenWithProtocol:(__unused NSString *)protocol
65+
{
66+
NSDictionary *registration = @{
67+
@"type" : @"register-entrypoints",
68+
@"entryPoints" : @[ _bundleURL.absoluteString ],
69+
};
70+
NSData *json = [NSJSONSerialization dataWithJSONObject:registration options:0 error:nil];
71+
NSURLSessionWebSocketMessage *msg = [[NSURLSessionWebSocketMessage alloc]
72+
initWithString:[[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]];
73+
[_webSocketTask sendMessage:msg
74+
completionHandler:^(__unused NSError *error){
75+
}];
76+
[self _listenForNextMessage];
77+
}
78+
79+
- (void)_listenForNextMessage
80+
{
81+
if (_stopped) {
82+
return;
83+
}
84+
__weak __typeof(self) weakSelf = self;
85+
[_webSocketTask receiveMessageWithCompletionHandler:^(NSURLSessionWebSocketMessage *message, NSError *error) {
86+
if (error || !message) {
87+
return;
88+
}
89+
[weakSelf _handleMessage:message];
90+
[weakSelf _listenForNextMessage];
91+
}];
92+
}
93+
94+
- (void)_handleMessage:(NSURLSessionWebSocketMessage *)message
95+
{
96+
if (message.type != NSURLSessionWebSocketMessageTypeString || _stopped) {
97+
return;
98+
}
99+
NSData *data = [message.string dataUsingEncoding:NSUTF8StringEncoding];
100+
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
101+
if ([json[@"type"] isEqualToString:@"update-start"]) {
102+
// Ignore the initial update that fires when the client first registers.
103+
// Only react to subsequent file changes.
104+
NSDictionary *body = json[@"body"];
105+
if ([body isKindOfClass:[NSDictionary class]] && [body[@"isInitialUpdate"] boolValue]) {
106+
return;
107+
}
108+
dispatch_async(dispatch_get_main_queue(), ^{
109+
if (self->_onFileChange) {
110+
self->_onFileChange();
111+
}
112+
});
113+
}
114+
}
115+
116+
- (void)URLSession:(__unused NSURLSession *)session
117+
webSocketTask:(__unused NSURLSessionWebSocketTask *)task
118+
didCloseWithCode:(__unused NSURLSessionWebSocketCloseCode)closeCode
119+
reason:(__unused NSData *)reason
120+
{
121+
}
122+
123+
@end
124+
125+
#endif

0 commit comments

Comments
 (0)