UNPKG

35.2 kBPlain TextView Raw
1import template from '@babel/template'
2import traverse, { NodePath } from '@babel/traverse'
3import * as t from '@babel/types'
4// import * as inquirer from 'inquirer'
5import {
6 chalk,
7 CSS_IMPORT_REG,
8 emptyDirectory,
9 pascalCase,
10 printLog,
11 processTypeEnum,
12 promoteRelativePath,
13 REG_IMAGE,
14 REG_TYPESCRIPT,
15 REG_URL,
16 resolveScriptPath
17} from '@tarojs/helper'
18import { AppConfig, TabBar } from '@tarojs/taro'
19import * as taroize from '@tarojs/taroize'
20import wxTransformer from '@tarojs/transformer-wx'
21import * as fs from 'fs-extra'
22import * as path from 'path'
23import * as postcss from 'postcss'
24import * as unitTransform from 'postcss-taro-unit-transform'
25import * as prettier from 'prettier'
26
27import babylonConfig from '../config/babylon'
28import Creator from '../create/creator'
29import { getPkgVersion } from '../util'
30import { generateMinimalEscapeCode } from '../util/astConvert'
31import { analyzeImportUrl, incrementId } from './helper'
32
33const prettierJSConfig: prettier.Options = {
34 semi: false,
35 singleQuote: true,
36 parser: 'babel'
37}
38
39const OUTPUT_STYLE_EXTNAME = '.scss'
40
41const WX_GLOBAL_FN = new Set<string>(['getApp', 'getCurrentPages', 'requirePlugin', 'Behavior'])
42
43interface IComponent {
44 name: string
45 path: string
46}
47
48interface IImport {
49 ast: t.File
50 name: string
51 wxs?: boolean
52}
53
54interface IParseAstOptions {
55 ast: t.File
56 sourceFilePath: string
57 outputFilePath: string
58 importStylePath?: string | null
59 depComponents?: Set<IComponent>
60 imports?: IImport[]
61 isApp?: boolean
62}
63
64interface ITaroizeOptions {
65 json?: string
66 script?: string
67 wxml?: string
68 path?: string
69 rootPath?: string
70}
71
72function processStyleImports (content: string, processFn: (a: string, b: string) => string) {
73 const style: string[] = []
74 const imports: string[] = []
75 const styleReg = new RegExp('.wxss')
76 content = content.replace(CSS_IMPORT_REG, (m, _$1, $2) => {
77 if (styleReg.test($2)) {
78 style.push(m)
79 imports.push($2)
80 if (processFn) {
81 return processFn(m, $2)
82 }
83 return ''
84 }
85 if (processFn) {
86 return processFn(m, $2)
87 }
88 return m
89 })
90 return {
91 content,
92 style,
93 imports
94 }
95}
96
97export default class Convertor {
98 root: string
99 convertRoot: string
100 convertDir: string
101 importsDir: string
102 fileTypes: any
103 pages: Set<string>
104 components: Set<IComponent>
105 hadBeenCopyedFiles: Set<string>
106 hadBeenBuiltComponents: Set<string>
107 hadBeenBuiltImports: Set<string>
108 entryJSPath: string
109 entryJSONPath: string
110 entryStylePath: string
111 entryJSON: AppConfig & {usingComponents?: Record<string, string>}
112 entryStyle: string
113 entryUsingComponents: Record<string, string>
114 framework: 'react' | 'vue'
115
116 constructor (root) {
117 this.root = root
118 this.convertRoot = path.join(this.root, 'taroConvert')
119 this.convertDir = path.join(this.convertRoot, 'src')
120 this.importsDir = path.join(this.convertDir, 'imports')
121 this.fileTypes = {
122 TEMPL: '.wxml',
123 STYLE: '.wxss',
124 CONFIG: '.json',
125 SCRIPT: '.js'
126 }
127 this.pages = new Set<string>()
128 this.components = new Set<IComponent>()
129 this.hadBeenCopyedFiles = new Set<string>()
130 this.hadBeenBuiltComponents = new Set<string>()
131 this.hadBeenBuiltImports = new Set<string>()
132 this.init()
133 }
134
135 init () {
136 console.log(chalk.green('开始代码转换...'))
137 this.initConvert()
138 this.getApp()
139 this.getPages()
140 this.getSitemapLocation()
141 this.getSubPackages()
142 }
143
144 initConvert () {
145 if (fs.existsSync(this.convertRoot)) {
146 emptyDirectory(this.convertRoot, { excludes: ['node_modules'] })
147 } else {
148 fs.ensureDirSync(this.convertRoot)
149 }
150 }
151
152 wxsIncrementId = incrementId()
153
154 parseAst ({
155 ast,
156 sourceFilePath,
157 outputFilePath,
158 importStylePath,
159 depComponents,
160 imports = []
161 }: IParseAstOptions): { ast: t.File, scriptFiles: Set<string> } {
162 const scriptFiles = new Set<string>()
163 // eslint-disable-next-line @typescript-eslint/no-this-alias
164 const self = this
165 let componentClassName: string
166 let needInsertImportTaro = false
167 traverse(ast, {
168 Program: {
169 enter (astPath) {
170 astPath.traverse({
171 ClassDeclaration (astPath) {
172 const node = astPath.node
173 let isTaroComponent = false
174 if (node.superClass) {
175 astPath.traverse({
176 ClassMethod (astPath) {
177 if (astPath.get('key').isIdentifier({ name: 'render' })) {
178 astPath.traverse({
179 JSXElement () {
180 isTaroComponent = true
181 }
182 })
183 }
184 }
185 })
186 if (isTaroComponent) {
187 componentClassName = node.id.name
188 }
189 }
190 },
191
192 ClassExpression (astPath) {
193 const node = astPath.node
194 if (node.superClass) {
195 let isTaroComponent = false
196 astPath.traverse({
197 ClassMethod (astPath) {
198 if (astPath.get('key').isIdentifier({ name: 'render' })) {
199 astPath.traverse({
200 JSXElement () {
201 isTaroComponent = true
202 }
203 })
204 }
205 }
206 })
207 if (isTaroComponent) {
208 if (node.id === null) {
209 const parentNode = astPath.parentPath.node as t.VariableDeclarator
210 if (t.isVariableDeclarator(astPath.parentPath)) {
211 componentClassName = (parentNode.id as t.Identifier).name
212 }
213 } else {
214 componentClassName = node.id!.name
215 }
216 }
217 }
218 },
219 ExportDefaultDeclaration (astPath) {
220 const node = astPath.node
221 const declaration = node.declaration
222 if (declaration && (declaration.type === 'ClassDeclaration' || declaration.type === 'ClassExpression')) {
223 const superClass = declaration.superClass
224 if (superClass) {
225 let isTaroComponent = false
226 astPath.traverse({
227 ClassMethod (astPath) {
228 if (astPath.get('key').isIdentifier({ name: 'render' })) {
229 astPath.traverse({
230 JSXElement () {
231 isTaroComponent = true
232 }
233 })
234 }
235 }
236 })
237 if (isTaroComponent) {
238 componentClassName = declaration.id!.name
239 }
240 }
241 }
242 },
243 ImportDeclaration (astPath) {
244 const node = astPath.node
245 const source = node.source
246 const value = source.value
247 analyzeImportUrl(self.root, sourceFilePath, scriptFiles, source, value)
248 },
249 CallExpression (astPath) {
250 const node = astPath.node
251 const calleePath = astPath.get('callee')
252 const callee = calleePath.node
253 if (callee.type === 'Identifier') {
254 if (callee.name === 'require') {
255 const args = node.arguments as Array<t.StringLiteral>
256 const value = args[0].value
257 analyzeImportUrl(self.root, sourceFilePath, scriptFiles, args[0], value)
258 } else if (WX_GLOBAL_FN.has(callee.name)) {
259 calleePath.replaceWith(t.memberExpression(t.identifier('Taro'), callee as t.Identifier))
260 needInsertImportTaro = true
261 }
262 }
263 },
264
265 MemberExpression (astPath) {
266 const node = astPath.node
267 const object = node.object
268 if (t.isIdentifier(object) && object.name === 'wx') {
269 node.object = t.identifier('Taro')
270 needInsertImportTaro = true
271 }
272 }
273 })
274 },
275 exit (astPath) {
276 const bodyNode = astPath.get('body') as NodePath<t.Node>[]
277 const lastImport = bodyNode.filter(p => p.isImportDeclaration()).pop()
278 const hasTaroImport = bodyNode.some(p => p.isImportDeclaration() && p.node.source.value === '@tarojs/taro')
279 if (needInsertImportTaro && !hasTaroImport) {
280 (astPath.node as t.Program).body.unshift(
281 t.importDeclaration([t.importDefaultSpecifier(t.identifier('Taro'))], t.stringLiteral('@tarojs/taro'))
282 )
283 }
284 astPath.traverse({
285 StringLiteral (astPath) {
286 const value = astPath.node.value
287 const extname = path.extname(value)
288 if (extname && REG_IMAGE.test(extname) && !REG_URL.test(value)) {
289 let sourceImagePath: string
290 if (path.isAbsolute(value)) {
291 sourceImagePath = path.join(self.root, value)
292 } else {
293 sourceImagePath = path.resolve(sourceFilePath, '..', value)
294 }
295 const imageRelativePath = promoteRelativePath(path.relative(sourceFilePath, sourceImagePath))
296 const outputImagePath = self.getDistFilePath(sourceImagePath)
297 if (fs.existsSync(sourceImagePath)) {
298 self.copyFileToTaro(sourceImagePath, outputImagePath)
299 printLog(processTypeEnum.COPY, '图片', self.generateShowPath(outputImagePath))
300 } else if (!t.isBinaryExpression(astPath.parent) || astPath.parent.operator !== '+') {
301 printLog(processTypeEnum.ERROR, '图片不存在', self.generateShowPath(sourceImagePath))
302 }
303 if (astPath.parentPath.isVariableDeclarator()) {
304 astPath.replaceWith(t.callExpression(t.identifier('require'), [t.stringLiteral(imageRelativePath)]))
305 } else if (astPath.parentPath.isJSXAttribute()) {
306 astPath.replaceWith(
307 t.jSXExpressionContainer(
308 t.callExpression(t.identifier('require'), [t.stringLiteral(imageRelativePath)])
309 )
310 )
311 }
312 }
313 }
314 })
315 if (lastImport) {
316 if (importStylePath) {
317 lastImport.insertAfter(
318 t.importDeclaration(
319 [],
320 t.stringLiteral(promoteRelativePath(path.relative(sourceFilePath, importStylePath)))
321 )
322 )
323 }
324 if (imports && imports.length) {
325 imports.forEach(({ name, ast, wxs }) => {
326 const importName = wxs ? name : pascalCase(name)
327 if (componentClassName === importName) {
328 return
329 }
330 const importPath = path.join(self.importsDir, importName + (wxs ? self.wxsIncrementId() : '') + '.js')
331 if (!self.hadBeenBuiltImports.has(importPath)) {
332 self.hadBeenBuiltImports.add(importPath)
333 self.writeFileToTaro(importPath, prettier.format(generateMinimalEscapeCode(ast), prettierJSConfig))
334 }
335 lastImport.insertAfter(
336 template(
337 `import ${importName} from '${promoteRelativePath(path.relative(outputFilePath, importPath))}'`,
338 babylonConfig
339 )()
340 )
341 })
342 }
343 if (depComponents && depComponents.size) {
344 depComponents.forEach(componentObj => {
345 const name = pascalCase(componentObj.name)
346 const component = componentObj.path
347 lastImport.insertAfter(
348 template(
349 `import ${name} from '${promoteRelativePath(path.relative(sourceFilePath, component))}'`,
350 babylonConfig
351 )()
352 )
353 })
354 }
355 }
356 }
357 }
358 })
359
360 return {
361 ast,
362 scriptFiles
363 }
364 }
365
366 getApp () {
367 this.entryJSPath = path.join(this.root, `app${this.fileTypes.SCRIPT}`)
368 this.entryJSONPath = path.join(this.root, `app${this.fileTypes.CONFIG}`)
369 this.entryStylePath = path.join(this.root, `app${this.fileTypes.STYLE}`)
370 try {
371 this.entryJSON = JSON.parse(String(fs.readFileSync(this.entryJSONPath)))
372
373 const using = this.entryJSON.usingComponents
374 if (using && Object.keys(using).length) {
375 for (const key in using) {
376 if (using[key].startsWith('plugin://')) continue
377 const componentPath = using[key]
378 using[key] = path.join(this.root, componentPath)
379 }
380 this.entryUsingComponents = using
381 delete this.entryJSON.usingComponents
382 }
383
384 printLog(processTypeEnum.CONVERT, '入口文件', this.generateShowPath(this.entryJSPath))
385 printLog(processTypeEnum.CONVERT, '入口配置', this.generateShowPath(this.entryJSONPath))
386 if (fs.existsSync(this.entryStylePath)) {
387 this.entryStyle = String(fs.readFileSync(this.entryStylePath))
388 printLog(processTypeEnum.CONVERT, '入口样式', this.generateShowPath(this.entryStylePath))
389 }
390 } catch (err) {
391 this.entryJSON = {}
392 console.log(chalk.red(`app${this.fileTypes.CONFIG} 读取失败,请检查!`))
393 process.exit(1)
394 }
395 }
396
397 getPages () {
398 const pages = this.entryJSON.pages
399 if (!pages || !pages.length) {
400 console.log(chalk.red(`app${this.fileTypes.CONFIG} 配置有误,缺少页面相关配置`))
401 return
402 }
403 this.pages = new Set(pages)
404 }
405
406 getSubPackages () {
407 const subPackages = this.entryJSON.subpackages || this.entryJSON.subPackages
408 if (!subPackages || !subPackages.length) {
409 return
410 }
411 subPackages.forEach(item => {
412 if (item.pages && item.pages.length) {
413 const root = item.root
414 item.pages.forEach(page => {
415 let pagePath = `${root}/${page}`
416 pagePath = pagePath.replace(/\/{2,}/g, '/')
417 this.pages.add(pagePath)
418 })
419 }
420 })
421 }
422
423 getSitemapLocation () {
424 // eslint-disable-next-line dot-notation
425 const sitemapLocation = this.entryJSON['sitemapLocation']
426 if (sitemapLocation) {
427 const sitemapFilePath = path.join(this.root, sitemapLocation)
428 if (fs.existsSync(sitemapFilePath)) {
429 const outputFilePath = path.join(this.convertRoot, sitemapLocation)
430 this.copyFileToTaro(sitemapFilePath, outputFilePath)
431 }
432 }
433 }
434
435 generateScriptFiles (files: Set<string>) {
436 if (!files) {
437 return
438 }
439 if (files.size) {
440 files.forEach(file => {
441 if (!fs.existsSync(file) || this.hadBeenCopyedFiles.has(file)) {
442 return
443 }
444 const code = fs.readFileSync(file).toString()
445 let outputFilePath = file.replace(this.root, this.convertDir)
446 const extname = path.extname(outputFilePath)
447 if (/\.wxs/.test(extname)) {
448 outputFilePath += '.js'
449 }
450 const transformResult = wxTransformer({
451 code,
452 sourcePath: file,
453 isNormal: true,
454 isTyped: REG_TYPESCRIPT.test(file)
455 })
456 const { ast, scriptFiles } = this.parseAst({
457 ast: transformResult.ast,
458 outputFilePath,
459 sourceFilePath: file
460 })
461 const jsCode = generateMinimalEscapeCode(ast)
462 this.writeFileToTaro(outputFilePath, prettier.format(jsCode, prettierJSConfig))
463 printLog(processTypeEnum.COPY, 'JS 文件', this.generateShowPath(outputFilePath))
464 this.hadBeenCopyedFiles.add(file)
465 this.generateScriptFiles(scriptFiles)
466 })
467 }
468 }
469
470 writeFileToTaro (dist: string, code: string) {
471 fs.ensureDirSync(path.dirname(dist))
472 fs.writeFileSync(dist, code)
473 }
474
475 copyFileToTaro (from: string, to: string, options?: fs.CopyOptionsSync) {
476 const filename = path.basename(from)
477 if (fs.statSync(from).isFile() && !path.extname(to)) {
478 fs.ensureDir(to)
479 return fs.copySync(from, path.join(to, filename), options)
480 }
481 fs.ensureDir(path.dirname(to))
482 return fs.copySync(from, to, options)
483 }
484
485 getDistFilePath (src: string, extname?: string): string {
486 if (!extname) return src.replace(this.root, this.convertDir)
487 return src.replace(this.root, this.convertDir).replace(path.extname(src), extname)
488 }
489
490 getConfigFilePath (src: string) {
491 const { dir, name } = path.parse(src)
492 return path.join(dir, name + '.config.js')
493 }
494
495 writeFileToConfig (src: string, json = '{}') {
496 const configSrc = this.getConfigFilePath(src)
497 const code = `export default ${json}`
498 this.writeFileToTaro(configSrc, prettier.format(code, prettierJSConfig))
499 }
500
501 generateShowPath (filePath: string): string {
502 return filePath
503 .replace(path.join(this.root, '/'), '')
504 .split(path.sep)
505 .join('/')
506 }
507
508 private formatFile (jsCode: string, template = '') {
509 let code = jsCode
510 const config = { ...prettierJSConfig }
511 if (this.framework === 'vue') {
512 code = `
513${template}
514<script>
515${code}
516</script>
517 `
518 config.parser = 'vue'
519 config.semi = false
520 config.htmlWhitespaceSensitivity = 'ignore'
521 }
522 return prettier.format(code, config)
523 }
524
525 generateEntry () {
526 try {
527 const entryJS = String(fs.readFileSync(this.entryJSPath))
528 const entryJSON = JSON.stringify(this.entryJSON)
529 const entryDistJSPath = this.getDistFilePath(this.entryJSPath)
530 const taroizeResult = taroize({
531 json: entryJSON,
532 script: entryJS,
533 path: this.root,
534 rootPath: this.root,
535 framework: this.framework,
536 isApp: true
537 })
538 const { ast, scriptFiles } = this.parseAst({
539 ast: taroizeResult.ast,
540 sourceFilePath: this.entryJSPath,
541 outputFilePath: entryDistJSPath,
542 importStylePath: this.entryStyle
543 ? this.entryStylePath.replace(path.extname(this.entryStylePath), OUTPUT_STYLE_EXTNAME)
544 : null,
545 isApp: true
546 })
547 const jsCode = generateMinimalEscapeCode(ast)
548 this.writeFileToTaro(entryDistJSPath, jsCode)
549 this.writeFileToConfig(entryDistJSPath, entryJSON)
550 printLog(processTypeEnum.GENERATE, '入口文件', this.generateShowPath(entryDistJSPath))
551 if (this.entryStyle) {
552 this.traverseStyle(this.entryStylePath, this.entryStyle)
553 }
554 this.generateScriptFiles(scriptFiles)
555 if (this.entryJSON.tabBar) {
556 this.generateTabBarIcon(this.entryJSON.tabBar)
557 this.generateCustomTabbar(this.entryJSON.tabBar)
558 }
559 } catch (err) {
560 console.log(err)
561 }
562 }
563
564 generateTabBarIcon (tabBar: TabBar) {
565 const { list = [] } = tabBar
566 const icons = new Set<string>()
567 if (Array.isArray(list) && list.length) {
568 list.forEach(item => {
569 if (typeof item.iconPath === 'string') icons.add(item.iconPath)
570 if (typeof item.selectedIconPath === 'string') icons.add(item.selectedIconPath)
571 })
572 if (icons.size > 0) {
573 Array.from(icons)
574 .map(icon => path.join(this.root, icon))
575 .forEach(iconPath => {
576 const iconDistPath = this.getDistFilePath(iconPath)
577 this.copyFileToTaro(iconPath, iconDistPath)
578 printLog(processTypeEnum.COPY, 'TabBar 图标', this.generateShowPath(iconDistPath))
579 })
580 }
581 }
582 }
583
584 generateCustomTabbar (tabBar: TabBar) {
585 if (!tabBar.custom) return
586
587 const customTabbarPath = path.join(this.root, 'custom-tab-bar')
588 if (fs.existsSync(customTabbarPath)) {
589 const customTabbarDistPath = this.getDistFilePath(customTabbarPath)
590 this.copyFileToTaro(customTabbarPath, customTabbarDistPath)
591 printLog(processTypeEnum.COPY, '自定义 TabBar', this.generateShowPath(customTabbarDistPath))
592 }
593 }
594
595 private getComponentDest (file: string) {
596 if (this.framework === 'react') {
597 return file
598 }
599
600 return path.join(path.dirname(file), path.basename(file, path.extname(file)) + '.vue')
601 }
602
603 traversePages () {
604 this.pages.forEach(page => {
605 const pagePath = path.join(this.root, page)
606 const pageJSPath = pagePath + this.fileTypes.SCRIPT
607 const pageDistJSPath = this.getDistFilePath(pageJSPath)
608 const pageConfigPath = pagePath + this.fileTypes.CONFIG
609 const pageStylePath = pagePath + this.fileTypes.STYLE
610 const pageTemplPath = pagePath + this.fileTypes.TEMPL
611
612 try {
613 const depComponents = new Set<IComponent>()
614 if (!fs.existsSync(pageJSPath)) {
615 throw new Error(`页面 ${page} 没有 JS 文件!`)
616 }
617 const param: ITaroizeOptions = {}
618 printLog(processTypeEnum.CONVERT, '页面文件', this.generateShowPath(pageJSPath))
619
620 let pageConfig
621 if (fs.existsSync(pageConfigPath)) {
622 printLog(processTypeEnum.CONVERT, '页面配置', this.generateShowPath(pageConfigPath))
623 const pageConfigStr = String(fs.readFileSync(pageConfigPath))
624 pageConfig = JSON.parse(pageConfigStr)
625 } else if (this.entryUsingComponents) {
626 pageConfig = {}
627 }
628 if (pageConfig) {
629 if (this.entryUsingComponents) {
630 pageConfig.usingComponents = {
631 ...pageConfig.usingComponents,
632 ...this.entryUsingComponents
633 }
634 }
635 const pageUsingComponents = pageConfig.usingComponents
636 if (pageUsingComponents) {
637 // 页面依赖组件
638 const usingComponents = {}
639 Object.keys(pageUsingComponents).forEach(component => {
640 const unResolveComponentPath: string = pageUsingComponents[component]
641 if (unResolveComponentPath.startsWith('plugin://')) {
642 usingComponents[component] = unResolveComponentPath
643 } else {
644 let componentPath
645 if (unResolveComponentPath.startsWith(this.root)) {
646 componentPath = unResolveComponentPath
647 } else {
648 componentPath = path.resolve(pageConfigPath, '..', pageUsingComponents[component])
649 if (!fs.existsSync(resolveScriptPath(componentPath))) {
650 componentPath = path.join(this.root, pageUsingComponents[component])
651 }
652 }
653
654 depComponents.add({
655 name: component,
656 path: componentPath
657 })
658 }
659 })
660 if (Object.keys(usingComponents).length === 0) {
661 delete pageConfig.usingComponents
662 } else {
663 pageConfig.usingComponents = usingComponents
664 }
665 }
666 param.json = JSON.stringify(pageConfig)
667 }
668
669 param.script = String(fs.readFileSync(pageJSPath))
670 if (fs.existsSync(pageTemplPath)) {
671 printLog(processTypeEnum.CONVERT, '页面模板', this.generateShowPath(pageTemplPath))
672 param.wxml = String(fs.readFileSync(pageTemplPath))
673 }
674 let pageStyle: string | null = null
675 if (fs.existsSync(pageStylePath)) {
676 printLog(processTypeEnum.CONVERT, '页面样式', this.generateShowPath(pageStylePath))
677 pageStyle = String(fs.readFileSync(pageStylePath))
678 }
679 param.path = path.dirname(pageJSPath)
680 param.rootPath = this.root
681 const taroizeResult = taroize({
682 ...param,
683 framework: this.framework
684 })
685 const { ast, scriptFiles } = this.parseAst({
686 ast: taroizeResult.ast,
687 sourceFilePath: pageJSPath,
688 outputFilePath: pageDistJSPath,
689 importStylePath: pageStyle ? pageStylePath.replace(path.extname(pageStylePath), OUTPUT_STYLE_EXTNAME) : null,
690 depComponents,
691 imports: taroizeResult.imports
692 })
693 const jsCode = generateMinimalEscapeCode(ast)
694 this.writeFileToTaro(this.getComponentDest(pageDistJSPath), this.formatFile(jsCode, taroizeResult.template))
695 this.writeFileToConfig(pageDistJSPath, param.json)
696 printLog(processTypeEnum.GENERATE, '页面文件', this.generateShowPath(pageDistJSPath))
697 if (pageStyle) {
698 this.traverseStyle(pageStylePath, pageStyle)
699 }
700 this.generateScriptFiles(scriptFiles)
701 this.traverseComponents(depComponents)
702 } catch (err) {
703 printLog(processTypeEnum.ERROR, '页面转换', this.generateShowPath(pageJSPath))
704 console.log(err)
705 }
706 })
707 }
708
709 traverseComponents (components: Set<IComponent>) {
710 if (!components || !components.size) {
711 return
712 }
713 components.forEach(componentObj => {
714 const component = componentObj.path
715 if (this.hadBeenBuiltComponents.has(component)) return
716 this.hadBeenBuiltComponents.add(component)
717
718 const componentJSPath = component + this.fileTypes.SCRIPT
719 const componentDistJSPath = this.getDistFilePath(componentJSPath)
720 const componentConfigPath = component + this.fileTypes.CONFIG
721 const componentStylePath = component + this.fileTypes.STYLE
722 const componentTemplPath = component + this.fileTypes.TEMPL
723
724 try {
725 const param: ITaroizeOptions = {}
726 const depComponents = new Set<IComponent>()
727 if (!fs.existsSync(componentJSPath)) {
728 throw new Error(`组件 ${component} 没有 JS 文件!`)
729 }
730 printLog(processTypeEnum.CONVERT, '组件文件', this.generateShowPath(componentJSPath))
731 if (fs.existsSync(componentConfigPath)) {
732 printLog(processTypeEnum.CONVERT, '组件配置', this.generateShowPath(componentConfigPath))
733 const componentConfigStr = String(fs.readFileSync(componentConfigPath))
734 const componentConfig = JSON.parse(componentConfigStr)
735 const componentUsingComponnets = componentConfig.usingComponents
736 if (componentUsingComponnets) {
737 // 页面依赖组件
738 Object.keys(componentUsingComponnets).forEach(component => {
739 let componentPath = path.resolve(componentConfigPath, '..', componentUsingComponnets[component])
740 if (!fs.existsSync(resolveScriptPath(componentPath))) {
741 componentPath = path.join(this.root, componentUsingComponnets[component])
742 }
743 depComponents.add({
744 name: component,
745 path: componentPath
746 })
747 })
748 delete componentConfig.usingComponents
749 }
750 param.json = JSON.stringify(componentConfig)
751 }
752 param.script = String(fs.readFileSync(componentJSPath))
753 if (fs.existsSync(componentTemplPath)) {
754 printLog(processTypeEnum.CONVERT, '组件模板', this.generateShowPath(componentTemplPath))
755 param.wxml = String(fs.readFileSync(componentTemplPath))
756 }
757 let componentStyle: string | null = null
758 if (fs.existsSync(componentStylePath)) {
759 printLog(processTypeEnum.CONVERT, '组件样式', this.generateShowPath(componentStylePath))
760 componentStyle = String(fs.readFileSync(componentStylePath))
761 }
762 param.path = path.dirname(componentJSPath)
763 param.rootPath = this.root
764 const taroizeResult = taroize({
765 ...param,
766 framework: this.framework
767 })
768 const { ast, scriptFiles } = this.parseAst({
769 ast: taroizeResult.ast,
770 sourceFilePath: componentJSPath,
771 outputFilePath: componentDistJSPath,
772 importStylePath: componentStyle
773 ? componentStylePath.replace(path.extname(componentStylePath), OUTPUT_STYLE_EXTNAME)
774 : null,
775 depComponents,
776 imports: taroizeResult.imports
777 })
778 const jsCode = generateMinimalEscapeCode(ast)
779 this.writeFileToTaro(this.getComponentDest(componentDistJSPath), this.formatFile(jsCode, taroizeResult.template))
780 printLog(processTypeEnum.GENERATE, '组件文件', this.generateShowPath(componentDistJSPath))
781 if (componentStyle) {
782 this.traverseStyle(componentStylePath, componentStyle)
783 }
784 this.generateScriptFiles(scriptFiles)
785 this.traverseComponents(depComponents)
786 } catch (err) {
787 printLog(processTypeEnum.ERROR, '组件转换', this.generateShowPath(componentJSPath))
788 console.log(err)
789 }
790 })
791 }
792
793 async styleUnitTransform (filePath: string, content: string) {
794 const postcssResult = await postcss([unitTransform()]).process(content, {
795 from: filePath
796 })
797 return postcssResult
798 }
799
800 processStyleAssets (content: string, stylePath: string, styleDist: string) {
801 const reg = /url\(["'](.+?)["']\)/g
802 let token = reg.exec(content)
803 stylePath = path.dirname(stylePath)
804 styleDist = path.dirname(styleDist)
805
806 while (token?.length) {
807 let url = token[1]
808
809 if (
810 url &&
811 url.indexOf('data:') !== 0 &&
812 url.indexOf('#') !== 0 &&
813 !(/^[a-z]+:\/\//.test(url))
814 ) {
815 url = url.trim()
816 url.replace(/[/\\]/g, path.sep)
817 url = url.split('?')[0]
818 url = url.split('#')[0]
819
820 const originPath = path.resolve(stylePath, url)
821 const destPath = path.resolve(styleDist, url)
822 const destDir = path.dirname(destPath)
823
824 if (!fs.existsSync(originPath)) {
825 printLog(processTypeEnum.WARNING, '静态资源', `找不到资源:${originPath}`)
826 } else if (!fs.existsSync(destPath)) {
827 fs.ensureDirSync(destDir)
828 fs.copyFile(originPath, destPath)
829 printLog(processTypeEnum.COPY, '样式资源', this.generateShowPath(destPath))
830 }
831 }
832
833 token = reg.exec(content)
834 }
835 }
836
837 async traverseStyle (filePath: string, style: string) {
838 const { imports, content } = processStyleImports(style, (str, stylePath) => {
839 let relativePath = stylePath
840 if (path.isAbsolute(relativePath)) {
841 relativePath = promoteRelativePath(path.relative(filePath, path.join(this.root, stylePath)))
842 }
843 return str.replace(stylePath, relativePath).replace('.wxss', OUTPUT_STYLE_EXTNAME)
844 })
845 const styleDist = this.getDistFilePath(filePath, OUTPUT_STYLE_EXTNAME)
846 this.processStyleAssets(content, filePath, styleDist)
847 const { css } = await this.styleUnitTransform(filePath, content)
848 this.writeFileToTaro(styleDist, css)
849 printLog(processTypeEnum.GENERATE, '样式文件', this.generateShowPath(styleDist))
850 if (imports && imports.length) {
851 imports.forEach(importItem => {
852 const importPath = path.isAbsolute(importItem)
853 ? path.join(this.root, importItem)
854 : path.resolve(path.dirname(filePath), importItem)
855 if (fs.existsSync(importPath)) {
856 const styleText = fs.readFileSync(importPath).toString()
857 this.traverseStyle(importPath, styleText)
858 }
859 })
860 }
861 }
862
863 generateConfigFiles () {
864 const creator = new Creator()
865 const templateName = 'default'
866 const configDir = path.join(this.convertRoot, 'config')
867 const pkgPath = path.join(this.convertRoot, 'package.json')
868 const projectName = 'taroConvert'
869 const description = ''
870 const version = getPkgVersion()
871 const dateObj = new Date()
872 const date = `${dateObj.getFullYear()}-${dateObj.getMonth() + 1}-${dateObj.getDate()}`
873 creator.template(templateName, 'package.json.tmpl', pkgPath, {
874 description,
875 projectName,
876 version,
877 css: 'sass',
878 typescript: false,
879 template: templateName,
880 framework: this.framework,
881 compiler: 'webpack5'
882 })
883 creator.template(templateName, path.join('config', 'index.js'), path.join(configDir, 'index.js'), {
884 date,
885 projectName,
886 framework: this.framework,
887 compiler: 'webpack5'
888 })
889 creator.template(templateName, path.join('config', 'dev.js'), path.join(configDir, 'dev.js'), {
890 framework: this.framework
891 })
892 creator.template(templateName, path.join('config', 'prod.js'), path.join(configDir, 'prod.js'), {
893 framework: this.framework
894 })
895 creator.template(templateName, 'project.config.json', path.join(this.convertRoot, 'project.config.json'), {
896 description,
897 projectName,
898 framework: this.framework
899 })
900 creator.template(templateName, '.gitignore', path.join(this.convertRoot, '.gitignore'))
901 creator.template(templateName, '.editorconfig', path.join(this.convertRoot, '.editorconfig'))
902 creator.template(templateName, '.eslintrc.js', path.join(this.convertRoot, '.eslintrc.js'), {
903 typescript: false,
904 framework: this.framework
905 })
906 creator.template(templateName, 'babel.config.js', path.join(this.convertRoot, 'babel.config.js'), {
907 typescript: false,
908 framework: this.framework
909 })
910 creator.template(templateName, path.join('src', 'index.html'), path.join(this.convertDir, 'index.html'),{
911 projectName
912 })
913 creator.fs.commit(() => {
914 const pkgObj = JSON.parse(fs.readFileSync(pkgPath).toString())
915 pkgObj.dependencies['@tarojs/with-weapp'] = `^${version}`
916 fs.writeJSONSync(pkgPath, pkgObj, {
917 spaces: 2,
918 EOL: '\n'
919 })
920 printLog(processTypeEnum.GENERATE, '文件', this.generateShowPath(path.join(configDir, 'index.js')))
921 printLog(processTypeEnum.GENERATE, '文件', this.generateShowPath(path.join(configDir, 'dev.js')))
922 printLog(processTypeEnum.GENERATE, '文件', this.generateShowPath(path.join(configDir, 'prod.js')))
923 printLog(processTypeEnum.GENERATE, '文件', this.generateShowPath(pkgPath))
924 printLog(
925 processTypeEnum.GENERATE,
926 '文件',
927 this.generateShowPath(path.join(this.convertRoot, 'project.config.json'))
928 )
929 printLog(processTypeEnum.GENERATE, '文件', this.generateShowPath(path.join(this.convertRoot, '.gitignore')))
930 printLog(processTypeEnum.GENERATE, '文件', this.generateShowPath(path.join(this.convertRoot, '.editorconfig')))
931 printLog(processTypeEnum.GENERATE, '文件', this.generateShowPath(path.join(this.convertRoot, '.eslintrc')))
932 printLog(processTypeEnum.GENERATE, '文件', this.generateShowPath(path.join(this.convertDir, 'index.html')))
933 this.showLog()
934 })
935 }
936
937 showLog () {
938 console.log()
939 console.log(
940 `${chalk.green('✔ ')} 转换成功,请进入 ${chalk.bold(
941 'taroConvert'
942 )} 目录下使用 npm 或者 yarn 安装项目依赖后再运行!`
943 )
944 }
945
946 run () {
947 // inquirer.prompt([{
948 // type: 'list',
949 // name: 'framework',
950 // message: '你想转换为哪种框架?',
951 // choices: ['react', 'vue'],
952 // default: 'react'
953 // }]).then(({ framework }) => {
954 this.framework = 'react'
955 this.generateEntry()
956 this.traversePages()
957 this.generateConfigFiles()
958 // })
959 }
960}