Skip to content
Closed
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
54 changes: 54 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,60 @@ jobs:
- run: npm run build
- run: npm test --workspace gyp-to-cmake --workspace react-native-node-api-cmake --workspace react-native-node-api-modules

test-linux-addon-examples:
name: Run addon examples on Linux
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/jod
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: "17"
distribution: "temurin"
- name: Setup Android SDK
uses: android-actions/setup-android@v3
# Version here should match the one in React Native template and packages/react-native-node-api-cmake/src/cli.ts
- run: sdkmanager --install "ndk;27.1.12297006"
# Add Android NDK's clang to PATH
- run: echo "$ANDROID_HOME/ndk/27.1.12297006/toolchains/llvm/prebuilt/linux-x86_64/bin" >> $GITHUB_PATH
- run: npm ci
- run: npm run build
- run: npm run copy-node-api-headers --workspace react-native-node-api-modules
- run: npm run build-weak-node-api --workspace react-native-node-api-modules
- run: npm run generate-weak-node-api-injector --workspace react-native-node-api-modules
- run: npm test --workspace react-native-node-addon-examples

test-windows-addon-examples:
name: Run addon examples on Windows
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/jod
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: "17"
distribution: "temurin"
- name: Setup Android SDK
uses: android-actions/setup-android@v3
# Version here should match the one in React Native template and packages/react-native-node-api-cmake/src/cli.ts
- run: sdkmanager --install "ndk;27.1.12297006"
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@v2
with:
version: "18.1"
- run: npm ci
- run: npm run build
- run: npm run copy-node-api-headers --workspace react-native-node-api-modules
- run: npm run build-weak-node-api --workspace react-native-node-api-modules
- run: npm run generate-weak-node-api-injector --workspace react-native-node-api-modules
- run: npm test --workspace react-native-node-addon-examples

test-macos:
name: Run tests which requires MacOS
runs-on: macos-latest
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"clean": "tsc --build --clean",
"dev": "tsc --build --watch",
"lint": "eslint .",
"test": "npm run test --workspace react-native-node-api-modules --workspace react-native-node-api-cmake --workspace gyp-to-cmake --workspace node-addon-examples"
"test": "npm run test --workspace react-native-node-api-modules --workspace react-native-node-api-cmake --workspace gyp-to-cmake --workspace react-native-node-addon-examples"
},
"author": {
"name": "Callstack",
Expand Down
3 changes: 2 additions & 1 deletion packages/node-addon-examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"build": "tsx scripts/build-examples.mts",
"copy-and-build": "npm run copy-examples && npm run gyp-to-cmake && npm run build",
"verify": "tsx scripts/verify-prebuilds.mts",
"test": "npm run copy-and-build && npm run verify"
"test": "tsx --test scripts/*.test.mts scripts/**/*.test.mts",
"test:integration": "npm run copy-and-build && npm run verify"
},
"devDependencies": {
"node-addon-examples": "github:nodejs/node-addon-examples#4213d4c9d07996ae68629c67926251e117f8e52a",
Expand Down
22 changes: 20 additions & 2 deletions packages/node-addon-examples/scripts/build-examples.mts
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import { execSync } from "node:child_process";
import { platform } from "node:os";

import { findCMakeProjects } from "./cmake-projects.mjs";

const projectDirectories = findCMakeProjects();

// Platform-specific build command
let buildCommand: string;
switch (platform()) {
case "darwin":
// macOS: build for both Android and Apple
buildCommand = "react-native-node-api-cmake --android --apple";
break;
case "win32":
case "linux":
// Windows and Linux: only Android
buildCommand = "react-native-node-api-cmake --android";
break;
default:
console.error(`Unsupported platform: ${platform()}`);
process.exit(1);
}

for (const projectDirectory of projectDirectories) {
console.log(`Running "react-native-node-api-cmake" in ${projectDirectory}`);
console.log(`Running "${buildCommand}" in ${projectDirectory}`);
execSync(
"react-native-node-api-cmake --android --apple",
buildCommand,
// "react-native-node-api-cmake --triplet aarch64-linux-android --triplet arm64-apple-ios-sim",
{
cwd: projectDirectory,
Expand Down
3 changes: 2 additions & 1 deletion packages/node-addon-examples/scripts/cmake-projects.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { readdirSync, statSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

export const EXAMPLES_DIR = new URL("../examples", import.meta.url).pathname;
export const EXAMPLES_DIR = fileURLToPath(new URL("../examples", import.meta.url));

export function findCMakeProjects(dir = EXAMPLES_DIR): string[] {
let results: string[] = [];
Expand Down
150 changes: 150 additions & 0 deletions packages/node-addon-examples/scripts/cmake-projects.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import assert from "node:assert/strict";
import { describe, it, TestContext } from "node:test";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";

import { EXAMPLES_DIR, findCMakeProjects } from "./cmake-projects.mjs";

function setupTempDirectory(context: TestContext, files: Record<string, string>) {
const tempDirectoryPath = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), "cmake-projects-test-"))
);

context.after(() => {
fs.rmSync(tempDirectoryPath, { recursive: true, force: true });
});

for (const [filePath, content] of Object.entries(files)) {
const fullPath = path.join(tempDirectoryPath, filePath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content, "utf8");
}

return tempDirectoryPath;
}

describe("EXAMPLES_DIR", () => {
it("should resolve to a valid platform-specific path", () => {
// Check that EXAMPLES_DIR is an absolute path
assert(path.isAbsolute(EXAMPLES_DIR), "EXAMPLES_DIR should be absolute");

// On Windows, should not start with /
if (process.platform === "win32") {
assert(
!EXAMPLES_DIR.startsWith("/"),
"Windows path should not start with /"
);
// Should match Windows path pattern (e.g., C:\... or D:\...)
assert(
/^[A-Za-z]:[\\/]/.test(EXAMPLES_DIR),
"Windows path should start with drive letter"
);
} else {
// On Unix-like systems, should start with /
assert(
EXAMPLES_DIR.startsWith("/"),
"Unix path should start with /"
);
}
});

it("should work correctly with path.join operations", (context) => {
const tempDir = setupTempDirectory(context, {
"test/subdir/file.txt": "test content",
});

// Simulate what happens in copy-examples.mts
const relativePath = "test/subdir";
const joinedPath = path.join(tempDir, relativePath);

// The joined path should exist and be accessible
assert(fs.existsSync(joinedPath), "Joined path should exist");
assert(
fs.statSync(joinedPath).isDirectory(),
"Joined path should be a directory"
);
});

it("should handle URL to path conversion correctly on all platforms", () => {
// Create a test URL similar to how EXAMPLES_DIR is created
const testUrl = new URL("../examples", import.meta.url);
const convertedPath = testUrl.pathname;

// The converted path should work with fs operations
// We can't test the actual EXAMPLES_DIR since it might not exist,
// but we can verify the conversion produces valid paths
if (process.platform === "win32") {
// On Windows, URL.pathname returns /C:/... which is invalid
// Our fix uses fileURLToPath which returns C:\...
assert(
!path.isAbsolute(convertedPath) || convertedPath.startsWith("/"),
"Direct URL.pathname on Windows produces invalid absolute paths"
);
}
});
});

describe("findCMakeProjects", () => {
it("should find CMakeLists.txt files recursively", (context) => {
const tempDir = setupTempDirectory(context, {
"project1/CMakeLists.txt": "# CMake file 1",
"project2/subdir/CMakeLists.txt": "# CMake file 2",
"project3/CMakeLists.txt": "# CMake file 3",
"not-a-project/other.txt": "not cmake",
});

const projects = findCMakeProjects(tempDir);

assert.equal(projects.length, 3, "Should find 3 CMake projects");

// Sort for consistent comparison
const sortedProjects = projects.sort();
const expectedProjects = [
path.join(tempDir, "project1"),
path.join(tempDir, "project2", "subdir"),
path.join(tempDir, "project3"),
].sort();

assert.deepEqual(sortedProjects, expectedProjects);
});

it("should handle empty directories", (context) => {
const tempDir = setupTempDirectory(context, {});
const projects = findCMakeProjects(tempDir);
assert.equal(projects.length, 0, "Should find no projects in empty dir");
});

it("should handle nested CMake projects", (context) => {
const tempDir = setupTempDirectory(context, {
"parent/CMakeLists.txt": "# Parent CMake",
"parent/child/CMakeLists.txt": "# Child CMake",
"parent/child/grandchild/CMakeLists.txt": "# Grandchild CMake",
});

const projects = findCMakeProjects(tempDir);

assert.equal(projects.length, 3, "Should find all nested projects");
assert(
projects.includes(path.join(tempDir, "parent")),
"Should include parent project"
);
assert(
projects.includes(path.join(tempDir, "parent", "child")),
"Should include child project"
);
assert(
projects.includes(path.join(tempDir, "parent", "child", "grandchild")),
"Should include grandchild project"
);
});

it("should work with Windows-style paths", { skip: process.platform !== "win32" }, (context) => {
const tempDir = setupTempDirectory(context, {
"windows\\style\\path\\CMakeLists.txt": "# CMake file",
});

const projects = findCMakeProjects(tempDir);
assert.equal(projects.length, 1, "Should find project with Windows path");
});
});
2 changes: 1 addition & 1 deletion packages/react-native-node-api-modules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"copy-node-api-headers": "tsx scripts/copy-node-api-headers.ts",
"generate-weak-node-api": "tsx scripts/generate-weak-node-api.ts",
"generate-weak-node-api-injector": "tsx scripts/generate-weak-node-api-injector.ts",
"build-weak-node-api": "npm run generate-weak-node-api && react-native-node-api-cmake --android --apple --no-auto-link --no-weak-node-api-linkage --xcframework-extension --source ./weak-node-api",
"build-weak-node-api": "tsx scripts/build-weak-node-api.ts",
"test": "tsx --test src/node/**/*.test.ts src/node/*.test.ts"
},
"keywords": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env tsx

import { execSync } from "node:child_process";
import { platform } from "node:os";

// First generate the weak node API
execSync("npm run generate-weak-node-api", { stdio: "inherit" });

// Build command with common flags
const baseCommand = "react-native-node-api-cmake --no-auto-link --no-weak-node-api-linkage --source ./weak-node-api";

// Platform-specific flags
let platformFlags = "";
switch (platform()) {
case "darwin":
// macOS: build for both Android and Apple
platformFlags = "--android --apple --xcframework-extension";
break;
case "win32":
// Windows: only Android (no Apple/Xcode support)
platformFlags = "--android";
break;
case "linux":
// Linux: only Android
platformFlags = "--android";
break;
default:
console.error(`Unsupported platform: ${platform()}`);
process.exit(1);
}

const fullCommand = `${baseCommand} ${platformFlags}`;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We could extend the CLI to have an --all-supported flag which would include all triplets which are supported by the platform. I think that would make this a bit more DRY, solve the immediate issue and provide a feature for users too.

console.log(`Running: ${fullCommand}`);
execSync(fullCommand, { stdio: "inherit" });
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import cp from "node:child_process";
import { spawnSyncCrossPlatform } from "./platform-utils.js";

import { FunctionDecl, getNodeApiFunctions } from "./node-api-functions";

Expand All @@ -20,6 +20,8 @@ export function generateSource(functions: FunctionDecl[]) {
#define WEAK_NODE_API_LIBRARY_NAME "@rpath/weak-node-api.framework/weak-node-api"
#elif defined(__ANDROID__)
#define WEAK_NODE_API_LIBRARY_NAME "libweak-node-api.so"
#elif defined(_WIN32)
#define WEAK_NODE_API_LIBRARY_NAME "weak-node-api.dll"
#else
#error "WEAK_NODE_API_LIBRARY_NAME cannot be defined for this platform"
#endif
Expand Down Expand Up @@ -58,7 +60,7 @@ async function run() {
const source = generateSource(nodeApiFunctions);
const sourcePath = path.join(CPP_SOURCE_PATH, "WeakNodeApiInjector.cpp");
await fs.promises.writeFile(sourcePath, source, "utf-8");
cp.spawnSync("clang-format", ["-i", sourcePath], { stdio: "inherit" });
spawnSyncCrossPlatform("clang-format", ["-i", sourcePath], { stdio: "inherit" });
}

run().catch((err) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import cp from "node:child_process";
import { spawnSyncCrossPlatform } from "./platform-utils.js";

import { FunctionDecl, getNodeApiFunctions } from "./node-api-functions";

Expand Down Expand Up @@ -74,12 +74,12 @@ async function run() {
const header = generateHeader(nodeApiFunctions);
const headerPath = path.join(WEAK_NODE_API_PATH, "weak_node_api.hpp");
await fs.promises.writeFile(headerPath, header, "utf-8");
cp.spawnSync("clang-format", ["-i", headerPath], { stdio: "inherit" });
spawnSyncCrossPlatform("clang-format", ["-i", headerPath], { stdio: "inherit" });

const source = generateSource(nodeApiFunctions);
const sourcePath = path.join(WEAK_NODE_API_PATH, "weak_node_api.cpp");
await fs.promises.writeFile(sourcePath, source, "utf-8");
cp.spawnSync("clang-format", ["-i", sourcePath], { stdio: "inherit" });
spawnSyncCrossPlatform("clang-format", ["-i", sourcePath], { stdio: "inherit" });
}

run().catch((err) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import path from "node:path";
import cp from "node:child_process";
import { execFileSyncCrossPlatform } from "./platform-utils.js";

import {
type NodeApiVersion,
Expand Down Expand Up @@ -29,7 +29,7 @@ const clangAstDump = z.object({
* @param version
*/
export function getNodeApiHeaderAST(version: NodeApiVersion) {
const output = cp.execFileSync(
const output = execFileSyncCrossPlatform(
"clang",
[
// Declare the Node API version
Expand All @@ -50,7 +50,7 @@ export function getNodeApiHeaderAST(version: NodeApiVersion) {
// Emitting the AST can produce a lot of output
maxBuffer: 1024 * 1024 * 10,
}
);
) as string;
const parsed = JSON.parse(output);
return clangAstDump.parse(parsed);
}
Expand Down
Loading