imxc 0.5.4 → 0.6.1

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,216 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { registerTemplate, buildImxDts, TSCONFIG, GITIGNORE, cmakeTemplate } from './index.js';
4
+ const APPSTATE_INTERFACE = `interface AppState {
5
+ filePath: string;
6
+ message: string;
7
+ onOpen: () => void;
8
+ onSave: () => void;
9
+ }`;
10
+ const APPSTATE_H = `#pragma once
11
+ #include <string>
12
+ #include <functional>
13
+
14
+ struct AppState {
15
+ std::string filePath = "";
16
+ std::string message = "";
17
+ std::function<void()> onOpen;
18
+ std::function<void()> onSave;
19
+ };
20
+ `;
21
+ const MAIN_CPP = `#include <imx/runtime.h>
22
+ #include <imx/renderer.h>
23
+ #include <imx/pfd.h>
24
+
25
+ #include <imgui.h>
26
+ #include <imgui_impl_glfw.h>
27
+ #include <imgui_impl_opengl3.h>
28
+ #include <GLFW/glfw3.h>
29
+
30
+ #include "AppState.h"
31
+
32
+ struct App {
33
+ GLFWwindow* window = nullptr;
34
+ ImGuiIO* io = nullptr;
35
+ imx::Runtime runtime;
36
+ AppState state;
37
+ };
38
+
39
+ static void render_frame(App& app) {
40
+ glfwMakeContextCurrent(app.window);
41
+ if (glfwGetWindowAttrib(app.window, GLFW_ICONIFIED) != 0) return;
42
+
43
+ int fb_w = 0, fb_h = 0;
44
+ glfwGetFramebufferSize(app.window, &fb_w, &fb_h);
45
+ if (fb_w <= 0 || fb_h <= 0) return;
46
+
47
+ ImGui_ImplOpenGL3_NewFrame();
48
+ ImGui_ImplGlfw_NewFrame();
49
+ ImGui::NewFrame();
50
+
51
+ imx::render_root(app.runtime, app.state);
52
+
53
+ ImGui::Render();
54
+ glViewport(0, 0, fb_w, fb_h);
55
+ glClearColor(0.12F, 0.12F, 0.15F, 1.0F);
56
+ glClear(GL_COLOR_BUFFER_BIT);
57
+ ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
58
+
59
+ if ((app.io->ConfigFlags & ImGuiConfigFlags_ViewportsEnable) != 0) {
60
+ ImGui::UpdatePlatformWindows();
61
+ ImGui::RenderPlatformWindowsDefault();
62
+ }
63
+
64
+ glfwMakeContextCurrent(app.window);
65
+ glfwSwapBuffers(app.window);
66
+ }
67
+
68
+ static void window_size_callback(GLFWwindow* window, int, int) {
69
+ auto* app = static_cast<App*>(glfwGetWindowUserPointer(window));
70
+ if (app) render_frame(*app);
71
+ }
72
+
73
+ int main() {
74
+ if (glfwInit() == 0) return 1;
75
+
76
+ const char* glsl_version = "#version 150";
77
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
78
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
79
+ glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
80
+ #ifdef __APPLE__
81
+ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
82
+ #endif
83
+
84
+ GLFWwindow* window = glfwCreateWindow(800, 600, "APP_NAME", nullptr, nullptr);
85
+ if (!window) { glfwTerminate(); return 1; }
86
+ glfwMakeContextCurrent(window);
87
+ glfwSwapInterval(1);
88
+
89
+ IMGUI_CHECKVERSION();
90
+ ImGui::CreateContext();
91
+ ImGuiIO& io = ImGui::GetIO();
92
+ io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
93
+ io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
94
+
95
+ ImGui::StyleColorsDark();
96
+ ImGuiStyle& style = ImGui::GetStyle();
97
+ if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
98
+ style.WindowRounding = 0.0F;
99
+ style.Colors[ImGuiCol_WindowBg].w = 1.0F;
100
+ }
101
+
102
+ ImGui_ImplGlfw_InitForOpenGL(window, true);
103
+ ImGui_ImplOpenGL3_Init(glsl_version);
104
+
105
+ App app;
106
+ app.window = window;
107
+ app.io = &io;
108
+ glfwSetWindowUserPointer(window, &app);
109
+ glfwSetWindowSizeCallback(window, window_size_callback);
110
+
111
+ app.state.onOpen = [&]() {
112
+ auto result = pfd::open_file("Open File").result();
113
+ if (!result.empty()) {
114
+ app.state.filePath = result[0];
115
+ app.state.message = "Opened: " + result[0];
116
+ app.runtime.request_frame();
117
+ }
118
+ };
119
+
120
+ app.state.onSave = [&]() {
121
+ auto result = pfd::save_file("Save File").result();
122
+ if (!result.empty()) {
123
+ app.state.filePath = result;
124
+ app.state.message = "Save to: " + result;
125
+ app.runtime.request_frame();
126
+ }
127
+ };
128
+
129
+ // GLFW file drop callback
130
+ glfwSetDropCallback(window, [](GLFWwindow* w, int count, const char** paths) {
131
+ auto* a = static_cast<App*>(glfwGetWindowUserPointer(w));
132
+ if (a && count > 0) {
133
+ a->state.filePath = paths[0];
134
+ a->state.message = "Dropped: " + std::string(paths[0]);
135
+ a->runtime.request_frame();
136
+ }
137
+ });
138
+
139
+ while (glfwWindowShouldClose(window) == 0) {
140
+ if (app.runtime.needs_frame()) {
141
+ glfwPollEvents();
142
+ } else {
143
+ glfwWaitEventsTimeout(0.1);
144
+ }
145
+ render_frame(app);
146
+ app.runtime.frame_rendered(ImGui::IsAnyItemActive());
147
+ }
148
+
149
+ ImGui_ImplOpenGL3_Shutdown();
150
+ ImGui_ImplGlfw_Shutdown();
151
+ ImGui::DestroyContext();
152
+ glfwDestroyWindow(window);
153
+ glfwTerminate();
154
+ return 0;
155
+ }
156
+
157
+ #ifdef _WIN32
158
+ #include <windows.h>
159
+ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { return main(); }
160
+ #endif
161
+ `;
162
+ const APP_TSX = `export default function App(props: AppState) {
163
+ return (
164
+ <DockSpace>
165
+ <Window title="File Dialog Demo">
166
+ <Column gap={8}>
167
+ <Text>Native File Dialogs + Drag & Drop</Text>
168
+ <Separator />
169
+ <Row gap={8}>
170
+ <Button title="Open File" onPress={props.onOpen} />
171
+ <Button title="Save File" onPress={props.onSave} />
172
+ </Row>
173
+ <Text>Drag & drop a file onto this window</Text>
174
+ {props.message !== "" && <Text color={[0, 1, 0, 1]}>{props.message}</Text>}
175
+ {props.filePath !== "" && <Text>Path: {props.filePath}</Text>}
176
+ </Column>
177
+ </Window>
178
+ </DockSpace>
179
+ );
180
+ }
181
+ `;
182
+ function generate(projectDir, projectName) {
183
+ const srcDir = path.join(projectDir, 'src');
184
+ if (fs.existsSync(path.join(srcDir, 'App.tsx'))) {
185
+ console.error(`Error: ${srcDir}/App.tsx already exists. Aborting.`);
186
+ process.exit(1);
187
+ }
188
+ fs.mkdirSync(srcDir, { recursive: true });
189
+ const publicDir = path.join(projectDir, 'public');
190
+ fs.mkdirSync(publicDir, { recursive: true });
191
+ // Write files
192
+ fs.writeFileSync(path.join(srcDir, 'main.cpp'), MAIN_CPP.replace('APP_NAME', projectName));
193
+ fs.writeFileSync(path.join(srcDir, 'AppState.h'), APPSTATE_H);
194
+ fs.writeFileSync(path.join(srcDir, 'App.tsx'), APP_TSX);
195
+ fs.writeFileSync(path.join(srcDir, 'imx.d.ts'), buildImxDts(APPSTATE_INTERFACE));
196
+ fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), TSCONFIG);
197
+ fs.writeFileSync(path.join(projectDir, 'CMakeLists.txt'), cmakeTemplate(projectName, 'https://github.com/bgocumlu/imx.git'));
198
+ fs.writeFileSync(path.join(projectDir, '.gitignore'), GITIGNORE);
199
+ console.log(`imxc: initialized project "${projectName}" with template "filedialog"`);
200
+ console.log('');
201
+ console.log(' Created:');
202
+ console.log(` src/main.cpp — app shell with pfd + GLFW drop callback`);
203
+ console.log(` src/AppState.h — C++ state struct with filePath/message fields`);
204
+ console.log(` src/App.tsx — your root component`);
205
+ console.log(` src/imx.d.ts — type definitions for IDE support`);
206
+ console.log(` tsconfig.json — TypeScript config`);
207
+ console.log(` CMakeLists.txt — build config with FetchContent`);
208
+ console.log(` .gitignore — ignores build/, node_modules/, *.ini`);
209
+ console.log(` public/ — static assets (copied to exe directory)`);
210
+ console.log('');
211
+ console.log(' Next steps:');
212
+ console.log(` cd ${projectName}`);
213
+ console.log(` cmake -B build`);
214
+ console.log(` cmake --build build`);
215
+ }
216
+ registerTemplate({ name: 'filedialog', description: 'Native file dialogs + drag & drop', generate });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,400 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { registerTemplate, buildImxDts, TSCONFIG, GITIGNORE } from './index.js';
4
+ const APPSTATE_INTERFACE = `interface AppState {
5
+ count: number;
6
+ speed: number;
7
+ watchCmd: string;
8
+ onIncrement: () => void;
9
+ onCopyCmd: () => void;
10
+ }`;
11
+ const APPSTATE_H = `#pragma once
12
+ #include <string>
13
+ #include <functional>
14
+
15
+ struct AppState {
16
+ int count = 0;
17
+ float speed = 5.0F;
18
+ std::string watchCmd;
19
+ std::function<void()> onIncrement;
20
+ std::function<void()> onCopyCmd;
21
+ };
22
+ `;
23
+ const HOTRELOAD_H = `#pragma once
24
+ #include <string>
25
+ #include <filesystem>
26
+ #include <iostream>
27
+ #include <imx/runtime.h>
28
+ #include <imgui.h>
29
+ #include "AppState.h"
30
+
31
+ #ifdef _WIN32
32
+ #ifndef WIN32_LEAN_AND_MEAN
33
+ #define WIN32_LEAN_AND_MEAN
34
+ #endif
35
+ #include <windows.h>
36
+ #else
37
+ #include <dlfcn.h>
38
+ #include <unistd.h>
39
+ #endif
40
+
41
+ struct HotModule {
42
+ using RenderFn = void(*)(imx::Runtime&, AppState&, ImGuiContext*);
43
+
44
+ #ifdef _WIN32
45
+ HMODULE handle = nullptr;
46
+ #else
47
+ void* handle = nullptr;
48
+ #endif
49
+ RenderFn render = nullptr;
50
+ std::filesystem::file_time_type last_write{};
51
+ std::string path;
52
+
53
+ bool load(const std::string& lib_path) {
54
+ path = lib_path;
55
+ if (!std::filesystem::exists(path)) {
56
+ std::cerr << "hotreload: " << path << " not found\\n";
57
+ return false;
58
+ }
59
+ last_write = std::filesystem::last_write_time(path);
60
+
61
+ #ifdef _WIN32
62
+ // Copy DLL to avoid locking the original (allows rebuild while loaded)
63
+ std::string copy_path = path + ".live";
64
+ std::filesystem::copy_file(path, copy_path, std::filesystem::copy_options::overwrite_existing);
65
+ handle = LoadLibraryA(copy_path.c_str());
66
+ if (!handle) {
67
+ std::cerr << "hotreload: LoadLibrary failed\\n";
68
+ return false;
69
+ }
70
+ render = reinterpret_cast<RenderFn>(GetProcAddress(handle, "imx_render"));
71
+ #else
72
+ handle = dlopen(path.c_str(), RTLD_NOW);
73
+ if (!handle) {
74
+ std::cerr << "hotreload: dlopen failed: " << dlerror() << "\\n";
75
+ return false;
76
+ }
77
+ render = reinterpret_cast<RenderFn>(dlsym(handle, "imx_render"));
78
+ #endif
79
+ if (!render) {
80
+ std::cerr << "hotreload: imx_render symbol not found\\n";
81
+ unload();
82
+ return false;
83
+ }
84
+ std::cout << "hotreload: loaded " << path << "\\n";
85
+ return true;
86
+ }
87
+
88
+ void unload() {
89
+ if (!handle) return;
90
+ #ifdef _WIN32
91
+ FreeLibrary(handle);
92
+ #else
93
+ dlclose(handle);
94
+ #endif
95
+ handle = nullptr;
96
+ render = nullptr;
97
+ }
98
+
99
+ bool check_reload() {
100
+ if (path.empty() || !std::filesystem::exists(path)) return false;
101
+ auto current = std::filesystem::last_write_time(path);
102
+ if (current == last_write) return false;
103
+ std::cout << "hotreload: change detected, reloading...\\n";
104
+ unload();
105
+ // Small delay to ensure file write is complete
106
+ #ifdef _WIN32
107
+ Sleep(100);
108
+ #else
109
+ usleep(100000);
110
+ #endif
111
+ if (load(path)) {
112
+ std::cout << "hotreload: reload successful\\n";
113
+ return true;
114
+ }
115
+ std::cerr << "hotreload: reload failed\\n";
116
+ return false;
117
+ }
118
+
119
+ ~HotModule() { unload(); }
120
+ };
121
+ `;
122
+ const UI_ENTRY_CPP = `#include <imx/runtime.h>
123
+ #include <imgui.h>
124
+ #include "AppState.h"
125
+
126
+ // Declared in app_root.gen.cpp (generated by imxc)
127
+ namespace imx {
128
+ template <> void render_root<AppState>(imx::Runtime& runtime, AppState& state);
129
+ }
130
+
131
+ #ifdef _WIN32
132
+ #define EXPORT __declspec(dllexport)
133
+ #else
134
+ #define EXPORT __attribute__((visibility("default")))
135
+ #endif
136
+
137
+ // ImGuiContext* must be passed from host — DLL has its own static ImGui globals
138
+ extern "C" EXPORT void imx_render(imx::Runtime& runtime, AppState& state, ImGuiContext* ctx) {
139
+ ImGui::SetCurrentContext(ctx);
140
+ imx::render_root(runtime, state);
141
+ }
142
+ `;
143
+ const MAIN_CPP = `#include <imx/runtime.h>
144
+ #include <imx/renderer.h>
145
+
146
+ #include <imgui.h>
147
+ #include <imgui_impl_glfw.h>
148
+ #include <imgui_impl_opengl3.h>
149
+ #include <GLFW/glfw3.h>
150
+
151
+ #include "hotreload.h"
152
+ #include "AppState.h"
153
+
154
+ struct App {
155
+ GLFWwindow* window = nullptr;
156
+ ImGuiIO* io = nullptr;
157
+ imx::Runtime runtime;
158
+ AppState state;
159
+ HotModule module;
160
+ };
161
+
162
+ static void render_frame(App& app) {
163
+ glfwMakeContextCurrent(app.window);
164
+ if (glfwGetWindowAttrib(app.window, GLFW_ICONIFIED) != 0) return;
165
+
166
+ int fb_w = 0, fb_h = 0;
167
+ glfwGetFramebufferSize(app.window, &fb_w, &fb_h);
168
+ if (fb_w <= 0 || fb_h <= 0) return;
169
+
170
+ ImGui_ImplOpenGL3_NewFrame();
171
+ ImGui_ImplGlfw_NewFrame();
172
+ ImGui::NewFrame();
173
+
174
+ app.module.check_reload();
175
+ if (app.module.render) {
176
+ app.module.render(app.runtime, app.state, ImGui::GetCurrentContext());
177
+ }
178
+
179
+ ImGui::Render();
180
+ glViewport(0, 0, fb_w, fb_h);
181
+ glClearColor(0.12F, 0.12F, 0.15F, 1.0F);
182
+ glClear(GL_COLOR_BUFFER_BIT);
183
+ ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
184
+
185
+ if ((app.io->ConfigFlags & ImGuiConfigFlags_ViewportsEnable) != 0) {
186
+ ImGui::UpdatePlatformWindows();
187
+ ImGui::RenderPlatformWindowsDefault();
188
+ }
189
+
190
+ glfwMakeContextCurrent(app.window);
191
+ glfwSwapBuffers(app.window);
192
+ }
193
+
194
+ static void window_size_callback(GLFWwindow* window, int, int) {
195
+ auto* app = static_cast<App*>(glfwGetWindowUserPointer(window));
196
+ if (app) render_frame(*app);
197
+ }
198
+
199
+ int main() {
200
+ if (glfwInit() == 0) return 1;
201
+
202
+ const char* glsl_version = "#version 150";
203
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
204
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
205
+ glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
206
+ #ifdef __APPLE__
207
+ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
208
+ #endif
209
+
210
+ GLFWwindow* window = glfwCreateWindow(1000, 700, "APP_NAME", nullptr, nullptr);
211
+ if (!window) { glfwTerminate(); return 1; }
212
+ glfwMakeContextCurrent(window);
213
+ glfwSwapInterval(1);
214
+
215
+ IMGUI_CHECKVERSION();
216
+ ImGui::CreateContext();
217
+ ImGuiIO& io = ImGui::GetIO();
218
+ io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
219
+ io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
220
+
221
+ ImGui::StyleColorsDark();
222
+ ImGuiStyle& style = ImGui::GetStyle();
223
+ if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
224
+ style.WindowRounding = 0.0F;
225
+ style.Colors[ImGuiCol_WindowBg].w = 1.0F;
226
+ }
227
+
228
+ ImGui_ImplGlfw_InitForOpenGL(window, true);
229
+ ImGui_ImplOpenGL3_Init(glsl_version);
230
+
231
+ App app;
232
+ app.window = window;
233
+ app.io = &io;
234
+ glfwSetWindowUserPointer(window, &app);
235
+ glfwSetWindowSizeCallback(window, window_size_callback);
236
+
237
+ app.state.onIncrement = [&]() { app.state.count++; };
238
+ app.state.watchCmd = "npx imxc watch src -o build/generated --build \\"cmake --build build --target imx_ui\\"";
239
+ app.state.onCopyCmd = [&]() { imx::clipboard_set(app.state.watchCmd.c_str()); };
240
+
241
+ #ifdef _WIN32
242
+ app.module.load("imx_ui.dll");
243
+ #elif defined(__APPLE__)
244
+ app.module.load("libimx_ui.dylib");
245
+ #else
246
+ app.module.load("libimx_ui.so");
247
+ #endif
248
+
249
+ while (glfwWindowShouldClose(window) == 0) {
250
+ if (app.runtime.needs_frame()) {
251
+ glfwPollEvents();
252
+ } else {
253
+ glfwWaitEventsTimeout(0.1);
254
+ }
255
+ render_frame(app);
256
+ app.runtime.frame_rendered(ImGui::IsAnyItemActive());
257
+ }
258
+
259
+ app.module.unload();
260
+
261
+ ImGui_ImplOpenGL3_Shutdown();
262
+ ImGui_ImplGlfw_Shutdown();
263
+ ImGui::DestroyContext();
264
+ glfwDestroyWindow(window);
265
+ glfwTerminate();
266
+ return 0;
267
+ }
268
+
269
+ #ifdef _WIN32
270
+ #include <windows.h>
271
+ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { return main(); }
272
+ #endif
273
+ `;
274
+ const APP_TSX = `export default function App(props: AppState) {
275
+ const [showAbout, setShowAbout] = useState(false);
276
+
277
+ return (
278
+ <DockSpace>
279
+ <Window title="Controls">
280
+ <Column gap={8}>
281
+ <Text>Count: {props.count}</Text>
282
+ <Button title="Increment" onPress={props.onIncrement} />
283
+ <SliderFloat label="Speed" value={props.speed} min={0} max={100} />
284
+ <Separator />
285
+ <Button title="About" onPress={() => setShowAbout(!showAbout)} />
286
+ </Column>
287
+ </Window>
288
+ <Window title="Hot Reload">
289
+ <Column gap={8}>
290
+ <Text>Run this in a second terminal to enable live reload:</Text>
291
+ <Text wrapped={true}>{props.watchCmd}</Text>
292
+ <Button title="Copy" onPress={props.onCopyCmd} />
293
+ </Column>
294
+ </Window>
295
+ {showAbout && <Window title="About">
296
+ <Text>Built with IMX (hot-reloadable!)</Text>
297
+ <Button title="Close" onPress={() => setShowAbout(false)} />
298
+ </Window>}
299
+ </DockSpace>
300
+ );
301
+ }
302
+ `;
303
+ function cmakeHotreload(projectName) {
304
+ return `cmake_minimum_required(VERSION 3.25)
305
+ project(${projectName} LANGUAGES CXX)
306
+
307
+ set(CMAKE_CXX_STANDARD 20)
308
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
309
+
310
+ include(FetchContent)
311
+ set(FETCHCONTENT_QUIET OFF)
312
+
313
+ FetchContent_Declare(
314
+ imx
315
+ GIT_REPOSITORY https://github.com/bgocumlu/imx.git
316
+ GIT_TAG v0.6.1
317
+ GIT_SHALLOW TRUE
318
+ GIT_PROGRESS TRUE
319
+ )
320
+ message(STATUS "Fetching IMX (includes ImGui + GLFW)...")
321
+ FetchContent_MakeAvailable(imx)
322
+
323
+ include(ImxCompile)
324
+
325
+ imx_compile_tsx(GENERATED
326
+ SOURCES src/App.tsx
327
+ OUTPUT_DIR \${CMAKE_BINARY_DIR}/generated
328
+ )
329
+
330
+ # UI shared library (hot-reloadable)
331
+ add_library(imx_ui SHARED
332
+ src/ui_entry.cpp
333
+ \${GENERATED}
334
+ )
335
+ target_link_libraries(imx_ui PRIVATE imx::renderer)
336
+ target_include_directories(imx_ui PRIVATE \${CMAKE_BINARY_DIR}/generated \${CMAKE_CURRENT_SOURCE_DIR}/src)
337
+
338
+ # Host executable (loads UI module dynamically)
339
+ add_executable(${projectName}
340
+ src/main.cpp
341
+ )
342
+ set_target_properties(${projectName} PROPERTIES WIN32_EXECUTABLE $<CONFIG:Release>)
343
+ target_link_libraries(${projectName} PRIVATE imx::renderer \${CMAKE_DL_LIBS})
344
+ target_include_directories(${projectName} PRIVATE \${CMAKE_CURRENT_SOURCE_DIR}/src)
345
+
346
+ # Copy DLL/SO next to host exe after build
347
+ add_custom_command(TARGET imx_ui POST_BUILD
348
+ COMMAND \${CMAKE_COMMAND} -E copy $<TARGET_FILE:imx_ui> $<TARGET_FILE_DIR:${projectName}>
349
+ COMMENT "Copying UI module to host directory"
350
+ )
351
+ add_dependencies(${projectName} imx_ui)
352
+
353
+ # Copy public/ assets to output directory
354
+ add_custom_command(TARGET ${projectName} POST_BUILD
355
+ COMMAND \${CMAKE_COMMAND} -E copy_directory
356
+ \${CMAKE_CURRENT_SOURCE_DIR}/public
357
+ $<TARGET_FILE_DIR:${projectName}>
358
+ COMMENT "Copying public/ assets"
359
+ )
360
+ `;
361
+ }
362
+ function generate(projectDir, projectName) {
363
+ const srcDir = path.join(projectDir, 'src');
364
+ if (fs.existsSync(path.join(srcDir, 'App.tsx'))) {
365
+ console.error(`Error: ${srcDir}/App.tsx already exists. Aborting.`);
366
+ process.exit(1);
367
+ }
368
+ fs.mkdirSync(srcDir, { recursive: true });
369
+ const publicDir = path.join(projectDir, 'public');
370
+ fs.mkdirSync(publicDir, { recursive: true });
371
+ // Write files
372
+ fs.writeFileSync(path.join(srcDir, 'main.cpp'), MAIN_CPP.replaceAll('APP_NAME', projectName));
373
+ fs.writeFileSync(path.join(srcDir, 'AppState.h'), APPSTATE_H);
374
+ fs.writeFileSync(path.join(srcDir, 'hotreload.h'), HOTRELOAD_H);
375
+ fs.writeFileSync(path.join(srcDir, 'ui_entry.cpp'), UI_ENTRY_CPP);
376
+ fs.writeFileSync(path.join(srcDir, 'App.tsx'), APP_TSX);
377
+ fs.writeFileSync(path.join(srcDir, 'imx.d.ts'), buildImxDts(APPSTATE_INTERFACE));
378
+ fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), TSCONFIG);
379
+ fs.writeFileSync(path.join(projectDir, 'CMakeLists.txt'), cmakeHotreload(projectName));
380
+ fs.writeFileSync(path.join(projectDir, '.gitignore'), GITIGNORE);
381
+ console.log(`imxc: initialized project "${projectName}" with template "hotreload"`);
382
+ console.log('');
383
+ console.log(' Created:');
384
+ console.log(` src/main.cpp — host exe with DLL hot reload loop`);
385
+ console.log(` src/AppState.h — C++ state struct (lives in host, survives reloads)`);
386
+ console.log(` src/hotreload.h — cross-platform DLL/SO loader`);
387
+ console.log(` src/ui_entry.cpp — DLL export wrapper for imx_render()`);
388
+ console.log(` src/App.tsx — your root component (compiled into DLL)`);
389
+ console.log(` src/imx.d.ts — type definitions for IDE support`);
390
+ console.log(` tsconfig.json — TypeScript config`);
391
+ console.log(` CMakeLists.txt — build config with host exe + UI shared lib`);
392
+ console.log(` .gitignore — ignores build/, node_modules/, *.ini`);
393
+ console.log(` public/ — static assets (copied to exe directory)`);
394
+ console.log('');
395
+ console.log(' Next steps:');
396
+ console.log(` cd ${projectName}`);
397
+ console.log(` cmake -B build`);
398
+ console.log(` cmake --build build`);
399
+ }
400
+ registerTemplate({ name: 'hotreload', description: 'DLL hot reload for live UI iteration', generate });