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