var through = require('through2'); var Readable = require('readable-stream').Readable; var concat = require('concat-stream'); var duplexer = require('duplexer2'); var acorn = require('acorn-node'); var walkAst = require('acorn-node/walk').full; var scan = require('scope-analyzer'); var unparse = require('escodegen').generate; var inspect = require('object-inspect'); var evaluate = require('static-eval'); var copy = require('shallow-copy'); var has = require('has'); var MagicString = require('magic-string'); var convertSourceMap = require('convert-source-map'); var mergeSourceMap = require('merge-source-map'); module.exports = function parse (modules, opts) { if (!opts) opts = {}; var vars = opts.vars || {}; var varModules = opts.varModules || {}; var parserOpts = copy(opts.parserOpts || {}); var updates = []; var moduleBindings = []; var sourcemapper; var inputMap; var output = through(); var body, ast; return duplexer(concat({ encoding: 'buffer' }, function (buf) { try { body = buf.toString('utf8').replace(/^#!/, '//#!'); var matches = false; for (var key in modules) { if (body.indexOf(key) !== -1) { matches = true; break; } } if (!matches) { // just pass it through output.end(buf); return; } if (opts.sourceMap) { inputMap = convertSourceMap.fromSource(body); if (inputMap) inputMap = inputMap.toObject(); body = convertSourceMap.removeComments(body); sourcemapper = new MagicString(body); } ast = acorn.parse(body, parserOpts); // scan.crawl does .parent tracking, so we can use acorn's builtin walker. scan.crawl(ast); walkAst(ast, walk); } catch (err) { return error(err) } finish(body); }), output); function finish (src) { var pos = 0; src = String(src); moduleBindings.forEach(function (binding) { if (binding.isReferenced()) { return; } var node = binding.initializer; if (node.type === 'VariableDeclarator') { var i = node.parent.declarations.indexOf(node); if (node.parent.declarations.length === 1) { // remove the entire declaration updates.push({ start: node.parent.start, offset: node.parent.end - node.parent.start, stream: st() }); } else if (i === node.parent.declarations.length - 1) { updates.push({ // remove ", a = 1" start: node.parent.declarations[i - 1].end, offset: node.end - node.parent.declarations[i - 1].end, stream: st() }); } else { updates.push({ // remove "a = 1, " start: node.start, offset: node.parent.declarations[i + 1].start - node.start, stream: st() }); } } else if (node.parent.type === 'SequenceExpression' && node.parent.expressions.length > 1) { var i = node.parent.expressions.indexOf(node); if (i === node.parent.expressions.length - 1) { updates.push({ // remove ", a = 1" start: node.parent.expressions[i - 1].end, offset: node.end - node.parent.expressions[i - 1].end, stream: st() }); } else { updates.push({ // remove "a = 1, " start: node.start, offset: node.parent.expressions[i + 1].start - node.start, stream: st() }); } } else { if (node.parent.type === 'ExpressionStatement') node = node.parent; updates.push({ start: node.start, offset: node.end - node.start, stream: st() }); } }); updates.sort(function(a, b) { return a.start - b.start; }); (function next () { if (updates.length === 0) return done(); var s = updates.shift(); output.write(src.slice(pos, s.start)); pos = s.start + s.offset; s.stream.pipe(output, { end: false }); if (opts.sourceMap) { s.stream.pipe(concat({ encoding: 'string' }, function (chunk) { // We have to give magic-string the replacement string, // so it can calculate the amount of lines and columns. if (s.offset === 0) { sourcemapper.appendRight(s.start, chunk); } else { sourcemapper.overwrite(s.start, s.start + s.offset, chunk); } })).on('finish', next); } else { s.stream.on('end', next); } })(); function done () { output.write(src.slice(pos)); if (opts.sourceMap) { var map = sourcemapper.generateMap({ source: opts.inputFilename || 'input.js', includeContent: true }); if (inputMap) { var merged = mergeSourceMap(inputMap, map); output.write('\n' + convertSourceMap.fromObject(merged).toComment() + '\n'); } else { output.write('\n//# sourceMappingURL=' + map.toUrl() + '\n'); } } output.end(); } } function error (msg) { var err = typeof msg === 'string' ? new Error(msg) : msg; output.emit('error', err); } function walk (node) { if (opts.sourceMap) { sourcemapper.addSourcemapLocation(node.start); sourcemapper.addSourcemapLocation(node.end); } var isreq = isRequire(node); var isreqm = false, isreqv = false, reqid; if (isreq) { reqid = node.arguments[0].value; isreqm = has(modules, reqid); isreqv = has(varModules, reqid); } if (isreqv && node.parent.type === 'VariableDeclarator' && node.parent.id.type === 'Identifier') { var binding = scan.getBinding(node.parent.id); if (binding) binding.value = varModules[reqid]; } else if (isreqv && node.parent.type === 'AssignmentExpression' && node.parent.left.type === 'Identifier') { var binding = scan.getBinding(node.parent.left); if (binding) binding.value = varModules[reqid]; } else if (isreqv && node.parent.type === 'MemberExpression' && isStaticProperty(node.parent.property) && node.parent.parent.type === 'VariableDeclarator' && node.parent.parent.id.type === 'Identifier') { var binding = scan.getBinding(node.parent.parent.id); var v = varModules[reqid][resolveProperty(node.parent.property)]; if (binding) binding.value = v; } else if (isreqv && node.parent.type === 'MemberExpression' && node.parent.property.type === 'Identifier') { //vars[node.parent.parent.id.name] = varModules[reqid]; } else if (isreqv && node.parent.type === 'CallExpression') { // } if (isreqm && node.parent.type === 'VariableDeclarator' && node.parent.id.type === 'Identifier') { var binding = scan.getBinding(node.parent.id); if (binding) { binding.module = modules[reqid]; binding.initializer = node.parent; binding.remove(node.parent.id); moduleBindings.push(binding); } } else if (isreqm && node.parent.type === 'AssignmentExpression' && node.parent.left.type === 'Identifier') { var binding = scan.getBinding(node.parent.left); if (binding) { binding.module = modules[reqid]; binding.initializer = node.parent; binding.remove(node.parent.left); moduleBindings.push(binding); } } else if (isreqm && node.parent.type === 'MemberExpression' && isStaticProperty(node.parent.property) && node.parent.parent.type === 'VariableDeclarator' && node.parent.parent.id.type === 'Identifier') { var binding = scan.getBinding(node.parent.parent.id); if (binding) { binding.module = modules[reqid][resolveProperty(node.parent.property)]; binding.initializer = node.parent.parent; binding.remove(node.parent.parent.id); moduleBindings.push(binding); } } else if (isreqm && node.parent.type === 'MemberExpression' && isStaticProperty(node.parent.property)) { var name = resolveProperty(node.parent.property); var cur = copy(node.parent.parent); cur.callee = copy(node.parent.property); cur.callee.parent = cur; traverse(cur.callee, modules[reqid][name]); } else if (isreqm && node.parent.type === 'CallExpression') { var cur = copy(node.parent); var iname = Math.pow(16,8) * Math.random(); cur.callee = { type: 'Identifier', name: '_' + Math.floor(iname).toString(16), parent: cur }; traverse(cur.callee, modules[reqid]); } if (node.type === 'Identifier') { var binding = scan.getBinding(node) if (binding && binding.module) traverse(node, binding.module, binding); } } function traverse (node, val, binding) { for (var p = node; p; p = p.parent) { if (p.start === undefined || p.end === undefined) continue; } if (node.parent.type === 'CallExpression') { if (typeof val !== 'function') { return error( 'tried to statically call ' + inspect(val) + ' as a function' ); } var xvars = getVars(node.parent, vars); xvars[node.name] = val; var res = evaluate(node.parent, xvars); if (res !== undefined) { if (binding) binding.remove(node) updates.push({ start: node.parent.start, offset: node.parent.end - node.parent.start, stream: isStream(res) ? wrapStream(res) : st(String(res)) }); } } else if (node.parent.type === 'MemberExpression') { if (!isStaticProperty(node.parent.property)) { return error( 'dynamic property in member expression: ' + body.slice(node.parent.start, node.parent.end) ); } var cur = node.parent.parent; if (cur.type === 'MemberExpression') { cur = cur.parent; if (cur.type !== 'CallExpression' && cur.parent.type === 'CallExpression') { cur = cur.parent; } } if (node.parent.type === 'MemberExpression' && (cur.type !== 'CallExpression' && cur.type !== 'MemberExpression')) { cur = node.parent; } var xvars = getVars(cur, vars); xvars[node.name] = val; var res = evaluate(cur, xvars); if (res === undefined && cur.type === 'CallExpression') { // static-eval can't safely evaluate code with callbacks, so do it manually in a safe way var callee = evaluate(cur.callee, xvars); var args = cur.arguments.map(function (arg) { // Return a function stub for callbacks so that `static-module` users // can do `callback.toString()` and get the original source if (arg.type === 'FunctionExpression' || arg.type === 'ArrowFunctionExpression') { var fn = function () { throw new Error('static-module: cannot call callbacks defined inside source code'); }; fn.toString = function () { return body.slice(arg.start, arg.end); }; return fn; } return evaluate(arg, xvars); }); if (callee !== undefined) { try { res = callee.apply(null, args); } catch (err) { // Evaluate to undefined } } } if (res !== undefined) { if (binding) binding.remove(node) updates.push({ start: cur.start, offset: cur.end - cur.start, stream: isStream(res) ? wrapStream(res) : st(String(res)) }); } } else if (node.parent.type === 'UnaryExpression') { var xvars = getVars(node.parent, vars); xvars[node.name] = val; var res = evaluate(node.parent, xvars); if (res !== undefined) { if (binding) binding.remove(node) updates.push({ start: node.parent.start, offset: node.parent.end - node.parent.start, stream: isStream(res) ? wrapStream(res) : st(String(res)) }); } else { output.emit('error', new Error( 'unsupported unary operator: ' + node.parent.operator )); } } else { output.emit('error', new Error( 'unsupported type for static module: ' + node.parent.type + '\nat expression:\n\n ' + unparse(node.parent) + '\n' )); } } } function isRequire (node) { var c = node.callee; return c && node.type === 'CallExpression' && c.type === 'Identifier' && c.name === 'require' ; } function isStream (s) { return s && typeof s === 'object' && typeof s.pipe === 'function'; } function wrapStream (s) { if (typeof s.read === 'function') return s else return (new Readable).wrap(s) } function isStaticProperty(node) { return node.type === 'Identifier' || node.type === 'Literal'; } function resolveProperty(node) { return node.type === 'Identifier' ? node.name : node.value; } function st (msg) { var r = new Readable; r._read = function () {}; if (msg != null) r.push(msg); r.push(null); return r; } function nearestScope(node) { do { var scope = scan.scope(node); if (scope) return scope; } while ((node = node.parent)); } function getVars(node, vars) { var xvars = copy(vars || {}); var scope = nearestScope(node); if (scope) { scope.forEachAvailable(function (binding, name) { if (binding.hasOwnProperty('value')) xvars[name] = binding.value; }); } return xvars; }