@decocms/nextjs 7.31.1 → 7.31.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/nextjs",
|
|
3
|
-
"version": "7.31.
|
|
3
|
+
"version": "7.31.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"lint:unused": "knip"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@decocms/blocks": "7.31.
|
|
33
|
-
"@decocms/blocks-admin": "7.31.
|
|
32
|
+
"@decocms/blocks": "7.31.3",
|
|
33
|
+
"@decocms/blocks-admin": "7.31.3"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"next": ">=15.0.0",
|
|
@@ -1,16 +1,67 @@
|
|
|
1
|
+
import { act } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
1
3
|
import { renderToString } from "react-dom/server";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
5
|
|
|
4
6
|
import { DraftPreviewBadge } from "./DraftPreviewBadge";
|
|
5
7
|
|
|
6
8
|
describe("DraftPreviewBadge (render)", () => {
|
|
7
|
-
|
|
9
|
+
// Raw SSR (react-dom/server) never runs effects — this is what a consumer
|
|
10
|
+
// site that never hydrates this component, or the server's own render
|
|
11
|
+
// pass, actually ships. The badge fails closed: nothing here, by design
|
|
12
|
+
// (see the module doc for why render-then-hide is worse).
|
|
13
|
+
it("renders nothing without a client to run the reveal effect", () => {
|
|
8
14
|
const html = renderToString(<DraftPreviewBadge pointer="fila.vtex.app@v1" />);
|
|
9
|
-
expect(html).
|
|
15
|
+
expect(html).toBe("");
|
|
10
16
|
});
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
describe("mounted in a browser", () => {
|
|
19
|
+
let container: HTMLDivElement;
|
|
20
|
+
let root: Root;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
container = document.createElement("div");
|
|
24
|
+
document.body.appendChild(container);
|
|
25
|
+
root = createRoot(container);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
act(() => {
|
|
30
|
+
root.unmount();
|
|
31
|
+
});
|
|
32
|
+
container.remove();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("reveals the preview-mode label after mount, once confirmed unframed", () => {
|
|
36
|
+
act(() => {
|
|
37
|
+
root.render(<DraftPreviewBadge pointer="fila.vtex.app@v1" />);
|
|
38
|
+
});
|
|
39
|
+
expect(container.textContent).toContain("Preview mode");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("embeds the deco mark inline (no external asset that could 404 on a consumer site)", () => {
|
|
43
|
+
act(() => {
|
|
44
|
+
root.render(<DraftPreviewBadge pointer="fila.vtex.app@v1" />);
|
|
45
|
+
});
|
|
46
|
+
expect(container.innerHTML).toContain("data:image/png;base64,");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("embedded in an iframe", () => {
|
|
50
|
+
const realTop = window.top;
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
Object.defineProperty(window, "top", {
|
|
53
|
+
value: realTop,
|
|
54
|
+
configurable: true,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("stays hidden — Studio's own preview surface already has its own chrome", () => {
|
|
59
|
+
Object.defineProperty(window, "top", { value: {}, configurable: true });
|
|
60
|
+
act(() => {
|
|
61
|
+
root.render(<DraftPreviewBadge pointer="fila.vtex.app@v1" />);
|
|
62
|
+
});
|
|
63
|
+
expect(container.textContent).toBe("");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
15
66
|
});
|
|
16
67
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import { buildExitUrl, buildShareUrl } from "./DraftPreviewBadge";
|
|
3
|
+
import { buildExitUrl, buildShareUrl, isFramed } from "./DraftPreviewBadge";
|
|
4
4
|
|
|
5
5
|
describe("buildShareUrl", () => {
|
|
6
6
|
it("pins the exact draft version so a recipient sees the same working tree", () => {
|
|
@@ -35,3 +35,21 @@ describe("buildExitUrl", () => {
|
|
|
35
35
|
);
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
describe("isFramed", () => {
|
|
40
|
+
it("is false for a same top window — the ordinary top-level tab case", () => {
|
|
41
|
+
const win = {} as typeof window;
|
|
42
|
+
win.top = win;
|
|
43
|
+
expect(isFramed(win)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("is true when top differs — embedded in another page's iframe", () => {
|
|
47
|
+
const win = {} as typeof window;
|
|
48
|
+
win.top = {} as typeof window; // a distinct object: the parent frame
|
|
49
|
+
expect(isFramed(win)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("fails open (not framed) with no window — SSR / no hydration", () => {
|
|
53
|
+
expect(isFramed(undefined)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -15,6 +15,22 @@
|
|
|
15
15
|
* the host site's CSS/Tailwind), and a very high z-index, so it renders the
|
|
16
16
|
* same on any consumer site. Only mounted when a draft is active
|
|
17
17
|
* (`DraftPreviewIndicator` gates it), so it never costs ordinary traffic.
|
|
18
|
+
*
|
|
19
|
+
* Hidden inside an iframe (`isFramed`): Studio's own preview surface embeds
|
|
20
|
+
* this exact draft render in an iframe that already has its own chrome
|
|
21
|
+
* (toolbar, version status) — floating a second "you're in preview" badge
|
|
22
|
+
* inside that frame would be redundant clutter, not a signal a reviewer
|
|
23
|
+
* needs. A reviewer following a shared preview link in their own top-level
|
|
24
|
+
* tab still sees it; only the embedded case is suppressed.
|
|
25
|
+
*
|
|
26
|
+
* Renders NOTHING until a client-side effect confirms it's safe to show —
|
|
27
|
+
* fails closed, not open. `isFramed()` needs `window`, so it can only ever
|
|
28
|
+
* be evaluated in the browser; rendering the badge by default and hiding it
|
|
29
|
+
* once framing is detected would flash it for a beat inside Studio's own
|
|
30
|
+
* iframe before it disappeared, and would hydration-mismatch (server has no
|
|
31
|
+
* `window`, so it would always render the "unframed" branch). Starting
|
|
32
|
+
* hidden on both the server and the client's matching first render, then
|
|
33
|
+
* revealing via `useEffect` only when confirmed unframed, avoids both.
|
|
18
34
|
*/
|
|
19
35
|
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
|
20
36
|
import { DECO_MARK_DATA_URI } from "./decoMark";
|
|
@@ -49,6 +65,19 @@ export function buildExitUrl(href: string): string {
|
|
|
49
65
|
return url.toString();
|
|
50
66
|
}
|
|
51
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Whether this render is embedded in another page's iframe — Studio's own
|
|
70
|
+
* preview surface, primarily. `window.top !== window` is a same-origin
|
|
71
|
+
* reference comparison, so it never throws cross-origin (unlike reading a
|
|
72
|
+
* cross-origin frame's properties). No `window` reports not-framed — a safe
|
|
73
|
+
* default for the function itself, though the badge only ever calls this
|
|
74
|
+
* from a `useEffect`, where `window` always exists. Pure and exported for
|
|
75
|
+
* tests.
|
|
76
|
+
*/
|
|
77
|
+
export function isFramed(win: typeof window | undefined = globalThis.window): boolean {
|
|
78
|
+
return typeof win !== "undefined" && win.top !== win;
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
const DECO_GREEN = "#0b3d1e";
|
|
53
82
|
/** Toast background — the deco lime. Text sits on it in dark green for contrast. */
|
|
54
83
|
const DECO_LIME = "#d0ec1a";
|
|
@@ -99,8 +128,18 @@ function ShareIcon() {
|
|
|
99
128
|
export function DraftPreviewBadge({ pointer }: DraftPreviewBadgeProps) {
|
|
100
129
|
const [open, setOpen] = useState(false);
|
|
101
130
|
const [copied, setCopied] = useState(false);
|
|
131
|
+
const [visible, setVisible] = useState(false);
|
|
102
132
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
103
133
|
|
|
134
|
+
// Reveal only after mount, and only when confirmed unframed — fails
|
|
135
|
+
// closed. Server and client's matching first render both start at this
|
|
136
|
+
// same `false`, so there's no hydration mismatch; this effect (client
|
|
137
|
+
// only, runs once) is the sole place `isFramed()` is ever evaluated. See
|
|
138
|
+
// the module doc for why the alternative (render then hide) is worse.
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!isFramed()) setVisible(true);
|
|
141
|
+
}, []);
|
|
142
|
+
|
|
104
143
|
// Dismiss on outside click or Escape — standard popover behaviour, and it
|
|
105
144
|
// keeps the badge from trapping focus on a reviewer's own page.
|
|
106
145
|
useEffect(() => {
|
|
@@ -137,6 +176,8 @@ export function DraftPreviewBadge({ pointer }: DraftPreviewBadgeProps) {
|
|
|
137
176
|
}
|
|
138
177
|
}, [pointer]);
|
|
139
178
|
|
|
179
|
+
if (!visible) return null;
|
|
180
|
+
|
|
140
181
|
return (
|
|
141
182
|
<div
|
|
142
183
|
ref={rootRef}
|