@netlify/plugin-nextjs 5.13.5 → 5.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ import {
13
13
  } from "../../esm-chunks/chunk-YUXQHOYO.js";
14
14
  import {
15
15
  require_semver
16
- } from "../../esm-chunks/chunk-TLQCAGE2.js";
16
+ } from "../../esm-chunks/chunk-TVEBGDAB.js";
17
17
  import {
18
18
  __toESM
19
19
  } from "../../esm-chunks/chunk-6BT4RYQJ.js";
@@ -13,7 +13,7 @@ import {
13
13
  } from "../../esm-chunks/chunk-YUXQHOYO.js";
14
14
  import {
15
15
  require_semver
16
- } from "../../esm-chunks/chunk-TLQCAGE2.js";
16
+ } from "../../esm-chunks/chunk-TVEBGDAB.js";
17
17
  import {
18
18
  __toESM
19
19
  } from "../../esm-chunks/chunk-6BT4RYQJ.js";
@@ -6,7 +6,7 @@
6
6
 
7
7
  import {
8
8
  require_semver
9
- } from "../esm-chunks/chunk-TLQCAGE2.js";
9
+ } from "../esm-chunks/chunk-TVEBGDAB.js";
10
10
  import {
11
11
  __toESM
12
12
  } from "../esm-chunks/chunk-6BT4RYQJ.js";
@@ -150,6 +150,10 @@ var PluginContext = class {
150
150
  get edgeHandlerDir() {
151
151
  return join(this.edgeFunctionsDir, EDGE_HANDLER_NAME);
152
152
  }
153
+ /** Absolute path to the skew protection config */
154
+ get skewProtectionConfigPath() {
155
+ return this.resolveFromPackagePath(".netlify/v1/skew-protection.json");
156
+ }
153
157
  constructor(options) {
154
158
  this.constants = options.constants;
155
159
  this.featureFlags = options.featureFlags;
@@ -0,0 +1,106 @@
1
+
2
+ var require = await (async () => {
3
+ var { createRequire } = await import("node:module");
4
+ return createRequire(import.meta.url);
5
+ })();
6
+
7
+ import "../esm-chunks/chunk-6BT4RYQJ.js";
8
+
9
+ // src/build/skew-protection.ts
10
+ import { mkdir, writeFile } from "node:fs/promises";
11
+ import { dirname } from "node:path";
12
+ var EnabledOrDisabledReason = /* @__PURE__ */ ((EnabledOrDisabledReason2) => {
13
+ EnabledOrDisabledReason2["OPT_OUT_DEFAULT"] = "off-default";
14
+ EnabledOrDisabledReason2["OPT_OUT_NO_VALID_DEPLOY_ID"] = "off-no-valid-deploy-id";
15
+ EnabledOrDisabledReason2["OPT_OUT_NO_VALID_DEPLOY_ID_ENV_VAR"] = "off-no-valid-deploy-id-env-var";
16
+ EnabledOrDisabledReason2["OPT_IN_FF"] = "on-ff";
17
+ EnabledOrDisabledReason2["OPT_IN_ENV_VAR"] = "on-env-var";
18
+ EnabledOrDisabledReason2["OPT_OUT_ENV_VAR"] = "off-env-var";
19
+ return EnabledOrDisabledReason2;
20
+ })(EnabledOrDisabledReason || {});
21
+ var optInOptions = /* @__PURE__ */ new Set([
22
+ "on-ff" /* OPT_IN_FF */,
23
+ "on-env-var" /* OPT_IN_ENV_VAR */
24
+ ]);
25
+ var skewProtectionConfig = {
26
+ patterns: [".*"],
27
+ sources: [
28
+ {
29
+ type: "cookie",
30
+ name: "__vdpl"
31
+ },
32
+ {
33
+ type: "header",
34
+ name: "X-Deployment-Id"
35
+ },
36
+ {
37
+ type: "query",
38
+ name: "dpl"
39
+ }
40
+ ]
41
+ };
42
+ function shouldEnableSkewProtection(ctx) {
43
+ let enabledOrDisabledReason = "off-default" /* OPT_OUT_DEFAULT */;
44
+ if (process.env.NETLIFY_NEXT_SKEW_PROTECTION === "true" || process.env.NETLIFY_NEXT_SKEW_PROTECTION === "1") {
45
+ enabledOrDisabledReason = "on-env-var" /* OPT_IN_ENV_VAR */;
46
+ } else if (process.env.NETLIFY_NEXT_SKEW_PROTECTION === "false" || process.env.NETLIFY_NEXT_SKEW_PROTECTION === "0") {
47
+ return {
48
+ enabled: false,
49
+ enabledOrDisabledReason: "off-env-var" /* OPT_OUT_ENV_VAR */
50
+ };
51
+ } else if (ctx.featureFlags?.["next-runtime-skew-protection"]) {
52
+ enabledOrDisabledReason = "on-ff" /* OPT_IN_FF */;
53
+ } else {
54
+ return {
55
+ enabled: false,
56
+ enabledOrDisabledReason: "off-default" /* OPT_OUT_DEFAULT */
57
+ };
58
+ }
59
+ if ((!process.env.DEPLOY_ID || process.env.DEPLOY_ID === "0") && optInOptions.has(enabledOrDisabledReason)) {
60
+ return {
61
+ enabled: false,
62
+ enabledOrDisabledReason: enabledOrDisabledReason === "on-env-var" /* OPT_IN_ENV_VAR */ && ctx.constants.IS_LOCAL ? (
63
+ // this case is singled out to provide visible feedback to users that env var has no effect
64
+ "off-no-valid-deploy-id-env-var" /* OPT_OUT_NO_VALID_DEPLOY_ID_ENV_VAR */
65
+ ) : (
66
+ // this is silent disablement to avoid spam logs for users opted in via feature flag
67
+ // that don't explicitly opt in via env var
68
+ "off-no-valid-deploy-id" /* OPT_OUT_NO_VALID_DEPLOY_ID */
69
+ )
70
+ };
71
+ }
72
+ return {
73
+ enabled: optInOptions.has(enabledOrDisabledReason),
74
+ enabledOrDisabledReason
75
+ };
76
+ }
77
+ var setSkewProtection = async (ctx, span) => {
78
+ const { enabled, enabledOrDisabledReason } = shouldEnableSkewProtection(ctx);
79
+ span.setAttribute("skewProtection", enabledOrDisabledReason);
80
+ if (!enabled) {
81
+ if (enabledOrDisabledReason === "off-no-valid-deploy-id-env-var" /* OPT_OUT_NO_VALID_DEPLOY_ID_ENV_VAR */) {
82
+ console.warn(
83
+ `NETLIFY_NEXT_SKEW_PROTECTION environment variable is set to ${process.env.NETLIFY_NEXT_SKEW_PROTECTION}, but skew protection is currently unavailable for CLI deploys. Skew protection will not be enabled.`
84
+ );
85
+ }
86
+ return;
87
+ }
88
+ if (enabledOrDisabledReason === "on-env-var" /* OPT_IN_ENV_VAR */) {
89
+ console.log(
90
+ `Setting up Next.js Skew Protection due to NETLIFY_NEXT_SKEW_PROTECTION=${process.env.NETLIFY_NEXT_SKEW_PROTECTION} environment variable.`
91
+ );
92
+ } else {
93
+ console.log("Setting up Next.js Skew Protection.");
94
+ }
95
+ process.env.NEXT_DEPLOYMENT_ID = process.env.DEPLOY_ID;
96
+ await mkdir(dirname(ctx.skewProtectionConfigPath), {
97
+ recursive: true
98
+ });
99
+ await writeFile(ctx.skewProtectionConfigPath, JSON.stringify(skewProtectionConfig));
100
+ };
101
+ export {
102
+ EnabledOrDisabledReason,
103
+ setSkewProtection,
104
+ shouldEnableSkewProtection,
105
+ skewProtectionConfig
106
+ };
@@ -9,7 +9,7 @@ import {
9
9
  } from "../esm-chunks/chunk-YUXQHOYO.js";
10
10
  import {
11
11
  require_semver
12
- } from "../esm-chunks/chunk-TLQCAGE2.js";
12
+ } from "../esm-chunks/chunk-TVEBGDAB.js";
13
13
  import {
14
14
  __toESM
15
15
  } from "../esm-chunks/chunk-6BT4RYQJ.js";
@@ -163,6 +163,9 @@ var require_identifiers = __commonJS({
163
163
  "use strict";
164
164
  var numeric = /^[0-9]+$/;
165
165
  var compareIdentifiers = (a, b) => {
166
+ if (typeof a === "number" && typeof b === "number") {
167
+ return a === b ? 0 : a < b ? -1 : 1;
168
+ }
166
169
  const anum = numeric.test(a);
167
170
  const bnum = numeric.test(b);
168
171
  if (anum && bnum) {
@@ -269,7 +272,25 @@ var require_semver = __commonJS({
269
272
  if (!(other instanceof _SemVer)) {
270
273
  other = new _SemVer(other, this.options);
271
274
  }
272
- return compareIdentifiers(this.major, other.major) || compareIdentifiers(this.minor, other.minor) || compareIdentifiers(this.patch, other.patch);
275
+ if (this.major < other.major) {
276
+ return -1;
277
+ }
278
+ if (this.major > other.major) {
279
+ return 1;
280
+ }
281
+ if (this.minor < other.minor) {
282
+ return -1;
283
+ }
284
+ if (this.minor > other.minor) {
285
+ return 1;
286
+ }
287
+ if (this.patch < other.patch) {
288
+ return -1;
289
+ }
290
+ if (this.patch > other.patch) {
291
+ return 1;
292
+ }
293
+ return 0;
273
294
  }
274
295
  comparePre(other) {
275
296
  if (!(other instanceof _SemVer)) {
@@ -1030,6 +1051,7 @@ var require_range = __commonJS({
1030
1051
  return result;
1031
1052
  };
1032
1053
  var parseComparator = (comp, options) => {
1054
+ comp = comp.replace(re[t.BUILD], "");
1033
1055
  debug("comp", comp, options);
1034
1056
  comp = replaceCarets(comp, options);
1035
1057
  debug("caret", comp);
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ import { clearStaleEdgeHandlers, createEdgeHandlers } from "./build/functions/ed
26
26
  import { clearStaleServerHandlers, createServerHandler } from "./build/functions/server.js";
27
27
  import { setImageConfig } from "./build/image-cdn.js";
28
28
  import { PluginContext } from "./build/plugin-context.js";
29
+ import { setSkewProtection } from "./build/skew-protection.js";
29
30
  import {
30
31
  verifyAdvancedAPIRoutes,
31
32
  verifyNetlifyFormsWorkaround,
@@ -49,7 +50,7 @@ var onPreBuild = async (options) => {
49
50
  console.warn(skipText);
50
51
  return;
51
52
  }
52
- await tracer.withActiveSpan("onPreBuild", async () => {
53
+ await tracer.withActiveSpan("onPreBuild", async (span) => {
53
54
  process.env.NEXT_PRIVATE_STANDALONE = "true";
54
55
  const ctx = new PluginContext(options);
55
56
  if (options.constants.IS_LOCAL) {
@@ -58,6 +59,7 @@ var onPreBuild = async (options) => {
58
59
  } else {
59
60
  await restoreBuildCache(ctx);
60
61
  }
62
+ await setSkewProtection(ctx, span);
61
63
  });
62
64
  };
63
65
  var onBuild = async (options) => {
@@ -204,16 +204,20 @@ var NetlifyCacheHandler = class {
204
204
  );
205
205
  return null;
206
206
  }
207
- const staleByTags = await this.checkCacheEntryStaleByTags(
207
+ const { stale: staleByTags, expired: expiredByTags } = await this.checkCacheEntryStaleByTags(
208
208
  blob,
209
209
  context.tags,
210
210
  context.softTags
211
211
  );
212
- if (staleByTags) {
213
- span.addEvent("Stale", { staleByTags, key, ttl });
212
+ if (expiredByTags) {
213
+ span.addEvent("Expired", { expiredByTags, key, ttl });
214
214
  return null;
215
215
  }
216
216
  this.captureResponseCacheLastModified(blob, key, span);
217
+ if (staleByTags) {
218
+ span.addEvent("Stale", { staleByTags, key, ttl });
219
+ blob.lastModified = -1;
220
+ }
217
221
  const isDataRequest = Boolean(context.fetchUrl);
218
222
  if (!isDataRequest) {
219
223
  this.captureCacheTags(blob.value, key);
@@ -350,8 +354,8 @@ var NetlifyCacheHandler = class {
350
354
  }
351
355
  });
352
356
  }
353
- async revalidateTag(tagOrTags) {
354
- return (0, import_tags_handler.markTagsAsStaleAndPurgeEdgeCache)(tagOrTags);
357
+ async revalidateTag(tagOrTags, durations) {
358
+ return (0, import_tags_handler.markTagsAsStaleAndPurgeEdgeCache)(tagOrTags, durations);
355
359
  }
356
360
  resetRequestCache() {
357
361
  }
@@ -365,16 +369,22 @@ var NetlifyCacheHandler = class {
365
369
  } else if (cacheEntry.value?.kind === "PAGE" || cacheEntry.value?.kind === "PAGES" || cacheEntry.value?.kind === "APP_PAGE" || cacheEntry.value?.kind === "ROUTE" || cacheEntry.value?.kind === "APP_ROUTE") {
366
370
  cacheTags = cacheEntry.value.headers?.[import_constants.NEXT_CACHE_TAGS_HEADER]?.split(/,|%2c/gi) || [];
367
371
  } else {
368
- return false;
372
+ return {
373
+ stale: false,
374
+ expired: false
375
+ };
369
376
  }
370
377
  if (this.revalidatedTags && this.revalidatedTags.length !== 0) {
371
378
  for (const tag of this.revalidatedTags) {
372
379
  if (cacheTags.includes(tag)) {
373
- return true;
380
+ return {
381
+ stale: true,
382
+ expired: true
383
+ };
374
384
  }
375
385
  }
376
386
  }
377
- return (0, import_tags_handler.isAnyTagStale)(cacheTags, cacheEntry.lastModified);
387
+ return (0, import_tags_handler.isAnyTagStaleOrExpired)(cacheTags, cacheEntry.lastModified);
378
388
  }
379
389
  };
380
390
  var cache_default = NetlifyCacheHandler;
@@ -20,8 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/run/handlers/tags-handler.cts
21
21
  var tags_handler_exports = {};
22
22
  __export(tags_handler_exports, {
23
- getMostRecentTagRevalidationTimestamp: () => getMostRecentTagRevalidationTimestamp,
24
- isAnyTagStale: () => isAnyTagStale,
23
+ getMostRecentTagExpirationTimestamp: () => getMostRecentTagExpirationTimestamp,
24
+ isAnyTagStaleOrExpired: () => isAnyTagStaleOrExpired,
25
25
  markTagsAsStaleAndPurgeEdgeCache: () => markTagsAsStaleAndPurgeEdgeCache,
26
26
  purgeEdgeCache: () => purgeEdgeCache
27
27
  });
@@ -86,58 +86,86 @@ var pipeline = (0, import_util.promisify)(import_stream.pipeline);
86
86
 
87
87
  // package.json
88
88
  var name = "@netlify/plugin-nextjs";
89
- var version = "5.13.5";
89
+ var version = "5.14.1";
90
90
 
91
91
  // src/run/handlers/tags-handler.cts
92
92
  var import_storage = require("../storage/storage.cjs");
93
93
  var import_request_context = require("./request-context.cjs");
94
94
  var purgeCacheUserAgent = `${name}@${version}`;
95
- async function getTagRevalidatedAt(tag, cacheStore) {
95
+ async function getTagManifest(tag, cacheStore) {
96
96
  const tagManifest = await cacheStore.get(tag, "tagManifest.get");
97
97
  if (!tagManifest) {
98
98
  return null;
99
99
  }
100
- return tagManifest.revalidatedAt;
100
+ return tagManifest;
101
101
  }
102
- async function getMostRecentTagRevalidationTimestamp(tags) {
102
+ async function getMostRecentTagExpirationTimestamp(tags) {
103
103
  if (tags.length === 0) {
104
104
  return 0;
105
105
  }
106
106
  const cacheStore = (0, import_storage.getMemoizedKeyValueStoreBackedByRegionalBlobStore)({ consistency: "strong" });
107
- const timestampsOrNulls = await Promise.all(
108
- tags.map((tag) => getTagRevalidatedAt(tag, cacheStore))
109
- );
110
- const timestamps = timestampsOrNulls.filter((timestamp) => timestamp !== null);
111
- if (timestamps.length === 0) {
107
+ const manifestsOrNulls = await Promise.all(tags.map((tag) => getTagManifest(tag, cacheStore)));
108
+ const expirationTimestamps = manifestsOrNulls.filter((manifest) => manifest !== null).map((manifest) => manifest.expireAt);
109
+ if (expirationTimestamps.length === 0) {
112
110
  return 0;
113
111
  }
114
- return Math.max(...timestamps);
112
+ return Math.max(...expirationTimestamps);
115
113
  }
116
- function isAnyTagStale(tags, timestamp) {
114
+ function isAnyTagStaleOrExpired(tags, timestamp) {
117
115
  if (tags.length === 0 || !timestamp) {
118
- return Promise.resolve(false);
116
+ return Promise.resolve({ stale: false, expired: false });
119
117
  }
120
118
  const cacheStore = (0, import_storage.getMemoizedKeyValueStoreBackedByRegionalBlobStore)({ consistency: "strong" });
121
119
  return new Promise((resolve, reject) => {
122
120
  const tagManifestPromises = [];
123
121
  for (const tag of tags) {
124
- const lastRevalidationTimestampPromise = getTagRevalidatedAt(tag, cacheStore);
122
+ const tagManifestPromise = getTagManifest(tag, cacheStore);
125
123
  tagManifestPromises.push(
126
- lastRevalidationTimestampPromise.then((lastRevalidationTimestamp) => {
127
- if (!lastRevalidationTimestamp) {
128
- return false;
124
+ tagManifestPromise.then((tagManifest) => {
125
+ if (!tagManifest) {
126
+ return { stale: false, expired: false };
127
+ }
128
+ const stale = tagManifest.staleAt >= timestamp;
129
+ const expired = tagManifest.expireAt >= timestamp && tagManifest.expireAt <= Date.now();
130
+ if (expired && stale) {
131
+ const expiredResult = {
132
+ stale,
133
+ expired
134
+ };
135
+ resolve(expiredResult);
136
+ return expiredResult;
129
137
  }
130
- const isStale = lastRevalidationTimestamp >= timestamp;
131
- if (isStale) {
132
- resolve(true);
133
- return true;
138
+ if (stale) {
139
+ const staleResult = {
140
+ stale,
141
+ expired,
142
+ expireAt: tagManifest.expireAt
143
+ };
144
+ return staleResult;
134
145
  }
135
- return false;
146
+ return { stale: false, expired: false };
136
147
  })
137
148
  );
138
149
  }
139
- Promise.all(tagManifestPromises).then((tagManifestAreStale) => {
140
- resolve(tagManifestAreStale.some((tagIsStale) => tagIsStale));
150
+ Promise.all(tagManifestPromises).then((tagManifestsAreStaleOrExpired) => {
151
+ let result = { stale: false, expired: false };
152
+ for (const tagResult of tagManifestsAreStaleOrExpired) {
153
+ if (tagResult.expired) {
154
+ result = tagResult;
155
+ break;
156
+ }
157
+ if (tagResult.stale) {
158
+ result = {
159
+ stale: true,
160
+ expired: false,
161
+ expireAt: (
162
+ // make sure to use expireAt that is lowest of all tags
163
+ result.stale && !result.expired && typeof result.expireAt === "number" ? Math.min(result.expireAt, tagResult.expireAt) : tagResult.expireAt
164
+ )
165
+ };
166
+ }
167
+ }
168
+ resolve(result);
141
169
  }).catch(reject);
142
170
  });
143
171
  }
@@ -154,13 +182,15 @@ function purgeEdgeCache(tagOrTags) {
154
182
  (0, import_request_context.getLogger)().withError(error).error(`[NextRuntime] Purging the cache for tags [${tags.join(",")}] failed`);
155
183
  });
156
184
  }
157
- async function doRevalidateTagAndPurgeEdgeCache(tags) {
158
- (0, import_request_context.getLogger)().withFields({ tags }).debug("doRevalidateTagAndPurgeEdgeCache");
185
+ async function doRevalidateTagAndPurgeEdgeCache(tags, durations) {
186
+ (0, import_request_context.getLogger)().withFields({ tags, durations }).debug("doRevalidateTagAndPurgeEdgeCache");
159
187
  if (tags.length === 0) {
160
188
  return;
161
189
  }
190
+ const now = Date.now();
162
191
  const tagManifest = {
163
- revalidatedAt: Date.now()
192
+ staleAt: now,
193
+ expireAt: now + (durations?.expire ? durations.expire * 1e3 : 0)
164
194
  };
165
195
  const cacheStore = (0, import_storage.getMemoizedKeyValueStoreBackedByRegionalBlobStore)({ consistency: "strong" });
166
196
  await Promise.all(
@@ -174,19 +204,31 @@ async function doRevalidateTagAndPurgeEdgeCache(tags) {
174
204
  );
175
205
  await purgeEdgeCache(tags);
176
206
  }
177
- function markTagsAsStaleAndPurgeEdgeCache(tagOrTags) {
207
+ function markTagsAsStaleAndPurgeEdgeCache(tagOrTags, durations) {
178
208
  const tags = getCacheTagsFromTagOrTags(tagOrTags);
179
- const revalidateTagPromise = doRevalidateTagAndPurgeEdgeCache(tags);
209
+ const revalidationKey = JSON.stringify({ tags, durations });
180
210
  const requestContext = (0, import_request_context.getRequestContext)();
181
211
  if (requestContext) {
212
+ const ongoingRevalidation = requestContext.ongoingRevalidations?.get(revalidationKey);
213
+ if (ongoingRevalidation) {
214
+ return ongoingRevalidation;
215
+ }
216
+ }
217
+ const revalidateTagPromise = doRevalidateTagAndPurgeEdgeCache(tags, durations);
218
+ if (requestContext) {
219
+ requestContext.ongoingRevalidations ??= /* @__PURE__ */ new Map();
220
+ requestContext.ongoingRevalidations.set(revalidationKey, revalidateTagPromise);
221
+ process.nextTick(() => {
222
+ requestContext.ongoingRevalidations?.delete(revalidationKey);
223
+ });
182
224
  requestContext.trackBackgroundWork(revalidateTagPromise);
183
225
  }
184
226
  return revalidateTagPromise;
185
227
  }
186
228
  // Annotate the CommonJS export names for ESM import in node:
187
229
  0 && (module.exports = {
188
- getMostRecentTagRevalidationTimestamp,
189
- isAnyTagStale,
230
+ getMostRecentTagExpirationTimestamp,
231
+ isAnyTagStaleOrExpired,
190
232
  markTagsAsStaleAndPurgeEdgeCache,
191
233
  purgeEdgeCache
192
234
  });
@@ -1381,8 +1381,8 @@ var LRUCache = class _LRUCache {
1381
1381
  // src/run/handlers/use-cache-handler.ts
1382
1382
  import { getLogger } from "./request-context.cjs";
1383
1383
  import {
1384
- getMostRecentTagRevalidationTimestamp,
1385
- isAnyTagStale,
1384
+ getMostRecentTagExpirationTimestamp,
1385
+ isAnyTagStaleOrExpired,
1386
1386
  markTagsAsStaleAndPurgeEdgeCache
1387
1387
  } from "./tags-handler.cjs";
1388
1388
  import { getTracer } from "./tracer.cjs";
@@ -1444,23 +1444,29 @@ var NetlifyDefaultUseCacheHandler = {
1444
1444
  });
1445
1445
  return void 0;
1446
1446
  }
1447
- if (await isAnyTagStale(entry.tags, entry.timestamp)) {
1448
- getLogger().withFields({ cacheKey, ttl, status: "STALE BY TAG" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
1447
+ const { stale, expired } = await isAnyTagStaleOrExpired(entry.tags, entry.timestamp);
1448
+ if (expired) {
1449
+ getLogger().withFields({ cacheKey, ttl, status: "EXPIRED BY TAG" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
1449
1450
  span.setAttributes({
1450
- cacheStatus: "stale tag, discarded",
1451
+ cacheStatus: "expired tag, discarded",
1451
1452
  ttl
1452
1453
  });
1453
1454
  return void 0;
1454
1455
  }
1455
- const [returnStream, newSaved] = entry.value.tee();
1456
+ let { revalidate, value } = entry;
1457
+ if (stale) {
1458
+ revalidate = -1;
1459
+ }
1460
+ const [returnStream, newSaved] = value.tee();
1456
1461
  entry.value = newSaved;
1457
- getLogger().withFields({ cacheKey, ttl, status: "HIT" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
1462
+ getLogger().withFields({ cacheKey, ttl, status: stale ? "STALE" : "HIT" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
1458
1463
  span.setAttributes({
1459
- cacheStatus: "hit",
1464
+ cacheStatus: stale ? "stale" : "hit",
1460
1465
  ttl
1461
1466
  });
1462
1467
  return {
1463
1468
  ...entry,
1469
+ revalidate,
1464
1470
  value: returnStream
1465
1471
  };
1466
1472
  }
@@ -1513,14 +1519,15 @@ var NetlifyDefaultUseCacheHandler = {
1513
1519
  },
1514
1520
  async refreshTags() {
1515
1521
  },
1516
- getExpiration: function(...tags) {
1522
+ getExpiration: function(...notNormalizedTags) {
1517
1523
  return getTracer().withActiveSpan(
1518
1524
  "DefaultUseCacheHandler.getExpiration",
1519
1525
  async (span) => {
1526
+ const tags = notNormalizedTags.flat();
1520
1527
  span.setAttributes({
1521
1528
  tags
1522
1529
  });
1523
- const expiration = await getMostRecentTagRevalidationTimestamp(tags);
1530
+ const expiration = await getMostRecentTagExpirationTimestamp(tags);
1524
1531
  getLogger().withFields({ tags, expiration }).debug(`[NetlifyDefaultUseCacheHandler] getExpiration`);
1525
1532
  span.setAttributes({
1526
1533
  expiration
@@ -1529,6 +1536,7 @@ var NetlifyDefaultUseCacheHandler = {
1529
1536
  }
1530
1537
  );
1531
1538
  },
1539
+ // this is for CacheHandlerV2
1532
1540
  expireTags(...tags) {
1533
1541
  return getTracer().withActiveSpan(
1534
1542
  "DefaultUseCacheHandler.expireTags",
@@ -1540,6 +1548,20 @@ var NetlifyDefaultUseCacheHandler = {
1540
1548
  await markTagsAsStaleAndPurgeEdgeCache(tags);
1541
1549
  }
1542
1550
  );
1551
+ },
1552
+ // this is for CacheHandlerV3 / Next 16
1553
+ updateTags(tags, durations) {
1554
+ return getTracer().withActiveSpan(
1555
+ "DefaultUseCacheHandler.updateTags",
1556
+ async (span) => {
1557
+ getLogger().withFields({ tags, durations }).debug(`[NetlifyDefaultUseCacheHandler] updateTags`);
1558
+ span.setAttributes({
1559
+ tags,
1560
+ durations: JSON.stringify(durations)
1561
+ });
1562
+ await markTagsAsStaleAndPurgeEdgeCache(tags, durations);
1563
+ }
1564
+ );
1543
1565
  }
1544
1566
  };
1545
1567
  function configureUseCacheHandlers() {
@@ -25,40 +25,12 @@ __export(regional_blob_store_exports, {
25
25
  });
26
26
  module.exports = __toCommonJS(regional_blob_store_exports);
27
27
 
28
- // node_modules/@netlify/blobs/dist/chunk-XR3MUBBK.js
29
- var NF_ERROR = "x-nf-error";
30
- var NF_REQUEST_ID = "x-nf-request-id";
31
- var BlobsInternalError = class extends Error {
32
- constructor(res) {
33
- let details = res.headers.get(NF_ERROR) || `${res.status} status code`;
34
- if (res.headers.has(NF_REQUEST_ID)) {
35
- details += `, ID: ${res.headers.get(NF_REQUEST_ID)}`;
36
- }
37
- super(`Netlify Blobs has generated an internal error (${details})`);
38
- this.name = "BlobsInternalError";
39
- }
40
- };
41
- var collectIterator = async (iterator) => {
42
- const result = [];
43
- for await (const item of iterator) {
44
- result.push(item);
45
- }
46
- return result;
47
- };
48
- var base64Decode = (input) => {
49
- const { Buffer: Buffer2 } = globalThis;
50
- if (Buffer2) {
51
- return Buffer2.from(input, "base64").toString();
52
- }
53
- return atob(input);
54
- };
55
- var base64Encode = (input) => {
56
- const { Buffer: Buffer2 } = globalThis;
57
- if (Buffer2) {
58
- return Buffer2.from(input).toString("base64");
59
- }
60
- return btoa(input);
61
- };
28
+ // node_modules/@netlify/blobs/node_modules/@netlify/runtime-utils/dist/main.js
29
+ var getString = (input) => typeof input === "string" ? input : JSON.stringify(input);
30
+ var base64Decode = globalThis.Buffer ? (input) => Buffer.from(input, "base64").toString() : (input) => atob(input);
31
+ var base64Encode = globalThis.Buffer ? (input) => Buffer.from(getString(input)).toString("base64") : (input) => btoa(getString(input));
32
+
33
+ // node_modules/@netlify/blobs/dist/chunk-HN33TXZT.js
62
34
  var getEnvironment = () => {
63
35
  const { Deno, Netlify, process: process2 } = globalThis;
64
36
  return Netlify?.env ?? Deno?.env ?? {
@@ -111,7 +83,7 @@ var encodeMetadata = (metadata) => {
111
83
  return payload;
112
84
  };
113
85
  var decodeMetadata = (header) => {
114
- if (!header || !header.startsWith(BASE64_PREFIX)) {
86
+ if (!header?.startsWith(BASE64_PREFIX)) {
115
87
  return {};
116
88
  }
117
89
  const encodedData = header.slice(BASE64_PREFIX.length);
@@ -132,6 +104,25 @@ var getMetadataFromResponse = (response) => {
132
104
  );
133
105
  }
134
106
  };
107
+ var NF_ERROR = "x-nf-error";
108
+ var NF_REQUEST_ID = "x-nf-request-id";
109
+ var BlobsInternalError = class extends Error {
110
+ constructor(res) {
111
+ let details = res.headers.get(NF_ERROR) || `${res.status} status code`;
112
+ if (res.headers.has(NF_REQUEST_ID)) {
113
+ details += `, ID: ${res.headers.get(NF_REQUEST_ID)}`;
114
+ }
115
+ super(`Netlify Blobs has generated an internal error (${details})`);
116
+ this.name = "BlobsInternalError";
117
+ }
118
+ };
119
+ var collectIterator = async (iterator) => {
120
+ const result = [];
121
+ for await (const item of iterator) {
122
+ result.push(item);
123
+ }
124
+ return result;
125
+ };
135
126
  var BlobsConsistencyError = class extends Error {
136
127
  constructor() {
137
128
  super(
@@ -283,6 +274,7 @@ var Client = class {
283
274
  }
284
275
  async makeRequest({
285
276
  body,
277
+ conditions = {},
286
278
  consistency,
287
279
  headers: extraHeaders,
288
280
  key,
@@ -306,6 +298,11 @@ var Client = class {
306
298
  if (method === "put") {
307
299
  headers["cache-control"] = "max-age=0, stale-while-revalidate=60";
308
300
  }
301
+ if ("onlyIfMatch" in conditions && conditions.onlyIfMatch) {
302
+ headers["if-match"] = conditions.onlyIfMatch;
303
+ } else if ("onlyIfNew" in conditions && conditions.onlyIfNew) {
304
+ headers["if-none-match"] = "*";
305
+ }
309
306
  const options = {
310
307
  body,
311
308
  headers,
@@ -344,6 +341,8 @@ var getClientOptions = (options, contextOverride) => {
344
341
  var DEPLOY_STORE_PREFIX = "deploy:";
345
342
  var LEGACY_STORE_INTERNAL_PREFIX = "netlify-internal/legacy-namespace/";
346
343
  var SITE_STORE_PREFIX = "site:";
344
+ var STATUS_OK = 200;
345
+ var STATUS_PRE_CONDITION_FAILED = 412;
347
346
  var Store = class _Store {
348
347
  constructor(options) {
349
348
  this.client = options.client;
@@ -468,36 +467,56 @@ var Store = class _Store {
468
467
  )
469
468
  );
470
469
  }
471
- async set(key, data, { metadata } = {}) {
470
+ async set(key, data, options = {}) {
472
471
  _Store.validateKey(key);
472
+ const conditions = _Store.getConditions(options);
473
473
  const res = await this.client.makeRequest({
474
+ conditions,
474
475
  body: data,
475
476
  key,
476
- metadata,
477
+ metadata: options.metadata,
477
478
  method: "put",
478
479
  storeName: this.name
479
480
  });
480
- if (res.status !== 200) {
481
- throw new BlobsInternalError(res);
481
+ const etag = res.headers.get("etag") ?? "";
482
+ if (conditions) {
483
+ return res.status === STATUS_PRE_CONDITION_FAILED ? { modified: false } : { etag, modified: true };
484
+ }
485
+ if (res.status === STATUS_OK) {
486
+ return {
487
+ etag,
488
+ modified: true
489
+ };
482
490
  }
491
+ throw new BlobsInternalError(res);
483
492
  }
484
- async setJSON(key, data, { metadata } = {}) {
493
+ async setJSON(key, data, options = {}) {
485
494
  _Store.validateKey(key);
495
+ const conditions = _Store.getConditions(options);
486
496
  const payload = JSON.stringify(data);
487
497
  const headers = {
488
498
  "content-type": "application/json"
489
499
  };
490
500
  const res = await this.client.makeRequest({
501
+ ...conditions,
491
502
  body: payload,
492
503
  headers,
493
504
  key,
494
- metadata,
505
+ metadata: options.metadata,
495
506
  method: "put",
496
507
  storeName: this.name
497
508
  });
498
- if (res.status !== 200) {
499
- throw new BlobsInternalError(res);
509
+ const etag = res.headers.get("etag") ?? "";
510
+ if (conditions) {
511
+ return res.status === STATUS_PRE_CONDITION_FAILED ? { modified: false } : { etag, modified: true };
500
512
  }
513
+ if (res.status === STATUS_OK) {
514
+ return {
515
+ etag,
516
+ modified: true
517
+ };
518
+ }
519
+ throw new BlobsInternalError(res);
501
520
  }
502
521
  static formatListResultBlob(result) {
503
522
  if (!result.key) {
@@ -508,6 +527,31 @@ var Store = class _Store {
508
527
  key: result.key
509
528
  };
510
529
  }
530
+ static getConditions(options) {
531
+ if ("onlyIfMatch" in options && "onlyIfNew" in options) {
532
+ throw new Error(
533
+ `The 'onlyIfMatch' and 'onlyIfNew' options are mutually exclusive. Using 'onlyIfMatch' will make the write succeed only if there is an entry for the key with the given content, while 'onlyIfNew' will make the write succeed only if there is no entry for the key.`
534
+ );
535
+ }
536
+ if ("onlyIfMatch" in options && options.onlyIfMatch) {
537
+ if (typeof options.onlyIfMatch !== "string") {
538
+ throw new Error(`The 'onlyIfMatch' property expects a string representing an ETag.`);
539
+ }
540
+ return {
541
+ onlyIfMatch: options.onlyIfMatch
542
+ };
543
+ }
544
+ if ("onlyIfNew" in options && options.onlyIfNew) {
545
+ if (typeof options.onlyIfNew !== "boolean") {
546
+ throw new Error(
547
+ `The 'onlyIfNew' property expects a boolean indicating whether the write should fail if an entry for the key already exists.`
548
+ );
549
+ }
550
+ return {
551
+ onlyIfNew: true
552
+ };
553
+ }
554
+ }
511
555
  static validateKey(key) {
512
556
  if (key === "") {
513
557
  throw new Error("Blob key must not be empty.");
@@ -25,7 +25,7 @@ __export(blob_types_exports, {
25
25
  });
26
26
  module.exports = __toCommonJS(blob_types_exports);
27
27
  var isTagManifest = (value) => {
28
- return typeof value === "object" && value !== null && "revalidatedAt" in value && typeof value.revalidatedAt === "number" && Object.keys(value).length === 1;
28
+ return typeof value === "object" && value !== null && "staleAt" in value && typeof value.staleAt === "number" && "expiredAt" in value && typeof value.expiredAt === "number" && Object.keys(value).length === 2;
29
29
  };
30
30
  var isHtmlBlob = (value) => {
31
31
  return typeof value === "object" && value !== null && "html" in value && "isFullyStaticPage" in value && typeof value.html === "string" && typeof value.isFullyStaticPage === "boolean" && Object.keys(value).length === 2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "5.13.5",
3
+ "version": "5.14.1",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",