UNPKG

23.6 kBPlain TextView Raw
1import * as path from 'path'
2import * as babel from 'babel-core'
3import traverse, { NodePath } from 'babel-traverse'
4import * as t from 'babel-types'
5import * as _ from 'lodash'
6import generate from 'babel-generator'
7import wxTransformer from '@tarojs/transformer-wx'
8import {
9 REG_STYLE,
10 REG_TYPESCRIPT,
11 REG_SCRIPTS,
12 resolveScriptPath,
13 resolveStylePath,
14 replaceAliasPath,
15 isAliasPath,
16 promoteRelativePath,
17 isNpmPkg,
18 generateEnvList,
19 generateConstantsList
20} from '@tarojs/helper'
21
22import babylonConfig from '../config/babylon'
23import { convertSourceStringToAstExpression as toAst, convertAstExpressionToVariable as toVar } from '../util/astConvert'
24
25const template = require('babel-template')
26
27const reactImportDefaultName = 'React'
28let taroImportDefaultName // import default from @tarojs/taro
29let componentClassName // get app.js class name
30const providerComponentName = 'Provider'
31const taroComponentsRNProviderName = 'TCRNProvider'
32const setStoreFuncName = 'setStore'
33const routerImportDefaultName = 'TaroRouter'
34const DEVICE_RATIO = 'deviceRatio'
35
36const taroApis = ['getEnv', 'ENV_TYPE', 'eventCenter', 'Events', 'internal_safe_get', 'internal_dynamic_recursive']
37
38const PACKAGES = {
39 '@tarojs/taro': '@tarojs/taro',
40 '@tarojs/taro-rn': '@tarojs/taro-rn',
41 '@tarojs/taro-router-rn': '@tarojs/taro-router-rn',
42 '@tarojs/redux': '@tarojs/redux',
43 '@tarojs/components': '@tarojs/components',
44 '@tarojs/components-rn': '@tarojs/components-rn',
45 react: 'react',
46 'react-native': 'react-native',
47 'react-redux-rn': '@tarojs/taro-redux-rn',
48 '@tarojs/mobx': '@tarojs/mobx',
49 '@tarojs/mobx-rn': '@tarojs/mobx-rn'
50}
51
52const additionalConstructorNode = toAst('Taro._$app = this')
53const superNode = t.expressionStatement(
54 t.callExpression(
55 // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
56 // @ts-ignore
57 t.super(),
58 [
59 t.identifier('props'),
60 t.identifier('context')
61 ]
62 )
63)
64
65function getInitPxTransformNode (projectConfig) {
66 const pxTransformConfig = { designWidth: projectConfig.designWidth || 750 }
67
68 if (projectConfig.hasOwnProperty(DEVICE_RATIO)) {
69 pxTransformConfig[DEVICE_RATIO] = projectConfig.deviceRatio
70 }
71 const initPxTransformNode = toAst(`Taro.initPxTransform(${JSON.stringify(pxTransformConfig)})`)
72 return initPxTransformNode
73}
74
75function getClassPropertyVisitor ({ pages, iconPaths, isEntryFile }) {
76 return astPath => {
77 const node = astPath.node
78 const key = node.key
79 const value = node.value
80 if (key.name !== 'config' || !t.isObjectExpression(value)) return
81 // 入口文件的 config ,与页面的分开处理
82 if (isEntryFile) {
83 // 读取 config 配置
84 astPath.traverse({
85 ObjectProperty (astPath) {
86 const node = astPath.node
87 const key = node.key
88 const value = node.value
89 // if (key.name !== 'pages' || !t.isArrayExpression(value)) return
90 if (key.name === 'pages' && t.isArrayExpression(value)) {
91 // 分包
92 let root = ''
93 const rootNode = astPath.parent.properties.find(v => {
94 return v.key.name === 'root'
95 })
96 root = rootNode ? rootNode.value.value : ''
97
98 value.elements.forEach(v => {
99 if (t.isStringLiteral(v)) {
100 const pagePath = `${root}/${v.value}`.replace(/\/{2,}/g, '/')
101 pages.push(pagePath.replace(/^\//, ''))
102 }
103 })
104 astPath.remove()
105 }
106 // window
107 if (key.name === 'window' && t.isObjectExpression(value)) {
108 return
109 }
110 if (key.name === 'tabBar' && t.isObjectExpression(value)) {
111 astPath.traverse({
112 ObjectProperty (astPath) {
113 const node = astPath.node as any
114 const value = node.value.value
115 if (
116 node.key.name === 'iconPath' ||
117 node.key.value === 'iconPath' ||
118 node.key.name === 'selectedIconPath' ||
119 node.key.value === 'selectedIconPath'
120 ) {
121 if (typeof value !== 'string') return
122 const iconName = _.camelCase(value)
123 if (iconPaths.indexOf(value) === -1) {
124 iconPaths.push(value)
125 }
126 astPath.insertAfter(
127 t.objectProperty(t.identifier(node.key.name || node.key.value), t.identifier(iconName))
128 )
129 astPath.remove()
130 }
131 }
132 })
133 }
134 }
135 })
136 }
137 astPath.node.static = 'true'
138 }
139}
140
141function getJSAst (code, filePath) {
142 return wxTransformer({
143 code,
144 sourcePath: filePath,
145 isNormal: true,
146 isTyped: REG_TYPESCRIPT.test(filePath),
147 adapter: 'rn'
148 }).ast
149}
150
151/**
152 * TS 编译器会把 class property 移到构造器,
153 * 而小程序要求 `config` 和所有函数在初始化(after new Class)之后就收集到所有的函数和 config 信息,
154 * 所以当如构造器里有 this.func = () => {...} 的形式,就给他转换成普通的 classProperty function
155 * 如果有 config 就给他还原
156 */
157function resetTSClassProperty (body) {
158 for (const method of body) {
159 if (t.isClassMethod(method) && method.kind === 'constructor') {
160 for (const statement of _.cloneDeep(method.body.body)) {
161 if (t.isExpressionStatement(statement) && t.isAssignmentExpression(statement.expression)) {
162 const expr = statement.expression
163 const { left, right } = expr
164 if (t.isMemberExpression(left) && t.isThisExpression(left.object) && t.isIdentifier(left.property)) {
165 if (
166 t.isArrowFunctionExpression(right) ||
167 t.isFunctionExpression(right) ||
168 (left.property.name === 'config' && t.isObjectExpression(right))
169 ) {
170 body.push(t.classProperty(left.property, right))
171 _.remove(method.body.body, statement)
172 }
173 }
174 }
175 }
176 }
177 }
178}
179
180const ClassDeclarationOrExpression = {
181 enter (astPath) {
182 const node = astPath.node
183 if (!node.superClass) return
184 if (node.superClass.type === 'MemberExpression' && node.superClass.object.name === taroImportDefaultName) {
185 node.superClass.object.name = taroImportDefaultName
186 if (node.id === null) {
187 const renameComponentClassName = '_TaroComponentClass'
188 componentClassName = renameComponentClassName
189 astPath.replaceWith(
190 t.classDeclaration(t.identifier(renameComponentClassName), node.superClass, node.body, node.decorators || [])
191 )
192 } else {
193 componentClassName = node.id.name
194 }
195 } else if (node.superClass.name === 'Component' || node.superClass.name === 'PureComponent') {
196 resetTSClassProperty(node.body.body)
197 if (node.id === null) {
198 const renameComponentClassName = '_TaroComponentClass'
199 componentClassName = renameComponentClassName
200 astPath.replaceWith(
201 t.classDeclaration(t.identifier(renameComponentClassName), node.superClass, node.body, node.decorators || [])
202 )
203 } else {
204 componentClassName = node.id.name
205 }
206 }
207 }
208}
209
210export function parseJSCode ({ code, filePath, isEntryFile, projectConfig }) {
211 let ast
212 ast = getJSAst(code, filePath)
213 const styleFiles: string[] = []
214 const pages: string[] = [] // app.js 里面的config 配置里面的 pages
215 const iconPaths: string[] = [] // app.js 里面的config 配置里面的需要引入的 iconPath
216 let hasAddReactImportDefaultName = false
217 let providorImportName
218 let storeName
219 let hasAppExportDefault
220 let classRenderReturnJSX
221
222 let hasConstructor = false
223 let hasComponentDidMount = false
224 let hasComponentDidShow = false
225 let hasComponentDidHide = false
226 let hasComponentWillUnmount = false
227 let hasJSX = false
228
229 traverse(ast, {
230 ClassExpression: ClassDeclarationOrExpression,
231 ClassDeclaration: ClassDeclarationOrExpression,
232 ExpressionStatement (astPath) {
233 const node = astPath.node as t.ExpressionStatement
234 const expression = node.expression as t.CallExpression
235 const callee = expression.callee as t.Identifier
236 if (callee && callee.name === 'require') {
237 const argument = expression.arguments[0] as t.StringLiteral
238 const value = argument.value
239 const valueExtname = path.extname(value)
240 if (REG_STYLE.test(valueExtname)) {
241 astPath.replaceWith(t.importDeclaration([], t.stringLiteral(value)))
242 }
243 }
244 },
245 ImportDeclaration (astPath) {
246 const node = astPath.node as t.ImportDeclaration
247 const source = node.source
248 let value = source.value
249 const valueExtname = path.extname(value)
250 const specifiers = node.specifiers
251 const pathAlias = projectConfig.alias || {}
252 if (isAliasPath(value, pathAlias)) {
253 source.value = value = replaceAliasPath(filePath, value, pathAlias)
254 }
255 // 引入的包为非 npm 包
256 if (!isNpmPkg(value)) {
257 // import 样式处理
258 if (REG_STYLE.test(valueExtname)) {
259 const stylePath = path.resolve(path.dirname(filePath), value)
260 if (styleFiles.indexOf(stylePath) < 0) {
261 // 样式条件文件编译 .rn.scss
262 const realStylePath = resolveStylePath(stylePath)
263 styleFiles.push(realStylePath)
264 }
265 }
266 if (value.indexOf('.') === 0) {
267 // const pathArr = value.split('/')
268 // if (pathArr.indexOf('pages') >= 0) {
269 // astPath.remove()
270 // } else
271 if (REG_SCRIPTS.test(value) || path.extname(value) === '') {
272 const absolutePath = path.resolve(filePath, '..', value)
273 const dirname = path.dirname(absolutePath)
274 const extname = path.extname(absolutePath)
275 const realFilePath = resolveScriptPath(path.join(dirname, path.basename(absolutePath, extname)))
276 const removeExtPath = realFilePath.replace(path.extname(realFilePath), '')
277 node.source = t.stringLiteral(
278 promoteRelativePath(path.relative(filePath, removeExtPath)).replace(/\\/g, '/')
279 )
280 }
281 }
282 return
283 }
284 if (value === PACKAGES['@tarojs/taro']) {
285 const specifier = specifiers.find(item => item.type === 'ImportDefaultSpecifier')
286 if (specifier) {
287 hasAddReactImportDefaultName = true
288 taroImportDefaultName = specifier.local.name
289 specifier.local.name = reactImportDefaultName
290 } else if (!hasAddReactImportDefaultName) {
291 hasAddReactImportDefaultName = true
292 node.specifiers.unshift(t.importDefaultSpecifier(t.identifier(reactImportDefaultName)))
293 }
294 // 删除从@tarojs/taro引入的 React
295 specifiers.forEach((item, index) => {
296 if (item.type === 'ImportDefaultSpecifier') {
297 specifiers.splice(index, 1)
298 }
299 })
300 const taroApisSpecifiers: t.ImportSpecifier[] = []
301 specifiers.forEach((item, index) => {
302 if (
303 (item as t.ImportSpecifier).imported &&
304 taroApis.indexOf((item as t.ImportSpecifier).imported.name) >= 0
305 ) {
306 taroApisSpecifiers.push(
307 t.importSpecifier(
308 t.identifier((item as t.ImportSpecifier).local.name),
309 t.identifier((item as t.ImportSpecifier).imported.name)
310 )
311 )
312 specifiers.splice(index, 1)
313 }
314 })
315 source.value = PACKAGES['@tarojs/taro-rn']
316
317 if (taroApisSpecifiers.length) {
318 astPath.insertBefore(t.importDeclaration(taroApisSpecifiers, t.stringLiteral(PACKAGES['@tarojs/taro-rn'])))
319 }
320 if (!specifiers.length) {
321 astPath.remove()
322 }
323 } else if (value === PACKAGES['@tarojs/redux']) {
324 const specifier = specifiers.find(item => {
325 return t.isImportSpecifier(item) && item.imported.name === providerComponentName
326 })
327 if (specifier) {
328 providorImportName = specifier.local.name
329 } else {
330 providorImportName = providerComponentName
331 specifiers.push(t.importSpecifier(t.identifier(providerComponentName), t.identifier(providerComponentName)))
332 }
333 source.value = PACKAGES['react-redux-rn']
334 } else if (value === PACKAGES['@tarojs/mobx']) {
335 const specifier = specifiers.find(item => {
336 return t.isImportSpecifier(item) && item.imported.name === providerComponentName
337 })
338 if (specifier) {
339 providorImportName = specifier.local.name
340 } else {
341 providorImportName = providerComponentName
342 specifiers.push(t.importSpecifier(t.identifier(providerComponentName), t.identifier(providerComponentName)))
343 }
344 source.value = PACKAGES['@tarojs/mobx-rn']
345 } else if (value === PACKAGES['@tarojs/components']) {
346 source.value = PACKAGES['@tarojs/components-rn']
347 }
348 },
349 ClassProperty: getClassPropertyVisitor({ pages, iconPaths, isEntryFile }),
350 ClassMethod: {
351 enter (astPath: NodePath<t.ClassMethod>) {
352 const node = astPath.node
353 const key = node.key
354 const keyName = toVar(key)
355 // 仅关注 app.js
356 if (!isEntryFile) return
357 // 初始化 生命周期函数判断
358 if (keyName === 'constructor') {
359 hasConstructor = true
360 } else if (keyName === 'componentDidMount') {
361 hasComponentDidMount = true
362 } else if (keyName === 'componentDidShow') {
363 hasComponentDidShow = true
364 } else if (keyName === 'componentDidHide') {
365 hasComponentDidHide = true
366 } else if (keyName === 'componentWillUnmount') {
367 hasComponentWillUnmount = true
368 }
369 // 获取 app.js 的 classRenderReturnJSX
370 if (keyName === 'render') {
371 astPath.traverse({
372 BlockStatement (astPath) {
373 if (astPath.parent === node) {
374 const node = astPath.node
375 astPath.traverse({
376 ReturnStatement (astPath) {
377 if (astPath.parent === node) {
378 astPath.traverse({
379 JSXElement (astPath) {
380 classRenderReturnJSX = generate(astPath.node).code
381 }
382 })
383 }
384 }
385 })
386 }
387 }
388 })
389 }
390 }
391 },
392
393 ExportDefaultDeclaration () {
394 if (isEntryFile) {
395 hasAppExportDefault = true
396 }
397 },
398 JSXElement: {
399 exit () {
400 hasJSX = true
401 }
402 },
403 JSXOpeningElement: {
404 enter (astPath) {
405 const node = astPath.node as t.JSXOpeningElement
406 if ((node.name as any).name === 'Provider') {
407 for (const v of node.attributes) {
408 if (v.name.name !== 'store') continue
409 storeName = (v.value as any).expression.name
410 break
411 }
412 }
413 }
414 },
415 Program: {
416 exit (astPath) {
417 const node = astPath.node as t.Program
418 astPath.traverse({
419 ClassMethod (astPath) {
420 const node = astPath.node
421 const key = node.key as t.Identifier
422
423 const keyName = toVar(key)
424 const isComponentDidMount = keyName === 'componentDidMount'
425 const isComponentWillUnmount = keyName === 'componentWillUnmount'
426 const isConstructor = keyName === 'constructor'
427
428 if (!isEntryFile) return
429
430 if (hasConstructor && isConstructor) {
431 node.body.body.push(additionalConstructorNode)
432 }
433
434 if (hasComponentDidShow && isComponentDidMount) {
435 const componentDidShowCallNode = toAst('this.componentDidShow()')
436 node.body.body.push(componentDidShowCallNode)
437 }
438
439 if (hasComponentDidHide && isComponentWillUnmount) {
440 const componentDidHideCallNode = toAst('this.componentDidHide()')
441 node.body.body.unshift(componentDidHideCallNode)
442 }
443
444 if (key.name === 'render') {
445 let funcBody = `
446 <${taroComponentsRNProviderName}>
447 ${classRenderReturnJSX}
448 </${taroComponentsRNProviderName}>`
449
450 if (pages.length > 0) {
451 funcBody = `
452 <${taroComponentsRNProviderName}>
453 <RootStack/>
454 </${taroComponentsRNProviderName}>`
455 }
456
457 if (providerComponentName && storeName) {
458 // 使用redux 或 mobx
459 funcBody = `
460 <${providorImportName} store={${storeName}}>
461 ${funcBody}
462 </${providorImportName}>`
463 }
464 node.body = template(`{return (${funcBody});}`, babylonConfig as any)() as any
465 }
466 },
467
468 ClassBody: {
469 exit (astPath: NodePath<t.ClassBody>) {
470 if (!isEntryFile) return
471 const node = astPath.node
472 if (hasComponentDidShow && !hasComponentDidMount) {
473 node.body.push(
474 t.classMethod(
475 'method',
476 t.identifier('componentDidMount'),
477 [],
478 t.blockStatement([toAst('this.componentDidShow && this.componentDidShow()') as t.Statement]),
479 false,
480 false
481 )
482 )
483 }
484 if (hasComponentDidHide && !hasComponentWillUnmount) {
485 node.body.push(
486 t.classMethod(
487 'method',
488 t.identifier('componentWillUnmount'),
489 [],
490 t.blockStatement([toAst('this.componentDidHide && this.componentDidHide()') as t.Statement]),
491 false,
492 false
493 )
494 )
495 }
496 if (!hasConstructor) {
497 node.body.unshift(
498 t.classMethod(
499 'constructor',
500 t.identifier('constructor'),
501 [t.identifier('props'), t.identifier('context')],
502 t.blockStatement([superNode, additionalConstructorNode] as t.Statement[]),
503 false,
504 false
505 )
506 )
507 }
508 }
509 },
510 CallExpression (astPath) {
511 const node = astPath.node
512 const callee = node.callee as t.Identifier
513 const calleeName = callee.name
514 const parentPath = astPath.parentPath
515
516 if (t.isMemberExpression(callee)) {
517 const object = callee.object as t.Identifier
518 const property = callee.property as t.Identifier
519 if (object.name === taroImportDefaultName && property.name === 'render') {
520 astPath.remove()
521 }
522 } else {
523 if (calleeName === setStoreFuncName) {
524 if (
525 parentPath.isAssignmentExpression() ||
526 parentPath.isExpressionStatement() ||
527 parentPath.isVariableDeclarator()
528 ) {
529 parentPath.remove()
530 }
531 }
532 }
533 }
534 })
535 // insert React
536 if (hasJSX) {
537 node.body.unshift(template("import React from 'react'", babylonConfig as any)())
538 }
539 // import Taro from @tarojs/taro-rn
540 if (taroImportDefaultName) {
541 const importTaro = template(
542 `import ${taroImportDefaultName} from '${PACKAGES['@tarojs/taro-rn']}'`,
543 babylonConfig as any
544 )()
545 node.body.unshift(importTaro as any)
546 }
547
548 if (isEntryFile) {
549 // 注入 import page from 'XXX'
550 pages.forEach(item => {
551 const pagePath = item.startsWith('/') ? item : `/${item}`
552 const screenName = _.camelCase(pagePath)
553 const importScreen = template(`import ${screenName} from '.${pagePath}'`, babylonConfig as any)()
554 node.body.unshift(importScreen as any)
555 })
556
557 // import tabBar icon
558 iconPaths.forEach(item => {
559 const iconPath = item.startsWith('/') ? item : `/${item}`
560 const iconName = _.camelCase(iconPath)
561 const importIcon = template(`import ${iconName} from '.${iconPath}'`, babylonConfig as any)()
562 node.body.unshift(importIcon as any)
563 })
564
565 // Taro.initRouter 生成 RootStack
566 const routerPages = pages
567 .map(item => {
568 const pagePath = item.startsWith('/') ? item : `/${item}`
569 const screenName = _.camelCase(pagePath)
570 return `['${item}',${screenName}]`
571 })
572 .join(',')
573 node.body.push(template(
574 `const RootStack = ${routerImportDefaultName}.initRouter(
575 [${routerPages}],
576 ${taroImportDefaultName},
577 App.config
578 )`,
579 babylonConfig as any
580 )() as any)
581 // initNativeApi
582 const initNativeApi = template(
583 `${taroImportDefaultName}.initNativeApi(${taroImportDefaultName})`,
584 babylonConfig as any
585 )()
586 node.body.push(initNativeApi as any)
587
588 // import @tarojs/taro-router-rn
589 const importTaroRouter = template(
590 `import TaroRouter from '${PACKAGES['@tarojs/taro-router-rn']}'`,
591 babylonConfig as any
592 )()
593 node.body.unshift(importTaroRouter as any)
594
595 // 根节点嵌套组件提供的 provider
596 const importTCRNProvider = template(
597 `import { Provider as ${taroComponentsRNProviderName} } from '${PACKAGES['@tarojs/components-rn']}'`,
598 babylonConfig
599 )()
600 node.body.unshift(importTCRNProvider)
601
602 // Taro.initPxTransform
603 node.body.push(getInitPxTransformNode(projectConfig) as any)
604
605 // export default App
606 if (!hasAppExportDefault) {
607 const appExportDefault = template(`export default ${componentClassName}`, babylonConfig as any)()
608 node.body.push(appExportDefault as any)
609 }
610 }
611 }
612 }
613 })
614 const constantsReplaceList = Object.assign(
615 {
616 'process.env.TARO_ENV': 'rn'
617 },
618 generateEnvList(projectConfig.env || {}),
619 generateConstantsList(projectConfig.defineConstants || {})
620 )
621 // TODO 使用 babel-plugin-transform-jsx-to-stylesheet 处理 JSX 里面样式的处理,删除无效的样式引入待优化
622
623 const plugins = [
624 [require('babel-plugin-transform-jsx-to-stylesheet'), { filePath }],
625 [require('babel-plugin-danger-remove-unused-import'), { ignore: ['@tarojs/taro', 'react', 'react-native', 'nervjs'] }],
626 [require('babel-plugin-transform-define').default, constantsReplaceList]
627 ]
628
629 // const babelConfig = projectConfig.plugins.babel
630 // const plugins = babelConfig.plugins.concat(extraBabelPlugins)
631 const newBabelConfig = Object.assign({}, { plugins })
632
633 ast = babel.transformFromAst(ast, code, newBabelConfig).ast
634
635 return {
636 code: unescape(generate(ast).code.replace(/\\u/g, '%u')),
637 styleFiles
638 }
639}