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;
};