react-native-update-cli 2.9.3 → 2.9.5

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/src/diff.ts CHANGED
@@ -6,9 +6,54 @@ import type { CommandContext } from './types';
6
6
  import { translateOptions } from './utils';
7
7
  import { isPPKBundleFileName, scriptName, tempDir } from './utils/constants';
8
8
  import { t } from './utils/i18n';
9
- import { enumZipEntries, readEntry } from './utils/zip-entries';
9
+ import {
10
+ enumZipEntries,
11
+ readEntry,
12
+ readEntryPrefix,
13
+ } from './utils/zip-entries';
14
+ import {
15
+ ZIP_ENTRY_SNIFF_BYTES,
16
+ zipOptionsForManifestEntry,
17
+ zipOptionsForPatchEntry,
18
+ zipOptionsForPayloadEntry,
19
+ } from './utils/zip-options';
10
20
 
11
21
  type Diff = (oldSource?: Buffer, newSource?: Buffer) => Buffer;
22
+ type HpatchCover = {
23
+ oldPos: number | string | bigint;
24
+ newPos: number | string | bigint;
25
+ len: number | string | bigint;
26
+ };
27
+ type HpatchCompatiblePlan = {
28
+ covers?: HpatchCover[];
29
+ };
30
+ type HdiffWithCoversOptions = {
31
+ mode?: 'replace' | 'merge' | 'native-coalesce';
32
+ };
33
+ type HdiffModule = {
34
+ diff?: Diff;
35
+ diffWithCovers?: (
36
+ oldSource: Buffer,
37
+ newSource: Buffer,
38
+ covers: HpatchCover[],
39
+ options?: HdiffWithCoversOptions,
40
+ ) => { diff?: Buffer };
41
+ };
42
+ type BsdiffModule = {
43
+ diff?: Diff;
44
+ };
45
+ type ChiffModule = {
46
+ hpatchCompatiblePlanResult?: (
47
+ oldSource: Buffer,
48
+ newSource: Buffer,
49
+ ) => HpatchCompatiblePlan;
50
+ hpatchApproximatePlanResult?: (
51
+ oldSource: Buffer,
52
+ newSource: Buffer,
53
+ ) => HpatchCompatiblePlan;
54
+ };
55
+ type ChiffHpatchPolicy = 'off' | 'costed';
56
+ type ChiffHpatchExactPolicy = 'off' | 'on';
12
57
  type EntryMap = Record<string, { crc32: number; fileName: string }>;
13
58
  type CrcMap = Record<number, string>;
14
59
  type CopyMap = Record<string, string>;
@@ -28,22 +73,134 @@ type DiffCommandConfig = {
28
73
 
29
74
  export { enumZipEntries, readEntry };
30
75
 
31
- const loadDiffModule = (pkgName: string): Diff | undefined => {
76
+ const loadModule = <T>(pkgName: string): T | undefined => {
32
77
  const resolvePaths = ['.', npm.packages, yarn.packages];
33
78
 
34
79
  try {
35
80
  const resolved = require.resolve(pkgName, { paths: resolvePaths });
36
- const mod = require(resolved);
37
- if (mod?.diff) {
38
- return mod.diff as Diff;
39
- }
81
+ return require(resolved) as T;
40
82
  } catch {}
41
83
 
42
84
  return undefined;
43
85
  };
44
86
 
45
- const hdiff = loadDiffModule('node-hdiffpatch');
46
- const bsdiff = loadDiffModule('node-bsdiff');
87
+ const hdiff = loadModule<HdiffModule>('node-hdiffpatch');
88
+ const bsdiff = loadModule<BsdiffModule>('node-bsdiff');
89
+ const chiff = loadModule<ChiffModule>('@chiff/node');
90
+
91
+ // Structured covers are experimental and can be expensive on real Hermes input.
92
+ // Keep native hdiff as the default unless the server explicitly opts in.
93
+ function resolveChiffHpatchPolicy(policy?: unknown): ChiffHpatchPolicy {
94
+ const value = String(
95
+ policy ?? process.env.RNU_CHIFF_HPATCH_POLICY ?? 'off',
96
+ ).toLowerCase();
97
+ if (
98
+ value === 'costed' ||
99
+ value === 'on' ||
100
+ value === 'true' ||
101
+ value === '1'
102
+ ) {
103
+ return 'costed';
104
+ }
105
+ return 'off';
106
+ }
107
+
108
+ function resolveChiffHpatchMinNativeBytes(value?: unknown): number {
109
+ const raw = value ?? process.env.RNU_CHIFF_HPATCH_MIN_NATIVE_BYTES ?? 4096;
110
+ const parsed = Number(raw);
111
+ if (!Number.isFinite(parsed) || parsed < 0) {
112
+ return 4096;
113
+ }
114
+ return Math.floor(parsed);
115
+ }
116
+
117
+ function resolveChiffHpatchExactPolicy(policy?: unknown): ChiffHpatchExactPolicy {
118
+ const value = String(
119
+ policy ?? process.env.RNU_CHIFF_HPATCH_EXACT_COVERS ?? 'off',
120
+ ).toLowerCase();
121
+ if (value === 'on' || value === 'true' || value === '1') {
122
+ return 'on';
123
+ }
124
+ return 'off';
125
+ }
126
+
127
+ function createChiffAwareHdiff(
128
+ hdiffModule: HdiffModule,
129
+ chiffModule: ChiffModule | undefined,
130
+ policy: ChiffHpatchPolicy,
131
+ minNativeBytes: number,
132
+ exactPolicy: ChiffHpatchExactPolicy,
133
+ ): Diff {
134
+ const baseDiff = hdiffModule.diff;
135
+ if (!baseDiff) {
136
+ throw new Error(t('nodeHdiffpatchRequired', { scriptName }));
137
+ }
138
+
139
+ if (policy === 'off') {
140
+ return baseDiff;
141
+ }
142
+
143
+ return (oldSource?: Buffer, newSource?: Buffer) => {
144
+ const nativeDiff = baseDiff(oldSource, newSource);
145
+ if (!oldSource || !newSource || !hdiffModule.diffWithCovers) {
146
+ return nativeDiff;
147
+ }
148
+
149
+ let bestDiff = nativeDiff;
150
+ const tryDiffWithCovers = (
151
+ covers: HpatchCover[],
152
+ mode: 'replace' | 'merge' | 'native-coalesce',
153
+ ) => {
154
+ try {
155
+ const result = hdiffModule.diffWithCovers?.(
156
+ oldSource,
157
+ newSource,
158
+ covers,
159
+ { mode },
160
+ );
161
+ if (
162
+ Buffer.isBuffer(result?.diff) &&
163
+ result.diff.length < bestDiff.length
164
+ ) {
165
+ bestDiff = result.diff;
166
+ }
167
+ } catch {}
168
+ };
169
+
170
+ tryDiffWithCovers([], 'native-coalesce');
171
+
172
+ if (nativeDiff.length < minNativeBytes) {
173
+ return bestDiff;
174
+ }
175
+
176
+ try {
177
+ const approximatePlan = chiffModule?.hpatchApproximatePlanResult?.(
178
+ oldSource,
179
+ newSource,
180
+ );
181
+ if (Array.isArray(approximatePlan?.covers)) {
182
+ tryDiffWithCovers(approximatePlan.covers, 'merge');
183
+ }
184
+ } catch {}
185
+
186
+ if (
187
+ exactPolicy === 'off' ||
188
+ !chiffModule?.hpatchCompatiblePlanResult
189
+ ) {
190
+ return bestDiff;
191
+ }
192
+
193
+ try {
194
+ const plan = chiffModule.hpatchCompatiblePlanResult(oldSource, newSource);
195
+ if (Array.isArray(plan.covers)) {
196
+ tryDiffWithCovers(plan.covers, 'replace');
197
+ tryDiffWithCovers(plan.covers, 'merge');
198
+ }
199
+ } catch {}
200
+
201
+ return bestDiff;
202
+ };
203
+ }
47
204
 
48
205
  function basename(fn: string): string | undefined {
49
206
  const m = /^(.+\/)[^\/]+\/?$/.exec(fn);
@@ -123,6 +280,7 @@ async function diffFromPPK(
123
280
  zipfile.addBuffer(
124
281
  diffFn(originSource, newSource),
125
282
  `${entry.fileName}.patch`,
283
+ zipOptionsForPatchEntry(),
126
284
  );
127
285
  //console.log('End diff');
128
286
  } else {
@@ -150,6 +308,11 @@ async function diffFromPPK(
150
308
  addEntry(basePath);
151
309
  }
152
310
 
311
+ const entryPrefix = await readEntryPrefix(
312
+ entry,
313
+ nextZipfile,
314
+ ZIP_ENTRY_SNIFF_BYTES,
315
+ );
153
316
  await new Promise<void>((resolve, reject) => {
154
317
  nextZipfile.openReadStream(entry, (err, readStream) => {
155
318
  if (err) {
@@ -160,7 +323,11 @@ async function diffFromPPK(
160
323
  new Error(`Unable to read zip entry: ${entry.fileName}`),
161
324
  );
162
325
  }
163
- zipfile.addReadStream(readStream, entry.fileName);
326
+ zipfile.addReadStream(
327
+ readStream,
328
+ entry.fileName,
329
+ zipOptionsForPayloadEntry(entry.fileName, entryPrefix),
330
+ );
164
331
  readStream.on('end', () => {
165
332
  //console.log('add finished');
166
333
  resolve(void 0);
@@ -183,6 +350,7 @@ async function diffFromPPK(
183
350
  zipfile.addBuffer(
184
351
  Buffer.from(JSON.stringify({ copies, deletes })),
185
352
  '__diff.json',
353
+ zipOptionsForManifestEntry(),
186
354
  );
187
355
  zipfile.end();
188
356
  await writePromise;
@@ -248,6 +416,7 @@ async function diffFromPackage(
248
416
  zipfile.addBuffer(
249
417
  diffFn(originSource, newSource),
250
418
  `${entry.fileName}.patch`,
419
+ zipOptionsForPatchEntry(),
251
420
  );
252
421
  //console.log('End diff');
253
422
  } else {
@@ -263,6 +432,11 @@ async function diffFromPackage(
263
432
  return;
264
433
  }
265
434
 
435
+ const entryPrefix = await readEntryPrefix(
436
+ entry,
437
+ nextZipfile,
438
+ ZIP_ENTRY_SNIFF_BYTES,
439
+ );
266
440
  await new Promise<void>((resolve, reject) => {
267
441
  nextZipfile.openReadStream(entry, (err, readStream) => {
268
442
  if (err) {
@@ -273,7 +447,11 @@ async function diffFromPackage(
273
447
  new Error(`Unable to read zip entry: ${entry.fileName}`),
274
448
  );
275
449
  }
276
- zipfile.addReadStream(readStream, entry.fileName);
450
+ zipfile.addReadStream(
451
+ readStream,
452
+ entry.fileName,
453
+ zipOptionsForPayloadEntry(entry.fileName, entryPrefix),
454
+ );
277
455
  readStream.on('end', () => {
278
456
  //console.log('add finished');
279
457
  resolve(void 0);
@@ -283,13 +461,22 @@ async function diffFromPackage(
283
461
  }
284
462
  });
285
463
 
286
- zipfile.addBuffer(Buffer.from(JSON.stringify({ copies })), '__diff.json');
464
+ zipfile.addBuffer(
465
+ Buffer.from(JSON.stringify({ copies })),
466
+ '__diff.json',
467
+ zipOptionsForManifestEntry(),
468
+ );
287
469
  zipfile.end();
288
470
  await writePromise;
289
471
  }
290
472
 
291
473
  type DiffCommandOptions = {
292
474
  customDiff?: Diff;
475
+ customHdiffModule?: HdiffModule;
476
+ customChiffModule?: ChiffModule;
477
+ chiffHpatchPolicy?: ChiffHpatchPolicy;
478
+ chiffHpatchMinNativeBytes?: number | string;
479
+ chiffHpatchExactCovers?: ChiffHpatchExactPolicy | boolean | string | number;
293
480
  [key: string]: any;
294
481
  };
295
482
 
@@ -302,16 +489,23 @@ function resolveDiffImplementation(
302
489
  }
303
490
 
304
491
  if (useHdiff) {
305
- if (!hdiff) {
492
+ const hdiffModule = options.customHdiffModule ?? hdiff;
493
+ if (!hdiffModule?.diff) {
306
494
  throw new Error(t('nodeHdiffpatchRequired', { scriptName }));
307
495
  }
308
- return hdiff;
496
+ return createChiffAwareHdiff(
497
+ hdiffModule,
498
+ options.customChiffModule ?? chiff,
499
+ resolveChiffHpatchPolicy(options.chiffHpatchPolicy),
500
+ resolveChiffHpatchMinNativeBytes(options.chiffHpatchMinNativeBytes),
501
+ resolveChiffHpatchExactPolicy(options.chiffHpatchExactCovers),
502
+ );
309
503
  }
310
504
 
311
- if (!bsdiff) {
505
+ if (!bsdiff?.diff) {
312
506
  throw new Error(t('nodeBsdiffRequired', { scriptName }));
313
507
  }
314
- return bsdiff;
508
+ return bsdiff.diff;
315
509
  }
316
510
 
317
511
  function diffArgsCheck(
@@ -40,10 +40,6 @@ export class ModuleManager {
40
40
  if (module.init) {
41
41
  module.init(this.provider);
42
42
  }
43
-
44
- // console.log(
45
- // `Module '${module.name}' (v${module.version}) registered successfully`,
46
- // );
47
43
  }
48
44
 
49
45
  unregisterModule(moduleName: string): void {
package/src/package.ts CHANGED
@@ -99,7 +99,7 @@ async function uploadNativePackage(
99
99
  const { appId: appIdInPkg, appKey: appKeyInPkg } = info;
100
100
  const { appId, appKey } = await getSelectedApp(config.platform);
101
101
 
102
- if (appIdInPkg && appIdInPkg != appId) {
102
+ if (appIdInPkg && String(appIdInPkg) !== appId) {
103
103
  throw new Error(t(config.appIdMismatchKey, { appIdInPkg, appId }));
104
104
  }
105
105
 
@@ -175,10 +175,11 @@ export async function listPackage(appId: string) {
175
175
 
176
176
  export async function choosePackage(appId: string) {
177
177
  const list = await listPackage(appId);
178
+ const packageMap = new Map(list?.map((v) => [v.id.toString(), v]));
178
179
 
179
180
  while (true) {
180
181
  const id = await question(t('enterNativePackageId'));
181
- const app = list?.find((v) => v.id.toString() === id);
182
+ const app = packageMap.get(id);
182
183
  if (app) {
183
184
  return app;
184
185
  }
@@ -329,7 +330,7 @@ export const packageCommands = {
329
330
  packages: async ({ options }: { options: { platform: Platform } }) => {
330
331
  const platform = await getPlatform(options.platform);
331
332
  const { appId } = await getSelectedApp(platform);
332
- await listPackage(appId);
333
+ await listPackage(String(appId));
333
334
  },
334
335
  deletePackage: async ({
335
336
  args,
@@ -347,7 +348,7 @@ export const packageCommands = {
347
348
 
348
349
  if (!appId) {
349
350
  const platform = await getPlatform(options.platform);
350
- appId = (await getSelectedApp(platform)).appId as string;
351
+ appId = (await getSelectedApp(platform)).appId;
351
352
  }
352
353
 
353
354
  // If no packageId provided as argument, let user choose from list
@@ -48,7 +48,7 @@ export const ping = async (url: string) => {
48
48
  if (!pingFinished) {
49
49
  // console.log('ping timeout', url);
50
50
  }
51
- }, 2000),
51
+ }, 5000),
52
52
  ),
53
53
  ]) as Promise<string | null>;
54
54
  };
@@ -28,6 +28,75 @@ export function readEntry(
28
28
  });
29
29
  }
30
30
 
31
+ export function readEntryPrefix(
32
+ entry: Entry,
33
+ zipFile: YauzlZipFile,
34
+ maxBytes: number,
35
+ ): Promise<Buffer> {
36
+ if (maxBytes <= 0) {
37
+ return Promise.resolve(Buffer.alloc(0));
38
+ }
39
+
40
+ const buffers: Buffer[] = [];
41
+ let length = 0;
42
+
43
+ return new Promise((resolve, reject) => {
44
+ zipFile.openReadStream(entry, (err, stream) => {
45
+ if (err) {
46
+ return reject(err);
47
+ }
48
+ if (!stream) {
49
+ return reject(new Error(`Unable to read zip entry: ${entry.fileName}`));
50
+ }
51
+
52
+ let settled = false;
53
+ const cleanup = () => {
54
+ stream.off('data', onData);
55
+ stream.off('end', onEnd);
56
+ stream.off('error', onError);
57
+ };
58
+ const finish = () => {
59
+ if (settled) {
60
+ return;
61
+ }
62
+ settled = true;
63
+ cleanup();
64
+ resolve(Buffer.concat(buffers, length));
65
+ };
66
+ const onData = (chunk: Buffer) => {
67
+ const remaining = maxBytes - length;
68
+ if (remaining <= 0) {
69
+ finish();
70
+ stream.destroy();
71
+ return;
72
+ }
73
+
74
+ const slice =
75
+ chunk.length > remaining ? chunk.subarray(0, remaining) : chunk;
76
+ buffers.push(slice);
77
+ length += slice.length;
78
+ if (length >= maxBytes) {
79
+ finish();
80
+ stream.destroy();
81
+ }
82
+ };
83
+ const onEnd = () => finish();
84
+ const onError = (error: Error) => {
85
+ if (settled) {
86
+ return;
87
+ }
88
+ settled = true;
89
+ cleanup();
90
+ reject(error);
91
+ };
92
+
93
+ stream.on('data', onData);
94
+ stream.once('end', onEnd);
95
+ stream.once('error', onError);
96
+ });
97
+ });
98
+ }
99
+
31
100
  export async function enumZipEntries(
32
101
  zipFn: string,
33
102
  callback: (
@@ -0,0 +1,173 @@
1
+ import path from 'path';
2
+ import * as fs from 'fs';
3
+
4
+ export type ZipEntryOptions = {
5
+ compress?: boolean;
6
+ compressionLevel?: number;
7
+ };
8
+
9
+ export const ZIP_ENTRY_SNIFF_BYTES = 64;
10
+
11
+ const alreadyCompressedExtensions = new Set([
12
+ '.7z',
13
+ '.aab',
14
+ '.apk',
15
+ '.br',
16
+ '.bz2',
17
+ '.gif',
18
+ '.gz',
19
+ '.heic',
20
+ '.jpeg',
21
+ '.jpg',
22
+ '.lzma',
23
+ '.mp3',
24
+ '.mp4',
25
+ '.ogg',
26
+ '.png',
27
+ '.webm',
28
+ '.webp',
29
+ '.woff',
30
+ '.woff2',
31
+ '.xz',
32
+ '.zip',
33
+ '.zst',
34
+ ]);
35
+
36
+ const HERMES_MAGIC = Buffer.from([
37
+ 0xc6, 0x1f, 0xbc, 0x03, 0xc1, 0x03, 0x19, 0x1f,
38
+ ]);
39
+ const HERMES_DELTA_MAGIC = Buffer.from([
40
+ 0x39, 0xe0, 0x43, 0xfc, 0x3e, 0xfc, 0xe6, 0xe0,
41
+ ]);
42
+
43
+ function startsWith(bytes: Buffer, signature: Buffer): boolean {
44
+ return (
45
+ bytes.length >= signature.length &&
46
+ bytes.subarray(0, signature.length).equals(signature)
47
+ );
48
+ }
49
+
50
+ function hasHermesBytecodeMagic(bytes: Buffer): boolean {
51
+ return (
52
+ startsWith(bytes, HERMES_MAGIC) || startsWith(bytes, HERMES_DELTA_MAGIC)
53
+ );
54
+ }
55
+
56
+ function hasAlreadyCompressedMagic(bytes: Buffer): boolean {
57
+ if (bytes.length < 2) {
58
+ return false;
59
+ }
60
+
61
+ if (startsWith(bytes, Buffer.from([0x89, 0x50, 0x4e, 0x47]))) {
62
+ return true;
63
+ }
64
+ if (startsWith(bytes, Buffer.from([0xff, 0xd8, 0xff]))) {
65
+ return true;
66
+ }
67
+ if (
68
+ startsWith(bytes, Buffer.from('GIF87a', 'ascii')) ||
69
+ startsWith(bytes, Buffer.from('GIF89a', 'ascii'))
70
+ ) {
71
+ return true;
72
+ }
73
+ if (
74
+ bytes.length >= 12 &&
75
+ bytes.subarray(0, 4).equals(Buffer.from('RIFF', 'ascii')) &&
76
+ bytes.subarray(8, 12).equals(Buffer.from('WEBP', 'ascii'))
77
+ ) {
78
+ return true;
79
+ }
80
+ if (
81
+ startsWith(bytes, Buffer.from([0x50, 0x4b, 0x03, 0x04])) ||
82
+ startsWith(bytes, Buffer.from([0x50, 0x4b, 0x05, 0x06])) ||
83
+ startsWith(bytes, Buffer.from([0x50, 0x4b, 0x07, 0x08]))
84
+ ) {
85
+ return true;
86
+ }
87
+ if (startsWith(bytes, Buffer.from([0x1f, 0x8b]))) {
88
+ return true;
89
+ }
90
+ if (startsWith(bytes, Buffer.from('BZh', 'ascii'))) {
91
+ return true;
92
+ }
93
+ if (startsWith(bytes, Buffer.from([0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00]))) {
94
+ return true;
95
+ }
96
+ if (startsWith(bytes, Buffer.from([0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c]))) {
97
+ return true;
98
+ }
99
+ if (startsWith(bytes, Buffer.from([0x28, 0xb5, 0x2f, 0xfd]))) {
100
+ return true;
101
+ }
102
+ if (bytes.length >= 12 && bytes.subarray(4, 8).equals(Buffer.from('ftyp'))) {
103
+ return true;
104
+ }
105
+ if (startsWith(bytes, Buffer.from('OggS', 'ascii'))) {
106
+ return true;
107
+ }
108
+ if (startsWith(bytes, Buffer.from('ID3', 'ascii'))) {
109
+ return true;
110
+ }
111
+ if (
112
+ bytes[0] === 0xff &&
113
+ bytes.length >= 2 &&
114
+ (bytes[1] & 0xe0) === 0xe0
115
+ ) {
116
+ return true;
117
+ }
118
+ if (
119
+ startsWith(bytes, Buffer.from('wOFF', 'ascii')) ||
120
+ startsWith(bytes, Buffer.from('wOF2', 'ascii'))
121
+ ) {
122
+ return true;
123
+ }
124
+
125
+ return false;
126
+ }
127
+
128
+ function readFilePrefix(filePath: string): Buffer {
129
+ const buffer = Buffer.alloc(ZIP_ENTRY_SNIFF_BYTES);
130
+ const fd = fs.openSync(filePath, 'r');
131
+ try {
132
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
133
+ return buffer.subarray(0, bytesRead);
134
+ } finally {
135
+ fs.closeSync(fd);
136
+ }
137
+ }
138
+
139
+ export function zipOptionsForPatchEntry(): ZipEntryOptions {
140
+ return { compress: false };
141
+ }
142
+
143
+ export function zipOptionsForManifestEntry(): ZipEntryOptions {
144
+ return { compress: false };
145
+ }
146
+
147
+ export function zipOptionsForPayloadEntry(
148
+ fileName: string,
149
+ prefix?: Buffer,
150
+ ): ZipEntryOptions {
151
+ if (prefix && prefix.length > 0) {
152
+ if (hasHermesBytecodeMagic(prefix)) {
153
+ // Hermes bytecode is binary, but still benefits significantly from zip deflate.
154
+ return { compress: true, compressionLevel: 9 };
155
+ }
156
+ if (hasAlreadyCompressedMagic(prefix)) {
157
+ return { compress: false };
158
+ }
159
+ }
160
+
161
+ const extension = path.extname(fileName).toLowerCase();
162
+ if (alreadyCompressedExtensions.has(extension)) {
163
+ return { compress: false };
164
+ }
165
+ return { compress: true, compressionLevel: 9 };
166
+ }
167
+
168
+ export function zipOptionsForPayloadFile(
169
+ filePath: string,
170
+ entryName = filePath,
171
+ ): ZipEntryOptions {
172
+ return zipOptionsForPayloadEntry(entryName, readFilePrefix(filePath));
173
+ }