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 { ApiConfig, ApiRouteType } from '../helpers/analysis' import type { NextConfig } from '../helpers/config' import type { NextServerType } from './handlerUtils' /* eslint-disable @typescript-eslint/no-var-requires */ 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 { getMultiValueHeaders, 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 const makeHandler = (conf: NextConfig, app, pageRoot, page) => { // 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 {} // 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._BYPASS_SSG = 'true' for (const [key, value] of Object.entries(conf.env)) { process.env[key] = String(value) } // 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 } // Scheduled functions don't have a URL, but we need to give one so Next knows the route to serve const url = event.rawUrl ? new URL(event.rawUrl) : new URL(path, process.env.URL || 'http://n') const port = Number.parseInt(url.port) || 80 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) { // Ensure that paths are encoded - but don't double-encode them event.path = event.rawUrl ? new URL(event.rawUrl).pathname : page // 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 // We know the page event.headers['x-matched-path'] = page const { headers, ...result } = await getBridge(event).launcher(event, context) // Convert all headers to multiValueHeaders const multiValueHeaders = getMultiValueHeaders(headers) multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate'] console.log(`[${event.httpMethod}] ${event.path} (API)`) return { ...result, multiValueHeaders, isBase64Encoded: result.encoding === 'base64', } } } /** * Handlers for API routes are simpler than page routes, but they each have a separate one */ export const getApiHandler = ({ page, config, publishDir = '../../../.next', appDir = '../../..', }: { page: string config: ApiConfig publishDir?: string appDir?: string }): string => // This is a string, but if you have the right editor plugin it should format as js javascript/* javascript */ ` const { Server } = require("http"); // We copy the file here rather than requiring from the node module const { Bridge } = require("./bridge"); const { getMaxAge, getMultiValueHeaders, getNextServer } = require('./handlerUtils') ${config.type === ApiRouteType.SCHEDULED ? `const { schedule } = require("@netlify/functions")` : ''} const { config } = require("${publishDir}/required-server-files.json") let staticManifest const path = require("path"); const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "server")); const handler = (${makeHandler.toString()})(config, "${appDir}", pageRoot, ${JSON.stringify(page)}) exports.handler = ${ config.type === ApiRouteType.SCHEDULED ? `schedule(${JSON.stringify(config.schedule)}, handler);` : 'handler' } `