UNPKG

liferay-module-config-generator

Version:
586 lines (473 loc) 19 kB
'use strict'; var beautify = require('js-beautify').js_beautify; var fs = require('fs'); var jsstana = require('jsstana'); var minimatch = require('minimatch'); var path = require('path'); var Promise = require('bluebird'); var recast = require('recast'); var sourceMap = require('source-map'); var upath = require('upath'); var walk = require('walk'); Promise.promisifyAll(fs); var builders = recast.types.builders; var REGEX_SOURCEMAP = /\/\/#\s+sourceMappingURL\s*=(.+)/; /** * ConfigGenerator implementation. * @class ConfigGenerator * @param {Object} options Configuration options */ function ConfigGenerator(options) { this._options = options; } ConfigGenerator.prototype = { constructor: ConfigGenerator, /** * Processes the passed files or folders and generates config file. * * @method process * @return {Promise} Returns a Promise which will be resolved with the generated config file */ process: function() { var self = this; self._modules = []; return new Promise(function(resolve, reject) { var base; var processors = []; if (self._options.base) { base = fs.readFileSync(path.resolve(self._options.base), 'utf8'); } // For every file or folder, create a promise, // parse the file, extract the config and store it // to the global modules array. // Once all files are being processed, store the generated config. for (var i = 0; i < self._options.args.length; i++) { var file = self._options.args[i]; var fileStats = fs.statSync(file); if (fileStats.isDirectory(file)) { var walker = walk.walk(file, { followLinks: false }); walker.on('file', self._onWalkerFile.bind(self)); processors.push(self._onWalkerEnd(walker)); } else if (fileStats.isFile()) { processors.push(self._processFile(file)); } } Promise.all(processors) .then(function(uselessPromises) { return self._generateConfig(); }) .then(function(config) { var content; if (self._options.config) { if (base) { content = base + self._options.config + '.modules = ' + JSON.stringify(config) + ';'; } else { content = 'var ' + self._options.config + ' = {modules: ' + JSON.stringify(config) + '};'; } } else { content = JSON.stringify(config); } return self._saveConfig(beautify(content)); }) .then(function(config) { resolve(config); }) .catch(function(error) { reject(error); }); }); }, /** * Extracts conditions from a module configuration. * * @method _extractCondition * @protected * @param {Object} ast AST to be processed * @return {Object} The extracted values for the conditional options */ _extractCondition: function(ast) { var self = this; var found; var meta = ast; var values = {}; jsstana.traverse(ast, function(node) { if (!found) { var match = jsstana.match('(ident META)', node); if (match) { jsstana.traverse(meta, function(node) { if (!found) { match = jsstana.match('(return)', node) || jsstana.match('(object)', node); if (match) { values = self._extractObjectValues(['path', 'fullPath', 'condition', 'group'], node); found = true; } } }); } else { meta = node; } } }); return values; }, /** * Extract values for some idents from an AST. * * @method _extractObjectValues * @param {Array} idents The idents which values should be looked up in the AST * @param {AST} ast The AST to be processed * @return {Object} An object with the extracted values for all found idents */ _extractObjectValues: function(idents, ast) { var self = this; var result = Object.create(null); var found; var ident; if (ast) { jsstana.traverse(ast, function(node) { if (found) { found = false; result[ident] = self._extractValue(node); } for (var i = 0; i < idents.length; i++) { ident = idents[i]; if (jsstana.match('(ident ' + ident + ')', node)) { found = true; break; } } }); } return result; }, /** * Extracts the value from a jsstana node. The value may be * Literal, ObjectExpression, ArrayExpression or FunctionExpression. * * @method _extractValue * @protected * @param {Object} node jsstana node which should be processed * @return {String} The extracted value from the node */ _extractValue: function(node) { var self = this; var i; if (node.type === 'Literal') { return node.value; } else if (node.type === 'ObjectExpression') { var obj = {}; for (i = 0; i < node.properties.length; i++) { var property = node.properties[i]; obj[property.key.name] = self._extractValue(property.value); } return obj; } else if (node.type === 'ArrayExpression') { var arr = []; for (i = 0; i < node.elements.length; i++) { arr.push(self._extractValue(node.elements[i])); } return arr; } else if (node.type === 'FunctionExpression') { return recast.print(node, { wrapColumn: Number.Infinity }).code; } }, /** * Generates a config object from all found modules. * * @method _generateConfig * @protected * @return {Promise} Returns a Promise which will be resolved with the generated configuration */ _generateConfig: function() { var self = this; return new Promise(function(resolve, reject) { var config = {}; for (var i = 0; i < self._modules.length; i++) { var module = self._modules[i]; var storedModule = config[module.name] = { dependencies: module.dependencies }; if (module.condition) { storedModule.condition = module.condition; } if (!self._options.ignorePath) { if (module.fullPath) { storedModule.fullPath = upath.toUnix(module.fullPath); } else { var dirname = path.dirname(module.name); var modulePath = module.path || (dirname !== '.' ? dirname + '/' + module.file : module.file); storedModule.path = upath.toUnix(modulePath); } } } resolve(config); }); }, /** * Generates a module name in case it is not present in the AMD definition. * * @method _generateModuleName * @protected * @param {String} file The file path to be processed and module name to be generated * @return {String} The generated module name */ _generateModuleName: function(file) { var self = this; var ext; if (!self._options.keepExtension) { ext = self._options.extension || path.extname(file); } var fileName = path.basename(file, ext); if (self._options.format) { var formatRegex = self._options.format[0].split('/'); formatRegex = new RegExp(formatRegex[1], formatRegex[2]); var replaceValue = self._options.format[1]; fileName = fileName.replace(formatRegex, replaceValue); } var moduleConfig = { name: '', version: '1.0.0' }; if (self._options.moduleConfig) { if (typeof self._options.moduleConfig === 'string') { var fileModuleConfig = path.resolve(self._options.moduleConfig); if (fs.existsSync(fileModuleConfig)) { moduleConfig = require(fileModuleConfig); } } else { moduleConfig = self._options.moduleConfig; } } fileName = path.join(path.dirname(file), fileName); var moduleName = ''; if (moduleConfig.name) { moduleName = moduleConfig.name + '@' + moduleConfig.version; } moduleName = path.join(moduleName, fileName.substring(self._options.moduleRoot.length)); if (self._options.lowerCase) { moduleName = moduleName.toLowerCase(); } return upath.toUnix(moduleName); }, /** * Retrieves the generated configuration for all found modules. * * @method _getConfig * @protected * @param {String} file The file which should be processed * @param {Object} ast The parsed AST of the file, which should be processed * @return {Promise} Returns a Promise which will be resolved with the generated configuration */ _getConfig: function(file, ast) { var self = this; return new Promise(function(resolve, reject) { var result = []; var saveFile = false; jsstana.traverse(ast, function(node) { var defineMatch = jsstana.match('(or (call define ?) (call define ? ?) (call define ? ? ?))', node); if (defineMatch) { if (self._options.namespace) { node.callee.name = self._options.namespace + '.' + node.callee.name; saveFile = true; } var dependencies; var moduleName; // If the module does not have a module id, nor dependencies, generate them if (node.arguments.length === 1) { moduleName = self._generateModuleName(file); dependencies = builders.arrayExpression( [builders.literal('module'), builders.literal('exports')] ); // Add the dependencies node.arguments.unshift(dependencies); // Add the module name node.arguments.unshift(builders.literal(moduleName)); saveFile = true; } else if (node.arguments.length === 2) { // If the first argument is the dependencies, just generate the module name if (node.arguments[0].type === 'ArrayExpression') { moduleName = self._generateModuleName(file); dependencies = node.arguments[0]; // Add the module name. node.arguments.unshift(builders.literal(moduleName)); } else { // The first argument is a module name and the second one is the implementation. // In this case we have to generate the dependencies and keep the module name. moduleName = node.arguments[0].value; dependencies = builders.arrayExpression( [builders.literal('module'), builders.literal('exports')] ); // Add the dependencies as a second argument and keep the module name as the first one. node.arguments.splice(1, 0, dependencies); } saveFile = true; } else { moduleName = node.arguments[0].value; dependencies = node.arguments[1]; } var config = { file: path.basename(file), name: moduleName, dependencies: self._extractValue(dependencies) }; var values = self._extractCondition(node); Object.keys(values || {}).forEach(function(key) { config[key] = values[key]; }); result.push(config); } if (self._options.namespace) { var requireMatch = jsstana.match('(or (call require ?) (call require ? ?) (call require ? ? ?))', node); if (requireMatch) { node.callee.name = self._options.namespace + '.' + node.callee.name; saveFile = true; } } }); if (saveFile && !self._options.skipFileOverride) { self._saveFile(file, ast); } resolve(result); }); }, /** * Listener which will be invoked when a file whiting the provided folder is found * * @method _onWalkerFile * @protected * @param {String} root The root directory of the file * @param {Object} fileStats Object with data about the file * @param {Function} next A callback function to be called once the file is processed */ _onWalkerFile: function(root, fileStats, next) { var self = this; var file = path.join(root, fileStats.name); if (minimatch(file, self._options.filePattern, { dot: true })) { self._processFile(file) .then(function(config) { next(); }); } else { next(); } }, /** * Listener which will be invoked once the walker processes all files in the provided directory. * * @method _onWalkerEnd * @param {Object} walker The walker object * @return {Promise} Returns a Promise which will be resolved with the root folder, file data and a "next" callback */ _onWalkerEnd: function(walker) { return new Promise(function(resolve, reject) { walker.on('end', resolve); }); }, /** * Parses the content of a file * * @method _parseFile * @protected * @param {String} file The file which should be parsed * @param {String} content The content of the file which should be parsed * @return {Promise} Returns a Promise which will be resolved with file's AST */ _parseFile: function(file, content) { return new Promise(function(resolve, reject) { var ast = recast.parse(content); resolve(ast); }); }, /** * Processes a file and generates configuration object for all modules found inside. * * @method _processFile * @protected * @param {String} file The file which should be processed * @return {Promise} Returns a Promise which will be resolved with the generated config */ _processFile: function(file) { var self = this; return new Promise(function(resolve) { fs.readFileAsync(file, 'utf-8') .then(function(content) { return self._parseFile(file, content); }) .then(function(ast) { return self._getConfig(file, ast); }) .then(function(config) { self._modules = self._modules.concat(config); resolve(config); }); }); }, /** * Saves file with the reprinted AST and updated source map, if any. * * @method _saveFile * @protected * @param {String} file The file to save * @param {Object} ast The AST of the file */ _saveFile: function(file, ast) { var content = recast.print(ast, { wrapColumn: Number.Infinity }).code; content = this._updateSourceMap(file, content); fs.writeFileSync(file, content); }, /** * Saves the generated configuration file on the hard drive. * * @method _saveConfig * @protected * @param {Object} config The configuration object to be saved * @return {Promise} Returns a Promise which will be resolved with the generated config file */ _saveConfig: function(config) { var self = this; return new Promise(function(resolve, reject) { if (self._options.output) { fs.writeFileAsync(self._options.output, config) .then(function() { resolve(config); }); } else { resolve(config); } }); }, /** * Updates the source and source map of the processed file. * * @method _updateSourceMap * @protected * @param {String} file The name of the processed file * @param {String} content The content of the processed file * @return {String} The modified content after updating the source map */ _updateSourceMap: function(file, content) { var sourceMapURLMatch = REGEX_SOURCEMAP.exec(content); if (sourceMapURLMatch) { var sourceMapURL = sourceMapURLMatch[1]; if (sourceMapURL) { var sourceMapContent = fs.readFileSync(path.resolve(path.dirname(file), sourceMapURL), 'utf-8'); var consumer = new sourceMap.SourceMapConsumer(sourceMapContent); var node = sourceMap.SourceNode.fromStringWithSourceMap(content, consumer); var result = node.toStringWithSourceMap(); content = result.code; var map = result.map; fs.writeFileSync(path.resolve(path.dirname(file), sourceMapURL), JSON.stringify(map)); } } return content; } }; module.exports = ConfigGenerator;