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/dist/index.js CHANGED
@@ -2,18 +2,138 @@
2
2
  import { renderToStaticMarkup } from "react-dom/server";
3
3
  import React from "react";
4
4
 
5
+ // src/server/live-reload.ts
6
+ var clients = new Set;
7
+ var liveReloadServer = null;
8
+ function startLiveReload(port = 35729) {
9
+ if (liveReloadServer)
10
+ return;
11
+ liveReloadServer = Bun.serve({
12
+ port,
13
+ fetch(req, server) {
14
+ if (server.upgrade(req, { data: {} })) {
15
+ return;
16
+ }
17
+ return new Response("Live Reload Server", { status: 200 });
18
+ },
19
+ websocket: {
20
+ open(ws) {
21
+ clients.add(ws);
22
+ },
23
+ close(ws) {
24
+ clients.delete(ws);
25
+ },
26
+ message() {}
27
+ }
28
+ });
29
+ }
30
+ function notifyReload() {
31
+ const message = JSON.stringify({ type: "reload" });
32
+ for (const client of clients) {
33
+ try {
34
+ client.send(message);
35
+ } catch {
36
+ clients.delete(client);
37
+ }
38
+ }
39
+ }
40
+ function getLiveReloadScript(port = 35729) {
41
+ return `
42
+ <script>
43
+ (function() {
44
+ const ws = new WebSocket('ws://localhost:${port}');
45
+ ws.onmessage = function(e) {
46
+ const data = JSON.parse(e.data);
47
+ if (data.type === 'reload') {
48
+ console.log('[KilatJS] Reloading...');
49
+ location.reload();
50
+ }
51
+ };
52
+ ws.onclose = function() {
53
+ console.log('[KilatJS] Live reload disconnected. Reconnecting...');
54
+ setTimeout(function() { location.reload(); }, 1000);
55
+ };
56
+ })();
57
+ </script>`;
58
+ }
59
+ async function watchDirectory(dir, onChange) {
60
+ const fileTimestamps = new Map;
61
+ let timeout = null;
62
+ let lastChange = 0;
63
+ const triggerChange = (filename) => {
64
+ const now = Date.now();
65
+ if (now - lastChange < 100)
66
+ return;
67
+ lastChange = now;
68
+ if (timeout)
69
+ clearTimeout(timeout);
70
+ timeout = setTimeout(() => {
71
+ onChange();
72
+ }, 50);
73
+ };
74
+ async function scanFiles() {
75
+ const timestamps = new Map;
76
+ try {
77
+ const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,css}");
78
+ for await (const file of glob.scan({ cwd: dir, absolute: true })) {
79
+ try {
80
+ const stat = Bun.file(file);
81
+ const lastModified = (await stat.stat())?.mtime?.getTime() || 0;
82
+ timestamps.set(file, lastModified);
83
+ } catch {}
84
+ }
85
+ } catch (error) {
86
+ console.warn(`⚠️ Could not scan directory ${dir}:`, error);
87
+ }
88
+ return timestamps;
89
+ }
90
+ const initialFiles = await scanFiles();
91
+ for (const [file, time] of initialFiles) {
92
+ fileTimestamps.set(file, time);
93
+ }
94
+ const pollInterval = setInterval(async () => {
95
+ const currentFiles = await scanFiles();
96
+ for (const [file, time] of currentFiles) {
97
+ const previousTime = fileTimestamps.get(file);
98
+ if (previousTime === undefined || previousTime < time) {
99
+ fileTimestamps.set(file, time);
100
+ if (previousTime !== undefined) {
101
+ const relativePath = file.replace(dir + "/", "");
102
+ triggerChange(relativePath);
103
+ }
104
+ }
105
+ }
106
+ for (const file of fileTimestamps.keys()) {
107
+ if (!currentFiles.has(file)) {
108
+ fileTimestamps.delete(file);
109
+ const relativePath = file.replace(dir + "/", "");
110
+ triggerChange(relativePath);
111
+ }
112
+ }
113
+ }, 500);
114
+ globalThis.__kilatWatchIntervals = globalThis.__kilatWatchIntervals || [];
115
+ globalThis.__kilatWatchIntervals.push(pollInterval);
116
+ }
117
+ function stopLiveReload() {
118
+ if (liveReloadServer) {
119
+ liveReloadServer.stop();
120
+ liveReloadServer = null;
121
+ }
122
+ clients.clear();
123
+ }
124
+
125
+ // src/adapters/react.ts
5
126
  class ReactAdapter {
6
127
  static async renderToString(component, props) {
7
128
  try {
8
129
  const element = React.createElement(component, props);
9
- const html = renderToStaticMarkup(element);
10
- return html;
130
+ return renderToStaticMarkup(element);
11
131
  } catch (error) {
12
132
  console.error("Error rendering React component:", error);
13
133
  throw error;
14
134
  }
15
135
  }
16
- static createDocument(html, meta = {}, config) {
136
+ static createDocument(html, meta = {}, config, options = {}) {
17
137
  const title = meta.title || "KilatJS App";
18
138
  const description = meta.description || "";
19
139
  const robots = meta.robots || "index,follow";
@@ -70,6 +190,8 @@ class ReactAdapter {
70
190
  `;
71
191
  metaTags += `<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
72
192
  `;
193
+ const liveReloadScript = config?.dev ? getLiveReloadScript() : "";
194
+ const clientScriptTag = options.clientScript ? `<script>(${options.clientScript.toString()})()</script>` : "";
73
195
  return `<!DOCTYPE html>
74
196
  <html lang="en">
75
197
  <head>
@@ -80,34 +202,8 @@ class ReactAdapter {
80
202
  <div id="root">
81
203
  ${html}
82
204
  </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>` : ""}
205
+ ${liveReloadScript}
206
+ ${clientScriptTag}
111
207
  </body>
112
208
  </html>`;
113
209
  }
@@ -260,6 +356,7 @@ class HTMXAdapter {
260
356
  <div id="root">
261
357
  ${html}
262
358
  </div>
359
+ ${config?.dev ? getLiveReloadScript() : ""}
263
360
  </body>
264
361
  </html>`;
265
362
  }
@@ -273,23 +370,19 @@ class Router {
273
370
  routes = new Map;
274
371
  config;
275
372
  staticPaths = new Map;
276
- serverId = Date.now().toString();
277
373
  fsRouter;
278
374
  routeCache = new Map;
279
375
  preloadedRoutes = new Map;
280
376
  routePatterns = [];
281
377
  apiRoutes = new Map;
282
- staticApiResponses = new Map;
283
378
  static NOT_FOUND_RESPONSE = new Response("404 Not Found", { status: 404 });
284
379
  static METHOD_NOT_ALLOWED_RESPONSE = new Response(JSON.stringify({ error: "Method Not Allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
285
380
  static INTERNAL_ERROR_RESPONSE = new Response(JSON.stringify({ error: "Internal Server Error" }), { status: 500, headers: { "Content-Type": "application/json" } });
286
381
  contextPool = [];
287
- poolIndex = 0;
288
382
  constructor(config) {
289
383
  this.config = config;
290
- const routesDir = config.routesDir.startsWith("/") ? config.routesDir : `${process.cwd()}/${config.routesDir}`;
291
384
  this.fsRouter = new Bun.FileSystemRouter({
292
- dir: routesDir,
385
+ dir: config.routesDir,
293
386
  style: "nextjs",
294
387
  origin: `http://${config.hostname || "localhost"}:${config.port || 3000}`
295
388
  });
@@ -300,33 +393,25 @@ class Router {
300
393
  getStaticPaths() {
301
394
  return this.staticPaths;
302
395
  }
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
- }
396
+ async loadRoutes(silent = false) {
397
+ this.fsRouter.reload();
398
+ this.routeCache.clear();
399
+ this.preloadedRoutes.clear();
400
+ this.routePatterns.length = 0;
401
+ this.apiRoutes.clear();
402
+ this.staticHtmlFiles.clear();
403
+ await this.preloadAllRoutes(silent);
404
+ if (!silent) {
405
+ console.log("\uD83D\uDD04 FileSystemRouter initialized with", this.preloadedRoutes.size, "routes");
321
406
  }
322
407
  }
323
- async preloadAllRoutes() {
324
- const routesDir = this.config.routesDir.startsWith("/") ? this.config.routesDir : `${process.cwd()}/${this.config.routesDir}`;
325
- await this.scanAndPreloadRoutes(routesDir, "");
408
+ async preloadAllRoutes(silent = false) {
409
+ await this.scanAndPreloadRoutes(this.config.routesDir, "", silent);
326
410
  }
327
- async scanAndPreloadRoutes(dir, basePath) {
411
+ staticHtmlFiles = new Map;
412
+ async scanAndPreloadRoutes(dir, _basePath, _silent = false) {
328
413
  try {
329
- const proc = Bun.spawn(["find", dir, "-name", "*.ts", "-o", "-name", "*.tsx", "-o", "-name", "*.js", "-o", "-name", "*.jsx"], {
414
+ const proc = Bun.spawn(["find", dir, "-name", "*.ts", "-o", "-name", "*.tsx", "-o", "-name", "*.js", "-o", "-name", "*.jsx", "-o", "-name", "*.html"], {
330
415
  stdout: "pipe"
331
416
  });
332
417
  const output = await new Response(proc.stdout).text();
@@ -334,13 +419,18 @@ class Router {
334
419
  `).filter(Boolean);
335
420
  for (const filePath of files) {
336
421
  const relativePath = filePath.replace(dir, "");
337
- let routePath = relativePath.replace(/\.(tsx?|jsx?)$/, "");
422
+ const isHtml = filePath.endsWith(".html");
423
+ let routePath = relativePath.replace(/\.(tsx?|jsx?|html)$/, "");
338
424
  if (routePath.endsWith("/index")) {
339
425
  routePath = routePath.slice(0, -6) || "/";
340
426
  }
341
427
  if (!routePath.startsWith("/")) {
342
428
  routePath = "/" + routePath;
343
429
  }
430
+ if (isHtml) {
431
+ this.staticHtmlFiles.set(routePath, filePath);
432
+ continue;
433
+ }
344
434
  const routeType = this.getRouteType(routePath);
345
435
  try {
346
436
  const routeExports = await import(filePath);
@@ -367,32 +457,29 @@ class Router {
367
457
  console.warn("Failed to scan routes:", error);
368
458
  }
369
459
  }
460
+ getStaticHtmlFile(path) {
461
+ return this.staticHtmlFiles.get(path);
462
+ }
370
463
  createRoutePattern(routePath) {
371
464
  const paramNames = [];
372
465
  let pattern = routePath.replace(/[.*+?^${}|\\]/g, "\\$&");
373
- pattern = pattern.replace(/\\?\[([^\]]+)\\?\]/g, (match, paramName) => {
466
+ pattern = pattern.replace(/\\?\[([^\]]+)\\?\]/g, (_match, paramName) => {
374
467
  paramNames.push(paramName);
375
468
  return "([^/]+)";
376
469
  });
377
470
  try {
378
471
  const regex = new RegExp(`^${pattern}$`);
379
- console.log(`Created pattern for ${routePath}: ${regex} (params: ${paramNames.join(", ")})`);
380
- return {
381
- regex,
382
- paramNames
383
- };
472
+ return { regex, paramNames };
384
473
  } catch (error) {
385
474
  console.warn(`Failed to create pattern for ${routePath}:`, error);
386
475
  return null;
387
476
  }
388
477
  }
389
478
  getRouteType(pathname) {
390
- if (pathname.startsWith("/api")) {
479
+ if (pathname.startsWith("/api"))
391
480
  return "api";
392
- }
393
- if (pathname.includes("[") || pathname.includes(":")) {
481
+ if (pathname.includes("[") || pathname.includes(":"))
394
482
  return "dynamic";
395
- }
396
483
  return "static";
397
484
  }
398
485
  matchRoute(path) {
@@ -416,8 +503,7 @@ class Router {
416
503
  routePattern.paramNames.forEach((name, index) => {
417
504
  params[name] = regexMatch[index + 1];
418
505
  });
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?)$/, "");
506
+ let routePath = routePattern.filePath.replace(this.config.routesDir, "").replace(/\.(tsx?|jsx?)$/, "");
421
507
  if (!routePath.startsWith("/")) {
422
508
  routePath = "/" + routePath;
423
509
  }
@@ -437,21 +523,6 @@ class Router {
437
523
  this.routeCache.set(path, match);
438
524
  return match;
439
525
  }
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
526
  async handleRequest(request) {
456
527
  const url = request.url;
457
528
  const pathStart = url.indexOf("/", 8);
@@ -460,11 +531,10 @@ class Router {
460
531
  if (path.startsWith("/api/")) {
461
532
  return this.handleApiRouteFast(request, path);
462
533
  }
463
- if (path.endsWith(".css") || path === "/favicon.ico" || path === "/_kilat/live-reload") {
534
+ if (path.endsWith(".css") || path === "/favicon.ico") {
464
535
  const staticResponse = await this.handleStaticFile(path);
465
- if (staticResponse) {
536
+ if (staticResponse)
466
537
  return staticResponse;
467
- }
468
538
  }
469
539
  if (!this.config.dev) {
470
540
  const outDir = this.config.outDir || "./dist";
@@ -473,11 +543,17 @@ class Router {
473
543
  if (await file.exists()) {
474
544
  return new Response(file);
475
545
  }
476
- if (!path.endsWith(".html")) {
477
- const indexFile = Bun.file(`${filePath}/index.html`);
478
- if (await indexFile.exists()) {
479
- return new Response(indexFile);
480
- }
546
+ }
547
+ const htmlFilePath = this.staticHtmlFiles.get(path);
548
+ if (htmlFilePath) {
549
+ const file = Bun.file(htmlFilePath);
550
+ if (await file.exists()) {
551
+ return new Response(file, {
552
+ headers: {
553
+ "Content-Type": "text/html; charset=utf-8",
554
+ "Cache-Control": this.config.dev ? "no-cache" : "public, max-age=3600"
555
+ }
556
+ });
481
557
  }
482
558
  }
483
559
  const match = this.matchRoute(path);
@@ -509,7 +585,7 @@ class Router {
509
585
  return data;
510
586
  }
511
587
  const meta = exports.meta || {};
512
- const html = await this.renderPage(exports.default, { data, params, state: context.state }, exports.ui, meta);
588
+ const html = await this.renderPage(exports.default, { data, params, state: context.state }, exports.ui, meta, { clientScript: exports.clientScript });
513
589
  const cacheControl = routeType === "dynamic" ? "no-cache" : "public, max-age=3600";
514
590
  this.returnContextToPool(context);
515
591
  return new Response(html, {
@@ -520,9 +596,8 @@ class Router {
520
596
  });
521
597
  } catch (error) {
522
598
  this.returnContextToPool(context);
523
- if (error instanceof Response) {
599
+ if (error instanceof Response)
524
600
  return error;
525
- }
526
601
  console.error("Error rendering page:", error);
527
602
  return Router.INTERNAL_ERROR_RESPONSE;
528
603
  }
@@ -539,7 +614,7 @@ class Router {
539
614
  params[name] = regexMatch[index + 1];
540
615
  });
541
616
  for (const [preloadedPath, preloadedExports] of this.preloadedRoutes.entries()) {
542
- if (preloadedPath.includes("[") && preloadedPath.startsWith("/api/") && this.pathMatchesPattern(preloadedPath, path)) {
617
+ if (preloadedPath.includes("[") && preloadedPath.startsWith("/api/")) {
543
618
  exports = preloadedExports;
544
619
  break;
545
620
  }
@@ -550,25 +625,22 @@ class Router {
550
625
  }
551
626
  }
552
627
  }
553
- if (!exports) {
628
+ if (!exports)
554
629
  return Router.NOT_FOUND_RESPONSE;
555
- }
556
- const method = request.method;
557
- const handler = exports[method] || exports.default;
630
+ const handler = exports[request.method] || exports.default;
558
631
  if (!handler || typeof handler !== "function") {
559
632
  return Router.METHOD_NOT_ALLOWED_RESPONSE;
560
633
  }
561
634
  try {
562
- const context = this.getMinimalApiContext(request, path, params);
635
+ const context = this.getMinimalApiContext(request, params);
563
636
  const response = await handler(context);
564
- if (response instanceof Response) {
637
+ if (response instanceof Response)
565
638
  return response;
566
- }
567
639
  return new Response(JSON.stringify(response), {
568
640
  headers: { "Content-Type": "application/json" }
569
641
  });
570
642
  } catch (error) {
571
- console.error(`API Error [${method} ${path}]:`, error);
643
+ console.error(`API Error [${request.method} ${path}]:`, error);
572
644
  return Router.INTERNAL_ERROR_RESPONSE;
573
645
  }
574
646
  }
@@ -593,7 +665,7 @@ class Router {
593
665
  this.contextPool.push(context);
594
666
  }
595
667
  }
596
- getMinimalApiContext(request, path, params = {}) {
668
+ getMinimalApiContext(request, params = {}) {
597
669
  return {
598
670
  request,
599
671
  params,
@@ -610,62 +682,24 @@ class Router {
610
682
  return new Response(cssFile, {
611
683
  headers: {
612
684
  "Content-Type": "text/css",
613
- "Cache-Control": "public, max-age=3600"
685
+ "Cache-Control": this.config.dev ? "no-cache" : "public, max-age=3600"
614
686
  }
615
687
  });
616
688
  }
617
- } catch (error) {}
689
+ } catch {}
618
690
  }
619
691
  if (path === "/favicon.ico") {
620
692
  return new Response(null, { status: 204 });
621
693
  }
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
694
  return null;
639
695
  }
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 = {}) {
696
+ async renderPage(PageComponent, props, uiFramework = "react", meta = {}, options = {}) {
665
697
  switch (uiFramework) {
666
698
  case "react":
667
699
  const reactContent = await ReactAdapter.renderToString(PageComponent, props);
668
- return ReactAdapter.createDocument(reactContent, meta, this.config);
700
+ return ReactAdapter.createDocument(reactContent, meta, this.config, {
701
+ clientScript: options.clientScript
702
+ });
669
703
  case "htmx":
670
704
  const htmxContent = await HTMXAdapter.renderToString(PageComponent, props);
671
705
  return HTMXAdapter.createDocument(htmxContent, meta, this.config);
@@ -678,71 +712,172 @@ class Router {
678
712
  class KilatServer {
679
713
  router;
680
714
  config;
715
+ appDir;
716
+ routesDir;
681
717
  constructor(config) {
682
718
  this.config = config;
683
- this.router = new Router(config);
719
+ const { appDir, routesDir } = this.resolvePaths(config);
720
+ this.appDir = appDir;
721
+ this.routesDir = routesDir;
722
+ this.router = new Router({ ...config, routesDir: this.routesDir });
723
+ }
724
+ resolvePaths(config) {
725
+ const cwd = process.cwd();
726
+ const appDir = config.appDir.startsWith("/") ? config.appDir : `${cwd}/${config.appDir}`;
727
+ const routesPath = `${appDir}/routes`;
728
+ const pagesPath = `${appDir}/pages`;
729
+ const checkRoutes = Bun.spawnSync(["test", "-d", routesPath]);
730
+ if (checkRoutes.exitCode === 0) {
731
+ return { appDir, routesDir: routesPath };
732
+ }
733
+ const checkPages = Bun.spawnSync(["test", "-d", pagesPath]);
734
+ if (checkPages.exitCode === 0) {
735
+ return { appDir, routesDir: pagesPath };
736
+ }
737
+ console.warn(`⚠️ No routes/ or pages/ folder found in ${appDir}, defaulting to routes/`);
738
+ return { appDir, routesDir: routesPath };
684
739
  }
685
740
  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);
741
+ const config = this.config;
742
+ const router = this.router;
743
+ if (config.tailwind?.enabled) {
744
+ await this.runTailwind(false, config.dev);
745
+ if (config.dev) {
746
+ this.runTailwind(true, true);
691
747
  }
692
748
  }
693
- await this.router.loadRoutes();
694
- const router = this.router;
695
- const config = this.config;
749
+ await router.loadRoutes(config.dev);
750
+ const routes = {};
751
+ const cssPath = config.tailwind?.cssPath || "./styles.css";
752
+ routes["/styles.css"] = async () => {
753
+ const file = Bun.file(cssPath);
754
+ if (await file.exists()) {
755
+ return new Response(file, {
756
+ headers: { "Content-Type": "text/css" }
757
+ });
758
+ }
759
+ return new Response("", { headers: { "Content-Type": "text/css" } });
760
+ };
696
761
  const server = Bun.serve({
697
762
  port: config.port || 3000,
698
763
  hostname: config.hostname || "localhost",
764
+ development: config.dev ? {
765
+ hmr: true,
766
+ console: true
767
+ } : false,
768
+ routes,
699
769
  async fetch(request) {
700
770
  return router.handleRequest(request);
701
771
  }
702
772
  });
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
773
  if (config.dev) {
711
- console.log("\uD83D\uDCC1 Using FileSystemRouter for dynamic route discovery");
712
- console.log("");
774
+ console.log(`
775
+ ⚡ KilatJS Dev Server
776
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
777
+ ➜ http://${server.hostname}:${server.port}
778
+ ➜ HMR + Live Reload enabled
779
+ ➜ Edit files and see changes instantly
780
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
781
+ `);
782
+ startLiveReload();
783
+ watchDirectory(this.routesDir, async () => {
784
+ await router.loadRoutes(true);
785
+ notifyReload();
786
+ });
787
+ const componentsDir = `${this.appDir}/components`;
788
+ const checkComponents = Bun.spawnSync(["test", "-d", componentsDir]);
789
+ if (checkComponents.exitCode === 0) {
790
+ watchDirectory(componentsDir, async () => {
791
+ await router.loadRoutes(true);
792
+ notifyReload();
793
+ });
794
+ }
795
+ } else {
796
+ console.log(`
797
+ \uD83D\uDE80 KilatJS Production Server
798
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
799
+ ➜ http://${server.hostname}:${server.port}
800
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
801
+ `);
713
802
  }
714
803
  return server;
715
804
  }
716
805
  async buildStatic() {
806
+ console.log(`
807
+ \uD83D\uDD28 KilatJS Production Build
808
+ `);
717
809
  if (this.config.tailwind?.enabled) {
718
- await this.runTailwind(false);
810
+ await this.runTailwind(false, false);
719
811
  }
720
- await this.router.loadRoutes();
721
- console.log(`\uD83D\uDD28 Building for hybrid deployment...
722
- `);
812
+ await this.router.loadRoutes(true);
723
813
  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");
814
+ await this.displayRouteAnalysis();
728
815
  await this.copyStaticAssets();
729
816
  await this.generateProductionServer();
730
817
  console.log(`
731
- Hybrid Build Complete!
818
+ ✅ Build Complete!
732
819
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
733
- Using Bun FileSystemRouter for all routes
734
- Output: ${import.meta.dir}/../${this.config.outDir}
820
+ Output: ${this.config.outDir}
735
821
 
736
- Run 'bun dist/server.js' to start the server.
822
+ Start: bun ${this.config.outDir}/server.js
737
823
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
824
+ `);
825
+ }
826
+ async displayRouteAnalysis() {
827
+ console.log("\uD83D\uDCC4 Routes Analysis:");
828
+ console.log("─────────────────────────────────────────────────────────");
829
+ const routes = [];
830
+ try {
831
+ const proc = Bun.spawn(["find", this.routesDir, "-name", "*.ts", "-o", "-name", "*.tsx", "-o", "-name", "*.js", "-o", "-name", "*.jsx"], {
832
+ stdout: "pipe"
833
+ });
834
+ const output = await new Response(proc.stdout).text();
835
+ const files = output.trim().split(`
836
+ `).filter(Boolean);
837
+ for (const filePath of files) {
838
+ const relativePath = filePath.replace(this.routesDir, "");
839
+ let routePath = relativePath.replace(/\.(tsx?|jsx?)$/, "");
840
+ if (routePath.endsWith("/index")) {
841
+ routePath = routePath.slice(0, -6) || "/";
842
+ }
843
+ if (!routePath.startsWith("/")) {
844
+ routePath = "/" + routePath;
845
+ }
846
+ let type;
847
+ if (routePath.startsWith("/api")) {
848
+ type = "API";
849
+ } else if (routePath.includes("[")) {
850
+ type = "Dynamic SSR";
851
+ } else {
852
+ type = "SSR";
853
+ }
854
+ const shortFile = relativePath.replace(/^\//, "");
855
+ routes.push({ path: routePath, type, file: shortFile });
856
+ }
857
+ } catch (error) {
858
+ console.warn("Failed to analyze routes:", error);
859
+ }
860
+ routes.sort((a, b) => a.path.localeCompare(b.path));
861
+ const maxPathLen = Math.max(...routes.map((r) => r.path.length), 10);
862
+ const maxTypeLen = Math.max(...routes.map((r) => r.type.length), 6);
863
+ for (const route of routes) {
864
+ const typeIcon = route.type === "API" ? "⚡" : route.type === "Dynamic SSR" ? "\uD83D\uDD04" : "\uD83D\uDCC4";
865
+ const paddedPath = route.path.padEnd(maxPathLen);
866
+ const paddedType = route.type.padEnd(maxTypeLen);
867
+ console.log(` ${typeIcon} ${paddedPath} ${paddedType} ${route.file}`);
868
+ }
869
+ console.log("─────────────────────────────────────────────────────────");
870
+ const apiCount = routes.filter((r) => r.type === "API").length;
871
+ const dynamicCount = routes.filter((r) => r.type === "Dynamic SSR").length;
872
+ const ssrCount = routes.filter((r) => r.type === "SSR").length;
873
+ console.log(` Total: ${routes.length} routes (${ssrCount} SSR, ${dynamicCount} Dynamic, ${apiCount} API)
738
874
  `);
739
875
  }
740
876
  async generateProductionServer() {
741
877
  console.log(`
742
878
  ⚙️ Generating production server with FileSystemRouter...`);
743
- const srcDir = this.config.routesDir.replace("/routes", "");
744
879
  const srcOutDir = `${this.config.outDir}/src`;
745
- await this.copyDir(srcDir, srcOutDir);
880
+ await this.copyDir(this.appDir, srcOutDir);
746
881
  console.log(` ✓ src/ (copied with all components and routes)`);
747
882
  const serverCode = `#!/usr/bin/env bun
748
883
  /**
@@ -833,7 +968,9 @@ Bun.serve({
833
968
  return new Response("404 Not Found", { status: 404 });
834
969
  }
835
970
 
836
- console.log(\`Route matched: \${path} -> \${match.filePath}\`);
971
+ // Use relative path for cleaner logs
972
+ const relativePath = match.filePath.replace(ROOT + "/", "");
973
+ console.log(\`Route matched: \${path} -> \${relativePath}\`);
837
974
 
838
975
  try {
839
976
  // Dynamically import the route
@@ -903,17 +1040,11 @@ Bun.serve({
903
1040
  await Bun.write(serverPath, serverCode);
904
1041
  console.log(` ✓ server.js (FileSystemRouter-based)`);
905
1042
  }
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) {
1043
+ async runTailwind(watch, silent = false) {
914
1044
  const { inputPath, cssPath } = this.config.tailwind || {};
915
1045
  if (!inputPath || !cssPath) {
916
- console.warn("⚠️ Tailwind enabled but inputPath or cssPath missing");
1046
+ if (!silent)
1047
+ console.warn("⚠️ Tailwind enabled but inputPath or cssPath missing");
917
1048
  return;
918
1049
  }
919
1050
  const resolvedInputPath = inputPath.startsWith("/") ? inputPath : `${process.cwd()}/${inputPath}`;
@@ -922,18 +1053,21 @@ Bun.serve({
922
1053
  if (watch) {
923
1054
  args.push("--watch");
924
1055
  }
925
- console.log(`\uD83C\uDFA8 ${watch ? "Watching" : "Building"} Tailwind CSS...`);
1056
+ if (!silent) {
1057
+ console.log(`\uD83C\uDFA8 ${watch ? "Watching" : "Building"} Tailwind CSS...`);
1058
+ }
926
1059
  try {
927
1060
  const proc = Bun.spawn(args, {
928
- stdout: "inherit",
929
- stderr: "inherit",
1061
+ stdout: silent ? "pipe" : "inherit",
1062
+ stderr: silent ? "pipe" : "inherit",
930
1063
  cwd: process.cwd()
931
1064
  });
932
1065
  if (!watch) {
933
1066
  await proc.exited;
934
1067
  }
935
1068
  } catch (error) {
936
- console.error("Failed to run Tailwind:", error);
1069
+ if (!silent)
1070
+ console.error("Failed to run Tailwind:", error);
937
1071
  }
938
1072
  }
939
1073
  async copyStaticAssets() {
@@ -979,7 +1113,7 @@ Bun.serve({
979
1113
  }
980
1114
  // src/index.ts
981
1115
  var defaultConfig = {
982
- routesDir: "./routes",
1116
+ appDir: "./src",
983
1117
  outDir: "./dist",
984
1118
  port: 3000,
985
1119
  hostname: "localhost",
@@ -1020,6 +1154,93 @@ function defineConfig(config) {
1020
1154
  function defineMeta(meta) {
1021
1155
  return meta;
1022
1156
  }
1157
+ async function runCLI(args = process.argv.slice(2)) {
1158
+ const command = args[0];
1159
+ if (!command) {
1160
+ console.error("Please specify a command: dev or build");
1161
+ process.exit(1);
1162
+ }
1163
+ async function loadConfig() {
1164
+ const configPath = `${process.cwd()}/kilat.config.ts`;
1165
+ const file = Bun.file(configPath);
1166
+ if (!await file.exists()) {
1167
+ console.warn("⚠️ No kilat.config.ts found, using defaults");
1168
+ return {};
1169
+ }
1170
+ try {
1171
+ const configModule = await import(configPath);
1172
+ return configModule.default || configModule;
1173
+ } catch (error) {
1174
+ console.error("Failed to load config:", error);
1175
+ return {};
1176
+ }
1177
+ }
1178
+ const config = await loadConfig();
1179
+ switch (command) {
1180
+ case "dev":
1181
+ const isProduction = args.includes("--production");
1182
+ const devServer = createKilat({ ...config, dev: !isProduction });
1183
+ await devServer.start();
1184
+ break;
1185
+ case "build":
1186
+ await buildStatic(config);
1187
+ break;
1188
+ case "serve":
1189
+ const serveOutDir = config.outDir || "./dist";
1190
+ const serverPath = `${process.cwd()}/${serveOutDir}/server.js`;
1191
+ const serverFile = Bun.file(serverPath);
1192
+ if (!await serverFile.exists()) {
1193
+ console.error(`❌ Production server not found at ${serveOutDir}/server.js`);
1194
+ console.error(` Run 'kilat build' first to generate the production build.`);
1195
+ process.exit(1);
1196
+ }
1197
+ const serveProc = Bun.spawn(["bun", serverPath], {
1198
+ stdio: ["inherit", "inherit", "inherit"],
1199
+ cwd: process.cwd()
1200
+ });
1201
+ await serveProc.exited;
1202
+ break;
1203
+ case "preview":
1204
+ const outDir = config.outDir || "./dist";
1205
+ const port = config.port || 3000;
1206
+ const hostname = config.hostname || "localhost";
1207
+ const root = `${process.cwd()}/${outDir}`;
1208
+ console.log(`
1209
+ \uD83D\uDD0D Preview server running:`);
1210
+ console.log(` ➜ http://${hostname}:${port}`);
1211
+ console.log(` Serving: ${outDir}
1212
+ `);
1213
+ Bun.serve({
1214
+ port,
1215
+ hostname,
1216
+ async fetch(req) {
1217
+ const url = new URL(req.url);
1218
+ let path = url.pathname;
1219
+ const filePath = `${root}${path}`;
1220
+ let file = Bun.file(filePath);
1221
+ if (await file.exists()) {
1222
+ return new Response(file);
1223
+ }
1224
+ const indexHtml = `${filePath}/index.html`;
1225
+ file = Bun.file(indexHtml);
1226
+ if (await file.exists()) {
1227
+ return new Response(file);
1228
+ }
1229
+ const rootIndex = `${root}/index.html`;
1230
+ file = Bun.file(rootIndex);
1231
+ if (await file.exists()) {
1232
+ return new Response(file);
1233
+ }
1234
+ return new Response("404 Not Found", { status: 404 });
1235
+ }
1236
+ });
1237
+ break;
1238
+ default:
1239
+ console.error(`Unknown command: ${command}`);
1240
+ console.log("Available commands: dev, build, serve, preview");
1241
+ process.exit(1);
1242
+ }
1243
+ }
1023
1244
  var Kilat = {
1024
1245
  createKilat,
1025
1246
  startDevServer,
@@ -1027,11 +1248,16 @@ var Kilat = {
1027
1248
  defineConfig,
1028
1249
  defineLoader,
1029
1250
  defineMeta,
1251
+ runCLI,
1030
1252
  defaultConfig
1031
1253
  };
1032
1254
  var src_default = Kilat;
1033
1255
  export {
1256
+ stopLiveReload,
1257
+ startLiveReload,
1034
1258
  startDevServer,
1259
+ runCLI,
1260
+ notifyReload,
1035
1261
  defineMeta,
1036
1262
  defineLoader,
1037
1263
  defineConfig,