react-native-update-cli 2.4.1 → 2.5.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/cli.json +14 -0
- package/lib/bundle.js +1 -7
- package/lib/locales/en.js +3 -0
- package/lib/locales/zh.js +3 -0
- package/lib/package.js +7 -0
- package/lib/utils/app-info-parser/aab.js +266 -0
- package/lib/utils/app-info-parser/apk.js +1 -1
- package/lib/utils/app-info-parser/index.js +7 -2
- package/lib/utils/index.js +137 -0
- package/lib/versions.js +22 -0
- package/package.json +3 -2
- package/proto/Configuration.proto +183 -0
- package/proto/Resources.proto +569 -0
- package/src/bundle.ts +2 -9
- package/src/locales/en.ts +3 -0
- package/src/locales/zh.ts +3 -0
- package/src/package.ts +12 -3
- package/src/utils/app-info-parser/aab.js +326 -0
- package/src/utils/app-info-parser/apk.js +1 -1
- package/src/utils/app-info-parser/index.ts +6 -2
- package/src/utils/index.ts +154 -0
- package/src/versions.ts +26 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
const Zip = require('./zip');
|
|
2
|
+
const yazl = require('yazl');
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { open: openZipFile } = require('yauzl');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
class AabParser extends Zip {
|
|
9
|
+
/**
|
|
10
|
+
* parser for parsing .aab file
|
|
11
|
+
* @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser
|
|
12
|
+
*/
|
|
13
|
+
constructor(file) {
|
|
14
|
+
super(file);
|
|
15
|
+
if (!(this instanceof AabParser)) {
|
|
16
|
+
return new AabParser(file);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 从 AAB 提取通用 APK
|
|
22
|
+
* 这个方法会合并 base/ 和所有 split/ 目录的内容
|
|
23
|
+
*
|
|
24
|
+
* @param {String} outputPath - 输出 APK 文件路径
|
|
25
|
+
* @param {Object} options - 选项
|
|
26
|
+
* @param {Boolean} options.includeAllSplits - 是否包含所有 split APK(默认 false,只提取 base)
|
|
27
|
+
* @param {Array<String>} options.splits - 指定要包含的 split APK 名称(如果指定,则只包含这些)
|
|
28
|
+
* @returns {Promise<String>} 返回输出文件路径
|
|
29
|
+
*/
|
|
30
|
+
async extractApk(outputPath, options = {}) {
|
|
31
|
+
const { includeAllSplits = false, splits = null } = options;
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
if (typeof this.file !== 'string') {
|
|
35
|
+
return reject(
|
|
36
|
+
new Error('AAB file path must be a string in Node.js environment'),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
openZipFile(this.file, { lazyEntries: true }, async (err, zipfile) => {
|
|
41
|
+
if (err) {
|
|
42
|
+
return reject(err);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// 1. 收集所有条目及其数据
|
|
47
|
+
const baseEntries = [];
|
|
48
|
+
const splitEntries = [];
|
|
49
|
+
const metaInfEntries = [];
|
|
50
|
+
let pendingReads = 0;
|
|
51
|
+
let hasError = false;
|
|
52
|
+
|
|
53
|
+
const processEntry = (entry, fileName) => {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
zipfile.openReadStream(entry, (err, readStream) => {
|
|
56
|
+
if (err) {
|
|
57
|
+
return reject(err);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const chunks = [];
|
|
61
|
+
readStream.on('data', (chunk) => chunks.push(chunk));
|
|
62
|
+
readStream.on('end', () => {
|
|
63
|
+
const buffer = Buffer.concat(chunks);
|
|
64
|
+
resolve(buffer);
|
|
65
|
+
});
|
|
66
|
+
readStream.on('error', reject);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
zipfile.on('entry', async (entry) => {
|
|
72
|
+
const fileName = entry.fileName;
|
|
73
|
+
|
|
74
|
+
// 跳过目录
|
|
75
|
+
if (fileName.endsWith('/')) {
|
|
76
|
+
zipfile.readEntry();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pendingReads++;
|
|
81
|
+
try {
|
|
82
|
+
const buffer = await processEntry(entry, fileName);
|
|
83
|
+
|
|
84
|
+
if (fileName.startsWith('base/')) {
|
|
85
|
+
// 将 base/manifest/AndroidManifest.xml 转换为 androidmanifest.xml(APK 中通常是小写)
|
|
86
|
+
// 将 base/resources.arsc 转换为 resources.arsc
|
|
87
|
+
let apkPath = fileName.replace(/^base\//, '');
|
|
88
|
+
if (apkPath === 'manifest/AndroidManifest.xml') {
|
|
89
|
+
apkPath = 'androidmanifest.xml';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
baseEntries.push({
|
|
93
|
+
buffer,
|
|
94
|
+
zipPath: fileName,
|
|
95
|
+
apkPath,
|
|
96
|
+
});
|
|
97
|
+
} else if (fileName.startsWith('split/')) {
|
|
98
|
+
splitEntries.push({
|
|
99
|
+
buffer,
|
|
100
|
+
zipPath: fileName,
|
|
101
|
+
});
|
|
102
|
+
} else if (fileName.startsWith('META-INF/')) {
|
|
103
|
+
metaInfEntries.push({
|
|
104
|
+
buffer,
|
|
105
|
+
zipPath: fileName,
|
|
106
|
+
apkPath: fileName,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// BundleConfig.pb 和其他文件不需要包含在 APK 中
|
|
110
|
+
|
|
111
|
+
pendingReads--;
|
|
112
|
+
zipfile.readEntry();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
pendingReads--;
|
|
115
|
+
if (!hasError) {
|
|
116
|
+
hasError = true;
|
|
117
|
+
reject(error);
|
|
118
|
+
}
|
|
119
|
+
zipfile.readEntry();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
zipfile.on('end', async () => {
|
|
124
|
+
// 等待所有读取完成
|
|
125
|
+
while (pendingReads > 0) {
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (hasError) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// 2. 创建新的 APK 文件
|
|
135
|
+
const zipFile = new yazl.ZipFile();
|
|
136
|
+
|
|
137
|
+
// 3. 添加 base 目录的所有文件
|
|
138
|
+
for (const { buffer, apkPath } of baseEntries) {
|
|
139
|
+
zipFile.addBuffer(buffer, apkPath);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 4. 添加 split APK 的内容(如果需要)
|
|
143
|
+
if (includeAllSplits || splits) {
|
|
144
|
+
const splitsToInclude = splits
|
|
145
|
+
? splitEntries.filter((se) =>
|
|
146
|
+
splits.some((s) => se.zipPath.includes(s)),
|
|
147
|
+
)
|
|
148
|
+
: splitEntries;
|
|
149
|
+
|
|
150
|
+
await this.mergeSplitApksFromBuffers(zipFile, splitsToInclude);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 5. 添加 META-INF(签名信息,虽然可能无效,但保留结构)
|
|
154
|
+
for (const { buffer, apkPath } of metaInfEntries) {
|
|
155
|
+
zipFile.addBuffer(buffer, apkPath);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 6. 写入文件
|
|
159
|
+
zipFile.outputStream
|
|
160
|
+
.pipe(fs.createWriteStream(outputPath))
|
|
161
|
+
.on('close', () => {
|
|
162
|
+
resolve(outputPath);
|
|
163
|
+
})
|
|
164
|
+
.on('error', (err) => {
|
|
165
|
+
reject(err);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
zipFile.end();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
reject(error);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
zipfile.on('error', reject);
|
|
175
|
+
zipfile.readEntry();
|
|
176
|
+
} catch (error) {
|
|
177
|
+
reject(error);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 合并 split APK 的内容(从已读取的 buffer)
|
|
185
|
+
*/
|
|
186
|
+
async mergeSplitApksFromBuffers(zipFile, splitEntries) {
|
|
187
|
+
for (const { buffer: splitBuffer } of splitEntries) {
|
|
188
|
+
if (splitBuffer) {
|
|
189
|
+
// 创建一个临时的 ZIP 文件来读取 split APK
|
|
190
|
+
const tempSplitPath = path.join(
|
|
191
|
+
os.tmpdir(),
|
|
192
|
+
`split_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.apk`,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await fs.writeFile(tempSplitPath, splitBuffer);
|
|
197
|
+
|
|
198
|
+
await new Promise((resolve, reject) => {
|
|
199
|
+
openZipFile(
|
|
200
|
+
tempSplitPath,
|
|
201
|
+
{ lazyEntries: true },
|
|
202
|
+
async (err, splitZipfile) => {
|
|
203
|
+
if (err) {
|
|
204
|
+
return reject(err);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
splitZipfile.on('entry', (splitEntry) => {
|
|
208
|
+
// 跳过 META-INF,因为签名信息不需要合并
|
|
209
|
+
if (splitEntry.fileName.startsWith('META-INF/')) {
|
|
210
|
+
splitZipfile.readEntry();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
splitZipfile.openReadStream(splitEntry, (err, readStream) => {
|
|
215
|
+
if (err) {
|
|
216
|
+
splitZipfile.readEntry();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const chunks = [];
|
|
221
|
+
readStream.on('data', (chunk) => chunks.push(chunk));
|
|
222
|
+
readStream.on('end', () => {
|
|
223
|
+
const buffer = Buffer.concat(chunks);
|
|
224
|
+
// 注意:如果文件已存在(在 base 中),split 中的会覆盖 base 中的
|
|
225
|
+
zipFile.addBuffer(buffer, splitEntry.fileName);
|
|
226
|
+
splitZipfile.readEntry();
|
|
227
|
+
});
|
|
228
|
+
readStream.on('error', () => {
|
|
229
|
+
splitZipfile.readEntry();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
splitZipfile.on('end', resolve);
|
|
235
|
+
splitZipfile.on('error', reject);
|
|
236
|
+
splitZipfile.readEntry();
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
} finally {
|
|
241
|
+
// 清理临时文件
|
|
242
|
+
await fs.remove(tempSplitPath).catch(() => {});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 解析 AAB 文件信息(类似 APK parser 的 parse 方法)
|
|
250
|
+
* 注意:AAB 中的 AndroidManifest.xml 在 base/manifest/AndroidManifest.xml
|
|
251
|
+
*/
|
|
252
|
+
async parse() {
|
|
253
|
+
// 尝试从 base/manifest/AndroidManifest.xml 读取 manifest
|
|
254
|
+
// 但 AAB 中的 manifest 可能是二进制格式,需要特殊处理
|
|
255
|
+
const manifestPath = 'base/manifest/AndroidManifest.xml';
|
|
256
|
+
const ResourceName = /^base\/resources\.arsc$/;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const manifestBuffer = await this.getEntry(
|
|
260
|
+
new RegExp(`^${manifestPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (!manifestBuffer) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
"AndroidManifest.xml can't be found in AAB base/manifest/",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let apkInfo = this._parseManifest(manifestBuffer);
|
|
270
|
+
|
|
271
|
+
// 尝试解析 resources.arsc
|
|
272
|
+
try {
|
|
273
|
+
const resourceBuffer = await this.getEntry(ResourceName);
|
|
274
|
+
if (resourceBuffer) {
|
|
275
|
+
const resourceMap = this._parseResourceMap(resourceBuffer);
|
|
276
|
+
const { mapInfoResource } = require('./utils');
|
|
277
|
+
apkInfo = mapInfoResource(apkInfo, resourceMap);
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
// resources.arsc 解析失败不影响基本信息
|
|
281
|
+
console.warn('[Warning] Failed to parse resources.arsc:', e.message);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return apkInfo;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
throw new Error(`Failed to parse AAB: ${error.message}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Parse manifest
|
|
292
|
+
* @param {Buffer} buffer // manifest file's buffer
|
|
293
|
+
*/
|
|
294
|
+
_parseManifest(buffer) {
|
|
295
|
+
try {
|
|
296
|
+
const ManifestXmlParser = require('./xml-parser/manifest');
|
|
297
|
+
const parser = new ManifestXmlParser(buffer, {
|
|
298
|
+
ignore: [
|
|
299
|
+
'application.activity',
|
|
300
|
+
'application.service',
|
|
301
|
+
'application.receiver',
|
|
302
|
+
'application.provider',
|
|
303
|
+
'permission-group',
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
return parser.parse();
|
|
307
|
+
} catch (e) {
|
|
308
|
+
throw new Error('Parse AndroidManifest.xml error: ' + e.message);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Parse resourceMap
|
|
314
|
+
* @param {Buffer} buffer // resourceMap file's buffer
|
|
315
|
+
*/
|
|
316
|
+
_parseResourceMap(buffer) {
|
|
317
|
+
try {
|
|
318
|
+
const ResourceFinder = require('./resource-finder');
|
|
319
|
+
return new ResourceFinder().processResourceTable(buffer);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
throw new Error('Parser resources.arsc error: ' + e.message);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = AabParser;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const ApkParser = require('./apk');
|
|
2
2
|
const IpaParser = require('./ipa');
|
|
3
3
|
const AppParser = require('./app');
|
|
4
|
-
const
|
|
4
|
+
const AabParser = require('./aab');
|
|
5
|
+
const supportFileTypes = ['ipa', 'apk', 'app', 'aab'];
|
|
5
6
|
|
|
6
7
|
class AppInfoParser {
|
|
7
8
|
file: string | File;
|
|
@@ -20,7 +21,7 @@ class AppInfoParser {
|
|
|
20
21
|
const fileType = splits[splits.length - 1].toLowerCase();
|
|
21
22
|
if (!supportFileTypes.includes(fileType)) {
|
|
22
23
|
throw new Error(
|
|
23
|
-
'Unsupported file type, only support .ipa
|
|
24
|
+
'Unsupported file type, only support .ipa, .apk, .app, or .aab file.',
|
|
24
25
|
);
|
|
25
26
|
}
|
|
26
27
|
this.file = file;
|
|
@@ -35,6 +36,9 @@ class AppInfoParser {
|
|
|
35
36
|
case 'app':
|
|
36
37
|
this.parser = new AppParser(this.file);
|
|
37
38
|
break;
|
|
39
|
+
case 'aab':
|
|
40
|
+
this.parser = new AabParser(this.file);
|
|
41
|
+
break;
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
parse() {
|
package/src/utils/index.ts
CHANGED
|
@@ -149,6 +149,160 @@ export async function getIpaInfo(fn: string) {
|
|
|
149
149
|
return { versionName, buildTime, ...appCredential };
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
export async function getAabInfo(fn: string) {
|
|
153
|
+
const protobuf = require('protobufjs');
|
|
154
|
+
const root = await protobuf.load(
|
|
155
|
+
path.join(__dirname, '../../proto/Resources.proto'),
|
|
156
|
+
);
|
|
157
|
+
const XmlNode = root.lookupType('aapt.pb.XmlNode');
|
|
158
|
+
|
|
159
|
+
const buffer = await readZipEntry(fn, 'base/manifest/AndroidManifest.xml');
|
|
160
|
+
|
|
161
|
+
const message = XmlNode.decode(buffer);
|
|
162
|
+
const object = XmlNode.toObject(message, {
|
|
163
|
+
enums: String,
|
|
164
|
+
longs: String,
|
|
165
|
+
bytes: String,
|
|
166
|
+
defaults: true,
|
|
167
|
+
arrays: true,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const manifestElement = object.element;
|
|
171
|
+
if (manifestElement.name !== 'manifest') {
|
|
172
|
+
throw new Error('Invalid manifest');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let versionName = '';
|
|
176
|
+
for (const attr of manifestElement.attribute) {
|
|
177
|
+
if (attr.name === 'versionName') {
|
|
178
|
+
versionName = attr.value;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let buildTime = 0;
|
|
183
|
+
const appCredential = {};
|
|
184
|
+
|
|
185
|
+
// Find application node
|
|
186
|
+
const applicationNode = manifestElement.child.find(
|
|
187
|
+
(c: any) => c.element && c.element.name === 'application',
|
|
188
|
+
);
|
|
189
|
+
if (applicationNode) {
|
|
190
|
+
const metaDataNodes = applicationNode.element.child.filter(
|
|
191
|
+
(c: any) => c.element && c.element.name === 'meta-data',
|
|
192
|
+
);
|
|
193
|
+
for (const meta of metaDataNodes) {
|
|
194
|
+
let name = '';
|
|
195
|
+
let value = '';
|
|
196
|
+
let resourceId = 0;
|
|
197
|
+
|
|
198
|
+
for (const attr of meta.element.attribute) {
|
|
199
|
+
if (attr.name === 'name') {
|
|
200
|
+
name = attr.value;
|
|
201
|
+
}
|
|
202
|
+
if (attr.name === 'value') {
|
|
203
|
+
value = attr.value;
|
|
204
|
+
if (attr.compiledItem?.ref?.id) {
|
|
205
|
+
resourceId = attr.compiledItem.ref.id;
|
|
206
|
+
} else if (attr.compiledItem?.prim?.intDecimalValue) {
|
|
207
|
+
value = attr.compiledItem.prim.intDecimalValue.toString();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (name === 'pushy_build_time') {
|
|
213
|
+
if (resourceId > 0) {
|
|
214
|
+
const resolvedValue = await resolveResource(fn, resourceId, root);
|
|
215
|
+
if (resolvedValue) {
|
|
216
|
+
value = resolvedValue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
buildTime = Number(value);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (buildTime === 0) {
|
|
225
|
+
throw new Error(t('buildTimeNotFound'));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { versionName, buildTime, ...appCredential };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function readZipEntry(fn: string, entryName: string): Promise<Buffer> {
|
|
232
|
+
const yauzl = require('yauzl');
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
yauzl.open(fn, { lazyEntries: true }, (err: any, zipfile: any) => {
|
|
235
|
+
if (err) return reject(err);
|
|
236
|
+
let found = false;
|
|
237
|
+
zipfile.readEntry();
|
|
238
|
+
zipfile.on('entry', (entry: any) => {
|
|
239
|
+
if (entry.fileName === entryName) {
|
|
240
|
+
found = true;
|
|
241
|
+
zipfile.openReadStream(entry, (err: any, readStream: any) => {
|
|
242
|
+
if (err) return reject(err);
|
|
243
|
+
const chunks: any[] = [];
|
|
244
|
+
readStream.on('data', (chunk: any) => chunks.push(chunk));
|
|
245
|
+
readStream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
246
|
+
readStream.on('error', reject);
|
|
247
|
+
});
|
|
248
|
+
} else {
|
|
249
|
+
zipfile.readEntry();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
zipfile.on('end', () => {
|
|
253
|
+
if (!found) reject(new Error(`${entryName} not found in AAB`));
|
|
254
|
+
});
|
|
255
|
+
zipfile.on('error', reject);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function resolveResource(
|
|
261
|
+
fn: string,
|
|
262
|
+
resourceId: number,
|
|
263
|
+
root: any,
|
|
264
|
+
): Promise<string | null> {
|
|
265
|
+
const pkgId = (resourceId >> 24) & 0xff;
|
|
266
|
+
const typeId = (resourceId >> 16) & 0xff;
|
|
267
|
+
const entryId = resourceId & 0xffff;
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const buffer = await readZipEntry(fn, 'base/resources.pb');
|
|
271
|
+
const ResourceTable = root.lookupType('aapt.pb.ResourceTable');
|
|
272
|
+
const message = ResourceTable.decode(buffer);
|
|
273
|
+
const object = ResourceTable.toObject(message, {
|
|
274
|
+
enums: String,
|
|
275
|
+
longs: String,
|
|
276
|
+
bytes: String,
|
|
277
|
+
defaults: true,
|
|
278
|
+
arrays: true,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Find package
|
|
282
|
+
const pkg = object.package.find((p: any) => p.packageId === pkgId);
|
|
283
|
+
if (!pkg) return null;
|
|
284
|
+
|
|
285
|
+
// Find type
|
|
286
|
+
const type = pkg.type.find((t: any) => t.typeId === typeId);
|
|
287
|
+
if (!type) return null;
|
|
288
|
+
|
|
289
|
+
// Find entry
|
|
290
|
+
const entry = type.entry.find((e: any) => e.entryId === entryId);
|
|
291
|
+
if (!entry) return null;
|
|
292
|
+
|
|
293
|
+
// Get value from configValue
|
|
294
|
+
if (entry.configValue && entry.configValue.length > 0) {
|
|
295
|
+
const val = entry.configValue[0].value;
|
|
296
|
+
if (val.item?.str) {
|
|
297
|
+
return val.item.str.value;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.warn('Failed to resolve resource:', e);
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
152
306
|
const localDir = path.resolve(os.homedir(), tempDir);
|
|
153
307
|
fs.ensureDirSync(localDir);
|
|
154
308
|
export function saveToLocal(originPath: string, destName: string) {
|
package/src/versions.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { get, getAllPackages, post, put, uploadFile } from './api';
|
|
1
|
+
import { get, getAllPackages, post, put, uploadFile, doDelete } from './api';
|
|
2
2
|
import { question, saveToLocal } from './utils';
|
|
3
3
|
import { t } from './utils/i18n';
|
|
4
4
|
|
|
@@ -343,4 +343,29 @@ export const versionCommands = {
|
|
|
343
343
|
await put(`/app/${appId}/version/${versionId}`, updateParams);
|
|
344
344
|
console.log(t('operationSuccess'));
|
|
345
345
|
},
|
|
346
|
+
deleteVersion: async ({
|
|
347
|
+
options,
|
|
348
|
+
}: {
|
|
349
|
+
options: VersionCommandOptions;
|
|
350
|
+
}) => {
|
|
351
|
+
let appId = options.appId;
|
|
352
|
+
if (!appId) {
|
|
353
|
+
const platform = await getPlatform(options.platform);
|
|
354
|
+
appId = (await getSelectedApp(platform)).appId;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let versionId = options.versionId;
|
|
358
|
+
if (!versionId) {
|
|
359
|
+
versionId = (await chooseVersion(appId as string)).id;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
await doDelete(`/app/${appId}/version/${versionId}`);
|
|
364
|
+
console.log(t('deleteVersionSuccess', { versionId }));
|
|
365
|
+
} catch (error: any) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
t('deleteVersionError', { versionId, error: error.message }),
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
},
|
|
346
371
|
};
|