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#if DEBUG 103 return NO; 104#else 105 // we only need this because the bundle URL of prod home never changes, so we need 106 // to use the legacy logic and load embedded home if and only if a cached copy doesn't exist. 107 return [super isUsingEmbeddedResource]; 108#endif 109 } 110} 111 112- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response 113{ 114 if (![response.MIMEType isEqualToString:@"application/javascript"]) { 115 NSString *errDescription = [self _getContentErrorDescriptionForResponse:((NSHTTPURLResponse *)response).allHeaderFields data:data]; 116 return [NSError errorWithDomain:NSURLErrorDomain 117 code:NSURLErrorCannotParseResponse 118 userInfo:@{ 119 NSLocalizedDescriptionKey: errDescription, 120 @"response": response, 121 @"data": data 122 }]; 123 } 124 return nil; 125} 126 127- (NSString *)_getContentErrorDescriptionForResponse:(NSDictionary *)headers data:(NSData *)data 128{ 129 NSString *result; 130 NSString *responseContentType = headers[@"Content-Type"]; 131 if ([responseContentType isEqualToString:@"application/json"]) { 132 NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 133 result = [NSString stringWithFormat:@"Expected JavaScript, but got JSON: %@", dataString]; 134 } else { 135 NSString *recoverySuggestion = @"Check that your internet connection is working."; 136 if ([responseContentType rangeOfString:@"text"].length > 0) { 137 NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 138 if ([dataString rangeOfString:@"tunnel"].length > 0) { 139 recoverySuggestion = @"Check that your internet connection is working and try restarting your tunnel."; 140 } 141 } 142 result = [NSString stringWithFormat:@"Expected JavaScript, but got content type '%@'. %@", responseContentType, recoverySuggestion]; 143 } 144 return result; 145} 146 147@end 148