weifuwu 0.18.0 → 0.18.2

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.
@@ -0,0 +1,936 @@
1
+ // ssr/ssr.ts
2
+ import { createElement } from "react";
3
+ import { createHash as createHash2 } from "node:crypto";
4
+ import { dirname as dirname2, resolve as resolve2 } from "node:path";
5
+ import { AsyncLocalStorage } from "node:async_hooks";
6
+
7
+ // ssr/compile.ts
8
+ import * as esbuild from "esbuild";
9
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
10
+ import { join, resolve, dirname } from "node:path";
11
+ import { pathToFileURL } from "node:url";
12
+ import { createHash } from "node:crypto";
13
+ var OUT_DIR = ".weifuwu/ssr";
14
+ var cache = /* @__PURE__ */ new Map();
15
+ var externals = [
16
+ "react",
17
+ "react-dom",
18
+ "esbuild",
19
+ "graphql",
20
+ "ws",
21
+ "zod",
22
+ "@graphql-tools/schema",
23
+ "ai"
24
+ ];
25
+ var _alias = null;
26
+ function resolveAliases() {
27
+ if (_alias) return _alias;
28
+ const configFiles = ["tsconfig.json", "jsconfig.json"];
29
+ for (const file of configFiles) {
30
+ const p = resolve(file);
31
+ if (existsSync(p)) {
32
+ try {
33
+ const config = JSON.parse(readFileSync(p, "utf-8"));
34
+ const paths = config.compilerOptions?.paths;
35
+ if (paths) {
36
+ const alias = {};
37
+ for (const [key, values] of Object.entries(paths)) {
38
+ const cleanKey = key.replace("/*", "");
39
+ const val = values[0]?.replace("/*", "");
40
+ if (val) alias[cleanKey] = resolve(dirname(p), val);
41
+ }
42
+ _alias = alias;
43
+ return alias;
44
+ }
45
+ } catch {
46
+ }
47
+ }
48
+ }
49
+ _alias = {};
50
+ return {};
51
+ }
52
+ function id(s) {
53
+ return createHash("md5").update(s).digest("hex").slice(0, 8);
54
+ }
55
+ function clearCompileCache() {
56
+ cache.clear();
57
+ _alias = null;
58
+ }
59
+ async function compileTsx(path) {
60
+ if (cache.has(path)) return cache.get(path);
61
+ const absPath = resolve(path);
62
+ mkdirSync(OUT_DIR, { recursive: true });
63
+ const hash = id(absPath);
64
+ const outPath = join(OUT_DIR, hash + ".js");
65
+ await esbuild.build({
66
+ entryPoints: { [hash]: absPath },
67
+ outdir: OUT_DIR,
68
+ format: "esm",
69
+ platform: "node",
70
+ jsx: "automatic",
71
+ jsxImportSource: "react",
72
+ bundle: true,
73
+ external: externals,
74
+ alias: resolveAliases(),
75
+ write: true,
76
+ allowOverwrite: true
77
+ });
78
+ const mod = await import(pathToFileURL(outPath).href);
79
+ cache.set(path, mod);
80
+ return mod;
81
+ }
82
+
83
+ // ssr/stream.ts
84
+ import { TextDecoder, TextEncoder } from "node:util";
85
+ var _publicEnv = null;
86
+ function getPublicEnv() {
87
+ if (_publicEnv) return _publicEnv;
88
+ _publicEnv = {};
89
+ for (const key of Object.keys(process.env)) {
90
+ if (key.startsWith("WEIFUWU_PUBLIC_")) {
91
+ _publicEnv[key] = process.env[key];
92
+ }
93
+ }
94
+ return _publicEnv;
95
+ }
96
+ function buildHeadPayload(opts) {
97
+ const { ctx, base, compiledTailwindCss } = opts;
98
+ let result = "";
99
+ if (ctx.prefs?.theme) {
100
+ result += `<script>!function(){var t=(document.cookie.match(/(?:^|;\\s*)theme=([^;]+)/)||[])[1]||'system';if(t==='system'){t=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'}document.documentElement.setAttribute('data-theme',t)}()</script>
101
+ `;
102
+ }
103
+ if (compiledTailwindCss) {
104
+ result += `<link rel="stylesheet" href="${base}/__wfw/style.css" />
105
+ `;
106
+ }
107
+ const localeData = ctx.parsed?.__localeData ?? globalThis.__LOCALE_DATA__;
108
+ if (localeData && Object.keys(localeData).length > 0) {
109
+ result += `<script>window.__LOCALE_DATA__=${JSON.stringify(localeData)}</script>
110
+ `;
111
+ }
112
+ const loaderData = opts.loaderData || {};
113
+ const ctxData = {
114
+ params: ctx.params,
115
+ query: ctx.query,
116
+ user: ctx.user,
117
+ parsed: ctx.parsed,
118
+ prefs: ctx.prefs,
119
+ loaderData
120
+ };
121
+ const publicEnv = getPublicEnv();
122
+ if (Object.keys(publicEnv).length > 0) {
123
+ ctxData.env = publicEnv;
124
+ }
125
+ result += `<script>window.__WEIFUWU_CTX=${JSON.stringify(ctxData)}</script>
126
+ `;
127
+ return result;
128
+ }
129
+ function buildBodyScripts(opts) {
130
+ const parts = [];
131
+ if (opts.loaderData && Object.keys(opts.loaderData).length > 0) {
132
+ parts.push(`<script>window.__WEIFUWU_PROPS=${JSON.stringify(opts.loaderData)}</script>`);
133
+ }
134
+ if (opts.bundle) {
135
+ parts.push(`<script type="module" src="${opts.base}${opts.bundle.url}"></script>`);
136
+ }
137
+ return parts.join("\n");
138
+ }
139
+ function streamResponse(reactStream, opts) {
140
+ const decoder = new TextDecoder();
141
+ const encoder = new TextEncoder();
142
+ const headPayload = buildHeadPayload(opts);
143
+ let buffer = "";
144
+ let headFlushed = false;
145
+ let extractedHead = "";
146
+ const output = new ReadableStream({
147
+ async start(controller) {
148
+ try {
149
+ const reader = reactStream.getReader();
150
+ async function push(chunk) {
151
+ buffer += decoder.decode(chunk, { stream: true });
152
+ if (!extractedHead) {
153
+ const m = buffer.match(/<template id="__wfw_head">([\s\S]*?)<\/template>/);
154
+ if (m) {
155
+ extractedHead = m[1];
156
+ buffer = buffer.replace(m[0], "");
157
+ }
158
+ }
159
+ if (!headFlushed) {
160
+ const idx = buffer.indexOf("</head>");
161
+ if (idx !== -1) {
162
+ const before = buffer.slice(0, idx);
163
+ let injection = "";
164
+ if (extractedHead) injection += "\n" + extractedHead;
165
+ injection += headPayload;
166
+ controller.enqueue(encoder.encode(before + injection));
167
+ buffer = buffer.slice(idx);
168
+ headFlushed = true;
169
+ }
170
+ return;
171
+ }
172
+ controller.enqueue(encoder.encode(buffer));
173
+ buffer = "";
174
+ }
175
+ while (true) {
176
+ const { done, value } = await reader.read();
177
+ if (done) break;
178
+ await push(value);
179
+ }
180
+ buffer = buffer.replace(/<template id="__wfw_head">[\s\S]*?<\/template>/g, "");
181
+ if (buffer) controller.enqueue(encoder.encode(buffer));
182
+ const body = buildBodyScripts(opts);
183
+ if (body) controller.enqueue(encoder.encode("\n" + body));
184
+ if (opts.isDev) {
185
+ controller.enqueue(encoder.encode(
186
+ `
187
+ <script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//'+location.host+'${opts.base}/__weifuwu/livereload');ws.onmessage=function(e){if(e.data==='reload')location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},500)}})()</script>`
188
+ ));
189
+ }
190
+ } catch {
191
+ const fallback = `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>500</title></head><body><h1>500 - Internal Server Error</h1></body></html>`;
192
+ controller.enqueue(encoder.encode(fallback));
193
+ } finally {
194
+ controller.close();
195
+ }
196
+ }
197
+ });
198
+ return new Response(output, {
199
+ status: opts.status ?? 200,
200
+ headers: { "content-type": "text/html; charset=utf-8" }
201
+ });
202
+ }
203
+
204
+ // tsx-context.ts
205
+ import { createContext } from "react";
206
+ var DEFAULT_CTX = { params: {}, query: {}, parsed: {}, prefs: {}, loaderData: {}, env: {}, user: {} };
207
+ var KEY = "__WEIFUWU_CTX_STORE";
208
+ function getStore() {
209
+ if (typeof globalThis !== "undefined" && globalThis[KEY]) {
210
+ return globalThis[KEY];
211
+ }
212
+ const s = {
213
+ _ctx: DEFAULT_CTX,
214
+ _snapshot: { params: DEFAULT_CTX.params, query: DEFAULT_CTX.query, user: DEFAULT_CTX.user, parsed: DEFAULT_CTX.parsed, prefs: DEFAULT_CTX.prefs, env: DEFAULT_CTX.env },
215
+ _listeners: /* @__PURE__ */ new Set(),
216
+ _alsGetStore: null
217
+ };
218
+ if (typeof globalThis !== "undefined") {
219
+ globalThis[KEY] = s;
220
+ }
221
+ return s;
222
+ }
223
+ var store = getStore();
224
+ function __registerAls(getStore2) {
225
+ store._alsGetStore = getStore2;
226
+ }
227
+ function setCtx(value) {
228
+ store._ctx = { ...store._ctx, ...value };
229
+ store._snapshot = { params: store._ctx.params, query: store._ctx.query, user: store._ctx.user, parsed: store._ctx.parsed, prefs: store._ctx.prefs, env: store._ctx.env };
230
+ store._listeners.forEach((fn) => fn());
231
+ }
232
+ var TsxContext = createContext(DEFAULT_CTX);
233
+
234
+ // ssr/ssr.ts
235
+ var als = new AsyncLocalStorage();
236
+ __registerAls(() => als.getStore());
237
+ var isDev = process.env.NODE_ENV !== "production";
238
+ function id2(s) {
239
+ return createHash2("md5").update(s).digest("hex").slice(0, 8);
240
+ }
241
+ async function buildClientBundle(entryPath, layoutPaths) {
242
+ try {
243
+ const absEntry = resolve2(entryPath);
244
+ const absLayouts = layoutPaths.map((p) => resolve2(p));
245
+ const layoutImports = absLayouts.map((p) => `import${JSON.stringify(p)};`).join("");
246
+ const _sc = `(function(){var k='__WEIFUWU_CTX_STORE';var s=typeof globalThis!='undefined'&&globalThis[k];if(!s)return function(){};return function(v){s._ctx={...s._ctx,...v};s._snapshot={params:s._ctx.params,query:s._ctx.query,user:s._ctx.user,parsed:s._ctx.parsed,prefs:s._ctx.prefs,env:s._ctx.env};s._listeners.forEach(function(fn){fn()})}})()`;
247
+ const code = [
248
+ layoutImports,
249
+ `import{hydrateRoot}from'react-dom/client';`,
250
+ `import{createElement,useState,useEffect}from'react';`,
251
+ `import{TsxContext}from'weifuwu/react';`,
252
+ `import P from${JSON.stringify(absEntry)};`,
253
+ `var setCtx=${_sc};`,
254
+ `const c=document.getElementById('__weifuwu_root');`,
255
+ `if(window.__WEIFUWU_PROPS)setCtx({loaderData:window.__WEIFUWU_PROPS});`,
256
+ `if(!window.__WFW_ROOT){`,
257
+ `function App(){`,
258
+ `const[p,setP]=useState({C:P});`,
259
+ `useEffect(()=>{window.__WFW_SET_PAGE=(C)=>{setCtx({loaderData:window.__WEIFUWU_PROPS});setP({C})}},[]);`,
260
+ `const ctx=window.__WEIFUWU_CTX||{};`,
261
+ `return createElement(TsxContext.Provider,{value:ctx},`,
262
+ `createElement(p.C,null))`,
263
+ `}`,
264
+ `window.__WFW_ROOT=hydrateRoot(c,createElement(App));`,
265
+ `}else{`,
266
+ `window.__WFW_SET_PAGE?.(P);`,
267
+ `}`
268
+ ].join("");
269
+ const { default: esbuild2 } = await import("esbuild");
270
+ const result = await esbuild2.build({
271
+ stdin: { contents: code, loader: "tsx", resolveDir: dirname2(absEntry) },
272
+ bundle: true,
273
+ format: "esm",
274
+ jsx: "automatic",
275
+ jsxImportSource: "react",
276
+ banner: { js: "self.process={env:{}};" },
277
+ loader: { ".node": "empty" },
278
+ write: false,
279
+ minify: true
280
+ });
281
+ return result.outputFiles[0].contents;
282
+ } catch (err) {
283
+ console.error("hydration bundle failed:", err);
284
+ return null;
285
+ }
286
+ }
287
+ var bundleRegistry = /* @__PURE__ */ new Map();
288
+ function serializeLoaderData(ctx) {
289
+ const data = {};
290
+ for (const key of Object.keys(ctx)) {
291
+ if (!["params", "query", "mountPath", "layoutStack"].includes(key)) {
292
+ data[key] = ctx[key];
293
+ }
294
+ }
295
+ return data;
296
+ }
297
+ function ssr(path) {
298
+ const entryId = id2(resolve2(path));
299
+ const bundleKey = `/__ssr/${entryId}.js`;
300
+ let bundleBuilt = false;
301
+ return async (req, ctx) => {
302
+ const pageMod = await compileTsx(path);
303
+ const Component = pageMod.default;
304
+ if (!Component) return new Response("", { status: 500 });
305
+ const layouts = ctx.layoutStack || [];
306
+ const layoutComponents = layouts.map((l) => l.component);
307
+ const layoutPaths = layouts.map((l) => l.path);
308
+ const base = (ctx.mountPath || "").replace(/\/$/, "");
309
+ const loaderData = serializeLoaderData(ctx);
310
+ const ctxValue = {
311
+ params: ctx.params,
312
+ query: ctx.query,
313
+ user: ctx.user ?? {},
314
+ parsed: ctx.parsed ?? {},
315
+ prefs: ctx.prefs ?? {},
316
+ loaderData,
317
+ env: ctx.env ?? {}
318
+ };
319
+ return als.run(ctxValue, async () => {
320
+ setCtx(ctxValue);
321
+ let element = createElement(
322
+ TsxContext.Provider,
323
+ { value: ctxValue },
324
+ createElement(
325
+ "div",
326
+ { id: "__weifuwu_root" },
327
+ createElement(Component, null)
328
+ )
329
+ );
330
+ if (layoutComponents.length === 0) {
331
+ element = createElement(
332
+ "html",
333
+ { lang: "en" },
334
+ createElement(
335
+ "head",
336
+ null,
337
+ createElement("meta", { charSet: "utf-8" }),
338
+ createElement("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
339
+ createElement("title", null, "weifuwu")
340
+ ),
341
+ createElement("body", null, element)
342
+ );
343
+ } else {
344
+ for (const L of layoutComponents.toReversed()) {
345
+ element = createElement(L, { children: element });
346
+ }
347
+ }
348
+ let bundle = null;
349
+ if (!bundleBuilt) {
350
+ const buf = await buildClientBundle(path, layoutPaths);
351
+ if (buf) {
352
+ bundleRegistry.set(bundleKey, buf);
353
+ bundleBuilt = true;
354
+ }
355
+ }
356
+ if (bundleRegistry.has(bundleKey)) {
357
+ bundle = { url: bundleKey };
358
+ }
359
+ const { renderToReadableStream } = await import("react-dom/server");
360
+ const stream = await renderToReadableStream(element);
361
+ return streamResponse(stream, {
362
+ ctx,
363
+ base,
364
+ isDev,
365
+ bundle,
366
+ loaderData
367
+ });
368
+ });
369
+ };
370
+ }
371
+ function ssrBundleHandler() {
372
+ return (req, ctx) => {
373
+ const url = new URL(req.url);
374
+ const buf = bundleRegistry.get(url.pathname);
375
+ return buf ? new Response(buf, {
376
+ headers: { "content-type": "application/javascript; charset=utf-8" }
377
+ }) : new Response("", { status: 404 });
378
+ };
379
+ }
380
+
381
+ // ssr/layout.ts
382
+ function layout(path) {
383
+ return async (req, ctx, next) => {
384
+ const mod = await compileTsx(path);
385
+ const Component = mod.default;
386
+ if (!Component) throw new Error(`Layout ${path} has no default export`);
387
+ ctx.layoutStack = [...ctx.layoutStack || [], { path, component: Component }];
388
+ return next(req, ctx);
389
+ };
390
+ }
391
+
392
+ // ssr/tailwind.ts
393
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
394
+ import { relative, resolve as resolve3 } from "node:path";
395
+ var isDev2 = process.env.NODE_ENV !== "production";
396
+ function tailwind(cssPath, scanDir) {
397
+ let compiledCss = "";
398
+ let twWatcher = null;
399
+ return async (req, ctx, next) => {
400
+ const url = new URL(req.url);
401
+ if (url.pathname === "/__wfw/style.css") {
402
+ if (!compiledCss) compiledCss = await compile(cssPath, scanDir);
403
+ return new Response(compiledCss || "", {
404
+ headers: { "content-type": "text/css; charset=utf-8" }
405
+ });
406
+ }
407
+ ctx.compiledTailwindCss = compiledCss;
408
+ if (isDev2 && !twWatcher) {
409
+ twWatcher = watchFile(cssPath, () => {
410
+ compiledCss = "";
411
+ });
412
+ }
413
+ return next(req, ctx);
414
+ };
415
+ }
416
+ async function compile(cssPath, scanDir) {
417
+ try {
418
+ const inputFile = resolve3(cssPath);
419
+ if (!existsSync2(inputFile)) {
420
+ mkdirSync2(dirname3(inputFile), { recursive: true });
421
+ writeFileSync(inputFile, '@import "tailwindcss"\n', "utf-8");
422
+ }
423
+ const { default: tailwindPlugin } = await import("@tailwindcss/postcss");
424
+ const { default: postcss } = await import("postcss");
425
+ let src = readFileSync2(inputFile, "utf-8");
426
+ const scanSource = scanDir ? relative(dirname3(inputFile), scanDir) || "." : ".";
427
+ const sourcePath = scanSource === "." ? "./" : `./${scanSource}/`;
428
+ src = `@source "${sourcePath}";
429
+ ${src}`;
430
+ const result = await postcss([tailwindPlugin()]).process(src, { from: inputFile });
431
+ return result.css;
432
+ } catch (err) {
433
+ console.warn("Tailwind CSS processing failed:", err.message);
434
+ return "";
435
+ }
436
+ }
437
+ function dirname3(p) {
438
+ return p.substring(0, p.lastIndexOf("/")) || "/";
439
+ }
440
+ function watchFile(path, onChange) {
441
+ let watcher = null;
442
+ import("chokidar").then((chokidar2) => {
443
+ watcher = chokidar2.default.watch(resolve3(path), { persistent: false });
444
+ watcher.on("change", onChange);
445
+ });
446
+ return watcher;
447
+ }
448
+
449
+ // ssr/not-found.ts
450
+ function notFound(path) {
451
+ return async (req, ctx) => {
452
+ if (!path) return new Response("Not Found", { status: 404 });
453
+ const mod = await compileTsx(path);
454
+ const Component = mod?.default;
455
+ const body = Component ? "404 - Not Found" : "404 - Not Found";
456
+ return new Response(body, {
457
+ status: 404,
458
+ headers: { "content-type": "text/html; charset=utf-8" }
459
+ });
460
+ };
461
+ }
462
+
463
+ // ssr/error-boundary.ts
464
+ import { createElement as createElement2 } from "react";
465
+ import { TextEncoder as TextEncoder2 } from "node:util";
466
+ function errorBoundary(errorPath) {
467
+ return async (req, ctx, next) => {
468
+ try {
469
+ return await next(req, ctx);
470
+ } catch (err) {
471
+ const mod = await compileTsx(errorPath);
472
+ const ErrorComponent = mod.default;
473
+ if (!ErrorComponent) throw err;
474
+ const layouts = (ctx.layoutStack || []).map((l) => l.component);
475
+ const stream = await import("react-dom/server").then((m) => m.renderToReadableStream(
476
+ createElement2(ErrorComponent, {
477
+ error: err instanceof Error ? err : new Error(String(err)),
478
+ reset: () => {
479
+ }
480
+ })
481
+ ));
482
+ const reader = stream.getReader();
483
+ const chunks = [];
484
+ while (true) {
485
+ const { done, value } = await reader.read();
486
+ if (done) break;
487
+ chunks.push(value);
488
+ }
489
+ const encoder = new TextEncoder2();
490
+ const body = chunks.reduce((acc, c) => {
491
+ const merged = new Uint8Array(acc.length + c.length);
492
+ merged.set(acc);
493
+ merged.set(c, acc.length);
494
+ return merged;
495
+ }, new Uint8Array(0));
496
+ return new Response(body, {
497
+ status: 500,
498
+ headers: { "content-type": "text/html; charset=utf-8" }
499
+ });
500
+ }
501
+ };
502
+ }
503
+
504
+ // ssr/live.ts
505
+ import chokidar from "chokidar";
506
+
507
+ // router.ts
508
+ import { WebSocketServer } from "ws";
509
+ var createTrieNode = () => ({
510
+ children: /* @__PURE__ */ new Map(),
511
+ handlers: /* @__PURE__ */ new Map(),
512
+ middlewares: /* @__PURE__ */ new Map(),
513
+ pathMws: []
514
+ });
515
+ var createWsNode = () => ({
516
+ children: /* @__PURE__ */ new Map(),
517
+ middlewares: []
518
+ });
519
+ var getTrieNode = (node, segment) => {
520
+ if (segment.startsWith(":")) {
521
+ if (!node.children.has(":")) {
522
+ const child2 = createTrieNode();
523
+ child2.param = segment.slice(1);
524
+ node.children.set(":", child2);
525
+ }
526
+ const child = node.children.get(":");
527
+ if (child.param !== segment.slice(1)) {
528
+ throw new Error(
529
+ `Param name conflict: ":${child.param}" already registered at this path position, cannot register ":"${segment.slice(1)}"`
530
+ );
531
+ }
532
+ return child;
533
+ }
534
+ if (!node.children.has(segment)) {
535
+ node.children.set(segment, createTrieNode());
536
+ }
537
+ return node.children.get(segment);
538
+ };
539
+ var matchTrieNode = (node, segment, params) => {
540
+ if (node.children.has(segment)) return node.children.get(segment);
541
+ if (node.children.has(":")) {
542
+ const child = node.children.get(":");
543
+ if (child.param) params[child.param] = segment;
544
+ return child;
545
+ }
546
+ return null;
547
+ };
548
+ var getWsNode = (node, segment) => {
549
+ if (segment === "*") {
550
+ node.wildcard = true;
551
+ return node;
552
+ }
553
+ if (segment.startsWith(":")) {
554
+ if (!node.children.has(":")) {
555
+ const child2 = createWsNode();
556
+ child2.param = segment.slice(1);
557
+ node.children.set(":", child2);
558
+ }
559
+ const child = node.children.get(":");
560
+ if (child.param !== segment.slice(1)) {
561
+ throw new Error(
562
+ `Param name conflict: ":${child.param}" already registered at this path position`
563
+ );
564
+ }
565
+ return child;
566
+ }
567
+ if (!node.children.has(segment)) {
568
+ node.children.set(segment, createWsNode());
569
+ }
570
+ return node.children.get(segment);
571
+ };
572
+ var matchWsNode = (node, segment, params) => {
573
+ if (node.children.has(segment)) return node.children.get(segment);
574
+ if (node.children.has(":")) {
575
+ const child = node.children.get(":");
576
+ if (child.param) params[child.param] = segment;
577
+ return child;
578
+ }
579
+ if (node.wildcard) return node;
580
+ return null;
581
+ };
582
+ var Router = class _Router {
583
+ root = createTrieNode();
584
+ wsRoot = createWsNode();
585
+ globalMws = [];
586
+ errorHandler;
587
+ wss = new WebSocketServer({ noServer: true });
588
+ use(arg1, arg2) {
589
+ if (typeof arg1 === "string") {
590
+ if (arg2 instanceof _Router) {
591
+ this._mountRouter(arg1, arg2);
592
+ } else if (typeof arg2 === "function") {
593
+ let node = this.root;
594
+ for (const segment of this.splitPath(arg1)) {
595
+ node = getTrieNode(node, segment);
596
+ }
597
+ node.pathMws.push(arg2);
598
+ }
599
+ } else if (arg1 instanceof _Router) {
600
+ this._mountRouter("/", arg1);
601
+ } else if (typeof arg1 === "function") {
602
+ this.globalMws.push(arg1);
603
+ }
604
+ return this;
605
+ }
606
+ get(path, ...args) {
607
+ return this.route("GET", path, ...args);
608
+ }
609
+ post(path, ...args) {
610
+ return this.route("POST", path, ...args);
611
+ }
612
+ put(path, ...args) {
613
+ return this.route("PUT", path, ...args);
614
+ }
615
+ delete(path, ...args) {
616
+ return this.route("DELETE", path, ...args);
617
+ }
618
+ patch(path, ...args) {
619
+ return this.route("PATCH", path, ...args);
620
+ }
621
+ head(path, ...args) {
622
+ return this.route("HEAD", path, ...args);
623
+ }
624
+ options(path, ...args) {
625
+ return this.route("OPTIONS", path, ...args);
626
+ }
627
+ all(path, ...args) {
628
+ return this.route("*", path, ...args);
629
+ }
630
+ onError(handler) {
631
+ this.errorHandler = handler;
632
+ return this;
633
+ }
634
+ route(method, path, ...args) {
635
+ const handler = args.pop();
636
+ const middlewares = args;
637
+ const segments = this.splitPath(path);
638
+ let node = this.root;
639
+ for (const segment of segments) {
640
+ if (segment === "*") {
641
+ const remaining = segments.indexOf("*") < segments.length - 1;
642
+ if (remaining) {
643
+ console.warn(`Route "${path}": segments after "*" are ignored`);
644
+ }
645
+ node.wildcard = true;
646
+ node.handlers.set(method, handler);
647
+ if (middlewares.length > 0) node.middlewares.set(method, middlewares);
648
+ return this;
649
+ }
650
+ node = getTrieNode(node, segment);
651
+ }
652
+ node.handlers.set(method, handler);
653
+ if (middlewares.length > 0) node.middlewares.set(method, middlewares);
654
+ return this;
655
+ }
656
+ ws(path, ...args) {
657
+ const handler = args.pop();
658
+ const middlewares = args;
659
+ const segments = this.splitPath(path);
660
+ let node = this.wsRoot;
661
+ for (const segment of segments) {
662
+ node = getWsNode(node, segment);
663
+ }
664
+ node.handler = handler;
665
+ if (middlewares.length > 0) node.middlewares = middlewares;
666
+ return this;
667
+ }
668
+ handler() {
669
+ return (req, ctx) => {
670
+ const url = new URL(req.url);
671
+ return this.handle(req, ctx, this.splitPath(url.pathname), Object.fromEntries(url.searchParams));
672
+ };
673
+ }
674
+ websocketHandler() {
675
+ const wsRoot = this.wsRoot;
676
+ const router = this;
677
+ return (req, socket, head) => {
678
+ const url = new URL(req.url ?? "/", "http://localhost");
679
+ const segments = url.pathname.split("/").filter(Boolean);
680
+ const match = router.matchWsTrie(wsRoot, segments);
681
+ if (match) {
682
+ const query = Object.fromEntries(url.searchParams);
683
+ const webReq = new Request(url.href, {
684
+ method: req.method ?? "GET",
685
+ headers: Object.fromEntries(
686
+ Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : v ?? ""])
687
+ )
688
+ });
689
+ const ctx = { params: match.params, query };
690
+ if (match.middlewares.length === 0) {
691
+ upgradeSocket(router.wss, req, socket, head, match.handler, ctx);
692
+ return;
693
+ }
694
+ let index = 0;
695
+ const dispatch = async (innerReq, ctx2) => {
696
+ if (index < match.middlewares.length) {
697
+ const mw = match.middlewares[index++];
698
+ return mw(innerReq, ctx2, dispatch);
699
+ }
700
+ return await new Promise((resolve4) => {
701
+ try {
702
+ upgradeSocket(router.wss, req, socket, head, match.handler, ctx2);
703
+ resolve4(new Response(null, { status: 101 }));
704
+ } catch {
705
+ socket.destroy();
706
+ resolve4(new Response("WebSocket upgrade failed", { status: 500 }));
707
+ }
708
+ });
709
+ };
710
+ Promise.resolve(dispatch(webReq, ctx)).then((result) => {
711
+ if (result.status !== 101) {
712
+ sendHttpResponseOnSocket(socket, result);
713
+ }
714
+ }).catch(() => {
715
+ socket.destroy();
716
+ });
717
+ return;
718
+ }
719
+ socket.destroy();
720
+ };
721
+ }
722
+ _mountRouter(prefix, sub) {
723
+ const base = prefix === "/" ? "" : prefix.replace(/\/$/, "");
724
+ const mountMw = (req, ctx, next) => {
725
+ ctx.mountPath = (ctx.mountPath || "") + base;
726
+ return next(req, ctx);
727
+ };
728
+ const routes = [];
729
+ this._collect(sub.root, "", routes, []);
730
+ for (const { method, path, handler, middlewares } of routes) {
731
+ this.route(method, base + path, mountMw, ...sub.globalMws, ...middlewares, handler);
732
+ }
733
+ const wsRoutes = [];
734
+ this._collectWs(sub.wsRoot, "", wsRoutes);
735
+ for (const { path, handler } of wsRoutes) {
736
+ this.ws(base + path, handler);
737
+ }
738
+ }
739
+ _collect(node, prefix, result, pathMwsAcc) {
740
+ const mws = [...pathMwsAcc, ...node.pathMws];
741
+ if (node.wildcard) {
742
+ for (const [method, handler] of node.handlers) {
743
+ result.push({ method, path: prefix + "/*", handler, middlewares: [...mws, ...node.middlewares.get(method) || []] });
744
+ }
745
+ } else {
746
+ for (const [method, handler] of node.handlers) {
747
+ result.push({ method, path: prefix || "/", handler, middlewares: [...mws, ...node.middlewares.get(method) || []] });
748
+ }
749
+ }
750
+ for (const [seg, child] of node.children) {
751
+ if (seg === ":") this._collect(child, prefix + "/:" + child.param, result, mws);
752
+ else this._collect(child, prefix + "/" + seg, result, mws);
753
+ }
754
+ }
755
+ _collectWs(node, prefix, result) {
756
+ if (node.handler) result.push({ path: prefix || "/", handler: node.handler });
757
+ for (const [seg, child] of node.children) {
758
+ if (seg === ":") this._collectWs(child, prefix + "/:" + child.param, result);
759
+ else this._collectWs(child, prefix + "/" + seg, result);
760
+ }
761
+ }
762
+ splitPath(path) {
763
+ return path.split("/").filter(Boolean);
764
+ }
765
+ matchTrie(method, segments) {
766
+ let node = this.root;
767
+ const params = {};
768
+ const pathMws = [];
769
+ let wildcardHandler = null;
770
+ let wildcardMws = [];
771
+ let wildcardIdx = -1;
772
+ for (let i = 0; i < segments.length; i++) {
773
+ pathMws.push(...node.pathMws);
774
+ if (node.wildcard) {
775
+ const h = node.handlers.get("*") || node.handlers.get(method);
776
+ if (h) {
777
+ wildcardHandler = h;
778
+ wildcardMws = node.middlewares.get(method) || node.middlewares.get("*") || [];
779
+ wildcardIdx = i;
780
+ }
781
+ }
782
+ const segment = segments[i];
783
+ if (!segment) break;
784
+ const next = matchTrieNode(node, segment, params);
785
+ if (!next) {
786
+ if (wildcardHandler) {
787
+ params["*"] = segments.slice(wildcardIdx).join("/");
788
+ return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params };
789
+ }
790
+ return null;
791
+ }
792
+ node = next;
793
+ }
794
+ pathMws.push(...node.pathMws);
795
+ const handler = node.handlers.get(method) || node.handlers.get("*");
796
+ if (handler) {
797
+ if (node.wildcard) params["*"] = segments.slice(segments.length).join("/");
798
+ return {
799
+ handler,
800
+ middlewares: node.middlewares.get(method) || node.middlewares.get("*") || [],
801
+ pathMws,
802
+ params
803
+ };
804
+ }
805
+ if (wildcardHandler) {
806
+ params["*"] = segments.slice(wildcardIdx).join("/");
807
+ return { handler: wildcardHandler, middlewares: wildcardMws, pathMws, params };
808
+ }
809
+ return null;
810
+ }
811
+ matchWsTrie(root, segments) {
812
+ let node = root;
813
+ const params = {};
814
+ for (const segment of segments) {
815
+ const next = matchWsNode(node, segment, params);
816
+ if (!next) return null;
817
+ node = next;
818
+ }
819
+ return node.handler ? { handler: node.handler, middlewares: node.middlewares, params } : null;
820
+ }
821
+ async handle(req, ctx, segments, query) {
822
+ const match = this.matchTrie(req.method, segments);
823
+ if (match?.handler) {
824
+ const { handler, middlewares: routeMws, pathMws, params } = match;
825
+ const allMws = this.globalMws.length + pathMws.length + routeMws.length === 0 ? [] : [...this.globalMws, ...pathMws, ...routeMws];
826
+ const ctxWithMatch = { ...ctx, params: { ...ctx.params, ...params } };
827
+ try {
828
+ return await this.runChain(allMws, handler, req, ctxWithMatch);
829
+ } catch (e) {
830
+ const err = e instanceof Error ? e : new Error(String(e));
831
+ return this.errorHandler ? this.errorHandler(err, req, ctxWithMatch) : new Response("Internal Server Error", { status: 500 });
832
+ }
833
+ }
834
+ if (this.globalMws.length > 0) {
835
+ try {
836
+ const delegate = () => new Response("Not Found", { status: 404 });
837
+ return await this.runChain(this.globalMws, delegate, req, ctx);
838
+ } catch (e) {
839
+ const err = e instanceof Error ? e : new Error(String(e));
840
+ return this.errorHandler ? this.errorHandler(err, req, ctx) : new Response("Internal Server Error", { status: 500 });
841
+ }
842
+ }
843
+ return new Response("Not Found", { status: 404 });
844
+ }
845
+ async runChain(middlewares, finalHandler, req, ctx) {
846
+ let index = 0;
847
+ const dispatch = async (req2, ctx2) => {
848
+ if (index < middlewares.length) {
849
+ const mw = middlewares[index++];
850
+ return mw ? await mw(req2, ctx2, dispatch) : new Response("Middleware error", { status: 500 });
851
+ }
852
+ return await finalHandler(req2, ctx2);
853
+ };
854
+ return dispatch(req, ctx);
855
+ }
856
+ };
857
+ function upgradeSocket(wss, req, socket, head, handler, ctx) {
858
+ wss.handleUpgrade(req, socket, head, (ws) => {
859
+ if (handler.open) {
860
+ handler.open(ws, ctx);
861
+ }
862
+ ws.on("message", (data) => {
863
+ handler.message?.(ws, ctx, data);
864
+ });
865
+ ws.on("close", () => {
866
+ handler.close?.(ws, ctx);
867
+ });
868
+ ws.on("error", (err) => {
869
+ handler.error?.(ws, ctx, err);
870
+ });
871
+ });
872
+ }
873
+ function sendHttpResponseOnSocket(socket, response) {
874
+ const statusLine = `HTTP/1.1 ${response.status} ${response.statusText}`;
875
+ const headerLines = [statusLine];
876
+ response.headers.forEach((value, key) => {
877
+ headerLines.push(`${key}: ${value}`);
878
+ });
879
+ headerLines.push("Connection: close");
880
+ headerLines.push("");
881
+ const headerStr = headerLines.join("\r\n");
882
+ response.arrayBuffer().then((buf) => {
883
+ const body = Buffer.from(buf);
884
+ socket.write(headerStr + "\r\n" + body.toString());
885
+ socket.end();
886
+ }).catch(() => {
887
+ socket.write(headerStr + "\r\n");
888
+ socket.end();
889
+ });
890
+ }
891
+
892
+ // ssr/live.ts
893
+ var clients = /* @__PURE__ */ new Set();
894
+ function broadcastReload() {
895
+ for (const ws of clients) {
896
+ try {
897
+ ws.send("reload");
898
+ } catch {
899
+ clients.delete(ws);
900
+ }
901
+ }
902
+ }
903
+ function liveReload(opts) {
904
+ const r = new Router();
905
+ r.ws("/__weifuwu/livereload", {
906
+ open(ws) {
907
+ clients.add(ws);
908
+ ws.on("close", () => clients.delete(ws));
909
+ ws.on("error", () => clients.delete(ws));
910
+ }
911
+ });
912
+ const watcher = chokidar.watch(opts.dirs, {
913
+ ignored: /(^|[/\\])\.|node_modules|[/\\]\.weifuwu[/\\]/,
914
+ ignoreInitial: true
915
+ });
916
+ watcher.on("change", (path) => {
917
+ if (!/\.tsx?$/.test(path)) return;
918
+ clearCompileCache();
919
+ setTimeout(broadcastReload, 50);
920
+ });
921
+ r.close = () => {
922
+ watcher.close();
923
+ clients.clear();
924
+ };
925
+ return r;
926
+ }
927
+ export {
928
+ clearCompileCache,
929
+ errorBoundary,
930
+ layout,
931
+ liveReload,
932
+ notFound,
933
+ ssr,
934
+ ssrBundleHandler,
935
+ tailwind
936
+ };