1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXApiUtil.h" 4#import "EXEnvironment.h" 5#import "EXJavaScriptResource.h" 6#import "EXKernelUtil.h" 7 8#import <React/RCTJavaScriptLoader.h> 9 10@interface EXJavaScriptResource () 11 12@property (nonatomic, assign) BOOL devToolsEnabled; 13 14@end 15 16@implementation EXJavaScriptResource 17 18- (instancetype)initWithBundleName:(NSString *)bundleName remoteUrl:(NSURL *)url devToolsEnabled:(BOOL)devToolsEnabled 19{ 20 if (self = [super initWithResourceName:bundleName resourceType:@"bundle" remoteUrl:url cachePath:[[self class] javaScriptCachePath]]) { 21 self.urlCache = [[self class] javaScriptCache]; 22 self.devToolsEnabled = devToolsEnabled; 23 } 24 return self; 25} 26 27+ (NSString *)javaScriptCachePath 28{ 29 return [[self class] cachePathWithName:@"Sources"]; 30} 31 32+ (NSURLCache *)javaScriptCache 33{ 34 static NSURLCache *cache; 35 36 static dispatch_once_t onceToken; 37 dispatch_once(&onceToken, ^{ 38 NSString *sourceDirectory = [self javaScriptCachePath]; 39 if (sourceDirectory) { 40 cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:sourceDirectory]; 41 } 42 }); 43 44 return cache; 45} 46 47- (void)loadResourceWithBehavior:(EXCachedResourceBehavior)behavior 48 progressBlock:(__nullable EXCachedResourceProgressBlock)progressBlock 49 successBlock:(EXCachedResourceSuccessBlock)successBlock 50 errorBlock:(EXCachedResourceErrorBlock)errorBlock 51{ 52 // For dev builds that use the packager use RCTJavaScriptLoader which handles the packager's multipart 53 // responses to show loading progress. 54 if (self.devToolsEnabled) { 55 __block EXLoadingProgress *progress = [EXLoadingProgress new]; 56 [RCTJavaScriptLoader loadBundleAtURL:self.remoteUrl onProgress:^(RCTLoadingProgress *progressData) { 57 progress.total = progressData.total; 58 progress.done = progressData.done; 59 progress.status = progressData.status ?: @"Building JavaScript bundle..."; 60 if (progressBlock) { 61 progressBlock(progress); 62 } 63 } onComplete:^(NSError *error, RCTSource *source) { 64 if (error != nil) { 65 // In case we received something else than JS add more info to the error specific to expo for 66 // things like tunnel errors. 67 if ([error.domain isEqualToString:@"JSServer"] && error.code == NSURLErrorCannotParseResponse) { 68 NSString *errDescription = [self _getContentErrorDescriptionForResponse:error.userInfo[@"headers"] data:error.userInfo[@"data"]]; 69 error = [NSError errorWithDomain:NSURLErrorDomain 70 code:NSURLErrorCannotParseResponse 71 userInfo:@{ 72 NSLocalizedDescriptionKey: errDescription, 73 @"headers": error.userInfo[@"headers"], 74 @"data": error.userInfo[@"data"] 75 }]; 76 } 77 errorBlock(error); 78 } else { 79 successBlock(source.data); 80 } 81 }]; 82 } else { 83 [super loadResourceWithBehavior:behavior 84 progressBlock:progressBlock 85 successBlock:successBlock 86 errorBlock:errorBlock]; 87 } 88} 89 90- (BOOL)isUsingEmbeddedResource 91{ 92 if ([EXEnvironment sharedEnvironment].isDetached) { 93 // if the URL of our request matches the remote URL of the embedded JS bundle, 94 // skip checking any caches and just immediately open the NSBundle copy 95 if ([EXEnvironment sharedEnvironment].embeddedBundleUrl && 96 [self.remoteUrl isEqual:[EXApiUtil encodedUrlFromString:[EXEnvironment sharedEnvironment].embeddedBundleUrl]]) { 97 return YES; 98 } else { 99 return NO; 100 } 101 } else { 102 // we only need this because the bundle URL of prod home never changes, so we need 103 // to use the legacy logic and load embedded home if and only if a cached copy doesn't exist. 104 // TODO: get rid of this branch once prod home is loaded like any other bundle!!!!!!! 105 return [super isUsingEmbeddedResource]; 106 } 107} 108 109- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response 110{ 111 if (![response.MIMEType isEqualToString:@"application/javascript"]) { 112 NSString *errDescription = [self _getContentErrorDescriptionForResponse:((NSHTTPURLResponse *)response).allHeaderFields data:data]; 113 return [NSError errorWithDomain:NSURLErrorDomain 114 code:NSURLErrorCannotParseResponse 115 userInfo:@{ 116 NSLocalizedDescriptionKey: errDescription, 117 @"response": response, 118 @"data": data 119 }]; 120 } 121 return nil; 122} 123 124- (NSString *)_getContentErrorDescriptionForResponse:(NSDictionary *)headers data:(NSData *)data 125{ 126 NSString *result; 127 NSString *responseContentType = headers[@"Content-Type"]; 128 if ([responseContentType isEqualToString:@"application/json"]) { 129 NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 130 result = [NSString stringWithFormat:@"Expected JavaScript, but got JSON: %@", dataString]; 131 } else { 132 NSString *recoverySuggestion = @"Check that your internet connection is working."; 133 if ([responseContentType rangeOfString:@"text"].length > 0) { 134 NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 135 if ([dataString rangeOfString:@"tunnel"].length > 0) { 136 recoverySuggestion = @"Check that your internet connection is working and try restarting your tunnel."; 137 } 138 } 139 result = [NSString stringWithFormat:@"Expected JavaScript, but got content type '%@'. %@", responseContentType, recoverySuggestion]; 140 } 141 return result; 142} 143 144@end 145