|
| 1 | +const { promises, createWriteStream, existsSync } = require('fs') |
1 | 2 | const { Server } = require('http')
|
| 3 | +const { tmpdir } = require('os') |
2 | 4 | const path = require('path')
|
| 5 | +const { promisify } = require('util') |
3 | 6 |
|
| 7 | +const streamPipeline = promisify(require('stream').pipeline) |
4 | 8 | const { Bridge } = require('@vercel/node/dist/bridge')
|
| 9 | +const fetch = require('node-fetch') |
5 | 10 |
|
6 | 11 | const makeHandler =
|
7 | 12 | () =>
|
8 | 13 | // We return a function and then call `toString()` on it to serialise it as the launcher function
|
9 |
| - (conf, app) => { |
10 |
| - // This is just so nft knows about the page entrypoints |
| 14 | + (conf, app, pageRoot, staticManifest = []) => { |
| 15 | + // This is just so nft knows about the page entrypoints. It's not actually used |
11 | 16 | try {
|
12 | 17 | // eslint-disable-next-line node/no-missing-require
|
13 | 18 | require.resolve('./pages.js')
|
14 | 19 | } catch {}
|
15 | 20 |
|
| 21 | + // Set during the request as it needs the host header. Hoisted so we can define the function once |
| 22 | + let base |
| 23 | + |
| 24 | + // Only do this if we have some static files moved to the CDN |
| 25 | + if (staticManifest.length !== 0) { |
| 26 | + // These are static page files that have been removed from the function bundle |
| 27 | + // In most cases these are served from the CDN, but for rewrites Next may try to read them |
| 28 | + // from disk. We need to intercept these and load them from the CDN instead |
| 29 | + // Sadly the only way to do this is to monkey-patch fs.promises. Yeah, I know. |
| 30 | + const staticFiles = new Set(staticManifest) |
| 31 | + |
| 32 | + // Yes, you can cache stuff locally in a Lambda |
| 33 | + const cacheDir = path.join(tmpdir(), 'next-static-cache') |
| 34 | + // Grab the real fs.promises.readFile... |
| 35 | + const readfileOrig = promises.readFile |
| 36 | + // ...then money-patch it to see if it's requesting a CDN file |
| 37 | + promises.readFile = async (file, options) => { |
| 38 | + // We only care about page files |
| 39 | + if (file.startsWith(pageRoot)) { |
| 40 | + // We only want the part after `pages/` |
| 41 | + const filePath = file.slice(pageRoot.length + 1) |
| 42 | + // Is it in the CDN and not local? |
| 43 | + if (staticFiles.has(filePath) && !existsSync(file)) { |
| 44 | + // This name is safe to use, because it's one that was already created by Next |
| 45 | + const cacheFile = path.join(cacheDir, filePath) |
| 46 | + // Have we already cached it? We ignore the cache if running locally to avoid staleness |
| 47 | + if ((!existsSync(cacheFile) || process.env.NETLIFY_DEV) && base) { |
| 48 | + await promises.mkdir(path.dirname(cacheFile), { recursive: true }) |
| 49 | + |
| 50 | + // Append the path to our host and we can load it like a regular page |
| 51 | + const url = `${base}/${filePath}` |
| 52 | + console.log(`Downloading ${url} to ${cacheFile}`) |
| 53 | + const response = await fetch(url) |
| 54 | + if (!response.ok) { |
| 55 | + // Next catches this and returns it as a not found file |
| 56 | + throw new Error(`Failed to fetch ${url}`) |
| 57 | + } |
| 58 | + // Stream it to disk |
| 59 | + await streamPipeline(response.body, createWriteStream(cacheFile)) |
| 60 | + } |
| 61 | + // Return the cache file |
| 62 | + return readfileOrig(cacheFile, options) |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + return readfileOrig(file, options) |
| 67 | + } |
| 68 | + } |
16 | 69 | let NextServer
|
17 | 70 | try {
|
18 | 71 | // next >= 11.0.1. Yay breaking changes in patch releases!
|
@@ -59,7 +112,12 @@ const makeHandler =
|
59 | 112 | // Next expects to be able to parse the query from the URL
|
60 | 113 | const query = new URLSearchParams(event.queryStringParameters).toString()
|
61 | 114 | event.path = query ? `${event.path}?${query}` : event.path
|
62 |
| - |
| 115 | + // Only needed if we're intercepting static files |
| 116 | + if (staticManifest.length !== 0) { |
| 117 | + const { host } = event.headers |
| 118 | + const protocol = event.headers['x-forwarded-proto'] || 'http' |
| 119 | + base = `${protocol}://${host}` |
| 120 | + } |
63 | 121 | const { headers, ...result } = await bridge.launcher(event, context)
|
64 | 122 | /** @type import("@netlify/functions").HandlerResponse */
|
65 | 123 |
|
@@ -88,15 +146,26 @@ const makeHandler =
|
88 | 146 |
|
89 | 147 | const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..' }) => `
|
90 | 148 | const { Server } = require("http");
|
| 149 | +const { tmpdir } = require('os') |
| 150 | +const { promises, createWriteStream, existsSync } = require("fs"); |
| 151 | +const { promisify } = require('util') |
| 152 | +const streamPipeline = promisify(require('stream').pipeline) |
91 | 153 | // We copy the file here rather than requiring from the node module
|
92 | 154 | const { Bridge } = require("./bridge");
|
| 155 | +const fetch = require('node-fetch') |
| 156 | +
|
93 | 157 | const { builder } = require("@netlify/functions");
|
94 | 158 | const { config } = require("${publishDir}/required-server-files.json")
|
| 159 | +let staticManifest |
| 160 | +try { |
| 161 | + staticManifest = require("${publishDir}/static-manifest.json") |
| 162 | +} catch {} |
95 | 163 | const path = require("path");
|
| 164 | +const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", config.target === "server" ? "server" : "serverless", "pages")); |
96 | 165 | exports.handler = ${
|
97 | 166 | isODB
|
98 |
| - ? `builder((${makeHandler().toString()})(config, "${appDir}"));` |
99 |
| - : `(${makeHandler().toString()})(config, "${appDir}");` |
| 167 | + ? `builder((${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest));` |
| 168 | + : `(${makeHandler().toString()})(config, "${appDir}", pageRoot, staticManifest);` |
100 | 169 | }
|
101 | 170 | `
|
102 | 171 |
|
|
0 commit comments