@netlify/plugin-nextjs
Version:
Run Next.js seamlessly on Netlify
385 lines (383 loc) • 16.2 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/run/handlers/cache.cts
var cache_exports = {};
__export(cache_exports, {
NetlifyCacheHandler: () => NetlifyCacheHandler,
default: () => cache_default
});
module.exports = __toCommonJS(cache_exports);
var import_node_buffer = require("node:buffer");
var import_node_path = require("node:path");
var import_posix = require("node:path/posix");
var import_constants = require("next/dist/lib/constants.js");
var import_cache_types = require("../../shared/cache-types.cjs");
var import_storage = require("../storage/storage.cjs");
var import_request_context = require("./request-context.cjs");
var import_tags_handler = require("./tags-handler.cjs");
var import_tracer = require("./tracer.cjs");
var memoizedPrerenderManifest;
var NetlifyCacheHandler = class {
options;
revalidatedTags;
cacheStore;
tracer = (0, import_tracer.getTracer)();
constructor(options) {
this.options = options;
this.revalidatedTags = options.revalidatedTags;
this.cacheStore = (0, import_storage.getMemoizedKeyValueStoreBackedByRegionalBlobStore)({ consistency: "strong" });
}
getTTL(blob) {
if (blob.value?.kind === "FETCH" || blob.value?.kind === "ROUTE" || blob.value?.kind === "APP_ROUTE" || blob.value?.kind === "PAGE" || blob.value?.kind === "PAGES" || blob.value?.kind === "APP_PAGE") {
const { revalidate } = blob.value;
if (typeof revalidate === "number") {
const revalidateAfter = revalidate * 1e3 + blob.lastModified;
return (revalidateAfter - Date.now()) / 1e3;
}
if (revalidate === false) {
return "PERMANENT";
}
}
return "NOT SET";
}
captureResponseCacheLastModified(cacheValue, key, getCacheKeySpan) {
if (cacheValue.value?.kind === "FETCH") {
return;
}
const requestContext = (0, import_request_context.getRequestContext)();
if (!requestContext) {
(0, import_tracer.recordWarning)(new Error("CacheHandler was called without a request context"), getCacheKeySpan);
return;
}
if (requestContext.responseCacheKey && requestContext.responseCacheKey !== key) {
requestContext.responseCacheGetLastModified = void 0;
(0, import_tracer.recordWarning)(
new Error(
`Multiple response cache keys used in single request: ["${requestContext.responseCacheKey}, "${key}"]`
),
getCacheKeySpan
);
return;
}
requestContext.responseCacheKey = key;
if (cacheValue.lastModified) {
requestContext.responseCacheGetLastModified = cacheValue.lastModified;
}
}
captureRouteRevalidateAndRemoveFromObject(cacheValue) {
const { revalidate, ...restOfRouteValue } = cacheValue;
const requestContext = (0, import_request_context.getRequestContext)();
if (requestContext) {
requestContext.routeHandlerRevalidate = revalidate;
}
return restOfRouteValue;
}
captureCacheTags(cacheValue, key) {
const requestContext = (0, import_request_context.getRequestContext)();
if (!requestContext) {
return;
}
if (requestContext.responseCacheTags) {
return;
}
if (!cacheValue) {
const cacheTags = [`_N_T_${key === "/index" ? "/" : encodeURI(key)}`];
requestContext.responseCacheTags = cacheTags;
return;
}
if (cacheValue.kind === "PAGE" || cacheValue.kind === "PAGES" || cacheValue.kind === "APP_PAGE" || cacheValue.kind === "ROUTE" || cacheValue.kind === "APP_ROUTE") {
if (cacheValue.headers?.[import_constants.NEXT_CACHE_TAGS_HEADER]) {
const cacheTags = cacheValue.headers[import_constants.NEXT_CACHE_TAGS_HEADER].split(/,|%2c/gi);
requestContext.responseCacheTags = cacheTags;
} else if ((cacheValue.kind === "PAGE" || cacheValue.kind === "PAGES") && typeof cacheValue.pageData === "object") {
const cacheTags = [`_N_T_${key === "/index" ? "/" : encodeURI(key)}`];
requestContext.responseCacheTags = cacheTags;
}
}
}
async getPrerenderManifest(serverDistDir) {
if (memoizedPrerenderManifest) {
return memoizedPrerenderManifest;
}
const prerenderManifestPath = (0, import_node_path.join)(serverDistDir, "..", "prerender-manifest.json");
try {
const { loadManifest } = await import("next/dist/server/load-manifest.external.js");
memoizedPrerenderManifest = loadManifest(prerenderManifestPath);
} catch {
const { loadManifest } = await import("next/dist/server/load-manifest.js");
memoizedPrerenderManifest = loadManifest(prerenderManifestPath);
}
return memoizedPrerenderManifest;
}
async injectEntryToPrerenderManifest(key, { revalidate, cacheControl }) {
if (this.options.serverDistDir && (typeof revalidate === "number" || revalidate === false || typeof cacheControl !== "undefined")) {
try {
const prerenderManifest = await this.getPrerenderManifest(this.options.serverDistDir);
if (typeof cacheControl !== "undefined") {
try {
const { SharedCacheControls } = await import(
// @ts-expect-error supporting multiple next version, this module is not resolvable with currently used dev dependency
// eslint-disable-next-line import/no-unresolved, n/no-missing-import
"next/dist/server/lib/incremental-cache/shared-cache-controls.external.js"
);
const sharedCacheControls = new SharedCacheControls(prerenderManifest);
sharedCacheControls.set(key, cacheControl);
} catch {
const { SharedCacheControls } = await import(
// @ts-expect-error supporting multiple next version, this module is not resolvable with currently used dev dependency
// eslint-disable-next-line import/no-unresolved, n/no-missing-import
"next/dist/server/lib/incremental-cache/shared-cache-controls.js"
);
const sharedCacheControls = new SharedCacheControls(prerenderManifest);
sharedCacheControls.set(key, cacheControl);
}
} else if (typeof revalidate === "number" || revalidate === false) {
try {
const { normalizePagePath } = await import("next/dist/shared/lib/page-path/normalize-page-path.js");
prerenderManifest.routes[key] = {
experimentalPPR: void 0,
dataRoute: (0, import_posix.join)("/_next/data", `${normalizePagePath(key)}.json`),
srcRoute: null,
// FIXME: provide actual source route, however, when dynamically appending it doesn't really matter
initialRevalidateSeconds: revalidate,
// Pages routes do not have a prefetch data route.
prefetchDataRoute: void 0
};
} catch {
const { SharedRevalidateTimings } = await import("next/dist/server/lib/incremental-cache/shared-revalidate-timings.js");
const sharedRevalidateTimings = new SharedRevalidateTimings(prerenderManifest);
sharedRevalidateTimings.set(key, revalidate);
}
}
} catch {
}
}
}
async get(...args) {
return this.tracer.withActiveSpan("get cache key", async (span) => {
const [key, context = {}] = args;
(0, import_request_context.getLogger)().debug(`[NetlifyCacheHandler.get]: ${key}`);
span.setAttributes({ key });
const blob = await this.cacheStore.get(key, "blobStore.get");
if (!blob) {
span.addEvent("Cache miss", { key });
return null;
}
const ttl = this.getTTL(blob);
if ((0, import_request_context.getRequestContext)()?.isBackgroundRevalidation && typeof ttl === "number" && ttl < 0) {
span.addEvent("Discarding stale entry due to SWR background revalidation request", {
key,
ttl
});
(0, import_request_context.getLogger)().withFields({
ttl,
key
}).debug(
`[NetlifyCacheHandler.get] Discarding stale entry due to SWR background revalidation request: ${key}`
);
return null;
}
const staleByTags = await this.checkCacheEntryStaleByTags(
blob,
context.tags,
context.softTags
);
if (staleByTags) {
span.addEvent("Stale", { staleByTags, key, ttl });
return null;
}
this.captureResponseCacheLastModified(blob, key, span);
const isDataRequest = Boolean(context.fetchUrl);
if (!isDataRequest) {
this.captureCacheTags(blob.value, key);
}
switch (blob.value?.kind) {
case "FETCH":
span.addEvent("FETCH", {
lastModified: blob.lastModified,
revalidate: context.revalidate,
ttl
});
return {
lastModified: blob.lastModified,
value: blob.value
};
case "ROUTE":
case "APP_ROUTE": {
span.addEvent(blob.value?.kind, {
lastModified: blob.lastModified,
status: blob.value.status,
revalidate: blob.value.revalidate,
ttl
});
const valueWithoutRevalidate = this.captureRouteRevalidateAndRemoveFromObject(blob.value);
return {
lastModified: blob.lastModified,
value: {
...valueWithoutRevalidate,
body: import_node_buffer.Buffer.from(valueWithoutRevalidate.body, "base64")
}
};
}
case "PAGE":
case "PAGES": {
const { revalidate, ...restOfPageValue } = blob.value;
const requestContext = (0, import_request_context.getRequestContext)();
if (requestContext) {
requestContext.pageHandlerRevalidate = revalidate;
}
span.addEvent(blob.value?.kind, { lastModified: blob.lastModified, revalidate, ttl });
await this.injectEntryToPrerenderManifest(key, blob.value);
return {
lastModified: blob.lastModified,
value: restOfPageValue
};
}
case "APP_PAGE": {
const requestContext = (0, import_request_context.getRequestContext)();
if (requestContext && blob.value?.kind === "APP_PAGE") {
requestContext.isCacheableAppPage = true;
}
const { revalidate, rscData, segmentData, ...restOfPageValue } = blob.value;
span.addEvent(blob.value?.kind, { lastModified: blob.lastModified, revalidate, ttl });
await this.injectEntryToPrerenderManifest(key, blob.value);
return {
lastModified: blob.lastModified,
value: {
...restOfPageValue,
rscData: rscData ? import_node_buffer.Buffer.from(rscData, "base64") : void 0,
segmentData: segmentData ? new Map(
Object.entries(segmentData).map(([segmentPath, base64EncodedSegment]) => [
segmentPath,
import_node_buffer.Buffer.from(base64EncodedSegment, "base64")
])
) : void 0
}
};
}
default:
span.recordException(new Error(`Unknown cache entry kind: ${blob.value?.kind}`));
}
return null;
});
}
transformToStorableObject(data, context) {
if (!data) {
return null;
}
if ((0, import_cache_types.isCachedRouteValue)(data)) {
return {
...data,
revalidate: context.revalidate ?? context.cacheControl?.revalidate,
cacheControl: context.cacheControl,
body: data.body.toString("base64")
};
}
if ((0, import_cache_types.isCachedPageValue)(data)) {
return {
...data,
revalidate: context.revalidate ?? context.cacheControl?.revalidate,
cacheControl: context.cacheControl
};
}
if (data?.kind === "APP_PAGE") {
return {
...data,
revalidate: context.revalidate ?? context.cacheControl?.revalidate,
cacheControl: context.cacheControl,
rscData: data.rscData?.toString("base64"),
segmentData: data.segmentData ? Object.fromEntries(
[...data.segmentData.entries()].map(([segmentPath, base64EncodedSegment]) => [
segmentPath,
base64EncodedSegment.toString("base64")
])
) : void 0
};
}
return data;
}
async set(...args) {
return this.tracer.withActiveSpan("set cache key", async (span) => {
const [key, data, context] = args;
const lastModified = Date.now();
span.setAttributes({ key, lastModified });
(0, import_request_context.getLogger)().debug(`[NetlifyCacheHandler.set]: ${key}`);
const value = this.transformToStorableObject(data, context);
const isDataReq = Boolean(context.fetchUrl);
if (!isDataReq) {
this.captureCacheTags(value, key);
}
await this.cacheStore.set(key, { lastModified, value }, "blobStore.set");
if (data?.kind === "APP_PAGE") {
const requestContext = (0, import_request_context.getRequestContext)();
if (requestContext) {
requestContext.isCacheableAppPage = true;
}
}
if (!data && !isDataReq || data?.kind === "PAGE" || data?.kind === "PAGES") {
const requestContext = (0, import_request_context.getRequestContext)();
if (requestContext?.didPagesRouterOnDemandRevalidate) {
const tag = `_N_T_${key === "/index" ? "/" : encodeURI(key)}`;
requestContext?.trackBackgroundWork((0, import_tags_handler.purgeEdgeCache)(tag));
}
}
});
}
async revalidateTag(tagOrTags) {
return (0, import_tags_handler.markTagsAsStaleAndPurgeEdgeCache)(tagOrTags);
}
resetRequestCache() {
}
/**
* Checks if a cache entry is stale through on demand revalidated tags
*/
checkCacheEntryStaleByTags(cacheEntry, tags = [], softTags = []) {
let cacheTags = [];
if (cacheEntry.value?.kind === "FETCH") {
cacheTags = [...tags, ...softTags];
} else if (cacheEntry.value?.kind === "PAGE" || cacheEntry.value?.kind === "PAGES" || cacheEntry.value?.kind === "APP_PAGE" || cacheEntry.value?.kind === "ROUTE" || cacheEntry.value?.kind === "APP_ROUTE") {
cacheTags = cacheEntry.value.headers?.[import_constants.NEXT_CACHE_TAGS_HEADER]?.split(/,|%2c/gi) || [];
} else {
return false;
}
if (this.revalidatedTags && this.revalidatedTags.length !== 0) {
for (const tag of this.revalidatedTags) {
if (cacheTags.includes(tag)) {
return true;
}
}
}
return (0, import_tags_handler.isAnyTagStale)(cacheTags, cacheEntry.lastModified);
}
};
var cache_default = NetlifyCacheHandler;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
NetlifyCacheHandler
});
;