@netlify/plugin-nextjs 5.14.0 → 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.
@@ -86,7 +86,7 @@ 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.14.0";
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");
@@ -206,9 +206,21 @@ async function doRevalidateTagAndPurgeEdgeCache(tags, durations) {
206
206
  }
207
207
  function markTagsAsStaleAndPurgeEdgeCache(tagOrTags, durations) {
208
208
  const tags = getCacheTagsFromTagOrTags(tagOrTags);
209
- const revalidateTagPromise = doRevalidateTagAndPurgeEdgeCache(tags, durations);
209
+ const revalidationKey = JSON.stringify({ tags, durations });
210
210
  const requestContext = (0, import_request_context.getRequestContext)();
211
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
+ });
212
224
  requestContext.trackBackgroundWork(revalidateTagPromise);
213
225
  }
214
226
  return revalidateTagPromise;
@@ -1444,24 +1444,29 @@ var NetlifyDefaultUseCacheHandler = {
1444
1444
  });
1445
1445
  return void 0;
1446
1446
  }
1447
- const { stale } = await isAnyTagStaleOrExpired(entry.tags, entry.timestamp);
1448
- if (stale) {
1449
- 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`);
1450
1450
  span.setAttributes({
1451
- cacheStatus: "stale tag, discarded",
1451
+ cacheStatus: "expired tag, discarded",
1452
1452
  ttl
1453
1453
  });
1454
1454
  return void 0;
1455
1455
  }
1456
- const [returnStream, newSaved] = entry.value.tee();
1456
+ let { revalidate, value } = entry;
1457
+ if (stale) {
1458
+ revalidate = -1;
1459
+ }
1460
+ const [returnStream, newSaved] = value.tee();
1457
1461
  entry.value = newSaved;
1458
- getLogger().withFields({ cacheKey, ttl, status: "HIT" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
1462
+ getLogger().withFields({ cacheKey, ttl, status: stale ? "STALE" : "HIT" }).debug(`[NetlifyDefaultUseCacheHandler] get result`);
1459
1463
  span.setAttributes({
1460
- cacheStatus: "hit",
1464
+ cacheStatus: stale ? "stale" : "hit",
1461
1465
  ttl
1462
1466
  });
1463
1467
  return {
1464
1468
  ...entry,
1469
+ revalidate,
1465
1470
  value: returnStream
1466
1471
  };
1467
1472
  }
@@ -1514,10 +1519,11 @@ var NetlifyDefaultUseCacheHandler = {
1514
1519
  },
1515
1520
  async refreshTags() {
1516
1521
  },
1517
- getExpiration: function(...tags) {
1522
+ getExpiration: function(...notNormalizedTags) {
1518
1523
  return getTracer().withActiveSpan(
1519
1524
  "DefaultUseCacheHandler.getExpiration",
1520
1525
  async (span) => {
1526
+ const tags = notNormalizedTags.flat();
1521
1527
  span.setAttributes({
1522
1528
  tags
1523
1529
  });
@@ -1530,6 +1536,7 @@ var NetlifyDefaultUseCacheHandler = {
1530
1536
  }
1531
1537
  );
1532
1538
  },
1539
+ // this is for CacheHandlerV2
1533
1540
  expireTags(...tags) {
1534
1541
  return getTracer().withActiveSpan(
1535
1542
  "DefaultUseCacheHandler.expireTags",
@@ -1541,6 +1548,20 @@ var NetlifyDefaultUseCacheHandler = {
1541
1548
  await markTagsAsStaleAndPurgeEdgeCache(tags);
1542
1549
  }
1543
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
+ );
1544
1565
  }
1545
1566
  };
1546
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.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/plugin-nextjs",
3
- "version": "5.14.0",
3
+ "version": "5.14.1",
4
4
  "description": "Run Next.js seamlessly on Netlify",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",