1/**
2 * AWS Lambda adapter for waku, forked from waku's own aws-lambda adapter
3 * (upstream `packages/waku/src/adapters/aws-lambda.ts` @ waku 1.0.0-beta.7).
4 *
5 * Functional changes from upstream:
6 *
7 * - The Lambda handler wrapper is this package's own streaming adapter
8 * (`../aws-lambda/index.ts` — `awslambda.streamifyResponse` +
9 * `HttpResponseStream` for a Function URL with `invokeMode:
10 * RESPONSE_STREAM`; `toBufferedLambdaHandler` when `streaming: false`)
11 * instead of `hono/aws-lambda`'s `streamHandle`/`handle`. The wrapper is
12 * published on `globalThis.__ALCHEMY_WAKU_AWS_LAMBDA_HANDLE__` at
13 * server-entry module scope, mirroring upstream's
14 * `__WAKU_AWS_LAMBDA_HANDLE__` global trick — the serve entry that reads
15 * it stays free of bare imports, so the shipped `dist/server` directory
16 * needs no `node_modules`.
17 * - `buildEnhancers` is empty: upstream's enhancer writes the
18 * `serve-aws-lambda.js` entry via a `waku/adapters/*` module resolved from
19 * the project, which is not guaranteed to exist for the pinned waku
20 * version's export map. The AWS deploy target's finishing pass
21 * (`./aws.ts`) writes the equivalent serve entry instead.
22 *
23 * Everything else (Hono app, rsc middleware, SSG static serving during
24 * build) is behavior-identical to upstream.
25 *
26 * This module is bundled into the Lambda server bundle by waku's vite
27 * pipeline (selected via the in-memory `Config.unstable_adapter`); it is not
28 * Node service code.
29 */
30import type { Context, MiddlewareHandler, Next } from "hono";
31import { bodyLimit } from "hono/body-limit";
32import { Hono } from "hono/tiny";
33import * as NodePath from "node:path";
34import { unstable_createServerEntryAdapter as createServerEntryAdapter } from "waku/adapter-builders";
35import {
36 unstable_constants as constants,
37 unstable_honoMiddleware as honoMiddleware,
38} from "waku/internals";
39import {
40 toBufferedLambdaHandler,
41 toLambdaHandler,
42 type FetchHandler,
43} from "../aws-lambda/index.ts";
44
45const { DIST_PUBLIC } = constants;
46const { rscMiddleware, middlewareRunner } = honoMiddleware;
47
48import { AWS_LAMBDA_HANDLE_GLOBAL } from "./aws-shared.ts";
49
50export { AWS_LAMBDA_HANDLE_GLOBAL } from "./aws-shared.ts";
51
52const DEFAULT_BODY_LIMIT_MAX_SIZE = 100 * 1024 * 1024;
53
54const DO_NOT_BUNDLE = "";
55
56const MIME_TYPES: Record<string, string> = {
57 ".html": "text/html; charset=utf-8",
58 ".js": "text/javascript; charset=utf-8",
59 ".mjs": "text/javascript; charset=utf-8",
60 ".css": "text/css; charset=utf-8",
61 ".json": "application/json",
62 ".txt": "text/plain; charset=utf-8",
63 ".svg": "image/svg+xml",
64 ".png": "image/png",
65 ".jpg": "image/jpeg",
66 ".jpeg": "image/jpeg",
67 ".gif": "image/gif",
68 ".ico": "image/x-icon",
69 ".webp": "image/webp",
70 ".woff": "font/woff",
71 ".woff2": "font/woff2",
72 ".wasm": "application/wasm",
73};
74
75/**
76 * Minimal static-file middleware over `node:fs`, replacing upstream's
77 * `@hono/node-server/serve-static` (a waku dependency this package cannot
78 * rely on resolving from the built server bundle). Only ever active during
79 * the SSG pass (`isBuild`), which runs in Node — the dynamic `node:fs`
80 * import keeps the module loadable on any runtime.
81 */
82const makeBuildStaticMiddleware = (root: string): MiddlewareHandler => {
83 return async (c: Context, next: Next) => {
84 if (c.req.method !== "GET" && c.req.method !== "HEAD") {
85 return next();
86 }
87 const fs = (await import(
88 /* @vite-ignore */ DO_NOT_BUNDLE + "node:fs/promises"
89 )) as typeof import("node:fs/promises");
90 let pathname: string;
91 try {
92 pathname = decodeURIComponent(new URL(c.req.url).pathname);
93 } catch {
94 return next();
95 }
96 if (pathname.includes("..")) {
97 return next();
98 }
99 if (pathname.endsWith("/")) {
100 pathname += "index.html";
101 }
102 const filePath = NodePath.join(root, pathname);
103 let data: Uint8Array;
104 try {
105 data = await fs.readFile(filePath);
106 } catch {
107 return next();
108 }
109 const contentType =
110 MIME_TYPES[NodePath.extname(filePath).toLowerCase()] ??
111 "application/octet-stream";
112 return c.body(data as never, 200, { "content-type": contentType });
113 };
114};
115
116/** Options accepted by the adapter (superset of upstream's). */
117export interface AwsLambdaAdapterOptions {
118 /**
119 * Whether the emitted Lambda handler streams its response
120 * (`awslambda.streamifyResponse`, requires a Function URL with
121 * `invokeMode: RESPONSE_STREAM`). `false` produces the buffered
122 * payload-v2 handler.
123 * @default true
124 */
125 streaming?: boolean;
126 bodyLimit?: Parameters<typeof bodyLimit>[0] | false;
127 middlewareFns?: Array<(opts: { app: Hono }) => MiddlewareHandler>;
128 middlewareModules?: Record<string, () => Promise<unknown>>;
129}
130
131/**
132 * Annotated via an instantiation expression over the public
133 * `createServerEntryAdapter` so the emitted declaration never references
134 * waku's non-exported `lib/types` module (TS2883).
135 */
136const awsLambdaAdapter: ReturnType<
137 typeof createServerEntryAdapter<AwsLambdaAdapterOptions | undefined>
138> = createServerEntryAdapter(
139 (
140 { processRequest, processBuild, config, isBuild, notFoundHtml },
141 options?: AwsLambdaAdapterOptions,
142 ) => {
143 const {
144 bodyLimit: bodyLimitOptions,
145 middlewareFns = [],
146 middlewareModules = {},
147 } = options || {};
148 const app = new Hono();
149 app.notFound((c) => {
150 if (notFoundHtml) {
151 return c.html(notFoundHtml, 404);
152 }
153 return c.text("404 Not Found", 404);
154 });
155 if (isBuild) {
156 // SSG pass only (runs in Node during `waku build`): serve the already
157 // emitted static files so prerender requests resolve assets. Never on
158 // Lambda — there the CDN serves `dist/public`.
159 app.use(
160 makeBuildStaticMiddleware(NodePath.join(config.distDir, DIST_PUBLIC)),
161 );
162 }
163 if (bodyLimitOptions !== false) {
164 app.use(
165 bodyLimit(bodyLimitOptions ?? { maxSize: DEFAULT_BODY_LIMIT_MAX_SIZE }),
166 );
167 }
168 for (const middlewareFn of middlewareFns) {
169 app.use(middlewareFn({ app }));
170 }
171 app.use(middlewareRunner(middlewareModules as never, { app }));
172 app.use(rscMiddleware({ processRequest }));
173
174 // The serve entry (written by the AWS target's finishing pass) passes its
175 // own `{ streaming }` choice — the target config's knob — with the
176 // adapter option as fallback.
177 (globalThis as Record<string, unknown>)[AWS_LAMBDA_HANDLE_GLOBAL] = (
178 fetchHandler: FetchHandler,
179 serveOptions?: { streaming?: boolean },
180 ) =>
181 (serveOptions?.streaming ?? options?.streaming ?? true)
182 ? toLambdaHandler(fetchHandler)
183 : toBufferedLambdaHandler(fetchHandler);
184
185 return {
186 fetch: app.fetch,
187 build: processBuild,
188 buildOptions: { distDir: config.distDir },
189 // Upstream lists `waku/adapters/aws-lambda-build-enhancer` here (the
190 // serve-entry writer). Dropped: the AWS deploy target's finishing pass
191 // owns the serve entry.
192 buildEnhancers: [],
193 };
194 },
195);
196
197export default awsLambdaAdapter;