UNPKG

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