597 lines
17 KiB
JavaScript
597 lines
17 KiB
JavaScript
'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} <origin> <next>`);
|
||
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.`);
|
||
}
|
||
}; |