Skip to content

Commit

Permalink
refactor(ios): use a bootstrap JS file before loading ti.main.js
Browse files Browse the repository at this point in the history
  • Loading branch information
sgtcoolguy committed Jan 13, 2021
1 parent 517b81c commit a66bf79
Show file tree
Hide file tree
Showing 12 changed files with 156 additions and 211 deletions.
4 changes: 4 additions & 0 deletions iphone/TitaniumKit/TitaniumKit/Sources/API/AssetsModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@
@end

@interface AssetsModule : ObjcModule <AssetsExports>

+ (NSData *)loadURL:(NSURL *)url;
+ (NSString *)readURL:(NSURL *)url;

@end
96 changes: 60 additions & 36 deletions iphone/TitaniumKit/TitaniumKit/Sources/API/AssetsModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
#import "AssetsModule.h"
#import "KrollModule.h"
#import "TiHost.h"
#import "TiModule.h"
#import "TiUtils.h"

typedef NS_ENUM(NSInteger, FileStatus) {
Expand All @@ -31,38 +30,15 @@ - (NSString *)readAsset:(NSString *)path
{
NSData *data;
if ([path hasPrefix:@"/"]) {
// drop leading '/' to actually make relative to base url/root dir
path = [self pathByStandarizingPath:[path substringFromIndex:1]];
// drop leading '/' to actually make relative to resources dir
NSURL *url = [TiHost resourceBasedURL:[path substringFromIndex:1] baseURL:NULL];
data = [AssetsModule loadURL:url];
} else if (![path hasPrefix:@"."]) {
// no leading '.' or '/', check if it's a core module's assets
data = [self loadCoreModuleAsset:path];
}
if (data == nil) {
// check if file exists by using cheat index.json which tells us if on disk or encrypted.
FileStatus status = [self fileStatus:path];
NSURL *url_ = [NSURL URLWithString:path relativeToURL:[self _baseURL]];

switch (status) {
case FileStatusExistsOnDisk:
data = [NSData dataWithContentsOfURL:url_]; // load from disk
break;

case FileStatusExistsEncrypted:
data = [TiUtils loadAppResource:url_]; // try to load encrypted file
break;

case FileStatusUnknown:
// There was no index.json so fallback to just trying to read from disk/encryption the slow way
data = [NSData dataWithContentsOfURL:url_];
if (data == nil) {
data = [TiUtils loadAppResource:url_];
}
break;

case FileStatusDoesntExist:
default:
return nil;
}
data = [AssetsModule loadFile:path baseURL:[self _baseURL]];
}

if (data != nil) {
Expand All @@ -71,25 +47,73 @@ - (NSString *)readAsset:(NSString *)path
return nil;
}

- (NSString *)pathByStandarizingPath:(NSString *)relativePath
// TODO: Move all the logic for loading app resources into TiUtils or TiHost so we can centralize calls and not expose AssetsModule everywhere

+ (NSString *)readURL:(NSURL *)url
{
NSData *data = [AssetsModule loadURL:url];
if (data != nil) {
return [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
}
return nil;
}

+ (NSData *)loadURL:(NSURL *)url
{
FileStatus status = FileStatusUnknown;
// This assumes it's a js or json file already!
if (url.isFileURL) { // if it's a resource, check index.json listing for status
status = [AssetsModule fileStatus:[TiHost resourceRelativePath:url]];
}
NSData *data;
switch (status) {
case FileStatusExistsOnDisk:
return [NSData dataWithContentsOfURL:url]; // load from disk

case FileStatusExistsEncrypted:
return [TiUtils loadAppResource:url]; // try to load encrypted file

case FileStatusUnknown:
// There was no index.json so fallback to just trying to read from disk/encryption the slow way
data = [NSData dataWithContentsOfURL:url];
if (data == nil) {
return [TiUtils loadAppResource:url];
}
return data;

case FileStatusDoesntExist:
default:
return nil;
}
}

+ (NSData *)loadFile:(NSString *)path baseURL:(NSURL *)baseURL
{
// Calling [relativePath stringByStandardizingPath]; does not resolve '..' segments because the path isn't absolute!
// so we hack around it here by making an URL that does point to absolute location...
NSURL *url_ = [NSURL URLWithString:relativePath relativeToURL:[self _baseURL]];
NSURL *url_ = [NSURL URLWithString:path relativeToURL:baseURL];
// "standardizing" it (i.e. removing '.' and '..' segments properly...
NSURL *standardizedURL = [url_ standardizedURL];
// Then asking for the relative path again
return [[standardizedURL relativePath] stringByStandardizingPath];
return [AssetsModule loadURL:[url_ standardizedURL]];
}

- (FileStatus)fileStatus:(NSString *)path
+ (FileStatus)fileStatus:(NSString *)path
{
// if it's not a js or json file, status is unknown!
NSString *extension = path.pathExtension;
if (![extension isEqual:@"js"] && ![extension isEqual:@"json"]) {
return FileStatusUnknown;
}
NSDictionary *files = [AssetsModule loadIndexJSON];
if (files.count == 0) {
// there was no index.json! status is unknown!
return FileStatusUnknown;
}
path = [@"Resources/" stringByAppendingString:path];
if ([path isEqualToString:@"/_index_.json"]) { // we know it exists! we loaded it
return FileStatusUnknown; // treat as "unknown" since we didn't record if we loaded in normal or encrypted...
}
// Initial path is assuemd to be of form: "/ti.main.js", "/app.js" or "/ti.kernel.js"
// Basically a path that looks absolute but is relative to Resources dir (app root)
path = [@"Resources" stringByAppendingString:path];
NSNumber *type = files[path];
if (type == nil) {
return FileStatusDoesntExist;
Expand All @@ -103,7 +127,7 @@ - (NSData *)loadCoreModuleAsset:(NSString *)path
NSArray<NSString *> *pathComponents = [path pathComponents];
NSString *moduleID = [pathComponents objectAtIndex:0];

id module = [KrollModule loadCoreModule:moduleID inContext:JSContext.currentContext];
id<Module> module = [KrollModule loadCoreModule:moduleID inContext:JSContext.currentContext];
if (module == nil) {
return nil;
}
Expand Down
189 changes: 44 additions & 145 deletions iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
#import "KrollBridge.h"
#import "APSAnalytics.h"
#import "AssetsModule.h"
#import "JSValue+Addons.h"
#import "KrollCallback.h"
#import "KrollModule.h"
Expand Down Expand Up @@ -220,66 +221,36 @@ - (void)evalFileOnThread:(NSString *)path context:(KrollContext *)context_
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

NSError *error = nil;
JSValueRef exception = NULL;

JSContextRef jsContext = [context_ context];

NSURL *url_ = [path hasPrefix:@"file:"] ? [NSURL URLWithString:path] : [NSURL fileURLWithPath:path];

if (![path hasPrefix:@"/"] && ![path hasPrefix:@"file:"]) {
url_ = [NSURL URLWithString:path relativeToURL:url];
}

NSString *jcode = nil;

if ([url_ isFileURL]) {
NSData *data = [TiUtils loadAppResource:url_];
if (data == nil) {
jcode = [NSString stringWithContentsOfFile:[url_ path] encoding:NSUTF8StringEncoding error:&error];
} else {
jcode = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
}
} else {
jcode = [NSString stringWithContentsOfURL:url_ encoding:NSUTF8StringEncoding error:&error];
}

if (error != nil) {
NSLog(@"[ERROR] Error loading path: %@, %@", path, error);

NSString *jcode = [AssetsModule readURL:url_];
if (jcode == nil) {
NSLog(@"[ERROR] Error loading path: %@", path);
evaluationError = YES;
TiScriptError *scriptError = nil;
// check for file not found a give a friendlier message
if ([error code] == 260 && [error domain] == NSCocoaErrorDomain) {
scriptError = [[TiScriptError alloc] initWithMessage:[NSString stringWithFormat:@"Could not find the file %@", [path lastPathComponent]] sourceURL:nil lineNo:0];
} else {
scriptError = [[TiScriptError alloc] initWithMessage:[NSString stringWithFormat:@"Error loading script %@. %@", [path lastPathComponent], [error description]] sourceURL:nil lineNo:0];
}
[[TiExceptionHandler defaultExceptionHandler] reportScriptError:scriptError];
TiScriptError *scriptError = [[TiScriptError alloc] initWithMessage:[NSString stringWithFormat:@"Error loading script %@.", [path lastPathComponent]] sourceURL:nil lineNo:0];
[TiExceptionHandler.defaultExceptionHandler reportScriptError:scriptError];
[scriptError release];
return;
}

const char *urlCString = [[url_ absoluteString] UTF8String];

JSStringRef jsCode = JSStringCreateWithCFString((CFStringRef)jcode);
JSStringRef jsURL = JSStringCreateWithUTF8CString(urlCString);
// When we run a file as the entry point of the app or a service:
// we bootstrap, then Module.runModule(source, filename, service)
JSGlobalContextRef jsContext = context_.context;
JSContext *objCContext = [JSContext contextWithJSGlobalContextRef:jsContext];
JSValue *moduleGlobal = objCContext.globalObject[@"Module"];
// make the path relative to the resources Dir!
NSString *relativePath = [TiHost resourceRelativePath:url_];
// FIXME: If the entry point is a service, we should pass that in here as last arg instead of null!
[moduleGlobal invokeMethod:@"runModule" withArguments:@[ jcode, relativePath, [NSNull null] ]];

if (exception == NULL) {
JSEvaluateScript(jsContext, jsCode, NULL, jsURL, 1, &exception);
if (exception == NULL) {
evaluationError = NO;
} else {
evaluationError = YES;
}
}
if (exception != NULL) {
if (objCContext.exception != nil) {
evaluationError = YES;
[TiExceptionHandler.defaultExceptionHandler reportScriptError:exception inKrollContext:context];
[TiExceptionHandler.defaultExceptionHandler reportScriptError:objCContext.exception inJSContext:objCContext];
}

JSStringRelease(jsCode);
JSStringRelease(jsURL);
[pool release];
}

Expand Down Expand Up @@ -362,102 +333,35 @@ - (void)didStartNewContext:(KrollContext *)kroll
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

JSGlobalContextRef jsContext = [kroll context];
JSGlobalContextRef jsContext = kroll.context;
JSContext *objcJSContext = [JSContext contextWithJSGlobalContextRef:jsContext];
JSValue *global = [objcJSContext globalObject];

// Make the global object itself available under the name "global"
[global defineReadOnlyProperty:@"global" withValue:global];

// define a "kroll" module
[global defineReadOnlyProperty:@"kroll" withValue:[[KrollModule alloc] init]];

// TODO: Move to real paths for __dirname/__filename, but that affects Android and may break users/debugging?
// NSString *dirname = [[TiHost resourcePath] stringByStandardizingPath];
// Set the __dirname and __filename for the app.js.
// For other files, it will be injected via the `TitaniumModuleRequireFormat` property
// For other files, it will be injected via require
[global defineReadOnlyProperty:@"__dirname" withValue:@"/"];
// NSString *filename = [dirname stringByAppendingString:@"/app.js"];
[global defineReadOnlyProperty:@"__filename" withValue:@"/app.js"];

// TODO: Move all this logic into JS code to set up lazy bindings?
// We should be able to create a lazy global.Ti/global.Titanium using global.kroll.binding()

// Now define "Ti" and "Titanium" on the global
TopTiModule *module = [[TopTiModule alloc] init];
JSValue *titanium = [JSValue valueWithObject:module inContext:objcJSContext];
NSDictionary *dictionary = @{
JSPropertyDescriptorEnumerableKey : @NO,
JSPropertyDescriptorWritableKey : @YES,
JSPropertyDescriptorConfigurableKey : @NO,
JSPropertyDescriptorValueKey : titanium
};
[global defineProperty:@"Titanium" descriptor:dictionary];
[global defineProperty:@"Ti" descriptor:dictionary];

// Hack the old-school way of doing a module here
NSArray *legacyModuleNames = @[ @"App",
@"Contacts",
@"Media",
@"Network",
@"Stream",
@"UI",
@"WatchSession",
@"XML" ];
for (NSString *name in legacyModuleNames) {
// We must generate the block and copy it to put it into heap or else every instance of the block shares
// the same "name" value. See https://stackoverflow.com/questions/7750907/blocks-loops-and-local-variables
JSValue * (^lazyLoad)(void) = ^() {
JSValue *result;
// TODO: Replace with KrollModule.binding()? Does same thing!
JSContext *currentContext = JSContext.currentContext;
id<Module> mod = [KrollModule loadCoreModule:name inContext:currentContext];
if (mod != nil) {
KrollObject *ko = [self registerProxy:mod]; // This basically retains the module for the lifetime of the bridge
result = [JSValue valueWithJSValueRef:[ko jsobject] inContext:currentContext];
} else {
result = [JSValue valueWithUndefinedInContext:currentContext];
}
[JSContext.currentThis defineReadOnlyProperty:name
withValue:result];
return result;
};
[titanium defineProperty:name
descriptor:@{
JSPropertyDescriptorConfigurableKey : @YES,
JSPropertyDescriptorGetKey : [[lazyLoad copy] autorelease]
}];
}

// New JSExport based modules
// Basically a whitelist of Ti.* modules to load lazily
NSArray *moduleNames = @[ @"Accelerometer", @"Analytics", @"API", @"Calendar", @"Codec", @"Database", @"Filesystem", @"Geolocation", @"Gesture", @"Locale", @"Platform", @"Utils" ];
for (NSString *name in moduleNames) {
// We must generate the block and copy it to put it into heap or else every instance of the block shares
// the same "name" value. See https://stackoverflow.com/questions/7750907/blocks-loops-and-local-variables
JSValue * (^lazyLoad)(void) = ^() {
// TODO: Align with and replace with kroll.binding()
// Note that with legacy moduels we call registerProxy, while with new we do not...
// We've forced a retain cycle between proxies and event listeners in ObjcProxy, so maybe that's the better route to go anyways?
JSContext *currentContext = JSContext.currentContext;
id<Module> mod = [KrollModule loadCoreModule:name inContext:currentContext];
JSValue *result;
if (mod != nil) {
result = [JSValue valueWithObject:mod inContext:currentContext];
} else {
result = [JSValue valueWithUndefinedInContext:currentContext];
}
[JSContext.currentThis defineReadOnlyProperty:name
withValue:result];
return result;
};
[titanium defineProperty:name
descriptor:@{
JSPropertyDescriptorConfigurableKey : @YES,
JSPropertyDescriptorGetKey : [[lazyLoad copy] autorelease]
}];
// NSString *filename = [dirname stringByAppendingString:@"/ti.kernel.js"];
[global defineReadOnlyProperty:@"__filename" withValue:@"/ti.kernel.js"];

// Load ti.kernel.js (kroll.js equivalent)
NSURL *bootstrapURL = [TiHost resourceBasedURL:@"ti.kernel.js" baseURL:NULL];
NSString *source = [AssetsModule readURL:bootstrapURL];

JSValue *bootstrapFunc = [objcJSContext evaluateScript:source withSourceURL:bootstrapURL];
if (objcJSContext.exception != nil) {
evaluationError = YES;
[TiExceptionHandler.defaultExceptionHandler reportScriptError:objcJSContext.exception inJSContext:objcJSContext];
}
[bootstrapFunc callWithArguments:@[ global, [[KrollModule alloc] init] ]];
if (objcJSContext.exception != nil) {
evaluationError = YES;
[TiExceptionHandler.defaultExceptionHandler reportScriptError:objcJSContext.exception inJSContext:objcJSContext];
}

JSValue *titanium = global[@"Ti"];
TopTiModule *module = [titanium toObject];
if ([[TiSharedConfig defaultConfig] isAnalyticsEnabled]) {
APSAnalytics *sharedAnalytics = [APSAnalytics sharedInstance];
NSString *buildType = [[TiSharedConfig defaultConfig] applicationBuildType];
Expand All @@ -470,10 +374,7 @@ - (void)didStartNewContext:(KrollContext *)kroll
[sharedAnalytics enableWithAppKey:guid andDeployType:deployType];
}

// FIXME: Android does a "bootstrap" which effectively loads some JS code up, and *then* it uses Module.runModule() to execute ti.main.js
// But we're defining Module.runModule in ti.main.js
// How can we hijack the requires to go through Module.runModule?

NSURL *startURL = nil;
//if we have a preload dictionary, register those static key/values into our namespace
if (preload != nil) {
for (NSString *name in preload) {
Expand All @@ -490,17 +391,15 @@ - (void)didStartNewContext:(KrollContext *)kroll
[ti setStaticValue:ko forKey:key purgable:NO];
}
}
// We need to run this before the app.js, which means it has to be here.
TiBindingRunLoopAnnounceStart(kroll);
[self evalFile:[url path] callback:self selector:@selector(booted)];
startURL = [url copy]; // should be the entry point of the background service js file
} else {
// now load the app.js file and get started
NSURL *startURL = [host startURL];
// We need to run this before the app.js, which means it has to be here.
TiBindingRunLoopAnnounceStart(kroll);
[self evalFile:[startURL absoluteString] callback:self selector:@selector(booted)];
startURL = [host startURL]; // should be ti.main.js
}

// We need to run this before the entry js file, which means it has to be here.
TiBindingRunLoopAnnounceStart(kroll);
[self evalFile:[startURL absoluteString] callback:self selector:@selector(booted)];

if (TiUtils.isHyperloopAvailable) {
Class cls = NSClassFromString(@"Hyperloop");
[cls performSelector:@selector(didStartNewContext:bridge:) withObject:kroll withObject:self];
Expand Down
Loading

0 comments on commit a66bf79

Please sign in to comment.