@decocms/nextjs 8.5.1 → 8.6.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.
Files changed (46) hide show
  1. package/README.md +81 -14
  2. package/dist/assignments.d.ts +95 -0
  3. package/dist/assignments.d.ts.map +1 -0
  4. package/dist/assignments.js +229 -0
  5. package/dist/assignments.js.map +1 -0
  6. package/dist/draft.d.ts +9 -7
  7. package/dist/draft.d.ts.map +1 -1
  8. package/dist/draft.js +10 -8
  9. package/dist/draft.js.map +1 -1
  10. package/dist/drafts.d.ts +29 -60
  11. package/dist/drafts.d.ts.map +1 -1
  12. package/dist/drafts.js +38 -117
  13. package/dist/drafts.js.map +1 -1
  14. package/dist/index.d.ts +5 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +5 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/instrumentation/index.d.ts +1 -1
  19. package/dist/instrumentation/index.js +3 -3
  20. package/dist/instrumentation/index.js.map +1 -1
  21. package/dist/proxy/index.d.ts +134 -30
  22. package/dist/proxy/index.d.ts.map +1 -1
  23. package/dist/proxy/index.js +169 -37
  24. package/dist/proxy/index.js.map +1 -1
  25. package/dist/server/index.d.ts +59 -20
  26. package/dist/server/index.d.ts.map +1 -1
  27. package/dist/server/index.js +129 -25
  28. package/dist/server/index.js.map +1 -1
  29. package/dist/server/slot.d.ts +19 -4
  30. package/dist/server/slot.d.ts.map +1 -1
  31. package/dist/server/slot.js +3 -9
  32. package/dist/server/slot.js.map +1 -1
  33. package/dist/visitor.d.ts +16 -0
  34. package/dist/visitor.d.ts.map +1 -0
  35. package/dist/visitor.js +16 -0
  36. package/dist/visitor.js.map +1 -0
  37. package/package.json +3 -3
  38. package/src/assignments.ts +278 -0
  39. package/src/draft.ts +10 -8
  40. package/src/drafts.ts +38 -126
  41. package/src/index.ts +5 -2
  42. package/src/instrumentation/index.ts +3 -3
  43. package/src/proxy/index.ts +269 -56
  44. package/src/server/index.ts +152 -33
  45. package/src/server/slot.ts +18 -5
  46. package/src/visitor.ts +16 -0
package/README.md CHANGED
@@ -6,8 +6,8 @@ Peers: `@decocms/blocks` (same version), `next` 16, `react` 19; optionally `@ope
6
6
 
7
7
  | Import | What it is for |
8
8
  | --- | --- |
9
- | `@decocms/nextjs/proxy` | Draft previews in `proxy.ts`: `decideDraft`, `draftRequestHeaders`, `rewriteToDraftRoute`, `applyDraft`, `isDraftRoutePath`, or `draftProxy` for a site with no proxy of its own. No React and no block map. |
10
- | `@decocms/nextjs/server` | Draft previews in Server Components: `bindDraft`, `activeDraftPointer`, `contentClient`. |
9
+ | `@decocms/nextjs/proxy` | What a request reads, decided once in `proxy.ts`: `decideDraft`, `draftRequestHeaders`, `rewriteToDraftRoute`, `applyDraft`, `isDraftRoutePath`, `decideVisitor`, `visitorRequestHeaders`, `applyVisitor`, `decideAssignments`, `assignmentsRequestHeaders`, or `draftProxy` for a site with no proxy of its own; `assignmentsKey` for a site's own cache key; `rewriteToAssignmentsRoute`, `isAssignmentsRoutePath`, `assignmentsRoutePath` and `assignmentsSegment` for Next's own cache. No React and no block map. |
10
+ | `@decocms/nextjs/server` | What the proxy decided, in Server Components: `bindDraft`, `activeDraftPointer`, `contentClient`; `assignedClient` for a page on the assignments route. |
11
11
  | `@decocms/nextjs` | `DraftPreviewIndicator` (the "Preview mode" badge on a draft page), `StudioBridge` (lets Studio's site editor drive the page) and `StudioPage` (tells it which page is on screen), plus the draft names and Studio's origins (below). |
12
12
  | `@decocms/nextjs/config` | `withDeco(nextConfig)`: the draft header rules and Studio's dev origins. CommonJS, so `next.config.js` can `require()` it. |
13
13
  | `@decocms/nextjs/instrumentation` | `register`, the `instrumentation.ts` hook that records `http.server.request.duration` from Next's own request span, and `serverRequestSpanProcessor()` for a site with its own OpenTelemetry SDK. |
@@ -18,9 +18,9 @@ Peers: `@decocms/blocks` (same version), `next` 16, `react` 19; optionally `@ope
18
18
  ## Drafts
19
19
 
20
20
  ```ts
21
- // src/cms.config.ts: the options both CMS handles share, and where drafts preview
22
- export const cmsConfig = { content };
23
- export const preview = { site: "my-store" };
21
+ // src/cms.config.ts: the options both CMS handles share (with `site`, hosted releases and
22
+ // the preview hosts Studio writes into them)
23
+ export const cmsConfig = { content, site: "my-store" };
24
24
 
25
25
  // src/cms.ts and src/cms.proxy.ts
26
26
  export const cms = createCMS({ blocks, ...cmsConfig });
@@ -29,28 +29,95 @@ export const proxyCms = createCMS({ blocks: {}, ...cmsConfig }); // no block map
29
29
  // src/proxy.ts
30
30
  export async function proxy(request: NextRequest) {
31
31
  if (isDraftRoutePath(request.nextUrl.pathname)) return new NextResponse(null, { status: 404 });
32
- const draft = await decideDraft(proxyCms, request, preview);
32
+ const draft = await decideDraft(proxyCms, request);
33
+ const visitor = decideVisitor(request, draft);
34
+ const assignments = await decideAssignments(proxyCms, draft, visitor);
35
+ const headers = assignmentsRequestHeaders(
36
+ visitorRequestHeaders(draftRequestHeaders(request, draft), visitor),
37
+ assignments,
38
+ );
33
39
  const response =
34
- rewriteToDraftRoute(request, draft) ??
35
- NextResponse.next({ request: { headers: draftRequestHeaders(request, draft) } });
36
- return applyDraft(response, draft);
40
+ rewriteToDraftRoute(request, draft, visitor, assignments) ??
41
+ NextResponse.next({ request: { headers } });
42
+ return applyVisitor(applyDraft(response, draft), visitor);
37
43
  }
38
44
 
39
45
  // src/app/%5Fdraft/[[...path]]/page.tsx: the dynamic draft route (a literal `_draft` folder never routes)
40
46
  export const dynamic = "force-dynamic";
41
47
  export default async function DraftPage({ params, searchParams }) {
42
- await bindDraft(cms, { searchParams: await searchParams, ...preview });
48
+ await bindDraft(cms, { searchParams: await searchParams });
43
49
  const client = contentClient(cms);
44
50
  // … resolve and render the page as on the published route …
45
51
  return <>{page}<DraftPreviewIndicator /></>;
46
52
  }
47
53
  ```
48
54
 
49
- Published pages stay static: the proxy sends only drafted requests to the draft route, sets the draft cookie, forwards the accepted pointer on `x-deco-draft` (removing any copy sent from outside), and marks the response `Cache-Control: no-store, private` and `X-Robots-Tag: noindex, nofollow`. A root layout that reads content calls `bindDraft(cms, preview)` too: it reads the forwarded header.
55
+ Published pages stay cached (ISR, below): the proxy sends only drafted requests to the draft route, sets the draft cookie, forwards the accepted version on `x-deco-version` (removing any copy sent from outside), and marks the response `Cache-Control: no-store, private` and `X-Robots-Tag: noindex, nofollow`. A layout that reads content goes with its route, so the cached pages read no request: under the draft route (`app/%5Fdraft/layout.tsx`) it calls `bindDraft(cms)` and `contentClient(cms)`, which read the forwarded headers; under the assignments route (`app/%5Fassignments/[assignments]/layout.tsx`) it reads through `await assignedClient(cms, (await params).assignments)`. `bindDraft` reads `headers()`, and a cached page that reads a request header renders on every request instead, so never call it in the root layout or on the assignments route.
56
+
57
+ A draft link can carry a published release's commit SHA instead of a draft (Studio's "Preview this version"), and either one can carry a preview time (`__at`) and tests and campaigns switched on or off (`__test`, `__campaign`), which `cms.readVersion` reads as the client's `date`, `experiments` and `campaigns`. All of it passes the same host check and renders on the draft route: a visitor on the live site never picks a version. A preview may switch on a test at 0%.
58
+
59
+ ### The visitor, the assignments and the client
60
+
61
+ The proxy decides once per request. `decideVisitor(request, draft)` reads the visitor with `readVisitor` from `@decocms/blocks/experiments` (the `deco_segment` cookie, or a new visitor); `applyVisitor` sends its cookie on a first visit only, never from a preview. `decideAssignments(proxyCms, draft, visitor)` then decides each test and campaign on or off: `cms.client({ visitor, version, date, experiments, campaigns })` with the visitor and the Studio link's version, moment and switched tests and campaigns, resolving the saved `experiments` and `campaigns` blocks. `visitorRequestHeaders` and `assignmentsRequestHeaders` forward them on `x-deco-visitor` and `x-deco-assignments` (`draftRequestHeaders` always removes copies sent from outside).
62
+
63
+ On the page, `bindDraft` binds the version and what the proxy forwarded (on the current release it binds the visitor and the assignments only when the release saves an `experiments` or `campaigns` block, and a test at 0% stays off whatever the header says), and `contentClient(cms)` builds `cms.client({ version, experiments, campaigns })` from them: the page decides nothing again. The pages trust those headers: behind the proxy they hold what it decided, and a request that reaches a page without it (a `matcher` that leaves the route out, or a site without the proxy) is read the same way, with whatever `x-deco-version` and `x-deco-assignments` it arrives with. Which routes go through the proxy, and how the others are protected, is the site's call. A cached page reads no request: the CMS's pages render on [the assignments route](#the-assignments-route), with the tests and campaigns the proxy decided in the path.
64
+
65
+ The binding caches nothing. A site that caches pages in front of Next (a CDN keyed on a request header) keys on the request's assignments, never on the visitor: `assignmentsKey(assignments)` from `@decocms/nextjs/proxy`, with what `decideAssignments` gave, is the version, the tests on and the campaigns running (`""` when there are none). Visitors on the same side of every test share one entry, and a campaign starting or ending changes the key. To let Next's own cache do it, see [the assignments route](#the-assignments-route).
66
+
67
+ ### The assignments route
68
+
69
+ The proxy decides, the page renders that. Next caches a page by its path (ISR), so the proxy puts its decision in the path: it rewrites each CMS page to `/_assignments/<assignments>/<path>`, and the page renders exactly the tests and campaigns that segment names. Next then keeps one entry per path and set of assignments and serves it to every visitor with the same ones; the page reads no request, so Next can cache it. CMS pages are ISR with `revalidate = 600` (10 minutes): Next serves a stored page for 10 minutes; the first request after that still gets the stored copy while Next renders the page again in the background for the requests after it. So a published change shows within about 10 minutes on a page people open, and a page nobody opened for longer shows its old copy once more. That delay (for example, a banner switching on late) is accepted. Never `force-static` on Next.js: a page that never expires would keep what it rendered first.
70
+
71
+ ```ts
72
+ // src/proxy.ts
73
+ import { isAssignmentsRoutePath, isDraftRoutePath, rewriteToAssignmentsRoute /* … */ } from "@decocms/nextjs/proxy";
74
+
75
+ export async function proxy(request: NextRequest) {
76
+ const { pathname } = request.nextUrl;
77
+ if (isDraftRoutePath(pathname) || isAssignmentsRoutePath(pathname)) return new NextResponse(null, { status: 404 });
78
+ const draft = await decideDraft(proxyCms, request, preview);
79
+ const visitor = decideVisitor(request, draft);
80
+ const assigned = isCmsPage(pathname) ? await rewriteToAssignmentsRoute(proxyCms, request, draft, visitor) : null;
81
+ if (assigned) return applyVisitor(applyDraft(assigned, draft), visitor);
82
+ const assignments = await decideAssignments(proxyCms, draft, visitor);
83
+ const headers = assignmentsRequestHeaders(visitorRequestHeaders(draftRequestHeaders(request, draft), visitor), assignments);
84
+ const response = rewriteToDraftRoute(request, draft, visitor, assignments) ?? NextResponse.next({ request: { headers } });
85
+ return applyVisitor(applyDraft(response, draft), visitor);
86
+ }
87
+
88
+ // Next's own files never go through the proxy: rewritten, every script and style would be a 404.
89
+ export const config = { matcher: ["/((?!_next/|favicon.ico).*)"] };
90
+ ```
91
+
92
+ ```tsx
93
+ // src/app/%5Fassignments/[assignments]/[[...path]]/page.tsx: every CMS page (a literal `_assignments` folder never routes)
94
+ import { assignedClient } from "@decocms/nextjs/server";
95
+
96
+ export const revalidate = 600; // 10 minutes; never force-static
97
+ export async function generateStaticParams() {
98
+ return []; // no path at build time: each renders on its first request, then is cached
99
+ }
100
+ export default async function Page({ params }) {
101
+ const { assignments, path } = await params;
102
+ const client = await assignedClient(cms, assignments);
103
+ if (!client) notFound();
104
+ // … match the path and resolve the page through `client`, as on any page …
105
+ }
106
+ ```
107
+
108
+ - **What the segment holds**: every test of the saved `experiments` block on or off for the visitor and every campaign of the saved `campaigns` block running or not now (`readAssignments` on the current release), as `assignmentsSegment` writes it: `-` for none, otherwise base64url, one spelling per set of assignments. Visitors on the same side of every test get the same path; the visitor's id is never in it, and the rewritten request carries neither the visitor nor a draft header. A campaign starting or ending moves every visitor to a new path.
109
+ - **The page renders what the path names**: `await assignedClient(cms, segment)` builds `cms.client({ experiments, campaigns })` with every test and campaign as the segment names it and no visitor, a client fully decided in advance, so every visitor on that side renders the same page. The page never checks a campaign's dates or rolls a test itself. A test now at 0% stays off whatever the segment names. A page Next renders again later (revalidation) shows the same tests and campaigns under the same path.
110
+ - **The route trusts the path its proxy writes**: the proxy answers a direct hit on `/_assignments/…` with a 404, as it does for `/_draft/…`, so behind the proxy only its own rewrite reaches the route. A request that reaches the page some other way (a proxy `matcher` that leaves the route out, or a site without the proxy) renders the tests and campaigns its path names: `assignedClient` only keeps off a test at 0% and a test or a campaign the content doesn't have, and returns `null` (a 404) for a segment the proxy never writes. Which routes go through the proxy, and how the others are protected, is the site's call. Leaving off rather than answering 404 matters because Next caches a 404 under the path too: a path the proxy wrote before the page's server saw newly published content still renders.
111
+ - **The URL asked for stays private**: when the content saves tests or campaigns, the rewrite marks the response `Cache-Control: private, no-cache`. Next keeps one cached page per path and set of assignments, but `/shoes` itself answers by the visitor's cookie and the moment, so a CDN in front must not keep Next's ISR `s-maxage` under it.
112
+ - **A cached page reads no request**: `headers()`, `cookies()` or `searchParams` anywhere in the route (its layouts included, so no `bindDraft` there) make Next render it on every request instead of caching it, and a client component calling `useSearchParams()` renders on the client up to its nearest `<Suspense>`. `force-static` used to hide both by handing the page empty values; with ISR the page takes what it needs from the path, as the assignments do.
113
+ - **Drafts and previews are never rewritten here**: a drafted request goes to the dynamic draft route, so nothing from a preview lands in Next's cache.
114
+ - **Which paths**: only the CMS's pages (`isCmsPage` above is the site's own: routes of the app's own, such as `/blog` here, render as before). `draftProxy(proxyCms, { assignments: (request) => isCmsPage(request.nextUrl.pathname) })` does the same for a site with no proxy of its own.
115
+
116
+ `examples/nextjs-binding-smoke` in the repository runs it: two visitors on opposite sides of a test get different paths, two on the same side get one cached entry (`x-nextjs-cache: HIT`), the route is ISR at 600 seconds, and a typed `/_assignments/…` path is a 404.
50
117
 
51
118
  ### Draft hosts
52
119
 
53
- v7's allowlist. A draft link (`?__draft=`) previews on the preview hosts the site declares (the `CMS` block's `preview.hosts`, or `createCMS`'s; a site that declares none has none, as in v7) and always on the hosts Deco runs the site on, `<site>.deco.site` and `<site>.deco-cx.workers.dev`. `previewHosts: [...]` replaces the declared list (v7's `DECO_ALLOWED_PREVIEW_HOSTS`, such as `["localhost:3000"]` in development); `previewHosts: "none"` turns previews off on every host. Hosts compare exactly, port included, on the host the visitor asked for (`x-forwarded-host`, else `host`). Pass the same options to `decideDraft` and `bindDraft`. On a host that may not preview, the page renders the release; `?__draft=off` still ends a preview anywhere.
120
+ A draft link (`?__draft=`) previews on the hosts the CMS allows, which `cms.readVersion` checks: the addresses Studio writes into the current release's metadata (`previewHosts` in `latest.json`, read with `createCMS`'s `site`), plus `createCMS({ preview: { hosts } })`. The site lists nothing else. With no list, every host previews: a site with no `latest.json` on the CDN yet, or one published before Studio wrote `previewHosts` (its next publish writes the list), and content with no `site` unless `preview.hosts` lists hosts (such as `["localhost"]`). If `latest.json` can't be read (a network error, a 5xx, a malformed file), previews stay closed, besides `preview.hosts`, until a read succeeds. The host is the one the visitor asked for (`x-forwarded-host`, else `host`); the proxy and `bindDraft` ask the same CMS. On a host that may not preview, the page renders the release; `?__draft=off` still ends a preview anywhere.
54
121
 
55
122
  ## The layout
56
123
 
@@ -79,7 +146,7 @@ It renders a `<meta>` that React places in the `<head>`, so the page's own marku
79
146
 
80
147
  ## Names and Studio's origins
81
148
 
82
- For a site that needs them in its own code (a route that sets its own headers, a CSP it writes itself), `@decocms/nextjs` exports what the proxy, the pages and `withDeco` use: `DRAFT_PARAM` (`__draft`), `DRAFT_COOKIE` (`__deco_draft`), `DRAFT_HEADER` (`x-deco-draft`), `draftResponseHeaders(pointer)`, `STUDIO_ORIGINS`, `STUDIO_FRAME_ANCESTORS` and `isStudioOrigin(origin)`. They have the same values as `@decocms/tanstack`'s; each binding keeps its own copy and never imports the other.
149
+ For a site that needs them in its own code (a route that sets its own headers, a CSP it writes itself), `@decocms/nextjs` exports what the proxy, the pages and `withDeco` use: `DRAFT_PARAM` (`__draft`), `DRAFT_COOKIE` (`__deco_draft`), `VERSION_HEADER` (`x-deco-version`), `VISITOR_HEADER` (`x-deco-visitor`), `ASSIGNMENTS_HEADER` (`x-deco-assignments`), `draftResponseHeaders(version)`, `STUDIO_ORIGINS`, `STUDIO_FRAME_ANCESTORS` and `isStudioOrigin(origin)`. They have the same values as `@decocms/tanstack`'s; each binding keeps its own copy and never imports the other.
83
150
 
84
151
  ## next.config
85
152
 
@@ -104,7 +171,7 @@ Next.js already measures every request it serves: one root span per request (`Ba
104
171
  | `http.route` | Next's route template: `/[[...path]]`, `/blog`, `/_draft/[[...path]]`. An RSC request counts under its page's route. `unmatched` when Next names none (an Edge route under `next start`) |
105
172
  | `http.request.method` | `GET`, `POST`, …; any other is `_OTHER` |
106
173
  | `status_class` | `2xx` … `5xx`; a request that threw is `5xx` |
107
- | `cache` | `DRAFT` for a draft render (the proxy forwarded `x-deco-draft`, or the request was rewritten onto `/_draft`), otherwise `BYPASS`: Next decides its own cache (`x-nextjs-cache`) on the response, which the span never sees |
174
+ | `cache` | `DRAFT` for a draft render (the proxy forwarded `x-deco-version`, or the request was rewritten onto `/_draft`), otherwise `BYPASS`: Next decides its own cache (`x-nextjs-cache`) on the response, which the span never sees |
108
175
 
109
176
  - `@opentelemetry/api` is an optional peer: `register` uses the same copy Next does (yours when installed, else Next's compiled one) and does nothing if neither loads.
110
177
  - **Your own OpenTelemetry setup** (`@vercel/otel`, `NodeSDK`): only one tracer provider can be registered, so `register` leaves yours alone and logs how to combine them. Add the processor to yours instead: `registerOTel({ serviceName, spanProcessors: ["auto", serverRequestSpanProcessor()] })`.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * A request's assignments: what its content gives it, decided once, in the
3
+ * proxy, and handed to the page. The version it reads (a Studio link's draft
4
+ * or release SHA, `null` for the current release), each test of the saved
5
+ * `experiments` block on or off for its visitor, and each campaign of the
6
+ * saved `campaigns` block running or not at its date, with a preview's
7
+ * switched tests and campaigns on top. The page builds
8
+ * `cms.client({ version, experiments, campaigns })` from them, a client that
9
+ * decides nothing again.
10
+ *
11
+ * Two requests with the same assignments render the same content, so they
12
+ * share a cache entry: `assignmentsKey` for a cache keyed on a string,
13
+ * `assignmentsSegment` for Next's own path-keyed cache. The visitor id itself
14
+ * is never in either, or every visitor would miss. The binding caches nothing
15
+ * itself.
16
+ *
17
+ * The same reading as `@decocms/tanstack/worker`'s.
18
+ */
19
+ import type { ClientOptions, CMS } from "@decocms/blocks";
20
+ import type { Assignments } from "@decocms/blocks/experiments";
21
+ export interface RequestAssignments {
22
+ /** The version read, `null` for the current release. */
23
+ version: string | null;
24
+ /** Each test of the saved `experiments` block: on or off. */
25
+ experiments: Readonly<Assignments>;
26
+ /** Each campaign of the saved `campaigns` block: running or not. */
27
+ campaigns: Readonly<Assignments>;
28
+ }
29
+ /**
30
+ * The request header the proxy hands the pages the assignments it decided on
31
+ * (`assignmentsSegment`), for `bindDraft` and `contentClient`. The proxy
32
+ * removes any copy that arrives from outside.
33
+ */
34
+ export declare const ASSIGNMENTS_HEADER = "x-deco-assignments";
35
+ /** What a page's path and the decisions header carry: the tests and campaigns, never the version or the visitor. */
36
+ export type PathAssignments = Pick<RequestAssignments, "experiments" | "campaigns">;
37
+ /**
38
+ * The assignments of one request: `cms.client(options)` (the version, the
39
+ * visitor, the date and a preview's switched tests and campaigns, as
40
+ * `cms.readVersion` reads them) resolving `experiments` and `campaigns`.
41
+ * Missing blocks (or content that doesn't load) give none, never an error.
42
+ */
43
+ export declare function readAssignments(cms: Pick<CMS, "client">, options: ClientOptions): Promise<RequestAssignments>;
44
+ /**
45
+ * `readAssignments`, and whether its page can differ by them: `true` when the
46
+ * content saves tests or campaigns (or couldn't be read, to stay safe), so a
47
+ * response keyed on them must never be shared by its URL alone.
48
+ */
49
+ export declare function readAssignmentsAndScope(cms: Pick<CMS, "client">, options: ClientOptions): Promise<{
50
+ assignments: RequestAssignments;
51
+ varies: boolean;
52
+ }>;
53
+ /**
54
+ * The assignments as one stable string for a cache key:
55
+ * `v=<version>|x=<tests on>|c=<campaigns running>`, names sorted and
56
+ * URL-encoded, empty parts left out; `""` for none. The names that are off
57
+ * aren't in it: within one content revision they are the rest.
58
+ */
59
+ export declare function assignmentsKey(assignments: RequestAssignments): string;
60
+ /**
61
+ * The route a page renders on under the assignments its proxy made:
62
+ * `/_assignments/<assignments>/<path>`, mounted as `app/%5Fassignments/[assignments]/[[...path]]`
63
+ * (a literal `_assignments` folder is private in Next and never routes). The proxy
64
+ * rewrites to it (`rewriteToAssignmentsRoute`) and answers a direct hit with
65
+ * its 404, so a visitor can't pick the assignments by typing a path.
66
+ */
67
+ export declare const ASSIGNMENTS_ROUTE_PREFIX = "/_assignments";
68
+ /**
69
+ * The tests and campaigns as one path segment, every name with its state
70
+ * (`x=+on,-off|c=+running,-not`, base64url-encoded so Next passes it to the
71
+ * page unchanged), for Next's own path-keyed cache (ISR, the full route
72
+ * cache) and for the decisions header (`ASSIGNMENTS_HEADER`): `-` for none.
73
+ * The same assignments always give the same segment; the visitor is never in
74
+ * it.
75
+ */
76
+ export declare function assignmentsSegment(assignments: PathAssignments): string;
77
+ /**
78
+ * The tests and campaigns a segment carries, or `null` when it isn't one
79
+ * `assignmentsSegment` writes (malformed, or the same assignments spelled
80
+ * another way), so each set of assignments has exactly one path.
81
+ */
82
+ export declare function parseAssignmentsSegment(segment: string): PathAssignments | null;
83
+ /** `/_assignments/<segment><pathname>` (the bare prefix and segment for `/`). */
84
+ export declare function assignmentsRoutePath(pathname: string, assignments: PathAssignments): string;
85
+ /** Whether `pathname` is on the assignments route: a site's proxy answers a direct hit there with its 404. */
86
+ export declare function isAssignmentsRoutePath(pathname: string): boolean;
87
+ /**
88
+ * The tests a path or header names, as a page renders them on the current
89
+ * release: a test at 0% stays off whatever it says, so a forged path or
90
+ * header can't switch on a test nobody is in (a preview, on its own version,
91
+ * may). A name the content doesn't have stays out anyway (`cms.client`
92
+ * keeps only the block's names). Read from memory, unresolved.
93
+ */
94
+ export declare function withoutDarkTests(cms: Pick<CMS, "forRelease">, assignments: PathAssignments): Promise<PathAssignments>;
95
+ //# sourceMappingURL=assignments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignments.d.ts","sourceRoot":"","sources":["../src/assignments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAE/D,MAAM,WAAW,kBAAkB;IACjC,wDAAwD;IACxD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,6DAA6D;IAC7D,WAAW,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IACnC,oEAAoE;IACpE,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;CAClC;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,oHAAoH;AACpH,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,kBAAkB,EAAE,aAAa,GAAG,WAAW,CAAC,CAAC;AA4CpF;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,EACxB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,kBAAkB,CAAC,CAE7B;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,EACxB,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC;IAAE,WAAW,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC,CAqB/D;AASD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,kBAAkB,GAAG,MAAM,CAStE;AAED;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AA8BxD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,eAAe,GAAG,MAAM,CAOvE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAwB/E;AAED,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,eAAe,GAAG,MAAM,CAG3F;AAED,8GAA8G;AAC9G,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIhE;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,EAC5B,WAAW,EAAE,eAAe,GAC3B,OAAO,CAAC,eAAe,CAAC,CAmB1B"}
@@ -0,0 +1,229 @@
1
+ /**
2
+ * The request header the proxy hands the pages the assignments it decided on
3
+ * (`assignmentsSegment`), for `bindDraft` and `contentClient`. The proxy
4
+ * removes any copy that arrives from outside.
5
+ */
6
+ export const ASSIGNMENTS_HEADER = "x-deco-assignments";
7
+ const EXPERIMENTS = { block: "experiments", type: "split-traffic" };
8
+ const CAMPAIGNS = { block: "campaigns", type: "calendar" };
9
+ /** Plain code-unit order, the same on every server and locale. */
10
+ const byCodeUnit = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
11
+ function isRecord(value) {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+ function record() {
15
+ return Object.create(null);
16
+ }
17
+ /**
18
+ * Whether the content saves a block of `type`: listing never fails on a
19
+ * missing block, so a site without tests or campaigns reports no error.
20
+ */
21
+ async function saves(client, type) {
22
+ const [list] = await client.list(type);
23
+ return (list?.length ?? 0) > 0;
24
+ }
25
+ /** The saved `block` resolved, each name `true` or `false`; `null` when the content saves none. */
26
+ async function decided(client, spec) {
27
+ try {
28
+ if (!(await saves(client, spec.type)))
29
+ return null;
30
+ const [value] = await client.resolve(spec.block);
31
+ const out = record();
32
+ if (!isRecord(value))
33
+ return out;
34
+ for (const name of Object.keys(value).sort(byCodeUnit))
35
+ out[name] = value[name] === true;
36
+ return out;
37
+ }
38
+ catch {
39
+ return record();
40
+ }
41
+ }
42
+ /**
43
+ * The assignments of one request: `cms.client(options)` (the version, the
44
+ * visitor, the date and a preview's switched tests and campaigns, as
45
+ * `cms.readVersion` reads them) resolving `experiments` and `campaigns`.
46
+ * Missing blocks (or content that doesn't load) give none, never an error.
47
+ */
48
+ export async function readAssignments(cms, options) {
49
+ return (await readAssignmentsAndScope(cms, options)).assignments;
50
+ }
51
+ /**
52
+ * `readAssignments`, and whether its page can differ by them: `true` when the
53
+ * content saves tests or campaigns (or couldn't be read, to stay safe), so a
54
+ * response keyed on them must never be shared by its URL alone.
55
+ */
56
+ export async function readAssignmentsAndScope(cms, options) {
57
+ const version = typeof options.version === "string" ? options.version : null;
58
+ const none = { version, experiments: record(), campaigns: record() };
59
+ let client;
60
+ try {
61
+ client = cms.client(options);
62
+ }
63
+ catch {
64
+ return { assignments: none, varies: true };
65
+ }
66
+ const [experiments, campaigns] = await Promise.all([
67
+ decided(client, EXPERIMENTS),
68
+ decided(client, CAMPAIGNS),
69
+ ]);
70
+ return {
71
+ assignments: {
72
+ version,
73
+ experiments: experiments ?? none.experiments,
74
+ campaigns: campaigns ?? none.campaigns,
75
+ },
76
+ varies: experiments !== null || campaigns !== null,
77
+ };
78
+ }
79
+ /** The names that are on, sorted. */
80
+ function onNames(assignments) {
81
+ return Object.keys(assignments)
82
+ .filter((name) => assignments[name] === true)
83
+ .sort(byCodeUnit);
84
+ }
85
+ /**
86
+ * The assignments as one stable string for a cache key:
87
+ * `v=<version>|x=<tests on>|c=<campaigns running>`, names sorted and
88
+ * URL-encoded, empty parts left out; `""` for none. The names that are off
89
+ * aren't in it: within one content revision they are the rest.
90
+ */
91
+ export function assignmentsKey(assignments) {
92
+ const list = (names) => names.map(encodeURIComponent).join(",");
93
+ const parts = [];
94
+ if (assignments.version)
95
+ parts.push(`v=${encodeURIComponent(assignments.version)}`);
96
+ const tests = onNames(assignments.experiments);
97
+ const campaigns = onNames(assignments.campaigns);
98
+ if (tests.length)
99
+ parts.push(`x=${list(tests)}`);
100
+ if (campaigns.length)
101
+ parts.push(`c=${list(campaigns)}`);
102
+ return parts.join("|");
103
+ }
104
+ /**
105
+ * The route a page renders on under the assignments its proxy made:
106
+ * `/_assignments/<assignments>/<path>`, mounted as `app/%5Fassignments/[assignments]/[[...path]]`
107
+ * (a literal `_assignments` folder is private in Next and never routes). The proxy
108
+ * rewrites to it (`rewriteToAssignmentsRoute`) and answers a direct hit with
109
+ * its 404, so a visitor can't pick the assignments by typing a path.
110
+ */
111
+ export const ASSIGNMENTS_ROUTE_PREFIX = "/_assignments";
112
+ /** The segment for content with no test and no campaign. */
113
+ const NONE = "-";
114
+ function toBase64Url(text) {
115
+ let binary = "";
116
+ for (const byte of new TextEncoder().encode(text))
117
+ binary += String.fromCharCode(byte);
118
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
119
+ }
120
+ function fromBase64Url(segment) {
121
+ if (!/^[A-Za-z0-9_-]+$/.test(segment))
122
+ return null;
123
+ try {
124
+ const binary = atob(segment.replace(/-/g, "+").replace(/_/g, "/"));
125
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
126
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
127
+ }
128
+ catch {
129
+ return null;
130
+ }
131
+ }
132
+ /** `+name` on, `-name` off, names URL-encoded and sorted. */
133
+ function encodeStates(states) {
134
+ return Object.keys(states)
135
+ .sort(byCodeUnit)
136
+ .map((name) => `${states[name] === true ? "+" : "-"}${encodeURIComponent(name)}`)
137
+ .join(",");
138
+ }
139
+ /**
140
+ * The tests and campaigns as one path segment, every name with its state
141
+ * (`x=+on,-off|c=+running,-not`, base64url-encoded so Next passes it to the
142
+ * page unchanged), for Next's own path-keyed cache (ISR, the full route
143
+ * cache) and for the decisions header (`ASSIGNMENTS_HEADER`): `-` for none.
144
+ * The same assignments always give the same segment; the visitor is never in
145
+ * it.
146
+ */
147
+ export function assignmentsSegment(assignments) {
148
+ const parts = [];
149
+ const tests = encodeStates(assignments.experiments);
150
+ const campaigns = encodeStates(assignments.campaigns);
151
+ if (tests)
152
+ parts.push(`x=${tests}`);
153
+ if (campaigns)
154
+ parts.push(`c=${campaigns}`);
155
+ return parts.length === 0 ? NONE : toBase64Url(parts.join("|"));
156
+ }
157
+ /**
158
+ * The tests and campaigns a segment carries, or `null` when it isn't one
159
+ * `assignmentsSegment` writes (malformed, or the same assignments spelled
160
+ * another way), so each set of assignments has exactly one path.
161
+ */
162
+ export function parseAssignmentsSegment(segment) {
163
+ if (segment === NONE)
164
+ return { experiments: record(), campaigns: record() };
165
+ const key = fromBase64Url(segment);
166
+ if (key === null)
167
+ return null;
168
+ const parsed = { experiments: record(), campaigns: record() };
169
+ const seen = new Set();
170
+ try {
171
+ for (const part of key.split("|")) {
172
+ const eq = part.indexOf("=");
173
+ const tag = part.slice(0, eq);
174
+ const list = part.slice(eq + 1);
175
+ if (eq <= 0 || list === "" || seen.has(tag) || (tag !== "x" && tag !== "c"))
176
+ return null;
177
+ seen.add(tag);
178
+ const target = tag === "x" ? parsed.experiments : parsed.campaigns;
179
+ for (const entry of list.split(",")) {
180
+ const state = entry[0];
181
+ if (state !== "+" && state !== "-")
182
+ return null;
183
+ target[decodeURIComponent(entry.slice(1))] = state === "+";
184
+ }
185
+ }
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ return assignmentsSegment(parsed) === segment ? parsed : null;
191
+ }
192
+ /** `/_assignments/<segment><pathname>` (the bare prefix and segment for `/`). */
193
+ export function assignmentsRoutePath(pathname, assignments) {
194
+ const base = `${ASSIGNMENTS_ROUTE_PREFIX}/${assignmentsSegment(assignments)}`;
195
+ return pathname === "/" || pathname === "" ? base : `${base}${pathname}`;
196
+ }
197
+ /** Whether `pathname` is on the assignments route: a site's proxy answers a direct hit there with its 404. */
198
+ export function isAssignmentsRoutePath(pathname) {
199
+ return (pathname === ASSIGNMENTS_ROUTE_PREFIX || pathname.startsWith(`${ASSIGNMENTS_ROUTE_PREFIX}/`));
200
+ }
201
+ /**
202
+ * The tests a path or header names, as a page renders them on the current
203
+ * release: a test at 0% stays off whatever it says, so a forged path or
204
+ * header can't switch on a test nobody is in (a preview, on its own version,
205
+ * may). A name the content doesn't have stays out anyway (`cms.client`
206
+ * keeps only the block's names). Read from memory, unresolved.
207
+ */
208
+ export async function withoutDarkTests(cms, assignments) {
209
+ const on = Object.keys(assignments.experiments).filter((name) => assignments.experiments[name] === true);
210
+ if (on.length === 0)
211
+ return assignments;
212
+ let saved = {};
213
+ try {
214
+ const [block] = await cms.forRelease().resolve(EXPERIMENTS.block, { run: false });
215
+ if (isRecord(block) && block.__resolveType === EXPERIMENTS.type)
216
+ saved = block;
217
+ }
218
+ catch {
219
+ // No experiments block: every test the client is given stays out.
220
+ }
221
+ const experiments = record();
222
+ for (const name of Object.keys(assignments.experiments)) {
223
+ const pct = Object.hasOwn(saved, name) ? saved[name] : undefined;
224
+ const dark = typeof pct !== "number" || !Number.isFinite(pct) || Math.round(pct) <= 0;
225
+ experiments[name] = assignments.experiments[name] === true && !dark;
226
+ }
227
+ return { experiments, campaigns: assignments.campaigns };
228
+ }
229
+ //# sourceMappingURL=assignments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignments.js","sourceRoot":"","sources":["../src/assignments.ts"],"names":[],"mappings":"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"}
package/dist/draft.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The draft names and headers of a Deco Next.js site: the `?__draft=` link,
3
3
  * the cookie that keeps a preview going, the request header the proxy hands
4
- * the pages, and the response headers of a page rendered from a draft. The
4
+ * the pages the version on, and the response headers of a page rendered from a draft. The
5
5
  * proxy reads and writes them (`./drafts.ts`, `./proxy`); the pages read the
6
6
  * header (`./server`); `./config/index.cjs` repeats them (`config.test.ts`
7
7
  * holds them equal). This package's own copy: it never imports
@@ -20,18 +20,20 @@ export declare const DRAFT_PARAM = "__draft";
20
20
  export declare const DRAFT_COOKIE = "__deco_draft";
21
21
  /**
22
22
  * The request header the proxy sets on the request it hands the pages once it
23
- * accepted the draft: the pointer, already checked. The proxy removes any
24
- * copy that arrives from outside, and `bindDraft` repeats the host check, so a
25
- * client can't send one itself.
23
+ * accepted a Studio link: the version (a draft pointer or a release's commit
24
+ * SHA), already checked, without the preview's moment and switched tests
25
+ * (those are decided in the proxy: `ASSIGNMENTS_HEADER`). The proxy removes
26
+ * any copy that arrives from outside, and `bindDraft` repeats the host check,
27
+ * so a client can't send one itself.
26
28
  */
27
- export declare const DRAFT_HEADER = "x-deco-draft";
29
+ export declare const VERSION_HEADER = "x-deco-version";
28
30
  /** The URL prefix a drafted request is rewritten onto (`app/%5Fdraft/[[...path]]`). */
29
31
  export declare const DRAFT_ROUTE_PREFIX = "/_draft";
30
32
  /**
31
33
  * The response headers of a page rendered from a draft: never stored by a
32
34
  * browser or a CDN (`Cache-Control: no-store, private`, `Vary: Cookie`) and
33
- * never indexed (`X-Robots-Tag: noindex, nofollow`). `{}` when `pointer` is
35
+ * never indexed (`X-Robots-Tag: noindex, nofollow`). `{}` when `version` is
34
36
  * `null`, so the result of the draft decision applies as is.
35
37
  */
36
- export declare function draftResponseHeaders(pointer: string | null | undefined): Record<string, string>;
38
+ export declare function draftResponseHeaders(version: string | null | undefined): Record<string, string>;
37
39
  //# sourceMappingURL=draft.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO/F"}
1
+ {"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,YAAY,CAAC;AAErC;;;GAGG;AACH,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,mBAAmB,CAAC;AAE/C,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO/F"}
package/dist/draft.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The draft names and headers of a Deco Next.js site: the `?__draft=` link,
3
3
  * the cookie that keeps a preview going, the request header the proxy hands
4
- * the pages, and the response headers of a page rendered from a draft. The
4
+ * the pages the version on, and the response headers of a page rendered from a draft. The
5
5
  * proxy reads and writes them (`./drafts.ts`, `./proxy`); the pages read the
6
6
  * header (`./server`); `./config/index.cjs` repeats them (`config.test.ts`
7
7
  * holds them equal). This package's own copy: it never imports
@@ -20,21 +20,23 @@ export const DRAFT_PARAM = "__draft";
20
20
  export const DRAFT_COOKIE = "__deco_draft";
21
21
  /**
22
22
  * The request header the proxy sets on the request it hands the pages once it
23
- * accepted the draft: the pointer, already checked. The proxy removes any
24
- * copy that arrives from outside, and `bindDraft` repeats the host check, so a
25
- * client can't send one itself.
23
+ * accepted a Studio link: the version (a draft pointer or a release's commit
24
+ * SHA), already checked, without the preview's moment and switched tests
25
+ * (those are decided in the proxy: `ASSIGNMENTS_HEADER`). The proxy removes
26
+ * any copy that arrives from outside, and `bindDraft` repeats the host check,
27
+ * so a client can't send one itself.
26
28
  */
27
- export const DRAFT_HEADER = "x-deco-draft";
29
+ export const VERSION_HEADER = "x-deco-version";
28
30
  /** The URL prefix a drafted request is rewritten onto (`app/%5Fdraft/[[...path]]`). */
29
31
  export const DRAFT_ROUTE_PREFIX = "/_draft";
30
32
  /**
31
33
  * The response headers of a page rendered from a draft: never stored by a
32
34
  * browser or a CDN (`Cache-Control: no-store, private`, `Vary: Cookie`) and
33
- * never indexed (`X-Robots-Tag: noindex, nofollow`). `{}` when `pointer` is
35
+ * never indexed (`X-Robots-Tag: noindex, nofollow`). `{}` when `version` is
34
36
  * `null`, so the result of the draft decision applies as is.
35
37
  */
36
- export function draftResponseHeaders(pointer) {
37
- if (!pointer)
38
+ export function draftResponseHeaders(version) {
39
+ if (!version)
38
40
  return {};
39
41
  return {
40
42
  "Cache-Control": "no-store, private",
package/dist/draft.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"draft.js","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AAErC;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C,uFAAuF;AACvF,MAAM,CAAC,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAkC;IACrE,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO;QACL,eAAe,EAAE,mBAAmB;QACpC,IAAI,EAAE,QAAQ;QACd,cAAc,EAAE,mBAAmB;KACpC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"draft.js","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AAErC;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,cAAc,CAAC;AAE3C;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAC;AAE/C,uFAAuF;AACvF,MAAM,CAAC,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAkC;IACrE,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO;QACL,eAAe,EAAE,mBAAmB;QACpC,IAAI,EAAE,QAAQ;QACd,cAAc,EAAE,mBAAmB;KACpC,CAAC;AACJ,CAAC"}