react-native-update-cli 2.5.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/cli.json +26 -0
- package/lib/api.js +45 -12
- package/lib/locales/en.js +13 -1
- package/lib/locales/zh.js +13 -1
- package/lib/package.js +53 -6
- package/lib/provider.js +3 -0
- package/lib/utils/app-info-parser/aab.js +165 -201
- package/lib/utils/app-info-parser/apk.js +25 -27
- package/lib/utils/app-info-parser/app.js +10 -11
- package/lib/utils/app-info-parser/index.js +8 -8
- package/lib/utils/app-info-parser/ipa.js +16 -21
- package/lib/utils/app-info-parser/resource-finder.js +365 -305
- package/lib/utils/app-info-parser/utils.js +78 -63
- package/lib/utils/app-info-parser/xml-parser/binary.js +57 -51
- package/lib/utils/app-info-parser/xml-parser/manifest.js +47 -39
- package/lib/utils/app-info-parser/zip.js +21 -11
- package/lib/utils/http-helper.js +1 -1
- package/package.json +1 -1
- package/src/api.ts +45 -19
- package/src/locales/en.ts +17 -0
- package/src/locales/zh.ts +15 -0
- package/src/modules/version-module.ts +1 -1
- package/src/package.ts +102 -11
- package/src/provider.ts +3 -0
- package/src/utils/app-info-parser/aab.ts +240 -0
- package/src/utils/app-info-parser/{apk.js → apk.ts} +30 -41
- package/src/utils/app-info-parser/app.ts +3 -0
- package/src/utils/app-info-parser/index.ts +4 -4
- package/src/utils/app-info-parser/{ipa.js → ipa.ts} +17 -31
- package/src/utils/app-info-parser/resource-finder.ts +508 -0
- package/src/utils/app-info-parser/utils.ts +162 -0
- package/src/utils/app-info-parser/xml-parser/{binary.js → binary.ts} +69 -61
- package/src/utils/app-info-parser/xml-parser/{manifest.js → manifest.ts} +50 -51
- package/src/utils/app-info-parser/zip.ts +86 -0
- package/src/utils/dep-versions.ts +7 -4
- package/src/utils/http-helper.ts +1 -1
- package/src/utils/latest-version/index.ts +2 -1
- package/src/versions.ts +1 -1
- package/src/utils/app-info-parser/aab.js +0 -326
- package/src/utils/app-info-parser/app.js +0 -16
- package/src/utils/app-info-parser/resource-finder.js +0 -495
- package/src/utils/app-info-parser/utils.js +0 -172
- package/src/utils/app-info-parser/zip.js +0 -66
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "Zip", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return Zip;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _utils = require("./utils");
|
|
2
12
|
const Unzip = require('isomorphic-unzip');
|
|
3
|
-
|
|
4
|
-
const { enumZipEntries, readEntry } = require('../../bundle');
|
|
13
|
+
let bundleZipUtils;
|
|
5
14
|
class Zip {
|
|
6
15
|
/**
|
|
7
16
|
* get entries by regexps, the return format is: { <filename>: <Buffer|Blob> }
|
|
8
17
|
* @param {Array} regexps // regexps for matching files
|
|
9
18
|
* @param {String} type // return type, can be buffer or blob, default buffer
|
|
10
19
|
*/ getEntries(regexps, type = 'buffer') {
|
|
11
|
-
|
|
20
|
+
const decoded = regexps.map((regex)=>(0, _utils.decodeNullUnicode)(regex));
|
|
12
21
|
return new Promise((resolve, reject)=>{
|
|
13
|
-
this.unzip.getBuffer(
|
|
22
|
+
this.unzip.getBuffer(decoded, {
|
|
14
23
|
type
|
|
15
24
|
}, (err, buffers)=>{
|
|
16
25
|
err ? reject(err) : resolve(buffers);
|
|
@@ -22,24 +31,26 @@ class Zip {
|
|
|
22
31
|
* @param {Regex} regex // regex for matching file
|
|
23
32
|
* @param {String} type // return type, can be buffer or blob, default buffer
|
|
24
33
|
*/ getEntry(regex, type = 'buffer') {
|
|
25
|
-
|
|
34
|
+
const decoded = (0, _utils.decodeNullUnicode)(regex);
|
|
26
35
|
return new Promise((resolve, reject)=>{
|
|
27
36
|
this.unzip.getBuffer([
|
|
28
|
-
|
|
37
|
+
decoded
|
|
29
38
|
], {
|
|
30
39
|
type
|
|
31
40
|
}, (err, buffers)=>{
|
|
32
|
-
|
|
33
|
-
err ? reject(err) : resolve(buffers[regex]);
|
|
41
|
+
err ? reject(err) : resolve(buffers[decoded]);
|
|
34
42
|
});
|
|
35
43
|
});
|
|
36
44
|
}
|
|
37
45
|
async getEntryFromHarmonyApp(regex) {
|
|
38
46
|
try {
|
|
47
|
+
const { enumZipEntries, readEntry } = bundleZipUtils != null ? bundleZipUtils : bundleZipUtils = require('../../bundle');
|
|
39
48
|
let originSource;
|
|
40
49
|
await enumZipEntries(this.file, (entry, zipFile)=>{
|
|
41
50
|
if (regex.test(entry.fileName)) {
|
|
42
|
-
return readEntry(entry, zipFile).then((
|
|
51
|
+
return readEntry(entry, zipFile).then((value)=>{
|
|
52
|
+
originSource = value;
|
|
53
|
+
});
|
|
43
54
|
}
|
|
44
55
|
});
|
|
45
56
|
return originSource;
|
|
@@ -48,7 +59,7 @@ class Zip {
|
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
constructor(file){
|
|
51
|
-
if (isBrowser()) {
|
|
62
|
+
if ((0, _utils.isBrowser)()) {
|
|
52
63
|
if (!(file instanceof window.Blob || typeof file.size !== 'undefined')) {
|
|
53
64
|
throw new Error('Param error: [file] must be an instance of Blob or File in browser.');
|
|
54
65
|
}
|
|
@@ -62,4 +73,3 @@ class Zip {
|
|
|
62
73
|
this.unzip = new Unzip(this.file);
|
|
63
74
|
}
|
|
64
75
|
}
|
|
65
|
-
module.exports = Zip;
|
package/lib/utils/http-helper.js
CHANGED
|
@@ -22,8 +22,8 @@ _export(exports, {
|
|
|
22
22
|
return testUrls;
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
|
-
const _constants = require("./constants");
|
|
26
25
|
const _nodefetch = /*#__PURE__*/ _interop_require_default(require("node-fetch"));
|
|
26
|
+
const _constants = require("./constants");
|
|
27
27
|
function _interop_require_default(obj) {
|
|
28
28
|
return obj && obj.__esModule ? obj : {
|
|
29
29
|
default: obj
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -6,30 +6,44 @@ import FormData from 'form-data';
|
|
|
6
6
|
import fetch from 'node-fetch';
|
|
7
7
|
import ProgressBar from 'progress';
|
|
8
8
|
import tcpp from 'tcp-ping';
|
|
9
|
+
import { getBaseUrl } from 'utils/http-helper';
|
|
9
10
|
import packageJson from '../package.json';
|
|
10
11
|
import type { Package, Session } from './types';
|
|
11
|
-
import {
|
|
12
|
-
credentialFile,
|
|
13
|
-
pricingPageUrl,
|
|
14
|
-
} from './utils/constants';
|
|
12
|
+
import { credentialFile, pricingPageUrl, IS_CRESC } from './utils/constants';
|
|
15
13
|
import { t } from './utils/i18n';
|
|
16
|
-
import { getBaseUrl } from 'utils/http-helper';
|
|
17
14
|
|
|
18
15
|
const tcpPing = util.promisify(tcpp.ping);
|
|
19
16
|
|
|
20
17
|
let session: Session | undefined;
|
|
21
18
|
let savedSession: Session | undefined;
|
|
22
|
-
|
|
19
|
+
let apiToken: string | undefined;
|
|
23
20
|
|
|
24
21
|
const userAgent = `react-native-update-cli/${packageJson.version}`;
|
|
25
22
|
|
|
26
23
|
export const getSession = () => session;
|
|
27
24
|
|
|
25
|
+
export const getApiToken = () => apiToken;
|
|
26
|
+
|
|
27
|
+
export const setApiToken = (token: string) => {
|
|
28
|
+
apiToken = token;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const loadApiTokenFromEnv = () => {
|
|
32
|
+
// Use CRESC_API_TOKEN for cresc, PUSHY_API_TOKEN for pushy
|
|
33
|
+
const envToken = IS_CRESC
|
|
34
|
+
? process.env.CRESC_API_TOKEN
|
|
35
|
+
: process.env.PUSHY_API_TOKEN;
|
|
36
|
+
if (envToken) {
|
|
37
|
+
apiToken = envToken;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
28
41
|
export const replaceSession = (newSession: { token: string }) => {
|
|
29
42
|
session = newSession;
|
|
30
43
|
};
|
|
31
44
|
|
|
32
45
|
export const loadSession = async () => {
|
|
46
|
+
loadApiTokenFromEnv();
|
|
33
47
|
if (fs.existsSync(credentialFile)) {
|
|
34
48
|
try {
|
|
35
49
|
replaceSession(JSON.parse(fs.readFileSync(credentialFile, 'utf8')));
|
|
@@ -82,27 +96,39 @@ async function query(url: string, options: fetch.RequestInit) {
|
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
function queryWithoutBody(method: string) {
|
|
85
|
-
return (api: string) =>
|
|
86
|
-
|
|
99
|
+
return (api: string) => {
|
|
100
|
+
const headers: Record<string, string> = {
|
|
101
|
+
'User-Agent': userAgent,
|
|
102
|
+
};
|
|
103
|
+
if (apiToken) {
|
|
104
|
+
headers['x-api-token'] = apiToken;
|
|
105
|
+
} else if (session?.token) {
|
|
106
|
+
headers['X-AccessToken'] = session.token;
|
|
107
|
+
}
|
|
108
|
+
return query(api, {
|
|
87
109
|
method,
|
|
88
|
-
headers
|
|
89
|
-
'User-Agent': userAgent,
|
|
90
|
-
'X-AccessToken': session ? session.token : '',
|
|
91
|
-
},
|
|
110
|
+
headers,
|
|
92
111
|
});
|
|
112
|
+
};
|
|
93
113
|
}
|
|
94
114
|
|
|
95
115
|
function queryWithBody(method: string) {
|
|
96
|
-
return (api: string, body?: Record<string, any>) =>
|
|
97
|
-
|
|
116
|
+
return (api: string, body?: Record<string, any>) => {
|
|
117
|
+
const headers: Record<string, string> = {
|
|
118
|
+
'User-Agent': userAgent,
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
};
|
|
121
|
+
if (apiToken) {
|
|
122
|
+
headers['x-api-token'] = apiToken;
|
|
123
|
+
} else if (session?.token) {
|
|
124
|
+
headers['X-AccessToken'] = session.token;
|
|
125
|
+
}
|
|
126
|
+
return query(api, {
|
|
98
127
|
method,
|
|
99
|
-
headers
|
|
100
|
-
'User-Agent': userAgent,
|
|
101
|
-
'Content-Type': 'application/json',
|
|
102
|
-
'X-AccessToken': session ? session.token : '',
|
|
103
|
-
},
|
|
128
|
+
headers,
|
|
104
129
|
body: JSON.stringify(body),
|
|
105
130
|
});
|
|
131
|
+
};
|
|
106
132
|
}
|
|
107
133
|
|
|
108
134
|
export const get = queryWithoutBody('GET');
|
package/src/locales/en.ts
CHANGED
|
@@ -2,6 +2,18 @@ export default {
|
|
|
2
2
|
addedToGitignore: 'Added {{line}} to .gitignore',
|
|
3
3
|
androidCrunchPngsWarning:
|
|
4
4
|
'The crunchPngs option of android seems not disabled (Please ignore this warning if already disabled), which may cause abnormal consumption of mobile network traffic. Please refer to https://cresc.dev/docs/getting-started#disable-crunchpngs-on-android \n',
|
|
5
|
+
aabOpenApksFailed: 'Failed to open generated .apks file',
|
|
6
|
+
aabReadUniversalApkFailed: 'Failed to read universal.apk',
|
|
7
|
+
aabUniversalApkNotFound: 'universal.apk not found in generated .apks',
|
|
8
|
+
aabBundletoolDownloadHint:
|
|
9
|
+
'bundletool not found. Downloading node-bundletool via npx (first run may take a while).',
|
|
10
|
+
aabManifestNotFound:
|
|
11
|
+
"AndroidManifest.xml can't be found in AAB base/manifest/",
|
|
12
|
+
aabParseResourcesWarning:
|
|
13
|
+
'[Warning] Failed to parse resources.arsc: {{error}}',
|
|
14
|
+
aabParseFailed: 'Failed to parse AAB: {{error}}',
|
|
15
|
+
aabParseManifestError: 'Parse AndroidManifest.xml error: {{error}}',
|
|
16
|
+
aabParseResourcesError: 'Parser resources.arsc error: {{error}}',
|
|
5
17
|
appId: 'App ID',
|
|
6
18
|
appIdMismatchApk:
|
|
7
19
|
'App ID mismatch! Current APK: {{appIdInPkg}}, current update.json: {{appId}}',
|
|
@@ -116,10 +128,14 @@ This can reduce the risk of inconsistent dependencies and supply chain attacks.
|
|
|
116
128
|
usageDiff: 'Usage: cresc {{command}} <origin> <next>',
|
|
117
129
|
usageParseApk: 'Usage: cresc parseApk <apk file>',
|
|
118
130
|
usageParseAab: 'Usage: cresc parseAab <aab file>',
|
|
131
|
+
usageExtractApk:
|
|
132
|
+
'Usage: cresc extractApk <aab file> [--output <apk file>] [--includeAllSplits] [--splits <split names>]',
|
|
119
133
|
usageParseApp: 'Usage: cresc parseApp <app file>',
|
|
120
134
|
usageParseIpa: 'Usage: cresc parseIpa <ipa file>',
|
|
121
135
|
usageUnderDevelopment: 'Usage is under development now.',
|
|
122
136
|
usageUploadApk: 'Usage: cresc uploadApk <apk file>',
|
|
137
|
+
usageUploadAab:
|
|
138
|
+
'Usage: cresc uploadAab <aab file> [--includeAllSplits] [--splits <split names>]',
|
|
123
139
|
usageUploadApp: 'Usage: cresc uploadApp <app file>',
|
|
124
140
|
usageUploadIpa: 'Usage: cresc uploadIpa <ipa file>',
|
|
125
141
|
versionBind:
|
|
@@ -147,4 +163,5 @@ This can reduce the risk of inconsistent dependencies and supply chain attacks.
|
|
|
147
163
|
'This function needs "node-bsdiff". Please run "{{scriptName}} install node-bsdiff" to install',
|
|
148
164
|
nodeHdiffpatchRequired:
|
|
149
165
|
'This function needs "node-hdiffpatch". Please run "{{scriptName}} install node-hdiffpatch" to install',
|
|
166
|
+
apkExtracted: 'APK extracted to {{output}}',
|
|
150
167
|
};
|
package/src/locales/zh.ts
CHANGED
|
@@ -2,6 +2,16 @@ export default {
|
|
|
2
2
|
addedToGitignore: '已将 {{line}} 添加到 .gitignore',
|
|
3
3
|
androidCrunchPngsWarning:
|
|
4
4
|
'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',
|
|
5
|
+
aabOpenApksFailed: '无法打开生成的 .apks 文件',
|
|
6
|
+
aabReadUniversalApkFailed: '无法读取 universal.apk',
|
|
7
|
+
aabUniversalApkNotFound: '在生成的 .apks 中未找到 universal.apk',
|
|
8
|
+
aabBundletoolDownloadHint:
|
|
9
|
+
'未找到 bundletool,正在通过 npx 下载 node-bundletool(首次下载可能需要一些时间)。',
|
|
10
|
+
aabManifestNotFound: '在 AAB 的 base/manifest/ 中找不到 AndroidManifest.xml',
|
|
11
|
+
aabParseResourcesWarning: '[警告] 解析 resources.arsc 失败:{{error}}',
|
|
12
|
+
aabParseFailed: '解析 AAB 失败:{{error}}',
|
|
13
|
+
aabParseManifestError: '解析 AndroidManifest.xml 出错:{{error}}',
|
|
14
|
+
aabParseResourcesError: '解析 resources.arsc 出错:{{error}}',
|
|
5
15
|
appId: '应用 id',
|
|
6
16
|
appIdMismatchApk:
|
|
7
17
|
'appId不匹配!当前apk: {{appIdInPkg}}, 当前update.json: {{appId}}',
|
|
@@ -110,9 +120,13 @@ export default {
|
|
|
110
120
|
usageDiff: '用法:pushy {{command}} <origin> <next>',
|
|
111
121
|
usageParseApk: '使用方法: pushy parseApk apk后缀文件',
|
|
112
122
|
usageParseAab: '使用方法: pushy parseAab aab后缀文件',
|
|
123
|
+
usageExtractApk:
|
|
124
|
+
'使用方法: pushy extractApk aab后缀文件 [--output apk文件] [--includeAllSplits] [--splits 分包名列表]',
|
|
113
125
|
usageParseApp: '使用方法: pushy parseApp app后缀文件',
|
|
114
126
|
usageParseIpa: '使用方法: pushy parseIpa ipa后缀文件',
|
|
115
127
|
usageUploadApk: '使用方法: pushy uploadApk apk后缀文件',
|
|
128
|
+
usageUploadAab:
|
|
129
|
+
'使用方法: pushy uploadAab aab后缀文件 [--includeAllSplits] [--splits 分包名列表]',
|
|
116
130
|
usageUploadApp: '使用方法: pushy uploadApp app后缀文件',
|
|
117
131
|
usageUploadIpa: '使用方法: pushy uploadIpa ipa后缀文件',
|
|
118
132
|
versionBind:
|
|
@@ -138,4 +152,5 @@ export default {
|
|
|
138
152
|
'此功能需要 "node-bsdiff"。请运行 "{{scriptName}} install node-bsdiff" 来安装',
|
|
139
153
|
nodeHdiffpatchRequired:
|
|
140
154
|
'此功能需要 "node-hdiffpatch"。请运行 "{{scriptName}} install node-hdiffpatch" 来安装',
|
|
155
|
+
apkExtracted: 'APK 已提取到 {{output}}',
|
|
141
156
|
};
|
package/src/package.ts
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import { getPlatform, getSelectedApp } from './app';
|
|
6
|
-
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
7
4
|
import Table from 'tty-table';
|
|
5
|
+
import { doDelete, getAllPackages, post, uploadFile } from './api';
|
|
6
|
+
import { getPlatform, getSelectedApp } from './app';
|
|
8
7
|
import type { Platform } from './types';
|
|
9
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getAabInfo,
|
|
10
|
+
getApkInfo,
|
|
11
|
+
getAppInfo,
|
|
12
|
+
getIpaInfo,
|
|
13
|
+
question,
|
|
14
|
+
saveToLocal,
|
|
15
|
+
} from './utils';
|
|
16
|
+
import { AabParser } from './utils/app-info-parser/aab';
|
|
10
17
|
import { depVersions } from './utils/dep-versions';
|
|
11
18
|
import { getCommitInfo } from './utils/git';
|
|
19
|
+
import { t } from './utils/i18n';
|
|
12
20
|
|
|
13
21
|
export async function listPackage(appId: string) {
|
|
14
|
-
const allPkgs = await getAllPackages(appId);
|
|
22
|
+
const allPkgs = (await getAllPackages(appId)) || [];
|
|
15
23
|
|
|
16
24
|
const header = [
|
|
17
25
|
{ value: t('nativePackageId') },
|
|
@@ -49,7 +57,7 @@ export async function choosePackage(appId: string) {
|
|
|
49
57
|
|
|
50
58
|
while (true) {
|
|
51
59
|
const id = await question(t('enterNativePackageId'));
|
|
52
|
-
const app = list
|
|
60
|
+
const app = list?.find((v) => v.id.toString() === id);
|
|
53
61
|
if (app) {
|
|
54
62
|
return app;
|
|
55
63
|
}
|
|
@@ -143,6 +151,48 @@ export const packageCommands = {
|
|
|
143
151
|
saveToLocal(fn, `${appId}/package/${id}.apk`);
|
|
144
152
|
console.log(t('apkUploadSuccess', { id, version: versionName, buildTime }));
|
|
145
153
|
},
|
|
154
|
+
uploadAab: async ({
|
|
155
|
+
args,
|
|
156
|
+
options,
|
|
157
|
+
}: {
|
|
158
|
+
args: string[];
|
|
159
|
+
options: Record<string, any>;
|
|
160
|
+
}) => {
|
|
161
|
+
const source = args[0];
|
|
162
|
+
if (!source || !source.endsWith('.aab')) {
|
|
163
|
+
throw new Error(t('usageUploadAab'));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const output = path.join(
|
|
167
|
+
os.tmpdir(),
|
|
168
|
+
`${path.basename(source, path.extname(source))}-${Date.now()}.apk`,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const includeAllSplits =
|
|
172
|
+
options.includeAllSplits === true || options.includeAllSplits === 'true';
|
|
173
|
+
const splits = options.splits
|
|
174
|
+
? String(options.splits)
|
|
175
|
+
.split(',')
|
|
176
|
+
.map((item) => item.trim())
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
: null;
|
|
179
|
+
|
|
180
|
+
const parser = new AabParser(source);
|
|
181
|
+
try {
|
|
182
|
+
await parser.extractApk(output, {
|
|
183
|
+
includeAllSplits,
|
|
184
|
+
splits,
|
|
185
|
+
});
|
|
186
|
+
await packageCommands.uploadApk({
|
|
187
|
+
args: [output],
|
|
188
|
+
options,
|
|
189
|
+
});
|
|
190
|
+
} finally {
|
|
191
|
+
if (await fs.pathExists(output)) {
|
|
192
|
+
await fs.remove(output);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
146
196
|
uploadApp: async ({
|
|
147
197
|
args,
|
|
148
198
|
options,
|
|
@@ -214,6 +264,42 @@ export const packageCommands = {
|
|
|
214
264
|
}
|
|
215
265
|
console.log(await getAabInfo(fn));
|
|
216
266
|
},
|
|
267
|
+
extractApk: async ({
|
|
268
|
+
args,
|
|
269
|
+
options,
|
|
270
|
+
}: {
|
|
271
|
+
args: string[];
|
|
272
|
+
options: Record<string, any>;
|
|
273
|
+
}) => {
|
|
274
|
+
const source = args[0];
|
|
275
|
+
if (!source || !source.endsWith('.aab')) {
|
|
276
|
+
throw new Error(t('usageExtractApk'));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const output =
|
|
280
|
+
options.output ||
|
|
281
|
+
path.join(
|
|
282
|
+
path.dirname(source),
|
|
283
|
+
`${path.basename(source, path.extname(source))}.apk`,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const includeAllSplits =
|
|
287
|
+
options.includeAllSplits === true || options.includeAllSplits === 'true';
|
|
288
|
+
const splits = options.splits
|
|
289
|
+
? String(options.splits)
|
|
290
|
+
.split(',')
|
|
291
|
+
.map((item) => item.trim())
|
|
292
|
+
.filter(Boolean)
|
|
293
|
+
: null;
|
|
294
|
+
|
|
295
|
+
const parser = new AabParser(source);
|
|
296
|
+
await parser.extractApk(output, {
|
|
297
|
+
includeAllSplits,
|
|
298
|
+
splits,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
console.log(t('apkExtracted', { output }));
|
|
302
|
+
},
|
|
217
303
|
packages: async ({ options }: { options: { platform: Platform } }) => {
|
|
218
304
|
const platform = await getPlatform(options.platform);
|
|
219
305
|
const { appId } = await getSelectedApp(platform);
|
|
@@ -224,12 +310,17 @@ export const packageCommands = {
|
|
|
224
310
|
options,
|
|
225
311
|
}: {
|
|
226
312
|
args: string[];
|
|
227
|
-
options: {
|
|
313
|
+
options: {
|
|
314
|
+
appId?: string;
|
|
315
|
+
packageId?: string;
|
|
316
|
+
packageVersion?: string;
|
|
317
|
+
platform?: Platform;
|
|
318
|
+
};
|
|
228
319
|
}) => {
|
|
229
320
|
let { appId, packageId, packageVersion } = options;
|
|
230
321
|
|
|
231
322
|
if (!appId) {
|
|
232
|
-
const platform = await getPlatform();
|
|
323
|
+
const platform = await getPlatform(options.platform);
|
|
233
324
|
appId = (await getSelectedApp(platform)).appId as string;
|
|
234
325
|
}
|
|
235
326
|
|
package/src/provider.ts
CHANGED
|
@@ -122,6 +122,9 @@ export class CLIProviderImpl implements CLIProvider {
|
|
|
122
122
|
case 'apk':
|
|
123
123
|
await packageCommands.uploadApk(context);
|
|
124
124
|
break;
|
|
125
|
+
case 'aab':
|
|
126
|
+
await packageCommands.uploadAab(context);
|
|
127
|
+
break;
|
|
125
128
|
case 'app':
|
|
126
129
|
await packageCommands.uploadApp(context);
|
|
127
130
|
break;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import { open as openZipFile } from 'yauzl';
|
|
6
|
+
import { t } from '../i18n';
|
|
7
|
+
import { ResourceFinder } from './resource-finder';
|
|
8
|
+
import { mapInfoResource } from './utils';
|
|
9
|
+
import { ManifestParser } from './xml-parser/manifest';
|
|
10
|
+
import { Zip } from './zip';
|
|
11
|
+
|
|
12
|
+
export class AabParser extends Zip {
|
|
13
|
+
file: string | File;
|
|
14
|
+
|
|
15
|
+
constructor(file: string | File) {
|
|
16
|
+
super(file);
|
|
17
|
+
this.file = file;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async extractApk(
|
|
21
|
+
outputPath: string,
|
|
22
|
+
{
|
|
23
|
+
includeAllSplits,
|
|
24
|
+
splits,
|
|
25
|
+
}: { includeAllSplits?: boolean; splits?: string[] | null },
|
|
26
|
+
) {
|
|
27
|
+
const normalizedSplits = Array.isArray(splits)
|
|
28
|
+
? splits.map((item) => item.trim()).filter(Boolean)
|
|
29
|
+
: [];
|
|
30
|
+
const modules = includeAllSplits
|
|
31
|
+
? null
|
|
32
|
+
: Array.from(new Set(['base', ...normalizedSplits]));
|
|
33
|
+
const modulesArgs = modules ? [`--modules=${modules.join(',')}`] : [];
|
|
34
|
+
|
|
35
|
+
const runCommand = (
|
|
36
|
+
command: string,
|
|
37
|
+
args: string[],
|
|
38
|
+
options: { stdio?: 'inherit'; env?: NodeJS.ProcessEnv } = {},
|
|
39
|
+
) =>
|
|
40
|
+
new Promise<void>((resolve, reject) => {
|
|
41
|
+
const inheritStdio = options.stdio === 'inherit';
|
|
42
|
+
const child = spawn(command, args, {
|
|
43
|
+
stdio: inheritStdio ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
env: options.env,
|
|
45
|
+
});
|
|
46
|
+
let stderr = '';
|
|
47
|
+
if (!inheritStdio) {
|
|
48
|
+
child.stderr?.on('data', (chunk) => {
|
|
49
|
+
stderr += chunk.toString();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
child.on('error', reject);
|
|
53
|
+
child.on('close', (code) => {
|
|
54
|
+
if (code === 0) {
|
|
55
|
+
resolve();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
reject(
|
|
59
|
+
new Error(
|
|
60
|
+
stderr.trim() || `Command failed: ${command} (code ${code})`,
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Create a temp file for the .apks output
|
|
67
|
+
const tempDir = os.tmpdir();
|
|
68
|
+
const tempApksPath = path.join(tempDir, `temp-${Date.now()}.apks`);
|
|
69
|
+
|
|
70
|
+
const needsNpxDownload = async () => {
|
|
71
|
+
try {
|
|
72
|
+
await runCommand('npx', [
|
|
73
|
+
'--no-install',
|
|
74
|
+
'node-bundletool',
|
|
75
|
+
'--version',
|
|
76
|
+
]);
|
|
77
|
+
return false;
|
|
78
|
+
} catch {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// 1. Build APKS (universal mode)
|
|
85
|
+
// We assume bundletool is in the path.
|
|
86
|
+
// User might need keystore to sign it properly but for simple extraction we stick to default debug key if possible or unsigned?
|
|
87
|
+
// actually bundletool build-apks signs with debug key by default if no keystore provided.
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await runCommand('bundletool', [
|
|
91
|
+
'build-apks',
|
|
92
|
+
'--mode=universal',
|
|
93
|
+
`--bundle=${this.file}`,
|
|
94
|
+
`--output=${tempApksPath}`,
|
|
95
|
+
'--overwrite',
|
|
96
|
+
...modulesArgs,
|
|
97
|
+
]);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// Fallback to npx node-bundletool if bundletool is not in PATH
|
|
100
|
+
// We use -y to avoid interactive prompt for installation
|
|
101
|
+
if (await needsNpxDownload()) {
|
|
102
|
+
console.log(t('aabBundletoolDownloadHint'));
|
|
103
|
+
}
|
|
104
|
+
await runCommand(
|
|
105
|
+
'npx',
|
|
106
|
+
[
|
|
107
|
+
'-y',
|
|
108
|
+
'node-bundletool',
|
|
109
|
+
'build-apks',
|
|
110
|
+
'--mode=universal',
|
|
111
|
+
`--bundle=${this.file}`,
|
|
112
|
+
`--output=${tempApksPath}`,
|
|
113
|
+
'--overwrite',
|
|
114
|
+
...modulesArgs,
|
|
115
|
+
],
|
|
116
|
+
{
|
|
117
|
+
stdio: 'inherit',
|
|
118
|
+
env: {
|
|
119
|
+
...process.env,
|
|
120
|
+
npm_config_progress: 'true',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 2. Extract universal.apk from the .apks (zip) file
|
|
127
|
+
await new Promise<void>((resolve, reject) => {
|
|
128
|
+
openZipFile(tempApksPath, { lazyEntries: true }, (err, zipfile) => {
|
|
129
|
+
if (err || !zipfile) {
|
|
130
|
+
reject(err || new Error(t('aabOpenApksFailed')));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let found = false;
|
|
135
|
+
zipfile.readEntry();
|
|
136
|
+
zipfile.on('entry', (entry) => {
|
|
137
|
+
if (entry.fileName === 'universal.apk') {
|
|
138
|
+
found = true;
|
|
139
|
+
zipfile.openReadStream(entry, (err, readStream) => {
|
|
140
|
+
if (err || !readStream) {
|
|
141
|
+
reject(err || new Error(t('aabReadUniversalApkFailed')));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const writeStream = fs.createWriteStream(outputPath);
|
|
145
|
+
readStream.pipe(writeStream);
|
|
146
|
+
writeStream.on('close', () => {
|
|
147
|
+
zipfile.close();
|
|
148
|
+
resolve();
|
|
149
|
+
});
|
|
150
|
+
writeStream.on('error', reject);
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
zipfile.readEntry();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
zipfile.on('end', () => {
|
|
158
|
+
if (!found) reject(new Error(t('aabUniversalApkNotFound')));
|
|
159
|
+
});
|
|
160
|
+
zipfile.on('error', reject);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
} finally {
|
|
164
|
+
// Cleanup
|
|
165
|
+
if (await fs.pathExists(tempApksPath)) {
|
|
166
|
+
await fs.remove(tempApksPath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 解析 AAB 文件信息(类似 APK parser 的 parse 方法)
|
|
173
|
+
* 注意:AAB 中的 AndroidManifest.xml 在 base/manifest/AndroidManifest.xml
|
|
174
|
+
*/
|
|
175
|
+
async parse() {
|
|
176
|
+
const manifestPath = 'base/manifest/AndroidManifest.xml';
|
|
177
|
+
const ResourceName = /^base\/resources\.arsc$/;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const manifestBuffer = await this.getEntry(
|
|
181
|
+
new RegExp(`^${escapeRegExp(manifestPath)}$`),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (!manifestBuffer) {
|
|
185
|
+
throw new Error(t('aabManifestNotFound'));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let apkInfo = this._parseManifest(manifestBuffer as Buffer);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const resourceBuffer = await this.getEntry(ResourceName);
|
|
192
|
+
if (resourceBuffer) {
|
|
193
|
+
const resourceMap = this._parseResourceMap(resourceBuffer as Buffer);
|
|
194
|
+
apkInfo = mapInfoResource(apkInfo, resourceMap);
|
|
195
|
+
}
|
|
196
|
+
} catch (e: any) {
|
|
197
|
+
console.warn(t('aabParseResourcesWarning', { error: e?.message ?? e }));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return apkInfo;
|
|
201
|
+
} catch (error: any) {
|
|
202
|
+
throw new Error(t('aabParseFailed', { error: error.message ?? error }));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Parse manifest
|
|
207
|
+
* @param {Buffer} buffer // manifest file's buffer
|
|
208
|
+
*/
|
|
209
|
+
private _parseManifest(buffer: Buffer) {
|
|
210
|
+
try {
|
|
211
|
+
const parser = new ManifestParser(buffer, {
|
|
212
|
+
ignore: [
|
|
213
|
+
'application.activity',
|
|
214
|
+
'application.service',
|
|
215
|
+
'application.receiver',
|
|
216
|
+
'application.provider',
|
|
217
|
+
'permission-group',
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
return parser.parse();
|
|
221
|
+
} catch (e: any) {
|
|
222
|
+
throw new Error(t('aabParseManifestError', { error: e?.message ?? e }));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Parse resourceMap
|
|
228
|
+
* @param {Buffer} buffer // resourceMap file's buffer
|
|
229
|
+
*/
|
|
230
|
+
private _parseResourceMap(buffer: Buffer) {
|
|
231
|
+
try {
|
|
232
|
+
return new ResourceFinder().processResourceTable(buffer);
|
|
233
|
+
} catch (e: any) {
|
|
234
|
+
throw new Error(t('aabParseResourcesError', { error: e?.message ?? e }));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const escapeRegExp = (value: string) =>
|
|
240
|
+
value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|