Skip to content

Commit 5cb73f5

Browse files
committed
Merge remote-tracking branch 'upstream/main' into replace-package-manager
2 parents 51df6e3 + 256e7a5 commit 5cb73f5

17 files changed

+362
-1
lines changed

apps/test-app/App.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "mocha-remote-react-native";
1010

1111
import { suites as nodeAddonExamplesSuites } from "@react-native-node-api/node-addon-examples";
12+
import { suites as nodeTestsSuites } from "@react-native-node-api/node-tests";
1213

1314
function describeIf(
1415
condition: boolean,
@@ -21,12 +22,14 @@ function describeIf(
2122
type Context = {
2223
allTests?: boolean;
2324
nodeAddonExamples?: boolean;
25+
nodeTests?: boolean;
2426
ferricExample?: boolean;
2527
};
2628

2729
function loadTests({
2830
allTests = false,
2931
nodeAddonExamples = allTests,
32+
nodeTests = allTests,
3033
ferricExample = allTests,
3134
}: Context) {
3235
describeIf(nodeAddonExamples, "Node Addon Examples", () => {
@@ -46,6 +49,22 @@ function loadTests({
4649
}
4750
});
4851

52+
describeIf(nodeTests, "Node Tests", () => {
53+
function registerTestSuite(suite: typeof nodeTestsSuites) {
54+
for (const [name, suiteOrTest] of Object.entries(suite)) {
55+
if (typeof suiteOrTest === "function") {
56+
it(name, suiteOrTest);
57+
} else {
58+
describe(name, () => {
59+
registerTestSuite(suiteOrTest);
60+
});
61+
}
62+
}
63+
}
64+
65+
registerTestSuite(nodeTestsSuites);
66+
});
67+
4968
describeIf(ferricExample, "ferric-example", () => {
5069
it("exports a callable sum function", () => {
5170
/* eslint-disable-next-line @typescript-eslint/no-require-imports -- TODO: Determine why a dynamic import doesn't work on Android */

apps/test-app/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
"test:android": "mocha-remote --exit-on-error -- concurrently --kill-others-on-fail --passthrough-arguments npm:metro 'npm:android -- {@}' --",
1111
"test:android:allTests": "MOCHA_REMOTE_CONTEXT=allTests pnpm test:android -- ",
1212
"test:android:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples pnpm test:android -- ",
13+
"test:android:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests pnpm test:android -- ",
1314
"test:android:ferricExample": "MOCHA_REMOTE_CONTEXT=ferricExample pnpm test:android -- ",
1415
"test:ios": "mocha-remote --exit-on-error -- concurrently --passthrough-arguments --kill-others-on-fail npm:metro 'npm:ios -- {@}' --",
1516
"test:ios:allTests": "MOCHA_REMOTE_CONTEXT=allTests pnpm test:ios -- ",
1617
"test:ios:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples pnpm test:ios -- ",
18+
"test:ios:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests pnpm test:ios -- ",
1719
"test:ios:ferricExample": "MOCHA_REMOTE_CONTEXT=ferricExample pnpm test:ios -- "
1820
},
1921
"dependencies": {
@@ -23,6 +25,7 @@
2325
"@react-native-community/cli": "^18.0.0",
2426
"@react-native-community/cli-platform-android": "^18.0.0",
2527
"@react-native-community/cli-platform-ios": "^18.0.0",
28+
"@react-native-node-api/node-tests": "*",
2629
"@react-native/babel-preset": "0.79.0",
2730
"@react-native/metro-config": "0.79.0",
2831
"@react-native/typescript-config": "0.79.0",

eslint.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export default tseslint.config(
1313
"packages/host/hermes/**",
1414
"packages/node-addon-examples/examples/**",
1515
"packages/ferric-example/ferric_example.d.ts",
16+
"packages/node-tests/node/**",
17+
"packages/node-tests/tests/**",
1618
]),
1719
eslint.configs.recommended,
1820
tseslint.configs.recommended,
@@ -23,6 +25,7 @@ export default tseslint.config(
2325
"packages/node-addon-examples/*.js",
2426
"packages/host/babel-plugin.js",
2527
"packages/host/react-native.config.js",
28+
"packages/node-tests/tests.generated.js",
2629
],
2730
languageOptions: {
2831
parserOptions: {

packages/node-tests/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node/
2+
tests/
3+
4+
tests.generated.js

packages/node-tests/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const buildType = "Release";

packages/node-tests/package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "@react-native-node-api/node-tests",
3+
"description": "Harness for running the Node.js tests from https://github.com/nodejs/node/tree/main/test",
4+
"type": "commonjs",
5+
"main": "tests.generated.js",
6+
"private": true,
7+
"homepage": "https://github.com/callstackincubator/react-native-node-api",
8+
"repository": {
9+
"type": "git",
10+
"url": "git+https://github.com/callstackincubator/react-native-node-api.git",
11+
"directory": "packages/node-tests"
12+
},
13+
"scripts": {
14+
"copy-tests": "tsx scripts/copy-tests.mts",
15+
"gyp-to-cmake": "gyp-to-cmake ./tests",
16+
"build-tests": "tsx scripts/build-tests.mts",
17+
"bundle": "rolldown -c rolldown.config.mts",
18+
"generate-entrypoint": "tsx scripts/generate-entrypoint.mts",
19+
"bootstrap": "node --run copy-tests && node --run gyp-to-cmake && node --run build-tests && node --run bundle && node --run generate-entrypoint"
20+
},
21+
"devDependencies": {
22+
"cmake-rn": "*",
23+
"gyp-to-cmake": "*",
24+
"prebuildify": "^6.0.1",
25+
"react-native-node-api": "^0.3.2",
26+
"read-pkg": "^9.0.1",
27+
"rolldown": "1.0.0-beta.29"
28+
}
29+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
5+
import { defineConfig, type RolldownOptions } from "rolldown";
6+
import { aliasPlugin, replacePlugin } from "rolldown/experimental";
7+
8+
function readGypTargetNames(gypFilePath: string): string[] {
9+
const contents = JSON.parse(fs.readFileSync(gypFilePath, "utf-8")) as unknown;
10+
assert(
11+
typeof contents === "object" && contents !== null,
12+
"Expected gyp file to contain a valid JSON object",
13+
);
14+
assert("targets" in contents, "Expected targets in gyp file");
15+
const { targets } = contents;
16+
assert(Array.isArray(targets), "Expected targets to be an array");
17+
return targets.map(({ target_name }) => {
18+
assert(
19+
typeof target_name === "string",
20+
"Expected target_name to be a string",
21+
);
22+
return target_name;
23+
});
24+
}
25+
26+
function testSuiteConfig(suitePath: string): RolldownOptions[] {
27+
const testFiles = fs.globSync("*.js", {
28+
cwd: suitePath,
29+
exclude: ["*.bundle.js"],
30+
});
31+
const gypFilePath = path.join(suitePath, "binding.gyp");
32+
const targetNames = readGypTargetNames(gypFilePath);
33+
return testFiles.map((testFile) => ({
34+
input: path.join(suitePath, testFile),
35+
output: {
36+
file: path.join(suitePath, path.basename(testFile, ".js") + ".bundle.js"),
37+
},
38+
resolve: {
39+
conditionNames: ["react-native"],
40+
},
41+
polyfillRequire: false,
42+
plugins: [
43+
// Replace dynamic require statements for addon targets to allow the babel plugin to handle them correctly
44+
replacePlugin(
45+
Object.fromEntries(
46+
targetNames.map((targetName) => [
47+
`require(\`./build/\${common.buildType}/${targetName}\`)`,
48+
`require("./build/Release/${targetName}")`,
49+
]),
50+
),
51+
{
52+
delimiters: ["", ""],
53+
},
54+
),
55+
replacePlugin(
56+
Object.fromEntries(
57+
targetNames.map((targetName) => [
58+
// Replace "__require" statement with a regular "require" to allow Metro to resolve it
59+
`__require("./build/Release/${targetName}")`,
60+
`require("./build/Release/${targetName}")`,
61+
]),
62+
),
63+
{
64+
delimiters: ["", ""],
65+
},
66+
),
67+
aliasPlugin({
68+
entries: [
69+
{
70+
find: "../../common",
71+
replacement: "./common.ts",
72+
},
73+
],
74+
}),
75+
],
76+
external: targetNames.map((targetName) => `./build/Release/${targetName}`),
77+
}));
78+
}
79+
80+
const suitePaths = fs
81+
.globSync("tests/*/*", {
82+
cwd: import.meta.dirname,
83+
withFileTypes: true,
84+
})
85+
.filter((dirent) => dirent.isDirectory())
86+
.map((dirent) =>
87+
path.join(
88+
path.relative(import.meta.dirname, dirent.parentPath),
89+
dirent.name,
90+
),
91+
);
92+
93+
export default defineConfig(suitePaths.flatMap(testSuiteConfig));
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import path from "node:path";
2+
import { spawnSync } from "node:child_process";
3+
4+
import { findCMakeProjects } from "./utils.mjs";
5+
6+
const rootPath = path.join(import.meta.dirname, "..");
7+
const projectPaths = findCMakeProjects();
8+
9+
for (const projectPath of projectPaths) {
10+
console.log(
11+
`Running "cmake-rn" in ${path.relative(
12+
rootPath,
13+
projectPath,
14+
)} to build for React Native`,
15+
);
16+
spawnSync("cmake-rn", [], { cwd: projectPath, stdio: "inherit" });
17+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
import cp from "node:child_process";
4+
5+
import { TESTS_DIR } from "./utils.mjs";
6+
7+
const NODE_REPO_URL = "https://github.com/nodejs/node.git";
8+
const NODE_REPO_DIR = path.resolve(import.meta.dirname, "../node");
9+
10+
const ALLOW_LIST = [
11+
"js-native-api/common.h",
12+
"js-native-api/common-inl.h",
13+
"js-native-api/entry_point.h",
14+
"js-native-api/2_function_arguments",
15+
// "node-api/test_async",
16+
// "node-api/test_buffer",
17+
];
18+
19+
console.log("Copying files to", TESTS_DIR);
20+
21+
// Clean up the destination directory before copying
22+
// fs.rmSync(EXAMPLES_DIR, { recursive: true, force: true });
23+
24+
if (!fs.existsSync(NODE_REPO_DIR)) {
25+
console.log(
26+
"Sparse and shallow cloning Node.js repository to",
27+
NODE_REPO_DIR,
28+
);
29+
30+
// Init a new git repository
31+
cp.execFileSync("git", ["init", NODE_REPO_DIR], {
32+
stdio: "inherit",
33+
});
34+
// Set the remote origin to the Node.js repository
35+
cp.execFileSync("git", ["remote", "add", "origin", NODE_REPO_URL], {
36+
stdio: "inherit",
37+
cwd: NODE_REPO_DIR,
38+
});
39+
// Enable sparse checkout
40+
cp.execFileSync(
41+
"git",
42+
["sparse-checkout", "set", "test/js-native-api", "test/node-api"],
43+
{
44+
stdio: "inherit",
45+
cwd: NODE_REPO_DIR,
46+
},
47+
);
48+
// Pull the latest changes from the master branch
49+
console.log("Pulling latest changes from Node.js repository...");
50+
cp.execFileSync("git", ["pull", "--depth=1", "origin", "main"], {
51+
stdio: "inherit",
52+
cwd: NODE_REPO_DIR,
53+
});
54+
}
55+
const SRC_DIR = path.join(NODE_REPO_DIR, "test");
56+
console.log("Copying files from", SRC_DIR);
57+
58+
for (const src of ALLOW_LIST) {
59+
const srcPath = path.join(SRC_DIR, src);
60+
const destPath = path.join(TESTS_DIR, src);
61+
62+
if (fs.existsSync(destPath)) {
63+
console.warn(
64+
`Destination path ${destPath} already exists - skipping copy of ${src}.`,
65+
);
66+
continue;
67+
}
68+
69+
console.log("Copying from", srcPath, "to", destPath);
70+
fs.cpSync(srcPath, destPath, { recursive: true });
71+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import path from "node:path";
4+
5+
const packageRoot = path.join(import.meta.dirname, "..");
6+
const entrypointPath = path.join(packageRoot, "tests.generated.js");
7+
8+
const testPaths = fs.globSync("**/*.bundle.js", {
9+
cwd: path.join(packageRoot, "tests"),
10+
});
11+
12+
interface TestSuite {
13+
[key: string]: string | TestSuite;
14+
}
15+
16+
const suites: TestSuite = {};
17+
18+
for (const testPath of testPaths) {
19+
const paths = testPath.split(path.sep);
20+
const testName = paths.pop();
21+
assert(typeof testName === "string");
22+
let parent: TestSuite = suites;
23+
for (const part of paths) {
24+
if (!parent[part]) {
25+
// Init if missing
26+
parent[part] = {};
27+
}
28+
assert(typeof parent[part] === "object");
29+
parent = parent[part];
30+
}
31+
parent[path.basename(testName, ".bundle.js")] = path.join("tests", testPath);
32+
}
33+
34+
function suiteToString(suite: TestSuite, indent = 1): string {
35+
const padding = " ".repeat(indent);
36+
return Object.entries(suite)
37+
.map(([key, value]) => {
38+
if (typeof value === "string") {
39+
return `${padding}"${key}": () => require("./${value}")`;
40+
} else {
41+
return `${padding}"${key}": {\n${suiteToString(
42+
value,
43+
indent + 1,
44+
)}\n${padding}}`;
45+
}
46+
})
47+
.join(", ");
48+
}
49+
50+
const comment = "Generated by ./scripts/generate-entrypoint.mts";
51+
52+
console.log(
53+
`Writing entrypoint to ${path.relative(
54+
import.meta.dirname,
55+
entrypointPath,
56+
)} for ${testPaths.length} tests ...`,
57+
);
58+
59+
fs.writeFileSync(
60+
entrypointPath,
61+
`/* ${comment} */\nmodule.exports.suites = {\n${suiteToString(suites)}\n};`,
62+
);

0 commit comments

Comments
 (0)