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
@@ -0,0 +1,1287 @@
1
+ "use strict";
2
+ /**
3
+ * [INPUT]: 依赖 Node 子进程/文件流/HTTP Range、Pakta CLI 临时目录与 ZIP 读取原语
4
+ * [OUTPUT]: 对外提供 Hermes base 选择、缓存、二进制字面量/指令等价校验、下载、清理及发布元数据能力
5
+ * [POS]: CLI Hermes delta 优化边界,所有失败降级到普通编译,不阻塞发布流程
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ var __importDefault = (this && this.__importDefault) || function (mod) {
41
+ return (mod && mod.__esModule) ? mod : { "default": mod };
42
+ };
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.LITERAL_SEPARATOR = exports.DEFAULT_CACHE_MAX_FILES = exports.DEFAULT_CACHE_MAX_BYTES = exports.BUNDLE_ENTRY_NAMES = exports.MIN_HERMES_COMPILER_COMMITLY_DATE = exports.MIN_HERMES_COMPILER_BUILD = exports.HERMES_BASE_DETAIL_MAX_CHARS = exports.BundleHashMismatchError = exports.PermanentBaseError = void 0;
45
+ exports.classifyHermesCommand = classifyHermesCommand;
46
+ exports.sha256Hex = sha256Hex;
47
+ exports.sha256File = sha256File;
48
+ exports.readFileHbcVersion = readFileHbcVersion;
49
+ exports.probeHbcVersion = probeHbcVersion;
50
+ exports.bundleEntryMatcher = bundleEntryMatcher;
51
+ exports.extractBundleFromArchive = extractBundleFromArchive;
52
+ exports.cacheDir = cacheDir;
53
+ exports.tmpDir = tmpDir;
54
+ exports.cacheLimits = cacheLimits;
55
+ exports.cacheLookup = cacheLookup;
56
+ exports.cachePut = cachePut;
57
+ exports.enforceCacheLimits = enforceCacheLimits;
58
+ exports.cleanCache = cleanCache;
59
+ exports.cacheStats = cacheStats;
60
+ exports.cleanStaleTmp = cleanStaleTmp;
61
+ exports.downloadToFile = downloadToFile;
62
+ exports.fetchBaseBundle = fetchBaseBundle;
63
+ exports.resolveHermesBase = resolveHermesBase;
64
+ exports.hermescArgsWithBase = hermescArgsWithBase;
65
+ exports.hermesBaseMeta = hermesBaseMeta;
66
+ exports.truncateHermesBaseDetail = truncateHermesBaseDetail;
67
+ exports.normalizeDisassemblyLine = normalizeDisassemblyLine;
68
+ exports.compareHermesBytecode = compareHermesBytecode;
69
+ exports.verifyHermesBaseEquivalence = verifyHermesBaseEquivalence;
70
+ // Hermes `-base-bytecode` (delta optimizing mode) support.
71
+ //
72
+ // Hermes re-sorts its string table by identifier frequency on every compile, so
73
+ // any source change renumbers most string IDs and the bytecode diff explodes
74
+ // (see hermes-base-bytecode-方案.md). Compiling with the previous HBC of the
75
+ // same app as `-base-bytecode` keeps old IDs stable and shrinks patches 5-30×.
76
+ // This module resolves that base: gating by hermesc build, HBC version probe,
77
+ // server lookup, a sha256-named local cache, download + verification, and the
78
+ // optional disassembly equivalence check. Every failure degrades to the plain
79
+ // compile — nothing here may block a release.
80
+ const child_process_1 = require("child_process");
81
+ const crypto_1 = require("crypto");
82
+ const fs_extra_1 = __importDefault(require("fs-extra"));
83
+ const os_1 = __importDefault(require("os"));
84
+ const path_1 = __importDefault(require("path"));
85
+ const stream_1 = require("stream");
86
+ const promises_1 = require("stream/promises");
87
+ const constants_1 = require("./constants");
88
+ const hbcTransform_1 = require("./hbcTransform");
89
+ const hermes_literals_1 = require("./hermes-literals");
90
+ const i18n_1 = require("./i18n");
91
+ const runtime_1 = require("./runtime");
92
+ const zip_entries_1 = require("./zip-entries");
93
+ const zip_range_1 = require("./zip-range");
94
+ /**
95
+ * The base cannot be obtained whatever the transport or how often it is
96
+ * retried (wrong content on the server, no bundle in the package, ...).
97
+ */
98
+ class PermanentBaseError extends Error {
99
+ }
100
+ exports.PermanentBaseError = PermanentBaseError;
101
+ /** The fetched bundle's sha256 differs from what the server recorded. */
102
+ class BundleHashMismatchError extends PermanentBaseError {
103
+ constructor(actual, expected) {
104
+ super(`bundleHash mismatch (${actual.slice(0, 12)} != ${expected.slice(0, 12)})`);
105
+ }
106
+ }
107
+ exports.BundleHashMismatchError = BundleHashMismatchError;
108
+ /** time allowed for a response to start (headers) */
109
+ const FETCH_HEADERS_TIMEOUT_MS = 30_000;
110
+ /** a download that receives nothing for this long is abandoned */
111
+ const STREAM_IDLE_TIMEOUT_MS = 60_000;
112
+ /** cachePut leftovers (`<hash>.<pid>.tmp`) older than this are evicted */
113
+ const STALE_CACHE_TMP_MS = 60 * 60 * 1000;
114
+ /** the server column is VARCHAR(500); it truncates too, this just saves bytes */
115
+ exports.HERMES_BASE_DETAIL_MAX_CHARS = 500;
116
+ // ---------------------------------------------------------------------------
117
+ // hermesc gating
118
+ // ---------------------------------------------------------------------------
119
+ /**
120
+ * Hermes V1 (static_h) builds before 2025-07-11 carried a delta-mode bug when a
121
+ * delta-produced base is reused; every released `hermes-compiler` is newer, and
122
+ * classic hermesc (HBC ≤96, react-native/sdks or hermes-engine) upgrades kinds
123
+ * in place and was never affected. Anything else is refused.
124
+ */
125
+ exports.MIN_HERMES_COMPILER_BUILD = 250829098;
126
+ exports.MIN_HERMES_COMPILER_COMMITLY_DATE = 20250901;
127
+ function classifyHermesCommand(hermesCommand) {
128
+ const normalized = hermesCommand.split(path_1.default.sep).join('/');
129
+ if (/\/react-native\/sdks\/hermesc\//.test(normalized) ||
130
+ /\/hermes-engine\//.test(normalized)) {
131
+ return { allowed: true, kind: 'classic' };
132
+ }
133
+ const match = /\/hermes-compiler\//.exec(normalized);
134
+ if (match) {
135
+ const packageRoot = normalized.slice(0, match.index + match[0].length);
136
+ let version = '';
137
+ try {
138
+ version = String(JSON.parse(fs_extra_1.default.readFileSync(path_1.default.join(packageRoot, 'package.json'), 'utf8')).version ?? '');
139
+ }
140
+ catch {
141
+ return {
142
+ allowed: false,
143
+ kind: 'hermes-compiler',
144
+ reason: 'unreadable hermes-compiler version',
145
+ };
146
+ }
147
+ const build = /^(\d{9})\./.exec(version);
148
+ if (build) {
149
+ const ok = Number(build[1]) >= exports.MIN_HERMES_COMPILER_BUILD;
150
+ return {
151
+ allowed: ok,
152
+ kind: 'hermes-compiler',
153
+ version,
154
+ reason: ok
155
+ ? undefined
156
+ : `hermes-compiler ${version} predates the delta-mode fix`,
157
+ };
158
+ }
159
+ const commitly = /^0\.(\d+)\.\d+-commitly-(\d{8})/.exec(version);
160
+ if (commitly) {
161
+ const ok = Number(commitly[1]) >= 14 &&
162
+ Number(commitly[2]) >= exports.MIN_HERMES_COMPILER_COMMITLY_DATE;
163
+ return {
164
+ allowed: ok,
165
+ kind: 'hermes-compiler',
166
+ version,
167
+ reason: ok
168
+ ? undefined
169
+ : `hermes-compiler ${version} predates the delta-mode fix`,
170
+ };
171
+ }
172
+ const stable = /^0\.(\d+)\.\d+$/.exec(version);
173
+ if (stable) {
174
+ const ok = Number(stable[1]) >= 14;
175
+ return {
176
+ allowed: ok,
177
+ kind: 'hermes-compiler',
178
+ version,
179
+ reason: ok
180
+ ? undefined
181
+ : `hermes-compiler ${version} predates the delta-mode fix`,
182
+ };
183
+ }
184
+ return {
185
+ allowed: false,
186
+ kind: 'hermes-compiler',
187
+ version,
188
+ reason: `unrecognized hermes-compiler version ${version}`,
189
+ };
190
+ }
191
+ return {
192
+ allowed: false,
193
+ kind: 'unknown',
194
+ reason: 'hermesc location not recognized',
195
+ };
196
+ }
197
+ // ---------------------------------------------------------------------------
198
+ // HBC helpers
199
+ // ---------------------------------------------------------------------------
200
+ function sha256Hex(data) {
201
+ return (0, crypto_1.createHash)('sha256').update(data).digest('hex');
202
+ }
203
+ /** sha256 of a file, streamed (a cached base can be tens of MB) */
204
+ async function sha256File(file) {
205
+ const hash = (0, crypto_1.createHash)('sha256');
206
+ for await (const chunk of fs_extra_1.default.createReadStream(file))
207
+ hash.update(chunk);
208
+ return hash.digest('hex');
209
+ }
210
+ /** HBC version from the first bytes of a file, without reading the rest */
211
+ async function readFileHbcVersion(file) {
212
+ const header = Buffer.alloc(128);
213
+ const fd = await fs_extra_1.default.open(file, 'r');
214
+ try {
215
+ const { bytesRead } = await fs_extra_1.default.read(fd, header, 0, 128, 0);
216
+ return (0, hbcTransform_1.getHbcVersion)(header.subarray(0, bytesRead));
217
+ }
218
+ catch {
219
+ return null;
220
+ }
221
+ finally {
222
+ await fs_extra_1.default.close(fd);
223
+ }
224
+ }
225
+ const PROBE_CACHE_FILE = 'hbc-versions.json';
226
+ const PROBE_CACHE_ENTRIES = 16;
227
+ /** identity of a hermesc binary for the probe cache: path + size + mtime */
228
+ function probeCacheKey(hermesCommand) {
229
+ try {
230
+ const stat = fs_extra_1.default.statSync(hermesCommand);
231
+ return `${path_1.default.resolve(hermesCommand)}|${stat.size}|${Math.floor(stat.mtimeMs)}`;
232
+ }
233
+ catch {
234
+ return null;
235
+ }
236
+ }
237
+ function readProbeCache() {
238
+ try {
239
+ const parsed = JSON.parse(fs_extra_1.default.readFileSync(path_1.default.join(cacheDir(), PROBE_CACHE_FILE), 'utf8'));
240
+ return parsed && typeof parsed === 'object' ? parsed : {};
241
+ }
242
+ catch {
243
+ return {};
244
+ }
245
+ }
246
+ function writeProbeCache(entries) {
247
+ try {
248
+ const keys = Object.keys(entries);
249
+ // keep the most recently inserted entries only
250
+ const trimmed = Object.fromEntries(keys.slice(-PROBE_CACHE_ENTRIES).map((key) => [key, entries[key]]));
251
+ fs_extra_1.default.ensureDirSync(cacheDir());
252
+ fs_extra_1.default.writeFileSync(path_1.default.join(cacheDir(), PROBE_CACHE_FILE), JSON.stringify(trimmed));
253
+ }
254
+ catch {
255
+ // the cache is a convenience; probing again next time is fine
256
+ }
257
+ }
258
+ /**
259
+ * Which HBC version this hermesc emits. The answer is remembered per binary
260
+ * (path, size, mtime) in the cache dir so only the first run of a given
261
+ * hermesc pays for the empty-file compile.
262
+ */
263
+ function probeHbcVersion(hermesCommand) {
264
+ const key = probeCacheKey(hermesCommand);
265
+ if (key) {
266
+ const cached = readProbeCache()[key];
267
+ if (Number.isInteger(cached) && cached > 0)
268
+ return cached;
269
+ }
270
+ const version = compileProbe(hermesCommand);
271
+ if (key && version !== null) {
272
+ const { [key]: _stale, ...others } = readProbeCache();
273
+ writeProbeCache({ ...others, [key]: version });
274
+ }
275
+ return version;
276
+ }
277
+ /** Compile an empty file to learn which HBC version this hermesc emits. */
278
+ function compileProbe(hermesCommand) {
279
+ const dir = fs_extra_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'rnu-hermes-probe-'));
280
+ try {
281
+ const input = path_1.default.join(dir, 'probe.js');
282
+ const output = path_1.default.join(dir, 'probe.hbc');
283
+ fs_extra_1.default.writeFileSync(input, '');
284
+ const result = (0, child_process_1.spawnSync)(hermesCommand, ['-emit-binary', '-out', output, input, '-O', '-w'], { stdio: 'ignore' });
285
+ if (result.status !== 0 || !fs_extra_1.default.existsSync(output)) {
286
+ return null;
287
+ }
288
+ const header = Buffer.alloc(128);
289
+ const fd = fs_extra_1.default.openSync(output, 'r');
290
+ try {
291
+ fs_extra_1.default.readSync(fd, header, 0, 128, 0);
292
+ }
293
+ finally {
294
+ fs_extra_1.default.closeSync(fd);
295
+ }
296
+ return (0, hbcTransform_1.getHbcVersion)(header);
297
+ }
298
+ catch {
299
+ return null;
300
+ }
301
+ finally {
302
+ fs_extra_1.default.removeSync(dir);
303
+ }
304
+ }
305
+ exports.BUNDLE_ENTRY_NAMES = [
306
+ 'index.bundlejs',
307
+ 'main.jsbundle',
308
+ 'bundle.harmony.js',
309
+ ];
310
+ const APK_BUNDLE = /^assets\/index\.android\.bundle$/;
311
+ const IPA_BUNDLE = /^payload\/[^/]+\.app\/main\.jsbundle$/i;
312
+ /** Which zip entry holds the JS/HBC bundle in each kind of artifact. */
313
+ function bundleEntryMatcher(artifactType) {
314
+ switch (artifactType) {
315
+ case 'apk':
316
+ return (name) => APK_BUNDLE.test(name);
317
+ case 'ipa':
318
+ return (name) => IPA_BUNDLE.test(name.toLowerCase());
319
+ default:
320
+ return (name) => exports.BUNDLE_ENTRY_NAMES.includes(name);
321
+ }
322
+ }
323
+ /**
324
+ * Extract the JS/HBC bundle from a local artifact: a raw bundle file, a ppk,
325
+ * or a native package (apk / ipa / harmony app). Returns null when not found.
326
+ */
327
+ async function extractBundleFromArchive(filePath) {
328
+ const ext = path_1.default.extname(filePath).toLowerCase();
329
+ if (ext === '.hbc' ||
330
+ ext === '.bundle' ||
331
+ ext === '.jsbundle' ||
332
+ ext === '.bundlejs' ||
333
+ ext === '.js') {
334
+ return fs_extra_1.default.readFile(filePath);
335
+ }
336
+ if (ext === '.ppk' || ext === '.apk' || ext === '.ipa') {
337
+ return readFirstZipEntry(filePath, bundleEntryMatcher(ext.slice(1)));
338
+ }
339
+ if (ext === '.app') {
340
+ // harmony .app wraps .hap archives; reuse the existing parser
341
+ const { default: AppInfoParser } = await Promise.resolve().then(() => __importStar(require('./app-info-parser')));
342
+ const parser = new AppInfoParser(filePath);
343
+ const [bundle] = await parser.parser.getEntriesFromHarmonyApp([
344
+ /^resources\/rawfile\/bundle\.harmony\.js$/,
345
+ ]);
346
+ return bundle ?? null;
347
+ }
348
+ // unknown extension: try as zip, then as raw file
349
+ const fromZip = await readFirstZipEntry(filePath, (name) => exports.BUNDLE_ENTRY_NAMES.includes(name)).catch(() => null);
350
+ return fromZip ?? fs_extra_1.default.readFile(filePath);
351
+ }
352
+ async function readFirstZipEntry(zipPath, matches) {
353
+ let found = null;
354
+ await (0, zip_entries_1.enumZipEntries)(zipPath, async (entry, zipFile) => {
355
+ if (!found && matches(entry.fileName)) {
356
+ found = await (0, zip_entries_1.readEntry)(entry, zipFile);
357
+ }
358
+ });
359
+ return found;
360
+ }
361
+ // ---------------------------------------------------------------------------
362
+ // cache (sha256-named bundles)
363
+ // ---------------------------------------------------------------------------
364
+ exports.DEFAULT_CACHE_MAX_BYTES = 500 * 1024 * 1024;
365
+ exports.DEFAULT_CACHE_MAX_FILES = 20;
366
+ function cacheDir() {
367
+ return process.env.PAKTA_CACHE_DIR || path_1.default.join(constants_1.tempDir, 'cache');
368
+ }
369
+ function tmpDir() {
370
+ return path_1.default.join(constants_1.tempDir, 'tmp');
371
+ }
372
+ function cacheLimits(overrideMaxMb) {
373
+ const envMb = Number(process.env.PAKTA_CACHE_MAX_MB);
374
+ const maxBytes = overrideMaxMb && overrideMaxMb > 0
375
+ ? overrideMaxMb * 1024 * 1024
376
+ : Number.isFinite(envMb) && envMb > 0
377
+ ? envMb * 1024 * 1024
378
+ : exports.DEFAULT_CACHE_MAX_BYTES;
379
+ return { maxBytes, maxFiles: exports.DEFAULT_CACHE_MAX_FILES };
380
+ }
381
+ /** Returns the cached bundle path when present and its sha256 still matches. */
382
+ async function cacheLookup(bundleHash) {
383
+ if (!/^[0-9a-f]{64}$/.test(bundleHash))
384
+ return null;
385
+ const file = path_1.default.join(cacheDir(), bundleHash);
386
+ if (!(await fs_extra_1.default.pathExists(file)))
387
+ return null;
388
+ if ((await sha256File(file).catch(() => '')) !== bundleHash) {
389
+ // corrupt entry: never trust cache content
390
+ await fs_extra_1.default.remove(file).catch(() => { });
391
+ return null;
392
+ }
393
+ const now = new Date();
394
+ await fs_extra_1.default.utimes(file, now, now).catch(() => { });
395
+ return file;
396
+ }
397
+ /** Store a bundle under its sha256 (LRU-evicting to the configured limits). */
398
+ async function cachePut(bundle, maxMb, knownHash) {
399
+ const bundleHash = knownHash ?? sha256Hex(bundle);
400
+ const dir = cacheDir();
401
+ await fs_extra_1.default.ensureDir(dir);
402
+ const file = path_1.default.join(dir, bundleHash);
403
+ if (!(await fs_extra_1.default.pathExists(file))) {
404
+ const tmp = `${file}.${process.pid}.tmp`;
405
+ await fs_extra_1.default.writeFile(tmp, bundle);
406
+ await fs_extra_1.default.rename(tmp, file);
407
+ }
408
+ else {
409
+ const now = new Date();
410
+ await fs_extra_1.default.utimes(file, now, now).catch(() => { });
411
+ }
412
+ await enforceCacheLimits(maxMb, bundleHash);
413
+ return { path: file, bundleHash };
414
+ }
415
+ async function enforceCacheLimits(maxMb, keep) {
416
+ const dir = cacheDir();
417
+ if (!(await fs_extra_1.default.pathExists(dir)))
418
+ return;
419
+ const { maxBytes, maxFiles } = cacheLimits(maxMb);
420
+ const entries = [];
421
+ const staleTmpCutoff = Date.now() - STALE_CACHE_TMP_MS;
422
+ for (const name of await fs_extra_1.default.readdir(dir)) {
423
+ const isTmp = isCacheTmpName(name);
424
+ if (!isTmp && !/^[0-9a-f]{64}$/.test(name))
425
+ continue;
426
+ const stat = await fs_extra_1.default.stat(path_1.default.join(dir, name)).catch(() => null);
427
+ if (!stat?.isFile())
428
+ continue;
429
+ if (isTmp) {
430
+ // an interrupted cachePut; nothing will ever rename it into place
431
+ if (stat.mtimeMs < staleTmpCutoff)
432
+ await fs_extra_1.default.remove(path_1.default.join(dir, name)).catch(() => { });
433
+ continue;
434
+ }
435
+ entries.push({ name, size: stat.size, mtime: stat.mtimeMs });
436
+ }
437
+ entries.sort((a, b) => b.mtime - a.mtime); // newest first
438
+ let total = 0;
439
+ let count = 0;
440
+ for (const entry of entries) {
441
+ total += entry.size;
442
+ count += 1;
443
+ if (entry.name !== keep && (total > maxBytes || count > maxFiles)) {
444
+ await fs_extra_1.default.remove(path_1.default.join(dir, entry.name)).catch(() => { });
445
+ }
446
+ }
447
+ }
448
+ /** `<sha256>.<pid>.tmp`, the staging name cachePut writes before renaming */
449
+ function isCacheTmpName(name) {
450
+ return /^[0-9a-f]{64}\.\d+\.tmp$/.test(name);
451
+ }
452
+ /** Remove every cached bundle (and staging leftovers); returns the bundle count. */
453
+ async function cleanCache() {
454
+ const dir = cacheDir();
455
+ if (!(await fs_extra_1.default.pathExists(dir)))
456
+ return 0;
457
+ let bundles = 0;
458
+ for (const name of await fs_extra_1.default.readdir(dir)) {
459
+ const isBundle = /^[0-9a-f]{64}$/.test(name);
460
+ if (!isBundle && !isCacheTmpName(name))
461
+ continue;
462
+ await fs_extra_1.default.remove(path_1.default.join(dir, name));
463
+ if (isBundle)
464
+ bundles += 1;
465
+ }
466
+ return bundles;
467
+ }
468
+ async function cacheStats() {
469
+ const dir = cacheDir();
470
+ let files = 0;
471
+ let bytes = 0;
472
+ if (await fs_extra_1.default.pathExists(dir)) {
473
+ for (const name of await fs_extra_1.default.readdir(dir)) {
474
+ if (!/^[0-9a-f]{64}$/.test(name))
475
+ continue;
476
+ const stat = await fs_extra_1.default.stat(path_1.default.join(dir, name)).catch(() => null);
477
+ if (stat?.isFile()) {
478
+ files += 1;
479
+ bytes += stat.size;
480
+ }
481
+ }
482
+ }
483
+ return { dir, files, bytes };
484
+ }
485
+ /** Remove leftovers of interrupted runs (older than 24h) from the tmp dir. */
486
+ async function cleanStaleTmp(maxAgeMs = 24 * 60 * 60 * 1000) {
487
+ const dir = tmpDir();
488
+ if (!(await fs_extra_1.default.pathExists(dir)))
489
+ return;
490
+ const cutoff = Date.now() - maxAgeMs;
491
+ for (const name of await fs_extra_1.default.readdir(dir)) {
492
+ const file = path_1.default.join(dir, name);
493
+ const stat = await fs_extra_1.default.stat(file).catch(() => null);
494
+ if (stat && stat.mtimeMs < cutoff)
495
+ await fs_extra_1.default.remove(file).catch(() => { });
496
+ }
497
+ }
498
+ // ---------------------------------------------------------------------------
499
+ // download
500
+ // ---------------------------------------------------------------------------
501
+ async function downloadToFile(url, destination) {
502
+ const controller = new AbortController();
503
+ const headersTimer = setTimeout(() => controller.abort(new Error('timeout waiting for response')), FETCH_HEADERS_TIMEOUT_MS);
504
+ let response;
505
+ try {
506
+ response = await (0, runtime_1.webFetch)(url, { signal: controller.signal });
507
+ }
508
+ finally {
509
+ clearTimeout(headersTimer);
510
+ }
511
+ if (!response.ok || !response.body) {
512
+ throw new Error(`download failed: HTTP ${response.status} ${url}`);
513
+ }
514
+ await saveResponse(response, destination, controller);
515
+ }
516
+ function hasBundleLocation(record) {
517
+ return (record.bundleOffset != null &&
518
+ record.bundleCompressedSize != null &&
519
+ record.bundleCompression != null &&
520
+ (record.bundleCompression === zip_range_1.ZIP_STORED ||
521
+ record.bundleCompression === zip_range_1.ZIP_DEFLATED));
522
+ }
523
+ /**
524
+ * Stream a response body to disk, giving up when no bytes arrive for
525
+ * STREAM_IDLE_TIMEOUT_MS. `controller` is the one the request was made with
526
+ * (aborting it also tears down the socket); a foreign response gets its own.
527
+ */
528
+ async function saveResponse(response, destination, controller = new AbortController()) {
529
+ if (!response.body)
530
+ throw new Error('empty response body');
531
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(destination));
532
+ const abort = () => controller.abort(new Error(`download stalled for ${STREAM_IDLE_TIMEOUT_MS / 1000}s`));
533
+ let idle = setTimeout(abort, STREAM_IDLE_TIMEOUT_MS);
534
+ const watchdog = new stream_1.PassThrough({
535
+ transform(chunk, _encoding, callback) {
536
+ clearTimeout(idle);
537
+ idle = setTimeout(abort, STREAM_IDLE_TIMEOUT_MS);
538
+ callback(null, chunk);
539
+ },
540
+ });
541
+ try {
542
+ await (0, promises_1.pipeline)(stream_1.Readable.fromWeb(response.body), watchdog, fs_extra_1.default.createWriteStream(destination), { signal: controller.signal });
543
+ }
544
+ finally {
545
+ clearTimeout(idle);
546
+ }
547
+ }
548
+ /**
549
+ * Obtain the base bundle with as little traffic as possible, in order:
550
+ * 1. one Range request for the bundle's compressed bytes when the server
551
+ * knows their location (ppk / apk / ipa published by a CLI that
552
+ * reported it);
553
+ * 2. the archive's central directory over Range, then just the bundle
554
+ * entry (any zip: ppk, apk, ipa — harmony .app nests a second zip and
555
+ * is skipped);
556
+ * 3. the whole archive, as before.
557
+ * A server that ignores Range (HTTP 200) streams the full archive into
558
+ * `archive` without a second request. Every result is verified against
559
+ * `record.bundleHash`. A mismatch of the located bytes (1) may only mean the
560
+ * recorded location is stale, so the directory transport (2) still runs; a
561
+ * mismatch of the entry itself (2, 3) is final — the same bytes would come
562
+ * back however they are fetched — and raises BundleHashMismatchError.
563
+ */
564
+ async function fetchBaseBundle(record, artifactType, archive, log = () => { }) {
565
+ const matches = bundleEntryMatcher(artifactType);
566
+ const verified = (bundle) => {
567
+ if (!bundle)
568
+ throw new PermanentBaseError('bundle entry not found');
569
+ const bundleHash = sha256Hex(bundle);
570
+ if (record.bundleHash && bundleHash !== record.bundleHash) {
571
+ throw new BundleHashMismatchError(bundleHash, record.bundleHash);
572
+ }
573
+ return { bundle, bundleHash };
574
+ };
575
+ const fromFullResponse = async (response) => {
576
+ await saveResponse(response, archive);
577
+ return {
578
+ ...verified(await extractBundleFromArchive(archive)),
579
+ transport: 'full',
580
+ };
581
+ };
582
+ const skip = (reason) => log((0, i18n_1.t)('hermesBaseRangeFallback', { reason }));
583
+ // harmony .app nests the bundle in a second zip, so a location inside the
584
+ // outer archive is never reported for it
585
+ if (artifactType !== 'app' && hasBundleLocation(record)) {
586
+ try {
587
+ const { data, fetchedBytes, totalBytes } = await (0, zip_range_1.fetchZipEntryData)(record.url, {
588
+ dataOffset: record.bundleOffset,
589
+ compressedSize: record.bundleCompressedSize,
590
+ compressionMethod: record.bundleCompression,
591
+ });
592
+ return {
593
+ ...verified(data),
594
+ transport: 'range-entry',
595
+ fetchedBytes,
596
+ totalBytes,
597
+ };
598
+ }
599
+ catch (error) {
600
+ if (error instanceof zip_range_1.RangeUnsupportedError) {
601
+ skip('server ignores Range');
602
+ return fromFullResponse(error.response);
603
+ }
604
+ skip(`entry range: ${error?.message ?? error}`);
605
+ }
606
+ }
607
+ if (artifactType !== 'app') {
608
+ try {
609
+ const remote = await (0, zip_range_1.openRemoteZip)(record.url);
610
+ if (remote.kind === 'full') {
611
+ skip('server ignores Range');
612
+ return fromFullResponse(remote.response);
613
+ }
614
+ const bundle = await (0, zip_range_1.readRemoteZipEntry)(remote.zipFile, matches, remote.reader);
615
+ return {
616
+ ...verified(bundle),
617
+ transport: 'range-zip',
618
+ fetchedBytes: remote.reader.fetchedBytes,
619
+ totalBytes: remote.reader.totalSize,
620
+ };
621
+ }
622
+ catch (error) {
623
+ if (error instanceof zip_range_1.RangeUnsupportedError) {
624
+ skip('server ignores Range');
625
+ return fromFullResponse(error.response);
626
+ }
627
+ // the real entry was read and is not the recorded one: final
628
+ if (error instanceof PermanentBaseError)
629
+ throw error;
630
+ skip(`zip range: ${error?.message ?? error}`);
631
+ }
632
+ }
633
+ await downloadToFile(record.url, archive);
634
+ return {
635
+ ...verified(await extractBundleFromArchive(archive)),
636
+ transport: 'full',
637
+ };
638
+ }
639
+ /**
640
+ * Pick and materialize the base bundle for this compile. Returns null (with
641
+ * the reason logged) whenever a base cannot be used; callers then compile
642
+ * without one.
643
+ */
644
+ async function resolveHermesBase(params) {
645
+ const log = params.log ?? (() => { });
646
+ const { option, bytecodeVersion } = params;
647
+ if (option === 'none') {
648
+ log((0, i18n_1.t)('hermesBaseNone', { reason: 'disabled by option' }));
649
+ return null;
650
+ }
651
+ if (option !== 'auto') {
652
+ // explicit local artifact
653
+ try {
654
+ const bundle = await extractBundleFromArchive(option);
655
+ if (!bundle)
656
+ throw new Error('bundle entry not found');
657
+ const version = (0, hbcTransform_1.getHbcVersion)(bundle);
658
+ if (version !== bytecodeVersion) {
659
+ throw new Error(`HBC version ${version ?? 'n/a'} != ${bytecodeVersion}`);
660
+ }
661
+ const cached = await cachePut(bundle, params.cacheMaxMb);
662
+ log((0, i18n_1.t)('hermesBaseUsing', { source: option, version: bytecodeVersion }));
663
+ return {
664
+ path: cached.path,
665
+ bytecodeVersion,
666
+ bundleHash: cached.bundleHash,
667
+ source: 'local',
668
+ };
669
+ }
670
+ catch (error) {
671
+ log((0, i18n_1.t)('hermesBaseNone', {
672
+ reason: `${option}: ${error?.message ?? error}`,
673
+ }));
674
+ return null;
675
+ }
676
+ }
677
+ if (!params.appId) {
678
+ log((0, i18n_1.t)('hermesBaseNone', { reason: 'app not selected' }));
679
+ return null;
680
+ }
681
+ let record = null;
682
+ try {
683
+ record = await retryOnce(() => params.fetchBase(params.appId, bytecodeVersion));
684
+ }
685
+ catch (error) {
686
+ log((0, i18n_1.t)('hermesBaseNone', {
687
+ reason: `server lookup failed: ${error?.message ?? error}`,
688
+ }));
689
+ return null;
690
+ }
691
+ const source = record?.artifactType && record.artifactType !== 'ppk'
692
+ ? 'native-package'
693
+ : record?.bytecodeVersion == null
694
+ ? 'latest-version'
695
+ : 'download';
696
+ if (!record) {
697
+ log((0, i18n_1.t)('hermesBaseNone', { reason: 'no published version yet' }));
698
+ return null;
699
+ }
700
+ if (record.bytecodeVersion != null &&
701
+ record.bytecodeVersion !== bytecodeVersion) {
702
+ log((0, i18n_1.t)('hermesBaseNone', {
703
+ reason: `server base is HBC ${record.bytecodeVersion}, need ${bytecodeVersion}`,
704
+ }));
705
+ return null;
706
+ }
707
+ // 1. cache by bundleHash
708
+ if (record.bundleHash) {
709
+ const hit = await cacheLookup(record.bundleHash);
710
+ if (hit && record.bytecodeVersion == null) {
711
+ // the server does not know the HBC version (legacy version, native
712
+ // package): the download path checks it, the cache path must too
713
+ const version = await readFileHbcVersion(hit);
714
+ if (version !== bytecodeVersion) {
715
+ log((0, i18n_1.t)('hermesBaseNone', {
716
+ reason: `cached base is HBC ${version ?? 'n/a'}, need ${bytecodeVersion}`,
717
+ }));
718
+ return null;
719
+ }
720
+ }
721
+ if (hit) {
722
+ log((0, i18n_1.t)('hermesBaseUsing', {
723
+ source: `cache ${record.bundleHash.slice(0, 12)}${record.versionId == null ? '' : ` (version ${record.versionId})`}`,
724
+ version: bytecodeVersion,
725
+ }));
726
+ return {
727
+ path: hit,
728
+ bytecodeVersion,
729
+ bundleHash: record.bundleHash,
730
+ versionId: record.versionId ?? undefined,
731
+ hash: record.hash,
732
+ source: 'cache',
733
+ };
734
+ }
735
+ }
736
+ // 2. download, extract, verify, cache — transport errors get a second try
737
+ for (let attempt = 1; attempt <= 2; attempt++) {
738
+ const dir = tmpDir();
739
+ await fs_extra_1.default.ensureDir(dir);
740
+ const artifactType = [
741
+ 'ppk',
742
+ 'apk',
743
+ 'ipa',
744
+ 'app',
745
+ ].includes(record.artifactType ?? '')
746
+ ? record.artifactType
747
+ : 'ppk';
748
+ const archive = path_1.default.join(dir, `base-${process.pid}-${Date.now()}-${attempt}.${artifactType}`);
749
+ try {
750
+ log((0, i18n_1.t)('hermesBaseDownloading', { url: record.url }));
751
+ const fetched = await fetchBaseBundle(record, artifactType, archive, log);
752
+ const { bundle, bundleHash: actualHash } = fetched;
753
+ if (fetched.transport !== 'full') {
754
+ log((0, i18n_1.t)('hermesBaseRangeFetched', {
755
+ fetchedKb: Math.ceil((fetched.fetchedBytes ?? 0) / 1024),
756
+ totalKb: Math.ceil((fetched.totalBytes ?? 0) / 1024),
757
+ }));
758
+ }
759
+ const version = (0, hbcTransform_1.getHbcVersion)(bundle);
760
+ if (version !== bytecodeVersion) {
761
+ log((0, i18n_1.t)('hermesBaseNone', {
762
+ reason: `downloaded base is HBC ${version ?? 'n/a'}, need ${bytecodeVersion}`,
763
+ }));
764
+ return null;
765
+ }
766
+ const cached = await cachePut(bundle, params.cacheMaxMb, actualHash);
767
+ log((0, i18n_1.t)('hermesBaseUsing', {
768
+ source: record.versionId == null
769
+ ? `native package ${record.hash.slice(0, 8)}`
770
+ : `version ${record.versionId} ${record.hash.slice(0, 8)}`,
771
+ version: bytecodeVersion,
772
+ }));
773
+ return {
774
+ path: cached.path,
775
+ bytecodeVersion,
776
+ bundleHash: cached.bundleHash,
777
+ versionId: record.versionId ?? undefined,
778
+ hash: record.hash,
779
+ source,
780
+ };
781
+ }
782
+ catch (error) {
783
+ if (attempt === 2 || error instanceof PermanentBaseError) {
784
+ log((0, i18n_1.t)('hermesBaseNone', {
785
+ reason: `download/verify failed: ${error?.message ?? error}`,
786
+ }));
787
+ return null;
788
+ }
789
+ }
790
+ finally {
791
+ await fs_extra_1.default.remove(archive).catch(() => { });
792
+ }
793
+ }
794
+ return null;
795
+ }
796
+ /** one retry after a short pause, for transient network failures */
797
+ async function retryOnce(action) {
798
+ try {
799
+ return await action();
800
+ }
801
+ catch {
802
+ await new Promise((resolve) => setTimeout(resolve, 500));
803
+ return action();
804
+ }
805
+ }
806
+ function hermescArgsWithBase(baseArgs, basePath) {
807
+ return basePath ? [...baseArgs, `-base-bytecode=${basePath}`] : baseArgs;
808
+ }
809
+ function hermesBaseMeta(selection, bytecodeVersion, check) {
810
+ const meta = {
811
+ bytecodeVersion,
812
+ baseVersionId: selection?.versionId ?? null,
813
+ baseHash: selection?.hash ?? null,
814
+ };
815
+ if (check) {
816
+ meta.hermesBaseOutcome = check.outcome;
817
+ const detail = truncateHermesBaseDetail(check.detail);
818
+ if (detail)
819
+ meta.hermesBaseDetail = detail;
820
+ }
821
+ return meta;
822
+ }
823
+ /** one line, at most HERMES_BASE_DETAIL_MAX_CHARS code points; '' when empty */
824
+ function truncateHermesBaseDetail(detail) {
825
+ const compact = (detail ?? '').replace(/\s+/g, ' ').trim();
826
+ const chars = Array.from(compact);
827
+ return chars.length <= exports.HERMES_BASE_DETAIL_MAX_CHARS
828
+ ? compact
829
+ : chars.slice(0, exports.HERMES_BASE_DETAIL_MAX_CHARS).join('');
830
+ }
831
+ // ---------------------------------------------------------------------------
832
+ // equivalence check (streams two `-dump-bytecode` outputs, never writes them)
833
+ // ---------------------------------------------------------------------------
834
+ const STRING_TABLE_LINE = /^\s*[is](\d+)\[[^\]]*\](?: #[0-9A-F]+)?: (.*)$/;
835
+ /**
836
+ * Normalize one disassembly line so that a delta-mode compile and a plain
837
+ * compile of the same source compare equal: string IDs are renumbered, literal
838
+ * buffers are laid out differently (offsets, short/long variants), and jump
839
+ * distances follow instruction widths. Everything else must match exactly.
840
+ */
841
+ /** separates rendered literal entries inside one normalized line */
842
+ exports.LITERAL_SEPARATOR = '\u001f';
843
+ /**
844
+ * `NewArrayWithBuffer rX, sizeHint, count, offset` and
845
+ * `NewObjectWithBuffer rX, sizeHint, count, keyOffset, valueOffset` (HBC
846
+ * v87–96) or `NewObjectWithBuffer[AndParent] rX, [rP,] shapeIndex,
847
+ * valueOffset` (v98) with the literals decoded from the binary buffers at
848
+ * those offsets, so two builds that lay their buffers out differently still
849
+ * compare by what each instruction builds. The shape index is dropped like an
850
+ * offset: it only locates the keys. Null when the operands do not fit the
851
+ * layout; an offset that cannot be decoded is spelled out (and so never
852
+ * equals a decoded one).
853
+ */
854
+ function renderBufferInstruction(opcode, operands, literals) {
855
+ const [sizeHint, count] = operands;
856
+ if (opcode.startsWith('NewArray')) {
857
+ if (operands.length !== 3)
858
+ return null;
859
+ const entries = literals.array(operands[2], count);
860
+ return `size=${sizeHint} n=${count} [${entries ? entries.join(exports.LITERAL_SEPARATOR) : `<undecodable@${operands[2]}>`}]`;
861
+ }
862
+ if (literals.layout === 'shaped') {
863
+ if (operands.length !== 2)
864
+ return null;
865
+ const [shapeIndex, valueOffset] = operands;
866
+ const shape = literals.shape(shapeIndex);
867
+ const keys = shape && literals.objectKeys(shape.keyOffset, shape.count);
868
+ const values = shape && literals.objectValues(valueOffset, shape.count);
869
+ if (!shape || !keys || !values) {
870
+ return `n=${shape?.count ?? '?'} {<undecodable@shape${shapeIndex}/${valueOffset}>}`;
871
+ }
872
+ const pairs = keys.map((k, i) => `${k}: ${values[i]}`);
873
+ return `n=${shape.count} {${pairs.join(exports.LITERAL_SEPARATOR)}}`;
874
+ }
875
+ if (operands.length !== 4)
876
+ return null;
877
+ const keys = literals.objectKeys(operands[2], count);
878
+ const values = literals.objectValues(operands[3], count);
879
+ if (!keys || !values) {
880
+ return `size=${sizeHint} n=${count} {<undecodable@${operands[2]}/${operands[3]}>}`;
881
+ }
882
+ const pairs = keys.map((k, i) => `${k}: ${values[i]}`);
883
+ return `size=${sizeHint} n=${count} {${pairs.join(exports.LITERAL_SEPARATOR)}}`;
884
+ }
885
+ function normalizeDisassemblyLine(line, strings, literals) {
886
+ // Cheap dispatch on the opcode before touching any regex: dumps run to
887
+ // millions of lines and only a handful of instructions need rewriting.
888
+ let indent = 0;
889
+ while (indent < line.length && isSpace(line.charCodeAt(indent)))
890
+ indent++;
891
+ let opEnd = indent;
892
+ while (opEnd < line.length && isLetter(line.charCodeAt(opEnd)))
893
+ opEnd++;
894
+ const opcode = line.slice(indent, opEnd);
895
+ if (opcode === 'Offset' && line.startsWith('Offset in debug table', indent)) {
896
+ return null;
897
+ }
898
+ let m;
899
+ if (opcode.startsWith('New') && opcode.includes('WithBuffer')) {
900
+ // v98's AndParent form takes the parent object in a second register,
901
+ // which is compared as is (it is not a buffer operand)
902
+ m =
903
+ /^(\s*New(?:Array|Object)WithBuffer)(?:Long)?(AndParent)?\s+(r\d+)(?:, (r\d+))?(.*)$/.exec(line);
904
+ if (m) {
905
+ const op = `${m[1]}${m[2] ?? ''}`;
906
+ const regs = m[4] ? `${m[3]} ${m[4]}` : m[3];
907
+ const nums = m[5].match(/\d+/g) ?? [];
908
+ if (literals) {
909
+ const rendered = renderBufferInstruction(m[1].trim(), nums.map(Number), literals);
910
+ if (rendered)
911
+ return `${op} ${regs} ${rendered}`;
912
+ }
913
+ // no binary buffers: only the size hint survives; the buffer content
914
+ // is compared as a whole instead (compareBuffers)
915
+ return `${op} ${regs} sizes=${nums.slice(0, 1).join(',')}`;
916
+ }
917
+ }
918
+ if (opcode.charCodeAt(0) === 0x4a /* J */) {
919
+ m = /^(\s*J[A-Za-z]+?)(Long)?\s+(L\d+|\d+)(.*)$/.exec(line);
920
+ if (m)
921
+ return `${m[1]} <tgt>${m[4]}`;
922
+ }
923
+ if (opcode.startsWith('DefineOwnById')) {
924
+ m = /^(\s*DefineOwnById\w*\s+r\d+, r\d+, \d+, )(\d+)$/.exec(line);
925
+ if (m)
926
+ line = `${m[1]}"${strings.get(Number(m[2])) ?? `?${m[2]}`}"`;
927
+ }
928
+ // Operand-width variants of one instruction (GetByIdShort/GetById/GetByIdLong,
929
+ // LoadConstString/LoadConstStringLongIndex, ...) only differ by how wide a
930
+ // string/function id or offset is encoded — a foreign base hands the new
931
+ // code's hot strings large ids, so the delta build legitimately picks the
932
+ // wider form. Fold the suffix and the column padding that follows it.
933
+ if (opEnd > indent &&
934
+ (opEnd === line.length || isSpace(line.charCodeAt(opEnd)))) {
935
+ const folded = foldWidthSuffix(opcode);
936
+ line = `${line.slice(0, indent)}${folded}${line.slice(opEnd).replace(/\s+/g, ' ')}`;
937
+ // Switch jump tables sit after the instructions; their relative offset (and
938
+ // the table header hermesc prints for them) moves with instruction widths.
939
+ // The two switch instructions carry that offset in different operands:
940
+ // StringSwitchImm rX, <id>, <jtOffset>, <defaultLabel>, <count>
941
+ // UIntSwitchImm rX, <jtOffset>, <defaultLabel>, <min>, <max>
942
+ // Folding only the first shape let a shifted UIntSwitchImm offset read as a
943
+ // real difference and drop an otherwise good delta build.
944
+ if (folded === 'StringSwitchImm') {
945
+ m = /^(\s*StringSwitchImm r\d+, \d+, )\d+(, L\d+, \d+)$/.exec(line);
946
+ if (m)
947
+ line = `${m[1]}<jt>${m[2]}`;
948
+ }
949
+ else if (folded === 'UIntSwitchImm') {
950
+ m = /^(\s*UIntSwitchImm r\d+, )\d+(, L\d+, \d+, \d+)$/.exec(line);
951
+ if (m)
952
+ line = `${m[1]}<jt>${m[2]}`;
953
+ }
954
+ else if (folded === 'offset' && /^\s*offset \d+$/.test(line)) {
955
+ line = line.replace(/\d+$/, '<jt>');
956
+ }
957
+ }
958
+ return line;
959
+ }
960
+ function isSpace(code) {
961
+ return code === 0x20 || code === 0x09 || code === 0x0d;
962
+ }
963
+ function isLetter(code) {
964
+ return (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a);
965
+ }
966
+ /** strip one operand-width suffix (LongIndex, Long, Short) off an opcode */
967
+ function foldWidthSuffix(opcode) {
968
+ for (const suffix of ['LongIndex', 'Long', 'Short']) {
969
+ if (opcode.length > suffix.length && opcode.endsWith(suffix)) {
970
+ return opcode.slice(0, -suffix.length);
971
+ }
972
+ }
973
+ return opcode;
974
+ }
975
+ /** Split a readable stream into lines without readline (Bun's readline async iterator misbehaves when pulled manually). */
976
+ async function* streamLines(stream) {
977
+ let rest = '';
978
+ for await (const chunk of stream) {
979
+ rest += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
980
+ let index = rest.indexOf('\n');
981
+ while (index >= 0) {
982
+ yield rest.slice(0, index);
983
+ rest = rest.slice(index + 1);
984
+ index = rest.indexOf('\n');
985
+ }
986
+ }
987
+ if (rest)
988
+ yield rest;
989
+ }
990
+ /** `Array Buffer:`, `Object Key Buffer:`, `Debug filename table:`, ... */
991
+ const SECTION_HEADER = /^([A-Z][A-Za-z ]*):$/;
992
+ /** literal buffer entry that refers to the string table by id */
993
+ const BUFFER_STRING_ENTRY = /^\[String (\d+)\]$/;
994
+ /** sections after the functions that only serve debuggers and profilers */
995
+ const TRAILER_SECTION = /^(?:Debug |Textified callees table)/;
996
+ /** stderr kept from a dump process */
997
+ const DUMP_STDERR_KEEP = 4 * 1024;
998
+ /** a diagnostic line is cut to this many characters */
999
+ const DETAIL_LINE_MAX = 120;
1000
+ function isFunctionHeader(line) {
1001
+ return line.startsWith('Function<') || line.startsWith('NCFunction<');
1002
+ }
1003
+ /**
1004
+ * Structured reader over one `hermesc -dump-bytecode -pretty-disassemble`
1005
+ * stream. Everything before the functions is either the string table (kept
1006
+ * to resolve ids), a literal buffer (kept, with string ids resolved to text,
1007
+ * so buffer *content* is compared) or ignored; the functions come out one at
1008
+ * a time, normalized; the debug tables after them are drained and ignored.
1009
+ */
1010
+ class DumpReader {
1011
+ proc;
1012
+ iterator;
1013
+ strings = new Map();
1014
+ /** literal buffers by section name, entries with string ids resolved */
1015
+ buffers = new Map();
1016
+ exit;
1017
+ section = null;
1018
+ pending = null;
1019
+ inFunctions = false;
1020
+ finished = false;
1021
+ /** set when the binary literal buffers of this side could be read */
1022
+ literals;
1023
+ constructor(proc, dumpTo, buffers) {
1024
+ this.proc = proc;
1025
+ // the string table is filled while the preamble streams by, before the
1026
+ // first instruction needs it
1027
+ if (buffers)
1028
+ this.literals = new hermes_literals_1.LiteralResolver(buffers, this.strings);
1029
+ const pass = new stream_1.PassThrough();
1030
+ proc.stdout.pipe(pass);
1031
+ if (dumpTo)
1032
+ proc.stdout.pipe(fs_extra_1.default.createWriteStream(dumpTo));
1033
+ this.iterator = streamLines(pass);
1034
+ let stderr = '';
1035
+ proc.stderr?.on('data', (chunk) => {
1036
+ stderr = (stderr + chunk.toString()).slice(-DUMP_STDERR_KEEP);
1037
+ });
1038
+ this.exit = new Promise((resolve) => {
1039
+ // a spawn failure (ENOENT) may leave stdout open and never 'close'
1040
+ proc.on('error', (error) => {
1041
+ pass.end();
1042
+ resolve({ code: null, signal: null, error, stderr });
1043
+ });
1044
+ proc.on('close', (code, signal) => resolve({ code, signal, stderr }));
1045
+ });
1046
+ }
1047
+ async nextLine() {
1048
+ if (this.pending !== null) {
1049
+ const line = this.pending;
1050
+ this.pending = null;
1051
+ return line;
1052
+ }
1053
+ if (this.finished)
1054
+ return null;
1055
+ const { value, done } = await this.iterator.next();
1056
+ if (done) {
1057
+ this.finished = true;
1058
+ return null;
1059
+ }
1060
+ return value;
1061
+ }
1062
+ /** consume the preamble up to (and returning) the first function header */
1063
+ async skipPreamble() {
1064
+ for (;;) {
1065
+ const line = await this.nextLine();
1066
+ if (line === null)
1067
+ return null;
1068
+ if (isFunctionHeader(line)) {
1069
+ this.inFunctions = true;
1070
+ return line;
1071
+ }
1072
+ const header = SECTION_HEADER.exec(line);
1073
+ if (header) {
1074
+ this.section = header[1];
1075
+ if (this.section.endsWith('Buffer'))
1076
+ this.buffers.set(this.section, []);
1077
+ continue;
1078
+ }
1079
+ if (this.section === 'Global String Table') {
1080
+ const m = STRING_TABLE_LINE.exec(line);
1081
+ if (m)
1082
+ this.strings.set(Number(m[1]), m[2]);
1083
+ }
1084
+ else if (this.section?.endsWith('Buffer') && line.trim() !== '') {
1085
+ const entry = line.trim();
1086
+ const ref = BUFFER_STRING_ENTRY.exec(entry);
1087
+ const text = ref ? this.strings.get(Number(ref[1])) : undefined;
1088
+ this.buffers
1089
+ .get(this.section)
1090
+ .push(ref
1091
+ ? `[String ${text === undefined ? `?${ref[1]}` : JSON.stringify(text)}]`
1092
+ : entry);
1093
+ }
1094
+ }
1095
+ }
1096
+ /** next function (header + normalized body), or null after the last one */
1097
+ async nextFunction() {
1098
+ const header = this.inFunctions
1099
+ ? await this.nextLine()
1100
+ : await this.skipPreamble();
1101
+ if (header === null)
1102
+ return null;
1103
+ // a non-function header here is a trailer section: no more functions
1104
+ if (!isFunctionHeader(header))
1105
+ return null;
1106
+ const lines = [];
1107
+ for (;;) {
1108
+ const line = await this.nextLine();
1109
+ if (line === null)
1110
+ break;
1111
+ if (isFunctionHeader(line) || TRAILER_SECTION.test(line)) {
1112
+ this.pending = line;
1113
+ break;
1114
+ }
1115
+ if (line.trim() === '')
1116
+ continue;
1117
+ const normalized = normalizeDisassemblyLine(line, this.strings, this.literals);
1118
+ if (normalized !== null)
1119
+ lines.push(normalized);
1120
+ }
1121
+ return { header, lines };
1122
+ }
1123
+ /** drain whatever is left and report how the process ended */
1124
+ async finish() {
1125
+ while ((await this.nextLine()) !== null) { }
1126
+ return this.exit;
1127
+ }
1128
+ kill() {
1129
+ this.proc.kill();
1130
+ }
1131
+ }
1132
+ function describeExit(exit) {
1133
+ if (exit.error)
1134
+ return exit.error.message;
1135
+ const how = exit.signal !== null ? `signal ${exit.signal}` : `exit ${exit.code}`;
1136
+ const stderr = exit.stderr.trim().split('\n').pop()?.trim();
1137
+ return stderr ? `${how}: ${stderr}` : how;
1138
+ }
1139
+ function clip(line) {
1140
+ const s = line.trim().split(exports.LITERAL_SEPARATOR).join(', ');
1141
+ return s.length > DETAIL_LINE_MAX ? `${s.slice(0, DETAIL_LINE_MAX - 1)}…` : s;
1142
+ }
1143
+ /**
1144
+ * For two New*WithBuffer lines that differ, the first literal that differs
1145
+ * (the whole line would be clipped before the difference for long literals).
1146
+ */
1147
+ function literalDifference(a, b) {
1148
+ const open = /^(\s*New\w+WithBuffer(?:AndParent)? r\d+(?: r\d+)?(?: size=\d+)? n=\d+ [[{])/;
1149
+ const ma = open.exec(a);
1150
+ const mb = open.exec(b);
1151
+ if (!ma || !mb)
1152
+ return null;
1153
+ const ea = a.slice(ma[1].length, -1).split(exports.LITERAL_SEPARATOR);
1154
+ const eb = b.slice(mb[1].length, -1).split(exports.LITERAL_SEPARATOR);
1155
+ const n = Math.min(ea.length, eb.length);
1156
+ for (let i = 0; i < n; i++) {
1157
+ if (ea[i] !== eb[i]) {
1158
+ return `${clip(ma[1].slice(0, -1))} entry ${i}: ${clip(ea[i])} vs ${clip(eb[i])}`;
1159
+ }
1160
+ }
1161
+ return null;
1162
+ }
1163
+ function compareBuffers(a, b) {
1164
+ for (const name of new Set([...a.keys(), ...b.keys()])) {
1165
+ const ea = a.get(name) ?? [];
1166
+ const eb = b.get(name) ?? [];
1167
+ const n = Math.min(ea.length, eb.length);
1168
+ for (let i = 0; i < n; i++) {
1169
+ if (ea[i] !== eb[i]) {
1170
+ return `${name} entry ${i}: ${clip(ea[i])} vs ${clip(eb[i])}`;
1171
+ }
1172
+ }
1173
+ if (ea.length !== eb.length) {
1174
+ return `${name}: ${ea.length} vs ${eb.length} entries`;
1175
+ }
1176
+ }
1177
+ return null;
1178
+ }
1179
+ function compareFunctions(a, b) {
1180
+ if (a.header !== b.header) {
1181
+ return `${clip(a.header)} vs ${clip(b.header)}`;
1182
+ }
1183
+ const n = Math.min(a.lines.length, b.lines.length);
1184
+ for (let i = 0; i < n; i++) {
1185
+ if (a.lines[i] !== b.lines[i]) {
1186
+ const literal = literalDifference(a.lines[i], b.lines[i]);
1187
+ return literal
1188
+ ? `${clip(a.header)} +${i + 1}: ${literal}`
1189
+ : `${clip(a.header)} +${i + 1}: ${clip(a.lines[i])} vs ${clip(b.lines[i])}`;
1190
+ }
1191
+ }
1192
+ if (a.lines.length !== b.lines.length) {
1193
+ return `${clip(a.header)}: ${a.lines.length} vs ${b.lines.length} lines`;
1194
+ }
1195
+ return null;
1196
+ }
1197
+ /**
1198
+ * Compare two HBC files by disassembly (see normalizeDisassemblyLine): the
1199
+ * literal buffers by content, then function by function. `-b` forces hermesc
1200
+ * to treat inputs as bytecode whatever their extension. Both dumps are
1201
+ * consumed as streams so the ~100 MB of text never touches the disk (unless
1202
+ * `dumpTo` asks for it). A dump process that fails or ends early is reported
1203
+ * as `dump-failed`, never as a difference: the two cases need different
1204
+ * follow-ups. The detail names the first difference so a rejected base can be
1205
+ * turned into a normalization rule or a bug report.
1206
+ */
1207
+ async function compareHermesBytecode(hermesCommand, withBase, plain, options = {}) {
1208
+ const spawnDump = (file) => (0, child_process_1.spawn)(hermesCommand, ['-b', '-dump-bytecode', '-pretty-disassemble', file], { stdio: ['ignore', 'pipe', 'pipe'] });
1209
+ // The literal buffers are read from both files up front (header + three
1210
+ // byte ranges). Only when both sides can be read do the instructions
1211
+ // compare decoded literals; otherwise both fall back to the dumped buffers
1212
+ // as a whole, so the two sides always normalize the same way.
1213
+ const [buffersA, buffersB] = await Promise.all([
1214
+ (0, hermes_literals_1.readLiteralBuffers)(withBase).catch(() => null),
1215
+ (0, hermes_literals_1.readLiteralBuffers)(plain).catch(() => null),
1216
+ ]);
1217
+ const binary = buffersA && buffersB && buffersA.version === buffersB.version;
1218
+ const literals = binary ? 'instruction' : 'buffer';
1219
+ const a = new DumpReader(spawnDump(withBase), options.dumpTo?.withBase, binary ? buffersA : null);
1220
+ const b = new DumpReader(spawnDump(plain), options.dumpTo?.plain, binary ? buffersB : null);
1221
+ let functions = 0;
1222
+ const different = (detail) => ({
1223
+ status: 'different',
1224
+ detail,
1225
+ functions,
1226
+ literals,
1227
+ });
1228
+ const dumpFailed = (side, exit) => ({
1229
+ status: 'dump-failed',
1230
+ detail: `${side} dump: ${describeExit(exit)}`,
1231
+ functions,
1232
+ literals,
1233
+ });
1234
+ try {
1235
+ for (;;) {
1236
+ const [fa, fb] = await Promise.all([a.nextFunction(), b.nextFunction()]);
1237
+ if (fa === null || fb === null) {
1238
+ // whoever ended: was that the end of a good dump or a failure?
1239
+ if (fa === null) {
1240
+ const exit = await a.finish();
1241
+ if (exit.error || exit.code !== 0)
1242
+ return dumpFailed('base', exit);
1243
+ }
1244
+ if (fb === null) {
1245
+ const exit = await b.finish();
1246
+ if (exit.error || exit.code !== 0)
1247
+ return dumpFailed('plain', exit);
1248
+ }
1249
+ if (fa !== null || fb !== null) {
1250
+ const extra = fa ?? fb;
1251
+ return different(`function count: ${clip(extra.header)} only in the ${fa ? 'base' : 'plain'} compile`);
1252
+ }
1253
+ if (functions === 0) {
1254
+ return {
1255
+ status: 'dump-failed',
1256
+ detail: 'no functions in the disassembly',
1257
+ functions,
1258
+ literals,
1259
+ };
1260
+ }
1261
+ return { status: 'equivalent', functions, literals };
1262
+ }
1263
+ if (functions === 0 && !binary) {
1264
+ // buffers precede the functions, so both are complete by now. This
1265
+ // whole-buffer comparison is the fallback: the builder overlaps and
1266
+ // deduplicates serialized literals, so a delta build may lay the same
1267
+ // literals out differently and this can reject a good build.
1268
+ const detail = compareBuffers(a.buffers, b.buffers);
1269
+ if (detail)
1270
+ return different(detail);
1271
+ }
1272
+ const detail = compareFunctions(fa, fb);
1273
+ if (detail)
1274
+ return different(detail);
1275
+ functions++;
1276
+ }
1277
+ }
1278
+ finally {
1279
+ a.kill();
1280
+ b.kill();
1281
+ }
1282
+ }
1283
+ /** Resolves true when equivalent; see compareHermesBytecode for the details. */
1284
+ async function verifyHermesBaseEquivalence(hermesCommand, withBase, plain) {
1285
+ const result = await compareHermesBytecode(hermesCommand, withBase, plain);
1286
+ return result.status === 'equivalent';
1287
+ }