├── .gitignore ├── example └── detect.js ├── package.json ├── index.js ├── LICENSE.md ├── readme.md ├── test └── test.js └── lib ├── lang.js └── parse.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | -------------------------------------------------------------------------------- /example/detect.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | amdetective = require('./index.js'); 3 | 4 | console.log('Reading file from first argument: ' + process.argv[2]); 5 | console.log(amdetective(fs.readFileSync(process.argv[2]).toString())); 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amdetective", 3 | "version": "0.3.0", 4 | "description": "Like node-detective, but for AMD/r.js files. Finds all calls to `require()` in AMD modules by walking the AST.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha -R spec ./test/test.js" 8 | }, 9 | "keywords": [ 10 | "require", 11 | "source", 12 | "analyze", 13 | "ast", 14 | "amd", 15 | "detective" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/mixu/amdetective.git" 20 | }, 21 | "author": "Mikito Takada (http://mixu.net/)", 22 | "license": "BSD-3-Clause", 23 | "dependencies": { 24 | "esprima": "3.1.0" 25 | }, 26 | "devDependencies": { 27 | "mocha": "^3.1.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var esprima = require('esprima'), 2 | parse = require('./lib/parse.js'); 3 | 4 | function find(fileContents, options) { 5 | options = options || {}; 6 | 7 | //Set up source input 8 | var moduleDeps = [], 9 | moduleList = [], 10 | astRoot = esprima.parse(fileContents); 11 | 12 | parse.recurse(astRoot, function (callName, config, name, deps) { 13 | if (!deps) { 14 | deps = []; 15 | } 16 | 17 | if (!name) { 18 | //If there is no module name, the dependencies are for 19 | //this file/default module name. 20 | moduleDeps = moduleDeps.concat(deps); 21 | } else { 22 | moduleList.push({ 23 | name: name, 24 | deps: deps 25 | }); 26 | } 27 | 28 | //If define was found, no need to dive deeper, unless 29 | //the config explicitly wants to dig deeper. 30 | return !!options.findNestedDependencies; 31 | }, options); 32 | 33 | return { 34 | moduleDeps: moduleDeps, 35 | moduleList: moduleList 36 | }; 37 | } 38 | 39 | function findSimple(fileContents, options) { 40 | var result = find(fileContents, options); 41 | return result.moduleDeps.concat(result.moduleList); 42 | } 43 | 44 | module.exports = findSimple; 45 | module.exports.find = find; 46 | module.exports.parse = parse; 47 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The "New" BSD License: 3 | ---------------------- 4 | 5 | Copyright (c) 2014, Mikito Takada 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of amdetective nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # amdetective 2 | 3 | Find all calls to `require()` in AMD modules by walking the AST. 4 | 5 | This module uses code extracted from [r.js](https://github.com/jrburke/r.js) rather than trying to write it's own version of r.js parsing. It depends on esprima (but not r.js). 6 | 7 | # Install 8 | 9 | ``` 10 | npm install amdetective 11 | ``` 12 | 13 | # example 14 | 15 | First, create `detect.js` which is just a four line CLI wrapper around `amdetective`: 16 | 17 | ````js 18 | var fs = require('fs'), 19 | amdetective = require('amdetective'); 20 | 21 | console.log('Reading file from first argument: ' + process.argv[2]); 22 | console.log(amdetective(fs.readFileSync(process.argv[2]).toString())); 23 | ```` 24 | 25 | Now, let's run it on a bunch of examples to see some output. You can also run this command on your own files to get more realistic examples. 26 | 27 | ## Definition Functions with Dependencies (simple.js) 28 | 29 | ````js 30 | require(['module1', 'path/to/module2'], function(a, b){ 31 | // ... 32 | }); 33 | ```` 34 | 35 | Running `node detect.js simple.js` produces: 36 | 37 | ```` 38 | Reading file from first argument: simple.js 39 | [ 'module1', 'path/to/module2' ] 40 | ```` 41 | 42 | ## Simplified CommonJS Wrapper (simple2.js) 43 | 44 | ````js 45 | define(function(require) { 46 | var a = require('some/file'), 47 | b = require('json!foo/bar'); 48 | // ... 49 | }); 50 | ```` 51 | 52 | Running `node detect.js simple2.js` produces: 53 | 54 | ```` 55 | Reading file from first argument: simple2.js 56 | [ 'require', 'some/file', 'json!foo/bar' ] 57 | ```` 58 | 59 | ## Named module (named.js) 60 | 61 | ````js 62 | define("foo/title", 63 | ["my/cart", "my/inventory"], 64 | function(cart, inventory) { 65 | } 66 | ); 67 | ```` 68 | 69 | Running `node detect.js simple2.js` produces: 70 | 71 | ```` 72 | Reading file from first argument: named.js 73 | [ { name: 'foo/title', deps: [ 'my/cart', 'my/inventory' ] } ] 74 | ```` 75 | 76 | Note how named modules are treated differently - this is just something that the underlying resolution code does so be prepared to deal with it. 77 | 78 | # Methods 79 | 80 | ## amdetective(src, opts) 81 | 82 | Given some source body `src`, return an array of all the `require()` call arguments detected by AMD/r.js. 83 | 84 | The options parameter `opts` is passed along to `parse.recurse()` in [lib/parse.js](https://github.com/mixu/amdetective/blob/master/lib/parse.js#L196). This is normally the build config options if it is passed. 85 | 86 | # License 87 | 88 | BSD 89 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var amdetective = require('../'); 3 | 4 | describe('amdetective', function() { 5 | 6 | // soo many different forms via http://requirejs.org/docs/api.html 7 | 8 | it('works on Simple Name/Value Pairs', function() { 9 | var input = 'define({ color: "black", size: "unisize" });'; 10 | assert.deepEqual(amdetective(input), []); 11 | }); 12 | 13 | it('works on Definition Functions', function() { 14 | var input = 'define(function () { return { color: "black", size: "unisize" } });'; 15 | assert.deepEqual(amdetective(input), []); 16 | }); 17 | 18 | it('works on Definition Functions with Dependencies', function() { 19 | var input = [ 20 | 'define(["./cart", "./inventory"], function(cart, inventory) {', 21 | '//return an object to define the "my/shirt" module.', 22 | ' return {', 23 | ' color: "blue",', 24 | ' size: "large",', 25 | ' addToCart: function() {', 26 | ' inventory.decrement(this);', 27 | ' cart.add(this);', 28 | ' }', 29 | ' }', 30 | '});'].join('\n'); 31 | assert.deepEqual(amdetective(input), [ './cart', './inventory' ]); 32 | }); 33 | 34 | it('works on a Module as a Function', function() { 35 | var input = [ 36 | 'define(["my/cart", "my/inventory"],', 37 | ' function(cart, inventory) {', 38 | ' //return a function to define "foo/title".', 39 | ' //It gets or sets the window title.', 40 | ' return function(title) {', 41 | ' return title ? (window.title = title) :', 42 | ' inventory.storeName + \' \' + cart.name;', 43 | ' }', 44 | ' }', 45 | ');' 46 | ].join('\n'); 47 | assert.deepEqual(amdetective(input), ['my/cart', 'my/inventory']); 48 | }); 49 | 50 | it('works on a Module with Simplified CommonJS Wrapper', function() { 51 | var input = [ 52 | 'define(function(require, exports, module) {', 53 | ' var a = require(\'a\'),', 54 | ' b = require(\'b\');', 55 | '', 56 | ' //Return the module value', 57 | ' return function () {};', 58 | ' }', 59 | ');' 60 | ].join('\n'); 61 | assert.deepEqual(amdetective(input), [ 'require', 'exports', 'module', 'a', 'b' ]); 62 | }); 63 | 64 | it('works on a Module with a Name', function() { 65 | var input = [ 66 | 'define("foo/title",', 67 | ' ["my/cart", "my/inventory"],', 68 | ' function(cart, inventory) {', 69 | ' //Define foo/title object in here.', 70 | ' }', 71 | ');' 72 | ].join('\n'); 73 | assert.deepEqual(amdetective(input), [ { name: 'foo/title', deps: [ 'my/cart', 'my/inventory' ] } ]); 74 | }); 75 | 76 | it('works on a Module with a name, no dependency and arrow function as factory', function() { 77 | var input = 'define("foo/title", () => { /* Define foo/title object in here. */ });'; 78 | assert.deepEqual(amdetective(input), [ { name: 'foo/title', deps: [] } ]); 79 | }); 80 | 81 | it('works on a named module with relative module names inside define()', function() { 82 | var input = [ 83 | 'define("foo/title", function() {', 84 | ' var a = require("a");', 85 | ' var b = require("b");', 86 | '});' 87 | ].join('\n'); 88 | assert.deepEqual(amdetective(input), [ { name: 'foo/title', deps: [ 'a', 'b' ] } ]); 89 | }); 90 | 91 | it('works with Relative module names inside define()', function() { 92 | var input = [ 93 | 'define(["require", "./relative/name"], function(require) {', 94 | ' var mod = require("./relative/name");', 95 | '});' 96 | ].join('\n'); 97 | assert.deepEqual(amdetective(input), [ 'require', './relative/name' ]); 98 | }); 99 | 100 | it('works with shortened syntax that is available for use with translating CommonJS modules', function() { 101 | var input = [ 102 | 'define(function(require) {', 103 | ' var mod = require("./relative/name");', 104 | '});' 105 | ].join('\n'); 106 | assert.deepEqual(amdetective(input), [ 'require', './relative/name' ]); 107 | }); 108 | 109 | it('works with Specify a JSONP Service Dependency', function() { 110 | var input = [ 111 | 'require(["http://example.com/api/data.json?callback=define"],', 112 | ' function (data) {', 113 | ' //The data object will be the API response for the', 114 | ' //JSONP data call.', 115 | ' console.log(data);', 116 | ' }', 117 | ');' 118 | ].join('\n'); 119 | assert.deepEqual(amdetective(input), [ 'http://example.com/api/data.json?callback=define' ]); 120 | }); 121 | 122 | it('works with Generate URLs relative to module', function() { 123 | // the `require.toUrl` is not treated specially here 124 | var input = [ 125 | 'define(["require"], function(require) {', 126 | ' var cssUrl = require.toUrl("./style.css");', 127 | '});' 128 | ].join('\n'); 129 | assert.deepEqual(amdetective(input), [ 'require' ]); 130 | }); 131 | it('is able to detect a require.config block', function () { 132 | var input = [ 133 | 'require.config({', 134 | ' paths: {', 135 | ' myModule: "path/to/my/module"', 136 | ' }', 137 | '});' 138 | ].join('\n'); 139 | assert.deepEqual(amdetective.parse.findConfig(input).config, { paths: { myModule: 'path/to/my/module'} }); 140 | }) 141 | }); 142 | -------------------------------------------------------------------------------- /lib/lang.js: -------------------------------------------------------------------------------- 1 | var lang, isJavaObj, 2 | hasOwn = Object.prototype.hasOwnProperty; 3 | /** 4 | * @license Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. 5 | * Available via the MIT or new BSD license. 6 | * see: http://github.com/jrburke/requirejs for details 7 | */ 8 | 9 | function hasProp(obj, prop) { 10 | return hasOwn.call(obj, prop); 11 | } 12 | 13 | isJavaObj = function () { 14 | return false; 15 | }; 16 | 17 | //Rhino, but not Nashorn (detected by importPackage not existing) 18 | //Can have some strange foreign objects. 19 | if (typeof java !== 'undefined' && java.lang && java.lang.Object && typeof importPackage !== 'undefined') { 20 | isJavaObj = function (obj) { 21 | return obj instanceof java.lang.Object; 22 | }; 23 | } 24 | 25 | lang = { 26 | backSlashRegExp: /\\/g, 27 | ostring: Object.prototype.toString, 28 | 29 | isArray: Array.isArray || function (it) { 30 | return lang.ostring.call(it) === "[object Array]"; 31 | }, 32 | 33 | isFunction: function(it) { 34 | return lang.ostring.call(it) === "[object Function]"; 35 | }, 36 | 37 | isRegExp: function(it) { 38 | return it && it instanceof RegExp; 39 | }, 40 | 41 | hasProp: hasProp, 42 | 43 | //returns true if the object does not have an own property prop, 44 | //or if it does, it is a falsy value. 45 | falseProp: function (obj, prop) { 46 | return !hasProp(obj, prop) || !obj[prop]; 47 | }, 48 | 49 | //gets own property value for given prop on object 50 | getOwn: function (obj, prop) { 51 | return hasProp(obj, prop) && obj[prop]; 52 | }, 53 | 54 | _mixin: function(dest, source, override){ 55 | var name; 56 | for (name in source) { 57 | if(source.hasOwnProperty(name) && 58 | (override || !dest.hasOwnProperty(name))) { 59 | dest[name] = source[name]; 60 | } 61 | } 62 | 63 | return dest; // Object 64 | }, 65 | 66 | /** 67 | * mixin({}, obj1, obj2) is allowed. If the last argument is a boolean, 68 | * then the source objects properties are force copied over to dest. 69 | */ 70 | mixin: function(dest){ 71 | var parameters = Array.prototype.slice.call(arguments), 72 | override, i, l; 73 | 74 | if (!dest) { dest = {}; } 75 | 76 | if (parameters.length > 2 && typeof arguments[parameters.length-1] === 'boolean') { 77 | override = parameters.pop(); 78 | } 79 | 80 | for (i = 1, l = parameters.length; i < l; i++) { 81 | lang._mixin(dest, parameters[i], override); 82 | } 83 | return dest; // Object 84 | }, 85 | 86 | /** 87 | * Does a deep mix of source into dest, where source values override 88 | * dest values if a winner is needed. 89 | * @param {Object} dest destination object that receives the mixed 90 | * values. 91 | * @param {Object} source source object contributing properties to mix 92 | * in. 93 | * @return {[Object]} returns dest object with the modification. 94 | */ 95 | deepMix: function(dest, source) { 96 | lang.eachProp(source, function (value, prop) { 97 | if (typeof value === 'object' && value && 98 | !lang.isArray(value) && !lang.isFunction(value) && 99 | !(value instanceof RegExp)) { 100 | 101 | if (!dest[prop]) { 102 | dest[prop] = {}; 103 | } 104 | lang.deepMix(dest[prop], value); 105 | } else { 106 | dest[prop] = value; 107 | } 108 | }); 109 | return dest; 110 | }, 111 | 112 | /** 113 | * Does a type of deep copy. Do not give it anything fancy, best 114 | * for basic object copies of objects that also work well as 115 | * JSON-serialized things, or has properties pointing to functions. 116 | * For non-array/object values, just returns the same object. 117 | * @param {Object} obj copy properties from this object 118 | * @param {Object} [ignoredProps] optional object whose own properties 119 | * are keys that should be ignored. 120 | * @return {Object} 121 | */ 122 | deeplikeCopy: function (obj, ignoredProps) { 123 | var type, result; 124 | 125 | if (lang.isArray(obj)) { 126 | result = []; 127 | obj.forEach(function(value) { 128 | result.push(lang.deeplikeCopy(value, ignoredProps)); 129 | }); 130 | return result; 131 | } 132 | 133 | type = typeof obj; 134 | if (obj === null || obj === undefined || type === 'boolean' || 135 | type === 'string' || type === 'number' || lang.isFunction(obj) || 136 | lang.isRegExp(obj)|| isJavaObj(obj)) { 137 | return obj; 138 | } 139 | 140 | //Anything else is an object, hopefully. 141 | result = {}; 142 | lang.eachProp(obj, function(value, key) { 143 | if (!ignoredProps || !hasProp(ignoredProps, key)) { 144 | result[key] = lang.deeplikeCopy(value, ignoredProps); 145 | } 146 | }); 147 | return result; 148 | }, 149 | 150 | delegate: (function () { 151 | // boodman/crockford delegation w/ cornford optimization 152 | function TMP() {} 153 | return function (obj, props) { 154 | TMP.prototype = obj; 155 | var tmp = new TMP(); 156 | TMP.prototype = null; 157 | if (props) { 158 | lang.mixin(tmp, props); 159 | } 160 | return tmp; // Object 161 | }; 162 | }()), 163 | 164 | /** 165 | * Helper function for iterating over an array. If the func returns 166 | * a true value, it will break out of the loop. 167 | */ 168 | each: function each(ary, func) { 169 | if (ary) { 170 | var i; 171 | for (i = 0; i < ary.length; i += 1) { 172 | if (func(ary[i], i, ary)) { 173 | break; 174 | } 175 | } 176 | } 177 | }, 178 | 179 | /** 180 | * Cycles over properties in an object and calls a function for each 181 | * property value. If the function returns a truthy value, then the 182 | * iteration is stopped. 183 | */ 184 | eachProp: function eachProp(obj, func) { 185 | var prop; 186 | for (prop in obj) { 187 | if (hasProp(obj, prop)) { 188 | if (func(obj[prop], prop)) { 189 | break; 190 | } 191 | } 192 | } 193 | }, 194 | 195 | //Similar to Function.prototype.bind, but the "this" object is specified 196 | //first, since it is easier to read/figure out what "this" will be. 197 | bind: function bind(obj, fn) { 198 | return function () { 199 | return fn.apply(obj, arguments); 200 | }; 201 | }, 202 | 203 | //Escapes a content string to be be a string that has characters escaped 204 | //for inclusion as part of a JS string. 205 | jsEscape: function (content) { 206 | return content.replace(/(["'\\])/g, '\\$1') 207 | .replace(/[\f]/g, "\\f") 208 | .replace(/[\b]/g, "\\b") 209 | .replace(/[\n]/g, "\\n") 210 | .replace(/[\t]/g, "\\t") 211 | .replace(/[\r]/g, "\\r"); 212 | } 213 | }; 214 | module.exports = lang; 215 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | var lang = require('./lang.js'), 2 | esprima = require('esprima'); 3 | 4 | /** 5 | * @license Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. 6 | * Available via the MIT or new BSD license. 7 | * see: http://github.com/jrburke/requirejs for details 8 | */ 9 | 10 | function arrayToString(ary) { 11 | var output = '['; 12 | if (ary) { 13 | ary.forEach(function (item, i) { 14 | output += (i > 0 ? ',' : '') + '"' + lang.jsEscape(item) + '"'; 15 | }); 16 | } 17 | output += ']'; 18 | 19 | return output; 20 | } 21 | 22 | //This string is saved off because JSLint complains 23 | //about obj.arguments use, as 'reserved word' 24 | var argPropName = 'arguments', 25 | //Default object to use for "scope" checking for UMD identifiers. 26 | emptyScope = {}, 27 | mixin = lang.mixin, 28 | hasProp = lang.hasProp; 29 | 30 | //From an esprima example for traversing its ast. 31 | function traverse(object, visitor) { 32 | var child; 33 | 34 | if (!object) { 35 | return; 36 | } 37 | 38 | if (visitor.call(null, object) === false) { 39 | return false; 40 | } 41 | for (var i = 0, keys = Object.keys(object); i < keys.length; i++) { 42 | child = object[keys[i]]; 43 | if (typeof child === 'object' && child !== null) { 44 | if (traverse(child, visitor) === false) { 45 | return false; 46 | } 47 | } 48 | } 49 | } 50 | 51 | //Like traverse, but visitor returning false just 52 | //stops that subtree analysis, not the rest of tree 53 | //visiting. 54 | function traverseBroad(object, visitor) { 55 | var child; 56 | 57 | if (!object) { 58 | return; 59 | } 60 | 61 | if (visitor.call(null, object) === false) { 62 | return false; 63 | } 64 | for (var i = 0, keys = Object.keys(object); i < keys.length; i++) { 65 | child = object[key]; 66 | if (typeof child === 'object' && child !== null) { 67 | traverseBroad(child, visitor); 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * Pulls out dependencies from an array literal with just string members. 74 | * If string literals, will just return those string values in an array, 75 | * skipping other items in the array. 76 | * 77 | * @param {Node} node an AST node. 78 | * 79 | * @returns {Array} an array of strings. 80 | * If null is returned, then it means the input node was not a valid 81 | * dependency. 82 | */ 83 | function getValidDeps(node) { 84 | if (!node || node.type !== 'ArrayExpression' || !node.elements) { 85 | return; 86 | } 87 | 88 | var deps = []; 89 | 90 | node.elements.some(function (elem) { 91 | if (elem.type === 'Literal') { 92 | deps.push(elem.value); 93 | } 94 | }); 95 | 96 | return deps.length ? deps : undefined; 97 | } 98 | 99 | // Detects regular or arrow function expressions as the desired expression 100 | // type. 101 | function isFnExpression(node) { 102 | return (node && (node.type === 'FunctionExpression' || 103 | node.type === 'ArrowFunctionExpression')); 104 | } 105 | 106 | /** 107 | * Main parse function. Returns a string of any valid require or 108 | * define/require.def calls as part of one JavaScript source string. 109 | * @param {String} moduleName the module name that represents this file. 110 | * It is used to create a default define if there is not one already for the 111 | * file. This allows properly tracing dependencies for builds. Otherwise, if 112 | * the file just has a require() call, the file dependencies will not be 113 | * properly reflected: the file will come before its dependencies. 114 | * @param {String} moduleName 115 | * @param {String} fileName 116 | * @param {String} fileContents 117 | * @param {Object} options optional options. insertNeedsDefine: true will 118 | * add calls to require.needsDefine() if appropriate. 119 | * @returns {String} JS source string or null, if no require or 120 | * define/require.def calls are found. 121 | */ 122 | function parse(moduleName, fileName, fileContents, options) { 123 | options = options || {}; 124 | 125 | //Set up source input 126 | var i, moduleCall, depString, 127 | moduleDeps = [], 128 | result = '', 129 | moduleList = [], 130 | needsDefine = true, 131 | astRoot = esprima.parse(fileContents); 132 | 133 | parse.recurse(astRoot, function (callName, config, name, deps, node, factoryIdentifier, fnExpScope) { 134 | if (!deps) { 135 | deps = []; 136 | } 137 | 138 | if (callName === 'define' && (!name || name === moduleName)) { 139 | needsDefine = false; 140 | } 141 | 142 | if (!name) { 143 | //If there is no module name, the dependencies are for 144 | //this file/default module name. 145 | moduleDeps = moduleDeps.concat(deps); 146 | } else { 147 | moduleList.push({ 148 | name: name, 149 | deps: deps 150 | }); 151 | } 152 | 153 | if (callName === 'define' && factoryIdentifier && hasProp(fnExpScope, factoryIdentifier)) { 154 | return factoryIdentifier; 155 | } 156 | 157 | //If define was found, no need to dive deeper, unless 158 | //the config explicitly wants to dig deeper. 159 | return !!options.findNestedDependencies; 160 | }, options); 161 | 162 | if (options.insertNeedsDefine && needsDefine) { 163 | result += 'require.needsDefine("' + moduleName + '");'; 164 | } 165 | 166 | if (moduleDeps.length || moduleList.length) { 167 | for (i = 0; i < moduleList.length; i++) { 168 | moduleCall = moduleList[i]; 169 | if (result) { 170 | result += '\n'; 171 | } 172 | 173 | //If this is the main module for this file, combine any 174 | //"anonymous" dependencies (could come from a nested require 175 | //call) with this module. 176 | if (moduleCall.name === moduleName) { 177 | moduleCall.deps = moduleCall.deps.concat(moduleDeps); 178 | moduleDeps = []; 179 | } 180 | 181 | depString = arrayToString(moduleCall.deps); 182 | result += 'define("' + moduleCall.name + '",' + 183 | depString + ');'; 184 | } 185 | if (moduleDeps.length) { 186 | if (result) { 187 | result += '\n'; 188 | } 189 | depString = arrayToString(moduleDeps); 190 | result += 'define("' + moduleName + '",' + depString + ');'; 191 | } 192 | } 193 | 194 | return result || null; 195 | } 196 | 197 | parse.traverse = traverse; 198 | parse.traverseBroad = traverseBroad; 199 | parse.isFnExpression = isFnExpression; 200 | 201 | /** 202 | * Handles parsing a file recursively for require calls. 203 | * @param {Array} parentNode the AST node to start with. 204 | * @param {Function} onMatch function to call on a parse match. 205 | * @param {Object} [options] This is normally the build config options if 206 | * it is passed. 207 | * @param {Object} [fnExpScope] holds list of function expresssion 208 | * argument identifiers, set up internally, not passed in 209 | */ 210 | parse.recurse = function (object, onMatch, options, fnExpScope) { 211 | //Like traverse, but skips if branches that would not be processed 212 | //after has application that results in tests of true or false boolean 213 | //literal values. 214 | var keys, child, result, i, params, param, tempObject, 215 | hasHas = options && options.has; 216 | 217 | fnExpScope = fnExpScope || emptyScope; 218 | 219 | if (!object) { 220 | return; 221 | } 222 | 223 | //If has replacement has resulted in if(true){} or if(false){}, take 224 | //the appropriate branch and skip the other one. 225 | if (hasHas && object.type === 'IfStatement' && object.test.type && 226 | object.test.type === 'Literal') { 227 | if (object.test.value) { 228 | //Take the if branch 229 | this.recurse(object.consequent, onMatch, options, fnExpScope); 230 | } else { 231 | //Take the else branch 232 | this.recurse(object.alternate, onMatch, options, fnExpScope); 233 | } 234 | } else { 235 | result = this.parseNode(object, onMatch, fnExpScope); 236 | if (result === false) { 237 | return; 238 | } else if (typeof result === 'string') { 239 | return result; 240 | } 241 | 242 | //Build up a "scope" object that informs nested recurse calls if 243 | //the define call references an identifier that is likely a UMD 244 | //wrapped function expression argument. 245 | //Catch (function(a) {... wrappers 246 | if (object.type === 'ExpressionStatement' && object.expression && 247 | object.expression.type === 'CallExpression' && object.expression.callee && 248 | isFnExpression(object.expression.callee)) { 249 | tempObject = object.expression.callee; 250 | } 251 | // Catch !function(a) {... wrappers 252 | if (object.type === 'UnaryExpression' && object.argument && 253 | object.argument.type === 'CallExpression' && object.argument.callee && 254 | isFnExpression(object.argument.callee)) { 255 | tempObject = object.argument.callee; 256 | } 257 | if (tempObject && tempObject.params && tempObject.params.length) { 258 | params = tempObject.params; 259 | fnExpScope = mixin({}, fnExpScope, true); 260 | for (i = 0; i < params.length; i++) { 261 | param = params[i]; 262 | if (param.type === 'Identifier') { 263 | fnExpScope[param.name] = true; 264 | } 265 | } 266 | } 267 | 268 | for (i = 0, keys = Object.keys(object); i < keys.length; i++) { 269 | child = object[keys[i]]; 270 | if (typeof child === 'object' && child !== null) { 271 | result = this.recurse(child, onMatch, options, fnExpScope); 272 | if (typeof result === 'string' && hasProp(fnExpScope, result)) { 273 | //The result was still in fnExpScope so break. Otherwise, 274 | //was a return from a a tree that had a UMD definition, 275 | //but now out of that scope so keep siblings. 276 | break; 277 | } 278 | } 279 | } 280 | 281 | //Check for an identifier for a factory function identifier being 282 | //passed in as a function expression, indicating a UMD-type of 283 | //wrapping. 284 | if (typeof result === 'string') { 285 | if (hasProp(fnExpScope, result)) { 286 | //result still in scope, keep jumping out indicating the 287 | //identifier still in use. 288 | return result; 289 | } 290 | 291 | return; 292 | } 293 | } 294 | }; 295 | 296 | /** 297 | * Determines if the file defines the require/define module API. 298 | * Specifically, it looks for the `define.amd = ` expression. 299 | * @param {String} fileName 300 | * @param {String} fileContents 301 | * @returns {Boolean} 302 | */ 303 | parse.definesRequire = function (fileName, fileContents) { 304 | var foundDefine = false, 305 | foundDefineAmd = false; 306 | 307 | traverse(esprima.parse(fileContents), function (node) { 308 | // Look for a top level declaration of a define, like 309 | // var requirejs, require, define, off Program body. 310 | if (node.type === 'Program' && node.body && node.body.length) { 311 | foundDefine = node.body.some(function(bodyNode) { 312 | // var define 313 | if (bodyNode.type === 'VariableDeclaration') { 314 | var decls = bodyNode.declarations; 315 | if (decls) { 316 | var hasVarDefine = decls.some(function(declNode) { 317 | return (declNode.type === 'VariableDeclarator' && 318 | declNode.id && 319 | declNode.id.type === 'Identifier' && 320 | declNode.id.name === 'define'); 321 | }); 322 | if (hasVarDefine) { 323 | return true; 324 | } 325 | } 326 | } 327 | 328 | // function define() {} 329 | if (bodyNode.type === 'FunctionDeclaration' && 330 | bodyNode.id && 331 | bodyNode.id.type === 'Identifier' && 332 | bodyNode.id.name === 'define') { 333 | return true; 334 | } 335 | 336 | 337 | 338 | 339 | 340 | 341 | }); 342 | } 343 | 344 | // Need define variable found first, before detecting define.amd. 345 | if (foundDefine && parse.hasDefineAmd(node)) { 346 | foundDefineAmd = true; 347 | 348 | //Stop traversal 349 | return false; 350 | } 351 | }); 352 | 353 | return foundDefine && foundDefineAmd; 354 | }; 355 | 356 | /** 357 | * Finds require("") calls inside a CommonJS anonymous module wrapped in a 358 | * define(function(require, exports, module){}) wrapper. These dependencies 359 | * will be added to a modified define() call that lists the dependencies 360 | * on the outside of the function. 361 | * @param {String} fileName 362 | * @param {String|Object} fileContents: a string of contents, or an already 363 | * parsed AST tree. 364 | * @returns {Array} an array of module names that are dependencies. Always 365 | * returns an array, but could be of length zero. 366 | */ 367 | parse.getAnonDeps = function (fileName, fileContents) { 368 | var astRoot = typeof fileContents === 'string' ? 369 | esprima.parse(fileContents) : fileContents, 370 | defFunc = this.findAnonDefineFactory(astRoot); 371 | 372 | return parse.getAnonDepsFromNode(defFunc); 373 | }; 374 | 375 | /** 376 | * Finds require("") calls inside a CommonJS anonymous module wrapped 377 | * in a define function, given an AST node for the definition function. 378 | * @param {Node} node the AST node for the definition function. 379 | * @returns {Array} and array of dependency names. Can be of zero length. 380 | */ 381 | parse.getAnonDepsFromNode = function (node) { 382 | var deps = [], 383 | funcArgLength; 384 | 385 | if (node) { 386 | this.findRequireDepNames(node, deps); 387 | 388 | //If no deps, still add the standard CommonJS require, exports, 389 | //module, in that order, to the deps, but only if specified as 390 | //function args. In particular, if exports is used, it is favored 391 | //over the return value of the function, so only add it if asked. 392 | funcArgLength = node.params && node.params.length; 393 | if (funcArgLength) { 394 | deps = (funcArgLength > 1 ? ["require", "exports", "module"] : 395 | ["require"]).concat(deps); 396 | } 397 | } 398 | return deps; 399 | }; 400 | 401 | parse.isDefineNodeWithArgs = function (node) { 402 | return node && node.type === 'CallExpression' && 403 | node.callee && node.callee.type === 'Identifier' && 404 | node.callee.name === 'define' && node[argPropName]; 405 | }; 406 | 407 | /** 408 | * Finds the function in define(function (require, exports, module){}); 409 | * @param {Array} node 410 | * @returns {Boolean} 411 | */ 412 | parse.findAnonDefineFactory = function (node) { 413 | var match; 414 | 415 | traverse(node, function (node) { 416 | var arg0, arg1; 417 | 418 | if (parse.isDefineNodeWithArgs(node)) { 419 | 420 | //Just the factory function passed to define 421 | arg0 = node[argPropName][0]; 422 | if (isFnExpression(arg0)) { 423 | match = arg0; 424 | return false; 425 | } 426 | 427 | //A string literal module ID followed by the factory function. 428 | arg1 = node[argPropName][1]; 429 | if (arg0.type === 'Literal' && isFnExpression(arg1)) { 430 | match = arg1; 431 | return false; 432 | } 433 | } 434 | }); 435 | 436 | return match; 437 | }; 438 | 439 | /** 440 | * Finds any config that is passed to requirejs. That includes calls to 441 | * require/requirejs.config(), as well as require({}, ...) and 442 | * requirejs({}, ...) 443 | * @param {String} fileContents 444 | * 445 | * @returns {Object} a config details object with the following properties: 446 | * - config: {Object} the config object found. Can be undefined if no 447 | * config found. 448 | * - range: {Array} the start index and end index in the contents where 449 | * the config was found. Can be undefined if no config found. 450 | * Can throw an error if the config in the file cannot be evaluated in 451 | * a build context to valid JavaScript. 452 | */ 453 | parse.findConfig = function (fileContents) { 454 | /*jslint evil: true */ 455 | var jsConfig, foundConfig, stringData, foundRange, quote, quoteMatch, 456 | quoteRegExp = /(:\s|\[\s*)(['"])/, 457 | astRoot = esprima.parse(fileContents, { 458 | loc: true 459 | }); 460 | 461 | traverse(astRoot, function (node) { 462 | var arg, 463 | requireType = parse.hasRequire(node); 464 | 465 | if (requireType && (requireType === 'require' || 466 | requireType === 'requirejs' || 467 | requireType === 'requireConfig' || 468 | requireType === 'requirejsConfig')) { 469 | 470 | arg = node[argPropName] && node[argPropName][0]; 471 | 472 | if (arg && arg.type === 'ObjectExpression') { 473 | stringData = parse.nodeToString(fileContents, arg); 474 | jsConfig = stringData.value; 475 | foundRange = stringData.range; 476 | return false; 477 | } 478 | } else { 479 | arg = parse.getRequireObjectLiteral(node); 480 | if (arg) { 481 | stringData = parse.nodeToString(fileContents, arg); 482 | jsConfig = stringData.value; 483 | foundRange = stringData.range; 484 | return false; 485 | } 486 | } 487 | }); 488 | 489 | if (jsConfig) { 490 | // Eval the config 491 | quoteMatch = quoteRegExp.exec(jsConfig); 492 | quote = (quoteMatch && quoteMatch[2]) || '"'; 493 | foundConfig = eval('(' + jsConfig + ')'); 494 | } 495 | 496 | return { 497 | config: foundConfig, 498 | range: foundRange, 499 | quote: quote 500 | }; 501 | }; 502 | 503 | /** Returns the node for the object literal assigned to require/requirejs, 504 | * for holding a declarative config. 505 | */ 506 | parse.getRequireObjectLiteral = function (node) { 507 | if (node.id && node.id.type === 'Identifier' && 508 | (node.id.name === 'require' || node.id.name === 'requirejs') && 509 | node.init && node.init.type === 'ObjectExpression') { 510 | return node.init; 511 | } 512 | }; 513 | 514 | /** 515 | * Renames require/requirejs/define calls to be ns + '.' + require/requirejs/define 516 | * Does *not* do .config calls though. See pragma.namespace for the complete 517 | * set of namespace transforms. This function is used because require calls 518 | * inside a define() call should not be renamed, so a simple regexp is not 519 | * good enough. 520 | * @param {String} fileContents the contents to transform. 521 | * @param {String} ns the namespace, *not* including trailing dot. 522 | * @return {String} the fileContents with the namespace applied 523 | */ 524 | parse.renameNamespace = function (fileContents, ns) { 525 | var lines, 526 | locs = [], 527 | astRoot = esprima.parse(fileContents, { 528 | loc: true 529 | }); 530 | 531 | parse.recurse(astRoot, function (callName, config, name, deps, node) { 532 | locs.push(node.loc); 533 | //Do not recurse into define functions, they should be using 534 | //local defines. 535 | return callName !== 'define'; 536 | }, {}); 537 | 538 | if (locs.length) { 539 | lines = fileContents.split('\n'); 540 | 541 | //Go backwards through the found locs, adding in the namespace name 542 | //in front. 543 | locs.reverse(); 544 | locs.forEach(function (loc) { 545 | var startIndex = loc.start.column, 546 | //start.line is 1-based, not 0 based. 547 | lineIndex = loc.start.line - 1, 548 | line = lines[lineIndex]; 549 | 550 | lines[lineIndex] = line.substring(0, startIndex) + 551 | ns + '.' + 552 | line.substring(startIndex, 553 | line.length); 554 | }); 555 | 556 | fileContents = lines.join('\n'); 557 | } 558 | 559 | return fileContents; 560 | }; 561 | 562 | /** 563 | * Finds all dependencies specified in dependency arrays and inside 564 | * simplified commonjs wrappers. 565 | * @param {String} fileName 566 | * @param {String} fileContents 567 | * 568 | * @returns {Array} an array of dependency strings. The dependencies 569 | * have not been normalized, they may be relative IDs. 570 | */ 571 | parse.findDependencies = function (fileName, fileContents, options) { 572 | var dependencies = [], 573 | astRoot = esprima.parse(fileContents); 574 | 575 | parse.recurse(astRoot, function (callName, config, name, deps) { 576 | if (deps) { 577 | dependencies = dependencies.concat(deps); 578 | } 579 | }, options); 580 | 581 | return dependencies; 582 | }; 583 | 584 | /** 585 | * Finds only CJS dependencies, ones that are the form 586 | * require('stringLiteral') 587 | */ 588 | parse.findCjsDependencies = function (fileName, fileContents) { 589 | var dependencies = []; 590 | 591 | traverse(esprima.parse(fileContents), function (node) { 592 | var arg; 593 | 594 | if (node && node.type === 'CallExpression' && node.callee && 595 | node.callee.type === 'Identifier' && 596 | node.callee.name === 'require' && node[argPropName] && 597 | node[argPropName].length === 1) { 598 | arg = node[argPropName][0]; 599 | if (arg.type === 'Literal') { 600 | dependencies.push(arg.value); 601 | } 602 | } 603 | }); 604 | 605 | return dependencies; 606 | }; 607 | 608 | //function define() {} 609 | parse.hasDefDefine = function (node) { 610 | return node.type === 'FunctionDeclaration' && node.id && 611 | node.id.type === 'Identifier' && node.id.name === 'define'; 612 | }; 613 | 614 | //define.amd = ... 615 | parse.hasDefineAmd = function (node) { 616 | return node && node.type === 'AssignmentExpression' && 617 | node.left && node.left.type === 'MemberExpression' && 618 | node.left.object && node.left.object.name === 'define' && 619 | node.left.property && node.left.property.name === 'amd'; 620 | }; 621 | 622 | //define.amd reference, as in: if (define.amd) 623 | parse.refsDefineAmd = function (node) { 624 | return node && node.type === 'MemberExpression' && 625 | node.object && node.object.name === 'define' && 626 | node.object.type === 'Identifier' && 627 | node.property && node.property.name === 'amd' && 628 | node.property.type === 'Identifier'; 629 | }; 630 | 631 | //require(), requirejs(), require.config() and requirejs.config() 632 | parse.hasRequire = function (node) { 633 | var callName, 634 | c = node && node.callee; 635 | 636 | if (node && node.type === 'CallExpression' && c) { 637 | if (c.type === 'Identifier' && 638 | (c.name === 'require' || 639 | c.name === 'requirejs')) { 640 | //A require/requirejs({}, ...) call 641 | callName = c.name; 642 | } else if (c.type === 'MemberExpression' && 643 | c.object && 644 | c.object.type === 'Identifier' && 645 | (c.object.name === 'require' || 646 | c.object.name === 'requirejs') && 647 | c.property && c.property.name === 'config') { 648 | // require/requirejs.config({}) call 649 | callName = c.object.name + 'Config'; 650 | } 651 | } 652 | 653 | return callName; 654 | }; 655 | 656 | //define() 657 | parse.hasDefine = function (node) { 658 | return node && node.type === 'CallExpression' && node.callee && 659 | node.callee.type === 'Identifier' && 660 | node.callee.name === 'define'; 661 | }; 662 | 663 | /** 664 | * If there is a named define in the file, returns the name. Does not 665 | * scan for mulitple names, just the first one. 666 | */ 667 | parse.getNamedDefine = function (fileContents) { 668 | var name; 669 | traverse(esprima.parse(fileContents), function (node) { 670 | if (node && node.type === 'CallExpression' && node.callee && 671 | node.callee.type === 'Identifier' && 672 | node.callee.name === 'define' && 673 | node[argPropName] && node[argPropName][0] && 674 | node[argPropName][0].type === 'Literal') { 675 | name = node[argPropName][0].value; 676 | return false; 677 | } 678 | }); 679 | 680 | return name; 681 | }; 682 | 683 | /** 684 | * Finds all the named define module IDs in a file. 685 | */ 686 | parse.getAllNamedDefines = function (fileContents, excludeMap) { 687 | var names = []; 688 | parse.recurse(esprima.parse(fileContents), 689 | function (callName, config, name, deps, node, factoryIdentifier, fnExpScope) { 690 | if (callName === 'define' && name) { 691 | if (!excludeMap.hasOwnProperty(name)) { 692 | names.push(name); 693 | } 694 | } 695 | 696 | //If a UMD definition that points to a factory that is an Identifier, 697 | //indicate processing should not traverse inside the UMD definition. 698 | if (callName === 'define' && factoryIdentifier && hasProp(fnExpScope, factoryIdentifier)) { 699 | return factoryIdentifier; 700 | } 701 | 702 | //If define was found, no need to dive deeper, unless 703 | //the config explicitly wants to dig deeper. 704 | return true; 705 | }, {}); 706 | 707 | return names; 708 | }; 709 | 710 | /** 711 | * Determines if define(), require({}|[]) or requirejs was called in the 712 | * file. Also finds out if define() is declared and if define.amd is called. 713 | */ 714 | parse.usesAmdOrRequireJs = function (fileName, fileContents) { 715 | var uses; 716 | 717 | traverse(esprima.parse(fileContents), function (node) { 718 | var type, callName, arg; 719 | 720 | if (parse.hasDefDefine(node)) { 721 | //function define() {} 722 | type = 'declaresDefine'; 723 | } else if (parse.hasDefineAmd(node)) { 724 | type = 'defineAmd'; 725 | } else { 726 | callName = parse.hasRequire(node); 727 | if (callName) { 728 | arg = node[argPropName] && node[argPropName][0]; 729 | if (arg && (arg.type === 'ObjectExpression' || 730 | arg.type === 'ArrayExpression')) { 731 | type = callName; 732 | } 733 | } else if (parse.hasDefine(node)) { 734 | type = 'define'; 735 | } 736 | } 737 | 738 | if (type) { 739 | if (!uses) { 740 | uses = {}; 741 | } 742 | uses[type] = true; 743 | } 744 | }); 745 | 746 | return uses; 747 | }; 748 | 749 | /** 750 | * Determines if require(''), exports.x =, module.exports =, 751 | * __dirname, __filename are used. So, not strictly traditional CommonJS, 752 | * also checks for Node variants. 753 | */ 754 | parse.usesCommonJs = function (fileName, fileContents) { 755 | var uses = null, 756 | assignsExports = false; 757 | 758 | 759 | traverse(esprima.parse(fileContents), function (node) { 760 | var type, 761 | exp = node.expression || node.init; 762 | 763 | if (node.type === 'Identifier' && 764 | (node.name === '__dirname' || node.name === '__filename')) { 765 | type = node.name.substring(2); 766 | } else if (node.type === 'VariableDeclarator' && node.id && 767 | node.id.type === 'Identifier' && 768 | node.id.name === 'exports') { 769 | //Hmm, a variable assignment for exports, so does not use cjs 770 | //exports. 771 | type = 'varExports'; 772 | } else if (exp && exp.type === 'AssignmentExpression' && exp.left && 773 | exp.left.type === 'MemberExpression' && exp.left.object) { 774 | if (exp.left.object.name === 'module' && exp.left.property && 775 | exp.left.property.name === 'exports') { 776 | type = 'moduleExports'; 777 | } else if (exp.left.object.name === 'exports' && 778 | exp.left.property) { 779 | type = 'exports'; 780 | } else if (exp.left.object.type === 'MemberExpression' && 781 | exp.left.object.object.name === 'module' && 782 | exp.left.object.property.name === 'exports' && 783 | exp.left.object.property.type === 'Identifier') { 784 | type = 'moduleExports'; 785 | } 786 | 787 | } else if (node && node.type === 'CallExpression' && node.callee && 788 | node.callee.type === 'Identifier' && 789 | node.callee.name === 'require' && node[argPropName] && 790 | node[argPropName].length === 1 && 791 | node[argPropName][0].type === 'Literal') { 792 | type = 'require'; 793 | } 794 | 795 | if (type) { 796 | if (type === 'varExports') { 797 | assignsExports = true; 798 | } else if (type !== 'exports' || !assignsExports) { 799 | if (!uses) { 800 | uses = {}; 801 | } 802 | uses[type] = true; 803 | } 804 | } 805 | }); 806 | 807 | return uses; 808 | }; 809 | 810 | 811 | parse.findRequireDepNames = function (node, deps) { 812 | traverse(node, function (node) { 813 | var arg; 814 | 815 | if (node && node.type === 'CallExpression' && node.callee && 816 | node.callee.type === 'Identifier' && 817 | node.callee.name === 'require' && 818 | node[argPropName] && node[argPropName].length === 1) { 819 | 820 | arg = node[argPropName][0]; 821 | if (arg.type === 'Literal') { 822 | deps.push(arg.value); 823 | } 824 | } 825 | }); 826 | }; 827 | 828 | /** 829 | * Determines if a specific node is a valid require or define/require.def 830 | * call. 831 | * @param {Array} node 832 | * @param {Function} onMatch a function to call when a match is found. 833 | * It is passed the match name, and the config, name, deps possible args. 834 | * The config, name and deps args are not normalized. 835 | * @param {Object} fnExpScope an object whose keys are all function 836 | * expression identifiers that should be in scope. Useful for UMD wrapper 837 | * detection to avoid parsing more into the wrapped UMD code. 838 | * 839 | * @returns {String} a JS source string with the valid require/define call. 840 | * Otherwise null. 841 | */ 842 | parse.parseNode = function (node, onMatch, fnExpScope) { 843 | var name, deps, cjsDeps, arg, factory, exp, refsDefine, bodyNode, 844 | args = node && node[argPropName], 845 | callName = parse.hasRequire(node), 846 | isUmd = false; 847 | 848 | if (callName === 'require' || callName === 'requirejs') { 849 | //A plain require/requirejs call 850 | arg = node[argPropName] && node[argPropName][0]; 851 | if (arg && arg.type !== 'ArrayExpression') { 852 | if (arg.type === 'ObjectExpression') { 853 | //A config call, try the second arg. 854 | arg = node[argPropName][1]; 855 | } 856 | } 857 | 858 | deps = getValidDeps(arg); 859 | if (!deps) { 860 | return; 861 | } 862 | 863 | return onMatch("require", null, null, deps, node); 864 | } else if (parse.hasDefine(node) && args && args.length) { 865 | name = args[0]; 866 | deps = args[1]; 867 | factory = args[2]; 868 | 869 | if (name.type === 'ArrayExpression') { 870 | //No name, adjust args 871 | factory = deps; 872 | deps = name; 873 | name = null; 874 | } else if (isFnExpression(name)) { 875 | //Just the factory, no name or deps 876 | factory = name; 877 | name = deps = null; 878 | } else if (name.type === 'Identifier' && args.length === 1 && 879 | hasProp(fnExpScope, name.name)) { 880 | //define(e) where e is a UMD identifier for the factory 881 | //function. 882 | isUmd = true; 883 | factory = name; 884 | name = null; 885 | } else if (name.type !== 'Literal') { 886 | //An object literal, just null out 887 | name = deps = factory = null; 888 | } 889 | 890 | if (name && name.type === 'Literal' && deps) { 891 | if (isFnExpression(deps)) { 892 | //deps is the factory 893 | factory = deps; 894 | deps = null; 895 | } else if (deps.type === 'ObjectExpression') { 896 | //deps is object literal, null out 897 | deps = factory = null; 898 | } else if (deps.type === 'Identifier') { 899 | if (args.length === 2) { 900 | //define('id', factory) 901 | deps = factory = null; 902 | } else if (args.length === 3 && isFnExpression(factory)) { 903 | //define('id', depsIdentifier, factory) 904 | //Since identifier, cannot know the deps, but do not 905 | //error out, assume they are taken care of outside of 906 | //static parsing. 907 | deps = null; 908 | } 909 | } 910 | } 911 | 912 | if (deps && deps.type === 'ArrayExpression') { 913 | deps = getValidDeps(deps); 914 | } else if (isFnExpression(factory)) { 915 | //If no deps and a factory function, could be a commonjs sugar 916 | //wrapper, scan the function for dependencies. 917 | cjsDeps = parse.getAnonDepsFromNode(factory); 918 | if (cjsDeps.length) { 919 | deps = cjsDeps; 920 | } 921 | } else if (deps || (factory && !isUmd)) { 922 | //Does not match the shape of an AMD call. 923 | return; 924 | } 925 | 926 | //Just save off the name as a string instead of an AST object. 927 | if (name && name.type === 'Literal') { 928 | name = name.value; 929 | } 930 | 931 | return onMatch("define", null, name, deps, node, 932 | (factory && factory.type === 'Identifier' ? factory.name : undefined), 933 | fnExpScope); 934 | } else if (node.type === 'CallExpression' && node.callee && 935 | isFnExpression(node.callee) && 936 | node.callee.body && node.callee.body.body && 937 | node.callee.body.body.length === 1 && 938 | node.callee.body.body[0].type === 'IfStatement') { 939 | bodyNode = node.callee.body.body[0]; 940 | //Look for a define(Identifier) case, but only if inside an 941 | //if that has a define.amd test 942 | if (bodyNode.consequent && bodyNode.consequent.body) { 943 | exp = bodyNode.consequent.body[0]; 944 | if (exp.type === 'ExpressionStatement' && exp.expression && 945 | parse.hasDefine(exp.expression) && 946 | exp.expression.arguments && 947 | exp.expression.arguments.length === 1 && 948 | exp.expression.arguments[0].type === 'Identifier') { 949 | 950 | //Calls define(Identifier) as first statement in body. 951 | //Confirm the if test references define.amd 952 | traverse(bodyNode.test, function (node) { 953 | if (parse.refsDefineAmd(node)) { 954 | refsDefine = true; 955 | return false; 956 | } 957 | }); 958 | 959 | if (refsDefine) { 960 | return onMatch("define", null, null, null, exp.expression, 961 | exp.expression.arguments[0].name, fnExpScope); 962 | } 963 | } 964 | } 965 | } 966 | }; 967 | 968 | /** 969 | * Converts an AST node into a JS source string by extracting 970 | * the node's location from the given contents string. Assumes 971 | * esprima.parse() with loc was done. 972 | * @param {String} contents 973 | * @param {Object} node 974 | * @returns {String} a JS source string. 975 | */ 976 | parse.nodeToString = function (contents, node) { 977 | var extracted, 978 | loc = node.loc, 979 | lines = contents.split('\n'), 980 | firstLine = loc.start.line > 1 ? 981 | lines.slice(0, loc.start.line - 1).join('\n') + '\n' : 982 | '', 983 | preamble = firstLine + 984 | lines[loc.start.line - 1].substring(0, loc.start.column); 985 | 986 | if (loc.start.line === loc.end.line) { 987 | extracted = lines[loc.start.line - 1].substring(loc.start.column, 988 | loc.end.column); 989 | } else { 990 | extracted = lines[loc.start.line - 1].substring(loc.start.column) + 991 | '\n' + 992 | lines.slice(loc.start.line, loc.end.line - 1).join('\n') + 993 | '\n' + 994 | lines[loc.end.line - 1].substring(0, loc.end.column); 995 | } 996 | 997 | return { 998 | value: extracted, 999 | range: [ 1000 | preamble.length, 1001 | preamble.length + extracted.length 1002 | ] 1003 | }; 1004 | }; 1005 | 1006 | /** 1007 | * Extracts license comments from JS text. 1008 | * @param {String} fileName 1009 | * @param {String} contents 1010 | * @returns {String} a string of license comments. 1011 | */ 1012 | parse.getLicenseComments = function (fileName, contents) { 1013 | var commentNode, refNode, subNode, value, i, j, 1014 | //xpconnect's Reflect does not support comment or range, but 1015 | //prefer continued operation vs strict parity of operation, 1016 | //as license comments can be expressed in other ways, like 1017 | //via wrap args, or linked via sourcemaps. 1018 | ast = esprima.parse(contents, { 1019 | comment: true, 1020 | range: true 1021 | }), 1022 | result = '', 1023 | existsMap = {}, 1024 | lineEnd = contents.indexOf('\r') === -1 ? '\n' : '\r\n'; 1025 | 1026 | if (ast.comments) { 1027 | for (i = 0; i < ast.comments.length; i++) { 1028 | commentNode = ast.comments[i]; 1029 | 1030 | if (commentNode.type === 'Line') { 1031 | value = '//' + commentNode.value + lineEnd; 1032 | refNode = commentNode; 1033 | 1034 | if (i + 1 >= ast.comments.length) { 1035 | value += lineEnd; 1036 | } else { 1037 | //Look for immediately adjacent single line comments 1038 | //since it could from a multiple line comment made out 1039 | //of single line comments. Like this comment. 1040 | for (j = i + 1; j < ast.comments.length; j++) { 1041 | subNode = ast.comments[j]; 1042 | if (subNode.type === 'Line' && 1043 | subNode.range[0] === refNode.range[1] + 1) { 1044 | //Adjacent single line comment. Collect it. 1045 | value += '//' + subNode.value + lineEnd; 1046 | refNode = subNode; 1047 | } else { 1048 | //No more single line comment blocks. Break out 1049 | //and continue outer looping. 1050 | break; 1051 | } 1052 | } 1053 | value += lineEnd; 1054 | i = j - 1; 1055 | } 1056 | } else { 1057 | value = '/*' + commentNode.value + '*/' + lineEnd + lineEnd; 1058 | } 1059 | 1060 | if (!existsMap[value] && (value.indexOf('license') !== -1 || 1061 | (commentNode.type === 'Block' && 1062 | value.indexOf('/*!') === 0) || 1063 | value.indexOf('opyright') !== -1 || 1064 | value.indexOf('(c)') !== -1)) { 1065 | 1066 | result += value; 1067 | existsMap[value] = true; 1068 | } 1069 | 1070 | } 1071 | } 1072 | 1073 | return result; 1074 | }; 1075 | 1076 | module.exports = parse; 1077 | --------------------------------------------------------------------------------