@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.
Files changed (335) hide show
  1. package/README.md +34 -2
  2. package/bin/text.js +29 -0
  3. package/bin/text.ts +13 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/cli/build.d.ts +39 -0
  6. package/dist/cli/build.d.ts.map +1 -0
  7. package/dist/cli/build.js +157 -0
  8. package/dist/cli/build.js.map +1 -0
  9. package/dist/cli/config.d.ts +22 -0
  10. package/dist/cli/config.d.ts.map +1 -0
  11. package/dist/cli/config.js +73 -0
  12. package/dist/cli/config.js.map +1 -0
  13. package/dist/cli/dev.d.ts +12 -0
  14. package/dist/cli/dev.d.ts.map +1 -0
  15. package/dist/cli/dev.js +49 -0
  16. package/dist/cli/dev.js.map +1 -0
  17. package/dist/cli/index.d.ts +26 -0
  18. package/dist/cli/index.d.ts.map +1 -0
  19. package/dist/cli/index.js +192 -0
  20. package/dist/cli/index.js.map +1 -0
  21. package/dist/cli/run.d.ts +31 -0
  22. package/dist/cli/run.d.ts.map +1 -0
  23. package/dist/cli/run.js +111 -0
  24. package/dist/cli/run.js.map +1 -0
  25. package/dist/cli/start.d.ts +14 -0
  26. package/dist/cli/start.d.ts.map +1 -0
  27. package/dist/cli/start.js +183 -0
  28. package/dist/cli/start.js.map +1 -0
  29. package/dist/cli/ui.d.ts +35 -0
  30. package/dist/cli/ui.d.ts.map +1 -0
  31. package/dist/cli/ui.js +73 -0
  32. package/dist/cli/ui.js.map +1 -0
  33. package/dist/client.d.ts +34 -0
  34. package/dist/client.d.ts.map +1 -0
  35. package/dist/client.js +93 -0
  36. package/dist/client.js.map +1 -0
  37. package/dist/config.d.ts +143 -0
  38. package/dist/config.d.ts.map +1 -0
  39. package/dist/config.js +28 -0
  40. package/dist/config.js.map +1 -0
  41. package/dist/context.d.ts +44 -0
  42. package/dist/context.d.ts.map +1 -0
  43. package/dist/context.js +26 -0
  44. package/dist/context.js.map +1 -0
  45. package/dist/dev-errors.d.ts +68 -0
  46. package/dist/dev-errors.d.ts.map +1 -0
  47. package/dist/dev-errors.js +121 -0
  48. package/dist/dev-errors.js.map +1 -0
  49. package/dist/devtools.d.ts +64 -0
  50. package/dist/devtools.d.ts.map +1 -0
  51. package/dist/devtools.js +186 -0
  52. package/dist/devtools.js.map +1 -0
  53. package/dist/frame-mirror.d.ts +19 -0
  54. package/dist/frame-mirror.d.ts.map +1 -0
  55. package/dist/frame-mirror.js +141 -0
  56. package/dist/frame-mirror.js.map +1 -0
  57. package/dist/frame-protocol.d.ts +175 -0
  58. package/dist/frame-protocol.d.ts.map +1 -0
  59. package/dist/frame-protocol.js +369 -0
  60. package/dist/frame-protocol.js.map +1 -0
  61. package/dist/history.d.ts +54 -0
  62. package/dist/history.d.ts.map +1 -0
  63. package/dist/history.js +152 -0
  64. package/dist/history.js.map +1 -0
  65. package/dist/index.d.ts +9 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +8 -0
  68. package/dist/index.js.map +1 -0
  69. package/dist/link.d.ts +32 -0
  70. package/dist/link.d.ts.map +1 -0
  71. package/dist/link.js +54 -0
  72. package/dist/link.js.map +1 -0
  73. package/dist/live-player.d.ts +83 -0
  74. package/dist/live-player.d.ts.map +1 -0
  75. package/dist/live-player.js +413 -0
  76. package/dist/live-player.js.map +1 -0
  77. package/dist/live-protocol.d.ts +132 -0
  78. package/dist/live-protocol.d.ts.map +1 -0
  79. package/dist/live-protocol.js +278 -0
  80. package/dist/live-protocol.js.map +1 -0
  81. package/dist/live-stub.d.ts +10 -0
  82. package/dist/live-stub.d.ts.map +1 -0
  83. package/dist/live-stub.js +32 -0
  84. package/dist/live-stub.js.map +1 -0
  85. package/dist/mcp/cli.d.ts +22 -0
  86. package/dist/mcp/cli.d.ts.map +1 -0
  87. package/dist/mcp/cli.js +177 -0
  88. package/dist/mcp/cli.js.map +1 -0
  89. package/dist/mcp/frame.d.ts +58 -0
  90. package/dist/mcp/frame.d.ts.map +1 -0
  91. package/dist/mcp/frame.js +169 -0
  92. package/dist/mcp/frame.js.map +1 -0
  93. package/dist/mcp/index.d.ts +15 -0
  94. package/dist/mcp/index.d.ts.map +1 -0
  95. package/dist/mcp/index.js +15 -0
  96. package/dist/mcp/index.js.map +1 -0
  97. package/dist/mcp/inspect.d.ts +95 -0
  98. package/dist/mcp/inspect.d.ts.map +1 -0
  99. package/dist/mcp/inspect.js +386 -0
  100. package/dist/mcp/inspect.js.map +1 -0
  101. package/dist/mcp/keys.d.ts +20 -0
  102. package/dist/mcp/keys.d.ts.map +1 -0
  103. package/dist/mcp/keys.js +87 -0
  104. package/dist/mcp/keys.js.map +1 -0
  105. package/dist/mcp/logs.d.ts +63 -0
  106. package/dist/mcp/logs.d.ts.map +1 -0
  107. package/dist/mcp/logs.js +217 -0
  108. package/dist/mcp/logs.js.map +1 -0
  109. package/dist/mcp/server.d.ts +64 -0
  110. package/dist/mcp/server.d.ts.map +1 -0
  111. package/dist/mcp/server.js +485 -0
  112. package/dist/mcp/server.js.map +1 -0
  113. package/dist/mcp/session.d.ts +230 -0
  114. package/dist/mcp/session.d.ts.map +1 -0
  115. package/dist/mcp/session.js +742 -0
  116. package/dist/mcp/session.js.map +1 -0
  117. package/dist/navigation-errors.d.ts +28 -0
  118. package/dist/navigation-errors.d.ts.map +1 -0
  119. package/dist/navigation-errors.js +43 -0
  120. package/dist/navigation-errors.js.map +1 -0
  121. package/dist/navigation.d.ts +21 -0
  122. package/dist/navigation.d.ts.map +1 -0
  123. package/dist/navigation.js +33 -0
  124. package/dist/navigation.js.map +1 -0
  125. package/dist/router.d.ts +76 -0
  126. package/dist/router.d.ts.map +1 -0
  127. package/dist/router.js +296 -0
  128. package/dist/router.js.map +1 -0
  129. package/dist/routing.d.ts +101 -0
  130. package/dist/routing.d.ts.map +1 -0
  131. package/dist/routing.js +286 -0
  132. package/dist/routing.js.map +1 -0
  133. package/dist/server/adapters/build-step.d.ts +34 -0
  134. package/dist/server/adapters/build-step.d.ts.map +1 -0
  135. package/dist/server/adapters/build-step.js +112 -0
  136. package/dist/server/adapters/build-step.js.map +1 -0
  137. package/dist/server/adapters/bun.d.ts +8 -0
  138. package/dist/server/adapters/bun.d.ts.map +1 -0
  139. package/dist/server/adapters/bun.js +89 -0
  140. package/dist/server/adapters/bun.js.map +1 -0
  141. package/dist/server/adapters/bundle.d.ts +21 -0
  142. package/dist/server/adapters/bundle.d.ts.map +1 -0
  143. package/dist/server/adapters/bundle.js +86 -0
  144. package/dist/server/adapters/bundle.js.map +1 -0
  145. package/dist/server/adapters/cloudflare.d.ts +29 -0
  146. package/dist/server/adapters/cloudflare.d.ts.map +1 -0
  147. package/dist/server/adapters/cloudflare.js +105 -0
  148. package/dist/server/adapters/cloudflare.js.map +1 -0
  149. package/dist/server/adapters/index.d.ts +10 -0
  150. package/dist/server/adapters/index.d.ts.map +1 -0
  151. package/dist/server/adapters/index.js +24 -0
  152. package/dist/server/adapters/index.js.map +1 -0
  153. package/dist/server/adapters/runtime-node.d.ts +16 -0
  154. package/dist/server/adapters/runtime-node.d.ts.map +1 -0
  155. package/dist/server/adapters/runtime-node.js +29 -0
  156. package/dist/server/adapters/runtime-node.js.map +1 -0
  157. package/dist/server/adapters/runtime.d.ts +42 -0
  158. package/dist/server/adapters/runtime.d.ts.map +1 -0
  159. package/dist/server/adapters/runtime.js +115 -0
  160. package/dist/server/adapters/runtime.js.map +1 -0
  161. package/dist/server/adapters/shared.d.ts +66 -0
  162. package/dist/server/adapters/shared.d.ts.map +1 -0
  163. package/dist/server/adapters/shared.js +124 -0
  164. package/dist/server/adapters/shared.js.map +1 -0
  165. package/dist/server/adapters/static.d.ts +9 -0
  166. package/dist/server/adapters/static.d.ts.map +1 -0
  167. package/dist/server/adapters/static.js +70 -0
  168. package/dist/server/adapters/static.js.map +1 -0
  169. package/dist/server/adapters/vercel.d.ts +43 -0
  170. package/dist/server/adapters/vercel.d.ts.map +1 -0
  171. package/dist/server/adapters/vercel.js +119 -0
  172. package/dist/server/adapters/vercel.js.map +1 -0
  173. package/dist/server/bun-jsx.d.ts +18 -0
  174. package/dist/server/bun-jsx.d.ts.map +1 -0
  175. package/dist/server/bun-jsx.js +56 -0
  176. package/dist/server/bun-jsx.js.map +1 -0
  177. package/dist/server/cache-route.d.ts +38 -0
  178. package/dist/server/cache-route.d.ts.map +1 -0
  179. package/dist/server/cache-route.js +109 -0
  180. package/dist/server/cache-route.js.map +1 -0
  181. package/dist/server/cache.d.ts +131 -0
  182. package/dist/server/cache.d.ts.map +1 -0
  183. package/dist/server/cache.js +409 -0
  184. package/dist/server/cache.js.map +1 -0
  185. package/dist/server/frames.d.ts +45 -0
  186. package/dist/server/frames.d.ts.map +1 -0
  187. package/dist/server/frames.js +294 -0
  188. package/dist/server/frames.js.map +1 -0
  189. package/dist/server/http.d.ts +80 -0
  190. package/dist/server/http.d.ts.map +1 -0
  191. package/dist/server/http.js +453 -0
  192. package/dist/server/http.js.map +1 -0
  193. package/dist/server/jsx-dev-runtime.d.ts +5 -0
  194. package/dist/server/jsx-dev-runtime.d.ts.map +1 -0
  195. package/dist/server/jsx-dev-runtime.js +6 -0
  196. package/dist/server/jsx-dev-runtime.js.map +1 -0
  197. package/dist/server/jsx-runtime.d.ts +10 -0
  198. package/dist/server/jsx-runtime.d.ts.map +1 -0
  199. package/dist/server/jsx-runtime.js +11 -0
  200. package/dist/server/jsx-runtime.js.map +1 -0
  201. package/dist/server/live-app.d.ts +87 -0
  202. package/dist/server/live-app.d.ts.map +1 -0
  203. package/dist/server/live-app.js +307 -0
  204. package/dist/server/live-app.js.map +1 -0
  205. package/dist/server/live-build.d.ts +70 -0
  206. package/dist/server/live-build.d.ts.map +1 -0
  207. package/dist/server/live-build.js +222 -0
  208. package/dist/server/live-build.js.map +1 -0
  209. package/dist/server/live-dev.d.ts +62 -0
  210. package/dist/server/live-dev.d.ts.map +1 -0
  211. package/dist/server/live-dev.js +313 -0
  212. package/dist/server/live-dev.js.map +1 -0
  213. package/dist/server/live-manifest.d.ts +23 -0
  214. package/dist/server/live-manifest.d.ts.map +1 -0
  215. package/dist/server/live-manifest.js +7 -0
  216. package/dist/server/live-manifest.js.map +1 -0
  217. package/dist/server/live-prod.d.ts +30 -0
  218. package/dist/server/live-prod.d.ts.map +1 -0
  219. package/dist/server/live-prod.js +97 -0
  220. package/dist/server/live-prod.js.map +1 -0
  221. package/dist/server/live-routes.d.ts +72 -0
  222. package/dist/server/live-routes.d.ts.map +1 -0
  223. package/dist/server/live-routes.js +274 -0
  224. package/dist/server/live-routes.js.map +1 -0
  225. package/dist/server/live-web.d.ts +16 -0
  226. package/dist/server/live-web.d.ts.map +1 -0
  227. package/dist/server/live-web.js +159 -0
  228. package/dist/server/live-web.js.map +1 -0
  229. package/dist/server/live.d.ts +138 -0
  230. package/dist/server/live.d.ts.map +1 -0
  231. package/dist/server/live.js +542 -0
  232. package/dist/server/live.js.map +1 -0
  233. package/dist/server/middleware-node.d.ts +48 -0
  234. package/dist/server/middleware-node.d.ts.map +1 -0
  235. package/dist/server/middleware-node.js +167 -0
  236. package/dist/server/middleware-node.js.map +1 -0
  237. package/dist/server/middleware-runtime.d.ts +97 -0
  238. package/dist/server/middleware-runtime.d.ts.map +1 -0
  239. package/dist/server/middleware-runtime.js +447 -0
  240. package/dist/server/middleware-runtime.js.map +1 -0
  241. package/dist/server/middleware.d.ts +145 -0
  242. package/dist/server/middleware.d.ts.map +1 -0
  243. package/dist/server/middleware.js +365 -0
  244. package/dist/server/middleware.js.map +1 -0
  245. package/dist/server/ppr.d.ts +22 -0
  246. package/dist/server/ppr.d.ts.map +1 -0
  247. package/dist/server/ppr.js +58 -0
  248. package/dist/server/ppr.js.map +1 -0
  249. package/dist/server/scan.d.ts +48 -0
  250. package/dist/server/scan.d.ts.map +1 -0
  251. package/dist/server/scan.js +161 -0
  252. package/dist/server/scan.js.map +1 -0
  253. package/dist/server/server-components.d.ts +65 -0
  254. package/dist/server/server-components.d.ts.map +1 -0
  255. package/dist/server/server-components.js +215 -0
  256. package/dist/server/server-components.js.map +1 -0
  257. package/dist/terminal.d.ts +5 -0
  258. package/dist/terminal.d.ts.map +1 -0
  259. package/dist/terminal.js +5 -0
  260. package/dist/terminal.js.map +1 -0
  261. package/dist/testing.d.ts +5 -0
  262. package/dist/testing.d.ts.map +1 -0
  263. package/dist/testing.js +5 -0
  264. package/dist/testing.js.map +1 -0
  265. package/dist/vite-plugin.d.ts +51 -0
  266. package/dist/vite-plugin.d.ts.map +1 -0
  267. package/dist/vite-plugin.js +403 -0
  268. package/dist/vite-plugin.js.map +1 -0
  269. package/package.json +130 -4
  270. package/src/cli/build.ts +182 -0
  271. package/src/cli/config.ts +72 -0
  272. package/src/cli/dev.ts +54 -0
  273. package/src/cli/index.ts +185 -0
  274. package/src/cli/run.ts +124 -0
  275. package/src/cli/start.ts +194 -0
  276. package/src/cli/ui.ts +93 -0
  277. package/src/client.tsx +122 -0
  278. package/src/config.ts +155 -0
  279. package/src/context.ts +69 -0
  280. package/src/dev-errors.ts +167 -0
  281. package/src/devtools.tsx +419 -0
  282. package/src/frame-mirror.ts +145 -0
  283. package/src/frame-protocol.ts +435 -0
  284. package/src/history.ts +201 -0
  285. package/src/index.ts +9 -0
  286. package/src/link.tsx +108 -0
  287. package/src/live-player.ts +449 -0
  288. package/src/live-protocol.ts +317 -0
  289. package/src/live-stub.tsx +32 -0
  290. package/src/mcp/cli.ts +188 -0
  291. package/src/mcp/frame.ts +224 -0
  292. package/src/mcp/index.ts +15 -0
  293. package/src/mcp/inspect.ts +445 -0
  294. package/src/mcp/keys.ts +86 -0
  295. package/src/mcp/logs.ts +225 -0
  296. package/src/mcp/server.ts +595 -0
  297. package/src/mcp/session.ts +821 -0
  298. package/src/navigation-errors.ts +60 -0
  299. package/src/navigation.ts +44 -0
  300. package/src/router.tsx +497 -0
  301. package/src/routing.ts +344 -0
  302. package/src/server/adapters/build-step.ts +135 -0
  303. package/src/server/adapters/bun.ts +92 -0
  304. package/src/server/adapters/bundle.ts +97 -0
  305. package/src/server/adapters/cloudflare.ts +111 -0
  306. package/src/server/adapters/index.ts +29 -0
  307. package/src/server/adapters/runtime-node.ts +34 -0
  308. package/src/server/adapters/runtime.ts +145 -0
  309. package/src/server/adapters/shared.ts +159 -0
  310. package/src/server/adapters/static.ts +68 -0
  311. package/src/server/adapters/vercel.ts +149 -0
  312. package/src/server/bun-jsx.ts +65 -0
  313. package/src/server/cache-route.ts +139 -0
  314. package/src/server/cache.ts +498 -0
  315. package/src/server/frames.ts +326 -0
  316. package/src/server/http.ts +467 -0
  317. package/src/server/jsx-dev-runtime.ts +9 -0
  318. package/src/server/jsx-runtime.ts +13 -0
  319. package/src/server/live-app.tsx +374 -0
  320. package/src/server/live-build.ts +258 -0
  321. package/src/server/live-dev.ts +357 -0
  322. package/src/server/live-manifest.ts +25 -0
  323. package/src/server/live-prod.ts +120 -0
  324. package/src/server/live-routes.ts +298 -0
  325. package/src/server/live-web.ts +170 -0
  326. package/src/server/live.ts +551 -0
  327. package/src/server/middleware-node.ts +168 -0
  328. package/src/server/middleware-runtime.ts +478 -0
  329. package/src/server/middleware.ts +451 -0
  330. package/src/server/ppr.ts +62 -0
  331. package/src/server/scan.ts +190 -0
  332. package/src/server/server-components.ts +224 -0
  333. package/src/terminal.ts +5 -0
  334. package/src/testing.ts +5 -0
  335. package/src/vite-plugin.ts +413 -0
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Build-time knowledge of server-rendered routes: which pages opt in with
3
+ * `export const runtime = "server"`, the virtual modules that render them, and
4
+ * the HTML document that boots the frame player.
5
+ */
6
+
7
+ import { readFileSync, statSync } from "node:fs";
8
+ import { createRequire } from "node:module";
9
+ import { dirname, join, resolve } from "node:path";
10
+ import { parseAst } from "vite";
11
+ import { BRAND_MARK, ENGINE_NAME, VERSION } from "@textjs/core";
12
+ import { matchAppRoutes, type RouteManifest } from "../routing";
13
+ import { generateRoutesModule, listPages, toStaticManifest, type AppScan, type ScannedSegment } from "./scan";
14
+
15
+ export const VIRTUAL_LIVE = "virtual:textjs/live";
16
+ export const VIRTUAL_PLAYER = "virtual:textjs/player";
17
+ /** Script `src` of the frame player in `text dev`. */
18
+ export const PLAYER_URL = "/@textjs/player";
19
+
20
+
21
+ /**
22
+ * Framework modules are imported by package specifier (not file path) so the
23
+ * virtual modules resolve them exactly like user code does, sharing one
24
+ * instance of the router context in the SSR graph.
25
+ */
26
+ export const LIVE_APP_MODULE = "@textjs/app/live-app";
27
+ export const LIVE_PLAYER_MODULE = "@textjs/app/live-player";
28
+ export const LIVE_STUB_MODULE = "@textjs/app/live-stub";
29
+ /** Suffix marking a server page import in the client manifest; the plugin loads the stub instead. */
30
+ export const SERVER_STUB_QUERY = "?textjs-server-stub";
31
+ /**
32
+ * Suffix for layouts / loading / error / not-found files of segments that only
33
+ * contain server pages: the browser never renders them, so they (and what they
34
+ * import) stay out of the client bundle.
35
+ */
36
+ export const SERVER_ONLY_QUERY = "?textjs-server-only";
37
+ export const JSX_RUNTIME_MODULE = "@textjs/app/server/jsx-runtime";
38
+ export const JSX_DEV_RUNTIME_MODULE = "@textjs/app/server/jsx-dev-runtime";
39
+
40
+ type EsbuildTransformSync = (code: string, options: { loader: string; format: string; jsx: string; sourcefile: string }) => { code: string };
41
+ let transformSync: EsbuildTransformSync | null = null;
42
+
43
+ /** esbuild's `transformSync` (a dependency of Vite, which Text.js depends on). */
44
+ function esbuild(): EsbuildTransformSync {
45
+ if (transformSync === null) {
46
+ const require = createRequire(import.meta.url);
47
+ const fromVite = createRequire(require.resolve("vite/package.json"));
48
+ transformSync = (fromVite("esbuild") as { transformSync: EsbuildTransformSync }).transformSync;
49
+ }
50
+ return transformSync;
51
+ }
52
+
53
+ const LOADERS: Record<string, string> = { ".ts": "ts", ".mts": "ts", ".cts": "ts", ".tsx": "tsx", ".js": "jsx", ".mjs": "js", ".cjs": "js", ".jsx": "jsx" };
54
+
55
+ interface AstNode {
56
+ type: string;
57
+ [key: string]: unknown;
58
+ }
59
+
60
+ function stringValue(node: unknown): string | undefined {
61
+ const n = node as AstNode | null;
62
+ if (!n) return undefined;
63
+ if (n.type === "Literal" && typeof n.value === "string") return n.value;
64
+ if (n.type === "TemplateLiteral" && (n.expressions as unknown[]).length === 0) return ((n.quasis as AstNode[])[0]?.value as { cooked?: string })?.cooked;
65
+ return undefined;
66
+ }
67
+
68
+ function exportName(node: unknown): string | undefined {
69
+ const n = node as AstNode;
70
+ return n.type === "Identifier" ? (n.name as string) : stringValue(n);
71
+ }
72
+
73
+ const runtimeCache = new Map<string, { mtime: number; value: string | undefined }>();
74
+
75
+ /**
76
+ * The value of a module's `runtime` export when it is a static string, read
77
+ * with a real parser: `export const runtime = "server"`, with a type
78
+ * annotation or `as const`, a template literal, `export { runtime }`,
79
+ * `export { x as runtime }` and re-exports (`export { runtime } from "./x"`).
80
+ * Comments and strings never match.
81
+ */
82
+ export function readRuntimeExport(file: string, depth = 0): string | undefined {
83
+ let mtime: number;
84
+ let code: string;
85
+ try {
86
+ mtime = statSync(file).mtimeMs;
87
+ const hit = runtimeCache.get(file);
88
+ if (hit && hit.mtime === mtime) return hit.value;
89
+ code = readFileSync(file, "utf8");
90
+ } catch {
91
+ return undefined;
92
+ }
93
+ let value: string | undefined;
94
+ if (/\bruntime\b/.test(code)) {
95
+ try {
96
+ const ext = /\.[^.\\/]+$/.exec(file)?.[0] ?? ".ts";
97
+ const js = esbuild()(code, { loader: LOADERS[ext] ?? "tsx", format: "esm", jsx: "automatic", sourcefile: file }).code;
98
+ value = exportedString(parseAst(js) as unknown as AstNode, file, "runtime", depth);
99
+ } catch {
100
+ value = undefined;
101
+ }
102
+ }
103
+ runtimeCache.set(file, { mtime, value });
104
+ return value;
105
+ }
106
+
107
+ /** The static string value of export `name` in a (transpiled) module, following relative re-exports. */
108
+ function exportedString(program: AstNode, file: string, name: string, depth: number): string | undefined {
109
+ const body = program.body as AstNode[];
110
+ const constants = new Map<string, string | undefined>();
111
+ // esbuild turns `export { runtime } from "./x"` into an import plus a local export.
112
+ const imports = new Map<string, { source: string; name: string }>();
113
+ for (const node of body) {
114
+ if (node.type !== "ImportDeclaration") continue;
115
+ for (const spec of node.specifiers as AstNode[]) {
116
+ if (spec.type === "ImportSpecifier") imports.set((spec.local as AstNode).name as string, { source: stringValue(node.source) as string, name: exportName(spec.imported) as string });
117
+ }
118
+ }
119
+ const follow = (source: string, name: string): string | undefined => {
120
+ if (depth >= 8 || !source.startsWith(".")) return undefined;
121
+ const target = resolveRelative(dirname(file), source);
122
+ return target ? readRuntimeExportNamed(target, name, depth + 1) : undefined;
123
+ };
124
+ for (const node of body) {
125
+ const decl = (node.type === "ExportNamedDeclaration" ? node.declaration : node) as AstNode | null;
126
+ if (decl?.type !== "VariableDeclaration") continue;
127
+ for (const d of decl.declarations as AstNode[]) {
128
+ const id = d.id as AstNode;
129
+ if (id.type === "Identifier") constants.set(id.name as string, decl.kind === "const" ? stringValue(d.init) : undefined);
130
+ }
131
+ }
132
+ for (const node of body) {
133
+ if (node.type !== "ExportNamedDeclaration") continue;
134
+ const decl = node.declaration as AstNode | null;
135
+ if (decl?.type === "VariableDeclaration") {
136
+ for (const d of decl.declarations as AstNode[]) if ((d.id as AstNode).type === "Identifier" && (d.id as AstNode).name === name) return constants.get(name);
137
+ continue;
138
+ }
139
+ for (const spec of (node.specifiers as AstNode[] | undefined) ?? []) {
140
+ if (exportName(spec.exported) !== name) continue;
141
+ const local = exportName(spec.local) as string;
142
+ const source = stringValue(node.source);
143
+ if (source !== undefined) return follow(source, local);
144
+ const imported = imports.get(local);
145
+ return imported ? follow(imported.source, imported.name) : constants.get(local);
146
+ }
147
+ }
148
+ return undefined;
149
+ }
150
+
151
+ function readRuntimeExportNamed(file: string, name: string, depth: number): string | undefined {
152
+ if (name === "runtime") return readRuntimeExport(file, depth);
153
+ try {
154
+ const ext = /\.[^.\\/]+$/.exec(file)?.[0] ?? ".ts";
155
+ const js = esbuild()(readFileSync(file, "utf8"), { loader: LOADERS[ext] ?? "tsx", format: "esm", jsx: "automatic", sourcefile: file }).code;
156
+ return exportedString(parseAst(js) as unknown as AstNode, file, name, depth);
157
+ } catch {
158
+ return undefined;
159
+ }
160
+ }
161
+
162
+ function resolveRelative(dir: string, spec: string): string | null {
163
+ const base = resolve(dir, spec);
164
+ for (const candidate of [base, ...Object.keys(LOADERS).map((e) => base + e), ...Object.keys(LOADERS).map((e) => join(base, `index${e}`))]) {
165
+ try {
166
+ if (statSync(candidate).isFile()) return candidate;
167
+ } catch {
168
+ // try the next
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+
174
+ export function isServerRuntimeFile(file: string | undefined): boolean {
175
+ return file ? readRuntimeExport(file) === "server" : false;
176
+ }
177
+
178
+ export interface ServerPage {
179
+ pattern: string;
180
+ /** Project-relative page file. */
181
+ file: string;
182
+ dynamic: boolean;
183
+ }
184
+
185
+ /** Pages that render on the server. */
186
+ export function listServerPages(scan: AppScan): ServerPage[] {
187
+ return listPages(scan)
188
+ .filter((p) => p.file !== undefined && isServerRuntimeFile(join(scan.root, p.file)))
189
+ .map((p) => ({ pattern: p.pattern, file: p.file as string, dynamic: p.dynamic }));
190
+ }
191
+
192
+ /** Route matcher for HTML requests: does `pathname` resolve to a server page? */
193
+ export function createRuntimeMatcher(scan: AppScan): (pathname: string) => "server" | "client" {
194
+ const manifest: RouteManifest = toStaticManifest(scan);
195
+ const server = new Set(listServerPages(scan).map((p) => p.file));
196
+ return (pathname) => {
197
+ if (server.size === 0) return "client";
198
+ const m = matchAppRoutes(manifest, pathname);
199
+ const file = m?.chain[m.chain.length - 1]?.files?.page;
200
+ return file !== undefined && server.has(file) ? "server" : "client";
201
+ };
202
+ }
203
+
204
+ /**
205
+ * The scan with every server page swapped for the client stub, so the browser
206
+ * bundle never contains server route modules (or anything they import).
207
+ */
208
+ export function withServerStubs(scan: AppScan): AppScan {
209
+ const hasClientPage = (s: ScannedSegment): boolean => (s.files.page !== undefined && !isServerRuntimeFile(s.files.page)) || s.children.some(hasClientPage);
210
+ const swap = (s: ScannedSegment): ScannedSegment => {
211
+ const files: ScannedSegment["files"] = { ...s.files };
212
+ const page = s.files.page;
213
+ if (page && isServerRuntimeFile(page)) files.page = `${page}${SERVER_STUB_QUERY}`;
214
+ // Nothing under this segment renders in the browser: keep its boundaries and layout out of the bundle too.
215
+ if (!hasClientPage(s)) {
216
+ for (const kind of ["layout", "loading", "error", "not-found"] as const) {
217
+ const f = s.files[kind];
218
+ if (f) files[kind] = `${f}${SERVER_ONLY_QUERY}`;
219
+ }
220
+ }
221
+ return { ...s, files, children: s.children.map(swap) };
222
+ };
223
+ return { ...scan, tree: swap(scan.tree) };
224
+ }
225
+
226
+ /** `virtual:textjs/live`: the SSR entry rendering live sessions and PPR shells. */
227
+ export function generateLiveEntryModule(scan: AppScan): string {
228
+ return `${generateRoutesModule(scan)}
229
+ export { createLiveApp, isServerPage, prerenderShell, routeRuntime } from ${JSON.stringify(LIVE_APP_MODULE)};
230
+ `;
231
+ }
232
+
233
+ export function generatePlayerEntryModule(): string {
234
+ return `import { bootFramePlayer } from ${JSON.stringify(LIVE_PLAYER_MODULE)};
235
+ bootFramePlayer();
236
+ `;
237
+ }
238
+
239
+ function escapeHtml(s: string): string {
240
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c] as string);
241
+ }
242
+
243
+ /** JSON safe to inline in a `<script>` element. */
244
+ export function inlineJson(value: unknown): string {
245
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
246
+ }
247
+
248
+ const FAVICON = `data:image/svg+xml,${encodeURIComponent(
249
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#0a0a0a"/><path d="M7 10h18L16 25z" fill="#ededed"/></svg>`,
250
+ )}`;
251
+
252
+ export interface LiveHtmlOptions {
253
+ title: string;
254
+ playerSrc: string;
255
+ basePath: string;
256
+ viewport?: { width?: number; height?: number; responsive?: boolean };
257
+ /** Inline PPR shell `<script>` (painted before the player loads). */
258
+ shell?: string;
259
+ }
260
+
261
+ /** The document for a server-rendered route: a static page pad plus the frame player. */
262
+ export function createLiveHtml(o: LiveHtmlOptions): string {
263
+ const config = { basePath: o.basePath, viewport: o.viewport ?? {}, title: o.title };
264
+ return `<!doctype html>
265
+ <html lang="en">
266
+ <head>
267
+ <meta charset="utf-8" />
268
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
269
+ <meta name="generator" content="Text.js ${VERSION} (${ENGINE_NAME})" />
270
+ <meta name="color-scheme" content="dark" />
271
+ <title>${escapeHtml(o.title)}</title>
272
+ <link rel="icon" href="${FAVICON}" />
273
+ <style>
274
+ html, body { margin: 0; min-height: 100%; background: #0a0a0a; color: #ededed; }
275
+ body { min-height: 100vh; display: flex; align-items: center; justify-content: center;
276
+ font-family: Geist, Inter, system-ui, -apple-system, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; }
277
+ #__textjs { text-align: center; line-height: 1.6; padding: 24px; }
278
+ #__textjs .mark { font-size: 28px; margin: 0 0 8px; }
279
+ #__textjs .title { font-size: 18px; font-weight: 600; margin: 0; }
280
+ #__textjs .hint { color: #a1a1a1; margin: 8px 0 0; }
281
+ #__textjs kbd { font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 12px; border: 1px solid #333; border-radius: 4px; padding: 1px 5px; color: #ededed; }
282
+ #__textjs .status { color: #666; font-size: 12px; margin: 16px 0 0; font-family: "SF Mono", Menlo, Consolas, monospace; }
283
+ </style>
284
+ </head>
285
+ <body>
286
+ <div id="__textjs" data-textjs-runtime="server">
287
+ <p class="mark">${BRAND_MARK}</p>
288
+ <p class="title">${escapeHtml(o.title)}</p>
289
+ <p class="hint">Open the console: <kbd>⌥⌘J</kbd> on macOS, <kbd>Ctrl+Shift+J</kbd> elsewhere. Then click here and type.</p>
290
+ <p class="status">Rendered on the server · <span data-textjs-status>connecting</span></p>
291
+ </div>
292
+ <noscript>${BRAND_MARK} Text.js apps render into console.log, which requires JavaScript.</noscript>
293
+ <script type="application/json" id="__textjs-live">${inlineJson(config)}</script>${o.shell ? `\n ${o.shell}` : ""}
294
+ <script type="module" src="${escapeHtml(o.playerSrc)}"></script>
295
+ </body>
296
+ </html>
297
+ `;
298
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Run a Node-style `(req, res)` handler (the live session server) inside a
3
+ * Web-standard `(Request) => Response` server such as `Bun.serve`: the
4
+ * response streams as soon as the handler flushes headers or writes, and the
5
+ * handler sees `close` when the client goes away.
6
+ */
7
+
8
+ import { EventEmitter } from "node:events";
9
+ import type { IncomingMessage, ServerResponse } from "node:http";
10
+
11
+ /** Read by `readRequestBody()` (see `server/http.ts`). */
12
+ const WEB_REQUEST = Symbol.for("textjs.webRequest");
13
+
14
+ type NodeStyleHandler = (req: IncomingMessage, res: ServerResponse) => Promise<boolean> | boolean;
15
+
16
+ class RequestShim extends EventEmitter {
17
+ readonly method: string;
18
+ readonly url: string;
19
+ readonly headers: Record<string, string> = {};
20
+ destroyed = false;
21
+ readonly socket: { remoteAddress?: string };
22
+
23
+ constructor(request: Request, url: string, remoteAddress?: string) {
24
+ super();
25
+ this.socket = { remoteAddress };
26
+ this.method = request.method.toUpperCase();
27
+ this.url = url;
28
+ request.headers.forEach((value, key) => {
29
+ this.headers[key] = value;
30
+ });
31
+ (this as unknown as Record<symbol, Request>)[WEB_REQUEST] = request;
32
+ }
33
+
34
+ setEncoding(): this {
35
+ return this;
36
+ }
37
+
38
+ destroy(): this {
39
+ this.destroyed = true;
40
+ return this;
41
+ }
42
+ }
43
+
44
+ class ResponseShim extends EventEmitter {
45
+ statusCode = 200;
46
+ statusMessage = "";
47
+ headersSent = false;
48
+ writableEnded = false;
49
+ destroyed = false;
50
+ readonly ready: Promise<Response>;
51
+ readonly #headers = new Headers();
52
+ #controller: ReadableStreamDefaultController<Uint8Array> | null = null;
53
+ #resolve!: (response: Response) => void;
54
+ #pendingDrain = false;
55
+ readonly #encoder = new TextEncoder();
56
+
57
+ constructor(signal: AbortSignal) {
58
+ super();
59
+ this.ready = new Promise((r) => {
60
+ this.#resolve = r;
61
+ });
62
+ signal.addEventListener("abort", () => this.#closed(), { once: true });
63
+ }
64
+
65
+ setHeader(name: string, value: number | string | readonly string[]): this {
66
+ this.#headers.delete(name);
67
+ for (const v of Array.isArray(value) ? value : [String(value)]) this.#headers.append(name, v);
68
+ return this;
69
+ }
70
+
71
+ getHeader(name: string): string | undefined {
72
+ return this.#headers.get(name) ?? undefined;
73
+ }
74
+
75
+ hasHeader(name: string): boolean {
76
+ return this.#headers.has(name);
77
+ }
78
+
79
+ removeHeader(name: string): void {
80
+ this.#headers.delete(name);
81
+ }
82
+
83
+ flushHeaders(): void {
84
+ this.#start();
85
+ }
86
+
87
+ write(chunk: string | Uint8Array): boolean {
88
+ if (this.writableEnded || this.destroyed) return false;
89
+ this.#start();
90
+ const controller = this.#controller;
91
+ if (!controller) return false;
92
+ try {
93
+ controller.enqueue(typeof chunk === "string" ? this.#encoder.encode(chunk) : chunk);
94
+ } catch {
95
+ this.#closed();
96
+ return false;
97
+ }
98
+ if ((controller.desiredSize ?? 1) > 0) return true;
99
+ this.#pendingDrain = true;
100
+ return false;
101
+ }
102
+
103
+ end(chunk?: string | Uint8Array): this {
104
+ if (this.writableEnded) return this;
105
+ if (!this.headersSent) {
106
+ // Nothing streamed yet: answer with a complete body.
107
+ this.writableEnded = true;
108
+ this.headersSent = true;
109
+ const body = chunk === undefined ? null : typeof chunk === "string" ? chunk : (chunk as Uint8Array<ArrayBuffer>);
110
+ this.#resolve(new Response(this.#bodyless() ? null : body, { status: this.statusCode, headers: this.#headers }));
111
+ queueMicrotask(() => this.#closed());
112
+ return this;
113
+ }
114
+ if (chunk !== undefined) this.write(chunk);
115
+ this.writableEnded = true;
116
+ try {
117
+ this.#controller?.close();
118
+ } catch {
119
+ // already closed by the client
120
+ }
121
+ queueMicrotask(() => this.#closed());
122
+ return this;
123
+ }
124
+
125
+ #bodyless(): boolean {
126
+ return this.statusCode === 204 || this.statusCode === 304;
127
+ }
128
+
129
+ #start(): void {
130
+ if (this.headersSent) return;
131
+ this.headersSent = true;
132
+ const stream = new ReadableStream<Uint8Array>(
133
+ {
134
+ start: (controller) => {
135
+ this.#controller = controller;
136
+ },
137
+ pull: () => {
138
+ if (!this.#pendingDrain) return;
139
+ this.#pendingDrain = false;
140
+ this.emit("drain");
141
+ },
142
+ cancel: () => this.#closed(),
143
+ },
144
+ { highWaterMark: 64 * 1024, size: (chunk) => chunk?.byteLength ?? 0 },
145
+ );
146
+ this.#resolve(new Response(stream, { status: this.statusCode, headers: this.#headers }));
147
+ }
148
+
149
+ #closed(): void {
150
+ if (this.destroyed) return;
151
+ this.destroyed = true;
152
+ this.writableEnded = true;
153
+ this.emit("close");
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Call `handler` with Node-compatible request / response objects. Resolves to
159
+ * the response, or `null` when the handler declined the request (`false`)
160
+ * without writing anything.
161
+ */
162
+ export async function runNodeStyle(request: Request, url: string, handler: NodeStyleHandler, remoteAddress?: string): Promise<Response | null> {
163
+ const req = new RequestShim(request, url, remoteAddress);
164
+ const res = new ResponseShim(request.signal);
165
+ const handled = Promise.resolve(handler(req as unknown as IncomingMessage, res as unknown as ServerResponse));
166
+ const first = await Promise.race([res.ready.then((r) => ({ response: r })), handled.then((ok) => ({ ok }))]);
167
+ if ("response" in first) return first.response;
168
+ if (!first.ok && !res.headersSent) return null;
169
+ return res.ready;
170
+ }