@textjs/app 0.0.0-stage → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/bin/text.js +29 -0
- package/bin/text.ts +13 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli/build.d.ts +39 -0
- package/dist/cli/build.d.ts.map +1 -0
- package/dist/cli/build.js +157 -0
- package/dist/cli/build.js.map +1 -0
- package/dist/cli/config.d.ts +22 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +73 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/dev.d.ts +12 -0
- package/dist/cli/dev.d.ts.map +1 -0
- package/dist/cli/dev.js +49 -0
- package/dist/cli/dev.js.map +1 -0
- package/dist/cli/index.d.ts +26 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +192 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.d.ts +31 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +111 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/start.d.ts +14 -0
- package/dist/cli/start.d.ts.map +1 -0
- package/dist/cli/start.js +183 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/ui.d.ts +35 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +73 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +93 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +143 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +28 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +44 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +26 -0
- package/dist/context.js.map +1 -0
- package/dist/dev-errors.d.ts +68 -0
- package/dist/dev-errors.d.ts.map +1 -0
- package/dist/dev-errors.js +121 -0
- package/dist/dev-errors.js.map +1 -0
- package/dist/devtools.d.ts +64 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +186 -0
- package/dist/devtools.js.map +1 -0
- package/dist/frame-mirror.d.ts +19 -0
- package/dist/frame-mirror.d.ts.map +1 -0
- package/dist/frame-mirror.js +141 -0
- package/dist/frame-mirror.js.map +1 -0
- package/dist/frame-protocol.d.ts +175 -0
- package/dist/frame-protocol.d.ts.map +1 -0
- package/dist/frame-protocol.js +369 -0
- package/dist/frame-protocol.js.map +1 -0
- package/dist/history.d.ts +54 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +152 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/link.d.ts +32 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +54 -0
- package/dist/link.js.map +1 -0
- package/dist/live-player.d.ts +83 -0
- package/dist/live-player.d.ts.map +1 -0
- package/dist/live-player.js +413 -0
- package/dist/live-player.js.map +1 -0
- package/dist/live-protocol.d.ts +132 -0
- package/dist/live-protocol.d.ts.map +1 -0
- package/dist/live-protocol.js +278 -0
- package/dist/live-protocol.js.map +1 -0
- package/dist/live-stub.d.ts +10 -0
- package/dist/live-stub.d.ts.map +1 -0
- package/dist/live-stub.js +32 -0
- package/dist/live-stub.js.map +1 -0
- package/dist/mcp/cli.d.ts +22 -0
- package/dist/mcp/cli.d.ts.map +1 -0
- package/dist/mcp/cli.js +177 -0
- package/dist/mcp/cli.js.map +1 -0
- package/dist/mcp/frame.d.ts +58 -0
- package/dist/mcp/frame.d.ts.map +1 -0
- package/dist/mcp/frame.js +169 -0
- package/dist/mcp/frame.js.map +1 -0
- package/dist/mcp/index.d.ts +15 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +15 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/inspect.d.ts +95 -0
- package/dist/mcp/inspect.d.ts.map +1 -0
- package/dist/mcp/inspect.js +386 -0
- package/dist/mcp/inspect.js.map +1 -0
- package/dist/mcp/keys.d.ts +20 -0
- package/dist/mcp/keys.d.ts.map +1 -0
- package/dist/mcp/keys.js +87 -0
- package/dist/mcp/keys.js.map +1 -0
- package/dist/mcp/logs.d.ts +63 -0
- package/dist/mcp/logs.d.ts.map +1 -0
- package/dist/mcp/logs.js +217 -0
- package/dist/mcp/logs.js.map +1 -0
- package/dist/mcp/server.d.ts +64 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +485 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/session.d.ts +230 -0
- package/dist/mcp/session.d.ts.map +1 -0
- package/dist/mcp/session.js +742 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/navigation-errors.d.ts +28 -0
- package/dist/navigation-errors.d.ts.map +1 -0
- package/dist/navigation-errors.js +43 -0
- package/dist/navigation-errors.js.map +1 -0
- package/dist/navigation.d.ts +21 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +33 -0
- package/dist/navigation.js.map +1 -0
- package/dist/router.d.ts +76 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +296 -0
- package/dist/router.js.map +1 -0
- package/dist/routing.d.ts +101 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +286 -0
- package/dist/routing.js.map +1 -0
- package/dist/server/adapters/build-step.d.ts +34 -0
- package/dist/server/adapters/build-step.d.ts.map +1 -0
- package/dist/server/adapters/build-step.js +112 -0
- package/dist/server/adapters/build-step.js.map +1 -0
- package/dist/server/adapters/bun.d.ts +8 -0
- package/dist/server/adapters/bun.d.ts.map +1 -0
- package/dist/server/adapters/bun.js +89 -0
- package/dist/server/adapters/bun.js.map +1 -0
- package/dist/server/adapters/bundle.d.ts +21 -0
- package/dist/server/adapters/bundle.d.ts.map +1 -0
- package/dist/server/adapters/bundle.js +86 -0
- package/dist/server/adapters/bundle.js.map +1 -0
- package/dist/server/adapters/cloudflare.d.ts +29 -0
- package/dist/server/adapters/cloudflare.d.ts.map +1 -0
- package/dist/server/adapters/cloudflare.js +105 -0
- package/dist/server/adapters/cloudflare.js.map +1 -0
- package/dist/server/adapters/index.d.ts +10 -0
- package/dist/server/adapters/index.d.ts.map +1 -0
- package/dist/server/adapters/index.js +24 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/runtime-node.d.ts +16 -0
- package/dist/server/adapters/runtime-node.d.ts.map +1 -0
- package/dist/server/adapters/runtime-node.js +29 -0
- package/dist/server/adapters/runtime-node.js.map +1 -0
- package/dist/server/adapters/runtime.d.ts +42 -0
- package/dist/server/adapters/runtime.d.ts.map +1 -0
- package/dist/server/adapters/runtime.js +115 -0
- package/dist/server/adapters/runtime.js.map +1 -0
- package/dist/server/adapters/shared.d.ts +66 -0
- package/dist/server/adapters/shared.d.ts.map +1 -0
- package/dist/server/adapters/shared.js +124 -0
- package/dist/server/adapters/shared.js.map +1 -0
- package/dist/server/adapters/static.d.ts +9 -0
- package/dist/server/adapters/static.d.ts.map +1 -0
- package/dist/server/adapters/static.js +70 -0
- package/dist/server/adapters/static.js.map +1 -0
- package/dist/server/adapters/vercel.d.ts +43 -0
- package/dist/server/adapters/vercel.d.ts.map +1 -0
- package/dist/server/adapters/vercel.js +119 -0
- package/dist/server/adapters/vercel.js.map +1 -0
- package/dist/server/bun-jsx.d.ts +18 -0
- package/dist/server/bun-jsx.d.ts.map +1 -0
- package/dist/server/bun-jsx.js +56 -0
- package/dist/server/bun-jsx.js.map +1 -0
- package/dist/server/cache-route.d.ts +38 -0
- package/dist/server/cache-route.d.ts.map +1 -0
- package/dist/server/cache-route.js +109 -0
- package/dist/server/cache-route.js.map +1 -0
- package/dist/server/cache.d.ts +131 -0
- package/dist/server/cache.d.ts.map +1 -0
- package/dist/server/cache.js +409 -0
- package/dist/server/cache.js.map +1 -0
- package/dist/server/frames.d.ts +45 -0
- package/dist/server/frames.d.ts.map +1 -0
- package/dist/server/frames.js +294 -0
- package/dist/server/frames.js.map +1 -0
- package/dist/server/http.d.ts +80 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +453 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/jsx-dev-runtime.d.ts +5 -0
- package/dist/server/jsx-dev-runtime.d.ts.map +1 -0
- package/dist/server/jsx-dev-runtime.js +6 -0
- package/dist/server/jsx-dev-runtime.js.map +1 -0
- package/dist/server/jsx-runtime.d.ts +10 -0
- package/dist/server/jsx-runtime.d.ts.map +1 -0
- package/dist/server/jsx-runtime.js +11 -0
- package/dist/server/jsx-runtime.js.map +1 -0
- package/dist/server/live-app.d.ts +87 -0
- package/dist/server/live-app.d.ts.map +1 -0
- package/dist/server/live-app.js +307 -0
- package/dist/server/live-app.js.map +1 -0
- package/dist/server/live-build.d.ts +70 -0
- package/dist/server/live-build.d.ts.map +1 -0
- package/dist/server/live-build.js +222 -0
- package/dist/server/live-build.js.map +1 -0
- package/dist/server/live-dev.d.ts +62 -0
- package/dist/server/live-dev.d.ts.map +1 -0
- package/dist/server/live-dev.js +313 -0
- package/dist/server/live-dev.js.map +1 -0
- package/dist/server/live-manifest.d.ts +23 -0
- package/dist/server/live-manifest.d.ts.map +1 -0
- package/dist/server/live-manifest.js +7 -0
- package/dist/server/live-manifest.js.map +1 -0
- package/dist/server/live-prod.d.ts +30 -0
- package/dist/server/live-prod.d.ts.map +1 -0
- package/dist/server/live-prod.js +97 -0
- package/dist/server/live-prod.js.map +1 -0
- package/dist/server/live-routes.d.ts +72 -0
- package/dist/server/live-routes.d.ts.map +1 -0
- package/dist/server/live-routes.js +274 -0
- package/dist/server/live-routes.js.map +1 -0
- package/dist/server/live-web.d.ts +16 -0
- package/dist/server/live-web.d.ts.map +1 -0
- package/dist/server/live-web.js +159 -0
- package/dist/server/live-web.js.map +1 -0
- package/dist/server/live.d.ts +138 -0
- package/dist/server/live.d.ts.map +1 -0
- package/dist/server/live.js +542 -0
- package/dist/server/live.js.map +1 -0
- package/dist/server/middleware-node.d.ts +48 -0
- package/dist/server/middleware-node.d.ts.map +1 -0
- package/dist/server/middleware-node.js +167 -0
- package/dist/server/middleware-node.js.map +1 -0
- package/dist/server/middleware-runtime.d.ts +97 -0
- package/dist/server/middleware-runtime.d.ts.map +1 -0
- package/dist/server/middleware-runtime.js +447 -0
- package/dist/server/middleware-runtime.js.map +1 -0
- package/dist/server/middleware.d.ts +145 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +365 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/ppr.d.ts +22 -0
- package/dist/server/ppr.d.ts.map +1 -0
- package/dist/server/ppr.js +58 -0
- package/dist/server/ppr.js.map +1 -0
- package/dist/server/scan.d.ts +48 -0
- package/dist/server/scan.d.ts.map +1 -0
- package/dist/server/scan.js +161 -0
- package/dist/server/scan.js.map +1 -0
- package/dist/server/server-components.d.ts +65 -0
- package/dist/server/server-components.d.ts.map +1 -0
- package/dist/server/server-components.js +215 -0
- package/dist/server/server-components.js.map +1 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.d.ts.map +1 -0
- package/dist/terminal.js +5 -0
- package/dist/terminal.js.map +1 -0
- package/dist/testing.d.ts +5 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +5 -0
- package/dist/testing.js.map +1 -0
- package/dist/vite-plugin.d.ts +51 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +403 -0
- package/dist/vite-plugin.js.map +1 -0
- package/package.json +130 -4
- package/src/cli/build.ts +182 -0
- package/src/cli/config.ts +72 -0
- package/src/cli/dev.ts +54 -0
- package/src/cli/index.ts +185 -0
- package/src/cli/run.ts +124 -0
- package/src/cli/start.ts +194 -0
- package/src/cli/ui.ts +93 -0
- package/src/client.tsx +122 -0
- package/src/config.ts +155 -0
- package/src/context.ts +69 -0
- package/src/dev-errors.ts +167 -0
- package/src/devtools.tsx +419 -0
- package/src/frame-mirror.ts +145 -0
- package/src/frame-protocol.ts +435 -0
- package/src/history.ts +201 -0
- package/src/index.ts +9 -0
- package/src/link.tsx +108 -0
- package/src/live-player.ts +449 -0
- package/src/live-protocol.ts +317 -0
- package/src/live-stub.tsx +32 -0
- package/src/mcp/cli.ts +188 -0
- package/src/mcp/frame.ts +224 -0
- package/src/mcp/index.ts +15 -0
- package/src/mcp/inspect.ts +445 -0
- package/src/mcp/keys.ts +86 -0
- package/src/mcp/logs.ts +225 -0
- package/src/mcp/server.ts +595 -0
- package/src/mcp/session.ts +821 -0
- package/src/navigation-errors.ts +60 -0
- package/src/navigation.ts +44 -0
- package/src/router.tsx +497 -0
- package/src/routing.ts +344 -0
- package/src/server/adapters/build-step.ts +135 -0
- package/src/server/adapters/bun.ts +92 -0
- package/src/server/adapters/bundle.ts +97 -0
- package/src/server/adapters/cloudflare.ts +111 -0
- package/src/server/adapters/index.ts +29 -0
- package/src/server/adapters/runtime-node.ts +34 -0
- package/src/server/adapters/runtime.ts +145 -0
- package/src/server/adapters/shared.ts +159 -0
- package/src/server/adapters/static.ts +68 -0
- package/src/server/adapters/vercel.ts +149 -0
- package/src/server/bun-jsx.ts +65 -0
- package/src/server/cache-route.ts +139 -0
- package/src/server/cache.ts +498 -0
- package/src/server/frames.ts +326 -0
- package/src/server/http.ts +467 -0
- package/src/server/jsx-dev-runtime.ts +9 -0
- package/src/server/jsx-runtime.ts +13 -0
- package/src/server/live-app.tsx +374 -0
- package/src/server/live-build.ts +258 -0
- package/src/server/live-dev.ts +357 -0
- package/src/server/live-manifest.ts +25 -0
- package/src/server/live-prod.ts +120 -0
- package/src/server/live-routes.ts +298 -0
- package/src/server/live-web.ts +170 -0
- package/src/server/live.ts +551 -0
- package/src/server/middleware-node.ts +168 -0
- package/src/server/middleware-runtime.ts +478 -0
- package/src/server/middleware.ts +451 -0
- package/src/server/ppr.ts +62 -0
- package/src/server/scan.ts +190 -0
- package/src/server/server-components.ts +224 -0
- package/src/terminal.ts +5 -0
- package/src/testing.ts +5 -0
- package/src/vite-plugin.ts +413 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `text build --adapter cloudflare`: Cloudflare Pages advanced mode.
|
|
3
|
+
* `dist/_worker.js` runs middleware and route handlers; `_routes.json` keeps
|
|
4
|
+
* hashed assets on Pages' static CDN; `env.ASSETS` serves everything else.
|
|
5
|
+
*
|
|
6
|
+
* Requires the `nodejs_compat` compatibility flag (the data cache uses
|
|
7
|
+
* `AsyncLocalStorage`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { matcherCouldMatch, middlewareMatchers } from "../middleware-runtime";
|
|
13
|
+
import type { MiddlewareModule } from "../middleware";
|
|
14
|
+
import { bundleServerEntry } from "./bundle";
|
|
15
|
+
import { assertNoServerRoutes, removeServerBundle, serverEntry, type AdapterContext, type AdapterResult } from "./shared";
|
|
16
|
+
|
|
17
|
+
export const CLOUDFLARE_COMPAT = { date: "2025-01-01", flags: ["nodejs_compat"] } as const;
|
|
18
|
+
|
|
19
|
+
const BODY = `
|
|
20
|
+
async function asset(env, url, request) {
|
|
21
|
+
const res = await env.ASSETS.fetch(new Request(url, { method: request.method, headers: request.headers }));
|
|
22
|
+
if (res.status === 404) return null;
|
|
23
|
+
if (!new URL(url).pathname.startsWith("/assets/") || !res.ok) return res;
|
|
24
|
+
// basePath apps serve hashed assets through the worker (they live at dist/assets, not dist/<base>/assets).
|
|
25
|
+
const out = new Response(res.body, res);
|
|
26
|
+
out.headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const handler = createRequestHandler({
|
|
31
|
+
...options,
|
|
32
|
+
assets(pathname, request, context) {
|
|
33
|
+
return asset(context.env, new URL(pathname, request.url), request);
|
|
34
|
+
},
|
|
35
|
+
document(request, context) {
|
|
36
|
+
// Pages redirects /index.html to /, so ask for the directory index.
|
|
37
|
+
return asset(context.env, new URL("/", request.url), request);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
async fetch(request, env, ctx) {
|
|
43
|
+
try {
|
|
44
|
+
return await handler(request, { env, waitUntil: (p) => ctx.waitUntil(p) });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(error);
|
|
47
|
+
return new Response("500 | Internal Server Error", { status: 500 });
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
export interface CloudflareRoutes {
|
|
54
|
+
version: 1;
|
|
55
|
+
include: string[];
|
|
56
|
+
exclude: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function listAssets(outDir: string): string[] {
|
|
60
|
+
const dir = join(outDir, "assets");
|
|
61
|
+
if (!existsSync(dir)) return [];
|
|
62
|
+
const out: string[] = [];
|
|
63
|
+
const walk = (d: string, rel: string): void => {
|
|
64
|
+
for (const name of readdirSync(d)) {
|
|
65
|
+
const p = join(d, name);
|
|
66
|
+
if (statSync(p).isDirectory()) walk(p, `${rel}/${name}`);
|
|
67
|
+
else out.push(`${rel}/${name}`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
walk(dir, "/assets");
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `_routes.json`: invoke the worker for everything except hashed assets.
|
|
76
|
+
* Pages serves excluded paths straight from `dist/`, so the exclusion only
|
|
77
|
+
* applies without a basePath (files live at `dist/assets`, not
|
|
78
|
+
* `dist/<base>/assets`), and only when no middleware matcher could cover an
|
|
79
|
+
* asset (`has` / `missing` are request-time and ignored here).
|
|
80
|
+
*/
|
|
81
|
+
export function cloudflareRoutes(ctx: AdapterContext, middleware: MiddlewareModule | null): CloudflareRoutes {
|
|
82
|
+
const base = ctx.basePath;
|
|
83
|
+
const matchers = middleware ? middlewareMatchers(middleware) : null;
|
|
84
|
+
const assets = listAssets(ctx.outDir);
|
|
85
|
+
const assetsRun = middleware ? [...assets, "/assets/__probe.js"].some((p) => matcherCouldMatch(matchers, p)) : false;
|
|
86
|
+
return { version: 1, include: [`${base}/*`], exclude: assetsRun || base ? [] : ["/assets/*"] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function adaptCloudflare(ctx: AdapterContext, middleware: MiddlewareModule | null): Promise<AdapterResult> {
|
|
90
|
+
assertNoServerRoutes(ctx, "cloudflare");
|
|
91
|
+
await bundleServerEntry({ root: ctx.root, config: ctx.config, code: serverEntry(ctx, { body: BODY }), outFile: join(ctx.outDir, "_worker.js"), target: "webworker" });
|
|
92
|
+
writeFileSync(join(ctx.outDir, "_routes.json"), `${JSON.stringify(cloudflareRoutes(ctx, middleware), null, 2)}\n`);
|
|
93
|
+
const headersFile = join(ctx.outDir, "_headers");
|
|
94
|
+
const rule = "/assets/*\n Cache-Control: public, max-age=31536000, immutable\n";
|
|
95
|
+
if (!existsSync(headersFile)) writeFileSync(headersFile, rule);
|
|
96
|
+
else if (!/^\/assets\/\*\s*$/m.test(readFileSync(headersFile, "utf8"))) writeFileSync(headersFile, `${readFileSync(headersFile, "utf8").trimEnd()}\n\n${rule}`);
|
|
97
|
+
// Without a top-level 404.html, Pages answers unknown files with index.html (200), and missing assets would render the app.
|
|
98
|
+
const notFoundFile = join(ctx.outDir, "404.html");
|
|
99
|
+
if (!existsSync(notFoundFile)) writeFileSync(notFoundFile, "404 | This page could not be found.\n");
|
|
100
|
+
// Pages would publish dist/.text (server bundles, manifests) as static files.
|
|
101
|
+
removeServerBundle(ctx.outDir);
|
|
102
|
+
return {
|
|
103
|
+
adapter: "cloudflare",
|
|
104
|
+
output: ctx.outDir,
|
|
105
|
+
notes: [
|
|
106
|
+
`Requires the ${CLOUDFLARE_COMPAT.flags.join(", ")} compatibility flag (Pages → Settings → Runtime, or wrangler.toml).`,
|
|
107
|
+
`Preview: bunx wrangler pages dev ${ctx.config.outDir ?? "dist"} --compatibility-flags=nodejs_compat`,
|
|
108
|
+
`Deploy: bunx wrangler pages deploy ${ctx.config.outDir ?? "dist"}`,
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `text build --adapter <name>` dispatch.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import type { MiddlewareModule } from "../middleware";
|
|
7
|
+
import { adaptBun } from "./bun";
|
|
8
|
+
import { adaptCloudflare } from "./cloudflare";
|
|
9
|
+
import { adaptStatic } from "./static";
|
|
10
|
+
import { adaptVercel } from "./vercel";
|
|
11
|
+
import type { AdapterContext, AdapterName, AdapterResult } from "./shared";
|
|
12
|
+
|
|
13
|
+
export { ADAPTERS, detectAdapter, isAdapterName, type AdapterContext, type AdapterName, type AdapterResult } from "./shared";
|
|
14
|
+
|
|
15
|
+
/** `.text/server/` bundle paths written by `text build`. */
|
|
16
|
+
export const SERVER_DIR = join(".text", "server");
|
|
17
|
+
|
|
18
|
+
export async function runAdapter(name: AdapterName, ctx: AdapterContext, middleware: MiddlewareModule | null): Promise<AdapterResult> {
|
|
19
|
+
switch (name) {
|
|
20
|
+
case "static":
|
|
21
|
+
return adaptStatic(ctx, middleware, join(ctx.outDir, SERVER_DIR, "api.mjs"));
|
|
22
|
+
case "bun":
|
|
23
|
+
return adaptBun(ctx);
|
|
24
|
+
case "vercel":
|
|
25
|
+
return adaptVercel(ctx, middleware);
|
|
26
|
+
case "cloudflare":
|
|
27
|
+
return adaptCloudflare(ctx, middleware);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.text/server/runtime.mjs`: `middleware.ts` and the `cacheHandler` store,
|
|
3
|
+
* bundled by `text build` for `text start`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { setCacheStore, type CacheStore } from "../cache";
|
|
10
|
+
import type { MiddlewareModule } from "../middleware";
|
|
11
|
+
|
|
12
|
+
export const RUNTIME_BUNDLE = "runtime.mjs";
|
|
13
|
+
|
|
14
|
+
export interface ServerRuntime {
|
|
15
|
+
middleware?: MiddlewareModule;
|
|
16
|
+
cacheStore?: CacheStore;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Entry source for `runtime.mjs`. */
|
|
20
|
+
export function serverRuntimeEntry(middlewareFile: string | null, cacheHandlerFile: string | null): string {
|
|
21
|
+
const lines: string[] = [];
|
|
22
|
+
if (middlewareFile) lines.push(`import * as middleware from ${JSON.stringify(middlewareFile)};`, "export { middleware };");
|
|
23
|
+
if (cacheHandlerFile) lines.push(`import cacheStore from ${JSON.stringify(cacheHandlerFile)};`, "export { cacheStore };");
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Load `runtime.mjs` from a build (installing its cache store). */
|
|
28
|
+
export async function loadServerRuntime(outDir: string, fresh = false): Promise<ServerRuntime | null> {
|
|
29
|
+
const file = join(outDir, ".text", "server", RUNTIME_BUNDLE);
|
|
30
|
+
if (!existsSync(file)) return null;
|
|
31
|
+
const mod = (await import(pathToFileURL(file).href + (fresh ? `?t=${Date.now()}` : ""))) as ServerRuntime;
|
|
32
|
+
if (mod.cacheStore) setCacheStore(mod.cacheStore);
|
|
33
|
+
return mod;
|
|
34
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Web-standard Text.js server (`(Request) => Response`) bundled into every
|
|
3
|
+
* deploy adapter: middleware → rewrites → route handlers (with the route
|
|
4
|
+
* cache) → static assets → the SPA document.
|
|
5
|
+
*
|
|
6
|
+
* No Node imports: this file runs on Bun, Node, Vercel and Cloudflare Workers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Rewrite } from "../../config";
|
|
10
|
+
import { matchAppRoutes, type RouteManifest, type RouteSegmentNode } from "../../routing";
|
|
11
|
+
import { runRouteHandlerWithCache } from "../cache-route";
|
|
12
|
+
import { setCacheStore, type CacheStore } from "../cache";
|
|
13
|
+
import { applyRewrites, compileRewrite, isLoopbackUrl, normalizeRequestTarget, proxyFetch, type RouteHandlerModule } from "../http";
|
|
14
|
+
import type { MiddlewareModule } from "../middleware";
|
|
15
|
+
import { rebuild, runMiddleware, withMiddlewareHeaders, type MiddlewareMerge } from "../middleware-runtime";
|
|
16
|
+
|
|
17
|
+
export interface RequestContext {
|
|
18
|
+
waitUntil?: (promise: Promise<unknown>) => void;
|
|
19
|
+
/** Platform bindings (Cloudflare `env`). */
|
|
20
|
+
env?: unknown;
|
|
21
|
+
/** The client's address, when the platform knows it. */
|
|
22
|
+
clientAddress?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RequestHandlerOptions {
|
|
26
|
+
/** API route manifest: leaf `files.page` values are keys into `handlers`. */
|
|
27
|
+
manifest: RouteManifest;
|
|
28
|
+
handlers: Record<string, RouteHandlerModule>;
|
|
29
|
+
middleware?: MiddlewareModule;
|
|
30
|
+
/** Skip middleware (it already ran at the edge, e.g. on Vercel). */
|
|
31
|
+
skipMiddleware?: boolean;
|
|
32
|
+
basePath?: string;
|
|
33
|
+
rewrites?: Rewrite[];
|
|
34
|
+
cacheStore?: CacheStore;
|
|
35
|
+
/** Serve a static file for `pathname` (without basePath), or `null`. */
|
|
36
|
+
assets?: (pathname: string, request: Request, context: RequestContext) => Promise<Response | null> | Response | null;
|
|
37
|
+
/** The SPA document (`index.html`) for page routes. */
|
|
38
|
+
document?: (request: Request, context: RequestContext) => Promise<Response | null> | Response | null;
|
|
39
|
+
/**
|
|
40
|
+
* Server-rendered routes (`runtime = "server"`) and prerendered shells, after
|
|
41
|
+
* middleware and rewrites. `url` is the path and query without basePath.
|
|
42
|
+
*/
|
|
43
|
+
live?: (request: Request, url: string, context: RequestContext) => Promise<Response | null> | Response | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type TextRequestHandler = (request: Request, context?: RequestContext) => Promise<Response>;
|
|
47
|
+
|
|
48
|
+
function notFound(): Response {
|
|
49
|
+
return new Response("404 | This page could not be found.", { status: 404, headers: { "content-type": "text/plain; charset=utf-8" } });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stripBase(pathname: string, basePath: string): string | null {
|
|
53
|
+
if (!basePath) return pathname;
|
|
54
|
+
if (pathname === basePath) return "/";
|
|
55
|
+
return pathname.startsWith(`${basePath}/`) ? pathname.slice(basePath.length) : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function matchApi(manifest: RouteManifest, pathname: string): { id: string; pattern: string; params: Record<string, string | string[]> } | null {
|
|
59
|
+
const m = matchAppRoutes(manifest, pathname);
|
|
60
|
+
if (!m) return null;
|
|
61
|
+
const leaf = m.chain[m.chain.length - 1] as RouteSegmentNode;
|
|
62
|
+
const id = leaf.files?.page;
|
|
63
|
+
return id ? { id, pattern: m.pattern, params: m.params } : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function safeDecode(pathname: string): string | null {
|
|
67
|
+
try {
|
|
68
|
+
return decodeURIComponent(pathname);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createRequestHandler(options: RequestHandlerOptions): TextRequestHandler {
|
|
75
|
+
const basePath = options.basePath ?? "";
|
|
76
|
+
const rewrites = (options.rewrites ?? []).map(compileRewrite);
|
|
77
|
+
if (options.cacheStore) setCacheStore(options.cacheStore);
|
|
78
|
+
|
|
79
|
+
return async (raw, context = {}) => {
|
|
80
|
+
// `//evil.com/x` (and `/\evil.com/x`) must never resolve to another origin downstream.
|
|
81
|
+
const rawUrl = new URL(raw.url);
|
|
82
|
+
const normalized = normalizeRequestTarget(`${rawUrl.pathname}${rawUrl.search}`);
|
|
83
|
+
const incoming = normalized === `${rawUrl.pathname}${rawUrl.search}` ? raw : rebuild(raw, new Headers(raw.headers), new URL(normalized, rawUrl.origin));
|
|
84
|
+
const url = new URL(incoming.url);
|
|
85
|
+
const stripped = stripBase(url.pathname, basePath);
|
|
86
|
+
if (stripped === null) return notFound();
|
|
87
|
+
let pathname = stripped;
|
|
88
|
+
let search = url.search;
|
|
89
|
+
let request = incoming;
|
|
90
|
+
let extra: Headers | null = null;
|
|
91
|
+
let merge: MiddlewareMerge = {};
|
|
92
|
+
const finish = (response: Response): Response => (extra ? withMiddlewareHeaders(response, extra, merge) : response);
|
|
93
|
+
|
|
94
|
+
if (options.middleware && !options.skipMiddleware) {
|
|
95
|
+
const outcome = await runMiddleware(options.middleware, request, { basePath, waitUntil: context.waitUntil });
|
|
96
|
+
if (outcome.type === "response") return outcome.response;
|
|
97
|
+
request = outcome.request;
|
|
98
|
+
extra = outcome.headers;
|
|
99
|
+
merge = { status: outcome.status, private: outcome.private };
|
|
100
|
+
if (outcome.rewrite) {
|
|
101
|
+
if (outcome.rewrite.origin !== url.origin) return finish(await proxyFetch(request, outcome.rewrite, { stripCredentials: !isLoopbackUrl(outcome.rewrite.href) }));
|
|
102
|
+
const p = stripBase(outcome.rewrite.pathname, basePath);
|
|
103
|
+
pathname = p ?? outcome.rewrite.pathname;
|
|
104
|
+
search = outcome.rewrite.search;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (rewrites.length > 0) {
|
|
109
|
+
const target = applyRewrites(rewrites, pathname + search);
|
|
110
|
+
if (target) {
|
|
111
|
+
if (/^[a-z]+:\/\//i.test(target)) return finish(await proxyFetch(request, target, { stripCredentials: !isLoopbackUrl(target) }));
|
|
112
|
+
const u = new URL(target, url.origin);
|
|
113
|
+
pathname = u.pathname;
|
|
114
|
+
search = u.search;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.live) {
|
|
119
|
+
const live = await options.live(request, pathname + search, context);
|
|
120
|
+
if (live) return finish(live);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const decoded = safeDecode(pathname);
|
|
124
|
+
if (decoded === null) return finish(new Response("400 | Bad Request", { status: 400 }));
|
|
125
|
+
const api = matchApi(options.manifest, decoded);
|
|
126
|
+
const mod = api ? options.handlers[api.id] : undefined;
|
|
127
|
+
if (api && mod) {
|
|
128
|
+
const target = new URL(pathname + search, url.origin);
|
|
129
|
+
const req = target.href === request.url ? request : rebuild(request, new Headers(request.headers), target);
|
|
130
|
+
return finish(await runRouteHandlerWithCache(mod, req, { params: api.params, pattern: api.pattern, pathname: decoded, waitUntil: context.waitUntil }));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const method = request.method.toUpperCase();
|
|
134
|
+
if (method !== "GET" && method !== "HEAD") return finish(new Response(null, { status: 405, headers: { allow: "GET, HEAD" } }));
|
|
135
|
+
const hidden = decoded.split(/[/\\]/).some((p) => p.startsWith(".") && p.length > 1);
|
|
136
|
+
if (!hidden && options.assets) {
|
|
137
|
+
const asset = await options.assets(decoded, request, context);
|
|
138
|
+
if (asset) return finish(asset);
|
|
139
|
+
}
|
|
140
|
+
const last = decoded.split("/").pop() ?? "";
|
|
141
|
+
if (last.includes(".") || hidden || !options.document) return finish(notFound());
|
|
142
|
+
const doc = await options.document(request, context);
|
|
143
|
+
return finish(doc ?? notFound());
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared adapter plumbing: the build context, generated server entries and
|
|
3
|
+
* route-pattern → regular-expression helpers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import type { Rewrite, TextConfig } from "../../config";
|
|
9
|
+
import { parseSegment } from "../../routing";
|
|
10
|
+
import { generateApiModule } from "../../vite-plugin";
|
|
11
|
+
import type { ApiRoute, AppScan } from "../scan";
|
|
12
|
+
import { txError } from "@textjs/core";
|
|
13
|
+
import { compileMatcherSource, matcherKeys } from "../middleware-runtime";
|
|
14
|
+
import { listServerPages } from "../live-routes";
|
|
15
|
+
import { frameworkFile } from "./bundle";
|
|
16
|
+
|
|
17
|
+
export const ADAPTERS = ["static", "bun", "vercel", "cloudflare"] as const;
|
|
18
|
+
export type AdapterName = (typeof ADAPTERS)[number];
|
|
19
|
+
|
|
20
|
+
export interface AdapterContext {
|
|
21
|
+
root: string;
|
|
22
|
+
/** The client build (`dist/`). */
|
|
23
|
+
outDir: string;
|
|
24
|
+
config: TextConfig;
|
|
25
|
+
scan: AppScan;
|
|
26
|
+
api: ApiRoute[];
|
|
27
|
+
basePath: string;
|
|
28
|
+
/** Config rewrites, resolved at build time. */
|
|
29
|
+
rewrites: Rewrite[];
|
|
30
|
+
middlewareFile: string | null;
|
|
31
|
+
cacheHandlerFile: string | null;
|
|
32
|
+
print(line?: string): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AdapterResult {
|
|
36
|
+
adapter: AdapterName;
|
|
37
|
+
/** Deployable output directory. */
|
|
38
|
+
output: string;
|
|
39
|
+
/** Lines for the build summary. */
|
|
40
|
+
notes: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isAdapterName(value: unknown): value is AdapterName {
|
|
44
|
+
return typeof value === "string" && (ADAPTERS as readonly string[]).includes(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The adapter implied by the build environment (`VERCEL=1`, `CF_PAGES=1`). */
|
|
48
|
+
export function detectAdapter(env: Record<string, string | undefined> = process.env): AdapterName | undefined {
|
|
49
|
+
if (env.VERCEL === "1") return "vercel";
|
|
50
|
+
if (env.CF_PAGES === "1") return "cloudflare";
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const json = JSON.stringify;
|
|
55
|
+
|
|
56
|
+
export interface EntryOptions {
|
|
57
|
+
/** Extra source appended after `options` is declared. */
|
|
58
|
+
body: string;
|
|
59
|
+
/** Import the route handler modules (default `true`). */
|
|
60
|
+
handlers?: boolean;
|
|
61
|
+
/** Import `middleware.ts` (default `true`). */
|
|
62
|
+
middleware?: boolean;
|
|
63
|
+
/** Import `createRequestHandler` (default `true`). */
|
|
64
|
+
runtime?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The common entry prelude: route handlers (`manifest`, `handlers`), the
|
|
69
|
+
* middleware module, the cache store and `options` for `createRequestHandler`.
|
|
70
|
+
*/
|
|
71
|
+
export function serverEntry(ctx: AdapterContext, opts: EntryOptions): string {
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
const withHandlers = opts.handlers !== false;
|
|
74
|
+
const withMiddleware = opts.middleware !== false && ctx.middlewareFile !== null;
|
|
75
|
+
if (withHandlers) lines.push(generateApiModule(ctx.scan));
|
|
76
|
+
else lines.push("export const manifest = { segment: \"\" };", "export const handlers = {};");
|
|
77
|
+
if (opts.runtime !== false) lines.push(`import { createRequestHandler } from ${json(frameworkFile("server/adapters/runtime.ts"))};`);
|
|
78
|
+
if (withMiddleware) lines.push(`import * as middleware from ${json(ctx.middlewareFile)};`);
|
|
79
|
+
const withStore = ctx.cacheHandlerFile !== null && opts.runtime !== false;
|
|
80
|
+
if (withStore) lines.push(`import cacheStore from ${json(ctx.cacheHandlerFile)};`);
|
|
81
|
+
lines.push(
|
|
82
|
+
`const options = { manifest, handlers, basePath: ${json(ctx.basePath)}, rewrites: ${json(ctx.rewrites)}${withMiddleware ? ", middleware" : ""}${withStore ? ", cacheStore" : ""} };`,
|
|
83
|
+
opts.body,
|
|
84
|
+
);
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function escapeRegex(s: string): string {
|
|
89
|
+
return s.replace(/[.+*?^${}()|[\]\\/]/g, "\\$&");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** App route pattern (`/api/posts/[id]`, `/api/[...slug]`) → anchored regex source with one outer capture. */
|
|
93
|
+
export function patternRegex(pattern: string, basePath = ""): string {
|
|
94
|
+
let body = "";
|
|
95
|
+
for (const seg of pattern.split("/").filter(Boolean)) {
|
|
96
|
+
const p = parseSegment(seg);
|
|
97
|
+
if (p.kind === "group") continue;
|
|
98
|
+
if (p.kind === "static") body += `\\/${escapeRegex(p.name)}`;
|
|
99
|
+
else if (p.kind === "dynamic") body += "\\/[^/]+";
|
|
100
|
+
else if (p.kind === "catchAll") body += "\\/[^/]+(?:\\/[^/]+)*";
|
|
101
|
+
else body += "(?:\\/[^/]+(?:\\/[^/]+)*)?";
|
|
102
|
+
}
|
|
103
|
+
return `^(${escapeRegex(basePath)}${body})\\/?$`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A middleware matcher, prefixed with basePath, as an anchored regex source. */
|
|
107
|
+
export function matcherRegex(source: string, basePath = ""): string {
|
|
108
|
+
const re = compileMatcherSource(source);
|
|
109
|
+
return `^${escapeRegex(basePath)}${re.slice(1)}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Config rewrite `source` → `{ src, dest }` with `$n` substitutions. */
|
|
113
|
+
export function rewriteRoute(rewrite: Rewrite, basePath = ""): { src: string; dest: string } {
|
|
114
|
+
// Capture order, including unnamed groups (`/(.*)`) and groups inside custom patterns.
|
|
115
|
+
const names = matcherKeys(rewrite.source);
|
|
116
|
+
const src = matcherRegex(rewrite.source, basePath);
|
|
117
|
+
const external = /^[a-z]+:\/\//i.test(rewrite.destination);
|
|
118
|
+
let dest = rewrite.destination.replace(/:([A-Za-z_]\w*)[*+?]?/g, (_, n: string) => {
|
|
119
|
+
const i = names.indexOf(n);
|
|
120
|
+
return i < 0 ? "" : `$${i + 1}`;
|
|
121
|
+
});
|
|
122
|
+
if (!external) dest = basePath + dest;
|
|
123
|
+
return { src, dest };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Copy `from` into `to`, skipping the `.text/` build metadata directory. */
|
|
127
|
+
export function copyStatic(from: string, to: string, skip: (rel: string) => boolean = () => false): void {
|
|
128
|
+
mkdirSync(to, { recursive: true });
|
|
129
|
+
const walk = (dir: string, rel: string): void => {
|
|
130
|
+
for (const name of readdirSync(dir)) {
|
|
131
|
+
const r = rel ? `${rel}/${name}` : name;
|
|
132
|
+
if (r === ".text" || skip(r)) continue;
|
|
133
|
+
const src = join(dir, name);
|
|
134
|
+
const dest = join(to, r);
|
|
135
|
+
if (statSync(src).isDirectory()) {
|
|
136
|
+
mkdirSync(dest, { recursive: true });
|
|
137
|
+
walk(src, r);
|
|
138
|
+
} else cpSync(src, dest);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
walk(from, "");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Remove `.text/` (server bundles and build manifests) from output a static host publishes as-is. */
|
|
145
|
+
export function removeServerBundle(outDir: string): void {
|
|
146
|
+
const dir = join(outDir, ".text");
|
|
147
|
+
if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Patterns of `export const runtime = "server"` pages. */
|
|
151
|
+
export function serverRoutePatterns(ctx: Pick<AdapterContext, "scan">): string[] {
|
|
152
|
+
return listServerPages(ctx.scan).map((p) => p.pattern);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Server-rendered routes keep a live session per tab: refuse platforms without a long-lived server. */
|
|
156
|
+
export function assertNoServerRoutes(ctx: Pick<AdapterContext, "scan">, adapter: AdapterName): void {
|
|
157
|
+
const routes = serverRoutePatterns(ctx);
|
|
158
|
+
if (routes.length > 0) throw txError("TX030", routes.join(", "), adapter);
|
|
159
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `text build --adapter static`: a pure static export. Static `GET` route
|
|
3
|
+
* handlers (`dynamic = "force-static"` / `revalidate`) are prerendered into
|
|
4
|
+
* files; anything that needs a server is a build error.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
import { isDynamicPattern, matchAppRoutes, type RouteManifest, type RouteSegmentNode } from "../../routing";
|
|
11
|
+
import { routeCacheConfig, runRouteHandlerWithCache } from "../cache-route";
|
|
12
|
+
import type { RouteHandlerModule } from "../http";
|
|
13
|
+
import type { MiddlewareModule } from "../middleware";
|
|
14
|
+
import { removeServerBundle, serverRoutePatterns, type AdapterContext, type AdapterResult } from "./shared";
|
|
15
|
+
|
|
16
|
+
export async function adaptStatic(ctx: AdapterContext, middleware: MiddlewareModule | null, apiBundle: string): Promise<AdapterResult> {
|
|
17
|
+
const problems: string[] = [];
|
|
18
|
+
if (middleware) problems.push("middleware.ts needs a server. Use --adapter bun, vercel or cloudflare (or `text start`).");
|
|
19
|
+
for (const pattern of serverRoutePatterns(ctx)) problems.push(`${pattern}: renders on the server (\`export const runtime = "server"\`). Use --adapter bun (or \`text start\`).`);
|
|
20
|
+
const notes: string[] = [];
|
|
21
|
+
if (ctx.rewrites.length > 0) notes.push("rewrites in text.config.ts are not applied by static hosts; configure them on your host.");
|
|
22
|
+
|
|
23
|
+
const written: string[] = [];
|
|
24
|
+
if (ctx.api.length > 0 && problems.length === 0) {
|
|
25
|
+
const bundle = (await import(`${pathToFileURL(apiBundle).href}?t=${Date.now()}`)) as { manifest: RouteManifest; handlers: Record<string, RouteHandlerModule> };
|
|
26
|
+
for (const route of ctx.api) {
|
|
27
|
+
if (isDynamicPattern(route.pattern)) {
|
|
28
|
+
problems.push(`${route.pattern}: dynamic segments need a server.`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const m = matchAppRoutes(bundle.manifest, route.pattern);
|
|
32
|
+
const id = m ? (m.chain[m.chain.length - 1] as RouteSegmentNode).files?.page : undefined;
|
|
33
|
+
const mod = id ? bundle.handlers[id] : undefined;
|
|
34
|
+
if (!mod) continue;
|
|
35
|
+
const others = ["POST", "PUT", "PATCH", "DELETE"].filter((k) => typeof mod[k] === "function");
|
|
36
|
+
if (typeof mod.GET !== "function" || others.length > 0) {
|
|
37
|
+
problems.push(`${route.pattern}: only GET route handlers can be exported statically${others.length ? ` (found ${others.join(", ")})` : ""}.`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!routeCacheConfig(mod, `${route.pattern}/route`).static) {
|
|
41
|
+
problems.push(`${route.pattern}: the route handler is dynamic. Add \`export const dynamic = "force-static"\` to prerender it.`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const res = await runRouteHandlerWithCache(mod, new Request(`http://localhost${ctx.basePath}${route.pattern}`), { params: {}, pattern: route.pattern, pathname: route.pattern, cache: false });
|
|
45
|
+
if (!res.ok) {
|
|
46
|
+
problems.push(`${route.pattern}: prerendering returned ${res.status}.`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const file = join(ctx.outDir, ...route.pattern.split("/").filter(Boolean));
|
|
50
|
+
if (existsSync(file) && statSync(file).isDirectory()) {
|
|
51
|
+
problems.push(`${route.pattern}: conflicts with a directory of the same name in the output.`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
55
|
+
writeFileSync(file, new Uint8Array(await res.arrayBuffer()));
|
|
56
|
+
written.push(route.pattern);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (problems.length > 0) {
|
|
60
|
+
throw new Error(`▼ Text.js: the static adapter can't export this app:\n${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
61
|
+
}
|
|
62
|
+
removeServerBundle(ctx.outDir);
|
|
63
|
+
// SPA fallback for hosts that serve 404.html for unknown paths (GitHub Pages, S3, …).
|
|
64
|
+
const fallback = join(ctx.outDir, "404.html");
|
|
65
|
+
if (!existsSync(fallback)) copyFileSync(join(ctx.outDir, "index.html"), fallback);
|
|
66
|
+
if (written.length > 0) notes.unshift(`Prerendered ${written.length} route handler${written.length === 1 ? "" : "s"}: ${written.join(", ")}`);
|
|
67
|
+
return { adapter: "static", output: ctx.outDir, notes };
|
|
68
|
+
}
|