diff --git a/iphone/Resources/hyperloop/hyperloop.bootstrap.js b/iphone/Resources/hyperloop/hyperloop.bootstrap.js new file mode 100644 index 00000000..e05c522e --- /dev/null +++ b/iphone/Resources/hyperloop/hyperloop.bootstrap.js @@ -0,0 +1,2 @@ +// This file will be overwritten by hyperloop build hook if at least 1 native class was detected in app. +// Will provide require/import bindings between native type name to hyperloop generated JS file. diff --git a/iphone/hooks/generate/code-generator.js b/iphone/hooks/generate/code-generator.js index 57d4fbf2..05b19f3d 100644 --- a/iphone/hooks/generate/code-generator.js +++ b/iphone/hooks/generate/code-generator.js @@ -45,6 +45,7 @@ class CodeGenerator { this.generateStructs(outputPath); this.generateModules(outputPath); this.generateCustoms(outputPath); + this.generateBootstrap(outputPath); } /** @@ -233,6 +234,40 @@ class CodeGenerator { util.generateFile(outputPath, 'custom', {framework:'Hyperloop', name:'Custom'}, code, '.m'); } + /** + * Generate a hyperloop bootstrap script to be loaded on app startup, but before the "app.js" gets loaded. + * Provides JS require/import alias names matching native class names to their equivalent JS files. + * @param {String} outputPath Path of directory to write bootstrap file to. + */ + generateBootstrap(outputPath) { + const fileLines = []; + const fetchBindingsFrom = (sourceTypes) => { + if (!sourceTypes) { + return; + } + const isModule = (sourceTypes == this.sourceSet.modules); + for (const typeName in sourceTypes) { + const frameworkName = sourceTypes[typeName].framework; + if (!frameworkName) { + continue; + } + const requireName = `/hyperloop/${frameworkName.toLowerCase()}/${typeName.toLowerCase()}`; + if (frameworkName !== typeName) { + fileLines.push(`binding.redirect('${frameworkName}/${typeName}', '${requireName}');`); + } + if (isModule) { + fileLines.push(`binding.redirect('${frameworkName}', '${requireName}');`); + } + } + }; + fetchBindingsFrom(this.sourceSet.classes); + fetchBindingsFrom(this.sourceSet.structs); + fetchBindingsFrom(this.sourceSet.modules); + + const filePath = path.join(outputPath, 'hyperloop.bootstrap.js'); + fs.writeFileSync(filePath, fileLines.join('\n') + '\n'); + } + /** * Checks if a module needs a native wrapper file generated. * diff --git a/iphone/hooks/generate/custom.js b/iphone/hooks/generate/custom.js index c6b06768..d6daecb0 100644 --- a/iphone/hooks/generate/custom.js +++ b/iphone/hooks/generate/custom.js @@ -3,9 +3,6 @@ * Copyright (c) 2015-2018 by Appcelerator, Inc. */ 'use strict'; -const fs = require('fs'); -const path = require('path'); -const util = require('util'); const utillib = require('./util'); const classgen = require('./class'); const babelParser = require('@babel/parser'); @@ -479,84 +476,6 @@ function addSymbolReference (state, node, key) { } } -/** - * Checks for methods that require refactoring and adds them to a list we - * use later on to show migration instructions. - * - * This can be removed with Hyperloop 3.0 or propably even earlier. - * - * @param {Object} state Parser state object - * @param {Object} node Node in the AST to inspect - */ -function addMigrationHelpIfNeeded(state, node) { - state.needMigration = state.needMigration || []; - var migrationTable = utillib.getMethodTableForMigration(); - - if (['CallExpression', 'MemberExpression'].indexOf(node.type) === -1) { - return; - } - - var migratableMethod = traverseUpAndFindMigratableMethod(node, migrationTable); - if (migratableMethod !== null) { - var entryExists = state.needMigration.some(function (m) { - return m.label === migratableMethod.label && m.line === migratableMethod.line; - }); - if (!entryExists) { - state.needMigration.push(migratableMethod); - } - } -} - -/** - * Traverse up in the AST to find all possible method calls that may require - * a migration note. - * - * Only handles nested Call- and MemberExpressions so we can detect stuff - * like this: - * - * var path1 = UIBundle.mainBundle().bundlePath - * var path2 = UIBundle.mainBundle().pathForImageResource() - * - * @param {Object} node Node in the AST to inspect - * @param {Object} migrationTable Object with mapping of class name and methods that need migration - * @return {Object|null} Object with info about matching call expression or null if none found - */ -function traverseUpAndFindMigratableMethod(node, migrationTable) { - if (!node) { - return null; - } - - if (['CallExpression', 'MemberExpression'].indexOf(node.type) === -1) { - return null; - } - - if (node.type === 'MemberExpression') { - return traverseUpAndFindMigratableMethod(node.object, migrationTable); - } - - var callee = node.callee; - if (callee.type !== 'MemberExpression') { - return null; - } - - if (callee.object.type !== 'Identifier') { - return traverseUpAndFindMigratableMethod(callee.object, migrationTable); - } - - var objectName = callee.object.name; - var methods = migrationTable.hasOwnProperty(objectName) ? migrationTable[objectName] : []; - var methodName = callee.property.name; - if (methods.indexOf(methodName) === -1) { - return null; - } - - return { - objectName: objectName, - methodName: methodName, - line: node.loc.start.line - }; -} - /** * parse a buf of JS into a state object */ @@ -577,7 +496,6 @@ Parser.prototype.parse = function (buf, fn, state) { // reset these per module state.classesByVariable = {}; state.referencedClasses = {}; - state.needMigration = []; // these are symbol references found in our source code. // this is a little brute force and sloppy but gets @@ -616,7 +534,6 @@ Parser.prototype.parse = function (buf, fn, state) { const prop = p.node.callee.name; isValidSymbol(prop) && (state.References.functions[prop] = (state.References.functions[prop] || 0) + 1); } - addMigrationHelpIfNeeded(state, p.node); if (isHyperloopMethodCall(p.node, 'defineClass')) { if (p.parent.type !== 'VariableDeclaration' && p.parent.type !== 'VariableDeclarator') { @@ -646,7 +563,6 @@ Parser.prototype.parse = function (buf, fn, state) { MemberExpression: function(p) { if (!/^(AssignmentExpression|CallExpression|ExpressionStatement|VariableDeclaration)$/.test(p.parent.type)) { addSymbolReference(state, p.node, 'getter'); - addMigrationHelpIfNeeded(state, p.node); } }, AssignmentExpression: function(p) { diff --git a/iphone/hooks/generate/index.js b/iphone/hooks/generate/index.js index 2642c873..53db2601 100644 --- a/iphone/hooks/generate/index.js +++ b/iphone/hooks/generate/index.js @@ -213,6 +213,7 @@ function generateFromJSON(name, json, state, callback, includes) { processProtocolInheritance(json.protocols); + var modules = {}; var sourceSet = { classes: {}, structs: {}, @@ -242,6 +243,7 @@ function generateFromJSON(name, json, state, callback, includes) { }); } sourceSet.classes[k] = genclass.generate(json, cls, state); + makeModule(modules, cls, state); }); // structs @@ -254,8 +256,6 @@ function generateFromJSON(name, json, state, callback, includes) { sourceSet.structs[k] = genstruct.generate(json, struct); }); - // modules - var modules = {}; // define module based functions json.functions && Object.keys(json.functions).forEach(function (k) { var func = json.functions[k]; diff --git a/iphone/hooks/generate/module.js b/iphone/hooks/generate/module.js index 2a69b3c5..b85cd304 100644 --- a/iphone/hooks/generate/module.js +++ b/iphone/hooks/generate/module.js @@ -4,8 +4,6 @@ */ 'use strict'; const util = require('./util'); -const path = require('path'); -const fs = require('fs'); function makeModule(json, module, state) { var entry = { @@ -15,7 +13,8 @@ function makeModule(json, module, state) { class_methods: [], obj_class_method: [], static_variables: {}, - blocks: module.blocks + blocks: module.blocks, + nested_types: {} }, framework: module.framework, filename: module.filename, @@ -58,6 +57,20 @@ function makeModule(json, module, state) { } }); + // Make framework's classes and structs available via the JS module's properties. + const copyNestedTypes = (sourceTypes) => { + if (sourceTypes) { + for (const typeName in sourceTypes) { + const typeInfo = sourceTypes[typeName]; + if ((typeInfo.framework === module.framework) && (typeName !== module.name)) { + entry.class.nested_types[typeName] = typeInfo; + } + } + } + }; + copyNestedTypes(json.classes); + copyNestedTypes(json.structs); + entry.renderedImports = util.makeImports(json, entry.imports); return entry; } diff --git a/iphone/hooks/generate/templates/class.ejs b/iphone/hooks/generate/templates/class.ejs index d17519aa..16b6c078 100644 --- a/iphone/hooks/generate/templates/class.ejs +++ b/iphone/hooks/generate/templates/class.ejs @@ -195,4 +195,10 @@ Object.setPrototypeOf = Object.setPrototypeOf || function(obj, proto) { return obj; } +Object.defineProperty(<%= data.class.name %>, '<%= data.class.name %>', { + value: <%= data.class.name %>, + enumerable: false, + writable: false +}); + module.exports = <%= data.class.name %>; diff --git a/iphone/hooks/generate/templates/module.ejs b/iphone/hooks/generate/templates/module.ejs index a7c7cae7..4b7a225e 100644 --- a/iphone/hooks/generate/templates/module.ejs +++ b/iphone/hooks/generate/templates/module.ejs @@ -75,6 +75,27 @@ keys.forEach(function (k, index) { %> }); <% } -%> +<% if (data.class.nested_types && Object.keys(data.class.nested_types).length) { -%> +// framework classes and structs +Object.defineProperties(<%= data.class.name %>, { +<% var keys = Object.keys(data.class.nested_types); +keys.forEach(function (nestedTypeName, index) { %> + <%=nestedTypeName%>: { + get: function() { + return require('/hyperloop/<%= data.framework.toLowerCase() %>/<%= nestedTypeName.toLowerCase() %>'); + }, + enumerable: true + }<%=index + 1 < keys.length ? ',':''%> +<% }) %> +}); +<% } -%> + <% if (!data.excludeHeader) { -%> +Object.defineProperty(<%= data.class.name %>, '<%= data.class.name %>', { + value: <%= data.class.name %>, + enumerable: false, + writable: false +}); + module.exports = <%= data.class.name %>; <% } -%> diff --git a/iphone/hooks/generate/templates/struct.ejs b/iphone/hooks/generate/templates/struct.ejs index 778ef3c7..3f4dd3ff 100644 --- a/iphone/hooks/generate/templates/struct.ejs +++ b/iphone/hooks/generate/templates/struct.ejs @@ -57,4 +57,10 @@ function $initialize () { $init = true; } +Object.defineProperty(<%= data.class.name %>, '<%= data.class.name %>', { + value: <%= data.class.name %>, + enumerable: false, + writable: false +}); + module.exports = <%= data.class.name %>; diff --git a/iphone/hooks/generate/util.js b/iphone/hooks/generate/util.js index 6359fa6c..298d3588 100644 --- a/iphone/hooks/generate/util.js +++ b/iphone/hooks/generate/util.js @@ -1267,41 +1267,6 @@ function resolveArg (metabase, imports, arg) { } } -/** - * Gets a mapping of all classes and their properties that are affected by - * the UIKIT_DEFINE_AS_PROPERTIES or FOUNDATION_SWIFT_SDK_EPOCH_AT_LEAST - * macros. - * - * UIKIT_DEFINE_AS_PROPERTIES and FOUNDATION_SWIFT_SDK_EPOCH_AT_LEAST introduce - * new readonly properties in favor of methods with the same name. This changes - * how one would access them in Hyperloop. - * - * For example: - * - * // < Hyperloop 2.0.0, as method - * var color = UIColor.redColor(); - * // >= Hyperloop 2.0.0, as property (note the missing parenthesis) - * var color = UIColor.redColor; - * - * @return {Object} Contains a mapping of class names and their affected properties - */ -function getMethodTableForMigration() { - var migrationFilename = 'migration-20161014143619.json'; - - if (getMethodTableForMigration.cachedTable) { - return getMethodTableForMigration.cachedTable; - } - - var migrationPathAndFilename = path.resolve(__dirname, path.join('../../data', migrationFilename)); - if (fs.existsSync(migrationPathAndFilename)) { - getMethodTableForMigration.cachedTable = JSON.parse(fs.readFileSync(migrationPathAndFilename).toString()); - } else { - getMethodTableForMigration.cachedTable = {}; - } - - return getMethodTableForMigration.cachedTable; -} - exports.repeat = repeat; exports.generateTemplate = generateTemplate; exports.makeImports = makeImports; @@ -1327,7 +1292,6 @@ exports.camelCase = camelCase; exports.resolveArg = resolveArg; exports.toValueDefault = toValueDefault; exports.isPrimitive = isPrimitive; -exports.getMethodTableForMigration = getMethodTableForMigration; Object.defineProperty(exports, 'logger', { get: function () { diff --git a/iphone/hooks/hyperloop.js b/iphone/hooks/hyperloop.js index 1e7550d1..ad824582 100644 --- a/iphone/hooks/hyperloop.js +++ b/iphone/hooks/hyperloop.js @@ -15,9 +15,6 @@ const IOS_MIN = '9.0'; // Set this to enforce a minimum Titanium SDK const TI_MIN = '8.0.0'; -// Minimum SDK to use the newer build.ios.compileJsFile hook -const COMPILE_JS_FILE_HOOK_SDK_MIN = '7.1.0'; - // Set the iOS SDK minium const IOS_SDK_MIN = '9.0'; @@ -27,7 +24,6 @@ const hm = require('hyperloop-metabase'); const metabase = hm.metabase; const ModuleMetadata = metabase.ModuleMetadata; const fs = require('fs-extra'); -const crypto = require('crypto'); const chalk = require('chalk'); const async = require('async'); const HL = chalk.magenta.inverse('Hyperloop'); @@ -35,7 +31,6 @@ const semver = require('semver'); const babelParser = require('@babel/parser'); const t = require('@babel/types'); -const generate = require('@babel/generator').default; const traverse = require('@babel/traverse').default; const generator = require('./generate'); @@ -82,7 +77,6 @@ function HyperloopiOSBuilder(logger, config, cli, appc, hyperloopConfig, builder this.cocoaPodsBuildSettings = {}; this.cocoaPodsProducts = []; this.headers = null; - this.needMigration = {}; // set our CLI logger hm.util.setLog(builder.logger); @@ -97,7 +91,7 @@ HyperloopiOSBuilder.prototype.compileJsFile = function (builder, callback) { const from = builder.args[1]; const to = builder.args[2]; - this.patchJSFile(obj, from, to, callback); + this.processJSFile(obj, from, to, callback); } catch (e) { callback(e); } @@ -406,7 +400,7 @@ HyperloopiOSBuilder.prototype.detectSwiftVersion = function detectSwiftVersion(c }; /** - * Re-write generated JS source + * Parses the given JS source for native framework usage. * @param {Object} obj - JS object holding data about the file * @param {String} obj.contents - current source code for the file * @param {String} obj.original - original source of the file @@ -414,7 +408,7 @@ HyperloopiOSBuilder.prototype.detectSwiftVersion = function detectSwiftVersion(c * @param {String} destinationFilename - path to destination JS file * @param {Function} cb - callback function */ -HyperloopiOSBuilder.prototype.patchJSFile = function patchJSFile(obj, sourceFilename, destinationFilename, cb) { +HyperloopiOSBuilder.prototype.processJSFile = function processJSFile(obj, sourceFilename, destinationFilename, cb) { const contents = obj.contents; // skip empty content if (!contents.length) { @@ -439,7 +433,6 @@ HyperloopiOSBuilder.prototype.patchJSFile = function patchJSFile(obj, sourceFile // require() calls with the Hyperloop layer const requireRegexp = /[\w_/\-\\.]+/ig; const self = this; - let changedAST = false; const HyperloopVisitor = { // ES5-style require calls CallExpression: function (p) { @@ -511,18 +504,11 @@ HyperloopiOSBuilder.prototype.patchJSFile = function patchJSFile(obj, sourceFile // record our includes in which case we found a match self.includes[include] = 1; } - - // replace the require to point to our generated file path - p.replaceWith( - t.callExpression(p.node.callee, [ t.stringLiteral('/' + ref) ]) - ); - changedAST = true; } }, // ES6+-style imports ImportDeclaration: function (p) { const theString = p.node.source; - const replacements = []; let requireMatch; if (theString && t.isStringLiteral(theString) // module name is a string literal && (requireMatch = theString.value.match(requireRegexp)) !== null // Is it a hyperloop require? @@ -593,49 +579,14 @@ HyperloopiOSBuilder.prototype.patchJSFile = function patchJSFile(obj, sourceFile // record our includes in which case we found a match self.includes[include] = 1; } - - // replace the import to point to our generated file path - replacements.push(t.importDeclaration([ t.importDefaultSpecifier(spec.local) ], t.stringLiteral('/' + ref))); }); - - // Apply replacements - const replaceCount = replacements.length; - if (replaceCount === 1) { - p.replaceWith(replacements[0]); - changedAST = true; - } else if (replaceCount > 1) { - p.replaceWithMultiple(replacements); - changedAST = true; - } } } }; const ast = babelParser.parse(contents, { sourceFilename: sourceFilename, sourceType: 'unambiguous' }); traverse(ast, HyperloopVisitor); - // if we didn't change the AST, no need to generate new source! - // If we *do* generate new source, try to retain the lines and comments to retain source map - let newContents = changedAST ? generate(ast, { retainLines: true, comments: true }, contents).code : contents; - - // TODO: Remove once we combine the custom acorn-based parser and the babelParser parser above! - // Or maybe it can go now? The migration stuff is noted that it could be removed in 3.0.0... - var needMigration = this.parserState.state.needMigration; - if (needMigration.length > 0) { - this.needMigration[sourceFilename] = needMigration; - - needMigration.forEach(function (token) { - newContents = newContents.replace(token.objectName + '.' + token.methodName + '()', token.objectName + '.' + token.methodName); - }); - } - if (contents === newContents) { - this.logger.debug('No change, skipping ' + chalk.cyan(destinationFilename)); - } else { - this.logger.debug('Writing ' + chalk.cyan(destinationFilename)); - // modify the contents stored in the state object passed through the hook, - // so that SDK CLI can use new contents for minification/transpilation - obj.contents = newContents; - } cb(); }; @@ -1070,10 +1021,6 @@ HyperloopiOSBuilder.prototype.wireupBuildHooks = function wireupBuildHooks() { this.cli.on('build.ios.xcodebuild', { pre: this.hookXcodebuild.bind(this) }); - - this.cli.on('build.post.build', { - post: this.displayMigrationInstructions.bind(this) - }); }; /** @@ -1474,48 +1421,6 @@ HyperloopiOSBuilder.prototype.hasCustomShellScriptBuildPhases = function hasCust return config.ios && config.ios.xcodebuild && config.ios.xcodebuild.scripts; }; -/** - * Displays migration instructions for certain methods that changed with iOS 10 - * and Hyperloop 2.0.0 - * - * Can be removed in a later version of Hyperloop - */ -HyperloopiOSBuilder.prototype.displayMigrationInstructions = function displayMigrationInstructions() { - var that = this; - - if (Object.keys(this.needMigration).length === 0) { - return; - } - - that.logger.error(''); - that.logger.error('!!! CODE MIGRATION REQUIRED !!!'); - that.logger.error(''); - that.logger.error('Due to changes introduced in iOS 10 and Hyperloop 2.0.0 some method calls need'); - that.logger.error('to be changed to property access. It seems like you used some of the affected'); - that.logger.error('methods.'); - that.logger.error(''); - that.logger.error('We tried to fix most of these automatically during compile time. However, we did'); - that.logger.error('not touch your original source files. Please see the list below to help you'); - that.logger.error('migrate your code.'); - that.logger.error(''); - that.logger.error('NOTE: Some line numbers and file names shown here are from your compiled Alloy'); - that.logger.error('source code and may differ from your original source code.'); - - Object.keys(this.needMigration).forEach(function (pathAndFilename) { - var tokens = that.needMigration[pathAndFilename]; - var relativePathAndFilename = pathAndFilename.replace(that.resourcesDir, 'Resources').replace(/^Resources\/iphone\/alloy\//, 'app/'); - that.logger.error(''); - that.logger.error(' File: ' + relativePathAndFilename); - tokens.forEach(function (token) { - var memberExpression = token.objectName + '.' + token.methodName; - var callExpression = memberExpression + '()'; - that.logger.error(' Line ' + token.line + ': ' + callExpression + ' -> ' + memberExpression); - }); - }); - - that.logger.error(''); -}; - /** * Clean up unwanted files. * @param {Object} data - The hook payload. diff --git a/iphone/titanium/HyperloopModule.m b/iphone/titanium/HyperloopModule.m index c4459763..38a19792 100644 --- a/iphone/titanium/HyperloopModule.m +++ b/iphone/titanium/HyperloopModule.m @@ -62,6 +62,27 @@ -(JSObjectRef) propsObject; static JSObjectRef HLObjectMake(JSContextRef ctx, JSClassRef cls, id obj); JSObjectRef HyperloopGetWrapperForId(id obj); +static void HyperloopRelease () { + if (context) { + [callbacks removeAllObjects]; + [modules removeAllObjects]; + ARCRelease(callbacks); + ARCRelease(modules); + classClassRef = NULL; + pointerClassRef = NULL; + constructorClassRef = NULL; + objectClassRef = NULL; + bridge = nil; + context = nil; + callbacks = nil; + modules = nil; + } + if (javaScriptWrappers) { + CFRelease(javaScriptWrappers); + javaScriptWrappers = NULL; + } +} + /** * gets the memory address of an Objective-C object as a string */ @@ -987,6 +1008,10 @@ @implementation Hyperloop */ +(void)willStartNewContext:(KrollContext *)kroll bridge:(KrollBridge *)krollbridge { // NSLog(@"[TRACE][HYPERLOOP] willStartNewContext %@", kroll); + + // Release objects belonging to last context. (Will only happen if LiveView restarts app's JS runtime.) + HyperloopRelease(); + context = kroll; bridge = krollbridge; JSGlobalContextRef ctx = (JSGlobalContextRef)[kroll context]; @@ -1107,30 +1132,7 @@ +(void)didStartNewContext:(KrollContext *)kroll bridge:(KrollBridge *)bridge{ */ +(void)willStopNewContext:(KrollContext *)kroll bridge:(KrollBridge *)bridge{ // NSLog(@"[TRACE][HYPERLOOP] willStopNewContext %@", kroll); - if (context) { - [callbacks removeAllObjects]; - [modules removeAllObjects]; - JSGlobalContextRef ctx = (JSGlobalContextRef)[kroll context]; - JSStringRef prop = JSStringCreateWithUTF8CString("Hyperloop"); - JSObjectRef globalObjectRef = JSContextGetGlobalObject(ctx); - JSValueRef objectRef = JSObjectGetProperty(ctx, globalObjectRef, prop, NULL); - JSValueUnprotect(ctx, objectRef); - JSObjectDeleteProperty(ctx, globalObjectRef, prop, NULL); - JSStringRelease(prop); - JSClassRelease(classClassRef); - JSClassRelease(pointerClassRef); - JSClassRelease(constructorClassRef); - JSClassRelease(objectClassRef); - JSGlobalContextRelease(ctx); - ARCRelease(callbacks); - ARCRelease(modules); - classClassRef = nil; - pointerClassRef = nil; - constructorClassRef = nil; - context = nil; - callbacks = nil; - modules = nil; - } + HyperloopRelease(); } /** diff --git a/package-lock.json b/package-lock.json index 5994ac51..722bf523 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hyperloop", - "version": "7.0.1", + "version": "7.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "hyperloop", - "version": "7.0.1", + "version": "7.0.2", "license": "UNLICENSED", "devDependencies": { "async": "^2.6.1", diff --git a/package.json b/package.json index 344d3091..6bdf30ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hyperloop", - "version": "7.0.1", + "version": "7.0.2", "description": "Access native APIs from within Titanium.", "keywords": [ "appcelerator",