Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ const config: ExpoConfig = {
"./plugins/withIosSceneLifecycle.cjs",
"./plugins/withAndroidCleartextTraffic.cjs",
"./plugins/withAndroidGradleHeap.cjs",
"./plugins/withAndroidFbjniVersion.cjs",
"./plugins/withAndroidInputBackground.cjs",
"./plugins/withAndroidModernPopupMenu.cjs",
"./plugins/withAndroidModernAlertDialog.cjs",
Expand Down
30 changes: 30 additions & 0 deletions apps/mobile/plugins/withAndroidFbjniVersion.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const { withProjectBuildGradle } = require("expo/config-plugins");

const FBJNI_FORCE = "resolutionStrategy.force 'com.facebook.fbjni:fbjni:0.7.0'";

module.exports = function withAndroidFbjniVersion(config) {
return withProjectBuildGradle(config, (nextConfig) => {
if (nextConfig.modResults.language !== "groovy") {
throw new Error("withAndroidFbjniVersion: project build.gradle must use Groovy.");
}

const contents = nextConfig.modResults.contents;
if (contents.includes(FBJNI_FORCE)) {
return nextConfig;
}

// react-native 0.86.3 requires fbjni 0.7.0. react-native-shiki-engine
// requests fbjni:+, whose NDK 28 binary is incompatible with RN's NDK 27
// libc++ and crashes before JavaScript starts.
nextConfig.modResults.contents = `${contents.trimEnd()}

allprojects {
configurations.all {
${FBJNI_FORCE}
}
}
`;

return nextConfig;
});
};
65 changes: 65 additions & 0 deletions apps/mobile/plugins/withAndroidFbjniVersion.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as NodeFS from "node:fs";
import * as NodeModule from "node:module";
import * as NodePath from "node:path";
import { describe, expect, it } from "vitest";
import withAndroidFbjniVersion from "./withAndroidFbjniVersion.cjs";

const buildGradle = `buildscript {
repositories {
google()
mavenCentral()
}
}

allprojects {
repositories {
google()
mavenCentral()
}
}
`;

async function transform(contents, language = "groovy") {
const config = withAndroidFbjniVersion({ name: "Test", slug: "test" });
const result = await config.mods.android.projectBuildGradle({
...config,
modRequest: { platform: "android", modName: "projectBuildGradle", introspect: false },
modResults: { language, contents },
});
return result.modResults.contents;
}

describe("Android fbjni version generation", () => {
it("forces the React Native fbjni version and stays idempotent", async () => {
const generated = await transform(buildGradle);

expect(generated).toContain(
"resolutionStrategy.force 'com.facebook.fbjni:fbjni:0.7.0'",
);
expect(await transform(generated)).toBe(generated);
});


it("keeps the forced native library aligned with the installed React Native runtime", async () => {
const require = NodeModule.createRequire(import.meta.url);
const reactNativeRoot = NodePath.dirname(require.resolve("react-native/package.json"));
const versions = NodeFS.readFileSync(
NodePath.join(reactNativeRoot, "gradle/libs.versions.toml"),
"utf8",
);
const reactNativeFbjni = versions.match(/^fbjni\s*=\s*"([^"]+)"/m)?.[1];
expect(reactNativeFbjni).toBeDefined();

const generated = await transform(buildGradle);
const forcedFbjni = generated.match(
/resolutionStrategy\.force 'com\.facebook\.fbjni:fbjni:([^']+)'/,
)?.[1];
expect(forcedFbjni).toBe(reactNativeFbjni);
});

it("fails visibly if Expo switches the project build file away from Groovy", async () => {
await expect(transform(buildGradle, "kotlin")).rejects.toThrow(
"project build.gradle must use Groovy",
);
});
});
Loading