From ded6da232545f83482777d09efebaf1042fdf795 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 07:07:42 +0000 Subject: [PATCH 1/2] fix: keep .js/.json precedence over .node and verify prebuild Info.plist Closes #424 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RFTxhqB2yuMZU7AvpM5nvx --- .changeset/quiet-files-resolve.md | 5 ++++ .../host/src/node/babel-plugin/plugin.test.ts | 2 +- packages/host/src/node/path-utils.ts | 7 +++++ packages/node-addon-examples/package.json | 1 + .../scripts/verify-prebuilds.mts | 27 +++++++++++++++++-- pnpm-lock.yaml | 3 +++ 6 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 .changeset/quiet-files-resolve.md diff --git a/.changeset/quiet-files-resolve.md b/.changeset/quiet-files-resolve.md new file mode 100644 index 00000000..6d7bc85f --- /dev/null +++ b/.changeset/quiet-files-resolve.md @@ -0,0 +1,5 @@ +--- +"react-native-node-api": patch +--- + +Fix `require('./addon')` being rewritten to a native addon when `addon.js` or `addon.json` exists next to `addon.node`, matching Node.js resolution order. diff --git a/packages/host/src/node/babel-plugin/plugin.test.ts b/packages/host/src/node/babel-plugin/plugin.test.ts index 7dae2979..d3fe5fac 100644 --- a/packages/host/src/node/babel-plugin/plugin.test.ts +++ b/packages/host/src/node/babel-plugin/plugin.test.ts @@ -129,7 +129,7 @@ describe("plugin", () => { itTransforms("and does not touch required JS files", { files: { "package.json": `{ "name": "my-package" }`, - // TODO: Add a ./my-addon.node to make this test complete + "my-addon.node": "// This is supposed to be a binary file", "my-addon.js": "// Some JS file", "index.js": ` const addon = require('./my-addon'); diff --git a/packages/host/src/node/path-utils.ts b/packages/host/src/node/path-utils.ts index 0cb4506a..c6294b28 100644 --- a/packages/host/src/node/path-utils.ts +++ b/packages/host/src/node/path-utils.ts @@ -66,6 +66,13 @@ const packageNameCache = new Map(); * TODO: Consider checking for a specific platform extension. */ export function isNodeApiModule(modulePath: string): boolean { + // Node resolves `.js` and `.json` before `.node` + if ( + !modulePath.endsWith(".node") && + [".js", ".json"].some((extension) => fs.existsSync(modulePath + extension)) + ) { + return false; + } { // HACK: Take a shortcut (if applicable): existing `.node` files are addons try { diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index acfd70ec..47e9ff76 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -30,6 +30,7 @@ "bootstrap": "node --run copy-and-build" }, "devDependencies": { + "@expo/plist": "0.4.7", "cmake-rn": "workspace:*", "node-addon-examples": "github:nodejs/node-addon-examples#4b7dd86a85644610e6de80154df9acac9329b509", "gyp-to-cmake": "workspace:*", diff --git a/packages/node-addon-examples/scripts/verify-prebuilds.mts b/packages/node-addon-examples/scripts/verify-prebuilds.mts index 94b4e1bb..68f8b49e 100644 --- a/packages/node-addon-examples/scripts/verify-prebuilds.mts +++ b/packages/node-addon-examples/scripts/verify-prebuilds.mts @@ -2,6 +2,8 @@ import fs from "node:fs"; import assert from "node:assert/strict"; import path from "node:path"; +import plist from "@expo/plist"; + import { DIRS } from "./cmake-projects.mjs"; const EXPECTED_ANDROID_ARCHS = ["armeabi-v7a", "arm64-v8a", "x86_64", "x86"]; @@ -37,6 +39,28 @@ async function verifyAndroidPrebuild(dirent: fs.Dirent) { } } +async function verifyInfoPlist(frameworkDir: string) { + const libraryName = path.basename(frameworkDir, ".framework"); + const info: unknown = plist.default.parse( + await fs.promises.readFile(path.join(frameworkDir, "Info.plist"), "utf8"), + ); + assert(typeof info === "object" && info !== null, "Expected a plist dict"); + const CFBundleExecutable: unknown = Reflect.get(info, "CFBundleExecutable"); + const CFBundleName: unknown = Reflect.get(info, "CFBundleName"); + const CFBundleIdentifier: unknown = Reflect.get(info, "CFBundleIdentifier"); + assert.equal(CFBundleExecutable, libraryName); + assert.equal(CFBundleName, libraryName); + assert( + typeof CFBundleIdentifier === "string" && + /^[A-Za-z0-9-.]+$/.test(CFBundleIdentifier), + `Invalid CFBundleIdentifier in ${frameworkDir}: ${String(CFBundleIdentifier)}`, + ); + assert( + fs.existsSync(path.join(frameworkDir, libraryName)), + `Missing executable ${libraryName} in ${frameworkDir}`, + ); +} + async function verifyApplePrebuild(dirent: fs.Dirent) { console.log("Verifying Apple prebuild", dirent.name, "in", dirent.parentPath); for (const arch of EXPECTED_XCFRAMEWORK_PLATFORMS) { @@ -65,8 +89,7 @@ async function verifyApplePrebuild(dirent: fs.Dirent) { "Expected only directory and files in framework", ); if (file.name === "Info.plist") { - // TODO: Verify the contents of the Info.plist file - continue; + await verifyInfoPlist(frameworkDir); } else { assert( !file.name.endsWith(".node"), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9632a67c..13230f65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -281,6 +281,9 @@ importers: specifier: workspace:* version: link:../host devDependencies: + '@expo/plist': + specifier: 0.4.7 + version: 0.4.7 cmake-rn: specifier: workspace:* version: link:../cmake-rn From fd9cfb999254ca1f004c804af1dbc42c3d758374 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 17:54:19 +0000 Subject: [PATCH 2/2] refactor: validate Info.plist with a zod schema Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RFTxhqB2yuMZU7AvpM5nvx --- packages/node-addon-examples/package.json | 3 ++- .../scripts/verify-prebuilds.mts | 26 +++++++++---------- pnpm-lock.yaml | 3 +++ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index 47e9ff76..803fcdbf 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -35,7 +35,8 @@ "node-addon-examples": "github:nodejs/node-addon-examples#4b7dd86a85644610e6de80154df9acac9329b509", "gyp-to-cmake": "workspace:*", "read-pkg": "^9.0.1", - "weak-node-api": "workspace:*" + "weak-node-api": "workspace:*", + "zod": "^4.1.11" }, "dependencies": { "assert": "^2.1.0", diff --git a/packages/node-addon-examples/scripts/verify-prebuilds.mts b/packages/node-addon-examples/scripts/verify-prebuilds.mts index 68f8b49e..6e93bd85 100644 --- a/packages/node-addon-examples/scripts/verify-prebuilds.mts +++ b/packages/node-addon-examples/scripts/verify-prebuilds.mts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import path from "node:path"; import plist from "@expo/plist"; +import * as z from "zod"; import { DIRS } from "./cmake-projects.mjs"; @@ -39,22 +40,21 @@ async function verifyAndroidPrebuild(dirent: fs.Dirent) { } } +const InfoPlistSchema = z.object({ + CFBundleExecutable: z.string(), + CFBundleName: z.string(), + CFBundleIdentifier: z.string().regex(/^[A-Za-z0-9-.]+$/), +}); + async function verifyInfoPlist(frameworkDir: string) { const libraryName = path.basename(frameworkDir, ".framework"); - const info: unknown = plist.default.parse( - await fs.promises.readFile(path.join(frameworkDir, "Info.plist"), "utf8"), - ); - assert(typeof info === "object" && info !== null, "Expected a plist dict"); - const CFBundleExecutable: unknown = Reflect.get(info, "CFBundleExecutable"); - const CFBundleName: unknown = Reflect.get(info, "CFBundleName"); - const CFBundleIdentifier: unknown = Reflect.get(info, "CFBundleIdentifier"); - assert.equal(CFBundleExecutable, libraryName); - assert.equal(CFBundleName, libraryName); - assert( - typeof CFBundleIdentifier === "string" && - /^[A-Za-z0-9-.]+$/.test(CFBundleIdentifier), - `Invalid CFBundleIdentifier in ${frameworkDir}: ${String(CFBundleIdentifier)}`, + const info = InfoPlistSchema.parse( + plist.default.parse( + await fs.promises.readFile(path.join(frameworkDir, "Info.plist"), "utf8"), + ), ); + assert.equal(info.CFBundleExecutable, libraryName); + assert.equal(info.CFBundleName, libraryName); assert( fs.existsSync(path.join(frameworkDir, libraryName)), `Missing executable ${libraryName} in ${frameworkDir}`, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13230f65..87b9f1b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -299,6 +299,9 @@ importers: weak-node-api: specifier: workspace:* version: link:../weak-node-api + zod: + specifier: ^4.1.11 + version: 4.4.3 packages/node-tests: devDependencies: