summaryrefslogtreecommitdiff
path: root/packages/integrations/netlify/src/netlify-functions.ts
diff options
context:
space:
mode:
authorGravatar Alexander Niebuhr <alexander@nbhr.io> 2023-10-13 09:26:07 +0200
committerGravatar Alexander Niebuhr <alexander@nbhr.io> 2023-10-13 09:26:07 +0200
commitb750be65ff69c5c219f3f74abbc1e6f8a64e6830 (patch)
tree35d2be4293d14f6b404611a569d3e7c554f0b8e8 /packages/integrations/netlify/src/netlify-functions.ts
parent93a1db68cd9cf3bb2a4d9f7a8af13cbd881eb701 (diff)
parentbf225d6df1fa25baa5f4cd0bc3a7c6a28d9b51ab (diff)
downloadastro-b750be65ff69c5c219f3f74abbc1e6f8a64e6830.tar.gz
astro-b750be65ff69c5c219f3f74abbc1e6f8a64e6830.tar.zst
astro-b750be65ff69c5c219f3f74abbc1e6f8a64e6830.zip
chore(netlify): migrate from `withastro/astro` to `withastro/adapters`
Diffstat (limited to 'packages/integrations/netlify/src/netlify-functions.ts')
-rw-r--r--packages/integrations/netlify/src/netlify-functions.ts225
1 files changed, 225 insertions, 0 deletions
diff --git a/packages/integrations/netlify/src/netlify-functions.ts b/packages/integrations/netlify/src/netlify-functions.ts
new file mode 100644
index 000000000..8c051d9f6
--- /dev/null
+++ b/packages/integrations/netlify/src/netlify-functions.ts
@@ -0,0 +1,225 @@
+import { builder, type Handler } from '@netlify/functions';
+import type { SSRManifest } from 'astro';
+import { App } from 'astro/app';
+import { applyPolyfills } from 'astro/app/node';
+import { ASTRO_LOCALS_HEADER } from './integration-functions.js';
+
+applyPolyfills();
+
+export interface Args {
+ builders?: boolean;
+ binaryMediaTypes?: string[];
+ edgeMiddleware: boolean;
+ functionPerRoute: boolean;
+}
+
+function parseContentType(header?: string) {
+ return header?.split(';')[0] ?? '';
+}
+
+const clientAddressSymbol = Symbol.for('astro.clientAddress');
+
+export const createExports = (manifest: SSRManifest, args: Args) => {
+ const app = new App(manifest);
+
+ const builders = args.builders ?? false;
+ const binaryMediaTypes = args.binaryMediaTypes ?? [];
+ const knownBinaryMediaTypes = new Set([
+ 'audio/3gpp',
+ 'audio/3gpp2',
+ 'audio/aac',
+ 'audio/midi',
+ 'audio/mpeg',
+ 'audio/ogg',
+ 'audio/opus',
+ 'audio/wav',
+ 'audio/webm',
+ 'audio/x-midi',
+ 'image/avif',
+ 'image/bmp',
+ 'image/gif',
+ 'image/vnd.microsoft.icon',
+ 'image/heif',
+ 'image/jpeg',
+ 'image/png',
+ 'image/svg+xml',
+ 'image/tiff',
+ 'image/webp',
+ 'video/3gpp',
+ 'video/3gpp2',
+ 'video/mp2t',
+ 'video/mp4',
+ 'video/mpeg',
+ 'video/ogg',
+ 'video/x-msvideo',
+ 'video/webm',
+ ...binaryMediaTypes,
+ ]);
+
+ const myHandler: Handler = async (event) => {
+ const { httpMethod, headers, rawUrl, body: requestBody, isBase64Encoded } = event;
+ const init: RequestInit = {
+ method: httpMethod,
+ headers: new Headers(headers as any),
+ };
+ // Attach the event body the request, with proper encoding.
+ if (httpMethod !== 'GET' && httpMethod !== 'HEAD') {
+ const encoding = isBase64Encoded ? 'base64' : 'utf-8';
+ init.body =
+ typeof requestBody === 'string' ? Buffer.from(requestBody, encoding) : requestBody;
+ }
+
+ const request = new Request(rawUrl, init);
+
+ const routeData = app.match(request);
+ const ip = headers['x-nf-client-connection-ip'];
+ Reflect.set(request, clientAddressSymbol, ip);
+
+ let locals: Record<string, unknown> = {};
+
+ if (request.headers.has(ASTRO_LOCALS_HEADER)) {
+ let localsAsString = request.headers.get(ASTRO_LOCALS_HEADER);
+ if (localsAsString) {
+ locals = JSON.parse(localsAsString);
+ }
+ }
+
+ let responseTtl = undefined;
+
+ locals.runtime = builders
+ ? {
+ setBuildersTtl(ttl: number) {
+ responseTtl = ttl;
+ },
+ }
+ : {};
+
+ const response: Response = await app.render(request, routeData, locals);
+ const responseHeaders = Object.fromEntries(response.headers.entries());
+
+ const responseContentType = parseContentType(responseHeaders['content-type']);
+ const responseIsBase64Encoded = knownBinaryMediaTypes.has(responseContentType);
+
+ let responseBody: string;
+ if (responseIsBase64Encoded) {
+ const ab = await response.arrayBuffer();
+ responseBody = Buffer.from(ab).toString('base64');
+ } else {
+ responseBody = await response.text();
+ }
+
+ const fnResponse: any = {
+ statusCode: response.status,
+ headers: responseHeaders,
+ body: responseBody,
+ isBase64Encoded: responseIsBase64Encoded,
+ ttl: responseTtl,
+ };
+
+ const cookies = response.headers.get('set-cookie');
+ if (cookies) {
+ fnResponse.multiValueHeaders = {
+ 'set-cookie': Array.isArray(cookies) ? cookies : splitCookiesString(cookies),
+ };
+ }
+
+ // Apply cookies set via Astro.cookies.set/delete
+ if (app.setCookieHeaders) {
+ const setCookieHeaders = Array.from(app.setCookieHeaders(response));
+ fnResponse.multiValueHeaders = fnResponse.multiValueHeaders || {};
+ if (!fnResponse.multiValueHeaders['set-cookie']) {
+ fnResponse.multiValueHeaders['set-cookie'] = [];
+ }
+ fnResponse.multiValueHeaders['set-cookie'].push(...setCookieHeaders);
+ }
+
+ return fnResponse;
+ };
+
+ const handler = builders ? builder(myHandler) : myHandler;
+
+ return { handler };
+};
+
+/*
+ From: https://github.com/nfriedly/set-cookie-parser/blob/5cae030d8ef0f80eec58459e3583d43a07b984cb/lib/set-cookie.js#L144
+ Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas
+ that are within a single set-cookie field-value, such as in the Expires portion.
+ This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2
+ Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128
+ React Native's fetch does this for *every* header, including set-cookie.
+ Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25
+ Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation
+*/
+function splitCookiesString(cookiesString: string): string[] {
+ if (Array.isArray(cookiesString)) {
+ return cookiesString;
+ }
+ if (typeof cookiesString !== 'string') {
+ return [];
+ }
+
+ let cookiesStrings = [];
+ let pos = 0;
+ let start;
+ let ch;
+ let lastComma;
+ let nextStart;
+ let cookiesSeparatorFound;
+
+ function skipWhitespace() {
+ while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) {
+ pos += 1;
+ }
+ return pos < cookiesString.length;
+ }
+
+ function notSpecialChar() {
+ ch = cookiesString.charAt(pos);
+
+ return ch !== '=' && ch !== ';' && ch !== ',';
+ }
+
+ while (pos < cookiesString.length) {
+ start = pos;
+ cookiesSeparatorFound = false;
+
+ while (skipWhitespace()) {
+ ch = cookiesString.charAt(pos);
+ if (ch === ',') {
+ // ',' is a cookie separator if we have later first '=', not ';' or ','
+ lastComma = pos;
+ pos += 1;
+
+ skipWhitespace();
+ nextStart = pos;
+
+ while (pos < cookiesString.length && notSpecialChar()) {
+ pos += 1;
+ }
+
+ // currently special character
+ if (pos < cookiesString.length && cookiesString.charAt(pos) === '=') {
+ // we found cookies separator
+ cookiesSeparatorFound = true;
+ // pos is inside the next cookie, so back up and return it.
+ pos = nextStart;
+ cookiesStrings.push(cookiesString.substring(start, lastComma));
+ start = pos;
+ } else {
+ // in param ',' or param separator ';',
+ // we continue from that comma
+ pos = lastComma + 1;
+ }
+ } else {
+ pos += 1;
+ }
+ }
+
+ if (!cookiesSeparatorFound || pos >= cookiesString.length) {
+ cookiesStrings.push(cookiesString.substring(start, cookiesString.length));
+ }
+ }
+
+ return cookiesStrings;
+}