UNPKG

@netlify/next-runtime

Version:
111 lines (110 loc) 4.27 kB
import glob from 'fast-glob'; import { readFile } from 'node:fs/promises'; import { basename, dirname, extname, resolve } from 'node:path'; import { join as joinPosix } from 'node:path/posix'; import { cpus } from 'os'; import pLimit from 'p-limit'; import { getBlobStore } from '../blob.js'; import { getPrerenderManifest } from '../config.js'; // static prerendered pages content with JSON data const isPage = (key, routes) => { return key.startsWith('server/pages') && routes.includes(key.replace(/^server\/pages/, '')); }; // static prerendered app content with RSC data const isApp = (path) => { return path.startsWith('server/app') && extname(path) === '.html'; }; // static prerendered app route handler const isRoute = (path) => { return path.startsWith('server/app') && extname(path) === '.body'; }; // fetch cache data (excluding tags manifest) const isFetch = (path) => { return path.startsWith('cache/fetch-cache') && extname(path) === ''; }; /** * Transform content file paths into cache entries for the blob store */ const buildPrerenderedContentEntries = async (src, routes) => { const paths = await glob([`cache/fetch-cache/*`, `server/+(app|pages)/**/*.+(html|body)`], { cwd: resolve(src), extglob: true, }); return paths.map(async (path) => { const key = joinPosix(dirname(path), basename(path, extname(path))); let value; if (isPage(key, routes)) { value = { kind: 'PAGE', html: await readFile(resolve(src, `${key}.html`), 'utf-8'), pageData: JSON.parse(await readFile(resolve(src, `${key}.json`), 'utf-8')), }; } if (isApp(path)) { value = { kind: 'PAGE', html: await readFile(resolve(src, `${key}.html`), 'utf-8'), pageData: await readFile(resolve(src, `${key}.rsc`), 'utf-8'), ...JSON.parse(await readFile(resolve(src, `${key}.meta`), 'utf-8')), }; } if (isRoute(path)) { value = { kind: 'ROUTE', body: await readFile(resolve(src, `${key}.body`), 'utf-8'), ...JSON.parse(await readFile(resolve(src, `${key}.meta`), 'utf-8')), }; } if (isFetch(path)) { value = { kind: 'FETCH', ...JSON.parse(await readFile(resolve(src, key), 'utf-8')), }; } return { key, value: { lastModified: Date.now(), value, }, }; }); }; /** * Upload prerendered content to the blob store and remove it from the bundle */ export const uploadPrerenderedContent = async ({ constants: { PUBLISH_DIR, NETLIFY_API_TOKEN, NETLIFY_API_HOST, SITE_ID }, }) => { // limit concurrent uploads to 2x the number of CPUs const limit = pLimit(Math.max(2, cpus().length)); // read prerendered content and build JSON key/values for the blob store let manifest; let blob; try { manifest = await getPrerenderManifest({ PUBLISH_DIR }); blob = getBlobStore({ NETLIFY_API_TOKEN, NETLIFY_API_HOST, SITE_ID }); } catch (error) { console.error(`Unable to upload prerendered content: ${error.message}`); return; } const entries = await Promise.allSettled(await buildPrerenderedContentEntries(PUBLISH_DIR, Object.keys(manifest.routes))); entries.forEach((result) => { if (result.status === 'rejected') { console.error(`Unable to read prerendered content: ${result.reason.message}`); } }); // upload JSON content data to the blob store const uploads = await Promise.allSettled(entries .filter((entry) => entry.status === 'fulfilled' && entry.value.value.value !== undefined) .map((entry) => { const result = entry; const { key, value } = result.value; return limit(() => blob.setJSON(key, value)); })); uploads.forEach((upload, index) => { if (upload.status === 'rejected') { const result = entries[index]; console.error(`Unable to store ${result.value?.key}: ${upload.reason.message}`); } }); };