@mpxjs/webpack-plugin
Version:
mpx compile core
574 lines (565 loc) • 18.8 kB
JavaScript
const runRules = require('../../run-rules')
const JSON5 = require('json5')
const getComponentConfigs = require('./component-config')
const normalizeComponentRules = require('../normalize-component-rules')
const isValidIdentifierStr = require('../../../utils/is-valid-identifier-str')
const { parseMustacheWithContext, stringifyWithResolveComputed } = require('../../../template-compiler/compiler')
const normalize = require('../../../utils/normalize')
const { dash2hump } = require('../../../utils/hump-dash')
module.exports = function getSpec ({ warn, error }) {
function getRnDirectiveEventHandle (mode) {
return function ({ name, value }, { eventRules, el }) {
const match = this.test.exec(name)
const prefix = match[1]
const eventName = match[2]
const modifierStr = match[3] || ''
const meta = {
modifierStr
}
const rPrefix = runRules(spec.event.prefix, prefix, { mode })
const rEventName = runRules(eventRules, eventName, { mode, data: { el } })
return {
name: rPrefix + rEventName + meta.modifierStr,
value
}
}
}
function rnEventRulesHandle (eventName) {
const eventMap = {
tap: 'tap',
longtap: 'longpress',
longpress: 'longpress',
touchstart: 'touchstart',
touchmove: 'touchmove',
touchend: 'touchend',
touchcancel: 'touchcancel',
transitionend: 'transitionend'
}
if (eventMap[eventName]) {
return eventMap[eventName]
} else {
error(`React native environment does not support [${eventName}] event!`)
}
}
function rnAccessibilityRulesHandle ({ name, value }) {
if (name === 'aria-role') {
return [
{
name: 'accessible',
value: true
},
{
name: 'accessibilityRole',
value: value
}
]
} else {
return { name, value }
}
}
const spec = {
supportedModes: ['ali', 'swan', 'qq', 'tt', 'web', 'qa', 'jd', 'dd', 'ios', 'android', 'harmony'],
// props预处理
preProps: [],
// props后处理
postProps: [
{
web ({ name, value }) {
const parsed = parseMustacheWithContext(value)
if (name.startsWith('data-')) {
return {
name: ':' + name,
value: `__ensureString(${parsed.result})`
}
} else if (parsed.hasBinding) {
return {
name: name === 'animation' ? 'v-animation' : ':' + name,
value: parsed.result
}
}
}
}
],
// 指令处理
directive: [
// 特殊指令
{
test: 'wx:for',
swan (obj, data) {
const attrsMap = data.el.attrsMap
const parsed = parseMustacheWithContext(obj.value)
let listName = parsed.result
const el = data.el
const itemName = attrsMap['wx:for-item'] || 'item'
const indexName = attrsMap['wx:for-index'] || 'index'
const keyName = attrsMap['wx:key'] || null
let keyStr = ''
if (parsed.hasBinding) {
listName = listName.slice(1, -1)
}
if (keyName) {
const parsed = parseMustacheWithContext(keyName)
if (parsed.hasBinding) {
// keyStr = ` trackBy ${parsed.result.slice(1, -1)}`
} else if (keyName === '*this') {
keyStr = ` trackBy ${itemName}`
} else {
if (!isValidIdentifierStr(keyName)) {
keyStr = ` trackBy ${itemName}['${keyName}']`
} else {
keyStr = ` trackBy ${itemName}.${keyName}`
}
}
}
if (el) {
const injectWxsProp = {
injectWxsPath: '~' + normalize.lib('runtime/swanHelper.wxs'),
injectWxsModuleName: 'mpxSwanHelper'
}
if (el.injectWxsProps && Array.isArray(el.injectWxsProps)) {
el.injectWxsProps.push(injectWxsProp)
} else {
el.injectWxsProps = [injectWxsProp]
}
}
return {
name: 's-for',
value: `${itemName}, ${indexName} in mpxSwanHelper.processFor(${listName})${keyStr}`
}
},
web ({ value }, { el }) {
const parsed = parseMustacheWithContext(value)
const attrsMap = el.attrsMap
const itemName = attrsMap['wx:for-item'] || 'item'
const indexName = attrsMap['wx:for-index'] || 'index'
return {
name: 'v-for',
value: `(${itemName}, ${indexName}) in ${parsed.result}`
}
}
},
{
test: 'wx:key',
swan () {
return false
},
web ({ value }, { el }) {
// vue的template中不能包含key,对应于小程序中的block
if (el.tag === 'block') return false
const itemName = el.attrsMap['wx:for-item'] || 'item'
const keyName = value
if (value === '*this') {
value = itemName
} else {
if (isValidIdentifierStr(keyName)) {
value = `${itemName}.${keyName}`
} else {
value = `${itemName}['${keyName}']`
}
}
return {
name: ':key',
value
}
}
},
{
// 在swan/web模式下删除for-index/for-item,转换为v/s-for表达式
test: /^wx:(for-item|for-index)$/,
swan () {
return false
},
web () {
return false
}
},
{
test: 'wx:model',
web ({ value }, { el }) {
el.hasModel = true
const attrsMap = el.attrsMap
const tagRE = /\{\{((?:.|\n|\r)+?)\}\}(?!})/
const stringify = JSON.stringify
const match = tagRE.exec(value)
if (match) {
const modelProp = attrsMap['wx:model-prop'] || 'value'
const modelEvent = attrsMap['wx:model-event'] || 'input'
const modelValuePathRaw = attrsMap['wx:model-value-path']
const modelValuePath = modelValuePathRaw === undefined ? 'value' : modelValuePathRaw
const modelFilter = attrsMap['wx:model-filter']
let modelValuePathArr
try {
modelValuePathArr = JSON5.parse(modelValuePath)
} catch (e) {
if (modelValuePath === '') {
modelValuePathArr = []
} else {
modelValuePathArr = modelValuePath.split('.')
}
}
const modelValue = match[1].trim()
return [
{
name: ':' + modelProp,
value: modelValue
},
{
name: 'mpxModelEvent',
value: modelEvent
},
{
name: '@mpxModel',
value: `__model(${stringifyWithResolveComputed(modelValue)}, $event, ${stringify(modelValuePathArr)}, ${stringify(modelFilter)})`
}
]
}
}
},
{
test: /^wx:(model-prop|model-event|model-value-path|model-filter)$/,
web () {
return false
}
},
{
// ref只支持字符串字面量
test: 'wx:ref',
web ({ value }) {
return {
name: 'ref',
value: `__mpx_ref_${value}__`
}
}
},
{
// style样式绑定
test: /^(style|wx:style)$/,
web ({ value }, { el }) {
if (el.isStyleParsed) {
return false
}
const styleBinding = []
el.isStyleParsed = true
el.attrsList.filter(item => this.test.test(item.name)).forEach((item) => {
const parsed = parseMustacheWithContext(item.value)
styleBinding.push(parsed.result)
})
return {
name: ':style',
value: `[${styleBinding}] | transRpxStyle`
}
}
},
{
// 样式类名绑定
test: /^(class|wx:class)$/,
web ({ name, value }, { el }) {
if (el.classMerged) return false
const classBinding = []
el.attrsList.filter(item => this.test.test(item.name)).forEach(({ name, value }) => {
const parsed = parseMustacheWithContext(value)
if (name === 'wx:class') {
classBinding.push(parsed.result)
} else if (name === 'class' && parsed.hasBinding === true) {
el.classMerged = true
classBinding.push(parsed.result)
}
})
if (el.classMerged) {
return {
name: ':class',
value: `[${classBinding}]`
}
} else if (name === 'wx:class') {
// 对于纯静态class不做合并转换
return {
name: ':class',
value: classBinding[0]
}
}
}
},
// 通用指令
{
test: /^wx:(.*)$/,
ali ({ name, value }) {
const dir = this.test.exec(name)[1]
return {
name: 'a:' + dir,
value
}
},
swan ({ name, value }) {
const dir = this.test.exec(name)[1]
return {
name: 's-' + dir,
value
}
},
qq ({ name, value }) {
const dir = this.test.exec(name)[1]
return {
name: 'qq:' + dir,
value
}
},
jd ({ name, value }) {
const dir = this.test.exec(name)[1]
return {
name: 'jd:' + dir,
value
}
},
tt ({ name, value }) {
const dir = this.test.exec(name)[1]
return {
name: 'tt:' + dir,
value
}
},
dd ({ name, value }) {
const dir = this.test.exec(name)[1]
return {
name: 'dd:' + dir,
value
}
},
web ({ name, value }) {
let dir = this.test.exec(name)[1]
const parsed = parseMustacheWithContext(value)
if (dir === 'elif') {
dir = 'else-if'
}
return {
name: 'v-' + dir,
value: parsed.result
}
}
},
// 事件
{
test: /^(bind|catch|capture-bind|capture-catch):?(.*?)(\..*)?$/,
ali ({ name, value }, { eventRules }) {
const match = this.test.exec(name)
const prefix = match[1]
const eventName = match[2]
const modifierStr = match[3] || ''
const rPrefix = runRules(spec.event.prefix, prefix, { mode: 'ali' })
const rEventName = runRules(eventRules, eventName, { mode: 'ali' })
return {
name: rPrefix + dash2hump(rEventName.replace(/^./, (matched) => {
return matched.toUpperCase()
})) + modifierStr,
value
}
},
swan ({ name, value }, { eventRules }) {
const match = this.test.exec(name)
const prefix = match[1]
const eventName = match[2]
const modifierStr = match[3] || ''
let rPrefix = runRules(spec.event.prefix, prefix, { mode: 'swan' })
const rEventName = runRules(eventRules, eventName, { mode: 'swan' })
if (rEventName.includes('-')) rPrefix += ':'
return {
name: rPrefix + rEventName + modifierStr,
value
}
},
qq ({ name, value }, { eventRules }) {
const match = this.test.exec(name)
const prefix = match[1]
const eventName = match[2]
const modifierStr = match[3] || ''
let rPrefix = runRules(spec.event.prefix, prefix, { mode: 'qq' })
const rEventName = runRules(eventRules, eventName, { mode: 'qq' })
if (rEventName.includes('-')) rPrefix += ':'
return {
name: rPrefix + rEventName + modifierStr,
value
}
},
jd ({ name, value }, { eventRules }) {
const match = this.test.exec(name)
const prefix = match[1]
const eventName = match[2]
const modifierStr = match[3] || ''
let rPrefix = runRules(spec.event.prefix, prefix, { mode: 'jd' })
const rEventName = runRules(eventRules, eventName, { mode: 'jd' })
if (rEventName.includes('-')) rPrefix += ':'
return {
name: rPrefix + rEventName + modifierStr,
value
}
},
tt ({ name, value }, { eventRules }) {
const match = this.test.exec(name)
const prefix = match[1]
const eventName = match[2]
const modifierStr = match[3] || ''
let rPrefix = runRules(spec.event.prefix, prefix, { mode: 'tt' })
const rEventName = runRules(eventRules, eventName, { mode: 'tt' })
if (rEventName.includes('-')) rPrefix += ':'
return {
name: rPrefix + rEventName + modifierStr,
value
}
},
dd ({ name, value }, { eventRules }) {
const match = this.test.exec(name)
const prefix = match[1]
const eventName = match[2]
const modifierStr = match[3] || ''
let rPrefix = runRules(spec.event.prefix, prefix, { mode: 'dd' })
const rEventName = runRules(eventRules, eventName, { mode: 'dd' })
if (rEventName.includes('-')) rPrefix += ':'
return {
name: rPrefix + rEventName + modifierStr,
value
}
},
web ({ name, value }, { eventRules, el, usingComponents }) {
const match = this.test.exec(name)
const prefix = match[1]
const eventName = match[2]
const modifierStr = match[3] || ''
const meta = {
modifierStr
}
const isComponent = usingComponents.indexOf(el.tag) !== -1 || el.tag === 'component'
const rPrefix = runRules(spec.event.prefix, prefix, { mode: 'web', meta })
const rEventName = runRules(eventRules, eventName, { mode: 'web', data: { isComponent } })
return {
name: rPrefix + rEventName + meta.modifierStr,
value
}
},
ios: getRnDirectiveEventHandle('ios'),
android: getRnDirectiveEventHandle('android'),
harmony: getRnDirectiveEventHandle('harmony')
},
// 无障碍
{
test: /^aria-(role|label)$/,
ali () {
warn('Ali environment does not support aria-role|label props!')
},
ios: rnAccessibilityRulesHandle,
android: rnAccessibilityRulesHandle,
harmony: rnAccessibilityRulesHandle
}
],
event: {
prefix: [
{
ali (prefix) {
const prefixMap = {
bind: 'on',
catch: 'catch'
}
if (!prefixMap[prefix]) {
error(`Ali environment does not support [${prefix}] event handling!`)
return
}
return prefixMap[prefix]
},
// 通过meta将prefix转化为modifier
web (prefix, data, meta) {
const modifierStr = meta.modifierStr
const modifierMap = modifierStr.split('.').reduce((map, key) => {
if (key) {
map[key] = true
}
return map
}, {})
switch (prefix) {
case 'catch':
modifierMap.stop = true
break
case 'capture-bind':
modifierMap.capture = true
break
case 'capture-catch':
modifierMap.stop = true
modifierMap.capture = true
break
}
// web中不支持proxy modifier
delete modifierMap.proxy
const tempModifierStr = Object.keys(modifierMap).join('.')
meta.modifierStr = tempModifierStr ? '.' + tempModifierStr : ''
return '@'
}
// ios (prefix) {
// const prefixMap = {
// bind: 'on',
// catch: 'catch'
// }
// if (!prefixMap[prefix]) {
// error(`React native environment does not support [${prefix}] event handling!`)
// return
// }
// return prefixMap[prefix]
// },
// android (prefix) {
// const prefixMap = {
// bind: 'on',
// catch: 'catch'
// }
// if (!prefixMap[prefix]) {
// error(`React native environment does not support [${prefix}] event handling!`)
// return
// }
// return prefixMap[prefix]
// }
}
],
rules: [
// 通用冒泡事件
{
test: /^(touchstart|touchmove|touchcancel|touchend|tap|longpress|longtap|transitionend|animationstart|animationiteration|animationend|touchforcechange)$/,
ali (eventName) {
const eventMap = {
touchstart: 'touchStart',
touchmove: 'touchMove',
touchend: 'touchEnd',
touchcancel: 'touchCancel',
tap: 'tap',
longtap: 'longTap',
longpress: 'longTap',
transitionend: 'transitionEnd',
animationstart: 'animationStart',
animationiteration: 'animationIteration',
animationend: 'animationEnd'
}
if (eventMap[eventName]) {
return eventMap[eventName]
} else {
error(`Ali environment does not support [${eventName}] event!`)
}
},
web (eventName) {
if (eventName === 'touchforcechange') {
error(`Web environment does not support [${eventName}] event!`)
}
},
ios: rnEventRulesHandle,
android: rnEventRulesHandle,
harmony: rnEventRulesHandle
},
// web event escape
{
test: /^click$/,
web (eventName, { isComponent }) {
// 自定义组件根节点
if (isComponent) {
return '_' + eventName
}
}
}
]
}
}
spec.rules = normalizeComponentRules(getComponentConfigs({ warn, error }).concat({}), spec)
return spec
}