import { parse } from 'himalaya-wxml' ; import * as t from 'babel-types' ; import { camelCase , cloneDeep } from 'lodash' ; import traverse , { NodePath , Visitor } from 'babel-traverse' ; import { buildTemplate , DEFAULT_Component_SET , buildImportStatement , buildBlockElement , parseCode , codeFrameError , isValidVarName } from './utils' ; import { specialEvents } from './events' ; import { parseTemplate , parseModule } from './template' ; import { usedComponents , errors , globals , THIRD_PARTY_COMPONENTS } from './global' ; import { reserveKeyWords } from './constant' ; import { parse as parseFile } from 'babylon' ; import { getCacheWxml , saveCacheWxml } from './cache' ; const { prettyPrint } = require ( 'html' ) ; const allCamelCase = ( str ) => str . charAt ( 0 ) . toUpperCase ( ) + camelCase ( str . substr ( 1 ) ) ; function buildSlotName ( slotName ) { return `render${ slotName [ 0 ] . toUpperCase ( ) + slotName . replace ( '-' , '' ) . slice ( 1 ) }` ; } export enum NodeType { Element = 'element' , Comment = 'comment' , Text = 'text' } export interface Element { type : NodeType . Element ; tagName : string ; children : AllKindNode [ ] ; attributes : Attribute [ ] ; } export interface Attribute { key : string ; value : string | null ; } export interface Comment { type : NodeType . Comment ; content : string ; } export interface Text { type : NodeType . Text ; content : string ; } export interface WXS { module : string ; src : string ; } export type AllKindNode = Element | Comment | Text ; export type Node = Element | Text ; interface Condition { condition : string ; path : NodePath < t . JSXElement > ; tester : t . JSXExpressionContainer ; } export type AttrValue = t . StringLiteral | t . JSXElement | t . JSXExpressionContainer | null ; export interface Imports { ast : t . File ; name : string ; wxs ? : boolean ; } export interface Wxml { wxses : WXS [ ] ; wxml ? : t . Node ; imports : Imports [ ] ; refIds : Set < string > ; } export const WX_IF = 'wx:if' ; export const WX_ELSE_IF = 'wx:elif' ; export const WX_FOR = 'wx:for' ; export const WX_FOR_ITEM = 'wx:for-item' ; export const WX_FOR_INDEX = 'wx:for-index' ; export const WX_KEY = 'wx:key' ; export const WX_ELSE = 'wx:else' ; export const wxTemplateCommand = [ WX_IF , WX_ELSE_IF , WX_FOR , WX_FOR_ITEM , WX_FOR_INDEX , WX_KEY , 'wx:else' ] ; function buildElement ( name , children = [ ] , attributes = [ ] ) { return { tagName : name , type : NodeType . Element , attributes , children } ; } export const createWxmlVistor = ( loopIds , refIds , dirPath , wxses = [ ] , imports = [ ] ) => { const jsxAttrVisitor = ( path ) => { const name = path . node . name as t . JSXIdentifier ; const jsx = path . findParent ( p => p . isJSXElement ( ) ) as NodePath < t . JSXElement > ; if ( name . name === 'hidden' ) { const value = path . get ( 'value' ) as NodePath < t . JSXExpressionContainer > ; if ( t . isJSXExpressionContainer ( value ) ) { const exclamation = t . unaryExpression ( '!' , value . node . expression ) ; path . set ( 'value' , t . jSXExpressionContainer ( exclamation ) ) ; path . set ( 'name' , t . jSXIdentifier ( WX_IF ) ) ; } } const valueCopy = cloneDeep ( path . get ( 'value' ) . node ) ; transformIf ( name . name , path , jsx , valueCopy ) ; const loopItem = transformLoop ( name . name , path , jsx , valueCopy ) ; if ( loopItem ) { if ( loopItem . index && ! refIds . has ( loopItem . index ) ) { loopIds . add ( loopItem . index ) ; } if ( loopItem . item && ! refIds . has ( loopItem . item ) ) { loopIds . add ( loopItem . item ) ; } } } ; const renameJSXKey = ( path ) => { const nodeName = path . node . name ; if ( path . parentPath . isJSXAttribute ( ) ) { if ( nodeName === WX_KEY ) { path . replaceWith ( t . jSXIdentifier ( 'key' ) ) ; } if ( nodeName . startsWith ( 'wx:' ) && ! wxTemplateCommand . includes ( nodeName ) ) { console . log ( `未知 wx 作用域属性： ${ nodeName }，该属性会被移除掉。` ) ; path . parentPath . remove ( ) ; } } } ; return { JSXAttribute : jsxAttrVisitor , JSXIdentifier : renameJSXKey , JSXElement : { enter ( path ) { const openingElement = path . get ( 'openingElement' ) ; const jsxName = openingElement . get ( 'name' ) ; const attrs = openingElement . get ( 'attributes' ) ; if ( ! jsxName . isJSXIdentifier ( ) ) { return ; } path . traverse ( { Identifier ( p ) { if ( ! p . isReferencedIdentifier ( ) ) { return ; } const jsxExprContainer = p . findParent ( p => p . isJSXExpressionContainer ( ) ) ; if ( ! jsxExprContainer || ! jsxExprContainer . isJSXExpressionContainer ( ) ) { return ; } if ( isValidVarName ( p . node . name ) ) { refIds . add ( p . node . name ) ; } } , JSXAttribute : jsxAttrVisitor , JSXIdentifier : renameJSXKey } ) ; const slotAttr = attrs . find ( a => a . node ?. name . name === 'slot' ) ; if ( slotAttr ) { const slotValue = slotAttr . node . value ; if ( slotValue && t . isStringLiteral ( slotValue ) ) { const slotName = slotValue . value ; const parentComponent = path . findParent ( p => p . isJSXElement ( ) && t . isJSXIdentifier ( p . node . openingElement . name ) && ! DEFAULT_Component_SET . has ( p . node . openingElement . name . name ) ) ; if ( parentComponent && parentComponent . isJSXElement ( ) ) { slotAttr . remove ( ) ; path . traverse ( { JSXAttribute : jsxAttrVisitor } ) ; const block = buildBlockElement ( ) ; block . children = [ cloneDeep ( path . node ) ] ; parentComponent . node . openingElement . attributes . push ( t . jSXAttribute ( t . jSXIdentifier ( buildSlotName ( slotName ) ) , t . jSXExpressionContainer ( block ) ) ) ; path . remove ( ) ; } } else { throw codeFrameError ( slotValue , 'slot 的值必须是一个字符串' ) ; } } const tagName = jsxName . node . name ; if ( tagName === 'Slot' ) { const nameAttr = attrs . find ( a => a . node . name . name === 'name' ) ; let slotName = '' ; if ( nameAttr ) { if ( nameAttr . node . value && t . isStringLiteral ( nameAttr . node . value ) ) { slotName = nameAttr . node . value . value ; } else { throw codeFrameError ( jsxName . node , 'slot 的值必须是一个字符串' ) ; } } const children = t . memberExpression ( t . memberExpression ( t . thisExpression ( ) , t . identifier ( 'props' ) ) , t . identifier ( slotName ? buildSlotName ( slotName ) : 'children' ) ) ; try { path . replaceWith ( path . parentPath . isJSXElement ( ) ? t . jSXExpressionContainer ( children ) : children ) ; } catch ( error ) { } } if ( tagName === 'Wxs' ) { wxses . push ( getWXS ( attrs . map ( a => a . node ) , path , imports ) ) ; } if ( tagName === 'Template' ) { const template = parseTemplate ( path , dirPath ) ; if ( template ) { const { ast : classDecl , name } = template ; const taroComponentsImport = buildImportStatement ( '@tarojs/components' , [ ... usedComponents ] ) ; const taroImport = buildImportStatement ( '@tarojs/taro' , [ ] , 'Taro' ) ; const reactImport = buildImportStatement ( 'react' , [ ] , 'React' ) ; const ast = t . file ( t . program ( [ ] ) ) ; ast . program . body . unshift ( taroComponentsImport , reactImport , taroImport , t . exportDefaultDeclaration ( classDecl ) ) ; const usedTemplate = new Set < string > ( ) ; traverse ( ast , { JSXIdentifier ( path ) { const node = path . node ; if ( node . name . endsWith ( 'Tmpl' ) && node . name . length > 4 && path . parentPath . isJSXOpeningElement ( ) ) { usedTemplate . add ( node . name ) ; } } } ) ; usedTemplate . forEach ( componentName => { if ( componentName !== classDecl . id . name ) { ast . program . body . unshift ( buildImportStatement ( `./${ componentName }` , [ ] , componentName ) ) ; } } ) ; imports . push ( { ast , name } ) ; } } if ( tagName === 'Import' ) { const mods = parseModule ( path , dirPath , 'import' ) ; if ( mods ) { imports . push ( ... mods ) ; } } if ( tagName === 'Include' ) { parseModule ( path , dirPath , 'include' ) ; } } , exit ( path ) { const openingElement = path . get ( 'openingElement' ) ; const jsxName = openingElement . get ( 'name' ) ; if ( ! jsxName . isJSXIdentifier ( { name : 'Block' } ) ) { return ; } const children = path . node . children ; if ( children . length === 1 ) { const caller = children [ 0 ] ; if ( t . isJSXExpressionContainer ( caller ) && t . isCallExpression ( caller . expression ) && ! path . parentPath . isExpressionStatement ( ) ) { try { path . replaceWith ( caller ) ; } catch ( error ) { } } } } } } as Visitor ; } ; export function parseWXML ( dirPath , wxml ? , parseImport ? ) { let parseResult = getCacheWxml ( dirPath ) ; if ( parseResult ) { return parseResult ; } try { wxml = prettyPrint ( wxml , { max_char : 0 , indent_char : 0 , unformatted : [ 'text' , 'wxs' ] } ) ; } catch ( error ) { } if ( ! parseImport ) { errors . length = 0 ; usedComponents . clear ( ) ; } usedComponents . add ( 'Block' ) ; const wxses = [ ] ; const imports = [ ] ; const refIds = new Set < string > ( ) ; const loopIds = new Set < string > ( ) ; if ( ! wxml ) { return { wxses , imports , refIds , wxml : t . nullLiteral ( ) } ; } const nodes = removEmptyTextAndComment ( parse ( wxml . trim ( ) ) ) ; const ast = t . file ( t . program ( [ t . expressionStatement ( parseNode ( buildElement ( 'block' , nodes as Node [ ] ) ) as t . Expression ) ] , [ ] ) ) ; traverse ( ast , createWxmlVistor ( loopIds , refIds , dirPath , wxses , imports ) ) ; refIds . forEach ( id => { if ( loopIds . has ( id ) || imports . filter ( i => i . wxs ) . map ( i => i . name ) . includes ( id ) ) { refIds . delete ( id ) ; } } ) ; parseResult = { wxses , imports , wxml : hydrate ( ast ) , refIds } ; saveCacheWxml ( dirPath , parseResult ) ; return parseResult ; } function getWXS ( attrs , path , imports ) { let moduleName = null ; let src = null ; for ( const attr of attrs ) { if ( t . isJSXIdentifier ( attr . name ) ) { const attrName = attr . name . name ; const attrValue = attr . value ; let value = null ; if ( attrValue === null ) { throw new Error ( 'WXS 标签的属性值不得为空' ) ; } if ( t . isStringLiteral ( attrValue ) ) { value = attrValue . value ; } else if ( t . isJSXExpressionContainer ( attrValue ) && t . isStringLiteral ( attrValue . expression ) ) { value = attrValue . expression . value ; } if ( attrName === 'module' ) { moduleName = value ; } if ( attrName === 'src' ) { src = value ; } } } if ( ! src ) { const { children : [ script ] } = path . node ; if ( ! t . isJSXText ( script ) ) { throw new Error ( 'wxs 如果没有 src 属性，标签内部必须有 wxs 代码。' ) ; } src = './wxs__' + moduleName ; const ast = parseCode ( script . value ) ; traverse ( ast , { CallExpression ( path ) { if ( t . isIdentifier ( path . node . callee , { name : 'getRegExp' } ) ) { console . warn ( codeFrameError ( path . node , '请使用 JavaScript 标准正则表达式把这个 getRegExp 函数重构。' ) ) ; } } } ) ; imports . push ( { ast , name : moduleName as string , wxs : true } ) ; } if ( ! moduleName || ! src ) { throw new Error ( '一个 WXS 需要同时存在两个属性：`wxs`, `src`' ) ; } path . remove ( ) ; return { module : moduleName , src } ; } function hydrate ( file ) { const ast = file . program . body [ 0 ] ; if ( ast && t . isExpressionStatement ( ast ) && t . isJSXElement ( ast . expression ) ) { const jsx = ast . expression ; if ( jsx . children . length === 1 ) { const children = jsx . children [ 0 ] ; return t . isJSXExpressionContainer ( children ) ? children . expression : children ; } else { return jsx ; } } } function transformLoop ( name , attr , jsx , value ) { const jsxElement = jsx . get ( 'openingElement' ) ; if ( ! jsxElement . node ) { return ; } const attrs = jsxElement . get ( 'attributes' ) . map ( a => a . node ) ; const wxForItem = attrs . find ( a => a . name . name === WX_FOR_ITEM ) ; const hasSinglewxForItem = wxForItem && wxForItem . value && t . isJSXExpressionContainer ( wxForItem . value ) ; if ( hasSinglewxForItem || name === WX_FOR || name === 'wx:for-items' ) { if ( ! value || ! t . isJSXExpressionContainer ( value ) ) { throw new Error ( 'wx:for 的值必须使用 "{{}}"  包裹' ) ; } attr . remove ( ) ; let item = t . stringLiteral ( 'item' ) ; let index = t . stringLiteral ( 'index' ) ; jsx . get ( 'openingElement' ) . get ( 'attributes' ) . forEach ( p => { const node = p . node ; if ( node . name . name === WX_FOR_ITEM ) { if ( ! node . value || ! t . isStringLiteral ( node . value ) ) { throw new Error ( WX_FOR_ITEM + ' 的值必须是一个字符串' ) ; } item = node . value ; p . remove ( ) ; } if ( node . name . name === WX_FOR_INDEX ) { if ( ! node . value || ! t . isStringLiteral ( node . value ) ) { throw new Error ( WX_FOR_INDEX + ' 的值必须是一个字符串' ) ; } index = node . value ; p . remove ( ) ; } } ) ; jsx . get ( 'openingElement' ) . get ( 'attributes' ) . forEach ( p => { const node = p . node ; if ( node . name . name === WX_KEY && t . isStringLiteral ( node . value ) ) { if ( node . value . value === '*this' ) { node . value = t . jSXExpressionContainer ( t . identifier ( item . value ) ) ; } else { node . value = t . jSXExpressionContainer ( t . memberExpression ( t . identifier ( item . value ) , t . identifier ( node . value . value ) ) ) ; } } } ) ; const replacement = t . jSXExpressionContainer ( t . callExpression ( t . memberExpression ( value . expression , t . identifier ( 'map' ) ) , [ t . arrowFunctionExpression ( [ t . identifier ( item . value ) , t . identifier ( index . value ) ] , t . blockStatement ( [ t . returnStatement ( jsx . node ) ] ) ) ] ) ) ; const block = buildBlockElement ( ) ; block . children = [ replacement ] ; try { jsx . replaceWith ( block ) ; } catch ( error ) { } return { item : item . value , index : index . value } ; } } function transformIf ( name , attr , jsx , value ) { if ( name !== WX_IF ) { return ; } if ( jsx . node . openingElement . attributes . some ( a => a . name . name === 'slot' ) ) { return ; } const conditions = [ ] ; let siblings = [ ] ; try { siblings = jsx . getAllNextSiblings ( ) . filter ( s => ! ( s . isJSXExpressionContainer ( ) && s . get ( 'expression' ) . isJSXEmptyExpression ( ) ) ) ; } catch ( error ) { return ; } if ( value === null || ! t . isJSXExpressionContainer ( value ) ) { console . error ( 'wx:if 的值需要用双括号 `{{}}` 包裹它的值' ) ; if ( value && t . isStringLiteral ( value ) ) { value = t . jSXExpressionContainer ( buildTemplate ( value . value ) ) ; } } conditions . push ( { condition : WX_IF , path : jsx , tester : value as t . JSXExpressionContainer } ) ; attr . remove ( ) ; for ( let index = 0 ; index < siblings . length ; index ++ ) { const sibling = siblings [ index ] ; const next = cloneDeep ( siblings [ index + 1 ] ) ; const currMatches = findWXIfProps ( sibling ) ; const nextMatches = findWXIfProps ( next ) ; if ( currMatches === null ) { break ; } conditions . push ( { condition : currMatches . reg . input as string , path : sibling as any , tester : currMatches . tester as t . JSXExpressionContainer } ) ; if ( nextMatches === null ) { break ; } } handleConditions ( conditions ) ; } function handleConditions ( conditions ) { if ( conditions . length === 1 ) { const ct = conditions [ 0 ] ; try { ct . path . replaceWith ( t . jSXExpressionContainer ( t . logicalExpression ( '&&' , ct . tester . expression , cloneDeep ( ct . path . node ) ) ) ) ; } catch ( error ) { } } if ( conditions . length > 1 ) { const lastLength = conditions . length - 1 ; const lastCon = conditions [ lastLength ] ; let lastAlternate = cloneDeep ( lastCon . path . node ) ; try { if ( lastCon . condition === WX_ELSE_IF ) { lastAlternate = t . logicalExpression ( '&&' , lastCon . tester . expression , lastAlternate ) ; } const node = conditions . slice ( 0 , lastLength ) . reduceRight ( ( acc , condition ) => { return t . conditionalExpression ( condition . tester . expression , cloneDeep ( condition . path . node ) , acc ) ; } , lastAlternate ) ; conditions [ 0 ] . path . replaceWith ( t . jSXExpressionContainer ( node ) ) ; conditions . slice ( 1 ) . forEach ( c => c . path . remove ( ) ) ; } catch ( error ) { console . error ( 'wx:elif 的值需要用双括号 `{{}}` 包裹它的值' ) ; } } } function findWXIfProps ( jsx ) { let matches = null ; jsx && jsx . isJSXElement ( ) && jsx . get ( 'openingElement' ) . get ( 'attributes' ) . some ( path => { const attr = path . node ; if ( t . isJSXIdentifier ( attr . name ) ) { const name = attr . name . name ; if ( name === WX_IF ) { return true ; } const match = name . match ( /wx:else|wx:elif/ ) ; if ( match ) { path . remove ( ) ; matches = { reg : match , tester : attr . value } ; return true ; } } return false ; } ) ; return matches ; } function parseNode ( node , tagName ? ) { if ( node . type === NodeType . Text ) { return parseText ( node , tagName ) ; } else if ( node . type === NodeType . Comment ) { const emptyStatement = t . jSXEmptyExpression ( ) ; emptyStatement . innerComments = [ { type : 'CommentBlock' , value : ' ' + node . content + ' ' } ] as any [ ] ; return t . jSXExpressionContainer ( emptyStatement ) ; } return parseElement ( node ) ; } function parseElement ( element ) { const tagName = t . jSXIdentifier ( THIRD_PARTY_COMPONENTS . has ( element . tagName ) ? element . tagName : allCamelCase ( element . tagName ) ) ; if ( DEFAULT_Component_SET . has ( tagName . name ) ) { usedComponents . add ( tagName . name ) ; } let attributes = element . attributes ; if ( tagName . name === 'Template' ) { let isSpread = false ; attributes = attributes . map ( attr => { if ( attr . key === 'data' ) { const value = attr . value || '' ; const content = parseContent ( value ) ; if ( content . type === 'expression' ) { isSpread = true ; const str = content . content ; const strLastIndex = str . length - 1 ; if ( str . includes ( '...' ) && str . includes ( ',' ) ) { attr . value = `{{${ str . slice ( 1 , strLastIndex ) }}}` ; } else { if ( str . includes ( '...' ) ) { attr . value = `{{${ str . slice ( 4 , strLastIndex ) }}}` ; } else if ( /^\(([A-Za-z]+)\)$/ . test ( str ) ) { attr . value = `{{${ str . replace ( /^\(([A-Za-z]+)\)$/ , '$1:$1' ) }}}` ; } else { attr . value = `{{${ str . slice ( 1 , strLastIndex ) }}}` ; } } } else { attr . value = content . content ; } } return attr ; } ) ; if ( isSpread ) { attributes . push ( { key : 'spread' , value : null } ) ; } } return t . jSXElement ( t . jSXOpeningElement ( tagName , attributes . map ( parseAttribute ) ) , t . jSXClosingElement ( tagName ) , removEmptyTextAndComment ( element . children ) . map ( ( el ) => parseNode ( el , element . tagName ) ) , false ) ; } export function removEmptyTextAndComment ( nodes ) { return nodes . filter ( node => { return node . type === NodeType . Element || ( node . type === NodeType . Text && node . content . trim ( ) . length !== 0 ) || node . type === NodeType . Comment ; } ) . filter ( ( node , index ) => ! ( index === 0 && node . type === NodeType . Comment ) ) ; } function parseText ( node , tagName ? ) { if ( tagName === 'wxs' ) { return t . jSXText ( node . content ) ; } const { type , content } = parseContent ( node . content ) ; if ( type === 'raw' ) { const text = content . replace ( /([{}]+)/g , "{'$1'}" ) ; return t . jSXText ( text ) ; } return t . jSXExpressionContainer ( buildTemplate ( content ) ) ; } const handlebarsRE = /\{\{((?:.|\n)+?)\}\}/g ; function singleQuote ( s ) { return `'${ s }'` ; } export function parseContent ( content , single = false ) { content = content . trim ( ) ; if ( ! handlebarsRE . test ( content ) ) { return { type : 'raw' , content } ; } const tokens = [ ] ; let lastIndex = ( handlebarsRE . lastIndex = 0 ) ; let match ; let index ; let tokenValue ; while ( ( match = handlebarsRE . exec ( content ) ) ) { index = match . index ; if ( index > lastIndex ) { tokenValue = content . slice ( lastIndex , index ) ; tokens . push ( single ? singleQuote ( tokenValue ) : JSON . stringify ( tokenValue ) ) ; } const exp = match [ 1 ] . trim ( ) ; tokens . push ( `(${ exp })` ) ; lastIndex = index + match [ 0 ] . length ; } if ( lastIndex < content . length ) { tokenValue = content . slice ( lastIndex ) ; tokens . push ( single ? singleQuote ( tokenValue ) : JSON . stringify ( tokenValue ) ) ; } return { type : 'expression' , content : tokens . join ( '+' ) } ; } function parseAttribute ( attr ) { let { key , value } = attr ; let jsxValue = null ; if ( value ) { if ( key === 'class' && value . startsWith ( '[' ) && value . endsWith ( ']' ) ) { value = value . slice ( 1 , value . length - 1 ) . replace ( ',' , '' ) ; console . log ( codeFrameError ( attr , 'Taro/React 不支持 class 传入数组，此写法可能无法得到正确的 class' ) ) ; } const { type , content } = parseContent ( value ) ; if ( type === 'raw' ) { jsxValue = t . stringLiteral ( content . replace ( /"/g , '\'' ) ) ; } else { let expr ; try { expr = buildTemplate ( content ) ; } catch ( error ) { const pureContent = content . slice ( 1 , content . length - 1 ) ; if ( reserveKeyWords . has ( pureContent ) ) { const err = `转换模板参数： \`${ key }: ${ value }\` 报错: \`${ pureContent }\` 是 JavaScript 保留字，请不要使用它作为值。` ; if ( key === WX_KEY ) { expr = t . stringLiteral ( '' ) ; } else { throw new Error ( err ) ; } } else if ( content . includes ( ':' ) || ( content . includes ( '...' ) && content . includes ( ',' ) ) ) { const file = parseFile ( `var a = ${ attr . value ! . slice ( 1 , attr . value ! . length - 1 ) }` , { plugins : [ 'objectRestSpread' ] } ) ; expr = file . program . body [ 0 ] . declarations [ 0 ] . init ; } else { const err = `转换模板参数： \`${ key }: ${ value }\` 报错` ; throw new Error ( err ) ; } } if ( t . isThisExpression ( expr ) ) { console . error ( '在参数中使用 `this` 可能会造成意想不到的结果，已将此参数修改为 `__placeholder__`，你可以在转换后的代码查找这个关键字修改。' ) ; expr = t . stringLiteral ( '__placeholder__' ) ; } jsxValue = t . jSXExpressionContainer ( expr ) ; } } const jsxKey = handleAttrKey ( key ) ; if ( /^on[A-Z]/ . test ( jsxKey ) && ! ( /^catch/ . test ( key ) ) && jsxValue && t . isStringLiteral ( jsxValue ) ) { jsxValue = t . jSXExpressionContainer ( t . memberExpression ( t . thisExpression ( ) , t . identifier ( jsxValue . value ) ) ) ; } if ( key . startsWith ( 'catch' ) && value ) { if ( value === 'true' || value . trim ( ) === '' ) { jsxValue = t . jSXExpressionContainer ( t . memberExpression ( t . thisExpression ( ) , t . identifier ( 'privateStopNoop' ) ) ) ; globals . hasCatchTrue = true ; } else if ( t . isStringLiteral ( jsxValue ) ) { jsxValue = t . jSXExpressionContainer ( t . callExpression ( t . memberExpression ( t . memberExpression ( t . thisExpression ( ) , t . identifier ( 'privateStopNoop' ) ) , t . identifier ( 'bind' ) ) , [ t . thisExpression ( ) , t . memberExpression ( t . thisExpression ( ) , t . identifier ( jsxValue . value ) ) ] ) ) ; } } return t . jSXAttribute ( t . jSXIdentifier ( jsxKey ) , jsxValue ) ; } function handleAttrKey ( key ) { if ( key . startsWith ( 'wx:' ) || key . startsWith ( 'wx-' ) || key . startsWith ( 'data-' ) ) { return key ; } else if ( key === 'class' ) { return 'className' ; } else if ( /^(bind|catch)[a-z|:]/ . test ( key ) ) { if ( specialEvents . has ( key ) ) { return specialEvents . get ( key ) ! ; } else { key = key . replace ( /^(bind:|catch:|bind|catch)/ , 'on' ) ; key = camelCase ( key ) ; if ( ! isValidVarName ( key ) ) { throw new Error ( `"${ key }" 不是一个有效 JavaScript 变量名` ) ; } return key . substr ( 0 , 2 ) + key [ 2 ] . toUpperCase ( ) + key . substr ( 3 ) ; } } return camelCase ( key ) ; }