Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ios): support LiveView #351

Merged
merged 3 commits into from
Jun 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions iphone/Resources/hyperloop/hyperloop.bootstrap.js
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 35 additions & 0 deletions iphone/hooks/generate/code-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class CodeGenerator {
this.generateStructs(outputPath);
this.generateModules(outputPath);
this.generateCustoms(outputPath);
this.generateBootstrap(outputPath);
}

/**
Expand Down Expand Up @@ -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.
*
Expand Down
84 changes: 0 additions & 84 deletions iphone/hooks/generate/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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
*/
Expand All @@ -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
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions iphone/hooks/generate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ function generateFromJSON(name, json, state, callback, includes) {

processProtocolInheritance(json.protocols);

var modules = {};
var sourceSet = {
classes: {},
structs: {},
Expand Down Expand Up @@ -242,6 +243,7 @@ function generateFromJSON(name, json, state, callback, includes) {
});
}
sourceSet.classes[k] = genclass.generate(json, cls, state);
makeModule(modules, cls, state);
});

// structs
Expand All @@ -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];
Expand Down
19 changes: 16 additions & 3 deletions iphone/hooks/generate/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 6 additions & 0 deletions iphone/hooks/generate/templates/class.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>;
21 changes: 21 additions & 0 deletions iphone/hooks/generate/templates/module.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>;
<% } -%>
6 changes: 6 additions & 0 deletions iphone/hooks/generate/templates/struct.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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 %>;
36 changes: 0 additions & 36 deletions iphone/hooks/generate/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 () {
Expand Down