wasm-metering
Version:
injects metering into webassembly binaries
301 lines (276 loc) • 10 kB
JavaScript
var convertSourceMap = require('convert-source-map')
var transformAst = require('transform-ast')
var through = require('through2')
var hyperx = require('hyperx')
var acorn = require('acorn')
var path = require('path')
var SVG_TAGS = require('./svg-tags')
var SUPPORTED_VIEWS = ['nanohtml', 'bel', 'yo-yo', 'choo', 'choo/html']
var DELIM = '~!@|@|@!~'
var VARNAME = 'nanohtml'
var SVGNS = 'http://www.w3.org/2000/svg'
var XLINKNS = '"http://www.w3.org/1999/xlink"'
var BOOL_PROPS = require('./bool-props').reduce(function (o, key) {
o[key] = 1
return o
}, {})
module.exports = function yoYoify (file, opts) {
if (/\.json$/.test(file)) return through()
var bufs = []
var viewVariables = []
var babelTemplateObjects = Object.create(null)
return through(write, end)
function write (buf, enc, next) {
bufs.push(buf)
next()
}
function end (cb) {
var src = Buffer.concat(bufs).toString('utf8')
var res
try {
res = transformAst(src, { ecmaVersion: 8, parser: acorn }, walk)
if (opts && opts._flags && opts._flags.debug) {
res = res.toString() + '\n' + convertSourceMap.fromObject(res.map).toComment() + '\n'
} else {
res = res.toString()
}
} catch (err) {
return cb(err)
}
this.push(res)
this.push(null)
}
function walk (node) {
if (isSupportedView(node)) {
if (node.arguments[0].value === 'bel' ||
node.arguments[0].value === 'choo/html' ||
node.arguments[0].value === 'nanohtml') {
// html and choo/html have no other exports that may be used
node.edit.update('{}')
}
if (node.parent.type === 'VariableDeclarator') {
viewVariables.push(node.parent.id.name)
}
}
if (node.type === 'VariableDeclarator' && node.init && BabelTemplateDefinition(node.init)) {
// Babel generates helper calls like
// _taggedTemplateLiteral(["<div","/>"], ["<div","/>"])
// The first parameter is the `cooked` template literal parts, and the second parameter is the `raw`
// template literal parts.
// We just pick the cooked parts.
babelTemplateObjects[node.id.name] = node.init.arguments[0]
}
if (node.type === 'TemplateLiteral' && node.parent.tag) {
var name = node.parent.tag.name || (node.parent.tag.object && node.parent.tag.object.name)
if (viewVariables.indexOf(name) !== -1) {
processNode(node.parent, [ node.quasis.map(cooked) ].concat(node.expressions.map(expr)))
}
}
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && viewVariables.indexOf(node.callee.name) !== -1) {
var template, expressions
if (node.arguments[0] && node.arguments[0].type === 'ArrayExpression') {
// Detect transpiled template strings like:
// html(["<div","/>"], {id: "test"})
// Emitted by Buble.
template = node.arguments[0].elements.map(function (part) { return part.value })
expressions = node.arguments.slice(1).map(expr)
processNode(node, [ template ].concat(expressions))
} else if (node.arguments[0] && node.arguments[0].type === 'Identifier') {
// Detect transpiled template strings like:
// html(_templateObject, {id: "test"})
// Emitted by Babel.
var templateObject = babelTemplateObjects[node.arguments[0].name]
template = templateObject.elements.map(function (part) { return part.value })
expressions = node.arguments.slice(1).map(expr)
processNode(node, [ template ].concat(expressions))
// Remove the _taggedTemplateLiteral helper call
templateObject.parent.edit.update('0')
}
}
}
}
function processNode (node, args) {
var resultArgs = []
var argCount = 0
var tagCount = 0
var needsAc = false
var needsSa = false
var hx = hyperx(function (tag, props, children) {
var res = []
var elname = VARNAME + tagCount
tagCount++
if (tag === '!--') {
return DELIM + [elname, 'var ' + elname + ' = document.createComment(' + JSON.stringify(props.comment) + ')', null].join(DELIM) + DELIM
}
// Whether this element needs a namespace
var namespace = props.namespace
if (!namespace && SVG_TAGS.indexOf(tag) !== -1) {
namespace = SVGNS
}
// Create the element
if (namespace) {
res.push('var ' + elname + ' = document.createElementNS(' + JSON.stringify(namespace) + ', ' + JSON.stringify(tag) + ')')
} else {
res.push('var ' + elname + ' = document.createElement(' + JSON.stringify(tag) + ')')
}
function addAttr (to, key, val) {
// Normalize className
if (key.toLowerCase() === '"classname"') {
key = '"class"'
}
// The for attribute gets transformed to htmlFor, but we just set as for
if (key === '"htmlFor"') {
key = '"for"'
}
// If a property is boolean, set itself to the key
if (BOOL_PROPS[key.slice(1, -1)]) {
if (val.slice(0, 9) === 'arguments') {
if (namespace) {
res.push('if (' + val + ' && ' + key + ') ' + to + '.setAttributeNS(null, ' + key + ', ' + key + ')')
} else {
res.push('if (' + val + ' && ' + key + ') ' + to + '.setAttribute(' + key + ', ' + key + ')')
}
return
} else {
if (val === 'true') val = key
else if (val === 'false') return
}
}
if (key.slice(1, 3) === 'on') {
res.push(to + '[' + key + '] = ' + val)
} else {
if (key === '"xlink:href"') {
res.push(to + '.setAttributeNS(' + XLINKNS + ', ' + key + ', ' + val + ')')
} else if (namespace && key.slice(0, 1) === '"') {
if (!/^xmlns($|:)/i.test(key.slice(1, -1))) {
// skip xmlns definitions
res.push(to + '.setAttributeNS(null, ' + key + ', ' + val + ')')
}
} else if (namespace) {
res.push('if (' + key + ') ' + to + '.setAttributeNS(null, ' + key + ', ' + val + ')')
} else if (key.slice(0, 1) === '"') {
res.push(to + '.setAttribute(' + key + ', ' + val + ')')
} else {
needsSa = true
res.push('sa(' + to + ', ' + key + ', ' + val + ')')
}
}
}
// Add properties to element
Object.keys(props).forEach(function (key) {
var prop = props[key]
var ksrcs = getSourceParts(key)
var srcs = getSourceParts(prop)
var k, val
if (srcs) {
val = ''
srcs.forEach(function (src, index) {
if (src.arg) {
if (index > 0) val += ' + '
if (src.before) val += JSON.stringify(src.before) + ' + '
val += 'arguments[' + argCount + ']'
if (src.after) val += ' + ' + JSON.stringify(src.after)
resultArgs.push(src.arg)
argCount++
}
})
} else {
val = JSON.stringify(prop)
}
if (ksrcs) {
k = ''
ksrcs.forEach(function (src, index) {
if (src.arg) {
if (index > 0) val += ' + '
if (src.before) val += JSON.stringify(src.before) + ' + '
k += 'arguments[' + argCount + ']'
if (src.after) k += ' + ' + JSON.stringify(src.after)
resultArgs.push(src.arg)
argCount++
}
})
} else {
k = JSON.stringify(key)
}
addAttr(elname, k, val)
})
if (Array.isArray(children)) {
var childs = []
children.forEach(function (child) {
var srcs = getSourceParts(child)
if (srcs) {
var src = srcs[0]
if (src.src) {
res.push(src.src)
}
if (src.name) {
childs.push(src.name)
}
if (src.arg) {
var argname = 'arguments[' + argCount + ']'
resultArgs.push(src.arg)
argCount++
childs.push(argname)
}
} else {
childs.push(JSON.stringify(child))
}
})
if (childs.length > 0) {
needsAc = true
res.push('ac(' + elname + ', [' + childs.join(',') + '])')
}
}
// Return delim'd parts as a child
return DELIM + [elname, res.join('\n'), null].join(DELIM) + DELIM
}, { comments: true })
// Run through hyperx
var res = hx.apply(null, args)
// Pull out the final parts and wrap in a closure with arguments
var src = getSourceParts(res)
if (src && src[0].src) {
var params = resultArgs.join(',')
node.edit.update('(function () {' +
(needsAc ? '\n var ac = require(\'' + path.resolve(__dirname, 'append-child.js').replace(/\\/g, '\\\\') + '\')' : '') +
(needsSa ? '\n var sa = require(\'' + path.resolve(__dirname, 'set-attribute.js').replace(/\\/g, '\\\\') + '\')' : '') +
'\n ' + src[0].src + '\n return ' + src[0].name + '\n }(' + params + '))')
}
}
function isSupportedView (node) {
return (node.type === 'CallExpression' &&
node.callee && node.callee.name === 'require' &&
node.arguments.length === 1 &&
SUPPORTED_VIEWS.indexOf(node.arguments[0].value) !== -1)
}
function BabelTemplateDefinition (node) {
return node.type === 'CallExpression' &&
node.callee.type === 'Identifier' && node.callee.name === '_taggedTemplateLiteral'
}
function cooked (node) { return node.value.cooked }
function expr (ex, idx) {
return DELIM + [null, null, ex.source()].join(DELIM) + DELIM
}
function getSourceParts (str) {
if (typeof str !== 'string') return false
if (str.indexOf(DELIM) === -1) return false
var parts = str.split(DELIM)
var chunk = parts.splice(0, 5)
var arr = [{
before: chunk[0],
name: chunk[1],
src: chunk[2],
arg: chunk[3],
after: chunk[4]
}]
while (parts.length > 0) {
chunk = parts.splice(0, 4)
arr.push({
before: '',
name: chunk[0],
src: chunk[1],
arg: chunk[2],
after: chunk[3]
})
}
return arr
}