Expose a method to synchronously load bundle if possible
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
This commit is contained in:
parent
80c71e5cae
commit
c7a590655c
|
@ -13,6 +13,18 @@
|
|||
|
||||
extern uint32_t const RCTRAMBundleMagicNumber;
|
||||
|
||||
extern NSString *const RCTJavaScriptLoaderErrorDomain;
|
||||
|
||||
NS_ENUM(NSInteger) {
|
||||
RCTJavaScriptLoaderErrorNoScriptURL = 1,
|
||||
RCTJavaScriptLoaderErrorFailedOpeningFile = 2,
|
||||
RCTJavaScriptLoaderErrorFailedReadingFile = 3,
|
||||
RCTJavaScriptLoaderErrorFailedStatingFile = 3,
|
||||
RCTJavaScriptLoaderErrorURLLoadFailed = 3,
|
||||
|
||||
RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously = 1000,
|
||||
};
|
||||
|
||||
@class RCTBridge;
|
||||
|
||||
/**
|
||||
|
@ -22,6 +34,19 @@ extern uint32_t const RCTRAMBundleMagicNumber;
|
|||
*/
|
||||
@interface RCTJavaScriptLoader : NSObject
|
||||
|
||||
+ (void)loadBundleAtURL:(NSURL *)moduleURL onComplete:(RCTSourceLoadBlock)onComplete;
|
||||
+ (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComplete;
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* Attempts to synchronously load the script at the given URL. The following two conditions must be met:
|
||||
* 1. It must be a file URL.
|
||||
* 2. It must point to a RAM bundle, or allowLoadingNonRAMBundles must be YES.
|
||||
* If the URL does not meet those conditions, this method will return nil and supply an error with the domain
|
||||
* RCTJavaScriptLoaderErrorDomain and the code RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously.
|
||||
*/
|
||||
+ (NSData *)attemptSynchronousLoadOfBundleAtURL:(NSURL *)scriptURL
|
||||
sourceLength:(int64_t *)sourceLength
|
||||
allowLoadingNonRAMBundles:(BOOL)allowLoadingNonRAMBundles
|
||||
error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
|
|
@ -19,134 +19,221 @@
|
|||
|
||||
uint32_t const RCTRAMBundleMagicNumber = 0xFB0BD1E5;
|
||||
|
||||
NSString *const RCTJavaScriptLoaderErrorDomain = @"RCTJavaScriptLoaderErrorDomain";
|
||||
|
||||
@implementation RCTJavaScriptLoader
|
||||
|
||||
RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
||||
|
||||
+ (void)loadBundleAtURL:(NSURL *)scriptURL onComplete:(RCTSourceLoadBlock)onComplete
|
||||
{
|
||||
NSString *unsanitizedScriptURLString = scriptURL.absoluteString;
|
||||
// Sanitize the script URL
|
||||
scriptURL = [RCTConvert NSURL:unsanitizedScriptURLString];
|
||||
|
||||
if (!scriptURL) {
|
||||
NSString *errorDescription = [NSString stringWithFormat:@"No script URL provided."
|
||||
@"unsanitizedScriptURLString:(%@)", unsanitizedScriptURLString];
|
||||
NSError *error = [NSError errorWithDomain:@"JavaScriptLoader" code:1 userInfo:@{
|
||||
NSLocalizedDescriptionKey: errorDescription
|
||||
}];
|
||||
onComplete(error, nil, 0);
|
||||
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) {
|
||||
// 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) {
|
||||
onComplete(RCTErrorWithMessage([NSString stringWithFormat:@"Error opening bundle %@", scriptURL.path]), nil, 0);
|
||||
return;
|
||||
if (!scriptURL.fileURL) {
|
||||
if (error) {
|
||||
*error = [NSError errorWithDomain:RCTJavaScriptLoaderErrorDomain
|
||||
code:RCTJavaScriptLoaderErrorCannotBeLoadedSynchronously
|
||||
userInfo:@{NSLocalizedDescriptionKey:
|
||||
@"Cannot load non-file URLs synchronously"}];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
uint32_t magicNumber;
|
||||
size_t readResult = fread(&magicNumber, sizeof(magicNumber), 1, bundle);
|
||||
fclose(bundle);
|
||||
if (readResult != 1) {
|
||||
onComplete(RCTErrorWithMessage(@"Error reading bundle"), nil, 0);
|
||||
return;
|
||||
// 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;
|
||||
}
|
||||
|
||||
magicNumber = NSSwapLittleIntToHost(magicNumber);
|
||||
if (magicNumber == RCTRAMBundleMagicNumber) {
|
||||
NSData *source = [NSData dataWithBytes:&magicNumber length:sizeof(magicNumber)];
|
||||
NSError *error = nil;
|
||||
int64_t sourceLength = 0;
|
||||
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;
|
||||
}
|
||||
|
||||
struct stat statInfo;
|
||||
if (stat(scriptURL.path.UTF8String, &statInfo) != 0) {
|
||||
error = RCTErrorWithMessage(@"Error reading bundle");
|
||||
} else {
|
||||
sourceLength = statInfo.st_size;
|
||||
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;
|
||||
}
|
||||
onComplete(error, source, sourceLength);
|
||||
} else {
|
||||
// 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 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) {
|
||||
NSURLSessionDataTask *task =
|
||||
[[NSURLSession sharedSession] dataTaskWithURL:scriptURL completionHandler:
|
||||
^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
|
||||
// Handle general request errors
|
||||
if (error) {
|
||||
if ([error.domain isEqualToString:NSURLErrorDomain]) {
|
||||
NSString *desc = [@"Could not connect to development server.\n\nEnsure 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\nURL: " stringByAppendingString:scriptURL.absoluteString];
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: desc,
|
||||
NSLocalizedFailureReasonErrorKey: error.localizedDescription,
|
||||
NSUnderlyingErrorKey: error,
|
||||
};
|
||||
error = [NSError errorWithDomain:@"JSServer"
|
||||
code:error.code
|
||||
userInfo:userInfo];
|
||||
}
|
||||
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) {
|
||||
NSString *rawText = [[NSString alloc] initWithData:data encoding:encoding];
|
||||
NSDictionary *userInfo;
|
||||
NSDictionary *errorDetails = RCTJSONParse(rawText, nil);
|
||||
if ([errorDetails isKindOfClass:[NSDictionary class]] &&
|
||||
[errorDetails[@"errors"] isKindOfClass:[NSArray class]]) {
|
||||
NSMutableArray<NSDictionary *> *fakeStack = [NSMutableArray new];
|
||||
for (NSDictionary *err in errorDetails[@"errors"]) {
|
||||
[fakeStack addObject: @{
|
||||
@"methodName": err[@"description"] ?: @"",
|
||||
@"file": err[@"filename"] ?: @"",
|
||||
@"lineNumber": err[@"lineNumber"] ?: @0
|
||||
}];
|
||||
}
|
||||
userInfo = @{
|
||||
NSLocalizedDescriptionKey: errorDetails[@"message"] ?: @"No message provided",
|
||||
@"stack": fakeStack,
|
||||
};
|
||||
} else {
|
||||
userInfo = @{NSLocalizedDescriptionKey: rawText};
|
||||
}
|
||||
error = [NSError errorWithDomain:@"JSServer"
|
||||
code:((NSHTTPURLResponse *)response).statusCode
|
||||
userInfo:userInfo];
|
||||
|
||||
onComplete(error, nil, 0);
|
||||
return;
|
||||
}
|
||||
onComplete(nil, data, data.length);
|
||||
}];
|
||||
// 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
|
||||
|
|
Loading…
Reference in New Issue