@rejaul/react-native-rust 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODE_OF_CONDUCT.md +22 -0
- package/CONTRIBUTING.md +66 -0
- package/LICENSE +7 -0
- package/README.md +223 -0
- package/bin/codegen/constants.js +39 -0
- package/bin/codegen/index.js +9 -0
- package/bin/codegen/project.js +168 -0
- package/bin/codegen/renderers.js +357 -0
- package/bin/codegen/spec.js +156 -0
- package/bin/codegen.js +1 -0
- package/bin/react-native-rust-lib.js +302 -0
- package/bin/react-native-rust.js +82 -0
- package/package.json +35 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our standard
|
|
4
|
+
|
|
5
|
+
Everyone participating in this project is expected to help make its community respectful, constructive, and welcoming to people with different backgrounds and experience levels.
|
|
6
|
+
|
|
7
|
+
Examples of behavior that supports this standard include:
|
|
8
|
+
|
|
9
|
+
- Being considerate and respectful in discussions and reviews.
|
|
10
|
+
- Giving and receiving technical feedback constructively.
|
|
11
|
+
- Focusing criticism on code and ideas, not people.
|
|
12
|
+
- Respecting differing viewpoints and experiences.
|
|
13
|
+
|
|
14
|
+
Unacceptable behavior includes harassment, discriminatory language, personal attacks, and deliberate disruption of project discussions.
|
|
15
|
+
|
|
16
|
+
## Reporting
|
|
17
|
+
|
|
18
|
+
Report conduct concerns to the project maintainers through the repository's contact options: https://github.com/DeveloperRejaul/react-native-rust. Maintainers will review reports and take appropriate action.
|
|
19
|
+
|
|
20
|
+
## Scope
|
|
21
|
+
|
|
22
|
+
This policy applies in project issues, pull requests, discussions, and other spaces used for project participation.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for helping improve `@rejaul/react-native-rust`.
|
|
4
|
+
|
|
5
|
+
## Project scope
|
|
6
|
+
|
|
7
|
+
This repository contains an npm CLI for React Native library authors. It targets the C++ TurboModule template produced by `create-react-native-library` and generates Rust handlers plus C ABI, C++, and TypeScript glue from the library's TypeScript `Spec`.
|
|
8
|
+
|
|
9
|
+
The current supported method types are synchronous `number` (`f64`), `boolean` (`bool`), and `void`. Unsupported signatures should fail with a clear error before generated files are changed. Do not claim support for strings, collections, promises, callbacks, or other unsupported types without implementing and testing the complete native mapping.
|
|
10
|
+
|
|
11
|
+
## Development setup
|
|
12
|
+
|
|
13
|
+
Requirements: Node.js 18 or newer and npm.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install
|
|
17
|
+
npm test
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The test suite uses temporary C++ TurboModule fixtures; it does not require Rust, Xcode, or the Android NDK. To check the publish contents:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm pack --dry-run
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Code generation changes
|
|
27
|
+
|
|
28
|
+
- Treat the TypeScript `Spec` as the source of truth.
|
|
29
|
+
- Keep generated C ABI declarations, C++ methods, and TypeScript wrappers consistent with each other.
|
|
30
|
+
- Preserve user-authored Rust handler bodies when `generate` is run again.
|
|
31
|
+
- Validate all signatures and existing handler compatibility before writing generated files.
|
|
32
|
+
- Add tests for supported type mappings, unsupported types, signature changes, repeat generation, and files that must not be overwritten.
|
|
33
|
+
- Keep generator output deterministic so a second generation with an unchanged `Spec` produces no diff.
|
|
34
|
+
|
|
35
|
+
## Manual platform validation
|
|
36
|
+
|
|
37
|
+
For changes affecting native linking, try the relevant platform build when the toolchain is available.
|
|
38
|
+
|
|
39
|
+
Android requires Rust Android targets, `cargo-ndk`, Android SDK/NDK, and an emulator or device:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
|
|
43
|
+
cargo install cargo-ndk
|
|
44
|
+
react-native-rust build android
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
iOS requires macOS, Xcode, and Rust Apple targets:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios
|
|
51
|
+
react-native-rust build ios
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If a platform build cannot be run, say which prerequisite is unavailable and rely on the fixture tests for the parts they cover.
|
|
55
|
+
|
|
56
|
+
## Pull requests
|
|
57
|
+
|
|
58
|
+
- Keep changes focused and explain the user-visible behavior or generator contract being changed.
|
|
59
|
+
- Include or update tests for behavior changes.
|
|
60
|
+
- Update the README when commands, supported types, platform requirements, or generated outputs change.
|
|
61
|
+
- Run `npm test`, `npm pack --dry-run`, and `git diff --check` before submitting when possible.
|
|
62
|
+
- Do not commit `node_modules`, Rust `target/` output, Android build output, or generated binaries.
|
|
63
|
+
|
|
64
|
+
## Reporting issues
|
|
65
|
+
|
|
66
|
+
For bugs or feature requests, include the CLI command, the relevant TypeScript `Spec` signature, operating system, toolchain versions, and the complete error output. Remove credentials and other private information before posting logs.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, DeveloperRejaul
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# @rejaul/react-native-rust
|
|
2
|
+
|
|
3
|
+
Generate Rust functions and native bindings from a React Native TurboModule TypeScript `Spec`. Write the API in TypeScript, implement its logic in Rust, and use the generated C ABI and C++ TurboModule bridge to call it from React Native.
|
|
4
|
+
|
|
5
|
+
The CLI currently targets the **C++ TurboModule** template created by [`create-react-native-library`](https://www.npmjs.com/package/create-react-native-library).
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Requirements](#requirements)
|
|
10
|
+
- [Create a library](#create-a-library)
|
|
11
|
+
- [Add Rust functions](#add-rust-functions)
|
|
12
|
+
- [Build and run](#build-and-run)
|
|
13
|
+
- [Supported types](#supported-types)
|
|
14
|
+
- [Publish](#publish)
|
|
15
|
+
- [Troubleshooting](#troubleshooting)
|
|
16
|
+
- [Contributing](#contributing)
|
|
17
|
+
- [Code of conduct](#code-of-conduct)
|
|
18
|
+
- [Contact](#contact)
|
|
19
|
+
- [License](#license)
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- Node.js 18 or newer for the CLI. Use a Node.js version supported by `create-react-native-library` when scaffolding a project.
|
|
24
|
+
- Rust stable (`rustup`, `cargo`).
|
|
25
|
+
- Android builds: Android SDK/NDK and `cargo-ndk`.
|
|
26
|
+
- iOS builds: macOS, Xcode command-line tools, and the Rust Apple targets.
|
|
27
|
+
|
|
28
|
+
## Create a library
|
|
29
|
+
|
|
30
|
+
Run from the parent folder where you want the library created:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npx --yes @rejaul/react-native-rust create react-native-awesome-library
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The command runs `create-react-native-library`. In its prompts, select a **TurboModule using C++**. The CLI then initializes Rust from the generated TypeScript `Spec` and configures the C ABI, C++ methods, TypeScript wrappers, iOS CocoaPods linkage, Android CMake linkage, npm scripts, and Rust output ignore rules.
|
|
37
|
+
|
|
38
|
+
Install the generated project's dependencies:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
cd react-native-awesome-library
|
|
42
|
+
yarn install
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To run the CLI from its source checkout before publishing, use the checkout's path from the desired parent folder:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
node /path/to/rust_to_cpp/bin/react-native-rust.js create react-native-awesome-library
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To add Rust support to an existing C++ TurboModule library, install the CLI and initialize it from that library's root:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
npm install --save-dev @rejaul/react-native-rust
|
|
55
|
+
npx react-native-rust init
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`init` refuses unsupported templates and existing `rust/` directories rather than overwriting them.
|
|
59
|
+
|
|
60
|
+
## Add Rust functions
|
|
61
|
+
|
|
62
|
+
The TypeScript `Spec` is the API source of truth. For example, declare a method in `src/NativeAwesomeLibrary.ts`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
export interface Spec extends TurboModule {
|
|
66
|
+
multiply(a: number, b: number): number;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Generate Rust and native glue from the library root:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
yarn rust:generate
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The CLI creates a handler in `rust/src/api/multiply.rs`. Add the implementation there:
|
|
77
|
+
|
|
78
|
+
```rust
|
|
79
|
+
pub fn multiply(a: f64, b: f64) -> f64 {
|
|
80
|
+
a * b
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The generated Rust ABI wrapper, C++ TurboModule method, and TypeScript wrapper connect this function to React Native. Do not hand-edit generated C++ or wrapper code. After changing the `Spec`, run `yarn rust:generate` again. Existing Rust function bodies are preserved, and signature mismatches are reported.
|
|
85
|
+
|
|
86
|
+
Run Rust tests with:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
yarn rust:test
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Build and run
|
|
93
|
+
|
|
94
|
+
Run these commands from the library root. Generate TurboModule Codegen output with `yarn prepare` before building the host app.
|
|
95
|
+
|
|
96
|
+
### Android
|
|
97
|
+
|
|
98
|
+
Install the Rust Android targets and `cargo-ndk` once:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
|
|
102
|
+
cargo install cargo-ndk
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Build the Rust archives and generate native Codegen output:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
yarn rust:build:android
|
|
109
|
+
yarn prepare
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
With an emulator running, start Metro in one terminal and launch Android from another:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
cd example && yarn start --reset-cache
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
cd example && yarn android
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The example command builds only for the connected device's ABI. Rust archives are generated for `arm64-v8a`, `armeabi-v7a`, `x86`, and `x86_64`.
|
|
123
|
+
|
|
124
|
+
### iOS
|
|
125
|
+
|
|
126
|
+
Build the Rust XCFramework and generate native Codegen output:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
yarn rust:build:ios
|
|
130
|
+
yarn prepare
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Install pods for the example app:
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
cd example/ios && pod install
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Start Metro in one terminal and launch an iOS simulator from another:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
cd example && yarn start --reset-cache
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
cd example && yarn ios --simulator "iPhone 17 Pro"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Rust outputs are written under `rust/build/` and included in the generated library's npm package configuration. Build each platform on a machine with its native toolchain.
|
|
150
|
+
|
|
151
|
+
The example screen in `example/src/App.tsx` exercises the supported scalar, JSON, Promise, and callback signatures.
|
|
152
|
+
|
|
153
|
+
## Supported types
|
|
154
|
+
|
|
155
|
+
The generator supports these TypeScript-to-Rust/C++ mappings:
|
|
156
|
+
|
|
157
|
+
| TypeScript | Rust | C++ |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| `number` | `f64` | `double` |
|
|
160
|
+
| `boolean` | `bool` | `bool` |
|
|
161
|
+
| `string` | `String` | `jsi::String` |
|
|
162
|
+
| `T[]`, `Array<T>`, `ReadonlyArray<T>` | `serde_json::Value` | `jsi::Array` |
|
|
163
|
+
| `CodegenTypes.UnsafeObject` | `serde_json::Value` | `jsi::Object` |
|
|
164
|
+
| `void` return | `()` | `void` |
|
|
165
|
+
| `Promise<T>` return | `Result<T, String>` | `jsi::Value` |
|
|
166
|
+
| `(args) => void` parameter | `&mut dyn FnMut(args)` | `jsi::Function` |
|
|
167
|
+
|
|
168
|
+
Arrays and `UnsafeObject` values cross the native boundary as JSON, so their contents must be JSON-safe. Promise results may use supported payload types, but nested Promises are not supported. Callbacks are invoked synchronously during the Rust handler call and may have up to four explicitly typed parameters; they must not be retained or called later. Promise methods cannot accept callbacks.
|
|
169
|
+
|
|
170
|
+
Typed object structs, optional or rest parameters, destructured parameters, generic methods, and overloads are not supported. Unsupported signatures fail before generated files are changed.
|
|
171
|
+
|
|
172
|
+
## Publish
|
|
173
|
+
|
|
174
|
+
Before publishing a generated React Native library, generate its TypeScript/native outputs and build the Rust artifacts for the platforms you intend to ship:
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
yarn prepare
|
|
178
|
+
yarn rust:build:ios
|
|
179
|
+
yarn rust:build:android
|
|
180
|
+
npm pack --dry-run
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
To validate and publish this CLI package:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
npm test
|
|
187
|
+
npm pack --dry-run
|
|
188
|
+
npm publish --access public
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Pushing a version tag such as `v1.0.1` runs [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which tests and publishes the matching package version. Configure npm trusted publishing for this repository and workflow before using the action; it publishes with provenance and does not require an `NPM_TOKEN` secret.
|
|
192
|
+
|
|
193
|
+
## Troubleshooting
|
|
194
|
+
|
|
195
|
+
- `cargo: command not found`: install Rust with `rustup`, then open a new terminal or run `source "$HOME/.cargo/env"`.
|
|
196
|
+
- Android CMake cannot find `android/generated/jni`: run `yarn prepare` from the library root.
|
|
197
|
+
- Android CMake cannot find a Rust `.a` archive: run `yarn rust:build:android` before launching the example.
|
|
198
|
+
- iOS reports a missing Rust XCFramework: run `yarn rust:build:ios`, then `pod install` in `example/ios`.
|
|
199
|
+
- A React Native type is reported as unsupported: check the mappings and signature restrictions in [Supported types](#supported-types).
|
|
200
|
+
|
|
201
|
+
## Contributing
|
|
202
|
+
|
|
203
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing, and contribution guidelines. Bug reports, feature requests, and pull requests are welcome. For code changes, run the CLI test suite and package check before submitting:
|
|
204
|
+
|
|
205
|
+
```sh
|
|
206
|
+
npm test
|
|
207
|
+
npm pack --dry-run
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
When changing type mappings or generated output, include tests for supported signatures, regeneration behavior, and unsupported signatures.
|
|
211
|
+
|
|
212
|
+
## Code of conduct
|
|
213
|
+
|
|
214
|
+
Participation in this project follows the [Code of Conduct](CODE_OF_CONDUCT.md). Please report conduct concerns through the repository's contact options.
|
|
215
|
+
|
|
216
|
+
## Contact
|
|
217
|
+
|
|
218
|
+
- Issues and feature requests: [GitHub Issues](https://github.com/DeveloperRejaul/react-native-rust/issues)
|
|
219
|
+
- Repository and maintainer: [DeveloperRejaul/react-native-rust](https://github.com/DeveloperRejaul/react-native-rust)
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
This project is distributed under the [ISC License](LICENSE).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const ts = require('typescript');
|
|
2
|
+
|
|
3
|
+
const HEADER_START = '// react-native-rust:generated-methods:start';
|
|
4
|
+
const HEADER_END = '// react-native-rust:generated-methods:end';
|
|
5
|
+
const SOURCE_START = '// react-native-rust:generated-methods:start';
|
|
6
|
+
const SOURCE_END = '// react-native-rust:generated-methods:end';
|
|
7
|
+
|
|
8
|
+
const rustKeywords = new Set([
|
|
9
|
+
'as', 'async', 'await', 'break', 'const', 'continue', 'crate', 'dyn', 'else', 'enum', 'extern', 'false',
|
|
10
|
+
'fn', 'for', 'if', 'impl', 'in', 'let', 'loop', 'match', 'mod', 'move', 'mut', 'pub', 'ref', 'return',
|
|
11
|
+
'self', 'Self', 'static', 'struct', 'super', 'trait', 'true', 'type', 'unsafe', 'use', 'where', 'while',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const cppKeywords = new Set([
|
|
15
|
+
'alignas', 'alignof', 'and', 'asm', 'auto', 'bool', 'break', 'case', 'catch', 'char', 'class', 'const',
|
|
16
|
+
'continue', 'default', 'delete', 'do', 'double', 'else', 'enum', 'explicit', 'export', 'extern', 'false',
|
|
17
|
+
'float', 'for', 'friend', 'goto', 'if', 'inline', 'int', 'long', 'namespace', 'new', 'noexcept', 'not',
|
|
18
|
+
'nullptr', 'operator', 'or', 'private', 'protected', 'public', 'register', 'return', 'short', 'signed',
|
|
19
|
+
'sizeof', 'static', 'struct', 'switch', 'template', 'this', 'throw', 'true', 'try', 'typedef', 'typename',
|
|
20
|
+
'union', 'unsigned', 'using', 'virtual', 'void', 'volatile', 'while', 'xor',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const supportedTypes = new Map([
|
|
24
|
+
[ts.SyntaxKind.NumberKeyword, { kind: 'number', typescript: 'number', rust: 'f64', cpp: 'double', defaultValue: '0.0' }],
|
|
25
|
+
[ts.SyntaxKind.BooleanKeyword, { kind: 'boolean', typescript: 'boolean', rust: 'bool', cpp: 'bool', defaultValue: 'false' }],
|
|
26
|
+
[ts.SyntaxKind.StringKeyword, { kind: 'json', typescript: 'string', rust: 'String', cpp: 'jsi::String', defaultValue: 'String::new()' }],
|
|
27
|
+
[ts.SyntaxKind.VoidKeyword, { kind: 'void', typescript: 'void', rust: '()', cpp: 'void', defaultValue: null }],
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
HEADER_START,
|
|
32
|
+
HEADER_END,
|
|
33
|
+
SOURCE_START,
|
|
34
|
+
SOURCE_END,
|
|
35
|
+
rustKeywords,
|
|
36
|
+
cppKeywords,
|
|
37
|
+
supportedTypes,
|
|
38
|
+
ts,
|
|
39
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const {
|
|
4
|
+
HEADER_START,
|
|
5
|
+
HEADER_END,
|
|
6
|
+
SOURCE_START,
|
|
7
|
+
SOURCE_END,
|
|
8
|
+
} = require('./constants');
|
|
9
|
+
const { renderCppMethods, renderRustExports, renderRustHandler, renderRustModuleList, renderWrappers } = require('./renderers');
|
|
10
|
+
|
|
11
|
+
function replaceMarkedRegion(source, startMarker, endMarker, generated) {
|
|
12
|
+
const start = source.indexOf(startMarker);
|
|
13
|
+
const end = source.indexOf(endMarker);
|
|
14
|
+
if (start < 0 || end < start) return null;
|
|
15
|
+
const block = `${startMarker}\n${generated}\n${endMarker}`;
|
|
16
|
+
return `${source.slice(0, start)}${block}${source.slice(end + endMarker.length)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function replaceTemplateMethod(source, markerStart, markerEnd, generated, methodNeedle) {
|
|
20
|
+
const functionIndex = source.indexOf(methodNeedle);
|
|
21
|
+
if (functionIndex < 0) throw new Error(`Could not locate C++ template method "${methodNeedle}".`);
|
|
22
|
+
const lineStart = source.lastIndexOf('\n', functionIndex) + 1;
|
|
23
|
+
const bodyStart = source.indexOf('{', functionIndex);
|
|
24
|
+
if (bodyStart < 0) throw new Error(`Could not locate the body for C++ method "${methodNeedle}".`);
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let bodyEnd = -1;
|
|
27
|
+
for (let index = bodyStart; index < source.length; index += 1) {
|
|
28
|
+
if (source[index] === '{') depth += 1;
|
|
29
|
+
if (source[index] === '}') {
|
|
30
|
+
depth -= 1;
|
|
31
|
+
if (depth === 0) {
|
|
32
|
+
bodyEnd = index + 1;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (bodyEnd < 0) throw new Error(`Could not find the end of C++ method "${methodNeedle}".`);
|
|
38
|
+
const block = `${markerStart}\n${generated}\n${markerEnd}`;
|
|
39
|
+
return `${source.slice(0, lineStart)}${block}${source.slice(bodyEnd)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hasDefaultMultiplyBody(source, moduleName) {
|
|
43
|
+
const methodIndex = source.indexOf(`${moduleName}Impl::multiply(`);
|
|
44
|
+
if (methodIndex < 0) return false;
|
|
45
|
+
const bodyStart = source.indexOf('{', methodIndex);
|
|
46
|
+
if (bodyStart < 0) return false;
|
|
47
|
+
let depth = 0;
|
|
48
|
+
for (let index = bodyStart; index < source.length; index += 1) {
|
|
49
|
+
if (source[index] === '{') depth += 1;
|
|
50
|
+
if (source[index] === '}') {
|
|
51
|
+
depth -= 1;
|
|
52
|
+
if (depth === 0) {
|
|
53
|
+
return source.slice(bodyStart + 1, index).replace(/\s+/g, '') === 'returna*b;';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function updateCppFile(source, markers, generated, templateMethod, methodNeedle, includeRustHeader = false) {
|
|
61
|
+
let updated = source;
|
|
62
|
+
if (includeRustHeader && !updated.includes('../rust/include/rust_api.h')) {
|
|
63
|
+
if (!updated.includes('#pragma once')) throw new Error('Unsupported C++ module header template.');
|
|
64
|
+
updated = updated.replace('#pragma once', '#pragma once\n\n#include "../rust/include/rust_api.h"');
|
|
65
|
+
}
|
|
66
|
+
const marked = replaceMarkedRegion(updated, markers[0], markers[1], generated);
|
|
67
|
+
if (marked !== null) return marked;
|
|
68
|
+
if (updated.includes(markers[0]) || updated.includes(markers[1])) {
|
|
69
|
+
throw new Error('Incomplete react-native-rust generated markers found in C++ source.');
|
|
70
|
+
}
|
|
71
|
+
if (!templateMethod) throw new Error('C++ generated markers are missing; refusing to overwrite the module.');
|
|
72
|
+
if (methodNeedle.endsWith(';')) {
|
|
73
|
+
const count = updated.split(methodNeedle).length - 1;
|
|
74
|
+
if (count !== 1) throw new Error('Could not uniquely locate the C++ template method declaration.');
|
|
75
|
+
return updated.replace(methodNeedle, `${markers[0]}\n${generated}\n${markers[1]}`);
|
|
76
|
+
}
|
|
77
|
+
return replaceTemplateMethod(updated, markers[0], markers[1], generated, methodNeedle);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeSignature(text) {
|
|
81
|
+
return text.replace(/\s+/g, '');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validateExistingHandler(filePath, method) {
|
|
85
|
+
const contents = fs.readFileSync(filePath, 'utf8');
|
|
86
|
+
const signaturePattern = new RegExp(`pub\\s+fn\\s+${method.rustName}\\s*\\(([^)]*)\\)\\s*->\\s*([^\\{]+)\\{`);
|
|
87
|
+
const match = contents.match(signaturePattern);
|
|
88
|
+
const args = method.params.map((param) => {
|
|
89
|
+
if (param.kind !== 'callback') return `${param.name}: ${param.rust}`;
|
|
90
|
+
const callbackTypes = param.params.map((callbackParam) => callbackParam.rust).join(', ');
|
|
91
|
+
return `${param.name}: &mut dyn FnMut(${callbackTypes})`;
|
|
92
|
+
}).join(', ');
|
|
93
|
+
const returnType = method.returnType.promise
|
|
94
|
+
? `Result<${method.returnType.rust}, String>`
|
|
95
|
+
: method.returnType.rust;
|
|
96
|
+
const expected = `${method.rustName}(${args}) -> ${returnType}`;
|
|
97
|
+
const expectedSignature = `pub fn ${expected} {`;
|
|
98
|
+
if (normalizeSignature(contents).includes(normalizeSignature(expectedSignature))) return;
|
|
99
|
+
if (!match || normalizeSignature(match[0].replace(/^pub\s+fn\s+/, '').replace(/\{\s*$/, '')) !== normalizeSignature(expected)) {
|
|
100
|
+
throw new Error(`Rust handler ${path.relative(process.cwd(), filePath)} does not match the TypeScript Spec. Update its signature before regenerating.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderProjectBindings(root, moduleName, methods, initialize) {
|
|
105
|
+
const headerPath = path.join(root, 'cpp', `${moduleName}Impl.h`);
|
|
106
|
+
const sourcePath = path.join(root, 'cpp', `${moduleName}Impl.cpp`);
|
|
107
|
+
const header = fs.readFileSync(headerPath, 'utf8');
|
|
108
|
+
const source = fs.readFileSync(sourcePath, 'utf8');
|
|
109
|
+
if (initialize && !hasDefaultMultiplyBody(source, moduleName)) {
|
|
110
|
+
throw new Error('The C++ multiply demo was customized; move its logic into Rust before running init. No files were changed.');
|
|
111
|
+
}
|
|
112
|
+
const cpp = renderCppMethods(methods, moduleName);
|
|
113
|
+
const generatedIncludes = cpp.source.match(/^#include .+$/gm) || [];
|
|
114
|
+
const cppSource = cpp.source.replace(/^#include .+\n/gm, '');
|
|
115
|
+
const headerTemplateMethod = 'double multiply(jsi::Runtime& rt, double a, double b);';
|
|
116
|
+
const sourceTemplateMethod = `${moduleName}Impl::multiply(`;
|
|
117
|
+
const updatedHeader = updateCppFile(header, [HEADER_START, HEADER_END], cpp.header, initialize, headerTemplateMethod, true);
|
|
118
|
+
let updatedSource = updateCppFile(source, [SOURCE_START, SOURCE_END], cppSource, initialize, sourceTemplateMethod);
|
|
119
|
+
if (generatedIncludes.length > 0) {
|
|
120
|
+
const namespaceIndex = updatedSource.indexOf('\nnamespace facebook::react {');
|
|
121
|
+
if (namespaceIndex < 0) throw new Error('Could not locate the C++ namespace for generated includes.');
|
|
122
|
+
const missingIncludes = generatedIncludes.filter((include) => !updatedSource.includes(include));
|
|
123
|
+
if (missingIncludes.length > 0) {
|
|
124
|
+
const insertionPoint = namespaceIndex + 1;
|
|
125
|
+
updatedSource = `${updatedSource.slice(0, insertionPoint)}${missingIncludes.join('\n')}\n${updatedSource.slice(insertionPoint)}`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const rustDirectory = path.join(root, 'rust', 'src');
|
|
130
|
+
const moduleList = renderRustModuleList(methods);
|
|
131
|
+
const updates = new Map([
|
|
132
|
+
[headerPath, updatedHeader],
|
|
133
|
+
[sourcePath, updatedSource],
|
|
134
|
+
[path.join(rustDirectory, 'lib.rs'), renderRustExports(methods)],
|
|
135
|
+
[path.join(rustDirectory, 'api', 'mod.rs'), moduleList],
|
|
136
|
+
]);
|
|
137
|
+
for (const method of methods) {
|
|
138
|
+
const handlerPath = path.join(rustDirectory, 'api', `${method.rustName}.rs`);
|
|
139
|
+
if (fs.existsSync(handlerPath)) validateExistingHandler(handlerPath, method);
|
|
140
|
+
else updates.set(handlerPath, renderRustHandler(method));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const wrappers = renderWrappers(methods, moduleName);
|
|
144
|
+
const generatedModulePath = path.join(root, 'src', 'rust-generated');
|
|
145
|
+
updates.set(`${generatedModulePath}.tsx`, wrappers.fallback);
|
|
146
|
+
updates.set(`${generatedModulePath}.native.tsx`, wrappers.native);
|
|
147
|
+
const indexPath = path.join(root, 'src', 'index.tsx');
|
|
148
|
+
let index = fs.readFileSync(indexPath, 'utf8');
|
|
149
|
+
for (const method of methods) {
|
|
150
|
+
const oldExport = new RegExp(`^export\\s*\\{\\s*${method.name}\\s*\\}\\s*from\\s*['"]\\./${method.name}['"];?\\s*$`, 'm');
|
|
151
|
+
index = index.replace(oldExport, '');
|
|
152
|
+
}
|
|
153
|
+
if (!index.includes("export * from './rust-generated';")) {
|
|
154
|
+
index = `${index.trimEnd()}\nexport * from './rust-generated';\n`;
|
|
155
|
+
}
|
|
156
|
+
updates.set(indexPath, index);
|
|
157
|
+
return updates;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
replaceMarkedRegion,
|
|
162
|
+
replaceTemplateMethod,
|
|
163
|
+
hasDefaultMultiplyBody,
|
|
164
|
+
updateCppFile,
|
|
165
|
+
normalizeSignature,
|
|
166
|
+
validateExistingHandler,
|
|
167
|
+
renderProjectBindings,
|
|
168
|
+
};
|