@plenary/nuxt 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +105 -2
- package/dist/module.d.ts +19 -0
- package/dist/module.js +17 -0
- package/dist/module.js.map +1 -0
- package/dist/runtime/server/cache.d.ts +33 -0
- package/dist/runtime/server/cache.js +29 -0
- package/dist/runtime/server/cache.js.map +1 -0
- package/dist/runtime/server/index.d.ts +4 -0
- package/dist/runtime/server/index.js +8 -0
- package/dist/runtime/server/index.js.map +1 -0
- package/dist/runtime/server/revalidate.d.ts +57 -0
- package/dist/runtime/server/revalidate.js +35 -0
- package/dist/runtime/server/revalidate.js.map +1 -0
- package/package.json +38 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick DeLoughry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,106 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @plenary/nuxt
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Nuxt glue for Plenary: turn entry webhooks into Nitro cache purges, so pages
|
|
4
|
+
cached with `swr`/`cache` route rules or `defineCachedEventHandler` are
|
|
5
|
+
regenerated the moment content changes.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// nuxt.config.ts
|
|
11
|
+
export default defineNuxtConfig({
|
|
12
|
+
modules: ["@plenary/nuxt"],
|
|
13
|
+
runtimeConfig: {
|
|
14
|
+
plenaryWebhookSecret: "", // NUXT_PLENARY_WEBHOOK_SECRET
|
|
15
|
+
},
|
|
16
|
+
routeRules: {
|
|
17
|
+
"/news/**": { swr: 3600 },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// server/api/revalidate.post.ts
|
|
24
|
+
import { createRevalidateHandler } from "@plenary/nuxt/server";
|
|
25
|
+
|
|
26
|
+
export default createRevalidateHandler({
|
|
27
|
+
secret: useRuntimeConfig().plenaryWebhookSecret,
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The module has no options. It registers the package's server runtime with
|
|
32
|
+
Nitro's bundler so the handler can reach Nitro's cache storage. Without it the
|
|
33
|
+
route answers 500 and the server log says `Package import specifier
|
|
34
|
+
"#nitro-internal-virtual/storage" is not defined`: Nitro's storage only exists
|
|
35
|
+
inside the Nitro bundle.
|
|
36
|
+
|
|
37
|
+
In the Plenary console, add a webhook on each model pointing at
|
|
38
|
+
`/api/revalidate`, subscribed to the entry events, with the secret in an
|
|
39
|
+
`x-plenary-secret` custom header (marked secret). That's the whole setup: every
|
|
40
|
+
event purges the cached responses of the entry's own URL, built from its folder
|
|
41
|
+
path and slug (`/news` + `hello-world` → `/news/hello-world`), and the next
|
|
42
|
+
request renders it fresh.
|
|
43
|
+
|
|
44
|
+
When a model's URLs don't mirror folder + slug, or other pages show its
|
|
45
|
+
content, map it:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
export default createRevalidateHandler({
|
|
49
|
+
secret: useRuntimeConfig().plenaryWebhookSecret,
|
|
50
|
+
paths: {
|
|
51
|
+
"blog-post": ({ entry }) => ["/blog", `/blog/${entry.slug}`],
|
|
52
|
+
"global-settings": "/", // fixed path or list also fine
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A mapped model replaces the default URL; compose with `entryUrl` from
|
|
58
|
+
`@plenary/client` to keep it. Unmapped slugless entries (singletons) revalidate nothing. The
|
|
59
|
+
`events` option narrows which entry events fire (default: all five —
|
|
60
|
+
revalidation is idempotent, so over-firing beats a stale page). The console's
|
|
61
|
+
**Test** button gets a dry run: a 200 reporting the paths it would have
|
|
62
|
+
purged.
|
|
63
|
+
|
|
64
|
+
The handler answers 401/405/400 for requests that will never succeed, so bad
|
|
65
|
+
deliveries fail fast in the delivery log instead of retrying for seven hours.
|
|
66
|
+
A successful delivery reports `purged`, the number of cache entries removed:
|
|
67
|
+
a persistent `0` for a page you know is cached usually means the cache lives
|
|
68
|
+
somewhere the handler can't see (below).
|
|
69
|
+
|
|
70
|
+
## What gets purged
|
|
71
|
+
|
|
72
|
+
Nitro has no cache tags. A cached response is stored under a key derived from
|
|
73
|
+
its request URL (`cache:nitro:routes:_:newshelloworld.<hash>.json` for
|
|
74
|
+
`/news/hello-world`), so the handler lists the `nitro:routes` and
|
|
75
|
+
`nitro:handlers` groups of `useStorage("cache")` and removes every entry filed
|
|
76
|
+
under one of the planned paths — all of a page's variants, including query
|
|
77
|
+
strings and `varies` headers. Nitro keeps only the first 16 word characters of
|
|
78
|
+
the path in the key, so a purge of a long path also removes pages sharing that
|
|
79
|
+
prefix (`/blog/2026/11/03/some-title` and `/blog/2026/11/03/some-other`). That
|
|
80
|
+
is harmless: the next request regenerates them.
|
|
81
|
+
|
|
82
|
+
Things the handler cannot reach:
|
|
83
|
+
|
|
84
|
+
- **Per-instance memory.** Nitro's default `cache` storage in production is
|
|
85
|
+
in-memory, so on a platform that runs several instances the purge only hits
|
|
86
|
+
the one that received the webhook. Mount a shared driver
|
|
87
|
+
(`nitro.storage.cache` → Redis, Cloudflare KV, Vercel KV, …) so every
|
|
88
|
+
instance sees it.
|
|
89
|
+
- **Platform ISR.** The `isr` route rule on Vercel and Netlify caches at the
|
|
90
|
+
platform's CDN, outside Nitro's storage. Use `swr` with a shared cache
|
|
91
|
+
driver instead, or purge through the platform's API.
|
|
92
|
+
- **`defineCachedFunction`.** Its keys are whatever your `getKey` returns, so
|
|
93
|
+
the handler leaves the `nitro:functions` group alone. Give such functions a
|
|
94
|
+
`maxAge` you can live with, or purge them yourself from a custom route built
|
|
95
|
+
on `purgeCachedPaths` (exported here) and `planRevalidation` (from
|
|
96
|
+
`@plenary/client`).
|
|
97
|
+
- **Custom `getKey`** on a cached handler, for the same reason.
|
|
98
|
+
|
|
99
|
+
## Authentication
|
|
100
|
+
|
|
101
|
+
Plenary does not sign webhook requests yet, so authentication is the shared
|
|
102
|
+
secret above (constant-time compared; also accepted as a `?secret=` query
|
|
103
|
+
parameter, names configurable via `secretHeader`/`secretQuery`). Payload
|
|
104
|
+
parsing, verification, and the path planning (`entryUrl`, `planRevalidation`)
|
|
105
|
+
live in `@plenary/client` for use outside Nuxt; HMAC verification will land
|
|
106
|
+
there when signing ships.
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NuxtModule } from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `@plenary/nuxt` module. It has no options: its one job is to hand the
|
|
5
|
+
* package's server runtime to Nitro's bundler, so a route built with
|
|
6
|
+
* `createRevalidateHandler` (from `@plenary/nuxt/server`) can reach Nitro's
|
|
7
|
+
* cache storage. `nitropack/runtime` only exists inside the Nitro bundle —
|
|
8
|
+
* loaded from node_modules at runtime it fails with `Package import specifier
|
|
9
|
+
* "#nitro-internal-virtual/storage" is not defined` — so the runtime
|
|
10
|
+
* directory is transpiled (inlined) rather than left external.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // nuxt.config.ts
|
|
14
|
+
* export default defineNuxtConfig({ modules: ["@plenary/nuxt"] });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
declare const module$1: NuxtModule;
|
|
18
|
+
|
|
19
|
+
export { module$1 as default };
|
package/dist/module.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createResolver, defineNuxtModule } from "@nuxt/kit";
|
|
2
|
+
const module = defineNuxtModule({
|
|
3
|
+
meta: {
|
|
4
|
+
name: "@plenary/nuxt",
|
|
5
|
+
configKey: "plenary",
|
|
6
|
+
compatibility: { nuxt: ">=4.0.0" }
|
|
7
|
+
},
|
|
8
|
+
setup(_options, nuxt) {
|
|
9
|
+
const { resolve } = createResolver(import.meta.url);
|
|
10
|
+
nuxt.options.build.transpile.push(resolve("./runtime"));
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
var module_default = module;
|
|
14
|
+
export {
|
|
15
|
+
module_default as default
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/module.ts"],"sourcesContent":["import { createResolver, defineNuxtModule } from \"@nuxt/kit\";\nimport type { NuxtModule } from \"@nuxt/schema\";\n\n/**\n * The `@plenary/nuxt` module. It has no options: its one job is to hand the\n * package's server runtime to Nitro's bundler, so a route built with\n * `createRevalidateHandler` (from `@plenary/nuxt/server`) can reach Nitro's\n * cache storage. `nitropack/runtime` only exists inside the Nitro bundle —\n * loaded from node_modules at runtime it fails with `Package import specifier\n * \"#nitro-internal-virtual/storage\" is not defined` — so the runtime\n * directory is transpiled (inlined) rather than left external.\n *\n * ```ts\n * // nuxt.config.ts\n * export default defineNuxtConfig({ modules: [\"@plenary/nuxt\"] });\n * ```\n */\nconst module: NuxtModule = defineNuxtModule({\n meta: {\n name: \"@plenary/nuxt\",\n configKey: \"plenary\",\n compatibility: { nuxt: \">=4.0.0\" },\n },\n setup(_options, nuxt) {\n const { resolve } = createResolver(import.meta.url);\n nuxt.options.build.transpile.push(resolve(\"./runtime\"));\n },\n});\n\nexport default module;\n"],"mappings":"AAAA,SAAS,gBAAgB,wBAAwB;AAiBjD,MAAM,SAAqB,iBAAiB;AAAA,EAC1C,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,WAAW;AAAA,IACX,eAAe,EAAE,MAAM,UAAU;AAAA,EACnC;AAAA,EACA,MAAM,UAAU,MAAM;AACpB,UAAM,EAAE,QAAQ,IAAI,eAAe,YAAY,GAAG;AAClD,SAAK,QAAQ,MAAM,UAAU,KAAK,QAAQ,WAAW,CAAC;AAAA,EACxD;AACF,CAAC;AAED,IAAO,iBAAQ;","names":[]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purging Nitro's response cache by path. Nitro has no cache tags: a cached
|
|
3
|
+
* response lives under a storage key derived from the request URL, so
|
|
4
|
+
* revalidating a path means finding and removing the keys that belong to it.
|
|
5
|
+
*
|
|
6
|
+
* `defineCachedEventHandler` (and the `cache`/`swr`/`isr` route rules, which
|
|
7
|
+
* wrap handlers in it) store entries as
|
|
8
|
+
*
|
|
9
|
+
* cache:nitro:routes:<name>:<pathname>.<hash>[:<header>.<hash>…].json
|
|
10
|
+
* cache:nitro:handlers:<name>:<pathname>.<hash>[:<header>.<hash>…].json
|
|
11
|
+
*
|
|
12
|
+
* where `<pathname>` is the URL path with every non-word character stripped
|
|
13
|
+
* and cut to 16 characters (`/news/hello-world` → `newshelloworld`, `/` →
|
|
14
|
+
* `index`) and `<hash>` digests the full URL including its query string. The
|
|
15
|
+
* hash is not recomputed here; entries are matched on the pathname part, so a
|
|
16
|
+
* purge of `/news/hello-world` also removes `/news/hello-world?utm=x` — every
|
|
17
|
+
* cached variant of the page — and, for paths whose stripped form exceeds 16
|
|
18
|
+
* characters, any other page sharing that prefix. Over-purging is harmless:
|
|
19
|
+
* the next request regenerates the page.
|
|
20
|
+
*/
|
|
21
|
+
/** The slice of Nitro's `useStorage("cache")` the purge needs. Structural so
|
|
22
|
+
* the published types don't depend on `unstorage`. */
|
|
23
|
+
interface CacheStorage {
|
|
24
|
+
getKeys(base?: string): Promise<string[]>;
|
|
25
|
+
removeItem(key: string): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** The `<pathname>` part Nitro files a path's cached responses under. */
|
|
28
|
+
declare function cacheKeyPathname(path: string): string;
|
|
29
|
+
/** Removes every cached response filed under one of `paths`. Returns the
|
|
30
|
+
* number of entries removed. */
|
|
31
|
+
declare function purgeCachedPaths(storage: CacheStorage, paths: string[]): Promise<number>;
|
|
32
|
+
|
|
33
|
+
export { type CacheStorage, cacheKeyPathname, purgeCachedPaths };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const PATH_KEYED_GROUPS = ["nitro:routes", "nitro:handlers"];
|
|
2
|
+
function cacheKeyPathname(path) {
|
|
3
|
+
const pathname = path.split(/[?#]/, 1)[0] ?? "";
|
|
4
|
+
let decoded;
|
|
5
|
+
try {
|
|
6
|
+
decoded = decodeURI(pathname);
|
|
7
|
+
} catch {
|
|
8
|
+
return "-";
|
|
9
|
+
}
|
|
10
|
+
return decoded.replace(/\W/g, "").slice(0, 16) || "index";
|
|
11
|
+
}
|
|
12
|
+
async function purgeCachedPaths(storage, paths) {
|
|
13
|
+
if (paths.length === 0) return 0;
|
|
14
|
+
const wanted = new Set(paths.map(cacheKeyPathname));
|
|
15
|
+
const matches = [];
|
|
16
|
+
for (const group of PATH_KEYED_GROUPS) {
|
|
17
|
+
for (const key of await storage.getKeys(group)) {
|
|
18
|
+
const pathname = key.slice(group.length + 1).split(":")[1]?.split(".")[0];
|
|
19
|
+
if (pathname !== void 0 && wanted.has(pathname)) matches.push(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
await Promise.all(matches.map((key) => storage.removeItem(key)));
|
|
23
|
+
return matches.length;
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
cacheKeyPathname,
|
|
27
|
+
purgeCachedPaths
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/runtime/server/cache.ts"],"sourcesContent":["/**\n * Purging Nitro's response cache by path. Nitro has no cache tags: a cached\n * response lives under a storage key derived from the request URL, so\n * revalidating a path means finding and removing the keys that belong to it.\n *\n * `defineCachedEventHandler` (and the `cache`/`swr`/`isr` route rules, which\n * wrap handlers in it) store entries as\n *\n * cache:nitro:routes:<name>:<pathname>.<hash>[:<header>.<hash>…].json\n * cache:nitro:handlers:<name>:<pathname>.<hash>[:<header>.<hash>…].json\n *\n * where `<pathname>` is the URL path with every non-word character stripped\n * and cut to 16 characters (`/news/hello-world` → `newshelloworld`, `/` →\n * `index`) and `<hash>` digests the full URL including its query string. The\n * hash is not recomputed here; entries are matched on the pathname part, so a\n * purge of `/news/hello-world` also removes `/news/hello-world?utm=x` — every\n * cached variant of the page — and, for paths whose stripped form exceeds 16\n * characters, any other page sharing that prefix. Over-purging is harmless:\n * the next request regenerates the page.\n */\n\n/** The slice of Nitro's `useStorage(\"cache\")` the purge needs. Structural so\n * the published types don't depend on `unstorage`. */\nexport interface CacheStorage {\n getKeys(base?: string): Promise<string[]>;\n removeItem(key: string): Promise<void>;\n}\n\n/** Cache groups whose keys follow the request-path convention: route rules\n * and `defineCachedEventHandler`. `nitro:functions` keys are caller-defined\n * and left alone. */\nconst PATH_KEYED_GROUPS = [\"nitro:routes\", \"nitro:handlers\"];\n\n/** The `<pathname>` part Nitro files a path's cached responses under. */\nexport function cacheKeyPathname(path: string): string {\n const pathname = path.split(/[?#]/, 1)[0] ?? \"\";\n let decoded: string;\n try {\n decoded = decodeURI(pathname);\n } catch {\n return \"-\";\n }\n return decoded.replace(/\\W/g, \"\").slice(0, 16) || \"index\";\n}\n\n/** Removes every cached response filed under one of `paths`. Returns the\n * number of entries removed. */\nexport async function purgeCachedPaths(storage: CacheStorage, paths: string[]): Promise<number> {\n if (paths.length === 0) return 0;\n const wanted = new Set(paths.map(cacheKeyPathname));\n const matches: string[] = [];\n for (const group of PATH_KEYED_GROUPS) {\n for (const key of await storage.getKeys(group)) {\n // key = <group>:<name>:<pathname>.<hash>[:…].json\n const pathname = key.slice(group.length + 1).split(\":\")[1]?.split(\".\")[0];\n if (pathname !== undefined && wanted.has(pathname)) matches.push(key);\n }\n }\n await Promise.all(matches.map((key) => storage.removeItem(key)));\n return matches.length;\n}\n"],"mappings":"AA+BA,MAAM,oBAAoB,CAAC,gBAAgB,gBAAgB;AAGpD,SAAS,iBAAiB,MAAsB;AACrD,QAAM,WAAW,KAAK,MAAM,QAAQ,CAAC,EAAE,CAAC,KAAK;AAC7C,MAAI;AACJ,MAAI;AACF,cAAU,UAAU,QAAQ;AAAA,EAC9B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,SAAO,QAAQ,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG,EAAE,KAAK;AACpD;AAIA,eAAsB,iBAAiB,SAAuB,OAAkC;AAC9F,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,SAAS,IAAI,IAAI,MAAM,IAAI,gBAAgB,CAAC;AAClD,QAAM,UAAoB,CAAC;AAC3B,aAAW,SAAS,mBAAmB;AACrC,eAAW,OAAO,MAAM,QAAQ,QAAQ,KAAK,GAAG;AAE9C,YAAM,WAAW,IAAI,MAAM,MAAM,SAAS,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,EAAE,CAAC;AACxE,UAAI,aAAa,UAAa,OAAO,IAAI,QAAQ,EAAG,SAAQ,KAAK,GAAG;AAAA,IACtE;AAAA,EACF;AACA,QAAM,QAAQ,IAAI,QAAQ,IAAI,CAAC,QAAQ,QAAQ,WAAW,GAAG,CAAC,CAAC;AAC/D,SAAO,QAAQ;AACjB;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/runtime/server/index.ts"],"sourcesContent":["/**\n * @plenary/nuxt/server — the server side of the Nuxt integration: a webhook\n * handler that purges Nitro's response cache for the paths an entry event\n * touches. Import it from `server/` routes; the `@plenary/nuxt` module must\n * be enabled for it to reach Nitro's storage.\n *\n * The path planner (`entryUrl`, `planRevalidation`) is `@plenary/client`'s and\n * is imported from there: re-exporting it here would make Nitro's bundler warn\n * about unused external imports on every build of a route that doesn't use it.\n */\n\nexport { createRevalidateHandler } from \"./revalidate.js\";\nexport { purgeCachedPaths, cacheKeyPathname, type CacheStorage } from \"./cache.js\";\nexport type { RevalidateOptions, RevalidatePaths, RevalidationPlan } from \"@plenary/client\";\n"],"mappings":"AAWA,SAAS,+BAA+B;AACxC,SAAS,kBAAkB,wBAA2C;","names":[]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as _plenary_client from '@plenary/client';
|
|
2
|
+
import { RevalidateOptions } from '@plenary/client';
|
|
3
|
+
import * as h3 from 'h3';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a Nitro event handler that turns Plenary entry webhooks into cache
|
|
7
|
+
* purges: every cached response of the entry's paths — `swr`/`cache` route
|
|
8
|
+
* rules and `defineCachedEventHandler` routes — is removed, so the next
|
|
9
|
+
* request renders fresh content.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // server/api/revalidate.post.ts
|
|
13
|
+
* import { createRevalidateHandler } from "@plenary/nuxt/server";
|
|
14
|
+
*
|
|
15
|
+
* export default createRevalidateHandler({
|
|
16
|
+
* secret: useRuntimeConfig().plenaryWebhookSecret,
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* With no `paths` map, each event revalidates the entry's own URL (folder
|
|
21
|
+
* path + slug). Point the webhook at the route, subscribed to the entry
|
|
22
|
+
* events, with the secret in an `x-plenary-secret` header.
|
|
23
|
+
*
|
|
24
|
+
* The console's Test button gets a dry run: a 200 reporting what would have
|
|
25
|
+
* been purged, without touching the cache.
|
|
26
|
+
*/
|
|
27
|
+
declare function createRevalidateHandler(options: RevalidateOptions): h3.EventHandler<h3.EventHandlerRequest, Promise<{
|
|
28
|
+
message: string;
|
|
29
|
+
revalidated?: undefined;
|
|
30
|
+
ignored?: undefined;
|
|
31
|
+
test?: undefined;
|
|
32
|
+
paths?: undefined;
|
|
33
|
+
purged?: undefined;
|
|
34
|
+
} | {
|
|
35
|
+
revalidated: boolean;
|
|
36
|
+
ignored: _plenary_client.EntryWebhookEvent | "webhook.test" | "form.submitted";
|
|
37
|
+
message?: undefined;
|
|
38
|
+
test?: undefined;
|
|
39
|
+
paths?: undefined;
|
|
40
|
+
purged?: undefined;
|
|
41
|
+
} | {
|
|
42
|
+
test: boolean;
|
|
43
|
+
paths: string[];
|
|
44
|
+
message?: undefined;
|
|
45
|
+
revalidated?: undefined;
|
|
46
|
+
ignored?: undefined;
|
|
47
|
+
purged?: undefined;
|
|
48
|
+
} | {
|
|
49
|
+
revalidated: boolean;
|
|
50
|
+
paths: string[];
|
|
51
|
+
purged: number;
|
|
52
|
+
message?: undefined;
|
|
53
|
+
ignored?: undefined;
|
|
54
|
+
test?: undefined;
|
|
55
|
+
}>>;
|
|
56
|
+
|
|
57
|
+
export { createRevalidateHandler };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineEventHandler, setResponseStatus, toWebRequest } from "h3";
|
|
2
|
+
import { useStorage } from "nitropack/runtime";
|
|
3
|
+
import {
|
|
4
|
+
parseWebhookRequest,
|
|
5
|
+
planRevalidation,
|
|
6
|
+
PlenaryWebhookError
|
|
7
|
+
} from "@plenary/client";
|
|
8
|
+
import { purgeCachedPaths } from "./cache.js";
|
|
9
|
+
function createRevalidateHandler(options) {
|
|
10
|
+
return defineEventHandler(async (event) => {
|
|
11
|
+
let payload;
|
|
12
|
+
try {
|
|
13
|
+
payload = await parseWebhookRequest(toWebRequest(event), options);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (err instanceof PlenaryWebhookError) {
|
|
16
|
+
setResponseStatus(event, err.status);
|
|
17
|
+
return { message: err.message };
|
|
18
|
+
}
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
const plan = planRevalidation(payload, options);
|
|
22
|
+
if (!plan.relevant) {
|
|
23
|
+
return { revalidated: false, ignored: payload.event };
|
|
24
|
+
}
|
|
25
|
+
if (plan.test) {
|
|
26
|
+
return { test: true, paths: plan.paths };
|
|
27
|
+
}
|
|
28
|
+
const purged = await purgeCachedPaths(useStorage("cache"), plan.paths);
|
|
29
|
+
return { revalidated: true, paths: plan.paths, purged };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
createRevalidateHandler
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=revalidate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/runtime/server/revalidate.ts"],"sourcesContent":["import { defineEventHandler, setResponseStatus, toWebRequest } from \"h3\";\nimport { useStorage } from \"nitropack/runtime\";\nimport {\n parseWebhookRequest,\n planRevalidation,\n PlenaryWebhookError,\n type RevalidateOptions,\n} from \"@plenary/client\";\nimport { purgeCachedPaths } from \"./cache.js\";\n\n/**\n * Builds a Nitro event handler that turns Plenary entry webhooks into cache\n * purges: every cached response of the entry's paths — `swr`/`cache` route\n * rules and `defineCachedEventHandler` routes — is removed, so the next\n * request renders fresh content.\n *\n * ```ts\n * // server/api/revalidate.post.ts\n * import { createRevalidateHandler } from \"@plenary/nuxt/server\";\n *\n * export default createRevalidateHandler({\n * secret: useRuntimeConfig().plenaryWebhookSecret,\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 purged, without touching the cache.\n */\nexport function createRevalidateHandler(options: RevalidateOptions) {\n return defineEventHandler(async (event) => {\n let payload;\n try {\n payload = await parseWebhookRequest(toWebRequest(event), options);\n } catch (err) {\n if (err instanceof PlenaryWebhookError) {\n setResponseStatus(event, err.status);\n return { message: err.message };\n }\n throw err;\n }\n\n const plan = planRevalidation(payload, options);\n if (!plan.relevant) {\n return { revalidated: false, ignored: payload.event };\n }\n if (plan.test) {\n return { test: true, paths: plan.paths };\n }\n const purged = await purgeCachedPaths(useStorage(\"cache\"), plan.paths);\n return { revalidated: true, paths: plan.paths, purged };\n });\n}\n"],"mappings":"AAAA,SAAS,oBAAoB,mBAAmB,oBAAoB;AACpE,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,wBAAwB;AAwB1B,SAAS,wBAAwB,SAA4B;AAClE,SAAO,mBAAmB,OAAO,UAAU;AACzC,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM,oBAAoB,aAAa,KAAK,GAAG,OAAO;AAAA,IAClE,SAAS,KAAK;AACZ,UAAI,eAAe,qBAAqB;AACtC,0BAAkB,OAAO,IAAI,MAAM;AACnC,eAAO,EAAE,SAAS,IAAI,QAAQ;AAAA,MAChC;AACA,YAAM;AAAA,IACR;AAEA,UAAM,OAAO,iBAAiB,SAAS,OAAO;AAC9C,QAAI,CAAC,KAAK,UAAU;AAClB,aAAO,EAAE,aAAa,OAAO,SAAS,QAAQ,MAAM;AAAA,IACtD;AACA,QAAI,KAAK,MAAM;AACb,aAAO,EAAE,MAAM,MAAM,OAAO,KAAK,MAAM;AAAA,IACzC;AACA,UAAM,SAAS,MAAM,iBAAiB,WAAW,OAAO,GAAG,KAAK,KAAK;AACrE,WAAO,EAAE,aAAa,MAAM,OAAO,KAAK,OAAO,OAAO;AAAA,EACxD,CAAC;AACH;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plenary/nuxt",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Plenary Nuxt integration: a module and server handler that turn entry webhooks into Nitro cache purges.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/module.js",
|
|
8
|
+
"module": "./dist/module.js",
|
|
9
|
+
"types": "./dist/module.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/module.d.ts",
|
|
13
|
+
"import": "./dist/module.js"
|
|
14
|
+
},
|
|
15
|
+
"./server": {
|
|
16
|
+
"types": "./dist/runtime/server/index.d.ts",
|
|
17
|
+
"import": "./dist/runtime/server/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@nuxt/kit": "^4.6.0",
|
|
25
|
+
"h3": "^1.15.0",
|
|
26
|
+
"@plenary/client": "1.1.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@nuxt/schema": "^4.6.0",
|
|
30
|
+
"@types/node": "^26.1.2",
|
|
31
|
+
"nitropack": "^2.13.6",
|
|
32
|
+
"unstorage": "^1.17.5"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
}
|
|
6
41
|
}
|