@kiryl.pekarski/payload-plugin-ab 1.0.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/README.md ADDED
@@ -0,0 +1,583 @@
1
+ # payload-plugin-ab
2
+
3
+ A/B testing plugin for [Payload CMS](https://payloadcms.com/) v3. Automatically maintains a **variant manifest** — a path-keyed map of A/B variant data — by hooking into Payload collection events. The manifest is designed to be read by Next.js middleware at the edge to route users to the correct variant without an additional database round-trip.
4
+
5
+ ## Table of Contents
6
+
7
+ - [How It Works](#how-it-works)
8
+ - [Key Features](#key-features)
9
+ - [Installation](#installation)
10
+ - [Quick Start](#quick-start)
11
+ - [Configuration Reference](#configuration-reference)
12
+ - [Plugin Options](#plugin-options)
13
+ - [CollectionABConfig](#collectionabconfig)
14
+ - [Storage Adapters](#storage-adapters)
15
+ - [Storage Adapters In Depth](#storage-adapters-in-depth)
16
+ - [payloadGlobalAdapter](#payloadglobaladapter)
17
+ - [vercelEdgeAdapter](#verceledgeadapter)
18
+ - [Using the Manifest in Next.js Middleware](#using-the-manifest-in-nextjs-middleware)
19
+ - [Localization Support](#localization-support)
20
+ - [TypeScript Generics](#typescript-generics)
21
+
22
+ ---
23
+
24
+ ## How It Works
25
+
26
+ The plugin follows a **write-on-change, read-at-edge** pattern:
27
+
28
+ ```
29
+ Payload Admin
30
+
31
+ │ Editor saves/deletes a variant document
32
+
33
+ afterChange / afterDelete hooks (injected by plugin)
34
+
35
+ │ Recompute all variants for the affected page path(s)
36
+
37
+ Storage Adapter (write)
38
+ ├─ payloadGlobalAdapter → Payload Global (JSON field, read via REST)
39
+ └─ vercelEdgeAdapter → Vercel Edge Config (read via @vercel/edge-config)
40
+
41
+
42
+ Manifest: { "/about": [variantA, variantB], "/pricing": [...] }
43
+
44
+
45
+ Next.js Middleware (edge-compatible read)
46
+
47
+ │ storage.read(pathname) → VariantData[] | null
48
+
49
+ └─ Route user to original page or variant
50
+ ```
51
+
52
+ The **manifest** is a plain JSON object:
53
+
54
+ ```ts
55
+ type Manifest<TVariantData> = Record<string, TVariantData[]>
56
+ // e.g. { "/en/about": [{ bucket: "b", rewritePath: "/en/variants/b/about" }] }
57
+ ```
58
+
59
+ A path with no variants is absent from the manifest (no-op for middleware). Only paths that have at least one active variant are written.
60
+
61
+ ---
62
+
63
+ ## Key Features
64
+
65
+ - **Zero-config hooks** — registers `afterChange` and `afterDelete` hooks on your variant collection automatically; no manual hook wiring required.
66
+ - **Locale-aware** — iterates over every configured Payload locale and writes a separate manifest entry per locale so multilingual A/B tests work out of the box.
67
+ - **Pluggable storage** — swap between the built-in adapters or implement your own `StorageAdapter`.
68
+ - **Edge-compatible reads** — both built-in adapters expose a `read()` method that can run inside Next.js middleware (no Node.js runtime required).
69
+ - **Fully typed** — the `TVariantData` generic flows through the entire plugin so your variant data is typed end-to-end.
70
+ - **Debug mode** — optionally expose the manifest Global in the Payload admin panel for inspection.
71
+
72
+ ---
73
+
74
+ ## Installation
75
+
76
+ ```bash
77
+ # pnpm (recommended)
78
+ pnpm add payload-plugin-ab
79
+
80
+ # npm
81
+ npm install payload-plugin-ab
82
+
83
+ # yarn
84
+ yarn add payload-plugin-ab
85
+ ```
86
+
87
+ **If you plan to use the Vercel Edge adapter**, also install its peer dependency:
88
+
89
+ ```bash
90
+ pnpm add @vercel/edge-config
91
+ ```
92
+
93
+ **Peer dependency**: `payload ^3.0.0` must already be installed in your project.
94
+
95
+ ---
96
+
97
+ ## Quick Start
98
+
99
+ This example wires up A/B testing for a `page` collection where variants live in a `page-variants` collection.
100
+
101
+ ### Step 1 — Create a variant collection
102
+
103
+ The variant collection requires a relationship back to the parent page and a bucket identifier that distinguishes the variant in the URL and cookie:
104
+
105
+ ```ts
106
+ // collections/PageVariants.ts
107
+ import type { CollectionConfig } from 'payload'
108
+
109
+ export const PageVariants: CollectionConfig = {
110
+ slug: 'page-variants',
111
+ fields: [
112
+ {
113
+ name: 'page',
114
+ type: 'relationship',
115
+ relationTo: 'page',
116
+ required: true,
117
+ index: true,
118
+ label: { en: 'Original Page', es: 'Página Original' },
119
+ admin: {
120
+ description: {
121
+ en: 'The page this variant belongs to.',
122
+ es: 'La página a la que pertenece esta variante.',
123
+ },
124
+ },
125
+ },
126
+ {
127
+ name: 'bucketID',
128
+ type: 'select',
129
+ required: true,
130
+ index: true,
131
+ options: [
132
+ { label: 'A', value: 'a' },
133
+ { label: 'B', value: 'b' },
134
+ { label: 'C', value: 'c' },
135
+ ],
136
+ label: { en: 'Bucket ID', es: 'ID de Variante' },
137
+ admin: {
138
+ description: {
139
+ en: 'Variant identifier used in the URL and cookie. Each page can have at most one variant per bucket.',
140
+ es: 'Identificador de variante usado en la URL y cookie. Cada página puede tener como máximo una variante por bucket.',
141
+ },
142
+ },
143
+ },
144
+ ],
145
+ }
146
+ ```
147
+
148
+ Each page can have at most one variant per bucket (`a`, `b`, `c`). Middleware uses the bucket ID to assign users and rewrite their request to the correct variant path.
149
+
150
+ ### Step 2 — Register the plugin
151
+
152
+ ```ts
153
+ // payload.config.ts
154
+ import { buildConfig } from 'payload'
155
+ import { abTestingPlugin } from 'payload-plugin-ab'
156
+ import { payloadGlobalAdapter } from 'payload-plugin-ab/adapters/payload-global'
157
+ import { Page } from './collections/Page'
158
+ import { PageVariants } from './collections/PageVariants'
159
+
160
+ // Shape of data stored per variant in the manifest.
161
+ // Your middleware reads this to decide where to rewrite the request.
162
+ type ABVariantData = {
163
+ bucket: string // 'a' | 'b' | 'c'
164
+ rewritePath: string // the URL this bucket should render
165
+ }
166
+
167
+ const abAdapter = payloadGlobalAdapter<ABVariantData>({
168
+ serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? '',
169
+ })
170
+
171
+ export default buildConfig({
172
+ collections: [Page, PageVariants],
173
+
174
+ plugins: [
175
+ abTestingPlugin<ABVariantData>({
176
+ debug: true, // set false in production to hide the manifest Global in admin
177
+ storage: abAdapter,
178
+ collections: {
179
+ // Key = parent collection slug
180
+ page: {
181
+ variantCollectionSlug: 'page-variants',
182
+
183
+ // Return the URL path used as the manifest key.
184
+ // Return null to skip this document (e.g. pages without a slug).
185
+ generatePath: ({ doc, locale }) => {
186
+ const slug = doc.slug as string | undefined
187
+ if (!slug) return null
188
+
189
+ return `/${locale}/${slug}`
190
+ },
191
+
192
+ // Return the data stored per variant in the manifest.
193
+ generateVariantData: ({ doc, variantDoc, locale }): ABVariantData => {
194
+ const slug = doc.slug as string
195
+
196
+ return {
197
+ bucket: variantDoc.bucketID as string,
198
+ rewritePath: `/${locale}/variants/${variantDoc.bucketID}/${slug}`,
199
+ }
200
+ },
201
+ },
202
+ },
203
+ }),
204
+ ],
205
+ })
206
+ ```
207
+
208
+ ### Step 3 — Read the manifest in middleware
209
+
210
+ ```ts
211
+ // middleware.ts (Next.js)
212
+ import { NextResponse } from 'next/server'
213
+ import type { NextRequest } from 'next/server'
214
+ import { payloadGlobalAdapter } from 'payload-plugin-ab/adapters/payload-global'
215
+
216
+ type ABVariantData = { bucket: string; rewritePath: string }
217
+
218
+ const storage = payloadGlobalAdapter<ABVariantData>({
219
+ serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? '',
220
+ })
221
+
222
+ const AB_BUCKET_COOKIE = 'ab-bucket'
223
+
224
+ /**
225
+ * Determines whether the current request should be rewritten to a variant path.
226
+ *
227
+ * - visiblePathname — the URL the user sees (used as the per-path cookie key)
228
+ * - manifestKey — the key to look up in the manifest (may differ from the
229
+ * visible path when locale prefixes or rewrites are involved)
230
+ * - originalRewritePath — internal path to rewrite to when the user is assigned
231
+ * the 'original' bucket on their first visit
232
+ *
233
+ * Returns a NextResponse with a rewrite (and sets a cookie on first visit),
234
+ * or null if no rewrite is needed (caller should fall through to NextResponse.next()).
235
+ */
236
+ async function resolveAbRewrite(
237
+ request: NextRequest,
238
+ visiblePathname: string,
239
+ manifestKey: string,
240
+ originalRewritePath: string,
241
+ ): Promise<NextResponse | null> {
242
+ let variants: ABVariantData[] | null = null
243
+
244
+ try {
245
+ variants = await storage.read(manifestKey)
246
+ } catch {
247
+ return null
248
+ }
249
+
250
+ if (!variants?.length) return null
251
+
252
+ const cookieName = `${AB_BUCKET_COOKIE}_${encodeURIComponent(visiblePathname)}`
253
+ const existingBucket = request.cookies.get(cookieName)?.value
254
+
255
+ let bucket = existingBucket
256
+ if (!bucket) {
257
+ // Include the original as one of the possible outcomes so not every user
258
+ // is sent to a variant — e.g. with 2 variants there is a 1-in-3 chance
259
+ // of seeing the original page.
260
+ const idx = Math.floor(Math.random() * (variants.length + 1))
261
+ bucket = idx === variants.length ? 'original' : variants[idx]!.bucket
262
+ }
263
+
264
+ if (bucket === 'original') {
265
+ if (!existingBucket) {
266
+ // First visit: persist the assignment, then rewrite to the original path
267
+ const url = request.nextUrl.clone()
268
+ url.pathname = originalRewritePath
269
+ const res = NextResponse.rewrite(url)
270
+ res.cookies.set(cookieName, 'original', { path: '/', sameSite: 'lax' })
271
+ return res
272
+ }
273
+ // Returning user assigned to original — no rewrite needed
274
+ return null
275
+ }
276
+
277
+ const match = variants.find((v) => v.bucket === bucket)
278
+ if (!match) return null
279
+
280
+ const url = request.nextUrl.clone()
281
+ url.pathname = match.rewritePath
282
+ const res = NextResponse.rewrite(url)
283
+ if (!existingBucket) {
284
+ res.cookies.set(cookieName, bucket, { path: '/', sameSite: 'lax' })
285
+ }
286
+ return res
287
+ }
288
+
289
+ export async function middleware(request: NextRequest) {
290
+ const { pathname } = request.nextUrl
291
+
292
+ const result = await resolveAbRewrite(
293
+ request,
294
+ pathname, // visible pathname — used as the per-path cookie key
295
+ pathname, // manifest key — adjust if you prepend a locale or other prefix
296
+ pathname, // original rewrite path — where to send the 'original' bucket
297
+ )
298
+
299
+ return result ?? NextResponse.next()
300
+ }
301
+
302
+ export const config = {
303
+ matcher: ['/((?!_next|api|favicon.ico).*)'],
304
+ }
305
+ ```
306
+
307
+ ---
308
+
309
+ ## Configuration Reference
310
+
311
+ ### Plugin Options
312
+
313
+ ```ts
314
+ interface AbTestingPluginConfig<TVariantData extends object> {
315
+ /** Enable or disable the plugin entirely. Default: true */
316
+ enabled?: boolean
317
+
318
+ /**
319
+ * When true, the manifest Global is visible in the Payload admin panel
320
+ * under the "System" group. Useful for debugging.
321
+ * Default: false
322
+ */
323
+ debug?: boolean
324
+
325
+ /**
326
+ * Map of parent collection slug → A/B config for that collection.
327
+ * You can configure multiple parent collections simultaneously.
328
+ */
329
+ collections: Record<string, CollectionABConfig<TVariantData>>
330
+
331
+ /** Storage adapter — payloadGlobalAdapter or vercelEdgeAdapter. */
332
+ storage: StorageAdapter<TVariantData>
333
+ }
334
+ ```
335
+
336
+ ### CollectionABConfig
337
+
338
+ ```ts
339
+ interface CollectionABConfig<TVariantData extends object> {
340
+ /**
341
+ * Slug of the variant collection (e.g. 'page-variants').
342
+ * Must be registered in your Payload config.
343
+ * Hooks are automatically added to this collection.
344
+ */
345
+ variantCollectionSlug: string
346
+
347
+ /**
348
+ * Maps a parent document to the URL path used as the manifest key.
349
+ *
350
+ * Return null to skip writing the manifest for that document
351
+ * (e.g. for drafts, documents without slugs, etc.).
352
+ *
353
+ * Called once per locale when localization is enabled.
354
+ */
355
+ generatePath: (args: { doc: Record<string, unknown>; locale: string }) => string | null
356
+
357
+ /**
358
+ * Builds the data object stored per variant in the manifest array.
359
+ * This is what your middleware reads — include everything it needs
360
+ * to make a routing decision (slug, weight, experiment ID, etc.).
361
+ *
362
+ * Called once per variant document, per locale.
363
+ */
364
+ generateVariantData: (args: {
365
+ doc: Record<string, unknown> // parent document
366
+ variantDoc: Record<string, unknown> // variant document
367
+ locale: string
368
+ }) => TVariantData
369
+ }
370
+ ```
371
+
372
+ ### Storage Adapters
373
+
374
+ Both adapters implement the same `StorageAdapter` interface:
375
+
376
+ ```ts
377
+ interface StorageAdapter<TVariantData extends object> {
378
+ /** Write variant data for a path. Called from afterChange hooks. */
379
+ write(path: string, variants: TVariantData[], payload: Payload): Promise<void>
380
+
381
+ /**
382
+ * Read variant data for a path.
383
+ * Must be Edge-compatible (no Node.js-only APIs).
384
+ * Returns null if the path has no variants.
385
+ */
386
+ read(path: string): Promise<TVariantData[] | null>
387
+
388
+ /** Remove all variant data for a path. Called from afterDelete hooks. */
389
+ clear(path: string, payload: Payload): Promise<void>
390
+
391
+ /**
392
+ * Optional: return a GlobalConfig to register in Payload.
393
+ * Used by payloadGlobalAdapter to store the manifest.
394
+ * Not needed for vercelEdgeAdapter.
395
+ */
396
+ createGlobal?(debug: boolean): GlobalConfig
397
+ }
398
+ ```
399
+
400
+ You can implement this interface yourself if neither built-in adapter fits your stack (e.g. Redis, Upstash, KV, etc.).
401
+
402
+ ---
403
+
404
+ ## Storage Adapters In Depth
405
+
406
+ ### payloadGlobalAdapter
407
+
408
+ Stores the manifest in a **Payload Global** as a JSON field. The manifest is then fetched from middleware via the Payload REST API.
409
+
410
+ **Import:**
411
+ ```ts
412
+ import { payloadGlobalAdapter } from 'payload-plugin-ab/adapters/payload-global'
413
+ ```
414
+
415
+ **Options:**
416
+
417
+ | Option | Type | Default | Description |
418
+ |---|---|---|---|
419
+ | `globalSlug` | `string` | `'_abManifest'` | Slug for the auto-created Payload Global |
420
+ | `serverURL` | `string` | `''` | Full origin of your Payload server (e.g. `https://cms.example.com`). Used by `read()` in middleware. |
421
+ | `apiRoute` | `string` | `'/api'` | Payload REST API prefix |
422
+
423
+ **Usage:**
424
+ ```ts
425
+ import { payloadGlobalAdapter } from 'payload-plugin-ab/adapters/payload-global'
426
+
427
+ const storage = payloadGlobalAdapter({
428
+ globalSlug: '_abManifest', // optional, this is the default
429
+ serverURL: 'https://cms.example.com',
430
+ apiRoute: '/api', // optional, this is the default
431
+ })
432
+ ```
433
+
434
+ **How it reads:** The `read()` method fetches `GET {serverURL}{apiRoute}/globals/{globalSlug}` with `cache: 'no-store'` and returns `data.manifest[path]`. This REST endpoint is public (the Global has `access.read: () => true`).
435
+
436
+ **When the Global is hidden:** By default the Global is not shown in the Payload admin. Set `debug: true` in the plugin config to make it visible under the **System** group.
437
+
438
+ ---
439
+
440
+ ### vercelEdgeAdapter
441
+
442
+ Stores the manifest in **Vercel Edge Config** — Vercel's ultra-low-latency global key-value store. Reads are served from the edge with sub-millisecond latency, making this the best option for Vercel-hosted projects.
443
+
444
+ **Import:**
445
+ ```ts
446
+ import { vercelEdgeAdapter } from 'payload-plugin-ab/adapters/vercel-edge'
447
+ ```
448
+
449
+ **Prerequisites:**
450
+
451
+ 1. Install the Edge Config client:
452
+ ```bash
453
+ pnpm add @vercel/edge-config
454
+ ```
455
+
456
+ 2. Create an Edge Config store in your Vercel project dashboard.
457
+
458
+ 3. Set the following environment variables:
459
+
460
+ | Variable | Description |
461
+ |---|---|
462
+ | `EDGE_CONFIG` | Connection string (from Vercel dashboard, e.g. `https://edge-config.vercel.com/ecfg_xxx?token=yyy`) |
463
+ | `EDGE_CONFIG_ID` | Edge Config store ID (e.g. `ecfg_xxx`) — passed as `configID` |
464
+ | `VERCEL_REST_API_ACCESS_TOKEN` | Vercel REST API token with read/write access — passed as `vercelRestAPIAccessToken` |
465
+
466
+ **Options:**
467
+
468
+ | Option | Type | Default | Description |
469
+ |---|---|---|---|
470
+ | `configID` | `string` | required | Edge Config store ID |
471
+ | `configURL` | `string` | required | Full Edge Config URL (same as `EDGE_CONFIG` env var) |
472
+ | `vercelRestAPIAccessToken` | `string` | required | Vercel REST API access token |
473
+ | `teamID` | `string` | — | Vercel team ID (for team-scoped projects) |
474
+ | `manifestKey` | `string` | `'ab-testing'` | Top-level key in Edge Config that holds the manifest object |
475
+
476
+ **Usage:**
477
+ ```ts
478
+ import { vercelEdgeAdapter } from 'payload-plugin-ab/adapters/vercel-edge'
479
+
480
+ const storage = vercelEdgeAdapter({
481
+ configID: process.env.EDGE_CONFIG_ID!,
482
+ configURL: process.env.EDGE_CONFIG!,
483
+ vercelRestAPIAccessToken: process.env.VERCEL_REST_API_ACCESS_TOKEN!,
484
+ teamID: process.env.VERCEL_TEAM_ID, // optional
485
+ manifestKey: 'ab-testing', // optional, this is the default
486
+ })
487
+ ```
488
+
489
+ **How it writes:** Updates are made via the Vercel REST API (`PATCH /v1/edge-config/{configID}/items`) using an `upsert` operation. The adapter maintains a local in-memory cache of the manifest to avoid re-reading Edge Config on every write within the same server process.
490
+
491
+ **How it reads:** Uses `@vercel/edge-config`'s `get(manifestKey)` — this is edge-compatible and extremely fast.
492
+
493
+ ---
494
+
495
+ ## Using the Manifest in Next.js Middleware
496
+
497
+ The `storage.read(path)` method is the only adapter method called from middleware. It returns:
498
+
499
+ - `TVariantData[]` — an array of variant objects for that path (at least one element)
500
+ - `null` — no variants configured for this path (pass through)
501
+
502
+ ### resolveAbRewrite pattern
503
+
504
+ The recommended approach is a dedicated `resolveAbRewrite` helper (shown in the Quick Start above) that handles bucket assignment, sticky sessions, and rewrites in one place. Key behaviours:
505
+
506
+ | Behaviour | Detail |
507
+ |---|---|
508
+ | **Sticky sessions** | Bucket is stored in a per-path cookie (`ab-bucket_<encoded-pathname>`) so the same user always sees the same variant. |
509
+ | **Original bucket** | The original page is included as an outcome alongside the configured variants. With _n_ variants there is a 1-in-(_n_+1) chance of landing on the original. |
510
+ | **First visit** | Cookie is written and the request is rewritten in a single response — no extra redirect. |
511
+ | **Returning user (original)** | The function returns `null` so the caller falls through to `NextResponse.next()` — no unnecessary rewrite. |
512
+ | **Error safety** | `storage.read()` is wrapped in try/catch; any adapter error results in `null` (pass through). |
513
+
514
+ The three path arguments let you decouple the URL the user sees, the manifest lookup key, and the internal rewrite target — useful when locale prefixes or custom rewrite rules are involved:
515
+
516
+ ```ts
517
+ await resolveAbRewrite(
518
+ request,
519
+ visiblePathname, // used as the per-path cookie key
520
+ manifestKey, // key to look up in the manifest
521
+ originalRewritePath, // internal path for the 'original' bucket
522
+ )
523
+ ```
524
+
525
+ ---
526
+
527
+ ## Localization Support
528
+
529
+ If your Payload config has `localization` enabled, the plugin automatically handles it. For every locale defined in `payload.config.localization.locales`, the hooks:
530
+
531
+ 1. Fetch the parent document in that locale.
532
+ 2. Call `generatePath({ doc, locale })` to determine the manifest key.
533
+ 3. Call `generateVariantData({ doc, variantDoc, locale })` for each variant.
534
+ 4. Write a separate manifest entry per locale.
535
+
536
+ This means you can generate locale-prefixed paths from any field on the document:
537
+
538
+ ```ts
539
+ generatePath: ({ doc, locale }) => {
540
+ const slug = doc.slug as string | undefined
541
+ if (!slug) return null
542
+
543
+ return `/${locale}/${slug}` // e.g. '/en/about', '/fr/about'
544
+ },
545
+ ```
546
+
547
+ No additional configuration is needed — locale support is automatic when `payload.config.localization` is set.
548
+
549
+ ---
550
+
551
+ ## TypeScript Generics
552
+
553
+ The plugin is fully generic over `TVariantData extends object`. This type represents the shape of each variant entry in the manifest. Define it once and it flows through everywhere:
554
+
555
+ ```ts
556
+ // Define your variant shape — include everything middleware needs to route the request
557
+ type ABVariantData = {
558
+ bucket: string // which bucket this variant belongs to ('a' | 'b' | 'c')
559
+ rewritePath: string // the internal URL path middleware should rewrite to
560
+ }
561
+
562
+ // Pass the type to the adapter
563
+ const storage = payloadGlobalAdapter<ABVariantData>({ serverURL: '...' })
564
+
565
+ // Pass the same type to the plugin
566
+ abTestingPlugin<ABVariantData>({
567
+ collections: {
568
+ page: {
569
+ variantCollectionSlug: 'page-variants',
570
+ generatePath: ({ doc, locale }) => { /* ... */ },
571
+
572
+ // variantDoc is Record<string, unknown> — cast fields as needed
573
+ generateVariantData: ({ variantDoc, locale }): ABVariantData => ({
574
+ bucket: variantDoc.bucketID as string,
575
+ rewritePath: `/${locale}/variants/${variantDoc.bucketID}`,
576
+ }),
577
+ },
578
+ },
579
+ storage,
580
+ })
581
+ ```
582
+
583
+ In middleware, `storage.read(path)` returns `ABVariantData[] | null` — fully typed.
@@ -0,0 +1,15 @@
1
+ import { S as StorageAdapter } from '../../config-CRUREAW_.js';
2
+ import 'payload';
3
+
4
+ interface PayloadGlobalAdapterConfig {
5
+ /** Slug for the Payload Global that stores the manifest. Default: '_abManifest' */
6
+ globalSlug?: string;
7
+ /** Server URL used to fetch the global via REST in middleware. Example: 'https://example.com' */
8
+ serverURL?: string;
9
+ /** Payload API route prefix. Default: '/api' */
10
+ apiRoute?: string;
11
+ }
12
+
13
+ declare function payloadGlobalAdapter<TVariantData extends object>(config?: PayloadGlobalAdapterConfig): StorageAdapter<TVariantData>;
14
+
15
+ export { payloadGlobalAdapter };
@@ -0,0 +1,82 @@
1
+ // src/adapters/payloadGlobal/api/fetchManifest.ts
2
+ async function fetchManifest(serverURL, apiRoute, slug, path) {
3
+ try {
4
+ const res = await fetch(`${serverURL}${apiRoute}/globals/${slug}`, {
5
+ cache: "no-store"
6
+ });
7
+ if (!res.ok) return null;
8
+ const data = await res.json();
9
+ return data?.manifest?.[path] ?? null;
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ // src/adapters/payloadGlobal/api/readManifest.ts
16
+ async function readManifest(payload, slug) {
17
+ try {
18
+ const doc = await payload.findGlobal({ slug, overrideAccess: true });
19
+ return doc?.manifest ?? {};
20
+ } catch {
21
+ return {};
22
+ }
23
+ }
24
+
25
+ // src/adapters/payloadGlobal/utils/createGlobal.ts
26
+ function createGlobal(slug, debug) {
27
+ return {
28
+ slug,
29
+ access: {
30
+ read: () => true
31
+ },
32
+ admin: {
33
+ hidden: !debug,
34
+ group: "System"
35
+ },
36
+ fields: [
37
+ {
38
+ name: "manifest",
39
+ type: "json",
40
+ admin: {
41
+ description: "A/B testing manifest. Managed automatically \u2014 do not edit manually."
42
+ }
43
+ }
44
+ ]
45
+ };
46
+ }
47
+
48
+ // src/adapters/payloadGlobal/index.ts
49
+ function payloadGlobalAdapter(config) {
50
+ const slug = config?.globalSlug ?? "_abManifest";
51
+ return {
52
+ async write(path, variants, payload) {
53
+ const currentManifest = await readManifest(payload, slug);
54
+ await payload.updateGlobal({
55
+ slug,
56
+ data: { manifest: { ...currentManifest, [path]: variants } },
57
+ overrideAccess: true
58
+ });
59
+ },
60
+ async read(path) {
61
+ const serverURL = config?.serverURL ?? "";
62
+ const apiRoute = config?.apiRoute ?? "/api";
63
+ return fetchManifest(serverURL, apiRoute, slug, path);
64
+ },
65
+ async clear(path, payload) {
66
+ const currentManifest = await readManifest(payload, slug);
67
+ delete currentManifest[path];
68
+ await payload.updateGlobal({
69
+ slug,
70
+ data: { manifest: currentManifest },
71
+ overrideAccess: true
72
+ });
73
+ },
74
+ createGlobal(debug = false) {
75
+ return createGlobal(slug, debug);
76
+ }
77
+ };
78
+ }
79
+ export {
80
+ payloadGlobalAdapter
81
+ };
82
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/payloadGlobal/api/fetchManifest.ts","../../../src/adapters/payloadGlobal/api/readManifest.ts","../../../src/adapters/payloadGlobal/utils/createGlobal.ts","../../../src/adapters/payloadGlobal/index.ts"],"sourcesContent":["export async function fetchManifest<TVariantData extends object>(\n serverURL: string,\n apiRoute: string,\n slug: string,\n path: string,\n): Promise<TVariantData[] | null> {\n try {\n const res = await fetch(`${serverURL}${apiRoute}/globals/${slug}`, {\n cache: \"no-store\",\n });\n\n if (!res.ok) return null;\n\n const data = await res.json();\n\n return (data?.manifest?.[path] as TVariantData[]) ?? null;\n } catch {\n return null;\n }\n}\n","import type { GlobalSlug, Payload } from \"payload\";\nimport type { Manifest } from \"../../../types/manifest\";\n\nexport async function readManifest(payload: Payload, slug: string): Promise<Manifest> {\n try {\n const doc = await payload.findGlobal({ slug: slug as GlobalSlug, overrideAccess: true });\n\n return (doc?.manifest as Manifest) ?? {};\n } catch {\n return {};\n }\n}\n","import type { GlobalConfig } from \"payload\";\n\nexport function createGlobal(slug: string, debug: boolean): GlobalConfig {\n return {\n slug,\n access: {\n read: () => true,\n },\n admin: {\n hidden: !debug,\n group: \"System\",\n },\n fields: [\n {\n name: \"manifest\",\n type: \"json\",\n admin: {\n description: \"A/B testing manifest. Managed automatically — do not edit manually.\",\n },\n },\n ],\n };\n}\n","import type { GlobalSlug } from \"payload\";\nimport type { StorageAdapter } from \"../../types/config\";\nimport type { PayloadGlobalAdapterConfig } from \"./config\";\nimport { fetchManifest } from \"./api/fetchManifest\";\nimport { readManifest } from \"./api/readManifest\";\nimport { createGlobal } from \"./utils/createGlobal\";\n\nexport function payloadGlobalAdapter<TVariantData extends object>(\n config?: PayloadGlobalAdapterConfig,\n): StorageAdapter<TVariantData> {\n const slug = config?.globalSlug ?? \"_abManifest\";\n\n return {\n async write(path, variants, payload) {\n const currentManifest = await readManifest(payload, slug);\n\n await payload.updateGlobal({\n slug: slug as GlobalSlug,\n data: { manifest: { ...currentManifest, [path]: variants } },\n overrideAccess: true,\n });\n },\n\n async read(path) {\n const serverURL = config?.serverURL ?? \"\";\n const apiRoute = config?.apiRoute ?? \"/api\";\n\n return fetchManifest<TVariantData>(serverURL, apiRoute, slug, path);\n },\n\n async clear(path, payload) {\n const currentManifest = await readManifest(payload, slug);\n\n delete currentManifest[path];\n\n await payload.updateGlobal({\n slug: slug as GlobalSlug,\n data: { manifest: currentManifest },\n overrideAccess: true,\n });\n },\n\n createGlobal(debug = false) {\n return createGlobal(slug, debug);\n },\n };\n}\n"],"mappings":";AAAA,eAAsB,cACpB,WACA,UACA,MACA,MACgC;AAChC,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,YAAY,IAAI,IAAI;AAAA,MACjE,OAAO;AAAA,IACT,CAAC;AAED,QAAI,CAAC,IAAI,GAAI,QAAO;AAEpB,UAAM,OAAO,MAAM,IAAI,KAAK;AAE5B,WAAQ,MAAM,WAAW,IAAI,KAAwB;AAAA,EACvD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;AChBA,eAAsB,aAAa,SAAkB,MAAiC;AACpF,MAAI;AACF,UAAM,MAAM,MAAM,QAAQ,WAAW,EAAE,MAA0B,gBAAgB,KAAK,CAAC;AAEvF,WAAQ,KAAK,YAAyB,CAAC;AAAA,EACzC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;;;ACTO,SAAS,aAAa,MAAc,OAA8B;AACvE,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,MACN,MAAM,MAAM;AAAA,IACd;AAAA,IACA,OAAO;AAAA,MACL,QAAQ,CAAC;AAAA,MACT,OAAO;AAAA,IACT;AAAA,IACA,QAAQ;AAAA,MACN;AAAA,QACE,MAAM;AAAA,QACN,MAAM;AAAA,QACN,OAAO;AAAA,UACL,aAAa;AAAA,QACf;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ACfO,SAAS,qBACd,QAC8B;AAC9B,QAAM,OAAO,QAAQ,cAAc;AAEnC,SAAO;AAAA,IACL,MAAM,MAAM,MAAM,UAAU,SAAS;AACnC,YAAM,kBAAkB,MAAM,aAAa,SAAS,IAAI;AAExD,YAAM,QAAQ,aAAa;AAAA,QACzB;AAAA,QACA,MAAM,EAAE,UAAU,EAAE,GAAG,iBAAiB,CAAC,IAAI,GAAG,SAAS,EAAE;AAAA,QAC3D,gBAAgB;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,KAAK,MAAM;AACf,YAAM,YAAY,QAAQ,aAAa;AACvC,YAAM,WAAW,QAAQ,YAAY;AAErC,aAAO,cAA4B,WAAW,UAAU,MAAM,IAAI;AAAA,IACpE;AAAA,IAEA,MAAM,MAAM,MAAM,SAAS;AACzB,YAAM,kBAAkB,MAAM,aAAa,SAAS,IAAI;AAExD,aAAO,gBAAgB,IAAI;AAE3B,YAAM,QAAQ,aAAa;AAAA,QACzB;AAAA,QACA,MAAM,EAAE,UAAU,gBAAgB;AAAA,QAClC,gBAAgB;AAAA,MAClB,CAAC;AAAA,IACH;AAAA,IAEA,aAAa,QAAQ,OAAO;AAC1B,aAAO,aAAa,MAAM,KAAK;AAAA,IACjC;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,20 @@
1
+ import { S as StorageAdapter } from '../../config-CRUREAW_.js';
2
+ import 'payload';
3
+
4
+ interface VercelEdgeAdapterConfig {
5
+ configID: string;
6
+ configURL: string;
7
+ vercelRestAPIAccessToken: string;
8
+ teamID?: string;
9
+ /** Top-level key in Edge Config that holds the manifest. Default: 'ab-testing' */
10
+ manifestKey?: string;
11
+ }
12
+
13
+ /**
14
+ * Vercel Edge Config adapter.
15
+ * Requires "pnpm add \@vercel/edge-config" and the following env vars:
16
+ * EDGE_CONFIG, EDGE_CONFIG_ID, VERCEL_REST_API_ACCESS_TOKEN
17
+ */
18
+ declare function vercelEdgeAdapter<TVariantData extends object>(config: VercelEdgeAdapterConfig): StorageAdapter<TVariantData>;
19
+
20
+ export { vercelEdgeAdapter };
@@ -0,0 +1,66 @@
1
+ // src/adapters/vercelEdge/api/readManifest.ts
2
+ async function readManifest(manifestKey) {
3
+ try {
4
+ const { get } = await import("@vercel/edge-config");
5
+ const manifest = await get(manifestKey);
6
+ return manifest ?? {};
7
+ } catch {
8
+ return {};
9
+ }
10
+ }
11
+
12
+ // src/adapters/vercelEdge/utils/buildHeaders.ts
13
+ function buildHeaders(config) {
14
+ return {
15
+ "Content-Type": "application/json",
16
+ Authorization: `Bearer ${config.vercelRestAPIAccessToken}`
17
+ };
18
+ }
19
+
20
+ // src/adapters/vercelEdge/api/updateEdgeConfig.ts
21
+ async function updateEdgeConfig(config, manifestKey, value) {
22
+ const teamQuery = config.teamID ? `?teamId=${config.teamID}` : "";
23
+ await fetch(`https://api.vercel.com/v1/edge-config/${config.configID}/items${teamQuery}`, {
24
+ method: "PATCH",
25
+ headers: buildHeaders(config),
26
+ body: JSON.stringify({ items: [{ operation: "upsert", key: manifestKey, value }] })
27
+ });
28
+ }
29
+
30
+ // src/adapters/vercelEdge/index.ts
31
+ function vercelEdgeAdapter(config) {
32
+ const manifestKey = config.manifestKey ?? "ab-testing";
33
+ let localCache = null;
34
+ async function getManifest() {
35
+ if (localCache === null) {
36
+ localCache = await readManifest(manifestKey);
37
+ }
38
+ return localCache;
39
+ }
40
+ return {
41
+ async write(path, variants) {
42
+ const currentManifest = await getManifest();
43
+ localCache = { ...currentManifest, [path]: variants };
44
+ await updateEdgeConfig(config, manifestKey, localCache);
45
+ },
46
+ async read(path) {
47
+ try {
48
+ const manifest = await readManifest(manifestKey);
49
+ return manifest?.[path] ?? null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ },
54
+ async clear(path) {
55
+ const currentManifest = await getManifest();
56
+ const updated = { ...currentManifest };
57
+ delete updated[path];
58
+ localCache = updated;
59
+ await updateEdgeConfig(config, manifestKey, localCache);
60
+ }
61
+ };
62
+ }
63
+ export {
64
+ vercelEdgeAdapter
65
+ };
66
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/adapters/vercelEdge/api/readManifest.ts","../../../src/adapters/vercelEdge/utils/buildHeaders.ts","../../../src/adapters/vercelEdge/api/updateEdgeConfig.ts","../../../src/adapters/vercelEdge/index.ts"],"sourcesContent":["import type { Manifest } from \"../../../types/manifest\";\n\nexport async function readManifest<TVariantData extends object = object>(\n manifestKey: string,\n): Promise<Manifest<TVariantData>> {\n try {\n const { get } = await import(\"@vercel/edge-config\");\n\n const manifest = await get<Manifest<TVariantData>>(manifestKey);\n\n return manifest ?? {};\n } catch {\n return {};\n }\n}\n","import type { VercelEdgeAdapterConfig } from \"../config\";\n\nexport function buildHeaders(config: VercelEdgeAdapterConfig): Record<string, string> {\n return {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${config.vercelRestAPIAccessToken}`,\n };\n}\n","import type { VercelEdgeAdapterConfig } from \"../config\";\nimport { buildHeaders } from \"../utils/buildHeaders\";\n\nexport async function updateEdgeConfig(config: VercelEdgeAdapterConfig, manifestKey: string, value: unknown) {\n const teamQuery = config.teamID ? `?teamId=${config.teamID}` : \"\";\n\n await fetch(`https://api.vercel.com/v1/edge-config/${config.configID}/items${teamQuery}`, {\n method: \"PATCH\",\n headers: buildHeaders(config),\n body: JSON.stringify({ items: [{ operation: \"upsert\", key: manifestKey, value }] }),\n });\n}\n","import type { StorageAdapter } from \"../../types/config\";\nimport type { Manifest } from \"../../types/manifest\";\nimport { readManifest } from \"./api/readManifest\";\nimport { updateEdgeConfig } from \"./api/updateEdgeConfig\";\nimport type { VercelEdgeAdapterConfig } from \"./config\";\n\n/**\n * Vercel Edge Config adapter.\n * Requires \"pnpm add \\@vercel/edge-config\" and the following env vars:\n * EDGE_CONFIG, EDGE_CONFIG_ID, VERCEL_REST_API_ACCESS_TOKEN\n */\nexport function vercelEdgeAdapter<TVariantData extends object>(\n config: VercelEdgeAdapterConfig,\n): StorageAdapter<TVariantData> {\n const manifestKey = config.manifestKey ?? \"ab-testing\";\n let localCache: Manifest<TVariantData> | null = null;\n\n async function getManifest() {\n if (localCache === null) {\n localCache = await readManifest<TVariantData>(manifestKey);\n }\n\n return localCache;\n }\n\n return {\n async write(path, variants) {\n const currentManifest = await getManifest();\n\n localCache = { ...currentManifest, [path]: variants };\n\n await updateEdgeConfig(config, manifestKey, localCache);\n },\n\n async read(path) {\n try {\n const manifest = await readManifest<TVariantData>(manifestKey);\n\n return manifest?.[path] ?? null;\n } catch {\n return null;\n }\n },\n\n async clear(path) {\n const currentManifest = await getManifest();\n const updated = { ...currentManifest };\n\n delete updated[path];\n\n localCache = updated;\n\n await updateEdgeConfig(config, manifestKey, localCache);\n },\n };\n}\n"],"mappings":";AAEA,eAAsB,aACpB,aACiC;AACjC,MAAI;AACF,UAAM,EAAE,IAAI,IAAI,MAAM,OAAO,qBAAqB;AAElD,UAAM,WAAW,MAAM,IAA4B,WAAW;AAE9D,WAAO,YAAY,CAAC;AAAA,EACtB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;;;ACZO,SAAS,aAAa,QAAyD;AACpF,SAAO;AAAA,IACL,gBAAgB;AAAA,IAChB,eAAe,UAAU,OAAO,wBAAwB;AAAA,EAC1D;AACF;;;ACJA,eAAsB,iBAAiB,QAAiC,aAAqB,OAAgB;AAC3G,QAAM,YAAY,OAAO,SAAS,WAAW,OAAO,MAAM,KAAK;AAE/D,QAAM,MAAM,yCAAyC,OAAO,QAAQ,SAAS,SAAS,IAAI;AAAA,IACxF,QAAQ;AAAA,IACR,SAAS,aAAa,MAAM;AAAA,IAC5B,MAAM,KAAK,UAAU,EAAE,OAAO,CAAC,EAAE,WAAW,UAAU,KAAK,aAAa,MAAM,CAAC,EAAE,CAAC;AAAA,EACpF,CAAC;AACH;;;ACAO,SAAS,kBACd,QAC8B;AAC9B,QAAM,cAAc,OAAO,eAAe;AAC1C,MAAI,aAA4C;AAEhD,iBAAe,cAAc;AAC3B,QAAI,eAAe,MAAM;AACvB,mBAAa,MAAM,aAA2B,WAAW;AAAA,IAC3D;AAEA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,MAAM,MAAM,UAAU;AAC1B,YAAM,kBAAkB,MAAM,YAAY;AAE1C,mBAAa,EAAE,GAAG,iBAAiB,CAAC,IAAI,GAAG,SAAS;AAEpD,YAAM,iBAAiB,QAAQ,aAAa,UAAU;AAAA,IACxD;AAAA,IAEA,MAAM,KAAK,MAAM;AACf,UAAI;AACF,cAAM,WAAW,MAAM,aAA2B,WAAW;AAE7D,eAAO,WAAW,IAAI,KAAK;AAAA,MAC7B,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,MAAM,MAAM,MAAM;AAChB,YAAM,kBAAkB,MAAM,YAAY;AAC1C,YAAM,UAAU,EAAE,GAAG,gBAAgB;AAErC,aAAO,QAAQ,IAAI;AAEnB,mBAAa;AAEb,YAAM,iBAAiB,QAAQ,aAAa,UAAU;AAAA,IACxD;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,37 @@
1
+ import { Payload, GlobalConfig } from 'payload';
2
+
3
+ interface StorageAdapter<TVariantData extends object = object> {
4
+ /** Write variant data for a path. Called from Payload afterChange hooks. */
5
+ write(path: string, variants: TVariantData[], payload: Payload): Promise<void>;
6
+ /** Read variant data for a path. Called from Next.js middleware — must be Edge-compatible. */
7
+ read(path: string): Promise<TVariantData[] | null>;
8
+ /** Remove all variant data for a path. Called from Payload afterDelete hooks. */
9
+ clear(path: string, payload: Payload): Promise<void>;
10
+ /** Optional: return a Global config to register in Payload (used by payloadGlobalAdapter). */
11
+ createGlobal?(debug: boolean): GlobalConfig;
12
+ }
13
+ interface CollectionABConfig<TVariantData extends object = object> {
14
+ variantCollectionSlug: string;
15
+ generatePath: (args: {
16
+ doc: Record<string, unknown>;
17
+ locale: string;
18
+ }) => string | null;
19
+ /** Generate the data stored per variant in the manifest. */
20
+ generateVariantData: (args: {
21
+ doc: Record<string, unknown>;
22
+ variantDoc: Record<string, unknown>;
23
+ locale: string;
24
+ }) => TVariantData;
25
+ }
26
+ interface AbTestingPluginConfig<TVariantData extends object = object> {
27
+ /** Default: true */
28
+ enabled?: boolean;
29
+ /** If true, the manifest global is visible in the Payload admin panel. Default: false */
30
+ debug?: boolean;
31
+ /** Map of parent collection slug => A/B config for that collection. */
32
+ collections: Record<string, CollectionABConfig<TVariantData>>;
33
+ /** Storage adapter instance (payloadGlobalAdapter or vercelEdgeAdapter). */
34
+ storage: StorageAdapter<TVariantData>;
35
+ }
36
+
37
+ export type { AbTestingPluginConfig as A, CollectionABConfig as C, StorageAdapter as S };
@@ -0,0 +1,9 @@
1
+ import { Plugin } from 'payload';
2
+ import { A as AbTestingPluginConfig } from './config-CRUREAW_.js';
3
+ export { C as CollectionABConfig, S as StorageAdapter } from './config-CRUREAW_.js';
4
+ export { payloadGlobalAdapter } from './adapters/payloadGlobal/index.js';
5
+ export { vercelEdgeAdapter } from './adapters/vercelEdge/index.js';
6
+
7
+ declare const abTestingPlugin: <TVariantData extends object>(pluginConfig: AbTestingPluginConfig<TVariantData>) => Plugin;
8
+
9
+ export { AbTestingPluginConfig, abTestingPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,334 @@
1
+ // src/utils/buildVariantToParentCollectionSlugsMap.ts
2
+ function buildVariantToParentCollectionSlugsMap(collections) {
3
+ const variantToParent = /* @__PURE__ */ new Map();
4
+ for (const [parentSlug, abConfig] of Object.entries(collections)) {
5
+ if (abConfig) variantToParent.set(abConfig.variantCollectionSlug, parentSlug);
6
+ }
7
+ return variantToParent;
8
+ }
9
+
10
+ // src/utils/resolveId.ts
11
+ var isValueIsBaseDocument = (value) => {
12
+ return "id" in value;
13
+ };
14
+ function resolveId(value) {
15
+ if (!value) return null;
16
+ if (typeof value === "number" || typeof value === "string") return value;
17
+ if (typeof value === "object" && isValueIsBaseDocument(value)) {
18
+ return value.id;
19
+ }
20
+ return null;
21
+ }
22
+
23
+ // src/utils/getLocales.ts
24
+ function getLocales(payload) {
25
+ const localization = payload.config.localization;
26
+ const locales = localization ? localization.locales ?? [] : [];
27
+ return locales.map((l) => typeof l === "string" ? l : l.code ?? String(l));
28
+ }
29
+
30
+ // src/hooks/buildAfterChangeHook.ts
31
+ function buildAfterChangeHook(parentCollectionSlug, abConfig, pluginConfig) {
32
+ return async ({ doc, req, previousDoc }) => {
33
+ const { payload } = req;
34
+ if (!payload) return;
35
+ const pageId = resolveId(doc.page);
36
+ if (!pageId) return;
37
+ const locales = getLocales(payload);
38
+ const previousPageId = previousDoc ? resolveId(previousDoc.page) : null;
39
+ if (previousPageId && previousPageId !== pageId) {
40
+ for (const locale of locales) {
41
+ const oldPageDoc = await payload.findByID({
42
+ collection: parentCollectionSlug,
43
+ id: previousPageId,
44
+ depth: 2,
45
+ locale,
46
+ overrideAccess: true,
47
+ req
48
+ });
49
+ if (!oldPageDoc) continue;
50
+ const oldManifestKey = abConfig.generatePath({ doc: oldPageDoc, locale });
51
+ if (!oldManifestKey) continue;
52
+ const remainingOldVariants = await payload.find({
53
+ collection: abConfig.variantCollectionSlug,
54
+ where: {
55
+ and: [{ page: { equals: previousPageId } }, { id: { not_equals: doc.id } }]
56
+ },
57
+ depth: 2,
58
+ locale,
59
+ overrideAccess: true,
60
+ limit: 100,
61
+ req
62
+ });
63
+ if (remainingOldVariants.docs.length === 0) {
64
+ await pluginConfig.storage.clear(oldManifestKey, payload);
65
+ } else {
66
+ const oldVariantData = remainingOldVariants.docs.map(
67
+ (variantDoc) => abConfig.generateVariantData({ doc: oldPageDoc, variantDoc, locale })
68
+ );
69
+ await pluginConfig.storage.write(oldManifestKey, oldVariantData, payload);
70
+ }
71
+ }
72
+ }
73
+ for (const locale of locales) {
74
+ const pageDoc = await payload.findByID({
75
+ collection: parentCollectionSlug,
76
+ id: pageId,
77
+ depth: 2,
78
+ locale,
79
+ overrideAccess: true,
80
+ req
81
+ });
82
+ if (!pageDoc) continue;
83
+ const manifestKey = abConfig.generatePath({ doc: pageDoc, locale });
84
+ if (!manifestKey) continue;
85
+ const allVariants = await payload.find({
86
+ collection: abConfig.variantCollectionSlug,
87
+ where: { page: { equals: pageId } },
88
+ depth: 2,
89
+ locale,
90
+ overrideAccess: true,
91
+ limit: 100,
92
+ req
93
+ });
94
+ const variantData = allVariants.docs.map(
95
+ (variantDoc) => abConfig.generateVariantData({ doc: pageDoc, variantDoc, locale })
96
+ );
97
+ await pluginConfig.storage.write(manifestKey, variantData, payload);
98
+ }
99
+ };
100
+ }
101
+
102
+ // src/hooks/buildAfterDeleteHook.ts
103
+ function buildAfterDeleteHook(parentCollectionSlug, abConfig, pluginConfig) {
104
+ return async ({ doc, req, id }) => {
105
+ const { payload } = req;
106
+ if (!payload) return;
107
+ const pageId = resolveId(doc.page);
108
+ if (!pageId) return;
109
+ const locales = getLocales(payload);
110
+ for (const locale of locales) {
111
+ const pageDoc = await payload.findByID({
112
+ collection: parentCollectionSlug,
113
+ id: pageId,
114
+ depth: 2,
115
+ locale,
116
+ overrideAccess: true,
117
+ req
118
+ });
119
+ if (!pageDoc) continue;
120
+ const manifestKey = abConfig.generatePath({ doc: pageDoc, locale });
121
+ if (!manifestKey) continue;
122
+ const remainingVariants = await payload.find({
123
+ collection: abConfig.variantCollectionSlug,
124
+ where: {
125
+ and: [{ page: { equals: pageId } }, { id: { not_equals: id } }]
126
+ },
127
+ depth: 2,
128
+ locale,
129
+ overrideAccess: true,
130
+ limit: 100,
131
+ req
132
+ });
133
+ if (remainingVariants.docs.length === 0) {
134
+ await pluginConfig.storage.clear(manifestKey, payload);
135
+ } else {
136
+ const variantData = remainingVariants.docs.map(
137
+ (variantDoc) => abConfig.generateVariantData({ doc: pageDoc, variantDoc, locale })
138
+ );
139
+ await pluginConfig.storage.write(manifestKey, variantData, payload);
140
+ }
141
+ }
142
+ };
143
+ }
144
+
145
+ // src/utils/addHooksToVariantCollections.ts
146
+ function addHooksToVariantCollections(config, pluginConfig, variantToParent) {
147
+ const patchedCollections = (config.collections ?? []).map((collection) => {
148
+ const parentSlug = variantToParent.get(collection.slug);
149
+ if (!parentSlug) return collection;
150
+ const abConfig = pluginConfig.collections[parentSlug];
151
+ if (!abConfig) return collection;
152
+ return {
153
+ ...collection,
154
+ hooks: {
155
+ ...collection.hooks,
156
+ afterChange: [
157
+ ...collection.hooks?.afterChange ?? [],
158
+ buildAfterChangeHook(parentSlug, abConfig, pluginConfig)
159
+ ],
160
+ afterDelete: [
161
+ ...collection.hooks?.afterDelete ?? [],
162
+ buildAfterDeleteHook(parentSlug, abConfig, pluginConfig)
163
+ ]
164
+ }
165
+ };
166
+ });
167
+ return patchedCollections;
168
+ }
169
+
170
+ // src/plugin.ts
171
+ var abTestingPlugin = (pluginConfig) => (incomingConfig) => {
172
+ const { enabled = true, debug = false, collections, storage } = pluginConfig;
173
+ if (!enabled) return incomingConfig;
174
+ const extraGlobals = storage.createGlobal ? [storage.createGlobal(debug)] : [];
175
+ const variantToParent = buildVariantToParentCollectionSlugsMap(collections);
176
+ const patchedCollections = addHooksToVariantCollections(
177
+ incomingConfig,
178
+ pluginConfig,
179
+ variantToParent
180
+ );
181
+ return {
182
+ ...incomingConfig,
183
+ collections: patchedCollections,
184
+ globals: [...incomingConfig.globals ?? [], ...extraGlobals]
185
+ };
186
+ };
187
+
188
+ // src/adapters/payloadGlobal/api/fetchManifest.ts
189
+ async function fetchManifest(serverURL, apiRoute, slug, path) {
190
+ try {
191
+ const res = await fetch(`${serverURL}${apiRoute}/globals/${slug}`, {
192
+ cache: "no-store"
193
+ });
194
+ if (!res.ok) return null;
195
+ const data = await res.json();
196
+ return data?.manifest?.[path] ?? null;
197
+ } catch {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ // src/adapters/payloadGlobal/api/readManifest.ts
203
+ async function readManifest(payload, slug) {
204
+ try {
205
+ const doc = await payload.findGlobal({ slug, overrideAccess: true });
206
+ return doc?.manifest ?? {};
207
+ } catch {
208
+ return {};
209
+ }
210
+ }
211
+
212
+ // src/adapters/payloadGlobal/utils/createGlobal.ts
213
+ function createGlobal(slug, debug) {
214
+ return {
215
+ slug,
216
+ access: {
217
+ read: () => true
218
+ },
219
+ admin: {
220
+ hidden: !debug,
221
+ group: "System"
222
+ },
223
+ fields: [
224
+ {
225
+ name: "manifest",
226
+ type: "json",
227
+ admin: {
228
+ description: "A/B testing manifest. Managed automatically \u2014 do not edit manually."
229
+ }
230
+ }
231
+ ]
232
+ };
233
+ }
234
+
235
+ // src/adapters/payloadGlobal/index.ts
236
+ function payloadGlobalAdapter(config) {
237
+ const slug = config?.globalSlug ?? "_abManifest";
238
+ return {
239
+ async write(path, variants, payload) {
240
+ const currentManifest = await readManifest(payload, slug);
241
+ await payload.updateGlobal({
242
+ slug,
243
+ data: { manifest: { ...currentManifest, [path]: variants } },
244
+ overrideAccess: true
245
+ });
246
+ },
247
+ async read(path) {
248
+ const serverURL = config?.serverURL ?? "";
249
+ const apiRoute = config?.apiRoute ?? "/api";
250
+ return fetchManifest(serverURL, apiRoute, slug, path);
251
+ },
252
+ async clear(path, payload) {
253
+ const currentManifest = await readManifest(payload, slug);
254
+ delete currentManifest[path];
255
+ await payload.updateGlobal({
256
+ slug,
257
+ data: { manifest: currentManifest },
258
+ overrideAccess: true
259
+ });
260
+ },
261
+ createGlobal(debug = false) {
262
+ return createGlobal(slug, debug);
263
+ }
264
+ };
265
+ }
266
+
267
+ // src/adapters/vercelEdge/api/readManifest.ts
268
+ async function readManifest2(manifestKey) {
269
+ try {
270
+ const { get } = await import("@vercel/edge-config");
271
+ const manifest = await get(manifestKey);
272
+ return manifest ?? {};
273
+ } catch {
274
+ return {};
275
+ }
276
+ }
277
+
278
+ // src/adapters/vercelEdge/utils/buildHeaders.ts
279
+ function buildHeaders(config) {
280
+ return {
281
+ "Content-Type": "application/json",
282
+ Authorization: `Bearer ${config.vercelRestAPIAccessToken}`
283
+ };
284
+ }
285
+
286
+ // src/adapters/vercelEdge/api/updateEdgeConfig.ts
287
+ async function updateEdgeConfig(config, manifestKey, value) {
288
+ const teamQuery = config.teamID ? `?teamId=${config.teamID}` : "";
289
+ await fetch(`https://api.vercel.com/v1/edge-config/${config.configID}/items${teamQuery}`, {
290
+ method: "PATCH",
291
+ headers: buildHeaders(config),
292
+ body: JSON.stringify({ items: [{ operation: "upsert", key: manifestKey, value }] })
293
+ });
294
+ }
295
+
296
+ // src/adapters/vercelEdge/index.ts
297
+ function vercelEdgeAdapter(config) {
298
+ const manifestKey = config.manifestKey ?? "ab-testing";
299
+ let localCache = null;
300
+ async function getManifest() {
301
+ if (localCache === null) {
302
+ localCache = await readManifest2(manifestKey);
303
+ }
304
+ return localCache;
305
+ }
306
+ return {
307
+ async write(path, variants) {
308
+ const currentManifest = await getManifest();
309
+ localCache = { ...currentManifest, [path]: variants };
310
+ await updateEdgeConfig(config, manifestKey, localCache);
311
+ },
312
+ async read(path) {
313
+ try {
314
+ const manifest = await readManifest2(manifestKey);
315
+ return manifest?.[path] ?? null;
316
+ } catch {
317
+ return null;
318
+ }
319
+ },
320
+ async clear(path) {
321
+ const currentManifest = await getManifest();
322
+ const updated = { ...currentManifest };
323
+ delete updated[path];
324
+ localCache = updated;
325
+ await updateEdgeConfig(config, manifestKey, localCache);
326
+ }
327
+ };
328
+ }
329
+ export {
330
+ abTestingPlugin,
331
+ payloadGlobalAdapter,
332
+ vercelEdgeAdapter
333
+ };
334
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/buildVariantToParentCollectionSlugsMap.ts","../src/utils/resolveId.ts","../src/utils/getLocales.ts","../src/hooks/buildAfterChangeHook.ts","../src/hooks/buildAfterDeleteHook.ts","../src/utils/addHooksToVariantCollections.ts","../src/plugin.ts","../src/adapters/payloadGlobal/api/fetchManifest.ts","../src/adapters/payloadGlobal/api/readManifest.ts","../src/adapters/payloadGlobal/utils/createGlobal.ts","../src/adapters/payloadGlobal/index.ts","../src/adapters/vercelEdge/api/readManifest.ts","../src/adapters/vercelEdge/utils/buildHeaders.ts","../src/adapters/vercelEdge/api/updateEdgeConfig.ts","../src/adapters/vercelEdge/index.ts"],"sourcesContent":["import type { CollectionABConfig } from \"../types/config\";\n\nexport function buildVariantToParentCollectionSlugsMap<TVariantData extends object>(\n collections: Record<string, CollectionABConfig<TVariantData>>,\n) {\n const variantToParent = new Map<string, string>();\n\n for (const [parentSlug, abConfig] of Object.entries(collections)) {\n if (abConfig) variantToParent.set(abConfig.variantCollectionSlug, parentSlug);\n }\n\n return variantToParent;\n}\n","interface BaseDocument {\n id: number | string;\n}\n\nconst isValueIsBaseDocument = (value: object): value is BaseDocument => {\n return \"id\" in value;\n};\n\nexport function resolveId(value: unknown) {\n if (!value) return null;\n\n if (typeof value === \"number\" || typeof value === \"string\") return value;\n\n if (typeof value === \"object\" && isValueIsBaseDocument(value)) {\n return value.id;\n }\n\n return null;\n}\n","import type { Payload } from \"payload\";\n\nexport function getLocales(payload: Payload): string[] {\n const localization = payload.config.localization;\n\n const locales = localization ? (localization.locales ?? []) : [];\n\n return locales.map((l) => (typeof l === \"string\" ? l : (l.code ?? String(l))));\n}\n","import type { CollectionAfterChangeHook, CollectionSlug, TypedLocale } from \"payload\";\nimport type { AbTestingPluginConfig, CollectionABConfig } from \"../types/config\";\nimport { resolveId } from \"../utils/resolveId\";\nimport { getLocales } from \"../utils/getLocales\";\n\nexport function buildAfterChangeHook<TVariantData extends object>(\n parentCollectionSlug: string,\n abConfig: CollectionABConfig<TVariantData>,\n pluginConfig: AbTestingPluginConfig<TVariantData>,\n): CollectionAfterChangeHook {\n return async ({ doc, req, previousDoc }) => {\n const { payload } = req;\n if (!payload) return;\n\n const pageId = resolveId(doc.page);\n if (!pageId) return;\n\n const locales = getLocales(payload);\n\n const previousPageId = previousDoc ? resolveId(previousDoc.page) : null;\n if (previousPageId && previousPageId !== pageId) {\n for (const locale of locales) {\n const oldPageDoc = await payload.findByID({\n collection: parentCollectionSlug as CollectionSlug,\n id: previousPageId,\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n req,\n });\n if (!oldPageDoc) continue;\n\n const oldManifestKey = abConfig.generatePath({ doc: oldPageDoc, locale });\n if (!oldManifestKey) continue;\n\n const remainingOldVariants = await payload.find({\n collection: abConfig.variantCollectionSlug as CollectionSlug,\n where: {\n and: [{ page: { equals: previousPageId } }, { id: { not_equals: doc.id } }],\n },\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n limit: 100,\n req,\n });\n\n if (remainingOldVariants.docs.length === 0) {\n await pluginConfig.storage.clear(oldManifestKey, payload);\n } else {\n const oldVariantData = remainingOldVariants.docs.map((variantDoc) =>\n abConfig.generateVariantData({ doc: oldPageDoc, variantDoc, locale }),\n );\n await pluginConfig.storage.write(oldManifestKey, oldVariantData, payload);\n }\n }\n }\n\n for (const locale of locales) {\n const pageDoc = await payload.findByID({\n collection: parentCollectionSlug as CollectionSlug,\n id: pageId,\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n req,\n });\n if (!pageDoc) continue;\n\n const manifestKey = abConfig.generatePath({ doc: pageDoc, locale });\n if (!manifestKey) continue;\n\n const allVariants = await payload.find({\n collection: abConfig.variantCollectionSlug as CollectionSlug,\n where: { page: { equals: pageId } },\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n limit: 100,\n req,\n });\n\n const variantData = allVariants.docs.map((variantDoc) =>\n abConfig.generateVariantData({ doc: pageDoc, variantDoc, locale }),\n );\n\n await pluginConfig.storage.write(manifestKey, variantData, payload);\n }\n };\n}\n","import type { CollectionAfterDeleteHook, CollectionSlug, TypedLocale } from \"payload\";\nimport type { AbTestingPluginConfig, CollectionABConfig } from \"../types/config\";\nimport { resolveId } from \"../utils/resolveId\";\nimport { getLocales } from \"../utils/getLocales\";\n\nexport function buildAfterDeleteHook<TVariantData extends object>(\n parentCollectionSlug: string,\n abConfig: CollectionABConfig<TVariantData>,\n pluginConfig: AbTestingPluginConfig<TVariantData>,\n): CollectionAfterDeleteHook {\n return async ({ doc, req, id }) => {\n const { payload } = req;\n if (!payload) return;\n\n const pageId = resolveId(doc.page);\n if (!pageId) return;\n\n const locales = getLocales(payload);\n\n for (const locale of locales) {\n const pageDoc = await payload.findByID({\n collection: parentCollectionSlug as CollectionSlug,\n id: pageId,\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n req,\n });\n if (!pageDoc) continue;\n\n const manifestKey = abConfig.generatePath({ doc: pageDoc, locale });\n if (!manifestKey) continue;\n\n const remainingVariants = await payload.find({\n collection: abConfig.variantCollectionSlug as CollectionSlug,\n where: {\n and: [{ page: { equals: pageId } }, { id: { not_equals: id } }],\n },\n depth: 2,\n locale: locale as TypedLocale,\n overrideAccess: true,\n limit: 100,\n req,\n });\n\n if (remainingVariants.docs.length === 0) {\n await pluginConfig.storage.clear(manifestKey, payload);\n } else {\n const variantData = remainingVariants.docs.map((variantDoc) =>\n abConfig.generateVariantData({ doc: pageDoc, variantDoc, locale }),\n );\n\n await pluginConfig.storage.write(manifestKey, variantData, payload);\n }\n }\n };\n}\n","import type { CollectionConfig, Config } from \"payload\";\nimport type { AbTestingPluginConfig } from \"../types/config\";\nimport { buildAfterChangeHook } from \"../hooks/buildAfterChangeHook\";\nimport { buildAfterDeleteHook } from \"../hooks/buildAfterDeleteHook\";\n\nexport function addHooksToVariantCollections<TVariantData extends object>(\n config: Config,\n pluginConfig: AbTestingPluginConfig<TVariantData>,\n variantToParent: Map<string, string>,\n) {\n const patchedCollections = (config.collections ?? []).map((collection) => {\n const parentSlug = variantToParent.get(collection.slug);\n if (!parentSlug) return collection;\n\n const abConfig = pluginConfig.collections[parentSlug];\n if (!abConfig) return collection;\n\n return {\n ...collection,\n hooks: {\n ...collection.hooks,\n afterChange: [\n ...(collection.hooks?.afterChange ?? []),\n buildAfterChangeHook(parentSlug, abConfig, pluginConfig),\n ],\n afterDelete: [\n ...(collection.hooks?.afterDelete ?? []),\n buildAfterDeleteHook(parentSlug, abConfig, pluginConfig),\n ],\n },\n } as CollectionConfig;\n });\n\n return patchedCollections;\n}\n","import type { Config, Plugin } from \"payload\";\nimport type { AbTestingPluginConfig } from \"./types/config\";\nimport { buildVariantToParentCollectionSlugsMap } from \"./utils/buildVariantToParentCollectionSlugsMap\";\nimport { addHooksToVariantCollections } from \"./utils/addHooksToVariantCollections\";\n\nexport const abTestingPlugin =\n <TVariantData extends object>(pluginConfig: AbTestingPluginConfig<TVariantData>): Plugin =>\n (incomingConfig: Config): Config => {\n const { enabled = true, debug = false, collections, storage } = pluginConfig;\n\n if (!enabled) return incomingConfig;\n\n const extraGlobals = storage.createGlobal ? [storage.createGlobal(debug)] : [];\n\n const variantToParent = buildVariantToParentCollectionSlugsMap<TVariantData>(collections);\n\n const patchedCollections = addHooksToVariantCollections<TVariantData>(\n incomingConfig,\n pluginConfig,\n variantToParent,\n );\n\n return {\n ...incomingConfig,\n collections: patchedCollections,\n globals: [...(incomingConfig.globals ?? []), ...extraGlobals],\n };\n };\n","export async function fetchManifest<TVariantData extends object>(\n serverURL: string,\n apiRoute: string,\n slug: string,\n path: string,\n): Promise<TVariantData[] | null> {\n try {\n const res = await fetch(`${serverURL}${apiRoute}/globals/${slug}`, {\n cache: \"no-store\",\n });\n\n if (!res.ok) return null;\n\n const data = await res.json();\n\n return (data?.manifest?.[path] as TVariantData[]) ?? null;\n } catch {\n return null;\n }\n}\n","import type { GlobalSlug, Payload } from \"payload\";\nimport type { Manifest } from \"../../../types/manifest\";\n\nexport async function readManifest(payload: Payload, slug: string): Promise<Manifest> {\n try {\n const doc = await payload.findGlobal({ slug: slug as GlobalSlug, overrideAccess: true });\n\n return (doc?.manifest as Manifest) ?? {};\n } catch {\n return {};\n }\n}\n","import type { GlobalConfig } from \"payload\";\n\nexport function createGlobal(slug: string, debug: boolean): GlobalConfig {\n return {\n slug,\n access: {\n read: () => true,\n },\n admin: {\n hidden: !debug,\n group: \"System\",\n },\n fields: [\n {\n name: \"manifest\",\n type: \"json\",\n admin: {\n description: \"A/B testing manifest. Managed automatically — do not edit manually.\",\n },\n },\n ],\n };\n}\n","import type { GlobalSlug } from \"payload\";\nimport type { StorageAdapter } from \"../../types/config\";\nimport type { PayloadGlobalAdapterConfig } from \"./config\";\nimport { fetchManifest } from \"./api/fetchManifest\";\nimport { readManifest } from \"./api/readManifest\";\nimport { createGlobal } from \"./utils/createGlobal\";\n\nexport function payloadGlobalAdapter<TVariantData extends object>(\n config?: PayloadGlobalAdapterConfig,\n): StorageAdapter<TVariantData> {\n const slug = config?.globalSlug ?? \"_abManifest\";\n\n return {\n async write(path, variants, payload) {\n const currentManifest = await readManifest(payload, slug);\n\n await payload.updateGlobal({\n slug: slug as GlobalSlug,\n data: { manifest: { ...currentManifest, [path]: variants } },\n overrideAccess: true,\n });\n },\n\n async read(path) {\n const serverURL = config?.serverURL ?? \"\";\n const apiRoute = config?.apiRoute ?? \"/api\";\n\n return fetchManifest<TVariantData>(serverURL, apiRoute, slug, path);\n },\n\n async clear(path, payload) {\n const currentManifest = await readManifest(payload, slug);\n\n delete currentManifest[path];\n\n await payload.updateGlobal({\n slug: slug as GlobalSlug,\n data: { manifest: currentManifest },\n overrideAccess: true,\n });\n },\n\n createGlobal(debug = false) {\n return createGlobal(slug, debug);\n },\n };\n}\n","import type { Manifest } from \"../../../types/manifest\";\n\nexport async function readManifest<TVariantData extends object = object>(\n manifestKey: string,\n): Promise<Manifest<TVariantData>> {\n try {\n const { get } = await import(\"@vercel/edge-config\");\n\n const manifest = await get<Manifest<TVariantData>>(manifestKey);\n\n return manifest ?? {};\n } catch {\n return {};\n }\n}\n","import type { VercelEdgeAdapterConfig } from \"../config\";\n\nexport function buildHeaders(config: VercelEdgeAdapterConfig): Record<string, string> {\n return {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${config.vercelRestAPIAccessToken}`,\n };\n}\n","import type { VercelEdgeAdapterConfig } from \"../config\";\nimport { buildHeaders } from \"../utils/buildHeaders\";\n\nexport async function updateEdgeConfig(config: VercelEdgeAdapterConfig, manifestKey: string, value: unknown) {\n const teamQuery = config.teamID ? `?teamId=${config.teamID}` : \"\";\n\n await fetch(`https://api.vercel.com/v1/edge-config/${config.configID}/items${teamQuery}`, {\n method: \"PATCH\",\n headers: buildHeaders(config),\n body: JSON.stringify({ items: [{ operation: \"upsert\", key: manifestKey, value }] }),\n });\n}\n","import type { StorageAdapter } from \"../../types/config\";\nimport type { Manifest } from \"../../types/manifest\";\nimport { readManifest } from \"./api/readManifest\";\nimport { updateEdgeConfig } from \"./api/updateEdgeConfig\";\nimport type { VercelEdgeAdapterConfig } from \"./config\";\n\n/**\n * Vercel Edge Config adapter.\n * Requires \"pnpm add \\@vercel/edge-config\" and the following env vars:\n * EDGE_CONFIG, EDGE_CONFIG_ID, VERCEL_REST_API_ACCESS_TOKEN\n */\nexport function vercelEdgeAdapter<TVariantData extends object>(\n config: VercelEdgeAdapterConfig,\n): StorageAdapter<TVariantData> {\n const manifestKey = config.manifestKey ?? \"ab-testing\";\n let localCache: Manifest<TVariantData> | null = null;\n\n async function getManifest() {\n if (localCache === null) {\n localCache = await readManifest<TVariantData>(manifestKey);\n }\n\n return localCache;\n }\n\n return {\n async write(path, variants) {\n const currentManifest = await getManifest();\n\n localCache = { ...currentManifest, [path]: variants };\n\n await updateEdgeConfig(config, manifestKey, localCache);\n },\n\n async read(path) {\n try {\n const manifest = await readManifest<TVariantData>(manifestKey);\n\n return manifest?.[path] ?? null;\n } catch {\n return null;\n }\n },\n\n async clear(path) {\n const currentManifest = await getManifest();\n const updated = { ...currentManifest };\n\n delete updated[path];\n\n localCache = updated;\n\n await updateEdgeConfig(config, manifestKey, localCache);\n },\n };\n}\n"],"mappings":"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","names":["readManifest","readManifest"]}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@kiryl.pekarski/payload-plugin-ab",
3
+ "version": "1.0.0",
4
+ "description": "A/B testing plugin for Payload CMS",
5
+ "keywords": [
6
+ "payload-cms",
7
+ "payload-plugin",
8
+ "ab-testing",
9
+ "split-testing",
10
+ "a-b-testing",
11
+ "page-variants",
12
+ "next.js",
13
+ "vercel-edge",
14
+ "edge-config",
15
+ "middleware",
16
+ "typescript",
17
+ "cms",
18
+ "conversion-optimization"
19
+ ],
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/focusreactive/payload-plugin-ab"
24
+ },
25
+ "author": "Kiryl Pekarski <kiryl.pekarski@fr.team>",
26
+ "type": "module",
27
+ "main": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "files": ["dist"],
30
+ "exports": {
31
+ ".": {
32
+ "import": "./dist/index.js",
33
+ "types": "./dist/index.d.ts"
34
+ },
35
+ "./adapters/payload-global": {
36
+ "import": "./dist/adapters/payloadGlobal/index.js",
37
+ "types": "./dist/adapters/payloadGlobal/index.d.ts"
38
+ },
39
+ "./adapters/vercel-edge": {
40
+ "import": "./dist/adapters/vercelEdge/index.js",
41
+ "types": "./dist/adapters/vercelEdge/index.d.ts"
42
+ }
43
+ },
44
+ "scripts": {
45
+ "build": "tsup",
46
+ "lint": "eslint src/",
47
+ "lint:fix": "eslint src/ --fix",
48
+ "format": "prettier --write src/",
49
+ "format:check": "prettier --check src/"
50
+ },
51
+ "peerDependencies": {
52
+ "payload": "^3.0.0"
53
+ },
54
+ "optionalDependencies": {
55
+ "@vercel/edge-config": "^1.0.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^20.0.0",
59
+ "eslint": "^9.0.0",
60
+ "eslint-config-prettier": "^9.0.0",
61
+ "payload": "^3.73.0",
62
+ "prettier": "^3.0.0",
63
+ "tsup": "^8.0.0",
64
+ "typescript": "^5.0.0",
65
+ "typescript-eslint": "^8.0.0"
66
+ }
67
+ }