Skip to content
Merged
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
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion packages/ferric-example/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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"
Expand Down
104 changes: 104 additions & 0 deletions packages/host/src/node/cli/apple.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
readAndParsePlist,
readFrameworkInfo,
readXcframeworkInfo,
restoreFrameworkLinks,
} from "./apple";
import { setupTempDirectory } from "../test-utils";

Expand Down Expand Up @@ -267,6 +268,109 @@ describe("apple", { skip: process.platform !== "darwin" }, () => {
);
});
});

describe("restoreFrameworkLinks", () => {
it("restores a versioned framework", async (context) => {
const infoPlistContents = `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleExecutable</key>
<string>example-addon</string>
</dict>
</plist>
`;

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": `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleExecutable</key>
<string>example-addon</string>
</dict>
</plist>
`,
},
});

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" }, () => {
Expand Down
51 changes: 51 additions & 0 deletions packages/host/src/node/cli/apple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Copy link

Choose a reason for hiding this comment

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

Bug: Dirent Path Access Incompatibility Causes Runtime Errors

The code uses dirent.parentPath to construct a path. This property isn't reliably available on fs.Dirent objects returned by non-recursive fs.readdir, which can lead to runtime errors, especially on older Node.js versions. The versionsPath variable is already in scope for this purpose.

Fix in Cursor Fix in Web

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,
Expand All @@ -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",
Expand Down
36 changes: 35 additions & 1 deletion packages/host/src/node/cli/program.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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}`,
},
);
}
},
),
);
Expand Down
1 change: 1 addition & 0 deletions packages/node-addon-examples/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
examples/
build/
10 changes: 10 additions & 0 deletions packages/node-addon-examples/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
1 change: 0 additions & 1 deletion packages/node-addon-examples/tests/.gitignore

This file was deleted.

7 changes: 7 additions & 0 deletions packages/node-tests/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Loading