# Forte CLI Implementation Plan
## Overview
Forte is a fullstack framework integrating a Rust backend + TypeScript/React frontend.
This document outlines the implementation plan for the Forte CLI tool.
## Current State
### Existing Components
| `forte/` | SSR server orchestration | PoC complete |
| `forte-manual/` | Manually generated example app | PoC complete |
| `forte-rs-to-ts/` | Rust → TypeScript type generation | Complete |
| `forte-json/` | Streaming JSON serialization | Complete |
| `fn0/` | WASM/JS execution engine | Complete |
| `ski/` | JavaScript runtime (Deno core) | Complete |
### Code Generation Responsibilities
| Backend routes (`route_generated.rs`) | `build.rs` | `cargo build` |
| Props types (`.props.ts`) | CLI → calls `forte-rs-to-ts` | `forte dev` |
| Frontend router (`routes.generated.ts`) | CLI | `forte dev` |
| Action types + client (`forte:actions`) | CLI | `forte dev` |
- Backend developer: Can work with just `cargo build`
- Frontend developer: Use `forte dev` to sync types + router
---
## CLI Command Structure
```
forte init <project-name> # Create new project
forte add page <path> # Add page
forte add action <name> # Add action
forte dev # Dev server (watch + codegen)
forte build # Production build
```
---
## Step 1: Project Structure Refactoring
Currently, SSR server logic is in `forte/src/main.rs`.
Separate CLI and server.
### Post-refactoring Structure
```
forte/
├── Cargo.toml
├── src/
│ ├── main.rs # CLI entry point
│ ├── cli/
│ │ ├── mod.rs
│ │ ├── init.rs # forte init
│ │ ├── add.rs # forte add
│ │ ├── dev.rs # forte dev
│ │ └── build.rs # forte build
│ ├── codegen/
│ │ ├── mod.rs
│ │ ├── backend_routes.rs # route_generated.rs generation
│ │ └── frontend_routes.rs # Frontend router generation
│ ├── server/
│ │ └── mod.rs # SSR server logic from current main.rs
│ ├── watcher.rs # File change detection
│ └── templates/ # Embedded templates
└── templates/ # Project/page template files
```
---
## Step 2: `forte dev` Implementation
Dev server + code generation + watch mode
### Execution Flow
```
forte dev
│
├─1. Initial code generation
│ ├─ Run forte-rs-to-ts → generate .props.ts
│ ├─ Generate backend routes → route_generated.rs
│ └─ Generate frontend routes → routes.generated.ts
│
├─2. Initial build
│ ├─ cargo build --target wasm32-wasip2 (backend)
│ └─ npm run build (frontend)
│
├─3. Start server
│ └─ http://localhost:3000
│
└─4. Watch mode
├─ rs/src/pages/** change → backend rebuild + codegen
└─ fe/src/pages/** change → frontend rebuild
```
### Code Generation Details
#### 2-1. Props Type Generation (using existing tool)
Call `forte-rs-to-ts`:
```rust
Command::new("forte-rs-to-ts")
.arg(&project_rs_dir)
.status()
```
#### 2-2. Backend Route Generation
Move current `rs/build.rs` logic to CLI.
`build.rs` simply calls CLI, or CLI generates directly.
Generated file: `rs/src/route_generated.rs`
#### 2-3. Frontend Router Generation
Auto-generate the hardcoded routing in current `fe/src/server.ts`.
Generated file: `fe/src/routes.generated.ts`
```typescript
// Auto-generated by forte CLI
import type { ComponentType } from 'react';
export interface Route {
pattern: RegExp;
params: string[];
load: () => Promise<{ default: ComponentType<any> }>;
}
export const routes: Route[] = [
{
pattern: /^\/$/,
params: [],
load: () => import('./pages/index/page'),
},
{
pattern: /^\/product\/([^/]+)$/,
params: ['id'],
load: () => import('./pages/product/[id]/page'),
},
];
```
`server.ts` imports this for routing:
```typescript
import { routes } from './routes.generated';
export async function handler(request: Request): Promise<Response> {
const url = new URL(request.url);
for (const route of routes) {
const match = url.pathname.match(route.pattern);
if (match) {
const params = Object.fromEntries(
route.params.map((name, i) => [name, match[i + 1]])
);
const { default: Page } = await route.load();
// ... render
}
}
}
```
---
## Step 3: `forte init` Implementation
### Command
```bash
forte init my-app
cd my-app
```
### Generated Structure
```
my-app/
├── Forte.toml
├── rs/
│ ├── Cargo.toml
│ ├── .cargo/config.toml
│ └── src/
│ ├── lib.rs
│ └── pages/
│ └── index/
│ └── mod.rs
└── fe/
├── package.json
├── tsconfig.json
├── rolldown.config.ts
└── src/
├── server.ts
└── pages/
└── index/
└── page.tsx
```
### Embedded Templates
Embed template files with `include_str!` or `rust-embed`:
```rust
static CARGO_TOML_TEMPLATE: &str = include_str!("../templates/rs/Cargo.toml.tmpl");
static PAGE_MOD_TEMPLATE: &str = include_str!("../templates/rs/page.mod.rs.tmpl");
// ...
```
### Variable Substitution
Simple template variable substitution:
- `{{project_name}}` → project name
- `{{page_name}}` → page name
---
## Step 4: `forte add page` Implementation
### Command
```bash
forte add page product/[id]
forte add page user/settings
```
### Behavior
1. Parse path
- `product/[id]` → static: `product`, dynamic: `id`
2. Generate backend file
- `rs/src/pages/product/[id]/mod.rs`
3. Generate frontend file
- `fe/src/pages/product/[id]/page.tsx`
4. Regenerate code
- Update `route_generated.rs`
- Update `routes.generated.ts`
- Generate `.props.ts`
### Templates
**rs/src/pages/{path}/mod.rs:**
```rust
use serde::Serialize;
#[derive(Serialize)]
pub enum Props {
Ok {
// TODO: Add your props here
},
}
// Infallible - errors are handled via Props variants
pub async fn handler(
_headers: std::collections::HashMap<String, String>,
{{#params}}
{{name}}: {{type}},
{{/params}}
) -> Props {
Props::Ok {}
}
```
**fe/src/pages/{path}/page.tsx:**
```tsx
import type { Props } from './.props';
export default function Page(props: Props) {
if (props.t !== 'Ok') {
return <div>Error</div>;
}
return (
<div>
<h1>{{page_name}}</h1>
{/* TODO: Implement your page */}
</div>
);
}
```
---
## Step 5: `forte build` Implementation
### Command
```bash
forte build
```
### Output
```
dist/
├── backend.wasm # or backend.cwasm (pre-compiled)
├── frontend/
│ └── server.js # SSR bundle
└── static/ # Static assets
```
### Build Steps
1. **Code generation** (same as dev)
2. **Backend build**
```bash
cargo build --release --target wasm32-wasip2
```
3. **CWASM pre-compilation** (optional)
```bash
wasmtime compile backend.wasm -o backend.cwasm
```
4. **Frontend build**
```bash
npm run build
```
5. **Copy static assets**
- `fe/public/` → `dist/static/`
- Asset hashing (optional)
---
## Step 6: Watch Mode + Hot Swap
### Dependencies
```toml
notify = "6" # File system watching
```
### Watch Targets
| `rs/src/pages/**/*.rs` | Props codegen + backend rebuild + WASM hot swap |
| `rs/src/actions/**/*.rs` | Action codegen + backend rebuild + WASM hot swap |
| `fe/src/pages/**/*.tsx` | Frontend rebuild + JS hot swap |
| `fe/public/**` | Static asset change notification |
| `Forte.toml` | Full restart |
### Hot Swap (no server restart)
**Backend WASM:**
```
File change → cargo build → fn0 cache invalidation → new WASM loaded on next request
```
**SSR JS:**
```
File change → npm run build → fn0 cache invalidation → new JS loaded on next request
```
**Client (Phase 1: LiveReload):**
```
File change → reload signal to browser
```
### Debouncing
Delay rebuild on rapid consecutive changes:
```rust
let debounce_duration = Duration::from_millis(100);
```
---
## E2E Test Strategy
### Principles
- **All commands verified through E2E tests**
- No manual testing, automate with code
- Use project created by `forte init` for testing other commands
### Test Structure
```
forte/
├── tests/
│ └── e2e/
│ ├── mod.rs
│ ├── init_test.rs # forte init tests
│ ├── dev_test.rs # forte dev tests
│ ├── add_test.rs # forte add tests
│ └── build_test.rs # forte build tests
```
### Test Dependencies
```toml
[dev-dependencies]
assert_cmd = "2" # CLI execution and verification
predicates = "3" # Output verification
tempfile = "3" # Temporary directories
reqwest = { version = "0.12", features = ["blocking"] } # HTTP requests
```
### Test Scenarios
**`forte init` test:**
```rust
#[test]
fn test_init_creates_project_structure() {
let temp = tempfile::tempdir().unwrap();
Command::cargo_bin("forte").unwrap()
.args(["init", "my-app"])
.current_dir(&temp)
.assert()
.success();
// Verify directory structure
assert!(temp.path().join("my-app/Forte.toml").exists());
assert!(temp.path().join("my-app/rs/Cargo.toml").exists());
assert!(temp.path().join("my-app/fe/package.json").exists());
}
```
**`forte dev` test:**
```rust
#[tokio::test]
async fn test_dev_server_responds() {
let temp = tempfile::tempdir().unwrap();
// 1. Create project with forte init
Command::cargo_bin("forte").unwrap()
.args(["init", "test-app"])
.current_dir(&temp)
.assert()
.success();
// 2. Run forte dev in background
let mut child = Command::cargo_bin("forte").unwrap()
.args(["dev"])
.current_dir(temp.path().join("test-app"))
.spawn()
.unwrap();
// 3. Wait for server start then make HTTP request
tokio::time::sleep(Duration::from_secs(10)).await;
let resp = reqwest::get("http://127.0.0.1:3000").await.unwrap();
assert!(resp.status().is_success());
// 4. Cleanup
child.kill().unwrap();
}
```
---
## Implementation Order
### Principles
- **Write test code first, then implement**
- Each step is considered complete when E2E tests pass
### Phase 1 (MVP)
| 1 | Project structure refactoring ✅ | - |
| 2 | `forte init` | Directory structure, required file generation |
| 3 | `forte dev` E2E test | init → dev → HTTP 200 response |
| 4 | Frontend router auto-generation | routes.generated.ts file verification |
| 5 | `forte add page` | Page file generation, codegen execution |
| 6 | Watch mode + hot swap | File change → rebuild → response change |
| 7 | `forte add action` | Action file generation, type generation |
| 8 | Static asset serving | /static/* path response |
| 9 | `forte build` | dist/ directory generation, file verification |
### Phase 2 (Future)
| Client HMR | WebSocket connection, module replacement |
| Production asset hashing | Hash-included filenames, manifest |
| Hydration support | Client JS execution verification |
---
## Dependencies
### CLI
```toml
[dependencies]
clap = { version = "4", features = ["derive"] }
notify = "6"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
toml = "0.8"
```
### Existing Tools
- `forte-rs-to-ts`: Props type generation (called as external binary)
- `fn0`: WASM/JS execution (library dependency)
- `forte-json`: JSON serialization (used by backend)
---
## Forte.toml Configuration
```toml
[project]
name = "my-app"
[dev]
port = 3000
[build]
# Whether to pre-compile CWASM
precompile_wasm = false
[paths]
backend = "rs"
frontend = "fe"
```
---
## Decisions Made
### 1. Code Generation Responsibilities
| Backend routes (`route_generated.rs`) | `build.rs` (kept) |
| Props/Action types, frontend router | CLI |
- Backend developer: Can work independently with just `cargo build`
- CLI calls `forte-rs-to-ts` for type generation
### 2. Hot Swap + HMR
**Phase 1 Implementation:**
- Backend WASM hot swap (no server restart)
- SSR JS hot swap (no server restart)
- Client: LiveReload (browser refresh)
**TODO (future must-have):**
- Client HMR (React Fast Refresh)
- Vite integration vs custom implementation → decide later
### 3. Action Implementation Approach
**Benchmarked against Astro Actions style**
Backend structure:
```
rs/src/
├── pages/
│ └── index/mod.rs
└── actions/
└── user/
└── login.rs # → actions.user.login()
```
**actions/user/login.rs:**
```rust
use serde::{Serialize, Deserialize};
#[derive(Deserialize)]
pub struct Params {
pub email: String,
pub password: String,
}
#[derive(Serialize)]
pub enum Response {
Ok { token: String },
Error { message: String },
}
// Infallible - errors are handled via Response variants
pub async fn handler(request: Request) -> Response {
Response::Ok { token: "...".to_string() }
}
```
**Frontend usage:**
```typescript
import { actions } from 'forte:actions';
const result = await actions.user.login({ email, password });
if (result.t === 'Ok') {
console.log(result.v.token);
}
```
- Nested path support: `actions/user/login.rs` → `actions.user.login()`
- `forte:actions` import approach: Astro style (details to be decided during implementation)
### 4. Static Asset Handling
- Directory: `fe/public/`
- Development: `forte dev` serves directly
- Production build:
- Asset hashing applied (cache invalidation)
- CDN path support required