Skip to content

Commit bfcc210

Browse files
committed
ARCHITECTURE.md and CLAUDE.md
1 parent 7b35edf commit bfcc210

File tree

2 files changed

+232
-6
lines changed

2 files changed

+232
-6
lines changed

ARCHITECTURE.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# Contributing to Array
2+
3+
## Architecture Overview
4+
5+
Array is an Electron app with a React renderer. The main process handles system operations (stateless), while the renderer owns all application state.
6+
7+
```
8+
Main Process (Node.js) Renderer Process (React)
9+
┌───────────────────────┐ ┌───────────────────────────┐
10+
│ DI Container │ │ DI Container │
11+
│ ├── GitService │ │ ├── TRPCClient │
12+
│ └── ... │ │ └── TaskService, ... │
13+
├───────────────────────┤ ├───────────────────────────┤
14+
│ tRPC Routers │ ◄─tRPC(ipcLink)─► │ tRPC Clients │
15+
│ (use DI services) │ │ ├── trpcReact (hooks) │
16+
├───────────────────────┤ │ └── trpcVanilla │
17+
│ System I/O │ ├───────────────────────────┤
18+
│ (fs, git, shell) │ │ Zustand Stores (state) │
19+
│ STATELESS │ │ ├── taskStore │
20+
└───────────────────────┘ │ ├── workspaceStore │
21+
│ └── ... │
22+
├───────────────────────────┤
23+
│ React UI │
24+
└───────────────────────────┘
25+
```
26+
27+
**Key points:**
28+
- Both processes use InversifyJS for DI
29+
- Renderer DI holds services + tRPC client; services can coordinate stores
30+
- Zustand stores own all application state (not in DI)
31+
- Main process is stateless - pure I/O operations only
32+
33+
## Dependency Injection
34+
35+
Both processes use [InversifyJS](https://inversify.io/) for dependency injection with singleton scope.
36+
37+
| Process | Container | Holds |
38+
|----------|--------------------|------------------------------------- |
39+
| Main | `src/main/di/` | Stateless services (GitService, etc.)|
40+
| Renderer | `src/renderer/di/` | Services + TRPCClient |
41+
42+
### Defining a Service
43+
44+
```typescript
45+
// src/main/services/my-service/service.ts (or src/renderer/services/)
46+
import { injectable } from "inversify";
47+
48+
@injectable()
49+
export class MyService {
50+
doSomething() {
51+
// ...
52+
}
53+
}
54+
```
55+
56+
### Registering a Service
57+
58+
```typescript
59+
// src/main/di/container.ts (or src/renderer/di/container.ts)
60+
container.bind<MyService>(TOKENS.MyService).to(MyService);
61+
```
62+
63+
```typescript
64+
// src/main/di/tokens.ts (or src/renderer/di/tokens.ts)
65+
export const MAIN_TOKENS = Object.freeze({
66+
MyService: Symbol.for("Main.MyService"),
67+
});
68+
```
69+
70+
### Using a Service
71+
72+
```typescript
73+
import { get } from "@main/di"; // or @renderer/di
74+
75+
const myService = get<MyService>(TOKENS.MyService);
76+
myService.doSomething();
77+
```
78+
79+
## IPC via tRPC
80+
81+
We use [tRPC](https://trpc.io/) with [trpc-electron](https://github.com/jsonnull/electron-trpc) for type-safe communication between main and renderer. The `ipcLink()` handles serialization over Electron IPC.
82+
83+
### Creating a Router (Main Process)
84+
85+
```typescript
86+
// src/main/trpc/routers/my-router.ts
87+
import { z } from "zod";
88+
import { router, publicProcedure } from "../trpc";
89+
import { get } from "@main/di";
90+
import { MAIN_TOKENS } from "@main/di/tokens";
91+
92+
export const myRouter = router({
93+
// Query - for read operations
94+
getData: publicProcedure
95+
.input(z.object({ id: z.string() }))
96+
.query(async ({ input }) => {
97+
const service = get<MyService>(MAIN_TOKENS.MyService);
98+
return service.getData(input.id);
99+
}),
100+
101+
// Mutation - for write operations
102+
updateData: publicProcedure
103+
.input(z.object({ id: z.string(), value: z.string() }))
104+
.mutation(async ({ input }) => {
105+
const service = get<MyService>(MAIN_TOKENS.MyService);
106+
return service.updateData(input.id, input.value);
107+
}),
108+
});
109+
```
110+
111+
### Registering the Router
112+
113+
```typescript
114+
// src/main/trpc/router.ts
115+
import { myRouter } from "./routers/my-router";
116+
117+
export const trpcRouter = router({
118+
my: myRouter,
119+
// ...
120+
});
121+
```
122+
123+
### Using tRPC in Renderer
124+
125+
**React hooks:**
126+
127+
```typescript
128+
import { trpcReact } from "@renderer/trpc/client";
129+
130+
function MyComponent() {
131+
// Queries
132+
const { data } = trpcReact.my.getData.useQuery({ id: "123" });
133+
134+
// Mutations
135+
const mutation = trpcReact.my.updateData.useMutation();
136+
const handleUpdate = () => mutation.mutate({ id: "123", value: "new" });
137+
}
138+
```
139+
140+
**Outside React (vanilla client):**
141+
142+
```typescript
143+
import { trpcVanilla } from "@renderer/trpc/client";
144+
145+
const data = await trpcVanilla.my.getData.query({ id: "123" });
146+
```
147+
148+
## State Management
149+
150+
**All application state lives in the renderer.** Main process services should be stateless/pure.
151+
152+
| Layer | State | Role |
153+
|-------|-------|------|
154+
| **Renderer** | Zustand stores | Owns all application state |
155+
| **Main** | Stateless | Pure operations (file I/O, git, shell, etc.) |
156+
157+
This keeps state predictable, easy to debug, and naturally supports patterns like undo/rollback.
158+
159+
### Example
160+
161+
```typescript
162+
// ❌ Bad - main service with state
163+
@injectable()
164+
class TaskService {
165+
private currentTask: Task | null = null; // Don't do this
166+
}
167+
168+
// ✅ Good - main service is pure
169+
@injectable()
170+
class TaskService {
171+
async readTask(id: string): Promise<Task> { /* ... */ }
172+
async writeTask(task: Task): Promise<void> { /* ... */ }
173+
}
174+
175+
// ✅ Good - state lives in renderer
176+
// src/renderer/stores/task-store.ts
177+
const useTaskStore = create<TaskState>((set) => ({
178+
currentTask: null,
179+
setCurrentTask: (task) => set({ currentTask: task }),
180+
}));
181+
```
182+
183+
## Services
184+
185+
Services encapsulate business logic and exist in both processes:
186+
187+
- **Main services** (`src/main/services/`) - System operations (file I/O, git, shell)
188+
- **Renderer services** (`src/renderer/services/`) - UI logic, API calls
189+
190+
Main services should be:
191+
192+
- **Injectable**: Decorated with `@injectable()` for DI
193+
- **Stateless**: No mutable instance state, pure operations only
194+
- **Single responsibility**: One concern per service
195+
196+
### Service Structure
197+
198+
```
199+
src/main/services/
200+
├── my-service/
201+
│ ├── service.ts # The injectable service class
202+
│ └── types.ts # Types and interfaces
203+
204+
src/renderer/services/
205+
├── my-service.ts # Renderer-side service
206+
```
207+
208+
## Adding a New Feature
209+
210+
1. **Create the service** in `src/main/services/`
211+
2. **Add DI token** in `src/main/di/tokens.ts`
212+
3. **Register service** in `src/main/di/container.ts`
213+
4. **Create tRPC router** in `src/main/trpc/routers/`
214+
5. **Add router** to `src/main/trpc/router.ts`
215+
6. **Use in renderer** via `trpcReact` hooks
216+
217+
## Code Style
218+
219+
See [CLAUDE.md](./CLAUDE.md) for linting, formatting, and import conventions.
220+
221+
Key points:
222+
- Use path aliases (`@main/*`, `@renderer/*`, etc.)
223+
- No barrel files - import directly from source
224+
- Use `logger` instead of `console.*`

CLAUDE.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,16 @@ Import directly from source files instead.
5454

5555
## Architecture
5656

57+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed patterns (DI, services, tRPC, state management).
58+
5759
### Electron App (apps/array)
5860

59-
- Main process: `src/main/` - Node.js, IPC handlers, services
60-
- Renderer process: `src/renderer/` - React app
61-
- Preload script: `src/main/preload.ts`
62-
- IPC bridge pattern between main/renderer
63-
- State management: Zustand stores in `src/renderer/stores/`
64-
- Testing: Vitest with React Testing Library
61+
- **Main process** (`src/main/`) - Stateless services, tRPC routers, system I/O
62+
- **Renderer process** (`src/renderer/`) - React app, all application state
63+
- **IPC**: tRPC over Electron IPC (type-safe)
64+
- **DI**: InversifyJS in both processes (`src/main/di/`, `src/renderer/di/`)
65+
- **State**: Zustand stores in renderer only - main is stateless
66+
- **Testing**: Vitest with React Testing Library
6567

6668
### Agent Package (packages/agent)
6769

0 commit comments

Comments
 (0)