import * as fs from 'fs-extra' import * as path from 'path' import { exec, spawn, spawnSync, execSync, SpawnSyncOptions } from 'child_process' import { performance } from 'perf_hooks' import * as _ from 'lodash' import * as klaw from 'klaw' import { TogglableOptions, IOption } from '@tarojs/taro/types/compile' import { PROJECT_CONFIG, processTypeEnum, REG_STYLE, REG_SCRIPTS, REG_TYPESCRIPT, chalk, chokidar, resolveScriptPath, printLog, shouldUseYarn, shouldUseCnpm, SOURCE_DIR, ENTRY } from '@tarojs/helper' import { getPkgVersion } from './util' import * as StyleProcess from './rn/styleProcess' import { parseJSCode as transformJSCode } from './rn/transformJS' import { convertToJDReact } from './jdreact/convert_to_jdreact' import { IBuildOptions } from './util/types' // import { Error } from 'tslint/lib/error' let isBuildingStyles = {} const styleDenpendencyTree = {} const depTree: { [key: string]: string[] } = {} const TEMP_DIR_NAME = 'rn_temp' const BUNDLE_DIR_NAME = 'bundle' class Compiler { projectConfig h5Config routerConfig appPath: string routerMode: string customRoutes: { [key: string]: string; } routerBasename: string sourcePath: string sourceDir: string // tempDir: string // bundleDir: string tempPath: string entryFilePath: string entryFileName: string entryBaseName: string babel: TogglableOptions csso: TogglableOptions uglify: TogglableOptions sass: IOption less: IOption stylus: IOption plugins: any[] rnConfig hasJDReactOutput: boolean babelConfig: any // pxTransformConfig // pathAlias constructor (appPath) { this.appPath = appPath this.projectConfig = require(resolveScriptPath(path.join(appPath, PROJECT_CONFIG)))(_.merge) const sourceDirName = this.projectConfig.sourceRoot || SOURCE_DIR this.sourceDir = path.join(appPath, sourceDirName) this.entryFilePath = resolveScriptPath(path.join(this.sourceDir, ENTRY)) this.entryFileName = path.basename(this.entryFilePath) this.entryBaseName = path.basename(this.entryFilePath, path.extname(this.entryFileName)) this.babel = this.projectConfig.babel this.csso = this.projectConfig.csso this.uglify = this.projectConfig.uglify this.plugins = this.projectConfig.plugins this.sass = this.projectConfig.sass this.stylus = this.projectConfig.stylus this.less = this.projectConfig.less this.rnConfig = this.projectConfig.rn || {} this.babelConfig = this.projectConfig.plugins.babel // 用来配置 babel // 直接输出编译后代码到指定目录 if (this.rnConfig.outPath) { this.tempPath = path.resolve(this.appPath, this.rnConfig.outPath) if (!fs.existsSync(this.tempPath)) { throw new Error(`outPath ${this.tempPath} 不存在`) } this.hasJDReactOutput = true } else { this.tempPath = path.join(appPath, TEMP_DIR_NAME) this.hasJDReactOutput = false } } isEntryFile (filePath) { return path.basename(filePath) === this.entryFileName } compileDepStyles (filePath, styleFiles) { if (isBuildingStyles[filePath] || styleFiles.length === 0) { return Promise.resolve({}) } isBuildingStyles[filePath] = true return Promise.all(styleFiles.map(async p => { // to css string const filePath = path.join(p) const fileExt = path.extname(filePath) printLog(processTypeEnum.COMPILE, _.camelCase(fileExt).toUpperCase(), filePath) return StyleProcess.loadStyle({ filePath, pluginsConfig: { sass: this.sass, less: this.less, stylus: this.stylus } }, this.appPath) })).then(resList => { // postcss return Promise.all(resList.map(item => { return StyleProcess.postCSS({ ...item as { css: string, filePath: string }, projectConfig: this.projectConfig }) })) }).then(resList => { const styleObjectEntire = {} resList.forEach(item => { const styleObject = StyleProcess.getStyleObject({ css: item.css, filePath: item.filePath }) // validate styleObject StyleProcess.validateStyle({ styleObject, filePath: item.filePath }) Object.assign(styleObjectEntire, styleObject) if (filePath !== this.entryFilePath) { // 非入口文件,合并全局样式 Object.assign(styleObjectEntire, _.get(styleDenpendencyTree, [this.entryFilePath, 'styleObjectEntire'], {})) } styleDenpendencyTree[filePath] = { styleFiles, styleObjectEntire } }) return JSON.stringify(styleObjectEntire, null, 2) }).then(css => { let tempFilePath = filePath.replace(this.sourceDir, this.tempPath) const basename = path.basename(tempFilePath, path.extname(tempFilePath)) tempFilePath = path.join(path.dirname(tempFilePath), `${basename}_styles.js`) StyleProcess.writeStyleFile({ css, tempFilePath }) }).catch((e) => { throw new Error(e) }) } initProjectFile () { // generator app.json const appJsonObject = Object.assign({ name: _.camelCase(require(path.join(this.appPath, 'package.json')).name) }, this.rnConfig.appJson) const indexJsStr = ` import {AppRegistry} from 'react-native'; import App from './${this.entryBaseName}'; import {name as appName} from './app.json'; AppRegistry.registerComponent(appName, () => App);` fs.writeFileSync(path.join(this.tempPath, 'index.js'), indexJsStr) printLog(processTypeEnum.GENERATE, 'index.js', path.join(this.tempPath, 'index.js')) fs.writeFileSync(path.join(this.tempPath, 'app.json'), JSON.stringify(appJsonObject, null, 2)) printLog(processTypeEnum.GENERATE, 'app.json', path.join(this.tempPath, 'app.json')) return Promise.resolve() } async processFile (filePath) { if (!fs.existsSync(filePath)) { return } const dirname = path.dirname(filePath) const distDirname = dirname.replace(this.sourceDir, this.tempPath) let distPath = path.format({ dir: distDirname, base: path.basename(filePath) }) const code = fs.readFileSync(filePath, 'utf-8') if (REG_STYLE.test(filePath)) { // do something } else if (REG_SCRIPTS.test(filePath)) { if (/\.jsx(\?.*)?$/.test(filePath)) { distPath = distPath.replace(/\.jsx(\?.*)?$/, '.js') } if (REG_TYPESCRIPT.test(filePath)) { distPath = distPath.replace(/\.(tsx|ts)(\?.*)?$/, '.js') } printLog(processTypeEnum.COMPILE, _.camelCase(path.extname(filePath)).toUpperCase(), filePath) // transformJSCode const transformResult = transformJSCode({ code, filePath, isEntryFile: this.isEntryFile(filePath), projectConfig: this.projectConfig }) const jsCode = transformResult.code fs.ensureDirSync(distDirname) fs.writeFileSync(distPath, Buffer.from(jsCode)) printLog(processTypeEnum.GENERATE, _.camelCase(path.extname(filePath)).toUpperCase(), distPath) // compileDepStyles const styleFiles = transformResult.styleFiles depTree[filePath] = styleFiles await this.compileDepStyles(filePath, styleFiles) } else { fs.ensureDirSync(distDirname) printLog(processTypeEnum.COPY, _.camelCase(path.extname(filePath)).toUpperCase(), filePath) fs.copySync(filePath, distPath) printLog(processTypeEnum.GENERATE, _.camelCase(path.extname(filePath)).toUpperCase(), distPath) } } /** * @description 编译文件,安装依赖 * @returns {Promise} */ buildTemp () { return new Promise((resolve) => { const filePaths: string[] = [] klaw(this.sourceDir) .on('data', file => { if (!file.stats.isDirectory()) { filePaths.push(file.path) } }) .on('error', (err, item) => { console.log(err.message) console.log(item.path) }) .on('end', () => { Promise.all(filePaths.map(filePath => this.processFile(filePath))) .then(() => { if (!this.hasJDReactOutput) { this.initProjectFile() resolve() } else { resolve() } }) }) }) } buildBundle () { fs.ensureDirSync(TEMP_DIR_NAME) process.chdir(TEMP_DIR_NAME) // 通过 jdreact 构建 bundle if (this.rnConfig.bundleType === 'jdreact') { console.log() console.log(chalk.green('生成JDReact 目录:')) console.log() convertToJDReact({ tempPath: this.tempPath, entryBaseName: this.entryBaseName }) return } // 默认打包到 bundle 文件夹 fs.ensureDirSync(BUNDLE_DIR_NAME) execSync( `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`, { stdio: 'inherit' }) } async perfWrap (callback, args?) { isBuildingStyles = {} // 清空 // 后期可以优化,不编译全部 const t0 = performance.now() await callback(args) const t1 = performance.now() printLog(processTypeEnum.COMPILE, `编译完成,花费${Math.round(t1 - t0)} ms`) console.log() } watchFiles () { const watcher = chokidar.watch(path.join(this.sourceDir), { ignored: /(^|[/\\])\../, persistent: true, ignoreInitial: true }) watcher .on('ready', () => { console.log() console.log(chalk.gray('初始化完毕,监听文件修改中...')) console.log() }) .on('add', filePath => { const relativePath = path.relative(this.appPath, filePath) printLog(processTypeEnum.CREATE, '添加文件', relativePath) this.perfWrap(this.buildTemp.bind(this)) }) .on('change', filePath => { const relativePath = path.relative(this.appPath, filePath) printLog(processTypeEnum.MODIFY, '文件变动', relativePath) if (REG_SCRIPTS.test(filePath)) { this.perfWrap(this.processFile.bind(this), filePath) } if (REG_STYLE.test(filePath)) { _.forIn(depTree, (styleFiles, jsFilePath) => { if (styleFiles.indexOf(filePath) > -1) { this.perfWrap(this.processFile.bind(this), jsFilePath) } }) } }) .on('unlink', filePath => { const relativePath = path.relative(this.appPath, filePath) printLog(processTypeEnum.UNLINK, '删除文件', relativePath) this.perfWrap(this.buildTemp.bind(this)) }) .on('error', error => console.log(`Watcher error: ${error}`)) } } function hasRNDep (appPath) { const pkgJson = require(path.join(appPath, 'package.json')) return Boolean(pkgJson.dependencies['react-native']) } function updatePkgJson (appPath) { const version = getPkgVersion() const RNDep = `{ "@tarojs/components-rn": "^${version}", "@tarojs/taro-rn": "^${version}", "@tarojs/taro-router-rn": "^${version}", "@tarojs/taro-redux-rn": "^${version}", "react": "16.3.1", "react-native": "0.55.4", "redux": "^4.0.0", "tslib": "^1.8.0" } ` return new Promise((resolve) => { const pkgJson = require(path.join(appPath, 'package.json')) // 未安装 RN 依赖,则更新 pkgjson,并重新安装依赖 if (!hasRNDep(appPath)) { pkgJson.dependencies = Object.assign({}, pkgJson.dependencies, JSON.parse(RNDep.replace(/(\r\n|\n|\r|\s+)/gm, ''))) fs.writeFileSync(path.join(appPath, 'package.json'), JSON.stringify(pkgJson, null, 2)) printLog(processTypeEnum.GENERATE, 'package.json', path.join(appPath, 'package.json')) installDep(appPath).then(() => { resolve() }) } else { resolve() } }) } function installDep (path: string) { return new Promise((resolve, reject) => { console.log() console.log(chalk.yellow('开始安装依赖~')) process.chdir(path) let command if (shouldUseYarn()) { command = 'yarn' } else if (shouldUseCnpm()) { command = 'cnpm install' } else { command = 'npm install' } exec(command, (err, stdout, stderr) => { if (err) reject(err) else { console.log(stdout) console.log(stderr) } resolve() }) }) } export { Compiler } export async function build (appPath: string, buildConfig: IBuildOptions) { const { watch } = buildConfig process.env.TARO_ENV = 'rn' const compiler = new Compiler(appPath) fs.ensureDirSync(compiler.tempPath) const t0 = performance.now() if (!hasRNDep(appPath)) { await updatePkgJson(appPath) } await compiler.buildTemp() const t1 = performance.now() printLog(processTypeEnum.COMPILE, `编译完成,花费${Math.round(t1 - t0)} ms`) if (watch) { compiler.watchFiles() if (!compiler.hasJDReactOutput) { startServerInNewWindow({ appPath }) } } else { compiler.buildBundle() } } /** * @description run packager server * copy from react-native/local-cli/runAndroid/runAndroid.js */ function startServerInNewWindow ({ port = 8081, appPath }) { // set up OS-specific filenames and commands const isWindows = /^win/.test(process.platform) const scriptFile = isWindows ? 'launchPackager.bat' : 'launchPackager.command' const packagerEnvFilename = isWindows ? '.packager.bat' : '.packager.env' const portExportContent = isWindows ? `set RCT_METRO_PORT=${port}` : `export RCT_METRO_PORT=${port}` // set up the launchpackager.(command|bat) file const scriptsDir = path.resolve(appPath, './node_modules', 'react-native', 'scripts') const launchPackagerScript = path.resolve(scriptsDir, scriptFile) const procConfig: SpawnSyncOptions = { cwd: scriptsDir } const terminal = process.env.REACT_TERMINAL // set up the .packager.(env|bat) file to ensure the packager starts on the right port const packagerEnvFile = path.join( appPath, 'node_modules', 'react-native', 'scripts', packagerEnvFilename ) // ensure we overwrite file by passing the 'w' flag fs.writeFileSync(packagerEnvFile, portExportContent, { encoding: 'utf8', flag: 'w' }) if (process.platform === 'darwin') { if (terminal) { return spawnSync( 'open', ['-a', terminal, launchPackagerScript], procConfig ) } return spawnSync('open', [launchPackagerScript], procConfig) } else if (process.platform === 'linux') { if (terminal) { return spawn( terminal, ['-e', 'sh ' + launchPackagerScript], procConfig ) } return spawn('sh', [launchPackagerScript], procConfig) } else if (/^win/.test(process.platform)) { procConfig.stdio = 'ignore' return spawn( 'cmd.exe', ['/C', launchPackagerScript], procConfig ) } else { console.log( chalk.red( `Cannot start the packager. Unknown platform ${process.platform}` ) ) } }