@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 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 `native/rust-module/`, adds it to the app as a local dependency, and generates its C++ bridge and Rust crate. It does not publish a separate library. App-local mode currently supports C++ TurboModule apps with both Android and iOS projects.
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
- Edit `native/rust-module/src/NativeRustApp.ts` to declare methods, then implement their generated handlers under `native/rust-module/rust/src/api/`. From the app root, regenerate the bindings and React Native Codegen output with:
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 = import_node_path.default.join("native", "rust-module");
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", "rust/"],
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
- ".gitignore": "node_modules/\nrust/target/\nrust/build/\n",
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
- return import_node_fs.default.existsSync(import_node_path.default.join(moduleRoot, "rust", "Cargo.toml")) ? moduleRoot : null;
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": `cargo test --manifest-path ${moduleDirectory.split(import_node_path.default.sep).join("/")}/rust/Cargo.toml`
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(`Created app-local Rust TurboModule in ${import_node_path.default.relative(appRoot, moduleRoot)}.`);
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:
@@ -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
  });
@@ -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, includeRustHeader = false) {
106
+ function updateCppFile(source, markers, generated, templateMethod, methodNeedle, rustHeaderPath) {
95
107
  let updated = source;
96
- if (includeRustHeader && !updated.includes("../rust/include/rust_api.h")) {
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", '#pragma once\n\n#include "../rust/include/rust_api.h"');
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 updatedHeader = updateCppFile(header, [import_constants.HEADER_START, import_constants.HEADER_END], cpp.header, initialize, headerTemplateMethod, true);
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(root, "rust", "src");
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 wrappers = (0, import_renderers.renderWrappers)(methods, moduleName, crateName);
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 '../rust/build/web/pkg/${crateName}.js';`,
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 = import_node_path.default.join(root, rustDirectory);
111
+ const rustPath = resolveRustRoot(root, manifest);
106
112
  if (import_node_fs.default.existsSync(rustPath)) {
107
- throw new Error("The rust/ directory already exists; no files were changed.");
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}/../rust/build/android/\${ANDROID_ABI}/lib${crateDirName}.a"
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 = "rust/build/ios/${moduleName}Rust.xcframework"`
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": "cargo test --manifest-path rust/Cargo.toml",
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 packageFiles = Array.isArray(updatedManifest.files) ? [...updatedManifest.files] : [];
226
- for (const filePath of ["rust/", "rust/build/", "rust/include/"]) {
227
- if (!packageFiles.includes(filePath)) packageFiles.push(filePath);
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
- const gitignorePath = import_node_path.default.join(root, ".gitignore");
233
- const gitignore = import_node_fs.default.existsSync(gitignorePath) ? import_node_fs.default.readFileSync(gitignorePath, "utf8") : "";
234
- const ignoredRustOutputs = ["rust/target/", "rust/build/"].filter((entry) => !gitignore.split(/\r?\n/).includes(entry));
235
- if (ignoredRustOutputs.length > 0) {
236
- const separator = gitignore.length > 0 && !gitignore.endsWith("\n") ? "\n" : "";
237
- updates.set(gitignorePath, `${gitignore}${separator}${ignoredRustOutputs.join("\n")}
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 = import_node_path.default.join(root, rustDirectory);
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, rustDirectory, "Cargo.toml"))) {
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 = import_node_path.default.join(root, rustDirectory);
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 rust/build/web/pkg for ${manifest.name}.`);
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
@@ -0,0 +1,2 @@
1
+ // Entry point for `require('@rejaul/react-native-rust/metro')`.
2
+ module.exports = require('./dist/metro.js');
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@rejaul/react-native-rust",
3
- "version": "1.1.1",
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": {