sveltekit-lambda-adapter
Version:
Adapter for SvelteKit apps to work on AWS Lambda
123 lines (108 loc) • 3.6 kB
text/typescript
import fs from 'node:fs';
import path from 'node:path';
import { installPolyfills } from '@sveltejs/kit/node/polyfills';
import { Server as ServerType } from '@sveltejs/kit';
// @ts-ignore
import { Server } from '../index.js';
// @ts-ignore
import { manifest } from '../manifest.js';
// @ts-ignore
import prerenderedFiles from './prerendered-file-list.js';
import type {
APIGatewayProxyEventV2,
APIGatewayProxyEvent,
CloudFrontRequestEvent
} from 'aws-lambda';
import { InternalEvent, convertFrom, convertTo } from './event-mapper.js';
import { isBinaryContentType } from './binary.js';
installPolyfills();
const app: ServerType = new Server(manifest);
await app.init({ env: process.env as Record<string, string> });
export async function handler(
event: APIGatewayProxyEventV2 | CloudFrontRequestEvent | APIGatewayProxyEvent
) {
// Parse Lambda event
const internalEvent = convertFrom(event);
// Set correct host header
if (internalEvent.headers['x-forwarded-host']) {
internalEvent.headers.host = internalEvent.headers['x-forwarded-host'];
}
// Check request is for prerendered file
if (internalEvent.method === 'GET') {
const filePath = isPrerenderedFile(internalEvent.rawPath);
if (filePath) {
return internalEvent.type === 'cf'
? formatCloudFrontPrerenderedResponse(event as CloudFrontRequestEvent, filePath)
: formatAPIGatewayPrerenderedResponse(internalEvent, filePath);
}
}
// Process request
const requestUrl = `https://${internalEvent.headers.host}${internalEvent.url}`;
const requestProps = {
method: internalEvent.method,
headers: internalEvent.headers,
body: ['GET', 'HEAD'].includes(internalEvent.method) ? undefined : internalEvent.body
};
const request = new Request(requestUrl, requestProps);
const response: Response = await app.respond(request, {
getClientAddress: () => internalEvent.remoteAddress
});
//Parse the response into lambda proxy response
if (response) {
const headers: Record<string, string[]> = {};
response.headers.forEach((value, key) => {
headers[key] = headers[key] || [];
headers[key].push(value);
});
const isBase64Encoded = isBinaryContentType(
Array.isArray(headers['content-type']) ? headers['content-type'][0] : headers['content-type']
);
const body = isBase64Encoded
? Buffer.from(await response.arrayBuffer()).toString('base64')
: await response.text();
return convertTo({
type: internalEvent.type,
statusCode: response.status,
headers,
isBase64Encoded,
body
});
}
return {
statusCode: 404,
body: 'Not found.'
};
}
function isPrerenderedFile(uri: string) {
// remove leading and trailing slashes
uri = uri.replace(/^\/|\/$/g, '');
if (uri === '') {
return prerenderedFiles.includes('index.html') ? 'index.html' : undefined;
}
if (prerenderedFiles.includes(uri)) {
return uri;
}
if (prerenderedFiles.includes(uri + '/index.html')) {
return uri + '/index.html';
}
if (prerenderedFiles.includes(uri + '.html')) {
return uri + '.html';
}
}
function formatCloudFrontPrerenderedResponse(event: CloudFrontRequestEvent, filePath: string) {
const request = event.Records[0].cf.request;
request.uri = `/${filePath}`;
return request;
}
function formatAPIGatewayPrerenderedResponse(internalEvent: InternalEvent, filePath: string) {
return convertTo({
type: internalEvent.type,
statusCode: 200,
headers: {
'content-type': 'text/html',
'cache-control': 'public, max-age=0, s-maxage=31536000, must-revalidate'
},
isBase64Encoded: false,
body: fs.readFileSync(path.join('prerendered', filePath), 'utf8')
});
}