'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.commands = undefined; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _path = require('path'); var _path2 = _interopRequireDefault(_path); var _utils = require('./utils'); var _fsExtra = require('fs-extra'); var fs = _interopRequireWildcard(_fsExtra); var _yazl = require('yazl'); var _yauzl = require('yauzl'); var _app = require('./app'); var _child_process = require('child_process'); var _os = require('os'); var _os2 = _interopRequireDefault(_os); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const g2js = require('gradle-to-js/lib/parser'); const properties = require('properties'); let bsdiff, hdiff, diff; try { bsdiff = require('node-bsdiff').diff; } catch (e) {} try { hdiff = require('node-hdiffpatch').diff; } catch (e) {} async function runReactNativeBundleCommand(bundleName, development, entryFile, outputFolder, platform, sourcemapOutput, config) { let gradleConfig = {}; if (platform === 'android') { gradleConfig = await checkGradleConfig(); if (gradleConfig.crunchPngs !== false) { console.warn('android的crunchPngs选项似乎尚未禁用(如已禁用则请忽略此提示),这可能导致热更包体积异常增大,具体请参考 https://pushy.reactnative.cn/docs/getting-started.html#%E7%A6%81%E7%94%A8-android-%E7%9A%84-crunch-%E4%BC%98%E5%8C%96 \n'); } } let reactNativeBundleArgs = []; let envArgs = process.env.PUSHY_ENV_ARGS; if (envArgs) { Array.prototype.push.apply(reactNativeBundleArgs, envArgs.trim().split(/\s+/)); } fs.emptyDirSync(outputFolder); let cliPath = require.resolve('react-native/local-cli/cli.js', { paths: [process.cwd()] }); try { require.resolve('expo-router', { paths: [process.cwd()] }); console.log(`expo-router detected, will use @expo/cli to bundle.\n`); // if using expo-router, use expo-cli cliPath = require.resolve('@expo/cli', { paths: [process.cwd()] }); } catch (e) {} const bundleCommand = cliPath.includes('@expo/cli') ? 'export:embed' : 'bundle'; Array.prototype.push.apply(reactNativeBundleArgs, [cliPath, bundleCommand, '--assets-dest', outputFolder, '--bundle-output', _path2.default.join(outputFolder, bundleName), '--dev', development, '--entry-file', entryFile, '--platform', platform, '--reset-cache']); if (sourcemapOutput) { reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput); } if (config) { reactNativeBundleArgs.push('--config', config); } const reactNativeBundleProcess = (0, _child_process.spawn)('node', reactNativeBundleArgs); console.log(`Running bundle command: node ${reactNativeBundleArgs.join(' ')}`); return new Promise((resolve, reject) => { reactNativeBundleProcess.stdout.on('data', data => { console.log(data.toString().trim()); }); reactNativeBundleProcess.stderr.on('data', data => { console.error(data.toString().trim()); }); reactNativeBundleProcess.on('close', async exitCode => { if (exitCode) { reject(new Error(`"react-native bundle" command exited with code ${exitCode}.`)); } else { let hermesEnabled = false; if (platform === 'android') { const gradlePropeties = await new Promise(resolve => { properties.parse('./android/gradle.properties', { path: true }, function (error, props) { if (error) { console.error(error); resolve(null); } resolve(props); }); }); hermesEnabled = gradlePropeties.hermesEnabled; if (typeof hermesEnabled !== 'boolean') hermesEnabled = gradleConfig.enableHermes; } else if (platform === 'ios' && fs.existsSync('ios/Pods/hermes-engine')) { hermesEnabled = true; } if (hermesEnabled) { await compileHermesByteCode(bundleName, outputFolder, sourcemapOutput); } resolve(null); } }); }); } function getHermesOSBin() { if (_os2.default.platform() === 'win32') return 'win64-bin'; if (_os2.default.platform() === 'darwin') return 'osx-bin'; if (_os2.default.platform() === 'linux') return 'linux64-bin'; } async function checkGradleConfig() { let enableHermes = false; let crunchPngs; try { const gradleConfig = await g2js.parseFile('android/app/build.gradle'); const projectConfig = gradleConfig['project.ext.react']; for (const packagerConfig of projectConfig) { if (packagerConfig.includes('enableHermes') && packagerConfig.includes('true')) { enableHermes = true; break; } } crunchPngs = gradleConfig.android.buildTypes.release.crunchPngs; } catch (e) {} return { enableHermes, crunchPngs }; } async function compileHermesByteCode(bundleName, outputFolder, sourcemapOutput) { console.log(`Hermes enabled, now compiling to hermes bytecode:\n`); // >= rn 0.69 const rnDir = _path2.default.dirname(require.resolve('react-native', { paths: [process.cwd()] })); let hermesPath = _path2.default.join(rnDir, `/sdks/hermesc/${getHermesOSBin()}`); // < rn 0.69 if (!fs.existsSync(hermesPath)) { hermesPath = `node_modules/hermes-engine/${getHermesOSBin()}`; } const hermesCommand = `${hermesPath}/hermesc`; const args = ['-emit-binary', '-out', _path2.default.join(outputFolder, bundleName), _path2.default.join(outputFolder, bundleName), '-O']; if (sourcemapOutput) { args.push('-output-source-map'); } console.log('Running hermesc: ' + hermesCommand + ' ' + args.join(' ') + '\n'); (0, _child_process.spawnSync)(hermesCommand, args, { stdio: 'ignore' }); } async function pack(dir, output) { console.log('Packing'); fs.ensureDirSync(_path2.default.dirname(output)); await new Promise((resolve, reject) => { const zipfile = new _yazl.ZipFile(); function addDirectory(root, rel) { if (rel) { zipfile.addEmptyDirectory(rel); } const childs = fs.readdirSync(root); for (const name of childs) { if (name === '.' || name === '..' || name === 'index.bundlejs.map') { continue; } const fullPath = _path2.default.join(root, name); const stat = fs.statSync(fullPath); if (stat.isFile()) { //console.log('adding: ' + rel+name); zipfile.addFile(fullPath, rel + name); } else if (stat.isDirectory()) { //console.log('adding: ' + rel+name+'/'); addDirectory(fullPath, rel + name + '/'); } } } addDirectory(dir, ''); zipfile.outputStream.on('error', err => reject(err)); zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function () { resolve(); }); zipfile.end(); }); console.log('ppk热更包已生成并保存到: ' + output); } function readEntire(entry, zipFile) { const buffers = []; return new Promise((resolve, reject) => { zipFile.openReadStream(entry, (err, stream) => { stream.pipe({ write(chunk) { buffers.push(chunk); }, end() { resolve(Buffer.concat(buffers)); }, prependListener() {}, on() {}, once() {}, emit() {} }); }); }); } function basename(fn) { const m = /^(.+\/)[^\/]+\/?$/.exec(fn); return m && m[1]; } async function diffFromPPK(origin, next, output) { fs.ensureDirSync(_path2.default.dirname(output)); const originEntries = {}; const originMap = {}; let originSource; await enumZipEntries(origin, (entry, zipFile) => { originEntries[entry.fileName] = entry; if (!/\/$/.test(entry.fileName)) { // isFile originMap[entry.crc32] = entry.fileName; if (entry.fileName === 'index.bundlejs') { // This is source. return readEntire(entry, zipFile).then(v => originSource = v); } } }); if (!originSource) { throw new Error(`Bundle file not found! Please use default bundle file name and path.`); } const copies = {}; const zipfile = new _yazl.ZipFile(); const writePromise = new Promise((resolve, reject) => { zipfile.outputStream.on('error', err => { throw err; }); zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function () { resolve(); }); }); const addedEntry = {}; function addEntry(fn) { //console.log(fn); if (!fn || addedEntry[fn]) { return; } const base = basename(fn); if (base) { addEntry(base); } zipfile.addEmptyDirectory(fn); } const newEntries = {}; await enumZipEntries(next, (entry, nextZipfile) => { newEntries[entry.fileName] = entry; if (/\/$/.test(entry.fileName)) { // Directory if (!originEntries[entry.fileName]) { addEntry(entry.fileName); } } else if (entry.fileName === 'index.bundlejs') { //console.log('Found bundle'); return readEntire(entry, nextZipfile).then(newSource => { //console.log('Begin diff'); zipfile.addBuffer(diff(originSource, newSource), 'index.bundlejs.patch'); //console.log('End diff'); }); } else { // If same file. const originEntry = originEntries[entry.fileName]; if (originEntry && originEntry.crc32 === entry.crc32) { // ignore return; } // If moved from other place if (originMap[entry.crc32]) { const base = basename(entry.fileName); if (!originEntries[base]) { addEntry(base); } copies[entry.fileName] = originMap[entry.crc32]; return; } // New file. addEntry(basename(entry.fileName)); return new Promise((resolve, reject) => { nextZipfile.openReadStream(entry, function (err, readStream) { if (err) { return reject(err); } zipfile.addReadStream(readStream, entry.fileName); readStream.on('end', () => { //console.log('add finished'); resolve(); }); }); }); } }); const deletes = {}; for (let k in originEntries) { if (!newEntries[k]) { console.log('Delete ' + k); deletes[k] = 1; } } //console.log({copies, deletes}); zipfile.addBuffer(Buffer.from(JSON.stringify({ copies, deletes })), '__diff.json'); zipfile.end(); await writePromise; } async function diffFromPackage(origin, next, output, originBundleName, transformPackagePath = v => v) { fs.ensureDirSync(_path2.default.dirname(output)); const originEntries = {}; const originMap = {}; let originSource; await enumZipEntries(origin, (entry, zipFile) => { if (!/\/$/.test(entry.fileName)) { const fn = transformPackagePath(entry.fileName); if (!fn) { return; } //console.log(fn); // isFile originEntries[fn] = entry.crc32; originMap[entry.crc32] = fn; if (fn === originBundleName) { // This is source. return readEntire(entry, zipFile).then(v => originSource = v); } } }); if (!originSource) { throw new Error(`Bundle file not found! Please use default bundle file name and path.`); } const copies = {}; const zipfile = new _yazl.ZipFile(); const writePromise = new Promise((resolve, reject) => { zipfile.outputStream.on('error', err => { throw err; }); zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function () { resolve(); }); }); await enumZipEntries(next, (entry, nextZipfile) => { if (/\/$/.test(entry.fileName)) { // Directory zipfile.addEmptyDirectory(entry.fileName); } else if (entry.fileName === 'index.bundlejs') { //console.log('Found bundle'); return readEntire(entry, nextZipfile).then(newSource => { //console.log('Begin diff'); zipfile.addBuffer(diff(originSource, newSource), 'index.bundlejs.patch'); //console.log('End diff'); }); } else { // If same file. if (originEntries[entry.fileName] === entry.crc32) { copies[entry.fileName] = ''; return; } // If moved from other place if (originMap[entry.crc32]) { copies[entry.fileName] = originMap[entry.crc32]; return; } return new Promise((resolve, reject) => { nextZipfile.openReadStream(entry, function (err, readStream) { if (err) { return reject(err); } zipfile.addReadStream(readStream, entry.fileName); readStream.on('end', () => { //console.log('add finished'); resolve(); }); }); }); } }); zipfile.addBuffer(Buffer.from(JSON.stringify({ copies })), '__diff.json'); zipfile.end(); await writePromise; } function enumZipEntries(zipFn, callback) { return new Promise((resolve, reject) => { (0, _yauzl.open)(zipFn, { lazyEntries: true }, (err, zipfile) => { if (err) { return reject(err); } zipfile.on('end', resolve); zipfile.on('error', reject); zipfile.on('entry', entry => { const result = callback(entry, zipfile); if (result && typeof result.then === 'function') { result.then(() => zipfile.readEntry()); } else { zipfile.readEntry(); } }); zipfile.readEntry(); }); }); } function diffArgsCheck(args, options, diffFn) { const [origin, next] = args; if (!origin || !next) { console.error(`Usage: pushy ${diffFn} `); process.exit(1); } if (diffFn.startsWith('hdiff')) { if (!hdiff) { console.error(`This function needs "node-hdiffpatch". Please run "npm i node-hdiffpatch" to install`); process.exit(1); } diff = hdiff; } else { if (!bsdiff) { console.error(`This function needs "node-bsdiff". Please run "npm i node-bsdiff" to install`); process.exit(1); } diff = bsdiff; } const { output } = options; return { origin, next, realOutput: output.replace(/\$\{time\}/g, '' + Date.now()) }; } const commands = exports.commands = { bundle: async function ({ options }) { const platform = (0, _app.checkPlatform)(options.platform || (await (0, _utils.question)('平台(ios/android):'))); let { bundleName, entryFile, intermediaDir, output, dev, sourcemap } = (0, _utils.translateOptions)(_extends({}, options, { platform })); const sourcemapOutput = _path2.default.join(intermediaDir, bundleName + '.map'); const realOutput = output.replace(/\$\{time\}/g, '' + Date.now()); if (!platform) { throw new Error('Platform must be specified.'); } const { version, major, minor } = (0, _utils.getRNVersion)(); console.log('Bundling with react-native: ', version); (0, _utils.printVersionCommand)(); await runReactNativeBundleCommand(bundleName, dev, entryFile, intermediaDir, platform, sourcemap ? sourcemapOutput : ''); await pack(_path2.default.resolve(intermediaDir), realOutput); const v = await (0, _utils.question)('是否现在上传此热更包?(Y/N)'); if (v.toLowerCase() === 'y') { await this.publish({ args: [realOutput], options: { platform } }); } }, async diff({ args, options }) { const { origin, next, realOutput } = diffArgsCheck(args, options, 'diff'); await diffFromPPK(origin, next, realOutput, 'index.bundlejs'); console.log(`${realOutput} generated.`); }, async hdiff({ args, options }) { const { origin, next, realOutput } = diffArgsCheck(args, options, 'hdiff'); await diffFromPPK(origin, next, realOutput, 'index.bundlejs'); console.log(`${realOutput} generated.`); }, async diffFromApk({ args, options }) { const { origin, next, realOutput } = diffArgsCheck(args, options, 'diffFromApk'); await diffFromPackage(origin, next, realOutput, 'assets/index.android.bundle'); console.log(`${realOutput} generated.`); }, async hdiffFromApk({ args, options }) { const { origin, next, realOutput } = diffArgsCheck(args, options, 'hdiffFromApk'); await diffFromPackage(origin, next, realOutput, 'assets/index.android.bundle'); console.log(`${realOutput} generated.`); }, async diffFromIpa({ args, options }) { const { origin, next, realOutput } = diffArgsCheck(args, options, 'diffFromIpa'); await diffFromPackage(origin, next, realOutput, 'main.jsbundle', v => { const m = /^Payload\/[^/]+\/(.+)$/.exec(v); return m && m[1]; }); console.log(`${realOutput} generated.`); }, async hdiffFromIpa({ args, options }) { const { origin, next, realOutput } = diffArgsCheck(args, options, 'hdiffFromIpa'); await diffFromPackage(origin, next, realOutput, 'main.jsbundle', v => { const m = /^Payload\/[^/]+\/(.+)$/.exec(v); return m && m[1]; }); console.log(`${realOutput} generated.`); } };