mirror of
https://github.com/status-im/react-native.git
synced 2025-01-17 04:50:59 +00:00
c7a590655c
Summary: This diff exposes a new experimental method `[RCTJavaScriptLoader +attemptSynchronousLoadOfBundleAtURL:sourceLength:error:]`. It may be used if you know that a specific call site must load its JavaScript synchronously, or else fail entirely. This new API will succeed for file URLs that point to a RAM bundle. It will fail for non-RAM-bundle files and for HTTP URLs. This also cleans up the error domain and codes for this class. This should be the only externally visible change from this diff if you don't use the new API: the codes and domains you receive from the API may change slightly. They were pretty sloppy and undocumented before, so I think this change is for the better. Reviewed By: bnham Differential Revision: D3545956 fbshipit-source-id: 30e65f4e8330d2d68f3f50ade077fdc1db4a435e
240 lines
9.2 KiB
Objective-C
Executable File
240 lines
9.2 KiB
Objective-C
Executable File
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "RCTJavaScriptLoader.h"
|
|
|
|
#import "RCTBridge.h"
|
|
#import "RCTConvert.h"
|
|
#import "RCTSourceCode.h"
|
|
#import "RCTUtils.h"
|
|
#import "RCTPerformanceLogger.h"
|
|
|
|
#include <sys/stat.h>
|
|
|
|
uint32_t const RCTRAMBundleMagicNumber = 0xFB0BD1E5;
|
|
|
|
NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain";
|
|
|
|
@implementation RCTJavaScriptLoader
|
|
|
|
RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|
|
|
+ (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComplete
|
|
{
|
|
int64_t sourceLength;
|
|
NSError *error;
|
|
NSData *data = [self attemptSynchronousLoadOfBundleAtURL:scriptURL
|
|
sourceLength:&sourceLength
|
|
allowLoadingNonRAMBundles:NO // we'll do it async
|
|
error:&error];
|
|
if (data) {
|
|
onComplete(nil, data, sourceLength);
|
|
return;
|
|
}
|
|
|
|
const BOOL isCannotLoadSyncError =
|
|
[error.domain isEqualToString:RCTJavaScriptLoaderErrorDomain]
|
|
&& error.code == RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously;
|
|
|
|
if (isCannotLoadSyncError) {
|
|
attemptAsynchronousLoadOfBundleAtURL(scriptURL, onComplete);
|
|
} else {
|
|
onComplete(error, nil, 0);
|
|
}
|
|
}
|
|
|
|
+ (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL
|
|
sourceLength:(int64_t *)sourceLength
|
|
allowLoadingNonRAMBundles:(BOOL)allowLoadingNonRAMBundles
|
|
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. "
|
|
@"unsanitizedScriptURLString:(%@)", unsanitizedScriptURLString]}];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
// Load local script file
|
|
if (!scriptURL.fileURL) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
|
|
code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
|
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
@"Cannot load non-file URLs synchronously"}];
|
|
}
|
|
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;
|
|
}
|
|
|
|
uint32_t magicNumber;
|
|
size_t readResult = fread(&magicNumber, sizeof(magicNumber), 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;
|
|
}
|
|
|
|
magicNumber = NSSwapLittleIntToHost(magicNumber);
|
|
if (magicNumber != RCTRAMBundleMagicNumber) {
|
|
if (allowLoadingNonRAMBundles) {
|
|
NSData *source = [NSData dataWithContentsOfFile:scriptURL.path
|
|
options:NSDataReadingMappedIfSafe
|
|
error:error];
|
|
if (sourceLength && source != nil) {
|
|
*sourceLength = source.length;
|
|
}
|
|
return source;
|
|
}
|
|
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
|
|
code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
|
|
userInfo:@{NSLocalizedDescriptionKey:
|
|
@"Cannot load non-RAM bundled files synchronously"}];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
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:&magicNumber length:sizeof(magicNumber)];
|
|
}
|
|
|
|
static void attemptAsynchronousLoadOfBundleAtURL(NSURL *scriptURL, 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, source, source.length);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Load remote script file
|
|
NSURLSessionDataTask *task =
|
|
[[NSURLSession sharedSession] dataTaskWithURL:scriptURL completionHandler:
|
|
^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
|
|
// 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\n"
|
|
"URL: " stringByAppendingString:scriptURL.absoluteString],
|
|
NSLocalizedFailureReasonErrorKey: error.localizedDescription,
|
|
NSUnderlyingErrorKey: error,
|
|
}];
|
|
}
|
|
onComplete(error, nil, 0);
|
|
return;
|
|
}
|
|
|
|
// Parse response as text
|
|
NSStringEncoding encoding = NSUTF8StringEncoding;
|
|
if (response.textEncodingName != nil) {
|
|
CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName);
|
|
if (cfEncoding != kCFStringEncodingInvalidId) {
|
|
encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding);
|
|
}
|
|
}
|
|
// Handle HTTP errors
|
|
if ([response isKindOfClass:[NSHTTPURLResponse class]] && ((NSHTTPURLResponse *)response).statusCode != 200) {
|
|
error = [NSError errorWithDomain:@"JSServer"
|
|
code:((NSHTTPURLResponse *)response).statusCode
|
|
userInfo:userInfoForRawResponse([[NSString alloc] initWithData:data encoding:encoding])];
|
|
onComplete(error, nil, 0);
|
|
return;
|
|
}
|
|
onComplete(nil, data, data.length);
|
|
}];
|
|
[task resume];
|
|
}
|
|
|
|
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 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
|