what-isr 0.10.0

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.
package/dist/index.js ADDED
@@ -0,0 +1,653 @@
1
+ // packages/cache/src/key.js
2
+ import { createHash } from "node:crypto";
3
+ var STRIP_EXACT = /* @__PURE__ */ new Set(["fbclid", "gclid", "_", "mc_cid", "mc_eid"]);
4
+ var STRIP_PREFIXES = ["utm_"];
5
+ function shouldStrip(name) {
6
+ if (STRIP_EXACT.has(name)) return true;
7
+ for (const p of STRIP_PREFIXES) if (name.startsWith(p)) return true;
8
+ return false;
9
+ }
10
+ function normalizePath(path) {
11
+ if (!path) return "/";
12
+ let p = String(path).split("?")[0].split("#")[0];
13
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
14
+ return p || "/";
15
+ }
16
+ function queryToEntries(query) {
17
+ if (!query) return [];
18
+ if (typeof query === "string") {
19
+ const s = query.startsWith("?") ? query.slice(1) : query;
20
+ if (!s) return [];
21
+ return [...new URLSearchParams(s).entries()];
22
+ }
23
+ const out = [];
24
+ for (const [k, v] of Object.entries(query)) {
25
+ if (Array.isArray(v)) for (const item of v) out.push([k, String(item)]);
26
+ else out.push([k, String(v)]);
27
+ }
28
+ return out;
29
+ }
30
+ function normalizeQuery(query) {
31
+ const entries = queryToEntries(query).filter(([k]) => !shouldStrip(k)).sort((a, b) => a[0] === b[0] ? a[1] < b[1] ? -1 : 1 : a[0] < b[0] ? -1 : 1);
32
+ return entries.map(([k, v]) => `${k}=${v}`).join("&");
33
+ }
34
+ function varyString(vary) {
35
+ if (!vary) return "";
36
+ return Object.entries(vary).sort((a, b) => a[0] < b[0] ? -1 : 1).map(([k, v]) => `${k}=${v}`).join("&");
37
+ }
38
+ function cacheKey({ path, query, vary } = {}) {
39
+ return [normalizePath(path), normalizeQuery(query), varyString(vary)].join("\0");
40
+ }
41
+ function hashKey(key) {
42
+ const hex = createHash("sha256").update(key).digest("hex");
43
+ return `${hex.slice(0, 2)}/${hex.slice(2, 4)}/${hex.slice(4)}`;
44
+ }
45
+
46
+ // packages/cache/src/stores/store-interface.js
47
+ function makeEntry(out, config = {}, now = Date.now()) {
48
+ const maxAge = Number(config.revalidate) || 0;
49
+ const swrWindow = config.swr != null ? Number(config.swr) : maxAge;
50
+ return {
51
+ html: out.html || "",
52
+ head: out.head || "",
53
+ state: out.state ?? null,
54
+ tags: out.tags || config.tags || [],
55
+ path: out.path || config.path,
56
+ status: out.status || 200,
57
+ partial: !!out.partial,
58
+ renderedAt: now,
59
+ maxAge,
60
+ swrWindow,
61
+ expiresAt: maxAge > 0 ? now + maxAge * 1e3 : Infinity
62
+ };
63
+ }
64
+ function isFresh(entry, now = Date.now()) {
65
+ return entry.expiresAt === Infinity || now < entry.expiresAt;
66
+ }
67
+ function isServableStale(entry, now = Date.now()) {
68
+ if (isFresh(entry, now)) return true;
69
+ if (entry.swrWindow == null) return false;
70
+ return now < entry.expiresAt + entry.swrWindow * 1e3;
71
+ }
72
+
73
+ // packages/cache/src/headers.js
74
+ function buildCacheHeaders(entry = {}, config = {}, cacheStatus = "MISS") {
75
+ const headers = { "X-What-Cache": cacheStatus };
76
+ const cacheable = (entry.maxAge > 0 || config.mode === "static" || config.mode === "hybrid") && config.mode !== "server";
77
+ if (!cacheable) {
78
+ headers["Cache-Control"] = "private, no-store";
79
+ return headers;
80
+ }
81
+ const sMaxAge = entry.partial ? 0 : entry.maxAge || 0;
82
+ const swr = entry.swrWindow != null ? entry.swrWindow : sMaxAge;
83
+ headers["Cache-Control"] = `public, s-maxage=${sMaxAge}, stale-while-revalidate=${swr}`;
84
+ if (entry.tags && entry.tags.length) {
85
+ headers["Cache-Tag"] = entry.tags.join(",");
86
+ headers["Surrogate-Key"] = entry.tags.join(" ");
87
+ }
88
+ return headers;
89
+ }
90
+
91
+ // packages/cache/src/isr.js
92
+ function createCacheEngine({ store, render, cdn, now = Date.now, logger = console } = {}) {
93
+ const inFlight = /* @__PURE__ */ new Map();
94
+ function keyFor(routeMatch) {
95
+ return cacheKey({ path: routeMatch.path, query: routeMatch.query, vary: routeMatch.vary });
96
+ }
97
+ function regenerate(key, routeMatch, renderOverride) {
98
+ const existing = inFlight.get(key);
99
+ if (existing) return existing;
100
+ const doRender = renderOverride || render;
101
+ const p = (async () => {
102
+ const out = await doRender(routeMatch, {});
103
+ const entry = makeEntry({ ...out, path: routeMatch.path }, routeMatch.config || {}, now());
104
+ await store.set(key, entry);
105
+ return entry;
106
+ })().finally(() => inFlight.delete(key));
107
+ inFlight.set(key, p);
108
+ return p;
109
+ }
110
+ function serve(entry, cacheStatus, config) {
111
+ return {
112
+ html: entry.html,
113
+ head: entry.head,
114
+ state: entry.state,
115
+ status: entry.status || 200,
116
+ cacheStatus,
117
+ headers: buildCacheHeaders(entry, config || {}, cacheStatus)
118
+ };
119
+ }
120
+ async function handle(routeMatch, renderOverride) {
121
+ const config = routeMatch.config || {};
122
+ if (config.mode === "server") {
123
+ const out = await (renderOverride || render)(routeMatch, {});
124
+ const entry2 = makeEntry({ ...out, path: routeMatch.path }, config, now());
125
+ return serve(entry2, "BYPASS", config);
126
+ }
127
+ const key = keyFor(routeMatch);
128
+ const entry = await store.get(key);
129
+ const t = now();
130
+ if (entry && isFresh(entry, t)) {
131
+ return serve(entry, "HIT", config);
132
+ }
133
+ if (entry && isServableStale(entry, t)) {
134
+ regenerate(key, routeMatch, renderOverride).catch((e) => logger.error?.("[what-isr] background regenerate failed:", e));
135
+ return serve(entry, "STALE", config);
136
+ }
137
+ if (entry && config.onMiss === "stale-if-error") {
138
+ try {
139
+ const fresh2 = await regenerate(key, routeMatch, renderOverride);
140
+ return serve(fresh2, "MISS", config);
141
+ } catch (e) {
142
+ logger.error?.("[what-isr] regenerate failed, serving stale:", e);
143
+ return serve(entry, "STALE", config);
144
+ }
145
+ }
146
+ const fresh = await regenerate(key, routeMatch, renderOverride);
147
+ return serve(fresh, "MISS", config);
148
+ }
149
+ async function revalidatePath(path, { regenerate: regen = false, routeResolver } = {}) {
150
+ const norm = normalizePath(path);
151
+ const deleted = await store.deleteByPath(norm);
152
+ if (cdn && cdn.purge) await cdn.purge([path]);
153
+ if (regen) {
154
+ const route = routeResolver ? routeResolver(norm) : { path: norm, query: {}, config: {} };
155
+ await regenerate(keyFor(route), route).catch((e) => logger.error?.("[what-isr] regen after revalidatePath failed:", e));
156
+ }
157
+ return deleted;
158
+ }
159
+ async function revalidateTag(tag, { regenerate: regen = false, routeResolver } = {}) {
160
+ const deleted = await store.deleteByTag(tag);
161
+ if (cdn && cdn.purgeTags) await cdn.purgeTags([tag]);
162
+ if (regen && routeResolver) {
163
+ for (const key of deleted) {
164
+ const route = routeResolver(key);
165
+ if (route) await regenerate(keyFor(route), route).catch(() => {
166
+ });
167
+ }
168
+ }
169
+ return deleted;
170
+ }
171
+ return {
172
+ handle,
173
+ regenerate: (routeMatch) => regenerate(keyFor(routeMatch), routeMatch),
174
+ revalidatePath,
175
+ revalidateTag,
176
+ keyFor,
177
+ store,
178
+ _inFlight: inFlight,
179
+ _now: now,
180
+ _cdn: cdn
181
+ };
182
+ }
183
+
184
+ // packages/cache/src/stores/memory-store.js
185
+ function createMemoryStore({ maxEntries = 1e3 } = {}) {
186
+ const map = /* @__PURE__ */ new Map();
187
+ const tagIndex = /* @__PURE__ */ new Map();
188
+ const pathIndex = /* @__PURE__ */ new Map();
189
+ function addToIndex(index, name, key) {
190
+ if (name == null) return;
191
+ let set = index.get(name);
192
+ if (!set) index.set(name, set = /* @__PURE__ */ new Set());
193
+ set.add(key);
194
+ }
195
+ function removeFromIndex(index, name, key) {
196
+ if (name == null) return;
197
+ const set = index.get(name);
198
+ if (set) {
199
+ set.delete(key);
200
+ if (set.size === 0) index.delete(name);
201
+ }
202
+ }
203
+ function indexEntry(key, entry) {
204
+ if (entry.tags) for (const t of entry.tags) addToIndex(tagIndex, t, key);
205
+ addToIndex(pathIndex, entry.path, key);
206
+ }
207
+ function deindexEntry(key, entry) {
208
+ if (entry.tags) for (const t of entry.tags) removeFromIndex(tagIndex, t, key);
209
+ removeFromIndex(pathIndex, entry.path, key);
210
+ }
211
+ function removeKey(key) {
212
+ const e = map.get(key);
213
+ if (!e) return false;
214
+ map.delete(key);
215
+ deindexEntry(key, e);
216
+ return true;
217
+ }
218
+ function deleteByIndex(index, name) {
219
+ const set = index.get(name);
220
+ if (!set) return [];
221
+ const deleted = [...set];
222
+ for (const k of deleted) removeKey(k);
223
+ return deleted;
224
+ }
225
+ return {
226
+ async get(key) {
227
+ const e = map.get(key);
228
+ if (!e) return null;
229
+ map.delete(key);
230
+ map.set(key, e);
231
+ return e;
232
+ },
233
+ async set(key, entry) {
234
+ if (map.has(key)) deindexEntry(key, map.get(key));
235
+ map.set(key, entry);
236
+ indexEntry(key, entry);
237
+ while (map.size > maxEntries) {
238
+ const oldest = map.keys().next().value;
239
+ removeKey(oldest);
240
+ }
241
+ },
242
+ async delete(key) {
243
+ return removeKey(key);
244
+ },
245
+ async deleteByTag(tag) {
246
+ return deleteByIndex(tagIndex, tag);
247
+ },
248
+ async deleteByPath(path) {
249
+ return deleteByIndex(pathIndex, path);
250
+ },
251
+ async clear() {
252
+ map.clear();
253
+ tagIndex.clear();
254
+ pathIndex.clear();
255
+ },
256
+ async keys() {
257
+ return [...map.keys()];
258
+ }
259
+ };
260
+ }
261
+
262
+ // packages/cache/src/stores/filesystem-store.js
263
+ import { mkdir, writeFile, readFile, rename, rm, readdir, stat } from "node:fs/promises";
264
+ import { join, dirname } from "node:path";
265
+ import { createHash as createHash2 } from "node:crypto";
266
+ function safeName(s) {
267
+ return createHash2("sha256").update(String(s)).digest("hex");
268
+ }
269
+ function createFilesystemStore({ dir }) {
270
+ const entriesDir = join(dir, "entries");
271
+ const tagsDir = join(dir, "tags");
272
+ const pathsDir = join(dir, "paths");
273
+ const entryFile = (key) => join(entriesDir, hashKey(key) + ".json");
274
+ const indexFile = (base, name) => join(base, safeName(name) + ".json");
275
+ async function atomicWrite(file, contents) {
276
+ await mkdir(dirname(file), { recursive: true });
277
+ const tmp = `${file}.${process.pid}.${safeName(file).slice(0, 8)}.tmp`;
278
+ await writeFile(tmp, contents);
279
+ await rename(tmp, file);
280
+ }
281
+ async function readJson(file) {
282
+ try {
283
+ return JSON.parse(await readFile(file, "utf8"));
284
+ } catch {
285
+ return null;
286
+ }
287
+ }
288
+ async function addToIndex(base, name, key) {
289
+ const file = indexFile(base, name);
290
+ const list = await readJson(file) || [];
291
+ if (!list.includes(key)) {
292
+ list.push(key);
293
+ await atomicWrite(file, JSON.stringify(list));
294
+ }
295
+ }
296
+ async function removeFromIndex(base, name, key) {
297
+ const file = indexFile(base, name);
298
+ const list = await readJson(file);
299
+ if (!list) return;
300
+ const next = list.filter((k) => k !== key);
301
+ if (next.length) await atomicWrite(file, JSON.stringify(next));
302
+ else await rm(file, { force: true });
303
+ }
304
+ async function deleteByIndex(base, name) {
305
+ const file = indexFile(base, name);
306
+ const keys = await readJson(file);
307
+ if (!keys) return [];
308
+ for (const k of keys) await removeKey(k);
309
+ await rm(file, { force: true });
310
+ return keys;
311
+ }
312
+ async function removeKey(key) {
313
+ const record = await readJson(entryFile(key));
314
+ if (!record) return false;
315
+ await rm(entryFile(key), { force: true });
316
+ const entry = record.entry || {};
317
+ for (const t of entry.tags || []) await removeFromIndex(tagsDir, t, key);
318
+ if (entry.path) await removeFromIndex(pathsDir, entry.path, key);
319
+ return true;
320
+ }
321
+ async function walkKeys() {
322
+ const out = [];
323
+ let shards;
324
+ try {
325
+ shards = await readdir(entriesDir);
326
+ } catch {
327
+ return out;
328
+ }
329
+ for (const a of shards) {
330
+ const aDir = join(entriesDir, a);
331
+ let st;
332
+ try {
333
+ st = await stat(aDir);
334
+ } catch {
335
+ continue;
336
+ }
337
+ if (!st.isDirectory()) continue;
338
+ for (const b of await readdir(aDir)) {
339
+ const bDir = join(aDir, b);
340
+ try {
341
+ if (!(await stat(bDir)).isDirectory()) continue;
342
+ } catch {
343
+ continue;
344
+ }
345
+ for (const f of await readdir(bDir)) {
346
+ if (!f.endsWith(".json")) continue;
347
+ const rec = await readJson(join(bDir, f));
348
+ if (rec && rec.key != null) out.push(rec.key);
349
+ }
350
+ }
351
+ }
352
+ return out;
353
+ }
354
+ return {
355
+ async get(key) {
356
+ const rec = await readJson(entryFile(key));
357
+ return rec ? rec.entry : null;
358
+ },
359
+ async set(key, entry) {
360
+ const prev = await readJson(entryFile(key));
361
+ if (prev && prev.entry) {
362
+ for (const t of prev.entry.tags || []) await removeFromIndex(tagsDir, t, key);
363
+ if (prev.entry.path) await removeFromIndex(pathsDir, prev.entry.path, key);
364
+ }
365
+ await atomicWrite(entryFile(key), JSON.stringify({ key, entry }));
366
+ for (const t of entry.tags || []) await addToIndex(tagsDir, t, key);
367
+ if (entry.path) await addToIndex(pathsDir, entry.path, key);
368
+ },
369
+ async delete(key) {
370
+ return removeKey(key);
371
+ },
372
+ async deleteByTag(tag) {
373
+ return deleteByIndex(tagsDir, tag);
374
+ },
375
+ async deleteByPath(path) {
376
+ return deleteByIndex(pathsDir, path);
377
+ },
378
+ async clear() {
379
+ await rm(dir, { recursive: true, force: true });
380
+ },
381
+ async keys() {
382
+ return walkKeys();
383
+ }
384
+ };
385
+ }
386
+
387
+ // packages/cache/src/stores/redis-store.js
388
+ function createRedisStore({ client, namespace = "what" } = {}) {
389
+ if (!client) throw new Error("[what-isr] createRedisStore requires { client }");
390
+ const ck = (key) => `${namespace}:cache:${key}`;
391
+ const tk = (tag) => `${namespace}:tag:${tag}`;
392
+ const pk = (path) => `${namespace}:path:${path}`;
393
+ async function deindex(key, entry) {
394
+ if (!entry) return;
395
+ for (const t of entry.tags || []) await client.srem(tk(t), key);
396
+ if (entry.path) await client.srem(pk(entry.path), key);
397
+ }
398
+ async function deleteBySet(setKey) {
399
+ const keys = await client.smembers(setKey) || [];
400
+ for (const k of keys) await client.del(ck(k));
401
+ await client.del(setKey);
402
+ return keys;
403
+ }
404
+ return {
405
+ async get(key) {
406
+ const v = await client.get(ck(key));
407
+ return v ? JSON.parse(v) : null;
408
+ },
409
+ async set(key, entry) {
410
+ const prev = await this.get(key);
411
+ if (prev) await deindex(key, prev);
412
+ await client.set(ck(key), JSON.stringify(entry));
413
+ for (const t of entry.tags || []) await client.sadd(tk(t), key);
414
+ if (entry.path) await client.sadd(pk(entry.path), key);
415
+ },
416
+ async delete(key) {
417
+ const entry = await this.get(key);
418
+ await client.del(ck(key));
419
+ await deindex(key, entry);
420
+ return !!entry;
421
+ },
422
+ async deleteByTag(tag) {
423
+ return deleteBySet(tk(tag));
424
+ },
425
+ async deleteByPath(path) {
426
+ return deleteBySet(pk(path));
427
+ },
428
+ async clear() {
429
+ if (typeof client.keys === "function") {
430
+ const all = await client.keys(`${namespace}:*`);
431
+ for (const k of all) await client.del(k);
432
+ }
433
+ },
434
+ async keys() {
435
+ if (typeof client.keys !== "function") return [];
436
+ const prefix = `${namespace}:cache:`;
437
+ const all = await client.keys(`${prefix}*`);
438
+ return all.map((k) => k.slice(prefix.length));
439
+ }
440
+ };
441
+ }
442
+
443
+ // packages/cache/src/paths.js
444
+ async function resolveStaticPaths(getStaticPaths, ctx = {}) {
445
+ if (typeof getStaticPaths !== "function") return { paths: [], fallback: false };
446
+ const result = await getStaticPaths(ctx);
447
+ return {
448
+ paths: result && result.paths || [],
449
+ fallback: result && "fallback" in result ? result.fallback : false
450
+ };
451
+ }
452
+ function buildPath(pattern, params = {}) {
453
+ return pattern.replace(/[:*]([A-Za-z0-9_]+)/g, (_, name) => {
454
+ const v = params[name];
455
+ return v == null ? "" : String(v);
456
+ });
457
+ }
458
+ function isKnownParams(staticPaths, params) {
459
+ return staticPaths.some((entry) => {
460
+ const p = entry.params || {};
461
+ const keys = /* @__PURE__ */ new Set([...Object.keys(p), ...Object.keys(params)]);
462
+ for (const k of keys) if (String(p[k]) !== String(params[k])) return false;
463
+ return true;
464
+ });
465
+ }
466
+ function decideFallback(fallback, isKnown) {
467
+ if (isKnown) return "serve";
468
+ if (fallback === "blocking") return "render";
469
+ if (fallback === true) return "skeleton";
470
+ return "notfound";
471
+ }
472
+
473
+ // packages/cache/src/webhook.js
474
+ function safeEqual(a, b) {
475
+ if (typeof a !== "string" || typeof b !== "string") return false;
476
+ if (a.length !== b.length) return false;
477
+ let result = 0;
478
+ for (let i = 0; i < a.length; i++) result |= a.charCodeAt(i) ^ b.charCodeAt(i);
479
+ return result === 0;
480
+ }
481
+ function createRevalidateWebhook(engine, options = {}) {
482
+ const { secret, header = "x-what-revalidate-secret", regenerate = false } = options;
483
+ return async function handle(reqLike) {
484
+ const provided = (reqLike.headers || {})[header] || (reqLike.headers || {})[header.toLowerCase()];
485
+ if (!secret || !safeEqual(provided || "", secret)) {
486
+ return { status: 401, body: { message: "Unauthorized" } };
487
+ }
488
+ const body = reqLike.body;
489
+ if (!body || typeof body !== "object") {
490
+ return { status: 400, body: { message: "Invalid body" } };
491
+ }
492
+ const { paths, tags, regenerate: regen = regenerate } = body;
493
+ if (!Array.isArray(paths) && !Array.isArray(tags)) {
494
+ return { status: 400, body: { message: "Provide `paths` and/or `tags` arrays" } };
495
+ }
496
+ const revalidated = { paths: [], tags: [] };
497
+ if (Array.isArray(paths)) {
498
+ for (const p of paths) {
499
+ await engine.revalidatePath(p, { regenerate: regen });
500
+ revalidated.paths.push(p);
501
+ }
502
+ }
503
+ if (Array.isArray(tags)) {
504
+ for (const t of tags) {
505
+ await engine.revalidateTag(t, { regenerate: regen });
506
+ revalidated.tags.push(t);
507
+ }
508
+ }
509
+ return { status: 200, body: { revalidated: true, ...revalidated } };
510
+ };
511
+ }
512
+
513
+ // packages/cache/src/scheduler.js
514
+ function createScheduler(engine, options = {}) {
515
+ const {
516
+ maxConcurrent = 4,
517
+ random = Math.random,
518
+ setTimer = setTimeout,
519
+ clearTimer = clearTimeout,
520
+ logger = console
521
+ } = options;
522
+ const tasks = [];
523
+ const queue = [];
524
+ let running = false;
525
+ let active = 0;
526
+ function jittered(intervalMs) {
527
+ return Math.round(intervalMs * (1 + random() * 0.1));
528
+ }
529
+ function schedule(task) {
530
+ task.timer = setTimer(() => fire(task), jittered(task.intervalMs));
531
+ if (task.timer && typeof task.timer.unref === "function") task.timer.unref();
532
+ }
533
+ function fire(task) {
534
+ if (!running) return;
535
+ if (active < maxConcurrent) runTask(task);
536
+ else queue.push(task);
537
+ }
538
+ async function runTask(task) {
539
+ active++;
540
+ try {
541
+ await engine.regenerate(task.route);
542
+ } catch (e) {
543
+ logger.error?.("[what-isr] scheduled regenerate failed:", e);
544
+ } finally {
545
+ active--;
546
+ if (running) schedule(task);
547
+ drain();
548
+ }
549
+ }
550
+ function drain() {
551
+ while (running && active < maxConcurrent && queue.length) {
552
+ runTask(queue.shift());
553
+ }
554
+ }
555
+ return {
556
+ register(route, { intervalMs }) {
557
+ tasks.push({ route, intervalMs, timer: null });
558
+ return this;
559
+ },
560
+ start() {
561
+ running = true;
562
+ for (const t of tasks) schedule(t);
563
+ return this;
564
+ },
565
+ stop() {
566
+ running = false;
567
+ for (const t of tasks) if (t.timer != null) clearTimer(t.timer);
568
+ queue.length = 0;
569
+ return this;
570
+ },
571
+ _tasks: tasks
572
+ };
573
+ }
574
+
575
+ // packages/cache/src/cdn/cloudflare.js
576
+ function createCloudflareCDN({ zoneId, apiToken } = {}) {
577
+ const endpoint = `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`;
578
+ const headers = { Authorization: `Bearer ${apiToken}`, "Content-Type": "application/json" };
579
+ async function post(body) {
580
+ if (!zoneId || !apiToken) return;
581
+ await fetch(endpoint, { method: "POST", headers, body: JSON.stringify(body) });
582
+ }
583
+ return {
584
+ purge: (urls) => post({ files: urls }),
585
+ purgeTags: (tags) => post({ tags })
586
+ };
587
+ }
588
+
589
+ // packages/cache/src/cdn/fastly.js
590
+ function createFastlyCDN({ serviceId, apiToken } = {}) {
591
+ const headers = { "Fastly-Key": apiToken, Accept: "application/json" };
592
+ async function purgeKey(key) {
593
+ if (!serviceId || !apiToken) return;
594
+ await fetch(`https://api.fastly.com/service/${serviceId}/purge/${encodeURIComponent(key)}`, {
595
+ method: "POST",
596
+ headers
597
+ });
598
+ }
599
+ return {
600
+ async purge(urls) {
601
+ for (const url of urls) {
602
+ if (!apiToken) return;
603
+ await fetch(url, { method: "PURGE", headers });
604
+ }
605
+ },
606
+ async purgeTags(tags) {
607
+ for (const t of tags) await purgeKey(t);
608
+ }
609
+ };
610
+ }
611
+
612
+ // packages/cache/src/cdn/vercel.js
613
+ function createVercelCDN({ token, projectId, teamId } = {}) {
614
+ const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
615
+ const qs = teamId ? `?teamId=${teamId}` : "";
616
+ async function purgeTags(tags) {
617
+ if (!token || !projectId) return;
618
+ await fetch(`https://api.vercel.com/v1/projects/${projectId}/cache/purge${qs}`, {
619
+ method: "POST",
620
+ headers,
621
+ body: JSON.stringify({ tags })
622
+ });
623
+ }
624
+ return {
625
+ purge: () => Promise.resolve(),
626
+ // Vercel purges by tag; URL purge is header-driven
627
+ purgeTags
628
+ };
629
+ }
630
+ export {
631
+ buildCacheHeaders,
632
+ buildPath,
633
+ cacheKey,
634
+ createCacheEngine,
635
+ createCloudflareCDN,
636
+ createFastlyCDN,
637
+ createFilesystemStore,
638
+ createMemoryStore,
639
+ createRedisStore,
640
+ createRevalidateWebhook,
641
+ createScheduler,
642
+ createVercelCDN,
643
+ decideFallback,
644
+ hashKey,
645
+ isFresh,
646
+ isKnownParams,
647
+ isServableStale,
648
+ makeEntry,
649
+ normalizePath,
650
+ normalizeQuery,
651
+ resolveStaticPaths
652
+ };
653
+ //# sourceMappingURL=index.js.map