@textjs/app 0.0.0-stage → 1.0.3
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/README.md +34 -2
- package/bin/text.js +29 -0
- package/bin/text.ts +13 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli/build.d.ts +39 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +157 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/config.d.ts +22 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +73 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/dev.d.ts +12 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +49 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/index.d.ts +26 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +192 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.d.ts +31 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +111 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/start.d.ts +14 -0
- package/dist/cli/start.d.ts.map +1 -0
- package/dist/cli/start.js +183 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/ui.d.ts +35 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +73 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +93 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +143 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +44 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +26 -0
- package/dist/context.js.map +1 -0
- package/dist/dev-errors.d.ts +68 -0
- package/dist/dev-errors.d.ts.map +1 -0
- package/dist/dev-errors.js +121 -0
- package/dist/dev-errors.js.map +1 -0
- package/dist/devtools.d.ts +64 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +186 -0
- package/dist/devtools.js.map +1 -0
- package/dist/frame-mirror.d.ts +19 -0
- package/dist/frame-mirror.d.ts.map +1 -0
- package/dist/frame-mirror.js +141 -0
- package/dist/frame-mirror.js.map +1 -0
- package/dist/frame-protocol.d.ts +175 -0
- package/dist/frame-protocol.d.ts.map +1 -0
- package/dist/frame-protocol.js +369 -0
- package/dist/frame-protocol.js.map +1 -0
- package/dist/history.d.ts +54 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +152 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/link.d.ts +32 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +54 -0
- package/dist/link.js.map +1 -0
- package/dist/live-player.d.ts +83 -0
- package/dist/live-player.d.ts.map +1 -0
- package/dist/live-player.js +413 -0
- package/dist/live-player.js.map +1 -0
- package/dist/live-protocol.d.ts +132 -0
- package/dist/live-protocol.d.ts.map +1 -0
- package/dist/live-protocol.js +278 -0
- package/dist/live-protocol.js.map +1 -0
- package/dist/live-stub.d.ts +10 -0
- package/dist/live-stub.d.ts.map +1 -0
- package/dist/live-stub.js +32 -0
- package/dist/live-stub.js.map +1 -0
- package/dist/mcp/cli.d.ts +22 -0
- package/dist/mcp/cli.d.ts.map +1 -0
- package/dist/mcp/cli.js +177 -0
- package/dist/mcp/cli.js.map +1 -0
- package/dist/mcp/frame.d.ts +58 -0
- package/dist/mcp/frame.d.ts.map +1 -0
- package/dist/mcp/frame.js +169 -0
- package/dist/mcp/frame.js.map +1 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/inspect.d.ts +95 -0
- package/dist/mcp/inspect.d.ts.map +1 -0
- package/dist/mcp/inspect.js +386 -0
- package/dist/mcp/inspect.js.map +1 -0
- package/dist/mcp/keys.d.ts +20 -0
- package/dist/mcp/keys.d.ts.map +1 -0
- package/dist/mcp/keys.js +87 -0
- package/dist/mcp/keys.js.map +1 -0
- package/dist/mcp/logs.d.ts +63 -0
- package/dist/mcp/logs.d.ts.map +1 -0
- package/dist/mcp/logs.js +217 -0
- package/dist/mcp/logs.js.map +1 -0
- package/dist/mcp/server.d.ts +64 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +485 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +230 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +742 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/navigation-errors.d.ts +28 -0
- package/dist/navigation-errors.d.ts.map +1 -0
- package/dist/navigation-errors.js +43 -0
- package/dist/navigation-errors.js.map +1 -0
- package/dist/navigation.d.ts +21 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +33 -0
- package/dist/navigation.js.map +1 -0
- package/dist/router.d.ts +76 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +296 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.d.ts +101 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +286 -0
- package/dist/routing.js.map +1 -0
- package/dist/server/adapters/build-step.d.ts +34 -0
- package/dist/server/adapters/build-step.d.ts.map +1 -0
- package/dist/server/adapters/build-step.js +112 -0
- package/dist/server/adapters/build-step.js.map +1 -0
- package/dist/server/adapters/bun.d.ts +8 -0
- package/dist/server/adapters/bun.d.ts.map +1 -0
- package/dist/server/adapters/bun.js +89 -0
- package/dist/server/adapters/bun.js.map +1 -0
- package/dist/server/adapters/bundle.d.ts +21 -0
- package/dist/server/adapters/bundle.d.ts.map +1 -0
- package/dist/server/adapters/bundle.js +86 -0
- package/dist/server/adapters/bundle.js.map +1 -0
- package/dist/server/adapters/cloudflare.d.ts +29 -0
- package/dist/server/adapters/cloudflare.d.ts.map +1 -0
- package/dist/server/adapters/cloudflare.js +105 -0
- package/dist/server/adapters/cloudflare.js.map +1 -0
- package/dist/server/adapters/index.d.ts +10 -0
- package/dist/server/adapters/index.d.ts.map +1 -0
- package/dist/server/adapters/index.js +24 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/runtime-node.d.ts +16 -0
- package/dist/server/adapters/runtime-node.d.ts.map +1 -0
- package/dist/server/adapters/runtime-node.js +29 -0
- package/dist/server/adapters/runtime-node.js.map +1 -0
- package/dist/server/adapters/runtime.d.ts +42 -0
- package/dist/server/adapters/runtime.d.ts.map +1 -0
- package/dist/server/adapters/runtime.js +115 -0
- package/dist/server/adapters/runtime.js.map +1 -0
- package/dist/server/adapters/shared.d.ts +66 -0
- package/dist/server/adapters/shared.d.ts.map +1 -0
- package/dist/server/adapters/shared.js +124 -0
- package/dist/server/adapters/shared.js.map +1 -0
- package/dist/server/adapters/static.d.ts +9 -0
- package/dist/server/adapters/static.d.ts.map +1 -0
- package/dist/server/adapters/static.js +70 -0
- package/dist/server/adapters/static.js.map +1 -0
- package/dist/server/adapters/vercel.d.ts +43 -0
- package/dist/server/adapters/vercel.d.ts.map +1 -0
- package/dist/server/adapters/vercel.js +119 -0
- package/dist/server/adapters/vercel.js.map +1 -0
- package/dist/server/bun-jsx.d.ts +18 -0
- package/dist/server/bun-jsx.d.ts.map +1 -0
- package/dist/server/bun-jsx.js +56 -0
- package/dist/server/bun-jsx.js.map +1 -0
- package/dist/server/cache-route.d.ts +38 -0
- package/dist/server/cache-route.d.ts.map +1 -0
- package/dist/server/cache-route.js +109 -0
- package/dist/server/cache-route.js.map +1 -0
- package/dist/server/cache.d.ts +131 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/cache.js +409 -0
- package/dist/server/cache.js.map +1 -0
- package/dist/server/frames.d.ts +45 -0
- package/dist/server/frames.d.ts.map +1 -0
- package/dist/server/frames.js +294 -0
- package/dist/server/frames.js.map +1 -0
- package/dist/server/http.d.ts +80 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +453 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/jsx-dev-runtime.d.ts +5 -0
- package/dist/server/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/server/jsx-dev-runtime.js +6 -0
- package/dist/server/jsx-dev-runtime.js.map +1 -0
- package/dist/server/jsx-runtime.d.ts +10 -0
- package/dist/server/jsx-runtime.d.ts.map +1 -0
- package/dist/server/jsx-runtime.js +11 -0
- package/dist/server/jsx-runtime.js.map +1 -0
- package/dist/server/live-app.d.ts +87 -0
- package/dist/server/live-app.d.ts.map +1 -0
- package/dist/server/live-app.js +307 -0
- package/dist/server/live-app.js.map +1 -0
- package/dist/server/live-build.d.ts +70 -0
- package/dist/server/live-build.d.ts.map +1 -0
- package/dist/server/live-build.js +222 -0
- package/dist/server/live-build.js.map +1 -0
- package/dist/server/live-dev.d.ts +62 -0
- package/dist/server/live-dev.d.ts.map +1 -0
- package/dist/server/live-dev.js +313 -0
- package/dist/server/live-dev.js.map +1 -0
- package/dist/server/live-manifest.d.ts +23 -0
- package/dist/server/live-manifest.d.ts.map +1 -0
- package/dist/server/live-manifest.js +7 -0
- package/dist/server/live-manifest.js.map +1 -0
- package/dist/server/live-prod.d.ts +30 -0
- package/dist/server/live-prod.d.ts.map +1 -0
- package/dist/server/live-prod.js +97 -0
- package/dist/server/live-prod.js.map +1 -0
- package/dist/server/live-routes.d.ts +72 -0
- package/dist/server/live-routes.d.ts.map +1 -0
- package/dist/server/live-routes.js +274 -0
- package/dist/server/live-routes.js.map +1 -0
- package/dist/server/live-web.d.ts +16 -0
- package/dist/server/live-web.d.ts.map +1 -0
- package/dist/server/live-web.js +159 -0
- package/dist/server/live-web.js.map +1 -0
- package/dist/server/live.d.ts +138 -0
- package/dist/server/live.d.ts.map +1 -0
- package/dist/server/live.js +542 -0
- package/dist/server/live.js.map +1 -0
- package/dist/server/middleware-node.d.ts +48 -0
- package/dist/server/middleware-node.d.ts.map +1 -0
- package/dist/server/middleware-node.js +167 -0
- package/dist/server/middleware-node.js.map +1 -0
- package/dist/server/middleware-runtime.d.ts +97 -0
- package/dist/server/middleware-runtime.d.ts.map +1 -0
- package/dist/server/middleware-runtime.js +447 -0
- package/dist/server/middleware-runtime.js.map +1 -0
- package/dist/server/middleware.d.ts +145 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +365 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/ppr.d.ts +22 -0
- package/dist/server/ppr.d.ts.map +1 -0
- package/dist/server/ppr.js +58 -0
- package/dist/server/ppr.js.map +1 -0
- package/dist/server/scan.d.ts +48 -0
- package/dist/server/scan.d.ts.map +1 -0
- package/dist/server/scan.js +161 -0
- package/dist/server/scan.js.map +1 -0
- package/dist/server/server-components.d.ts +65 -0
- package/dist/server/server-components.d.ts.map +1 -0
- package/dist/server/server-components.js +215 -0
- package/dist/server/server-components.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +5 -0
- package/dist/terminal.js.map +1 -0
- package/dist/testing.d.ts +5 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +5 -0
- package/dist/testing.js.map +1 -0
- package/dist/vite-plugin.d.ts +51 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +403 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +130 -4
- package/src/cli/build.ts +182 -0
- package/src/cli/config.ts +72 -0
- package/src/cli/dev.ts +54 -0
- package/src/cli/index.ts +185 -0
- package/src/cli/run.ts +124 -0
- package/src/cli/start.ts +194 -0
- package/src/cli/ui.ts +93 -0
- package/src/client.tsx +122 -0
- package/src/config.ts +155 -0
- package/src/context.ts +69 -0
- package/src/dev-errors.ts +167 -0
- package/src/devtools.tsx +419 -0
- package/src/frame-mirror.ts +145 -0
- package/src/frame-protocol.ts +435 -0
- package/src/history.ts +201 -0
- package/src/index.ts +9 -0
- package/src/link.tsx +108 -0
- package/src/live-player.ts +449 -0
- package/src/live-protocol.ts +317 -0
- package/src/live-stub.tsx +32 -0
- package/src/mcp/cli.ts +188 -0
- package/src/mcp/frame.ts +224 -0
- package/src/mcp/index.ts +15 -0
- package/src/mcp/inspect.ts +445 -0
- package/src/mcp/keys.ts +86 -0
- package/src/mcp/logs.ts +225 -0
- package/src/mcp/server.ts +595 -0
- package/src/mcp/session.ts +821 -0
- package/src/navigation-errors.ts +60 -0
- package/src/navigation.ts +44 -0
- package/src/router.tsx +497 -0
- package/src/routing.ts +344 -0
- package/src/server/adapters/build-step.ts +135 -0
- package/src/server/adapters/bun.ts +92 -0
- package/src/server/adapters/bundle.ts +97 -0
- package/src/server/adapters/cloudflare.ts +111 -0
- package/src/server/adapters/index.ts +29 -0
- package/src/server/adapters/runtime-node.ts +34 -0
- package/src/server/adapters/runtime.ts +145 -0
- package/src/server/adapters/shared.ts +159 -0
- package/src/server/adapters/static.ts +68 -0
- package/src/server/adapters/vercel.ts +149 -0
- package/src/server/bun-jsx.ts +65 -0
- package/src/server/cache-route.ts +139 -0
- package/src/server/cache.ts +498 -0
- package/src/server/frames.ts +326 -0
- package/src/server/http.ts +467 -0
- package/src/server/jsx-dev-runtime.ts +9 -0
- package/src/server/jsx-runtime.ts +13 -0
- package/src/server/live-app.tsx +374 -0
- package/src/server/live-build.ts +258 -0
- package/src/server/live-dev.ts +357 -0
- package/src/server/live-manifest.ts +25 -0
- package/src/server/live-prod.ts +120 -0
- package/src/server/live-routes.ts +298 -0
- package/src/server/live-web.ts +170 -0
- package/src/server/live.ts +551 -0
- package/src/server/middleware-node.ts +168 -0
- package/src/server/middleware-runtime.ts +478 -0
- package/src/server/middleware.ts +451 -0
- package/src/server/ppr.ts +62 -0
- package/src/server/scan.ts +190 -0
- package/src/server/server-components.ts +224 -0
- package/src/terminal.ts +5 -0
- package/src/testing.ts +5 -0
- package/src/vite-plugin.ts +413 -0
package/src/routing.ts
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure App Router matching: route manifests, segment ranking, params,
|
|
3
|
+
* not-found resolution and metadata. Shared by the browser router, the Vite
|
|
4
|
+
* plugin and the `text build` route summary.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ComponentType } from "react";
|
|
8
|
+
import type { Metadata } from "./config";
|
|
9
|
+
|
|
10
|
+
export type RouteParams = Record<string, string | string[]>;
|
|
11
|
+
export type SearchParamsRecord = Record<string, string | string[]>;
|
|
12
|
+
|
|
13
|
+
export interface RouteModule {
|
|
14
|
+
default?: ComponentType<any>;
|
|
15
|
+
metadata?: Metadata;
|
|
16
|
+
generateMetadata?: (props: { params: RouteParams; searchParams: SearchParamsRecord }) => Metadata;
|
|
17
|
+
/** `"force-dynamic"` marks a route `ƒ Dynamic` even without dynamic segments. */
|
|
18
|
+
dynamic?: "auto" | "force-static" | "force-dynamic";
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type LazyRouteModule = () => Promise<RouteModule>;
|
|
23
|
+
export type RouteModuleRef = RouteModule | LazyRouteModule;
|
|
24
|
+
|
|
25
|
+
export const ROUTE_FILE_KINDS = ["layout", "page", "loading", "error", "not-found"] as const;
|
|
26
|
+
export type RouteFileKind = (typeof ROUTE_FILE_KINDS)[number];
|
|
27
|
+
|
|
28
|
+
/** One directory of the `app/` tree. */
|
|
29
|
+
export interface RouteSegmentNode {
|
|
30
|
+
/** Directory name: `""` (root), `"logs"`, `"[id]"`, `"[...slug]"`, `"[[...slug]]"`, `"(group)"`. */
|
|
31
|
+
segment: string;
|
|
32
|
+
layout?: RouteModuleRef;
|
|
33
|
+
page?: RouteModuleRef;
|
|
34
|
+
loading?: RouteModuleRef;
|
|
35
|
+
error?: RouteModuleRef;
|
|
36
|
+
notFound?: RouteModuleRef;
|
|
37
|
+
/** Source files (project-relative) for DevTools and the build summary. */
|
|
38
|
+
files?: Partial<Record<RouteFileKind, string>>;
|
|
39
|
+
children?: RouteSegmentNode[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type RouteManifest = RouteSegmentNode;
|
|
43
|
+
|
|
44
|
+
export type SegmentKind = "static" | "dynamic" | "catchAll" | "optionalCatchAll" | "group";
|
|
45
|
+
|
|
46
|
+
export interface ParsedSegment {
|
|
47
|
+
kind: SegmentKind;
|
|
48
|
+
/** Literal value for static segments, param name for dynamic ones. */
|
|
49
|
+
name: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseSegment(segment: string): ParsedSegment {
|
|
53
|
+
let m = /^\[\[\.\.\.([^\]]+)\]\]$/.exec(segment);
|
|
54
|
+
if (m) return { kind: "optionalCatchAll", name: m[1] as string };
|
|
55
|
+
m = /^\[\.\.\.([^\]]+)\]$/.exec(segment);
|
|
56
|
+
if (m) return { kind: "catchAll", name: m[1] as string };
|
|
57
|
+
m = /^\[([^\]]+)\]$/.exec(segment);
|
|
58
|
+
if (m) return { kind: "dynamic", name: m[1] as string };
|
|
59
|
+
if (/^\(.+\)$/.test(segment)) return { kind: "group", name: segment };
|
|
60
|
+
return { kind: "static", name: segment };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const RANK: Record<SegmentKind, number> = { static: 0, group: 1, dynamic: 2, catchAll: 3, optionalCatchAll: 4 };
|
|
64
|
+
|
|
65
|
+
/** Children ordered by match priority: static > group > [param] > [...slug] > [[...slug]]. */
|
|
66
|
+
export function rankedChildren(node: RouteSegmentNode): RouteSegmentNode[] {
|
|
67
|
+
const list = node.children ? node.children.slice() : [];
|
|
68
|
+
return list.sort((a, b) => {
|
|
69
|
+
const r = RANK[parseSegment(a.segment).kind] - RANK[parseSegment(b.segment).kind];
|
|
70
|
+
return r !== 0 ? r : a.segment.localeCompare(b.segment);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface RouteMatch {
|
|
75
|
+
/** Normalized pathname that was matched. */
|
|
76
|
+
pathname: string;
|
|
77
|
+
/** Route pattern, e.g. `/deployments/[id]` (`/_not-found` for unmatched paths). */
|
|
78
|
+
pattern: string;
|
|
79
|
+
params: RouteParams;
|
|
80
|
+
/** Segment nodes from the root to the matched leaf. */
|
|
81
|
+
chain: RouteSegmentNode[];
|
|
82
|
+
/** `true` when the path did not match a page and the chain ends at the nearest `not-found`. */
|
|
83
|
+
notFound: boolean;
|
|
84
|
+
/** `true` for routes with dynamic segments or `dynamic = "force-dynamic"`. */
|
|
85
|
+
dynamic: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function splitPath(pathname: string): string[] {
|
|
89
|
+
const parts: string[] = [];
|
|
90
|
+
for (const raw of pathname.split("/")) {
|
|
91
|
+
if (raw.length === 0) continue;
|
|
92
|
+
let p = raw;
|
|
93
|
+
try {
|
|
94
|
+
p = decodeURIComponent(raw);
|
|
95
|
+
} catch {
|
|
96
|
+
// keep the raw segment
|
|
97
|
+
}
|
|
98
|
+
parts.push(p);
|
|
99
|
+
}
|
|
100
|
+
return parts;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function normalizePathname(pathname: string): string {
|
|
104
|
+
const parts = pathname.split("/").filter((p) => p.length > 0);
|
|
105
|
+
return `/${parts.join("/")}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Route pattern for a chain of segments (route groups are omitted). */
|
|
109
|
+
export function chainPattern(chain: readonly RouteSegmentNode[]): string {
|
|
110
|
+
const parts: string[] = [];
|
|
111
|
+
for (const n of chain) {
|
|
112
|
+
if (n.segment === "") continue;
|
|
113
|
+
if (parseSegment(n.segment).kind === "group") continue;
|
|
114
|
+
parts.push(n.segment);
|
|
115
|
+
}
|
|
116
|
+
return `/${parts.join("/")}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function matchFrom(node: RouteSegmentNode, parts: readonly string[], i: number, params: RouteParams, chain: RouteSegmentNode[]): RouteSegmentNode[] | null {
|
|
120
|
+
if (i === parts.length && node.page) return chain;
|
|
121
|
+
for (const child of rankedChildren(node)) {
|
|
122
|
+
const seg = parseSegment(child.segment);
|
|
123
|
+
let next: RouteSegmentNode[] | null = null;
|
|
124
|
+
switch (seg.kind) {
|
|
125
|
+
case "static":
|
|
126
|
+
if (i < parts.length && parts[i] === seg.name) next = matchFrom(child, parts, i + 1, params, [...chain, child]);
|
|
127
|
+
break;
|
|
128
|
+
case "group":
|
|
129
|
+
next = matchFrom(child, parts, i, params, [...chain, child]);
|
|
130
|
+
break;
|
|
131
|
+
case "dynamic":
|
|
132
|
+
if (i < parts.length) {
|
|
133
|
+
const p = { ...params, [seg.name]: parts[i] as string };
|
|
134
|
+
next = matchFrom(child, parts, i + 1, p, [...chain, child]);
|
|
135
|
+
if (next) Object.assign(params, p);
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
case "catchAll":
|
|
139
|
+
case "optionalCatchAll": {
|
|
140
|
+
const rest = parts.slice(i);
|
|
141
|
+
if ((rest.length > 0 || seg.kind === "optionalCatchAll") && child.page) {
|
|
142
|
+
if (rest.length > 0) params[seg.name] = rest;
|
|
143
|
+
next = [...chain, child];
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (next) return next;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isForceDynamic(chain: readonly RouteSegmentNode[]): boolean {
|
|
154
|
+
for (const n of chain) {
|
|
155
|
+
for (const ref of [n.layout, n.page]) {
|
|
156
|
+
const mod = peekModule(ref);
|
|
157
|
+
if (mod?.dynamic === "force-dynamic") return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function isDynamicPattern(pattern: string): boolean {
|
|
164
|
+
return pattern.includes("[");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Match a pathname against the manifest. Returns `null` when no page matches. */
|
|
168
|
+
export function matchAppRoutes(manifest: RouteManifest, pathname: string): RouteMatch | null {
|
|
169
|
+
const parts = splitPath(pathname);
|
|
170
|
+
const params: RouteParams = {};
|
|
171
|
+
const chain = matchFrom(manifest, parts, 0, params, [manifest]);
|
|
172
|
+
if (!chain) return null;
|
|
173
|
+
const pattern = chainPattern(chain);
|
|
174
|
+
return {
|
|
175
|
+
pathname: normalizePathname(pathname),
|
|
176
|
+
pattern,
|
|
177
|
+
params,
|
|
178
|
+
chain,
|
|
179
|
+
notFound: false,
|
|
180
|
+
dynamic: isDynamicPattern(pattern) || isForceDynamic(chain),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Longest chain of segment directories matching a prefix of `pathname`,
|
|
186
|
+
* truncated at the deepest directory with a `not-found` file (or the root).
|
|
187
|
+
*/
|
|
188
|
+
export function resolveNotFoundChain(manifest: RouteManifest, pathname: string): { chain: RouteSegmentNode[]; params: RouteParams } {
|
|
189
|
+
const parts = splitPath(pathname);
|
|
190
|
+
const params: RouteParams = {};
|
|
191
|
+
let chain: RouteSegmentNode[] = [manifest];
|
|
192
|
+
let node = manifest;
|
|
193
|
+
let i = 0;
|
|
194
|
+
outer: while (i < parts.length) {
|
|
195
|
+
for (const child of rankedChildren(node)) {
|
|
196
|
+
const seg = parseSegment(child.segment);
|
|
197
|
+
if (seg.kind === "static" && parts[i] === seg.name) {
|
|
198
|
+
chain = [...chain, child];
|
|
199
|
+
node = child;
|
|
200
|
+
i++;
|
|
201
|
+
continue outer;
|
|
202
|
+
}
|
|
203
|
+
if (seg.kind === "dynamic") {
|
|
204
|
+
params[seg.name] = parts[i] as string;
|
|
205
|
+
chain = [...chain, child];
|
|
206
|
+
node = child;
|
|
207
|
+
i++;
|
|
208
|
+
continue outer;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
let end = 0;
|
|
214
|
+
for (let k = 0; k < chain.length; k++) if ((chain[k] as RouteSegmentNode).notFound) end = k;
|
|
215
|
+
return { chain: chain.slice(0, end + 1), params };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Match a pathname, falling back to the nearest `not-found` boundary. */
|
|
219
|
+
export function resolveRoute(manifest: RouteManifest, pathname: string): RouteMatch {
|
|
220
|
+
const match = matchAppRoutes(manifest, pathname);
|
|
221
|
+
if (match) return match;
|
|
222
|
+
const { chain, params } = resolveNotFoundChain(manifest, pathname);
|
|
223
|
+
return {
|
|
224
|
+
pathname: normalizePathname(pathname),
|
|
225
|
+
pattern: "/_not-found",
|
|
226
|
+
params,
|
|
227
|
+
chain,
|
|
228
|
+
notFound: true,
|
|
229
|
+
dynamic: false,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface FlatRoute {
|
|
234
|
+
pattern: string;
|
|
235
|
+
dynamic: boolean;
|
|
236
|
+
/** Project-relative page file, when known. */
|
|
237
|
+
file?: string;
|
|
238
|
+
/** Layout files wrapping the page, root first. */
|
|
239
|
+
layouts: string[];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Every page in the manifest, `/` first, then alphabetical. */
|
|
243
|
+
export function flattenRoutes(manifest: RouteManifest): FlatRoute[] {
|
|
244
|
+
const out: FlatRoute[] = [];
|
|
245
|
+
const walk = (node: RouteSegmentNode, chain: RouteSegmentNode[]): void => {
|
|
246
|
+
const next = [...chain, node];
|
|
247
|
+
if (node.page) {
|
|
248
|
+
const pattern = chainPattern(next);
|
|
249
|
+
out.push({
|
|
250
|
+
pattern,
|
|
251
|
+
dynamic: isDynamicPattern(pattern) || isForceDynamic(next),
|
|
252
|
+
file: node.files?.page,
|
|
253
|
+
layouts: next.map((n) => n.files?.layout).filter((f): f is string => typeof f === "string"),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
for (const c of node.children ?? []) walk(c, next);
|
|
257
|
+
};
|
|
258
|
+
walk(manifest, []);
|
|
259
|
+
return out.sort((a, b) => (a.pattern === "/" ? -1 : b.pattern === "/" ? 1 : a.pattern.localeCompare(b.pattern)));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Modules & metadata
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
const loaded = new WeakMap<LazyRouteModule, RouteModule>();
|
|
267
|
+
|
|
268
|
+
export function isLazyModule(ref: RouteModuleRef): ref is LazyRouteModule {
|
|
269
|
+
return typeof ref === "function";
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The module for `ref` if it is eager or has already loaded. */
|
|
273
|
+
export function peekModule(ref: RouteModuleRef | undefined): RouteModule | undefined {
|
|
274
|
+
if (ref === undefined) return undefined;
|
|
275
|
+
return isLazyModule(ref) ? loaded.get(ref) : ref;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function loadModule(ref: RouteModuleRef): Promise<RouteModule> {
|
|
279
|
+
if (!isLazyModule(ref)) return ref;
|
|
280
|
+
const cached = loaded.get(ref);
|
|
281
|
+
if (cached) return cached;
|
|
282
|
+
const mod = await ref();
|
|
283
|
+
loaded.set(ref, mod);
|
|
284
|
+
return mod;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface ResolvedMetadata {
|
|
288
|
+
title?: string;
|
|
289
|
+
description?: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function readMetadata(mod: RouteModule | undefined, params: RouteParams, searchParams: SearchParamsRecord): Metadata | undefined {
|
|
293
|
+
if (!mod) return undefined;
|
|
294
|
+
if (typeof mod.generateMetadata === "function") {
|
|
295
|
+
try {
|
|
296
|
+
return mod.generateMetadata({ params, searchParams });
|
|
297
|
+
} catch {
|
|
298
|
+
return mod.metadata;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return mod.metadata;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Merge `metadata` from layouts (root first) and the page, applying `title.template`. */
|
|
305
|
+
export function resolveMetadata(match: RouteMatch, searchParams: SearchParamsRecord = {}): ResolvedMetadata {
|
|
306
|
+
const out: ResolvedMetadata = {};
|
|
307
|
+
let template: string | undefined;
|
|
308
|
+
const leaf = match.chain[match.chain.length - 1];
|
|
309
|
+
const refs: Array<RouteModuleRef | undefined> = [];
|
|
310
|
+
for (const n of match.chain) refs.push(n.layout);
|
|
311
|
+
refs.push(match.notFound ? leaf?.notFound : leaf?.page);
|
|
312
|
+
for (const ref of refs) {
|
|
313
|
+
const meta = readMetadata(peekModule(ref), match.params, searchParams);
|
|
314
|
+
if (!meta) continue;
|
|
315
|
+
if (typeof meta.description === "string") out.description = meta.description;
|
|
316
|
+
const t = meta.title;
|
|
317
|
+
if (t === undefined) continue;
|
|
318
|
+
if (typeof t === "string") out.title = template ? template.replace("%s", t) : t;
|
|
319
|
+
else {
|
|
320
|
+
if (typeof t.absolute === "string") out.title = t.absolute;
|
|
321
|
+
else if (typeof t.default === "string") out.title = t.default;
|
|
322
|
+
if (typeof t.template === "string") template = t.template;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function searchParamsToRecord(search: string | URLSearchParams): SearchParamsRecord {
|
|
329
|
+
const sp = typeof search === "string" ? new URLSearchParams(search) : search;
|
|
330
|
+
const out: SearchParamsRecord = {};
|
|
331
|
+
for (const [k, v] of sp) {
|
|
332
|
+
const prev = out[k];
|
|
333
|
+
if (prev === undefined) out[k] = v;
|
|
334
|
+
else out[k] = Array.isArray(prev) ? [...prev, v] : [prev, v];
|
|
335
|
+
}
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Format params for display: `{ id: "dpl_1" }`. */
|
|
340
|
+
export function formatParams(params: RouteParams): string {
|
|
341
|
+
const keys = Object.keys(params);
|
|
342
|
+
if (keys.length === 0) return "{}";
|
|
343
|
+
return `{ ${keys.map((k) => `${k}: ${JSON.stringify(params[k])}`).join(", ")} }`;
|
|
344
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `text build` hooks for middleware, the cache handler and adapters
|
|
3
|
+
* (kept here so `cli/build.ts` only calls two functions).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import type { MiddlewareModule } from "../middleware";
|
|
11
|
+
import { normalizeBasePath, type Rewrite, type TextConfig } from "../../config";
|
|
12
|
+
import { findMiddlewareFile } from "../middleware-node";
|
|
13
|
+
import { compileMatcher } from "../middleware-runtime";
|
|
14
|
+
import type { ApiRoute, AppScan } from "../scan";
|
|
15
|
+
import { toPosix } from "../scan";
|
|
16
|
+
import { bundleServerEntry } from "./bundle";
|
|
17
|
+
import { runAdapter } from "./index";
|
|
18
|
+
import { RUNTIME_BUNDLE, serverRuntimeEntry } from "./runtime-node";
|
|
19
|
+
import { detectAdapter, isAdapterName, type AdapterName, type AdapterResult } from "./shared";
|
|
20
|
+
|
|
21
|
+
export interface ServerFiles {
|
|
22
|
+
middlewareFile: string | null;
|
|
23
|
+
cacheHandlerFile: string | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function serverFiles(root: string, config: TextConfig): ServerFiles {
|
|
27
|
+
const mw = findMiddlewareFile(root);
|
|
28
|
+
let cacheHandlerFile: string | null = null;
|
|
29
|
+
if (config.cacheHandler) {
|
|
30
|
+
cacheHandlerFile = resolve(root, config.cacheHandler);
|
|
31
|
+
if (!existsSync(cacheHandlerFile)) throw new Error(`▼ Text.js: cacheHandler ${JSON.stringify(config.cacheHandler)} does not exist.`);
|
|
32
|
+
}
|
|
33
|
+
return { middlewareFile: mw ? toPosix(mw) : null, cacheHandlerFile: cacheHandlerFile ? toPosix(cacheHandlerFile) : null };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Bundle `.text/server/runtime.mjs` (middleware + cache handler) for `text start`. Returns `true` when written. */
|
|
37
|
+
export async function buildServerRuntime(root: string, config: TextConfig, outDir: string): Promise<boolean> {
|
|
38
|
+
const files = serverFiles(root, config);
|
|
39
|
+
if (!files.middlewareFile && !files.cacheHandlerFile) return false;
|
|
40
|
+
await bundleServerEntry({
|
|
41
|
+
root,
|
|
42
|
+
config,
|
|
43
|
+
code: serverRuntimeEntry(files.middlewareFile, files.cacheHandlerFile),
|
|
44
|
+
outFile: join(outDir, ".text", "server", RUNTIME_BUNDLE),
|
|
45
|
+
target: "node",
|
|
46
|
+
});
|
|
47
|
+
// Validate the matcher at build time instead of on the first request.
|
|
48
|
+
const mw = await loadMiddlewareConfig(root, config, outDir);
|
|
49
|
+
if (mw) compileMatcher(mw.config?.matcher);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const CONFIG_MARK = "\u0000textjs-middleware-config:";
|
|
54
|
+
const EXTRACT_TIMEOUT_MS = 60_000;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* `middleware.ts`'s `config` export, read in a short-lived child process: the
|
|
58
|
+
* build never runs the cache handler, and middleware top-level side effects
|
|
59
|
+
* (database clients, timers) can't crash or hang it. `null` without middleware.
|
|
60
|
+
*/
|
|
61
|
+
export async function loadMiddlewareConfig(root: string, config: TextConfig, outDir: string): Promise<MiddlewareModule | null> {
|
|
62
|
+
const { middlewareFile } = serverFiles(root, config);
|
|
63
|
+
if (!middlewareFile) return null;
|
|
64
|
+
mkdirSync(join(outDir, ".text"), { recursive: true });
|
|
65
|
+
const dir = mkdtempSync(join(outDir, ".text", "middleware-config-"));
|
|
66
|
+
try {
|
|
67
|
+
const bundle = join(dir, "config.mjs");
|
|
68
|
+
await bundleServerEntry({
|
|
69
|
+
root,
|
|
70
|
+
config,
|
|
71
|
+
code: `import * as middleware from ${JSON.stringify(middlewareFile)};\nexport const config = middleware.config;`,
|
|
72
|
+
outFile: bundle,
|
|
73
|
+
target: "node",
|
|
74
|
+
});
|
|
75
|
+
const runner = join(dir, "extract.mjs");
|
|
76
|
+
writeFileSync(
|
|
77
|
+
runner,
|
|
78
|
+
`const m = await import(process.argv[2]);\nprocess.stdout.write(${JSON.stringify(CONFIG_MARK)} + JSON.stringify(m.config ?? null) + "\\n");\nprocess.exit(0);\n`,
|
|
79
|
+
);
|
|
80
|
+
const r = spawnSync(process.execPath, [runner, pathToFileURL(bundle).href], { cwd: root, encoding: "utf8", timeout: EXTRACT_TIMEOUT_MS, env: { ...process.env, NODE_ENV: "production" } });
|
|
81
|
+
const line = (r.stdout ?? "").split("\n").find((l) => l.startsWith(CONFIG_MARK));
|
|
82
|
+
if (r.error || line === undefined) {
|
|
83
|
+
const reason = r.error?.message ?? (r.stderr || `exit code ${r.status}`);
|
|
84
|
+
throw new Error(`▼ Text.js: could not read \`config\` from ${middlewareFile}:\n${reason.trim()}`);
|
|
85
|
+
}
|
|
86
|
+
const value = JSON.parse(line.slice(CONFIG_MARK.length)) as MiddlewareModule["config"] | null;
|
|
87
|
+
return value ? { config: value } : {};
|
|
88
|
+
} finally {
|
|
89
|
+
rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Resolve `--adapter` (or the environment's default) and validate it. */
|
|
94
|
+
export function resolveAdapter(name: string | undefined, env: Record<string, string | undefined> = process.env): AdapterName | undefined {
|
|
95
|
+
const value = name ?? env.TEXTJS_ADAPTER ?? detectAdapter(env);
|
|
96
|
+
if (value === undefined || value === "" || value === "node") return undefined;
|
|
97
|
+
if (!isAdapterName(value)) throw new Error(`▼ Text.js: unknown adapter ${JSON.stringify(value)}. Use static, bun, vercel or cloudflare.`);
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AdapterStep {
|
|
102
|
+
root: string;
|
|
103
|
+
config: TextConfig;
|
|
104
|
+
outDir: string;
|
|
105
|
+
scan: AppScan;
|
|
106
|
+
api: ApiRoute[];
|
|
107
|
+
adapter: AdapterName;
|
|
108
|
+
print(line?: string): void;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runBuildAdapter(step: AdapterStep): Promise<AdapterResult> {
|
|
112
|
+
const files = serverFiles(step.root, step.config);
|
|
113
|
+
const rewrites: Rewrite[] = typeof step.config.rewrites === "function" ? await step.config.rewrites() : (step.config.rewrites ?? []);
|
|
114
|
+
const middleware = await loadMiddlewareConfig(step.root, step.config, step.outDir);
|
|
115
|
+
const result = await runAdapter(
|
|
116
|
+
step.adapter,
|
|
117
|
+
{
|
|
118
|
+
root: step.root,
|
|
119
|
+
outDir: step.outDir,
|
|
120
|
+
config: step.config,
|
|
121
|
+
scan: step.scan,
|
|
122
|
+
api: step.api,
|
|
123
|
+
basePath: normalizeBasePath(step.config.basePath),
|
|
124
|
+
rewrites,
|
|
125
|
+
middlewareFile: files.middlewareFile,
|
|
126
|
+
cacheHandlerFile: files.cacheHandlerFile,
|
|
127
|
+
print: step.print,
|
|
128
|
+
},
|
|
129
|
+
middleware,
|
|
130
|
+
);
|
|
131
|
+
if (step.config.proxy && Object.keys(step.config.proxy).length > 0) {
|
|
132
|
+
result.notes.push("`proxy` in text.config.ts only runs in `text dev` / `text start` and is not part of adapter output; use rewrites.");
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `text build --adapter bun`: a self-contained `dist/server.js` for
|
|
3
|
+
* `bun dist/server.js` (static files, middleware, route handlers, SPA document).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { bundleServerEntry, frameworkFile } from "./bundle";
|
|
8
|
+
import { serverEntry, type AdapterContext, type AdapterResult } from "./shared";
|
|
9
|
+
|
|
10
|
+
export const BUN_SERVER_FILE = "server.js";
|
|
11
|
+
|
|
12
|
+
const BODY = `
|
|
13
|
+
import { existsSync, statSync } from "node:fs";
|
|
14
|
+
import { join, normalize, sep } from "node:path";
|
|
15
|
+
import { loadLiveProduction } from ${JSON.stringify(frameworkFile("server/live-prod.ts"))};
|
|
16
|
+
import { runNodeStyle } from ${JSON.stringify(frameworkFile("server/live-web.ts"))};
|
|
17
|
+
|
|
18
|
+
const dir = import.meta.dir;
|
|
19
|
+
const self = join(dir, ${JSON.stringify(BUN_SERVER_FILE)});
|
|
20
|
+
|
|
21
|
+
// Server-rendered routes (\`runtime = "server"\`) and prerendered shells: Bun is a long-lived server, so live sessions run here.
|
|
22
|
+
const live = existsSync(join(dir, ".text", "live-manifest.json"))
|
|
23
|
+
? await loadLiveProduction(dir, {
|
|
24
|
+
middleware: options.middleware,
|
|
25
|
+
trustProxy: process.env.TRUST_PROXY === "1",
|
|
26
|
+
origin: process.env.TEXTJS_ORIGIN,
|
|
27
|
+
onError: (error) => console.error(error),
|
|
28
|
+
})
|
|
29
|
+
: null;
|
|
30
|
+
|
|
31
|
+
function fileResponse(path, immutable) {
|
|
32
|
+
const file = Bun.file(path);
|
|
33
|
+
return new Response(file, {
|
|
34
|
+
headers: {
|
|
35
|
+
"content-type": file.type,
|
|
36
|
+
"cache-control": immutable ? "public, max-age=31536000, immutable" : "public, max-age=0, must-revalidate",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const handler = createRequestHandler({
|
|
42
|
+
...options,
|
|
43
|
+
live: live ? (request, url, context) => runNodeStyle(request, url, (req, res) => live.handle(req, res, url), context.clientAddress) : undefined,
|
|
44
|
+
assets(pathname) {
|
|
45
|
+
let path = normalize(join(dir, pathname));
|
|
46
|
+
if ((path !== dir && !path.startsWith(dir + sep)) || path === self) return null;
|
|
47
|
+
try {
|
|
48
|
+
// Directories serve their index.html (prerendered console shells).
|
|
49
|
+
if (statSync(path).isDirectory()) path = join(path, "index.html");
|
|
50
|
+
if (!statSync(path).isFile()) return null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const res = fileResponse(path, pathname.startsWith("/assets/"));
|
|
55
|
+
if (path.endsWith(".html")) res.headers.set("content-type", "text/html; charset=utf-8");
|
|
56
|
+
return res;
|
|
57
|
+
},
|
|
58
|
+
document() {
|
|
59
|
+
const res = fileResponse(join(dir, "index.html"), false);
|
|
60
|
+
res.headers.set("content-type", "text/html; charset=utf-8");
|
|
61
|
+
return res;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (import.meta.main) {
|
|
66
|
+
const server = Bun.serve({
|
|
67
|
+
// Live sessions stream server-sent events (15s heartbeats): don't idle them out.
|
|
68
|
+
idleTimeout: 60,
|
|
69
|
+
port: Number(process.env.PORT ?? 3000),
|
|
70
|
+
hostname: process.env.HOSTNAME ?? "0.0.0.0",
|
|
71
|
+
fetch(request, server) {
|
|
72
|
+
return handler(request, { clientAddress: server.requestIP(request)?.address }).catch((error) => {
|
|
73
|
+
console.error(error);
|
|
74
|
+
return new Response("500 | Internal Server Error", { status: 500 });
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const stop = () => {
|
|
79
|
+
live?.close();
|
|
80
|
+
server.stop();
|
|
81
|
+
process.exit(0);
|
|
82
|
+
};
|
|
83
|
+
process.once("SIGINT", stop);
|
|
84
|
+
process.once("SIGTERM", stop);
|
|
85
|
+
console.log("▼ Text.js ready on http://" + (server.hostname === "0.0.0.0" ? "localhost" : server.hostname) + ":" + server.port + options.basePath);
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
export async function adaptBun(ctx: AdapterContext): Promise<AdapterResult> {
|
|
90
|
+
await bundleServerEntry({ root: ctx.root, config: ctx.config, code: serverEntry(ctx, { body: BODY }), outFile: join(ctx.outDir, BUN_SERVER_FILE), target: "bun" });
|
|
91
|
+
return { adapter: "bun", output: ctx.outDir, notes: [`Run with: bun ${join(ctx.config.outDir ?? "dist", BUN_SERVER_FILE)}`] };
|
|
92
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle generated server entries (adapter servers, edge middleware,
|
|
3
|
+
* `text start` runtime) into single self-contained ES modules with Vite.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
7
|
+
import { builtinModules } from "node:module";
|
|
8
|
+
import { basename, dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { build, mergeConfig, type InlineConfig, type Plugin } from "vite";
|
|
11
|
+
import type { TextConfig } from "../../config";
|
|
12
|
+
import { toPosix } from "../scan";
|
|
13
|
+
|
|
14
|
+
const here = dirname(realpathSync(fileURLToPath(import.meta.url)));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Absolute path of a framework source file (`server/adapters/runtime.ts`),
|
|
18
|
+
* also when this module runs from `dist/` (the sources ship in `src/`).
|
|
19
|
+
*/
|
|
20
|
+
export function frameworkFile(rel: string): string {
|
|
21
|
+
const fromHere = join(here, "..", "..", rel);
|
|
22
|
+
if (existsSync(fromHere)) return toPosix(fromHere);
|
|
23
|
+
for (let dir = here; dirname(dir) !== dir; dir = dirname(dir)) {
|
|
24
|
+
const pkgFile = join(dir, "package.json");
|
|
25
|
+
if (!existsSync(pkgFile)) continue;
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgFile, "utf8")) as { name?: string };
|
|
27
|
+
if (pkg.name === "@textjs/app" && existsSync(join(dir, "src", rel))) return toPosix(join(dir, "src", rel));
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`▼ Text.js: could not locate the framework source file src/${rel}.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type BundleTarget = "node" | "bun" | "webworker" | "edge";
|
|
33
|
+
|
|
34
|
+
export interface BundleOptions {
|
|
35
|
+
root: string;
|
|
36
|
+
config: TextConfig;
|
|
37
|
+
/** Generated entry module source. */
|
|
38
|
+
code: string;
|
|
39
|
+
outFile: string;
|
|
40
|
+
target: BundleTarget;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ENTRY = "virtual:textjs/server-entry";
|
|
44
|
+
|
|
45
|
+
export async function bundleServerEntry(opts: BundleOptions): Promise<void> {
|
|
46
|
+
const worker = opts.target === "webworker" || opts.target === "edge";
|
|
47
|
+
const entryPlugin: Plugin = {
|
|
48
|
+
name: "textjs:server-entry",
|
|
49
|
+
enforce: "pre",
|
|
50
|
+
resolveId(id) {
|
|
51
|
+
return id.endsWith(ENTRY) ? `\0${ENTRY}` : null;
|
|
52
|
+
},
|
|
53
|
+
load(id) {
|
|
54
|
+
return id === `\0${ENTRY}` ? opts.code : null;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
// Workers resolve Node built-ins only by their `node:` names (nodejs_compat); dependencies often import `fs` / `async_hooks`.
|
|
58
|
+
const builtins: Plugin = {
|
|
59
|
+
name: "textjs:node-builtins",
|
|
60
|
+
enforce: "pre",
|
|
61
|
+
resolveId(id) {
|
|
62
|
+
if (!worker) return null;
|
|
63
|
+
if (id.startsWith("node:")) return { id, external: true };
|
|
64
|
+
return builtinModules.includes(id) && !id.startsWith("_") ? { id: `node:${id}`, external: true } : null;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const inline: InlineConfig = {
|
|
68
|
+
configFile: false,
|
|
69
|
+
root: opts.root,
|
|
70
|
+
mode: "production",
|
|
71
|
+
logLevel: "warn",
|
|
72
|
+
clearScreen: false,
|
|
73
|
+
publicDir: false,
|
|
74
|
+
define: worker ? { "process.env.NODE_ENV": JSON.stringify("production") } : undefined,
|
|
75
|
+
resolve: worker ? { conditions: ["workerd", "worker", "edge-light", "browser", "import", "module", "default"] } : undefined,
|
|
76
|
+
ssr: {
|
|
77
|
+
target: worker ? "webworker" : "node",
|
|
78
|
+
noExternal: true,
|
|
79
|
+
...(worker ? { resolve: { conditions: ["workerd", "worker", "edge-light", "import", "module", "default"], externalConditions: ["workerd", "worker"] } } : {}),
|
|
80
|
+
},
|
|
81
|
+
build: {
|
|
82
|
+
ssr: ENTRY,
|
|
83
|
+
outDir: dirname(opts.outFile),
|
|
84
|
+
emptyOutDir: false,
|
|
85
|
+
copyPublicDir: false,
|
|
86
|
+
target: worker ? "es2022" : "node18",
|
|
87
|
+
minify: false,
|
|
88
|
+
reportCompressedSize: false,
|
|
89
|
+
rollupOptions: {
|
|
90
|
+
external: worker ? [/^node:/] : undefined,
|
|
91
|
+
output: { entryFileNames: basename(opts.outFile), format: "es", inlineDynamicImports: true },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
plugins: [entryPlugin, builtins],
|
|
95
|
+
};
|
|
96
|
+
await build(opts.config.vite ? mergeConfig(inline, opts.config.vite) : inline);
|
|
97
|
+
}
|