|
| 1 | +/* eslint-disable @typescript-eslint/no-var-requires */ |
| 2 | +import type { NextConfig } from '../helpers/config' |
| 3 | + |
| 4 | +const { promises } = require('fs') |
| 5 | +const { Server } = require('http') |
| 6 | +const path = require('path') |
| 7 | +// eslint-disable-next-line node/prefer-global/url, node/prefer-global/url-search-params |
| 8 | +const { URLSearchParams, URL } = require('url') |
| 9 | + |
| 10 | +const { Bridge } = require('@vercel/node/dist/bridge') |
| 11 | + |
| 12 | +const { augmentFsModule, getMaxAge, getMultiValueHeaders, getNextServer } = require('./handlerUtils') |
| 13 | + |
| 14 | +const makeHandler = |
| 15 | + () => |
| 16 | + // We return a function and then call `toString()` on it to serialise it as the launcher function |
| 17 | + // eslint-disable-next-line max-params |
| 18 | + (conf: NextConfig, app, pageRoot, staticManifest: Array<[string, string]> = [], mode = 'ssr') => { |
| 19 | + // This is just so nft knows about the page entrypoints. It's not actually used |
| 20 | + try { |
| 21 | + // eslint-disable-next-line node/no-missing-require |
| 22 | + require.resolve('./pages.js') |
| 23 | + } catch {} |
| 24 | + // eslint-disable-next-line no-underscore-dangle |
| 25 | + process.env._BYPASS_SSG = 'true' |
| 26 | + |
| 27 | + const ONE_YEAR_IN_SECONDS = 31536000 |
| 28 | + |
| 29 | + // We don't want to write ISR files to disk in the lambda environment |
| 30 | + conf.experimental.isrFlushToDisk = false |
| 31 | + |
| 32 | + // Set during the request as it needs the host header. Hoisted so we can define the function once |
| 33 | + let base |
| 34 | + |
| 35 | + augmentFsModule({ promises, staticManifest, pageRoot, getBase: () => base }) |
| 36 | + |
| 37 | + const NextServer = getNextServer() |
| 38 | + |
| 39 | + const nextServer = new NextServer({ |
| 40 | + conf, |
| 41 | + dir: path.resolve(__dirname, app), |
| 42 | + customServer: false, |
| 43 | + }) |
| 44 | + const requestHandler = nextServer.getRequestHandler() |
| 45 | + const server = new Server(async (req, res) => { |
| 46 | + try { |
| 47 | + await requestHandler(req, res) |
| 48 | + } catch (error) { |
| 49 | + console.error(error) |
| 50 | + throw new Error('server function error') |
| 51 | + } |
| 52 | + }) |
| 53 | + const bridge = new Bridge(server) |
| 54 | + bridge.listen() |
| 55 | + |
| 56 | + return async (event, context) => { |
| 57 | + let requestMode = mode |
| 58 | + // Ensure that paths are encoded - but don't double-encode them |
| 59 | + event.path = new URL(event.path, event.rawUrl).pathname |
| 60 | + // Next expects to be able to parse the query from the URL |
| 61 | + const query = new URLSearchParams(event.queryStringParameters).toString() |
| 62 | + event.path = query ? `${event.path}?${query}` : event.path |
| 63 | + // Only needed if we're intercepting static files |
| 64 | + if (staticManifest.length !== 0) { |
| 65 | + const { host } = event.headers |
| 66 | + const protocol = event.headers['x-forwarded-proto'] || 'http' |
| 67 | + base = `${protocol}://${host}` |
| 68 | + } |
| 69 | + const { headers, ...result } = await bridge.launcher(event, context) |
| 70 | + |
| 71 | + // Convert all headers to multiValueHeaders |
| 72 | + |
| 73 | + const multiValueHeaders = getMultiValueHeaders(headers) |
| 74 | + |
| 75 | + if (multiValueHeaders['set-cookie']?.[0]?.includes('__prerender_bypass')) { |
| 76 | + delete multiValueHeaders.etag |
| 77 | + multiValueHeaders['cache-control'] = ['no-cache'] |
| 78 | + } |
| 79 | + |
| 80 | + // Sending SWR headers causes undefined behaviour with the Netlify CDN |
| 81 | + const cacheHeader = multiValueHeaders['cache-control']?.[0] |
| 82 | + |
| 83 | + if (cacheHeader?.includes('stale-while-revalidate')) { |
| 84 | + if (requestMode === 'odb' && process.env.EXPERIMENTAL_ODB_TTL) { |
| 85 | + requestMode = 'isr' |
| 86 | + const ttl = getMaxAge(cacheHeader) |
| 87 | + // Long-expiry TTL is basically no TTL |
| 88 | + if (ttl > 0 && ttl < ONE_YEAR_IN_SECONDS) { |
| 89 | + result.ttl = ttl |
| 90 | + } |
| 91 | + multiValueHeaders['x-rendered-at'] = [new Date().toISOString()] |
| 92 | + } |
| 93 | + multiValueHeaders['cache-control'] = ['public, max-age=0, must-revalidate'] |
| 94 | + } |
| 95 | + multiValueHeaders['x-render-mode'] = [requestMode] |
| 96 | + return { |
| 97 | + ...result, |
| 98 | + multiValueHeaders, |
| 99 | + isBase64Encoded: result.encoding === 'base64', |
| 100 | + } |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | +export const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..' }): string => ` |
| 105 | +const { Server } = require("http"); |
| 106 | +const { promises } = require("fs"); |
| 107 | +// We copy the file here rather than requiring from the node module |
| 108 | +const { Bridge } = require("./bridge"); |
| 109 | +const { augmentFsModule, getMaxAge, getMultiValueHeaders, getNextServer } = require('./handlerUtils') |
| 110 | +
|
| 111 | +const { builder } = require("@netlify/functions"); |
| 112 | +const { config } = require("${publishDir}/required-server-files.json") |
| 113 | +let staticManifest |
| 114 | +try { |
| 115 | + staticManifest = require("${publishDir}/static-manifest.json") |
| 116 | +} catch {} |
| 117 | +const path = require("path"); |
| 118 | +const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", config.target === "server" ? "server" : "serverless", "pages")); |
| 119 | +exports.handler = ${ |
| 120 | + isODB |
| 121 | + ? `builder((${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest, 'odb'));` |
| 122 | + : `(${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest, 'ssr');` |
| 123 | +} |
| 124 | +` |
| 125 | +/* eslint-enable @typescript-eslint/no-var-requires */ |
0 commit comments