add taro-transformer-wx
This commit is contained in:
parent
cb83549436
commit
77658152c0
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "omio",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.7",
|
||||
"description": "Omi for old browsers(IE8+ and mobile browsers).",
|
||||
"main": "dist/omi.js",
|
||||
"jsnext:main": "dist/omi.esm.js",
|
||||
|
|
|
@ -36,8 +36,8 @@ options.root.Omi = {
|
|||
getHost,
|
||||
renderToString
|
||||
}
|
||||
options.root.omi = Omi
|
||||
options.root.Omi.version = 'omio-1.3.6'
|
||||
options.root.omi = options.root.Omi
|
||||
options.root.Omi.version = 'omio-1.3.7'
|
||||
|
||||
export default {
|
||||
h,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
单元测试先跑起来
|
||||
https://github.com/NervJS/taro/blob/master/packages/taro-transformer-wx/__tests__/loop.spec.ts
|
|
@ -0,0 +1,4 @@
|
|||
/lib
|
||||
/node_modules
|
||||
t.js
|
||||
tt.js
|
|
@ -0,0 +1,12 @@
|
|||
dist
|
||||
node_modules
|
||||
.cache
|
||||
.DS_Store
|
||||
lerna-debug.log
|
||||
yarn-error.log
|
||||
_book
|
||||
.idea
|
||||
lib
|
||||
t.js
|
||||
tt.js
|
||||
test.js
|
|
@ -0,0 +1,43 @@
|
|||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
<a name="0.0.69-beta.1"></a>
|
||||
## [0.0.69-beta.1](https://github.com/NervJS/taro/compare/v0.0.69-beta.0...v0.0.69-beta.1) (2018-07-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **transformer:** 支持 style 传入对象 ([d0be191](https://github.com/NervJS/taro/commit/d0be191))
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="0.0.67-beta.3"></a>
|
||||
## [0.0.67-beta.3](https://github.com/NervJS/taro/compare/v0.0.67-beta.2...v0.0.67-beta.3) (2018-07-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **transformer:** 单独使用的自定义也加入 key ([a231a90](https://github.com/NervJS/taro/commit/a231a90))
|
||||
* **transformer:** 所有设置 if 条件都加入 block ([a32661e](https://github.com/NervJS/taro/commit/a32661e))
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="0.0.67-beta.1"></a>
|
||||
## [0.0.67-beta.1](https://github.com/NervJS/taro/compare/v0.0.67-beta.0...v0.0.67-beta.1) (2018-07-04)
|
||||
|
||||
|
||||
|
||||
|
||||
**Note:** Version bump only for package @tarojs/transformer-wx
|
||||
|
||||
<a name="0.0.67-beta.0"></a>
|
||||
## [0.0.67-beta.0](https://github.com/NervJS/taro/compare/v0.0.66...v0.0.67-beta.0) (2018-07-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **transformer:** 循环自定义组件的 iterator 重命名 ([a9cf461](https://github.com/NervJS/taro/commit/a9cf461))
|
|
@ -0,0 +1,3 @@
|
|||
# @tarojs/transformer-wx
|
||||
|
||||
把 JSX 语法转换成可以在小程序运行的字符串模板。
|
|
@ -0,0 +1,160 @@
|
|||
import transform from '../src'
|
||||
import { buildComponent, baseCode, baseOptions } from './utils'
|
||||
import { INTERNAL_SAFE_GET, DEFAULT_Component_SET } from '../src/constant'
|
||||
|
||||
const projAppJS = `
|
||||
import { define, render, WeElement } from '../../src/omi'
|
||||
import './hello-element'
|
||||
|
||||
define('my-app', class extends WeElement {
|
||||
data = { abc: 'abc', passToChild: 123 }
|
||||
|
||||
install() {
|
||||
this.dd = { a: 1 }
|
||||
}
|
||||
|
||||
onMyEvent = evt => {
|
||||
this.data.abc = ' by ' + evt.detail.name
|
||||
this.data.passToChild = 1234
|
||||
this.dd.a++
|
||||
this.update()
|
||||
}
|
||||
|
||||
css() {
|
||||
return \`
|
||||
div{
|
||||
color: green;
|
||||
}\`
|
||||
}
|
||||
|
||||
render(props, data) {
|
||||
return (
|
||||
<div>
|
||||
Hello {props.name} {data.abc} {this.dd.a}
|
||||
<hello-element
|
||||
onMyEvent={this.onMyEvent}
|
||||
propFromParent={data.passToChild}
|
||||
dd={this.dd}
|
||||
msg="WeElement"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
`
|
||||
|
||||
describe('基本功能', () => {
|
||||
test('导出包', () => {
|
||||
expect(transform).not.toBe(undefined)
|
||||
})
|
||||
|
||||
test('projAppJS', () =>{
|
||||
const { code, ast, template } = transform({
|
||||
...baseOptions,
|
||||
code: projAppJS
|
||||
})
|
||||
|
||||
console.log(code)
|
||||
console.log(template)
|
||||
})
|
||||
|
||||
describe('基本转换', () => {
|
||||
const { code, ast, template } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(baseCode)
|
||||
})
|
||||
|
||||
test('转换结果不为空', () => {
|
||||
expect(code).not.toBeFalsy()
|
||||
expect(ast).not.toBeFalsy()
|
||||
expect(template).not.toBeFalsy()
|
||||
})
|
||||
|
||||
test('render() 方法会被去除', () => {
|
||||
expect(code.includes('render()')).toBeFalsy()
|
||||
expect(code.includes('render ()')).toBeFalsy()
|
||||
})
|
||||
|
||||
test('_createdData() 方法会一直存在', () => {
|
||||
expect(code.includes('_createdData()')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
test('支持 TypeScript', () => {
|
||||
expect(() => transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a: string = '';
|
||||
` + baseCode),
|
||||
isTyped: true
|
||||
})).not.toThrow()
|
||||
})
|
||||
|
||||
test('支持 Flow ', () => {
|
||||
expect(() => transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
function concat(a: string, b: string) {
|
||||
return a + b;
|
||||
}
|
||||
` + baseCode)
|
||||
})).not.toThrow()
|
||||
})
|
||||
|
||||
test.skip('支持 async/await', () => {
|
||||
const { code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(baseCode, `
|
||||
async f () {
|
||||
await ''
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
expect(
|
||||
code.trim().startsWith(`import "@tarojs/async-await";`)
|
||||
).toBeTruthy()
|
||||
})
|
||||
|
||||
test('支持 Redux Provider', () => {
|
||||
const { code, template } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<Provider store={test}>
|
||||
<View />
|
||||
</Provider>
|
||||
)`,
|
||||
'',
|
||||
`import { Provider, connect } from '@tarojs/redux';
|
||||
const test = {};`
|
||||
)
|
||||
})
|
||||
|
||||
expect(template.includes('Provider')).toBeFalsy()
|
||||
expect(code.includes('setStore(test);')).toBeTruthy()
|
||||
})
|
||||
|
||||
test('导入 internal 方法', () => {
|
||||
const { code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(baseCode)
|
||||
})
|
||||
|
||||
expect(code.includes(INTERNAL_SAFE_GET)).toBeTruthy()
|
||||
})
|
||||
|
||||
test('isApp 为 true 时只返回 ast', () => {
|
||||
const { code, ast, template } = transform({
|
||||
...baseOptions,
|
||||
isApp: true,
|
||||
code: buildComponent(baseCode)
|
||||
})
|
||||
|
||||
expect(ast).not.toBeUndefined()
|
||||
expect(code).toBeUndefined()
|
||||
expect(template).toBeUndefined()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,127 @@
|
|||
import transform from '../src'
|
||||
import { buildComponent, baseCode, baseOptions, evalClass, Custom } from './utils'
|
||||
import { isObject } from 'lodash'
|
||||
|
||||
describe('components', () => {
|
||||
test('components 一直存在并且是一个数组', () => {
|
||||
const { components } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(baseCode)
|
||||
})
|
||||
|
||||
expect(Array.isArray(components)).toBeTruthy()
|
||||
})
|
||||
|
||||
test('$components works', () => {
|
||||
const { components } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const { list } = this.state
|
||||
return (
|
||||
<Custom />
|
||||
)
|
||||
`, '', `import { Custom } from './utils'`)
|
||||
})
|
||||
|
||||
expect(components[0].name).toEqual('custom')
|
||||
})
|
||||
|
||||
describe('component results', () => {
|
||||
test('component results', () => {
|
||||
const { ast, components } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const { list } = this.state
|
||||
return (
|
||||
<Custom />
|
||||
)
|
||||
`, '', `import { Custom } from './utils'`)
|
||||
})
|
||||
|
||||
const component = components[0]
|
||||
expect(components.length).toBe(1)
|
||||
expect(component.name).toBe('custom')
|
||||
expect(component.path).toBe('./utils')
|
||||
})
|
||||
|
||||
test('component results 重复不会添加', () => {
|
||||
const { ast, components } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const { list } = this.state
|
||||
return (
|
||||
<View>
|
||||
<Custom />
|
||||
<Custom />
|
||||
</View>
|
||||
)
|
||||
`, '', `import { Custom } from './utils'`)
|
||||
})
|
||||
const component = components[0]
|
||||
expect(components.length).toBe(1)
|
||||
expect(component.name).toBe('custom')
|
||||
expect(component.path).toBe('./utils')
|
||||
})
|
||||
|
||||
test('component results 能在单层循环使用', () => {
|
||||
const { ast, components } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const { list } = this.state
|
||||
return (
|
||||
<View>
|
||||
{list.map(item => <Custom />)}
|
||||
</View>
|
||||
)
|
||||
`, '', `import { Custom } from './utils'`)
|
||||
})
|
||||
const component = components[0]
|
||||
expect(components.length).toBe(1)
|
||||
expect(component.name).toBe('custom')
|
||||
expect(component.path).toBe('./utils')
|
||||
})
|
||||
|
||||
test('component results 能在多层循环使用', () => {
|
||||
const { components, code, template } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const { list } = this.state
|
||||
return (
|
||||
<View>
|
||||
{list.map(item => {
|
||||
return (
|
||||
<View>
|
||||
{item.children.map(child => <Custom />)}
|
||||
</View>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)
|
||||
`, '', `import { Custom } from './utils'`)
|
||||
})
|
||||
const component = components[0]
|
||||
expect(components.length).toBe(1)
|
||||
expect(component.name).toBe('custom')
|
||||
expect(component.path).toBe('./utils')
|
||||
})
|
||||
})
|
||||
test('重复使用同一组件不会增加 $components', () => {
|
||||
const { components } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const { list } = this.state
|
||||
return (
|
||||
<View>
|
||||
<Custom />
|
||||
<Custom />
|
||||
</View>
|
||||
)
|
||||
`, '', `import { Custom } from './utils'`)
|
||||
})
|
||||
|
||||
const component = components[0]
|
||||
expect(components.length).toBe(1)
|
||||
expect(component.name).toBe('custom')
|
||||
expect(component.path).toBe('./utils')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,775 @@
|
|||
import transform from '../src'
|
||||
import { buildComponent, baseOptions, evalClass, prettyPrint } from './utils'
|
||||
|
||||
describe('if statement', () => {
|
||||
test('简单情况', () => {
|
||||
const { template, ast,code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const tasks = []
|
||||
if (tasks !== null) {
|
||||
return <View className='page-body' >
|
||||
</View>
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='page-body'>
|
||||
<Text>Hello world!</Text>
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<block wx:if=\"{{tasks !== null}}\">
|
||||
<view class=\"page-body\"></view>
|
||||
</block>
|
||||
<view class=\"page-body\" wx:else>
|
||||
<text>Hello world!</text>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('两个平级的 ifStatement', () => {
|
||||
const { template, ast } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const tasks = []
|
||||
if (tasks !== null) {
|
||||
return <View className='page-body' >
|
||||
</View>
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return <View className='page-body'>
|
||||
<Text>{tasks.length}</Text>
|
||||
</View>
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='page-body'>
|
||||
<Text>Hello world!</Text>
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<block wx:if=\"{{tasks !== null}}\">
|
||||
<view class=\"page-body\"></view>
|
||||
</block>
|
||||
<block wx:elif=\"{{tasks.length === 0}}\">
|
||||
<view class=\"page-body\">
|
||||
<text>{{tasks.length}}</text>
|
||||
</view>
|
||||
</block>
|
||||
<view class=\"page-body\" wx:else>
|
||||
<text>Hello world!</text>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('if 的 test 含有复杂表达式', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const tasks = []
|
||||
if (JSON.stringify(tasks) !== '[]') {
|
||||
return <View className='page-body' >
|
||||
</View>
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
const inst = evalClass(ast)
|
||||
expect(inst.state.anonymousState__temp).toBe(false)
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<block wx:if=\"{{anonymousState__temp}}\">
|
||||
<view class=\"page-body\"></view>
|
||||
</block>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('if 的 block 含有复杂表达式', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const tasks = []
|
||||
if (true) {
|
||||
return <View className={JSON.stringify(tasks)} >
|
||||
</View>
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<block wx:if="{{true}}">
|
||||
<view class="{{_$anonymousState__temp}}"></view>
|
||||
</block>
|
||||
</block>
|
||||
`))
|
||||
|
||||
const inst = evalClass(ast)
|
||||
expect(inst.state._$anonymousState__temp).toEqual('[]')
|
||||
expect(Object.keys(inst.state).length).toBe(1)
|
||||
})
|
||||
|
||||
test.skip('if-else', () => {
|
||||
const { template, ast } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const tasks = []
|
||||
const content = null
|
||||
if (tasks !== null) {
|
||||
content = <View className='page-body' >
|
||||
</View>
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
content = <View className='page-body'>
|
||||
<Text>{tasks.length}</Text>
|
||||
</View>
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='page-body'>
|
||||
{content}
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<block wx:if=\"{{tasks !== null}}\">
|
||||
<view class=\"page-body\"></view>
|
||||
</block>
|
||||
<block wx:elif=\"{{tasks.length === 0}}\">
|
||||
<view class=\"page-body\">
|
||||
<text>{{tasks.length}}</text>
|
||||
</view>
|
||||
</block>
|
||||
<view class=\"page-body\" wx:else>
|
||||
<text>Hello world!</text>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
})
|
||||
|
||||
describe('三元表达式', () => {
|
||||
|
||||
describe('consequet 为 JSX', () => {
|
||||
|
||||
test('alternate 为空字符串', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const { title } = this.props
|
||||
return (
|
||||
<View>
|
||||
{ title ? <Text>yes</Text> : '' }
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block wx:if="{{title}}">
|
||||
<text>yes</text>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('alternate 为字符串', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const { title } = this.props
|
||||
return (
|
||||
<View>
|
||||
{ title ? <Text>yes</Text> : 'no' }
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block>
|
||||
<block wx:if=\"{{title}}\">
|
||||
<text>yes</text>
|
||||
</block>
|
||||
<block wx:else>{{'no'}}</block>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('alternate 为 null', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const { title } = this.props
|
||||
return (
|
||||
<View>
|
||||
{ title ? <Text>yes</Text> : null }
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block wx:if="{{title}}">
|
||||
<text>yes</text>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('alternate 为 undefied', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const { title } = this.props
|
||||
return (
|
||||
<View>
|
||||
{ title ? <Text>yes</Text> : undefined }
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block wx:if="{{title}}">
|
||||
<text>yes</text>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('alternate 为 数字', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const { title } = this.props
|
||||
return (
|
||||
<View>
|
||||
{ title ? <Text>yes</Text> : 123 }
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block>
|
||||
<block wx:if=\"{{title}}\">
|
||||
<text>yes</text>
|
||||
</block>
|
||||
<block wx:else>{{123}}</block>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('alternate 为 数字 0', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const { title } = this.props
|
||||
return (
|
||||
<View>
|
||||
{ title ? <Text>yes</Text> : 0 }
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block wx:if="{{title}}">
|
||||
<text>yes</text>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('alternate 为变量', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const { title, test } = this.props
|
||||
return (
|
||||
<View>
|
||||
{ title ? <Text>yes</Text> : test }
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block>
|
||||
<block wx:if=\"{{title}}\">
|
||||
<text>yes</text>
|
||||
</block>
|
||||
<block wx:else>{{test}}</block>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('alternate 为函数', () => {
|
||||
const { template, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const { title, test } = this.props
|
||||
return (
|
||||
<View>
|
||||
{ title ? <Text>yes</Text> : escape(test) }
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block>
|
||||
<block wx:if=\"{{title}}\">
|
||||
<text>yes</text>
|
||||
</block>
|
||||
<block wx:else>{{anonymousState__temp}}</block>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe('inline 表达式', () => {
|
||||
describe('work with this.props.children', () => {
|
||||
test('|| 逻辑表达式', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const text = 'test'
|
||||
return (
|
||||
<View>
|
||||
{text || this.props.children}
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block>
|
||||
<block wx:if="{{text}}">{{text}}</block>
|
||||
<block wx:else>
|
||||
<slot></slot>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('三元表达式', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const text = 'test'
|
||||
return (
|
||||
<View>
|
||||
{text ? text : this.props.children}
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block>
|
||||
<block wx:if="{{text}}">{{text}}</block>
|
||||
<block wx:else>
|
||||
<slot></slot>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('逻辑非表达式', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const text = 'test'
|
||||
return (
|
||||
<View>
|
||||
{!text && this.props.children}
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block wx:if=\"{{!text}}\">
|
||||
<slot></slot>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('逻辑非表达式 2', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const text = 'test'
|
||||
return (
|
||||
<View>
|
||||
{!text && <Btn />}
|
||||
</View>
|
||||
)
|
||||
`, '', `import Btn from './btn'`)
|
||||
})
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block wx:if=\"{{!text}}\">
|
||||
<btn __triggerObserer=\"{{ _triggerObserer }}\"></btn>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
})
|
||||
describe('匿名 state 生成也需要带上表达式条件', () => {
|
||||
test('三元表达式', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const tasks = []
|
||||
return (
|
||||
tasks && tasks.length ? <View className={String('page')}>
|
||||
<Text>Hello world!</Text>
|
||||
</View> : null
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const inst = evalClass(ast)
|
||||
expect(inst.state.anonymousState__temp).toBe(null) // 默认设置为 null
|
||||
})
|
||||
|
||||
test('逻辑表达式', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const tasks = []
|
||||
return (
|
||||
tasks && tasks.length && <View className={String('Page')}>
|
||||
<Text>Hello world!</Text>
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const inst = evalClass(ast)
|
||||
expect(inst.state.anonymousState__temp).toBe(null) // 默认设置为 null
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('switch case', () => {
|
||||
test('switch 嵌套 if', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
let body;
|
||||
const type = {}
|
||||
switch (this.props.testType) {
|
||||
case type.direct: {
|
||||
if (type.d2) {
|
||||
body = (<View>1</View>)
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
{body}
|
||||
</View>
|
||||
);
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block wx:if=\"{{testType === type.direct}}\">
|
||||
<block wx:if=\"{{type.d2}}\">
|
||||
<view>1</view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('if 嵌套 switch', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
let body;
|
||||
const type = {}
|
||||
if (type.d2) {
|
||||
switch (this.props.testType) {
|
||||
case type.direct: {
|
||||
body = (<View>1</View>)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
{body}
|
||||
</View>
|
||||
);
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block wx:if=\"{{type.d2}}\">
|
||||
<block wx:if=\"{{testType === type.direct}}\">
|
||||
<view>1</view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('只有一个 case', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
let body;
|
||||
const type = {}
|
||||
switch (this.props.testType) {
|
||||
case type.direct: {
|
||||
body = (<View>1</View>)
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
{body}
|
||||
</View>
|
||||
);
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block>
|
||||
<block wx:if="{{testType === type.direct}}">
|
||||
<view>1</view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('多个 case', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
let body;
|
||||
const type = {}
|
||||
switch (this.props.testType) {
|
||||
case type.direct: {
|
||||
body = (<View>1</View>)
|
||||
break;
|
||||
}
|
||||
case type.d2: {
|
||||
body = (<View>2</View>)
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
{body}
|
||||
</View>
|
||||
);
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block>
|
||||
<block wx:if="{{testType === type.direct}}">
|
||||
<view>1</view>
|
||||
</block>
|
||||
<block wx:elif="{{testType === type.d2}}">
|
||||
<view>2</view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('有 default case', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
let body;
|
||||
const type = {}
|
||||
switch (this.props.testType) {
|
||||
case type.direct: {
|
||||
body = (<View>1</View>)
|
||||
break;
|
||||
}
|
||||
case type.d2: {
|
||||
body = (<View>2</View>)
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
body = (<View>default</View>)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
{body}
|
||||
</View>
|
||||
);
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block>
|
||||
<block wx:if="{{testType === type.direct}}">
|
||||
<view>1</view>
|
||||
</block>
|
||||
<block wx:elif="{{testType === type.d2}}">
|
||||
<view>2</view>
|
||||
</block>
|
||||
<view wx:else>default</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('case 的语句也需要执行', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
let body;
|
||||
const type = {}
|
||||
switch (type) {
|
||||
case this.props.direct: {
|
||||
body = (<View>1</View>)
|
||||
break;
|
||||
}
|
||||
case this.props.d2: {
|
||||
body = (<View>2</View>)
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
this.test = ''
|
||||
body = (<View>default</View>)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
{body}
|
||||
</View>
|
||||
);
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.test).toBe('')
|
||||
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<block>
|
||||
<block wx:if=\"{{type === direct}}\">
|
||||
<view>1</view>
|
||||
</block>
|
||||
<block wx:elif=\"{{type === d2}}\">
|
||||
<view>2</view>
|
||||
</block>
|
||||
<view wx:else>default</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
})
|
|
@ -0,0 +1,219 @@
|
|||
import transform from '../src'
|
||||
import {
|
||||
buildComponent,
|
||||
baseCode,
|
||||
baseOptions,
|
||||
evalClass,
|
||||
Custom,
|
||||
removeShadowData,
|
||||
prettyPrint
|
||||
} from './utils'
|
||||
|
||||
describe('event', () => {
|
||||
test('普通绑定', () => {
|
||||
const { template, ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View onClick={this.handleClick} />
|
||||
)
|
||||
`,
|
||||
'handleClick = () => ({})',
|
||||
`import { Custom } from './utils'`
|
||||
)
|
||||
})
|
||||
const instance = evalClass(ast)
|
||||
removeShadowData(instance.state)
|
||||
expect(instance.state).toEqual({})
|
||||
expect(instance.$$events).toEqual(['handleClick'])
|
||||
expect(template).toMatch(`bindtap="handleClick"`)
|
||||
})
|
||||
|
||||
test('bind 绑定', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View onClick={this.handleClick.bind(this)} />
|
||||
)
|
||||
`,
|
||||
'handleClick = () => ({})',
|
||||
`import { Custom } from './utils'`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
removeShadowData(instance.state)
|
||||
|
||||
expect(instance.state).toEqual({})
|
||||
expect(template).toMatch(`bindtap="handleClick"`)
|
||||
expect(instance.$$events).toEqual(['handleClick'])
|
||||
expect(template).toMatch(`data-e-tap-so="this"`)
|
||||
})
|
||||
|
||||
test('bind 绑定支持写数字', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View onClick={this.handleClick.bind(this, 666)} />
|
||||
)
|
||||
`,
|
||||
'handleClick = () => ({})',
|
||||
`import { Custom } from './utils'`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
removeShadowData(instance.state)
|
||||
expect(instance.state).toEqual({})
|
||||
expect(instance.$$events).toEqual(['handleClick'])
|
||||
expect(template).toMatch(`data-e-tap-a-a="{{666}}`)
|
||||
})
|
||||
|
||||
test('bind 绑定支持写数字 2', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View onClick={this.handleClick.bind(this, 666, 777)} />
|
||||
)
|
||||
`,
|
||||
'handleClick = () => ({})',
|
||||
`import { Custom } from './utils'`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
removeShadowData(instance.state)
|
||||
expect(instance.state).toEqual({})
|
||||
expect(instance.$$events).toEqual(['handleClick'])
|
||||
expect(template).toMatch(`data-e-tap-a-a="{{666}}`)
|
||||
expect(template).toMatch(`data-e-tap-a-b="{{777}}`)
|
||||
})
|
||||
|
||||
test('bind 绑定支持写字面量对象', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View onClick={this.handleClick.bind(this, { a: 1 })} />
|
||||
)
|
||||
`,
|
||||
'handleClick = () => ({})',
|
||||
`import { Custom } from './utils'`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
removeShadowData(instance.state)
|
||||
expect(instance.state).toEqual({ anonymousState__temp: { a: 1 } })
|
||||
expect(instance.$$events).toEqual(['handleClick'])
|
||||
expect(template).toMatch(
|
||||
`data-e-tap-a-a=\"{{anonymousState__temp}}`
|
||||
)
|
||||
// expect(template).toMatch(`data-e-handleClick-a-b="{{777}}`)
|
||||
})
|
||||
|
||||
describe('this.props.func', () => {
|
||||
test('简单情况', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View onClick={this.props.handleClick} />
|
||||
)
|
||||
`,
|
||||
'handleClick = () => ({})',
|
||||
`import { Custom } from './utils'`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
|
||||
expect(template).toMatch(`<view bindtap="funPrivate1"></view>`)
|
||||
expect(instance.$$events).toEqual(['funPrivate1'])
|
||||
})
|
||||
|
||||
test('相同的事件', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View>
|
||||
<Text onClick={this.props.handleClick} />
|
||||
<Text onClick={this.props.handleClick} />
|
||||
</View>
|
||||
)
|
||||
`,
|
||||
'handleClick = () => ({})',
|
||||
`import { Custom } from './utils'`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<text bindtap=\"funPrivate2\"></text>
|
||||
<text bindtap=\"funPrivate2\"></text>
|
||||
</view>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
expect(instance.$$events).toEqual(['funPrivate2'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('bind 函数参数含有复杂表达式, c95c8b27868cb7d7aa7c2ff10617876679b38086', () => {
|
||||
test('正常使用', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View onClick={this.handleClick.bind(this, escape('test'))} />
|
||||
)
|
||||
`,
|
||||
'handleClick = () => ({})',
|
||||
`import { Custom } from './utils'`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
|
||||
expect(Object.keys(instance.state).length).toBe(1)
|
||||
expect(instance.state.anonymousState__temp).toBe('test')
|
||||
})
|
||||
|
||||
test('循环中有复杂表达式', () => {
|
||||
expect(() => {
|
||||
transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
[].map(() => {
|
||||
return <View onClick={this.handleClick.bind(this, escape('test'))} />
|
||||
})
|
||||
)
|
||||
`,
|
||||
'handleClick = () => ({})',
|
||||
`import { Custom } from './utils'`
|
||||
)
|
||||
})
|
||||
}).toThrowError(
|
||||
/在循环中使用 bind 时,需要声明将此复杂表达式声明为一个变量再放入 bind 参数中。/
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,195 @@
|
|||
import transform from '../src'
|
||||
import { buildComponent, baseCode, baseOptions, evalClass, Custom, prettyPrint } from './utils'
|
||||
|
||||
describe('ref', () => {
|
||||
describe('正常使用', () => {
|
||||
test('字符串', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View ref='a' />
|
||||
)
|
||||
`)
|
||||
})
|
||||
// console.log(instance)
|
||||
const instance = evalClass(ast)
|
||||
const refs = instance.$$refs
|
||||
expect(refs[0].type).toBe('dom')
|
||||
expect(refs[0].refName).toBe('a')
|
||||
expect(refs[0].fn).toBe(null)
|
||||
expect(template).toMatch(/<view id="[a-zA-Z]{5}"><\/view>/)
|
||||
})
|
||||
|
||||
test('自定义组件', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<Custom ref='a' />
|
||||
)
|
||||
`)
|
||||
})
|
||||
// console.log(instance)
|
||||
const instance = evalClass(ast)
|
||||
const refs = instance.$$refs
|
||||
expect(refs[0].type).toBe('component')
|
||||
expect(refs[0].refName).toBe('a')
|
||||
expect(refs[0].fn).toBe(null)
|
||||
expect(template).toMatch(/<custom id="[a-zA-Z]{5}" __triggerObserer=\"{{ _triggerObserer }}\"><\/custom>/)
|
||||
})
|
||||
|
||||
test('字符串模板', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View ref={\`a\`} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
// console.log(instance)
|
||||
const instance = evalClass(ast)
|
||||
const refs = instance.$$refs
|
||||
expect(refs[0].type).toBe('dom')
|
||||
expect(refs[0].refName).toBe('a')
|
||||
expect(refs[0].fn).toBe(null)
|
||||
expect(template).toMatch(/<view id="[a-zA-Z]{5}"><\/view>/)
|
||||
})
|
||||
|
||||
test('inline 函数', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View ref={() => this.ref = ''} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
// console.log(instance)
|
||||
const instance = evalClass(ast)
|
||||
const refs = instance.$$refs
|
||||
expect(refs[0].type).toBe('dom')
|
||||
expect(refs[0].refName).toBe('')
|
||||
refs[0].fn()
|
||||
expect(instance.ref).toBe('')
|
||||
expect(template).toMatch(/<view id="[a-zA-Z]{5}"><\/view>/)
|
||||
})
|
||||
|
||||
test('函数', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View ref={this.mapView} />
|
||||
)
|
||||
`, `mapView = () => this.ref = ''`)
|
||||
})
|
||||
// console.log(instance)
|
||||
const instance = evalClass(ast)
|
||||
const refs = instance.$$refs
|
||||
expect(refs[0].type).toBe('dom')
|
||||
expect(refs[0].refName).toBe('')
|
||||
refs[0].fn()
|
||||
expect(instance.ref).toBe('')
|
||||
expect(template).toMatch(/<view id="[a-zA-Z]{5}"><\/view>/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('loop', () => {
|
||||
test('内置组件', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const array = [{ list: [] }, { list: [] }]
|
||||
return (
|
||||
<View>{array.map((item, index) => {
|
||||
return <CoverView ref={(node) => this.coverView[index] = node}>{item.list.map(item2 => <Text>{item2}</Text>)}</CoverView>
|
||||
})}</View>
|
||||
)
|
||||
`, `coverView = []`)
|
||||
})
|
||||
// console.log(instance)
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.coverView).toEqual(['test-ref', 'test-ref'])
|
||||
expect(template).toMatch(`<cover-view id="{{`)
|
||||
expect(template).toMatch(` + index}}"`)
|
||||
})
|
||||
|
||||
test('内置组件 + 其它复杂表达式', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const array = [{ list: [] }, { list: [] }]
|
||||
return (
|
||||
<View>{array.map((item, index) => {
|
||||
return <CoverView style={item} ref={(node) => this.coverView[index] = node}>{item.list.map(item2 => <Text>{item2}</Text>)}</CoverView>
|
||||
})}</View>
|
||||
)
|
||||
`, `coverView = []`)
|
||||
})
|
||||
// console.log(instance)
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.coverView).toEqual(['test-ref', 'test-ref'])
|
||||
})
|
||||
|
||||
test('自定义组件组件', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const array = [{ list: [] }, { list: [] }]
|
||||
return (
|
||||
<View>{array.map((item, index) => {
|
||||
return <Cover ref={(node) => this.coverView[index] = node}>{item.list.map(item2 => <Text>{item2}</Text>)}</Cover>
|
||||
})}</View>
|
||||
)
|
||||
`, `coverView = []`)
|
||||
})
|
||||
// console.log(instance)
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.coverView).toEqual(['test-component-ref', 'test-component-ref'])
|
||||
expect(template).toMatch(`<cover id="{{`)
|
||||
expect(template).toMatch(prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<cover id=\"{{item.$loopState__temp2}}\" __triggerObserer=\"{{ _triggerObserer }}\" wx:for=\"{{loopArray0}}\" wx:for-item=\"item\" wx:for-index=\"index\">
|
||||
<text wx:for=\"{{item.$original.list}}\" wx:for-item=\"item2\">{{item2}}</text>
|
||||
</cover>
|
||||
</view>
|
||||
</block>
|
||||
`))
|
||||
})
|
||||
|
||||
test('内置组件多重循环', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const array = [{ list: ['', ''] }, { list: [''] }]
|
||||
return (
|
||||
<View>{array.map((item, index) => {
|
||||
return <CoverView ref={(node) => this.coverView[index] = node}>
|
||||
{item.list.map((item2, idx) => <Text ref={(node) => this.text[index] = item.list}>{item2}</Text>)}
|
||||
</CoverView>
|
||||
})}</View>
|
||||
)
|
||||
`, `coverView = [];text = []`)
|
||||
})
|
||||
// console.log(instance)
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.coverView).toEqual(['test-ref', 'test-ref'])
|
||||
expect(instance.text).toEqual([['', ''], ['']])
|
||||
expect(template).toMatch(`<cover-view id="{{`)
|
||||
expect(template).toMatch(` + index}}"`)
|
||||
})
|
||||
|
||||
})
|
||||
})
|
|
@ -0,0 +1,824 @@
|
|||
import transform from '../src'
|
||||
import { buildComponent, baseCode, baseOptions, evalClass } from './utils'
|
||||
|
||||
function removeShadowData (obj: Object) {
|
||||
if (obj['__data']) {
|
||||
delete obj['__data']
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
describe('State', () => {
|
||||
describe('使用 object pattern 从 this 取 state', () => {
|
||||
test('只有一个 pattern', () => {
|
||||
const { ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { state } = this
|
||||
return (
|
||||
<View className={'icon-' + this.props.type}>测试 + {this.props.type}</View>
|
||||
)
|
||||
`,
|
||||
`state = { type: 'test' }`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.__state.type).toBe('test')
|
||||
expect(instance.state.type).toBe('test')
|
||||
expect(code).not.toMatch('const { state } = this')
|
||||
expect(code).toMatch(`const state = this.__state`)
|
||||
})
|
||||
|
||||
test('state 或 props 只能单独从 this 中解构', () => {
|
||||
expect(() =>
|
||||
transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { state, fuck } = this
|
||||
return (
|
||||
<View className={'icon-' + this.props.type}>测试 + {this.type}</View>
|
||||
)
|
||||
`,
|
||||
`state = { type: 'test' }`
|
||||
)
|
||||
})
|
||||
).toThrowError(/state 或 props 只能单独从 this 中解构/)
|
||||
})
|
||||
|
||||
test('可以使用 state 关键字作为 state', () => {
|
||||
const { ast, code, template } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { state } = this.state.task
|
||||
return (
|
||||
<View>{state}</View>
|
||||
)
|
||||
`,
|
||||
`state = { task: { state: null } }`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.state.state).toBe(null)
|
||||
expect(instance.$usedState.includes('state')).toBe(true)
|
||||
})
|
||||
|
||||
test('可以使用 props 关键字作为 state', () => {
|
||||
const { ast, code, template } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { props } = this.state.task
|
||||
return (
|
||||
<View>{state}</View>
|
||||
)
|
||||
`,
|
||||
`state = { task: { props: null } }`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.state.props).toBe(null)
|
||||
expect(instance.$usedState.includes('props')).toBe(true)
|
||||
})
|
||||
|
||||
test('可以使用 style', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View style={'width:' + this.state.rate + 'px;'}>
|
||||
<View />
|
||||
</View>
|
||||
)`,
|
||||
`state = { rate: 5 }`
|
||||
)
|
||||
})
|
||||
|
||||
expect(template).toMatch(`<view style="{{'width:' + rate + 'px;'}}">`)
|
||||
})
|
||||
|
||||
test.skip('可以使用 template style', () => {
|
||||
const { template, ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const rate = 5;
|
||||
return (
|
||||
<View style={\`width: \$\{rate\}px;\`}>
|
||||
<View />
|
||||
</View>
|
||||
)`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
|
||||
expect(instance.state.anonymousState__temp).toBe(undefined)
|
||||
expect(template).toMatch(`<view style=\"{{'width: ' + rate + 'px;'}}\">`)
|
||||
})
|
||||
|
||||
test('可以使用array of object', () => {
|
||||
const { template, ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const rate = 5;
|
||||
return (
|
||||
<View test={[{ a: 1 }]}>
|
||||
<View />
|
||||
</View>
|
||||
)`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
|
||||
expect(instance.state.anonymousState__temp).toEqual([{ a: 1 }])
|
||||
expect(template).toMatch(`<view test="{{anonymousState__temp}}">`)
|
||||
})
|
||||
|
||||
// state 和 props 需要单独解构
|
||||
test.skip('多个 pattern', () => {
|
||||
const { ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { state, props } = this
|
||||
return (
|
||||
<View className={'icon-' + this.props.type}>测试 + {this.props.type}</View>
|
||||
)
|
||||
`,
|
||||
`state = { type: 'test' }`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.__state.type).toBe('test')
|
||||
expect(instance.state.type).toBe('test')
|
||||
expect(code).not.toMatch('const { state } = this')
|
||||
expect(code).toMatch(`const { props } = this`)
|
||||
expect(code).toMatch(`const state = this.__state`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('可以从 this 中取值', () => {
|
||||
test('直接写 this.xxx', () => {
|
||||
const { ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View>{this.list}</View>
|
||||
)
|
||||
`,
|
||||
`list = ['a']`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.state.list).toEqual(['a'])
|
||||
})
|
||||
|
||||
test('从 this 解构出来出来的变量不会重复, 00269d4f55c21d5f8531ae2b6f70203f690ffa09', () => {
|
||||
const { ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View class={this.list}>{this.list}</View>
|
||||
)
|
||||
`,
|
||||
`list = ['a']`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.state.list).toEqual(['a'])
|
||||
})
|
||||
|
||||
test('从 this 解构出来出来的变量不得与 render 作用域定义的变量重复 derived from this', () => {
|
||||
expect(() => {
|
||||
transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { list } = this
|
||||
return (
|
||||
<View class={list}>{this.list}</View>
|
||||
)
|
||||
`,
|
||||
`list = ['a']`
|
||||
)
|
||||
})
|
||||
}).toThrowError(/此变量声明与/)
|
||||
})
|
||||
|
||||
test('从 this 解构出来出来的变量不得与 render 作用域定义的变量重复 derived from state', () => {
|
||||
expect(() => {
|
||||
transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { list } = this.state
|
||||
return (
|
||||
<View class={list}>{this.list}</View>
|
||||
)
|
||||
`,
|
||||
`list = ['a']`
|
||||
)
|
||||
})
|
||||
}).toThrowError(/此变量声明与/)
|
||||
})
|
||||
|
||||
test('从 this 解构出来出来的变量不得与 render 作用域定义的变量重复 derived from props ', () => {
|
||||
expect(() => {
|
||||
transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { list } = this.props
|
||||
return (
|
||||
<View class={list}>{this.list}</View>
|
||||
)
|
||||
`,
|
||||
`list = ['a']`
|
||||
)
|
||||
})
|
||||
}).toThrowError(/此变量声明与/)
|
||||
})
|
||||
|
||||
test('从 this 解构出来出来的变量不得与 render 作用域定义的变量重复 const decl ', () => {
|
||||
expect(() => {
|
||||
transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const list = []
|
||||
return (
|
||||
<View class={list}>{this.list}</View>
|
||||
)
|
||||
`,
|
||||
`list = ['a']`
|
||||
)
|
||||
})
|
||||
}).toThrowError(/此变量声明与/)
|
||||
})
|
||||
|
||||
test('可以写成员表达式', () => {
|
||||
const { ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View>{this.list.length}</View>
|
||||
)
|
||||
`,
|
||||
`list = ['a']`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.state.list).toEqual(['a'])
|
||||
})
|
||||
|
||||
test('可以从 this 中解构', () => {
|
||||
const { ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { list } = this
|
||||
return (
|
||||
<View>{list}</View>
|
||||
)
|
||||
`,
|
||||
`list = ['a']`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.state.list).toEqual(['a'])
|
||||
})
|
||||
|
||||
test('可以从 this 中解构之后使用成员表达式', () => {
|
||||
const { ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { list } = this
|
||||
return (
|
||||
<View>{list.length}</View>
|
||||
)
|
||||
`,
|
||||
`list = ['a']`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.state.list).toEqual(['a'])
|
||||
})
|
||||
|
||||
test('不解构', () => {
|
||||
const { ast, code } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const list = this.list
|
||||
return (
|
||||
<View>{list.length}</View>
|
||||
)
|
||||
`,
|
||||
`list = ['a']`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.state.list).toEqual(['a'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('$usedState', () => {
|
||||
test('$usedState 一直存在并且是一个 array', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(baseCode)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(Array.isArray(instance.$usedState)).toBeTruthy()
|
||||
})
|
||||
|
||||
test('没有被定义也会被加上', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`return <View />`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(Array.isArray(instance.$usedState)).toBeTruthy()
|
||||
})
|
||||
|
||||
test('直接从 this.state 可以引用', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(baseCode)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.$usedState[0]).toBe('list')
|
||||
})
|
||||
|
||||
test('this.props', () => {
|
||||
const { ast, code, template } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View test={this.props.a === this.props.b} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.$usedState).toEqual(['a', 'b'])
|
||||
expect(instance.state).toEqual({})
|
||||
})
|
||||
|
||||
test('props', () => {
|
||||
const { ast, code, template } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const { a, b } = this.props
|
||||
return (
|
||||
<View test={a === b} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.$usedState).toEqual(['a', 'b'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('createData()', () => {
|
||||
test('没有定义即便使用也不会存在', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`return <View />`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({})
|
||||
})
|
||||
|
||||
test('可以从 this.state 中使用', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(baseCode, `state = { list: [] }`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({ list: [] })
|
||||
})
|
||||
|
||||
test('可以从 变量定义中 中使用', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const list = this.state.list
|
||||
return (
|
||||
<View className='index'>
|
||||
<View className='title'>{this.state.title}</View>
|
||||
<View className='content'>
|
||||
{list.map(item => {
|
||||
return (
|
||||
<View className='item'>{item}</View>
|
||||
)
|
||||
})}
|
||||
<Button className='add' onClick={this.add}>添加</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
`,
|
||||
`state = { list: [] }`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({ list: [] })
|
||||
})
|
||||
|
||||
test('可以从 Object pattern 定义中使用', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { list } = this.state
|
||||
return (
|
||||
<View className='index'>
|
||||
<View className='title'>{this.state.title}</View>
|
||||
<View className='content'>
|
||||
{list.map(item => {
|
||||
return (
|
||||
<View className='item'>{item}</View>
|
||||
)
|
||||
})}
|
||||
<Button className='add' onClick={this.add}>添加</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
`,
|
||||
`state = { list: [] }`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({ list: [] })
|
||||
})
|
||||
|
||||
test('map 的 callee 也需要加入 state', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { list } = this.state
|
||||
const ary = list.filter(Boolean)
|
||||
return (
|
||||
<View className='index'>
|
||||
<View className='title'>{this.state.title}</View>
|
||||
<View className='content'>
|
||||
{ary.map(item => {
|
||||
return (
|
||||
<View className='item'>{item}</View>
|
||||
)
|
||||
})}
|
||||
<Button className='add' onClick={this.add}>添加</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
`,
|
||||
`state = { list: [] }`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
list: [],
|
||||
ary: []
|
||||
})
|
||||
})
|
||||
|
||||
test('成员表达式的 map callee 也需要加入 state', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { obj } = this.state
|
||||
const ary = obj.list
|
||||
return (
|
||||
<View className='index'>
|
||||
<View className='title'>{this.state.title}</View>
|
||||
<View className='content'>
|
||||
{ary.map(item => {
|
||||
return (
|
||||
<View className='item'>{item}</View>
|
||||
)
|
||||
})}
|
||||
<Button className='add' onClick={this.add}>添加</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
`,
|
||||
`state = { obj: { list: [] } }`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
obj: { list: [] },
|
||||
ary: []
|
||||
})
|
||||
})
|
||||
|
||||
describe('自定义组件', () => {
|
||||
describe('Identifier', () => {
|
||||
test('逻辑表达式', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = true
|
||||
const b = ''
|
||||
return <Custom test={a && b} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: true,
|
||||
b: ''
|
||||
})
|
||||
})
|
||||
|
||||
test('条件表达式', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = true
|
||||
const b = ''
|
||||
const c = ''
|
||||
return <Custom test={a ? b : c} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: true,
|
||||
b: '',
|
||||
c: ''
|
||||
})
|
||||
})
|
||||
|
||||
test('作用域有值', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = true
|
||||
return <Custom test={a} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({ a: true })
|
||||
})
|
||||
|
||||
test('作用域有值但没用到', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = true
|
||||
const b = ''
|
||||
const c = ''
|
||||
return <Custom test={a} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({ a: true })
|
||||
})
|
||||
})
|
||||
|
||||
describe('JSXAttribute', () => {
|
||||
test('支持成员表达式', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = { a: '' }
|
||||
return <Custom test={a.a} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: { a: '' }
|
||||
})
|
||||
})
|
||||
|
||||
test('三元表达式支持成员表达式', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = { a: '' }
|
||||
const b = { b: '' }
|
||||
const c = { c: '' }
|
||||
return <Custom test={a.a ? b.b : c.c} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: { a: '' },
|
||||
b: { b: '' },
|
||||
c: { c: '' }
|
||||
})
|
||||
})
|
||||
|
||||
test('逻辑表达式支持成员表达式', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = { a: '' }
|
||||
const b = { b: '' }
|
||||
return <Custom test={a.a && b.b} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: { a: '' },
|
||||
b: { b: '' }
|
||||
})
|
||||
})
|
||||
|
||||
test('逻辑表达式支持成员表达式 2', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = { a: '' }
|
||||
const b = { b: '' }
|
||||
return <Custom test={a.a || b.b} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: { a: '' },
|
||||
b: { b: '' }
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('自定义组件', () => {
|
||||
describe('Identifier', () => {
|
||||
test('逻辑表达式', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = true
|
||||
const b = ''
|
||||
return <View test={a && b} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: true,
|
||||
b: ''
|
||||
})
|
||||
})
|
||||
|
||||
test('条件表达式', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = true
|
||||
const b = ''
|
||||
const c = ''
|
||||
return <View test={a ? b : c} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: true,
|
||||
b: '',
|
||||
c: ''
|
||||
})
|
||||
})
|
||||
|
||||
test('作用域有值', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = true
|
||||
return <Custom test={a} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({ a: true })
|
||||
})
|
||||
|
||||
test('作用域有值但没用到', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = true
|
||||
const b = ''
|
||||
const c = ''
|
||||
return <View test={a} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({ a: true })
|
||||
})
|
||||
})
|
||||
|
||||
describe('JSXAttribute', () => {
|
||||
test('支持成员表达式', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = { a: '' }
|
||||
return <View test={a.a} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: { a: '' }
|
||||
})
|
||||
})
|
||||
|
||||
test('三元表达式支持成员表达式', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = { a: '' }
|
||||
const b = { b: '' }
|
||||
const c = { c: '' }
|
||||
return <View test={a.a ? b.b : c.c} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: { a: '' },
|
||||
b: { b: '' },
|
||||
c: { c: '' }
|
||||
})
|
||||
})
|
||||
|
||||
test('逻辑表达式支持成员表达式', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = { a: '' }
|
||||
const b = { b: '' }
|
||||
return <View test={a.a && b.b} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: { a: '' },
|
||||
b: { b: '' }
|
||||
})
|
||||
})
|
||||
|
||||
test('逻辑表达式支持成员表达式 2', () => {
|
||||
const { ast } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(`
|
||||
const a = { a: '' }
|
||||
const b = { b: '' }
|
||||
return <View test={a.a || b.b} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(removeShadowData(instance._createData())).toEqual({
|
||||
a: { a: '' },
|
||||
b: { b: '' }
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,911 @@
|
|||
import transform from '../src'
|
||||
import {
|
||||
buildComponent,
|
||||
baseOptions,
|
||||
evalClass,
|
||||
removeShadowData,
|
||||
prettyPrint
|
||||
} from './utils'
|
||||
|
||||
describe('Template', () => {
|
||||
describe('inline style', () => {
|
||||
test('简单情况', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View style={{ color: 'red' }} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const inst = evalClass(ast, '', true)
|
||||
|
||||
expect(template).toMatch(
|
||||
`<view style="{{anonymousState__temp}}"></view>`
|
||||
)
|
||||
expect(inst.state['anonymousState__temp']).toMatch(`color:red`)
|
||||
})
|
||||
|
||||
test('key 有 - 符号', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View style={{ 'fontSize': '16px' }} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const inst = evalClass(ast, '', true)
|
||||
|
||||
expect(template).toMatch(
|
||||
`<view style="{{anonymousState__temp}}"></view>`
|
||||
)
|
||||
expect(inst.state['anonymousState__temp']).toMatch(`font-size:16px`)
|
||||
})
|
||||
|
||||
test('多个对象', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View style={{ 'fontSize': '16px', color: 'red' }} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const inst = evalClass(ast, '', true)
|
||||
|
||||
expect(template).toMatch(
|
||||
`<view style="{{anonymousState__temp}}"></view>`
|
||||
)
|
||||
expect(inst.state['anonymousState__temp']).toMatch(
|
||||
`font-size:16px;color:red`
|
||||
)
|
||||
})
|
||||
|
||||
test('不转换字符串', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View style={'color: red'} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const inst = evalClass(ast, '', true)
|
||||
removeShadowData(inst.state)
|
||||
expect(inst.state).toEqual({})
|
||||
expect(template).toMatch(`<view style=\"color: red\"></view>`)
|
||||
})
|
||||
|
||||
test('不转换字符串 literal', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View style='color: red' />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const inst = evalClass(ast, '', true)
|
||||
removeShadowData(inst.state)
|
||||
expect(inst.state).toEqual({})
|
||||
expect(template).toMatch(`<view style=\"color: red\"></view>`)
|
||||
})
|
||||
|
||||
test('不转换字符串想加', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View style={'color:' + 'red'} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const inst = evalClass(ast, '', true)
|
||||
removeShadowData(inst.state)
|
||||
expect(inst.state).toEqual({})
|
||||
expect(template).toMatch(`<view style=\"{{'color:' + 'red'}}\"></view>`)
|
||||
})
|
||||
|
||||
test('转换变量', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const style = 'color:' + 'red'
|
||||
return (
|
||||
<View style={style} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const inst = evalClass(ast, '', true)
|
||||
removeShadowData(inst.state)
|
||||
expect(Object.keys(inst.state).length).toEqual(1)
|
||||
expect(template).toMatch(
|
||||
`<view style="{{anonymousState__temp}}"></view>`
|
||||
)
|
||||
expect(inst.state['anonymousState__temp']).toMatch(`color:red`)
|
||||
})
|
||||
|
||||
test('不转换自定义组件', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const style = 'color:' + 'red'
|
||||
return (
|
||||
<Test test={style} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const inst = evalClass(ast, '', true)
|
||||
removeShadowData(inst.state)
|
||||
expect(Object.keys(inst.state).length).toEqual(1)
|
||||
expect(template).toMatch(
|
||||
`<test test=\"{{style}}\" __triggerObserer=\"{{ _triggerObserer }}\"></test>`
|
||||
)
|
||||
expect(inst.state.style).toEqual('color:' + 'red')
|
||||
})
|
||||
|
||||
test('能在循环中使用, 无 return', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const array = ['test1', 'test2', 'test3']
|
||||
return (
|
||||
<View>{array.map(item => <View style={{ 'fontSize': '16px', color: 'red' }} />)}</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast, '', true)
|
||||
removeShadowData(instance.state)
|
||||
|
||||
expect(template).toMatch(
|
||||
`<view style="{{item.$loopState__temp2}}" wx:for="{{loopArray0}}" wx:for-item="item"></view>`
|
||||
)
|
||||
const styles = instance.state.loopArray0.map(i => i.$loopState__temp2)
|
||||
expect(styles[0]).toBe('font-size:16px;color:red')
|
||||
expect(styles[1]).toBe('font-size:16px;color:red')
|
||||
})
|
||||
|
||||
test('能在循环中使用, 有 return', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const array = ['test1', 'test2', 'test3']
|
||||
return (
|
||||
<View>{array.map(item => {
|
||||
return <View style={{ 'fontSize': '16px', color: 'red' }} />
|
||||
})}</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast, '', true)
|
||||
removeShadowData(instance.state)
|
||||
|
||||
expect(template).toMatch(
|
||||
`<view style="{{item.$loopState__temp2}}" wx:for="{{loopArray0}}" wx:for-item="item"></view>`
|
||||
)
|
||||
const styles = instance.state.loopArray0.map(i => i.$loopState__temp2)
|
||||
expect(styles[0]).toBe('font-size:16px;color:red')
|
||||
expect(styles[1]).toBe('font-size:16px;color:red')
|
||||
})
|
||||
|
||||
test('能在多层循环中使用', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const array = [{ list: [{}] }]
|
||||
return (
|
||||
<View>{array.map(item => {
|
||||
return <View style={{ 'fontSize': '12px', color: 'red' }}>
|
||||
{item.list.map(l => <Image style={{ 'fontSize': '16px', color: 'green' }} />)}
|
||||
</View>
|
||||
})}</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast, '', true)
|
||||
removeShadowData(instance.state)
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<view style="{{item.$loopState__temp2}}" wx:for="{{loopArray0}}" wx:for-item="item">
|
||||
<image style="{{l.$loopState__temp4}}" wx:for="{{item.$anonymousCallee__0}}" wx:for-item="l"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
|
||||
expect(Object.keys(instance.state).length).toBeLessThanOrEqual(2)
|
||||
expect(instance.state.loopArray0[0].$loopState__temp2).toMatch(
|
||||
`font-size:12px;color:red`
|
||||
)
|
||||
expect(
|
||||
instance.state.loopArray0[0].$anonymousCallee__0[0].$loopState__temp4
|
||||
).toMatch(`font-size:16px;color:green`)
|
||||
})
|
||||
|
||||
test('能在多层循环中使用 2', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const array = [{ list: [{}] }]
|
||||
let a2 = ['test1', 'test2', 'test3']
|
||||
return (
|
||||
<View>{array.map(item => {
|
||||
return <View style={{ 'fontSize': '12px', color: 'red' }}>
|
||||
{item.list.map(l => <Image style={{ 'fontSize': '16px', color: 'green' }} />)}
|
||||
{a2.map(a => <View style={{ 'fontSize': '20px', color: 'yellow' }} />)}
|
||||
</View>
|
||||
})}</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast, '', true)
|
||||
removeShadowData(instance.state)
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<view>
|
||||
<view style="{{item.$loopState__temp2}}" wx:for="{{loopArray0}}" wx:for-item="item">
|
||||
<image style="{{l.$loopState__temp4}}" wx:for="{{item.$anonymousCallee__0}}" wx:for-item="l"
|
||||
/>
|
||||
<view style="{{a.$loopState__temp6}}" wx:for="{{item.$anonymousCallee__1}}" wx:for-item="a"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
|
||||
expect(Object.keys(instance.state).length).toBeLessThanOrEqual(3)
|
||||
expect(instance.state.loopArray0[0].$loopState__temp2).toMatch(
|
||||
`font-size:12px;color:red`
|
||||
)
|
||||
expect(
|
||||
instance.state.loopArray0[0].$anonymousCallee__0[0].$loopState__temp4
|
||||
).toMatch(`font-size:16px;color:green`)
|
||||
expect(
|
||||
instance.state.loopArray0[0].$anonymousCallee__1[0].$loopState__temp6
|
||||
).toMatch(`font-size:20px;color:yellow`)
|
||||
})
|
||||
})
|
||||
|
||||
test('暂不支持 JSX 成员表达式', () => {
|
||||
expect(() => {
|
||||
transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <View.A />
|
||||
`)
|
||||
})
|
||||
}).toThrow()
|
||||
})
|
||||
|
||||
describe('使用 [] 获取成员表达式', () => {
|
||||
test('可以直接使用 this.state ', () => {
|
||||
const { template, ast } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View>{this.state.list[this.state.index]}</View>
|
||||
)
|
||||
`,
|
||||
`state = {
|
||||
list:['a','b','c'],
|
||||
index:0
|
||||
}`
|
||||
)
|
||||
})
|
||||
expect(template).toMatch('anonymousState__temp')
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(instance.state.anonymousState__temp).toBe('a')
|
||||
})
|
||||
|
||||
test('可以使用 props ', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(
|
||||
`
|
||||
return (
|
||||
<View>{this.state.list[this.props.index]}</View>
|
||||
)
|
||||
`,
|
||||
`state = {
|
||||
list:['a','b','c'],
|
||||
index:0
|
||||
}`
|
||||
)
|
||||
})
|
||||
|
||||
expect(template).toMatch('anonymousState__temp')
|
||||
})
|
||||
|
||||
test('使用标识符', () => {
|
||||
const { template, code, ast } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { list, index } = this.state
|
||||
return (
|
||||
<View>{list[index]}</View>
|
||||
)
|
||||
`,
|
||||
`state = {
|
||||
list:['a','b','c'],
|
||||
index:0
|
||||
}; static defaultProps = { index: 0 }`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
expect(template).not.toMatch('anonymousState__temp')
|
||||
expect(instance.$usedState).toEqual(['list', 'index'])
|
||||
})
|
||||
})
|
||||
|
||||
test('不支持 spread 表达式', () => {
|
||||
expect(() => {
|
||||
transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <View {...this.props.a} />
|
||||
`)
|
||||
})
|
||||
}).toThrow()
|
||||
})
|
||||
|
||||
describe('大小写', () => {
|
||||
test('单驼峰内置组件', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <View />
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch('<view></view>')
|
||||
})
|
||||
|
||||
test('双驼峰内置组件', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <ScrollView />
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch('<scroll-view></scroll-view>')
|
||||
})
|
||||
|
||||
test('className 变为 class', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <ScrollView className='a' />
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch('<scroll-view class="a"></scroll-view>')
|
||||
})
|
||||
|
||||
test('expression 有多个 this.props.xx 成员表达式', () => {
|
||||
const { template, code, ast } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <ScrollView className={this.props.iconList && this.props.iconList.length > 3 ? 'iconlist_wrap' : 'iconlist_wrap wrap-less'} />
|
||||
`)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
|
||||
expect(instance.$usedState).toEqual(['iconList'])
|
||||
|
||||
expect(template).toMatch(
|
||||
`<scroll-view class=\"{{iconList && iconList.length > 3 ? 'iconlist_wrap' : 'iconlist_wrap wrap-less'}}\"></scroll-view>`
|
||||
)
|
||||
})
|
||||
|
||||
describe('props 为布尔值', () => {
|
||||
test('内置组件', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <ScrollView hidden />
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
'<scroll-view hidden="{{true}}"></scroll-view>'
|
||||
)
|
||||
})
|
||||
|
||||
test('直接写值', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <ScrollView hidden={true} />
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
'<scroll-view hidden="{{true}}"></scroll-view>'
|
||||
)
|
||||
})
|
||||
|
||||
test('内置组件 + 特殊 props', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <ScrollView scrollX />
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
'<scroll-view scroll-x="{{true}}"></scroll-view>'
|
||||
)
|
||||
})
|
||||
|
||||
test('内置组件 + 特殊 props + 直接写值', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <ScrollView scrollX={true} />
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
'<scroll-view scroll-x="{{true}}"></scroll-view>'
|
||||
)
|
||||
})
|
||||
|
||||
test('内置组件 2', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <View hidden />
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch('<view hidden="{{true}}"></view>')
|
||||
})
|
||||
|
||||
test('自定义组件不写值', () => {
|
||||
const { template, code, ast } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(
|
||||
`
|
||||
return <Custom hidden />
|
||||
`,
|
||||
``,
|
||||
`import { Custom } from './utils'`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
// const props = instance.$props.Custom()
|
||||
// expect(props.$name).toBe('Custom')
|
||||
// expect(props.hidden).toBe(true)
|
||||
expect(template).toMatch(
|
||||
`<custom hidden=\"{{true}}\" __triggerObserer=\"{{ _triggerObserer }}\"></custom>`
|
||||
)
|
||||
})
|
||||
|
||||
test('自定义组件循环', () => {
|
||||
const { template, code, ast } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(
|
||||
`
|
||||
const array = [1, 2, 3]
|
||||
return (
|
||||
<View>
|
||||
{array.map(a1 => <Custom />)}
|
||||
</View>
|
||||
)
|
||||
`,
|
||||
``,
|
||||
`import { Custom } from './utils'`
|
||||
)
|
||||
})
|
||||
|
||||
const instance = evalClass(ast)
|
||||
// const props = instance.$props.Custom()
|
||||
// expect(props.$name).toBe('Custom')
|
||||
// expect(props.hidden).toBe(true)
|
||||
expect(template).toMatch(
|
||||
`<custom wx:for=\"{{array}}\" __triggerObserer=\"{{ _triggerObserer }}\" wx:for-item=\"a1\"></custom>`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test('驼峰式应该变为下划线式', () => {
|
||||
const { template, ast } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return <View hoverClass='test' />
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch('<view hover-class="test"></view>')
|
||||
})
|
||||
|
||||
describe('JSX 元素引用', () => {
|
||||
test('逻辑表达式破坏引用', () => {
|
||||
const { template, ast } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
const numbers =[...Array(10).keys()]
|
||||
const listItems = numbers.map((number) => {
|
||||
return <View key={number}><Text class='li' >我是第{number+1}个数字</Text></View>
|
||||
})
|
||||
return (
|
||||
<View className='container'>
|
||||
{listItems}
|
||||
<View>
|
||||
{this.state.enable && listItems}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<view class=\"container\">
|
||||
<view wx:key=\"number\" wx:for=\"{{numbers}}\" wx:for-item=\"number\">
|
||||
<text class=\"li\">我是第{{number + 1}}个数字</text>
|
||||
</view>
|
||||
<view>
|
||||
<block wx:if=\"{{enable}}\">
|
||||
<view wx:key=\"number\" wx:for=\"{{numbers}}\" wx:for-item=\"number\">
|
||||
<text class=\"li\">我是第{{number + 1}}个数字</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test('第三方组件事件首字母小写', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { list } = this.state
|
||||
return (
|
||||
<ec-chart onChange={this.handleChange} />
|
||||
)
|
||||
`,
|
||||
`config = { usingComponents: { 'ec-chart': '../path' } }`
|
||||
)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<ec-chart bindchange="handleChange" __triggerObserer="{{ _triggerObserer }}"></ec-chart>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
})
|
||||
|
||||
test('第三方组件事件首字母小写 2', () => {
|
||||
const { template } = transform({
|
||||
...baseOptions,
|
||||
code: buildComponent(
|
||||
`
|
||||
const { list } = this.state
|
||||
return (
|
||||
<ec-chart onchange={this.handleChange} />
|
||||
)
|
||||
`,
|
||||
`config = { usingComponents: { 'ec-chart': '../path' } }`
|
||||
)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<ec-chart bindchange="handleChange" __triggerObserer="{{ _triggerObserer }}"></ec-chart>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
})
|
||||
|
||||
// test('本来是下划线不用再转', () => {
|
||||
// const { template } = transform({
|
||||
// ...baseOptions,
|
||||
// isRoot: true,
|
||||
// code: buildComponent(`
|
||||
// return <View className='index'>
|
||||
// { [0, 1, 2, 3].map(i => <Text key={i}>{ i }</Text>) }
|
||||
// </View>
|
||||
// `)
|
||||
// })
|
||||
|
||||
// // expect(template).toMatch('<view data-id="1"></view>')
|
||||
// })
|
||||
})
|
||||
})
|
||||
|
||||
describe('字符不转义', () => {
|
||||
test('在 jsx attr 中', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View className={'中文' + '测试'} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<view class="{{'中文' + '测试'}}"></view>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
})
|
||||
|
||||
test('在 jsx children 中', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View>中文 测试</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<view>中文 测试</view>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
})
|
||||
|
||||
test('在 jsx children 中使用 jsx expression container', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View>{ '中文' + '测试' }</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<view>{{'中文' + '测试'}}</view>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
})
|
||||
|
||||
describe('void component', () => {
|
||||
test('input', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<Input></Input>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<input/>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
})
|
||||
|
||||
test('image', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<Image></Image>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<image/>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
})
|
||||
|
||||
test('import', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<Import></Import>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<import/>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
})
|
||||
|
||||
test('link', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<Link />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<link __triggerObserer=\"{{ _triggerObserer }}\"></link>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
})
|
||||
|
||||
test('同一个作用域的JSX 变量延时赋值没有意义', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
let a;
|
||||
a = <Text />
|
||||
return (
|
||||
<View>{a}</View>
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
expect(template).toMatch(
|
||||
prettyPrint(`
|
||||
<block>
|
||||
<view><text></text></view>
|
||||
</block>
|
||||
`)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('复杂表达式', () => {
|
||||
test('array of array', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View test={[{}]} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
let inst = evalClass(ast)
|
||||
expect(Object.keys(inst.state).length).toBe(1)
|
||||
expect(inst.state.anonymousState__temp).toEqual([{}])
|
||||
})
|
||||
|
||||
test('array of array', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View test={[[]]} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
let inst = evalClass(ast)
|
||||
expect(Object.keys(inst.state).length).toBe(1)
|
||||
expect(inst.state.anonymousState__temp).toEqual([[]])
|
||||
})
|
||||
|
||||
test('function', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View test={escape('')} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
let inst = evalClass(ast)
|
||||
expect(Object.keys(inst.state).length).toBe(1)
|
||||
expect(inst.state.anonymousState__temp).toEqual('')
|
||||
})
|
||||
|
||||
test('function', () => {
|
||||
const { template, ast, code } = transform({
|
||||
...baseOptions,
|
||||
isRoot: true,
|
||||
code: buildComponent(`
|
||||
return (
|
||||
<View test={escape('')} />
|
||||
)
|
||||
`)
|
||||
})
|
||||
|
||||
let inst = evalClass(ast)
|
||||
expect(Object.keys(inst.state).length).toBe(1)
|
||||
expect(inst.state.anonymousState__temp).toEqual('')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,173 @@
|
|||
import traverse from 'babel-traverse'
|
||||
import * as t from 'babel-types'
|
||||
import generate from 'babel-generator'
|
||||
import * as html from 'html'
|
||||
|
||||
export function prettyPrint (str: string): string {
|
||||
return html.prettyPrint(str, { max_char: 0 })
|
||||
}
|
||||
|
||||
export function buildComponent (
|
||||
renderBody: string,
|
||||
classMethod = '',
|
||||
head = ''
|
||||
) {
|
||||
return `
|
||||
${head}
|
||||
import Taro, { Component } from '@tarojs/taro'
|
||||
import { View, Button } from '@tarojs/components'
|
||||
|
||||
export default class Index extends Component {
|
||||
${classMethod}
|
||||
|
||||
|
||||
render () {
|
||||
${renderBody}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
const internalFunction = `function isObject(arg) {
|
||||
return arg === Object(arg) && typeof arg !== 'function';
|
||||
}
|
||||
|
||||
function getElementById (a, b, c) {
|
||||
if (c) {
|
||||
return 'test-component-ref'
|
||||
}
|
||||
return 'test-ref'
|
||||
}
|
||||
|
||||
function internal_get_original(item) {
|
||||
if (isObject(item)) {
|
||||
return item.$original || item;
|
||||
}
|
||||
|
||||
return item;
|
||||
};
|
||||
function dashify(str, options) {
|
||||
if (typeof str !== 'string') {
|
||||
throw new TypeError('expected a string');
|
||||
}
|
||||
|
||||
return str.trim().replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\W/g, function (m) {
|
||||
return /[À-ž]/.test(m) ? m : '-';
|
||||
}).replace(/^-+|-+$/g, '').replace(/-{2,}/g, function (m) {
|
||||
return options && options.condense ? '-' : m;
|
||||
}).toLowerCase();
|
||||
}
|
||||
|
||||
function internal_inline_style(obj) {
|
||||
if (obj == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (obj === null || obj === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!isObject(obj)) {
|
||||
throw new TypeError('style 只能是一个对象或字符串。');
|
||||
}
|
||||
|
||||
return Object.keys(obj).map(function (key) {
|
||||
return dashify(key).concat(':').concat(obj[key]);
|
||||
}).join(';');
|
||||
}
|
||||
`
|
||||
|
||||
export const baseCode = `
|
||||
return (
|
||||
<View className='index'>
|
||||
<View className='title'>title</View>
|
||||
<View className='content'>
|
||||
{this.state.list.map(item => {
|
||||
return (
|
||||
<View className='item'>{item}</View>
|
||||
)
|
||||
})}
|
||||
<Button className='add' onClick={this.add}>添加</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
`
|
||||
|
||||
export function removeShadowData (obj: any) {
|
||||
if (obj['__data']) {
|
||||
delete obj['__data']
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
export const baseOptions = {
|
||||
isRoot: false,
|
||||
isApp: false,
|
||||
sourcePath: __dirname,
|
||||
outputPath: __dirname,
|
||||
code: '',
|
||||
isTyped: false
|
||||
}
|
||||
|
||||
export function evalClass (ast: t.File, props = '', isRequire = false) {
|
||||
let mainClass!: t.ClassDeclaration
|
||||
const statements = new Set<t.ExpressionStatement>()
|
||||
|
||||
traverse(ast, {
|
||||
ClassDeclaration (path) {
|
||||
mainClass = path.node
|
||||
},
|
||||
/**
|
||||
* 目前 node 的版本支持不了 class-properties
|
||||
* 但 babel 又有 bug,某些情况竟然把转换后的 class-properties 编译到 super 之前
|
||||
* 不然用 babel.transformFromAst 就完事了
|
||||
* 现在只能自己实现这个 feature 的部分功能了,真 tm 麻烦
|
||||
* @TODO 有空再给他们提 PR 吧
|
||||
*/
|
||||
ClassProperty (path) {
|
||||
const { key, value } = path.node
|
||||
statements.add(t.expressionStatement(t.assignmentExpression(
|
||||
'=',
|
||||
t.memberExpression(
|
||||
t.thisExpression(),
|
||||
key
|
||||
),
|
||||
value
|
||||
)))
|
||||
path.remove()
|
||||
}
|
||||
})
|
||||
|
||||
for (const method of mainClass.body.body) {
|
||||
// constructor 即便没有被定义也会被加上
|
||||
if (t.isClassMethod(method) && method.kind === 'constructor') {
|
||||
const index = method.body.body.findIndex(node => t.isSuper(node))
|
||||
method.body.body.push(
|
||||
t.expressionStatement(t.assignmentExpression(
|
||||
'=',
|
||||
t.memberExpression(
|
||||
t.thisExpression(),
|
||||
t.identifier('state')
|
||||
),
|
||||
t.callExpression(t.memberExpression(t.thisExpression(), t.identifier('_createData')), [])
|
||||
))
|
||||
)
|
||||
method.body.body.splice(index, 0, ...statements)
|
||||
}
|
||||
}
|
||||
|
||||
let code = `function f() {};` +
|
||||
generate(t.classDeclaration(t.identifier('Test'), t.identifier('f'), mainClass.body, [])).code +
|
||||
';' + `new Test(${props})`
|
||||
|
||||
code = internalFunction + code
|
||||
|
||||
// tslint:disable-next-line
|
||||
return eval(code)
|
||||
}
|
||||
|
||||
export class Custom {}
|
|
@ -0,0 +1,2 @@
|
|||
var transform = require('./lib/src').default
|
||||
module.exports = module.exports.default = transform
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"name": "@tarojs/transformer-wx",
|
||||
"version": "1.2.13",
|
||||
"description": "Transfrom Nerv Component to Wechat mini program.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/NervJS/taro.git"
|
||||
},
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"index.js",
|
||||
"lib",
|
||||
"cli.js",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"test:cov": "jest --coverage && npm run lint",
|
||||
"test": "jest",
|
||||
"dev": "tsc -w --pretty",
|
||||
"lint": "tslint",
|
||||
"build": "tsc"
|
||||
},
|
||||
"author": "O2Team",
|
||||
"license": "MIT",
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx",
|
||||
"json",
|
||||
"node"
|
||||
],
|
||||
"testPathIgnorePatterns": [
|
||||
"node_modules",
|
||||
"utils"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0-beta.44",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-helper-evaluate-path": "^0.5.0",
|
||||
"babel-helper-mark-eval-scopes": "^0.4.3",
|
||||
"babel-helper-remove-or-void": "^0.4.3",
|
||||
"babel-plugin-danger-remove-unused-import": "^1.1.1",
|
||||
"babel-plugin-minify-dead-code": "^0.5.2",
|
||||
"babel-plugin-remove-dead-code": "^1.3.2",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-define": "^1.3.0",
|
||||
"babel-plugin-transform-es2015-template-literals": "^6.22.0",
|
||||
"babel-plugin-transform-flow-strip-types": "^6.22.0",
|
||||
"babel-traverse": "^6.26.0",
|
||||
"babel-types": "^6.26.0",
|
||||
"eslint": "^4.15.0",
|
||||
"eslint-plugin-taro": "1.2.13",
|
||||
"html": "^1.0.0",
|
||||
"lodash": "^4.17.5",
|
||||
"prettier": "^1.14.2",
|
||||
"typescript": "^3.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tarojs/taro": "1.2.13",
|
||||
"@types/babel-core": "^6.25.5",
|
||||
"@types/babel-generator": "^6.25.1",
|
||||
"@types/babel-template": "^6.25.0",
|
||||
"@types/babel-traverse": "6.25.3",
|
||||
"@types/babel-types": "^6.25.2",
|
||||
"@types/eslint": "^4.16.5",
|
||||
"@types/jest": "^22.2.3",
|
||||
"@types/lodash": "^4.14.105",
|
||||
"@types/node": "^9.6.2",
|
||||
"jest": "^23.0.1",
|
||||
"jest-cli": "^22.1.4",
|
||||
"ts-jest": "^22.4.6",
|
||||
"tslint": "^5.10.0",
|
||||
"tslint-config-prettier": "^1.10.0",
|
||||
"tslint-config-standard": "^7.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/NervJS/taro/issues"
|
||||
},
|
||||
"homepage": "https://github.com/NervJS/taro#readme"
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
export const enum Adapters {
|
||||
weapp = 'weapp',
|
||||
swan = 'swan',
|
||||
alipay = 'alipay',
|
||||
quickapp = 'quickapp',
|
||||
tt = 'tt'
|
||||
}
|
||||
|
||||
interface Adapter {
|
||||
if: string,
|
||||
else: string,
|
||||
elseif: string,
|
||||
for: string,
|
||||
forItem: string,
|
||||
forIndex: string,
|
||||
key: string
|
||||
type: Adapters
|
||||
}
|
||||
|
||||
const weixinAdapter: Adapter = {
|
||||
if: 'wx:if',
|
||||
else: 'wx:else',
|
||||
elseif: 'wx:elif',
|
||||
for: 'wx:for',
|
||||
forItem: 'wx:for-item',
|
||||
forIndex: 'wx:for-index',
|
||||
key: 'wx:key',
|
||||
type: Adapters.weapp
|
||||
}
|
||||
|
||||
const swanAdapter: Adapter = {
|
||||
if: 's-if',
|
||||
else: 's-else',
|
||||
elseif: 's-elif',
|
||||
for: 's-for',
|
||||
forItem: 's-for-item',
|
||||
forIndex: 's-for-index',
|
||||
key: 's-key',
|
||||
type: Adapters.swan
|
||||
}
|
||||
|
||||
const alipayAdapter: Adapter = {
|
||||
if: 'a:if',
|
||||
else: 'a:else',
|
||||
elseif: 'a:elif',
|
||||
for: 'a:for',
|
||||
forItem: 'a:for-item',
|
||||
forIndex: 'a:for-index',
|
||||
key: 'a:key',
|
||||
type: Adapters.alipay
|
||||
}
|
||||
|
||||
const ttAdapter: Adapter = {
|
||||
if: 'tt:if',
|
||||
else: 'tt:else',
|
||||
elseif: 'tt:elif',
|
||||
for: 'tt:for',
|
||||
forItem: 'tt:for-item',
|
||||
forIndex: 'tt:for-index',
|
||||
key: 'tt:key',
|
||||
type: Adapters.tt
|
||||
}
|
||||
|
||||
export let Adapter: Adapter = weixinAdapter
|
||||
|
||||
export function setAdapter (adapter: Adapters) {
|
||||
switch (adapter.toLowerCase()) {
|
||||
case Adapters.swan:
|
||||
Adapter = swanAdapter
|
||||
break
|
||||
case Adapters.alipay:
|
||||
Adapter = alipayAdapter
|
||||
break
|
||||
case Adapters.tt:
|
||||
Adapter = ttAdapter
|
||||
break
|
||||
default:
|
||||
Adapter = weixinAdapter
|
||||
break
|
||||
}
|
||||
}
|
|
@ -0,0 +1,743 @@
|
|||
import { NodePath } from 'babel-traverse'
|
||||
import * as t from 'babel-types'
|
||||
import {
|
||||
codeFrameError,
|
||||
hasComplexExpression,
|
||||
generateAnonymousState,
|
||||
findMethodName,
|
||||
pathResolver,
|
||||
createRandomLetters,
|
||||
isContainJSXElement,
|
||||
getSlotName,
|
||||
isArrayMapCallExpression,
|
||||
incrementId,
|
||||
isContainStopPropagation
|
||||
} from './utils'
|
||||
import { DEFAULT_Component_SET } from './constant'
|
||||
import { kebabCase, uniqueId, get as safeGet, set as safeSet } from 'lodash'
|
||||
import { RenderParser } from './render'
|
||||
import { findJSXAttrByName } from './jsx'
|
||||
import { Adapters, Adapter } from './adapter'
|
||||
import { LoopRef } from './interface'
|
||||
import generate from 'babel-generator'
|
||||
|
||||
type ClassMethodsMap = Map<string, NodePath<t.ClassMethod | t.ClassProperty>>
|
||||
|
||||
function buildConstructor () {
|
||||
const ctor = t.classMethod(
|
||||
'constructor',
|
||||
t.identifier('constructor'),
|
||||
[t.identifier('props')],
|
||||
t.blockStatement([
|
||||
t.expressionStatement(
|
||||
t.callExpression(t.identifier('super'), [
|
||||
t.identifier('props')
|
||||
])
|
||||
)
|
||||
])
|
||||
)
|
||||
return ctor
|
||||
}
|
||||
|
||||
function processThisPropsFnMemberProperties (
|
||||
member: t.MemberExpression,
|
||||
path: NodePath<t.CallExpression>,
|
||||
args: Array<t.Expression | t.SpreadElement>,
|
||||
binded: boolean
|
||||
) {
|
||||
const propertyArray: string[] = []
|
||||
function traverseMember (member: t.MemberExpression) {
|
||||
const object = member.object
|
||||
const property = member.property
|
||||
|
||||
if (t.isIdentifier(property)) {
|
||||
propertyArray.push(property.name)
|
||||
}
|
||||
|
||||
if (t.isMemberExpression(object)) {
|
||||
if (t.isThisExpression(object.object) &&
|
||||
t.isIdentifier(object.property) &&
|
||||
object.property.name === 'props'
|
||||
) {
|
||||
if (Adapters.alipay === Adapter.type) {
|
||||
if (binded) args.shift()
|
||||
path.replaceWith(
|
||||
t.callExpression(
|
||||
t.memberExpression(t.thisExpression(), t.identifier('__triggerPropsFn')),
|
||||
[
|
||||
t.stringLiteral(propertyArray.reverse().join('.')),
|
||||
t.arrayExpression(args)
|
||||
]
|
||||
)
|
||||
)
|
||||
} else {
|
||||
path.replaceWith(
|
||||
t.callExpression(
|
||||
t.memberExpression(t.thisExpression(), t.identifier('__triggerPropsFn')),
|
||||
[t.stringLiteral(propertyArray.reverse().join('.')), t.callExpression(
|
||||
t.memberExpression(t.arrayExpression([t.nullLiteral()]), t.identifier('concat')),
|
||||
[t.arrayExpression(args)]
|
||||
)]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
traverseMember(object)
|
||||
}
|
||||
}
|
||||
traverseMember(member)
|
||||
}
|
||||
|
||||
interface Result {
|
||||
template: string
|
||||
components: {
|
||||
name: string,
|
||||
path: string,
|
||||
type: string
|
||||
}[],
|
||||
componentProperies: string[]
|
||||
}
|
||||
|
||||
interface Ref {
|
||||
refName?: string,
|
||||
type: 'component' | 'dom',
|
||||
id: string,
|
||||
fn?: t.FunctionExpression | t.ArrowFunctionExpression | t.MemberExpression
|
||||
}
|
||||
|
||||
class Transformer {
|
||||
public result: Result = {
|
||||
template: '',
|
||||
components: [],
|
||||
componentProperies: []
|
||||
}
|
||||
private methods: ClassMethodsMap = new Map()
|
||||
private initState: Set<string> = new Set()
|
||||
private jsxReferencedIdentifiers = new Set<t.Identifier>()
|
||||
private customComponents: Map<string, { sourcePath: string, type: string }> = new Map()
|
||||
private anonymousMethod: Map<string, string> = new Map()
|
||||
private renderMethod: null | NodePath<t.ClassMethod> = null
|
||||
private moduleNames: string[]
|
||||
private classPath: NodePath<t.ClassDeclaration>
|
||||
private customComponentNames = new Set<string>()
|
||||
private usedState = new Set<string>()
|
||||
private loopStateName: Map<NodePath<t.CallExpression>, string> = new Map()
|
||||
private customComponentData: Array<t.ObjectProperty> = []
|
||||
private componentProperies: Set<string>
|
||||
private sourcePath: string
|
||||
private refs: Ref[] = []
|
||||
private loopRefs: Map<t.JSXElement, LoopRef> = new Map()
|
||||
private anonymousFuncCounter = incrementId()
|
||||
|
||||
constructor (
|
||||
path: NodePath<t.ClassDeclaration>,
|
||||
sourcePath: string,
|
||||
componentProperies: string[]
|
||||
) {
|
||||
this.classPath = path
|
||||
this.sourcePath = sourcePath
|
||||
this.moduleNames = Object.keys(path.scope.getAllBindings('module'))
|
||||
this.componentProperies = new Set(componentProperies)
|
||||
this.compile()
|
||||
}
|
||||
|
||||
setMultipleSlots () {
|
||||
const body = this.classPath.node.body.body
|
||||
if (body.some(c => t.isClassProperty(c) && c.key.name === 'multipleSlots')) {
|
||||
return
|
||||
}
|
||||
const multipleSlots: any = t.classProperty(t.identifier('multipleSlots'), t.booleanLiteral(true))
|
||||
multipleSlots.static = true
|
||||
body.push(multipleSlots)
|
||||
}
|
||||
|
||||
createStringRef (componentName: string, id: string, refName: string) {
|
||||
this.refs.push({
|
||||
type: DEFAULT_Component_SET.has(componentName) ? 'dom' : 'component',
|
||||
id,
|
||||
refName
|
||||
})
|
||||
}
|
||||
|
||||
createFunctionRef (componentName: string, id: string, fn) {
|
||||
this.refs.push({
|
||||
type: DEFAULT_Component_SET.has(componentName) ? 'dom' : 'component',
|
||||
id,
|
||||
fn
|
||||
})
|
||||
}
|
||||
|
||||
handleRefs () {
|
||||
const objExpr = this.refs.map(ref => {
|
||||
return t.objectExpression([
|
||||
t.objectProperty(
|
||||
t.identifier('type'),
|
||||
t.stringLiteral(ref.type)
|
||||
),
|
||||
t.objectProperty(
|
||||
t.identifier('id'),
|
||||
t.stringLiteral(ref.id)
|
||||
),
|
||||
t.objectProperty(
|
||||
t.identifier('refName'),
|
||||
t.stringLiteral(ref.refName || '')
|
||||
),
|
||||
t.objectProperty(
|
||||
t.identifier('fn'),
|
||||
ref.fn ? ref.fn : t.nullLiteral()
|
||||
)
|
||||
])
|
||||
})
|
||||
|
||||
this.classPath.node.body.body.push(t.classProperty(
|
||||
t.identifier('$$refs'),
|
||||
t.arrayExpression(objExpr)
|
||||
))
|
||||
}
|
||||
|
||||
traverse () {
|
||||
const self = this
|
||||
self.classPath.traverse({
|
||||
JSXOpeningElement: (path) => {
|
||||
const jsx = path.node
|
||||
const attrs = jsx.attributes
|
||||
if (!t.isJSXIdentifier(jsx.name)) {
|
||||
return
|
||||
}
|
||||
const loopCallExpr = path.findParent(p => isArrayMapCallExpression(p))
|
||||
const componentName = jsx.name.name
|
||||
const refAttr = findJSXAttrByName(attrs, 'ref')
|
||||
if (!refAttr) {
|
||||
return
|
||||
}
|
||||
const idAttr = findJSXAttrByName(attrs, 'id')
|
||||
let id: string = createRandomLetters(5)
|
||||
let idExpr: t.Expression
|
||||
if (!idAttr) {
|
||||
if (loopCallExpr && loopCallExpr.isCallExpression()) {
|
||||
const [ func ] = loopCallExpr.node.arguments
|
||||
let indexId: t.Identifier | null = null
|
||||
if (t.isFunctionExpression(func) || t.isArrowFunctionExpression(func)) {
|
||||
const params = func.params as t.Identifier[]
|
||||
indexId = params[1]
|
||||
}
|
||||
if (indexId === null || !t.isIdentifier(indexId!)) {
|
||||
throw codeFrameError(path.node, '在循环中使用 ref 必须暴露循环的第二个参数 `index`')
|
||||
}
|
||||
attrs.push(t.jSXAttribute(t.jSXIdentifier('id'), t.jSXExpressionContainer(
|
||||
t.binaryExpression('+', t.stringLiteral(id), indexId)
|
||||
)))
|
||||
} else {
|
||||
attrs.push(t.jSXAttribute(t.jSXIdentifier('id'), t.stringLiteral(id)))
|
||||
}
|
||||
} else {
|
||||
const idValue = idAttr.value
|
||||
if (t.isStringLiteral(idValue)) {
|
||||
id = idValue.value
|
||||
} else if (t.isJSXExpressionContainer(idValue)) {
|
||||
if (t.isStringLiteral(idValue.expression)) {
|
||||
id = idValue.expression.value
|
||||
} else {
|
||||
idExpr = idValue.expression
|
||||
}
|
||||
}
|
||||
}
|
||||
if (t.isStringLiteral(refAttr.value)) {
|
||||
if (loopCallExpr) {
|
||||
throw codeFrameError(refAttr, '循环中的 ref 只能使用函数。')
|
||||
}
|
||||
this.createStringRef(componentName, id, refAttr.value.value)
|
||||
}
|
||||
if (t.isJSXExpressionContainer(refAttr.value)) {
|
||||
const expr = refAttr.value.expression
|
||||
if (t.isStringLiteral(expr)) {
|
||||
if (loopCallExpr) {
|
||||
throw codeFrameError(refAttr, '循环中的 ref 只能使用函数。')
|
||||
}
|
||||
this.createStringRef(componentName, id, expr.value)
|
||||
} else if (t.isArrowFunctionExpression(expr) || t.isMemberExpression(expr)) {
|
||||
const type = DEFAULT_Component_SET.has(componentName) ? 'dom' : 'component'
|
||||
if (loopCallExpr) {
|
||||
this.loopRefs.set(path.parentPath.node as t.JSXElement, {
|
||||
id: idExpr! || id,
|
||||
fn: expr,
|
||||
type,
|
||||
component: path.parentPath as NodePath<t.JSXElement>
|
||||
})
|
||||
} else {
|
||||
this.refs.push({
|
||||
type,
|
||||
id,
|
||||
fn: expr
|
||||
})
|
||||
}
|
||||
} else {
|
||||
throw codeFrameError(refAttr, 'ref 仅支持传入字符串、匿名箭头函数和 class 中已声明的函数')
|
||||
}
|
||||
}
|
||||
for (const [index, attr] of attrs.entries()) {
|
||||
if (attr === refAttr) {
|
||||
attrs.splice(index, 1)
|
||||
}
|
||||
}
|
||||
},
|
||||
ClassMethod (path) {
|
||||
const node = path.node
|
||||
if (t.isIdentifier(node.key)) {
|
||||
const name = node.key.name
|
||||
self.methods.set(name, path)
|
||||
if (name === 'render') {
|
||||
self.renderMethod = path
|
||||
path.traverse({
|
||||
ReturnStatement (returnPath) {
|
||||
const arg = returnPath.node.argument
|
||||
const ifStem = returnPath.findParent(p => p.isIfStatement())
|
||||
if (ifStem && ifStem.isIfStatement() && arg === null) {
|
||||
const consequent = ifStem.get('consequent')
|
||||
if (consequent.isBlockStatement() && consequent.node.body.includes(returnPath.node)) {
|
||||
returnPath.get('argument').replaceWith(t.nullLiteral())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (name === 'constructor') {
|
||||
path.traverse({
|
||||
AssignmentExpression (p) {
|
||||
if (
|
||||
t.isMemberExpression(p.node.left) &&
|
||||
t.isThisExpression(p.node.left.object) &&
|
||||
t.isIdentifier(p.node.left.property) &&
|
||||
p.node.left.property.name === 'state' &&
|
||||
t.isObjectExpression(p.node.right)
|
||||
) {
|
||||
const properties = p.node.right.properties
|
||||
properties.forEach(p => {
|
||||
if (t.isObjectProperty(p) && t.isIdentifier(p.key)) {
|
||||
self.initState.add(p.key.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
IfStatement (path) {
|
||||
const test = path.get('test') as NodePath<t.Expression>
|
||||
const consequent = path.get('consequent')
|
||||
if (isContainJSXElement(consequent) && hasComplexExpression(test)) {
|
||||
const scope = self.renderMethod && self.renderMethod.scope || path.scope
|
||||
generateAnonymousState(scope, test, self.jsxReferencedIdentifiers, true)
|
||||
}
|
||||
},
|
||||
ClassProperty (path) {
|
||||
const { key: { name }, value } = path.node
|
||||
if (t.isArrowFunctionExpression(value) || t.isFunctionExpression(value)) {
|
||||
self.methods.set(name, path)
|
||||
}
|
||||
if (name === 'state' && t.isObjectExpression(value)) {
|
||||
value.properties.forEach(p => {
|
||||
if (t.isObjectProperty(p)) {
|
||||
if (t.isIdentifier(p.key)) {
|
||||
self.initState.add(p.key.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
JSXExpressionContainer (path) {
|
||||
const attr = path.findParent(p => p.isJSXAttribute()) as NodePath<t.JSXAttribute>
|
||||
const isFunctionProp = attr && typeof attr.node.name.name === 'string' && attr.node.name.name.startsWith('on')
|
||||
path.traverse({
|
||||
MemberExpression (path) {
|
||||
const sibling = path.getSibling('property')
|
||||
if (
|
||||
path.get('object').isThisExpression() &&
|
||||
(path.get('property').isIdentifier({ name: 'props' }) || path.get('property').isIdentifier({ name: 'state' })) &&
|
||||
sibling.isIdentifier()
|
||||
) {
|
||||
if (!isFunctionProp) {
|
||||
self.usedState.add(sibling.node.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const expression = path.get('expression') as NodePath<t.Expression>
|
||||
const scope = self.renderMethod && self.renderMethod.scope || path.scope
|
||||
const calleeExpr = expression.get('callee')
|
||||
const parentPath = path.parentPath
|
||||
if (
|
||||
hasComplexExpression(expression) &&
|
||||
!isFunctionProp &&
|
||||
!(calleeExpr &&
|
||||
calleeExpr.isMemberExpression() &&
|
||||
calleeExpr.get('object').isMemberExpression() &&
|
||||
calleeExpr.get('property').isIdentifier({ name: 'bind' })) // is not bind
|
||||
) {
|
||||
generateAnonymousState(scope, expression, self.jsxReferencedIdentifiers)
|
||||
} else {
|
||||
if (parentPath.isJSXAttribute()) {
|
||||
if (!(expression.isMemberExpression() || expression.isIdentifier()) && parentPath.node.name.name === 'key') {
|
||||
generateAnonymousState(scope, expression, self.jsxReferencedIdentifiers)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!attr) return
|
||||
const key = attr.node.name
|
||||
const value = attr.node.value
|
||||
if (!t.isJSXIdentifier(key)) {
|
||||
return
|
||||
}
|
||||
if (t.isJSXIdentifier(key) && key.name.startsWith('on') && t.isJSXExpressionContainer(value)) {
|
||||
const expr = value.expression
|
||||
if (t.isCallExpression(expr) && t.isMemberExpression(expr.callee) && t.isIdentifier(expr.callee.property, { name: 'bind' })) {
|
||||
self.buildPropsAnonymousFunc(attr, expr, true)
|
||||
} else if (t.isMemberExpression(expr)) {
|
||||
self.buildPropsAnonymousFunc(attr, expr as any, false)
|
||||
} else if (t.isArrowFunctionExpression(expr)) {
|
||||
const exprPath = attr.get('value.expression')
|
||||
const stemParent = path.getStatementParent()
|
||||
const counter = self.anonymousFuncCounter()
|
||||
const anonymousFuncName = `anonymousFunc${counter}`
|
||||
const isCatch = isContainStopPropagation(exprPath)
|
||||
const classBody = self.classPath.node.body.body
|
||||
const loopCallExpr = path.findParent(p => isArrayMapCallExpression(p)) as NodePath<t.CallExpression>
|
||||
let index: t.Identifier
|
||||
if (loopCallExpr) {
|
||||
index = safeGet(loopCallExpr, 'node.arguments[0].params[1]')
|
||||
if (!t.isIdentifier(index)) {
|
||||
index = t.identifier('__index' + counter)
|
||||
safeSet(loopCallExpr, 'node.arguments[0].params[1]', index)
|
||||
}
|
||||
classBody.push(t.classProperty(t.identifier(anonymousFuncName + 'Array'), t.arrayExpression([])))
|
||||
const arrayFunc = t.memberExpression(
|
||||
t.memberExpression(t.thisExpression(), t.identifier(anonymousFuncName + 'Array')),
|
||||
t.identifier(index.name),
|
||||
true
|
||||
)
|
||||
classBody.push(
|
||||
t.classMethod('method', t.identifier(anonymousFuncName), [t.identifier(index.name), t.identifier('e')], t.blockStatement([
|
||||
isCatch ? t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('e'), t.identifier('stopPropagation')), [])) : t.emptyStatement(),
|
||||
t.expressionStatement(t.logicalExpression('&&', arrayFunc, t.callExpression(arrayFunc, [t.identifier('e')])))
|
||||
]))
|
||||
)
|
||||
exprPath.replaceWith(t.callExpression(
|
||||
t.memberExpression(
|
||||
t.memberExpression(t.thisExpression(), t.identifier(anonymousFuncName)),
|
||||
t.identifier('bind')
|
||||
),
|
||||
[t.thisExpression(), t.identifier(index.name)]
|
||||
))
|
||||
stemParent.insertBefore(
|
||||
t.expressionStatement(t.assignmentExpression(
|
||||
'=',
|
||||
arrayFunc,
|
||||
expr
|
||||
))
|
||||
)
|
||||
} else {
|
||||
classBody.push(
|
||||
t.classMethod('method', t.identifier(anonymousFuncName), [t.identifier('e')], t.blockStatement([
|
||||
isCatch ? t.expressionStatement(t.callExpression(t.memberExpression(t.identifier('e'), t.identifier('stopPropagation')), [])) : t.emptyStatement()
|
||||
]))
|
||||
)
|
||||
exprPath.replaceWith(t.memberExpression(t.thisExpression(), t.identifier(anonymousFuncName)))
|
||||
stemParent.insertBefore(
|
||||
t.expressionStatement(t.assignmentExpression(
|
||||
'=',
|
||||
t.memberExpression(t.thisExpression(), t.identifier(anonymousFuncName)),
|
||||
expr
|
||||
))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw codeFrameError(path.node, '组件事件传参只能在使用匿名箭头函数,或使用类作用域下的确切引用(this.handleXX || this.props.handleXX),或使用 bind。')
|
||||
}
|
||||
}
|
||||
const jsx = path.findParent(p => p.isJSXOpeningElement()) as NodePath<t.JSXOpeningElement>
|
||||
if (!jsx) return
|
||||
const jsxName = jsx.node.name
|
||||
if (!t.isJSXIdentifier(jsxName)) return
|
||||
if (expression.isJSXElement()) return
|
||||
if (DEFAULT_Component_SET.has(jsxName.name) || expression.isIdentifier() || expression.isMemberExpression() || expression.isLiteral() || expression.isLogicalExpression() || expression.isConditionalExpression() || key.name.startsWith('on') || expression.isCallExpression()) return
|
||||
generateAnonymousState(scope, expression, self.jsxReferencedIdentifiers)
|
||||
},
|
||||
JSXElement (path) {
|
||||
const id = path.node.openingElement.name
|
||||
if (
|
||||
t.isJSXIdentifier(id) &&
|
||||
!DEFAULT_Component_SET.has(id.name) &&
|
||||
self.moduleNames.indexOf(id.name) !== -1
|
||||
) {
|
||||
const name = id.name
|
||||
const binding = self.classPath.scope.getBinding(name)
|
||||
if (binding && t.isImportDeclaration(binding.path.parent)) {
|
||||
const sourcePath = binding.path.parent.source.value
|
||||
if (binding.path.isImportDefaultSpecifier()) {
|
||||
self.customComponents.set(name, {
|
||||
sourcePath,
|
||||
type: 'default'
|
||||
})
|
||||
} else {
|
||||
self.customComponents.set(name, {
|
||||
sourcePath,
|
||||
type: 'pattern'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
MemberExpression: (path) => {
|
||||
const object = path.get('object')
|
||||
const property = path.get('property')
|
||||
if (
|
||||
!(
|
||||
object.isThisExpression() && property.isIdentifier({ name: 'props' })
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const parentPath = path.parentPath
|
||||
if (parentPath.isMemberExpression()) {
|
||||
const siblingProp = parentPath.get('property')
|
||||
if (siblingProp.isIdentifier()) {
|
||||
const name = siblingProp.node.name
|
||||
if (name === 'children') {
|
||||
parentPath.replaceWith(t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier('slot'), [], true), t.jSXClosingElement(t.jSXIdentifier('slot')), [], true))
|
||||
} else if (/^render[A-Z]/.test(name)) {
|
||||
const slotName = getSlotName(name)
|
||||
parentPath.replaceWith(t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier('slot'), [
|
||||
t.jSXAttribute(t.jSXIdentifier('name'), t.stringLiteral(slotName))
|
||||
], true), t.jSXClosingElement(t.jSXIdentifier('slot')), []))
|
||||
this.setMultipleSlots()
|
||||
} else {
|
||||
self.componentProperies.add(siblingProp.node.name)
|
||||
}
|
||||
}
|
||||
} else if (parentPath.isVariableDeclarator()) {
|
||||
const siblingId = parentPath.get('id')
|
||||
if (siblingId.isObjectPattern()) {
|
||||
const properties = siblingId.node.properties
|
||||
for (const prop of properties) {
|
||||
if (t.isRestProperty(prop)) {
|
||||
throw codeFrameError(prop.loc, 'this.props 不支持使用 rest property 语法,请把每一个 prop 都单独列出来')
|
||||
} else if (t.isIdentifier(prop.key)) {
|
||||
self.componentProperies.add(prop.key.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
CallExpression (path) {
|
||||
const node = path.node
|
||||
const callee = node.callee
|
||||
if (t.isMemberExpression(callee) && t.isMemberExpression(callee.object)) {
|
||||
const property = callee.property
|
||||
if (t.isIdentifier(property)) {
|
||||
if (property.name.startsWith('on')) {
|
||||
self.componentProperies.add(`__fn_${property.name}`)
|
||||
processThisPropsFnMemberProperties(callee, path, node.arguments, false)
|
||||
} else if (property.name === 'call' || property.name === 'apply') {
|
||||
self.componentProperies.add(`__fn_${property.name}`)
|
||||
processThisPropsFnMemberProperties(callee.object, path, node.arguments, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
buildPropsAnonymousFunc = (attr: NodePath<t.JSXAttribute>, expr: t.CallExpression, isBind = false) => {
|
||||
const { code } = generate(expr)
|
||||
if (code.startsWith('this.props')) {
|
||||
const methodName = findMethodName(expr)
|
||||
const hasMethodName = this.anonymousMethod.has(methodName) || !methodName
|
||||
const funcName = hasMethodName
|
||||
? this.anonymousMethod.get(methodName)!
|
||||
// 测试时使用1个稳定的 uniqueID 便于测试,实际使用5个英文字母,否则小程序不支持
|
||||
: process.env.NODE_ENV === 'test' ? uniqueId('funPrivate') : `funPrivate${createRandomLetters(5)}`
|
||||
this.anonymousMethod.set(methodName, funcName)
|
||||
const newVal = isBind
|
||||
? t.callExpression(t.memberExpression(t.memberExpression(t.thisExpression(), t.identifier(funcName)), t.identifier('bind')), expr.arguments || [])
|
||||
: t.memberExpression(t.thisExpression(), t.identifier(funcName))
|
||||
attr.get('value.expression').replaceWith(newVal)
|
||||
this.methods.set(funcName, null as any)
|
||||
this.componentProperies.add(methodName)
|
||||
if (hasMethodName) {
|
||||
return
|
||||
}
|
||||
const attrName = attr.node.name
|
||||
if (t.isJSXIdentifier(attrName) && attrName.name.startsWith('on')) {
|
||||
this.componentProperies.add(`__fn_${attrName.name}`)
|
||||
}
|
||||
if (methodName.startsWith('on')) {
|
||||
this.componentProperies.add(`__fn_${methodName}`)
|
||||
}
|
||||
const method = t.classMethod('method', t.identifier(funcName), [], t.blockStatement([
|
||||
t.expressionStatement(t.callExpression(
|
||||
t.memberExpression(t.thisExpression(), t.identifier('__triggerPropsFn')),
|
||||
[t.stringLiteral(methodName), t.arrayExpression([t.spreadElement(t.identifier('arguments'))])]
|
||||
))
|
||||
]))
|
||||
this.classPath.node.body.body = this.classPath.node.body.body.concat(method)
|
||||
}
|
||||
}
|
||||
|
||||
setComponents () {
|
||||
this.customComponents.forEach((component, name) => {
|
||||
this.result.components.push({
|
||||
path: pathResolver(component.sourcePath, this.sourcePath),
|
||||
name: kebabCase(name),
|
||||
type: component.type
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
setMethods () {
|
||||
const methods: Array<NodePath<t.ClassProperty | t.ClassMethod>> = (this.classPath as any).get('body').get('body')
|
||||
for (const method of methods) {
|
||||
if (method.isClassMethod()) {
|
||||
const key = method.get('key')
|
||||
if (key.isIdentifier()) {
|
||||
this.methods.set(key.node.name, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetConstructor () {
|
||||
const body = this.classPath.node.body.body
|
||||
if (!this.methods.has('constructor')) {
|
||||
const ctor = buildConstructor()
|
||||
body.unshift(ctor)
|
||||
}
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return
|
||||
}
|
||||
for (const method of body) {
|
||||
if (t.isClassMethod(method) && method.kind === 'constructor') {
|
||||
method.kind = 'method'
|
||||
method.key = t.identifier('_constructor')
|
||||
if (t.isBlockStatement(method.body)) {
|
||||
for (const statement of method.body.body) {
|
||||
if (t.isExpressionStatement(statement)) {
|
||||
const expr = statement.expression
|
||||
if (t.isCallExpression(expr) && (t.isIdentifier(expr.callee, { name: 'super' }) || t.isSuper(expr.callee))) {
|
||||
expr.callee = t.memberExpression(t.identifier('super'), t.identifier('_constructor'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleLifecyclePropParam (propParam: t.LVal, properties: Set<string>) {
|
||||
let propsName: string | null = null
|
||||
if (!propParam) {
|
||||
return null
|
||||
}
|
||||
if (t.isIdentifier(propParam)) {
|
||||
propsName = propParam.name
|
||||
} else if (t.isObjectPattern(propParam)) {
|
||||
for (const prop of propParam.properties) {
|
||||
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
||||
properties.add(prop.key.name)
|
||||
} else if (t.isRestProperty(prop) && t.isIdentifier(prop.argument)) {
|
||||
propsName = prop.argument.name
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw codeFrameError(propParam.loc, '此生命周期的第一个参数只支持写标识符或对象解构')
|
||||
}
|
||||
return propsName
|
||||
}
|
||||
|
||||
findMoreProps () {
|
||||
// 第一个参数是 props 的生命周期
|
||||
const lifeCycles = new Set([
|
||||
// 'constructor',
|
||||
'componentDidUpdate',
|
||||
'shouldComponentUpdate',
|
||||
'getDerivedStateFromProps',
|
||||
'getSnapshotBeforeUpdate',
|
||||
'componentWillReceiveProps',
|
||||
'componentWillUpdate'
|
||||
])
|
||||
const properties = new Set<string>()
|
||||
this.methods.forEach((method, name) => {
|
||||
if (!lifeCycles.has(name)) {
|
||||
return
|
||||
}
|
||||
const node = method.node
|
||||
let propsName: null | string = null
|
||||
if (t.isClassMethod(node)) {
|
||||
propsName = this.handleLifecyclePropParam(node.params[0], properties)
|
||||
} else if (t.isArrowFunctionExpression(node.value) || t.isFunctionExpression(node.value)) {
|
||||
propsName = this.handleLifecyclePropParam(node.value.params[0], properties)
|
||||
}
|
||||
if (propsName === null) {
|
||||
return
|
||||
}
|
||||
method.traverse({
|
||||
MemberExpression (path) {
|
||||
if (!path.isReferencedMemberExpression()) {
|
||||
return
|
||||
}
|
||||
const { object, property } = path.node
|
||||
if (t.isIdentifier(object, { name: propsName }) && t.isIdentifier(property)) {
|
||||
properties.add(property.name)
|
||||
}
|
||||
},
|
||||
VariableDeclarator (path) {
|
||||
const { id, init } = path.node
|
||||
if (t.isObjectPattern(id) && t.isIdentifier(init, { name: propsName })) {
|
||||
for (const prop of id.properties) {
|
||||
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key)) {
|
||||
properties.add(prop.key.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
properties.forEach((value) => {
|
||||
this.componentProperies.add(value)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
parseRender () {
|
||||
if (this.renderMethod) {
|
||||
this.result.template = this.result.template
|
||||
+ new RenderParser(
|
||||
this.renderMethod,
|
||||
this.methods,
|
||||
this.initState,
|
||||
this.jsxReferencedIdentifiers,
|
||||
this.usedState,
|
||||
this.loopStateName,
|
||||
this.customComponentNames,
|
||||
this.customComponentData,
|
||||
this.componentProperies,
|
||||
this.loopRefs
|
||||
).outputTemplate
|
||||
}
|
||||
}
|
||||
|
||||
compile () {
|
||||
this.traverse()
|
||||
this.setMethods()
|
||||
this.setComponents()
|
||||
this.resetConstructor()
|
||||
this.findMoreProps()
|
||||
this.handleRefs()
|
||||
this.parseRender()
|
||||
this.result.componentProperies = [...this.componentProperies]
|
||||
}
|
||||
}
|
||||
|
||||
export { Transformer }
|
|
@ -0,0 +1,118 @@
|
|||
import { Adapters } from './adapter'
|
||||
|
||||
export const THIRD_PARTY_COMPONENTS = new Set<string>()
|
||||
|
||||
// tslint:disable-next-line:variable-name
|
||||
export const DEFAULT_Component_SET = new Set<string>([
|
||||
'View',
|
||||
'ScrollView',
|
||||
'Swiper',
|
||||
'MovableView',
|
||||
'CoverView',
|
||||
'CoverImage',
|
||||
'Icon',
|
||||
'Text',
|
||||
'RichText',
|
||||
'Progress',
|
||||
'Button',
|
||||
'Checkbox',
|
||||
'Form',
|
||||
'Input',
|
||||
'Label',
|
||||
'Picker',
|
||||
'PickerView',
|
||||
'PickerViewColumn',
|
||||
'Radio',
|
||||
'RadioGroup',
|
||||
'CheckboxGroup',
|
||||
'Slider',
|
||||
'Switch',
|
||||
'Textarea',
|
||||
'Navigator',
|
||||
'Audio',
|
||||
'Image',
|
||||
'Video',
|
||||
'Camera',
|
||||
'LivePlayer',
|
||||
'LivePusher',
|
||||
'Map',
|
||||
'Canvas',
|
||||
'OpenData',
|
||||
'WebView',
|
||||
'SwiperItem',
|
||||
'MovableArea',
|
||||
'MovableView',
|
||||
'FunctionalPageNavigator',
|
||||
'Ad',
|
||||
'Block',
|
||||
'Import',
|
||||
'OfficialAccount'
|
||||
])
|
||||
|
||||
export const INTERNAL_SAFE_GET = 'internal_safe_get'
|
||||
|
||||
export const TARO_PACKAGE_NAME = '@tarojs/taro'
|
||||
|
||||
export const COMPONENTS_PACKAGE_NAME = '@tarojs/components'
|
||||
|
||||
export const REDUX_PACKAGE_NAME = '@tarojs/redux'
|
||||
|
||||
export const MOBX_PACKAGE_NAME = '@tarojs/mobx'
|
||||
|
||||
export const MAP_CALL_ITERATOR = '__item'
|
||||
|
||||
export const INTERNAL_INLINE_STYLE = 'internal_inline_style'
|
||||
|
||||
export const INTERNAL_GET_ORIGNAL = 'internal_get_original'
|
||||
|
||||
export const GEL_ELEMENT_BY_ID = 'getElementById'
|
||||
|
||||
export const LOOP_STATE = '$loopState'
|
||||
|
||||
export let LOOP_ORIGINAL = '$original'
|
||||
|
||||
export const setLoopOriginal = (s: string) => LOOP_ORIGINAL = s
|
||||
|
||||
export const LOOP_CALLEE = '$anonymousCallee_'
|
||||
|
||||
export const SPECIAL_COMPONENT_PROPS = new Map<string, Set<string>>()
|
||||
|
||||
SPECIAL_COMPONENT_PROPS.set(
|
||||
'Progress',
|
||||
new Set([
|
||||
'activeColor',
|
||||
'backgroundColor'
|
||||
])
|
||||
)
|
||||
|
||||
export const IMAGE_COMPONENTS = new Set<string>([
|
||||
'Image',
|
||||
'CoverImage'
|
||||
])
|
||||
|
||||
export const swanSpecialAttrs = {
|
||||
'ScrollView': ['scrollTop', 'scrollLeft', 'scrollIntoView'],
|
||||
'Input': ['value'],
|
||||
'Textarea': ['value'],
|
||||
'MovableView': ['x', 'y'],
|
||||
'Slider': ['value']
|
||||
}
|
||||
|
||||
export const ALIPAY_BUBBLE_EVENTS = new Set<string>([
|
||||
'onTouchStart',
|
||||
'onTouchMove',
|
||||
'onTouchEnd',
|
||||
'onTouchCancel',
|
||||
'onClick',
|
||||
'onLongTap'
|
||||
])
|
||||
|
||||
export const TRANSFORM_COMPONENT_PROPS = new Map<Adapters, { [key: string]: { [key: string]: string } }>()
|
||||
|
||||
TRANSFORM_COMPONENT_PROPS.set(Adapters.alipay, {
|
||||
'Canvas': {
|
||||
'canvasId': 'id'
|
||||
}
|
||||
})
|
||||
|
||||
export const lessThanSignPlacehold = '__LESS_THAN_SIGN_PLACEHOLDER__'
|
|
@ -0,0 +1,64 @@
|
|||
const voidHtmlTags = new Set<string>([
|
||||
// 'image',
|
||||
'img',
|
||||
'input',
|
||||
'import'
|
||||
])
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
voidHtmlTags.add('image')
|
||||
}
|
||||
|
||||
interface Options {
|
||||
name: string,
|
||||
attributes: object,
|
||||
value: string
|
||||
}
|
||||
|
||||
function stringifyAttributes (input: object) {
|
||||
const attributes: string[] = []
|
||||
|
||||
for (const key of Object.keys(input)) {
|
||||
let value = input[key]
|
||||
|
||||
if (value === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value = value.join(' ')
|
||||
}
|
||||
|
||||
let attribute = key
|
||||
|
||||
if (value !== true) {
|
||||
attribute += `="${String(value)}"`
|
||||
}
|
||||
|
||||
attributes.push(attribute)
|
||||
}
|
||||
|
||||
return attributes.length > 0 ? ' ' + attributes.join(' ') : ''
|
||||
|
||||
}
|
||||
|
||||
export const createHTMLElement = (options: Options) => {
|
||||
options = Object.assign(
|
||||
{
|
||||
name: 'div',
|
||||
attributes: {},
|
||||
value: ''
|
||||
},
|
||||
options
|
||||
)
|
||||
|
||||
const isVoidTag = voidHtmlTags.has(options.name)
|
||||
|
||||
let ret = `<${options.name}${stringifyAttributes(options.attributes)}${isVoidTag ? `/` : '' }>`
|
||||
|
||||
if (!isVoidTag) {
|
||||
ret += `${options.value}</${options.name}>`
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import { CLIEngine } from 'eslint'
|
||||
import { Visitor } from 'babel-traverse'
|
||||
import { codeFrameError } from './utils'
|
||||
|
||||
const cli = new CLIEngine({
|
||||
baseConfig: {
|
||||
extends: ['plugin:taro/transformer']
|
||||
},
|
||||
useEslintrc: false,
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
legacyDecorators: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const eslintValidation: () => {
|
||||
visitor: Visitor
|
||||
} = () => {
|
||||
return {
|
||||
visitor: {
|
||||
Program (_, state) {
|
||||
const { file: { code } } = state
|
||||
const report = cli.executeOnText(code)
|
||||
if (report.errorCount > 0) {
|
||||
for (const result of report.results) {
|
||||
for (const msg of result.messages) {
|
||||
const err = codeFrameError({
|
||||
start: {
|
||||
line: msg.line,
|
||||
column: msg.column
|
||||
},
|
||||
end: {
|
||||
line: msg.endLine,
|
||||
column: msg.endColumn
|
||||
}
|
||||
}, msg.message)
|
||||
// tslint:disable-next-line
|
||||
console.warn('\n' + `ESLint(${msg.ruleId}) 错误:` + err.message + '\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,560 @@
|
|||
import traverse, { Binding, NodePath } from 'babel-traverse'
|
||||
import generate from 'babel-generator'
|
||||
import { prettyPrint } from 'html'
|
||||
import { transform as parse } from 'babel-core'
|
||||
import * as ts from 'typescript'
|
||||
import { Transformer } from './class'
|
||||
import { setting, findFirstIdentifierFromMemberExpression, isContainJSXElement, codeFrameError, isArrayMapCallExpression, getSuperClassCode } from './utils'
|
||||
import * as t from 'babel-types'
|
||||
import { DEFAULT_Component_SET, INTERNAL_SAFE_GET, TARO_PACKAGE_NAME, REDUX_PACKAGE_NAME, MOBX_PACKAGE_NAME, IMAGE_COMPONENTS, INTERNAL_INLINE_STYLE, THIRD_PARTY_COMPONENTS, INTERNAL_GET_ORIGNAL, setLoopOriginal, GEL_ELEMENT_BY_ID, lessThanSignPlacehold } from './constant'
|
||||
import { Adapters, setAdapter, Adapter } from './adapter'
|
||||
import { Options, setTransformOptions, buildBabelTransformOptions } from './options'
|
||||
import { get as safeGet } from 'lodash'
|
||||
|
||||
const template = require('babel-template')
|
||||
|
||||
function getIdsFromMemberProps (member: t.MemberExpression) {
|
||||
let ids: string[] = []
|
||||
const { object, property } = member
|
||||
if (t.isMemberExpression(object)) {
|
||||
ids = ids.concat(getIdsFromMemberProps(object))
|
||||
}
|
||||
if (t.isThisExpression(object)) {
|
||||
ids.push('this')
|
||||
}
|
||||
if (t.isIdentifier(object)) {
|
||||
ids.push(object.name)
|
||||
}
|
||||
if (t.isIdentifier(property)) {
|
||||
ids.push(property.name)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
/**
|
||||
* TS 编译器会把 class property 移到构造器,
|
||||
* 而小程序要求 `config` 和所有函数在初始化(after new Class)之后就收集到所有的函数和 config 信息,
|
||||
* 所以当如构造器里有 this.func = () => {...} 的形式,就给他转换成普通的 classProperty function
|
||||
* 如果有 config 就给他还原
|
||||
*/
|
||||
function resetTSClassProperty (body: (t.ClassMethod | t.ClassProperty)[]) {
|
||||
for (const method of body) {
|
||||
if (t.isClassMethod(method) && method.kind === 'constructor') {
|
||||
if (t.isBlockStatement(method.body)) {
|
||||
method.body.body = method.body.body.filter(statement => {
|
||||
if (t.isExpressionStatement(statement) && t.isAssignmentExpression(statement.expression)) {
|
||||
const expr = statement.expression
|
||||
const { left, right } = expr
|
||||
if (
|
||||
t.isMemberExpression(left) &&
|
||||
t.isThisExpression(left.object) &&
|
||||
t.isIdentifier(left.property)
|
||||
) {
|
||||
if (
|
||||
(t.isArrowFunctionExpression(right) || t.isFunctionExpression(right))
|
||||
||
|
||||
(left.property.name === 'config' && t.isObjectExpression(right))
|
||||
) {
|
||||
const classProp = t.classProperty(left.property, right)
|
||||
body.push(classProp)
|
||||
handleThirdPartyComponent(classProp)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findDeclarationScope (path: NodePath<t.Node>, id: t.Identifier) {
|
||||
const scopePath = path.findParent(p => !!p.scope.getOwnBindingIdentifier(id.name))
|
||||
if (scopePath) {
|
||||
return scopePath
|
||||
}
|
||||
throw codeFrameError(path.node, '该引用从未被定义')
|
||||
}
|
||||
|
||||
function buildFullPathThisPropsRef (id: t.Identifier, memberIds: string[], path: NodePath<t.Node>) {
|
||||
const scopePath = findDeclarationScope(path, id)
|
||||
const binding = scopePath.scope.getOwnBinding(id.name)
|
||||
if (binding) {
|
||||
const bindingPath = binding.path
|
||||
if (bindingPath.isVariableDeclarator()) {
|
||||
const dclId = bindingPath.get('id')
|
||||
const dclInit = bindingPath.get('init')
|
||||
let dclInitIds: string[] = []
|
||||
if (dclInit.isMemberExpression()) {
|
||||
dclInitIds = getIdsFromMemberProps(dclInit.node)
|
||||
if (dclId.isIdentifier()) {
|
||||
memberIds.shift()
|
||||
}
|
||||
if (dclInitIds[0] === 'this' && dclInitIds[1] === 'props') {
|
||||
return template(dclInitIds.concat(memberIds).join('.'))().expression
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleThirdPartyComponent (expr: t.ClassMethod | t.ClassProperty) {
|
||||
if (t.isClassProperty(expr) && expr.key.name === 'config' && t.isObjectExpression(expr.value)) {
|
||||
const properties = expr.value.properties
|
||||
for (const prop of properties) {
|
||||
if (
|
||||
t.isObjectProperty(prop) &&
|
||||
(t.isIdentifier(prop.key, { name: 'usingComponents' }) || t.isStringLiteral(prop.key, { value: 'usingComponents' })) &&
|
||||
t.isObjectExpression(prop.value)
|
||||
) {
|
||||
for (const value of prop.value.properties) {
|
||||
if (t.isObjectProperty(value)) {
|
||||
if (t.isStringLiteral(value.key)) {
|
||||
THIRD_PARTY_COMPONENTS.add(value.key.value)
|
||||
}
|
||||
if (t.isIdentifier(value.key)) {
|
||||
THIRD_PARTY_COMPONENTS.add(value.key.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Result {
|
||||
template: string
|
||||
components: {
|
||||
name: string,
|
||||
path: string,
|
||||
type: string
|
||||
}[],
|
||||
componentProperies: string[]
|
||||
}
|
||||
|
||||
interface TransformResult extends Result {
|
||||
code: string,
|
||||
ast: t.File
|
||||
}
|
||||
|
||||
export default function transform (options: Options): TransformResult {
|
||||
if (options.adapter) {
|
||||
setAdapter(options.adapter)
|
||||
}
|
||||
if (Adapter.type === Adapters.swan) {
|
||||
setLoopOriginal('privateOriginal')
|
||||
}
|
||||
THIRD_PARTY_COMPONENTS.clear()
|
||||
const code = options.isTyped
|
||||
? ts.transpile(options.code, {
|
||||
jsx: ts.JsxEmit.Preserve,
|
||||
target: ts.ScriptTarget.ESNext,
|
||||
importHelpers: true,
|
||||
noEmitHelpers: true
|
||||
})
|
||||
: options.code
|
||||
options.env = Object.assign({ 'process.env.TARO_ENV': options.adapter || 'weapp' }, options.env || {})
|
||||
setTransformOptions(options)
|
||||
setting.sourceCode = code
|
||||
// babel-traverse 无法生成 Hub
|
||||
// 导致 Path#getSource|buildCodeFrameError 都无法直接使用
|
||||
// 原因大概是 babylon.parse 没有生成 File 实例导致 scope 和 path 原型上都没有 `file`
|
||||
// 将来升级到 babel@7 可以直接用 parse 而不是 transform
|
||||
const ast = parse(code, buildBabelTransformOptions()).ast as t.File
|
||||
if (options.isNormal) {
|
||||
return { ast } as any
|
||||
}
|
||||
// transformFromAst(ast, code)
|
||||
let result
|
||||
const componentSourceMap = new Map<string, string[]>()
|
||||
const imageSource = new Set<string>()
|
||||
const importSources = new Set<string>()
|
||||
let componentProperies: string[] = []
|
||||
let mainClass!: NodePath<t.ClassDeclaration>
|
||||
let storeName!: string
|
||||
let renderMethod!: NodePath<t.ClassMethod>
|
||||
let isImportTaro = false
|
||||
traverse(ast, {
|
||||
TemplateLiteral (path) {
|
||||
const nodes: t.Expression[] = []
|
||||
const { quasis, expressions } = path.node
|
||||
let index = 0
|
||||
if (path.parentPath.isTaggedTemplateExpression()) {
|
||||
return
|
||||
}
|
||||
for (const elem of quasis) {
|
||||
if (elem.value.cooked) {
|
||||
nodes.push(t.stringLiteral(elem.value.cooked))
|
||||
}
|
||||
|
||||
if (index < expressions.length) {
|
||||
const expr = expressions[index++]
|
||||
if (!t.isStringLiteral(expr, { value: '' })) {
|
||||
nodes.push(expr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// + 号连接符必须保证第一和第二个 node 都是字符串
|
||||
if (!t.isStringLiteral(nodes[0]) && !t.isStringLiteral(nodes[1])) {
|
||||
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)
|
||||
},
|
||||
ClassDeclaration (path) {
|
||||
mainClass = path
|
||||
const superClass = getSuperClassCode(path)
|
||||
if (superClass) {
|
||||
try {
|
||||
componentProperies = transform({
|
||||
isRoot: false,
|
||||
isApp: false,
|
||||
code: superClass.code,
|
||||
isTyped: true,
|
||||
sourcePath: superClass.sourcePath,
|
||||
outputPath: superClass.sourcePath
|
||||
}).componentProperies
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
},
|
||||
ClassExpression (path) {
|
||||
mainClass = path as any
|
||||
},
|
||||
ClassMethod (path) {
|
||||
if (t.isIdentifier(path.node.key) && path.node.key.name === 'render') {
|
||||
renderMethod = path
|
||||
}
|
||||
},
|
||||
IfStatement (path) {
|
||||
const consequent = path.get('consequent')
|
||||
if (!consequent.isBlockStatement()) {
|
||||
consequent.replaceWith(
|
||||
t.blockStatement([
|
||||
consequent.node as any
|
||||
])
|
||||
)
|
||||
}
|
||||
},
|
||||
CallExpression (path) {
|
||||
const callee = path.get('callee')
|
||||
if (isContainJSXElement(path)) {
|
||||
return
|
||||
}
|
||||
if (callee.isReferencedMemberExpression()) {
|
||||
const id = findFirstIdentifierFromMemberExpression(callee.node)
|
||||
const property = callee.node.property
|
||||
if (t.isIdentifier(property) && property.name.startsWith('on')) {
|
||||
const funcExpr = path.findParent(p => p.isFunctionExpression())
|
||||
if (funcExpr && funcExpr.isFunctionExpression()) {
|
||||
const taroAPI = funcExpr.findParent(p => p.isCallExpression() && t.isMemberExpression(p.node.callee) && t.isIdentifier(p.node.callee.object, { name: 'Taro' }))
|
||||
if (taroAPI && taroAPI.isCallExpression()) {
|
||||
throw codeFrameError(funcExpr.node, '在回调函数使用从 props 传递的函数时,请把回调函数改造为箭头函数并一直使用 `this` 取值')
|
||||
}
|
||||
}
|
||||
}
|
||||
const calleeIds = getIdsFromMemberProps(callee.node)
|
||||
if (t.isIdentifier(id) && id.name.startsWith('on') && Adapters.alipay !== Adapter.type) {
|
||||
const fullPath = buildFullPathThisPropsRef(id, calleeIds, path)
|
||||
if (fullPath) {
|
||||
path.replaceWith(
|
||||
t.callExpression(
|
||||
fullPath,
|
||||
path.node.arguments
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callee.isReferencedIdentifier()) {
|
||||
const id = callee.node
|
||||
const ids = [id.name]
|
||||
if (t.isIdentifier(id) && id.name.startsWith('on')) {
|
||||
const funcExpr = path.findParent(p => p.isFunctionExpression())
|
||||
if (funcExpr && funcExpr.isFunctionExpression()) {
|
||||
const taroAPI = funcExpr.findParent(p => p.isCallExpression() && t.isMemberExpression(p.node.callee) && t.isIdentifier(p.node.callee.object, { name: 'Taro' }))
|
||||
if (taroAPI && taroAPI.isCallExpression()) {
|
||||
throw codeFrameError(funcExpr.node, '在回调函数使用从 props 传递的函数时,请把回调函数改造为箭头函数并一直使用 `this` 取值')
|
||||
}
|
||||
}
|
||||
const fullPath = buildFullPathThisPropsRef(id, ids, path)
|
||||
if (fullPath) {
|
||||
path.replaceWith(
|
||||
t.callExpression(
|
||||
fullPath,
|
||||
path.node.arguments
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// JSXIdentifier (path) {
|
||||
// const parentPath = path.parentPath
|
||||
// if (!parentPath.isJSXAttribute()) {
|
||||
// return
|
||||
// }
|
||||
// const element = parentPath.parentPath
|
||||
// if (!element.isJSXOpeningElement()) {
|
||||
// return
|
||||
// }
|
||||
// const elementName = element.get('name')
|
||||
// if (!elementName.isJSXIdentifier()) {
|
||||
// return
|
||||
// }
|
||||
// if (DEFAULT_Component_SET.has(elementName.node.name)) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// const expr = parentPath.get('value.expression')
|
||||
|
||||
// },
|
||||
JSXElement (path) {
|
||||
const assignment = path.findParent(p => p.isAssignmentExpression())
|
||||
if (assignment && assignment.isAssignmentExpression() && !options.isTyped) {
|
||||
const left = assignment.node.left
|
||||
if (t.isIdentifier(left)) {
|
||||
const binding = assignment.scope.getBinding(left.name)
|
||||
if (binding && binding.scope === assignment.scope) {
|
||||
if (binding.path.isVariableDeclarator()) {
|
||||
binding.path.node.init = path.node
|
||||
assignment.remove()
|
||||
} else {
|
||||
throw codeFrameError(path.node, '同一个作用域的JSX 变量延时赋值没有意义。详见:https://github.com/NervJS/taro/issues/550')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const switchStatement = path.findParent(p => p.isSwitchStatement())
|
||||
if (switchStatement && switchStatement.isSwitchStatement()) {
|
||||
const { discriminant, cases } = switchStatement.node
|
||||
const ifStatement = cases.map((Case, index) => {
|
||||
const [ consequent ] = Case.consequent
|
||||
if (!t.isBlockStatement(consequent)) {
|
||||
throw codeFrameError(switchStatement.node, '含有 JSX 的 switch case 语句必须每种情况都用花括号 `{}` 包裹结果')
|
||||
}
|
||||
const block = t.blockStatement(consequent.body.filter(b => !t.isBreakStatement(b)))
|
||||
if (index !== cases.length - 1 && t.isNullLiteral(Case.test)) {
|
||||
throw codeFrameError(Case, '含有 JSX 的 switch case 语句只有最后一个 case 才能是 default')
|
||||
}
|
||||
const test = Case.test === null ? t.nullLiteral() : t.binaryExpression('===', discriminant, Case.test)
|
||||
return { block, test }
|
||||
}).reduceRight((ifStatement, item) => {
|
||||
if (t.isNullLiteral(item.test)) {
|
||||
ifStatement.alternate = item.block
|
||||
return ifStatement
|
||||
}
|
||||
const newStatement = t.ifStatement(
|
||||
item.test,
|
||||
item.block,
|
||||
t.isBooleanLiteral(ifStatement.test, { value: false })
|
||||
? ifStatement.alternate
|
||||
: ifStatement
|
||||
)
|
||||
return newStatement
|
||||
}, t.ifStatement(t.booleanLiteral(false), t.blockStatement([])))
|
||||
|
||||
switchStatement.insertAfter(ifStatement)
|
||||
switchStatement.remove()
|
||||
}
|
||||
const isForStatement = (p) => p && (p.isForStatement() || p.isForInStatement() || p.isForOfStatement())
|
||||
|
||||
const forStatement = path.findParent(isForStatement)
|
||||
if (isForStatement(forStatement)) {
|
||||
throw codeFrameError(forStatement.node, '不行使用 for 循环操作 JSX 元素,详情:https://github.com/NervJS/taro/blob/master/packages/eslint-plugin-taro/docs/manipulate-jsx-as-array.md')
|
||||
}
|
||||
|
||||
const loopCallExpr = path.findParent(p => isArrayMapCallExpression(p))
|
||||
if (loopCallExpr && loopCallExpr.isCallExpression()) {
|
||||
const [ func ] = loopCallExpr.node.arguments
|
||||
if (t.isArrowFunctionExpression(func) && !t.isBlockStatement(func.body)) {
|
||||
func.body = t.blockStatement([
|
||||
t.returnStatement(func.body)
|
||||
])
|
||||
}
|
||||
}
|
||||
},
|
||||
JSXOpeningElement (path) {
|
||||
const { name } = path.node.name as t.JSXIdentifier
|
||||
if (name === 'Provider') {
|
||||
const modules = path.scope.getAllBindings('module')
|
||||
const providerBinding = Object.values(modules).some((m: Binding) => m.identifier.name === 'Provider')
|
||||
if (providerBinding) {
|
||||
path.node.name = t.jSXIdentifier('View')
|
||||
const store = path.node.attributes.find(attr => attr.name.name === 'store')
|
||||
if (store && t.isJSXExpressionContainer(store.value) && t.isIdentifier(store.value.expression)) {
|
||||
storeName = store.value.expression.name
|
||||
}
|
||||
path.node.attributes = []
|
||||
}
|
||||
}
|
||||
|
||||
if (IMAGE_COMPONENTS.has(name)) {
|
||||
for (const attr of path.node.attributes) {
|
||||
if (
|
||||
attr.name.name === 'src'
|
||||
) {
|
||||
if (t.isStringLiteral(attr.value)) {
|
||||
imageSource.add(attr.value.value)
|
||||
} else if (t.isJSXExpressionContainer(attr.value)) {
|
||||
if (t.isStringLiteral(attr.value.expression)) {
|
||||
imageSource.add(attr.value.expression.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
JSXAttribute (path) {
|
||||
const { name, value } = path.node
|
||||
if (!t.isJSXIdentifier(name) || value === null || t.isStringLiteral(value) || t.isJSXElement(value)) {
|
||||
return
|
||||
}
|
||||
|
||||
const expr = value.expression as any
|
||||
const exprPath = path.get('value.expression')
|
||||
const classDecl = path.findParent(p => p.isClassDeclaration())
|
||||
const classDeclName = classDecl && classDecl.isClassDeclaration() && safeGet(classDecl, 'node.id.name', '')
|
||||
let isConverted = false
|
||||
if (classDeclName) {
|
||||
isConverted = classDeclName === '_C' || classDeclName.endsWith('Tmpl')
|
||||
}
|
||||
if (!t.isBinaryExpression(expr, { operator: '+' }) && !t.isLiteral(expr) && name.name === 'style' && !isConverted) {
|
||||
const jsxID = path.findParent(p => p.isJSXOpeningElement()).get('name')
|
||||
if (jsxID && jsxID.isJSXIdentifier() && DEFAULT_Component_SET.has(jsxID.node.name)) {
|
||||
exprPath.replaceWith(
|
||||
t.callExpression(t.identifier(INTERNAL_INLINE_STYLE), [expr])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (name.name.startsWith('on')) {
|
||||
if (exprPath.isReferencedIdentifier()) {
|
||||
const ids = [expr.name]
|
||||
const fullPath = buildFullPathThisPropsRef(expr, ids, path)
|
||||
if (fullPath) {
|
||||
exprPath.replaceWith(fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
if (exprPath.isReferencedMemberExpression()) {
|
||||
const id = findFirstIdentifierFromMemberExpression(expr)
|
||||
const ids = getIdsFromMemberProps(expr)
|
||||
if (t.isIdentifier(id)) {
|
||||
const fullPath = buildFullPathThisPropsRef(id, ids, path)
|
||||
if (fullPath) {
|
||||
exprPath.replaceWith(fullPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @TODO: bind 的处理待定
|
||||
}
|
||||
},
|
||||
ImportDeclaration (path) {
|
||||
const source = path.node.source.value
|
||||
if (importSources.has(source)) {
|
||||
throw codeFrameError(path.node, '无法在同一文件重复 import 相同的包。')
|
||||
} else {
|
||||
importSources.add(source)
|
||||
}
|
||||
const names: string[] = []
|
||||
if (source === TARO_PACKAGE_NAME) {
|
||||
isImportTaro = true
|
||||
path.node.specifiers.push(
|
||||
t.importSpecifier(t.identifier(INTERNAL_SAFE_GET), t.identifier(INTERNAL_SAFE_GET)),
|
||||
t.importSpecifier(t.identifier(INTERNAL_GET_ORIGNAL), t.identifier(INTERNAL_GET_ORIGNAL)),
|
||||
t.importSpecifier(t.identifier(INTERNAL_INLINE_STYLE), t.identifier(INTERNAL_INLINE_STYLE)),
|
||||
t.importSpecifier(t.identifier(GEL_ELEMENT_BY_ID), t.identifier(GEL_ELEMENT_BY_ID))
|
||||
)
|
||||
}
|
||||
if (
|
||||
source === REDUX_PACKAGE_NAME || source === MOBX_PACKAGE_NAME
|
||||
) {
|
||||
path.node.specifiers.forEach((s, index, specs) => {
|
||||
if (s.local.name === 'Provider') {
|
||||
specs.splice(index, 1)
|
||||
specs.push(
|
||||
t.importSpecifier(t.identifier('setStore'), t.identifier('setStore'))
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
path.traverse({
|
||||
ImportDefaultSpecifier (path) {
|
||||
const name = path.node.local.name
|
||||
DEFAULT_Component_SET.has(name) || names.push(name)
|
||||
},
|
||||
ImportSpecifier (path) {
|
||||
const name = path.node.imported.name
|
||||
DEFAULT_Component_SET.has(name) || names.push(name)
|
||||
if (source === TARO_PACKAGE_NAME && name === 'Component') {
|
||||
path.node.local = t.identifier('__BaseComponent')
|
||||
}
|
||||
}
|
||||
})
|
||||
componentSourceMap.set(source, names)
|
||||
}
|
||||
})
|
||||
|
||||
if (!isImportTaro) {
|
||||
ast.program.body.unshift(
|
||||
t.importDeclaration([
|
||||
t.importDefaultSpecifier(t.identifier('Taro')),
|
||||
t.importSpecifier(t.identifier(INTERNAL_SAFE_GET), t.identifier(INTERNAL_SAFE_GET)),
|
||||
t.importSpecifier(t.identifier(INTERNAL_GET_ORIGNAL), t.identifier(INTERNAL_GET_ORIGNAL)),
|
||||
t.importSpecifier(t.identifier(INTERNAL_INLINE_STYLE), t.identifier(INTERNAL_INLINE_STYLE))
|
||||
], t.stringLiteral('@tarojs/taro'))
|
||||
)
|
||||
}
|
||||
|
||||
if (!mainClass) {
|
||||
throw new Error('未找到 Taro.Component 的类定义')
|
||||
}
|
||||
|
||||
mainClass.node.body.body.forEach(handleThirdPartyComponent)
|
||||
const storeBinding = mainClass.scope.getBinding(storeName)
|
||||
mainClass.scope.rename('Component', '__BaseComponent')
|
||||
if (storeBinding) {
|
||||
const statementPath = storeBinding.path.getStatementParent()
|
||||
if (statementPath) {
|
||||
ast.program.body.forEach((node, index, body) => {
|
||||
if (node === statementPath.node) {
|
||||
body.splice(index + 1, 0, t.expressionStatement(
|
||||
t.callExpression(t.identifier('setStore'), [
|
||||
t.identifier(storeName)
|
||||
])
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
resetTSClassProperty(mainClass.node.body.body)
|
||||
if (options.isApp) {
|
||||
renderMethod.replaceWith(
|
||||
t.classMethod('method', t.identifier('_createData'), [], t.blockStatement([]))
|
||||
)
|
||||
return { ast } as TransformResult
|
||||
}
|
||||
result = new Transformer(mainClass, options.sourcePath, componentProperies).result
|
||||
result.code = generate(ast).code
|
||||
result.ast = ast
|
||||
const lessThanSignReg = new RegExp(lessThanSignPlacehold, 'g')
|
||||
result.compressedTemplate = result.template
|
||||
result.template = prettyPrint(result.template, {
|
||||
max_char: 0,
|
||||
unformatted: process.env.NODE_ENV === 'test' ? [] : ['text']
|
||||
})
|
||||
result.template = result.template.replace(lessThanSignReg, '<')
|
||||
result.imageSrcs = Array.from(imageSource)
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { NodePath } from 'babel-traverse'
|
||||
import * as t from 'babel-types'
|
||||
|
||||
interface LoopRef {
|
||||
id: string | t.Expression,
|
||||
fn: t.FunctionExpression | t.ArrowFunctionExpression | t.MemberExpression,
|
||||
type: 'component' | 'dom',
|
||||
component: NodePath<t.JSXElement>
|
||||
}
|
|
@ -0,0 +1,287 @@
|
|||
import generate from 'babel-generator'
|
||||
import { NodePath } from 'babel-traverse'
|
||||
import * as t from 'babel-types'
|
||||
import { kebabCase } from 'lodash'
|
||||
import {
|
||||
DEFAULT_Component_SET,
|
||||
SPECIAL_COMPONENT_PROPS,
|
||||
swanSpecialAttrs,
|
||||
THIRD_PARTY_COMPONENTS,
|
||||
TRANSFORM_COMPONENT_PROPS,
|
||||
lessThanSignPlacehold
|
||||
} from './constant'
|
||||
import { createHTMLElement } from './create-html-element'
|
||||
import { codeFrameError, decodeUnicode } from './utils'
|
||||
import { Adapter, Adapters } from './adapter'
|
||||
|
||||
export function isStartWithWX (str: string) {
|
||||
return str[0] === 'w' && str[1] === 'x'
|
||||
}
|
||||
|
||||
const specialComponentName = ['block', 'Block', 'slot', 'Slot']
|
||||
|
||||
export function removeJSXThisProperty (path: NodePath<t.ThisExpression>) {
|
||||
if (!path.parentPath.isCallExpression()) {
|
||||
const p = path.getSibling('property')
|
||||
if (
|
||||
p.isIdentifier({ name: 'props' }) ||
|
||||
p.isIdentifier({ name: 'state' })
|
||||
) {
|
||||
path.parentPath.replaceWithSourceString('this')
|
||||
} else {
|
||||
path.parentPath.replaceWith(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function findJSXAttrByName (attrs: t.JSXAttribute[], name: string) {
|
||||
for (const attr of attrs) {
|
||||
if (!t.isJSXIdentifier(attr.name)) {
|
||||
break
|
||||
}
|
||||
if (attr.name.name === name) {
|
||||
return attr
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function buildRefTemplate (name: string, refName?: string, loop?: boolean, key?: t.JSXAttribute) {
|
||||
const attrs = [
|
||||
t.jSXAttribute(t.jSXIdentifier('is'), t.stringLiteral(name)),
|
||||
t.jSXAttribute(t.jSXIdentifier('data'), t.stringLiteral(`{{...${refName ? `${loop ? '' : '$$'}${refName}` : '__data'}}}`))
|
||||
]
|
||||
if (key) {
|
||||
attrs.push(key)
|
||||
}
|
||||
return t.jSXElement(
|
||||
t.jSXOpeningElement(t.jSXIdentifier('template'), attrs),
|
||||
t.jSXClosingElement(t.jSXIdentifier('template')),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
export function buildJSXAttr (name: string, value: t.Identifier | t.Expression) {
|
||||
return t.jSXAttribute(t.jSXIdentifier(name), t.jSXExpressionContainer(value))
|
||||
}
|
||||
|
||||
export function newJSXIfAttr (
|
||||
jsx: t.JSXElement,
|
||||
value: t.Identifier | t.Expression
|
||||
) {
|
||||
jsx.openingElement.attributes.push(buildJSXAttr(Adapter.if, value))
|
||||
}
|
||||
|
||||
export function setJSXAttr (
|
||||
jsx: t.JSXElement,
|
||||
name: string,
|
||||
value?: t.StringLiteral | t.JSXExpressionContainer | t.JSXElement,
|
||||
path?: NodePath<t.JSXElement>
|
||||
) {
|
||||
const element = jsx.openingElement
|
||||
if (!t.isJSXIdentifier(element.name)) {
|
||||
return
|
||||
}
|
||||
if (element.name.name === 'Block' || element.name.name === 'block' || !path) {
|
||||
jsx.openingElement.attributes.push(
|
||||
t.jSXAttribute(t.jSXIdentifier(name), value)
|
||||
)
|
||||
} else {
|
||||
const block = buildBlockElement()
|
||||
setJSXAttr(block, name, value)
|
||||
block.children = [jsx]
|
||||
path.node = block
|
||||
}
|
||||
}
|
||||
|
||||
export function isAllLiteral (...args) {
|
||||
return args.every(p => t.isLiteral(p))
|
||||
}
|
||||
|
||||
export function buildBlockElement () {
|
||||
return t.jSXElement(
|
||||
t.jSXOpeningElement(t.jSXIdentifier('block'), []),
|
||||
t.jSXClosingElement(t.jSXIdentifier('block')),
|
||||
[]
|
||||
)
|
||||
}
|
||||
|
||||
function parseJSXChildren (
|
||||
children: (t.JSXElement | t.JSXText | t.JSXExpressionContainer)[]
|
||||
): string {
|
||||
return children
|
||||
.filter(child => {
|
||||
return !(t.isJSXText(child) && child.value.trim() === '')
|
||||
})
|
||||
.reduce((str, child) => {
|
||||
if (t.isJSXText(child)) {
|
||||
const strings: string[] = []
|
||||
child.value.split(/(\r?\n\s*)/).forEach((val) => {
|
||||
const value = val.replace(/\u00a0/g, ' ').trimLeft()
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
if (value.startsWith('\n')) {
|
||||
return
|
||||
}
|
||||
strings.push(value)
|
||||
})
|
||||
return str + strings.join('')
|
||||
}
|
||||
if (t.isJSXElement(child)) {
|
||||
return str + parseJSXElement(child)
|
||||
}
|
||||
if (t.isJSXExpressionContainer(child)) {
|
||||
if (t.isJSXElement(child.expression)) {
|
||||
return str + parseJSXElement(child.expression)
|
||||
}
|
||||
return str + `{${
|
||||
decodeUnicode(
|
||||
generate(child, {
|
||||
quotes: 'single',
|
||||
jsonCompatibleStrings: true
|
||||
})
|
||||
.code
|
||||
)
|
||||
.replace(/(this\.props\.)|(this\.state\.)/g, '')
|
||||
.replace(/(props\.)|(state\.)/g, '')
|
||||
.replace(/this\./g, '')
|
||||
.replace(/</g, lessThanSignPlacehold)
|
||||
}}`
|
||||
}
|
||||
return str
|
||||
}, '')
|
||||
}
|
||||
|
||||
export function parseJSXElement (element: t.JSXElement): string {
|
||||
const children = element.children
|
||||
const { attributes, name } = element.openingElement
|
||||
const TRIGGER_OBSERER = Adapter.type === Adapters.swan ? 'privateTriggerObserer' : '__triggerObserer'
|
||||
if (t.isJSXMemberExpression(name)) {
|
||||
throw codeFrameError(name.loc, '暂不支持 JSX 成员表达式')
|
||||
}
|
||||
const componentName = name.name
|
||||
const isDefaultComponent = DEFAULT_Component_SET.has(componentName)
|
||||
const componentSpecialProps = SPECIAL_COMPONENT_PROPS.get(componentName)
|
||||
const componentTransfromProps = TRANSFORM_COMPONENT_PROPS.get(Adapter.type)
|
||||
let hasElseAttr = false
|
||||
attributes.forEach((a, index) => {
|
||||
if (a.name.name === Adapter.else && !['block', 'Block'].includes(componentName) && !isDefaultComponent) {
|
||||
hasElseAttr = true
|
||||
attributes.splice(index, 1)
|
||||
}
|
||||
})
|
||||
if (hasElseAttr) {
|
||||
return createHTMLElement({
|
||||
name: 'block',
|
||||
attributes: {
|
||||
[Adapter.else]: true
|
||||
},
|
||||
value: parseJSXChildren([element])
|
||||
})
|
||||
}
|
||||
let attributesTrans = {}
|
||||
if (attributes.length) {
|
||||
attributesTrans = attributes.reduce((obj, attr) => {
|
||||
if (t.isJSXSpreadAttribute(attr)) {
|
||||
throw codeFrameError(attr.loc, 'JSX 参数暂不支持 ...spread 表达式')
|
||||
}
|
||||
let name = attr.name.name
|
||||
if (DEFAULT_Component_SET.has(componentName)) {
|
||||
if (name === 'className') {
|
||||
name = 'class'
|
||||
}
|
||||
}
|
||||
let value: string | boolean = true
|
||||
let attrValue = attr.value
|
||||
if (typeof name === 'string') {
|
||||
const isAlipayEvent = Adapter.type === Adapters.alipay && /(^on[A-Z_])|(^catch[A-Z_])/.test(name)
|
||||
if (t.isStringLiteral(attrValue)) {
|
||||
value = attrValue.value
|
||||
} else if (t.isJSXExpressionContainer(attrValue)) {
|
||||
let isBindEvent =
|
||||
(name.startsWith('bind') && name !== 'bind') || (name.startsWith('catch') && name !== 'catch')
|
||||
let code = decodeUnicode(generate(attrValue.expression, {
|
||||
quotes: 'single',
|
||||
concise: true
|
||||
}).code)
|
||||
.replace(/"/g, "'")
|
||||
.replace(/(this\.props\.)|(this\.state\.)/g, '')
|
||||
.replace(/this\./g, '')
|
||||
if (
|
||||
Adapters.swan === Adapter.type &&
|
||||
code !== 'true' &&
|
||||
code !== 'false' &&
|
||||
swanSpecialAttrs[componentName] &&
|
||||
swanSpecialAttrs[componentName].includes(name)
|
||||
) {
|
||||
value = `{= ${code} =}`
|
||||
} else {
|
||||
if (Adapter.key === name) {
|
||||
const splitCode = code.split('.')
|
||||
if (splitCode.length > 1) {
|
||||
value = splitCode.slice(1).join('.')
|
||||
} else {
|
||||
value = code
|
||||
}
|
||||
} else {
|
||||
value = isBindEvent || isAlipayEvent ? code : `{{${code}}}`
|
||||
}
|
||||
}
|
||||
if (Adapter.type === Adapters.swan && name === Adapter.for) {
|
||||
value = code
|
||||
}
|
||||
if (t.isStringLiteral(attrValue.expression)) {
|
||||
value = attrValue.expression.value
|
||||
}
|
||||
} else if (attrValue === null && name !== Adapter.else) {
|
||||
value = `{{true}}`
|
||||
}
|
||||
if (THIRD_PARTY_COMPONENTS.has(componentName) && /^bind/.test(name) && name.includes('-')) {
|
||||
name = name.replace(/^bind/, 'bind:')
|
||||
}
|
||||
if (componentTransfromProps && componentTransfromProps[componentName]) {
|
||||
const transfromProps = componentTransfromProps[componentName]
|
||||
Object.keys(transfromProps).forEach(oriName => {
|
||||
if (transfromProps.hasOwnProperty(name as string)) {
|
||||
name = transfromProps[oriName]
|
||||
}
|
||||
})
|
||||
}
|
||||
if ((componentName === 'Input' || componentName === 'input') && name === 'maxLength') {
|
||||
obj['maxlength'] = value
|
||||
} else if (
|
||||
componentSpecialProps && componentSpecialProps.has(name) ||
|
||||
name.startsWith('__fn_') ||
|
||||
isAlipayEvent
|
||||
) {
|
||||
obj[name] = value
|
||||
} else {
|
||||
obj[isDefaultComponent && !name.includes('-') && !name.includes(':') ? kebabCase(name) : name] = value
|
||||
}
|
||||
}
|
||||
if (!isDefaultComponent && !specialComponentName.includes(componentName)) {
|
||||
obj[TRIGGER_OBSERER] = '{{ _triggerObserer }}'
|
||||
}
|
||||
return obj
|
||||
}, {})
|
||||
} else if (!isDefaultComponent && !specialComponentName.includes(componentName)) {
|
||||
attributesTrans[TRIGGER_OBSERER] = '{{ _triggerObserer }}'
|
||||
}
|
||||
|
||||
return createHTMLElement({
|
||||
name: kebabCase(componentName),
|
||||
attributes: attributesTrans,
|
||||
value: parseJSXChildren(children)
|
||||
})
|
||||
}
|
||||
|
||||
export function generateHTMLTemplate (template: t.JSXElement, name: string) {
|
||||
return createHTMLElement({
|
||||
name: 'template',
|
||||
attributes: {
|
||||
name
|
||||
},
|
||||
value: parseJSXElement(template)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
export enum Lifecycle {
|
||||
constructor = 'constructor',
|
||||
componentWillMount = 'componentWillMount',
|
||||
componentDidMount = 'componentDidMount',
|
||||
componentWillUpdate = 'componentWillUpdate',
|
||||
componentDidUpdate = 'componentDidUpdate',
|
||||
componentWillUnmount = 'componentWillUnmount',
|
||||
componentDidCatch = 'componentDidCatch',
|
||||
componentDidShow = 'componentDidShow',
|
||||
componentDidHide = 'componentDidHide',
|
||||
componentDidAttached = 'componentDidAttached',
|
||||
componentDidMoved = 'componentDidMoved',
|
||||
shouldComponentUpdate = 'shouldComponentUpdate',
|
||||
componentWillReceiveProps = 'componentWillReceiveProps'
|
||||
}
|
||||
|
||||
export const PageLifecycle = {
|
||||
[Lifecycle.componentDidMount]: 'onLaunch',
|
||||
[Lifecycle.componentWillMount]: 'onLoad',
|
||||
[Lifecycle.componentWillUnmount]: 'onUnload',
|
||||
[Lifecycle.componentDidShow]: 'onShow',
|
||||
[Lifecycle.componentDidHide]: 'onHide'
|
||||
}
|
||||
|
||||
export const ComponentLifeCycle = {
|
||||
[Lifecycle.componentWillMount]: 'created',
|
||||
[Lifecycle.componentDidAttached]: 'attached',
|
||||
[Lifecycle.componentDidMount]: 'ready',
|
||||
[Lifecycle.componentDidMoved]: 'moved',
|
||||
[Lifecycle.componentWillUnmount]: 'detached'
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
import { NodePath } from 'babel-traverse'
|
||||
import * as t from 'babel-types'
|
||||
import {
|
||||
newJSXIfAttr,
|
||||
reverseBoolean,
|
||||
findIdentifierFromStatement,
|
||||
isEmptyDeclarator,
|
||||
codeFrameError,
|
||||
isBlockIfStatement,
|
||||
isContainFunction,
|
||||
setTemplate,
|
||||
buildConstVariableDeclaration
|
||||
} from './utils'
|
||||
import {
|
||||
setJSXAttr,
|
||||
buildBlockElement
|
||||
} from './jsx'
|
||||
import { LOOP_STATE } from './constant'
|
||||
import { Adapter } from './adapter'
|
||||
|
||||
// @TODO
|
||||
// 重构 parseRender 和 parseLoop 失败
|
||||
// 尚不清楚 babel 的 state 和 context 传参机制
|
||||
// 目前先写两份代码,有时间看看 babel 具体对 state 和 context 做了什么导致传参失败
|
||||
export function parseLoopBody (
|
||||
body: NodePath<t.BlockStatement>,
|
||||
jsxDeclarations: Set<NodePath<t.Node>>,
|
||||
// @TODO
|
||||
// 把 templates 换成 Map 可以支持 shalow variables declared
|
||||
// 现在先用 ESLint 的 no-shalow 顶着
|
||||
templates: Map<string, t.JSXElement>,
|
||||
loopScopes: Set<string>,
|
||||
finalReturnElement: t.JSXElement,
|
||||
returnedPaths: NodePath<t.Node>[]
|
||||
) {
|
||||
const bodyScope = body.scope
|
||||
body.traverse({
|
||||
JSXElement (jsxElementPath) {
|
||||
const parentNode = jsxElementPath.parent
|
||||
const parentPath = jsxElementPath.parentPath
|
||||
const isFinalReturn = jsxElementPath.getFunctionParent().isClassMethod()
|
||||
const isJSXChildren = t.isJSXElement(parentNode)
|
||||
if (!isJSXChildren) {
|
||||
let statementParent = jsxElementPath.getStatementParent()
|
||||
if (
|
||||
!(
|
||||
statementParent.isVariableDeclaration() ||
|
||||
statementParent.isExpressionStatement()
|
||||
)
|
||||
) {
|
||||
statementParent = statementParent.findParent(
|
||||
s => s.isVariableDeclaration() || s.isExpressionStatement()
|
||||
) as NodePath<t.Statement>
|
||||
}
|
||||
jsxDeclarations.add(statementParent)
|
||||
if (t.isVariableDeclarator(parentNode)) {
|
||||
if (statementParent) {
|
||||
const name = findIdentifierFromStatement(statementParent.node as t.VariableDeclaration)
|
||||
// setTemplate(name, path, templates)
|
||||
name && templates.set(name, jsxElementPath.node)
|
||||
}
|
||||
} else if (t.isLogicalExpression(parentNode)) {
|
||||
const { left, operator } = parentNode
|
||||
if (operator === '&&') {
|
||||
if (t.isExpression(left)) {
|
||||
newJSXIfAttr(jsxElementPath.node, left)
|
||||
parentPath.replaceWith(jsxElementPath.node)
|
||||
if (statementParent) {
|
||||
const name = findIdentifierFromStatement(statementParent.node as t.VariableDeclaration)
|
||||
setTemplate(name, jsxElementPath, templates)
|
||||
// name && templates.set(name, path.node)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (t.isConditionalExpression(parentNode)) {
|
||||
const { test, consequent, alternate } = parentNode
|
||||
const block = buildBlockElement()
|
||||
if (t.isJSXElement(consequent) && t.isLiteral(alternate)) {
|
||||
const { value, confident } = parentPath.get('alternate').evaluate()
|
||||
if (confident && !value) {
|
||||
newJSXIfAttr(block, test)
|
||||
block.children = [ jsxElementPath.node ]
|
||||
// newJSXIfAttr(jsxElementPath.node, test)
|
||||
parentPath.replaceWith(block)
|
||||
if (statementParent) {
|
||||
const name = findIdentifierFromStatement(
|
||||
statementParent.node as t.VariableDeclaration
|
||||
)
|
||||
setTemplate(name, jsxElementPath, templates)
|
||||
// name && templates.set(name, path.node)
|
||||
}
|
||||
}
|
||||
} else if (t.isLiteral(consequent) && t.isJSXElement(consequent)) {
|
||||
if (t.isNullLiteral(consequent)) {
|
||||
newJSXIfAttr(block, reverseBoolean(test))
|
||||
// newJSXIfAttr(jsxElementPath.node, reverseBoolean(test))
|
||||
parentPath.replaceWith(block)
|
||||
if (statementParent) {
|
||||
const name = findIdentifierFromStatement(
|
||||
statementParent.node as t.VariableDeclaration
|
||||
)
|
||||
setTemplate(name, jsxElementPath, templates)
|
||||
// name && templates.set(name, path.node)
|
||||
}
|
||||
}
|
||||
} else if (t.isJSXElement(consequent) && t.isJSXElement(alternate)) {
|
||||
const block2 = buildBlockElement()
|
||||
block.children = [consequent]
|
||||
newJSXIfAttr(block, test)
|
||||
setJSXAttr(block2, Adapter.else)
|
||||
block2.children = [alternate]
|
||||
const parentBlock = buildBlockElement()
|
||||
parentBlock.children = [block, block2]
|
||||
parentPath.replaceWith(parentBlock)
|
||||
if (statementParent) {
|
||||
const name = findIdentifierFromStatement(
|
||||
statementParent.node as t.VariableDeclaration
|
||||
)
|
||||
setTemplate(name, jsxElementPath, templates)
|
||||
}
|
||||
} else {
|
||||
// console.log('todo')
|
||||
}
|
||||
} else if (t.isReturnStatement(parentNode)) {
|
||||
if (!isFinalReturn) {
|
||||
const caller = parentPath.findParent(p => p.isCallExpression())
|
||||
if (caller.isCallExpression()) {
|
||||
const callee = caller.node.callee
|
||||
if (
|
||||
t.isMemberExpression(callee) &&
|
||||
t.isIdentifier(callee.property) &&
|
||||
callee.property.name === 'map'
|
||||
) {
|
||||
let ary = callee.object
|
||||
const blockStatementPath = parentPath.findParent(p => p.isBlockStatement()) as NodePath<t.BlockStatement>
|
||||
const body = blockStatementPath.node.body
|
||||
let stateToBeAssign = new Set<string>()
|
||||
for (const statement of body) {
|
||||
if (t.isVariableDeclaration(statement)) {
|
||||
for (const dcl of statement.declarations) {
|
||||
if (t.isIdentifier(dcl.id)) {
|
||||
const scope = blockStatementPath.scope
|
||||
const stateName = scope.generateUid(LOOP_STATE)
|
||||
stateToBeAssign.add(stateName)
|
||||
blockStatementPath.scope.rename(dcl.id.name, stateName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (t.isCallExpression(ary) || isContainFunction(caller.get('callee').get('object'))) {
|
||||
const variableName = `anonymousState_${bodyScope.generateUid()}`
|
||||
caller.getStatementParent().insertBefore(
|
||||
buildConstVariableDeclaration(variableName, ary)
|
||||
)
|
||||
ary = t.identifier(variableName)
|
||||
}
|
||||
setJSXAttr(jsxElementPath.node, Adapter.for, t.jSXExpressionContainer(ary))
|
||||
const [func] = caller.node.arguments
|
||||
if (
|
||||
t.isFunctionExpression(func) ||
|
||||
t.isArrowFunctionExpression(func)
|
||||
) {
|
||||
const [item, index] = func.params
|
||||
if (t.isIdentifier(item)) {
|
||||
setJSXAttr(
|
||||
jsxElementPath.node,
|
||||
Adapter.forItem,
|
||||
t.stringLiteral(item.name)
|
||||
)
|
||||
loopScopes.add(item.name)
|
||||
} else {
|
||||
setJSXAttr(
|
||||
jsxElementPath.node,
|
||||
Adapter.forItem,
|
||||
t.stringLiteral('__item')
|
||||
)
|
||||
}
|
||||
if (t.isIdentifier(index)) {
|
||||
setJSXAttr(
|
||||
jsxElementPath.node,
|
||||
Adapter.forIndex,
|
||||
t.stringLiteral(index.name)
|
||||
)
|
||||
loopScopes.add(index.name)
|
||||
}
|
||||
caller.replaceWith(jsxElementPath.node)
|
||||
if (statementParent) {
|
||||
const name = findIdentifierFromStatement(
|
||||
statementParent.node as t.VariableDeclaration
|
||||
)
|
||||
// setTemplate(name, path, templates)
|
||||
name && templates.set(name, jsxElementPath.node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const ifStatement = parentPath.findParent(p => p.isIfStatement())
|
||||
const blockStatement = parentPath.findParent(p => p.isBlockStatement())
|
||||
const block = finalReturnElement || buildBlockElement()
|
||||
if (isBlockIfStatement(ifStatement, blockStatement)) {
|
||||
const { test, alternate, consequent } = ifStatement.node
|
||||
if (alternate === blockStatement.node) {
|
||||
throw codeFrameError(parentNode.loc, '不必要的 else 分支,请遵从 ESLint consistent-return: https://eslint.org/docs/rules/consistent-return')
|
||||
} else if (consequent === blockStatement.node) {
|
||||
const parentIfStatement = ifStatement.findParent(p => p.isIfStatement())
|
||||
if (parentIfStatement) {
|
||||
setJSXAttr(
|
||||
jsxElementPath.node,
|
||||
Adapter.elseif,
|
||||
t.jSXExpressionContainer(test)
|
||||
)
|
||||
} else {
|
||||
newJSXIfAttr(jsxElementPath.node, test)
|
||||
}
|
||||
}
|
||||
} else if (block.children.length !== 0) {
|
||||
setJSXAttr(jsxElementPath.node, Adapter.else)
|
||||
}
|
||||
block.children.push(jsxElementPath.node)
|
||||
finalReturnElement = block
|
||||
returnedPaths.push(parentPath)
|
||||
}
|
||||
} else if (t.isArrowFunctionExpression(parentNode)) {
|
||||
//
|
||||
} else if (t.isAssignmentExpression(parentNode)) {
|
||||
if (t.isIdentifier(parentNode.left)) {
|
||||
const name = parentNode.left.name
|
||||
const bindingNode = bodyScope.getOwnBinding(name)!.path.node
|
||||
const block = templates.get(name) || buildBlockElement()
|
||||
if (isEmptyDeclarator(bindingNode)) {
|
||||
const ifStatement = parentPath.findParent(p => p.isIfStatement())
|
||||
const blockStatement = parentPath.findParent(p =>
|
||||
p.isBlockStatement()
|
||||
)
|
||||
if (isBlockIfStatement(ifStatement, blockStatement)) {
|
||||
const { test, alternate, consequent } = ifStatement.node
|
||||
if (alternate === blockStatement.node) {
|
||||
setJSXAttr(jsxElementPath.node, Adapter.else)
|
||||
} else if (consequent === blockStatement.node) {
|
||||
const parentIfStatement = ifStatement.findParent(p =>
|
||||
p.isIfStatement()
|
||||
) as NodePath<t.IfStatement>
|
||||
if (parentIfStatement && parentIfStatement.get('alternate') === ifStatement) {
|
||||
setJSXAttr(
|
||||
jsxElementPath.node,
|
||||
Adapter.elseif,
|
||||
t.jSXExpressionContainer(test)
|
||||
)
|
||||
} else {
|
||||
if (parentIfStatement) {
|
||||
newJSXIfAttr(block, parentIfStatement.node.test)
|
||||
}
|
||||
newJSXIfAttr(jsxElementPath.node, test)
|
||||
}
|
||||
}
|
||||
block.children.push(jsxElementPath.node)
|
||||
// setTemplate(name, path, templates)
|
||||
name && templates.set(name, block)
|
||||
}
|
||||
} else {
|
||||
throw codeFrameError(
|
||||
jsxElementPath.node.loc,
|
||||
'请将 JSX 赋值表达式初始化为 null,然后再进行 if 条件表达式赋值。'
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (!t.isJSXElement(parentNode)) {
|
||||
// throwError(path, '考虑只对 JSX 元素赋值一次。')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import { Adapters } from './adapter'
|
||||
import { eslintValidation } from './eslint'
|
||||
import { TransformOptions } from 'babel-core'
|
||||
|
||||
export interface Options {
|
||||
isRoot?: boolean,
|
||||
isApp: boolean,
|
||||
outputPath: string,
|
||||
sourcePath: string,
|
||||
code: string,
|
||||
isTyped: boolean,
|
||||
isNormal?: boolean,
|
||||
env?: object,
|
||||
adapter?: Adapters
|
||||
}
|
||||
|
||||
export const transformOptions: Options = {} as any
|
||||
|
||||
export const setTransformOptions = (options: Options) => {
|
||||
for (const key in options) {
|
||||
if (options.hasOwnProperty(key)) {
|
||||
transformOptions[key] = options[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const buildBabelTransformOptions: () => TransformOptions = () => {
|
||||
return {
|
||||
parserOpts: {
|
||||
sourceType: 'module',
|
||||
plugins: [
|
||||
'classProperties',
|
||||
'jsx',
|
||||
'flow',
|
||||
'flowComment',
|
||||
'trailingFunctionCommas',
|
||||
'asyncFunctions',
|
||||
'exponentiationOperator',
|
||||
'asyncGenerators',
|
||||
'objectRestSpread',
|
||||
'decorators',
|
||||
'dynamicImport'
|
||||
] as any[]
|
||||
},
|
||||
plugins: [
|
||||
require('babel-plugin-transform-flow-strip-types'),
|
||||
[require('babel-plugin-transform-define').default, transformOptions.env]
|
||||
].concat(process.env.ESLINT === 'false' || transformOptions.isNormal || transformOptions.isTyped ? [] : eslintValidation)
|
||||
.concat((process.env.NODE_ENV === 'test') ? [] : require('babel-plugin-remove-dead-code').default)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import * as t from 'babel-types'
|
||||
|
||||
function isString (node) {
|
||||
return t.isLiteral(node as any) && typeof node.value === 'string'
|
||||
}
|
||||
|
||||
function buildBinaryExpression (left, right) {
|
||||
return t.binaryExpression('+', left, right)
|
||||
}
|
||||
export function templateLiterals (path, state) {
|
||||
|
||||
let nodes: Array<Object> = []
|
||||
|
||||
const expressions = path.get('expressions')
|
||||
|
||||
for (const elem of (path.node.quasis)) {
|
||||
nodes.push(t.stringLiteral(elem.value.cooked))
|
||||
|
||||
const expr = expressions.shift()
|
||||
if (expr) {
|
||||
// tslint:disable-next-line:no-multi-spaces
|
||||
if (state.opts.spec && !expr.isBaseType('string') && !expr.isBaseType('number')) {
|
||||
nodes.push(t.callExpression(t.identifier('String'), [expr.node]))
|
||||
} else {
|
||||
nodes.push(expr.node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filter out empty string literals
|
||||
nodes = nodes.filter((n) => !t.isLiteral(n, { value: '' }))
|
||||
|
||||
// since `+` is left-to-right associative
|
||||
// ensure the first node is a string if first/second isn't
|
||||
if (!isString(nodes[0]) && !isString(nodes[1])) {
|
||||
nodes.unshift(t.stringLiteral(''))
|
||||
}
|
||||
|
||||
if (nodes.length > 1) {
|
||||
let root = buildBinaryExpression(nodes.shift(), nodes.shift())
|
||||
|
||||
for (const node of nodes) {
|
||||
root = buildBinaryExpression(root, node)
|
||||
}
|
||||
|
||||
path.replaceWith(root)
|
||||
} else {
|
||||
path.replaceWith(nodes[0])
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,509 @@
|
|||
import * as t from 'babel-types'
|
||||
import generate from 'babel-generator'
|
||||
import { codeFrameColumns } from '@babel/code-frame'
|
||||
import { NodePath, Scope } from 'babel-traverse'
|
||||
import { LOOP_STATE, TARO_PACKAGE_NAME } from './constant'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import { buildBlockElement } from './jsx'
|
||||
import { Adapter } from './adapter'
|
||||
import { transformOptions } from './options'
|
||||
const template = require('babel-template')
|
||||
|
||||
export const incrementId = () => {
|
||||
let id = 0
|
||||
return () => id++
|
||||
}
|
||||
|
||||
export function getSuperClassCode (path: NodePath<t.ClassDeclaration>) {
|
||||
const superClass = path.node.superClass
|
||||
if (t.isIdentifier(superClass)) {
|
||||
const binding = path.scope.getBinding(superClass.name)
|
||||
if (binding && binding.kind === 'module') {
|
||||
const bindingPath = binding.path.parentPath
|
||||
if (bindingPath.isImportDeclaration()) {
|
||||
const source = bindingPath.node.source
|
||||
if (source.value === TARO_PACKAGE_NAME) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const p = pathResolver(source.value, transformOptions.sourcePath) + (transformOptions.isTyped ? '.tsx' : '.js')
|
||||
const code = fs.readFileSync(p, 'utf8')
|
||||
return {
|
||||
code,
|
||||
sourcePath: source.value
|
||||
}
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isContainStopPropagation (path: NodePath<t.Node> | null | undefined) {
|
||||
let matched = false
|
||||
if (path) {
|
||||
path.traverse({
|
||||
Identifier (p) {
|
||||
if (
|
||||
p.node.name === 'stopPropagation' &&
|
||||
p.parentPath.parentPath.isCallExpression()
|
||||
) {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
export function decodeUnicode (s: string) {
|
||||
return unescape(s.replace(/\\(u[0-9a-fA-F]{4})/gm, '%$1'))
|
||||
}
|
||||
|
||||
export function isVarName (str: string) {
|
||||
if (typeof str !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (str.trim() !== str) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
new Function(str, 'var ' + str)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function findMethodName (expression: t.Expression): string {
|
||||
let methodName
|
||||
if (
|
||||
t.isIdentifier(expression) ||
|
||||
t.isJSXIdentifier(expression)
|
||||
) {
|
||||
methodName = expression.name
|
||||
} else if (t.isStringLiteral(expression)) {
|
||||
methodName = expression.value
|
||||
} else if (
|
||||
t.isMemberExpression(expression) &&
|
||||
t.isIdentifier(expression.property)
|
||||
) {
|
||||
const { code } = generate(expression)
|
||||
const ids = code.split('.')
|
||||
if (ids[0] === 'this' && ids[1] === 'props' && ids[2]) {
|
||||
methodName = code.replace('this.props.', '')
|
||||
} else {
|
||||
methodName = expression.property.name
|
||||
}
|
||||
} else if (
|
||||
t.isCallExpression(expression) &&
|
||||
t.isMemberExpression(expression.callee) &&
|
||||
t.isIdentifier(expression.callee.object)
|
||||
) {
|
||||
methodName = expression.callee.object.name
|
||||
} else if (
|
||||
t.isCallExpression(expression) &&
|
||||
t.isMemberExpression(expression.callee) &&
|
||||
t.isMemberExpression(expression.callee.object) &&
|
||||
t.isIdentifier(expression.callee.property) &&
|
||||
expression.callee.property.name === 'bind' &&
|
||||
t.isIdentifier(expression.callee.object.property)
|
||||
) {
|
||||
methodName = expression.callee.object.property.name
|
||||
} else {
|
||||
throw codeFrameError(expression.loc, '当 props 为事件时(props name 以 `on` 开头),只能传入一个 this 作用域下的函数。')
|
||||
}
|
||||
return methodName
|
||||
}
|
||||
|
||||
export function setParentCondition (jsx: NodePath<t.Node>, expr: t.Expression, array = false) {
|
||||
const conditionExpr = jsx.findParent(p => p.isConditionalExpression())
|
||||
const logicExpr = jsx.findParent(p => p.isLogicalExpression({ operator: '&&' }))
|
||||
if (array) {
|
||||
const ifAttrSet = new Set<string>([
|
||||
Adapter.if,
|
||||
Adapter.else
|
||||
])
|
||||
const logicalJSX = jsx.findParent(p => p.isJSXElement() && p.node.openingElement.attributes.some(a => ifAttrSet.has(a.name.name as string))) as NodePath<t.JSXElement>
|
||||
if (logicalJSX) {
|
||||
const attr = logicalJSX.node.openingElement.attributes.find(a => ifAttrSet.has(a.name.name as string))
|
||||
if (attr) {
|
||||
if (attr.name.name === Adapter.else) {
|
||||
const prevElement: NodePath<t.JSXElement | null> = (logicalJSX as any).getPrevSibling()
|
||||
if (prevElement && prevElement.isJSXElement()) {
|
||||
const attr = prevElement.node.openingElement.attributes.find(a => a.name.name === Adapter.if)
|
||||
if (attr && t.isJSXExpressionContainer(attr.value)) {
|
||||
expr = t.conditionalExpression(reverseBoolean(cloneDeep(attr.value.expression)), expr, t.arrayExpression())
|
||||
return expr
|
||||
}
|
||||
}
|
||||
} else if (t.isJSXExpressionContainer(attr.value)) {
|
||||
expr = t.conditionalExpression(cloneDeep(attr.value.expression), expr, t.arrayExpression())
|
||||
return expr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (conditionExpr && conditionExpr.isConditionalExpression()) {
|
||||
const consequent = conditionExpr.get('consequent')
|
||||
if (consequent === jsx || jsx.findParent(p => p === consequent)) {
|
||||
expr = t.conditionalExpression(cloneDeep(conditionExpr.get('test').node) as any, expr, array ? t.arrayExpression([]) : t.nullLiteral())
|
||||
}
|
||||
}
|
||||
if (logicExpr && logicExpr.isLogicalExpression({ operator: '&&' })) {
|
||||
const consequent = logicExpr.get('right')
|
||||
if (consequent === jsx || jsx.findParent(p => p === consequent)) {
|
||||
expr = t.conditionalExpression(cloneDeep(logicExpr.get('left').node) as any, expr, array ? t.arrayExpression([]) : t.nullLiteral())
|
||||
}
|
||||
}
|
||||
return expr
|
||||
}
|
||||
|
||||
export function generateAnonymousState (
|
||||
scope: Scope,
|
||||
expression: NodePath<t.Expression>,
|
||||
refIds: Set<t.Identifier>,
|
||||
isLogical?: boolean
|
||||
) {
|
||||
let variableName = `anonymousState_${scope.generateUid()}`
|
||||
let statementParent = expression.getStatementParent()
|
||||
if (!statementParent) {
|
||||
throw codeFrameError(expression.node.loc, '无法生成匿名 State,尝试先把值赋到一个变量上再把变量调换。')
|
||||
}
|
||||
const jsx = isLogical ? expression : expression.findParent(p => p.isJSXElement())
|
||||
const callExpr = jsx.findParent(p => p.isCallExpression() && isArrayMapCallExpression(p)) as NodePath<t.CallExpression>
|
||||
const ifExpr = jsx.findParent(p => p.isIfStatement())
|
||||
const blockStatement = jsx.findParent(p => p.isBlockStatement() && p.parentPath === ifExpr) as NodePath<t.BlockStatement>
|
||||
const expr = setParentCondition(jsx, cloneDeep(expression.node))
|
||||
if (!callExpr) {
|
||||
refIds.add(t.identifier(variableName))
|
||||
statementParent.insertBefore(
|
||||
buildConstVariableDeclaration(variableName, expr)
|
||||
)
|
||||
if (blockStatement && blockStatement.isBlockStatement()) {
|
||||
blockStatement.traverse({
|
||||
VariableDeclarator: (p) => {
|
||||
const { id, init } = p.node
|
||||
if (t.isIdentifier(id) && !id.name.startsWith(LOOP_STATE)) {
|
||||
const newId = scope.generateDeclaredUidIdentifier('$' + id.name)
|
||||
refIds.forEach((refId) => {
|
||||
if (refId.name === variableName && !variableName.startsWith('_$')) {
|
||||
refIds.delete(refId)
|
||||
}
|
||||
})
|
||||
variableName = newId.name
|
||||
refIds.add(t.identifier(variableName))
|
||||
blockStatement.scope.rename(id.name, newId.name)
|
||||
p.parentPath.replaceWith(
|
||||
template('ID = INIT;')({ ID: newId, INIT: init })
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
variableName = `${LOOP_STATE}_${callExpr.scope.generateUid()}`
|
||||
const func = callExpr.node.arguments[0]
|
||||
if (t.isArrowFunctionExpression(func)) {
|
||||
if (!t.isBlockStatement(func.body)) {
|
||||
func.body = t.blockStatement([
|
||||
buildConstVariableDeclaration(variableName, expr),
|
||||
t.returnStatement(func.body)
|
||||
])
|
||||
} else {
|
||||
func.body.body.splice(func.body.body.length - 1, 0, buildConstVariableDeclaration(variableName, expr))
|
||||
}
|
||||
}
|
||||
}
|
||||
const id = t.identifier(variableName)
|
||||
expression.replaceWith(id)
|
||||
return id
|
||||
}
|
||||
|
||||
export function isArrayMapCallExpression (callExpression: NodePath<t.Node>): callExpression is NodePath<t.CallExpression> {
|
||||
return callExpression &&
|
||||
t.isCallExpression(callExpression.node) &&
|
||||
t.isMemberExpression(callExpression.node.callee) &&
|
||||
t.isIdentifier(callExpression.node.callee.property, { name: 'map' })
|
||||
}
|
||||
|
||||
export function buildConstVariableDeclaration (
|
||||
variableName: string,
|
||||
expresion: t.Expression
|
||||
) {
|
||||
return t.variableDeclaration('const', [
|
||||
t.variableDeclarator(t.identifier(variableName), expresion)
|
||||
])
|
||||
}
|
||||
|
||||
export function setTemplate (name: string, path: NodePath<t.Node>, templates) {
|
||||
const parentPath = path.parentPath
|
||||
const jsxChildren = parentPath.findParent(p => p.isJSXElement())
|
||||
if (name && !jsxChildren) {
|
||||
templates.set(name, path.node)
|
||||
}
|
||||
}
|
||||
|
||||
export function isContainFunction (p: NodePath<t.Node>) {
|
||||
let bool = false
|
||||
p.traverse({
|
||||
CallExpression () {
|
||||
bool = true
|
||||
}
|
||||
})
|
||||
return bool
|
||||
}
|
||||
|
||||
function slash (input: string) {
|
||||
const isExtendedLengthPath = /^\\\\\?\\/.test(input)
|
||||
const hasNonAscii = /[^\u0000-\u0080]+/.test(input)
|
||||
const hasChinese = /[^\u4e00-\u9fa5]+/.test(input) // has Chinese characters
|
||||
|
||||
if (isExtendedLengthPath || (hasNonAscii && !hasChinese)) {
|
||||
return input
|
||||
}
|
||||
|
||||
return input.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
export function pathResolver (source: string, location: string) {
|
||||
const extName = path.extname(source)
|
||||
const promotedPath = source
|
||||
if (!['js', 'tsx'].includes(extName)) {
|
||||
try {
|
||||
const pathExist = fs.existsSync(path.resolve(path.dirname(location), source, 'index.js'))
|
||||
const tsxPathExist = fs.existsSync(path.resolve(path.dirname(location), source, 'index.tsx'))
|
||||
if (pathExist || tsxPathExist) {
|
||||
let p = path.join(promotedPath, 'index')
|
||||
if (!p.startsWith('.')) {
|
||||
p = './' + p
|
||||
}
|
||||
return slash(p)
|
||||
}
|
||||
return slash(promotedPath)
|
||||
} catch (error) {
|
||||
return slash(promotedPath)
|
||||
}
|
||||
}
|
||||
return slash(promotedPath.split('.').slice(0, -1).join('.'))
|
||||
}
|
||||
|
||||
export function codeFrameError (node, msg: string) {
|
||||
let errMsg = ''
|
||||
try {
|
||||
errMsg = codeFrameColumns(setting.sourceCode, node && node.type && node.loc ? node.loc : node, {
|
||||
highlightCode: true
|
||||
})
|
||||
} catch (error) {
|
||||
errMsg = 'failed to locate source'
|
||||
}
|
||||
return new Error(`${msg}
|
||||
-----
|
||||
${errMsg}`)
|
||||
}
|
||||
|
||||
export const setting = {
|
||||
sourceCode: ''
|
||||
}
|
||||
|
||||
export function createUUID () {
|
||||
return '$' + 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
let r = Math.random() * 16 | 0
|
||||
let v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||
return v.toString(16)
|
||||
}).replace(/-/g, '').slice(0, 8)
|
||||
}
|
||||
|
||||
export function createRandomLetters (n: number) {
|
||||
const str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
return Array(n).join().split(',').map(function () { return str.charAt(Math.floor(Math.random() * str.length)) }).join('')
|
||||
}
|
||||
|
||||
export function isBlockIfStatement (ifStatement, blockStatement): ifStatement is NodePath<t.IfStatement> {
|
||||
return ifStatement && blockStatement &&
|
||||
ifStatement.isIfStatement() &&
|
||||
blockStatement.isBlockStatement()
|
||||
}
|
||||
|
||||
export function buildCodeFrame (code: string) {
|
||||
return (loc: t.SourceLocation) => codeFrameColumns(code, loc) as string
|
||||
}
|
||||
|
||||
export function isNumeric (n) {
|
||||
return !isNaN(parseFloat(n)) && isFinite(n)
|
||||
}
|
||||
|
||||
export function buildJSXAttr (name: string, value: t.Identifier | t.Expression) {
|
||||
return t.jSXAttribute(t.jSXIdentifier(name), t.jSXExpressionContainer(value))
|
||||
}
|
||||
|
||||
export function newJSXIfAttr (jsx: t.JSXElement, value: t.Identifier | t.Expression, path?: NodePath<t.JSXElement>) {
|
||||
const element = jsx.openingElement
|
||||
if (!t.isJSXIdentifier(element.name)) {
|
||||
return
|
||||
}
|
||||
if (element.name.name === 'Block' || element.name.name === 'block' || !path) {
|
||||
element.attributes.push(buildJSXAttr(Adapter.if, value))
|
||||
} else {
|
||||
const block = buildBlockElement()
|
||||
newJSXIfAttr(block, value)
|
||||
block.children.push(jsx)
|
||||
path.node = block
|
||||
}
|
||||
}
|
||||
|
||||
export function getSlotName (name: string) {
|
||||
return name.slice(6).toLowerCase()
|
||||
}
|
||||
|
||||
export function isContainJSXElement (path: NodePath<t.Node>) {
|
||||
let matched = false
|
||||
path.traverse({
|
||||
JSXElement (p) {
|
||||
matched = true
|
||||
p.stop()
|
||||
}
|
||||
})
|
||||
return matched
|
||||
}
|
||||
|
||||
export function hasComplexExpression (path: NodePath<t.Node>) {
|
||||
let matched = false
|
||||
if (isContainJSXElement(path)) {
|
||||
return false
|
||||
}
|
||||
if (path.isObjectExpression()) {
|
||||
return true
|
||||
}
|
||||
if (path.isTemplateLiteral() || path.isCallExpression()) {
|
||||
return true
|
||||
}
|
||||
if (path.isArrayExpression()) {
|
||||
const { elements } = path.node
|
||||
if (elements.some(el => t.isObjectExpression(el as any) || t.isArrayExpression(el))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
path.traverse({
|
||||
CallExpression: (p) => {
|
||||
matched = true
|
||||
p.stop()
|
||||
},
|
||||
TemplateLiteral (p) {
|
||||
matched = true
|
||||
p.stop()
|
||||
},
|
||||
ObjectExpression (p) {
|
||||
matched = true
|
||||
p.stop()
|
||||
},
|
||||
ArrayExpression (p) {
|
||||
const { elements } = p.node
|
||||
if (elements.some(el => t.isObjectExpression(el as any))) {
|
||||
return true
|
||||
}
|
||||
},
|
||||
TaggedTemplateExpression (p) {
|
||||
matched = true
|
||||
p.stop()
|
||||
},
|
||||
MemberExpression (path) {
|
||||
const jsxElement = path.findParent(p => p.isJSXExpressionContainer())
|
||||
const object = path.get('object')
|
||||
const property = path.get('property')
|
||||
const parentPath = path.parentPath
|
||||
if (
|
||||
jsxElement &&
|
||||
object.isThisExpression() &&
|
||||
property.isIdentifier({ name: 'state' }) &&
|
||||
parentPath.isMemberExpression() &&
|
||||
parentPath.parentPath.isMemberExpression()
|
||||
) {
|
||||
const sourceCode = parentPath.parentPath.getSource()
|
||||
if (sourceCode.includes('[') && sourceCode.includes(']')) {
|
||||
matched = true
|
||||
path.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return matched
|
||||
}
|
||||
|
||||
export function findFirstIdentifierFromMemberExpression (node: t.MemberExpression, member?): t.Identifier {
|
||||
let id
|
||||
let object = node.object as any
|
||||
while (true) {
|
||||
if (t.identifier(object) && !t.isMemberExpression(object)) {
|
||||
id = object
|
||||
if (member) {
|
||||
object = member
|
||||
}
|
||||
break
|
||||
}
|
||||
object = object.object
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
export function getArgumentName (arg) {
|
||||
if (t.isThisExpression(arg)) {
|
||||
return 'this'
|
||||
} else if (t.isNullLiteral(arg)) {
|
||||
return 'null'
|
||||
} else if (t.isStringLiteral(arg) || t.isNumericLiteral(arg)) {
|
||||
return arg.value
|
||||
} else if (t.isIdentifier(arg)) {
|
||||
return arg.name
|
||||
} else {
|
||||
return generate(arg).code
|
||||
}
|
||||
throw new Error(`bind 不支持传入该参数: ${arg}`)
|
||||
}
|
||||
|
||||
export function isAllLiteral (...args) {
|
||||
return args.every(p => t.isLiteral(p))
|
||||
}
|
||||
|
||||
export function reverseBoolean (expression: t.Expression) {
|
||||
return t.unaryExpression(
|
||||
'!',
|
||||
expression
|
||||
)
|
||||
}
|
||||
|
||||
export function isEmptyDeclarator (node: t.Node) {
|
||||
if (
|
||||
t.isVariableDeclarator(node) &&
|
||||
(node.init === null ||
|
||||
t.isNullLiteral(node.init))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function toLetters (num: number): string {
|
||||
let mod = num % 26
|
||||
let pow = num / 26 | 0
|
||||
let out = mod ? String.fromCharCode(64 + mod) : (--pow, 'Z')
|
||||
const letter = pow ? toLetters(pow) + out : out
|
||||
return letter.toLowerCase()
|
||||
}
|
||||
|
||||
export function findIdentifierFromStatement (statement: t.Node) {
|
||||
if (t.isVariableDeclaration(statement)) {
|
||||
const declarator = statement.declarations.find(s => t.isIdentifier(s.id))
|
||||
if (declarator && t.isIdentifier(declarator.id)) {
|
||||
return declarator.id.name
|
||||
}
|
||||
}
|
||||
return '__return'
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"removeComments": false,
|
||||
"preserveConstEnums": true,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitAny": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"outDir": "lib",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strictNullChecks": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"rootDir": ".",
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"__tests__",
|
||||
"node_modules",
|
||||
"dist",
|
||||
"tests",
|
||||
"jest",
|
||||
"lib",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"compileOnSave": false
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": ["tslint-config-standard"],
|
||||
"rules": {
|
||||
"no-console": true,
|
||||
"no-unused-variable": false,
|
||||
"member-ordering": false,
|
||||
"no-debugger": true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue