diff --git a/React/Base/RCTJSStackFrame.m b/React/Base/RCTJSStackFrame.m index a199f72e4..08aabec6e 100644 --- a/React/Base/RCTJSStackFrame.m +++ b/React/Base/RCTJSStackFrame.m @@ -18,7 +18,7 @@ static NSRegularExpression *RCTJSStackFrameRegex() static NSRegularExpression *_regex; dispatch_once(&onceToken, ^{ NSError *regexError; - _regex = [NSRegularExpression regularExpressionWithPattern:@"^([^@]+)@(.*):(\\d+):(\\d+)$" options:0 error:®exError]; + _regex = [NSRegularExpression regularExpressionWithPattern:@"^(?:([^@]+)@)?(.*):(\\d+):(\\d+)$" options:0 error:®exError]; if (regexError) { RCTLogError(@"Failed to build regex: %@", [regexError localizedDescription]); } @@ -56,7 +56,9 @@ static NSRegularExpression *RCTJSStackFrameRegex() return nil; } - NSString *methodName = [line substringWithRange:[match rangeAtIndex:1]]; + // methodName may not be present for e.g. anonymous functions + const NSRange methodNameRange = [match rangeAtIndex:1]; + NSString *methodName = methodNameRange.location == NSNotFound ? nil : [line substringWithRange:methodNameRange]; NSString *file = [line substringWithRange:[match rangeAtIndex:2]]; NSString *lineNumber = [line substringWithRange:[match rangeAtIndex:3]]; NSString *column = [line substringWithRange:[match rangeAtIndex:4]]; @@ -69,7 +71,7 @@ static NSRegularExpression *RCTJSStackFrameRegex() + (instancetype)stackFrameWithDictionary:(NSDictionary *)dict { - return [[self alloc] initWithMethodName:dict[@"methodName"] + return [[self alloc] initWithMethodName:RCTNilIfNull(dict[@"methodName"]) file:dict[@"file"] lineNumber:[RCTNilIfNull(dict[@"lineNumber"]) integerValue] column:[RCTNilIfNull(dict[@"column"]) integerValue]]; diff --git a/React/Base/RCTReloadCommand.h b/React/Base/RCTReloadCommand.h index 7852ebc00..352ffd173 100644 --- a/React/Base/RCTReloadCommand.h +++ b/React/Base/RCTReloadCommand.h @@ -17,3 +17,6 @@ /** Registers a weakly-held observer of the Command+R reload key command. */ RCT_EXTERN void RCTRegisterReloadCommandListener(id listener); + +/** Triggers a reload for all current listeners. You shouldn't need to use this directly in most cases. */ +RCT_EXTERN void RCTTriggerReloadCommandListeners(void); diff --git a/React/Base/RCTReloadCommand.m b/React/Base/RCTReloadCommand.m index 34c782a90..8428511a9 100644 --- a/React/Base/RCTReloadCommand.m +++ b/React/Base/RCTReloadCommand.m @@ -9,11 +9,15 @@ #import "RCTReloadCommand.h" +#import "RCTAssert.h" #import "RCTKeyCommands.h" +/** main queue only */ +static NSHashTable> *listeners; + void RCTRegisterReloadCommandListener(id listener) { - static NSHashTable> *listeners; + RCTAssertMainQueue(); // because registerKeyCommandWithInput: must be called on the main thread static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ listeners = [NSHashTable weakObjectsHashTable]; @@ -21,17 +25,19 @@ void RCTRegisterReloadCommandListener(id listener) modifierFlags:UIKeyModifierCommand action: ^(__unused UIKeyCommand *command) { - NSArray> *copiedListeners; - @synchronized (listeners) { // avoid mutation-while-enumerating - copiedListeners = [listeners allObjects]; - } - for (id l in copiedListeners) { - [l didReceiveReloadCommand]; - } + RCTTriggerReloadCommandListeners(); }]; }); + [listeners addObject:listener]; +} - @synchronized (listeners) { - [listeners addObject:listener]; +void RCTTriggerReloadCommandListeners(void) +{ + RCTAssertMainQueue(); + // Copy to protect against mutation-during-enumeration. + // If listeners hasn't been initialized yet we get nil, which works just fine. + NSArray> *copiedListeners = [listeners allObjects]; + for (id l in copiedListeners) { + [l didReceiveReloadCommand]; } } diff --git a/React/Modules/RCTRedBox.h b/React/Modules/RCTRedBox.h index 8d7df0bd5..db72279cc 100644 --- a/React/Modules/RCTRedBox.h +++ b/React/Modules/RCTRedBox.h @@ -13,6 +13,8 @@ #import #import +@class RCTJSStackFrame; + @interface RCTRedBox : NSObject - (void)registerErrorCustomizer:(id)errorCustomizer; @@ -22,9 +24,17 @@ - (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack; - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack; - (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack; +- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray *)stack; +- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray *)stack; - (void)dismiss; +/** Overrides bridge.bundleURL. Modify on main thread only. You shouldn't need to use this. */ +@property (nonatomic, strong) NSURL *overrideBundleURL; + +/** Overrides the default behavior of calling [bridge reload] on reload. You shouldn't need to use this. */ +@property (nonatomic, strong) dispatch_block_t overrideReloadAction; + @end /** diff --git a/React/Modules/RCTRedBox.m b/React/Modules/RCTRedBox.m index 7eeb19bb0..ad1a835c5 100644 --- a/React/Modules/RCTRedBox.m +++ b/React/Modules/RCTRedBox.m @@ -255,7 +255,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) cell.selectedBackgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2]; } - cell.textLabel.text = stackFrame.methodName; + cell.textLabel.text = stackFrame.methodName ?: @"(unnamed method)"; if (stackFrame.file) { cell.detailTextLabel.text = [self formatFrameSource:stackFrame]; } else { @@ -375,7 +375,7 @@ RCT_EXPORT_MODULE() - (void)showErrorMessage:(NSString *)message { - [self showErrorMessage:message withStack:nil isUpdate:NO]; + [self showErrorMessage:message withParsedStack:nil isUpdate:NO]; } - (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details @@ -383,36 +383,42 @@ RCT_EXPORT_MODULE() [self showErrorMessage:message withDetails:details stack:nil]; } -- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details stack:(NSArray *)stack { +- (void)showErrorMessage:(NSString *)message withDetails:(NSString *)details stack:(NSArray *)stack { NSString *combinedMessage = message; if (details) { combinedMessage = [NSString stringWithFormat:@"%@\n\n%@", message, details]; } - [self showErrorMessage:combinedMessage withStack:stack isUpdate:NO]; + [self showErrorMessage:combinedMessage withParsedStack:stack isUpdate:NO]; } - (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack { NSArray *stack = [RCTJSStackFrame stackFramesWithLines:rawStack]; - [self showErrorMessage:message withStack:stack isUpdate:NO]; + [self showErrorMessage:message withParsedStack:stack isUpdate:NO]; } -- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack +- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack { - [self showErrorMessage:message withStack:stack isUpdate:NO]; + [self showErrorMessage:message withParsedStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:NO]; } -- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack +- (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack { - [self showErrorMessage:message withStack:stack isUpdate:YES]; + [self showErrorMessage:message withParsedStack:[RCTJSStackFrame stackFramesWithDictionaries:stack] isUpdate:YES]; } -- (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack isUpdate:(BOOL)isUpdate +- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray *)stack { - if (![[stack firstObject] isKindOfClass:[RCTJSStackFrame class]]) { - stack = [RCTJSStackFrame stackFramesWithDictionaries:stack]; - } + [self showErrorMessage:message withParsedStack:stack isUpdate:NO]; +} +- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray *)stack +{ + [self showErrorMessage:message withParsedStack:stack isUpdate:YES]; +} + +- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray *)stack isUpdate:(BOOL)isUpdate +{ dispatch_async(dispatch_get_main_queue(), ^{ if (!self->_window) { self->_window = [[RCTRedBoxWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; @@ -441,7 +447,8 @@ RCT_EXPORT_METHOD(dismiss) - (void)redBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow openStackFrameInEditor:(RCTJSStackFrame *)stackFrame { - if (![_bridge.bundleURL.scheme hasPrefix:@"http"]) { + NSURL *const bundleURL = _overrideBundleURL ?: _bridge.bundleURL; + if (![bundleURL.scheme hasPrefix:@"http"]) { RCTLogWarn(@"Cannot open stack frame in editor because you're not connected to the packager."); return; } @@ -449,7 +456,7 @@ RCT_EXPORT_METHOD(dismiss) NSData *stackFrameJSON = [RCTJSONStringify([stackFrame toDictionary], NULL) dataUsingEncoding:NSUTF8StringEncoding]; NSString *postLength = [NSString stringWithFormat:@"%tu", stackFrameJSON.length]; NSMutableURLRequest *request = [NSMutableURLRequest new]; - request.URL = [NSURL URLWithString:@"/open-stack-frame" relativeToURL:_bridge.bundleURL]; + request.URL = [NSURL URLWithString:@"/open-stack-frame" relativeToURL:bundleURL]; request.HTTPMethod = @"POST"; request.HTTPBody = stackFrameJSON; [request setValue:postLength forHTTPHeaderField:@"Content-Length"]; @@ -460,7 +467,11 @@ RCT_EXPORT_METHOD(dismiss) - (void)reloadFromRedBoxWindow:(__unused RCTRedBoxWindow *)redBoxWindow { - [_bridge reload]; + if (_overrideReloadAction) { + _overrideReloadAction(); + } else { + [_bridge reload]; + } [self dismiss]; } @@ -487,6 +498,8 @@ RCT_EXPORT_METHOD(dismiss) - (void)showErrorMessage:(NSString *)message withRawStack:(NSString *)rawStack {} - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack {} - (void)updateErrorMessage:(NSString *)message withStack:(NSArray *)stack {} +- (void)showErrorMessage:(NSString *)message withParsedStack:(NSArray *)stack {} +- (void)updateErrorMessage:(NSString *)message withParsedStack:(NSArray *)stack {} - (void)showErrorMessage:(NSString *)message withStack:(NSArray *)stack isUpdate:(BOOL)isUpdate {} - (void)dismiss {}