lazarv / react-server

The easiest way to build React apps with server-side rendering
https://react-server.dev
MIT License
121 stars 6 forks source link

Hosting Adapter AWS Lambda #30

Open aheissenberger opened 2 months ago

aheissenberger commented 2 months ago

Description

I would like to help to implement the Adapter as I have done this for WAKU and Vike.dev (BATI.dev).

Suggested solution

using SST or CDK.

Alternative

No response

Additional context

Is there any other example which targets a node environment except for the Vercel adapter?

Validations

lazarv commented 2 months ago

That would be awesome @aheissenberger!

Currently the only adapter in existence is for Vercel. Creating the @lazarv/react-server-adapter-sst would have been my next target after moving @lazarv/react-server-router around.

To implement another adapter, there are some dependencies regarding moving any code from the Vercel adapter either into the core package or into a shared package for adapters, like @lazarv/react-server-adapter-core.

What I have in mind already:

The adapters for @lazarv/react-server are more close to the various adapters SvelteKit have at https://github.com/sveltejs/kit/tree/main/packages. These are just generator / build scripts instead of Vite / Rollup plugins used in Waku. I'm not familiar with Vike deploy adapters.

The entrypoint for the Vercel serverless function is just using the middleware mode of the framework at https://github.com/lazarv/react-server/blob/main/packages/react-server-adapter-vercel/functions/index.mjs. I think something similar will be ok for AWS too, but surely needs more than this.

aheissenberger commented 2 months ago

I looked at the existing code and the problems are:

  1. the hattip middleware is wrapped with the @hattip/adapter-node. Each serverless provider (e.g. aws: @hattip/adapter-aws-lambda) needs its own hattip adapter which should be part of the adapter package and not part of the core.
  2. handling static files differ too. A typical AWS setup will use a Cloudfront (CDN) in front of a AWS Lambda (Serverless Function). The static files are deployed to a S3 bucket (block storage). As Cloudfront has limited possibilities (max. 25) to split traffic based on root path (e.g. /asset, /public) there is a need to serve static assets which are directly in the root directory or in dynamic directories with a static handler (e.g. @hattip/static) from the lambda. There is a possibility to use Lambda@Edge to create a Routingfunction with all known static routs but this brings the problems of a deployment which needs to handle more than one region as the Lambda@Edge function can only live in the US East (N. Virginia)region.

    Here is a code I used to wrap the vike.dev middleware for AWS Lambda:

    
    import { existsSync } from "node:fs";
    import awsLambdaAdapter from "@hattip/adapter-aws-lambda";
    import { walk } from "@hattip/walk";
    import type { FileInfo } from "@hattip/walk";
    import { createStaticMiddleware } from "@hattip/static";
    import { createFileReader } from "@hattip/static/fs";
    import hattipHandler from "@batijs/hattip/hattip-entry";
    import type { Handler, APIGatewayProxyResultV2, APIGatewayProxyEventV2 } from "aws-lambda";

const root = new URL("./dist/client", import.meta.url); const staticRootExists = existsSync(root); const files = staticRootExists ? walk(root) : new Map<string, FileInfo>(); const staticMiddleware = staticRootExists ? createStaticMiddleware(files, createFileReader(root), { urlRoot: "/", }) : undefined;

const awsHandler = awsLambdaAdapter((ctx) => { if (hattipHandler === undefined) throw new Error("hattipHandler is undefined"); if (staticMiddleware === undefined) return hattipHandler(ctx); return staticMiddleware(ctx) || hattipHandler(ctx); });

export const handler: Handler<APIGatewayProxyEventV2, APIGatewayProxyResultV2> = awsHandler;



The deployment tool `sst` or `aws cdk` will need the entry point to the aws handler and a link to the folder with the static files for the s3 bucket. The entry point is than again bundled by the tools (set, idk) to include all dependencies as part of the deployment to aws.

I think I will need your help to get the restructuring of (1.) done.
lazarv commented 2 months ago
  1. Very true, I'm trying to keep this in my mind to be able to provide a Hattip adapter from configuration or similar, but I'm also thinking about making this abstract so it's easier to change the web framework for ex. using Hono, to get even better performance and better integration with other frameworks.
  2. All the static files used only on the client-side should be transferred into an S3 bucket and yes CloudFront is needed to route them properly, but it's ok and enough to just have a very simple rule like with Vercel to serve all static files first and then as a fallback call the Lambda. For a simple Node.js deployment using Docker or whatever there's a custom static file handler at https://github.com/lazarv/react-server/blob/main/packages/react-server/lib/handlers/static.mjs, but this or any other static file handling middleware is not needed in case of any serverless solution.

Regarding dependency management I think that the @vercel/nft package is doing a great job, stripping down any unused files even from the packages used.

It might would be a good approach in case of AWS sst or cdk to have a generated output that the developer can integrate into an existing sst or cdk setup, but also provide a simple default for easy deployment.

I think I will need your help to get the restructuring of (1.) done.

Yes I can prepare everything for this work to get started, I'll prioritize it up.

aheissenberger commented 2 months ago

2. very simple rule like with Vercel to serve all static files first and then as a fallback call the Lambda.

Did you got this working? This only works for client side routing and not for server side routing - at least I never got that working ;-)

here is what Vike.dev is using to abstract the adapters: https://github.com/magne4000/universal-middleware

I am not sure if this kind of abstraction is really needed compared to choose one middleware and stick with it. Based on numbers its h3 which is used most and second by hono(hattip benchmarks) as the speed king but I am not an expert in this field. For people using your framework it should not matter what you choose to use as long as there is an adapter for the most used deployment platforms and based on my experience it is not hard to create a new one if the platform is not directly supported.

I have never used @vercel/nft but bundling withesbuild including dependencies strips down the whole deployment usually to under 5 MB.

When I looked at packages/react-server/lib/start/create-server.mjs there have been dependencies from node: which works with AWS Lambda but not with Lambda@Edge and other serverless runtimes running in a browser environment.

For the start I would provide a CDK Stack as its only javascript and does not need a native binary to be installed as it is the case with sst which can be more difficult in a CD pipeline. The benefits of providing sst deployments is the higher performance and the support of any target provided by terraform adapters.

lazarv commented 2 months ago

You can also just try to copy together a production build of a simple example (even just a Hello World!) and deploy it manually to AWS Lambda by using any tech to be aware of any pain points which now exists, using https://github.com/lazarv/react-server/blob/main/packages/react-server/lib/start/node.mjs and https://github.com/lazarv/react-server/blob/main/packages/react-server/lib/start/create-server.mjs as a starting point to create a Lamda handler. The only necessary steps in this are:

When using a build which not included any client components, none of the static files are needed to make this work. I would suggest a Hello World! with a Math.random().

Following this there should be an AWS Lambda handler as result which only needs some refactor to include it in the framework similar to @lazarv/react-server/node. Any other part of a complete deployment is about collecting necessary files.

Supporting Edge runtimes like Lambda@Edge or Vercel Edge is a feature I'm not working right now at all. I consider it nice to have for now. It needs major architectural work in the framework, starting with running router middlewares (like in Next.js) on the Edge.

I have never used @vercel/nft but bundling withesbuild including dependencies strips down the whole deployment usually to under 5 MB.

The issue with the esbuild approach could be that we need the exact files generated at a production build as client components for SSR are lazily resolved on-demand using the manifest files for server, client and browser. But I'm not against it. Although Vite will drop esbuild when Rolldown is ready (I think there will be some announcement around this very soon at ViteConf) and then it's not a good choice to stick to it on the long run. But for now if it works, I'm ok with it.

lazarv commented 2 months ago

Hi @aheissenberger! If you're still interested, then you can use @lazarv/react-server-adapter-core to create a new adapter based on the Vercel adapter and check out https://react-server.dev/deploy/api for more information. Let me know if you have any questions or suggestions about it.

aheissenberger commented 1 month ago

Hi @aheissenberger! If you're still interested, then you can use @lazarv/react-server-adapter-core to create a new adapter based on the Vercel adapter and check out https://react-server.dev/deploy/api for more information. Let me know if you have any questions or suggestions about it.

still interested but was busy with work 😄