react-native/React/Base/RCTJavaScriptLoader.mm

366 lines
14 KiB
Plaintext
Executable File

/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* 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 <jschelpers/JavaScriptCore.h>
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTMultipartDataTask.h"
#import "RCTPerformanceLogger.h"
#import "RCTUtils.h"
NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain";
@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