diff --git a/.eslintrc.js b/.eslintrc.js
index b1f42868dfb8..2adc439e07cf 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -48,6 +48,7 @@ module.exports = {
'react/prop-types': 0,
'react/no-find-dom-node': 0,
'react/no-unknown-property': 0,
+ 'import/no-named-default': 'off',
quotes: [2, 'single', { avoidEscape: true, allowTemplateLiterals: true }],
semi: [2, 'never'],
'simple-import-sort/imports': [2, {
diff --git a/packages/postcss-unit-transform/.eslintrc.js b/packages/postcss-unit-transform/.eslintrc.js
new file mode 100644
index 000000000000..f62166e98416
--- /dev/null
+++ b/packages/postcss-unit-transform/.eslintrc.js
@@ -0,0 +1,17 @@
+module.exports = {
+ parser: '@typescript-eslint/parser',
+ plugins: [
+ '@typescript-eslint'
+ ],
+ parserOptions: { },
+ extends: [
+ 'eslint:recommended',
+ 'standard',
+ 'plugin:@typescript-eslint/recommended',
+ 'prettier'
+ ],
+ rules: {
+ '@typescript-eslint/no-unused-vars': 0,
+ '@typescript-eslint/no-var-requires': 0
+ }
+}
diff --git a/packages/postcss-unit-transform/README.md b/packages/postcss-unit-transform/README.md
new file mode 100644
index 000000000000..7bae3cd4a6f3
--- /dev/null
+++ b/packages/postcss-unit-transform/README.md
@@ -0,0 +1,3 @@
+# postcss-taro-unit-transform
+
+小程序的单位转换
diff --git a/packages/postcss-unit-transform/index.js b/packages/postcss-unit-transform/index.js
new file mode 100644
index 000000000000..f2a0c1ebf46f
--- /dev/null
+++ b/packages/postcss-unit-transform/index.js
@@ -0,0 +1,17 @@
+const postcss = require('postcss')
+
+module.exports = postcss.plugin('postcss-taro-unit-transform', plugin)
+
+function plugin (opts) {
+ return function (root) {
+ root.walkDecls(function (decl) {
+ let value = decl.value
+ value = value.replace(/([0-9.]+)px/ig, function (match, size) {
+ return (parseInt(size, 10) * 2) + 'px'
+ }).replace(/([0-9.]+)rpx/ig, function (match, size) {
+ return size + 'px'
+ })
+ decl.value = value
+ })
+ }
+}
diff --git a/packages/postcss-unit-transform/package.json b/packages/postcss-unit-transform/package.json
new file mode 100644
index 000000000000..62c8519cc3f7
--- /dev/null
+++ b/packages/postcss-unit-transform/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "postcss-taro-unit-transform",
+ "version": "3.6.8",
+ "description": "小程序单位转换",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "luckyadam",
+ "license": "MIT",
+ "dependencies": {
+ "postcss": "^6.0.21",
+ "typescript": "^4.7.4"
+ },
+ "devDependencies": {
+ "@typescript-eslint/eslint-plugin": "^5.17.0",
+ "@typescript-eslint/parser": "^5.17.0"
+ }
+}
diff --git a/packages/postcss-unit-transform/yarn.lock b/packages/postcss-unit-transform/yarn.lock
new file mode 100644
index 000000000000..c35b098af910
--- /dev/null
+++ b/packages/postcss-unit-transform/yarn.lock
@@ -0,0 +1,53 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-3.2.1.tgz?cache=0&sync_timestamp=1589682811931&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fansi-styles%2Fdownload%2Fansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ dependencies:
+ color-convert "^1.9.0"
+
+chalk@^2.4.1:
+ version "2.4.2"
+ resolved "https://registry.npm.taobao.org/chalk/download/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.npm.taobao.org/color-convert/download/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ dependencies:
+ color-name "1.1.3"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npm.taobao.org/color-name/download/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.npm.taobao.org/escape-string-regexp/download/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npm.taobao.org/has-flag/download/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+
+postcss@^6.0.21:
+ version "6.0.23"
+ resolved "https://registry.npm.taobao.org/postcss/download/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
+ dependencies:
+ chalk "^2.4.1"
+ source-map "^0.6.1"
+ supports-color "^5.4.0"
+
+source-map@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz?cache=0&sync_timestamp=1589682764497&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsource-map%2Fdownload%2Fsource-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
+supports-color@^5.3.0, supports-color@^5.4.0:
+ version "5.5.0"
+ resolved "https://registry.npm.taobao.org/supports-color/download/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ dependencies:
+ has-flag "^3.0.0"
diff --git a/packages/taro-cli-convertor/package.json b/packages/taro-cli-convertor/package.json
index 9e221d3fb691..b73c52fa77d5 100644
--- a/packages/taro-cli-convertor/package.json
+++ b/packages/taro-cli-convertor/package.json
@@ -30,16 +30,17 @@
"author": "O2Team",
"license": "MIT",
"dependencies": {
+ "@babel/generator": "^7.20.0",
"@babel/template": "^7.20.0",
"@babel/traverse": "^7.20.0",
"@babel/types": "^7.20.0",
+ "@tarojs/cli": "workspace:*",
"@tarojs/helper": "workspace:*",
"@tarojs/taroize": "workspace:*",
- "@tarojs/cli": "workspace:*",
- "@tarojs/transformer-wx": "^2.2.10",
- "postcss-taro-unit-transform": "^2.2.10",
- "prettier": "^2.7.1",
- "postcss": "^8.4.18"
+ "@tarojs/transformer-wx": "workspace:*",
+ "postcss": "^8.4.18",
+ "postcss-taro-unit-transform": "workspace:*",
+ "prettier": "^2.7.1"
},
"devDependencies": {
"@babel/parser": "^7.14.5",
diff --git a/packages/taro-cli-convertor/src/index.ts b/packages/taro-cli-convertor/src/index.ts
index 72f3890605e1..9f6729eb83ed 100644
--- a/packages/taro-cli-convertor/src/index.ts
+++ b/packages/taro-cli-convertor/src/index.ts
@@ -348,7 +348,7 @@ export default class Convertor {
template(
`import ${importName} from '${promoteRelativePath(path.relative(outputFilePath, importPath))}'`,
babylonConfig
- )()
+ )() as t.Statement
)
})
}
@@ -360,7 +360,7 @@ export default class Convertor {
template(
`import ${name} from '${promoteRelativePath(path.relative(sourceFilePath, component))}'`,
babylonConfig
- )()
+ )() as t.Statement
)
})
}
@@ -896,13 +896,17 @@ ${code}
date,
projectName,
framework: this.framework,
- compiler: 'webpack5'
+ compiler: 'webpack5',
+ typescript: false
})
creator.template(templateName, path.join('config', 'dev.js'), path.join(configDir, 'dev.js'), {
- framework: this.framework
+ framework: this.framework,
+ compiler: 'webpack5',
+ typescript: false
})
creator.template(templateName, path.join('config', 'prod.js'), path.join(configDir, 'prod.js'), {
- framework: this.framework
+ framework: this.framework,
+ typescript: false
})
creator.template(templateName, 'project.config.json', path.join(this.convertRoot, 'project.config.json'), {
description,
diff --git a/packages/taro-transformer-wx/.eslintignore b/packages/taro-transformer-wx/.eslintignore
new file mode 100644
index 000000000000..df0d70107835
--- /dev/null
+++ b/packages/taro-transformer-wx/.eslintignore
@@ -0,0 +1,4 @@
+/lib
+/node_modules
+t.js
+tt.js
\ No newline at end of file
diff --git a/packages/taro-transformer-wx/.eslintrc.js b/packages/taro-transformer-wx/.eslintrc.js
new file mode 100644
index 000000000000..3ec5c59edf52
--- /dev/null
+++ b/packages/taro-transformer-wx/.eslintrc.js
@@ -0,0 +1,44 @@
+module.exports = {
+ parser: '@typescript-eslint/parser',
+ plugins: [
+ '@typescript-eslint'
+ ],
+ parserOptions: { },
+ extends: [
+ 'eslint:recommended',
+ 'standard',
+ 'plugin:@typescript-eslint/recommended',
+ 'prettier'
+ ],
+ rules: {
+ '@typescript-eslint/ban-ts-comment': 0,
+ '@typescript-eslint/explicit-function-return-type': 0,
+ '@typescript-eslint/explicit-module-boundary-types': 0,
+ '@typescript-eslint/indent': [2, 2],
+ '@typescript-eslint/member-delimiter-style': [1, { multiline: { delimiter: 'none' }, singleline: { delimiter: 'comma' } }],
+ '@typescript-eslint/no-empty-function': 0,
+ '@typescript-eslint/no-explicit-any': 0,
+ '@typescript-eslint/no-namespace': 0,
+ '@typescript-eslint/no-non-null-assertion': 0,
+ '@typescript-eslint/no-this-alias': 0,
+ '@typescript-eslint/no-unused-vars': 0,
+ '@typescript-eslint/no-use-before-define': [1, { functions: false, classes: false }],
+ '@typescript-eslint/no-var-requires': 0,
+ '@typescript-eslint/ban-types': 0,
+ 'no-restricted-globals': 'off',
+ 'no-cond-assign': 'off',
+ 'no-inner-declarations': 'off',
+ 'no-unmodified-loop-condition': 'off',
+ 'no-return-assign': 'off',
+ 'no-console': 'off',
+ 'no-self-compare': 'off',
+ 'no-control-regex': 'off',
+ 'no-new-func': 'off',
+ 'no-new': 'off',
+ 'prefer-const': 'off',
+ 'no-empty': 'off',
+ 'no-unsafe-optional-chaining': 'off',
+ 'no-prototype-builtins': 'off',
+ camelcase: 'off'
+ }
+}
diff --git a/packages/taro-transformer-wx/.gitignore b/packages/taro-transformer-wx/.gitignore
new file mode 100644
index 000000000000..d1ba3df56449
--- /dev/null
+++ b/packages/taro-transformer-wx/.gitignore
@@ -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
diff --git a/packages/taro-transformer-wx/.vscode/launch.json b/packages/taro-transformer-wx/.vscode/launch.json
new file mode 100644
index 000000000000..ddfd37da6718
--- /dev/null
+++ b/packages/taro-transformer-wx/.vscode/launch.json
@@ -0,0 +1,28 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "node",
+ "name": "t.js",
+ "request": "launch",
+ "args": ["--runInBand"],
+ "cwd": "${workspaceFolder}",
+ "console": "integratedTerminal",
+ "internalConsoleOptions": "neverOpen",
+ "program": "${workspaceFolder}/t.js"
+ },
+ {
+ "type": "node",
+ "name": "vscode-jest-tests",
+ "request": "launch",
+ "args": ["--runInBand"],
+ "cwd": "${workspaceFolder}",
+ "console": "integratedTerminal",
+ "internalConsoleOptions": "neverOpen",
+ "program": "${workspaceFolder}/node_modules/jest/bin/jest"
+ }
+ ]
+}
diff --git a/packages/taro-transformer-wx/CHANGELOG.md b/packages/taro-transformer-wx/CHANGELOG.md
new file mode 100644
index 000000000000..a18f017a3cfa
--- /dev/null
+++ b/packages/taro-transformer-wx/CHANGELOG.md
@@ -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.
+
+
+## [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))
+
+
+
+
+
+## [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))
+
+
+
+
+
+## [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
+
+
+## [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))
diff --git a/packages/taro-transformer-wx/README.md b/packages/taro-transformer-wx/README.md
new file mode 100644
index 000000000000..080b5161834b
--- /dev/null
+++ b/packages/taro-transformer-wx/README.md
@@ -0,0 +1,3 @@
+# @tarojs/transformer-wx
+
+把 JSX 语法转换成可以在小程序运行的字符串模板。
\ No newline at end of file
diff --git a/packages/taro-transformer-wx/index.js b/packages/taro-transformer-wx/index.js
new file mode 100644
index 000000000000..6c4e96308ecf
--- /dev/null
+++ b/packages/taro-transformer-wx/index.js
@@ -0,0 +1,4 @@
+var transform = require('./lib/src').default
+
+module.exports = transform
+module.exports.default = transform
diff --git a/packages/taro-transformer-wx/package.json b/packages/taro-transformer-wx/package.json
new file mode 100644
index 000000000000..88e0986a86bc
--- /dev/null
+++ b/packages/taro-transformer-wx/package.json
@@ -0,0 +1,112 @@
+{
+ "name": "@tarojs/transformer-wx",
+ "version": "3.6.8",
+ "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 packages/taro-transformer-wx/__tests__/base.spec.ts",
+ "testAll": "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.21.4",
+ "@babel/core": "^7.21.8",
+ "@babel/generator": "^7.21.4",
+ "@babel/plugin-proposal-class-properties": "^7.14.5",
+ "@babel/plugin-proposal-decorators": "^7.14.5",
+ "@babel/plugin-proposal-object-rest-spread": "^7.14.5",
+ "@babel/plugin-proposal-optional-chaining": "^7.21.0",
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-dynamic-import": "7.8.3",
+ "@babel/plugin-transform-async-to-generator": "^7.22.5",
+ "@babel/plugin-transform-exponentiation-operator": "^7.22.5",
+ "@babel/plugin-transform-flow-strip-types": "^7.22.5",
+ "@babel/plugin-transform-react-jsx": "^7.14.5",
+ "@babel/preset-env": "^7.14.5",
+ "@babel/template": "7.21.9",
+ "@babel/traverse": "^7.21.4",
+ "@babel/types": "^7.21.4",
+ "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": "^1.3.2",
+ "babel-plugin-preval": "1.6.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-do-expressions": "^6.22.0",
+ "babel-plugin-transform-es2015-template-literals": "^6.22.0",
+ "babel-plugin-transform-export-extensions": "^6.22.0",
+ "babel-plugin-transform-flow-strip-types": "^6.22.0",
+ "eslint-plugin-react": "7.10.0",
+ "eslint-plugin-taro": "2.2.19",
+ "html": "^1.0.0",
+ "lodash": "^4.17.21",
+ "lodash-es": "4.17.21",
+ "prettier": "^2.7.1",
+ "typescript": "^4.7.4"
+ },
+ "devDependencies": {
+ "@tarojs/taro": "2.2.19",
+ "@types/babel-core": "6.25.6",
+ "@types/babel-generator": "6.25.3",
+ "@types/babel-template": "6.25.2",
+ "@types/babel-traverse": "6.25.3",
+ "@types/babel-types": "7.0.8",
+ "@types/eslint": "^7.14.3",
+ "@types/jest": "^22.2.3",
+ "@types/lodash": "^4.14.105",
+ "@types/node": "^9.6.2",
+ "babel-jest": "^29.5.0",
+ "eslint": "5.16.0",
+ "jest": "^29.3.1",
+ "jest-cli": "^29.3.1",
+ "ts-jest": "^29.0.5",
+ "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"
+}
diff --git a/packages/taro-transformer-wx/src/adapter.ts b/packages/taro-transformer-wx/src/adapter.ts
new file mode 100644
index 000000000000..c97f26587688
--- /dev/null
+++ b/packages/taro-transformer-wx/src/adapter.ts
@@ -0,0 +1,129 @@
+export const enum Adapters {
+ weapp = 'weapp',
+ swan = 'swan',
+ alipay = 'alipay',
+ quickapp = 'quickapp',
+ tt = 'tt',
+ qq = 'qq',
+ jd = 'jd'
+}
+
+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
+}
+
+const quickappAdapter: Adapter = {
+ if: 'if',
+ else: 'else',
+ elseif: 'elif',
+ for: 'for',
+ forItem: 'for-item',
+ forIndex: 'for-index',
+ key: 'key',
+ type: Adapters.quickapp
+}
+
+const qqAdapter: Adapter = {
+ if: 'qq:if',
+ else: 'qq:else',
+ elseif: 'qq:elif',
+ for: 'qq:for',
+ forItem: 'qq:for-item',
+ forIndex: 'qq:for-index',
+ key: 'qq:key',
+ type: Adapters.qq
+}
+
+const jdAdapter: Adapter = {
+ if: 'jd:if',
+ else: 'jd:else',
+ elseif: 'jd:elif',
+ for: 'jd:for',
+ forItem: 'jd:for-item',
+ forIndex: 'jd:for-index',
+ key: 'jd:key',
+ type: Adapters.jd
+}
+
+export let Adapter: Adapter = weixinAdapter
+
+export const isNewPropsSystem = () => {
+ return [Adapters.weapp, Adapters.swan, Adapters.tt, Adapters.qq, Adapters.alipay, Adapters.quickapp, Adapters.jd].includes(Adapter.type)
+}
+
+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
+ case Adapters.quickapp:
+ Adapter = quickappAdapter
+ break
+ case Adapters.qq:
+ Adapter = qqAdapter
+ break
+ case Adapters.jd:
+ Adapter = jdAdapter
+ break
+ default:
+ Adapter = weixinAdapter
+ break
+ }
+}
diff --git a/packages/taro-transformer-wx/src/class-method-renamer.ts b/packages/taro-transformer-wx/src/class-method-renamer.ts
new file mode 100644
index 000000000000..44836cdef935
--- /dev/null
+++ b/packages/taro-transformer-wx/src/class-method-renamer.ts
@@ -0,0 +1,77 @@
+import { NodePath,Visitor } from '@babel/traverse'
+import * as t from '@babel/types'
+
+import { isDerivedFromThis } from './utils'
+
+function buildMethodName (n: string) {
+ return `render${n.charAt(0).toUpperCase() + n.slice(1)}`
+}
+
+export const buildVistor = () => {
+ const renameMap = new Map()
+
+ const classMethodRenamer: () => {
+ visitor: Visitor
+ } = () => {
+ return {
+ visitor: {
+ JSXElement (path) {
+ let methodName = ''
+ const classMethod = path.findParent(p => p.isClassMethod())
+ if (classMethod && classMethod.isClassMethod() && t.isIdentifier(classMethod.node.key)) {
+ methodName = (classMethod.node.key as any).name
+ if (methodName.startsWith('render') || methodName === 'constructor') {
+ return
+ }
+ classMethod.node.key = t.identifier(buildMethodName(methodName))
+ }
+
+ const classProp = path.findParent(p => p.isClassProperty())
+ if (classProp && classProp.isClassProperty()) {
+ methodName = (classProp.node.key as any).name
+ if (methodName.startsWith('render')) {
+ return
+ }
+ if (!t.isArrowFunctionExpression(classProp.node.value)) {
+ return
+ }
+ classProp.replaceWith(t.classMethod(
+ 'method',
+ t.identifier(buildMethodName(methodName)),
+ (classProp.node.value as any).params,
+ t.isBlockStatement((classProp.node.value as any).body) ? (classProp.node.value as any).body : t.blockStatement([
+ t.returnStatement((classProp.node.value as any).body)
+ ])
+ ))
+ return
+ }
+
+ if (methodName.length > 0 && !methodName.startsWith('render')) {
+ renameMap.set(methodName, buildMethodName(methodName))
+ }
+ },
+ Identifier (path: NodePath) {
+ const name = path.node.name
+ if (renameMap.has(name)) {
+ const memberExpr = path.parentPath
+ if (memberExpr.isMemberExpression() && memberExpr.parentPath.isCallExpression()) {
+ const object = memberExpr.get('object')
+ if (t.isThisExpression(object)) {
+ path.replaceWith(t.identifier(buildMethodName(name)))
+ } else if (t.isIdentifier(object) && isDerivedFromThis(path.scope, object.name)) {
+ memberExpr.replaceWith(t.memberExpression(
+ t.thisExpression(),
+ t.identifier(buildMethodName(name))
+ ))
+ }
+ } else if (memberExpr.isCallExpression() && isDerivedFromThis(path.scope, name)) {
+ path.scope.rename(name, buildMethodName(name))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return classMethodRenamer
+}
diff --git a/packages/taro-transformer-wx/src/class.ts b/packages/taro-transformer-wx/src/class.ts
new file mode 100644
index 000000000000..c4da5379fce8
--- /dev/null
+++ b/packages/taro-transformer-wx/src/class.ts
@@ -0,0 +1,1169 @@
+import generate from '@babel/generator'
+import { NodePath } from '@babel/traverse'
+import * as t from '@babel/types'
+import { get as safeGet, kebabCase, set as safeSet,uniqueId } from 'lodash'
+import { extname, sep } from 'path'
+
+import { Adapter, Adapters, isNewPropsSystem } from './adapter'
+import { ANONYMOUS_FUNC, CLASS_COMPONENT_UID, COMPONENTS_PACKAGE_NAME, CONTEXT_PROVIDER,DEFAULT_Component_SET, DEFAULT_Component_SET_COPY, FN_PREFIX } from './constant'
+import { isTestEnv } from './env'
+import { Status } from './functional'
+import { LoopRef } from './interface'
+import { findJSXAttrByName } from './jsx'
+import { RenderParser } from './render'
+import { injectRenderPropsEmiter } from './render-props'
+import {
+ codeFrameError,
+ createRandomLetters,
+ findFirstIdentifierFromMemberExpression,
+ findMethodName,
+ generateAnonymousState,
+ getSlotName,
+ getSuperClassPath,
+ hasComplexExpression,
+ incrementId,
+ isArrayMapCallExpression,
+ isContainJSXElement,
+ isContainStopPropagation,
+ isDerivedFromProps,
+ pathResolver} from './utils'
+
+const stopPropagationExpr = require('@babel/template').default(`typeof e === 'object' && e.stopPropagation && e.stopPropagation()`)
+
+// const stopPropagationExpr = require('babel-template')(`typeof e === 'object' && e.stopPropagation && e.stopPropagation()`)
+
+type ClassMethodsMap = Map>
+
+const NODE_MODULES = 'node_modules'
+
+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,
+ args: Array
+) {
+ 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 (!isNewPropsSystem()) {
+ 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
+ private renderJSX: Map> = new Map()
+ private refIdMap: Map, Set> = new Map()
+ private initState: Set = new Set()
+ private customComponents: Map = new Map()
+ private anonymousMethod: Map = new Map()
+ private moduleNames: string[]
+ private classPath: NodePath
+ private customComponentNames = new Set()
+ private usedState = new Set()
+ private componentProperies: Set
+ private sourcePath: string
+ private sourceDir: string
+ private refs: Ref[] = []
+ private loopRefs: Map = new Map()
+ private anonymousFuncCounter = incrementId()
+ private importJSXs = new Set()
+ private refObjExpr: t.ObjectExpression[] = []
+
+ constructor (
+ path: NodePath,
+ sourcePath: string,
+ componentProperies: string[],
+ sourceDir: string,
+ methods: ClassMethodsMap
+ ) {
+ this.classPath = path
+ this.sourcePath = sourcePath
+ this.sourceDir = sourceDir
+ this.moduleNames = Object.keys(path.scope.getAllBindings('module'))
+ this.componentProperies = new Set(componentProperies)
+ this.methods = methods
+ this.compile()
+ }
+
+ setMultipleSlots () {
+ const body = this.classPath.node.body.body
+ if (body.some(c => t.isClassProperty(c) && (c.key as any).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 () {
+ this.refObjExpr = 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()
+ )
+ ])
+ })
+
+ const _constructor = this.classPath.node.body.body.find(item => {
+ const constructorName = isTestEnv ? 'constructor' : '_constructor'
+ if (t.isClassMethod(item) && t.isIdentifier(item.key) && item.key.name === constructorName) {
+ return true
+ }
+ return false
+ })
+
+ if (_constructor && t.isClassMethod(_constructor) && Adapter.type !== Adapters.quickapp) {
+ _constructor.body.body.push(
+ t.expressionStatement(t.assignmentExpression(
+ '=',
+ t.memberExpression(t.thisExpression(), t.identifier('$$refs')),
+ t.newExpression(t.memberExpression(t.identifier('Taro'), t.identifier('RefsArray')), [])
+ ))
+ )
+ }
+ }
+
+ setComponentPath () {
+ let componentPath
+ const nodeModulesIndex = this.sourcePath.indexOf(NODE_MODULES)
+ if (nodeModulesIndex >= 0) {
+ componentPath = this.sourcePath.substring(nodeModulesIndex)
+ } else {
+ componentPath = this.sourcePath.replace(this.sourceDir, '')
+ }
+ componentPath = componentPath.replace(extname(componentPath), '')
+ componentPath = componentPath.split(sep).join('/')
+ if (componentPath.startsWith('/')) {
+ componentPath = componentPath.slice(1)
+ }
+ const $$componentPath: any = t.classProperty(t.identifier('$$componentPath'), t.stringLiteral(componentPath))
+ $$componentPath.static = true
+ this.classPath.node.body.body.push($$componentPath)
+ }
+
+ buildAnonyMousFunc = (jsxExpr: NodePath, attr: NodePath, expr: t.Expression) => {
+ const exprPath = attr.get('value.expression') as any
+ const stemParent = jsxExpr.getStatementParent() as any
+ const counter = this.anonymousFuncCounter()
+ const anonymousFuncName = `${ANONYMOUS_FUNC}${counter}`
+ const isCatch = isContainStopPropagation(exprPath as any)
+ const classBody = this.classPath.node.body.body
+ const loopCallExpr = jsxExpr.findParent(p => isArrayMapCallExpression(p as any)) as NodePath
+ let index: t.Identifier
+ const self = this
+ if (loopCallExpr) {
+ index = safeGet(loopCallExpr, 'node.arguments[0].params[1]') as any
+ if (!t.isIdentifier(index)) {
+ index = t.identifier('__index' + counter)
+ safeSet(loopCallExpr, 'node.arguments[0].params[1]', index)
+ }
+ classBody.push(t.classProperty(t.identifier(anonymousFuncName + 'Map'), t.objectExpression([])))
+ const indexKey = (stemParent as any).scope.generateUid('$indexKey')
+ // tslint:disable-next-line: no-inner-declarations
+ function findParentLoopCallExprIndices (callExpr: NodePath) {
+ const indices: Set = new Set([])
+ // tslint:disable-next-line: no-conditional-assignment
+ while (callExpr = callExpr.findParent(p => isArrayMapCallExpression(p as any) && p !== callExpr) as NodePath) {
+ let index = safeGet(callExpr, 'node.arguments[0].params[1]') as any
+ if (!t.isIdentifier(index)) {
+ index = t.identifier('__index' + self.anonymousFuncCounter()) as any
+ safeSet(callExpr, 'node.arguments[0].params[1]', index)
+ }
+ indices.add(index as any)
+ }
+ return indices
+ }
+ const indices = [...findParentLoopCallExprIndices(loopCallExpr)].reverse()
+ const indexKeyDecl = t.variableDeclaration('const', [t.variableDeclarator(
+ t.identifier(indexKey),
+ indices.length === 0
+ ? t.binaryExpression('+', t.stringLiteral(createRandomLetters(5)), index)
+ : t.templateLiteral(
+ [
+ t.templateElement({ raw: createRandomLetters(5) }),
+ ...indices.map(() => t.templateElement({ raw: '-' })),
+ t.templateElement({ raw: '' })
+ ],
+ [
+ ...indices.map(i => t.identifier(i.name)),
+ index
+ ]
+ )
+ )])
+
+ const func = loopCallExpr.node.arguments[0]
+ if (t.isArrowFunctionExpression(func)) {
+ const body = loopCallExpr.get('arguments')[0].get('body.body') as any
+ if (!t.isBlockStatement(func.body)) {
+ func.body = t.blockStatement([
+ indexKeyDecl,
+ t.returnStatement(func.body)
+ ])
+ } else {
+ // func.body.body.push(indexKeyDecl)
+ // 只有 path 的方法才能触发 traverse
+ body[body.length - 1].insertBefore(indexKeyDecl)
+ }
+ const arrayFunc = t.memberExpression(
+ t.memberExpression(t.thisExpression(), t.identifier(anonymousFuncName + 'Map')),
+ t.identifier(indexKey),
+ true
+ )
+ classBody.push(
+ t.classMethod('method', t.identifier(anonymousFuncName), [t.identifier(indexKey), t.restElement(t.identifier('e'))], t.blockStatement([
+ isCatch ? stopPropagationExpr() : t.emptyStatement(),
+ t.returnStatement(t.logicalExpression('&&', arrayFunc, t.callExpression(arrayFunc, [t.spreadElement(t.identifier('e'))])))
+ ]))
+ )
+ exprPath.replaceWith(t.callExpression(
+ t.memberExpression(
+ t.memberExpression(t.thisExpression(), t.identifier(anonymousFuncName)),
+ t.identifier('bind')
+ ),
+ [t.thisExpression(), t.identifier(indexKey)]
+ ))
+ body[body.length - 1].insertBefore(
+ t.expressionStatement(t.assignmentExpression(
+ '=',
+ arrayFunc,
+ expr
+ ))
+ )
+ } else {
+ throw codeFrameError(func, '返回 JSX 的循环语句必须使用箭头函数')
+ }
+ } 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
+ ))
+ )
+ }
+ }
+
+ private jsxClosureFuncDecl = new Set>()
+
+ renameJSXClassFunc = (propName: string, methodName: string, callPath: NodePath, args: any[], isClosure = false) => {
+ const parentPath = callPath.parentPath as any
+ if (parentPath.isCallExpression()) {
+ return
+ }
+ const callee = !isClosure
+ ? t.memberExpression(
+ t.thisExpression(),
+ t.identifier(`_create${propName.slice(6)}Data`)
+ )
+ : t.identifier(propName)
+ const templateAttr = [
+ t.jSXAttribute(t.jSXIdentifier('is'), t.stringLiteral(propName)),
+ t.jSXAttribute(t.jSXIdentifier('data'), t.jSXExpressionContainer(
+ t.callExpression(
+ t.callExpression(callee, [t.binaryExpression(
+ '+',
+ methodName === 'render'
+ ? t.identifier('__prefix')
+ : t.identifier(CLASS_COMPONENT_UID),
+ t.stringLiteral(createRandomLetters(10))
+ )]),
+ args
+ )
+ ))
+ ]
+ this.jsxClosureFuncDecl.add(parentPath)
+ callPath.replaceWith(t.jSXElement(
+ t.jSXOpeningElement(t.jSXIdentifier('Template'), templateAttr),
+ t.jSXClosingElement(t.jSXIdentifier('Template')),
+ [],
+ false
+ ))
+ }
+
+ traverse () {
+ const self = this
+ let hasRender = false
+ 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 as any))
+ const componentName = jsx.name.name
+ const refAttr = findJSXAttrByName(attrs as any, 'ref')
+ if (!refAttr) {
+ return
+ }
+ const idAttr = findJSXAttrByName(attrs as any, '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 as t.TSNonNullExpression
+ }
+ }
+ }
+ 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
+ })
+ } else {
+ this.refs.push({
+ type,
+ id,
+ fn: expr
+ })
+ }
+ } else if (t.isIdentifier(expr)) {
+ const type = DEFAULT_Component_SET.has(componentName) ? 'dom' : 'component'
+ const binding = path.scope.getBinding(expr.name)
+ const decl = t.expressionStatement(
+ t.assignmentExpression(
+ '=',
+ t.memberExpression(t.thisExpression(), expr),
+ expr
+ )
+ )
+ if (binding) {
+ binding.path.parentPath?.insertAfter(decl)
+ } else {
+ path.getStatementParent()?.insertBefore(decl)
+ }
+ this.refs.push({
+ type,
+ id,
+ fn: t.memberExpression(t.thisExpression(), expr)
+ })
+ } else {
+ throw codeFrameError(refAttr, 'ref 仅支持传入字符串、匿名箭头函数和 class 中已声明的函数')
+ }
+ }
+ if (Adapters.alipay === Adapter.type) {
+ attrs.push(t.jSXAttribute(
+ t.jSXIdentifier('onTaroCollectChilds'),
+ t.jSXExpressionContainer(t.memberExpression(t.thisExpression(), t.identifier('$collectChilds')))
+ ))
+ }
+ for (const [index, attr] of attrs.entries()) {
+ if (attr === refAttr) {
+ attrs.splice(index, 1)
+ }
+ }
+ },
+ ClassMethod (classMethodPath) {
+ const node = classMethodPath.node
+ if (t.isIdentifier(node.key)) {
+ const methodName = node.key.name
+ self.methods.set(methodName, classMethodPath as any)
+ if (methodName.startsWith('render')) {
+ if (!isContainJSXElement(classMethodPath as any)) {
+ throw codeFrameError(classMethodPath.node, '以 render 开头的类函数必须返回 JSX,否则会导致渲染失败。如果是为了渲染字符串,建议更名。\n' +
+ '以 VSCode 为例:右键点击选择方法名,点击 rename symbol(重命名符号),输入新方法名。')
+ }
+ hasRender = true
+ self.renderJSX.set(methodName, classMethodPath as any)
+ self.refIdMap.set(classMethodPath as any, new Set([]))
+ classMethodPath.traverse({
+ ReturnStatement (returnPath) {
+ const arg = returnPath.node.argument
+ const ifStem = returnPath.findParent(p => p.isIfStatement())
+ // tslint:disable-next-line: strict-type-predicates
+ if (ifStem && classMethodPath.node.body.body.some(s => s === ifStem.node) && ifStem.isIfStatement() && arg === null) {
+ const consequent = ifStem.get('consequent')
+ if (t.isBlockStatement(consequent) && (consequent as any).node.body.includes(returnPath.node)) {
+ returnPath.get('argument').replaceWith(t.nullLiteral())
+ }
+ }
+ },
+ CallExpression: {
+ enter (callPath: NodePath) {
+ const callee = callPath.get('callee')
+ const args = callPath.node.arguments
+ if (callee.isMemberExpression()) {
+ const { object, property } = callee.node
+ if (t.isThisExpression(object) && t.isIdentifier(property) && property.name.startsWith('render')) {
+ const propName = property.name
+ if (!self.methods.has(propName)) {
+ const o = getSuperClassPath(self.classPath)
+ if (o) {
+ const p = o.resolvePath.endsWith('.js') ? o.resolvePath.slice(0, o.resolvePath.length - 3) : o.resolvePath
+ self.importJSXs.add(``)
+ }
+ }
+ self.renameJSXClassFunc(propName, methodName, callPath, args)
+ }
+ }
+ if (callee.isIdentifier()) {
+ const nodeName = callee.node.name
+ if (nodeName.startsWith('renderClosure')) {
+ self.renameJSXClassFunc(nodeName, methodName, callPath, args, true)
+ }
+ }
+ },
+ exit (callPath: NodePath) {
+ const jsxExpr = callPath.parentPath
+ if (!jsxExpr.isJSXExpressionContainer()) {
+ return
+ }
+ const jsxAttr = jsxExpr.parentPath
+ if (!jsxAttr.isJSXAttribute()) {
+ return
+ }
+ const { name: attrName } = jsxAttr.node
+ if (!t.isJSXIdentifier(attrName, { name: 'data' })) {
+ return
+ }
+ generateAnonymousState(callPath.scope, callPath as any, self.refIdMap.get(classMethodPath as any)!)
+ }
+ }
+ })
+ }
+ if (methodName.startsWith('render')) {
+ self.renderJSX.set(methodName, classMethodPath as any)
+ self.refIdMap.set(classMethodPath as any, new Set([]))
+ }
+ if (methodName === 'constructor') {
+ classMethodPath.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)
+ }
+ })
+ }
+ }
+ })
+ }
+ }
+ },
+ ClassBody: {
+ exit (path) {
+ const node = path.node as t.ClassBody
+ if (!hasRender) {
+ node.body.push(t.classMethod('method', t.identifier('_createData'), [], t.blockStatement([])))
+ }
+ }
+ },
+ IfStatement: (path) => {
+ const test = path.get('test') as NodePath
+ const consequent = path.get('consequent') as NodePath
+ if (isContainJSXElement(consequent) && hasComplexExpression(test as any)) {
+ this.renderJSX.forEach(method => {
+ const renderMethod = path.findParent(p => method === p)
+ if (renderMethod && renderMethod.isClassMethod()) {
+ const scope = renderMethod && renderMethod.scope || path.scope
+ generateAnonymousState(scope, test, this.refIdMap.get(renderMethod as any)!, true)
+ }
+ })
+ }
+ },
+ ClassProperty (path) {
+ const { key: { name }, value } = (path.node as any)
+ if (t.isArrowFunctionExpression(value) || t.isFunctionExpression(value)) {
+ self.methods.set(name, path as any)
+ if (name.startsWith('render')) {
+ path.replaceWith(t.classMethod(
+ 'method',
+ t.identifier(name),
+ value.params,
+ t.isBlockStatement(value.body) ? value.body : t.blockStatement([
+ t.returnStatement(value.body)
+ ])
+ ))
+ }
+ }
+ 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
+ if (!attr) {
+ const expr = path.get('expression')
+ if (expr.isBooleanLiteral() || expr.isNullLiteral()) {
+ path.remove()
+ return
+ }
+ }
+ const isFunctionProp = attr && typeof attr.node.name.name === 'string' && attr.node.name.name.startsWith('on')
+ let renderMethod: NodePath
+ self.renderJSX.forEach(method => {
+ renderMethod = path.findParent(p => method === p) as NodePath
+ })
+
+ const jsxReferencedIdentifiers = self.refIdMap.get(renderMethod!)!
+
+ 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
+ const scope = renderMethod! && renderMethod!.scope || path.scope
+ const calleeExpr = expression.get('callee')
+ const parentPath = path.parentPath
+ if (
+ hasComplexExpression(expression as NodePath) &&
+ !isFunctionProp &&
+ !(calleeExpr &&
+ t.isMemberExpression(calleeExpr) &&
+ (calleeExpr as any).get('object').isMemberExpression() &&
+ (calleeExpr as any).get('property').isIdentifier({ name: 'bind' })) // is not bind
+ ) {
+ const calleeName = t.isIdentifier(calleeExpr) && calleeExpr.name
+ if (typeof calleeName === 'string' && calleeName.startsWith('render') && isDerivedFromProps((calleeExpr as any).scope, calleeName)) {
+ return
+ }
+ if (t.isMemberExpression(calleeExpr) && isDerivedFromProps((calleeExpr as any).scope, findFirstIdentifierFromMemberExpression(calleeExpr).name)) {
+ const idName = findFirstIdentifierFromMemberExpression(calleeExpr).name
+ if (isDerivedFromProps((calleeExpr as any).scope, idName) && t.isIdentifier(calleeExpr.property) && calleeExpr.property.name.startsWith('render')) {
+ return
+ }
+ }
+ generateAnonymousState(scope, expression, jsxReferencedIdentifiers)
+ } else {
+ if (parentPath.isJSXAttribute()) {
+ if (!(expression.isMemberExpression() || expression.isIdentifier()) && parentPath.node.name.name === 'key') {
+ generateAnonymousState(scope, expression, jsxReferencedIdentifiers)
+ }
+ }
+ }
+ if (!attr) return
+ const key = attr.node.name
+ const value = attr.node.value
+ if (!t.isJSXIdentifier(key)) {
+ return
+ }
+
+ const jsx = path.findParent(p => p.isJSXOpeningElement()) as NodePath
+
+ 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' }) &&
+ !Status.isSFC
+ ) {
+ if (
+ (!isNewPropsSystem()) ||
+ (t.isJSXIdentifier(jsx.node.name) && DEFAULT_Component_SET.has(jsx.node.name.name))
+ ) {
+ self.buildPropsAnonymousFunc(attr, expr, true, path)
+ }
+ } else if (t.isMemberExpression(expr)) {
+ if (
+ (!isNewPropsSystem()) ||
+ (t.isJSXIdentifier(jsx.node.name) && DEFAULT_Component_SET.has(jsx.node.name.name))
+ ) {
+ self.buildPropsAnonymousFunc(attr, expr as any, false, path as any)
+ }
+ } else if (!t.isLiteral(expr)) {
+ self.buildAnonyMousFunc(path as any, attr, expr as any)
+ } else {
+ throw codeFrameError(path.node, '组件事件传参不能传入基本类型')
+ }
+ }
+ 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
+ if (isContainJSXElement(path as any)) return
+ generateAnonymousState(scope, expression, jsxReferencedIdentifiers)
+ },
+ Identifier (path) {
+ const isStartWithRender = /^render[A-Z]/.test(path.node.name)
+ const isInJSXExprContainer = !!path.findParent(p => p.isJSXExpressionContainer())
+ if (!isInJSXExprContainer) {
+ return
+ }
+ if (path.node.name === 'children' || isStartWithRender) {
+ const parentPath = path.parentPath
+ const slot = t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier('slot'), [], true), t.jSXClosingElement(t.jSXIdentifier('slot')), [], true)
+ if (isStartWithRender) {
+ slot.openingElement.attributes.push(t.jSXAttribute(t.jSXIdentifier('name'), t.stringLiteral(getSlotName(path.node.name))))
+ self.setMultipleSlots()
+ }
+ if (parentPath.isCallExpression() && parentPath.parentPath.isJSXExpressionContainer()) {
+ if (isDerivedFromProps(path.scope, path.node.name)) {
+ injectRenderPropsEmiter(parentPath as any, path.node.name)
+ parentPath.replaceWith(slot)
+ }
+ }
+ if (parentPath.isMemberExpression() && parentPath.parentPath.isCallExpression()) {
+ if (isDerivedFromProps(path.scope, findFirstIdentifierFromMemberExpression(parentPath.node as any).name)) {
+ injectRenderPropsEmiter(parentPath.parentPath as any, path.node.name)
+ parentPath.parentPath.replaceWith(slot)
+ }
+ }
+ if (
+ parentPath.isMemberExpression() &&
+ parentPath.isReferenced() &&
+ (
+ parentPath.parentPath.isJSXExpressionContainer() ||
+ parentPath.parentPath.isLogicalExpression() ||
+ parentPath.parentPath.isConditionalExpression()
+ )
+ ) {
+ const object = parentPath.get('object')
+ if (t.isIdentifier(object)) {
+ const objectName = object.name
+ if (isDerivedFromProps(path.scope, objectName)) {
+ parentPath.replaceWith(slot)
+ }
+ }
+ } else if (path.isReferencedIdentifier()) {
+ if (isDerivedFromProps(path.scope, 'children')) {
+ parentPath.replaceWith(slot)
+ }
+ }
+ }
+ },
+ JSXElement (path) {
+ const id = path.node.openingElement.name
+ if (
+ t.isJSXIdentifier(id) &&
+ !DEFAULT_Component_SET.has(id.name)
+ ) {
+ if (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
+ const specs = binding.path.parent.specifiers.filter(s => t.isImportSpecifier(s)) as Array
+ if (binding.path.isImportDefaultSpecifier()) {
+ self.customComponents.set(name, {
+ sourcePath,
+ type: 'default'
+ })
+ } else {
+ const spec = specs.find(s => s.local.name === name && (s.imported as any).name !== name)
+ if (spec) {
+ self.customComponents.set(name, {
+ sourcePath,
+ type: 'pattern',
+ imported: (spec.imported as t.StringLiteral).value
+ })
+ } else {
+ self.customComponents.set(name, {
+ sourcePath,
+ type: 'pattern'
+ })
+ }
+ }
+ }
+ }
+
+ if (id.name.endsWith(CONTEXT_PROVIDER)) {
+ const valueAttr = path.node.openingElement.attributes.find(a => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === 'value')
+ const contextName = id.name.slice(0, id.name.length - CONTEXT_PROVIDER.length)
+ if (valueAttr && t.isJSXAttribute(valueAttr)) {
+ if (t.isJSXElement(valueAttr.value)) {
+ throw codeFrameError(valueAttr.value, 'Provider 的 value 只能传入一个字符串或普通表达式,不能传入 JSX')
+ } else {
+ const value = t.isStringLiteral(valueAttr.value) ? valueAttr.value : (valueAttr.value as t.JSXExpressionContainer)!.expression
+ const expr = t.expressionStatement(t.callExpression(
+ t.memberExpression(t.identifier(contextName), t.identifier('Provider')),
+ [value as any]
+ ))
+ path.getStatementParent()?.insertBefore(expr)
+ path.replaceWith(t.jSXElement(
+ t.jSXOpeningElement(t.jSXIdentifier('Block'), []),
+ t.jSXClosingElement(t.jSXIdentifier('Block')),
+ path.node.children as any
+ ))
+ }
+ }
+ }
+ }
+ },
+ 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 (t.isIdentifier(siblingProp)) {
+ const name = siblingProp.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)
+ if (parentPath.parentPath.isCallExpression()) {
+ injectRenderPropsEmiter(parentPath.parentPath as any, name)
+ parentPath.parentPath.replaceWith(t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier('slot'), [
+ t.jSXAttribute(t.jSXIdentifier('name'), t.stringLiteral(slotName))
+ ], true), t.jSXClosingElement(t.jSXIdentifier('slot')), []))
+ } else {
+ 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.name)
+ }
+ }
+ } else if (parentPath.isVariableDeclarator()) {
+ const siblingId = parentPath.get('id')
+ if (t.isObjectPattern(siblingId)) {
+ const properties = siblingId.properties
+ for (const prop of properties) {
+ if (t.isRestProperty(prop)) {
+ throw codeFrameError(prop.loc, 'this.props 不支持使用 rest property 语法,请把每一个 prop 都单独列出来')
+ } else if (t.isObjectProperty(prop) && 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_PREFIX}${property.name}`)
+ processThisPropsFnMemberProperties(callee, path as any, (node as any).arguments)
+ } else if (property.name === 'call' || property.name === 'apply') {
+ self.componentProperies.add(`${FN_PREFIX}${property.name}`)
+ processThisPropsFnMemberProperties(callee.object, path as any, (node as any).arguments)
+ }
+ }
+ }
+ }
+ })
+ }
+
+ buildPropsAnonymousFunc = (attr: NodePath, expr: t.CallExpression, isBind = false, path) => {
+ const { code } = generate(expr as any)
+ const id = t.isMemberExpression(expr.callee) ? findFirstIdentifierFromMemberExpression(expr.callee) : null
+ if (
+ code.startsWith('this.props') ||
+ (id && isDerivedFromProps(attr.scope, id.name))
+ ) {
+ const methodName = findMethodName(expr)
+ const uniqueMethodName = `${methodName}${String(isBind)}`
+ const hasMethodName = this.anonymousMethod.has(uniqueMethodName) || !methodName
+ const funcName = hasMethodName
+ ? this.anonymousMethod.get(uniqueMethodName)!
+ // 测试时使用1个稳定的 uniqueID 便于测试,实际使用5个英文字母,否则小程序不支持
+ : isTestEnv ? uniqueId('funPrivate') : `funPrivate${createRandomLetters(5)}`
+ this.anonymousMethod.set(uniqueMethodName, 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') as any).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_PREFIX}${attrName.name}`)
+ }
+ if (methodName.startsWith('on')) {
+ this.componentProperies.add(`${FN_PREFIX}${methodName}`)
+ }
+ const method = !isNewPropsSystem() ?
+ 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'))])]
+ ))
+ ])) :
+ t.classMethod('method', t.identifier(funcName), [], t.blockStatement([
+ t.returnStatement(t.callExpression(
+ t.memberExpression(
+ t.memberExpression(
+ t.memberExpression(t.thisExpression(), t.identifier('props')),
+ t.identifier(methodName)
+ ),
+ t.identifier('apply')
+ ),
+ [
+ isBind ? t.identifier('this') : t.identifier('undefined'),
+ t.callExpression(
+ t.memberExpression(
+ t.memberExpression(t.memberExpression(t.identifier('Array'), t.identifier('prototype')), t.identifier('slice')),
+ t.identifier('call')
+ ),
+ [t.identifier('arguments'), t.numericLiteral(1)]
+ )
+ ]
+ ))
+ ]))
+ this.classPath.node.body.body = this.classPath.node.body.body.concat(method)
+ // @ts-ignore
+ } else if (t.isMemberExpression(expr) && !t.isThisExpression(expr.object)) {
+ // @TODO: 新旧 props 系统在事件处理上耦合太深,快应用应用新 props 把旧 props 系统逻辑全部清楚
+ this.buildAnonyMousFunc(path, attr, expr)
+ }
+ }
+
+ setComponents () {
+ const components: string[] = []
+ this.customComponents.forEach((component, name) => {
+ if (name.startsWith('Taro') && component.sourcePath === COMPONENTS_PACKAGE_NAME) {
+ return
+ }
+ if (Adapter.type === Adapters.quickapp && DEFAULT_Component_SET_COPY.has(name)) {
+ return
+ }
+ components.push(name)
+ this.result.components.push({
+ path: pathResolver(component.sourcePath, this.sourcePath),
+ name: component.imported ? kebabCase(name) + '|' + kebabCase(component.imported) : kebabCase(name),
+ type: component.type
+ })
+ })
+ this.classPath.node.body.body.push(
+ t.classProperty(t.identifier('customComponents'), t.arrayExpression(
+ components.map(c => t.stringLiteral(c))
+ ))
+ )
+ }
+
+ setMethods () {
+ const methods: Array> = (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 (isTestEnv) {
+ 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) {
+ 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.isRestElement(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()
+ 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.importJSXs.size) {
+ this.importJSXs.forEach(s => {
+ this.result.template += s + '\n'
+ })
+ }
+ if (this.renderJSX.size) {
+ this.renderJSX.forEach((method, methodName) => {
+ this.result.template = this.result.template
+ + new RenderParser(
+ method,
+ this.methods,
+ this.initState,
+ this.refIdMap.get(method)!,
+ this.usedState,
+ this.customComponentNames,
+ this.componentProperies,
+ this.loopRefs,
+ this.refObjExpr,
+ methodName
+ ).outputTemplate + '\n'
+ })
+ } else {
+ throw codeFrameError(this.classPath.node.loc, '没有定义 render 方法')
+ }
+ }
+
+ clearClosureMethods () {
+ this.classPath.node.body.body = this.classPath.node.body.body.filter(m => {
+ if (m && t.isClassMethod(m) && t.isIdentifier(m.key) && m.key.name.startsWith('_createClosure')) {
+ return false
+ }
+ return true
+ })
+ }
+
+ compile () {
+ this.traverse()
+ this.setMethods()
+ this.setComponents()
+ this.resetConstructor()
+ this.findMoreProps()
+ this.handleRefs()
+ this.parseRender()
+ this.setComponentPath()
+ this.clearClosureMethods()
+ this.result.componentProperies = [...this.componentProperies]
+ }
+}
+
+export { Transformer }
diff --git a/packages/taro-transformer-wx/src/constant.ts b/packages/taro-transformer-wx/src/constant.ts
new file mode 100644
index 000000000000..61f70ca93518
--- /dev/null
+++ b/packages/taro-transformer-wx/src/constant.ts
@@ -0,0 +1,172 @@
+import { Adapters } from './adapter'
+
+export const THIRD_PARTY_COMPONENTS = new Set()
+
+// tslint:disable-next-line:variable-name
+export const DEFAULT_Component_SET = new Set([
+ 'View',
+ 'ScrollView',
+ 'Swiper',
+ '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',
+ 'Template',
+ 'Editor'
+])
+
+// tslint:disable-next-line:variable-name
+export const DEFAULT_Component_SET_COPY = new Set([])
+DEFAULT_Component_SET.forEach((c) => DEFAULT_Component_SET_COPY.add(c))
+
+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 HANDLE_LOOP_REF = 'handleLoopRef'
+
+export const PROPS_MANAGER = 'propsManager'
+
+export const GEN_COMP_ID = 'genCompid'
+
+export const GEN_LOOP_COMPID = 'genLoopCompid'
+
+export const CLASS_COMPONENT_UID = '_$uid'
+
+export let LOOP_STATE = '$loopState'
+
+export const setLoopState = (s: string) => LOOP_STATE = s
+
+export let PREV_COMPID = '$prevCompid'
+
+export let COMPID = '$compid'
+
+export const setCompId = (s: string) => COMPID = s
+
+export let LOOP_ORIGINAL = '$original'
+
+export const setLoopOriginal = (s: string) => LOOP_ORIGINAL = s
+
+export let LOOP_CALLEE = '$anonymousCallee_'
+
+export let setLoopCallee = (s: string) => LOOP_CALLEE = s
+
+export const CONTEXT_PROVIDER = 'PrivateContextProvider'
+
+export const SPECIAL_COMPONENT_PROPS = new Map>()
+
+export let IS_TARO_READY = '$taroCompReady'
+
+export const setIsTaroReady = (s: string) => IS_TARO_READY = s
+
+SPECIAL_COMPONENT_PROPS.set(
+ 'Progress',
+ new Set([
+ 'activeColor',
+ 'backgroundColor'
+ ])
+)
+
+export const IMAGE_COMPONENTS = new Set([
+ '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([
+ 'onTouchStart',
+ 'onTouchMove',
+ 'onTouchEnd',
+ 'onTouchCancel',
+ 'onClick',
+ 'onLongTap'
+])
+
+export const ANONYMOUS_FUNC = 'anonymousFunc'
+
+export const TRANSFORM_COMPONENT_PROPS = new Map()
+
+TRANSFORM_COMPONENT_PROPS.set(Adapters.alipay, {
+ 'Canvas': {
+ 'canvasId': 'id'
+ }
+})
+
+export const lessThanSignPlacehold = '__LESS_THAN_SIGN_PLACEHOLDER__'
+
+export let FN_PREFIX = '__fn_'
+
+export const setFnPrefix = (s: string) => FN_PREFIX = s
+
+export const quickappComponentName = new Set([
+ 'Swiper',
+ 'Audio',
+ 'Image',
+ 'Progress',
+ // 'Text',
+ 'Input',
+ 'Label',
+ 'Picker',
+ 'Slider',
+ 'Switch',
+ 'Textarea',
+ 'Video',
+ 'Camera',
+ 'Canvas',
+ 'Map',
+ 'Button'
+])
diff --git a/packages/taro-transformer-wx/src/create-html-element.ts b/packages/taro-transformer-wx/src/create-html-element.ts
new file mode 100644
index 000000000000..15d7ea96792c
--- /dev/null
+++ b/packages/taro-transformer-wx/src/create-html-element.ts
@@ -0,0 +1,118 @@
+import { camelCase } from 'lodash'
+
+import { Adapter,Adapters } from './adapter'
+import { DEFAULT_Component_SET_COPY, LOOP_ORIGINAL,quickappComponentName } from './constant'
+import { isTestEnv } from './env'
+import { transformOptions } from './options'
+
+const voidHtmlTags = new Set([
+ // 'image',
+ 'img',
+ 'input',
+ 'import'
+])
+
+if (isTestEnv) {
+ voidHtmlTags.add('image')
+}
+
+export const capitalized = (name: string) => name.charAt(0).toUpperCase() + name.slice(1)
+
+interface Options {
+ name: string
+ attributes: object
+ value: string
+
+
+}
+
+function stringifyAttributes (input: object, componentName: string) {
+ 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 (Adapters.quickapp === Adapter.type && key === 'style') {
+ const nameCapitalized = capitalized(componentName)
+ if (
+ !['div', 'text'].includes(componentName) &&
+ (quickappComponentName.has(nameCapitalized) || DEFAULT_Component_SET_COPY.has(nameCapitalized))
+ ) {
+ attribute = 'customstyle'
+ }
+ }
+
+ if (
+ process.env.NODE_ENV !== 'test' &&
+ (Adapters.weapp === Adapter.type || Adapters.qq === Adapter.type) &&
+ key === Adapter.key &&
+ typeof value === 'string'
+ ) {
+ value = value.split(`${LOOP_ORIGINAL}.`).join('')
+ }
+
+ if (value !== true) {
+ attribute += `="${String(value)}"`
+ }
+
+ attributes.push(attribute)
+ }
+
+ return attributes.length > 0 ? ' ' + attributes.join(' ') : ''
+
+}
+
+export const createHTMLElement = (options: Options, isFirstEmit = false) => {
+ options = Object.assign(
+ {
+ name: 'div',
+ attributes: {},
+ value: ''
+ },
+ options
+ )
+ const name = options.name
+ if (Adapters.quickapp === Adapter.type) {
+ const nameCapitalized = capitalized(name)
+ if (quickappComponentName.has(nameCapitalized)) {
+ options.name = `taro-${name}`
+ if (options.attributes.className) {
+ options.attributes.class = options.attributes.className
+ delete options.attributes.className
+ }
+ }
+ if (isFirstEmit && name === 'div' && transformOptions.isRoot) {
+ options.name = 'taro-page'
+ for (const key in options.attributes) {
+ if (options.attributes.hasOwnProperty(key)) {
+ const attr = options.attributes[key]
+ options.attributes[camelCase(key)] = attr
+ delete options.attributes[key]
+ }
+ }
+ }
+ if (name === 'view') {
+ options.name = 'div'
+ }
+ }
+
+ const isVoidTag = voidHtmlTags.has(options.name)
+
+ let ret = `<${options.name}${stringifyAttributes(options.attributes, name)}${isVoidTag ? `/` : '' }>`
+
+ if (!isVoidTag) {
+ ret += `${options.value}${options.name}>`
+ }
+
+ return ret
+}
diff --git a/packages/taro-transformer-wx/src/env.ts b/packages/taro-transformer-wx/src/env.ts
new file mode 100644
index 000000000000..d32c965d6a72
--- /dev/null
+++ b/packages/taro-transformer-wx/src/env.ts
@@ -0,0 +1 @@
+export const isTestEnv = process.env.NODE_ENV === 'test'
diff --git a/packages/taro-transformer-wx/src/eslint.ts b/packages/taro-transformer-wx/src/eslint.ts
new file mode 100644
index 000000000000..351fa37fab53
--- /dev/null
+++ b/packages/taro-transformer-wx/src/eslint.ts
@@ -0,0 +1,62 @@
+// import * as t from '@babel/types'
+import { PluginPass } from '@babel/core'
+import { Visitor } from '@babel/traverse'
+import { CLIEngine } from 'eslint'
+
+import { codeFrameError } from './utils'
+
+const cli = new CLIEngine({
+ baseConfig: {
+ extends: ['plugin:taro/transformer']
+ },
+ rules: {
+ 'react/no-multi-comp': [2, { ignoreStateless: false }]
+ },
+ plugins: ['react'],
+ useEslintrc: false,
+ parser: 'babel-eslint',
+ parserOptions: {
+ ecmaVersion: 2018,
+ ecmaFeatures: {
+ jsx: true,
+ legacyDecorators: true
+ }
+ },
+ settings: {
+ react: {
+ pragma: 'Taro',
+ version: 'detect'
+ }
+ }
+} as any)
+
+export const eslintValidation: () => {
+ visitor: Visitor
+} = () => {
+ return {
+ visitor: {
+ Program (_, state: PluginPass) {
+ 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.replace('Declare only one React component per file', '一个文件只能定义一个 Taro 类或 Taro 函数式组件') + '\n')
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/taro-transformer-wx/src/functional.ts b/packages/taro-transformer-wx/src/functional.ts
new file mode 100644
index 000000000000..f429dc7619f7
--- /dev/null
+++ b/packages/taro-transformer-wx/src/functional.ts
@@ -0,0 +1,195 @@
+import generate from '@babel/generator'
+import { Visitor } from '@babel/traverse'
+import * as t from '@babel/types'
+import { cloneDeep } from 'lodash'
+
+import { DEFAULT_Component_SET } from './constant'
+import { transformOptions } from './options'
+import { injectRenderPropsListener } from './render-props'
+import { buildConstVariableDeclaration,codeFrameError } from './utils'
+
+function initialIsCapital (word: string) {
+ return word[0] !== word[0].toLowerCase()
+}
+
+export const Status = {
+ isSFC: false
+}
+
+const renderFnReg = /^render[A-Z]/
+
+export const functionalComponent: () => {
+ visitor: Visitor
+} = () => {
+ let propsIdentifier: t.Identifier | null
+ let propsList: string[] = []
+ return {
+ visitor: {
+ JSXElement (path) {
+ const arrowFuncExpr = path.findParent(p => p.isArrowFunctionExpression())
+ const funcExpr = path.findParent(p => p.isFunctionExpression())
+ if (funcExpr && funcExpr.isFunctionExpression() && funcExpr.parentPath.isVariableDeclarator()) {
+ const { params, body, async } = funcExpr.node
+ funcExpr.replaceWith(t.arrowFunctionExpression(params as Array, body as any, async))
+ return
+ }
+ if (arrowFuncExpr && arrowFuncExpr.isArrowFunctionExpression()) {
+ if (arrowFuncExpr.parentPath.isVariableDeclarator()) {
+ const valDecl = arrowFuncExpr.parentPath.parentPath
+ if (!valDecl.isVariableDeclaration() && !valDecl.isFunctionDeclaration() && !valDecl.isFunctionExpression()) {
+ throw codeFrameError(valDecl.node, '函数式组件不能同时定义多个值')
+ }
+ const id = arrowFuncExpr.parentPath.node.id
+ if (!t.isIdentifier(id)) {
+ throw codeFrameError(id, '函数式组件只能使用普通标识符定义')
+ }
+ if (!initialIsCapital(id.name)) {
+ return
+ }
+ const hasClassDecl = arrowFuncExpr.findParent(p => p.isClassDeclaration())
+ if (hasClassDecl) {
+ // @TODO: 加上链接
+ return
+ }
+ const { body } = arrowFuncExpr.node
+ if (t.isBlockStatement(body)) {
+ valDecl.replaceWith(t.functionDeclaration(id, arrowFuncExpr.node.params as any, body))
+ } else {
+ valDecl.replaceWith(t.functionDeclaration(id, arrowFuncExpr.node.params as any, t.blockStatement([
+ t.returnStatement(body as any)
+ ])))
+ }
+ return
+ } else if (arrowFuncExpr.parentPath.isExportDefaultDeclaration()) {
+ const { body, params } = arrowFuncExpr.node
+ const func = t.functionDeclaration(
+ t.identifier('AnonymousSFC'),
+ params as any,
+ t.isBlockStatement(body) ? body : t.blockStatement([
+ t.returnStatement(body as any)
+ ])
+ )
+ arrowFuncExpr.parentPath.insertAfter(t.exportDefaultDeclaration(t.identifier('AnonymousSFC')))
+ arrowFuncExpr.parentPath.replaceWith(func)
+ return
+ }
+ }
+
+ const functionDecl = path.findParent(p => p.isFunctionDeclaration())
+ if (functionDecl && functionDecl.isFunctionDeclaration()) {
+ propsIdentifier = null
+ propsList = []
+ const hasClassDecl = path.findParent(p => p.isClassDeclaration() || p.isClassExpression())
+ if (hasClassDecl) {
+ // @TODO: 加上链接
+ return
+ }
+ let { id, body, params } = functionDecl.node
+ let arg: null | t.LVal = null
+ // tslint:disable-next-line: strict-type-predicates
+ if (id === null) {
+ functionDecl.node.id = t.identifier('YourShouldGiveTheComponentAName')
+ id = functionDecl.node.id
+ }
+ if (params.length > 1) {
+ throw codeFrameError(id, '函数式组件的参数最多只能传入一个')
+ } else if (params.length === 1) {
+ arg = params[0] as any
+ }
+ if (!(typeof id === 'undefined') && !initialIsCapital(id.name)) {
+ throw codeFrameError(id, `普通函数式组件命名规则请遵守帕斯卡命名法(Pascal Case), 如果是在函数内声明闭包组件,则需要使用函数表达式的写法。
+形如:
+const ${id?.name} = ${generate(t.arrowFunctionExpression(params as any, body as any)).code}
+ `)
+ }
+ const insertDecls: t.VariableDeclaration[] = []
+ if (arg) {
+ if (t.isIdentifier(arg)) {
+ insertDecls.push(buildConstVariableDeclaration(arg.name, t.memberExpression(t.thisExpression(), t.identifier('props'))))
+ propsIdentifier = arg
+ } else if (t.isObjectPattern(arg)) {
+ let hasChildren = false
+ for (const [index, p] of arg.properties.entries()) {
+ if (t.isObjectProperty(p) && t.isIdentifier(p.key, { name: 'children' })) {
+ hasChildren = true
+ arg.properties.splice(index, 1)
+ } else if (t.isObjectProperty(p) && t.isIdentifier(p.key)) {
+ propsList.push(p.key.name)
+ }
+ }
+ insertDecls.push(
+ t.variableDeclaration('const', [
+ t.variableDeclarator(arg, t.memberExpression(t.thisExpression(), t.identifier('props')))
+ ])
+ )
+ if (hasChildren) {
+ insertDecls.push(
+ t.variableDeclaration('const', [
+ t.variableDeclarator(t.objectPattern([
+ t.objectProperty(t.identifier('children'), t.identifier('children')) as any
+ ]), t.memberExpression(t.thisExpression(), t.identifier('props')))
+ ])
+ )
+ }
+ } else if (t.isAssignmentPattern(arg)) {
+ throw codeFrameError(arg, '给函数式组件的第一个参数设置默认参数是没有意义的,因为 props 永远都有值(不传 props 的时候是个空对象),所以默认参数永远都不会执行。')
+ } else {
+ throw codeFrameError(arg, '函数式组件只支持传入一个简单标识符或使用对象结构')
+ }
+ }
+ Status.isSFC = true
+ path.traverse({
+ JSXExpressionContainer (path) {
+ const expr = path.get('expression').node
+ if (t.isMemberExpression(expr)) {
+ const { object, property } = expr
+ if (
+ t.isIdentifier(object) &&
+ t.isIdentifier(property) &&
+ renderFnReg.test(property.name) &&
+ propsIdentifier &&
+ object.name === propsIdentifier.name
+ ) {
+ path.set('expression', t.memberExpression(
+ t.memberExpression(t.thisExpression(), t.identifier('props')),
+ t.identifier(property.name)
+ ))
+ }
+ } else if (t.isIdentifier(expr)) {
+ if (!renderFnReg.test(expr.name)) return
+ const prop = propsList.find(prop => prop === expr.name)
+ if (!prop) return
+ path.set('expression', t.memberExpression(
+ t.memberExpression(t.thisExpression(), t.identifier('props')),
+ t.identifier(expr.name)
+ ))
+ }
+ }
+ })
+ const cloneBody = cloneDeep(body) as any
+ insertDecls.forEach(decl => cloneBody.body.unshift(decl))
+ const classDecl = t.classDeclaration(id as t.Identifier, t.memberExpression(t.identifier('Taro'), t.identifier('Component')), t.classBody([
+ t.classMethod('method', t.identifier('render'), [], cloneBody)
+ ]), [])
+ functionDecl.replaceWith(classDecl)
+ }
+ },
+ JSXAttribute (path) {
+ const { name, value } = path.node
+ const jsxElementPath = path.parentPath.parentPath
+ if (t.isJSXIdentifier(name) && t.isJSXElement(jsxElementPath) && transformOptions.isNormal !== true) {
+ const componentName = ((jsxElementPath.node as any).openingElement as any).name.name
+ if (/^render[A-Z]/.test(name.name) && !DEFAULT_Component_SET.has(componentName)) {
+ if (!t.isJSXExpressionContainer(value)) {
+ throw codeFrameError(value, '以 render 开头的 props 只能传入包含一个 JSX 元素的 JSX 表达式。')
+ }
+ const expression = value.expression
+ if (t.isArrowFunctionExpression(expression)) {
+ injectRenderPropsListener(path as any, name.name, expression, componentName)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/taro-transformer-wx/src/index.ts b/packages/taro-transformer-wx/src/index.ts
new file mode 100644
index 000000000000..859740d3c147
--- /dev/null
+++ b/packages/taro-transformer-wx/src/index.ts
@@ -0,0 +1,942 @@
+import * as babel from '@babel/core'
+import generate from '@babel/generator'
+// import * as template from '@babel/template'
+// const template = require('babel-template')
+import classProperties from '@babel/plugin-proposal-class-properties'
+import decorators from '@babel/plugin-proposal-decorators'
+import objectRestSpread from '@babel/plugin-proposal-object-rest-spread'
+import optionalChaining from '@babel/plugin-proposal-optional-chaining'
+import asyncGenerators from '@babel/plugin-syntax-async-generators'
+import dynamicImport from '@babel/plugin-syntax-dynamic-import'
+import asyncFunctions from '@babel/plugin-transform-async-to-generator'
+import exponentiationOperator from '@babel/plugin-transform-exponentiation-operator'
+import flowStrip from '@babel/plugin-transform-flow-strip-types'
+import jsxPlugin from '@babel/plugin-transform-react-jsx'
+import traverse, { Binding, NodePath } from '@babel/traverse'
+import * as t from '@babel/types'
+import { prettyPrint } from 'html'
+import { cloneDeep, get as safeGet, isArray,snakeCase } from 'lodash'
+// import { transform as parse } from '@babel/core'
+// import parser from '@babel/parser'
+import * as ts from 'typescript'
+
+import { Adapter,Adapters, setAdapter } from './adapter'
+import { Transformer } from './class'
+import {
+ COMPONENTS_PACKAGE_NAME,
+ CONTEXT_PROVIDER,
+ DEFAULT_Component_SET,
+ GEN_COMP_ID,
+ GEN_LOOP_COMPID,
+ HANDLE_LOOP_REF,
+ IMAGE_COMPONENTS,
+ INTERNAL_GET_ORIGNAL,
+ INTERNAL_INLINE_STYLE,
+ INTERNAL_SAFE_GET,
+ lessThanSignPlacehold,
+ MOBX_PACKAGE_NAME,
+ PROPS_MANAGER,
+ quickappComponentName,
+ REDUX_PACKAGE_NAME,
+ setCompId,
+ setFnPrefix,
+ setIsTaroReady,
+ setLoopCallee,
+ setLoopOriginal,
+ setLoopState,
+ TARO_PACKAGE_NAME,
+ THIRD_PARTY_COMPONENTS} from './constant'
+import { isTestEnv } from './env'
+// import { Options, setTransformOptions, buildBabelTransformOptions } from './options'
+import { Options, setTransformOptions } from './options'
+import {
+ codeFrameError,
+ findFirstIdentifierFromMemberExpression,
+ getSuperClassCode,
+ isArrayMapCallExpression,
+ isContainJSXElement,
+ replaceJSXTextWithTextComponent,
+ setting} from './utils'
+
+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 handleClosureJSXFunc (jsx: NodePath, mainClass: NodePath) {
+ // 在 ./functional.ts 会把 FunctionExpression 转化为 arrowFunctionExpr
+ // 所以我们这里只处理一种情况
+ const arrowFunc = jsx.findParent(p => p.isArrowFunctionExpression())
+ if (arrowFunc && arrowFunc.isArrowFunctionExpression()) {
+ const parentPath = arrowFunc.parentPath
+ if (parentPath.isVariableDeclarator()) {
+ const id = parentPath.node.id
+ if (t.isIdentifier(id) && /^render[A-Z]/.test(id.name)) {
+ const funcName = `renderClosure${id.name.slice(6, id.name.length)}`
+ mainClass.node.body.body.push(
+ t.classProperty(
+ t.identifier(funcName),
+ cloneDeep(arrowFunc.node as any)
+ )
+ )
+ parentPath.scope.rename(id.name, funcName)
+ arrowFunc.replaceWith(t.memberExpression(
+ t.thisExpression(),
+ t.identifier(funcName)
+ ) as any)
+ }
+ }
+ }
+}
+
+function findDeclarationScope (path: NodePath, 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) {
+ 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 (t.isMemberExpression(dclInit)) {
+ dclInitIds = getIdsFromMemberProps((dclInit as any).node)
+ if (t.isIdentifier(dclId)) {
+ 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 as t.Identifier).name === 'config' && t.isObjectExpression(expr.value)) {
+ const properties = expr.value.properties
+ findThirdPartyComponent(properties as any)
+ }
+}
+
+function findThirdPartyComponent (properties: (t.ObjectMethod | t.ObjectProperty | t.SpreadProperty)[]) {
+ 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)
+ }
+ }
+ }
+ }
+ }
+}
+interface Result {
+ template?: string
+ componentProperies?: string[]
+}
+
+export interface TransformResult extends Result {
+ ast: t.File
+ code?: string
+ imageSrcs?: string
+ compressedTemplate?: string
+ sourcemap?: object
+ components: {
+ name: string
+ path: string
+ type: string
+ }[]
+}
+
+export type TransformOptions = Options
+
+
+function parseCode (code: string) {
+ return (babel.transformSync(code, {
+ ast: true,
+ sourceType: 'module',
+ plugins: [
+ classProperties,
+ jsxPlugin,
+ flowStrip,
+ asyncFunctions,
+ exponentiationOperator,
+ asyncGenerators,
+ objectRestSpread,
+ [decorators, {legacy: true}],
+ dynamicImport,
+ optionalChaining
+ ]
+ }) as { ast: t.File }).ast
+}
+
+export default function transform (options: TransformOptions): TransformResult {
+ if (options.adapter) {
+ setAdapter(options.adapter)
+ if (Adapter.type === Adapters.quickapp) {
+ DEFAULT_Component_SET.clear()
+ DEFAULT_Component_SET.add('div')
+ DEFAULT_Component_SET.add('Text')
+ setFnPrefix('prv-fn-')
+ }
+ }
+ if (Adapter.type === Adapters.swan || Adapter.type === Adapters.quickapp) {
+ setLoopOriginal('privateOriginal')
+ setLoopCallee('anonymousCallee_')
+ setLoopState('loopState')
+ }
+ if (Adapter.type === Adapters.quickapp) {
+ setIsTaroReady('priTaroCompReady')
+ setCompId('priCompid')
+ }
+ const defaultResult: TransformResult = {
+ ast: {} as any,
+ code: '',
+ imageSrcs: '',
+ compressedTemplate: '',
+ components: []
+ }
+ THIRD_PARTY_COMPONENTS.clear()
+ const code = options.isTyped
+ ? ts.transpile(options.code, {
+ jsx: options.sourcePath.endsWith('.tsx') ? ts.JsxEmit.Preserve : ts.JsxEmit.None,
+ target: ts.ScriptTarget.ESNext,
+ importHelpers: true,
+ noEmitHelpers: true,
+ emitDecoratorMetadata: process.env.TS_METADATA === 'true'
+ })
+ : options.code
+ options.env = Object.assign({ 'process.env.TARO_ENV': options.adapter || 'weapp' }, options.env || {})
+ setTransformOptions(options)
+ setting.sourceCode = code
+ let hasReduxBinding = false
+ // babel-traverse 无法生成 Hub
+ // 导致 Path#getSource|buildCodeFrameError 都无法直接使用
+ // 原因大概是 babylon.parse 没有生成 File 实例导致 scope 和 path 原型上都没有 `file`
+ // 将来升级到 babel@7 可以直接用 parse 而不是 transform
+ // const ast = parser.parse(code, buildBabelTransformOptions() as any) as t.File
+
+ const ast = parseCode(code)
+
+
+ // traverse(ast, {
+ // JSXElement (p) {
+ // setIsNormal(false)
+ // p.stop()
+ // },
+ // ImportDeclaration (path) {
+ // const { source, specifiers } = path.node
+ // if (source.value === TARO_PACKAGE_NAME) {
+ // if (specifiers.some(s => s.local.name === 'Component')) {
+ // setIsNormal(false)
+ // path.stop()
+ // }
+ // }
+ // }
+ // })
+ if (options.isNormal) {
+ if (options.isTyped) {
+ const mainClassNode = ast.program.body.find(v => {
+ return t.isClassDeclaration(v)
+ }) as t.ClassDeclaration | undefined
+ if (mainClassNode) {
+ resetTSClassProperty(mainClassNode.body.body as any)
+ }
+ }
+ const code = generate(ast.program as any).code
+ return {
+ ...defaultResult,
+ ast,
+ code
+ }
+ }
+ // transformFromAst(ast, code)
+ let result
+ const componentSourceMap = new Map()
+ const imageSource = new Set()
+ const importSources = new Set()
+ const classMethods = new Map>()
+ let componentProperies: string[] = []
+ let mainClass!: NodePath
+ let storeName!: string
+ let renderMethod!: NodePath
+ let isImportTaro = false
+ traverse(ast as any, {
+ Program: {
+ exit (path) {
+ for (const stem of path.node.body) {
+ if (t.isImportDeclaration(stem)) {
+ if (stem.source.value === TARO_PACKAGE_NAME) {
+ const specs = stem.specifiers
+ if (specs.some(s => t.isImportDefaultSpecifier(s) && s.local.name === 'Taro')) {
+ continue
+ }
+ specs.unshift(t.importDefaultSpecifier(t.identifier('Taro')))
+ }
+ }
+ }
+ }
+ },
+ MemberExpression (path) {
+ const { property } = path.node
+ const right = path.getSibling('right')
+ if (t.isIdentifier(property, { name: 'config' }) && path.parentPath.isAssignmentExpression() && right.isObjectExpression()) {
+ const properties = right.node.properties
+ findThirdPartyComponent(properties as any)
+ }
+ },
+ JSXText (path) {
+ if (Adapter.type !== Adapters.quickapp) {
+ return
+ }
+ const value = path.node.value
+ if (!value.trim()) {
+ return
+ }
+
+ replaceJSXTextWithTextComponent(path as any)
+ },
+ 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 as any)
+ }
+ }
+ }
+
+ // + 号连接符必须保证第一和第二个 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 as any)
+ },
+ ClassDeclaration (path) {
+ mainClass = path as any
+ const superClass = getSuperClassCode(path as any)
+ if (superClass) {
+ try {
+ componentProperies = transform({
+ isRoot: false,
+ isApp: false,
+ code: superClass.code,
+ isTyped: true,
+ sourcePath: superClass.sourcePath,
+ sourceDir: options.sourceDir
+ }).componentProperies!
+ } catch (error) {
+ //
+ }
+ }
+ },
+ ClassExpression (path) {
+ mainClass = path as any
+ },
+ ClassMethod (path) {
+ if (t.isIdentifier(path.node.key)) {
+ if (path.node.key.name === 'render') {
+ renderMethod = path as any
+ }
+ classMethods.set(path.node.key.name, path as any)
+ }
+ },
+ IfStatement (path) {
+ const consequent = path.get('consequent')
+ if (!consequent.isBlockStatement()) {
+ consequent.replaceWith(
+ t.blockStatement([
+ consequent.node as any
+ ]) as any
+ )
+ }
+ },
+ CallExpression (path) {
+ const callee = path.get('callee')
+ if (isContainJSXElement(path as any)) {
+ return
+ }
+ if (callee.isReferencedMemberExpression()) {
+ const id = findFirstIdentifierFromMemberExpression(callee.node as any)
+ 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 as any)
+ if (t.isIdentifier(id) && id.name.startsWith('on') && Adapters.alipay !== Adapter.type) {
+ const fullPath = buildFullPathThisPropsRef(id, calleeIds, path as any)
+ if (fullPath) {
+ path.replaceWith(
+ t.callExpression(
+ fullPath,
+ path.node.arguments as any
+ ) as any
+ )
+ }
+ }
+ }
+
+ if (callee.isReferencedIdentifier()) {
+ const id = callee.node as t.Identifier
+ const ids = [id.name]
+ if (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 as any)
+ if (fullPath) {
+ path.replaceWith(
+ t.callExpression(
+ fullPath,
+ path.node.arguments as any
+ ) as any
+ )
+ }
+ }
+ }
+ },
+ // 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')
+
+ // },
+ JSXMemberExpression (path) {
+ const { property, object } = path.node
+ if (!t.isJSXIdentifier(property, { name: 'Provider' })) {
+ throw codeFrameError(property, '只能在使用 Context.Provider 的情况下才能使用 JSX 成员表达式')
+ }
+ if (!t.isJSXIdentifier(object)) {
+ return
+ }
+ const jsx = path.parentPath.parentPath
+ if (jsx?.isJSXElement()) {
+ const componentName = `${object.name}${CONTEXT_PROVIDER}`
+ jsx.node.openingElement.name = t.jSXIdentifier(componentName) as any
+ if (jsx.node.closingElement) {
+ jsx.node.closingElement.name = t.jSXIdentifier(componentName) as any
+ }
+ }
+ },
+ 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)) as any)
+ if (index !== cases.length - 1 && t.isNullLiteral(Case.test)) {
+ throw codeFrameError(Case, '含有 JSX 的 switch case 语句只有最后一个 case 才能是 default')
+ }
+ // tslint:disable-next-line: strict-type-predicates
+ const test = Case.test === null ? t.nullLiteral() : t.binaryExpression('===', discriminant as any, Case.test as any)
+ 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 as any)
+ 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 as any))
+ 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 as any)
+ ]) as any
+ }
+ }
+ handleClosureJSXFunc(path as any, mainClass)
+ },
+ JSXOpeningElement (path) {
+ const { name } = path.node.name as t.JSXIdentifier
+ const binding = path.scope.getBinding(name)
+ if (process.env.NODE_ENV !== 'test' && binding && binding.kind === 'module') {
+ const bindingPath = binding.path
+ if (bindingPath.parentPath?.isImportDeclaration()) {
+ const source = bindingPath.parentPath.node.source
+ if (DEFAULT_Component_SET.has(name) && source.value !== COMPONENTS_PACKAGE_NAME) {
+ throw codeFrameError(bindingPath.parentPath.node, `内置组件名: '${name}' 只能从 ${COMPONENTS_PACKAGE_NAME} 引入。`)
+ }
+
+ if (name === 'Fragment') {
+ path.node.name = t.jSXIdentifier('block') as any
+ }
+ }
+ }
+
+ if (Adapter.type === Adapters.quickapp) {
+ if (name === 'View') {
+ path.node.name = t.jSXIdentifier('div') as any
+ }
+ if (name === 'Block') {
+ path.node.name = t.jSXIdentifier('block') as any
+ }
+ }
+
+ 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') as any
+ const store = path.node.attributes.find(attr => t.isJSXAttribute(attr) && attr.name.name === 'store')
+ if (store && t.isJSXAttribute(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 (
+ t.isJSXAttribute(attr) && attr.name.name === 'src'
+ ) {
+ if (t.isJSXAttribute(attr) && t.isStringLiteral(attr.value)) {
+ imageSource.add(attr.value.value)
+ } else if (t.isJSXAttribute(attr) && t.isJSXExpressionContainer(attr.value)) {
+ if (t.isStringLiteral(attr.value.expression)) {
+ imageSource.add(attr.value.expression.value)
+ }
+ }
+ }
+ }
+ }
+ },
+ JSXAttribute (path) {
+ const { name, value } = path.node
+
+ if (options.jsxAttributeNameReplace) {
+ for (const r in options.jsxAttributeNameReplace) {
+ if (options.jsxAttributeNameReplace.hasOwnProperty(r)) {
+ const element = options.jsxAttributeNameReplace[r]
+ if (t.isJSXIdentifier(name, { name: r })) {
+ path.node.name = t.jSXIdentifier(element) as any
+ }
+ }
+ }
+ }
+
+ // tslint:disable-next-line: strict-type-predicates
+ if (!t.isJSXIdentifier(name) || value === null || t.isStringLiteral(value) || t.isJSXElement(value)) {
+ return
+ }
+
+ const expr = (value as t.JSXExpressionContainer)?.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') as any
+ if (jsxID && jsxID.isJSXIdentifier() && DEFAULT_Component_SET.has(jsxID.node.name)) {
+ if (!isArray(exprPath)) {
+ exprPath.replaceWith(
+ t.callExpression(t.identifier(INTERNAL_INLINE_STYLE), [expr]) as any
+ )
+ } else {
+ exprPath.forEach(item => item.replaceWith(
+ t.callExpression(t.identifier(INTERNAL_INLINE_STYLE), [expr]) as any
+ ))
+ }
+
+ }
+ }
+
+ if (name.name.startsWith('on')) {
+ if ((exprPath as NodePath).isReferencedIdentifier()) {
+ const ids = [expr.name]
+ const fullPath = buildFullPathThisPropsRef(expr, ids, path as any)
+ if (fullPath) {
+ (exprPath as NodePath).replaceWith(fullPath)
+ }
+ }
+
+ if ((exprPath as NodePath).isReferencedMemberExpression()) {
+ const id = findFirstIdentifierFromMemberExpression(expr)
+ const ids = getIdsFromMemberProps(expr)
+ if (t.isIdentifier(id)) {
+ const fullPath = buildFullPathThisPropsRef(id, ids, path as any)
+ if (fullPath) {
+ (exprPath as NodePath).replaceWith(fullPath)
+ }
+ }
+ }
+
+ // @TODO: bind 的处理待定
+ }
+ },
+ ClassProperty (path) {
+ const { key: { name }, value } = path.node as any
+ if (t.isArrowFunctionExpression(value) || t.isFunctionExpression(value)) {
+ classMethods.set(name, path as any)
+ if (name.startsWith('render')) {
+ path.replaceWith(t.classMethod(
+ 'method',
+ t.identifier(name),
+ value.params,
+ t.isBlockStatement(value.body) ? value.body : t.blockStatement([
+ t.returnStatement(value.body)
+ ])
+ ) as any)
+ }
+ }
+ if (Adapter.type !== Adapters.quickapp) {
+ return
+ }
+ if ((path.node.key as t.Identifier).name === 'defaultProps' && t.isObjectExpression(path.node.value)) {
+ const props = path.node.value.properties
+ for (const prop of props) {
+ if (t.isObjectProperty(prop)) {
+ if (t.isStringLiteral(prop.key) && /[A-Z]/.test(prop.key.value) && !prop.key.value.startsWith('on')) {
+ prop.key = t.stringLiteral(snakeCase(prop.key.value)) as any
+ }
+ if (t.isIdentifier(prop.key) && /[A-Z]/.test(prop.key.name) && !prop.key.name.startsWith('on')) {
+ prop.key = t.identifier(snakeCase(prop.key.name)) as any
+ }
+ }
+ }
+ }
+ },
+ AssignmentExpression (path) {
+ if (Adapter.type !== Adapters.quickapp) {
+ return
+ }
+ const { left, right } = path.node
+ if (t.isMemberExpression(left) && t.isIdentifier(left.property, { name: 'defaultProps' }) && t.isObjectExpression(right)) {
+ const props = right.properties
+ for (const prop of props) {
+ if (t.isObjectProperty(prop)) {
+ if (t.isStringLiteral(prop.key) && /[A-Z]/.test(prop.key.value) && !prop.key.value.startsWith('on')) {
+ prop.key = t.stringLiteral(snakeCase(prop.key.value)) as any
+ }
+ if (t.isIdentifier(prop.key) && /[A-Z]/.test(prop.key.name) && !prop.key.name.startsWith('on')) {
+ prop.key = t.identifier(snakeCase(prop.key.name)) as any
+ }
+ }
+ }
+ }
+ },
+ 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 === COMPONENTS_PACKAGE_NAME && Adapters.quickapp === Adapter.type) {
+ path.node.specifiers.forEach((s) => {
+ if (t.isImportSpecifier(s)) {
+ const originalName = (s.imported as t.Identifier).name
+ if (quickappComponentName.has(originalName)) {
+ const importedName = `Taro${originalName}`
+ ;(s.imported as t.Identifier).name = importedName
+ s.local.name = importedName
+ }
+ }
+ })
+ }
+ if (source === TARO_PACKAGE_NAME) {
+ isImportTaro = true
+ path.node.specifiers.push(
+ t.importSpecifier(t.identifier(INTERNAL_SAFE_GET), t.identifier(INTERNAL_SAFE_GET)) as any,
+ t.importSpecifier(t.identifier(INTERNAL_GET_ORIGNAL), t.identifier(INTERNAL_GET_ORIGNAL)) as any,
+ t.importSpecifier(t.identifier(INTERNAL_INLINE_STYLE), t.identifier(INTERNAL_INLINE_STYLE)) as any,
+ t.importSpecifier(t.identifier(HANDLE_LOOP_REF), t.identifier(HANDLE_LOOP_REF)) as any,
+ t.importSpecifier(t.identifier(GEN_COMP_ID), t.identifier(GEN_COMP_ID)) as any,
+ t.importSpecifier(t.identifier(GEN_LOOP_COMPID), t.identifier(GEN_LOOP_COMPID)) as any
+ )
+ if (Adapter.type !== Adapters.alipay) {
+ path.node.specifiers.push(
+ t.importSpecifier(t.identifier(PROPS_MANAGER), t.identifier(PROPS_MANAGER)) as any
+ )
+ }
+ }
+ 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')) as any
+ )
+ if (source === REDUX_PACKAGE_NAME) {
+ hasReduxBinding = true
+ specs.push(
+ t.importSpecifier(t.identifier('ReduxContext'), t.identifier('ReduxContext')) as any
+ )
+ }
+ }
+ })
+ }
+ path.traverse({
+ ImportDefaultSpecifier (path) {
+ const name = path.node.local.name
+ names.push(name)
+ },
+ ImportSpecifier (path) {
+ const name = (path.node.imported as t.Identifier).name
+ names.push(name)
+ if (source === TARO_PACKAGE_NAME && name === 'Component') {
+ path.node.local = t.identifier('__BaseComponent') as any
+ }
+ }
+ })
+ componentSourceMap.set(source, names)
+ }
+ })
+
+ if (!isImportTaro) {
+ const specifiers = [
+ 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.importSpecifier(t.identifier(HANDLE_LOOP_REF), t.identifier(HANDLE_LOOP_REF)),
+ t.importSpecifier(t.identifier(GEN_COMP_ID), t.identifier(GEN_COMP_ID)),
+ t.importSpecifier(t.identifier(GEN_LOOP_COMPID), t.identifier(GEN_LOOP_COMPID))
+ ]
+ if (Adapter.type !== Adapters.alipay) {
+ specifiers.push(t.importSpecifier(t.identifier(PROPS_MANAGER), t.identifier(PROPS_MANAGER)))
+ }
+ ast.program.body.unshift(
+ t.importDeclaration(specifiers, t.stringLiteral('@tarojs/taro'))
+ )
+ }
+
+ if (!mainClass) {
+ const code = generate(ast.program as any).code
+ return {
+ ...defaultResult,
+ ast,
+ code
+ }
+ }
+
+ if (Adapter.type === Adapters.alipay) {
+ const body = ast.program.body
+ for (const i in body) {
+ if (t.isImportDeclaration(body[i]) && !t.isImportDeclaration(body[Number(i) + 1])) {
+ body.splice(Number(i) + 1, 0, t.variableDeclaration(
+ 'const',
+ [t.variableDeclarator(
+ t.identifier('propsManager'),
+ t.memberExpression(
+ t.identifier('my'),
+ t.identifier('propsManager')
+ )
+ )]
+ ))
+ break
+ }
+ }
+ }
+
+ 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.every((node, index, body) => {
+ if (node === statementPath.node) {
+ const settingReduxProvider = t.expressionStatement(
+ t.callExpression(t.memberExpression(t.identifier('ReduxContext'), t.identifier('Provider')), [
+ t.objectExpression([
+ t.objectProperty(t.identifier('store'), t.identifier(storeName))
+ ])
+ ])
+ )
+ const ifStem = t.ifStatement(t.memberExpression(t.identifier('ReduxContext'), t.identifier('Provider')), t.blockStatement([
+ settingReduxProvider,
+ settingReduxProvider // 第一次调用初始化,第二次赋值
+ ]))
+ body.splice(index + 1, 0, t.expressionStatement(
+ t.callExpression(t.identifier('setStore'), [
+ t.identifier(storeName)
+ ])
+ ), hasReduxBinding ? ifStem : t.emptyStatement())
+ return false
+ }
+ return true
+ })
+ }
+ }
+ resetTSClassProperty(mainClass.node.body.body as any)
+ if (options.isApp) {
+ renderMethod.replaceWith(
+ t.classMethod('method', t.identifier('_createData'), [], t.blockStatement([])) as any
+ )
+ const code = generate(ast.program as any).code
+ return {
+ ...defaultResult,
+ ast,
+ code
+ }
+ }
+ result = new Transformer(mainClass as any, options.sourcePath, componentProperies, options.sourceDir!, classMethods as any).result
+ result.code = generate(ast.program as any).code
+ result.ast = ast
+ const lessThanSignReg = new RegExp(lessThanSignPlacehold, 'g')
+ result.compressedTemplate = result.template.replace(lessThanSignReg, '<')
+ result.template = prettyPrint(result.template, {
+ max_char: 0,
+ unformatted: isTestEnv ? [] : ['text']
+ })
+ result.template = result.template.replace(lessThanSignReg, '<')
+ result.imageSrcs = Array.from(imageSource)
+ return result
+}
diff --git a/packages/taro-transformer-wx/src/interface.d.ts b/packages/taro-transformer-wx/src/interface.d.ts
new file mode 100644
index 000000000000..83285b42cdde
--- /dev/null
+++ b/packages/taro-transformer-wx/src/interface.d.ts
@@ -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
+}
diff --git a/packages/taro-transformer-wx/src/jsx.ts b/packages/taro-transformer-wx/src/jsx.ts
new file mode 100644
index 000000000000..dde7b34be20e
--- /dev/null
+++ b/packages/taro-transformer-wx/src/jsx.ts
@@ -0,0 +1,348 @@
+import generate from '@babel/generator'
+import { NodePath } from '@babel/traverse'
+import * as t from '@babel/types'
+import { kebabCase, snakeCase } from 'lodash'
+
+import { Adapter, Adapters, isNewPropsSystem } from './adapter'
+import {
+ DEFAULT_Component_SET,
+ DEFAULT_Component_SET_COPY,
+ FN_PREFIX,
+ lessThanSignPlacehold,
+ SPECIAL_COMPONENT_PROPS,
+ swanSpecialAttrs,
+ THIRD_PARTY_COMPONENTS,
+ TRANSFORM_COMPONENT_PROPS} from './constant'
+import { createHTMLElement } from './create-html-element'
+import { Status } from './functional'
+import { transformOptions } from './options'
+import { codeFrameError, decodeUnicode } from './utils'
+
+export function isStartWithWX (str: string) {
+ return str[0] === 'w' && str[1] === 'x'
+}
+
+const specialComponentName = ['block', 'Block', 'slot', 'Slot']
+
+export function removeJSXThisProperty (path: NodePath) {
+ 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 | t.JSXSpreadAttribute)[], name: string) {
+ for (const attr of attrs) {
+ if (!t.isJSXAttribute(attr)) continue
+
+ 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 isSwan = Adapter.type === Adapters.swan
+ const dataString = isSwan ? `{{{...${refName ? `${loop ? '' : '$$'}${refName}` : '__data'}}}}` : `{{...${refName ? `${loop ? '' : '$$'}${refName}` : '__data'}}}`
+ const attrs = [
+ t.jSXAttribute(t.jSXIdentifier('is'), t.stringLiteral(name)),
+ t.jSXAttribute(t.jSXIdentifier('data'), t.stringLiteral(dataString))
+ ]
+ 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
+) {
+ if ((name === Adapter.forIndex || name === Adapter.forItem) && Adapter.type === Adapters.quickapp) {
+ return
+ }
+ const element = jsx.openingElement
+ // tslint:disable-next-line: strict-type-predicates
+ if (element == null || !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 buildTrueJSXAttrValue () {
+ return t.jSXExpressionContainer(t.booleanLiteral(true))
+}
+
+export function generateJSXAttr (ast: t.Node) {
+ const options = {
+ quotes: "single",
+ retainFunctionParens: true // 如果您需要 JSON 兼容的字符串,请改用此选项
+ };
+ const code = decodeUnicode(
+ generate(ast,options).code
+ )
+ .replace(/ t.isLiteral(p))
+}
+
+export function buildBlockElement (attrs: t.JSXAttribute[] = [], isView = false) {
+ let blockName = Adapter.type === Adapters.quickapp ? 'div' : 'block'
+ if (isView) {
+ blockName = 'View'
+ }
+ return t.jSXElement(
+ t.jSXOpeningElement(t.jSXIdentifier(blockName), attrs),
+ t.jSXClosingElement(t.jSXIdentifier(blockName)),
+ []
+ )
+}
+
+function parseJSXChildren (
+ children: (t.JSXElement | t.JSXText | t.JSXExpressionContainer)[]
+): string {
+ return children
+ .reduce((str, child) => {
+ if (t.isJSXText(child)) {
+ const strings: string[] = []
+ child.value.split(/(\r?\n\s*)/).forEach((val) => {
+ const value = val
+ .replace(/\u00a0/g, ' ')
+ .replace(/\u2002/g, ' ')
+ .replace(/\u2003/g, ' ')
+ 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 + `{${generateJSXAttr(child)}}`
+ }
+ return str
+ }, '')
+}
+
+export function parseJSXElement (element: t.JSXElement, isFirstEmit = false): string {
+ const children = element.children
+ const { attributes, name } = element.openingElement
+ const TRIGGER_OBSERER = Adapter.type === Adapters.swan || Adapter.type === Adapters.quickapp ? 'privateTriggerObserer' : '__triggerObserer'
+ const TRIGGER_OBSERER_KEY = Adapter.type === Adapters.quickapp ? 'privateTriggerObsererKey' : '_triggerObserer'
+ if (t.isJSXMemberExpression(name)) {
+ throw codeFrameError(name.loc, '暂不支持 JSX 成员表达式')
+ }
+ const componentName = name.name
+ const isDefaultComponent = DEFAULT_Component_SET.has(componentName as string)
+ const componentSpecialProps = SPECIAL_COMPONENT_PROPS.get(componentName as string)
+ const componentTransfromProps = TRANSFORM_COMPONENT_PROPS.get(Adapter.type)
+ let hasElseAttr = false
+ const isJSXMetHod = componentName === 'Template' && attributes.some(a => t.isJSXAttribute(a) && a.name.name === 'is' && t.isStringLiteral(a.value) && a.value.value.startsWith('render'))
+ attributes.forEach((a, index) => {
+ if (t.isJSXAttribute(a) && a.name.name === Adapter.else && !['block', 'Block'].includes(componentName as string) && !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)) {
+ if (isNewPropsSystem()) return {}
+ // @ts-ignore
+ throw codeFrameError(attr.loc, 'JSX 参数暂不支持 ...spread 表达式')
+ }
+ let name = attr.name.name
+ if (DEFAULT_Component_SET.has(componentName as string)) {
+ if (name === 'className') {
+ name = 'class'
+ }
+ if (typeof name === 'string' && /(^on[A-Z_])|(^catch[A-Z_])/.test(name) && Adapter.type === Adapters.quickapp) {
+ name = name.toLowerCase()
+ }
+ }
+ if (Adapters.quickapp === Adapter.type && !DEFAULT_Component_SET_COPY.has(componentName as string) && typeof name === 'string' && !/(^on[A-Z_])|(^catch[A-Z_])/.test(name)) {
+ name = snakeCase(name)
+ }
+ let value: string | boolean = true
+ let attrValue = attr.value
+ if (typeof name === 'string') {
+ const isAlipayOrQuickappEvent = (Adapter.type === Adapters.alipay || Adapter.type === Adapters.quickapp) && /(^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')
+ const options = {
+ quotes: 'single',
+ concise: true
+ };
+ let code = decodeUnicode(generate(attrValue.expression, options).code)
+ .replace(/"/g, "'")
+ .replace(/(this\.props\.)|(this\.state\.)/g, '')
+ .replace(/this\./g, '')
+ if (
+ Adapters.swan === Adapter.type &&
+ code !== 'true' &&
+ code !== 'false' &&
+ swanSpecialAttrs[componentName as string] &&
+ swanSpecialAttrs[componentName as string].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 {
+ const isTemplateData = isJSXMetHod && name === 'data'
+ value = isBindEvent || isAlipayOrQuickappEvent ? code : `{{${isJSXMetHod && name === 'data' ? '...' : ''}${code}}}`
+ if (isTemplateData && Adapters.swan === Adapter.type) {
+ value = `{${value}}`
+ }
+ }
+ }
+ if (Adapter.type === Adapters.swan && name === Adapter.for) {
+ value = code
+ }
+ if (t.isStringLiteral(attrValue.expression)) {
+ value = attrValue.expression.value
+ }
+ // tslint:disable-next-line: strict-type-predicates
+ } else if (attrValue === null && name !== Adapter.else) {
+ value = `{{true}}`
+ }
+ if (THIRD_PARTY_COMPONENTS.has(componentName as string) && /^bind/.test(name) && name.includes('-')) {
+ name = name.replace(/^bind/, 'bind:')
+ }
+ if (componentTransfromProps && componentTransfromProps[componentName as string]) {
+ const transfromProps = componentTransfromProps[componentName as string]
+ Object.keys(transfromProps).forEach(oriName => {
+ if (name === oriName) {
+ name = transfromProps[oriName]
+ }
+ })
+ }
+ if ((componentName === 'Input' || componentName === 'input') && name === 'maxLength') {
+ obj.maxlength = value
+ } else if (
+ (componentSpecialProps && componentSpecialProps.has(name)) ||
+ name.startsWith(FN_PREFIX) ||
+ isAlipayOrQuickappEvent
+ ) {
+ obj[name] = value
+ } else {
+ obj[isDefaultComponent && !name.includes('-') && !name.includes(':') ? kebabCase(name) : name] = value
+ }
+ }
+ if (!isDefaultComponent && !specialComponentName.includes(componentName as string) && !isNewPropsSystem()) {
+ obj[TRIGGER_OBSERER] = `{{ ${TRIGGER_OBSERER_KEY} }}`
+ }
+ return obj
+ }, {})
+ } else if (!isDefaultComponent && !specialComponentName.includes(componentName as string)) {
+ if (!isNewPropsSystem()) {
+ attributesTrans[TRIGGER_OBSERER] = `{{ ${TRIGGER_OBSERER_KEY} }}`
+ }
+ }
+
+ let elementStr
+
+ if (isFirstEmit && Adapters.quickapp === Adapter.type && !transformOptions.isRoot) {
+ const rootAttributes = Object.assign({}, attributesTrans)
+ delete rootAttributes[Adapter.if]
+ elementStr = createHTMLElement({
+ name: kebabCase(componentName as string),
+ attributes: rootAttributes,
+ value: createHTMLElement({
+ name: 'block',
+ attributes: { [Adapter.if]: attributesTrans[Adapter.if] },
+ value: parseJSXChildren(children as any)
+ })
+ }, isFirstEmit)
+ } else {
+ elementStr = createHTMLElement({
+ name: kebabCase(componentName as string),
+ attributes: attributesTrans,
+ value: parseJSXChildren(children as any)
+ }, isFirstEmit)
+ }
+
+ return elementStr
+}
+
+export function generateHTMLTemplate (template: t.JSXElement, name: string) {
+ return createHTMLElement({
+ name: 'template',
+ attributes: {
+ name
+ },
+ value: parseJSXElement(template)
+ })
+}
diff --git a/packages/taro-transformer-wx/src/lifecycle.ts b/packages/taro-transformer-wx/src/lifecycle.ts
new file mode 100644
index 000000000000..f52025784000
--- /dev/null
+++ b/packages/taro-transformer-wx/src/lifecycle.ts
@@ -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'
+}
diff --git a/packages/taro-transformer-wx/src/options.ts b/packages/taro-transformer-wx/src/options.ts
new file mode 100644
index 000000000000..78410689900b
--- /dev/null
+++ b/packages/taro-transformer-wx/src/options.ts
@@ -0,0 +1,68 @@
+import { TransformOptions } from '@babel/core'
+
+import { Adapters } from './adapter'
+import { buildVistor } from './class-method-renamer'
+import { isTestEnv } from './env'
+import { eslintValidation } from './eslint'
+import { functionalComponent, Status } from './functional'
+
+export interface Options {
+ isRoot?: boolean
+ isApp?: boolean
+ // outputPath: string,
+ sourcePath: string
+ sourceDir?: string
+ code: string
+ isTyped: boolean
+ isNormal?: boolean
+ env?: Object
+ adapter?: Adapters
+ jsxAttributeNameReplace?: Object
+ rootProps?: object
+}
+
+export let transformOptions: Options = {} as Options
+
+export const setTransformOptions = (options: Options) => {
+ transformOptions = { ...options }
+}
+
+export const buildBabelTransformOptions: () => TransformOptions = () => {
+ Status.isSFC = false
+ let plugins = [
+ require('babel-plugin-transform-do-expressions'),
+ require('babel-plugin-transform-export-extensions'),
+ require('babel-plugin-transform-flow-strip-types'),
+ [require('babel-plugin-transform-define').default, transformOptions.env]
+ ]
+ if (!transformOptions.isNormal) {
+ plugins.push(buildVistor())
+ }
+ return {
+ filename: transformOptions.sourcePath,
+ babelrc: false,
+ parserOpts: {
+ sourceType: 'module',
+ plugins: [
+ 'classProperties',
+ 'jsx',
+ 'flow',
+ 'flowComment',
+ 'trailingFunctionCommas',
+ 'asyncFunctions',
+ 'exponentiationOperator',
+ 'asyncGenerators',
+ 'objectRestSpread',
+ 'decorators',
+ 'dynamicImport',
+ 'doExpressions',
+ 'exportExtensions'
+ ] as any[]
+ },
+ plugins: plugins
+ .concat(require('babel-plugin-preval'))
+ .concat(process.env.TARO_ENV === 'rn' ? [] : functionalComponent)
+ .concat(process.env.ESLINT === 'false' || transformOptions.isNormal || transformOptions.isTyped ? [] : eslintValidation)
+ .concat((isTestEnv) ? [] : require('babel-plugin-minify-dead-code').default)
+ }
+}
diff --git a/packages/taro-transformer-wx/src/plugins.ts b/packages/taro-transformer-wx/src/plugins.ts
new file mode 100644
index 000000000000..2ef27fb089a7
--- /dev/null
+++ b/packages/taro-transformer-wx/src/plugins.ts
@@ -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