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