plusui-native-connect 0.1.54
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 +14 -0
- package/src/advanced-bindgen.js +2 -0
- package/src/connect.js +2 -0
- package/src/index.js +202 -0
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "plusui-native-connect",
|
|
3
|
+
"version": "0.1.54",
|
|
4
|
+
"description": "Generate frontend ↔ backend connection bindings for PlusUI applications",
|
|
5
|
+
"main": "src/connect.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/connect.js",
|
|
9
|
+
"generate": "node src/connect.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"plusui-connect": "src/connect.js"
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/connect.js
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { mkdir, readFile, writeFile, readdir } from 'fs/promises';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { join, resolve, extname } from 'path';
|
|
6
|
+
|
|
7
|
+
const SCAN_DIR_CANDIDATES = ['src', 'frontend/src', 'Core/Features', 'Core/src', 'Core'];
|
|
8
|
+
const TS_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts']);
|
|
9
|
+
const CPP_EXTENSIONS = new Set(['.cpp', '.cc', '.cxx', '.hpp', '.h', '.hh', '.hxx']);
|
|
10
|
+
const IGNORE_SEGMENTS = new Set(['node_modules', '.git', 'dist', 'build', '.plusui', 'generated']);
|
|
11
|
+
|
|
12
|
+
const EMIT_PATTERN = /connect\s*\.\s*emit\s*\(\s*['\"]([^'\"]+)['\"]/g;
|
|
13
|
+
const ON_PATTERN = /connect\s*\.\s*on\s*\(\s*['\"]([^'\"]+)['\"]/g;
|
|
14
|
+
|
|
15
|
+
function isIgnored(path) {
|
|
16
|
+
const normalized = path.replace(/\\/g, '/');
|
|
17
|
+
return normalized
|
|
18
|
+
.split('/')
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.some((segment) => IGNORE_SEGMENTS.has(segment));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function walkFiles(rootDir, collected = []) {
|
|
24
|
+
if (!existsSync(rootDir) || isIgnored(rootDir)) {
|
|
25
|
+
return collected;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const full = join(rootDir, entry.name);
|
|
31
|
+
if (isIgnored(full)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (entry.isDirectory()) {
|
|
36
|
+
await walkFiles(full, collected);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ext = extname(entry.name).toLowerCase();
|
|
41
|
+
if (TS_EXTENSIONS.has(ext) || CPP_EXTENSIONS.has(ext)) {
|
|
42
|
+
collected.push(full);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return collected;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseConnectUsage(content) {
|
|
50
|
+
const emits = [];
|
|
51
|
+
const listens = [];
|
|
52
|
+
|
|
53
|
+
let match = null;
|
|
54
|
+
while ((match = EMIT_PATTERN.exec(content)) !== null) {
|
|
55
|
+
emits.push(match[1]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
while ((match = ON_PATTERN.exec(content)) !== null) {
|
|
59
|
+
listens.push(match[1]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { emits, listens };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toSortedUnique(values) {
|
|
66
|
+
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toPascalCase(value) {
|
|
70
|
+
return value
|
|
71
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
72
|
+
.split(' ')
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
75
|
+
.join('');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function generateTsBindings(channels, projectRoot) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
lines.push('/**');
|
|
81
|
+
lines.push(' * Auto-generated by plusui-connect');
|
|
82
|
+
lines.push(' * DO NOT EDIT - Changes will be overwritten');
|
|
83
|
+
lines.push(' * Source: connect.emit()/connect.on() usage scan');
|
|
84
|
+
lines.push(' */');
|
|
85
|
+
lines.push('import { connect as runtimeConnect } from "plusui-native-core/connect";');
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push(`export const CONNECT_PROJECT_ROOT = ${JSON.stringify(projectRoot.replace(/\\/g, '/'))};`);
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push('export const CONNECT_CHANNELS = [');
|
|
90
|
+
for (const channel of channels) {
|
|
91
|
+
lines.push(` ${JSON.stringify(channel)},`);
|
|
92
|
+
}
|
|
93
|
+
lines.push('] as const;');
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push('export type ConnectChannel = (typeof CONNECT_CHANNELS)[number];');
|
|
96
|
+
lines.push('export type ConnectPayload = Record<string, unknown>;');
|
|
97
|
+
lines.push('');
|
|
98
|
+
lines.push('export const connect = {');
|
|
99
|
+
lines.push(' emit(channel: ConnectChannel | string, payload: ConnectPayload = {}): void {');
|
|
100
|
+
lines.push(' runtimeConnect.emit(channel, payload);');
|
|
101
|
+
lines.push(' },');
|
|
102
|
+
lines.push(' on(channel: ConnectChannel | string, handler: (payload: ConnectPayload) => void): () => void {');
|
|
103
|
+
lines.push(' return runtimeConnect.on(channel, handler);');
|
|
104
|
+
lines.push(' },');
|
|
105
|
+
lines.push('};');
|
|
106
|
+
lines.push('');
|
|
107
|
+
|
|
108
|
+
for (const channel of channels) {
|
|
109
|
+
const base = toPascalCase(channel);
|
|
110
|
+
lines.push(`export const CHANNEL_${base} = ${JSON.stringify(channel)} as const;`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
lines.push('');
|
|
114
|
+
lines.push('export default connect;');
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function generateCppBindings(channels, projectRoot) {
|
|
119
|
+
const lines = [];
|
|
120
|
+
lines.push('/**');
|
|
121
|
+
lines.push(' * Auto-generated by plusui-connect');
|
|
122
|
+
lines.push(' * DO NOT EDIT - Changes will be overwritten');
|
|
123
|
+
lines.push(' * Source: connect.emit()/connect.on() usage scan');
|
|
124
|
+
lines.push(' */');
|
|
125
|
+
lines.push('#pragma once');
|
|
126
|
+
lines.push('#include <array>');
|
|
127
|
+
lines.push('#include <string_view>');
|
|
128
|
+
lines.push('');
|
|
129
|
+
lines.push('namespace plusui::connect {');
|
|
130
|
+
lines.push(`inline constexpr std::string_view kProjectRoot = ${JSON.stringify(projectRoot.replace(/\\/g, '/'))};`);
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push(`inline constexpr std::array<std::string_view, ${channels.length}> kChannels = {`);
|
|
133
|
+
for (const channel of channels) {
|
|
134
|
+
lines.push(` ${JSON.stringify(channel)},`);
|
|
135
|
+
}
|
|
136
|
+
lines.push('};');
|
|
137
|
+
lines.push('');
|
|
138
|
+
lines.push('} // namespace plusui::connect');
|
|
139
|
+
return lines.join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function generateManifest(channels, files) {
|
|
143
|
+
return JSON.stringify(
|
|
144
|
+
{
|
|
145
|
+
generatedBy: 'plusui-connect',
|
|
146
|
+
generatedAt: new Date().toISOString(),
|
|
147
|
+
channels,
|
|
148
|
+
filesScanned: files,
|
|
149
|
+
},
|
|
150
|
+
null,
|
|
151
|
+
2
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function main() {
|
|
156
|
+
const args = process.argv.slice(2);
|
|
157
|
+
const projectRoot = resolve(args[0] || process.cwd());
|
|
158
|
+
const outputDir = resolve(args[1] || join(projectRoot, 'src', 'Bindings'));
|
|
159
|
+
|
|
160
|
+
const scanRoots = SCAN_DIR_CANDIDATES.map((rel) => join(projectRoot, rel)).filter((dir) => existsSync(dir));
|
|
161
|
+
if (scanRoots.length === 0) {
|
|
162
|
+
throw new Error('No scan roots found. Expected one of: src, frontend/src, Core/Features, Core/src, Core');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const files = [];
|
|
166
|
+
for (const root of scanRoots) {
|
|
167
|
+
await walkFiles(root, files);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const emitted = [];
|
|
171
|
+
const listened = [];
|
|
172
|
+
|
|
173
|
+
for (const filePath of files) {
|
|
174
|
+
const content = await readFile(filePath, 'utf8');
|
|
175
|
+
const { emits, listens } = parseConnectUsage(content);
|
|
176
|
+
emitted.push(...emits);
|
|
177
|
+
listened.push(...listens);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const channels = toSortedUnique([...emitted, ...listened]);
|
|
181
|
+
|
|
182
|
+
await mkdir(outputDir, { recursive: true });
|
|
183
|
+
|
|
184
|
+
const tsOutPath = join(outputDir, 'bindings.gen.ts');
|
|
185
|
+
const cppOutPath = join(outputDir, 'bindings.gen.hpp');
|
|
186
|
+
const manifestPath = join(outputDir, 'connect.manifest.json');
|
|
187
|
+
|
|
188
|
+
await writeFile(tsOutPath, generateTsBindings(channels, projectRoot));
|
|
189
|
+
await writeFile(cppOutPath, generateCppBindings(channels, projectRoot));
|
|
190
|
+
await writeFile(manifestPath, generateManifest(channels, files.map((p) => p.replace(/\\/g, '/')).sort()));
|
|
191
|
+
|
|
192
|
+
console.log(`Scanned ${files.length} files`);
|
|
193
|
+
console.log(`Detected ${channels.length} channel(s)`);
|
|
194
|
+
console.log(`Generated: ${tsOutPath}`);
|
|
195
|
+
console.log(`Generated: ${cppOutPath}`);
|
|
196
|
+
console.log(`Generated: ${manifestPath}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
main().catch((error) => {
|
|
200
|
+
console.error(error);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
});
|