amdextract
Version:
Uses AST to extract AMD modules, their parts and an optimized output without unused dependencies while keeping the original format.
255 lines (202 loc) • 7.25 kB
JavaScript
/*
* amdextract
* https://github.com/mehdishojaei/amdextract
*
* Copyright (c) 2013 Mehdi Shojaei
* Licensed under the MIT license.
*/
;
var esprima = require('esprima'),
estraverse = require('estraverse');
var toString = Object.prototype.toString;
function traverse(object, visitor) {
var key, child,
result = visitor(object);
if (result || result === false) {
return result;
}
for (var i = 0, keys = Object.keys(object), length = keys.length; i < length; i++) {
key = keys[i];
child = object[key];
if (typeof child === 'object' && child !== null) {
if (typeof child.key === 'undefined') {
child.key = key;
}
if (result = traverse(child, visitor)) {
return result;
}
}
}
return false;
}
function getModules(parsedCode) {
var modules = [],
comments = parsedCode.comments,
tokens = parsedCode.tokens;
traverse(parsedCode, function(object) {
if (object.type === 'ExpressionStatement') {
var expression = object.expression;
if (expression.type === 'CallExpression' && expression.callee &&
expression.callee.type === 'Identifier' && expression.callee.name === 'define') {
expression.callee = estraverse.attachComments(expression.callee, comments, tokens);
var module = {},
leadingComments = expression.callee.leadingComments,
exceptsPaths = [],
id, paths, pathsIndex, callback, callbackIndex;
if (leadingComments) {
leadingComments.forEach(function(leadingComment) {
var matches = /^\s*exceptsPaths\s*:\s*(\S+(?:\s*,\s*\S+)*)\s*$/m.exec(leadingComment.value);
if (matches) {
Array.prototype.push.apply(exceptsPaths, matches[1].split(/\s*,\s*/));
}
});
}
module.exceptsPaths = exceptsPaths;
id = expression.arguments[0];
if (id && id.type === 'Literal') {
module.id = id;
}
pathsIndex = module.id ? 1 : 0;
paths = expression.arguments[pathsIndex];
if (paths && paths.type === 'ArrayExpression') {
module.paths = paths.elements.map(function(element) {
return estraverse.attachComments(element, comments, tokens);
});
}
callbackIndex = pathsIndex + 1;
callback = expression.arguments[callbackIndex];
if (callback && callback.type === 'FunctionExpression') {
module.dependencies = callback.params.map(function(param) {
return estraverse.attachComments(param, comments, tokens);
});
module.body = callback.body;
}
modules.push(module);
}
}
});
return modules;
}
function findUseage(variable, parsedCode) {
return traverse(parsedCode, function(object) {
if (object.type === 'FunctionExpression' || object.type === 'FunctionDeclaration') {
var params = object.params, obj;
if (obj = findUseage(variable, object.body)) {
for (var i = 0, length = params.length; i < length; i++) {
var param = params[i];
if (param.type === obj.type && param.name === obj.name) {
break;
}
}
if (i === length) {
return obj;
}
}
// Do not traverse function body.
return false;
} else if (object.type === 'Identifier' &&
(object.name === variable || (object.key && object.key.name && object.key.name.indexof(variable) !== -1)) &&
object.key !== 'property' && object.key !== 'id') {
return object;
}
});
}
function optimizeContent(content, modules) {
var rangesToRemove = [],
output = '',
start = 0,
regEx = /[\s,]/;
modules.forEach(function(module) {
module.unusedPaths.concat(module.unusedDependencies).forEach(function(item) {
var range = [];
range[0] = item.range[0];
range[1] = item.range[1];
if (item.leadingComments) {
range[0] = item.leadingComments[0].range[0];
}
if (item.trailingComments) {
range[1] = item.trailingComments[item.trailingComments.length - 1].range[1];
}
rangesToRemove.push(range);
});
});
rangesToRemove.forEach(function(range, index) {
var rangeStart = range[0] - 1,
rangeEnd = range[1],
commaVisited = false;
if (index === 0 || range[0] > rangesToRemove[index - 1][1]) {
for (var char = content[rangeStart]; regEx.test(char); char = content[--rangeStart]) {
if (char === ',') {
commaVisited = true;
}
}
}
if (!commaVisited) {
for (var char = content[rangeEnd]; regEx.test(char); char = content[++rangeEnd])
;
}
range[0] = rangeStart + 1;
range[1] = rangeEnd;
if (range[0] > start) {
output += content.substring(start, range[0]);
}
start = range[1];
});
output += content.substring(start);
return output;
}
function isString(obj) {
return toString.call(obj) === "[object String]";
}
function isRegExp(obj) {
return toString.call(obj) === "[object RegExp]";
}
function isException(exceptions, item) {
return exceptions.some(function (exception) {
if (isString(exception)) {
return exception === item;
} else if (isRegExp(exception)) {
return exception.test(item);
}
});
}
module.exports.parse = function (content, options) {
options = options || {};
options.excepts = Array.isArray(options.excepts) ? options.excepts : [];
options.exceptsPaths = Array.isArray(options.exceptsPaths) ? options.exceptsPaths : [];
var parsedCode = esprima.parse(content, { range: true, comment: true, tokens: true }),
modules = getModules(parsedCode),
result = {};
result.results = modules.map(function(module) {
var moduleId = module.id,
paths = module.paths || [],
dependencies = module.dependencies || [],
unusedPaths,
unusedDependencies,
excepts = options.excepts,
exceptsPaths = module.exceptsPaths.concat(options.exceptsPaths);
unusedDependencies = dependencies.filter(function(dependency, index) {
return !isException(excepts, dependency.name) &&
(index >= paths.length || !isException(exceptsPaths, paths[index].value)) &&
!findUseage(dependency.name, module.body);
});
unusedPaths = unusedDependencies.map(function(dependency) {
return paths[dependencies.indexOf(dependency)];
}).concat(paths.slice(dependencies.length)).filter(function(path) {
return path && !isException(exceptsPaths, path.value);
});
module.unusedDependencies = unusedDependencies;
module.unusedPaths = unusedPaths;
return {
moduleId: moduleId ? moduleId.value : void 0,
paths: paths.map(function(path) { return path.value; }),
dependencies: dependencies.map(function(dep) { return dep.name; }),
unusedPaths: unusedPaths.map(function(path) { return path.value; }),
unusedDependencies: unusedDependencies.map(function(dep) { return dep.name; })
};
});
if (options.removeUnusedDependencies) {
result.optimizedContent = optimizeContent(content, modules);
}
return result;
};