@mandujs/core 0.22.1 → 0.24.0
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/package.json +1 -1
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -0
- package/src/bundler/dev.ts +288 -0
- package/src/bundler/reverse-import-graph.ts +339 -0
- package/src/bundler/safe-build.test.ts +54 -0
- package/src/bundler/safe-build.ts +33 -7
- package/src/client/prefetch-helper.ts +55 -0
- package/src/config/mandu.ts +106 -0
- package/src/config/validate.ts +87 -1
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -0
- package/src/desktop/__tests__/window.test.ts +79 -3
- package/src/desktop/webview-fallback.ts +583 -0
- package/src/desktop/window.ts +527 -492
- package/src/perf/hmr-markers.ts +12 -0
- package/src/runtime/adapter-bun.ts +64 -62
- package/src/runtime/server.ts +243 -12
- package/src/runtime/ssr.ts +146 -7
- package/src/runtime/streaming-ssr.ts +89 -3
- package/src/testing/db.ts +157 -0
- package/src/testing/index.ts +59 -1
- package/src/testing/mocks.ts +203 -0
- package/src/testing/server.ts +196 -0
- package/src/testing/session.ts +190 -0
- package/src/testing/snapshot.ts +444 -0
package/src/perf/hmr-markers.ts
CHANGED
|
@@ -175,6 +175,18 @@ export const HMR_PERF = {
|
|
|
175
175
|
* prewarm before the user hits a file save. */
|
|
176
176
|
JIT_PREWARM: "boot:jit-prewarm",
|
|
177
177
|
|
|
178
|
+
/** Phase 11 C — Deep-path JIT prewarm extension. Phase 7.3 A closed the
|
|
179
|
+
* first-iter gap from 41 ms to 25 ms by prewarming the React hot set;
|
|
180
|
+
* R0.3 diagnostics traced the remaining ~15 ms to the
|
|
181
|
+
* `registerManifestHandlers` deep-path (cli `util/handlers` +
|
|
182
|
+
* `util/bun` bundledImport + `@mandujs/core/bundler/safe-build`
|
|
183
|
+
* internals) which only execute on the FIRST SSR reload. This marker
|
|
184
|
+
* measures the settling time of the deep-import Promise — still
|
|
185
|
+
* fire-and-forget, still NOT on the critical path, so values are
|
|
186
|
+
* informational only. Target: first-iter ≤ 15 ms (hard) / ≤ 20 ms
|
|
187
|
+
* (soft). See `packages/cli/src/util/jit-prewarm.ts`. */
|
|
188
|
+
JIT_PREWARM_DEEP: "boot:jit-prewarm-deep",
|
|
189
|
+
|
|
178
190
|
/** API route handler reload (`handleAPIChange`) — `.route.ts` /
|
|
179
191
|
* `.route.tsx` change. Symmetric to `SSR_HANDLER_RELOAD` for page /
|
|
180
192
|
* layout reloads. Phase 7.2 §7.4 flagged that `handleAPIChange` was
|
|
@@ -1,62 +1,64 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Bun Adapter (기본 어댑터)
|
|
3
|
-
* Bun.serve() 기반 서버 생성
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ManduAdapter, AdapterOptions, AdapterServer } from "./adapter";
|
|
7
|
-
import { startServer, type ManduServer } from "./server";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Bun 어댑터 (기본)
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* // mandu.config.ts
|
|
15
|
-
* import { adapterBun } from "@mandujs/core";
|
|
16
|
-
*
|
|
17
|
-
* export default {
|
|
18
|
-
* adapter: adapterBun(),
|
|
19
|
-
* };
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
export function adapterBun(): ManduAdapter {
|
|
23
|
-
return {
|
|
24
|
-
name: "adapter-bun",
|
|
25
|
-
|
|
26
|
-
createServer(options: AdapterOptions): AdapterServer {
|
|
27
|
-
let manduServer: ManduServer | null = null;
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
async fetch(req: Request): Promise<Response> {
|
|
31
|
-
if (!manduServer) {
|
|
32
|
-
return new Response("Server not started", { status: 503 });
|
|
33
|
-
}
|
|
34
|
-
// 내부 서버로 프록시
|
|
35
|
-
const url = new URL(req.url);
|
|
36
|
-
const targetUrl = `http://localhost:${manduServer.server.port}${url.pathname}${url.search}`;
|
|
37
|
-
return globalThis.fetch(new Request(targetUrl, req));
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
async listen(port: number, hostname?: string) {
|
|
41
|
-
manduServer = startServer(options.manifest, {
|
|
42
|
-
...options.serverOptions,
|
|
43
|
-
port,
|
|
44
|
-
hostname,
|
|
45
|
-
rootDir: options.rootDir,
|
|
46
|
-
bundleManifest: options.bundleManifest,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
port: manduServer.server.port ?? port,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Bun Adapter (기본 어댑터)
|
|
3
|
+
* Bun.serve() 기반 서버 생성
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ManduAdapter, AdapterOptions, AdapterServer } from "./adapter";
|
|
7
|
+
import { startServer, type ManduServer } from "./server";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bun 어댑터 (기본)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // mandu.config.ts
|
|
15
|
+
* import { adapterBun } from "@mandujs/core";
|
|
16
|
+
*
|
|
17
|
+
* export default {
|
|
18
|
+
* adapter: adapterBun(),
|
|
19
|
+
* };
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function adapterBun(): ManduAdapter {
|
|
23
|
+
return {
|
|
24
|
+
name: "adapter-bun",
|
|
25
|
+
|
|
26
|
+
createServer(options: AdapterOptions): AdapterServer {
|
|
27
|
+
let manduServer: ManduServer | null = null;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
async fetch(req: Request): Promise<Response> {
|
|
31
|
+
if (!manduServer) {
|
|
32
|
+
return new Response("Server not started", { status: 503 });
|
|
33
|
+
}
|
|
34
|
+
// 내부 서버로 프록시
|
|
35
|
+
const url = new URL(req.url);
|
|
36
|
+
const targetUrl = `http://localhost:${manduServer.server.port}${url.pathname}${url.search}`;
|
|
37
|
+
return globalThis.fetch(new Request(targetUrl, req));
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async listen(port: number, hostname?: string) {
|
|
41
|
+
manduServer = startServer(options.manifest, {
|
|
42
|
+
...options.serverOptions,
|
|
43
|
+
port,
|
|
44
|
+
hostname,
|
|
45
|
+
rootDir: options.rootDir,
|
|
46
|
+
bundleManifest: options.bundleManifest,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
port: manduServer.server.port ?? port,
|
|
51
|
+
// Report the effective bind address. startServer() defaults to
|
|
52
|
+
// 0.0.0.0 when no hostname is supplied. See #190.
|
|
53
|
+
hostname: hostname ?? "0.0.0.0",
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async close() {
|
|
58
|
+
manduServer?.stop();
|
|
59
|
+
manduServer = null;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
package/src/runtime/server.ts
CHANGED
|
@@ -350,6 +350,32 @@ export interface ServerOptions {
|
|
|
350
350
|
* When set, token-protected endpoints such as `/_mandu/cache` become available.
|
|
351
351
|
*/
|
|
352
352
|
managementToken?: string;
|
|
353
|
+
/**
|
|
354
|
+
* Issue #192 — enable CSS View Transitions auto-inject (default `true`).
|
|
355
|
+
* When `true`, every SSR response gets
|
|
356
|
+
* `<style>@view-transition{navigation:auto}</style>` in its `<head>`,
|
|
357
|
+
* giving supported browsers a default crossfade on cross-document
|
|
358
|
+
* navigation. Pass `false` to suppress (typically wired from
|
|
359
|
+
* `ManduConfig.transitions`).
|
|
360
|
+
*/
|
|
361
|
+
transitions?: boolean;
|
|
362
|
+
/**
|
|
363
|
+
* Issue #192 — enable the hover prefetch helper (default `true`).
|
|
364
|
+
* When `true`, every SSR response gets a ~500-byte inline script that
|
|
365
|
+
* prefetches same-origin links on hover. Pass `false` to suppress
|
|
366
|
+
* (typically wired from `ManduConfig.prefetch`). Individual links can
|
|
367
|
+
* also opt out via `data-no-prefetch`.
|
|
368
|
+
*/
|
|
369
|
+
prefetch?: boolean;
|
|
370
|
+
/**
|
|
371
|
+
* Issue #191 — override dev-mode `_devtools.js` injection.
|
|
372
|
+
* Wired from `ManduConfig.dev.devtools`.
|
|
373
|
+
* - `true` → force inject on every page (SSR-only + Kitchen).
|
|
374
|
+
* - `false` → force skip on every page.
|
|
375
|
+
* - `undefined` → default. Inject iff the page's route has at least
|
|
376
|
+
* one island. Pure-SSR pages download zero devtools.
|
|
377
|
+
*/
|
|
378
|
+
devtools?: boolean;
|
|
353
379
|
}
|
|
354
380
|
|
|
355
381
|
export interface ManduServer {
|
|
@@ -449,6 +475,31 @@ export interface ServerRegistrySettings {
|
|
|
449
475
|
cacheStore?: CacheStore;
|
|
450
476
|
/** Internal management token for local runtime control */
|
|
451
477
|
managementToken?: string;
|
|
478
|
+
/**
|
|
479
|
+
* Edge runtime flag — disables filesystem-dependent features (static file
|
|
480
|
+
* serving, Kitchen dashboard, image optimization, SSG fallback loaders).
|
|
481
|
+
* Set by `@mandujs/edge` adapters (Cloudflare Workers, Deno Deploy, Vercel Edge).
|
|
482
|
+
* Default: false (Bun/Node runtime with full FS access).
|
|
483
|
+
*/
|
|
484
|
+
edge?: boolean;
|
|
485
|
+
/**
|
|
486
|
+
* Issue #192 — threaded from `ServerOptions.transitions`.
|
|
487
|
+
* `undefined` is treated as `true` at the SSR call-site (enabled by
|
|
488
|
+
* default); `false` suppresses the `<style>@view-transition>` injection.
|
|
489
|
+
*/
|
|
490
|
+
transitions?: boolean;
|
|
491
|
+
/**
|
|
492
|
+
* Issue #192 — threaded from `ServerOptions.prefetch`.
|
|
493
|
+
* `undefined` is treated as `true` at the SSR call-site (enabled by
|
|
494
|
+
* default); `false` suppresses the hover prefetch `<script>` injection.
|
|
495
|
+
*/
|
|
496
|
+
prefetch?: boolean;
|
|
497
|
+
/**
|
|
498
|
+
* Issue #191 — threaded from `ServerOptions.devtools`. `undefined`
|
|
499
|
+
* means "use default (islands → inject)"; `true` / `false` force the
|
|
500
|
+
* dev-mode `_devtools.js` `<script>` injection on / off. No-op in prod.
|
|
501
|
+
*/
|
|
502
|
+
devtools?: boolean;
|
|
452
503
|
}
|
|
453
504
|
|
|
454
505
|
export class ServerRegistry {
|
|
@@ -1882,6 +1933,9 @@ async function renderPageSSR(
|
|
|
1882
1933
|
criticalData: loaderData as Record<string, unknown> | undefined,
|
|
1883
1934
|
enableClientRouter: true,
|
|
1884
1935
|
cssPath: settings.cssPath,
|
|
1936
|
+
transitions: settings.transitions,
|
|
1937
|
+
prefetch: settings.prefetch,
|
|
1938
|
+
devtools: settings.devtools,
|
|
1885
1939
|
onShellReady: () => {
|
|
1886
1940
|
if (settings.isDev) {
|
|
1887
1941
|
console.log(`[Mandu Streaming] Shell ready: ${route.id}`);
|
|
@@ -1917,6 +1971,9 @@ async function renderPageSSR(
|
|
|
1917
1971
|
routePattern: route.pattern,
|
|
1918
1972
|
cssPath: settings.cssPath,
|
|
1919
1973
|
islandPreWrapped: !!needsIslandWrap,
|
|
1974
|
+
transitions: settings.transitions,
|
|
1975
|
+
prefetch: settings.prefetch,
|
|
1976
|
+
devtools: settings.devtools,
|
|
1920
1977
|
});
|
|
1921
1978
|
return ok(cookies ? cookies.applyToResponse(ssrResponse) : ssrResponse);
|
|
1922
1979
|
} catch (error) {
|
|
@@ -1953,6 +2010,9 @@ async function renderPageSSR(
|
|
|
1953
2010
|
title: "Mandu App — Error",
|
|
1954
2011
|
isDev: settings.isDev,
|
|
1955
2012
|
cssPath: settings.cssPath,
|
|
2013
|
+
transitions: settings.transitions,
|
|
2014
|
+
prefetch: settings.prefetch,
|
|
2015
|
+
devtools: settings.devtools,
|
|
1956
2016
|
});
|
|
1957
2017
|
return ok(cookies ? cookies.applyToResponse(errorHtml) : errorHtml);
|
|
1958
2018
|
}
|
|
@@ -2051,6 +2111,9 @@ async function renderNotFoundPage(
|
|
|
2051
2111
|
title: "Not Found",
|
|
2052
2112
|
isDev: settings.isDev,
|
|
2053
2113
|
cssPath: settings.cssPath,
|
|
2114
|
+
transitions: settings.transitions,
|
|
2115
|
+
prefetch: settings.prefetch,
|
|
2116
|
+
devtools: settings.devtools,
|
|
2054
2117
|
});
|
|
2055
2118
|
|
|
2056
2119
|
// renderSSR returns a 200; override to 404 without losing headers.
|
|
@@ -2408,18 +2471,23 @@ async function handleRequestInternal(
|
|
|
2408
2471
|
}
|
|
2409
2472
|
|
|
2410
2473
|
// 1. 정적 파일 서빙 시도 (최우선)
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2474
|
+
// Edge runtimes (Cloudflare Workers, etc.) have no filesystem — skip and
|
|
2475
|
+
// let the platform's asset pipeline (Wrangler [assets], Vercel _static, …)
|
|
2476
|
+
// handle static routing instead.
|
|
2477
|
+
if (!settings.edge) {
|
|
2478
|
+
const staticFileResult = await serveStaticFile(pathname, settings, req);
|
|
2479
|
+
if (staticFileResult.handled) {
|
|
2480
|
+
const staticResponse = staticFileResult.response!;
|
|
2481
|
+
if (settings.cors && isCorsRequest(req)) {
|
|
2482
|
+
const corsOptions: CorsOptions = typeof settings.cors === 'object' ? settings.cors : {};
|
|
2483
|
+
return ok(applyCorsToResponse(staticResponse, req, corsOptions));
|
|
2484
|
+
}
|
|
2485
|
+
return ok(staticResponse);
|
|
2417
2486
|
}
|
|
2418
|
-
return ok(staticResponse);
|
|
2419
2487
|
}
|
|
2420
2488
|
|
|
2421
2489
|
// 1.5. Image optimization handler (/_mandu/image)
|
|
2422
|
-
if (pathname === "/_mandu/image") {
|
|
2490
|
+
if (!settings.edge && pathname === "/_mandu/image") {
|
|
2423
2491
|
const imageResponse = await handleImageRequest(req, settings.rootDir, settings.publicDir);
|
|
2424
2492
|
if (imageResponse) return ok(imageResponse);
|
|
2425
2493
|
}
|
|
@@ -2473,6 +2541,9 @@ async function handleRequestInternal(
|
|
|
2473
2541
|
title: "Not Found",
|
|
2474
2542
|
isDev: settings.isDev,
|
|
2475
2543
|
cssPath: settings.cssPath,
|
|
2544
|
+
transitions: settings.transitions,
|
|
2545
|
+
prefetch: settings.prefetch,
|
|
2546
|
+
devtools: settings.devtools,
|
|
2476
2547
|
});
|
|
2477
2548
|
const headers = new Headers(html.headers);
|
|
2478
2549
|
const body = await html.text();
|
|
@@ -2574,10 +2645,43 @@ function startBunServerWithFallback(options: {
|
|
|
2574
2645
|
|
|
2575
2646
|
// ========== Server Startup ==========
|
|
2576
2647
|
|
|
2648
|
+
/**
|
|
2649
|
+
* Format a base URL for startup logging based on the bound hostname.
|
|
2650
|
+
*
|
|
2651
|
+
* When binding to wildcard addresses (`0.0.0.0`, `::`, or empty string),
|
|
2652
|
+
* the server listens on all interfaces — browsers must use `localhost`
|
|
2653
|
+
* or a specific loopback address to connect. We surface both IPv4 and IPv6
|
|
2654
|
+
* loopback URLs so the user can pick whichever their OS prefers.
|
|
2655
|
+
*
|
|
2656
|
+
* Returns `{ primary, additional }` where `primary` is the canonical URL
|
|
2657
|
+
* for UX (open-in-browser, runtime control) and `additional` are supplementary
|
|
2658
|
+
* URLs shown in the startup log.
|
|
2659
|
+
*/
|
|
2660
|
+
export function formatServerAddresses(
|
|
2661
|
+
hostname: string | undefined,
|
|
2662
|
+
port: number
|
|
2663
|
+
): { primary: string; additional: string[] } {
|
|
2664
|
+
const isWildcardV4 = hostname === "0.0.0.0" || hostname === undefined || hostname === "";
|
|
2665
|
+
const isWildcardV6 = hostname === "::" || hostname === "[::]";
|
|
2666
|
+
if (isWildcardV4 || isWildcardV6) {
|
|
2667
|
+
return {
|
|
2668
|
+
primary: `http://localhost:${port}`,
|
|
2669
|
+
additional: [`http://127.0.0.1:${port}`, `http://[::1]:${port}`],
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
// Bracket IPv6 literals for URL syntax.
|
|
2673
|
+
const host = hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
2674
|
+
return { primary: `http://${host}:${port}`, additional: [] };
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2577
2677
|
export function startServer(manifest: RoutesManifest, options: ServerOptions = {}): ManduServer {
|
|
2578
2678
|
const {
|
|
2579
2679
|
port = 3000,
|
|
2580
|
-
|
|
2680
|
+
// Default to 0.0.0.0 (dual-stack wildcard on IPv4) so `localhost` resolves
|
|
2681
|
+
// to 127.0.0.1 via OS-level IPv4-preferred lookups (e.g., Windows). Users
|
|
2682
|
+
// can still pin `hostname: "::1"` or `hostname: "127.0.0.1"` explicitly.
|
|
2683
|
+
// See issue #190.
|
|
2684
|
+
hostname = "0.0.0.0",
|
|
2581
2685
|
rootDir = process.cwd(),
|
|
2582
2686
|
isDev = false,
|
|
2583
2687
|
hmrPort,
|
|
@@ -2591,6 +2695,9 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
2591
2695
|
guardConfig = null,
|
|
2592
2696
|
cache: cacheOption,
|
|
2593
2697
|
managementToken,
|
|
2698
|
+
transitions,
|
|
2699
|
+
prefetch,
|
|
2700
|
+
devtools,
|
|
2594
2701
|
} = options;
|
|
2595
2702
|
|
|
2596
2703
|
// cssPath 처리:
|
|
@@ -2626,6 +2733,9 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
2626
2733
|
rateLimit: rateLimitOptions,
|
|
2627
2734
|
cssPath,
|
|
2628
2735
|
managementToken,
|
|
2736
|
+
transitions,
|
|
2737
|
+
prefetch,
|
|
2738
|
+
devtools,
|
|
2629
2739
|
};
|
|
2630
2740
|
|
|
2631
2741
|
registry.rateLimiter = rateLimitOptions ? new MemoryRateLimiter() : null;
|
|
@@ -2725,8 +2835,13 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
2725
2835
|
registry.settings = { ...registry.settings, hmrPort: actualPort };
|
|
2726
2836
|
}
|
|
2727
2837
|
|
|
2838
|
+
const addresses = formatServerAddresses(hostname, actualPort);
|
|
2839
|
+
|
|
2728
2840
|
if (isDev) {
|
|
2729
|
-
console.log(`🥟 Mandu Dev Server
|
|
2841
|
+
console.log(`🥟 Mandu Dev Server listening at ${addresses.primary}`);
|
|
2842
|
+
if (addresses.additional.length > 0) {
|
|
2843
|
+
console.log(` (also reachable at ${addresses.additional.join(", ")})`);
|
|
2844
|
+
}
|
|
2730
2845
|
if (registry.settings.hmrPort) {
|
|
2731
2846
|
console.log(`🔥 HMR enabled on port ${registry.settings.hmrPort + PORTS.HMR_OFFSET}`);
|
|
2732
2847
|
}
|
|
@@ -2738,10 +2853,13 @@ export function startServer(manifest: RoutesManifest, options: ServerOptions = {
|
|
|
2738
2853
|
console.log(`🌊 Streaming SSR enabled`);
|
|
2739
2854
|
}
|
|
2740
2855
|
if (registry.kitchen) {
|
|
2741
|
-
console.log(`🍳 Kitchen dashboard at
|
|
2856
|
+
console.log(`🍳 Kitchen dashboard at ${addresses.primary}/__kitchen`);
|
|
2742
2857
|
}
|
|
2743
2858
|
} else {
|
|
2744
|
-
console.log(`🥟 Mandu server
|
|
2859
|
+
console.log(`🥟 Mandu server listening at ${addresses.primary}`);
|
|
2860
|
+
if (addresses.additional.length > 0) {
|
|
2861
|
+
console.log(` (also reachable at ${addresses.additional.join(", ")})`);
|
|
2862
|
+
}
|
|
2745
2863
|
if (streaming) {
|
|
2746
2864
|
console.log(`🌊 Streaming SSR enabled`);
|
|
2747
2865
|
}
|
|
@@ -2769,6 +2887,119 @@ export const pageLoaders = defaultRegistry.pageLoaders;
|
|
|
2769
2887
|
export const pageHandlers = defaultRegistry.pageHandlers;
|
|
2770
2888
|
export const routeComponents = defaultRegistry.routeComponents;
|
|
2771
2889
|
|
|
2890
|
+
// ========== Runtime-Neutral Fetch Handler Factory ==========
|
|
2891
|
+
|
|
2892
|
+
/**
|
|
2893
|
+
* Options for {@link createAppFetchHandler}. Subset of {@link ServerOptions}
|
|
2894
|
+
* that makes sense in edge/serverless runtimes — no listen/port/hmr fields.
|
|
2895
|
+
*/
|
|
2896
|
+
export interface AppFetchHandlerOptions {
|
|
2897
|
+
/** Project root (used for module path validation). Required. */
|
|
2898
|
+
rootDir: string;
|
|
2899
|
+
/** Bundle manifest (Island hydration). Optional in pure-SSR apps. */
|
|
2900
|
+
bundleManifest?: BundleManifest;
|
|
2901
|
+
/** CORS config — `true` allows all origins, object for fine-grained rules. */
|
|
2902
|
+
cors?: boolean | CorsOptions;
|
|
2903
|
+
/** Streaming SSR toggle. Default: `false`. */
|
|
2904
|
+
streaming?: boolean;
|
|
2905
|
+
/** Rate limit policy. Memory-backed; edge runtimes should prefer durable stores. */
|
|
2906
|
+
rateLimit?: boolean | RateLimitOptions;
|
|
2907
|
+
/**
|
|
2908
|
+
* CSS link injection target for SSR. Typically `"/.mandu/client/globals.css"`
|
|
2909
|
+
* when Tailwind is in use. `false` disables injection.
|
|
2910
|
+
*/
|
|
2911
|
+
cssPath?: string | false;
|
|
2912
|
+
/** Custom registry override (defaults to the global registry). */
|
|
2913
|
+
registry?: ServerRegistry;
|
|
2914
|
+
/**
|
|
2915
|
+
* Mark this handler as edge-hosted. Skips filesystem-dependent features
|
|
2916
|
+
* (static file serving, Kitchen dashboard, image optimization). Set to
|
|
2917
|
+
* `true` by `@mandujs/edge` adapters.
|
|
2918
|
+
*/
|
|
2919
|
+
edge?: boolean;
|
|
2920
|
+
/**
|
|
2921
|
+
* Optional global middleware function. When omitted, the handler does not
|
|
2922
|
+
* attempt to auto-load `middleware.ts` from disk (important for edge
|
|
2923
|
+
* bundles where FS is unavailable). Adapters should pass pre-compiled
|
|
2924
|
+
* middleware at build time.
|
|
2925
|
+
*/
|
|
2926
|
+
middleware?: {
|
|
2927
|
+
fn: MiddlewareFn;
|
|
2928
|
+
config?: MiddlewareConfig | null;
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
/**
|
|
2933
|
+
* Build a runtime-neutral `fetch(req) → Promise<Response>` handler from a
|
|
2934
|
+
* routes manifest. Reuses the same request pipeline as `startServer()`
|
|
2935
|
+
* (CORS, middleware, router, SSR, API handlers) but without binding to
|
|
2936
|
+
* `Bun.serve`. Suitable for Cloudflare Workers, Deno Deploy, Vercel Edge,
|
|
2937
|
+
* Netlify Edge, and any other Web-Fetch host.
|
|
2938
|
+
*
|
|
2939
|
+
* Handler registration (`registerApiHandler`, `registerPageHandler`, …) must
|
|
2940
|
+
* happen *before* calling this factory — same contract as `startServer`.
|
|
2941
|
+
*
|
|
2942
|
+
* @example
|
|
2943
|
+
* ```ts
|
|
2944
|
+
* // Cloudflare Workers entry
|
|
2945
|
+
* import { createAppFetchHandler } from "@mandujs/core";
|
|
2946
|
+
* import manifest from "./.mandu/routes.manifest.json";
|
|
2947
|
+
* import "./.mandu/edge-workers/register.js"; // populates registries
|
|
2948
|
+
*
|
|
2949
|
+
* const fetch = createAppFetchHandler(manifest, {
|
|
2950
|
+
* rootDir: "/",
|
|
2951
|
+
* edge: true,
|
|
2952
|
+
* cssPath: false,
|
|
2953
|
+
* });
|
|
2954
|
+
*
|
|
2955
|
+
* export default { fetch };
|
|
2956
|
+
* ```
|
|
2957
|
+
*/
|
|
2958
|
+
export function createAppFetchHandler(
|
|
2959
|
+
manifest: RoutesManifest,
|
|
2960
|
+
options: AppFetchHandlerOptions
|
|
2961
|
+
): (req: Request) => Promise<Response> {
|
|
2962
|
+
const {
|
|
2963
|
+
rootDir,
|
|
2964
|
+
bundleManifest,
|
|
2965
|
+
cors = false,
|
|
2966
|
+
streaming = false,
|
|
2967
|
+
rateLimit = false,
|
|
2968
|
+
cssPath = false,
|
|
2969
|
+
registry = defaultRegistry,
|
|
2970
|
+
edge = false,
|
|
2971
|
+
middleware,
|
|
2972
|
+
} = options;
|
|
2973
|
+
|
|
2974
|
+
const corsOptions: CorsOptions | false = cors === true ? {} : cors;
|
|
2975
|
+
const rateLimitOptions = normalizeRateLimitOptions(rateLimit);
|
|
2976
|
+
|
|
2977
|
+
registry.settings = {
|
|
2978
|
+
isDev: false,
|
|
2979
|
+
bundleManifest,
|
|
2980
|
+
rootDir,
|
|
2981
|
+
publicDir: "public",
|
|
2982
|
+
cors: corsOptions,
|
|
2983
|
+
streaming,
|
|
2984
|
+
rateLimit: rateLimitOptions,
|
|
2985
|
+
cssPath,
|
|
2986
|
+
edge,
|
|
2987
|
+
};
|
|
2988
|
+
|
|
2989
|
+
registry.rateLimiter = rateLimitOptions ? new MemoryRateLimiter() : null;
|
|
2990
|
+
|
|
2991
|
+
const router = new Router(manifest.routes);
|
|
2992
|
+
|
|
2993
|
+
return createFetchHandler({
|
|
2994
|
+
router,
|
|
2995
|
+
registry,
|
|
2996
|
+
corsOptions,
|
|
2997
|
+
middlewareFn: middleware?.fn ?? null,
|
|
2998
|
+
middlewareConfig: middleware?.config ?? null,
|
|
2999
|
+
handleRequest,
|
|
3000
|
+
});
|
|
3001
|
+
}
|
|
3002
|
+
|
|
2772
3003
|
// ========== Rate Limiting Public API ==========
|
|
2773
3004
|
|
|
2774
3005
|
/**
|