lasso
Version:
Lasso.js is a build tool and runtime library for building and bundling all of the resources needed by a web application
339 lines (284 loc) • 11.2 kB
JavaScript
var path = require('path');
var espree = require('espree');
var codeFrame = require('babel-code-frame');
var estraverse = require('estraverse');
var ok = require('assert').ok;
var cwd = process.cwd();
var parseOpts = {
range: true,
sourceType: 'script',
ecmaVersion: espree.latestEcmaVersion
};
var shortCircuitRegExp = /require\(|require\.resolve\(|.async\(|#async|process|Buffer/;
function isRequire(node) {
return node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length === 1 &&
node.arguments[0].type === 'Literal' &&
typeof node.arguments[0].value === 'string';
}
function isRequireResolve(node) {
return node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'require' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'resolve' &&
node.arguments.length === 1 &&
node.arguments[0].type === 'Literal';
}
function isRequireFor(node, moduleName) {
return node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length === 1 &&
node.arguments[0].type === 'Literal' &&
node.arguments[0].value === moduleName;
}
function isRequireForAsyncLoader(node) {
return isRequireFor(node, 'lasso-loader') ||
isRequireFor(node, 'raptor-loader');
}
function isAsyncNode(node, scope) {
if (!node.arguments || !node.arguments.length) {
return false;
}
if (node.type !== 'CallExpression' ||
node.callee.type !== 'MemberExpression' ||
node.callee.property.type !== 'Identifier' ||
node.callee.property.name !== 'async') {
return false;
}
if (isRequireForAsyncLoader(node.callee.object)) {
return true;
}
if (node.callee.object.type === 'Identifier' &&
(scope[node.callee.object.name] === 'lasso-loader')) {
return true;
}
return false;
}
function parseAsyncNode(node, scope) {
if (!isAsyncNode(node, scope)) {
return;
}
var args = node.arguments;
var numArguments = args.length;
if ((numArguments < 1) || (numArguments > 2)) {
return;
}
var dependencies = [];
var hasInlineDependencies = false;
var packageIdProvided;
var firstArg = args[0];
// We only care if about the async calls if the first argument is an array
if (numArguments === 2) {
if (firstArg.type === 'ArrayExpression') {
hasInlineDependencies = true;
// call is something like:
// require('lasso-loader').async(['./dep1.js', './dep2.js'], callback)
var elems = firstArg.elements;
for (var i = 0; i < elems.length; i++) {
dependencies.push(elems[i].value);
}
} else {
// call is something like:
// require('lasso-loader').async('somePackageId', callback)
// require('lasso-loader').async(someVariable, callback)
packageIdProvided = true;
}
}
var callbackNode = args[numArguments - 1];
var hasFunctionBody =
(callbackNode.type === 'FunctionExpression') ||
(callbackNode.type === 'FunctionDeclaration');
return {
node: node,
requires: [],
dependencies: dependencies,
args: args,
callbackNode: callbackNode,
// require('lasso-loader').async(packageId, function() {}) is used
// then `packageIdProvided` will be `true`
packageIdProvided: packageIdProvided,
// Store the range of the first arg in case we need to replace
// or add to it.
firstArgRange: args[0].range,
// If the first argument to require('lasso-loader').async([...], callback) is
// is used then `hasInlineDependencies` will be `true`
hasInlineDependencies: hasInlineDependencies,
// If the last argument to require('lasso-loader').async(...)
// is a `FunctionDeclaration` or `FunctionExpression` then
// `hasFunctionBody` will be `true`.
hasFunctionBody: hasFunctionBody,
toString: function() {
return '[async: ' + this.name + ', dependencies=' + JSON.stringify(dependencies) + ']';
}
};
}
function recordGlobalsHelper(node, scope, foundGlobals) {
var id;
if (!node || node.type !== 'Identifier') {
return;
}
id = node.name;
if (id === 'require' ||
id === 'exports' ||
id === 'module' ||
id === 'arguments' ||
id === '__dirname' ||
id === '__filename') {
// We don't require about these "globals"
return;
}
if (!scope[id]) {
foundGlobals[id] = true;
}
}
function recordGlobals(node, parentNode, scope, foundGlobals) {
if (node.type === 'Identifier') {
if (parentNode.type === 'MemberExpression' ||
parentNode.type === 'Property' ||
parentNode.type === 'VariableDeclarator' ||
parentNode.type === 'FunctionDeclaration' ||
parentNode.type === 'FunctionExpression') {
return;
}
recordGlobalsHelper(node, scope, foundGlobals);
} else if (node.type === 'MemberExpression') {
if (parentNode !== 'MemberExpression') {
recordGlobalsHelper(node.object, scope, foundGlobals);
}
} else if (node.type === 'Property') {
recordGlobalsHelper(node.value, scope, foundGlobals);
} else if (node.type === 'VariableDeclarator') {
recordGlobalsHelper(node.init, scope, foundGlobals);
} else if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') {
if (node.type === 'FunctionDeclaration' && node.id) {
delete foundGlobals[node.id.name];
}
// Skip the params and the function ID and just look at the body nodes
node.body.body.forEach((bodyNode) => {
recordGlobalsHelper(bodyNode, scope, foundGlobals);
});
}
}
module.exports = function inspect(src, options) {
ok(src != null, 'src is requried');
var allowShortcircuit = !options || options.allowShortcircuit !== false;
if (allowShortcircuit && shortCircuitRegExp.test(src) === false) {
// Nothing of interest so nothing to do
return {
requires: [],
foundGlobals: {},
asyncBlocks: []
};
}
var requires = [];
var scopeStack = [{}];
var asyncScopeStack = [];
var asyncStack = [];
var curAsyncInfo = null;
var asyncBlocks = [];
var foundGlobals = {};
var parsedAst;
try {
parsedAst = espree.parse(src, parseOpts);
} catch (err) {
if (!err.lineNumber) {
throw err;
}
var filename = options && options.filename;
var errorLoc = '(' + err.lineNumber + ',' + err.column + '): ';
if (filename) {
errorLoc = path.relative(cwd, filename) + errorLoc;
}
var frame = codeFrame(src, err.lineNumber, err.column, { highlightCode: true });
throw new SyntaxError(errorLoc + err.message + '\n' + frame);
}
estraverse.traverse(parsedAst, {
enter: function(node, parentNode) {
var scope = scopeStack[scopeStack.length - 1];
if (node.type === 'VariableDeclaration') {
node.declarations.forEach(function(varDecl) {
if (varDecl.init && isRequireForAsyncLoader(varDecl.init)) {
scope[varDecl.id.name] = 'lasso-loader';
} else {
scope[varDecl.id.name] = true;
}
});
} else if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
scope = Object.create(scope);
node.params.forEach((param) => {
scope[param.name] = true;
});
if (node.type === 'FunctionDeclaration') {
scopeStack[scopeStack.length - 1][node.id.name] = true;
}
scopeStack.push(scope);
}
recordGlobals(node, parentNode, scope, foundGlobals);
var requirePath;
if (!scope.require && (isRequire(node) || isRequireResolve(node))) {
requirePath = node.arguments[0].value;
var range = node.range;
var firstArgRange = node.arguments[0].range;
if (asyncScopeStack.length) {
// We are in the scope of an async callback function so this
// is a dependency that will be lazily loaded
if (requirePath !== 'lasso-loader' && requirePath !== 'raptor-loader') {
var lastAsyncInfo = asyncScopeStack[asyncScopeStack.length - 1];
lastAsyncInfo.requires.push({
path: requirePath,
range: range,
argRange: firstArgRange
});
lastAsyncInfo.dependencies.push({
type: 'require',
path: requirePath
});
}
} else {
requires.push({
path: requirePath,
range: range,
argRange: firstArgRange
});
}
}
var asyncInfo;
if ((asyncInfo = parseAsyncNode(node, scopeStack[scopeStack.length - 1]))) {
curAsyncInfo = asyncInfo;
asyncBlocks.push(asyncInfo);
asyncStack.push(asyncInfo);
} else if (curAsyncInfo && node === curAsyncInfo.callbackNode) {
// We are in the scope of the async callback function so
// all dependencies below this will be async
asyncScopeStack.push(curAsyncInfo);
curAsyncInfo = null;
}
},
leave: function(node, parentNode) {
if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
scopeStack.pop();
}
if (asyncStack.length && node === asyncStack[asyncStack.length - 1].node) {
asyncStack.pop();
} else if (asyncScopeStack.length && node === asyncScopeStack[asyncScopeStack.length - 1].callbackNode) {
asyncScopeStack.pop();
}
}
});
asyncBlocks = asyncBlocks.map((asyncBlock) => {
delete asyncBlock.node;
delete asyncBlock.args;
delete asyncBlock.callbackNode;
return asyncBlock;
});
return {
requires: requires,
foundGlobals: foundGlobals,
asyncBlocks: asyncBlocks
};
};