1
0
mirror of https://gitcode.com/github-mirrors/react-native-update-cli.git synced 2025-09-17 18:06:10 +08:00
Code Issues Packages Projects Releases Wiki Activity GitHub Gitee

Compare commits

..

9 Commits

Author SHA1 Message Date
Sunny Luo
814a9d10fb Update package.json 2025-02-13 17:17:31 +08:00
sunnylqm
c08c5c0b07 fix taro cli path 2025-02-13 16:13:48 +08:00
sunnylqm
dc8c134ff0 v1.40.0-beta.0 2025-02-13 16:07:25 +08:00
sunnylqm
1d1e6cde0f support taro 2025-02-13 16:02:04 +08:00
sunny.luo
f16aff5674 Improve file filtering during bundle packing
# Conflicts:
#	package.json
#	src/bundle.js
2025-02-10 17:22:00 +08:00
Sunny Luo
d7da311c5e Update package.json 2025-02-09 16:15:04 +08:00
波仔糕
abef760f43 add logic to remove soucemap and merge sourcemap params (#9)
* add logic to remove soucemap and merge sourcemap params

* Update bundle.js

---------

Co-authored-by: Sunny Luo <sunnylqm@gmail.com>
2025-02-09 16:14:45 +08:00
Sunny Luo
467ef0c60d Update package.json 2025-01-23 23:15:22 +08:00
波仔糕
0b08c7760d Support sentry (#8)
* add logic to support sentry

* udpate

* change reference path

* support git commits and version info

* udate

* add try catch for require.resolve

* update upload sourcemap workflow
2025-01-23 22:02:13 +08:00
7 changed files with 396 additions and 119 deletions

View File

@@ -145,6 +145,15 @@
},
"sourcemap": {
"default": false
},
"taro": {
"default": false
},
"expo": {
"default": false
},
"rncli": {
"default": false
}
}
},

View File

@@ -1,6 +1,6 @@
{
"name": "react-native-update-cli",
"version": "1.38.2",
"version": "1.40.0",
"description": "Command tools for javaScript updater with `pushy` service for react native apps.",
"main": "index.js",
"bin": {

View File

@@ -3,7 +3,7 @@ import { getRNVersion, translateOptions } from './utils';
import * as fs from 'fs-extra';
import { ZipFile } from 'yazl';
import { open as openZipFile } from 'yauzl';
import { question, printVersionCommand } from './utils';
import { question, checkPlugins } from './utils';
import { checkPlatform } from './app';
import { spawn, spawnSync } from 'node:child_process';
import semverSatisfies from 'semver/functions/satisfies';
@@ -22,16 +22,33 @@ try {
hdiff = require('node-hdiffpatch').diff;
} catch (e) {}
async function runReactNativeBundleCommand(
async function runReactNativeBundleCommand({
bundleName,
development,
dev,
entryFile,
outputFolder,
platform,
sourcemapOutput,
config,
) {
let gradleConfig = {};
cli,
}: {
bundleName: string;
dev: string;
entryFile: string;
outputFolder: string;
platform: string;
sourcemapOutput: string;
config?: string;
cli: {
taro?: boolean;
expo?: boolean;
rncli?: boolean;
};
}) {
let gradleConfig: {
crunchPngs?: boolean;
enableHermes?: boolean;
} = {};
if (platform === 'android') {
gradleConfig = await checkGradleConfig();
if (gradleConfig.crunchPngs !== false) {
@@ -41,7 +58,7 @@ async function runReactNativeBundleCommand(
}
}
const reactNativeBundleArgs = [];
const reactNativeBundleArgs: string[] = [];
const envArgs = process.env.PUSHY_ENV_ARGS;
@@ -54,26 +71,31 @@ async function runReactNativeBundleCommand(
fs.emptyDirSync(outputFolder);
let cliPath;
let cliPath: string | undefined;
let usingExpo = false;
try {
cliPath = require.resolve('@expo/cli', {
paths: [process.cwd()],
});
const expoCliVersion = JSON.parse(
fs.readFileSync(
require.resolve('@expo/cli/package.json', {
paths: [process.cwd()],
}),
),
).version;
// expo cli 0.10.17 (expo 49) 开始支持 bundle:embed
if (semverSatisfies(expoCliVersion, '>= 0.10.17')) {
usingExpo = true;
}
} catch (e) {}
if (!usingExpo) {
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()],
}),
),
).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', {
@@ -85,18 +107,49 @@ async function runReactNativeBundleCommand(
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();
}
const bundleCommand = usingExpo
? 'export:embed'
: platform === 'harmony'
? 'bundle-harmony'
: 'bundle';
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',
development,
dev,
'--entry-file',
entryFile,
]);
@@ -116,14 +169,24 @@ async function runReactNativeBundleCommand(
outputFolder,
'--bundle-output',
path.join(outputFolder, bundleName),
'--dev',
development,
'--entry-file',
entryFile,
'--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);
@@ -156,17 +219,19 @@ async function runReactNativeBundleCommand(
),
);
} else {
let hermesEnabled = false;
let hermesEnabled: boolean | undefined = false;
if (platform === 'android') {
const gradlePropeties = await new Promise((resolve) => {
const gradlePropeties = await new Promise<{
hermesEnabled?: boolean;
}>((resolve) => {
properties.parse(
'./android/gradle.properties',
{ path: true },
(error, props) => {
(error: any, props: { hermesEnabled?: boolean }) => {
if (error) {
console.error(error);
resolve(null);
resolve({});
}
resolve(props);
@@ -190,6 +255,7 @@ async function runReactNativeBundleCommand(
bundleName,
outputFolder,
sourcemapOutput,
!isSentry,
);
}
resolve(null);
@@ -198,7 +264,7 @@ async function runReactNativeBundleCommand(
});
}
async function copyHarmonyBundle(outputFolder) {
async function copyHarmonyBundle(outputFolder: string) {
const harmonyRawPath = 'harmony/entry/src/main/resources/rawfile';
try {
await fs.ensureDir(harmonyRawPath);
@@ -212,7 +278,7 @@ async function copyHarmonyBundle(outputFolder) {
await fs.ensureDir(outputFolder);
await fs.copy(harmonyRawPath, outputFolder);
} catch (error) {
} catch (error: any) {
console.error('copyHarmonyBundle 错误:', error);
throw new Error(`复制文件失败: ${error.message}`);
}
@@ -250,9 +316,10 @@ async function checkGradleConfig() {
}
async function compileHermesByteCode(
bundleName,
outputFolder,
sourcemapOutput,
bundleName: string,
outputFolder: string,
sourcemapOutput: string,
shouldCleanSourcemap: boolean,
) {
console.log('Hermes enabled, now compiling to hermes bytecode:\n');
// >= rn 0.69
@@ -309,22 +376,120 @@ async function compileHermesByteCode(
},
);
}
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 pack(dir, output) {
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((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
const zipfile = new ZipFile();
function addDirectory(root, rel) {
function addDirectory(root: string, rel: string) {
if (rel) {
zipfile.addEmptyDirectory(rel);
}
const childs = fs.readdirSync(root);
for (const name of childs) {
if (name === '.' || name === '..' || name === 'index.bundlejs.map') {
if (
ignorePackingFileNames.includes(name) ||
ignorePackingExtensions.some((ext) => name.endsWith(`.${ext}`))
) {
continue;
}
const fullPath = path.join(root, name);
@@ -341,7 +506,7 @@ async function pack(dir, output) {
addDirectory(dir, '');
zipfile.outputStream.on('error', (err) => reject(err));
zipfile.outputStream.on('error', (err: any) => reject(err));
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', () => {
resolve();
});
@@ -350,12 +515,12 @@ async function pack(dir, output) {
console.log(`ppk热更包已生成并保存到: ${output}`);
}
export function readEntire(entry, zipFile) {
const buffers = [];
export function readEntire(entry: string, zipFile: ZipFile) {
const buffers: Buffer[] = [];
return new Promise((resolve, reject) => {
zipFile.openReadStream(entry, (err, stream) => {
zipFile.openReadStream(entry, (err: any, stream: any) => {
stream.pipe({
write(chunk) {
write(chunk: Buffer) {
buffers.push(chunk);
},
end() {
@@ -370,12 +535,12 @@ export function readEntire(entry, zipFile) {
});
}
function basename(fn) {
function basename(fn: string) {
const m = /^(.+\/)[^\/]+\/?$/.exec(fn);
return m?.[1];
}
async function diffFromPPK(origin, next, output) {
async function diffFromPPK(origin: string, next: string, output: string) {
fs.ensureDirSync(path.dirname(output));
const originEntries = {};
@@ -420,7 +585,7 @@ async function diffFromPPK(origin, next, output) {
const addedEntry = {};
function addEntry(fn) {
function addEntry(fn: string) {
//console.log(fn);
if (!fn || addedEntry[fn]) {
return;
@@ -517,11 +682,11 @@ async function diffFromPPK(origin, next, output) {
}
async function diffFromPackage(
origin,
next,
output,
originBundleName,
transformPackagePath = (v) => v,
origin: string,
next: string,
output: string,
originBundleName: string,
transformPackagePath = (v: string) => v,
) {
fs.ensureDirSync(path.dirname(output));
@@ -530,7 +695,7 @@ async function diffFromPackage(
let originSource;
await enumZipEntries(origin, (entry, zipFile) => {
await enumZipEntries(origin, (entry: any, zipFile: any) => {
if (!/\/$/.test(entry.fileName)) {
const fn = transformPackagePath(entry.fileName);
if (!fn) {
@@ -624,55 +789,66 @@ async function diffFromPackage(
await writePromise;
}
export async function enumZipEntries(zipFn, callback, nestedPath = '') {
export async function enumZipEntries(
zipFn: string,
callback: (entry: any, zipFile: any) => void,
nestedPath = '',
) {
return new Promise((resolve, reject) => {
openZipFile(zipFn, { lazyEntries: true }, async (err, 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);
openZipFile(
zipFn,
{ lazyEntries: true },
async (err: any, zipfile: ZipFile) => {
if (err) {
return reject(err);
}
zipfile.readEntry();
});
zipfile.on('end', resolve);
zipfile.on('error', reject);
zipfile.on('entry', async (entry) => {
const fullPath = nestedPath + entry.fileName;
zipfile.readEntry();
});
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();
},
);
});
}
@@ -718,11 +894,24 @@ export const commands = {
options.platform || (await question('平台(ios/android/harmony):')),
);
const { bundleName, entryFile, intermediaDir, output, dev, sourcemap } =
translateOptions({
...options,
platform,
});
const {
bundleName,
entryFile,
intermediaDir,
output,
dev,
sourcemap,
taro,
expo,
rncli,
} = translateOptions({
...options,
platform,
});
const bundleParams = await checkPlugins();
const sourcemapPlugin = bundleParams.sourcemap;
const isSentry = bundleParams.sentry;
const sourcemapOutput = path.join(intermediaDir, `${bundleName}.map`);
@@ -736,25 +925,39 @@ export const commands = {
console.log(`Bundling with react-native: ${version}`);
await runReactNativeBundleCommand(
await runReactNativeBundleCommand({
bundleName,
dev,
entryFile,
intermediaDir,
outputFolder: intermediaDir,
platform,
sourcemap ? sourcemapOutput : '',
);
sourcemapOutput: sourcemap || sourcemapPlugin ? sourcemapOutput : '',
cli: {
taro,
expo,
rncli,
},
});
await pack(path.resolve(intermediaDir), realOutput);
const v = await question('是否现在上传此热更包?(Y/N)');
if (v.toLowerCase() === 'y') {
await this.publish({
const versionName = await this.publish({
args: [realOutput],
options: {
platform,
},
});
if (isSentry) {
await copyDebugidForSentry(bundleName, intermediaDir, sourcemapOutput);
await uploadSourcemapForSentry(
bundleName,
intermediaDir,
sourcemapOutput,
versionName,
);
}
}
},

28
src/utils/check-plugin.ts Normal file
View File

@@ -0,0 +1,28 @@
import { plugins } from './plugin-config';
interface BundleParams {
sentry: boolean;
sourcemap: boolean;
[key: string]: any;
}
export async function checkPlugins(): Promise<BundleParams> {
const params: BundleParams = {
sentry: false,
sourcemap: false,
};
for (const plugin of plugins) {
try {
const isEnabled = await plugin.detect();
if (isEnabled && plugin.bundleParams) {
Object.assign(params, plugin.bundleParams);
console.log(`检测到 ${plugin.name} 插件,应用相应打包配置`);
}
} catch (err) {
console.warn(`检测 ${plugin.name} 插件时出错:`, err);
}
}
return params;
}

View File

@@ -6,6 +6,7 @@ import AppInfoParser from './app-info-parser';
import semverSatisfies from 'semver/functions/satisfies';
import chalk from 'chalk';
import latestVersion from '@badisi/latest-version';
import { checkPlugins } from './check-plugin';
import { read } from 'read';
@@ -225,3 +226,5 @@ export async function printVersionCommand() {
}
export const pricingPageUrl = 'https://pushy.reactnative.cn/pricing.html';
export { checkPlugins };

View File

@@ -0,0 +1,32 @@
import fs from 'fs-extra';
interface PluginConfig {
name: string;
bundleParams?: {
[key: string]: any;
};
detect: () => Promise<boolean>;
}
export const plugins: PluginConfig[] = [
{
name: 'sentry',
bundleParams: {
sentry: true,
sourcemap: true,
},
detect: async () => {
try {
await fs.access('ios/sentry.properties');
return true;
} catch {
try {
await fs.access('android/sentry.properties');
return true;
} catch {
return false;
}
}
}
}
];

View File

@@ -97,8 +97,9 @@ export const commands = {
const { hash } = await uploadFile(fn);
const versionName = name || (await question('输入版本名称: ')) || '(未命名)';
const { id } = await post(`/app/${appId}/version/create`, {
name: name || (await question('输入版本名称: ')) || '(未命名)',
name: versionName,
hash,
description: description || (await question('输入版本描述:')),
metaInfo: metaInfo || (await question('输入自定义的 meta info:')),
@@ -111,6 +112,7 @@ export const commands = {
if (v.toLowerCase() === 'y') {
await this.update({ args: [], options: { versionId: id, platform } });
}
return versionName;
},
versions: async ({ options }) => {
const platform = checkPlatform(