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

Compare commits

..

11 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
sunnylqm
b24b27d100 ts 2025-01-11 20:23:25 +08:00
sunnylqm
4f0784172f fix expo cli 2025-01-11 20:20:16 +08:00
12 changed files with 475 additions and 183 deletions

View File

@@ -8,7 +8,8 @@
"recommended": true, "recommended": true,
"suspicious": { "suspicious": {
"noExplicitAny": "off", "noExplicitAny": "off",
"noAssignInExpressions": "off" "noAssignInExpressions": "off",
"noDoubleEquals": "off"
}, },
"style": { "style": {
"noNonNullAssertion": "off" "noNonNullAssertion": "off"

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import fs from 'node:fs';
import Table from 'tty-table'; import Table from 'tty-table';
import { post, get, doDelete } from './api'; import { post, get, doDelete } from './api';
import type { Platform } from './types';
const validPlatforms = { const validPlatforms = {
ios: 1, ios: 1,
@@ -10,14 +11,14 @@ const validPlatforms = {
harmony: 1, harmony: 1,
}; };
export function checkPlatform(platform) { export function checkPlatform(platform: Platform) {
if (!validPlatforms[platform]) { if (!validPlatforms[platform]) {
throw new Error(`无法识别的平台 '${platform}'`); throw new Error(`无法识别的平台 '${platform}'`);
} }
return platform; return platform;
} }
export function getSelectedApp(platform) { export function getSelectedApp(platform: Platform) {
checkPlatform(platform); checkPlatform(platform);
if (!fs.existsSync('update.json')) { if (!fs.existsSync('update.json')) {
@@ -34,7 +35,7 @@ export function getSelectedApp(platform) {
return updateInfo[platform]; return updateInfo[platform];
} }
export async function listApp(platform) { export async function listApp(platform: Platform) {
const { data } = await get('/app/list'); const { data } = await get('/app/list');
const list = platform ? data.filter((v) => v.platform === platform) : data; const list = platform ? data.filter((v) => v.platform === platform) : data;
@@ -58,12 +59,12 @@ export async function listApp(platform) {
return list; return list;
} }
export async function chooseApp(platform) { export async function chooseApp(platform: Platform) {
const list = await listApp(platform); const list = await listApp(platform);
while (true) { while (true) {
const id = await question('输入应用 id:'); const id = await question('输入应用 id:');
const app = list.find((v) => v.id === (id | 0)); const app = list.find((v) => v.id === Number(id));
if (app) { if (app) {
return app; return app;
} }

View File

@@ -3,15 +3,17 @@ import { getRNVersion, translateOptions } from './utils';
import * as fs from 'fs-extra'; import * as fs from 'fs-extra';
import { ZipFile } from 'yazl'; import { ZipFile } from 'yazl';
import { open as openZipFile } from 'yauzl'; import { open as openZipFile } from 'yauzl';
import { question, printVersionCommand } from './utils'; import { question, checkPlugins } from './utils';
import { checkPlatform } from './app'; import { checkPlatform } from './app';
import { spawn, spawnSync } from 'node:child_process'; import { spawn, spawnSync } from 'node:child_process';
import semverSatisfies from 'semver/functions/satisfies';
const g2js = require('gradle-to-js/lib/parser'); const g2js = require('gradle-to-js/lib/parser');
import os from 'os'; import os from 'node:os';
const properties = require('properties'); const properties = require('properties');
const path = require('path');
let bsdiff, hdiff, diff; let bsdiff;
let hdiff;
let diff;
try { try {
bsdiff = require('node-bsdiff').diff; bsdiff = require('node-bsdiff').diff;
} catch (e) {} } catch (e) {}
@@ -20,16 +22,33 @@ try {
hdiff = require('node-hdiffpatch').diff; hdiff = require('node-hdiffpatch').diff;
} catch (e) {} } catch (e) {}
async function runReactNativeBundleCommand( async function runReactNativeBundleCommand({
bundleName, bundleName,
development, dev,
entryFile, entryFile,
outputFolder, outputFolder,
platform, platform,
sourcemapOutput, sourcemapOutput,
config, config,
) { cli,
let gradleConfig = {}; }: {
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') { if (platform === 'android') {
gradleConfig = await checkGradleConfig(); gradleConfig = await checkGradleConfig();
if (gradleConfig.crunchPngs !== false) { if (gradleConfig.crunchPngs !== false) {
@@ -39,9 +58,9 @@ async function runReactNativeBundleCommand(
} }
} }
let reactNativeBundleArgs = []; const reactNativeBundleArgs: string[] = [];
let envArgs = process.env.PUSHY_ENV_ARGS; const envArgs = process.env.PUSHY_ENV_ARGS;
if (envArgs) { if (envArgs) {
Array.prototype.push.apply( Array.prototype.push.apply(
@@ -52,15 +71,31 @@ async function runReactNativeBundleCommand(
fs.emptyDirSync(outputFolder); fs.emptyDirSync(outputFolder);
let cliPath; let cliPath: string | undefined;
let usingExpo = false; let usingExpo = false;
const getExpoCli = () => {
try { try {
cliPath = require.resolve('@expo/cli', { cliPath = require.resolve('@expo/cli', {
paths: [process.cwd()], 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; usingExpo = true;
} catch (e) { } else {
cliPath = undefined;
}
} catch (e) {}
};
const getRnCli = () => {
try { try {
// rn >= 0.75 // rn >= 0.75
cliPath = require.resolve('@react-native-community/cli/build/bin.js', { cliPath = require.resolve('@react-native-community/cli/build/bin.js', {
@@ -72,14 +107,49 @@ async function runReactNativeBundleCommand(
paths: [process.cwd()], 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 (platform == 'harmony') { 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, [ Array.prototype.push.apply(reactNativeBundleArgs, [
cliPath, cliPath,
bundleCommand, bundleCommand,
'--dev', '--dev',
development, dev,
'--entry-file', '--entry-file',
entryFile, entryFile,
]); ]);
@@ -91,8 +161,7 @@ async function runReactNativeBundleCommand(
if (config) { if (config) {
reactNativeBundleArgs.push('--config', config); reactNativeBundleArgs.push('--config', config);
} }
} } else {
else{
Array.prototype.push.apply(reactNativeBundleArgs, [ Array.prototype.push.apply(reactNativeBundleArgs, [
cliPath, cliPath,
bundleCommand, bundleCommand,
@@ -100,15 +169,25 @@ async function runReactNativeBundleCommand(
outputFolder, outputFolder,
'--bundle-output', '--bundle-output',
path.join(outputFolder, bundleName), path.join(outputFolder, bundleName),
'--dev',
development,
'--entry-file',
entryFile,
'--platform', '--platform',
platform, platform,
'--reset-cache', '--reset-cache',
]); ]);
if (cli.taro) {
reactNativeBundleArgs.push(...[
'--type',
'rn',
])
} else {
reactNativeBundleArgs.push(...[
'--dev',
dev,
'--entry-file',
entryFile,
])
}
if (sourcemapOutput) { if (sourcemapOutput) {
reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput); reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput);
} }
@@ -140,17 +219,19 @@ async function runReactNativeBundleCommand(
), ),
); );
} else { } else {
let hermesEnabled = false; let hermesEnabled: boolean | undefined = false;
if (platform === 'android') { if (platform === 'android') {
const gradlePropeties = await new Promise((resolve) => { const gradlePropeties = await new Promise<{
hermesEnabled?: boolean;
}>((resolve) => {
properties.parse( properties.parse(
'./android/gradle.properties', './android/gradle.properties',
{ path: true }, { path: true },
function (error, props) { (error: any, props: { hermesEnabled?: boolean }) => {
if (error) { if (error) {
console.error(error); console.error(error);
resolve(null); resolve({});
} }
resolve(props); resolve(props);
@@ -174,6 +255,7 @@ async function runReactNativeBundleCommand(
bundleName, bundleName,
outputFolder, outputFolder,
sourcemapOutput, sourcemapOutput,
!isSentry,
); );
} }
resolve(null); resolve(null);
@@ -182,7 +264,7 @@ async function runReactNativeBundleCommand(
}); });
} }
async function copyHarmonyBundle(outputFolder) { async function copyHarmonyBundle(outputFolder: string) {
const harmonyRawPath = 'harmony/entry/src/main/resources/rawfile'; const harmonyRawPath = 'harmony/entry/src/main/resources/rawfile';
try { try {
await fs.ensureDir(harmonyRawPath); await fs.ensureDir(harmonyRawPath);
@@ -196,7 +278,7 @@ async function copyHarmonyBundle(outputFolder) {
await fs.ensureDir(outputFolder); await fs.ensureDir(outputFolder);
await fs.copy(harmonyRawPath, outputFolder); await fs.copy(harmonyRawPath, outputFolder);
} catch (error) { } catch (error: any) {
console.error('copyHarmonyBundle 错误:', error); console.error('copyHarmonyBundle 错误:', error);
throw new Error(`复制文件失败: ${error.message}`); throw new Error(`复制文件失败: ${error.message}`);
} }
@@ -234,11 +316,12 @@ async function checkGradleConfig() {
} }
async function compileHermesByteCode( async function compileHermesByteCode(
bundleName, bundleName: string,
outputFolder, outputFolder: string,
sourcemapOutput, sourcemapOutput: string,
shouldCleanSourcemap: boolean,
) { ) {
console.log(`Hermes enabled, now compiling to hermes bytecode:\n`); console.log('Hermes enabled, now compiling to hermes bytecode:\n');
// >= rn 0.69 // >= rn 0.69
const rnDir = path.dirname( const rnDir = path.dirname(
require.resolve('react-native', { require.resolve('react-native', {
@@ -264,13 +347,11 @@ async function compileHermesByteCode(
if (sourcemapOutput) { if (sourcemapOutput) {
fs.copyFileSync( fs.copyFileSync(
sourcemapOutput, sourcemapOutput,
path.join(outputFolder, bundleName + '.txt.map'), path.join(outputFolder, `${bundleName}.txt.map`),
); );
args.push('-output-source-map'); args.push('-output-source-map');
} }
console.log( console.log(`Running hermesc: ${hermesCommand} ${args.join(' ')}`);
'Running hermesc: ' + hermesCommand + ' ' + args.join(' ') + '\n',
);
spawnSync(hermesCommand, args, { spawnSync(hermesCommand, args, {
stdio: 'ignore', stdio: 'ignore',
}); });
@@ -280,13 +361,13 @@ async function compileHermesByteCode(
if (!fs.existsSync(composerPath)) { if (!fs.existsSync(composerPath)) {
return; return;
} }
console.log(`Composing source map`); console.log('Composing source map');
spawnSync( spawnSync(
'node', 'node',
[ [
composerPath, composerPath,
path.join(outputFolder, bundleName + '.txt.map'), path.join(outputFolder, `${bundleName}.txt.map`),
path.join(outputFolder, bundleName + '.map'), path.join(outputFolder, `${bundleName}.map`),
'-o', '-o',
sourcemapOutput, sourcemapOutput,
], ],
@@ -295,22 +376,120 @@ async function compileHermesByteCode(
}, },
); );
} }
fs.removeSync(path.join(outputFolder, bundleName + '.txt.map')); if (shouldCleanSourcemap) {
fs.removeSync(path.join(outputFolder, `${bundleName}.txt.map`));
}
} }
async function pack(dir, output) { 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'); console.log('Packing');
fs.ensureDirSync(path.dirname(output)); fs.ensureDirSync(path.dirname(output));
await new Promise((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const zipfile = new ZipFile(); const zipfile = new ZipFile();
function addDirectory(root, rel) { function addDirectory(root: string, rel: string) {
if (rel) { if (rel) {
zipfile.addEmptyDirectory(rel); zipfile.addEmptyDirectory(rel);
} }
const childs = fs.readdirSync(root); const childs = fs.readdirSync(root);
for (const name of childs) { for (const name of childs) {
if (name === '.' || name === '..' || name === 'index.bundlejs.map') { if (
ignorePackingFileNames.includes(name) ||
ignorePackingExtensions.some((ext) => name.endsWith(`.${ext}`))
) {
continue; continue;
} }
const fullPath = path.join(root, name); const fullPath = path.join(root, name);
@@ -320,30 +499,28 @@ async function pack(dir, output) {
zipfile.addFile(fullPath, rel + name); zipfile.addFile(fullPath, rel + name);
} else if (stat.isDirectory()) { } else if (stat.isDirectory()) {
//console.log('adding: ' + rel+name+'/'); //console.log('adding: ' + rel+name+'/');
addDirectory(fullPath, rel + name + '/'); addDirectory(fullPath, `${rel}${name}/`);
} }
} }
} }
addDirectory(dir, ''); addDirectory(dir, '');
zipfile.outputStream.on('error', (err) => reject(err)); zipfile.outputStream.on('error', (err: any) => reject(err));
zipfile.outputStream zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', () => {
.pipe(fs.createWriteStream(output))
.on('close', function () {
resolve(); resolve();
}); });
zipfile.end(); zipfile.end();
}); });
console.log('ppk热更包已生成并保存到: ' + output); console.log(`ppk热更包已生成并保存到: ${output}`);
} }
export function readEntire(entry, zipFile) { export function readEntire(entry: string, zipFile: ZipFile) {
const buffers = []; const buffers: Buffer[] = [];
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
zipFile.openReadStream(entry, (err, stream) => { zipFile.openReadStream(entry, (err: any, stream: any) => {
stream.pipe({ stream.pipe({
write(chunk) { write(chunk: Buffer) {
buffers.push(chunk); buffers.push(chunk);
}, },
end() { end() {
@@ -358,12 +535,12 @@ export function readEntire(entry, zipFile) {
}); });
} }
function basename(fn) { function basename(fn: string) {
const m = /^(.+\/)[^\/]+\/?$/.exec(fn); const m = /^(.+\/)[^\/]+\/?$/.exec(fn);
return m && m[1]; return m?.[1];
} }
async function diffFromPPK(origin, next, output) { async function diffFromPPK(origin: string, next: string, output: string) {
fs.ensureDirSync(path.dirname(output)); fs.ensureDirSync(path.dirname(output));
const originEntries = {}; const originEntries = {};
@@ -377,7 +554,10 @@ async function diffFromPPK(origin, next, output) {
// isFile // isFile
originMap[entry.crc32] = entry.fileName; originMap[entry.crc32] = entry.fileName;
if (entry.fileName === 'index.bundlejs' || entry.fileName === 'bundle.harmony.js') { if (
entry.fileName === 'index.bundlejs' ||
entry.fileName === 'bundle.harmony.js'
) {
// This is source. // This is source.
return readEntire(entry, zipFile).then((v) => (originSource = v)); return readEntire(entry, zipFile).then((v) => (originSource = v));
} }
@@ -386,7 +566,7 @@ async function diffFromPPK(origin, next, output) {
if (!originSource) { if (!originSource) {
throw new Error( throw new Error(
`Bundle file not found! Please use default bundle file name and path.`, 'Bundle file not found! Please use default bundle file name and path.',
); );
} }
@@ -398,16 +578,14 @@ async function diffFromPPK(origin, next, output) {
zipfile.outputStream.on('error', (err) => { zipfile.outputStream.on('error', (err) => {
throw err; throw err;
}); });
zipfile.outputStream zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', () => {
.pipe(fs.createWriteStream(output))
.on('close', function () {
resolve(); resolve();
}); });
}); });
const addedEntry = {}; const addedEntry = {};
function addEntry(fn) { function addEntry(fn: string) {
//console.log(fn); //console.log(fn);
if (!fn || addedEntry[fn]) { if (!fn || addedEntry[fn]) {
return; return;
@@ -471,7 +649,7 @@ async function diffFromPPK(origin, next, output) {
addEntry(basename(entry.fileName)); addEntry(basename(entry.fileName));
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
nextZipfile.openReadStream(entry, function (err, readStream) { nextZipfile.openReadStream(entry, (err, readStream) => {
if (err) { if (err) {
return reject(err); return reject(err);
} }
@@ -487,9 +665,9 @@ async function diffFromPPK(origin, next, output) {
const deletes = {}; const deletes = {};
for (let k in originEntries) { for (const k in originEntries) {
if (!newEntries[k]) { if (!newEntries[k]) {
console.log('Delete ' + k); console.log(`Delete ${k}`);
deletes[k] = 1; deletes[k] = 1;
} }
} }
@@ -504,11 +682,11 @@ async function diffFromPPK(origin, next, output) {
} }
async function diffFromPackage( async function diffFromPackage(
origin, origin: string,
next, next: string,
output, output: string,
originBundleName, originBundleName: string,
transformPackagePath = (v) => v, transformPackagePath = (v: string) => v,
) { ) {
fs.ensureDirSync(path.dirname(output)); fs.ensureDirSync(path.dirname(output));
@@ -517,7 +695,7 @@ async function diffFromPackage(
let originSource; let originSource;
await enumZipEntries(origin, (entry, zipFile) => { await enumZipEntries(origin, (entry: any, zipFile: any) => {
if (!/\/$/.test(entry.fileName)) { if (!/\/$/.test(entry.fileName)) {
const fn = transformPackagePath(entry.fileName); const fn = transformPackagePath(entry.fileName);
if (!fn) { if (!fn) {
@@ -538,7 +716,7 @@ async function diffFromPackage(
if (!originSource) { if (!originSource) {
throw new Error( throw new Error(
`Bundle file not found! Please use default bundle file name and path.`, 'Bundle file not found! Please use default bundle file name and path.',
); );
} }
@@ -550,9 +728,7 @@ async function diffFromPackage(
zipfile.outputStream.on('error', (err) => { zipfile.outputStream.on('error', (err) => {
throw err; throw err;
}); });
zipfile.outputStream zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', () => {
.pipe(fs.createWriteStream(output))
.on('close', function () {
resolve(); resolve();
}); });
}); });
@@ -594,7 +770,7 @@ async function diffFromPackage(
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
nextZipfile.openReadStream(entry, function (err, readStream) { nextZipfile.openReadStream(entry, (err, readStream) => {
if (err) { if (err) {
return reject(err); return reject(err);
} }
@@ -613,9 +789,16 @@ async function diffFromPackage(
await writePromise; 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) => { return new Promise((resolve, reject) => {
openZipFile(zipFn, { lazyEntries: true }, async (err, zipfile) => { openZipFile(
zipFn,
{ lazyEntries: true },
async (err: any, zipfile: ZipFile) => {
if (err) { if (err) {
return reject(err); return reject(err);
} }
@@ -630,7 +813,10 @@ export async function enumZipEntries(zipFn, callback, nestedPath = '') {
!entry.fileName.endsWith('/') && !entry.fileName.endsWith('/') &&
entry.fileName.toLowerCase().endsWith('.hap') entry.fileName.toLowerCase().endsWith('.hap')
) { ) {
const tempDir = path.join(os.tmpdir(), 'nested_zip_' + Date.now()); const tempDir = path.join(
os.tmpdir(),
`nested_zip_${Date.now()}`,
);
await fs.ensureDir(tempDir); await fs.ensureDir(tempDir);
const tempZipPath = path.join(tempDir, 'temp.zip'); const tempZipPath = path.join(tempDir, 'temp.zip');
@@ -644,7 +830,7 @@ export async function enumZipEntries(zipFn, callback, nestedPath = '') {
}); });
}); });
await enumZipEntries(tempZipPath, callback, fullPath + '/'); await enumZipEntries(tempZipPath, callback, `${fullPath}/`);
await fs.remove(tempDir); await fs.remove(tempDir);
} }
@@ -661,7 +847,8 @@ export async function enumZipEntries(zipFn, callback, nestedPath = '') {
}); });
zipfile.readEntry(); zipfile.readEntry();
}); },
);
}); });
} }
@@ -697,7 +884,7 @@ function diffArgsCheck(args, options, diffFn) {
return { return {
origin, origin,
next, next,
realOutput: output.replace(/\$\{time\}/g, '' + Date.now()), realOutput: output.replace(/\$\{time\}/g, `${Date.now()}`),
}; };
} }
@@ -707,15 +894,28 @@ export const commands = {
options.platform || (await question('平台(ios/android/harmony):')), options.platform || (await question('平台(ios/android/harmony):')),
); );
let { bundleName, entryFile, intermediaDir, output, dev, sourcemap } = const {
translateOptions({ bundleName,
entryFile,
intermediaDir,
output,
dev,
sourcemap,
taro,
expo,
rncli,
} = translateOptions({
...options, ...options,
platform, platform,
}); });
const sourcemapOutput = path.join(intermediaDir, bundleName + '.map'); const bundleParams = await checkPlugins();
const sourcemapPlugin = bundleParams.sourcemap;
const isSentry = bundleParams.sentry;
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now()); const sourcemapOutput = path.join(intermediaDir, `${bundleName}.map`);
const realOutput = output.replace(/\$\{time\}/g, `${Date.now()}`);
if (!platform) { if (!platform) {
throw new Error('Platform must be specified.'); throw new Error('Platform must be specified.');
@@ -723,27 +923,41 @@ export const commands = {
const { version, major, minor } = getRNVersion(); const { version, major, minor } = getRNVersion();
console.log('Bundling with react-native: ', version); console.log(`Bundling with react-native: ${version}`);
await runReactNativeBundleCommand( await runReactNativeBundleCommand({
bundleName, bundleName,
dev, dev,
entryFile, entryFile,
intermediaDir, outputFolder: intermediaDir,
platform, platform,
sourcemap ? sourcemapOutput : '', sourcemapOutput: sourcemap || sourcemapPlugin ? sourcemapOutput : '',
); cli: {
taro,
expo,
rncli,
},
});
await pack(path.resolve(intermediaDir), realOutput); await pack(path.resolve(intermediaDir), realOutput);
const v = await question('是否现在上传此热更包?(Y/N)'); const v = await question('是否现在上传此热更包?(Y/N)');
if (v.toLowerCase() === 'y') { if (v.toLowerCase() === 'y') {
await this.publish({ const versionName = await this.publish({
args: [realOutput], args: [realOutput],
options: { options: {
platform, platform,
}, },
}); });
if (isSentry) {
await copyDebugidForSentry(bundleName, intermediaDir, sourcemapOutput);
await uploadSourcemapForSentry(
bundleName,
intermediaDir,
sourcemapOutput,
versionName,
);
}
} }
}, },
@@ -817,7 +1031,7 @@ export const commands = {
await diffFromPackage(origin, next, realOutput, 'main.jsbundle', (v) => { await diffFromPackage(origin, next, realOutput, 'main.jsbundle', (v) => {
const m = /^Payload\/[^/]+\/(.+)$/.exec(v); const m = /^Payload\/[^/]+\/(.+)$/.exec(v);
return m && m[1]; return m?.[1];
}); });
console.log(`${realOutput} generated.`); console.log(`${realOutput} generated.`);
@@ -832,7 +1046,7 @@ export const commands = {
await diffFromPackage(origin, next, realOutput, 'main.jsbundle', (v) => { await diffFromPackage(origin, next, realOutput, 'main.jsbundle', (v) => {
const m = /^Payload\/[^/]+\/(.+)$/.exec(v); const m = /^Payload\/[^/]+\/(.+)$/.exec(v);
return m && m[1]; return m?.[1];
}); });
console.log(`${realOutput} generated.`); console.log(`${realOutput} generated.`);

View File

@@ -6,7 +6,7 @@ import { checkPlatform, getSelectedApp } from './app';
import { getApkInfo, getIpaInfo, getAppInfo } from './utils'; import { getApkInfo, getIpaInfo, getAppInfo } from './utils';
import Table from 'tty-table'; import Table from 'tty-table';
export async function listPackage(appId) { export async function listPackage(appId: string) {
const { data } = await get(`/app/${appId}/package/list?limit=1000`); const { data } = await get(`/app/${appId}/package/list?limit=1000`);
const header = [{ value: '原生包 Id' }, { value: '原生版本' }]; const header = [{ value: '原生包 Id' }, { value: '原生版本' }];
@@ -35,12 +35,12 @@ export async function listPackage(appId) {
return data; return data;
} }
export async function choosePackage(appId) { export async function choosePackage(appId: string) {
const list = await listPackage(appId); const list = await listPackage(appId);
while (true) { while (true) {
const id = await question('输入原生包 id:'); const id = await question('输入原生包 id:');
const app = list.find((v) => v.id === (id | 0)); const app = list.find((v) => v.id === Number(id));
if (app) { if (app) {
return app; return app;
} }
@@ -48,7 +48,7 @@ export async function choosePackage(appId) {
} }
export const commands = { export const commands = {
uploadIpa: async function ({ args }) { uploadIpa: async ({ args }: { args: string[] }) => {
const fn = args[0]; const fn = args[0];
if (!fn || !fn.endsWith('.ipa')) { if (!fn || !fn.endsWith('.ipa')) {
throw new Error('使用方法: pushy uploadIpa ipa后缀文件'); throw new Error('使用方法: pushy uploadIpa ipa后缀文件');
@@ -85,7 +85,7 @@ export const commands = {
`已成功上传ipa原生包id: ${id}, version: ${versionName}, buildTime: ${buildTime}`, `已成功上传ipa原生包id: ${id}, version: ${versionName}, buildTime: ${buildTime}`,
); );
}, },
uploadApk: async function ({ args }) { uploadApk: async ({ args }) => {
const fn = args[0]; const fn = args[0];
if (!fn || !fn.endsWith('.apk')) { if (!fn || !fn.endsWith('.apk')) {
throw new Error('使用方法: pushy uploadApk apk后缀文件'); throw new Error('使用方法: pushy uploadApk apk后缀文件');
@@ -122,7 +122,7 @@ export const commands = {
`已成功上传apk原生包id: ${id}, version: ${versionName}, buildTime: ${buildTime}`, `已成功上传apk原生包id: ${id}, version: ${versionName}, buildTime: ${buildTime}`,
); );
}, },
uploadApp: async function ({ args }) { uploadApp: async ({ args }) => {
const fn = args[0]; const fn = args[0];
if (!fn || !fn.endsWith('.app')) { if (!fn || !fn.endsWith('.app')) {
throw new Error('使用方法: pushy uploadApp app后缀文件'); throw new Error('使用方法: pushy uploadApp app后缀文件');
@@ -135,7 +135,6 @@ export const commands = {
} = await getAppInfo(fn); } = await getAppInfo(fn);
const { appId, appKey } = await getSelectedApp('harmony'); const { appId, appKey } = await getSelectedApp('harmony');
if (appIdInPkg && appIdInPkg != appId) { if (appIdInPkg && appIdInPkg != appId) {
throw new Error( throw new Error(
`appId不匹配当前app: ${appIdInPkg}, 当前update.json: ${appId}`, `appId不匹配当前app: ${appIdInPkg}, 当前update.json: ${appId}`,
@@ -160,28 +159,28 @@ export const commands = {
`已成功上传app原生包id: ${id}, version: ${versionName}, buildTime: ${buildTime}`, `已成功上传app原生包id: ${id}, version: ${versionName}, buildTime: ${buildTime}`,
); );
}, },
parseApp: async function ({ args }) { parseApp: async ({ args }) => {
const fn = args[0]; const fn = args[0];
if (!fn || !fn.endsWith('.app')) { if (!fn || !fn.endsWith('.app')) {
throw new Error('使用方法: pushy parseApp app后缀文件'); throw new Error('使用方法: pushy parseApp app后缀文件');
} }
console.log(await getAppInfo(fn)); console.log(await getAppInfo(fn));
}, },
parseIpa: async function ({ args }) { parseIpa: async ({ args }) => {
const fn = args[0]; const fn = args[0];
if (!fn || !fn.endsWith('.ipa')) { if (!fn || !fn.endsWith('.ipa')) {
throw new Error('使用方法: pushy parseIpa ipa后缀文件'); throw new Error('使用方法: pushy parseIpa ipa后缀文件');
} }
console.log(await getIpaInfo(fn)); console.log(await getIpaInfo(fn));
}, },
parseApk: async function ({ args }) { parseApk: async ({ args }) => {
const fn = args[0]; const fn = args[0];
if (!fn || !fn.endsWith('.apk')) { if (!fn || !fn.endsWith('.apk')) {
throw new Error('使用方法: pushy parseApk apk后缀文件'); throw new Error('使用方法: pushy parseApk apk后缀文件');
} }
console.log(await getApkInfo(fn)); console.log(await getApkInfo(fn));
}, },
packages: async function ({ options }) { packages: async ({ options }) => {
const platform = checkPlatform( const platform = checkPlatform(
options.platform || (await question('平台(ios/android/harmony):')), options.platform || (await question('平台(ios/android/harmony):')),
); );

View File

@@ -6,3 +6,5 @@ declare global {
export interface Session { export interface Session {
token: string; token: string;
} }
export type Platform = 'ios' | 'android' | 'harmony';

View File

@@ -2,12 +2,12 @@ import { question } from './utils';
import { post, get, replaceSession, saveSession, closeSession } from './api'; import { post, get, replaceSession, saveSession, closeSession } from './api';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
function md5(str) { function md5(str: string) {
return crypto.createHash('md5').update(str).digest('hex'); return crypto.createHash('md5').update(str).digest('hex');
} }
export const commands = { export const commands = {
login: async ({ args }) => { login: async ({ args }: { args: string[] }) => {
const email = args[0] || (await question('email:')); const email = args[0] || (await question('email:'));
const pwd = args[1] || (await question('password:', true)); const pwd = args[1] || (await question('password:', true));
const { token, info } = await post('/user/login', { const { token, info } = await post('/user/login', {

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 semverSatisfies from 'semver/functions/satisfies';
import chalk from 'chalk'; import chalk from 'chalk';
import latestVersion from '@badisi/latest-version'; import latestVersion from '@badisi/latest-version';
import { checkPlugins } from './check-plugin';
import { read } from 'read'; import { read } from 'read';
@@ -22,12 +23,13 @@ export async function question(query, password) {
export function translateOptions(options) { export function translateOptions(options) {
const ret = {}; const ret = {};
for (let key in options) { for (const key in options) {
const v = options[key]; const v = options[key];
if (typeof v === 'string') { if (typeof v === 'string') {
ret[key] = v.replace(/\$\{(\w+)\}/g, function (v, n) { ret[key] = v.replace(
return options[n] || process.env[n] || v; /\$\{(\w+)\}/g,
}); (v, n) => options[n] || process.env[n] || v,
);
} else { } else {
ret[key] = v; ret[key] = v;
} }
@@ -124,7 +126,7 @@ export async function getAppInfo(fn) {
return { versionName, buildTime, ...appCredential }; return { versionName, buildTime, ...appCredential };
} }
export async function getIpaInfo(fn) { export async function getIpaInfo(fn: string) {
const appInfoParser = new AppInfoParser(fn); const appInfoParser = new AppInfoParser(fn);
const bundleFile = await appInfoParser.parser.getEntry( const bundleFile = await appInfoParser.parser.getEntry(
/payload\/.+?\.app\/main.jsbundle/, /payload\/.+?\.app\/main.jsbundle/,
@@ -217,10 +219,12 @@ export async function printVersionCommand() {
); );
} else if (semverSatisfies(pushyVersion, '10.0.0 - 10.17.0')) { } else if (semverSatisfies(pushyVersion, '10.0.0 - 10.17.0')) {
console.warn( console.warn(
`当前版本已不再支持,请升级到 v10 的最新小版本(代码无需改动,可直接热更): npm i react-native-update@10`, '当前版本已不再支持,请升级到 v10 的最新小版本(代码无需改动,可直接热更): npm i react-native-update@10',
); );
} }
} }
} }
export const pricingPageUrl = 'https://pushy.reactnative.cn/pricing.html'; 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 { hash } = await uploadFile(fn);
const versionName = name || (await question('输入版本名称: ')) || '(未命名)';
const { id } = await post(`/app/${appId}/version/create`, { const { id } = await post(`/app/${appId}/version/create`, {
name: name || (await question('输入版本名称: ')) || '(未命名)', name: versionName,
hash, hash,
description: description || (await question('输入版本描述:')), description: description || (await question('输入版本描述:')),
metaInfo: metaInfo || (await question('输入自定义的 meta info:')), metaInfo: metaInfo || (await question('输入自定义的 meta info:')),
@@ -111,6 +112,7 @@ export const commands = {
if (v.toLowerCase() === 'y') { if (v.toLowerCase() === 'y') {
await this.update({ args: [], options: { versionId: id, platform } }); await this.update({ args: [], options: { versionId: id, platform } });
} }
return versionName;
}, },
versions: async ({ options }) => { versions: async ({ options }) => {
const platform = checkPlatform( const platform = checkPlatform(