feat: add fate vite integration

This commit is contained in:
Aarnav Tale
2026-05-16 20:37:57 -04:00
parent 04ff2138d2
commit ad7e58570f
8 changed files with 107 additions and 14 deletions
+1
View File
@@ -1,4 +1,5 @@
node_modules
/.fate
/.react-router
/.cache
/.data
+2
View File
@@ -552,6 +552,8 @@ Do not add project-level wrappers such as `HeadplaneLivePublisher`, `HeadplaneDa
- Added a plain Vite SPA `index.html` and `app/spa` entry with TanStack Router and a raw Fate client shell using `createClient` + `createHTTPTransport`.
- Moved the old React Router Vite config to `vite-old.config.ts` and replaced `vite.config.ts` with a clean SPA config.
- Added Hono and `@hono/node-server`, plus a minimal Hono server shell in `app/server/hono-app.ts`, `app/server/hono-dev.ts`, and `app/server/hono-main.ts`.
- Added `app/server/fate.ts`, which exports a raw Fate server and live bus. Hono mounts Fate's `createHonoFateHandler(fate)` at `/admin/fate` and `/admin/fate/*`, passing the existing app context through Hono variables.
- Wired the official `react-fate/vite` plugin to `app/server/fate.ts`, ignored generated `.fate/` output, and made `pnpm run typecheck` run `fate generate` before `tsgo` so generated `react-fate/client` typings exist from a clean checkout.
- `pnpm dev` now runs the Hono/Vite middleware shell with local `.data` storage for the example config; `pnpm build` now runs `vite build` for the SPA.
- Fate's Drizzle peer currently warns against the repo's Drizzle `1.0.0-beta.21`; avoid Fate's Drizzle adapter until that compatibility is resolved, and start with a direct Headscale source/resolver instead.
+71
View File
@@ -0,0 +1,71 @@
import {
createFateServer,
createLiveEventBus,
type SourceDefinition,
type SourceRegistry,
} from "@nkzw/fate/server";
import type { Context } from "hono";
import type { AppContext } from "./context";
import type { RuntimeApiClient } from "./headscale/api/endpoints";
import type { Principal } from "./web/auth";
export interface HonoFateEnv {
Variables: {
appContext: AppContext;
};
}
interface FateContext {
api: RuntimeApiClient;
app: AppContext;
principal: Principal;
request: Request;
}
type FateAdapterContext = Context<HonoFateEnv>;
export const live = createLiveEventBus();
export const roots = {};
const queries = {};
const lists = {};
const mutations = {};
const registry = new Map() as SourceRegistry<FateContext>;
export const fate = createFateServer<
FateContext,
typeof roots,
typeof queries,
typeof lists,
typeof mutations,
FateAdapterContext
>({
context: async ({ adapterContext, request }) => {
if (!adapterContext) {
throw new Error("Fate request is missing Hono context");
}
const app = adapterContext.get("appContext");
const principal = await app.auth.require(request);
const api = app.hsApi.getRuntimeClient(app.auth.getHeadscaleApiKey(principal));
return {
api,
app,
principal,
request,
};
},
live,
roots,
sources: {
registry,
getSource(target) {
return target as SourceDefinition;
},
},
});
export type { FateAdapterContext, FateContext };
+11 -3
View File
@@ -1,9 +1,11 @@
import { versions } from "node:process";
import { serveStatic } from "@hono/node-server/serve-static";
import { createHonoFateHandler } from "@nkzw/fate/server";
import { Hono, type Context } from "hono";
import type { AppContext } from "./context";
import { fate, type HonoFateEnv } from "./fate";
interface HonoAppOptions {
context: AppContext;
@@ -12,7 +14,13 @@ interface HonoAppOptions {
}
export function createHeadplaneHonoApp({ context, prefix, staticRoot }: HonoAppOptions) {
const app = new Hono();
const app = new Hono<HonoFateEnv>();
const fateHandler = createHonoFateHandler(fate);
app.use("*", async (c, next) => {
c.set("appContext", context);
await next();
});
const health = async (c: Context) => {
const api = context.hsApi.getRuntimeClient("fake-api-key");
@@ -86,8 +94,8 @@ export function createHeadplaneHonoApp({ context, prefix, staticRoot }: HonoAppO
}
});
app.all(`${prefix}/fate`, (c) => c.json({ error: "Fate server is not mounted yet" }, 501));
app.all(`${prefix}/fate/*`, (c) => c.json({ error: "Fate server is not mounted yet" }, 501));
app.all(`${prefix}/fate`, fateHandler);
app.all(`${prefix}/fate/*`, fateHandler);
if (staticRoot) {
const stripPrefix = (path: string) => path.slice(prefix.length) || "/";
+5 -8
View File
@@ -1,15 +1,12 @@
import { RouterProvider } from "@tanstack/react-router";
import { FateClient, createClient, createHTTPTransport } from "react-fate";
import { FateClient } from "react-fate";
import { createFateClient } from "react-fate/client";
import { router } from "./router";
const fate = createClient({
roots: {},
transport: createHTTPTransport({
fetch: (input, init) => fetch(input, { ...init, credentials: "include" }),
url: `${__PREFIX__}/fate`,
}),
types: [],
const fate = createFateClient({
fetch: (input, init) => fetch(input, { ...init, credentials: "include" }),
url: `${__PREFIX__}/fate`,
});
export function App() {
+1 -1
View File
@@ -9,7 +9,7 @@
"build": "vite build",
"dev": "HEADPLANE_CONFIG_PATH=./config.example.yaml HEADPLANE_SERVER__DATA_PATH=./.data tsx app/server/hono-dev.ts",
"start": "tsx app/server/hono-main.ts",
"typecheck": "react-router typegen && tsgo",
"typecheck": "fate generate && react-router typegen && tsgo",
"test:unit": "vitest run --project unit",
"test:integration": "vitest run --project integration:*",
"docs:dev": "vitepress dev docs",
+7 -1
View File
@@ -1,5 +1,11 @@
{
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
"include": [
"**/*",
"**/.server/**/*",
"**/.client/**/*",
".fate/**/*",
".react-router/types/**/*"
],
"exclude": ["docs"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2023"],
+9 -1
View File
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { fate } from "react-fate/vite";
import { defineConfig } from "vite";
import { parse } from "yaml";
@@ -41,7 +42,14 @@ const { server } = parse(config);
export default defineConfig({
appType: "spa",
base: `${PREFIX}/`,
plugins: [react(), tailwindcss()],
plugins: [
fate({
module: "./app/server/fate.ts",
transport: "native",
}),
react(),
tailwindcss(),
],
server: {
host: server.host,
port: server.port,