Skip to content

Commit 877b4c7

Browse files
authored
Improve Expo plugin compatibility (#46)
* Improve Expo Android CodePush bundle wiring compatibility * fix expo test setup
1 parent 9182032 commit 877b4c7

3 files changed

Lines changed: 86 additions & 40 deletions

File tree

expo.js

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -193,35 +193,43 @@ const withAndroidMainApplication = (config) => {
193193
}
194194
}
195195

196-
// --- 4. Wire up CodePush bundle file ---
197-
if (!content.includes("CodePush.getJSBundleFile()")) {
198-
const hermesEnabledAnchor = /(override\s+val\s+isHermesEnabled:\s*Boolean\s*=\s*BuildConfig\.IS_HERMES_ENABLED)\s*\n/m;
199-
if (hermesEnabledAnchor.test(content)) {
200-
// RN < 0.82: uses ReactNativeHost with getJSBundleFile() override
201-
const getJSBundleFileMethodString = `
196+
// --- 4. Wire up CodePush bundle file ---
197+
if (!content.includes("CodePush.getJSBundleFile()")) {
198+
const getJSBundleFileMethodString = `
202199
override fun getJSBundleFile(): String {
203200
return CodePush.getJSBundleFile()
204201
}`;
205-
content = content.replace(hermesEnabledAnchor, `$1\n${getJSBundleFileMethodString}\n`);
202+
const reactNativeHostAnchors = [
203+
/(override\s+fun\s+getJSMainModuleName\(\):\s*String\s*=\s*[^\n]+)\s*\n/m,
204+
/(override\s+fun\s+getUseDeveloperSupport\(\):\s*Boolean\s*=\s*BuildConfig\.DEBUG)\s*\n/m,
205+
/(override\s+val\s+isHermesEnabled:\s*Boolean\s*=\s*BuildConfig\.IS_HERMES_ENABLED)\s*\n/m,
206+
/(override\s+val\s+isNewArchEnabled:\s*Boolean\s*=\s*BuildConfig\.IS_NEW_ARCHITECTURE_ENABLED)\s*\n/m,
207+
];
208+
const reactNativeHostAnchor = reactNativeHostAnchors.find(anchor => anchor.test(content));
209+
210+
if (reactNativeHostAnchor) {
211+
// RN <= 0.81 and Expo SDK 54 still configure the bundle via ReactNativeHost.
212+
// Expo wraps the host, but ReactNativeHostWrapper delegates getJSBundleFile() to the wrapped host.
213+
content = content.replace(reactNativeHostAnchor, `$1\n${getJSBundleFileMethodString}\n`);
214+
} else {
215+
// RN 0.82+: uses ReactHost via getDefaultReactHost() — pass jsBundleFilePath parameter
216+
// Match the closing parenthesis of the getDefaultReactHost() call
217+
const reactHostCallRegex = /(getDefaultReactHost\([\s\S]*?packageList\s*=[\s\S]*?\})([\s\S]*?\))/m;
218+
if (reactHostCallRegex.test(content)) {
219+
content = content.replace(reactHostCallRegex, (match, beforeClose, closing) => {
220+
// Check if jsBundleFilePath is already set
221+
if (match.includes('jsBundleFilePath')) return match;
222+
// Insert the parameter before the closing parentheses
223+
return `${beforeClose},\n jsBundleFilePath = CodePush.getJSBundleFile()${closing}`;
224+
});
206225
} else {
207-
// RN 0.82+: uses ReactHost via getDefaultReactHost() — pass jsBundleFilePath parameter
208-
// Match the closing parenthesis of the getDefaultReactHost() call
209-
const reactHostCallRegex = /(getDefaultReactHost\([\s\S]*?packageList\s*=[\s\S]*?\})([\s\S]*?\))/m;
210-
if (reactHostCallRegex.test(content)) {
211-
content = content.replace(reactHostCallRegex, (match, beforeClose, closing) => {
212-
// Check if jsBundleFilePath is already set
213-
if (match.includes('jsBundleFilePath')) return match;
214-
// Insert the parameter before the closing parentheses
215-
return `${beforeClose},\n jsBundleFilePath = CodePush.getJSBundleFile()${closing}`;
216-
});
217-
} else {
218-
WarningAggregator.addWarningAndroid(
219-
'codepush-plugin',
220-
'Could not find getDefaultReactHost() call in MainApplication. CodePush bundle file path not configured.'
221-
);
222-
}
226+
WarningAggregator.addWarningAndroid(
227+
'codepush-plugin',
228+
'Could not detect a supported React host configuration in MainApplication. CodePush bundle file path not configured.'
229+
);
223230
}
224231
}
232+
}
225233

226234
modConfig.modResults.contents = content;
227235
return modConfig;

test/template/app.json

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,15 @@
44
"slug": "TestCodePush",
55
"version": "1.0.0",
66
"orientation": "portrait",
7-
"icon": "./assets/icon.png",
87
"userInterfaceStyle": "light",
98
"newArchEnabled": true,
10-
"splash": {
11-
"image": "./assets/splash-icon.png",
12-
"resizeMode": "contain",
13-
"backgroundColor": "#ffffff"
14-
},
159
"ios": {
1610
"supportsTablet": true,
1711
"bundleIdentifier": "com.testcodepush"
1812
},
1913
"android": {
20-
"adaptiveIcon": {
21-
"foregroundImage": "./assets/adaptive-icon.png",
22-
"backgroundColor": "#ffffff"
23-
},
24-
"edgeToEdgeEnabled": true,
2514
"package": "com.testcodepush"
2615
},
27-
"web": {
28-
"favicon": "./assets/favicon.png"
29-
},
3016
"plugins": [
3117
[
3218
"@code-push-next/react-native-code-push/expo",
@@ -40,7 +26,15 @@
4026
"CodePushServerURL": "http://10.0.2.2:3001"
4127
}
4228
}
29+
],
30+
[
31+
"expo-build-properties",
32+
{
33+
"ios": {
34+
"deploymentTarget": "15.5"
35+
}
36+
}
4337
]
4438
]
4539
}
46-
}
40+
}

test/test.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,44 @@ import Q = require("q");
1212

1313
import del = require("del");
1414

15+
function ensureAndroidCleartextTraffic(androidManifestPath: string): void {
16+
const androidManifestContents = fs.readFileSync(androidManifestPath, "utf8");
17+
18+
if (androidManifestContents.includes("android:usesCleartextTraffic=\"true\"")) {
19+
return;
20+
}
21+
22+
let nextContents = androidManifestContents;
23+
24+
if (androidManifestContents.includes("android:usesCleartextTraffic=\"false\"")) {
25+
nextContents = androidManifestContents.replace("android:usesCleartextTraffic=\"false\"", "android:usesCleartextTraffic=\"true\"");
26+
} else if (/<application\b/.test(androidManifestContents)) {
27+
nextContents = androidManifestContents.replace(/<application\b/, "<application android:usesCleartextTraffic=\"true\"");
28+
} else {
29+
throw new Error(`Could not find <application> tag in AndroidManifest.xml: ${androidManifestPath}`);
30+
}
31+
32+
if (nextContents !== androidManifestContents) {
33+
fs.writeFileSync(androidManifestPath, nextContents, "utf8");
34+
}
35+
}
36+
37+
function installExpoBundleTooling(projectPath: string): Q.Promise<void> {
38+
const packageJsonPath = path.join(projectPath, "package.json");
39+
const packageJsonContents = fs.readFileSync(packageJsonPath, "utf8");
40+
const packageJson = JSON.parse(packageJsonContents);
41+
const reactNativeVersion = packageJson.dependencies && packageJson.dependencies["react-native"];
42+
43+
if (!reactNativeVersion) {
44+
throw new Error(`Could not determine react-native version from ${packageJsonPath}`);
45+
}
46+
47+
return TestUtil.getProcessOutput(
48+
`npm install --save-dev @react-native/metro-config@${reactNativeVersion}`,
49+
{ cwd: projectPath }
50+
).then(() => { return null; });
51+
}
52+
1553
//////////////////////////////////////////////////////////////////////////////////////////
1654
// Create the platforms to run the tests on.
1755

@@ -86,7 +124,7 @@ class RNAndroid extends Platform.Android implements RNPlatform {
86124
// we use hard-coded deployment key and server url in app.json
87125
return Q.Promise<void>((resolve, reject) => {
88126
TestUtil.replaceString(androidMainActivityPath, "\"main\"", `"${TestConfig.TestAppName}"`);
89-
TestUtil.replaceString(AndroidManifest, "\\${usesCleartextTraffic}", "true");
127+
ensureAndroidCleartextTraffic(AndroidManifest);
90128
resolve(null);
91129
});
92130
}
@@ -329,11 +367,17 @@ class RNProjectManager extends ProjectManager {
329367
mkdirp.sync(projectDirectory);
330368

331369
if (TestConfig.isExpoApp) {
332-
return TestUtil.getProcessOutput(`npx create-expo-app@latest ${appName} --template blank`, { cwd: projectDirectory, timeout: 30 * 60 * 1000 })
370+
return TestUtil.getProcessOutput(`npx create-expo-app@latest ${appName} --template blank@sdk-55`, { cwd: projectDirectory, timeout: 30 * 60 * 1000 })
333371
.then((e) => { console.log(`"npx expo init ${appName}" success. cwd=${projectDirectory}`); return e; })
334372
.then(this.copyTemplate.bind(this, templatePath, projectDirectory))
335373
.then<void>(TestUtil.getProcessOutput.bind(undefined, TestConfig.thisPluginInstallString, { cwd: path.join(projectDirectory, TestConfig.TestAppName) }))
374+
.then(installExpoBundleTooling.bind(undefined, path.join(projectDirectory, TestConfig.TestAppName)))
375+
.then<void>(TestUtil.getProcessOutput.bind(undefined, "npx expo install expo-build-properties", { cwd: path.join(projectDirectory, TestConfig.TestAppName) }))
336376
.then(TestUtil.getProcessOutput.bind(undefined, `npx expo prebuild --clean`, { cwd: path.join(projectDirectory, TestConfig.TestAppName) }))
377+
.then(() => {
378+
ensureAndroidCleartextTraffic(path.join(projectDirectory, TestConfig.TestAppName, "android", "app", "src", "main", "AndroidManifest.xml"));
379+
return null;
380+
})
337381
.then(() => { return null; });
338382
} else {
339383
return TestUtil.getProcessOutput("npx @react-native-community/cli init " + appName + " --version 0.82.1 --install-pods", { cwd: projectDirectory, timeout: 30 * 60 * 1000 })

0 commit comments

Comments
 (0)