UNPKG

alloy

Version:

TiDev Titanium MVC Framework

1,043 lines (907 loc) 32.9 kB
var U = require('../../utils'), colors = require('colors'), path = require('path'), os = require('os'), fs = require('fs-extra'), walkSync = require('walk-sync'), jsonlint = require('@prantlf/jsonlint'), logger = require('../../logger'), astController = require('./ast/controller'), _ = require('lodash'), styler = require('./styler'), XMLSerializer = require('@xmldom/xmldom').XMLSerializer, CONST = require('../../common/constants'), sourceMapper = require('./sourceMapper'); /////////////////////////////////////// ////////// private variables ////////// /////////////////////////////////////// var alloyRoot = path.join(__dirname, '..', '..'), platformsDir = path.join(alloyRoot, '..', 'platforms'), alloyUniqueIdPrefix = '__alloyId', alloyUniqueIdCounter = 0, JSON_NULL = JSON.parse('null'), compilerConfig; /////////////////////////////// ////////// constants ////////// /////////////////////////////// var RESERVED_ATTRIBUTES = [ 'platform', 'formFactor', 'if', CONST.BIND_COLLECTION, CONST.BIND_WHERE, CONST.AUTOSTYLE_PROPERTY, 'ns', 'method', 'module' ], RESERVED_ATTRIBUTES_REQ_INC = [ 'platform', 'type', 'src', 'formFactor', 'if', CONST.BIND_COLLECTION, CONST.BIND_WHERE, CONST.AUTOSTYLE_PROPERTY, 'ns', 'method', 'module' ], RESERVED_EVENT_REGEX = new RegExp(`^(?:(${CONST.PLATFORMS.join('|')}):)?on([A-Z].+)`); // load CONDITION_MAP with platforms exports.CONDITION_MAP = { handheld: { runtime: '!Alloy.isTablet' }, tablet: { runtime: 'Alloy.isTablet' } }; _.each(CONST.PLATFORMS, function(p) { exports.CONDITION_MAP[p] = require(path.join(platformsDir, p, 'index'))['condition']; }); exports.bindingsMap = {}; exports.destroyCode = ''; exports.postCode = ''; exports.models = []; exports.dataFunctionNames = {}; ////////////////////////////////////// ////////// public interface ////////// ////////////////////////////////////// exports.getCompilerConfig = function() { return compilerConfig; }; exports.generateVarName = function(id, name) { if (_.includes(CONST.JS_RESERVED_ALL, id)) { U.die([ 'Invalid ID "' + id + '" for <' + name + '>.', 'Can\'t use reserved Javascript words as IDs.', 'Reserved words: [' + CONST.JS_RESERVED_ALL.sort().join(',') + ']' ]); } return '$.__views["' + id + '"]'; }; exports.generateUniqueId = function() { return alloyUniqueIdPrefix + alloyUniqueIdCounter++; }; exports.getNodeFullname = function(node) { var name = node.nodeName, ns = node.getAttribute('ns') || CONST.IMPLICIT_NAMESPACES[name] || CONST.NAMESPACE_DEFAULT, fullname = ns + '.' + name; return fullname; }; exports.isNodeForCurrentPlatform = function(node) { var isForCurrentPlatform = !node.hasAttribute('platform') || !compilerConfig || !compilerConfig.alloyConfig; _.each(node.getAttribute('platform').split(','), function(p) { // need to account for multiple platforms and negation, such as // platform=ios,android or platform=!ios or platform="android,!mobileweb" p = p.trim(); if (p === compilerConfig.alloyConfig.platform || (p.indexOf('!') === 0 && p.slice(1) !== compilerConfig.alloyConfig.platform)) { isForCurrentPlatform = true; } }); return isForCurrentPlatform; }; exports.getParserArgs = function(node, state, opts) { state = state || {}; opts = opts || {}; var defaultId = opts.defaultId || undefined, doSetId = opts.doSetId === false ? false : true, name = node.nodeName, ns = node.getAttribute('ns') || CONST.IMPLICIT_NAMESPACES[name] || CONST.NAMESPACE_DEFAULT, fullname = (ns && ns.length) ? (ns + '.' + name) : name, id = node.getAttribute('id') || defaultId || exports.generateUniqueId(), platform = node.getAttribute('platform'), formFactor = node.getAttribute('formFactor'), tssIf = node.getAttribute('if'), platformObj; // make sure we're not reusing the default ID for the first top level element if (id === exports.currentDefaultId && (node.parentNode && node.parentNode.nodeName !== 'Alloy') && !node.__idWarningHandled) { logger.warn([ '<' + name + '> at line ' + node.lineNumber + ' is using this view\'s default ID "' + id + '". ' + 'Only a top-level element in a view should use the default ID' ]); node.__idWarningHandled = true; } // handle binding arguments var bindObj = {}; bindObj[CONST.BIND_COLLECTION] = node.getAttribute(CONST.BIND_COLLECTION); bindObj[CONST.BIND_WHERE] = node.getAttribute(CONST.BIND_WHERE); bindObj[CONST.BIND_TRANSFORM] = node.getAttribute(CONST.BIND_TRANSFORM); bindObj[CONST.BIND_FUNCTION] = node.getAttribute(CONST.BIND_FUNCTION); // cleanup namespaces and nodes ns = ns.replace(/^Titanium\./, 'Ti.'); if (doSetId && !_.includes(CONST.MODEL_ELEMENTS, fullname)) { node.setAttribute('id', id); } // process the platform attribute if (platform) { platformObj = {}; _.each((platform).split(','), function(p) { var matches = U.trim(p).match(/^(\!{0,1})(.+)/); if (matches !== null) { var negate = matches[1]; var name = matches[2]; if (_.includes(CONST.PLATFORMS, name)) { if (negate === '!') { _.each(_.without(CONST.PLATFORMS, name), function(n) { platformObj[n] = true; }); } else { platformObj[name] = true; } return; } } U.die('Invalid platform type found: ' + p); }); } // get create arguments and events from attributes var createArgs = {}, events = []; var attrs = _.includes(['Alloy.Require'], fullname) ? RESERVED_ATTRIBUTES_REQ_INC : RESERVED_ATTRIBUTES; // determine whether to autoStyle this component // 1. autoStyle attribute // 2. autoStyle from <Alloy> // 3. autoStyle from config.json var autoStyle = (function() { var prop = CONST.AUTOSTYLE_PROPERTY; if (node.hasAttribute(prop)) { return node.getAttribute(prop) === 'true'; } else { return exports[prop]; } })(); // TODO: Add the apiName until TIMOB-12553 is resolved if (autoStyle) { createArgs[CONST.APINAME_PROPERTY] = fullname; } _.each(node.attributes, function(attr) { var attrName = attr.nodeName; if (_.includes(attrs, attrName)) { return; } var matches = attrName.match(RESERVED_EVENT_REGEX); if (matches !== null && exports.isNodeForCurrentPlatform(node) && !_.includes(CONST.SPECIAL_PROPERTY_NAMES, attrName)) { if (matches[1] && compilerConfig.alloyConfig.platform !== matches[1]) { return; } events.push({ name: U.lcfirst(matches[2]), value: node.getAttribute(attrName) }); } else { var theValue = node.getAttribute(attrName); // find platform specific attributes var attributeParts = attrName.split(':'); if ( attributeParts.length === 2 && _.includes(CONST.PLATFORMS, attributeParts[0])) { // if this attribute is for this platform, create it without namespace. if ( attributeParts[0] === compilerConfig.alloyConfig.platform ) { attrName = attributeParts[1]; } else { return; } } if (/(^|\+)\s*(?:(?:Ti|Titanium|Alloy.Globals|Alloy.CFG|\$.args)\.|L\(.+\)\s*$|WPATH\()/.test(theValue)) { var match = theValue.match(/^\s*L\([^'"]+\)\s*$/); if (match !== null) { theValue = theValue.replace(/\(/g, '("').replace(/\)/g, '")'); } theValue = styler.STYLE_EXPR_PREFIX + theValue; } if (attrName === 'class') { if (autoStyle) { createArgs[CONST.CLASS_PROPERTY] = theValue.split(/\s+/) || []; } } else { if (theValue === 'true') { theValue = true; } else if (theValue === 'false') { theValue = false; } else { var n = parseInt(theValue); if (!isNaN(n) && String(n) === theValue.trim()) { theValue = n; } else { n = parseFloat(theValue); if (!isNaN(n) && String(n) === theValue.trim()) { theValue = n; } } } _.set(createArgs, attrName, theValue ); } } }); if (autoStyle && !createArgs[CONST.CLASS_PROPERTY]) { createArgs[CONST.CLASS_PROPERTY] = []; } return _.extend({ ns: ns, name: name, id: id, fullname: fullname, formFactor: node.getAttribute('formFactor'), symbol: exports.generateVarName(id, name), classes: node.getAttribute('class').split(' ') || [], tssIf: node.getAttribute('if').split(',') || [], parent: state.parent || {}, platform: platformObj, createArgs: createArgs, events: events }, bindObj); }; exports.generateNodeExtended = function(node, state, newState) { return exports.generateNode(node, _.extend(_.clone(state), newState)); }; exports.generateNode = function(node, state, defaultId, isTopLevel, isModelOrCollection) { if (node.nodeType != 1) return ''; if (!exports.isNodeForCurrentPlatform(node)) { return ''; } var args = exports.getParserArgs(node, state, { defaultId: defaultId }), codeTemplate = 'if (<%= condition %>) {\n<%= content %>}\n', code = { content: '', pre: '' }; // Check for platform specific considerations var conditionType = compilerConfig && compilerConfig.alloyConfig && compilerConfig.alloyConfig.platform ? 'compile' : 'runtime'; if (args.platform) { var conditionArray = []; _.each(args.platform, function(v, k) { conditionArray.push(exports.CONDITION_MAP[k][conditionType]); }); code.condition = '(' + conditionArray.join(' || ') + ')'; } //Add form factor condition, if application form-factor specific runtime check if (args.formFactor && exports.CONDITION_MAP[args.formFactor]) { var check = exports.CONDITION_MAP[args.formFactor].runtime; code.condition = (code.condition) ? code.condition += ' && ' + check : check; } // ALOY-871: add the if condition check args.tssIf = _.compact(args.tssIf); if (args.tssIf.length > 0) { if (code.condition) { code.condition += (' && (' + args.tssIf.join(' || ') + ')'); } else { code.condition = args.tssIf.join(' || '); } } // pass relevant conditional information in state if (code.condition) { if (state.condition) { state.condition += '&&' + code.condition; } else { state.condition = code.condition; } } // Determine which parser to use for this node var parsersDir = path.join(alloyRoot, 'commands', 'compile', 'parsers'); var parserRequire = 'default'; if (_.includes(fs.readdirSync(parsersDir), args.fullname + '.js')) { parserRequire = args.fullname + '.js'; } // Execute the appropriate tag parser and append code var isLocal = state.local; // [ALOY-787] keeping track of widget id var widgetId = state.widgetId; state = require('./parsers/' + parserRequire).parse(node, state) || { parent: {} }; code.content += state.code; state.widgetId = widgetId; // Use local variable if given if (isLocal && state.parent) { args.symbol = state.parent.symbol || args.symbol; } // Use manually given args.symbol if present if (state.args) { args.symbol = state.args.symbol || args.symbol; } // add to list of top level views, if its top level if (isTopLevel) { if (state.isProxyProperty) { delete state.isProxyProperty; code.content += state.parent.symbol + ' && $.addProxyProperty("' + state.propertyName + '", ' + state.parent.symbol + ');\n'; } else { code.content += args.symbol + ' && $.addTopLevelView(' + args.symbol + ');\n'; } } // handle any model/collection code if (state.modelCode) { code.pre += state.modelCode; delete state.modelCode; } // handle any events from markup if (args.events && args.events.length > 0 && !_.includes(CONST.SKIP_EVENT_HANDLING, args.fullname) && !state.isViewTemplate) { // determine which function name to use for event handling: // * addEventListener() for Titanium proxies // * on() for everything else (controllers, models, collections) var eventFunc = /^Alloy\.(?:Collection|Model|Require|Widget)/.test(args.fullname) ? 'on' : 'addEventListener'; _.each(args.events, function(ev) { var eventObj = { obj: isModelOrCollection ? state.args.symbol : args.symbol, ev: ev.name, cb: ev.value, escapedCb: ev.value.replace(/'/g, "\\'"), func: eventFunc }, postCode; if (_.includes(['Alloy.Widget', 'Alloy.Require'], args.fullname)) { eventObj.obj = state.controller; } // create templates for immediate and deferred event handler creation var theDefer = _.template("__defers['<%= obj %>!<%= ev %>!<%= escapedCb %>']")(eventObj); var theEvent; if (eventFunc === 'addEventListener') { theEvent = _.template("$.addListener(<%= obj %>,'<%= ev %>',<%= cb %>)")(eventObj); } else { theEvent = _.template("<%= obj %>.<%= func %>('<%= ev %>',<%= cb %>)")(eventObj); } var deferTemplate = theDefer + ' && ' + theEvent + ';'; var immediateTemplate; if (/[\.\[]/.test(eventObj.cb)) { immediateTemplate = 'try{' + theEvent + ';}catch(e){' + theDefer + '=true;}'; } else { immediateTemplate = '<%= cb %>?' + theEvent + ':' + theDefer + '=true;'; } // add the generated code to the view code and post-controller code respectively code.content += _.template(immediateTemplate)(eventObj); postCode = _.template(deferTemplate)(eventObj); exports.postCode += state.condition ? _.template(codeTemplate)({ condition: state.condition, content: postCode }) : postCode; }); } // Continue parsing if necessary if (state.parent) { var states = _.isArray(state.parent) ? state.parent : [state.parent]; _.each(states, function(p) { var parent = p.node; if (!parent) { return; } for (var i = 0, l = parent.childNodes.length; i < l; i++) { var newState = _.defaults({ parent: p }, state); if (node.hasAttribute('formFactor') || state.parentFormFactor) { // propagate the form factor down through the hierarchy newState.parentFormFactor = (node.getAttribute('formFactor') || state.parentFormFactor); } code.content += exports.generateNode(parent.childNodes.item(i), newState); } }); } if (!isModelOrCollection) { return code.condition ? _.template(codeTemplate)(code) : code.content; } else { return { content: code.condition ? _.template(codeTemplate)(code) : code.content, pre: code.condition ? _.template(codeTemplate)({content:code.pre}) : code.pre }; } }; exports.expandRequireNode = function(requireNode, doRecursive) { var cloneNode = requireNode.cloneNode(true); function getViewRequirePath(node) { var regex = new RegExp('\\.' + CONST.FILE_EXT.VIEW + '$'), src = node.getAttribute('src'), fullname = exports.getNodeFullname(node), name = node.getAttribute('name') || CONST.NAME_WIDGET_DEFAULT, type = fullname === 'Alloy.Widget' ? 'widget' : node.getAttribute('type') || CONST.REQUIRE_TYPE_DEFAULT, fullpaths = []; var platform; if (compilerConfig && compilerConfig.alloyConfig && compilerConfig.alloyConfig.platform) { platform = compilerConfig.alloyConfig.platform; } // Must be a view, with a valid src, in a <Require> element if (!src) { return null; } else if (fullname === 'Alloy.Require' && type === 'view') { if (platform) { fullpaths.push(path.join(compilerConfig.dir.views, platform, src)); } fullpaths.push(path.join(compilerConfig.dir.views, src)); } else if (fullname === 'Alloy.Widget' || (fullname === 'Alloy.Require' && type === 'widget')) { U.getWidgetDirectories(compilerConfig.dir.home).forEach(function(wDir) { if (wDir.manifest.id === src) { if (platform) { fullpaths.push(path.join(wDir.dir, CONST.DIR.VIEW, platform, name)); } fullpaths.push(path.join(wDir.dir, CONST.DIR.VIEW, name)); } }); } else { return null; } // check the extensions on the paths to check var found = false; var fullpath; for (var i = 0; i < fullpaths.length; i++) { fullpath = fullpaths[i]; fullpath += regex.test(fullpath) ? '' : '.' + CONST.FILE_EXT.VIEW; if (fs.existsSync(fullpath)) { found = true; break; } } // abort if there's no view to be found if (!found) { U.die([ type + ' "' + src + '" ' + (type === 'widget' ? 'view "' + name + '" ' : '') + 'does not exist.', 'The following paths were inspected:' ].concat(fullpaths)); } return fullpath; } //create function, it expects 2 values. function insertAfter(newElement, targetElement) { //target is what you want it to go after. Look for this elements parent. var parent = targetElement.parentNode; //if the parents lastchild is the targetElement... if (parent.lastchild == targetElement) { //add the newElement after the target element. parent.appendChild(newElement); } else { // else the target has siblings, insert the new element between the target and it's next sibling. parent.insertBefore(newElement, targetElement.nextSibling); } } function processRequire(node, isFirst) { // make sure we have a valid required view and get its path var fullpath = getViewRequirePath(node); if (fullpath === null) { return; } // re-assemble XML with required elements if (isFirst) { cloneNode = U.XML.getAlloyFromFile(fullpath); } else { var newDocRoot = U.XML.getAlloyFromFile(fullpath); _.each(U.XML.getElementsFromNodes(newDocRoot.childNodes), function(n) { insertAfter(n, node); }); node.parentNode.removeChild(node); } } // Expand the <Require>, recursively if specified if (getViewRequirePath(cloneNode) !== null) { processRequire(cloneNode, true); while (doRecursive) { var reqs = cloneNode.getElementsByTagName('Require'); var widgets = cloneNode.getElementsByTagName('Widget'); var all = []; // condense node lists into a single array _.each(reqs, function(req) { all.push(req); }); _.each(widgets, function(widget) { all.push(widget); }); // find all the valid widgets/requires var viewRequires = _.filter(reqs, function(req) { return getViewRequirePath(req) !== null; }); if (viewRequires.length === 0) { break; } // TODO: https://jira.appcelerator.org/browse/ALOY-256 //_.each(viewRequires, processRequire); processRequire(viewRequires[0]); } } return cloneNode; }; exports.inspectRequireNode = function(node) { var newNode = exports.expandRequireNode(node, true); var children = U.XML.getElementsFromNodes(newNode.childNodes); var names = []; _.each(children, function(c) { var args = exports.getParserArgs(c); // skip model elements when inspecting nodes for <Require> if (_.includes(CONST.MODEL_ELEMENTS, args.fullname)) { newNode.removeChild(c); return; } names.push(args.fullname); }); return { children: U.XML.getElementsFromNodes(newNode.childNodes), length: names.length, names: names }; }; exports.copyWidgetResources = function(resources, resourceDir, widgetId, opts) { opts = opts || {}; var platform; if (compilerConfig && compilerConfig.alloyConfig && compilerConfig.alloyConfig.platform) { platform = compilerConfig.alloyConfig.platform; } _.each(resources, function(dir) { if (!path.existsSync(dir)) { return; } logger.trace('WIDGET_SRC=' + path.relative(compilerConfig.dir.project, dir)); var files = walkSync(dir); _.each(files, function(file) { file = path.normalize(file); var source = path.join(dir, file); // make sure the file exists and that it is not filtered if (!fs.existsSync(source) || (opts.filter && opts.filter.test(file)) || (opts.exceptions && _.includes(opts.exceptions, file))) { return; } if (fs.statSync(source).isFile()) { var dirname = path.dirname(file); var parts = dirname.split(/[\/\\]/); if (opts.titaniumFolder && parts[0] === opts.titaniumFolder) { dirname = parts.slice(1).join('/'); } var destDir = path.join(resourceDir, dirname, widgetId); var dest = path.join(destDir, path.basename(file)); if (!path.existsSync(destDir)) { fs.mkdirpSync(destDir); } logger.trace('Copying ' + file.yellow + ' --> ' + path.relative(compilerConfig.dir.project, dest).yellow + '...'); U.copyFileSync(source, dest); if (path.extname(source) === '.js' && compilerConfig.sourcemap) { sourceMapper.generateSourceMap({ target: { filename: file, filepath: dest, }, data: {}, origFile: { filename: file, filepath: source } }, compilerConfig); } } }); // [ALOY-1002] Remove ios folder copied from widget var iosDir = path.join(resourceDir, 'ios'); if (fs.existsSync(iosDir)) { fs.removeSync(iosDir); } logger.trace(' '); }); if (opts.theme) { // if this widget has been themed, copy its theme assets atop the stock ones var widgetThemeDir = path.join(compilerConfig.dir.project, 'app', 'themes', opts.theme, 'widgets', widgetId); if (fs.existsSync(widgetThemeDir)) { logger.trace('Processing themed widgets'); var widgetAssetSourceDir = path.join(widgetThemeDir, 'assets'); var widgetAssetTargetDir = path.join(resourceDir, widgetId); if (fs.existsSync(widgetAssetSourceDir)) { fs.copySync(widgetAssetSourceDir, widgetAssetTargetDir, {preserveTimestamps: true}); } // platform-specific assets from the widget must override those of the theme if (platform && path.existsSync(path.join(resources[0], platform))) { fs.copySync(path.join(resources[0], platform), widgetAssetTargetDir, {preserveTimestamps: true}); } // however platform-specific theme assets must override the platform assets from the widget if (platform && path.existsSync(path.join(widgetAssetSourceDir, platform))) { logger.trace('Processing platform-specific theme assets for the ' + widgetId + ' widget'); widgetAssetSourceDir = path.join(widgetAssetSourceDir, platform); fs.copySync(widgetAssetSourceDir, widgetAssetTargetDir, {preserveTimestamps: true}); } // [ALOY-1002] Remove platform-specific folders copied from theme if (fs.existsSync(widgetAssetTargetDir)) { var files = walkSync(widgetAssetTargetDir); _.each(files, function(file) { var source = path.join(widgetAssetTargetDir, file); if (path.existsSync(source) && fs.statSync(source).isDirectory()) { fs.removeSync(source); } }); } } } }; exports.mergeI18N = function mergeI18N(src, dest, opts) { var serializer = new XMLSerializer(); opts || (opts = {}); (function walk(src, dest) { if (!fs.existsSync(src)) return; fs.readdirSync(src).forEach(function (name) { var srcFile = path.join(src, name); var destFile = path.join(dest, name); if (!fs.existsSync(srcFile)) return; if (fs.statSync(srcFile).isDirectory()) { if (!fs.existsSync(destFile)) { fs.mkdirpSync(destFile); } return walk(srcFile, destFile); } if (!fs.existsSync(destFile)) { logger.debug('Writing ' + destFile.yellow); return U.copyFileSync(srcFile, destFile); } if (!/\.xml$/.test(srcFile)) { return; } // merge! var existing = {}; var destXml = U.XML.parseFromFile(destFile); var destDoc = destXml.documentElement; var srcXml = U.XML.parseFromFile(srcFile); var srcDoc = srcXml.documentElement; if (!destDoc) { U.die('Error processing "' + destFile + '"'); } if (!srcDoc) { U.die('Error processing "' + srcFile + '"'); } _.each(destDoc.getElementsByTagName('string'), function (node) { var name = node.getAttribute('name'); existing[name] = node; }); _.each(srcDoc.getElementsByTagName('string'), function (node) { var name = node.getAttribute('name'); if (!Object.prototype.hasOwnProperty.call(existing, name)) { destDoc.appendChild(destXml.createTextNode('\t')); destDoc.appendChild(node); destDoc.appendChild(destXml.createTextNode('\n')); } else if (opts.override) { destDoc.replaceChild(node, existing[name]); } }); logger.debug('Merging ' + srcFile.yellow + ' --> ' + destFile.yellow); fs.writeFileSync(destFile, serializer.serializeToString(destXml), 'utf8'); }); }(src, dest)); }; function updateImplicitNamspaces(platform) { switch (platform) { case 'android': break; case 'ios': break; } } exports.createCompileConfig = function(inputPath, outputPath, alloyConfig, buildLog) { var dirs = ['assets', 'config', 'controllers', 'lib', 'migrations', 'models', 'styles', 'themes', 'vendor', 'views', 'widgets']; var libDirs = ['builtins', 'template']; var resources = path.resolve(path.join(outputPath, 'Resources')); var obj = { alloyConfig: alloyConfig, dir: { home: path.resolve(inputPath), project: path.resolve(outputPath), resources: resources, resourcesAlloy: path.join(resources, 'alloy') }, buildLog: buildLog }; // create list of dirs _.each(dirs, function(dir) { obj.dir[dir] = path.resolve(path.join(inputPath, dir)); }); _.each(libDirs, function(dir) { obj.dir[dir] = path.resolve(path.join(alloyRoot, dir)); }); // ensure the generated directories exist U.ensureDir(obj.dir.resources); // process and normalize the config.json file var configs = _.defaults(generateConfig(obj), { // sets the theme theme: undefined, // are we going to generate sourcemaps? sourcemap: true, // are we enabling dynamic styling for all generated components? autoStyle: false, // the list of widget dependencies dependencies: {}, // TODO: Include no adapters by default adapters: CONST.ADAPTERS }); // normalize adapters if (!configs.adapters) { configs.adapters = []; } else if (!_.isArray(configs.adapters)) { configs.adapters = [configs.adapters]; } logger.debug(JSON.stringify(configs, null, ' ').split(os.EOL)); // update implicit namespaces, if possible updateImplicitNamspaces(alloyConfig.platform); // keep a copy of the config for this module compilerConfig = _.extend(obj, configs); return obj; }; function generateConfig(obj) { var buildLog = obj.buildLog; var o = {}; var alloyConfig = obj.alloyConfig; var platform = require('../../../platforms/' + alloyConfig.platform + '/index').titaniumFolder; //var defaultCfg = 'module.exports=' + JSON.stringify(o) + ';'; // get the app and resources locations var appCfg = path.join(obj.dir.home, 'config.' + CONST.FILE_EXT.CONFIG); var resourcesBase = (function() { var base = obj.dir.resources; if (platform) { base = path.join(base, platform); } return path.join(base, 'alloy'); })(); var resourcesCfg = path.join(resourcesBase, 'CFG.js'); // parse config.json, if it exists if (path.existsSync(appCfg)) { o = exports.parseConfig(appCfg, alloyConfig, o); if (o.theme) { var themeCfg = path.join(obj.dir.home, 'themes', o.theme, 'config.' + CONST.FILE_EXT.CONFIG); // parse theme config.json, if it exists if (path.existsSync(themeCfg)) { o = exports.parseConfig(themeCfg, alloyConfig, o); } } } // only regenerate the CFG.js when necessary var hash = U.createHashFromString(JSON.stringify(o)); if (buildLog.data.cfgHash && buildLog.data.cfgHash === hash && fs.existsSync(path.join(obj.dir.resources, 'alloy', 'CFG.js'))) { // use cached CFG.js file logger.info(' [config.json] config.json unchanged, using cached config.json...'); } else { // cached CFG.js is out of sync with config.json, regenerate and save logger.info(' [config.json] regenerating CFG.js from config.json...'); buildLog.data.cfgHash = hash; // write out the config runtime module fs.mkdirpSync(resourcesBase); //logger.debug('Writing "Resources/' + (platform ? platform + '/' : '') + 'alloy/CFG.js"...'); var output = 'module.exports=' + JSON.stringify(o) + ';'; fs.writeFileSync(resourcesCfg, output); // TODO: deal with TIMOB-14884 var baseFolder = path.join(obj.dir.resources, 'alloy'); if (!fs.existsSync(baseFolder)) { fs.mkdirpSync(baseFolder); } fs.writeFileSync(path.join(baseFolder, 'CFG.js'), output); } return o; } exports.parseConfig = function(file, alloyConfig, o) { var j, distType; try { j = jsonlint.parse(fs.readFileSync(file, 'utf8')); } catch (e) { U.die('Error processing "config.' + CONST.FILE_EXT.CONFIG + '"', e); } _.each(j, function(v, k) { if (!/^(?:env\:|os\:|dist\:)/.test(k) && k !== 'global') { logger.debug(k + ' = ' + JSON.stringify(v)); o[k] = v; } }); if (alloyConfig) { o = _.extend(o, j['global']); o = _.extend(o, j['env:' + alloyConfig.deploytype]); o = _.extend(o, j['os:' + alloyConfig.platform]); o = _.extend(o, j['env:' + alloyConfig.deploytype + ' os:' + alloyConfig.platform]); o = _.extend(o, j['os:' + alloyConfig.platform + ' env:' + alloyConfig.deploytype]); if (alloyConfig.deploytype === 'production' && alloyConfig.target) { distType = _.find(CONST.DIST_TYPES, function (obj) { return obj.value.indexOf(alloyConfig.target) !== -1; }); if (distType) { distType = distType.key.toLowerCase().replace('_', ':'); o = _.extend(o, j[distType]); o = _.extend(o, j['os:' + alloyConfig.platform + ' ' + distType]); } } if (alloyConfig.theme) { o.theme = alloyConfig.theme; } } return o; }; exports.loadController = function(file) { var code = { parentControllerName: '', controller: '', pre: '', es6mods: '' }, contents; // Read the controller file try { if (!path.existsSync(file)) { return code; } contents = fs.readFileSync(file, 'utf8'); } catch (e) { U.die('Error reading controller file "' + file + '".', e); } // get the base controller for this controller, also process import/export statements var controller = astController.processController(contents, file); code.controller = controller.code; code.parentControllerName = controller.base; code.es6mods = controller.es6mods; return code; }; exports.validateNodeName = function(node, names) { var fullname = exports.getNodeFullname(node); var ret = null; if (!_.isArray(names)) { names = [names]; } // Is the node name in the given list of valid names? ret = _.find(names, function(name) { return name === fullname; }); if (ret) { return ret; } // Is it an Alloy.Require? if (fullname === 'Alloy.Require' || fullname === 'Alloy.Widget') { var inspect = exports.inspectRequireNode(node); ret = _.find(inspect.children, function(n) { return _.includes(names, exports.getNodeFullname(n)); }); if (ret) { return exports.getNodeFullname(ret); } } return null; }; exports.generateCollectionBindingTemplate = function(args) { var code = ''; var COLLECTION_BINDING_EVENTS = CONST.COLLECTION_BINDING_EVENTS_092; // Check if not 0.9.2 and if it's a supported version as we'll default to 0.9.2 if the version is not supported if (compilerConfig.backbone !== '0.9.2' && CONST.SUPPORTED_BACKBONE_VERSIONS.includes(compilerConfig.backbone)) { COLLECTION_BINDING_EVENTS = CONST.COLLECTION_BINDING_EVENTS; } // Determine the collection variable to use var obj = { name: args[CONST.BIND_COLLECTION] }; var col = _.template((exports.currentManifest ? CONST.WIDGET_OBJECT : 'Alloy') + ".Collections['<%= name %>'] || <%= name %>")(obj); var colVar = exports.generateUniqueId(); // Create the code for the filter and transform functions var where = args[CONST.BIND_WHERE]; var transform = args[CONST.BIND_TRANSFORM]; var whereCode = where ? where + '(' + colVar + ')' : colVar + '.models'; var transformCode = transform ? transform + '(<%= localModel %>)' : '_.isFunction(<%= localModel %>.transform)?<%= localModel %>.transform():<%= localModel %>.toJSON()'; var handlerFunc = args[CONST.BIND_FUNCTION] || exports.generateUniqueId(); if (args.parentFormFactor) { if (!exports.dataFunctionNames[handlerFunc]) { exports.dataFunctionNames[handlerFunc] = []; } exports.dataFunctionNames[handlerFunc].push(args.parentFormFactor); // append the form factor for the code below handlerFunc += U.ucfirst(args.parentFormFactor); } // construct code template code += 'var ' + colVar + '=' + col + ';'; code += 'function ' + handlerFunc + '(e) {'; code += ' if (e && e.fromAdapter) { return; }'; code += ' var opts = ' + handlerFunc + '.opts || {};'; code += ' var models = ' + whereCode + ';'; code += ' var len = models.length;'; code += '<%= pre %>'; code += ' for (var i = 0; i < len; i++) {'; code += ' var <%= localModel %> = models[i];'; if (!args.isDataBoundMap) { code += ' <%= localModel %>.' + CONST.BIND_TRANSFORM_VAR + ' = ' + transformCode + ';'; } else { // because (ti.map).annotations[] doesn't accept an array of anonymous objects // we convert them to actual Annotations before pushing them to the array code += " <%= annotationArray %>.push(require('ti.map').createAnnotation(" + transformCode + '));'; } code += '<%= items %>'; code += ' }'; code += '<%= post %>'; code += '};'; code += colVar + ".on('" + COLLECTION_BINDING_EVENTS + "'," + handlerFunc + ');'; exports.destroyCode += colVar + ' && ' + ((args.parentFormFactor) ? 'Alloy.is' + U.ucfirst(args.parentFormFactor) + ' && ' : '' ) + colVar + ".off('" + COLLECTION_BINDING_EVENTS + "'," + handlerFunc + ');'; return code; };