UNPKG

15.5 kBPlain TextView Raw
1import * as fs from 'fs-extra'
2import * as path from 'path'
3import { exec, spawn, spawnSync, execSync, SpawnSyncOptions } from 'child_process'
4import { performance } from 'perf_hooks'
5import * as chokidar from 'chokidar'
6import * as _ from 'lodash'
7import * as klaw from 'klaw'
8import { TogglableOptions, IOption } from '@tarojs/taro/types/compile'
9import {
10 PROJECT_CONFIG,
11 processTypeEnum,
12 REG_STYLE,
13 REG_SCRIPTS,
14 REG_TYPESCRIPT,
15 resolveScriptPath,
16 printLog,
17 shouldUseYarn,
18 shouldUseCnpm,
19 chalk
20} from '@tarojs/helper'
21
22import { getPkgVersion, checkCliAndFrameworkVersion } from './util'
23import CONFIG from './config'
24import * as StyleProcess from './rn/styleProcess'
25import { parseJSCode as transformJSCode } from './rn/transformJS'
26import { convertToJDReact } from './jdreact/convert_to_jdreact'
27import { IBuildOptions } from './util/types'
28// import { Error } from 'tslint/lib/error'
29
30let isBuildingStyles = {}
31const styleDenpendencyTree = {}
32
33const depTree: {
34 [key: string]: string[]
35} = {}
36
37const TEMP_DIR_NAME = 'rn_temp'
38const BUNDLE_DIR_NAME = 'bundle'
39
40class Compiler {
41 projectConfig
42 h5Config
43 routerConfig
44 appPath: string
45 routerMode: string
46 customRoutes: {
47 [key: string]: string
48 }
49 routerBasename: string
50 sourcePath: string
51 sourceDir: string
52 // tempDir: string
53 // bundleDir: string
54 tempPath: string
55 entryFilePath: string
56 entryFileName: string
57 entryBaseName: string
58 babel: TogglableOptions
59 csso: TogglableOptions
60 uglify: TogglableOptions
61 sass: IOption
62 less: IOption
63 stylus: IOption
64 plugins: any[]
65 rnConfig
66 hasJDReactOutput: boolean
67 // babelConfig: any
68 // pxTransformConfig
69 // pathAlias
70
71 constructor (appPath) {
72 this.appPath = appPath
73 this.projectConfig = require(resolveScriptPath(path.join(appPath, PROJECT_CONFIG)))(_.merge)
74 const sourceDirName = this.projectConfig.sourceRoot || CONFIG.SOURCE_DIR
75 this.sourceDir = path.join(appPath, sourceDirName)
76 this.entryFilePath = resolveScriptPath(path.join(this.sourceDir, CONFIG.ENTRY))
77 this.entryFileName = path.basename(this.entryFilePath)
78 this.entryBaseName = path.basename(this.entryFilePath, path.extname(this.entryFileName))
79 this.babel = this.projectConfig.babel
80 this.csso = this.projectConfig.csso
81 this.uglify = this.projectConfig.uglify
82 this.plugins = this.projectConfig.plugins
83 this.sass = this.projectConfig.sass
84 this.stylus = this.projectConfig.stylus
85 this.less = this.projectConfig.less
86 this.rnConfig = this.projectConfig.rn || {}
87 // this.babelConfig = this.projectConfig.plugins.babel // 用来配置 babel
88
89 // 直接输出编译后代码到指定目录
90 if (this.rnConfig.outPath) {
91 this.tempPath = path.resolve(this.appPath, this.rnConfig.outPath)
92 if (!fs.existsSync(this.tempPath)) {
93 throw new Error(`outPath ${this.tempPath} 不存在`)
94 }
95 this.hasJDReactOutput = true
96 } else {
97 this.tempPath = path.join(appPath, TEMP_DIR_NAME)
98 this.hasJDReactOutput = false
99 }
100 }
101
102 isEntryFile (filePath) {
103 return path.basename(filePath) === this.entryFileName
104 }
105
106 compileDepStyles (filePath, styleFiles) {
107 if (isBuildingStyles[filePath] || styleFiles.length === 0) {
108 return Promise.resolve({})
109 }
110 isBuildingStyles[filePath] = true
111 return Promise.all(styleFiles.map(async p => { // to css string
112 const filePath = path.join(p)
113 const fileExt = path.extname(filePath)
114 printLog(processTypeEnum.COMPILE, _.camelCase(fileExt).toUpperCase(), filePath)
115 return StyleProcess.loadStyle({
116 filePath,
117 pluginsConfig: {
118 sass: this.sass,
119 less: this.less,
120 stylus: this.stylus
121 }
122 }, this.appPath)
123 })).then(resList => { // postcss
124 return Promise.all(resList.map(item => {
125 return StyleProcess.postCSS({...item as { css: string, filePath: string }, projectConfig: this.projectConfig})
126 }))
127 }).then(resList => {
128 const styleObjectEntire = {}
129 resList.forEach(item => {
130 const styleObject = StyleProcess.getStyleObject({css: item.css, filePath: item.filePath})
131 // validate styleObject
132 StyleProcess.validateStyle({styleObject, filePath: item.filePath})
133
134 Object.assign(styleObjectEntire, styleObject)
135 if (filePath !== this.entryFilePath) { // 非入口文件,合并全局样式
136 Object.assign(styleObjectEntire, _.get(styleDenpendencyTree, [this.entryFilePath, 'styleObjectEntire'], {}))
137 }
138 styleDenpendencyTree[filePath] = {
139 styleFiles,
140 styleObjectEntire
141 }
142 })
143 return JSON.stringify(styleObjectEntire, null, 2)
144 }).then(css => {
145 let tempFilePath = filePath.replace(this.sourceDir, this.tempPath)
146 const basename = path.basename(tempFilePath, path.extname(tempFilePath))
147 tempFilePath = path.join(path.dirname(tempFilePath), `${basename}_styles.js`)
148
149 StyleProcess.writeStyleFile({css, tempFilePath})
150 }).catch((e) => {
151 throw new Error(e)
152 })
153 }
154
155 initProjectFile () {
156 // generator app.json
157 const appJsonObject = Object.assign({
158 name: _.camelCase(require(path.join(this.appPath, 'package.json')).name)
159 }, this.rnConfig.appJson)
160
161 const indexJsStr = `
162 import {AppRegistry} from 'react-native';
163 import App from './${this.entryBaseName}';
164 import {name as appName} from './app.json';
165
166 AppRegistry.registerComponent(appName, () => App);`
167
168 fs.writeFileSync(path.join(this.tempPath, 'index.js'), indexJsStr)
169 printLog(processTypeEnum.GENERATE, 'index.js', path.join(this.tempPath, 'index.js'))
170 fs.writeFileSync(path.join(this.tempPath, 'app.json'), JSON.stringify(appJsonObject, null, 2))
171 printLog(processTypeEnum.GENERATE, 'app.json', path.join(this.tempPath, 'app.json'))
172 return Promise.resolve()
173 }
174
175 async processFile (filePath) {
176 if (!fs.existsSync(filePath)) {
177 return
178 }
179 const dirname = path.dirname(filePath)
180 const distDirname = dirname.replace(this.sourceDir, this.tempPath)
181 let distPath = path.format({dir: distDirname, base: path.basename(filePath)})
182 const code = fs.readFileSync(filePath, 'utf-8')
183 if (REG_STYLE.test(filePath)) {
184 // do something
185 } else if (REG_SCRIPTS.test(filePath)) {
186 if (/\.jsx(\?.*)?$/.test(filePath)) {
187 distPath = distPath.replace(/\.jsx(\?.*)?$/, '.js')
188 }
189 if (REG_TYPESCRIPT.test(filePath)) {
190 distPath = distPath.replace(/\.(tsx|ts)(\?.*)?$/, '.js')
191 }
192 printLog(processTypeEnum.COMPILE, _.camelCase(path.extname(filePath)).toUpperCase(), filePath)
193 // transformJSCode
194 const transformResult = transformJSCode({
195 code, filePath, isEntryFile: this.isEntryFile(filePath), projectConfig: this.projectConfig
196 })
197 const jsCode = transformResult.code
198 fs.ensureDirSync(distDirname)
199 fs.writeFileSync(distPath, Buffer.from(jsCode))
200 printLog(processTypeEnum.GENERATE, _.camelCase(path.extname(filePath)).toUpperCase(), distPath)
201 // compileDepStyles
202 const styleFiles = transformResult.styleFiles
203 depTree[filePath] = styleFiles
204 await this.compileDepStyles(filePath, styleFiles)
205 } else {
206 fs.ensureDirSync(distDirname)
207 printLog(processTypeEnum.COPY, _.camelCase(path.extname(filePath)).toUpperCase(), filePath)
208 fs.copySync(filePath, distPath)
209 printLog(processTypeEnum.GENERATE, _.camelCase(path.extname(filePath)).toUpperCase(), distPath)
210 }
211 }
212
213 /**
214 * @description 编译文件,安装依赖
215 * @returns {Promise}
216 */
217 buildTemp () {
218 return new Promise((resolve, reject) => {
219 const filePaths: string[] = []
220 this.processFile(this.entryFilePath).then(() => {
221 klaw(this.sourceDir)
222 .on('data', file => {
223 if (!file.stats.isDirectory()) {
224 filePaths.push(file.path)
225 }
226 })
227 .on('error', (err, item) => {
228 console.log(err.message)
229 console.log(item.path)
230 })
231 .on('end', () => {
232 Promise.all(filePaths.filter(f => f !== this.entryFilePath).map(filePath => this.processFile(filePath)))
233 .then(() => {
234 if (!this.hasJDReactOutput) {
235 this.initProjectFile()
236 resolve()
237 } else {
238 resolve()
239 }
240 })
241 })
242 })
243 })
244 }
245
246 buildBundle () {
247 fs.ensureDirSync(TEMP_DIR_NAME)
248 process.chdir(TEMP_DIR_NAME)
249 // 通过 jdreact 构建 bundle
250 if (this.rnConfig.bundleType === 'jdreact') {
251 console.log()
252 console.log(chalk.green('生成JDReact 目录:'))
253 console.log()
254 convertToJDReact({
255 tempPath: this.tempPath, entryBaseName: this.entryBaseName
256 })
257 return
258 }
259 // 默认打包到 bundle 文件夹
260 fs.ensureDirSync(BUNDLE_DIR_NAME)
261 execSync(
262 `node ../node_modules/react-native/local-cli/cli.js bundle --entry-file ./${TEMP_DIR_NAME}/index.js --bundle-output ./${BUNDLE_DIR_NAME}/index.bundle --assets-dest ./${BUNDLE_DIR_NAME} --dev false`,
263 {stdio: 'inherit'})
264 }
265
266 async perfWrap (callback, args?) {
267 isBuildingStyles = {} // 清空
268 // 后期可以优化,不编译全部
269 const t0 = performance.now()
270 await callback(args)
271 const t1 = performance.now()
272 printLog(processTypeEnum.COMPILE, `编译完成,花费${Math.round(t1 - t0)} ms`)
273 console.log()
274 }
275
276 watchFiles () {
277 const watcher = chokidar.watch(path.join(this.sourceDir), {
278 ignored: /(^|[/\\])\../,
279 persistent: true,
280 ignoreInitial: true
281 })
282
283 watcher
284 .on('ready', () => {
285 console.log()
286 console.log(chalk.gray('初始化完毕,监听文件修改中...'))
287 console.log()
288 })
289 .on('add', filePath => {
290 const relativePath = path.relative(this.appPath, filePath)
291 printLog(processTypeEnum.CREATE, '添加文件', relativePath)
292 this.perfWrap(this.buildTemp.bind(this))
293 })
294 .on('change', filePath => {
295 const relativePath = path.relative(this.appPath, filePath)
296 printLog(processTypeEnum.MODIFY, '文件变动', relativePath)
297 if (REG_SCRIPTS.test(filePath)) {
298 this.perfWrap(this.processFile.bind(this), filePath)
299 }
300 if (REG_STYLE.test(filePath)) {
301 _.forIn(depTree, (styleFiles, jsFilePath) => {
302 if (styleFiles.indexOf(filePath) > -1) {
303 this.perfWrap(this.processFile.bind(this), jsFilePath)
304 }
305 })
306 }
307 })
308 .on('unlink', filePath => {
309 const relativePath = path.relative(this.appPath, filePath)
310 printLog(processTypeEnum.UNLINK, '删除文件', relativePath)
311 this.perfWrap(this.buildTemp.bind(this))
312 })
313 .on('error', error => console.log(`Watcher error: ${error}`))
314 }
315}
316
317function hasRNDep (appPath) {
318 const pkgJson = require(path.join(appPath, 'package.json'))
319 return Boolean(pkgJson.dependencies['react-native'])
320}
321
322function updatePkgJson (appPath) {
323 const version = getPkgVersion()
324 const RNDep = `{
325 "@tarojs/components-rn": "^${version}",
326 "@tarojs/taro-rn": "^${version}",
327 "@tarojs/taro-router-rn": "^${version}",
328 "@tarojs/taro-redux-rn": "^${version}",
329 "react": "16.8.0",
330 "react-native": "0.59.9",
331 "redux": "^4.0.0",
332 "tslib": "^1.8.0"
333 }
334 `
335 return new Promise((resolve, reject) => {
336 const pkgJson = require(path.join(appPath, 'package.json'))
337 // 未安装 RN 依赖,则更新 pkgjson,并重新安装依赖
338 if (!hasRNDep(appPath)) {
339 pkgJson.dependencies = Object.assign({}, pkgJson.dependencies, JSON.parse(RNDep.replace(/(\r\n|\n|\r|\s+)/gm, '')))
340 fs.writeFileSync(path.join(appPath, 'package.json'), JSON.stringify(pkgJson, null, 2))
341 printLog(processTypeEnum.GENERATE, 'package.json', path.join(appPath, 'package.json'))
342 installDep(appPath).then(() => {
343 resolve()
344 })
345 } else {
346 resolve()
347 }
348 })
349}
350
351function installDep (path: string) {
352 return new Promise((resolve, reject) => {
353 console.log()
354 console.log(chalk.yellow('开始安装依赖~'))
355 process.chdir(path)
356 let command
357 if (shouldUseYarn()) {
358 command = 'yarn'
359 } else if (shouldUseCnpm()) {
360 command = 'cnpm install'
361 } else {
362 command = 'npm install'
363 }
364 exec(command, (err, stdout, stderr) => {
365 if (err) reject()
366 else {
367 console.log(stdout)
368 console.log(stderr)
369 }
370 resolve()
371 })
372 })
373}
374
375export { Compiler }
376
377export async function build (appPath: string, buildConfig: IBuildOptions) {
378 const {watch, port} = buildConfig
379 process.env.TARO_ENV = 'rn'
380 await checkCliAndFrameworkVersion(appPath, 'rn')
381 const compiler = new Compiler(appPath)
382 fs.ensureDirSync(compiler.tempPath)
383 const t0 = performance.now()
384
385 if (!hasRNDep(appPath)) {
386 await updatePkgJson(appPath)
387 }
388 try {
389 await compiler.buildTemp()
390 } catch (e) {
391 throw e
392 }
393 const t1 = performance.now()
394 printLog(processTypeEnum.COMPILE, `编译完成,花费${Math.round(t1 - t0)} ms`)
395 // rn 配置添加onlyTaroToRn字段,支持项目构建只编译不打包
396 if (compiler.rnConfig.onlyTaroToRn) return
397 if (watch) {
398 compiler.watchFiles()
399 if (!compiler.hasJDReactOutput) {
400 startServerInNewWindow({port, appPath})
401 }
402 } else {
403 compiler.buildBundle()
404 }
405}
406
407/**
408 * @description run packager server
409 * copy from react-native/local-cli/runAndroid/runAndroid.js
410 */
411function startServerInNewWindow ({port = 8081, appPath}) {
412 // set up OS-specific filenames and commands
413 const isWindows = /^win/.test(process.platform)
414 const scriptFile = isWindows
415 ? 'launchPackager.bat'
416 : 'launchPackager.command'
417 const packagerEnvFilename = isWindows ? '.packager.bat' : '.packager.env'
418 const portExportContent = isWindows
419 ? `set RCT_METRO_PORT=${port}`
420 : `export RCT_METRO_PORT=${port}`
421
422 // set up the launchpackager.(command|bat) file
423 const scriptsDir = path.resolve(appPath, './node_modules', 'react-native', 'scripts')
424 const launchPackagerScript = path.resolve(scriptsDir, scriptFile)
425 const procConfig: SpawnSyncOptions = {cwd: scriptsDir}
426 const terminal = process.env.REACT_TERMINAL
427
428 // set up the .packager.(env|bat) file to ensure the packager starts on the right port
429 const packagerEnvFile = path.join(
430 appPath,
431 'node_modules',
432 'react-native',
433 'scripts',
434 packagerEnvFilename
435 )
436
437 // ensure we overwrite file by passing the 'w' flag
438 fs.writeFileSync(packagerEnvFile, portExportContent, {
439 encoding: 'utf8',
440 flag: 'w'
441 })
442
443 if (process.platform === 'darwin') {
444 if (terminal) {
445 return spawnSync(
446 'open',
447 ['-a', terminal, launchPackagerScript],
448 procConfig
449 )
450 }
451 return spawnSync('open', [launchPackagerScript], procConfig)
452 } else if (process.platform === 'linux') {
453 if (terminal) {
454 return spawn(
455 terminal,
456 ['-e', 'sh ' + launchPackagerScript],
457 procConfig
458 )
459 }
460 return spawn('sh', [launchPackagerScript], procConfig)
461 } else if (/^win/.test(process.platform)) {
462 procConfig.stdio = 'ignore'
463 return spawn(
464 'cmd.exe',
465 ['/C', launchPackagerScript],
466 procConfig
467 )
468 } else {
469 console.log(
470 chalk.red(
471 `Cannot start the packager. Unknown platform ${process.platform}`
472 )
473 )
474 }
475}