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.
@@ -0,0 +1,179 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const xcode = require('xcode');
6
+
7
+ const PHASE_NAME = 'Bundle React Native code and images';
8
+ const BEGIN = '# react-native-debug-toolkit: begin debug bundle';
9
+ const END = '# react-native-debug-toolkit: end debug bundle';
10
+ const BLOCK = `${BEGIN}\nexport FORCE_BUNDLING=1\n${END}`;
11
+
12
+ function stripQuotes(value) {
13
+ return String(value || '').replace(/^"|"$/g, '');
14
+ }
15
+
16
+ function findProjects(cwd) {
17
+ const iosDir = path.join(cwd, 'ios');
18
+ if (!fs.existsSync(iosDir)) {
19
+ return [];
20
+ }
21
+
22
+ return fs.readdirSync(iosDir)
23
+ .filter((entry) => entry.endsWith('.xcodeproj') && entry !== 'Pods.xcodeproj')
24
+ .map((entry) => path.join(iosDir, entry, 'project.pbxproj'))
25
+ .filter((file) => fs.existsSync(file));
26
+ }
27
+
28
+ function decodeScript(value) {
29
+ if (!value) {
30
+ return '';
31
+ }
32
+
33
+ try {
34
+ return JSON.parse(value);
35
+ } catch {
36
+ return stripQuotes(value);
37
+ }
38
+ }
39
+
40
+ function encodeScript(value) {
41
+ return JSON.stringify(value);
42
+ }
43
+
44
+ function phaseName(phase) {
45
+ return stripQuotes(phase.name || phase.comment);
46
+ }
47
+
48
+ function isAppTarget(target) {
49
+ return stripQuotes(target.productType) === 'com.apple.product-type.application';
50
+ }
51
+
52
+ function targetName(target) {
53
+ return stripQuotes(target.name || target.productName || target.comment);
54
+ }
55
+
56
+ function targetBuildPhaseIds(target) {
57
+ return (target.buildPhases || []).map((phaseRef) => {
58
+ if (typeof phaseRef === 'string') {
59
+ return phaseRef;
60
+ }
61
+ return phaseRef.value;
62
+ });
63
+ }
64
+
65
+ function sectionEntries(objects, sectionName, isaName) {
66
+ const section = objects[sectionName];
67
+ if (section) {
68
+ return Object.entries(section)
69
+ .filter(([key]) => !key.endsWith('_comment'));
70
+ }
71
+
72
+ return Object.entries(objects)
73
+ .filter(([key, value]) => !key.endsWith('_comment') && value && value.isa === isaName)
74
+ .map(([key, value]) => [key, { ...value, comment: objects[`${key}_comment`] }]);
75
+ }
76
+
77
+ function loadTarget(cwd, iosTarget) {
78
+ const projects = findProjects(cwd);
79
+ if (projects.length !== 1) {
80
+ throw new Error(`Expected one Xcode project, found ${projects.length}. Pass --ios-target after selecting the app project.`);
81
+ }
82
+
83
+ const pbxFile = projects[0];
84
+ const proj = xcode.project(pbxFile).parseSync();
85
+ const objects = proj.hash.project.objects;
86
+ const nativeTargets = sectionEntries(objects, 'PBXNativeTarget', 'PBXNativeTarget');
87
+ const shellPhases = new Map(sectionEntries(objects, 'PBXShellScriptBuildPhase', 'PBXShellScriptBuildPhase'));
88
+
89
+ const appTargets = nativeTargets
90
+ .filter(([, target]) => isAppTarget(target));
91
+
92
+ const matches = iosTarget
93
+ ? appTargets.filter(([, target]) => targetName(target) === iosTarget)
94
+ : appTargets;
95
+
96
+ if (matches.length !== 1) {
97
+ throw new Error(`Expected one iOS app target, found ${matches.length}. Pass --ios-target <name>.`);
98
+ }
99
+
100
+ const [, target] = matches[0];
101
+ for (const phaseId of targetBuildPhaseIds(target)) {
102
+ const phase = shellPhases.get(phaseId);
103
+ if (!phase || phaseName(phase) !== PHASE_NAME) {
104
+ continue;
105
+ }
106
+
107
+ const script = decodeScript(phase.shellScript);
108
+ if (!script.includes('react-native-xcode.sh') && !script.includes('with-environment.sh')) {
109
+ throw new Error(`${PHASE_NAME} does not call React Native bundling script.`);
110
+ }
111
+
112
+ return { pbxFile, proj, phase };
113
+ }
114
+
115
+ throw new Error(`${PHASE_NAME} phase not found on iOS app target.`);
116
+ }
117
+
118
+ function insertBlock(script) {
119
+ if (script.includes(BEGIN)) {
120
+ return script;
121
+ }
122
+ return `${BLOCK}\n${script}`;
123
+ }
124
+
125
+ function removeBlock(script) {
126
+ return script.replace(
127
+ /# react-native-debug-toolkit: begin debug bundle\r?\nexport FORCE_BUNDLING=1\r?\n# react-native-debug-toolkit: end debug bundle\r?\n?/g,
128
+ '',
129
+ );
130
+ }
131
+
132
+ function writeProject(ctx, script) {
133
+ ctx.phase.shellScript = encodeScript(script);
134
+ fs.writeFileSync(ctx.pbxFile, ctx.proj.writeSync());
135
+ }
136
+
137
+ function setupIosBundle(options) {
138
+ const ctx = loadTarget(options.cwd, options.iosTarget);
139
+ const script = decodeScript(ctx.phase.shellScript);
140
+ const next = insertBlock(script);
141
+
142
+ if (next === script) {
143
+ return { ok: true, changed: false };
144
+ }
145
+
146
+ writeProject(ctx, next);
147
+ return { ok: true, changed: true };
148
+ }
149
+
150
+ function undoIosBundle(options) {
151
+ const ctx = loadTarget(options.cwd, options.iosTarget);
152
+ const script = decodeScript(ctx.phase.shellScript);
153
+ const next = removeBlock(script);
154
+
155
+ if (next === script) {
156
+ return { ok: true, changed: false };
157
+ }
158
+
159
+ writeProject(ctx, next);
160
+ return { ok: true, changed: true };
161
+ }
162
+
163
+ function checkIosBundle(options) {
164
+ const ctx = loadTarget(options.cwd, options.iosTarget);
165
+ const script = decodeScript(ctx.phase.shellScript);
166
+
167
+ return {
168
+ ok: script.includes(BLOCK),
169
+ changed: false,
170
+ };
171
+ }
172
+
173
+ module.exports = {
174
+ setupIosBundle,
175
+ undoIosBundle,
176
+ checkIosBundle,
177
+ BEGIN,
178
+ END,
179
+ };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ const { setupIosBundle, undoIosBundle, checkIosBundle } = require('./ios');
4
+ const { setupAndroidBundle, undoAndroidBundle, checkAndroidBundle } = require('./android');
5
+
6
+ async function setupBundle(options) {
7
+ const platforms = options.platform === 'all' ? ['ios', 'android'] : [options.platform];
8
+ const results = [];
9
+
10
+ for (const platform of platforms) {
11
+ if (platform === 'ios') {
12
+ if (options.undo) {
13
+ results.push(undoIosBundle(options));
14
+ } else if (options.check) {
15
+ results.push(checkIosBundle(options));
16
+ } else {
17
+ results.push(setupIosBundle(options));
18
+ }
19
+ continue;
20
+ }
21
+
22
+ if (platform === 'android') {
23
+ if (options.undo) {
24
+ results.push(undoAndroidBundle(options));
25
+ } else if (options.check) {
26
+ results.push(checkAndroidBundle(options));
27
+ } else {
28
+ results.push(setupAndroidBundle(options));
29
+ }
30
+ continue;
31
+ }
32
+
33
+ throw new Error(`Unsupported platform: ${platform}`);
34
+ }
35
+
36
+ return results;
37
+ }
38
+
39
+ module.exports = { setupBundle };
@@ -0,0 +1,147 @@
1
+ // react-native-debug-toolkit: persistent debug/development bundle generation
2
+
3
+ import java.util.Locale
4
+ import javax.inject.Inject
5
+ import org.gradle.api.DefaultTask
6
+ import org.gradle.api.GradleException
7
+ import org.gradle.api.file.DirectoryProperty
8
+ import org.gradle.api.file.RegularFileProperty
9
+ import org.gradle.api.provider.ListProperty
10
+ import org.gradle.api.provider.Property
11
+ import org.gradle.api.tasks.Input
12
+ import org.gradle.api.tasks.InputDirectory
13
+ import org.gradle.api.tasks.InputFile
14
+ import org.gradle.api.tasks.Optional
15
+ import org.gradle.api.tasks.OutputDirectory
16
+ import org.gradle.api.tasks.TaskAction
17
+ import org.gradle.process.ExecOperations
18
+
19
+ abstract class DebugToolkitBundleTask extends DefaultTask {
20
+ @Inject
21
+ abstract ExecOperations getExecOperations()
22
+
23
+ @InputDirectory
24
+ abstract DirectoryProperty getRoot()
25
+
26
+ @InputFile
27
+ abstract RegularFileProperty getCliFile()
28
+
29
+ @InputFile
30
+ @Optional
31
+ abstract RegularFileProperty getEntryFile()
32
+
33
+ @InputFile
34
+ @Optional
35
+ abstract RegularFileProperty getBundleConfig()
36
+
37
+ @Input
38
+ abstract ListProperty<String> getNodeExecutableAndArgs()
39
+
40
+ @Input
41
+ abstract Property<String> getBundleCommand()
42
+
43
+ @Input
44
+ abstract Property<String> getBundleAssetName()
45
+
46
+ @Input
47
+ abstract ListProperty<String> getExtraPackagerArgs()
48
+
49
+ @OutputDirectory
50
+ abstract DirectoryProperty getJsBundleDir()
51
+
52
+ @OutputDirectory
53
+ abstract DirectoryProperty getResourcesDir()
54
+
55
+ @TaskAction
56
+ void run() {
57
+ def rootFile = root.get().asFile
58
+ def bundleOutput = new File(jsBundleDir.get().asFile, bundleAssetName.get())
59
+ def entry = resolveEntryFile(rootFile)
60
+ jsBundleDir.get().asFile.mkdirs()
61
+ resourcesDir.get().asFile.mkdirs()
62
+
63
+ def command = []
64
+ command.addAll(nodeExecutableAndArgs.get())
65
+ command.add(cliFile.get().asFile.absolutePath)
66
+ command.add(bundleCommand.get())
67
+ command.addAll([
68
+ "--platform", "android",
69
+ "--dev", "true",
70
+ "--entry-file", entry.absolutePath,
71
+ "--bundle-output", bundleOutput.absolutePath,
72
+ "--assets-dest", resourcesDir.get().asFile.absolutePath,
73
+ ])
74
+
75
+ if (bundleConfig.isPresent()) {
76
+ command.add("--config")
77
+ command.add(bundleConfig.get().asFile.absolutePath)
78
+ }
79
+
80
+ command.addAll(extraPackagerArgs.get())
81
+
82
+ execOperations.exec {
83
+ workingDir(rootFile)
84
+ commandLine(command)
85
+ }
86
+ }
87
+
88
+ private File resolveEntryFile(File rootFile) {
89
+ if (entryFile.isPresent()) {
90
+ return entryFile.get().asFile
91
+ }
92
+
93
+ def androidEntry = new File(rootFile, "index.android.js")
94
+ return androidEntry.exists() ? androidEntry : new File(rootFile, "index.js")
95
+ }
96
+ }
97
+
98
+ def toolkitCapitalize = { String value ->
99
+ value.length() == 0 ? value : value.substring(0, 1).toUpperCase(Locale.ROOT) + value.substring(1)
100
+ }
101
+
102
+ def toolkitReactExt = project.extensions.findByName("react")
103
+ def toolkitAndroidComponents = project.extensions.findByName("androidComponents")
104
+
105
+ if (toolkitAndroidComponents == null) {
106
+ throw new GradleException("react-native-debug-toolkit debug-bundle.gradle requires the Android Gradle Plugin.")
107
+ }
108
+
109
+ def toolkitDefaultRoot = project.rootProject.layout.projectDirectory.dir("../")
110
+ def toolkitDebuggableVariants = toolkitReactExt?.debuggableVariants?.getOrElse(["debug"]) ?: ["debug"]
111
+
112
+ toolkitAndroidComponents.onVariants(toolkitAndroidComponents.selector().all()) { variant ->
113
+ def variantName = variant.name
114
+ def isDebuggable = toolkitDebuggableVariants.any { it.equalsIgnoreCase(variantName) }
115
+
116
+ if (!isDebuggable) {
117
+ return
118
+ }
119
+
120
+ def capName = toolkitCapitalize(variantName)
121
+ def bundleTask = tasks.register("createDebugToolkit${capName}JsAndAssets", DebugToolkitBundleTask) {
122
+ group = "react"
123
+ description = "Generate embedded React Native JS bundle for ${variantName} debug build."
124
+
125
+ root.set(toolkitReactExt?.root ?: toolkitDefaultRoot)
126
+ cliFile.set(toolkitReactExt?.cliFile ?: root.file("node_modules/react-native/cli.js"))
127
+ if (toolkitReactExt?.entryFile?.isPresent()) {
128
+ entryFile.set(toolkitReactExt.entryFile)
129
+ }
130
+ if (toolkitReactExt?.bundleConfig?.isPresent()) {
131
+ bundleConfig.set(toolkitReactExt.bundleConfig)
132
+ }
133
+ nodeExecutableAndArgs.set(toolkitReactExt?.nodeExecutableAndArgs ?: ["node"])
134
+ bundleCommand.set(toolkitReactExt?.bundleCommand ?: "bundle")
135
+ bundleAssetName.set(toolkitReactExt?.bundleAssetName ?: "index.android.bundle")
136
+ extraPackagerArgs.set(toolkitReactExt?.extraPackagerArgs ?: [])
137
+ jsBundleDir.set(project.layout.buildDirectory.dir("generated/assets/react-native-debug-toolkit/${variantName}"))
138
+ resourcesDir.set(project.layout.buildDirectory.dir("generated/res/react-native-debug-toolkit/${variantName}"))
139
+ }
140
+
141
+ variant.sources.assets?.addGeneratedSourceDirectory(bundleTask) { task ->
142
+ task.jsBundleDir
143
+ }
144
+ variant.sources.res?.addGeneratedSourceDirectory(bundleTask) { task ->
145
+ task.resourcesDir
146
+ }
147
+ }
@@ -1,23 +0,0 @@
1
- // react-native-debug-toolkit: Debug bundle generation for Android
2
- // Applied via: apply from: "../../node_modules/react-native-debug-toolkit/scripts/android-debug-bundle.gradle"
3
-
4
- tasks.register('generateDebugBundle', Exec) {
5
- commandLine 'npx', 'react-native', 'bundle',
6
- '--platform', 'android',
7
- '--dev', 'true',
8
- '--entry-file', project.ext.has('debugToolkitEntryFile')
9
- ? project.ext.debugToolkitEntryFile : 'index.js',
10
- '--bundle-output', "${projectDir}/src/main/assets/index.android.bundle",
11
- '--assets-dest', "${projectDir}/src/main/res"
12
- }
13
-
14
- afterEvaluate {
15
- // Disable for release variants
16
- tasks.matching { it.name.contains('Release') }.configureEach {
17
- tasks.named('generateDebugBundle').get().enabled = false
18
- }
19
- // Wire as preBuild dependency for debug variants
20
- android.applicationVariants.matching { it.buildType.name == 'debug' }.configureEach {
21
- preBuildProvider.configure { dependsOn 'generateDebugBundle' }
22
- }
23
- }
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env bash
2
- # react-native-debug-toolkit: EAS Build postInstall hook
3
- # Usage in eas.json:
4
- # "postInstall": "bash node_modules/react-native-debug-toolkit/scripts/eas-postinstall.sh"
5
- set -e
6
- npx debug-toolkit embed --yes
@@ -1,116 +0,0 @@
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
- };
@@ -1,109 +0,0 @@
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 };