@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.
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,182 @@
1
+ import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { join, relative, resolve } from "node:path";
3
+ import { gzipSync } from "node:zlib";
4
+ import { build, mergeConfig, type InlineConfig } from "vite";
5
+ import { ENGINE_NAME, VERSION } from "@textjs/core";
6
+ import { normalizeBasePath } from "../config";
7
+ import { listApiRoutes, listPages, scanApp } from "../server/scan";
8
+ import { buildLiveRoutes } from "../server/live-build";
9
+ import { listServerPages } from "../server/live-routes";
10
+ import { textjsVitePlugin, VIRTUAL_API } from "../vite-plugin";
11
+ import { buildServerRuntime, resolveAdapter, runBuildAdapter, serverFiles } from "../server/adapters/build-step";
12
+ import type { AdapterResult } from "../server/adapters/shared";
13
+ import { loadTextConfig } from "./config";
14
+ import { brand, color, fail, formatBytes, LEGEND, ok, routeTable, SERVER_LEGEND, summarize, type SummaryRoute } from "./ui";
15
+
16
+ export interface BuildOptions {
17
+ root: string;
18
+ /** Deploy adapter (`--adapter`); defaults to `TEXTJS_ADAPTER`, or Vercel / Cloudflare Pages when building there. */
19
+ adapter?: string;
20
+ /** Print the summary (default `true`). */
21
+ log?: boolean;
22
+ }
23
+
24
+ export interface BuildManifest {
25
+ version: string;
26
+ engine: string;
27
+ basePath: string;
28
+ routes: Array<{ pattern: string; type: "static" | "dynamic"; file?: string }>;
29
+ api: Array<{ pattern: string }>;
30
+ firstLoad: { bytes: number; gzip: number };
31
+ }
32
+
33
+ export interface BuildResult {
34
+ outDir: string;
35
+ routes: SummaryRoute[];
36
+ manifest: BuildManifest;
37
+ output: string;
38
+ durationMs: number;
39
+ /** Deploy output written by `--adapter`. */
40
+ adapter?: AdapterResult;
41
+ }
42
+
43
+ /** `.text/` inside the output directory holds build metadata and the API server bundle. */
44
+ export const META_DIR = ".text";
45
+
46
+ function jsFiles(dir: string): string[] {
47
+ const out: string[] = [];
48
+ const walk = (d: string): void => {
49
+ for (const name of readdirSync(d)) {
50
+ const p = join(d, name);
51
+ if (name === META_DIR) continue;
52
+ if (statSync(p).isDirectory()) walk(p);
53
+ else if (name.endsWith(".js")) out.push(p);
54
+ }
55
+ };
56
+ walk(dir);
57
+ return out;
58
+ }
59
+
60
+ export async function buildApp(opts: BuildOptions): Promise<BuildResult> {
61
+ const log = opts.log ?? true;
62
+ const lines: string[] = [];
63
+ const print = (s = ""): void => {
64
+ lines.push(s);
65
+ if (log) console.log(s);
66
+ };
67
+ const started = performance.now();
68
+ const { config } = await loadTextConfig(opts.root, "build");
69
+ const outDir = resolve(opts.root, config.outDir ?? "dist");
70
+
71
+ print(` ${brand()}`);
72
+ print();
73
+ print(" Creating an optimized console production build...");
74
+ print();
75
+
76
+ const base: InlineConfig = {
77
+ configFile: false,
78
+ root: opts.root,
79
+ mode: "production",
80
+ logLevel: "warn",
81
+ clearScreen: false,
82
+ build: { outDir, emptyOutDir: true },
83
+ plugins: [textjsVitePlugin({ ...config, root: opts.root })],
84
+ };
85
+ const scan = scanApp(opts.root, config.appDir ?? "app");
86
+ let adapterName: ReturnType<typeof resolveAdapter>;
87
+ try {
88
+ adapterName = resolveAdapter(opts.adapter);
89
+ } catch (error) {
90
+ print(fail(error instanceof Error ? error.message : String(error)));
91
+ throw error;
92
+ }
93
+ let hasMiddleware = false;
94
+ const api = listApiRoutes(scan);
95
+ try {
96
+ await build(config.vite ? mergeConfig(base, config.vite) : base);
97
+ if (api.length > 0) await buildApiServer();
98
+ hasMiddleware = (await buildServerRuntime(opts.root, config, outDir)) && serverFiles(opts.root, config).middlewareFile !== null;
99
+ } catch (error) {
100
+ print(fail(color.red("Failed to compile.")));
101
+ print();
102
+ print(error instanceof Error ? (error.stack ?? error.message) : String(error));
103
+ throw error;
104
+ }
105
+
106
+ async function buildApiServer(): Promise<void> {
107
+ const serverBuild: InlineConfig = {
108
+ configFile: false,
109
+ root: opts.root,
110
+ mode: "production",
111
+ logLevel: "warn",
112
+ clearScreen: false,
113
+ build: {
114
+ ssr: VIRTUAL_API,
115
+ outDir: join(outDir, META_DIR, "server"),
116
+ emptyOutDir: true,
117
+ target: "node18",
118
+ rollupOptions: { output: { entryFileNames: "api.mjs", format: "es" } },
119
+ },
120
+ // Bundle the framework into api.mjs so `text start` runs on plain Node.
121
+ ssr: { noExternal: ["@textjs/app", /^@textjs\//] },
122
+ plugins: [textjsVitePlugin({ ...config, root: opts.root })],
123
+ };
124
+ await build(config.vite ? mergeConfig(serverBuild, config.vite) : serverBuild);
125
+ }
126
+
127
+ const compiledMs = performance.now() - started;
128
+ print(ok(`Compiled successfully in ${(compiledMs / 1000).toFixed(1)}s`));
129
+
130
+ const pages = listPages(scan);
131
+ print(ok("Collecting route data"));
132
+ print(ok(`Generating static console cells (${pages.filter((p) => !p.dynamic).length}/${pages.length})`));
133
+
134
+ let bytes = 0;
135
+ let gzip = 0;
136
+ for (const f of jsFiles(outDir)) {
137
+ const buf = readFileSync(f);
138
+ bytes += buf.length;
139
+ gzip += gzipSync(buf).length;
140
+ }
141
+ // Partial Prerendering shells + server-rendered routes (after measuring the client bundle).
142
+ const live = await buildLiveRoutes({ root: opts.root, config, outDir, scan, plugins: [textjsVitePlugin({ ...config, root: opts.root })] });
143
+ if (live) {
144
+ const player = live.player ? `, frame player ${formatBytes(live.player.bytes)} (${formatBytes(live.player.gzip)} gzip)` : "";
145
+ print(ok(`Prerendering console shells (${live.shells}/${live.staticRoutes})${live.manifest.serverRoutes.length > 0 ? ` · ${live.manifest.serverRoutes.length} server-rendered` : ""}${player}`));
146
+ for (const s of live.skipped) print(color.gray(` ${s.pattern} streams without a shell: ${s.reason.split("\n")[0]}`));
147
+ }
148
+ const routes = summarize(pages, api, new Set(listServerPages(scan).map((p) => p.pattern)));
149
+ const manifest: BuildManifest = {
150
+ version: VERSION,
151
+ engine: ENGINE_NAME,
152
+ basePath: normalizeBasePath(config.basePath),
153
+ routes: pages.map((p) => ({ pattern: p.pattern, type: p.dynamic ? "dynamic" : "static", file: p.file })),
154
+ api: api.map((a) => ({ pattern: a.pattern })),
155
+ firstLoad: { bytes, gzip },
156
+ };
157
+ mkdirSync(join(outDir, META_DIR), { recursive: true });
158
+ writeFileSync(join(outDir, META_DIR, "routes-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
159
+ print(ok("Finalizing console optimization"));
160
+ print();
161
+ print(routeTable(routes));
162
+ print(`+ First Load JS shared by all${" ".repeat(13)}${formatBytes(bytes)} ${color.gray(`(${formatBytes(gzip)} gzip)`)}`);
163
+ if (hasMiddleware) print(`ƒ Middleware`);
164
+ print();
165
+ print(routes.some((r) => r.server) ? `${LEGEND}\n${SERVER_LEGEND}` : LEGEND);
166
+ print();
167
+ let adapter: AdapterResult | undefined;
168
+ if (adapterName) {
169
+ try {
170
+ adapter = await runBuildAdapter({ root: opts.root, config, outDir, scan, api, adapter: adapterName, print });
171
+ } catch (error) {
172
+ print(fail(color.red(`Adapter ${adapterName} failed.`)));
173
+ print();
174
+ print(error instanceof Error ? error.message : String(error));
175
+ throw error;
176
+ }
177
+ print(ok(`Adapter ${color.bold(adapter.adapter)} → ${relative(opts.root, adapter.output) || "."}`));
178
+ for (const note of adapter.notes) print(color.gray(` ${note}`));
179
+ print();
180
+ }
181
+ return { outDir, routes, manifest, output: lines.join("\n"), durationMs: performance.now() - started, adapter };
182
+ }
@@ -0,0 +1,72 @@
1
+ import { existsSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import { loadConfigFromFile } from "vite";
4
+ import { txWarn } from "@textjs/core";
5
+ import type { TextConfig } from "../config";
6
+
7
+ /** Every `TextConfig` key; the `Record` type fails typecheck when a new option is not listed. */
8
+ const CONFIG_KEYS: Record<keyof TextConfig, true> = {
9
+ viewport: true,
10
+ devIndicators: true,
11
+ showMirrorDefault: true,
12
+ outDir: true,
13
+ appDir: true,
14
+ basePath: true,
15
+ title: true,
16
+ hints: true,
17
+ action: true,
18
+ proxy: true,
19
+ rewrites: true,
20
+ configureServer: true,
21
+ vite: true,
22
+ cacheHandler: true,
23
+ ppr: true,
24
+ live: true,
25
+ server: true,
26
+ };
27
+
28
+ /** Warn (once each) about keys Text.js does not read. */
29
+ export function warnUnknownConfigKeys(config: object, file: string): void {
30
+ for (const key of Object.keys(config)) if (!(key in CONFIG_KEYS)) txWarn("TX029", JSON.stringify(key), basename(file));
31
+ }
32
+
33
+ export const CONFIG_FILES = ["text.config.ts", "text.config.mts", "text.config.js", "text.config.mjs"] as const;
34
+
35
+ export function findConfigFile(root: string): string | null {
36
+ for (const name of CONFIG_FILES) {
37
+ const p = join(root, name);
38
+ if (existsSync(p)) return p;
39
+ }
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Run `fn` and put `process.env.NODE_ENV` back afterwards. Vite's module
45
+ * runner (the Node config loader) sets an unset `NODE_ENV` to
46
+ * `"development"`, which would make the following `vite build` bundle
47
+ * React's development builds.
48
+ */
49
+ export async function preservingNodeEnv<T>(fn: () => Promise<T>): Promise<T> {
50
+ const before = process.env.NODE_ENV;
51
+ try {
52
+ return await fn();
53
+ } finally {
54
+ if (before === undefined) delete process.env.NODE_ENV;
55
+ else process.env.NODE_ENV = before;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Load `text.config.ts`. Bun runs TypeScript dependencies natively, so the
61
+ * config is bundled with esbuild; on Node, Vite's module runner transforms
62
+ * TypeScript workspace packages (`@textjs/app` sources) imported by the config.
63
+ */
64
+ export async function loadTextConfig(root: string, command: "serve" | "build", mode = command === "build" ? "production" : "development"): Promise<{ config: TextConfig; file: string | null }> {
65
+ const file = findConfigFile(root);
66
+ if (!file) return { config: {}, file: null };
67
+ const loader = typeof process.versions.bun === "string" ? "bundle" : "runner";
68
+ const loaded = await preservingNodeEnv(() => loadConfigFromFile({ command, mode }, file, root, "silent", undefined, loader));
69
+ const config = (loaded?.config ?? {}) as TextConfig;
70
+ warnUnknownConfigKeys(config, file);
71
+ return { config, file };
72
+ }
package/src/cli/dev.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { relative } from "node:path";
2
+ import { createServer, mergeConfig, type InlineConfig, type ViteDevServer } from "vite";
3
+ import { textjsVitePlugin, type TextLogger } from "../vite-plugin";
4
+ import { loadTextConfig } from "./config";
5
+ import { banner, color, devtoolsHint, ok } from "./ui";
6
+
7
+ export interface DevOptions {
8
+ root: string;
9
+ port?: number;
10
+ host?: string | boolean;
11
+ /** Print logs (default `true`). */
12
+ log?: boolean;
13
+ }
14
+
15
+ export function cliLogger(enabled = true): TextLogger {
16
+ return {
17
+ info: (m) => {
18
+ if (enabled) console.log(color.gray(m));
19
+ },
20
+ error: (m) => {
21
+ if (enabled) console.error(color.red(m));
22
+ },
23
+ };
24
+ }
25
+
26
+ export async function startDevServer(opts: DevOptions): Promise<ViteDevServer> {
27
+ const started = performance.now();
28
+ const { config, file } = await loadTextConfig(opts.root, "serve");
29
+ const log = opts.log ?? true;
30
+ const inline: InlineConfig = {
31
+ configFile: false,
32
+ root: opts.root,
33
+ mode: "development",
34
+ logLevel: log ? "warn" : "silent",
35
+ clearScreen: false,
36
+ server: { port: opts.port ?? 3000, host: opts.host, strictPort: false },
37
+ plugins: [textjsVitePlugin({ ...config, root: opts.root, logger: cliLogger(log) })],
38
+ };
39
+ const server = await createServer(config.vite ? mergeConfig(inline, config.vite) : inline);
40
+ await server.listen();
41
+ if (log) {
42
+ const urls = server.resolvedUrls;
43
+ const lines: Array<[string, string]> = [["Local", color.cyan(urls?.local[0] ?? `http://localhost:${opts.port ?? 3000}`)]];
44
+ if (urls?.network[0]) lines.push(["Network", color.cyan(urls.network[0])]);
45
+ if (file) lines.push(["Config", relative(opts.root, file)]);
46
+ console.log(`\n${banner(lines)}\n`);
47
+ console.log(ok("Starting..."));
48
+ console.log(ok(`Ready in ${Math.round(performance.now() - started)}ms`));
49
+ console.log("");
50
+ console.log(devtoolsHint());
51
+ console.log("");
52
+ }
53
+ return server;
54
+ }
@@ -0,0 +1,185 @@
1
+ // Command modules (Vite, the dev server, the MCP server) load lazily: `text mcp`
2
+ // must own stdout before any third-party module runs.
3
+ import { spawn } from "node:child_process";
4
+ import { resolve } from "node:path";
5
+ import { txError, txMessage, VERSION } from "@textjs/core";
6
+ import { brand, color, fail } from "./ui";
7
+
8
+ export interface ParsedArgs {
9
+ command: string;
10
+ dir: string;
11
+ port?: number;
12
+ host?: string | boolean;
13
+ /** `text proxy --target <url>`. */
14
+ target?: string;
15
+ /** `text proxy --allow-origin <origin>` (repeatable). */
16
+ allowOrigins: string[];
17
+ /** `text build --adapter <static|bun|vercel|cloudflare>`. */
18
+ adapter?: string;
19
+ /** `text run --path <pathname>`. */
20
+ path?: string;
21
+ help: boolean;
22
+ version: boolean;
23
+ }
24
+
25
+ export function parseArgs(argv: readonly string[]): ParsedArgs {
26
+ const out: ParsedArgs = { command: "", dir: ".", allowOrigins: [], help: false, version: false };
27
+ const positional: string[] = [];
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const a = argv[i] as string;
30
+ if (a === "-h" || a === "--help") out.help = true;
31
+ else if (a === "-v" || a === "--version") out.version = true;
32
+ else if (a === "-p" || a === "--port") out.port = Number(argv[++i]);
33
+ else if (a.startsWith("--port=")) out.port = Number(a.slice(7));
34
+ else if (a === "--path") out.path = argv[++i];
35
+ else if (a.startsWith("--path=")) out.path = a.slice(7);
36
+ else if (a === "--adapter") out.adapter = argv[++i];
37
+ else if (a.startsWith("--adapter=")) out.adapter = a.slice(10);
38
+ else if (a === "--target") out.target = argv[++i];
39
+ else if (a.startsWith("--target=")) out.target = a.slice(9);
40
+ else if (a === "--allow-origin") {
41
+ const v = argv[++i];
42
+ if (v) out.allowOrigins.push(v);
43
+ } else if (a.startsWith("--allow-origin=")) out.allowOrigins.push(a.slice(15));
44
+ else if (a === "-H" || a === "--host" || a === "--hostname") {
45
+ const next = argv[i + 1];
46
+ if (next !== undefined && !next.startsWith("-") && /[.:]|localhost/.test(next)) {
47
+ out.host = next;
48
+ i++;
49
+ } else out.host = true;
50
+ } else if (a.startsWith("--host=")) out.host = a.slice(7);
51
+ else positional.push(a);
52
+ }
53
+ out.command = positional[0] ?? "";
54
+ if (positional[1]) out.dir = positional[1];
55
+ if (out.port !== undefined && !Number.isFinite(out.port)) out.port = undefined;
56
+ return out;
57
+ }
58
+
59
+ export const HELP = `Usage: text <command> [options] [dir]
60
+
61
+ Commands:
62
+ dev Start the development server (Vite + React Fast Refresh)
63
+ build Create an optimized console production build
64
+ start Serve the production build
65
+ run Render the app in this terminal (Bun)
66
+ proxy Bridge textjs.org/opencode to your local OpenCode server
67
+ (same as bunx @textjs/opencode proxy)
68
+ mcp Serve the app to coding agents over MCP (stdio, Bun)
69
+
70
+ Options:
71
+ -p, --port <port> Port (default 3000; proxy 4096)
72
+ --path <pathname> run: initial route (default /)
73
+ --adapter <name> build: static | bun | vercel | cloudflare
74
+ --target <url> proxy: OpenCode server URL (default: discover)
75
+ --allow-origin <o> proxy: allow another browser origin
76
+ -H, --host [host] Expose on the network (optionally bind a host)
77
+ -v, --version Print the Text.js version
78
+ -h, --help Show this help
79
+ `;
80
+
81
+ /** Commands that import route files as TypeScript and so need Bun. */
82
+ const BUN_COMMANDS = new Set(["run", "mcp"]);
83
+ const REEXEC_ENV = "TEXTJS_REEXEC_BUN";
84
+
85
+ /**
86
+ * Re-run this CLI under Bun (same script and arguments, inherited stdio,
87
+ * forwarded signals). Resolves with the child's exit code, or `null` when
88
+ * Bun is not installed.
89
+ */
90
+ export function reexecUnderBun(argv: readonly string[], script = process.argv[1]): Promise<number | null> {
91
+ if (!script || process.env[REEXEC_ENV]) return Promise.resolve(null);
92
+ return new Promise((done) => {
93
+ const child = spawn("bun", [script, ...argv], { stdio: "inherit", env: { ...process.env, [REEXEC_ENV]: "1" } });
94
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"] as const;
95
+ const forward = (signal: NodeJS.Signals): void => void child.kill(signal);
96
+ for (const s of signals) process.on(s, forward);
97
+ const finish = (code: number | null): void => {
98
+ for (const s of signals) process.off(s, forward);
99
+ done(code);
100
+ };
101
+ child.once("error", () => finish(null));
102
+ child.once("exit", (code, signal) => finish(code ?? (signal ? 1 : 0)));
103
+ });
104
+ }
105
+
106
+ export async function main(argv: readonly string[]): Promise<number> {
107
+ const args = parseArgs(argv);
108
+ if (args.version) {
109
+ console.log(`Text.js v${VERSION}`);
110
+ return 0;
111
+ }
112
+ if (BUN_COMMANDS.has(args.command) && !args.help && typeof process.versions.bun !== "string") {
113
+ const code = await reexecUnderBun(argv);
114
+ if (code !== null) return code;
115
+ // No Bun: `text mcp` reports TX019 on stderr (stdout is reserved for JSON-RPC).
116
+ if (args.command === "mcp") {
117
+ process.stderr.write(`${txMessage("TX019", "mcp", "mcp")}\n`);
118
+ return 1;
119
+ }
120
+ }
121
+ if (args.command === "mcp") {
122
+ // Owns stdout (JSON-RPC) and its own flags; see `text mcp --help`.
123
+ const { runMcpCli } = await import("../mcp/cli");
124
+ return runMcpCli(argv);
125
+ }
126
+ if (args.help || args.command === "" || args.command === "help") {
127
+ console.log(`\n ${brand()}\n The Console Framework for the Web\n`);
128
+ console.log(HELP);
129
+ return args.command === "" && !args.help ? 1 : 0;
130
+ }
131
+ const root = resolve(args.dir);
132
+ try {
133
+ switch (args.command) {
134
+ case "dev": {
135
+ const { startDevServer } = await import("./dev");
136
+ const server = await startDevServer({ root, port: args.port, host: args.host });
137
+ const stop = async (): Promise<void> => {
138
+ await server.close();
139
+ process.exit(0);
140
+ };
141
+ process.once("SIGINT", stop);
142
+ process.once("SIGTERM", stop);
143
+ await new Promise<never>(() => undefined);
144
+ return 0;
145
+ }
146
+ case "build": {
147
+ const { buildApp } = await import("./build");
148
+ await buildApp({ root, adapter: args.adapter });
149
+ return 0;
150
+ }
151
+ case "start": {
152
+ const { startProductionServer } = await import("./start");
153
+ const s = await startProductionServer({ root, port: args.port, host: typeof args.host === "string" ? args.host : args.host ? "0.0.0.0" : undefined });
154
+ const stop = async (): Promise<void> => {
155
+ await s.close();
156
+ process.exit(0);
157
+ };
158
+ process.once("SIGINT", stop);
159
+ process.once("SIGTERM", stop);
160
+ await new Promise<never>(() => undefined);
161
+ return 0;
162
+ }
163
+ case "run": {
164
+ if (typeof process.versions.bun !== "string") throw txError("TX019", "run", "run");
165
+ const { runInTerminal } = await import("./run");
166
+ const app = await runInTerminal({ root, path: args.path });
167
+ await app.waitUntilExit();
168
+ return 0;
169
+ }
170
+ case "proxy": {
171
+ // One implementation: the bridge lives in @textjs/opencode (`bunx @textjs/opencode proxy`).
172
+ const { runProxy } = await import("@textjs/opencode");
173
+ return await runProxy({ port: args.port, target: args.target, allowOrigins: args.allowOrigins });
174
+ }
175
+ default:
176
+ console.error(fail(txMessage("TX020", color.bold(args.command))));
177
+ if (args.command === "deploy") console.error(color.gray(" To deploy, run text build --adapter <static|bun|vercel|cloudflare>."));
178
+ console.log(HELP);
179
+ return 1;
180
+ }
181
+ } catch (error) {
182
+ if (args.command !== "build") console.error(fail(error instanceof Error ? error.message : String(error)));
183
+ return 1;
184
+ }
185
+ }
package/src/cli/run.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * `text run` — render the `app/` directory in the terminal: same routes,
3
+ * layouts, components and route handlers, drawn with truecolor ANSI instead
4
+ * of `console.log`. Runs on Bun (route files are imported as TypeScript).
5
+ */
6
+
7
+ import { existsSync } from "node:fs";
8
+ import { relative } from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+ import { createElement } from "react";
11
+ import { txError } from "@textjs/core";
12
+ import { renderToTerminal, type TerminalInput, type TerminalOutput, type TerminalRoot } from "@textjs/react/terminal";
13
+ import type { ThemeName } from "@textjs/react";
14
+ import { createMemoryHistory } from "../history";
15
+ import { AppRouter } from "../router";
16
+ import { ROUTE_FILE_KINDS, type RouteManifest, type RouteModule, type RouteSegmentNode } from "../routing";
17
+ import { matchApiRoute, scanApp, toApiManifest, toPosix, type AppScan, type ScannedSegment } from "../server/scan";
18
+ import { runRouteHandler, type RouteHandlerModule } from "../server/http";
19
+ import { installServerJsxRuntime } from "../server/bun-jsx";
20
+ import { prepareServerManifest } from "../server/live-app";
21
+ import { ServerComponentCache, ServerComponentsProvider } from "../server/server-components";
22
+ import { loadTextConfig } from "./config";
23
+
24
+ export interface RunOptions {
25
+ root: string;
26
+ /** Initial pathname (default `/`). */
27
+ path?: string;
28
+ theme?: ThemeName;
29
+ stdin?: TerminalInput;
30
+ stdout?: TerminalOutput;
31
+ /** Skip `text.config.ts` (tests). */
32
+ skipConfig?: boolean;
33
+ }
34
+
35
+ const importFile = (file: string): Promise<RouteModule> => import(pathToFileURL(file).href) as Promise<RouteModule>;
36
+
37
+ /** A route manifest whose modules are imported on demand from disk. */
38
+ export function toRuntimeManifest(scan: AppScan): RouteManifest {
39
+ const convert = (s: ScannedSegment): RouteSegmentNode | null => {
40
+ const node: RouteSegmentNode = { segment: s.segment, files: {} };
41
+ let has = false;
42
+ for (const kind of ROUTE_FILE_KINDS) {
43
+ const f = s.files[kind];
44
+ if (!f) continue;
45
+ has = true;
46
+ const key = kind === "not-found" ? "notFound" : kind;
47
+ (node as unknown as Record<string, unknown>)[key] = () => importFile(f);
48
+ (node.files as Record<string, string>)[kind] = toPosix(relative(scan.root, f));
49
+ }
50
+ const children = s.children.map(convert).filter((c): c is RouteSegmentNode => c !== null);
51
+ if (children.length > 0) node.children = children;
52
+ return has || children.length > 0 || s.segment === "" ? node : null;
53
+ };
54
+ return convert(scan.tree) as RouteManifest;
55
+ }
56
+
57
+ /** Import every route module up front (no Suspense flashes on the first frame). */
58
+ export async function loadRuntimeManifest(scan: AppScan): Promise<RouteManifest> {
59
+ const manifest = toRuntimeManifest(scan);
60
+ const tasks: Array<Promise<void>> = [];
61
+ const walk = (node: RouteSegmentNode): void => {
62
+ for (const key of ["layout", "page", "loading", "error", "notFound"] as const) {
63
+ const ref = node[key];
64
+ if (typeof ref === "function") tasks.push(ref().then((m) => void (node[key] = m)));
65
+ }
66
+ node.children?.forEach(walk);
67
+ };
68
+ walk(manifest);
69
+ await Promise.all(tasks);
70
+ return manifest;
71
+ }
72
+
73
+ /**
74
+ * Wrap `fetch` so same-origin paths (`fetch("/api/health")`) run the app's
75
+ * route handlers in-process, exactly as `text dev` would serve them.
76
+ */
77
+ export function createLocalFetch(scan: AppScan, base: typeof fetch = fetch, origin = "http://localhost:3000"): typeof fetch {
78
+ const api = toApiManifest(scan);
79
+ const local = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
80
+ const raw = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
81
+ if (!raw.startsWith("/") || raw.startsWith("//")) return base(input, init);
82
+ const url = new URL(raw, origin);
83
+ const hit = matchApiRoute(api, url.pathname);
84
+ if (!hit) return new Response("Not Found", { status: 404 });
85
+ const mod = (await import(pathToFileURL(hit.file).href)) as RouteHandlerModule;
86
+ const request = input instanceof Request ? new Request(url, input) : new Request(url, init);
87
+ return runRouteHandler(mod, request, hit.params);
88
+ };
89
+ return Object.assign(local, { preconnect: () => undefined }) as unknown as typeof fetch;
90
+ }
91
+
92
+ /** Render the app in the terminal. Resolves once it is mounted. */
93
+ export async function runInTerminal(opts: RunOptions): Promise<TerminalRoot> {
94
+ const scan = scanApp(opts.root);
95
+ if (!existsSync(scan.appDir)) throw txError("TX018", opts.root);
96
+ let theme = opts.theme;
97
+ if (!opts.skipConfig && theme === undefined) {
98
+ const { config } = await loadTextConfig(opts.root, "serve");
99
+ const t = (config as { viewport?: { theme?: unknown } }).viewport?.theme;
100
+ if (typeof t === "string") theme = t as ThemeName;
101
+ }
102
+ const g = globalThis as { fetch: typeof fetch };
103
+ const originalFetch = g.fetch;
104
+ g.fetch = createLocalFetch(scan, originalFetch);
105
+ const history = createMemoryHistory(opts.path ?? "/");
106
+ // `async function` components (e.g. `runtime = "server"` pages) render like in `text dev`.
107
+ installServerJsxRuntime(opts.root);
108
+ const routes = prepareServerManifest(await loadRuntimeManifest(scan));
109
+ const app = await renderToTerminal(
110
+ createElement(ServerComponentsProvider, { cache: new ServerComponentCache() }, createElement(AppRouter, { routes, history, theme, documentTitle: false, errorStore: null })),
111
+ {
112
+ stdin: opts.stdin,
113
+ stdout: opts.stdout,
114
+ theme,
115
+ onExit: () => {
116
+ g.fetch = originalFetch;
117
+ },
118
+ },
119
+ );
120
+ void app.waitUntilExit().then(() => {
121
+ g.fetch = originalFetch;
122
+ });
123
+ return app;
124
+ }