Skip to content

Commit 5f7edef

Browse files
committed
Merge remote-tracking branch 'upstream/main' into reduce-ios-install-time
2 parents cd15c34 + 3f63ccf commit 5f7edef

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+2045
-718
lines changed

.changeset/rich-donuts-mix.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
"react-native-node-api-test-app": patch
2+
"@react-native-node-api/test-app": patch
33
"react-native-node-api": patch
44
---
55

.github/workflows/check.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,28 @@ jobs:
2424
- uses: actions/setup-node@v4
2525
with:
2626
node-version: lts/jod
27+
# Set up JDK and Android SDK only because we need weak-node-api, to build ferric-example and to run the linting
28+
# TODO: Remove this once we have a way to run linting without building the native code
29+
- name: Set up JDK 17
30+
uses: actions/setup-java@v3
31+
with:
32+
java-version: "17"
33+
distribution: "temurin"
34+
- name: Setup Android SDK
35+
uses: android-actions/setup-android@v3
36+
with:
37+
packages: tools platform-tools ndk;${{ env.NDK_VERSION }}
38+
- run: rustup target add x86_64-linux-android
2739
- run: npm ci
40+
- run: npm run build
41+
# Bootstrap host package to get weak-node-api and ferric-example to get types
42+
# TODO: Solve this by adding an option to ferric to build only types or by committing the types into the repo as a fixture for an "init" command
43+
- run: npm run bootstrap --workspace react-native-node-api
44+
- run: npm run bootstrap --workspace @react-native-node-api/ferric-example
2845
- run: npm run lint
46+
env:
47+
DEBUG: eslint:eslint
48+
- run: npm run prettier:check
2949
unit-tests:
3050
strategy:
3151
fail-fast: false

.prettierignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Ignore artifacts
2+
dist
3+
build
4+
Pods
5+
target
6+
.cxx
7+
8+
# Ignore hermes
9+
packages/host/hermes
10+
packages/node-addon-examples/examples
11+
packages/node-tests/node
12+
packages/node-tests/tests
13+
packages/node-tests/*.generated.js

apps/test-app/App.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,70 @@ 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,
1516
title: string,
16-
fn: (this: Mocha.Suite) => void
17+
fn: (this: Mocha.Suite) => void,
1718
) {
1819
return condition ? describe(title, fn) : describe.skip(title, fn);
1920
}
2021

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", () => {
3336
for (const [suiteName, examples] of Object.entries(
34-
nodeAddonExamplesSuites
37+
nodeAddonExamplesSuites,
3538
)) {
3639
describe(suiteName, () => {
3740
for (const [exampleName, requireExample] of Object.entries(examples)) {
3841
it(exampleName, async () => {
3942
const test = requireExample();
4043
if (test instanceof Function) {
41-
await test();
44+
const result = test();
45+
if (result instanceof Promise) {
46+
await result;
47+
}
4248
}
4349
});
4450
}
4551
});
4652
}
4753
});
4854

55+
describeIf(nodeTests, "Node Tests", () => {
56+
function registerTestSuite(suite: typeof nodeTestsSuites) {
57+
for (const [name, suiteOrTest] of Object.entries(suite)) {
58+
if (typeof suiteOrTest === "function") {
59+
it(name, suiteOrTest);
60+
} else {
61+
describe(name, () => {
62+
registerTestSuite(suiteOrTest);
63+
});
64+
}
65+
}
66+
}
67+
68+
registerTestSuite(nodeTestsSuites);
69+
});
70+
4971
describeIf(ferricExample, "ferric-example", () => {
5072
it("exports a callable sum function", () => {
51-
/* eslint-disable-next-line @typescript-eslint/no-require-imports -- TODO: Determine why a dynamic import doesn't work on Android */
52-
const exampleAddon = require("ferric-example");
73+
const exampleAddon =
74+
/* eslint-disable-next-line @typescript-eslint/no-require-imports -- TODO: Determine why a dynamic import doesn't work on Android */
75+
require("ferric-example") as typeof import("ferric-example");
5376
const result = exampleAddon.sum(1, 3);
5477
if (result !== 4) {
5578
throw new Error(`Expected 1 + 3 to equal 4, but got ${result}`);

apps/test-app/babel.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module.exports = {
2-
presets: ['module:@react-native/babel-preset'],
2+
presets: ["module:@react-native/babel-preset"],
33
// plugins: [['module:react-native-node-api/babel-plugin', { stripPathSuffix: true }]],
4-
plugins: ['module:react-native-node-api/babel-plugin'],
4+
plugins: ["module:react-native-node-api/babel-plugin"],
55
};

apps/test-app/package.json

Lines changed: 4 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 npm run test:android -- ",
1212
"test:android:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples npm run test:android -- ",
13+
"test:android:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests npm run test:android -- ",
1314
"test:android:ferricExample": "MOCHA_REMOTE_CONTEXT=ferricExample npm run 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 npm run test:ios -- ",
1617
"test:ios:nodeAddonExamples": "MOCHA_REMOTE_CONTEXT=nodeAddonExamples npm run test:ios -- ",
18+
"test:ios:nodeTests": "MOCHA_REMOTE_CONTEXT=nodeTests npm run test:ios -- ",
1719
"test:ios:ferricExample": "MOCHA_REMOTE_CONTEXT=ferricExample npm run test:ios -- "
1820
},
1921
"dependencies": {
@@ -23,6 +25,8 @@
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-addon-examples": "*",
29+
"@react-native-node-api/node-tests": "*",
2630
"@react-native/babel-preset": "0.79.0",
2731
"@react-native/metro-config": "0.79.0",
2832
"@react-native/typescript-config": "0.79.0",

apps/test-app/react-native.config.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
const project = (() => {
32
try {
43
const { configureProjects } = require("react-native-test-app");
@@ -25,4 +24,4 @@ const project = (() => {
2524

2625
module.exports = {
2726
...(project ? { project } : undefined),
28-
};
27+
};

docs/ANDROID.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,35 @@
44

55
Because we're using a version of Hermes patched with Node-API support, we need to build React Native from source.
66

7+
Follow [the React Native documentation on how to build from source](https://reactnative.dev/contributing/how-to-build-from-source#update-your-project-to-build-from-source).
8+
9+
In particular, you will have to edit the `android/settings.gradle` file as follows:
10+
11+
> ```diff
12+
> // ...
13+
> include ':app'
14+
> includeBuild('../node_modules/@react-native/gradle-plugin')
15+
>
16+
> + includeBuild('../node_modules/react-native') {
17+
> + dependencySubstitution {
18+
> + substitute(module("com.facebook.react:react-android")).using(project(":packages:react-native:ReactAndroid"))
19+
> + substitute(module("com.facebook.react:react-native")).using(project(":packages:react-native:ReactAndroid"))
20+
> + substitute(module("com.facebook.react:hermes-android")).using(project(":packages:react-native:ReactAndroid:hermes-engine"))
21+
> + substitute(module("com.facebook.react:hermes-engine")).using(project(":packages:react-native:ReactAndroid:hermes-engine"))
22+
> + }
23+
> + }
24+
> ```
25+
26+
To download our custom version of Hermes, you need to run from your app package:
27+
28+
```
29+
npx react-native-node-api vendor-hermes
30+
```
31+
32+
This will print a path which needs to be stored in `REACT_NATIVE_OVERRIDE_HERMES_DIR` to instruct the React Native Gradle scripts to use it.
33+
34+
This can be combined into a single line:
35+
736
```
837
export REACT_NATIVE_OVERRIDE_HERMES_DIR=`npx react-native-node-api vendor-hermes --silent`
938
```

docs/HOW-IT-WORKS.md

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,14 @@ The generated code looks something like this:
2525

2626
```javascript
2727
module.exports = require("react-native-node-api").requireNodeAddon(
28-
"calculator-lib--prebuild"
28+
"calculator-lib--prebuild",
2929
);
3030
```
3131

3232
> [!NOTE]
3333
> In the time of writing, this code only supports iOS as passes the path to the library with its .framework.
3434
> We plan on generalizing this soon 🤞
3535
36-
### A note on the need for path-hashing
37-
38-
Notice that the `requireNodeAddon` call doesn't reference the library by it's original name (`prebuild.node`) but instead a name containing a hash.
39-
40-
In Node.js dynamic libraries sharing names can be disambiguated based off their path on disk. Dynamic libraries added to an iOS application are essentially hoisted and occupy a shared global namespace. This leads to collisions and makes it impossible to disambiguate multiple libraries sharing the same name. We need a way to map a require call, referencing the library by its path relative to the JS file, into a unique name of the library once it's added into the application.
41-
42-
To work around this issue, we scan for and copy any library (including its entire xcframework structure with nested framework directories) from the dependency package into our host package when the app builds and reference these from its podspec (as vendored_frameworks). We use a special file in the xcframeworks containing Node-API modules. To avoid collisions we rename xcframework, framework and library files to a unique name, containing a hash. The hash is computed based off the package-name of the containing package and the relative path from the package root to the library file (with any platform specific file extensions replaced with the neutral ".node" extension).
43-
4436
## Transformed code calls into `react-native-node-api`, loading the platform specific dynamic library
4537

4638
The native implementation of `requireNodeAddon` is responsible for loading the dynamic library and allow the Node-API module to register its initialization function, either by exporting a `napi_register_module_v1` function or by calling the (deprecated) `napi_module_register` function.

docs/USAGE.md

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,15 @@ The app developer has to install both `calculator-lib` as well as `react-native-
1717
The reason for the latter is a current limitation of the React Native Community CLI which doesn't consider transitive dependencies when enumerating packages for auto-linking.
1818

1919
> [!WARNING]
20-
> It's important to match the exact version of the `react-native-node-api` declared as peer dependency by `calculator-lib`.
20+
> It's important to match the version range of the `react-native-node-api` declared as a peer dependency by `calculator-lib`.
2121
22-
For the app to resolve the Node-API dynamic library files, the app developer must update their Metro config to use a `resolveRequest` function exported from `react-native-node-api`:
22+
For the app to resolve the Node-API dynamic library files, the app developer must update their Babel config to use a `requireNodeAddon` function exported from `react-native-node-api`:
2323

2424
```javascript
25-
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
26-
const nodeApi = require("react-native-node-api/metro-config");
27-
module.exports = mergeConfig(getDefaultConfig(__dirname), {
28-
resolver: { resolveRequest: nodeApi.resolveRequest },
29-
});
25+
module.exports = {
26+
presets: ["module:@react-native/babel-preset"],
27+
plugins: ["module:react-native-node-api/babel-plugin"], // 👈 This needs to be added to the babel.config.js of the app
28+
};
3029
```
3130

3231
At some point the app code will import (or require) the entrypoint of `calculator-lib`:
@@ -121,18 +120,36 @@ NAPI_MODULE_INIT(/* napi_env env, napi_value exports */) {
121120
}
122121
```
123122
124-
### Build the prebuilt binaries
123+
```cmake
124+
# CMakeLists.txt
125125
126-
```
127-
npx react-native-node-api build ./addon.c
126+
cmake_minimum_required(VERSION 3.15...3.31)
127+
project(addon)
128+
129+
add_compile_definitions(-DNAPI_VERSION=4)
130+
131+
file(GLOB SOURCE_FILES "addon.c")
132+
133+
add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC})
134+
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
135+
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_JS_INC})
136+
target_link_libraries(${PROJECT_NAME} PRIVATE ${CMAKE_JS_LIB})
137+
target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_17)
138+
139+
if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)
140+
# Generate node.lib
141+
execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS})
142+
endif()
128143
```
129144

130-
This is a shorthand command which generates a CMake project from the single source-file and prebuilds for both the Apple and Android platforms. See the [CLI documentation](./CLI.md) for more information on the options available and [documentation on prebuilds](./PREBUILDS.md) for the specifics on their format and structure.
145+
### Build the prebuilt binaries
131146

132-
<!-- TODO: Add a listing of the files produced when running command: Some temp (cached) CMakeList.txt, the CMake project dir, 2x platform specific prebuild directories -->
147+
```
148+
npx cmake-rn
149+
```
133150

134151
### Load and export the native module
135152

136153
```javascript
137-
module.exports = require("./prebuild.node");
154+
module.exports = require("./build/Release/addon.node");
138155
```

0 commit comments

Comments
 (0)