import { events } from "@gtkx/ffi"; import { setHotReloading, update } from "@gtkx/react"; import { createServer, type InlineConfig, type ViteDevServer } from "vite"; import { isReactRefreshBoundary, performRefresh } from "./refresh-runtime.js"; import { gtkxAssets } from "./vite-plugin-gtkx-assets.js"; import { gtkxGSettings } from "./vite-plugin-gtkx-gsettings.js"; import { gtkxRefresh } from "./vite-plugin-gtkx-refresh.js"; import { swcSsrRefresh } from "./vite-plugin-swc-ssr-refresh.js"; /** * Options for the GTKX development server. */ export type DevServerOptions = { /** Path to the entry file (e.g., "src/dev.tsx") */ entry: string; /** Additional Vite configuration */ vite?: InlineConfig; }; type AppModule = { default: () => React.ReactNode; }; /** * Creates a Vite-based development server with hot module replacement. * * Provides fast refresh for React components and full reload for other changes. * The server watches for file changes and automatically updates the running * GTK application. * * @param options - Server configuration including entry point and Vite options * @returns A Vite development server instance * * @example * ```tsx * import { createDevServer } from "@gtkx/cli"; * import { render } from "@gtkx/react"; * * const server = await createDevServer({ * entry: "./src/dev.tsx", * }); * * const mod = await server.ssrLoadModule("./src/dev.tsx"); * render(, mod.appId); * ``` * * @see {@link DevServerOptions} for configuration options */ export const createDevServer = async (options: DevServerOptions): Promise => { const { entry, vite: viteConfig } = options; const moduleExports = new Map>(); const server = await createServer({ ...viteConfig, appType: "custom", plugins: [ gtkxGSettings(), gtkxAssets(), swcSsrRefresh(), gtkxRefresh(), { name: "gtkx:remove-react-dom-optimized", enforce: "post", config(config) { config.optimizeDeps ??= {}; config.optimizeDeps.include = config.optimizeDeps.include?.filter( (dep) => dep !== "react-dom" && !dep.startsWith("react-dom/"), ); }, }, ], server: { ...viteConfig?.server, middlewareMode: true, }, optimizeDeps: { ...viteConfig?.optimizeDeps, noDiscovery: true, include: [], }, ssr: { ...viteConfig?.ssr, external: true, }, }); const loadModule = async (): Promise => { const mod = (await server.ssrLoadModule(entry)) as AppModule; moduleExports.set(entry, { ...mod }); return mod; }; const invalidateAllModules = (): void => { for (const module of server.moduleGraph.idToModuleMap.values()) { server.moduleGraph.invalidateModule(module); } }; const invalidateModuleAndImporters = (filePath: string): void => { const module = server.moduleGraph.getModuleById(filePath); if (module) { server.moduleGraph.invalidateModule(module); for (const importer of module.importers) { server.moduleGraph.invalidateModule(importer); } } }; events.on("stop", () => { server.close(); }); server.watcher.on("change", async (changedPath) => { try { const module = server.moduleGraph.getModuleById(changedPath); if (!module) { return; } console.log(`[gtkx] File changed: ${changedPath}`); invalidateModuleAndImporters(changedPath); const newMod = (await server.ssrLoadModule(changedPath)) as Record; moduleExports.set(changedPath, { ...newMod }); if (isReactRefreshBoundary(newMod)) { console.log("[gtkx] Fast refreshing..."); performRefresh(); console.log("[gtkx] Fast refresh complete"); return; } console.log("[gtkx] Full reload..."); invalidateAllModules(); const mod = await loadModule(); const App = mod.default; if (typeof App !== "function") { console.error("[gtkx] Entry file must export a default function component"); return; } setHotReloading(true); try { await update(); } finally { setHotReloading(false); } console.log("[gtkx] Full reload complete"); } catch (error) { console.error("[gtkx] Hot reload failed:", error); } }); return server; };