Skip to content
6 changes: 3 additions & 3 deletions src/build/vite/prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,11 @@ function lazyService(loader) {
let promise, mod
return {
fetch(req) {
if (mod) { return mod.fetch(req) }
if (mod) { return (mod.fetch || mod.default?.fetch)(req) }
if (!promise) {
promise = loader().then(_mod => (mod = _mod.default || _mod))
promise = loader().then(_mod => (mod = _mod))
}
return promise.then(mod => mod.fetch(req))
return promise.then(mod => (mod.fetch || mod.default?.fetch)(req))
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/presets/aws-lambda/preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export type { AwsLambdaOptions as PresetOptions } from "./types.ts";
const awsLambda = defineNitroPreset(
{
entry: "./aws-lambda/runtime/aws-lambda",
commands: {
preview: "npx srvx --prod ./",
},
awsLambda: {
streaming: false,
},
Expand Down
87 changes: 43 additions & 44 deletions src/presets/aws-lambda/runtime/aws-lambda-streaming.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,52 @@
import "#nitro/virtual/polyfills";
import { useNitroApp } from "nitro/app";
import { awsRequest, awsResponseHeaders } from "./_utils.ts";

import type { StreamingResponse } from "@netlify/functions";
import type { Readable } from "node:stream";
import type { APIGatewayProxyEventV2 } from "aws-lambda";
import {
handleLambdaEventWithStream,
invokeLambdaHandler,
toLambdaHandler,
type AWSLambdaHandler,
type AWSLambdaResponseStream,
type AWSLambdaStreamingHandler,
type AwsLambdaEvent,
} from "srvx/aws-lambda";
import { tracingSrvxPlugins } from "#nitro/virtual/tracing";

import type { Context } from "aws-lambda";

const nitroApp = useNitroApp();

export const handler = awslambda.streamifyResponse(
async (event: APIGatewayProxyEventV2, responseStream, context) => {
const request = awsRequest(event, context);

const response = await nitroApp.fetch(request);

const httpResponseMetadata: Omit<StreamingResponse, "body"> = {
statusCode: response.status,
...awsResponseHeaders(response),
};
const lambdaHandler = toLambdaHandler({
fetch: nitroApp.fetch,
plugins: [...tracingSrvxPlugins],
});

const handlerWithPreviewFallback = ((
event: AwsLambdaEvent,
responseStreamOrContext: AWSLambdaResponseStream | Context,
context?: Context
) => {
if (context) {
return handleLambdaEventWithStream(
nitroApp.fetch,
event,
responseStreamOrContext as AWSLambdaResponseStream,
context
);
}

if (!httpResponseMetadata.headers!["transfer-encoding"]) {
httpResponseMetadata.headers!["transfer-encoding"] = "chunked";
}
return lambdaHandler(event, responseStreamOrContext as Context);
}) as AWSLambdaStreamingHandler & AWSLambdaHandler;

const body =
response.body ??
new ReadableStream<string>({
start(controller) {
controller.enqueue("");
controller.close();
},
});
const awsLambdaGlobal = globalThis as typeof globalThis & {
awslambda?: {
streamifyResponse: (handler: AWSLambdaStreamingHandler) => AWSLambdaStreamingHandler;
};
};

const writer = awslambda.HttpResponseStream.from(responseStream, httpResponseMetadata);
export const handler = awsLambdaGlobal.awslambda?.streamifyResponse
? awsLambdaGlobal.awslambda.streamifyResponse(handlerWithPreviewFallback)
: handlerWithPreviewFallback;

const reader = body.getReader();
await streamToNodeStream(reader, responseStream);
writer.end();
}
);

async function streamToNodeStream(
reader: Readable | ReadableStreamDefaultReader,
writer: NodeJS.WritableStream
) {
let readResult = await reader.read();
while (!readResult.done) {
writer.write(readResult.value);
readResult = await reader.read();
}
writer.end();
}
export default {
fetch: (request: Request) => invokeLambdaHandler(lambdaHandler, request),
};
31 changes: 9 additions & 22 deletions src/presets/aws-lambda/runtime/aws-lambda.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,15 @@
import "#nitro/virtual/polyfills";
import { useNitroApp } from "nitro/app";
import { awsRequest, awsResponseHeaders, awsResponseBody } from "./_utils.ts";

import type {
APIGatewayProxyEvent,
APIGatewayProxyEventV2,
APIGatewayProxyResult,
APIGatewayProxyResultV2,
Context,
} from "aws-lambda";
import { invokeLambdaHandler, toLambdaHandler } from "srvx/aws-lambda";
import { tracingSrvxPlugins } from "#nitro/virtual/tracing";

const nitroApp = useNitroApp();

export async function handler(
event: APIGatewayProxyEvent | APIGatewayProxyEventV2,
context: Context
): Promise<APIGatewayProxyResult | APIGatewayProxyResultV2> {
const request = awsRequest(event, context);

const response = await nitroApp.fetch(request);
export const handler = toLambdaHandler({
fetch: nitroApp.fetch,
plugins: [...tracingSrvxPlugins],
});

return {
statusCode: response.status,
...awsResponseHeaders(response),
...(await awsResponseBody(response)),
};
}
export default {
fetch: (request: Request) => invokeLambdaHandler(handler, request),
};
84 changes: 83 additions & 1 deletion test/presets/aws-lambda.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { APIGatewayProxyEvent, APIGatewayProxyEventV2 } from "aws-lambda";
import { resolve } from "pathe";
import { describe } from "vitest";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { parseURL, parseQuery } from "ufo";
import { setupTest, testNitro } from "../tests.ts";

Expand Down Expand Up @@ -42,6 +43,14 @@ describe("nitro:preset:aws-lambda-v2", async () => {
return webResponse(res);
};
});

it("exports a preview fetch through the Lambda adapter", async () => {
const entry = await import(resolve(ctx.outDir, "server/index.mjs"));
const response = await entry.default.fetch(new Request("http://localhost/api/hello"));
await expect(response.json()).resolves.toMatchObject({
message: "Hello API",
});
});
});

describe("nitro:preset:aws-lambda-v1", async () => {
Expand Down Expand Up @@ -70,6 +79,79 @@ describe("nitro:preset:aws-lambda-v1", async () => {
});
});

describe("nitro:preset:aws-lambda-streaming", async () => {
const ctx = await setupTest("aws-lambda", {
config: {
awsLambda: {
streaming: true,
},
},
outDirSuffix: "-streaming",
});

it("exports a preview fetch through the streaming Lambda adapter", async () => {
const entry = await import(resolve(ctx.outDir, "server/index.mjs"));
const response = await entry.default.fetch(new Request("http://localhost/api/hello"));
await expect(response.json()).resolves.toMatchObject({
message: "Hello API",
});
});
});

describe("nitro:preset:aws-lambda-streaming-vite-ssr", async () => {
const ctx = await setupTest("aws-lambda", {
config: {
builder: "vite",
rootDir: fileURLToPath(new URL("../vite/lambda-ssr-fixture", import.meta.url)),
awsLambda: {
streaming: true,
},
},
outDirSuffix: "-streaming-vite-ssr",
});

it("handles SSR services with named fetch exports on the streaming handler path", async () => {
const { handler } = await import(resolve(ctx.outDir, "server/index.mjs"));
const event = {
rawPath: "/",
headers: {
host: "localhost",
},
rawQueryString: "",
queryStringParameters: {},
body: "",
isBase64Encoded: false,
version: "2",
routeKey: "",
requestContext: {
accountId: "",
apiId: "",
domainName: "localhost",
domainPrefix: "",
requestId: "",
routeKey: "",
stage: "",
time: "",
timeEpoch: 0,
http: {
path: "/",
protocol: "http",
userAgent: "",
sourceIp: "",
method: "GET",
},
},
} satisfies APIGatewayProxyEventV2;

const response = await handler(event);

expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toMatchObject({
ok: true,
});
});
});

function webResponse(awsResponse: any) {
const headers = new Headers(awsResponse.headers);
const setCookie =
Expand Down
14 changes: 13 additions & 1 deletion test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -767,14 +767,26 @@ export function testNitro(
});

describe("async context", () => {
it.skipIf(!ctx.nitro!.options.node)("works", async () => {
it.skipIf(!ctx.nitro!.options.node || ctx.lambdaV1)("works", async () => {
const { data } = await callHandler({ url: "/context?foo" });
expect(data).toMatchObject({
context: {
path: "/context?foo",
},
});
});

it.runIf(ctx.nitro!.options.node && ctx.lambdaV1)(
"works with Lambda v1 query string normalization",
async () => {
const { data } = await callHandler({ url: "/context?foo" });
expect(data).toMatchObject({
context: {
path: "/context?foo=",
},
});
}
);
});

describe.skipIf(!ctx.supportsEnv)("environment variables", () => {
Expand Down
9 changes: 9 additions & 0 deletions test/vite/lambda-ssr-fixture/app/entry-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
async function render(_request: Request) {
return Response.json({
ok: true,
});
}

export { render as fetch };

export default render;
6 changes: 6 additions & 0 deletions test/vite/lambda-ssr-fixture/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
plugins: [nitro({ serverDir: "./" })],
});
Loading