Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add edge support to aws-lambda presets #1557

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
"get-port-please": "^3.0.1",
"miniflare": "^2.14.1",
"prettier": "^3.0.2",
"sst": "^2.24.16",
"typescript": "^5.2.2",
"unbuild": "^2.0.0",
"undici": "^5.23.0",
Expand All @@ -163,4 +164,4 @@
]
}
}
}
}
5,854 changes: 4,715 additions & 1,139 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ async function _build(nitro: Nitro, rollupConfig: RollupConfig) {
const buildInfo = {
date: new Date(),
preset: nitro.options.preset,
entry: nitro.options.entry.split("/").pop(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have no way to know which entry a preset use, maybe this should be in another PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted in #1649

commands: {
preview: nitro.options.commands.preview,
deploy: nitro.options.commands.deploy,
Expand Down
31 changes: 30 additions & 1 deletion src/presets/aws-lambda.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
import { defineNitroPreset } from "../preset";
import { generateCdkApp } from "../utils/cdk";
import { generateSST } from "../utils/sst";

export const awsLambda = defineNitroPreset({
entry: "#internal/nitro/entries/aws-lambda",
entry: `#internal/nitro/entries/aws-lambda-{{ awsLambda.target }}`,
awsLambda: {
// we need this defined here so it's picked up by the template in lambda entries
target: (process.env.NITRO_AWS_LAMBDA_TARGET || "default") as any,
},
hooks: {
"rollup:before": (nitro) => {
const target = nitro.options.awsLambda?.target as unknown;
if (!target || target === "default") {
nitro.logger.warn(
"Neither `awsLambda.target` or `NITRO_AWS_LAMBDA_TARGET` is set. Set the target to remove this warning. See https://nitro.unjs.io/deploy/providers/aws-lambda for more information."
);
// Default to single region lambda
nitro.options.awsLambda = { target: "single" };
}
},
async compiled(nitro) {
if (nitro.options.awsLambda.sst) {
await generateSST(nitro);
}
if (
nitro.options.awsLambda.target === "edge" &&
nitro.options.awsLambda.cdk === true
) {
await generateCdkApp(nitro);
}
},
},
});
2 changes: 2 additions & 0 deletions src/runtime/entries/aws-lambda-default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// we need this file to detect if the user is passing a `target` property
export { handler } from "./aws-lambda-single";
43 changes: 43 additions & 0 deletions src/runtime/entries/aws-lambda-edge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type {
CloudFrontRequestEvent,
CloudFrontResultResponse,
Context,
} from "aws-lambda";
import "#internal/nitro/virtual/polyfill";
import { nitroApp } from "../app";
import {
normalizeCloudfrontBody,
normalizeCloudfrontIncomingHeaders,
normalizeCloudfrontOutgoingHeaders,
normalizeLambdaOutgoingBody,
} from "../utils.lambda";

export const handler = async function handler(
event: CloudFrontRequestEvent,
context: Context
): Promise<CloudFrontResultResponse> {
const request = event.Records[0].cf.request;

const url = getFullUrl(request.uri, request.querystring);
const headers = normalizeCloudfrontIncomingHeaders(request.headers);
const body = normalizeCloudfrontBody(request.body);

const r = await nitroApp.localCall({
event,
url,
context,
headers,
method: request.method,
body,
});

return {
status: r.status.toString(),
headers: normalizeCloudfrontOutgoingHeaders(r.headers),
body: normalizeLambdaOutgoingBody(r.body, r.headers),

Check failure on line 37 in src/runtime/entries/aws-lambda-edge.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Type 'Promise<{ type: "text" | "binary"; body: string; }>' is not assignable to type 'string'.
};
};

function getFullUrl(uri: string, queryString?: string) {
return queryString ? `${uri}?${queryString}` : uri;
}
40 changes: 39 additions & 1 deletion src/runtime/utils.lambda.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { Readable } from "node:stream";
import type { APIGatewayProxyEventHeaders } from "aws-lambda";
import type {
APIGatewayProxyEventHeaders,
CloudFrontHeaders,
CloudFrontRequest,
} from "aws-lambda";

export function normalizeLambdaIncomingHeaders(
headers?: APIGatewayProxyEventHeaders
Expand Down Expand Up @@ -89,3 +93,37 @@ const TEXT_TYPE_RE = /^text\/|\/(json|xml)|utf-?8/;
function isTextType(contentType = "") {
return TEXT_TYPE_RE.test(contentType);
}

export function normalizeCloudfrontOutgoingHeaders(
headers: Record<string, string | number | string[] | undefined>
): CloudFrontHeaders {
return Object.fromEntries(
Object.entries(headers)
.filter(([key]) => !["content-length"].includes(key))
.map(([key, v]) => [
key,
Array.isArray(v)
? v.map((value) => ({ key, value }))
: [{ key, value: v.toString() }],
])
);
}

export function normalizeCloudfrontIncomingHeaders(headers: CloudFrontHeaders) {
return Object.fromEntries(
Object.entries(headers).map(([key, keyValues]) => [
key,
keyValues.map(({ value }) => value),
])
);
}

export function normalizeCloudfrontBody(body?: CloudFrontRequest["body"]) {
if (body === undefined) {
return undefined;
}

return body.encoding === "base64"
? decodeURIComponent(Buffer.from(body.data, "base64").toString("utf8"))
: body.data;
}
19 changes: 18 additions & 1 deletion src/types/presets.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SSTConfig } from "sst";
import type { HttpsOptions } from "firebase-functions/v2/https";
import type { RuntimeOptions, region } from "firebase-functions";
import type { CloudflarePagesRoutes } from "../presets/cloudflare-pages";

/**
* Vercel Build Output Configuration
* @see https://vercel.com/docs/build-output-api/v3
Expand Down Expand Up @@ -95,6 +95,22 @@ interface FirebaseOptionsGen2 extends FirebaseOptionsBase {

type FirebaseOptions = FirebaseOptionsGen1 | FirebaseOptionsGen2;

interface AWSLambdaOptionsBase {
target: "single" | "edge";
sst?: boolean;
sstOptions?: Awaited<ReturnType<SSTConfig["config"]>>;
}

interface AwsLambdaOptionsSingleRegion extends AWSLambdaOptionsBase {
target: "single";
}

interface AwsLambdaOptionsEdge extends AWSLambdaOptionsBase {
target: "edge";
cdk?: boolean;
}

type AWSLambdaOptions = AwsLambdaOptionsSingleRegion | AwsLambdaOptionsEdge;
/**
* https://vercel.com/docs/build-output-api/v3/primitives#serverless-function-configuration
*/
Expand Down Expand Up @@ -149,6 +165,7 @@ export interface PresetOptions {
functions?: VercelServerlessFunctionConfig;
};
firebase: FirebaseOptions;
awsLambda: AWSLambdaOptions;
cloudflare: {
pages: {
/**
Expand Down
Loading
Loading