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