plusui-native-bindgen 0.1.51 ā 0.1.52
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 +1 -1
- package/src/advanced-bindgen.js +618 -657
package/src/advanced-bindgen.js
CHANGED
|
@@ -1,743 +1,704 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { mkdir, readFile, writeFile, readdir } from 'fs/promises';
|
|
4
4
|
import { existsSync } from 'fs';
|
|
5
|
-
import { join,
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
app: 'App',
|
|
27
|
-
browser: 'Browser',
|
|
28
|
-
router: 'Browser',
|
|
29
|
-
clipboard: 'Clipboard',
|
|
30
|
-
display: 'Display',
|
|
31
|
-
keyboard: 'Keyboard',
|
|
32
|
-
menu: 'Menu',
|
|
33
|
-
tray: 'Tray',
|
|
34
|
-
webgpu: 'WebGPU',
|
|
35
|
-
webview: 'WebView',
|
|
36
|
-
window: 'Window',
|
|
37
|
-
event: 'Event',
|
|
38
|
-
events: 'Event',
|
|
5
|
+
import { join, resolve } from 'path';
|
|
6
|
+
|
|
7
|
+
const CONNECT_SCHEMA_PATHS = [
|
|
8
|
+
'Core/Features/Connection/schema/connection.schema',
|
|
9
|
+
'Features/Connection/schema/connection.schema',
|
|
10
|
+
'connection.schema',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const LEGACY_SCHEMA_PATHS = [
|
|
14
|
+
'bridge.d.ts',
|
|
15
|
+
'src/bridge.d.ts',
|
|
16
|
+
'Core/bridge.d.ts',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// NEW: Scan for inline annotations in C++ files
|
|
20
|
+
const CPP_PATTERNS = {
|
|
21
|
+
call: /CONNECT_CALL\s*\(\s*(\w+)\s*,\s*([^,]+)\s*,\s*([^)]+)\s*\)/g,
|
|
22
|
+
fire: /CONNECT_FIRE\s*\(\s*(\w+)\s*,\s*([^)]+)\s*\)/g,
|
|
23
|
+
event: /CONNECT_EVENT\s*\(\s*(\w+)\s*,\s*([^)]+)\s*\)/g,
|
|
24
|
+
stream: /CONNECT_STREAM\s*\(\s*(\w+)\s*,\s*([^)]+)\s*\)/g,
|
|
25
|
+
channel: /CONNECT_CHANNEL\s*\(\s*(\w+)\s*,\s*([^)]+)\s*\)/g,
|
|
39
26
|
};
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const index = fileName.lastIndexOf('.');
|
|
47
|
-
return index >= 0 ? fileName.slice(index).toLowerCase() : '';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function walkFiles(rootDir, predicate, out = []) {
|
|
51
|
-
if (!existsSync(rootDir)) {
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
56
|
-
for (const entry of entries) {
|
|
57
|
-
const fullPath = join(rootDir, entry.name);
|
|
58
|
-
|
|
59
|
-
if (entry.isDirectory()) {
|
|
60
|
-
if (isIgnoredPathSegment(entry.name)) {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Never walk inside installed core package for custom-header scan
|
|
65
|
-
const normalized = fullPath.replace(/\\/g, '/').toLowerCase();
|
|
66
|
-
if (normalized.includes('/node_modules/plusui-native-core/')) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
await walkFiles(fullPath, predicate, out);
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (entry.isFile() && predicate(fullPath, entry.name)) {
|
|
75
|
-
out.push(fullPath);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
28
|
+
const LEGACY_KIND_MAP = {
|
|
29
|
+
call: 'call',
|
|
30
|
+
fire: 'fire',
|
|
31
|
+
event: 'event',
|
|
32
|
+
};
|
|
78
33
|
|
|
79
|
-
|
|
34
|
+
function toPascalCase(value) {
|
|
35
|
+
return value
|
|
36
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
37
|
+
.split(' ')
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
40
|
+
.join('');
|
|
80
41
|
}
|
|
81
42
|
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
43
|
+
function parseFieldMap(source) {
|
|
44
|
+
const text = source.trim();
|
|
45
|
+
if (!text) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return text
|
|
50
|
+
.split(',')
|
|
51
|
+
.map((chunk) => chunk.trim())
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.map((chunk) => {
|
|
54
|
+
const match = chunk.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*([a-zA-Z_][a-zA-Z0-9_<>|\[\]]*)$/);
|
|
55
|
+
if (!match) {
|
|
56
|
+
throw new Error(`Invalid field syntax: "${chunk}"`);
|
|
57
|
+
}
|
|
58
|
+
return { name: match[1], type: match[2] };
|
|
59
|
+
});
|
|
91
60
|
}
|
|
92
61
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
62
|
+
function normalizeSource(content) {
|
|
63
|
+
return content
|
|
64
|
+
.replace(/\r/g, '\n')
|
|
65
|
+
.replace(/#.*$/gm, '')
|
|
66
|
+
.replace(/\/\/.*$/gm, '')
|
|
67
|
+
.replace(/\s+/g, ' ')
|
|
68
|
+
.trim();
|
|
99
69
|
}
|
|
100
70
|
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
71
|
+
function parseConnectSchema(content, schemaPath) {
|
|
72
|
+
const normalized = normalizeSource(content);
|
|
73
|
+
const pattern = /connect\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+(call|fire|event|stream|channel)\s+in\s*\{([^}]*)\}(?:\s+out\s*\{([^}]*)\})?/g;
|
|
74
|
+
const methods = [];
|
|
75
|
+
|
|
76
|
+
let match = null;
|
|
77
|
+
while ((match = pattern.exec(normalized)) !== null) {
|
|
78
|
+
const name = match[1];
|
|
79
|
+
const kind = match[2];
|
|
80
|
+
const inFields = parseFieldMap(match[3] || '');
|
|
81
|
+
const outFields = parseFieldMap(match[4] || '');
|
|
82
|
+
|
|
83
|
+
methods.push({
|
|
84
|
+
name,
|
|
85
|
+
kind,
|
|
86
|
+
params: inFields,
|
|
87
|
+
result: outFields,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (methods.length === 0) {
|
|
92
|
+
throw new Error(`No connect entries found in ${schemaPath}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return methods;
|
|
104
96
|
}
|
|
105
97
|
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
98
|
+
function parseLegacyBridgeDts(content) {
|
|
99
|
+
const linesRaw = content.split('\n');
|
|
100
|
+
let insideBridge = false;
|
|
101
|
+
let braceCount = 0;
|
|
102
|
+
const bridgeBodyLines = [];
|
|
103
|
+
|
|
104
|
+
for (const line of linesRaw) {
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (!insideBridge) {
|
|
107
|
+
if (trimmed.match(/(?:export\s+)?interface\s+Bridge\s*{/)) {
|
|
108
|
+
insideBridge = true;
|
|
109
|
+
braceCount = 1;
|
|
110
|
+
const opens = (line.match(/{/g) || []).length;
|
|
111
|
+
const closes = (line.match(/}/g) || []).length;
|
|
112
|
+
braceCount += opens - 1 - closes;
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
117
115
|
}
|
|
118
116
|
|
|
119
|
-
|
|
120
|
-
}
|
|
117
|
+
const opens = (line.match(/{/g) || []).length;
|
|
118
|
+
const closes = (line.match(/}/g) || []).length;
|
|
119
|
+
braceCount += opens - closes;
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const importRegex = /import\s*\{([^}]+)\}\s*from\s*['"]plusui-native-core['"]/g;
|
|
126
|
-
let match;
|
|
127
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
128
|
-
const names = match[1]
|
|
129
|
-
.split(',')
|
|
130
|
-
.map(part => part.trim())
|
|
131
|
-
.filter(Boolean)
|
|
132
|
-
.map(part => part.split(/\s+as\s+/i)[0].trim());
|
|
133
|
-
for (const name of names) {
|
|
134
|
-
named.add(name);
|
|
135
|
-
}
|
|
121
|
+
if (braceCount === 0) {
|
|
122
|
+
break;
|
|
136
123
|
}
|
|
137
124
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
125
|
+
bridgeBodyLines.push(line);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const lines = bridgeBodyLines
|
|
129
|
+
.map((line) => line.trim())
|
|
130
|
+
.filter((line) => line && !line.startsWith('//') && !line.startsWith('/*') && !line.startsWith('*'));
|
|
131
|
+
|
|
132
|
+
const methods = [];
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
const propMatch = line.match(/^(\w+)\s*:\s*(.+);$/);
|
|
135
|
+
if (propMatch && !line.includes('(')) {
|
|
136
|
+
methods.push({
|
|
137
|
+
name: propMatch[1],
|
|
138
|
+
kind: 'event',
|
|
139
|
+
params: [{ name: 'value', type: propMatch[2].trim() }],
|
|
140
|
+
result: [],
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
148
143
|
}
|
|
149
144
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
function collectNamespaceImportAliases(content) {
|
|
154
|
-
const aliases = new Set();
|
|
155
|
-
const nsRegex = /import\s+\*\s+as\s+(\w+)\s+from\s+['"]plusui-native-core['"]/g;
|
|
156
|
-
let match;
|
|
157
|
-
while ((match = nsRegex.exec(content)) !== null) {
|
|
158
|
-
aliases.add(match[1]);
|
|
145
|
+
const methodMatch = line.match(/^(\w+)\s*\(([^)]*)\)\s*:\s*(.+);$/);
|
|
146
|
+
if (!methodMatch) {
|
|
147
|
+
continue;
|
|
159
148
|
}
|
|
160
|
-
return aliases;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function detectUsedCoreFeaturesFromContent(content) {
|
|
164
|
-
const used = new Set();
|
|
165
149
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
150
|
+
const name = methodMatch[1];
|
|
151
|
+
const rawParams = methodMatch[2];
|
|
152
|
+
const returnType = methodMatch[3].trim();
|
|
153
|
+
|
|
154
|
+
const params = rawParams
|
|
155
|
+
? rawParams
|
|
156
|
+
.split(',')
|
|
157
|
+
.map((p) => p.trim())
|
|
158
|
+
.filter(Boolean)
|
|
159
|
+
.map((p) => {
|
|
160
|
+
const [paramName, paramType] = p.split(':').map((value) => value.trim());
|
|
161
|
+
return { name: paramName, type: paramType || 'unknown' };
|
|
162
|
+
})
|
|
163
|
+
: [];
|
|
164
|
+
|
|
165
|
+
if (name.startsWith('on')) {
|
|
166
|
+
methods.push({ name, kind: 'event', params, result: [] });
|
|
167
|
+
continue;
|
|
171
168
|
}
|
|
172
169
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
170
|
+
if (returnType.startsWith('Promise<')) {
|
|
171
|
+
const resultType = returnType.replace(/^Promise<(.+)>$/, '$1');
|
|
172
|
+
methods.push({
|
|
173
|
+
name,
|
|
174
|
+
kind: 'call',
|
|
175
|
+
params,
|
|
176
|
+
result: [{ name: 'value', type: resultType }],
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
183
179
|
}
|
|
184
180
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
while ((usageMatch = directUsageRegex.exec(content)) !== null) {
|
|
188
|
-
const feature = CORE_SYMBOL_TO_FEATURE[usageMatch[1].toLowerCase()];
|
|
189
|
-
if (feature) {
|
|
190
|
-
used.add(feature);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
181
|
+
methods.push({ name, kind: 'fire', params, result: [] });
|
|
182
|
+
}
|
|
193
183
|
|
|
194
|
-
|
|
184
|
+
return methods;
|
|
195
185
|
}
|
|
196
186
|
|
|
197
|
-
async function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return WEB_IO.has(ext) || CPP_IO.has(ext);
|
|
203
|
-
}
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const usedFeatures = new Set();
|
|
207
|
-
for (const filePath of sourceFiles) {
|
|
208
|
-
const content = await readFile(filePath, 'utf-8').catch(() => '');
|
|
209
|
-
if (!content) continue;
|
|
210
|
-
|
|
211
|
-
for (const feature of detectUsedCoreFeaturesFromContent(content)) {
|
|
212
|
-
usedFeatures.add(feature);
|
|
213
|
-
}
|
|
187
|
+
async function loadSchema(projectRoot) {
|
|
188
|
+
for (const relPath of CONNECT_SCHEMA_PATHS) {
|
|
189
|
+
const fullPath = join(projectRoot, relPath);
|
|
190
|
+
if (!existsSync(fullPath)) {
|
|
191
|
+
continue;
|
|
214
192
|
}
|
|
215
193
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const coreMethods = new Map();
|
|
194
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
195
|
+
const methods = parseConnectSchema(content, fullPath);
|
|
196
|
+
return { methods, schemaPath: fullPath, schemaKind: 'connect' };
|
|
197
|
+
}
|
|
221
198
|
|
|
222
|
-
|
|
223
|
-
|
|
199
|
+
for (const relPath of LEGACY_SCHEMA_PATHS) {
|
|
200
|
+
const fullPath = join(projectRoot, relPath);
|
|
201
|
+
if (!existsSync(fullPath)) {
|
|
202
|
+
continue;
|
|
224
203
|
}
|
|
225
204
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const tsFiles = await walkFiles(
|
|
233
|
-
featureDir,
|
|
234
|
-
(filePath, fileName) => WEB_IO.has(getExtension(fileName))
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
const methods = new Set();
|
|
238
|
-
for (const tsFile of tsFiles) {
|
|
239
|
-
const content = await readFile(tsFile, 'utf-8').catch(() => '');
|
|
240
|
-
if (!content) continue;
|
|
241
|
-
|
|
242
|
-
const invokeRegex = /(?:invoke|invokeFn)\(\s*['"]([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)['"]/g;
|
|
243
|
-
let match;
|
|
244
|
-
while ((match = invokeRegex.exec(content)) !== null) {
|
|
245
|
-
methods.add(match[2]);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
coreMethods.set(featureFolder, Array.from(methods).sort((a, b) => a.localeCompare(b)));
|
|
250
|
-
}
|
|
205
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
206
|
+
const methods = parseLegacyBridgeDts(content);
|
|
207
|
+
return { methods, schemaPath: fullPath, schemaKind: 'legacy' };
|
|
208
|
+
}
|
|
251
209
|
|
|
252
|
-
|
|
210
|
+
throw new Error(
|
|
211
|
+
`No schema found. Expected one of: ${CONNECT_SCHEMA_PATHS.concat(LEGACY_SCHEMA_PATHS).join(', ')}`
|
|
212
|
+
);
|
|
253
213
|
}
|
|
254
214
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const bindingSources = [];
|
|
262
|
-
|
|
263
|
-
for (const backendPath of backendFiles) {
|
|
264
|
-
const content = await readFile(backendPath, 'utf-8').catch(() => '');
|
|
265
|
-
if (!content) continue;
|
|
266
|
-
|
|
267
|
-
// Unified PLUSUI_BIND macro parsing
|
|
268
|
-
// Format: PLUSUI_BIND(name, returnType, ...paramTypes)
|
|
269
|
-
const bindRegex = /PLUSUI_BIND\(\s*(\w+)\s*,\s*([^,\)]+)\s*((?:,\s*[^,\)]+\s*)*)\)/g;
|
|
270
|
-
|
|
271
|
-
const bindings = [];
|
|
272
|
-
|
|
273
|
-
let bindMatch;
|
|
274
|
-
while ((bindMatch = bindRegex.exec(content)) !== null) {
|
|
275
|
-
const name = bindMatch[1];
|
|
276
|
-
const returnType = normalizeCppType(bindMatch[2]);
|
|
277
|
-
const params = (bindMatch[3] || '')
|
|
278
|
-
.split(',')
|
|
279
|
-
.map(token => token.trim())
|
|
280
|
-
.filter(Boolean)
|
|
281
|
-
.map((paramType, index) => ({
|
|
282
|
-
name: `arg${index}`,
|
|
283
|
-
type: normalizeCppType(paramType),
|
|
284
|
-
}));
|
|
285
|
-
|
|
286
|
-
// Auto-detect direction based on naming convention
|
|
287
|
-
// Methods starting with "on" are events (backend ā frontend)
|
|
288
|
-
// Otherwise they're methods (frontend ā backend)
|
|
289
|
-
const isEvent = name.startsWith('on') && returnType === 'void';
|
|
290
|
-
const kind = isEvent ? 'event' : 'method';
|
|
291
|
-
|
|
292
|
-
bindings.push({ name, returnType, params, kind });
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (bindings.length === 0) {
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const baseName = basename(backendPath).replace(/\.(hpp|h|hh|hxx|cpp|cc|cxx)$/i, '');
|
|
300
|
-
|
|
301
|
-
// Separate into operations and events for compatibility
|
|
302
|
-
const operations = bindings.filter(b => b.kind === 'method');
|
|
303
|
-
const events = bindings.filter(b => b.kind === 'event').map(b => ({
|
|
304
|
-
name: b.name,
|
|
305
|
-
dataType: b.params[0]?.type || 'any'
|
|
306
|
-
}));
|
|
307
|
-
|
|
308
|
-
bindingSources.push({
|
|
309
|
-
name: baseName,
|
|
310
|
-
sourcePath: backendPath,
|
|
311
|
-
operations,
|
|
312
|
-
events,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return bindingSources;
|
|
215
|
+
function tsType(type) {
|
|
216
|
+
if (type === 'string' || type === 'number' || type === 'boolean' || type === 'unknown') {
|
|
217
|
+
return type;
|
|
218
|
+
}
|
|
219
|
+
return 'unknown';
|
|
317
220
|
}
|
|
318
221
|
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
lines.push(' const char* source;');
|
|
332
|
-
lines.push(' const char* feature;');
|
|
333
|
-
lines.push(' const char* method;');
|
|
334
|
-
lines.push('};');
|
|
335
|
-
lines.push('');
|
|
336
|
-
lines.push('inline const std::vector<BindingEntry>& getBindingEntries() {');
|
|
337
|
-
lines.push(' static const std::vector<BindingEntry> entries = {');
|
|
338
|
-
|
|
339
|
-
for (const [featureName, methods] of coreFeatureMethods.entries()) {
|
|
340
|
-
for (const methodName of methods) {
|
|
341
|
-
lines.push(` {"core", "${featureName}", "${methodName}"},`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
for (const bindingSource of customBindings) {
|
|
346
|
-
for (const operation of bindingSource.operations) {
|
|
347
|
-
lines.push(` {"custom", "${bindingSource.name}", "${operation.name}"},`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
222
|
+
function cppType(type) {
|
|
223
|
+
if (type === 'string') {
|
|
224
|
+
return 'std::string';
|
|
225
|
+
}
|
|
226
|
+
if (type === 'number') {
|
|
227
|
+
return 'double';
|
|
228
|
+
}
|
|
229
|
+
if (type === 'boolean') {
|
|
230
|
+
return 'bool';
|
|
231
|
+
}
|
|
232
|
+
return 'nlohmann::json';
|
|
233
|
+
}
|
|
350
234
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
lines.push('} // namespace plusui::bindings');
|
|
356
|
-
lines.push('');
|
|
235
|
+
function emitTsTypeAlias(aliasName, fields) {
|
|
236
|
+
if (fields.length === 0) {
|
|
237
|
+
return `export type ${aliasName} = {};`;
|
|
238
|
+
}
|
|
357
239
|
|
|
358
|
-
|
|
240
|
+
const body = fields.map((field) => ` ${field.name}: ${tsType(field.type)};`).join('\n');
|
|
241
|
+
return `export type ${aliasName} = {\n${body}\n};`;
|
|
359
242
|
}
|
|
360
243
|
|
|
361
|
-
function
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
244
|
+
function generateConnectionTs(methods) {
|
|
245
|
+
const lines = [];
|
|
246
|
+
lines.push('/**');
|
|
247
|
+
lines.push(' * Auto-generated by plusui-bindgen');
|
|
248
|
+
lines.push(' * DO NOT EDIT - Changes will be overwritten');
|
|
249
|
+
lines.push(' * Generated from: connection.schema');
|
|
250
|
+
lines.push(' */');
|
|
251
|
+
lines.push('import { connection } from "../connection";');
|
|
252
|
+
lines.push('');
|
|
253
|
+
|
|
254
|
+
// Generate type definitions
|
|
255
|
+
lines.push('// ============================================');
|
|
256
|
+
lines.push('// Type Definitions');
|
|
257
|
+
lines.push('// ============================================');
|
|
258
|
+
lines.push('');
|
|
259
|
+
|
|
260
|
+
for (const method of methods) {
|
|
261
|
+
const base = toPascalCase(method.name);
|
|
365
262
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
263
|
+
// Input types
|
|
264
|
+
if (method.params.length > 0) {
|
|
265
|
+
lines.push(emitTsTypeAlias(`${base}In`, method.params));
|
|
266
|
+
} else {
|
|
267
|
+
lines.push(`export type ${base}In = void;`);
|
|
268
|
+
}
|
|
370
269
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
lines.push(
|
|
375
|
-
|
|
376
|
-
lines.push(
|
|
270
|
+
// Output types for calls
|
|
271
|
+
if (method.kind === 'call') {
|
|
272
|
+
if (method.result.length > 0) {
|
|
273
|
+
lines.push(emitTsTypeAlias(`${base}Out`, method.result));
|
|
274
|
+
} else {
|
|
275
|
+
lines.push(`export type ${base}Out = void;`);
|
|
276
|
+
}
|
|
377
277
|
}
|
|
378
278
|
|
|
379
|
-
|
|
380
|
-
|
|
279
|
+
// Data types for events/streams/channels
|
|
280
|
+
if (method.kind === 'event' || method.kind === 'stream' || method.kind === 'channel') {
|
|
281
|
+
if (method.params.length > 0) {
|
|
282
|
+
lines.push(emitTsTypeAlias(`${base}Data`, method.params));
|
|
283
|
+
} else {
|
|
284
|
+
lines.push(`export type ${base}Data = void;`);
|
|
285
|
+
}
|
|
381
286
|
}
|
|
382
287
|
|
|
383
|
-
lines.push('}');
|
|
384
288
|
lines.push('');
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
lines.push('// ============================================');
|
|
292
|
+
lines.push('// Connection API');
|
|
293
|
+
lines.push('// ============================================');
|
|
294
|
+
lines.push('');
|
|
295
|
+
lines.push('/**');
|
|
296
|
+
lines.push(' * Unified connection API for frontend <-> backend communication');
|
|
297
|
+
lines.push(' * - call: Request/response (async)');
|
|
298
|
+
lines.push(' * - fire: One-way to backend');
|
|
299
|
+
lines.push(' * - events: Listen to backend notifications');
|
|
300
|
+
lines.push(' * - streams: Subscribe to backend data streams');
|
|
301
|
+
lines.push(' * - channels: Bidirectional pub/sub');
|
|
302
|
+
lines.push(' */');
|
|
303
|
+
lines.push('export const connect = {');
|
|
304
|
+
|
|
305
|
+
for (const method of methods) {
|
|
306
|
+
const base = toPascalCase(method.name);
|
|
392
307
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
lines.push(
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (argNames) {
|
|
410
|
-
lines.push(` return invoke("${bindingSource.name}.${operation.name}", [${argNames}]) as ${promiseType};`);
|
|
411
|
-
} else {
|
|
412
|
-
lines.push(` return invoke("${bindingSource.name}.${operation.name}", []) as ${promiseType};`);
|
|
413
|
-
}
|
|
414
|
-
lines.push(' }');
|
|
308
|
+
if (method.kind === 'call') {
|
|
309
|
+
const hasParams = method.params.length > 0;
|
|
310
|
+
const hasResult = method.result.length > 0;
|
|
311
|
+
const paramType = hasParams ? `${base}In` : 'void';
|
|
312
|
+
const returnType = hasResult ? `${base}Out` : 'void';
|
|
313
|
+
|
|
314
|
+
lines.push('');
|
|
315
|
+
lines.push(` /** [CALL] ${method.name} - Request/response */`);
|
|
316
|
+
if (hasParams) {
|
|
317
|
+
lines.push(` ${method.name}: (args: ${paramType}): Promise<${returnType}> => `);
|
|
318
|
+
lines.push(` connection.call<${returnType}, ${paramType}>("${method.name}", args),`);
|
|
319
|
+
} else {
|
|
320
|
+
lines.push(` ${method.name}: (): Promise<${returnType}> => `);
|
|
321
|
+
lines.push(` connection.call<${returnType}, Record<string, never>>("${method.name}", {}),`);
|
|
322
|
+
}
|
|
323
|
+
continue;
|
|
415
324
|
}
|
|
416
325
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
lines.push(`
|
|
425
|
-
lines.push(
|
|
426
|
-
|
|
427
|
-
lines.push(
|
|
428
|
-
lines.push(
|
|
326
|
+
if (method.kind === 'fire') {
|
|
327
|
+
const hasParams = method.params.length > 0;
|
|
328
|
+
const paramType = hasParams ? `${base}In` : 'void';
|
|
329
|
+
|
|
330
|
+
lines.push('');
|
|
331
|
+
lines.push(` /** [FIRE] ${method.name} - One-way to backend */`);
|
|
332
|
+
if (hasParams) {
|
|
333
|
+
lines.push(` ${method.name}: (args: ${paramType}): void => `);
|
|
334
|
+
lines.push(` connection.fire<${paramType}>("${method.name}", args),`);
|
|
335
|
+
} else {
|
|
336
|
+
lines.push(` ${method.name}: (): void => `);
|
|
337
|
+
lines.push(` connection.fire("${method.name}", {}),`);
|
|
338
|
+
}
|
|
339
|
+
continue;
|
|
429
340
|
}
|
|
430
341
|
|
|
431
|
-
if (
|
|
432
|
-
|
|
342
|
+
if (method.kind === 'event') {
|
|
343
|
+
lines.push('');
|
|
344
|
+
lines.push(` /** [EVENT] ${method.name} - Listen to backend notification */`);
|
|
345
|
+
lines.push(` ${method.name}: (callback: (data: ${base}Data) => void): (() => void) => `);
|
|
346
|
+
lines.push(` connection.on<${base}Data>("${method.name}", callback),`);
|
|
347
|
+
continue;
|
|
433
348
|
}
|
|
434
349
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
350
|
+
if (method.kind === 'stream') {
|
|
351
|
+
lines.push('');
|
|
352
|
+
lines.push(` /** [STREAM] ${method.name} - Subscribe to backend data stream */`);
|
|
353
|
+
lines.push(` ${method.name}: {`);
|
|
354
|
+
lines.push(` subscribe: (callback: (data: ${base}Data) => void): (() => void) => `);
|
|
355
|
+
lines.push(` connection.stream<${base}Data>("${method.name}").subscribe(callback),`);
|
|
356
|
+
lines.push(' },');
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
439
359
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
lines.push(' * plusui.myFeature.onSomeEvent((data) => { ... });');
|
|
454
|
-
lines.push(' */');
|
|
455
|
-
lines.push('');
|
|
456
|
-
lines.push('// Binding bridge to native layer');
|
|
457
|
-
lines.push('type InvokeFn = (method: string, args?: unknown[]) => Promise<unknown>;');
|
|
458
|
-
lines.push('');
|
|
459
|
-
lines.push('function getInvoke(): InvokeFn {');
|
|
460
|
-
lines.push(' const w = typeof window !== "undefined" ? window as any : globalThis as any;');
|
|
461
|
-
lines.push(' if (typeof w.__invoke__ !== "function") {');
|
|
462
|
-
lines.push(' throw new Error("PlusUI binding bridge (__invoke__) not available");');
|
|
463
|
-
lines.push(' }');
|
|
464
|
-
lines.push(' return w.__invoke__;');
|
|
465
|
-
lines.push('}');
|
|
466
|
-
lines.push('');
|
|
467
|
-
lines.push('async function invoke(method: string, args: unknown[] = []): Promise<unknown> {');
|
|
468
|
-
lines.push(' return getInvoke()(method, args);');
|
|
469
|
-
lines.push('}');
|
|
470
|
-
lines.push('');
|
|
360
|
+
if (method.kind === 'channel') {
|
|
361
|
+
lines.push('');
|
|
362
|
+
lines.push(` /** [CHANNEL] ${method.name} - Bidirectional pub/sub */`);
|
|
363
|
+
lines.push(` ${method.name}: {`);
|
|
364
|
+
lines.push(` subscribe: (callback: (data: ${base}Data) => void): (() => void) => `);
|
|
365
|
+
lines.push(` connection.channel<${base}Data>("${method.name}").subscribe(callback),`);
|
|
366
|
+
lines.push(` publish: (data: ${base}Data): void => `);
|
|
367
|
+
lines.push(` connection.channel<${base}Data>("${method.name}").publish(data),`);
|
|
368
|
+
lines.push(' },');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
lines.push('};');
|
|
471
373
|
|
|
472
|
-
|
|
473
|
-
|
|
374
|
+
lines.push('');
|
|
375
|
+
lines.push('// Legacy export for backwards compatibility');
|
|
376
|
+
lines.push('export const bindings = connect;');
|
|
377
|
+
lines.push('export default connect;');
|
|
378
|
+
lines.push('');
|
|
474
379
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
380
|
+
return lines.join('\n');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function emitCppStruct(structName, fields) {
|
|
384
|
+
if (fields.length === 0) {
|
|
385
|
+
return `struct ${structName} {};`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const lines = [];
|
|
389
|
+
lines.push(`struct ${structName} {`);
|
|
390
|
+
for (const field of fields) {
|
|
391
|
+
lines.push(` ${cppType(field.type)} ${field.name};`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Add from_json helper
|
|
395
|
+
lines.push('');
|
|
396
|
+
lines.push(' static ' + structName + ' from_json(const nlohmann::json& j) {');
|
|
397
|
+
lines.push(' ' + structName + ' result;');
|
|
398
|
+
for (const field of fields) {
|
|
399
|
+
lines.push(` if (j.contains("${field.name}")) { result.${field.name} = j["${field.name}"]; }`);
|
|
400
|
+
}
|
|
401
|
+
lines.push(' return result;');
|
|
402
|
+
lines.push(' }');
|
|
403
|
+
|
|
404
|
+
// Add to_json helper
|
|
405
|
+
lines.push('');
|
|
406
|
+
lines.push(' nlohmann::json to_json() const {');
|
|
407
|
+
lines.push(' nlohmann::json j;');
|
|
408
|
+
for (const field of fields) {
|
|
409
|
+
lines.push(` j["${field.name}"] = ${field.name};`);
|
|
410
|
+
}
|
|
411
|
+
lines.push(' return j;');
|
|
412
|
+
lines.push(' }');
|
|
413
|
+
|
|
414
|
+
lines.push('};');
|
|
415
|
+
return lines.join('\n');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function generateConnectionCpp(methods) {
|
|
419
|
+
const lines = [];
|
|
420
|
+
lines.push('/**');
|
|
421
|
+
lines.push(' * Auto-generated by plusui-bindgen');
|
|
422
|
+
lines.push(' * DO NOT EDIT - Changes will be overwritten');
|
|
423
|
+
lines.push(' * Generated from: connection.schema');
|
|
424
|
+
lines.push(' */');
|
|
425
|
+
lines.push('#pragma once');
|
|
426
|
+
lines.push('#include <plusui/connection.hpp>');
|
|
427
|
+
lines.push('#include <string>');
|
|
428
|
+
lines.push('#include <vector>');
|
|
429
|
+
lines.push('');
|
|
430
|
+
lines.push('namespace plusui {');
|
|
431
|
+
lines.push('namespace connect {');
|
|
432
|
+
lines.push('');
|
|
433
|
+
|
|
434
|
+
lines.push('// ============================================');
|
|
435
|
+
lines.push('// Type Definitions');
|
|
436
|
+
lines.push('// ============================================');
|
|
437
|
+
lines.push('');
|
|
438
|
+
|
|
439
|
+
// Generate structs for all methods
|
|
440
|
+
for (const method of methods) {
|
|
441
|
+
const base = toPascalCase(method.name);
|
|
442
|
+
|
|
443
|
+
// Input struct
|
|
444
|
+
if (method.params.length > 0) {
|
|
445
|
+
lines.push(emitCppStruct(`${base}In`, method.params));
|
|
446
|
+
lines.push('');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Output struct for calls
|
|
450
|
+
if (method.kind === 'call' && method.result.length > 0) {
|
|
451
|
+
lines.push(emitCppStruct(`${base}Out`, method.result));
|
|
452
|
+
lines.push('');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Data struct for events/streams/channels
|
|
456
|
+
if (['event', 'stream', 'channel'].includes(method.kind) && method.params.length > 0) {
|
|
457
|
+
lines.push(emitCppStruct(`${base}Data`, method.params));
|
|
458
|
+
lines.push('');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
lines.push('// ============================================');
|
|
463
|
+
lines.push('// Generated Bindings Base Class');
|
|
464
|
+
lines.push('// ============================================');
|
|
465
|
+
lines.push('');
|
|
466
|
+
lines.push('/**');
|
|
467
|
+
lines.push(' * Auto-generated connection bindings');
|
|
468
|
+
lines.push(' * Inherit from this class and implement the handler methods');
|
|
469
|
+
lines.push(' */');
|
|
470
|
+
lines.push('class Bindings : public Connection {');
|
|
471
|
+
lines.push('public:');
|
|
472
|
+
lines.push('');
|
|
473
|
+
|
|
474
|
+
lines.push(' // ========================================');
|
|
475
|
+
lines.push(' // Handler Methods (implement these)');
|
|
476
|
+
lines.push(' // ========================================');
|
|
477
|
+
lines.push('');
|
|
478
|
+
|
|
479
|
+
// Generate virtual handler methods
|
|
480
|
+
for (const method of methods) {
|
|
481
|
+
const base = toPascalCase(method.name);
|
|
482
|
+
|
|
483
|
+
if (method.kind === 'call') {
|
|
484
|
+
const hasParams = method.params.length > 0;
|
|
485
|
+
const hasResult = method.result.length > 0;
|
|
486
|
+
const paramType = hasParams ? `const ${base}In&` : '';
|
|
487
|
+
const returnType = hasResult ? `${base}Out` : 'void';
|
|
488
|
+
|
|
489
|
+
lines.push(` /** [CALL] ${method.name} - Request/response handler */`);
|
|
490
|
+
if (hasParams) {
|
|
491
|
+
lines.push(` virtual ${returnType} handle_${method.name}(${paramType} args) = 0;`);
|
|
492
|
+
} else {
|
|
493
|
+
lines.push(` virtual ${returnType} handle_${method.name}() = 0;`);
|
|
494
|
+
}
|
|
495
|
+
lines.push('');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (method.kind === 'fire') {
|
|
499
|
+
const hasParams = method.params.length > 0;
|
|
500
|
+
const paramType = hasParams ? `const ${base}In&` : '';
|
|
501
|
+
|
|
502
|
+
lines.push(` /** [FIRE] ${method.name} - One-way handler */`);
|
|
503
|
+
if (hasParams) {
|
|
504
|
+
lines.push(` virtual void handle_${method.name}(${paramType} args) = 0;`);
|
|
505
|
+
} else {
|
|
506
|
+
lines.push(` virtual void handle_${method.name}() = 0;`);
|
|
507
|
+
}
|
|
508
|
+
lines.push('');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (method.kind === 'channel') {
|
|
512
|
+
const hasParams = method.params.length > 0;
|
|
513
|
+
const paramType = hasParams ? `const ${base}Data&` : '';
|
|
514
|
+
|
|
515
|
+
lines.push(` /** [CHANNEL] ${method.name} - Publish handler */`);
|
|
516
|
+
if (hasParams) {
|
|
517
|
+
lines.push(` virtual void handle_${method.name}_publish(${paramType} data) = 0;`);
|
|
518
|
+
} else {
|
|
519
|
+
lines.push(` virtual void handle_${method.name}_publish() = 0;`);
|
|
520
|
+
}
|
|
521
|
+
lines.push('');
|
|
481
522
|
}
|
|
523
|
+
}
|
|
482
524
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
525
|
+
lines.push(' // ========================================');
|
|
526
|
+
lines.push(' // Emit Methods (call these to send to frontend)');
|
|
527
|
+
lines.push(' // ========================================');
|
|
528
|
+
lines.push('');
|
|
529
|
+
|
|
530
|
+
// Generate emit methods for events/streams/channels
|
|
531
|
+
for (const method of methods) {
|
|
532
|
+
if (!['event', 'stream', 'channel'].includes(method.kind)) {
|
|
533
|
+
continue;
|
|
489
534
|
}
|
|
490
535
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
lines.push(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
536
|
+
const base = toPascalCase(method.name);
|
|
537
|
+
const hasParams = method.params.length > 0;
|
|
538
|
+
const paramType = hasParams ? `const ${base}Data&` : '';
|
|
539
|
+
|
|
540
|
+
let kindLabel = method.kind.toUpperCase();
|
|
541
|
+
lines.push(` /** [${kindLabel}] ${method.name} - Emit to frontend */`);
|
|
542
|
+
|
|
543
|
+
if (hasParams) {
|
|
544
|
+
lines.push(` void emit_${method.name}(${paramType} data) {`);
|
|
545
|
+
lines.push(` auto payload = data.to_json();`);
|
|
500
546
|
} else {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
547
|
+
lines.push(` void emit_${method.name}() {`);
|
|
548
|
+
lines.push(' auto payload = nlohmann::json::object();');
|
|
504
549
|
}
|
|
505
|
-
lines.push('};');
|
|
506
|
-
lines.push('');
|
|
507
550
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
lines.push('');
|
|
551
|
+
if (method.kind === 'event') {
|
|
552
|
+
lines.push(` emit("${method.name}", payload);`);
|
|
553
|
+
} else if (method.kind === 'stream') {
|
|
554
|
+
lines.push(` emit("${method.name}", payload);`);
|
|
555
|
+
} else if (method.kind === 'channel') {
|
|
556
|
+
lines.push(` emit("${method.name}", payload);`);
|
|
515
557
|
}
|
|
516
558
|
|
|
517
|
-
lines.push('
|
|
559
|
+
lines.push(' }');
|
|
518
560
|
lines.push('');
|
|
519
|
-
|
|
520
|
-
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
lines.push('protected:');
|
|
564
|
+
lines.push(' // ========================================');
|
|
565
|
+
lines.push(' // Message Dispatcher (auto-generated)');
|
|
566
|
+
lines.push(' // ========================================');
|
|
567
|
+
lines.push('');
|
|
568
|
+
lines.push(' void handleMessage(const std::string &name, const nlohmann::json &payload) override {');
|
|
569
|
+
lines.push(' try {');
|
|
570
|
+
|
|
571
|
+
// Generate CALL handlers (now just emit response)
|
|
572
|
+
const callMethods = methods.filter((m) => m.kind === 'call');
|
|
573
|
+
if (callMethods.length > 0) {
|
|
574
|
+
lines.push(' // CALL handlers (request/response pattern)');
|
|
575
|
+
for (const method of callMethods) {
|
|
576
|
+
const base = toPascalCase(method.name);
|
|
577
|
+
const hasParams = method.params.length > 0;
|
|
578
|
+
const hasResult = method.result.length > 0;
|
|
579
|
+
|
|
580
|
+
lines.push(` if (name == "${method.name}") {`);
|
|
581
|
+
if (hasParams && hasResult) {
|
|
582
|
+
lines.push(` auto args = ${base}In::from_json(payload);`);
|
|
583
|
+
lines.push(` auto res = handle_${method.name}(args);`);
|
|
584
|
+
lines.push(` emit("${method.name}Result", res.to_json());`);
|
|
585
|
+
} else if (hasParams) {
|
|
586
|
+
lines.push(` auto args = ${base}In::from_json(payload);`);
|
|
587
|
+
lines.push(` handle_${method.name}(args);`);
|
|
588
|
+
lines.push(` emit("${method.name}Result", nlohmann::json::object());`);
|
|
589
|
+
} else if (hasResult) {
|
|
590
|
+
lines.push(` auto res = handle_${method.name}();`);
|
|
591
|
+
lines.push(` emit("${method.name}Result", res.to_json());`);
|
|
592
|
+
} else {
|
|
593
|
+
lines.push(` handle_${method.name}();`);
|
|
594
|
+
lines.push(` emit("${method.name}Result", nlohmann::json::object());`);
|
|
595
|
+
}
|
|
596
|
+
lines.push(' return;');
|
|
597
|
+
lines.push(' }');
|
|
598
|
+
}
|
|
599
|
+
lines.push('');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Generate FIRE handlers (fire & forget)
|
|
603
|
+
const fireMethods = methods.filter((m) => m.kind === 'fire');
|
|
604
|
+
if (fireMethods.length > 0) {
|
|
605
|
+
lines.push(' // FIRE handlers (fire & forget pattern)');
|
|
606
|
+
for (const method of fireMethods) {
|
|
607
|
+
const base = toPascalCase(method.name);
|
|
608
|
+
const hasParams = method.params.length > 0;
|
|
609
|
+
|
|
610
|
+
lines.push(` if (name == "${method.name}") {`);
|
|
611
|
+
if (hasParams) {
|
|
612
|
+
lines.push(` auto args = ${base}In::from_json(payload);`);
|
|
613
|
+
lines.push(` handle_${method.name}(args);`);
|
|
614
|
+
} else {
|
|
615
|
+
lines.push(` handle_${method.name}();`);
|
|
616
|
+
}
|
|
617
|
+
lines.push(' return;');
|
|
618
|
+
lines.push(' }');
|
|
619
|
+
}
|
|
620
|
+
lines.push('');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Generate CHANNEL handlers
|
|
624
|
+
const channelMethods = methods.filter((m) => m.kind === 'channel');
|
|
625
|
+
if (channelMethods.length > 0) {
|
|
626
|
+
lines.push(' // CHANNEL publish handlers');
|
|
627
|
+
for (const method of channelMethods) {
|
|
628
|
+
const base = toPascalCase(method.name);
|
|
629
|
+
const hasParams = method.params.length > 0;
|
|
630
|
+
|
|
631
|
+
lines.push(` if (name == "${method.name}") {`);
|
|
632
|
+
if (hasParams) {
|
|
633
|
+
lines.push(` auto data = ${base}Data::from_json(payload);`);
|
|
634
|
+
lines.push(` handle_${method.name}_publish(data);`);
|
|
635
|
+
} else {
|
|
636
|
+
lines.push(` handle_${method.name}_publish();`);
|
|
637
|
+
}
|
|
638
|
+
lines.push(' return;');
|
|
639
|
+
lines.push(' }');
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
lines.push(' } catch (const std::exception& e) {');
|
|
644
|
+
lines.push(' // Error handling - log or ignore');
|
|
645
|
+
lines.push(' }');
|
|
646
|
+
lines.push(' }');
|
|
647
|
+
lines.push('};');
|
|
648
|
+
lines.push('');
|
|
649
|
+
lines.push('} // namespace connect');
|
|
650
|
+
lines.push('} // namespace plusui');
|
|
651
|
+
lines.push('');
|
|
652
|
+
|
|
653
|
+
return lines.join('\n');
|
|
521
654
|
}
|
|
522
655
|
|
|
523
|
-
function
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
lines.push('// Custom backend binding template');
|
|
527
|
-
lines.push('');
|
|
528
|
-
lines.push('#include "custom.bindings.gen.hpp"');
|
|
529
|
-
lines.push('');
|
|
530
|
-
|
|
531
|
-
for (const bindingSource of customBindings) {
|
|
532
|
-
lines.push(`// Source: ${bindingSource.name}`);
|
|
533
|
-
for (const operation of bindingSource.operations) {
|
|
534
|
-
lines.push(`// TODO: Implement ${operation.kind} binding for ${bindingSource.name}.${operation.name}`);
|
|
535
|
-
}
|
|
536
|
-
for (const eventItem of bindingSource.events) {
|
|
537
|
-
lines.push(`// TODO: Implement event binding for ${bindingSource.name}.${eventItem.name}`);
|
|
538
|
-
}
|
|
539
|
-
lines.push('');
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if (customBindings.length === 0) {
|
|
543
|
-
lines.push('// No custom backend bindings discovered yet.');
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
return lines.join('\n');
|
|
547
|
-
}
|
|
656
|
+
async function writeOutputs(projectRoot, methods, schemaKind) {
|
|
657
|
+
const tsOutDir = resolve(projectRoot, 'Core/Features/Connection/generated');
|
|
658
|
+
const cppOutDir = tsOutDir;
|
|
548
659
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
lines.push('// Auto-generated by plusui-bindgen');
|
|
552
|
-
lines.push('// Native backend binding template');
|
|
553
|
-
lines.push('');
|
|
554
|
-
lines.push('#include "core.bindings.gen.hpp"');
|
|
555
|
-
lines.push('');
|
|
660
|
+
await mkdir(tsOutDir, { recursive: true });
|
|
661
|
+
await mkdir(cppOutDir, { recursive: true });
|
|
556
662
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return lines.join('\n');
|
|
560
|
-
}
|
|
663
|
+
const tsContent = generateConnectionTs(methods);
|
|
664
|
+
const cppContent = generateConnectionCpp(methods);
|
|
561
665
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
for (const methodName of methods) {
|
|
565
|
-
lines.push(`// TODO: wire native binding for ${featureName}.${methodName}`);
|
|
566
|
-
}
|
|
567
|
-
lines.push('');
|
|
568
|
-
}
|
|
666
|
+
const tsPath = join(tsOutDir, 'bindings.ts');
|
|
667
|
+
const cppPath = join(cppOutDir, 'bindings.hpp');
|
|
569
668
|
|
|
570
|
-
|
|
571
|
-
|
|
669
|
+
await writeFile(tsPath, tsContent);
|
|
670
|
+
await writeFile(cppPath, cppContent);
|
|
572
671
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
.map(bindingSource => `<tr><td>${bindingSource.name}</td><td>custom</td><td>${bindingSource.operations.map(m => `${m.name} [${m.kind}]`).join(', ') || '-'}</td></tr>`)
|
|
580
|
-
.join('');
|
|
581
|
-
|
|
582
|
-
return `<!doctype html>
|
|
583
|
-
<html>
|
|
584
|
-
<head>
|
|
585
|
-
<meta charset="utf-8" />
|
|
586
|
-
<title>PlusUI Bindings Summary</title>
|
|
587
|
-
</head>
|
|
588
|
-
<body>
|
|
589
|
-
<h1>PlusUI Bindings Summary</h1>
|
|
590
|
-
<p>Generated by plusui bindgen</p>
|
|
591
|
-
<table border="1" cellspacing="0" cellpadding="6">
|
|
592
|
-
<thead>
|
|
593
|
-
<tr>
|
|
594
|
-
<th>Feature</th>
|
|
595
|
-
<th>Type</th>
|
|
596
|
-
<th>Methods</th>
|
|
597
|
-
</tr>
|
|
598
|
-
</thead>
|
|
599
|
-
<tbody>
|
|
600
|
-
${coreRows}${customRows}
|
|
601
|
-
</tbody>
|
|
602
|
-
</table>
|
|
603
|
-
</body>
|
|
604
|
-
</html>
|
|
605
|
-
`;
|
|
606
|
-
}
|
|
672
|
+
const legacyTsOut = resolve(projectRoot, 'src/generated');
|
|
673
|
+
const legacyCppOut = resolve(projectRoot, 'Core/generated');
|
|
674
|
+
await mkdir(legacyTsOut, { recursive: true });
|
|
675
|
+
await mkdir(legacyCppOut, { recursive: true });
|
|
676
|
+
await writeFile(join(legacyTsOut, 'bridge.ts'), tsContent);
|
|
677
|
+
await writeFile(join(legacyCppOut, 'bridge.hpp'), cppContent);
|
|
607
678
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const usedCoreFeatures = await detectUsedCoreFeatures(root);
|
|
614
|
-
const coreFeatureMethods = await parseCoreFeatureMethods(coreFeaturesDir, usedCoreFeatures);
|
|
615
|
-
const customBindings = await parseCustomBackendBindings(root);
|
|
616
|
-
|
|
617
|
-
await mkdir(outputDir, { recursive: true });
|
|
618
|
-
|
|
619
|
-
const nativeBackendDir = join(outputDir, 'NativeBindings', 'CPP_IO');
|
|
620
|
-
const nativeFrontendDir = join(outputDir, 'NativeBindings', 'WEB_IO');
|
|
621
|
-
const customBackendDir = join(outputDir, 'CustomBindings', 'CPP_IO');
|
|
622
|
-
const customFrontendDir = join(outputDir, 'CustomBindings', 'WEB_IO');
|
|
623
|
-
const includeNativeBackendDir = join(includeOutputDir, 'NativeBindings', 'CPP_IO');
|
|
624
|
-
const includeCustomBackendDir = join(includeOutputDir, 'CustomBindings', 'CPP_IO');
|
|
625
|
-
|
|
626
|
-
await mkdir(nativeBackendDir, { recursive: true });
|
|
627
|
-
await mkdir(nativeFrontendDir, { recursive: true });
|
|
628
|
-
await mkdir(customBackendDir, { recursive: true });
|
|
629
|
-
await mkdir(customFrontendDir, { recursive: true });
|
|
630
|
-
await mkdir(includeNativeBackendDir, { recursive: true });
|
|
631
|
-
await mkdir(includeCustomBackendDir, { recursive: true });
|
|
632
|
-
|
|
633
|
-
const cppOut = join(outputDir, 'bindings.gen.hpp');
|
|
634
|
-
const tsOut = join(outputDir, 'bindings.gen.ts');
|
|
635
|
-
const reportOut = join(outputDir, 'bindings.report.json');
|
|
636
|
-
const nativeCppOut = join(nativeBackendDir, 'core.bindings.gen.hpp');
|
|
637
|
-
const nativeCppTemplateOut = join(nativeBackendDir, 'core.bindings.gen.cpp');
|
|
638
|
-
const nativeTsOut = join(nativeFrontendDir, 'core.bindings.gen.ts');
|
|
639
|
-
const customCppOut = join(customBackendDir, 'custom.bindings.gen.hpp');
|
|
640
|
-
const customCppTemplateOut = join(customBackendDir, 'custom.bindings.gen.cpp');
|
|
641
|
-
const customTsOut = join(customFrontendDir, 'custom.bindings.gen.ts');
|
|
642
|
-
const customHtmlOut = join(customFrontendDir, 'custom.bindings.gen.html');
|
|
643
|
-
const includeCppOut = join(includeOutputDir, 'bindings.gen.hpp');
|
|
644
|
-
const includeNativeCppOut = join(includeNativeBackendDir, 'core.bindings.gen.hpp');
|
|
645
|
-
const includeCustomCppOut = join(includeCustomBackendDir, 'custom.bindings.gen.hpp');
|
|
646
|
-
|
|
647
|
-
const allCppBindings = generateCppBindings(customBindings, coreFeatureMethods);
|
|
648
|
-
const nativeCppBindings = generateCppBindings([], coreFeatureMethods);
|
|
649
|
-
const customCppBindings = generateCppBindings(customBindings, new Map());
|
|
650
|
-
|
|
651
|
-
await writeFile(cppOut, allCppBindings);
|
|
652
|
-
await writeFile(tsOut, generateTsBindings(coreFeatureMethods, customBindings));
|
|
653
|
-
await writeFile(nativeCppOut, nativeCppBindings);
|
|
654
|
-
await writeFile(nativeCppTemplateOut, generateNativeCppTemplate(coreFeatureMethods));
|
|
655
|
-
await writeFile(nativeTsOut, generateTsBindings(coreFeatureMethods, []));
|
|
656
|
-
await writeFile(customCppOut, customCppBindings);
|
|
657
|
-
await writeFile(customCppTemplateOut, generateCustomCppTemplate(customBindings));
|
|
658
|
-
await writeFile(customTsOut, generateTsBindings(new Map(), customBindings));
|
|
659
|
-
await writeFile(customHtmlOut, generateFrontendHtmlSummary(coreFeatureMethods, customBindings));
|
|
660
|
-
await writeFile(includeCppOut, allCppBindings);
|
|
661
|
-
await writeFile(includeNativeCppOut, nativeCppBindings);
|
|
662
|
-
await writeFile(includeCustomCppOut, customCppBindings);
|
|
663
|
-
|
|
664
|
-
const report = {
|
|
665
|
-
projectRoot: root,
|
|
666
|
-
outputDir: resolve(outputDir),
|
|
667
|
-
extensionBuckets: {
|
|
668
|
-
WEB_IO: Array.from(WEB_IO),
|
|
669
|
-
CPP_IO: Array.from(CPP_IO),
|
|
670
|
-
},
|
|
671
|
-
coreFeaturesDir,
|
|
672
|
-
usedCoreFeatures: Array.from(usedCoreFeatures).sort((a, b) => a.localeCompare(b)),
|
|
673
|
-
generatedCoreFeatures: Array.from(coreFeatureMethods.entries()).map(([name, methods]) => ({ name, methods })),
|
|
674
|
-
customBindings: customBindings.map(bindingSource => ({
|
|
675
|
-
name: bindingSource.name,
|
|
676
|
-
sourcePath: relative(root, bindingSource.sourcePath),
|
|
677
|
-
operations: bindingSource.operations.map(m => ({ name: m.name, kind: m.kind })),
|
|
678
|
-
events: bindingSource.events.map(e => e.name),
|
|
679
|
-
})),
|
|
680
|
-
generatedFiles: [
|
|
681
|
-
relative(root, cppOut),
|
|
682
|
-
relative(root, tsOut),
|
|
683
|
-
relative(root, nativeCppOut),
|
|
684
|
-
relative(root, nativeCppTemplateOut),
|
|
685
|
-
relative(root, nativeTsOut),
|
|
686
|
-
relative(root, customCppOut),
|
|
687
|
-
relative(root, customCppTemplateOut),
|
|
688
|
-
relative(root, customTsOut),
|
|
689
|
-
relative(root, customHtmlOut),
|
|
690
|
-
relative(root, includeCppOut),
|
|
691
|
-
relative(root, includeNativeCppOut),
|
|
692
|
-
relative(root, includeCustomCppOut),
|
|
693
|
-
relative(root, reportOut),
|
|
694
|
-
],
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
await writeFile(reportOut, JSON.stringify(report, null, 2));
|
|
698
|
-
|
|
699
|
-
const coreMethodCount = Array.from(coreFeatureMethods.values()).reduce((sum, list) => sum + list.length, 0);
|
|
700
|
-
const customBindingCount = customBindings.reduce((sum, bindingSource) => sum + bindingSource.operations.length + bindingSource.events.length, 0);
|
|
701
|
-
|
|
702
|
-
console.log('\nš§ PlusUI Bindgen\n');
|
|
703
|
-
console.log(`Project: ${root}`);
|
|
704
|
-
console.log(`Output: ${resolve(outputDir)}\n`);
|
|
705
|
-
console.log(`Core features used: ${coreFeatureMethods.size}`);
|
|
706
|
-
console.log(`Core methods generated: ${coreMethodCount}`);
|
|
707
|
-
console.log(`Custom binding sources discovered: ${customBindings.length}`);
|
|
708
|
-
console.log(`Custom bindings generated: ${customBindingCount}`);
|
|
709
|
-
console.log('');
|
|
710
|
-
console.log(`Generated: ${relative(root, cppOut)}`);
|
|
711
|
-
console.log(`Generated: ${relative(root, tsOut)}`);
|
|
712
|
-
console.log(`Generated: ${relative(root, nativeCppOut)}`);
|
|
713
|
-
console.log(`Generated: ${relative(root, nativeCppTemplateOut)}`);
|
|
714
|
-
console.log(`Generated: ${relative(root, nativeTsOut)}`);
|
|
715
|
-
console.log(`Generated: ${relative(root, customCppOut)}`);
|
|
716
|
-
console.log(`Generated: ${relative(root, customCppTemplateOut)}`);
|
|
717
|
-
console.log(`Generated: ${relative(root, customTsOut)}`);
|
|
718
|
-
console.log(`Generated: ${relative(root, customHtmlOut)}`);
|
|
719
|
-
console.log(`Generated: ${relative(root, includeCppOut)}`);
|
|
720
|
-
console.log(`Generated: ${relative(root, includeNativeCppOut)}`);
|
|
721
|
-
console.log(`Generated: ${relative(root, includeCustomCppOut)}`);
|
|
722
|
-
console.log(`Generated: ${relative(root, reportOut)}`);
|
|
723
|
-
console.log('\n⨠Bindings generated successfully!\n');
|
|
679
|
+
console.log(`š Schema mode: ${schemaKind}`);
|
|
680
|
+
console.log(`Generated TS bindings: ${tsPath}`);
|
|
681
|
+
console.log(`Generated C++ bindings: ${cppPath}`);
|
|
682
|
+
console.log(`Synced legacy outputs: ${join(legacyTsOut, 'bridge.ts')} / ${join(legacyCppOut, 'bridge.hpp')}`);
|
|
724
683
|
}
|
|
725
684
|
|
|
726
685
|
async function main() {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
686
|
+
const args = process.argv.slice(2);
|
|
687
|
+
const projectRoot = args[0] || process.cwd();
|
|
688
|
+
|
|
689
|
+
const { methods, schemaPath, schemaKind } = await loadSchema(projectRoot);
|
|
690
|
+
console.log(`Using schema: ${schemaPath}`);
|
|
691
|
+
|
|
692
|
+
const normalizedMethods = methods.map((method) => ({
|
|
693
|
+
...method,
|
|
694
|
+
kind: LEGACY_KIND_MAP[method.kind] || method.kind,
|
|
695
|
+
}));
|
|
736
696
|
|
|
737
|
-
|
|
697
|
+
await writeOutputs(projectRoot, normalizedMethods, schemaKind);
|
|
698
|
+
console.log('ā
Bindings generation complete!');
|
|
738
699
|
}
|
|
739
700
|
|
|
740
|
-
main().catch(error => {
|
|
741
|
-
|
|
742
|
-
|
|
701
|
+
main().catch((error) => {
|
|
702
|
+
console.error(error);
|
|
703
|
+
process.exit(1);
|
|
743
704
|
});
|