react-native-debug-toolkit 3.3.2 → 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.
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const APPLY_FROM_MARKER =
7
+ '// react-native-debug-toolkit: debug bundle embed';
8
+ const APPLY_FROM_LINE =
9
+ APPLY_FROM_MARKER +
10
+ '\napply from: "../../node_modules/react-native-debug-toolkit/scripts/android-debug-bundle.gradle"';
11
+
12
+ function findBuildGradle(projectRoot) {
13
+ const gradlePath = path.join(projectRoot, 'android', 'app', 'build.gradle');
14
+ const gradleKtsPath = path.join(
15
+ projectRoot,
16
+ 'android',
17
+ 'app',
18
+ 'build.gradle.kts'
19
+ );
20
+
21
+ if (fs.existsSync(gradleKtsPath)) {
22
+ return 'android/app/build.gradle.kts';
23
+ }
24
+ if (fs.existsSync(gradlePath)) {
25
+ return 'android/app/build.gradle';
26
+ }
27
+ return null;
28
+ }
29
+
30
+ function ensureAssetsDir(projectRoot) {
31
+ const assetsDir = path.join(
32
+ projectRoot,
33
+ 'android',
34
+ 'app',
35
+ 'src',
36
+ 'main',
37
+ 'assets'
38
+ );
39
+ if (!fs.existsSync(assetsDir)) {
40
+ fs.mkdirSync(assetsDir, { recursive: true });
41
+ console.log(' Created android/app/src/main/assets/');
42
+ }
43
+ }
44
+
45
+ function injectGradleApply(projectRoot, buildGradleRelPath, entryFile, options) {
46
+ const fullPath = path.join(projectRoot, buildGradleRelPath);
47
+ let content = fs.readFileSync(fullPath, 'utf-8');
48
+
49
+ // Idempotent check
50
+ if (content.includes(APPLY_FROM_MARKER)) {
51
+ console.log(
52
+ ` ${buildGradleRelPath}: apply-from already present. Skipping.`
53
+ );
54
+ return { buildGradle: buildGradleRelPath, entryFile };
55
+ }
56
+
57
+ // Set entry file as ext property if non-default
58
+ let entryFileExt = '';
59
+ if (entryFile && entryFile !== 'index.js') {
60
+ entryFileExt = `\nproject.ext.debugToolkitEntryFile = '${entryFile}'`;
61
+ }
62
+
63
+ content += '\n\n' + APPLY_FROM_LINE + entryFileExt + '\n';
64
+ fs.writeFileSync(fullPath, content);
65
+
66
+ ensureAssetsDir(projectRoot);
67
+ console.log(` ${buildGradleRelPath}: Injected apply-from for debug bundle.`);
68
+
69
+ return { buildGradle: buildGradleRelPath, entryFile };
70
+ }
71
+
72
+ function undoGradleApply(projectRoot, config) {
73
+ if (!config.android) {
74
+ console.log(' No Android configuration found. Skipping.');
75
+ return;
76
+ }
77
+
78
+ const fullPath = path.join(projectRoot, config.android.buildGradle);
79
+ if (!fs.existsSync(fullPath)) {
80
+ console.log(` ${config.android.buildGradle} not found. Skipping.`);
81
+ return;
82
+ }
83
+
84
+ let content = fs.readFileSync(fullPath, 'utf-8');
85
+ if (!content.includes(APPLY_FROM_MARKER)) {
86
+ console.log(' apply-from not found. Skipping.');
87
+ return;
88
+ }
89
+
90
+ // Remove the marker line, apply-from line, and optional entryFile ext
91
+ const lines = content.split('\n');
92
+ const filtered = lines.filter((line) => {
93
+ if (line === APPLY_FROM_MARKER) return false;
94
+ if (
95
+ line.startsWith('apply from:') &&
96
+ line.includes('react-native-debug-toolkit/scripts/android-debug-bundle.gradle')
97
+ )
98
+ return false;
99
+ if (line.startsWith("project.ext.debugToolkitEntryFile = '")) return false;
100
+ return true;
101
+ });
102
+
103
+ // Trim trailing empty lines introduced by our injection
104
+ while (filtered.length > 0 && filtered[filtered.length - 1].trim() === '') {
105
+ filtered.pop();
106
+ }
107
+
108
+ fs.writeFileSync(fullPath, filtered.join('\n') + '\n');
109
+ console.log(' Removed apply-from from build.gradle.');
110
+ }
111
+
112
+ module.exports = {
113
+ findBuildGradle,
114
+ injectGradleApply,
115
+ undoGradleApply,
116
+ };
@@ -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 };
@@ -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 };