import { parse , stringify } from 'himalaya-wxml' ; import * as t from 'babel-types' ; import traverse , { Visitor } from 'babel-traverse' ; import { AllKindNode , Attribute , WX_IF , WX_ELSE_IF , WX_ELSE , WX_FOR , parseContent , WX_KEY , Element , WX_FOR_ITEM , WX_FOR_INDEX , NodeType , Text } from './wxml' ; import { buildTemplateName , getWXMLsource } from './template' ; import * as fs from 'fs' ; import { relative , resolve } from 'path' ; import { setting , parseCode , buildImportStatement , codeFrameError } from './utils' ; import { replaceIdentifier , replaceMemberExpression } from './script' ; import { kebabCase } from 'lodash' ; const { prettyPrint } = require ( 'html' ) ; interface Result { ast : t . File ; template : string ; imports : VueImport [ ] ; } export function parseVue ( dirPath , wxml , jsCode = '' ) { let ast = parseCode ( jsCode ) ; let foundWXInstance = false ; const vistor = { BlockStatement ( path ) { path . scope . rename ( 'wx' , 'Taro' ) ; } , Identifier ( path ) { if ( path . isReferenced ( ) && path . node . name === 'wx' ) { path . replaceWith ( t . identifier ( 'Taro' ) ) ; } } , CallExpression ( path ) { const callee = path . get ( 'callee' ) ; replaceIdentifier ( callee ) ; replaceMemberExpression ( callee ) ; if ( callee . isIdentifier ( { name : 'Page' } ) || callee . isIdentifier ( { name : 'Component' } ) || callee . isIdentifier ( { name : 'App' } ) ) { foundWXInstance = true ; const componentType = callee . node . name ; ast . program . body . push ( t . exportDefaultDeclaration ( t . callExpression ( t . identifier ( 'withWeapp' ) , [ path . node . arguments [ 0 ] , t . stringLiteral ( componentType ) ] ) ) ) ; path . remove ( ) ; } } } ; traverse ( ast , vistor ) ; if ( ! foundWXInstance ) { ast = parseCode ( jsCode + ';Component({})' ) ; traverse ( ast , vistor ) ; } const taroImport = buildImportStatement ( '@tarojs/taro' , [ ] , 'Taro' ) ; const withWeappImport = buildImportStatement ( '@tarojs/with-weapp' , [ ] , 'withWeapp' ) ; ast . program . body . unshift ( taroImport , withWeappImport ) ; const { imports , template } = parseWXML ( dirPath , wxml , [ ] ) ; return { ast , imports , template } ; } export function parseWXML ( dirPath , wxml , imports ) { try { wxml = prettyPrint ( wxml , { max_char : 0 , indent_char : 0 , unformatted : [ 'text' , 'wxs' ] } ) ; } catch ( error ) { } const nodes = parse ( wxml . trim ( ) ) . map ( node => parseNode ( node , dirPath , imports ) ) ; const template = generateVueFile ( nodes ) ; return { nodes , template , imports } ; } function parseElement ( element , dirPath , imports ) { let forItem = 'item' ; let forIndex = 'index' ; switch ( element . tagName ) { case 'template' : parseTemplate ( element , imports ) ; break ; case 'wxs' : parseWXS ( element , imports ) ; break ; case 'import' : case 'include' : parseModule ( element , dirPath , imports ) ; return { type : NodeType . Text , content : '' } ; default : break ; } return { tagName : element . tagName , type : element . type , children : element . children . map ( child => parseNode ( child , dirPath , imports ) ) , attributes : element . attributes . filter ( a => { let match = true ; if ( a . key === WX_FOR_ITEM ) { match = false ; forItem = a . value || forItem ; } else if ( a . key === WX_FOR_INDEX ) { match = false ; forIndex = a . value || forIndex ; } return match ; } ) . map ( a => { return parseAttribute ( a , forItem , forIndex ) ; } ) } ; } function parseNode ( node , dirPath , imports ) { if ( node . type === NodeType . Text ) { return node ; } else if ( node . type === NodeType . Comment ) { return node ; } return parseElement ( node , dirPath , imports ) ; } const VUE_IF = 'v-if' ; const VUE_ELSE_IF = 'v-else-if' ; const VUE_FOR = 'v-for' ; const VUE_ELSE = 'v-else' ; const VUE_KEY = 'key' ; const WX_FOR_2 = 'wx:for-items' ; function parseAttribute ( attr , forItem , forIndex ) { let { key , value } = attr ; let isVueDirectives = true ; const isBind = key . startsWith ( 'bind' ) ; const isCatch = key . startsWith ( 'catch' ) ; const isEvent = isBind || isCatch ; switch ( key ) { case WX_IF : key = VUE_IF ; break ; case WX_ELSE_IF : key = VUE_ELSE_IF ; break ; case WX_ELSE : key = VUE_ELSE ; break ; case WX_FOR : case WX_FOR_2 : key = VUE_FOR ; break ; case WX_KEY : key = VUE_KEY ; value = value || forIndex ; isVueDirectives = false ; break ; default : isVueDirectives = false ; break ; } const { type , content } = parseContent ( value ?? '' , true ) ; if ( type === 'expression' ) { if ( content ?. startsWith ( '(' ) && content . endsWith ( ')' ) ) { value = content . slice ( 1 , content . length - 1 ) ; } else { value = content ; } if ( key === VUE_FOR ) { value = `(${ forItem }, ${ forIndex }) in ${ value }` ; } if ( key === 'data' ) { if ( value . includes ( ':' ) || ( value . includes ( '...' ) && value . includes ( ',' ) ) ) { value = `{ ${ value } }` ; } } if ( ! isVueDirectives && ! isEvent ) { key = ':' + key ; } } if ( key === VUE_KEY ) { if ( value ?. includes ( 'this' ) || value ?. includes ( '*item' ) ) { value = forItem ; } if ( value !== forItem || value !== forIndex ) { value = `${ forItem }.${ value } || ${ value } || ${ forIndex }` ; } key = ':' + key ; } if ( value === null && key !== VUE_ELSE ) { value = 'true' ; } if ( isBind ) { key = key . replace ( /^bind/g , '@' ) ; } if ( isCatch ) { key = key . replace ( /^catch/g , '@' ) + '.stop' ; } if ( isEvent && value === 'true' ) { value = 'emptyHandler' ; } if ( key . startsWith ( ':' ) ) { key = ':' + kebabCase ( key . slice ( 1 ) ) ; } else { key = kebabCase ( key ) ; } return { key , value } ; } function createElement ( tagName ) { return { tagName , type : NodeType . Element , children : [ ] , attributes : [ ] } ; } export function generateVueFile ( children ) { const template = createElement ( 'template' ) ; const container = createElement ( 'block' ) ; container . children = children ; template . children = [ container ] ; return stringify ( [ template ] ) ; } interface VueImport { name ? : string ; template ? : string ; ast ? : t . File ; wxs ? : boolean ; } export function parseTemplate ( element , imports ) { const { attributes , children } = element ; const is = attributes . find ( a => a . key === 'is' ) ; const data = attributes . find ( a => a . key === 'data' ) ; const name = attributes . find ( a => a . key === 'name' ) ; if ( name ) { const value = name . value ; const { type } = parseContent ( value ?? '' ) ; if ( type === 'expression' ) { console . warn ( 'template 的属性 name 只能是一个字符串，考虑更改以下源码逻辑：\n' , stringify ( element ) ) ; } const componentName = buildTemplateName ( name . key ) ; const component = parseWXML ( '' , stringify ( children ) , imports ) ! ; imports . push ( { name : componentName , template : component . template } ) ; } else if ( is ) { const value = is . value ; if ( ! value ) { console . warn ( 'template 的 `is` 属性不能为空' , stringify ( element ) ) ; } const { type } = parseContent ( value ?? '' ) ; if ( type === 'expression' ) { console . warn ( 'template 的属性 is 只能是一个字符串，考虑更改以下源码逻辑：\n' , stringify ( element ) ) ; } element . tagName = buildTemplateName ( value ! , false ) ; element . attributes = [ ] ; if ( data ) { element . attributes . push ( { key : 'data' , value : data . value } ) ; } } else { throw new Error ( 'template 标签必须指名 `is` 或 `name` 任意一个标签:\n' + stringify ( element ) ) ; } return element ; } export function parseModule ( element , dirPath , imports ) { const { attributes , tagName } = element ; const src = attributes . find ( a => a . key === 'src' ) ; if ( ! src ) { throw new Error ( `${ tagName } 标签必须包含 \`src\` 属性` + '\n' + stringify ( element ) ) ; } let srcValue = src . value ?? '' ; const { type } = parseContent ( srcValue ) ; if ( type === 'expression' ) { console . warn ( tagName + '的属性 src 只能是一个字符串，考虑更改以下源码逻辑：\n' , stringify ( element ) ) ; } if ( srcValue . startsWith ( '/' ) ) { const vpath = resolve ( setting . rootPath , srcValue . substr ( 1 ) ) ; if ( ! fs . existsSync ( vpath ) ) { throw new Error ( `import/include 的 src 请填入相对路径再进行转换：src="${ srcValue }"` ) ; } let relativePath = relative ( dirPath , vpath ) ; relativePath = relativePath . replace ( /\\/g , '/' ) ; if ( relativePath . indexOf ( '.' ) !== 0 ) { srcValue = './' + relativePath ; } srcValue = relativePath ; } if ( tagName === 'import' ) { const wxml = getWXMLsource ( dirPath , srcValue , tagName ) ; const mods = parseWXML ( resolve ( dirPath , srcValue ) , wxml , imports || [ ] ) ?. imports ; imports . push ( ... ( mods || [ ] ) ) ; } else { console . warn ( `暂不支持 ${ tagName } 标签的转换` , '考虑修改源码使用 import 替代\n' + stringify ( element ) ) ; } } function parseWXS ( element , imports ) { let moduleName = null ; let src = null ; const { attributes } = element ; for ( const attr of attributes ) { const { key , value } = attr ; if ( key === 'module' ) { moduleName = value ; } if ( key === 'src' ) { src = value ; } } if ( ! src ) { const script = element . children . reduce ( ( acc , node ) => { if ( node . type === NodeType . Text ) { return acc + node . content ; } return acc ; } , '' ) ; src = './wxs__' + moduleName ; const ast = parseCode ( script ) ; 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 } as any ) ; } if ( ! moduleName || ! src ) { throw new Error ( '一个 WXS 需要同时存在两个属性：`wxs`, `src`' ) ; } }