import * as path from 'path' import * as babel from 'babel-core' import traverse, { NodePath } from 'babel-traverse' import * as t from 'babel-types' import * as _ from 'lodash' import generate from 'babel-generator' import wxTransformer from '@tarojs/transformer-wx' import { REG_STYLE, REG_TYPESCRIPT, REG_SCRIPTS, resolveScriptPath, resolveStylePath, replaceAliasPath, isAliasPath, promoteRelativePath, isNpmPkg, generateEnvList, generateConstantsList } from '@tarojs/helper' import babylonConfig from '../config/babylon' import { convertSourceStringToAstExpression as toAst, convertAstExpressionToVariable as toVar } from '../util/astConvert' const template = require('babel-template') const reactImportDefaultName = 'React' let taroImportDefaultName // import default from @tarojs/taro let componentClassName // get app.js class name const providerComponentName = 'Provider' const taroComponentsRNProviderName = 'TCRNProvider' const setStoreFuncName = 'setStore' const routerImportDefaultName = 'TaroRouter' const DEVICE_RATIO = 'deviceRatio' const taroApis = ['getEnv', 'ENV_TYPE', 'eventCenter', 'Events', 'internal_safe_get', 'internal_dynamic_recursive'] const PACKAGES = { '@tarojs/taro': '@tarojs/taro', '@agreejs/taro-rn': '@agreejs/taro-rn', '@tarojs/taro-router-rn': '@tarojs/taro-router-rn', '@tarojs/redux': '@tarojs/redux', '@tarojs/components': '@tarojs/components', '@tarojs/components-rn': '@tarojs/components-rn', react: 'react', 'react-native': 'react-native', 'react-redux-rn': '@tarojs/taro-redux-rn', '@tarojs/mobx': '@tarojs/mobx', '@tarojs/mobx-rn': '@tarojs/mobx-rn' } const additionalConstructorNode = toAst('Taro._$app = this') const superNode = t.expressionStatement( t.callExpression( // @ts-ignore t.super(), [ t.identifier('props'), t.identifier('context') ] ) ) function getInitPxTransformNode (projectConfig) { const pxTransformConfig = { designWidth: projectConfig.designWidth || 750 } if (projectConfig.hasOwnProperty(DEVICE_RATIO)) { pxTransformConfig[DEVICE_RATIO] = projectConfig.deviceRatio } const initPxTransformNode = toAst(`Taro.initPxTransform(${JSON.stringify(pxTransformConfig)})`) return initPxTransformNode } function getClassPropertyVisitor ({ pages, iconPaths, isEntryFile }) { return astPath => { const node = astPath.node const key = node.key const value = node.value if (key.name !== 'config' || !t.isObjectExpression(value)) return // 入口文件的 config ,与页面的分开处理 if (isEntryFile) { // 读取 config 配置 astPath.traverse({ ObjectProperty (astPath) { const node = astPath.node const key = node.key const value = node.value // if (key.name !== 'pages' || !t.isArrayExpression(value)) return if (key.name === 'pages' && t.isArrayExpression(value)) { // 分包 let root = '' const rootNode = astPath.parent.properties.find(v => { return v.key.name === 'root' }) root = rootNode ? rootNode.value.value : '' value.elements.forEach(v => { if (t.isStringLiteral(v)) { const pagePath = `${root}/${v.value}`.replace(/\/{2,}/g, '/') pages.push(pagePath.replace(/^\//, '')) } }) astPath.remove() } // window if (key.name === 'window' && t.isObjectExpression(value)) { return } if (key.name === 'tabBar' && t.isObjectExpression(value)) { astPath.traverse({ ObjectProperty (astPath) { const node = astPath.node as any const value = node.value.value if ( node.key.name === 'iconPath' || node.key.value === 'iconPath' || node.key.name === 'selectedIconPath' || node.key.value === 'selectedIconPath' ) { if (typeof value !== 'string') return const iconName = _.camelCase(value) if (iconPaths.indexOf(value) === -1) { iconPaths.push(value) } astPath.insertAfter( t.objectProperty(t.identifier(node.key.name || node.key.value), t.identifier(iconName)) ) astPath.remove() } } }) } } }) } astPath.node.static = 'true' } } function getJSAst (code, filePath) { return wxTransformer({ code, sourcePath: filePath, isNormal: true, isTyped: REG_TYPESCRIPT.test(filePath), adapter: 'rn' }).ast } /** * TS 编译器会把 class property 移到构造器, * 而小程序要求 `config` 和所有函数在初始化(after new Class)之后就收集到所有的函数和 config 信息, * 所以当如构造器里有 this.func = () => {...} 的形式,就给他转换成普通的 classProperty function * 如果有 config 就给他还原 */ function resetTSClassProperty (body) { for (const method of body) { if (t.isClassMethod(method) && method.kind === 'constructor') { for (const statement of _.cloneDeep(method.body.body)) { 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)) ) { body.push(t.classProperty(left.property, right)) _.remove(method.body.body, statement) } } } } } } } const ClassDeclarationOrExpression = { enter (astPath) { const node = astPath.node if (!node.superClass) return if (node.superClass.type === 'MemberExpression' && node.superClass.object.name === taroImportDefaultName) { node.superClass.object.name = taroImportDefaultName if (node.id === null) { const renameComponentClassName = '_TaroComponentClass' componentClassName = renameComponentClassName astPath.replaceWith( t.classDeclaration(t.identifier(renameComponentClassName), node.superClass, node.body, node.decorators || []) ) } else { componentClassName = node.id.name } } else if (node.superClass.name === 'Component' || node.superClass.name === 'PureComponent') { resetTSClassProperty(node.body.body) if (node.id === null) { const renameComponentClassName = '_TaroComponentClass' componentClassName = renameComponentClassName astPath.replaceWith( t.classDeclaration(t.identifier(renameComponentClassName), node.superClass, node.body, node.decorators || []) ) } else { componentClassName = node.id.name } } } } export function parseJSCode ({ code, filePath, isEntryFile, projectConfig }) { let ast ast = getJSAst(code, filePath) const styleFiles: string[] = [] const pages: string[] = [] // app.js 里面的config 配置里面的 pages const iconPaths: string[] = [] // app.js 里面的config 配置里面的需要引入的 iconPath let hasAddReactImportDefaultName = false let providorImportName let storeName let hasAppExportDefault let classRenderReturnJSX let hasConstructor = false let hasComponentDidMount = false let hasComponentDidShow = false let hasComponentDidHide = false let hasComponentWillUnmount = false let hasJSX = false traverse(ast, { ClassExpression: ClassDeclarationOrExpression, ClassDeclaration: ClassDeclarationOrExpression, ExpressionStatement (astPath) { const node = astPath.node as t.ExpressionStatement const expression = node.expression as t.CallExpression const callee = expression.callee as t.Identifier if (callee && callee.name === 'require') { const argument = expression.arguments[0] as t.StringLiteral const value = argument.value const valueExtname = path.extname(value) if (REG_STYLE.test(valueExtname)) { astPath.replaceWith(t.importDeclaration([], t.stringLiteral(value))) } } }, ImportDeclaration (astPath) { const node = astPath.node as t.ImportDeclaration const source = node.source let value = source.value const valueExtname = path.extname(value) const specifiers = node.specifiers const pathAlias = projectConfig.alias || {} if (isAliasPath(value, pathAlias)) { source.value = value = replaceAliasPath(filePath, value, pathAlias) } // 引入的包为非 npm 包 if (!isNpmPkg(value)) { // import 样式处理 if (REG_STYLE.test(valueExtname)) { const stylePath = path.resolve(path.dirname(filePath), value) if (styleFiles.indexOf(stylePath) < 0) { // 样式条件文件编译 .rn.scss const realStylePath = resolveStylePath(stylePath) styleFiles.push(realStylePath) } } if (value.indexOf('.') === 0) { // const pathArr = value.split('/') // if (pathArr.indexOf('pages') >= 0) { // astPath.remove() // } else if (REG_SCRIPTS.test(value) || path.extname(value) === '') { const absolutePath = path.resolve(filePath, '..', value) const dirname = path.dirname(absolutePath) const extname = path.extname(absolutePath) const realFilePath = resolveScriptPath(path.join(dirname, path.basename(absolutePath, extname))) const removeExtPath = realFilePath.replace(path.extname(realFilePath), '') node.source = t.stringLiteral( promoteRelativePath(path.relative(filePath, removeExtPath)).replace(/\\/g, '/') ) } } return } if (value === PACKAGES['@tarojs/taro']) { const specifier = specifiers.find(item => item.type === 'ImportDefaultSpecifier') if (specifier) { hasAddReactImportDefaultName = true taroImportDefaultName = specifier.local.name specifier.local.name = reactImportDefaultName } else if (!hasAddReactImportDefaultName) { hasAddReactImportDefaultName = true node.specifiers.unshift(t.importDefaultSpecifier(t.identifier(reactImportDefaultName))) } // 删除从@tarojs/taro引入的 React specifiers.forEach((item, index) => { if (item.type === 'ImportDefaultSpecifier') { specifiers.splice(index, 1) } }) const taroApisSpecifiers: t.ImportSpecifier[] = [] specifiers.forEach((item, index) => { if ( (item as t.ImportSpecifier).imported && taroApis.indexOf((item as t.ImportSpecifier).imported.name) >= 0 ) { taroApisSpecifiers.push( t.importSpecifier( t.identifier((item as t.ImportSpecifier).local.name), t.identifier((item as t.ImportSpecifier).imported.name) ) ) specifiers.splice(index, 1) } }) source.value = PACKAGES['@agreejs/taro-rn'] if (taroApisSpecifiers.length) { astPath.insertBefore(t.importDeclaration(taroApisSpecifiers, t.stringLiteral(PACKAGES['@agreejs/taro-rn']))) } if (!specifiers.length) { astPath.remove() } } else if (value === PACKAGES['@tarojs/redux']) { const specifier = specifiers.find(item => { return t.isImportSpecifier(item) && item.imported.name === providerComponentName }) if (specifier) { providorImportName = specifier.local.name } else { providorImportName = providerComponentName specifiers.push(t.importSpecifier(t.identifier(providerComponentName), t.identifier(providerComponentName))) } source.value = PACKAGES['react-redux-rn'] } else if (value === PACKAGES['@tarojs/mobx']) { const specifier = specifiers.find(item => { return t.isImportSpecifier(item) && item.imported.name === providerComponentName }) if (specifier) { providorImportName = specifier.local.name } else { providorImportName = providerComponentName specifiers.push(t.importSpecifier(t.identifier(providerComponentName), t.identifier(providerComponentName))) } source.value = PACKAGES['@tarojs/mobx-rn'] } else if (value === PACKAGES['@tarojs/components']) { source.value = PACKAGES['@tarojs/components-rn'] } }, ClassProperty: getClassPropertyVisitor({ pages, iconPaths, isEntryFile }), ClassMethod: { enter (astPath: NodePath) { const node = astPath.node const key = node.key const keyName = toVar(key) // 仅关注 app.js if (!isEntryFile) return // 初始化 生命周期函数判断 if (keyName === 'constructor') { hasConstructor = true } else if (keyName === 'componentDidMount') { hasComponentDidMount = true } else if (keyName === 'componentDidShow') { hasComponentDidShow = true } else if (keyName === 'componentDidHide') { hasComponentDidHide = true } else if (keyName === 'componentWillUnmount') { hasComponentWillUnmount = true } // 获取 app.js 的 classRenderReturnJSX if (keyName === 'render') { astPath.traverse({ BlockStatement (astPath) { if (astPath.parent === node) { const node = astPath.node astPath.traverse({ ReturnStatement (astPath) { if (astPath.parent === node) { astPath.traverse({ JSXElement (astPath) { classRenderReturnJSX = generate(astPath.node).code } }) } } }) } } }) } } }, ExportDefaultDeclaration () { if (isEntryFile) { hasAppExportDefault = true } }, JSXElement: { exit () { hasJSX = true } }, JSXOpeningElement: { enter (astPath) { const node = astPath.node as t.JSXOpeningElement if ((node.name as any).name === 'Provider') { for (const v of node.attributes) { if (v.name.name !== 'store') continue storeName = (v.value as any).expression.name break } } } }, Program: { exit (astPath) { const node = astPath.node as t.Program astPath.traverse({ ClassMethod (astPath) { const node = astPath.node const key = node.key as t.Identifier const keyName = toVar(key) const isComponentDidMount = keyName === 'componentDidMount' const isComponentWillUnmount = keyName === 'componentWillUnmount' const isConstructor = keyName === 'constructor' if (!isEntryFile) return if (hasConstructor && isConstructor) { node.body.body.push(additionalConstructorNode) } if (hasComponentDidShow && isComponentDidMount) { const componentDidShowCallNode = toAst('this.componentDidShow()') node.body.body.push(componentDidShowCallNode) } if (hasComponentDidHide && isComponentWillUnmount) { const componentDidHideCallNode = toAst('this.componentDidHide()') node.body.body.unshift(componentDidHideCallNode) } if (key.name === 'render') { let funcBody = ` <${taroComponentsRNProviderName}> ${classRenderReturnJSX} ` if (pages.length > 0) { funcBody = ` <${taroComponentsRNProviderName}> ` } if (providerComponentName && storeName) { // 使用redux 或 mobx funcBody = ` <${providorImportName} store={${storeName}}> ${funcBody} ` } node.body = template(`{return (${funcBody});}`, babylonConfig as any)() as any } }, ClassBody: { exit (astPath: NodePath) { if (!isEntryFile) return const node = astPath.node if (hasComponentDidShow && !hasComponentDidMount) { node.body.push( t.classMethod( 'method', t.identifier('componentDidMount'), [], t.blockStatement([toAst('this.componentDidShow && this.componentDidShow()') as t.Statement]), false, false ) ) } if (hasComponentDidHide && !hasComponentWillUnmount) { node.body.push( t.classMethod( 'method', t.identifier('componentWillUnmount'), [], t.blockStatement([toAst('this.componentDidHide && this.componentDidHide()') as t.Statement]), false, false ) ) } if (!hasConstructor) { node.body.unshift( t.classMethod( 'constructor', t.identifier('constructor'), [t.identifier('props'), t.identifier('context')], t.blockStatement([superNode, additionalConstructorNode] as t.Statement[]), false, false ) ) } } }, CallExpression (astPath) { const node = astPath.node const callee = node.callee as any const calleeName = callee.name const parentPath = astPath.parentPath if (t.isMemberExpression(callee)) { const object = callee.object as t.Identifier const property = callee.property as t.Identifier if (object.name === taroImportDefaultName && property.name === 'render') { astPath.remove() } } else { if (calleeName === setStoreFuncName) { if ( parentPath.isAssignmentExpression() || parentPath.isExpressionStatement() || parentPath.isVariableDeclarator() ) { parentPath.remove() } } } } }) // insert React if (hasJSX) { node.body.unshift(template("import React from 'react'", babylonConfig as any)()) } // import Taro from @tarojs/taro-rn if (taroImportDefaultName) { const importTaro = template( `import ${taroImportDefaultName} from '${PACKAGES['@agreejs/taro-rn']}'`, babylonConfig as any )() node.body.unshift(importTaro as any) } if (isEntryFile) { // 注入 import page from 'XXX' pages.forEach(item => { const pagePath = item.startsWith('/') ? item : `/${item}` const screenName = _.camelCase(pagePath) const importScreen = template(`import ${screenName} from '.${pagePath}'`, babylonConfig as any)() node.body.unshift(importScreen as any) }) // import tabBar icon iconPaths.forEach(item => { const iconPath = item.startsWith('/') ? item : `/${item}` const iconName = _.camelCase(iconPath) const importIcon = template(`import ${iconName} from '.${iconPath}'`, babylonConfig as any)() node.body.unshift(importIcon as any) }) // Taro.initRouter 生成 RootStack const routerPages = pages .map(item => { const pagePath = item.startsWith('/') ? item : `/${item}` const screenName = _.camelCase(pagePath) return `['${item}',${screenName}]` }) .join(',') node.body.push(template( `const RootStack = ${routerImportDefaultName}.initRouter( [${routerPages}], ${taroImportDefaultName}, App.config )`, babylonConfig as any )() as any) // initNativeApi const initNativeApi = template( `${taroImportDefaultName}.initNativeApi(${taroImportDefaultName})`, babylonConfig as any )() node.body.push(initNativeApi as any) // import @tarojs/taro-router-rn const importTaroRouter = template( `import TaroRouter from '${PACKAGES['@tarojs/taro-router-rn']}'`, babylonConfig as any )() node.body.unshift(importTaroRouter as any) // 根节点嵌套组件提供的 provider const importTCRNProvider = template( `import { Provider as ${taroComponentsRNProviderName} } from '${PACKAGES['@tarojs/components-rn']}'`, babylonConfig )() node.body.unshift(importTCRNProvider) // Taro.initPxTransform node.body.push(getInitPxTransformNode(projectConfig) as any) // export default App if (!hasAppExportDefault) { const appExportDefault = template(`export default ${componentClassName}`, babylonConfig as any)() node.body.push(appExportDefault as any) } } } } }) const constantsReplaceList = Object.assign( { 'process.env.TARO_ENV': 'rn' }, generateEnvList(projectConfig.env || {}), generateConstantsList(projectConfig.defineConstants || {}) ) // TODO 使用 babel-plugin-transform-jsx-to-stylesheet 处理 JSX 里面样式的处理,删除无效的样式引入待优化 const plugins = [ [require('babel-plugin-transform-jsx-to-stylesheet'), { filePath }], [require('babel-plugin-danger-remove-unused-import'), { ignore: ['@tarojs/taro', 'react', 'react-native', 'nervjs'] }], [require('babel-plugin-transform-define').default, constantsReplaceList] ] // const babelConfig = projectConfig.plugins.babel // const plugins = babelConfig.plugins.concat(extraBabelPlugins) const newBabelConfig = Object.assign({}, { plugins }) ast = babel.transformFromAst(ast, code, newBabelConfig).ast return { code: unescape(generate(ast).code.replace(/\\u/g, '%u')), styleFiles } }