plusui-native 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +162 -0
  2. package/package.json +36 -0
  3. package/src/assets/icon-generator.js +251 -0
  4. package/src/assets/resource-embedder.js +351 -0
  5. package/src/doctor/detectors/cmake.js +84 -0
  6. package/src/doctor/detectors/compiler.js +145 -0
  7. package/src/doctor/detectors/just.js +45 -0
  8. package/src/doctor/detectors/nodejs.js +57 -0
  9. package/src/doctor/detectors/webview2.js +66 -0
  10. package/src/doctor/index.js +184 -0
  11. package/src/doctor/installers/linux.js +121 -0
  12. package/src/doctor/installers/macos.js +123 -0
  13. package/src/doctor/installers/windows.js +117 -0
  14. package/src/doctor/reporter.js +219 -0
  15. package/src/index.js +904 -0
  16. package/templates/base/Justfile +115 -0
  17. package/templates/base/README.md.template +168 -0
  18. package/templates/base/assets/README.md +88 -0
  19. package/templates/base/assets/icon.png +0 -0
  20. package/templates/manager.js +217 -0
  21. package/templates/react/.vscode/c_cpp_properties.json +24 -0
  22. package/templates/react/CMakeLists.txt.template +151 -0
  23. package/templates/react/frontend/index.html +12 -0
  24. package/templates/react/frontend/package.json.template +24 -0
  25. package/templates/react/frontend/src/App.tsx +134 -0
  26. package/templates/react/frontend/src/main.tsx +10 -0
  27. package/templates/react/frontend/src/styles/app.css +140 -0
  28. package/templates/react/frontend/tsconfig.json +21 -0
  29. package/templates/react/frontend/tsconfig.node.json +11 -0
  30. package/templates/react/frontend/vite.config.ts +14 -0
  31. package/templates/react/main.cpp.template +201 -0
  32. package/templates/react/package.json.template +23 -0
  33. package/templates/solid/.vscode/c_cpp_properties.json +24 -0
  34. package/templates/solid/CMakeLists.txt.template +151 -0
  35. package/templates/solid/frontend/index.html +12 -0
  36. package/templates/solid/frontend/package.json.template +21 -0
  37. package/templates/solid/frontend/src/App.tsx +133 -0
  38. package/templates/solid/frontend/src/main.tsx +5 -0
  39. package/templates/solid/frontend/src/styles/app.css +140 -0
  40. package/templates/solid/frontend/tsconfig.json +22 -0
  41. package/templates/solid/frontend/tsconfig.node.json +11 -0
  42. package/templates/solid/frontend/vite.config.ts +14 -0
  43. package/templates/solid/main.cpp.template +192 -0
  44. package/templates/solid/package.json.template +23 -0
package/README.md ADDED
@@ -0,0 +1,162 @@
1
+ # PlusUI
2
+
3
+ Build C++ desktop apps with web tech. Like Tauri, but for C++.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install CLI globally
9
+ npm install -g plusui-cli
10
+
11
+ # Create a new project
12
+ plusui create myapp
13
+ cd myapp
14
+
15
+ # Install dependencies
16
+ npm install
17
+
18
+ # Run in development mode (with HMR)
19
+ npm run dev
20
+
21
+ # Build for production
22
+ npm run build
23
+ ```
24
+
25
+ ## Project Structure
26
+
27
+ ```
28
+ myapp/
29
+ ├── src/
30
+ │ ├── main.cpp # C++ backend
31
+ │ ├── index.html # Frontend entry
32
+ │ └── ...
33
+ ├── plusui-core/ # Framework (git submodule)
34
+ ├── package.json
35
+ └── CMakeLists.txt
36
+ ```
37
+
38
+ ## C++ API
39
+
40
+ ```cpp
41
+ #include <plusui/app.hpp>
42
+
43
+ int main() {
44
+ auto wv = plusui::createApp()
45
+ .title("My App")
46
+ .width(1024)
47
+ .height(768)
48
+ .build();
49
+
50
+ // Navigate to frontend
51
+ wv.navigate("file://${CMAKE_CURRENT_SOURCE_DIR}/src/index.html");
52
+
53
+ // Listen for events from frontend
54
+ wv.on("greet", [](const std::string& name) {
55
+ std::cout << "Hello, " << name << "!" << std::endl;
56
+ });
57
+
58
+ // Run the app
59
+ plusui::createApp().run();
60
+ return 0;
61
+ }
62
+ ```
63
+
64
+ ## TypeScript API
65
+
66
+ ```typescript
67
+ import { createPlusUI } from '@plusui/core';
68
+
69
+ const plusui = createPlusUI();
70
+
71
+ // Call C++ function
72
+ const result = await plusui.invoke<number>('add', [1, 2]);
73
+
74
+ // Listen for events
75
+ plusui.on('server-event', (data) => {
76
+ console.log('Received:', data);
77
+ });
78
+
79
+ // Emit event to C++
80
+ plusui.emit('greet', 'World');
81
+
82
+ // One-time listener
83
+ plusui.once('ready', () => {
84
+ console.log('App ready!');
85
+ });
86
+ ```
87
+
88
+ ## Events vs Invoke
89
+
90
+ | Method | Direction | Use Case |
91
+ |--------|-----------|----------|
92
+ | `invoke()` | Frontend → Backend | Request/response |
93
+ | `emit()` | Frontend → Backend | Fire-and-forget |
94
+ | `on()` | Bidirectional | Subscriptions |
95
+
96
+ ## Configuration
97
+
98
+ ```typescript
99
+ const plusui = createPlusUI({
100
+ title: 'My App',
101
+ width: 800,
102
+ height: 600,
103
+ resizable: true,
104
+ devtools: true,
105
+ });
106
+ ```
107
+
108
+ ## Building
109
+
110
+ ```bash
111
+ # Development (auto-rebuild + HMR)
112
+ npm run dev
113
+
114
+ # Production build
115
+ npm run build
116
+
117
+ # Run built app
118
+ npm run start
119
+
120
+ # Regenerate bindings manually (app-level)
121
+ npm run bind
122
+ ```
123
+
124
+ Bindgen runs per app project (typically `src/features` -> `src/Bindings_Generated`).
125
+ `dev` and all `build` commands auto-refresh bindings first when `src/features` exists.
126
+
127
+ ## Requirements
128
+
129
+ - **Windows**: WebView2 Runtime (pre-installed on Win10/11)
130
+ - **macOS**: Xcode Command Line Tools
131
+ - **Linux**: WebKit2GTK (`sudo apt install libwebkit2gtk-4.0-dev`)
132
+
133
+ ## Architecture
134
+
135
+ ```
136
+ ┌─────────────────────────────────────┐
137
+ │ Frontend (Web) │
138
+ │ React / Vue / Svelte / Vanilla │
139
+ └─────────────────┬───────────────────┘
140
+ │ Events / Invoke
141
+ ┌─────────────────▼───────────────────┐
142
+ │ TypeScript SDK │
143
+ │ @plusui/core │
144
+ └─────────────────┬───────────────────┘
145
+ │ JSON IPC
146
+ ┌─────────────────▼───────────────────┐
147
+ │ C++ Core │
148
+ │ plusui::createApp() │
149
+ │ webview.h │
150
+ └─────────────────┬───────────────────┘
151
+
152
+ ┌─────────────────▼───────────────────┐
153
+ │ Native WebView │
154
+ │ Windows: WebView2 (Edge) │
155
+ │ macOS: WKWebView (WebKit) │
156
+ │ Linux: WebKitGTK │
157
+ └─────────────────────────────────────┘
158
+ ```
159
+
160
+ ## License
161
+
162
+ MIT
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "plusui-native",
3
+ "version": "0.2.0",
4
+ "description": "PlusUI CLI - Build C++ desktop apps with web tech",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "plusui": "./src/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/index.js",
12
+ "dev": "node src/index.js dev"
13
+ },
14
+ "keywords": ["desktop", "cpp", "webview", "gui"],
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "chokidar": "^3.5.3",
18
+ "vite": "^5.0.0",
19
+ "commander": "^12.0.0",
20
+ "chalk": "^5.3.0",
21
+ "ora": "^8.0.0",
22
+ "semver": "^7.6.0",
23
+ "which": "^4.0.0",
24
+ "execa": "^8.0.1",
25
+ "@plusui-native/builder": "*",
26
+ "@plusui-native/bindgen": "*"
27
+ },
28
+ "peerDependencies": {
29
+ "plusui-native-core": "*",
30
+ "plusui-native-bindgen": "*"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "preferGlobal": true
36
+ }
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Icon Generator for PlusUI
5
+ * Generates all platform-specific icon formats from a single source icon
6
+ *
7
+ * Supports:
8
+ * - Windows: .ico (16x16, 32x32, 48x48, 64x64, 128x128, 256x256)
9
+ * - macOS: .icns (16x16@1x/2x, 32x32@1x/2x, 128x128@1x/2x, 256x256@1x/2x, 512x512@1x/2x)
10
+ * - Linux: PNG (16x16, 32x32, 48x48, 64x64, 128x128, 256x256, 512x512)
11
+ * - Tray icons: 16x16, 32x32 (for system tray)
12
+ * - Taskbar/Title bar: 32x32, 48x48
13
+ */
14
+
15
+ import { readFile, writeFile, mkdir } from 'fs/promises';
16
+ import { existsSync } from 'fs';
17
+ import { join, basename, extname } from 'path';
18
+ import { execSync } from 'child_process';
19
+
20
+ const SIZES = {
21
+ windows: [16, 32, 48, 64, 128, 256],
22
+ macos: [16, 32, 128, 256, 512],
23
+ linux: [16, 32, 48, 64, 128, 256, 512],
24
+ tray: [16, 32],
25
+ taskbar: [32, 48],
26
+ };
27
+
28
+ class IconGenerator {
29
+ constructor() {
30
+ this.hasSharp = this.checkSharp();
31
+ this.hasImageMagick = this.checkImageMagick();
32
+ }
33
+
34
+ checkSharp() {
35
+ try {
36
+ require.resolve('sharp');
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ checkImageMagick() {
44
+ try {
45
+ execSync('magick -version', { stdio: 'ignore' });
46
+ return true;
47
+ } catch {
48
+ try {
49
+ execSync('convert -version', { stdio: 'ignore' });
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+ }
56
+
57
+ async checkDependencies() {
58
+ if (!this.hasSharp && !this.hasImageMagick) {
59
+ console.log('⚠️ No image processor found.');
60
+ console.log('\nOption 1 (Recommended): Install Sharp');
61
+ console.log(' npm install sharp --save-dev');
62
+ console.log('\nOption 2: Install ImageMagick');
63
+ console.log(' Windows: winget install ImageMagick.ImageMagick');
64
+ console.log(' macOS: brew install imagemagick');
65
+ console.log(' Linux: sudo apt install imagemagick');
66
+ return false;
67
+ }
68
+ return true;
69
+ }
70
+
71
+ async resizeWithSharp(inputPath, outputPath, size) {
72
+ const sharp = require('sharp');
73
+ await sharp(inputPath)
74
+ .resize(size, size, {
75
+ fit: 'contain',
76
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
77
+ })
78
+ .png()
79
+ .toFile(outputPath);
80
+ }
81
+
82
+ async resizeWithImageMagick(inputPath, outputPath, size) {
83
+ const cmd = this.hasImageMagick && execSync('which magick 2>/dev/null', { encoding: 'utf8' }).trim()
84
+ ? 'magick'
85
+ : 'convert';
86
+
87
+ execSync(
88
+ `${cmd} "${inputPath}" -resize ${size}x${size} -background none -gravity center -extent ${size}x${size} "${outputPath}"`,
89
+ { stdio: 'ignore' }
90
+ );
91
+ }
92
+
93
+ async resize(inputPath, outputPath, size) {
94
+ if (this.hasSharp) {
95
+ await this.resizeWithSharp(inputPath, outputPath, size);
96
+ } else {
97
+ await this.resizeWithImageMagick(inputPath, outputPath, size);
98
+ }
99
+ }
100
+
101
+ async generateWindowsIco(inputPath, outputPath) {
102
+ console.log(' 📦 Generating Windows .ico...');
103
+ const tempDir = join(process.cwd(), '.icon-temp');
104
+ await mkdir(tempDir, { recursive: true });
105
+
106
+ const pngPaths = [];
107
+ for (const size of SIZES.windows) {
108
+ const pngPath = join(tempDir, `icon_${size}.png`);
109
+ await this.resize(inputPath, pngPath, size);
110
+ pngPaths.push(pngPath);
111
+ }
112
+
113
+ // Try to use ImageMagick to create .ico
114
+ if (this.hasImageMagick) {
115
+ try {
116
+ const cmd = execSync('which magick 2>/dev/null', { encoding: 'utf8' }).trim() ? 'magick' : 'convert';
117
+ execSync(`${cmd} ${pngPaths.join(' ')} "${outputPath}"`, { stdio: 'ignore' });
118
+ console.log(' ✓ Created app.ico');
119
+ } catch (e) {
120
+ console.log(' ⚠️ Could not create .ico, using PNG fallback');
121
+ // Copy largest PNG as fallback
122
+ const fallback = pngPaths[pngPaths.length - 1];
123
+ await writeFile(outputPath.replace('.ico', '.png'), await readFile(fallback));
124
+ }
125
+ } else {
126
+ // Just copy PNGs
127
+ for (let i = 0; i < SIZES.windows.length; i++) {
128
+ const size = SIZES.windows[i];
129
+ const dest = outputPath.replace('.ico', `_${size}.png`);
130
+ await writeFile(dest, await readFile(pngPaths[i]));
131
+ console.log(` ✓ Created icon_${size}.png`);
132
+ }
133
+ }
134
+
135
+ // Cleanup temp
136
+ for (const p of pngPaths) {
137
+ try { await rm(p); } catch {}
138
+ }
139
+ }
140
+
141
+ async generateMacOSIcns(inputPath, outputPath) {
142
+ console.log(' 🍎 Generating macOS .icns...');
143
+ const iconsetDir = outputPath.replace('.icns', '.iconset');
144
+ await mkdir(iconsetDir, { recursive: true });
145
+
146
+ const sizes = [
147
+ [16, 'icon_16x16.png'],
148
+ [32, 'icon_16x16@2x.png'],
149
+ [32, 'icon_32x32.png'],
150
+ [64, 'icon_32x32@2x.png'],
151
+ [128, 'icon_128x128.png'],
152
+ [256, 'icon_128x128@2x.png'],
153
+ [256, 'icon_256x256.png'],
154
+ [512, 'icon_256x256@2x.png'],
155
+ [512, 'icon_512x512.png'],
156
+ [1024, 'icon_512x512@2x.png'],
157
+ ];
158
+
159
+ for (const [size, filename] of sizes) {
160
+ const dest = join(iconsetDir, filename);
161
+ await this.resize(inputPath, dest, size);
162
+ }
163
+
164
+ // Try to create .icns on macOS
165
+ if (process.platform === 'darwin') {
166
+ try {
167
+ execSync(`iconutil -c icns "${iconsetDir}" -o "${outputPath}"`, { stdio: 'ignore' });
168
+ console.log(' ✓ Created app.icns');
169
+ } catch {
170
+ console.log(' ⚠️ Could not create .icns, keeping .iconset folder');
171
+ }
172
+ } else {
173
+ console.log(' ℹ️ .iconset folder created (use iconutil on macOS to convert)');
174
+ }
175
+ }
176
+
177
+ async generateLinuxIcons(inputPath, outputDir) {
178
+ console.log(' 🐧 Generating Linux icons...');
179
+ await mkdir(outputDir, { recursive: true });
180
+
181
+ for (const size of SIZES.linux) {
182
+ const dest = join(outputDir, `icon_${size}x${size}.png`);
183
+ await this.resize(inputPath, dest, size);
184
+ console.log(` ✓ Created icon_${size}x${size}.png`);
185
+ }
186
+ }
187
+
188
+ async generateTrayIcons(inputPath, outputDir) {
189
+ console.log(' 🔔 Generating tray icons...');
190
+ await mkdir(outputDir, { recursive: true });
191
+
192
+ for (const size of SIZES.tray) {
193
+ const dest = join(outputDir, `tray_${size}x${size}.png`);
194
+ await this.resize(inputPath, dest, size);
195
+ console.log(` ✓ Created tray_${size}x${size}.png`);
196
+ }
197
+ }
198
+
199
+ async generate(inputPath, outputBase) {
200
+ console.log(`\n🎨 Generating icons from: ${basename(inputPath)}\n`);
201
+
202
+ if (!existsSync(inputPath)) {
203
+ throw new Error(`Input icon not found: ${inputPath}`);
204
+ }
205
+
206
+ const hasDeps = await this.checkDependencies();
207
+ if (!hasDeps) {
208
+ throw new Error('Missing dependencies');
209
+ }
210
+
211
+ // Create output directories
212
+ const dirs = {
213
+ windows: join(outputBase, 'windows'),
214
+ macos: join(outputBase, 'macos'),
215
+ linux: join(outputBase, 'linux'),
216
+ tray: join(outputBase, 'tray'),
217
+ };
218
+
219
+ for (const dir of Object.values(dirs)) {
220
+ await mkdir(dir, { recursive: true });
221
+ }
222
+
223
+ // Generate platform-specific icons
224
+ await this.generateWindowsIco(inputPath, join(dirs.windows, 'app.ico'));
225
+ await this.generateMacOSIcns(inputPath, join(dirs.macos, 'app.icns'));
226
+ await this.generateLinuxIcons(inputPath, dirs.linux);
227
+ await this.generateTrayIcons(inputPath, dirs.tray);
228
+
229
+ console.log('\n✨ Icon generation complete!\n');
230
+ console.log('Generated icons:');
231
+ console.log(` 📁 ${dirs.windows}`);
232
+ console.log(` 📁 ${dirs.macos}`);
233
+ console.log(` 📁 ${dirs.linux}`);
234
+ console.log(` 📁 ${dirs.tray}`);
235
+ }
236
+ }
237
+
238
+ // CLI
239
+ if (import.meta.url === `file://${process.argv[1]}`) {
240
+ const args = process.argv.slice(2);
241
+ const inputPath = args[0] || join(process.cwd(), 'assets', 'icon.png');
242
+ const outputBase = args[1] || join(process.cwd(), 'assets', 'icons');
243
+
244
+ const generator = new IconGenerator();
245
+ generator.generate(inputPath, outputBase).catch(err => {
246
+ console.error('❌ Error:', err.message);
247
+ process.exit(1);
248
+ });
249
+ }
250
+
251
+ export { IconGenerator };