@plenary/next 1.0.1 → 1.1.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.
@@ -0,0 +1,66 @@
1
+ // src/tags.ts
2
+ function modelTag(modelKey) {
3
+ return `plenary:model:${modelKey}`;
4
+ }
5
+ function entryTag(slugOrId) {
6
+ return `plenary:entry:${slugOrId}`;
7
+ }
8
+ function modelEntriesTag(modelKey) {
9
+ return `plenary:model-entries:${modelKey}`;
10
+ }
11
+ var CONTENT_URL = /\/api\/v1\/content\/([^/?#]+)(?:\/([^/?#]+))?/;
12
+ function pinnedSlug(url) {
13
+ const query = url.split("?")[1]?.split("#")[0];
14
+ if (!query) return void 0;
15
+ const params = new URLSearchParams(query);
16
+ const values = [...params.getAll("where:slug"), ...params.getAll("where:slug.$eq")];
17
+ return values.length === 1 ? values[0] : void 0;
18
+ }
19
+ function createTaggedFetch(baseFetch = fetch) {
20
+ return ((input, init) => {
21
+ const method = init?.method ?? "GET";
22
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
23
+ const match = method === "GET" ? CONTENT_URL.exec(url) : null;
24
+ if (!match?.[1]) return baseFetch(input, init);
25
+ const model = decodeURIComponent(match[1]);
26
+ const entry = match[2] ? decodeURIComponent(match[2]) : pinnedSlug(url);
27
+ const tags = entry ? [modelEntriesTag(model), entryTag(entry)] : [modelTag(model)];
28
+ const next = init?.next;
29
+ const merged = {
30
+ ...init,
31
+ next: { ...next, tags: [...next?.tags ?? [], ...tags] }
32
+ };
33
+ return baseFetch(input, merged);
34
+ });
35
+ }
36
+
37
+ // src/resolve.ts
38
+ import {
39
+ planRevalidation as planPaths,
40
+ isEntryWebhookPayload
41
+ } from "@plenary/client";
42
+ import { entryUrl } from "@plenary/client";
43
+ function planRevalidation(payload, options) {
44
+ const plan = planPaths(payload, options);
45
+ const tags = [];
46
+ if (plan.relevant && options.tags && isEntryWebhookPayload(payload)) {
47
+ const modelKey = payload.model.key;
48
+ if (modelKey) {
49
+ tags.push(modelTag(modelKey));
50
+ if (payload.changed.some((p) => p === "slug" || p === "path")) tags.push(modelEntriesTag(modelKey));
51
+ }
52
+ tags.push(entryTag(payload.entry.id));
53
+ if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
54
+ }
55
+ return { ...plan, tags };
56
+ }
57
+
58
+ export {
59
+ modelTag,
60
+ entryTag,
61
+ modelEntriesTag,
62
+ createTaggedFetch,
63
+ planRevalidation,
64
+ entryUrl
65
+ };
66
+ //# sourceMappingURL=chunk-7XALU3L6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tags.ts","../src/resolve.ts"],"sourcesContent":["/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by list reads of a model's content, which any entry event on\n * the model can change. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Tag carried by every single-entry read of a model. A webhook revalidates it\n * only when an entry's slug or folder changes: the page at the old URL was\n * cached under the old slug, which the payload no longer names. */\nexport function modelEntriesTag(modelKey: string): string {\n return `plenary:model-entries:${modelKey}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/** The slug a list read pins with `where: { slug }`, if it pins exactly one. */\nfunction pinnedSlug(url: string): string | undefined {\n const query = url.split(\"?\")[1]?.split(\"#\")[0];\n if (!query) return undefined;\n const params = new URLSearchParams(query);\n const values = [...params.getAll(\"where:slug\"), ...params.getAll(\"where:slug.$eq\")];\n return values.length === 1 ? values[0] : undefined;\n}\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * A read of one entry, `/api/v1/content/{model}/{slugOrId}` or a list read\n * pinned with `where: { slug }`, is tagged with that entry, so editing another\n * entry leaves its page cached. Other GET requests to `/api/v1/content/{model}`\n * are tagged with the model. Anything else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const model = decodeURIComponent(match[1]);\n const entry = match[2] ? decodeURIComponent(match[2]) : pinnedSlug(url);\n const tags = entry ? [modelEntriesTag(model), entryTag(entry)] : [modelTag(model)];\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. The path\n * planning lives in `@plenary/client`; this adds the cache tags. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n planRevalidation as planPaths,\n isEntryWebhookPayload,\n type RevalidateOptions as PathOptions,\n type RevalidationPlan as PathPlan,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelEntriesTag, modelTag } from \"./tags.js\";\n\nexport { entryUrl, type RevalidatePaths } from \"@plenary/client\";\n\nexport interface RevalidateOptions extends PathOptions {\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\nexport interface RevalidationPlan extends PathPlan {\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const plan = planPaths(payload, options);\n const tags: string[] = [];\n if (plan.relevant && options.tags && isEntryWebhookPayload(payload)) {\n const modelKey = payload.model.key;\n if (modelKey) {\n tags.push(modelTag(modelKey));\n if (payload.changed.some((p) => p === \"slug\" || p === \"path\")) tags.push(modelEntriesTag(modelKey));\n }\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n return { ...plan, tags };\n}\n"],"mappings":";AAUO,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAIO,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAKO,SAAS,gBAAgB,UAA0B;AACxD,SAAO,yBAAyB,QAAQ;AAC1C;AAQA,IAAM,cAAc;AAGpB,SAAS,WAAW,KAAiC;AACnD,QAAM,QAAQ,IAAI,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,EAAE,CAAC;AAC7C,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,SAAS,IAAI,gBAAgB,KAAK;AACxC,QAAM,SAAS,CAAC,GAAG,OAAO,OAAO,YAAY,GAAG,GAAG,OAAO,OAAO,gBAAgB,CAAC;AAClF,SAAO,OAAO,WAAW,IAAI,OAAO,CAAC,IAAI;AAC3C;AAeO,SAAS,kBAAkB,YAA0B,OAAqB;AAC/E,UAAQ,CAAC,OAA0B,SAAuB;AACxD,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,MACJ,OAAO,UAAU,WAAW,QAAQ,iBAAiB,MAAM,MAAM,OAAO,MAAM;AAChF,UAAM,QAAQ,WAAW,QAAQ,YAAY,KAAK,GAAG,IAAI;AACzD,QAAI,CAAC,QAAQ,CAAC,EAAG,QAAO,UAAU,OAAO,IAAI;AAE7C,UAAM,QAAQ,mBAAmB,MAAM,CAAC,CAAC;AACzC,UAAM,QAAQ,MAAM,CAAC,IAAI,mBAAmB,MAAM,CAAC,CAAC,IAAI,WAAW,GAAG;AACtE,UAAM,OAAO,QAAQ,CAAC,gBAAgB,KAAK,GAAG,SAAS,KAAK,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC;AACjF,UAAM,OAAQ,MAAsC;AACpD,UAAM,SAA0B;AAAA,MAC9B,GAAG;AAAA,MACH,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,GAAI,MAAM,QAAQ,CAAC,GAAI,GAAG,IAAI,EAAE;AAAA,IAC1D;AACA,WAAO,UAAU,OAAO,MAAM;AAAA,EAChC;AACF;;;ACpEA;AAAA,EACE,oBAAoB;AAAA,EACpB;AAAA,OAIK;AAGP,SAAS,gBAAsC;AAaxC,SAAS,iBACd,SACA,SACkB;AAClB,QAAM,OAAO,UAAU,SAAS,OAAO;AACvC,QAAM,OAAiB,CAAC;AACxB,MAAI,KAAK,YAAY,QAAQ,QAAQ,sBAAsB,OAAO,GAAG;AACnE,UAAM,WAAW,QAAQ,MAAM;AAC/B,QAAI,UAAU;AACZ,WAAK,KAAK,SAAS,QAAQ,CAAC;AAC5B,UAAI,QAAQ,QAAQ,KAAK,CAAC,MAAM,MAAM,UAAU,MAAM,MAAM,EAAG,MAAK,KAAK,gBAAgB,QAAQ,CAAC;AAAA,IACpG;AACA,SAAK,KAAK,SAAS,QAAQ,MAAM,EAAE,CAAC;AACpC,QAAI,QAAQ,MAAM,KAAM,MAAK,KAAK,SAAS,QAAQ,MAAM,IAAI,CAAC;AAAA,EAChE;AACA,SAAO,EAAE,GAAG,MAAM,KAAK;AACzB;","names":[]}
package/dist/index.cjs CHANGED
@@ -23,7 +23,8 @@ __export(index_exports, {
23
23
  createRevalidateHandler: () => createRevalidateHandler,
24
24
  createTaggedFetch: () => createTaggedFetch,
25
25
  entryTag: () => entryTag,
26
- entryUrl: () => entryUrl,
26
+ entryUrl: () => import_client2.entryUrl,
27
+ modelEntriesTag: () => modelEntriesTag,
27
28
  modelTag: () => modelTag,
28
29
  planRevalidation: () => planRevalidation
29
30
  });
@@ -31,7 +32,7 @@ module.exports = __toCommonJS(index_exports);
31
32
 
32
33
  // src/revalidate.ts
33
34
  var import_cache = require("next/cache");
34
- var import_client2 = require("@plenary/client");
35
+ var import_client3 = require("@plenary/client");
35
36
 
36
37
  // src/resolve.ts
37
38
  var import_client = require("@plenary/client");
@@ -43,15 +44,26 @@ function modelTag(modelKey) {
43
44
  function entryTag(slugOrId) {
44
45
  return `plenary:entry:${slugOrId}`;
45
46
  }
47
+ function modelEntriesTag(modelKey) {
48
+ return `plenary:model-entries:${modelKey}`;
49
+ }
46
50
  var CONTENT_URL = /\/api\/v1\/content\/([^/?#]+)(?:\/([^/?#]+))?/;
51
+ function pinnedSlug(url) {
52
+ const query = url.split("?")[1]?.split("#")[0];
53
+ if (!query) return void 0;
54
+ const params = new URLSearchParams(query);
55
+ const values = [...params.getAll("where:slug"), ...params.getAll("where:slug.$eq")];
56
+ return values.length === 1 ? values[0] : void 0;
57
+ }
47
58
  function createTaggedFetch(baseFetch = fetch) {
48
59
  return ((input, init) => {
49
60
  const method = init?.method ?? "GET";
50
61
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
51
62
  const match = method === "GET" ? CONTENT_URL.exec(url) : null;
52
63
  if (!match?.[1]) return baseFetch(input, init);
53
- const tags = [modelTag(decodeURIComponent(match[1]))];
54
- if (match[2]) tags.push(entryTag(decodeURIComponent(match[2])));
64
+ const model = decodeURIComponent(match[1]);
65
+ const entry = match[2] ? decodeURIComponent(match[2]) : pinnedSlug(url);
66
+ const tags = entry ? [modelEntriesTag(model), entryTag(entry)] : [modelTag(model)];
55
67
  const next = init?.next;
56
68
  const merged = {
57
69
  ...init,
@@ -62,35 +74,20 @@ function createTaggedFetch(baseFetch = fetch) {
62
74
  }
63
75
 
64
76
  // src/resolve.ts
65
- function entryUrl(payload) {
66
- const { slug, path } = payload.entry;
67
- if (!slug) return null;
68
- const dir = path && path !== "/" ? path : "";
69
- return `${dir}/${slug}`;
70
- }
77
+ var import_client2 = require("@plenary/client");
71
78
  function planRevalidation(payload, options) {
72
- const none = { relevant: false, test: false, paths: [], tags: [] };
73
- if (!(0, import_client.isEntryWebhookPayload)(payload)) return none;
74
- const test = payload.event === import_client.WEBHOOK_TEST_EVENT;
75
- if (!test && options.events && !options.events.includes(payload.event)) {
76
- return none;
77
- }
78
- const modelKey = payload.model.key;
79
- const mapped = modelKey === void 0 ? void 0 : options.paths?.[modelKey];
80
- let paths;
81
- if (mapped === void 0) {
82
- paths = (options.entryUrl ?? entryUrl)(payload);
83
- } else {
84
- paths = typeof mapped === "function" ? mapped(payload) : mapped;
85
- }
86
- const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
79
+ const plan = (0, import_client.planRevalidation)(payload, options);
87
80
  const tags = [];
88
- if (options.tags) {
89
- if (modelKey) tags.push(modelTag(modelKey));
81
+ if (plan.relevant && options.tags && (0, import_client.isEntryWebhookPayload)(payload)) {
82
+ const modelKey = payload.model.key;
83
+ if (modelKey) {
84
+ tags.push(modelTag(modelKey));
85
+ if (payload.changed.some((p) => p === "slug" || p === "path")) tags.push(modelEntriesTag(modelKey));
86
+ }
90
87
  tags.push(entryTag(payload.entry.id));
91
88
  if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
92
89
  }
93
- return { relevant: true, test, paths: [...new Set(pathList)], tags };
90
+ return { ...plan, tags };
94
91
  }
95
92
 
96
93
  // src/revalidate.ts
@@ -98,9 +95,9 @@ function createRevalidateHandler(options) {
98
95
  return async (request) => {
99
96
  let payload;
100
97
  try {
101
- payload = await (0, import_client2.parseWebhookRequest)(request, options);
98
+ payload = await (0, import_client3.parseWebhookRequest)(request, options);
102
99
  } catch (err) {
103
- if (err instanceof import_client2.PlenaryWebhookError) {
100
+ if (err instanceof import_client3.PlenaryWebhookError) {
104
101
  return Response.json({ message: err.message }, { status: err.status });
105
102
  }
106
103
  throw err;
@@ -123,6 +120,7 @@ function createRevalidateHandler(options) {
123
120
  createTaggedFetch,
124
121
  entryTag,
125
122
  entryUrl,
123
+ modelEntriesTag,
126
124
  modelTag,
127
125
  planRevalidation
128
126
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/revalidate.ts","../src/resolve.ts","../src/tags.ts"],"sourcesContent":["/**\n * @plenary/next — Next.js glue for Plenary: webhook-driven cache revalidation\n * and cache-tagged content fetching. The Pages Router handler lives in\n * `@plenary/next/pages` so this entry stays free of `next` type imports.\n */\n\nexport { createRevalidateHandler } from \"./revalidate.js\";\nexport {\n entryUrl,\n planRevalidation,\n type RevalidateOptions,\n type RevalidatePaths,\n type RevalidationPlan,\n} from \"./resolve.js\";\nexport { createTaggedFetch, modelTag, entryTag } from \"./tags.js\";\n","import { revalidatePath, revalidateTag } from \"next/cache\";\nimport { parseWebhookRequest, PlenaryWebhookError } from \"@plenary/client\";\nimport { planRevalidation, type RevalidateOptions } from \"./resolve.js\";\n\n/**\n * Builds an App Router route handler that turns Plenary entry webhooks into\n * `revalidatePath`/`revalidateTag` calls.\n *\n * ```ts\n * // app/api/revalidate/route.ts\n * export const POST = createRevalidateHandler({\n * secret: process.env.PLENARY_WEBHOOK_SECRET!,\n * });\n * ```\n *\n * With no `paths` map, each event revalidates the entry's own URL (folder\n * path + slug). Point the webhook at the route, subscribed to the entry\n * events, with the secret in an `x-plenary-secret` header.\n *\n * The console's Test button gets a dry run: a 200 reporting what would have\n * been revalidated, without touching the cache.\n */\nexport function createRevalidateHandler(\n options: RevalidateOptions,\n): (request: Request) => Promise<Response> {\n return async (request) => {\n let payload;\n try {\n payload = await parseWebhookRequest(request, options);\n } catch (err) {\n if (err instanceof PlenaryWebhookError) {\n return Response.json({ message: err.message }, { status: err.status });\n }\n throw err;\n }\n\n const plan = planRevalidation(payload, options);\n if (!plan.relevant) {\n return Response.json({ revalidated: false, ignored: payload.event });\n }\n if (plan.test) {\n return Response.json({ test: true, paths: plan.paths, tags: plan.tags });\n }\n for (const path of plan.paths) revalidatePath(path);\n for (const tag of plan.tags) revalidateTag(tag);\n return Response.json({ revalidated: true, paths: plan.paths, tags: plan.tags });\n };\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n isEntryWebhookPayload,\n WEBHOOK_TEST_EVENT,\n type EntryWebhookEvent,\n type EntryWebhookPayload,\n type ParseWebhookOptions,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelTag } from \"./tags.js\";\n\n/** Paths for one model: a fixed path, a list, or a function of the payload.\n * A function may return `null`/`undefined` to revalidate nothing. */\nexport type RevalidatePaths =\n | string\n | string[]\n | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);\n\nexport interface RevalidateOptions extends ParseWebhookOptions {\n /**\n * Extra or overriding paths per model key. A mapped model replaces the\n * default URL entirely — include `entryUrl(payload)` (exported) in the\n * returned list to keep it.\n */\n paths?: Record<string, RevalidatePaths>;\n /** Builds the default URL for unmapped models. Defaults to joining the\n * entry's folder `path` and `slug`; return `null` to skip. */\n entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;\n /** Entry events that trigger revalidation. Default: all five — revalidation\n * is idempotent and cheap, so over-firing beats a stale page. */\n events?: EntryWebhookEvent[];\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\n/** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →\n * `/blog/spring-sale`). Entries without a slug (singletons) get no default. */\nexport function entryUrl(payload: EntryWebhookPayload): string | null {\n const { slug, path } = payload.entry;\n if (!slug) return null;\n const dir = path && path !== \"/\" ? path : \"\";\n return `${dir}/${slug}`;\n}\n\nexport interface RevalidationPlan {\n /** Whether this payload is one the handler acts on. */\n relevant: boolean;\n /** True for the console's Test button — report, don't revalidate. */\n test: boolean;\n paths: string[];\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const none = { relevant: false, test: false, paths: [], tags: [] };\n if (!isEntryWebhookPayload(payload)) return none;\n\n const test = payload.event === WEBHOOK_TEST_EVENT;\n if (!test && options.events && !options.events.includes(payload.event as EntryWebhookEvent)) {\n return none;\n }\n\n const modelKey = payload.model.key;\n const mapped = modelKey === undefined ? undefined : options.paths?.[modelKey];\n let paths: string | string[] | null | undefined;\n if (mapped === undefined) {\n paths = (options.entryUrl ?? entryUrl)(payload);\n } else {\n paths = typeof mapped === \"function\" ? mapped(payload) : mapped;\n }\n const pathList = paths == null ? [] : typeof paths === \"string\" ? [paths] : paths;\n\n const tags: string[] = [];\n if (options.tags) {\n if (modelKey) tags.push(modelTag(modelKey));\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n\n return { relevant: true, test, paths: [...new Set(pathList)], tags };\n}\n","/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by every read of a model's content. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * GET requests to `/api/v1/content/{model}` are tagged with the model, and\n * `/api/v1/content/{model}/{slugOrId}` with the model and the entry. Anything\n * else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const tags = [modelTag(decodeURIComponent(match[1]))];\n if (match[2]) tags.push(entryTag(decodeURIComponent(match[2])));\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAA8C;AAC9C,IAAAA,iBAAyD;;;ACKzD,oBAOO;;;ACJA,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAIO,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAQA,IAAM,cAAc;AAcb,SAAS,kBAAkB,YAA0B,OAAqB;AAC/E,UAAQ,CAAC,OAA0B,SAAuB;AACxD,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,MACJ,OAAO,UAAU,WAAW,QAAQ,iBAAiB,MAAM,MAAM,OAAO,MAAM;AAChF,UAAM,QAAQ,WAAW,QAAQ,YAAY,KAAK,GAAG,IAAI;AACzD,QAAI,CAAC,QAAQ,CAAC,EAAG,QAAO,UAAU,OAAO,IAAI;AAE7C,UAAM,OAAO,CAAC,SAAS,mBAAmB,MAAM,CAAC,CAAC,CAAC,CAAC;AACpD,QAAI,MAAM,CAAC,EAAG,MAAK,KAAK,SAAS,mBAAmB,MAAM,CAAC,CAAC,CAAC,CAAC;AAC9D,UAAM,OAAQ,MAAsC;AACpD,UAAM,SAA0B;AAAA,MAC9B,GAAG;AAAA,MACH,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,GAAI,MAAM,QAAQ,CAAC,GAAI,GAAG,IAAI,EAAE;AAAA,IAC1D;AACA,WAAO,UAAU,OAAO,MAAM;AAAA,EAChC;AACF;;;ADbO,SAAS,SAAS,SAA6C;AACpE,QAAM,EAAE,MAAM,KAAK,IAAI,QAAQ;AAC/B,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,MAAM,QAAQ,SAAS,MAAM,OAAO;AAC1C,SAAO,GAAG,GAAG,IAAI,IAAI;AACvB;AAYO,SAAS,iBACd,SACA,SACkB;AAClB,QAAM,OAAO,EAAE,UAAU,OAAO,MAAM,OAAO,OAAO,CAAC,GAAG,MAAM,CAAC,EAAE;AACjE,MAAI,KAAC,qCAAsB,OAAO,EAAG,QAAO;AAE5C,QAAM,OAAO,QAAQ,UAAU;AAC/B,MAAI,CAAC,QAAQ,QAAQ,UAAU,CAAC,QAAQ,OAAO,SAAS,QAAQ,KAA0B,GAAG;AAC3F,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,QAAQ,MAAM;AAC/B,QAAM,SAAS,aAAa,SAAY,SAAY,QAAQ,QAAQ,QAAQ;AAC5E,MAAI;AACJ,MAAI,WAAW,QAAW;AACxB,aAAS,QAAQ,YAAY,UAAU,OAAO;AAAA,EAChD,OAAO;AACL,YAAQ,OAAO,WAAW,aAAa,OAAO,OAAO,IAAI;AAAA,EAC3D;AACA,QAAM,WAAW,SAAS,OAAO,CAAC,IAAI,OAAO,UAAU,WAAW,CAAC,KAAK,IAAI;AAE5E,QAAM,OAAiB,CAAC;AACxB,MAAI,QAAQ,MAAM;AAChB,QAAI,SAAU,MAAK,KAAK,SAAS,QAAQ,CAAC;AAC1C,SAAK,KAAK,SAAS,QAAQ,MAAM,EAAE,CAAC;AACpC,QAAI,QAAQ,MAAM,KAAM,MAAK,KAAK,SAAS,QAAQ,MAAM,IAAI,CAAC;AAAA,EAChE;AAEA,SAAO,EAAE,UAAU,MAAM,MAAM,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC,GAAG,KAAK;AACrE;;;ADpEO,SAAS,wBACd,SACyC;AACzC,SAAO,OAAO,YAAY;AACxB,QAAI;AACJ,QAAI;AACF,gBAAU,UAAM,oCAAoB,SAAS,OAAO;AAAA,IACtD,SAAS,KAAK;AACZ,UAAI,eAAe,oCAAqB;AACtC,eAAO,SAAS,KAAK,EAAE,SAAS,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,MACvE;AACA,YAAM;AAAA,IACR;AAEA,UAAM,OAAO,iBAAiB,SAAS,OAAO;AAC9C,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO,SAAS,KAAK,EAAE,aAAa,OAAO,SAAS,QAAQ,MAAM,CAAC;AAAA,IACrE;AACA,QAAI,KAAK,MAAM;AACb,aAAO,SAAS,KAAK,EAAE,MAAM,MAAM,OAAO,KAAK,OAAO,MAAM,KAAK,KAAK,CAAC;AAAA,IACzE;AACA,eAAW,QAAQ,KAAK,MAAO,kCAAe,IAAI;AAClD,eAAW,OAAO,KAAK,KAAM,iCAAc,GAAG;AAC9C,WAAO,SAAS,KAAK,EAAE,aAAa,MAAM,OAAO,KAAK,OAAO,MAAM,KAAK,KAAK,CAAC;AAAA,EAChF;AACF;","names":["import_client"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/revalidate.ts","../src/resolve.ts","../src/tags.ts"],"sourcesContent":["/**\n * @plenary/next — Next.js glue for Plenary: webhook-driven cache revalidation\n * and cache-tagged content fetching. The Pages Router handler lives in\n * `@plenary/next/pages` so this entry stays free of `next` type imports.\n */\n\nexport { createRevalidateHandler } from \"./revalidate.js\";\nexport {\n entryUrl,\n planRevalidation,\n type RevalidateOptions,\n type RevalidatePaths,\n type RevalidationPlan,\n} from \"./resolve.js\";\nexport { createTaggedFetch, modelTag, modelEntriesTag, entryTag } from \"./tags.js\";\n","import { revalidatePath, revalidateTag } from \"next/cache\";\nimport { parseWebhookRequest, PlenaryWebhookError } from \"@plenary/client\";\nimport { planRevalidation, type RevalidateOptions } from \"./resolve.js\";\n\n/**\n * Builds an App Router route handler that turns Plenary entry webhooks into\n * `revalidatePath`/`revalidateTag` calls.\n *\n * ```ts\n * // app/api/revalidate/route.ts\n * export const POST = createRevalidateHandler({\n * secret: process.env.PLENARY_WEBHOOK_SECRET!,\n * });\n * ```\n *\n * With no `paths` map, each event revalidates the entry's own URL (folder\n * path + slug). Point the webhook at the route, subscribed to the entry\n * events, with the secret in an `x-plenary-secret` header.\n *\n * The console's Test button gets a dry run: a 200 reporting what would have\n * been revalidated, without touching the cache.\n */\nexport function createRevalidateHandler(\n options: RevalidateOptions,\n): (request: Request) => Promise<Response> {\n return async (request) => {\n let payload;\n try {\n payload = await parseWebhookRequest(request, options);\n } catch (err) {\n if (err instanceof PlenaryWebhookError) {\n return Response.json({ message: err.message }, { status: err.status });\n }\n throw err;\n }\n\n const plan = planRevalidation(payload, options);\n if (!plan.relevant) {\n return Response.json({ revalidated: false, ignored: payload.event });\n }\n if (plan.test) {\n return Response.json({ test: true, paths: plan.paths, tags: plan.tags });\n }\n for (const path of plan.paths) revalidatePath(path);\n for (const tag of plan.tags) revalidateTag(tag);\n return Response.json({ revalidated: true, paths: plan.paths, tags: plan.tags });\n };\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. The path\n * planning lives in `@plenary/client`; this adds the cache tags. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n planRevalidation as planPaths,\n isEntryWebhookPayload,\n type RevalidateOptions as PathOptions,\n type RevalidationPlan as PathPlan,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelEntriesTag, modelTag } from \"./tags.js\";\n\nexport { entryUrl, type RevalidatePaths } from \"@plenary/client\";\n\nexport interface RevalidateOptions extends PathOptions {\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\nexport interface RevalidationPlan extends PathPlan {\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const plan = planPaths(payload, options);\n const tags: string[] = [];\n if (plan.relevant && options.tags && isEntryWebhookPayload(payload)) {\n const modelKey = payload.model.key;\n if (modelKey) {\n tags.push(modelTag(modelKey));\n if (payload.changed.some((p) => p === \"slug\" || p === \"path\")) tags.push(modelEntriesTag(modelKey));\n }\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n return { ...plan, tags };\n}\n","/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by list reads of a model's content, which any entry event on\n * the model can change. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Tag carried by every single-entry read of a model. A webhook revalidates it\n * only when an entry's slug or folder changes: the page at the old URL was\n * cached under the old slug, which the payload no longer names. */\nexport function modelEntriesTag(modelKey: string): string {\n return `plenary:model-entries:${modelKey}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/** The slug a list read pins with `where: { slug }`, if it pins exactly one. */\nfunction pinnedSlug(url: string): string | undefined {\n const query = url.split(\"?\")[1]?.split(\"#\")[0];\n if (!query) return undefined;\n const params = new URLSearchParams(query);\n const values = [...params.getAll(\"where:slug\"), ...params.getAll(\"where:slug.$eq\")];\n return values.length === 1 ? values[0] : undefined;\n}\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * A read of one entry, `/api/v1/content/{model}/{slugOrId}` or a list read\n * pinned with `where: { slug }`, is tagged with that entry, so editing another\n * entry leaves its page cached. Other GET requests to `/api/v1/content/{model}`\n * are tagged with the model. Anything else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const model = decodeURIComponent(match[1]);\n const entry = match[2] ? decodeURIComponent(match[2]) : pinnedSlug(url);\n const tags = entry ? [modelEntriesTag(model), entryTag(entry)] : [modelTag(model)];\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n"],"mappings":"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","names":["import_client","import_client","planPaths"]}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { R as RevalidateOptions } from './resolve-DzNy1GAi.cjs';
2
- export { a as RevalidatePaths, b as RevalidationPlan, e as entryUrl, p as planRevalidation } from './resolve-DzNy1GAi.cjs';
3
- import '@plenary/client';
1
+ import { R as RevalidateOptions } from './resolve-CvKtOFT_.cjs';
2
+ export { a as RevalidationPlan, p as planRevalidation } from './resolve-CvKtOFT_.cjs';
3
+ export { RevalidatePaths, entryUrl } from '@plenary/client';
4
4
 
5
5
  /**
6
6
  * Builds an App Router route handler that turns Plenary entry webhooks into
@@ -29,11 +29,16 @@ declare function createRevalidateHandler(options: RevalidateOptions): (request:
29
29
  * webhook handler can revalidate every page that used a piece of content
30
30
  * without a path map.
31
31
  */
32
- /** Tag carried by every read of a model's content. */
32
+ /** Tag carried by list reads of a model's content, which any entry event on
33
+ * the model can change. */
33
34
  declare function modelTag(modelKey: string): string;
34
35
  /** Tag for one entry. The content API addresses entries by slug or UUID, and a
35
36
  * webhook payload carries both, so both spellings are revalidated. */
36
37
  declare function entryTag(slugOrId: string): string;
38
+ /** Tag carried by every single-entry read of a model. A webhook revalidates it
39
+ * only when an entry's slug or folder changes: the page at the old URL was
40
+ * cached under the old slug, which the payload no longer names. */
41
+ declare function modelEntriesTag(modelKey: string): string;
37
42
  /**
38
43
  * Wraps a fetch implementation so Plenary content reads carry cache tags.
39
44
  * Pass it to `createClient`:
@@ -42,10 +47,11 @@ declare function entryTag(slugOrId: string): string;
42
47
  * const plenary = createClient({ ...config, fetch: createTaggedFetch() });
43
48
  * ```
44
49
  *
45
- * GET requests to `/api/v1/content/{model}` are tagged with the model, and
46
- * `/api/v1/content/{model}/{slugOrId}` with the model and the entry. Anything
47
- * else passes through untouched.
50
+ * A read of one entry, `/api/v1/content/{model}/{slugOrId}` or a list read
51
+ * pinned with `where: { slug }`, is tagged with that entry, so editing another
52
+ * entry leaves its page cached. Other GET requests to `/api/v1/content/{model}`
53
+ * are tagged with the model. Anything else passes through untouched.
48
54
  */
49
55
  declare function createTaggedFetch(baseFetch?: typeof fetch): typeof fetch;
50
56
 
51
- export { RevalidateOptions, createRevalidateHandler, createTaggedFetch, entryTag, modelTag };
57
+ export { RevalidateOptions, createRevalidateHandler, createTaggedFetch, entryTag, modelEntriesTag, modelTag };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { R as RevalidateOptions } from './resolve-DzNy1GAi.js';
2
- export { a as RevalidatePaths, b as RevalidationPlan, e as entryUrl, p as planRevalidation } from './resolve-DzNy1GAi.js';
3
- import '@plenary/client';
1
+ import { R as RevalidateOptions } from './resolve-CvKtOFT_.js';
2
+ export { a as RevalidationPlan, p as planRevalidation } from './resolve-CvKtOFT_.js';
3
+ export { RevalidatePaths, entryUrl } from '@plenary/client';
4
4
 
5
5
  /**
6
6
  * Builds an App Router route handler that turns Plenary entry webhooks into
@@ -29,11 +29,16 @@ declare function createRevalidateHandler(options: RevalidateOptions): (request:
29
29
  * webhook handler can revalidate every page that used a piece of content
30
30
  * without a path map.
31
31
  */
32
- /** Tag carried by every read of a model's content. */
32
+ /** Tag carried by list reads of a model's content, which any entry event on
33
+ * the model can change. */
33
34
  declare function modelTag(modelKey: string): string;
34
35
  /** Tag for one entry. The content API addresses entries by slug or UUID, and a
35
36
  * webhook payload carries both, so both spellings are revalidated. */
36
37
  declare function entryTag(slugOrId: string): string;
38
+ /** Tag carried by every single-entry read of a model. A webhook revalidates it
39
+ * only when an entry's slug or folder changes: the page at the old URL was
40
+ * cached under the old slug, which the payload no longer names. */
41
+ declare function modelEntriesTag(modelKey: string): string;
37
42
  /**
38
43
  * Wraps a fetch implementation so Plenary content reads carry cache tags.
39
44
  * Pass it to `createClient`:
@@ -42,10 +47,11 @@ declare function entryTag(slugOrId: string): string;
42
47
  * const plenary = createClient({ ...config, fetch: createTaggedFetch() });
43
48
  * ```
44
49
  *
45
- * GET requests to `/api/v1/content/{model}` are tagged with the model, and
46
- * `/api/v1/content/{model}/{slugOrId}` with the model and the entry. Anything
47
- * else passes through untouched.
50
+ * A read of one entry, `/api/v1/content/{model}/{slugOrId}` or a list read
51
+ * pinned with `where: { slug }`, is tagged with that entry, so editing another
52
+ * entry leaves its page cached. Other GET requests to `/api/v1/content/{model}`
53
+ * are tagged with the model. Anything else passes through untouched.
48
54
  */
49
55
  declare function createTaggedFetch(baseFetch?: typeof fetch): typeof fetch;
50
56
 
51
- export { RevalidateOptions, createRevalidateHandler, createTaggedFetch, entryTag, modelTag };
57
+ export { RevalidateOptions, createRevalidateHandler, createTaggedFetch, entryTag, modelEntriesTag, modelTag };
package/dist/index.js CHANGED
@@ -2,9 +2,10 @@ import {
2
2
  createTaggedFetch,
3
3
  entryTag,
4
4
  entryUrl,
5
+ modelEntriesTag,
5
6
  modelTag,
6
7
  planRevalidation
7
- } from "./chunk-BMLKEIKX.js";
8
+ } from "./chunk-7XALU3L6.js";
8
9
 
9
10
  // src/revalidate.ts
10
11
  import { revalidatePath, revalidateTag } from "next/cache";
@@ -37,6 +38,7 @@ export {
37
38
  createTaggedFetch,
38
39
  entryTag,
39
40
  entryUrl,
41
+ modelEntriesTag,
40
42
  modelTag,
41
43
  planRevalidation
42
44
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/revalidate.ts"],"sourcesContent":["import { revalidatePath, revalidateTag } from \"next/cache\";\nimport { parseWebhookRequest, PlenaryWebhookError } from \"@plenary/client\";\nimport { planRevalidation, type RevalidateOptions } from \"./resolve.js\";\n\n/**\n * Builds an App Router route handler that turns Plenary entry webhooks into\n * `revalidatePath`/`revalidateTag` calls.\n *\n * ```ts\n * // app/api/revalidate/route.ts\n * export const POST = createRevalidateHandler({\n * secret: process.env.PLENARY_WEBHOOK_SECRET!,\n * });\n * ```\n *\n * With no `paths` map, each event revalidates the entry's own URL (folder\n * path + slug). Point the webhook at the route, subscribed to the entry\n * events, with the secret in an `x-plenary-secret` header.\n *\n * The console's Test button gets a dry run: a 200 reporting what would have\n * been revalidated, without touching the cache.\n */\nexport function createRevalidateHandler(\n options: RevalidateOptions,\n): (request: Request) => Promise<Response> {\n return async (request) => {\n let payload;\n try {\n payload = await parseWebhookRequest(request, options);\n } catch (err) {\n if (err instanceof PlenaryWebhookError) {\n return Response.json({ message: err.message }, { status: err.status });\n }\n throw err;\n }\n\n const plan = planRevalidation(payload, options);\n if (!plan.relevant) {\n return Response.json({ revalidated: false, ignored: payload.event });\n }\n if (plan.test) {\n return Response.json({ test: true, paths: plan.paths, tags: plan.tags });\n }\n for (const path of plan.paths) revalidatePath(path);\n for (const tag of plan.tags) revalidateTag(tag);\n return Response.json({ revalidated: true, paths: plan.paths, tags: plan.tags });\n };\n}\n"],"mappings":";;;;;;;;;AAAA,SAAS,gBAAgB,qBAAqB;AAC9C,SAAS,qBAAqB,2BAA2B;AAqBlD,SAAS,wBACd,SACyC;AACzC,SAAO,OAAO,YAAY;AACxB,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,oBAAoB,SAAS,OAAO;AAAA,IACtD,SAAS,KAAK;AACZ,UAAI,eAAe,qBAAqB;AACtC,eAAO,SAAS,KAAK,EAAE,SAAS,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,MACvE;AACA,YAAM;AAAA,IACR;AAEA,UAAM,OAAO,iBAAiB,SAAS,OAAO;AAC9C,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO,SAAS,KAAK,EAAE,aAAa,OAAO,SAAS,QAAQ,MAAM,CAAC;AAAA,IACrE;AACA,QAAI,KAAK,MAAM;AACb,aAAO,SAAS,KAAK,EAAE,MAAM,MAAM,OAAO,KAAK,OAAO,MAAM,KAAK,KAAK,CAAC;AAAA,IACzE;AACA,eAAW,QAAQ,KAAK,MAAO,gBAAe,IAAI;AAClD,eAAW,OAAO,KAAK,KAAM,eAAc,GAAG;AAC9C,WAAO,SAAS,KAAK,EAAE,aAAa,MAAM,OAAO,KAAK,OAAO,MAAM,KAAK,KAAK,CAAC;AAAA,EAChF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/revalidate.ts"],"sourcesContent":["import { revalidatePath, revalidateTag } from \"next/cache\";\nimport { parseWebhookRequest, PlenaryWebhookError } from \"@plenary/client\";\nimport { planRevalidation, type RevalidateOptions } from \"./resolve.js\";\n\n/**\n * Builds an App Router route handler that turns Plenary entry webhooks into\n * `revalidatePath`/`revalidateTag` calls.\n *\n * ```ts\n * // app/api/revalidate/route.ts\n * export const POST = createRevalidateHandler({\n * secret: process.env.PLENARY_WEBHOOK_SECRET!,\n * });\n * ```\n *\n * With no `paths` map, each event revalidates the entry's own URL (folder\n * path + slug). Point the webhook at the route, subscribed to the entry\n * events, with the secret in an `x-plenary-secret` header.\n *\n * The console's Test button gets a dry run: a 200 reporting what would have\n * been revalidated, without touching the cache.\n */\nexport function createRevalidateHandler(\n options: RevalidateOptions,\n): (request: Request) => Promise<Response> {\n return async (request) => {\n let payload;\n try {\n payload = await parseWebhookRequest(request, options);\n } catch (err) {\n if (err instanceof PlenaryWebhookError) {\n return Response.json({ message: err.message }, { status: err.status });\n }\n throw err;\n }\n\n const plan = planRevalidation(payload, options);\n if (!plan.relevant) {\n return Response.json({ revalidated: false, ignored: payload.event });\n }\n if (plan.test) {\n return Response.json({ test: true, paths: plan.paths, tags: plan.tags });\n }\n for (const path of plan.paths) revalidatePath(path);\n for (const tag of plan.tags) revalidateTag(tag);\n return Response.json({ revalidated: true, paths: plan.paths, tags: plan.tags });\n };\n}\n"],"mappings":";;;;;;;;;;AAAA,SAAS,gBAAgB,qBAAqB;AAC9C,SAAS,qBAAqB,2BAA2B;AAqBlD,SAAS,wBACd,SACyC;AACzC,SAAO,OAAO,YAAY;AACxB,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,oBAAoB,SAAS,OAAO;AAAA,IACtD,SAAS,KAAK;AACZ,UAAI,eAAe,qBAAqB;AACtC,eAAO,SAAS,KAAK,EAAE,SAAS,IAAI,QAAQ,GAAG,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,MACvE;AACA,YAAM;AAAA,IACR;AAEA,UAAM,OAAO,iBAAiB,SAAS,OAAO;AAC9C,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO,SAAS,KAAK,EAAE,aAAa,OAAO,SAAS,QAAQ,MAAM,CAAC;AAAA,IACrE;AACA,QAAI,KAAK,MAAM;AACb,aAAO,SAAS,KAAK,EAAE,MAAM,MAAM,OAAO,KAAK,OAAO,MAAM,KAAK,KAAK,CAAC;AAAA,IACzE;AACA,eAAW,QAAQ,KAAK,MAAO,gBAAe,IAAI;AAClD,eAAW,OAAO,KAAK,KAAM,eAAc,GAAG;AAC9C,WAAO,SAAS,KAAK,EAAE,aAAa,MAAM,OAAO,KAAK,OAAO,MAAM,KAAK,KAAK,CAAC;AAAA,EAChF;AACF;","names":[]}
package/dist/pages.cjs CHANGED
@@ -23,7 +23,7 @@ __export(pages_exports, {
23
23
  createPagesRevalidateHandler: () => createPagesRevalidateHandler
24
24
  });
25
25
  module.exports = __toCommonJS(pages_exports);
26
- var import_client2 = require("@plenary/client");
26
+ var import_client3 = require("@plenary/client");
27
27
 
28
28
  // src/resolve.ts
29
29
  var import_client = require("@plenary/client");
@@ -35,37 +35,25 @@ function modelTag(modelKey) {
35
35
  function entryTag(slugOrId) {
36
36
  return `plenary:entry:${slugOrId}`;
37
37
  }
38
+ function modelEntriesTag(modelKey) {
39
+ return `plenary:model-entries:${modelKey}`;
40
+ }
38
41
 
39
42
  // src/resolve.ts
40
- function entryUrl(payload) {
41
- const { slug, path } = payload.entry;
42
- if (!slug) return null;
43
- const dir = path && path !== "/" ? path : "";
44
- return `${dir}/${slug}`;
45
- }
43
+ var import_client2 = require("@plenary/client");
46
44
  function planRevalidation(payload, options) {
47
- const none = { relevant: false, test: false, paths: [], tags: [] };
48
- if (!(0, import_client.isEntryWebhookPayload)(payload)) return none;
49
- const test = payload.event === import_client.WEBHOOK_TEST_EVENT;
50
- if (!test && options.events && !options.events.includes(payload.event)) {
51
- return none;
52
- }
53
- const modelKey = payload.model.key;
54
- const mapped = modelKey === void 0 ? void 0 : options.paths?.[modelKey];
55
- let paths;
56
- if (mapped === void 0) {
57
- paths = (options.entryUrl ?? entryUrl)(payload);
58
- } else {
59
- paths = typeof mapped === "function" ? mapped(payload) : mapped;
60
- }
61
- const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
45
+ const plan = (0, import_client.planRevalidation)(payload, options);
62
46
  const tags = [];
63
- if (options.tags) {
64
- if (modelKey) tags.push(modelTag(modelKey));
47
+ if (plan.relevant && options.tags && (0, import_client.isEntryWebhookPayload)(payload)) {
48
+ const modelKey = payload.model.key;
49
+ if (modelKey) {
50
+ tags.push(modelTag(modelKey));
51
+ if (payload.changed.some((p) => p === "slug" || p === "path")) tags.push(modelEntriesTag(modelKey));
52
+ }
65
53
  tags.push(entryTag(payload.entry.id));
66
54
  if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
67
55
  }
68
- return { relevant: true, test, paths: [...new Set(pathList)], tags };
56
+ return { ...plan, tags };
69
57
  }
70
58
 
71
59
  // src/pages.ts
@@ -84,15 +72,15 @@ function createPagesRevalidateHandler(options) {
84
72
  const fromHeader = req.headers[header];
85
73
  const fromQuery = req.query[query];
86
74
  const provided = (Array.isArray(fromHeader) ? fromHeader[0] : fromHeader) ?? (Array.isArray(fromQuery) ? fromQuery[0] : fromQuery);
87
- if (!(0, import_client2.verifyWebhookSecret)(provided, options.secret)) {
75
+ if (!(0, import_client3.verifyWebhookSecret)(provided, options.secret)) {
88
76
  res.status(401).json({ message: "Invalid webhook secret" });
89
77
  return;
90
78
  }
91
79
  let payload;
92
80
  try {
93
- payload = (0, import_client2.parseWebhookPayload)(req.body);
81
+ payload = (0, import_client3.parseWebhookPayload)(req.body);
94
82
  } catch (err) {
95
- if (err instanceof import_client2.PlenaryWebhookError) {
83
+ if (err instanceof import_client3.PlenaryWebhookError) {
96
84
  res.status(err.status).json({ message: err.message });
97
85
  return;
98
86
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pages.ts","../src/resolve.ts","../src/tags.ts"],"sourcesContent":["import type { NextApiRequest, NextApiResponse } from \"next\";\nimport {\n parseWebhookPayload,\n verifyWebhookSecret,\n PlenaryWebhookError,\n} from \"@plenary/client\";\nimport { planRevalidation, type RevalidateOptions } from \"./resolve.js\";\n\n/**\n * The Pages Router counterpart of `createRevalidateHandler`, built on\n * `res.revalidate`.\n *\n * ```ts\n * // pages/api/revalidate.ts\n * export default createPagesRevalidateHandler({\n * secret: process.env.PLENARY_WEBHOOK_SECRET!,\n * });\n * ```\n *\n * Cache tags are an App Router feature; the `tags` option is ignored here.\n */\nexport function createPagesRevalidateHandler(\n options: RevalidateOptions,\n): (req: NextApiRequest, res: NextApiResponse) => Promise<void> {\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n\n return async (req, res) => {\n if (req.method !== \"POST\") {\n res.status(405).json({ message: \"Method not allowed\" });\n return;\n }\n if (!options.secret) {\n res.status(500).json({ message: \"Webhook secret is not configured\" });\n return;\n }\n const fromHeader = req.headers[header];\n const fromQuery = req.query[query];\n const provided =\n (Array.isArray(fromHeader) ? fromHeader[0] : fromHeader) ??\n (Array.isArray(fromQuery) ? fromQuery[0] : fromQuery);\n if (!verifyWebhookSecret(provided, options.secret)) {\n res.status(401).json({ message: \"Invalid webhook secret\" });\n return;\n }\n\n let payload;\n try {\n payload = parseWebhookPayload(req.body);\n } catch (err) {\n if (err instanceof PlenaryWebhookError) {\n res.status(err.status).json({ message: err.message });\n return;\n }\n throw err;\n }\n\n const plan = planRevalidation(payload, options);\n if (!plan.relevant) {\n res.json({ revalidated: false, ignored: payload.event });\n return;\n }\n if (plan.test) {\n res.json({ test: true, paths: plan.paths });\n return;\n }\n await Promise.all(plan.paths.map((p) => res.revalidate(p)));\n res.json({ revalidated: true, paths: plan.paths });\n };\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n isEntryWebhookPayload,\n WEBHOOK_TEST_EVENT,\n type EntryWebhookEvent,\n type EntryWebhookPayload,\n type ParseWebhookOptions,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelTag } from \"./tags.js\";\n\n/** Paths for one model: a fixed path, a list, or a function of the payload.\n * A function may return `null`/`undefined` to revalidate nothing. */\nexport type RevalidatePaths =\n | string\n | string[]\n | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);\n\nexport interface RevalidateOptions extends ParseWebhookOptions {\n /**\n * Extra or overriding paths per model key. A mapped model replaces the\n * default URL entirely — include `entryUrl(payload)` (exported) in the\n * returned list to keep it.\n */\n paths?: Record<string, RevalidatePaths>;\n /** Builds the default URL for unmapped models. Defaults to joining the\n * entry's folder `path` and `slug`; return `null` to skip. */\n entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;\n /** Entry events that trigger revalidation. Default: all five — revalidation\n * is idempotent and cheap, so over-firing beats a stale page. */\n events?: EntryWebhookEvent[];\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\n/** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →\n * `/blog/spring-sale`). Entries without a slug (singletons) get no default. */\nexport function entryUrl(payload: EntryWebhookPayload): string | null {\n const { slug, path } = payload.entry;\n if (!slug) return null;\n const dir = path && path !== \"/\" ? path : \"\";\n return `${dir}/${slug}`;\n}\n\nexport interface RevalidationPlan {\n /** Whether this payload is one the handler acts on. */\n relevant: boolean;\n /** True for the console's Test button — report, don't revalidate. */\n test: boolean;\n paths: string[];\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const none = { relevant: false, test: false, paths: [], tags: [] };\n if (!isEntryWebhookPayload(payload)) return none;\n\n const test = payload.event === WEBHOOK_TEST_EVENT;\n if (!test && options.events && !options.events.includes(payload.event as EntryWebhookEvent)) {\n return none;\n }\n\n const modelKey = payload.model.key;\n const mapped = modelKey === undefined ? undefined : options.paths?.[modelKey];\n let paths: string | string[] | null | undefined;\n if (mapped === undefined) {\n paths = (options.entryUrl ?? entryUrl)(payload);\n } else {\n paths = typeof mapped === \"function\" ? mapped(payload) : mapped;\n }\n const pathList = paths == null ? [] : typeof paths === \"string\" ? [paths] : paths;\n\n const tags: string[] = [];\n if (options.tags) {\n if (modelKey) tags.push(modelTag(modelKey));\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n\n return { relevant: true, test, paths: [...new Set(pathList)], tags };\n}\n","/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by every read of a model's content. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * GET requests to `/api/v1/content/{model}` are tagged with the model, and\n * `/api/v1/content/{model}/{slugOrId}` with the model and the entry. Anything\n * else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const tags = [modelTag(decodeURIComponent(match[1]))];\n if (match[2]) tags.push(entryTag(decodeURIComponent(match[2])));\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n"],"mappings":"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","names":["import_client"]}
1
+ {"version":3,"sources":["../src/pages.ts","../src/resolve.ts","../src/tags.ts"],"sourcesContent":["import type { NextApiRequest, NextApiResponse } from \"next\";\nimport {\n parseWebhookPayload,\n verifyWebhookSecret,\n PlenaryWebhookError,\n} from \"@plenary/client\";\nimport { planRevalidation, type RevalidateOptions } from \"./resolve.js\";\n\n/**\n * The Pages Router counterpart of `createRevalidateHandler`, built on\n * `res.revalidate`.\n *\n * ```ts\n * // pages/api/revalidate.ts\n * export default createPagesRevalidateHandler({\n * secret: process.env.PLENARY_WEBHOOK_SECRET!,\n * });\n * ```\n *\n * Cache tags are an App Router feature; the `tags` option is ignored here.\n */\nexport function createPagesRevalidateHandler(\n options: RevalidateOptions,\n): (req: NextApiRequest, res: NextApiResponse) => Promise<void> {\n const header = options.secretHeader ?? \"x-plenary-secret\";\n const query = options.secretQuery ?? \"secret\";\n\n return async (req, res) => {\n if (req.method !== \"POST\") {\n res.status(405).json({ message: \"Method not allowed\" });\n return;\n }\n if (!options.secret) {\n res.status(500).json({ message: \"Webhook secret is not configured\" });\n return;\n }\n const fromHeader = req.headers[header];\n const fromQuery = req.query[query];\n const provided =\n (Array.isArray(fromHeader) ? fromHeader[0] : fromHeader) ??\n (Array.isArray(fromQuery) ? fromQuery[0] : fromQuery);\n if (!verifyWebhookSecret(provided, options.secret)) {\n res.status(401).json({ message: \"Invalid webhook secret\" });\n return;\n }\n\n let payload;\n try {\n payload = parseWebhookPayload(req.body);\n } catch (err) {\n if (err instanceof PlenaryWebhookError) {\n res.status(err.status).json({ message: err.message });\n return;\n }\n throw err;\n }\n\n const plan = planRevalidation(payload, options);\n if (!plan.relevant) {\n res.json({ revalidated: false, ignored: payload.event });\n return;\n }\n if (plan.test) {\n res.json({ test: true, paths: plan.paths });\n return;\n }\n await Promise.all(plan.paths.map((p) => res.revalidate(p)));\n res.json({ revalidated: true, paths: plan.paths });\n };\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. The path\n * planning lives in `@plenary/client`; this adds the cache tags. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n planRevalidation as planPaths,\n isEntryWebhookPayload,\n type RevalidateOptions as PathOptions,\n type RevalidationPlan as PathPlan,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelEntriesTag, modelTag } from \"./tags.js\";\n\nexport { entryUrl, type RevalidatePaths } from \"@plenary/client\";\n\nexport interface RevalidateOptions extends PathOptions {\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\nexport interface RevalidationPlan extends PathPlan {\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const plan = planPaths(payload, options);\n const tags: string[] = [];\n if (plan.relevant && options.tags && isEntryWebhookPayload(payload)) {\n const modelKey = payload.model.key;\n if (modelKey) {\n tags.push(modelTag(modelKey));\n if (payload.changed.some((p) => p === \"slug\" || p === \"path\")) tags.push(modelEntriesTag(modelKey));\n }\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n return { ...plan, tags };\n}\n","/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by list reads of a model's content, which any entry event on\n * the model can change. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Tag carried by every single-entry read of a model. A webhook revalidates it\n * only when an entry's slug or folder changes: the page at the old URL was\n * cached under the old slug, which the payload no longer names. */\nexport function modelEntriesTag(modelKey: string): string {\n return `plenary:model-entries:${modelKey}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/** The slug a list read pins with `where: { slug }`, if it pins exactly one. */\nfunction pinnedSlug(url: string): string | undefined {\n const query = url.split(\"?\")[1]?.split(\"#\")[0];\n if (!query) return undefined;\n const params = new URLSearchParams(query);\n const values = [...params.getAll(\"where:slug\"), ...params.getAll(\"where:slug.$eq\")];\n return values.length === 1 ? values[0] : undefined;\n}\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * A read of one entry, `/api/v1/content/{model}/{slugOrId}` or a list read\n * pinned with `where: { slug }`, is tagged with that entry, so editing another\n * entry leaves its page cached. Other GET requests to `/api/v1/content/{model}`\n * are tagged with the model. Anything else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const model = decodeURIComponent(match[1]);\n const entry = match[2] ? decodeURIComponent(match[2]) : pinnedSlug(url);\n const tags = entry ? [modelEntriesTag(model), entryTag(entry)] : [modelTag(model)];\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,IAAAA,iBAIO;;;ACEP,oBAMO;;;ACHA,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAIO,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAKO,SAAS,gBAAgB,UAA0B;AACxD,SAAO,yBAAyB,QAAQ;AAC1C;;;ADTA,IAAAC,iBAA+C;AAaxC,SAAS,iBACd,SACA,SACkB;AAClB,QAAM,WAAO,cAAAC,kBAAU,SAAS,OAAO;AACvC,QAAM,OAAiB,CAAC;AACxB,MAAI,KAAK,YAAY,QAAQ,YAAQ,qCAAsB,OAAO,GAAG;AACnE,UAAM,WAAW,QAAQ,MAAM;AAC/B,QAAI,UAAU;AACZ,WAAK,KAAK,SAAS,QAAQ,CAAC;AAC5B,UAAI,QAAQ,QAAQ,KAAK,CAAC,MAAM,MAAM,UAAU,MAAM,MAAM,EAAG,MAAK,KAAK,gBAAgB,QAAQ,CAAC;AAAA,IACpG;AACA,SAAK,KAAK,SAAS,QAAQ,MAAM,EAAE,CAAC;AACpC,QAAI,QAAQ,MAAM,KAAM,MAAK,KAAK,SAAS,QAAQ,MAAM,IAAI,CAAC;AAAA,EAChE;AACA,SAAO,EAAE,GAAG,MAAM,KAAK;AACzB;;;ADxBO,SAAS,6BACd,SAC8D;AAC9D,QAAM,SAAS,QAAQ,gBAAgB;AACvC,QAAM,QAAQ,QAAQ,eAAe;AAErC,SAAO,OAAO,KAAK,QAAQ;AACzB,QAAI,IAAI,WAAW,QAAQ;AACzB,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,qBAAqB,CAAC;AACtD;AAAA,IACF;AACA,QAAI,CAAC,QAAQ,QAAQ;AACnB,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,mCAAmC,CAAC;AACpE;AAAA,IACF;AACA,UAAM,aAAa,IAAI,QAAQ,MAAM;AACrC,UAAM,YAAY,IAAI,MAAM,KAAK;AACjC,UAAM,YACH,MAAM,QAAQ,UAAU,IAAI,WAAW,CAAC,IAAI,gBAC5C,MAAM,QAAQ,SAAS,IAAI,UAAU,CAAC,IAAI;AAC7C,QAAI,KAAC,oCAAoB,UAAU,QAAQ,MAAM,GAAG;AAClD,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,SAAS,yBAAyB,CAAC;AAC1D;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,oBAAU,oCAAoB,IAAI,IAAI;AAAA,IACxC,SAAS,KAAK;AACZ,UAAI,eAAe,oCAAqB;AACtC,YAAI,OAAO,IAAI,MAAM,EAAE,KAAK,EAAE,SAAS,IAAI,QAAQ,CAAC;AACpD;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAEA,UAAM,OAAO,iBAAiB,SAAS,OAAO;AAC9C,QAAI,CAAC,KAAK,UAAU;AAClB,UAAI,KAAK,EAAE,aAAa,OAAO,SAAS,QAAQ,MAAM,CAAC;AACvD;AAAA,IACF;AACA,QAAI,KAAK,MAAM;AACb,UAAI,KAAK,EAAE,MAAM,MAAM,OAAO,KAAK,MAAM,CAAC;AAC1C;AAAA,IACF;AACA,UAAM,QAAQ,IAAI,KAAK,MAAM,IAAI,CAAC,MAAM,IAAI,WAAW,CAAC,CAAC,CAAC;AAC1D,QAAI,KAAK,EAAE,aAAa,MAAM,OAAO,KAAK,MAAM,CAAC;AAAA,EACnD;AACF;","names":["import_client","import_client","planPaths"]}
package/dist/pages.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NextApiRequest, NextApiResponse } from 'next';
2
- import { R as RevalidateOptions } from './resolve-DzNy1GAi.cjs';
2
+ import { R as RevalidateOptions } from './resolve-CvKtOFT_.cjs';
3
3
  import '@plenary/client';
4
4
 
5
5
  /**
package/dist/pages.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NextApiRequest, NextApiResponse } from 'next';
2
- import { R as RevalidateOptions } from './resolve-DzNy1GAi.js';
2
+ import { R as RevalidateOptions } from './resolve-CvKtOFT_.js';
3
3
  import '@plenary/client';
4
4
 
5
5
  /**
package/dist/pages.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  planRevalidation
3
- } from "./chunk-BMLKEIKX.js";
3
+ } from "./chunk-7XALU3L6.js";
4
4
 
5
5
  // src/pages.ts
6
6
  import {
@@ -0,0 +1,21 @@
1
+ import { RevalidateOptions as RevalidateOptions$1, RevalidationPlan as RevalidationPlan$1, WebhookPayload } from '@plenary/client';
2
+
3
+ /**
4
+ * The revalidation decision, shared by the App Router and Pages Router
5
+ * handlers: which paths (and tags) one webhook payload maps to. The path
6
+ * planning lives in `@plenary/client`; this adds the cache tags. Pure — the
7
+ * handlers own the framework calls.
8
+ */
9
+
10
+ interface RevalidateOptions extends RevalidateOptions$1 {
11
+ /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used
12
+ * by `createTaggedFetch`. App Router only. */
13
+ tags?: boolean;
14
+ }
15
+ interface RevalidationPlan extends RevalidationPlan$1 {
16
+ tags: string[];
17
+ }
18
+ /** Decides what one payload revalidates. */
19
+ declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
20
+
21
+ export { type RevalidateOptions as R, type RevalidationPlan as a, planRevalidation as p };
@@ -0,0 +1,21 @@
1
+ import { RevalidateOptions as RevalidateOptions$1, RevalidationPlan as RevalidationPlan$1, WebhookPayload } from '@plenary/client';
2
+
3
+ /**
4
+ * The revalidation decision, shared by the App Router and Pages Router
5
+ * handlers: which paths (and tags) one webhook payload maps to. The path
6
+ * planning lives in `@plenary/client`; this adds the cache tags. Pure — the
7
+ * handlers own the framework calls.
8
+ */
9
+
10
+ interface RevalidateOptions extends RevalidateOptions$1 {
11
+ /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used
12
+ * by `createTaggedFetch`. App Router only. */
13
+ tags?: boolean;
14
+ }
15
+ interface RevalidationPlan extends RevalidationPlan$1 {
16
+ tags: string[];
17
+ }
18
+ /** Decides what one payload revalidates. */
19
+ declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
20
+
21
+ export { type RevalidateOptions as R, type RevalidationPlan as a, planRevalidation as p };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plenary/next",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Plenary Next.js integration: webhook revalidation handlers and cache-tagged fetch.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -23,7 +23,7 @@
23
23
  "dist"
24
24
  ],
25
25
  "dependencies": {
26
- "@plenary/client": "1.0.1"
26
+ "@plenary/client": "1.1.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "next": "^14 || ^15 || ^16"
@@ -1,69 +0,0 @@
1
- // src/tags.ts
2
- function modelTag(modelKey) {
3
- return `plenary:model:${modelKey}`;
4
- }
5
- function entryTag(slugOrId) {
6
- return `plenary:entry:${slugOrId}`;
7
- }
8
- var CONTENT_URL = /\/api\/v1\/content\/([^/?#]+)(?:\/([^/?#]+))?/;
9
- function createTaggedFetch(baseFetch = fetch) {
10
- return ((input, init) => {
11
- const method = init?.method ?? "GET";
12
- const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
13
- const match = method === "GET" ? CONTENT_URL.exec(url) : null;
14
- if (!match?.[1]) return baseFetch(input, init);
15
- const tags = [modelTag(decodeURIComponent(match[1]))];
16
- if (match[2]) tags.push(entryTag(decodeURIComponent(match[2])));
17
- const next = init?.next;
18
- const merged = {
19
- ...init,
20
- next: { ...next, tags: [...next?.tags ?? [], ...tags] }
21
- };
22
- return baseFetch(input, merged);
23
- });
24
- }
25
-
26
- // src/resolve.ts
27
- import {
28
- isEntryWebhookPayload,
29
- WEBHOOK_TEST_EVENT
30
- } from "@plenary/client";
31
- function entryUrl(payload) {
32
- const { slug, path } = payload.entry;
33
- if (!slug) return null;
34
- const dir = path && path !== "/" ? path : "";
35
- return `${dir}/${slug}`;
36
- }
37
- function planRevalidation(payload, options) {
38
- const none = { relevant: false, test: false, paths: [], tags: [] };
39
- if (!isEntryWebhookPayload(payload)) return none;
40
- const test = payload.event === WEBHOOK_TEST_EVENT;
41
- if (!test && options.events && !options.events.includes(payload.event)) {
42
- return none;
43
- }
44
- const modelKey = payload.model.key;
45
- const mapped = modelKey === void 0 ? void 0 : options.paths?.[modelKey];
46
- let paths;
47
- if (mapped === void 0) {
48
- paths = (options.entryUrl ?? entryUrl)(payload);
49
- } else {
50
- paths = typeof mapped === "function" ? mapped(payload) : mapped;
51
- }
52
- const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
53
- const tags = [];
54
- if (options.tags) {
55
- if (modelKey) tags.push(modelTag(modelKey));
56
- tags.push(entryTag(payload.entry.id));
57
- if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
58
- }
59
- return { relevant: true, test, paths: [...new Set(pathList)], tags };
60
- }
61
-
62
- export {
63
- modelTag,
64
- entryTag,
65
- createTaggedFetch,
66
- entryUrl,
67
- planRevalidation
68
- };
69
- //# sourceMappingURL=chunk-BMLKEIKX.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/tags.ts","../src/resolve.ts"],"sourcesContent":["/**\n * Cache-tag conventions shared by the tagged fetch and the revalidation\n * handler. A page that fetches Plenary content through\n * {@link createTaggedFetch} is tagged with the model and entry it read, so the\n * webhook handler can revalidate every page that used a piece of content\n * without a path map.\n */\n\n/** Tag carried by every read of a model's content. */\nexport function modelTag(modelKey: string): string {\n return `plenary:model:${modelKey}`;\n}\n\n/** Tag for one entry. The content API addresses entries by slug or UUID, and a\n * webhook payload carries both, so both spellings are revalidated. */\nexport function entryTag(slugOrId: string): string {\n return `plenary:entry:${slugOrId}`;\n}\n\n/** Next.js extends `RequestInit` with `next`; declared locally so this package\n * compiles against the standard DOM lib. */\ninterface NextRequestInit extends RequestInit {\n next?: { tags?: string[]; revalidate?: number | false };\n}\n\nconst CONTENT_URL = /\\/api\\/v1\\/content\\/([^/?#]+)(?:\\/([^/?#]+))?/;\n\n/**\n * Wraps a fetch implementation so Plenary content reads carry cache tags.\n * Pass it to `createClient`:\n *\n * ```ts\n * const plenary = createClient({ ...config, fetch: createTaggedFetch() });\n * ```\n *\n * GET requests to `/api/v1/content/{model}` are tagged with the model, and\n * `/api/v1/content/{model}/{slugOrId}` with the model and the entry. Anything\n * else passes through untouched.\n */\nexport function createTaggedFetch(baseFetch: typeof fetch = fetch): typeof fetch {\n return ((input: RequestInfo | URL, init?: RequestInit) => {\n const method = init?.method ?? \"GET\";\n const url =\n typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const match = method === \"GET\" ? CONTENT_URL.exec(url) : null;\n if (!match?.[1]) return baseFetch(input, init);\n\n const tags = [modelTag(decodeURIComponent(match[1]))];\n if (match[2]) tags.push(entryTag(decodeURIComponent(match[2])));\n const next = (init as NextRequestInit | undefined)?.next;\n const merged: NextRequestInit = {\n ...init,\n next: { ...next, tags: [...(next?.tags ?? []), ...tags] },\n };\n return baseFetch(input, merged);\n }) as typeof fetch;\n}\n","/**\n * The revalidation decision, shared by the App Router and Pages Router\n * handlers: which paths (and tags) one webhook payload maps to. Pure — the\n * handlers own the framework calls.\n */\n\nimport {\n isEntryWebhookPayload,\n WEBHOOK_TEST_EVENT,\n type EntryWebhookEvent,\n type EntryWebhookPayload,\n type ParseWebhookOptions,\n type WebhookPayload,\n} from \"@plenary/client\";\nimport { entryTag, modelTag } from \"./tags.js\";\n\n/** Paths for one model: a fixed path, a list, or a function of the payload.\n * A function may return `null`/`undefined` to revalidate nothing. */\nexport type RevalidatePaths =\n | string\n | string[]\n | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);\n\nexport interface RevalidateOptions extends ParseWebhookOptions {\n /**\n * Extra or overriding paths per model key. A mapped model replaces the\n * default URL entirely — include `entryUrl(payload)` (exported) in the\n * returned list to keep it.\n */\n paths?: Record<string, RevalidatePaths>;\n /** Builds the default URL for unmapped models. Defaults to joining the\n * entry's folder `path` and `slug`; return `null` to skip. */\n entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;\n /** Entry events that trigger revalidation. Default: all five — revalidation\n * is idempotent and cheap, so over-firing beats a stale page. */\n events?: EntryWebhookEvent[];\n /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used\n * by `createTaggedFetch`. App Router only. */\n tags?: boolean;\n}\n\n/** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →\n * `/blog/spring-sale`). Entries without a slug (singletons) get no default. */\nexport function entryUrl(payload: EntryWebhookPayload): string | null {\n const { slug, path } = payload.entry;\n if (!slug) return null;\n const dir = path && path !== \"/\" ? path : \"\";\n return `${dir}/${slug}`;\n}\n\nexport interface RevalidationPlan {\n /** Whether this payload is one the handler acts on. */\n relevant: boolean;\n /** True for the console's Test button — report, don't revalidate. */\n test: boolean;\n paths: string[];\n tags: string[];\n}\n\n/** Decides what one payload revalidates. */\nexport function planRevalidation(\n payload: WebhookPayload,\n options: RevalidateOptions,\n): RevalidationPlan {\n const none = { relevant: false, test: false, paths: [], tags: [] };\n if (!isEntryWebhookPayload(payload)) return none;\n\n const test = payload.event === WEBHOOK_TEST_EVENT;\n if (!test && options.events && !options.events.includes(payload.event as EntryWebhookEvent)) {\n return none;\n }\n\n const modelKey = payload.model.key;\n const mapped = modelKey === undefined ? undefined : options.paths?.[modelKey];\n let paths: string | string[] | null | undefined;\n if (mapped === undefined) {\n paths = (options.entryUrl ?? entryUrl)(payload);\n } else {\n paths = typeof mapped === \"function\" ? mapped(payload) : mapped;\n }\n const pathList = paths == null ? [] : typeof paths === \"string\" ? [paths] : paths;\n\n const tags: string[] = [];\n if (options.tags) {\n if (modelKey) tags.push(modelTag(modelKey));\n tags.push(entryTag(payload.entry.id));\n if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));\n }\n\n return { relevant: true, test, paths: [...new Set(pathList)], tags };\n}\n"],"mappings":";AASO,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAIO,SAAS,SAAS,UAA0B;AACjD,SAAO,iBAAiB,QAAQ;AAClC;AAQA,IAAM,cAAc;AAcb,SAAS,kBAAkB,YAA0B,OAAqB;AAC/E,UAAQ,CAAC,OAA0B,SAAuB;AACxD,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,MACJ,OAAO,UAAU,WAAW,QAAQ,iBAAiB,MAAM,MAAM,OAAO,MAAM;AAChF,UAAM,QAAQ,WAAW,QAAQ,YAAY,KAAK,GAAG,IAAI;AACzD,QAAI,CAAC,QAAQ,CAAC,EAAG,QAAO,UAAU,OAAO,IAAI;AAE7C,UAAM,OAAO,CAAC,SAAS,mBAAmB,MAAM,CAAC,CAAC,CAAC,CAAC;AACpD,QAAI,MAAM,CAAC,EAAG,MAAK,KAAK,SAAS,mBAAmB,MAAM,CAAC,CAAC,CAAC,CAAC;AAC9D,UAAM,OAAQ,MAAsC;AACpD,UAAM,SAA0B;AAAA,MAC9B,GAAG;AAAA,MACH,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,GAAI,MAAM,QAAQ,CAAC,GAAI,GAAG,IAAI,EAAE;AAAA,IAC1D;AACA,WAAO,UAAU,OAAO,MAAM;AAAA,EAChC;AACF;;;AClDA;AAAA,EACE;AAAA,EACA;AAAA,OAKK;AA8BA,SAAS,SAAS,SAA6C;AACpE,QAAM,EAAE,MAAM,KAAK,IAAI,QAAQ;AAC/B,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,MAAM,QAAQ,SAAS,MAAM,OAAO;AAC1C,SAAO,GAAG,GAAG,IAAI,IAAI;AACvB;AAYO,SAAS,iBACd,SACA,SACkB;AAClB,QAAM,OAAO,EAAE,UAAU,OAAO,MAAM,OAAO,OAAO,CAAC,GAAG,MAAM,CAAC,EAAE;AACjE,MAAI,CAAC,sBAAsB,OAAO,EAAG,QAAO;AAE5C,QAAM,OAAO,QAAQ,UAAU;AAC/B,MAAI,CAAC,QAAQ,QAAQ,UAAU,CAAC,QAAQ,OAAO,SAAS,QAAQ,KAA0B,GAAG;AAC3F,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,QAAQ,MAAM;AAC/B,QAAM,SAAS,aAAa,SAAY,SAAY,QAAQ,QAAQ,QAAQ;AAC5E,MAAI;AACJ,MAAI,WAAW,QAAW;AACxB,aAAS,QAAQ,YAAY,UAAU,OAAO;AAAA,EAChD,OAAO;AACL,YAAQ,OAAO,WAAW,aAAa,OAAO,OAAO,IAAI;AAAA,EAC3D;AACA,QAAM,WAAW,SAAS,OAAO,CAAC,IAAI,OAAO,UAAU,WAAW,CAAC,KAAK,IAAI;AAE5E,QAAM,OAAiB,CAAC;AACxB,MAAI,QAAQ,MAAM;AAChB,QAAI,SAAU,MAAK,KAAK,SAAS,QAAQ,CAAC;AAC1C,SAAK,KAAK,SAAS,QAAQ,MAAM,EAAE,CAAC;AACpC,QAAI,QAAQ,MAAM,KAAM,MAAK,KAAK,SAAS,QAAQ,MAAM,IAAI,CAAC;AAAA,EAChE;AAEA,SAAO,EAAE,UAAU,MAAM,MAAM,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC,GAAG,KAAK;AACrE;","names":[]}
@@ -1,43 +0,0 @@
1
- import { ParseWebhookOptions, EntryWebhookPayload, EntryWebhookEvent, WebhookPayload } from '@plenary/client';
2
-
3
- /**
4
- * The revalidation decision, shared by the App Router and Pages Router
5
- * handlers: which paths (and tags) one webhook payload maps to. Pure — the
6
- * handlers own the framework calls.
7
- */
8
-
9
- /** Paths for one model: a fixed path, a list, or a function of the payload.
10
- * A function may return `null`/`undefined` to revalidate nothing. */
11
- type RevalidatePaths = string | string[] | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);
12
- interface RevalidateOptions extends ParseWebhookOptions {
13
- /**
14
- * Extra or overriding paths per model key. A mapped model replaces the
15
- * default URL entirely — include `entryUrl(payload)` (exported) in the
16
- * returned list to keep it.
17
- */
18
- paths?: Record<string, RevalidatePaths>;
19
- /** Builds the default URL for unmapped models. Defaults to joining the
20
- * entry's folder `path` and `slug`; return `null` to skip. */
21
- entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;
22
- /** Entry events that trigger revalidation. Default: all five — revalidation
23
- * is idempotent and cheap, so over-firing beats a stale page. */
24
- events?: EntryWebhookEvent[];
25
- /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used
26
- * by `createTaggedFetch`. App Router only. */
27
- tags?: boolean;
28
- }
29
- /** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →
30
- * `/blog/spring-sale`). Entries without a slug (singletons) get no default. */
31
- declare function entryUrl(payload: EntryWebhookPayload): string | null;
32
- interface RevalidationPlan {
33
- /** Whether this payload is one the handler acts on. */
34
- relevant: boolean;
35
- /** True for the console's Test button — report, don't revalidate. */
36
- test: boolean;
37
- paths: string[];
38
- tags: string[];
39
- }
40
- /** Decides what one payload revalidates. */
41
- declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
42
-
43
- export { type RevalidateOptions as R, type RevalidatePaths as a, type RevalidationPlan as b, entryUrl as e, planRevalidation as p };
@@ -1,43 +0,0 @@
1
- import { ParseWebhookOptions, EntryWebhookPayload, EntryWebhookEvent, WebhookPayload } from '@plenary/client';
2
-
3
- /**
4
- * The revalidation decision, shared by the App Router and Pages Router
5
- * handlers: which paths (and tags) one webhook payload maps to. Pure — the
6
- * handlers own the framework calls.
7
- */
8
-
9
- /** Paths for one model: a fixed path, a list, or a function of the payload.
10
- * A function may return `null`/`undefined` to revalidate nothing. */
11
- type RevalidatePaths = string | string[] | ((payload: EntryWebhookPayload) => string | string[] | null | undefined);
12
- interface RevalidateOptions extends ParseWebhookOptions {
13
- /**
14
- * Extra or overriding paths per model key. A mapped model replaces the
15
- * default URL entirely — include `entryUrl(payload)` (exported) in the
16
- * returned list to keep it.
17
- */
18
- paths?: Record<string, RevalidatePaths>;
19
- /** Builds the default URL for unmapped models. Defaults to joining the
20
- * entry's folder `path` and `slug`; return `null` to skip. */
21
- entryUrl?: (payload: EntryWebhookPayload) => string | null | undefined;
22
- /** Entry events that trigger revalidation. Default: all five — revalidation
23
- * is idempotent and cheap, so over-firing beats a stale page. */
24
- events?: EntryWebhookEvent[];
25
- /** Also revalidate the `plenary:model:*` / `plenary:entry:*` cache tags used
26
- * by `createTaggedFetch`. App Router only. */
27
- tags?: boolean;
28
- }
29
- /** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →
30
- * `/blog/spring-sale`). Entries without a slug (singletons) get no default. */
31
- declare function entryUrl(payload: EntryWebhookPayload): string | null;
32
- interface RevalidationPlan {
33
- /** Whether this payload is one the handler acts on. */
34
- relevant: boolean;
35
- /** True for the console's Test button — report, don't revalidate. */
36
- test: boolean;
37
- paths: string[];
38
- tags: string[];
39
- }
40
- /** Decides what one payload revalidates. */
41
- declare function planRevalidation(payload: WebhookPayload, options: RevalidateOptions): RevalidationPlan;
42
-
43
- export { type RevalidateOptions as R, type RevalidatePaths as a, type RevalidationPlan as b, entryUrl as e, planRevalidation as p };