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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function isExpoProject(projectRoot) {
|
|
7
|
+
// app.json with expo field
|
|
8
|
+
const appJsonPath = path.join(projectRoot, 'app.json');
|
|
9
|
+
if (fs.existsSync(appJsonPath)) {
|
|
10
|
+
try {
|
|
11
|
+
const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf-8'));
|
|
12
|
+
if (appJson.expo) return true;
|
|
13
|
+
} catch (_) {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// app.config.js or app.config.ts
|
|
17
|
+
if (
|
|
18
|
+
fs.existsSync(path.join(projectRoot, 'app.config.js')) ||
|
|
19
|
+
fs.existsSync(path.join(projectRoot, 'app.config.ts'))
|
|
20
|
+
) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// expo in package.json dependencies
|
|
25
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
26
|
+
if (fs.existsSync(pkgPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
29
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
30
|
+
if (deps.expo) return true;
|
|
31
|
+
} catch (_) {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isExpoGo(projectRoot) {
|
|
38
|
+
// Expo Go doesn't support embedded bundles
|
|
39
|
+
// If no ios/ or android/ dirs, likely Expo Go
|
|
40
|
+
const hasNativeDirs =
|
|
41
|
+
fs.existsSync(path.join(projectRoot, 'ios')) ||
|
|
42
|
+
fs.existsSync(path.join(projectRoot, 'android'));
|
|
43
|
+
|
|
44
|
+
if (!hasNativeDirs) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isPrebuilt(projectRoot) {
|
|
51
|
+
// Dev client / bare workflow after prebuild
|
|
52
|
+
return (
|
|
53
|
+
fs.existsSync(path.join(projectRoot, 'ios')) &&
|
|
54
|
+
fs.existsSync(path.join(projectRoot, 'android'))
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getExpoEntryPoint(projectRoot) {
|
|
59
|
+
const appJsonPath = path.join(projectRoot, 'app.json');
|
|
60
|
+
if (fs.existsSync(appJsonPath)) {
|
|
61
|
+
try {
|
|
62
|
+
const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf-8'));
|
|
63
|
+
if (appJson.expo && appJson.expo.entryPoint) {
|
|
64
|
+
return appJson.expo.entryPoint;
|
|
65
|
+
}
|
|
66
|
+
} catch (_) {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Expo default with .expo virtual entry
|
|
70
|
+
const virtualEntry = path.join(
|
|
71
|
+
projectRoot,
|
|
72
|
+
'.expo',
|
|
73
|
+
'.virtual-metro-entry'
|
|
74
|
+
);
|
|
75
|
+
if (fs.existsSync(virtualEntry)) {
|
|
76
|
+
return '.expo/.virtual-metro-entry';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function checkExpo(projectRoot, options) {
|
|
83
|
+
const expo = isExpoProject(projectRoot);
|
|
84
|
+
if (!expo) {
|
|
85
|
+
return { expo: false, skip: false };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isExpoGo(projectRoot)) {
|
|
89
|
+
return {
|
|
90
|
+
expo: true,
|
|
91
|
+
skip: true,
|
|
92
|
+
reason:
|
|
93
|
+
'Expo Go does not support embedded bundles. Use expo-dev-client or run npx expo prebuild first.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!isPrebuilt(projectRoot)) {
|
|
98
|
+
return {
|
|
99
|
+
expo: true,
|
|
100
|
+
skip: true,
|
|
101
|
+
reason:
|
|
102
|
+
'Native directories not found. Run npx expo prebuild first, then retry.',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { expo: true, skip: false, entryPoint: getExpoEntryPoint(projectRoot) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { isExpoProject, checkExpo, getExpoEntryPoint };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const pbxproj = require('pbxproj');
|
|
6
|
+
|
|
7
|
+
const BUNDLE_PHASE_NAME = 'Bundle React Native code and images';
|
|
8
|
+
const FORCE_BUNDLING_LINE = 'export FORCE_BUNDLING=1';
|
|
9
|
+
const CONFIG_FILE = '.debug-toolkit-embed.json';
|
|
10
|
+
|
|
11
|
+
function findXcodeproj(projectRoot) {
|
|
12
|
+
const iosDir = path.join(projectRoot, 'ios');
|
|
13
|
+
if (!fs.existsSync(iosDir)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const entries = fs.readdirSync(iosDir).filter(
|
|
17
|
+
(e) => e.endsWith('.xcodeproj') && e !== 'Pods.xcodeproj'
|
|
18
|
+
);
|
|
19
|
+
if (entries.length === 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return entries;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function findPbxproj(xcodeprojPath) {
|
|
26
|
+
const pbxPath = path.join(xcodeprojPath, 'project.pbxproj');
|
|
27
|
+
if (!fs.existsSync(pbxPath)) {
|
|
28
|
+
throw new Error(`project.pbxproj not found at ${pbxPath}`);
|
|
29
|
+
}
|
|
30
|
+
return pbxPath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getBundleScriptPhase(pbxFile) {
|
|
34
|
+
const proj = new pbxproj(pbxFile).parseSync();
|
|
35
|
+
const sections = proj.hash.project.objects.PBXShellScriptBuildPhase;
|
|
36
|
+
if (!sections) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
for (const [key, section] of Object.entries(sections)) {
|
|
40
|
+
if (
|
|
41
|
+
section.name === BUNDLE_PHASE_NAME ||
|
|
42
|
+
section.name === `"${BUNDLE_PHASE_NAME}"`
|
|
43
|
+
) {
|
|
44
|
+
return { id: key, section, proj, pbxFile };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function injectForceBundling(projectRoot, xcodeprojRelPath, entryFile, options) {
|
|
51
|
+
const xcodeprojPath = path.join(projectRoot, xcodeprojRelPath);
|
|
52
|
+
const pbxFile = findPbxproj(xcodeprojPath);
|
|
53
|
+
const result = getBundleScriptPhase(pbxFile);
|
|
54
|
+
if (!result) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`"${BUNDLE_PHASE_NAME}" script phase not found in ${xcodeprojRelPath}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const scriptBody = result.section.shellScript;
|
|
61
|
+
// Already injected — idempotent
|
|
62
|
+
if (scriptBody && scriptBody.includes(FORCE_BUNDLING_LINE)) {
|
|
63
|
+
console.log(` ${xcodeprojRelPath}: FORCE_BUNDLING already set. Skipping.`);
|
|
64
|
+
return { xcodeproj: xcodeprojRelPath, scriptPhaseId: result.id, entryFile };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Decode shellScript (it's usually a quoted string)
|
|
68
|
+
let decoded = scriptBody;
|
|
69
|
+
try {
|
|
70
|
+
decoded = JSON.parse(scriptBody);
|
|
71
|
+
} catch (_) {
|
|
72
|
+
// Not JSON-encoded, use as-is
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const newScript = FORCE_BUNDLING_LINE + '\n' + decoded;
|
|
76
|
+
result.section.shellScript = JSON.stringify(newScript);
|
|
77
|
+
|
|
78
|
+
fs.writeFileSync(pbxFile, result.proj.writeSync());
|
|
79
|
+
console.log(` ${xcodeprojRelPath}: Injected FORCE_BUNDLING=1`);
|
|
80
|
+
|
|
81
|
+
return { xcodeproj: xcodeprojRelPath, scriptPhaseId: result.id, entryFile };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function undoForceBundling(projectRoot, config) {
|
|
85
|
+
if (!config.ios) {
|
|
86
|
+
console.log(' No iOS configuration found. Skipping.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const xcodeprojPath = path.join(projectRoot, config.ios.xcodeproj);
|
|
91
|
+
const pbxFile = findPbxproj(xcodeprojPath);
|
|
92
|
+
const result = getBundleScriptPhase(pbxFile);
|
|
93
|
+
if (!result) {
|
|
94
|
+
console.log(' Bundle script phase not found. Skipping.');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const scriptBody = result.section.shellScript;
|
|
99
|
+
let decoded = scriptBody;
|
|
100
|
+
try {
|
|
101
|
+
decoded = JSON.parse(scriptBody);
|
|
102
|
+
} catch (_) {}
|
|
103
|
+
|
|
104
|
+
if (!decoded.includes(FORCE_BUNDLING_LINE)) {
|
|
105
|
+
console.log(' FORCE_BUNDLING not found in script. Skipping.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const newScript = decoded
|
|
110
|
+
.split('\n')
|
|
111
|
+
.filter((line) => line !== FORCE_BUNDLING_LINE)
|
|
112
|
+
.join('\n');
|
|
113
|
+
result.section.shellScript = JSON.stringify(newScript);
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(pbxFile, result.proj.writeSync());
|
|
116
|
+
console.log(' Removed FORCE_BUNDLING=1 from script phase.');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { findXcodeproj, injectForceBundling, undoForceBundling };
|
package/scripts/embed.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
|
|
7
|
+
const ios = require('./embed-ios');
|
|
8
|
+
const android = require('./embed-android');
|
|
9
|
+
const expo = require('./embed-expo');
|
|
10
|
+
|
|
11
|
+
const CONFIG_FILE = '.debug-toolkit-embed.json';
|
|
12
|
+
|
|
13
|
+
function readConfig(projectRoot) {
|
|
14
|
+
const configPath = path.join(projectRoot, CONFIG_FILE);
|
|
15
|
+
if (!fs.existsSync(configPath)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeConfig(projectRoot, config) {
|
|
22
|
+
const configPath = path.join(projectRoot, CONFIG_FILE);
|
|
23
|
+
const payload = { version: 1, ...config };
|
|
24
|
+
fs.writeFileSync(configPath, JSON.stringify(payload, null, 2) + '\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function deleteConfig(projectRoot) {
|
|
28
|
+
const configPath = path.join(projectRoot, CONFIG_FILE);
|
|
29
|
+
if (fs.existsSync(configPath)) {
|
|
30
|
+
fs.unlinkSync(configPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function detectEntryFile(projectRoot) {
|
|
35
|
+
const candidates = [
|
|
36
|
+
'index.js',
|
|
37
|
+
'index.ts',
|
|
38
|
+
'index.tsx',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const candidate of candidates) {
|
|
42
|
+
if (fs.existsSync(path.join(projectRoot, candidate))) {
|
|
43
|
+
return candidate;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Expo virtual entry
|
|
48
|
+
if (fs.existsSync(path.join(projectRoot, '.expo', '.virtual-metro-entry'))) {
|
|
49
|
+
return '.expo/.virtual-metro-entry';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return 'index.js';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function prompt(question, options) {
|
|
56
|
+
if (options && options.yes) {
|
|
57
|
+
return Promise.resolve(true);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const rl = readline.createInterface({
|
|
61
|
+
input: process.stdin,
|
|
62
|
+
output: process.stdout,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
rl.question(question + ' [Y/n] ', (answer) => {
|
|
67
|
+
rl.close();
|
|
68
|
+
resolve(answer.toLowerCase() !== 'n');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function embed(projectRoot, argv) {
|
|
74
|
+
const args = argv || [];
|
|
75
|
+
const yes = args.includes('--yes');
|
|
76
|
+
const platformIdx = args.indexOf('--platform');
|
|
77
|
+
const platform =
|
|
78
|
+
platformIdx >= 0 ? args[platformIdx + 1] : null; // 'ios' | 'android'
|
|
79
|
+
|
|
80
|
+
console.log('debug-toolkit embed');
|
|
81
|
+
console.log('---');
|
|
82
|
+
|
|
83
|
+
// Check Expo
|
|
84
|
+
const expoResult = expo.checkExpo(projectRoot, { yes });
|
|
85
|
+
if (expoResult.expo) {
|
|
86
|
+
console.log('Detected Expo project.');
|
|
87
|
+
if (expoResult.skip) {
|
|
88
|
+
console.log(` Skipping: ${expoResult.reason}`);
|
|
89
|
+
if (platform === null || platform === 'ios') {
|
|
90
|
+
// nothing
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
console.log(' Native directories found — proceeding.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const existingConfig = readConfig(projectRoot);
|
|
98
|
+
const config = {};
|
|
99
|
+
|
|
100
|
+
// Entry file
|
|
101
|
+
const entryFile =
|
|
102
|
+
(expoResult.entryPoint) || detectEntryFile(projectRoot);
|
|
103
|
+
console.log(`Entry file: ${entryFile}`);
|
|
104
|
+
|
|
105
|
+
// iOS
|
|
106
|
+
if (platform === null || platform === 'ios') {
|
|
107
|
+
const xcodeprojEntries = ios.findXcodeproj(projectRoot);
|
|
108
|
+
if (xcodeprojEntries && xcodeprojEntries.length > 0) {
|
|
109
|
+
let chosen = xcodeprojEntries[0];
|
|
110
|
+
if (xcodeprojEntries.length > 1) {
|
|
111
|
+
if (yes) {
|
|
112
|
+
console.log(
|
|
113
|
+
` Multiple Xcode projects found. Using: ${chosen}`
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
console.log(' Multiple Xcode projects found:');
|
|
117
|
+
xcodeprojEntries.forEach((e, i) =>
|
|
118
|
+
console.log(` ${i + 1}. ${e}`)
|
|
119
|
+
);
|
|
120
|
+
const idx = await prompt(
|
|
121
|
+
` Select project [1-${xcodeprojEntries.length}]`,
|
|
122
|
+
{ yes }
|
|
123
|
+
);
|
|
124
|
+
// Default to first
|
|
125
|
+
chosen = xcodeprojEntries[0];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const xcodeprojRelPath = path.join('ios', chosen);
|
|
130
|
+
const shouldEmbed =
|
|
131
|
+
yes ||
|
|
132
|
+
(await prompt(
|
|
133
|
+
` Inject FORCE_BUNDLING into ${xcodeprojRelPath}?`,
|
|
134
|
+
{ yes }
|
|
135
|
+
));
|
|
136
|
+
|
|
137
|
+
if (shouldEmbed !== false) {
|
|
138
|
+
config.ios = ios.injectForceBundling(
|
|
139
|
+
projectRoot,
|
|
140
|
+
xcodeprojRelPath,
|
|
141
|
+
entryFile,
|
|
142
|
+
{ yes }
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
console.log(' No Xcode project found. Skipping iOS.');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Android
|
|
151
|
+
if (platform === null || platform === 'android') {
|
|
152
|
+
const buildGradle = android.findBuildGradle(projectRoot);
|
|
153
|
+
if (buildGradle) {
|
|
154
|
+
const shouldEmbed =
|
|
155
|
+
yes ||
|
|
156
|
+
(await prompt(
|
|
157
|
+
` Inject debug bundle task into ${buildGradle}?`,
|
|
158
|
+
{ yes }
|
|
159
|
+
));
|
|
160
|
+
|
|
161
|
+
if (shouldEmbed !== false) {
|
|
162
|
+
config.android = android.injectGradleApply(
|
|
163
|
+
projectRoot,
|
|
164
|
+
buildGradle,
|
|
165
|
+
entryFile,
|
|
166
|
+
{ yes }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
console.log(' No Android build.gradle found. Skipping Android.');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Write config
|
|
175
|
+
if (Object.keys(config).length > 0) {
|
|
176
|
+
writeConfig(projectRoot, config);
|
|
177
|
+
console.log(`\nConfiguration saved to ${CONFIG_FILE}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log('\nDone. Debug builds will now embed a JS bundle.');
|
|
181
|
+
console.log('Use DevConnect to switch Metro hosts at runtime.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function undo(projectRoot, argv) {
|
|
185
|
+
const args = argv || [];
|
|
186
|
+
const yes = args.includes('--yes');
|
|
187
|
+
|
|
188
|
+
console.log('debug-toolkit embed --undo');
|
|
189
|
+
console.log('---');
|
|
190
|
+
|
|
191
|
+
const config = readConfig(projectRoot);
|
|
192
|
+
if (!config) {
|
|
193
|
+
console.log(`No ${CONFIG_FILE} found. Nothing to undo.`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const shouldUndo =
|
|
198
|
+
yes || (await prompt('Remove all debug-toolkit embed injections?', { yes }));
|
|
199
|
+
|
|
200
|
+
if (shouldUndo === false) {
|
|
201
|
+
console.log('Cancelled.');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
ios.undoForceBundling(projectRoot, config);
|
|
206
|
+
android.undoGradleApply(projectRoot, config);
|
|
207
|
+
|
|
208
|
+
deleteConfig(projectRoot);
|
|
209
|
+
console.log(`\nRemoved ${CONFIG_FILE}.`);
|
|
210
|
+
console.log('Done.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function main(argv) {
|
|
214
|
+
const projectRoot = process.cwd();
|
|
215
|
+
const args = argv || [];
|
|
216
|
+
|
|
217
|
+
if (args.includes('--undo')) {
|
|
218
|
+
return undo(projectRoot, args);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return embed(projectRoot, args);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = { main, embed, undo };
|
|
@@ -35,7 +35,12 @@ import {
|
|
|
35
35
|
saveDaemonPort,
|
|
36
36
|
saveMetroPort,
|
|
37
37
|
} from './devConnectPreferences';
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
applyMetroBundle,
|
|
40
|
+
getNativeDiagnostics,
|
|
41
|
+
resetMetroBundle,
|
|
42
|
+
type NativeDiagnostics,
|
|
43
|
+
} from './nativeDevConnect';
|
|
39
44
|
import type { DevConnectFeatureControls, DevConnectSettingsPatch, DevConnectState } from './types';
|
|
40
45
|
|
|
41
46
|
const CONNECTION_TIMEOUT_MS = 2000;
|
|
@@ -72,6 +77,8 @@ export function DevConnectTab({ snapshot, feature }: DebugFeatureRenderProps<Dev
|
|
|
72
77
|
const [message, setMessage] = useState<string | null>(null);
|
|
73
78
|
const [sending, setSending] = useState(false);
|
|
74
79
|
const [metroBusy, setMetroBusy] = useState(false);
|
|
80
|
+
const [diagData, setDiagData] = useState<NativeDiagnostics | null>(null);
|
|
81
|
+
const [diagOpen, setDiagOpen] = useState(false);
|
|
75
82
|
|
|
76
83
|
const isSim = snapshot.isSimulator;
|
|
77
84
|
|
|
@@ -80,7 +87,20 @@ export function DevConnectTab({ snapshot, feature }: DebugFeatureRenderProps<Dev
|
|
|
80
87
|
}, [feature]);
|
|
81
88
|
|
|
82
89
|
useEffect(() => {
|
|
83
|
-
|
|
90
|
+
getNativeDiagnostics().then((result) => {
|
|
91
|
+
if (result) {
|
|
92
|
+
setDiagData(result);
|
|
93
|
+
console.info(
|
|
94
|
+
`[DevConnect] debugBuild=${result.isDebugBuild} appDelegate=${result.appDelegateClass} persistedHost=${result.persistedMetroHost ?? 'none'}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}).catch(() => {});
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
const refreshDiag = useCallback(() => {
|
|
101
|
+
getNativeDiagnostics().then((result) => {
|
|
102
|
+
if (result) setDiagData(result);
|
|
103
|
+
}).catch(() => {});
|
|
84
104
|
}, []);
|
|
85
105
|
|
|
86
106
|
useEffect(() => {
|
|
@@ -376,8 +396,11 @@ export function DevConnectTab({ snapshot, feature }: DebugFeatureRenderProps<Dev
|
|
|
376
396
|
}
|
|
377
397
|
}, [snapshot.nativeMetroAvailable]);
|
|
378
398
|
|
|
399
|
+
// Metro host switching only works in Debug builds. diagData is iOS-populated; when we know
|
|
400
|
+
// it's a Release build, disable the controls (Android reports null → stays enabled).
|
|
401
|
+
const metroReleaseBlocked = diagData ? !diagData.isDebugBuild : false;
|
|
379
402
|
const canConnect = isSim || (Boolean(normalizeComputerHost(computerHost)) && Boolean(normalizePort(daemonPort)));
|
|
380
|
-
const canUseMetro = Boolean(metroTarget) && snapshot.nativeMetroAvailable && !metroBusy;
|
|
403
|
+
const canUseMetro = Boolean(metroTarget) && snapshot.nativeMetroAvailable && !metroBusy && !metroReleaseBlocked;
|
|
381
404
|
const busy = sending || syncState === 'checking';
|
|
382
405
|
const subnetPrefix = snapshot.subnetPrefix;
|
|
383
406
|
const ipPlaceholder = subnetPrefix ? `${subnetPrefix}...` : '192.168.1.10';
|
|
@@ -484,9 +507,20 @@ export function DevConnectTab({ snapshot, feature }: DebugFeatureRenderProps<Dev
|
|
|
484
507
|
{message ? <Text style={styles.message}>{message}</Text> : null}
|
|
485
508
|
|
|
486
509
|
<View style={styles.section}>
|
|
487
|
-
<
|
|
510
|
+
<View style={styles.sectionTitleRow}>
|
|
511
|
+
<Text style={styles.sectionTitle}>Remote JS Bundle</Text>
|
|
512
|
+
{diagData ? (
|
|
513
|
+
<View style={styles.swizzleBadge}>
|
|
514
|
+
<View style={[styles.swizzleDot, diagData.isDebugBuild ? styles.dotGreen : styles.dotRed]} />
|
|
515
|
+
<Text style={styles.swizzleBadgeText}>
|
|
516
|
+
{diagData.isDebugBuild ? 'debug build' : 'release: disabled'}
|
|
517
|
+
</Text>
|
|
518
|
+
</View>
|
|
519
|
+
) : null}
|
|
520
|
+
</View>
|
|
488
521
|
<Text style={styles.sectionDesc}>
|
|
489
|
-
|
|
522
|
+
Starts from the embedded bundle. After you apply a computer IP, hot-reloads from that
|
|
523
|
+
Metro. Debug builds only — use Reset to go back to the embedded bundle.
|
|
490
524
|
</Text>
|
|
491
525
|
|
|
492
526
|
{!metroUrls ? (
|
|
@@ -530,7 +564,70 @@ export function DevConnectTab({ snapshot, feature }: DebugFeatureRenderProps<Dev
|
|
|
530
564
|
{!snapshot.nativeMetroAvailable ? (
|
|
531
565
|
<Text style={styles.hint}>Native DevConnect requires pod install / Gradle sync and app rebuild.</Text>
|
|
532
566
|
) : null}
|
|
567
|
+
|
|
568
|
+
{metroReleaseBlocked ? (
|
|
569
|
+
<Text style={styles.diagWarning}>
|
|
570
|
+
⚠ This is a Release build. Metro host switching is disabled — RN loads the embedded
|
|
571
|
+
bundle and strips the packager machinery in Release. Run a Debug build to switch hosts.
|
|
572
|
+
</Text>
|
|
573
|
+
) : null}
|
|
533
574
|
</View>
|
|
575
|
+
|
|
576
|
+
{snapshot.nativeMetroAvailable && diagData ? (
|
|
577
|
+
<View style={styles.section}>
|
|
578
|
+
<TouchableOpacity
|
|
579
|
+
style={styles.diagHeader}
|
|
580
|
+
onPress={() => { setDiagOpen((v) => !v); refreshDiag(); }}
|
|
581
|
+
activeOpacity={0.7}
|
|
582
|
+
>
|
|
583
|
+
<Text style={styles.sectionTitle}>iOS Bundle Status</Text>
|
|
584
|
+
<Text style={styles.diagChevron}>{diagOpen ? '▲' : '▼'}</Text>
|
|
585
|
+
</TouchableOpacity>
|
|
586
|
+
|
|
587
|
+
{diagOpen ? (
|
|
588
|
+
<View style={styles.diagCard}>
|
|
589
|
+
<View style={styles.diagRow}>
|
|
590
|
+
<Text style={styles.diagKey}>AppDelegate</Text>
|
|
591
|
+
<Text style={styles.diagVal}>{diagData.appDelegateClass}</Text>
|
|
592
|
+
</View>
|
|
593
|
+
<View style={styles.diagRow}>
|
|
594
|
+
<Text style={styles.diagKey}>packagerHost</Text>
|
|
595
|
+
<Text style={styles.diagVal}>{diagData.persistedMetroHost ?? '—'}</Text>
|
|
596
|
+
</View>
|
|
597
|
+
<View style={styles.diagRow}>
|
|
598
|
+
<Text style={styles.diagKey}>embedded</Text>
|
|
599
|
+
<Text style={[styles.diagVal, diagData.hasEmbeddedBundle ? styles.diagGood : styles.diagWarn]}>
|
|
600
|
+
{diagData.hasEmbeddedBundle ? 'main.jsbundle' : 'missing'}
|
|
601
|
+
</Text>
|
|
602
|
+
</View>
|
|
603
|
+
<View style={styles.diagRow}>
|
|
604
|
+
<Text style={styles.diagKey}>build</Text>
|
|
605
|
+
<Text style={[styles.diagVal, diagData.isDebugBuild ? styles.diagGood : styles.diagWarn]}>
|
|
606
|
+
{diagData.isDebugBuild ? 'Debug' : 'Release'}
|
|
607
|
+
</Text>
|
|
608
|
+
</View>
|
|
609
|
+
<View style={styles.diagRow}>
|
|
610
|
+
<Text style={styles.diagKey}>embedded-first</Text>
|
|
611
|
+
<Text style={[styles.diagVal, diagData.embeddedFirstHookInstalled ? styles.diagGood : styles.diagWarn]}>
|
|
612
|
+
{diagData.embeddedFirstHookInstalled ? 'active' : 'inactive'}
|
|
613
|
+
</Text>
|
|
614
|
+
</View>
|
|
615
|
+
{!diagData.embeddedFirstHookInstalled ? (
|
|
616
|
+
<Text style={styles.diagWarning}>
|
|
617
|
+
⚠ Embedded-first hook not active (bundleRoot=
|
|
618
|
+
{diagData.bundleRootHookInstalled ? 'Y' : 'N'}). Rebuild after pod install.
|
|
619
|
+
Without it, Debug may still try Metro on launch.
|
|
620
|
+
</Text>
|
|
621
|
+
) : diagData.hasEmbeddedBundle === false ? (
|
|
622
|
+
<Text style={styles.diagWarning}>
|
|
623
|
+
⚠ main.jsbundle is missing from the app package. Build with an embedded bundle
|
|
624
|
+
(e.g. export/bundle) or cold start cannot use offline JS.
|
|
625
|
+
</Text>
|
|
626
|
+
) : null}
|
|
627
|
+
</View>
|
|
628
|
+
) : null}
|
|
629
|
+
</View>
|
|
630
|
+
) : null}
|
|
534
631
|
</ScrollView>
|
|
535
632
|
</KeyboardAvoidingView>
|
|
536
633
|
);
|
|
@@ -635,4 +732,30 @@ const styles = StyleSheet.create({
|
|
|
635
732
|
paddingVertical: 7,
|
|
636
733
|
},
|
|
637
734
|
urlText: { flex: 1, fontSize: 13, fontFamily: 'Courier', color: Colors.text },
|
|
735
|
+
sectionTitleRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 },
|
|
736
|
+
swizzleBadge: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: 7, paddingVertical: 3, borderRadius: 10, backgroundColor: Colors.surface, borderWidth: 1, borderColor: Colors.border },
|
|
737
|
+
swizzleDot: { width: 6, height: 6, borderRadius: 3 },
|
|
738
|
+
dotGreen: { backgroundColor: '#34C759' },
|
|
739
|
+
dotRed: { backgroundColor: '#FF3B30' },
|
|
740
|
+
swizzleBadgeText: { fontSize: 11, color: Colors.textSecondary, fontFamily: 'Courier' },
|
|
741
|
+
diagHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 6 },
|
|
742
|
+
diagChevron: { fontSize: 12, color: Colors.textSecondary },
|
|
743
|
+
diagCard: {
|
|
744
|
+
backgroundColor: Colors.surface,
|
|
745
|
+
borderWidth: 1,
|
|
746
|
+
borderColor: Colors.border,
|
|
747
|
+
borderRadius: 10,
|
|
748
|
+
padding: 10,
|
|
749
|
+
},
|
|
750
|
+
diagRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 4 },
|
|
751
|
+
diagKey: { fontSize: 12, color: Colors.textSecondary, fontFamily: 'Courier' },
|
|
752
|
+
diagVal: { fontSize: 12, color: Colors.text, fontFamily: 'Courier', fontWeight: '600' },
|
|
753
|
+
diagGood: { color: '#34C759' },
|
|
754
|
+
diagWarn: { color: '#FF9500' },
|
|
755
|
+
diagWarning: {
|
|
756
|
+
marginTop: 10,
|
|
757
|
+
fontSize: 11,
|
|
758
|
+
color: '#FF9500',
|
|
759
|
+
lineHeight: 16,
|
|
760
|
+
},
|
|
638
761
|
});
|
|
@@ -2,6 +2,18 @@ import { NativeModules } from 'react-native';
|
|
|
2
2
|
|
|
3
3
|
import { buildMetroTarget } from './devConnectUtils';
|
|
4
4
|
|
|
5
|
+
export interface NativeDiagnostics {
|
|
6
|
+
persistedMetroHost: string | null;
|
|
7
|
+
appDelegateClass: string;
|
|
8
|
+
// Metro host switching only works in Debug builds (RN strips the packager machinery in
|
|
9
|
+
// Release). False means the Remote JS Bundle controls should be disabled.
|
|
10
|
+
isDebugBuild: boolean;
|
|
11
|
+
hasEmbeddedBundle?: boolean;
|
|
12
|
+
embeddedFirstHookInstalled?: boolean;
|
|
13
|
+
packagerHookInstalled?: boolean;
|
|
14
|
+
bundleRootHookInstalled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
interface DebugToolkitDevConnectNativeModule {
|
|
6
18
|
applyMetroHost: (hostPort: string) => Promise<{ hostPort?: string } | void>;
|
|
7
19
|
resetMetroHost: () => Promise<void>;
|
|
@@ -9,6 +21,7 @@ interface DebugToolkitDevConnectNativeModule {
|
|
|
9
21
|
getLocalIp?: () => Promise<string | null>;
|
|
10
22
|
isDebugBuild?: () => Promise<boolean>;
|
|
11
23
|
getPreference?: (key: string) => Promise<string | null>;
|
|
24
|
+
getDiagnostics?: () => Promise<NativeDiagnostics>;
|
|
12
25
|
}
|
|
13
26
|
|
|
14
27
|
type MetroBundleFailureReason =
|
|
@@ -154,17 +167,14 @@ export async function nativeIsDebugBuild(): Promise<boolean | null> {
|
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
|
|
157
|
-
export async function
|
|
170
|
+
export async function getNativeDiagnostics(): Promise<NativeDiagnostics | null> {
|
|
158
171
|
const nativeModule = getNativeModule();
|
|
159
|
-
if (!nativeModule?.
|
|
160
|
-
return;
|
|
172
|
+
if (!nativeModule?.getDiagnostics) {
|
|
173
|
+
return null;
|
|
161
174
|
}
|
|
162
175
|
try {
|
|
163
|
-
|
|
164
|
-
if (raw && typeof raw === 'string') {
|
|
165
|
-
console.info(`[${label}] Last native diagnostic:`, raw);
|
|
166
|
-
}
|
|
176
|
+
return await nativeModule.getDiagnostics();
|
|
167
177
|
} catch {
|
|
168
|
-
|
|
178
|
+
return null;
|
|
169
179
|
}
|
|
170
180
|
}
|