@tetherto/dev-websites-core 0.3.4 → 0.3.6
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/README.md +77 -28
- package/dist/cache/handler.d.mts +51 -0
- package/dist/cache/handler.d.mts.map +1 -0
- package/dist/cache/handler.mjs +178 -0
- package/dist/cache/handler.mjs.map +1 -0
- package/dist/cache/index.d.mts +170 -0
- package/dist/cache/index.d.mts.map +1 -0
- package/dist/cache/index.mjs +297 -0
- package/dist/cache/index.mjs.map +1 -0
- package/dist/cms/google-oauth.mjs +1 -1
- package/dist/cms/index.d.mts +2 -2
- package/dist/cms/index.mjs +2 -2
- package/dist/cms.mjs +514 -4
- package/dist/cms.mjs.map +1 -1
- package/dist/index.d.mts +113 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index5.d.mts +2 -2
- package/dist/ui/components/Button/Button.d.mts +1 -1
- package/dist/ui/components/Card/Card.d.mts +1 -1
- package/package.json +15 -2
package/README.md
CHANGED
|
@@ -2,22 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Shared CMS, SEO, i18n, utils, UI, and analytics primitives for WDK / QVAC / PEAR / MDK websites.
|
|
4
4
|
|
|
5
|
-
| Import
|
|
6
|
-
|
|
|
7
|
-
| `@tetherto/dev-websites-core/cms`
|
|
8
|
-
| `@tetherto/dev-websites-core/
|
|
9
|
-
| `@tetherto/dev-websites-core/
|
|
10
|
-
| `@tetherto/dev-websites-core/
|
|
11
|
-
| `@tetherto/dev-websites-core/
|
|
12
|
-
| `@tetherto/dev-websites-core/
|
|
13
|
-
| `@tetherto/dev-websites-core/
|
|
14
|
-
| `@tetherto/dev-websites-core/
|
|
15
|
-
| `@tetherto/dev-websites-core/ui
|
|
16
|
-
| `@tetherto/dev-websites-core/ui/
|
|
17
|
-
| `@tetherto/dev-websites-core/ui/
|
|
18
|
-
| `@tetherto/dev-websites-core/ui/
|
|
19
|
-
| `@tetherto/dev-websites-core/ui/theme
|
|
20
|
-
| `@tetherto/dev-websites-core/ui/theme/
|
|
5
|
+
| Import | Contents |
|
|
6
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
7
|
+
| `@tetherto/dev-websites-core/cms` | Payload `buildCmsConfig`, stock collections, access, Lexical, media I/O |
|
|
8
|
+
| `@tetherto/dev-websites-core/cache` | Mongo `cacheHandler` factory plus Payload revalidation helpers |
|
|
9
|
+
| `@tetherto/dev-websites-core/cache/handler` | Default Next `cacheHandler` (reads `MONGODB_URI`) |
|
|
10
|
+
| `@tetherto/dev-websites-core/cms/google-oauth` | Google admin login handlers (`google-auth-library` peer) |
|
|
11
|
+
| `@tetherto/dev-websites-core/seo` | `createMetadataFactory`, `getSiteUrl`, `getOgBackgroundDataUrl` |
|
|
12
|
+
| `@tetherto/dev-websites-core/i18n` | `Languages` / `LanguageLabel` |
|
|
13
|
+
| `@tetherto/dev-websites-core/utils` | `createLogger`, `toPlainValue`, `stripShikiPreBackground` |
|
|
14
|
+
| `@tetherto/dev-websites-core/analytics` | `GtmLoader`, `createTrackedLink` (optional `@next/third-parties` peer) |
|
|
15
|
+
| `@tetherto/dev-websites-core/ui` | UI primitives (`Button`, `Dialog`, `Pagination`, `Tooltip`, …) + `cn` |
|
|
16
|
+
| `@tetherto/dev-websites-core/ui/google-oauth` | Payload `GoogleOAuthButton` (Tailwind classes; source `dist` from Payload CSS) |
|
|
17
|
+
| `@tetherto/dev-websites-core/ui/a11y` | `focusRingClassName`, `VisuallyHidden` |
|
|
18
|
+
| `@tetherto/dev-websites-core/ui/carousel` | `Carousel` (+ optional `embla-carousel-react` peer) |
|
|
19
|
+
| `@tetherto/dev-websites-core/ui/theme.css` | One-shot: tokens + Tailwind v4 `@theme` preset |
|
|
20
|
+
| `@tetherto/dev-websites-core/ui/theme/tokens.css` | CSS variables (HSL channels) |
|
|
21
|
+
| `@tetherto/dev-websites-core/ui/theme/preset.css` | Tailwind v4 `@theme` map |
|
|
22
|
+
| `@tetherto/dev-websites-core/ui/theme/preset` | JS Tailwind preset for `tailwind.config.ts` |
|
|
21
23
|
|
|
22
24
|
### Install
|
|
23
25
|
|
|
@@ -160,7 +162,7 @@ export default {
|
|
|
160
162
|
theme: {
|
|
161
163
|
extend: {
|
|
162
164
|
colors: {
|
|
163
|
-
accent: { DEFAULT: 'hsl(var(--color-text-action) / <alpha-value>)'
|
|
165
|
+
accent: { DEFAULT: 'hsl(var(--color-text-action) / <alpha-value>)' /* … */ },
|
|
164
166
|
// …
|
|
165
167
|
},
|
|
166
168
|
},
|
|
@@ -206,7 +208,7 @@ Registered by `buildCmsConfig` in this order. Disable or extend any of them via
|
|
|
206
208
|
|
|
207
209
|
Changelog stock ships shared fields only; site-specific body / GitHub /
|
|
208
210
|
`relatedBlock` fields go in `overrides.fields`. Revalidation hooks go in
|
|
209
|
-
`overrides.hooks` (
|
|
211
|
+
`overrides.hooks` (from `@tetherto/dev-websites-core/cache`) — hook arrays are **appended**
|
|
210
212
|
to stock hooks, not replaced.
|
|
211
213
|
|
|
212
214
|
Draft collections enable Payload `schedulePublish`. `buildCmsConfig` starts
|
|
@@ -214,6 +216,52 @@ Draft collections enable Payload `schedulePublish`. `buildCmsConfig` starts
|
|
|
214
216
|
process (`next start` / Docker). Override or disable via `payload.jobs`. This
|
|
215
217
|
is not for serverless hosts.
|
|
216
218
|
|
|
219
|
+
### Cache handler
|
|
220
|
+
|
|
221
|
+
`@tetherto/dev-websites-core/cache/handler` backs Next's ISR / route / fetch
|
|
222
|
+
cache with Mongo so every pod of a deployment shares one cache instead of a
|
|
223
|
+
per-instance filesystem. It reads `MONGODB_URI`, keeps one client (reused
|
|
224
|
+
across dev reloads), and exports the class Next constructs with `new Handler(ctx)`.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
// next.config.ts
|
|
228
|
+
cacheHandler: require.resolve('@tetherto/dev-websites-core/cache/handler'),
|
|
229
|
+
cacheMaxMemorySize: 0,
|
|
230
|
+
outputFileTracingIncludes: {
|
|
231
|
+
'/*': ['./node_modules/@tetherto/dev-websites-core/dist/cache/**/*'],
|
|
232
|
+
},
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`createMongoCacheHandler({ clientPromise })` is the same class for a site that
|
|
236
|
+
already owns a client. Any object matching `MongoCacheClient` works (a real
|
|
237
|
+
`MongoClient` is assignable).
|
|
238
|
+
|
|
239
|
+
`/cache` also exports the Payload revalidation helpers
|
|
240
|
+
(`safeRevalidatePath`, `safeRevalidateTag`, `shouldSkipRevalidate`,
|
|
241
|
+
`createPublishedSlugRevalidateHooks`, `createAlwaysRevalidateHooks`,
|
|
242
|
+
`createBlogRevalidators`, `createChangelogRevalidators`). Import those from
|
|
243
|
+
`@tetherto/dev-websites-core/cache` — not from the handler entry, which
|
|
244
|
+
connects to Mongo on load.
|
|
245
|
+
|
|
246
|
+
Defaults: database `next_cache`, collection `fetch_cache`, 7-day TTL refreshed
|
|
247
|
+
on every write, page keys scoped by `.next/BUILD_ID`. Override with `dbName`,
|
|
248
|
+
`collectionName`, `entryTtlSeconds`, `buildId`.
|
|
249
|
+
|
|
250
|
+
Behaviour worth knowing:
|
|
251
|
+
|
|
252
|
+
- `get()` returns rows of any age so Next can serve stale while it revalidates.
|
|
253
|
+
Expiry is Next's decision, not the handler's.
|
|
254
|
+
- Page and route keys are prefixed with the build id, because rendered HTML
|
|
255
|
+
embeds chunk filenames that 404 after a deploy. Fetch keys are content
|
|
256
|
+
hashes and stay shared across builds.
|
|
257
|
+
- Non-2xx page/route responses are never written, and poisoned rows from
|
|
258
|
+
earlier writes are deleted on read (`shouldSkipCacheWrite` is exported).
|
|
259
|
+
Without this, a scanner hitting unknown ISR slugs rewrites a 404 row on
|
|
260
|
+
every request.
|
|
261
|
+
- Page tags fall back to the `x-next-cache-tags` header, since Next only passes
|
|
262
|
+
`ctx.tags` for fetch entries — without it `revalidateTag` cannot purge
|
|
263
|
+
rendered HTML.
|
|
264
|
+
|
|
217
265
|
### Peer dependencies
|
|
218
266
|
|
|
219
267
|
CMS / Next (required for `/cms` consumers):
|
|
@@ -230,17 +278,18 @@ UI (required for `/ui` consumers):
|
|
|
230
278
|
Read from `process.env` at runtime by the package — **set them in the consuming
|
|
231
279
|
site's environment** (`.env` / hosting / CI); `@tetherto/dev-websites-core` never defines them.
|
|
232
280
|
|
|
233
|
-
| Variable | Used by | Notes
|
|
234
|
-
| --------------------------------------------- | ------------------------------------------------- |
|
|
235
|
-
| `AWS_S3_BUCKET` | `media`, `createS3MediaPlugin`, `readMediaBuffer` | When set, media reads/writes go to S3; otherwise disk (`<cwd>/media`).
|
|
236
|
-
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | S3 access | Optional; falls back to the default AWS credential chain.
|
|
237
|
-
| `AWS_S3_REGION` | S3 access | Defaults to `us-east-1`.
|
|
281
|
+
| Variable | Used by | Notes |
|
|
282
|
+
| --------------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
283
|
+
| `AWS_S3_BUCKET` | `media`, `createS3MediaPlugin`, `readMediaBuffer` | When set, media reads/writes go to S3; otherwise disk (`<cwd>/media`). |
|
|
284
|
+
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | S3 access | Optional; falls back to the default AWS credential chain. |
|
|
285
|
+
| `AWS_S3_REGION` | S3 access | Defaults to `us-east-1`. |
|
|
238
286
|
| `AWS_S3_ENDPOINT` | S3 access | Optional custom endpoint (S3-compatible stores on QVAC / PEAR). Omit on WDK for default AWS S3. |
|
|
239
|
-
| `AWS_S3_FORCE_PATH_STYLE` | S3 access | Set to `true` when the custom endpoint requires path-style addressing.
|
|
240
|
-
| `NEXT_PUBLIC_SITE_URL` | `getSiteUrl` | Public origin; trailing slash trimmed. Warns in production if this and `VERCEL_URL` are unset.
|
|
241
|
-
| `VERCEL_URL` | `getSiteUrl` | Fallback origin on Vercel when `NEXT_PUBLIC_SITE_URL` is unset.
|
|
242
|
-
| `LOG_LEVEL` | `createLogger` | `debug` \| `info` \| `warn` \| `error`. Defaults to `info` in production, `debug` otherwise.
|
|
243
|
-
| `
|
|
287
|
+
| `AWS_S3_FORCE_PATH_STYLE` | S3 access | Set to `true` when the custom endpoint requires path-style addressing. |
|
|
288
|
+
| `NEXT_PUBLIC_SITE_URL` | `getSiteUrl` | Public origin; trailing slash trimmed. Warns in production if this and `VERCEL_URL` are unset. |
|
|
289
|
+
| `VERCEL_URL` | `getSiteUrl` | Fallback origin on Vercel when `NEXT_PUBLIC_SITE_URL` is unset. |
|
|
290
|
+
| `LOG_LEVEL` | `createLogger` | `debug` \| `info` \| `warn` \| `error`. Defaults to `info` in production, `debug` otherwise. |
|
|
291
|
+
| `MONGODB_URI` | `cache/handler` | Required when the default cache handler is loaded. |
|
|
292
|
+
| `NEXT_PHASE` | `requireEnvAtRuntime` | Build phases skip the required-env check (no DB connection). |
|
|
244
293
|
|
|
245
294
|
Hard-required vars (e.g. `PAYLOAD_SECRET`, `MONGODB_URI`) should be read via
|
|
246
295
|
`requireEnvAtRuntime(name)` — it logs and throws at runtime when the var is
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/cache/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A cache entry as Next hands it to `set()`. Only the fields this handler reads
|
|
4
|
+
* are named; `kind` is `'APP_PAGE'`, `'APP_ROUTE'`, `'PAGES'`, `'FETCH'`, or
|
|
5
|
+
* `'IMAGE'`, and the rest of the payload is stored verbatim.
|
|
6
|
+
*/
|
|
7
|
+
type CachedValue = {
|
|
8
|
+
kind?: string;
|
|
9
|
+
status?: number;
|
|
10
|
+
html?: string;
|
|
11
|
+
headers?: Record<string, string | undefined>;
|
|
12
|
+
/** Stored as a plain object; Next iterates it as a Map, so `get()` rebuilds one. */
|
|
13
|
+
segmentData?: Record<string, unknown> | Map<string, unknown>;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
type CachedEntry = {
|
|
17
|
+
lastModified?: number;
|
|
18
|
+
value: CachedValue | null;
|
|
19
|
+
};
|
|
20
|
+
/** The `ctx` Next passes to `set()`; only `tags` is meaningful here. */
|
|
21
|
+
type CacheSetContext = {
|
|
22
|
+
tags?: string[];
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/cache/createMongoCacheHandler.d.ts
|
|
26
|
+
type MongoCacheHandler = {
|
|
27
|
+
get(key: string): Promise<CachedEntry | null>;
|
|
28
|
+
set(key: string, value: CachedValue | null, ctx?: CacheSetContext): Promise<void>;
|
|
29
|
+
delete(key: string): Promise<void>;
|
|
30
|
+
resetRequestCache(): void;
|
|
31
|
+
revalidateTag(tags: string | string[], durations?: unknown): Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Next instantiates the configured handler with `new Handler(ctx)`, so the
|
|
35
|
+
* factory returns a class rather than an instance.
|
|
36
|
+
*/
|
|
37
|
+
type MongoCacheHandlerConstructor = new (ctx?: unknown) => MongoCacheHandler;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/cache/handler.d.ts
|
|
40
|
+
/**
|
|
41
|
+
* Default Next.js `cacheHandler`. Point `cacheHandler` in `next.config` at
|
|
42
|
+
* `@tetherto/dev-websites-core/cache/handler`. Reads `MONGODB_URI` itself so
|
|
43
|
+
* sites do not ship their own handler or Mongo client.
|
|
44
|
+
*
|
|
45
|
+
* Kept off the `/cache` barrel: importing the revalidation helpers must not
|
|
46
|
+
* open a database connection.
|
|
47
|
+
*/
|
|
48
|
+
declare const _default: MongoCacheHandlerConstructor;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { _default as default };
|
|
51
|
+
//# sourceMappingURL=handler.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.mts","names":[],"sources":["../../src/cache/types.ts","../../src/cache/createMongoCacheHandler.ts","../../src/cache/handler.ts"],"mappings":";;;;;;KAgCY;EACV;EACA;EACA;EACA,UAAU;;EAEV,cAAc,0BAA0B;GACvC;;KAWS;EACV;EACA,OAAO;;;KAIG;EACV;;;;KCtBU;EACV,IAAI,cAAc,QAAQ;EAC1B,IAAI,aAAa,OAAO,oBAAoB,MAAM,kBAAkB;EACpE,OAAO,cAAc;EACrB;EACA,cAAc,yBAAyB,sBAAsB;;;;;;KAOnD,oCAAoC,kBAAkB"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MongoClient } from "mongodb";
|
|
4
|
+
//#region src/cache/shouldSkipCacheWrite.ts
|
|
5
|
+
/**
|
|
6
|
+
* ISR pages with `dynamicParams = true` still render for unknown paths, call
|
|
7
|
+
* `notFound()`, and Next then asks the handler to persist the 404. Scanners
|
|
8
|
+
* hitting junk URLs rewrite that row on every request and bloat the cache
|
|
9
|
+
* collection and the Mongo oplog, so non-success page/route responses are
|
|
10
|
+
* never stored.
|
|
11
|
+
*
|
|
12
|
+
* `FETCH` entries are unaffected: Next nests their upstream status under
|
|
13
|
+
* `value.data.status`, not the top-level `status` read here.
|
|
14
|
+
*/
|
|
15
|
+
function shouldSkipCacheWrite(value) {
|
|
16
|
+
if (!value || typeof value !== "object") return false;
|
|
17
|
+
const status = value.status;
|
|
18
|
+
if (typeof status === "number" && (status < 200 || status >= 400)) return true;
|
|
19
|
+
if (typeof value.html === "string" && value.html.includes("id=\"__next_error__\"")) return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/cache/createMongoCacheHandler.ts
|
|
24
|
+
const DEFAULT_DB_NAME = "next_cache";
|
|
25
|
+
const DEFAULT_COLLECTION_NAME = "fetch_cache";
|
|
26
|
+
/**
|
|
27
|
+
* `get()` deliberately returns rows of any age so Next can serve them stale
|
|
28
|
+
* while it revalidates, which means nothing expires on its own. Mongo drops a
|
|
29
|
+
* row this long after its last write; anything still in use is rewritten by
|
|
30
|
+
* revalidation far sooner, so only paths nobody requests anymore fall out.
|
|
31
|
+
*/
|
|
32
|
+
const DEFAULT_ENTRY_TTL_SECONDS = 604800;
|
|
33
|
+
function readBuildId() {
|
|
34
|
+
try {
|
|
35
|
+
return readFileSync(path.join(process.cwd(), ".next", "BUILD_ID"), "utf8").trim();
|
|
36
|
+
} catch {
|
|
37
|
+
return "development";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Next only passes `ctx.tags` for fetch entries. Page and route entries carry
|
|
42
|
+
* theirs — including the `_N_T_<path>` tag behind `revalidatePath` — in the
|
|
43
|
+
* response headers, so without this fallback `revalidateTag` can never purge
|
|
44
|
+
* rendered HTML and CMS edits wait out the route's full revalidate window.
|
|
45
|
+
*/
|
|
46
|
+
function resolveTags(value, ctx) {
|
|
47
|
+
if (ctx?.tags?.length) return ctx.tags;
|
|
48
|
+
const header = value?.headers?.["x-next-cache-tags"];
|
|
49
|
+
return typeof header === "string" ? header.split(",").filter(Boolean) : [];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Mongo-backed `cacheHandler` for Next's ISR / route / fetch cache, so every
|
|
53
|
+
* instance of a deployment shares one cache instead of a per-pod filesystem.
|
|
54
|
+
*
|
|
55
|
+
* Sites normally point `cacheHandler` at `@tetherto/dev-websites-core/cache/handler`,
|
|
56
|
+
* which builds the client from `MONGODB_URI`. Use this factory when the site
|
|
57
|
+
* already owns a client and wants to pass it in.
|
|
58
|
+
*
|
|
59
|
+
* Wire it with `cacheMaxMemorySize: 0`, and include
|
|
60
|
+
* `node_modules/@tetherto/dev-websites-core/dist/cache` in
|
|
61
|
+
* `outputFileTracingIncludes` so the standalone server can load the handler.
|
|
62
|
+
*/
|
|
63
|
+
function createMongoCacheHandler(options) {
|
|
64
|
+
const { clientPromise, dbName = DEFAULT_DB_NAME, collectionName = DEFAULT_COLLECTION_NAME, entryTtlSeconds = DEFAULT_ENTRY_TTL_SECONDS } = options;
|
|
65
|
+
/**
|
|
66
|
+
* Rendered HTML embeds the chunk filenames of the build that produced it, so
|
|
67
|
+
* an entry left by a previous build loads but never hydrates — its chunks are
|
|
68
|
+
* gone (404). Page keys are therefore scoped per build, while instances of
|
|
69
|
+
* the same build keep sharing them.
|
|
70
|
+
*
|
|
71
|
+
* During `next build` `.next/BUILD_ID` does not exist yet, which is safe
|
|
72
|
+
* because Next writes prerenders to disk and never calls `set()` for page
|
|
73
|
+
* entries during the build.
|
|
74
|
+
*/
|
|
75
|
+
const buildId = options.buildId ?? readBuildId();
|
|
76
|
+
/**
|
|
77
|
+
* Page and route entries are keyed by pathname; fetch entries are keyed by a
|
|
78
|
+
* content hash and stay shared across builds, so upstream data survives a
|
|
79
|
+
* deploy.
|
|
80
|
+
*/
|
|
81
|
+
const scopeKey = (key) => key.startsWith("/") ? `${buildId}${key}` : key;
|
|
82
|
+
let indexPromise;
|
|
83
|
+
const getCollection = async () => {
|
|
84
|
+
const collection = (await clientPromise).db(dbName).collection(collectionName);
|
|
85
|
+
indexPromise ??= Promise.all([
|
|
86
|
+
collection.createIndex({ key: 1 }, { unique: true }),
|
|
87
|
+
collection.createIndex({ tags: 1 }),
|
|
88
|
+
collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 })
|
|
89
|
+
]).catch(() => {});
|
|
90
|
+
await indexPromise;
|
|
91
|
+
return collection;
|
|
92
|
+
};
|
|
93
|
+
return class MongoCacheHandlerImpl {
|
|
94
|
+
/**
|
|
95
|
+
* Returns the entry whatever its age. Next compares `lastModified` against
|
|
96
|
+
* the route's own revalidate window and decides itself whether to serve it
|
|
97
|
+
* as fresh, serve it stale while refreshing in the background, or
|
|
98
|
+
* re-render. Dropping stale rows here turns every expiry into a blocking
|
|
99
|
+
* render.
|
|
100
|
+
*/
|
|
101
|
+
async get(key) {
|
|
102
|
+
const collection = await getCollection();
|
|
103
|
+
const scoped = scopeKey(key);
|
|
104
|
+
const entry = await collection.findOne({ key: scoped });
|
|
105
|
+
if (!entry) return null;
|
|
106
|
+
if (shouldSkipCacheWrite(entry.value)) {
|
|
107
|
+
await collection.deleteOne({ key: scoped });
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const value = entry.value ?? null;
|
|
111
|
+
const segmentData = value?.segmentData;
|
|
112
|
+
return {
|
|
113
|
+
lastModified: entry.lastModified,
|
|
114
|
+
value: segmentData && !(segmentData instanceof Map) ? {
|
|
115
|
+
...value,
|
|
116
|
+
segmentData: new Map(Object.entries(segmentData))
|
|
117
|
+
} : value
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async set(key, value, ctx) {
|
|
121
|
+
if (shouldSkipCacheWrite(value)) return;
|
|
122
|
+
const collection = await getCollection();
|
|
123
|
+
const scoped = scopeKey(key);
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
await collection.updateOne({ key: scoped }, { $set: {
|
|
126
|
+
key: scoped,
|
|
127
|
+
value,
|
|
128
|
+
lastModified: now,
|
|
129
|
+
expiresAt: new Date(now + entryTtlSeconds * 1e3),
|
|
130
|
+
tags: resolveTags(value, ctx)
|
|
131
|
+
} }, { upsert: true });
|
|
132
|
+
}
|
|
133
|
+
async delete(key) {
|
|
134
|
+
await (await getCollection()).deleteOne({ key: scopeKey(key) });
|
|
135
|
+
}
|
|
136
|
+
resetRequestCache() {}
|
|
137
|
+
async revalidateTag(tags, _durations) {
|
|
138
|
+
const list = typeof tags === "string" ? [tags] : tags;
|
|
139
|
+
if (!list?.length) return;
|
|
140
|
+
await (await getCollection()).deleteMany({ tags: { $in: list } });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/cache/createMongoClient.ts
|
|
146
|
+
const options = { promoteBuffers: true };
|
|
147
|
+
/**
|
|
148
|
+
* One Mongo client for the cache handler. In development the promise lives on
|
|
149
|
+
* `globalThis` so hot reload does not open a new connection on every compile.
|
|
150
|
+
*
|
|
151
|
+
* `promoteBuffers` makes BSON Binary come back as Node `Buffer`, which is what
|
|
152
|
+
* Next stores for route and image cache entries.
|
|
153
|
+
*/
|
|
154
|
+
function createMongoClientPromise() {
|
|
155
|
+
const uri = process.env.MONGODB_URI;
|
|
156
|
+
if (!uri) throw new Error("Missing MONGODB_URI");
|
|
157
|
+
if (process.env.NODE_ENV === "development") {
|
|
158
|
+
const globalForMongo = globalThis;
|
|
159
|
+
globalForMongo._mongoClientPromise ??= new MongoClient(uri, options).connect();
|
|
160
|
+
return globalForMongo._mongoClientPromise;
|
|
161
|
+
}
|
|
162
|
+
return new MongoClient(uri, options).connect();
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/cache/handler.ts
|
|
166
|
+
/**
|
|
167
|
+
* Default Next.js `cacheHandler`. Point `cacheHandler` in `next.config` at
|
|
168
|
+
* `@tetherto/dev-websites-core/cache/handler`. Reads `MONGODB_URI` itself so
|
|
169
|
+
* sites do not ship their own handler or Mongo client.
|
|
170
|
+
*
|
|
171
|
+
* Kept off the `/cache` barrel: importing the revalidation helpers must not
|
|
172
|
+
* open a database connection.
|
|
173
|
+
*/
|
|
174
|
+
var handler_default = createMongoCacheHandler({ clientPromise: createMongoClientPromise() });
|
|
175
|
+
//#endregion
|
|
176
|
+
export { handler_default as default };
|
|
177
|
+
|
|
178
|
+
//# sourceMappingURL=handler.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.mjs","names":[],"sources":["../../src/cache/shouldSkipCacheWrite.ts","../../src/cache/createMongoCacheHandler.ts","../../src/cache/createMongoClient.ts","../../src/cache/handler.ts"],"sourcesContent":["import type { CachedValue } from './types.js'\n\n/**\n * ISR pages with `dynamicParams = true` still render for unknown paths, call\n * `notFound()`, and Next then asks the handler to persist the 404. Scanners\n * hitting junk URLs rewrite that row on every request and bloat the cache\n * collection and the Mongo oplog, so non-success page/route responses are\n * never stored.\n *\n * `FETCH` entries are unaffected: Next nests their upstream status under\n * `value.data.status`, not the top-level `status` read here.\n */\nexport function shouldSkipCacheWrite(value: CachedValue | null | undefined): boolean {\n if (!value || typeof value !== 'object') return false\n\n const status = value.status\n if (typeof status === 'number' && (status < 200 || status >= 400)) return true\n\n if (typeof value.html === 'string' && value.html.includes('id=\"__next_error__\"')) {\n return true\n }\n\n return false\n}\n","import { readFileSync } from 'node:fs'\nimport path from 'node:path'\n\nimport { shouldSkipCacheWrite } from './shouldSkipCacheWrite.js'\n\nimport type {\n CacheSetContext,\n CachedEntry,\n CachedValue,\n MongoCacheClient,\n MongoCacheCollection,\n} from './types.js'\n\nconst DEFAULT_DB_NAME = 'next_cache'\nconst DEFAULT_COLLECTION_NAME = 'fetch_cache'\n\n/**\n * `get()` deliberately returns rows of any age so Next can serve them stale\n * while it revalidates, which means nothing expires on its own. Mongo drops a\n * row this long after its last write; anything still in use is rewritten by\n * revalidation far sooner, so only paths nobody requests anymore fall out.\n */\nconst DEFAULT_ENTRY_TTL_SECONDS = 60 * 60 * 24 * 7\n\nexport type MongoCacheHandlerOptions = {\n /** Connected client, owned by the consuming app so this package needs no driver. */\n clientPromise: Promise<MongoCacheClient>\n dbName?: string\n collectionName?: string\n /** TTL applied on every write. Defaults to 7 days. */\n entryTtlSeconds?: number\n /** Overrides the build id read from `.next/BUILD_ID`. */\n buildId?: string\n}\n\nexport type MongoCacheHandler = {\n get(key: string): Promise<CachedEntry | null>\n set(key: string, value: CachedValue | null, ctx?: CacheSetContext): Promise<void>\n delete(key: string): Promise<void>\n resetRequestCache(): void\n revalidateTag(tags: string | string[], durations?: unknown): Promise<void>\n}\n\n/**\n * Next instantiates the configured handler with `new Handler(ctx)`, so the\n * factory returns a class rather than an instance.\n */\nexport type MongoCacheHandlerConstructor = new (ctx?: unknown) => MongoCacheHandler\n\nfunction readBuildId(): string {\n try {\n return readFileSync(path.join(process.cwd(), '.next', 'BUILD_ID'), 'utf8').trim()\n } catch {\n return 'development'\n }\n}\n\n/**\n * Next only passes `ctx.tags` for fetch entries. Page and route entries carry\n * theirs — including the `_N_T_<path>` tag behind `revalidatePath` — in the\n * response headers, so without this fallback `revalidateTag` can never purge\n * rendered HTML and CMS edits wait out the route's full revalidate window.\n */\nfunction resolveTags(value: CachedValue | null | undefined, ctx?: CacheSetContext): string[] {\n if (ctx?.tags?.length) return ctx.tags\n\n const header = value?.headers?.['x-next-cache-tags']\n return typeof header === 'string' ? header.split(',').filter(Boolean) : []\n}\n\n/**\n * Mongo-backed `cacheHandler` for Next's ISR / route / fetch cache, so every\n * instance of a deployment shares one cache instead of a per-pod filesystem.\n *\n * Sites normally point `cacheHandler` at `@tetherto/dev-websites-core/cache/handler`,\n * which builds the client from `MONGODB_URI`. Use this factory when the site\n * already owns a client and wants to pass it in.\n *\n * Wire it with `cacheMaxMemorySize: 0`, and include\n * `node_modules/@tetherto/dev-websites-core/dist/cache` in\n * `outputFileTracingIncludes` so the standalone server can load the handler.\n */\nexport function createMongoCacheHandler(\n options: MongoCacheHandlerOptions\n): MongoCacheHandlerConstructor {\n const {\n clientPromise,\n dbName = DEFAULT_DB_NAME,\n collectionName = DEFAULT_COLLECTION_NAME,\n entryTtlSeconds = DEFAULT_ENTRY_TTL_SECONDS,\n } = options\n\n /**\n * Rendered HTML embeds the chunk filenames of the build that produced it, so\n * an entry left by a previous build loads but never hydrates — its chunks are\n * gone (404). Page keys are therefore scoped per build, while instances of\n * the same build keep sharing them.\n *\n * During `next build` `.next/BUILD_ID` does not exist yet, which is safe\n * because Next writes prerenders to disk and never calls `set()` for page\n * entries during the build.\n */\n const buildId = options.buildId ?? readBuildId()\n\n /**\n * Page and route entries are keyed by pathname; fetch entries are keyed by a\n * content hash and stay shared across builds, so upstream data survives a\n * deploy.\n */\n const scopeKey = (key: string): string => (key.startsWith('/') ? `${buildId}${key}` : key)\n\n // Next builds a fresh handler per request, so this has to live outside the\n // class to stay a one-off instead of an extra round-trip on every read.\n let indexPromise: Promise<unknown> | undefined\n\n const getCollection = async (): Promise<MongoCacheCollection> => {\n const client = await clientPromise\n const collection = client.db(dbName).collection(collectionName)\n\n indexPromise ??= Promise.all([\n collection.createIndex({ key: 1 }, { unique: true }),\n // `revalidateTag` deletes by `{ tags: { $in } }`; without this it COLLSCANs.\n collection.createIndex({ tags: 1 }),\n collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }),\n ]).catch(() => {})\n await indexPromise\n\n return collection\n }\n\n return class MongoCacheHandlerImpl implements MongoCacheHandler {\n /**\n * Returns the entry whatever its age. Next compares `lastModified` against\n * the route's own revalidate window and decides itself whether to serve it\n * as fresh, serve it stale while refreshing in the background, or\n * re-render. Dropping stale rows here turns every expiry into a blocking\n * render.\n */\n async get(key: string): Promise<CachedEntry | null> {\n const collection = await getCollection()\n const scoped = scopeKey(key)\n const entry = await collection.findOne({ key: scoped })\n if (!entry) return null\n\n // Heals rows written before error responses were skipped.\n if (shouldSkipCacheWrite(entry.value)) {\n await collection.deleteOne({ key: scoped })\n return null\n }\n\n const value = entry.value ?? null\n const segmentData = value?.segmentData\n\n return {\n lastModified: entry.lastModified,\n value:\n segmentData && !(segmentData instanceof Map)\n ? { ...value, segmentData: new Map(Object.entries(segmentData)) }\n : value,\n }\n }\n\n async set(key: string, value: CachedValue | null, ctx?: CacheSetContext): Promise<void> {\n if (shouldSkipCacheWrite(value)) return\n\n const collection = await getCollection()\n const scoped = scopeKey(key)\n const now = Date.now()\n\n await collection.updateOne(\n { key: scoped },\n {\n $set: {\n key: scoped,\n value,\n lastModified: now,\n expiresAt: new Date(now + entryTtlSeconds * 1000),\n tags: resolveTags(value, ctx),\n },\n },\n { upsert: true }\n )\n }\n\n async delete(key: string): Promise<void> {\n const collection = await getCollection()\n await collection.deleteOne({ key: scopeKey(key) })\n }\n\n resetRequestCache(): void {}\n\n async revalidateTag(tags: string | string[], _durations?: unknown): Promise<void> {\n const list = typeof tags === 'string' ? [tags] : tags\n if (!list?.length) return\n\n const collection = await getCollection()\n await collection.deleteMany({ tags: { $in: list } })\n }\n }\n}\n","import { MongoClient, type MongoClientOptions } from 'mongodb'\n\nconst options: MongoClientOptions = { promoteBuffers: true }\n\ntype GlobalWithMongo = typeof globalThis & {\n _mongoClientPromise?: Promise<MongoClient>\n}\n\n/**\n * One Mongo client for the cache handler. In development the promise lives on\n * `globalThis` so hot reload does not open a new connection on every compile.\n *\n * `promoteBuffers` makes BSON Binary come back as Node `Buffer`, which is what\n * Next stores for route and image cache entries.\n */\nexport function createMongoClientPromise(): Promise<MongoClient> {\n const uri = process.env.MONGODB_URI\n if (!uri) {\n throw new Error('Missing MONGODB_URI')\n }\n\n if (process.env.NODE_ENV === 'development') {\n const globalForMongo = globalThis as GlobalWithMongo\n globalForMongo._mongoClientPromise ??= new MongoClient(uri, options).connect()\n return globalForMongo._mongoClientPromise\n }\n\n return new MongoClient(uri, options).connect()\n}\n","import { createMongoCacheHandler } from './createMongoCacheHandler.js'\nimport { createMongoClientPromise } from './createMongoClient.js'\n\n/**\n * Default Next.js `cacheHandler`. Point `cacheHandler` in `next.config` at\n * `@tetherto/dev-websites-core/cache/handler`. Reads `MONGODB_URI` itself so\n * sites do not ship their own handler or Mongo client.\n *\n * Kept off the `/cache` barrel: importing the revalidation helpers must not\n * open a database connection.\n */\nexport default createMongoCacheHandler({ clientPromise: createMongoClientPromise() })\n"],"mappings":"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"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from "payload";
|
|
2
|
+
//#region src/cache/types.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Structural subset of the `mongodb` driver that the cache handler needs.
|
|
5
|
+
*
|
|
6
|
+
* Declaring it here instead of importing from `mongodb` keeps the driver out of
|
|
7
|
+
* the `/cache` entry, so revalidation helpers can be imported without loading it.
|
|
8
|
+
* A real `MongoClient` is assignable to `MongoCacheClient`.
|
|
9
|
+
*/
|
|
10
|
+
type MongoCacheClient = {
|
|
11
|
+
db(name: string): {
|
|
12
|
+
collection(name: string): MongoCacheCollection;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
type MongoCacheFilter = Record<string, unknown>;
|
|
16
|
+
type MongoCacheCollection = {
|
|
17
|
+
findOne(filter: MongoCacheFilter): Promise<MongoCacheRow | null>;
|
|
18
|
+
updateOne(filter: MongoCacheFilter, update: MongoCacheFilter, options: {
|
|
19
|
+
upsert: boolean;
|
|
20
|
+
}): Promise<unknown>;
|
|
21
|
+
deleteOne(filter: MongoCacheFilter): Promise<unknown>;
|
|
22
|
+
deleteMany(filter: MongoCacheFilter): Promise<unknown>;
|
|
23
|
+
createIndex(spec: Record<string, number>, options?: Record<string, unknown>): Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A cache entry as Next hands it to `set()`. Only the fields this handler reads
|
|
27
|
+
* are named; `kind` is `'APP_PAGE'`, `'APP_ROUTE'`, `'PAGES'`, `'FETCH'`, or
|
|
28
|
+
* `'IMAGE'`, and the rest of the payload is stored verbatim.
|
|
29
|
+
*/
|
|
30
|
+
type CachedValue = {
|
|
31
|
+
kind?: string;
|
|
32
|
+
status?: number;
|
|
33
|
+
html?: string;
|
|
34
|
+
headers?: Record<string, string | undefined>;
|
|
35
|
+
/** Stored as a plain object; Next iterates it as a Map, so `get()` rebuilds one. */
|
|
36
|
+
segmentData?: Record<string, unknown> | Map<string, unknown>;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
};
|
|
39
|
+
type MongoCacheRow = {
|
|
40
|
+
key?: string;
|
|
41
|
+
lastModified?: number;
|
|
42
|
+
tags?: string[];
|
|
43
|
+
value?: CachedValue | null;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
};
|
|
46
|
+
type CachedEntry = {
|
|
47
|
+
lastModified?: number;
|
|
48
|
+
value: CachedValue | null;
|
|
49
|
+
};
|
|
50
|
+
/** The `ctx` Next passes to `set()`; only `tags` is meaningful here. */
|
|
51
|
+
type CacheSetContext = {
|
|
52
|
+
tags?: string[];
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/cache/createMongoCacheHandler.d.ts
|
|
56
|
+
type MongoCacheHandlerOptions = {
|
|
57
|
+
/** Connected client, owned by the consuming app so this package needs no driver. */
|
|
58
|
+
clientPromise: Promise<MongoCacheClient>;
|
|
59
|
+
dbName?: string;
|
|
60
|
+
collectionName?: string;
|
|
61
|
+
/** TTL applied on every write. Defaults to 7 days. */
|
|
62
|
+
entryTtlSeconds?: number;
|
|
63
|
+
/** Overrides the build id read from `.next/BUILD_ID`. */
|
|
64
|
+
buildId?: string;
|
|
65
|
+
};
|
|
66
|
+
type MongoCacheHandler = {
|
|
67
|
+
get(key: string): Promise<CachedEntry | null>;
|
|
68
|
+
set(key: string, value: CachedValue | null, ctx?: CacheSetContext): Promise<void>;
|
|
69
|
+
delete(key: string): Promise<void>;
|
|
70
|
+
resetRequestCache(): void;
|
|
71
|
+
revalidateTag(tags: string | string[], durations?: unknown): Promise<void>;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Next instantiates the configured handler with `new Handler(ctx)`, so the
|
|
75
|
+
* factory returns a class rather than an instance.
|
|
76
|
+
*/
|
|
77
|
+
type MongoCacheHandlerConstructor = new (ctx?: unknown) => MongoCacheHandler;
|
|
78
|
+
/**
|
|
79
|
+
* Mongo-backed `cacheHandler` for Next's ISR / route / fetch cache, so every
|
|
80
|
+
* instance of a deployment shares one cache instead of a per-pod filesystem.
|
|
81
|
+
*
|
|
82
|
+
* Sites normally point `cacheHandler` at `@tetherto/dev-websites-core/cache/handler`,
|
|
83
|
+
* which builds the client from `MONGODB_URI`. Use this factory when the site
|
|
84
|
+
* already owns a client and wants to pass it in.
|
|
85
|
+
*
|
|
86
|
+
* Wire it with `cacheMaxMemorySize: 0`, and include
|
|
87
|
+
* `node_modules/@tetherto/dev-websites-core/dist/cache` in
|
|
88
|
+
* `outputFileTracingIncludes` so the standalone server can load the handler.
|
|
89
|
+
*/
|
|
90
|
+
declare function createMongoCacheHandler(options: MongoCacheHandlerOptions): MongoCacheHandlerConstructor;
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/cache/shouldSkipCacheWrite.d.ts
|
|
93
|
+
/**
|
|
94
|
+
* ISR pages with `dynamicParams = true` still render for unknown paths, call
|
|
95
|
+
* `notFound()`, and Next then asks the handler to persist the 404. Scanners
|
|
96
|
+
* hitting junk URLs rewrite that row on every request and bloat the cache
|
|
97
|
+
* collection and the Mongo oplog, so non-success page/route responses are
|
|
98
|
+
* never stored.
|
|
99
|
+
*
|
|
100
|
+
* `FETCH` entries are unaffected: Next nests their upstream status under
|
|
101
|
+
* `value.data.status`, not the top-level `status` read here.
|
|
102
|
+
*/
|
|
103
|
+
declare function shouldSkipCacheWrite(value: CachedValue | null | undefined): boolean;
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/cache/shouldSkipRevalidate.d.ts
|
|
106
|
+
/**
|
|
107
|
+
* Standalone scripts (e.g. seed) pass `context: { disableRevalidate: true }`
|
|
108
|
+
* through the Payload Local API so collection hooks can skip cache invalidation —
|
|
109
|
+
* `revalidatePath` throws outside an active Next.js request context.
|
|
110
|
+
*/
|
|
111
|
+
declare function shouldSkipRevalidate(req: PayloadRequest | undefined): boolean;
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/cache/safeRevalidatePath.d.ts
|
|
114
|
+
declare function safeRevalidatePath(path: string, type?: 'layout' | 'page'): void;
|
|
115
|
+
declare function safeRevalidateTag(tag: string, profile: 'max'): void;
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/cache/createPublishedSlugRevalidateHooks.d.ts
|
|
118
|
+
type PublishedSlugRevalidateHandlers = {
|
|
119
|
+
onPublishedSlugs: (slugs: Iterable<string>) => void;
|
|
120
|
+
onPublishedList?: () => void;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Payload hooks that revalidate when a draft/published doc with a `slug` changes.
|
|
124
|
+
* Skips when `req.context.disableRevalidate` is set (seeds).
|
|
125
|
+
*/
|
|
126
|
+
declare function createPublishedSlugRevalidateHooks(handlers: PublishedSlugRevalidateHandlers): {
|
|
127
|
+
afterChange: CollectionAfterChangeHook;
|
|
128
|
+
afterDelete: CollectionAfterDeleteHook;
|
|
129
|
+
};
|
|
130
|
+
/** Hooks that always call `onChange` (e.g. categories / tags that affect listings). */
|
|
131
|
+
declare function createAlwaysRevalidateHooks(onChange: () => void): {
|
|
132
|
+
afterChange: CollectionAfterChangeHook;
|
|
133
|
+
afterDelete: CollectionAfterDeleteHook;
|
|
134
|
+
};
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/cache/createPathRevalidators.d.ts
|
|
137
|
+
type BlogRevalidatorPaths = {
|
|
138
|
+
/** e.g. `(slug, locale) => \`/${locale}/blog/${slug}\`` — concrete URL, no `type` */
|
|
139
|
+
post: (slug: string, locale: string) => string;
|
|
140
|
+
/** Route pattern including route groups, e.g. `/[locale]/(marketing)/blog/[slug]` */
|
|
141
|
+
postDynamic: string;
|
|
142
|
+
list: string;
|
|
143
|
+
serverSitemap?: string;
|
|
144
|
+
sitemap?: string;
|
|
145
|
+
llms?: string;
|
|
146
|
+
};
|
|
147
|
+
type BlogRevalidators = {
|
|
148
|
+
revalidatePost: (slugs: Iterable<string>) => void;
|
|
149
|
+
revalidateList: () => void;
|
|
150
|
+
revalidateAllSurfaces: () => void;
|
|
151
|
+
};
|
|
152
|
+
declare function createBlogRevalidators(locales: readonly string[], paths: BlogRevalidatorPaths): BlogRevalidators;
|
|
153
|
+
type ChangelogRevalidatorPaths = {
|
|
154
|
+
detail: (slug: string, locale: string) => string;
|
|
155
|
+
detailDynamic: string;
|
|
156
|
+
list: string;
|
|
157
|
+
rss?: string;
|
|
158
|
+
serverSitemap?: string;
|
|
159
|
+
sitemap?: string;
|
|
160
|
+
llms?: string;
|
|
161
|
+
};
|
|
162
|
+
type ChangelogRevalidators = {
|
|
163
|
+
revalidateEntry: (slugs: Iterable<string>) => void;
|
|
164
|
+
revalidateList: () => void;
|
|
165
|
+
revalidateAllSurfaces: () => void;
|
|
166
|
+
};
|
|
167
|
+
declare function createChangelogRevalidators(locales: readonly string[], paths: ChangelogRevalidatorPaths): ChangelogRevalidators;
|
|
168
|
+
//#endregion
|
|
169
|
+
export { type BlogRevalidatorPaths, type BlogRevalidators, type CacheSetContext, type CachedEntry, type CachedValue, type ChangelogRevalidatorPaths, type ChangelogRevalidators, type MongoCacheClient, type MongoCacheCollection, type MongoCacheFilter, type MongoCacheHandler, type MongoCacheHandlerConstructor, type MongoCacheHandlerOptions, type MongoCacheRow, type PublishedSlugRevalidateHandlers, createAlwaysRevalidateHooks, createBlogRevalidators, createChangelogRevalidators, createMongoCacheHandler, createPublishedSlugRevalidateHooks, safeRevalidatePath, safeRevalidateTag, shouldSkipCacheWrite, shouldSkipRevalidate };
|
|
170
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/cache/types.ts","../../src/cache/createMongoCacheHandler.ts","../../src/cache/shouldSkipCacheWrite.ts","../../src/cache/shouldSkipRevalidate.ts","../../src/cache/safeRevalidatePath.ts","../../src/cache/createPublishedSlugRevalidateHooks.ts","../../src/cache/createPathRevalidators.ts"],"mappings":";;;;;;;;;KAOY;EACV,GAAG;IACD,WAAW,eAAe;;;KAIlB,mBAAmB;KAEnB;EACV,QAAQ,QAAQ,mBAAmB,QAAQ;EAC3C,UACE,QAAQ,kBACR,QAAQ,kBACR;IAAW;MACV;EACH,UAAU,QAAQ,mBAAmB;EACrC,WAAW,QAAQ,mBAAmB;EACtC,YAAY,MAAM,wBAAwB,UAAU,0BAA0B;;;;;;;KAQpE;EACV;EACA;EACA;EACA,UAAU;;EAEV,cAAc,0BAA0B;GACvC;;KAGS;EACV;EACA;EACA;EACA,QAAQ;GACP;;KAGS;EACV;EACA,OAAO;;;KAIG;EACV;;;;KCjCU;;EAEV,eAAe,QAAQ;EACvB;EACA;;EAEA;;EAEA;;KAGU;EACV,IAAI,cAAc,QAAQ;EAC1B,IAAI,aAAa,OAAO,oBAAoB,MAAM,kBAAkB;EACpE,OAAO,cAAc;EACrB;EACA,cAAc,yBAAyB,sBAAsB;;;;;;KAOnD,oCAAoC,kBAAkB;;;;;;;;;;;;;iBAmClD,wBACd,SAAS,2BACR;;;;;;;;;;;;;iBCxEa,qBAAqB,OAAO;;;;;;;;iBCL5B,qBAAqB,KAAK;;;iBCsB1B,mBAAmB,cAAc;iBAUjC,kBAAkB,aAAa;;;KCnCnC;EACV,mBAAmB,OAAO;EAC1B;;;;;;iBAOc,mCAAmC,UAAU;EAC3D,aAAa;EACb,aAAa;;;iBA8BC,4BAA4B;EAC1C,aAAa;EACb,aAAa;;;;KC7CH;;EAEV,OAAO,cAAc;;EAErB;EACA;EACA;EACA;EACA;;KAGU;EACV,iBAAiB,OAAO;EACxB;EACA;;iBAGc,uBACd,4BACA,OAAO,uBACN;KA4BS;EACV,SAAS,cAAc;EACvB;EACA;EACA;EACA;EACA;EACA;;KAGU;EACV,kBAAkB,OAAO;EACzB;EACA;;iBAGc,4BACd,4BACA,OAAO,4BACN"}
|