add taro-transformer-wx

This commit is contained in:
dntzhang 2019-02-20 16:10:29 +08:00
parent cb83549436
commit 77658152c0
35 changed files with 12283 additions and 3 deletions

View File

@ -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",

View File

@ -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,

2
packages/omix/readme.md Normal file
View File

@ -0,0 +1,2 @@
单元测试先跑起来
https://github.com/NervJS/taro/blob/master/packages/taro-transformer-wx/__tests__/loop.spec.ts

View File

@ -0,0 +1,4 @@
/lib
/node_modules
t.js
tt.js

12
packages/omix/taro-transformer-wx/.gitignore vendored Executable file
View File

@ -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

View File

@ -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))

View File

@ -0,0 +1,3 @@
# @tarojs/transformer-wx
把 JSX 语法转换成可以在小程序运行的字符串模板。

View File

@ -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()
})
})

View File

@ -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')
})
})

View File

@ -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>
`))
})
})

View File

@ -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

View File

@ -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}}"`)
})
})
})

View File

@ -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: '' }
})
})
})
})
})
})

View File

@ -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('')
})
})
})

View File

@ -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 {}

View File

@ -0,0 +1,2 @@
var transform = require('./lib/src').default
module.exports = module.exports.default = transform

View File

@ -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"
}

View File

@ -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
}
}

View File

@ -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 }

View File

@ -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__'

View File

@ -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
}

View File

@ -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')
}
}
}
}
}
}
}

View File

@ -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
}

View File

@ -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>
}

View File

@ -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, '&nbsp;').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)
})
}

View File

@ -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'
}

View File

@ -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 元素赋值一次。')
}
}
}
})
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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'
}

View File

@ -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
}

View File

@ -0,0 +1,9 @@
{
"extends": ["tslint-config-standard"],
"rules": {
"no-console": true,
"no-unused-variable": false,
"member-ordering": false,
"no-debugger": true
}
}