19088dd0eSBen Roth// Copyright 2015-present 650 Industries. All rights reserved. 29088dd0eSBen Roth 39211cde4SEric Samelson#import "EXApiUtil.h" 4*00fcd3c6SBen Roth#import "EXEnvironment.h" 59088dd0eSBen Roth#import "EXJavaScriptResource.h" 69088dd0eSBen Roth#import "EXKernelUtil.h" 79088dd0eSBen Roth 89088dd0eSBen Roth#import <React/RCTJavaScriptLoader.h> 99088dd0eSBen Roth 109088dd0eSBen Roth@interface EXJavaScriptResource () 119088dd0eSBen Roth 129088dd0eSBen Roth@property (nonatomic, assign) BOOL devToolsEnabled; 139088dd0eSBen Roth 149088dd0eSBen Roth@end 159088dd0eSBen Roth 169088dd0eSBen Roth@implementation EXJavaScriptResource 179088dd0eSBen Roth 189088dd0eSBen Roth- (instancetype)initWithBundleName:(NSString *)bundleName remoteUrl:(NSURL *)url devToolsEnabled:(BOOL)devToolsEnabled 199088dd0eSBen Roth{ 209088dd0eSBen Roth if (self = [super initWithResourceName:bundleName resourceType:@"bundle" remoteUrl:url cachePath:[[self class] javaScriptCachePath]]) { 219088dd0eSBen Roth self.urlCache = [[self class] javaScriptCache]; 229088dd0eSBen Roth self.devToolsEnabled = devToolsEnabled; 239088dd0eSBen Roth } 249088dd0eSBen Roth return self; 259088dd0eSBen Roth} 269088dd0eSBen Roth 279088dd0eSBen Roth+ (NSString *)javaScriptCachePath 289088dd0eSBen Roth{ 299088dd0eSBen Roth NSString *cachesDirectory = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject; 309088dd0eSBen Roth NSString *sourceDirectory = [cachesDirectory stringByAppendingPathComponent:@"Sources"]; 319088dd0eSBen Roth 329088dd0eSBen Roth BOOL cacheDirectoryExists = [[NSFileManager defaultManager] fileExistsAtPath:sourceDirectory isDirectory:nil]; 339088dd0eSBen Roth if (!cacheDirectoryExists) { 349088dd0eSBen Roth NSError *error; 359088dd0eSBen Roth BOOL created = [[NSFileManager defaultManager] createDirectoryAtPath:sourceDirectory 369088dd0eSBen Roth withIntermediateDirectories:YES 379088dd0eSBen Roth attributes:nil 389088dd0eSBen Roth error:&error]; 399088dd0eSBen Roth if (created) { 409088dd0eSBen Roth cacheDirectoryExists = YES; 419088dd0eSBen Roth } else { 429088dd0eSBen Roth DDLogError(@"Could not create source cache directory: %@", error.localizedDescription); 439088dd0eSBen Roth } 449088dd0eSBen Roth } 459088dd0eSBen Roth 469088dd0eSBen Roth return (cacheDirectoryExists) ? sourceDirectory : nil; 479088dd0eSBen Roth} 489088dd0eSBen Roth 499088dd0eSBen Roth+ (NSURLCache *)javaScriptCache 509088dd0eSBen Roth{ 519088dd0eSBen Roth static NSURLCache *cache; 529088dd0eSBen Roth 539088dd0eSBen Roth static dispatch_once_t onceToken; 549088dd0eSBen Roth dispatch_once(&onceToken, ^{ 559088dd0eSBen Roth NSString *sourceDirectory = [self javaScriptCachePath]; 569088dd0eSBen Roth if (sourceDirectory) { 579088dd0eSBen Roth cache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:sourceDirectory]; 589088dd0eSBen Roth } 599088dd0eSBen Roth }); 609088dd0eSBen Roth 619088dd0eSBen Roth return cache; 629088dd0eSBen Roth} 639088dd0eSBen Roth 649088dd0eSBen Roth- (void)loadResourceWithBehavior:(EXCachedResourceBehavior)behavior 659088dd0eSBen Roth progressBlock:(__nullable EXCachedResourceProgressBlock)progressBlock 669088dd0eSBen Roth successBlock:(EXCachedResourceSuccessBlock)successBlock 679088dd0eSBen Roth errorBlock:(EXCachedResourceErrorBlock)errorBlock 689088dd0eSBen Roth{ 699088dd0eSBen Roth // For dev builds that use the packager use RCTJavaScriptLoader which handles the packager's multipart 709088dd0eSBen Roth // responses to show loading progress. 719088dd0eSBen Roth if (self.devToolsEnabled) { 729088dd0eSBen Roth __block EXLoadingProgress *progress = [EXLoadingProgress new]; 739088dd0eSBen Roth [RCTJavaScriptLoader loadBundleAtURL:self.remoteUrl onProgress:^(RCTLoadingProgress *progressData) { 749088dd0eSBen Roth progress.total = progressData.total; 759088dd0eSBen Roth progress.done = progressData.done; 769088dd0eSBen Roth progress.status = progressData.status ?: @"Building JavaScript bundle..."; 779088dd0eSBen Roth if (progressBlock) { 789088dd0eSBen Roth progressBlock(progress); 799088dd0eSBen Roth } 809088dd0eSBen Roth } onComplete:^(NSError *error, RCTSource *source) { 819088dd0eSBen Roth if (error != nil) { 829088dd0eSBen Roth // In case we received something else than JS add more info to the error specific to expo for 839088dd0eSBen Roth // things like tunnel errors. 849088dd0eSBen Roth if ([error.domain isEqualToString:@"JSServer"] && error.code == NSURLErrorCannotParseResponse) { 859088dd0eSBen Roth NSString *errDescription = [self _getContentErrorDescriptionForResponse:error.userInfo[@"headers"] data:error.userInfo[@"data"]]; 869088dd0eSBen Roth error = [NSError errorWithDomain:NSURLErrorDomain 879088dd0eSBen Roth code:NSURLErrorCannotParseResponse 889088dd0eSBen Roth userInfo:@{ 899088dd0eSBen Roth NSLocalizedDescriptionKey: errDescription, 909088dd0eSBen Roth @"headers": error.userInfo[@"headers"], 919088dd0eSBen Roth @"data": error.userInfo[@"data"] 929088dd0eSBen Roth }]; 939088dd0eSBen Roth } 949088dd0eSBen Roth errorBlock(error); 959088dd0eSBen Roth } else { 969088dd0eSBen Roth successBlock(source.data); 979088dd0eSBen Roth } 989088dd0eSBen Roth }]; 999088dd0eSBen Roth } else { 1009088dd0eSBen Roth [super loadResourceWithBehavior:behavior 1019088dd0eSBen Roth progressBlock:progressBlock 1029088dd0eSBen Roth successBlock:successBlock 1039088dd0eSBen Roth errorBlock:errorBlock]; 1049088dd0eSBen Roth } 1059088dd0eSBen Roth} 1069088dd0eSBen Roth 1079211cde4SEric Samelson- (BOOL)isUsingEmbeddedResource 1089211cde4SEric Samelson{ 1099211cde4SEric Samelson // if the URL of our request matches the remote URL of the embedded JS bundle, 1109211cde4SEric Samelson // skip checking any caches and just immediately open the NSBundle copy 111*00fcd3c6SBen Roth if ([EXEnvironment sharedEnvironment].isShell && 112*00fcd3c6SBen Roth [EXEnvironment sharedEnvironment].embeddedBundleUrl && 113*00fcd3c6SBen Roth [self.remoteUrl isEqual:[EXApiUtil encodedUrlFromString:[EXEnvironment sharedEnvironment].embeddedBundleUrl]]) { 1149211cde4SEric Samelson return YES; 1159211cde4SEric Samelson } else { 1169211cde4SEric Samelson return [super isUsingEmbeddedResource]; 1179211cde4SEric Samelson } 1189211cde4SEric Samelson} 1199211cde4SEric Samelson 1209088dd0eSBen Roth- (NSError *)_validateResponseData:(NSData *)data response:(NSURLResponse *)response 1219088dd0eSBen Roth{ 1229088dd0eSBen Roth if (![response.MIMEType isEqualToString:@"application/javascript"]) { 1239088dd0eSBen Roth NSString *errDescription = [self _getContentErrorDescriptionForResponse:((NSHTTPURLResponse *)response).allHeaderFields data:data]; 1249088dd0eSBen Roth return [NSError errorWithDomain:NSURLErrorDomain 1259088dd0eSBen Roth code:NSURLErrorCannotParseResponse 1269088dd0eSBen Roth userInfo:@{ 1279088dd0eSBen Roth NSLocalizedDescriptionKey: errDescription, 1289088dd0eSBen Roth @"response": response, 1299088dd0eSBen Roth @"data": data 1309088dd0eSBen Roth }]; 1319088dd0eSBen Roth } 1329088dd0eSBen Roth return nil; 1339088dd0eSBen Roth} 1349088dd0eSBen Roth 1359088dd0eSBen Roth- (NSString *)_getContentErrorDescriptionForResponse:(NSDictionary *)headers data:(NSData *)data 1369088dd0eSBen Roth{ 1379088dd0eSBen Roth NSString *result; 1389088dd0eSBen Roth NSString *responseContentType = headers[@"Content-Type"]; 1399088dd0eSBen Roth if ([responseContentType isEqualToString:@"application/json"]) { 1409088dd0eSBen Roth NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 1419088dd0eSBen Roth result = [NSString stringWithFormat:@"Expected JavaScript, but got JSON: %@", dataString]; 1429088dd0eSBen Roth } else { 1439088dd0eSBen Roth NSString *recoverySuggestion = @"Check that your internet connection is working."; 1449088dd0eSBen Roth if ([responseContentType rangeOfString:@"text"].length > 0) { 1459088dd0eSBen Roth NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 1469088dd0eSBen Roth if ([dataString rangeOfString:@"tunnel"].length > 0) { 1479088dd0eSBen Roth recoverySuggestion = @"Check that your internet connection is working and try restarting your tunnel."; 1489088dd0eSBen Roth } 1499088dd0eSBen Roth } 1509088dd0eSBen Roth result = [NSString stringWithFormat:@"Expected JavaScript, but got content type '%@'. %@", responseContentType, recoverySuggestion]; 1519088dd0eSBen Roth } 1529088dd0eSBen Roth return result; 1539088dd0eSBen Roth} 1549088dd0eSBen Roth 1559088dd0eSBen Roth@end 156