react-native-debug-toolkit 3.3.3 → 3.3.4
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 +35 -2
- package/README.zh-CN.md +35 -2
- package/app.plugin.js +51 -0
- package/bin/debug-toolkit.js +13 -12
- package/dev-client.js +3 -0
- package/ios/DebugToolkitDevConnect.mm +8 -41
- package/package.json +4 -2
- package/scripts/bundle/android.js +101 -0
- package/scripts/bundle/cli.js +57 -0
- package/scripts/bundle/doctor.js +38 -0
- package/scripts/bundle/ios.js +179 -0
- package/scripts/bundle/setup.js +39 -0
- package/scripts/debug-bundle.gradle +147 -0
- package/scripts/android-debug-bundle.gradle +0 -23
- package/scripts/eas-postinstall.sh +0 -6
- package/scripts/embed-android.js +0 -116
- package/scripts/embed-expo.js +0 -109
- package/scripts/embed-ios.js +0 -119
- package/scripts/embed.js +0 -224
package/README.md
CHANGED
|
@@ -77,11 +77,44 @@ 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
|
-
|
|
80
|
+
### Embedded Debug Bundle
|
|
81
|
+
|
|
82
|
+
Debug builds need an embedded JS bundle for cold start when Metro is off.
|
|
83
|
+
|
|
84
|
+
Bare React Native:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx debug-toolkit setup-bundle
|
|
88
|
+
git diff
|
|
89
|
+
git commit -am "chore: enable debug bundle embedding"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Expo dev-client:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"expo": {
|
|
97
|
+
"plugins": [
|
|
98
|
+
["react-native-debug-toolkit/dev-client", { "embedBundle": true }]
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Verify built artifacts:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx debug-toolkit doctor-bundle --platform ios --app path/to/App.app
|
|
108
|
+
npx debug-toolkit doctor-bundle --platform android --apk path/to/app-debug.apk
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
After setup, build machines run normal Xcode, Gradle, React Native, or EAS commands. Do not run a separate mutation command on every build.
|
|
112
|
+
|
|
113
|
+
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. Use **Reset** to go back to the embedded bundle.
|
|
81
114
|
|
|
82
115
|
> **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
116
|
|
|
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).
|
|
117
|
+
**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).
|
|
85
118
|
|
|
86
119
|
The IP and ports are persisted through AsyncStorage when installed, or through the native module after rebuild.
|
|
87
120
|
|
package/README.zh-CN.md
CHANGED
|
@@ -77,11 +77,44 @@ App 内打开 Debug Panel -> `DevConnect` -> `Send Once` 或 `Start Live Sync`
|
|
|
77
77
|
|
|
78
78
|
DevConnect 自动识别模拟器/真机,模拟器下自动使用本机 Metro/daemon 地址。真机需输入电脑 IP 地址。
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
### Debug 包内置 Bundle
|
|
81
|
+
|
|
82
|
+
Debug 包要在 Metro 关闭时冷启动,必须内置 JS bundle。
|
|
83
|
+
|
|
84
|
+
Bare React Native:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx debug-toolkit setup-bundle
|
|
88
|
+
git diff
|
|
89
|
+
git commit -am "chore: enable debug bundle embedding"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Expo dev-client:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"expo": {
|
|
97
|
+
"plugins": [
|
|
98
|
+
["react-native-debug-toolkit/dev-client", { "embedBundle": true }]
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
验证产物:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx debug-toolkit doctor-bundle --platform ios --app path/to/App.app
|
|
108
|
+
npx debug-toolkit doctor-bundle --platform android --apk path/to/app-debug.apk
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
setup 后配置进仓库,打包机继续跑正常 Xcode、Gradle、React Native 或 EAS 命令。不要每次打包再跑额外修改命令。
|
|
112
|
+
|
|
113
|
+
Remote JS Bundle:先在电脑启动 Metro,在 `DevConnect` 输入电脑 IP 和 Metro 端口,然后点 `Use Metro Bundle`。DevConnect 会持久化 host 并从 Metro 热重载。点 **Reset** 可回到内置 bundle。
|
|
81
114
|
|
|
82
115
|
> **仅 Debug 包可用。** Release 包控件会显示 `release: disabled` 并禁用。
|
|
83
116
|
|
|
84
|
-
**iOS — 无需改 AppDelegate。** 安装本库后会 hook `RCTBundleURLProvider`:**未配置 IP 时冷启动走内置 `main.jsbundle`**,只有在面板里应用 host 后才连 Metro(可避免 Metro 未开时的 `.expo/.virtual-metro-entry`
|
|
117
|
+
**iOS — 无需改 AppDelegate。** 安装本库后会 hook `RCTBundleURLProvider`:**未配置 IP 时冷启动走内置 `main.jsbundle`**,只有在面板里应用 host 后才连 Metro(可避免 Metro 未开时的 `.expo/.virtual-metro-entry` 红屏)。
|
|
85
118
|
|
|
86
119
|
IP 和端口会通过 AsyncStorage 持久化;如果没装 AsyncStorage,则在重建后通过本库原生模块持久化。
|
|
87
120
|
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function markTestConfig(config) {
|
|
4
|
+
config.extra = config.extra || {};
|
|
5
|
+
config.extra.reactNativeDebugToolkit = {
|
|
6
|
+
...(config.extra.reactNativeDebugToolkit || {}),
|
|
7
|
+
embedBundle: true,
|
|
8
|
+
};
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function loadConfigPlugins() {
|
|
13
|
+
try {
|
|
14
|
+
return require('@expo/config-plugins');
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'react-native-debug-toolkit/dev-client requires @expo/config-plugins during Expo prebuild.',
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function withDebugToolkitDevClient(config, props = {}) {
|
|
23
|
+
if (!props.embedBundle) {
|
|
24
|
+
return config;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (props._testOnly) {
|
|
28
|
+
return markTestConfig(config);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { withDangerousMod } = loadConfigPlugins();
|
|
32
|
+
const { setupIosBundle } = require('./scripts/bundle/ios');
|
|
33
|
+
const { setupAndroidBundle } = require('./scripts/bundle/android');
|
|
34
|
+
|
|
35
|
+
config = withDangerousMod(config, ['ios', async (modConfig) => {
|
|
36
|
+
setupIosBundle({
|
|
37
|
+
cwd: modConfig.modRequest.projectRoot,
|
|
38
|
+
iosTarget: props.iosTarget,
|
|
39
|
+
});
|
|
40
|
+
return modConfig;
|
|
41
|
+
}]);
|
|
42
|
+
|
|
43
|
+
config = withDangerousMod(config, ['android', async (modConfig) => {
|
|
44
|
+
setupAndroidBundle({ cwd: modConfig.modRequest.projectRoot });
|
|
45
|
+
return modConfig;
|
|
46
|
+
}]);
|
|
47
|
+
|
|
48
|
+
return config;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = withDebugToolkitDevClient;
|
package/bin/debug-toolkit.js
CHANGED
|
@@ -27,21 +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
|
|
30
|
+
+ ' debug-toolkit setup-bundle [--platform ios|android] [--undo] [--check] [--ios-target <name>]\n'
|
|
31
|
+
+ ' debug-toolkit doctor-bundle --platform ios --app <path-to.app>\n'
|
|
32
|
+
+ ' debug-toolkit doctor-bundle --platform android --apk <path-to.apk>\n'
|
|
31
33
|
+ '\n'
|
|
32
34
|
+ 'Starts the debug toolkit: daemon (HTTP + Web Console) and MCP stdio server.\n'
|
|
33
35
|
+ '\n'
|
|
34
36
|
+ 'Commands:\n'
|
|
35
|
-
+ '
|
|
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'
|
|
37
|
+
+ ' setup-bundle Persistently configure host app debug builds to embed JS bundle\n'
|
|
38
|
+
+ ' doctor-bundle Verify source config or built app package contains embedded bundle\n'
|
|
41
39
|
+ '\n'
|
|
42
40
|
+ 'Daemon options:\n'
|
|
43
41
|
+ ' --host <addr> Host to bind (default: 0.0.0.0)\n'
|
|
44
|
-
|
|
42
|
+
+ ' --port <port> Port to bind (default: 3799)\n'
|
|
45
43
|
+ ' --token <str> Auth token for daemon endpoints\n'
|
|
46
44
|
+ ' --store <path> Device log store path\n'
|
|
47
45
|
+ ' --daemon-only Start only the HTTP daemon and Web Console\n'
|
|
@@ -66,10 +64,13 @@ async function main() {
|
|
|
66
64
|
return;
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
67
|
+
if (['setup-bundle', 'doctor-bundle', 'embed'].includes(args[0])) {
|
|
68
|
+
const { runBundleCli } = require('../scripts/bundle/cli');
|
|
69
|
+
const code = await runBundleCli(args);
|
|
70
|
+
if (code !== 0) {
|
|
71
|
+
process.exitCode = code;
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
const host = readOption(args, '--host', process.env.DEBUG_TOOLKIT_DAEMON_HOST || DEFAULT_HOST);
|
package/dev-client.js
ADDED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
// Debug-only Metro host switching — zero host-app native changes required.
|
|
16
16
|
//
|
|
17
17
|
// RN/Expo Debug templates call jsBundleURLForBundleRoot:fallbackURLProvider:, which consults
|
|
18
|
-
// -packagerServerHostPort (guessPackagerHost on simulator when Metro is running). We hook
|
|
19
|
-
//
|
|
20
|
-
//
|
|
18
|
+
// -packagerServerHostPort (guessPackagerHost on simulator when Metro is running). We only hook
|
|
19
|
+
// jsBundleURLForBundleRoot:fallbackURLProvider: so no DevConnect host starts from main.jsbundle.
|
|
20
|
+
// Persisted DevConnect hosts still flow through RCTBundleURLProvider, preserving RN's reachability
|
|
21
|
+
// and fallback behavior when a saved Metro host is stale.
|
|
21
22
|
//
|
|
22
23
|
// Optional: host apps may call DebugToolkitMetroBundleURL() from bundleURL() for explicit control.
|
|
23
24
|
|
|
@@ -26,7 +27,6 @@ static NSString *const kBundleRoot = @"index";
|
|
|
26
27
|
static NSString *const kExpoVirtualMetroEntry = @".expo/.virtual-metro-entry";
|
|
27
28
|
static NSString *const kMetroHostKey = @"_devconnect_metro_host";
|
|
28
29
|
|
|
29
|
-
static BOOL gPackagerHookInstalled = NO;
|
|
30
30
|
static BOOL gBundleRootHookInstalled = NO;
|
|
31
31
|
|
|
32
32
|
static NSURL *(*gOrigJsBundleURLForBundleRootWithFallback)(id, SEL, NSString *, NSURL * _Nonnull (^)(void));
|
|
@@ -97,17 +97,6 @@ static NSURL *DevConnectMetroURLForPersistedHost(void)
|
|
|
97
97
|
return [settings jsBundleURLForBundleRoot:DevConnectMetroBundleRoot()];
|
|
98
98
|
}
|
|
99
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;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
100
|
/// Primary hook: return embedded main.jsbundle before RN/Expo tries Metro / virtual-metro-entry.
|
|
112
101
|
static NSURL *replacement_jsBundleURLForBundleRoot_fallback(
|
|
113
102
|
id self, SEL _cmd, NSString *bundleRoot, NSURL * _Nonnull (^fallbackURLProvider)(void))
|
|
@@ -127,25 +116,6 @@ static NSURL *replacement_jsBundleURLForBundleRoot_fallback(
|
|
|
127
116
|
return fallbackURLProvider ? fallbackURLProvider() : nil;
|
|
128
117
|
}
|
|
129
118
|
|
|
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
119
|
static void DebugToolkitInstallBundleRootHook(Class cls)
|
|
150
120
|
{
|
|
151
121
|
if (gBundleRootHookInstalled) {
|
|
@@ -170,7 +140,7 @@ static void DebugToolkitInstallBundleRootHook(Class cls)
|
|
|
170
140
|
|
|
171
141
|
static void DebugToolkitInstallAllHooks(void)
|
|
172
142
|
{
|
|
173
|
-
if (gBundleRootHookInstalled
|
|
143
|
+
if (gBundleRootHookInstalled) {
|
|
174
144
|
return;
|
|
175
145
|
}
|
|
176
146
|
|
|
@@ -181,15 +151,12 @@ static void DebugToolkitInstallAllHooks(void)
|
|
|
181
151
|
}
|
|
182
152
|
|
|
183
153
|
DebugToolkitInstallBundleRootHook(cls);
|
|
184
|
-
DebugToolkitInstallPackagerHook(cls);
|
|
185
154
|
|
|
186
155
|
static BOOL didLogOutcome = NO;
|
|
187
|
-
if (!didLogOutcome
|
|
156
|
+
if (!didLogOutcome) {
|
|
188
157
|
didLogOutcome = YES;
|
|
189
158
|
if (DevConnectEmbeddedFirstHooksActive()) {
|
|
190
|
-
NSLog(@"[DevConnect] embedded-first
|
|
191
|
-
gBundleRootHookInstalled ? @"Y" : @"N",
|
|
192
|
-
gPackagerHookInstalled ? @"Y" : @"N");
|
|
159
|
+
NSLog(@"[DevConnect] embedded-first hook active");
|
|
193
160
|
} else {
|
|
194
161
|
NSLog(@"[DevConnect] embedded-first hooks FAILED — rebuild / check React linkage");
|
|
195
162
|
}
|
|
@@ -365,7 +332,7 @@ RCT_EXPORT_METHOD(getDiagnostics:(RCTPromiseResolveBlock)resolve
|
|
|
365
332
|
@"isDebugBuild": @(isDebug),
|
|
366
333
|
@"hasEmbeddedBundle": @(DebugToolkitEmbeddedBundleURL() != nil),
|
|
367
334
|
@"embeddedFirstHookInstalled": @(DevConnectEmbeddedFirstHooksActive()),
|
|
368
|
-
@"packagerHookInstalled": @
|
|
335
|
+
@"packagerHookInstalled": @NO,
|
|
369
336
|
@"bundleRootHookInstalled": @(gBundleRootHookInstalled),
|
|
370
337
|
});
|
|
371
338
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-debug-toolkit",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.4",
|
|
4
4
|
"description": "A local-first React Native debug toolkit with Web Console, HTTP API, and MCP support for AI-readable app logs",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"bin",
|
|
12
12
|
"node",
|
|
13
13
|
"scripts",
|
|
14
|
+
"app.plugin.js",
|
|
15
|
+
"dev-client.js",
|
|
14
16
|
"ios",
|
|
15
17
|
"android",
|
|
16
18
|
"react-native-debug-toolkit.podspec",
|
|
@@ -95,6 +97,6 @@
|
|
|
95
97
|
},
|
|
96
98
|
"dependencies": {
|
|
97
99
|
"@babel/runtime": "^7.29.2",
|
|
98
|
-
"
|
|
100
|
+
"xcode": "^3.0.1"
|
|
99
101
|
}
|
|
100
102
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const BEGIN = '// react-native-debug-toolkit: begin debug bundle';
|
|
8
|
+
const END = '// react-native-debug-toolkit: end debug bundle';
|
|
9
|
+
const REL_SCRIPT = '../../node_modules/react-native-debug-toolkit/scripts/debug-bundle.gradle';
|
|
10
|
+
const EXPECTED_TASK = 'createDebugToolkitDebugJsAndAssets';
|
|
11
|
+
const GRADLE_TASKS_ARGS = [':app:tasks', '--all', '--no-daemon', '--console=plain'];
|
|
12
|
+
|
|
13
|
+
function findGradleFile(cwd) {
|
|
14
|
+
const groovy = path.join(cwd, 'android/app/build.gradle');
|
|
15
|
+
const kotlin = path.join(cwd, 'android/app/build.gradle.kts');
|
|
16
|
+
|
|
17
|
+
if (fs.existsSync(kotlin)) {
|
|
18
|
+
return { file: kotlin, kind: 'kotlin' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (fs.existsSync(groovy)) {
|
|
22
|
+
return { file: groovy, kind: 'groovy' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw new Error('android/app/build.gradle(.kts) not found.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function block(kind) {
|
|
29
|
+
const applyLine = kind === 'kotlin'
|
|
30
|
+
? `apply(from = "${REL_SCRIPT}")`
|
|
31
|
+
: `apply from: "${REL_SCRIPT}"`;
|
|
32
|
+
|
|
33
|
+
return `${BEGIN}\n${applyLine}\n${END}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function removeBlock(content) {
|
|
37
|
+
return content.replace(
|
|
38
|
+
/\/\/ react-native-debug-toolkit: begin debug bundle\n[\s\S]*?\/\/ react-native-debug-toolkit: end debug bundle\n?/g,
|
|
39
|
+
'',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setupAndroidBundle(options) {
|
|
44
|
+
const gradle = findGradleFile(options.cwd);
|
|
45
|
+
const content = fs.readFileSync(gradle.file, 'utf8');
|
|
46
|
+
|
|
47
|
+
if (content.includes(BEGIN)) {
|
|
48
|
+
return { ok: true, changed: false, file: gradle.file };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(gradle.file, `${content.trimEnd()}\n\n${block(gradle.kind)}\n`);
|
|
52
|
+
return { ok: true, changed: true, file: gradle.file };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function undoAndroidBundle(options) {
|
|
56
|
+
const gradle = findGradleFile(options.cwd);
|
|
57
|
+
const content = fs.readFileSync(gradle.file, 'utf8');
|
|
58
|
+
const next = removeBlock(content).replace(/\n{3,}/g, '\n\n');
|
|
59
|
+
|
|
60
|
+
if (next === content) {
|
|
61
|
+
return { ok: true, changed: false, file: gradle.file };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync(gradle.file, next);
|
|
65
|
+
return { ok: true, changed: true, file: gradle.file };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function checkAndroidBundle(options) {
|
|
69
|
+
const gradle = findGradleFile(options.cwd);
|
|
70
|
+
const content = fs.readFileSync(gradle.file, 'utf8');
|
|
71
|
+
const hasMarker = content.includes(BEGIN) && content.includes('scripts/debug-bundle.gradle');
|
|
72
|
+
|
|
73
|
+
if (!hasMarker) {
|
|
74
|
+
return { ok: false, reason: 'marker_missing', file: gradle.file };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const androidDir = path.join(options.cwd, 'android');
|
|
78
|
+
const gradlew = path.join(androidDir, 'gradlew');
|
|
79
|
+
const command = fs.existsSync(gradlew) ? gradlew : 'gradle';
|
|
80
|
+
const runGradle = options.runGradle || ((cmd, args, opts) => execFileSync(cmd, args, {
|
|
81
|
+
cwd: opts.cwd,
|
|
82
|
+
encoding: 'utf8',
|
|
83
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
84
|
+
}));
|
|
85
|
+
const output = runGradle(command, GRADLE_TASKS_ARGS, { cwd: androidDir });
|
|
86
|
+
|
|
87
|
+
if (!String(output).includes(EXPECTED_TASK)) {
|
|
88
|
+
return { ok: false, reason: 'gradle_task_missing', file: gradle.file };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { ok: true, file: gradle.file };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
setupAndroidBundle,
|
|
96
|
+
undoAndroidBundle,
|
|
97
|
+
checkAndroidBundle,
|
|
98
|
+
BEGIN,
|
|
99
|
+
END,
|
|
100
|
+
EXPECTED_TASK,
|
|
101
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function readOption(args, name) {
|
|
4
|
+
const index = args.indexOf(name);
|
|
5
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function hasFlag(args, name) {
|
|
9
|
+
return args.includes(name);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseCommon(args, cwd) {
|
|
13
|
+
return {
|
|
14
|
+
cwd,
|
|
15
|
+
platform: readOption(args, '--platform') || 'all',
|
|
16
|
+
undo: hasFlag(args, '--undo'),
|
|
17
|
+
check: hasFlag(args, '--check'),
|
|
18
|
+
iosTarget: readOption(args, '--ios-target'),
|
|
19
|
+
yes: hasFlag(args, '--yes'),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function runBundleCli(args, deps = {}) {
|
|
24
|
+
const cwd = deps.cwd || process.cwd();
|
|
25
|
+
const io = deps.io || {
|
|
26
|
+
writeOut: (value) => process.stdout.write(value),
|
|
27
|
+
writeErr: (value) => process.stderr.write(value),
|
|
28
|
+
};
|
|
29
|
+
const command = args[0];
|
|
30
|
+
|
|
31
|
+
if (command === 'setup-bundle') {
|
|
32
|
+
const setupBundle = deps.setupBundle || require('./setup').setupBundle;
|
|
33
|
+
await setupBundle(parseCommon(args.slice(1), cwd));
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (command === 'doctor-bundle') {
|
|
38
|
+
const doctorBundle = deps.doctorBundle || require('./doctor').doctorBundle;
|
|
39
|
+
await doctorBundle({
|
|
40
|
+
cwd,
|
|
41
|
+
platform: readOption(args.slice(1), '--platform') || 'all',
|
|
42
|
+
app: readOption(args.slice(1), '--app'),
|
|
43
|
+
apk: readOption(args.slice(1), '--apk'),
|
|
44
|
+
});
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (command === 'embed') {
|
|
49
|
+
io.writeErr('Unknown command: embed\nUse: debug-toolkit setup-bundle\n');
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
io.writeErr(`Unknown command: ${command || '(none)'}\n`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { runBundleCli };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
function readZipEntriesWithUnzip(file) {
|
|
8
|
+
const output = execFileSync('unzip', ['-Z1', file], { encoding: 'utf8' });
|
|
9
|
+
return output.split(/\r?\n/).filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function doctorIos(options) {
|
|
13
|
+
if (!options.app) throw new Error('--app is required for iOS doctor.');
|
|
14
|
+
const bundle = path.join(options.app, 'main.jsbundle');
|
|
15
|
+
if (!fs.existsSync(bundle)) {
|
|
16
|
+
throw new Error(`main.jsbundle not found in ${options.app}`);
|
|
17
|
+
}
|
|
18
|
+
return { ok: true, platform: 'ios', bundle };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function doctorAndroid(options) {
|
|
22
|
+
if (!options.apk) throw new Error('--apk is required for Android doctor.');
|
|
23
|
+
const readZipEntries = options.readZipEntries || readZipEntriesWithUnzip;
|
|
24
|
+
const entries = await readZipEntries(options.apk);
|
|
25
|
+
const bundle = entries.find((entry) => /^assets\/.+\.bundle$/.test(entry));
|
|
26
|
+
if (!bundle) {
|
|
27
|
+
throw new Error(`Android JS bundle not found in ${options.apk}`);
|
|
28
|
+
}
|
|
29
|
+
return { ok: true, platform: 'android', bundle };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function doctorBundle(options) {
|
|
33
|
+
if (options.platform === 'ios') return doctorIos(options);
|
|
34
|
+
if (options.platform === 'android') return doctorAndroid(options);
|
|
35
|
+
throw new Error(`Unsupported platform for doctor-bundle: ${options.platform}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { doctorBundle, readZipEntriesWithUnzip };
|