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 +362 -701
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -39
- package/dist/index.d.ts +15 -39
- package/dist/index.js +361 -700
- package/dist/index.js.map +1 -1
- package/package.json +24 -27
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
|
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
|
|
115
|
-
|
|
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
|
|
70
|
+
function scanPages(dir, appDir, baseRoute = "") {
|
|
123
71
|
const routes = [];
|
|
124
72
|
if (!import_fs.default.existsSync(dir)) return routes;
|
|
125
|
-
|
|
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()
|
|
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 (
|
|
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
|
|
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 === "
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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
|
|
111
|
+
return routes;
|
|
518
112
|
}
|
|
519
|
-
function scanApiRoutes(dir
|
|
113
|
+
function scanApiRoutes(dir, baseRoute = "") {
|
|
520
114
|
const routes = [];
|
|
521
115
|
if (!import_fs.default.existsSync(dir)) return routes;
|
|
522
|
-
|
|
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
|
|
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
|
|
549
|
-
const
|
|
550
|
-
if (!
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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 {
|
|
564
|
-
|
|
242
|
+
import React, { Suspense } from 'react';
|
|
243
|
+
import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
|
|
565
244
|
|
|
566
|
-
|
|
567
|
-
${
|
|
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
|
-
|
|
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
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
|
320
|
+
name: "vite-plugin-bini",
|
|
674
321
|
enforce: "pre",
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
server.watcher.
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
if (
|
|
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("
|
|
736
|
-
if (
|
|
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("
|
|
740
|
-
if (
|
|
741
|
-
scheduleRegen(server);
|
|
386
|
+
server.watcher.on("change", (file) => {
|
|
387
|
+
if (file.startsWith(fullApiDir)) reloadApi();
|
|
742
388
|
});
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
|
399
|
+
const request = new Request(url, {
|
|
771
400
|
method: req.method,
|
|
772
401
|
headers: req.headers,
|
|
773
402
|
body
|
|
774
403
|
});
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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 =
|
|
502
|
+
var index_default = bini;
|
|
842
503
|
// Annotate the CommonJS export names for ESM import in node:
|
|
843
504
|
0 && (module.exports = {
|
|
844
|
-
|
|
505
|
+
bini
|
|
845
506
|
});
|
|
846
507
|
//# sourceMappingURL=index.cjs.map
|