plusui-native-bindgen 0.1.49 → 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.
@@ -1,743 +1,704 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readdir, readFile, writeFile, mkdir, stat } from 'fs/promises';
3
+ import { mkdir, readFile, writeFile, readdir } from 'fs/promises';
4
4
  import { existsSync } from 'fs';
5
- import { join, relative, basename, resolve } from 'path';
6
-
7
- const IGNORE_DIRS = new Set([
8
- 'node_modules',
9
- '.git',
10
- '.svn',
11
- '.hg',
12
- '.next',
13
- '.nuxt',
14
- '.plusui',
15
- 'build',
16
- 'dist',
17
- 'out',
18
- '.idea',
19
- '.vscode',
20
- ]);
21
-
22
- const WEB_IO = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.html']);
23
- const CPP_IO = new Set(['.h', '.hpp', '.hh', '.hxx', '.cpp', '.cc', '.cxx']);
24
-
25
- const CORE_SYMBOL_TO_FEATURE = {
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
- function isIgnoredPathSegment(name) {
42
- return IGNORE_DIRS.has(name);
43
- }
44
-
45
- function getExtension(fileName) {
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
- return out;
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 normalizeCppType(rawType) {
83
- const cleaned = rawType.trim().toLowerCase();
84
-
85
- if (cleaned === 'void') return 'void';
86
- if (cleaned === 'bool' || cleaned === 'boolean') return 'boolean';
87
- if (cleaned === 'int' || cleaned === 'int32_t' || cleaned === 'int64_t' || cleaned === 'float' || cleaned === 'double') return 'number';
88
- if (cleaned.includes('std::string') || cleaned === 'string') return 'string';
89
- if (cleaned.includes('json') || cleaned.includes('nlohmann::json')) return 'any';
90
- return 'any';
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 pascalCase(value) {
94
- return value
95
- .split(/[^a-zA-Z0-9]+/)
96
- .filter(Boolean)
97
- .map(part => part.charAt(0).toUpperCase() + part.slice(1))
98
- .join('');
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 camelCase(value) {
102
- const p = pascalCase(value);
103
- return p ? p.charAt(0).toLowerCase() + p.slice(1) : value;
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 findCoreFeaturesDir(projectRoot) {
107
- const candidates = [
108
- join(projectRoot, 'node_modules', 'plusui-native-core', 'Core', 'Features'),
109
- join(projectRoot, 'node_modules', 'plusui-native-core', 'Features'),
110
- join(projectRoot, 'Core', 'Features'),
111
- ];
112
-
113
- for (const dir of candidates) {
114
- if (existsSync(dir)) {
115
- return dir;
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
- return null;
120
- }
117
+ const opens = (line.match(/{/g) || []).length;
118
+ const closes = (line.match(/}/g) || []).length;
119
+ braceCount += opens - closes;
121
120
 
122
- function collectNamedImports(content) {
123
- const named = new Set();
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
- const requireRegex = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"]plusui-native-core['"]\s*\)/g;
139
- while ((match = requireRegex.exec(content)) !== null) {
140
- const names = match[1]
141
- .split(',')
142
- .map(part => part.trim())
143
- .filter(Boolean)
144
- .map(part => part.split(':')[0].trim());
145
- for (const name of names) {
146
- named.add(name);
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
- return named;
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
- for (const importedName of collectNamedImports(content)) {
167
- const feature = CORE_SYMBOL_TO_FEATURE[importedName.toLowerCase()];
168
- if (feature) {
169
- used.add(feature);
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
- for (const alias of collectNamespaceImportAliases(content)) {
174
- const nsUsageRegex = new RegExp(`\\b${alias}\\.(\\w+)\\b`, 'g');
175
- let match;
176
- while ((match = nsUsageRegex.exec(content)) !== null) {
177
- const symbol = match[1].toLowerCase();
178
- const feature = CORE_SYMBOL_TO_FEATURE[symbol];
179
- if (feature) {
180
- used.add(feature);
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
- const directUsageRegex = /\b(app|browser|router|clipboard|display|keyboard|menu|tray|webgpu|webview|window|event|events)\s*\./g;
186
- let usageMatch;
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
- return used;
184
+ return methods;
195
185
  }
196
186
 
197
- async function detectUsedCoreFeatures(projectRoot) {
198
- const sourceFiles = await walkFiles(
199
- projectRoot,
200
- (filePath, fileName) => {
201
- const ext = getExtension(fileName);
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
- return usedFeatures;
217
- }
218
-
219
- async function parseCoreFeatureMethods(coreFeaturesDir, usedFeatures) {
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
- if (!coreFeaturesDir || usedFeatures.size === 0) {
223
- return coreMethods;
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
- for (const featureFolder of usedFeatures) {
227
- const featureDir = join(coreFeaturesDir, featureFolder);
228
- if (!existsSync(featureDir)) {
229
- continue;
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
- return coreMethods;
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
- async function parseCustomBackendBindings(projectRoot) {
256
- const backendFiles = await walkFiles(
257
- projectRoot,
258
- (filePath, fileName) => CPP_IO.has(getExtension(fileName))
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 generateCppBindings(customBindings, coreFeatureMethods) {
320
- const lines = [];
321
- lines.push('// Auto-generated by plusui-bindgen');
322
- lines.push('// App-scoped binding metadata (no core/package files are modified)');
323
- lines.push('#pragma once');
324
- lines.push('');
325
- lines.push('#include <string>');
326
- lines.push('#include <vector>');
327
- lines.push('');
328
- lines.push('namespace plusui::bindings {');
329
- lines.push('');
330
- lines.push('struct BindingEntry {');
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
- lines.push(' };');
352
- lines.push(' return entries;');
353
- lines.push('}');
354
- lines.push('');
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
- return lines.join('\n');
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 generateCoreTsClass(featureName, methods) {
362
- const className = `${pascalCase(featureName)}Bindings`;
363
- const instanceName = camelCase(featureName);
364
- const lines = [];
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
- lines.push(`/**`);
367
- lines.push(` * ${featureName} - Native Core Bindings`);
368
- lines.push(` */`);
369
- lines.push(`export class ${className} {`);
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
- for (const method of methods) {
372
- lines.push('');
373
- lines.push(` /** Core method: ${featureName}.${method} */`);
374
- lines.push(` async ${method}(...args: unknown[]): Promise<unknown> {`);
375
- lines.push(` return invoke("${featureName.toLowerCase()}.${method}", args);`);
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
- if (methods.length === 0) {
380
- lines.push(' // No methods discovered');
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
- return { className, instanceName, code: lines.join('\n') };
386
- }
387
-
388
- function generateCustomTsClass(bindingSource) {
389
- const className = `${pascalCase(bindingSource.name)}Bindings`;
390
- const instanceName = camelCase(bindingSource.name);
391
- const lines = [];
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
- lines.push(`/**`);
394
- lines.push(` * ${bindingSource.name} - Custom Bindings`);
395
- lines.push(` * Generated from: ${basename(bindingSource.sourcePath)}`);
396
- lines.push(` */`);
397
- lines.push(`export class ${className} {`);
398
-
399
- // Generate methods (frontend → backend calls)
400
- for (const operation of bindingSource.operations) {
401
- const params = operation.params.map((param, idx) => `${param.name}: ${param.type === 'void' ? 'unknown' : param.type}`).join(', ');
402
- const argNames = operation.params.map(param => param.name).join(', ');
403
- const returnType = operation.returnType === 'void' ? 'void' : operation.returnType;
404
- const promiseType = `Promise<${returnType}>`;
405
-
406
- lines.push('');
407
- lines.push(` /** Call backend method: ${operation.name} */`);
408
- lines.push(` async ${operation.name}(${params}): ${promiseType} {`);
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
- // Generate event listeners (backend → frontend events)
418
- for (const eventItem of bindingSource.events) {
419
- lines.push('');
420
- lines.push(` /** Listen to backend event: ${eventItem.name} */`);
421
- lines.push(` ${eventItem.name}(handler: (data: ${eventItem.dataType}) => void): () => void {`);
422
- lines.push(` const eventName = "plusui:binding:${bindingSource.name}.${eventItem.name}";`);
423
- lines.push(' const listener = (e: Event) => {');
424
- lines.push(` handler((e as CustomEvent<${eventItem.dataType}>).detail);`);
425
- lines.push(' };');
426
- lines.push(' window.addEventListener(eventName, listener);');
427
- lines.push(' return () => window.removeEventListener(eventName, listener);');
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 (bindingSource.operations.length === 0 && bindingSource.events.length === 0) {
432
- lines.push(' // No bindings discovered');
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
- lines.push('}');
436
- lines.push('');
437
- return { className, instanceName, code: lines.join('\n') };
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
- function generateTsBindings(coreFeatureMethods, customBindings) {
441
- const lines = [];
442
- lines.push('/**');
443
- lines.push(' * PlusUI Generated Bindings');
444
- lines.push(' * Auto-generated by plusui-bindgen');
445
- lines.push(' * ');
446
- lines.push(' * Usage:');
447
- lines.push(' * import { plusui } from "./bindings.gen";');
448
- lines.push(' * ');
449
- lines.push(' * // Call backend methods');
450
- lines.push(' * await plusui.myFeature.myMethod(args);');
451
- lines.push(' * ');
452
- lines.push(' * // Listen to backend events');
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
- const bindingEntries = [];
473
- const bindingExports = [];
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
- // Generate native core feature bindings
476
- for (const [featureName, methods] of coreFeatureMethods.entries()) {
477
- const generated = generateCoreTsClass(featureName, methods);
478
- lines.push(generated.code);
479
- bindingEntries.push(` ${generated.instanceName}: new ${generated.className}(),`);
480
- bindingExports.push(generated.instanceName);
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
- // Generate custom bindings
484
- for (const bindingSource of customBindings) {
485
- const generated = generateCustomTsClass(bindingSource);
486
- lines.push(generated.code);
487
- bindingEntries.push(` ${generated.instanceName}: new ${generated.className}(),`);
488
- bindingExports.push(generated.instanceName);
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
- // Create unified export
492
- lines.push('/**');
493
- lines.push(' * Unified PlusUI binding interface');
494
- lines.push(' * Access all native and custom bindings through this object');
495
- lines.push(' */');
496
- lines.push('export const plusui = {');
497
- if (bindingEntries.length === 0) {
498
- lines.push(' // No bindings discovered yet');
499
- lines.push(' // Add PLUSUI_BIND annotations to your C++ code and run: plusui generate');
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
- for (const entry of bindingEntries) {
502
- lines.push(entry);
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
- // Also export individually for flexibility
509
- if (bindingExports.length > 0) {
510
- lines.push('// Individual exports (alternative import style)');
511
- for (const exportName of bindingExports) {
512
- lines.push(`export const ${exportName} = plusui.${exportName};`);
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('export default plusui;');
559
+ lines.push(' }');
518
560
  lines.push('');
519
-
520
- return lines.join('\n');
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 generateCustomCppTemplate(customBindings) {
524
- const lines = [];
525
- lines.push('// Auto-generated by plusui-bindgen');
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
- function generateNativeCppTemplate(coreFeatureMethods) {
550
- const lines = [];
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
- if (coreFeatureMethods.size === 0) {
558
- lines.push('// No native core bindings detected for this app yet.');
559
- return lines.join('\n');
560
- }
663
+ const tsContent = generateConnectionTs(methods);
664
+ const cppContent = generateConnectionCpp(methods);
561
665
 
562
- for (const [featureName, methods] of coreFeatureMethods.entries()) {
563
- lines.push(`// Native feature: ${featureName}`);
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
- return lines.join('\n');
571
- }
669
+ await writeFile(tsPath, tsContent);
670
+ await writeFile(cppPath, cppContent);
572
671
 
573
- function generateFrontendHtmlSummary(coreFeatureMethods, customBindings) {
574
- const coreRows = Array.from(coreFeatureMethods.entries())
575
- .map(([feature, methods]) => `<tr><td>${feature}</td><td>native</td><td>${methods.join(', ') || '-'}</td></tr>`)
576
- .join('');
577
-
578
- const customRows = customBindings
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
- async function generateBindings(projectRoot, outputDir) {
609
- const root = resolve(projectRoot);
610
- const coreFeaturesDir = findCoreFeaturesDir(root);
611
- const includeOutputDir = join(root, 'include', 'Bindings');
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
- const args = process.argv.slice(2);
728
- const projectRoot = args[0] || '.';
729
- const outputDir = args[1] || './src/Bindings';
730
-
731
- const rootStats = await stat(projectRoot).catch(() => null);
732
- if (!rootStats || !rootStats.isDirectory()) {
733
- console.log(`āš ļø Project root is missing or not a directory: ${projectRoot}`);
734
- process.exit(1);
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
- await generateBindings(projectRoot, outputDir);
697
+ await writeOutputs(projectRoot, normalizedMethods, schemaKind);
698
+ console.log('āœ… Bindings generation complete!');
738
699
  }
739
700
 
740
- main().catch(error => {
741
- console.error('āŒ Error:', error.message);
742
- process.exit(1);
701
+ main().catch((error) => {
702
+ console.error(error);
703
+ process.exit(1);
743
704
  });