From 091ec4b3b702d982485d0cff12b1de95a5ed663d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 28 Oct 2025 13:50:55 +0100 Subject: [PATCH 1/5] Set up files in private packages --- package-lock.json | 2 ++ packages/ferric-example/package.json | 8 +++++++- packages/node-addon-examples/.gitignore | 1 + packages/node-addon-examples/package.json | 10 ++++++++++ packages/node-addon-examples/tests/.gitignore | 1 - packages/node-tests/package.json | 7 +++++++ 6 files changed, 27 insertions(+), 2 deletions(-) delete mode 100644 packages/node-addon-examples/tests/.gitignore diff --git a/package-lock.json b/package-lock.json index ccc7603b..76bde18c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14897,6 +14897,7 @@ }, "packages/node-addon-examples": { "name": "@react-native-node-api/node-addon-examples", + "version": "0.1.0", "dependencies": { "assert": "^2.1.0" }, @@ -14909,6 +14910,7 @@ }, "packages/node-tests": { "name": "@react-native-node-api/node-tests", + "version": "0.1.0", "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", diff --git a/packages/ferric-example/package.json b/packages/ferric-example/package.json index 73678bb3..0695c049 100644 --- a/packages/ferric-example/package.json +++ b/packages/ferric-example/package.json @@ -1,8 +1,8 @@ { "name": "@react-native-node-api/ferric-example", + "version": "0.1.1", "private": true, "type": "commonjs", - "version": "0.1.1", "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { "type": "git", @@ -11,6 +11,12 @@ }, "main": "ferric_example.js", "types": "ferric_example.d.ts", + "files": [ + "ferric_example.js", + "ferric_example.d.ts", + "ferric_example.apple.node", + "ferric_example.android.node" + ], "scripts": { "build": "ferric build", "bootstrap": "node --run build" diff --git a/packages/node-addon-examples/.gitignore b/packages/node-addon-examples/.gitignore index d838da98..7470cb91 100644 --- a/packages/node-addon-examples/.gitignore +++ b/packages/node-addon-examples/.gitignore @@ -1 +1,2 @@ examples/ +build/ diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index db48db3f..010d2f31 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -1,7 +1,17 @@ { "name": "@react-native-node-api/node-addon-examples", + "version": "0.1.0", "type": "commonjs", "main": "dist/index.js", + "files": [ + "dist", + "examples/**/package.json", + "examples/**/*.js", + "tests/**/package.json", + "tests/**/*.js", + "**/*.apple.node/**", + "**/*.android.node/**" + ], "private": true, "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { diff --git a/packages/node-addon-examples/tests/.gitignore b/packages/node-addon-examples/tests/.gitignore deleted file mode 100644 index 378eac25..00000000 --- a/packages/node-addon-examples/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build diff --git a/packages/node-tests/package.json b/packages/node-tests/package.json index eec9e5ed..fd1d497b 100644 --- a/packages/node-tests/package.json +++ b/packages/node-tests/package.json @@ -1,8 +1,15 @@ { "name": "@react-native-node-api/node-tests", + "version": "0.1.0", "description": "Harness for running the Node.js tests from https://github.com/nodejs/node/tree/main/test", "type": "commonjs", "main": "tests.generated.js", + "files": [ + "dist", + "tests/**/*.js", + "**/*.apple.node/**", + "**/*.android.node/**" + ], "private": true, "homepage": "https://github.com/callstackincubator/react-native-node-api", "repository": { From ee229f57b23b2f298c1dd133fb6e2b933c243981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 28 Oct 2025 15:43:58 +0100 Subject: [PATCH 2/5] Reconstruct missing symbolic links if needed --- packages/host/src/node/cli/apple.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index c5eec200..6053bd5f 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -201,6 +201,15 @@ export async function linkVersionedFramework({ "darwin", "Linking Apple addons are only supported on macOS", ); + // Reconstruct missing symbolic links if needed + const versionAPath = path.join(frameworkPath, "Versions", "A"); + const versionCurrentPath = path.join(frameworkPath, "Versions", "Current"); + if (fs.existsSync(versionAPath) && !fs.existsSync(versionCurrentPath)) { + await fs.promises.symlink( + path.relative(path.dirname(versionCurrentPath), versionAPath), + versionCurrentPath, + ); + } const frameworkInfoPath = path.join( frameworkPath, "Versions", @@ -209,6 +218,23 @@ export async function linkVersionedFramework({ "Info.plist", ); const frameworkInfo = await readFrameworkInfo(frameworkInfoPath); + const libraryRealPath = path.join( + frameworkPath, + "Versions", + "Current", + frameworkInfo.CFBundleExecutable, + ); + const libraryLinkPath = path.join( + frameworkPath, + frameworkInfo.CFBundleExecutable, + ); + // Reconstruct missing symbolic links if needed + if (fs.existsSync(libraryRealPath) && !fs.existsSync(libraryLinkPath)) { + await fs.promises.symlink( + path.relative(path.dirname(libraryLinkPath), libraryRealPath), + libraryLinkPath, + ); + } // Update install name await spawn( "install_name_tool", From 0fd21f901e8f44e448bd9983c115c9a53069a939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 28 Oct 2025 17:07:15 +0100 Subject: [PATCH 3/5] Restore weak-node-api symlinks --- packages/host/src/node/cli/apple.ts | 54 ++++++++++++++++----------- packages/host/src/node/cli/program.ts | 36 +++++++++++++++++- 2 files changed, 68 insertions(+), 22 deletions(-) diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index 6053bd5f..b92487db 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -192,15 +192,11 @@ export async function linkFlatFramework({ } } -export async function linkVersionedFramework({ - frameworkPath, - newLibraryName, -}: LinkFrameworkOptions) { - assert.equal( - process.platform, - "darwin", - "Linking Apple addons are only supported on macOS", - ); +/** + * NPM packages aren't preserving internal symlinks inside versioned frameworks. + * This function attempts to restore those. + */ +export async function restoreFrameworkLinks(frameworkPath: string) { // Reconstruct missing symbolic links if needed const versionAPath = path.join(frameworkPath, "Versions", "A"); const versionCurrentPath = path.join(frameworkPath, "Versions", "Current"); @@ -210,24 +206,18 @@ export async function linkVersionedFramework({ versionCurrentPath, ); } - const frameworkInfoPath = path.join( - frameworkPath, - "Versions", - "Current", - "Resources", - "Info.plist", + + const { CFBundleExecutable } = await readFrameworkInfo( + path.join(frameworkPath, "Versions", "Current", "Resources", "Info.plist"), ); - const frameworkInfo = await readFrameworkInfo(frameworkInfoPath); + const libraryRealPath = path.join( frameworkPath, "Versions", "Current", - frameworkInfo.CFBundleExecutable, - ); - const libraryLinkPath = path.join( - frameworkPath, - frameworkInfo.CFBundleExecutable, + CFBundleExecutable, ); + const libraryLinkPath = path.join(frameworkPath, CFBundleExecutable); // Reconstruct missing symbolic links if needed if (fs.existsSync(libraryRealPath) && !fs.existsSync(libraryLinkPath)) { await fs.promises.symlink( @@ -235,6 +225,28 @@ export async function linkVersionedFramework({ libraryLinkPath, ); } +} + +export async function linkVersionedFramework({ + frameworkPath, + newLibraryName, +}: LinkFrameworkOptions) { + assert.equal( + process.platform, + "darwin", + "Linking Apple addons are only supported on macOS", + ); + + await restoreFrameworkLinks(frameworkPath); + + const frameworkInfoPath = path.join( + frameworkPath, + "Versions", + "Current", + "Resources", + "Info.plist", + ); + const frameworkInfo = await readFrameworkInfo(frameworkInfoPath); // Update install name await spawn( "install_name_tool", diff --git a/packages/host/src/node/cli/program.ts b/packages/host/src/node/cli/program.ts index 169ca85b..85b9016f 100644 --- a/packages/host/src/node/cli/program.ts +++ b/packages/host/src/node/cli/program.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import path from "node:path"; import { EventEmitter } from "node:stream"; +import fs from "node:fs"; import { Command, @@ -26,8 +27,9 @@ import { import { command as vendorHermes } from "./hermes"; import { packageNameOption, pathSuffixOption } from "./options"; import { linkModules, pruneLinkedModules, ModuleLinker } from "./link-modules"; -import { linkXcframework } from "./apple"; +import { linkXcframework, restoreFrameworkLinks } from "./apple"; import { linkAndroidDir } from "./android"; +import { weakNodeApiPath } from "../weak-node-api"; // We're attaching a lot of listeners when spawning in parallel EventEmitter.defaultMaxListeners = 100; @@ -169,6 +171,38 @@ program await pruneLinkedModules(platform, modules); } } + + if (apple) { + await oraPromise( + async () => { + const xcframeworkPath = path.join( + weakNodeApiPath, + "weak-node-api.xcframework", + ); + await Promise.all( + [ + path.join(xcframeworkPath, "macos-x86_64"), + path.join(xcframeworkPath, "macos-arm64"), + path.join(xcframeworkPath, "macos-arm64_x86_64"), + ].map(async (slicePath) => { + const frameworkPath = path.join( + slicePath, + "weak-node-api.framework", + ); + if (fs.existsSync(frameworkPath)) { + await restoreFrameworkLinks(frameworkPath); + } + }), + ); + }, + { + text: "Restoring weak-node-api symlinks", + successText: "Restored weak-node-api symlinks", + failText: (error) => + `Failed to restore weak-node-api symlinks: ${error.message}`, + }, + ); + } }, ), ); From ce3f0d2be3281194016551f454991150509a3a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sat, 1 Nov 2025 18:12:12 +0100 Subject: [PATCH 4/5] Improve robustness of restoreFrameworkLinks By not expecting the current version name to be "A" --- packages/host/src/node/cli/apple.ts | 35 ++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index b92487db..3ff64cef 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -198,25 +198,38 @@ export async function linkFlatFramework({ */ export async function restoreFrameworkLinks(frameworkPath: string) { // Reconstruct missing symbolic links if needed - const versionAPath = path.join(frameworkPath, "Versions", "A"); - const versionCurrentPath = path.join(frameworkPath, "Versions", "Current"); - if (fs.existsSync(versionAPath) && !fs.existsSync(versionCurrentPath)) { + const versionsPath = path.join(frameworkPath, "Versions"); + const versionCurrentPath = path.join(versionsPath, "Current"); + + assert( + fs.existsSync(versionsPath), + `Expected "Versions" directory inside versioned framework '${frameworkPath}'`, + ); + + if (!fs.existsSync(versionCurrentPath)) { + const versionDirectoryEntries = await fs.promises.readdir(versionsPath, { + withFileTypes: true, + }); + const versionDirectoryPaths = versionDirectoryEntries + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => path.join(dirent.parentPath, dirent.name)); + assert.equal( + versionDirectoryPaths.length, + 1, + `Expected a single directory in ${versionsPath}, found ${JSON.stringify(versionDirectoryPaths)}`, + ); + const [versionDirectoryPath] = versionDirectoryPaths; await fs.promises.symlink( - path.relative(path.dirname(versionCurrentPath), versionAPath), + path.relative(path.dirname(versionCurrentPath), versionDirectoryPath), versionCurrentPath, ); } const { CFBundleExecutable } = await readFrameworkInfo( - path.join(frameworkPath, "Versions", "Current", "Resources", "Info.plist"), + path.join(versionCurrentPath, "Resources", "Info.plist"), ); - const libraryRealPath = path.join( - frameworkPath, - "Versions", - "Current", - CFBundleExecutable, - ); + const libraryRealPath = path.join(versionCurrentPath, CFBundleExecutable); const libraryLinkPath = path.join(frameworkPath, CFBundleExecutable); // Reconstruct missing symbolic links if needed if (fs.existsSync(libraryRealPath) && !fs.existsSync(libraryLinkPath)) { From 178c7eacb1b67fc640f82a289b85c90eec25ae2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 2 Nov 2025 13:13:20 +0100 Subject: [PATCH 5/5] Add tests for restoreFrameworkLinks --- packages/host/src/node/cli/apple.test.ts | 104 +++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/packages/host/src/node/cli/apple.test.ts b/packages/host/src/node/cli/apple.test.ts index 7914a408..7d32afde 100644 --- a/packages/host/src/node/cli/apple.test.ts +++ b/packages/host/src/node/cli/apple.test.ts @@ -9,6 +9,7 @@ import { readAndParsePlist, readFrameworkInfo, readXcframeworkInfo, + restoreFrameworkLinks, } from "./apple"; import { setupTempDirectory } from "../test-utils"; @@ -267,6 +268,109 @@ describe("apple", { skip: process.platform !== "darwin" }, () => { ); }); }); + + describe("restoreFrameworkLinks", () => { + it("restores a versioned framework", async (context) => { + const infoPlistContents = ` + + + + + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + example-addon + + + `; + + const tempDirectoryPath = setupTempDirectory(context, { + "foo.framework": { + Versions: { + A: { + Resources: { + "Info.plist": infoPlistContents, + }, + "example-addon": "", + }, + }, + }, + }); + + const frameworkPath = path.join(tempDirectoryPath, "foo.framework"); + const currentVersionPath = path.join( + frameworkPath, + "Versions", + "Current", + ); + const binaryLinkPath = path.join(frameworkPath, "example-addon"); + const realBinaryPath = path.join( + frameworkPath, + "Versions", + "A", + "example-addon", + ); + + async function assertVersionedFramework() { + const currentStat = await fs.promises.lstat(currentVersionPath); + assert( + currentStat.isSymbolicLink(), + "Expected Current symlink to be restored", + ); + assert.equal( + await fs.promises.realpath(currentVersionPath), + path.join(frameworkPath, "Versions", "A"), + ); + + const binaryStat = await fs.promises.lstat(binaryLinkPath); + assert( + binaryStat.isSymbolicLink(), + "Expected binary symlink to be restored", + ); + assert.equal( + await fs.promises.realpath(binaryLinkPath), + realBinaryPath, + ); + } + + await restoreFrameworkLinks(frameworkPath); + await assertVersionedFramework(); + + // Calling again to expect a no-op + await restoreFrameworkLinks(frameworkPath); + await assertVersionedFramework(); + }); + + it("throws on a flat framework", async (context) => { + const tempDirectoryPath = setupTempDirectory(context, { + "foo.framework": { + "Info.plist": ` + + + + + CFBundlePackageType + FMWK + CFBundleInfoDictionaryVersion + 6.0 + CFBundleExecutable + example-addon + + + `, + }, + }); + + const frameworkPath = path.join(tempDirectoryPath, "foo.framework"); + + await assert.rejects( + () => restoreFrameworkLinks(frameworkPath), + /Expected "Versions" directory inside versioned framework/, + ); + }); + }); }); describe("apple on non-darwin", { skip: process.platform === "darwin" }, () => {