@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/cli/start.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { createServer, type Server, type ServerResponse } from "node:http";
|
|
3
|
+
import type { AddressInfo } from "node:net";
|
|
4
|
+
import { extname, join, normalize, resolve, sep } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { BRAND_MARK, txError, txMessage } from "@textjs/core";
|
|
7
|
+
import { normalizeBasePath, type ProxyTarget } from "../config";
|
|
8
|
+
import { matchApiRoute } from "../server/scan";
|
|
9
|
+
import { loadLiveProduction } from "../server/live-prod";
|
|
10
|
+
import { applyRewrites, compileRewrite, isAllowedHost, isValidHostHeader, normalizeRequestTarget, proxyRequest, requestOrigin, sendWebResponse, toWebRequest, type RouteHandlerModule } from "../server/http";
|
|
11
|
+
import type { RouteManifest } from "../routing";
|
|
12
|
+
import { loadServerRuntime } from "../server/adapters/runtime-node";
|
|
13
|
+
import { runRouteHandlerWithCache } from "../server/cache-route";
|
|
14
|
+
import { runNodeMiddleware } from "../server/middleware-node";
|
|
15
|
+
import { META_DIR } from "./build";
|
|
16
|
+
import { loadTextConfig } from "./config";
|
|
17
|
+
import { banner, color, fail, ok } from "./ui";
|
|
18
|
+
|
|
19
|
+
const MIME: Record<string, string> = {
|
|
20
|
+
".html": "text/html; charset=utf-8",
|
|
21
|
+
".js": "text/javascript; charset=utf-8",
|
|
22
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
23
|
+
".css": "text/css; charset=utf-8",
|
|
24
|
+
".json": "application/json; charset=utf-8",
|
|
25
|
+
".svg": "image/svg+xml",
|
|
26
|
+
".png": "image/png",
|
|
27
|
+
".jpg": "image/jpeg",
|
|
28
|
+
".ico": "image/x-icon",
|
|
29
|
+
".wasm": "application/wasm",
|
|
30
|
+
".txt": "text/plain; charset=utf-8",
|
|
31
|
+
".woff2": "font/woff2",
|
|
32
|
+
".map": "application/json; charset=utf-8",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface StartOptions {
|
|
36
|
+
root: string;
|
|
37
|
+
port?: number;
|
|
38
|
+
host?: string;
|
|
39
|
+
log?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface StartedServer {
|
|
43
|
+
server: Server;
|
|
44
|
+
url: string;
|
|
45
|
+
close(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function startProductionServer(opts: StartOptions): Promise<StartedServer> {
|
|
49
|
+
const started = performance.now();
|
|
50
|
+
const log = opts.log ?? true;
|
|
51
|
+
const { config } = await loadTextConfig(opts.root, "serve", "production");
|
|
52
|
+
const outDir = resolve(opts.root, config.outDir ?? "dist");
|
|
53
|
+
const indexFile = join(outDir, "index.html");
|
|
54
|
+
if (!existsSync(indexFile)) {
|
|
55
|
+
throw txError("TX017", config.outDir ?? "dist");
|
|
56
|
+
}
|
|
57
|
+
const basePath = normalizeBasePath(config.basePath);
|
|
58
|
+
const rewrites = (typeof config.rewrites === "function" ? await config.rewrites() : (config.rewrites ?? [])).map(compileRewrite);
|
|
59
|
+
const proxies = Object.entries(config.proxy ?? {}).map(([prefix, v]) => [prefix, typeof v === "string" ? { target: v } : v] as [string, ProxyTarget]);
|
|
60
|
+
|
|
61
|
+
const apiFile = join(outDir, META_DIR, "server", "api.mjs");
|
|
62
|
+
type ApiBundle = { manifest: RouteManifest; handlers: Record<string, RouteHandlerModule> };
|
|
63
|
+
const api: ApiBundle | null = existsSync(apiFile) ? ((await import(pathToFileURL(apiFile).href)) as ApiBundle) : null;
|
|
64
|
+
const indexHtml = readFileSync(indexFile);
|
|
65
|
+
// `middleware.ts` and `cacheHandler` (bundled by `text build`).
|
|
66
|
+
const runtime = await loadServerRuntime(outDir);
|
|
67
|
+
// Prerendered console shells (PPR) and server-rendered routes (`runtime = "server"`).
|
|
68
|
+
const live = await loadLiveProduction(outDir, {
|
|
69
|
+
log: log ? (m) => console.log(color.gray(m)) : undefined,
|
|
70
|
+
onError: (e) => console.error(fail(e instanceof Error ? (e.stack ?? e.message) : String(e))),
|
|
71
|
+
middleware: runtime?.middleware,
|
|
72
|
+
trustProxy: config.server?.trustProxy ?? process.env.TRUST_PROXY === "1",
|
|
73
|
+
origin: config.server?.origin ?? process.env.TEXTJS_ORIGIN,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const port = opts.port ?? 3000;
|
|
77
|
+
// Loopback only unless `--host` is passed explicitly.
|
|
78
|
+
const host = opts.host ?? "127.0.0.1";
|
|
79
|
+
const loopback = host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]";
|
|
80
|
+
const originOptions = {
|
|
81
|
+
origin: config.server?.origin ?? process.env.TEXTJS_ORIGIN,
|
|
82
|
+
trustProxy: config.server?.trustProxy ?? process.env.TRUST_PROXY === "1",
|
|
83
|
+
};
|
|
84
|
+
// DNS rebinding: a loopback-bound server only answers loopback / IP / allowed host names.
|
|
85
|
+
const allowedHosts = config.server?.allowedHosts ?? [];
|
|
86
|
+
const notFound = (res: ServerResponse): void => {
|
|
87
|
+
res.statusCode = 404;
|
|
88
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
89
|
+
res.end("404 | This page could not be found.");
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const server = createServer((req, res) => {
|
|
93
|
+
const t0 = performance.now();
|
|
94
|
+
const done = (): void => {
|
|
95
|
+
if (log) console.log(color.gray(` ${req.method} ${(req.url ?? "/").split("?")[0]} ${res.statusCode} in ${Math.round(performance.now() - t0)}ms`));
|
|
96
|
+
};
|
|
97
|
+
void (async () => {
|
|
98
|
+
if (loopback ? !isAllowedHost(req.headers.host, allowedHosts) : !isValidHostHeader(req.headers.host)) {
|
|
99
|
+
res.statusCode = loopback ? 403 : 400;
|
|
100
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
101
|
+
res.end(loopback ? `${BRAND_MARK} ${txMessage("TX028", JSON.stringify((req.headers.host ?? "").replace(/:\d+$/, "")), "server.allowedHosts")}` : "400 | Bad Request");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
req.url = normalizeRequestTarget(req.url);
|
|
105
|
+
const origin = requestOrigin(req, originOptions);
|
|
106
|
+
let url = req.url;
|
|
107
|
+
if (basePath) {
|
|
108
|
+
if (url === basePath || url.startsWith(`${basePath}/`) || url.startsWith(`${basePath}?`)) url = url.slice(basePath.length) || "/";
|
|
109
|
+
else return notFound(res);
|
|
110
|
+
}
|
|
111
|
+
if (runtime?.middleware) {
|
|
112
|
+
const next = await runNodeMiddleware(runtime.middleware, req, res, { basePath, origin });
|
|
113
|
+
if (next === null) return;
|
|
114
|
+
url = next;
|
|
115
|
+
}
|
|
116
|
+
const rewritten = rewrites.length > 0 ? applyRewrites(rewrites, url) : null;
|
|
117
|
+
if (rewritten) {
|
|
118
|
+
if (/^[a-z]+:\/\//i.test(rewritten)) return proxyRequest(req, res, rewritten, origin);
|
|
119
|
+
url = rewritten;
|
|
120
|
+
}
|
|
121
|
+
for (const [prefix, target] of proxies) {
|
|
122
|
+
if (url.startsWith(prefix)) {
|
|
123
|
+
const path = target.rewrite ? target.rewrite(url) : url;
|
|
124
|
+
return proxyRequest(req, res, new URL(path, target.target).href, origin);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (live && (await live.handle(req, res, url))) return;
|
|
128
|
+
let pathname: string;
|
|
129
|
+
try {
|
|
130
|
+
pathname = decodeURIComponent(url.split("?")[0] ?? "/");
|
|
131
|
+
} catch {
|
|
132
|
+
res.statusCode = 400;
|
|
133
|
+
return res.end("400 | Bad Request");
|
|
134
|
+
}
|
|
135
|
+
if (api) {
|
|
136
|
+
const m = matchApiRoute(api.manifest, pathname);
|
|
137
|
+
const mod = m ? api.handlers[m.file] : undefined;
|
|
138
|
+
if (m && mod) {
|
|
139
|
+
const request = toWebRequest(Object.assign(req, { url }), origin, res);
|
|
140
|
+
return sendWebResponse(res, await runRouteHandlerWithCache(mod, request, { params: m.params, pattern: m.pattern, pathname }), request.method);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
144
|
+
res.statusCode = 405;
|
|
145
|
+
return res.end();
|
|
146
|
+
}
|
|
147
|
+
const filePath = normalize(join(outDir, pathname));
|
|
148
|
+
const inside = filePath === outDir || filePath.startsWith(outDir + sep);
|
|
149
|
+
const hidden = pathname.split(/[/\\]/).some((p) => p.startsWith(".") && p.length > 1);
|
|
150
|
+
if (inside && !hidden && existsSync(filePath) && statSync(filePath).isFile()) {
|
|
151
|
+
res.statusCode = 200;
|
|
152
|
+
res.setHeader("Content-Type", MIME[extname(filePath)] ?? "application/octet-stream");
|
|
153
|
+
res.setHeader("Cache-Control", pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "public, max-age=0, must-revalidate");
|
|
154
|
+
if (req.method === "HEAD") return res.end();
|
|
155
|
+
createReadStream(filePath).pipe(res);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const last = pathname.split("/").pop() ?? "";
|
|
159
|
+
if (last.includes(".") || hidden) return notFound(res);
|
|
160
|
+
res.statusCode = 200;
|
|
161
|
+
res.setHeader("Content-Type", MIME[".html"] as string);
|
|
162
|
+
res.setHeader("Cache-Control", "public, max-age=0, must-revalidate");
|
|
163
|
+
res.end(req.method === "HEAD" ? undefined : indexHtml);
|
|
164
|
+
})()
|
|
165
|
+
.catch((error: unknown) => {
|
|
166
|
+
if (log) console.error(fail(error instanceof Error ? (error.stack ?? error.message) : String(error)));
|
|
167
|
+
if (!res.headersSent) res.statusCode = 500;
|
|
168
|
+
res.end();
|
|
169
|
+
})
|
|
170
|
+
.finally(done);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await new Promise<void>((resolveListen, reject) => {
|
|
174
|
+
server.once("error", reject);
|
|
175
|
+
server.listen(port, host, () => resolveListen());
|
|
176
|
+
});
|
|
177
|
+
const addr = server.address() as AddressInfo;
|
|
178
|
+
const url = `http://${host === "0.0.0.0" || host === "127.0.0.1" ? "localhost" : host}:${addr.port}${basePath}`;
|
|
179
|
+
if (log) {
|
|
180
|
+
console.log(`\n${banner([["Local", color.cyan(url)]])}\n`);
|
|
181
|
+
console.log(ok("Starting..."));
|
|
182
|
+
console.log(ok(`Ready in ${Math.round(performance.now() - started)}ms`));
|
|
183
|
+
console.log("");
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
server,
|
|
187
|
+
url,
|
|
188
|
+
close: () =>
|
|
189
|
+
new Promise<void>((r) => {
|
|
190
|
+
live?.close();
|
|
191
|
+
server.close(() => r());
|
|
192
|
+
}),
|
|
193
|
+
};
|
|
194
|
+
}
|
package/src/cli/ui.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { BRAND_MARK, ENGINE_NAME, VERSION } from "@textjs/core";
|
|
2
|
+
import type { FlatRoute } from "../routing";
|
|
3
|
+
|
|
4
|
+
const enabled = (): boolean => {
|
|
5
|
+
if (typeof process === "undefined") return false;
|
|
6
|
+
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "") return false;
|
|
7
|
+
if (process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== "0") return true;
|
|
8
|
+
return Boolean(process.stdout && process.stdout.isTTY);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const wrap =
|
|
12
|
+
(open: number, close: number) =>
|
|
13
|
+
(s: string): string =>
|
|
14
|
+
enabled() ? `\x1b[${open}m${s}\x1b[${close}m` : s;
|
|
15
|
+
|
|
16
|
+
export const color = {
|
|
17
|
+
bold: wrap(1, 22),
|
|
18
|
+
dim: wrap(2, 22),
|
|
19
|
+
red: wrap(31, 39),
|
|
20
|
+
green: wrap(32, 39),
|
|
21
|
+
yellow: wrap(33, 39),
|
|
22
|
+
cyan: wrap(36, 39),
|
|
23
|
+
magenta: wrap(35, 39),
|
|
24
|
+
gray: wrap(90, 39),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function stripAnsi(s: string): string {
|
|
28
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function brand(): string {
|
|
32
|
+
return `${color.bold(`${BRAND_MARK} Text.js ${VERSION}`)} ${color.dim(`(${ENGINE_NAME})`)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function banner(lines: Array<[string, string]>): string {
|
|
36
|
+
const out = [` ${brand()}`];
|
|
37
|
+
for (const [k, v] of lines) out.push(` - ${`${k}:`.padEnd(14)}${v}`);
|
|
38
|
+
return out.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const ok = (msg: string): string => ` ${color.green("✓")} ${msg}`;
|
|
42
|
+
export const fail = (msg: string): string => ` ${color.red("⨯")} ${msg}`;
|
|
43
|
+
export const info = (msg: string): string => ` ${color.gray("○")} ${msg}`;
|
|
44
|
+
|
|
45
|
+
export interface SummaryRoute {
|
|
46
|
+
pattern: string;
|
|
47
|
+
dynamic: boolean;
|
|
48
|
+
target: string;
|
|
49
|
+
/** `export const runtime = "server"`. */
|
|
50
|
+
server?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ROUTE_COL = 37;
|
|
54
|
+
const TYPE_COL = 11;
|
|
55
|
+
|
|
56
|
+
/** The Next.js-style route summary table printed by `text build`. */
|
|
57
|
+
export function routeTable(routes: readonly SummaryRoute[]): string {
|
|
58
|
+
const lines = [`${color.bold("Route (app)".padEnd(ROUTE_COL + 4))}${color.bold("Type".padEnd(TYPE_COL))}${color.bold("Target")}`];
|
|
59
|
+
routes.forEach((r, i) => {
|
|
60
|
+
const corner = routes.length === 1 ? "─" : i === 0 ? "┌" : i === routes.length - 1 ? "└" : "├";
|
|
61
|
+
const sym = r.server ? "◇" : r.dynamic ? "ƒ" : "○";
|
|
62
|
+
const pattern = r.pattern.length > ROUTE_COL - 1 ? `${r.pattern.slice(0, ROUTE_COL - 2)}…` : r.pattern;
|
|
63
|
+
const type = r.server ? "Server" : r.dynamic ? "Dynamic" : "Static";
|
|
64
|
+
lines.push(`${color.gray(corner)} ${sym} ${pattern.padEnd(ROUTE_COL)}${type.padEnd(TYPE_COL)}${color.gray(r.target)}`);
|
|
65
|
+
});
|
|
66
|
+
return lines.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const LEGEND = ["○ (Static) prerendered as static console cells", "ƒ (Dynamic) on-demand console reconciliation"].join("\n");
|
|
70
|
+
/** Appended to {@link LEGEND} when the app has server-rendered routes. */
|
|
71
|
+
export const SERVER_LEGEND = "◇ (Server) rendered on the server, streamed as frames";
|
|
72
|
+
|
|
73
|
+
export function summarize(pages: readonly FlatRoute[], api: ReadonlyArray<{ pattern: string }>, serverPatterns: ReadonlySet<string> = new Set()): SummaryRoute[] {
|
|
74
|
+
const out: SummaryRoute[] = pages.map((p) =>
|
|
75
|
+
serverPatterns.has(p.pattern) ? { pattern: p.pattern, dynamic: true, server: true, target: "frame player (SSE)" } : { pattern: p.pattern, dynamic: p.dynamic, target: "console.log (%c)" },
|
|
76
|
+
);
|
|
77
|
+
if (!out.some((r) => r.pattern === "/_not-found")) {
|
|
78
|
+
const nf = { pattern: "/_not-found", dynamic: false, target: "console.log (%c)" };
|
|
79
|
+
out.splice(out[0]?.pattern === "/" ? 1 : 0, 0, nf);
|
|
80
|
+
}
|
|
81
|
+
for (const a of api) out.push({ pattern: a.pattern, dynamic: true, target: "Route Handler" });
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatBytes(n: number): string {
|
|
86
|
+
if (n < 1000) return `${n} B`;
|
|
87
|
+
return `${(n / 1000).toFixed(1)} kB`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function devtoolsHint(): string {
|
|
91
|
+
const keys = process.platform === "darwin" ? "⌘⌥J" : "Ctrl+Shift+J";
|
|
92
|
+
return info(`Open DevTools Console (${keys}) to view your app`);
|
|
93
|
+
}
|
package/src/client.tsx
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@textjs/app/client` — browser bootstrap used by the virtual entry that `text dev`
|
|
3
|
+
* and `text build` generate: mounts the console root, the App Router, the
|
|
4
|
+
* Dev Overlay and the DOM `<FocusPad>`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createElement } from "react";
|
|
8
|
+
import { createRoot } from "react-dom/client";
|
|
9
|
+
import { createConsoleRoot, type ConsoleRoot } from "@textjs/react";
|
|
10
|
+
import { FocusPad } from "@textjs/react/dom";
|
|
11
|
+
import { toClientConfig, type ClientConfig } from "./config";
|
|
12
|
+
import { connectViteHmr, devErrors, installGlobalErrorHandlers, TextDevOverlay, type HotContextLike } from "./devtools";
|
|
13
|
+
import { connectFrameMirror, type MirrorHotContext } from "./frame-mirror";
|
|
14
|
+
import { createBrowserHistory, type RouterHistory } from "./history";
|
|
15
|
+
import { isNavigationError } from "./navigation-errors";
|
|
16
|
+
import { AppRouter } from "./router";
|
|
17
|
+
import type { RouteManifest } from "./routing";
|
|
18
|
+
|
|
19
|
+
export interface BootOptions {
|
|
20
|
+
routes: RouteManifest;
|
|
21
|
+
config?: Partial<ClientConfig>;
|
|
22
|
+
/** `import.meta.env.DEV`. */
|
|
23
|
+
dev?: boolean;
|
|
24
|
+
/** `import.meta.hot`. */
|
|
25
|
+
hot?: HotContextLike;
|
|
26
|
+
/** Element (or id) for the DOM `<FocusPad>` (default `#__textjs`, created when missing). */
|
|
27
|
+
container?: HTMLElement | string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TextApp {
|
|
31
|
+
root: ConsoleRoot;
|
|
32
|
+
history: RouterHistory;
|
|
33
|
+
/** Swap the route manifest (HMR when route files change). */
|
|
34
|
+
updateRoutes(routes: RouteManifest): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
declare global {
|
|
38
|
+
interface Window {
|
|
39
|
+
__TEXTJS_APP__?: TextApp;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveContainer(container: BootOptions["container"]): HTMLElement {
|
|
44
|
+
if (container instanceof HTMLElement) return container;
|
|
45
|
+
const id = typeof container === "string" ? container : "__textjs";
|
|
46
|
+
let el = document.getElementById(id);
|
|
47
|
+
if (!el) {
|
|
48
|
+
el = document.createElement("div");
|
|
49
|
+
el.id = id;
|
|
50
|
+
document.body.appendChild(el);
|
|
51
|
+
}
|
|
52
|
+
return el;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Boot a Text.js app into `console.log`. */
|
|
56
|
+
export async function bootTextApp(options: BootOptions): Promise<TextApp> {
|
|
57
|
+
const config = { ...toClientConfig({}), ...options.config };
|
|
58
|
+
const dev = options.dev ?? false;
|
|
59
|
+
const vp = config.viewport ?? {};
|
|
60
|
+
const seen = new WeakSet<object>();
|
|
61
|
+
|
|
62
|
+
const root = await createConsoleRoot({
|
|
63
|
+
// Follows the docked DevTools console by default; width × height is the
|
|
64
|
+
// size used when it can't be measured (undocked DevTools) or when `responsive: false`.
|
|
65
|
+
width: vp.width ?? 120,
|
|
66
|
+
height: vp.height ?? 36,
|
|
67
|
+
responsive: vp.responsive ?? true,
|
|
68
|
+
theme: vp.theme,
|
|
69
|
+
// A prerendered shell (PPR) already cleared the console: append below it.
|
|
70
|
+
initialClear: typeof window === "undefined" || (window as { __TEXTJS_SHELL__?: number }).__TEXTJS_SHELL__ !== 1,
|
|
71
|
+
// …and its clear opened DevTools' post-clear pacing window.
|
|
72
|
+
previousFrameAt: typeof window === "undefined" ? undefined : (window as { __TEXTJS_SHELL_AT__?: number }).__TEXTJS_SHELL_AT__,
|
|
73
|
+
onCaughtError: (error) => {
|
|
74
|
+
if (isNavigationError(error)) return;
|
|
75
|
+
if (typeof error === "object" && error !== null) {
|
|
76
|
+
if (seen.has(error)) return;
|
|
77
|
+
seen.add(error);
|
|
78
|
+
}
|
|
79
|
+
console.error(error);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const history = createBrowserHistory({ basePath: config.basePath });
|
|
83
|
+
|
|
84
|
+
const render = (routes: RouteManifest): void => {
|
|
85
|
+
root.render(
|
|
86
|
+
createElement(AppRouter, {
|
|
87
|
+
routes,
|
|
88
|
+
history,
|
|
89
|
+
theme: vp.theme,
|
|
90
|
+
defaultTitle: config.title || undefined,
|
|
91
|
+
errorStore: dev ? devErrors : null,
|
|
92
|
+
devOverlay: dev && config.devIndicators ? createElement(TextDevOverlay) : null,
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
render(options.routes);
|
|
97
|
+
|
|
98
|
+
if (typeof document !== "undefined") {
|
|
99
|
+
if (!document.title && config.title) document.title = config.title;
|
|
100
|
+
const el = resolveContainer(options.container);
|
|
101
|
+
createRoot(el).render(
|
|
102
|
+
createElement(FocusPad, {
|
|
103
|
+
root,
|
|
104
|
+
title: config.title || undefined,
|
|
105
|
+
hints: config.hints,
|
|
106
|
+
action: config.action,
|
|
107
|
+
showMirrorDefault: config.showMirrorDefault,
|
|
108
|
+
style: { width: "min(960px, calc(100vw - 48px))", minHeight: "min(640px, calc(100vh - 48px))", boxSizing: "border-box" },
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (dev) {
|
|
114
|
+
if (options.hot) connectViteHmr(options.hot);
|
|
115
|
+
if (options.hot && typeof (options.hot as Partial<MirrorHotContext>).send === "function") connectFrameMirror(root, options.hot as MirrorHotContext, history);
|
|
116
|
+
installGlobalErrorHandlers();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const app: TextApp = { root, history, updateRoutes: render };
|
|
120
|
+
if (typeof window !== "undefined") window.__TEXTJS_APP__ = app;
|
|
121
|
+
return app;
|
|
122
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `text.config.ts` and route `metadata` types.
|
|
3
|
+
*
|
|
4
|
+
* This module is imported by browser code, so Vite / Node types are referenced
|
|
5
|
+
* structurally and never imported at runtime.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ThemeName } from "@textjs/react";
|
|
9
|
+
|
|
10
|
+
export interface ViewportConfig {
|
|
11
|
+
/** Grid width in cells when fixed, or when docked DevTools can't be measured (default `120`). */
|
|
12
|
+
width?: number;
|
|
13
|
+
/** Grid height in cells when fixed, or when docked DevTools can't be measured (default `36`). */
|
|
14
|
+
height?: number;
|
|
15
|
+
/** Follow the docked DevTools console size (default `true`); `false` pins `width × height`. */
|
|
16
|
+
responsive?: boolean;
|
|
17
|
+
/** Initial theme preset (default `"geistDark"`). */
|
|
18
|
+
theme?: ThemeName;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Rewrite {
|
|
22
|
+
/** Path pattern: `/docs/:slug`, `/blog/:path*`. */
|
|
23
|
+
source: string;
|
|
24
|
+
/** Internal path or absolute URL (proxied). */
|
|
25
|
+
destination: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ProxyTarget {
|
|
29
|
+
target: string;
|
|
30
|
+
changeOrigin?: boolean;
|
|
31
|
+
ws?: boolean;
|
|
32
|
+
rewrite?: (path: string) => string;
|
|
33
|
+
secure?: boolean;
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The subset of Vite's dev server the `configureServer` hook relies on. */
|
|
38
|
+
export interface TextDevServer {
|
|
39
|
+
middlewares: { use(...args: unknown[]): unknown };
|
|
40
|
+
config: { root: string; server: { port?: number } };
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TextConfig {
|
|
45
|
+
/** Console grid defaults. */
|
|
46
|
+
viewport?: ViewportConfig;
|
|
47
|
+
/** Mount the in-console Dev Overlay (the `[▼]` pill, its inspector and the Redbox) in development (default `true`). */
|
|
48
|
+
devIndicators?: boolean;
|
|
49
|
+
/** Open the FocusPad's inline console preview by default (default `false`). */
|
|
50
|
+
showMirrorDefault?: boolean;
|
|
51
|
+
/** Build output directory (default `"dist"`). */
|
|
52
|
+
outDir?: string;
|
|
53
|
+
/** App Router directory relative to the project root (default `"app"`). */
|
|
54
|
+
appDir?: string;
|
|
55
|
+
/** Mount the app under a sub-path, e.g. `"/console"`. */
|
|
56
|
+
basePath?: string;
|
|
57
|
+
/** Title shown in the FocusPad and as the default `document.title`. */
|
|
58
|
+
title?: string;
|
|
59
|
+
/** The two or three keys worth pressing, shown on the page's FocusPad, e.g. `[{ keys: "↑ ↓", label: "count" }]`. */
|
|
60
|
+
hints?: Array<{ keys: string; label: string }>;
|
|
61
|
+
/** What to do first, shown as "Then click here and …" (default: `press <first hint>`). */
|
|
62
|
+
action?: string;
|
|
63
|
+
/** Dev / start server proxy rules, keyed by path prefix. */
|
|
64
|
+
proxy?: Record<string, string | ProxyTarget>;
|
|
65
|
+
/** Rewrites applied before routing. External destinations are proxied. */
|
|
66
|
+
rewrites?: Rewrite[] | (() => Rewrite[] | Promise<Rewrite[]>);
|
|
67
|
+
/** Extend the Vite dev server (custom middleware, local service discovery, …). */
|
|
68
|
+
configureServer?: (server: TextDevServer) => void | (() => void) | Promise<void | (() => void)>;
|
|
69
|
+
/** Module (relative to the root) default-exporting a `CacheStore` for `@textjs/app/cache`. */
|
|
70
|
+
cacheHandler?: string;
|
|
71
|
+
/** Escape hatch merged into the generated Vite config. */
|
|
72
|
+
vite?: Record<string, unknown>;
|
|
73
|
+
/**
|
|
74
|
+
* Partial Prerendering: `text build` renders each static route's first frame
|
|
75
|
+
* (with `<Suspense>` fallbacks for dynamic holes) into its HTML so the console
|
|
76
|
+
* paints before JavaScript loads (default `true`).
|
|
77
|
+
*/
|
|
78
|
+
ppr?: boolean;
|
|
79
|
+
/** Server-rendered routes (`export const runtime = "server"`). */
|
|
80
|
+
live?: LiveConfig;
|
|
81
|
+
/** `text start` request handling: public origin, reverse proxies and allowed hosts. */
|
|
82
|
+
server?: ServerConfig;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ServerConfig {
|
|
86
|
+
/**
|
|
87
|
+
* The public origin (`https://console.example.com`). When set, `request.url`
|
|
88
|
+
* always uses it instead of the `Host` header. Also `TEXTJS_ORIGIN`.
|
|
89
|
+
*/
|
|
90
|
+
origin?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Trust `X-Forwarded-Host` / `X-Forwarded-Proto` from a reverse proxy in
|
|
93
|
+
* front of `text start` (default `false`; also `TRUST_PROXY=1`).
|
|
94
|
+
*/
|
|
95
|
+
trustProxy?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Extra `Host` names `text start` answers when bound to loopback (default:
|
|
98
|
+
* `localhost`, `*.localhost` and IP addresses). `true` disables the check.
|
|
99
|
+
* A leading `.` allows subdomains.
|
|
100
|
+
*/
|
|
101
|
+
allowedHosts?: string[] | true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface LiveConfig {
|
|
105
|
+
/** Concurrent sessions per server process (default `256`). */
|
|
106
|
+
maxSessions?: number;
|
|
107
|
+
/** How long a disconnected tab's session waits to be resumed, in ms (default `30000`). */
|
|
108
|
+
reconnectGraceMs?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Type-safe `text.config.ts` helper. */
|
|
112
|
+
export function defineConfig(config: TextConfig): TextConfig {
|
|
113
|
+
return config;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface MetadataTitleTemplate {
|
|
117
|
+
default: string;
|
|
118
|
+
template?: string;
|
|
119
|
+
absolute?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface Metadata {
|
|
123
|
+
title?: string | MetadataTitleTemplate | { absolute: string; default?: string; template?: string };
|
|
124
|
+
description?: string;
|
|
125
|
+
[key: string]: unknown;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Client-safe configuration serialized into the browser bundle. */
|
|
129
|
+
export interface ClientConfig {
|
|
130
|
+
viewport: ViewportConfig;
|
|
131
|
+
devIndicators: boolean;
|
|
132
|
+
showMirrorDefault: boolean;
|
|
133
|
+
basePath: string;
|
|
134
|
+
title: string;
|
|
135
|
+
hints?: Array<{ keys: string; label: string }>;
|
|
136
|
+
action?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function toClientConfig(config: TextConfig): ClientConfig {
|
|
140
|
+
return {
|
|
141
|
+
viewport: { ...config.viewport },
|
|
142
|
+
devIndicators: config.devIndicators ?? true,
|
|
143
|
+
showMirrorDefault: config.showMirrorDefault ?? false,
|
|
144
|
+
basePath: normalizeBasePath(config.basePath),
|
|
145
|
+
title: config.title ?? "",
|
|
146
|
+
...(config.hints ? { hints: config.hints.map((h) => ({ keys: h.keys, label: h.label })) } : {}),
|
|
147
|
+
...(config.action ? { action: config.action } : {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function normalizeBasePath(basePath: string | undefined): string {
|
|
152
|
+
if (!basePath || basePath === "/") return "";
|
|
153
|
+
const withSlash = basePath.startsWith("/") ? basePath : `/${basePath}`;
|
|
154
|
+
return withSlash.replace(/\/+$/, "");
|
|
155
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { sharedContext, type TextTheme, type ThemeName } from "@textjs/react";
|
|
3
|
+
import { txError } from "@textjs/core";
|
|
4
|
+
import type { RouterHistory, RouterLocation } from "./history";
|
|
5
|
+
import type { ResolvedMetadata, RouteManifest, RouteMatch } from "./routing";
|
|
6
|
+
|
|
7
|
+
export interface NavigateOptions {
|
|
8
|
+
/** Accepted for Next.js parity; console frames have no document scroll. */
|
|
9
|
+
scroll?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AppRouterInstance {
|
|
13
|
+
push(href: string, options?: NavigateOptions): void;
|
|
14
|
+
replace(href: string, options?: NavigateOptions): void;
|
|
15
|
+
back(): void;
|
|
16
|
+
forward(): void;
|
|
17
|
+
/** Re-render the current route and reset its error boundaries. */
|
|
18
|
+
refresh(): void;
|
|
19
|
+
/** Preload a lazy route's modules. */
|
|
20
|
+
prefetch(href: string): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RouterState {
|
|
24
|
+
location: RouterLocation;
|
|
25
|
+
match: RouteMatch;
|
|
26
|
+
metadata: ResolvedMetadata;
|
|
27
|
+
router: AppRouterInstance;
|
|
28
|
+
history: RouterHistory;
|
|
29
|
+
manifest: RouteManifest;
|
|
30
|
+
/** Bumped by `router.refresh()`. */
|
|
31
|
+
refreshKey: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const RouterContext = sharedContext<RouterState | null>("routerContext", null, "TextRouterContext");
|
|
35
|
+
|
|
36
|
+
export function useRouterState(): RouterState {
|
|
37
|
+
const state = useContext(RouterContext);
|
|
38
|
+
if (state === null) {
|
|
39
|
+
throw txError("TX014", "Navigation hooks");
|
|
40
|
+
}
|
|
41
|
+
return state;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useOptionalRouterState(): RouterState | null {
|
|
45
|
+
return useContext(RouterContext);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** App-wide shell controls (theme presets, wireframe) shared by DevTools and user settings pages. */
|
|
49
|
+
export interface AppShellState {
|
|
50
|
+
theme: TextTheme;
|
|
51
|
+
themeName: ThemeName | string;
|
|
52
|
+
setTheme(theme: TextTheme | ThemeName): void;
|
|
53
|
+
cycleTheme(): void;
|
|
54
|
+
wireframe: boolean;
|
|
55
|
+
setWireframe(enabled: boolean): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const AppShellContext = sharedContext<AppShellState | null>("appShellContext", null, "TextAppShellContext");
|
|
59
|
+
|
|
60
|
+
/** Theme preset controls for the running app. */
|
|
61
|
+
export function useAppTheme(): AppShellState {
|
|
62
|
+
const state = useContext(AppShellContext);
|
|
63
|
+
if (state === null) throw txError("TX014", "useAppTheme()");
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useOptionalAppShell(): AppShellState | null {
|
|
68
|
+
return useContext(AppShellContext);
|
|
69
|
+
}
|