first commit

This commit is contained in:
2026-09-08 20:28:54 +08:00
commit 2ada8d3d5a
37380 changed files with 4886169 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
const {
ID,
isVar
} = require('./util')
const {
isComponent
} = require('../util')
let isPlatformReservedTag
function no (a, b, c) {
return false
}
function isBuiltInTag (tag) {
if (
tag === 'slot' ||
tag === 'component' ||
tag === 'keep-alive'
) {
return true
}
}
function isStatic (node) {
if (node.type === 2) {
return false
}
if (node.type === 3) {
return true
}
if (node.staticClass || node.classBinding || node.styleBinding) {
return false
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag)
))
}
function markStatic (node) {
const isStaticNode = isStatic(node)
if (isStaticNode) { // 静态节点且仅包含 ID 属性
if (
node.attrs &&
node.attrs.length === 1 &&
!node.key &&
!node.ref &&
!node.slotTarget
) {
node.plain = true
}
}
if (node.type === 1) {
// 需要保留 staticClass , selectComponent,externalClasses
// delete node.staticClass
delete node.staticStyle
const isCustomComponent = isComponent(node.tag)
if (node.attrs && !isCustomComponent && node.tag !== 'keep-alive') { // 移除静态属性
// 保留 id 属性, selectComponent 需要使用
node.attrs = node.attrs.filter(attr => {
const {
name,
value
} = attr
return name === 'id' ||
name === ID ||
// name.indexOf('data-') === 0 || // TODO dataset
isVar(value)
})
}
node.children = node.children.filter(child => { // 移除静态文本
if (child.type === 3) { // ASTText
if (!isCustomComponent) {
return false
}
child.text = '' // slot <custom>ABCD</custom>
}
return true
})
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
}
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
}
}
}
if (isStaticNode) { // 静态节点且仅包含 ID 属性
if (!node.attrsMap || !node.attrsMap.id) { // 保留 id 属性, selectComponent 需要使用
// 在 vue2.0 中 https://github.com/fxy060608/vue/blob/app-service/src/core/vdom/patch.js#L41
// sameVnode 中会对比vnode的data,如果一个有值,一个没值,会触发新增逻辑
// app端service和view编译时均会为每个节点生成_i属性,但service层为了优化包体积,性能,会对静态节点移除_i属性
// 如果app-service中优化移除了_i属性,而app-view中又保留了,就导致两者运行时的逻辑不一样
// <view v-if="true" @click="click"><custom/></view><view v-else><custom/></view>
// 上述写法,第一个view始终有data{onClick:click});第二个viewservice层如果移除_i,则没有data,而view层会保留_i,又有data
// 导致:app-service触发的是custom create逻辑、而app-view触发了custom update逻辑
// 故:service层也不应该移除_i属性,但为了影响范围小一些,目前仅在if/for等条件节点上启用此逻辑,确保此情况下service和view的data均存在
if ((node.for || node.if || node.else || node.elseif) && node.children && node.children.length) {
if (!node.plain) { // 已经包含了其他data属性,不需要attrs来激活data,可以删除
delete node.attrs
} else { // 如果plain为true,需要调整为false,否则generate时会忽略data的生成
node.plain = false
}
} else {
delete node.attrs
}
}
}
}
module.exports = function optimize (root, options) {
isPlatformReservedTag = options.isReservedTag || no
markStatic(root)
}
@@ -0,0 +1,124 @@
const {
ID,
C_IS,
C_REF,
C_NAME,
V_IF,
V_FOR,
V_ELSE_IF,
isVar
} = require('../util')
const parseTextExpr = require('./text-parser')
function parseRef (el, genVar) {
if (el.ref && isVar(el.ref)) {
el.ref = genVar(C_REF, el.ref)
}
}
function parseSlotName (el, genVar) {
if (el.slotName && isVar(el.slotName)) {
el.slotName = genVar(C_NAME, el.slotName)
}
}
function parseIs (el, genVar) {
if (!el.component) {
return
}
if (isVar(el.component)) {
el.component = genVar(C_IS, el.component)
}
}
function isProcessed (exp) {
return String(exp).indexOf('_$') === 0
}
// 当根节点是由if,elseif,else组成,会调用多次parseIf来解析root
function parseIf (el, createGenVar, isScopedSlot) {
if (!el.if) {
return
}
if (el.slotTarget && el.tag === 'template') { // new v-slot
isScopedSlot = false
}
el.ifConditions.forEach(con => {
if (!isProcessed(con.exp) && isVar(con.exp)) {
con.exp = createGenVar(con.block.attrsMap[ID], isScopedSlot)(con.block.elseif ? V_ELSE_IF : V_IF, con.exp)
}
})
if (!isProcessed(el.if)) {
el.if = createGenVar(el.attrsMap[ID], isScopedSlot)(V_IF, el.if)
}
}
function parseFor (el, createGenVar, isScopedSlot, fill = false) {
if (el.for && isVar(el.for)) {
el.for = createGenVar(el.forId, isScopedSlot)(
V_FOR,
fill
? `{forItems:${el.for},fill:true}`
: `{forItems:${el.for}}`
)
return true
}
}
function parseBinding (el, genVar) {
el.staticClass && (el.staticClass = genVar('sc', el.staticClass))
el.classBinding && (el.classBinding = genVar('c', el.classBinding))
el.styleBinding && (el.styleBinding = genVar('s', el.styleBinding))
}
function parseDirs (el, genVar, ignoreDirs = []) {
el.directives && el.directives.forEach(dir => {
if (ignoreDirs.indexOf(dir.name) === -1) {
dir.value && (dir.value = genVar('v-' + dir.name, dir.value))
dir.isDynamicArg && (dir.arg = genVar('v-' + dir.name + '-arg', dir.arg))
}
})
}
function parseAttrs (el, genVar) {
el.attrs && el.attrs.forEach(attr => {
const {
name,
value
} = attr
if (
name !== ID &&
// name.indexOf('data-') !== 0 && // TODO dataset 保留
name.indexOf('change:') !== 0 && // wxs change:prop
isVar(value) &&
value.indexOf('_$') !== 0 // 已被提前处理过了,如 wxs prop:_$gc(2,'change:prop')
) {
attr.value = genVar('a-' + name, value)
}
})
}
function parseProps (el, genVar) {
el.props && el.props.forEach(prop => {
isVar(prop.value) && (prop.value = genVar('a-' + prop.name, prop.value))
})
}
function parseText (el, parent, state) {
// fixed by xxxxxx 注意:保持平台一致性,trim 一下
el.parent && (el.parent = parent)
el.expression = parseTextExpr(el.text.trim(), false, state).expression
}
module.exports = {
parseIs,
parseRef,
parseSlotName,
parseIf,
parseFor,
parseText,
parseDirs,
parseAttrs,
parseProps,
parseBinding
}
@@ -0,0 +1,30 @@
const {
ID,
hasOwn,
addRawAttr
} = require('../util')
module.exports = function parseBlock (el, parent) {
if (el.tag === 'template' && !hasOwn(el.attrsMap, ID)) {
/**
* <current-user v-slot="{ user }">
* {{ user.firstName }}
* </current-user>
*/
addRawAttr(el, ID, parent.attrsMap[ID])
} else if (el.tag === 'block') {
el.tag = 'template'
const vForKey = el.key
if (vForKey) {
delete el.key
el.children.forEach((childEl, index) => {
const childVForKey = childEl.key
if (childVForKey) {
childEl.key = `${childVForKey}+'_'+${vForKey}+'_${index}'`
} else {
childEl.key = `${vForKey}+'_${index}'`
}
})
}
}
}
@@ -0,0 +1,20 @@
const {
ID,
elements
} = require('../util')
const {
isComponent
} = require('../../util')
// 仅限 view 层
module.exports = function parseComponent (el) {
// 需要把自定义组件的 attrs, props 全干掉
if (el.tag && !elements.includes(el.tag) && isComponent(el.tag)) {
// 仅保留 id、ID、data
el.attrs && (el.attrs = el.attrs.filter(attr => {
const name = attr.name
return name === 'id' || name === ID || name.indexOf('data-') === 0
}))
}
}
@@ -0,0 +1,29 @@
const deprecated = {
events: {
tap: 'click',
longtap: 'longpress'
}
}
module.exports = function parseEvent (el) {
if (el.events || el.nativeEvents) {
const {
events: eventsMap
} = deprecated
normalizeEvent(el.events, eventsMap)
normalizeEvent(el.nativeEvents, eventsMap)
}
}
function normalizeEvent (events, eventsMap) {
if (!events) {
return
}
Object.keys(events).forEach(name => {
// 过时事件类型转换
if (eventsMap[name]) {
events[eventsMap[name]] = events[name]
delete events[name]
// warnLogs.add(`警告:事件${name}已过时,推荐使用${eventsMap[name]}代替`)
}
})
}
@@ -0,0 +1,115 @@
/* @flow */
const validDivisionCharRE = /[\w).+\-_$\]]/
module.exports = function parseFilters (exp) {
let inSingle = false
let inDouble = false
let inTemplateString = false
let inRegex = false
let curly = 0
let square = 0
let paren = 0
let lastFilterIndex = 0
let c, prev, i, expression, filters
for (i = 0; i < exp.length; i++) {
prev = c
c = exp.charCodeAt(i)
if (inSingle) {
if (c === 0x27 && prev !== 0x5C) inSingle = false
} else if (inDouble) {
if (c === 0x22 && prev !== 0x5C) inDouble = false
} else if (inTemplateString) {
if (c === 0x60 && prev !== 0x5C) inTemplateString = false
} else if (inRegex) {
if (c === 0x2f && prev !== 0x5C) inRegex = false
} else if (
c === 0x7C && // pipe
exp.charCodeAt(i + 1) !== 0x7C &&
exp.charCodeAt(i - 1) !== 0x7C &&
!curly && !square && !paren
) {
if (expression === undefined) {
// first filter, end of expression
lastFilterIndex = i + 1
expression = exp.slice(0, i).trim()
} else {
pushFilter()
}
} else {
switch (c) {
case 0x22:
inDouble = true
break // "
case 0x27:
inSingle = true
break // '
case 0x60:
inTemplateString = true
break // `
case 0x28:
paren++
break // (
case 0x29:
paren--
break // )
case 0x5B:
square++
break // [
case 0x5D:
square--
break // ]
case 0x7B:
curly++
break // {
case 0x7D:
curly--
break // }
}
if (c === 0x2f) { // /
let j = i - 1
let p
// find first non-whitespace prev char
for (; j >= 0; j--) {
p = exp.charAt(j)
if (p !== ' ') break
}
if (!p || !validDivisionCharRE.test(p)) {
inRegex = true
}
}
}
}
if (expression === undefined) {
expression = exp.slice(0, i).trim()
} else if (lastFilterIndex !== 0) {
pushFilter()
}
function pushFilter () {
(filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim())
lastFilterIndex = i + 1
}
if (filters) {
for (i = 0; i < filters.length; i++) {
expression = wrapFilter(expression, filters[i])
}
}
return expression
}
function wrapFilter (exp, filter) {
const i = filter.indexOf('(')
if (i < 0) {
// _f: resolveFilter
return `_f("${filter}")(${exp})`
} else {
const name = filter.slice(0, i)
const args = filter.slice(i + 1)
return `_f("${name}")(${exp}${args !== ')' ? ',' + args : args}`
}
}
@@ -0,0 +1,19 @@
const {
hasOwn,
elements
} = require('../util')
const tags = require('@dcloudio/uni-cli-shared/lib/tags')
// 仅限 view 层
module.exports = function parseTag (el) {
const tag = el.tag
const element = elements.find(element => tag === element || 'uni-' + tag === element)
if (element) {
el.tag = element
return
}
if (el.tag.indexOf('v-uni-') !== 0 && hasOwn(tags, el.tag)) {
el.tag = 'v-uni-' + el.tag
}
}
@@ -0,0 +1,62 @@
/* @flow */
const parseFilters = require('./filter-parser')
function cached (fn) {
const cache = Object.create(null)
return function cachedFn (str) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}
}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
const regexEscapeRE = /[-.*+?^${}()|[\]/\\]/g
const buildRegex = cached(delimiters => {
const open = delimiters[0].replace(regexEscapeRE, '\\$&')
const close = delimiters[1].replace(regexEscapeRE, '\\$&')
return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})
module.exports = function parseText (
text,
delimiters,
state
) {
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if (!tagRE.test(text)) {
return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {
index = match.index
// push text token
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
if (!state.service) {
tokens.push(JSON.stringify(tokenValue))
}
}
// tag token
const exp = parseFilters(match[1].trim())
tokens.push(`(${state.genVar('t' + (state.childIndex) + '-' + (state.index++), '_s(' + exp + ')')})`)
rawTokens.push({
'@binding': exp
})
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
if (!state.service) {
tokens.push(JSON.stringify(tokenValue))
}
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
@@ -0,0 +1,56 @@
const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/
function isWxsEvent (expr, filterModules) {
return !!filterModules.find(name => expr.indexOf(name + '.') === 0)
}
function parseWxsViewEvent (expr, filterModules) {
if (!simplePathRE.test(expr)) {
return expr
}
if (isWxsEvent(expr, filterModules)) {
return `$event = $handleWxsEvent($event);${expr}($event, $getComponentDescriptor())`
}
return expr
}
module.exports = function parseWxsEvents (el, {
filterModules,
isAppService,
isAppView
}) {
if (!filterModules || !filterModules.length) {
return
}
if (!el.events) {
return
}
if (isAppService) {
Object.keys(el.events).forEach(name => {
const handlers = el.events[name]
if (Array.isArray(handlers)) {
el.events[name] = handlers.filter(handler => {
return !isWxsEvent(handler.value, filterModules)
})
if (!el.events[name].length) {
delete el.events[name]
}
} else {
if (isWxsEvent(handlers.value, filterModules)) {
delete el.events[name]
}
}
})
} else if (isAppView) {
Object.keys(el.events).forEach(name => {
const handlers = el.events[name]
if (Array.isArray(handlers)) {
handlers.forEach(handler => {
handler.value = parseWxsViewEvent(handler.value, filterModules)
})
} else {
handlers.value = parseWxsViewEvent(handlers.value, filterModules)
}
})
}
}
@@ -0,0 +1,56 @@
const {
ID,
SET_DATA,
GET_CHANGE_DATA
} = require('../util')
function isWxsChangeProp (attr) {
return attr.name.indexOf('change:') === 0
}
function createSetDataGenVar (id) {
return function genVar (name, value) {
return `${SET_DATA}(${id},'${name}',${value})`
}
}
function createGetChangeDataGenVar (id) {
return function genVar (name) {
return `${GET_CHANGE_DATA}(${id},'${name}')`
}
}
module.exports = function parseWxsProps (el, {
isAppService,
isAppView
}) {
if (!el.attrs) {
return
}
const wxsChangeProps = []
el.attrs = el.attrs.filter(attr => {
if (isWxsChangeProp(attr)) {
wxsChangeProps.push(attr.name.replace('change:', ''))
if (isAppService) { // service 层移除 change:prop
return false
}
}
return true
})
if (!wxsChangeProps.length) {
return
}
const genSetVar = createSetDataGenVar(el.attrsMap[ID])
const genGetVar = createGetChangeDataGenVar(el.attrsMap[ID])
el.attrs.forEach(attr => {
if (wxsChangeProps.includes(attr.name)) {
if (isAppService) {
attr.value = genSetVar('change:' + attr.value, attr.value)
} else if (isAppView) {
attr.value = genGetVar('change:' + attr.value)
}
}
})
}
@@ -0,0 +1,15 @@
const {
ID,
hasOwn,
addRawAttr
} = require('./util')
module.exports = function preTransformNode (el, options) {
if (!hasOwn(options, 'nid')) {
options.nid = 0
}
addRawAttr(el, ID, options.nid++)
if (el.attrsMap['v-for']) {
el.forId = el.attrsMap[ID]
}
}
+210
View File
@@ -0,0 +1,210 @@
const {
ID,
V_FOR,
SET_DATA,
isVar,
getNewId,
getForEl,
processForKey,
updateForEleId,
traverseNode,
updateScopedSlotEleId
} = require('./util')
const {
isComponent
} = require('../util')
const {
parseIs,
parseRef,
parseSlotName,
parseIf,
parseFor,
parseText,
parseDirs,
parseAttrs,
parseProps,
parseBinding
} = require('./parser/base-parser')
const parseEvent = require('./parser/event-parser')
const parseBlock = require('./parser/block-parser')
const parseWxsProps = require('./parser/wxs-props-parser')
const parseWxsEvents = require('./parser/wxs-events-parser')
const preTransformNode = require('./pre-transform-node')
const optimize = require('./optimizer')
function createGenVar (id, isScopedSlot) {
if (isScopedSlot) {
return function genVar (name, value) {
return `_svm.${SET_DATA}(${id},'${name}',${value})`
}
}
return function genVar (name, value) {
return `${SET_DATA}(${id},'${name}',${value})`
}
}
function parseKey (el, isScopedSlot) {
// add default key
if (processForKey(el)) {
el = el.children[0] // 当 template 下仅文本时,处理第一个动态文本
}
if (!el.key || el.key.indexOf(SET_DATA) === 0) {
return
}
const forEl = getForEl(el)
if (!forEl) {
return isVar(el.key) && (el.key = createGenVar(el.attrsMap[ID], isScopedSlot)('a-key', el.key))
}
if (!isVar(forEl.for)) {
return
}
const forId = forEl.forId
const it = forEl.iterator2
const genVar = createGenVar(forId, isScopedSlot)
if (forEl === el) { // <view v-for="item in items" :key="item.id"></view>
el.key = genVar(V_FOR, `{forIndex:${it},key:${el.key}}`)
} else { // <template v-for="item in items"><view :key="item.id+'1'"></view><view :key="item.id+'2'"></view></template>
const keyIndex = forEl.children.indexOf(el)
el.key = genVar(V_FOR, `{forIndex:${it},keyIndex:${keyIndex},key:${el.key}}`)
}
if (el.tag === 'slot') {
el.attrs.push({ name: 'key', value: el.key })
}
}
function parseComponentAttrs (el, genVar) {
el.attrs && el.attrs.forEach(attr => {
const {
name,
value
} = attr
if (isVar(value) && (name === 'id' || name.indexOf('data-') === 0)) {
attr.value = genVar('a-' + name, value)
}
})
}
function checkAutoFill (el) {
if (
el.for &&
(
el.tag === 'template' ||
el.tag === 'block'
) &&
!el.children.find(child =>
child.type === 1 &&
child.tag !== 'template' &&
child.tag !== 'block'
)
) {
return true
}
return false
}
function transformNode (el, parent, state, isScopedSlot) {
if (el.type === 3) {
// fixed by xxxxxx 注意:保持平台一致性,trim 一下,理论上service不需要,保险起见也处理一遍
el.text = el.text.trim()
return
}
parseBlock(el, parent)
parseEvent(el)
updateForEleId(el, state)
updateScopedSlotEleId(el, state)
if (el.type === 2) {
let pid = parent.attrsMap[ID]
if (isScopedSlot && String(pid).indexOf('_si') === -1) {
pid = getNewId(pid, '_si')
}
return parseText(el, parent, {
childIndex: state.childIndex || 0,
index: 0,
service: true,
// <uni-popup>{{content}}</uni-popup>
genVar: createGenVar(pid, isScopedSlot)
})
}
const genVar = createGenVar(el.attrsMap[ID], isScopedSlot)
parseIs(el, genVar)
parseRef(el, genVar)
parseSlotName(el, genVar)
parseFor(el, createGenVar, isScopedSlot, checkAutoFill(el))
parseKey(el, isScopedSlot)
parseIf(el, createGenVar, isScopedSlot)
parseBinding(el, genVar)
parseDirs(el, genVar, ['model'])
parseWxsProps(el, {
isAppService: true
})
if (!isComponent(el.tag)) {
parseAttrs(el, genVar)
} else { // 目前的方案需要同步dataset
parseComponentAttrs(el, genVar)
}
parseProps(el, genVar)
parseWxsEvents(el, {
filterModules: state.filterModules,
isAppService: true
})
}
function postTransformNode (el, options) {
if (!el.parent) { // 从根节点开始递归处理
if (options.root) { // 当根节点是由if,elseif,else组成
parseIf(options.root, createGenVar)
} else {
options.root = el
}
traverseNode(el, false, {
createGenVar,
forIteratorId: 0,
transformNode,
filterModules: options.filterModules
})
optimize(el, options)
}
}
function genVModel (el, isScopedSlot) {
if (
(el.tag === 'input' || el.tag === 'textarea') &&
el.directives &&
el.directives.find(dir => dir.name === 'model')
) {
const prop = el.props.find(prop => prop.name === 'value')
prop.value = createGenVar(el.attrsMap[ID], isScopedSlot)('v-model', prop.value)
}
if (el.model) {
el.model.value = createGenVar(el.attrsMap[ID], isScopedSlot)('v-model', el.model.value)
}
}
function genData (el) {
delete el.$parentIterator3
genVModel(el)
return ''
}
module.exports = {
preTransformNode,
postTransformNode,
genData
}
+297
View File
@@ -0,0 +1,297 @@
const {
parseExpression
} = require('@babel/parser')
const t = require('@babel/types')
const ID = '_i'
const ITERATOR1 = '$1'
const ITERATOR2 = '$2'
const ITERATOR3 = '$3'
const SET_DATA = '_$s'
const GET_DATA = '_$g'
const SET_MP_CLASS = '_$smc'
const GET_CHANGE_DATA = '_$gc' // wxs
const C_IS = 'is'
const C_SLOT_TARGET = 'st'
const C_REF = 'ref'
const C_NAME = 'name'
const V_FOR = 'f'
const V_IF = 'i'
const V_ELSE_IF = 'e'
// web components
const elements = ['uni-view']
function isVar (str) {
if (!str) {
return false
}
const expr = parseExpression(str)
if (
t.isStringLiteral(expr) ||
t.isNumericLiteral(expr) ||
t.isBooleanLiteral(expr) ||
t.isNullLiteral(expr)
) {
return false
}
return true
}
function addRawAttr (el, name, value) {
el.attrsMap[name] = value
el.attrsList.push({
name,
value
})
}
function updateEleId (el, it, state) {
if (el.type !== 1) {
return
}
const newId = getNewId(el.attrsMap[ID], it)
addRawAttr(el, ID, newId)
if (el.attrs) {
const attr = el.attrs.find(attr => attr.name === ID)
attr.value = newId
}
el.children.forEach(child => {
if (!child.for) { // 忽略嵌套 for
updateEleId(child, it)
} else {
child.$parentIterator3 = (child.$parentIterator3 ? (child.$parentIterator3 + '+') : '') + it
child.forId = `${child.forId}+'-'+${it}`
}
})
el.ifConditions && el.ifConditions.forEach((con, index) => {
index !== 0 && updateEleId(con.block, it, state)
})
el.scopedSlots && Object.values(el.scopedSlots).forEach((slot, index) => {
updateEleId(slot, it, state)
})
}
function getBindingAttr (el, name) {
return getAndRemoveAttr(el, ':' + name) ||
getAndRemoveAttr(el, 'v-bind:' + name)
}
function getAndRemoveAttr (el, name) {
let val
if ((val = el.attrsMap[name]) != null) {
const list = el.attrsList
for (let i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
list.splice(i, 1)
break
}
}
}
delete el.attrsMap[name]
return val
}
function updateForIterator (el, state) {
if (!el.for) {
return
}
// 简单处理,确保所有 for 循环,均包含 1,2,3
const forIteratorId = state.forIteratorId++
if (!el.iterator1) {
el.iterator1 = ITERATOR1 + forIteratorId
}
if (!el.iterator2) {
el.iterator2 = ITERATOR2 + forIteratorId
}
if (!el.iterator3) {
el.iterator3 = ITERATOR3 + forIteratorId
}
}
function updateForEleId (el, state) {
updateForIterator(el, state)
if (el.for) {
const it = el.$parentIterator3 ? (el.$parentIterator3 + '+' + "'-'" + '+' + el.iterator3) : el.iterator3
updateEleId(el, it, state)
}
}
function getNewId (id, it) {
return Number.isInteger(id) ? `("${id}-"+${it})` : `(${id}+${it})`
}
function updateScopedSlotEleId (el, state) {
// TODO 暂不考虑 scopedSlot 嵌套情况
if (el.slotScope) {
const getNewId = function (id, it) {
return Number.isInteger(id) ? `("${id}-"+${it})` : `(${id}+"-"+${it})`
}
const updateEleId = function (el) {
if (el.type !== 1) {
return
}
const it = '_si'
const newId = getNewId(el.attrsMap[ID], it)
if (el.forId) {
el.forId = getNewId(el.forId, it)
}
addRawAttr(el, ID, newId)
if (el.attrs) {
const attr = el.attrs.find(attr => attr.name === ID)
attr.value = newId
}
el.children.forEach(child => {
if (!child.slotScope) { // 忽略嵌套 scopedSlot
updateEleId(child, state)
}
})
}
if (el.tag === 'template' && el.slotTarget) { // new v-slot
el.children.forEach(child => {
if (!child.slotScope) { // 忽略嵌套 scopedSlot
updateEleId(child, state)
}
})
} else { // old slot-scope
updateEleId(el)
}
}
}
function getForEl (el) {
if (el.for) {
return el
}
if (el.parent && el.parent.for && (el.parent.tag === 'template' || el.parent.tag === 'block')) {
return el.parent
}
}
function processForKey (el) {
const forEl = getForEl(el)
if (forEl && !el.key) { // 占位的 text 标签也无需添加 key
if (!isVar(forEl.for)) { // <view v-for="10"></view>
return
}
const it = forEl.iterator3
if (forEl.tag === 'template' || forEl.tag === 'block') {
if (forEl !== el) {
const keyIndex = forEl.children.indexOf(el)
el.key = `${forEl.forId}+'-${keyIndex}'+${it}`
} else { // 当 template 下只有文本节点
if (
el.children &&
el.children.length &&
!el.children.find(child => child.type === 1)
) {
el.children[0].parent = el
if (!el.children.find(child => child.key)) {
el.children[0].key = `${forEl.forId}+'-0'+${it}`
}
return true
}
}
} else {
el.key = `${forEl.forId}+'-'+${it}`
}
}
}
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}
function traverseNode (el, parent, state, isScopedSlot) {
state.transformNode(el, parent, state, isScopedSlot)
el.children && el.children.forEach((child, index) => {
state.childIndex = index
traverseNode(child, el, state, isScopedSlot)
})
el.ifConditions && el.ifConditions.forEach((con, index) => {
if (index !== 0) {
state.childIndex = index
traverseNode(con.block, el, state, isScopedSlot)
}
})
el.scopedSlots && Object.values(el.scopedSlots).forEach((slot, index) => {
state.childIndex = index
slot.slotScope = `${slot.slotScope}, _svm, _si`
if (slot.slotTargetDynamic && slot.slotTarget) {
slot.slotTarget = state.createGenVar(slot.attrsMap[ID])(C_SLOT_TARGET, slot.slotTarget)
}
traverseNode(slot, el, state, true)
})
}
function addAttr (el, name, value, dynamic) {
const attrs = dynamic
? (el.dynamicAttrs || (el.dynamicAttrs = []))
: (el.attrs || (el.attrs = []))
attrs.push({
name,
value,
dynamic
})
el.plain = false
}
function removeRawAttr (el, name) {
delete el.attrsMap[name]
const index = el.attrsList.findIndex(attr => attr.name === name)
index !== -1 && el.attrsList.splice(index, 1)
}
function removeRawBindingAttr (el, name) {
removeRawAttr(el, ':' + name)
removeRawAttr(el, 'v-bind:' + name)
}
function addHandler (el, name, value, important) {
const events = el.events || (el.events = {})
const handlers = events[name]
const newHandler = {
value: value.trim(),
dynamic: undefined
}
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler)
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
module.exports = {
C_IS,
C_REF,
C_NAME,
V_FOR,
V_IF,
V_ELSE_IF,
ID,
SET_DATA,
GET_DATA,
SET_MP_CLASS,
GET_CHANGE_DATA,
elements,
isVar,
hasOwn,
addAttr,
addRawAttr,
removeRawAttr,
removeRawBindingAttr,
getNewId,
getForEl,
addHandler,
processForKey,
updateForEleId,
updateScopedSlotEleId,
getBindingAttr,
getAndRemoveAttr,
traverseNode
}
+250
View File
@@ -0,0 +1,250 @@
const {
ID,
GET_DATA,
isVar,
getNewId,
getForEl,
updateForEleId,
updateScopedSlotEleId,
processForKey,
traverseNode
} = require('./util')
const {
parseIs,
parseRef,
parseSlotName,
parseIf,
parseFor,
parseText,
parseAttrs,
parseProps,
parseBinding
} = require('./parser/base-parser')
const parseTag = require('./parser/tag-parser')
const parseEvent = require('./parser/event-parser')
const parseBlock = require('./parser/block-parser')
const parseComponent = require('./parser/component-parser')
const parseWxsProps = require('./parser/wxs-props-parser')
const parseWxsEvents = require('./parser/wxs-events-parser')
const basePreTransformNode = require('./pre-transform-node')
function createGenVar (id, isScopedSlot) {
if (isScopedSlot) {
return function genVar (name, value) {
return `_svm.${GET_DATA}(${id},'${name}')`
}
}
return function genVar (name) {
return `${GET_DATA}(${id},'${name}')`
}
}
function parseKey (el, isScopedSlot) {
// add default key
processForKey(el)
if (el.key) { // renderList key
const forEl = getForEl(el)
if (forEl) {
if (!isVar(forEl.for)) {
return
}
if (forEl === el) { // <view v-for="item in items" :key="item.id"></view>
el.key = forEl.alias
} else { // <template v-for="item in items"><view :key="item.id+'1'"></view><view :key="item.id+'2'"></view></template>
const keyIndex = forEl.children.indexOf(el)
el.key = `${forEl.alias}['k${keyIndex}']`
}
} else {
isVar(el.key) && (el.key = createGenVar(el.attrsMap[ID], isScopedSlot)('a-key'))
}
}
}
function parseDirs (el, genVar, ignoreDirs, includeDirs = []) {
if (!el.directives) {
return
}
el.directives = el.directives.filter(dir => {
if (includeDirs.indexOf(dir.name) !== -1) {
if (ignoreDirs.indexOf(dir.name) === -1) {
dir.value && (dir.value = genVar('v-' + dir.name, dir.value))
dir.isDynamicArg && (dir.arg = genVar('v-' + dir.name + '-arg', dir.arg))
}
return true
}
})
}
const includeDirs = [
'text',
'html',
'bind',
'model',
'show',
'if',
'else',
'else-if',
'for',
'on',
'bind',
'slot',
'pre',
'cloak',
'once'
]
const ignoreDirs = ['model']
function transformNode (el, parent, state, isScopedSlot) {
if (el.type === 3) {
// fixed by xxxxxx 注意:保持平台一致性,trim 一下
el.text = el.text.trim()
return
}
parseBlock(el, parent)
parseComponent(el)
parseEvent(el)
// 更新 id
updateForEleId(el, state)
updateScopedSlotEleId(el, state)
if (el.type === 2) {
let pid = parent.attrsMap[ID]
if (isScopedSlot && String(pid).indexOf('_si') === -1) {
pid = getNewId(pid, '_si')
}
return parseText(el, parent, {
childIndex: state.childIndex || 0,
index: 0,
view: true,
// <uni-popup>{{content}}</uni-popup>
genVar: createGenVar(pid, isScopedSlot)
})
}
const genVar = createGenVar(el.attrsMap[ID], isScopedSlot)
parseIs(el, genVar)
parseRef(el, genVar)
parseSlotName(el, genVar)
if (parseFor(el, createGenVar, isScopedSlot)) {
if (el.alias[0] === '{') { // <div><li v-for=" { a, b } in items"></li></div>
el.alias = '$item'
}
}
parseKey(el, isScopedSlot)
parseIf(el, createGenVar, isScopedSlot)
parseBinding(el, genVar)
parseDirs(el, genVar, ignoreDirs, includeDirs)
parseWxsProps(el, {
isAppView: true
})
// if (el.attrs) { // TODO 过滤 dataset
// el.attrs = el.attrs.filter(attr => attr.name.indexOf('data-') !== 0)
// }
parseAttrs(el, genVar)
parseProps(el, genVar)
parseWxsEvents(el, {
filterModules: state.filterModules,
isAppView: true
})
}
function postTransformNode (el, options) {
if (!el.parent) { // 从根节点开始递归处理
if (options.root) { // 当根节点是由if,elseif,else组成
parseIf(options.root, createGenVar)
} else {
options.root = el
}
traverseNode(el, false, {
createGenVar,
forIteratorId: 0,
transformNode,
filterModules: options.filterModules
})
}
}
function handleViewEvents (events) {
Object.keys(events).forEach(name => {
const eventOpts = events[name]
// wxs
if (eventOpts.value && eventOpts.value.indexOf('$handleWxsEvent') !== -1) {
return
}
const modifiers = Object.create(null)
let type = name
const isPassive = type.charAt(0) === '&'
type = isPassive ? type.slice(1) : type
const isOnce = type.charAt(0) === '~'
type = isOnce ? type.slice(1) : type
const isCapture = type.charAt(0) === '!'
type = isCapture ? type.slice(1) : type
isPassive && (modifiers.passive = true)
isOnce && (modifiers.once = true)
isCapture && (modifiers.capture = true)
if (Array.isArray(eventOpts)) {
eventOpts.forEach(eventOpt => {
eventOpt.modifiers && Object.assign(modifiers, eventOpt.modifiers)
})
} else {
eventOpts.modifiers && Object.assign(modifiers, eventOpts.modifiers)
}
if (Object.keys(modifiers).length) {
events[name] = {
value: `$handleViewEvent($event,${JSON.stringify(modifiers)})`
}
} else {
events[name] = {
value: '$handleViewEvent($event)'
}
}
})
}
function genVModel (el, isScopedSlot) {
if (el.model) {
el.model.value = createGenVar(el.attrsMap[ID], isScopedSlot)('v-model', el.model.value)
if ((el.tag === 'v-uni-input' || el.tag === 'v-uni-textarea') && !(el.events && el.events.input)) {
el.model.callback = `function($$v){$handleVModelEvent(${el.attrsMap[ID]},$$v)}`
} else {
el.model.callback = 'function(){}'
}
}
}
function genData (el) {
delete el.$parentIterator3
genVModel(el)
// 放在 postTransformNode 中处理的时机太靠前,v-model 等指令会新增 event
el.events && handleViewEvents(el.events)
el.nativeEvents && handleViewEvents(el.nativeEvents)
return ''
}
module.exports = {
preTransformNode: function (el, options) {
parseTag(el)
return basePreTransformNode(el, options)
},
postTransformNode,
genData
}
+107
View File
@@ -0,0 +1,107 @@
const url = require('url')
const transformAssetUrls = {
audio: 'src',
video: ['src', 'poster'],
img: 'src',
image: 'src',
'cover-image': 'src',
// h5
'v-uni-audio': 'src',
'v-uni-video': ['src', 'poster'],
'v-uni-image': 'src',
'v-uni-cover-image': 'src',
// nvue
'u-image': 'src',
'u-video': ['src', 'poster']
}
function urlToRequire (url) {
const returnValue = `"${url}"`
// same logic as in transform-require.js
const firstChar = url.charAt(0)
if (firstChar === '.' || firstChar === '~' || firstChar === '@') {
if (firstChar === '~') {
const secondChar = url.charAt(1)
url = url.slice(secondChar === '/' ? 2 : 1)
}
const uriParts = parseUriParts(url)
if (!uriParts.hash) { // fixed by xxxxxx (v3 template中需要加/)
return `require("${url}")`
} else { // fixed by xxxxxx (v3 template中需要加/)
// support uri fragment case by excluding it from
// the require and instead appending it as string;
// assuming that the path part is sufficient according to
// the above caseing(t.i. no protocol-auth-host parts expected)
return `require("${uriParts.path}") + "${uriParts.hash}"`
}
}
return returnValue
}
/**
* vuejs/component-compiler-utils#22 Support uri fragment in transformed require
* @param urlString an url as a string
*/
function parseUriParts (urlString) {
// initialize return value
/* eslint-disable node/no-deprecated-api */
const returnValue = url.parse('')
if (urlString) {
// A TypeError is thrown if urlString is not a string
// @see https://nodejs.org/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost
if (typeof urlString === 'string') {
// check is an uri
/* eslint-disable node/no-deprecated-api */
return url.parse(urlString) // take apart the uri
}
}
return returnValue
}
function rewrite (attr, name, options) {
if (attr.name === name) {
const value = attr.value
// only transform static URLs
if (value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') {
if (!options.h5) { // 非 H5 平台
attr.value = attr.value
.replace('"@/', '"/')
.replace('"~@/', '"/')
}
// v3,h5
const needRequire = options.service || options.view || options.h5
if (needRequire) {
attr.value = urlToRequire(attr.value.slice(1, -1))
}
return true
}
}
return false
}
module.exports = {
postTransformNode: (node, options) => {
if (!node.attrs) {
return
}
const attributes = transformAssetUrls[node.tag]
if (!attributes) {
return
}
if (typeof attributes === 'string') {
if (node.attrs.some(attr => rewrite(attr, attributes, options))) {
if (options.service || options.view) {
node.hasBindings = true
}
}
} else if (Array.isArray(attributes)) {
attributes.forEach(item => {
if (node.attrs.some(attr => rewrite(attr, item, options))) {
if (options.service || options.view) {
node.hasBindings = true
}
}
})
}
}
}
+123
View File
@@ -0,0 +1,123 @@
const path = require('path')
const {
hyphenate,
isComponent
} = require('./util')
const {
removeExt
} = require('@dcloudio/uni-cli-shared/lib/util')
const {
getAutoComponents
} = require('@dcloudio/uni-cli-shared/lib/pages')
const {
updateUsingAutoImportComponents
} = require('@dcloudio/uni-cli-shared/lib/cache')
function formatSource (source) {
if (source.indexOf('@/') === 0) { // 根目录
source = source.replace('@/', '')
} else { // node_modules
if (process.env.UNI_PLATFORM === 'mp-alipay') {
if (source.indexOf('@') === 0) {
source = source.replace('@', 'npm-scope-')
}
}
source = 'node-modules/' + source
}
return removeExt(source)
}
function getWebpackChunkName (source) {
return formatSource(source)
}
function updateMPUsingAutoImportComponents (autoComponents, options) {
if (!options.resourcePath) {
return
}
const resourcePath = options.resourcePath.replace(path.extname(options.resourcePath), '')
if (resourcePath === 'App') {
return
}
const usingAutoImportComponents = Object.create(null)
autoComponents.forEach(({
name,
source
}) => {
// 自定义组件统一格式化为 kebab-case
usingAutoImportComponents[hyphenate(name)] = '/' + formatSource(source)
})
updateUsingAutoImportComponents(resourcePath, usingAutoImportComponents) // 更新json
}
function generateAutoComponentsCode (autoComponents, dynamic = false) {
const components = []
autoComponents.forEach(({
name,
source
}) => {
// 统一转换为驼峰命名
name = name.replace(/-(\w)/g, (_, str) => str.toUpperCase())
if (dynamic) {
components.push(
`'${name}': function(){return import(/* webpackChunkName: "${getWebpackChunkName(source)}" */'${source}')}`
)
} else {
components.push(`'${name}': require('${source}').default`)
}
})
if (process.env.NODE_ENV === 'production') {
return `var components = {${components.join(',')}}`
}
return `var components;
try{
components = {${components.join(',')}}
}catch(e){
if(e.message.indexOf('Cannot find module') !== -1 && e.message.indexOf('.vue') !== -1){
console.error(e.message)
console.error('1. 排查组件名称拼写是否正确')
console.error('2. 排查组件是否符合 easycom 规范,文档:https://uniapp.dcloud.net.cn/collocation/pages?id=easycom')
console.error('3. 若组件不符合 easycom 规范,需手动引入,并在 components 中注册该组件')
} else {
throw e
}
}`
}
function compileTemplate (source, options, compile) {
const res = compile(source, options)
const autoComponents = getAutoComponents([...(options.isUnaryTag.autoComponents || [])])
if (autoComponents.length) {
// console.log('检测到的自定义组件:' + JSON.stringify(autoComponents))
res.components = generateAutoComponentsCode(autoComponents, options.mp)
} else {
res.components = 'var components;'
}
if (options.mp) { // 小程序 更新 json 每次编译都要调整,保证热更新时增减组件一致
updateMPUsingAutoImportComponents(autoComponents || [], options)
}
return res
}
const compilerModule = {
preTransformNode (el, options) {
if (el.tag === 'match-media' && process.env.UNI_PLATFORM !== 'mp-weixin') {
el.tag = 'uni-match-media'
}
if (process.env.UNI_PLATFORM === 'quickapp-native') {
// 排查所有标签
(options.isUnaryTag.autoComponents || (options.isUnaryTag.autoComponents = new Set())).add(el.tag)
} else if (isComponent(el.tag, options.mp && options.mp.platform) && el.tag !== 'App') { // App.vue
// 挂在 isUnaryTag 上边,可以保证外部访问到
(options.isUnaryTag.autoComponents || (options.isUnaryTag.autoComponents = new Set())).add(el.tag)
}
}
}
module.exports = {
compileTemplate,
module: compilerModule
}
+21
View File
@@ -0,0 +1,21 @@
const dirRE = /^v-|^@|^:|^#/
const PROPS = ['id', 'class', 'style', 'inline-template']
module.exports = {
preTransformNode (el) {
if (!el.attrsList) {
return
}
el.attrsList.forEach(attr => {
if (attr.bool) {
if (!dirRE.test(attr.name) && !PROPS.includes(attr.name)) {
delete el.attrsMap[attr.name]
attr.name = ':' + attr.name
attr.value = 'true'
el.attrsMap[attr.name] = attr.value
}
}
})
}
}
+61
View File
@@ -0,0 +1,61 @@
var range = 2
function generateCodeFrame (
source,
start,
end
) {
source = source.replace(/\r\n/g, '\n') // 替换\r\n 为 \n
/* eslint-disable no-void */
if (start === void 0) start = 0
if (end === void 0) end = source.length
var lines = source.split(/\n/) // 替换\r?\n 为 \n,不然 length 对不上,导致死循环
var count = 0
var res = []
for (var i = 0; i < lines.length; i++) {
count += lines[i].length + 1
if (count >= start) {
for (var j = i - range; j <= i + range || end > count; j++) {
if (j < 0 || j >= lines.length) {
continue
}
res.push(('' + (j + 1) + (repeat$1(' ', 3 - String(j + 1).length)) + '| ' + (lines[j])))
var lineLength = lines[j].length
if (j === i) {
// push underline
var pad = start - (count - lineLength) + 1
var length = end > count ? lineLength - pad : end - start
res.push(' | ' + repeat$1(' ', pad) + repeat$1('^', length))
} else if (j > i) {
if (end > count) {
var length$1 = Math.min(end - count, lineLength)
res.push(' | ' + repeat$1('^', length$1))
}
count += lineLength + 1
}
}
break
}
}
return res.join('\n')
}
function repeat$1 (str, n) {
var result = ''
if (n > 0) {
while (true) { // eslint-disable-line
if (n & 1) {
result += str
}
n >>>= 1
if (n <= 0) {
break
}
str += str
}
}
return result
}
module.exports = generateCodeFrame
+150
View File
@@ -0,0 +1,150 @@
const METHOD_CREATE_ELEMENT = '_c' // createElement
const METHOD_MARK_ONCE = '_o' // markOnce
const METHOD_TO_NUMBER = '_n' // toNumber
const METHOD_TO_STRING = '_s' // toString
const METHOD_RENDER_LIST = '_l' // renderList
const METHOD_RENDER_SLOT = '_t' // renderSlot
const METHOD_LOOSE_EQUAL = '_q' // looseEqual
const METHOD_LOOSE_INDEX_OF = '_i' // looseIndexOf
const METHOD_RENDER_STATIC = '_m' // renderStatic
const METHOD_RESOLVE_FILTER = '_f' // resolveFilter
const METHOD_CHECK_KEY_CODES = '_k' // checkKeyCodes
const METHOD_BIND_OBJECT_PROPS = '_b' // bindObjectProps
const METHOD_CREATE_TEXT_VNODE = '_v' // createTextVNode
const METHOD_CREATE_EMPTY_VNODE = '_e' // createEmptyVNode
const METHOD_RESOLVE_SCOPED_SLOTS = '_u' // resolveScopedSlots
const METHOD_BIND_OBJECT_LISTENERS = '_g' // bindObjectListeners
const METHOD_BIND_DYNAMIC_KEYS = '_d' // bindDynamicKeys
const METHOD_PREPEND_MODIFIER = '_p' // prependModifier
const METHOD_SET = '$set' // $set
const INTERNAL_SET_MODEL = '__set_model'
const INTERNAL_SET_SYNC = '__set_sync'
const INTERNAL_GET_ORIG = '__get_orig'
const INTERNAL_GET_CLASS = '__get_class'
const INTERNAL_GET_STYLE = '__get_style'
const INTERNAL_GET_EVENT = '__get_event'
const INTERNAL_GET_REFS = '__get_refs'
const INTERNAL_EVENT_PROXY = '__e'
const INTERNAL_EVENT_LINK = '__l'
const INTERNAL_EVENT_WRAP = '__w'
const ALLOWED_GLOBAL_OBJECT = [
'Math',
'Number',
'Date',
'Array',
'Object',
'Boolean',
'String',
'RegExp',
'Map',
'Set',
'JSON',
'Intl',
'console',
'Infinity',
'undefined',
'NaN',
'isFinite',
'isNaN',
'parseFloat',
'parseInt',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent',
'require',
'arguments'
]
module.exports = {
SELF_CLOSING_TAGS: ['input'], // 百度需要自闭合
VUE_EVENT_MODIFIERS: {
capture: '!',
once: '~',
passive: '&',
custom: '^'
},
ALLOWED_GLOBAL_OBJECT,
CLASS_REF: 'vue-ref',
CLASS_REF_IN_FOR: 'vue-ref-in-for',
VAR_MP: '$mp',
VAR_ROOT: '$root',
VAR_ORIGINAL: '$orig',
VAR_INDEX: '$index',
VAR_FILTER: 'F',
ATTR_DATA_EVENT_OPTS: 'data-event-opts',
ATTR_DATA_COM_TYPE: 'data-com-type',
ATTR_DATA_EVENT_PARAMS: 'data-event-params',
ATTR_DATA_EVENT_LIST: 'data-event-list',
ATTR_SLOT_ORIGIN: 'slot-origin',
ATTR_DATA_CUSTOM_HIDDEN: 'data-custom-hidden',
VIRTUAL_HOST_STYLE: 'virtualHostStyle',
VIRTUAL_HOST_CLASS: 'virtualHostClass',
INTERNAL_GET_ORIG,
INTERNAL_GET_CLASS,
INTERNAL_GET_STYLE,
INTERNAL_GET_EVENT,
INTERNAL_GET_REFS,
INTERNAL_EVENT_PROXY,
INTERNAL_EVENT_LINK,
INTERNAL_EVENT_WRAP,
INTERNAL_SET_MODEL,
INTERNAL_SET_SYNC,
METHOD_BUILT_IN: [
METHOD_SET,
INTERNAL_SET_MODEL,
INTERNAL_SET_SYNC,
INTERNAL_GET_ORIG,
INTERNAL_GET_CLASS,
INTERNAL_GET_STYLE,
INTERNAL_GET_EVENT,
INTERNAL_GET_REFS,
INTERNAL_EVENT_PROXY,
METHOD_CREATE_ELEMENT, // createElement
METHOD_MARK_ONCE, // markOnce
METHOD_TO_NUMBER, // toNumber
METHOD_TO_STRING, // toString
METHOD_RENDER_LIST, // renderList
METHOD_RENDER_SLOT, // renderSlot
METHOD_LOOSE_EQUAL, // looseEqual
METHOD_LOOSE_INDEX_OF, // looseIndexOf
METHOD_RENDER_STATIC, // renderStatic
METHOD_RESOLVE_FILTER, // resolveFilter
METHOD_CHECK_KEY_CODES, // checkKeyCodes
METHOD_BIND_OBJECT_PROPS, // bindObjectProps
METHOD_CREATE_TEXT_VNODE, // createTextVNode
METHOD_CREATE_EMPTY_VNODE, // createEmptyVNode
METHOD_RESOLVE_SCOPED_SLOTS, // resolveScopedSlots
METHOD_BIND_OBJECT_LISTENERS, // bindObjectListeners
METHOD_BIND_DYNAMIC_KEYS, // bindDynamicKeys
METHOD_PREPEND_MODIFIER // prependModifier
],
METHOD_CREATE_ELEMENT,
METHOD_TO_STRING,
METHOD_RENDER_LIST,
METHOD_RESOLVE_FILTER,
METHOD_RENDER_SLOT,
METHOD_CREATE_EMPTY_VNODE,
METHOD_RESOLVE_SCOPED_SLOTS,
PREFIX_GLOBAL: 'g',
PREFIX_ATTR: 'a',
PREFIX_METHOD: 'm',
PREFIX_FILTER: 'f',
PREFIX_FOR: 'l',
PREFIX_CLASS: 'c',
PREFIX_STYLE: 's',
PREFIX_EVENT: 'e',
PREFIX_TEXT: 't',
IDENTIFIER_FOR: '__$$for$$__',
IDENTIFIER_ATTR: '__$$attr$$__',
IDENTIFIER_METHOD: '__$$method$$__',
IDENTIFIER_FILTER: '__$$filter$$__',
IDENTIFIER_CLASS: '__$$class$$__',
IDENTIFIER_STYLE: '__$$style$$__',
IDENTIFIER_EVENT: '__$$event$$__',
IDENTIFIER_GLOBAL: '__$$global$$__',
IDENTIFIER_TEXT: '__$$text$$__'
}
+66
View File
@@ -0,0 +1,66 @@
const t = require('@babel/types')
const babelTraverse = require('@babel/traverse').default
const {
METHOD_RENDER_LIST
} = require('../constants')
function getDataPath (identifier, parent, scope) {
if (
!(t.isCallExpression(parent)) &&
// not id of a Declaration
!(t.isDeclaration(parent) && parent.id === identifier) &&
// not a params of a function
!(t.isFunction(parent) && parent.params.indexOf(identifier) > -1) &&
// not a key of Property
!(parent.type === 'ObjectProperty' && parent.key === identifier && !parent.computed) &&
// not in an Array destructure pattern
!(parent.type === 'ArrayPattern') &&
// not in an Object destructure pattern
!(parent.parent && parent.parent.type === 'ObjectPattern') &&
// not already in scope
!scope.hasBinding(identifier.name)
) {
return identifier.name
}
}
function getDataPathByMemberExpression (node, ret) {
if (t.isMemberExpression(node.object)) {
getDataPathByMemberExpression(node.object, ret)
} else if (t.isIdentifier(node.object)) {
ret.push(node.object.name)
}
ret.push(node.property.name || node.property.value)
}
const visitor = {
Identifier (path) {
const dataPath = getDataPath(path.node, path.parent, path.scope)
if (dataPath) {
console.log('....identifier', dataPath)
} else {
// console.log('....ignore', path.node.name)
}
},
MemberExpression (path) {
const dataPathArray = []
getDataPathByMemberExpression(path.node, dataPathArray)
path.skip()
},
CallExpression (path) {
const callee = path.node.callee
if (callee.name === METHOD_RENDER_LIST) {
// for
// path.skip()
}
}
}
module.exports = function traverse (ast, state) {
const data = Object.create(null)
babelTraverse(ast, visitor, undefined, {
data
})
}
+138
View File
@@ -0,0 +1,138 @@
const getCompilerOptions = require('./mp.js')
const TAGS = {
br: 'view',
hr: 'view',
p: 'view',
h1: 'view',
h2: 'view',
h3: 'view',
h4: 'view',
h5: 'view',
h6: 'view',
abbr: 'view',
address: 'view',
b: 'view',
bdi: 'view',
bdo: 'view',
blockquote: 'view',
cite: 'view',
code: 'view',
del: 'view',
ins: 'view',
dfn: 'view',
em: 'view',
strong: 'view',
samp: 'view',
kbd: 'view',
var: 'view',
i: 'view',
mark: 'view',
pre: 'view',
q: 'view',
ruby: 'view',
rp: 'view',
rt: 'view',
s: 'view',
small: 'view',
sub: 'view',
sup: 'view',
time: 'view',
u: 'view',
wbr: 'view',
// 表单元素
// form: 'form',
// input: 'input',
// textarea: 'textarea',
// button: 'button',
select: 'picker',
option: 'view',
optgroup: 'view',
// label: 'label',
fieldset: 'view',
datalist: 'picker',
legend: 'view',
output: 'view',
// 框架
iframe: 'view',
// 图像
img: 'image',
// canvas: 'canvas',
figure: 'view',
figcaption: 'view',
// 音视频
// audio: 'audio',
source: 'audio',
// video: 'video',
track: 'video',
// 链接
a: 'navigator',
nav: 'view',
link: 'navigator',
// 列表
ul: 'view',
ol: 'view',
li: 'view',
dl: 'view',
dt: 'view',
dd: 'view',
menu: 'view',
command: 'view',
// 表格table
table: 'view',
caption: 'view',
th: 'view',
td: 'view',
tr: 'view',
thead: 'view',
tbody: 'view',
tfoot: 'view',
col: 'view',
colgroup: 'view',
// 样式 节
div: 'view',
main: 'view',
span: 'label',
header: 'view',
footer: 'view',
section: 'view',
article: 'view',
aside: 'view',
details: 'view',
dialog: 'view',
summary: 'view',
// progress: 'progress',
meter: 'progress', // todo
head: 'view', // todo
meta: 'view', // todo
base: 'text', // todo
// 'map': 'image', // TODO不是很恰当
area: 'navigator', // j结合map使用
script: 'view',
noscript: 'view',
embed: 'view',
object: 'view',
param: 'view'
}
module.exports = {
/**
* getTagName
* @param {string} tagName
* @param {string} [platform]
* @returns {boolean}
*/
getTagName (tagName, platform) {
// 排除各平台内置组件
if (platform && getCompilerOptions(platform).isNativeTag(tagName)) {
return tagName
}
return TAGS[tagName] || tagName
}
}
+302
View File
@@ -0,0 +1,302 @@
const path = require('path')
const hash = require('hash-sum')
const parser = require('@babel/parser')
const {
parseComponent,
compile,
compileToFunctions,
ssrCompile,
ssrCompileToFunctions
} = require('@dcloudio/vue-cli-plugin-uni/packages/vue-template-compiler')
const traverseScript = require('./script/traverse')
const generateScript = require('./script/generate')
const traverseTemplate = require('./template/traverse')
const generateTemplate = require('./template/generate')
const compilerModule = require('./module')
const compilerModuleUniad = require('./module.uniad')
const compilerAlipayModule = require('./module-alipay')
const compilerToutiaoModule = require('./module-toutiao')
const generateCodeFrame = require('./codeframe')
const {
isComponent,
isUnaryTag
} = require('./util')
const {
module: autoComponentsModule,
compileTemplate
} = require('./auto-components')
const isWin = /^win/.test(process.platform)
const normalizePath = path => (isWin ? path.replace(/\\/g, '/') : path)
module.exports = {
compile (source, options = {}) {
if (Array.isArray(options.modules)) {
options.modules.push(compilerModuleUniad)
}
if ( // 启用摇树优化后,需要过滤内置组件
!options.autoComponentResourcePath ||
options.autoComponentResourcePath.indexOf('@dcloudio/uni-h5/src') === -1
) {
(options.modules || (options.modules = [])).push(autoComponentsModule)
}
if (!options.modules) {
options.modules = []
}
// transformAssetUrls
options.modules.push(require('./asset-url'))
options.modules.push(require('./bool-attr'))
options.isUnaryTag = isUnaryTag
// 将 autoComponents 挂在 isUnaryTag 上边
options.isUnaryTag.autoComponents = new Set()
options.preserveWhitespace = false
if (options.service) {
options.modules.push(require('./app/service'))
options.optimize = false // 启用 staticRenderFns
// domProps => attrs
options.mustUseProp = () => false
options.isReservedTag = (tagName) => !isComponent(tagName, options.mp && options.mp.platform) // 非组件均为内置
options.getTagNamespace = () => false
try {
return compileTemplate(source, options, compile)
} catch (e) {
console.error(source)
throw e
}
} else if (options.view) {
options.modules.push(require('./app/view'))
options.optimize = false // 暂不启用 staticRenderFns
options.isUnaryTag = isUnaryTag
options.isReservedTag = (tagName) => false // 均为组件
try {
return compileTemplate(source, options, compile)
} catch (e) {
console.error(source)
throw e
}
} else if (options['quickapp-native']) {
// 后续改版,应统一由具体包实现
options.modules.push(require('@dcloudio/uni-quickapp-native/lib/compiler-module'))
}
if (!options.mp) { // h5,quickapp-native
return compileTemplate(source, options, compile)
}
options.modules.push(compilerModule)
if (options.mp.platform === 'mp-alipay') {
options.modules.push(compilerAlipayModule)
} else if (options.mp.platform === 'mp-toutiao' || options.mp.platform === 'mp-lark') {
options.modules.push(compilerToutiaoModule)
}
const res = compileTemplate(source, Object.assign(options, {
optimize: false
}), compile)
options.mp.platform = require('./mp')(options.mp.platform)
options.mp.scopeId = options.scopeId
options.mp.resourcePath = options.resourcePath
if (options.resourcePath) {
options.mp.hashId = hash(options.resourcePath)
} else {
options.mp.hashId = ''
}
options.mp.globalUsingComponents = options.globalUsingComponents || Object.create(null)
options.mp.filterModules = Object.keys(options.filterModules || {})
// (可用的原生微信小程序组件,global+scoped)
options.mp.wxComponents = options.wxComponents || Object.create(null)
Object.assign(options.mp.wxComponents, {
'uniad-plugin': 'plugin://uni-ad/ad'
})
const state = {
ast: {},
script: '',
template: '',
errors: new Set(),
tips: new Set(),
options: options.mp
}
// console.log(`function render(){${res.render}}`)
const ast = parser.parse(`function render(){${res.render}}`)
let template = ''
try {
res.render = generateScript(traverseScript(ast, state), state)
template = generateTemplate(traverseTemplate(ast, state), state)
} catch (e) {
console.error(e)
throw new Error('Compile failed at ' + options.resourcePath.replace(
path.extname(options.resourcePath),
'.vue'
))
}
res.specialMethods = state.options.specialMethods || new Set()
delete state.options.specialMethods
res.files = state.files || {}
delete state.files
// resolve scoped slots
res.generic = state.generic || []
delete state.generic
// define scoped slots
res.componentGenerics = state.componentGenerics || {}
delete state.componentGenerics
state.errors.forEach(msg => {
res.errors.push({
msg
})
})
const resourcePath = options.resourcePath.replace(path.extname(options.resourcePath), '')
state.tips.forEach(msg => {
console.log(`提示:${msg}
at ${resourcePath}.vue:1`)
})
/**
* TODO
* 方案0.最佳方案是在 loader 中直接 emitFile,但目前 vue template-loader 不好介入,自定义的 compiler 结果又无法顺利返回给 loader
* 方案1.通过 loader 传递 emitFile 来提交生成 wxml,需要一个 template loader 来给自定义 compier 增加 emitFile
* 方案2.缓存 wxml 内容,由 plugin 生成 assets 来提交生成 wxml
* ...暂时使用方案1
*/
if (options.emitFile) {
// cache
if (process.env.UNI_USING_CACHE) {
const oldEmitFile = options.emitFile
process.UNI_CACHE_TEMPLATES = {}
options.emitFile = function emitFile (name, content) {
const absolutePath = path.resolve(process.env.UNI_OUTPUT_DIR, name)
process.UNI_CACHE_TEMPLATES[absolutePath] = content
oldEmitFile(name, content)
}
}
if (options.updateSpecialMethods) {
options.updateSpecialMethods(resourcePath, [...res.specialMethods])
}
const filterTemplate = []
options.mp.filterModules.forEach(name => {
const filterModule = options.filterModules[name]
if (filterModule.type !== 'renderjs' && filterModule.attrs.lang !== 'renderjs') {
if (
filterModule.attrs &&
filterModule.attrs.src &&
filterModule.attrs.src.indexOf('@/') === 0
) {
const src = filterModule.attrs.src
filterModule.attrs.src = normalizePath(path.relative(
path.dirname(resourcePath), src.replace('@/', '')
))
}
filterTemplate.push(
options.mp.platform.createFilterTag(
options.filterTagName,
filterModule
)
)
}
})
if (filterTemplate.length) {
template = filterTemplate.join('\n') + '\n' + template
}
if (
process.UNI_ENTRY[resourcePath] &&
process.env.UNI_PLATFORM !== 'app-plus' &&
process.env.UNI_PLATFORM !== 'h5'
) {
// 检查是否启用 shadow
let colorType = false
const pageJsonStr = options.getJsonFile(resourcePath)
if (pageJsonStr) {
try {
const windowJson = JSON.parse(pageJsonStr)
if (process.env.UNI_PLATFORM === 'mp-alipay') {
colorType = windowJson.allowsBounceVertical === 'NO' &&
windowJson.navigationBarShadow &&
windowJson.navigationBarShadow.colorType
} else {
colorType = windowJson.disableScroll &&
windowJson.navigationBarShadow &&
windowJson.navigationBarShadow.colorType
}
} catch (e) {}
}
if (colorType) {
template = options.getShadowTemplate(colorType) + template
}
}
options.emitFile(options.resourcePath, template)
if (res.files) {
Object.keys(res.files).forEach(name => {
options.emitFile(name, res.files[name])
})
}
if (state.options.usingGlobalComponents) {
options.updateUsingGlobalComponents(
resourcePath,
state.options.usingGlobalComponents
)
}
if (
res.generic &&
res.generic.length &&
options.updateGenericComponents
) {
options.updateGenericComponents(
resourcePath,
res.generic
)
}
if (
res.componentGenerics &&
Object.keys(res.componentGenerics).length &&
options.updateComponentGenerics
) {
options.updateComponentGenerics(
resourcePath,
res.componentGenerics
)
}
} else {
res.template = template
}
return res
},
parseComponent,
compileToFunctions,
ssrCompile,
ssrCompileToFunctions,
generateCodeFrame
}
+26
View File
@@ -0,0 +1,26 @@
module.exports = {
postTransformNode (el) {
const attrsMap = el.attrsMap
if (attrsMap['open-type'] !== 'getPhoneNumber') {
return
}
const getPhoneNumberValue = attrsMap['@getphonenumber'] || attrsMap['v-on:getphonenumber']
if (!getPhoneNumberValue) {
return
}
el.attrs.find(attr => attr.name === 'open-type').value = '"getAuthorize"'
el.attrs.push({
name: 'scope',
value: '"phoneNumber"'
})
delete el.events.getphonenumber
el.events.getAuthorize = {
value: '$onAliGetAuthorize(\'' + getPhoneNumberValue + '\',$event)'
}
el.events.error = {
value: '$onAliAuthError(\'' + getPhoneNumberValue + '\',$event)'
}
}
}
+24
View File
@@ -0,0 +1,24 @@
module.exports = {
postTransformNode (el) {
if (el.tag === 'swiper') {
const attrsMap = el.attrsMap
let touchable
if (attrsMap[':disable-touch']) {
touchable = `!(${attrsMap[':disable-touch']})`
} else if ('disable-touch' in attrsMap) {
touchable = 'false'
}
if (touchable) {
const attr = el.attrs.find(attr => attr.name === ':touchable')
if (attr) {
attr.value = touchable
} else {
el.attrs.push({
name: ':touchable',
value: touchable
})
}
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
const {
hasOwn
} = require('./util')
const onRE = /^@|^v-on:/
function removeAttr (el, name) {
if (hasOwn(el.attrsMap, name)) {
delete el.attrsMap[name]
el.attrsList.splice(el.attrsList.findIndex(attr => attr.name === name), 1)
return true
}
}
module.exports = {
preTransformNode (el, {
warn
}) {
const attrsMap = el.attrsMap
if (el.tag === 'slot' && !(attrsMap.name || attrsMap[':name'])) {
el.attrsList.push({
name: 'SLOT_DEFAULT',
value: true
})
attrsMap.SLOT_DEFAULT = true
}
// 处理 attr
el.attrsList.forEach(attr => {
if (
attr.name.indexOf('v-model') === 0 &&
attr.name.indexOf('.lazy') !== -1
) {
const origName = attr.name
const newName = origName.replace('.lazy', '')
attr.name = newName
attrsMap[newName] = attr.value
delete attrsMap[origName]
} else if (onRE.test(attr.name) && !attr.value.trim()) { // 事件为空
attr.value = '__HOLDER__'
attrsMap[attr.name] = attr.value
}
})
// 暂不支持的指令
const dirs = ['v-once', 'v-pre', 'v-cloak']
dirs.forEach(dir => {
if (removeAttr(el, dir)) {
warn(`unsupported directive ${dir}`, false, true)
}
})
//
}
}
+24
View File
@@ -0,0 +1,24 @@
const AD_COMPONENTS = ['uniad', 'ad-rewarded-video', 'ad-fullscreen-video', 'ad-interstitial']
module.exports = {
preTransformNode (el, {
warn
}) {
if (process.env.UNI_PLATFORM === 'mp-weixin') {
if (el.tag === 'ad' && (el.attrsMap.adpid || el.attrsMap[':adpid'])) {
el.tag = 'uniad'
}
if (AD_COMPONENTS.indexOf(el.tag) > -1) {
process.env.USE_UNI_AD = true
process.env.HAS_WXAD = '1'
}
} else if (process.env.UNI_PLATFORM === 'mp-alipay') {
if (el.tag === 'ad' && (el.attrsMap.adpid || el.attrsMap[':adpid'])) {
el.tag = 'uniad'
}
if (AD_COMPONENTS.indexOf(el.tag) > -1) {
process.env.USE_UNI_AD_ALIPAY = true
}
}
}
}
+242
View File
@@ -0,0 +1,242 @@
const uniI18n = require('@dcloudio/uni-cli-i18n')
const EVENTS = {
click: 'tap'
}
const tags = {
// 小程序平台通用组件
base: [
'slot',
'block',
'component',
'template',
'ad',
'audio',
'button',
'camera',
'canvas',
'checkbox',
'checkbox-group',
'cover-image',
'cover-view',
'form',
'functional-page-navigator',
'icon',
'image',
'input',
'label',
'live-player',
'live-pusher',
'map',
'movable-area',
'movable-view',
'navigator',
'official-account',
'open-data',
'picker',
'picker-view',
'picker-view-column',
'progress',
'radio',
'radio-group',
'rich-text',
'scroll-view',
'slider',
'swiper',
'swiper-item',
'switch',
'text',
'textarea',
'video',
'view',
'web-view',
'editor'
],
'mp-baidu': [
'animation-video',
'animation-view',
'ar-camera',
'rtc-room',
'rtc-room-item',
'tabs',
'tab-item',
'follow-swan',
'login',
'inline-payment-panel',
'talos-linear-gradient',
'talos-rc-view',
'talos-nested-scroll-view',
'talos-nested-scroll-top-container',
'talos-nested-scroll-bottom-container',
'talos-waterfall-view',
'talos-waterfall-item',
'talos-waterfall-header',
'talos-waterfall-footer',
'talos-pull-refresh',
'talos-control-container',
'talos-na-refresh-control',
'talos-modal',
'talos-svg'
],
'mp-weixin': [
'page-container',
'page-meta',
'navigation-bar',
'match-media',
'share-element',
'channel-live',
'channel-video',
'voip-room',
'root-portal',
'subscribe',
// 手势组件
'tap-gesture-handler',
'double-tap-gesture-handler',
'scale-gesture-handler',
'force-press-gesture-handler',
'pan-gesture-handler',
'vertical-drag-gesture-handler',
'horizontal-drag-gesture-handler',
'long-press-gesture-handler',
// 其他
'draggable-sheet',
'grid-builder',
'grid-view',
'list-view',
'list-builder',
'nested-scroll-body',
'nested-scroll-header',
'open-container',
'share-element',
'snapshot',
// 'span', // todo: 临时移除 span 的支持,后续判断 skyline 环境进行区分 ask 190418
'sticky-header',
'sticky-section',
'open-data-list',
'open-data-item'
],
// 支付宝小程序平台独有组件
'mp-alipay': [
'lifestyle',
'life-follow',
'contact-button',
'spread',
'error-view',
'poster',
'cashier',
'ix-grid',
'ix-native-grid',
'ix-native-list',
'mkt',
'page-container',
'page-meta',
'lottie',
'join-group-chat',
'subscribe-message'
],
// 抖音小程序平台独有组件
'mp-toutiao': [
'aweme-data',
'consume-card',
'pay-button',
'rate-button',
'member-button',
'confirm-receipt-button',
'live-preview',
'aweme-live-book',
'aweme-user-card',
'rtc-room'
],
'mp-kuaishou': [
'follow-service',
'payment-list',
'playlet'
]
}
const baseCompiler = {
ref: 'data-ref',
refInFor: 'data-ref-in-for',
specialEvents: {},
/**
* TODO 暂时先简单判断是不是自定义组件,
* 如果要依赖真实导入的组件识别,需要 template-loader 与 script-loader 结合,
* 目前 template 在前,script 在后,要做的话,就需要把 wxml 的生成机制放到 plugin 中才可以拿到真实的组件列表
*/
isComponent (tagName) {
return !this.isNativeTag(tagName)
},
isNativeTag (tagName) {
return tags.base.concat(tags[this.name] || []).includes(tagName)
},
createFilterTag (filterTag, {
content,
attrs
}) {
content = content.trim()
if (content) {
return `<${filterTag} module="${attrs.module}">
${content}
</${filterTag}>`
} else if (attrs.src) {
return `<${filterTag} src="${attrs.src}" module="${attrs.module}"></${filterTag}>`
}
},
getEventType (eventType) {
return EVENTS[eventType] || eventType
},
formatEventType (eventName, isCatch, isCapture, isCustom) {
let eventType = 'bind'
if (isCatch) {
eventType = 'catch'
}
if (isCapture) {
return `capture-${eventType}:${eventName}`
}
if (isCustom) {
return `${eventType}:${eventName}`
}
return `${eventType}${eventName}` // 原生组件不支持 bind:input 等写法,统一使用 bindinput
},
createScopedSlots (slotName, props, state) {
state.errors.add(uniI18n.__('templateCompiler.notCurrentlySupportScopedSlot', {
0: `[${slotName}]`
}))
return {
type: 'slot',
attr: {
name: slotName
},
children: []
}
},
resolveScopedSlots (slotName, componentName, paramExprNode, returnExprNodes, {
traverseExpr,
normalizeChildren
}, state) {
state.errors.add(uniI18n.__('templateCompiler.notCurrentlySupportScopedSlot', {
0: `[${slotName}]`
}))
return {
type: 'view',
attr: {
slot: slotName
},
children: []
}
}
}
module.exports = function getCompilerOptions (platform) {
let id = '@dcloudio/uni-' + platform
if (global.uniPlugin) {
id = global.uniPlugin.id
}
return Object.assign({
name: platform
},
baseCompiler,
require(id + '/lib/uni.compiler.js')
)
}
@@ -0,0 +1,4 @@
const mpWeixin = require('./mp-weixin')
module.exports = Object.assign({}, mpWeixin, {
prefix: 'ks:'
})
+5
View File
@@ -0,0 +1,5 @@
const babelGenerate = require('@babel/generator').default
module.exports = function generate (ast, state) {
return babelGenerate(ast, state.options).code
}
@@ -0,0 +1,29 @@
const {
IDENTIFIER_ATTR
} = require('../../../constants')
const {
isSimpleObjectExpression,
hasEscapeQuote
} = require('../../../util')
const getMemberExpr = require('../member-expr')
function checkObjectExpression (path) {
return path.isObjectExpression() && !isSimpleObjectExpression(path.node)
}
module.exports = function processAttrs (paths, path, state, isComponent, tagName) {
const attrsPath = paths.attrs
if (attrsPath) {
attrsPath.get('value.properties').forEach(propertyPath => {
const valuePath = propertyPath.get('value')
// 对于简单的ObjectExpression不再单独处理,改为在转换temlplte时用()包裹(微信、QQ
// 属性中包含转义引号时部分小程序平台报错或显示异常
if (checkObjectExpression(valuePath) || hasEscapeQuote(valuePath)) {
valuePath.replaceWith(getMemberExpr(path, IDENTIFIER_ATTR, valuePath.node, state))
}
})
}
return []
}
@@ -0,0 +1,143 @@
const t = require('@babel/types')
const uniI18n = require('@dcloudio/uni-cli-i18n')
const {
VIRTUAL_HOST_CLASS
} = require('../../../constants')
const {
getCode,
isRootElement
} = require('../../../util')
function processClassArrayExpressionElements (classArrayExpression) {
let binaryExpression
classArrayExpression.elements.forEach(expr => {
if (t.isArrayExpression(expr)) {
expr = processClassArrayExpressionElements(expr)
}
if (!binaryExpression) {
binaryExpression = t.parenthesizedExpression(expr)
} else {
binaryExpression = t.parenthesizedExpression(t.binaryExpression(
'+',
t.binaryExpression(
'+',
binaryExpression,
t.stringLiteral(' ')
),
expr
))
}
})
return binaryExpression
}
function processStaticClass (classArrayExpression, staticClassPath, state) {
if (staticClassPath) {
const staticClassPathArr = staticClassPath.node.value.value.split(' ')
for (let len = staticClassPathArr.length, index = len - 1; index >= 0; index--) {
classArrayExpression.elements.unshift(t.stringLiteral(staticClassPathArr[index]))
}
staticClassPath.remove()
}
const transPlatform = ['mp-toutiao', 'mp-alipay', 'mp-lark']
if (transPlatform.includes(state.options.platform.name)) {
// classArrayExpression => binaryExpression
return processClassArrayExpressionElements(classArrayExpression)
}
return classArrayExpression
}
function processClassObjectExpression (classValuePath) {
const elements = []
const propertyPaths = classValuePath.get('properties')
propertyPaths.forEach(propertyPath => {
const key = propertyPath.node.key
elements.push(
t.conditionalExpression(
t.parenthesizedExpression(propertyPath.node.value),
propertyPath.node.computed ? t.parenthesizedExpression(key) : t.stringLiteral(key.name || key.value),
t.stringLiteral('')
)
)
})
return t.arrayExpression(elements)
}
function processClassArrayExpression (classValuePath) {
const elementPaths = classValuePath.get('elements')
elementPaths.forEach(elementPath => {
if (elementPath.isObjectExpression()) {
elementPath.replaceWith(processClassObjectExpression(elementPath))
}
})
return classValuePath.node
}
module.exports = function processClass (paths, path, state) {
const classPath = paths.class
const staticClassPath = paths.staticClass
const mergeVirtualHostAttributes = state.options.mergeVirtualHostAttributes
let classArrayExpression
if (classPath) {
const classValuePath = classPath.get('value')
if (classValuePath.isObjectExpression()) { // object
classArrayExpression = processClassObjectExpression(classValuePath)
} else if (classValuePath.isArrayExpression()) { // array
classArrayExpression = processClassArrayExpression(classValuePath)
} else if (
classValuePath.isStringLiteral() || // :class="'a'"
classValuePath.isIdentifier() || // TODO 需要优化到下一个条件,:class="classObject"
classValuePath.isMemberExpression() || // 需要优化到下一个条件,:class="item.classObject"
classValuePath.isConditionalExpression() ||
classValuePath.isLogicalExpression() ||
classValuePath.isBinaryExpression()
) {
// 理论上 ConditionalExpression,LogicalExpression 可能存在 classObject,应该__get_class,还是先不考虑这种情况吧
// ConditionalExpression :class="index === currentIndex ? activeStyle : itemStyle"
// BinaryExpression :class="'m-content-head-'+message.user"
classArrayExpression = t.arrayExpression([classValuePath.node])
} else if (
classValuePath.isIdentifier() ||
classValuePath.isMemberExpression()
) { // classObject :class="classObject" :class="vm.classObject"
// TODO 目前先不考虑 classObject,styleObject
// const args = [classPath.node.value]
// if (staticClassPath) {
// args.push(staticClassPath.node.value)
// staticClassPath.remove()
// }
// classValuePath.replaceWith(
// getMemberExpr(
// classPath,
// IDENTIFIER_CLASS,
// t.callExpression(t.identifier(INTERNAL_GET_CLASS), args),
// state
// )
// )
} else {
state.errors.add(':class' + uniI18n.__('templateCompiler.noSupportSyntax', { 0: getCode(classValuePath.node) }))
}
}
if (mergeVirtualHostAttributes && isRootElement(path.parentPath)) {
const virtualHostClass = t.identifier(VIRTUAL_HOST_CLASS)
if (classArrayExpression) {
classArrayExpression.elements.push(virtualHostClass)
} else {
classArrayExpression = t.arrayExpression([virtualHostClass])
const property = t.objectProperty(t.identifier('class'), processStaticClass(classArrayExpression, staticClassPath, state))
path.node.properties.push(property)
return []
}
}
if (classArrayExpression) {
const classValuePath = classPath.get('value')
classValuePath.replaceWith(processStaticClass(classArrayExpression, staticClassPath, state))
}
return []
}
@@ -0,0 +1,71 @@
const t = require('@babel/types')
const {
getModelEventFunctionExpr
} = require('./util')
module.exports = function processDir (paths, path, state) {
const directivesPath = paths.directives
if (directivesPath) {
/**
* directives: [{
* name: "model",
* rawName: "v-model",
* value: (aaa.cart_amount),
* expression: "aaa.cart_amount"
*}],
*/
const modelObjectExpr = directivesPath.node.value.elements.find(
objectExpression => {
return objectExpression.properties.find(property => {
return property.key.name === 'name' && property.value.value === 'model'
})
}
)
if (modelObjectExpr) {
const exprProperty = modelObjectExpr.properties.find(property => {
return property.key.name === 'expression'
})
const modifiersProperty = modelObjectExpr.properties.find(property => {
return property.key.name === 'modifiers'
})
if (exprProperty) {
const onPath = paths.on
const existingInput = onPath.node.value.properties.find(
property => property.key.value === 'input'
)
if (existingInput) {
let existingInputFuncExpr
// remove old model input event
if (!t.isArrayExpression(existingInput.value)) {
existingInputFuncExpr = existingInput.value
existingInput.value = t.arrayExpression([])
} else {
existingInputFuncExpr = existingInput.value.elements.shift()
}
if (existingInputFuncExpr) {
const modifiers = []
if (modifiersProperty) {
const properties = modifiersProperty.value.properties
if (properties.find(property => property.key.value === 'number')) {
modifiers.push(t.stringLiteral('number'))
}
if (properties.find(property => property.key.value === 'trim')) {
modifiers.push(t.stringLiteral('trim'))
}
}
existingInput.value.elements.unshift(
getModelEventFunctionExpr(
existingInputFuncExpr,
exprProperty.value.value.trim(),
modifiers
)
)
}
}
}
}
}
return []
}
@@ -0,0 +1,581 @@
const t = require('@babel/types')
const template = require('@babel/template').default
const {
IDENTIFIER_EVENT,
VUE_EVENT_MODIFIERS,
INTERNAL_EVENT_PROXY,
ATTR_DATA_EVENT_OPTS,
ATTR_DATA_EVENT_PARAMS,
INTERNAL_SET_SYNC,
INTERNAL_SET_MODEL,
ALLOWED_GLOBAL_OBJECT
} = require('../../../constants')
const {
getCode,
customize,
processMemberExpression,
replaceMemberExpression,
hasMemberExpression
} = require('../../../util')
const {
getEventExpressionStatement
} = require('../statements')
const defaultArgs = t.arrayExpression([t.stringLiteral('$event')])
function addEventExpressionStatement (funcPath, state, isCustom) {
const identifier = t.identifier(IDENTIFIER_EVENT)
const stringLiteral = t.stringLiteral(IDENTIFIER_EVENT)
state.identifierArray.push([identifier, stringLiteral])
state.initExpressionStatementArray.push(getEventExpressionStatement(identifier, funcPath.node))
const arrayExpression = [
stringLiteral
]
const args = []
if (!isCustom) { // native events
args.push(t.stringLiteral('$event'))
arrayExpression.push(t.arrayExpression(args))
} else { // custom events
}
// if (state.scoped) { // add forItem,forIndex
// const scopedArgs = []
// state.scoped.forEach(scoped => {
// if (scoped.forIndex && scoped.forIndex !== scoped.forItem) {
// scopedArgs.push(t.identifier(scoped.forIndex))
// }
// scopedArgs.push(t.identifier(scoped.forItem))
// })
// scopedArgs.reverse().forEach(arg => {
// args.push(arg)
// })
// }
return t.arrayExpression(arrayExpression)
}
function getIdentifierName (element) {
if (t.isMemberExpression(element)) {
return getIdentifierName(element.object)
}
return element.name.split('.')[0]
}
function getScoped (scopedArray, element, methodName, state) {
const identifierName = getIdentifierName(element)
const scoped = scopedArray.find(scoped => {
if (scoped.forItem === identifierName && !scoped.forKey) {
return true
}
})
if (scoped) {
const forExtra = t.cloneDeep(t.arrayExpression(scoped.forExtra))
if (t.isMemberExpression(element)) {
// 简单处理
// item['order']=>item.order
element = processMemberExpression(element, state)
// v-for="item in data.items" :key="item.data.id"
// v-for="meta in item.metas" :key="meta.id" @tap="change(meta,meta.b,true)"
// ['data.items','data.id',item.data.id]
// ['metas','id',meta.id]=>['metas','id',meta.id,'b']
forExtra.elements[forExtra.elements.length - 1].elements.push(
t.stringLiteral(
getExtraDataPath(
getCode(element).replace(scoped.forItem + '.', ''), methodName
)
)
)
}
return forExtra
}
}
function isForIndex (scopedArray, element) {
if (t.isIdentifier(element)) {
return scopedArray.find(scoped => {
if (scoped.forIndex === element.name) {
return true
}
})
}
return false
}
function isForItem (scopedArray, element) {
if (t.isIdentifier(element)) {
return scopedArray.find(scoped => {
if (scoped.forItem === element.name) {
return true
}
})
}
return false
}
function isForKey (scopedArray, element) {
if (t.isIdentifier(element)) {
return scopedArray.find(scoped => {
if (scoped.forKey === element.name) {
return true
}
})
}
return false
}
function getExtraDataPath (dataPath, methodName) {
if (methodName === INTERNAL_SET_SYNC) {
const dataPaths = dataPath.split('.')
dataPaths.pop()
return dataPaths.join('.')
}
return dataPath
}
function parseMethod (method, state) {
const elements = method.elements
const methodName = elements[0].value
const argsArrayExpr = elements[1]
if (argsArrayExpr) {
const extraArrayElements = []
argsArrayExpr.elements = argsArrayExpr.elements.map((element) => {
if (t.isIdentifier(element) || t.isMemberExpression(element)) { // item or item.b
if (state.scoped.length) {
const forExtra = getScoped(state.scoped, element, methodName, state)
if (!forExtra) {
if (isForIndex(state.scoped, element) || isForItem(state.scoped, element) || isForKey(state.scoped, element)) {
return element
} else {
extraArrayElements.push(replaceMemberExpression(t.stringLiteral(
getExtraDataPath(getCode(processMemberExpression(element, state)),
methodName)
), state))
}
} else {
extraArrayElements.push(forExtra)
}
} else {
extraArrayElements.push(replaceMemberExpression(t.stringLiteral(
getExtraDataPath(getCode(processMemberExpression(element, state)), methodName)
), state))
}
return t.stringLiteral('$' + (extraArrayElements.length - 1))
} else if ( // +1=>1
t.isUnaryExpression(element) &&
element.operator === '+' &&
t.isNumericLiteral(element.argument)
) {
element = t.numericLiteral(element.argument.value)
} else if (t.isObjectExpression(element)) {
// {name:'a',b:'c',d:123}=>[['name','a'],['b','c'],['d',123]]
const objectExprElements = [
t.stringLiteral('o')
]
element.properties.forEach(property => {
objectExprElements.push(t.arrayExpression([
t.stringLiteral(property.key.name || property.key.value),
t.cloneDeep(property.value)
]))
})
element = t.arrayExpression(objectExprElements)
}
return element
})
if (extraArrayElements.length) {
elements.push(t.arrayExpression(extraArrayElements))
}
}
}
function getMethodName (methodName) {
return methodName === '__HOLDER__' ? '' : methodName
}
function parseEventByCallExpression (callExpr, methods) {
let methodName = callExpr.callee.name
if (methodName === '$set') {
methodName = INTERNAL_SET_SYNC
}
const arrayExpression = [t.stringLiteral(getMethodName(methodName))]
const args = callExpr.arguments
if (methodName === INTERNAL_SET_SYNC) {
// v-bind:title.sync="doc.title"
// ['$set',['doc.a','title','$event']]
const argsExpression = []
argsExpression.push(
t.memberExpression(args[0], t.identifier(args[1].value))
)
argsExpression.push(t.stringLiteral(args[1].value))
argsExpression.push(t.stringLiteral('$event'))
arrayExpression.push(t.arrayExpression(argsExpression))
} else {
if (args.length) {
const argsExpression = []
args.forEach(arg => {
if (t.isIdentifier(arg) && arg.name === '$event') {
argsExpression.push(t.stringLiteral('$event'))
} else {
argsExpression.push(arg)
}
})
arrayExpression.push(t.arrayExpression(argsExpression))
}
}
methods.push(t.arrayExpression(arrayExpression))
}
function isValuePath (path) {
return path.key !== 'key' && path.key !== 'id' && (path.key !== 'property' || path.parent.computed) && !(path.key === 'value' && path.parentPath.parentPath.isObjectPattern()) && !(path.key === 'left' && path.parentPath.parentPath.parentPath.isObjectPattern())
}
const isSafeScoped = (state) => {
const scopedArray = state.scoped
let checkForIndex = false
for (let index = 0; index < scopedArray.length; index++) {
const scoped = scopedArray[index]
const arrayExtra = scoped.forExtra[0].elements[0].value
// 判断仅外层遍历对象是否包含了 index 参数
if (checkForIndex && scoped.forIndex && scoped.forKey) {
return false
}
if (index === 0 && !(scoped.forIndex && scoped.forKey)) {
checkForIndex = true
}
// 简易判断 v-for 中是否包含复杂表达式:数组、对象、方法
if (typeof arrayExtra === 'string' && (arrayExtra.startsWith('[') || arrayExtra.startsWith('{') || /\(.*\)/.test(arrayExtra))) {
return false
}
}
return true
}
function parseEvent (keyPath, valuePath, state, isComponent, isNativeOn = false, tagName, ret) {
const key = keyPath.node
let type = key.value || key.name || ''
const isCustom = isComponent && !isNativeOn
let isCatch = false
let isCapture = false
let isPassive = false
let isOnce = false
const methods = []
const params = []
if (type) {
isPassive = type.charAt(0) === VUE_EVENT_MODIFIERS.passive
type = isPassive ? type.slice(1) : type
isOnce = type.charAt(0) === VUE_EVENT_MODIFIERS.once // Prefixed last, checked first
type = isOnce ? type.slice(1) : type
isCapture = type.charAt(0) === VUE_EVENT_MODIFIERS.capture
type = isCapture ? type.slice(1) : type
const specialEvents = state.options.platform.specialEvents
const isSpecialEvent = specialEvents[tagName] && Object.keys(specialEvents[tagName]).includes(type)
if (!valuePath.isArrayExpression()) {
valuePath = [valuePath]
} else {
valuePath = valuePath.get('elements')
}
valuePath.forEach(funcPath => {
if ( // wxs event
funcPath.isMemberExpression() &&
t.isIdentifier(funcPath.node.object) &&
state.options.filterModules.includes(funcPath.node.object.name)
) {
const {
getEventType,
formatEventType
} = state.options.platform
const wxsEventType = formatEventType(getEventType(type))
if (key.value) {
key.value = wxsEventType
} else {
key.name = wxsEventType
}
} else if (funcPath.isIdentifier()) { // on:{click:handle}
if (!isSpecialEvent) {
const arrayExpression = [t.stringLiteral(getMethodName(funcPath.node.name))]
if (!isCustom) { // native events
arrayExpression.push(defaultArgs)
}
methods.push(t.arrayExpression(arrayExpression))
} else {
if (!state.options.specialMethods) {
state.options.specialMethods = new Set()
}
state.options.specialMethods.add(funcPath.node.name)
}
} else if (isSpecialEvent) {
state.errors.add(
`${tagName} 组件 ${type} 事件仅支持 @${type}="methodName" 方式绑定`
)
} else {
let anonymous = true
// "click":function($event) {click1(item);click2(item);}
const body = funcPath.node.body && funcPath.node.body.body
const funcParams = funcPath.node.params
if (body && body.length && funcParams && funcParams.length === 1 && !hasMemberExpression(funcPath) && isSafeScoped(state)) {
const exprStatements = body.filter(node => {
return t.isExpressionStatement(node) && t.isCallExpression(node.expression) && !node.expression.arguments.find(element => {
// click1(item().a)
if (t.isMemberExpression(element)) {
try {
getIdentifierName(element)
} catch {
return true
}
}
})
})
if (exprStatements.length === body.length) {
const paramPath = funcPath.get('params')[0]
const paramName = paramPath.node.name
if (paramName !== '$event') {
funcPath.get('body').traverse({
Identifier (path) {
const node = path.node
const binding = path.scope.getBinding(node.name)
if (binding && binding.identifier === paramPath.node && isValuePath(path)) {
path.replaceWith(t.identifier('$event'))
}
}
})
paramPath.replaceWith(t.identifier('$event'))
}
anonymous = false
exprStatements.forEach(exprStatement => {
parseEventByCallExpression(exprStatement.expression, methods)
})
}
}
const testCatch = function (stop) {
return function (path) {
// TODO 仅使用 name 容易误判
if (path.node.object.name === '$event' && path.node.property.name ===
'stopPropagation') {
isCatch = true
stop && path.stop()
}
}
}
// 如果 v-for 遍历的值为 数组、对象、方法 则进入底部匿名表达式处理
if (anonymous && isSafeScoped(state)) {
funcPath.traverse({
noScope: true,
MemberExpression: testCatch(),
AssignmentExpression (path) { // "update:title": function($event) {title = $event}
const left = path.node.left
const right = path.node.right
// v-bind:title.sync="title"
if (t.isIdentifier(left) &&
t.isIdentifier(right) &&
right.name === '$event' &&
type.indexOf('update:') === 0) {
methods.push(t.arrayExpression( // ['$set',['title','$event']]
[
t.stringLiteral(INTERNAL_SET_SYNC),
t.arrayExpression([
t.identifier(left.name),
t.stringLiteral(left.name),
t.stringLiteral('$event')
])
]
))
anonymous = false
path.stop()
}
},
ReturnStatement (path) {
const argument = path.node.argument
if (t.isCallExpression(argument)) {
if (t.isIdentifier(argument.callee)) { // || t.isMemberExpression(argument.callee)
anonymous = false
parseEventByCallExpression(argument, methods)
}
}
}
})
}
if (anonymous) {
// 处理复杂表达式中使用的局部变量(主要在v-for中定义)
funcPath.traverse({
MemberExpression: testCatch(),
Identifier (path) {
const scope = path.scope
const node = path.node
const name = node.name
if (!ALLOWED_GLOBAL_OBJECT.includes(name) && isValuePath(path) && scope && !scope.hasOwnBinding(name) && scope.hasBinding(name) && !params.includes(name) && name !== 'undefined') {
params.push(name)
}
}
})
params.forEach(name => {
funcPath.node.params.push(t.identifier(name))
})
if (params.length) {
if (!isCustom) {
const bodyStatements = funcPath.get('body.body')
const returnStatement = bodyStatements[0]
if (t.isReturnStatement(returnStatement) && t.isCallExpression(returnStatement.node.argument) && returnStatement.node.argument.callee.name === INTERNAL_SET_MODEL) {
funcPath.node.body.body.unshift(template('$event=$event.target.value')())
}
}
let argumentsName = 'arguments'
if (funcPath.isArrowFunctionExpression()) {
argumentsName = 'args'
funcPath.node.params.push(t.restElement(t.identifier(argumentsName)))
}
const datasetUid = funcPath.scope.generateDeclaredUidIdentifier().name
const paramsUid = funcPath.scope.generateDeclaredUidIdentifier().name
const dataset = ATTR_DATA_EVENT_PARAMS.substring(5)
const code = `var ${datasetUid}=${argumentsName}[${argumentsName}.length-1].currentTarget.dataset,${paramsUid}=${datasetUid}.${dataset.replace(/-([a-z])/, (_, str) => str.toUpperCase())}||${datasetUid}['${dataset}'],${params.map(item => `${item}=${paramsUid}.${item}`).join(',')}`
funcPath.node.body.body.unshift(template(code, { syntacticPlaceholders: true })())
}
methods.push(addEventExpressionStatement(funcPath, state, isComponent, isNativeOn))
}
}
})
}
return {
type,
params,
methods,
modifiers: {
isCatch,
isCapture,
isPassive,
isOnce,
isCustom
}
}
}
function _processEvent (path, state, isComponent, isNativeOn = false, tagName, ret) {
const opts = []
// remove invalid event
path.node.value.properties = path.node.value.properties.filter(property => {
return property.key.value || property.key.name
})
const len = path.node.value.properties.length
for (let i = 0; i < len; i++) {
const propertyPath = path.get(`value.properties.${i}`)
const keyPath = propertyPath.get('key')
const valuePath = propertyPath.get('value')
const {
type,
params,
methods,
modifiers: {
isCatch,
isCapture,
isOnce,
isCustom
}
} = parseEvent(
keyPath,
valuePath,
state,
isComponent,
isNativeOn,
tagName,
ret
)
if (!methods.length) {
continue
}
methods.forEach(method => {
parseMethod(method, state) // 解析参数
})
const getEventType = state.options.platform.getEventType
let optType = isCustom ? customize(type) : getEventType(type) // 比如自定义组件使用了 click 自定义事件
if (isOnce) {
optType = VUE_EVENT_MODIFIERS.once + optType
}
if (isCustom) {
optType = VUE_EVENT_MODIFIERS.custom + optType
}
opts.push({
opt: t.arrayExpression([
t.stringLiteral(optType),
t.arrayExpression(methods)
]),
params
})
keyPath.replaceWith(
t.stringLiteral(
state.options.platform.formatEventType(
isCustom ? customize(type) : getEventType(type), // 比如自定义组件使用了 click 自定义事件
isCatch,
isCapture,
isCustom
)
)
)
valuePath.replaceWith(t.stringLiteral(INTERNAL_EVENT_PROXY))
}
return opts
}
module.exports = function processEvent (paths, path, state, isComponent, tagName) {
const onPath = paths.on
const nativeOnPath = paths.nativeOn
const ret = []
const opts = []
const params = []
if (onPath) {
_processEvent(onPath, state, isComponent, false, tagName, ret).forEach(({ opt, params: array }) => {
opts.push(opt)
params.push(...array)
})
}
if (nativeOnPath) {
_processEvent(nativeOnPath, state, isComponent, true, tagName, ret).forEach(({ opt, params: array }) => {
opts.push(opt)
params.push(...array)
})
}
if (!opts.length) {
return ret
}
ret.push(
t.objectProperty(
t.stringLiteral(ATTR_DATA_EVENT_OPTS),
t.arrayExpression(opts)
)
)
if (params.length) {
ret.push(
t.objectProperty(
t.stringLiteral(ATTR_DATA_EVENT_PARAMS),
t.objectExpression(params.map(param => t.objectProperty(t.identifier(param), t.identifier(param), false, true)))
)
)
}
return ret
}
@@ -0,0 +1,156 @@
const t = require('@babel/types')
const {
METHOD_CREATE_ELEMENT,
ATTR_DATA_EVENT_OPTS,
ATTR_DATA_COM_TYPE,
ATTR_DATA_EVENT_LIST,
ATTR_DATA_EVENT_PARAMS,
ATTR_DATA_CUSTOM_HIDDEN,
INTERNAL_EVENT_WRAP
} = require('../../../constants')
const processRef = require('./ref')
const processAttrs = require('./attrs')
const processClass = require('./class')
const processEvent = require('./event')
const processStyle = require('./style')
const processModel = require('./model')
const processDir = require('./directives')
module.exports = function traverseData (path, state, tagName) {
if (path.node.$mpProcessed) {
return
}
path.node.$mpProcessed = true
const paths = {}
const propertyPaths = path.get('properties')
propertyPaths.forEach((propertyPath, index) => {
paths[propertyPath.node.key.name] = propertyPath
})
const addAttrProperties = []
const isComponent = state.options.platform.isComponent(tagName)
const processes = [processAttrs, processRef, processClass, processModel, processDir, processEvent, processStyle]
// ref(add staticClass) > class,model,dir(add input event)>event
processes.forEach(process => {
process(paths, path, state, isComponent, tagName).forEach((property) => {
addAttrProperties.push(property)
})
})
// 该组件是引入的小程序组件
const wxComponent = state.options.wxComponents[tagName]
if (wxComponent) {
addAttrProperties.push(
t.objectProperty(
t.stringLiteral(ATTR_DATA_COM_TYPE),
t.stringLiteral('wx')
)
)
if (state.options.platform.name === 'mp-alipay') {
if (!wxComponent.startsWith('plugin://')) {
addAttrProperties.push(
t.objectProperty(
t.stringLiteral('ref'),
t.stringLiteral('__r')
)
)
}
const on = path.node.properties.find(prop => prop.key.name === 'on')
if (on) {
const properties = on.value.properties
const list = []
for (let index = 0; index < properties.length; index++) {
const element = properties[index]
if (element.value.value === '__e') {
list.push(element.key.value)
}
}
if (list.length) {
addAttrProperties.push(
t.objectProperty(
t.stringLiteral(ATTR_DATA_EVENT_LIST),
t.stringLiteral(list.join(','))
)
)
}
}
if (wxComponent.startsWith('plugin://')) {
const wrapperTag = 'plugin-wrapper'
const orgPath = path.parentPath
const orgNode = orgPath.node
const args = orgNode.arguments
const orgTag = args[0]
orgTag.$mpPlugin = true
args[0] = t.stringLiteral(wrapperTag)
const orgOptions = args[1]
const orgOptionsProps = orgOptions.properties
const targetAttrs = []
const targetOptionsProps = [
t.objectProperty(t.identifier('attrs'), t.objectExpression(targetAttrs))
]
const uniAttrs = [
ATTR_DATA_EVENT_OPTS,
ATTR_DATA_COM_TYPE,
ATTR_DATA_EVENT_PARAMS,
ATTR_DATA_EVENT_LIST,
ATTR_DATA_CUSTOM_HIDDEN,
'vue-id'
]
for (let a = orgOptionsProps.length - 1; a >= 0; a--) {
const prop = orgOptionsProps[a]
if (prop.key.name === 'attrs') {
const attrs = prop.value.properties
for (let b = attrs.length - 1; b >= 0; b--) {
const element = attrs[b]
const key = element.key.value
if (!uniAttrs.includes(key)) {
attrs.splice(b, 1)
targetAttrs.push(element)
}
}
attrs.push(t.objectProperty(t.stringLiteral('onPluginWrap'), t.stringLiteral(INTERNAL_EVENT_WRAP)))
} else if (prop.key.name === 'on') {
const ons = prop.value.properties
ons.forEach(item => {
const attrs = path.node.properties.find(prop => prop.key.name === 'attrs').value.properties
const vueId = attrs.find(prop => prop.key.value === 'vue-id').value
const eventName = item.key.value
targetAttrs.push(t.objectProperty(t.stringLiteral(eventName), t.binaryExpression('+', t.stringLiteral(eventName), vueId)))
})
} else {
orgOptionsProps.splice(a, 1)
targetOptionsProps.push(prop)
}
}
const orgChild = args[2]
const targetOptions = t.objectExpression(targetOptionsProps)
targetOptions.$mpProcessed = true
const targetArguments = [
orgTag,
targetOptions
]
if (orgChild) {
targetArguments.push(orgChild)
}
const targetNode = t.callExpression(t.identifier(METHOD_CREATE_ELEMENT), targetArguments)
args[2] = targetNode
}
}
}
if (addAttrProperties.length) {
const attrsPath = paths.attrs
if (attrsPath) {
attrsPath.node.value.properties = attrsPath.node.value.properties.concat(addAttrProperties)
} else {
path.node.properties.unshift(
t.objectProperty(t.identifier('attrs'), t.objectExpression(addAttrProperties))
)
}
}
}
@@ -0,0 +1,74 @@
const t = require('@babel/types')
const {
getModelEventFunctionExpr
} = require('./util')
module.exports = function processRef (paths, path, state) {
const modelPath = paths.model
if (modelPath) {
const properties = modelPath.node.value.properties
const [callbackProperty] = properties.splice(properties.findIndex(property => {
return property.key.name === 'callback'
}), 1)
const valueProperty = properties.find(
property => property.key.name === 'value'
)
const exprProperty = properties.find(
property => property.key.name === 'expression'
)
const prop = exprProperty.value.value.trim()
const onPath = paths.on
// on:{'input':__m('msg',$event)}
if (!onPath) {
path.node.properties.unshift(
t.objectProperty(t.identifier('on'), t.objectExpression([
t.objectProperty(
t.stringLiteral('input'),
getModelEventFunctionExpr(
callbackProperty.value,
prop
)
)
]))
)
paths.on = path.get('properties').find(
propertyPath => propertyPath.node.key.name === 'on'
)
} else {
const existingInput = onPath.node.value.properties.find(
property => property.key.value === 'input'
)
if (existingInput) {
if (!t.isArrayExpression(existingInput.value)) {
existingInput.value = t.arrayExpression([existingInput.value])
}
existingInput.value.elements.unshift(getModelEventFunctionExpr(
callbackProperty.value,
prop
))
} else {
onPath.node.value.properties.push(
t.objectProperty(
t.stringLiteral('input'),
getModelEventFunctionExpr(
callbackProperty.value,
prop
)
)
)
}
}
return [ // attrs:{value:value}
t.objectProperty(
t.stringLiteral(process.env.UNI_USING_VUE3 ? 'modelValue' : 'value'),
valueProperty.value
)
]
}
return []
}
@@ -0,0 +1,43 @@
const t = require('@babel/types')
const {
CLASS_REF,
CLASS_REF_IN_FOR
} = require('../../../constants')
module.exports = function processRef (paths, path, state) {
const refPath = paths.ref
if (refPath) {
if (state.options.platform.name === 'mp-alipay') {
return [
t.objectProperty( // data-ref="" ,data-ref-in-for=""
t.stringLiteral('ref'),
t.stringLiteral('__r')
),
t.objectProperty( // data-ref="" ,data-ref-in-for=""
t.stringLiteral(state.inFor ? state.options.platform.refInFor : state.options.platform.ref),
refPath.node.value
)
]
}
const refClass = state.inFor ? CLASS_REF_IN_FOR : CLASS_REF
const staticClassPath = paths.staticClass
if (staticClassPath) { // append
staticClassPath.node.value.value = staticClassPath.node.value.value + ' ' + refClass
} else { // add staticClass
path.node.properties.unshift(
t.objectProperty(t.identifier('staticClass'), t.stringLiteral(refClass))
)
paths.staticClass = path.get('properties').find(
propertyPath => propertyPath.node.key.name === 'staticClass'
)
}
return [
t.objectProperty( // data-ref="" ,头条 vue-ref
t.stringLiteral(state.options.platform.ref),
refPath.node.value
)
]
}
return []
}
@@ -0,0 +1,203 @@
const t = require('@babel/types')
const {
IDENTIFIER_STYLE,
INTERNAL_GET_STYLE,
VIRTUAL_HOST_STYLE
} = require('../../../constants')
const {
getCode,
hyphenate,
isRootElement
} = require('../../../util')
const getMemberExpr = require('../member-expr')
const REGEX_PX = /(:|\s|\(|\/)[+-]?\d+(\.\d+)?u?px/g
const REGEX_UPX = /(:|\s|\(|\/)[+-]?\d+(\.\d+)?upx/g
function processStaticStyleUnit (styleStr, state) {
if (typeof styleStr === 'string') {
let matches = styleStr.match(REGEX_UPX)
if (matches && matches.length) {
matches.forEach(function (match) {
styleStr = styleStr.replace(match, match.substr(0, match.length - 3) + 'rpx')
})
}
// TODO 不应该再支持 px 转 rpx
if (state.options.transformPx) { // 需要转换 px
matches = styleStr.match(REGEX_PX)
if (matches && matches.length) {
matches.forEach(function (match) {
styleStr = styleStr.replace(match, match.substr(0, match.length - 2) + 'rpx')
})
}
}
}
return styleStr
}
function getStaticStyleStringLiteral (staticStylePath, state) {
const staticStyle = staticStylePath.node.value.properties
.map(property => {
return `${property.key.value}:${property.value.value}`
})
.join(';')
const staticStyleStr = processStaticStyleUnit(staticStyle, state).trim()
return t.stringLiteral(staticStyleStr + (!staticStyleStr.endsWith(';') ? ';' : ''))
}
function processStaticStyle (binaryExpressions, staticStylePath, state) {
let binaryExpression
binaryExpressions.forEach(binaryExpr => {
if (!binaryExpression) {
if (staticStylePath) {
binaryExpression = t.binaryExpression(
'+',
getStaticStyleStringLiteral(staticStylePath, state),
binaryExpr
)
staticStylePath.remove()
} else {
binaryExpression = binaryExpr
}
} else {
binaryExpression = t.binaryExpression(
'+',
binaryExpression,
binaryExpr
)
}
})
return binaryExpression
}
function processStyleObjectExpression (styleValuePath) {
const binaryExpressions = []
const propertyPaths = styleValuePath.get('properties')
propertyPaths.forEach(propertyPath => {
const key = propertyPath.node.key
binaryExpressions.push(
t.binaryExpression(
'+',
t.binaryExpression(
'+',
t.stringLiteral(hyphenate(key.name || key.value) + ':'),
t.parenthesizedExpression(propertyPath.node.value)
),
t.stringLiteral(';')
)
)
})
return binaryExpressions
}
function processStyleArrayExpression (elementPaths) {
let binaryExpressions = []
elementPaths.forEach(elementPath => {
binaryExpressions = binaryExpressions.concat(processStyleObjectExpression(elementPath))
})
return binaryExpressions
}
function generateGetStyle (stylePath, styleValuePath, staticStylePath, state) {
const args = [stylePath.node.value]
if (staticStylePath) {
args.push(staticStylePath.node.value)
staticStylePath.remove()
}
styleValuePath.replaceWith(
getMemberExpr(
stylePath,
IDENTIFIER_STYLE,
t.callExpression(t.identifier(INTERNAL_GET_STYLE), args),
state
)
)
}
module.exports = function processStyle (paths, path, state) {
const stylePath = paths.style
const staticStylePath = paths.staticStyle
const mergeVirtualHostAttributes = state.options.mergeVirtualHostAttributes
if (stylePath) {
const styleValuePath = stylePath.get('value')
if (styleValuePath.isObjectExpression()) {
// {} {...{}} {...{color}}
const hasDynamicContent = styleValuePath.node.properties.some(prop =>
!t.isObjectProperty(prop) && !t.isObjectExpression(prop.value)
)
const isEmptyObject = styleValuePath.node.properties.length === 0
if (hasDynamicContent || isEmptyObject) {
generateGetStyle(stylePath, styleValuePath, staticStylePath, state)
} else {
styleValuePath.replaceWith(
processStaticStyle(
processStyleObjectExpression(styleValuePath),
staticStylePath,
state
)
)
}
} else if (styleValuePath.isArrayExpression()) { // array
const elementPaths = styleValuePath.get('elements')
const dynamicStyle = elementPaths.find(elementPath => !elementPath.isObjectExpression())
if (dynamicStyle) {
generateGetStyle(stylePath, styleValuePath, staticStylePath, state)
} else {
styleValuePath.replaceWith(
processStaticStyle(
processStyleArrayExpression(elementPaths),
staticStylePath,
state
)
)
}
} else if (
styleValuePath.isStringLiteral() || // :style="'background:red'"
styleValuePath.isIdentifier() || // TODO 需要优化到下一个条件,:style="styleObject"
styleValuePath.isMemberExpression() || // TODO 需要优化到下一个条件,:style="item.styleObject"
styleValuePath.isConditionalExpression() ||
styleValuePath.isLogicalExpression() ||
styleValuePath.isBinaryExpression()
) {
// 理论上 ConditionalExpression,LogicalExpression 可能存在 styleObject,应该__get_style,还是先不考虑这种情况吧
// ConditionalExpression :style="index === currentIndex ? activeStyle : itemStyle"
// BinaryExpression :style="'m-content-head-'+message.user"
styleValuePath.replaceWith(
processStaticStyle(
[t.parenthesizedExpression(styleValuePath.node)],
staticStylePath,
state
)
)
} else if (
styleValuePath.isIdentifier() ||
styleValuePath.isMemberExpression()
) { // TODO 目前先不考虑 classObject,styleObject
// generateGetStyle(stylePath, styleValuePath, staticStylePath, state)
} else {
state.errors.add(`:style 不支持 ${getCode(styleValuePath.node)} 语法`)
}
if (mergeVirtualHostAttributes && isRootElement(path.parentPath)) {
styleValuePath.replaceWith(t.binaryExpression('+', styleValuePath.node, t.identifier(VIRTUAL_HOST_STYLE)))
}
} else if (staticStylePath) {
if (mergeVirtualHostAttributes && isRootElement(path.parentPath)) {
const styleNode = processStaticStyle([t.identifier(VIRTUAL_HOST_STYLE)], staticStylePath, state)
const property = t.objectProperty(t.identifier('style'), styleNode)
path.node.properties.push(property)
return []
}
staticStylePath.get('value').replaceWith(getStaticStyleStringLiteral(staticStylePath, state))
} else {
if (mergeVirtualHostAttributes && isRootElement(path.parentPath)) {
const property = t.objectProperty(t.identifier('style'), t.identifier(VIRTUAL_HOST_STYLE))
path.node.properties.push(property)
}
}
return []
}
@@ -0,0 +1 @@
// 需要转换 h5 标签至 staticClass 的样式名
@@ -0,0 +1,47 @@
const t = require('@babel/types')
const babelTraverse = require('@babel/traverse').default
const {
INTERNAL_SET_MODEL
} = require('../../../constants')
module.exports = {
getModelEventFunctionExpr (funcExpr, propPath, modifiers = []) {
let targetExpr
let keyExpr
babelTraverse(funcExpr, {
noScope: true,
CallExpression (path) {
if (path.node.callee.name === '$set') {
targetExpr = path.node.arguments[0]
keyExpr = path.node.arguments[1]
}
}
})
if (!targetExpr || !keyExpr) {
targetExpr = t.stringLiteral('')
keyExpr = t.stringLiteral(propPath)
}
return t.functionExpression(
null,
[t.identifier('$event')],
t.blockStatement(
[
t.returnStatement(
t.callExpression(
t.identifier(INTERNAL_SET_MODEL),
[
targetExpr,
keyExpr,
t.identifier('$event'),
t.arrayExpression(modifiers)
]
)
)
]
)
)
}
}
@@ -0,0 +1,179 @@
const t = require('@babel/types')
const {
VAR_FILTER
} = require('../../constants')
const GLOBAL_METHODS = [
'parseInt',
'parseFloat',
'isNaN',
'isFinite',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent'
]
const GLOBAL_OBJECTS = {
Math: [
'abs',
'acos',
'asin',
'atan',
'atan2',
'ceil',
'cos',
'exp',
'floor',
'log',
'max',
'min',
'pow',
'random',
'round',
'sin',
'sqrt',
'tan'
],
JSON: [
'stringify',
'parse'
]
}
const BUILT_IN_METHODS = [
// number
'toString',
'toLocaleString',
'valueOf',
'toFixed',
'toExponential',
'toPrecision',
// string
// 'toString',
// 'valueOf',
'charAt',
'charCodeAt',
'concat',
'indexOf',
'lastIndexOf',
'localeCompare',
'match',
'replace',
'search',
'slice',
'split',
'substring',
'toLowerCase',
'toLocaleLowerCase',
'toUpperCase',
'toLocaleUpperCase',
'trim',
// boolean
// 'toString',
// 'valueOf',
// object
// 'toString',
// function
// 'toString',
// array
// 'toString',
// 'concat',
'join',
'pop',
'push',
'reverse',
'shift',
// 'slice',
'sort',
'splice',
'unshift',
// 'indexOf',
// 'lastIndexOf',
'every',
'some',
'forEach',
'map',
'filter',
'reduce',
'reduceRight'
]
function getGlobalMethodFilter (callExpr) {
const callee = callExpr.callee
if (callee) {
const name = callee.name
if (name && GLOBAL_METHODS.includes(name)) {
return t.callExpression(
t.memberExpression(
t.identifier(VAR_FILTER),
t.identifier(name)
),
callExpr.arguments
)
}
}
return false
}
function getGlobalObjectFilter (callExpr) {
const callee = callExpr.callee
if (t.isMemberExpression(callee)) {
const object = callee.object
const property = callee.property
const propertyName = property.name || property.value
const methods = GLOBAL_OBJECTS[object.name]
if (methods && methods.includes(propertyName)) {
return t.callExpression(
t.memberExpression(
t.identifier(VAR_FILTER),
t.identifier(propertyName)
),
callExpr.arguments
)
}
}
return false
}
function getMemberFilter (callExpr) {
const callee = callExpr.callee
if (t.isMemberExpression(callee)) {
const property = callee.property
const propertyName = property.name || property.value
if (BUILT_IN_METHODS.includes(propertyName)) {
return t.callExpression(
t.memberExpression(
t.identifier(VAR_FILTER),
t.identifier(propertyName)
),
[
callee.object,
...callExpr.arguments
]
)
}
}
}
function processFilter (callExpr, path) {
const globalMethodFilter = getGlobalMethodFilter(callExpr)
if (globalMethodFilter) {
path.replaceWith(globalMethodFilter)
return true
}
const globalObjectFilter = getGlobalObjectFilter(callExpr)
if (globalObjectFilter) {
path.replaceWith(globalObjectFilter)
return true
}
const memberFilter = getMemberFilter(callExpr)
if (memberFilter) {
path.replaceWith(memberFilter)
return true
}
return false
}
module.exports = processFilter
@@ -0,0 +1,139 @@
const t = require('@babel/types')
const babelTraverse = require('@babel/traverse').default
const {
VAR_ROOT,
IDENTIFIER_FOR,
IDENTIFIER_ATTR,
IDENTIFIER_METHOD,
IDENTIFIER_FILTER,
IDENTIFIER_CLASS,
IDENTIFIER_STYLE,
IDENTIFIER_EVENT,
IDENTIFIER_GLOBAL,
IDENTIFIER_TEXT,
PREFIX_ATTR,
PREFIX_GLOBAL,
PREFIX_METHOD,
PREFIX_FILTER,
PREFIX_FOR,
PREFIX_CLASS,
PREFIX_STYLE,
PREFIX_EVENT,
PREFIX_TEXT
} = require('../../constants')
const {
getInItIfStatement,
getDataExpressionStatement,
getRenderSlotStatement
} = require('./statements')
const visitor = require('./visitor')
function reIdentifier (identifierArray) {
const identifierOpts = {
[IDENTIFIER_FOR]: {
prefix: PREFIX_FOR,
id: 0
},
[IDENTIFIER_METHOD]: {
prefix: PREFIX_METHOD,
id: 0
},
[IDENTIFIER_FILTER]: {
prefix: PREFIX_FILTER,
id: 0
},
[IDENTIFIER_CLASS]: {
prefix: PREFIX_CLASS,
id: 0
},
[IDENTIFIER_STYLE]: {
prefix: PREFIX_STYLE,
id: 0
},
[IDENTIFIER_EVENT]: {
prefix: PREFIX_EVENT,
id: 0
},
[IDENTIFIER_GLOBAL]: {
prefix: PREFIX_GLOBAL,
id: 0
},
[IDENTIFIER_ATTR]: {
prefix: PREFIX_ATTR,
id: 0
},
[IDENTIFIER_TEXT]: {
prefix: PREFIX_TEXT,
id: 0
}
}
// TODO order
identifierArray.forEach(identifier => {
if (Array.isArray(identifier)) {
let opts = false
identifier.forEach(stringLiteral => {
const key = t.isStringLiteral(stringLiteral) ? 'value' : 'name'
if (opts === false) {
opts = identifierOpts[stringLiteral[key]]
stringLiteral[key] = `${opts.prefix + opts.id++}`
} else {
stringLiteral[key] = `${opts.prefix + (opts.id - 1)}`
}
})
} else {
const key = t.isStringLiteral(identifier) ? 'value' : 'name'
const opts = identifierOpts[identifier[key]]
identifier[key] = `${opts.prefix + opts.id++}`
}
})
}
module.exports = function traverse (ast, state) {
const identifierArray = []
const blockStatementBody = []
const objectPropertyArray = []
const initExpressionStatementArray = []
const renderSlotStatementArray = []
const resolveSlotStatementArray = []
// TODO 待重构,至少 filtermethod 等实现方式要调整
babelTraverse(ast, visitor, undefined, {
scoped: [],
context: VAR_ROOT,
options: state.options,
errors: state.errors,
tips: state.tips,
identifierArray: identifierArray,
propertyArray: objectPropertyArray,
declarationArray: blockStatementBody,
initExpressionStatementArray: initExpressionStatementArray,
renderSlotStatementArray,
resolveSlotStatementArray
})
if (initExpressionStatementArray.length) {
blockStatementBody.push(getInItIfStatement(initExpressionStatementArray))
}
if (objectPropertyArray.length) {
blockStatementBody.push(getDataExpressionStatement(objectPropertyArray))
}
if (renderSlotStatementArray.length) {
blockStatementBody.push(getRenderSlotStatement(state, renderSlotStatementArray))
}
if (resolveSlotStatementArray.length) {
blockStatementBody.push(...resolveSlotStatementArray)
}
reIdentifier(identifierArray)
return t.withStatement(
t.thisExpression(),
t.blockStatement(blockStatementBody)
)
}
@@ -0,0 +1,134 @@
const t = require('@babel/types')
const traverse = require('@babel/traverse').default
const {
VAR_ROOT,
IDENTIFIER_METHOD,
IDENTIFIER_FILTER,
IDENTIFIER_GLOBAL,
METHOD_RENDER_LIST
} = require('../../constants')
function isMatch (name, forItem, forIndex) {
return name === forItem || name === forIndex
}
function findScoped (path, test, state) {
if (!path) {
return state
}
const scoped = state.scoped.find(scoped => {
const {
forItem,
forIndex,
path: listPath
} = scoped
const funPath = path.findParent(path =>
path.isFunctionExpression() &&
t.isCallExpression(path.parentPath) &&
path.parentPath.node.callee.name === METHOD_RENDER_LIST
)
if (funPath && funPath.parentPath === listPath) {
// TODO 为兼容历史结构仅在当前 list 父级存在 v-if 返回
const parent = listPath.findParent(path => path.isFunctionExpression() || path.isConditionalExpression())
if (parent && parent.isConditionalExpression()) {
return true
}
} else {
return false
}
let match = false
path.traverse({
noScope: true,
Identifier (path) {
if (!match && path.key !== 'key' && (path.key !== 'property' || path.parent.computed)) {
match = isMatch(path.node.name, forItem, forIndex)
if (match) {
path.stop()
}
}
}
})
if (!match && test) {
traverse(t.arrayExpression([test]), {
noScope: true,
Identifier (path) {
if (!match && path.key !== 'key' && (path.key !== 'property' || path.parent.computed)) {
const node = path.node
match = isMatch(node.name, forItem, forIndex) || scoped.declarationArray.find(({ declarations }) => declarations.find(({ id }) => id === node))
if (match) {
path.stop()
}
}
}
})
}
return match
})
if (!scoped && state.scoped.length > 1) {
return state.scoped[1] // 取父
}
return scoped || state
}
function findTest (path, state) {
let tests
if (path) {
while (path.parentPath && path.key !== 'body') {
if (path.key === 'consequent' || path.key === 'alternate') {
const testOrig = path.container.test
let test = t.arrayExpression([t.cloneDeep(testOrig)])
traverse(test, {
noScope: true,
MemberExpression (memberExpressionPath) {
const names = state.scoped.map(scoped => scoped.forItem)
const node = memberExpressionPath.node
const objectName = node.object.name
const property = node.property
const propertyName = property.name
if (objectName === VAR_ROOT || (names.includes(objectName) && (propertyName === IDENTIFIER_METHOD || propertyName === IDENTIFIER_FILTER || propertyName === IDENTIFIER_GLOBAL))) {
const array = []
let tempPath = memberExpressionPath
while (tempPath.parentPath) {
const key = tempPath.key
array.unshift(typeof key === 'number' ? `[${key}]` : `.${key}`)
tempPath = tempPath.parentPath
}
memberExpressionPath.replaceWith(path.parentPath.get('test' + array.join('')).node.property)
}
}
})
test = test.elements[0]
if (path.key === 'alternate') {
test = t.unaryExpression('!', test)
}
tests = tests ? t.logicalExpression('&&', test, tests) : test
}
path = path.parentPath
}
}
return tests
}
module.exports = function getMemberExpr (path, name, init, state, variableDeclaration = true) {
const test = findTest(path, state)
const scoped = findScoped(path, test, state)
if (!variableDeclaration) {
scoped.declarationArray.push(t.expressionStatement(init))
return
}
const identifier = t.identifier(name)
scoped.propertyArray.push(t.objectProperty(identifier, identifier))
scoped.declarationArray.push(
t.variableDeclaration('var', [t.variableDeclarator(identifier, test ? t.conditionalExpression(test, init, t.nullLiteral()) : init)])
)
state.identifierArray.push(identifier)
const contextIdentifier = t.identifier(scoped.context)
contextIdentifier.$mpProcessed = true
return t.memberExpression(contextIdentifier, identifier)
}
@@ -0,0 +1,235 @@
const t = require('@babel/types')
const {
VAR_ORIGINAL,
VAR_INDEX,
IDENTIFIER_FOR,
METHOD_RENDER_LIST
} = require('../../constants')
const {
getMapCallExpression
} = require('./statements')
const {
hasOwn,
genCode,
traverseKey,
processMemberExpression,
getForIndexIdentifier,
isSimpleObjectExpression,
traverseFilter
} = require('../../util')
const getMemberExpr = require('./member-expr')
const origVisitor = {
noScope: true,
Identifier (path) {
if (
!path.node.$mpProcessed &&
path.node.name === this.forItem &&
path.isReferencedIdentifier()
) {
const forItemIdentifier = t.identifier(this.forItem)
forItemIdentifier.$mpProcessed = true
path.replaceWith(
t.memberExpression(forItemIdentifier, t.identifier(VAR_ORIGINAL))
)
}
},
FunctionExpression (path) {
const callee = path.parentPath.node.callee
if (t.isIdentifier(callee) && callee.name === METHOD_RENDER_LIST) {
path.traverse(origVisitor, {
forItem: this.forItem
})
path.skip()
}
}
}
function isRefrence (forItem, code) {
if (forItem === code) {
return true
}
return code.indexOf(forItem + '.') === 0
}
function replaceRefrence (forItem, code) {
if (forItem === code) {
return ''
}
return code.replace(forItem + '.', '')
}
function getForExtra (forItem, forIndex, path, state) {
const arg0 = path.node.arguments[0]
const isNumeric = t.isNumericLiteral(arg0)
const isString = t.isStringLiteral(arg0)
let forCode = genCode(processMemberExpression(arg0, state), true)
const forKey = traverseKey(path.node)
const origForKeyCode = t.isIdentifier(forKey) && forKey.name
let forKeyCode = ''
if (forKey) {
forKeyCode = genCode(processMemberExpression(forKey, state), true)
if (isRefrence(forItem, forKeyCode)) {
forKeyCode = replaceRefrence(forItem, forKeyCode)
}
}
const forExtraElements = []
if (state.scoped.length) {
const scoped = state.scoped.find(scoped => isRefrence(scoped.forItem, forCode))
if (scoped) {
forCode = replaceRefrence(scoped.forItem, forCode)
forExtraElements.push(...scoped.forExtra)
}
}
let forCodeElem = t.stringLiteral(forCode)
if (isNumeric) {
forCodeElem = t.numericLiteral(arg0.value)
} else if (isString) {
forCodeElem = t.stringLiteral('#s#' + forCode)
}
if (forItem === origForKeyCode) { // 以自身为 key,则依据 forIndex 查找 ['list','',__i0__],['list','',index]
forExtraElements.push(
t.arrayExpression(
[
forCodeElem,
t.stringLiteral(''),
t.identifier(forIndex)
]
)
)
} else {
forExtraElements.push(
t.arrayExpression(
[
forCodeElem,
t.stringLiteral(forIndex === forKeyCode ? '' : forKeyCode),
forKey || t.identifier(forIndex)
]
)
)
}
return forExtraElements
}
module.exports = function traverseRenderList (path, state) {
const functionExpression = path.get('arguments.1')
const params = functionExpression.node.params
const forItem = params[0].name
let forIndex = params.length > 1 && params[1].name
let forKey = params.length > 2 && params[2].name
if (forKey) {
[forKey, forIndex] = [forIndex, forKey]
}
if (!forIndex) {
if (!hasOwn(state.options, '$forIndexId')) {
state.options.$forIndexId = 0
}
forIndex = getForIndexIdentifier(state.options.$forIndexId++)
params.push(t.identifier(forIndex))
}
const forStateScoped = {
context: forItem,
forItem,
forKey,
forIndex,
forExtra: getForExtra(forItem, forIndex, path, state),
propertyArray: [],
declarationArray: [],
renderSlotStatementArray: [],
path
}
const forState = {
inFor: true,
context: state.context,
options: state.options,
errors: state.errors,
tips: state.tips,
scoped: [forStateScoped].concat(state.scoped),
identifierArray: state.identifierArray,
propertyArray: [],
declarationArray: [],
computedProperty: {},
initExpressionStatementArray: state.initExpressionStatementArray,
renderSlotStatementArray: state.renderSlotStatementArray,
resolveSlotStatementArray: state.resolveSlotStatementArray
}
functionExpression.traverse(require('./visitor'), forState)
const forPath = path.get('arguments.0')
if (forStateScoped.propertyArray.length || forStateScoped.renderSlotStatementArray.length || forKey) {
// for => map
forPath.replaceWith(
getMemberExpr(
forPath,
IDENTIFIER_FOR,
getMapCallExpression(
forPath.node,
forStateScoped.propertyArray,
forStateScoped.declarationArray,
forStateScoped.renderSlotStatementArray,
[], // eventPropertyArray
forItem,
forKey,
forIndex,
state
),
forState
)
)
functionExpression.traverse(origVisitor, {
forItem
})
if (forKey) {
functionExpression.traverse({
Identifier (path) {
if (
!path.node.$mpProcessed &&
path.node.name === this.forIndex &&
path.isReferencedIdentifier()
) {
const forItemIdentifier = t.identifier(VAR_INDEX)
forItemIdentifier.$mpProcessed = true
path.replaceWith(
t.memberExpression(t.identifier(this.forItem), forItemIdentifier)
)
}
}
}, {
forItem,
forIndex
})
}
const keys = Object.keys(forState.computedProperty)
if (keys.length) {
keys.forEach(key => {
const property = forState.computedProperty[key]
if (t.isMemberExpression(property) && property.object.name === forItem) {
property.object = t.memberExpression(t.identifier(forItem), t.identifier(VAR_ORIGINAL))
forState.options.replaceCodes[key] = `'+${genCode(property, true)}+'`
}
})
}
} else if ((forPath.isCallExpression() && !traverseFilter(forPath.node.callee, state)) || (forPath.isObjectExpression() && !isSimpleObjectExpression(forPath.node))) {
forPath.replaceWith(getMemberExpr(forPath, IDENTIFIER_FOR, forPath.node, forState))
} else {
forPath.traverse(require('./visitor'), forState)
}
forState.propertyArray.forEach(property => {
state.propertyArray.push(property)
})
forState.declarationArray.forEach(declaration => {
state.declarationArray.push(declaration)
})
}
@@ -0,0 +1,54 @@
const t = require('@babel/types')
const initStatement = t.expressionStatement(t.callExpression(t.identifier('$initSSP'), []))
const resolveStatement = t.expressionStatement(t.callExpression(t.identifier('$callSSP'), []))
module.exports = function getRenderSlot (path, state) {
const name = path.get('arguments.0')
const arg2 = path.get('arguments.2')
const arg3 = path.get('arguments.3')
let valueNode
if (arg3) {
// v-bind:object
valueNode = arg3.node
} else if (arg2 && !arg2.isNullLiteral()) {
if (arg2.isObjectExpression()) {
const propertiesPath = arg2.get('properties')
const oldProperties = []
const newProperties = []
propertiesPath.forEach(path => {
const properties = path.get('key').isStringLiteral({ value: 'SLOT_DEFAULT' }) ? oldProperties : newProperties
properties.push(state.options.scopedSlotsCompiler === 'auto' ? path.node : t.cloneNode(path.node, true))
})
if (!newProperties.length) {
return
}
valueNode = t.objectExpression(newProperties)
if (state.options.scopedSlotsCompiler !== 'auto') {
arg2.replaceWith(t.objectExpression(oldProperties))
}
} else {
valueNode = arg2.node
}
}
if (valueNode) {
if (!state.declarationArray.includes(initStatement)) {
state.declarationArray.push(initStatement)
}
const indexNode = t.callExpression(t.identifier('$setSSP'), [name.node, valueNode])
const slotMultipleInstance = state.options.scopedSlotsCompiler === 'augmented' && state.options.slotMultipleInstance
if (slotMultipleInstance) {
// 插槽名拼接 '.'+index
name.replaceWith(t.binaryExpression('+', name.node, t.binaryExpression('+', t.stringLiteral('.'), indexNode)))
} else {
const scoped = state.scoped
// TODO 判断是否包含作用域内变量
const renderSlotStatementArray = scoped && scoped.length ? scoped[scoped.length - 1].renderSlotStatementArray : state.renderSlotStatementArray
renderSlotStatementArray.push(t.expressionStatement(indexNode))
}
if (!state.resolveSlotStatementArray.includes(resolveStatement)) {
state.resolveSlotStatementArray.push(resolveStatement)
}
}
// TODO 组件嵌套
}
@@ -0,0 +1,159 @@
const t = require('@babel/types')
const template = require('@babel/template').default
const {
METHOD_BUILT_IN,
METHOD_CREATE_EMPTY_VNODE,
METHOD_CREATE_ELEMENT,
METHOD_RENDER_LIST
} = require('../../constants')
function findBinding (fnPath, idPath) {
const name = idPath.node.name
return fnPath.scope.bindings[name].referencePaths.find(refPath => refPath === idPath)
}
function needAugmentedSlotMode (path, ids, state) {
const platformName = state.options.platform.name
const fnPath = path.parentPath
let need
path.traverse({
noScope: false,
Property (path) {
// 跳过事件
if (path.node.key.name === 'on') {
const parentPath = path.parentPath.parentPath
if (t.isCallExpression(parentPath) && parentPath.node.callee.name === METHOD_CREATE_ELEMENT) {
path.skip()
}
}
},
Identifier (path) {
const name = path.node.name
if (path.key !== 'key' && (path.key !== 'property' || path.parent.computed)) {
// 使用作用域内方法或作用域外数据
if (name in ids) {
if (path.key === 'callee') {
need = true
}
} else if (!path.scope.hasBinding(name) && !METHOD_BUILT_IN.includes(name)) {
// 原生支持作用域插槽的平台允许使用作用域外数据,暂时只考虑作用内的数据作为方法参数的情况
if (['mp-baidu', 'mp-alipay'].includes(platformName)) {
if (path.key === 'callee') {
path.parentPath.traverse({
noScope: false,
Identifier (path) {
const name = path.node.name
if (name in ids && findBinding(fnPath, path)) {
need = true
path.stop()
}
}
})
}
} else {
need = true
}
}
} else if (platformName === 'mp-weixin' && path.key === 'property' && name === 'length') {
// 微信小程序平台无法观测 Array length 访问:https://developers.weixin.qq.com/community/develop/doc/000c8ee47d87a0d5b6685a8cb57000
need = true
}
if (need) {
path.stop()
}
}
})
return need
}
function replaceId (path, ids) {
let replaced
const fnPath = path.parentPath
path.traverse({
noScope: false,
Identifier (path) {
const name = path.node.name
if (name in ids && findBinding(fnPath, path)) {
path.replaceWith(t.cloneNode(ids[name], true))
replaced = true
}
}
})
return replaced
}
module.exports = function getResolveScopedSlots (parent, state) {
const elements0 = parent.get('arguments.0.elements.0')
let objectPath = elements0
// TODO v-else
if (objectPath.isConditionalExpression()) {
objectPath = objectPath.get('consequent')
}
if (objectPath.isCallExpression()) {
objectPath = objectPath.get('arguments.1.body.body.0.argument')
}
const properties = objectPath.get('properties')
const fn = properties.find(path => path.get('key').isIdentifier({ name: 'fn' }))
const params = fn.get('value.params.0')
if (!params) {
return
}
const vueId = parent.parentPath.parentPath.get('properties').find(path => path.get('key').isIdentifier({ name: 'attrs' })).get('value').get('properties').find(path => path.get('key').isStringLiteral({ value: 'vue-id' })).get('value').node
// TODO 多层 v-for 嵌套时,后续处理作用域可能发生变化,需安全重命名
const slotPath = properties.find(path => path.get('key').isIdentifier({ name: 'key' })).get('value')
const slotNode = slotPath.node
const slotMultipleInstance = state.options.scopedSlotsCompiler === 'augmented' && state.options.slotMultipleInstance
const scopedSlotsParams = {
item: elements0.scope.generateUidIdentifier('item'),
index: elements0.scope.generateUidIdentifier('index')
}
const ids = {}
function updateIds (vueId, slot, value, key) {
let node = slotMultipleInstance ? scopedSlotsParams.item : t.callExpression(t.identifier('$getSSP'), [vueId, slot])
if (key) {
node = t.memberExpression(node, t.stringLiteral(key), true)
}
ids[value] = node
}
if (params.isObjectPattern()) {
params.get('properties').forEach(prop => {
updateIds(vueId, slotNode, prop.get('value').node.name, prop.get('key').node.name)
})
} else if (params.isIdentifier()) {
updateIds(vueId, slotNode, params.node.name)
}
const fnBody = fn.get('value.body')
// 非原生支持作用域插槽的平台在含有动态 slotName 的情况下,scopedSlotsCompiler 指定使用增强编译模式
const isStaticSlotName = t.isStringLiteral(slotNode)
if (state.options.scopedSlotsCompiler === 'augmented' || needAugmentedSlotMode(fnBody, ids, state) || (!['mp-baidu', 'mp-alipay'].includes(state.options.platform.name) && !isStaticSlotName)) {
if (replaceId(fnBody, ids)) {
const test = t.callExpression(t.identifier('$hasSSP'), [vueId])
// scopedSlotsCompiler auto
objectPath.node.scopedSlotsCompiler = 'augmented'
if (slotMultipleInstance) {
// elements0 节点替换增加一层循环
let node = elements0.node
const builder = template(`${METHOD_RENDER_LIST}($getSSP(%%vueId%%, %%slot%%, true), function (%%item%%, %%index%%) {return %%node%%})`)
node = builder({
vueId,
slot: slotNode,
node,
item: scopedSlotsParams.item,
index: scopedSlotsParams.index
}).expression
node = t.conditionalExpression(test, node, t.callExpression(t.identifier(METHOD_CREATE_EMPTY_VNODE), []))
elements0.replaceWith(node)
// 插槽名拼接 '.'+index
// 百度、字节小程序不支持 v-for 嵌套 slot,且支持渲染多个实例,固定输出到第一个
const indexNode = ['mp-baidu', 'mp-toutiao'].includes(state.options.platform.name) ? t.numericLiteral(0) : scopedSlotsParams.index
slotPath.replaceWith(t.binaryExpression('+', slotNode, t.binaryExpression('+', t.stringLiteral('.'), indexNode)))
} else {
const orgin = fnBody.get('body.0.argument')
const elements = orgin.get('elements')
const node = (elements.length === 1 ? elements[0] : orgin).node
orgin.replaceWith(t.arrayExpression([t.conditionalExpression(test, node, t.callExpression(t.identifier(METHOD_CREATE_EMPTY_VNODE), []))]))
}
}
}
}
@@ -0,0 +1,188 @@
const t = require('@babel/types')
const {
VAR_MP,
VAR_ROOT,
VAR_ORIGINAL,
VAR_INDEX,
INTERNAL_GET_ORIG,
IDENTIFIER_METHOD,
IDENTIFIER_FILTER
} = require('../../constants')
/**
* e0=e=>count++
*/
function getEventExpressionStatement (left, right) {
return t.expressionStatement(
t.assignmentExpression(
'=',
left,
right
)
)
}
/**
* if(!_isMounted){}
*/
function getInItIfStatement (expressionStatementArray) {
return t.ifStatement(
t.unaryExpression(
'!',
t.identifier('_isMounted')
),
t.blockStatement(expressionStatementArray)
)
}
function getRenderSlotStatement (state, renderSlotStatementArray, forItem) {
function cloneNode (node) {
if (Array.isArray(node)) {
return node.map(function (item) {
return cloneNode(item)
})
} else if (typeof node === 'object') {
if (!node) {
return node
}
if (t.isMemberExpression(node)) { // 纠正被处理过的对象
const name = node.object.name
// identifier 使用原值以被后续修改
if ((name === VAR_ROOT || name === forItem) && t.isIdentifier(node.property) && [IDENTIFIER_METHOD, IDENTIFIER_FILTER].includes(node.property.name)) {
return node.property
}
} else if (t.isIdentifier(node, { name: forItem })) { // 预处理 forItem
return t.identifier(VAR_ORIGINAL)
}
const target = Object.create(node)
Object.keys(node).forEach(function (key) {
target[key] = cloneNode(node[key])
})
return target
} else {
return node
}
}
renderSlotStatementArray.forEach(renderSlotStatement => {
const argument = renderSlotStatement.expression.arguments[1]
if (t.isObjectExpression(argument)) {
// 克隆以避免影响模板
argument.properties = cloneNode(argument.properties)
}
})
const blockStatement = t.blockStatement(renderSlotStatementArray)
if (state.options.scopedSlotsCompiler === 'auto') {
return t.ifStatement(
t.binaryExpression('===',
t.memberExpression(t.memberExpression(t.identifier('$scope'), t.identifier(state.options.platform.name === 'mp-alipay' ? 'props' : 'data')), t.identifier('scopedSlotsCompiler')), t.stringLiteral('augmented')
),
blockStatement
)
}
return blockStatement
}
/**
* items.map(function(item,index){return {}})
*/
function getMapCallExpression (
object,
objectPropertyArray,
declarationArray,
renderSlotStatementArray,
eventPropertyArray,
forItem,
forKey,
forIndex,
state
) {
const blockStatement = []
// var $orgi = __get_orig(forItem)
blockStatement.push(t.variableDeclaration('var', [
t.variableDeclarator(t.identifier(VAR_ORIGINAL), t.callExpression(t.identifier(INTERNAL_GET_ORIG), [
t.identifier(forItem)
]))
]))
if (declarationArray.length) {
declarationArray.forEach(declaration => {
blockStatement.push(declaration)
})
}
if (renderSlotStatementArray.length) {
blockStatement.push(getRenderSlotStatement(state, renderSlotStatementArray, forItem))
}
blockStatement.push(t.returnStatement(
// return {$orgi:$orgi}
t.objectExpression(
[
t.objectProperty(
t.identifier(VAR_ORIGINAL),
t.identifier(VAR_ORIGINAL)
)
].concat(objectPropertyArray)
.concat(forKey && forIndex ? [t.objectProperty(
t.identifier(VAR_INDEX),
t.identifier(forIndex)
)] : [])
)
))
const params = [t.identifier(forItem)]
if (forKey) {
params.push(t.identifier(forKey))
}
if (forIndex) {
params.push(t.identifier(forIndex))
}
return t.callExpression(t.identifier('__map'), [
object,
t.functionExpression(
null,
params,
t.blockStatement(blockStatement)
)
])
}
/**
* $mp.data = Object.assign({},{$root:{}})
*/
function getDataExpressionStatement (objectPropertyArray) {
return t.expressionStatement(
t.assignmentExpression(
'=',
t.memberExpression(
// left
t.identifier(VAR_MP),
t.identifier('data')
),
t.callExpression(
// right
t.memberExpression(
// Object.assign
t.identifier('Object'),
t.identifier('assign')
),
[
t.objectExpression([]), // {}
t.objectExpression([
// {$root:{}}
t.objectProperty(
t.identifier(VAR_ROOT),
t.objectExpression(objectPropertyArray)
)
])
]
)
)
)
}
module.exports = {
getInItIfStatement,
getMapCallExpression,
getDataExpressionStatement,
getEventExpressionStatement,
getRenderSlotStatement
}
@@ -0,0 +1,325 @@
const t = require('@babel/types')
const uniI18n = require('@dcloudio/uni-cli-i18n')
const {
METHOD_CREATE_ELEMENT,
METHOD_TO_STRING,
METHOD_RENDER_LIST,
METHOD_BUILT_IN,
METHOD_RESOLVE_FILTER,
METHOD_RENDER_SLOT,
METHOD_RESOLVE_SCOPED_SLOTS,
IDENTIFIER_FILTER,
IDENTIFIER_METHOD,
IDENTIFIER_GLOBAL,
IDENTIFIER_TEXT
} = require('../../constants')
const {
getTagName
} = require('../../h5')
const {
hasOwn,
hyphenate,
traverseFilter,
getComponentName,
hasEscapeQuote,
hasLengthProperty,
isRootElement
} = require('../../util')
const traverseData = require('./data')
const traverseRenderList = require('./render-list')
const getMemberExpr = require('./member-expr')
const getRenderSlot = require('./render-slot')
const getResolveScopedSlots = require('./resolve-scoped-slots')
function addStaticClass (path, staticClass) {
const dataPath = path.get('arguments.1')
if (dataPath && dataPath.isObjectExpression()) {
const staticClassProperty = dataPath.node.properties.find(property => property.key.name === 'staticClass')
if (staticClassProperty) { // update
staticClassProperty.value.value = staticClassProperty.value.value + ' ' + staticClass
} else { // add
dataPath.node.properties.push(
t.objectProperty(t.identifier('staticClass'), t.stringLiteral(staticClass))
)
}
} else { // {staticClass:'data-v-aaa'}
const args = path.node.arguments
args.splice(1, 0, t.objectExpression(
[
t.objectProperty(t.identifier('staticClass'), t.stringLiteral(staticClass))
]
))
}
}
function addVueId (path, state) {
// const platformName = state.options.platform.name
// if ( // 暂不对 mp-weixin,app-plus 增加 vueId
// platformName === 'mp-weixin' ||
// platformName === 'app-plus'
// ) {
// return
// }
if (!hasOwn(state.options, '$vueId')) {
state.options.$vueId = 1
}
const hashId = state.options.hashId
const vueId = (hashId ? (hashId + '-') : '') + (state.options.$vueId++)
let value
if (state.scoped.length) {
const scopeds = state.scoped
const len = scopeds.length
if (len > 1) { // v-for 嵌套,forIndex 不允许重复
const forIndexSet = new Set()
for (let i = 0; i < len; i++) {
const scoped = scopeds[i]
forIndexSet.add(scoped.forIndex)
if (forIndexSet.size !== i + 1) {
state.errors.add(uniI18n.__('templateCompiler.forNestedIndexNameNoArrowRepeat', { 0: 'v-for', 1: scoped.forIndex }))
break
}
}
}
for (let i = len - 1; i >= 0; i--) {
const scoped = scopeds[i]
if (!value) {
value = t.binaryExpression('+', t.stringLiteral(vueId + '-'), t.identifier(scoped.forIndex))
} else {
value = t.binaryExpression('+',
t.binaryExpression('+', value, t.stringLiteral('-')),
t.identifier(scoped.forIndex)
)
}
}
} else {
value = t.stringLiteral(vueId)
}
const objectProperty = t.objectProperty(
t.stringLiteral('vue-id'),
value
)
const dataPath = path.get('arguments.1')
if (dataPath && dataPath.isObjectExpression()) {
const attrsProperty = dataPath.node.properties.find(property => property.key.name === 'attrs')
if (attrsProperty) {
attrsProperty.value.properties.unshift(objectProperty)
} else {
dataPath.node.properties.push(
t.objectProperty(t.identifier('attrs'), t.objectExpression([
objectProperty
]))
)
}
} else { // {attrs:{'vue-id':'2'}}
const args = path.node.arguments
args.splice(1, 0, t.objectExpression(
[
t.objectProperty(t.identifier('attrs'), t.objectExpression([
objectProperty
]))
]
))
}
}
function checkUsingGlobalComponents (name, globalUsingComponents, state) {
if (globalUsingComponents && globalUsingComponents[name]) {
if (!state.options.usingGlobalComponents) {
state.options.usingGlobalComponents = Object.create(null)
}
state.options.usingGlobalComponents[name] = globalUsingComponents[name]
}
}
module.exports = {
noScope: false,
MemberExpression (path) {
if ( // t.m(123)
t.isIdentifier(path.node.object) &&
this.options.filterModules.includes(path.node.object.name)
) {
path.skip()
}
// 微信小程序平台无法观测 Array length 访问:https://developers.weixin.qq.com/community/develop/doc/000c8ee47d87a0d5b6685a8cb57000
if (this.options.platform.name === 'mp-weixin' && hasLengthProperty(path)) {
let newPath = path
while (newPath) {
path = newPath
newPath = path.findParent((path) => path.isLogicalExpression())
}
path.skip()
if (path.findParent((path) => path.shouldSkip || (this.options.scopedSlotsCompiler === 'legacy' && path.isCallExpression() && path.node.callee.name === METHOD_RESOLVE_SCOPED_SLOTS))) {
return
}
path.replaceWith(getMemberExpr(path, IDENTIFIER_GLOBAL, path.node, this))
}
},
CallExpression (path) {
const callee = path.node.callee
if (traverseFilter(callee, this)) {
return path.skip()
} else if (t.isIdentifier(callee)) {
const methodName = callee.name
switch (methodName) {
case METHOD_CREATE_ELEMENT:
{
const tagNode = path.node.arguments[0]
if (t.isStringLiteral(tagNode)) {
// 需要把标签增加到 class 样式中
const tagName = getTagName(tagNode.value, this.options.platform.name)
if (tagName !== tagNode.value) {
addStaticClass(path, '_' + tagNode.value)
}
tagNode.value = getComponentName(hyphenate(tagName))
// 组件增加 vueId
// 跳过支付宝插件组件
if (this.options.platform.isComponent(tagNode.value) && !tagNode.$mpPlugin) {
addVueId(path, this)
}
// 查找全局组件
checkUsingGlobalComponents(
tagNode.value,
this.options.globalUsingComponents,
this
)
}
if (this.options.scopeId) {
addStaticClass(path, this.options.scopeId)
}
// 根节点无 attrs 时添加空对象,方便后续合并外层 attrs
if (this.options.mergeVirtualHostAttributes && !t.isObjectExpression(path.node.arguments[1]) && isRootElement(path)) {
path.node.arguments.splice(1, 0, t.objectExpression([]))
}
const dataPath = path.get('arguments.1')
dataPath && dataPath.isObjectExpression() && traverseData(dataPath, this, tagNode.value)
}
break
case METHOD_TO_STRING:
{
const stringPath = path.get('arguments.0')
if (hasEscapeQuote(stringPath)) {
// 属性中包含转义引号时部分小程序平台报错或显示异常
// TODO 简单情况翻转外层引号
stringPath.replaceWith(getMemberExpr(path, IDENTIFIER_TEXT, stringPath.node, this))
}
const stringNodes = stringPath.node
stringNodes.$toString = true
path.replaceWith(stringNodes)
}
break
case METHOD_RENDER_LIST:
traverseRenderList(path, this)
path.skip()
break
default:
// TODO 检测是否是 filterModules
if (!METHOD_BUILT_IN.includes(methodName)) {
if (
path.findParent(
path =>
path.isObjectProperty() && ['on', 'nativeOn'].includes(path.node.key.name)
)
// path is model.callback
// || path.findParent(path => path.isObjectProperty() && path.node.key.name === 'callback' && t.isFunctionExpression(path.node.value) && t.isObjectProperty(path.parentPath.parentPath) && path.parentPath.parentPath.node.key.name === 'model')
) {
// event
return path.skip()
}
let newPath = path
while (newPath) {
path = newPath
newPath = path.findParent((path) => path.isLogicalExpression())
}
path.skip()
if (path.findParent((path) => path.shouldSkip)) {
return
}
path.replaceWith(
getMemberExpr(
path,
methodName === METHOD_RESOLVE_FILTER
? IDENTIFIER_FILTER
: IDENTIFIER_METHOD,
path.node,
this
)
)
} else if (this.options.scopedSlotsCompiler === 'auto' || this.options.scopedSlotsCompiler === 'augmented') {
if (methodName === METHOD_RESOLVE_SCOPED_SLOTS) {
getResolveScopedSlots(path, this)
} else if (methodName === METHOD_RENDER_SLOT) {
getRenderSlot(path, this)
}
}
break
}
} else if (
t.isCallExpression(callee) &&
t.isIdentifier(callee.callee) &&
callee.callee.name === METHOD_RESOLVE_FILTER
) {
// multi filter
path.replaceWith(getMemberExpr(path, IDENTIFIER_FILTER, path.node, this))
} else if (
t.isMemberExpression(callee) // message.split('').reverse().join('')
) {
// Object.assign...
let newPath = path
while (newPath) {
path = newPath
newPath = path.findParent((path) => path.isLogicalExpression())
}
path.skip()
if (path.findParent((path) => path.shouldSkip)) {
return
}
path.replaceWith(getMemberExpr(path, IDENTIFIER_GLOBAL, path.node, this))
}
},
TemplateLiteral (path) {
const nodes = []
const expressions = path.get('expressions')
let index = 0
for (const elem of path.node.quasis) {
if (elem.value.cooked) {
nodes.push(t.stringLiteral(elem.value.cooked))
}
if (index < expressions.length) {
const expr = expressions[index++]
const node = expr.node
if (!t.isStringLiteral(node, {
value: ''
})) {
nodes.push(node)
}
}
}
// since `+` is left-to-right associative
// ensure the first node is a string if first/second isn't
const considerSecondNode = !t.isStringLiteral(nodes[1])
if (!t.isStringLiteral(nodes[0]) && considerSecondNode) {
nodes.unshift(t.stringLiteral(''))
}
let root = nodes[0]
for (let i = 1; i < nodes.length; i++) {
root = t.binaryExpression('+', root, nodes[i])
}
path.replaceWith(root)
}
}
+200
View File
@@ -0,0 +1,200 @@
const {
hasOwn
} = require('../util')
const {
SELF_CLOSING_TAGS,
INTERNAL_EVENT_LINK,
VIRTUAL_HOST_STYLE,
VIRTUAL_HOST_CLASS,
ATTR_SLOT_ORIGIN
} = require('../constants')
const uniI18n = require('@dcloudio/uni-cli-i18n')
function processElement (ast, state, isRoot) {
const platform = state.options.platform
const platformName = platform.name
const mergeVirtualHostAttributes = state.options.mergeVirtualHostAttributes
// <template slot="f"></template>
if (ast.type === 'template' && hasOwn(ast.attr, 'slot')) {
ast.type = 'view'
}
// 由于小程序端 default 不等同于默认插槽,统一移除 default 命名
if (ast.type === 'slot' && hasOwn(ast.attr, 'name') && ast.attr.name === 'default') {
delete ast.attr.name
} else if (hasOwn(ast.attr, 'slot') && ast.attr.slot === 'default') {
delete ast.attr.slot
}
if (hasOwn(ast.attr, 'textContent')) {
ast.children = [ast.attr.textContent]
delete ast.attr.textContent
}
if (hasOwn(ast.attr, 'innerHTML')) {
ast.children = [{
type: 'rich-text',
attr: {
nodes: ast.attr.innerHTML
},
children: []
}]
delete ast.attr.innerHTML
}
if (platform.isComponent(ast.type)) {
if (platformName === 'mp-alipay') {
ast.attr.onVueInit = INTERNAL_EVENT_LINK
} else if (platformName !== 'mp-baidu') {
ast.attr['bind:' + INTERNAL_EVENT_LINK] = INTERNAL_EVENT_LINK
}
// TODO 过滤小程序原生组件
{
// 处理自定义组件虚拟节点样式
if (mergeVirtualHostAttributes) {
const obj = {
style: VIRTUAL_HOST_STYLE,
class: VIRTUAL_HOST_CLASS
}
Object.keys(obj).forEach(key => {
if (key in ast.attr) {
ast.attr[obj[key]] = ast.attr[key]
}
// 支付宝小程序自定义组件外部属性始终无效
if (platformName === 'mp-alipay') {
delete ast.attr[key]
}
})
}
// 标记自定义组件插槽
const children = ast.children
// default slot
let defaultSlot = false
const slots = []
for (let i = children.length - 1; i >= 0; i--) {
const childElement = children[i]
/**
* 百度、字节、支付宝小程序支持使用 block 作为命名插槽根节点,支付宝不支持在 block 使用动态插槽名
* <block slot="left"></block> => <view slot="left"></view>
*/
const attr = typeof childElement !== 'string' && childElement.attr
const slot = attr && (attr[ATTR_SLOT_ORIGIN] || attr.slot)
if (slot) {
delete attr[ATTR_SLOT_ORIGIN]
if (!['mp-baidu', 'mp-toutiao'].includes(platformName) && !(platformName === 'mp-alipay' && !/\{\{.+?\}\}/.test(attr.slot)) && attr.slot && attr.slot !== 'default' && childElement.type === 'block') {
childElement.type = 'view'
}
if (!slots.includes(slot)) {
slots.push(slot)
}
} else {
defaultSlot = true
}
}
if (defaultSlot) {
slots.push('default')
}
if (ast.attr.generic) {
Object.keys(ast.attr.generic).forEach(scopedSlotName => {
slots.push(scopedSlotName)
})
if (platformName === 'mp-toutiao' || platformName === 'mp-lark') {
// 用于字节跳动|飞书小程序模拟抽象节点
ast.attr.generic = `{{${JSON.stringify(ast.attr.generic)}}}`.replace(/"/g, '\'')
} else {
delete ast.attr.generic
}
}
if (slots.length) { // 标记 slots
ast.attr['vue-slots'] = '{{[' + slots.reverse().map(slotName => {
const res = slotName.match(/\{\{(.+?)\}\}/)
return res ? res[1] : `'${slotName}'`
}).join(',') + ']}}'
}
}
if (ast.attr.id && ast.attr.id.indexOf('{{') === 0) {
state.tips.add(uniI18n.__('templateCompiler.idAttribNotAllowInCustomComponentProps', { 0: ast.type }))
}
if (hasOwn(ast.attr, 'data') && platformName !== 'mp-toutiao') { // 百度中会出现异常情况
// TODO 暂不输出
// state.tips.add(`data 作为属性保留名,不允许在自定义组件 ${ast.type} 中定义为 props`)
}
}
}
function genElement (ast, state, isRoot = false) {
if (!ast) {
return ''
}
if (typeof ast === 'string') {
return genText(ast, state)
}
processElement(ast, state, isRoot)
const names = Object.keys(ast.attr)
const props = names.length
? ' ' +
names
.map(name => {
if (name.includes(':else')) {
return name
}
if (ast.attr[name] === '' && name !== 'value') { // value属性需要保留=''
return name
}
let value = ast.attr[name]
// 微信和QQ小程序解析 {{{}}} 报错,需要使用()包裹
value = value.replace(/(\{\{)(\{.+?\})(\}\})/, '$1($2)$3')
return `${name}="${value}"`
})
.join(' ')
: ''
if (SELF_CLOSING_TAGS.includes(ast.type)) {
return `<${ast.type}${props}/>`
}
let children = ast.children
.map(child => {
return genElement(child, state, isRoot && ast.type === 'block') // 如果根节点是 block,则继续 root
})
.join('')
if (ast.scoped) { // 简单处理的 scoped slots 子节点的变量
children = children.replace(new RegExp(ast.scoped + '.', 'g'), '')
}
return `<${ast.type}${props}>${children}</${ast.type}>`
}
function genText (ast, state) {
return ast
}
function parsePageMeta (ast, state) {
// 目前仅 mp-weixin 支持 page-meta
if (state.options.platform.name === 'mp-weixin') {
const children = ast.children
if (Array.isArray(children) && children.find(child => child.type === 'page-meta')) {
return children
}
}
return ast
}
module.exports = function generate (ast, state) {
ast = parsePageMeta(ast, state)
if (!Array.isArray(ast)) {
ast = [ast]
}
let code = ast.map(ast => genElement(ast, state, true)).join('')
const replaceCodes = state.options.replaceCodes
if (replaceCodes) {
Object.keys(replaceCodes).forEach(key => {
code = code.replace(new RegExp(key.replace('$', '\\$'), 'g'), replaceCodes[key])
})
}
return code
}
+631
View File
@@ -0,0 +1,631 @@
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 (platforms.includes(platform) && isComponent(node.type, 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 ''
}
+393
View File
@@ -0,0 +1,393 @@
const t = require('@babel/types')
const babelTraverse = require('@babel/traverse').default
const babelGenerate = require('@babel/generator').default
const babelTemplate = require('@babel/template').default
const uniI18n = require('@dcloudio/uni-cli-i18n')
const {
METHOD_RENDER_LIST,
METHOD_RESOLVE_SCOPED_SLOTS,
METHOD_CREATE_ELEMENT
} = require('./constants')
function cached (fn) {
const cache = Object.create(null)
return function cachedFn (str) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}
}
const customizeRE = /:/g
const camelizeRE = /-(\w)/g
const hyphenateRE = /\B([A-Z])/g
const camelize = cached((str) => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})
function getCode (node) {
return babelGenerate(t.cloneDeep(node), {
compact: true,
jsescOption: {
quotes: 'single',
minimal: true
}
}).code
}
function traverseKey (ast, state) {
let forKey = false
babelTraverse(ast, {
noScope: true,
ObjectProperty (path) {
if (forKey) {
return
}
if (path.node.key.name === 'key') {
forKey = path.node.value
path.stop()
}
},
CallExpression (path) {
if (path.node.callee.name === METHOD_RENDER_LIST) {
path.stop()
} else if (path.node.callee.name === METHOD_RESOLVE_SCOPED_SLOTS) {
path.skip()
}
}
})
return forKey
}
function traverseFilter (ast, state) {
const filterModules = state.options.filterModules
if (!filterModules.length) {
return false
}
let isFilter = false
babelTraverse(ast, {
noScope: true,
Identifier (path) {
if (filterModules.includes(path.node.name)) {
const parentNode = path.parent
if ( // t.msg || t['msg']
t.isMemberExpression(parentNode) &&
parentNode.object === path.node &&
(
t.isIdentifier(parentNode.property) ||
t.isLiteral(parentNode.property)
)
) {
isFilter = true
path.stop()
}
}
}
})
return isFilter
}
function wrapper (code, reverse = false) {
return reverse ? `{{!(${code})}}` : `{{${code}}}`
}
function genCode (node, noWrapper = false, reverse = false, quotes = true) {
if (t.isStringLiteral(node)) {
return reverse ? `!(${node.value})` : node.value
} else if (t.isIdentifier(node)) {
return noWrapper ? node.name : wrapper(node.name, reverse)
}
let code = getCode(node)
if (quotes) {
code = code.replace(/"/g, '\'')
}
return noWrapper ? code : wrapper(code, reverse)
}
function getForIndexIdentifier (id) {
return `__i${id}__`
}
function getForKey (forKey, forIndex, state) {
if (forKey) {
if (t.isIdentifier(forKey)) {
if (forIndex !== forKey.name) { // 非 forIndex
if (state.options.platform.name === 'mp-baidu') return getCode(forKey)
return '*this'
} else {
// TODO
// state.tips.add(`非 h5 平台 v-for 循环不支持使用索引值 ${forIndex} 作为 key,详情参考:https://uniapp.dcloud.io/use?id=key`)
return forKey.name
}
} else if (t.isMemberExpression(forKey)) {
if (state.options.platform.name === 'mp-baidu') return getCode(forKey)
return forKey.property.name || forKey.property.value
} else {
state.tips.add(uniI18n.__('templateCompiler.noH5KeyNoSupportExpression', { 0: getCode(forKey), 1: 'https://uniapp.dcloud.io/use?id=key' }))
}
}
return ''
}
function processMemberProperty (node, state) {
if (node.computed) {
const property = node.property
if (t.isNumericLiteral(property)) {
node.property = t.identifier('__$n' + property.value)
} else if (!t.isStringLiteral(property)) {
if (!hasOwn(state.options, '__m__')) {
state.options.__m__ = 0
state.options.replaceCodes = {}
}
const identifier = '__$m' + (state.options.__m__++) + '__'
const code = { property }
code.toString = function () {
return `'+${genCode(this.property, true)}+'`
}
state.options.replaceCodes[identifier] = code
if (state.computedProperty) {
state.computedProperty[identifier] = property
}
node.property = t.identifier(identifier)
}
node.computed = false
}
}
function replaceMemberExpression (stringLiteral, state) {
let code = `'${stringLiteral.value}'`
const replaceCodes = state.options.replaceCodes
if (replaceCodes) {
const options = {}
Object.keys(replaceCodes).forEach(key => {
const newCode = code.replace(new RegExp(key.replace('$', '\\$'), 'g'), `'+%%${key}%%+'`)
if (newCode !== code) {
options[key] = replaceCodes[key].property
code = newCode
}
})
const buildRequire = babelTemplate(code, { syntacticPlaceholders: true })
if (Object.keys(options).length) {
const ast = buildRequire(options)
return ast.expression
}
}
return stringLiteral
}
function processMemberExpression (element, state) {
// item['order']=>item.order
if (t.isMemberExpression(element)) {
element = t.cloneDeep(element)
if (t.isStringLiteral(element.property)) {
element.computed = false
}
// item[itemIndex[0]] = item[__$0__]
// item[1]=item['1']
processMemberProperty(element, state)
babelTraverse(element, {
noScope: true,
MemberExpression (path) {
processMemberProperty(path.node, state)
}
})
babelTraverse(element, {
noScope: true,
MemberExpression (path) {
if (t.isStringLiteral(path.node.property)) {
path.node.computed = false
}
},
StringLiteral (path) {
path.replaceWith(t.identifier(path.node.value))
}
})
}
return element
}
function hasOwn (obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key)
}
const tags = require('@dcloudio/uni-cli-shared/lib/tags')
const {
isBuiltInComponent
} = require('@dcloudio/uni-cli-shared/lib/pages')
const {
getTagName
} = require('./h5')
/**
* isComponent
* @param {string} tagName
* @param {string} [platform]
* @returns {boolean}
*/
function isComponent (tagName, platform) {
if (
tagName === 'block' ||
tagName === 'component' ||
tagName === 'template' ||
tagName === 'keep-alive'
) {
return false
}
// mp-weixin 底层支持 page-meta,navigation-bar
if (process.env.UNI_PLATFORM === 'mp-weixin') {
if (isBuiltInComponent(tagName)) {
return false
}
}
return !hasOwn(tags, getTagName(tagName.replace(/^v-uni-/, ''), platform))
}
function makeMap (str, expectsLowerCase) {
const map = Object.create(null)
const list = str.split(',')
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase
? val => map[val.toLowerCase()]
: val => map[val]
}
/**
* 微信、QQ小程序模板支持的简单类型
* @param {*} node
*/
function isSimpleObjectExpression (node) {
return t.isObjectExpression(node) && node.properties.length && !node.properties.find(({
key,
value
}) => !t.isIdentifier(key) || !(t.isIdentifier(value) || t.isStringLiteral(value) || t.isBooleanLiteral(value) ||
t.isNumericLiteral(value) || t.isNullLiteral(value)))
}
/**
* 是否包含转义引号
* @param {*} path
* @returns {boolean}
*/
function hasEscapeQuote (path) {
let has = false
function hasEscapeQuote (node) {
const quote = node.extra ? node.extra.raw[0] : '"'
if (node.value.includes(quote)) {
return true
}
}
if (path.isStringLiteral()) {
return hasEscapeQuote(path.node)
} else {
path.traverse({
noScope: true,
StringLiteral (path) {
if (hasEscapeQuote(path.node)) {
has = true
path.stop()
}
},
TemplateElement (path) {
if (path.node.value.cooked.includes('\'')) {
has = true
path.stop()
}
}
})
}
return has
}
/**
* 是否包含属性 length 访问
* @param {*} path
* @returns {boolean}
*/
function hasLengthProperty (path) {
let has = false
function hasLengthProperty (node) {
const property = node.property
// 暂不考虑动态拼接和模板字符串
return t.isIdentifier(property, { name: 'length' }) || t.isStringLiteral(property, { value: 'length' })
}
if (path.isMemberExpression()) {
return hasLengthProperty(path.node)
} else {
path.traverse({
noScope: true,
MemberExpression (path) {
if (hasLengthProperty(path.node)) {
has = true
path.stop()
}
}
})
}
return has
}
function isRootElement (path) {
const result = path.findParent(path => (path.isCallExpression() && path.get('callee').isIdentifier({ name: METHOD_CREATE_ELEMENT })) || path.isReturnStatement())
return result.isReturnStatement()
}
/**
* 事件绑定是否存在成员表达式 => obj.click2()
* @param {*} path
* @returns {boolean}
*/
const hasMemberExpression = (funcPath) => {
let result = false
funcPath.get('body').traverse({
CallExpression (path) {
if (t.isMemberExpression(path.node.callee)) {
result = true
path.stop()
}
}
})
return result
}
module.exports = {
hasOwn,
isUnaryTag: makeMap(
'image,area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' +
'link,meta,param,source,track,wbr'
),
isComponent,
genCode,
getCode,
camelize,
customize: cached((str) => {
return camelize(str.replace(customizeRE, '-'))
}),
capitalize: cached(str => {
return str.charAt(0).toUpperCase() + str.slice(1)
}),
hyphenate: cached((str) => {
return str.replace(hyphenateRE, '-$1').toLowerCase()
}),
getForKey,
traverseKey,
traverseFilter,
getComponentName: cached((str) => {
if (str.indexOf('wx-') === 0) {
return str.replace('wx-', 'weixin-')
}
return str
}),
processMemberExpression,
replaceMemberExpression,
getForIndexIdentifier,
isSimpleObjectExpression,
hasEscapeQuote,
hasLengthProperty,
isRootElement,
hasMemberExpression
}