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/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" }, () => { diff --git a/packages/host/src/node/cli/apple.ts b/packages/host/src/node/cli/apple.ts index c5eec200..3ff64cef 100644 --- a/packages/host/src/node/cli/apple.ts +++ b/packages/host/src/node/cli/apple.ts @@ -192,6 +192,54 @@ export async function linkFlatFramework({ } } +/** + * 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 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), versionDirectoryPath), + versionCurrentPath, + ); + } + + const { CFBundleExecutable } = await readFrameworkInfo( + path.join(versionCurrentPath, "Resources", "Info.plist"), + ); + + 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)) { + await fs.promises.symlink( + path.relative(path.dirname(libraryLinkPath), libraryRealPath), + libraryLinkPath, + ); + } +} + export async function linkVersionedFramework({ frameworkPath, newLibraryName, @@ -201,6 +249,9 @@ export async function linkVersionedFramework({ "darwin", "Linking Apple addons are only supported on macOS", ); + + await restoreFrameworkLinks(frameworkPath); + const frameworkInfoPath = path.join( frameworkPath, "Versions", 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}`, + }, + ); + } }, ), ); 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": {