Skip to content

Commit 6665402

Browse files
authored
Path hashing and babel plugin (#7)
* Rename "host" to "react-native" * Remove metro resolver and bindings polyfill * Derive xcframeworks with path hashes * Add a babel plugin * Add a clean command * Avoid running install_name_tool when building * Add TODOs * Add more examples * Update docs on "how it works" * Assert status of commands * Move cli to a single file * Add tests * Fix tests * Running cli only once per "pod install" and via script_phase * Make CLI pretty and faster * Increase max listeners on stdout and stderr * Always stop the spinner * Incrementally copy and prune stale xcframeworks * Increase max listeners on process
1 parent 3b4bf54 commit 6665402

35 files changed

+1218
-249
lines changed

apps/test-app/babel.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
module.exports = {
22
presets: ['module:@react-native/babel-preset'],
3+
plugins: ['module:react-native-node-api-modules/babel-plugin'],
34
};

apps/test-app/metro.config.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const { makeMetroConfig } = require("@rnx-kit/metro-config");
2-
const { resolveRequest } = require("react-native-node-api-modules/metro-resolver");
32

43
module.exports = makeMetroConfig({
54
transformer: {
@@ -10,7 +9,4 @@ module.exports = makeMetroConfig({
109
},
1110
}),
1211
},
13-
resolver: {
14-
resolveRequest,
15-
}
1612
});

apps/test-app/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"private": true,
44
"version": "0.1.0",
55
"scripts": {
6-
"start": "react-native start",
6+
"start": "react-native start --reset-cache",
77
"android": "react-native run-android",
88
"ios": "react-native run-ios",
99
"pod-install": "cd ios && pod install"

docs/HOW-IT-WORKS.md

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
# How it works
32

43
This document will outline what happens throughout the various parts of the system, when the app calls the `add` method on the library introduced in the ["usage" document](./USAGE.md).
@@ -8,49 +7,50 @@ This document will outline what happens throughout the various parts of the syst
87
Everything starts from the consuming app importing the `calculator-lib`.
98
Metro handles the resolution and the `calculator-lib`'s entrypoint is added to the JavaScript-bundle when bundling.
109

11-
## `calculator-lib` does `require("./prebuild.node")`, the bundler resolves `./prebuild.node` and we generate JavaScript to load it
12-
13-
The library has a require call to a `.node` file, which would normally not have any special meaning, but because the app developer has added the `resolveRequest` function from the `react-native-node-api-modules/metro-config`, the resolution gets intercepted when the app is being bundled by Metro and resolved by the `react-native-node-api-modules` package.
14-
15-
> [!NOTE]
16-
> While this flow is supported through Metro only, we want to generalize and support multiple alternative bundlers too.
10+
## `calculator-lib` does `require("./prebuild.node")` which is transformed into a call into the host TurboModule
1711

18-
The generated code is platform specific and looks something like this:
12+
The library has a require call to a `.node` file, which would normally not have any special meaning:
1913

2014
```javascript
21-
// When resolving with `platform === "ios"`
22-
import { loadModuleOnApple } from "react-native-node-api-modules";
23-
export default loadModuleOnApple({
24-
xcframeworkPath: "../../prebuild.node.xcframework",
25-
frameworkName: "MyAddon.framework",
26-
dylibName: "MyAddon",
27-
});
15+
module.exports = require("./prebuild.node");
2816
```
2917

18+
Since the app developer has added the `react-native-node-api-modules/babel-plugin` to their Babel configuration, the require statement gets transformed when the app is being bundled by Metro, into a `requireNodeAddon` call on our TurboModule.
19+
20+
The generated code looks something like this:
21+
3022
```javascript
31-
// When resolving with `platform === "android"`
32-
import { loadModuleOnAndroid } from "react-native-node-api-modules";
33-
export default loadModuleOnAndroid({
34-
libsPath: "../../prebuild.node.android",
35-
soName: "my-addon.so",
36-
});
23+
module.exports = require("react-native-node-api-modules").requireNodeAddon(
24+
"node-api-2e9fc79b.framework/node-api-2e9fc79b"
25+
);
3726
```
3827

39-
<!-- The exact shape and location of this generated code is TDB -->
28+
> [!NOTE]
29+
> In the time of writing, this code only supports iOS as passes the path to the library with its .framework.
30+
> We plan on generalizing this soon 🤞
31+
32+
### A note on the need for path-hashing
4033

41-
## Generated code calls into `react-native-node-api-modules`, which loads the platform specific dynamic library
34+
Notice that the `requireNodeAddon` call doesn't reference the library by it's original name (`prebuild.node`) but instead a name containing a hash.
4235

43-
The native implementation of `loadModuleOnApple` and `loadModuleOnAndroid` 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.
44-
In any case the native code stores the initialization function in a static data-structure.
36+
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.
37+
38+
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).
39+
40+
## Transformed code calls into `react-native-node-api-modules`, loading the platform specific dynamic library
41+
42+
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.
43+
44+
In any case the native code stores the initialization function in a data-structure.
4545

4646
## `react-native-node-api-modules` creates a `node_env` and initialize the Node-API module
4747

48-
The initialization function of a Node-API module expects a `node_env`.
49-
If we don't have one for the current `jsi::Runtime` already, one is created, by calling `createNodeApiEnv` on the `jsi::Runtime`.
48+
The initialization function of a Node-API module expects a `node_env`, which we create by calling `createNodeApiEnv` on the `jsi::Runtime`.
5049

5150
## The library's C++ code initialize the `exports` object
5251

5352
An `exports` object is created for the Node-API module and both the `napi_env` and `exports` object is passed to the Node-API module's initialization function and the third party code is able to call the Node-API free functions:
53+
5454
- The engine-specific functions (see [js_native_api.h](https://github.com/nodejs/node/blob/main/src/js_native_api.h)) are implemented by the `jsi::Runtime` (currently only Hermes supports this).
5555
- The runtime-specific functions (see [node_api.h](https://github.com/nodejs/node/blob/main/src/node_api.h)) are implemented by `react-native-node-api-modules`.
5656

0 commit comments

Comments
 (0)