@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,357 @@
1
+ function renderRustFfiModule() {
2
+ return `use serde::de::DeserializeOwned;
3
+ use serde::Serialize;
4
+ use std::ffi::c_void;
5
+ use std::panic::{catch_unwind, AssertUnwindSafe};
6
+ use std::slice;
7
+
8
+ #[repr(C)]
9
+ #[derive(Clone, Copy)]
10
+ pub struct RustSlice {
11
+ pub data: *const u8,
12
+ pub len: usize,
13
+ }
14
+
15
+ #[repr(C)]
16
+ pub struct RustBuffer {
17
+ pub data: *mut u8,
18
+ pub len: usize,
19
+ pub capacity: usize,
20
+ pub is_error: bool,
21
+ }
22
+
23
+ #[repr(C)]
24
+ #[derive(Clone, Copy)]
25
+ pub struct RustCallback {
26
+ pub context: *mut c_void,
27
+ pub invoke: unsafe extern "C" fn(*mut c_void, RustSlice),
28
+ }
29
+
30
+ pub unsafe fn decode_json<T: DeserializeOwned>(value: RustSlice) -> Result<T, String> {
31
+ let bytes = if value.len == 0 {
32
+ &[]
33
+ } else {
34
+ if value.data.is_null() {
35
+ return Err("Received a null JSON buffer".to_string());
36
+ }
37
+ slice::from_raw_parts(value.data, value.len)
38
+ };
39
+ serde_json::from_slice(bytes).map_err(|error| format!("Invalid JSON input: {error}"))
40
+ }
41
+
42
+ fn into_buffer(mut bytes: Vec<u8>, is_error: bool) -> RustBuffer {
43
+ let buffer = RustBuffer {
44
+ data: bytes.as_mut_ptr(),
45
+ len: bytes.len(),
46
+ capacity: bytes.capacity(),
47
+ is_error,
48
+ };
49
+ std::mem::forget(bytes);
50
+ buffer
51
+ }
52
+
53
+ pub fn encode_result<T: Serialize>(result: Result<T, String>) -> RustBuffer {
54
+ match result {
55
+ Ok(value) => match serde_json::to_vec(&value) {
56
+ Ok(bytes) => into_buffer(bytes, false),
57
+ Err(error) => into_buffer(error.to_string().into_bytes(), true),
58
+ },
59
+ Err(error) => into_buffer(error.into_bytes(), true),
60
+ }
61
+ }
62
+
63
+ pub fn catch_json<T: Serialize>(call: impl FnOnce() -> Result<T, String>) -> RustBuffer {
64
+ let result = catch_unwind(AssertUnwindSafe(call))
65
+ .unwrap_or_else(|_| Err("Rust handler panicked".to_string()));
66
+ encode_result(result)
67
+ }
68
+
69
+ #[no_mangle]
70
+ pub extern "C" fn rnrs_buffer_free(buffer: RustBuffer) {
71
+ if !buffer.data.is_null() {
72
+ unsafe {
73
+ drop(Vec::from_raw_parts(buffer.data, buffer.len, buffer.capacity));
74
+ }
75
+ }
76
+ }
77
+ `;
78
+ }
79
+
80
+ function renderRustExports(methods) {
81
+ const lines = ['mod api;', 'mod ffi;', 'pub use ffi::{rnrs_buffer_free, RustBuffer, RustCallback, RustSlice};', ''];
82
+ for (const method of methods) {
83
+ const abiParams = method.params.map((param) => `${param.name}: ffi::${param.kind === 'callback' ? 'RustCallback' : 'RustSlice'}`).join(', ');
84
+ lines.push('#[no_mangle]');
85
+ lines.push(`pub extern "C" fn ${method.symbol}(${abiParams}) -> ffi::RustBuffer {`);
86
+ lines.push(' ffi::catch_json(|| {');
87
+
88
+ for (const param of method.params) {
89
+ if (param.kind !== 'callback') {
90
+ lines.push(` let ${param.name}: ${param.rust} = unsafe { ffi::decode_json(${param.name}) }?;`);
91
+ continue;
92
+ }
93
+ const callbackParams = param.params.map((callbackParam) => `${callbackParam.name}: ${callbackParam.rust}`).join(', ');
94
+ const callbackValues = param.params.map((callbackParam) => callbackParam.name).join(', ');
95
+ const payload = callbackValues.length === 0 ? 'Vec::<u8>::new()' : `serde_json::to_vec(&(${callbackValues}${param.params.length === 1 ? ',' : ''})).unwrap_or_default()`;
96
+ lines.push(` let mut ${param.name}_callback = |${callbackParams}| {`);
97
+ lines.push(` let payload = ${payload};`);
98
+ lines.push(` let slice = ffi::RustSlice { data: payload.as_ptr(), len: payload.len() };`);
99
+ lines.push(` unsafe { (${param.name}.invoke)(${param.name}.context, slice); }`);
100
+ lines.push(' };');
101
+ }
102
+
103
+ const callArgs = method.params.map((param) => param.kind === 'callback' ? `&mut ${param.name}_callback` : param.name).join(', ');
104
+ const call = `api::${method.rustName}::${method.rustName}(${callArgs})`;
105
+ if (method.returnType.promise) {
106
+ lines.push(` ${call}`);
107
+ } else if (method.returnType.kind === 'void') {
108
+ lines.push(` ${call};`);
109
+ lines.push(' Ok(())');
110
+ } else {
111
+ lines.push(` Ok(${call})`);
112
+ }
113
+ lines.push(' })', '}', '');
114
+ }
115
+ return lines.join('\n');
116
+ }
117
+
118
+ function renderRustHandler(method) {
119
+ const params = method.params.map((param) => {
120
+ if (param.kind === 'callback') {
121
+ const callbackTypes = param.params.map((callbackParam) => callbackParam.rust).join(', ');
122
+ return `${param.name}: &mut dyn FnMut(${callbackTypes})`;
123
+ }
124
+ return `${param.name}: ${param.rust}`;
125
+ }).join(', ');
126
+ const returnType = method.returnType.promise ? `Result<${method.returnType.rust}, String>` : method.returnType.rust;
127
+ const lines = [`// TODO: Replace the generated placeholder with the method implementation.`, `pub fn ${method.rustName}(${params}) -> ${returnType} {`];
128
+ for (const param of method.params) {
129
+ if (param.kind === 'callback') lines.push(` let _ = &mut ${param.name};`);
130
+ else lines.push(` let _ = ${param.name};`);
131
+ }
132
+ if (method.returnType.promise) lines.push(` Ok(${method.returnType.defaultValue})`);
133
+ else if (method.returnType.defaultValue !== null) lines.push(` ${method.returnType.defaultValue}`);
134
+ lines.push('}', '');
135
+ return lines.join('\n');
136
+ }
137
+
138
+ function renderRustModuleList(methods) {
139
+ return `${methods.map((method) => `pub(crate) mod ${method.rustName};`).join('\n')}\n`;
140
+ }
141
+
142
+ function renderCppHelpers() {
143
+ return `#include <exception>
144
+ #include <string>
145
+ #include <thread>
146
+ #include <vector>
147
+
148
+ struct RustBufferOwner {
149
+ RustBuffer value;
150
+ ~RustBufferOwner() { rnrs_buffer_free(value); }
151
+ };
152
+
153
+ static std::string rnrsStringify(jsi::Runtime& runtime, const jsi::Value& value) {
154
+ auto json = runtime.global().getPropertyAsObject(runtime, "JSON");
155
+ auto stringify = json.getPropertyAsFunction(runtime, "stringify");
156
+ auto result = stringify.call(runtime, value);
157
+ if (!result.isString()) throw jsi::JSError(runtime, "Value cannot be serialized as JSON");
158
+ return result.getString(runtime).utf8(runtime);
159
+ }
160
+
161
+ static jsi::Value rnrsParseJson(jsi::Runtime& runtime, const uint8_t* data, size_t length) {
162
+ auto json = runtime.global().getPropertyAsObject(runtime, "JSON");
163
+ auto parse = json.getPropertyAsFunction(runtime, "parse");
164
+ auto text = jsi::String::createFromUtf8(runtime, data, length);
165
+ return parse.call(runtime, std::move(text));
166
+ }
167
+
168
+ static jsi::Value rnrsFromRust(jsi::Runtime& runtime, RustBuffer buffer) {
169
+ RustBufferOwner owner{buffer};
170
+ if (buffer.is_error) {
171
+ std::string message(reinterpret_cast<const char*>(buffer.data), buffer.len);
172
+ throw jsi::JSError(runtime, message);
173
+ }
174
+ return rnrsParseJson(runtime, buffer.data, buffer.len);
175
+ }
176
+
177
+ struct RustCallbackContext {
178
+ jsi::Runtime* runtime;
179
+ jsi::Function* function;
180
+ std::exception_ptr exception;
181
+ };
182
+
183
+ extern "C" void rnrsDispatchCallback(void* context, RustSlice payload) noexcept {
184
+ auto* callback = static_cast<RustCallbackContext*>(context);
185
+ try {
186
+ auto value = rnrsParseJson(*callback->runtime, payload.data, payload.len);
187
+ auto array = value.asObject(*callback->runtime).asArray(*callback->runtime);
188
+ std::vector<jsi::Value> arguments;
189
+ arguments.reserve(array.size(*callback->runtime));
190
+ for (size_t index = 0; index < array.size(*callback->runtime); index++) {
191
+ arguments.push_back(array.getValueAtIndex(*callback->runtime, index));
192
+ }
193
+ callback->function->call(*callback->runtime, static_cast<const jsi::Value*>(arguments.data()), arguments.size());
194
+ } catch (...) {
195
+ callback->exception = std::current_exception();
196
+ }
197
+ }
198
+
199
+ template <typename Work>
200
+ static jsi::Value rnrsMakePromise(
201
+ jsi::Runtime& runtime,
202
+ std::shared_ptr<CallInvoker> jsInvoker,
203
+ Work work) {
204
+ auto promiseConstructor = runtime.global().getPropertyAsFunction(runtime, "Promise");
205
+ auto executor = jsi::Function::createFromHostFunction(
206
+ runtime,
207
+ jsi::PropNameID::forAscii(runtime, "reactNativeRustExecutor"),
208
+ 2,
209
+ [jsInvoker = std::move(jsInvoker), work = std::move(work)](
210
+ jsi::Runtime& executorRuntime,
211
+ const jsi::Value&,
212
+ const jsi::Value* arguments,
213
+ size_t count) mutable -> jsi::Value {
214
+ if (count != 2) throw jsi::JSError(executorRuntime, "Promise executor requires resolve and reject");
215
+ auto resolve = std::make_shared<jsi::Function>(arguments[0].asObject(executorRuntime).asFunction(executorRuntime));
216
+ auto reject = std::make_shared<jsi::Function>(arguments[1].asObject(executorRuntime).asFunction(executorRuntime));
217
+ std::thread([
218
+ jsInvoker,
219
+ work = std::move(work),
220
+ resolve,
221
+ reject]() mutable {
222
+ RustBuffer result = work();
223
+ jsInvoker->invokeAsync([
224
+ result,
225
+ resolve,
226
+ reject](jsi::Runtime& callbackRuntime) mutable {
227
+ if (result.is_error) {
228
+ std::string message(reinterpret_cast<const char*>(result.data), result.len);
229
+ rnrs_buffer_free(result);
230
+ reject->call(callbackRuntime, jsi::String::createFromUtf8(callbackRuntime, message));
231
+ return;
232
+ }
233
+ try {
234
+ auto value = rnrsFromRust(callbackRuntime, result);
235
+ resolve->call(callbackRuntime, std::move(value));
236
+ } catch (...) {
237
+ reject->call(callbackRuntime, jsi::String::createFromUtf8(callbackRuntime, "Could not decode Rust Promise result"));
238
+ }
239
+ });
240
+ }).detach();
241
+ return jsi::Value::undefined();
242
+ });
243
+ return promiseConstructor.callAsConstructor(runtime, std::move(executor));
244
+ }`;
245
+ }
246
+
247
+ function renderCppMethods(methods, moduleName) {
248
+ const cppArgName = (index) => `rnrsArg${index}`;
249
+ const cppType = (method) => method.returnType.promise ? 'jsi::Value' : method.returnType.cpp;
250
+ const declarations = methods.map((method) => {
251
+ const params = method.params.map((param, index) => `${param.cpp} ${cppArgName(index)}`);
252
+ return ` ${cppType(method)} ${method.name}(jsi::Runtime& rnrsRuntime${params.length ? `, ${params.join(', ')}` : ''});`;
253
+ });
254
+
255
+ const definitions = methods.map((method) => {
256
+ const params = method.params.map((param, index) => `${param.cpp} ${cppArgName(index)}`);
257
+ const signature = `${cppType(method)} ${moduleName}Impl::${method.name}(\n jsi::Runtime& rnrsRuntime${params.length ? `,\n ${params.join(',\n ')}` : ''}\n)`;
258
+ const jsonArgs = [];
259
+ const callbackArgs = [];
260
+ const setup = [];
261
+ method.params.forEach((param, index) => {
262
+ const name = cppArgName(index);
263
+ if (param.kind === 'callback') {
264
+ const context = `rnrsCallbackContext${index}`;
265
+ const bridge = `rnrsCallbackBridge${index}`;
266
+ setup.push(` RustCallbackContext ${context}{&rnrsRuntime, &${name}, nullptr};`);
267
+ setup.push(` RustCallback ${bridge}{&${context}, &rnrsDispatchCallback};`);
268
+ callbackArgs.push({ name, context, bridge });
269
+ return;
270
+ }
271
+ const json = `rnrsJson${index}`;
272
+ const slice = `rnrsSlice${index}`;
273
+ setup.push(` auto ${json} = rnrsStringify(rnrsRuntime, jsi::Value(rnrsRuntime, std::move(${name})));`);
274
+ setup.push(` RustSlice ${slice}{reinterpret_cast<const uint8_t*>(${json}.data()), ${json}.size()};`);
275
+ jsonArgs.push({ json, slice });
276
+ });
277
+
278
+ const ffiArgs = method.params.map((param, index) => (
279
+ param.kind === 'callback' ? `rnrsCallbackBridge${index}` : `rnrsSlice${index}`
280
+ ));
281
+ if (method.returnType.promise) {
282
+ const captures = jsonArgs.map(({ json }) => `${json} = std::move(${json})`);
283
+ const workerLines = [];
284
+ method.params.forEach((param, index) => {
285
+ if (param.kind !== 'callback') {
286
+ const json = `rnrsJson${index}`;
287
+ workerLines.push(` RustSlice rnrsSlice${index}{reinterpret_cast<const uint8_t*>(${json}.data()), ${json}.size()};`);
288
+ }
289
+ });
290
+ workerLines.push(` return ${method.symbol}(${ffiArgs.join(', ')});`);
291
+ return [signature + ' {', ...setup, ` return rnrsMakePromise(rnrsRuntime, jsInvoker_, [${captures.join(', ')}]() mutable {`, ...workerLines, ' });', '}'].join('\n');
292
+ }
293
+
294
+ const call = `auto rnrsResult = ${method.symbol}(${ffiArgs.join(', ')});`;
295
+ const callbackErrorChecks = callbackArgs.map(({ context }) => ` if (${context}.exception) { rnrs_buffer_free(rnrsResult); std::rethrow_exception(${context}.exception); }`);
296
+ const decoded = 'auto rnrsValue = rnrsFromRust(rnrsRuntime, rnrsResult);';
297
+ let resultLine;
298
+ switch (method.returnType.kind) {
299
+ case 'void': resultLine = ' (void)rnrsValue;\n return;'; break;
300
+ case 'number': resultLine = ' return rnrsValue.asNumber();'; break;
301
+ case 'boolean': resultLine = ' return rnrsValue.asBool();'; break;
302
+ case 'string': resultLine = ' return rnrsValue.asString(rnrsRuntime);'; break;
303
+ case 'array': resultLine = ' return rnrsValue.asObject(rnrsRuntime).asArray(rnrsRuntime);'; break;
304
+ case 'json':
305
+ if (method.returnType.cpp === 'jsi::String') resultLine = ' return rnrsValue.asString(rnrsRuntime);';
306
+ else if (method.returnType.cpp === 'jsi::Array') resultLine = ' return rnrsValue.asObject(rnrsRuntime).asArray(rnrsRuntime);';
307
+ else resultLine = ' return rnrsValue.asObject(rnrsRuntime);';
308
+ break;
309
+ default: throw new Error(`No C++ return conversion for ${method.returnType.kind}.`);
310
+ }
311
+ return [signature + ' {', ...setup, ` ${call}`, ...callbackErrorChecks, ` ${decoded}`, resultLine, '}'].join('\n');
312
+ });
313
+ return {
314
+ header: declarations.join('\n'),
315
+ source: `${renderCppHelpers()}\n\n${definitions.join('\n\n')}`,
316
+ };
317
+ }
318
+
319
+ function renderWrappers(methods, moduleName) {
320
+ const nativeLines = [`import ${moduleName} from './Native${moduleName}';`, ''];
321
+ const fallbackLines = [];
322
+ const usesUnsafeObject = methods.some((method) => (
323
+ method.returnType.typescript.includes('UnsafeObject')
324
+ || method.params.some((param) => param.typescript.includes('UnsafeObject'))
325
+ ));
326
+ if (usesUnsafeObject) {
327
+ const codegenTypesDeclaration = 'declare namespace CodegenTypes { type UnsafeObject = object; }';
328
+ nativeLines.unshift(codegenTypesDeclaration);
329
+ fallbackLines.push(codegenTypesDeclaration, '');
330
+ }
331
+ for (const method of methods) {
332
+ const tsParams = method.params.map((param) => `${param.name}: ${param.typescript}`).join(', ');
333
+ const args = method.params.map((param) => param.name).join(', ');
334
+ const nativeCall = `${moduleName}.${method.name}(${args})`;
335
+ nativeLines.push(`export function ${method.name}(${tsParams}): ${method.returnType.typescript} {`);
336
+ nativeLines.push(method.returnType.typescript === 'void' ? ` ${nativeCall};` : ` return ${nativeCall};`);
337
+ nativeLines.push('}', '');
338
+
339
+ const fallbackParams = method.params.map((param) => `_${param.name}: ${param.typescript}`).join(', ');
340
+ fallbackLines.push(`export function ${method.name}(${fallbackParams}): ${method.returnType.typescript} {`);
341
+ fallbackLines.push(" throw new Error('This method is only supported on native platforms.');", '}', '');
342
+ }
343
+ return {
344
+ native: nativeLines.join('\n'),
345
+ fallback: fallbackLines.join('\n'),
346
+ };
347
+ }
348
+
349
+ module.exports = {
350
+ renderRustFfiModule,
351
+ renderRustExports,
352
+ renderRustHandler,
353
+ renderRustModuleList,
354
+ renderCppHelpers,
355
+ renderCppMethods,
356
+ renderWrappers,
357
+ };
@@ -0,0 +1,156 @@
1
+ const { supportedTypes, rustKeywords, cppKeywords, ts } = require('./constants');
2
+
3
+ function typeInfo(typeNode, sourceFile, methodName, role) {
4
+ const info = supportedTypes.get(typeNode.kind);
5
+ if (info && !(role === 'parameter' && typeNode.kind === ts.SyntaxKind.VoidKeyword)) return info;
6
+
7
+ if (role === 'return' && ts.isTypeReferenceNode(typeNode) && typeNode.typeName.getText(sourceFile) === 'Promise') {
8
+ if (typeNode.typeArguments?.length !== 1) throw new Error(`Promise return in ${methodName}() must declare one result type.`);
9
+ const result = typeInfo(typeNode.typeArguments[0], sourceFile, methodName, 'promise result');
10
+ if (result.kind === 'callback' || result.promise) throw new Error(`Nested Promise or callback results are not supported in ${methodName}().`);
11
+ return { ...result, typescript: typeNode.getText(sourceFile), promise: true };
12
+ }
13
+
14
+ const typeName = ts.isTypeReferenceNode(typeNode) ? typeNode.typeName.getText(sourceFile) : '';
15
+ const objectType = typeNode.kind === ts.SyntaxKind.ObjectKeyword
16
+ || ['CodegenTypes.UnsafeObject', 'UnsafeObject'].includes(typeName);
17
+ if (objectType) {
18
+ return {
19
+ kind: 'json',
20
+ typescript: typeNode.getText(sourceFile),
21
+ rust: 'serde_json::Value',
22
+ cpp: 'jsi::Object',
23
+ defaultValue: 'serde_json::json!({})',
24
+ };
25
+ }
26
+
27
+ const arrayElement = ts.isArrayTypeNode(typeNode)
28
+ ? typeNode.elementType
29
+ : ts.isTypeReferenceNode(typeNode)
30
+ && ['Array', 'ReadonlyArray'].includes(typeNode.typeName.getText(sourceFile))
31
+ && typeNode.typeArguments?.length === 1
32
+ ? typeNode.typeArguments[0]
33
+ : null;
34
+ if (arrayElement) {
35
+ const elementType = typeInfo(arrayElement, sourceFile, methodName, 'array element');
36
+ if (elementType.kind === 'callback' || elementType.promise || elementType.kind === 'void') {
37
+ throw new Error(`Unsupported array element type in ${methodName}().`);
38
+ }
39
+ return {
40
+ kind: 'json',
41
+ typescript: typeNode.getText(sourceFile),
42
+ rust: 'serde_json::Value',
43
+ cpp: 'jsi::Array',
44
+ defaultValue: 'serde_json::json!([])',
45
+ };
46
+ }
47
+
48
+ const text = typeNode.getText(sourceFile);
49
+ throw new Error(`Unsupported ${role} type "${text}" in ${methodName}(). Supported types are number, boolean, string, arrays, and CodegenTypes.UnsafeObject${role === 'return' ? ', plus void and Promise<T>' : ''}.`);
50
+ }
51
+
52
+ function callbackInfo(typeNode, sourceFile, methodName) {
53
+ if (!ts.isFunctionTypeNode(typeNode) || typeNode.typeParameters?.length) {
54
+ throw new Error(`Unsupported callback signature in ${methodName}().`);
55
+ }
56
+ if (typeNode.type.kind !== ts.SyntaxKind.VoidKeyword || typeNode.parameters.length > 4) {
57
+ throw new Error(`Callbacks in ${methodName}() must return void and have at most four parameters.`);
58
+ }
59
+ const params = typeNode.parameters.map((parameter) => {
60
+ if (!ts.isIdentifier(parameter.name) || parameter.questionToken || parameter.dotDotDotToken || !parameter.type) {
61
+ throw new Error(`Callback parameters in ${methodName}() must be named, required, and explicitly typed.`);
62
+ }
63
+ const info = typeInfo(parameter.type, sourceFile, methodName, 'callback parameter');
64
+ if (info.promise || info.kind === 'callback' || info.kind === 'void') {
65
+ throw new Error(`Unsupported callback parameter type in ${methodName}().`);
66
+ }
67
+ return { name: parameter.name.text, ...info };
68
+ });
69
+ return { kind: 'callback', typescript: typeNode.getText(sourceFile), cpp: 'jsi::Function', params };
70
+ }
71
+
72
+ function toSnakeCase(name) {
73
+ return name
74
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
75
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
76
+ .toLowerCase();
77
+ }
78
+
79
+ function validateIdentifier(name, location) {
80
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name) || rustKeywords.has(name) || cppKeywords.has(name)) {
81
+ throw new Error(`Unsupported identifier "${name}" in ${location}; use a non-keyword identifier.`);
82
+ }
83
+ }
84
+
85
+ function parseSpec(sourceText, fileName = 'NativeModule.ts') {
86
+ const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
87
+ if (sourceFile.parseDiagnostics.length > 0) {
88
+ const diagnostic = sourceFile.parseDiagnostics[0];
89
+ throw new Error(`Could not parse ${fileName}: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`);
90
+ }
91
+
92
+ const spec = sourceFile.statements.find((statement) => (
93
+ ts.isInterfaceDeclaration(statement) && statement.name.text === 'Spec'
94
+ ));
95
+ if (!spec) throw new Error(`No "Spec" interface was found in ${fileName}.`);
96
+ if (spec.typeParameters?.length) throw new Error('Generic TurboModule Spec interfaces are not supported.');
97
+ if (spec.members.length === 0) throw new Error('The Spec interface must declare at least one method.');
98
+
99
+ const names = new Set();
100
+ const rustNames = new Set();
101
+ const methods = spec.members.map((member) => {
102
+ if (!ts.isMethodSignature(member)) {
103
+ throw new Error('Only method signatures are supported in the Spec interface.');
104
+ }
105
+ if (!member.name || !ts.isIdentifier(member.name) || member.questionToken || member.typeParameters?.length) {
106
+ throw new Error('Optional, computed, and generic Spec methods are not supported.');
107
+ }
108
+
109
+ const name = member.name.text;
110
+ validateIdentifier(name, 'Spec method');
111
+ if (names.has(name)) throw new Error(`Overloaded method "${name}" is not supported.`);
112
+ names.add(name);
113
+ const rustName = toSnakeCase(name);
114
+ validateIdentifier(rustName, `Rust mapping for ${name}()`);
115
+ if (rustNames.has(rustName)) throw new Error(`Multiple Spec methods map to the Rust name "${rustName}".`);
116
+ rustNames.add(rustName);
117
+
118
+ const params = member.parameters.map((parameter) => {
119
+ if (!ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.questionToken || parameter.initializer) {
120
+ throw new Error(`Rest, optional, and destructured parameters are not supported in ${name}().`);
121
+ }
122
+ const paramName = parameter.name.text;
123
+ validateIdentifier(paramName, `parameter in ${name}()`);
124
+ if (!parameter.type) throw new Error(`Parameter "${paramName}" in ${name}() needs an explicit type.`);
125
+ const info = ts.isFunctionTypeNode(parameter.type)
126
+ ? callbackInfo(parameter.type, sourceFile, name)
127
+ : typeInfo(parameter.type, sourceFile, name, 'parameter');
128
+ return { name: paramName, ...info };
129
+ });
130
+ if (new Set(params.map((param) => param.name)).size !== params.length) {
131
+ throw new Error(`Duplicate parameter names are not supported in ${name}().`);
132
+ }
133
+ if (!member.type) throw new Error(`Method "${name}()" needs an explicit return type.`);
134
+ return {
135
+ name,
136
+ rustName,
137
+ symbol: `rnrs_${rustName}`,
138
+ params,
139
+ returnType: typeInfo(member.type, sourceFile, name, 'return'),
140
+ };
141
+ });
142
+ for (const method of methods) {
143
+ if (method.returnType.promise && method.params.some((param) => param.kind === 'callback')) {
144
+ throw new Error(`Callbacks in Promise method ${method.name}() are not supported. Use a Codegen event emitter for asynchronous callbacks.`);
145
+ }
146
+ }
147
+ return methods;
148
+ }
149
+
150
+ module.exports = {
151
+ typeInfo,
152
+ callbackInfo,
153
+ toSnakeCase,
154
+ validateIdentifier,
155
+ parseSpec,
156
+ };
package/bin/codegen.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./codegen/index');