1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXApiUtil.h" 4#import "EXJavaScriptResource.h" 5#import "EXKernelUtil.h" 6#import "EXShellManager.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 NSString *cachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 30 NSString *sourceDirectory = [cachesDirectory stringByAppendingPathComponent:@"Sources"]; 31 32 BOOL cacheDirectoryExists = [[NSFileManager defaultManager] fileExistsAtPath:sourceDirectory isDirectory:nil]; 33 if (!cacheDirectoryExists) { 34 NSError *error; 35 BOOL created = [[NSFileManager defaultManager] createDirectoryAtPath:sourceDirectory 36 withIntermediateDirectories:YES 37 attributes:nil 38 error:&error]; 39 if (created) { 40 cacheDirectoryExists = YES; 41 } else { 42 DDLogError(@"Could not create source cache directory: %@", error.localizedDescription); 43 } 44 } 45 46 return (cacheDirectoryExists) ? sourceDirectory : nil; 47} 48 49+ (NSURLCache *)javaScriptCache 50{ 51 static NSURLCache *cache; 52 53 static dispatch_once_t onceToken; 54 dispatch_once(&onceToken, ^{ 55 NSString *sourceDirectory = [self javaScriptCachePath]; 56 if (sourceDirectory) { 57 cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:sourceDirectory]; 58 } 59 }); 60 61 return cache; 62} 63 64- (void)loadResourceWithBehavior:(EXCachedResourceBehavior)behavior 65 progressBlock:(__nullable EXCachedResourceProgressBlock)progressBlock 66 successBlock:(EXCachedResourceSuccessBlock)successBlock 67 errorBlock:(EXCachedResourceErrorBlock)errorBlock 68{ 69 // For dev builds that use the packager use RCTJavaScriptLoader which handles the packager's multipart 70 // responses to show loading progress. 71 if (self.devToolsEnabled) { 72 __block EXLoadingProgress *progress = [EXLoadingProgress new]; 73 [RCTJavaScriptLoader loadBundleAtURL:self.remoteUrl onProgress:^(RCTLoadingProgress *progressData) { 74 progress.total = progressData.total; 75 progress.done = progressData.done; 76 progress.status = progressData.status ?: @"Building JavaScript bundle..."; 77 if (progressBlock) { 78 progressBlock(progress); 79 } 80 } onComplete:^(NSError *error, RCTSource *source) { 81 if (error != nil) { 82 // In case we received something else than JS add more info to the error specific to expo for 83 // things like tunnel errors. 84 if ([error.domain isEqualToString:@"JSServer"] && error.code == NSURLErrorCannotParseResponse) { 85 NSString *errDescription = [self _getContentErrorDescriptionForResponse:error.userInfo[@"headers"] data:error.userInfo[@"data"]]; 86 error = [NSError errorWithDomain:NSURLErrorDomain 87 code:NSURLErrorCannotParseResponse 88 userInfo:@{ 89 NSLocalizedDescriptionKey: errDescription, 90 @"headers": error.userInfo[@"headers"], 91 @"data": error.userInfo[@"data"] 92 }]; 93 } 94 errorBlock(error); 95 } else { 96 successBlock(source.data); 97 } 98 }]; 99 } else { 100 [super loadResourceWithBehavior:behavior 101 progressBlock:progressBlock 102 successBlock:successBlock 103 errorBlock:errorBlock]; 104 } 105} 106 107- (BOOL)isUsingEmbeddedResource 108{ 109 // if the URL of our request matches the remote URL of the embedded JS bundle, 110 // skip checking any caches and just immediately open the NSBundle copy 111 if ([EXShellManager sharedInstance].isShell && 112 [EXShellManager sharedInstance].embeddedBundleUrl && 113 [self.remoteUrl isEqual:[EXApiUtil encodedUrlFromString:[EXShellManager sharedInstance].embeddedBundleUrl]]) { 114 return YES; 115 } else { 116 return [super isUsingEmbeddedResource]; 117 } 118} 119 120- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response 121{ 122 if (![response.MIMEType isEqualToString:@"application/javascript"]) { 123 NSString *errDescription = [self _getContentErrorDescriptionForResponse:((NSHTTPURLResponse *)response).allHeaderFields data:data]; 124 return [NSError errorWithDomain:NSURLErrorDomain 125 code:NSURLErrorCannotParseResponse 126 userInfo:@{ 127 NSLocalizedDescriptionKey: errDescription, 128 @"response": response, 129 @"data": data 130 }]; 131 } 132 return nil; 133} 134 135- (NSString *)_getContentErrorDescriptionForResponse:(NSDictionary *)headers data:(NSData *)data 136{ 137 NSString *result; 138 NSString *responseContentType = headers[@"Content-Type"]; 139 if ([responseContentType isEqualToString:@"application/json"]) { 140 NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 141 result = [NSString stringWithFormat:@"Expected JavaScript, but got JSON: %@", dataString]; 142 } else { 143 NSString *recoverySuggestion = @"Check that your internet connection is working."; 144 if ([responseContentType rangeOfString:@"text"].length > 0) { 145 NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 146 if ([dataString rangeOfString:@"tunnel"].length > 0) { 147 recoverySuggestion = @"Check that your internet connection is working and try restarting your tunnel."; 148 } 149 } 150 result = [NSString stringWithFormat:@"Expected JavaScript, but got content type '%@'. %@", responseContentType, recoverySuggestion]; 151 } 152 return result; 153} 154 155@end 156