kilatjs 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1046 @@
1
+ // src/adapters/react.ts
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import React from "react";
4
+
5
+ class ReactAdapter {
6
+ static async renderToString(component, props) {
7
+ try {
8
+ const element = React.createElement(component, props);
9
+ const html = renderToStaticMarkup(element);
10
+ return html;
11
+ } catch (error) {
12
+ console.error("Error rendering React component:", error);
13
+ throw error;
14
+ }
15
+ }
16
+ static createDocument(html, meta = {}, config) {
17
+ const title = meta.title || "KilatJS App";
18
+ const description = meta.description || "";
19
+ const robots = meta.robots || "index,follow";
20
+ let metaTags = "";
21
+ metaTags += `<meta charset="utf-8" />
22
+ `;
23
+ metaTags += `<meta name="viewport" content="width=device-width, initial-scale=1" />
24
+ `;
25
+ metaTags += `<meta name="robots" content="${robots}" />
26
+ `;
27
+ if (description) {
28
+ metaTags += `<meta name="description" content="${this.escapeHtml(description)}" />
29
+ `;
30
+ }
31
+ if (meta.canonical) {
32
+ metaTags += `<link rel="canonical" href="${this.escapeHtml(meta.canonical)}" />
33
+ `;
34
+ }
35
+ metaTags += `<meta property="og:title" content="${this.escapeHtml(meta.ogTitle || title)}" />
36
+ `;
37
+ if (meta.ogDescription || description) {
38
+ metaTags += `<meta property="og:description" content="${this.escapeHtml(meta.ogDescription || description)}" />
39
+ `;
40
+ }
41
+ if (meta.ogImage) {
42
+ metaTags += `<meta property="og:image" content="${this.escapeHtml(meta.ogImage)}" />
43
+ `;
44
+ }
45
+ const twitterCard = meta.twitterCard || "summary";
46
+ metaTags += `<meta name="twitter:card" content="${twitterCard}" />
47
+ `;
48
+ const reservedKeys = [
49
+ "title",
50
+ "description",
51
+ "robots",
52
+ "canonical",
53
+ "ogTitle",
54
+ "ogDescription",
55
+ "ogImage",
56
+ "twitterCard"
57
+ ];
58
+ Object.entries(meta).forEach(([key, value]) => {
59
+ if (!reservedKeys.includes(key) && typeof value === "string") {
60
+ metaTags += `<meta name="${this.escapeHtml(key)}" content="${this.escapeHtml(value)}" />
61
+ `;
62
+ }
63
+ });
64
+ const cssUrl = config?.dev ? `/styles.css?v=${Date.now()}` : "/styles.css";
65
+ metaTags += `<link rel="stylesheet" href="${cssUrl}" />
66
+ `;
67
+ metaTags += `<link rel="preconnect" href="https://fonts.googleapis.com" />
68
+ `;
69
+ metaTags += `<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
70
+ `;
71
+ metaTags += `<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
72
+ `;
73
+ return `<!DOCTYPE html>
74
+ <html lang="en">
75
+ <head>
76
+ <title>${this.escapeHtml(title)}</title>
77
+ ${metaTags}
78
+ </head>
79
+ <body>
80
+ <div id="root">
81
+ ${html}
82
+ </div>
83
+ ${config?.dev ? `<script>
84
+ (function() {
85
+ let currentServerId = null;
86
+ let isReconnecting = false;
87
+
88
+ function connect() {
89
+ const source = new EventSource('/_kilat/live-reload');
90
+
91
+ source.onmessage = (event) => {
92
+ const newServerId = event.data;
93
+ if (currentServerId === null) {
94
+ currentServerId = newServerId;
95
+ } else if (currentServerId !== newServerId) {
96
+ // Server ID changed, reload!
97
+ location.reload();
98
+ }
99
+ };
100
+
101
+ source.onerror = () => {
102
+ source.close();
103
+ // Try to reconnect in 1s
104
+ setTimeout(connect, 1000);
105
+ };
106
+ }
107
+
108
+ connect();
109
+ })();
110
+ </script>` : ""}
111
+ </body>
112
+ </html>`;
113
+ }
114
+ static escapeHtml(str) {
115
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
116
+ }
117
+ }
118
+
119
+ // src/adapters/htmx.ts
120
+ class HTMXAdapter {
121
+ static async renderToString(template, props) {
122
+ try {
123
+ const html = await template(props);
124
+ return html;
125
+ } catch (error) {
126
+ console.error("Error rendering HTMX template:", error);
127
+ throw error;
128
+ }
129
+ }
130
+ static isHTMXRequest(request) {
131
+ return request.headers.get("HX-Request") === "true";
132
+ }
133
+ static isBoostedRequest(request) {
134
+ return request.headers.get("HX-Boosted") === "true";
135
+ }
136
+ static getCurrentUrl(request) {
137
+ return request.headers.get("HX-Current-URL");
138
+ }
139
+ static getTrigger(request) {
140
+ return request.headers.get("HX-Trigger");
141
+ }
142
+ static getTriggerName(request) {
143
+ return request.headers.get("HX-Trigger-Name");
144
+ }
145
+ static getTarget(request) {
146
+ return request.headers.get("HX-Target");
147
+ }
148
+ static createResponse(html, options = {}) {
149
+ const headers = new Headers({
150
+ "Content-Type": "text/html; charset=utf-8"
151
+ });
152
+ if (options.retarget) {
153
+ headers.set("HX-Retarget", options.retarget);
154
+ }
155
+ if (options.reswap) {
156
+ headers.set("HX-Reswap", options.reswap);
157
+ }
158
+ if (options.trigger) {
159
+ headers.set("HX-Trigger", options.trigger);
160
+ }
161
+ if (options.triggerAfterSettle) {
162
+ headers.set("HX-Trigger-After-Settle", options.triggerAfterSettle);
163
+ }
164
+ if (options.triggerAfterSwap) {
165
+ headers.set("HX-Trigger-After-Swap", options.triggerAfterSwap);
166
+ }
167
+ if (options.redirect) {
168
+ headers.set("HX-Redirect", options.redirect);
169
+ }
170
+ if (options.refresh) {
171
+ headers.set("HX-Refresh", "true");
172
+ }
173
+ if (options.pushUrl) {
174
+ headers.set("HX-Push-Url", options.pushUrl);
175
+ }
176
+ if (options.replaceUrl) {
177
+ headers.set("HX-Replace-Url", options.replaceUrl);
178
+ }
179
+ return new Response(html, { headers, status: options.status || 200 });
180
+ }
181
+ static redirectResponse(url) {
182
+ return new Response(null, {
183
+ headers: {
184
+ "HX-Redirect": url
185
+ }
186
+ });
187
+ }
188
+ static refreshResponse() {
189
+ return new Response(null, {
190
+ headers: {
191
+ "HX-Refresh": "true"
192
+ }
193
+ });
194
+ }
195
+ static createDocument(html, meta = {}, config) {
196
+ const title = meta.title || "KilatJS App";
197
+ const description = meta.description || "";
198
+ const robots = meta.robots || "index,follow";
199
+ let metaTags = "";
200
+ metaTags += `<meta charset="utf-8" />
201
+ `;
202
+ metaTags += `<meta name="viewport" content="width=device-width, initial-scale=1" />
203
+ `;
204
+ metaTags += `<meta name="robots" content="${robots}" />
205
+ `;
206
+ if (description) {
207
+ metaTags += `<meta name="description" content="${this.escapeHtml(description)}" />
208
+ `;
209
+ }
210
+ if (meta.canonical) {
211
+ metaTags += `<link rel="canonical" href="${this.escapeHtml(meta.canonical)}" />
212
+ `;
213
+ }
214
+ metaTags += `<meta property="og:title" content="${this.escapeHtml(meta.ogTitle || title)}" />
215
+ `;
216
+ if (meta.ogDescription || description) {
217
+ metaTags += `<meta property="og:description" content="${this.escapeHtml(meta.ogDescription || description)}" />
218
+ `;
219
+ }
220
+ if (meta.ogImage) {
221
+ metaTags += `<meta property="og:image" content="${this.escapeHtml(meta.ogImage)}" />
222
+ `;
223
+ }
224
+ const twitterCard = meta.twitterCard || "summary";
225
+ metaTags += `<meta name="twitter:card" content="${twitterCard}" />
226
+ `;
227
+ const reservedKeys = [
228
+ "title",
229
+ "description",
230
+ "robots",
231
+ "canonical",
232
+ "ogTitle",
233
+ "ogDescription",
234
+ "ogImage",
235
+ "twitterCard"
236
+ ];
237
+ Object.entries(meta).forEach(([key, value]) => {
238
+ if (!reservedKeys.includes(key) && typeof value === "string") {
239
+ metaTags += `<meta name="${this.escapeHtml(key)}" content="${this.escapeHtml(value)}" />
240
+ `;
241
+ }
242
+ });
243
+ metaTags += `<link rel="stylesheet" href="/styles.css" />
244
+ `;
245
+ metaTags += `<script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
246
+ `;
247
+ metaTags += `<link rel="preconnect" href="https://fonts.googleapis.com" />
248
+ `;
249
+ metaTags += `<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
250
+ `;
251
+ metaTags += `<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
252
+ `;
253
+ return `<!DOCTYPE html>
254
+ <html lang="en">
255
+ <head>
256
+ <title>${this.escapeHtml(title)}</title>
257
+ ${metaTags}
258
+ </head>
259
+ <body hx-boost="true">
260
+ <div id="root">
261
+ ${html}
262
+ </div>
263
+ </body>
264
+ </html>`;
265
+ }
266
+ static escapeHtml(str) {
267
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
268
+ }
269
+ }
270
+
271
+ // src/core/router.ts
272
+ class Router {
273
+ routes = new Map;
274
+ config;
275
+ staticPaths = new Map;
276
+ serverId = Date.now().toString();
277
+ fsRouter;
278
+ routeCache = new Map;
279
+ preloadedRoutes = new Map;
280
+ routePatterns = [];
281
+ apiRoutes = new Map;
282
+ staticApiResponses = new Map;
283
+ static NOT_FOUND_RESPONSE = new Response("404 Not Found", { status: 404 });
284
+ static METHOD_NOT_ALLOWED_RESPONSE = new Response(JSON.stringify({ error: "Method Not Allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
285
+ static INTERNAL_ERROR_RESPONSE = new Response(JSON.stringify({ error: "Internal Server Error" }), { status: 500, headers: { "Content-Type": "application/json" } });
286
+ contextPool = [];
287
+ poolIndex = 0;
288
+ constructor(config) {
289
+ this.config = config;
290
+ const routesDir = config.routesDir.startsWith("/") ? config.routesDir : `${process.cwd()}/${config.routesDir}`;
291
+ this.fsRouter = new Bun.FileSystemRouter({
292
+ dir: routesDir,
293
+ style: "nextjs",
294
+ origin: `http://${config.hostname || "localhost"}:${config.port || 3000}`
295
+ });
296
+ }
297
+ getRoutes() {
298
+ return this.routes;
299
+ }
300
+ getStaticPaths() {
301
+ return this.staticPaths;
302
+ }
303
+ async loadRoutes() {
304
+ if (this.config.dev) {
305
+ this.fsRouter.reload();
306
+ this.routeCache.clear();
307
+ this.preloadedRoutes.clear();
308
+ this.routePatterns.length = 0;
309
+ }
310
+ await this.preloadAllRoutes();
311
+ console.log("\uD83D\uDD04 FileSystemRouter initialized with", this.preloadedRoutes.size, "preloaded routes");
312
+ if (this.config.dev) {
313
+ console.log("\uD83D\uDCCB Preloaded routes:");
314
+ for (const [route, exports] of this.preloadedRoutes.entries()) {
315
+ console.log(` ${route} (${route.includes("[") ? "dynamic" : "static"})`);
316
+ }
317
+ console.log("\uD83D\uDCCB Dynamic route patterns:", this.routePatterns.length);
318
+ for (const pattern of this.routePatterns) {
319
+ console.log(` ${pattern.pattern} -> ${pattern.filePath}`);
320
+ }
321
+ }
322
+ }
323
+ async preloadAllRoutes() {
324
+ const routesDir = this.config.routesDir.startsWith("/") ? this.config.routesDir : `${process.cwd()}/${this.config.routesDir}`;
325
+ await this.scanAndPreloadRoutes(routesDir, "");
326
+ }
327
+ async scanAndPreloadRoutes(dir, basePath) {
328
+ try {
329
+ const proc = Bun.spawn(["find", dir, "-name", "*.ts", "-o", "-name", "*.tsx", "-o", "-name", "*.js", "-o", "-name", "*.jsx"], {
330
+ stdout: "pipe"
331
+ });
332
+ const output = await new Response(proc.stdout).text();
333
+ const files = output.trim().split(`
334
+ `).filter(Boolean);
335
+ for (const filePath of files) {
336
+ const relativePath = filePath.replace(dir, "");
337
+ let routePath = relativePath.replace(/\.(tsx?|jsx?)$/, "");
338
+ if (routePath.endsWith("/index")) {
339
+ routePath = routePath.slice(0, -6) || "/";
340
+ }
341
+ if (!routePath.startsWith("/")) {
342
+ routePath = "/" + routePath;
343
+ }
344
+ const routeType = this.getRouteType(routePath);
345
+ try {
346
+ const routeExports = await import(filePath);
347
+ this.preloadedRoutes.set(routePath, routeExports);
348
+ if (routeType === "api") {
349
+ this.apiRoutes.set(routePath, routeExports);
350
+ }
351
+ if (routePath.includes("[")) {
352
+ const pattern = this.createRoutePattern(routePath);
353
+ if (pattern) {
354
+ this.routePatterns.push({
355
+ pattern: pattern.regex,
356
+ filePath,
357
+ paramNames: pattern.paramNames,
358
+ routeType
359
+ });
360
+ }
361
+ }
362
+ } catch (error) {
363
+ console.warn(`Failed to preload route ${filePath}:`, error);
364
+ }
365
+ }
366
+ } catch (error) {
367
+ console.warn("Failed to scan routes:", error);
368
+ }
369
+ }
370
+ createRoutePattern(routePath) {
371
+ const paramNames = [];
372
+ let pattern = routePath.replace(/[.*+?^${}|\\]/g, "\\$&");
373
+ pattern = pattern.replace(/\\?\[([^\]]+)\\?\]/g, (match, paramName) => {
374
+ paramNames.push(paramName);
375
+ return "([^/]+)";
376
+ });
377
+ try {
378
+ const regex = new RegExp(`^${pattern}$`);
379
+ console.log(`Created pattern for ${routePath}: ${regex} (params: ${paramNames.join(", ")})`);
380
+ return {
381
+ regex,
382
+ paramNames
383
+ };
384
+ } catch (error) {
385
+ console.warn(`Failed to create pattern for ${routePath}:`, error);
386
+ return null;
387
+ }
388
+ }
389
+ getRouteType(pathname) {
390
+ if (pathname.startsWith("/api")) {
391
+ return "api";
392
+ }
393
+ if (pathname.includes("[") || pathname.includes(":")) {
394
+ return "dynamic";
395
+ }
396
+ return "static";
397
+ }
398
+ matchRoute(path) {
399
+ if (this.routeCache.has(path)) {
400
+ return this.routeCache.get(path);
401
+ }
402
+ let match = null;
403
+ if (this.preloadedRoutes.has(path)) {
404
+ match = {
405
+ route: path,
406
+ params: {},
407
+ exports: this.preloadedRoutes.get(path),
408
+ routeType: this.getRouteType(path)
409
+ };
410
+ }
411
+ if (!match) {
412
+ for (const routePattern of this.routePatterns) {
413
+ const regexMatch = path.match(routePattern.pattern);
414
+ if (regexMatch) {
415
+ const params = {};
416
+ routePattern.paramNames.forEach((name, index) => {
417
+ params[name] = regexMatch[index + 1];
418
+ });
419
+ const routesDir = this.config.routesDir.startsWith("/") ? this.config.routesDir : `${process.cwd()}/${this.config.routesDir}`;
420
+ let routePath = routePattern.filePath.replace(routesDir, "").replace(/\.(tsx?|jsx?)$/, "");
421
+ if (!routePath.startsWith("/")) {
422
+ routePath = "/" + routePath;
423
+ }
424
+ const exports = this.preloadedRoutes.get(routePath);
425
+ if (exports) {
426
+ match = {
427
+ route: path,
428
+ params,
429
+ exports,
430
+ routeType: routePattern.routeType
431
+ };
432
+ break;
433
+ }
434
+ }
435
+ }
436
+ }
437
+ this.routeCache.set(path, match);
438
+ return match;
439
+ }
440
+ pathMatchesPattern(pattern, path) {
441
+ const patternParts = pattern.split("/").filter(Boolean);
442
+ const pathParts = path.split("/").filter(Boolean);
443
+ if (patternParts.length !== pathParts.length) {
444
+ return false;
445
+ }
446
+ for (let i = 0;i < patternParts.length; i++) {
447
+ const patternPart = patternParts[i];
448
+ const pathPart = pathParts[i];
449
+ if (!patternPart.startsWith("[") && patternPart !== pathPart) {
450
+ return false;
451
+ }
452
+ }
453
+ return true;
454
+ }
455
+ async handleRequest(request) {
456
+ const url = request.url;
457
+ const pathStart = url.indexOf("/", 8);
458
+ const pathEnd = url.indexOf("?", pathStart);
459
+ const path = pathEnd === -1 ? url.slice(pathStart) : url.slice(pathStart, pathEnd);
460
+ if (path.startsWith("/api/")) {
461
+ return this.handleApiRouteFast(request, path);
462
+ }
463
+ if (path.endsWith(".css") || path === "/favicon.ico" || path === "/_kilat/live-reload") {
464
+ const staticResponse = await this.handleStaticFile(path);
465
+ if (staticResponse) {
466
+ return staticResponse;
467
+ }
468
+ }
469
+ if (!this.config.dev) {
470
+ const outDir = this.config.outDir || "./dist";
471
+ const filePath = `${outDir}${path === "/" ? "/index.html" : path}`;
472
+ const file = Bun.file(filePath);
473
+ if (await file.exists()) {
474
+ return new Response(file);
475
+ }
476
+ if (!path.endsWith(".html")) {
477
+ const indexFile = Bun.file(`${filePath}/index.html`);
478
+ if (await indexFile.exists()) {
479
+ return new Response(indexFile);
480
+ }
481
+ }
482
+ }
483
+ const match = this.matchRoute(path);
484
+ if (!match) {
485
+ return Router.NOT_FOUND_RESPONSE;
486
+ }
487
+ const { params, exports, routeType } = match;
488
+ const context = this.getContextFromPool(request, params, url);
489
+ if (request.method !== "GET" && request.method !== "HEAD") {
490
+ const actionHandler = exports[request.method];
491
+ if (actionHandler && typeof actionHandler === "function") {
492
+ try {
493
+ const result = await actionHandler(context);
494
+ this.returnContextToPool(context);
495
+ return result;
496
+ } catch (error) {
497
+ this.returnContextToPool(context);
498
+ console.error(`Error handling ${request.method}:`, error);
499
+ return Router.INTERNAL_ERROR_RESPONSE;
500
+ }
501
+ }
502
+ this.returnContextToPool(context);
503
+ return Router.METHOD_NOT_ALLOWED_RESPONSE;
504
+ }
505
+ try {
506
+ const data = exports.load ? await exports.load(context) : {};
507
+ if (data instanceof Response) {
508
+ this.returnContextToPool(context);
509
+ return data;
510
+ }
511
+ const meta = exports.meta || {};
512
+ const html = await this.renderPage(exports.default, { data, params, state: context.state }, exports.ui, meta);
513
+ const cacheControl = routeType === "dynamic" ? "no-cache" : "public, max-age=3600";
514
+ this.returnContextToPool(context);
515
+ return new Response(html, {
516
+ headers: {
517
+ "Content-Type": "text/html; charset=utf-8",
518
+ "Cache-Control": cacheControl
519
+ }
520
+ });
521
+ } catch (error) {
522
+ this.returnContextToPool(context);
523
+ if (error instanceof Response) {
524
+ return error;
525
+ }
526
+ console.error("Error rendering page:", error);
527
+ return Router.INTERNAL_ERROR_RESPONSE;
528
+ }
529
+ }
530
+ async handleApiRouteFast(request, path) {
531
+ let exports = this.apiRoutes.get(path);
532
+ let params = {};
533
+ if (!exports) {
534
+ for (const routePattern of this.routePatterns) {
535
+ if (routePattern.routeType === "api") {
536
+ const regexMatch = path.match(routePattern.pattern);
537
+ if (regexMatch) {
538
+ routePattern.paramNames.forEach((name, index) => {
539
+ params[name] = regexMatch[index + 1];
540
+ });
541
+ for (const [preloadedPath, preloadedExports] of this.preloadedRoutes.entries()) {
542
+ if (preloadedPath.includes("[") && preloadedPath.startsWith("/api/") && this.pathMatchesPattern(preloadedPath, path)) {
543
+ exports = preloadedExports;
544
+ break;
545
+ }
546
+ }
547
+ if (exports)
548
+ break;
549
+ }
550
+ }
551
+ }
552
+ }
553
+ if (!exports) {
554
+ return Router.NOT_FOUND_RESPONSE;
555
+ }
556
+ const method = request.method;
557
+ const handler = exports[method] || exports.default;
558
+ if (!handler || typeof handler !== "function") {
559
+ return Router.METHOD_NOT_ALLOWED_RESPONSE;
560
+ }
561
+ try {
562
+ const context = this.getMinimalApiContext(request, path, params);
563
+ const response = await handler(context);
564
+ if (response instanceof Response) {
565
+ return response;
566
+ }
567
+ return new Response(JSON.stringify(response), {
568
+ headers: { "Content-Type": "application/json" }
569
+ });
570
+ } catch (error) {
571
+ console.error(`API Error [${method} ${path}]:`, error);
572
+ return Router.INTERNAL_ERROR_RESPONSE;
573
+ }
574
+ }
575
+ getContextFromPool(request, params, url) {
576
+ if (this.contextPool.length > 0) {
577
+ const context = this.contextPool.pop();
578
+ context.request = request;
579
+ context.params = params;
580
+ context.query = new URLSearchParams(url.split("?")[1] || "");
581
+ context.state = {};
582
+ return context;
583
+ }
584
+ return {
585
+ request,
586
+ params,
587
+ query: new URLSearchParams(url.split("?")[1] || ""),
588
+ state: {}
589
+ };
590
+ }
591
+ returnContextToPool(context) {
592
+ if (this.contextPool.length < 100) {
593
+ this.contextPool.push(context);
594
+ }
595
+ }
596
+ getMinimalApiContext(request, path, params = {}) {
597
+ return {
598
+ request,
599
+ params,
600
+ query: new URLSearchParams(request.url.split("?")[1] || ""),
601
+ state: {}
602
+ };
603
+ }
604
+ async handleStaticFile(path) {
605
+ if (path.endsWith(".css")) {
606
+ const cssPath = this.config.tailwind?.cssPath || "./styles.css";
607
+ try {
608
+ const cssFile = Bun.file(cssPath);
609
+ if (await cssFile.exists()) {
610
+ return new Response(cssFile, {
611
+ headers: {
612
+ "Content-Type": "text/css",
613
+ "Cache-Control": "public, max-age=3600"
614
+ }
615
+ });
616
+ }
617
+ } catch (error) {}
618
+ }
619
+ if (path === "/favicon.ico") {
620
+ return new Response(null, { status: 204 });
621
+ }
622
+ if (path === "/_kilat/live-reload") {
623
+ const serverId = this.serverId;
624
+ return new Response(new ReadableStream({
625
+ start(controller) {
626
+ controller.enqueue(`data: ${serverId}
627
+
628
+ `);
629
+ }
630
+ }), {
631
+ headers: {
632
+ "Content-Type": "text/event-stream",
633
+ "Cache-Control": "no-cache",
634
+ Connection: "keep-alive"
635
+ }
636
+ });
637
+ }
638
+ return null;
639
+ }
640
+ create404Response() {
641
+ const html = `<!DOCTYPE html>
642
+ <html lang="en">
643
+ <head>
644
+ <title>404 - Page Not Found</title>
645
+ <meta charset="utf-8" />
646
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
647
+ <link rel="stylesheet" href="/styles.css" />
648
+ </head>
649
+ <body>
650
+ <div id="root">
651
+ <div class="container">
652
+ <h1>404 - Page Not Found</h1>
653
+ <p>The page you're looking for doesn't exist.</p>
654
+ <a href="/">Go back home</a>
655
+ </div>
656
+ </div>
657
+ </body>
658
+ </html>`;
659
+ return new Response(html, {
660
+ status: 404,
661
+ headers: { "Content-Type": "text/html; charset=utf-8" }
662
+ });
663
+ }
664
+ async renderPage(PageComponent, props, uiFramework = "react", meta = {}) {
665
+ switch (uiFramework) {
666
+ case "react":
667
+ const reactContent = await ReactAdapter.renderToString(PageComponent, props);
668
+ return ReactAdapter.createDocument(reactContent, meta, this.config);
669
+ case "htmx":
670
+ const htmxContent = await HTMXAdapter.renderToString(PageComponent, props);
671
+ return HTMXAdapter.createDocument(htmxContent, meta, this.config);
672
+ default:
673
+ throw new Error(`Unsupported UI framework: ${uiFramework}`);
674
+ }
675
+ }
676
+ }
677
+ // src/server/server.ts
678
+ class KilatServer {
679
+ router;
680
+ config;
681
+ constructor(config) {
682
+ this.config = config;
683
+ this.router = new Router(config);
684
+ }
685
+ async start() {
686
+ if (this.config.tailwind?.enabled) {
687
+ console.log("\uD83C\uDFA8 Building Tailwind CSS...");
688
+ await this.runTailwind(false);
689
+ if (this.config.dev) {
690
+ this.runTailwind(true);
691
+ }
692
+ }
693
+ await this.router.loadRoutes();
694
+ const router = this.router;
695
+ const config = this.config;
696
+ const server = Bun.serve({
697
+ port: config.port || 3000,
698
+ hostname: config.hostname || "localhost",
699
+ async fetch(request) {
700
+ return router.handleRequest(request);
701
+ }
702
+ });
703
+ console.log(`
704
+ \uD83D\uDE80 KilatJS Server Running!
705
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━
706
+ Local: http://${server.hostname}:${server.port}
707
+ Mode: ${config.dev ? "Development" : "Production"}
708
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━
709
+ `);
710
+ if (config.dev) {
711
+ console.log("\uD83D\uDCC1 Using FileSystemRouter for dynamic route discovery");
712
+ console.log("");
713
+ }
714
+ return server;
715
+ }
716
+ async buildStatic() {
717
+ if (this.config.tailwind?.enabled) {
718
+ await this.runTailwind(false);
719
+ }
720
+ await this.router.loadRoutes();
721
+ console.log(`\uD83D\uDD28 Building for hybrid deployment...
722
+ `);
723
+ await this.ensureDir(this.config.outDir);
724
+ console.log("\uD83D\uDCC4 Static build with FileSystemRouter:");
725
+ console.log("─────────────────────────────────────");
726
+ console.log(" Using Bun's built-in FileSystemRouter for dynamic routing");
727
+ console.log(" Static assets will be copied to output directory");
728
+ await this.copyStaticAssets();
729
+ await this.generateProductionServer();
730
+ console.log(`
731
+ ✅ Hybrid Build Complete!
732
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
733
+ Using Bun FileSystemRouter for all routes
734
+ Output: ${import.meta.dir}/../${this.config.outDir}
735
+
736
+ Run 'bun dist/server.js' to start the server.
737
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
738
+ `);
739
+ }
740
+ async generateProductionServer() {
741
+ console.log(`
742
+ ⚙️ Generating production server with FileSystemRouter...`);
743
+ const srcDir = this.config.routesDir.replace("/routes", "");
744
+ const srcOutDir = `${this.config.outDir}/src`;
745
+ await this.copyDir(srcDir, srcOutDir);
746
+ console.log(` ✓ src/ (copied with all components and routes)`);
747
+ const serverCode = `#!/usr/bin/env bun
748
+ /**
749
+ * KilatJS Production Server with FileSystemRouter
750
+ * Generated at: ${new Date().toISOString()}
751
+ *
752
+ * Uses Bun's built-in FileSystemRouter for optimal performance
753
+ */
754
+
755
+ const PORT = process.env.PORT || ${this.config.port || 3000};
756
+ const HOST = process.env.HOST || "${this.config.hostname || "localhost"}";
757
+ const ROOT = import.meta.dir;
758
+
759
+ // Initialize FileSystemRouter
760
+ const fsRouter = new Bun.FileSystemRouter({
761
+ dir: ROOT + "/src/routes",
762
+ style: "nextjs",
763
+ origin: \`http://\${HOST}:\${PORT}\`,
764
+ });
765
+
766
+ function getRouteType(pathname) {
767
+ if (pathname.startsWith("/api")) return "api";
768
+ if (pathname.includes("[") || pathname.includes(":")) return "dynamic";
769
+ return "static";
770
+ }
771
+
772
+ // React SSR support
773
+ let React, ReactDOMServer;
774
+ try {
775
+ React = await import("react");
776
+ ReactDOMServer = await import("react-dom/server");
777
+ } catch (e) {
778
+ console.warn("React not available for SSR");
779
+ }
780
+
781
+ async function renderPage(Component, props, meta = {}) {
782
+ if (!ReactDOMServer || !React) {
783
+ return "<html><body>React not available for SSR</body></html>";
784
+ }
785
+
786
+ const content = ReactDOMServer.renderToString(React.createElement(Component, props));
787
+ return \`<!DOCTYPE html>
788
+ <html lang="en">
789
+ <head>
790
+ <meta charset="utf-8" />
791
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
792
+ <title>\${meta.title || "KilatJS App"}</title>
793
+ \${meta.description ? \`<meta name="description" content="\${meta.description}" />\` : ""}
794
+ <link rel="stylesheet" href="/styles.css" />
795
+ </head>
796
+ <body>
797
+ <div id="root">\${content}</div>
798
+ </body>
799
+ </html>\`;
800
+ }
801
+
802
+ console.log(\`
803
+ \uD83D\uDE80 KilatJS Production Server (FileSystemRouter)
804
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
805
+ http://\${HOST}:\${PORT}
806
+ Using Bun FileSystemRouter for optimal performance
807
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
808
+ \`);
809
+
810
+ Bun.serve({
811
+ port: PORT,
812
+ hostname: HOST,
813
+ async fetch(req) {
814
+ const url = new URL(req.url);
815
+ const path = url.pathname;
816
+
817
+ // 1. Try static files first
818
+ const staticFile = Bun.file(ROOT + (path === "/" ? "/index.html" : path));
819
+ if (await staticFile.exists()) {
820
+ return new Response(staticFile);
821
+ }
822
+
823
+ // 2. Try index.html for directories
824
+ const indexFile = Bun.file(ROOT + path + "/index.html");
825
+ if (await indexFile.exists()) {
826
+ return new Response(indexFile);
827
+ }
828
+
829
+ // 3. Use FileSystemRouter for dynamic routes
830
+ const match = fsRouter.match(path);
831
+ if (!match) {
832
+ console.log(\`No route match found for: \${path}\`);
833
+ return new Response("404 Not Found", { status: 404 });
834
+ }
835
+
836
+ console.log(\`Route matched: \${path} -> \${match.filePath}\`);
837
+
838
+ try {
839
+ // Dynamically import the route
840
+ const routeExports = await import(match.filePath);
841
+ const routeType = getRouteType(match.pathname);
842
+ const params = match.params || {};
843
+ const ctx = {
844
+ request: req,
845
+ params,
846
+ query: url.searchParams,
847
+ state: {}
848
+ };
849
+
850
+ // API routes
851
+ if (routeType === "api") {
852
+ const handler = routeExports[req.method] || routeExports.default;
853
+ if (!handler) {
854
+ return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
855
+ status: 405,
856
+ headers: { "Content-Type": "application/json" }
857
+ });
858
+ }
859
+
860
+ const result = await handler(ctx);
861
+ if (result instanceof Response) return result;
862
+
863
+ return new Response(JSON.stringify(result), {
864
+ headers: { "Content-Type": "application/json" }
865
+ });
866
+ }
867
+
868
+ // Page routes - handle HTTP methods
869
+ if (req.method !== "GET" && req.method !== "HEAD") {
870
+ const handler = routeExports[req.method];
871
+ if (handler) return await handler(ctx);
872
+ return new Response("Method Not Allowed", { status: 405 });
873
+ }
874
+
875
+ // GET requests - SSR
876
+ const data = routeExports.load ? await routeExports.load(ctx) : {};
877
+ if (data instanceof Response) return data;
878
+
879
+ const html = await renderPage(
880
+ routeExports.default,
881
+ { data, params, state: {} },
882
+ routeExports.meta || {}
883
+ );
884
+
885
+ const cacheControl = routeType === "dynamic" ? "no-cache" : "public, max-age=3600";
886
+
887
+ return new Response(html, {
888
+ headers: {
889
+ "Content-Type": "text/html; charset=utf-8",
890
+ "Cache-Control": cacheControl
891
+ }
892
+ });
893
+
894
+ } catch (error) {
895
+ if (error instanceof Response) return error;
896
+ console.error("Route error:", error);
897
+ return new Response("Internal Server Error", { status: 500 });
898
+ }
899
+ }
900
+ });
901
+ `;
902
+ const serverPath = `${this.config.outDir}/server.js`;
903
+ await Bun.write(serverPath, serverCode);
904
+ console.log(` ✓ server.js (FileSystemRouter-based)`);
905
+ }
906
+ formatSize(bytes) {
907
+ if (bytes < 1024)
908
+ return `${bytes} B`;
909
+ if (bytes < 1024 * 1024)
910
+ return `${(bytes / 1024).toFixed(1)} KB`;
911
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
912
+ }
913
+ async runTailwind(watch) {
914
+ const { inputPath, cssPath } = this.config.tailwind || {};
915
+ if (!inputPath || !cssPath) {
916
+ console.warn("⚠️ Tailwind enabled but inputPath or cssPath missing");
917
+ return;
918
+ }
919
+ const resolvedInputPath = inputPath.startsWith("/") ? inputPath : `${process.cwd()}/${inputPath}`;
920
+ const resolvedCssPath = cssPath.startsWith("/") ? cssPath : `${process.cwd()}/${cssPath}`;
921
+ const args = ["bunx", "@tailwindcss/cli", "-i", resolvedInputPath, "-o", resolvedCssPath];
922
+ if (watch) {
923
+ args.push("--watch");
924
+ }
925
+ console.log(`\uD83C\uDFA8 ${watch ? "Watching" : "Building"} Tailwind CSS...`);
926
+ try {
927
+ const proc = Bun.spawn(args, {
928
+ stdout: "inherit",
929
+ stderr: "inherit",
930
+ cwd: process.cwd()
931
+ });
932
+ if (!watch) {
933
+ await proc.exited;
934
+ }
935
+ } catch (error) {
936
+ console.error("Failed to run Tailwind:", error);
937
+ }
938
+ }
939
+ async copyStaticAssets() {
940
+ console.log(`
941
+ \uD83D\uDCE6 Copying static assets...`);
942
+ const cssPath = this.config.tailwind?.cssPath || "./styles.css";
943
+ const cssFile = Bun.file(cssPath);
944
+ if (await cssFile.exists()) {
945
+ const outputCssPath = `${this.config.outDir}/styles.css`;
946
+ await Bun.write(outputCssPath, cssFile);
947
+ console.log(` ✓ styles.css`);
948
+ }
949
+ if (this.config.publicDir) {
950
+ const publicFile = Bun.file(this.config.publicDir);
951
+ if (await publicFile.exists()) {
952
+ await this.copyDir(this.config.publicDir, this.config.outDir);
953
+ console.log(` ✓ public assets`);
954
+ }
955
+ }
956
+ }
957
+ async copyDir(src, dest) {
958
+ try {
959
+ const proc = Bun.spawn(["cp", "-r", src, dest], {
960
+ stdout: "pipe",
961
+ stderr: "pipe"
962
+ });
963
+ await proc.exited;
964
+ } catch (error) {
965
+ console.warn(`Failed to copy ${src} to ${dest}:`, error);
966
+ }
967
+ }
968
+ async ensureDir(dir) {
969
+ try {
970
+ const proc = Bun.spawn(["mkdir", "-p", dir], {
971
+ stdout: "pipe",
972
+ stderr: "pipe"
973
+ });
974
+ await proc.exited;
975
+ } catch (error) {
976
+ console.warn(`Failed to create directory ${dir}:`, error);
977
+ }
978
+ }
979
+ }
980
+ // src/index.ts
981
+ var defaultConfig = {
982
+ routesDir: "./routes",
983
+ outDir: "./dist",
984
+ port: 3000,
985
+ hostname: "localhost",
986
+ dev: false,
987
+ tailwind: {
988
+ enabled: false,
989
+ cssPath: "./styles.css"
990
+ }
991
+ };
992
+ function createKilat(config = {}) {
993
+ const tailwindConfig = {
994
+ enabled: config.tailwind?.enabled ?? defaultConfig.tailwind?.enabled ?? false,
995
+ inputPath: config.tailwind?.inputPath,
996
+ cssPath: config.tailwind?.cssPath ?? defaultConfig.tailwind?.cssPath ?? "./styles.css",
997
+ configPath: config.tailwind?.configPath ?? defaultConfig.tailwind?.configPath
998
+ };
999
+ const finalConfig = {
1000
+ ...defaultConfig,
1001
+ ...config,
1002
+ tailwind: tailwindConfig
1003
+ };
1004
+ return new KilatServer(finalConfig);
1005
+ }
1006
+ async function startDevServer(config = {}) {
1007
+ const server = createKilat({ ...config, dev: true });
1008
+ return server.start();
1009
+ }
1010
+ async function buildStatic(config = {}) {
1011
+ const server = createKilat(config);
1012
+ return server.buildStatic();
1013
+ }
1014
+ function defineLoader(loader) {
1015
+ return loader;
1016
+ }
1017
+ function defineConfig(config) {
1018
+ return config;
1019
+ }
1020
+ function defineMeta(meta) {
1021
+ return meta;
1022
+ }
1023
+ var Kilat = {
1024
+ createKilat,
1025
+ startDevServer,
1026
+ buildStatic,
1027
+ defineConfig,
1028
+ defineLoader,
1029
+ defineMeta,
1030
+ defaultConfig
1031
+ };
1032
+ var src_default = Kilat;
1033
+ export {
1034
+ startDevServer,
1035
+ defineMeta,
1036
+ defineLoader,
1037
+ defineConfig,
1038
+ defaultConfig,
1039
+ src_default as default,
1040
+ createKilat,
1041
+ buildStatic,
1042
+ Router,
1043
+ ReactAdapter,
1044
+ KilatServer,
1045
+ HTMXAdapter
1046
+ };