plusui-native 0.2.107 ā 0.2.109
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 +4 -4
- package/src/assets/icon-generator.js +251 -251
- package/src/assets/resource-embedder.js +351 -351
- package/src/index.js +1357 -1357
- package/templates/base/Justfile +115 -115
- package/templates/base/README.md.template +185 -185
- package/templates/base/assets/README.md +88 -88
- package/templates/manager.js +261 -261
- package/templates/react/CMakeLists.txt.template +199 -199
- package/templates/react/frontend/vite.config.ts +36 -36
- package/templates/react/main.cpp.template +109 -109
- package/templates/solid/CMakeLists.txt.template +199 -199
- package/templates/solid/frontend/vite.config.ts +36 -36
- package/templates/solid/main.cpp.template +109 -109
|
@@ -1,351 +1,351 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Cross-Platform Resource Embedder for PlusUI
|
|
5
|
-
*
|
|
6
|
-
* Embeds frontend assets and icons into the binary for single-executable distribution:
|
|
7
|
-
* - Windows: Generates .rc file with RCDATA resources
|
|
8
|
-
* - macOS: Copies resources to app bundle
|
|
9
|
-
* - Linux: Generates C++ header with binary data
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readFile, writeFile, readdir, mkdir, stat, copyFile } from 'fs/promises';
|
|
13
|
-
import { existsSync, statSync } from 'fs';
|
|
14
|
-
import { join, relative, basename, extname, dirname } from 'path';
|
|
15
|
-
import { createHash } from 'crypto';
|
|
16
|
-
|
|
17
|
-
class ResourceEmbedder {
|
|
18
|
-
constructor(options = {}) {
|
|
19
|
-
this.verbose = options.verbose || false;
|
|
20
|
-
this.compression = options.compression !== false;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
log(msg) {
|
|
24
|
-
if (this.verbose) console.log(msg);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Get all files recursively from a directory
|
|
29
|
-
*/
|
|
30
|
-
async getAllFiles(dirPath, arrayOfFiles = []) {
|
|
31
|
-
const files = await readdir(dirPath);
|
|
32
|
-
|
|
33
|
-
for (const file of files) {
|
|
34
|
-
const filePath = join(dirPath, file);
|
|
35
|
-
const s = await stat(filePath);
|
|
36
|
-
|
|
37
|
-
if (s.isDirectory()) {
|
|
38
|
-
arrayOfFiles = await this.getAllFiles(filePath, arrayOfFiles);
|
|
39
|
-
} else {
|
|
40
|
-
arrayOfFiles.push(filePath);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return arrayOfFiles;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Generate Windows .rc resource file
|
|
49
|
-
*/
|
|
50
|
-
async generateWindowsRC(assetsDir, outputPath) {
|
|
51
|
-
console.log(' šŖ Generating Windows resource file (.rc)...');
|
|
52
|
-
|
|
53
|
-
if (!existsSync(assetsDir)) {
|
|
54
|
-
console.log(' ā ļø Assets directory not found, skipping');
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const files = await this.getAllFiles(assetsDir);
|
|
59
|
-
let rcContent = `// Auto-generated by PlusUI - DO NOT EDIT
|
|
60
|
-
// Embedded resources for single-executable distribution
|
|
61
|
-
|
|
62
|
-
#include <windows.h>
|
|
63
|
-
|
|
64
|
-
`;
|
|
65
|
-
|
|
66
|
-
let count = 0;
|
|
67
|
-
const resources = [];
|
|
68
|
-
|
|
69
|
-
for (const file of files) {
|
|
70
|
-
const relPath = relative(assetsDir, file).replace(/\\/g, '/');
|
|
71
|
-
const resourceId = `IDR_${relPath.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`;
|
|
72
|
-
const winPath = file.replace(/\//g, '\\\\');
|
|
73
|
-
|
|
74
|
-
// Store for manifest
|
|
75
|
-
resources.push({ id: resourceId, path: relPath, file });
|
|
76
|
-
|
|
77
|
-
rcContent += `${resourceId} RCDATA "${winPath}"\n`;
|
|
78
|
-
count++;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Add icon if exists
|
|
82
|
-
const iconPaths = [
|
|
83
|
-
join(assetsDir, 'icons', 'windows', 'app.ico'),
|
|
84
|
-
join(assetsDir, 'icon.ico'),
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
for (const iconPath of iconPaths) {
|
|
88
|
-
if (existsSync(iconPath)) {
|
|
89
|
-
rcContent += `\n// Application icon\nIDI_APP_ICON ICON "${iconPath.replace(/\//g, '\\\\')}"\n`;
|
|
90
|
-
console.log(` ā Added app icon: ${basename(iconPath)}`);
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
96
|
-
await writeFile(outputPath, rcContent);
|
|
97
|
-
|
|
98
|
-
// Generate resource manifest (C++ header)
|
|
99
|
-
await this.generateResourceManifest(resources, outputPath.replace('.rc', '_manifest.hpp'));
|
|
100
|
-
|
|
101
|
-
console.log(` ā Created ${basename(outputPath)} (${count} resources)`);
|
|
102
|
-
return outputPath;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Generate C++ header with resource manifest
|
|
107
|
-
*/
|
|
108
|
-
async generateResourceManifest(resources, outputPath) {
|
|
109
|
-
let content = `// Auto-generated by PlusUI - DO NOT EDIT
|
|
110
|
-
#pragma once
|
|
111
|
-
|
|
112
|
-
#include <string>
|
|
113
|
-
#include <unordered_map>
|
|
114
|
-
|
|
115
|
-
namespace plusui {
|
|
116
|
-
namespace resources {
|
|
117
|
-
|
|
118
|
-
// Resource ID to path mapping
|
|
119
|
-
static const std::unordered_map<std::string, int> RESOURCE_MAP = {
|
|
120
|
-
`;
|
|
121
|
-
|
|
122
|
-
let idCounter = 100;
|
|
123
|
-
for (const res of resources) {
|
|
124
|
-
content += ` {"${res.path}", ${idCounter}},\n`;
|
|
125
|
-
idCounter++;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
content += `};
|
|
129
|
-
|
|
130
|
-
// Get resource data from embedded resources
|
|
131
|
-
#ifdef _WIN32
|
|
132
|
-
#include <windows.h>
|
|
133
|
-
inline std::string getResource(const std::string& path) {
|
|
134
|
-
auto it = RESOURCE_MAP.find(path);
|
|
135
|
-
if (it == RESOURCE_MAP.end()) return "";
|
|
136
|
-
|
|
137
|
-
HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(it->second), RT_RCDATA);
|
|
138
|
-
if (!hRes) return "";
|
|
139
|
-
|
|
140
|
-
HGLOBAL hData = LoadResource(NULL, hRes);
|
|
141
|
-
if (!hData) return "";
|
|
142
|
-
|
|
143
|
-
DWORD size = SizeofResource(NULL, hRes);
|
|
144
|
-
const char* data = static_cast<const char*>(LockResource(hData));
|
|
145
|
-
|
|
146
|
-
return std::string(data, size);
|
|
147
|
-
}
|
|
148
|
-
#else
|
|
149
|
-
inline std::string getResource(const std::string& path) {
|
|
150
|
-
// Use embedded data from binary (see embedded_resources.hpp)
|
|
151
|
-
extern const std::unordered_map<std::string, std::pair<const unsigned char*, size_t>>& EMBEDDED_DATA;
|
|
152
|
-
auto it = EMBEDDED_DATA.find(path);
|
|
153
|
-
if (it == EMBEDDED_DATA.end()) return "";
|
|
154
|
-
return std::string(reinterpret_cast<const char*>(it->second.first), it->second.second);
|
|
155
|
-
}
|
|
156
|
-
#endif
|
|
157
|
-
|
|
158
|
-
} // namespace resources
|
|
159
|
-
} // namespace plusui
|
|
160
|
-
`;
|
|
161
|
-
|
|
162
|
-
await writeFile(outputPath, content);
|
|
163
|
-
console.log(` ā Created resource manifest`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Generate Linux/macOS binary embedding header
|
|
168
|
-
*/
|
|
169
|
-
async generateBinaryHeader(assetsDir, outputPath) {
|
|
170
|
-
console.log(' š§ Generating binary embedded resources...');
|
|
171
|
-
|
|
172
|
-
if (!existsSync(assetsDir)) {
|
|
173
|
-
console.log(' ā ļø Assets directory not found, skipping');
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const files = await this.getAllFiles(assetsDir);
|
|
178
|
-
|
|
179
|
-
let content = `// Auto-generated by PlusUI - DO NOT EDIT
|
|
180
|
-
#pragma once
|
|
181
|
-
|
|
182
|
-
#include <unordered_map>
|
|
183
|
-
#include <utility>
|
|
184
|
-
#include <string>
|
|
185
|
-
|
|
186
|
-
namespace plusui {
|
|
187
|
-
namespace resources {
|
|
188
|
-
|
|
189
|
-
`;
|
|
190
|
-
|
|
191
|
-
let count = 0;
|
|
192
|
-
const dataMap = [];
|
|
193
|
-
|
|
194
|
-
for (const file of files) {
|
|
195
|
-
const relPath = relative(assetsDir, file).replace(/\\/g, '/');
|
|
196
|
-
const varName = `ASSET_${relPath.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`;
|
|
197
|
-
const data = await readFile(file);
|
|
198
|
-
|
|
199
|
-
content += `// ${relPath}\nstatic const unsigned char ${varName}[] = {\n`;
|
|
200
|
-
|
|
201
|
-
for (let i = 0; i < data.length; i++) {
|
|
202
|
-
if (i % 16 === 0) content += ' ';
|
|
203
|
-
content += `0x${data[i].toString(16).padStart(2, '0')}`;
|
|
204
|
-
if (i < data.length - 1) content += ',';
|
|
205
|
-
if ((i + 1) % 16 === 0 || i === data.length - 1) content += '\n';
|
|
206
|
-
else content += ' ';
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
content += `};\nstatic const size_t ${varName}_LEN = ${data.length};\n\n`;
|
|
210
|
-
|
|
211
|
-
dataMap.push({ path: relPath, varName, size: data.length });
|
|
212
|
-
count++;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Generate lookup map
|
|
216
|
-
content += `// Resource lookup map\nstatic const std::unordered_map<std::string, std::pair<const unsigned char*, size_t>> EMBEDDED_DATA = {\n`;
|
|
217
|
-
|
|
218
|
-
for (const item of dataMap) {
|
|
219
|
-
content += ` {"${item.path}", {${item.varName}, ${item.varName}_LEN}},\n`;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
content += `};\n\n`;
|
|
223
|
-
|
|
224
|
-
// Add convenience function
|
|
225
|
-
content += `inline std::string getResource(const std::string& path) {
|
|
226
|
-
auto it = EMBEDDED_DATA.find(path);
|
|
227
|
-
if (it == EMBEDDED_DATA.end()) return "";
|
|
228
|
-
return std::string(reinterpret_cast<const char*>(it->second.first), it->second.second);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
} // namespace resources
|
|
232
|
-
} // namespace plusui
|
|
233
|
-
`;
|
|
234
|
-
|
|
235
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
236
|
-
await writeFile(outputPath, content);
|
|
237
|
-
|
|
238
|
-
console.log(` ā Created ${basename(outputPath)} (${count} resources)`);
|
|
239
|
-
return outputPath;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Generate macOS app bundle resources
|
|
244
|
-
*/
|
|
245
|
-
async generateMacOSBundle(assetsDir, bundlePath) {
|
|
246
|
-
console.log(' š Copying resources to macOS bundle...');
|
|
247
|
-
|
|
248
|
-
if (!existsSync(assetsDir)) {
|
|
249
|
-
console.log(' ā ļø Assets directory not found, skipping');
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const resourcesDir = join(bundlePath, 'Contents', 'Resources');
|
|
254
|
-
await mkdir(resourcesDir, { recursive: true });
|
|
255
|
-
|
|
256
|
-
const files = await this.getAllFiles(assetsDir);
|
|
257
|
-
let count = 0;
|
|
258
|
-
|
|
259
|
-
for (const file of files) {
|
|
260
|
-
const relPath = relative(assetsDir, file);
|
|
261
|
-
const dest = join(resourcesDir, relPath);
|
|
262
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
263
|
-
await copyFile(file, dest);
|
|
264
|
-
count++;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Copy icon if exists
|
|
268
|
-
const iconPath = join(assetsDir, 'icons', 'macos', 'app.icns');
|
|
269
|
-
if (existsSync(iconPath)) {
|
|
270
|
-
await copyFile(iconPath, join(resourcesDir, 'app.icns'));
|
|
271
|
-
console.log(` ā Added app icon`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
console.log(` ā Copied ${count} resources to bundle`);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Main embed function - chooses platform
|
|
279
|
-
*/
|
|
280
|
-
async embed(assetsDir, outputDir, platform = process.platform) {
|
|
281
|
-
console.log(`\nš¦ Embedding resources for ${platform}...\n`);
|
|
282
|
-
|
|
283
|
-
await mkdir(outputDir, { recursive: true });
|
|
284
|
-
|
|
285
|
-
switch (platform) {
|
|
286
|
-
case 'win32':
|
|
287
|
-
return await this.generateWindowsRC(
|
|
288
|
-
assetsDir,
|
|
289
|
-
join(outputDir, 'resources.rc')
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
case 'darwin':
|
|
293
|
-
// macOS uses both binary embedding and bundle resources
|
|
294
|
-
await this.generateBinaryHeader(
|
|
295
|
-
assetsDir,
|
|
296
|
-
join(outputDir, 'embedded_resources.hpp')
|
|
297
|
-
);
|
|
298
|
-
// Bundle copying happens at app bundle creation time
|
|
299
|
-
break;
|
|
300
|
-
|
|
301
|
-
case 'linux':
|
|
302
|
-
return await this.generateBinaryHeader(
|
|
303
|
-
assetsDir,
|
|
304
|
-
join(outputDir, 'embedded_resources.hpp')
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
default:
|
|
308
|
-
throw new Error(`Unsupported platform: ${platform}`);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
console.log('\n⨠Resource embedding complete!\n');
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Embed for all platforms
|
|
316
|
-
*/
|
|
317
|
-
async embedAll(assetsDir, outputDir) {
|
|
318
|
-
console.log('\nš¦ Embedding resources for all platforms...\n');
|
|
319
|
-
|
|
320
|
-
await this.embed(assetsDir, join(outputDir, 'windows'), 'win32');
|
|
321
|
-
await this.embed(assetsDir, join(outputDir, 'linux'), 'linux');
|
|
322
|
-
await this.embed(assetsDir, join(outputDir, 'darwin'), 'darwin');
|
|
323
|
-
|
|
324
|
-
console.log('\n⨠All platforms complete!\n');
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// CLI
|
|
329
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
330
|
-
const args = process.argv.slice(2);
|
|
331
|
-
const command = args[0];
|
|
332
|
-
const assetsDir = args[1] || join(process.cwd(), 'frontend', 'dist');
|
|
333
|
-
const outputDir = args[2] || join(process.cwd(), 'generated', 'resources');
|
|
334
|
-
|
|
335
|
-
const embedder = new ResourceEmbedder({ verbose: true });
|
|
336
|
-
|
|
337
|
-
if (command === 'all') {
|
|
338
|
-
embedder.embedAll(assetsDir, outputDir).catch(err => {
|
|
339
|
-
console.error('ā Error:', err.message);
|
|
340
|
-
process.exit(1);
|
|
341
|
-
});
|
|
342
|
-
} else {
|
|
343
|
-
const platform = command || process.platform;
|
|
344
|
-
embedder.embed(assetsDir, outputDir, platform).catch(err => {
|
|
345
|
-
console.error('ā Error:', err.message);
|
|
346
|
-
process.exit(1);
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
export { ResourceEmbedder };
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cross-Platform Resource Embedder for PlusUI
|
|
5
|
+
*
|
|
6
|
+
* Embeds frontend assets and icons into the binary for single-executable distribution:
|
|
7
|
+
* - Windows: Generates .rc file with RCDATA resources
|
|
8
|
+
* - macOS: Copies resources to app bundle
|
|
9
|
+
* - Linux: Generates C++ header with binary data
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFile, writeFile, readdir, mkdir, stat, copyFile } from 'fs/promises';
|
|
13
|
+
import { existsSync, statSync } from 'fs';
|
|
14
|
+
import { join, relative, basename, extname, dirname } from 'path';
|
|
15
|
+
import { createHash } from 'crypto';
|
|
16
|
+
|
|
17
|
+
class ResourceEmbedder {
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.verbose = options.verbose || false;
|
|
20
|
+
this.compression = options.compression !== false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
log(msg) {
|
|
24
|
+
if (this.verbose) console.log(msg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get all files recursively from a directory
|
|
29
|
+
*/
|
|
30
|
+
async getAllFiles(dirPath, arrayOfFiles = []) {
|
|
31
|
+
const files = await readdir(dirPath);
|
|
32
|
+
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const filePath = join(dirPath, file);
|
|
35
|
+
const s = await stat(filePath);
|
|
36
|
+
|
|
37
|
+
if (s.isDirectory()) {
|
|
38
|
+
arrayOfFiles = await this.getAllFiles(filePath, arrayOfFiles);
|
|
39
|
+
} else {
|
|
40
|
+
arrayOfFiles.push(filePath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return arrayOfFiles;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate Windows .rc resource file
|
|
49
|
+
*/
|
|
50
|
+
async generateWindowsRC(assetsDir, outputPath) {
|
|
51
|
+
console.log(' šŖ Generating Windows resource file (.rc)...');
|
|
52
|
+
|
|
53
|
+
if (!existsSync(assetsDir)) {
|
|
54
|
+
console.log(' ā ļø Assets directory not found, skipping');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const files = await this.getAllFiles(assetsDir);
|
|
59
|
+
let rcContent = `// Auto-generated by PlusUI - DO NOT EDIT
|
|
60
|
+
// Embedded resources for single-executable distribution
|
|
61
|
+
|
|
62
|
+
#include <windows.h>
|
|
63
|
+
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
let count = 0;
|
|
67
|
+
const resources = [];
|
|
68
|
+
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const relPath = relative(assetsDir, file).replace(/\\/g, '/');
|
|
71
|
+
const resourceId = `IDR_${relPath.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`;
|
|
72
|
+
const winPath = file.replace(/\//g, '\\\\');
|
|
73
|
+
|
|
74
|
+
// Store for manifest
|
|
75
|
+
resources.push({ id: resourceId, path: relPath, file });
|
|
76
|
+
|
|
77
|
+
rcContent += `${resourceId} RCDATA "${winPath}"\n`;
|
|
78
|
+
count++;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add icon if exists
|
|
82
|
+
const iconPaths = [
|
|
83
|
+
join(assetsDir, 'icons', 'windows', 'app.ico'),
|
|
84
|
+
join(assetsDir, 'icon.ico'),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
for (const iconPath of iconPaths) {
|
|
88
|
+
if (existsSync(iconPath)) {
|
|
89
|
+
rcContent += `\n// Application icon\nIDI_APP_ICON ICON "${iconPath.replace(/\//g, '\\\\')}"\n`;
|
|
90
|
+
console.log(` ā Added app icon: ${basename(iconPath)}`);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
96
|
+
await writeFile(outputPath, rcContent);
|
|
97
|
+
|
|
98
|
+
// Generate resource manifest (C++ header)
|
|
99
|
+
await this.generateResourceManifest(resources, outputPath.replace('.rc', '_manifest.hpp'));
|
|
100
|
+
|
|
101
|
+
console.log(` ā Created ${basename(outputPath)} (${count} resources)`);
|
|
102
|
+
return outputPath;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generate C++ header with resource manifest
|
|
107
|
+
*/
|
|
108
|
+
async generateResourceManifest(resources, outputPath) {
|
|
109
|
+
let content = `// Auto-generated by PlusUI - DO NOT EDIT
|
|
110
|
+
#pragma once
|
|
111
|
+
|
|
112
|
+
#include <string>
|
|
113
|
+
#include <unordered_map>
|
|
114
|
+
|
|
115
|
+
namespace plusui {
|
|
116
|
+
namespace resources {
|
|
117
|
+
|
|
118
|
+
// Resource ID to path mapping
|
|
119
|
+
static const std::unordered_map<std::string, int> RESOURCE_MAP = {
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
let idCounter = 100;
|
|
123
|
+
for (const res of resources) {
|
|
124
|
+
content += ` {"${res.path}", ${idCounter}},\n`;
|
|
125
|
+
idCounter++;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
content += `};
|
|
129
|
+
|
|
130
|
+
// Get resource data from embedded resources
|
|
131
|
+
#ifdef _WIN32
|
|
132
|
+
#include <windows.h>
|
|
133
|
+
inline std::string getResource(const std::string& path) {
|
|
134
|
+
auto it = RESOURCE_MAP.find(path);
|
|
135
|
+
if (it == RESOURCE_MAP.end()) return "";
|
|
136
|
+
|
|
137
|
+
HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(it->second), RT_RCDATA);
|
|
138
|
+
if (!hRes) return "";
|
|
139
|
+
|
|
140
|
+
HGLOBAL hData = LoadResource(NULL, hRes);
|
|
141
|
+
if (!hData) return "";
|
|
142
|
+
|
|
143
|
+
DWORD size = SizeofResource(NULL, hRes);
|
|
144
|
+
const char* data = static_cast<const char*>(LockResource(hData));
|
|
145
|
+
|
|
146
|
+
return std::string(data, size);
|
|
147
|
+
}
|
|
148
|
+
#else
|
|
149
|
+
inline std::string getResource(const std::string& path) {
|
|
150
|
+
// Use embedded data from binary (see embedded_resources.hpp)
|
|
151
|
+
extern const std::unordered_map<std::string, std::pair<const unsigned char*, size_t>>& EMBEDDED_DATA;
|
|
152
|
+
auto it = EMBEDDED_DATA.find(path);
|
|
153
|
+
if (it == EMBEDDED_DATA.end()) return "";
|
|
154
|
+
return std::string(reinterpret_cast<const char*>(it->second.first), it->second.second);
|
|
155
|
+
}
|
|
156
|
+
#endif
|
|
157
|
+
|
|
158
|
+
} // namespace resources
|
|
159
|
+
} // namespace plusui
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
await writeFile(outputPath, content);
|
|
163
|
+
console.log(` ā Created resource manifest`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Generate Linux/macOS binary embedding header
|
|
168
|
+
*/
|
|
169
|
+
async generateBinaryHeader(assetsDir, outputPath) {
|
|
170
|
+
console.log(' š§ Generating binary embedded resources...');
|
|
171
|
+
|
|
172
|
+
if (!existsSync(assetsDir)) {
|
|
173
|
+
console.log(' ā ļø Assets directory not found, skipping');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const files = await this.getAllFiles(assetsDir);
|
|
178
|
+
|
|
179
|
+
let content = `// Auto-generated by PlusUI - DO NOT EDIT
|
|
180
|
+
#pragma once
|
|
181
|
+
|
|
182
|
+
#include <unordered_map>
|
|
183
|
+
#include <utility>
|
|
184
|
+
#include <string>
|
|
185
|
+
|
|
186
|
+
namespace plusui {
|
|
187
|
+
namespace resources {
|
|
188
|
+
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
let count = 0;
|
|
192
|
+
const dataMap = [];
|
|
193
|
+
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
const relPath = relative(assetsDir, file).replace(/\\/g, '/');
|
|
196
|
+
const varName = `ASSET_${relPath.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}`;
|
|
197
|
+
const data = await readFile(file);
|
|
198
|
+
|
|
199
|
+
content += `// ${relPath}\nstatic const unsigned char ${varName}[] = {\n`;
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < data.length; i++) {
|
|
202
|
+
if (i % 16 === 0) content += ' ';
|
|
203
|
+
content += `0x${data[i].toString(16).padStart(2, '0')}`;
|
|
204
|
+
if (i < data.length - 1) content += ',';
|
|
205
|
+
if ((i + 1) % 16 === 0 || i === data.length - 1) content += '\n';
|
|
206
|
+
else content += ' ';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
content += `};\nstatic const size_t ${varName}_LEN = ${data.length};\n\n`;
|
|
210
|
+
|
|
211
|
+
dataMap.push({ path: relPath, varName, size: data.length });
|
|
212
|
+
count++;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Generate lookup map
|
|
216
|
+
content += `// Resource lookup map\nstatic const std::unordered_map<std::string, std::pair<const unsigned char*, size_t>> EMBEDDED_DATA = {\n`;
|
|
217
|
+
|
|
218
|
+
for (const item of dataMap) {
|
|
219
|
+
content += ` {"${item.path}", {${item.varName}, ${item.varName}_LEN}},\n`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
content += `};\n\n`;
|
|
223
|
+
|
|
224
|
+
// Add convenience function
|
|
225
|
+
content += `inline std::string getResource(const std::string& path) {
|
|
226
|
+
auto it = EMBEDDED_DATA.find(path);
|
|
227
|
+
if (it == EMBEDDED_DATA.end()) return "";
|
|
228
|
+
return std::string(reinterpret_cast<const char*>(it->second.first), it->second.second);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
} // namespace resources
|
|
232
|
+
} // namespace plusui
|
|
233
|
+
`;
|
|
234
|
+
|
|
235
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
236
|
+
await writeFile(outputPath, content);
|
|
237
|
+
|
|
238
|
+
console.log(` ā Created ${basename(outputPath)} (${count} resources)`);
|
|
239
|
+
return outputPath;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Generate macOS app bundle resources
|
|
244
|
+
*/
|
|
245
|
+
async generateMacOSBundle(assetsDir, bundlePath) {
|
|
246
|
+
console.log(' š Copying resources to macOS bundle...');
|
|
247
|
+
|
|
248
|
+
if (!existsSync(assetsDir)) {
|
|
249
|
+
console.log(' ā ļø Assets directory not found, skipping');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const resourcesDir = join(bundlePath, 'Contents', 'Resources');
|
|
254
|
+
await mkdir(resourcesDir, { recursive: true });
|
|
255
|
+
|
|
256
|
+
const files = await this.getAllFiles(assetsDir);
|
|
257
|
+
let count = 0;
|
|
258
|
+
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
const relPath = relative(assetsDir, file);
|
|
261
|
+
const dest = join(resourcesDir, relPath);
|
|
262
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
263
|
+
await copyFile(file, dest);
|
|
264
|
+
count++;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Copy icon if exists
|
|
268
|
+
const iconPath = join(assetsDir, 'icons', 'macos', 'app.icns');
|
|
269
|
+
if (existsSync(iconPath)) {
|
|
270
|
+
await copyFile(iconPath, join(resourcesDir, 'app.icns'));
|
|
271
|
+
console.log(` ā Added app icon`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log(` ā Copied ${count} resources to bundle`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Main embed function - chooses platform
|
|
279
|
+
*/
|
|
280
|
+
async embed(assetsDir, outputDir, platform = process.platform) {
|
|
281
|
+
console.log(`\nš¦ Embedding resources for ${platform}...\n`);
|
|
282
|
+
|
|
283
|
+
await mkdir(outputDir, { recursive: true });
|
|
284
|
+
|
|
285
|
+
switch (platform) {
|
|
286
|
+
case 'win32':
|
|
287
|
+
return await this.generateWindowsRC(
|
|
288
|
+
assetsDir,
|
|
289
|
+
join(outputDir, 'resources.rc')
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
case 'darwin':
|
|
293
|
+
// macOS uses both binary embedding and bundle resources
|
|
294
|
+
await this.generateBinaryHeader(
|
|
295
|
+
assetsDir,
|
|
296
|
+
join(outputDir, 'embedded_resources.hpp')
|
|
297
|
+
);
|
|
298
|
+
// Bundle copying happens at app bundle creation time
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case 'linux':
|
|
302
|
+
return await this.generateBinaryHeader(
|
|
303
|
+
assetsDir,
|
|
304
|
+
join(outputDir, 'embedded_resources.hpp')
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
default:
|
|
308
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log('\n⨠Resource embedding complete!\n');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Embed for all platforms
|
|
316
|
+
*/
|
|
317
|
+
async embedAll(assetsDir, outputDir) {
|
|
318
|
+
console.log('\nš¦ Embedding resources for all platforms...\n');
|
|
319
|
+
|
|
320
|
+
await this.embed(assetsDir, join(outputDir, 'windows'), 'win32');
|
|
321
|
+
await this.embed(assetsDir, join(outputDir, 'linux'), 'linux');
|
|
322
|
+
await this.embed(assetsDir, join(outputDir, 'darwin'), 'darwin');
|
|
323
|
+
|
|
324
|
+
console.log('\n⨠All platforms complete!\n');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// CLI
|
|
329
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
330
|
+
const args = process.argv.slice(2);
|
|
331
|
+
const command = args[0];
|
|
332
|
+
const assetsDir = args[1] || join(process.cwd(), 'frontend', 'dist');
|
|
333
|
+
const outputDir = args[2] || join(process.cwd(), 'generated', 'resources');
|
|
334
|
+
|
|
335
|
+
const embedder = new ResourceEmbedder({ verbose: true });
|
|
336
|
+
|
|
337
|
+
if (command === 'all') {
|
|
338
|
+
embedder.embedAll(assetsDir, outputDir).catch(err => {
|
|
339
|
+
console.error('ā Error:', err.message);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
});
|
|
342
|
+
} else {
|
|
343
|
+
const platform = command || process.platform;
|
|
344
|
+
embedder.embed(assetsDir, outputDir, platform).catch(err => {
|
|
345
|
+
console.error('ā Error:', err.message);
|
|
346
|
+
process.exit(1);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export { ResourceEmbedder };
|