/** * @license Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. * see: http://github.com/jrburke/requirejs for details */ 'use strict'; var esprima = require('./esprima'), lang = require('./lang'); function arrayToString(ary) { var output = '['; if (ary) { ary.forEach(function(item, i) { output += (i > 0 ? ',' : '') + '"' + lang.jsEscape(item) + '"'; }); } output += ']'; return output; } //This string is saved off because JSLint complains //about obj.arguments use, as 'reserved word' var argPropName = 'arguments'; //From an esprima example for traversing its ast. function traverse(object, visitor) { var key, child; if (!object) { return; } if (visitor.call(null, object) === false) { return false; } for (key in object) { if (object.hasOwnProperty(key)) { child = object[key]; if (typeof child === 'object' && child !== null) { if (traverse(child, visitor) === false) { return false; } } } } } /** * Pulls out dependencies from an array literal with just string members. * If string literals, will just return those string values in an array, * skipping other items in the array. * * @param {Node} node an AST node. * * @returns {Array} an array of strings. * If null is returned, then it means the input node was not a valid * dependency. */ function getValidDeps(node) { if (!node || node.type !== 'ArrayExpression' || !node.elements) { return; } var deps = []; node.elements.some(function(elem) { if (elem.type === 'Literal') { deps.push(elem.value); } }); return deps.length ? deps : undefined; } /** * Main parse function. Returns a string of any valid require or * define/require.def calls as part of one JavaScript source string. * @param {String} moduleName the module name that represents this file. * It is used to create a default define if there is not one already for the * file. This allows properly tracing dependencies for builds. Otherwise, if * the file just has a require() call, the file dependencies will not be * properly reflected: the file will come before its dependencies. * @param {String} moduleName * @param {String} fileName * @param {String} fileContents * @param {Object} options optional options. insertNeedsDefine: true will * add calls to require.needsDefine() if appropriate. * @returns {String} JS source string or null, if no require or * define/require.def calls are found. */ function parse(moduleName, fileName, fileContents, options) { options = options || {}; //Set up source input var i, moduleCall, depString, moduleDeps = [], result = '', moduleList = [], needsDefine = true, astRoot = esprima.parse(fileContents); parse.recurse(astRoot, function(callName, config, name, deps) { if (!deps) { deps = []; } if (callName === 'define' && (!name || name === moduleName)) { needsDefine = false; } if (!name) { //If there is no module name, the dependencies are for //this file/default module name. moduleDeps = moduleDeps.concat(deps); } else { moduleList.push({ name: name, deps: deps }); } //If define was found, no need to dive deeper, unless //the config explicitly wants to dig deeper. return !!options.findNestedDependencies; }, options); if (options.insertNeedsDefine && needsDefine) { result += 'require.needsDefine("' + moduleName + '");'; } if (moduleDeps.length || moduleList.length) { for (i = 0; i < moduleList.length; i++) { moduleCall = moduleList[i]; if (result) { result += '\n'; } //If this is the main module for this file, combine any //"anonymous" dependencies (could come from a nested require //call) with this module. if (moduleCall.name === moduleName) { moduleCall.deps = moduleCall.deps.concat(moduleDeps); moduleDeps = []; } depString = arrayToString(moduleCall.deps); result += 'define("' + moduleCall.name + '",' + depString + ');'; } if (moduleDeps.length) { if (result) { result += '\n'; } depString = arrayToString(moduleDeps); result += 'define("' + moduleName + '",' + depString + ');'; } } return result || null; } /** * Handles parsing a file recursively for require calls. * @param {Array} parentNode the AST node to start with. * @param {Function} onMatch function to call on a parse match. * @param {Object} [options] This is normally the build config options if * it is passed. */ parse.recurse = function(object, onMatch, options) { //Like traverse, but skips if branches that would not be processed //after has application that results in tests of true or false boolean //literal values. var key, child, hasHas = options && options.has; if (!object) { return; } //If has replacement has resulted in if(true){} or if(false){}, take //the appropriate branch and skip the other one. if (hasHas && object.type === 'IfStatement' && object.test.type && object.test.type === 'Literal') { if (object.test.value) { //Take the if branch this.recurse(object.consequent, onMatch, options); } else { //Take the else branch this.recurse(object.alternate, onMatch, options); } } else { if (this.parseNode(object, onMatch) === false) { return; } for (key in object) { if (object.hasOwnProperty(key)) { child = object[key]; if (typeof child === 'object' && child !== null) { this.recurse(child, onMatch, options); } } } } }; /** * Determines if the file defines the require/define module API. * Specifically, it looks for the `define.amd = ` expression. * @param {String} fileName * @param {String} fileContents * @returns {Boolean} */ parse.definesRequire = function(fileName, fileContents) { var found = false; traverse(esprima.parse(fileContents), function(node) { if (parse.hasDefineAmd(node)) { found = true; //Stop traversal return false; } }); return found; }; /** * Finds require("") calls inside a CommonJS anonymous module wrapped in a * define(function(require, exports, module){}) wrapper. These dependencies * will be added to a modified define() call that lists the dependencies * on the outside of the function. * @param {String} fileName * @param {String} fileContents * @returns {Array} an array of module names that are dependencies. Always * returns an array, but could be of length zero. */ parse.getAnonDeps = function(fileName, fileContents) { var astRoot = esprima.parse(fileContents), defFunc = this.findAnonDefineFactory(astRoot); return parse.getAnonDepsFromNode(defFunc); }; /** * Finds require("") calls inside a CommonJS anonymous module wrapped * in a define function, given an AST node for the definition function. * @param {Node} node the AST node for the definition function. * @returns {Array} and array of dependency names. Can be of zero length. */ parse.getAnonDepsFromNode = function(node) { var deps = [], funcArgLength; if (node) { this.findRequireDepNames(node, deps); //If no deps, still add the standard CommonJS require, exports, //module, in that order, to the deps, but only if specified as //function args. In particular, if exports is used, it is favored //over the return value of the function, so only add it if asked. funcArgLength = node.params && node.params.length; if (funcArgLength) { deps = (funcArgLength > 1 ? ["require", "exports", "module"] : ["require"]).concat(deps); } } return deps; }; /** * Finds the function in define(function (require, exports, module){}); * @param {Array} node * @returns {Boolean} */ parse.findAnonDefineFactory = function(node) { var match; traverse(node, function(node) { var arg0, arg1; if (node && node.type === 'CallExpression' && node.callee && node.callee.type === 'Identifier' && node.callee.name === 'define' && node[argPropName]) { //Just the factory function passed to define arg0 = node[argPropName][0]; if (arg0 && arg0.type === 'FunctionExpression') { match = arg0; return false; } //A string literal module ID followed by the factory function. arg1 = node[argPropName][1]; if (arg0.type === 'Literal' && arg1 && arg1.type === 'FunctionExpression') { match = arg1; return false; } } }); return match; }; /** * Finds any config that is passed to requirejs. That includes calls to * require/requirejs.config(), as well as require({}, ...) and * requirejs({}, ...) * @param {String} fileContents * * @returns {Object} a config details object with the following properties: * - config: {Object} the config object found. Can be undefined if no * config found. * - range: {Array} the start index and end index in the contents where * the config was found. Can be undefined if no config found. * Can throw an error if the config in the file cannot be evaluated in * a build context to valid JavaScript. */ parse.findConfig = function(fileContents) { /*jslint evil: true */ var jsConfig, foundRange, foundConfig, astRoot = esprima.parse(fileContents, { range: true }); traverse(astRoot, function(node) { var arg, requireType = parse.hasRequire(node); if (requireType && (requireType === 'require' || requireType === 'requirejs' || requireType === 'requireConfig' || requireType === 'requirejsConfig')) { arg = node[argPropName] && node[argPropName][0]; if (arg && arg.type === 'ObjectExpression') { jsConfig = parse.nodeToString(fileContents, arg); foundRange = arg.range; return false; } } else { arg = parse.getRequireObjectLiteral(node); if (arg) { jsConfig = parse.nodeToString(fileContents, arg); foundRange = arg.range; return false; } else { arg = parse.getObjectLiteral(node); if (arg) { jsConfig = parse.nodeToString(fileContents, arg); foundRange = arg.range; return false; } } } }); if (jsConfig) { foundConfig = eval('(' + jsConfig + ')'); } return { config: foundConfig, range: foundRange }; }; /** Returns the node for the object literal assigned to require/requirejs, * for holding a declarative config. */ parse.getRequireObjectLiteral = function(node) { if (node.id && node.id.type === 'Identifier' && (node.id.name === 'require' || node.id.name === 'requirejs') && node.init && node.init.type === 'ObjectExpression') { return node.init; } }; /** * for holding requirejs build file */ parse.getObjectLiteral = function (node) { if (node.type && node.type === 'ExpressionStatement' && (node.expression && !node.arguments) && (node.expression.type && node.expression.type === 'ObjectExpression') && (node.expression.properties && node.expression.properties.length > 0) && (node.expression.properties[0].type && node.expression.properties[0].type === 'Property')) return node; }; /** * Renames require/requirejs/define calls to be ns + '.' + require/requirejs/define * Does *not* do .config calls though. See pragma.namespace for the complete * set of namespace transforms. This function is used because require calls * inside a define() call should not be renamed, so a simple regexp is not * good enough. * @param {String} fileContents the contents to transform. * @param {String} ns the namespace, *not* including trailing dot. * @return {String} the fileContents with the namespace applied */ parse.renameNamespace = function(fileContents, ns) { var ranges = [], astRoot = esprima.parse(fileContents, { range: true }); parse.recurse(astRoot, function(callName, config, name, deps, node) { ranges.push(node.range); //Do not recurse into define functions, they should be using //local defines. return callName !== 'define'; }, {}); //Go backwards through the found ranges, adding in the namespace name //in front. ranges.reverse(); ranges.forEach(function(range) { fileContents = fileContents.substring(0, range[0]) + ns + '.' + fileContents.substring(range[0]); }); return fileContents; }; /** * Finds all dependencies specified in dependency arrays and inside * simplified commonjs wrappers. * @param {String} fileName * @param {String} fileContents * * @returns {Array} an array of dependency strings. The dependencies * have not been normalized, they may be relative IDs. */ parse.findDependencies = function(fileName, fileContents, options) { var dependencies = [], astRoot = esprima.parse(fileContents); parse.recurse(astRoot, function(callName, config, name, deps) { if (deps) { dependencies = dependencies.concat(deps); } }, options); return dependencies; }; /** * Finds only CJS dependencies, ones that are the form * require('stringLiteral') */ parse.findCjsDependencies = function(fileName, fileContents) { var dependencies = []; traverse(esprima.parse(fileContents), function(node) { var arg; if (node && node.type === 'CallExpression' && node.callee && node.callee.type === 'Identifier' && node.callee.name === 'require' && node[argPropName] && node[argPropName].length === 1) { arg = node[argPropName][0]; if (arg.type === 'Literal') { dependencies.push(arg.value); } } }); return dependencies; }; //function define() {} parse.hasDefDefine = function(node) { return node.type === 'FunctionDeclaration' && node.id && node.id.type === 'Identifier' && node.id.name === 'define'; }; //define.amd = ... parse.hasDefineAmd = function(node) { return node && node.type === 'AssignmentExpression' && node.left && node.left.type === 'MemberExpression' && node.left.object && node.left.object.name === 'define' && node.left.property && node.left.property.name === 'amd'; }; //require(), requirejs(), require.config() and requirejs.config() parse.hasRequire = function(node) { var callName, c = node && node.callee; if (node && node.type === 'CallExpression' && c) { if (c.type === 'Identifier' && (c.name === 'require' || c.name === 'requirejs')) { //A require/requirejs({}, ...) call callName = c.name; } else if (c.type === 'MemberExpression' && c.object && c.object.type === 'Identifier' && (c.object.name === 'require' || c.object.name === 'requirejs') && c.property && c.property.name === 'config') { // require/requirejs.config({}) call callName = c.object.name + 'Config'; } } return callName; }; //define() parse.hasDefine = function(node) { return node && node.type === 'CallExpression' && node.callee && node.callee.type === 'Identifier' && node.callee.name === 'define'; }; /** * If there is a named define in the file, returns the name. Does not * scan for mulitple names, just the first one. */ parse.getNamedDefine = function(fileContents) { var name; traverse(esprima.parse(fileContents), function(node) { if (node && node.type === 'CallExpression' && node.callee && node.callee.type === 'Identifier' && node.callee.name === 'define' && node[argPropName] && node[argPropName][0] && node[argPropName][0].type === 'Literal') { name = node[argPropName][0].value; return false; } }); return name; }; /** * Determines if define(), require({}|[]) or requirejs was called in the * file. Also finds out if define() is declared and if define.amd is called. */ parse.usesAmdOrRequireJs = function(fileName, fileContents) { var uses; traverse(esprima.parse(fileContents), function(node) { var type, callName, arg; if (parse.hasDefDefine(node)) { //function define() {} type = 'declaresDefine'; } else if (parse.hasDefineAmd(node)) { type = 'defineAmd'; } else { callName = parse.hasRequire(node); if (callName) { arg = node[argPropName] && node[argPropName][0]; if (arg && (arg.type === 'ObjectExpression' || arg.type === 'ArrayExpression')) { type = callName; } } else if (parse.hasDefine(node)) { type = 'define'; } } if (type) { if (!uses) { uses = {}; } uses[type] = true; } }); return uses; }; /** * Determines if require(''), exports.x =, module.exports =, * __dirname, __filename are used. So, not strictly traditional CommonJS, * also checks for Node variants. */ parse.usesCommonJs = function(fileName, fileContents) { var uses = null, assignsExports = false; traverse(esprima.parse(fileContents), function(node) { var type, exp = node.expression; if (node.type === 'Identifier' && (node.name === '__dirname' || node.name === '__filename')) { type = node.name.substring(2); } else if (node.type === 'VariableDeclarator' && node.id && node.id.type === 'Identifier' && node.id.name === 'exports') { //Hmm, a variable assignment for exports, so does not use cjs //exports. type = 'varExports'; } else if (exp && exp.type === 'AssignmentExpression' && exp.left && exp.left.type === 'MemberExpression' && exp.left.object) { if (exp.left.object.name === 'module' && exp.left.property && exp.left.property.name === 'exports') { type = 'moduleExports'; } else if (exp.left.object.name === 'exports' && exp.left.property) { type = 'exports'; } } else if (node && node.type === 'CallExpression' && node.callee && node.callee.type === 'Identifier' && node.callee.name === 'require' && node[argPropName] && node[argPropName].length === 1 && node[argPropName][0].type === 'Literal') { type = 'require'; } if (type) { if (type === 'varExports') { assignsExports = true; } else if (type !== 'exports' || !assignsExports) { if (!uses) { uses = {}; } uses[type] = true; } } }); return uses; }; parse.findRequireDepNames = function(node, deps) { traverse(node, function(node) { var arg; if (node && node.type === 'CallExpression' && node.callee && node.callee.type === 'Identifier' && node.callee.name === 'require' && node[argPropName] && node[argPropName].length === 1) { arg = node[argPropName][0]; if (arg.type === 'Literal') { deps.push(arg.value); } } }); }; /** * Determines if a specific node is a valid require or define/require.def * call. * @param {Array} node * @param {Function} onMatch a function to call when a match is found. * It is passed the match name, and the config, name, deps possible args. * The config, name and deps args are not normalized. * * @returns {String} a JS source string with the valid require/define call. * Otherwise null. */ parse.parseNode = function(node, onMatch) { var name, deps, cjsDeps, arg, factory, args = node && node[argPropName], callName = parse.hasRequire(node); if (callName === 'require' || callName === 'requirejs') { //A plain require/requirejs call arg = node[argPropName] && node[argPropName][0]; if (arg.type !== 'ArrayExpression') { if (arg.type === 'ObjectExpression') { //A config call, try the second arg. arg = node[argPropName][1]; } } deps = getValidDeps(arg); if (!deps) { return; } return onMatch("require", null, null, deps, node); } else if (parse.hasDefine(node) && args && args.length) { name = args[0]; deps = args[1]; factory = args[2]; if (name.type === 'ArrayExpression') { //No name, adjust args factory = deps; deps = name; name = null; } else if (name.type === 'FunctionExpression') { //Just the factory, no name or deps factory = name; name = deps = null; } else if (name.type !== 'Literal') { //An object literal, just null out name = deps = factory = null; } if (name && name.type === 'Literal' && deps) { if (deps.type === 'FunctionExpression') { //deps is the factory factory = deps; deps = null; } else if (deps.type === 'ObjectExpression') { //deps is object literal, null out deps = factory = null; } } if (deps && deps.type === 'ArrayExpression') { deps = getValidDeps(deps); } else if (factory && factory.type === 'FunctionExpression') { //If no deps and a factory function, could be a commonjs sugar //wrapper, scan the function for dependencies. cjsDeps = parse.getAnonDepsFromNode(factory); if (cjsDeps.length) { deps = cjsDeps; } } else if (deps || factory) { //Does not match the shape of an AMD call. return; } //Just save off the name as a string instead of an AST object. if (name && name.type === 'Literal') { name = name.value; } return onMatch("define", null, name, deps, node); } }; /** * Converts an AST node into a JS source string by extracting * the node's location from the given contents string. Assumes * esprima.parse() with ranges was done. * @param {String} contents * @param {Object} node * @returns {String} a JS source string. */ parse.nodeToString = function(contents, node) { var range = node.range; return contents.substring(range[0], range[1]); }; /** * Extracts license comments from JS text. * @param {String} fileName * @param {String} contents * @returns {String} a string of license comments. */ parse.getLicenseComments = function(fileName, contents) { var commentNode, refNode, subNode, value, i, j, ast = esprima.parse(contents, { comment: true }), result = '', existsMap = {}, lineEnd = contents.indexOf('\r') === -1 ? '\n' : '\r\n'; if (ast.comments) { for (i = 0; i < ast.comments.length; i++) { commentNode = ast.comments[i]; if (commentNode.type === 'Line') { value = '//' + commentNode.value + lineEnd; refNode = commentNode; if (i + 1 >= ast.comments.length) { value += lineEnd; } else { //Look for immediately adjacent single line comments //since it could from a multiple line comment made out //of single line comments. Like this comment. for (j = i + 1; j < ast.comments.length; j++) { subNode = ast.comments[j]; if (subNode.type === 'Line' && subNode.range[0] === refNode.range[1]) { //Adjacent single line comment. Collect it. value += '//' + subNode.value + lineEnd; refNode = subNode; } else { //No more single line comment blocks. Break out //and continue outer looping. break; } } value += lineEnd; i = j - 1; } } else { value = '/*' + commentNode.value + '*/' + lineEnd + lineEnd; } if (!existsMap[value] && (value.indexOf('license') !== -1 || (commentNode.type === 'Block' && value.indexOf('/*!') === 0) || value.indexOf('opyright') !== -1 || value.indexOf('(c)') !== -1)) { result += value; existsMap[value] = true; } } } return result; }; module.exports = parse;