RCTJavaScriptLoader.mm 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. /*
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. */
  7. #import "RCTJavaScriptLoader.h"
  8. #import <sys/stat.h>
  9. #import <cxxreact/JSBundleType.h>
  10. #import "RCTBridge.h"
  11. #import "RCTConvert.h"
  12. #import "RCTMultipartDataTask.h"
  13. #import "RCTPerformanceLogger.h"
  14. #import "RCTUtils.h"
  15. NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain";
  16. static const int32_t JSNoBytecodeFileFormatVersion = -1;
  17. @interface RCTSource () {
  18. @public
  19. NSURL *_url;
  20. NSData *_data;
  21. NSUInteger _length;
  22. NSInteger _filesChangedCount;
  23. }
  24. @end
  25. @implementation RCTSource
  26. static RCTSource *RCTSourceCreate(NSURL *url, NSData *data, int64_t length) NS_RETURNS_RETAINED
  27. {
  28. RCTSource *source = [RCTSource new];
  29. source->_url = url;
  30. source->_data = data;
  31. source->_length = length;
  32. source->_filesChangedCount = RCTSourceFilesChangedCountNotBuiltByBundler;
  33. return source;
  34. }
  35. @end
  36. @implementation RCTLoadingProgress
  37. - (NSString *)description
  38. {
  39. NSMutableString *desc = [NSMutableString new];
  40. [desc appendString:_status ?: @"Loading"];
  41. if ([_total integerValue] > 0) {
  42. [desc appendFormat:@" %ld%% (%@/%@)", (long)(100 * [_done integerValue] / [_total integerValue]), _done, _total];
  43. }
  44. [desc appendString:@"\u2026"];
  45. return desc;
  46. }
  47. @end
  48. @implementation RCTJavaScriptLoader
  49. RCT_NOT_IMPLEMENTED(-(instancetype)init)
  50. + (void)loadBundleAtURL:(NSURL *)scriptURL
  51. onProgress:(RCTSourceLoadProgressBlock)onProgress
  52. onComplete:(RCTSourceLoadBlock)onComplete
  53. {
  54. int64_t sourceLength;
  55. NSError *error;
  56. NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL
  57. runtimeBCVersion:JSNoBytecodeFileFormatVersion
  58. sourceLength:&sourceLength
  59. error:&error];
  60. if (data) {
  61. onComplete(nil, RCTSourceCreate(scriptURL, data, sourceLength));
  62. return;
  63. }
  64. const BOOL isCannotLoadSyncError = [error.domain isEqualToString:RCTJavaScriptLoaderErrorDomain] &&
  65. error.code == RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously;
  66. if (isCannotLoadSyncError) {
  67. attemptAsynchronousLoadOfBundleAtURL(scriptURL, onProgress, onComplete);
  68. } else {
  69. onComplete(error, nil);
  70. }
  71. }
  72. + (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL
  73. runtimeBCVersion:(int32_t)runtimeBCVersion
  74. sourceLength:(int64_t *)sourceLength
  75. error:(NSError **)error
  76. {
  77. NSString *unsanitizedScriptURLString = scriptURL.absoluteString;
  78. // Sanitize the script URL
  79. scriptURL = sanitizeURL(scriptURL);
  80. if (!scriptURL) {
  81. if (error) {
  82. *error = [NSError
  83. errorWithDomain:RCTJavaScriptLoaderErrorDomain
  84. code:RCTJavaScriptLoaderErrorNoScriptURL
  85. userInfo:@{
  86. NSLocalizedDescriptionKey : [NSString
  87. stringWithFormat:@"No script URL provided. Make sure the packager is "
  88. @"running or you have embedded a JS bundle in your application bundle.\n\n"
  89. @"unsanitizedScriptURLString = %@",
  90. unsanitizedScriptURLString]
  91. }];
  92. }
  93. return nil;
  94. }
  95. // Load local script file
  96. if (!scriptURL.fileURL) {
  97. if (error) {
  98. *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
  99. code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
  100. userInfo:@{
  101. NSLocalizedDescriptionKey :
  102. [NSString stringWithFormat:@"Cannot load %@ URLs synchronously", scriptURL.scheme]
  103. }];
  104. }
  105. return nil;
  106. }
  107. // Load the first 4 bytes to check if the bundle is regular or RAM ("Random Access Modules" bundle).
  108. // The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`.
  109. // The benefit of RAM bundle over a regular bundle is that we can lazily inject
  110. // modules into JSC as they're required.
  111. FILE *bundle = fopen(scriptURL.path.UTF8String, "r");
  112. if (!bundle) {
  113. if (error) {
  114. *error = [NSError
  115. errorWithDomain:RCTJavaScriptLoaderErrorDomain
  116. code:RCTJavaScriptLoaderErrorFailedOpeningFile
  117. userInfo:@{
  118. NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening bundle %@", scriptURL.path]
  119. }];
  120. }
  121. return nil;
  122. }
  123. facebook::react::BundleHeader header;
  124. size_t readResult = fread(&header, sizeof(header), 1, bundle);
  125. fclose(bundle);
  126. if (readResult != 1) {
  127. if (error) {
  128. *error = [NSError
  129. errorWithDomain:RCTJavaScriptLoaderErrorDomain
  130. code:RCTJavaScriptLoaderErrorFailedReadingFile
  131. userInfo:@{
  132. NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error reading bundle %@", scriptURL.path]
  133. }];
  134. }
  135. return nil;
  136. }
  137. facebook::react::ScriptTag tag = facebook::react::parseTypeFromHeader(header);
  138. switch (tag) {
  139. case facebook::react::ScriptTag::RAMBundle:
  140. break;
  141. case facebook::react::ScriptTag::String: {
  142. #if RCT_ENABLE_INSPECTOR
  143. NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:error];
  144. if (sourceLength && source != nil) {
  145. *sourceLength = source.length;
  146. }
  147. return source;
  148. #else
  149. if (error) {
  150. *error =
  151. [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
  152. code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
  153. userInfo:@{NSLocalizedDescriptionKey : @"Cannot load text/javascript files synchronously"}];
  154. }
  155. return nil;
  156. #endif
  157. }
  158. case facebook::react::ScriptTag::BCBundle:
  159. if (runtimeBCVersion == JSNoBytecodeFileFormatVersion || runtimeBCVersion < 0) {
  160. if (error) {
  161. *error = [NSError
  162. errorWithDomain:RCTJavaScriptLoaderErrorDomain
  163. code:RCTJavaScriptLoaderErrorBCNotSupported
  164. userInfo:@{NSLocalizedDescriptionKey : @"Bytecode bundles are not supported by this runtime."}];
  165. }
  166. return nil;
  167. } else if ((uint32_t)runtimeBCVersion != header.version) {
  168. if (error) {
  169. NSString *errDesc = [NSString
  170. stringWithFormat:@"BC Version Mismatch. Expect: %d, Actual: %u", runtimeBCVersion, header.version];
  171. *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
  172. code:RCTJavaScriptLoaderErrorBCVersion
  173. userInfo:@{NSLocalizedDescriptionKey : errDesc}];
  174. }
  175. return nil;
  176. }
  177. break;
  178. }
  179. struct stat statInfo;
  180. if (stat(scriptURL.path.UTF8String, &statInfo) != 0) {
  181. if (error) {
  182. *error = [NSError
  183. errorWithDomain:RCTJavaScriptLoaderErrorDomain
  184. code:RCTJavaScriptLoaderErrorFailedStatingFile
  185. userInfo:@{
  186. NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error stating bundle %@", scriptURL.path]
  187. }];
  188. }
  189. return nil;
  190. }
  191. if (sourceLength) {
  192. *sourceLength = statInfo.st_size;
  193. }
  194. return [NSData dataWithBytes:&header length:sizeof(header)];
  195. }
  196. static void parseHeaders(NSDictionary *headers, RCTSource *source)
  197. {
  198. source->_filesChangedCount = [headers[@"X-Metro-Files-Changed-Count"] integerValue];
  199. }
  200. static void attemptAsynchronousLoadOfBundleAtURL(
  201. NSURL *scriptURL,
  202. RCTSourceLoadProgressBlock onProgress,
  203. RCTSourceLoadBlock onComplete)
  204. {
  205. scriptURL = sanitizeURL(scriptURL);
  206. if (scriptURL.fileURL) {
  207. // Reading in a large bundle can be slow. Dispatch to the background queue to do it.
  208. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  209. NSError *error = nil;
  210. NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:&error];
  211. onComplete(error, RCTSourceCreate(scriptURL, source, source.length));
  212. });
  213. return;
  214. }
  215. RCTMultipartDataTask *task = [[RCTMultipartDataTask alloc] initWithURL:scriptURL
  216. partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) {
  217. if (!done) {
  218. if (onProgress) {
  219. onProgress(progressEventFromData(data));
  220. }
  221. return;
  222. }
  223. // Handle general request errors
  224. if (error) {
  225. if ([error.domain isEqualToString:NSURLErrorDomain]) {
  226. error = [NSError
  227. errorWithDomain:RCTJavaScriptLoaderErrorDomain
  228. code:RCTJavaScriptLoaderErrorURLLoadFailed
  229. userInfo:@{
  230. NSLocalizedDescriptionKey :
  231. [@"Could not connect to development server.\n\n"
  232. "Ensure the following:\n"
  233. "- Node server is running and available on the same network - run 'npm start' from react-native root\n"
  234. "- Node server URL is correctly set in AppDelegate\n"
  235. "- WiFi is enabled and connected to the same network as the Node Server\n\n"
  236. "URL: " stringByAppendingString:scriptURL.absoluteString],
  237. NSLocalizedFailureReasonErrorKey : error.localizedDescription,
  238. NSUnderlyingErrorKey : error,
  239. }];
  240. }
  241. onComplete(error, nil);
  242. return;
  243. }
  244. // For multipart responses packager sets X-Http-Status header in case HTTP status code
  245. // is different from 200 OK
  246. NSString *statusCodeHeader = headers[@"X-Http-Status"];
  247. if (statusCodeHeader) {
  248. statusCode = [statusCodeHeader integerValue];
  249. }
  250. if (statusCode != 200) {
  251. error =
  252. [NSError errorWithDomain:@"JSServer"
  253. code:statusCode
  254. userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data
  255. encoding:NSUTF8StringEncoding])];
  256. onComplete(error, nil);
  257. return;
  258. }
  259. // Validate that the packager actually returned javascript.
  260. NSString *contentType = headers[@"Content-Type"];
  261. NSString *mimeType = [[contentType componentsSeparatedByString:@";"] firstObject];
  262. if (![mimeType isEqualToString:@"application/javascript"] && ![mimeType isEqualToString:@"text/javascript"]) {
  263. NSString *description = [NSString
  264. stringWithFormat:@"Expected MIME-Type to be 'application/javascript' or 'text/javascript', but got '%@'.",
  265. mimeType];
  266. error = [NSError
  267. errorWithDomain:@"JSServer"
  268. code:NSURLErrorCannotParseResponse
  269. userInfo:@{NSLocalizedDescriptionKey : description, @"headers" : headers, @"data" : data}];
  270. onComplete(error, nil);
  271. return;
  272. }
  273. RCTSource *source = RCTSourceCreate(scriptURL, data, data.length);
  274. parseHeaders(headers, source);
  275. onComplete(nil, source);
  276. }
  277. progressHandler:^(NSDictionary *headers, NSNumber *loaded, NSNumber *total) {
  278. // Only care about download progress events for the javascript bundle part.
  279. if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"]) {
  280. onProgress(progressEventFromDownloadProgress(loaded, total));
  281. }
  282. }];
  283. [task startTask];
  284. }
  285. static NSURL *sanitizeURL(NSURL *url)
  286. {
  287. // Why we do this is lost to time. We probably shouldn't; passing a valid URL is the caller's responsibility not ours.
  288. return [RCTConvert NSURL:url.absoluteString];
  289. }
  290. static RCTLoadingProgress *progressEventFromData(NSData *rawData)
  291. {
  292. NSString *text = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding];
  293. id info = RCTJSONParse(text, nil);
  294. if (!info || ![info isKindOfClass:[NSDictionary class]]) {
  295. return nil;
  296. }
  297. RCTLoadingProgress *progress = [RCTLoadingProgress new];
  298. progress.status = info[@"status"];
  299. progress.done = info[@"done"];
  300. progress.total = info[@"total"];
  301. return progress;
  302. }
  303. static RCTLoadingProgress *progressEventFromDownloadProgress(NSNumber *total, NSNumber *done)
  304. {
  305. RCTLoadingProgress *progress = [RCTLoadingProgress new];
  306. progress.status = @"Downloading JavaScript bundle";
  307. // Progress values are in bytes transform them to kilobytes for smaller numbers.
  308. progress.done = done != nil ? @([done integerValue] / 1024) : nil;
  309. progress.total = total != nil ? @([total integerValue] / 1024) : nil;
  310. return progress;
  311. }
  312. static NSDictionary *userInfoForRawResponse(NSString *rawText)
  313. {
  314. NSDictionary *parsedResponse = RCTJSONParse(rawText, nil);
  315. if (![parsedResponse isKindOfClass:[NSDictionary class]]) {
  316. return @{NSLocalizedDescriptionKey : rawText};
  317. }
  318. NSArray *errors = parsedResponse[@"errors"];
  319. if (![errors isKindOfClass:[NSArray class]]) {
  320. return @{NSLocalizedDescriptionKey : rawText};
  321. }
  322. NSMutableArray<NSDictionary *> *fakeStack = [NSMutableArray new];
  323. for (NSDictionary *err in errors) {
  324. [fakeStack addObject:@{
  325. @"methodName" : err[@"description"] ?: @"",
  326. @"file" : err[@"filename"] ?: @"",
  327. @"lineNumber" : err[@"lineNumber"] ?: @0
  328. }];
  329. }
  330. return
  331. @{NSLocalizedDescriptionKey : parsedResponse[@"message"] ?: @"No message provided", @"stack" : [fakeStack copy]};
  332. }
  333. @end