@plenary/next 1.0.0 → 1.0.2

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.
@@ -5,15 +5,26 @@ function modelTag(modelKey) {
5
5
  function entryTag(slugOrId) {
6
6
  return `plenary:entry:${slugOrId}`;
7
7
  }
8
+ function modelEntriesTag(modelKey) {
9
+ return `plenary:model-entries:${modelKey}`;
10
+ }
8
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
+ }
9
19
  function createTaggedFetch(baseFetch = fetch) {
10
20
  return ((input, init) => {
11
21
  const method = init?.method ?? "GET";
12
22
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
13
23
  const match = method === "GET" ? CONTENT_URL.exec(url) : null;
14
24
  if (!match?.[1]) return baseFetch(input, init);
15
- const tags = [modelTag(decodeURIComponent(match[1]))];
16
- if (match[2]) tags.push(entryTag(decodeURIComponent(match[2])));
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)];
17
28
  const next = init?.next;
18
29
  const merged = {
19
30
  ...init,
@@ -32,6 +43,7 @@ function entryUrl(payload) {
32
43
  const { slug, path } = payload.entry;
33
44
  if (!slug) return null;
34
45
  const dir = path && path !== "/" ? path : "";
46
+ if (!dir && slug === "home") return "/";
35
47
  return `${dir}/${slug}`;
36
48
  }
37
49
  function planRevalidation(payload, options) {
@@ -52,7 +64,10 @@ function planRevalidation(payload, options) {
52
64
  const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
53
65
  const tags = [];
54
66
  if (options.tags) {
55
- if (modelKey) tags.push(modelTag(modelKey));
67
+ if (modelKey) {
68
+ tags.push(modelTag(modelKey));
69
+ if (payload.changed.some((p) => p === "slug" || p === "path")) tags.push(modelEntriesTag(modelKey));
70
+ }
56
71
  tags.push(entryTag(payload.entry.id));
57
72
  if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
58
73
  }
@@ -62,8 +77,9 @@ function planRevalidation(payload, options) {
62
77
  export {
63
78
  modelTag,
64
79
  entryTag,
80
+ modelEntriesTag,
65
81
  createTaggedFetch,
66
82
  entryUrl,
67
83
  planRevalidation
68
84
  };
69
- //# sourceMappingURL=chunk-BMLKEIKX.js.map
85
+ //# sourceMappingURL=chunk-J5OXC4XR.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. 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, modelEntriesTag, 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`). A root-folder entry slugged `home` is the site's home\n * page, `/`. 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 if (!dir && slug === \"home\") return \"/\";\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) {\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\n return { relevant: true, test, paths: [...new Set(pathList)], tags };\n}\n"],"mappings":"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","names":[]}
package/dist/index.cjs CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  createTaggedFetch: () => createTaggedFetch,
25
25
  entryTag: () => entryTag,
26
26
  entryUrl: () => entryUrl,
27
+ modelEntriesTag: () => modelEntriesTag,
27
28
  modelTag: () => modelTag,
28
29
  planRevalidation: () => planRevalidation
29
30
  });
@@ -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,
@@ -66,6 +78,7 @@ function entryUrl(payload) {
66
78
  const { slug, path } = payload.entry;
67
79
  if (!slug) return null;
68
80
  const dir = path && path !== "/" ? path : "";
81
+ if (!dir && slug === "home") return "/";
69
82
  return `${dir}/${slug}`;
70
83
  }
71
84
  function planRevalidation(payload, options) {
@@ -86,7 +99,10 @@ function planRevalidation(payload, options) {
86
99
  const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
87
100
  const tags = [];
88
101
  if (options.tags) {
89
- if (modelKey) tags.push(modelTag(modelKey));
102
+ if (modelKey) {
103
+ tags.push(modelTag(modelKey));
104
+ if (payload.changed.some((p) => p === "slug" || p === "path")) tags.push(modelEntriesTag(modelKey));
105
+ }
90
106
  tags.push(entryTag(payload.entry.id));
91
107
  if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
92
108
  }
@@ -123,6 +139,7 @@ function createRevalidateHandler(options) {
123
139
  createTaggedFetch,
124
140
  entryTag,
125
141
  entryUrl,
142
+ modelEntriesTag,
126
143
  modelTag,
127
144
  planRevalidation
128
145
  });
@@ -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":"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","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. 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, modelEntriesTag, 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`). A root-folder entry slugged `home` is the site's home\n * page, `/`. 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 if (!dir && slug === \"home\") return \"/\";\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) {\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\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 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"]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
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';
1
+ import { R as RevalidateOptions } from './resolve-RT8_HYfz.cjs';
2
+ export { a as RevalidatePaths, b as RevalidationPlan, e as entryUrl, p as planRevalidation } from './resolve-RT8_HYfz.cjs';
3
3
  import '@plenary/client';
4
4
 
5
5
  /**
@@ -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,5 +1,5 @@
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';
1
+ import { R as RevalidateOptions } from './resolve-RT8_HYfz.js';
2
+ export { a as RevalidatePaths, b as RevalidationPlan, e as entryUrl, p as planRevalidation } from './resolve-RT8_HYfz.js';
3
3
  import '@plenary/client';
4
4
 
5
5
  /**
@@ -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-J5OXC4XR.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
@@ -35,12 +35,16 @@ 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
43
  function entryUrl(payload) {
41
44
  const { slug, path } = payload.entry;
42
45
  if (!slug) return null;
43
46
  const dir = path && path !== "/" ? path : "";
47
+ if (!dir && slug === "home") return "/";
44
48
  return `${dir}/${slug}`;
45
49
  }
46
50
  function planRevalidation(payload, options) {
@@ -61,7 +65,10 @@ function planRevalidation(payload, options) {
61
65
  const pathList = paths == null ? [] : typeof paths === "string" ? [paths] : paths;
62
66
  const tags = [];
63
67
  if (options.tags) {
64
- if (modelKey) tags.push(modelTag(modelKey));
68
+ if (modelKey) {
69
+ tags.push(modelTag(modelKey));
70
+ if (payload.changed.some((p) => p === "slug" || p === "path")) tags.push(modelEntriesTag(modelKey));
71
+ }
65
72
  tags.push(entryTag(payload.entry.id));
66
73
  if (payload.entry.slug) tags.push(entryTag(payload.entry.slug));
67
74
  }
@@ -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. 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, modelEntriesTag, 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`). A root-folder entry slugged `home` is the site's home\n * page, `/`. 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 if (!dir && slug === \"home\") return \"/\";\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) {\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\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 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"]}
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-RT8_HYfz.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-RT8_HYfz.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-J5OXC4XR.js";
4
4
 
5
5
  // src/pages.ts
6
6
  import {
@@ -27,7 +27,8 @@ interface RevalidateOptions extends ParseWebhookOptions {
27
27
  tags?: boolean;
28
28
  }
29
29
  /** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →
30
- * `/blog/spring-sale`). Entries without a slug (singletons) get no default. */
30
+ * `/blog/spring-sale`). A root-folder entry slugged `home` is the site's home
31
+ * page, `/`. Entries without a slug (singletons) get no default. */
31
32
  declare function entryUrl(payload: EntryWebhookPayload): string | null;
32
33
  interface RevalidationPlan {
33
34
  /** Whether this payload is one the handler acts on. */
@@ -27,7 +27,8 @@ interface RevalidateOptions extends ParseWebhookOptions {
27
27
  tags?: boolean;
28
28
  }
29
29
  /** Default URL for an entry: folder path + slug (`/blog` + `spring-sale` →
30
- * `/blog/spring-sale`). Entries without a slug (singletons) get no default. */
30
+ * `/blog/spring-sale`). A root-folder entry slugged `home` is the site's home
31
+ * page, `/`. Entries without a slug (singletons) get no default. */
31
32
  declare function entryUrl(payload: EntryWebhookPayload): string | null;
32
33
  interface RevalidationPlan {
33
34
  /** Whether this payload is one the handler acts on. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plenary/next",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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.0"
26
+ "@plenary/client": "1.0.2"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "next": "^14 || ^15 || ^16"
@@ -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":[]}