feat: US-003 — electron-trpc IPC bridge and appRouter scaffold
- Install electron-trpc, @trpc/server, @trpc/client, @trpc/react-query, @tanstack/react-query, zod - Create appRouter in src/main/router/index.ts with stub routers for: health, clients, projects, tasks, checkpoints, notes, ai - health.ping procedure returns 'pong' - All procedure inputs validated with Zod schemas - Configure IPC handler in main process (createIPCHandler) and preload (exposeElectronTRPC) - Create renderer trpc client in src/renderer/lib/trpc.ts with ipcLink - Wrap app with TRPCProvider + QueryClientProvider in src/renderer/index.tsx - Verify health.ping in HomePage component (shows 'tRPC IPC bridge: pong') - Typecheck passes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import path from 'node:path';
|
||||
import started from 'electron-squirrel-startup';
|
||||
import { createIPCHandler } from 'electron-trpc/main';
|
||||
import { initDb } from './db';
|
||||
import { appRouter } from './router';
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
if (started) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
const createWindow = () => {
|
||||
const createWindow = (): BrowserWindow => {
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
@@ -36,6 +38,8 @@ const createWindow = () => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
return mainWindow;
|
||||
};
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
@@ -43,7 +47,8 @@ const createWindow = () => {
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', () => {
|
||||
initDb();
|
||||
createWindow();
|
||||
const win = createWindow();
|
||||
createIPCHandler({ router: appRouter, windows: [win] });
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
|
||||
163
src/main/router/index.ts
Normal file
163
src/main/router/index.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { initTRPC } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
const t = initTRPC.create();
|
||||
|
||||
const router = t.router;
|
||||
const publicProcedure = t.procedure;
|
||||
|
||||
// Health router — ping/pong verification
|
||||
const healthRouter = router({
|
||||
ping: publicProcedure.query(() => 'pong' as const),
|
||||
});
|
||||
|
||||
// Stub routers — full implementations come in later user stories
|
||||
const clientsRouter = router({
|
||||
list: publicProcedure.query(() => []),
|
||||
create: publicProcedure
|
||||
.input(z.object({ name: z.string(), parentId: z.string().optional(), industry: z.string().optional() }))
|
||||
.mutation(() => null),
|
||||
update: publicProcedure
|
||||
.input(z.object({ id: z.string(), name: z.string().optional(), industry: z.string().optional() }))
|
||||
.mutation(() => null),
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(() => null),
|
||||
deleteWithCascade: publicProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(() => null),
|
||||
});
|
||||
|
||||
const projectsRouter = router({
|
||||
list: publicProcedure
|
||||
.input(z.object({ clientId: z.string().optional(), includeArchived: z.boolean().optional() }).optional())
|
||||
.query(() => []),
|
||||
listAll: publicProcedure.query(() => [] as Array<{ id: string; name: string }>),
|
||||
get: publicProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(() => null),
|
||||
create: publicProcedure
|
||||
.input(z.object({ name: z.string(), clientId: z.string().optional() }))
|
||||
.mutation(() => null),
|
||||
update: publicProcedure
|
||||
.input(z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
clientId: z.string().optional(),
|
||||
status: z.enum(['active', 'archived']).optional(),
|
||||
aiSummary: z.string().optional(),
|
||||
}))
|
||||
.mutation(() => null),
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(() => null),
|
||||
});
|
||||
|
||||
const tasksRouter = router({
|
||||
list: publicProcedure
|
||||
.input(z.object({
|
||||
projectId: z.string().optional(),
|
||||
status: z.enum(['todo', 'in_progress', 'done']).optional(),
|
||||
search: z.string().optional(),
|
||||
orderBy: z.enum(['dueDate', 'priority', 'createdAt']).optional(),
|
||||
}).optional())
|
||||
.query(() => []),
|
||||
create: publicProcedure
|
||||
.input(z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
priority: z.string().optional(),
|
||||
assignee: z.string().optional(),
|
||||
dueDate: z.number().optional(),
|
||||
projectId: z.string().optional(),
|
||||
}))
|
||||
.mutation(() => null),
|
||||
update: publicProcedure
|
||||
.input(z.object({
|
||||
id: z.string(),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
priority: z.string().optional(),
|
||||
assignee: z.string().optional(),
|
||||
dueDate: z.number().optional(),
|
||||
projectId: z.string().optional(),
|
||||
}))
|
||||
.mutation(() => null),
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(() => null),
|
||||
});
|
||||
|
||||
const checkpointsRouter = router({
|
||||
list: publicProcedure
|
||||
.input(z.object({ projectId: z.string().optional() }).optional())
|
||||
.query(() => []),
|
||||
create: publicProcedure
|
||||
.input(z.object({
|
||||
projectId: z.string(),
|
||||
title: z.string(),
|
||||
date: z.number(),
|
||||
isAiSuggested: z.number().optional(),
|
||||
isApproved: z.number().optional(),
|
||||
}))
|
||||
.mutation(() => null),
|
||||
update: publicProcedure
|
||||
.input(z.object({
|
||||
id: z.string(),
|
||||
title: z.string().optional(),
|
||||
date: z.number().optional(),
|
||||
isApproved: z.number().optional(),
|
||||
}))
|
||||
.mutation(() => null),
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(() => null),
|
||||
});
|
||||
|
||||
const notesRouter = router({
|
||||
list: publicProcedure
|
||||
.input(z.object({ projectId: z.string().optional() }).optional())
|
||||
.query(() => []),
|
||||
get: publicProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(() => null),
|
||||
create: publicProcedure
|
||||
.input(z.object({ title: z.string(), content: z.string(), projectId: z.string().optional() }))
|
||||
.mutation(() => null),
|
||||
update: publicProcedure
|
||||
.input(z.object({ id: z.string(), title: z.string().optional(), content: z.string().optional() }))
|
||||
.mutation(() => null),
|
||||
delete: publicProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(() => null),
|
||||
});
|
||||
|
||||
const aiRouter = router({
|
||||
chat: publicProcedure
|
||||
.input(z.object({
|
||||
message: z.string(),
|
||||
context: z.object({
|
||||
type: z.enum(['global', 'project']),
|
||||
projectId: z.string().optional(),
|
||||
}),
|
||||
}))
|
||||
.mutation(() => ({ response: '' })),
|
||||
setToken: publicProcedure
|
||||
.input(z.object({ token: z.string() }))
|
||||
.mutation(() => null),
|
||||
hasToken: publicProcedure.query(() => false),
|
||||
});
|
||||
|
||||
export const appRouter = router({
|
||||
health: healthRouter,
|
||||
clients: clientsRouter,
|
||||
projects: projectsRouter,
|
||||
tasks: tasksRouter,
|
||||
checkpoints: checkpointsRouter,
|
||||
notes: notesRouter,
|
||||
ai: aiRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
@@ -1,2 +1,7 @@
|
||||
// See the Electron documentation for details on how to use preload scripts:
|
||||
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
||||
import { exposeElectronTRPC } from 'electron-trpc/main';
|
||||
|
||||
process.once('loaded', () => {
|
||||
exposeElectronTRPC();
|
||||
});
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { StrictMode, useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { RouterProvider } from '@tanstack/react-router';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ipcLink } from 'electron-trpc/renderer';
|
||||
import { router } from './router';
|
||||
import { trpc } from './lib/trpc';
|
||||
import './globals.css';
|
||||
|
||||
function App() {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
const [trpcClient] = useState(() =>
|
||||
trpc.createClient({
|
||||
links: [ipcLink()],
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) throw new Error('Root element not found');
|
||||
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
4
src/renderer/lib/trpc.ts
Normal file
4
src/renderer/lib/trpc.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createTRPCReact } from '@trpc/react-query';
|
||||
import type { AppRouter } from '../../main/router';
|
||||
|
||||
export const trpc = createTRPCReact<AppRouter>();
|
||||
@@ -1,10 +1,13 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { trpc } from '@/lib/trpc';
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
component: HomePage,
|
||||
});
|
||||
|
||||
function HomePage() {
|
||||
const pingQuery = trpc.health.ping.useQuery();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -27,6 +30,11 @@ function HomePage() {
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Adiuva is ready. Start building.
|
||||
</p>
|
||||
{pingQuery.data && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
tRPC IPC bridge: {pingQuery.data}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user