| 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 | */ |
| 30 | import type { Context, MiddlewareHandler, Next } from "hono"; |
| 31 | import { bodyLimit } from "hono/body-limit"; |
| 32 | import { Hono } from "hono/tiny"; |
| 33 | import * as NodePath from "node:path"; |
| 34 | import { unstable_createServerEntryAdapter as createServerEntryAdapter } from "waku/adapter-builders"; |
| 35 | import { |
| 36 | unstable_constants as constants, |
| 37 | unstable_honoMiddleware as honoMiddleware, |
| 38 | } from "waku/internals"; |
| 39 | import { |
| 40 | toBufferedLambdaHandler, |
| 41 | toLambdaHandler, |
| 42 | type FetchHandler, |
| 43 | } from "../aws-lambda/index.ts"; |
| 44 | |
| 45 | const { DIST_PUBLIC } = constants; |
| 46 | const { rscMiddleware, middlewareRunner } = honoMiddleware; |
| 47 | |
| 48 | import { AWS_LAMBDA_HANDLE_GLOBAL } from "./aws-shared.ts"; |
| 49 | |
| 50 | export { AWS_LAMBDA_HANDLE_GLOBAL } from "./aws-shared.ts"; |
| 51 | |
| 52 | const DEFAULT_BODY_LIMIT_MAX_SIZE = 100 * 1024 * 1024; |
| 53 | |
| 54 | const DO_NOT_BUNDLE = ""; |
| 55 | |
| 56 | const 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 | */ |
| 82 | const 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). */ |
| 117 | export 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 | */ |
| 136 | const 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 | |
| 197 | export default awsLambdaAdapter; |