@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.
- package/README.md +81 -14
- package/dist/assignments.d.ts +95 -0
- package/dist/assignments.d.ts.map +1 -0
- package/dist/assignments.js +229 -0
- package/dist/assignments.js.map +1 -0
- package/dist/draft.d.ts +9 -7
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +10 -8
- package/dist/draft.js.map +1 -1
- package/dist/drafts.d.ts +29 -60
- package/dist/drafts.d.ts.map +1 -1
- package/dist/drafts.js +38 -117
- package/dist/drafts.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/instrumentation/index.d.ts +1 -1
- package/dist/instrumentation/index.js +3 -3
- package/dist/instrumentation/index.js.map +1 -1
- package/dist/proxy/index.d.ts +134 -30
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +169 -37
- package/dist/proxy/index.js.map +1 -1
- package/dist/server/index.d.ts +59 -20
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +129 -25
- package/dist/server/index.js.map +1 -1
- package/dist/server/slot.d.ts +19 -4
- package/dist/server/slot.d.ts.map +1 -1
- package/dist/server/slot.js +3 -9
- package/dist/server/slot.js.map +1 -1
- package/dist/visitor.d.ts +16 -0
- package/dist/visitor.d.ts.map +1 -0
- package/dist/visitor.js +16 -0
- package/dist/visitor.js.map +1 -0
- package/package.json +3 -3
- package/src/assignments.ts +278 -0
- package/src/draft.ts +10 -8
- package/src/drafts.ts +38 -126
- package/src/index.ts +5 -2
- package/src/instrumentation/index.ts +3 -3
- package/src/proxy/index.ts +269 -56
- package/src/server/index.ts +152 -33
- package/src/server/slot.ts +18 -5
- package/src/visitor.ts +16 -0
package/src/drafts.ts
CHANGED
|
@@ -1,109 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Which hosts may render drafts:
|
|
3
|
-
*
|
|
4
|
-
* `
|
|
5
|
-
* `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
133
|
-
* first, then the draft cookie; `null` for `?__draft=off` and
|
|
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
|
|
56
|
+
export function readDraftVersion(request: RequestLike): RequestVersion | null {
|
|
137
57
|
const param = draftParam(request);
|
|
138
58
|
if (param === DRAFT_OFF) return null;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return
|
|
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
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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, "
|
|
104
|
+
cms: Pick<CMS, "readVersion">,
|
|
185
105
|
request: RequestLike,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
/**
|
|
227
|
-
|
|
228
|
-
|
|
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`, `
|
|
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,
|
|
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-
|
|
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 {
|
|
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,
|
|
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;
|