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