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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/README.zh-CN.md +167 -0
  4. package/cli.json +525 -0
  5. package/lib/api.d.ts +56 -0
  6. package/lib/api.js +671 -0
  7. package/lib/app.d.ts +107 -0
  8. package/lib/app.js +329 -0
  9. package/lib/auto-update.d.ts +35 -0
  10. package/lib/auto-update.js +507 -0
  11. package/lib/bin.d.ts +2 -0
  12. package/lib/bin.js +133 -0
  13. package/lib/bundle-pack.d.ts +6 -0
  14. package/lib/bundle-pack.js +91 -0
  15. package/lib/bundle-runner.d.ts +168 -0
  16. package/lib/bundle-runner.js +1305 -0
  17. package/lib/bundle.d.ts +45 -0
  18. package/lib/bundle.js +226 -0
  19. package/lib/cache.d.ts +10 -0
  20. package/lib/cache.js +25 -0
  21. package/lib/commands.d.ts +11 -0
  22. package/lib/commands.js +72 -0
  23. package/lib/diff.d.ts +18 -0
  24. package/lib/diff.js +690 -0
  25. package/lib/exports.d.ts +11 -0
  26. package/lib/exports.js +20 -0
  27. package/lib/install.d.ts +9 -0
  28. package/lib/install.js +51 -0
  29. package/lib/locales/en.d.ts +187 -0
  30. package/lib/locales/en.js +194 -0
  31. package/lib/locales/zh.d.ts +187 -0
  32. package/lib/locales/zh.js +194 -0
  33. package/lib/native-package.d.ts +21 -0
  34. package/lib/native-package.js +298 -0
  35. package/lib/package.d.ts +68 -0
  36. package/lib/package.js +487 -0
  37. package/lib/provider.d.ts +24 -0
  38. package/lib/provider.js +244 -0
  39. package/lib/standalone-publish.d.ts +17 -0
  40. package/lib/standalone-publish.js +433 -0
  41. package/lib/symbolicate.d.ts +35 -0
  42. package/lib/symbolicate.js +130 -0
  43. package/lib/types.d.ts +152 -0
  44. package/lib/types.js +2 -0
  45. package/lib/user.d.ts +8 -0
  46. package/lib/user.js +50 -0
  47. package/lib/utils/add-gitignore.d.ts +6 -0
  48. package/lib/utils/add-gitignore.js +44 -0
  49. package/lib/utils/app-info-parser/aab.d.ts +27 -0
  50. package/lib/utils/app-info-parser/aab.js +256 -0
  51. package/lib/utils/app-info-parser/apk.d.ts +19 -0
  52. package/lib/utils/app-info-parser/apk.js +95 -0
  53. package/lib/utils/app-info-parser/app.d.ts +9 -0
  54. package/lib/utils/app-info-parser/app.js +15 -0
  55. package/lib/utils/app-info-parser/index.d.ts +21 -0
  56. package/lib/utils/app-info-parser/index.js +51 -0
  57. package/lib/utils/app-info-parser/ipa.d.ts +19 -0
  58. package/lib/utils/app-info-parser/ipa.js +101 -0
  59. package/lib/utils/app-info-parser/resource-finder.d.ts +49 -0
  60. package/lib/utils/app-info-parser/resource-finder.js +429 -0
  61. package/lib/utils/app-info-parser/utils.d.ts +36 -0
  62. package/lib/utils/app-info-parser/utils.js +140 -0
  63. package/lib/utils/app-info-parser/xml-parser/binary.d.ts +61 -0
  64. package/lib/utils/app-info-parser/xml-parser/binary.js +577 -0
  65. package/lib/utils/app-info-parser/xml-parser/manifest.d.ts +15 -0
  66. package/lib/utils/app-info-parser/xml-parser/manifest.js +207 -0
  67. package/lib/utils/app-info-parser/zip.d.ts +29 -0
  68. package/lib/utils/app-info-parser/zip.js +97 -0
  69. package/lib/utils/check-lockfile.d.ts +6 -0
  70. package/lib/utils/check-lockfile.js +93 -0
  71. package/lib/utils/check-plugin.d.ts +12 -0
  72. package/lib/utils/check-plugin.js +36 -0
  73. package/lib/utils/command-result.d.ts +8 -0
  74. package/lib/utils/command-result.js +27 -0
  75. package/lib/utils/constants.d.ts +10 -0
  76. package/lib/utils/constants.js +39 -0
  77. package/lib/utils/dep-versions.d.ts +20 -0
  78. package/lib/utils/dep-versions.js +129 -0
  79. package/lib/utils/git.d.ts +14 -0
  80. package/lib/utils/git.js +64 -0
  81. package/lib/utils/hbcTransform.d.ts +111 -0
  82. package/lib/utils/hbcTransform.js +561 -0
  83. package/lib/utils/hermes-base.d.ts +227 -0
  84. package/lib/utils/hermes-base.js +1287 -0
  85. package/lib/utils/hermes-literals.d.ts +64 -0
  86. package/lib/utils/hermes-literals.js +260 -0
  87. package/lib/utils/http-helper.d.ts +10 -0
  88. package/lib/utils/http-helper.js +150 -0
  89. package/lib/utils/i18n.d.ts +22 -0
  90. package/lib/utils/i18n.js +46 -0
  91. package/lib/utils/index.d.ts +67 -0
  92. package/lib/utils/index.js +545 -0
  93. package/lib/utils/latest-version/index.d.ts +154 -0
  94. package/lib/utils/latest-version/index.js +331 -0
  95. package/lib/utils/options.d.ts +10 -0
  96. package/lib/utils/options.js +68 -0
  97. package/lib/utils/plugin-config.d.ts +14 -0
  98. package/lib/utils/plugin-config.js +66 -0
  99. package/lib/utils/runtime.d.ts +54 -0
  100. package/lib/utils/runtime.js +263 -0
  101. package/lib/utils/slim-sourcemap.d.ts +28 -0
  102. package/lib/utils/slim-sourcemap.js +106 -0
  103. package/lib/utils/zip-entries.d.ts +10 -0
  104. package/lib/utils/zip-entries.js +211 -0
  105. package/lib/utils/zip-options.d.ts +17 -0
  106. package/lib/utils/zip-options.js +190 -0
  107. package/lib/utils/zip-range.d.ts +148 -0
  108. package/lib/utils/zip-range.js +581 -0
  109. package/lib/utils/zip-raw-writer.d.ts +52 -0
  110. package/lib/utils/zip-raw-writer.js +209 -0
  111. package/lib/version-operations.d.ts +21 -0
  112. package/lib/version-operations.js +320 -0
  113. package/lib/versions.d.ts +66 -0
  114. package/lib/versions.js +402 -0
  115. package/package.json +113 -0
  116. package/proto/Configuration.proto +183 -0
  117. package/proto/Resources.proto +569 -0
package/lib/diff.js ADDED
@@ -0,0 +1,690 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.diffCommands = exports.readEntry = exports.enumZipEntries = void 0;
40
+ exports.zipEntryContentKey = zipEntryContentKey;
41
+ exports.zipEntriesHaveSameContent = zipEntriesHaveSameContent;
42
+ /**
43
+ * [INPUT]: 依赖 ZIP 读写工具、native-package 条目映射、HDiff 实现与 Hermes 字节码变换能力
44
+ * [OUTPUT]: 对外提供 diffCommands、ZIP 读取工具重导出及条目内容比较函数
45
+ * [POS]: CLI 本地补丁生成层,将旧 PPK 或原生基线与目标 PPK 转为补丁文件;上传登记由 standalone-publish 承担,普通发布不会自动调用本层
46
+ */
47
+ const node_crypto_1 = require("node:crypto");
48
+ const fs = __importStar(require("fs-extra"));
49
+ const global_dirs_1 = require("global-dirs");
50
+ const os_1 = __importDefault(require("os"));
51
+ const path_1 = __importDefault(require("path"));
52
+ const yazl_1 = require("yazl");
53
+ const native_package_1 = require("./native-package");
54
+ const utils_1 = require("./utils");
55
+ const constants_1 = require("./utils/constants");
56
+ const hbcTransform_1 = require("./utils/hbcTransform");
57
+ const i18n_1 = require("./utils/i18n");
58
+ const zip_entries_1 = require("./utils/zip-entries");
59
+ Object.defineProperty(exports, "enumZipEntries", { enumerable: true, get: function () { return zip_entries_1.enumZipEntries; } });
60
+ Object.defineProperty(exports, "readEntry", { enumerable: true, get: function () { return zip_entries_1.readEntry; } });
61
+ const zip_options_1 = require("./utils/zip-options");
62
+ /**
63
+ * 判定两个 zip entry 内容相同的键:crc32 单独并不足以证明内容一致(32 位
64
+ * 校验和碰撞并不罕见,尤其是长度不同的文件),必须同时要求解压后长度相等。
65
+ * 仅用于生成端匹配,manifest 里的 copies / copiesCrc 格式不变。
66
+ */
67
+ function zipEntryContentKey(entry) {
68
+ return `${entry.crc32}:${entry.uncompressedSize}`;
69
+ }
70
+ function zipEntriesHaveSameContent(left, right) {
71
+ return (left.crc32 === right.crc32 &&
72
+ left.uncompressedSize === right.uncompressedSize);
73
+ }
74
+ const loadModule = (pkgName) => {
75
+ const nodePathDirs = (process.env.NODE_PATH || '')
76
+ .split(path_1.default.delimiter)
77
+ .filter(Boolean);
78
+ const resolvePaths = ['.', ...nodePathDirs, global_dirs_1.npm.packages, global_dirs_1.yarn.packages];
79
+ try {
80
+ const resolved = require.resolve(pkgName, { paths: resolvePaths });
81
+ return require(resolved);
82
+ }
83
+ catch { }
84
+ return undefined;
85
+ };
86
+ const hdiff = loadModule('node-hdiffpatch');
87
+ function basename(fn) {
88
+ const m = /^(.+\/)[^/]+\/?$/.exec(fn);
89
+ return m?.[1];
90
+ }
91
+ function createOutputZip(output) {
92
+ fs.ensureDirSync(path_1.default.dirname(output));
93
+ const zipfile = new yazl_1.ZipFile();
94
+ const writeStream = fs.createWriteStream(output);
95
+ const writePromise = new Promise((resolve, reject) => {
96
+ zipfile.outputStream.on('error', reject);
97
+ // without this, a full disk / read-only target left the promise pending
98
+ writeStream.on('error', reject);
99
+ zipfile.outputStream.pipe(writeStream).on('close', () => {
100
+ resolve(void 0);
101
+ });
102
+ });
103
+ // 失败时释放输出 fd 并删掉半成品,避免残留一个截断的 patch 包
104
+ const abort = async () => {
105
+ writePromise.catch(() => { });
106
+ zipfile.outputStream.unpipe(writeStream);
107
+ // yazl 的 outputStream 类型声明缺 destroy,运行时是 PassThrough
108
+ zipfile.outputStream.destroy();
109
+ if (!writeStream.closed) {
110
+ // 等 fd 真正关闭再删文件,避免 Windows 上删除被占用文件失败
111
+ await new Promise((resolve) => {
112
+ writeStream.once('close', () => resolve());
113
+ if (!writeStream.destroyed) {
114
+ writeStream.destroy();
115
+ }
116
+ });
117
+ }
118
+ await fs.rm(output, { force: true });
119
+ };
120
+ return { zipfile, writePromise, abort };
121
+ }
122
+ /**
123
+ * 打开输出包,执行 build 填充条目并返回 manifest,最后写入 __diff.json
124
+ * 收尾。build 或收尾任何一步失败都会关闭输出流、删除半成品并重新抛出。
125
+ */
126
+ async function writeDiffZip(output, build) {
127
+ const { zipfile, writePromise, abort } = createOutputZip(output);
128
+ try {
129
+ const manifest = await build(zipfile);
130
+ await finishDiffZip(zipfile, writePromise, manifest);
131
+ }
132
+ catch (error) {
133
+ await abort();
134
+ throw error;
135
+ }
136
+ }
137
+ // baseline patch 小于该值时跳过变换尝试:节省一次 diff 的 CPU,
138
+ // 且此时变换收益上限(~25%)也抵不过元数据开销
139
+ const HBC_TRANSFORM_MIN_BASELINE_BYTES = 64 * 1024;
140
+ function reportDiffPhase(options, metric) {
141
+ try {
142
+ options.onDiffPhase?.(metric);
143
+ }
144
+ catch {
145
+ // Observability must never break patch generation.
146
+ }
147
+ }
148
+ const bundleSourceSize = (source) => source.kind === 'buffer' ? source.data.length : source.size;
149
+ const bundleSourceBuffer = async (source) => source.kind === 'buffer' ? source.data : fs.readFile(source.path);
150
+ async function bundleSourceFile(source, outputPath) {
151
+ if (source.kind === 'file') {
152
+ return source.path;
153
+ }
154
+ await fs.writeFile(outputPath, source.data);
155
+ return outputPath;
156
+ }
157
+ async function hashFile(filePath) {
158
+ const hash = (0, node_crypto_1.createHash)('sha256');
159
+ for await (const chunk of fs.createReadStream(filePath)) {
160
+ hash.update(chunk);
161
+ }
162
+ return hash.digest('hex');
163
+ }
164
+ async function filesEqual(leftPath, rightPath) {
165
+ const [leftStat, rightStat] = await Promise.all([
166
+ fs.stat(leftPath),
167
+ fs.stat(rightPath),
168
+ ]);
169
+ if (leftStat.size !== rightStat.size) {
170
+ return false;
171
+ }
172
+ const [leftHash, rightHash] = await Promise.all([
173
+ hashFile(leftPath),
174
+ hashFile(rightPath),
175
+ ]);
176
+ return leftHash === rightHash;
177
+ }
178
+ /**
179
+ * 大 bundle 流式路径:bundle 直接从 ZIP entry 落盘后传入 native,
180
+ * 避免 old/new/restored 的整块 Buffer 副本。HBC 变换 T/T⁻¹ 也在文件上
181
+ * 按段定位改写(transformHbcFile),全程不持有整个 bundle。native 已
182
+ * 自校验的普通产物不再重复 patch;自定义/变换路径仍保留 round-trip 校验。
183
+ */
184
+ async function buildStreamBundlePatch(originSource, newSource, options, workRoot) {
185
+ const diffStreamFn = options.diffStreamFn;
186
+ const patchStreamFn = options.patchStreamFn;
187
+ if (!diffStreamFn || !patchStreamFn) {
188
+ throw new Error('stream diff requires diffStream/patchStream from node-hdiffpatch');
189
+ }
190
+ const tempRoot = fs.mkdtempSync(path_1.default.join(workRoot, 'stream-'));
191
+ const inputBytes = bundleSourceSize(originSource) + bundleSourceSize(newSource);
192
+ const extractionDurationMs = (originSource.kind === 'file' ? originSource.materializeDurationMs : 0) +
193
+ (newSource.kind === 'file' ? newSource.materializeDurationMs : 0);
194
+ let phaseStartedAt = performance.now();
195
+ const rawOldFile = await bundleSourceFile(originSource, path_1.default.join(tempRoot, 'old.bin'));
196
+ const rawNewFile = await bundleSourceFile(newSource, path_1.default.join(tempRoot, 'new.bin'));
197
+ // 变换走文件版 T:只读头部选布局、按段定位改写,不把 bundle 读进内存
198
+ const transformedOldFile = path_1.default.join(tempRoot, 'old-transformed.bin');
199
+ const transformedNewFile = path_1.default.join(tempRoot, 'new-transformed.bin');
200
+ const pair = options.hbcTransform
201
+ ? await (0, hbcTransform_1.tryTransformPairFiles)(rawOldFile, rawNewFile, transformedOldFile, transformedNewFile)
202
+ : null;
203
+ reportDiffPhase(options, {
204
+ phase: 'materialize',
205
+ durationMs: extractionDurationMs + performance.now() - phaseStartedAt,
206
+ inputBytes,
207
+ });
208
+ // 与内存路径同一约束:变换候选连同元数据开销必须比 baseline 更小才采用,
209
+ // 因此变换命中时两个候选都要生成再比较。
210
+ phaseStartedAt = performance.now();
211
+ const rawPatchFile = path_1.default.join(tempRoot, 'patch-raw.bin');
212
+ await diffStreamFn(rawOldFile, rawNewFile, rawPatchFile);
213
+ let patchFile = rawPatchFile;
214
+ let patchBytes = (await fs.stat(rawPatchFile)).size;
215
+ let chosenPair = null;
216
+ if (pair) {
217
+ const transformedPatchFile = path_1.default.join(tempRoot, 'patch-transformed.bin');
218
+ await diffStreamFn(transformedOldFile, transformedNewFile, transformedPatchFile);
219
+ const transformedPatchBytes = (await fs.stat(transformedPatchFile)).size;
220
+ const metaOverhead = Buffer.byteLength(JSON.stringify(pair.meta));
221
+ if (transformedPatchBytes + metaOverhead < patchBytes) {
222
+ patchFile = transformedPatchFile;
223
+ patchBytes = transformedPatchBytes;
224
+ chosenPair = pair;
225
+ }
226
+ }
227
+ reportDiffPhase(options, {
228
+ phase: 'diff',
229
+ durationMs: performance.now() - phaseStartedAt,
230
+ inputBytes,
231
+ outputBytes: patchBytes,
232
+ });
233
+ // 变换路径必须验证 T⁻¹ 和元数据;未声明自校验的自定义 diff 也保留
234
+ // round-trip。node-hdiffpatch native 普通路径已经在返回前完整验证。
235
+ if (chosenPair || !options.streamOutputVerified) {
236
+ phaseStartedAt = performance.now();
237
+ const restoredFile = path_1.default.join(tempRoot, 'restored.bin');
238
+ await patchStreamFn(chosenPair ? transformedOldFile : rawOldFile, patchFile, restoredFile);
239
+ if (chosenPair) {
240
+ // T⁻¹ 同样在文件上按段改写,再以流式哈希比对,内存与 bundle 大小无关
241
+ const restoredRawFile = path_1.default.join(tempRoot, 'restored-raw.bin');
242
+ const inverted = await (0, hbcTransform_1.transformHbcFile)(restoredFile, restoredRawFile, chosenPair.layout, true);
243
+ if (!inverted || !(await filesEqual(restoredRawFile, rawNewFile))) {
244
+ throw new Error('stream diff round-trip verification failed');
245
+ }
246
+ }
247
+ else if (!(await filesEqual(restoredFile, rawNewFile))) {
248
+ throw new Error('stream diff round-trip verification failed');
249
+ }
250
+ reportDiffPhase(options, {
251
+ phase: 'verify',
252
+ durationMs: performance.now() - phaseStartedAt,
253
+ inputBytes,
254
+ outputBytes: patchBytes,
255
+ });
256
+ }
257
+ else {
258
+ reportDiffPhase(options, {
259
+ phase: 'verify',
260
+ durationMs: 0,
261
+ inputBytes,
262
+ outputBytes: patchBytes,
263
+ skipped: true,
264
+ });
265
+ }
266
+ return {
267
+ patch: { kind: 'file', path: patchFile },
268
+ ...(chosenPair ? { hbcTransform: chosenPair.meta } : {}),
269
+ };
270
+ }
271
+ /**
272
+ * 生成 bundle patch:默认现状路径;开启 hbcTransform 时对 Hermes bundle
273
+ * 做双候选择优(baseline vs 变换域 + 元数据开销),且变换候选必须通过
274
+ * 本地 round-trip 还原自检才会被采用——收益永不为负,未知格式天然安全。
275
+ */
276
+ async function buildBundlePatch(diffFn, originSource, newSource, options, workRoot) {
277
+ if (options.streamThresholdBytes &&
278
+ originSource &&
279
+ Math.max(bundleSourceSize(originSource), bundleSourceSize(newSource)) >=
280
+ options.streamThresholdBytes) {
281
+ return buildStreamBundlePatch(originSource, newSource, options, workRoot);
282
+ }
283
+ const [originBuffer, newBuffer] = await Promise.all([
284
+ originSource
285
+ ? bundleSourceBuffer(originSource)
286
+ : Promise.resolve(undefined),
287
+ bundleSourceBuffer(newSource),
288
+ ]);
289
+ const baseline = await diffFn(originBuffer, newBuffer);
290
+ if (!options.hbcTransform ||
291
+ !options.patchFn ||
292
+ !originBuffer ||
293
+ baseline.length <
294
+ (options.minBaselineBytes ?? HBC_TRANSFORM_MIN_BASELINE_BYTES)) {
295
+ return { patch: { kind: 'buffer', data: baseline } };
296
+ }
297
+ const pair = (0, hbcTransform_1.tryTransformPair)(originBuffer, newBuffer);
298
+ if (!pair) {
299
+ return { patch: { kind: 'buffer', data: baseline } };
300
+ }
301
+ try {
302
+ const candidate = await diffFn(pair.tOld, pair.tNew);
303
+ const metaOverhead = Buffer.byteLength(JSON.stringify(pair.meta));
304
+ if (candidate.length + metaOverhead >= baseline.length) {
305
+ return { patch: { kind: 'buffer', data: baseline } };
306
+ }
307
+ // patched 是 patchFn 新返回的 Buffer,原地做 T⁻¹ 省去一次整块拷贝
308
+ const patched = options.patchFn(pair.tOld, candidate);
309
+ if (!(0, hbcTransform_1.transformHbcWithLayoutInPlace)(patched, pair.layout, true) ||
310
+ Buffer.compare(patched, newBuffer) !== 0) {
311
+ console.warn((0, i18n_1.t)('hbcTransformRoundTripFailed'));
312
+ return { patch: { kind: 'buffer', data: baseline } };
313
+ }
314
+ return {
315
+ patch: { kind: 'buffer', data: candidate },
316
+ hbcTransform: pair.meta,
317
+ };
318
+ }
319
+ catch {
320
+ return { patch: { kind: 'buffer', data: baseline } };
321
+ }
322
+ }
323
+ async function addBundlePatch(zipfile, entry, nextZipfile, diffFn, originSource, bundleOptions, hbcTransformMetas, workRoot) {
324
+ const shouldStream = !!bundleOptions.streamThresholdBytes &&
325
+ !!originSource &&
326
+ Math.max(bundleSourceSize(originSource), entry.uncompressedSize) >=
327
+ bundleOptions.streamThresholdBytes;
328
+ let newSource;
329
+ if (shouldStream) {
330
+ const nextRoot = fs.mkdtempSync(path_1.default.join(workRoot, 'next-'));
331
+ const nextPath = path_1.default.join(nextRoot, 'bundle.bin');
332
+ const startedAt = performance.now();
333
+ await (0, zip_entries_1.writeEntry)(entry, nextZipfile, nextPath);
334
+ newSource = {
335
+ kind: 'file',
336
+ path: nextPath,
337
+ size: entry.uncompressedSize,
338
+ materializeDurationMs: performance.now() - startedAt,
339
+ };
340
+ }
341
+ else {
342
+ newSource = { kind: 'buffer', data: await (0, zip_entries_1.readEntry)(entry, nextZipfile) };
343
+ }
344
+ const { patch, hbcTransform } = await buildBundlePatch(diffFn, originSource, newSource, bundleOptions, workRoot);
345
+ const patchEntryName = `${entry.fileName}.patch`;
346
+ if (hbcTransform) {
347
+ hbcTransformMetas[patchEntryName] = hbcTransform;
348
+ }
349
+ if (patch.kind === 'file') {
350
+ zipfile.addFile(patch.path, patchEntryName, (0, zip_options_1.zipOptionsForPatchEntry)());
351
+ }
352
+ else {
353
+ zipfile.addBuffer(patch.data, patchEntryName, (0, zip_options_1.zipOptionsForPatchEntry)());
354
+ }
355
+ }
356
+ async function readOriginBundleSource(entry, zipFile, bundleOptions, workRoot) {
357
+ if (!bundleOptions.streamThresholdBytes) {
358
+ return { kind: 'buffer', data: await (0, zip_entries_1.readEntry)(entry, zipFile) };
359
+ }
360
+ const originRoot = fs.mkdtempSync(path_1.default.join(workRoot, 'origin-'));
361
+ const originPath = path_1.default.join(originRoot, 'bundle.bin');
362
+ const startedAt = performance.now();
363
+ await (0, zip_entries_1.writeEntry)(entry, zipFile, originPath);
364
+ return {
365
+ kind: 'file',
366
+ path: originPath,
367
+ size: entry.uncompressedSize,
368
+ materializeDurationMs: performance.now() - startedAt,
369
+ };
370
+ }
371
+ async function addFileFromZipEntry(zipfile, entry, nextZipfile) {
372
+ const entryPrefix = await (0, zip_entries_1.readEntryPrefix)(entry, nextZipfile, zip_options_1.ZIP_ENTRY_SNIFF_BYTES);
373
+ await new Promise((resolve, reject) => {
374
+ nextZipfile.openReadStream(entry, (err, readStream) => {
375
+ if (err) {
376
+ return reject(err);
377
+ }
378
+ if (!readStream) {
379
+ return reject(new Error(`Unable to read zip entry: ${entry.fileName}`));
380
+ }
381
+ zipfile.addReadStream(readStream, entry.fileName, (0, zip_options_1.zipOptionsForPayloadEntry)(entry.fileName, entryPrefix));
382
+ readStream.on('error', reject);
383
+ readStream.on('end', () => {
384
+ resolve(void 0);
385
+ });
386
+ });
387
+ });
388
+ }
389
+ function finishDiffZip(zipfile, writePromise, manifest) {
390
+ const diffManifest = Buffer.from(JSON.stringify(manifest));
391
+ zipfile.addBuffer(diffManifest, '__diff.json', (0, zip_options_1.zipOptionsForManifestEntry)(diffManifest.length));
392
+ zipfile.end();
393
+ return writePromise;
394
+ }
395
+ async function diffFromPPK(origin, next, output, diffFn, bundleOptions, workRoot) {
396
+ const originEntries = {};
397
+ const originMap = {};
398
+ let originSource;
399
+ await (0, zip_entries_1.enumZipEntries)(origin, async (entry, zipFile) => {
400
+ originEntries[entry.fileName] = entry;
401
+ if (!/\/$/.test(entry.fileName)) {
402
+ // isFile
403
+ originMap[zipEntryContentKey(entry)] = entry.fileName;
404
+ if ((0, constants_1.isPPKBundleFileName)(entry.fileName)) {
405
+ // This is source.
406
+ originSource = await readOriginBundleSource(entry, zipFile, bundleOptions, workRoot);
407
+ }
408
+ }
409
+ });
410
+ if (!originSource) {
411
+ throw new Error((0, i18n_1.t)('bundleFileNotFound'));
412
+ }
413
+ const copies = {};
414
+ const bundleSource = originSource;
415
+ await writeDiffZip(output, async (zipfile) => {
416
+ const addedEntry = {};
417
+ function addEntry(fn) {
418
+ //console.log(fn);
419
+ if (!fn || addedEntry[fn]) {
420
+ return;
421
+ }
422
+ addedEntry[fn] = true;
423
+ const base = basename(fn);
424
+ if (base) {
425
+ addEntry(base);
426
+ }
427
+ zipfile.addEmptyDirectory(fn);
428
+ }
429
+ const newEntries = {};
430
+ const hbcTransformMetas = {};
431
+ await (0, zip_entries_1.enumZipEntries)(next, async (entry, nextZipfile) => {
432
+ newEntries[entry.fileName] = entry;
433
+ if (/\/$/.test(entry.fileName)) {
434
+ // Directory
435
+ if (!originEntries[entry.fileName]) {
436
+ addEntry(entry.fileName);
437
+ }
438
+ }
439
+ else if ((0, constants_1.isPPKBundleFileName)(entry.fileName)) {
440
+ await addBundlePatch(zipfile, entry, nextZipfile, diffFn, bundleSource, bundleOptions, hbcTransformMetas, workRoot);
441
+ }
442
+ else {
443
+ // If same file.
444
+ const originEntry = originEntries[entry.fileName];
445
+ if (originEntry && zipEntriesHaveSameContent(originEntry, entry)) {
446
+ // ignore
447
+ return;
448
+ }
449
+ // If moved from other place
450
+ const movedFrom = originMap[zipEntryContentKey(entry)];
451
+ if (movedFrom) {
452
+ const base = basename(entry.fileName);
453
+ if (base && !originEntries[base]) {
454
+ addEntry(base);
455
+ }
456
+ copies[entry.fileName] = movedFrom;
457
+ return;
458
+ }
459
+ // New file.
460
+ const basePath = basename(entry.fileName);
461
+ if (basePath) {
462
+ addEntry(basePath);
463
+ }
464
+ await addFileFromZipEntry(zipfile, entry, nextZipfile);
465
+ }
466
+ });
467
+ const deletes = {};
468
+ for (const k in originEntries) {
469
+ if (!newEntries[k]) {
470
+ console.log((0, i18n_1.t)('deleteFile', { file: k }));
471
+ deletes[k] = 1;
472
+ }
473
+ }
474
+ return {
475
+ copies,
476
+ deletes,
477
+ ...(Object.keys(hbcTransformMetas).length
478
+ ? { hbcTransform: hbcTransformMetas }
479
+ : {}),
480
+ };
481
+ });
482
+ }
483
+ async function diffFromPackage(origin, next, output, diffFn, platform, bundleOptions, workRoot) {
484
+ // 路径 → 内容键(crc32 + 解压后长度)
485
+ const originEntries = {};
486
+ const originMap = {};
487
+ let originSource;
488
+ // Content checksum (CRC32) for every copied entry, keyed by target path.
489
+ // Two consumers on the client: locating a file by content when its origin
490
+ // path is not present verbatim on device (APK baseline diff applied on an
491
+ // AAB/split-apk install whose res/ paths were shortened), and verifying
492
+ // that a path-matched file still has the expected bytes before copying
493
+ // (BUNDLEHASH_MIGRATION.md §4.2.1 — a rebuilt binary may keep the path but
494
+ // drift the content; mismatch must fall back to full instead of silently
495
+ // installing a wrong resource).
496
+ const copiesCrc = {};
497
+ await (0, zip_entries_1.enumZipEntries)(origin, async (entry, zipFile) => {
498
+ if (!/\/$/.test(entry.fileName)) {
499
+ const resolvedEntry = (0, native_package_1.resolveNativePackageEntry)(platform, entry.fileName);
500
+ if (!resolvedEntry) {
501
+ return;
502
+ }
503
+ const fn = resolvedEntry.diffPath;
504
+ //console.log(fn);
505
+ // isFile
506
+ const contentKey = zipEntryContentKey(entry);
507
+ originEntries[fn] = contentKey;
508
+ originMap[contentKey] = fn;
509
+ if (resolvedEntry.kind === 'bundle') {
510
+ // This is source.
511
+ originSource = await readOriginBundleSource(entry, zipFile, bundleOptions, workRoot);
512
+ }
513
+ }
514
+ });
515
+ if (!originSource) {
516
+ throw new Error((0, i18n_1.t)('bundleFileNotFound'));
517
+ }
518
+ const copies = {};
519
+ const bundleSource = originSource;
520
+ await writeDiffZip(output, async (zipfile) => {
521
+ const hbcTransformMetas = {};
522
+ await (0, zip_entries_1.enumZipEntries)(next, async (entry, nextZipfile) => {
523
+ if (/\/$/.test(entry.fileName)) {
524
+ // Directory
525
+ zipfile.addEmptyDirectory(entry.fileName);
526
+ }
527
+ else if ((0, constants_1.isPPKBundleFileName)(entry.fileName)) {
528
+ await addBundlePatch(zipfile, entry, nextZipfile, diffFn, bundleSource, bundleOptions, hbcTransformMetas, workRoot);
529
+ }
530
+ else {
531
+ // If same file.
532
+ const contentKey = zipEntryContentKey(entry);
533
+ if (originEntries[entry.fileName] === contentKey) {
534
+ copies[entry.fileName] = '';
535
+ copiesCrc[entry.fileName] = entry.crc32;
536
+ return;
537
+ }
538
+ // If moved from other place
539
+ const movedFrom = originMap[contentKey];
540
+ if (movedFrom) {
541
+ copies[entry.fileName] = movedFrom;
542
+ copiesCrc[entry.fileName] = entry.crc32;
543
+ return;
544
+ }
545
+ await addFileFromZipEntry(zipfile, entry, nextZipfile);
546
+ }
547
+ });
548
+ return {
549
+ copies,
550
+ copiesCrc,
551
+ ...(Object.keys(hbcTransformMetas).length
552
+ ? { hbcTransform: hbcTransformMetas }
553
+ : {}),
554
+ };
555
+ });
556
+ }
557
+ function resolveDiffImplementation(options) {
558
+ if (options.customDiff) {
559
+ return options.customDiff;
560
+ }
561
+ const hdiffModule = options.customHdiffModule ?? hdiff;
562
+ if (!hdiffModule?.diff) {
563
+ throw new Error((0, i18n_1.t)('nodeHdiffpatchRequired', { scriptName: constants_1.scriptName }));
564
+ }
565
+ return hdiffModule.diff;
566
+ }
567
+ // round-trip 自检需要 patch 能力;拿不到时禁用变换(安全优先),不报错
568
+ function resolvePatchImplementation(options) {
569
+ if (options.customPatch) {
570
+ return options.customPatch;
571
+ }
572
+ const hdiffModule = options.customHdiffModule ?? hdiff;
573
+ return hdiffModule?.patch;
574
+ }
575
+ function resolveBundleDiffOptions(options) {
576
+ const hdiffModule = options.customHdiffModule ?? hdiff;
577
+ const streamOptions = {};
578
+ const observerOptions = typeof options.onDiffPhase === 'function'
579
+ ? { onDiffPhase: options.onDiffPhase }
580
+ : {};
581
+ if (typeof options.bundleStreamThreshold === 'number' &&
582
+ options.bundleStreamThreshold > 0) {
583
+ const singleDiffStreamFn = options.customDiffSingleStream ?? hdiffModule?.diffSingleStream;
584
+ const singlePatchStreamFn = options.customPatchSingleStream ?? hdiffModule?.patchSingleStream;
585
+ const diffStreamFn = options.customDiffStream ?? hdiffModule?.diffStream;
586
+ const patchStreamFn = options.customPatchStream ?? hdiffModule?.patchStream;
587
+ if (singleDiffStreamFn && singlePatchStreamFn) {
588
+ streamOptions.streamThresholdBytes = options.bundleStreamThreshold;
589
+ streamOptions.diffStreamFn = singleDiffStreamFn;
590
+ streamOptions.patchStreamFn = singlePatchStreamFn;
591
+ streamOptions.streamEmitsSingleFormat = true;
592
+ streamOptions.streamOutputVerified = options.customDiffSingleStream
593
+ ? options.customDiffSingleStreamVerified === true
594
+ : hdiffModule?.capabilities?.diffSingleStreamVerifiesOutput === true;
595
+ }
596
+ else if (diffStreamFn && patchStreamFn) {
597
+ streamOptions.streamThresholdBytes = options.bundleStreamThreshold;
598
+ streamOptions.diffStreamFn = diffStreamFn;
599
+ streamOptions.patchStreamFn = patchStreamFn;
600
+ streamOptions.streamEmitsSingleFormat = false;
601
+ streamOptions.streamOutputVerified = options.customDiffStream
602
+ ? options.customDiffStreamVerified === true
603
+ : hdiffModule?.capabilities?.diffStreamVerifiesOutput === true;
604
+ }
605
+ else {
606
+ console.warn((0, i18n_1.t)('bundleStreamNeedsHdiffpatch'));
607
+ }
608
+ }
609
+ if (!options.hbcTransform) {
610
+ return { ...observerOptions, ...streamOptions };
611
+ }
612
+ const patchFn = resolvePatchImplementation(options);
613
+ if (!patchFn) {
614
+ console.warn((0, i18n_1.t)('hbcTransformNeedsPatch'));
615
+ return { ...observerOptions, ...streamOptions };
616
+ }
617
+ return {
618
+ hbcTransform: true,
619
+ patchFn,
620
+ ...observerOptions,
621
+ ...streamOptions,
622
+ ...(typeof options.hbcTransformMinBaseline === 'number'
623
+ ? { minBaselineBytes: options.hbcTransformMinBaseline }
624
+ : {}),
625
+ };
626
+ }
627
+ function diffArgsCheck(args, options, diffFnName) {
628
+ const [origin, next] = args;
629
+ if (!origin || !next) {
630
+ throw new Error((0, i18n_1.t)('usageDiff', { command: diffFnName }));
631
+ }
632
+ const diffFn = resolveDiffImplementation(options);
633
+ const { output } = (0, utils_1.translateOptions)({
634
+ ...options,
635
+ tempDir: constants_1.tempDir,
636
+ });
637
+ if (typeof output !== 'string') {
638
+ throw new Error((0, i18n_1.t)('outputPathRequired'));
639
+ }
640
+ return {
641
+ origin,
642
+ next,
643
+ diffFn,
644
+ realOutput: output.replace(/\$\{time\}/g, `${Date.now()}`),
645
+ };
646
+ }
647
+ const createDiffCommand = ({ diffFnName, target }) => async ({ args, options }) => {
648
+ const { origin, next, realOutput, diffFn } = diffArgsCheck(args, options, diffFnName);
649
+ const bundleOptions = resolveBundleDiffOptions(options);
650
+ const workRoot = fs.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'rnu-bundle-diff-'));
651
+ try {
652
+ if (target.kind === 'ppk') {
653
+ await diffFromPPK(origin, next, realOutput, diffFn, bundleOptions, workRoot);
654
+ }
655
+ else {
656
+ await diffFromPackage(origin, next, realOutput, diffFn, target.platform, bundleOptions, workRoot);
657
+ }
658
+ }
659
+ finally {
660
+ fs.rmSync(workRoot, { recursive: true, force: true });
661
+ }
662
+ console.log((0, i18n_1.t)('diffPackageGenerated', { output: realOutput }));
663
+ };
664
+ exports.diffCommands = {
665
+ hdiff: createDiffCommand({
666
+ diffFnName: 'hdiff',
667
+ target: { kind: 'ppk' },
668
+ }),
669
+ hdiffFromApk: createDiffCommand({
670
+ diffFnName: 'hdiffFromApk',
671
+ target: {
672
+ kind: 'package',
673
+ platform: 'android',
674
+ },
675
+ }),
676
+ hdiffFromApp: createDiffCommand({
677
+ diffFnName: 'hdiffFromApp',
678
+ target: {
679
+ kind: 'package',
680
+ platform: 'harmony',
681
+ },
682
+ }),
683
+ hdiffFromIpa: createDiffCommand({
684
+ diffFnName: 'hdiffFromIpa',
685
+ target: {
686
+ kind: 'package',
687
+ platform: 'ios',
688
+ },
689
+ }),
690
+ };