112 lines
3.4 KiB
JavaScript
112 lines
3.4 KiB
JavaScript
'use strict';
|
|
|
|
var detect = require('acorn-globals');
|
|
var acorn = require('acorn');
|
|
var walk = require('acorn/dist/walk');
|
|
|
|
// hacky fix for https://github.com/marijnh/acorn/issues/227
|
|
function reallyParse(source) {
|
|
return acorn.parse(source, {
|
|
ecmaVersion: 6,
|
|
allowReturnOutsideFunction: true
|
|
});
|
|
}
|
|
|
|
module.exports = addWith
|
|
|
|
/**
|
|
* Mimic `with` as far as possible but at compile time
|
|
*
|
|
* @param {String} obj The object part of a with expression
|
|
* @param {String} src The body of the with expression
|
|
* @param {Array.<String>} exclude A list of variable names to explicitly exclude
|
|
*/
|
|
function addWith(obj, src, exclude) {
|
|
obj = obj + ''
|
|
src = src + ''
|
|
exclude = exclude || []
|
|
exclude = exclude.concat(detect(obj).map(function (global) { return global.name; }))
|
|
var vars = detect(src).map(function (global) { return global.name; })
|
|
.filter(function (v) {
|
|
return exclude.indexOf(v) === -1
|
|
&& v !== 'undefined'
|
|
&& v !== 'this'
|
|
})
|
|
|
|
if (vars.length === 0) return src
|
|
|
|
var declareLocal = ''
|
|
var local = 'locals_for_with'
|
|
var result = 'result_of_with'
|
|
if (/^[a-zA-Z0-9$_]+$/.test(obj)) {
|
|
local = obj
|
|
} else {
|
|
while (vars.indexOf(local) != -1 || exclude.indexOf(local) != -1) {
|
|
local += '_'
|
|
}
|
|
declareLocal = 'var ' + local + ' = (' + obj + ')'
|
|
}
|
|
while (vars.indexOf(result) != -1 || exclude.indexOf(result) != -1) {
|
|
result += '_'
|
|
}
|
|
|
|
var inputVars = vars.map(function (v) {
|
|
return JSON.stringify(v) + ' in ' + local + '?' +
|
|
local + '.' + v + ':' +
|
|
'typeof ' + v + '!=="undefined"?' + v + ':undefined'
|
|
})
|
|
|
|
src = '(function (' + vars.join(', ') + ') {' +
|
|
src +
|
|
'}.call(this' + inputVars.map(function (v) { return ',' + v; }).join('') + '))'
|
|
|
|
return ';' + declareLocal + ';' + unwrapReturns(src, result) + ';'
|
|
}
|
|
|
|
/**
|
|
* Take a self calling function, and unwrap it such that return inside the function
|
|
* results in return outside the function
|
|
*
|
|
* @param {String} src Some JavaScript code representing a self-calling function
|
|
* @param {String} result A temporary variable to store the result in
|
|
*/
|
|
function unwrapReturns(src, result) {
|
|
var originalSource = src
|
|
var hasReturn = false
|
|
var ast = reallyParse(src)
|
|
var ref
|
|
src = src.split('')
|
|
|
|
// get a reference to the function that was inserted to add an inner context
|
|
if ((ref = ast.body).length !== 1
|
|
|| (ref = ref[0]).type !== 'ExpressionStatement'
|
|
|| (ref = ref.expression).type !== 'CallExpression'
|
|
|| (ref = ref.callee).type !== 'MemberExpression' || ref.computed !== false || ref.property.name !== 'call'
|
|
|| (ref = ref.object).type !== 'FunctionExpression')
|
|
throw new Error('AST does not seem to represent a self-calling function')
|
|
var fn = ref
|
|
|
|
walk.recursive(ast, null, {
|
|
Function: function (node, st, c) {
|
|
if (node === fn) {
|
|
c(node.body, st, "ScopeBody");
|
|
}
|
|
},
|
|
ReturnStatement: function (node) {
|
|
hasReturn = true;
|
|
replace(node, 'return {value: (' + (node.argument ? source(node.argument) : 'undefined') + ')};');
|
|
}
|
|
});
|
|
function source(node) {
|
|
return src.slice(node.start, node.end).join('')
|
|
}
|
|
function replace(node, str) {
|
|
for (var i = node.start; i < node.end; i++) {
|
|
src[i] = ''
|
|
}
|
|
src[node.start] = str
|
|
}
|
|
if (!hasReturn) return originalSource
|
|
else return 'var ' + result + '=' + src.join('') + ';if (' + result + ') return ' + result + '.value'
|
|
}
|