diff --git a/Alloy/commands/compile/index.js b/Alloy/commands/compile/index.js index 1a742621e..f09b4c37e 100755 --- a/Alloy/commands/compile/index.js +++ b/Alloy/commands/compile/index.js @@ -23,6 +23,8 @@ var ejs = require('ejs'), BuildLog = require('./BuildLog'), Orphanage = require('./Orphanage'); +const MvcCompileTask = require('./tasks/mvc-compile-task'); + var alloyRoot = path.join(__dirname, '..', '..'), viewRegex = new RegExp('\\.' + CONST.FILE_EXT.VIEW + '$'), controllerRegex = new RegExp('\\.' + CONST.FILE_EXT.CONTROLLER + '$'), @@ -418,93 +420,52 @@ module.exports = function(args, program) { CU.models.push(m); }); - // Create a regex for determining which platform-specific - // folders should be used in the compile process - var filteredPlatforms = _.reject(CONST.PLATFORM_FOLDERS_ALLOY, function(p) { - return p === buildPlatform; - }); - filteredPlatforms = _.map(filteredPlatforms, function(p) { return p + '[\\\\\\/]'; }); - var filterRegex = new RegExp('^(?:(?!' + filteredPlatforms.join('|') + '))'); - - // don't process XML/controller files inside .svn folders (ALOY-839) - var excludeRegex = new RegExp('(?:^|[\\/\\\\])(?:' + CONST.EXCLUDED_FILES.join('|') + ')(?:$|[\\/\\\\])'); - - // Process all views/controllers and generate their runtime - // commonjs modules and source maps. - var tracker = {}; - _.each(widgetDirs, function(collection) { - // generate runtime controllers from views - var theViewDir = path.join(collection.dir, CONST.DIR.VIEW); - if (fs.existsSync(theViewDir)) { - _.each(walkSync(theViewDir), function(view) { - view = path.normalize(view); - if (viewRegex.test(view) && filterRegex.test(view) && !excludeRegex.test(view)) { - // make sure this controller is only generated once - var theFile = view.substring(0, view.lastIndexOf('.')); - var theKey = theFile.replace(new RegExp('^' + buildPlatform + '[\\/\\\\]'), ''); - var fp = path.join(collection.dir, theKey); - if (tracker[fp]) { return; } - - // generate runtime controller - logger.info('[' + view + '] ' + (collection.manifest ? collection.manifest.id + - ' ' : '') + 'view processing...'); - parseAlloyComponent(view, collection.dir, collection.manifest, null, restrictionPath); - tracker[fp] = true; - } - }); - } - - // generate runtime controllers from any controller code that has no - // corresponding view markup - var theControllerDir = path.join(collection.dir, CONST.DIR.CONTROLLER); - if (fs.existsSync(theControllerDir)) { - _.each(walkSync(theControllerDir), function(controller) { - controller = path.normalize(controller); - if (controllerRegex.test(controller) && filterRegex.test(controller) && !excludeRegex.test(controller)) { - // make sure this controller is only generated once - var theFile = controller.substring(0, controller.lastIndexOf('.')); - var theKey = theFile.replace(new RegExp('^' + buildPlatform + '[\\/\\\\]'), ''); - var fp = path.join(collection.dir, theKey); - if (tracker[fp]) { return; } - - // generate runtime controller - logger.info('[' + controller + '] ' + (collection.manifest ? - collection.manifest.id + ' ' : '') + 'controller processing...'); - parseAlloyComponent(controller, collection.dir, collection.manifest, true, restrictionPath); - tracker[fp] = true; - } + return Promise.resolve() + .then(() => { + const mvcCompileTask = new MvcCompileTask({ + incrementalDirectory: path.join(compileConfig.dir.project, 'build', 'alloy', 'incremental', 'mvc'), + logger, + compileConfig, + restrictionPath, + parseAlloyComponent, + sourceCollections: widgetDirs, + targetPlatform: buildPlatform }); - } - }); - logger.info(''); - - generateAppJs(paths, compileConfig, restrictionPath, compilerMakeFile); - - // ALOY-905: workaround TiSDK < 3.2.0 iOS device build bug where it can't reference app.js - // in platform-specific folders, so we just copy the platform-specific one to - // the Resources folder. - if (buildPlatform === 'ios' && tiapp.version.lt('3.2.0')) { - U.copyFileSync(path.join(paths.resources, titaniumFolder, 'app.js'), path.join(paths.resources, 'app.js')); - } + return mvcCompileTask.run(); + }) + .then(() => { + generateAppJs(paths, compileConfig, restrictionPath, compilerMakeFile); + + // ALOY-905: workaround TiSDK < 3.2.0 iOS device build bug where it can't reference app.js + // in platform-specific folders, so we just copy the platform-specific one to + // the Resources folder. + if (buildPlatform === 'ios' && tiapp.version.lt('3.2.0')) { + U.copyFileSync(path.join(paths.resources, titaniumFolder, 'app.js'), path.join(paths.resources, 'app.js')); + } - // optimize code - logger.info('----- OPTIMIZING -----'); + // optimize code + logger.info('----- OPTIMIZING -----'); - if (restrictionSkipOptimize) { - logger.info('Skipping optimize due to file restriction.'); - } else { - optimizeCompiledCode(alloyConfig, paths); - } + if (restrictionSkipOptimize) { + logger.info('Skipping optimize due to file restriction.'); + } else { + optimizeCompiledCode(alloyConfig, paths); + } - // trigger our custom compiler makefile - if (compilerMakeFile.isActive) { - compilerMakeFile.trigger('post:compile', _.clone(compileConfig)); - } + // trigger our custom compiler makefile + if (compilerMakeFile.isActive) { + compilerMakeFile.trigger('post:compile', _.clone(compileConfig)); + } - // write out the log for this build - buildLog.write(); + // write out the log for this build + buildLog.write(); - BENCHMARK('TOTAL', true); + BENCHMARK('TOTAL', true); + }) + .catch(e => { + logger.error(e); + U.die(e.message); + }); }; diff --git a/Alloy/commands/compile/tasks/mvc-compile-task.js b/Alloy/commands/compile/tasks/mvc-compile-task.js new file mode 100644 index 000000000..15e15f19f --- /dev/null +++ b/Alloy/commands/compile/tasks/mvc-compile-task.js @@ -0,0 +1,142 @@ +const _ = require('lodash'); +const { IncrementalFileTask } = require('appc-tasks'); +const fs = require('fs'); +const path = require('path'); +const walkSync = require('walk-sync'); + +const CONST = require('../../../common/constants'); + +const viewRegex = new RegExp('\\.' + CONST.FILE_EXT.VIEW + '$'); +const controllerRegex = new RegExp('\\.' + CONST.FILE_EXT.CONTROLLER + '$'); +const excludeRegex = new RegExp('(?:^|[\\/\\\\])(?:' + CONST.EXCLUDED_FILES.join('|') + ')(?:$|[\\/\\\\])'); + +/** + * Task to compile controllers/views and their styles + */ +class MvcCompileTask extends IncrementalFileTask { + /** + * Constructs a new MVC compile task. + * + * @param {Object} options Configuration object for this task + */ + constructor(options) { + options.name = options.name || 'mvc-compile'; + super(options); + + this.targetPlatform = options.targetPlatform; + this.restrictionPath = options.restrictionPath; + this.parseAlloyComponent = options.parseAlloyComponent; + + // Create a regex for determining which platform-specific + // folders should be used in the compile process + let filteredPlatforms = _.reject(CONST.PLATFORM_FOLDERS_ALLOY, p => { + return p === this.targetPlatform; + }); + filteredPlatforms = _.map(filteredPlatforms, function(p) { return p + '[\\\\\\/]'; }); + this.filterRegex = new RegExp('^(?:(?!' + filteredPlatforms.join('|') + '))'); + + this.processed = {}; + this.rawSourceCollections = []; + + if (options.sourceCollections) { + this.sourceCollections = options.sourceCollections; + } + } + + set sourceCollections(sourceCollections) { + this.inputFiles = new Set(); + this.rawSourceCollections = sourceCollections; + const candidateSourcePaths = [CONST.DIR.VIEW, CONST.DIR.CONTROLLER]; + _.each(sourceCollections, collection => { + for (const candidateSourcePath of candidateSourcePaths) { + var sourcePath = path.join(collection.dir, candidateSourcePath); + if (!fs.existsSync(sourcePath)) { + continue; + } + + _.each(walkSync(sourcePath), componentFilename => { + componentFilename = path.normalize(componentFilename); + const componentRegex = candidateSourcePath === CONST.DIR.VIEW ? viewRegex : controllerRegex; + if (componentRegex.test(componentFilename) && this.filterRegex.test(componentFilename) && !excludeRegex.test(componentFilename)) { + this.addInputFile(path.join(sourcePath, componentFilename)); + } + }); + } + }); + } + + /** + * Does a full task run, processing every input file. + * + * @return {Promise} + */ + doFullTaskRun() { + // @todo make this parallel? + for (const inputFile of this.inputFiles) { + this.compileAlloyComponent(inputFile); + } + + return Promise.resolve(); + } + + /** + * Does an incremental task run, processing only changed files. + * + * @param {Map} changedFiles Map of file paths and their current file state (created, changed, deleted) + * @return {Promise} + */ + doIncrementalTaskRun(changedFiles) { + changedFiles.forEach((fileState, filePath) => { + if (fileState === 'deleted') { + // @todo delete associated controller, view and styles + } else { + this.compileAlloyComponent(filePath); + } + }); + + return Promise.resolve(); + } + + /** + * Finds the source collection a file part of. + * + * @param {String} filePath Full path to the file for which to look up the source collection. + */ + findSourceCollection(filePath) { + for (const collection of this.rawSourceCollections) { + if (filePath.startsWith(collection.dir)) { + return collection; + } + } + + throw new Error(`Source collection not found for ${filePath}.`); + } + + /** + * Compiles an Alloy component (controller/view). + * + * Note that this only needs to be done once for either the view OR the controller. + * + * @param {*} inputFile + */ + compileAlloyComponent(inputFile) { + const collection = this.findSourceCollection(inputFile); + const parseAsController = controllerRegex.test(inputFile); + let relativeComponentPath = inputFile.replace(collection.dir, ''); + relativeComponentPath = relativeComponentPath.replace(new RegExp(`^${path.sep}?(views|controllers)${path.sep}?`), ''); + + var relativeComponentPathWithoutExtension = relativeComponentPath.substring(0, relativeComponentPath.lastIndexOf('.')); + var componentIdentifier = relativeComponentPathWithoutExtension.replace(new RegExp('^' + this.targetPlatform + '[\\/\\\\]'), ''); + var fullComponentIdentifier = path.join(collection.dir, componentIdentifier); + if (this.processed[fullComponentIdentifier]) { + return; + } + + this.logger.info('[' + relativeComponentPath + '] ' + (collection.manifest ? collection.manifest.id + + ' ' : '') + `${parseAsController ? 'controller' : 'view'} processing...`); + this.parseAlloyComponent(relativeComponentPath, collection.dir, collection.manifest, parseAsController, this.restrictionPath); + this.processed[fullComponentIdentifier] = true; + } +} + +module.exports = MvcCompileTask; diff --git a/package-lock.json b/package-lock.json index 70e45e64c..802a09094 100644 --- a/package-lock.json +++ b/package-lock.json @@ -414,6 +414,27 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "appc-tasks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/appc-tasks/-/appc-tasks-1.0.1.tgz", + "integrity": "sha1-aOG8cq/leLYloW5imalMWtMtmAY=", + "requires": { + "file-state-monitor": "^1.0.0", + "fs-extra": "^4.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, "argparse": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", @@ -1566,6 +1587,26 @@ "object-assign": "^4.0.1" } }, + "file-state-monitor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-state-monitor/-/file-state-monitor-1.0.0.tgz", + "integrity": "sha512-Tmn8VmqNW++Vyd0aMgNPR1F+56gp4GE9iY1rpM5X4YrN1yDxLVBfL2Q7qr6FCyoYyNiyOHCFidK1Mpl5t58BSA==", + "requires": { + "fs-extra": "^4.0.0" + }, + "dependencies": { + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, "filelist": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/filelist/-/filelist-0.0.6.tgz", diff --git a/package.json b/package.json index 8944296b8..f5eea27bc 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@babel/parser": "^7.4.5", "@babel/traverse": "^7.4.5", "@babel/types": "^7.4.4", + "appc-tasks": "^1.0.1", "async": "^2.4.0", "chmodr": "^1.0.2", "colors": "^1.1.2",