UNPKG

15.1 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 _ from 'lodash'
6import * as klaw from 'klaw'
7import { TogglableOptions, IOption } from '@tarojs/taro/types/compile'
8import {
9 PROJECT_CONFIG,
10 processTypeEnum,
11 REG_STYLE,
12 REG_SCRIPTS,
13 REG_TYPESCRIPT,
14 chalk,
15 chokidar,
16 resolveScriptPath,
17 printLog,
18 shouldUseYarn,
19 shouldUseCnpm,
20 SOURCE_DIR,
21 ENTRY
22} from '@tarojs/helper'
23
24import { getPkgVersion } from './util'
25import * as StyleProcess from './rn/styleProcess'
26import { parseJSCode as transformJSCode } from './rn/transformJS'
27import { convertToJDReact } from './jdreact/convert_to_jdreact'
28import { IBuildOptions } from './util/types'
29// import { Error } from 'tslint/lib/error'
30
31let isBuildingStyles = {}
32const styleDenpendencyTree = {}
33
34const depTree: {
35 [key: string]: string[]
36} = {}
37
38const TEMP_DIR_NAME = 'rn_temp'
39const BUNDLE_DIR_NAME = 'bundle'
40
41class Compiler {
42 projectConfig
43 h5Config
44 routerConfig
45 appPath: string
46 routerMode: string
47 customRoutes: {
48 [key: string]: string;
49 }
50
51 routerBasename: string
52 sourcePath: string
53 sourceDir: string
54 // tempDir: string
55 // bundleDir: string
56 tempPath: string
57 entryFilePath: string
58 entryFileName: string
59 entryBaseName: string
60 babel: TogglableOptions
61 csso: TogglableOptions
62 uglify: TogglableOptions
63 sass: IOption
64 less: IOption
65 stylus: IOption
66 plugins: any[]
67 rnConfig
68 hasJDReactOutput: boolean
69 babelConfig: any
70 // pxTransformConfig
71 // pathAlias
72
73 constructor (appPath) {
74 this.appPath = appPath
75 this.projectConfig = require(resolveScriptPath(path.join(appPath, PROJECT_CONFIG)))(_.merge)
76 const sourceDirName = this.projectConfig.sourceRoot || SOURCE_DIR
77 this.sourceDir = path.join(appPath, sourceDirName)
78 this.entryFilePath = resolveScriptPath(path.join(this.sourceDir, ENTRY))
79 this.entryFileName = path.basename(this.entryFilePath)
80 this.entryBaseName = path.basename(this.entryFilePath, path.extname(this.entryFileName))
81 this.babel = this.projectConfig.babel
82 this.csso = this.projectConfig.csso
83 this.uglify = this.projectConfig.uglify
84 this.plugins = this.projectConfig.plugins
85 this.sass = this.projectConfig.sass
86 this.stylus = this.projectConfig.stylus
87 this.less = this.projectConfig.less
88 this.rnConfig = this.projectConfig.rn || {}
89 this.babelConfig = this.projectConfig.plugins.babel // 用来配置 babel
90
91 // 直接输出编译后代码到指定目录
92 if (this.rnConfig.outPath) {
93 this.tempPath = path.resolve(this.appPath, this.rnConfig.outPath)
94 if (!fs.existsSync(this.tempPath)) {
95 throw new Error(`outPath ${this.tempPath} 不存在`)
96 }
97 this.hasJDReactOutput = true
98 } else {
99 this.tempPath = path.join(appPath, TEMP_DIR_NAME)
100 this.hasJDReactOutput = false
101 }
102 }
103
104 isEntryFile (filePath) {
105 return path.basename(filePath) === this.entryFileName
106 }
107
108 compileDepStyles (filePath, styleFiles) {
109 if (isBuildingStyles[filePath] || styleFiles.length === 0) {
110 return Promise.resolve({})
111 }
112 isBuildingStyles[filePath] = true
113 return Promise.all(styleFiles.map(async p => { // to css string
114 const filePath = path.join(p)
115 const fileExt = path.extname(filePath)
116 printLog(processTypeEnum.COMPILE, _.camelCase(fileExt).toUpperCase(), filePath)
117 return StyleProcess.loadStyle({
118 filePath,
119 pluginsConfig: {
120 sass: this.sass,
121 less: this.less,
122 stylus: this.stylus
123 }
124 }, this.appPath)
125 })).then(resList => { // postcss
126 return Promise.all(resList.map(item => {
127 return StyleProcess.postCSS({ ...item as { css: string, filePath: string }, projectConfig: this.projectConfig })
128 }))
129 }).then(resList => {
130 const styleObjectEntire = {}
131 resList.forEach(item => {
132 const styleObject = StyleProcess.getStyleObject({ css: item.css, filePath: item.filePath })
133 // validate styleObject
134 StyleProcess.validateStyle({ styleObject, filePath: item.filePath })
135
136 Object.assign(styleObjectEntire, styleObject)
137 if (filePath !== this.entryFilePath) { // 非入口文件,合并全局样式
138 Object.assign(styleObjectEntire, _.get(styleDenpendencyTree, [this.entryFilePath, 'styleObjectEntire'], {}))
139 }
140 styleDenpendencyTree[filePath] = {
141 styleFiles,
142 styleObjectEntire
143 }
144 })
145 return JSON.stringify(styleObjectEntire, null, 2)
146 }).then(css => {
147 let tempFilePath = filePath.replace(this.sourceDir, this.tempPath)
148 const basename = path.basename(tempFilePath, path.extname(tempFilePath))
149 tempFilePath = path.join(path.dirname(tempFilePath), `${basename}_styles.js`)
150
151 StyleProcess.writeStyleFile({ css, tempFilePath })
152 }).catch((e) => {
153 throw new Error(e)
154 })
155 }
156
157 initProjectFile () {
158 // generator app.json
159 const appJsonObject = Object.assign({
160 name: _.camelCase(require(path.join(this.appPath, 'package.json')).name)
161 }, this.rnConfig.appJson)
162
163 const indexJsStr = `
164 import {AppRegistry} from 'react-native';
165 import App from './${this.entryBaseName}';
166 import {name as appName} from './app.json';
167
168 AppRegistry.registerComponent(appName, () => App);`
169
170 fs.writeFileSync(path.join(this.tempPath, 'index.js'), indexJsStr)
171 printLog(processTypeEnum.GENERATE, 'index.js', path.join(this.tempPath, 'index.js'))
172 fs.writeFileSync(path.join(this.tempPath, 'app.json'), JSON.stringify(appJsonObject, null, 2))
173 printLog(processTypeEnum.GENERATE, 'app.json', path.join(this.tempPath, 'app.json'))
174 return Promise.resolve()
175 }
176
177 async processFile (filePath) {
178 if (!fs.existsSync(filePath)) {
179 return
180 }
181 const dirname = path.dirname(filePath)
182 const distDirname = dirname.replace(this.sourceDir, this.tempPath)
183 let distPath = path.format({ dir: distDirname, base: path.basename(filePath) })
184 const code = fs.readFileSync(filePath, 'utf-8')
185 if (REG_STYLE.test(filePath)) {
186 // do something
187 } else if (REG_SCRIPTS.test(filePath)) {
188 if (/\.jsx(\?.*)?$/.test(filePath)) {
189 distPath = distPath.replace(/\.jsx(\?.*)?$/, '.js')
190 }
191 if (REG_TYPESCRIPT.test(filePath)) {
192 distPath = distPath.replace(/\.(tsx|ts)(\?.*)?$/, '.js')
193 }
194 printLog(processTypeEnum.COMPILE, _.camelCase(path.extname(filePath)).toUpperCase(), filePath)
195 // transformJSCode
196 const transformResult = transformJSCode({
197 code, filePath, isEntryFile: this.isEntryFile(filePath), projectConfig: this.projectConfig
198 })
199 const jsCode = transformResult.code
200 fs.ensureDirSync(distDirname)
201 fs.writeFileSync(distPath, Buffer.from(jsCode))
202 printLog(processTypeEnum.GENERATE, _.camelCase(path.extname(filePath)).toUpperCase(), distPath)
203 // compileDepStyles
204 const styleFiles = transformResult.styleFiles
205 depTree[filePath] = styleFiles
206 await this.compileDepStyles(filePath, styleFiles)
207 } else {
208 fs.ensureDirSync(distDirname)
209 printLog(processTypeEnum.COPY, _.camelCase(path.extname(filePath)).toUpperCase(), filePath)
210 fs.copySync(filePath, distPath)
211 printLog(processTypeEnum.GENERATE, _.camelCase(path.extname(filePath)).toUpperCase(), distPath)
212 }
213 }
214
215 /**
216 * @description 编译文件,安装依赖
217 * @returns {Promise}
218 */
219 buildTemp () {
220 return new Promise((resolve) => {
221 const filePaths: string[] = []
222 klaw(this.sourceDir)
223 .on('data', file => {
224 if (!file.stats.isDirectory()) {
225 filePaths.push(file.path)
226 }
227 })
228 .on('error', (err, item) => {
229 console.log(err.message)
230 console.log(item.path)
231 })
232 .on('end', () => {
233 Promise.all(filePaths.map(filePath => this.processFile(filePath)))
234 .then(() => {
235 if (!this.hasJDReactOutput) {
236 this.initProjectFile()
237 resolve()
238 } else {
239 resolve()
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.3.1",
330 "react-native": "0.55.4",
331 "redux": "^4.0.0",
332 "tslib": "^1.8.0"
333 }
334 `
335 return new Promise((resolve) => {
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(err)
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 } = buildConfig
379 process.env.TARO_ENV = 'rn'
380 const compiler = new Compiler(appPath)
381 fs.ensureDirSync(compiler.tempPath)
382 const t0 = performance.now()
383
384 if (!hasRNDep(appPath)) {
385 await updatePkgJson(appPath)
386 }
387 await compiler.buildTemp()
388 const t1 = performance.now()
389 printLog(processTypeEnum.COMPILE, `编译完成,花费${Math.round(t1 - t0)} ms`)
390
391 if (watch) {
392 compiler.watchFiles()
393 if (!compiler.hasJDReactOutput) {
394 startServerInNewWindow({ appPath })
395 }
396 } else {
397 compiler.buildBundle()
398 }
399}
400
401/**
402 * @description run packager server
403 * copy from react-native/local-cli/runAndroid/runAndroid.js
404 */
405function startServerInNewWindow ({ port = 8081, appPath }) {
406 // set up OS-specific filenames and commands
407 const isWindows = /^win/.test(process.platform)
408 const scriptFile = isWindows
409 ? 'launchPackager.bat'
410 : 'launchPackager.command'
411 const packagerEnvFilename = isWindows ? '.packager.bat' : '.packager.env'
412 const portExportContent = isWindows
413 ? `set RCT_METRO_PORT=${port}`
414 : `export RCT_METRO_PORT=${port}`
415
416 // set up the launchpackager.(command|bat) file
417 const scriptsDir = path.resolve(appPath, './node_modules', 'react-native', 'scripts')
418 const launchPackagerScript = path.resolve(scriptsDir, scriptFile)
419 const procConfig: SpawnSyncOptions = { cwd: scriptsDir }
420 const terminal = process.env.REACT_TERMINAL
421
422 // set up the .packager.(env|bat) file to ensure the packager starts on the right port
423 const packagerEnvFile = path.join(
424 appPath,
425 'node_modules',
426 'react-native',
427 'scripts',
428 packagerEnvFilename
429 )
430
431 // ensure we overwrite file by passing the 'w' flag
432 fs.writeFileSync(packagerEnvFile, portExportContent, {
433 encoding: 'utf8',
434 flag: 'w'
435 })
436
437 if (process.platform === 'darwin') {
438 if (terminal) {
439 return spawnSync(
440 'open',
441 ['-a', terminal, launchPackagerScript],
442 procConfig
443 )
444 }
445 return spawnSync('open', [launchPackagerScript], procConfig)
446 } else if (process.platform === 'linux') {
447 if (terminal) {
448 return spawn(
449 terminal,
450 ['-e', 'sh ' + launchPackagerScript],
451 procConfig
452 )
453 }
454 return spawn('sh', [launchPackagerScript], procConfig)
455 } else if (/^win/.test(process.platform)) {
456 procConfig.stdio = 'ignore'
457 return spawn(
458 'cmd.exe',
459 ['/C', launchPackagerScript],
460 procConfig
461 )
462 } else {
463 console.log(
464 chalk.red(
465 `Cannot start the packager. Unknown platform ${process.platform}`
466 )
467 )
468 }
469}