UNPKG

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