bini-router 1.0.8 → 1.0.9

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
@@ -30,501 +30,90 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- biniroute: () => biniroute,
33
+ bini: () => bini,
34
34
  default: () => index_default
35
35
  });
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");
40
- var import_url = require("url");
41
39
  var PAGE_FILES = ["page.tsx", "page.jsx", "page.ts", "page.js"];
42
40
  var LAYOUT_FILES = ["layout.tsx", "layout.jsx", "layout.ts", "layout.js"];
43
41
  var SUPPORTED_EXTS = [".tsx", ".jsx", ".ts", ".js"];
44
42
  var NOT_FOUND_FILES = SUPPORTED_EXTS.map((e) => `not-found${e}`);
45
- var SPECIAL_BASES = /* @__PURE__ */ new Set(["page", "layout", "not-found", "loading", "error"]);
46
43
  var API_EXTS = [".ts", ".js"];
47
- var DEBOUNCE_MS = 60;
48
- var EVENT_DEDUP_MS = 500;
49
- var EVENT_TTL_MS = 2e3;
50
- var APP_DIR = import_path.default.join(process.cwd(), "src/app");
51
- var API_DIR = import_path.default.join(process.cwd(), "src/app/api");
52
- function norm(p) {
53
- return p.replace(/\\/g, "/");
54
- }
55
- function isInDir(file, dir) {
56
- return norm(file).startsWith(norm(dir));
57
- }
58
- function readTsconfigAliases() {
59
- const aliases = {};
60
- try {
61
- const tsconfigPath = import_path.default.join(process.cwd(), "tsconfig.json");
62
- if (!import_fs.default.existsSync(tsconfigPath)) return aliases;
63
- const raw = import_fs.default.readFileSync(tsconfigPath, "utf8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
64
- const tsconfig = JSON.parse(raw);
65
- const paths = tsconfig?.compilerOptions?.paths ?? {};
66
- const baseUrl = tsconfig?.compilerOptions?.baseUrl ?? ".";
67
- for (const [alias, targets] of Object.entries(paths)) {
68
- const cleanAlias = alias.replace(/\/\*$/, "");
69
- const cleanTarget = (targets[0] ?? "").replace(/\/\*$/, "");
70
- aliases[cleanAlias] = import_path.default.resolve(process.cwd(), baseUrl, cleanTarget);
71
- }
72
- } catch {
73
- }
74
- return aliases;
75
- }
76
- function toImportPath(filePath, aliases) {
77
- for (const [alias, target] of Object.entries(aliases)) {
78
- if (norm(filePath).startsWith(norm(target) + "/")) {
79
- const rest = norm(filePath).slice(norm(target).length + 1).replace(/\.(tsx|ts|jsx|js)$/, "");
80
- return `${alias}/${rest}`;
81
- }
82
- }
83
- return "./" + norm(import_path.default.relative(import_path.default.join(process.cwd(), "src"), filePath)).replace(/\.(tsx|ts|jsx|js)$/, "");
44
+ function findFile(dir, candidates) {
45
+ return candidates.find((f) => import_fs.default.existsSync(import_path.default.join(dir, f))) ?? null;
84
46
  }
85
47
  function hasDefaultExport(filePath) {
86
48
  try {
87
- return import_fs.default.readFileSync(filePath, "utf8").includes("export default");
88
- } catch {
89
- return false;
90
- }
91
- }
92
- function isHtmlShellLayout(filePath) {
93
- try {
94
- return /<html[\s>]/i.test(import_fs.default.readFileSync(filePath, "utf8"));
49
+ const content = import_fs.default.readFileSync(filePath, "utf8");
50
+ return content.includes("export default");
95
51
  } catch {
96
52
  return false;
97
53
  }
98
54
  }
99
- function isUsableLayout(filePath) {
100
- return hasDefaultExport(filePath) && !isHtmlShellLayout(filePath);
101
- }
102
- function findFile(dir, candidates) {
103
- return candidates.find((f) => import_fs.default.existsSync(import_path.default.join(dir, f))) ?? null;
104
- }
105
- function getAppFile() {
106
- const ts = import_path.default.join(process.cwd(), "src/App.tsx");
107
- return import_fs.default.existsSync(ts) ? ts : import_path.default.join(process.cwd(), "src/App.jsx");
108
- }
109
- function resolveLayoutChain(pageDir) {
55
+ function resolveLayoutChain(pageDir, appDir) {
110
56
  const chain = [];
111
57
  let current = pageDir;
112
58
  while (true) {
113
59
  const layout = findFile(current, LAYOUT_FILES);
114
- if (layout) chain.unshift(import_path.default.join(current, layout));
115
- if (import_path.default.resolve(current) === import_path.default.resolve(APP_DIR)) break;
60
+ if (layout && hasDefaultExport(import_path.default.join(current, layout))) {
61
+ chain.unshift(import_path.default.join(current, layout));
62
+ }
63
+ if (import_path.default.resolve(current) === import_path.default.resolve(appDir)) break;
116
64
  const parent = import_path.default.dirname(current);
117
65
  if (parent === current) break;
118
66
  current = parent;
119
67
  }
120
68
  return chain;
121
69
  }
122
- function scanRoutes(dir, baseRoute = "") {
70
+ function scanPages(dir, appDir, baseRoute = "") {
123
71
  const routes = [];
124
72
  if (!import_fs.default.existsSync(dir)) return routes;
125
- let entries;
126
- try {
127
- entries = import_fs.default.readdirSync(dir, { withFileTypes: true });
128
- } catch {
129
- return routes;
130
- }
73
+ const entries = import_fs.default.readdirSync(dir, { withFileTypes: true });
131
74
  for (const entry of entries) {
132
- if (!entry.isFile() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
75
+ if (!entry.isFile()) continue;
133
76
  const ext = import_path.default.extname(entry.name);
134
77
  const base = import_path.default.basename(entry.name, ext);
135
78
  if (!SUPPORTED_EXTS.includes(ext)) continue;
136
- if (SPECIAL_BASES.has(base)) continue;
79
+ if (base === "page" || base === "layout" || base === "not-found" || base.startsWith("_")) continue;
80
+ if (entry.name.startsWith(".")) continue;
81
+ const filePath = import_path.default.join(dir, entry.name);
82
+ if (!hasDefaultExport(filePath)) continue;
137
83
  routes.push({
138
84
  routePath: `${baseRoute}/${base}`,
139
- filePath: import_path.default.join(dir, entry.name),
140
- layouts: resolveLayoutChain(dir),
85
+ filePath,
86
+ layouts: resolveLayoutChain(dir, appDir),
141
87
  dynamic: false
142
88
  });
143
89
  }
144
90
  for (const entry of entries) {
145
91
  if (!entry.isDirectory()) continue;
146
- if (entry.name === "node_modules" || entry.name.startsWith(".") || entry.name === "api") continue;
92
+ if (entry.name === "api" || entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
147
93
  const fullPath = import_path.default.join(dir, entry.name);
148
94
  const isDynamic = entry.name.startsWith("[") && entry.name.endsWith("]");
149
95
  const segment = isDynamic ? `:${entry.name.slice(1, -1)}` : entry.name;
150
96
  const routePath = `${baseRoute}/${segment}`;
151
97
  const pageFile = findFile(fullPath, PAGE_FILES);
152
98
  if (pageFile) {
153
- routes.push({
154
- routePath,
155
- filePath: import_path.default.join(fullPath, pageFile),
156
- layouts: resolveLayoutChain(fullPath),
157
- dynamic: isDynamic
158
- });
159
- }
160
- routes.push(...scanRoutes(fullPath, routePath));
161
- }
162
- return routes;
163
- }
164
- function deduplicateRoutes(routes) {
165
- const seen = /* @__PURE__ */ new Set();
166
- return routes.filter((r) => {
167
- if (seen.has(r.routePath)) return false;
168
- seen.add(r.routePath);
169
- return true;
170
- });
171
- }
172
- function parseLayoutTitle(layoutFile) {
173
- let src = "";
174
- try {
175
- src = import_fs.default.readFileSync(layoutFile, "utf8");
176
- } catch {
177
- return null;
178
- }
179
- const startIdx = src.indexOf("export const metadata");
180
- if (startIdx === -1) return null;
181
- const braceStart = src.indexOf("{", startIdx);
182
- if (braceStart === -1) return null;
183
- let depth = 0, end = braceStart;
184
- for (let i = braceStart; i < src.length; i++) {
185
- if (src[i] === "{") depth++;
186
- else if (src[i] === "}") {
187
- depth--;
188
- if (depth === 0) {
189
- end = i;
190
- break;
191
- }
192
- }
193
- }
194
- const block = src.slice(braceStart, end + 1);
195
- const match = /['"]?title['"]?\s*:\s*['"`]([^'"`]+)['"`]/.exec(block);
196
- return match ? match[1] : null;
197
- }
198
- function renderChain(layouts, routesInChain, layoutNames, pageNames, layoutTitles, indent) {
199
- const pad = " ".repeat(indent);
200
- if (layouts.length === 0) {
201
- return routesInChain.map(
202
- (r) => `${pad}<Route path="${r.routePath}" element={<Suspense fallback={<Spinner />}><ErrorBoundary><${pageNames.get(r.filePath)} /></ErrorBoundary></Suspense>} />`
203
- ).join("\n");
204
- }
205
- const [head, ...tail] = layouts;
206
- const title = layoutTitles.get(head);
207
- const titleSetter = title ? `<TitleSetter title=${JSON.stringify(title)} />` : "";
208
- const inner = renderChain(tail, routesInChain, layoutNames, pageNames, layoutTitles, indent + 2);
209
- const name = layoutNames.get(head);
210
- return [
211
- `${pad}<Route element={<>${titleSetter}<Suspense fallback={<Spinner />}><ErrorBoundary><${name}><Outlet /></${name}></ErrorBoundary></Suspense></>}>`,
212
- inner,
213
- `${pad}</Route>`
214
- ].join("\n");
215
- }
216
- function generateApp() {
217
- const aliases = readTsconfigAliases();
218
- const routes = scanRoutes(APP_DIR);
219
- const rootPage = findFile(APP_DIR, PAGE_FILES);
220
- if (rootPage) {
221
- routes.unshift({
222
- routePath: "/",
223
- filePath: import_path.default.join(APP_DIR, rootPage),
224
- layouts: resolveLayoutChain(APP_DIR),
225
- dynamic: false
226
- });
227
- }
228
- const routesFiltered = routes.map((r) => ({
229
- ...r,
230
- layouts: r.layouts.filter((l) => isUsableLayout(l))
231
- }));
232
- const validRoutes = deduplicateRoutes(
233
- routesFiltered.filter((r) => hasDefaultExport(r.filePath))
234
- );
235
- validRoutes.sort((a, b) => {
236
- if (a.dynamic !== b.dynamic) return a.dynamic ? 1 : -1;
237
- return a.routePath.length - b.routePath.length;
238
- });
239
- const notFoundFile = NOT_FOUND_FILES.find((f) => import_fs.default.existsSync(import_path.default.join(APP_DIR, f)));
240
- const notFound = notFoundFile && hasDefaultExport(import_path.default.join(APP_DIR, notFoundFile)) ? notFoundFile : void 0;
241
- const allLayouts = /* @__PURE__ */ new Set();
242
- for (const r of validRoutes) r.layouts.forEach((l) => {
243
- if (isUsableLayout(l)) allLayouts.add(l);
244
- });
245
- const layoutNames = /* @__PURE__ */ new Map();
246
- const pageNames = /* @__PURE__ */ new Map();
247
- const layoutTitles = /* @__PURE__ */ new Map();
248
- let li = 0, pi = 0;
249
- for (const l of allLayouts) {
250
- layoutNames.set(l, `Layout${li++}`);
251
- const title = parseLayoutTitle(l);
252
- if (title) layoutTitles.set(l, title);
253
- }
254
- for (const r of validRoutes) {
255
- if (!pageNames.has(r.filePath)) pageNames.set(r.filePath, `Page${pi++}`);
256
- }
257
- const lazyImports = [];
258
- for (const [fp, name] of layoutNames)
259
- lazyImports.push(`const ${name} = React.lazy(() => import('${toImportPath(fp, aliases)}'));`);
260
- if (notFound)
261
- lazyImports.push(`const NotFound = React.lazy(() => import('${toImportPath(import_path.default.join(APP_DIR, notFound), aliases)}'));`);
262
- const emittedPages = /* @__PURE__ */ new Set();
263
- for (const r of validRoutes) {
264
- if (emittedPages.has(r.filePath)) continue;
265
- emittedPages.add(r.filePath);
266
- const name = pageNames.get(r.filePath);
267
- if (!name) continue;
268
- lazyImports.push(`const ${name} = React.lazy(() => import('${toImportPath(r.filePath, aliases)}'));`);
269
- }
270
- const chainMap = /* @__PURE__ */ new Map();
271
- for (const r of validRoutes) {
272
- const key = r.layouts.join("|");
273
- if (!chainMap.has(key)) chainMap.set(key, { layouts: r.layouts, routes: [] });
274
- chainMap.get(key).routes.push(r);
275
- }
276
- const routeLines = [];
277
- for (const [, { layouts, routes: cr }] of chainMap)
278
- routeLines.push(renderChain(layouts, cr, layoutNames, pageNames, layoutTitles, 8));
279
- const catchAll = notFound ? ` <Route path="*" element={<Suspense fallback={<Spinner />}><NotFound /></Suspense>} />` : ` <Route path="*" element={<Default404 />} />`;
280
- return `// \u26A0\uFE0F Auto-generated by bini-router \u2014 do not edit.
281
- import React, { Suspense } from 'react';
282
- import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
283
- import './app/globals.css';
284
-
285
- ${lazyImports.join("\n")}
286
-
287
- // \u2500\u2500\u2500 Error Boundary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
288
- class ErrorBoundary extends React.Component<
289
- { children: React.ReactNode },
290
- { error: Error | null }
291
- > {
292
- constructor(props: { children: React.ReactNode }) {
293
- super(props);
294
- this.state = { error: null };
295
- }
296
- static getDerivedStateFromError(error: Error) { return { error }; }
297
- override render() {
298
- if (this.state.error) return (
299
- <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'system-ui,sans-serif', padding: '2rem' }}>
300
- <div style={{ maxWidth: 480, width: '100%', textAlign: 'center' }}>
301
- <h2 style={{ color: '#e74c3c', marginBottom: '1rem' }}>Something went wrong</h2>
302
- <pre style={{ background: '#fef2f2', padding: '1rem', borderRadius: '0.5rem', textAlign: 'left', fontSize: '0.8rem', color: '#e74c3c', overflow: 'auto' }}>{this.state.error.toString()}</pre>
303
- <button onClick={() => this.setState({ error: null })} style={{ marginTop: '1rem', padding: '0.5rem 1.5rem', background: '#00CFFF', color: 'white', border: 'none', borderRadius: '0.5rem', cursor: 'pointer', fontWeight: 600 }}>
304
- Try again
305
- </button>
306
- </div>
307
- </div>
308
- );
309
- return this.props.children;
310
- }
311
- }
312
-
313
- function Spinner() {
314
- return (
315
- <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
316
- <div style={{ width: 32, height: 32, border: '3px solid #eee', borderTop: '3px solid #00CFFF', borderRadius: '50%', animation: 'spin 0.8s linear infinite' }} />
317
- <style>{\`@keyframes spin{to{transform:rotate(360deg)}}\`}</style>
318
- </div>
319
- );
320
- }
321
-
322
- // Renders nothing \u2014 just sets document.title when the layout mounts.
323
- function TitleSetter({ title }: { title: string }) {
324
- React.useEffect(() => { document.title = title; }, [title]);
325
- return null;
326
- }
327
-
328
- ${notFound ? "" : `function Default404() {
329
- return (
330
- <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'linear-gradient(135deg,#00CFFF,#0077FF)', color: 'white', fontFamily: 'system-ui,sans-serif' }}>
331
- <div style={{ textAlign: 'center' }}>
332
- <h1 style={{ fontSize: '5rem', fontWeight: 800, margin: 0 }}>404</h1>
333
- <p style={{ fontSize: '1.25rem', margin: '0.5rem 0 2rem' }}>Page not found</p>
334
- <a href="/" style={{ padding: '0.65rem 1.5rem', background: 'white', color: '#00CFFF', textDecoration: 'none', borderRadius: '0.5rem', fontWeight: 600 }}>\u2190 Back to Home</a>
335
- </div>
336
- </div>
337
- );
338
- }`}
339
-
340
- export default function App() {
341
- return (
342
- <BrowserRouter>
343
- <Routes>
344
- ${routeLines.join("\n")}
345
- ${catchAll}
346
- </Routes>
347
- </BrowserRouter>
348
- );
349
- }
350
- `;
351
- }
352
- function parseAppMetadata() {
353
- const layout = findFile(APP_DIR, LAYOUT_FILES);
354
- if (!layout) return {};
355
- let src = "";
356
- try {
357
- src = import_fs.default.readFileSync(import_path.default.join(APP_DIR, layout), "utf8");
358
- } catch {
359
- return {};
360
- }
361
- const startIdx = src.indexOf("export const metadata");
362
- if (startIdx === -1) return {};
363
- const braceStart = src.indexOf("{", startIdx);
364
- if (braceStart === -1) return {};
365
- let depth = 0, end = braceStart;
366
- for (let i = braceStart; i < src.length; i++) {
367
- if (src[i] === "{") depth++;
368
- else if (src[i] === "}") {
369
- depth--;
370
- if (depth === 0) {
371
- end = i;
372
- break;
373
- }
374
- }
375
- }
376
- const block = src.slice(braceStart, end + 1);
377
- function extractBlock(source, key) {
378
- const re = new RegExp(`['"]?${key}['"]?\\s*:\\s*\\{`);
379
- const match = re.exec(source);
380
- if (!match) return void 0;
381
- let d = 0, i = match.index + match[0].length - 1;
382
- const start = i;
383
- for (; i < source.length; i++) {
384
- if (source[i] === "{") d++;
385
- else if (source[i] === "}") {
386
- d--;
387
- if (d === 0) return source.slice(start, i + 1);
388
- }
389
- }
390
- return void 0;
391
- }
392
- function extractArray(source, key) {
393
- const re = new RegExp(`['"]?${key}['"]?\\s*:\\s*\\[`);
394
- const match = re.exec(source);
395
- if (!match) return void 0;
396
- let d = 0, i = match.index + match[0].length - 1;
397
- const start = i;
398
- for (; i < source.length; i++) {
399
- if (source[i] === "[") d++;
400
- else if (source[i] === "]") {
401
- d--;
402
- if (d === 0) return source.slice(start, i + 1);
99
+ const filePath = import_path.default.join(fullPath, pageFile);
100
+ if (hasDefaultExport(filePath)) {
101
+ routes.push({
102
+ routePath,
103
+ filePath,
104
+ layouts: resolveLayoutChain(fullPath, appDir),
105
+ dynamic: isDynamic
106
+ });
403
107
  }
404
108
  }
405
- return void 0;
406
- }
407
- function str(source, key) {
408
- return source.match(
409
- new RegExp(`['"]?${key}['"]?\\s*:\\s*['"\`]([^'"\`
410
- ]+)['"\`]`)
411
- )?.[1];
412
- }
413
- function firstArrayStr(source, key) {
414
- const arr = extractArray(source, key);
415
- if (!arr) return void 0;
416
- return arr.match(/url\s*:\s*['"]([^'"]+)['"]/)?.[1] ?? arr.match(/['"]([^'"]+)['"]/)?.[1];
417
- }
418
- function allArrayStrs(source, key) {
419
- const arr = extractArray(source, key);
420
- if (!arr) return [];
421
- return [...arr.matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
422
- }
423
- const meta = {};
424
- if (str(block, "title")) meta.title = str(block, "title");
425
- if (str(block, "description")) meta.description = str(block, "description");
426
- if (str(block, "viewport")) meta.viewport = str(block, "viewport");
427
- if (str(block, "themeColor")) meta.themeColor = str(block, "themeColor");
428
- if (str(block, "charset")) meta.charset = str(block, "charset");
429
- if (str(block, "robots")) meta.robots = str(block, "robots");
430
- if (str(block, "canonical")) meta.canonical = str(block, "canonical");
431
- if (str(block, "manifest")) meta.manifest = str(block, "manifest");
432
- const kwStr = str(block, "keywords");
433
- if (kwStr) {
434
- meta.keywords = kwStr;
435
- } else {
436
- const kwArr = allArrayStrs(block, "keywords");
437
- if (kwArr.length) meta.keywords = kwArr.join(", ");
438
- }
439
- const authorStr = str(block, "author");
440
- if (authorStr) {
441
- meta.author = authorStr;
442
- } else {
443
- const authorsArr = extractArray(block, "authors");
444
- if (authorsArr) {
445
- const name = authorsArr.match(/name\s*:\s*['"]([^'"]+)['"]/)?.[1];
446
- if (name) meta.author = name;
447
- }
448
- }
449
- if (!meta.canonical) {
450
- const base = block.match(/metadataBase\s*:\s*new\s+URL\s*\(\s*['"]([^'"]+)['"]/)?.[1];
451
- if (base) meta.canonical = base;
452
- }
453
- const ogBlock = extractBlock(block, "openGraph");
454
- if (ogBlock) {
455
- meta.openGraph = {
456
- title: str(ogBlock, "title"),
457
- description: str(ogBlock, "description"),
458
- url: str(ogBlock, "url"),
459
- type: str(ogBlock, "type"),
460
- image: firstArrayStr(ogBlock, "images") ?? str(ogBlock, "image")
461
- };
462
- }
463
- const twBlock = extractBlock(block, "twitter");
464
- if (twBlock) {
465
- meta.twitter = {
466
- card: str(twBlock, "card"),
467
- title: str(twBlock, "title"),
468
- description: str(twBlock, "description"),
469
- creator: str(twBlock, "creator"),
470
- image: firstArrayStr(twBlock, "images") ?? str(twBlock, "image")
471
- };
472
- }
473
- const iconsBlock = extractBlock(block, "icons");
474
- if (iconsBlock) {
475
- meta.icons = {
476
- icon: collectIconEntries(iconsBlock, "icon"),
477
- shortcut: collectIconEntries(iconsBlock, "shortcut"),
478
- apple: collectIconEntries(iconsBlock, "apple")
479
- };
480
- }
481
- return meta;
482
- }
483
- function collectIconEntries(source, key) {
484
- const arr = extractArrayRaw(source, key);
485
- if (!arr) return [];
486
- const entries = [];
487
- const objRe = /\{([^}]+)\}/g;
488
- let m;
489
- while ((m = objRe.exec(arr)) !== null) {
490
- const obj = m[1];
491
- const url = obj.match(/url\s*:\s*['"]([^'"]+)['"]/)?.[1];
492
- if (!url) continue;
493
- entries.push({
494
- url,
495
- type: obj.match(/type\s*:\s*['"]([^'"]+)['"]/)?.[1],
496
- sizes: obj.match(/sizes\s*:\s*['"]([^'"]+)['"]/)?.[1]
497
- });
498
- }
499
- if (!entries.length) {
500
- return [...arr.matchAll(/['"]([^'"]+)['"]/g)].map((x) => ({ url: x[1] }));
501
- }
502
- return entries;
503
- }
504
- function extractArrayRaw(source, key) {
505
- const re = new RegExp(`['"]?${key}['"]?\\s*:\\s*\\[`);
506
- const match = re.exec(source);
507
- if (!match) return void 0;
508
- let d = 0, i = match.index + match[0].length - 1;
509
- const start = i;
510
- for (; i < source.length; i++) {
511
- if (source[i] === "[") d++;
512
- else if (source[i] === "]") {
513
- d--;
514
- if (d === 0) return source.slice(start, i + 1);
515
- }
109
+ routes.push(...scanPages(fullPath, appDir, routePath));
516
110
  }
517
- return void 0;
111
+ return routes;
518
112
  }
519
- function scanApiRoutes(dir = API_DIR, baseRoute = "") {
113
+ function scanApiRoutes(dir, baseRoute = "") {
520
114
  const routes = [];
521
115
  if (!import_fs.default.existsSync(dir)) return routes;
522
- let entries;
523
- try {
524
- entries = import_fs.default.readdirSync(dir, { withFileTypes: true });
525
- } catch {
526
- return routes;
527
- }
116
+ const entries = import_fs.default.readdirSync(dir, { withFileTypes: true });
528
117
  for (const entry of entries) {
529
118
  if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
530
119
  const fullPath = import_path.default.join(dir, entry.name);
@@ -540,307 +129,379 @@ function scanApiRoutes(dir = API_DIR, baseRoute = "") {
540
129
  if (!API_EXTS.includes(ext)) continue;
541
130
  const isCatchAll = base.startsWith("[...") && base.endsWith("]");
542
131
  const isDynamic = base.startsWith("[") && base.endsWith("]");
543
- const routePath = isCatchAll ? `${baseRoute}/*` : base === "index" ? baseRoute || "/" : isDynamic ? `${baseRoute}/:${base.slice(1, -1)}` : `${baseRoute}/${base}`;
132
+ const isIndex = base === "index";
133
+ let routePath;
134
+ if (isCatchAll) {
135
+ routePath = `${baseRoute}/*`;
136
+ } else if (isIndex) {
137
+ routePath = baseRoute || "/";
138
+ } else if (isDynamic) {
139
+ routePath = `${baseRoute}/:${base.slice(1, -1)}`;
140
+ } else {
141
+ routePath = `${baseRoute}/${base}`;
142
+ }
544
143
  routes.push({ routePath, filePath: fullPath });
545
144
  }
546
145
  return routes;
547
146
  }
548
- function generateNitroApiEntry() {
549
- const routes = scanApiRoutes();
550
- if (!routes.length) return "";
551
- const imports = [];
552
- const registrations = [];
553
- routes.forEach((route, i) => {
554
- const sourcePath = route.filePath.replace(/\\/g, "/");
555
- const projectRoot = process.cwd().replace(/\\/g, "/");
556
- const relativePath = sourcePath.startsWith(projectRoot) ? "./" + import_path.default.relative(projectRoot, sourcePath).replace(/\\/g, "/") : sourcePath;
557
- imports.push(`import handler${i} from '${relativePath}';`);
558
- registrations.push(
559
- ` app.all('${route.routePath}', async (c) => { try { return await handler${i}(c.req.raw); } catch (e: any) { return c.json({ error: e.message }, 500); } });`
560
- );
147
+ function parseMetadata(appDir) {
148
+ const layoutFile = findFile(appDir, LAYOUT_FILES);
149
+ if (!layoutFile) return {};
150
+ const layoutPath = import_path.default.join(appDir, layoutFile);
151
+ let content = "";
152
+ try {
153
+ content = import_fs.default.readFileSync(layoutPath, "utf8");
154
+ } catch {
155
+ return {};
156
+ }
157
+ const meta = {};
158
+ const metadataMatch = content.match(/export const metadata = ({[^;]+})/);
159
+ if (!metadataMatch) return meta;
160
+ const metadataStr = metadataMatch[1];
161
+ const title = metadataStr.match(/title:\s*['"`]([^'"`]+)['"`]/);
162
+ if (title) meta.title = title[1];
163
+ const description = metadataStr.match(/description:\s*['"`]([^'"`]+)['"`]/);
164
+ if (description) meta.description = description[1];
165
+ const viewport = metadataStr.match(/viewport:\s*['"`]([^'"`]+)['"`]/);
166
+ if (viewport) meta.viewport = viewport[1];
167
+ const themeColor = metadataStr.match(/themeColor:\s*['"`]([^'"`]+)['"`]/);
168
+ if (themeColor) meta.themeColor = themeColor[1];
169
+ const charset = metadataStr.match(/charset:\s*['"`]([^'"`]+)['"`]/);
170
+ if (charset) meta.charset = charset[1];
171
+ const robots = metadataStr.match(/robots:\s*['"`]([^'"`]+)['"`]/);
172
+ if (robots) meta.robots = robots[1];
173
+ return meta;
174
+ }
175
+ function generateReactRouterApp(appDir) {
176
+ if (!import_fs.default.existsSync(appDir)) return null;
177
+ const routes = scanPages(appDir, appDir);
178
+ const rootPage = findFile(appDir, PAGE_FILES);
179
+ if (rootPage) {
180
+ const rootPath = import_path.default.join(appDir, rootPage);
181
+ if (hasDefaultExport(rootPath)) {
182
+ routes.unshift({
183
+ routePath: "/",
184
+ filePath: rootPath,
185
+ layouts: resolveLayoutChain(appDir, appDir),
186
+ dynamic: false
187
+ });
188
+ }
189
+ }
190
+ const notFoundFile = NOT_FOUND_FILES.find(
191
+ (f) => import_fs.default.existsSync(import_path.default.join(appDir, f)) && hasDefaultExport(import_path.default.join(appDir, f))
192
+ );
193
+ const layoutGroups = /* @__PURE__ */ new Map();
194
+ for (const route of routes) {
195
+ const key = route.layouts.join("|");
196
+ if (!layoutGroups.has(key)) layoutGroups.set(key, []);
197
+ layoutGroups.get(key).push(route);
198
+ }
199
+ const pageImports = [];
200
+ const layoutImports = [];
201
+ const pageNames = /* @__PURE__ */ new Map();
202
+ const layoutNames = /* @__PURE__ */ new Map();
203
+ let pageCounter = 0;
204
+ let layoutCounter = 0;
205
+ for (const route of routes) {
206
+ if (!pageNames.has(route.filePath)) {
207
+ const name = `Page${pageCounter++}`;
208
+ pageNames.set(route.filePath, name);
209
+ const importPath = import_path.default.relative(appDir, route.filePath).replace(/\\/g, "/").replace(/\.tsx?$/, "");
210
+ pageImports.push(`const ${name} = React.lazy(() => import('./${importPath}'));`);
211
+ }
212
+ for (const layout of route.layouts) {
213
+ if (!layoutNames.has(layout)) {
214
+ const name = `Layout${layoutCounter++}`;
215
+ layoutNames.set(layout, name);
216
+ const importPath = import_path.default.relative(appDir, layout).replace(/\\/g, "/").replace(/\.tsx?$/, "");
217
+ layoutImports.push(`const ${name} = React.lazy(() => import('./${importPath}'));`);
218
+ }
219
+ }
220
+ }
221
+ function renderLayoutChain(layouts, routes2, indent = 8) {
222
+ const pad = " ".repeat(indent);
223
+ if (layouts.length === 0) {
224
+ return routes2.map(
225
+ (r) => `${pad}<Route path="${r.routePath}" element={<${pageNames.get(r.filePath)} />} />`
226
+ ).join("\n");
227
+ }
228
+ const [current, ...rest] = layouts;
229
+ const inner = renderLayoutChain(rest, routes2, indent + 2);
230
+ return `${pad}<Route element={<${layoutNames.get(current)}><Outlet /></${layoutNames.get(current)}>}>
231
+ ${inner}
232
+ ${pad}</Route>`;
233
+ }
234
+ const routeSections = [];
235
+ layoutGroups.forEach((groupRoutes) => {
236
+ if (groupRoutes.length > 0) {
237
+ routeSections.push(renderLayoutChain(groupRoutes[0].layouts, groupRoutes));
238
+ }
561
239
  });
240
+ const notFoundRoute = notFoundFile ? ` <Route path="*" element={<NotFound />} />` : ` <Route path="*" element={<div>404 - Page Not Found</div>} />`;
562
241
  return `// Auto-generated by bini-router
563
- import { Hono } from 'hono';
564
- ${imports.join("\n")}
242
+ import React, { Suspense } from 'react';
243
+ import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
565
244
 
566
- const app = new Hono();
567
- ${registrations.join("\n")}
245
+ ${layoutImports.join("\n")}
246
+ ${pageImports.join("\n")}
247
+ ${notFoundFile ? `const NotFound = React.lazy(() => import('./${import_path.default.basename(notFoundFile).replace(/\.tsx?$/, "")}'));` : ""}
568
248
 
569
- export default app;
570
- `;
249
+ function Loading() {
250
+ return <div style={{ padding: '2rem', textAlign: 'center' }}>Loading...</div>;
571
251
  }
572
- function buildApiWithNitro() {
573
- const routes = scanApiRoutes();
574
- if (routes.length === 0) return;
575
- console.log("\n\u{1F4E6} Building API routes with Nitro...");
576
- const serverDir = import_path.default.join(process.cwd(), "server");
577
- import_fs.default.mkdirSync(serverDir, { recursive: true });
578
- const entry = generateNitroApiEntry();
579
- import_fs.default.writeFileSync(import_path.default.join(serverDir, "index.ts"), entry, "utf8");
580
- const nitroConfigPath = import_path.default.join(process.cwd(), "nitro.config.ts");
581
- if (!import_fs.default.existsSync(nitroConfigPath)) {
582
- const config = `import { defineNitroConfig } from 'nitropack/config';
583
252
 
584
- export default defineNitroConfig({
585
- compatibilityDate: '2026-03-11',
586
- handlers: [
587
- { route: '/api/**', handler: '~/server/index' }
588
- ],
589
- output: {
590
- dir: '.output',
591
- publicDir: '.output/public',
592
- serverDir: '.output/server'
593
- }
594
- });
253
+ export default function App() {
254
+ return (
255
+ <BrowserRouter>
256
+ <Suspense fallback={<Loading />}>
257
+ <Routes>
258
+ ${routeSections.join("\n\n")}
259
+ ${notFoundRoute}
260
+ </Routes>
261
+ </Suspense>
262
+ </BrowserRouter>
263
+ );
264
+ }
595
265
  `;
596
- import_fs.default.writeFileSync(nitroConfigPath, config, "utf8");
597
- }
598
- try {
599
- console.log("\u{1F528} Building with Nitro...");
600
- (0, import_child_process.execSync)("npx nitropack build", {
601
- stdio: "inherit",
602
- cwd: process.cwd(),
603
- env: { ...process.env, NODE_ENV: "production" }
604
- });
605
- console.log("\u2705 API routes built successfully");
606
- const distDir = import_path.default.join(process.cwd(), "dist");
607
- const publicDir = import_path.default.join(process.cwd(), ".output", "public");
608
- if (import_fs.default.existsSync(distDir)) {
609
- console.log("\u{1F4C2} Copying frontend build to Nitro public directory...");
610
- if (import_fs.default.existsSync(publicDir)) {
611
- import_fs.default.rmSync(publicDir, { recursive: true, force: true });
612
- }
613
- import_fs.default.cpSync(distDir, publicDir, { recursive: true });
614
- console.log("\u2705 Frontend assets copied to .output/public");
615
- }
616
- } catch (error) {
617
- console.error("\u274C API build failed:", error);
618
- process.exit(1);
619
- }
620
266
  }
621
- function biniroute() {
622
- let debounceTimer = null;
623
- let lastGeneratedCode = "";
624
- const eventLog = /* @__PURE__ */ new Map();
625
- function shouldProcess(file, event) {
626
- const key = `${file}:${event}`;
627
- const now = Date.now();
628
- if (now - (eventLog.get(key) ?? 0) < EVENT_DEDUP_MS) return false;
629
- eventLog.set(key, now);
630
- for (const [k, v] of eventLog) if (now - v > EVENT_TTL_MS) eventLog.delete(k);
631
- return true;
632
- }
633
- function isPageFile(f) {
634
- const nf = norm(f);
635
- const base = import_path.default.basename(f, import_path.default.extname(f));
636
- const ext = import_path.default.extname(f);
637
- if (!SUPPORTED_EXTS.includes(ext)) return false;
638
- if (!isInDir(nf, norm(APP_DIR))) return false;
639
- if (isInDir(nf, norm(API_DIR))) return false;
640
- if (base.startsWith("_")) return false;
641
- return true;
642
- }
643
- function isApiFile(f) {
644
- const nf = norm(f);
645
- return isInDir(nf, norm(API_DIR)) && API_EXTS.includes(import_path.default.extname(f));
646
- }
647
- function applyApp() {
648
- if (!import_fs.default.existsSync(APP_DIR)) return null;
649
- const code = generateApp();
650
- if (code === lastGeneratedCode) return null;
651
- import_fs.default.writeFileSync(getAppFile(), code, "utf8");
652
- lastGeneratedCode = code;
653
- return code;
654
- }
655
- function scheduleRegen(server, delay = DEBOUNCE_MS) {
656
- if (debounceTimer) clearTimeout(debounceTimer);
657
- debounceTimer = setTimeout(() => {
658
- debounceTimer = null;
659
- if (applyApp() !== null) {
660
- server.ws.send({ type: "full-reload", path: "*" });
267
+ async function createDevServer(appDir, apiDir, enableCors) {
268
+ const { Hono } = await import("hono");
269
+ const app = new Hono();
270
+ if (enableCors) {
271
+ const { cors } = await import("hono/cors");
272
+ app.use("*", cors({
273
+ origin: "*",
274
+ allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
275
+ allowHeaders: ["Content-Type", "Authorization"]
276
+ }));
277
+ }
278
+ const routes = scanApiRoutes(apiDir);
279
+ for (const route of routes) {
280
+ const routePath = route.routePath;
281
+ const filePath = route.filePath;
282
+ app.all(routePath, async (c) => {
283
+ try {
284
+ const mod = await import(filePath + "?t=" + Date.now());
285
+ if (mod.default) {
286
+ if (typeof mod.default === "function") {
287
+ return await mod.default(c.req.raw);
288
+ }
289
+ return c.json({ error: "Default export must be a function" }, 500);
290
+ }
291
+ const method = c.req.method.toLowerCase();
292
+ if (mod[method] && typeof mod[method] === "function") {
293
+ return await mod[method](c.req.raw);
294
+ }
295
+ return c.json({ error: "No handler found" }, 404);
296
+ } catch (e) {
297
+ console.error(`API Error (${routePath}):`, e);
298
+ return c.json({ error: e.message }, 500);
661
299
  }
662
- }, delay);
663
- }
664
- function addSpaFallback(server) {
665
- server.middlewares.use((req, res, next) => {
666
- const url = req.url;
667
- if (url.includes(".")) return next();
668
- req.url = "/index.html";
669
- next();
670
300
  });
671
301
  }
302
+ return app;
303
+ }
304
+ function bini(options = {}) {
305
+ const {
306
+ srcDir = "src",
307
+ appDir = "src/app",
308
+ apiDir = "src/app/api",
309
+ api = true,
310
+ cors = true,
311
+ outDir = "dist"
312
+ } = options;
313
+ const cwd = process.cwd();
314
+ const fullAppDir = import_path.default.join(cwd, appDir);
315
+ const fullApiDir = import_path.default.join(cwd, apiDir);
316
+ const fullOutDir = import_path.default.join(cwd, outDir);
317
+ let devApp = null;
318
+ let watcher = null;
672
319
  return {
673
- name: "bini-router",
320
+ name: "vite-plugin-bini",
674
321
  enforce: "pre",
675
- transform(code, id) {
676
- const nid = norm(id);
677
- if (!isInDir(nid, norm(APP_DIR))) return;
678
- const ext = import_path.default.extname(id);
679
- if (!SUPPORTED_EXTS.includes(ext)) return;
680
- if (!code.includes("export const metadata")) return;
681
- let result = code;
682
- let idx = result.indexOf("export const metadata");
683
- while (idx !== -1) {
684
- const braceIdx = result.indexOf("{", idx);
685
- if (braceIdx === -1) break;
686
- let depth = 0, end = braceIdx;
687
- for (let i = braceIdx; i < result.length; i++) {
688
- if (result[i] === "{") depth++;
689
- else if (result[i] === "}") {
690
- depth--;
691
- if (depth === 0) {
692
- end = i;
693
- break;
694
- }
695
- }
322
+ // Transform index.html with metadata
323
+ transformIndexHtml: {
324
+ order: "pre",
325
+ handler(html) {
326
+ const meta = parseMetadata(fullAppDir);
327
+ if (!meta.title && !meta.description) return html;
328
+ const tags = [];
329
+ if (meta.charset) {
330
+ tags.push(`<meta charset="${meta.charset}">`);
331
+ }
332
+ if (meta.viewport) {
333
+ tags.push(`<meta name="viewport" content="${meta.viewport}">`);
334
+ } else {
335
+ tags.push(`<meta name="viewport" content="width=device-width, initial-scale=1.0">`);
696
336
  }
697
- let tail = end + 1;
698
- while (tail < result.length && (result[tail] === " " || result[tail] === " ")) tail++;
699
- if (tail < result.length && result[tail] === ";") tail++;
700
- while (tail < result.length && (result[tail] === "\n" || result[tail] === "\r")) tail++;
701
- result = result.slice(0, idx) + result.slice(tail);
702
- idx = result.indexOf("export const metadata", idx);
337
+ if (meta.title) {
338
+ tags.push(`<title>${meta.title}</title>`);
339
+ }
340
+ if (meta.description) {
341
+ tags.push(`<meta name="description" content="${meta.description}">`);
342
+ }
343
+ if (meta.themeColor) {
344
+ tags.push(`<meta name="theme-color" content="${meta.themeColor}">`);
345
+ }
346
+ if (meta.keywords) {
347
+ tags.push(`<meta name="keywords" content="${meta.keywords}">`);
348
+ }
349
+ if (meta.author) {
350
+ tags.push(`<meta name="author" content="${meta.author}">`);
351
+ }
352
+ if (meta.robots) {
353
+ tags.push(`<meta name="robots" content="${meta.robots}">`);
354
+ }
355
+ return html.replace(
356
+ "</head>",
357
+ tags.map((t) => ` ${t}`).join("\n") + "\n</head>"
358
+ );
703
359
  }
704
- return { code: result, map: null };
705
- },
706
- config() {
707
- applyApp();
708
360
  },
361
+ // Generate React Router app during build
709
362
  buildStart() {
710
- applyApp();
711
- },
712
- // This runs after Vite build is complete
713
- closeBundle() {
714
- buildApiWithNitro();
715
- },
716
- buildEnd() {
717
- if (debounceTimer) {
718
- clearTimeout(debounceTimer);
719
- debounceTimer = null;
363
+ if (!import_fs.default.existsSync(fullAppDir)) return;
364
+ const appContent = generateReactRouterApp(fullAppDir);
365
+ if (appContent) {
366
+ const appPath = import_path.default.join(cwd, srcDir, "App.tsx");
367
+ import_fs.default.writeFileSync(appPath, appContent, "utf-8");
368
+ this.addWatchFile(appPath);
720
369
  }
721
370
  },
371
+ // Handle API routes in dev server
722
372
  async configureServer(server) {
723
- if (!import_fs.default.existsSync(APP_DIR)) return;
724
- server.watcher.add(APP_DIR);
725
- server.watcher.on("add", (f) => isPageFile(f) && shouldProcess(f, "add") && scheduleRegen(server, 300));
726
- server.watcher.on("unlink", (f) => isPageFile(f) && shouldProcess(f, "unlink") && scheduleRegen(server));
727
- server.watcher.on("change", (f) => isPageFile(f) && shouldProcess(f, "change") && scheduleRegen(server));
728
- server.watcher.on("change", (f) => {
729
- const base = import_path.default.basename(f, import_path.default.extname(f));
730
- const inAppRoot = import_path.default.resolve(import_path.default.dirname(f)) === import_path.default.resolve(APP_DIR);
731
- if (!inAppRoot || base !== "layout") return;
732
- server.moduleGraph.invalidateAll();
733
- server.ws.send({ type: "full-reload", path: "*" });
373
+ watcher = server;
374
+ if (!api || !import_fs.default.existsSync(fullApiDir)) return;
375
+ server.watcher.add(fullApiDir);
376
+ const reloadApi = () => {
377
+ devApp = null;
378
+ server.ws.send({ type: "full-reload", path: "/api/*" });
379
+ };
380
+ server.watcher.on("add", (file) => {
381
+ if (file.startsWith(fullApiDir)) reloadApi();
734
382
  });
735
- server.watcher.on("addDir", (d) => {
736
- if (!isInDir(norm(d), norm(APP_DIR)) || d.includes("node_modules") || d.includes("api")) return;
737
- setTimeout(() => PAGE_FILES.some((f) => import_fs.default.existsSync(import_path.default.join(d, f))) && scheduleRegen(server), 300);
383
+ server.watcher.on("unlink", (file) => {
384
+ if (file.startsWith(fullApiDir)) reloadApi();
738
385
  });
739
- server.watcher.on("unlinkDir", (d) => {
740
- if (isInDir(norm(d), norm(APP_DIR)) && !d.includes("node_modules") && !d.includes("api"))
741
- scheduleRegen(server);
386
+ server.watcher.on("change", (file) => {
387
+ if (file.startsWith(fullApiDir)) reloadApi();
742
388
  });
743
- if (import_fs.default.existsSync(API_DIR)) {
744
- server.middlewares.use("/api", async (req, res, next) => {
745
- try {
746
- const { Hono } = await import("hono");
747
- const app = new Hono();
748
- const routes = scanApiRoutes();
749
- for (const route of routes) {
750
- const importUrl = (0, import_url.pathToFileURL)(route.filePath).href + "?t=" + Date.now();
751
- const mod = await import(
752
- /* @vite-ignore */
753
- importUrl
754
- );
755
- const handler = mod.default;
756
- if (typeof handler === "function") {
757
- app.all(route.routePath, async (c) => {
758
- try {
759
- return await handler(c.req.raw);
760
- } catch (e) {
761
- return c.json({ error: e.message }, 500);
762
- }
763
- });
764
- }
765
- }
766
- const url = `http://${req.headers.host}${req.url}`;
767
- const chunks = [];
768
- for await (const chunk of req) chunks.push(chunk);
389
+ server.middlewares.use("/api", async (req, res, next) => {
390
+ try {
391
+ if (!devApp) {
392
+ devApp = await createDevServer(fullAppDir, fullApiDir, cors);
393
+ }
394
+ const url = `http://${req.headers.host}${req.url}`;
395
+ const chunks = [];
396
+ req.on("data", (chunk) => chunks.push(chunk));
397
+ req.on("end", async () => {
769
398
  const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
770
- const webReq = new Request(url, {
399
+ const request = new Request(url, {
771
400
  method: req.method,
772
401
  headers: req.headers,
773
402
  body
774
403
  });
775
- const webRes = await app.fetch(webReq);
776
- res.statusCode = webRes.status;
777
- webRes.headers.forEach((value, key) => res.setHeader(key, value));
778
- const buffer = await webRes.arrayBuffer();
779
- res.end(Buffer.from(buffer));
780
- } catch (e) {
781
- console.error("API Error:", e);
782
- next(e);
783
- }
784
- });
785
- }
404
+ try {
405
+ const response = await devApp.fetch(request);
406
+ res.statusCode = response.status;
407
+ response.headers.forEach((value, key) => {
408
+ res.setHeader(key, value);
409
+ });
410
+ const buffer = Buffer.from(await response.arrayBuffer());
411
+ res.end(buffer);
412
+ } catch (err) {
413
+ next(err);
414
+ }
415
+ });
416
+ } catch (err) {
417
+ next(err);
418
+ }
419
+ });
786
420
  },
421
+ // Handle preview server
787
422
  async configurePreviewServer(server) {
788
- addSpaFallback(server);
789
- },
790
- transformIndexHtml: {
791
- order: "pre",
792
- handler(html) {
793
- const meta = parseAppMetadata();
794
- const title = meta.title ?? "Bini App";
795
- const vp = meta.viewport ?? "width=device-width, initial-scale=1.0";
796
- const lines = [];
797
- lines.push(`<meta charset="${meta.charset ?? "UTF-8"}" />`);
798
- lines.push(`<meta name="viewport" content="${vp}" />`);
799
- lines.push(`<title>${title}</title>`);
800
- if (meta.description) lines.push(`<meta name="description" content="${meta.description}" />`);
801
- if (meta.themeColor) lines.push(`<meta name="theme-color" content="${meta.themeColor}" />`);
802
- if (meta.robots) lines.push(`<meta name="robots" content="${meta.robots}" />`);
803
- if (meta.keywords) lines.push(`<meta name="keywords" content="${meta.keywords}" />`);
804
- if (meta.author) lines.push(`<meta name="author" content="${meta.author}" />`);
805
- if (meta.canonical) lines.push(`<link rel="canonical" href="${meta.canonical}" />`);
806
- if (meta.manifest) lines.push(`<link rel="manifest" href="${meta.manifest}" />`);
807
- for (const entry of meta.icons?.icon ?? []) {
808
- const type = entry.type ? ` type="${entry.type}"` : "";
809
- const sizes = entry.sizes ? ` sizes="${entry.sizes}"` : "";
810
- lines.push(`<link rel="icon" href="${entry.url}"${type}${sizes} />`);
811
- }
812
- for (const entry of meta.icons?.shortcut ?? []) {
813
- lines.push(`<link rel="shortcut icon" href="${entry.url}" />`);
423
+ if (!api || !import_fs.default.existsSync(fullApiDir)) return;
424
+ server.middlewares.use("/api", async (req, res, next) => {
425
+ try {
426
+ const app = await createDevServer(fullAppDir, fullApiDir, cors);
427
+ const url = `http://${req.headers.host}${req.url}`;
428
+ const chunks = [];
429
+ req.on("data", (chunk) => chunks.push(chunk));
430
+ req.on("end", async () => {
431
+ const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
432
+ const request = new Request(url, {
433
+ method: req.method,
434
+ headers: req.headers,
435
+ body
436
+ });
437
+ const response = await app.fetch(request);
438
+ res.statusCode = response.status;
439
+ response.headers.forEach((value, key) => {
440
+ res.setHeader(key, value);
441
+ });
442
+ const buffer = Buffer.from(await response.arrayBuffer());
443
+ res.end(buffer);
444
+ });
445
+ } catch (err) {
446
+ next(err);
814
447
  }
815
- for (const entry of meta.icons?.apple ?? []) {
816
- const sizes = entry.sizes ? ` sizes="${entry.sizes}"` : "";
817
- const type = entry.type ? ` type="${entry.type}"` : "";
818
- lines.push(`<link rel="apple-touch-icon" href="${entry.url}"${sizes}${type} />`);
448
+ });
449
+ },
450
+ // Copy API files to dist (with user's adapter code intact)
451
+ async generateBundle() {
452
+ if (!api || !import_fs.default.existsSync(fullApiDir)) return;
453
+ const processDir = (dir) => {
454
+ const entries = import_fs.default.readdirSync(dir, { withFileTypes: true });
455
+ for (const entry of entries) {
456
+ const srcPath = import_path.default.join(dir, entry.name);
457
+ const relPath = import_path.default.relative(fullApiDir, srcPath);
458
+ if (entry.isDirectory()) {
459
+ processDir(srcPath);
460
+ } else if (entry.isFile()) {
461
+ const ext = import_path.default.extname(entry.name);
462
+ if (![".ts", ".js", ".tsx", ".jsx"].includes(ext)) continue;
463
+ const content = import_fs.default.readFileSync(srcPath, "utf-8");
464
+ const outFileName = entry.name.replace(/\.tsx?$/, ".js").replace(/\.jsx?$/, ".js");
465
+ const outPath = import_path.default.join("app", "api", relPath.replace(/\.tsx?$/, ".js").replace(/\.jsx?$/, ".js"));
466
+ this.emitFile({
467
+ type: "asset",
468
+ fileName: outPath,
469
+ source: content
470
+ });
471
+ }
819
472
  }
820
- if (meta.openGraph?.title) {
821
- lines.push(`<meta property="og:type" content="${meta.openGraph.type ?? "website"}" />`);
822
- lines.push(`<meta property="og:title" content="${meta.openGraph.title}" />`);
823
- if (meta.openGraph.description) lines.push(`<meta property="og:description" content="${meta.openGraph.description}" />`);
824
- if (meta.openGraph.url) lines.push(`<meta property="og:url" content="${meta.openGraph.url}" />`);
825
- if (meta.openGraph.image) lines.push(`<meta property="og:image" content="${meta.openGraph.image}" />`);
473
+ };
474
+ processDir(fullApiDir);
475
+ const hasVercelAdapter = (() => {
476
+ try {
477
+ return import_fs.default.readdirSync(fullApiDir, { recursive: true }).some((f) => f.toString().includes("hono/vercel"));
478
+ } catch {
479
+ return false;
826
480
  }
827
- if (meta.twitter?.title) {
828
- lines.push(`<meta name="twitter:card" content="${meta.twitter.card ?? "summary_large_image"}" />`);
829
- lines.push(`<meta name="twitter:title" content="${meta.twitter.title}" />`);
830
- if (meta.twitter.description) lines.push(`<meta name="twitter:description" content="${meta.twitter.description}" />`);
831
- if (meta.twitter.creator) lines.push(`<meta name="twitter:creator" content="${meta.twitter.creator}" />`);
832
- if (meta.twitter.image) lines.push(`<meta name="twitter:image" content="${meta.twitter.image}" />`);
481
+ })();
482
+ if (hasVercelAdapter) {
483
+ const vercelPath = import_path.default.join(cwd, "vercel.json");
484
+ if (!import_fs.default.existsSync(vercelPath)) {
485
+ const vercelConfig = {
486
+ functions: {
487
+ "app/api/**/*": {
488
+ runtime: "edge@0.2.1"
489
+ }
490
+ }
491
+ };
492
+ import_fs.default.writeFileSync(vercelPath, JSON.stringify(vercelConfig, null, 2));
833
493
  }
834
- const injected = lines.map((l) => ` ${l}`).join("\n");
835
- 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}
836
- </head>`);
837
494
  }
495
+ },
496
+ // Clean up
497
+ buildEnd() {
498
+ devApp = null;
838
499
  }
839
500
  };
840
501
  }
841
- var index_default = biniroute;
502
+ var index_default = bini;
842
503
  // Annotate the CommonJS export names for ESM import in node:
843
504
  0 && (module.exports = {
844
- biniroute
505
+ bini
845
506
  });
846
507
  //# sourceMappingURL=index.cjs.map