@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
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The React half of server-rendered console routes: mounts the App Router in a
|
|
3
|
+
* headless console root on the server (Bun / Node), applies forwarded input
|
|
4
|
+
* and exposes the painted `CellBuffer` for the live transport and for Partial
|
|
5
|
+
* Prerendering. Loaded through Vite's SSR graph in `text dev` and bundled into
|
|
6
|
+
* `.text/server/live.mjs` by `text build`, so route modules, `@textjs/react`
|
|
7
|
+
* and this module share one React instance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useContext, useEffect, type ReactNode } from "react";
|
|
11
|
+
import {
|
|
12
|
+
CssConsoleDriver,
|
|
13
|
+
initYoga,
|
|
14
|
+
InputEngine,
|
|
15
|
+
type CellBuffer,
|
|
16
|
+
type ConsoleTarget,
|
|
17
|
+
type RendererScheduler,
|
|
18
|
+
} from "@textjs/core";
|
|
19
|
+
import { createConsoleRootSync, type ConsoleRoot, type ThemeName } from "@textjs/react";
|
|
20
|
+
import { RouterContext } from "../context";
|
|
21
|
+
import { createMemoryHistory, parseHref, type HistoryAction, type MemoryHistory } from "../history";
|
|
22
|
+
import type { LiveInput } from "../live-protocol";
|
|
23
|
+
import { AppRouter } from "../router";
|
|
24
|
+
import { resolveRoute, type RouteManifest, type RouteModule, type RouteModuleRef, type RouteSegmentNode } from "../routing";
|
|
25
|
+
import { isAsyncComponent, serverComponent, ServerComponentCache, ServerComponentsProvider } from "./server-components";
|
|
26
|
+
|
|
27
|
+
/** `export const runtime = "server"` in `page.tsx`. */
|
|
28
|
+
export type RouteRuntime = "server" | "client";
|
|
29
|
+
|
|
30
|
+
export interface LiveAppOptions {
|
|
31
|
+
/** Eager route manifest (every page imported). */
|
|
32
|
+
routes: RouteManifest;
|
|
33
|
+
/** Initial href (`pathname + search`). */
|
|
34
|
+
path: string;
|
|
35
|
+
cols: number;
|
|
36
|
+
rows: number;
|
|
37
|
+
theme?: ThemeName;
|
|
38
|
+
/** `document.title` fallback when no route sets one. */
|
|
39
|
+
defaultTitle?: string;
|
|
40
|
+
focused?: boolean;
|
|
41
|
+
/** A frame with changed cells was painted into {@link LiveApp.buffer}. */
|
|
42
|
+
onFrame?: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* The app navigated. `runtime` is the target route's runtime; `delta` is how
|
|
45
|
+
* far a `"pop"` moved through the history.
|
|
46
|
+
*/
|
|
47
|
+
onNavigate?: (href: string, action: HistoryAction, runtime: RouteRuntime, delta?: number) => void;
|
|
48
|
+
/**
|
|
49
|
+
* May the session render `href` (a server route)? Runs `middleware.ts` for
|
|
50
|
+
* the target before every server-side route transition. Denied transitions
|
|
51
|
+
* are handed to the browser as a document load, where middleware answers.
|
|
52
|
+
*/
|
|
53
|
+
authorize?: (href: string) => Promise<boolean>;
|
|
54
|
+
onTitle?: (title: string) => void;
|
|
55
|
+
onError?: (error: unknown) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface LiveApp {
|
|
59
|
+
readonly root: ConsoleRoot;
|
|
60
|
+
readonly buffer: CellBuffer;
|
|
61
|
+
readonly history: MemoryHistory;
|
|
62
|
+
input(event: LiveInput): void;
|
|
63
|
+
/** Apply a batch of input events (one POST) and paint once. */
|
|
64
|
+
inputMany?(events: readonly LiveInput[]): void;
|
|
65
|
+
/** Pause rendering while no browser is attached; resume (and repaint) on attach. */
|
|
66
|
+
setActive?(active: boolean): void;
|
|
67
|
+
/** Swap the manifest (dev: a route module changed). Keeps the history. */
|
|
68
|
+
setRoutes(routes: RouteManifest): void;
|
|
69
|
+
/** Lay out and paint synchronously. */
|
|
70
|
+
flush(): void;
|
|
71
|
+
dispose(): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const NULL_CONSOLE: ConsoleTarget = { clear() {}, log() {} };
|
|
75
|
+
|
|
76
|
+
const timeoutScheduler: RendererScheduler = {
|
|
77
|
+
request: (cb) => setTimeout(cb, 0),
|
|
78
|
+
cancel: (h) => clearTimeout(h as ReturnType<typeof setTimeout>),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const prepared = new WeakMap<RouteManifest, RouteManifest>();
|
|
82
|
+
|
|
83
|
+
/** Wrap async `default` exports (pages, layouts, …) so they render on the console reconciler. */
|
|
84
|
+
export function prepareServerManifest(routes: RouteManifest): RouteManifest {
|
|
85
|
+
const hit = prepared.get(routes);
|
|
86
|
+
if (hit) return hit;
|
|
87
|
+
const wrapRef = (ref: RouteModuleRef | undefined): RouteModuleRef | undefined => {
|
|
88
|
+
if (ref === undefined || typeof ref === "function") return ref;
|
|
89
|
+
return isAsyncComponent(ref.default) ? ({ ...ref, default: serverComponent(ref.default as never) } as RouteModule) : ref;
|
|
90
|
+
};
|
|
91
|
+
const walk = (node: RouteSegmentNode): RouteSegmentNode => {
|
|
92
|
+
const out: RouteSegmentNode = { ...node };
|
|
93
|
+
for (const k of ["layout", "page", "loading", "error", "notFound"] as const) {
|
|
94
|
+
if (node[k] !== undefined) out[k] = wrapRef(node[k]);
|
|
95
|
+
}
|
|
96
|
+
if (node.children) out.children = node.children.map(walk);
|
|
97
|
+
return out;
|
|
98
|
+
};
|
|
99
|
+
const result = walk(routes);
|
|
100
|
+
prepared.set(routes, result);
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** `true` only when `pathname` matches a page that opts into `runtime = "server"` (HTML routing, new sessions). */
|
|
105
|
+
export function isServerPage(routes: RouteManifest, pathname: string): boolean {
|
|
106
|
+
const match = resolveRoute(routes, pathname);
|
|
107
|
+
if (match.notFound) return false;
|
|
108
|
+
const page = match.chain[match.chain.length - 1]?.page;
|
|
109
|
+
return page !== undefined && typeof page !== "function" && page.runtime === "server";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The runtime of the page `pathname` resolves to (in-app navigation: not-found pages render where they are requested). */
|
|
113
|
+
export function routeRuntime(routes: RouteManifest, pathname: string): RouteRuntime {
|
|
114
|
+
const match = resolveRoute(routes, pathname);
|
|
115
|
+
if (match.notFound) return "server";
|
|
116
|
+
const page = match.chain[match.chain.length - 1]?.page;
|
|
117
|
+
return page !== undefined && typeof page !== "function" && page.runtime === "server" ? "server" : "client";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function TitleReporter({ fallback, onTitle }: { fallback?: string; onTitle?: (title: string) => void }): ReactNode {
|
|
121
|
+
const state = useContext(RouterContext);
|
|
122
|
+
const title = state?.metadata.title ?? fallback;
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (title !== undefined && onTitle) onTitle(title);
|
|
125
|
+
}, [title, onTitle]);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function pathOf(href: string): string {
|
|
130
|
+
const q = href.search(/[?#]/);
|
|
131
|
+
return q < 0 ? href : href.slice(0, q);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Mount a live session's App Router on the server. */
|
|
135
|
+
export async function createLiveApp(options: LiveAppOptions): Promise<LiveApp> {
|
|
136
|
+
await initYoga();
|
|
137
|
+
const engine = new InputEngine({ document: null });
|
|
138
|
+
engine.setFocused(options.focused ?? true);
|
|
139
|
+
const root = createConsoleRootSync({
|
|
140
|
+
width: options.cols,
|
|
141
|
+
height: options.rows,
|
|
142
|
+
responsive: false,
|
|
143
|
+
theme: options.theme,
|
|
144
|
+
input: engine,
|
|
145
|
+
console: NULL_CONSOLE,
|
|
146
|
+
clear: "never",
|
|
147
|
+
scheduler: timeoutScheduler,
|
|
148
|
+
minFrameInterval: 0,
|
|
149
|
+
registerGlobal: false,
|
|
150
|
+
onFrame: (frame) => {
|
|
151
|
+
if (!frame.skipped) options.onFrame?.();
|
|
152
|
+
},
|
|
153
|
+
onCaughtError: (error) => options.onError?.(error),
|
|
154
|
+
onUncaughtError: (error) => options.onError?.(error),
|
|
155
|
+
onRecoverableError: () => undefined,
|
|
156
|
+
});
|
|
157
|
+
const inner = createMemoryHistory(options.path);
|
|
158
|
+
const cache = new ServerComponentCache();
|
|
159
|
+
let routes = prepareServerManifest(options.routes);
|
|
160
|
+
let fromClient = false;
|
|
161
|
+
let disposed = false;
|
|
162
|
+
let lastIndex = inner.index;
|
|
163
|
+
let navSeq = 0;
|
|
164
|
+
|
|
165
|
+
const unlisten = inner.listen((location, action) => {
|
|
166
|
+
const delta = inner.index - lastIndex;
|
|
167
|
+
lastIndex = inner.index;
|
|
168
|
+
if (fromClient) return;
|
|
169
|
+
options.onNavigate?.(location.href, action, routeRuntime(routes, location.pathname), action === "pop" ? delta : undefined);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Route transitions to server pages wait for `authorize` (middleware for the
|
|
174
|
+
* target). Denied ones become a document load of the target, which runs
|
|
175
|
+
* middleware over HTTP (redirects, 401s, rewrites).
|
|
176
|
+
*/
|
|
177
|
+
const guard = (href: string, action: HistoryAction, apply: () => void): void => {
|
|
178
|
+
const target = parseHref(href, inner.location.pathname).href;
|
|
179
|
+
if (!options.authorize || routeRuntime(routes, pathOf(target)) === "client") {
|
|
180
|
+
navSeq++;
|
|
181
|
+
apply();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const seq = ++navSeq;
|
|
185
|
+
options.authorize(target).then(
|
|
186
|
+
(ok) => {
|
|
187
|
+
if (disposed || seq !== navSeq) return;
|
|
188
|
+
if (ok) apply();
|
|
189
|
+
else options.onNavigate?.(target, action, "client");
|
|
190
|
+
},
|
|
191
|
+
(error: unknown) => {
|
|
192
|
+
options.onError?.(error);
|
|
193
|
+
if (!disposed && seq === navSeq) options.onNavigate?.(target, action, "client");
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
const history: MemoryHistory = Object.create(inner, {
|
|
198
|
+
push: { value: (href: string) => guard(href, "push", () => inner.push(href)) },
|
|
199
|
+
replace: { value: (href: string) => guard(href, "replace", () => inner.replace(href)) },
|
|
200
|
+
go: {
|
|
201
|
+
value: (delta: number) => {
|
|
202
|
+
const target = inner.entries[inner.index + delta];
|
|
203
|
+
if (target) guard(target.href, "pop", () => inner.go(delta));
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
back: { value: () => history.go(-1) },
|
|
207
|
+
forward: { value: () => history.go(1) },
|
|
208
|
+
}) as MemoryHistory;
|
|
209
|
+
|
|
210
|
+
const render = (): void => {
|
|
211
|
+
root.render(
|
|
212
|
+
<ServerComponentsProvider cache={cache}>
|
|
213
|
+
<AppRouter routes={routes} history={history} theme={options.theme} documentTitle={false} errorStore={null}>
|
|
214
|
+
<TitleReporter fallback={options.defaultTitle} onTitle={options.onTitle} />
|
|
215
|
+
</AppRouter>
|
|
216
|
+
</ServerComponentsProvider>,
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
render();
|
|
220
|
+
|
|
221
|
+
let active = true;
|
|
222
|
+
const applyInput = (event: LiveInput): void => {
|
|
223
|
+
if (root.unmounted) return;
|
|
224
|
+
switch (event.type) {
|
|
225
|
+
case "key":
|
|
226
|
+
case "paste":
|
|
227
|
+
case "wheel":
|
|
228
|
+
engine.emit(event.type, event as never);
|
|
229
|
+
break;
|
|
230
|
+
case "focus":
|
|
231
|
+
engine.setFocused(event.focused);
|
|
232
|
+
break;
|
|
233
|
+
case "resize":
|
|
234
|
+
root.renderer.resize(event.cols, event.rows);
|
|
235
|
+
break;
|
|
236
|
+
case "nav": {
|
|
237
|
+
const href = parseHref(event.href, inner.location.pathname).href;
|
|
238
|
+
if (href === inner.location.href) break;
|
|
239
|
+
// Browser Back / Forward: move through the same entries when they line up, so both histories agree.
|
|
240
|
+
const delta = event.delta;
|
|
241
|
+
const aligned = delta !== undefined && inner.entries[inner.index + delta]?.href === href;
|
|
242
|
+
guard(href, "pop", () => {
|
|
243
|
+
fromClient = true;
|
|
244
|
+
try {
|
|
245
|
+
if (aligned) inner.go(delta);
|
|
246
|
+
else inner.push(href);
|
|
247
|
+
} finally {
|
|
248
|
+
fromClient = false;
|
|
249
|
+
}
|
|
250
|
+
// Onto a client-rendered route: hand the document over.
|
|
251
|
+
if (routeRuntime(routes, inner.location.pathname) === "client") options.onNavigate?.(inner.location.href, "pop", "client");
|
|
252
|
+
});
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
root,
|
|
260
|
+
history,
|
|
261
|
+
get buffer() {
|
|
262
|
+
return root.renderer.buffer;
|
|
263
|
+
},
|
|
264
|
+
input: applyInput,
|
|
265
|
+
inputMany(events) {
|
|
266
|
+
if (events.length <= 1 || !active) {
|
|
267
|
+
for (const e of events) applyInput(e);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// One paint for the whole batch; React still commits between events.
|
|
271
|
+
root.renderer.pause();
|
|
272
|
+
try {
|
|
273
|
+
for (const e of events) applyInput(e);
|
|
274
|
+
} finally {
|
|
275
|
+
if (!root.unmounted) root.renderer.resume();
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
setActive(next) {
|
|
279
|
+
if (next === active || root.unmounted) return;
|
|
280
|
+
active = next;
|
|
281
|
+
if (next) root.renderer.resume();
|
|
282
|
+
else root.renderer.pause();
|
|
283
|
+
},
|
|
284
|
+
setRoutes(next) {
|
|
285
|
+
routes = prepareServerManifest(next);
|
|
286
|
+
cache.clear();
|
|
287
|
+
if (!root.unmounted) render();
|
|
288
|
+
},
|
|
289
|
+
flush() {
|
|
290
|
+
if (!root.unmounted) root.flushSyncRender();
|
|
291
|
+
},
|
|
292
|
+
dispose() {
|
|
293
|
+
disposed = true;
|
|
294
|
+
unlisten();
|
|
295
|
+
cache.clear();
|
|
296
|
+
if (!root.unmounted) root.unmount();
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Partial Prerendering
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
export interface ShellCall {
|
|
306
|
+
method: "clear" | "log";
|
|
307
|
+
args: string[];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface PrerenderShellOptions {
|
|
311
|
+
routes: RouteManifest;
|
|
312
|
+
path: string;
|
|
313
|
+
cols: number;
|
|
314
|
+
rows: number;
|
|
315
|
+
theme?: ThemeName;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Render the static shell of a route: the first committed frame, with every
|
|
320
|
+
* `<Suspense>` boundary that is still waiting (async server components,
|
|
321
|
+
* `use()`, lazy modules) showing its fallback. `fetch` never resolves while
|
|
322
|
+
* rendering, so data loaded in effects stays in its loading state too.
|
|
323
|
+
*
|
|
324
|
+
* Returns the exact `console.clear()` / `console.log()` calls the client's
|
|
325
|
+
* driver would make for this frame (curtain included).
|
|
326
|
+
*/
|
|
327
|
+
export async function prerenderShell(options: PrerenderShellOptions): Promise<ShellCall[]> {
|
|
328
|
+
await initYoga();
|
|
329
|
+
const g = globalThis as { fetch?: typeof fetch };
|
|
330
|
+
const realFetch = g.fetch;
|
|
331
|
+
g.fetch = (() => new Promise<Response>(() => undefined)) as unknown as typeof fetch;
|
|
332
|
+
// Any error the render hit (caught by an error boundary, `redirect()` / `notFound()` included)
|
|
333
|
+
// means the first frame is not the route's shell: never bake it into HTML.
|
|
334
|
+
const errors: unknown[] = [];
|
|
335
|
+
const root = createConsoleRootSync({
|
|
336
|
+
width: options.cols,
|
|
337
|
+
height: options.rows,
|
|
338
|
+
responsive: false,
|
|
339
|
+
theme: options.theme,
|
|
340
|
+
input: new InputEngine({ document: null }),
|
|
341
|
+
console: NULL_CONSOLE,
|
|
342
|
+
clear: "never",
|
|
343
|
+
scheduler: { request: () => null, cancel: () => undefined },
|
|
344
|
+
minFrameInterval: 0,
|
|
345
|
+
registerGlobal: false,
|
|
346
|
+
onCaughtError: (error) => void errors.push(error),
|
|
347
|
+
onUncaughtError: (error) => void errors.push(error),
|
|
348
|
+
onRecoverableError: () => undefined,
|
|
349
|
+
});
|
|
350
|
+
try {
|
|
351
|
+
const history = createMemoryHistory(options.path);
|
|
352
|
+
root.render(
|
|
353
|
+
<ServerComponentsProvider cache={new ServerComponentCache()}>
|
|
354
|
+
<AppRouter routes={prepareServerManifest(options.routes)} history={history} theme={options.theme} documentTitle={false} errorStore={null} />
|
|
355
|
+
</ServerComponentsProvider>,
|
|
356
|
+
);
|
|
357
|
+
root.flushSyncRender();
|
|
358
|
+
if (errors.length > 0) {
|
|
359
|
+
const first = errors[0];
|
|
360
|
+
throw new Error(`rendering ${options.path} threw: ${first instanceof Error ? first.message : String(first)}`, { cause: first });
|
|
361
|
+
}
|
|
362
|
+
if (history.location.href !== parseHref(options.path).href) throw new Error(`rendering ${options.path} navigated to ${history.location.href}`);
|
|
363
|
+
const calls: ShellCall[] = [];
|
|
364
|
+
const capture: ConsoleTarget = {
|
|
365
|
+
clear: () => void calls.push({ method: "clear", args: [] }),
|
|
366
|
+
log: (...args: unknown[]) => void calls.push({ method: "log", args: args.map(String) }),
|
|
367
|
+
};
|
|
368
|
+
new CssConsoleDriver({ console: capture, curtain: true }).render(root.renderer.buffer);
|
|
369
|
+
return calls;
|
|
370
|
+
} finally {
|
|
371
|
+
root.unmount();
|
|
372
|
+
g.fetch = realFetch;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `text build` step for server-rendered routes and Partial Prerendering:
|
|
3
|
+
*
|
|
4
|
+
* 1. `.text/server/live.mjs` — the SSR bundle (every route, the App Router and
|
|
5
|
+
* the console renderer) that renders live sessions in `text start` and
|
|
6
|
+
* the static shells below.
|
|
7
|
+
* 2. `assets/player-[hash].js` — the frame player, when any page is
|
|
8
|
+
* `export const runtime = "server"`.
|
|
9
|
+
* 3. Static shells: each static route's first frame inlined into its HTML
|
|
10
|
+
* (`index.html`, `<route>/index.html`; server routes under `.text/pages/`).
|
|
11
|
+
* 4. `.text/live-manifest.json` — what `text start` needs to serve them.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { pathToFileURL } from "node:url";
|
|
18
|
+
import { gzipSync } from "node:zlib";
|
|
19
|
+
import { build, mergeConfig, type InlineConfig, type Rollup } from "vite";
|
|
20
|
+
import { normalizeBasePath, type TextConfig } from "../config";
|
|
21
|
+
import type { ShellCall } from "./live-app";
|
|
22
|
+
import { textjsLivePlugin } from "./live-dev";
|
|
23
|
+
import { createLiveHtml, listServerPages, VIRTUAL_LIVE, VIRTUAL_PLAYER } from "./live-routes";
|
|
24
|
+
import { injectShell, shellPath, shellScript } from "./ppr";
|
|
25
|
+
import { LIVE_MANIFEST, type LiveManifest } from "./live-manifest";
|
|
26
|
+
import { listPages, type AppScan } from "./scan";
|
|
27
|
+
|
|
28
|
+
export { LIVE_MANIFEST, type LiveManifest } from "./live-manifest";
|
|
29
|
+
|
|
30
|
+
/** How long one route may take to prerender before it streams without a shell. */
|
|
31
|
+
export const PRERENDER_TIMEOUT_MS = 10_000;
|
|
32
|
+
|
|
33
|
+
type PrerenderResult = { calls: ShellCall[] } | { error: string };
|
|
34
|
+
|
|
35
|
+
const RESULT_MARK = "\u0000textjs-prerender:";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Prerender shells in a disposable child process: route modules' top-level
|
|
39
|
+
* side effects (timers, sockets, a patched `fetch`) can't hang or leak into the
|
|
40
|
+
* build, and each route gets `timeoutMs`. The child exits once its results are
|
|
41
|
+
* written.
|
|
42
|
+
*/
|
|
43
|
+
export async function prerenderInChild(
|
|
44
|
+
liveBundle: string,
|
|
45
|
+
job: { paths: string[]; cols: number; rows: number; theme?: string; timeoutMs: number },
|
|
46
|
+
): Promise<Record<string, PrerenderResult>> {
|
|
47
|
+
if (job.paths.length === 0) return {};
|
|
48
|
+
const runner = join(dirname(liveBundle), `prerender-${process.pid}-${Date.now()}.mjs`);
|
|
49
|
+
writeFileSync(
|
|
50
|
+
runner,
|
|
51
|
+
`const job = JSON.parse(process.argv[3]);
|
|
52
|
+
const out = {};
|
|
53
|
+
const timeout = (ms) => new Promise((_, reject) => setTimeout(() => reject(new Error("timed out after " + ms + "ms")), ms).unref?.());
|
|
54
|
+
try {
|
|
55
|
+
const entry = await Promise.race([import(process.argv[2]), timeout(job.timeoutMs)]);
|
|
56
|
+
for (const path of job.paths) {
|
|
57
|
+
try {
|
|
58
|
+
const calls = await Promise.race([entry.prerenderShell({ routes: entry.routes, path, cols: job.cols, rows: job.rows, theme: job.theme }), timeout(job.timeoutMs)]);
|
|
59
|
+
out[path] = { calls };
|
|
60
|
+
} catch (error) {
|
|
61
|
+
out[path] = { error: error instanceof Error ? error.message : String(error) };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
for (const path of job.paths) out[path] ??= { error: error instanceof Error ? error.message : String(error) };
|
|
66
|
+
}
|
|
67
|
+
process.stdout.write(${JSON.stringify(RESULT_MARK)} + JSON.stringify(out) + "\\n", () => process.exit(0));
|
|
68
|
+
`,
|
|
69
|
+
);
|
|
70
|
+
try {
|
|
71
|
+
const budget = job.timeoutMs * job.paths.length + 30_000;
|
|
72
|
+
const output = await new Promise<string>((resolveRun) => {
|
|
73
|
+
const child = spawn(process.execPath, [runner, pathToFileURL(liveBundle).href, JSON.stringify(job)], { stdio: ["ignore", "pipe", "inherit"], env: process.env });
|
|
74
|
+
let stdout = "";
|
|
75
|
+
child.stdout.setEncoding("utf8");
|
|
76
|
+
child.stdout.on("data", (chunk: string) => {
|
|
77
|
+
stdout += chunk;
|
|
78
|
+
});
|
|
79
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), budget);
|
|
80
|
+
child.on("close", () => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
resolveRun(stdout);
|
|
83
|
+
});
|
|
84
|
+
child.on("error", () => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
resolveRun(stdout);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
const line = output.split("\n").find((l) => l.startsWith(RESULT_MARK));
|
|
90
|
+
if (line === undefined) return Object.fromEntries(job.paths.map((p) => [p, { error: "the prerender process exited without results" }]));
|
|
91
|
+
return JSON.parse(line.slice(RESULT_MARK.length)) as Record<string, PrerenderResult>;
|
|
92
|
+
} finally {
|
|
93
|
+
rmSync(runner, { force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface LiveBuildOptions {
|
|
98
|
+
root: string;
|
|
99
|
+
config: TextConfig;
|
|
100
|
+
outDir: string;
|
|
101
|
+
scan: AppScan;
|
|
102
|
+
/** Build plugins (the caller passes `textjsVitePlugin(config)`). */
|
|
103
|
+
plugins: InlineConfig["plugins"];
|
|
104
|
+
/** Per-route prerender budget (default {@link PRERENDER_TIMEOUT_MS}). */
|
|
105
|
+
prerenderTimeoutMs?: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface LiveBuildResult {
|
|
109
|
+
manifest: LiveManifest;
|
|
110
|
+
shells: number;
|
|
111
|
+
staticRoutes: number;
|
|
112
|
+
/** Routes whose shell could not be prerendered, with the reason. */
|
|
113
|
+
skipped: Array<{ pattern: string; reason: string }>;
|
|
114
|
+
player: { bytes: number; gzip: number } | null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const META = ".text";
|
|
118
|
+
|
|
119
|
+
function htmlFileFor(pattern: string, server: boolean): string {
|
|
120
|
+
const dir = pattern === "/" ? "" : pattern.slice(1);
|
|
121
|
+
if (server) return join(META, "pages", dir, "index.html");
|
|
122
|
+
return dir ? join(dir, "index.html") : "index.html";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Run after the client build. Returns `null` when there is nothing to do. */
|
|
126
|
+
export async function buildLiveRoutes(o: LiveBuildOptions): Promise<LiveBuildResult | null> {
|
|
127
|
+
const { config, outDir, scan } = o;
|
|
128
|
+
const basePath = normalizeBasePath(config.basePath);
|
|
129
|
+
const serverPages = listServerPages(scan);
|
|
130
|
+
const serverSet = new Set(serverPages.map((p) => p.pattern));
|
|
131
|
+
const pages = listPages(scan);
|
|
132
|
+
const ppr = config.ppr !== false;
|
|
133
|
+
const staticPages = ppr ? pages.filter((p) => !p.dynamic) : [];
|
|
134
|
+
if (serverPages.length === 0 && staticPages.length === 0) return null;
|
|
135
|
+
|
|
136
|
+
const withVite = (c: InlineConfig): InlineConfig => (config.vite ? mergeConfig(c, config.vite) : c);
|
|
137
|
+
const serverDir = join(outDir, META, "server");
|
|
138
|
+
await build(
|
|
139
|
+
withVite({
|
|
140
|
+
configFile: false,
|
|
141
|
+
root: o.root,
|
|
142
|
+
mode: "production",
|
|
143
|
+
logLevel: "error",
|
|
144
|
+
clearScreen: false,
|
|
145
|
+
build: {
|
|
146
|
+
ssr: VIRTUAL_LIVE,
|
|
147
|
+
outDir: serverDir,
|
|
148
|
+
emptyOutDir: false,
|
|
149
|
+
target: "node18",
|
|
150
|
+
rollupOptions: { output: { entryFileNames: "live.mjs", format: "es" } },
|
|
151
|
+
},
|
|
152
|
+
ssr: { noExternal: [/^@textjs\//] },
|
|
153
|
+
plugins: o.plugins,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
let player: LiveBuildResult["player"] = null;
|
|
158
|
+
let playerUrl: string | null = null;
|
|
159
|
+
if (serverPages.length > 0) {
|
|
160
|
+
const out = (await build(
|
|
161
|
+
withVite({
|
|
162
|
+
configFile: false,
|
|
163
|
+
root: o.root,
|
|
164
|
+
base: basePath ? `${basePath}/` : "/",
|
|
165
|
+
mode: "production",
|
|
166
|
+
logLevel: "error",
|
|
167
|
+
clearScreen: false,
|
|
168
|
+
publicDir: false,
|
|
169
|
+
build: {
|
|
170
|
+
outDir,
|
|
171
|
+
emptyOutDir: false,
|
|
172
|
+
target: "es2022",
|
|
173
|
+
modulePreload: false,
|
|
174
|
+
rollupOptions: { input: { player: VIRTUAL_PLAYER }, output: { entryFileNames: "assets/player-[hash].js", format: "es" } },
|
|
175
|
+
},
|
|
176
|
+
// The player imports only @textjs/core: no React, no route modules.
|
|
177
|
+
plugins: textjsLivePlugin({ appDir: config.appDir, root: o.root }),
|
|
178
|
+
}),
|
|
179
|
+
)) as Rollup.RollupOutput | Rollup.RollupOutput[];
|
|
180
|
+
const outputs = Array.isArray(out) ? out : [out];
|
|
181
|
+
const chunk = outputs.flatMap((r) => r.output).find((c): c is Rollup.OutputChunk => c.type === "chunk" && c.isEntry);
|
|
182
|
+
if (chunk) {
|
|
183
|
+
const code = readFileSync(join(outDir, chunk.fileName));
|
|
184
|
+
player = { bytes: code.length, gzip: gzipSync(code).length };
|
|
185
|
+
playerUrl = `${basePath}/${chunk.fileName}`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const title = config.title ?? "\u25BC Text.js";
|
|
190
|
+
const manifest: LiveManifest = {
|
|
191
|
+
version: 1,
|
|
192
|
+
basePath,
|
|
193
|
+
title,
|
|
194
|
+
viewport: config.viewport ?? {},
|
|
195
|
+
player: playerUrl,
|
|
196
|
+
serverRoutes: serverPages.map((p) => p.pattern),
|
|
197
|
+
pages: {},
|
|
198
|
+
liveHtml: null,
|
|
199
|
+
...(config.live ? { live: config.live } : {}),
|
|
200
|
+
};
|
|
201
|
+
const write = (rel: string, html: string): void => {
|
|
202
|
+
const file = join(outDir, rel);
|
|
203
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
204
|
+
writeFileSync(file, html);
|
|
205
|
+
};
|
|
206
|
+
const liveDoc = (shell?: string): string => createLiveHtml({ title, basePath, viewport: config.viewport, playerSrc: playerUrl ?? "", shell });
|
|
207
|
+
if (playerUrl) {
|
|
208
|
+
manifest.liveHtml = join(META, "live.html");
|
|
209
|
+
write(manifest.liveHtml, liveDoc());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const skipped: LiveBuildResult["skipped"] = [];
|
|
213
|
+
let shells = 0;
|
|
214
|
+
if (staticPages.length > 0) {
|
|
215
|
+
const indexFile = join(outDir, "index.html");
|
|
216
|
+
const template = existsSync(indexFile) ? readFileSync(indexFile, "utf8") : null;
|
|
217
|
+
const vp = config.viewport ?? {};
|
|
218
|
+
const wanted = staticPages.filter((p) => serverSet.has(p.pattern) || template !== null);
|
|
219
|
+
const results = await prerenderInChild(join(serverDir, "live.mjs"), {
|
|
220
|
+
paths: wanted.map((p) => p.pattern),
|
|
221
|
+
cols: vp.width ?? 120,
|
|
222
|
+
rows: vp.height ?? 36,
|
|
223
|
+
theme: vp.theme,
|
|
224
|
+
timeoutMs: o.prerenderTimeoutMs ?? PRERENDER_TIMEOUT_MS,
|
|
225
|
+
});
|
|
226
|
+
for (const page of wanted) {
|
|
227
|
+
const server = serverSet.has(page.pattern);
|
|
228
|
+
try {
|
|
229
|
+
const result = results[page.pattern] ?? { error: "prerendering did not finish" };
|
|
230
|
+
if ("error" in result) throw new Error(result.error);
|
|
231
|
+
const script = shellScript(result.calls, shellPath(basePath, page.pattern));
|
|
232
|
+
const rel = htmlFileFor(page.pattern, server);
|
|
233
|
+
write(rel, server ? liveDoc(script) : injectShell(template as string, script));
|
|
234
|
+
manifest.pages[page.pattern] = rel;
|
|
235
|
+
shells++;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
skipped.push({ pattern: page.pattern, reason: error instanceof Error ? error.message : String(error) });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
write(join(META, LIVE_MANIFEST), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
242
|
+
return { manifest, shells, staticRoutes: staticPages.length, skipped, player };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Bytes of every client `.js` asset except the frame player (for size comparisons). */
|
|
246
|
+
export function clientBundleBytes(outDir: string): { bytes: number; gzip: number } {
|
|
247
|
+
let bytes = 0;
|
|
248
|
+
let gzip = 0;
|
|
249
|
+
const dir = join(outDir, "assets");
|
|
250
|
+
if (!existsSync(dir)) return { bytes, gzip };
|
|
251
|
+
for (const name of readdirSync(dir)) {
|
|
252
|
+
if (!name.endsWith(".js") || name.startsWith("player-")) continue;
|
|
253
|
+
const buf = readFileSync(join(dir, name));
|
|
254
|
+
bytes += buf.length;
|
|
255
|
+
gzip += gzipSync(buf).length;
|
|
256
|
+
}
|
|
257
|
+
return { bytes, gzip };
|
|
258
|
+
}
|