react-native-debug-toolkit 3.3.1 → 3.3.3
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 +7 -1
- package/README.zh-CN.md +7 -1
- package/bin/debug-toolkit.js +16 -1
- package/ios/DebugToolkitDevConnect.h +17 -0
- package/ios/DebugToolkitDevConnect.mm +312 -170
- package/lib/commonjs/features/devConnect/DevConnectTab.js +186 -7
- package/lib/commonjs/features/devConnect/DevConnectTab.js.map +1 -1
- package/lib/commonjs/features/devConnect/nativeDevConnect.js +6 -9
- package/lib/commonjs/features/devConnect/nativeDevConnect.js.map +1 -1
- package/lib/module/features/devConnect/DevConnectTab.js +187 -8
- package/lib/module/features/devConnect/DevConnectTab.js.map +1 -1
- package/lib/module/features/devConnect/nativeDevConnect.js +5 -8
- package/lib/module/features/devConnect/nativeDevConnect.js.map +1 -1
- package/lib/typescript/src/features/devConnect/DevConnectTab.d.ts.map +1 -1
- package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts +10 -1
- package/lib/typescript/src/features/devConnect/nativeDevConnect.d.ts.map +1 -1
- package/package.json +4 -2
- package/scripts/android-debug-bundle.gradle +23 -0
- package/scripts/eas-postinstall.sh +6 -0
- package/scripts/embed-android.js +116 -0
- package/scripts/embed-expo.js +109 -0
- package/scripts/embed-ios.js +119 -0
- package/scripts/embed.js +224 -0
- package/src/features/devConnect/DevConnectTab.tsx +128 -5
- package/src/features/devConnect/nativeDevConnect.ts +18 -8
package/README.md
CHANGED
|
@@ -77,7 +77,13 @@ In the app, open Debug Panel -> `DevConnect` -> `Send Once` or `Start Live Sync`
|
|
|
77
77
|
|
|
78
78
|
DevConnect auto-detects simulator/emulator and uses local host settings automatically. On real devices, enter your computer IP to connect.
|
|
79
79
|
|
|
80
|
-
For Remote JS Bundle, run Metro on your computer, enter computer IP and Metro port in `DevConnect`, then tap `Use Metro Bundle`. DevConnect
|
|
80
|
+
For Remote JS Bundle, run Metro on your computer, enter computer IP and Metro port in `DevConnect`, then tap `Use Metro Bundle`. DevConnect persists the host and hot-reloads from Metro.
|
|
81
|
+
|
|
82
|
+
> **Debug builds only.** Metro host switching works in Debug builds. Release builds load the embedded bundle and the controls are disabled (`release: disabled` badge).
|
|
83
|
+
|
|
84
|
+
**iOS — no AppDelegate changes required.** On install, DevConnect hooks `RCTBundleURLProvider` so the app **cold-starts from the embedded `main.jsbundle`** and only connects to Metro after you apply a host in the panel (fixes Expo `.expo/.virtual-metro-entry` red screens when Metro is off). Use **Reset** to go back to the embedded bundle.
|
|
85
|
+
|
|
86
|
+
The IP and ports are persisted through AsyncStorage when installed, or through the native module after rebuild.
|
|
81
87
|
|
|
82
88
|
QR scan is optional. Install `react-native-camera-kit` or `expo-camera` in the app to enable the scan button. The app must request camera permission before scanning.
|
|
83
89
|
|
package/README.zh-CN.md
CHANGED
|
@@ -77,7 +77,13 @@ App 内打开 Debug Panel -> `DevConnect` -> `Send Once` 或 `Start Live Sync`
|
|
|
77
77
|
|
|
78
78
|
DevConnect 自动识别模拟器/真机,模拟器下自动使用本机 Metro/daemon 地址。真机需输入电脑 IP 地址。
|
|
79
79
|
|
|
80
|
-
Remote JS Bundle:先在电脑启动 Metro,在 `DevConnect` 输入电脑 IP 和 Metro 端口,然后点 `Use Metro Bundle`。DevConnect
|
|
80
|
+
Remote JS Bundle:先在电脑启动 Metro,在 `DevConnect` 输入电脑 IP 和 Metro 端口,然后点 `Use Metro Bundle`。DevConnect 会持久化 host 并从 Metro 热重载。
|
|
81
|
+
|
|
82
|
+
> **仅 Debug 包可用。** Release 包控件会显示 `release: disabled` 并禁用。
|
|
83
|
+
|
|
84
|
+
**iOS — 无需改 AppDelegate。** 安装本库后会 hook `RCTBundleURLProvider`:**未配置 IP 时冷启动走内置 `main.jsbundle`**,只有在面板里应用 host 后才连 Metro(可避免 Metro 未开时的 `.expo/.virtual-metro-entry` 红屏)。点 **Reset** 可回到内置 bundle。
|
|
85
|
+
|
|
86
|
+
IP 和端口会通过 AsyncStorage 持久化;如果没装 AsyncStorage,则在重建后通过本库原生模块持久化。
|
|
81
87
|
|
|
82
88
|
扫码是可选能力。App 安装 `react-native-camera-kit` 或 `expo-camera` 后,DevConnect 才显示扫码按钮。App 仍需自己配置相机权限文案,并在使用扫码前申请相机权限。
|
|
83
89
|
|
package/bin/debug-toolkit.js
CHANGED
|
@@ -27,10 +27,19 @@ function hasHelpFlag(args) {
|
|
|
27
27
|
function printHelp() {
|
|
28
28
|
process.stderr.write(
|
|
29
29
|
'Usage: debug-toolkit [--host 0.0.0.0] [--port 3799] [--token dev-token] [--store ~/.react-native-debug-toolkit/daemon-devices.json] [--daemon-only]\n'
|
|
30
|
+
+ ' debug-toolkit embed [--platform ios|android] [--undo] [--yes]\n'
|
|
30
31
|
+ '\n'
|
|
31
32
|
+ 'Starts the debug toolkit: daemon (HTTP + Web Console) and MCP stdio server.\n'
|
|
32
33
|
+ '\n'
|
|
33
|
-
+ '
|
|
34
|
+
+ 'Commands:\n'
|
|
35
|
+
+ ' embed Embed JS bundle in debug builds (run in host app root)\n'
|
|
36
|
+
+ '\n'
|
|
37
|
+
+ 'Embed options:\n'
|
|
38
|
+
+ ' --platform <p> Target platform: ios or android (default: both)\n'
|
|
39
|
+
+ ' --undo Remove embed injections\n'
|
|
40
|
+
+ ' --yes Skip confirmations (CI/EAS)\n'
|
|
41
|
+
+ '\n'
|
|
42
|
+
+ 'Daemon options:\n'
|
|
34
43
|
+ ' --host <addr> Host to bind (default: 0.0.0.0)\n'
|
|
35
44
|
+ ' --port <port> Port to bind (default: 3799)\n'
|
|
36
45
|
+ ' --token <str> Auth token for daemon endpoints\n'
|
|
@@ -57,6 +66,12 @@ async function main() {
|
|
|
57
66
|
return;
|
|
58
67
|
}
|
|
59
68
|
|
|
69
|
+
// Route embed subcommand
|
|
70
|
+
if (args[0] === 'embed') {
|
|
71
|
+
const { main: embedMain } = require('../scripts/embed');
|
|
72
|
+
return embedMain(args.slice(1));
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
const host = readOption(args, '--host', process.env.DEBUG_TOOLKIT_DAEMON_HOST || DEFAULT_HOST);
|
|
61
76
|
const port = Number(readOption(args, '--port', process.env.DEBUG_TOOLKIT_DAEMON_PORT || DEFAULT_PORT));
|
|
62
77
|
const token = readOption(args, '--token', process.env.DEBUG_TOOLKIT_DAEMON_TOKEN || '');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Returns a Metro bundle URL when the user has applied a host via DevConnect; otherwise nil.
|
|
7
|
+
|
|
8
|
+
Zero-config: installing this pod hooks RCTBundleURLProvider so cold start uses the embedded
|
|
9
|
+
main.jsbundle until DevConnect applies a host. You do not need to change AppDelegate.
|
|
10
|
+
|
|
11
|
+
Optional override for custom bundleURL() implementations:
|
|
12
|
+
|
|
13
|
+
if let metro = DebugToolkitMetroBundleURL() { return metro }
|
|
14
|
+
*/
|
|
15
|
+
FOUNDATION_EXPORT NSURL * _Nullable DebugToolkitMetroBundleURL(void);
|
|
16
|
+
|
|
17
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -1,49 +1,220 @@
|
|
|
1
|
+
#import "DebugToolkitDevConnect.h"
|
|
2
|
+
|
|
1
3
|
#import <Foundation/Foundation.h>
|
|
4
|
+
#import <UIKit/UIKit.h>
|
|
2
5
|
#import <React/RCTBridge.h>
|
|
3
6
|
#import <React/RCTBundleManager.h>
|
|
4
7
|
#import <React/RCTBridgeModule.h>
|
|
5
8
|
#import <React/RCTBundleURLProvider.h>
|
|
6
|
-
#import <React/RCTDefines.h>
|
|
7
9
|
#import <React/RCTReloadCommand.h>
|
|
8
10
|
#import <objc/runtime.h>
|
|
9
11
|
#include <ifaddrs.h>
|
|
10
12
|
#include <arpa/inet.h>
|
|
11
13
|
#include <net/if.h>
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
// Debug-only Metro host switching — zero host-app native changes required.
|
|
16
|
+
//
|
|
17
|
+
// RN/Expo Debug templates call jsBundleURLForBundleRoot:fallbackURLProvider:, which consults
|
|
18
|
+
// -packagerServerHostPort (guessPackagerHost on simulator when Metro is running). We hook:
|
|
19
|
+
// 1. jsBundleURLForBundleRoot:fallbackURLProvider: — no DevConnect host → main.jsbundle first.
|
|
20
|
+
// 2. packagerServerHostPort — no DevConnect host → nil (block auto-discovered localhost:8081).
|
|
21
|
+
//
|
|
22
|
+
// Optional: host apps may call DebugToolkitMetroBundleURL() from bundleURL() for explicit control.
|
|
23
|
+
|
|
24
|
+
static NSString *const kBundleRoot = @"index";
|
|
25
|
+
// Expo prebuild templates pass this to jsBundleURLForBundleRoot: in Debug (see expo/expo#21643).
|
|
26
|
+
static NSString *const kExpoVirtualMetroEntry = @".expo/.virtual-metro-entry";
|
|
27
|
+
static NSString *const kMetroHostKey = @"_devconnect_metro_host";
|
|
28
|
+
|
|
29
|
+
static BOOL gPackagerHookInstalled = NO;
|
|
30
|
+
static BOOL gBundleRootHookInstalled = NO;
|
|
15
31
|
|
|
16
|
-
|
|
32
|
+
static NSURL *(*gOrigJsBundleURLForBundleRootWithFallback)(id, SEL, NSString *, NSURL * _Nonnull (^)(void));
|
|
17
33
|
|
|
18
|
-
static
|
|
34
|
+
static BOOL DevConnectEmbeddedFirstHooksActive(void)
|
|
35
|
+
{
|
|
36
|
+
return gBundleRootHookInstalled;
|
|
37
|
+
}
|
|
19
38
|
|
|
20
|
-
static NSURL *
|
|
39
|
+
static NSURL *DebugToolkitEmbeddedBundleURL(void)
|
|
21
40
|
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static NSString *DevConnectPersistedMetroHost(void)
|
|
45
|
+
{
|
|
46
|
+
return [[NSUserDefaults standardUserDefaults] stringForKey:kMetroHostKey];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static void DevConnectSetPersistedMetroHost(NSString *_Nullable hostPort)
|
|
50
|
+
{
|
|
51
|
+
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
52
|
+
if (hostPort.length > 0) {
|
|
53
|
+
[defaults setObject:hostPort forKey:kMetroHostKey];
|
|
54
|
+
} else {
|
|
55
|
+
[defaults removeObjectForKey:kMetroHostKey];
|
|
29
56
|
}
|
|
30
|
-
|
|
31
|
-
|
|
57
|
+
[defaults synchronize];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static void DebugToolkitPrepareBundleSourceIfNeeded(void)
|
|
61
|
+
{
|
|
62
|
+
// Touch sharedSettings so RCTBundleURLProvider is linked before hook install retries.
|
|
63
|
+
RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings];
|
|
64
|
+
if (DevConnectPersistedMetroHost().length > 0) {
|
|
65
|
+
return;
|
|
32
66
|
}
|
|
33
|
-
|
|
67
|
+
[settings resetToDefaults];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static BOOL DevConnectIsExpoProject(void)
|
|
71
|
+
{
|
|
72
|
+
static dispatch_once_t onceToken;
|
|
73
|
+
static BOOL isExpo = NO;
|
|
74
|
+
dispatch_once(&onceToken, ^{
|
|
75
|
+
// Heuristic: matches common Expo prebuild / dev-client binaries.
|
|
76
|
+
isExpo = NSClassFromString(@"EXAppDelegateWrapper") != nil
|
|
77
|
+
|| [[NSBundle mainBundle] objectForInfoDictionaryKey:@"EXUpdatesURL"] != nil
|
|
78
|
+
|| [[NSBundle mainBundle] objectForInfoDictionaryKey:@"EXPO_RUNTIME_VERSION"] != nil;
|
|
79
|
+
});
|
|
80
|
+
return isExpo;
|
|
34
81
|
}
|
|
35
82
|
|
|
36
|
-
|
|
83
|
+
/// Bundle root Metro expects when DevConnect steers the packager (index for plain RN, virtual entry for Expo).
|
|
84
|
+
static NSString *DevConnectMetroBundleRoot(void)
|
|
37
85
|
{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
86
|
+
return DevConnectIsExpoProject() ? kExpoVirtualMetroEntry : kBundleRoot;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static NSURL *DevConnectMetroURLForPersistedHost(void)
|
|
90
|
+
{
|
|
91
|
+
NSString *host = DevConnectPersistedMetroHost();
|
|
92
|
+
if (host.length == 0) {
|
|
93
|
+
return nil;
|
|
94
|
+
}
|
|
95
|
+
RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings];
|
|
96
|
+
settings.jsLocation = host;
|
|
97
|
+
return [settings jsBundleURLForBundleRoot:DevConnectMetroBundleRoot()];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// When no DevConnect host: block guessPackagerHost (simulator often has Metro on :8081).
|
|
101
|
+
static NSString *replacement_packagerServerHostPort(id self, SEL _cmd)
|
|
102
|
+
{
|
|
103
|
+
NSString *host = DevConnectPersistedMetroHost();
|
|
104
|
+
if (host.length == 0) {
|
|
105
|
+
return nil;
|
|
106
|
+
}
|
|
107
|
+
[(RCTBundleURLProvider *)self setJsLocation:host];
|
|
108
|
+
return host;
|
|
45
109
|
}
|
|
46
110
|
|
|
111
|
+
/// Primary hook: return embedded main.jsbundle before RN/Expo tries Metro / virtual-metro-entry.
|
|
112
|
+
static NSURL *replacement_jsBundleURLForBundleRoot_fallback(
|
|
113
|
+
id self, SEL _cmd, NSString *bundleRoot, NSURL * _Nonnull (^fallbackURLProvider)(void))
|
|
114
|
+
{
|
|
115
|
+
if (DevConnectPersistedMetroHost().length == 0) {
|
|
116
|
+
NSURL *embedded = DebugToolkitEmbeddedBundleURL();
|
|
117
|
+
if (embedded) {
|
|
118
|
+
NSLog(@"[DevConnect] cold start → embedded bundle (root=%@)", bundleRoot);
|
|
119
|
+
return embedded;
|
|
120
|
+
}
|
|
121
|
+
NSLog(@"[DevConnect] no embedded main.jsbundle — falling back to Metro (root=%@)", bundleRoot);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (gOrigJsBundleURLForBundleRootWithFallback) {
|
|
125
|
+
return gOrigJsBundleURLForBundleRootWithFallback(self, _cmd, bundleRoot, fallbackURLProvider);
|
|
126
|
+
}
|
|
127
|
+
return fallbackURLProvider ? fallbackURLProvider() : nil;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static void DebugToolkitInstallPackagerHook(Class cls)
|
|
131
|
+
{
|
|
132
|
+
if (gPackagerHookInstalled) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
Method method = class_getInstanceMethod(cls, @selector(packagerServerHostPort));
|
|
136
|
+
if (!method) {
|
|
137
|
+
NSLog(@"[DevConnect] packagerServerHostPort not found");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
IMP replacement = (IMP)replacement_packagerServerHostPort;
|
|
141
|
+
if (method_getImplementation(method) == replacement) {
|
|
142
|
+
gPackagerHookInstalled = YES;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
method_setImplementation(method, replacement);
|
|
146
|
+
gPackagerHookInstalled = YES;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static void DebugToolkitInstallBundleRootHook(Class cls)
|
|
150
|
+
{
|
|
151
|
+
if (gBundleRootHookInstalled) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
SEL selector = @selector(jsBundleURLForBundleRoot:fallbackURLProvider:);
|
|
155
|
+
Method method = class_getInstanceMethod(cls, selector);
|
|
156
|
+
if (!method) {
|
|
157
|
+
NSLog(@"[DevConnect] jsBundleURLForBundleRoot:fallbackURLProvider: not found");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
IMP replacement = (IMP)replacement_jsBundleURLForBundleRoot_fallback;
|
|
161
|
+
gOrigJsBundleURLForBundleRootWithFallback =
|
|
162
|
+
(NSURL * (*)(id, SEL, NSString *, NSURL * _Nonnull (^)(void)))method_getImplementation(method);
|
|
163
|
+
if ((IMP)gOrigJsBundleURLForBundleRootWithFallback == replacement) {
|
|
164
|
+
gBundleRootHookInstalled = YES;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
method_setImplementation(method, replacement);
|
|
168
|
+
gBundleRootHookInstalled = YES;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static void DebugToolkitInstallAllHooks(void)
|
|
172
|
+
{
|
|
173
|
+
if (gBundleRootHookInstalled && gPackagerHookInstalled) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
Class cls = NSClassFromString(@"RCTBundleURLProvider");
|
|
178
|
+
if (!cls) {
|
|
179
|
+
NSLog(@"[DevConnect] RCTBundleURLProvider not loaded — hooks will retry");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
DebugToolkitInstallBundleRootHook(cls);
|
|
184
|
+
DebugToolkitInstallPackagerHook(cls);
|
|
185
|
+
|
|
186
|
+
static BOOL didLogOutcome = NO;
|
|
187
|
+
if (!didLogOutcome && (gBundleRootHookInstalled || gPackagerHookInstalled)) {
|
|
188
|
+
didLogOutcome = YES;
|
|
189
|
+
if (DevConnectEmbeddedFirstHooksActive()) {
|
|
190
|
+
NSLog(@"[DevConnect] embedded-first hooks active (bundleRoot=%@ packager=%@)",
|
|
191
|
+
gBundleRootHookInstalled ? @"Y" : @"N",
|
|
192
|
+
gPackagerHookInstalled ? @"Y" : @"N");
|
|
193
|
+
} else {
|
|
194
|
+
NSLog(@"[DevConnect] embedded-first hooks FAILED — rebuild / check React linkage");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
NSURL *DebugToolkitMetroBundleURL(void)
|
|
200
|
+
{
|
|
201
|
+
return DevConnectMetroURLForPersistedHost();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// RCT_EXPORT_MODULE defines +load for module registration — use a separate class for hooks.
|
|
205
|
+
@interface DebugToolkitDevConnectBootstrap : NSObject
|
|
206
|
+
@end
|
|
207
|
+
|
|
208
|
+
@implementation DebugToolkitDevConnectBootstrap
|
|
209
|
+
|
|
210
|
+
+ (void)load
|
|
211
|
+
{
|
|
212
|
+
DebugToolkitPrepareBundleSourceIfNeeded();
|
|
213
|
+
DebugToolkitInstallAllHooks();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@end
|
|
217
|
+
|
|
47
218
|
#pragma mark - Module
|
|
48
219
|
|
|
49
220
|
@interface DebugToolkitDevConnect : NSObject <RCTBridgeModule>
|
|
@@ -56,61 +227,64 @@ RCT_EXPORT_MODULE(DebugToolkitDevConnect)
|
|
|
56
227
|
@synthesize bridge = _bridge;
|
|
57
228
|
@synthesize bundleManager = _bundleManager;
|
|
58
229
|
|
|
59
|
-
__attribute__((constructor))
|
|
60
|
-
static void devconnect_swizzle_init(void)
|
|
61
|
-
{
|
|
62
|
-
// Runs after all +load methods, before main() — same timing as +load but no conflict with RCT_EXPORT_MODULE
|
|
63
|
-
swizzleSourceURLForBridge(NSClassFromString(@"AppDelegate"));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
230
|
- (instancetype)init
|
|
67
231
|
{
|
|
68
232
|
if ((self = [super init])) {
|
|
69
|
-
|
|
70
|
-
if (!original_sourceURLForBridge) {
|
|
71
|
-
Class delegateClass = object_getClass([UIApplication sharedApplication].delegate);
|
|
72
|
-
swizzleSourceURLForBridge(delegateClass);
|
|
73
|
-
}
|
|
233
|
+
DebugToolkitInstallAllHooks();
|
|
74
234
|
}
|
|
75
235
|
return self;
|
|
76
236
|
}
|
|
77
237
|
|
|
78
|
-
+ (BOOL)requiresMainQueueSetup
|
|
79
|
-
{
|
|
80
|
-
return NO;
|
|
81
|
-
}
|
|
238
|
+
+ (BOOL)requiresMainQueueSetup { return NO; }
|
|
239
|
+
- (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); }
|
|
82
240
|
|
|
83
|
-
-
|
|
241
|
+
#pragma mark - Bundle Manager Resolution
|
|
242
|
+
|
|
243
|
+
- (RCTBundleManager *)resolveBundleManager
|
|
84
244
|
{
|
|
85
|
-
|
|
245
|
+
if (_bundleManager) return _bundleManager;
|
|
246
|
+
if (_bridge) return [_bridge moduleForClass:[RCTBundleManager class]];
|
|
247
|
+
return nil;
|
|
86
248
|
}
|
|
87
249
|
|
|
88
|
-
#pragma mark -
|
|
250
|
+
#pragma mark - Host Parsing
|
|
89
251
|
|
|
90
|
-
- (
|
|
252
|
+
- (NSString *)normalizeHostPort:(NSString *)hostPort
|
|
91
253
|
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
254
|
+
NSRange sep = [hostPort rangeOfString:@":" options:NSBackwardsSearch];
|
|
255
|
+
NSString *host = sep.location == NSNotFound ? hostPort : [hostPort substringToIndex:sep.location];
|
|
256
|
+
NSString *portStr = sep.location == NSNotFound ? @"" : [hostPort substringFromIndex:sep.location + 1];
|
|
257
|
+
|
|
258
|
+
NSNumberFormatter *formatter = [NSNumberFormatter new];
|
|
259
|
+
formatter.numberStyle = NSNumberFormatterDecimalStyle;
|
|
260
|
+
NSNumber *parsed = [formatter numberFromString:portStr];
|
|
261
|
+
int port;
|
|
262
|
+
if (parsed && parsed.intValue > 0 && parsed.intValue <= 65535) {
|
|
263
|
+
port = parsed.intValue;
|
|
264
|
+
} else {
|
|
265
|
+
#ifdef RCT_METRO_PORT
|
|
266
|
+
port = RCT_METRO_PORT;
|
|
267
|
+
#else
|
|
268
|
+
port = 8081;
|
|
269
|
+
#endif
|
|
97
270
|
}
|
|
98
|
-
return
|
|
271
|
+
return [NSString stringWithFormat:@"%@:%d", host, port];
|
|
99
272
|
}
|
|
100
273
|
|
|
101
|
-
#pragma mark -
|
|
274
|
+
#pragma mark - Reload helper
|
|
102
275
|
|
|
103
|
-
|
|
104
|
-
rejecter:(__unused RCTPromiseRejectBlock)reject)
|
|
276
|
+
- (void)reloadWithBundleURL:(NSURL *)bundleURL reason:(NSString *)reason
|
|
105
277
|
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
reject(@"native_error", exception.reason, nil);
|
|
278
|
+
if (bundleURL) {
|
|
279
|
+
RCTBundleManager *bm = [self resolveBundleManager];
|
|
280
|
+
if (bm) bm.bundleURL = bundleURL;
|
|
281
|
+
RCTReloadCommandSetBundleURL(bundleURL);
|
|
111
282
|
}
|
|
283
|
+
RCTTriggerReloadCommandListeners(reason);
|
|
112
284
|
}
|
|
113
285
|
|
|
286
|
+
#pragma mark - Exported Methods
|
|
287
|
+
|
|
114
288
|
RCT_EXPORT_METHOD(applyMetroHost:(NSString *)hostPort
|
|
115
289
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
116
290
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
@@ -120,71 +294,24 @@ RCT_EXPORT_METHOD(applyMetroHost:(NSString *)hostPort
|
|
|
120
294
|
reject(@"invalid_host", @"Metro host cannot be empty.", nil);
|
|
121
295
|
return;
|
|
122
296
|
}
|
|
297
|
+
NSString *normalized = [self normalizeHostPort:hostPort];
|
|
123
298
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Parse host and port
|
|
127
|
-
NSRange separator = [hostPort rangeOfString:@":" options:NSBackwardsSearch];
|
|
128
|
-
NSString *host = separator.location == NSNotFound
|
|
129
|
-
? hostPort
|
|
130
|
-
: [hostPort substringToIndex:separator.location];
|
|
131
|
-
NSString *port = separator.location == NSNotFound
|
|
132
|
-
? @""
|
|
133
|
-
: [hostPort substringFromIndex:separator.location + 1];
|
|
134
|
-
|
|
135
|
-
if (host.length == 0 && port.length == 0) {
|
|
136
|
-
[self resetMetroHost:resolve rejecter:reject];
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
NSNumberFormatter *formatter = [NSNumberFormatter new];
|
|
141
|
-
formatter.numberStyle = NSNumberFormatterDecimalStyle;
|
|
142
|
-
NSNumber *portNumber = [formatter numberFromString:port];
|
|
143
|
-
if (portNumber == nil) {
|
|
144
|
-
#ifdef RCT_METRO_PORT
|
|
145
|
-
portNumber = [NSNumber numberWithInt:RCT_METRO_PORT];
|
|
146
|
-
#else
|
|
147
|
-
portNumber = [NSNumber numberWithInt:8081];
|
|
148
|
-
#endif
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
NSString *normalizedHostPort = [NSString stringWithFormat:@"%@:%d", host, portNumber.intValue];
|
|
152
|
-
|
|
153
|
-
// Persist for AppDelegate swizzle (Release mode + restart)
|
|
154
|
-
[[NSUserDefaults standardUserDefaults] setObject:normalizedHostPort forKey:kDevConnectMetroHost];
|
|
155
|
-
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
156
|
-
|
|
157
|
-
// Also set jsLocation (Debug mode + hot reload)
|
|
158
|
-
settings.jsLocation = normalizedHostPort;
|
|
159
|
-
|
|
160
|
-
// Try hot reload via bundleManager (works in Debug)
|
|
161
|
-
NSURL *bundleURL = nil;
|
|
162
|
-
if (DebugToolkitBundleRoot.length > 0) {
|
|
163
|
-
bundleURL = [settings jsBundleURLForBundleRoot:DebugToolkitBundleRoot];
|
|
164
|
-
}
|
|
165
|
-
RCTBundleManager *bm = [self resolveBundleManager];
|
|
166
|
-
if (bm) {
|
|
167
|
-
bm.bundleURL = bundleURL;
|
|
168
|
-
}
|
|
169
|
-
#ifdef RCTReloadCommandSetBundleURL
|
|
170
|
-
else if (bundleURL) {
|
|
171
|
-
RCTReloadCommandSetBundleURL(bundleURL);
|
|
172
|
-
}
|
|
173
|
-
#endif
|
|
174
|
-
|
|
175
|
-
NSLog(@"[DevConnect] applyMetroHost: %@ | bm=%@ | bridge=%@ | url=%@",
|
|
176
|
-
normalizedHostPort, bm ? @"YES" : @"nil", _bridge ? @"YES" : @"nil", bundleURL);
|
|
299
|
+
DevConnectSetPersistedMetroHost(normalized);
|
|
177
300
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
301
|
+
RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings];
|
|
302
|
+
settings.jsLocation = normalized;
|
|
303
|
+
NSURL *bundleURL = [settings jsBundleURLForBundleRoot:DevConnectMetroBundleRoot()];
|
|
304
|
+
|
|
305
|
+
[self reloadWithBundleURL:bundleURL reason:@"Dev menu - apply changes"];
|
|
306
|
+
|
|
307
|
+
NSLog(@"[DevConnect] applyMetroHost host=%@ url=%@", normalized, bundleURL);
|
|
308
|
+
resolve(@{
|
|
309
|
+
@"hostPort": normalized,
|
|
310
|
+
@"bundleURL": bundleURL.absoluteString ?: [NSNull null],
|
|
311
|
+
});
|
|
312
|
+
} @catch (NSException *e) {
|
|
313
|
+
NSLog(@"[DevConnect] applyMetroHost EXCEPTION: %@", e.reason);
|
|
314
|
+
reject(@"native_error", e.reason ?: @"unknown", nil);
|
|
188
315
|
}
|
|
189
316
|
}
|
|
190
317
|
|
|
@@ -192,29 +319,57 @@ RCT_EXPORT_METHOD(resetMetroHost:(RCTPromiseResolveBlock)resolve
|
|
|
192
319
|
rejecter:(__unused RCTPromiseRejectBlock)reject)
|
|
193
320
|
{
|
|
194
321
|
@try {
|
|
195
|
-
|
|
196
|
-
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kDevConnectMetroHost];
|
|
197
|
-
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
322
|
+
DevConnectSetPersistedMetroHost(nil);
|
|
198
323
|
|
|
199
|
-
// Reset RN settings
|
|
200
324
|
RCTBundleURLProvider *settings = [RCTBundleURLProvider sharedSettings];
|
|
201
325
|
[settings resetToDefaults];
|
|
202
|
-
NSURL *bundleURL = [settings jsBundleURLForFallbackExtension:nil];
|
|
203
326
|
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
|
|
327
|
+
NSURL *embedded = DebugToolkitEmbeddedBundleURL();
|
|
328
|
+
if (!embedded) {
|
|
329
|
+
embedded = [settings jsBundleURLForFallbackExtension:nil];
|
|
207
330
|
}
|
|
208
331
|
|
|
209
|
-
|
|
210
|
-
|
|
332
|
+
[self reloadWithBundleURL:embedded reason:@"Dev menu - reset to default"];
|
|
333
|
+
|
|
334
|
+
NSLog(@"[DevConnect] resetMetroHost url=%@", embedded);
|
|
211
335
|
resolve([NSNull null]);
|
|
212
|
-
} @catch (NSException *
|
|
213
|
-
NSLog(@"[DevConnect] resetMetroHost EXCEPTION: %@",
|
|
214
|
-
reject(@"native_error",
|
|
336
|
+
} @catch (NSException *e) {
|
|
337
|
+
NSLog(@"[DevConnect] resetMetroHost EXCEPTION: %@", e.reason);
|
|
338
|
+
reject(@"native_error", e.reason ?: @"unknown", nil);
|
|
215
339
|
}
|
|
216
340
|
}
|
|
217
341
|
|
|
342
|
+
RCT_EXPORT_METHOD(getMetroHost:(RCTPromiseResolveBlock)resolve
|
|
343
|
+
rejecter:(__unused RCTPromiseRejectBlock)reject)
|
|
344
|
+
{
|
|
345
|
+
NSString *host = DevConnectPersistedMetroHost();
|
|
346
|
+
resolve(host.length > 0 ? host : [NSNull null]);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
RCT_EXPORT_METHOD(getDiagnostics:(RCTPromiseResolveBlock)resolve
|
|
350
|
+
rejecter:(__unused RCTPromiseRejectBlock)reject)
|
|
351
|
+
{
|
|
352
|
+
Class realDelegate = object_getClass([UIApplication sharedApplication].delegate);
|
|
353
|
+
NSString *delegateName = realDelegate ? NSStringFromClass(realDelegate) : @"unknown";
|
|
354
|
+
NSString *persisted = DevConnectPersistedMetroHost();
|
|
355
|
+
|
|
356
|
+
#if DEBUG
|
|
357
|
+
BOOL isDebug = YES;
|
|
358
|
+
#else
|
|
359
|
+
BOOL isDebug = NO;
|
|
360
|
+
#endif
|
|
361
|
+
|
|
362
|
+
resolve(@{
|
|
363
|
+
@"persistedMetroHost": persisted.length > 0 ? persisted : [NSNull null],
|
|
364
|
+
@"appDelegateClass": delegateName,
|
|
365
|
+
@"isDebugBuild": @(isDebug),
|
|
366
|
+
@"hasEmbeddedBundle": @(DebugToolkitEmbeddedBundleURL() != nil),
|
|
367
|
+
@"embeddedFirstHookInstalled": @(DevConnectEmbeddedFirstHooksActive()),
|
|
368
|
+
@"packagerHookInstalled": @(gPackagerHookInstalled),
|
|
369
|
+
@"bundleRootHookInstalled": @(gBundleRootHookInstalled),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
218
373
|
RCT_EXPORT_METHOD(getPreference:(NSString *)key
|
|
219
374
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
220
375
|
rejecter:(__unused RCTPromiseRejectBlock)reject)
|
|
@@ -222,8 +377,8 @@ RCT_EXPORT_METHOD(getPreference:(NSString *)key
|
|
|
222
377
|
@try {
|
|
223
378
|
NSString *value = [[NSUserDefaults standardUserDefaults] stringForKey:key];
|
|
224
379
|
resolve(value ?: [NSNull null]);
|
|
225
|
-
} @catch (NSException *
|
|
226
|
-
reject(@"native_error",
|
|
380
|
+
} @catch (NSException *e) {
|
|
381
|
+
reject(@"native_error", e.reason ?: @"unknown", nil);
|
|
227
382
|
}
|
|
228
383
|
}
|
|
229
384
|
|
|
@@ -236,8 +391,8 @@ RCT_EXPORT_METHOD(setPreference:(NSString *)key
|
|
|
236
391
|
[[NSUserDefaults standardUserDefaults] setObject:value forKey:key];
|
|
237
392
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
238
393
|
resolve([NSNull null]);
|
|
239
|
-
} @catch (NSException *
|
|
240
|
-
reject(@"native_error",
|
|
394
|
+
} @catch (NSException *e) {
|
|
395
|
+
reject(@"native_error", e.reason ?: @"unknown", nil);
|
|
241
396
|
}
|
|
242
397
|
}
|
|
243
398
|
|
|
@@ -256,40 +411,27 @@ RCT_EXPORT_METHOD(getLocalIp:(RCTPromiseResolveBlock)resolve
|
|
|
256
411
|
{
|
|
257
412
|
@try {
|
|
258
413
|
struct ifaddrs *interfaces = NULL;
|
|
259
|
-
if (getifaddrs(&interfaces)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
iface = interfaces;
|
|
276
|
-
while (iface != NULL) {
|
|
277
|
-
if (iface->ifa_addr != NULL && iface->ifa_addr->sa_family == AF_INET && !(iface->ifa_flags & IFF_LOOPBACK)) {
|
|
278
|
-
char addrStr[INET_ADDRSTRLEN];
|
|
279
|
-
struct sockaddr_in *sin = (struct sockaddr_in *)iface->ifa_addr;
|
|
280
|
-
inet_ntop(AF_INET, &sin->sin_addr, addrStr, sizeof(addrStr));
|
|
281
|
-
NSString *ip = [NSString stringWithUTF8String:addrStr];
|
|
282
|
-
freeifaddrs(interfaces);
|
|
283
|
-
resolve(ip);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
iface = iface->ifa_next;
|
|
287
|
-
}
|
|
414
|
+
if (getifaddrs(&interfaces) != 0) {
|
|
415
|
+
resolve([NSNull null]);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
NSString *preferred = nil;
|
|
420
|
+
NSString *fallback = nil;
|
|
421
|
+
for (struct ifaddrs *iface = interfaces; iface != NULL; iface = iface->ifa_next) {
|
|
422
|
+
if (!iface->ifa_addr || iface->ifa_addr->sa_family != AF_INET) continue;
|
|
423
|
+
if (iface->ifa_flags & IFF_LOOPBACK) continue;
|
|
424
|
+
char addrStr[INET_ADDRSTRLEN];
|
|
425
|
+
struct sockaddr_in *sin = (struct sockaddr_in *)iface->ifa_addr;
|
|
426
|
+
inet_ntop(AF_INET, &sin->sin_addr, addrStr, sizeof(addrStr));
|
|
427
|
+
NSString *ip = [NSString stringWithUTF8String:addrStr];
|
|
428
|
+
if (strcmp(iface->ifa_name, "en0") == 0) { preferred = ip; break; }
|
|
429
|
+
if (!fallback) fallback = ip;
|
|
288
430
|
}
|
|
289
431
|
freeifaddrs(interfaces);
|
|
290
|
-
resolve([NSNull null]);
|
|
291
|
-
} @catch (NSException *
|
|
292
|
-
reject(@"native_error",
|
|
432
|
+
resolve(preferred ?: fallback ?: [NSNull null]);
|
|
433
|
+
} @catch (NSException *e) {
|
|
434
|
+
reject(@"native_error", e.reason ?: @"unknown", nil);
|
|
293
435
|
}
|
|
294
436
|
}
|
|
295
437
|
|