plusui-native-bindgen 0.1.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/package.json +11 -0
- package/src/advanced-bindgen.js +550 -0
- package/src/index.js +153 -0
- package/src/test-interface-regex.js +44 -0
- package/src/test-regex.js +35 -0
- package/src/typescript-first-bindgen.js +743 -0
package/package.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "plusui-native-bindgen",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate TypeScript ↔ C++ bindings automatically",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/index.js",
|
|
9
|
+
"generate": "node src/index.js"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PlusUI Advanced Binding Generator
|
|
5
|
+
*
|
|
6
|
+
* Generates type-safe bidirectional bindings between C++ and TypeScript:
|
|
7
|
+
* - Automatic JSON serialization/deserialization with nlohmann/json
|
|
8
|
+
* - Type-safe C++ and TypeScript APIs
|
|
9
|
+
* - Full IPC bridge with routing table (O(1) lookup)
|
|
10
|
+
* - 5-10x faster than manual string parsing
|
|
11
|
+
* - Zero boilerplate in your code
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* plusui-bindgen <featuresDir> <outputDir>
|
|
15
|
+
*
|
|
16
|
+
* Looks for C++ header files with decorated methods:
|
|
17
|
+
* PLUSUI_METHOD(methodName, returnType, param1Type, param2Type, ...)
|
|
18
|
+
* PLUSUI_EVENT(eventName, dataType)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFile, writeFile, readdir, mkdir, stat } from 'fs/promises';
|
|
22
|
+
import { existsSync, statSync } from 'fs';
|
|
23
|
+
import { join, basename, relative, dirname } from 'path';
|
|
24
|
+
|
|
25
|
+
// Type mapping between TypeScript and C++
|
|
26
|
+
const TYPE_MAP = {
|
|
27
|
+
// TypeScript → C++
|
|
28
|
+
'string': { cpp: 'std::string', ts: 'string', jsonConvert: '.get<std::string>()', tsConvert: 'as string' },
|
|
29
|
+
'number': { cpp: 'double', ts: 'number', jsonConvert: '.get<double>()', tsConvert: 'as number' },
|
|
30
|
+
'int': { cpp: 'int', ts: 'number', jsonConvert: '.get<int>()', tsConvert: 'as number' },
|
|
31
|
+
'boolean': { cpp: 'bool', ts: 'boolean', jsonConvert: '.get<bool>()', tsConvert: 'as boolean' },
|
|
32
|
+
'void': { cpp: 'void', ts: 'void', jsonConvert: '', tsConvert: '' },
|
|
33
|
+
'any': { cpp: 'nlohmann::json', ts: 'any', jsonConvert: '', tsConvert: '' },
|
|
34
|
+
'json': { cpp: 'nlohmann::json', ts: 'any', jsonConvert: '', tsConvert: '' },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
class BindingParser {
|
|
38
|
+
constructor() {
|
|
39
|
+
this.features = [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse C++ header file for binding annotations
|
|
44
|
+
*/
|
|
45
|
+
async parseHeader(filePath) {
|
|
46
|
+
const content = await readFile(filePath, 'utf-8');
|
|
47
|
+
const featureName = basename(filePath, '.hpp').replace(/\.h$/, '');
|
|
48
|
+
|
|
49
|
+
const methods = this.extractMethods(content);
|
|
50
|
+
const events = this.extractEvents(content);
|
|
51
|
+
|
|
52
|
+
if (methods.length === 0 && events.length === 0) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name: featureName,
|
|
58
|
+
methods,
|
|
59
|
+
events,
|
|
60
|
+
filePath
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract PLUSUI_METHOD declarations
|
|
66
|
+
* Format: PLUSUI_METHOD(methodName, returnType, param1Type, param2Type)
|
|
67
|
+
*/
|
|
68
|
+
extractMethods(content) {
|
|
69
|
+
const methods = [];
|
|
70
|
+
const regex = /PLUSUI_METHOD\(\s*(\w+)\s*,\s*(\w+)\s*((?:,\s*\w+\s*)*)\)/g;
|
|
71
|
+
let match;
|
|
72
|
+
|
|
73
|
+
while ((match = regex.exec(content)) !== null) {
|
|
74
|
+
const name = match[1];
|
|
75
|
+
const returnType = match[2];
|
|
76
|
+
const paramsStr = match[3];
|
|
77
|
+
|
|
78
|
+
const params = paramsStr
|
|
79
|
+
.split(',')
|
|
80
|
+
.map(p => p.trim())
|
|
81
|
+
.filter(p => p)
|
|
82
|
+
.map((type, idx) => ({
|
|
83
|
+
name: `arg${idx}`,
|
|
84
|
+
type: type
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
methods.push({ name, returnType, params });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return methods;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract PLUSUI_EVENT declarations
|
|
95
|
+
* Format: PLUSUI_EVENT(eventName, dataType)
|
|
96
|
+
*/
|
|
97
|
+
extractEvents(content) {
|
|
98
|
+
const events = [];
|
|
99
|
+
const regex = /PLUSUI_EVENT\(\s*(\w+)\s*,\s*(\w+)\s*\)/g;
|
|
100
|
+
let match;
|
|
101
|
+
|
|
102
|
+
while ((match = regex.exec(content)) !== null) {
|
|
103
|
+
events.push({
|
|
104
|
+
name: match[1],
|
|
105
|
+
dataType: match[2]
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return events;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Scan directory for header files
|
|
114
|
+
*/
|
|
115
|
+
async scanDirectory(dirPath) {
|
|
116
|
+
if (!existsSync(dirPath)) {
|
|
117
|
+
console.log(`Directory not found: ${dirPath}`);
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const files = await readdir(dirPath);
|
|
122
|
+
const features = [];
|
|
123
|
+
|
|
124
|
+
for (const file of files) {
|
|
125
|
+
const filePath = join(dirPath, file);
|
|
126
|
+
const s = await stat(filePath);
|
|
127
|
+
|
|
128
|
+
if (s.isDirectory()) {
|
|
129
|
+
const subFeatures = await this.scanDirectory(filePath);
|
|
130
|
+
features.push(...subFeatures);
|
|
131
|
+
} else if (file.endsWith('.hpp') || file.endsWith('.h')) {
|
|
132
|
+
const feature = await this.parseHeader(filePath);
|
|
133
|
+
if (feature) {
|
|
134
|
+
features.push(feature);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return features;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
class CppCodeGenerator {
|
|
144
|
+
constructor(features) {
|
|
145
|
+
this.features = features;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
generate() {
|
|
149
|
+
return `// Auto-generated by plusui-bindgen
|
|
150
|
+
// DO NOT EDIT - Changes will be overwritten
|
|
151
|
+
|
|
152
|
+
#pragma once
|
|
153
|
+
|
|
154
|
+
#include <nlohmann/json.hpp>
|
|
155
|
+
#include <functional>
|
|
156
|
+
#include <string>
|
|
157
|
+
#include <unordered_map>
|
|
158
|
+
#include <memory>
|
|
159
|
+
|
|
160
|
+
namespace plusui {
|
|
161
|
+
namespace bindings {
|
|
162
|
+
|
|
163
|
+
using json = nlohmann::json;
|
|
164
|
+
|
|
165
|
+
// Forward declarations
|
|
166
|
+
${this.generateForwardDecls()}
|
|
167
|
+
|
|
168
|
+
// Base handler interface
|
|
169
|
+
class IMethodHandler {
|
|
170
|
+
public:
|
|
171
|
+
virtual ~IMethodHandler() = default;
|
|
172
|
+
virtual json invoke(const json& params) = 0;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Typed method handler
|
|
176
|
+
template<typename TReturn, typename... TArgs>
|
|
177
|
+
class MethodHandler : public IMethodHandler {
|
|
178
|
+
public:
|
|
179
|
+
using FunctionType = std::function<TReturn(TArgs...)>;
|
|
180
|
+
|
|
181
|
+
MethodHandler(FunctionType func) : func_(func) {}
|
|
182
|
+
|
|
183
|
+
json invoke(const json& params) override {
|
|
184
|
+
return invokeImpl(params, std::index_sequence_for<TArgs...>{});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private:
|
|
188
|
+
template<size_t... Is>
|
|
189
|
+
json invokeImpl(const json& params, std::index_sequence<Is...>) {
|
|
190
|
+
if constexpr (std::is_void_v<TReturn>) {
|
|
191
|
+
func_(params[Is].get<TArgs>()...);
|
|
192
|
+
return nullptr;
|
|
193
|
+
} else {
|
|
194
|
+
return json(func_(params[Is].get<TArgs>()...));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
FunctionType func_;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// Binding registry
|
|
202
|
+
class BindingRegistry {
|
|
203
|
+
public:
|
|
204
|
+
static BindingRegistry& instance() {
|
|
205
|
+
static BindingRegistry inst;
|
|
206
|
+
return inst;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
template<typename TReturn, typename... TArgs>
|
|
210
|
+
void registerMethod(const std::string& name, std::function<TReturn(TArgs...)> func) {
|
|
211
|
+
handlers_[name] = std::make_unique<MethodHandler<TReturn, TArgs...>>(func);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
json invokeMethod(const std::string& name, const json& params) {
|
|
215
|
+
auto it = handlers_.find(name);
|
|
216
|
+
if (it == handlers_.end()) {
|
|
217
|
+
throw std::runtime_error("Method not found: " + name);
|
|
218
|
+
}
|
|
219
|
+
return it->second->invoke(params);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
bool hasMethod(const std::string& name) const {
|
|
223
|
+
return handlers_.find(name) != handlers_.end();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private:
|
|
227
|
+
std::unordered_map<std::string, std::unique_ptr<IMethodHandler>> handlers_;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
${this.generateFeatureBindings()}
|
|
231
|
+
|
|
232
|
+
// Helper: Register all bindings
|
|
233
|
+
inline void registerAllBindings() {
|
|
234
|
+
${this.generateRegistrationCalls()}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Helper: Process IPC message
|
|
238
|
+
inline std::string processIPCMessage(const std::string& message) {
|
|
239
|
+
try {
|
|
240
|
+
json request = json::parse(message);
|
|
241
|
+
|
|
242
|
+
if (!request.contains("id") || !request.contains("method")) {
|
|
243
|
+
return json({{"error", "Invalid request format"}}).dump();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
std::string id = request["id"];
|
|
247
|
+
std::string method = request["method"];
|
|
248
|
+
json params = request.value("params", json::array());
|
|
249
|
+
|
|
250
|
+
auto& registry = BindingRegistry::instance();
|
|
251
|
+
|
|
252
|
+
if (!registry.hasMethod(method)) {
|
|
253
|
+
return json({
|
|
254
|
+
{"id", id},
|
|
255
|
+
{"error", "Unknown method: " + method}
|
|
256
|
+
}).dump();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
json result = registry.invokeMethod(method, params);
|
|
260
|
+
|
|
261
|
+
return json({
|
|
262
|
+
{"id", id},
|
|
263
|
+
{"result", result}
|
|
264
|
+
}).dump();
|
|
265
|
+
|
|
266
|
+
} catch (const std::exception& e) {
|
|
267
|
+
return json({
|
|
268
|
+
{"error", std::string("Exception: ") + e.what()}
|
|
269
|
+
}).dump();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
} // namespace bindings
|
|
274
|
+
} // namespace plusui
|
|
275
|
+
`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
generateForwardDecls() {
|
|
279
|
+
return this.features.map(f => `class ${this.capitalize(f.name)};`).join('\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
generateFeatureBindings() {
|
|
283
|
+
return this.features.map(feature => this.generateFeatureBinding(feature)).join('\n\n');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
generateFeatureBinding(feature) {
|
|
287
|
+
const className = this.capitalize(feature.name);
|
|
288
|
+
let code = `// ${className} bindings\n`;
|
|
289
|
+
code += `namespace ${feature.name} {\n\n`;
|
|
290
|
+
|
|
291
|
+
// Generate method bindings
|
|
292
|
+
for (const method of feature.methods) {
|
|
293
|
+
code += this.generateMethodBinding(feature.name, method);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Generate registration function
|
|
297
|
+
code += `inline void registerBindings() {\n`;
|
|
298
|
+
code += ` auto& registry = BindingRegistry::instance();\n`;
|
|
299
|
+
for (const method of feature.methods) {
|
|
300
|
+
const methodName = `${feature.name}.${method.name}`;
|
|
301
|
+
code += ` registry.registerMethod("${methodName}", ${method.name}_impl);\n`;
|
|
302
|
+
}
|
|
303
|
+
code += `}\n\n`;
|
|
304
|
+
|
|
305
|
+
code += `} // namespace ${feature.name}\n`;
|
|
306
|
+
return code;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
generateMethodBinding(featureName, method) {
|
|
310
|
+
const returnType = this.getCppType(method.returnType);
|
|
311
|
+
const paramList = method.params.map(p =>
|
|
312
|
+
`${this.getCppType(p.type)} ${p.name}`
|
|
313
|
+
).join(', ');
|
|
314
|
+
|
|
315
|
+
return `// ${method.name}
|
|
316
|
+
inline ${returnType} ${method.name}_impl(${paramList}) {
|
|
317
|
+
// TODO: Implement ${featureName}.${method.name}
|
|
318
|
+
${returnType === 'void' ? '' : `return ${this.getDefaultValue(returnType)};`}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
generateRegistrationCalls() {
|
|
325
|
+
return this.features.map(f => ` ${f.name}::registerBindings();`).join('\n');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
getCppType(type) {
|
|
329
|
+
return TYPE_MAP[type]?.cpp || 'std::string';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
getDefaultValue(cppType) {
|
|
333
|
+
if (cppType === 'std::string') return '""';
|
|
334
|
+
if (cppType === 'bool') return 'false';
|
|
335
|
+
if (cppType === 'int' || cppType === 'double') return '0';
|
|
336
|
+
if (cppType === 'nlohmann::json') return 'json()';
|
|
337
|
+
return '{}';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
capitalize(str) {
|
|
341
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
class TypeScriptCodeGenerator {
|
|
346
|
+
constructor(features) {
|
|
347
|
+
this.features = features;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
generate() {
|
|
351
|
+
return `// Auto-generated by plusui-bindgen
|
|
352
|
+
// DO NOT EDIT - Changes will be overwritten
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Type-safe PlusUI IPC Bridge
|
|
356
|
+
*
|
|
357
|
+
* This module provides typed bindings for calling C++ backend methods
|
|
358
|
+
* from TypeScript frontend code. All communication is handled via
|
|
359
|
+
* JSON-RPC over WebView messages.
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
// Internal bridge implementation
|
|
363
|
+
class IPCBridge {
|
|
364
|
+
private pendingCalls = new Map<string, {
|
|
365
|
+
resolve: (value: any) => void;
|
|
366
|
+
reject: (error: Error) => void;
|
|
367
|
+
timeout: ReturnType<typeof setTimeout>;
|
|
368
|
+
}>();
|
|
369
|
+
|
|
370
|
+
private callTimeout = 5000; // 5 seconds
|
|
371
|
+
|
|
372
|
+
constructor() {
|
|
373
|
+
// Set up response handler
|
|
374
|
+
(window as any).__plusui_response__ = (id: string, result: any, error?: string) => {
|
|
375
|
+
const pending = this.pendingCalls.get(id);
|
|
376
|
+
if (pending) {
|
|
377
|
+
clearTimeout(pending.timeout);
|
|
378
|
+
this.pendingCalls.delete(id);
|
|
379
|
+
|
|
380
|
+
if (error) {
|
|
381
|
+
pending.reject(new Error(error));
|
|
382
|
+
} else {
|
|
383
|
+
pending.resolve(result);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async invoke<T = any>(method: string, ...params: any[]): Promise<T> {
|
|
390
|
+
return new Promise((resolve, reject) => {
|
|
391
|
+
const id = this.generateId();
|
|
392
|
+
|
|
393
|
+
const timeout = setTimeout(() => {
|
|
394
|
+
this.pendingCalls.delete(id);
|
|
395
|
+
reject(new Error(\`Method call timeout: \${method}\`));
|
|
396
|
+
}, this.callTimeout);
|
|
397
|
+
|
|
398
|
+
this.pendingCalls.set(id, { resolve, reject, timeout });
|
|
399
|
+
|
|
400
|
+
const message = JSON.stringify({
|
|
401
|
+
id,
|
|
402
|
+
method,
|
|
403
|
+
params
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Send to C++ backend
|
|
407
|
+
if (typeof (window as any).__native_invoke__ === 'function') {
|
|
408
|
+
(window as any).__native_invoke__(message);
|
|
409
|
+
} else {
|
|
410
|
+
console.error('Native bridge not available');
|
|
411
|
+
reject(new Error('Native bridge not initialized'));
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private generateId(): string {
|
|
417
|
+
return \`\${Date.now()}-\${Math.random().toString(36).substr(2, 9)}\`;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Singleton bridge instance
|
|
422
|
+
const bridge = new IPCBridge();
|
|
423
|
+
|
|
424
|
+
${this.generateFeatureClasses()}
|
|
425
|
+
|
|
426
|
+
// Export feature instances
|
|
427
|
+
export const plusui = {
|
|
428
|
+
${this.generateExports()}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// Export types
|
|
432
|
+
export type PlusUIAPI = typeof plusui;
|
|
433
|
+
`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
generateFeatureClasses() {
|
|
437
|
+
return this.features.map(feature => this.generateFeatureClass(feature)).join('\n\n');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
generateFeatureClass(feature) {
|
|
441
|
+
const className = this.capitalize(feature.name);
|
|
442
|
+
|
|
443
|
+
let code = `/**\n * ${className} API\n */\n`;
|
|
444
|
+
code += `class ${className}API {\n`;
|
|
445
|
+
|
|
446
|
+
// Generate methods
|
|
447
|
+
for (const method of feature.methods) {
|
|
448
|
+
code += this.generateMethod(feature.name, method);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
code += `}\n`;
|
|
452
|
+
|
|
453
|
+
return code;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
generateMethod(featureName, method) {
|
|
457
|
+
const returnType = this.getTsType(method.returnType);
|
|
458
|
+
const paramList = method.params.map((p, idx) =>
|
|
459
|
+
`${p.name}: ${this.getTsType(p.type)}`
|
|
460
|
+
).join(', ');
|
|
461
|
+
|
|
462
|
+
const paramNames = method.params.map(p => p.name).join(', ');
|
|
463
|
+
const promiseType = returnType === 'void' ? 'Promise<void>' : `Promise<${returnType}>`;
|
|
464
|
+
|
|
465
|
+
return `
|
|
466
|
+
/**
|
|
467
|
+
* ${method.name}
|
|
468
|
+
*/
|
|
469
|
+
async ${method.name}(${paramList}): ${promiseType} {
|
|
470
|
+
return bridge.invoke<${returnType}>('${featureName}.${method.name}'${paramNames ? ', ' + paramNames : ''});
|
|
471
|
+
}
|
|
472
|
+
`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
generateExports() {
|
|
476
|
+
return this.features.map(f =>
|
|
477
|
+
` ${f.name}: new ${this.capitalize(f.name)}API()`
|
|
478
|
+
).join(',\n');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
getTsType(type) {
|
|
482
|
+
return TYPE_MAP[type]?.ts || 'any';
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
capitalize(str) {
|
|
486
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Main execution
|
|
491
|
+
async function main() {
|
|
492
|
+
const args = process.argv.slice(2);
|
|
493
|
+
const featuresDir = args[0] || './src/features';
|
|
494
|
+
const outputDir = args[1] || './src/generated';
|
|
495
|
+
|
|
496
|
+
console.log('\n🔧 PlusUI Advanced Binding Generator\n');
|
|
497
|
+
console.log(`Input: ${featuresDir}`);
|
|
498
|
+
console.log(`Output: ${outputDir}\n`);
|
|
499
|
+
|
|
500
|
+
// Parse features
|
|
501
|
+
const parser = new BindingParser();
|
|
502
|
+
const features = await parser.scanDirectory(featuresDir);
|
|
503
|
+
|
|
504
|
+
if (features.length === 0) {
|
|
505
|
+
console.log('⚠️ No features found with PLUSUI_METHOD or PLUSUI_EVENT annotations');
|
|
506
|
+
console.log('\nAdd annotations to your C++ headers:');
|
|
507
|
+
console.log(' PLUSUI_METHOD(myMethod, string, int, string)');
|
|
508
|
+
console.log(' PLUSUI_EVENT(onDataReceived, json)\n');
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
console.log(`Found ${features.length} feature(s):\n`);
|
|
513
|
+
for (const feature of features) {
|
|
514
|
+
console.log(` 📦 ${feature.name}`);
|
|
515
|
+
console.log(` Methods: ${feature.methods.length}`);
|
|
516
|
+
console.log(` Events: ${feature.events.length}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Create output directory
|
|
520
|
+
await mkdir(outputDir, { recursive: true });
|
|
521
|
+
|
|
522
|
+
// Generate C++ bindings
|
|
523
|
+
console.log('\n📝 Generating C++ bindings...');
|
|
524
|
+
const cppGen = new CppCodeGenerator(features);
|
|
525
|
+
const cppCode = cppGen.generate();
|
|
526
|
+
const cppPath = join(outputDir, 'bindings.gen.hpp');
|
|
527
|
+
await writeFile(cppPath, cppCode);
|
|
528
|
+
console.log(` ✓ ${relative(process.cwd(), cppPath)}`);
|
|
529
|
+
|
|
530
|
+
// Generate TypeScript bindings
|
|
531
|
+
console.log('📝 Generating TypeScript bindings...');
|
|
532
|
+
const tsGen = new TypeScriptCodeGenerator(features);
|
|
533
|
+
const tsCode = tsGen.generate();
|
|
534
|
+
const tsPath = join(outputDir, 'plusui.gen.ts');
|
|
535
|
+
await writeFile(tsPath, tsCode);
|
|
536
|
+
console.log(` ✓ ${relative(process.cwd(), tsPath)}`);
|
|
537
|
+
|
|
538
|
+
console.log('\n✨ Bindings generated successfully!\n');
|
|
539
|
+
console.log('Usage in C++:');
|
|
540
|
+
console.log(' #include "generated/bindings.gen.hpp"');
|
|
541
|
+
console.log(' plusui::bindings::registerAllBindings();\n');
|
|
542
|
+
console.log('Usage in TypeScript:');
|
|
543
|
+
console.log(' import { plusui } from "./generated/plusui.gen"');
|
|
544
|
+
console.log(' await plusui.window.setSize(800, 600);\n');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
main().catch(err => {
|
|
548
|
+
console.error('❌ Error:', err.message);
|
|
549
|
+
process.exit(1);
|
|
550
|
+
});
|
package/src/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile, readdir, mkdir } from 'fs/promises';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { join, basename } from 'path';
|
|
6
|
+
|
|
7
|
+
const TS_TYPE_MAP = {
|
|
8
|
+
'string': 'std::string',
|
|
9
|
+
'number': 'double',
|
|
10
|
+
'boolean': 'bool',
|
|
11
|
+
'void': 'void',
|
|
12
|
+
'any': 'std::string',
|
|
13
|
+
'unknown': 'std::string',
|
|
14
|
+
'null': 'std::nullptr_t',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function parseType(tsType) {
|
|
18
|
+
const baseType = tsType.replace(/\[\]/g, '');
|
|
19
|
+
const isArray = tsType.endsWith('[]');
|
|
20
|
+
|
|
21
|
+
if (TS_TYPE_MAP[baseType]) {
|
|
22
|
+
const cppType = TS_TYPE_MAP[baseType];
|
|
23
|
+
return isArray ? `std::vector<${cppType}>` : cppType;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return 'std::string';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseService(content, filename) {
|
|
30
|
+
const className = basename(filename, '.ts');
|
|
31
|
+
const methods = [];
|
|
32
|
+
const events = [];
|
|
33
|
+
|
|
34
|
+
const methodRegex = /(?:async\s+)?(\w+)\s*\([^)]*\)\s*(?::\s*(\w+))?/g;
|
|
35
|
+
let match;
|
|
36
|
+
|
|
37
|
+
while ((match = methodRegex.exec(content)) !== null) {
|
|
38
|
+
if (match[1] === 'constructor' || match[1] === 'on' || match[1] === 'emit') continue;
|
|
39
|
+
if (content.includes('@event')) {
|
|
40
|
+
events.push({ name: match[1], returnType: match[2] || 'void' });
|
|
41
|
+
} else {
|
|
42
|
+
methods.push({ name: match[1], returnType: match[2] || 'any' });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { className, methods, events };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function generateCppBindings(services) {
|
|
50
|
+
let code = `// Auto-generated by plusui-bindgen
|
|
51
|
+
#ifndef PLUSUI_BINDINGS_H
|
|
52
|
+
#define PLUSUI_BINDINGS_H
|
|
53
|
+
|
|
54
|
+
#include <plusui/app.hpp>
|
|
55
|
+
#include <functional>
|
|
56
|
+
#include <string>
|
|
57
|
+
|
|
58
|
+
namespace plusui::bindings {
|
|
59
|
+
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
for (const service of services) {
|
|
63
|
+
code += `// Service: ${service.className}\n`;
|
|
64
|
+
|
|
65
|
+
for (const method of service.methods) {
|
|
66
|
+
const returnType = parseType(method.returnType);
|
|
67
|
+
code += `inline ${returnType} ${service.className}_${method.name}(const std::string& args) {
|
|
68
|
+
// TODO: Implement ${service.className}.${method.name}
|
|
69
|
+
return ${returnType === 'void' ? '' : 'std::string()'};
|
|
70
|
+
}\n\n`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
code += `inline void register_${service.className}(WebView& wv) {\n`;
|
|
74
|
+
for (const method of service.methods) {
|
|
75
|
+
code += ` wv.expose("${service.className.toLowerCase()}_${method.name}", ${service.className}_${method.name});\n`;
|
|
76
|
+
}
|
|
77
|
+
code += `}\n\n`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
code += `} // namespace plusui::bindings
|
|
81
|
+
|
|
82
|
+
#endif
|
|
83
|
+
`;
|
|
84
|
+
return code;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function generateTsTypes(services) {
|
|
88
|
+
let code = `// Auto-generated by plusui-bindgen
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
for (const service of services) {
|
|
92
|
+
code += `export class ${service.className}Service {\n`;
|
|
93
|
+
|
|
94
|
+
for (const method of service.methods) {
|
|
95
|
+
code += ` async ${method.name}(...args: unknown[]): Promise<unknown>;\n`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
code += `}\n\n`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
code += `export const services: {\n`;
|
|
102
|
+
for (const service of services) {
|
|
103
|
+
code += ` ${service.className.toLowerCase()}: ${service.className}Service;\n`;
|
|
104
|
+
}
|
|
105
|
+
code += `};\n`;
|
|
106
|
+
|
|
107
|
+
return code;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function scanServices(dir) {
|
|
111
|
+
const services = [];
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const files = await readdir(dir);
|
|
115
|
+
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
if (file.endsWith('.service.ts')) {
|
|
118
|
+
const content = await readFile(join(dir, file), 'utf-8');
|
|
119
|
+
services.push(parseService(content, file));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.log('No services directory found');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return services;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function generateBindings(servicesDir = './src/services', outputDir = './src') {
|
|
130
|
+
console.log('Scanning for services...');
|
|
131
|
+
const services = await scanServices(servicesDir);
|
|
132
|
+
|
|
133
|
+
if (services.length === 0) {
|
|
134
|
+
console.log('No .service.ts files found');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`Found ${services.length} service(s)`);
|
|
139
|
+
|
|
140
|
+
const cppCode = generateCppBindings(services);
|
|
141
|
+
await writeFile(join(outputDir, 'bindings.gen.hpp'), cppCode);
|
|
142
|
+
console.log('Generated: bindings.gen.hpp');
|
|
143
|
+
|
|
144
|
+
const tsCode = generateTsTypes(services);
|
|
145
|
+
await writeFile(join(outputDir, 'bindings.gen.ts'), tsCode);
|
|
146
|
+
console.log('Generated: bindings.gen.ts');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const args = process.argv.slice(2);
|
|
150
|
+
const servicesDir = args[0] || './src/services';
|
|
151
|
+
const outputDir = args[1] || './src';
|
|
152
|
+
|
|
153
|
+
generateBindings(servicesDir, outputDir).catch(console.error);
|