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 { ATTR_DATA_CUSTOM_HIDDEN, ATTR_SLOT_ORIGIN } = 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') || t.isNullLiteral(conditionalExprNode.alternate) ) ) { // 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': // vue@2.7.0 https://github.com/vuejs/vue/pull/12195 已经修复这个问题(question/184192),后续升级vue版本后可以删除 if (property.key.name === 'staticClass' && property.value.value) { property.value.value = property.value.value.replace(/\s+/g, ' ').trim() } 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-xhs', 'mp-toutiao', 'mp-lark'] if (isComponent(node.type) && platforms.includes(platform)) { // 字节跳动|飞书小程序自定义属性不会反应在DOM上,只能使用事件格式 key = `${platform === 'mp-toutiao' || platform === 'mp-lark' ? 'bind:-' : ''}${ATTR_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, isStaticSlotName = true) { 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: { // 移除动态拼接的 index 部分 [prefix + 'if']: isStaticSlotName ? '{{$slots.' + slotName + '}}' : '{{$slots[' + slotName.replace(/^{{/, '').replace(/}}$/, '').replace(/\+\('\.'\+\S+?\)$/, '') + ']}}' }, children: [].concat(slotNode) }, { type: 'block', attr: { [prefix + 'else']: '' }, children: normalizeChildren( traverseExpr(fallbackNodes, state) ) }] } function traverseRenderSlot (callExprNode, state) { const slotNameNode = callExprNode.arguments[0] const isStaticSlotName = t.isStringLiteral(slotNameNode) const slotName = isStaticSlotName ? slotNameNode.value : genCode(slotNameNode) let deleteSlotName = false // 标记是否组件 slot 手动指定了 name="default" if (state.options.scopedSlotsCompiler !== 'augmented' && callExprNode.arguments.length > 2) { // 作用域插槽 const props = {} const arg2 = callExprNode.arguments[2] const arg3 = callExprNode.arguments[3] let bindings if (t.isObjectExpression(arg2)) { arg2.properties.forEach(property => { props[property.key.value] = genCode(property.value) }) } else if (arg3) { bindings = genCode(arg3) } deleteSlotName = props.SLOT_DEFAULT && Object.keys(props).length === 1 if (!deleteSlotName) { // TODO 非原生支持作用域插槽的平台在未启用增强的模式下也允许使用动态插槽名 if (!isStaticSlotName && !['mp-baidu', 'mp-alipay'].includes(state.options.platform.name)) { state.errors.add(uniI18n.__('templateCompiler.notSupportDynamicSlotName', { 0: 'v-slot' })) return } delete props.SLOT_DEFAULT return genSlotNode( slotName, state.options.platform.createScopedSlots(slotName, bindings || 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, isStaticSlotName) } function traverseResolveScopedSlots (callExprNode, state) { const options = state.options const prefix = options.platform.directive const platformName = options.platform.name const vIfAttrName = prefix + 'if' const vForAttrName = prefix + 'for' // 模板标签支持 slot 属性的平台 // 百度、字节小程序仅支持在根节点使用 slot 属性 const supportTemplateSlotPlatforms = ['mp-baidu', 'mp-toutiao'] // 支持访问当前节点 v-for 作用域的平台 const supportCurrentScopePlatforms = ['mp-weixin', 'mp-alipay'] function merge (node, ignore, vIfs = [], top, needRealNode) { if (!top) { // 支付宝小程序使用静态插槽时可以在非实体节点使用 slot 属性,其他小程序 named slot 需移动到实体节点 const slot = node.attr.slot needRealNode = slot && slot !== 'default' && !supportTemplateSlotPlatforms.includes(platformName) && !(platformName === 'mp-alipay' && !/\{\{.+?\}\}/.test(slot)) node = { children: [node] } top = node } let children = node.children let nodeAttr = node.attr || {} function resolveVIf () { if (vIfs.length) { // 简易合并 nodeAttr[vIfAttrName] = vIfs.length > 1 ? `{{${vIfs.map(str => str.replace(/^\{\{(.+)\}\}$/, '($1)')).join('&&')}}}` : vIfs[0] vIfs.length = 0 } } if (Array.isArray(children)) { children = children.filter(child => !!child) let slotNode if (children.length === 1) { let child = children[0] if (child.type) { const attr = child.attr || {} // 除 v-if 外与父节点无同名属性且当前节点无 v-for 作用域且父节点 v-for 支持访问当前节点作用域,向上合并 // TODO 父节点访问变量不与当前 v-for 作用域内变量同名时,可向上合并 if (!Object.keys(attr).find(key => key !== vIfAttrName && key in nodeAttr) && !attr[vForAttrName] && (supportCurrentScopePlatforms.includes(platformName) || !nodeAttr[vForAttrName])) { if (attr[vIfAttrName]) { vIfs.push(attr[vIfAttrName]) delete attr[vIfAttrName] } child.attr = nodeAttr = Object.assign(attr, nodeAttr) for (const key in child) { node[key] = child[key] } child = node } else { resolveVIf() } if (ignore.includes(child.type)) { return merge(child, ignore, vIfs, top, needRealNode) } else if (needRealNode) { slotNode = child } } else if (needRealNode) { node.type = 'text' slotNode = node } } else if (needRealNode) { // TODO 依据子节点类型 node.type = 'view' slotNode = node } if (slotNode && slotNode !== top) { // TODO 多层 v-for 嵌套时,此处理导致作用域发生变化,需安全重命名 slot name ['slot', 'slot-scope'].forEach(key => { const topAttr = top.attr if (key in topAttr) { slotNode.attr[key] = topAttr[key] delete topAttr[key] } }) } } resolveVIf() return top } return callExprNode.arguments[0].elements.map(slotNode => { let keyProperty = false let fnProperty = false let proxyProperty = false let vIfNode let vForNode // TODO v-else if (t.isConditionalExpression(slotNode)) { // vIfCode = genCode(slotNode.test) vIfNode = t.cloneNode(slotNode, true) slotNode = slotNode.consequent } if (t.isCallExpression(slotNode)) { vForNode = t.cloneNode(slotNode, true) slotNode = slotNode.arguments[1].body.body[0].argument } slotNode.properties.forEach(property => { switch (property.key.name) { case 'key': keyProperty = property break case 'fn': fnProperty = property break case 'proxy': proxyProperty = property } }) const slotNameNode = keyProperty.value const isStaticSlotName = t.isStringLiteral(slotNameNode) const slotName = isStaticSlotName ? slotNameNode.value : genCode(slotNameNode) // 移除动态拼接的 index 部分 // TODO 动态 slotName 如使用到 v-for 作用域变量,输出固定名称 $dynamic const slotNameOrigin = isStaticSlotName ? slotName : slotName.replace(/\+\('\.'\+\S+?\)\}\}$/, '}}') let returnExprNodes = fnProperty.value.body.body[0].argument if (vForNode) { vForNode.arguments[1].body.body[0].argument = returnExprNodes returnExprNodes = vForNode } if (vIfNode) { vIfNode.consequent = returnExprNodes returnExprNodes = vIfNode } const parentNode = callExprNode.$node if (options.scopedSlotsCompiler !== 'augmented' && slotNode.scopedSlotsCompiler !== 'augmented' && !proxyProperty) { // 暂不处理旧版编译模式对于动态 slotName 的处理 const resourcePath = options.resourcePath const ownerName = path.basename(resourcePath, path.extname(resourcePath)) const parentName = parentNode.type const paramExprNode = fnProperty.value.params[0] const node = 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 (['mp-baidu', 'mp-alipay'].includes(platformName)) { node.attr[ATTR_SLOT_ORIGIN] = slotNameOrigin return merge(node, ['template', 'block']) } return node } if (options.scopedSlotsCompiler === 'auto' && slotNode.scopedSlotsCompiler === 'augmented') { parentNode.attr['scoped-slots-compiler'] = 'augmented' } // 除百度、字节外其他小程序仅默认插槽可以支持多个节点 return merge({ type: 'block', children: normalizeChildren(traverseExpr(returnExprNodes, state)), attr: { slot: slotName, [ATTR_SLOT_ORIGIN]: slotNameOrigin } }, ['template', 'block']) }) } 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 '' }