diff --git a/src/presets/netlify.ts b/src/presets/netlify.ts index fcfe2cdef3..73a9c7c9f7 100644 --- a/src/presets/netlify.ts +++ b/src/presets/netlify.ts @@ -6,6 +6,7 @@ import type { Nitro } from '../types' // Netlify functions export const netlify = defineNitroPreset({ extends: 'aws-lambda', + entry: '#internal/nitro/entries/netlify', output: { dir: '{{ rootDir }}/.netlify/functions-internal', publicDir: '{{ rootDir }}/dist' @@ -80,6 +81,11 @@ async function writeRedirects (nitro: Nitro) { const redirectsPath = join(nitro.options.output.publicDir, '_redirects') let contents = '/* /.netlify/functions/server 200' + // Rewrite SWR and static paths to builder functions + for (const [key] of Object.entries(nitro.options.routes).filter(([_, value]) => value.swr || value.static)) { + contents = `${key.replace('/**', '/*')}\t/.netlify/builders/server 200\n` + contents + } + for (const [key, value] of Object.entries(nitro.options.routes).filter(([_, value]) => value.redirect)) { const redirect = typeof value.redirect === 'string' ? { to: value.redirect } : value.redirect // TODO: update to 307 when netlify support 307/308 diff --git a/src/runtime/entries/netlify.ts b/src/runtime/entries/netlify.ts new file mode 100644 index 0000000000..1c7e1569fe --- /dev/null +++ b/src/runtime/entries/netlify.ts @@ -0,0 +1,59 @@ +import '#internal/nitro/virtual/polyfill' +import type { Handler, HandlerResponse, HandlerContext, HandlerEvent } from '@netlify/functions/dist/main' +import type { APIGatewayProxyEventHeaders } from 'aws-lambda' +import { withQuery } from 'ufo' +import { createRouter as createMatcher } from 'radix3' +import { nitroApp } from '../app' +import { useRuntimeConfig } from '../config' + +export const handler: Handler = async function handler (event, context) { + const config = useRuntimeConfig() + const routerOptions = createMatcher({ routes: config.nitro.routes }) + + const query = { ...event.queryStringParameters, ...event.multiValueQueryStringParameters } + const url = withQuery(event.path, query) + const routeOptions = routerOptions.lookup(url) || {} + + if (routeOptions.static || routeOptions.swr) { + const builder = await import('@netlify/functions').then(r => r.builder || r.default.builder) + const ttl = typeof routeOptions.swr === 'number' ? routeOptions.swr : 60 + const swrHandler = routeOptions.swr + ? ((event, context) => lambda(event, context).then(r => ({ ...r, ttl }))) as Handler + : lambda + return builder(swrHandler)(event, context) as any + } + + return lambda(event, context) +} + +async function lambda (event: HandlerEvent, context: HandlerContext): Promise { + const query = { ...event.queryStringParameters, ...(event).multiValueQueryStringParameters } + const url = withQuery((event).path, query) + const method = (event).httpMethod || 'get' + + const r = await nitroApp.localCall({ + event, + url, + context, + headers: normalizeIncomingHeaders(event.headers), + method, + query, + body: event.body // TODO: handle event.isBase64Encoded + }) + + return { + statusCode: r.status, + headers: normalizeOutgoingHeaders(r.headers), + body: r.body.toString() + } +} + +function normalizeIncomingHeaders (headers?: APIGatewayProxyEventHeaders) { + return Object.fromEntries(Object.entries(headers || {}).map(([key, value]) => [key.toLowerCase(), value!])) +} + +function normalizeOutgoingHeaders (headers: Record) { + return Object.fromEntries(Object.entries(headers) + .filter(([key]) => !['set-cookie'].includes(key)) + .map(([k, v]) => [k, Array.isArray(v) ? v.join(',') : v!])) +} diff --git a/src/types/nitro.ts b/src/types/nitro.ts index b28e9d473d..524e28f0b5 100644 --- a/src/types/nitro.ts +++ b/src/types/nitro.ts @@ -68,6 +68,7 @@ export interface NitroConfig extends DeepPartial { export interface NitroRouteOption { swr?: boolean | number + static?: boolean redirect?: string | { to: string, statusCode?: 301 | 302 | 307 | 308 } headers?: Record cors?: boolean diff --git a/test/presets/netlify.test.ts b/test/presets/netlify.test.ts index 3470d1f95f..8b53daa5e4 100644 --- a/test/presets/netlify.test.ts +++ b/test/presets/netlify.test.ts @@ -37,6 +37,7 @@ describe('nitro:preset:netlify', async () => { /rules/nested/* /base 301 /rules/redirect/obj https://nitro.unjs.io/ 308 /rules/redirect /base 301 + /rules/static /.netlify/builders/server 200 /* /.netlify/functions/server 200" `) /* eslint-enable no-tabs */ diff --git a/test/tests.ts b/test/tests.ts index 6a2ad3b07c..396b05eba0 100644 --- a/test/tests.ts +++ b/test/tests.ts @@ -38,6 +38,7 @@ export async function setupTest (preset) { '/rules/headers': { headers: { 'cache-control': 's-maxage=60' } }, '/rules/cors': { cors: true, headers: { 'access-control-allowed-methods': 'GET' } }, '/rules/redirect': { redirect: '/base' }, + '/rules/static': { static: true }, '/rules/redirect/obj': { redirect: { to: 'https://nitro.unjs.io/', statusCode: 308 } },