import { HandlerContext, HandlerEvent } from '@netlify/functions' import type { Bridge as NodeBridge } from '@vercel/node-bridge/bridge' // Aliasing like this means the editor may be able to syntax-highlight the string import { outdent as javascript } from 'outdent' import type { NextConfig } from '../helpers/config' import type { NextServerType } from './handlerUtils' /* eslint-disable @typescript-eslint/no-var-requires */ const { promises } = require('fs') const { Server } = require('http') const path = require('path') // eslint-disable-next-line n/prefer-global/url, n/prefer-global/url-search-params const { URLSearchParams, URL } = require('url') const { Bridge } = require('@vercel/node-bridge/bridge') const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, getNextServer, } = require('./handlerUtils') /* eslint-enable @typescript-eslint/no-var-requires */ type Mutable = { -readonly [K in keyof T]: T[K] } // We return a function and then call `toString()` on it to serialise it as the launcher function // eslint-disable-next-line max-params const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[string, string]> = [], mode = 'ssr') => { // Change working directory into the site root, unless using Nx, which moves the // dist directory and handles this itself const dir = path.resolve(__dirname, app) if (pageRoot.startsWith(dir)) { process.chdir(dir) } // This is just so nft knows about the page entrypoints. It's not actually used try { // eslint-disable-next-line n/no-missing-require require.resolve('./pages.js') } catch {} const ONE_YEAR_IN_SECONDS = 31536000 // React assumes you want development mode if NODE_ENV is unset. ;(process.env as Mutable).NODE_ENV ||= 'production' // We don't want to write ISR files to disk in the lambda environment conf.experimental.isrFlushToDisk = false // This is our flag that we use when patching the source // eslint-disable-next-line no-underscore-dangle process.env._REVALIDATE_SSG = 'true' for (const [key, value] of Object.entries(conf.env)) { process.env[key] = String(value) } // Set during the request as it needs to get it from the request URL. Defaults to the URL env var let base = process.env.URL augmentFsModule({ promises, staticManifest, pageRoot, getBase: () => base }) // We memoize this because it can be shared between requests, but don't instantiate it until // the first request because we need the host and port. let bridge: NodeBridge const getBridge = (event: HandlerEvent): NodeBridge => { if (bridge) { return bridge } const url = new URL(event.rawUrl) const port = Number.parseInt(url.port) || 80 base = url.origin const NextServer: NextServerType = getNextServer() const nextServer = new NextServer({ conf, dir, customServer: false, hostname: url.hostname, port, }) const requestHandler = nextServer.getRequestHandler() const server = new Server(async (req, res) => { try { await requestHandler(req, res) } catch (error) { console.error(error) throw new Error('Error handling request. See function logs for details.') } }) bridge = new Bridge(server) bridge.listen() return bridge } return async function handler(event: HandlerEvent, context: HandlerContext) { let requestMode = mode const prefetchResponse = getPrefetchResponse(event, mode) if (prefetchResponse) { return prefetchResponse } // Ensure that paths are encoded - but don't double-encode them event.path = new URL(event.rawUrl).pathname // Next expects to be able to parse the query from the URL const query = new URLSearchParams(event.queryStringParameters).toString() event.path = query ? `${event.path}?${query}` : event.path const graphToken = event.netlifyGraphToken if (graphToken && requestMode !== 'ssr') { // Prefix with underscore to help us determine the origin of the token // allows us to write better error messages // eslint-disable-next-line no-underscore-dangle process.env._NETLIFY_GRAPH_TOKEN = graphToken } const { headers, ...result } = await getBridge(event).launcher(event, context) // Convert all headers to multiValueHeaders const multiValueHeaders = getMultiValueHeaders(headers) if (event.headers['x-next-debug-logging']) { const response = { headers: multiValueHeaders, statusCode: result.statusCode, } console.log('Origin response:', JSON.stringify(response, null, 2)) } if (multiValueHeaders['set-cookie']?.[0]?.includes('__prerender_bypass')) { delete multiValueHeaders.etag multiValueHeaders['cache-control'] = ['no-cache'] } // Sending SWR headers causes undefined behaviour with the Netlify CDN const cacheHeader = multiValueHeaders['cache-control']?.[0] if (cacheHeader?.includes('stale-while-revalidate')) { if (requestMode === 'odb') { const ttl = getMaxAge(cacheHeader) // Long-expiry TTL is basically no TTL, so we'll skip it if (ttl > 0 && ttl < ONE_YEAR_IN_SECONDS) { // ODBs currently have a minimum TTL of 60 seconds result.ttl = Math.max(ttl, 60) } const ephemeralCodes = [301, 302, 307, 308, 404] if (ttl === ONE_YEAR_IN_SECONDS && ephemeralCodes.includes(result.statusCode)) { // Only cache for 60s if default TTL provided result.ttl = 60 } if (result.ttl > 0) { requestMode = `odb ttl=${result.ttl}` } } multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate'] } multiValueHeaders['x-nf-render-mode'] = [requestMode] console.log(`[${event.httpMethod}] ${event.path} (${requestMode?.toUpperCase()})`) return { ...result, multiValueHeaders, isBase64Encoded: result.encoding === 'base64', } } } export const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..' }): string => // This is a string, but if you have the right editor plugin it should format as js javascript/* javascript */ ` const { Server } = require("http"); const { promises } = require("fs"); // We copy the file here rather than requiring from the node module const { Bridge } = require("./bridge"); const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, getNextServer } = require('./handlerUtils') ${isODB ? `const { builder } = require("@netlify/functions")` : ''} const { config } = require("${publishDir}/required-server-files.json") let staticManifest try { staticManifest = require("${publishDir}/static-manifest.json") } catch {} const path = require("path"); const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "server")); exports.handler = ${ isODB ? `builder((${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'odb'));` : `(${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'ssr');` } `