bini-router 1.0.4 → 1.0.5

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.cjs CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  module.exports = __toCommonJS(index_exports);
37
37
  var import_fs = __toESM(require("fs"), 1);
38
38
  var import_path = __toESM(require("path"), 1);
39
+ var import_child_process = require("child_process");
39
40
  var PAGE_FILES = ["page.tsx", "page.jsx", "page.ts", "page.js"];
40
41
  var LAYOUT_FILES = ["layout.tsx", "layout.jsx", "layout.ts", "layout.js"];
41
42
  var SUPPORTED_EXTS = [".tsx", ".jsx", ".ts", ".js"];
@@ -45,13 +46,13 @@ var API_EXTS = [".ts", ".js"];
45
46
  var DEBOUNCE_MS = 60;
46
47
  var EVENT_DEDUP_MS = 500;
47
48
  var EVENT_TTL_MS = 2e3;
49
+ var APP_DIR = import_path.default.join(process.cwd(), "src/app");
50
+ var API_DIR = import_path.default.join(process.cwd(), "src/app/api");
48
51
  function norm(p) {
49
52
  return p.replace(/\\/g, "/");
50
53
  }
51
54
  function isInDir(file, dir) {
52
- const nFile = norm(file);
53
- const nDir = norm(dir).replace(/\/$/, "");
54
- return nFile.startsWith(nDir + "/") || nFile === nDir;
55
+ return norm(file).startsWith(norm(dir));
55
56
  }
56
57
  function readTsconfigAliases() {
57
58
  const aliases = {};
@@ -104,20 +105,20 @@ function getAppFile() {
104
105
  const ts = import_path.default.join(process.cwd(), "src/App.tsx");
105
106
  return import_fs.default.existsSync(ts) ? ts : import_path.default.join(process.cwd(), "src/App.jsx");
106
107
  }
107
- function resolveLayoutChain(pageDir, appDir) {
108
+ function resolveLayoutChain(pageDir) {
108
109
  const chain = [];
109
110
  let current = pageDir;
110
111
  while (true) {
111
112
  const layout = findFile(current, LAYOUT_FILES);
112
113
  if (layout) chain.unshift(import_path.default.join(current, layout));
113
- if (import_path.default.resolve(current) === import_path.default.resolve(appDir)) break;
114
+ if (import_path.default.resolve(current) === import_path.default.resolve(APP_DIR)) break;
114
115
  const parent = import_path.default.dirname(current);
115
116
  if (parent === current) break;
116
117
  current = parent;
117
118
  }
118
119
  return chain;
119
120
  }
120
- function scanRoutes(dir, appDir, baseRoute = "") {
121
+ function scanRoutes(dir, baseRoute = "") {
121
122
  const routes = [];
122
123
  if (!import_fs.default.existsSync(dir)) return routes;
123
124
  let entries;
@@ -135,7 +136,7 @@ function scanRoutes(dir, appDir, baseRoute = "") {
135
136
  routes.push({
136
137
  routePath: `${baseRoute}/${base}`,
137
138
  filePath: import_path.default.join(dir, entry.name),
138
- layouts: resolveLayoutChain(dir, appDir),
139
+ layouts: resolveLayoutChain(dir),
139
140
  dynamic: false
140
141
  });
141
142
  }
@@ -151,11 +152,11 @@ function scanRoutes(dir, appDir, baseRoute = "") {
151
152
  routes.push({
152
153
  routePath,
153
154
  filePath: import_path.default.join(fullPath, pageFile),
154
- layouts: resolveLayoutChain(fullPath, appDir),
155
+ layouts: resolveLayoutChain(fullPath),
155
156
  dynamic: isDynamic
156
157
  });
157
158
  }
158
- routes.push(...scanRoutes(fullPath, appDir, routePath));
159
+ routes.push(...scanRoutes(fullPath, routePath));
159
160
  }
160
161
  return routes;
161
162
  }
@@ -211,15 +212,15 @@ function renderChain(layouts, routesInChain, layoutNames, pageNames, layoutTitle
211
212
  `${pad}</Route>`
212
213
  ].join("\n");
213
214
  }
214
- function generateApp(appDir) {
215
+ function generateApp() {
215
216
  const aliases = readTsconfigAliases();
216
- const routes = scanRoutes(appDir, appDir);
217
- const rootPage = findFile(appDir, PAGE_FILES);
217
+ const routes = scanRoutes(APP_DIR);
218
+ const rootPage = findFile(APP_DIR, PAGE_FILES);
218
219
  if (rootPage) {
219
220
  routes.unshift({
220
221
  routePath: "/",
221
- filePath: import_path.default.join(appDir, rootPage),
222
- layouts: resolveLayoutChain(appDir, appDir),
222
+ filePath: import_path.default.join(APP_DIR, rootPage),
223
+ layouts: resolveLayoutChain(APP_DIR),
223
224
  dynamic: false
224
225
  });
225
226
  }
@@ -234,8 +235,8 @@ function generateApp(appDir) {
234
235
  if (a.dynamic !== b.dynamic) return a.dynamic ? 1 : -1;
235
236
  return a.routePath.length - b.routePath.length;
236
237
  });
237
- const notFoundFile = NOT_FOUND_FILES.find((f) => import_fs.default.existsSync(import_path.default.join(appDir, f)));
238
- const notFound = notFoundFile && hasDefaultExport(import_path.default.join(appDir, notFoundFile)) ? notFoundFile : void 0;
238
+ const notFoundFile = NOT_FOUND_FILES.find((f) => import_fs.default.existsSync(import_path.default.join(APP_DIR, f)));
239
+ const notFound = notFoundFile && hasDefaultExport(import_path.default.join(APP_DIR, notFoundFile)) ? notFoundFile : void 0;
239
240
  const allLayouts = /* @__PURE__ */ new Set();
240
241
  for (const r of validRoutes) r.layouts.forEach((l) => {
241
242
  if (isUsableLayout(l)) allLayouts.add(l);
@@ -256,7 +257,7 @@ function generateApp(appDir) {
256
257
  for (const [fp, name] of layoutNames)
257
258
  lazyImports.push(`const ${name} = React.lazy(() => import('${toImportPath(fp, aliases)}'));`);
258
259
  if (notFound)
259
- lazyImports.push(`const NotFound = React.lazy(() => import('${toImportPath(import_path.default.join(appDir, notFound), aliases)}'));`);
260
+ lazyImports.push(`const NotFound = React.lazy(() => import('${toImportPath(import_path.default.join(APP_DIR, notFound), aliases)}'));`);
260
261
  const emittedPages = /* @__PURE__ */ new Set();
261
262
  for (const r of validRoutes) {
262
263
  if (emittedPages.has(r.filePath)) continue;
@@ -318,7 +319,6 @@ function Spinner() {
318
319
  }
319
320
 
320
321
  // Renders nothing \u2014 just sets document.title when the layout mounts.
321
- // Placed outside ErrorBoundary so a layout crash doesn't block the title update.
322
322
  function TitleSetter({ title }: { title: string }) {
323
323
  React.useEffect(() => { document.title = title; }, [title]);
324
324
  return null;
@@ -348,12 +348,12 @@ ${catchAll}
348
348
  }
349
349
  `;
350
350
  }
351
- function parseAppMetadata(appDir) {
352
- const layout = findFile(appDir, LAYOUT_FILES);
351
+ function parseAppMetadata() {
352
+ const layout = findFile(APP_DIR, LAYOUT_FILES);
353
353
  if (!layout) return {};
354
354
  let src = "";
355
355
  try {
356
- src = import_fs.default.readFileSync(import_path.default.join(appDir, layout), "utf8");
356
+ src = import_fs.default.readFileSync(import_path.default.join(APP_DIR, layout), "utf8");
357
357
  } catch {
358
358
  return {};
359
359
  }
@@ -515,7 +515,7 @@ function extractArrayRaw(source, key) {
515
515
  }
516
516
  return void 0;
517
517
  }
518
- function scanApiRoutes(dir, baseRoute = "") {
518
+ function scanApiRoutes(dir = API_DIR, baseRoute = "") {
519
519
  const routes = [];
520
520
  if (!import_fs.default.existsSync(dir)) return routes;
521
521
  let entries;
@@ -544,136 +544,34 @@ 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") return await handler.fetch(c.req.raw);
567
- if (typeof handler === "function") {
568
- return await handler(c.req.raw);
569
- }
570
- return c.json({ error: "Default export must be a function or Hono app" }, 500);
571
- } catch (e) {
572
- return c.json({ error: e.message }, 500);
573
- }
574
- });
575
- }
576
- return app;
577
- }
578
- async function handleApiRequest(req, res, next, apiDir, enableCors, getCache, setCache) {
579
- try {
580
- let app = getCache();
581
- if (!app) {
582
- app = await buildHonoApp(apiDir, enableCors);
583
- setCache(app);
584
- }
585
- const host = req.headers.host ?? "localhost";
586
- const url = `http://${host}/api${req.url}`;
587
- const chunks = [];
588
- for await (const chunk of req) chunks.push(chunk);
589
- const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
590
- const webReq = new Request(url, {
591
- method: req.method,
592
- headers: req.headers,
593
- body: body?.length ? body : void 0
594
- });
595
- const webRes = await app.fetch(webReq);
596
- res.statusCode = webRes.status;
597
- webRes.headers.forEach((v, k) => res.setHeader(k, v));
598
- res.end(Buffer.from(await webRes.arrayBuffer()));
599
- } catch (e) {
600
- next(e);
601
- }
602
- }
603
- function buildNitroRoutes(srcApiDir, enableCors) {
604
- if (!import_fs.default.existsSync(srcApiDir)) return;
605
- const routes = scanApiRoutes(srcApiDir);
606
- if (!routes.length) return;
607
- const cwd = process.cwd();
608
- const nitroApiDir = import_path.default.join(cwd, "server", "routes", "api");
609
- if (import_fs.default.existsSync(nitroApiDir)) {
610
- import_fs.default.rmSync(nitroApiDir, { recursive: true, force: true });
611
- }
612
- import_fs.default.mkdirSync(nitroApiDir, { recursive: true });
613
- for (const route of routes) {
614
- const relFromApiDir = norm(import_path.default.relative(srcApiDir, route.filePath));
615
- const outFile = import_path.default.join(nitroApiDir, relFromApiDir);
616
- import_fs.default.mkdirSync(import_path.default.dirname(outFile), { recursive: true });
617
- const importRel = norm(import_path.default.relative(import_path.default.dirname(outFile), route.filePath)).replace(/\.(ts|tsx)$/, "");
618
- const importPath = importRel.startsWith(".") ? importRel : `./${importRel}`;
619
- const code = [
620
- `// Auto-generated by bini-router \u2014 DO NOT EDIT.`,
621
- `// Source: ${norm(import_path.default.relative(cwd, route.filePath))}`,
622
- `import { Hono } from 'hono';`,
623
- ...enableCors ? [`import { cors } from 'hono/cors';`] : [],
624
- `import { defineEventHandler, getRequestURL, getHeaders, readRawBody } from 'h3';`,
625
- `import sourceHandler from '${importPath}';`,
626
- ``,
627
- `// Wrap the user's handler so we get a standard fetch() interface.`,
628
- `// Supports:`,
629
- `// export default async function handler(req: Request) { ... } \u2190 web standard`,
630
- `// export default new Hono() \u2190 Hono app`,
631
- `function makeApp(): Hono {`,
632
- ` if (sourceHandler instanceof Hono) return sourceHandler;`,
633
- ` const app = new Hono();`,
634
- ...enableCors ? [` app.use('*', cors({ origin: '*', allowMethods: ['GET','POST','PUT','PATCH','DELETE','OPTIONS'], allowHeaders: ['Content-Type','Authorization'] }));`] : [],
635
- ` app.all('*', async c => {`,
636
- ` try {`,
637
- ` const res = await (sourceHandler as (req: Request) => Promise<Response>)(c.req.raw);`,
638
- ` return res;`,
639
- ` } catch (e: any) {`,
640
- ` return c.json({ error: e.message ?? 'Internal server error' }, 500);`,
641
- ` }`,
642
- ` });`,
643
- ` return app;`,
644
- `}`,
645
- ``,
646
- `const app = makeApp();`,
647
- ``,
648
- `export default defineEventHandler(async (event) => {`,
649
- ` // Manually reconstruct a Web Request from h3 event to avoid the`,
650
- ` // toWebRequest() body-stream bug on Netlify/Lambda (h3 issue #578).`,
651
- ` const url = getRequestURL(event);`,
652
- ` const headers = new Headers(getHeaders(event) as Record<string, string>);`,
653
- ` const method = (event.method ?? event.node?.req?.method ?? 'GET').toUpperCase();`,
654
- ` const hasBody = !['GET', 'HEAD', 'OPTIONS'].includes(method);`,
655
- ` let body: BodyInit | undefined;`,
656
- ` if (hasBody) {`,
657
- ` const raw = await readRawBody(event, false);`,
658
- ` if (raw && raw.length > 0) body = raw;`,
659
- ` }`,
660
- ` const req = new Request(url.toString(), { method, headers, body });`,
661
- ` return app.fetch(req);`,
662
- `});`,
663
- ``
664
- ].join("\n");
665
- import_fs.default.writeFileSync(outFile, code, "utf8");
666
- }
667
- console.log(`[bini-router] \u2713 Generated ${routes.length} Nitro route(s) in server/routes/api/
668
- `);
547
+ function generateNitroApiEntry() {
548
+ const routes = scanApiRoutes();
549
+ if (!routes.length) return "";
550
+ const imports = [];
551
+ const registrations = [];
552
+ routes.forEach((route, i) => {
553
+ const relPath = import_path.default.relative(
554
+ import_path.default.join(process.cwd(), "server"),
555
+ route.filePath
556
+ ).replace(/\\/g, "/").replace(/\.(ts|js)$/, "");
557
+ imports.push(`import handler${i} from './${relPath}';`);
558
+ registrations.push(
559
+ ` app.all('${route.routePath}', async (c) => { try { return await handler${i}(c.req.raw); } catch (e) { return c.json({ error: e.message }, 500); } });`
560
+ );
561
+ });
562
+ return `// Auto-generated by bini-router
563
+ import { Hono } from 'hono';
564
+ ${imports.join("\n")}
565
+
566
+ const app = new Hono();
567
+ ${registrations.join("\n")}
568
+
569
+ export default app;
570
+ `;
669
571
  }
670
- function biniroute(options = {}) {
671
- const { cors: enableCors = true } = options;
672
- const getAppDir = () => import_path.default.join(process.cwd(), options.appDir ?? "src/app");
673
- const getApiDir = () => import_path.default.join(process.cwd(), options.apiDir ?? "src/app/api");
572
+ function biniroute() {
674
573
  let debounceTimer = null;
675
574
  let lastGeneratedCode = "";
676
- let honoCache = null;
677
575
  const eventLog = /* @__PURE__ */ new Map();
678
576
  function shouldProcess(file, event) {
679
577
  const key = `${file}:${event}`;
@@ -688,19 +586,18 @@ function biniroute(options = {}) {
688
586
  const base = import_path.default.basename(f, import_path.default.extname(f));
689
587
  const ext = import_path.default.extname(f);
690
588
  if (!SUPPORTED_EXTS.includes(ext)) return false;
691
- if (!isInDir(nf, norm(getAppDir()))) return false;
692
- if (isInDir(nf, norm(getApiDir()))) return false;
589
+ if (!isInDir(nf, norm(APP_DIR))) return false;
590
+ if (isInDir(nf, norm(API_DIR))) return false;
693
591
  if (base.startsWith("_")) return false;
694
592
  return true;
695
593
  }
696
594
  function isApiFile(f) {
697
595
  const nf = norm(f);
698
- return isInDir(nf, norm(getApiDir())) && API_EXTS.includes(import_path.default.extname(f));
596
+ return isInDir(nf, norm(API_DIR)) && API_EXTS.includes(import_path.default.extname(f));
699
597
  }
700
598
  function applyApp() {
701
- const dir = getAppDir();
702
- if (!import_fs.default.existsSync(dir)) return null;
703
- const code = generateApp(dir);
599
+ if (!import_fs.default.existsSync(APP_DIR)) return null;
600
+ const code = generateApp();
704
601
  if (code === lastGeneratedCode) return null;
705
602
  import_fs.default.writeFileSync(getAppFile(), code, "utf8");
706
603
  lastGeneratedCode = code;
@@ -718,19 +615,53 @@ function biniroute(options = {}) {
718
615
  function addSpaFallback(server) {
719
616
  server.middlewares.use((req, res, next) => {
720
617
  const url = req.url;
721
- if (url.startsWith("/api") || url.includes(".")) return next();
618
+ if (url.includes(".")) return next();
722
619
  req.url = "/index.html";
723
620
  next();
724
621
  });
725
622
  }
623
+ function buildApiWithNitro() {
624
+ const routes = scanApiRoutes();
625
+ if (routes.length === 0) return;
626
+ console.log("\n\u{1F4E6} Building API routes with Nitro...");
627
+ const serverDir = import_path.default.join(process.cwd(), "server");
628
+ const apiDir = import_path.default.join(serverDir, "api");
629
+ import_fs.default.mkdirSync(apiDir, { recursive: true });
630
+ const entry = generateNitroApiEntry();
631
+ import_fs.default.writeFileSync(import_path.default.join(serverDir, "index.ts"), entry, "utf8");
632
+ routes.forEach((route) => {
633
+ const destPath = import_path.default.join(apiDir, import_path.default.basename(route.filePath));
634
+ import_fs.default.copyFileSync(route.filePath, destPath);
635
+ });
636
+ const nitroConfig = import_path.default.join(process.cwd(), "nitro.config.ts");
637
+ if (!import_fs.default.existsSync(nitroConfig)) {
638
+ const config = `import { defineNitroConfig } from 'nitropack/config';
639
+
640
+ export default defineNitroConfig({
641
+ handlers: [
642
+ { route: '/api/**', handler: '~/server/index' }
643
+ ]
644
+ });
645
+ `;
646
+ import_fs.default.writeFileSync(nitroConfig, config, "utf8");
647
+ }
648
+ try {
649
+ (0, import_child_process.execSync)("npx nitropack build", {
650
+ stdio: "inherit",
651
+ cwd: process.cwd()
652
+ });
653
+ console.log("\u2705 API routes built successfully");
654
+ } catch (error) {
655
+ console.error("\u274C API build failed:", error);
656
+ process.exit(1);
657
+ }
658
+ }
726
659
  return {
727
660
  name: "bini-router",
728
661
  enforce: "pre",
729
- // Strip `export const metadata = {...}` from all src/app files before
730
- // @vitejs/plugin-react's Fast Refresh transform sees them.
731
662
  transform(code, id) {
732
663
  const nid = norm(id);
733
- if (!isInDir(nid, norm(getAppDir()))) return;
664
+ if (!isInDir(nid, norm(APP_DIR))) return;
734
665
  const ext = import_path.default.extname(id);
735
666
  if (!SUPPORTED_EXTS.includes(ext)) return;
736
667
  if (!code.includes("export const metadata")) return;
@@ -765,89 +696,83 @@ function biniroute(options = {}) {
765
696
  buildStart() {
766
697
  applyApp();
767
698
  },
699
+ // This runs after Vite build is complete
768
700
  closeBundle() {
769
- buildNitroRoutes(getApiDir(), enableCors);
701
+ buildApiWithNitro();
770
702
  },
771
703
  buildEnd() {
772
- honoCache = null;
773
704
  if (debounceTimer) {
774
705
  clearTimeout(debounceTimer);
775
706
  debounceTimer = null;
776
707
  }
777
708
  },
778
709
  async configureServer(server) {
779
- const appDir = getAppDir();
780
- const apiDir = getApiDir();
781
- if (!import_fs.default.existsSync(appDir)) return;
782
- server.watcher.add(appDir);
710
+ if (!import_fs.default.existsSync(APP_DIR)) return;
711
+ server.watcher.add(APP_DIR);
783
712
  server.watcher.on("add", (f) => isPageFile(f) && shouldProcess(f, "add") && scheduleRegen(server, 300));
784
713
  server.watcher.on("unlink", (f) => isPageFile(f) && shouldProcess(f, "unlink") && scheduleRegen(server));
785
714
  server.watcher.on("change", (f) => isPageFile(f) && shouldProcess(f, "change") && scheduleRegen(server));
786
715
  server.watcher.on("change", (f) => {
787
716
  const base = import_path.default.basename(f, import_path.default.extname(f));
788
- const inAppRoot = import_path.default.resolve(import_path.default.dirname(f)) === import_path.default.resolve(appDir);
717
+ const inAppRoot = import_path.default.resolve(import_path.default.dirname(f)) === import_path.default.resolve(APP_DIR);
789
718
  if (!inAppRoot || base !== "layout") return;
790
719
  server.moduleGraph.invalidateAll();
791
720
  server.ws.send({ type: "full-reload", path: "*" });
792
721
  });
793
722
  server.watcher.on("addDir", (d) => {
794
- const nd = norm(d);
795
- if (!isInDir(nd, norm(appDir)) || d.includes("node_modules") || isInDir(nd, norm(apiDir))) return;
723
+ if (!isInDir(norm(d), norm(APP_DIR)) || d.includes("node_modules") || d.includes("api")) return;
796
724
  setTimeout(() => PAGE_FILES.some((f) => import_fs.default.existsSync(import_path.default.join(d, f))) && scheduleRegen(server), 300);
797
725
  });
798
726
  server.watcher.on("unlinkDir", (d) => {
799
- const nd = norm(d);
800
- if (isInDir(nd, norm(appDir)) && !d.includes("node_modules") && !isInDir(nd, norm(apiDir)))
727
+ if (isInDir(norm(d), norm(APP_DIR)) && !d.includes("node_modules") && !d.includes("api"))
801
728
  scheduleRegen(server);
802
729
  });
803
- if (import_fs.default.existsSync(apiDir)) {
804
- server.watcher.add(apiDir);
805
- const resetApi = () => {
806
- honoCache = null;
807
- server.ws.send({ type: "full-reload", path: "*" });
808
- };
809
- server.watcher.on("add", (f) => isApiFile(f) && resetApi());
810
- server.watcher.on("unlink", (f) => isApiFile(f) && resetApi());
811
- server.watcher.on("change", (f) => isApiFile(f) && resetApi());
812
- server.middlewares.use(
813
- "/api",
814
- (req, res, next) => handleApiRequest(
815
- req,
816
- res,
817
- next,
818
- apiDir,
819
- enableCors,
820
- () => honoCache,
821
- (v) => {
822
- honoCache = v;
730
+ if (import_fs.default.existsSync(API_DIR)) {
731
+ server.middlewares.use("/api", async (req, res, next) => {
732
+ try {
733
+ const { Hono } = await import("hono");
734
+ const app = new Hono();
735
+ const routes = scanApiRoutes();
736
+ for (const route of routes) {
737
+ const mod = await import(route.filePath + "?t=" + Date.now());
738
+ const handler = mod.default;
739
+ if (typeof handler === "function") {
740
+ app.all(route.routePath, async (c) => {
741
+ try {
742
+ return await handler(c.req.raw);
743
+ } catch (e) {
744
+ return c.json({ error: e.message }, 500);
745
+ }
746
+ });
747
+ }
823
748
  }
824
- )
825
- );
749
+ const url = `http://${req.headers.host}${req.url}`;
750
+ const chunks = [];
751
+ for await (const chunk of req) chunks.push(chunk);
752
+ const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
753
+ const webReq = new Request(url, {
754
+ method: req.method,
755
+ headers: req.headers,
756
+ body
757
+ });
758
+ const webRes = await app.fetch(webReq);
759
+ res.statusCode = webRes.status;
760
+ webRes.headers.forEach((value, key) => res.setHeader(key, value));
761
+ const buffer = await webRes.arrayBuffer();
762
+ res.end(Buffer.from(buffer));
763
+ } catch (e) {
764
+ next(e);
765
+ }
766
+ });
826
767
  }
827
768
  },
828
769
  async configurePreviewServer(server) {
829
770
  addSpaFallback(server);
830
- const apiDir = getApiDir();
831
- if (!import_fs.default.existsSync(apiDir)) return;
832
- server.middlewares.use(
833
- "/api",
834
- (req, res, next) => handleApiRequest(
835
- req,
836
- res,
837
- next,
838
- apiDir,
839
- enableCors,
840
- () => honoCache,
841
- (v) => {
842
- honoCache = v;
843
- }
844
- )
845
- );
846
771
  },
847
772
  transformIndexHtml: {
848
773
  order: "pre",
849
774
  handler(html) {
850
- const meta = parseAppMetadata(getAppDir());
775
+ const meta = parseAppMetadata();
851
776
  const title = meta.title ?? "Bini App";
852
777
  const vp = meta.viewport ?? "width=device-width, initial-scale=1.0";
853
778
  const lines = [];
@@ -875,18 +800,18 @@ function biniroute(options = {}) {
875
800
  lines.push(`<link rel="apple-touch-icon" href="${entry.url}"${sizes}${type} />`);
876
801
  }
877
802
  if (meta.openGraph?.title) {
878
- lines.push(`<meta property="og:type" content="${meta.openGraph.type ?? "website"}" />`);
879
- lines.push(`<meta property="og:title" content="${meta.openGraph.title}" />`);
803
+ lines.push(`<meta property="og:type" content="${meta.openGraph.type ?? "website"}" />`);
804
+ lines.push(`<meta property="og:title" content="${meta.openGraph.title}" />`);
880
805
  if (meta.openGraph.description) lines.push(`<meta property="og:description" content="${meta.openGraph.description}" />`);
881
- if (meta.openGraph.url) lines.push(`<meta property="og:url" content="${meta.openGraph.url}" />`);
882
- if (meta.openGraph.image) lines.push(`<meta property="og:image" content="${meta.openGraph.image}" />`);
806
+ if (meta.openGraph.url) lines.push(`<meta property="og:url" content="${meta.openGraph.url}" />`);
807
+ if (meta.openGraph.image) lines.push(`<meta property="og:image" content="${meta.openGraph.image}" />`);
883
808
  }
884
809
  if (meta.twitter?.title) {
885
- lines.push(`<meta name="twitter:card" content="${meta.twitter.card ?? "summary_large_image"}" />`);
886
- lines.push(`<meta name="twitter:title" content="${meta.twitter.title}" />`);
810
+ lines.push(`<meta name="twitter:card" content="${meta.twitter.card ?? "summary_large_image"}" />`);
811
+ lines.push(`<meta name="twitter:title" content="${meta.twitter.title}" />`);
887
812
  if (meta.twitter.description) lines.push(`<meta name="twitter:description" content="${meta.twitter.description}" />`);
888
- if (meta.twitter.creator) lines.push(`<meta name="twitter:creator" content="${meta.twitter.creator}" />`);
889
- if (meta.twitter.image) lines.push(`<meta name="twitter:image" content="${meta.twitter.image}" />`);
813
+ if (meta.twitter.creator) lines.push(`<meta name="twitter:creator" content="${meta.twitter.creator}" />`);
814
+ if (meta.twitter.image) lines.push(`<meta name="twitter:image" content="${meta.twitter.image}" />`);
890
815
  }
891
816
  const injected = lines.map((l) => ` ${l}`).join("\n");
892
817
  return html.replace(/<meta\s+charset[^>]*\/?>/gi, "").replace(/<meta\s+name="viewport"[^>]*\/?>/gi, "").replace(/<meta\s+name="description"[^>]*\/?>/gi, "").replace(/<meta\s+name="theme-color"[^>]*\/?>/gi, "").replace(/<meta\s+name="robots"[^>]*\/?>/gi, "").replace(/<meta\s+name="keywords"[^>]*\/?>/gi, "").replace(/<meta\s+name="author"[^>]*\/?>/gi, "").replace(/<meta\s+property="og:[^"]*"[^>]*\/?>/gi, "").replace(/<meta\s+name="twitter:[^"]*"[^>]*\/?>/gi, "").replace(/<link\s+rel="canonical"[^>]*\/?>/gi, "").replace(/<link\s+rel="manifest"[^>]*\/?>/gi, "").replace(/<link\s+rel="icon"[^>]*\/?>/gi, "").replace(/<link\s+rel="shortcut icon"[^>]*\/?>/gi, "").replace(/<link\s+rel="apple-touch-icon"[^>]*\/?>/gi, "").replace(/<title>.*?<\/title>/si, "").replace("</head>", `${injected}