bini-router 1.0.12 → 1.0.14

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
@@ -352,21 +352,20 @@ server/index.ts
352
352
 
353
353
  ### 🔺 Vercel
354
354
 
355
- ```bash
356
- pnpm add @hono/vercel
357
- ```
355
+ No extra package needed — Vercel runs a Hono app as a direct default export.
358
356
 
359
357
  `vite build` auto-generates `api/index.ts`:
360
358
 
361
359
  ```ts
362
360
  // ⚠️ Auto-generated — do not edit. Add routes in src/app/api/ only.
363
- import { handle } from '@hono/vercel';
364
- import _route0 from './src/app/api/test';
365
- import _route1 from './src/app/api/users';
366
- let app = _route0;
367
- app = app.route('/', _route1);
368
- export const config = { runtime: 'edge' };
369
- export default handle(app);
361
+ import { Hono } from 'hono';
362
+ import _route0 from '../src/app/api/test';
363
+ import _route1 from '../src/app/api/users';
364
+ const app = new Hono();
365
+ app.route('/', _route0);
366
+ app.route('/', _route1);
367
+ export const runtime = 'edge';
368
+ export default app;
370
369
  ```
371
370
 
372
371
  Add `vercel.json`:
package/dist/index.cjs CHANGED
@@ -544,62 +544,115 @@ function scanApiRoutes(dir, baseRoute = "") {
544
544
  }
545
545
  return routes;
546
546
  }
547
- async function buildHonoApp(apiDir, enableCors) {
548
- const { Hono } = await import("hono");
549
- const app = new Hono();
550
- if (enableCors) {
551
- const { cors } = await import("hono/cors");
552
- app.use("*", cors({
553
- origin: "*",
554
- allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
555
- allowHeaders: ["Content-Type", "Authorization"]
556
- }));
557
- }
558
- for (const route of scanApiRoutes(apiDir)) {
559
- const filePath = route.filePath;
560
- app.all(route.routePath, async (c) => {
561
- try {
562
- const { pathToFileURL } = await import("url");
563
- const mod = await import(pathToFileURL(filePath).href + "?t=" + Date.now());
564
- const handler = mod.default;
565
- if (!handler) return c.json({ error: "No default export" }, 500);
566
- if (typeof handler.fetch === "function") {
567
- return await handler.fetch(c.req.raw);
568
- }
569
- if (typeof handler === "function") {
570
- return await handler(c.req.raw);
571
- }
572
- return c.json({
573
- error: "Default export must be a Hono app or async function(req: Request). Do not wrap with handle() or serve() in route files."
574
- }, 500);
575
- } catch (e) {
576
- return c.json({ error: e.message }, 500);
577
- }
578
- });
547
+ function matchRoute(pattern, pathname) {
548
+ const patParts = pattern.split("/").filter(Boolean);
549
+ const urlParts = pathname.split("/").filter(Boolean);
550
+ const isCatchAll = patParts[patParts.length - 1] === "*";
551
+ if (isCatchAll) {
552
+ const prefix = patParts.slice(0, -1);
553
+ if (urlParts.length < prefix.length) return null;
554
+ for (let i = 0; i < prefix.length; i++) {
555
+ if (prefix[i].startsWith(":")) continue;
556
+ if (prefix[i] !== urlParts[i]) return null;
557
+ }
558
+ return { "*": urlParts.slice(prefix.length).join("/") };
559
+ }
560
+ if (patParts.length !== urlParts.length) return null;
561
+ const params = {};
562
+ for (let i = 0; i < patParts.length; i++) {
563
+ if (patParts[i].startsWith(":")) {
564
+ params[patParts[i].slice(1)] = decodeURIComponent(urlParts[i]);
565
+ } else if (patParts[i] !== urlParts[i]) {
566
+ return null;
567
+ }
568
+ }
569
+ return params;
570
+ }
571
+ var moduleCache = /* @__PURE__ */ new Map();
572
+ async function importHandler(filePath) {
573
+ const { pathToFileURL } = await import("url");
574
+ let mtime = 0;
575
+ try {
576
+ mtime = import_fs.default.statSync(filePath).mtimeMs;
577
+ } catch {
579
578
  }
580
- return app;
579
+ const cached = moduleCache.get(filePath);
580
+ if (cached && cached.mtime === mtime) return cached.handler;
581
+ const mod = await import(pathToFileURL(filePath).href + "?t=" + mtime);
582
+ const handler = mod.default ?? null;
583
+ moduleCache.set(filePath, { mtime, handler });
584
+ return handler;
581
585
  }
582
586
  async function handleApiRequest(req, res, next, apiDir, enableCors, getCache, setCache) {
583
587
  try {
584
- let app = getCache();
585
- if (!app) {
586
- app = await buildHonoApp(apiDir, enableCors);
587
- setCache(app);
588
+ if (enableCors && req.method === "OPTIONS") {
589
+ res.statusCode = 204;
590
+ res.setHeader("Access-Control-Allow-Origin", "*");
591
+ res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
592
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");
593
+ res.setHeader("Access-Control-Max-Age", "86400");
594
+ res.end();
595
+ return;
596
+ }
597
+ let cache = getCache();
598
+ if (!cache) {
599
+ cache = { routes: scanApiRoutes(apiDir, "/api") };
600
+ setCache(cache);
588
601
  }
589
602
  const host = req.headers.host ?? "localhost";
590
- const url = `http://${host}/api${req.url}`;
603
+ const url = `http://${host}${req.url}`;
604
+ const pathname = new URL(url).pathname;
591
605
  const chunks = [];
592
606
  for await (const chunk of req) chunks.push(chunk);
593
607
  const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
608
+ const method = req.method.toUpperCase();
594
609
  const webReq = new Request(url, {
595
- method: req.method,
610
+ method,
596
611
  headers: req.headers,
597
- body: body?.length ? body : void 0
612
+ body: !["GET", "HEAD"].includes(method) && body?.length ? body : void 0
598
613
  });
599
- const webRes = await app.fetch(webReq);
600
- res.statusCode = webRes.status;
601
- webRes.headers.forEach((v, k) => res.setHeader(k, v));
602
- res.end(Buffer.from(await webRes.arrayBuffer()));
614
+ for (const route of cache.routes) {
615
+ const handler = await importHandler(route.filePath);
616
+ if (!handler) continue;
617
+ let webRes;
618
+ try {
619
+ if (typeof handler.fetch === "function") {
620
+ webRes = await handler.fetch(webReq.clone());
621
+ if (webRes.status === 404) continue;
622
+ } else if (typeof handler === "function") {
623
+ const params = matchRoute(route.routePath, pathname);
624
+ if (params === null) continue;
625
+ const existingHeaders = {};
626
+ webReq.headers.forEach((v, k) => {
627
+ existingHeaders[k] = v;
628
+ });
629
+ const reqWithParams = new Request(webReq.clone(), {
630
+ headers: { ...existingHeaders, "x-bini-params": JSON.stringify(params) }
631
+ });
632
+ webRes = await handler(reqWithParams);
633
+ } else {
634
+ continue;
635
+ }
636
+ } catch {
637
+ continue;
638
+ }
639
+ const finalHeaders = {};
640
+ webRes.headers.forEach((v, k) => {
641
+ finalHeaders[k] = v;
642
+ });
643
+ if (enableCors) {
644
+ finalHeaders["access-control-allow-origin"] = "*";
645
+ finalHeaders["access-control-allow-methods"] = "GET,POST,PUT,PATCH,DELETE,OPTIONS";
646
+ finalHeaders["access-control-allow-headers"] = "Content-Type,Authorization";
647
+ }
648
+ res.statusCode = webRes.status;
649
+ for (const [k, v] of Object.entries(finalHeaders)) res.setHeader(k, v);
650
+ res.end(Buffer.from(await webRes.arrayBuffer()));
651
+ return;
652
+ }
653
+ res.statusCode = 404;
654
+ res.setHeader("Content-Type", "application/json");
655
+ res.end(JSON.stringify({ error: `No API handler found for ${req.url}` }));
603
656
  } catch (e) {
604
657
  next(e);
605
658
  }
@@ -647,13 +700,12 @@ function buildProductionEntry(srcApiDir, platform, enableCors) {
647
700
  lines = [
648
701
  ...header,
649
702
  `import { Hono } from 'hono';`,
650
- `import { handle } from '@hono/vercel';`,
651
703
  ...corsImport ? [corsImport] : [],
652
704
  ...imports,
653
705
  ...appSetup,
654
706
  ``,
655
- `export const config = { runtime: 'edge' };`,
656
- `export default handle(app);`
707
+ `export const runtime = 'edge';`,
708
+ `export default app;`
657
709
  ];
658
710
  } else if (platform === "cloudflare") {
659
711
  outFile = import_path.default.join(cwd, "worker.ts");
@@ -791,6 +843,7 @@ function biniroute(options = {}) {
791
843
  },
792
844
  buildEnd() {
793
845
  honoCache = null;
846
+ moduleCache.clear();
794
847
  if (debounceTimer) {
795
848
  clearTimeout(debounceTimer);
796
849
  debounceTimer = null;
@@ -823,16 +876,17 @@ function biniroute(options = {}) {
823
876
  });
824
877
  if (import_fs.default.existsSync(apiDir)) {
825
878
  server.watcher.add(apiDir);
826
- const resetApi = () => {
879
+ const resetApi = (f) => {
827
880
  honoCache = null;
881
+ if (f) moduleCache.delete(f);
828
882
  server.ws.send({ type: "full-reload", path: "*" });
829
883
  };
830
- server.watcher.on("add", (f) => isApiFile(f) && resetApi());
831
- server.watcher.on("unlink", (f) => isApiFile(f) && resetApi());
832
- server.watcher.on("change", (f) => isApiFile(f) && resetApi());
833
- server.middlewares.use(
834
- "/api",
835
- (req, res, next) => handleApiRequest(
884
+ server.watcher.on("add", (f) => isApiFile(f) && resetApi(f));
885
+ server.watcher.on("unlink", (f) => isApiFile(f) && resetApi(f));
886
+ server.watcher.on("change", (f) => isApiFile(f) && resetApi(f));
887
+ server.middlewares.use((req, res, next) => {
888
+ if (!req.url?.startsWith("/api")) return next();
889
+ handleApiRequest(
836
890
  req,
837
891
  res,
838
892
  next,
@@ -842,17 +896,17 @@ function biniroute(options = {}) {
842
896
  (v) => {
843
897
  honoCache = v;
844
898
  }
845
- )
846
- );
899
+ );
900
+ });
847
901
  }
848
902
  },
849
903
  async configurePreviewServer(server) {
850
904
  addSpaFallback(server);
851
905
  const apiDir = getApiDir();
852
906
  if (!import_fs.default.existsSync(apiDir)) return;
853
- server.middlewares.use(
854
- "/api",
855
- (req, res, next) => handleApiRequest(
907
+ server.middlewares.use((req, res, next) => {
908
+ if (!req.url?.startsWith("/api")) return next();
909
+ handleApiRequest(
856
910
  req,
857
911
  res,
858
912
  next,
@@ -862,8 +916,8 @@ function biniroute(options = {}) {
862
916
  (v) => {
863
917
  honoCache = v;
864
918
  }
865
- )
866
- );
919
+ );
920
+ });
867
921
  },
868
922
  transformIndexHtml: {
869
923
  order: "pre",