1 | import * as fs from 'fs-extra'
|
2 | import * as path from 'path'
|
3 | import { exec, spawn, spawnSync, execSync, SpawnSyncOptions } from 'child_process'
|
4 | import { performance } from 'perf_hooks'
|
5 | import * as chokidar from 'chokidar'
|
6 | import chalk from 'chalk'
|
7 | import * as _ from 'lodash'
|
8 | import * as klaw from 'klaw'
|
9 | import { TogglableOptions, ICommonPlugin, IOption } from '@tarojs/taro/types/compile'
|
10 |
|
11 | import * as Util from './util'
|
12 | import CONFIG from './config'
|
13 | import * as StyleProcess from './rn/styleProcess'
|
14 | import { parseJSCode as transformJSCode } from './rn/transformJS'
|
15 | import { PROJECT_CONFIG, processTypeEnum, REG_STYLE, REG_SCRIPTS, REG_TYPESCRIPT, BUILD_TYPES } from './util/constants'
|
16 | import { convertToJDReact } from './jdreact/convert_to_jdreact'
|
17 | import { IBuildOptions } from './util/types'
|
18 |
|
19 |
|
20 | let isBuildingStyles = {}
|
21 | const styleDenpendencyTree = {}
|
22 |
|
23 | const depTree: {
|
24 | [key: string]: string[]
|
25 | } = {}
|
26 |
|
27 | const TEMP_DIR_NAME = 'rn_temp'
|
28 | const BUNDLE_DIR_NAME = 'bundle'
|
29 |
|
30 | class 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 |
|
43 |
|
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 |
|
58 |
|
59 |
|
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 |
|
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 => {
|
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 => {
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
205 |
|
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 |
|
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 |
|
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 |
|
307 | function hasRNDep (appPath) {
|
308 | const pkgJson = require(path.join(appPath, 'package.json'))
|
309 | return Boolean(pkgJson.dependencies['react-native'])
|
310 | }
|
311 |
|
312 | function 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 |
|
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 |
|
341 | function 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 |
|
365 | export { Compiler }
|
366 |
|
367 | export 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 |
|
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 |
|
399 |
|
400 |
|
401 | function startServerInNewWindow ({port = 8081, appPath}) {
|
402 |
|
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 |
|
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 |
|
419 | const packagerEnvFile = path.join(
|
420 | appPath,
|
421 | 'node_modules',
|
422 | 'react-native',
|
423 | 'scripts',
|
424 | packagerEnvFilename
|
425 | )
|
426 |
|
427 |
|
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 | }
|