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 * as _ from 'lodash'
|
7 | import * as klaw from 'klaw'
|
8 | import { TogglableOptions, IOption } from '@tarojs/taro/types/compile'
|
9 | import {
|
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 |
|
22 | import { getPkgVersion, checkCliAndFrameworkVersion } from './util'
|
23 | import CONFIG from './config'
|
24 | import * as StyleProcess from './rn/styleProcess'
|
25 | import { parseJSCode as transformJSCode } from './rn/transformJS'
|
26 | import { convertToJDReact } from './jdreact/convert_to_jdreact'
|
27 | import { IBuildOptions } from './util/types'
|
28 |
|
29 |
|
30 | let isBuildingStyles = {}
|
31 | const styleDenpendencyTree = {}
|
32 |
|
33 | const depTree: {
|
34 | [key: string]: string[]
|
35 | } = {}
|
36 |
|
37 | const TEMP_DIR_NAME = 'rn_temp'
|
38 | const BUNDLE_DIR_NAME = 'bundle'
|
39 |
|
40 | class 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 |
|
53 |
|
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 |
|
68 |
|
69 |
|
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 |
|
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 => {
|
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 => {
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
215 |
|
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 |
|
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 |
|
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 |
|
317 | function hasRNDep (appPath) {
|
318 | const pkgJson = require(path.join(appPath, 'package.json'))
|
319 | return Boolean(pkgJson.dependencies['react-native'])
|
320 | }
|
321 |
|
322 | function 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 |
|
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 |
|
351 | function 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 |
|
375 | export { Compiler }
|
376 |
|
377 | export 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 |
|
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 |
|
409 |
|
410 |
|
411 | function startServerInNewWindow ({port = 8081, appPath}) {
|
412 |
|
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 |
|
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 |
|
429 | const packagerEnvFile = path.join(
|
430 | appPath,
|
431 | 'node_modules',
|
432 | 'react-native',
|
433 | 'scripts',
|
434 | packagerEnvFilename
|
435 | )
|
436 |
|
437 |
|
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 | }
|