const path = require('path') const t = require('@babel/types') const babelTraverse = require('@babel/traverse').default const generate = require('./generate') const uniI18n = require('@dcloudio/uni-cli-i18n') const { genCode, getCode, getForKey, traverseKey, isComponent } = require('../util') const { ATTE_DATA_CUSTOM_HIDDEN } = require('../constants') module.exports = function traverse (ast, state = {}) { babelTraverse(ast, { WithStatement (path) { state.ast = traverseExpr(path.node.body.body[0].argument, state) } }) initParent(state.ast) return state.ast } function initParent (ast, parentNode) { if (Array.isArray(ast)) { ast.forEach(node => initParent(node, parentNode)) } else if (typeof ast === 'object') { ast.parent = parentNode const vueId = ast.$vueId if (vueId) { const vuePid = getVueParentId(parentNode) if (vuePid) { ast.attr['vue-id'] = genCode( t.binaryExpression( '+', t.binaryExpression( '+', t.parenthesizedExpression(vueId), t.stringLiteral(',') ), t.parenthesizedExpression(vuePid) ) ) } } initParent(ast.children, ast) } } function getVueParentId (parentNode) { if (!parentNode) { return } return parentNode.$vueId || getVueParentId(parentNode.parent) } function traverseExpr (exprNode, state) { if (t.isCallExpression(exprNode)) { return traverseCallExpr(exprNode, state) } else if (t.isConditionalExpression(exprNode)) { return traverseConditionalExpr(exprNode, state) } else if (t.isArrayExpression(exprNode)) { return traverseArrayExpression(exprNode, state) } else if (t.isIdentifier(exprNode) && exprNode.name === 'undefined') { return { type: 'block', attr: {}, children: [] } } else if (t.isUnaryExpression(exprNode) && exprNode.operator === 'void') { return false } else { throw new Error(`暂不支持 ${getCode(exprNode)} 语法`) } } const traverses = { _c: traverseCreateElement, _t: traverseRenderSlot, _l: traverseRenderList, _u: traverseResolveScopedSlots, _v: traverseCreateTextVNode, _e: traverseCreateEmptyVNode, _g: '暂不支持 v-on="$listeners" 用法', _b: '暂不支持 v-bind="" 用法' } function traverseCallExpr (callExprNode, state) { const traverse = traverses[callExprNode.callee.name] if (!traverse) { throw new Error( `CallExpression ${callExprNode.callee.name} is not yet implemented` ) } else if (typeof traverse === 'string') { throw new Error(traverse) } return traverse(callExprNode, state) } function traverseConditionalExpr (conditionalExprNode, state) { const prefix = state.options.platform.directive const ret = [{ type: 'block', attr: { [prefix + 'if']: genCode(conditionalExprNode.test) }, children: normalizeChildren( traverseExpr(conditionalExprNode.consequent, state) ) }] if ( !( t.isCallExpression(conditionalExprNode.alternate) && t.isIdentifier(conditionalExprNode.alternate.callee) && conditionalExprNode.alternate.callee.name === '_e' ) ) { // test?_c():_e() ret.push({ type: 'block', attr: { [prefix + 'else']: '' }, children: normalizeChildren( traverseExpr(conditionalExprNode.alternate, state) ) }) } return ret } function traverseCreateElement (callExprNode, state) { const args = callExprNode.arguments const tagNode = args[0] if (!t.isStringLiteral(tagNode)) { throw new Error(`暂不支持动态组件[${tagNode.name}]`) } const node = { type: tagNode.value, attr: {}, children: [] } if (args.length < 2) { return node } const dataNodeOrChildNodes = args[1] if (t.isObjectExpression(dataNodeOrChildNodes)) { Object.assign(node.attr, traverseDataNode(dataNodeOrChildNodes, state, node)) } else { node.children = normalizeChildren(traverseExpr(dataNodeOrChildNodes, state)) } if (args.length < 3) { return node } const childNodes = args[2] if (!t.isNumericLiteral(childNodes)) { if (node.children && node.children.length) { node.children = node.children.concat(normalizeChildren(traverseExpr(childNodes, state))) } else { node.children = normalizeChildren(traverseExpr(childNodes, state)) } } return node } function traverseDataNode (dataNode, state, node) { const ret = {} const specialEvents = state.options.platform.specialEvents[node.type] || {} const specialEventNames = Object.keys(specialEvents) dataNode.properties.forEach(property => { switch (property.key.name) { case 'slot': ret.slot = genCode(property.value) break case 'scopedSlots': // Vue 2.6 property.value.$node = node node.children = normalizeChildren(traverseExpr(property.value, state)) break case 'attrs': case 'domProps': case 'on': case 'nativeOn': property.value.properties.forEach(attrProperty => { if (attrProperty.key.value === 'vue-id') { // initParent 时再处理 vue-id node.$vueId = attrProperty.value ret[attrProperty.key.value] = genCode(attrProperty.value) } else { if (specialEventNames.includes(attrProperty.key.value)) { if (t.isIdentifier(attrProperty.value)) { ret[specialEvents[attrProperty.key.value]] = attrProperty.value.name } } else { ret[attrProperty.key.value] = genCode(attrProperty.value) } } }) break case 'class': case 'staticClass': ret.class = genCode(property.value) break case 'style': case 'staticStyle': ret.style = genCode(property.value) break case 'directives': property.value.elements.find(objectExpression => { if (t.isObjectExpression(objectExpression)) { const nameProperty = objectExpression.properties[0] const isShowDir = nameProperty && nameProperty.key.name === 'name' && t.isStringLiteral(nameProperty.value) && nameProperty.value.value === 'show' if (isShowDir) { objectExpression.properties.find(valueProperty => { const isValue = valueProperty.key.name === 'value' if (isValue) { let key // 自定义组件不支持 hidden 属性 const platform = state.options.platform.name const platforms = ['mp-weixin', 'mp-qq', 'mp-jd', 'mp-toutiao', 'mp-lark'] if (isComponent(node.type) && platforms.includes(platform)) { // 字节跳动|飞书小程序自定义属性不会反应在DOM上,只能使用事件格式 key = `${platform === 'mp-toutiao' || platform === 'mp-lark' ? 'bind:-' : ''}${ATTE_DATA_CUSTOM_HIDDEN}` } else { key = 'hidden' } ret[key] = genCode(valueProperty.value, false, true) } return isValue }) } return isShowDir } }) break } }) return ret } function normalizeChildren (nodes) { if (!Array.isArray(nodes)) { nodes = [nodes] } return nodes.filter(node => { if (typeof node === 'string' && !node.trim()) { return false } return true }) } function traverseArrayExpression (arrayExprNodes, state) { return arrayExprNodes.elements.reduce((nodes, exprNode) => { return nodes.concat(traverseExpr(exprNode, state)) }, []) } function genSlotNode (slotName, slotNode, fallbackNodes, state) { if (!fallbackNodes || t.isNullLiteral(fallbackNodes)) { return slotNode } // 支付宝小程序默认插槽为 $default if (state.options.platform.name === 'mp-alipay') { slotName = slotName === 'default' ? '$default' : slotName } const prefix = state.options.platform.directive return [{ type: 'block', attr: { [prefix + 'if']: '{{$slots.' + slotName + '}}' }, children: [].concat(slotNode) }, { type: 'block', attr: { [prefix + 'else']: '' }, children: normalizeChildren( traverseExpr(fallbackNodes, state) ) }] } function traverseRenderSlot (callExprNode, state) { if (!t.isStringLiteral(callExprNode.arguments[0])) { state.errors.add(uniI18n.__('templateCompiler.notSupportDynamicSlotName', { 0: 'v-slot' })) return } const slotName = callExprNode.arguments[0].value let deleteSlotName = false // 标记是否组件 slot 手动指定了 name="default" if (state.options.scopedSlotsCompiler !== 'augmented' && callExprNode.arguments.length > 2) { // 作用域插槽 const props = {} callExprNode.arguments[2].properties.forEach(property => { props[property.key.value] = genCode(property.value) }) deleteSlotName = props.SLOT_DEFAULT && Object.keys(props).length === 1 if (!deleteSlotName) { delete props.SLOT_DEFAULT return genSlotNode( slotName, state.options.platform.createScopedSlots(slotName, props, state), callExprNode.arguments[1], state ) } } const node = { type: 'slot', attr: { name: slotName }, children: [] } if (deleteSlotName) { delete node.attr.name } return genSlotNode(slotName, node, callExprNode.arguments[1], state) } function traverseResolveScopedSlots (callExprNode, state) { function single (children, slotName, ignore) { if (Array.isArray(children) && children.length === 1) { const child = children[0] if (!child.type) { return } if (ignore.includes(child.type)) { return single(child.children, slotName, ignore) } child.attr = child.attr || {} child.attr.slot = slotName return true } } return callExprNode.arguments[0].elements.map(slotNode => { let keyProperty = false let fnProperty = false let proxyProperty = false slotNode.properties.forEach(property => { switch (property.key.name) { case 'key': keyProperty = property break case 'fn': fnProperty = property break case 'proxy': proxyProperty = property } }) const slotName = keyProperty.value.value const returnExprNodes = fnProperty.value.body.body[0].argument const parentNode = callExprNode.$node if (slotNode.scopedSlotsCompiler !== 'augmented' && !proxyProperty) { const resourcePath = state.options.resourcePath const ownerName = path.basename(resourcePath, path.extname(resourcePath)) const parentName = parentNode.type const paramExprNode = fnProperty.value.params[0] return state.options.platform.resolveScopedSlots( slotName, { genCode, generate, ownerName, parentName, parentNode, resourcePath, paramExprNode, returnExprNodes, traverseExpr: function (exprNode, state) { const ast = traverseExpr(exprNode, state) initParent(ast) return ast }, normalizeChildren }, state ) } if (state.options.scopedSlotsCompiler === 'auto' && slotNode.scopedSlotsCompiler === 'augmented') { parentNode.attr['scoped-slots-compiler'] = 'augmented' } const children = normalizeChildren(traverseExpr(returnExprNodes, state)) // 除百度、字节外其他小程序仅默认插槽可以支持多个节点 if (single(children, slotName, ['template', 'block'])) { return children[0] } return { type: 'block', attr: { slot: slotName }, children } }) } function traverseRenderList (callExprNode, state) { const params = callExprNode.arguments[1].params const forItem = params.length > 0 ? params[0].name : 'item' const forIndex = params.length > 1 ? params[1].name : '' const forReturnStatementArgument = callExprNode.arguments[1].body.body[0].argument const forKey = traverseKey(forReturnStatementArgument, state) const prefix = state.options.platform.directive const isBaidu = state.options.platform.name === 'mp-baidu' let forValue = genCode(callExprNode.arguments[0], isBaidu) if (isBaidu && forKey) { forValue += ` trackBy ${getForKey(forKey, forIndex, state)}` } const attr = { [prefix + 'for']: forValue, [prefix + 'for-item']: forItem } if (forIndex) { attr[prefix + 'for-index'] = forIndex } if (forKey && !isBaidu) { const key = getForKey(forKey, forIndex, state) if (key) { attr[prefix + 'key'] = key } } const children = traverseExpr(forReturnStatementArgument, state) // 支付宝小程序在 block 标签上使用 key 时顺序不能保障 if (state.options.platform.name === 'mp-alipay' && t.isCallExpression(forReturnStatementArgument) && children && children.type) { children.attr = children.attr || {} Object.assign(children.attr, attr) return children } return { type: 'block', attr, children: normalizeChildren(children) } } function getLeftStringLiteral (expr) { if (t.isBinaryExpression(expr) && !expr.$toString) { return getLeftStringLiteral(expr.left) } else if (t.isStringLiteral(expr)) { return expr } } function trim (text, type) { // TODO 保留换行符? if (type === 'left') { text = text.trimLeft() } else if (type === 'right') { text = text.trimRight() } else { text = text.trim() } return text } function traverseCreateTextVNode (callExprNode, state) { // trimStart|Left and trimEnd|End const arg = callExprNode.arguments[0] if (t.isStringLiteral(arg)) { arg.value = trim(arg.value) } else if (t.isBinaryExpression(arg) && !arg.$toString) { // 非_s() // right const right = arg.right if (t.isStringLiteral(right)) { right.value = trim(right.value, 'right') } // left const left = getLeftStringLiteral(arg.left) if (left && left.value) { left.value = trim(left.value, 'left') } } if ( state.options.platform.name === 'mp-baidu' || state.options.platform.name === 'mp-qq' ) { const code = genCode(arg, false, false, false) if (code.indexOf('{{') === 0) { if (state.options.platform.name === 'mp-qq') { // 似乎百度也可以走该逻辑, 为了稳定性,仅限 qq return code.replace(/\\n/g, '\\\\n').replace(/\\t/g, '\\\\t') } return code.replace(/([^\\])\\n/g, '$1\\\\n').replace(/([^\\])\\t/g, '$1\\\\t') } return code } return genCode(arg, false, false, false).replace(/\\\\n/g, '\\n') } function traverseCreateEmptyVNode (callExprNode, state) { return '' }