kilatjs 0.1.2 → 0.1.4
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 +318 -352
- package/dist/cli.js +735 -21
- package/dist/core/types.d.ts +6 -0
- package/dist/index.js +758 -26
- package/dist/server/live-reload.d.ts +2 -2
- package/dist/server/server.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,78 +20,18 @@ KilatJS is a server-first web framework inspired by the simplicity of PHP and th
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## ⚡ Quick Start
|
|
24
24
|
|
|
25
|
-
###
|
|
25
|
+
### Create a New Project
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
# Create a new project
|
|
29
|
-
mkdir my-app && cd my-app
|
|
30
|
-
bun init
|
|
31
|
-
|
|
32
|
-
# Install KilatJS
|
|
33
|
-
bun add kilatjs react react-dom
|
|
34
|
-
|
|
35
|
-
# Install dev dependencies
|
|
36
|
-
bun add -d @types/react @types/react-dom typescript tailwindcss
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### Create Your First App
|
|
40
|
-
|
|
41
|
-
**1. Create config file `kilat.config.ts`:**
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
import { defineConfig } from "kilatjs";
|
|
45
|
-
|
|
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
|
-
```
|
|
57
|
-
|
|
58
|
-
**2. Create input CSS `input.css`:**
|
|
59
|
-
|
|
60
|
-
```css
|
|
61
|
-
@import "tailwindcss";
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**3. Create your first route `src/routes/index.tsx`:**
|
|
65
|
-
|
|
66
|
-
```tsx
|
|
67
|
-
export const meta = {
|
|
68
|
-
title: "Welcome to KilatJS",
|
|
69
|
-
description: "A Bun-native, HTML-first framework",
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export async function load() {
|
|
73
|
-
return {
|
|
74
|
-
message: "Hello from the server!",
|
|
75
|
-
time: new Date().toLocaleTimeString(),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
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
|
-
```
|
|
90
|
-
|
|
91
|
-
**4. Run the dev server:**
|
|
27
|
+
The easiest way to start is using the KilatJS CLI:
|
|
92
28
|
|
|
93
29
|
```bash
|
|
94
|
-
|
|
30
|
+
# Create a new project in my-app folder
|
|
31
|
+
bunx kilat create my-app
|
|
32
|
+
cd my-app
|
|
33
|
+
bun install
|
|
34
|
+
bun run dev
|
|
95
35
|
```
|
|
96
36
|
|
|
97
37
|
Visit [http://localhost:3000](http://localhost:3000) 🎉
|
|
@@ -100,12 +40,13 @@ Visit [http://localhost:3000](http://localhost:3000) 🎉
|
|
|
100
40
|
|
|
101
41
|
## 📖 CLI Commands
|
|
102
42
|
|
|
103
|
-
| Command
|
|
104
|
-
|
|
105
|
-
| `kilat
|
|
106
|
-
| `kilat
|
|
107
|
-
| `kilat
|
|
108
|
-
| `kilat
|
|
43
|
+
| Command | Description |
|
|
44
|
+
| -------------------- | ----------------------------------------------- |
|
|
45
|
+
| `kilat create [dir]` | Create a new project from template |
|
|
46
|
+
| `kilat dev` | Start development server with HMR + Live Reload |
|
|
47
|
+
| `kilat build` | Build for production |
|
|
48
|
+
| `kilat serve` | Run production server (`bun dist/server.js`) |
|
|
49
|
+
| `kilat preview` | Preview build output |
|
|
109
50
|
|
|
110
51
|
### Development Output
|
|
111
52
|
|
|
@@ -118,54 +59,42 @@ Visit [http://localhost:3000](http://localhost:3000) 🎉
|
|
|
118
59
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
119
60
|
```
|
|
120
61
|
|
|
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
|
-
```
|
|
143
|
-
|
|
144
62
|
---
|
|
145
63
|
|
|
146
64
|
## 📁 Project Structure
|
|
147
65
|
|
|
66
|
+
KilatJS uses a simplified, root-level structure:
|
|
67
|
+
|
|
148
68
|
```
|
|
149
69
|
my-app/
|
|
70
|
+
├── public/ # Static assets & index.html
|
|
71
|
+
├── routes/ # SSR routes (index.tsx, about.tsx)
|
|
72
|
+
├── components/ # React/Kilat components
|
|
150
73
|
├── kilat.config.ts # Configuration
|
|
151
74
|
├── input.css # Tailwind input
|
|
152
|
-
├──
|
|
153
|
-
├──
|
|
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
|
|
75
|
+
├── index.client.tsx # (Optional) React client entry
|
|
76
|
+
├── App.tsx # (Optional) Client-side app
|
|
164
77
|
└── dist/ # Production build
|
|
165
78
|
```
|
|
166
79
|
|
|
167
80
|
---
|
|
168
81
|
|
|
82
|
+
## ⚛️ React Client Support (Opt-in)
|
|
83
|
+
|
|
84
|
+
While KilatJS is HTML-first, you can easily add a full React client-side application for complex interactive features.
|
|
85
|
+
|
|
86
|
+
1. **Auto-detection**: If `index.client.tsx` exists in your root, KilatJS will automatically bundle it.
|
|
87
|
+
2. **Setup**: Create a `public/index.html` with a mount point:
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<div id="root"></div>
|
|
91
|
+
<script type="module" src="/client/index.client.js"></script>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
3. **Route**: Define `clientRoute: "/client"` in `kilat.config.ts` to host your SPA.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
169
98
|
## 📄 Route Contract
|
|
170
99
|
|
|
171
100
|
Each route file can export:
|
|
@@ -173,30 +102,36 @@ Each route file can export:
|
|
|
173
102
|
```tsx
|
|
174
103
|
// SEO meta tags
|
|
175
104
|
export const meta = {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
105
|
+
title: "Page Title",
|
|
106
|
+
description: "Page description",
|
|
107
|
+
robots: "index,follow",
|
|
108
|
+
ogTitle: "Open Graph Title",
|
|
109
|
+
ogDescription: "OG description",
|
|
110
|
+
ogImage: "https://example.com/image.jpg",
|
|
182
111
|
};
|
|
183
112
|
|
|
184
113
|
// Server-side data loading
|
|
185
114
|
export async function load(ctx) {
|
|
186
|
-
|
|
187
|
-
|
|
115
|
+
const data = await fetchData();
|
|
116
|
+
return { items: data };
|
|
188
117
|
}
|
|
189
118
|
|
|
190
119
|
// HTTP method handlers (POST, PUT, DELETE, etc.)
|
|
191
120
|
export async function POST(ctx) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
121
|
+
const formData = await ctx.request.formData();
|
|
122
|
+
// Process form...
|
|
123
|
+
return Response.redirect("/success", 302);
|
|
195
124
|
}
|
|
196
125
|
|
|
197
126
|
// Page component (receives data from load())
|
|
198
127
|
export default function Page({ data, params, state }) {
|
|
199
|
-
|
|
128
|
+
return (
|
|
129
|
+
<div>
|
|
130
|
+
{data.items.map((item) => (
|
|
131
|
+
<p>{item.name}</p>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
200
135
|
}
|
|
201
136
|
```
|
|
202
137
|
|
|
@@ -210,28 +145,34 @@ export default function Page({ data, params, state }) {
|
|
|
210
145
|
|
|
211
146
|
```tsx
|
|
212
147
|
interface LayoutProps {
|
|
213
|
-
|
|
214
|
-
|
|
148
|
+
children: React.ReactNode;
|
|
149
|
+
title?: string;
|
|
215
150
|
}
|
|
216
151
|
|
|
217
152
|
export function Layout({ children, title = "My Blog" }: LayoutProps) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
153
|
+
return (
|
|
154
|
+
<div className="min-h-screen bg-gray-50">
|
|
155
|
+
<header className="bg-white shadow">
|
|
156
|
+
<nav className="max-w-4xl mx-auto px-4 py-4">
|
|
157
|
+
<a href="/" className="text-xl font-bold">
|
|
158
|
+
My Blog
|
|
159
|
+
</a>
|
|
160
|
+
<div className="float-right space-x-4">
|
|
161
|
+
<a href="/" className="text-gray-600 hover:text-gray-900">
|
|
162
|
+
Home
|
|
163
|
+
</a>
|
|
164
|
+
<a href="/blog" className="text-gray-600 hover:text-gray-900">
|
|
165
|
+
Blog
|
|
166
|
+
</a>
|
|
167
|
+
<a href="/about" className="text-gray-600 hover:text-gray-900">
|
|
168
|
+
About
|
|
169
|
+
</a>
|
|
170
|
+
</div>
|
|
171
|
+
</nav>
|
|
172
|
+
</header>
|
|
173
|
+
<main className="max-w-4xl mx-auto px-4 py-8">{children}</main>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
235
176
|
}
|
|
236
177
|
```
|
|
237
178
|
|
|
@@ -243,37 +184,47 @@ export function Layout({ children, title = "My Blog" }: LayoutProps) {
|
|
|
243
184
|
import { Layout } from "../../components/Layout";
|
|
244
185
|
|
|
245
186
|
export const meta = {
|
|
246
|
-
|
|
247
|
-
|
|
187
|
+
title: "Blog - My Site",
|
|
188
|
+
description: "Read our latest articles",
|
|
248
189
|
};
|
|
249
190
|
|
|
250
191
|
export async function load() {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
192
|
+
// In real app, fetch from database
|
|
193
|
+
const posts = [
|
|
194
|
+
{
|
|
195
|
+
id: 1,
|
|
196
|
+
slug: "hello-world",
|
|
197
|
+
title: "Hello World",
|
|
198
|
+
excerpt: "My first post...",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 2,
|
|
202
|
+
slug: "getting-started",
|
|
203
|
+
title: "Getting Started",
|
|
204
|
+
excerpt: "Learn how to...",
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
return { posts };
|
|
257
208
|
}
|
|
258
209
|
|
|
259
210
|
export default function BlogPage({ data }) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
211
|
+
return (
|
|
212
|
+
<Layout>
|
|
213
|
+
<h1 className="text-3xl font-bold mb-8">Blog</h1>
|
|
214
|
+
<div className="space-y-6">
|
|
215
|
+
{data.posts.map((post) => (
|
|
216
|
+
<article key={post.id} className="bg-white p-6 rounded-lg shadow">
|
|
217
|
+
<h2 className="text-xl font-semibold">
|
|
218
|
+
<a href={`/blog/${post.slug}`} className="hover:text-blue-600">
|
|
219
|
+
{post.title}
|
|
220
|
+
</a>
|
|
221
|
+
</h2>
|
|
222
|
+
<p className="text-gray-600 mt-2">{post.excerpt}</p>
|
|
223
|
+
</article>
|
|
224
|
+
))}
|
|
225
|
+
</div>
|
|
226
|
+
</Layout>
|
|
227
|
+
);
|
|
277
228
|
}
|
|
278
229
|
```
|
|
279
230
|
|
|
@@ -285,42 +236,45 @@ export default function BlogPage({ data }) {
|
|
|
285
236
|
import { Layout } from "../../components/Layout";
|
|
286
237
|
|
|
287
238
|
export const meta = {
|
|
288
|
-
|
|
289
|
-
|
|
239
|
+
title: "Blog Post",
|
|
240
|
+
description: "Read this article",
|
|
290
241
|
};
|
|
291
242
|
|
|
292
243
|
export async function load({ params }) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
244
|
+
// params.slug contains the URL parameter
|
|
245
|
+
const post = await getPostBySlug(params.slug);
|
|
246
|
+
|
|
247
|
+
if (!post) {
|
|
248
|
+
throw new Response("Not Found", { status: 404 });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return { post };
|
|
301
252
|
}
|
|
302
253
|
|
|
303
254
|
async function getPostBySlug(slug: string) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
255
|
+
// In real app, fetch from database
|
|
256
|
+
const posts = {
|
|
257
|
+
"hello-world": { title: "Hello World", content: "This is my first post!" },
|
|
258
|
+
"getting-started": {
|
|
259
|
+
title: "Getting Started",
|
|
260
|
+
content: "Let me show you how...",
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
return posts[slug] || null;
|
|
310
264
|
}
|
|
311
265
|
|
|
312
266
|
export default function BlogPostPage({ data, params }) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
267
|
+
return (
|
|
268
|
+
<Layout>
|
|
269
|
+
<article>
|
|
270
|
+
<h1 className="text-4xl font-bold mb-4">{data.post.title}</h1>
|
|
271
|
+
<div className="prose max-w-none">{data.post.content}</div>
|
|
272
|
+
</article>
|
|
273
|
+
<a href="/blog" className="text-blue-600 mt-8 inline-block">
|
|
274
|
+
← Back to Blog
|
|
275
|
+
</a>
|
|
276
|
+
</Layout>
|
|
277
|
+
);
|
|
324
278
|
}
|
|
325
279
|
```
|
|
326
280
|
|
|
@@ -332,40 +286,40 @@ export default function BlogPostPage({ data, params }) {
|
|
|
332
286
|
import { RouteContext } from "kilatjs";
|
|
333
287
|
|
|
334
288
|
const posts = [
|
|
335
|
-
|
|
336
|
-
|
|
289
|
+
{ id: 1, title: "Hello World", slug: "hello-world" },
|
|
290
|
+
{ id: 2, title: "Getting Started", slug: "getting-started" },
|
|
337
291
|
];
|
|
338
292
|
|
|
339
293
|
// GET /api/posts
|
|
340
294
|
export async function GET(ctx: RouteContext) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
295
|
+
const search = ctx.query.get("search")?.toLowerCase();
|
|
296
|
+
|
|
297
|
+
let results = posts;
|
|
298
|
+
if (search) {
|
|
299
|
+
results = posts.filter((p) => p.title.toLowerCase().includes(search));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return new Response(JSON.stringify({ data: results }), {
|
|
303
|
+
headers: { "Content-Type": "application/json" },
|
|
304
|
+
});
|
|
351
305
|
}
|
|
352
306
|
|
|
353
307
|
// POST /api/posts
|
|
354
308
|
export async function POST(ctx: RouteContext) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
309
|
+
const body = await ctx.request.json();
|
|
310
|
+
|
|
311
|
+
const newPost = {
|
|
312
|
+
id: posts.length + 1,
|
|
313
|
+
title: body.title,
|
|
314
|
+
slug: body.title.toLowerCase().replace(/\s+/g, "-"),
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
posts.push(newPost);
|
|
318
|
+
|
|
319
|
+
return new Response(JSON.stringify({ data: newPost }), {
|
|
320
|
+
status: 201,
|
|
321
|
+
headers: { "Content-Type": "application/json" },
|
|
322
|
+
});
|
|
369
323
|
}
|
|
370
324
|
```
|
|
371
325
|
|
|
@@ -377,65 +331,65 @@ export async function POST(ctx: RouteContext) {
|
|
|
377
331
|
import { Layout } from "../components/Layout";
|
|
378
332
|
|
|
379
333
|
export const meta = {
|
|
380
|
-
|
|
381
|
-
|
|
334
|
+
title: "Contact Us",
|
|
335
|
+
description: "Get in touch with us",
|
|
382
336
|
};
|
|
383
337
|
|
|
384
338
|
// Handle form submission
|
|
385
339
|
export async function POST({ request }) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
340
|
+
const formData = await request.formData();
|
|
341
|
+
const name = formData.get("name");
|
|
342
|
+
const email = formData.get("email");
|
|
343
|
+
const message = formData.get("message");
|
|
344
|
+
|
|
345
|
+
// Save to database, send email, etc.
|
|
346
|
+
console.log("Contact form:", { name, email, message });
|
|
347
|
+
|
|
348
|
+
// PRG: Post-Redirect-Get pattern
|
|
349
|
+
return Response.redirect("/contact/success", 302);
|
|
396
350
|
}
|
|
397
351
|
|
|
398
352
|
export default function ContactPage() {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
353
|
+
return (
|
|
354
|
+
<Layout>
|
|
355
|
+
<h1 className="text-3xl font-bold mb-8">Contact Us</h1>
|
|
356
|
+
<form method="POST" className="max-w-md space-y-4">
|
|
357
|
+
<div>
|
|
358
|
+
<label className="block text-sm font-medium mb-1">Name</label>
|
|
359
|
+
<input
|
|
360
|
+
type="text"
|
|
361
|
+
name="name"
|
|
362
|
+
required
|
|
363
|
+
className="w-full px-3 py-2 border rounded-lg"
|
|
364
|
+
/>
|
|
365
|
+
</div>
|
|
366
|
+
<div>
|
|
367
|
+
<label className="block text-sm font-medium mb-1">Email</label>
|
|
368
|
+
<input
|
|
369
|
+
type="email"
|
|
370
|
+
name="email"
|
|
371
|
+
required
|
|
372
|
+
className="w-full px-3 py-2 border rounded-lg"
|
|
373
|
+
/>
|
|
374
|
+
</div>
|
|
375
|
+
<div>
|
|
376
|
+
<label className="block text-sm font-medium mb-1">Message</label>
|
|
377
|
+
<textarea
|
|
378
|
+
name="message"
|
|
379
|
+
rows={4}
|
|
380
|
+
required
|
|
381
|
+
className="w-full px-3 py-2 border rounded-lg"
|
|
382
|
+
/>
|
|
383
|
+
</div>
|
|
384
|
+
<button
|
|
385
|
+
type="submit"
|
|
386
|
+
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
|
|
387
|
+
>
|
|
388
|
+
Send Message
|
|
389
|
+
</button>
|
|
390
|
+
</form>
|
|
391
|
+
</Layout>
|
|
392
|
+
);
|
|
439
393
|
}
|
|
440
394
|
```
|
|
441
395
|
|
|
@@ -445,19 +399,25 @@ export default function ContactPage() {
|
|
|
445
399
|
import { Layout } from "../../components/Layout";
|
|
446
400
|
|
|
447
401
|
export const meta = {
|
|
448
|
-
|
|
402
|
+
title: "Message Sent",
|
|
449
403
|
};
|
|
450
404
|
|
|
451
405
|
export default function ContactSuccessPage() {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
406
|
+
return (
|
|
407
|
+
<Layout>
|
|
408
|
+
<div className="text-center py-12">
|
|
409
|
+
<h1 className="text-3xl font-bold text-green-600 mb-4">
|
|
410
|
+
✓ Message Sent!
|
|
411
|
+
</h1>
|
|
412
|
+
<p className="text-gray-600 mb-8">
|
|
413
|
+
Thank you for contacting us. We'll get back to you soon.
|
|
414
|
+
</p>
|
|
415
|
+
<a href="/" className="text-blue-600 hover:underline">
|
|
416
|
+
← Back to Home
|
|
417
|
+
</a>
|
|
418
|
+
</div>
|
|
419
|
+
</Layout>
|
|
420
|
+
);
|
|
461
421
|
}
|
|
462
422
|
```
|
|
463
423
|
|
|
@@ -468,26 +428,26 @@ export default function ContactSuccessPage() {
|
|
|
468
428
|
```tsx
|
|
469
429
|
// src/routes/dashboard.tsx
|
|
470
430
|
export async function load({ request }) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
431
|
+
const cookie = request.headers.get("cookie");
|
|
432
|
+
const session = await getSession(cookie);
|
|
433
|
+
|
|
434
|
+
if (!session) {
|
|
435
|
+
// Redirect to login if not authenticated
|
|
436
|
+
throw Response.redirect("/login", 302);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return { user: session.user };
|
|
480
440
|
}
|
|
481
441
|
|
|
482
442
|
export default function DashboardPage({ data }) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
443
|
+
return (
|
|
444
|
+
<div>
|
|
445
|
+
<h1>Welcome, {data.user.name}!</h1>
|
|
446
|
+
<form method="POST" action="/logout">
|
|
447
|
+
<button type="submit">Logout</button>
|
|
448
|
+
</form>
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
491
451
|
}
|
|
492
452
|
```
|
|
493
453
|
|
|
@@ -500,16 +460,16 @@ export default function DashboardPage({ data }) {
|
|
|
500
460
|
import { defineConfig } from "kilatjs";
|
|
501
461
|
|
|
502
462
|
export default defineConfig({
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
463
|
+
appDir: "./src", // Source directory (auto-detects routes/ or pages/)
|
|
464
|
+
outDir: "./dist", // Production build output
|
|
465
|
+
port: 3000, // Server port
|
|
466
|
+
hostname: "localhost", // Server hostname
|
|
467
|
+
publicDir: "./public", // Static assets directory
|
|
468
|
+
tailwind: {
|
|
469
|
+
enabled: true, // Enable Tailwind CSS
|
|
470
|
+
inputPath: "./input.css",
|
|
471
|
+
cssPath: "./styles.css",
|
|
472
|
+
},
|
|
513
473
|
});
|
|
514
474
|
```
|
|
515
475
|
|
|
@@ -526,6 +486,7 @@ KilatJS leverages Bun's runtime directly:
|
|
|
526
486
|
- 🔍 **Bun.Glob** - Pattern matching for routes
|
|
527
487
|
|
|
528
488
|
**Performance:**
|
|
489
|
+
|
|
529
490
|
- 3x faster cold starts vs Node.js
|
|
530
491
|
- Native TypeScript - no transpilation
|
|
531
492
|
- Zero-copy streaming for static files
|
|
@@ -555,25 +516,25 @@ Export a function that runs in the browser. TypeScript/LSP fully supports it!
|
|
|
555
516
|
|
|
556
517
|
// Client-side script - auto-injected, LSP checks the code!
|
|
557
518
|
export function clientScript() {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
519
|
+
let count = 0;
|
|
520
|
+
const countEl = document.getElementById("count")!;
|
|
521
|
+
|
|
522
|
+
document.getElementById("decrement")!.onclick = () => {
|
|
523
|
+
countEl.textContent = String(--count);
|
|
524
|
+
};
|
|
525
|
+
document.getElementById("increment")!.onclick = () => {
|
|
526
|
+
countEl.textContent = String(++count);
|
|
527
|
+
};
|
|
567
528
|
}
|
|
568
529
|
|
|
569
530
|
export default function CounterPage() {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
531
|
+
return (
|
|
532
|
+
<div>
|
|
533
|
+
<button id="decrement">-</button>
|
|
534
|
+
<span id="count">0</span>
|
|
535
|
+
<button id="increment">+</button>
|
|
536
|
+
</div>
|
|
537
|
+
);
|
|
577
538
|
}
|
|
578
539
|
```
|
|
579
540
|
|
|
@@ -585,13 +546,13 @@ Lightweight reactivity with declarative HTML attributes:
|
|
|
585
546
|
|
|
586
547
|
```tsx
|
|
587
548
|
export default function AlpinePage() {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
549
|
+
return (
|
|
550
|
+
<div x-data="{ count: 0 }">
|
|
551
|
+
<button x-on:click="count--">-</button>
|
|
552
|
+
<span x-text="count">0</span>
|
|
553
|
+
<button x-on:click="count++">+</button>
|
|
554
|
+
</div>
|
|
555
|
+
);
|
|
595
556
|
}
|
|
596
557
|
```
|
|
597
558
|
|
|
@@ -601,11 +562,8 @@ Server-driven UI updates without JavaScript:
|
|
|
601
562
|
|
|
602
563
|
```html
|
|
603
564
|
<!-- Can use .html files directly in routes/ -->
|
|
604
|
-
<button
|
|
605
|
-
|
|
606
|
-
hx-target="#result"
|
|
607
|
-
hx-swap="innerHTML">
|
|
608
|
-
Load
|
|
565
|
+
<button hx-get="/api/greeting" hx-target="#result" hx-swap="innerHTML">
|
|
566
|
+
Load
|
|
609
567
|
</button>
|
|
610
568
|
<div id="result"></div>
|
|
611
569
|
```
|
|
@@ -618,33 +576,41 @@ Best for mutations - no JS required:
|
|
|
618
576
|
|
|
619
577
|
```tsx
|
|
620
578
|
<form method="POST" action="/contact">
|
|
621
|
-
|
|
622
|
-
|
|
579
|
+
<input name="email" type="email" />
|
|
580
|
+
<button type="submit">Submit</button>
|
|
623
581
|
</form>
|
|
624
582
|
```
|
|
625
583
|
|
|
626
|
-
|
|
584
|
+
## 🛠️ Configuration Reference
|
|
627
585
|
|
|
628
|
-
|
|
586
|
+
```ts
|
|
587
|
+
// kilat.config.ts
|
|
588
|
+
import { defineConfig } from "kilatjs";
|
|
629
589
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
590
|
+
export default defineConfig({
|
|
591
|
+
appDir: ".", // Root directory for routes/ and components/
|
|
592
|
+
outDir: "./dist", // Production build output
|
|
593
|
+
port: 3000, // Server port
|
|
594
|
+
publicDir: "./public", // Static assets directory (auto-detects index.html)
|
|
595
|
+
clientRoute: "/client", // (Optional) Path for React client SPA
|
|
596
|
+
tailwind: {
|
|
597
|
+
enabled: true, // Enable Tailwind CSS
|
|
598
|
+
inputPath: "./input.css",
|
|
599
|
+
cssPath: "./public/styles.css",
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
```
|
|
635
603
|
|
|
636
|
-
|
|
604
|
+
---
|
|
637
605
|
|
|
638
|
-
|
|
606
|
+
## ⚡ Hybrid Approach
|
|
639
607
|
|
|
640
|
-
|
|
641
|
-
- ✅ Better SEO (real HTML)
|
|
642
|
-
- ✅ Works without JavaScript
|
|
643
|
-
- ❌ No `useState`, `useEffect`, `onClick`
|
|
608
|
+
KilatJS uniquely allows you to mix **HTML-First SSR** with **Full React CSR**:
|
|
644
609
|
|
|
645
|
-
**
|
|
610
|
+
- **Use SSR** for public pages, SEO, blogs, and landing pages.
|
|
611
|
+
- **Use CSR** for complex dashboards, interactive editors, or apps requiring shared state.
|
|
646
612
|
|
|
647
|
-
>
|
|
613
|
+
> **Note**: KilatJS keeps these worlds separate. SSR pages are pure HTML (no React runtime), while CSR pages are full React apps. This gives you the speed of a static site with the power of a modern app.
|
|
648
614
|
|
|
649
615
|
---
|
|
650
616
|
|