@rejaul/react-native-rust 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { spawnSync } = require('node:child_process');
6
+ const { parseSpec, renderProjectBindings, renderRustFfiModule } = require('./codegen');
7
+
8
+ const rustDirectory = 'rust';
9
+ const iosTargets = ['aarch64-apple-ios', 'aarch64-apple-ios-sim', 'x86_64-apple-ios'];
10
+ const androidTargets = [
11
+ ['arm64-v8a', 'aarch64-linux-android'],
12
+ ['armeabi-v7a', 'armv7-linux-androideabi'],
13
+ ['x86', 'i686-linux-android'],
14
+ ['x86_64', 'x86_64-linux-android'],
15
+ ];
16
+
17
+ function run(command, args, options = {}) {
18
+ const result = spawnSync(command, args, {
19
+ cwd: options.cwd || process.cwd(),
20
+ encoding: 'utf8',
21
+ stdio: options.inherit ? 'inherit' : 'pipe',
22
+ });
23
+ if (result.error) return { ok: false, message: result.error.message };
24
+ if (result.status !== 0) {
25
+ return {
26
+ ok: false,
27
+ message: (result.stderr || result.stdout || `${command} exited with ${result.status}`).trim(),
28
+ };
29
+ }
30
+ return { ok: true, output: (result.stdout || '').trim() };
31
+ }
32
+
33
+ function readJson(filePath) {
34
+ try {
35
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
36
+ } catch (error) {
37
+ throw new Error(`Cannot read ${filePath}: ${error.message}`);
38
+ }
39
+ }
40
+
41
+ function packageRoot() {
42
+ const root = process.cwd();
43
+ const manifestPath = path.join(root, 'package.json');
44
+ if (!fs.existsSync(manifestPath)) {
45
+ throw new Error('Run this command from the root of a React Native library.');
46
+ }
47
+ const manifest = readJson(manifestPath);
48
+ const dependencies = { ...manifest.dependencies, ...manifest.peerDependencies };
49
+ if (!dependencies['react-native']) {
50
+ throw new Error('This package does not declare react-native as a dependency or peer dependency.');
51
+ }
52
+
53
+ const configPath = path.join(root, 'react-native.config.js');
54
+ if (!fs.existsSync(configPath)) {
55
+ throw new Error('Missing react-native.config.js. Create the library with the C++ module template.');
56
+ }
57
+ const config = fs.readFileSync(configPath, 'utf8');
58
+ const moduleMatch = config.match(/cxxModuleHeaderName\s*:\s*['"]([a-zA-Z0-9_]+)Impl['"]/);
59
+ if (!moduleMatch || !config.includes('cxxModuleCMakeListsPath')) {
60
+ throw new Error('The library is not configured as a C++ TurboModule. Choose the C++ module template.');
61
+ }
62
+ return { root, manifest, manifestPath, moduleName: moduleMatch[1] };
63
+ }
64
+
65
+ function init() {
66
+ const { root, manifest, manifestPath, moduleName } = packageRoot();
67
+ const rustPath = path.join(root, rustDirectory);
68
+ if (fs.existsSync(rustPath)) {
69
+ throw new Error('The rust/ directory already exists; no files were changed.');
70
+ }
71
+ const scripts = manifest.scripts || {};
72
+ if (scripts['rust:test'] || scripts['rust:generate']) {
73
+ throw new Error('package.json already defines a rust:test or rust:generate script; no files were changed.');
74
+ }
75
+
76
+ const projectFiles = {
77
+ cppHeader: path.join(root, 'cpp', `${moduleName}Impl.h`),
78
+ cppSource: path.join(root, 'cpp', `${moduleName}Impl.cpp`),
79
+ nativeSpec: path.join(root, 'src', `Native${moduleName}.ts`),
80
+ libraryIndex: path.join(root, 'src', 'index.tsx'),
81
+ androidCmake: path.join(root, 'android', 'CMakeLists.txt'),
82
+ };
83
+ const podspecs = fs.readdirSync(root).filter((fileName) => fileName.endsWith('.podspec'));
84
+ if (podspecs.length !== 1) throw new Error('Expected exactly one CocoaPods spec file. No files were changed.');
85
+ projectFiles.podspec = path.join(root, podspecs[0]);
86
+ if (Object.values(projectFiles).some((filePath) => !fs.existsSync(filePath))) {
87
+ throw new Error('Expected C++ library template files were not found. No files were changed.');
88
+ }
89
+
90
+ const contents = Object.fromEntries(Object.entries(projectFiles).map(([key, filePath]) => [
91
+ key,
92
+ fs.readFileSync(filePath, 'utf8'),
93
+ ]));
94
+ const targetMatch = contents.androidCmake.match(/add_library\(\s*([a-zA-Z0-9_-]+)\s+STATIC/);
95
+ if (!targetMatch || !contents.cppHeader.includes('#pragma once')) {
96
+ throw new Error('The native files do not match the supported C++ TurboModule template. No files were changed.');
97
+ }
98
+ const methods = parseSpec(contents.nativeSpec, path.relative(root, projectFiles.nativeSpec));
99
+
100
+ const crateName = (manifest.name || 'react-native-rust')
101
+ .replace(/^@[^/]+\//, '')
102
+ .replace(/[^a-zA-Z0-9_]+/g, '_');
103
+ const crateDirName = /^\d/.test(crateName) ? `rust_${crateName.toLowerCase()}` : crateName.toLowerCase();
104
+ const targetName = targetMatch[1];
105
+ const linkMatch = contents.androidCmake.match(/target_link_libraries\(\s*([a-zA-Z0-9_-]+)([\s\S]*?)\n\)/);
106
+ if (!linkMatch || linkMatch[1] !== targetName) {
107
+ throw new Error('Could not locate the C++ target link block in android/CMakeLists.txt.');
108
+ }
109
+ const androidLink = `${linkMatch[0].slice(0, -2)}\n rust_core\n log\n dl\n m\n)`;
110
+ const rustImport = `add_library(rust_core STATIC IMPORTED)\nset_target_properties(rust_core PROPERTIES\n IMPORTED_LOCATION "\${CMAKE_CURRENT_LIST_DIR}/../rust/build/android/\${ANDROID_ABI}/lib${crateDirName}.a"\n)\n\n`;
111
+ const androidCmake = contents.androidCmake
112
+ .replace(linkMatch[0], androidLink)
113
+ .replace(/add_library\(/, `${rustImport}add_library(`);
114
+
115
+ const podspecSource = contents.podspec.match(/^\s*s\.source_files\s*=.*$/m);
116
+ if (!podspecSource) throw new Error('Could not locate source_files in the CocoaPods spec.');
117
+ const podspec = contents.podspec.replace(
118
+ podspecSource[0],
119
+ `${podspecSource[0]}\n s.vendored_frameworks = "rust/build/ios/${moduleName}Rust.xcframework"`,
120
+ );
121
+
122
+ const rustFiles = {
123
+ 'Cargo.toml': `[package]\nname = "${crateDirName}"\nversion = "0.1.0"\nedition = "2021"\n\n[lib]\ncrate-type = ["staticlib"]\n\n[dependencies]\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"\n\n[build-dependencies]\ncbindgen = "0.26"\n`,
124
+ 'build.rs': `use std::path::PathBuf;\n\nfn main() {\n let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();\n let output = PathBuf::from(&crate_dir).join("include/rust_api.h");\n std::fs::create_dir_all(output.parent().unwrap()).unwrap();\n cbindgen::generate(&crate_dir)\n .expect("failed to generate the Rust C header")\n .write_to_file(output);\n}\n`,
125
+ 'cbindgen.toml': `language = "C++"\n`,
126
+ 'src/ffi.rs': renderRustFfiModule(),
127
+ 'README.md': `# Rust core\n\nRust handlers are generated from the TurboModule Spec interface in src/Native${moduleName}.ts. Edit the generated functions under rust/src/api/ and regenerate native glue with npx react-native-rust generate.\n\nRun 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.\n`,
128
+ };
129
+
130
+ const updates = renderProjectBindings(root, moduleName, methods, true);
131
+ updates.set(projectFiles.androidCmake, androidCmake);
132
+ updates.set(projectFiles.podspec, podspec);
133
+ const cliManifest = readJson(path.join(__dirname, '..', 'package.json'));
134
+ const cliRoot = path.resolve(__dirname, '..');
135
+ const cliIsInstalled = cliRoot.split(path.sep).includes('node_modules');
136
+ const cliDependency = cliIsInstalled
137
+ ? `^${cliManifest.version}`
138
+ : `file:${path.relative(root, cliRoot).split(path.sep).join('/') || '.'}`;
139
+ const updatedManifest = {
140
+ ...manifest,
141
+ devDependencies: {
142
+ ...manifest.devDependencies,
143
+ [cliManifest.name]: manifest.devDependencies?.[cliManifest.name] || cliDependency,
144
+ },
145
+ scripts: {
146
+ ...scripts,
147
+ 'rust:test': 'cargo test --manifest-path rust/Cargo.toml',
148
+ 'rust:generate': 'react-native-rust generate',
149
+ 'rust:build:ios': 'react-native-rust build ios',
150
+ 'rust:build:android': 'react-native-rust build android',
151
+ },
152
+ };
153
+ const packageFiles = Array.isArray(updatedManifest.files) ? [...updatedManifest.files] : [];
154
+ for (const filePath of ['rust/', 'rust/build/', 'rust/include/']) {
155
+ if (!packageFiles.includes(filePath)) packageFiles.push(filePath);
156
+ }
157
+ updatedManifest.files = packageFiles;
158
+ updates.set(manifestPath, `${JSON.stringify(updatedManifest, null, 2)}\n`);
159
+
160
+ const gitignorePath = path.join(root, '.gitignore');
161
+ const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
162
+ const ignoredRustOutputs = ['rust/target/', 'rust/build/']
163
+ .filter((entry) => !gitignore.split(/\r?\n/).includes(entry));
164
+ if (ignoredRustOutputs.length > 0) {
165
+ const separator = gitignore.length > 0 && !gitignore.endsWith('\n') ? '\n' : '';
166
+ updates.set(gitignorePath, `${gitignore}${separator}${ignoredRustOutputs.join('\n')}\n`);
167
+ }
168
+
169
+ for (const [relativePath, fileContents] of Object.entries(rustFiles)) {
170
+ const outputPath = path.join(rustPath, relativePath);
171
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
172
+ fs.writeFileSync(outputPath, fileContents);
173
+ }
174
+ for (const [filePath, fileContents] of updates) {
175
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
176
+ fs.writeFileSync(filePath, fileContents);
177
+ }
178
+
179
+ console.log(`Generated ${methods.length} Rust handler(s) and native bindings from Spec.`);
180
+ console.log('Run `npx react-native-rust build ios` or `npx react-native-rust build android` before building the React Native app.');
181
+ }
182
+
183
+ function generate() {
184
+ const { root, moduleName } = packageRoot();
185
+ const rustPath = path.join(root, rustDirectory);
186
+ if (!fs.existsSync(path.join(rustPath, 'Cargo.toml'))) {
187
+ throw new Error('Rust is not initialized. Run `npx react-native-rust init` first.');
188
+ }
189
+ const nativeSpecPath = path.join(root, 'src', `Native${moduleName}.ts`);
190
+ const methods = parseSpec(fs.readFileSync(nativeSpecPath, 'utf8'), path.relative(root, nativeSpecPath));
191
+ const updates = renderProjectBindings(root, moduleName, methods, false);
192
+ for (const [filePath, fileContents] of updates) {
193
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
194
+ fs.writeFileSync(filePath, fileContents);
195
+ }
196
+ console.log(`Regenerated Rust, C++, and TypeScript glue for ${methods.length} Spec method(s).`);
197
+ }
198
+
199
+ function doctor(target) {
200
+ const { root } = packageRoot();
201
+ if (!fs.existsSync(path.join(root, rustDirectory, 'Cargo.toml'))) {
202
+ throw new Error('Rust is not initialized. Run `npx react-native-rust init` first.');
203
+ }
204
+ let failed = false;
205
+ for (const command of ['rustc', 'cargo']) {
206
+ const result = run(command, ['--version']);
207
+ console.log(`${result.ok ? 'OK' : 'MISSING'} ${command}${result.output ? `: ${result.output}` : ''}`);
208
+ failed ||= !result.ok;
209
+ }
210
+
211
+ if (target === 'ios') {
212
+ const xcode = run('xcodebuild', ['-version']);
213
+ const lipo = run('lipo', ['-version']);
214
+ console.log(`${xcode.ok ? 'OK' : 'MISSING'} Xcode`);
215
+ console.log(`${lipo.ok ? 'OK' : 'MISSING'} lipo`);
216
+ failed ||= process.platform !== 'darwin' || !xcode.ok || !lipo.ok;
217
+ }
218
+ if (target === 'android') {
219
+ const ndkPath = process.env.ANDROID_NDK_HOME || process.env.ANDROID_NDK_ROOT;
220
+ const cargoNdk = run('cargo', ['ndk', '--version']);
221
+ console.log(`${ndkPath ? 'OK' : 'CHECK'} Android NDK path (cargo-ndk can auto-detect Android Studio installs)`);
222
+ console.log(`${cargoNdk.ok ? 'OK' : 'MISSING'} cargo-ndk`);
223
+ failed ||= !cargoNdk.ok;
224
+ }
225
+ if (failed) process.exitCode = 1;
226
+ }
227
+
228
+ function build(platform) {
229
+ const { root, manifest, moduleName } = packageRoot();
230
+ const rustRoot = path.join(root, rustDirectory);
231
+ const manifestPath = path.join(rustRoot, 'Cargo.toml');
232
+ if (!fs.existsSync(manifestPath)) {
233
+ throw new Error('Rust is not initialized. Run `npx react-native-rust init` first.');
234
+ }
235
+ const cargoManifest = fs.readFileSync(manifestPath, 'utf8');
236
+ const crateMatch = cargoManifest.match(/^name\s*=\s*"([a-zA-Z0-9_-]+)"/m);
237
+ if (!crateMatch) throw new Error('Could not read the Rust crate name from rust/Cargo.toml.');
238
+ const crateName = crateMatch[1];
239
+ const releasePath = (target) => path.join(rustRoot, 'target', target, 'release', `lib${crateName}.a`);
240
+
241
+ if (platform === 'ios') {
242
+ for (const target of iosTargets) {
243
+ const result = run('cargo', ['build', '--release', '--target', target, '--manifest-path', manifestPath], { inherit: true });
244
+ if (!result.ok) throw new Error(`iOS build failed for ${target}: ${result.message}`);
245
+ }
246
+ const buildPath = path.join(rustRoot, 'build', 'ios');
247
+ fs.mkdirSync(buildPath, { recursive: true });
248
+ const simulatorDirectory = path.join(buildPath, 'simulator');
249
+ fs.mkdirSync(simulatorDirectory, { recursive: true });
250
+ const simulatorArchive = path.join(simulatorDirectory, `lib${crateName}.a`);
251
+ const lipo = run('lipo', [
252
+ '-create',
253
+ releasePath('aarch64-apple-ios-sim'),
254
+ releasePath('x86_64-apple-ios'),
255
+ '-output', simulatorArchive,
256
+ ], { inherit: true });
257
+ if (!lipo.ok) throw new Error(`Could not combine iOS simulator archives: ${lipo.message}`);
258
+
259
+ const frameworkPath = path.join(buildPath, `${moduleName}Rust.xcframework`);
260
+ fs.rmSync(frameworkPath, { recursive: true, force: true });
261
+ const xcframework = run('xcodebuild', [
262
+ '-create-xcframework',
263
+ '-library', releasePath('aarch64-apple-ios'),
264
+ '-headers', path.join(rustRoot, 'include'),
265
+ '-library', simulatorArchive,
266
+ '-headers', path.join(rustRoot, 'include'),
267
+ '-output', frameworkPath,
268
+ ], { inherit: true });
269
+ if (!xcframework.ok) throw new Error(`Could not create the iOS XCFramework: ${xcframework.message}`);
270
+ console.log(`Built ${path.relative(root, frameworkPath)} for ${manifest.name}.`);
271
+ return;
272
+ }
273
+
274
+ if (platform === 'android') {
275
+ const result = run('cargo', ['ndk',
276
+ '-t', 'arm64-v8a',
277
+ '-t', 'armeabi-v7a',
278
+ '-t', 'x86',
279
+ '-t', 'x86_64',
280
+ 'build', '--release',
281
+ ], { inherit: true, cwd: rustRoot });
282
+ if (!result.ok) throw new Error(`Android build failed: ${result.message}`);
283
+ for (const [abi, target] of androidTargets) {
284
+ const outputDirectory = path.join(rustRoot, 'build', 'android', abi);
285
+ fs.mkdirSync(outputDirectory, { recursive: true });
286
+ fs.copyFileSync(releasePath(target), path.join(outputDirectory, `lib${crateName}.a`));
287
+ }
288
+ console.log(`Built Android static archives for ${manifest.name}.`);
289
+ return;
290
+ }
291
+ throw new Error('Choose a build target: ios or android.');
292
+ }
293
+
294
+ module.exports = {
295
+ run,
296
+ readJson,
297
+ packageRoot,
298
+ init,
299
+ generate,
300
+ doctor,
301
+ build,
302
+ };
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { init, generate, doctor, build } = require('./react-native-rust-lib');
4
+
5
+ function printHelp() {
6
+ console.log(`react-native-rust - Rust helper for React Native C++ TurboModule libraries
7
+
8
+ Usage:
9
+ react-native-rust create <name> Create a React Native library and initialize Rust support
10
+ react-native-rust init Add Rust support and generate functions from the Spec interface
11
+ react-native-rust generate Regenerate Rust, C++, and TypeScript glue from Spec
12
+ react-native-rust doctor [target] Check Rust and optional ios/android prerequisites
13
+ react-native-rust build ios Build an iOS XCFramework
14
+ react-native-rust build android Build Android static archives for supported ABIs
15
+ react-native-rust --help Show this help
16
+
17
+ Create a library first with the C++ module template from:
18
+ npx create-react-native-library@latest`);
19
+ }
20
+
21
+ function createProject(projectName) {
22
+ const { spawnSync } = require('node:child_process');
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+
26
+ if (!projectName || projectName.startsWith('-') || path.basename(projectName) !== projectName) {
27
+ throw new Error('Usage: react-native-rust create <library-name>');
28
+ }
29
+
30
+ const projectPath = path.resolve(process.cwd(), projectName);
31
+ if (fs.existsSync(projectPath)) {
32
+ throw new Error(`The target directory already exists: ${projectPath}`);
33
+ }
34
+
35
+ console.log('In the scaffolder prompts, select a TurboModule library using C++.');
36
+ const scaffold = spawnSync('npx', ['--yes', 'create-react-native-library@latest', projectName], {
37
+ cwd: process.cwd(),
38
+ stdio: 'inherit',
39
+ encoding: 'utf8',
40
+ });
41
+ if (scaffold.error) throw new Error(`React Native library creation failed: ${scaffold.error.message}`);
42
+ if (scaffold.status !== 0) throw new Error('React Native library creation failed.');
43
+ if (!fs.existsSync(path.join(projectPath, 'package.json'))) {
44
+ throw new Error(`The scaffolder did not create ${projectPath}.`);
45
+ }
46
+
47
+ const initialized = spawnSync(process.execPath, [__filename, 'init'], {
48
+ cwd: projectPath,
49
+ stdio: 'inherit',
50
+ encoding: 'utf8',
51
+ });
52
+ if (initialized.error) {
53
+ throw new Error(`Rust initialization failed in ${projectPath}: ${initialized.error.message}`);
54
+ }
55
+ if (initialized.status !== 0) {
56
+ throw new Error(`Rust initialization failed in ${projectPath}.`);
57
+ }
58
+ console.log(`Created ${projectName}. Next run \`cd ${projectName} && yarn install\`.`);
59
+ }
60
+
61
+ function main(args) {
62
+ const [command, option] = args;
63
+ if (!command || command === '--help' || command === '-h') return printHelp();
64
+ if (command === 'create') return createProject(option);
65
+ if (command === 'init') return init();
66
+ if (command === 'generate') return generate();
67
+ if (command === 'doctor') {
68
+ if (option && option !== 'ios' && option !== 'android') {
69
+ throw new Error('Choose a doctor target: ios or android.');
70
+ }
71
+ return doctor(option);
72
+ }
73
+ if (command === 'build') return build(option);
74
+ throw new Error(`Unknown command: ${command}. Run react-native-rust --help for usage.`);
75
+ }
76
+
77
+ try {
78
+ main(process.argv.slice(2));
79
+ } catch (error) {
80
+ console.error(`Error: ${error.message}`);
81
+ process.exitCode = 1;
82
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@rejaul/react-native-rust",
3
+ "version": "1.0.1",
4
+ "description": "Generate Rust and native bindings from React Native TurboModule TypeScript interfaces.",
5
+ "engines": {
6
+ "node": ">=18"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "README.md",
11
+ "LICENSE",
12
+ "CONTRIBUTING.md",
13
+ "CODE_OF_CONDUCT.md"
14
+ ],
15
+ "bin": {
16
+ "react-native-rust": "./bin/react-native-rust.js"
17
+ },
18
+ "scripts": {
19
+ "test": "node --test"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/DeveloperRejaul/react-native-rust.git"
24
+ },
25
+ "keywords": [],
26
+ "author": "",
27
+ "license": "ISC",
28
+ "bugs": {
29
+ "url": "https://github.com/DeveloperRejaul/react-native-rust/issues"
30
+ },
31
+ "homepage": "https://github.com/DeveloperRejaul/react-native-rust#readme",
32
+ "dependencies": {
33
+ "typescript": "^5.9.3"
34
+ }
35
+ }