123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- /*
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
- #import "RCTJavaScriptLoader.h"
- #import <sys/stat.h>
- #import <cxxreact/JSBundleType.h>
- #import "RCTBridge.h"
- #import "RCTConvert.h"
- #import "RCTMultipartDataTask.h"
- #import "RCTPerformanceLogger.h"
- #import "RCTUtils.h"
- NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain";
- static const int32_t JSNoBytecodeFileFormatVersion = -1;
- @interface RCTSource () {
- @public
- NSURL *_url;
- NSData *_data;
- NSUInteger _length;
- NSInteger _filesChangedCount;
- }
- @end
- @implementation RCTSource
- static RCTSource *RCTSourceCreate(NSURL *url, NSData *data, int64_t length) NS_RETURNS_RETAINED
- {
- RCTSource *source = [RCTSource new];
- source->_url = url;
- source->_data = data;
- source->_length = length;
- source->_filesChangedCount = RCTSourceFilesChangedCountNotBuiltByBundler;
- return source;
- }
- @end
- @implementation RCTLoadingProgress
- - (NSString *)description
- {
- NSMutableString *desc = [NSMutableString new];
- [desc appendString:_status ?: @"Loading"];
- if ([_total integerValue] > 0) {
- [desc appendFormat:@" %ld%% (%@/%@)", (long)(100 * [_done integerValue] / [_total integerValue]), _done, _total];
- }
- [desc appendString:@"\u2026"];
- return desc;
- }
- @end
- @implementation RCTJavaScriptLoader
- RCT_NOT_IMPLEMENTED(-(instancetype)init)
- + (void)loadBundleAtURL:(NSURL *)scriptURL
- onProgress:(RCTSourceLoadProgressBlock)onProgress
- onComplete:(RCTSourceLoadBlock)onComplete
- {
- int64_t sourceLength;
- NSError *error;
- NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL
- runtimeBCVersion:JSNoBytecodeFileFormatVersion
- sourceLength:&sourceLength
- error:&error];
- if (data) {
- onComplete(nil, RCTSourceCreate(scriptURL, data, sourceLength));
- return;
- }
- const BOOL isCannotLoadSyncError = [error.domain isEqualToString:RCTJavaScriptLoaderErrorDomain] &&
- error.code == RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously;
- if (isCannotLoadSyncError) {
- attemptAsynchronousLoadOfBundleAtURL(scriptURL, onProgress, onComplete);
- } else {
- onComplete(error, nil);
- }
- }
- + (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL
- runtimeBCVersion:(int32_t)runtimeBCVersion
- sourceLength:(int64_t *)sourceLength
- error:(NSError **)error
- {
- NSString *unsanitizedScriptURLString = scriptURL.absoluteString;
- // Sanitize the script URL
- scriptURL = sanitizeURL(scriptURL);
- if (!scriptURL) {
- if (error) {
- *error = [NSError
- errorWithDomain:RCTJavaScriptLoaderErrorDomain
- code:RCTJavaScriptLoaderErrorNoScriptURL
- userInfo:@{
- NSLocalizedDescriptionKey : [NSString
- stringWithFormat:@"No script URL provided. Make sure the packager is "
- @"running or you have embedded a JS bundle in your application bundle.\n\n"
- @"unsanitizedScriptURLString = %@",
- unsanitizedScriptURLString]
- }];
- }
- return nil;
- }
- // Load local script file
- if (!scriptURL.fileURL) {
- if (error) {
- *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
- code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
- userInfo:@{
- NSLocalizedDescriptionKey :
- [NSString stringWithFormat:@"Cannot load %@ URLs synchronously", scriptURL.scheme]
- }];
- }
- return nil;
- }
- // Load the first 4 bytes to check if the bundle is regular or RAM ("Random Access Modules" bundle).
- // The RAM bundle has a magic number in the 4 first bytes `(0xFB0BD1E5)`.
- // The benefit of RAM bundle over a regular bundle is that we can lazily inject
- // modules into JSC as they're required.
- FILE *bundle = fopen(scriptURL.path.UTF8String, "r");
- if (!bundle) {
- if (error) {
- *error = [NSError
- errorWithDomain:RCTJavaScriptLoaderErrorDomain
- code:RCTJavaScriptLoaderErrorFailedOpeningFile
- userInfo:@{
- NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error opening bundle %@", scriptURL.path]
- }];
- }
- return nil;
- }
- facebook::react::BundleHeader header;
- size_t readResult = fread(&header, sizeof(header), 1, bundle);
- fclose(bundle);
- if (readResult != 1) {
- if (error) {
- *error = [NSError
- errorWithDomain:RCTJavaScriptLoaderErrorDomain
- code:RCTJavaScriptLoaderErrorFailedReadingFile
- userInfo:@{
- NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error reading bundle %@", scriptURL.path]
- }];
- }
- return nil;
- }
- facebook::react::ScriptTag tag = facebook::react::parseTypeFromHeader(header);
- switch (tag) {
- case facebook::react::ScriptTag::RAMBundle:
- break;
- case facebook::react::ScriptTag::String: {
- #if RCT_ENABLE_INSPECTOR
- NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:error];
- if (sourceLength && source != nil) {
- *sourceLength = source.length;
- }
- return source;
- #else
- if (error) {
- *error =
- [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
- code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
- userInfo:@{NSLocalizedDescriptionKey : @"Cannot load text/javascript files synchronously"}];
- }
- return nil;
- #endif
- }
- case facebook::react::ScriptTag::BCBundle:
- if (runtimeBCVersion == JSNoBytecodeFileFormatVersion || runtimeBCVersion < 0) {
- if (error) {
- *error = [NSError
- errorWithDomain:RCTJavaScriptLoaderErrorDomain
- code:RCTJavaScriptLoaderErrorBCNotSupported
- userInfo:@{NSLocalizedDescriptionKey : @"Bytecode bundles are not supported by this runtime."}];
- }
- return nil;
- } else if ((uint32_t)runtimeBCVersion != header.version) {
- if (error) {
- NSString *errDesc = [NSString
- stringWithFormat:@"BC Version Mismatch. Expect: %d, Actual: %u", runtimeBCVersion, header.version];
- *error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
- code:RCTJavaScriptLoaderErrorBCVersion
- userInfo:@{NSLocalizedDescriptionKey : errDesc}];
- }
- return nil;
- }
- break;
- }
- struct stat statInfo;
- if (stat(scriptURL.path.UTF8String, &statInfo) != 0) {
- if (error) {
- *error = [NSError
- errorWithDomain:RCTJavaScriptLoaderErrorDomain
- code:RCTJavaScriptLoaderErrorFailedStatingFile
- userInfo:@{
- NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Error stating bundle %@", scriptURL.path]
- }];
- }
- return nil;
- }
- if (sourceLength) {
- *sourceLength = statInfo.st_size;
- }
- return [NSData dataWithBytes:&header length:sizeof(header)];
- }
- static void parseHeaders(NSDictionary *headers, RCTSource *source)
- {
- source->_filesChangedCount = [headers[@"X-Metro-Files-Changed-Count"] integerValue];
- }
- static void attemptAsynchronousLoadOfBundleAtURL(
- NSURL *scriptURL,
- RCTSourceLoadProgressBlock onProgress,
- RCTSourceLoadBlock onComplete)
- {
- scriptURL = sanitizeURL(scriptURL);
- if (scriptURL.fileURL) {
- // Reading in a large bundle can be slow. Dispatch to the background queue to do it.
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- NSError *error = nil;
- NSData *source = [NSData dataWithContentsOfFile:scriptURL.path options:NSDataReadingMappedIfSafe error:&error];
- onComplete(error, RCTSourceCreate(scriptURL, source, source.length));
- });
- return;
- }
- RCTMultipartDataTask *task = [[RCTMultipartDataTask alloc] initWithURL:scriptURL
- partHandler:^(NSInteger statusCode, NSDictionary *headers, NSData *data, NSError *error, BOOL done) {
- if (!done) {
- if (onProgress) {
- onProgress(progressEventFromData(data));
- }
- return;
- }
- // Handle general request errors
- if (error) {
- if ([error.domain isEqualToString:NSURLErrorDomain]) {
- error = [NSError
- errorWithDomain:RCTJavaScriptLoaderErrorDomain
- code:RCTJavaScriptLoaderErrorURLLoadFailed
- userInfo:@{
- NSLocalizedDescriptionKey :
- [@"Could not connect to development server.\n\n"
- "Ensure the following:\n"
- "- Node server is running and available on the same network - run 'npm start' from react-native root\n"
- "- Node server URL is correctly set in AppDelegate\n"
- "- WiFi is enabled and connected to the same network as the Node Server\n\n"
- "URL: " stringByAppendingString:scriptURL.absoluteString],
- NSLocalizedFailureReasonErrorKey : error.localizedDescription,
- NSUnderlyingErrorKey : error,
- }];
- }
- onComplete(error, nil);
- return;
- }
- // For multipart responses packager sets X-Http-Status header in case HTTP status code
- // is different from 200 OK
- NSString *statusCodeHeader = headers[@"X-Http-Status"];
- if (statusCodeHeader) {
- statusCode = [statusCodeHeader integerValue];
- }
- if (statusCode != 200) {
- error =
- [NSError errorWithDomain:@"JSServer"
- code:statusCode
- userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data
- encoding:NSUTF8StringEncoding])];
- onComplete(error, nil);
- return;
- }
- // Validate that the packager actually returned javascript.
- NSString *contentType = headers[@"Content-Type"];
- NSString *mimeType = [[contentType componentsSeparatedByString:@";"] firstObject];
- if (![mimeType isEqualToString:@"application/javascript"] && ![mimeType isEqualToString:@"text/javascript"]) {
- NSString *description = [NSString
- stringWithFormat:@"Expected MIME-Type to be 'application/javascript' or 'text/javascript', but got '%@'.",
- mimeType];
- error = [NSError
- errorWithDomain:@"JSServer"
- code:NSURLErrorCannotParseResponse
- userInfo:@{NSLocalizedDescriptionKey : description, @"headers" : headers, @"data" : data}];
- onComplete(error, nil);
- return;
- }
- RCTSource *source = RCTSourceCreate(scriptURL, data, data.length);
- parseHeaders(headers, source);
- onComplete(nil, source);
- }
- progressHandler:^(NSDictionary *headers, NSNumber *loaded, NSNumber *total) {
- // Only care about download progress events for the javascript bundle part.
- if ([headers[@"Content-Type"] isEqualToString:@"application/javascript"]) {
- onProgress(progressEventFromDownloadProgress(loaded, total));
- }
- }];
- [task startTask];
- }
- static NSURL *sanitizeURL(NSURL *url)
- {
- // Why we do this is lost to time. We probably shouldn't; passing a valid URL is the caller's responsibility not ours.
- return [RCTConvert NSURL:url.absoluteString];
- }
- static RCTLoadingProgress *progressEventFromData(NSData *rawData)
- {
- NSString *text = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding];
- id info = RCTJSONParse(text, nil);
- if (!info || ![info isKindOfClass:[NSDictionary class]]) {
- return nil;
- }
- RCTLoadingProgress *progress = [RCTLoadingProgress new];
- progress.status = info[@"status"];
- progress.done = info[@"done"];
- progress.total = info[@"total"];
- return progress;
- }
- static RCTLoadingProgress *progressEventFromDownloadProgress(NSNumber *total, NSNumber *done)
- {
- RCTLoadingProgress *progress = [RCTLoadingProgress new];
- progress.status = @"Downloading JavaScript bundle";
- // Progress values are in bytes transform them to kilobytes for smaller numbers.
- progress.done = done != nil ? @([done integerValue] / 1024) : nil;
- progress.total = total != nil ? @([total integerValue] / 1024) : nil;
- return progress;
- }
- static NSDictionary *userInfoForRawResponse(NSString *rawText)
- {
- NSDictionary *parsedResponse = RCTJSONParse(rawText, nil);
- if (![parsedResponse isKindOfClass:[NSDictionary class]]) {
- return @{NSLocalizedDescriptionKey : rawText};
- }
- NSArray *errors = parsedResponse[@"errors"];
- if (![errors isKindOfClass:[NSArray class]]) {
- return @{NSLocalizedDescriptionKey : rawText};
- }
- NSMutableArray<NSDictionary *> *fakeStack = [NSMutableArray new];
- for (NSDictionary *err in errors) {
- [fakeStack addObject:@{
- @"methodName" : err[@"description"] ?: @"",
- @"file" : err[@"filename"] ?: @"",
- @"lineNumber" : err[@"lineNumber"] ?: @0
- }];
- }
- return
- @{NSLocalizedDescriptionKey : parsedResponse[@"message"] ?: @"No message provided", @"stack" : [fakeStack copy]};
- }
- @end
|