1
0
mirror of https://gitcode.com/github-mirrors/react-native-update-cli.git synced 2025-09-16 09:41:38 +08:00
Code Issues Packages Projects Releases Wiki Activity GitHub Gitee
Files
react-native-update-cli/src/bundle.ts
Sunny Luo d0495fb271 Feat/deps (#10)
* init

* add deps

* sort key
2025-03-14 13:44:47 +08:00

1055 lines
26 KiB
TypeScript

import path from 'node:path';
import { translateOptions } from './utils';
import * as fs from 'fs-extra';
import { ZipFile } from 'yazl';
import { open as openZipFile } from 'yauzl';
import { question, checkPlugins } from './utils';
import { checkPlatform } from './app';
import { spawn, spawnSync } from 'node:child_process';
import semverSatisfies from 'semver/functions/satisfies';
const g2js = require('gradle-to-js/lib/parser');
import os from 'node:os';
const properties = require('properties');
import { depVersions } from './utils/dep-versions';
let bsdiff;
let hdiff;
let diff;
try {
bsdiff = require('node-bsdiff').diff;
} catch (e) {}
try {
hdiff = require('node-hdiffpatch').diff;
} catch (e) {}
async function runReactNativeBundleCommand({
bundleName,
dev,
entryFile,
outputFolder,
platform,
sourcemapOutput,
config,
disableHermes,
cli,
}: {
bundleName: string;
dev: string;
entryFile: string;
outputFolder: string;
platform: string;
sourcemapOutput: string;
config?: string;
disableHermes?: boolean;
cli: {
taro?: boolean;
expo?: boolean;
rncli?: boolean;
};
}) {
let gradleConfig: {
crunchPngs?: boolean;
enableHermes?: boolean;
} = {};
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',
);
}
}
const reactNativeBundleArgs: string[] = [];
const envArgs = process.env.PUSHY_ENV_ARGS;
if (envArgs) {
Array.prototype.push.apply(
reactNativeBundleArgs,
envArgs.trim().split(/\s+/),
);
}
fs.emptyDirSync(outputFolder);
let cliPath: string | undefined;
let usingExpo = false;
const getExpoCli = () => {
try {
cliPath = require.resolve('@expo/cli', {
paths: [process.cwd()],
});
const expoCliVersion = JSON.parse(
fs
.readFileSync(
require.resolve('@expo/cli/package.json', {
paths: [process.cwd()],
}),
)
.toString(),
).version;
// expo cli 0.10.17 (expo 49) 开始支持 bundle:embed
if (semverSatisfies(expoCliVersion, '>= 0.10.17')) {
usingExpo = true;
} else {
cliPath = undefined;
}
} catch (e) {}
};
const getRnCli = () => {
try {
// rn >= 0.75
cliPath = require.resolve('@react-native-community/cli/build/bin.js', {
paths: [process.cwd()],
});
} catch (e) {
// rn < 0.75
cliPath = require.resolve('react-native/local-cli/cli.js', {
paths: [process.cwd()],
});
}
};
const getTaroCli = () => {
try {
cliPath = require.resolve('@tarojs/cli/bin/taro', {
paths: [process.cwd()],
});
} catch (e) {}
};
if (cli.expo) {
getExpoCli();
} else if (cli.taro) {
getTaroCli();
} else if (cli.rncli) {
getRnCli();
}
if (!cliPath) {
getExpoCli();
if (!usingExpo) {
getRnCli();
}
}
const bundleParams = await checkPlugins();
const isSentry = bundleParams.sentry;
let bundleCommand = 'bundle';
if (usingExpo) {
bundleCommand = 'export:embed';
} else if (platform === 'harmony') {
bundleCommand = 'bundle-harmony';
} else if (cli.taro) {
bundleCommand = 'build';
}
if (platform === 'harmony') {
Array.prototype.push.apply(reactNativeBundleArgs, [
cliPath,
bundleCommand,
'--dev',
dev,
'--entry-file',
entryFile,
]);
if (sourcemapOutput) {
reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput);
}
if (config) {
reactNativeBundleArgs.push('--config', config);
}
} else {
Array.prototype.push.apply(reactNativeBundleArgs, [
cliPath,
bundleCommand,
'--assets-dest',
outputFolder,
'--bundle-output',
path.join(outputFolder, bundleName),
'--platform',
platform,
'--reset-cache',
]);
if (cli.taro) {
reactNativeBundleArgs.push(...['--type', 'rn']);
} else {
reactNativeBundleArgs.push(...['--dev', dev, '--entry-file', entryFile]);
}
if (sourcemapOutput) {
reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput);
}
if (config) {
reactNativeBundleArgs.push('--config', config);
}
}
const reactNativeBundleProcess = 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: boolean | undefined = false;
if (disableHermes) {
hermesEnabled = false;
console.log('Hermes disabled');
} else if (platform === 'android') {
const gradlePropeties = await new Promise<{
hermesEnabled?: boolean;
}>((resolve) => {
properties.parse(
'./android/gradle.properties',
{ path: true },
(error: any, props: { hermesEnabled?: boolean }) => {
if (error) {
console.error(error);
resolve({});
}
resolve(props);
},
);
});
hermesEnabled = gradlePropeties.hermesEnabled;
if (typeof hermesEnabled !== 'boolean')
hermesEnabled = gradleConfig.enableHermes;
} else if (
platform === 'ios' &&
fs.existsSync('ios/Pods/hermes-engine')
) {
hermesEnabled = true;
} else if (platform === 'harmony') {
await copyHarmonyBundle(outputFolder);
}
if (hermesEnabled) {
await compileHermesByteCode(
bundleName,
outputFolder,
sourcemapOutput,
!isSentry,
);
}
resolve(null);
}
});
});
}
async function copyHarmonyBundle(outputFolder: string) {
const harmonyRawPath = 'harmony/entry/src/main/resources/rawfile';
try {
await fs.ensureDir(harmonyRawPath);
try {
await fs.access(harmonyRawPath, fs.constants.W_OK);
} catch (error) {
await fs.chmod(harmonyRawPath, 0o755);
}
await fs.remove(path.join(harmonyRawPath, 'update.json'));
await fs.copy('update.json', path.join(harmonyRawPath, 'update.json'));
await fs.ensureDir(outputFolder);
await fs.copy(harmonyRawPath, outputFolder);
} catch (error: any) {
console.error('copyHarmonyBundle 错误:', error);
throw new Error(`复制文件失败: ${error.message}`);
}
}
function getHermesOSBin() {
if (os.platform() === 'win32') return 'win64-bin';
if (os.platform() === 'darwin') return 'osx-bin';
if (os.platform() === 'linux') return 'linux64-bin';
}
async function checkGradleConfig() {
let enableHermes = false;
let crunchPngs: boolean | undefined;
try {
const gradleConfig = await g2js.parseFile('android/app/build.gradle');
crunchPngs = gradleConfig.android.buildTypes.release.crunchPngs;
const projectConfig = gradleConfig['project.ext.react'];
if (projectConfig) {
for (const packagerConfig of projectConfig) {
if (
packagerConfig.includes('enableHermes') &&
packagerConfig.includes('true')
) {
enableHermes = true;
break;
}
}
}
} catch (e) {}
return {
enableHermes,
crunchPngs,
};
}
async function compileHermesByteCode(
bundleName: string,
outputFolder: string,
sourcemapOutput: string,
shouldCleanSourcemap: boolean,
) {
console.log('Hermes enabled, now compiling to hermes bytecode:\n');
// >= rn 0.69
const rnDir = path.dirname(
require.resolve('react-native', {
paths: [process.cwd()],
}),
);
let hermesPath = path.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',
path.join(outputFolder, bundleName),
path.join(outputFolder, bundleName),
'-O',
];
if (sourcemapOutput) {
fs.copyFileSync(
sourcemapOutput,
path.join(outputFolder, `${bundleName}.txt.map`),
);
args.push('-output-source-map');
}
console.log(`Running hermesc: ${hermesCommand} ${args.join(' ')}`);
spawnSync(hermesCommand, args, {
stdio: 'ignore',
});
if (sourcemapOutput) {
const composerPath =
'node_modules/react-native/scripts/compose-source-maps.js';
if (!fs.existsSync(composerPath)) {
return;
}
console.log('Composing source map');
spawnSync(
'node',
[
composerPath,
path.join(outputFolder, `${bundleName}.txt.map`),
path.join(outputFolder, `${bundleName}.map`),
'-o',
sourcemapOutput,
],
{
stdio: 'ignore',
},
);
}
if (shouldCleanSourcemap) {
fs.removeSync(path.join(outputFolder, `${bundleName}.txt.map`));
}
}
async function copyDebugidForSentry(
bundleName: string,
outputFolder: string,
sourcemapOutput: string,
) {
if (sourcemapOutput) {
let copyDebugidPath;
try {
copyDebugidPath = require.resolve(
'@sentry/react-native/scripts/copy-debugid.js',
{
paths: [process.cwd()],
},
);
} catch (error) {
console.error(
'无法找到 Sentry copy-debugid.js 脚本文件,请确保已正确安装 @sentry/react-native',
);
return;
}
if (!fs.existsSync(copyDebugidPath)) {
return;
}
console.log('Copying debugid');
spawnSync(
'node',
[
copyDebugidPath,
path.join(outputFolder, `${bundleName}.txt.map`),
path.join(outputFolder, `${bundleName}.map`),
],
{
stdio: 'ignore',
},
);
}
fs.removeSync(path.join(outputFolder, `${bundleName}.txt.map`));
}
async function uploadSourcemapForSentry(
bundleName: string,
outputFolder: string,
sourcemapOutput: string,
version: string,
) {
if (sourcemapOutput) {
let sentryCliPath;
try {
sentryCliPath = require.resolve('@sentry/cli/bin/sentry-cli', {
paths: [process.cwd()],
});
} catch (error) {
console.error('无法找到 Sentry CLI 工具,请确保已正确安装 @sentry/cli');
return;
}
if (!fs.existsSync(sentryCliPath)) {
return;
}
spawnSync(
'node',
[sentryCliPath, 'releases', 'set-commits', version, '--auto'],
{
stdio: 'inherit',
},
);
console.log(`Sentry release created for version: ${version}`);
console.log('Uploading sourcemap');
spawnSync(
'node',
[
sentryCliPath,
'releases',
'files',
version,
'upload-sourcemaps',
'--strip-prefix',
path.join(process.cwd(), outputFolder),
path.join(outputFolder, bundleName),
path.join(outputFolder, `${bundleName}.map`),
],
{
stdio: 'inherit',
},
);
}
}
const ignorePackingFileNames = ['.', '..', 'index.bundlejs.map'];
const ignorePackingExtensions = ['DS_Store'];
async function pack(dir: string, output: string) {
console.log('Packing');
fs.ensureDirSync(path.dirname(output));
await new Promise<void>((resolve, reject) => {
const zipfile = new ZipFile();
function addDirectory(root: string, rel: string) {
if (rel) {
zipfile.addEmptyDirectory(rel);
}
const childs = fs.readdirSync(root);
for (const name of childs) {
if (
ignorePackingFileNames.includes(name) ||
ignorePackingExtensions.some((ext) => name.endsWith(`.${ext}`))
) {
continue;
}
const fullPath = path.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: any) => reject(err));
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', () => {
resolve();
});
zipfile.end();
});
console.log(`ppk热更包已生成并保存到: ${output}`);
}
export function readEntire(entry: string, zipFile: ZipFile) {
const buffers: Buffer[] = [];
return new Promise((resolve, reject) => {
zipFile.openReadStream(entry, (err: any, stream: any) => {
stream.pipe({
write(chunk: Buffer) {
buffers.push(chunk);
},
end() {
resolve(Buffer.concat(buffers));
},
prependListener() {},
on() {},
once() {},
emit() {},
});
});
});
}
function basename(fn: string) {
const m = /^(.+\/)[^\/]+\/?$/.exec(fn);
return m?.[1];
}
async function diffFromPPK(origin: string, next: string, output: string) {
fs.ensureDirSync(path.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' ||
entry.fileName === 'bundle.harmony.js'
) {
// 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 ZipFile();
const writePromise = new Promise((resolve, reject) => {
zipfile.outputStream.on('error', (err) => {
throw err;
});
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', () => {
resolve();
});
});
const addedEntry = {};
function addEntry(fn: string) {
//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 (entry.fileName === 'bundle.harmony.js') {
//console.log('Found bundle');
return readEntire(entry, nextZipfile).then((newSource) => {
//console.log('Begin diff');
zipfile.addBuffer(
diff(originSource, newSource),
'bundle.harmony.js.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, (err, readStream) => {
if (err) {
return reject(err);
}
zipfile.addReadStream(readStream, entry.fileName);
readStream.on('end', () => {
//console.log('add finished');
resolve();
});
});
});
}
});
const deletes = {};
for (const 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: string,
next: string,
output: string,
originBundleName: string,
transformPackagePath = (v: string) => v,
) {
fs.ensureDirSync(path.dirname(output));
const originEntries = {};
const originMap = {};
let originSource;
await enumZipEntries(origin, (entry: any, zipFile: any) => {
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 ZipFile();
const writePromise = new Promise((resolve, reject) => {
zipfile.outputStream.on('error', (err) => {
throw err;
});
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', () => {
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 (entry.fileName === 'bundle.harmony.js') {
//console.log('Found bundle');
return readEntire(entry, nextZipfile).then((newSource) => {
//console.log('Begin diff');
zipfile.addBuffer(
diff(originSource, newSource),
'bundle.harmony.js.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, (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;
}
export async function enumZipEntries(
zipFn: string,
callback: (entry: any, zipFile: any) => void,
nestedPath = '',
) {
return new Promise((resolve, reject) => {
openZipFile(
zipFn,
{ lazyEntries: true },
async (err: any, zipfile: ZipFile) => {
if (err) {
return reject(err);
}
zipfile.on('end', resolve);
zipfile.on('error', reject);
zipfile.on('entry', async (entry) => {
const fullPath = nestedPath + entry.fileName;
try {
if (
!entry.fileName.endsWith('/') &&
entry.fileName.toLowerCase().endsWith('.hap')
) {
const tempDir = path.join(
os.tmpdir(),
`nested_zip_${Date.now()}`,
);
await fs.ensureDir(tempDir);
const tempZipPath = path.join(tempDir, 'temp.zip');
await new Promise((res, rej) => {
zipfile.openReadStream(entry, async (err, readStream) => {
if (err) return rej(err);
const writeStream = fs.createWriteStream(tempZipPath);
readStream.pipe(writeStream);
writeStream.on('finish', res);
writeStream.on('error', rej);
});
});
await enumZipEntries(tempZipPath, callback, `${fullPath}/`);
await fs.remove(tempDir);
}
const result = callback(entry, zipfile, fullPath);
if (result && typeof result.then === 'function') {
await result;
}
} catch (error) {
console.error('处理文件时出错:', error);
}
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()}`),
};
}
export const commands = {
bundle: async function ({ options }) {
const platform = checkPlatform(
options.platform || (await question('平台(ios/android/harmony):')),
);
const {
bundleName,
entryFile,
intermediaDir,
output,
dev,
sourcemap,
taro,
expo,
rncli,
disableHermes,
} = translateOptions({
...options,
platform,
});
const bundleParams = await checkPlugins();
const sourcemapPlugin = bundleParams.sourcemap;
const isSentry = bundleParams.sentry;
const sourcemapOutput = path.join(intermediaDir, `${bundleName}.map`);
const realOutput = output.replace(/\$\{time\}/g, `${Date.now()}`);
if (!platform) {
throw new Error('Platform must be specified.');
}
console.log(`Bundling with react-native: ${depVersions['react-native']}`);
await runReactNativeBundleCommand({
bundleName,
dev,
entryFile,
outputFolder: intermediaDir,
platform,
sourcemapOutput: sourcemap || sourcemapPlugin ? sourcemapOutput : '',
disableHermes,
cli: {
taro,
expo,
rncli,
},
});
await pack(path.resolve(intermediaDir), realOutput);
const v = await question('是否现在上传此热更包?(Y/N)');
if (v.toLowerCase() === 'y') {
const versionName = await this.publish({
args: [realOutput],
options: {
platform,
},
});
if (isSentry) {
await copyDebugidForSentry(bundleName, intermediaDir, sourcemapOutput);
await uploadSourcemapForSentry(
bundleName,
intermediaDir,
sourcemapOutput,
versionName,
);
}
}
},
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 hdiffFromApp({ args, options }) {
const { origin, next, realOutput } = diffArgsCheck(
args,
options,
'hdiffFromApp',
);
await diffFromPackage(
origin,
next,
realOutput,
'resources/rawfile/bundle.harmony.js',
);
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?.[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?.[1];
});
console.log(`${realOutput} generated.`);
},
};