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": {