@rejaul/react-native-rust 1.1.1 → 2.0.0
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/README.md +55 -2
- package/dist/app.js +22 -8
- package/dist/codegen/index.js +6 -2
- package/dist/codegen/project.js +27 -7
- package/dist/codegen/renderers.js +2 -2
- package/dist/dev/buildQueue.js +54 -0
- package/dist/dev/watcher.js +90 -0
- package/dist/metro.js +100 -0
- package/dist/react-native-rust-lib.js +107 -25
- package/dist/react-native-rust.js +14 -0
- package/metro.js +2 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ The generated [`react-native-awesome-library-example`](react-native-awesome-libr
|
|
|
19
19
|
- [Create a library](#create-a-library)
|
|
20
20
|
- [Add Rust functions](#add-rust-functions)
|
|
21
21
|
- [Use Rust in an app](#use-rust-in-an-app)
|
|
22
|
+
- [Automatic development mode](#automatic-development-mode)
|
|
22
23
|
- [Build and run](#build-and-run)
|
|
23
24
|
- [Supported types](#supported-types)
|
|
24
25
|
- [Publish](#publish)
|
|
@@ -109,9 +110,20 @@ npx react-native-rust init --app
|
|
|
109
110
|
npm install
|
|
110
111
|
```
|
|
111
112
|
|
|
112
|
-
The command creates a private local TurboModule package in `
|
|
113
|
+
The command creates a private local TurboModule package in `.rust-native/` (hidden, generated C++/TurboModule glue only — never hand-edit it), and puts the Rust crate itself at the app's own root, `rust/`, alongside `android/` and `ios/`. It adds `.rust-native/` to the app as a local dependency and generates its C++ bridge. It does not publish a separate library. App-local mode currently supports C++ TurboModule apps with both Android and iOS projects.
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
```
|
|
116
|
+
my-app/
|
|
117
|
+
├── rust/ ← edit this: your Rust crate
|
|
118
|
+
│ ├── Cargo.toml
|
|
119
|
+
│ └── src/api/
|
|
120
|
+
├── .rust-native/ ← generated TurboModule glue; do not edit
|
|
121
|
+
├── android/
|
|
122
|
+
├── ios/
|
|
123
|
+
└── package.json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Edit `.rust-native/src/NativeRustApp.ts` to declare methods, then implement their generated handlers under `rust/src/api/`. From the app root, regenerate the bindings and React Native Codegen output with:
|
|
115
127
|
|
|
116
128
|
```sh
|
|
117
129
|
npm run rust:generate
|
|
@@ -134,6 +146,44 @@ npx react-native run-ios
|
|
|
134
146
|
|
|
135
147
|
The generated module is app-local and can be used from the app's JavaScript imports. The root `example/` project in this repository exercises this workflow.
|
|
136
148
|
|
|
149
|
+
## Automatic development mode
|
|
150
|
+
|
|
151
|
+
The commands above (`rust:generate`, `rust:build:*`) still work exactly as before and remain fully supported. For day-to-day development, `react-native-rust watch` runs instead, watching the `Spec` and `rust/` and reacting automatically:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
Legacy workflow: init → generate → build (repeated by hand after every change)
|
|
155
|
+
New workflow: init once → npx react-native-rust watch (or npx react-native start, with Metro integration below)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
On every save, the watcher classifies the change and reacts accordingly, matching what a manual run of `generate`/`build` would already produce:
|
|
159
|
+
|
|
160
|
+
- **A Rust file under `rust/src/`, or `Cargo.toml`/`Cargo.lock`, changes** (an implementation-only edit): it runs an incremental `cargo build` only. Native bindings are not regenerated.
|
|
161
|
+
- **The `Spec` file changes** (a new method, or a signature change): it runs `generate()` first — preserving hand-written Rust handler bodies, exactly like running `rust:generate` by hand — and then `cargo build`.
|
|
162
|
+
- Rapid successive saves are debounced and coalesced into one build; a build already in progress is never interrupted, and at most one more run is queued behind it.
|
|
163
|
+
- A failed build or a failed code generation prints the error and keeps watching; it does not crash the process or leave stale generated files.
|
|
164
|
+
|
|
165
|
+
Run it directly:
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
npx react-native-rust watch
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Or wire it into Metro so `npx react-native start` runs it automatically:
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
// metro.config.js
|
|
175
|
+
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
|
|
176
|
+
const { withRust } = require('@rejaul/react-native-rust/metro');
|
|
177
|
+
|
|
178
|
+
module.exports = withRust(mergeConfig(getDefaultConfig(__dirname), {}));
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`withRust()` detects your Rust project automatically at `rust/` (both library mode and app-local mode keep it at the project root) and adds it to `watchFolders`; pass `{ rustDir: './rust' }` only if your Rust project lives somewhere non-standard. It starts the watcher only while Metro's dev server is actually serving requests — never during a production/release bundle — and its `[Rust]`/`[RN Rust]` output streams into the same terminal as Metro's own. If Rust hasn't been initialized yet, it no-ops with a one-line notice instead of erroring, so it's safe to add before running `init`.
|
|
182
|
+
|
|
183
|
+
If Metro integration isn't wired up (or doesn't fit your setup), run `npx react-native-rust watch` in a separate terminal alongside `npx react-native start` — it does the same thing standalone.
|
|
184
|
+
|
|
185
|
+
**What this does not do:** Rust and C++ are native code. A running Android/iOS app cannot hot-swap a rebuilt native library the way Metro hot-swaps JavaScript. Each cycle rebuilds the Rust crate and prints a reminder that you still need to run `npm run rust:build:ios` / `rust:build:android` and reinstall the app to load the change on a simulator or device. A `Spec` change also regenerates `src/rust-generated.native.tsx`, which Metro's own JS watcher will pick up and Fast Refresh on its own, same as any other JS/TS change.
|
|
186
|
+
|
|
137
187
|
## Build and run
|
|
138
188
|
|
|
139
189
|
Run these commands from the library root. Generate TurboModule Codegen output with `yarn prepare` before building the host app.
|
|
@@ -271,6 +321,9 @@ Pushing a version tag such as `v1.0.2` runs [`.github/workflows/publish.yml`](.g
|
|
|
271
321
|
- `wasm-pack: command not found`: install it with `cargo install wasm-pack`, or run `react-native-rust doctor web` to check both prerequisites.
|
|
272
322
|
- The web app throws "Call and await initRustWeb()...": await `initRustWeb()` once before calling any other generated function; loading the `.wasm` file is asynchronous even though the generated calls are not.
|
|
273
323
|
- A React Native type is reported as unsupported: check the mappings and signature restrictions in [Supported types](#supported-types).
|
|
324
|
+
- `react-native-rust watch` prints "Rust is not initialized": run `react-native-rust init` (or `init --app`) first; the watcher requires `rust/Cargo.toml` to exist.
|
|
325
|
+
- Changes made in a running app never appear, even after "`[Rust] Build completed`": native code cannot hot-swap into a running process. Run `npm run rust:build:ios` / `rust:build:android` and reinstall the app, as [Automatic development mode](#automatic-development-mode) describes.
|
|
326
|
+
- `npx react-native start` doesn't print any `[Rust]` lines: confirm `metro.config.js` calls `withRust(...)` and that `rust/Cargo.toml` exists at the project root; otherwise run `npx react-native-rust watch` in a separate terminal.
|
|
274
327
|
|
|
275
328
|
## Contributing
|
|
276
329
|
|
package/dist/app.js
CHANGED
|
@@ -38,7 +38,7 @@ var import_node_fs = __toESM(require("node:fs"));
|
|
|
38
38
|
var import_node_path = __toESM(require("node:path"));
|
|
39
39
|
var import_node_child_process = require("node:child_process");
|
|
40
40
|
var import_react_native_rust_lib = require("./react-native-rust-lib");
|
|
41
|
-
const moduleDirectory =
|
|
41
|
+
const moduleDirectory = ".rust-native";
|
|
42
42
|
const moduleName = "RustApp";
|
|
43
43
|
const packageName = "rust-app-native-module";
|
|
44
44
|
function writeFiles(root, files) {
|
|
@@ -58,7 +58,7 @@ function scaffoldFiles(appName) {
|
|
|
58
58
|
main: "src/index.tsx",
|
|
59
59
|
"react-native": "src/index.tsx",
|
|
60
60
|
types: "src/index.tsx",
|
|
61
|
-
files: ["src/", "cpp/", "android/", "ios/", "*.podspec", "react-native.config.js"
|
|
61
|
+
files: ["src/", "cpp/", "android/", "ios/", "*.podspec", "react-native.config.js"],
|
|
62
62
|
peerDependencies: { react: "*", "react-native": ">=0.76" },
|
|
63
63
|
codegenConfig: {
|
|
64
64
|
name: `${moduleName}Spec`,
|
|
@@ -67,7 +67,9 @@ function scaffoldFiles(appName) {
|
|
|
67
67
|
outputDir: { ios: "ios/generated", android: "android/generated" },
|
|
68
68
|
android: { javaPackageName },
|
|
69
69
|
includesGeneratedCode: true
|
|
70
|
-
}
|
|
70
|
+
},
|
|
71
|
+
// The Rust crate lives at the app's own root, not inside this generated module package.
|
|
72
|
+
reactNativeRust: { rustDir: "../rust" }
|
|
71
73
|
}, null, 2)}
|
|
72
74
|
`,
|
|
73
75
|
"react-native.config.js": `module.exports = {
|
|
@@ -93,11 +95,13 @@ function scaffoldFiles(appName) {
|
|
|
93
95
|
s.source = { :git => "https://example.invalid" }
|
|
94
96
|
s.platforms = { :ios => min_ios_version_supported }
|
|
95
97
|
s.source_files = "cpp/**/*.{hpp,cpp,c,h}", "ios/**/*.{h,m,mm}", "ios/generated/*.{h,cpp,mm}"
|
|
96
|
-
s.vendored_frameworks = "rust/build/ios/${moduleName}Rust.xcframework"
|
|
97
98
|
install_modules_dependencies(s)
|
|
98
99
|
end
|
|
99
100
|
`,
|
|
100
|
-
|
|
101
|
+
// rust-include, rust-build-android, rust-build-ios: generated by `init`/`build` to reach
|
|
102
|
+
// the Rust crate at the app's own root (see resolveOutsideReference) — a real symlink or, for
|
|
103
|
+
// iOS, a copied build artifact, never hand-authored.
|
|
104
|
+
".gitignore": "node_modules/\nrust-include\nrust-build-android\nrust-build-ios\nrust-build-web-pkg\n",
|
|
101
105
|
"src/NativeRustApp.ts": `import { TurboModuleRegistry, type TurboModule } from 'react-native';
|
|
102
106
|
|
|
103
107
|
export interface Spec extends TurboModule {
|
|
@@ -188,7 +192,8 @@ using namespace facebook::react;
|
|
|
188
192
|
}
|
|
189
193
|
function appModuleRoot(appRoot = process.cwd()) {
|
|
190
194
|
const moduleRoot = import_node_path.default.join(appRoot, moduleDirectory);
|
|
191
|
-
|
|
195
|
+
if (!import_node_fs.default.existsSync(import_node_path.default.join(moduleRoot, "package.json"))) return null;
|
|
196
|
+
return import_node_fs.default.existsSync(import_node_path.default.join(appRoot, "rust", "Cargo.toml")) ? moduleRoot : null;
|
|
192
197
|
}
|
|
193
198
|
function inDirectory(directory, callback) {
|
|
194
199
|
const previousDirectory = process.cwd();
|
|
@@ -250,16 +255,25 @@ function initApp() {
|
|
|
250
255
|
"rust:generate": "react-native-rust generate",
|
|
251
256
|
"rust:build:ios": "react-native-rust build ios",
|
|
252
257
|
"rust:build:android": "react-native-rust build android",
|
|
253
|
-
"rust:test":
|
|
258
|
+
"rust:test": "cargo test --manifest-path rust/Cargo.toml"
|
|
254
259
|
};
|
|
255
260
|
runCodegen(appRoot, moduleRoot);
|
|
256
261
|
import_node_fs.default.writeFileSync(appManifestPath, `${JSON.stringify(appManifest, null, 2)}
|
|
257
262
|
`);
|
|
263
|
+
const gitignorePath = import_node_path.default.join(appRoot, ".gitignore");
|
|
264
|
+
const gitignore = import_node_fs.default.existsSync(gitignorePath) ? import_node_fs.default.readFileSync(gitignorePath, "utf8") : "";
|
|
265
|
+
const ignoredRustOutputs = ["rust/target/", "rust/build/"].filter((entry) => !gitignore.split(/\r?\n/).includes(entry));
|
|
266
|
+
if (ignoredRustOutputs.length > 0) {
|
|
267
|
+
const separator = gitignore.length > 0 && !gitignore.endsWith("\n") ? "\n" : "";
|
|
268
|
+
import_node_fs.default.writeFileSync(gitignorePath, `${gitignore}${separator}${ignoredRustOutputs.join("\n")}
|
|
269
|
+
`);
|
|
270
|
+
}
|
|
258
271
|
} catch (error) {
|
|
259
272
|
import_node_fs.default.rmSync(moduleRoot, { recursive: true, force: true });
|
|
273
|
+
import_node_fs.default.rmSync(import_node_path.default.join(appRoot, "rust"), { recursive: true, force: true });
|
|
260
274
|
throw error;
|
|
261
275
|
}
|
|
262
|
-
console.log(
|
|
276
|
+
console.log("Created app-local Rust TurboModule: edit rust/src/api/, glue lives in .rust-native/ (generated, do not edit).");
|
|
263
277
|
console.log("Install app dependencies, then run `npm run rust:build:android` or `npm run rust:build:ios`.");
|
|
264
278
|
}
|
|
265
279
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/codegen/index.js
CHANGED
|
@@ -20,7 +20,9 @@ var codegen_exports = {};
|
|
|
20
20
|
__export(codegen_exports, {
|
|
21
21
|
parseSpec: () => import_spec.parseSpec,
|
|
22
22
|
renderProjectBindings: () => import_project.renderProjectBindings,
|
|
23
|
-
renderRustFfiModule: () => import_renderers.renderRustFfiModule
|
|
23
|
+
renderRustFfiModule: () => import_renderers.renderRustFfiModule,
|
|
24
|
+
resolveOutsideReference: () => import_project.resolveOutsideReference,
|
|
25
|
+
toPosixRelative: () => import_project.toPosixRelative
|
|
24
26
|
});
|
|
25
27
|
module.exports = __toCommonJS(codegen_exports);
|
|
26
28
|
var import_spec = require("./spec");
|
|
@@ -30,5 +32,7 @@ var import_project = require("./project");
|
|
|
30
32
|
0 && (module.exports = {
|
|
31
33
|
parseSpec,
|
|
32
34
|
renderProjectBindings,
|
|
33
|
-
renderRustFfiModule
|
|
35
|
+
renderRustFfiModule,
|
|
36
|
+
resolveOutsideReference,
|
|
37
|
+
toPosixRelative
|
|
34
38
|
});
|
package/dist/codegen/project.js
CHANGED
|
@@ -33,6 +33,8 @@ __export(project_exports, {
|
|
|
33
33
|
renderProjectBindings: () => renderProjectBindings,
|
|
34
34
|
replaceMarkedRegion: () => replaceMarkedRegion,
|
|
35
35
|
replaceTemplateMethod: () => replaceTemplateMethod,
|
|
36
|
+
resolveOutsideReference: () => resolveOutsideReference,
|
|
37
|
+
toPosixRelative: () => toPosixRelative,
|
|
36
38
|
updateCppFile: () => updateCppFile,
|
|
37
39
|
validateExistingHandler: () => validateExistingHandler
|
|
38
40
|
});
|
|
@@ -41,6 +43,16 @@ var import_node_fs = __toESM(require("node:fs"));
|
|
|
41
43
|
var import_node_path = __toESM(require("node:path"));
|
|
42
44
|
var import_constants = require("./constants");
|
|
43
45
|
var import_renderers = require("./renderers");
|
|
46
|
+
function toPosixRelative(from, to) {
|
|
47
|
+
return import_node_path.default.relative(from, to).split(import_node_path.default.sep).join("/");
|
|
48
|
+
}
|
|
49
|
+
function resolveOutsideReference(root, targetDir, linkName) {
|
|
50
|
+
if (!toPosixRelative(root, targetDir).startsWith("..")) return targetDir;
|
|
51
|
+
const linkPath = import_node_path.default.join(root, linkName);
|
|
52
|
+
import_node_fs.default.rmSync(linkPath, { force: true });
|
|
53
|
+
import_node_fs.default.symlinkSync(toPosixRelative(root, targetDir), linkPath);
|
|
54
|
+
return linkPath;
|
|
55
|
+
}
|
|
44
56
|
function replaceMarkedRegion(source, startMarker, endMarker, generated) {
|
|
45
57
|
const start = source.indexOf(startMarker);
|
|
46
58
|
const end = source.indexOf(endMarker);
|
|
@@ -91,11 +103,13 @@ function hasDefaultMultiplyBody(source, moduleName) {
|
|
|
91
103
|
}
|
|
92
104
|
return false;
|
|
93
105
|
}
|
|
94
|
-
function updateCppFile(source, markers, generated, templateMethod, methodNeedle,
|
|
106
|
+
function updateCppFile(source, markers, generated, templateMethod, methodNeedle, rustHeaderPath) {
|
|
95
107
|
let updated = source;
|
|
96
|
-
if (
|
|
108
|
+
if (rustHeaderPath && !updated.includes(`#include "${rustHeaderPath}"`)) {
|
|
97
109
|
if (!updated.includes("#pragma once")) throw new Error("Unsupported C++ module header template.");
|
|
98
|
-
updated = updated.replace("#pragma once",
|
|
110
|
+
updated = updated.replace("#pragma once", `#pragma once
|
|
111
|
+
|
|
112
|
+
#include "${rustHeaderPath}"`);
|
|
99
113
|
}
|
|
100
114
|
const marked = replaceMarkedRegion(updated, markers[0], markers[1], generated);
|
|
101
115
|
if (marked !== null) return marked;
|
|
@@ -132,7 +146,7 @@ function validateExistingHandler(filePath, method) {
|
|
|
132
146
|
throw new Error(`Rust handler ${import_node_path.default.relative(process.cwd(), filePath)} does not match the TypeScript Spec. Update its signature before regenerating.`);
|
|
133
147
|
}
|
|
134
148
|
}
|
|
135
|
-
function renderProjectBindings(root, moduleName, methods, initialize, crateName) {
|
|
149
|
+
function renderProjectBindings(root, moduleName, methods, initialize, crateName, rustRoot = import_node_path.default.join(root, "rust")) {
|
|
136
150
|
const headerPath = import_node_path.default.join(root, "cpp", `${moduleName}Impl.h`);
|
|
137
151
|
const sourcePath = import_node_path.default.join(root, "cpp", `${moduleName}Impl.cpp`);
|
|
138
152
|
const header = import_node_fs.default.readFileSync(headerPath, "utf8");
|
|
@@ -145,7 +159,9 @@ function renderProjectBindings(root, moduleName, methods, initialize, crateName)
|
|
|
145
159
|
const cppSource = cpp.source.replace(/^#include .+\n/gm, "");
|
|
146
160
|
const headerTemplateMethod = "double multiply(jsi::Runtime& rt, double a, double b);";
|
|
147
161
|
const sourceTemplateMethod = `${moduleName}Impl::multiply(`;
|
|
148
|
-
const
|
|
162
|
+
const rustIncludeTarget = resolveOutsideReference(root, import_node_path.default.join(rustRoot, "include"), "rust-include");
|
|
163
|
+
const rustHeaderPath = `${toPosixRelative(import_node_path.default.join(root, "cpp"), rustIncludeTarget)}/rust_api.h`;
|
|
164
|
+
const updatedHeader = updateCppFile(header, [import_constants.HEADER_START, import_constants.HEADER_END], cpp.header, initialize, headerTemplateMethod, rustHeaderPath);
|
|
149
165
|
let updatedSource = updateCppFile(source, [import_constants.SOURCE_START, import_constants.SOURCE_END], cppSource, initialize, sourceTemplateMethod);
|
|
150
166
|
if (generatedIncludes.length > 0) {
|
|
151
167
|
const namespaceIndex = updatedSource.indexOf("\nnamespace facebook::react {");
|
|
@@ -157,7 +173,7 @@ function renderProjectBindings(root, moduleName, methods, initialize, crateName)
|
|
|
157
173
|
${updatedSource.slice(insertionPoint)}`;
|
|
158
174
|
}
|
|
159
175
|
}
|
|
160
|
-
const rustDirectory = import_node_path.default.join(
|
|
176
|
+
const rustDirectory = import_node_path.default.join(rustRoot, "src");
|
|
161
177
|
const moduleList = (0, import_renderers.renderRustModuleList)(methods);
|
|
162
178
|
const updates = /* @__PURE__ */ new Map([
|
|
163
179
|
[headerPath, updatedHeader],
|
|
@@ -171,7 +187,9 @@ ${updatedSource.slice(insertionPoint)}`;
|
|
|
171
187
|
if (import_node_fs.default.existsSync(handlerPath)) validateExistingHandler(handlerPath, method);
|
|
172
188
|
else updates.set(handlerPath, (0, import_renderers.renderRustHandler)(method));
|
|
173
189
|
}
|
|
174
|
-
const
|
|
190
|
+
const wasmPkgTarget = resolveOutsideReference(root, import_node_path.default.join(rustRoot, "build", "web", "pkg"), "rust-build-web-pkg");
|
|
191
|
+
const wasmPkgPath = toPosixRelative(import_node_path.default.join(root, "src"), wasmPkgTarget);
|
|
192
|
+
const wrappers = (0, import_renderers.renderWrappers)(methods, moduleName, crateName, wasmPkgPath);
|
|
175
193
|
const generatedModulePath = import_node_path.default.join(root, "src", "rust-generated");
|
|
176
194
|
updates.set(`${generatedModulePath}.tsx`, wrappers.web);
|
|
177
195
|
updates.set(`${generatedModulePath}.native.tsx`, wrappers.native);
|
|
@@ -196,6 +214,8 @@ export * from './rust-generated';
|
|
|
196
214
|
renderProjectBindings,
|
|
197
215
|
replaceMarkedRegion,
|
|
198
216
|
replaceTemplateMethod,
|
|
217
|
+
resolveOutsideReference,
|
|
218
|
+
toPosixRelative,
|
|
199
219
|
updateCppFile,
|
|
200
220
|
validateExistingHandler
|
|
201
221
|
});
|
|
@@ -391,7 +391,7 @@ function renderCppMethods(methods, moduleName) {
|
|
|
391
391
|
${definitions.join("\n\n")}`
|
|
392
392
|
};
|
|
393
393
|
}
|
|
394
|
-
function renderWrappers(methods, moduleName, crateName) {
|
|
394
|
+
function renderWrappers(methods, moduleName, crateName, wasmPkgPath = "../rust/build/web/pkg") {
|
|
395
395
|
const nativeLines = [
|
|
396
396
|
`import ${moduleName} from './Native${moduleName}';`,
|
|
397
397
|
"",
|
|
@@ -402,7 +402,7 @@ function renderWrappers(methods, moduleName, crateName) {
|
|
|
402
402
|
""
|
|
403
403
|
];
|
|
404
404
|
const webLines = [
|
|
405
|
-
`import wasmInit, * as rnrsWasm from '
|
|
405
|
+
`import wasmInit, * as rnrsWasm from '${wasmPkgPath}/${crateName}.js';`,
|
|
406
406
|
"",
|
|
407
407
|
"let rnrsWasmReady = false;",
|
|
408
408
|
"let rnrsWasmInit: Promise<void> | null = null;",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var buildQueue_exports = {};
|
|
20
|
+
__export(buildQueue_exports, {
|
|
21
|
+
SingleFlightQueue: () => SingleFlightQueue
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(buildQueue_exports);
|
|
24
|
+
class SingleFlightQueue {
|
|
25
|
+
constructor(task) {
|
|
26
|
+
this.task = task;
|
|
27
|
+
}
|
|
28
|
+
task;
|
|
29
|
+
running = false;
|
|
30
|
+
pending = false;
|
|
31
|
+
/** Requests a run. Coalesced into the in-flight run's follow-up if one is already running. */
|
|
32
|
+
request() {
|
|
33
|
+
if (this.running) {
|
|
34
|
+
this.pending = true;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
void this.run();
|
|
38
|
+
}
|
|
39
|
+
async run() {
|
|
40
|
+
this.running = true;
|
|
41
|
+
do {
|
|
42
|
+
this.pending = false;
|
|
43
|
+
try {
|
|
44
|
+
await this.task();
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
} while (this.pending);
|
|
48
|
+
this.running = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
52
|
+
0 && (module.exports = {
|
|
53
|
+
SingleFlightQueue
|
|
54
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var watcher_exports = {};
|
|
30
|
+
__export(watcher_exports, {
|
|
31
|
+
classifyChange: () => classifyChange,
|
|
32
|
+
watchRustProject: () => watchRustProject
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(watcher_exports);
|
|
35
|
+
var import_node_path = __toESM(require("node:path"));
|
|
36
|
+
var import_chokidar = __toESM(require("chokidar"));
|
|
37
|
+
function classifyChange(filePath, target) {
|
|
38
|
+
const resolved = import_node_path.default.resolve(filePath);
|
|
39
|
+
if (resolved === import_node_path.default.resolve(target.specPath)) return "spec";
|
|
40
|
+
const rustSrc = import_node_path.default.resolve(target.rustRoot, "src");
|
|
41
|
+
const cargoToml = import_node_path.default.resolve(target.rustRoot, "Cargo.toml");
|
|
42
|
+
const cargoLock = import_node_path.default.resolve(target.rustRoot, "Cargo.lock");
|
|
43
|
+
if (resolved === cargoToml || resolved === cargoLock) return "rust";
|
|
44
|
+
if (resolved.startsWith(`${rustSrc}${import_node_path.default.sep}`) && resolved.endsWith(".rs")) return "rust";
|
|
45
|
+
return "other";
|
|
46
|
+
}
|
|
47
|
+
function watchRustProject(options) {
|
|
48
|
+
const { target, onChange, debounceMs = 200, log = console.log } = options;
|
|
49
|
+
const watcher = import_chokidar.default.watch(
|
|
50
|
+
[import_node_path.default.join(target.rustRoot, "**", "*.rs"), import_node_path.default.join(target.rustRoot, "Cargo.toml"), import_node_path.default.join(target.rustRoot, "Cargo.lock"), target.specPath],
|
|
51
|
+
{
|
|
52
|
+
ignored: [
|
|
53
|
+
import_node_path.default.join(target.rustRoot, "target", "**"),
|
|
54
|
+
import_node_path.default.join(target.rustRoot, "build", "**"),
|
|
55
|
+
import_node_path.default.join(target.rustRoot, "include", "**"),
|
|
56
|
+
"**/node_modules/**",
|
|
57
|
+
"**/.git/**"
|
|
58
|
+
],
|
|
59
|
+
ignoreInitial: true,
|
|
60
|
+
// Waits for a file's size to stop changing before firing, so editors that write in
|
|
61
|
+
// multiple chunks (atomic saves, rename-into-place) produce one event, not several.
|
|
62
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 20 }
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
let pendingKinds = /* @__PURE__ */ new Set();
|
|
66
|
+
let timer = null;
|
|
67
|
+
const schedule = (filePath, eventLabel) => {
|
|
68
|
+
const kind = classifyChange(filePath, target);
|
|
69
|
+
if (kind === "other") return;
|
|
70
|
+
pendingKinds.add(kind);
|
|
71
|
+
log(`[Rust] ${eventLabel} ${import_node_path.default.relative(import_node_path.default.dirname(target.rustRoot), filePath)}`);
|
|
72
|
+
if (timer) clearTimeout(timer);
|
|
73
|
+
timer = setTimeout(() => {
|
|
74
|
+
const kinds = pendingKinds;
|
|
75
|
+
pendingKinds = /* @__PURE__ */ new Set();
|
|
76
|
+
timer = null;
|
|
77
|
+
onChange(kinds);
|
|
78
|
+
}, debounceMs);
|
|
79
|
+
};
|
|
80
|
+
watcher.on("add", (filePath) => schedule(filePath, "Added"));
|
|
81
|
+
watcher.on("change", (filePath) => schedule(filePath, "Changed"));
|
|
82
|
+
watcher.on("unlink", (filePath) => schedule(filePath, "Deleted"));
|
|
83
|
+
watcher.on("error", (error) => log(`[Rust] Watcher error: ${error.message}`));
|
|
84
|
+
return watcher;
|
|
85
|
+
}
|
|
86
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
87
|
+
0 && (module.exports = {
|
|
88
|
+
classifyChange,
|
|
89
|
+
watchRustProject
|
|
90
|
+
});
|
package/dist/metro.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var metro_exports = {};
|
|
30
|
+
__export(metro_exports, {
|
|
31
|
+
findRustDir: () => findRustDir,
|
|
32
|
+
stopRustWatcher: () => stopRustWatcher,
|
|
33
|
+
withRust: () => withRust
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(metro_exports);
|
|
36
|
+
var import_node_fs = __toESM(require("node:fs"));
|
|
37
|
+
var import_node_path = __toESM(require("node:path"));
|
|
38
|
+
var import_node_child_process = require("node:child_process");
|
|
39
|
+
let watcherProcess = null;
|
|
40
|
+
function findRustDir(projectRoot, rustDir) {
|
|
41
|
+
const resolved = import_node_path.default.resolve(projectRoot, rustDir || "rust");
|
|
42
|
+
return import_node_fs.default.existsSync(import_node_path.default.join(resolved, "Cargo.toml")) ? resolved : null;
|
|
43
|
+
}
|
|
44
|
+
function ensureWatcherRunning(projectRoot, notify) {
|
|
45
|
+
if (watcherProcess) return;
|
|
46
|
+
const cliEntry = import_node_path.default.join(__dirname, "react-native-rust.js");
|
|
47
|
+
const child = (0, import_node_child_process.spawn)(process.execPath, [cliEntry, "watch"], { cwd: projectRoot });
|
|
48
|
+
watcherProcess = child;
|
|
49
|
+
child.stdout?.on("data", (chunk) => process.stdout.write(chunk));
|
|
50
|
+
child.stderr?.on("data", (chunk) => process.stderr.write(chunk));
|
|
51
|
+
child.on("exit", (code) => {
|
|
52
|
+
watcherProcess = null;
|
|
53
|
+
if (code !== 0 && code !== null) {
|
|
54
|
+
notify(`[RN Rust] The Rust watcher exited unexpectedly (code ${code}). Restart Metro to retry, or run \`npx react-native-rust watch\` manually.`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const stop = () => {
|
|
58
|
+
if (watcherProcess) watcherProcess.kill("SIGINT");
|
|
59
|
+
};
|
|
60
|
+
process.once("exit", stop);
|
|
61
|
+
process.once("SIGINT", stop);
|
|
62
|
+
process.once("SIGTERM", stop);
|
|
63
|
+
}
|
|
64
|
+
function stopRustWatcher() {
|
|
65
|
+
if (!watcherProcess) return Promise.resolve();
|
|
66
|
+
const child = watcherProcess;
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
child.once("exit", () => resolve());
|
|
69
|
+
child.kill("SIGINT");
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function withRust(config, options = {}) {
|
|
73
|
+
const projectRoot = config.projectRoot || process.cwd();
|
|
74
|
+
const rustDir = findRustDir(projectRoot, options.rustDir);
|
|
75
|
+
const existingWatchFolders = config.watchFolders || [];
|
|
76
|
+
const watchFolders = rustDir && !existingWatchFolders.includes(rustDir) ? [...existingWatchFolders, rustDir] : existingWatchFolders;
|
|
77
|
+
const previousEnhanceMiddleware = config.server?.enhanceMiddleware;
|
|
78
|
+
return {
|
|
79
|
+
...config,
|
|
80
|
+
watchFolders,
|
|
81
|
+
server: {
|
|
82
|
+
...config.server,
|
|
83
|
+
enhanceMiddleware: (middleware, server) => {
|
|
84
|
+
const next = previousEnhanceMiddleware ? previousEnhanceMiddleware(middleware, server) : middleware;
|
|
85
|
+
if (rustDir) {
|
|
86
|
+
ensureWatcherRunning(projectRoot, console.log);
|
|
87
|
+
} else {
|
|
88
|
+
console.log("[RN Rust] No rust/Cargo.toml found; skipping the automatic Rust watcher. Run `npx react-native-rust init` (or `init --app`) to add Rust support.");
|
|
89
|
+
}
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
96
|
+
0 && (module.exports = {
|
|
97
|
+
findRustDir,
|
|
98
|
+
stopRustWatcher,
|
|
99
|
+
withRust
|
|
100
|
+
});
|
|
@@ -34,13 +34,16 @@ __export(react_native_rust_lib_exports, {
|
|
|
34
34
|
init: () => init,
|
|
35
35
|
packageRoot: () => packageRoot,
|
|
36
36
|
readJson: () => readJson,
|
|
37
|
-
run: () => run
|
|
37
|
+
run: () => run,
|
|
38
|
+
watch: () => watch
|
|
38
39
|
});
|
|
39
40
|
module.exports = __toCommonJS(react_native_rust_lib_exports);
|
|
40
41
|
var import_node_fs = __toESM(require("node:fs"));
|
|
41
42
|
var import_node_path = __toESM(require("node:path"));
|
|
42
43
|
var import_node_child_process = require("node:child_process");
|
|
43
44
|
var import_codegen = require("./codegen");
|
|
45
|
+
var import_watcher = require("./dev/watcher");
|
|
46
|
+
var import_buildQueue = require("./dev/buildQueue");
|
|
44
47
|
const rustDirectory = "rust";
|
|
45
48
|
const iosTargets = ["aarch64-apple-ios", "aarch64-apple-ios-sim", "x86_64-apple-ios"];
|
|
46
49
|
const androidTargets = [
|
|
@@ -93,6 +96,9 @@ function packageRoot() {
|
|
|
93
96
|
}
|
|
94
97
|
return { root, manifest, manifestPath, moduleName: moduleMatch[1] };
|
|
95
98
|
}
|
|
99
|
+
function resolveRustRoot(root, manifest) {
|
|
100
|
+
return import_node_path.default.resolve(root, manifest.reactNativeRust?.rustDir || rustDirectory);
|
|
101
|
+
}
|
|
96
102
|
function readCrateName(rustRoot) {
|
|
97
103
|
const manifestPath = import_node_path.default.join(rustRoot, "Cargo.toml");
|
|
98
104
|
const cargoManifest = import_node_fs.default.readFileSync(manifestPath, "utf8");
|
|
@@ -102,9 +108,9 @@ function readCrateName(rustRoot) {
|
|
|
102
108
|
}
|
|
103
109
|
function init() {
|
|
104
110
|
const { root, manifest, manifestPath, moduleName } = packageRoot();
|
|
105
|
-
const rustPath =
|
|
111
|
+
const rustPath = resolveRustRoot(root, manifest);
|
|
106
112
|
if (import_node_fs.default.existsSync(rustPath)) {
|
|
107
|
-
throw new Error("
|
|
113
|
+
throw new Error(`${import_node_path.default.relative(root, rustPath) || "rust"}/ already exists; no files were changed.`);
|
|
108
114
|
}
|
|
109
115
|
const scripts = manifest.scripts || {};
|
|
110
116
|
if (scripts["rust:test"] || scripts["rust:generate"]) {
|
|
@@ -145,19 +151,22 @@ function init() {
|
|
|
145
151
|
dl
|
|
146
152
|
m
|
|
147
153
|
)`;
|
|
154
|
+
const androidRustBuildTarget = (0, import_codegen.resolveOutsideReference)(root, import_node_path.default.join(rustPath, "build", "android"), "rust-build-android");
|
|
155
|
+
const androidRustBuildPath = (0, import_codegen.toPosixRelative)(import_node_path.default.join(root, "android"), androidRustBuildTarget);
|
|
148
156
|
const rustImport = `add_library(rust_core STATIC IMPORTED)
|
|
149
157
|
set_target_properties(rust_core PROPERTIES
|
|
150
|
-
IMPORTED_LOCATION "\${CMAKE_CURRENT_LIST_DIR}
|
|
158
|
+
IMPORTED_LOCATION "\${CMAKE_CURRENT_LIST_DIR}/${androidRustBuildPath}/\${ANDROID_ABI}/lib${crateDirName}.a"
|
|
151
159
|
)
|
|
152
160
|
|
|
153
161
|
`;
|
|
154
162
|
const androidCmake = contents.androidCmake.replace(linkMatch[0], androidLink).replace(/add_library\(/, `${rustImport}add_library(`);
|
|
155
163
|
const podspecSource = contents.podspec.match(/^\s*s\.source_files\s*=.*$/m);
|
|
156
164
|
if (!podspecSource) throw new Error("Could not locate source_files in the CocoaPods spec.");
|
|
165
|
+
const iosRustBuildPath = (0, import_codegen.toPosixRelative)(root, import_node_path.default.join(rustPath, "build", "ios")).startsWith("..") ? "rust-build-ios" : (0, import_codegen.toPosixRelative)(root, import_node_path.default.join(rustPath, "build", "ios"));
|
|
157
166
|
const podspec = contents.podspec.replace(
|
|
158
167
|
podspecSource[0],
|
|
159
168
|
`${podspecSource[0]}
|
|
160
|
-
s.vendored_frameworks = "
|
|
169
|
+
s.vendored_frameworks = "${iosRustBuildPath}/${moduleName}Rust.xcframework"`
|
|
161
170
|
);
|
|
162
171
|
const rustFiles = {
|
|
163
172
|
"Cargo.toml": `[package]
|
|
@@ -200,7 +209,7 @@ Rust handlers are generated from the TurboModule Spec interface in src/Native${m
|
|
|
200
209
|
Run cargo test --manifest-path rust/Cargo.toml for Rust tests. Build iOS and Android artifacts with npx react-native-rust build ios and npx react-native-rust build android before native app builds. Build the react-native-web WebAssembly package with npx react-native-rust build web (requires wasm-pack and the wasm32-unknown-unknown Rust target).
|
|
201
210
|
`
|
|
202
211
|
};
|
|
203
|
-
const updates = (0, import_codegen.renderProjectBindings)(root, moduleName, methods, true, crateDirName);
|
|
212
|
+
const updates = (0, import_codegen.renderProjectBindings)(root, moduleName, methods, true, crateDirName, rustPath);
|
|
204
213
|
updates.set(projectFiles.androidCmake, androidCmake);
|
|
205
214
|
updates.set(projectFiles.podspec, podspec);
|
|
206
215
|
const cliManifest = readJson(import_node_path.default.join(__dirname, "..", "package.json"));
|
|
@@ -215,27 +224,32 @@ Run cargo test --manifest-path rust/Cargo.toml for Rust tests. Build iOS and And
|
|
|
215
224
|
},
|
|
216
225
|
scripts: {
|
|
217
226
|
...scripts,
|
|
218
|
-
"rust:test":
|
|
227
|
+
"rust:test": `cargo test --manifest-path ${(0, import_codegen.toPosixRelative)(root, rustPath)}/Cargo.toml`,
|
|
219
228
|
"rust:generate": "react-native-rust generate",
|
|
220
229
|
"rust:build:ios": "react-native-rust build ios",
|
|
221
230
|
"rust:build:android": "react-native-rust build android",
|
|
222
231
|
"rust:build:web": "react-native-rust build web"
|
|
223
232
|
}
|
|
224
233
|
};
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
234
|
+
const rustIsInsideRoot = !(0, import_codegen.toPosixRelative)(root, rustPath).startsWith("..");
|
|
235
|
+
if (rustIsInsideRoot) {
|
|
236
|
+
const packageFiles = Array.isArray(updatedManifest.files) ? [...updatedManifest.files] : [];
|
|
237
|
+
for (const filePath of ["rust/", "rust/build/", "rust/include/"]) {
|
|
238
|
+
if (!packageFiles.includes(filePath)) packageFiles.push(filePath);
|
|
239
|
+
}
|
|
240
|
+
updatedManifest.files = packageFiles;
|
|
228
241
|
}
|
|
229
|
-
updatedManifest.files = packageFiles;
|
|
230
242
|
updates.set(manifestPath, `${JSON.stringify(updatedManifest, null, 2)}
|
|
231
243
|
`);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
244
|
+
if (rustIsInsideRoot) {
|
|
245
|
+
const gitignorePath = import_node_path.default.join(root, ".gitignore");
|
|
246
|
+
const gitignore = import_node_fs.default.existsSync(gitignorePath) ? import_node_fs.default.readFileSync(gitignorePath, "utf8") : "";
|
|
247
|
+
const ignoredRustOutputs = ["rust/target/", "rust/build/"].filter((entry) => !gitignore.split(/\r?\n/).includes(entry));
|
|
248
|
+
if (ignoredRustOutputs.length > 0) {
|
|
249
|
+
const separator = gitignore.length > 0 && !gitignore.endsWith("\n") ? "\n" : "";
|
|
250
|
+
updates.set(gitignorePath, `${gitignore}${separator}${ignoredRustOutputs.join("\n")}
|
|
238
251
|
`);
|
|
252
|
+
}
|
|
239
253
|
}
|
|
240
254
|
for (const [relativePath, fileContents] of Object.entries(rustFiles)) {
|
|
241
255
|
const outputPath = import_node_path.default.join(rustPath, relativePath);
|
|
@@ -250,14 +264,14 @@ Run cargo test --manifest-path rust/Cargo.toml for Rust tests. Build iOS and And
|
|
|
250
264
|
console.log("Run `npx react-native-rust build ios` or `npx react-native-rust build android` before building the React Native app.");
|
|
251
265
|
}
|
|
252
266
|
function generate() {
|
|
253
|
-
const { root, moduleName } = packageRoot();
|
|
254
|
-
const rustPath =
|
|
267
|
+
const { root, manifest, moduleName } = packageRoot();
|
|
268
|
+
const rustPath = resolveRustRoot(root, manifest);
|
|
255
269
|
if (!import_node_fs.default.existsSync(import_node_path.default.join(rustPath, "Cargo.toml"))) {
|
|
256
270
|
throw new Error("Rust is not initialized. Run `npx react-native-rust init` first.");
|
|
257
271
|
}
|
|
258
272
|
const nativeSpecPath = import_node_path.default.join(root, "src", `Native${moduleName}.ts`);
|
|
259
273
|
const methods = (0, import_codegen.parseSpec)(import_node_fs.default.readFileSync(nativeSpecPath, "utf8"), import_node_path.default.relative(root, nativeSpecPath));
|
|
260
|
-
const updates = (0, import_codegen.renderProjectBindings)(root, moduleName, methods, false, readCrateName(rustPath));
|
|
274
|
+
const updates = (0, import_codegen.renderProjectBindings)(root, moduleName, methods, false, readCrateName(rustPath), rustPath);
|
|
261
275
|
for (const [filePath, fileContents] of updates) {
|
|
262
276
|
import_node_fs.default.mkdirSync(import_node_path.default.dirname(filePath), { recursive: true });
|
|
263
277
|
import_node_fs.default.writeFileSync(filePath, fileContents);
|
|
@@ -265,8 +279,8 @@ function generate() {
|
|
|
265
279
|
console.log(`Regenerated Rust, C++, and TypeScript glue for ${methods.length} Spec method(s).`);
|
|
266
280
|
}
|
|
267
281
|
function doctor(target) {
|
|
268
|
-
const { root } = packageRoot();
|
|
269
|
-
if (!import_node_fs.default.existsSync(import_node_path.default.join(root,
|
|
282
|
+
const { root, manifest } = packageRoot();
|
|
283
|
+
if (!import_node_fs.default.existsSync(import_node_path.default.join(resolveRustRoot(root, manifest), "Cargo.toml"))) {
|
|
270
284
|
throw new Error("Rust is not initialized. Run `npx react-native-rust init` first.");
|
|
271
285
|
}
|
|
272
286
|
let failed = false;
|
|
@@ -301,13 +315,14 @@ function doctor(target) {
|
|
|
301
315
|
}
|
|
302
316
|
function build(platform) {
|
|
303
317
|
const { root, manifest, moduleName } = packageRoot();
|
|
304
|
-
const rustRoot =
|
|
318
|
+
const rustRoot = resolveRustRoot(root, manifest);
|
|
305
319
|
const manifestPath = import_node_path.default.join(rustRoot, "Cargo.toml");
|
|
306
320
|
if (!import_node_fs.default.existsSync(manifestPath)) {
|
|
307
321
|
throw new Error("Rust is not initialized. Run `npx react-native-rust init` first.");
|
|
308
322
|
}
|
|
309
323
|
const crateName = readCrateName(rustRoot);
|
|
310
324
|
const releasePath = (target) => import_node_path.default.join(rustRoot, "target", target, "release", `lib${crateName}.a`);
|
|
325
|
+
(0, import_codegen.resolveOutsideReference)(root, import_node_path.default.join(rustRoot, "build", "android"), "rust-build-android");
|
|
311
326
|
if (platform === "ios") {
|
|
312
327
|
for (const target of iosTargets) {
|
|
313
328
|
const result = run("cargo", ["build", "--release", "--target", target, "--manifest-path", manifestPath], { inherit: true });
|
|
@@ -342,6 +357,12 @@ function build(platform) {
|
|
|
342
357
|
frameworkPath
|
|
343
358
|
], { inherit: true });
|
|
344
359
|
if (!xcframework.ok) throw new Error(`Could not create the iOS XCFramework: ${xcframework.message}`);
|
|
360
|
+
if ((0, import_codegen.toPosixRelative)(root, frameworkPath).startsWith("..")) {
|
|
361
|
+
const podFrameworkPath = import_node_path.default.join(root, "rust-build-ios", `${moduleName}Rust.xcframework`);
|
|
362
|
+
import_node_fs.default.rmSync(import_node_path.default.dirname(podFrameworkPath), { recursive: true, force: true });
|
|
363
|
+
import_node_fs.default.mkdirSync(import_node_path.default.dirname(podFrameworkPath), { recursive: true });
|
|
364
|
+
import_node_fs.default.cpSync(frameworkPath, podFrameworkPath, { recursive: true });
|
|
365
|
+
}
|
|
345
366
|
console.log(`Built ${import_node_path.default.relative(root, frameworkPath)} for ${manifest.name}.`);
|
|
346
367
|
return;
|
|
347
368
|
}
|
|
@@ -371,11 +392,71 @@ function build(platform) {
|
|
|
371
392
|
if (platform === "web") {
|
|
372
393
|
const result = run("wasm-pack", ["build", "--target", "web", "--out-dir", import_node_path.default.join("build", "web", "pkg")], { inherit: true, cwd: rustRoot });
|
|
373
394
|
if (!result.ok) throw new Error(`Web build failed: ${result.message}`);
|
|
374
|
-
console.log(`Built
|
|
395
|
+
console.log(`Built ${(0, import_codegen.toPosixRelative)(root, import_node_path.default.join(rustRoot, "build", "web", "pkg"))} for ${manifest.name}.`);
|
|
375
396
|
return;
|
|
376
397
|
}
|
|
377
398
|
throw new Error("Choose a build target: ios, android, or web.");
|
|
378
399
|
}
|
|
400
|
+
function watch(options = {}) {
|
|
401
|
+
const { root, manifest, moduleName } = packageRoot();
|
|
402
|
+
const rustRoot = resolveRustRoot(root, manifest);
|
|
403
|
+
const cargoManifest = import_node_path.default.join(rustRoot, "Cargo.toml");
|
|
404
|
+
if (!import_node_fs.default.existsSync(cargoManifest)) {
|
|
405
|
+
throw new Error("Rust is not initialized. Run `npx react-native-rust init` first.");
|
|
406
|
+
}
|
|
407
|
+
const log = options.log || console.log;
|
|
408
|
+
const target = { rustRoot, specPath: import_node_path.default.join(root, "src", `Native${moduleName}.ts`) };
|
|
409
|
+
let pendingKinds = /* @__PURE__ */ new Set();
|
|
410
|
+
async function runWatchCycle(kinds) {
|
|
411
|
+
if (kinds.has("spec")) {
|
|
412
|
+
log("[RN Rust] Spec changed. Regenerating native bindings...");
|
|
413
|
+
try {
|
|
414
|
+
generate();
|
|
415
|
+
options.onGenerated?.();
|
|
416
|
+
} catch (error) {
|
|
417
|
+
log("[RN Rust] Code generation failed.");
|
|
418
|
+
log("Possible causes:");
|
|
419
|
+
log(" - invalid Rust API");
|
|
420
|
+
log(" - unsupported parameter type");
|
|
421
|
+
log(" - unsupported return type");
|
|
422
|
+
log(" - invalid annotation/spec");
|
|
423
|
+
log(` ${error.message}`);
|
|
424
|
+
log("[Rust] Waiting for changes...");
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
log("[RN Rust] Native bindings updated.");
|
|
428
|
+
}
|
|
429
|
+
log("[Rust] Building...");
|
|
430
|
+
const start = Date.now();
|
|
431
|
+
const result = run("cargo", ["build", "--manifest-path", cargoManifest], { inherit: true });
|
|
432
|
+
const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
|
|
433
|
+
if (!result.ok) {
|
|
434
|
+
log("[Rust] Build failed.");
|
|
435
|
+
log("[Rust] Waiting for changes...");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
log(`[Rust] Build completed in ${elapsed}s`);
|
|
439
|
+
log("[Rust] Native library updated.");
|
|
440
|
+
log("[RN Rust] Native code cannot hot-swap into a running app: run `npm run rust:build:ios` / `rust:build:android` / `rust:build:web` and reinstall (or refresh the browser tab, for react-native-web) to load this change.");
|
|
441
|
+
log("[Rust] Waiting for changes...");
|
|
442
|
+
}
|
|
443
|
+
const queue = new import_buildQueue.SingleFlightQueue(() => {
|
|
444
|
+
const kinds = pendingKinds;
|
|
445
|
+
pendingKinds = /* @__PURE__ */ new Set();
|
|
446
|
+
return runWatchCycle(kinds);
|
|
447
|
+
});
|
|
448
|
+
const watcher = (0, import_watcher.watchRustProject)({
|
|
449
|
+
target,
|
|
450
|
+
log,
|
|
451
|
+
onChange: (kinds) => {
|
|
452
|
+
for (const kind of kinds) pendingKinds.add(kind);
|
|
453
|
+
queue.request();
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
log(`[Rust] Watching ${import_node_path.default.relative(process.cwd(), rustRoot) || "rust"}/ and ${import_node_path.default.relative(process.cwd(), target.specPath)}`);
|
|
457
|
+
log("[Rust] Waiting for changes...");
|
|
458
|
+
return { stop: () => watcher.close() };
|
|
459
|
+
}
|
|
379
460
|
// Annotate the CommonJS export names for ESM import in node:
|
|
380
461
|
0 && (module.exports = {
|
|
381
462
|
build,
|
|
@@ -384,5 +465,6 @@ function build(platform) {
|
|
|
384
465
|
init,
|
|
385
466
|
packageRoot,
|
|
386
467
|
readJson,
|
|
387
|
-
run
|
|
468
|
+
run,
|
|
469
|
+
watch
|
|
388
470
|
});
|
|
@@ -39,6 +39,7 @@ Usage:
|
|
|
39
39
|
react-native-rust build ios Build an iOS XCFramework
|
|
40
40
|
react-native-rust build android Build Android static archives for supported ABIs
|
|
41
41
|
react-native-rust build web Build a WebAssembly package for react-native-web
|
|
42
|
+
react-native-rust watch Watch the Spec and rust/ for changes and rebuild automatically
|
|
42
43
|
react-native-rust --help Show this help
|
|
43
44
|
|
|
44
45
|
Create a library first with the C++ module template from:
|
|
@@ -106,6 +107,19 @@ function main(args) {
|
|
|
106
107
|
(0, import_app.runCodegen)(appRoot, appModule);
|
|
107
108
|
return (0, import_app.inDirectory)(appModule, () => (0, import_react_native_rust_lib.build)(option));
|
|
108
109
|
}
|
|
110
|
+
if (command === "watch") {
|
|
111
|
+
if (appModule) process.chdir(appModule);
|
|
112
|
+
const handle = (0, import_react_native_rust_lib.watch)(appModule ? { onGenerated: () => (0, import_app.runCodegen)(appRoot, appModule) } : {});
|
|
113
|
+
const shutdown = (signal) => {
|
|
114
|
+
console.log(`
|
|
115
|
+
[Rust] Received ${signal}, stopping watcher...`);
|
|
116
|
+
handle.stop().finally(() => process.exit(0));
|
|
117
|
+
};
|
|
118
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
119
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
120
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
109
123
|
throw new Error(`Unknown command: ${command}. Run react-native-rust --help for usage.`);
|
|
110
124
|
}
|
|
111
125
|
try {
|
package/metro.js
ADDED
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rejaul/react-native-rust",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Generate Rust and native bindings from React Native TurboModule TypeScript interfaces.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"dist/",
|
|
10
|
+
"metro.js",
|
|
10
11
|
"README.md",
|
|
11
12
|
"LICENSE",
|
|
12
13
|
"CONTRIBUTING.md",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
},
|
|
48
49
|
"homepage": "https://github.com/DeveloperRejaul/react-native-rust#readme",
|
|
49
50
|
"dependencies": {
|
|
51
|
+
"chokidar": "^3.6.0",
|
|
50
52
|
"typescript": "^5.9.3"
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|