From b4908f79c76c7c0d45064f9b49f2efdae4bef48f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20L=C3=A1z=C3=A1r?= Date: Sun, 1 Mar 2026 00:34:54 +0100 Subject: [PATCH 1/4] feat: aws deployment adapter --- .gitignore | 3 + packages/create-react-server/steps/deploy.mjs | 3 +- .../adapters/aws/functions/handler.mjs | 330 +++++++++++++++ packages/react-server/adapters/aws/index.mjs | 379 ++++++++++++++++++ packages/react-server/package.json | 4 + 5 files changed, 718 insertions(+), 1 deletion(-) create mode 100644 packages/react-server/adapters/aws/functions/handler.mjs create mode 100644 packages/react-server/adapters/aws/index.mjs diff --git a/.gitignore b/.gitignore index e8517e85..407cee73 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ netlify.toml deno.lock .bun .deno +.aws +template.json +samconfig.toml *.pem *.sqlite *.log diff --git a/packages/create-react-server/steps/deploy.mjs b/packages/create-react-server/steps/deploy.mjs index fc4ead40..c1e2742e 100644 --- a/packages/create-react-server/steps/deploy.mjs +++ b/packages/create-react-server/steps/deploy.mjs @@ -57,7 +57,6 @@ export default async (context) => { name: "AWS", value: "aws", description: "Deploy to AWS Lambda", - disabled: "(coming soon)", }, ], theme, @@ -75,6 +74,7 @@ export default async (context) => { cloudflare: "Cloudflare Workers/Pages", bun: "Bun", deno: "Deno", + aws: "AWS Lambda", }; const adapterIgnore = { vercel: [".vercel", "vercel.json"], @@ -82,6 +82,7 @@ export default async (context) => { cloudflare: [".cloudflare", ".wrangler", "wrangler.toml"], bun: [".bun"], deno: [".deno"], + aws: [".aws", "template.json", "samconfig.toml"], }; if (adapter in adapterIgnore) { diff --git a/packages/react-server/adapters/aws/functions/handler.mjs b/packages/react-server/adapters/aws/functions/handler.mjs new file mode 100644 index 00000000..1b21cb36 --- /dev/null +++ b/packages/react-server/adapters/aws/functions/handler.mjs @@ -0,0 +1,330 @@ +import { readFileSync, statSync } from "node:fs"; +import { join, extname } from "node:path"; +import { reactServer } from "@lazarv/react-server/edge"; +import { createContext } from "@lazarv/react-server/http"; + +let serverPromise = null; + +/** + * Static file manifest loaded at cold start. + * Maps URL paths to relative file paths on disk. + * Generated at build time by the adapter. + */ +/** + * Lambda extracts the deployment package to /var/task. + * process.cwd() also returns /var/task in Lambda. + */ +const TASK_ROOT = process.env.LAMBDA_TASK_ROOT || process.cwd(); + +let staticManifest = null; +try { + staticManifest = JSON.parse( + readFileSync(join(TASK_ROOT, "static-manifest.json"), "utf-8") + ); +} catch { + // No manifest — all requests go to SSR + staticManifest = {}; +} + +/** + * Common MIME types for web assets. + */ +const MIME_TYPES = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".xml": "application/xml; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".avif": "image/avif", + ".ico": "image/x-icon", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".ttf": "font/ttf", + ".otf": "font/otf", + ".eot": "application/vnd.ms-fontobject", + ".txt": "text/plain; charset=utf-8", + ".map": "application/json; charset=utf-8", + ".wasm": "application/wasm", + ".mp4": "video/mp4", + ".webm": "video/webm", + ".mp3": "audio/mpeg", + ".ogg": "audio/ogg", + ".pdf": "application/pdf", + ".gz": "application/gzip", + ".br": "application/x-brotli", + ".x-component": "text/x-component; charset=utf-8", +}; + +/** + * Cache-control based on file type and path. + * - Build assets / client components (content-hashed): immutable, 1 year + * - HTML / x-component: must-revalidate + * - Everything else: 10 minutes + */ +function getCacheControl(urlPath, contentType) { + if ( + contentType?.includes("text/html") || + contentType?.includes("text/x-component") + ) { + return "must-revalidate"; + } + if (urlPath.startsWith("/assets/") || urlPath.startsWith("/client/")) { + return "public, max-age=31536000, immutable"; + } + return "public, max-age=600"; +} + +/** + * Root of static files inside the Lambda deployment package. + */ +const staticDir = join(TASK_ROOT, "static"); + +/** + * Serve a static file if the URL matches the build-time manifest. + * Returns a Response or null. + */ +function tryServeStatic(urlPath) { + let entry = staticManifest[urlPath]; + + // Try /foo → /foo/index.html + if (!entry) { + const withIndex = urlPath.endsWith("/") + ? urlPath + "index.html" + : urlPath + "/index.html"; + entry = staticManifest[withIndex]; + if (entry) urlPath = withIndex; + } + + if (!entry) return null; + + const filePath = join(staticDir, entry); + try { + const stat = statSync(filePath); + if (!stat.isFile()) return null; + + const ext = extname(entry); + const contentType = MIME_TYPES[ext] || "application/octet-stream"; + const body = readFileSync(filePath); + + return new Response(body, { + status: 200, + headers: { + "content-type": contentType, + "content-length": String(stat.size), + "cache-control": getCacheControl(urlPath, contentType), + etag: `W/"${stat.size}-${stat.mtimeMs | 0}"`, + "last-modified": stat.mtime.toUTCString(), + }, + }); + } catch { + return null; + } +} + +/** + * Build a standard Request from a Lambda event (API Gateway v2 / Function URL). + */ +function buildRequest(event) { + const origin = + process.env.ORIGIN || + `${event.headers?.["x-forwarded-proto"] || "https"}://${event.requestContext?.domainName || event.headers?.host || "localhost"}`; + + const url = new URL( + event.rawPath + (event.rawQueryString ? `?${event.rawQueryString}` : ""), + origin + ); + + const method = + event.requestContext?.http?.method || event.httpMethod || "GET"; + const headers = new Headers(event.headers || {}); + + if (event.cookies) { + headers.set("cookie", event.cookies.join("; ")); + } + + const init = { method, headers }; + if (method !== "GET" && method !== "HEAD" && event.body) { + init.body = event.isBase64Encoded + ? Buffer.from(event.body, "base64") + : event.body; + } + + return { request: new Request(url.toString(), init), origin }; +} + +/** + * Main request handler. + * 1. Static files (GET only) — served from the Lambda filesystem, zero SSR overhead + * 2. Everything else — SSR via react-server + * + * CloudFront caches responses based on the Cache-Control headers set above, + * so after the first request each static file is served from edge cache. + */ +async function handleRequest(event, context) { + const { request, origin } = buildRequest(event); + + // ---- Static files (GET only) ---- + if (request.method === "GET") { + const url = new URL(request.url); + const staticResponse = tryServeStatic(url.pathname); + if (staticResponse) return staticResponse; + } + + // ---- SSR via react-server ---- + try { + if (!serverPromise) { + serverPromise = reactServer({ + origin, + outDir: "./.react-server", + }); + } + + const { handler } = await serverPromise; + + const httpContext = createContext(request, { + origin, + runtime: "aws-lambda", + platformExtras: { event, context }, + }); + + const response = await handler(httpContext); + + if (!response) { + return new Response("Not Found", { + status: 404, + headers: { "content-type": "text/plain" }, + }); + } + + if (httpContext._setCookies?.length) { + const headers = new Headers(response.headers); + for (const c of httpContext._setCookies) { + headers.append("set-cookie", c); + } + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers, + }); + } + + return response; + } catch (e) { + console.error(e); + return new Response(e.message || "Internal Server Error", { + status: 500, + headers: { "content-type": "text/plain" }, + }); + } +} + +/** + * Convert a Web Response to API Gateway v2 proxy result (buffered). + */ +async function toApiGatewayResponse(response) { + const headers = {}; + const cookies = []; + + response.headers.forEach((value, key) => { + if (key.toLowerCase() === "set-cookie") { + cookies.push(value); + } else { + headers[key] = value; + } + }); + + const contentType = response.headers.get("content-type") || ""; + const isBinary = + !contentType.startsWith("text/") && + !contentType.includes("json") && + !contentType.includes("xml") && + !contentType.includes("javascript") && + !contentType.includes("x-component"); + + let body; + let isBase64Encoded = false; + + if (isBinary) { + const arrayBuffer = await response.arrayBuffer(); + body = Buffer.from(arrayBuffer).toString("base64"); + isBase64Encoded = true; + } else { + body = await response.text(); + } + + return { + statusCode: response.status, + headers, + ...(cookies.length > 0 ? { cookies } : {}), + body, + isBase64Encoded, + }; +} + +/** + * Streaming handler — used with Lambda Function URLs / RESPONSE_STREAM invoke mode. + */ +function createStreamingHandler() { + return awslambda.streamifyResponse(async (event, responseStream, context) => { + const response = await handleRequest(event, context); + + if (!(response instanceof Response)) { + responseStream = awslambda.HttpResponseStream.from(responseStream, { + statusCode: response.statusCode || 404, + headers: response.headers || {}, + }); + responseStream.write(response.body || "Not Found"); + responseStream.end(); + return; + } + + const responseHeaders = Object.fromEntries(response.headers.entries()); + const setCookies = [...response.headers.entries()] + .filter(([key]) => key.toLowerCase() === "set-cookie") + .map(([, value]) => value); + + responseStream = awslambda.HttpResponseStream.from(responseStream, { + statusCode: response.status, + headers: responseHeaders, + ...(setCookies.length > 0 ? { cookies: setCookies } : {}), + }); + + if (response.body) { + const reader = response.body.getReader(); + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + responseStream.write(value); + } + } finally { + reader.releaseLock(); + } + } + + responseStream.end(); + }); +} + +/** + * Buffered handler — used with standard API Gateway v2. + */ +async function standardHandler(event, context) { + const response = await handleRequest(event, context); + if (!(response instanceof Response)) return response; + return toApiGatewayResponse(response); +} + +// Use streaming when available, otherwise buffered. +export const handler = + typeof globalThis.awslambda !== "undefined" && + typeof globalThis.awslambda.streamifyResponse === "function" + ? createStreamingHandler() + : standardHandler; diff --git a/packages/react-server/adapters/aws/index.mjs b/packages/react-server/adapters/aws/index.mjs new file mode 100644 index 00000000..d9c0c304 --- /dev/null +++ b/packages/react-server/adapters/aws/index.mjs @@ -0,0 +1,379 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { cp, mkdir } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import * as sys from "@lazarv/react-server/lib/sys.mjs"; +import { + banner, + clearDirectory, + createAdapter, + message, + success, + writeJSON, +} from "@lazarv/react-server/adapters/core"; + +const cwd = sys.cwd(); +const outDir = join(cwd, ".aws"); +const outStaticDir = join(outDir, "static"); +const functionsDir = join(outDir, "functions"); +const adapterDir = dirname(fileURLToPath(import.meta.url)); + +/** + * Build options that the AWS Lambda adapter requires. + * Uses edge build mode to bundle the server into a single file for Lambda deployment. + */ +export const buildOptions = { + edge: { + entry: join(adapterDir, "functions/handler.mjs"), + }, +}; + +export const adapter = createAdapter({ + name: "AWS Lambda", + outDir, + outStaticDir, + handler: async function ({ adapterOptions, files, options }) { + banner("building AWS Lambda function", { emoji: "⚡" }); + + const outServerDir = join(functionsDir, "server"); + await clearDirectory(outServerDir); + + // Copy bundled server files + await mkdir(join(outServerDir, ".react-server/server"), { + recursive: true, + }); + await cp( + join(cwd, ".react-server/server/edge.mjs"), + join(outServerDir, ".react-server/server/edge.mjs") + ); + + // Copy source map file if sourcemaps are enabled + if (options.sourcemap) { + const edgeMapPath = join(cwd, ".react-server/server/edge.mjs.map"); + if (existsSync(edgeMapPath)) { + await cp( + edgeMapPath, + join(outServerDir, ".react-server/server/edge.mjs.map") + ); + } + } + + // Copy ALL static files into the Lambda deployment package. + // The Lambda handler serves static files directly from disk with proper + // Cache-Control headers. CloudFront caches responses at the edge, so + // after the first request each static file is served from edge cache + // without invoking Lambda. + // + // This avoids: + // - CloudFront Function size limits (10KB) + // - Origin Group failover limitations (GET/HEAD only) + // - Hardcoded path patterns that could conflict with user routes + banner("bundling static files into Lambda package", { emoji: "📦" }); + + // outStaticDir was already populated by createAdapter (copy.static, + // copy.assets, copy.client, copy.public). Copy the assembled tree + // into the Lambda package. + await cp(outStaticDir, join(outServerDir, "static"), { recursive: true }); + + // Build the static file manifest: URL path → relative file path on disk. + // The Lambda handler loads this at cold-start for O(1) static file lookups. + const [staticFiles, assetFiles, clientFiles, publicFiles] = + await Promise.all([ + files.static(), + files.assets(), + files.client(), + files.public(), + ]); + + const manifest = {}; + for (const f of staticFiles) manifest[`/${f}`] = f; + for (const f of assetFiles) manifest[`/${f}`] = f; + for (const f of clientFiles) manifest[`/${f}`] = f; + for (const f of publicFiles) manifest[`/${f}`] = f; + + writeFileSync( + join(outServerDir, "static-manifest.json"), + JSON.stringify(manifest) + ); + + success(`${Object.keys(manifest).length} static files bundled`); + + message("creating", "Lambda function handler"); + + // Create index.mjs entry point that re-exports from the bundled server + writeFileSync( + join(outServerDir, "index.mjs"), + `export { handler } from "./.react-server/server/edge.mjs";\n` + ); + + // Create package.json for ESM support in Lambda + writeFileSync( + join(outServerDir, "package.json"), + JSON.stringify({ type: "module" }, null, 2) + ); + + success("Lambda function created"); + + // ---- SAM template ---- + + banner("creating AWS deployment configuration", { emoji: "⚙️" }); + + // Resolve application name from adapter options or package.json + let appName = adapterOptions?.name; + if (!appName) { + const packageJsonPath = join(cwd, "package.json"); + if (existsSync(packageJsonPath)) { + try { + const packageJson = JSON.parse( + readFileSync(packageJsonPath, "utf-8") + ); + appName = packageJson.name?.replace(/^@[^/]+\//, ""); + } catch { + // Ignore parsing errors + } + } + } + appName = appName ?? "react-server-app"; + + // Sanitize app name for use as CloudFormation resource names + const sanitizedName = appName.replace(/[^a-zA-Z0-9]/g, ""); + const stackName = adapterOptions?.stackName ?? appName; + + const runtime = adapterOptions?.runtime ?? "nodejs20.x"; + const memorySize = adapterOptions?.memorySize ?? 1024; + const timeout = adapterOptions?.timeout ?? 30; + const architecture = adapterOptions?.architecture ?? "arm64"; + + const samTemplate = { + AWSTemplateFormatVersion: "2010-09-09", + Transform: "AWS::Serverless-2016-10-31", + Description: `${appName} - deployed with @lazarv/react-server`, + Globals: { + Function: { + Timeout: timeout, + Runtime: runtime, + MemorySize: memorySize, + Architectures: [architecture], + Environment: { + Variables: { + NODE_ENV: "production", + ...(options.sourcemap + ? { NODE_OPTIONS: "--enable-source-maps" } + : {}), + ...adapterOptions?.environment, + }, + }, + }, + }, + Resources: { + // Lambda function with Function URL for streaming support. + // The handler serves both static files and SSR responses. + [`${sanitizedName}Function`]: { + Type: "AWS::Serverless::Function", + Properties: { + CodeUri: ".aws/functions/server/", + Handler: "index.handler", + FunctionUrlConfig: { + AuthType: adapterOptions?.authType ?? "NONE", + InvokeMode: "RESPONSE_STREAM", + }, + ...adapterOptions?.functionProperties, + }, + }, + ...(adapterOptions?.cloudfront !== false + ? { + // Custom CachePolicy: respect origin Cache-Control headers. + // + // DefaultTTL/MinTTL = 0: CloudFront defers to the origin's + // Cache-Control header. If the origin omits Cache-Control, + // the response is not cached. + // + // MaxTTL = 1 year: allows immutable build assets (which set + // max-age=31536000) to be cached at the edge for their full + // lifetime. + // + // Cookies are excluded from the cache key so all users share + // the same edge-cached static files. Cookies are still + // forwarded to Lambda via the OriginRequestPolicy. + [`${sanitizedName}CachePolicy`]: { + Type: "AWS::CloudFront::CachePolicy", + Properties: { + CachePolicyConfig: { + Name: `${stackName}-cache-policy`, + DefaultTTL: 0, + MinTTL: 0, + MaxTTL: 31536000, + ParametersInCacheKeyAndForwardedToOrigin: { + CookiesConfig: { CookieBehavior: "none" }, + HeadersConfig: { HeaderBehavior: "none" }, + QueryStringsConfig: { QueryStringBehavior: "all" }, + EnableAcceptEncodingBrotli: true, + EnableAcceptEncodingGzip: true, + }, + }, + }, + }, + // CloudFront distribution: single Lambda origin. + // + // Architecture: + // Viewer → CloudFront → Lambda (serves everything) + // + // The Lambda handler serves static files from its deployment + // package with appropriate Cache-Control headers: + // - Build assets (content-hashed): immutable, 1 year + // - Pre-rendered HTML / x-component: must-revalidate + // - Public files: 10 minutes + // - Dynamic SSR: controlled by react-server + // + // CloudFront caches GET/HEAD responses at the edge based on + // these headers, so after the first request each file is + // served directly from the edge cache without Lambda. + // + // This design: + // ✓ No CloudFront Function size limits + // ✓ All HTTP methods work (POST for server actions, etc.) + // ✓ No hardcoded path patterns + // ✓ No Origin Group failover (only works for GET/HEAD) + // ✓ Scales to any number of static files + [`${sanitizedName}Distribution`]: { + Type: "AWS::CloudFront::Distribution", + Properties: { + DistributionConfig: { + Enabled: true, + DefaultRootObject: "", + Origins: [ + { + Id: "LambdaOrigin", + DomainName: { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Fn::GetAtt": [ + `${sanitizedName}FunctionUrl`, + "FunctionUrl", + ], + }, + ], + }, + ], + }, + CustomOriginConfig: { + OriginProtocolPolicy: "https-only", + }, + }, + ], + DefaultCacheBehavior: { + TargetOriginId: "LambdaOrigin", + ViewerProtocolPolicy: "redirect-to-https", + AllowedMethods: [ + "GET", + "HEAD", + "OPTIONS", + "PUT", + "POST", + "PATCH", + "DELETE", + ], + CachedMethods: ["GET", "HEAD"], + CachePolicyId: { + Ref: `${sanitizedName}CachePolicy`, + }, + // Forward all viewer values except Host to Lambda + OriginRequestPolicyId: + "b689b0a8-53d0-40ab-baf2-68738e2966ac", + Compress: true, + }, + ...adapterOptions?.cloudfront?.distributionConfig, + }, + }, + }, + } + : {}), + // Optional S3 bucket for static file sync. + // Not used by CloudFront — serves as a sync target for + // 'aws s3 sync .aws/static/ s3:///' or as an + // external asset store when needed. + ...(adapterOptions?.s3Bucket !== false + ? { + [`${sanitizedName}StaticBucket`]: { + Type: "AWS::S3::Bucket", + Properties: { + BucketName: + adapterOptions?.s3Bucket?.bucketName ?? + `${stackName}-static`, + ...adapterOptions?.s3Bucket?.properties, + }, + }, + } + : {}), + ...adapterOptions?.resources, + }, + Outputs: { + [`${sanitizedName}FunctionUrl`]: { + Description: "Lambda Function URL", + Value: { + "Fn::GetAtt": [`${sanitizedName}FunctionUrl`, "FunctionUrl"], + }, + }, + ...(adapterOptions?.cloudfront !== false + ? { + [`${sanitizedName}DistributionDomain`]: { + Description: "CloudFront distribution domain", + Value: { + "Fn::GetAtt": [`${sanitizedName}Distribution`, "DomainName"], + }, + }, + } + : {}), + ...(adapterOptions?.s3Bucket !== false + ? { + [`${sanitizedName}StaticBucketName`]: { + Description: + "S3 bucket for static assets (optional sync target)", + Value: { Ref: `${sanitizedName}StaticBucket` }, + }, + } + : {}), + ...adapterOptions?.outputs, + }, + }; + + // Allow user to override or extend the template + const finalTemplate = adapterOptions?.template + ? typeof adapterOptions.template === "function" + ? adapterOptions.template(samTemplate) + : { ...samTemplate, ...adapterOptions.template } + : samTemplate; + + message("creating", "SAM template (template.json)"); + + // Write as JSON (SAM CLI supports both YAML and JSON; JSON is simpler to generate) + await writeJSON(join(cwd, "template.json"), finalTemplate); + + success("AWS deployment configuration created"); + }, + deploy: ({ adapterOptions }) => { + const stackName = + adapterOptions?.stackName ?? adapterOptions?.name ?? "react-server-app"; + return { + command: "sam", + args: [ + "deploy", + "--guided", + "--stack-name", + stackName, + "--capabilities", + "CAPABILITY_IAM", + ...(adapterOptions?.deployArgs ?? []), + ], + }; + }, +}); + +export default function defineConfig(adapterOptions) { + return async (_, root, options) => adapter(adapterOptions, root, options); +} diff --git a/packages/react-server/package.json b/packages/react-server/package.json index 2e285901..260ac43c 100644 --- a/packages/react-server/package.json +++ b/packages/react-server/package.json @@ -130,6 +130,10 @@ "types": "./adapters/adapter.d.ts", "default": "./adapters/vercel/index.mjs" }, + "./adapters/aws": { + "types": "./adapters/adapter.d.ts", + "default": "./adapters/aws/index.mjs" + }, "./worker": { "types": "./worker/index.d.ts", "default": "./worker/index.mjs" From 08a38a7748c58eb0d3dc635d91cdcb988833120d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20L=C3=A1z=C3=A1r?= Date: Sun, 1 Mar 2026 00:46:45 +0100 Subject: [PATCH 2/4] docs: add aws adapter --- docs/src/pages/en/(pages)/deploy/adapters.mdx | 5 +- docs/src/pages/en/(pages)/deploy/aws.mdx | 306 ++++++++++++++++++ docs/src/pages/en/deploy.(index).mdx | 2 +- docs/src/pages/ja/(pages)/deploy/adapters.mdx | 5 +- docs/src/pages/ja/(pages)/deploy/aws.mdx | 250 ++++++++++++++ docs/src/pages/ja/deploy.(index).mdx | 2 +- 6 files changed, 566 insertions(+), 4 deletions(-) create mode 100644 docs/src/pages/en/(pages)/deploy/aws.mdx create mode 100644 docs/src/pages/ja/(pages)/deploy/aws.mdx diff --git a/docs/src/pages/en/(pages)/deploy/adapters.mdx b/docs/src/pages/en/(pages)/deploy/adapters.mdx index 143c820c..6316f280 100644 --- a/docs/src/pages/en/(pages)/deploy/adapters.mdx +++ b/docs/src/pages/en/(pages)/deploy/adapters.mdx @@ -17,6 +17,7 @@ You can use adapters to configure your app for different deployment environments - [x] Vercel - [x] Netlify - [x] Cloudflare Workers/Pages +- [x] AWS Lambda - [x] Bun - [x] Deno @@ -24,10 +25,12 @@ You can use adapters to configure your app for different deployment environments ## Configuration -Add `adapter` entry to your `react-server.config.mjs` file. You can specify the name of a built-in adapter (`vercel`, `netlify`, `cloudflare`, `bun`, or `deno`) as a string, or use an external adapter package. +Add `adapter` entry to your `react-server.config.mjs` file. You can specify the name of a built-in adapter (`vercel`, `netlify`, `cloudflare`, `aws`, `bun`, or `deno`) as a string, or use an external adapter package. > **Note:** When running a production build with **Bun** or **Deno**, the corresponding adapter is automatically detected and used without any configuration. You can override this with an explicit `adapter` setting in your config or via `--adapter ` on the CLI. Use `--no-adapter` to disable auto-detection. +> **Note:** The `aws` adapter requires the [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) and configured AWS credentials. See the [AWS Lambda](/deploy/aws) page for setup instructions. + ```mjs export default { adapter: 'vercel', diff --git a/docs/src/pages/en/(pages)/deploy/aws.mdx b/docs/src/pages/en/(pages)/deploy/aws.mdx new file mode 100644 index 00000000..57709c3e --- /dev/null +++ b/docs/src/pages/en/(pages)/deploy/aws.mdx @@ -0,0 +1,306 @@ +--- +title: AWS Lambda +category: Deploy +order: 4 +--- + +import Link from "../../../../components/Link.jsx"; + +# AWS Lambda + +To deploy to AWS, use the built-in `aws` adapter. This adapter deploys your application as an AWS Lambda function with a CloudFront CDN distribution, using AWS SAM (Serverless Application Model) for infrastructure management. + + +## Prerequisites + + +You need the following tools installed: + +- **AWS CLI** — for managing AWS credentials +- **AWS SAM CLI** — for building and deploying the SAM template + +```sh +brew install awscli aws-sam-cli +``` + +Then configure your AWS credentials: + +```sh +aws configure +``` + +You'll need your AWS Access Key ID and Secret Access Key. You can create these in the [AWS IAM Console](https://console.aws.amazon.com/iam/) under **Users → Security credentials → Access keys**. + + +## Installation + + +No additional packages are needed — the adapter is built into `@lazarv/react-server`. + +Add the adapter to your `react-server.config.mjs` file: + +```mjs +export default { + adapter: "aws", +}; +``` + + +## Configuration + + +You can customize the adapter by passing options: + +```mjs +export default { + adapter: [ + "aws", + { + name: "my-app", // Application name (used in resource names) + stackName: "my-app-stack", // CloudFormation stack name + runtime: "nodejs20.x", // Lambda runtime (default: "nodejs20.x") + memorySize: 1024, // Lambda memory in MB (default: 1024) + timeout: 30, // Lambda timeout in seconds (default: 30) + architecture: "arm64", // Lambda architecture (default: "arm64") + authType: "NONE", // Function URL auth type (default: "NONE") + environment: { // Additional environment variables + MY_API_KEY: "value", + }, + }, + ], +}; +``` + +### Configuration Options + +- `name`: Application name used for AWS resource names. Falls back to `package.json` name (without scope) or `"react-server-app"`. +- `stackName`: CloudFormation stack name used during deployment. Defaults to `name`. +- `runtime`: Lambda runtime identifier (default: `"nodejs20.x"`). +- `memorySize`: Lambda function memory in MB (default: `1024`). +- `timeout`: Lambda function timeout in seconds (default: `30`). +- `architecture`: Lambda CPU architecture, `"arm64"` or `"x86_64"` (default: `"arm64"`). Arm64 (Graviton2) offers better price-performance. +- `authType`: Function URL authentication type (default: `"NONE"`). Set to `"AWS_IAM"` to require IAM auth. +- `environment`: Additional environment variables added to the Lambda function. `NODE_ENV` is always set to `"production"`. +- `functionProperties`: Additional SAM `AWS::Serverless::Function` properties merged into the function resource. +- `cloudfront`: Set to `false` to skip CloudFront distribution creation. Use `cloudfront.distributionConfig` to extend the generated CloudFront `DistributionConfig`. +- `s3Bucket`: Set to `false` to skip S3 bucket creation. Use `s3Bucket.bucketName` and `s3Bucket.properties` to customize. +- `resources`: Additional CloudFormation resources merged into the template. +- `outputs`: Additional CloudFormation outputs merged into the template. +- `template`: Override or extend the SAM template. Pass an object to merge, or a function `(template) => template` for full control. +- `deployArgs`: Additional arguments passed to `sam deploy`. + + +## Deploy + + +Build and deploy your application: + +```sh +pnpm react-server build [root] --adapter aws +sam deploy --guided --stack-name my-app --capabilities CAPABILITY_IAM +``` + +Or use the `--deploy` flag to build and deploy in one step: + +```sh +pnpm react-server build [root] --adapter aws --deploy +``` + +The first deployment uses `--guided` mode which prompts for configuration and saves it to `samconfig.toml`. Subsequent deployments use the saved configuration automatically. + + +## Architecture + + +The adapter creates the following AWS resources: + +- **Lambda Function** with a Function URL (streaming enabled via `RESPONSE_STREAM`) +- **CloudFront Distribution** as a CDN in front of the Lambda function +- **CloudFront Cache Policy** that respects origin `Cache-Control` headers +- **S3 Bucket** (optional) as a static file sync target + +### How it works + +All requests flow through a single Lambda origin: + +``` +Viewer → CloudFront → Lambda + ├─ Static file? → Serve from disk with Cache-Control + └─ Dynamic? → SSR via react-server +``` + +The Lambda handler includes all static files in its deployment package and serves them directly from the filesystem. Each response includes appropriate `Cache-Control` headers: + +- **Build assets** (`/assets/*`, `/client/*`): `public, max-age=31536000, immutable` — cached at the edge for 1 year (filenames are content-hashed) +- **Pre-rendered HTML / x-component**: `must-revalidate` — CloudFront always revalidates with Lambda +- **Public files**: `public, max-age=600` — cached for 10 minutes + +After the first request, CloudFront serves cached static files directly from the edge without invoking Lambda. The custom cache policy (`DefaultTTL: 0`, `MinTTL: 0`, `MaxTTL: 31536000`) defers entirely to the origin's `Cache-Control` headers. + + +## CloudFront Configuration + + +### Custom domain + +To use a custom domain with your CloudFront distribution, extend the distribution config: + +```mjs +export default { + adapter: [ + "aws", + { + cloudfront: { + distributionConfig: { + Aliases: ["www.example.com"], + ViewerCertificate: { + AcmCertificateArn: "arn:aws:acm:us-east-1:123456789:certificate/abc-123", + SslSupportMethod: "sni-only", + MinimumProtocolVersion: "TLSv1.2_2021", + }, + }, + }, + }, + ], +}; +``` + +> **Note:** ACM certificates for CloudFront must be in the `us-east-1` region. + +### Without CloudFront + +To deploy without CloudFront (Lambda Function URL only): + +```mjs +export default { + adapter: [ + "aws", + { + cloudfront: false, + }, + ], +}; +``` + + +## SAM Template + + +The adapter generates a `template.json` file in your project root. This is a standard AWS SAM template that can be customized via `adapterOptions.template`: + +```mjs +export default { + adapter: [ + "aws", + { + template: (template) => { + // Add a DynamoDB table + template.Resources.MyTable = { + Type: "AWS::DynamoDB::Table", + Properties: { + TableName: "my-table", + AttributeDefinitions: [ + { AttributeName: "id", AttributeType: "S" }, + ], + KeySchema: [ + { AttributeName: "id", KeyType: "HASH" }, + ], + BillingMode: "PAY_PER_REQUEST", + }, + }; + return template; + }, + }, + ], +}; +``` + + +## Troubleshooting + + +### `zsh: command not found: sam` + +Install the AWS SAM CLI: + +```sh +brew install aws-sam-cli +``` + +### `zsh: command not found: aws` + +Install the AWS CLI: + +```sh +brew install awscli +``` + +### `Error: Unable to locate credentials` + +Configure your AWS credentials: + +```sh +aws configure +``` + +You'll need your Access Key ID, Secret Access Key, default region, and output format. Visit the [AWS IAM Console](https://console.aws.amazon.com/iam/) to create access keys. + +If you're using AWS SSO: + +```sh +aws sso login --profile your-profile +``` + +### `Failed to create/update the stack ... ROLLBACK_COMPLETE` + +A previous deployment failed and CloudFormation is stuck. Delete the failed stack and try again: + +```sh +sam delete --stack-name your-stack-name +sam deploy --guided --stack-name your-stack-name --capabilities CAPABILITY_IAM +``` + +### Lambda cold starts + +Cold starts are expected with Lambda functions. To minimize their impact: + +- Use `arm64` architecture (default) — Graviton2 processors have faster cold starts +- Increase `memorySize` — Lambda allocates CPU proportional to memory +- Consider using [Lambda Provisioned Concurrency](https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html) for production workloads + +You can add provisioned concurrency via the template override: + +```mjs +export default { + adapter: [ + "aws", + { + template: (template) => { + const fnKey = Object.keys(template.Resources).find( + (k) => template.Resources[k].Type === "AWS::Serverless::Function" + ); + if (fnKey) { + template.Resources[fnKey].Properties.ProvisionedConcurrencyConfig = { + ProvisionedConcurrentExecutions: 1, + }; + } + return template; + }, + }, + ], +}; +``` + +### CloudFront cache invalidation + +After redeploying, CloudFront may still serve stale cached content. Create an invalidation: + +```sh +aws cloudfront create-invalidation \ + --distribution-id YOUR_DISTRIBUTION_ID \ + --paths "/*" +``` + +Build assets with content-hashed filenames (`/assets/*`, `/client/*`) don't need invalidation — new deployments generate new filenames automatically. + +> **Note:** For additional AWS-specific features like VPC configuration, IAM policies, or Lambda layers, use the `functionProperties`, `resources`, and `template` options. Refer to the [AWS SAM documentation](https://docs.aws.amazon.com/serverless-application-model/) for the full template specification. diff --git a/docs/src/pages/en/deploy.(index).mdx b/docs/src/pages/en/deploy.(index).mdx index b30ab60e..c2a0d27f 100644 --- a/docs/src/pages/en/deploy.(index).mdx +++ b/docs/src/pages/en/deploy.(index).mdx @@ -4,6 +4,6 @@ When you're finished with your app, you can deploy it to the web. The framework You will learn how to use [adapters](/deploy/adapters) to configure your app for different deployment environments. -You can also learn how to deploy your app to different platforms using the available built-in adapters. The framework provides adapters for [Vercel](/deploy/vercel), [Netlify](/deploy/netlify), [Cloudflare](/deploy/cloudflare), [Bun](/deploy/bun), and [Deno](/deploy/deno). +You can also learn how to deploy your app to different platforms using the available built-in adapters. The framework provides adapters for [Vercel](/deploy/vercel), [Netlify](/deploy/netlify), [Cloudflare](/deploy/cloudflare), [AWS Lambda](/deploy/aws), [Bun](/deploy/bun), and [Deno](/deploy/deno). Find more information about how to implement custom deployment adapters in the [Adapter API](/deploy/api) section. \ No newline at end of file diff --git a/docs/src/pages/ja/(pages)/deploy/adapters.mdx b/docs/src/pages/ja/(pages)/deploy/adapters.mdx index fbe83d00..0a502a40 100644 --- a/docs/src/pages/ja/(pages)/deploy/adapters.mdx +++ b/docs/src/pages/ja/(pages)/deploy/adapters.mdx @@ -17,6 +17,7 @@ import Link from "../../../../components/Link.jsx"; - [x] Vercel - [x] Netlify - [x] Cloudflare Workers/Pages +- [x] AWS Lambda - [x] Bun - [x] Deno @@ -24,10 +25,12 @@ import Link from "../../../../components/Link.jsx"; ## 設定 -`react-server.config.mjs`ファイルに `adapter` エントリを追加します。ビルトインアダプタの名前(`vercel`、`netlify`、`cloudflare`、`bun`、または `deno`)を文字列で指定するか、外部アダプタパッケージを使用できます。 +`react-server.config.mjs`ファイルに `adapter` エントリを追加します。ビルトインアダプタの名前(`vercel`、`netlify`、`cloudflare`、`aws`、`bun`、または `deno`)を文字列で指定するか、外部アダプタパッケージを使用できます。 > **Note:** **Bun** または **Deno** でプロダクションビルドを実行すると、対応するアダプタが自動的に検出・使用されます。設定は不要です。明示的な `adapter` 設定またはCLIの `--adapter ` で上書きできます。自動検出を無効にするには `--no-adapter` を使用してください。 +> **Note:** `aws` アダプタには [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) と設定済みのAWS認証情報が必要です。セットアップ手順は [AWS Lambda](/deploy/aws) ページを参照してください。 + ```mjs export default { adapter: 'vercel', diff --git a/docs/src/pages/ja/(pages)/deploy/aws.mdx b/docs/src/pages/ja/(pages)/deploy/aws.mdx new file mode 100644 index 00000000..2079d5f5 --- /dev/null +++ b/docs/src/pages/ja/(pages)/deploy/aws.mdx @@ -0,0 +1,250 @@ +--- +title: AWS Lambda +category: Deploy +order: 4 +--- + +import Link from "../../../../components/Link.jsx"; + +# AWS Lambda + +AWSにデプロイするには、ビルトインの `aws` アダプタを使用します。このアダプタは、AWS SAM(Serverless Application Model)を使用して、AWS Lambda関数とCloudFront CDNディストリビューションとしてアプリケーションをデプロイします。 + + +## 前提条件 + + +以下のツールがインストールされている必要があります: + +- **AWS CLI** — AWS認証情報の管理用 +- **AWS SAM CLI** — SAMテンプレートのビルドとデプロイ用 + +```sh +brew install awscli aws-sam-cli +``` + +次にAWS認証情報を設定します: + +```sh +aws configure +``` + +AWSアクセスキーIDとシークレットアクセスキーが必要です。[AWS IAMコンソール](https://console.aws.amazon.com/iam/)の **ユーザー → セキュリティ認証情報 → アクセスキー** で作成できます。 + + +## インストール + + +追加のパッケージは不要です — アダプタは `@lazarv/react-server` に組み込まれています。 + +`react-server.config.mjs` ファイルにアダプタを追加します: + +```mjs +export default { + adapter: "aws", +}; +``` + + +## 設定 + + +オプションを渡してアダプタをカスタマイズできます: + +```mjs +export default { + adapter: [ + "aws", + { + name: "my-app", // アプリケーション名(リソース名に使用) + stackName: "my-app-stack", // CloudFormationスタック名 + runtime: "nodejs20.x", // Lambdaランタイム(デフォルト: "nodejs20.x") + memorySize: 1024, // Lambdaメモリ(MB)(デフォルト: 1024) + timeout: 30, // Lambdaタイムアウト(秒)(デフォルト: 30) + architecture: "arm64", // Lambdaアーキテクチャ(デフォルト: "arm64") + authType: "NONE", // Function URL認証タイプ(デフォルト: "NONE") + environment: { // 追加の環境変数 + MY_API_KEY: "value", + }, + }, + ], +}; +``` + +### 設定オプション + +- `name`: AWSリソース名に使用されるアプリケーション名。`package.json` の名前(スコープなし)または `"react-server-app"` にフォールバックします。 +- `stackName`: デプロイ時に使用されるCloudFormationスタック名。デフォルトは `name` です。 +- `runtime`: Lambdaランタイム識別子(デフォルト: `"nodejs20.x"`)。 +- `memorySize`: Lambda関数のメモリ(MB)(デフォルト: `1024`)。 +- `timeout`: Lambda関数のタイムアウト(秒)(デフォルト: `30`)。 +- `architecture`: Lambda CPUアーキテクチャ、`"arm64"` または `"x86_64"`(デフォルト: `"arm64"`)。Arm64(Graviton2)はコストパフォーマンスが優れています。 +- `authType`: Function URL認証タイプ(デフォルト: `"NONE"`)。IAM認証を要求するには `"AWS_IAM"` に設定します。 +- `environment`: Lambda関数に追加される環境変数。`NODE_ENV` は常に `"production"` に設定されます。 +- `functionProperties`: 関数リソースにマージされるSAM `AWS::Serverless::Function` プロパティ。 +- `cloudfront`: `false` に設定するとCloudFrontディストリビューションの作成をスキップします。`cloudfront.distributionConfig` で生成されたCloudFront `DistributionConfig` を拡張できます。 +- `s3Bucket`: `false` に設定するとS3バケットの作成をスキップします。`s3Bucket.bucketName` と `s3Bucket.properties` でカスタマイズできます。 +- `resources`: テンプレートにマージされる追加のCloudFormationリソース。 +- `outputs`: テンプレートにマージされる追加のCloudFormation出力。 +- `template`: SAMテンプレートのオーバーライドまたは拡張。オブジェクトを渡してマージするか、関数 `(template) => template` で完全に制御できます。 +- `deployArgs`: `sam deploy` に渡される追加引数。 + + +## デプロイ + + +アプリケーションをビルドしてデプロイします: + +```sh +pnpm react-server build [root] --adapter aws +sam deploy --guided --stack-name my-app --capabilities CAPABILITY_IAM +``` + +または `--deploy` フラグを使用してビルドとデプロイを一度に実行します: + +```sh +pnpm react-server build [root] --adapter aws --deploy +``` + +初回デプロイでは `--guided` モードが使用され、設定を求められ `samconfig.toml` に保存されます。以降のデプロイでは保存された設定が自動的に使用されます。 + + +## アーキテクチャ + + +アダプタは以下のAWSリソースを作成します: + +- **Lambda関数** — Function URL付き(`RESPONSE_STREAM` によるストリーミング有効) +- **CloudFrontディストリビューション** — Lambda関数前のCDN +- **CloudFrontキャッシュポリシー** — オリジンの `Cache-Control` ヘッダーを尊重 +- **S3バケット**(オプション)— 静的ファイル同期ターゲット + +### 仕組み + +すべてのリクエストは単一のLambdaオリジンを通過します: + +``` +ビューア → CloudFront → Lambda + ├─ 静的ファイル? → ディスクから配信 + Cache-Control + └─ 動的? → react-serverによるSSR +``` + +Lambdaハンドラはデプロイパッケージ内にすべての静的ファイルを含み、ファイルシステムから直接配信します。各レスポンスには適切な `Cache-Control` ヘッダーが含まれます: + +- **ビルドアセット**(`/assets/*`、`/client/*`):`public, max-age=31536000, immutable` — エッジで1年間キャッシュ(ファイル名はコンテンツハッシュ) +- **プリレンダリングされたHTML / x-component**: `must-revalidate` — CloudFrontは常にLambdaで再検証 +- **パブリックファイル**: `public, max-age=600` — 10分間キャッシュ + +最初のリクエスト後、CloudFrontはLambdaを呼び出さずにエッジから直接キャッシュされた静的ファイルを配信します。 + + +## CloudFront設定 + + +### カスタムドメイン + +CloudFrontディストリビューションでカスタムドメインを使用するには、ディストリビューション設定を拡張します: + +```mjs +export default { + adapter: [ + "aws", + { + cloudfront: { + distributionConfig: { + Aliases: ["www.example.com"], + ViewerCertificate: { + AcmCertificateArn: "arn:aws:acm:us-east-1:123456789:certificate/abc-123", + SslSupportMethod: "sni-only", + MinimumProtocolVersion: "TLSv1.2_2021", + }, + }, + }, + }, + ], +}; +``` + +> **Note:** CloudFront用のACM証明書は `us-east-1` リージョンに作成する必要があります。 + +### CloudFrontなし + +CloudFrontなしでデプロイする場合(Lambda Function URLのみ): + +```mjs +export default { + adapter: [ + "aws", + { + cloudfront: false, + }, + ], +}; +``` + + +## トラブルシューティング + + +### `zsh: command not found: sam` + +AWS SAM CLIをインストールしてください: + +```sh +brew install aws-sam-cli +``` + +### `zsh: command not found: aws` + +AWS CLIをインストールしてください: + +```sh +brew install awscli +``` + +### `Error: Unable to locate credentials` + +AWS認証情報を設定してください: + +```sh +aws configure +``` + +アクセスキーID、シークレットアクセスキー、デフォルトリージョン、出力形式が必要です。[AWS IAMコンソール](https://console.aws.amazon.com/iam/)でアクセスキーを作成できます。 + +AWS SSOを使用している場合: + +```sh +aws sso login --profile your-profile +``` + +### `Failed to create/update the stack ... ROLLBACK_COMPLETE` + +以前のデプロイが失敗し、CloudFormationがスタックした状態です。失敗したスタックを削除してから再試行してください: + +```sh +sam delete --stack-name your-stack-name +sam deploy --guided --stack-name your-stack-name --capabilities CAPABILITY_IAM +``` + +### Lambdaコールドスタート + +Lambda関数ではコールドスタートが発生します。影響を最小限にするには: + +- `arm64` アーキテクチャを使用(デフォルト)— Graviton2プロセッサはコールドスタートが高速 +- `memorySize` を増やす — LambdaはメモリにCPUを比例配分 +- 本番ワークロードには [Lambdaプロビジョンド同時実行](https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html) を検討 + +### CloudFrontキャッシュの無効化 + +再デプロイ後、CloudFrontが古いキャッシュコンテンツを配信する場合があります。無効化を作成してください: + +```sh +aws cloudfront create-invalidation \ + --distribution-id YOUR_DISTRIBUTION_ID \ + --paths "/*" +``` + +コンテンツハッシュ付きファイル名のビルドアセット(`/assets/*`、`/client/*`)は無効化不要です — 新しいデプロイで自動的に新しいファイル名が生成されます。 + +> **Note:** VPC設定、IAMポリシー、Lambdaレイヤーなどの追加のAWS固有機能については、`functionProperties`、`resources`、`template` オプションを使用してください。完全なテンプレート仕様については [AWS SAMドキュメント](https://docs.aws.amazon.com/serverless-application-model/) を参照してください。 diff --git a/docs/src/pages/ja/deploy.(index).mdx b/docs/src/pages/ja/deploy.(index).mdx index 0eb49092..9f8e5db3 100644 --- a/docs/src/pages/ja/deploy.(index).mdx +++ b/docs/src/pages/ja/deploy.(index).mdx @@ -4,6 +4,6 @@ [adapters](/deploy/adapters) を使用して、さまざまなデプロイ環境向けにアプリを構成する方法を学びます。 -また、利用可能なビルトインアダプタを使用して、アプリをさまざまなプラットフォームにデプロイする方法を学ぶこともできます。フレームワークでは [Vercel](/deploy/vercel)、[Netlify](/deploy/netlify)、[Cloudflare](/deploy/cloudflare)、[Bun](/deploy/bun)、[Deno](/deploy/deno) のアダプタを提供しています。 +また、利用可能なビルトインアダプタを使用して、アプリをさまざまなプラットフォームにデプロイする方法を学ぶこともできます。フレームワークでは [Vercel](/deploy/vercel)、[Netlify](/deploy/netlify)、[Cloudflare](/deploy/cloudflare)、[AWS Lambda](/deploy/aws)、[Bun](/deploy/bun)、[Deno](/deploy/deno) のアダプタを提供しています。 デプロイメント アダプタの実装方法については [Adapter API](/deploy/api) セクションを参照してください。 From d4fcbffa592542e5334617f2cc3af03e4c316a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20L=C3=A1z=C3=A1r?= Date: Sun, 1 Mar 2026 00:55:45 +0100 Subject: [PATCH 3/4] chore: remove s3 bucket --- docs/src/pages/en/(pages)/deploy/aws.mdx | 2 -- docs/src/pages/ja/(pages)/deploy/aws.mdx | 2 -- packages/react-server/adapters/aws/index.mjs | 26 -------------------- 3 files changed, 30 deletions(-) diff --git a/docs/src/pages/en/(pages)/deploy/aws.mdx b/docs/src/pages/en/(pages)/deploy/aws.mdx index 57709c3e..72ff6437 100644 --- a/docs/src/pages/en/(pages)/deploy/aws.mdx +++ b/docs/src/pages/en/(pages)/deploy/aws.mdx @@ -83,7 +83,6 @@ export default { - `environment`: Additional environment variables added to the Lambda function. `NODE_ENV` is always set to `"production"`. - `functionProperties`: Additional SAM `AWS::Serverless::Function` properties merged into the function resource. - `cloudfront`: Set to `false` to skip CloudFront distribution creation. Use `cloudfront.distributionConfig` to extend the generated CloudFront `DistributionConfig`. -- `s3Bucket`: Set to `false` to skip S3 bucket creation. Use `s3Bucket.bucketName` and `s3Bucket.properties` to customize. - `resources`: Additional CloudFormation resources merged into the template. - `outputs`: Additional CloudFormation outputs merged into the template. - `template`: Override or extend the SAM template. Pass an object to merge, or a function `(template) => template` for full control. @@ -117,7 +116,6 @@ The adapter creates the following AWS resources: - **Lambda Function** with a Function URL (streaming enabled via `RESPONSE_STREAM`) - **CloudFront Distribution** as a CDN in front of the Lambda function - **CloudFront Cache Policy** that respects origin `Cache-Control` headers -- **S3 Bucket** (optional) as a static file sync target ### How it works diff --git a/docs/src/pages/ja/(pages)/deploy/aws.mdx b/docs/src/pages/ja/(pages)/deploy/aws.mdx index 2079d5f5..28795e9d 100644 --- a/docs/src/pages/ja/(pages)/deploy/aws.mdx +++ b/docs/src/pages/ja/(pages)/deploy/aws.mdx @@ -83,7 +83,6 @@ export default { - `environment`: Lambda関数に追加される環境変数。`NODE_ENV` は常に `"production"` に設定されます。 - `functionProperties`: 関数リソースにマージされるSAM `AWS::Serverless::Function` プロパティ。 - `cloudfront`: `false` に設定するとCloudFrontディストリビューションの作成をスキップします。`cloudfront.distributionConfig` で生成されたCloudFront `DistributionConfig` を拡張できます。 -- `s3Bucket`: `false` に設定するとS3バケットの作成をスキップします。`s3Bucket.bucketName` と `s3Bucket.properties` でカスタマイズできます。 - `resources`: テンプレートにマージされる追加のCloudFormationリソース。 - `outputs`: テンプレートにマージされる追加のCloudFormation出力。 - `template`: SAMテンプレートのオーバーライドまたは拡張。オブジェクトを渡してマージするか、関数 `(template) => template` で完全に制御できます。 @@ -117,7 +116,6 @@ pnpm react-server build [root] --adapter aws --deploy - **Lambda関数** — Function URL付き(`RESPONSE_STREAM` によるストリーミング有効) - **CloudFrontディストリビューション** — Lambda関数前のCDN - **CloudFrontキャッシュポリシー** — オリジンの `Cache-Control` ヘッダーを尊重 -- **S3バケット**(オプション)— 静的ファイル同期ターゲット ### 仕組み diff --git a/packages/react-server/adapters/aws/index.mjs b/packages/react-server/adapters/aws/index.mjs index d9c0c304..f1e6232b 100644 --- a/packages/react-server/adapters/aws/index.mjs +++ b/packages/react-server/adapters/aws/index.mjs @@ -293,23 +293,6 @@ export const adapter = createAdapter({ }, } : {}), - // Optional S3 bucket for static file sync. - // Not used by CloudFront — serves as a sync target for - // 'aws s3 sync .aws/static/ s3:///' or as an - // external asset store when needed. - ...(adapterOptions?.s3Bucket !== false - ? { - [`${sanitizedName}StaticBucket`]: { - Type: "AWS::S3::Bucket", - Properties: { - BucketName: - adapterOptions?.s3Bucket?.bucketName ?? - `${stackName}-static`, - ...adapterOptions?.s3Bucket?.properties, - }, - }, - } - : {}), ...adapterOptions?.resources, }, Outputs: { @@ -329,15 +312,6 @@ export const adapter = createAdapter({ }, } : {}), - ...(adapterOptions?.s3Bucket !== false - ? { - [`${sanitizedName}StaticBucketName`]: { - Description: - "S3 bucket for static assets (optional sync target)", - Value: { Ref: `${sanitizedName}StaticBucket` }, - }, - } - : {}), ...adapterOptions?.outputs, }, }; From b5a6046dfb7941cb25bf3db0b9083257f48234d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viktor=20L=C3=A1z=C3=A1r?= Date: Sun, 1 Mar 2026 16:47:08 +0100 Subject: [PATCH 4/4] chore: add aws to adapter grid component --- docs/src/components/AdapterGrid.jsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/components/AdapterGrid.jsx b/docs/src/components/AdapterGrid.jsx index c9d7e5c4..6c4f5e69 100644 --- a/docs/src/components/AdapterGrid.jsx +++ b/docs/src/components/AdapterGrid.jsx @@ -14,6 +14,11 @@ const adapters = [ href: "/deploy/cloudflare", description: "Workers & Pages", }, + { + name: "AWS Lambda", + href: "/deploy/aws", + description: "Serverless functions", + }, { name: "Bun", href: "/deploy/bun",