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 _ from 'lodash'
|
6 | import * as klaw from 'klaw'
|
7 | import { TogglableOptions, IOption } from '@tarojs/taro/types/compile'
|
8 | import {
|
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 |
|
24 | import { getPkgVersion } from './util'
|
25 | import * as StyleProcess from './rn/styleProcess'
|
26 | import { parseJSCode as transformJSCode } from './rn/transformJS'
|
27 | import { convertToJDReact } from './jdreact/convert_to_jdreact'
|
28 | import { IBuildOptions } from './util/types'
|
29 |
|
30 |
|
31 | let isBuildingStyles = {}
|
32 | const styleDenpendencyTree = {}
|
33 |
|
34 | const depTree: {
|
35 | [key: string]: string[]
|
36 | } = {}
|
37 |
|
38 | const TEMP_DIR_NAME = 'rn_temp'
|
39 | const BUNDLE_DIR_NAME = 'bundle'
|
40 |
|
41 | class 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 |
|
55 |
|
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 |
|
71 |
|
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
|
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 => {
|
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 => {
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
217 |
|
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 |
|
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.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 |
|
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(err)
|
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 } = 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 |
|
403 |
|
404 |
|
405 | function startServerInNewWindow ({ port = 8081, appPath }) {
|
406 |
|
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 |
|
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 |
|
423 | const packagerEnvFile = path.join(
|
424 | appPath,
|
425 | 'node_modules',
|
426 | 'react-native',
|
427 | 'scripts',
|
428 | packagerEnvFilename
|
429 | )
|
430 |
|
431 |
|
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 | }
|