@bravemobile/react-native-code-push 13.1.0 → 13.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -394,7 +394,18 @@ module.exports = Config;
394
394
  - Modifying an existing release history with the `update-history` command.
395
395
 
396
396
 
397
- **(2) For `code-push.config.ts` (TypeScript) to work properly, you may need to update your `tsconfig.json`.**
397
+ **(2) A `code-push.config.ts` (TypeScript) file needs a loader installed.**
398
+
399
+ Install `tsx`, which requires no further setup:
400
+
401
+ ```bash
402
+ npm install --save-dev tsx
403
+ ```
404
+
405
+ `ts-node` also keeps working, and is used when `tsx` is not installed. Note that `ts-node` is no
406
+ longer maintained, so support for it is **deprecated and will be removed in a future major
407
+ version** — please migrate to `tsx`. Until then, `ts-node` needs the following `tsconfig.json`
408
+ setup:
398
409
 
399
410
  ```diff
400
411
  {
@@ -414,6 +425,8 @@ module.exports = Config;
414
425
 
415
426
  ```
416
427
 
428
+ A `code-push.config.js` (JavaScript) file needs neither.
429
+
417
430
 
418
431
  ### 6. Diff Updates (Optional)
419
432
 
@@ -21,10 +21,6 @@
21
21
  private final ** mBundleLoader;
22
22
  }
23
23
  # bridgeless
24
- -keepclassmembers class com.facebook.react.defaults.DefaultReactHostDelegate {
25
- private ** jsBundleLoader;
26
- }
27
- # bridgeless
28
24
  -keepclassmembers class * extends com.facebook.react.runtime.ReactHostDelegate {
29
25
  private ** jsBundleLoader;
30
26
  private ** _jsBundleLoader;
@@ -35,9 +31,6 @@
35
31
  private final ** reactHostDelegate; # RN 0.81+
36
32
  }
37
33
 
38
- # Can't find referenced class org.bouncycastle.**
39
- -dontwarn com.nimbusds.jose.**
40
-
41
34
  # The binary patch applier is found by name from the native library.
42
35
  -keepclasseswithmembernames class com.microsoft.codepush.react.HDiffPatchNative {
43
36
  native <methods>;
@@ -51,6 +51,9 @@ public class CodePush extends TurboReactPackage {
51
51
  return mServerUrl;
52
52
  }
53
53
 
54
+ /**
55
+ * Entry point used by React Native autolinking to create and retain the package instance.
56
+ */
54
57
  public static synchronized CodePush getInstance(Context context, boolean isDebugMode) {
55
58
  if (mCurrentInstance == null) {
56
59
  mCurrentInstance = new CodePush(context, isDebugMode);
@@ -59,6 +59,15 @@ public class CodePushNativeModule extends NativeCodePushSpec {
59
59
  private boolean _restartInProgress = false;
60
60
  private ArrayList<Boolean> _restartQueue = new ArrayList<>();
61
61
 
62
+ /**
63
+ * Whether the React Native instance this module was created for is still the one running.
64
+ *
65
+ * A reload replaces the instance while work this module started can still be in flight,
66
+ * and a frame callback posted to the process-wide choreographer outlives the executor
67
+ * this module shuts down. Read from whichever thread that work is on, so it is volatile.
68
+ */
69
+ private volatile boolean mGenerationAlive = true;
70
+
62
71
  public CodePushNativeModule(ReactApplicationContext reactContext, CodePush codePush, CodePushUpdateManager codePushUpdateManager, CodePushTelemetryManager codePushTelemetryManager, SettingsManager settingsManager) {
63
72
  super(reactContext);
64
73
 
@@ -85,6 +94,7 @@ public class CodePushNativeModule extends NativeCodePushSpec {
85
94
 
86
95
  @Override
87
96
  public void invalidate() {
97
+ mGenerationAlive = false;
88
98
  clearLifecycleEventListener();
89
99
  mBackgroundExecutor.shutdownNow();
90
100
  super.invalidate();
@@ -264,6 +274,14 @@ public class CodePushNativeModule extends NativeCodePushSpec {
264
274
  }
265
275
 
266
276
  private void emitDownloadProgressEvent(DownloadProgress downloadProgress) {
277
+ // Every progress event arrives here, including the ones a frame callback delivers
278
+ // after this module was invalidated: shutting the executor down does not cancel a
279
+ // callback already posted to the choreographer, and the context it would emit
280
+ // through has been destroyed by then.
281
+ if (!mGenerationAlive) {
282
+ return;
283
+ }
284
+
267
285
  if (mEventEmitterCallback != null) {
268
286
  emitOnDownloadProgress(downloadProgress.createWritableMap());
269
287
  return;
@@ -275,6 +293,13 @@ public class CodePushNativeModule extends NativeCodePushSpec {
275
293
  }
276
294
 
277
295
  private void restartAppInternal(boolean onlyIfUpdateIsPending) {
296
+ // A restart requested by an instance that has been replaced would reload the one
297
+ // that replaced it, which nobody asked for.
298
+ if (!mGenerationAlive) {
299
+ CodePushUtils.log("Ignoring a restart requested by a React Native instance that is no longer running");
300
+ return;
301
+ }
302
+
278
303
  if (this._restartInProgress) {
279
304
  CodePushUtils.log("Restart request queued until the current restart is completed");
280
305
  this._restartQueue.add(onlyIfUpdateIsPending);
@@ -1,20 +1,15 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { createRequire } from "module";
4
- const nodeRequire = createRequire(import.meta.url);
5
4
  /**
6
5
  * allows to require a config file with .ts extension
7
6
  */
8
7
  function requireConfig(filePath) {
9
8
  const ext = path.extname(filePath);
9
+ // Resolve the loader from the project the config file lives in, not from the CLI install.
10
+ const projectRequire = createRequire(filePath);
10
11
  if (ext === '.ts') {
11
- try {
12
- nodeRequire('ts-node/register');
13
- }
14
- catch {
15
- console.error('ts-node not found. Please install ts-node as a devDependency.');
16
- process.exit(1);
17
- }
12
+ return unwrapDefaultExport(requireTsConfig(projectRequire, filePath));
18
13
  }
19
14
  else if (ext === '.js') {
20
15
  // do nothing
@@ -22,7 +17,38 @@ function requireConfig(filePath) {
22
17
  else {
23
18
  throw new Error(`Unsupported file extension: ${ext}`);
24
19
  }
25
- return nodeRequire(filePath);
20
+ return unwrapDefaultExport(projectRequire(filePath));
21
+ }
22
+ /**
23
+ * tsx is preferred: it needs no tsconfig setup and resolves tsconfig `paths` aliases on its own.
24
+ * ts-node keeps working for projects that already have it configured.
25
+ */
26
+ function requireTsConfig(projectRequire, filePath) {
27
+ if (canResolve(projectRequire, 'tsx/cjs/api')) {
28
+ const { require: tsxRequire } = projectRequire('tsx/cjs/api');
29
+ return tsxRequire(filePath, filePath);
30
+ }
31
+ if (canResolve(projectRequire, 'ts-node/register')) {
32
+ console.warn('warn: Loading the config file with ts-node, which is no longer maintained. Support for it ' +
33
+ 'will be removed in a future major version - please install tsx instead (`npm i -D tsx`).');
34
+ projectRequire('ts-node/register');
35
+ return projectRequire(filePath);
36
+ }
37
+ console.error('A TypeScript config file needs a loader. Please install tsx as a devDependency (`npm i -D tsx`).');
38
+ process.exit(1);
39
+ }
40
+ function canResolve(projectRequire, id) {
41
+ try {
42
+ projectRequire.resolve(id);
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ // `export default` compiles to `exports.default`; `module.exports =` is returned as is.
50
+ function unwrapDefaultExport(loaded) {
51
+ return (loaded.default ?? loaded);
26
52
  }
27
53
  export function findAndReadConfigFile(startDir, configFileName) {
28
54
  let dir = startDir;
package/cli/package.json CHANGED
@@ -19,11 +19,15 @@
19
19
  "yauzl": "^3.2.0"
20
20
  },
21
21
  "peerDependencies": {
22
- "ts-node": ">=10"
22
+ "ts-node": ">=10",
23
+ "tsx": ">=4"
23
24
  },
24
25
  "peerDependenciesMeta": {
25
26
  "ts-node": {
26
27
  "optional": true
28
+ },
29
+ "tsx": {
30
+ "optional": true
27
31
  }
28
32
  },
29
33
  "engines": {
@@ -3,28 +3,62 @@ import path from "path";
3
3
  import { createRequire } from "module";
4
4
  import type { CliConfigInterface } from "../../typings/react-native-code-push.d.ts";
5
5
 
6
- const nodeRequire = createRequire(import.meta.url);
7
-
8
6
  /**
9
7
  * allows to require a config file with .ts extension
10
8
  */
11
9
  function requireConfig(filePath: string): CliConfigInterface {
12
10
  const ext = path.extname(filePath);
11
+ // Resolve the loader from the project the config file lives in, not from the CLI install.
12
+ const projectRequire = createRequire(filePath);
13
13
 
14
14
  if (ext === '.ts') {
15
- try {
16
- nodeRequire('ts-node/register');
17
- } catch {
18
- console.error('ts-node not found. Please install ts-node as a devDependency.');
19
- process.exit(1);
20
- }
15
+ return unwrapDefaultExport(requireTsConfig(projectRequire, filePath));
21
16
  } else if (ext === '.js') {
22
17
  // do nothing
23
18
  } else {
24
19
  throw new Error(`Unsupported file extension: ${ext}`);
25
20
  }
26
21
 
27
- return nodeRequire(filePath) as CliConfigInterface;
22
+ return unwrapDefaultExport(projectRequire(filePath));
23
+ }
24
+
25
+ /**
26
+ * tsx is preferred: it needs no tsconfig setup and resolves tsconfig `paths` aliases on its own.
27
+ * ts-node keeps working for projects that already have it configured.
28
+ */
29
+ function requireTsConfig(projectRequire: NodeRequire, filePath: string): { default?: unknown } {
30
+ if (canResolve(projectRequire, 'tsx/cjs/api')) {
31
+ const { require: tsxRequire } = projectRequire('tsx/cjs/api') as {
32
+ require: (id: string, fromFile: string) => { default?: unknown };
33
+ };
34
+ return tsxRequire(filePath, filePath);
35
+ }
36
+
37
+ if (canResolve(projectRequire, 'ts-node/register')) {
38
+ console.warn(
39
+ 'warn: Loading the config file with ts-node, which is no longer maintained. Support for it ' +
40
+ 'will be removed in a future major version - please install tsx instead (`npm i -D tsx`).',
41
+ );
42
+ projectRequire('ts-node/register');
43
+ return projectRequire(filePath);
44
+ }
45
+
46
+ console.error('A TypeScript config file needs a loader. Please install tsx as a devDependency (`npm i -D tsx`).');
47
+ process.exit(1);
48
+ }
49
+
50
+ function canResolve(projectRequire: NodeRequire, id: string): boolean {
51
+ try {
52
+ projectRequire.resolve(id);
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ // `export default` compiles to `exports.default`; `module.exports =` is returned as is.
60
+ function unwrapDefaultExport(loaded: { default?: unknown }): CliConfigInterface {
61
+ return (loaded.default ?? loaded) as CliConfigInterface;
28
62
  }
29
63
 
30
64
  export function findAndReadConfigFile(startDir: string, configFileName: string): CliConfigInterface {
@@ -36,6 +36,10 @@
36
36
  @interface CodePush : RCTEventEmitter
37
37
  #endif
38
38
 
39
+ /*
40
+ * Returns the bundle embedded in the binary, without resolving an installed update.
41
+ * The update pipeline uses this as the base for binary patches and asset diffs.
42
+ */
39
43
  + (NSURL *)binaryBundleURL;
40
44
  /*
41
45
  * This method is used to retrieve the URL for the most recent
@@ -64,8 +68,10 @@
64
68
  subdirectory:(NSString *)resourceSubdirectory
65
69
  bundle:(NSBundle *)resourceBundle;
66
70
 
71
+ /* Internal storage root used by CodePushPackage. */
67
72
  + (NSString *)getApplicationSupportDirectory;
68
73
 
74
+ /* Returns the embedded assets path used as the base for asset diffs. */
69
75
  + (NSString *)bundleAssetsPath;
70
76
 
71
77
  /*
@@ -76,9 +82,8 @@
76
82
  + (void)overrideAppVersion:(NSString *)appVersion;
77
83
 
78
84
  /*
79
- * This method allows dynamically setting the app's
80
- * deployment key, in addition to setting it via
81
- * the Info.plist file's CodePushDeploymentKey setting.
85
+ * Retained for compatibility. The current implementation ignores the argument
86
+ * and uses the deprecated deployment key placeholder.
82
87
  */
83
88
  + (void)setDeploymentKey:(NSString *)deploymentKey;
84
89
 
@@ -1,6 +1,7 @@
1
1
  #import <React/RCTAssert.h>
2
2
  #import <React/RCTBridgeModule.h>
3
3
  #import <React/RCTConvert.h>
4
+ #import <React/RCTInvalidating.h>
4
5
  #import <React/RCTEventDispatcher.h>
5
6
  #import <React/RCTRootView.h>
6
7
  #import <React/RCTUtils.h>
@@ -8,7 +9,9 @@
8
9
 
9
10
  #import "CodePush.h"
10
11
 
11
- @interface CodePush () <RCTFrameUpdateObserver
12
+ #include <atomic>
13
+
14
+ @interface CodePush () <RCTFrameUpdateObserver, RCTInvalidating
12
15
  #ifdef RCT_NEW_ARCH_ENABLED
13
16
  , NativeCodePushSpec
14
17
  #endif
@@ -32,6 +35,17 @@
32
35
  BOOL _allowed;
33
36
  BOOL _restartInProgress;
34
37
  NSMutableArray *_restartQueue;
38
+
39
+ /*
40
+ * Whether the React Native instance this module was created for is still the one
41
+ * running.
42
+ *
43
+ * A reload starts the next instance before the one being replaced has finished being
44
+ * torn down, so work this instance started can still be running with nothing left to
45
+ * hand its result to. Read from whichever thread that work is on, written when the
46
+ * instance is invalidated, so it is atomic.
47
+ */
48
+ std::atomic<bool> _generationAlive;
35
49
  }
36
50
 
37
51
  RCT_EXPORT_MODULE()
@@ -307,7 +321,42 @@ static NSString *const LatestRollbackCountKey = @"count";
307
321
  [[NSNotificationCenter defaultCenter] removeObserver:self];
308
322
  }
309
323
 
324
+ /*
325
+ * Called when the React Native instance this module belongs to is being replaced.
326
+ *
327
+ * A download in flight keeps this module alive past that point, and the suspend timer and
328
+ * the resume notifications keep it reachable too. None of them may act afterwards: the
329
+ * runtime they would report to is gone, and the restart they would trigger belongs to a
330
+ * generation that is no longer on screen.
331
+ */
332
+ - (void)invalidate
333
+ {
334
+ _generationAlive.store(false);
335
+
336
+ [_appSuspendTimer invalidate];
337
+ _appSuspendTimer = nil;
338
+ _hasResumeListener = NO;
339
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
340
+
341
+ #ifndef RCT_NEW_ARCH_ENABLED
342
+ [super invalidate];
343
+ #endif
344
+ }
345
+
346
+ /* Whether this module still belongs to the React Native instance that is running. */
347
+ - (BOOL)isGenerationAlive
348
+ {
349
+ return _generationAlive.load();
350
+ }
351
+
310
352
  - (void)dispatchDownloadProgressEvent {
353
+ // The download that is reporting here can be one this module started before the app was
354
+ // reloaded, in which case the event emitter it would reach belongs to a runtime that has
355
+ // been torn down.
356
+ if (![self isGenerationAlive]) {
357
+ return;
358
+ }
359
+
311
360
  // Notify the script-side about the progress
312
361
  NSDictionary *progress = @{
313
362
  @"totalBytes" : [NSNumber numberWithLongLong:_latestExpectedContentLength],
@@ -375,6 +424,7 @@ static NSString *const LatestRollbackCountKey = @"count";
375
424
  _restartInProgress = NO;
376
425
  _restartQueue = [NSMutableArray arrayWithCapacity:1];
377
426
  _lastProgressEventTime = 0;
427
+ _generationAlive.store(true);
378
428
 
379
429
  self = [super init];
380
430
  if (self) {
@@ -533,6 +583,11 @@ static NSString *const LatestRollbackCountKey = @"count";
533
583
  */
534
584
  - (void)loadBundle
535
585
  {
586
+ if (![self isGenerationAlive]) {
587
+ CPLog(@"Ignoring a reload requested by a React Native instance that is no longer running.");
588
+ return;
589
+ }
590
+
536
591
  @synchronized([CodePush class]) {
537
592
  hasInitializedUpdateAfterRestartForCurrentLoad = NO;
538
593
  }
@@ -802,6 +857,11 @@ RCT_EXPORT_METHOD(downloadUpdate:(NSDictionary*)updatePackage
802
857
 
803
858
  - (void)restartAppInternal:(BOOL)onlyIfUpdateIsPending
804
859
  {
860
+ if (![self isGenerationAlive]) {
861
+ CPLog(@"Ignoring a restart requested by a React Native instance that is no longer running.");
862
+ return;
863
+ }
864
+
805
865
  if (_restartInProgress) {
806
866
  CPLog(@"Restart request queued until the current restart is completed.");
807
867
  [_restartQueue addObject:@(onlyIfUpdateIsPending)];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bravemobile/react-native-code-push",
3
- "version": "13.1.0",
3
+ "version": "13.2.0",
4
4
  "description": "React Native plugin for the CodePush service",
5
5
  "main": "src/CodePush.js",
6
6
  "react-native": "src/CodePush.js",
@@ -71,8 +71,8 @@
71
71
  ],
72
72
  "scripts": {
73
73
  "setup": "npm install --quiet --no-progress",
74
- "setup-example-app": "ts-node --project scripts/setupExampleApp/tsconfig.json scripts/setupExampleApp/runSetupExampleApp.ts",
75
- "setup-expo-example-app": "ts-node --project scripts/setupExampleApp/tsconfig.json scripts/setupExpoExampleApp/runSetupExpoExampleApp.ts",
74
+ "setup-example-app": "tsx scripts/setupExampleApp/runSetupExampleApp.ts",
75
+ "setup-expo-example-app": "tsx scripts/setupExpoExampleApp/runSetupExpoExampleApp.ts",
76
76
  "type:src": "tsc --project tsconfig.json --noEmit",
77
77
  "type:cli": "npm run --workspace cli typecheck",
78
78
  "type:scripts": "tsc --project scripts/tsconfig.json --noEmit",
@@ -82,7 +82,7 @@
82
82
  "prepack": "npm run build:cli",
83
83
  "eslint": "eslint --quiet .",
84
84
  "jest": "jest src/CodePush.test.js src/versioning/* expo/* && npm run --workspace cli test",
85
- "e2e": "ts-node --project e2e/tsconfig.json e2e/run.ts"
85
+ "e2e": "tsx e2e/run.ts"
86
86
  },
87
87
  "publishConfig": {
88
88
  "access": "public"
@@ -102,11 +102,19 @@
102
102
  },
103
103
  "peerDependencies": {
104
104
  "expo": ">=50.0.0",
105
- "react-native": "*"
105
+ "react-native": "*",
106
+ "ts-node": ">=10",
107
+ "tsx": ">=4"
106
108
  },
107
109
  "peerDependenciesMeta": {
108
110
  "expo": {
109
111
  "optional": true
112
+ },
113
+ "ts-node": {
114
+ "optional": true
115
+ },
116
+ "tsx": {
117
+ "optional": true
110
118
  }
111
119
  },
112
120
  "devDependencies": {
@@ -128,7 +136,7 @@
128
136
  "jest": "^29.7.0",
129
137
  "react": "19.2.3",
130
138
  "react-native": "0.84.1",
131
- "ts-node": "^10.9.2",
139
+ "tsx": "^4.23.13",
132
140
  "typescript": "5.0.4",
133
141
  "typescript-eslint": "^8.11.0"
134
142
  },