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
8 changes: 7 additions & 1 deletion Example/expoUsePushy/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@
},
"web": {
"favicon": "./assets/favicon.png"
}
},
"plugins": [
"./plugins/with-rct-bridge-bridging-header",
"expo-font",
"expo-router",
"expo-web-browser"
]
}
}
2,632 changes: 1,625 additions & 1,007 deletions Example/expoUsePushy/bun.lock

Large diffs are not rendered by default.

55 changes: 28 additions & 27 deletions Example/expoUsePushy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,41 @@
"lint": "expo lint"
},
"dependencies": {
"@expo/vector-icons": "^14.1.0",
"@expo/vector-icons": "^15.0.3",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6",
"expo": "~53.0.22",
"expo-blur": "~14.1.5",
"expo-constants": "~17.1.7",
"expo-font": "~13.3.2",
"expo-haptics": "~14.1.4",
"expo-image": "~2.4.0",
"expo-linking": "~7.1.7",
"expo-router": "~5.1.5",
"expo-splash-screen": "~0.30.10",
"expo-status-bar": "~2.2.3",
"expo-symbols": "~0.4.5",
"expo-system-ui": "~5.0.11",
"expo-web-browser": "~14.2.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.6",
"react-native-gesture-handler": "~2.24.0",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-update": "^10.34.4",
"react-native-web": "~0.20.0",
"react-native-webview": "13.13.5"
"expo": "^54.0.0",
"expo-blur": "~15.0.8",
"expo-constants": "~18.0.13",
"expo-font": "~14.0.12",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linking": "~8.0.12",
"expo-router": "~6.0.24",
"expo-splash-screen": "~31.0.13",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9",
"expo-web-browser": "~15.0.11",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-update": "^10.42.6",
"react-native-web": "^0.21.0",
"react-native-webview": "13.15.0",
"react-native-worklets": "0.5.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "~19.0.10",
"typescript": "~5.8.3",
"@types/react": "~19.1.10",
"eslint": "^9.25.0",
"eslint-config-expo": "~9.2.0"
"eslint-config-expo": "~10.0.0",
"typescript": "~5.9.2"
},
"private": true
}
43 changes: 43 additions & 0 deletions app.plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const fs = require("fs");
const path = require("path");

function requireConfigPlugins() {
try {
return require("@expo/config-plugins");
} catch {
return require(require.resolve("@expo/config-plugins", {
paths: [process.cwd()],
}));
}
}

const { withDangerousMod } = requireConfigPlugins();
Comment on lines +4 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the plugin file referenced by app.json
fd -uu -t f 'with-rct-bridge-bridging-header'
fd -uu -t f 'app.plugin.js'

Repository: reactnativecn/react-native-update

Length of output: 185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the example config and plugin entrypoint references
git ls-files 'Example/expoUsePushy/*' 'Example/expoUsePushy/**/*' 'app.plugin.js'

printf '\n--- app.json ---\n'
sed -n '1,220p' Example/expoUsePushy/app.json

printf '\n--- app.plugin.js ---\n'
cat -n app.plugin.js

printf '\n--- plugin path search ---\n'
fd -uu -t f 'with-rct-bridge-bridging-header' Example/expoUsePushy . || true
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' '"\.\/plugins\/with-rct-bridge-bridging-header"|with-rct-bridge-bridging-header|app\.plugin\.js' Example/expoUsePushy .

Repository: reactnativecn/react-native-update

Length of output: 3194


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- example package.json ---'
sed -n '1,220p' Example/expoUsePushy/package.json

printf '\n%s\n' '--- references to app.plugin.js / with-rct-bridge-bridging-header ---'
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' 'app\.plugin\.js|with-rct-bridge-bridging-header|plugins/\./plugins|plugins"' Example/expoUsePushy .

printf '\n%s\n' '--- files named plugins or plugin entrypoints ---'
fd -uu -t f 'app.plugin.js' .
fd -uu -t d 'plugins' Example/expoUsePushy .

Repository: reactnativecn/react-native-update

Length of output: 50389


🌐 Web query:

Expo config plugin relative path app.plugin.js app.json plugins "./plugins/with-rct-bridge-bridging-header" resolution

💡 Result:

In Expo, local config plugins referenced by a relative path in your app.json (e.g., "./plugins/with-rct-bridge-bridging-header") are resolved relative to the project root [1]. When you specify a string path in the plugins array, Expo CLI resolves it using Node's module resolution system [1]. For a local file like "./plugins/with-rct-bridge-bridging-header", Expo will attempt to load the file by appending supported extensions if necessary (such as.js,.ts,.cjs,.mjs, etc., following the latest resolution logic) [2]. Key points regarding this resolution: 1. Local vs. Module Resolution: If the string does not start with a relative path indicator (./ or../) or an absolute path, Expo treats it as an npm package [1]. Using./ ensures it is treated as a local file relative to your project's root directory [1]. 2. Supported Formats: As of May 2026, Expo has aligned config plugin resolution with standard app config resolution, supporting a wider range of formats including.js,.ts,.cjs,.mjs,.cts, and.mts [2]. This allows you to write local plugins in TypeScript or modern JavaScript without requiring manual pre-transpilation in some workflows [2]. 3. Plugin Structure: While npm packages are expected to export the plugin via an app.plugin.js file (or the main field in package.json) [3][4], a local file referenced directly by path is loaded as a module [1]. The exported function from that file will be treated as the config plugin [5][1]. 4. Troubleshooting: If your plugin fails to load, ensure the file exists at the path specified relative to the project root and that it exports the plugin function correctly (usually as the default export or via module.exports) [5][1]. Recent updates have improved error reporting for plugin resolution failures [2].

Citations:


Point the Expo plugin entry at an existing module
Example/expoUsePushy/app.json points to "./plugins/with-rct-bridge-bridging-header", but there’s no matching module under Example/expoUsePushy/plugins/, and the root app.plugin.js won’t be loaded from that entry. Expo resolves local plugins relative to the project root, so prebuild will fail unless the path is corrected or the plugin file is added.

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 7-9: Avoid require with non-literal values
Context: require(require.resolve("@expo/config-plugins", {
paths: [process.cwd()],
}))
Note: [CWE-829] Inclusion of Functionality from Untrusted Control Sphere (dynamic require).

(detect-non-literal-require)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.plugin.js` around lines 4 - 14, The Expo plugin entry is pointing to a
missing local module, so prebuild cannot resolve the plugin. Update the app.json
plugin reference to an existing plugin module or add the expected file under the
project’s plugins directory, and make sure the plugin exports from app.plugin.js
are actually reachable by the configured entry path. Use the app.plugin.js and
withDangerousMod symbols to locate the plugin wiring.


const RCT_BRIDGE_IMPORT = "#import <React/RCTBridge.h>";

function withPushyRCTBridgeImport(config) {
return withDangerousMod(config, [
"ios",
(config) => {
const { platformProjectRoot, projectName } = config.modRequest;
const bridgingHeaderPath = path.join(
platformProjectRoot,
projectName,
`${projectName}-Bridging-Header.h`
);

const contents = fs.readFileSync(bridgingHeaderPath, "utf8");

if (!contents.includes(RCT_BRIDGE_IMPORT)) {
fs.writeFileSync(
bridgingHeaderPath,
`${contents.trimEnd()}\n\n${RCT_BRIDGE_IMPORT}\n`
);
}
Comment on lines +29 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against a missing bridging header before reading.

fs.readFileSync will throw an opaque ENOENT if ${projectName}-Bridging-Header.h doesn't exist (e.g. depending on prebuild ordering or a project without one), failing the entire iOS prebuild. Consider skipping (or creating the file) when it's absent.

🛡️ Proposed guard
+      if (!fs.existsSync(bridgingHeaderPath)) {
+        return config;
+      }
+
       const contents = fs.readFileSync(bridgingHeaderPath, "utf8");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const contents = fs.readFileSync(bridgingHeaderPath, "utf8");
if (!contents.includes(RCT_BRIDGE_IMPORT)) {
fs.writeFileSync(
bridgingHeaderPath,
`${contents.trimEnd()}\n\n${RCT_BRIDGE_IMPORT}\n`
);
}
if (!fs.existsSync(bridgingHeaderPath)) {
return config;
}
const contents = fs.readFileSync(bridgingHeaderPath, "utf8");
if (!contents.includes(RCT_BRIDGE_IMPORT)) {
fs.writeFileSync(
bridgingHeaderPath,
`${contents.trimEnd()}\n\n${RCT_BRIDGE_IMPORT}\n`
);
}
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 31-34: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(
bridgingHeaderPath,
${contents.trimEnd()}\n\n${RCT_BRIDGE_IMPORT}\n
)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.plugin.js` around lines 29 - 36, Guard the bridging header handling in
app.plugin.js so the iOS prebuild does not fail when
`${projectName}-Bridging-Header.h` is missing. In the logic around
`fs.readFileSync(bridgingHeaderPath, "utf8")`, add a check for file existence
first (or create the file if that is the intended behavior) before reading or
writing. Keep the import-injection behavior tied to `RCT_BRIDGE_IMPORT`, but
skip gracefully when the bridging header is absent.


return config;
},
]);
}

module.exports = withPushyRCTBridgeImport;
21 changes: 18 additions & 3 deletions harmony/pushy/src/main/ets/UpdateContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export class UpdateContext {
private static DEBUG: boolean = false;
private static isUsingBundleUrl: boolean = false;
private static ignoreRollback: boolean = false;
private static cachedPackageVersion: string = '';
private static cachedBuildTime: string = '';

constructor(context: common.UIAbilityContext) {
this.context = context;
Expand Down Expand Up @@ -60,28 +62,38 @@ export class UpdateContext {
}

public getPackageVersion(): string {
if (UpdateContext.cachedPackageVersion) {
return UpdateContext.cachedPackageVersion;
}
try {
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
this.getBundleFlags(),
);
return bundleInfo?.versionName || 'Unknown';
UpdateContext.cachedPackageVersion = bundleInfo?.versionName || 'Unknown';
return UpdateContext.cachedPackageVersion;
Comment on lines 68 to +73

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Return an empty package version instead of 'Unknown'.

Line 72 turns a failed/empty versionName lookup into a truthy sentinel, so the new guard at Lines 259-261 will not fire. That still lets syncStateWithBinaryVersion() run with a synthetic binary version and can clear persisted update state on a false mismatch.

Proposed fix
     try {
       const bundleInfo = bundleManager.getBundleInfoForSelfSync(
         this.getBundleFlags(),
       );
-      UpdateContext.cachedPackageVersion = bundleInfo?.versionName || 'Unknown';
+      UpdateContext.cachedPackageVersion = bundleInfo?.versionName || '';
       return UpdateContext.cachedPackageVersion;
     } catch (error) {
       console.error('Failed to get bundle info:', error);
       return '';
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
this.getBundleFlags(),
);
return bundleInfo?.versionName || 'Unknown';
UpdateContext.cachedPackageVersion = bundleInfo?.versionName || 'Unknown';
return UpdateContext.cachedPackageVersion;
try {
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
this.getBundleFlags(),
);
UpdateContext.cachedPackageVersion = bundleInfo?.versionName || '';
return UpdateContext.cachedPackageVersion;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harmony/pushy/src/main/ets/UpdateContext.ts` around lines 68 - 73, The
fallback in UpdateContext.getBundleVersion is using the sentinel string
'Unknown', which prevents the new empty-version guard from detecting a failed
bundle lookup. Change the cached package version assignment so a missing or
empty bundleInfo.versionName resolves to an empty string instead, and keep the
logic in getBundleVersion and syncStateWithBinaryVersion aligned so the latter
only runs when a real binary version is available.

} catch (error) {
console.error('Failed to get bundle info:', error);
return '';
}
}

public getBuildTime(): string {
if (UpdateContext.cachedBuildTime) {
return UpdateContext.cachedBuildTime;
}
try {
const content =
this.context.resourceManager.getRawFileContentSync('meta.json');
const metaData = JSON.parse(
new util.TextDecoder().decodeToString(content),
) as Record<string, string | number | boolean | null | undefined>;
if (metaData.pushy_build_time) {
return String(metaData.pushy_build_time);
UpdateContext.cachedBuildTime = String(metaData.pushy_build_time);
return UpdateContext.cachedBuildTime;
}
} catch {}
} catch (error) {
console.error('Failed to read build time from raw file:', error);
}
return '';
}

Expand Down Expand Up @@ -244,6 +256,9 @@ export class UpdateContext {
packageVersion: string,
buildTime: string,
): void {
if (!packageVersion || !buildTime) {
return;
}
const currentState = this.getStateSnapshot();
const nextState = NativePatchCore.syncStateWithBinaryVersion(
packageVersion,
Expand Down
1 change: 0 additions & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,4 @@ log('bootup status', {
isFirstTimeDebug,
isDebugChannel,
cInfo,
uuid,
});
Loading