imxc 0.5.4 → 0.6.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.
- package/dist/compile.d.ts +8 -0
- package/dist/compile.js +285 -25
- package/dist/components.js +597 -45
- package/dist/diagnostics.js +3 -2
- package/dist/emitter.d.ts +6 -3
- package/dist/emitter.js +1454 -322
- package/dist/index.js +34 -7
- package/dist/init.d.ts +7 -1
- package/dist/init.js +43 -455
- package/dist/ir.d.ts +437 -5
- package/dist/lowering.d.ts +4 -3
- package/dist/lowering.js +770 -57
- package/dist/parser.d.ts +1 -0
- package/dist/templates/async.d.ts +1 -0
- package/dist/templates/async.js +228 -0
- package/dist/templates/custom.d.ts +15 -0
- package/dist/templates/custom.js +945 -0
- package/dist/templates/filedialog.d.ts +1 -0
- package/dist/templates/filedialog.js +216 -0
- package/dist/templates/hotreload.d.ts +1 -0
- package/dist/templates/hotreload.js +400 -0
- package/dist/templates/index.d.ts +16 -0
- package/dist/templates/index.js +553 -0
- package/dist/templates/minimal.d.ts +1 -0
- package/dist/templates/minimal.js +165 -0
- package/dist/templates/networking.d.ts +1 -0
- package/dist/templates/networking.js +244 -0
- package/dist/templates/persistence.d.ts +1 -0
- package/dist/templates/persistence.js +238 -0
- package/dist/validator.d.ts +1 -0
- package/dist/validator.js +51 -22
- package/dist/watch.d.ts +2 -1
- package/dist/watch.js +21 -4
- package/package.json +2 -4
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { buildImxDts, TSCONFIG, GITIGNORE, cmakeTemplate } from './index.js';
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Shared helper content (async.h used by both async and networking)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const ASYNC_H = `#pragma once
|
|
8
|
+
#include <thread>
|
|
9
|
+
#include <functional>
|
|
10
|
+
#include <imx/runtime.h>
|
|
11
|
+
|
|
12
|
+
namespace imx {
|
|
13
|
+
|
|
14
|
+
// Runs \`work\` on a background thread, then calls \`on_done\` with the result.
|
|
15
|
+
// Calls request_frame() so the UI wakes up to display the result.
|
|
16
|
+
// Replace with a thread pool if you need to limit concurrency.
|
|
17
|
+
template<typename T>
|
|
18
|
+
void run_async(Runtime& runtime, std::function<T()> work, std::function<void(T)> on_done) {
|
|
19
|
+
std::thread([&runtime, work = std::move(work), on_done = std::move(on_done)]() {
|
|
20
|
+
T result = work();
|
|
21
|
+
on_done(std::move(result));
|
|
22
|
+
runtime.request_frame();
|
|
23
|
+
}).detach();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
} // namespace imx
|
|
27
|
+
`;
|
|
28
|
+
const PERSISTENCE_H = `#pragma once
|
|
29
|
+
#include <fstream>
|
|
30
|
+
#include <string>
|
|
31
|
+
#include <imx/json.hpp>
|
|
32
|
+
|
|
33
|
+
namespace imx {
|
|
34
|
+
|
|
35
|
+
// Save state as formatted JSON. Returns true on success.
|
|
36
|
+
// Saves next to the executable by default — change path for platform app data dirs.
|
|
37
|
+
template<typename T>
|
|
38
|
+
bool save_json(const std::string& path, const T& state) {
|
|
39
|
+
std::ofstream f(path);
|
|
40
|
+
if (!f) return false;
|
|
41
|
+
f << nlohmann::json(state).dump(2);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Load state from JSON file. Returns true on success, leaves state unchanged on failure.
|
|
46
|
+
template<typename T>
|
|
47
|
+
bool load_json(const std::string& path, T& state) {
|
|
48
|
+
std::ifstream f(path);
|
|
49
|
+
if (!f) return false;
|
|
50
|
+
nlohmann::json j = nlohmann::json::parse(f, nullptr, false);
|
|
51
|
+
if (j.is_discarded()) return false;
|
|
52
|
+
nlohmann::from_json(j, state);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
} // namespace imx
|
|
57
|
+
`;
|
|
58
|
+
const HOTRELOAD_H = `#pragma once
|
|
59
|
+
#include <string>
|
|
60
|
+
#include <filesystem>
|
|
61
|
+
#include <iostream>
|
|
62
|
+
#include <imx/runtime.h>
|
|
63
|
+
#include <imgui.h>
|
|
64
|
+
#include "AppState.h"
|
|
65
|
+
|
|
66
|
+
#ifdef _WIN32
|
|
67
|
+
#ifndef WIN32_LEAN_AND_MEAN
|
|
68
|
+
#define WIN32_LEAN_AND_MEAN
|
|
69
|
+
#endif
|
|
70
|
+
#include <windows.h>
|
|
71
|
+
#else
|
|
72
|
+
#include <dlfcn.h>
|
|
73
|
+
#include <unistd.h>
|
|
74
|
+
#endif
|
|
75
|
+
|
|
76
|
+
struct HotModule {
|
|
77
|
+
using RenderFn = void(*)(imx::Runtime&, AppState&, ImGuiContext*);
|
|
78
|
+
|
|
79
|
+
#ifdef _WIN32
|
|
80
|
+
HMODULE handle = nullptr;
|
|
81
|
+
#else
|
|
82
|
+
void* handle = nullptr;
|
|
83
|
+
#endif
|
|
84
|
+
RenderFn render = nullptr;
|
|
85
|
+
std::filesystem::file_time_type last_write{};
|
|
86
|
+
std::string path;
|
|
87
|
+
|
|
88
|
+
bool load(const std::string& lib_path) {
|
|
89
|
+
path = lib_path;
|
|
90
|
+
if (!std::filesystem::exists(path)) {
|
|
91
|
+
std::cerr << "hotreload: " << path << " not found\\n";
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
last_write = std::filesystem::last_write_time(path);
|
|
95
|
+
|
|
96
|
+
#ifdef _WIN32
|
|
97
|
+
// Copy DLL to avoid locking the original (allows rebuild while loaded)
|
|
98
|
+
std::string copy_path = path + ".live";
|
|
99
|
+
std::filesystem::copy_file(path, copy_path, std::filesystem::copy_options::overwrite_existing);
|
|
100
|
+
handle = LoadLibraryA(copy_path.c_str());
|
|
101
|
+
if (!handle) {
|
|
102
|
+
std::cerr << "hotreload: LoadLibrary failed\\n";
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
render = reinterpret_cast<RenderFn>(GetProcAddress(handle, "imx_render"));
|
|
106
|
+
#else
|
|
107
|
+
handle = dlopen(path.c_str(), RTLD_NOW);
|
|
108
|
+
if (!handle) {
|
|
109
|
+
std::cerr << "hotreload: dlopen failed: " << dlerror() << "\\n";
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
render = reinterpret_cast<RenderFn>(dlsym(handle, "imx_render"));
|
|
113
|
+
#endif
|
|
114
|
+
if (!render) {
|
|
115
|
+
std::cerr << "hotreload: imx_render symbol not found\\n";
|
|
116
|
+
unload();
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
std::cout << "hotreload: loaded " << path << "\\n";
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
void unload() {
|
|
124
|
+
if (!handle) return;
|
|
125
|
+
#ifdef _WIN32
|
|
126
|
+
FreeLibrary(handle);
|
|
127
|
+
#else
|
|
128
|
+
dlclose(handle);
|
|
129
|
+
#endif
|
|
130
|
+
handle = nullptr;
|
|
131
|
+
render = nullptr;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
bool check_reload() {
|
|
135
|
+
if (path.empty() || !std::filesystem::exists(path)) return false;
|
|
136
|
+
auto current = std::filesystem::last_write_time(path);
|
|
137
|
+
if (current == last_write) return false;
|
|
138
|
+
std::cout << "hotreload: change detected, reloading...\\n";
|
|
139
|
+
unload();
|
|
140
|
+
// Small delay to ensure file write is complete
|
|
141
|
+
#ifdef _WIN32
|
|
142
|
+
Sleep(100);
|
|
143
|
+
#else
|
|
144
|
+
usleep(100000);
|
|
145
|
+
#endif
|
|
146
|
+
if (load(path)) {
|
|
147
|
+
std::cout << "hotreload: reload successful\\n";
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
std::cerr << "hotreload: reload failed\\n";
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
~HotModule() { unload(); }
|
|
155
|
+
};
|
|
156
|
+
`;
|
|
157
|
+
const UI_ENTRY_CPP = `#include <imx/runtime.h>
|
|
158
|
+
#include <imgui.h>
|
|
159
|
+
#include "AppState.h"
|
|
160
|
+
|
|
161
|
+
// Declared in app_root.gen.cpp (generated by imxc)
|
|
162
|
+
namespace imx {
|
|
163
|
+
template <> void render_root<AppState>(imx::Runtime& runtime, AppState& state);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#ifdef _WIN32
|
|
167
|
+
#define EXPORT __declspec(dllexport)
|
|
168
|
+
#else
|
|
169
|
+
#define EXPORT __attribute__((visibility("default")))
|
|
170
|
+
#endif
|
|
171
|
+
|
|
172
|
+
// ImGuiContext* must be passed from host — DLL has its own static ImGui globals
|
|
173
|
+
extern "C" EXPORT void imx_render(imx::Runtime& runtime, AppState& state, ImGuiContext* ctx) {
|
|
174
|
+
ImGui::SetCurrentContext(ctx);
|
|
175
|
+
imx::render_root(runtime, state);
|
|
176
|
+
}
|
|
177
|
+
`;
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Feature definitions (prefixed names to avoid collisions when combined)
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
export const FEATURES = [
|
|
182
|
+
{
|
|
183
|
+
name: 'async',
|
|
184
|
+
description: 'Background tasks with std::thread',
|
|
185
|
+
includes: ['#include <thread>', '#include <chrono>'],
|
|
186
|
+
appStateCppHeaders: ['#include <string>'],
|
|
187
|
+
appStateCppFields: [
|
|
188
|
+
' bool loading = false;',
|
|
189
|
+
' std::string result = "";',
|
|
190
|
+
' std::function<void()> onFetchAsync;',
|
|
191
|
+
].join('\n'),
|
|
192
|
+
appStateTsFields: [
|
|
193
|
+
' loading: boolean;',
|
|
194
|
+
' result: string;',
|
|
195
|
+
' onFetchAsync: () => void;',
|
|
196
|
+
].join('\n'),
|
|
197
|
+
callbacks: ` app.state.onFetchAsync = [&]() {
|
|
198
|
+
app.state.loading = true;
|
|
199
|
+
app.state.result = "";
|
|
200
|
+
imx::run_async<std::string>(
|
|
201
|
+
app.runtime,
|
|
202
|
+
[]() {
|
|
203
|
+
// Simulate work — replace with real computation
|
|
204
|
+
std::this_thread::sleep_for(std::chrono::seconds(2));
|
|
205
|
+
return std::string("Data loaded successfully!");
|
|
206
|
+
},
|
|
207
|
+
[&](std::string res) {
|
|
208
|
+
app.state.result = std::move(res);
|
|
209
|
+
app.state.loading = false;
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
};`,
|
|
213
|
+
tsxWindow: ` <Window title="Async Demo">
|
|
214
|
+
<Column gap={8}>
|
|
215
|
+
<Text>Background Task Example</Text>
|
|
216
|
+
<Separator />
|
|
217
|
+
<Button
|
|
218
|
+
title="Fetch Data"
|
|
219
|
+
onPress={props.onFetchAsync}
|
|
220
|
+
disabled={props.loading}
|
|
221
|
+
/>
|
|
222
|
+
{props.loading && <Text color={[1, 0.8, 0, 1]}>Loading...</Text>}
|
|
223
|
+
{props.result !== "" && <Text color={[0, 1, 0, 1]}>Result: {props.result}</Text>}
|
|
224
|
+
</Column>
|
|
225
|
+
</Window>`,
|
|
226
|
+
extraFiles: { 'async.h': ASYNC_H },
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'persistence',
|
|
230
|
+
description: 'JSON save/load with nlohmann/json',
|
|
231
|
+
includes: [],
|
|
232
|
+
appStateCppHeaders: ['#include <string>', '#include <imx/json.hpp>'],
|
|
233
|
+
appStateCppFields: [
|
|
234
|
+
' std::string name = "World";',
|
|
235
|
+
' float volume = 50.0F;',
|
|
236
|
+
' bool darkMode = true;',
|
|
237
|
+
' std::function<void()> onSaveState;',
|
|
238
|
+
' std::function<void()> onLoadState;',
|
|
239
|
+
].join('\n'),
|
|
240
|
+
appStateTsFields: [
|
|
241
|
+
' name: string;',
|
|
242
|
+
' volume: number;',
|
|
243
|
+
' darkMode: boolean;',
|
|
244
|
+
' onSaveState: () => void;',
|
|
245
|
+
' onLoadState: () => void;',
|
|
246
|
+
].join('\n'),
|
|
247
|
+
callbacks: ` app.state.onSaveState = [&]() {
|
|
248
|
+
imx::save_json("state.json", app.state);
|
|
249
|
+
};
|
|
250
|
+
app.state.onLoadState = [&]() {
|
|
251
|
+
imx::load_json("state.json", app.state);
|
|
252
|
+
app.runtime.request_frame();
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Auto-load saved state (silently fails if no file exists)
|
|
256
|
+
imx::load_json("state.json", app.state);`,
|
|
257
|
+
tsxWindow: ` <Window title="Persistence Demo">
|
|
258
|
+
<Column gap={8}>
|
|
259
|
+
<Text>JSON Save/Load Example</Text>
|
|
260
|
+
<Separator />
|
|
261
|
+
<TextInput label="Name" value={props.name} />
|
|
262
|
+
<SliderFloat label="Volume" value={props.volume} min={0} max={100} />
|
|
263
|
+
<Checkbox label="Dark Mode" value={props.darkMode} />
|
|
264
|
+
<Separator />
|
|
265
|
+
<Row gap={8}>
|
|
266
|
+
<Button title="Save" onPress={props.onSaveState} />
|
|
267
|
+
<Button title="Load" onPress={props.onLoadState} />
|
|
268
|
+
</Row>
|
|
269
|
+
</Column>
|
|
270
|
+
</Window>`,
|
|
271
|
+
extraFiles: { 'persistence.h': PERSISTENCE_H },
|
|
272
|
+
dataFields: ['name', 'volume', 'darkMode'],
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'networking',
|
|
276
|
+
description: 'HTTP client with cpp-httplib',
|
|
277
|
+
requires: ['async'],
|
|
278
|
+
includes: ['#include <thread>', '#include <imx/httplib.h>'],
|
|
279
|
+
appStateCppHeaders: ['#include <string>'],
|
|
280
|
+
appStateCppFields: [
|
|
281
|
+
' std::string url = "http://jsonplaceholder.typicode.com/todos/1";',
|
|
282
|
+
' std::string response = "";',
|
|
283
|
+
' bool loading = false;',
|
|
284
|
+
' std::function<void()> onFetchNet;',
|
|
285
|
+
].join('\n'),
|
|
286
|
+
appStateTsFields: [
|
|
287
|
+
' url: string;',
|
|
288
|
+
' response: string;',
|
|
289
|
+
' loading: boolean;',
|
|
290
|
+
' onFetchNet: () => void;',
|
|
291
|
+
].join('\n'),
|
|
292
|
+
callbacks: ` // Wire up the HTTP fetch callback
|
|
293
|
+
app.state.onFetchNet = [&]() {
|
|
294
|
+
app.state.loading = true;
|
|
295
|
+
app.state.response = "";
|
|
296
|
+
std::string url = app.state.url;
|
|
297
|
+
imx::run_async<std::string>(
|
|
298
|
+
app.runtime,
|
|
299
|
+
[url]() -> std::string {
|
|
300
|
+
// Parse http://host/path
|
|
301
|
+
auto scheme_end = url.find("://");
|
|
302
|
+
if (scheme_end == std::string::npos)
|
|
303
|
+
return "Error: invalid URL (must start with http://)";
|
|
304
|
+
auto host_start = scheme_end + 3;
|
|
305
|
+
auto path_start = url.find('/', host_start);
|
|
306
|
+
std::string host = (path_start != std::string::npos)
|
|
307
|
+
? url.substr(0, path_start) : url;
|
|
308
|
+
std::string path = (path_start != std::string::npos)
|
|
309
|
+
? url.substr(path_start) : "/";
|
|
310
|
+
|
|
311
|
+
// For HTTPS: #define CPPHTTPLIB_OPENSSL_SUPPORT and link OpenSSL
|
|
312
|
+
httplib::Client cli(host);
|
|
313
|
+
cli.set_connection_timeout(5);
|
|
314
|
+
cli.set_read_timeout(5);
|
|
315
|
+
auto res = cli.Get(path);
|
|
316
|
+
if (res) return res->body;
|
|
317
|
+
return "Error: request failed";
|
|
318
|
+
},
|
|
319
|
+
[&](std::string body) {
|
|
320
|
+
app.state.response = std::move(body);
|
|
321
|
+
app.state.loading = false;
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
};`,
|
|
325
|
+
tsxWindow: ` <Window title="Networking Demo">
|
|
326
|
+
<Column gap={8}>
|
|
327
|
+
<Text>HTTP Client Example</Text>
|
|
328
|
+
<Separator />
|
|
329
|
+
<TextInput label="URL" value={props.url} />
|
|
330
|
+
<Button title="Fetch" onPress={props.onFetchNet} disabled={props.loading} />
|
|
331
|
+
{props.loading && <Text color={[1, 0.8, 0, 1]}>Loading...</Text>}
|
|
332
|
+
{props.response !== "" && <Text wrapped={true}>{props.response}</Text>}
|
|
333
|
+
</Column>
|
|
334
|
+
</Window>`,
|
|
335
|
+
extraFiles: { 'async.h': ASYNC_H },
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: 'filedialog',
|
|
339
|
+
description: 'Native file dialogs + drag & drop',
|
|
340
|
+
includes: ['#include <imx/pfd.h>'],
|
|
341
|
+
appStateCppHeaders: ['#include <string>'],
|
|
342
|
+
appStateCppFields: [
|
|
343
|
+
' std::string filePath = "";',
|
|
344
|
+
' std::string message = "";',
|
|
345
|
+
' std::function<void()> onOpenDialog;',
|
|
346
|
+
' std::function<void()> onSaveDialog;',
|
|
347
|
+
].join('\n'),
|
|
348
|
+
appStateTsFields: [
|
|
349
|
+
' filePath: string;',
|
|
350
|
+
' message: string;',
|
|
351
|
+
' onOpenDialog: () => void;',
|
|
352
|
+
' onSaveDialog: () => void;',
|
|
353
|
+
].join('\n'),
|
|
354
|
+
callbacks: ` app.state.onOpenDialog = [&]() {
|
|
355
|
+
auto result = pfd::open_file("Open File").result();
|
|
356
|
+
if (!result.empty()) {
|
|
357
|
+
app.state.filePath = result[0];
|
|
358
|
+
app.state.message = "Opened: " + result[0];
|
|
359
|
+
app.runtime.request_frame();
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
app.state.onSaveDialog = [&]() {
|
|
364
|
+
auto result = pfd::save_file("Save File").result();
|
|
365
|
+
if (!result.empty()) {
|
|
366
|
+
app.state.filePath = result;
|
|
367
|
+
app.state.message = "Save to: " + result;
|
|
368
|
+
app.runtime.request_frame();
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// GLFW file drop callback
|
|
373
|
+
glfwSetDropCallback(window, [](GLFWwindow* w, int count, const char** paths) {
|
|
374
|
+
auto* a = static_cast<App*>(glfwGetWindowUserPointer(w));
|
|
375
|
+
if (a && count > 0) {
|
|
376
|
+
a->state.filePath = paths[0];
|
|
377
|
+
a->state.message = "Dropped: " + std::string(paths[0]);
|
|
378
|
+
a->runtime.request_frame();
|
|
379
|
+
}
|
|
380
|
+
});`,
|
|
381
|
+
tsxWindow: ` <Window title="File Dialog Demo">
|
|
382
|
+
<Column gap={8}>
|
|
383
|
+
<Text>Native File Dialogs + Drag & Drop</Text>
|
|
384
|
+
<Separator />
|
|
385
|
+
<Row gap={8}>
|
|
386
|
+
<Button title="Open File" onPress={props.onOpenDialog} />
|
|
387
|
+
<Button title="Save File" onPress={props.onSaveDialog} />
|
|
388
|
+
</Row>
|
|
389
|
+
<Text>Drag & drop a file onto this window</Text>
|
|
390
|
+
{props.message !== "" && <Text color={[0, 1, 0, 1]}>{props.message}</Text>}
|
|
391
|
+
{props.filePath !== "" && <Text>Path: {props.filePath}</Text>}
|
|
392
|
+
</Column>
|
|
393
|
+
</Window>`,
|
|
394
|
+
extraFiles: {},
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: 'hotreload',
|
|
398
|
+
description: 'DLL hot reload for live UI iteration',
|
|
399
|
+
includes: [],
|
|
400
|
+
appStateCppHeaders: ['#include <string>'],
|
|
401
|
+
appStateCppFields: ' std::string watchCmd;\n std::function<void()> onCopyCmd;',
|
|
402
|
+
appStateTsFields: ' watchCmd: string;\n onCopyCmd: () => void;',
|
|
403
|
+
callbacks: ` app.state.watchCmd = "npx imxc watch src -o build/generated --build \\"cmake --build build --target imx_ui\\"";
|
|
404
|
+
app.state.onCopyCmd = [&]() { imx::clipboard_set(app.state.watchCmd.c_str()); };`,
|
|
405
|
+
tsxWindow: ` <Window title="Hot Reload">
|
|
406
|
+
<Column gap={8}>
|
|
407
|
+
<Text>Run this in a second terminal to enable live reload:</Text>
|
|
408
|
+
<Text wrapped={true}>{props.watchCmd}</Text>
|
|
409
|
+
<Button title="Copy" onPress={props.onCopyCmd} />
|
|
410
|
+
</Column>
|
|
411
|
+
</Window>`,
|
|
412
|
+
extraFiles: {},
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// CMake for hot reload (host exe + UI shared lib)
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
function cmakeHotreloadCombined(projectName) {
|
|
419
|
+
return `cmake_minimum_required(VERSION 3.25)
|
|
420
|
+
project(${projectName} LANGUAGES CXX)
|
|
421
|
+
|
|
422
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
423
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
424
|
+
|
|
425
|
+
include(FetchContent)
|
|
426
|
+
set(FETCHCONTENT_QUIET OFF)
|
|
427
|
+
|
|
428
|
+
FetchContent_Declare(
|
|
429
|
+
imx
|
|
430
|
+
GIT_REPOSITORY https://github.com/bgocumlu/imx.git
|
|
431
|
+
GIT_TAG v0.6.0
|
|
432
|
+
GIT_SHALLOW TRUE
|
|
433
|
+
GIT_PROGRESS TRUE
|
|
434
|
+
)
|
|
435
|
+
message(STATUS "Fetching IMX (includes ImGui + GLFW)...")
|
|
436
|
+
FetchContent_MakeAvailable(imx)
|
|
437
|
+
|
|
438
|
+
include(ImxCompile)
|
|
439
|
+
|
|
440
|
+
imx_compile_tsx(GENERATED
|
|
441
|
+
SOURCES src/App.tsx
|
|
442
|
+
OUTPUT_DIR \${CMAKE_BINARY_DIR}/generated
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# UI shared library (hot-reloadable)
|
|
446
|
+
add_library(imx_ui SHARED
|
|
447
|
+
src/ui_entry.cpp
|
|
448
|
+
\${GENERATED}
|
|
449
|
+
)
|
|
450
|
+
target_link_libraries(imx_ui PRIVATE imx::renderer)
|
|
451
|
+
target_include_directories(imx_ui PRIVATE \${CMAKE_BINARY_DIR}/generated \${CMAKE_CURRENT_SOURCE_DIR}/src)
|
|
452
|
+
|
|
453
|
+
# Host executable (loads UI module dynamically)
|
|
454
|
+
add_executable(${projectName}
|
|
455
|
+
src/main.cpp
|
|
456
|
+
)
|
|
457
|
+
set_target_properties(${projectName} PROPERTIES WIN32_EXECUTABLE $<CONFIG:Release>)
|
|
458
|
+
target_link_libraries(${projectName} PRIVATE imx::renderer \${CMAKE_DL_LIBS})
|
|
459
|
+
target_include_directories(${projectName} PRIVATE \${CMAKE_CURRENT_SOURCE_DIR}/src)
|
|
460
|
+
|
|
461
|
+
# Copy DLL/SO next to host exe after build
|
|
462
|
+
add_custom_command(TARGET imx_ui POST_BUILD
|
|
463
|
+
COMMAND \${CMAKE_COMMAND} -E copy $<TARGET_FILE:imx_ui> $<TARGET_FILE_DIR:${projectName}>
|
|
464
|
+
COMMENT "Copying UI module to host directory"
|
|
465
|
+
)
|
|
466
|
+
add_dependencies(${projectName} imx_ui)
|
|
467
|
+
|
|
468
|
+
# Copy public/ assets to output directory
|
|
469
|
+
add_custom_command(TARGET ${projectName} POST_BUILD
|
|
470
|
+
COMMAND \${CMAKE_COMMAND} -E copy_directory
|
|
471
|
+
\${CMAKE_CURRENT_SOURCE_DIR}/public
|
|
472
|
+
$<TARGET_FILE_DIR:${projectName}>
|
|
473
|
+
COMMENT "Copying public/ assets"
|
|
474
|
+
)
|
|
475
|
+
`;
|
|
476
|
+
}
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
// Base main.cpp template with placeholders
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
function buildMainCpp(projectName, extraIncludes, callbacks, hasPersistence) {
|
|
481
|
+
const includeBlock = extraIncludes ? '\n' + extraIncludes : '';
|
|
482
|
+
const persistenceInclude = hasPersistence ? '\n#include "persistence.h"' : '';
|
|
483
|
+
return `#include <imx/runtime.h>
|
|
484
|
+
#include <imx/renderer.h>
|
|
485
|
+
|
|
486
|
+
#include <imgui.h>
|
|
487
|
+
#include <imgui_impl_glfw.h>
|
|
488
|
+
#include <imgui_impl_opengl3.h>
|
|
489
|
+
#include <GLFW/glfw3.h>
|
|
490
|
+
${includeBlock}${persistenceInclude}
|
|
491
|
+
#include "AppState.h"
|
|
492
|
+
|
|
493
|
+
struct App {
|
|
494
|
+
GLFWwindow* window = nullptr;
|
|
495
|
+
ImGuiIO* io = nullptr;
|
|
496
|
+
imx::Runtime runtime;
|
|
497
|
+
AppState state;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
static void render_frame(App& app) {
|
|
501
|
+
glfwMakeContextCurrent(app.window);
|
|
502
|
+
if (glfwGetWindowAttrib(app.window, GLFW_ICONIFIED) != 0) return;
|
|
503
|
+
|
|
504
|
+
int fb_w = 0, fb_h = 0;
|
|
505
|
+
glfwGetFramebufferSize(app.window, &fb_w, &fb_h);
|
|
506
|
+
if (fb_w <= 0 || fb_h <= 0) return;
|
|
507
|
+
|
|
508
|
+
ImGui_ImplOpenGL3_NewFrame();
|
|
509
|
+
ImGui_ImplGlfw_NewFrame();
|
|
510
|
+
ImGui::NewFrame();
|
|
511
|
+
|
|
512
|
+
imx::render_root(app.runtime, app.state);
|
|
513
|
+
|
|
514
|
+
ImGui::Render();
|
|
515
|
+
glViewport(0, 0, fb_w, fb_h);
|
|
516
|
+
glClearColor(0.12F, 0.12F, 0.15F, 1.0F);
|
|
517
|
+
glClear(GL_COLOR_BUFFER_BIT);
|
|
518
|
+
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
|
519
|
+
|
|
520
|
+
if ((app.io->ConfigFlags & ImGuiConfigFlags_ViewportsEnable) != 0) {
|
|
521
|
+
ImGui::UpdatePlatformWindows();
|
|
522
|
+
ImGui::RenderPlatformWindowsDefault();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
glfwMakeContextCurrent(app.window);
|
|
526
|
+
glfwSwapBuffers(app.window);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
static void window_size_callback(GLFWwindow* window, int, int) {
|
|
530
|
+
auto* app = static_cast<App*>(glfwGetWindowUserPointer(window));
|
|
531
|
+
if (app) render_frame(*app);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
int main() {
|
|
535
|
+
if (glfwInit() == 0) return 1;
|
|
536
|
+
|
|
537
|
+
const char* glsl_version = "#version 150";
|
|
538
|
+
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
|
539
|
+
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
|
|
540
|
+
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
|
541
|
+
#ifdef __APPLE__
|
|
542
|
+
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
|
|
543
|
+
#endif
|
|
544
|
+
|
|
545
|
+
GLFWwindow* window = glfwCreateWindow(800, 600, "${projectName}", nullptr, nullptr);
|
|
546
|
+
if (!window) { glfwTerminate(); return 1; }
|
|
547
|
+
glfwMakeContextCurrent(window);
|
|
548
|
+
glfwSwapInterval(1);
|
|
549
|
+
|
|
550
|
+
IMGUI_CHECKVERSION();
|
|
551
|
+
ImGui::CreateContext();
|
|
552
|
+
ImGuiIO& io = ImGui::GetIO();
|
|
553
|
+
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
|
554
|
+
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
|
|
555
|
+
|
|
556
|
+
ImGui::StyleColorsDark();
|
|
557
|
+
ImGuiStyle& style = ImGui::GetStyle();
|
|
558
|
+
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
|
559
|
+
style.WindowRounding = 0.0F;
|
|
560
|
+
style.Colors[ImGuiCol_WindowBg].w = 1.0F;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
ImGui_ImplGlfw_InitForOpenGL(window, true);
|
|
564
|
+
ImGui_ImplOpenGL3_Init(glsl_version);
|
|
565
|
+
|
|
566
|
+
App app;
|
|
567
|
+
app.window = window;
|
|
568
|
+
app.io = &io;
|
|
569
|
+
glfwSetWindowUserPointer(window, &app);
|
|
570
|
+
glfwSetWindowSizeCallback(window, window_size_callback);
|
|
571
|
+
|
|
572
|
+
${callbacks}
|
|
573
|
+
|
|
574
|
+
while (glfwWindowShouldClose(window) == 0) {
|
|
575
|
+
if (app.runtime.needs_frame()) {
|
|
576
|
+
glfwPollEvents();
|
|
577
|
+
} else {
|
|
578
|
+
glfwWaitEventsTimeout(0.1);
|
|
579
|
+
}
|
|
580
|
+
render_frame(app);
|
|
581
|
+
app.runtime.frame_rendered(ImGui::IsAnyItemActive());
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
ImGui_ImplOpenGL3_Shutdown();
|
|
585
|
+
ImGui_ImplGlfw_Shutdown();
|
|
586
|
+
ImGui::DestroyContext();
|
|
587
|
+
glfwDestroyWindow(window);
|
|
588
|
+
glfwTerminate();
|
|
589
|
+
return 0;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
#ifdef _WIN32
|
|
593
|
+
#include <windows.h>
|
|
594
|
+
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { return main(); }
|
|
595
|
+
#endif
|
|
596
|
+
`;
|
|
597
|
+
}
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
// Hot reload main.cpp template (DLL-loading variant)
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
function buildMainCppHotreload(projectName, extraIncludes, callbacks, hasPersistence) {
|
|
602
|
+
const includeBlock = extraIncludes ? '\n' + extraIncludes : '';
|
|
603
|
+
const persistenceInclude = hasPersistence ? '\n#include "persistence.h"' : '';
|
|
604
|
+
return `#include <imx/runtime.h>
|
|
605
|
+
#include <imx/renderer.h>
|
|
606
|
+
|
|
607
|
+
#include <imgui.h>
|
|
608
|
+
#include <imgui_impl_glfw.h>
|
|
609
|
+
#include <imgui_impl_opengl3.h>
|
|
610
|
+
#include <GLFW/glfw3.h>
|
|
611
|
+
${includeBlock}${persistenceInclude}
|
|
612
|
+
#include "hotreload.h"
|
|
613
|
+
#include "AppState.h"
|
|
614
|
+
|
|
615
|
+
struct App {
|
|
616
|
+
GLFWwindow* window = nullptr;
|
|
617
|
+
ImGuiIO* io = nullptr;
|
|
618
|
+
imx::Runtime runtime;
|
|
619
|
+
AppState state;
|
|
620
|
+
HotModule module;
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
static void render_frame(App& app) {
|
|
624
|
+
glfwMakeContextCurrent(app.window);
|
|
625
|
+
if (glfwGetWindowAttrib(app.window, GLFW_ICONIFIED) != 0) return;
|
|
626
|
+
|
|
627
|
+
int fb_w = 0, fb_h = 0;
|
|
628
|
+
glfwGetFramebufferSize(app.window, &fb_w, &fb_h);
|
|
629
|
+
if (fb_w <= 0 || fb_h <= 0) return;
|
|
630
|
+
|
|
631
|
+
ImGui_ImplOpenGL3_NewFrame();
|
|
632
|
+
ImGui_ImplGlfw_NewFrame();
|
|
633
|
+
ImGui::NewFrame();
|
|
634
|
+
|
|
635
|
+
app.module.check_reload();
|
|
636
|
+
if (app.module.render) {
|
|
637
|
+
app.module.render(app.runtime, app.state, ImGui::GetCurrentContext());
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
ImGui::Render();
|
|
641
|
+
glViewport(0, 0, fb_w, fb_h);
|
|
642
|
+
glClearColor(0.12F, 0.12F, 0.15F, 1.0F);
|
|
643
|
+
glClear(GL_COLOR_BUFFER_BIT);
|
|
644
|
+
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
|
645
|
+
|
|
646
|
+
if ((app.io->ConfigFlags & ImGuiConfigFlags_ViewportsEnable) != 0) {
|
|
647
|
+
ImGui::UpdatePlatformWindows();
|
|
648
|
+
ImGui::RenderPlatformWindowsDefault();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
glfwMakeContextCurrent(app.window);
|
|
652
|
+
glfwSwapBuffers(app.window);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
static void window_size_callback(GLFWwindow* window, int, int) {
|
|
656
|
+
auto* app = static_cast<App*>(glfwGetWindowUserPointer(window));
|
|
657
|
+
if (app) render_frame(*app);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
int main() {
|
|
661
|
+
if (glfwInit() == 0) return 1;
|
|
662
|
+
|
|
663
|
+
const char* glsl_version = "#version 150";
|
|
664
|
+
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
|
|
665
|
+
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
|
|
666
|
+
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
|
667
|
+
#ifdef __APPLE__
|
|
668
|
+
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
|
|
669
|
+
#endif
|
|
670
|
+
|
|
671
|
+
GLFWwindow* window = glfwCreateWindow(800, 600, "${projectName}", nullptr, nullptr);
|
|
672
|
+
if (!window) { glfwTerminate(); return 1; }
|
|
673
|
+
glfwMakeContextCurrent(window);
|
|
674
|
+
glfwSwapInterval(1);
|
|
675
|
+
|
|
676
|
+
IMGUI_CHECKVERSION();
|
|
677
|
+
ImGui::CreateContext();
|
|
678
|
+
ImGuiIO& io = ImGui::GetIO();
|
|
679
|
+
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
|
680
|
+
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
|
|
681
|
+
|
|
682
|
+
ImGui::StyleColorsDark();
|
|
683
|
+
ImGuiStyle& style = ImGui::GetStyle();
|
|
684
|
+
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
|
685
|
+
style.WindowRounding = 0.0F;
|
|
686
|
+
style.Colors[ImGuiCol_WindowBg].w = 1.0F;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
ImGui_ImplGlfw_InitForOpenGL(window, true);
|
|
690
|
+
ImGui_ImplOpenGL3_Init(glsl_version);
|
|
691
|
+
|
|
692
|
+
App app;
|
|
693
|
+
app.window = window;
|
|
694
|
+
app.io = &io;
|
|
695
|
+
glfwSetWindowUserPointer(window, &app);
|
|
696
|
+
glfwSetWindowSizeCallback(window, window_size_callback);
|
|
697
|
+
|
|
698
|
+
${callbacks}
|
|
699
|
+
|
|
700
|
+
#ifdef _WIN32
|
|
701
|
+
app.module.load("imx_ui.dll");
|
|
702
|
+
#elif defined(__APPLE__)
|
|
703
|
+
app.module.load("libimx_ui.dylib");
|
|
704
|
+
#else
|
|
705
|
+
app.module.load("libimx_ui.so");
|
|
706
|
+
#endif
|
|
707
|
+
|
|
708
|
+
while (glfwWindowShouldClose(window) == 0) {
|
|
709
|
+
if (app.runtime.needs_frame()) {
|
|
710
|
+
glfwPollEvents();
|
|
711
|
+
} else {
|
|
712
|
+
glfwWaitEventsTimeout(0.1);
|
|
713
|
+
}
|
|
714
|
+
render_frame(app);
|
|
715
|
+
app.runtime.frame_rendered(ImGui::IsAnyItemActive());
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
app.module.unload();
|
|
719
|
+
|
|
720
|
+
ImGui_ImplOpenGL3_Shutdown();
|
|
721
|
+
ImGui_ImplGlfw_Shutdown();
|
|
722
|
+
ImGui::DestroyContext();
|
|
723
|
+
glfwDestroyWindow(window);
|
|
724
|
+
glfwTerminate();
|
|
725
|
+
return 0;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
#ifdef _WIN32
|
|
729
|
+
#include <windows.h>
|
|
730
|
+
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { return main(); }
|
|
731
|
+
#endif
|
|
732
|
+
`;
|
|
733
|
+
}
|
|
734
|
+
// ---------------------------------------------------------------------------
|
|
735
|
+
// AppState.h builder
|
|
736
|
+
// ---------------------------------------------------------------------------
|
|
737
|
+
function buildAppStateH(headers, fields, nlohmannMacroFields) {
|
|
738
|
+
const dedupedHeaders = [...new Set(['#include <functional>', ...headers])];
|
|
739
|
+
const headerBlock = dedupedHeaders.join('\n');
|
|
740
|
+
const macroLine = nlohmannMacroFields
|
|
741
|
+
? `\n// Only serialize data fields — callbacks are not persisted\nNLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AppState, ${nlohmannMacroFields.join(', ')})\n`
|
|
742
|
+
: '';
|
|
743
|
+
return `#pragma once
|
|
744
|
+
${headerBlock}
|
|
745
|
+
|
|
746
|
+
struct AppState {
|
|
747
|
+
${fields}
|
|
748
|
+
};
|
|
749
|
+
${macroLine}`;
|
|
750
|
+
}
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
// App.tsx builder
|
|
753
|
+
// ---------------------------------------------------------------------------
|
|
754
|
+
function buildAppTsx(windows) {
|
|
755
|
+
return `export default function App(props: AppState) {
|
|
756
|
+
return (
|
|
757
|
+
<DockSpace>
|
|
758
|
+
${windows}
|
|
759
|
+
</DockSpace>
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
`;
|
|
763
|
+
}
|
|
764
|
+
// ---------------------------------------------------------------------------
|
|
765
|
+
// Deduplicate C++ struct fields by field name
|
|
766
|
+
// ---------------------------------------------------------------------------
|
|
767
|
+
function deduplicateFields(fieldsBlock) {
|
|
768
|
+
const seen = new Set();
|
|
769
|
+
const result = [];
|
|
770
|
+
for (const line of fieldsBlock.split('\n')) {
|
|
771
|
+
const trimmed = line.trim();
|
|
772
|
+
if (trimmed === '')
|
|
773
|
+
continue;
|
|
774
|
+
// Extract field name: match "type name = ..." or "std::function<...> name;"
|
|
775
|
+
const match = trimmed.match(/(\w+)\s*[=;]/);
|
|
776
|
+
if (match) {
|
|
777
|
+
const fieldName = match[1];
|
|
778
|
+
if (seen.has(fieldName))
|
|
779
|
+
continue;
|
|
780
|
+
seen.add(fieldName);
|
|
781
|
+
}
|
|
782
|
+
result.push(line);
|
|
783
|
+
}
|
|
784
|
+
return result.join('\n');
|
|
785
|
+
}
|
|
786
|
+
// ---------------------------------------------------------------------------
|
|
787
|
+
// Deduplicate TS interface fields by field name
|
|
788
|
+
// ---------------------------------------------------------------------------
|
|
789
|
+
function deduplicateTsFields(fieldsBlock) {
|
|
790
|
+
const seen = new Set();
|
|
791
|
+
const result = [];
|
|
792
|
+
for (const line of fieldsBlock.split('\n')) {
|
|
793
|
+
const trimmed = line.trim();
|
|
794
|
+
if (trimmed === '')
|
|
795
|
+
continue;
|
|
796
|
+
// Extract field name: "name: type;"
|
|
797
|
+
const match = trimmed.match(/^(\w+)\s*[?]?\s*:/);
|
|
798
|
+
if (match) {
|
|
799
|
+
const fieldName = match[1];
|
|
800
|
+
if (seen.has(fieldName))
|
|
801
|
+
continue;
|
|
802
|
+
seen.add(fieldName);
|
|
803
|
+
}
|
|
804
|
+
result.push(line);
|
|
805
|
+
}
|
|
806
|
+
return result.join('\n');
|
|
807
|
+
}
|
|
808
|
+
// ---------------------------------------------------------------------------
|
|
809
|
+
// Main entry: generateCombined()
|
|
810
|
+
// ---------------------------------------------------------------------------
|
|
811
|
+
export function generateCombined(featureNames, projectDir, projectName) {
|
|
812
|
+
// 1. Resolve feature objects
|
|
813
|
+
const featureMap = new Map();
|
|
814
|
+
for (const f of FEATURES)
|
|
815
|
+
featureMap.set(f.name, f);
|
|
816
|
+
// Validate all names exist
|
|
817
|
+
for (const name of featureNames) {
|
|
818
|
+
if (!featureMap.has(name)) {
|
|
819
|
+
console.error(`Error: unknown feature "${name}". Available: ${FEATURES.map(f => f.name).join(', ')}`);
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
// 2. Resolve dependencies (auto-add required features)
|
|
824
|
+
const selected = new Set(featureNames);
|
|
825
|
+
let changed = true;
|
|
826
|
+
while (changed) {
|
|
827
|
+
changed = false;
|
|
828
|
+
for (const name of [...selected]) {
|
|
829
|
+
const feat = featureMap.get(name);
|
|
830
|
+
if (feat.requires) {
|
|
831
|
+
for (const dep of feat.requires) {
|
|
832
|
+
if (!selected.has(dep)) {
|
|
833
|
+
selected.add(dep);
|
|
834
|
+
changed = true;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
// Deterministic order: follow FEATURES array order
|
|
841
|
+
const ordered = FEATURES.filter(f => selected.has(f.name));
|
|
842
|
+
// 4. Collect everything
|
|
843
|
+
const allIncludes = [];
|
|
844
|
+
const allCppFields = [];
|
|
845
|
+
const allCppHeaders = [];
|
|
846
|
+
const allTsFields = [];
|
|
847
|
+
const allCallbacks = [];
|
|
848
|
+
const allWindows = [];
|
|
849
|
+
const allExtraFiles = {};
|
|
850
|
+
let nlohmannFields = null;
|
|
851
|
+
for (const feat of ordered) {
|
|
852
|
+
for (const inc of feat.includes) {
|
|
853
|
+
if (!allIncludes.includes(inc))
|
|
854
|
+
allIncludes.push(inc);
|
|
855
|
+
}
|
|
856
|
+
if (feat.appStateCppFields)
|
|
857
|
+
allCppFields.push(feat.appStateCppFields);
|
|
858
|
+
for (const h of feat.appStateCppHeaders) {
|
|
859
|
+
if (!allCppHeaders.includes(h))
|
|
860
|
+
allCppHeaders.push(h);
|
|
861
|
+
}
|
|
862
|
+
if (feat.appStateTsFields)
|
|
863
|
+
allTsFields.push(feat.appStateTsFields);
|
|
864
|
+
if (feat.callbacks)
|
|
865
|
+
allCallbacks.push(feat.callbacks);
|
|
866
|
+
if (feat.tsxWindow)
|
|
867
|
+
allWindows.push(feat.tsxWindow);
|
|
868
|
+
Object.assign(allExtraFiles, feat.extraFiles);
|
|
869
|
+
if (feat.dataFields) {
|
|
870
|
+
nlohmannFields = nlohmannFields || [];
|
|
871
|
+
nlohmannFields.push(...feat.dataFields);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// Deduplicate struct fields (e.g. `loading` from both async and networking)
|
|
875
|
+
const mergedCppFields = deduplicateFields(allCppFields.join('\n'));
|
|
876
|
+
const mergedTsFields = deduplicateTsFields(allTsFields.join('\n'));
|
|
877
|
+
// 5. Check for existing files
|
|
878
|
+
const srcDir = path.join(projectDir, 'src');
|
|
879
|
+
if (fs.existsSync(path.join(srcDir, 'App.tsx'))) {
|
|
880
|
+
console.error(`Error: ${srcDir}/App.tsx already exists. Aborting.`);
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
884
|
+
const publicDir = path.join(projectDir, 'public');
|
|
885
|
+
fs.mkdirSync(publicDir, { recursive: true });
|
|
886
|
+
// 6. Build files
|
|
887
|
+
const hasHotreload = selected.has('hotreload');
|
|
888
|
+
const hasPersistence = selected.has('persistence');
|
|
889
|
+
const includeLines = allIncludes.length > 0 ? allIncludes.join('\n') : '';
|
|
890
|
+
// Add async.h include if async.h is in extra files
|
|
891
|
+
const hasAsyncH = 'async.h' in allExtraFiles;
|
|
892
|
+
const asyncInclude = hasAsyncH ? '#include "async.h"' : '';
|
|
893
|
+
const fullIncludes = [includeLines, asyncInclude].filter(Boolean).join('\n');
|
|
894
|
+
// Build main.cpp
|
|
895
|
+
const mainCpp = hasHotreload
|
|
896
|
+
? buildMainCppHotreload(projectName, fullIncludes, allCallbacks.join('\n\n'), hasPersistence)
|
|
897
|
+
: buildMainCpp(projectName, fullIncludes, allCallbacks.join('\n\n'), hasPersistence);
|
|
898
|
+
const appStateH = buildAppStateH(allCppHeaders, mergedCppFields, nlohmannFields);
|
|
899
|
+
const appStateInterface = `interface AppState {\n${mergedTsFields}\n}`;
|
|
900
|
+
const appTsx = buildAppTsx(allWindows.join('\n'));
|
|
901
|
+
const imxDts = buildImxDts(appStateInterface);
|
|
902
|
+
// Build CMakeLists
|
|
903
|
+
const cmake = hasHotreload
|
|
904
|
+
? cmakeHotreloadCombined(projectName)
|
|
905
|
+
: cmakeTemplate(projectName, 'https://github.com/bgocumlu/imx.git');
|
|
906
|
+
// 7. Write files
|
|
907
|
+
fs.writeFileSync(path.join(srcDir, 'main.cpp'), mainCpp);
|
|
908
|
+
fs.writeFileSync(path.join(srcDir, 'AppState.h'), appStateH);
|
|
909
|
+
fs.writeFileSync(path.join(srcDir, 'App.tsx'), appTsx);
|
|
910
|
+
fs.writeFileSync(path.join(srcDir, 'imx.d.ts'), imxDts);
|
|
911
|
+
fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), TSCONFIG);
|
|
912
|
+
fs.writeFileSync(path.join(projectDir, 'CMakeLists.txt'), cmake);
|
|
913
|
+
fs.writeFileSync(path.join(projectDir, '.gitignore'), GITIGNORE);
|
|
914
|
+
const createdFiles = [
|
|
915
|
+
'src/main.cpp',
|
|
916
|
+
'src/AppState.h',
|
|
917
|
+
'src/App.tsx',
|
|
918
|
+
'src/imx.d.ts',
|
|
919
|
+
];
|
|
920
|
+
for (const [filename, content] of Object.entries(allExtraFiles)) {
|
|
921
|
+
fs.writeFileSync(path.join(srcDir, filename), content);
|
|
922
|
+
createdFiles.push(`src/${filename}`);
|
|
923
|
+
}
|
|
924
|
+
// Write hotreload-specific files
|
|
925
|
+
if (hasHotreload) {
|
|
926
|
+
fs.writeFileSync(path.join(srcDir, 'hotreload.h'), HOTRELOAD_H);
|
|
927
|
+
fs.writeFileSync(path.join(srcDir, 'ui_entry.cpp'), UI_ENTRY_CPP);
|
|
928
|
+
createdFiles.push('src/hotreload.h', 'src/ui_entry.cpp');
|
|
929
|
+
}
|
|
930
|
+
createdFiles.push('tsconfig.json', 'CMakeLists.txt', '.gitignore', 'public/');
|
|
931
|
+
// 8. Print summary
|
|
932
|
+
const featureList = ordered.map(f => f.name).join(', ');
|
|
933
|
+
console.log(`imxc: initialized project "${projectName}" with features: ${featureList}`);
|
|
934
|
+
console.log('');
|
|
935
|
+
console.log(' Created:');
|
|
936
|
+
for (const file of createdFiles) {
|
|
937
|
+
const pad = file.length < 22 ? ' '.repeat(22 - file.length) : ' ';
|
|
938
|
+
console.log(` ${file}${pad}`);
|
|
939
|
+
}
|
|
940
|
+
console.log('');
|
|
941
|
+
console.log(' Next steps:');
|
|
942
|
+
console.log(` cd ${projectName}`);
|
|
943
|
+
console.log(` cmake -B build`);
|
|
944
|
+
console.log(` cmake --build build`);
|
|
945
|
+
}
|