@decocms/nextjs 8.5.2 → 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/src/drafts.ts CHANGED
@@ -1,109 +1,29 @@
1
1
  /**
2
- * Which hosts may render drafts: v7's allowlist (`@decocms/blocks` 7.x
3
- * `cms/draftSource.ts`: `readAllowedHosts`, `isDraftHostAllowed`,
4
- * `isDraftPreviewEnabled`), ported as is. v7 read its override from the
5
- * `DECO_ALLOWED_PREVIEW_HOSTS` variable; here it is the `previewHosts` option,
6
- * and the site passes the same options to the proxy (`decideDraft`) and to
7
- * its pages (`bindDraft`).
2
+ * Which hosts may render drafts: the ones the CMS allows (`cms.readVersion`):
3
+ * the addresses Studio writes into the current release's metadata, plus
4
+ * `createCMS`'s `preview.hosts`; every host while the site has published no
5
+ * list, or with no `site` and no `preview.hosts`. The site declares nothing
6
+ * here (PO 2026-10-22). The proxy
7
+ * (`decideDraft`) and the pages (`bindDraft`) ask the same CMS.
8
8
  *
9
- * - The site's own preview hosts: the ones it declares, in the `CMS` block or
10
- * as `createCMS({ preview: { hosts } })` (matched as `cms.draftPointer`
11
- * matches them). A site that declares none has none, as in v7: blocks'
12
- * own default (every host) doesn't count as a declaration.
13
- * - The hosts Deco runs the site on, named after the site and always added:
14
- * `<site>.deco.site` and `<site>.deco-cx.workers.dev`, exact.
15
- * - `previewHosts: [...]` replaces the site's own list (the inferred hosts are
16
- * still added); `previewHosts: "none"` turns previews off on every host.
17
- *
18
- * Hosts compare exactly, lowercased, port included (`localhost:3000`). The
19
- * host is the one the visitor asked for: `x-forwarded-host` behind a proxy,
20
- * else `host`.
9
+ * The host is the one the visitor asked for: `x-forwarded-host` behind a
10
+ * proxy, else `host`.
21
11
  *
22
12
  * The draft itself is the capability: a host check only bounds where a draft
23
13
  * link works. A request the check refuses gets the release, never an error.
24
14
  *
15
+ * What a link carries is a version, as `cms.readVersion` reads it: a draft
16
+ * pointer, or a published release's commit SHA (Studio's "Preview this
17
+ * version"), with the preview's moment and switched tests and campaigns
18
+ * (`__at`, `__test`, `__campaign`; `parseVersion` from `@decocms/blocks`).
19
+ * Both pass the same host check: a visitor on the live site never picks a
20
+ * version.
21
+ *
25
22
  * No React, no Next.js and no module state: the proxy and the pages share it.
26
23
  */
27
- import { type CMS, parseDraftPointer, type RequestLike } from "@decocms/blocks";
24
+ import { type CMS, parseVersion, type RequestLike, type RequestVersion } from "@decocms/blocks";
28
25
  import { DRAFT_COOKIE, DRAFT_PARAM } from "./draft.ts";
29
26
 
30
- export interface PreviewPolicy {
31
- /** The site's name: `<site>.deco.site` and `<site>.deco-cx.workers.dev` may preview. */
32
- site?: string;
33
- /** Replaces the `CMS` block's preview hosts; `"none"` turns previews off everywhere. */
34
- previewHosts?: readonly string[] | "none";
35
- }
36
-
37
- /** The hosts Deco runs `site` on, which may always preview (unless previews are off). */
38
- export function inferredPreviewHosts(site: string | undefined): string[] {
39
- const s = (site ?? "").trim().toLowerCase();
40
- return s ? [`${s}.deco.site`, `${s}.deco-cx.workers.dev`] : [];
41
- }
42
-
43
- function overrideHosts(policy: PreviewPolicy): string[] {
44
- if (policy.previewHosts === "none") return ["none"];
45
- return (policy.previewHosts ?? []).map((h) => h.trim().toLowerCase()).filter(Boolean);
46
- }
47
-
48
- /**
49
- * Whether `host` may preview without asking the CMS: `true` (an override or
50
- * inferred host), `false` (previews off, or the override leaves it out), or
51
- * `"cms"` when only the site's declared preview hosts can say (none declared:
52
- * `false`, see {@link declaresPreviewHosts}).
53
- */
54
- export function previewHostDecision(host: string, policy: PreviewPolicy): boolean | "cms" {
55
- const override = overrideHosts(policy);
56
- if (override.includes("none")) return false;
57
- const h = host.trim().toLowerCase();
58
- if (h === "") return false;
59
- if (inferredPreviewHosts(policy.site).includes(h)) return true;
60
- if (policy.previewHosts !== undefined) return override.includes(h);
61
- return "cms";
62
- }
63
-
64
- /**
65
- * Whether any host may preview (v7's `isDraftPreviewEnabled`): a page checks
66
- * it before it reads the request's headers, so a site with previews off never
67
- * turns a static page dynamic. `false` for `previewHosts: "none"`; `true` with
68
- * a site name or an override list; otherwise whether the site declares
69
- * preview hosts and they allow any.
70
- */
71
- export async function isDraftPreviewEnabled(
72
- cms: Pick<CMS, "settings" | "forRelease">,
73
- policy: PreviewPolicy,
74
- ): Promise<boolean> {
75
- const override = overrideHosts(policy);
76
- if (override.includes("none")) return false;
77
- if (inferredPreviewHosts(policy.site).length > 0) return true;
78
- if (policy.previewHosts !== undefined) return override.length > 0;
79
- return (await declaresPreviewHosts(cms)) && (await cms.settings()).preview.hosts.length > 0;
80
- }
81
-
82
- /**
83
- * Whether the site declares its own preview hosts: in the `CMS` block, or as
84
- * `createCMS`'s `preview.hosts`. When it declares none, `cms.settings()`
85
- * reports blocks' default, every host (`["*"]`), which v7 never had: there,
86
- * a site without a list previewed only on its Deco hosts. The saved `CMS`
87
- * block tells an explicit `["*"]` from that default.
88
- */
89
- export async function declaresPreviewHosts(
90
- cms: Pick<CMS, "settings" | "forRelease">,
91
- ): Promise<boolean> {
92
- const { hosts } = (await cms.settings()).preview;
93
- if (hosts.length !== 1 || hosts[0] !== "*") return true;
94
- try {
95
- const [block] = await cms.forRelease().resolve<unknown>("CMS", { run: false });
96
- const preview = isRecord(block) ? block.preview : undefined;
97
- return isRecord(preview) && ("hosts" in preview || "__resolveType" in preview);
98
- } catch {
99
- return false;
100
- }
101
- }
102
-
103
- function isRecord(value: unknown): value is Record<string, unknown> {
104
- return typeof value === "object" && value !== null && !Array.isArray(value);
105
- }
106
-
107
27
  const DRAFT_OFF = "off";
108
28
 
109
29
  function draftParam(request: RequestLike): string | null {
@@ -129,16 +49,16 @@ function readCookie(header: string | null, name: string): string | null {
129
49
  }
130
50
 
131
51
  /**
132
- * The pointer a request carries, as `@decocms/blocks` reads it: `?__draft=`
133
- * first, then the draft cookie; `null` for `?__draft=off` and for a value
134
- * that doesn't parse.
52
+ * What a request's Studio link asks for, as `@decocms/blocks` reads it:
53
+ * `?__draft=` first, then the draft cookie; `null` for `?__draft=off` and
54
+ * for a value that doesn't parse.
135
55
  */
136
- export function readDraftPointer(request: RequestLike): string | null {
56
+ export function readDraftVersion(request: RequestLike): RequestVersion | null {
137
57
  const param = draftParam(request);
138
58
  if (param === DRAFT_OFF) return null;
139
- if (param && parseDraftPointer(param) !== null) return param;
140
- const cookie = readCookie(request.headers.get("cookie"), DRAFT_COOKIE);
141
- return cookie && parseDraftPointer(cookie) !== null ? cookie : null;
59
+ const fromLink = param ? parseVersion(param) : null;
60
+ if (fromLink !== null) return fromLink;
61
+ return parseVersion(readCookie(request.headers.get("cookie"), DRAFT_COOKIE));
142
62
  }
143
63
 
144
64
  /** Whether the request carries a draft signal at all: without one the CMS is never asked. */
@@ -176,29 +96,17 @@ export function requestLike(
176
96
  }
177
97
 
178
98
  /**
179
- * The draft `request` renders under `policy`, or `null` for the release: the
180
- * pointer it carries, on a host the policy (or, when only it can say, the
181
- * `CMS` block) allows.
99
+ * What `request` renders (a draft pointer or a release's commit SHA, with
100
+ * the preview's moment and switched tests and campaigns), or `null` for the
101
+ * current release: what its Studio link carries, on a host the CMS allows.
182
102
  */
183
103
  export async function allowedDraft(
184
- cms: Pick<CMS, "draftPointer" | "settings" | "forRelease">,
104
+ cms: Pick<CMS, "readVersion">,
185
105
  request: RequestLike,
186
- policy: PreviewPolicy,
187
- ): Promise<string | null> {
188
- const pointer = readDraftPointer(request);
189
- if (pointer === null) return null;
190
- let host: string;
191
- try {
192
- host = new URL(request.url).host;
193
- } catch {
194
- return null;
195
- }
196
- const decision = previewHostDecision(host, policy);
197
- const allowed =
198
- decision === "cms"
199
- ? (await declaresPreviewHosts(cms)) && (await cms.draftPointer(request)) !== null
200
- : decision;
201
- return allowed ? pointer : null;
106
+ ): Promise<RequestVersion | null> {
107
+ const read = readDraftVersion(request);
108
+ if (read === null) return null;
109
+ return (await cms.readVersion(request)) === null ? null : read;
202
110
  }
203
111
 
204
112
  /**
@@ -223,7 +131,11 @@ export function endsPreview(request: RequestLike): boolean {
223
131
  return draftParam(request) === DRAFT_OFF;
224
132
  }
225
133
 
226
- /** Whether the request's own `?__draft=` carries `pointer` (a draft link, not the cookie). */
227
- export function fromDraftLink(request: RequestLike, pointer: string): boolean {
228
- return draftParam(request) === pointer;
134
+ /**
135
+ * The request's own `?__draft=` value when it is a version (a draft link,
136
+ * not the cookie): the value its cookie stores; `null` otherwise.
137
+ */
138
+ export function draftLinkValue(request: RequestLike): string | null {
139
+ const param = draftParam(request);
140
+ return param && param !== DRAFT_OFF && parseVersion(param) !== null ? param : null;
229
141
  }
package/src/index.ts CHANGED
@@ -11,7 +11,8 @@
11
11
  * Outside Studio's frame, "." opens the page in Studio's editor.
12
12
  * - `StudioPage`: rendered inside each CMS page, `<StudioPage pathTemplate={page.path} />`,
13
13
  * tells the bridge which page "." opens (the root layout doesn't re-render per page).
14
- * - `DRAFT_PARAM`, `DRAFT_COOKIE`, `DRAFT_HEADER`, `draftResponseHeaders`,
14
+ * - `DRAFT_PARAM`, `DRAFT_COOKIE`, `VERSION_HEADER`, `draftResponseHeaders`,
15
+ * `VISITOR_HEADER`, `ASSIGNMENTS_HEADER`,
15
16
  * `STUDIO_ORIGINS`, `STUDIO_FRAME_ANCESTORS`, `isStudioOrigin`: for a site
16
17
  * that needs them in its own code (a route that sets its own headers, a CSP
17
18
  * it writes itself). The package's own copy: it never imports
@@ -23,10 +24,12 @@
23
24
  */
24
25
 
25
26
  export { DraftBadge, StudioPage, type StudioPageInfo } from "@decocms/blocks/preview";
27
+ export { ASSIGNMENTS_HEADER } from "./assignments.ts";
26
28
  export {
27
29
  DraftPreviewIndicator,
28
30
  type DraftPreviewIndicatorProps,
29
31
  } from "./DraftPreviewIndicator.tsx";
30
- export { DRAFT_COOKIE, DRAFT_HEADER, DRAFT_PARAM, draftResponseHeaders } from "./draft.ts";
32
+ export { DRAFT_COOKIE, DRAFT_PARAM, draftResponseHeaders, VERSION_HEADER } from "./draft.ts";
31
33
  export { StudioBridge, type StudioBridgeProps } from "./StudioBridge.ts";
32
34
  export { isStudioOrigin, STUDIO_FRAME_ANCESTORS, STUDIO_ORIGINS } from "./studio.ts";
35
+ export { VISITOR_HEADER } from "./visitor.ts";
@@ -24,7 +24,7 @@
24
24
  * Labels: `http.route` is Next's route template (`unmatched` when Next names
25
25
  * none); RSC requests count under their page's route, as the requests they
26
26
  * are. `cache` is `DRAFT` for a draft render (the proxy forwarded
27
- * `x-deco-draft`, or the request was rewritten onto `/_draft`) and otherwise
27
+ * `x-deco-version`, or the request was rewritten onto `/_draft`) and otherwise
28
28
  * `BYPASS`: Next decides its cache (`x-nextjs-cache`) on the response, which
29
29
  * the span never sees.
30
30
  */
@@ -40,7 +40,7 @@ import type {
40
40
  Tracer,
41
41
  TracerProvider,
42
42
  } from "@opentelemetry/api";
43
- import { DRAFT_HEADER, DRAFT_ROUTE_PREFIX } from "../draft.ts";
43
+ import { DRAFT_ROUTE_PREFIX, VERSION_HEADER } from "../draft.ts";
44
44
 
45
45
  type Api = typeof import("@opentelemetry/api");
46
46
 
@@ -235,7 +235,7 @@ function draftPropagator(DRAFT: symbol): TextMapPropagator {
235
235
  inject() {},
236
236
  extract(context, carrier, getter) {
237
237
  try {
238
- const value = getter.get(carrier, DRAFT_HEADER);
238
+ const value = getter.get(carrier, VERSION_HEADER);
239
239
  return value !== undefined && value !== "" ? context.setValue(DRAFT, true) : context;
240
240
  } catch {
241
241
  return context;