UNPKG

@openveo/api

Version:
539 lines (462 loc) 18.2 kB
'use strict'; /** * @module angularJs/parser */ var fs = require('fs'); var path = require('path'); var async = require('async'); var esprima = require('esprima'); var htmlMinifier = require('html-minifier-terser'); var ConfigExpression = process.requireApi('lib/angularJs/expressions/ConfigExpression.js'); var ElementExpression = process.requireApi('lib/angularJs/expressions/ElementExpression.js'); var expressionFactory = process.requireApi('lib/angularJs/expressions/expressionFactory.js'); var FilterExpression = process.requireApi('lib/angularJs/expressions/FilterExpression.js'); var InjectExpression = process.requireApi('lib/angularJs/expressions/InjectExpression.js'); var RouteExpression = process.requireApi('lib/angularJs/expressions/RouteExpression.js'); var fileSystem = process.requireApi('lib/fileSystem.js'); var utilApi = process.requireApi('lib/util.js'); /** * Fetches a script from a list of scripts. * * @method findScript * @param {Array} scripts The list of scripts * @param {String} property The script property used to identify the script to fetch * @param {String} value The expected value of the script property * @return {(Object|null)} The found script or null if not found * @private */ function findScript(scripts, property, value) { for (var i = 0; i < scripts.length; i++) { if ((Object.prototype.toString.call(scripts[i][property]) === '[object Array]' && scripts[i][property].indexOf(value) > -1) || (scripts[i][property] === value) ) { return scripts[i]; } } return null; } /** * Browses a flat list of scripts to find a script longest dependency chains. * * Each script may have several dependencies, each dependency can also have several dependencies. * findLongestDependencyChains helps find the longest dependency chain of one of the script. * As the script may have several longest dependency chain, a list of chains is returned. * * A chain is an ordered list of script paths. * * This is recursive. * * @method findLongestDependencyChains * @param {Array} scripts The flat list of scripts with for each script: * @param {Array} scripts[].dependencies The list of dependency names of the script * @param {Array} scripts[].definitions The list of definition names of the script * @param {String} scripts[].path The script path * @param {Object} [script] The script to analyze (default to the first one of the list of scripts) * @param {Array} [modulesToIgnore] The list of module names to ignore to avoid circular dependencies * @return {Array} The longest dependency chains * @private */ function findLongestDependencyChains(scripts, script, modulesToIgnore) { var chains = []; if (!script) script = scripts[0]; // Avoid circular dependencies if (modulesToIgnore && script.module && modulesToIgnore.indexOf(script.module) !== -1) return chains; // Get script dependencies if (script.dependencies && script.dependencies.length) { var longestChainLength; // Find dependency chains of the script script.dependencies.forEach(function(dependency) { var definitionScript = findScript(scripts, 'definitions', dependency); if (definitionScript) chains = chains.concat(findLongestDependencyChains(scripts, definitionScript, script.definitions)); }); if (chains.length > 0) { // Keep the longest chain(s) chains.sort(function(chain1, chain2) { // -1 : chain1 before chain2 // 0 : nothing change // 1 : chain1 after chain2 if (chain1.length > chain2.length) return -1; else if (chain1.length < chain2.length) return 1; else return 0; }); longestChainLength = chains[0].length; chains = chains.filter(function(chain) { if (chain.length === longestChainLength) { chain.push(script.path); return true; } return false; }); return chains; } } chains.push([script.path]); return chains; } /** * Retrieves CSS and JS files from tree of scripts in a flattened order. * * This is recursive. * * @method getTreeResources * @param {Object} node The node from where to start * @param {String} node.path The file path * @param {Array} node.styles The list of css / scss file paths * @return {module:angularJs/parser~getTreeResourcesReturnValue} The list of files by types * @private */ function getTreeResources(node) { var resources = {childrenCss: [], childrenJs: [], subChildrenCss: [], subChildrenJs: []}; // Add css and js of node children then dedupe if (node.children) { node.children.forEach(function(subNode) { var subResources = getTreeResources(subNode); resources.childrenJs = utilApi.joinArray(resources.childrenJs, [subNode.path]); resources.childrenCss = utilApi.joinArray(resources.childrenCss, subNode.styles); resources.subChildrenCss = utilApi.joinArray(resources.subChildrenCss, subResources.childrenCss); resources.subChildrenJs = utilApi.joinArray(resources.subChildrenJs, subResources.childrenJs); resources.subChildrenCss = utilApi.joinArray(resources.subChildrenCss, subResources.subChildrenCss); resources.subChildrenJs = utilApi.joinArray(resources.subChildrenJs, subResources.subChildrenJs); }); } else { // Add current node css and js then dedupe resources.childrenCss = utilApi.joinArray(resources.childrenCss, node.styles); resources.childrenJs = utilApi.joinArray(resources.childrenJs, [node.path]); } return resources; } /** * Builds the dependencies tree. * * @method buildTree * @static * @param {Array} scripts The flat list of scripts with for each script: * @param {Array} dependencies The list of dependency names of the script * @param {Array} definitions The list of definition names of the script * @param {String} path The script path * @return {Array} The list of scripts with their dependencies */ module.exports.buildTree = function(scripts) { var chains = []; var tree = { children: [] }; var currentTreeNode = tree; // Get the longest dependency chain for each script with the highest dependency // as the first element of the chain scripts.forEach(function(script) { chains = chains.concat(findLongestDependencyChains(scripts, script)); }); // Sort chains by length with longest chains first chains.sort(function(chain1, chain2) { // -1 : chain1 before chain2 // 0 : nothing change // 1 : chain1 after chain2 if (chain1.length > chain2.length) return -1; else if (chain1.length < chain2.length) return 1; else return 0; }); // Add each chain to the tree chains.forEach(function(chain) { // Add each element of the chain as a child of its parent chain.forEach(function(scriptPath) { var currentScript = findScript(scripts, 'path', scriptPath); var alreadyExists = false; if (!currentTreeNode.children) currentTreeNode.children = []; // Check if current script does not exist in node children for (var i = 0; i < currentTreeNode.children.length; i++) { if (currentTreeNode.children[i].path === currentScript.path) { alreadyExists = true; break; } } // Add script to the tree if (!alreadyExists) currentTreeNode.children.push(currentScript); currentTreeNode = currentScript; }); currentTreeNode = tree; }); return tree; }; /** * Finds out AngularJS definitions and dependencies for the given content. * * This is recursive. * * The following JavaScript expressions are used to identify definitions: * - angular.module('moduleName', []) * - angular.module('moduleName').component() * - angular.module('moduleName').directive() * - angular.module('moduleName').controller() * - angular.module('moduleName').factory() * - angular.module('moduleName').service() * - angular.module('moduleName').constant() * - angular.module('moduleName').service() * - angular.module('moduleName').decorator() * - angular.module('moduleName').filter() * - angular.module('moduleName').config() * - angular.module('moduleName').run() * * The following JavaScript expressions are used to identify dependencies: * - MyAngularJsElement.$inject = ['Dependency1', 'Dependency2']; * - angular.module('moduleName', ['DependencyModule']) * * The following JavaScript expressions are used to identify associated modules: * - angular.module('moduleName') * * @method findDependencies * @static * @param {Object} expression The JavaScript expression to analyze */ module.exports.findDependencies = function(jsExpression) { var self = this; var expression; var results = { definitions: [], dependencies: [], module: null }; if (!jsExpression) return results; /** * Merges results from sub expressions into results for the current expression. * * @param {Object} newResults Sub expressions results * @param {Array} [newResults.definitions] The list of definitions in sub expression * @param {Array} [newResults.dependencies] The list of dependencies in sub expression * @param {String} [newResults.module] The name of the module the definitions belong to */ function mergeResults(newResults) { if (newResults.definitions) results.definitions = utilApi.joinArray(results.definitions, newResults.definitions); if (newResults.dependencies) results.dependencies = utilApi.joinArray(results.dependencies, newResults.dependencies); if (newResults.module) results.module = newResults.module; } if (jsExpression.type === 'CallExpression' && jsExpression.callee.type === 'MemberExpression') { if (Object.values(ElementExpression.ELEMENTS).indexOf(jsExpression.callee.property.name) > -1) { expression = expressionFactory.getElementExpression(jsExpression.callee.property.name, jsExpression); if (expression.isValid()) { var newResults = { definitions: expression.isDefinition() ? [expression.getName()] : [], dependencies: expression.getDependencies() }; if (!expression.isDefinition() && expression.getElementType() === ElementExpression.ELEMENTS.MODULE) newResults.module = expression.getName(); mergeResults(newResults); } } else if (jsExpression.callee.property.name === 'config' || jsExpression.callee.property.name === 'run') { expression = new ConfigExpression(jsExpression); if (expression.isValid()) { mergeResults({ dependencies: expression.getDependencies() }); } } else if (jsExpression.callee.property.name === 'when') { expression = new RouteExpression(jsExpression); if (expression.isValid()) { mergeResults({ definitions: expression.getDefinitions(), dependencies: expression.getDependencies() }); } } } if (jsExpression.type === 'AssignmentExpression' && jsExpression.left.property && jsExpression.left.property.name === '$inject' ) { expression = new InjectExpression(jsExpression); if (expression.isValid()) { mergeResults({ dependencies: expression.getDependencies() }); } } if (jsExpression.type === 'CallExpression' && jsExpression.callee.name === '$filter') { expression = new FilterExpression(jsExpression); if (expression.isValid()) { mergeResults({ dependencies: [expression.getDependency()] }); } } if (Object.prototype.toString.call(jsExpression) === '[object Object]') { for (var property in jsExpression) mergeResults(self.findDependencies(jsExpression[property])); } else if (Object.prototype.toString.call(jsExpression) === '[object Array]') { jsExpression.forEach(function(value) { mergeResults(self.findDependencies(value)); }); } return results; }; /** * Generates an AngularJS file with all given templates directly loaded into $templateCache. * * @method generateTemplatesCache * @static * @param {Array} templatesPath The list of templates files paths to add to cache * @param {String} outputPath The path of the resulting AngularJS file * @param {String} moduleName The AngularJS module to use to load templates into $templateCache * @param {String} [prefix] A prefix to apply to each template name * @param {callback} callback Function to call when its done */ module.exports.generateTemplatesCache = function(templatesPaths, outputPath, moduleName, prefix, callback) { if (!moduleName) return callback(new TypeError('moduleName should be a string')); var script = ' \'use strict\';\n'; var minifyFunctions = []; var minify = function(templatePath) { return function(callback) { fs.readFile(templatePath, function(readError, templateContent) { if (readError) return callback(readError); var minified = false; htmlMinifier.minify(templateContent.toString(), { collapseBooleanAttributes: true, collapseWhitespace: true, removeComments: true, removeEmptyAttributes: true, removeRedundantAttributes: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true }).then(function(minifiedTemplate) { minified = true; var parsedPath = path.parse(templatePath); callback(null, { name: (prefix ? prefix : '') + parsedPath.name + parsedPath.ext, template: minifiedTemplate }); }).catch(function(minifyError) { if (!minified) callback(minifyError); }); }); }; }; var escapeTemplate = function(template) { return template.split(/^/gm).map(function(line) { var quote = '\''; line = line.replace(/\\/g, '\\\\'); line = line.replace(/\n/g, '\\n'); line = line.replace(/\r/g, '\\r'); var quoteRegExp = new RegExp(quote, 'g'); line = line.replace(quoteRegExp, '\\' + quote); return quote + line + quote; }).join(' +\n ') || '""'; }; for (var templatePath of templatesPaths) { minifyFunctions.push(minify(templatePath)); } async.parallel(minifyFunctions, function(error, minifiedTemplates) { if (error) return callback(error); for (var minifiedTemplate of minifiedTemplates) { script += '\n $templateCache.put(\'' + minifiedTemplate.name + '\',\n ' + escapeTemplate(minifiedTemplate.template) + '\n );\n'; } fileSystem.mkdir(path.dirname(outputPath), function(mkdirError) { if (mkdirError) return callback(mkdirError); fs.writeFile( outputPath, 'angular.module(\'' + moduleName + '\')' + '.run([\'$templateCache\', function($templateCache) {\n' + script + '\n}]);\n', callback ); }); }); }; /** * Retrieves CSS and JS files from tree of scripts in a flattened order. * * @method getResources * @static * @param {Object} tree The tree of resources * @return {module:angularJs/parser~getResourcesReturnValue} The list of files by types */ module.exports.getResources = function(tree) { var resources = getTreeResources(tree); return { css: utilApi.joinArray(resources.childrenCss, resources.subChildrenCss), js: utilApi.joinArray(resources.childrenJs, resources.subChildrenJs) }; }; /** * Orders a list of components JavaScript and SCSS files. * * JavaScript and SCSS files are ordered in the way they should be laoded. * * @param {Array} sourcesFilesPaths The list of JavaScript and SCSS sources to order * @return {module:angularJs/parser~orderSourcesCallback} Function to call when its done */ module.exports.orderSources = function(sourcesFilesPaths, callback) { var self = this; var analyzeAsyncFunctions = []; var scripts = []; var styles = []; sourcesFilesPaths.forEach(function(sourceFilePath) { var pathDescriptor = path.parse(sourceFilePath); if (pathDescriptor.ext === '.js') { // JavaScript files analyzeAsyncFunctions.push(function(callback) { fs.readFile(sourceFilePath, function(error, content) { var contentString = content.toString(); // Try to find parents of the source var programExpressions = esprima.parseScript(contentString); var script = self.findDependencies(programExpressions); script.path = sourceFilePath; script.styles = []; scripts.push(script); callback(error); }); }); } else if (pathDescriptor.ext === '.css' || pathDescriptor.ext === '.scss' ) { // CSS / SCSS files styles.push(sourceFilePath); } }); async.parallel(analyzeAsyncFunctions, function(error) { if (error) return callback(error); // Associate css files with scripts styles.forEach(function(style) { for (var i = 0; i < scripts.length; i++) { var originalScriptPath = scripts[i].path; var originalStylePath = style; if (path.dirname(originalScriptPath) === path.dirname(originalStylePath)) { scripts[i].styles.push(style); return; } } }); callback(null, self.getResources(self.buildTree(scripts))); }); }; /** * @typedef {Object} module:angularJs/parser~getTreeResourcesReturnValue * @property {Array} childrenCss The list of children CSS files * @property {Array} childrenJs The list of children CSS files * @property {Array} subChildrenCss The list of sub children CSS files * @property {Array} subChildrenJs The list of sub children JS files */ /** * @typedef {Object} module:angularJs/parser~getResourcesReturnValue * @property {Array} css The list of css files in the right order * @property {Array} js The list of js files in the right order */ /** * @typedef {Object} module:angularJs/parser~orderSourcesCallback * @param {(Error|null)} error The error if an error occurred, null otherwise * @param {(Object|Undefined)} sources JavaScript and SCSS sources * @param {Array} sources.js JavaScript sources * @param {Array} sources.css SCSS sources */