From 8a1f774b314ce2d2489e9e3412a915156aa18694 Mon Sep 17 00:00:00 2001 From: Minsik Kim Date: Thu, 9 Apr 2026 23:17:44 +0900 Subject: [PATCH 1/2] Improve Expo Android CodePush bundle wiring compatibility --- expo.js | 54 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/expo.js b/expo.js index c0a4783d..2a38171a 100644 --- a/expo.js +++ b/expo.js @@ -193,35 +193,43 @@ const withAndroidMainApplication = (config) => { } } - // --- 4. Wire up CodePush bundle file --- - if (!content.includes("CodePush.getJSBundleFile()")) { - const hermesEnabledAnchor = /(override\s+val\s+isHermesEnabled:\s*Boolean\s*=\s*BuildConfig\.IS_HERMES_ENABLED)\s*\n/m; - if (hermesEnabledAnchor.test(content)) { - // RN < 0.82: uses ReactNativeHost with getJSBundleFile() override - const getJSBundleFileMethodString = ` + // --- 4. Wire up CodePush bundle file --- + if (!content.includes("CodePush.getJSBundleFile()")) { + const getJSBundleFileMethodString = ` override fun getJSBundleFile(): String { return CodePush.getJSBundleFile() }`; - content = content.replace(hermesEnabledAnchor, `$1\n${getJSBundleFileMethodString}\n`); + const reactNativeHostAnchors = [ + /(override\s+fun\s+getJSMainModuleName\(\):\s*String\s*=\s*[^\n]+)\s*\n/m, + /(override\s+fun\s+getUseDeveloperSupport\(\):\s*Boolean\s*=\s*BuildConfig\.DEBUG)\s*\n/m, + /(override\s+val\s+isHermesEnabled:\s*Boolean\s*=\s*BuildConfig\.IS_HERMES_ENABLED)\s*\n/m, + /(override\s+val\s+isNewArchEnabled:\s*Boolean\s*=\s*BuildConfig\.IS_NEW_ARCHITECTURE_ENABLED)\s*\n/m, + ]; + const reactNativeHostAnchor = reactNativeHostAnchors.find(anchor => anchor.test(content)); + + if (reactNativeHostAnchor) { + // RN <= 0.81 and Expo SDK 54 still configure the bundle via ReactNativeHost. + // Expo wraps the host, but ReactNativeHostWrapper delegates getJSBundleFile() to the wrapped host. + content = content.replace(reactNativeHostAnchor, `$1\n${getJSBundleFileMethodString}\n`); + } else { + // RN 0.82+: uses ReactHost via getDefaultReactHost() — pass jsBundleFilePath parameter + // Match the closing parenthesis of the getDefaultReactHost() call + const reactHostCallRegex = /(getDefaultReactHost\([\s\S]*?packageList\s*=[\s\S]*?\})([\s\S]*?\))/m; + if (reactHostCallRegex.test(content)) { + content = content.replace(reactHostCallRegex, (match, beforeClose, closing) => { + // Check if jsBundleFilePath is already set + if (match.includes('jsBundleFilePath')) return match; + // Insert the parameter before the closing parentheses + return `${beforeClose},\n jsBundleFilePath = CodePush.getJSBundleFile()${closing}`; + }); } else { - // RN 0.82+: uses ReactHost via getDefaultReactHost() — pass jsBundleFilePath parameter - // Match the closing parenthesis of the getDefaultReactHost() call - const reactHostCallRegex = /(getDefaultReactHost\([\s\S]*?packageList\s*=[\s\S]*?\})([\s\S]*?\))/m; - if (reactHostCallRegex.test(content)) { - content = content.replace(reactHostCallRegex, (match, beforeClose, closing) => { - // Check if jsBundleFilePath is already set - if (match.includes('jsBundleFilePath')) return match; - // Insert the parameter before the closing parentheses - return `${beforeClose},\n jsBundleFilePath = CodePush.getJSBundleFile()${closing}`; - }); - } else { - WarningAggregator.addWarningAndroid( - 'codepush-plugin', - 'Could not find getDefaultReactHost() call in MainApplication. CodePush bundle file path not configured.' - ); - } + WarningAggregator.addWarningAndroid( + 'codepush-plugin', + 'Could not detect a supported React host configuration in MainApplication. CodePush bundle file path not configured.' + ); } } + } modConfig.modResults.contents = content; return modConfig; From 4603270f3466fa6cbf95a11b3c11185a0a243194 Mon Sep 17 00:00:00 2001 From: Minsik Kim Date: Wed, 8 Apr 2026 18:22:02 +0900 Subject: [PATCH 2/2] fix expo test setup --- test/template/app.json | 24 ++++++++------------- test/test.ts | 48 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/test/template/app.json b/test/template/app.json index 3508691c..688070ef 100644 --- a/test/template/app.json +++ b/test/template/app.json @@ -4,29 +4,15 @@ "slug": "TestCodePush", "version": "1.0.0", "orientation": "portrait", - "icon": "./assets/icon.png", "userInterfaceStyle": "light", "newArchEnabled": true, - "splash": { - "image": "./assets/splash-icon.png", - "resizeMode": "contain", - "backgroundColor": "#ffffff" - }, "ios": { "supportsTablet": true, "bundleIdentifier": "com.testcodepush" }, "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#ffffff" - }, - "edgeToEdgeEnabled": true, "package": "com.testcodepush" }, - "web": { - "favicon": "./assets/favicon.png" - }, "plugins": [ [ "@code-push-next/react-native-code-push/expo", @@ -40,7 +26,15 @@ "CodePushServerURL": "http://10.0.2.2:3001" } } + ], + [ + "expo-build-properties", + { + "ios": { + "deploymentTarget": "15.5" + } + } ] ] } -} \ No newline at end of file +} diff --git a/test/test.ts b/test/test.ts index 7ab4a3a8..7e59e782 100644 --- a/test/test.ts +++ b/test/test.ts @@ -12,6 +12,44 @@ import Q = require("q"); import del = require("del"); +function ensureAndroidCleartextTraffic(androidManifestPath: string): void { + const androidManifestContents = fs.readFileSync(androidManifestPath, "utf8"); + + if (androidManifestContents.includes("android:usesCleartextTraffic=\"true\"")) { + return; + } + + let nextContents = androidManifestContents; + + if (androidManifestContents.includes("android:usesCleartextTraffic=\"false\"")) { + nextContents = androidManifestContents.replace("android:usesCleartextTraffic=\"false\"", "android:usesCleartextTraffic=\"true\""); + } else if (/ tag in AndroidManifest.xml: ${androidManifestPath}`); + } + + if (nextContents !== androidManifestContents) { + fs.writeFileSync(androidManifestPath, nextContents, "utf8"); + } +} + +function installExpoBundleTooling(projectPath: string): Q.Promise { + const packageJsonPath = path.join(projectPath, "package.json"); + const packageJsonContents = fs.readFileSync(packageJsonPath, "utf8"); + const packageJson = JSON.parse(packageJsonContents); + const reactNativeVersion = packageJson.dependencies && packageJson.dependencies["react-native"]; + + if (!reactNativeVersion) { + throw new Error(`Could not determine react-native version from ${packageJsonPath}`); + } + + return TestUtil.getProcessOutput( + `npm install --save-dev @react-native/metro-config@${reactNativeVersion}`, + { cwd: projectPath } + ).then(() => { return null; }); +} + ////////////////////////////////////////////////////////////////////////////////////////// // Create the platforms to run the tests on. @@ -86,7 +124,7 @@ class RNAndroid extends Platform.Android implements RNPlatform { // we use hard-coded deployment key and server url in app.json return Q.Promise((resolve, reject) => { TestUtil.replaceString(androidMainActivityPath, "\"main\"", `"${TestConfig.TestAppName}"`); - TestUtil.replaceString(AndroidManifest, "\\${usesCleartextTraffic}", "true"); + ensureAndroidCleartextTraffic(AndroidManifest); resolve(null); }); } @@ -329,11 +367,17 @@ class RNProjectManager extends ProjectManager { mkdirp.sync(projectDirectory); if (TestConfig.isExpoApp) { - return TestUtil.getProcessOutput(`npx create-expo-app@latest ${appName} --template blank`, { cwd: projectDirectory, timeout: 30 * 60 * 1000 }) + return TestUtil.getProcessOutput(`npx create-expo-app@latest ${appName} --template blank@sdk-55`, { cwd: projectDirectory, timeout: 30 * 60 * 1000 }) .then((e) => { console.log(`"npx expo init ${appName}" success. cwd=${projectDirectory}`); return e; }) .then(this.copyTemplate.bind(this, templatePath, projectDirectory)) .then(TestUtil.getProcessOutput.bind(undefined, TestConfig.thisPluginInstallString, { cwd: path.join(projectDirectory, TestConfig.TestAppName) })) + .then(installExpoBundleTooling.bind(undefined, path.join(projectDirectory, TestConfig.TestAppName))) + .then(TestUtil.getProcessOutput.bind(undefined, "npx expo install expo-build-properties", { cwd: path.join(projectDirectory, TestConfig.TestAppName) })) .then(TestUtil.getProcessOutput.bind(undefined, `npx expo prebuild --clean`, { cwd: path.join(projectDirectory, TestConfig.TestAppName) })) + .then(() => { + ensureAndroidCleartextTraffic(path.join(projectDirectory, TestConfig.TestAppName, "android", "app", "src", "main", "AndroidManifest.xml")); + return null; + }) .then(() => { return null; }); } else { return TestUtil.getProcessOutput("npx @react-native-community/cli init " + appName + " --version 0.82.1 --install-pods", { cwd: projectDirectory, timeout: 30 * 60 * 1000 })