rn-update-cli 1.0.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/LICENSE +21 -0
- package/README.md +167 -0
- package/README.zh-CN.md +167 -0
- package/cli.json +525 -0
- package/lib/api.d.ts +56 -0
- package/lib/api.js +671 -0
- package/lib/app.d.ts +107 -0
- package/lib/app.js +329 -0
- package/lib/auto-update.d.ts +35 -0
- package/lib/auto-update.js +507 -0
- package/lib/bin.d.ts +2 -0
- package/lib/bin.js +133 -0
- package/lib/bundle-pack.d.ts +6 -0
- package/lib/bundle-pack.js +91 -0
- package/lib/bundle-runner.d.ts +168 -0
- package/lib/bundle-runner.js +1305 -0
- package/lib/bundle.d.ts +45 -0
- package/lib/bundle.js +226 -0
- package/lib/cache.d.ts +10 -0
- package/lib/cache.js +25 -0
- package/lib/commands.d.ts +11 -0
- package/lib/commands.js +72 -0
- package/lib/diff.d.ts +18 -0
- package/lib/diff.js +690 -0
- package/lib/exports.d.ts +11 -0
- package/lib/exports.js +20 -0
- package/lib/install.d.ts +9 -0
- package/lib/install.js +51 -0
- package/lib/locales/en.d.ts +187 -0
- package/lib/locales/en.js +194 -0
- package/lib/locales/zh.d.ts +187 -0
- package/lib/locales/zh.js +194 -0
- package/lib/native-package.d.ts +21 -0
- package/lib/native-package.js +298 -0
- package/lib/package.d.ts +68 -0
- package/lib/package.js +487 -0
- package/lib/provider.d.ts +24 -0
- package/lib/provider.js +244 -0
- package/lib/standalone-publish.d.ts +17 -0
- package/lib/standalone-publish.js +433 -0
- package/lib/symbolicate.d.ts +35 -0
- package/lib/symbolicate.js +130 -0
- package/lib/types.d.ts +152 -0
- package/lib/types.js +2 -0
- package/lib/user.d.ts +8 -0
- package/lib/user.js +50 -0
- package/lib/utils/add-gitignore.d.ts +6 -0
- package/lib/utils/add-gitignore.js +44 -0
- package/lib/utils/app-info-parser/aab.d.ts +27 -0
- package/lib/utils/app-info-parser/aab.js +256 -0
- package/lib/utils/app-info-parser/apk.d.ts +19 -0
- package/lib/utils/app-info-parser/apk.js +95 -0
- package/lib/utils/app-info-parser/app.d.ts +9 -0
- package/lib/utils/app-info-parser/app.js +15 -0
- package/lib/utils/app-info-parser/index.d.ts +21 -0
- package/lib/utils/app-info-parser/index.js +51 -0
- package/lib/utils/app-info-parser/ipa.d.ts +19 -0
- package/lib/utils/app-info-parser/ipa.js +101 -0
- package/lib/utils/app-info-parser/resource-finder.d.ts +49 -0
- package/lib/utils/app-info-parser/resource-finder.js +429 -0
- package/lib/utils/app-info-parser/utils.d.ts +36 -0
- package/lib/utils/app-info-parser/utils.js +140 -0
- package/lib/utils/app-info-parser/xml-parser/binary.d.ts +61 -0
- package/lib/utils/app-info-parser/xml-parser/binary.js +577 -0
- package/lib/utils/app-info-parser/xml-parser/manifest.d.ts +15 -0
- package/lib/utils/app-info-parser/xml-parser/manifest.js +207 -0
- package/lib/utils/app-info-parser/zip.d.ts +29 -0
- package/lib/utils/app-info-parser/zip.js +97 -0
- package/lib/utils/check-lockfile.d.ts +6 -0
- package/lib/utils/check-lockfile.js +93 -0
- package/lib/utils/check-plugin.d.ts +12 -0
- package/lib/utils/check-plugin.js +36 -0
- package/lib/utils/command-result.d.ts +8 -0
- package/lib/utils/command-result.js +27 -0
- package/lib/utils/constants.d.ts +10 -0
- package/lib/utils/constants.js +39 -0
- package/lib/utils/dep-versions.d.ts +20 -0
- package/lib/utils/dep-versions.js +129 -0
- package/lib/utils/git.d.ts +14 -0
- package/lib/utils/git.js +64 -0
- package/lib/utils/hbcTransform.d.ts +111 -0
- package/lib/utils/hbcTransform.js +561 -0
- package/lib/utils/hermes-base.d.ts +227 -0
- package/lib/utils/hermes-base.js +1287 -0
- package/lib/utils/hermes-literals.d.ts +64 -0
- package/lib/utils/hermes-literals.js +260 -0
- package/lib/utils/http-helper.d.ts +10 -0
- package/lib/utils/http-helper.js +150 -0
- package/lib/utils/i18n.d.ts +22 -0
- package/lib/utils/i18n.js +46 -0
- package/lib/utils/index.d.ts +67 -0
- package/lib/utils/index.js +545 -0
- package/lib/utils/latest-version/index.d.ts +154 -0
- package/lib/utils/latest-version/index.js +331 -0
- package/lib/utils/options.d.ts +10 -0
- package/lib/utils/options.js +68 -0
- package/lib/utils/plugin-config.d.ts +14 -0
- package/lib/utils/plugin-config.js +66 -0
- package/lib/utils/runtime.d.ts +54 -0
- package/lib/utils/runtime.js +263 -0
- package/lib/utils/slim-sourcemap.d.ts +28 -0
- package/lib/utils/slim-sourcemap.js +106 -0
- package/lib/utils/zip-entries.d.ts +10 -0
- package/lib/utils/zip-entries.js +211 -0
- package/lib/utils/zip-options.d.ts +17 -0
- package/lib/utils/zip-options.js +190 -0
- package/lib/utils/zip-range.d.ts +148 -0
- package/lib/utils/zip-range.js +581 -0
- package/lib/utils/zip-raw-writer.d.ts +52 -0
- package/lib/utils/zip-raw-writer.js +209 -0
- package/lib/version-operations.d.ts +21 -0
- package/lib/version-operations.js +320 -0
- package/lib/versions.d.ts +66 -0
- package/lib/versions.js +402 -0
- package/package.json +113 -0
- package/proto/Configuration.proto +183 -0
- package/proto/Resources.proto +569 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.publishStandalone = publishStandalone;
|
|
7
|
+
exports.retryStandalonePublish = retryStandalonePublish;
|
|
8
|
+
exports.registerStandalonePdiff = registerStandalonePdiff;
|
|
9
|
+
/**
|
|
10
|
+
* [INPUT]: 依赖独立 Go API、对象上传、渠道/原生包查询与版本参数契约
|
|
11
|
+
* [OUTPUT]: 对外提供 UpdatePackage/Deployment 上传(含受热更配额校验的 source map 归档)、默认 100% 全量的交互或显式目标解析、准确 ID 重试及逐构建 pdiff 登记
|
|
12
|
+
* [POS]: CLI 独立服务投放编排层;一个包可在多个渠道×原生版本目标上原子保存并发布
|
|
13
|
+
*/
|
|
14
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const os_1 = __importDefault(require("os"));
|
|
17
|
+
const path_1 = __importDefault(require("path"));
|
|
18
|
+
const api_1 = require("./api");
|
|
19
|
+
const app_1 = require("./app");
|
|
20
|
+
const package_1 = require("./package");
|
|
21
|
+
const utils_1 = require("./utils");
|
|
22
|
+
const hermes_base_1 = require("./utils/hermes-base");
|
|
23
|
+
const slim_sourcemap_1 = require("./utils/slim-sourcemap");
|
|
24
|
+
const http_helper_1 = require("./utils/http-helper");
|
|
25
|
+
const i18n_1 = require("./utils/i18n");
|
|
26
|
+
const options_1 = require("./utils/options");
|
|
27
|
+
const DEFAULT_STANDALONE_ROLLOUT = 100;
|
|
28
|
+
function hasStandaloneTargetSelection(options) {
|
|
29
|
+
return Boolean(options.targets ||
|
|
30
|
+
options.packageId ||
|
|
31
|
+
options.packageVersion ||
|
|
32
|
+
options.packageVersionRange ||
|
|
33
|
+
options.minPackageVersion ||
|
|
34
|
+
options.maxPackageVersion);
|
|
35
|
+
}
|
|
36
|
+
function resolveTargetRollout(value) {
|
|
37
|
+
const rollout = value === undefined ? DEFAULT_STANDALONE_ROLLOUT : Number(value);
|
|
38
|
+
if (!Number.isInteger(rollout) || rollout < 1 || rollout > 100) {
|
|
39
|
+
throw new Error((0, i18n_1.t)('rolloutRangeError'));
|
|
40
|
+
}
|
|
41
|
+
return rollout;
|
|
42
|
+
}
|
|
43
|
+
function nativePackageVersion(pkg) {
|
|
44
|
+
return String(pkg.versionName ?? pkg.name ?? '').trim();
|
|
45
|
+
}
|
|
46
|
+
/** Choose the native build that receives an interactively published package. */
|
|
47
|
+
async function chooseStandaloneTarget(appId, options) {
|
|
48
|
+
const channels = await (0, api_1.getChannels)(appId);
|
|
49
|
+
const allPackages = (await (0, api_1.getAllPackages)(appId)) ?? [];
|
|
50
|
+
const requestedChannel = String(options.channel || '').trim().toLowerCase();
|
|
51
|
+
let packages = allPackages;
|
|
52
|
+
if (requestedChannel) {
|
|
53
|
+
const channel = channels.find((item) => item.id === requestedChannel ||
|
|
54
|
+
item.code.trim().toLowerCase() === requestedChannel);
|
|
55
|
+
if (!channel) {
|
|
56
|
+
throw new Error(`渠道 ${requestedChannel} 尚未在应用中登记。`);
|
|
57
|
+
}
|
|
58
|
+
packages = allPackages.filter((pkg) => pkg.channelId === channel.id);
|
|
59
|
+
}
|
|
60
|
+
if (packages.length === 0) {
|
|
61
|
+
throw new Error(requestedChannel
|
|
62
|
+
? `渠道 ${requestedChannel} 下没有可绑定的原生包。`
|
|
63
|
+
: (0, i18n_1.t)('noPackagesFound', { appId }));
|
|
64
|
+
}
|
|
65
|
+
const channelNames = new Map(channels.map((channel) => [channel.id, channel.code]));
|
|
66
|
+
const displayPackages = packages.map((pkg) => ({
|
|
67
|
+
...pkg,
|
|
68
|
+
channel: pkg.channel || channelNames.get(pkg.channelId || ''),
|
|
69
|
+
}));
|
|
70
|
+
const selected = await (0, package_1.choosePackage)(appId, displayPackages);
|
|
71
|
+
const channelId = String(selected.channelId || '').trim();
|
|
72
|
+
const packageVersion = nativePackageVersion(selected);
|
|
73
|
+
if (!channelId || !packageVersion) {
|
|
74
|
+
throw new Error('所选原生包缺少渠道或版本信息,无法创建投放目标。');
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
channelId,
|
|
78
|
+
packageVersion,
|
|
79
|
+
rollout: resolveTargetRollout(options.rollout),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function standaloneOperationKey(appId, packageId, deploymentIds) {
|
|
83
|
+
const identity = [appId, packageId, ...deploymentIds].sort().join("\x00");
|
|
84
|
+
return `cli-${(0, hermes_base_1.sha256Hex)(Buffer.from(identity)).slice(0, 64)}`;
|
|
85
|
+
}
|
|
86
|
+
function standaloneDraftOperationKey(appId, packageId, targets) {
|
|
87
|
+
const identity = [
|
|
88
|
+
appId,
|
|
89
|
+
packageId,
|
|
90
|
+
'save',
|
|
91
|
+
...targets
|
|
92
|
+
.map((target) => `${target.channelId}\x00${target.packageVersion}\x00${target.rollout}`)
|
|
93
|
+
.sort(),
|
|
94
|
+
].join('\x00');
|
|
95
|
+
return `cli-${(0, hermes_base_1.sha256Hex)(Buffer.from(identity)).slice(0, 64)}`;
|
|
96
|
+
}
|
|
97
|
+
/** Keep Hermes verification fields separate from the user's opaque metaInfo. */
|
|
98
|
+
function standaloneHermesOutcomeFields(value) {
|
|
99
|
+
if (!value?.hermesBaseOutcome)
|
|
100
|
+
return {};
|
|
101
|
+
const fields = {
|
|
102
|
+
hermesBaseOutcome: value.hermesBaseOutcome,
|
|
103
|
+
};
|
|
104
|
+
const detail = (0, hermes_base_1.truncateHermesBaseDetail)(value.hermesBaseDetail);
|
|
105
|
+
if (detail)
|
|
106
|
+
fields.hermesBaseDetail = detail;
|
|
107
|
+
return fields;
|
|
108
|
+
}
|
|
109
|
+
async function findStandalonePackageByHash(appId, hash) {
|
|
110
|
+
const pageSize = 100;
|
|
111
|
+
for (let page = 1;; page += 1) {
|
|
112
|
+
const response = await (0, api_1.get)((0, api_1.servicePath)(`/apps/${encodeURIComponent(appId)}/update-packages?page=${page}&pageSize=${pageSize}`));
|
|
113
|
+
const result = (0, api_1.unwrapData)(response);
|
|
114
|
+
const existing = result.items?.find((item) => item.hash === hash);
|
|
115
|
+
if (existing)
|
|
116
|
+
return existing;
|
|
117
|
+
if (!result.items?.length || (result.total !== undefined && page * pageSize >= result.total)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Convert CLI channel codes/targets into UUID targets used by the Go API. */
|
|
123
|
+
async function resolveStandaloneTargets(appId, options, dryRun = false) {
|
|
124
|
+
const channels = await (0, api_1.getChannels)(appId);
|
|
125
|
+
const byCode = new Map(channels.map((channel) => [channel.code, channel]));
|
|
126
|
+
const byId = new Map(channels.map((channel) => [channel.id, channel]));
|
|
127
|
+
let rawTargets;
|
|
128
|
+
if (options.targets) {
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(fs_1.default.readFileSync(String(options.targets), 'utf8'));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw new Error(`无法读取 targets 文件:${error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
}
|
|
136
|
+
const candidate = Array.isArray(parsed)
|
|
137
|
+
? parsed
|
|
138
|
+
: parsed &&
|
|
139
|
+
typeof parsed === 'object' &&
|
|
140
|
+
Array.isArray(parsed.targets)
|
|
141
|
+
? parsed.targets
|
|
142
|
+
: null;
|
|
143
|
+
if (!candidate) {
|
|
144
|
+
throw new Error('targets.json 必须是数组,或包含 targets 数组。');
|
|
145
|
+
}
|
|
146
|
+
rawTargets = candidate;
|
|
147
|
+
}
|
|
148
|
+
else if (options.packageId) {
|
|
149
|
+
const packages = (await (0, api_1.getAllPackages)(appId)) ?? [];
|
|
150
|
+
const selected = packages.find((pkg) => String(pkg.id) === String(options.packageId));
|
|
151
|
+
if (!selected) {
|
|
152
|
+
throw new Error((0, i18n_1.t)('nativePackageIdNotFound', { id: options.packageId }));
|
|
153
|
+
}
|
|
154
|
+
const packageVersion = nativePackageVersion(selected);
|
|
155
|
+
if (!selected.channelId || !packageVersion) {
|
|
156
|
+
throw new Error('指定的原生包缺少渠道或版本信息,无法创建投放目标。');
|
|
157
|
+
}
|
|
158
|
+
rawTargets = [
|
|
159
|
+
{
|
|
160
|
+
channelId: selected.channelId,
|
|
161
|
+
packageVersion,
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
rawTargets = [
|
|
167
|
+
{
|
|
168
|
+
channel: options.channel || 'default',
|
|
169
|
+
packageVersion: options.packageVersion,
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
if (rawTargets.length === 0) {
|
|
174
|
+
throw new Error('至少指定一个渠道版本组。');
|
|
175
|
+
}
|
|
176
|
+
return Promise.all(rawTargets.map(async (target) => {
|
|
177
|
+
const rawChannel = target.channel || options.channel || 'default';
|
|
178
|
+
let channel = target.channelId
|
|
179
|
+
? byId.get(target.channelId)
|
|
180
|
+
: byCode.get(rawChannel.trim().toLowerCase());
|
|
181
|
+
if (!channel && !target.channelId && !dryRun) {
|
|
182
|
+
const resolved = await (0, app_1.getOrCreateChannel)(appId, rawChannel);
|
|
183
|
+
channel = resolved.channel;
|
|
184
|
+
if (resolved.created) {
|
|
185
|
+
console.log((0, i18n_1.t)('channelAutoCreated', { channel: rawChannel.trim().toLowerCase() }));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!channel) {
|
|
189
|
+
throw new Error(`渠道 ${target.channelId || target.channel || options.channel || 'default'} 尚未在应用中登记。`);
|
|
190
|
+
}
|
|
191
|
+
const packageVersion = String(target.packageVersion || options.packageVersion || '').trim();
|
|
192
|
+
if (!packageVersion) {
|
|
193
|
+
throw new Error('独立服务发布必须指定 packageVersion。');
|
|
194
|
+
}
|
|
195
|
+
const targetRollout = resolveTargetRollout(target.rollout === undefined ? options.rollout : target.rollout);
|
|
196
|
+
return { channelId: channel.id, packageVersion, rollout: targetRollout };
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
async function publishStandalone(fn, appId, options) {
|
|
200
|
+
const nonInteractive = (0, options_1.getBooleanOption)(options, 'no-interactive', false) || (0, utils_1.isNonInteractive)();
|
|
201
|
+
const hasExplicitTargets = hasStandaloneTargetSelection(options);
|
|
202
|
+
let targets;
|
|
203
|
+
if (hasExplicitTargets || options.dryRun) {
|
|
204
|
+
targets = await resolveStandaloneTargets(appId, options, Boolean(options.dryRun));
|
|
205
|
+
}
|
|
206
|
+
else if (nonInteractive) {
|
|
207
|
+
throw new Error((0, i18n_1.t)('standaloneTargetRequired'));
|
|
208
|
+
}
|
|
209
|
+
if (options.dryRun) {
|
|
210
|
+
console.log(JSON.stringify({ appId, targets, dryRun: true }, null, 2));
|
|
211
|
+
return String(options.name || 'dry-run');
|
|
212
|
+
}
|
|
213
|
+
const hash = (0, hermes_base_1.sha256Hex)(fs_1.default.readFileSync(fn));
|
|
214
|
+
const sourcemapPath = typeof options.sourcemap === 'string' && options.sourcemap.trim()
|
|
215
|
+
? options.sourcemap.trim()
|
|
216
|
+
: undefined;
|
|
217
|
+
if (sourcemapPath && !fs_1.default.existsSync(sourcemapPath)) {
|
|
218
|
+
throw new Error((0, i18n_1.t)('sourceMapNotFound', { path: sourcemapPath }));
|
|
219
|
+
}
|
|
220
|
+
// Source maps are hot-update artifacts. Pack the map before upload so the
|
|
221
|
+
// server checks the bytes that will actually be archived, while retaining
|
|
222
|
+
// the CLI's relative-path/sourceContent slimming contract.
|
|
223
|
+
let uploadSourcemapPath = sourcemapPath;
|
|
224
|
+
let packTempDir;
|
|
225
|
+
if (sourcemapPath) {
|
|
226
|
+
packTempDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'rnu-sourcemap-'));
|
|
227
|
+
uploadSourcemapPath = path_1.default.join(packTempDir, path_1.default.basename(sourcemapPath));
|
|
228
|
+
fs_1.default.writeFileSync(uploadSourcemapPath, (0, slim_sourcemap_1.packSourceMap)(fs_1.default.readFileSync(sourcemapPath, 'utf8'), process.cwd()));
|
|
229
|
+
}
|
|
230
|
+
let packageValue;
|
|
231
|
+
let createdNewPackage = false;
|
|
232
|
+
const existing = await findStandalonePackageByHash(appId, hash);
|
|
233
|
+
if (existing) {
|
|
234
|
+
packageValue = existing;
|
|
235
|
+
if (uploadSourcemapPath) {
|
|
236
|
+
console.log(chalk_1.default.yellow((0, i18n_1.t)('sourceMapAlreadyArchivedWarning')));
|
|
237
|
+
}
|
|
238
|
+
if (packTempDir) {
|
|
239
|
+
fs_1.default.rmSync(packTempDir, { recursive: true, force: true });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
const name = options.name ||
|
|
244
|
+
(nonInteractive ? '' : await (0, utils_1.question)((0, i18n_1.t)('versionNameQuestion'))) ||
|
|
245
|
+
(0, i18n_1.t)('unnamed');
|
|
246
|
+
const description = options.description ??
|
|
247
|
+
(nonInteractive ? '' : await (0, utils_1.question)((0, i18n_1.t)('versionDescriptionQuestion')));
|
|
248
|
+
const metaInfo = options.metaInfo ??
|
|
249
|
+
(nonInteractive ? '' : await (0, utils_1.question)((0, i18n_1.t)('versionMetaInfoQuestion')));
|
|
250
|
+
try {
|
|
251
|
+
const [uploaded, sourceMapUpload] = await Promise.all([
|
|
252
|
+
(0, api_1.uploadFile)(fn, undefined, appId),
|
|
253
|
+
uploadSourcemapPath ? (0, api_1.uploadFile)(uploadSourcemapPath, undefined, appId) : Promise.resolve(undefined),
|
|
254
|
+
]);
|
|
255
|
+
const fullKey = uploaded.key || uploaded.hash;
|
|
256
|
+
const sourceMapKey = sourceMapUpload?.key || sourceMapUpload?.hash;
|
|
257
|
+
const response = await (0, api_1.post)((0, api_1.servicePath)(`/apps/${encodeURIComponent(appId)}/update-packages`), {
|
|
258
|
+
name,
|
|
259
|
+
hash,
|
|
260
|
+
description,
|
|
261
|
+
metaInfo,
|
|
262
|
+
...standaloneHermesOutcomeFields(options.hermesBase),
|
|
263
|
+
diffVersion: 0,
|
|
264
|
+
...(options.diffFromHash ? { diffFromHash: options.diffFromHash } : {}),
|
|
265
|
+
fullKey,
|
|
266
|
+
diffKey: '',
|
|
267
|
+
pdiffKey: '',
|
|
268
|
+
...(sourceMapKey ? { sourceMapKey } : {}),
|
|
269
|
+
});
|
|
270
|
+
packageValue = (0, api_1.unwrapData)(response);
|
|
271
|
+
createdNewPackage = true;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
const raced = await findStandalonePackageByHash(appId, hash);
|
|
275
|
+
if (!raced)
|
|
276
|
+
throw error;
|
|
277
|
+
packageValue = raced;
|
|
278
|
+
}
|
|
279
|
+
finally {
|
|
280
|
+
if (packTempDir) {
|
|
281
|
+
fs_1.default.rmSync(packTempDir, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (!targets) {
|
|
286
|
+
const bind = await (0, utils_1.question)((0, i18n_1.t)('updateNativePackageQuestion'));
|
|
287
|
+
if (bind.toLowerCase() !== 'y') {
|
|
288
|
+
if (createdNewPackage) {
|
|
289
|
+
console.log((0, i18n_1.t)('packageUploadSuccess', { id: packageValue.id }));
|
|
290
|
+
}
|
|
291
|
+
return packageValue.id;
|
|
292
|
+
}
|
|
293
|
+
targets = [await chooseStandaloneTarget(appId, options)];
|
|
294
|
+
}
|
|
295
|
+
const deploymentResponse = await (0, api_1.get)((0, api_1.servicePath)(`/update-packages/${encodeURIComponent(packageValue.id)}/deployments`));
|
|
296
|
+
const existingDeployments = (0, api_1.unwrapData)(deploymentResponse);
|
|
297
|
+
const existingByTarget = new Map(existingDeployments.map((deployment) => [
|
|
298
|
+
`${deployment.channelId}\x00${deployment.packageVersion}`,
|
|
299
|
+
deployment,
|
|
300
|
+
]));
|
|
301
|
+
const duplicateTargets = new Set();
|
|
302
|
+
for (const target of targets) {
|
|
303
|
+
const targetKey = `${target.channelId}\x00${target.packageVersion}`;
|
|
304
|
+
if (duplicateTargets.has(targetKey)) {
|
|
305
|
+
throw new Error(`targets.json 重复指定目标:${target.channelId}/${target.packageVersion}。`);
|
|
306
|
+
}
|
|
307
|
+
duplicateTargets.add(targetKey);
|
|
308
|
+
}
|
|
309
|
+
const missingTargets = targets.filter((target) => !existingByTarget.has(`${target.channelId}\x00${target.packageVersion}`));
|
|
310
|
+
const savedByTarget = new Map();
|
|
311
|
+
if (missingTargets.length > 0) {
|
|
312
|
+
const draftIdempotencyKey = standaloneDraftOperationKey(appId, packageValue.id, missingTargets);
|
|
313
|
+
const response = await (0, api_1.post)((0, api_1.servicePath)(`/apps/${encodeURIComponent(appId)}/deployments/batch`), {
|
|
314
|
+
idempotencyKey: draftIdempotencyKey,
|
|
315
|
+
commands: missingTargets.map((target) => ({
|
|
316
|
+
packageId: packageValue.id,
|
|
317
|
+
channelId: target.channelId,
|
|
318
|
+
packageVersion: target.packageVersion,
|
|
319
|
+
rollout: target.rollout,
|
|
320
|
+
forceBoot: false,
|
|
321
|
+
action: 'save',
|
|
322
|
+
})),
|
|
323
|
+
});
|
|
324
|
+
const saved = (0, api_1.unwrapData)(response);
|
|
325
|
+
for (const deployment of saved) {
|
|
326
|
+
savedByTarget.set(`${deployment.channelId}\x00${deployment.packageVersion}`, deployment);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const created = targets.map((target) => {
|
|
330
|
+
const targetKey = `${target.channelId}\x00${target.packageVersion}`;
|
|
331
|
+
const deployment = existingByTarget.get(targetKey) || savedByTarget.get(targetKey);
|
|
332
|
+
if (!deployment) {
|
|
333
|
+
throw new Error(`服务端未返回目标策略:${target.channelId}/${target.packageVersion}。`);
|
|
334
|
+
}
|
|
335
|
+
return { ...deployment, ...target, packageId: packageValue.id };
|
|
336
|
+
});
|
|
337
|
+
const idempotencyKey = standaloneOperationKey(appId, packageValue.id, created.map((deployment) => deployment.id));
|
|
338
|
+
try {
|
|
339
|
+
await (0, api_1.post)((0, api_1.servicePath)(`/apps/${encodeURIComponent(appId)}/deployments/batch`), {
|
|
340
|
+
idempotencyKey,
|
|
341
|
+
commands: created.map((deployment) => ({ id: deployment.id, packageId: packageValue.id, channelId: deployment.channelId, packageVersion: deployment.packageVersion, rollout: deployment.rollout, forceBoot: false, action: 'publish', expectedRevision: deployment.revision })),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
console.error(`发布失败,投放 ID:${created.map((deployment) => deployment.id).join(', ')}`);
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
console.log(JSON.stringify({ packageId: packageValue.id, deploymentIds: created.map((deployment) => deployment.id), idempotencyKey, targets }, null, 2));
|
|
349
|
+
return packageValue.id;
|
|
350
|
+
}
|
|
351
|
+
/** Retry publishing deployments returned by a failed standalone publish.
|
|
352
|
+
*
|
|
353
|
+
* Retry uses exact deployment ids and revisions; no artifact upload or new
|
|
354
|
+
* package is created.
|
|
355
|
+
*/
|
|
356
|
+
async function retryStandalonePublish(appId, releaseIdsInput, dryRun = false) {
|
|
357
|
+
const deploymentIds = releaseIdsInput
|
|
358
|
+
.split(',')
|
|
359
|
+
.map((value) => value.trim())
|
|
360
|
+
.filter(Boolean);
|
|
361
|
+
if (deploymentIds.length === 0) {
|
|
362
|
+
throw new Error('deploymentIds 不能为空。');
|
|
363
|
+
}
|
|
364
|
+
const deployments = await Promise.all(deploymentIds.map(async (deploymentId) => {
|
|
365
|
+
const response = await (0, api_1.get)((0, api_1.servicePath)(`/deployments/${encodeURIComponent(deploymentId)}`));
|
|
366
|
+
const deployment = (0, api_1.unwrapData)(response);
|
|
367
|
+
if (!deployment || deployment.applicationId !== appId || !deployment.packageId || !deployment.channelId || !deployment.packageVersion) {
|
|
368
|
+
throw new Error(`投放 ${deploymentId} 不属于应用 ${appId} 或缺少目标身份。`);
|
|
369
|
+
}
|
|
370
|
+
return deployment;
|
|
371
|
+
}));
|
|
372
|
+
const commands = deployments.map(({ id, packageId, channelId, packageVersion, rollout, forceBoot, revision }) => ({ id, packageId, channelId, packageVersion, rollout, forceBoot, revision, action: 'publish' }));
|
|
373
|
+
const packageIds = [...new Set(deployments.map((deployment) => deployment.packageId))];
|
|
374
|
+
if (packageIds.length !== 1) {
|
|
375
|
+
throw new Error('deploymentIds 必须属于同一个热更新包。');
|
|
376
|
+
}
|
|
377
|
+
const idempotencyKey = standaloneOperationKey(appId, packageIds[0], deploymentIds);
|
|
378
|
+
if (dryRun) {
|
|
379
|
+
console.log(JSON.stringify({ appId, deploymentIds, idempotencyKey, commands, dryRun: true }, null, 2));
|
|
380
|
+
return deploymentIds.join(',');
|
|
381
|
+
}
|
|
382
|
+
await (0, api_1.post)((0, api_1.servicePath)(`/apps/${encodeURIComponent(appId)}/deployments/batch`), { idempotencyKey, commands });
|
|
383
|
+
console.log(JSON.stringify({ deploymentIds, idempotencyKey, commands, retried: true }, null, 2));
|
|
384
|
+
return deploymentIds.join(',');
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Upload and register one native-package patch for one exact build.
|
|
388
|
+
*
|
|
389
|
+
* The server deliberately does not expose a group-wide pdiff field for this
|
|
390
|
+
* command: a patch is only valid against the nativeVersionId it was built
|
|
391
|
+
* from. A dry-run resolves local arguments but never uploads or mutates a
|
|
392
|
+
* release mapping.
|
|
393
|
+
*/
|
|
394
|
+
async function registerStandalonePdiff(filePath, options) {
|
|
395
|
+
if (!(0, http_helper_1.isStandaloneService)()) {
|
|
396
|
+
throw new Error('逐构建 pdiff 登记需要独立服务(请设置 RNU_SERVICE_URL)。');
|
|
397
|
+
}
|
|
398
|
+
const deploymentId = String(options.deploymentId || options.releaseId || '').trim();
|
|
399
|
+
const nativeVersionId = String(options.nativeVersionId || '').trim();
|
|
400
|
+
const diffFromHash = String(options.diffFromHash || '').trim().toLowerCase();
|
|
401
|
+
if (!deploymentId || !nativeVersionId || !diffFromHash) {
|
|
402
|
+
throw new Error('pdiff 登记需要 --deploymentId、--nativeVersionId 和 --diffFromHash。');
|
|
403
|
+
}
|
|
404
|
+
if (!/^[0-9a-f]{64}$/.test(diffFromHash)) {
|
|
405
|
+
throw new Error('--diffFromHash 必须是 64 位小写 SHA-256 摘要。');
|
|
406
|
+
}
|
|
407
|
+
if (!filePath || !fs_1.default.existsSync(filePath)) {
|
|
408
|
+
throw new Error(`pdiff 文件不存在:${filePath || '(未指定)'}`);
|
|
409
|
+
}
|
|
410
|
+
const patchSuffixes = ['.apk.patch', '.ipa.patch', '.hap.patch', '.app.patch'];
|
|
411
|
+
if (!patchSuffixes.some((suffix) => filePath.toLowerCase().endsWith(suffix))) {
|
|
412
|
+
throw new Error('pdiff 文件必须以 .apk.patch、.ipa.patch、.hap.patch 或 .app.patch 结尾。');
|
|
413
|
+
}
|
|
414
|
+
const appId = await (0, app_1.resolveAppId)(options);
|
|
415
|
+
if (options.dryRun) {
|
|
416
|
+
const preview = { appId, deploymentId, nativeVersionId, diffFromHash, filePath, dryRun: true };
|
|
417
|
+
console.log(JSON.stringify(preview, null, 2));
|
|
418
|
+
return deploymentId;
|
|
419
|
+
}
|
|
420
|
+
const uploaded = await (0, api_1.uploadFile)(filePath, undefined, appId);
|
|
421
|
+
const key = uploaded.key || uploaded.hash;
|
|
422
|
+
const response = await (0, api_1.put)((0, api_1.servicePath)(`/deployments/${encodeURIComponent(deploymentId)}/pdiffs/${encodeURIComponent(nativeVersionId)}`), { diffFromHash, key });
|
|
423
|
+
const mapping = (0, api_1.unwrapData)(response);
|
|
424
|
+
const result = {
|
|
425
|
+
deploymentId,
|
|
426
|
+
nativeVersionId,
|
|
427
|
+
diffFromHash,
|
|
428
|
+
key: mapping?.key || key,
|
|
429
|
+
id: mapping?.id,
|
|
430
|
+
};
|
|
431
|
+
console.log(JSON.stringify(result, null, 2));
|
|
432
|
+
return deploymentId;
|
|
433
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 依赖 source-map、服务端版本查询、应用选择、网络运行时与 source map 裁剪器
|
|
3
|
+
* [OUTPUT]: 对外提供 FrameConsumer、symbolicateStack 与 symbolicateCommands
|
|
4
|
+
* [POS]: CLI 崩溃栈符号化边界,把发布 bundle 的栈位置映射回源码,不参与 bundle 构建
|
|
5
|
+
*/
|
|
6
|
+
export interface FrameConsumer {
|
|
7
|
+
originalPositionFor(position: {
|
|
8
|
+
line: number;
|
|
9
|
+
column: number;
|
|
10
|
+
}): {
|
|
11
|
+
source?: string | null;
|
|
12
|
+
line?: number | null;
|
|
13
|
+
column?: number | null;
|
|
14
|
+
name?: string | null;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Rewrites every bundle frame of `stack` to its original source position.
|
|
19
|
+
* JS engines report 1-based columns, so they are shifted to the 0-based
|
|
20
|
+
* columns source maps use; Hermes bytecode frames (`address at`) carry the
|
|
21
|
+
* bytecode offset, which the composed hermes map keys directly.
|
|
22
|
+
*/
|
|
23
|
+
export declare function symbolicateStack(stack: string, consumer: FrameConsumer): {
|
|
24
|
+
text: string;
|
|
25
|
+
mapped: number;
|
|
26
|
+
};
|
|
27
|
+
export declare const symbolicateCommands: {
|
|
28
|
+
symbolicate: ({ args, options, }: {
|
|
29
|
+
args: string[];
|
|
30
|
+
options: Record<string, unknown>;
|
|
31
|
+
}) => Promise<{
|
|
32
|
+
versionId: string;
|
|
33
|
+
mapped: number;
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* [INPUT]: 依赖 source-map、服务端版本查询、应用选择、网络运行时与 source map 裁剪器
|
|
4
|
+
* [OUTPUT]: 对外提供 FrameConsumer、symbolicateStack 与 symbolicateCommands
|
|
5
|
+
* [POS]: CLI 崩溃栈符号化边界,把发布 bundle 的栈位置映射回源码,不参与 bundle 构建
|
|
6
|
+
*/
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.symbolicateCommands = void 0;
|
|
12
|
+
exports.symbolicateStack = symbolicateStack;
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const source_map_1 = require("source-map");
|
|
15
|
+
const api_1 = require("./api");
|
|
16
|
+
const app_1 = require("./app");
|
|
17
|
+
const i18n_1 = require("./utils/i18n");
|
|
18
|
+
const runtime_1 = require("./utils/runtime");
|
|
19
|
+
const slim_sourcemap_1 = require("./utils/slim-sourcemap");
|
|
20
|
+
const versions_1 = require("./versions");
|
|
21
|
+
// A frame location inside a shipped bundle: `<bundle>:<line>:<column>`. Covers
|
|
22
|
+
// `index.bundlejs`, `index.android.bundle`, `main.jsbundle`, `bundle.harmony.js`
|
|
23
|
+
// and plain `.js`/`.hbc` names; an optional `address at ` prefix marks a
|
|
24
|
+
// Hermes bytecode frame (column = bytecode offset, already 0-based).
|
|
25
|
+
const FRAME_PATTERN = /(address at )?((?:[\w.-]+\/)*[\w.-]+\.(?:bundlejs|jsbundle|bundle|hbc|js))(?:\?[^\s:)]*)?:(\d+):(\d+)/g;
|
|
26
|
+
/**
|
|
27
|
+
* Rewrites every bundle frame of `stack` to its original source position.
|
|
28
|
+
* JS engines report 1-based columns, so they are shifted to the 0-based
|
|
29
|
+
* columns source maps use; Hermes bytecode frames (`address at`) carry the
|
|
30
|
+
* bytecode offset, which the composed hermes map keys directly.
|
|
31
|
+
*/
|
|
32
|
+
function symbolicateStack(stack, consumer) {
|
|
33
|
+
let mapped = 0;
|
|
34
|
+
const text = stack.replace(FRAME_PATTERN, (frame, hermes, _bundle, line, column) => {
|
|
35
|
+
const position = consumer.originalPositionFor({
|
|
36
|
+
line: Number(line),
|
|
37
|
+
column: hermes ? Number(column) : Math.max(0, Number(column) - 1),
|
|
38
|
+
});
|
|
39
|
+
if (!position.source ||
|
|
40
|
+
position.line === null ||
|
|
41
|
+
position.line === undefined) {
|
|
42
|
+
return frame;
|
|
43
|
+
}
|
|
44
|
+
mapped++;
|
|
45
|
+
const original = `${position.source}:${position.line}:${(position.column ?? 0) + 1}`;
|
|
46
|
+
return position.name ? `${original} (${position.name})` : original;
|
|
47
|
+
});
|
|
48
|
+
return { text, mapped };
|
|
49
|
+
}
|
|
50
|
+
async function findVersionByHash(appId, hash) {
|
|
51
|
+
const limit = 100;
|
|
52
|
+
// The list is newest-first; crashes are usually about recent versions.
|
|
53
|
+
for (let offset = 0; offset < 100 * limit; offset += limit) {
|
|
54
|
+
const page = await (0, versions_1.fetchVersions)(appId, offset, limit);
|
|
55
|
+
const found = page.find((version) => version.hash === hash);
|
|
56
|
+
if (found) {
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
if (page.length < limit) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
async function readStack(source) {
|
|
66
|
+
if (!source || source === '-') {
|
|
67
|
+
// No file and an interactive terminal: nothing is piped in, so waiting on
|
|
68
|
+
// stdin would just hang. `-` is an explicit request for stdin either way.
|
|
69
|
+
if (!source && process.stdin.isTTY) {
|
|
70
|
+
throw new Error((0, i18n_1.t)('symbolicateUsage'));
|
|
71
|
+
}
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const chunks = [];
|
|
74
|
+
process.stdin.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
75
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
76
|
+
process.stdin.on('error', reject);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return fs_1.default.readFileSync(source, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
exports.symbolicateCommands = {
|
|
82
|
+
symbolicate: async ({ args, options, }) => {
|
|
83
|
+
const hash = typeof options.hash === 'string' ? options.hash : '';
|
|
84
|
+
const versionIdOption = typeof options.versionId === 'string' ? options.versionId : '';
|
|
85
|
+
if (!hash && !versionIdOption) {
|
|
86
|
+
throw new Error((0, i18n_1.t)('symbolicateUsage'));
|
|
87
|
+
}
|
|
88
|
+
const platform = await (0, app_1.getPlatform)(typeof options.platform === 'string'
|
|
89
|
+
? options.platform
|
|
90
|
+
: undefined);
|
|
91
|
+
const appId = typeof options.appId === 'string' && options.appId
|
|
92
|
+
? options.appId
|
|
93
|
+
: (await (0, app_1.getSelectedApp)(platform, options.config))
|
|
94
|
+
.appId;
|
|
95
|
+
let versionId = versionIdOption;
|
|
96
|
+
if (!versionId) {
|
|
97
|
+
const version = await findVersionByHash(appId, hash);
|
|
98
|
+
if (!version) {
|
|
99
|
+
throw new Error((0, i18n_1.t)('symbolicateVersionNotFound', { hash }));
|
|
100
|
+
}
|
|
101
|
+
versionId = String(version.id);
|
|
102
|
+
}
|
|
103
|
+
const located = await (0, api_1.get)(`/app/${appId}/version/${versionId}/sourceMap`);
|
|
104
|
+
if (!located?.url) {
|
|
105
|
+
throw new Error((0, i18n_1.t)('symbolicateNoSourceMap', { id: versionId }));
|
|
106
|
+
}
|
|
107
|
+
const response = await (0, runtime_1.webFetch)(located.url);
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(`Failed to download source map: HTTP ${response.status}`);
|
|
110
|
+
}
|
|
111
|
+
// Maps archived by recent CLI versions are gzipped; older ones are plain
|
|
112
|
+
// JSON, and unpackSourceMap accepts both.
|
|
113
|
+
const rawMap = JSON.parse((0, slim_sourcemap_1.unpackSourceMap)(Buffer.from(await response.arrayBuffer())));
|
|
114
|
+
const consumer = new source_map_1.SourceMapConsumer(rawMap);
|
|
115
|
+
const stack = await readStack(args[0]);
|
|
116
|
+
const { text, mapped } = symbolicateStack(stack, consumer);
|
|
117
|
+
if (typeof options.output === 'string' && options.output) {
|
|
118
|
+
fs_1.default.writeFileSync(options.output, text);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
process.stdout.write(text.endsWith('\n') ? text : `${text}\n`);
|
|
122
|
+
}
|
|
123
|
+
console.error((0, i18n_1.t)('symbolicateDone', {
|
|
124
|
+
count: mapped,
|
|
125
|
+
id: versionId,
|
|
126
|
+
hash: located.hash ?? hash,
|
|
127
|
+
}));
|
|
128
|
+
return { versionId, mapped };
|
|
129
|
+
},
|
|
130
|
+
};
|