@textjs/app 0.0.0-stage → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-system scanner for the `app/` directory (Node only).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { join, relative, sep } from "node:path";
|
|
7
|
+
import { flattenRoutes, matchAppRoutes, ROUTE_FILE_KINDS, type FlatRoute, type RouteFileKind, type RouteManifest, type RouteParams, type RouteSegmentNode } from "../routing";
|
|
8
|
+
|
|
9
|
+
export const ROUTE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"] as const;
|
|
10
|
+
|
|
11
|
+
export interface ScannedSegment {
|
|
12
|
+
segment: string;
|
|
13
|
+
dir: string;
|
|
14
|
+
/** Absolute file paths. */
|
|
15
|
+
files: Partial<Record<RouteFileKind, string>>;
|
|
16
|
+
/** Absolute `route.ts` path (API handler). */
|
|
17
|
+
route?: string;
|
|
18
|
+
children: ScannedSegment[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AppScan {
|
|
22
|
+
root: string;
|
|
23
|
+
appDir: string;
|
|
24
|
+
tree: ScannedSegment;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function toPosix(p: string): string {
|
|
28
|
+
return sep === "\\" ? p.split(sep).join("/") : p;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findFile(dir: string, base: string, entries: Set<string>): string | undefined {
|
|
32
|
+
for (const ext of ROUTE_EXTENSIONS) if (entries.has(base + ext)) return join(dir, base + ext);
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function scanDir(dir: string, segment: string): ScannedSegment {
|
|
37
|
+
const entries = new Set(readdirSync(dir));
|
|
38
|
+
const files: Partial<Record<RouteFileKind, string>> = {};
|
|
39
|
+
for (const kind of ROUTE_FILE_KINDS) {
|
|
40
|
+
const f = findFile(dir, kind, entries);
|
|
41
|
+
if (f) files[kind] = f;
|
|
42
|
+
}
|
|
43
|
+
const node: ScannedSegment = { segment, dir, files, route: findFile(dir, "route", entries), children: [] };
|
|
44
|
+
for (const name of [...entries].sort()) {
|
|
45
|
+
if (name.startsWith(".") || name.startsWith("_") || name === "node_modules") continue;
|
|
46
|
+
const full = join(dir, name);
|
|
47
|
+
let isDir = false;
|
|
48
|
+
try {
|
|
49
|
+
isDir = statSync(full).isDirectory();
|
|
50
|
+
} catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (isDir) node.children.push(scanDir(full, name));
|
|
54
|
+
}
|
|
55
|
+
return node;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Scan `<root>/<appDir>` into a segment tree. */
|
|
59
|
+
export function scanApp(root: string, appDir = "app"): AppScan {
|
|
60
|
+
const dir = join(root, appDir);
|
|
61
|
+
if (!existsSync(dir)) return { root, appDir: dir, tree: { segment: "", dir, files: {}, children: [] } };
|
|
62
|
+
return { root, appDir: dir, tree: scanDir(dir, "") };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** `true` when `file` is a route file the manifest depends on. */
|
|
66
|
+
export function isRouteFile(file: string, appDir: string): boolean {
|
|
67
|
+
const rel = toPosix(relative(appDir, file));
|
|
68
|
+
if (rel.startsWith("..")) return false;
|
|
69
|
+
const base = rel.split("/").pop() ?? "";
|
|
70
|
+
const name = base.replace(/\.(tsx|ts|jsx|js)$/, "");
|
|
71
|
+
if (name === base) return false;
|
|
72
|
+
return (ROUTE_FILE_KINDS as readonly string[]).includes(name) || name === "route";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const FORCE_DYNAMIC = /export\s+const\s+dynamic\s*=\s*["']force-dynamic["']/;
|
|
76
|
+
|
|
77
|
+
function isForceDynamicFile(file: string | undefined): boolean {
|
|
78
|
+
if (!file) return false;
|
|
79
|
+
try {
|
|
80
|
+
return FORCE_DYNAMIC.test(readFileSync(file, "utf8"));
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A manifest with placeholder modules (for summaries and matching on the server). */
|
|
87
|
+
export function toStaticManifest(scan: AppScan): RouteManifest {
|
|
88
|
+
const convert = (s: ScannedSegment): RouteSegmentNode => {
|
|
89
|
+
const node: RouteSegmentNode = { segment: s.segment, files: {} };
|
|
90
|
+
for (const kind of ROUTE_FILE_KINDS) {
|
|
91
|
+
const f = s.files[kind];
|
|
92
|
+
if (!f) continue;
|
|
93
|
+
const key = kind === "not-found" ? "notFound" : kind;
|
|
94
|
+
(node as unknown as Record<string, unknown>)[key] = isForceDynamicFile(f) ? { dynamic: "force-dynamic" } : {};
|
|
95
|
+
(node.files as Record<string, string>)[kind] = toPosix(relative(scan.root, f));
|
|
96
|
+
}
|
|
97
|
+
const children = s.children.map(convert).filter((c) => hasContent(c));
|
|
98
|
+
if (children.length > 0) node.children = children;
|
|
99
|
+
return node;
|
|
100
|
+
};
|
|
101
|
+
return convert(scan.tree);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hasContent(n: RouteSegmentNode): boolean {
|
|
105
|
+
return Boolean(n.page || n.layout || n.loading || n.error || n.notFound || (n.children && n.children.length > 0));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ApiRoute {
|
|
109
|
+
pattern: string;
|
|
110
|
+
file: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** API route handlers as a manifest (route files stand in for pages). */
|
|
114
|
+
export function toApiManifest(scan: AppScan): RouteManifest {
|
|
115
|
+
const convert = (s: ScannedSegment): RouteSegmentNode => {
|
|
116
|
+
const node: RouteSegmentNode = { segment: s.segment };
|
|
117
|
+
if (s.route) {
|
|
118
|
+
node.page = {};
|
|
119
|
+
node.files = { page: s.route };
|
|
120
|
+
}
|
|
121
|
+
const children = s.children.map(convert).filter((c) => c.page || (c.children && c.children.length > 0));
|
|
122
|
+
if (children.length > 0) node.children = children;
|
|
123
|
+
return node;
|
|
124
|
+
};
|
|
125
|
+
return convert(scan.tree);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function listApiRoutes(scan: AppScan): ApiRoute[] {
|
|
129
|
+
return flattenRoutes(toApiManifest(scan)).map((r) => ({ pattern: r.pattern, file: r.file as string }));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function matchApiRoute(api: RouteManifest, pathname: string): { file: string; params: RouteParams; pattern: string } | null {
|
|
133
|
+
const m = matchAppRoutes(api, pathname);
|
|
134
|
+
if (!m) return null;
|
|
135
|
+
const leaf = m.chain[m.chain.length - 1] as RouteSegmentNode;
|
|
136
|
+
const file = leaf.files?.page;
|
|
137
|
+
return file ? { file, params: m.params, pattern: m.pattern } : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function listPages(scan: AppScan): FlatRoute[] {
|
|
141
|
+
return flattenRoutes(toStaticManifest(scan));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface GenerateOptions {
|
|
145
|
+
/** Emit `() => import()` for pages (code-split) instead of static imports. */
|
|
146
|
+
lazyPages?: boolean;
|
|
147
|
+
/** Add the self-accepting HMR footer. */
|
|
148
|
+
hmr?: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Generate the `virtual:textjs/routes` module source. */
|
|
152
|
+
export function generateRoutesModule(scan: AppScan, options: GenerateOptions = {}): string {
|
|
153
|
+
const imports: string[] = [];
|
|
154
|
+
let n = 0;
|
|
155
|
+
const ref = (file: string, lazy: boolean): string => {
|
|
156
|
+
const spec = JSON.stringify(toPosix(file));
|
|
157
|
+
if (lazy) return `() => import(${spec})`;
|
|
158
|
+
const id = `m${n++}`;
|
|
159
|
+
imports.push(`import * as ${id} from ${spec};`);
|
|
160
|
+
return id;
|
|
161
|
+
};
|
|
162
|
+
const emit = (s: ScannedSegment, indent: string): string | null => {
|
|
163
|
+
const props: string[] = [`segment: ${JSON.stringify(s.segment)}`];
|
|
164
|
+
const files: string[] = [];
|
|
165
|
+
for (const kind of ROUTE_FILE_KINDS) {
|
|
166
|
+
const f = s.files[kind];
|
|
167
|
+
if (!f) continue;
|
|
168
|
+
const key = kind === "not-found" ? "notFound" : kind;
|
|
169
|
+
props.push(`${key}: ${ref(f, kind === "page" && options.lazyPages === true)}`);
|
|
170
|
+
files.push(`${JSON.stringify(kind)}: ${JSON.stringify(toPosix(relative(scan.root, f)))}`);
|
|
171
|
+
}
|
|
172
|
+
if (files.length > 0) props.push(`files: { ${files.join(", ")} }`);
|
|
173
|
+
const kids = s.children.map((c) => emit(c, `${indent} `)).filter((c): c is string => c !== null);
|
|
174
|
+
if (kids.length > 0) props.push(`children: [\n${kids.map((k) => `${indent} ${k}`).join(",\n")}\n${indent}]`);
|
|
175
|
+
if (files.length === 0 && kids.length === 0 && s.segment !== "") return null;
|
|
176
|
+
return `{ ${props.join(", ")} }`;
|
|
177
|
+
};
|
|
178
|
+
const body = emit(scan.tree, "") ?? `{ segment: "" }`;
|
|
179
|
+
const lines = [...imports, "", `export const routes = ${body};`, "export default routes;"];
|
|
180
|
+
if (options.hmr) {
|
|
181
|
+
lines.push(
|
|
182
|
+
"if (import.meta.hot) {",
|
|
183
|
+
" import.meta.hot.accept((mod) => {",
|
|
184
|
+
" if (mod && typeof window !== 'undefined' && window.__TEXTJS_APP__) window.__TEXTJS_APP__.updateRoutes(mod.routes);",
|
|
185
|
+
" });",
|
|
186
|
+
"}",
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return lines.join("\n");
|
|
190
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async Server Components for server-rendered console routes.
|
|
3
|
+
*
|
|
4
|
+
* React's client reconciler (which the console renderer is) re-runs an
|
|
5
|
+
* `async function` component on every retry, creating a fresh promise each
|
|
6
|
+
* time, so it never settles. On the server, `text dev` / `text build` alias
|
|
7
|
+
* `react/jsx-runtime` to {@link ./jsx-runtime}, which swaps every async
|
|
8
|
+
* component for a wrapper that memoizes its promise per session, navigation
|
|
9
|
+
* and props, then unwraps it with `use()` (suspending to the nearest
|
|
10
|
+
* `<Suspense>` / `loading.tsx` meanwhile). The data access stays on the server:
|
|
11
|
+
* these modules never ship to the frame player.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createElement, use, useContext, type ComponentType, type ReactNode } from "react";
|
|
15
|
+
import { sharedContext } from "@textjs/react";
|
|
16
|
+
import { RouterContext } from "../context";
|
|
17
|
+
|
|
18
|
+
type AnyComponent = (props: any) => unknown;
|
|
19
|
+
|
|
20
|
+
/** Promises kept per cache (least recently used are dropped first). */
|
|
21
|
+
export const SERVER_COMPONENT_CACHE_SIZE = 256;
|
|
22
|
+
|
|
23
|
+
/** Per-session promise cache, cleared whenever the route or `router.refresh()` generation changes. */
|
|
24
|
+
export class ServerComponentCache {
|
|
25
|
+
private generation = "";
|
|
26
|
+
private readonly entries = new Map<string, Promise<ReactNode>>();
|
|
27
|
+
private readonly ids = new WeakMap<object, number>();
|
|
28
|
+
private nextId = 1;
|
|
29
|
+
private inflight = 0;
|
|
30
|
+
private settledAt = 0;
|
|
31
|
+
private readonly maxEntries: number;
|
|
32
|
+
private readonly onActivity: (() => void) | undefined;
|
|
33
|
+
|
|
34
|
+
/** `onActivity` runs whenever a component's promise starts or settles (idle tracking). */
|
|
35
|
+
constructor(options: { maxEntries?: number; onActivity?: () => void } = {}) {
|
|
36
|
+
this.maxEntries = options.maxEntries ?? SERVER_COMPONENT_CACHE_SIZE;
|
|
37
|
+
this.onActivity = options.onActivity;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get size(): number {
|
|
41
|
+
return this.entries.size;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Component promises that have not settled yet. */
|
|
45
|
+
get pending(): number {
|
|
46
|
+
return this.inflight;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `Date.now()` when a component promise last settled (0 = never). */
|
|
50
|
+
get lastSettled(): number {
|
|
51
|
+
return this.settledAt;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
clear(): void {
|
|
55
|
+
this.entries.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Stable identity id for functions, symbols-as-objects and non-plain objects. */
|
|
59
|
+
idOf(value: object): number {
|
|
60
|
+
let id = this.ids.get(value);
|
|
61
|
+
if (id === undefined) {
|
|
62
|
+
id = this.nextId++;
|
|
63
|
+
this.ids.set(value, id);
|
|
64
|
+
}
|
|
65
|
+
return id;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
read(component: AnyComponent, props: object, generation: string): Promise<ReactNode> {
|
|
69
|
+
if (generation !== this.generation) {
|
|
70
|
+
this.generation = generation;
|
|
71
|
+
this.entries.clear();
|
|
72
|
+
}
|
|
73
|
+
const key = `${this.idOf(component)}|${stableKey(props, this, 0)}`;
|
|
74
|
+
let p = this.entries.get(key);
|
|
75
|
+
if (p !== undefined) {
|
|
76
|
+
// Most recently used last.
|
|
77
|
+
this.entries.delete(key);
|
|
78
|
+
this.entries.set(key, p);
|
|
79
|
+
} else {
|
|
80
|
+
try {
|
|
81
|
+
p = Promise.resolve(component(props) as ReactNode | Promise<ReactNode>);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
p = Promise.reject(error);
|
|
84
|
+
}
|
|
85
|
+
// Rejections surface through `use()` to the nearest error boundary.
|
|
86
|
+
this.inflight++;
|
|
87
|
+
this.onActivity?.();
|
|
88
|
+
const settled = (): void => {
|
|
89
|
+
this.inflight--;
|
|
90
|
+
this.settledAt = Date.now();
|
|
91
|
+
this.onActivity?.();
|
|
92
|
+
};
|
|
93
|
+
p.then(settled, settled);
|
|
94
|
+
this.entries.set(key, p);
|
|
95
|
+
for (const k of this.entries.keys()) {
|
|
96
|
+
if (this.entries.size <= this.maxEntries) break;
|
|
97
|
+
this.entries.delete(k);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return p;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const MAX_DEPTH = 64;
|
|
105
|
+
const REACT_ELEMENT = Symbol.for("react.transitional.element");
|
|
106
|
+
const REACT_ELEMENT_LEGACY = Symbol.for("react.element");
|
|
107
|
+
|
|
108
|
+
let warnedFunctionProp = false;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Structural key for props: primitives, plain objects, arrays, `Date`, `URL`,
|
|
112
|
+
* `URLSearchParams`, `RegExp`, `Map`, `Set`, class instances (by constructor
|
|
113
|
+
* and own fields) and React elements by value; component types by identity
|
|
114
|
+
* (they're module-level). Function props aren't part of the key: a callback
|
|
115
|
+
* rebuilt on every render would otherwise miss on every retry and suspend
|
|
116
|
+
* forever. Props rebuilt on every parent render keep the same key, so a
|
|
117
|
+
* retried render reuses the in-flight promise.
|
|
118
|
+
*/
|
|
119
|
+
export function stableKey(value: unknown, cache: ServerComponentCache, depth: number, seen: WeakSet<object> = new WeakSet()): string {
|
|
120
|
+
switch (typeof value) {
|
|
121
|
+
case "string":
|
|
122
|
+
return JSON.stringify(value);
|
|
123
|
+
case "number":
|
|
124
|
+
case "boolean":
|
|
125
|
+
case "bigint":
|
|
126
|
+
return `${typeof value}:${String(value)}`;
|
|
127
|
+
case "undefined":
|
|
128
|
+
return "u";
|
|
129
|
+
case "symbol":
|
|
130
|
+
return `s:${String(value)}`;
|
|
131
|
+
case "function":
|
|
132
|
+
if (!warnedFunctionProp) {
|
|
133
|
+
warnedFunctionProp = true;
|
|
134
|
+
console.warn("▼ Text.js: an async server component received a function prop. Functions aren't part of its cache key; pass data instead of callbacks.");
|
|
135
|
+
}
|
|
136
|
+
return "f";
|
|
137
|
+
}
|
|
138
|
+
if (value === null) return "n";
|
|
139
|
+
const obj = value as Record<string | symbol, unknown>;
|
|
140
|
+
if (seen.has(obj)) return "cycle";
|
|
141
|
+
if (depth >= MAX_DEPTH) return `o#${cache.idOf(obj)}`;
|
|
142
|
+
seen.add(obj);
|
|
143
|
+
try {
|
|
144
|
+
const next = (v: unknown): string => stableKey(v, cache, depth + 1, seen);
|
|
145
|
+
if (Array.isArray(obj)) return `[${obj.map(next).join(",")}]`;
|
|
146
|
+
const tag = obj.$$typeof;
|
|
147
|
+
if (tag === REACT_ELEMENT || tag === REACT_ELEMENT_LEGACY) {
|
|
148
|
+
const type = obj.type;
|
|
149
|
+
const t = typeof type === "string" ? type : typeof type === "object" || typeof type === "function" ? `#${cache.idOf(type as object)}` : String(type);
|
|
150
|
+
return `<${t}:${String(obj.key)}:${next(obj.props)}>`;
|
|
151
|
+
}
|
|
152
|
+
if (obj instanceof Date) return `d:${obj.getTime()}`;
|
|
153
|
+
if (obj instanceof URL) return `url:${JSON.stringify(obj.href)}`;
|
|
154
|
+
if (obj instanceof URLSearchParams) return `qs:${JSON.stringify(obj.toString())}`;
|
|
155
|
+
if (obj instanceof RegExp) return `re:${JSON.stringify(obj.toString())}`;
|
|
156
|
+
if (obj instanceof Map) return `map{${[...obj].map(([k, v]) => `${next(k)}=>${next(v)}`).sort().join(",")}}`;
|
|
157
|
+
if (obj instanceof Set) return `set{${[...obj].map(next).sort().join(",")}}`;
|
|
158
|
+
const proto = Object.getPrototypeOf(obj) as { constructor?: { name?: string } } | null;
|
|
159
|
+
const prefix = proto !== Object.prototype && proto !== null ? `${proto.constructor?.name ?? "?"}` : "";
|
|
160
|
+
const keys = Object.keys(obj).sort();
|
|
161
|
+
return `${prefix}{${keys.map((k) => `${JSON.stringify(k)}:${next(obj[k])}`).join(",")}}`;
|
|
162
|
+
} finally {
|
|
163
|
+
seen.delete(obj);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const ServerComponentCacheContext = sharedContext<ServerComponentCache | null>("serverComponentCache", null, "TextServerComponentCache");
|
|
168
|
+
|
|
169
|
+
/** Used when an async component renders outside a live session (tests, `text run`); bounded like a session's. */
|
|
170
|
+
const fallbackCache = new ServerComponentCache();
|
|
171
|
+
|
|
172
|
+
/** Drop every promise cached outside live sessions (`text run` between apps, tests). */
|
|
173
|
+
export function clearFallbackServerComponentCache(): void {
|
|
174
|
+
fallbackCache.clear();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const AsyncFunction = (async () => undefined).constructor;
|
|
178
|
+
|
|
179
|
+
export function isAsyncComponent(type: unknown): type is AnyComponent {
|
|
180
|
+
return typeof type === "function" && type instanceof AsyncFunction;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const wrappers = new WeakMap<AnyComponent, ComponentType<any>>();
|
|
184
|
+
|
|
185
|
+
/** Wrap an `async function` component so it renders on the console reconciler. */
|
|
186
|
+
export function serverComponent<P extends object>(component: (props: P) => Promise<ReactNode>): ComponentType<P> {
|
|
187
|
+
const fn = component as AnyComponent;
|
|
188
|
+
let wrapped = wrappers.get(fn);
|
|
189
|
+
if (wrapped === undefined) {
|
|
190
|
+
const Wrapped = (props: P): ReactNode => {
|
|
191
|
+
const cache = useContext(ServerComponentCacheContext) ?? fallbackCache;
|
|
192
|
+
const router = useContext(RouterContext);
|
|
193
|
+
const generation = router ? `${router.location.key}:${router.refreshKey}` : "";
|
|
194
|
+
return use(cache.read(fn, props, generation));
|
|
195
|
+
};
|
|
196
|
+
Wrapped.displayName = fn.name ? `${fn.name}` : "ServerComponent";
|
|
197
|
+
wrapped = Wrapped as ComponentType<any>;
|
|
198
|
+
wrappers.set(fn, wrapped);
|
|
199
|
+
}
|
|
200
|
+
return wrapped as ComponentType<P>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** `type` with async function components swapped for their wrappers. */
|
|
204
|
+
const REACT_MEMO = Symbol.for("react.memo");
|
|
205
|
+
const memoWrappers = new WeakMap<object, object>();
|
|
206
|
+
|
|
207
|
+
export function resolveServerType<T>(type: T): T {
|
|
208
|
+
if (isAsyncComponent(type)) return serverComponent(type as never) as T;
|
|
209
|
+
// `React.memo(async function …)`: wrap the inner component, keep the memo.
|
|
210
|
+
if (typeof type === "object" && type !== null && (type as { $$typeof?: symbol }).$$typeof === REACT_MEMO && isAsyncComponent((type as { type?: unknown }).type)) {
|
|
211
|
+
let wrapped = memoWrappers.get(type);
|
|
212
|
+
if (wrapped === undefined) {
|
|
213
|
+
wrapped = { ...(type as object), type: serverComponent((type as unknown as { type: never }).type) };
|
|
214
|
+
memoWrappers.set(type, wrapped);
|
|
215
|
+
}
|
|
216
|
+
return wrapped as T;
|
|
217
|
+
}
|
|
218
|
+
return type;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Provide a session's cache to every async component below. */
|
|
222
|
+
export function ServerComponentsProvider({ cache, children }: { cache: ServerComponentCache; children?: ReactNode }): ReactNode {
|
|
223
|
+
return createElement(ServerComponentCacheContext, { value: cache }, children);
|
|
224
|
+
}
|
package/src/terminal.ts
ADDED