kilatjs 0.1.4 → 0.1.6

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 CHANGED
@@ -321,6 +321,41 @@ export async function POST(ctx: RouteContext) {
321
321
  headers: { "Content-Type": "application/json" },
322
322
  });
323
323
  }
324
+
325
+ /**
326
+ * NEW: Standalone API Mode with Swagger Documentation
327
+ * KilatJS provides a dedicated core for building pure APIs.
328
+ */
329
+
330
+ // example/server.ts
331
+ import KilatJS, { swagger, RouteContext } from "kilatjs/core";
332
+
333
+ const app = new KilatJS();
334
+
335
+ // Register Swagger documentation
336
+ app.use(
337
+ swagger(app, {
338
+ path: "/docs",
339
+ title: "KilatJS API",
340
+ description: "Automatic API documentation",
341
+ })
342
+ );
343
+
344
+ // Route with metadata for Swagger
345
+ app.post(
346
+ "/api/echo",
347
+ {
348
+ summary: "Echo request",
349
+ body: { text: "string" }, // Optional: for documentation
350
+ response: { echo: {}, receivedAt: "string" }, // Optional: for documentation
351
+ },
352
+ async (ctx: RouteContext) => {
353
+ const body = await ctx.request.json();
354
+ return { echo: body, receivedAt: new Date() };
355
+ }
356
+ );
357
+
358
+ app.listen(4000);
324
359
  ```
325
360
 
326
361
  ### Step 5: Create Contact Form with PRG Pattern
@@ -614,6 +649,46 @@ KilatJS uniquely allows you to mix **HTML-First SSR** with **Full React CSR**:
614
649
 
615
650
  ---
616
651
 
652
+ ## ⚡ Standalone API & Documentation
653
+
654
+ KilatJS provides a dedicated core for building pure JSON APIs with automatic documentation.
655
+
656
+ ### Features
657
+
658
+ 1. **Automatic Swagger Documentation**: Use the `swagger` middleware to generate interactive documentation.
659
+ 2. **Typed Routes**: Use `RouteOptions` to provide metadata, request bodies, and response structures.
660
+ 3. **Core API**: Use the lightweight `KilatJS` core for JSON-first services.
661
+
662
+ ### Example
663
+
664
+ ```ts
665
+ import KilatJS, { swagger, RouteContext } from "kilatjs/core";
666
+
667
+ const app = new KilatJS();
668
+
669
+ app.use(
670
+ swagger(app, {
671
+ path: "/docs",
672
+ title: "My API Documentation",
673
+ })
674
+ );
675
+
676
+ app.get(
677
+ "/api/users",
678
+ {
679
+ summary: "List users",
680
+ response: { users: [] },
681
+ },
682
+ (ctx) => {
683
+ return { users: ["Alice", "Bob"] };
684
+ }
685
+ );
686
+
687
+ app.listen(4000);
688
+ ```
689
+
690
+ ---
691
+
617
692
  ## 📝 License
618
693
 
619
694
  MIT
package/dist/cli.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env bun
2
- // @bun
3
2
  var __create = Object.create;
4
3
  var __getProtoOf = Object.getPrototypeOf;
5
4
  var __defProp = Object.defineProperty;
@@ -34,7 +33,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
34
33
  throw Error('Dynamic require of "' + x + '" is not supported');
35
34
  });
36
35
 
37
- // src/server/live-reload.ts
36
+ // server/live-reload.ts
38
37
  var exports_live_reload = {};
39
38
  __export(exports_live_reload, {
40
39
  watchDirectory: () => watchDirectory,
@@ -186,7 +185,7 @@ var init_live_reload = __esm(() => {
186
185
  clients = new Set;
187
186
  });
188
187
 
189
- // src/adapters/react.ts
188
+ // adapters/react.ts
190
189
  import { renderToStaticMarkup } from "react-dom/server";
191
190
  import React from "react";
192
191
 
@@ -282,7 +281,7 @@ var init_react = __esm(() => {
282
281
  init_live_reload();
283
282
  });
284
283
 
285
- // src/adapters/htmx.ts
284
+ // adapters/htmx.ts
286
285
  class HTMXAdapter {
287
286
  static async renderToString(template, props) {
288
287
  try {
@@ -438,7 +437,7 @@ var init_htmx = __esm(() => {
438
437
  init_live_reload();
439
438
  });
440
439
 
441
- // src/core/router.ts
440
+ // core/router.ts
442
441
  var Router;
443
442
  var init_router = __esm(() => {
444
443
  init_react();
@@ -452,6 +451,7 @@ var init_router = __esm(() => {
452
451
  preloadedRoutes = new Map;
453
452
  routePatterns = [];
454
453
  apiRoutes = new Map;
454
+ standaloneMiddleware = null;
455
455
  static NOT_FOUND_RESPONSE = new Response("404 Not Found", { status: 404 });
456
456
  static METHOD_NOT_ALLOWED_RESPONSE = new Response(JSON.stringify({ error: "Method Not Allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
457
457
  static INTERNAL_ERROR_RESPONSE = new Response(JSON.stringify({ error: "Internal Server Error" }), { status: 500, headers: { "Content-Type": "application/json" } });
@@ -477,11 +477,40 @@ var init_router = __esm(() => {
477
477
  this.routePatterns.length = 0;
478
478
  this.apiRoutes.clear();
479
479
  this.staticHtmlFiles.clear();
480
+ this.standaloneMiddleware = null;
481
+ await this.loadMiddlewareFile();
480
482
  await this.preloadAllRoutes(silent);
481
483
  if (!silent) {
482
484
  console.log("\uD83D\uDD04 FileSystemRouter initialized with", this.preloadedRoutes.size, "routes");
483
485
  }
484
486
  }
487
+ async loadMiddlewareFile() {
488
+ const filenames = ["middleware.ts", "middleware.js"];
489
+ const possibleDirs = [
490
+ this.config.appDir,
491
+ process.cwd()
492
+ ];
493
+ for (const dir of possibleDirs) {
494
+ for (const filename of filenames) {
495
+ let filePath = `${dir}/${filename}`.replace("//", "/");
496
+ if (!filePath.startsWith("/")) {
497
+ filePath = `${process.cwd()}/${filePath}`;
498
+ }
499
+ const file = Bun.file(filePath);
500
+ if (await file.exists()) {
501
+ try {
502
+ const module = await import(filePath);
503
+ if (module.middleware) {
504
+ this.standaloneMiddleware = module;
505
+ return;
506
+ }
507
+ } catch (e) {
508
+ console.warn(`Failed to load middleware from ${filePath}:`, e);
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
485
514
  async preloadAllRoutes(silent = false) {
486
515
  await this.scanAndPreloadRoutes(this.config.routesDir, "", silent);
487
516
  }
@@ -600,11 +629,65 @@ var init_router = __esm(() => {
600
629
  this.routeCache.set(path, match);
601
630
  return match;
602
631
  }
632
+ async executeMiddlewares(middlewares, context, handler) {
633
+ let index = -1;
634
+ const next = async () => {
635
+ index++;
636
+ if (index < middlewares.length) {
637
+ try {
638
+ return await middlewares[index](context, next);
639
+ } catch (error) {
640
+ console.error("Middleware Error:", error);
641
+ return Router.INTERNAL_ERROR_RESPONSE;
642
+ }
643
+ }
644
+ return await handler();
645
+ };
646
+ return await next();
647
+ }
648
+ shouldRunStandaloneMiddleware(path) {
649
+ if (!this.standaloneMiddleware || !this.standaloneMiddleware.middleware)
650
+ return false;
651
+ const config = this.standaloneMiddleware.config;
652
+ if (!config || !config.matcher)
653
+ return true;
654
+ const matchers = Array.isArray(config.matcher) ? config.matcher : [config.matcher];
655
+ for (const matcher of matchers) {
656
+ if (matcher === path)
657
+ return true;
658
+ if (matcher.endsWith("*")) {
659
+ const prefix = matcher.slice(0, -1);
660
+ if (path.startsWith(prefix))
661
+ return true;
662
+ }
663
+ if (matcher.includes(":")) {
664
+ const pattern = matcher.replace(/:[^\/]+/g, "[^/]+");
665
+ const regex = new RegExp(`^${pattern}$`);
666
+ if (regex.test(path))
667
+ return true;
668
+ }
669
+ }
670
+ return false;
671
+ }
603
672
  async handleRequest(request) {
604
673
  const url = request.url;
605
674
  const pathStart = url.indexOf("/", 8);
606
675
  const pathEnd = url.indexOf("?", pathStart);
607
676
  const path = pathEnd === -1 ? url.slice(pathStart) : url.slice(pathStart, pathEnd);
677
+ if (this.shouldRunStandaloneMiddleware(path)) {
678
+ const context = {
679
+ request,
680
+ params: {},
681
+ query: new URLSearchParams(url.split("?")[1] || ""),
682
+ state: {}
683
+ };
684
+ return await this.standaloneMiddleware.middleware(context, async () => {
685
+ return await this.internalDispatch(request, path, url);
686
+ });
687
+ }
688
+ return await this.internalDispatch(request, path, url);
689
+ }
690
+ async internalDispatch(request, path, url) {
608
691
  if (path.startsWith("/api/")) {
609
692
  return this.handleApiRouteFast(request, path);
610
693
  }
@@ -658,44 +741,57 @@ var init_router = __esm(() => {
658
741
  }
659
742
  const { params, exports, routeType } = match;
660
743
  const context = this.getContextFromPool(request, params, url);
661
- if (request.method !== "GET" && request.method !== "HEAD") {
662
- const actionHandler = exports[request.method];
663
- if (actionHandler && typeof actionHandler === "function") {
664
- try {
665
- const result = await actionHandler(context);
666
- this.returnContextToPool(context);
667
- return result;
668
- } catch (error) {
669
- this.returnContextToPool(context);
670
- console.error(`Error handling ${request.method}:`, error);
671
- return Router.INTERNAL_ERROR_RESPONSE;
744
+ const middlewares = [
745
+ ...this.config.middlewares || [],
746
+ ...exports.middlewares || []
747
+ ];
748
+ const handler = async () => {
749
+ if (request.method !== "GET" && request.method !== "HEAD") {
750
+ const actionHandler = exports[request.method];
751
+ if (actionHandler && typeof actionHandler === "function") {
752
+ try {
753
+ return await actionHandler(context);
754
+ } catch (error) {
755
+ console.error(`Error handling ${request.method}:`, error);
756
+ return Router.INTERNAL_ERROR_RESPONSE;
757
+ }
672
758
  }
759
+ return Router.METHOD_NOT_ALLOWED_RESPONSE;
673
760
  }
674
- this.returnContextToPool(context);
675
- return Router.METHOD_NOT_ALLOWED_RESPONSE;
676
- }
761
+ try {
762
+ const data = exports.load ? await exports.load(context) : {};
763
+ if (data instanceof Response) {
764
+ return data;
765
+ }
766
+ const meta = exports.meta || {};
767
+ const html = await this.renderPage(exports.default, { data, params, state: context.state }, exports.ui, meta, { clientScript: exports.clientScript });
768
+ const cacheControl = routeType === "dynamic" ? "no-cache" : "public, max-age=3600";
769
+ return new Response(html, {
770
+ headers: {
771
+ "Content-Type": "text/html; charset=utf-8",
772
+ "Cache-Control": cacheControl
773
+ }
774
+ });
775
+ } catch (error) {
776
+ if (error instanceof Response)
777
+ return error;
778
+ console.error("Error rendering page:", error);
779
+ return Router.INTERNAL_ERROR_RESPONSE;
780
+ }
781
+ };
677
782
  try {
678
- const data = exports.load ? await exports.load(context) : {};
679
- if (data instanceof Response) {
783
+ if (middlewares.length > 0) {
784
+ const response = await this.executeMiddlewares(middlewares, context, handler);
785
+ this.returnContextToPool(context);
786
+ return response;
787
+ } else {
788
+ const response = await handler();
680
789
  this.returnContextToPool(context);
681
- return data;
790
+ return response;
682
791
  }
683
- const meta = exports.meta || {};
684
- const html = await this.renderPage(exports.default, { data, params, state: context.state }, exports.ui, meta, { clientScript: exports.clientScript });
685
- const cacheControl = routeType === "dynamic" ? "no-cache" : "public, max-age=3600";
686
- this.returnContextToPool(context);
687
- return new Response(html, {
688
- headers: {
689
- "Content-Type": "text/html; charset=utf-8",
690
- "Cache-Control": cacheControl
691
- }
692
- });
693
792
  } catch (error) {
694
793
  this.returnContextToPool(context);
695
- if (error instanceof Response)
696
- return error;
697
- console.error("Error rendering page:", error);
698
- return Router.INTERNAL_ERROR_RESPONSE;
794
+ throw error;
699
795
  }
700
796
  }
701
797
  async handleApiRouteFast(request, path) {
@@ -723,21 +819,32 @@ var init_router = __esm(() => {
723
819
  }
724
820
  if (!exports)
725
821
  return Router.NOT_FOUND_RESPONSE;
726
- const handler = exports[request.method] || exports.default;
727
- if (!handler || typeof handler !== "function") {
728
- return Router.METHOD_NOT_ALLOWED_RESPONSE;
729
- }
730
- try {
731
- const context = this.getMinimalApiContext(request, params);
732
- const response = await handler(context);
733
- if (response instanceof Response)
734
- return response;
735
- return new Response(JSON.stringify(response), {
736
- headers: { "Content-Type": "application/json" }
737
- });
738
- } catch (error) {
739
- console.error(`API Error [${request.method} ${path}]:`, error);
740
- return Router.INTERNAL_ERROR_RESPONSE;
822
+ const context = this.getMinimalApiContext(request, params);
823
+ const middlewares = [
824
+ ...this.config.middlewares || [],
825
+ ...exports.middlewares || []
826
+ ];
827
+ const handler = async () => {
828
+ const apiHandler = exports[request.method] || exports.default;
829
+ if (!apiHandler || typeof apiHandler !== "function") {
830
+ return Router.METHOD_NOT_ALLOWED_RESPONSE;
831
+ }
832
+ try {
833
+ const response = await apiHandler(context);
834
+ if (response instanceof Response)
835
+ return response;
836
+ return new Response(JSON.stringify(response), {
837
+ headers: { "Content-Type": "application/json" }
838
+ });
839
+ } catch (error) {
840
+ console.error(`API Error [${request.method} ${path}]:`, error);
841
+ return Router.INTERNAL_ERROR_RESPONSE;
842
+ }
843
+ };
844
+ if (middlewares.length > 0) {
845
+ return await this.executeMiddlewares(middlewares, context, handler);
846
+ } else {
847
+ return await handler();
741
848
  }
742
849
  }
743
850
  getContextFromPool(request, params, url) {
@@ -805,7 +912,6 @@ var init_router = __esm(() => {
805
912
  }
806
913
  };
807
914
  });
808
-
809
915
  // node:path
810
916
  function assertPath(path) {
811
917
  if (typeof path !== "string")
@@ -1127,7 +1233,7 @@ var init_path = __esm(() => {
1127
1233
  posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
1128
1234
  });
1129
1235
 
1130
- // src/server/server.ts
1236
+ // server/server.ts
1131
1237
  class KilatServer {
1132
1238
  router;
1133
1239
  config;
@@ -1138,7 +1244,7 @@ class KilatServer {
1138
1244
  const { appDir, routesDir } = this.resolvePaths(config);
1139
1245
  this.appDir = appDir;
1140
1246
  this.routesDir = routesDir;
1141
- this.router = new Router({ ...config, routesDir: this.routesDir });
1247
+ this.router = new Router({ ...config, appDir: this.appDir, routesDir: this.routesDir });
1142
1248
  }
1143
1249
  resolvePaths(config) {
1144
1250
  const cwd = process.cwd();
@@ -1274,6 +1380,12 @@ class KilatServer {
1274
1380
  notifyReload(filename);
1275
1381
  });
1276
1382
  }
1383
+ watchDirectory(this.appDir, async (filename) => {
1384
+ if (filename === "middleware.ts" || filename === "middleware.js" || filename === "kilat.config.ts") {
1385
+ await router.loadRoutes(true);
1386
+ notifyReload(filename);
1387
+ }
1388
+ });
1277
1389
  } else {
1278
1390
  console.log(`
1279
1391
  \uD83D\uDE80 KilatJS Production Server
@@ -1634,7 +1746,8 @@ var init_server = __esm(() => {
1634
1746
  init_path();
1635
1747
  init_live_reload();
1636
1748
  });
1637
- // src/index.ts
1749
+
1750
+ // index.ts
1638
1751
  var exports_src = {};
1639
1752
  __export(exports_src, {
1640
1753
  stopLiveReload: () => stopLiveReload,
@@ -2026,7 +2139,7 @@ var init_src = __esm(() => {
2026
2139
  src_default = Kilat;
2027
2140
  });
2028
2141
 
2029
- // src/cli.ts
2142
+ // cli.ts
2030
2143
  var args = process.argv.slice(2);
2031
2144
  var command = args[0];
2032
2145
  var isHotMode = process.env.__KILAT_HOT === "1";
@@ -0,0 +1,5 @@
1
+ export { default as KilatJS, default } from "./kilat";
2
+ export * from "../middleware/cors";
3
+ export * from "../middleware/jwt";
4
+ export * from "../middleware/swagger";
5
+ export * from "./types";