kilatjs 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +481 -260
- package/dist/adapters/react.d.ts +6 -1
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +1335 -0
- package/dist/core/router.d.ts +10 -7
- package/dist/core/types.d.ts +4 -1
- package/dist/core/vite-router.d.ts +34 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +425 -199
- package/dist/server/dev-server.d.ts +0 -0
- package/dist/server/live-reload.d.ts +27 -0
- package/dist/server/server.d.ts +11 -1
- package/dist/server/vite-dev.d.ts +38 -0
- package/dist/server/vite-server.d.ts +17 -0
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -14,425 +14,646 @@
|
|
|
14
14
|
|
|
15
15
|
KilatJS is a server-first web framework inspired by the simplicity of PHP and the power of modern component libraries. It rejects the complexity of SPAs, hydration, and client-side routing in favor of **real HTML rendered on the server**.
|
|
16
16
|
|
|
17
|
-
**🔥 Powered by Bun's Native APIs - No Abstractions, Pure Performance:**
|
|
18
|
-
- 🚀 **Bun.FileSystemRouter** - Zero-config file-based routing at native speed
|
|
19
|
-
- ⚡ **Bun.serve()** - HTTP/2 server with built-in WebSocket support
|
|
20
|
-
- 🔧 **Bun.file()** - Streaming file operations with automatic MIME detection
|
|
21
|
-
- 📦 **Bun.build()** - Native TypeScript/JSX bundling (no Webpack/Vite needed)
|
|
22
|
-
- 🔄 **Bun.spawn()** - Process management for build tools and external commands
|
|
23
|
-
- 💾 **Bun.write()** - Optimized file writing with atomic operations
|
|
24
|
-
- 🔍 **Bun.glob()** - Pattern matching for static site generation
|
|
25
|
-
|
|
26
17
|
### One-Line Positioning
|
|
27
18
|
|
|
28
19
|
> **"Request → Load → Render HTML → Response. That's it."**
|
|
29
20
|
|
|
30
21
|
---
|
|
31
22
|
|
|
32
|
-
##
|
|
23
|
+
## � *Quick Start
|
|
33
24
|
|
|
34
|
-
|
|
25
|
+
### Installation
|
|
35
26
|
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
dir: "./routes"
|
|
41
|
-
});
|
|
27
|
+
```bash
|
|
28
|
+
# Create a new project
|
|
29
|
+
mkdir my-app && cd my-app
|
|
30
|
+
bun init
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
port: 3000,
|
|
46
|
-
fetch: async (request) => {
|
|
47
|
-
// Zero-copy request handling
|
|
48
|
-
return new Response(Bun.file("./static/index.html"));
|
|
49
|
-
}
|
|
50
|
-
};
|
|
32
|
+
# Install KilatJS
|
|
33
|
+
bun add kilatjs react react-dom
|
|
51
34
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
entrypoints: ["./src/index.tsx"],
|
|
55
|
-
outdir: "./dist",
|
|
56
|
-
target: "bun"
|
|
57
|
-
});
|
|
35
|
+
# Install dev dependencies
|
|
36
|
+
bun add -d @types/react @types/react-dom typescript tailwindcss
|
|
58
37
|
```
|
|
59
38
|
|
|
60
|
-
|
|
61
|
-
- ⚡ **3x faster** cold starts vs Node.js frameworks
|
|
62
|
-
- 🚀 **Native TypeScript** - no transpilation overhead
|
|
63
|
-
- 💾 **Zero-copy streaming** for static files
|
|
64
|
-
- 🔧 **Built-in bundling** eliminates build tool complexity
|
|
39
|
+
### Create Your First App
|
|
65
40
|
|
|
66
|
-
|
|
41
|
+
**1. Create config file `kilat.config.ts`:**
|
|
67
42
|
|
|
68
|
-
|
|
43
|
+
```ts
|
|
44
|
+
import { defineConfig } from "kilatjs";
|
|
69
45
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
46
|
+
export default defineConfig({
|
|
47
|
+
appDir: "./src",
|
|
48
|
+
outDir: "./dist",
|
|
49
|
+
port: 3000,
|
|
50
|
+
tailwind: {
|
|
51
|
+
enabled: true,
|
|
52
|
+
inputPath: "./input.css",
|
|
53
|
+
cssPath: "./styles.css",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
75
57
|
|
|
76
|
-
|
|
58
|
+
**2. Create input CSS `input.css`:**
|
|
77
59
|
|
|
78
|
-
|
|
60
|
+
```css
|
|
61
|
+
@import "tailwindcss";
|
|
62
|
+
```
|
|
79
63
|
|
|
80
|
-
|
|
64
|
+
**3. Create your first route `src/routes/index.tsx`:**
|
|
81
65
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- ❌ Dependency injection
|
|
88
|
-
- ❌ SPA build assumptions
|
|
89
|
-
- ❌ Vite as a hard dependency
|
|
66
|
+
```tsx
|
|
67
|
+
export const meta = {
|
|
68
|
+
title: "Welcome to KilatJS",
|
|
69
|
+
description: "A Bun-native, HTML-first framework",
|
|
70
|
+
};
|
|
90
71
|
|
|
91
|
-
|
|
72
|
+
export async function load() {
|
|
73
|
+
return {
|
|
74
|
+
message: "Hello from the server!",
|
|
75
|
+
time: new Date().toLocaleTimeString(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
92
78
|
|
|
93
|
-
|
|
79
|
+
export default function HomePage({ data }) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
|
82
|
+
<div className="bg-white p-8 rounded-lg shadow-lg">
|
|
83
|
+
<h1 className="text-3xl font-bold text-gray-900">{data.message}</h1>
|
|
84
|
+
<p className="text-gray-600 mt-2">Server time: {data.time}</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
94
90
|
|
|
95
|
-
|
|
91
|
+
**4. Run the dev server:**
|
|
96
92
|
|
|
97
93
|
```bash
|
|
98
|
-
|
|
94
|
+
bunx kilat dev
|
|
99
95
|
```
|
|
100
96
|
|
|
101
|
-
|
|
97
|
+
Visit [http://localhost:3000](http://localhost:3000) 🎉
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 📖 CLI Commands
|
|
102
|
+
|
|
103
|
+
| Command | Description |
|
|
104
|
+
|---------|-------------|
|
|
105
|
+
| `kilat dev` | Start development server with HMR + Live Reload |
|
|
106
|
+
| `kilat build` | Build for production |
|
|
107
|
+
| `kilat serve` | Run production server (`bun dist/server.js`) |
|
|
108
|
+
| `kilat preview` | Preview static files |
|
|
109
|
+
|
|
110
|
+
### Development Output
|
|
102
111
|
|
|
103
|
-
```
|
|
104
|
-
|
|
112
|
+
```
|
|
113
|
+
⚡ KilatJS Dev Server
|
|
114
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
115
|
+
➜ http://localhost:3000
|
|
116
|
+
➜ HMR + Live Reload enabled
|
|
117
|
+
➜ Edit files and see changes instantly
|
|
118
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
105
119
|
```
|
|
106
120
|
|
|
107
|
-
|
|
121
|
+
### Build Output
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
🔨 KilatJS Production Build
|
|
125
|
+
|
|
126
|
+
🎨 Building Tailwind CSS...
|
|
127
|
+
|
|
128
|
+
📄 Routes Analysis:
|
|
129
|
+
─────────────────────────────────────────────────────────
|
|
130
|
+
📄 / SSR index.tsx
|
|
131
|
+
📄 /about SSR about.tsx
|
|
132
|
+
⚡ /api/posts API api/posts.ts
|
|
133
|
+
🔄 /blog/[slug] Dynamic SSR blog/[slug].tsx
|
|
134
|
+
─────────────────────────────────────────────────────────
|
|
135
|
+
Total: 15 routes (10 SSR, 3 Dynamic, 2 API)
|
|
136
|
+
|
|
137
|
+
✅ Build Complete!
|
|
138
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
139
|
+
Output: ./dist
|
|
140
|
+
Start: bun ./dist/server.js
|
|
141
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
142
|
+
```
|
|
108
143
|
|
|
109
144
|
---
|
|
110
145
|
|
|
111
146
|
## 📁 Project Structure
|
|
112
147
|
|
|
113
148
|
```
|
|
114
|
-
|
|
149
|
+
my-app/
|
|
150
|
+
├── kilat.config.ts # Configuration
|
|
151
|
+
├── input.css # Tailwind input
|
|
152
|
+
├── styles.css # Generated CSS
|
|
115
153
|
├── src/
|
|
116
|
-
│ ├──
|
|
117
|
-
│
|
|
118
|
-
│
|
|
119
|
-
│
|
|
120
|
-
│
|
|
121
|
-
│
|
|
122
|
-
│
|
|
123
|
-
│ └──
|
|
124
|
-
|
|
125
|
-
│
|
|
126
|
-
|
|
127
|
-
│ ├── styles.css # CSS styles
|
|
128
|
-
│ ├── components/
|
|
129
|
-
│ │ └── Layout.tsx # Shared layout
|
|
130
|
-
│ └── routes/
|
|
131
|
-
│ ├── index.tsx # Home page
|
|
132
|
-
│ ├── about.tsx # About page
|
|
133
|
-
│ ├── contact.tsx # Contact form
|
|
134
|
-
│ ├── contact/
|
|
135
|
-
│ │ └── success.tsx
|
|
136
|
-
│ └── blog/
|
|
137
|
-
│ ├── index.tsx # Blog listing
|
|
138
|
-
│ └── [slug].tsx # Dynamic blog posts
|
|
139
|
-
└── README.md
|
|
154
|
+
│ ├── components/ # Shared components
|
|
155
|
+
│ │ └── Layout.tsx
|
|
156
|
+
│ └── routes/ # File-based routing
|
|
157
|
+
│ ├── index.tsx # → /
|
|
158
|
+
│ ├── about.tsx # → /about
|
|
159
|
+
│ ├── blog/
|
|
160
|
+
│ │ ├── index.tsx # → /blog
|
|
161
|
+
│ │ └── [slug].tsx # → /blog/:slug
|
|
162
|
+
│ └── api/
|
|
163
|
+
│ └── posts.ts # → /api/posts
|
|
164
|
+
└── dist/ # Production build
|
|
140
165
|
```
|
|
141
166
|
|
|
142
167
|
---
|
|
143
168
|
|
|
144
169
|
## 📄 Route Contract
|
|
145
170
|
|
|
146
|
-
Each route file
|
|
171
|
+
Each route file can export:
|
|
147
172
|
|
|
148
173
|
```tsx
|
|
149
|
-
// Rendering mode: "static" (build-time) or "ssr" (request-time)
|
|
150
|
-
export const mode = "ssr";
|
|
151
|
-
|
|
152
|
-
// UI framework: "react" | "solid" | "vue" (default: react)
|
|
153
|
-
export const ui = "react";
|
|
154
|
-
|
|
155
174
|
// SEO meta tags
|
|
156
175
|
export const meta = {
|
|
157
176
|
title: "Page Title",
|
|
158
|
-
description: "Page description
|
|
177
|
+
description: "Page description",
|
|
159
178
|
robots: "index,follow",
|
|
160
179
|
ogTitle: "Open Graph Title",
|
|
161
|
-
ogDescription: "
|
|
162
|
-
ogImage: "https://example.com/image.jpg"
|
|
180
|
+
ogDescription: "OG description",
|
|
181
|
+
ogImage: "https://example.com/image.jpg",
|
|
163
182
|
};
|
|
164
183
|
|
|
165
|
-
//
|
|
166
|
-
export function getStaticPaths() {
|
|
167
|
-
return ["/blog/post-1", "/blog/post-2"];
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Server-only data loading
|
|
184
|
+
// Server-side data loading
|
|
171
185
|
export async function load(ctx) {
|
|
172
|
-
const data = await
|
|
173
|
-
return {
|
|
186
|
+
const data = await fetchData();
|
|
187
|
+
return { items: data };
|
|
174
188
|
}
|
|
175
189
|
|
|
176
|
-
// HTTP
|
|
190
|
+
// HTTP method handlers (POST, PUT, DELETE, etc.)
|
|
177
191
|
export async function POST(ctx) {
|
|
178
192
|
const formData = await ctx.request.formData();
|
|
179
193
|
// Process form...
|
|
180
194
|
return Response.redirect("/success", 302);
|
|
181
195
|
}
|
|
182
196
|
|
|
183
|
-
//
|
|
197
|
+
// Page component (receives data from load())
|
|
184
198
|
export default function Page({ data, params, state }) {
|
|
185
|
-
return <div>{data.
|
|
199
|
+
return <div>{data.items.map(item => <p>{item.name}</p>)}</div>;
|
|
186
200
|
}
|
|
187
201
|
```
|
|
188
202
|
|
|
189
203
|
---
|
|
190
204
|
|
|
191
|
-
##
|
|
205
|
+
## 🔧 Tutorial: Building a Blog
|
|
192
206
|
|
|
193
|
-
|
|
207
|
+
### Step 1: Create Layout Component
|
|
208
|
+
|
|
209
|
+
**`src/components/Layout.tsx`:**
|
|
194
210
|
|
|
195
211
|
```tsx
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const email = formData.get("email");
|
|
200
|
-
|
|
201
|
-
await saveToDatabase({ email });
|
|
202
|
-
|
|
203
|
-
// PRG: Post-Redirect-Get pattern
|
|
204
|
-
return Response.redirect("/contact/success", 302);
|
|
212
|
+
interface LayoutProps {
|
|
213
|
+
children: React.ReactNode;
|
|
214
|
+
title?: string;
|
|
205
215
|
}
|
|
206
216
|
|
|
207
|
-
export
|
|
217
|
+
export function Layout({ children, title = "My Blog" }: LayoutProps) {
|
|
208
218
|
return (
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
219
|
+
<div className="min-h-screen bg-gray-50">
|
|
220
|
+
<header className="bg-white shadow">
|
|
221
|
+
<nav className="max-w-4xl mx-auto px-4 py-4">
|
|
222
|
+
<a href="/" className="text-xl font-bold">My Blog</a>
|
|
223
|
+
<div className="float-right space-x-4">
|
|
224
|
+
<a href="/" className="text-gray-600 hover:text-gray-900">Home</a>
|
|
225
|
+
<a href="/blog" className="text-gray-600 hover:text-gray-900">Blog</a>
|
|
226
|
+
<a href="/about" className="text-gray-600 hover:text-gray-900">About</a>
|
|
227
|
+
</div>
|
|
228
|
+
</nav>
|
|
229
|
+
</header>
|
|
230
|
+
<main className="max-w-4xl mx-auto px-4 py-8">
|
|
231
|
+
{children}
|
|
232
|
+
</main>
|
|
233
|
+
</div>
|
|
213
234
|
);
|
|
214
235
|
}
|
|
215
236
|
```
|
|
216
237
|
|
|
217
|
-
|
|
238
|
+
### Step 2: Create Blog List Page
|
|
218
239
|
|
|
219
|
-
|
|
240
|
+
**`src/routes/blog/index.tsx`:**
|
|
220
241
|
|
|
221
|
-
|
|
242
|
+
```tsx
|
|
243
|
+
import { Layout } from "../../components/Layout";
|
|
222
244
|
|
|
223
|
-
|
|
245
|
+
export const meta = {
|
|
246
|
+
title: "Blog - My Site",
|
|
247
|
+
description: "Read our latest articles",
|
|
248
|
+
};
|
|
224
249
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
250
|
+
export async function load() {
|
|
251
|
+
// In real app, fetch from database
|
|
252
|
+
const posts = [
|
|
253
|
+
{ id: 1, slug: "hello-world", title: "Hello World", excerpt: "My first post..." },
|
|
254
|
+
{ id: 2, slug: "getting-started", title: "Getting Started", excerpt: "Learn how to..." },
|
|
255
|
+
];
|
|
256
|
+
return { posts };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export default function BlogPage({ data }) {
|
|
260
|
+
return (
|
|
261
|
+
<Layout>
|
|
262
|
+
<h1 className="text-3xl font-bold mb-8">Blog</h1>
|
|
263
|
+
<div className="space-y-6">
|
|
264
|
+
{data.posts.map(post => (
|
|
265
|
+
<article key={post.id} className="bg-white p-6 rounded-lg shadow">
|
|
266
|
+
<h2 className="text-xl font-semibold">
|
|
267
|
+
<a href={`/blog/${post.slug}`} className="hover:text-blue-600">
|
|
268
|
+
{post.title}
|
|
269
|
+
</a>
|
|
270
|
+
</h2>
|
|
271
|
+
<p className="text-gray-600 mt-2">{post.excerpt}</p>
|
|
272
|
+
</article>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
</Layout>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
228
278
|
```
|
|
229
279
|
|
|
230
|
-
|
|
280
|
+
### Step 3: Create Dynamic Blog Post Page
|
|
231
281
|
|
|
232
|
-
|
|
233
|
-
import { createKilat } from 'kilatjs';
|
|
282
|
+
**`src/routes/blog/[slug].tsx`:**
|
|
234
283
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
284
|
+
```tsx
|
|
285
|
+
import { Layout } from "../../components/Layout";
|
|
286
|
+
|
|
287
|
+
export const meta = {
|
|
288
|
+
title: "Blog Post",
|
|
289
|
+
description: "Read this article",
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export async function load({ params }) {
|
|
293
|
+
// params.slug contains the URL parameter
|
|
294
|
+
const post = await getPostBySlug(params.slug);
|
|
295
|
+
|
|
296
|
+
if (!post) {
|
|
297
|
+
throw new Response("Not Found", { status: 404 });
|
|
240
298
|
}
|
|
241
|
-
|
|
299
|
+
|
|
300
|
+
return { post };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function getPostBySlug(slug: string) {
|
|
304
|
+
// In real app, fetch from database
|
|
305
|
+
const posts = {
|
|
306
|
+
"hello-world": { title: "Hello World", content: "This is my first post!" },
|
|
307
|
+
"getting-started": { title: "Getting Started", content: "Let me show you how..." },
|
|
308
|
+
};
|
|
309
|
+
return posts[slug] || null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export default function BlogPostPage({ data, params }) {
|
|
313
|
+
return (
|
|
314
|
+
<Layout>
|
|
315
|
+
<article>
|
|
316
|
+
<h1 className="text-4xl font-bold mb-4">{data.post.title}</h1>
|
|
317
|
+
<div className="prose max-w-none">
|
|
318
|
+
{data.post.content}
|
|
319
|
+
</div>
|
|
320
|
+
</article>
|
|
321
|
+
<a href="/blog" className="text-blue-600 mt-8 inline-block">← Back to Blog</a>
|
|
322
|
+
</Layout>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
242
325
|
```
|
|
243
326
|
|
|
244
|
-
|
|
327
|
+
### Step 4: Create API Route
|
|
245
328
|
|
|
246
|
-
|
|
329
|
+
**`src/routes/api/posts.ts`:**
|
|
247
330
|
|
|
248
|
-
|
|
331
|
+
```ts
|
|
332
|
+
import { RouteContext } from "kilatjs";
|
|
249
333
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
334
|
+
const posts = [
|
|
335
|
+
{ id: 1, title: "Hello World", slug: "hello-world" },
|
|
336
|
+
{ id: 2, title: "Getting Started", slug: "getting-started" },
|
|
337
|
+
];
|
|
253
338
|
|
|
254
|
-
|
|
255
|
-
export
|
|
339
|
+
// GET /api/posts
|
|
340
|
+
export async function GET(ctx: RouteContext) {
|
|
341
|
+
const search = ctx.query.get("search")?.toLowerCase();
|
|
342
|
+
|
|
343
|
+
let results = posts;
|
|
344
|
+
if (search) {
|
|
345
|
+
results = posts.filter(p => p.title.toLowerCase().includes(search));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return new Response(JSON.stringify({ data: results }), {
|
|
349
|
+
headers: { "Content-Type": "application/json" },
|
|
350
|
+
});
|
|
351
|
+
}
|
|
256
352
|
|
|
257
|
-
|
|
258
|
-
|
|
353
|
+
// POST /api/posts
|
|
354
|
+
export async function POST(ctx: RouteContext) {
|
|
355
|
+
const body = await ctx.request.json();
|
|
356
|
+
|
|
357
|
+
const newPost = {
|
|
358
|
+
id: posts.length + 1,
|
|
359
|
+
title: body.title,
|
|
360
|
+
slug: body.title.toLowerCase().replace(/\s+/g, "-"),
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
posts.push(newPost);
|
|
364
|
+
|
|
365
|
+
return new Response(JSON.stringify({ data: newPost }), {
|
|
366
|
+
status: 201,
|
|
367
|
+
headers: { "Content-Type": "application/json" },
|
|
368
|
+
});
|
|
259
369
|
}
|
|
260
370
|
```
|
|
261
371
|
|
|
262
|
-
###
|
|
372
|
+
### Step 5: Create Contact Form with PRG Pattern
|
|
263
373
|
|
|
264
|
-
|
|
265
|
-
- PHP-style execution
|
|
266
|
-
- Infinite routes allowed
|
|
374
|
+
**`src/routes/contact.tsx`:**
|
|
267
375
|
|
|
268
376
|
```tsx
|
|
269
|
-
|
|
377
|
+
import { Layout } from "../components/Layout";
|
|
378
|
+
|
|
379
|
+
export const meta = {
|
|
380
|
+
title: "Contact Us",
|
|
381
|
+
description: "Get in touch with us",
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// Handle form submission
|
|
385
|
+
export async function POST({ request }) {
|
|
386
|
+
const formData = await request.formData();
|
|
387
|
+
const name = formData.get("name");
|
|
388
|
+
const email = formData.get("email");
|
|
389
|
+
const message = formData.get("message");
|
|
390
|
+
|
|
391
|
+
// Save to database, send email, etc.
|
|
392
|
+
console.log("Contact form:", { name, email, message });
|
|
393
|
+
|
|
394
|
+
// PRG: Post-Redirect-Get pattern
|
|
395
|
+
return Response.redirect("/contact/success", 302);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export default function ContactPage() {
|
|
399
|
+
return (
|
|
400
|
+
<Layout>
|
|
401
|
+
<h1 className="text-3xl font-bold mb-8">Contact Us</h1>
|
|
402
|
+
<form method="POST" className="max-w-md space-y-4">
|
|
403
|
+
<div>
|
|
404
|
+
<label className="block text-sm font-medium mb-1">Name</label>
|
|
405
|
+
<input
|
|
406
|
+
type="text"
|
|
407
|
+
name="name"
|
|
408
|
+
required
|
|
409
|
+
className="w-full px-3 py-2 border rounded-lg"
|
|
410
|
+
/>
|
|
411
|
+
</div>
|
|
412
|
+
<div>
|
|
413
|
+
<label className="block text-sm font-medium mb-1">Email</label>
|
|
414
|
+
<input
|
|
415
|
+
type="email"
|
|
416
|
+
name="email"
|
|
417
|
+
required
|
|
418
|
+
className="w-full px-3 py-2 border rounded-lg"
|
|
419
|
+
/>
|
|
420
|
+
</div>
|
|
421
|
+
<div>
|
|
422
|
+
<label className="block text-sm font-medium mb-1">Message</label>
|
|
423
|
+
<textarea
|
|
424
|
+
name="message"
|
|
425
|
+
rows={4}
|
|
426
|
+
required
|
|
427
|
+
className="w-full px-3 py-2 border rounded-lg"
|
|
428
|
+
/>
|
|
429
|
+
</div>
|
|
430
|
+
<button
|
|
431
|
+
type="submit"
|
|
432
|
+
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
|
|
433
|
+
>
|
|
434
|
+
Send Message
|
|
435
|
+
</button>
|
|
436
|
+
</form>
|
|
437
|
+
</Layout>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
270
440
|
```
|
|
271
441
|
|
|
272
|
-
|
|
442
|
+
**`src/routes/contact/success.tsx`:**
|
|
273
443
|
|
|
274
|
-
|
|
444
|
+
```tsx
|
|
445
|
+
import { Layout } from "../../components/Layout";
|
|
275
446
|
|
|
276
|
-
|
|
447
|
+
export const meta = {
|
|
448
|
+
title: "Message Sent",
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
export default function ContactSuccessPage() {
|
|
452
|
+
return (
|
|
453
|
+
<Layout>
|
|
454
|
+
<div className="text-center py-12">
|
|
455
|
+
<h1 className="text-3xl font-bold text-green-600 mb-4">✓ Message Sent!</h1>
|
|
456
|
+
<p className="text-gray-600 mb-8">Thank you for contacting us. We'll get back to you soon.</p>
|
|
457
|
+
<a href="/" className="text-blue-600 hover:underline">← Back to Home</a>
|
|
458
|
+
</div>
|
|
459
|
+
</Layout>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## 🔐 Authentication Example
|
|
277
467
|
|
|
278
468
|
```tsx
|
|
469
|
+
// src/routes/dashboard.tsx
|
|
279
470
|
export async function load({ request }) {
|
|
280
|
-
const
|
|
471
|
+
const cookie = request.headers.get("cookie");
|
|
472
|
+
const session = await getSession(cookie);
|
|
281
473
|
|
|
282
474
|
if (!session) {
|
|
283
|
-
|
|
475
|
+
// Redirect to login if not authenticated
|
|
476
|
+
throw Response.redirect("/login", 302);
|
|
284
477
|
}
|
|
285
478
|
|
|
286
479
|
return { user: session.user };
|
|
287
480
|
}
|
|
288
|
-
```
|
|
289
481
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
482
|
+
export default function DashboardPage({ data }) {
|
|
483
|
+
return (
|
|
484
|
+
<div>
|
|
485
|
+
<h1>Welcome, {data.user.name}!</h1>
|
|
486
|
+
<form method="POST" action="/logout">
|
|
487
|
+
<button type="submit">Logout</button>
|
|
488
|
+
</form>
|
|
489
|
+
</div>
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
```
|
|
293
493
|
|
|
294
494
|
---
|
|
295
495
|
|
|
296
|
-
##
|
|
297
|
-
|
|
298
|
-
### Development
|
|
496
|
+
## 🛠️ Configuration Reference
|
|
299
497
|
|
|
300
498
|
```ts
|
|
301
|
-
//
|
|
302
|
-
import {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
dev: true,
|
|
499
|
+
// kilat.config.ts
|
|
500
|
+
import { defineConfig } from "kilatjs";
|
|
501
|
+
|
|
502
|
+
export default defineConfig({
|
|
503
|
+
appDir: "./src", // Source directory (auto-detects routes/ or pages/)
|
|
504
|
+
outDir: "./dist", // Production build output
|
|
505
|
+
port: 3000, // Server port
|
|
506
|
+
hostname: "localhost", // Server hostname
|
|
507
|
+
publicDir: "./public", // Static assets directory
|
|
311
508
|
tailwind: {
|
|
312
|
-
enabled:
|
|
313
|
-
|
|
314
|
-
|
|
509
|
+
enabled: true, // Enable Tailwind CSS
|
|
510
|
+
inputPath: "./input.css",
|
|
511
|
+
cssPath: "./styles.css",
|
|
512
|
+
},
|
|
315
513
|
});
|
|
316
|
-
|
|
317
|
-
server.start();
|
|
318
514
|
```
|
|
319
515
|
|
|
320
|
-
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## 🔥 Why Bun Native?
|
|
321
519
|
|
|
322
|
-
|
|
323
|
-
import { buildStatic } from 'kilatjs';
|
|
520
|
+
KilatJS leverages Bun's runtime directly:
|
|
324
521
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
522
|
+
- ⚡ **Bun.FileSystemRouter** - Zero-config file-based routing
|
|
523
|
+
- 🚀 **Bun.serve()** - HTTP server with WebSocket support
|
|
524
|
+
- 📦 **Bun.build()** - Native TypeScript/JSX bundling
|
|
525
|
+
- 💾 **Bun.file()** - Streaming file operations
|
|
526
|
+
- 🔍 **Bun.Glob** - Pattern matching for routes
|
|
330
527
|
|
|
331
|
-
|
|
528
|
+
**Performance:**
|
|
529
|
+
- 3x faster cold starts vs Node.js
|
|
530
|
+
- Native TypeScript - no transpilation
|
|
531
|
+
- Zero-copy streaming for static files
|
|
332
532
|
|
|
333
|
-
|
|
533
|
+
---
|
|
334
534
|
|
|
335
|
-
|
|
336
|
-
|-----------|--------|-------|
|
|
337
|
-
| React | ✅ Default | Full SSR support |
|
|
338
|
-
| Solid | 🚧 Planned | Coming soon |
|
|
339
|
-
| Vue | 🚧 Planned | Coming soon |
|
|
535
|
+
## ✅ Core Principles
|
|
340
536
|
|
|
341
|
-
**
|
|
537
|
+
1. **HTML is the protocol** — Every response is complete, semantic HTML
|
|
538
|
+
2. **Server owns all truth** — No client-side state management
|
|
539
|
+
3. **Every change = HTTP request** — Forms, mutations, navigation
|
|
540
|
+
4. **UI frameworks render only** — React for templating, not orchestration
|
|
541
|
+
5. **JavaScript is optional** — Sites work without JS
|
|
342
542
|
|
|
343
543
|
---
|
|
344
544
|
|
|
345
|
-
##
|
|
545
|
+
## ⚡ Client-Side Interactivity
|
|
346
546
|
|
|
347
|
-
|
|
547
|
+
KilatJS is server-first, but you can add client-side interactivity using these patterns:
|
|
348
548
|
|
|
349
|
-
|
|
350
|
-
- HTML is always complete
|
|
351
|
-
- `<head>` is server-rendered
|
|
352
|
-
- No JS required for content
|
|
353
|
-
- Meta tags are first-class
|
|
549
|
+
### 1. `clientScript` Export (Recommended)
|
|
354
550
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
## 📜 Philosophy
|
|
551
|
+
Export a function that runs in the browser. TypeScript/LSP fully supports it!
|
|
358
552
|
|
|
359
|
-
|
|
553
|
+
```tsx
|
|
554
|
+
// src/routes/counter.tsx
|
|
360
555
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
556
|
+
// Client-side script - auto-injected, LSP checks the code!
|
|
557
|
+
export function clientScript() {
|
|
558
|
+
let count = 0;
|
|
559
|
+
const countEl = document.getElementById("count")!;
|
|
560
|
+
|
|
561
|
+
document.getElementById("decrement")!.onclick = () => {
|
|
562
|
+
countEl.textContent = String(--count);
|
|
563
|
+
};
|
|
564
|
+
document.getElementById("increment")!.onclick = () => {
|
|
565
|
+
countEl.textContent = String(++count);
|
|
566
|
+
};
|
|
567
|
+
}
|
|
368
568
|
|
|
369
|
-
|
|
569
|
+
export default function CounterPage() {
|
|
570
|
+
return (
|
|
571
|
+
<div>
|
|
572
|
+
<button id="decrement">-</button>
|
|
573
|
+
<span id="count">0</span>
|
|
574
|
+
<button id="increment">+</button>
|
|
575
|
+
</div>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
```
|
|
370
579
|
|
|
371
|
-
|
|
580
|
+
The framework automatically injects the script - no manual `<script>` tags needed!
|
|
372
581
|
|
|
373
|
-
###
|
|
582
|
+
### 2. Alpine.js
|
|
374
583
|
|
|
375
|
-
|
|
584
|
+
Lightweight reactivity with declarative HTML attributes:
|
|
376
585
|
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
enabled: boolean;
|
|
387
|
-
cssPath: string;
|
|
388
|
-
configPath?: string;
|
|
389
|
-
};
|
|
586
|
+
```tsx
|
|
587
|
+
export default function AlpinePage() {
|
|
588
|
+
return (
|
|
589
|
+
<div x-data="{ count: 0 }">
|
|
590
|
+
<button x-on:click="count--">-</button>
|
|
591
|
+
<span x-text="count">0</span>
|
|
592
|
+
<button x-on:click="count++">+</button>
|
|
593
|
+
</div>
|
|
594
|
+
);
|
|
390
595
|
}
|
|
391
596
|
```
|
|
392
597
|
|
|
393
|
-
###
|
|
598
|
+
### 3. HTMX
|
|
394
599
|
|
|
395
|
-
|
|
600
|
+
Server-driven UI updates without JavaScript:
|
|
396
601
|
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
602
|
+
```html
|
|
603
|
+
<!-- Can use .html files directly in routes/ -->
|
|
604
|
+
<button
|
|
605
|
+
hx-get="/api/greeting"
|
|
606
|
+
hx-target="#result"
|
|
607
|
+
hx-swap="innerHTML">
|
|
608
|
+
Load
|
|
609
|
+
</button>
|
|
610
|
+
<div id="result"></div>
|
|
404
611
|
```
|
|
405
612
|
|
|
406
|
-
|
|
613
|
+
KilatJS supports `.html` files as routes - perfect for HTMX pages!
|
|
407
614
|
|
|
408
|
-
|
|
615
|
+
### 4. Native HTML Forms
|
|
409
616
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
617
|
+
Best for mutations - no JS required:
|
|
618
|
+
|
|
619
|
+
```tsx
|
|
620
|
+
<form method="POST" action="/contact">
|
|
621
|
+
<input name="email" type="email" />
|
|
622
|
+
<button type="submit">Submit</button>
|
|
623
|
+
</form>
|
|
416
624
|
```
|
|
417
625
|
|
|
418
626
|
---
|
|
419
627
|
|
|
420
|
-
##
|
|
628
|
+
## ❌ What KilatJS Doesn't Do
|
|
421
629
|
|
|
422
|
-
|
|
630
|
+
- ❌ Client-Side Rendering (CSR)
|
|
631
|
+
- ❌ React Hydration / Islands
|
|
632
|
+
- ❌ Client-side routing
|
|
633
|
+
- ❌ React hooks (`useState`, `useEffect`) - No client React runtime
|
|
634
|
+
- ❌ SPA patterns
|
|
423
635
|
|
|
424
|
-
|
|
636
|
+
### Why No React Hooks?
|
|
425
637
|
|
|
426
|
-
|
|
638
|
+
KilatJS uses `renderToStaticMarkup` which outputs pure HTML without React runtime. This means:
|
|
427
639
|
|
|
428
|
-
|
|
640
|
+
- ✅ Faster page loads (no React bundle)
|
|
641
|
+
- ✅ Better SEO (real HTML)
|
|
642
|
+
- ✅ Works without JavaScript
|
|
643
|
+
- ❌ No `useState`, `useEffect`, `onClick`
|
|
429
644
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
645
|
+
**For interactivity, use `clientScript`, Alpine.js, HTMX, or vanilla JS instead.**
|
|
646
|
+
|
|
647
|
+
> If you need React hooks with SSR, consider Next.js or Remix which have built-in hydration.
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## 📝 License
|
|
652
|
+
|
|
653
|
+
MIT
|
|
433
654
|
|
|
434
655
|
---
|
|
435
656
|
|
|
436
657
|
<p align="center">
|
|
437
|
-
<strong>Built with 💜 for the real web using Bun
|
|
438
|
-
</p>
|
|
658
|
+
<strong>Built with 💜 for the real web using Bun.</strong>
|
|
659
|
+
</p>
|