From d8bddd8d1c93b766a8ec5cbb9956a2544d3e8621 Mon Sep 17 00:00:00 2001 From: Chris Williams Date: Fri, 18 Jan 2019 14:09:40 -0500 Subject: [PATCH] perf(ios): improve performance of require function Generate an index of js/json files (_index_.json) packaged into the app for faster existence checks/reading. Generate index when running local dev xcode build using node script. Fixes TIMOB-26742 --- .../TitaniumKit/Sources/API/KrollBridge.h | 2 +- .../TitaniumKit/Sources/API/KrollBridge.m | 644 ++++++++++++------ iphone/cli/commands/_build.js | 86 ++- .../iphone/Titanium.xcodeproj/project.pbxproj | 2 +- support/dev/index_generator.js | 30 + 5 files changed, 521 insertions(+), 243 deletions(-) create mode 100755 support/dev/index_generator.js diff --git a/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.h b/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.h index a1287df56ec..278fcf8c612 100644 --- a/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.h +++ b/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.h @@ -38,7 +38,7 @@ extern NSString *TitaniumModuleRequireFormat; KrollContext *context; NSDictionary *preload; NSMutableDictionary *modules; - NSMutableDictionary *pathCache; + NSMutableDictionary *packageJSONMainCache; TitaniumObject *titanium; KrollObject *console; BOOL shutdown; diff --git a/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.m b/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.m index 67c353fff45..3ef0d5d57c1 100644 --- a/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.m +++ b/iphone/TitaniumKit/TitaniumKit/Sources/API/KrollBridge.m @@ -31,6 +31,45 @@ //Defined private method inside TiBindingRunLoop.m (Perhaps to move to .c?) void TiBindingRunLoopAnnounceStart(TiBindingRunLoop runLoop); +typedef NS_ENUM(NSInteger, FileStatus) { + DoesntExist, + ExistsOnDisk, + ExistsEncrypted +}; + +typedef NS_ENUM(NSInteger, ModuleType) { + Native, + JS, + JSON, + NativeJS +}; +@interface ResolvedModule : NSObject { + @public + ModuleType type; + NSString *path; +} +- (id)initWithType:(ModuleType)modType andPath:(NSString *)modPath; +@end + +@implementation ResolvedModule +- (id)initWithType:(ModuleType)modType andPath:(NSString *)modPath +{ + if ((self = [super init])) { + type = modType; + path = [modPath retain]; + } + + return self; +} + +- (void)dealloc +{ + [path release]; + + [super dealloc]; +} +@end + @implementation TitaniumObject - (NSDictionary *)modules @@ -229,7 +268,7 @@ - (id)init NSLog(@"[DEBUG] INIT: %@", self); #endif modules = [[NSMutableDictionary alloc] init]; - pathCache = [[NSMutableDictionary alloc] init]; + packageJSONMainCache = [[NSMutableDictionary alloc] init]; proxyLock = OS_SPINLOCK_INIT; OSSpinLockLock(&krollBridgeRegistryLock); CFSetAddValue(krollBridgeRegistry, self); @@ -725,6 +764,7 @@ - (id)krollObjectForProxy:(id)proxy - (KrollWrapper *)loadCommonJSModule:(NSString *)code withSourceURL:(NSURL *)sourceURL { + // FIXME: Can we skip all of this now? Doesn't we already properly resolve paths? // This takes care of resolving paths like `../../foo.js` sourceURL = [NSURL fileURLWithPath:[[sourceURL path] stringByStandardizingPath]]; @@ -775,12 +815,39 @@ - (NSString *)pathToModuleClassName:(NSString *)path return modulename; } -- (TiModule *)loadTopLevelNativeModule:(TiModule *)module withPath:(NSString *)path withContext:(KrollContext *)kroll +- (TiModule *)loadCoreModule:(NSString *)moduleID withContext:(KrollContext *)kroll { + NSString *moduleClassName = [self pathToModuleClassName:moduleID]; + Class moduleClass = NSClassFromString(moduleClassName); + // If no such module exists, bail out! + if (moduleClass == nil) { + return nil; + } + + // If there is a JS file that collides with the given path, + // warn the user of the collision, but prefer the native/core module + NSURL *jsPath = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@.js", [[NSURL fileURLWithPath:[TiHost resourcePath] isDirectory:YES] path], moduleID]]; + if ([[NSFileManager defaultManager] fileExistsAtPath:[jsPath absoluteString]]) { + NSLog(@"[WARN] The requested path '%@' has a collison between a native Ti%@um API/module and a JS file.", moduleID, @"tani"); + NSLog(@"[WARN] The native Ti%@um API/module will be loaded in preference.", @"tani"); + NSLog(@"[WARN] If you intended to address the JS file, please require the path using a prefixed string such as require('./%@') or require('/%@') instead.", moduleID, moduleID); + } + + // Ok, we have a native module, make sure instantiate and cache it + TiModule *module = [modules objectForKey:moduleID]; + if (module == nil) { + module = [[moduleClass alloc] _initWithPageContext:self]; + [module setHost:host]; + [module _setName:moduleClassName]; + [modules setObject:module forKey:moduleID]; + [module autorelease]; + } + // does it have JS? No, then nothing else to do... if (![module isJSModule]) { return module; } + NSData *data = [module moduleJS]; if (data == nil) { // Uh oh, no actual data. Let's just punt and return the native module as-is @@ -788,7 +855,7 @@ - (TiModule *)loadTopLevelNativeModule:(TiModule *)module withPath:(NSString *)p } NSString *contents = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; - NSURL *url_ = [TiHost resourceBasedURL:path baseURL:NULL]; + NSURL *url_ = [TiHost resourceBasedURL:moduleID baseURL:NULL]; KrollWrapper *wrapper = [self loadCommonJSModule:contents withSourceURL:url_]; // For right now, we need to mix any compiled JS on top of a compiled module, so that both components @@ -815,64 +882,22 @@ - (TiModule *)loadTopLevelNativeModule:(TiModule *)module withPath:(NSString *)p return module; } -- (id)loadCoreModule:(NSString *)path withContext:(KrollContext *)kroll +- (KrollWrapper *)loadCoreModuleAsset:(NSString *)path withContext:(KrollContext *)kroll { - // make sure path doesn't begin with ., .., or / - // Can't be a "core" module then - if ([path hasPrefix:@"/"] || [path hasPrefix:@"."]) { - return nil; - } - - // moduleId then is the first path component - // try to load up the native module's class... - NSString *moduleID = [[path pathComponents] objectAtIndex:0]; - NSString *moduleClassName = [self pathToModuleClassName:moduleID]; - Class moduleClass = NSClassFromString(moduleClassName); - // If no such module exists, bail out! - if (moduleClass == nil) { - return nil; - } - - // If there is a JS file that collides with the given path, - // warn the user of the collision, but prefer the native/core module - NSURL *jsPath = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@.js", [[NSURL fileURLWithPath:[TiHost resourcePath] isDirectory:YES] path], path]]; - if ([[NSFileManager defaultManager] fileExistsAtPath:[jsPath absoluteString]]) { - NSLog(@"[WARN] The requested path '%@' has a collison between a native Ti%@um API/module and a JS file.", path, @"tani"); - NSLog(@"[WARN] The native Ti%@um API/module will be loaded in preference.", @"tani"); - NSLog(@"[WARN] If you intended to address the JS file, please require the path using a prefixed string such as require('./%@') or require('/%@') instead.", path, path); - } + NSArray *pathComponents = [path pathComponents]; + NSString *moduleID = [pathComponents objectAtIndex:0]; - // Ok, we have a native module, make sure instantiate and cache it - TiModule *module = [modules objectForKey:moduleID]; - if (module == nil) { - module = [[moduleClass alloc] _initWithPageContext:self]; - [module setHost:host]; - [module _setName:moduleClassName]; - [modules setObject:module forKey:moduleID]; - [module autorelease]; - } - - // Are they just trying to load the top-level module? NSRange separatorLocation = [path rangeOfString:@"/"]; - if (separatorLocation.location == NSNotFound) { - // Indicates toplevel module - return [self loadTopLevelNativeModule:module withPath:path withContext:kroll]; - } - - // check rest of path + // check rest of path FIXME: Just rejoin pathComponents? NSString *assetPath = [path substringFromIndex:separatorLocation.location + 1]; - // Treat require('module.id/module.id') == require('module.id') - if ([assetPath isEqualToString:moduleID]) { - return [self loadTopLevelNativeModule:module withPath:path withContext:kroll]; - } - // not top-level module! + TiModule *module = [self loadCoreModule:moduleID withContext:kroll]; + // Try to load the file as module asset! NSString *filepath = [assetPath stringByAppendingString:@".js"]; NSData *data = [module loadModuleAsset:filepath]; // does it exist in module? if (data == nil) { - // nope, return nil so we can try to fall back to resource in user's app return nil; } NSString *contents = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; @@ -882,15 +907,25 @@ - (id)loadCoreModule:(NSString *)path withContext:(KrollContext *)kroll - (NSString *)loadFile:(NSString *)path { + // 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 host] baseURL]]; - NSData *data = [TiUtils loadAppResource:url_]; // try to load encrypted file - - if (data == nil) { - data = [NSData dataWithContentsOfURL:url_]; + NSData *data; + switch (status) { + case ExistsOnDisk: + data = [NSData dataWithContentsOfURL:url_]; // load from disk + break; + + case ExistsEncrypted: + data = [TiUtils loadAppResource:url_]; // try to load encrypted file + break; + + case DoesntExist: + default: + return nil; } if (data != nil) { - [self setCurrentURL:[NSURL URLWithString:[path stringByDeletingLastPathComponent] relativeToURL:[[self host] baseURL]]]; return [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; } return nil; @@ -918,19 +953,14 @@ - (KrollWrapper *)loadJavascriptObject:(NSString *)data fromFile:(NSString *)fil - (KrollWrapper *)loadJavascriptText:(NSString *)data fromFile:(NSString *)filename withContext:(KrollContext *)kroll { NSURL *url_ = [TiHost resourceBasedURL:filename baseURL:NULL]; - KrollWrapper *wrapper = [self loadCommonJSModule:data withSourceURL:url_]; + KrollWrapper *module = [self loadCommonJSModule:data withSourceURL:url_]; - if (![wrapper respondsToSelector:@selector(replaceValue:forKey:notification:)]) { + if (![module respondsToSelector:@selector(replaceValue:forKey:notification:)]) { @throw [NSException exceptionWithName:@"org.appcelerator.kroll" reason:[NSString stringWithFormat:@"Module \"%@\" failed to leave a valid exports object", filename] userInfo:nil]; } - // register the module if it's pure JS - KrollWrapper *module = (id)wrapper; - - // cache the module by filename - [modules setObject:module forKey:filename]; if (filename != nil && module != nil) { // uri is optional but we point it to where we loaded it [module replaceValue:[NSString stringWithFormat:@"app://%@", filename] forKey:@"uri" notification:NO]; @@ -940,93 +970,107 @@ - (KrollWrapper *)loadJavascriptText:(NSString *)data fromFile:(NSString *)filen return module; } -- (KrollWrapper *)cachedLoadAsFile:(NSString *)path asJSON:(BOOL)json withContext:(KrollContext *)kroll -{ - // check cache first - if (modules != nil) { - KrollWrapper *module = [modules objectForKey:path]; - if (module != nil) { - return module; - } - } - - // Fall back to trying to load file - NSString *data = [self loadFile:path]; - if (data != nil) { - if (json) { - return [self loadJavascriptObject:data fromFile:path withContext:context]; - } - return [self loadJavascriptText:data fromFile:path withContext:context]; - } - return nil; -} - - (KrollWrapper *)loadAsFile:(NSString *)path withContext:(KrollContext *)kroll { NSString *filename = path; + NSString *data; // 1. If X is a file, load X as JavaScript text. STOP // Note: I modified the algorithm here to handle .json files as JSON, everything else as JS NSString *ext = [filename pathExtension]; BOOL json = (ext != nil && [ext isEqual:@"json"]); - KrollWrapper *module = [self cachedLoadAsFile:filename asJSON:json withContext:context]; - if (module != nil) { - return module; + data = [self loadFile:filename]; + if (data != nil) { + if (json) { + return [self loadJavascriptObject:data fromFile:filename withContext:context]; + } else { + return [self loadJavascriptText:data fromFile:filename withContext:context]; + } } // 2. If X.js is a file, load X.js as JavaScript text. STOP filename = [path stringByAppendingString:@".js"]; - module = [self cachedLoadAsFile:filename asJSON:NO withContext:context]; - if (module != nil) { - return module; + data = [self loadFile:filename]; + if (data != nil) { + return [self loadJavascriptText:data fromFile:filename withContext:context]; } // 3. If X.json is a file, parse X.json to a JavaScript Object. STOP filename = [path stringByAppendingString:@".json"]; - module = [self cachedLoadAsFile:filename asJSON:YES withContext:context]; - if (module != nil) { - return module; + data = [self loadFile:filename]; + if (data != nil) { + return [self loadJavascriptObject:data fromFile:filename withContext:context]; } // failed to load anything! return nil; } +/* changes current URL to keep track when requiring to know our working path to resolve from */ +- (void)updateCurrentURLFromPath:(NSString *)path +{ + [self setCurrentURL:[NSURL URLWithString:[path stringByDeletingLastPathComponent] relativeToURL:[[self host] baseURL]]]; +} + +/* returns full path to main file declared in given package json, only if it exists! */ +- (NSString *)packageJSONMain:(NSString *)packageJsonPath +{ + // check special package.json cache + NSString *m = packageJSONMainCache[packageJsonPath]; + if (m != nil) { + return m; + } + + NSString *data = [self loadFile:packageJsonPath]; + if (data == nil) { + return nil; + } + + // a. Parse X/package.json, and look for "main" field. + // Just cheat and use TiUtils.jsonParse here, rather than loading the package.json as a JS object... + NSDictionary *json = [TiUtils jsonParse:data]; + if (json == nil) { + return nil; + } + + id main = [json objectForKey:@"main"]; + if ([main isKindOfClass:[NSString class]]) { + NSString *mainString = (NSString *)main; + NSString *x = [packageJsonPath stringByDeletingLastPathComponent]; // parent dir of package.json + // b. let M = X + (json main field) + m = [x stringByAppendingPathComponent:mainString]; + m = [self pathByStandarizingPath:m]; + if ([self fileExists:m]) { + packageJSONMainCache[packageJsonPath] = m; // cache from package.json to main value + return m; + } + } + return nil; +} + - (KrollWrapper *)loadAsDirectory:(NSString *)path withContext:(KrollContext *)kroll { - // FIXME Use loadJavascriptObject: or cachedLoadAsFile: to get package.json and then get the main value out of it? // 1. If X/package.json is a file, NSString *filename = [path stringByAppendingPathComponent:@"package.json"]; - NSString *data = [self loadFile:filename]; - if (data != nil) { - // a. Parse X/package.json, and look for "main" field. - // Just cheat and use TiUtils.jsonParse here, rather than loading the package.json as a JS object... - NSDictionary *json = [TiUtils jsonParse:data]; - if (json != nil) { - id main = [json objectForKey:@"main"]; - NSString *mainString = nil; - if ([main isKindOfClass:[NSString class]]) { - mainString = (NSString *)main; - // b. let M = X + (json main field) - NSString *m = [[path stringByAppendingPathComponent:mainString] stringByStandardizingPath]; - // c. LOAD_AS_FILE(M) - return [self loadAsFile:m withContext:context]; - } - } + NSString *resolvedFile = [self packageJSONMain:filename]; + if (resolvedFile != nil) { + // c. LOAD_AS_FILE(M) + return [self loadAsFile:resolvedFile withContext:context]; } + NSString *data; // 2. If X/index.js is a file, load X/index.js as JavaScript text. STOP filename = [path stringByAppendingPathComponent:@"index.js"]; - KrollWrapper *module = [self cachedLoadAsFile:filename asJSON:NO withContext:context]; - if (module != nil) { - return module; + data = [self loadFile:filename]; + if (data != nil) { + return [self loadJavascriptText:data fromFile:filename withContext:context]; } // 3. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP filename = [path stringByAppendingPathComponent:@"index.json"]; - module = [self cachedLoadAsFile:filename asJSON:YES withContext:context]; - if (module != nil) { - return module; + data = [self loadFile:filename]; + if (data != nil) { + return [self loadJavascriptObject:data fromFile:filename withContext:context]; } return nil; @@ -1034,28 +1078,13 @@ - (KrollWrapper *)loadAsDirectory:(NSString *)path withContext:(KrollContext *)k - (KrollWrapper *)loadAsFileOrDirectory:(NSString *)path withContext:(KrollContext *)kroll { - // FIXME Can we improve perf a little here by detecting if the target is a file or directory first? - // i.e. - // - if node_modules/whatever exists and is a dir, we can skip checking for node_modules/whatever.js at least - // - if it doesn't exist at all, we can skip checking: - // - node_modules/whatever - // - node_modules/whatever/index.js - // - node_modules/whatever/package.json - // - node_modules/whatever/index.json - // - node_modules/whatever/whatever.js - // a. LOAD_AS_FILE(Y + X) KrollWrapper *module = [self loadAsFile:path withContext:context]; if (module) { return module; } // b. LOAD_AS_DIRECTORY(Y + X) - module = [self loadAsDirectory:path withContext:context]; - if (module) { - return module; - } - - return nil; + return [self loadAsDirectory:path withContext:context]; } - (NSArray *)nodeModulesPaths:(NSString *)path @@ -1113,109 +1142,286 @@ - (KrollWrapper *)loadNodeModules:(NSString *)path withDir:(NSString *)start wit return nil; } ++ (NSDictionary *)loadIndexJSON +{ + static NSDictionary *props; + + if (props == nil) { + + NSString *indexJsonPath = [[TiHost resourcePath] stringByAppendingPathComponent:@"_index_.json"]; + // check for encrypted copy first + NSData *jsonData = [TiUtils loadAppResource:[NSURL fileURLWithPath:indexJsonPath]]; + if (jsonData == nil) { + // Not found in encrypted file, this means we're in development mode, get it from the filesystem + jsonData = [NSData dataWithContentsOfFile:indexJsonPath]; + } + + NSString *errorString = nil; + // Get the JSON data and create the NSDictionary. + if (jsonData) { + NSError *error = nil; + props = [[NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error] retain]; + errorString = [error localizedDescription]; + } else { + // If we have no data... + // This should never happen on a Titanium app using the node.js CLI + errorString = @"File not found"; + } + if (errorString != nil) { + DebugLog(@"[ERROR] Could not load _index_.json require index, error was %@", errorString); + // Create an empty dictioary to avoid running this code over and over again. + props = [[NSDictionary dictionary] retain]; + } + } + return props; +} + +- (FileStatus)fileStatus:(NSString *)path +{ + NSDictionary *files = [KrollBridge loadIndexJSON]; + NSNumber *type = files[[@"Resources/" stringByAppendingString:path]]; + if (type == nil) { + return DoesntExist; + } + NSInteger intType = [type integerValue]; + return (FileStatus)intType; +} + +- (BOOL)fileExists:(NSString *)path +{ + return [self fileStatus:path] != DoesntExist; +} + +- (ResolvedModule *)tryFile:(NSString *)path +{ + // 1. If X is a file, load X as JavaScript text. STOP + if ([self fileExists:path]) { + NSString *ext = [path pathExtension]; + BOOL json = (ext != nil && [ext isEqual:@"json"]); + enum ModuleType type = JS; + if (json) { + type = JSON; + } + return [[ResolvedModule alloc] initWithType:type andPath:path]; + } + + // 2. If X.js is a file, load X.js as JavaScript text. STOP + NSString *asJS = [path stringByAppendingString:@".js"]; + if ([self fileExists:asJS]) { + return [[ResolvedModule alloc] initWithType:JS andPath:asJS]; + } + + // 3. If X.json is a file, parse X.json to a JavaScript Object. STOP + NSString *asJSON = [path stringByAppendingString:@".json"]; + if ([self fileExists:asJSON]) { + return [[ResolvedModule alloc] initWithType:JSON andPath:asJSON]; + } + + return nil; +} + +- (ResolvedModule *)tryDirectory:(NSString *)path +{ + NSString *packageJSON = [path stringByAppendingPathComponent:@"package.json"]; + NSString *resolved = [self packageJSONMain:packageJSON]; + if (resolved != nil) { + return [self tryFile:resolved]; + } + + NSString *indexJS = [path stringByAppendingPathComponent:@"index.js"]; + if ([self fileExists:indexJS]) { + return [[ResolvedModule alloc] initWithType:JS andPath:indexJS]; + } + + NSString *indexJSON = [path stringByAppendingPathComponent:@"index.json"]; + if ([self fileExists:indexJSON]) { + return [[ResolvedModule alloc] initWithType:JSON andPath:indexJSON]; + } + return nil; +} + +- (ResolvedModule *)tryFileOrDirectory:(NSString *)path +{ + ResolvedModule *resolved = [self tryFile:path]; + if (resolved) { + return resolved; + } + return [self tryDirectory:path]; +} + +- (ResolvedModule *)tryNativeModule:(NSString *)path +{ + // FIXME: Why doesn't iOS have a nice registry of external module ids like Android? Can't modules register + // to some dictionary? + + // moduleId then is the first path component + // try to load up the native module's class... + NSArray *pathComponents = [path pathComponents]; + NSString *moduleID = [pathComponents objectAtIndex:0]; + NSString *moduleClassName = [self pathToModuleClassName:moduleID]; + Class moduleClass = NSClassFromString(moduleClassName); + // If no such module exists, bail out! + if (moduleClass == nil) { + return nil; + } + + // Are they just trying to load the top-level module? If so, return that as our path + NSRange separatorLocation = [path rangeOfString:@"/"]; + if (separatorLocation.location == NSNotFound) { + return [[ResolvedModule alloc] initWithType:Native andPath:moduleID]; + } + + // check rest of path + NSString *assetPath = [path substringFromIndex:separatorLocation.location + 1]; + // Treat require('module.id/module.id') == require('module.id') + if ([assetPath isEqualToString:moduleID]) { + return [[ResolvedModule alloc] initWithType:Native andPath:moduleID]; + } + + // we need to load the actual module to determine beyond this... + + // Ok, we have a native module, make sure instantiate and cache it + TiModule *module = [modules objectForKey:moduleID]; + if (module == nil) { + module = [[moduleClass alloc] _initWithPageContext:self]; + [module setHost:host]; + [module _setName:moduleClassName]; + [modules setObject:module forKey:moduleID]; + [module autorelease]; + } + + // not top-level module! + NSString *filepath = [assetPath stringByAppendingString:@".js"]; + NSData *data = [module loadModuleAsset:filepath]; + // does it exist in module? + if (data == nil) { + // nope, return nil so we can try to fall back to resource in user's app + return nil; + } + // asset inside module + return [[ResolvedModule alloc] initWithType:NativeJS andPath:filepath]; +} + +- (NSString *)pathByStandarizingPath:(NSString *)relativePath +{ + // 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 host] baseURL]]; + // "standardizing" it (i.e. removing '.' and '..' segments properly... + NSURL *standardizedURL = [url_ standardizedURL]; + // Then asking for the relative path again + return [[standardizedURL relativePath] stringByStandardizingPath]; +} + +- (ResolvedModule *)resolveRequire:(NSString *)path withWorkingPath:(NSString *)workingPath +{ + // if starts with '/', treat as "absolute" already! + if ([path hasPrefix:@"/"]) { + // drop leading '/' to actually make relative to base url/root dir + return [self tryFileOrDirectory:[self pathByStandarizingPath:[path substringFromIndex:1]]]; + } + // if starts with '.', resolve relatively (to working dir) + if ([path hasPrefix:@"."]) { + NSString *relativePath = (workingPath == nil) ? path : [workingPath stringByAppendingPathComponent:path]; + // FIXME: Blow up if workingPath == nil && [path hasPrefix:@".."]? (i.e. they try to go outside the "root" dir) + return [self tryFileOrDirectory:[self pathByStandarizingPath:relativePath]]; + } + + // check for core/native module + ResolvedModule *nativeModuleOrAsset = [self tryNativeModule:path]; + if (nativeModuleOrAsset != nil) { + return nativeModuleOrAsset; + } + + // check for Titanium CommonJS module + if (![path containsString:@"/"]) { + // For CommonJS we need to look for module.id/module.id.js first... + NSString *filename = [[path stringByAppendingPathComponent:path] stringByAppendingPathExtension:@"js"]; + if ([self fileExists:filename]) { + return [[ResolvedModule alloc] initWithType:JS andPath:filename]; + } + + // Then try module.id as directory + ResolvedModule *resolved = [self tryDirectory:path]; + if (resolved != nil) { + return resolved; + } + } + + // Check for node_modules + NSArray *dirs = [self nodeModulesPaths:workingPath]; + for (NSString *dir in dirs) { + ResolvedModule *resolved = [self tryFileOrDirectory:[dir stringByAppendingPathComponent:path]]; + if (resolved) { + return resolved; + } + } + + // Fall back to treating as absolute path missing leading'/' + return [self tryFileOrDirectory:[self pathByStandarizingPath:path]]; +} + +// Returns (TiModule *) or (KrollWrapper *) - (id)require:(KrollContext *)kroll path:(NSString *)path { NSURL *oldURL = [self currentURL]; NSString *workingPath = [oldURL relativePath]; - NSMutableString *pathCacheKey; - @try { - // First let's check if we cached the resolved path for this require string - // and if we did, try and load a cached module for this path - if (pathCache != nil && modules != nil) { - // We generate a path resolution cache key, first part is the requested module id/path - pathCacheKey = [[path stringByAppendingString:@"|"] mutableCopy]; - // If request is not-absolute and we're not at the top-level dir, then append current dir as second part of cache key - if (workingPath != nil && ![path hasPrefix:@"/"]) { - pathCacheKey = [[pathCacheKey stringByAppendingString:workingPath] mutableCopy]; - } - NSString *resolvedPath = [pathCache objectForKey:pathCacheKey]; - if (resolvedPath != nil) { - TiModule *module = [modules objectForKey:resolvedPath]; - if (module != nil) { - return module; - } - } - } - - id module; // may be TiModule* if it was a core module with no hybrid JS, or KrollWrapper* in all other cases - @try { - // 1. If X is a core module, - module = [self loadCoreModule:path withContext:kroll]; - if (module) { - // a. return the core module - // b. STOP - return module; - } + ResolvedModule *resolved = [self resolveRequire:path withWorkingPath:workingPath]; + // failed to resolve it! + if (resolved == nil) { + NSString *arch = [TiUtils currentArchitecture]; + @throw [NSException exceptionWithName:@"org.test.kroll" reason:[NSString stringWithFormat:@"Couldn't find module: %@ for architecture: %@", path, arch] userInfo:nil]; // TODO Set 'code' property to 'MODULE_NOT_FOUND' to match Node? + } - // 2. If X begins with './' or '/' or '../' - if ([path hasPrefix:@"./"] || [path hasPrefix:@"../"]) { - // Need base path to work from for relative modules... - NSString *relativePath = (workingPath == nil) ? path : [workingPath stringByAppendingPathComponent:path]; - module = [self loadAsFileOrDirectory:[relativePath stringByStandardizingPath] withContext:context]; - if (module) { - return module; - } - // Treat '/' special as absolute, drop the leading '/' - } else if ([path hasPrefix:@"/"]) { - module = [self loadAsFileOrDirectory:[[path substringFromIndex:1] stringByStandardizingPath] withContext:context]; - if (module) { - return module; - } - } else { - // TODO Grab the first path segment and see if it's a node module or commonJS module - // We should be able to organize the modules in folder to determine if the user is attempting to - // load one of them! - - // Look for CommonJS module - if (![path containsString:@"/"]) { - // For CommonJS we need to look for module.id/module.id.js first... - // Only look for this _exact file_. DO NOT APPEND .js or .json to it! - NSString *filename = [[path stringByAppendingPathComponent:path] stringByAppendingPathExtension:@"js"]; - module = [self cachedLoadAsFile:filename asJSON:NO withContext:context]; - if (module) { - return module; - } - - // Then try module.id as directory - module = [self loadAsDirectory:path withContext:context]; - if (module) { - return module; - } - } + ModuleType type = resolved->type; + NSString *resolvedFilename = resolved->path; - // Need base path to work from for determining the node_modules search paths. - module = [self loadNodeModules:path withDir:workingPath withContext:context]; - if (module) { - return module; - } + [self updateCurrentURLFromPath:resolvedFilename]; // for future requires, use this resolved filepath as the base working dir/URL - // We'd like to warn users about legacy style require syntax so they can update, but the new syntax is not backwards compatible. - // So for now, let's just be quite about it. In future versions of the SDK (7.0?) we should warn (once 5.x is end of life so backwards compat is not necessary) - //NSLog(@"require called with un-prefixed module id: %@, should be a core or CommonJS module. Falling back to old Ti behavior and assuming it's an absolute path: /%@", path, path); - module = [self loadAsFileOrDirectory:[path stringByStandardizingPath] withContext:context]; - if (module) { - return module; - } + id module; + @try { + if (modules != nil) { + module = [modules objectForKey:resolvedFilename]; + if (module != nil) { + return module; } } - @finally { - // Cache the resolved path for this request if we got a module - if (module != nil && pathCache != nil && pathCacheKey != nil) { - // I cannot find a nicer way of grabbing the filepath out of the "id" or "uri" properties for the module! - NSArray *keys = [modules allKeysForObject:module]; - if (keys != nil) { - NSString *filename = keys[0]; - if (filename) { // native modules may have no value - [pathCache setObject:filename forKey:pathCacheKey]; - } - } + + NSString *data; + switch (type) { + case Native: + module = [self loadCoreModule:resolvedFilename withContext:kroll]; + break; + case JS: + data = [self loadFile:resolvedFilename]; + if (data != nil) { + module = [self loadJavascriptText:data fromFile:resolvedFilename withContext:context]; } + break; + case JSON: + data = [self loadFile:resolvedFilename]; + if (data != nil) { + module = [self loadJavascriptObject:data fromFile:resolvedFilename withContext:context]; + } + break; + case NativeJS: + module = [self loadCoreModuleAsset:resolvedFilename withContext:context]; + break; + + default: + break; } } @finally { [self setCurrentURL:oldURL]; + // Cache the resolved path for this request if we got a module + if (module != nil && resolvedFilename != nil) { + [modules setObject:module forKey:resolvedFilename]; + return module; + } } + // should never happen! // 4. THROW "not found" NSString *arch = [TiUtils currentArchitecture]; @throw [NSException exceptionWithName:@"org.test.kroll" reason:[NSString stringWithFormat:@"Couldn't find module: %@ for architecture: %@", path, arch] userInfo:nil]; // TODO Set 'code' property to 'MODULE_NOT_FOUND' to match Node? diff --git a/iphone/cli/commands/_build.js b/iphone/cli/commands/_build.js index 8f4d1e089ec..fc93e794502 100644 --- a/iphone/cli/commands/_build.js +++ b/iphone/cli/commands/_build.js @@ -157,7 +157,11 @@ function iOSBuilder() { // a list of relative paths to js files that need to be encrypted // note: the filename will have all periods replaced with underscores + // FIXME: Use a Map from original names -> encrypted names this.jsFilesToEncrypt = []; + // a list of relative paths to js files that have been encrypted + // note: this is the original filename used by our require _index_.json and referenced within the app + this.jsFilesEncrypted = []; // set to true if any js files changed so that we can trigger encryption to run this.jsFilesChanged = false; @@ -2248,6 +2252,7 @@ iOSBuilder.prototype.run = function (logger, config, cli, finished) { // titanium related tasks 'writeDebugProfilePlists', 'copyResources', + 'generateRequireIndex', // has to be run before encryption, since index may be encrypted 'encryptJSFiles', 'writeI18NFiles', 'processTiSymbols', @@ -5825,9 +5830,10 @@ iOSBuilder.prototype.copyResources = function copyResources(next) { if (file.indexOf('/') === 0) { file = path.basename(file); } + this.jsFilesEncrypted.push(file); // original name file = file.replace(/\./g, '_'); info.dest = path.join(this.buildAssetsDir, file); - this.jsFilesToEncrypt.push(file); + this.jsFilesToEncrypt.push(file); // encrypted name } try { @@ -5904,11 +5910,15 @@ iOSBuilder.prototype.copyResources = function copyResources(next) { function writeBootstrapJson() { this.logger.info(__('Writing bootstrap json')); - const bootstrapJsonRelativePath = this.encryptJS ? path.join('ti_internal', 'bootstrap_json') : path.join('ti.internal', 'bootstrap.json'), - bootstrapJsonAbsolutePath = path.join(this.encryptJS ? this.buildAssetsDir : this.xcodeAppDir, bootstrapJsonRelativePath), - bootstrapJsonString = JSON.stringify({ scripts: jsBootstrapFiles }); + const originalBootstrapJsonName = path.join('ti.internal', 'bootstrap.json'); + const bootstrapJsonRelativePath = this.encryptJS ? path.join('ti_internal', 'bootstrap_json') : originalBootstrapJsonName; + const bootstrapJsonAbsolutePath = path.join(this.encryptJS ? this.buildAssetsDir : this.xcodeAppDir, bootstrapJsonRelativePath); + const bootstrapJsonString = JSON.stringify({ scripts: jsBootstrapFiles }); - this.encryptJS && this.jsFilesToEncrypt.push(bootstrapJsonRelativePath); + if (this.encryptJS) { + this.jsFilesEncrypted.push(originalBootstrapJsonName); // original name + this.jsFilesToEncrypt.push(bootstrapJsonRelativePath); // encrypted name + } if (!fs.existsSync(bootstrapJsonAbsolutePath) || (bootstrapJsonString !== fs.readFileSync(bootstrapJsonAbsolutePath).toString())) { this.logger.debug(__('Writing %s', bootstrapJsonAbsolutePath.cyan)); @@ -5929,14 +5939,17 @@ iOSBuilder.prototype.copyResources = function copyResources(next) { function writeAppProps() { this.logger.info(__('Writing app properties')); - const appPropsFile = this.encryptJS ? path.join(this.buildAssetsDir, '_app_props__json') : path.join(this.xcodeAppDir, '_app_props_.json'), - props = {}; + const appPropsFile = this.encryptJS ? path.join(this.buildAssetsDir, '_app_props__json') : path.join(this.xcodeAppDir, '_app_props_.json'); + const props = {}; - this.encryptJS && this.jsFilesToEncrypt.push('_app_props__json'); + if (this.encryptJS) { + this.jsFilesEncrypted.push('_app_props_.json'); // original name + this.jsFilesToEncrypt.push('_app_props__json'); // encrypted name + } - this.tiapp.properties && Object.keys(this.tiapp.properties).forEach(function (prop) { + this.tiapp.properties && Object.keys(this.tiapp.properties).forEach(prop => { props[prop] = this.tiapp.properties[prop].value; - }, this); + }); const contents = JSON.stringify(props); if (!fs.existsSync(appPropsFile) || contents !== fs.readFileSync(appPropsFile).toString()) { @@ -6003,12 +6016,10 @@ iOSBuilder.prototype.encryptJSFiles = function encryptJSFiles(next) { } const titaniumPrepHook = this.cli.createHook('build.ios.titaniumprep', this, function (exe, args, opts, done) { - let tries = 0, - completed = false; + let tries = 0; + let completed = false; - this.jsFilesToEncrypt.forEach(function (file) { - this.logger.debug(__('Preparing %s', file.cyan)); - }, this); + this.jsFilesToEncrypt.forEach(file => this.logger.debug(__('Preparing %s', file.cyan))); async.whilst( function () { @@ -6030,13 +6041,8 @@ iOSBuilder.prototype.encryptJSFiles = function encryptJSFiles(next) { child.stdin.write(this.jsFilesToEncrypt.join('\n')); child.stdin.end(); - child.stdout.on('data', function (data) { - out += data.toString(); - }); - - child.stderr.on('data', function (data) { - err += data.toString(); - }); + child.stdout.on('data', data => out += data.toString()); + child.stderr.on('data', data => err += data.toString()); child.on('close', function (code) { if (code) { @@ -6092,6 +6098,42 @@ iOSBuilder.prototype.encryptJSFiles = function encryptJSFiles(next) { ); }; +iOSBuilder.prototype.generateRequireIndex = function generateRequireIndex(callback) { + const index = {}; + const binAssetsDir = this.xcodeAppDir.replace(/\\/g, '/'); + + // Write _index_.json file with our JS/JSON file listing. This may also be encrypted + const destFilename = this.encryptJS ? '_index__json' : '_index_.json'; + const destFile = path.join(this.encryptJS ? this.buildAssetsDir : this.xcodeAppDir, destFilename); + if (this.encryptJS) { + this.jsFilesToEncrypt.push(destFilename); + } + + // Grab unencrypted JS/JSON files + (function walk(dir) { + fs.readdirSync(dir).forEach(filename => { + const file = path.join(dir, filename); + if (fs.existsSync(file)) { + if (fs.statSync(file).isDirectory()) { + walk(file); + } else if (/\.js(on)?$/.test(filename)) { + index[file.replace(/\\/g, '/').replace(binAssetsDir + '/', 'Resources/')] = 1; // 1 for exists on disk + } + } + }); + }(this.xcodeAppDir)); + + // Grab encrypted JS/JSON files + this.jsFilesEncrypted.forEach(file => { + index['Resources/' + file.replace(/\\/g, '/')] = 2; // 2 for encrypted + }); + + delete index['Resources/_app_props_.json']; + + fs.existsSync(destFile) && fs.unlinkSync(destFile); + fs.writeFile(destFile, JSON.stringify(index), callback); +}; + iOSBuilder.prototype.writeI18NFiles = function writeI18NFiles() { this.logger.info(__('Writing i18n files')); diff --git a/iphone/iphone/Titanium.xcodeproj/project.pbxproj b/iphone/iphone/Titanium.xcodeproj/project.pbxproj index 68653682355..862b2176ffb 100644 --- a/iphone/iphone/Titanium.xcodeproj/project.pbxproj +++ b/iphone/iphone/Titanium.xcodeproj/project.pbxproj @@ -2108,7 +2108,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "cp -Rf \"$PROJECT_DIR/../../common/Resources/.\" \"$TARGET_BUILD_DIR/$PRODUCT_NAME.app\"\ncp -Rf \"$PROJECT_DIR/../Resources/.\" \"$TARGET_BUILD_DIR/$PRODUCT_NAME.app\"\n\"$PROJECT_DIR/../../support/dev/localecompiler.py\" \"$PROJECT_DIR/..\" ios simulator \"$TARGET_BUILD_DIR/$PRODUCT_NAME.app\"\nmkdir -p \"$TARGET_BUILD_DIR/$PRODUCT_NAME.app/modules/ui/images\"\ncp -Rf \"$PROJECT_DIR/../Resources/modules/ui/.\" \"$TARGET_BUILD_DIR/$PRODUCT_NAME.app/modules/ui/images\"\n"; + shellScript = "cp -Rf \"$PROJECT_DIR/../../common/Resources/.\" \"$TARGET_BUILD_DIR/$PRODUCT_NAME.app\"\ncp -Rf \"$PROJECT_DIR/../Resources/.\" \"$TARGET_BUILD_DIR/$PRODUCT_NAME.app\"\n\"$PROJECT_DIR/../../support/dev/localecompiler.py\" \"$PROJECT_DIR/..\" ios simulator \"$TARGET_BUILD_DIR/$PRODUCT_NAME.app\"\nmkdir -p \"$TARGET_BUILD_DIR/$PRODUCT_NAME.app/modules/ui/images\"\ncp -Rf \"$PROJECT_DIR/../Resources/modules/ui/.\" \"$TARGET_BUILD_DIR/$PRODUCT_NAME.app/modules/ui/images\"\n\"\$PROJECT_DIR/../../support/dev/index_generator.js\" \"$TARGET_BUILD_DIR/$PRODUCT_NAME.app\"\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/support/dev/index_generator.js b/support/dev/index_generator.js new file mode 100755 index 00000000000..cb82b3edce5 --- /dev/null +++ b/support/dev/index_generator.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node +/** + * generates an _index_.json file for use when running Titanium under xcode + */ +'use strict'; + +const fs = require('fs-extra'); +const path = require('path'); + +const dirToTraverse = process.argv[2]; +// gather all the js/json files under this dir and generate an index.json file in it + +const index = {}; +const destFile = path.join(dirToTraverse, '_index_.json'); + +(function walk(dir) { + fs.readdirSync(dir).forEach(function (filename) { + var file = path.join(dir, filename); + if (fs.existsSync(file)) { + if (fs.statSync(file).isDirectory()) { + walk(file); + } else if (/\.js(on)?$/.test(filename)) { + index[file.replace(/\\/g, '/').replace(dirToTraverse + '/', 'Resources/')] = 1; + } + } + }); +}(dirToTraverse)); + +fs.existsSync(destFile) && fs.unlinkSync(destFile); +fs.writeFileSync(destFile, JSON.stringify(index));