@decocms/nextjs 8.13.0 → 8.15.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 CHANGED
@@ -130,7 +130,7 @@ import { StudioBridge } from "@decocms/nextjs";
130
130
  </body>
131
131
  ```
132
132
 
133
- Studio's origins are the package's own (`STUDIO_ORIGINS`), so the bridge takes only the site's name. v7's "." edit shortcut is on by default (`editShortcut={false}` turns it off), and `nonce` is optional.
133
+ Studio's origins are the package's own, so the bridge takes only the site's name. v7's "." edit shortcut is on by default (`editShortcut={false}` turns it off), and `nonce` is optional. The bridge is v8 only: v7's framed handler (`scrollToComponent`, `editor::rerender`) is gone, so no script on the page compares the window with `top` or `parent`, and a test runner that rewrites "frame-busting" code in the HTML it proxies (Cypress's `modifyObstructiveCode`) serves the page byte for byte.
134
134
 
135
135
  The bridge also mounts the Studio channel on every page, production included: a small inline script (about 0.7 kB gzipped) through which Studio loads its own editor code (the section wireframe, the page swap after a save) into the page when it frames it. It is the only way Studio runs code in the page: the bridge evaluates no script Studio sends. The page trusts a list of exact Studio origins: `studioOrigins` when given, else the `DECO_STUDIO_ORIGIN` environment variable, comma-separated (a local Studio, such as `http://localhost:3000`), else Studio's production and staging origins (`DEFAULT_STUDIO_ORIGINS`). Framed, the page posts `{ type: "deco:ready" }` with each of them as the target origin (only a parent at one of them receives it) and imports a module only when Studio's answer, `{ type: "deco:load", url }`, comes from exactly one of them and names a URL on the origin of that message; not framed, or framed by any other origin, it does nothing. A site with a Content Security Policy passes it through `withStudioCsp(directives)`, which adds the same list to `script-src` (and a `frame-ancestors` with Studio's, when the policy has none): `headers: [{ source: "/:path*", headers: [{ key: "Content-Security-Policy", value: withStudioCsp("script-src 'self' 'unsafe-inline'; object-src 'none'").join("; ") }] }]`, or the same in the site's proxy. The inline scripts take the site's `nonce`. The message and module types are in `@decocms/blocks/protocol`.
136
136
 
@@ -1,18 +1,40 @@
1
1
  /**
2
- * `StudioBridge` of `@decocms/blocks/preview` with Studio's origins filled in
3
- * (`STUDIO_ORIGINS`, `./studio.ts`): the package owns where Studio runs, so a
4
- * site passes only its name. With it, the Studio channel (`StudioChannel`):
5
- * on every page, production included, framed by one of Studio's origins
6
- * (`studioOrigins` / `DECO_STUDIO_ORIGIN` / Studio's production and staging
7
- * origins, each exact) it loads the editor's own modules from that origin;
8
- * otherwise it does nothing. Two inline scripts and no hooks, so a Server
9
- * Component renders it as is. The site's CSP allows the same origins with
10
- * `withStudioCsp`.
2
+ * What a Next.js site renders for Deco Studio's site editor: the Studio
3
+ * channel (`StudioChannel` of `@decocms/blocks/preview`) and v7's "." edit
4
+ * shortcut (`./studioShortcut.ts`). On every page, production included,
5
+ * framed by one of Studio's origins (`studioOrigins` / `DECO_STUDIO_ORIGIN` /
6
+ * Studio's production and staging origins, each exact), the channel loads
7
+ * the editor's own modules from that origin; otherwise it does nothing. Not
8
+ * framed, "." opens the page in Studio's editor.
9
+ *
10
+ * v8 only: v7's framed handler (`scrollToComponent`, `editor::rerender`) is
11
+ * gone, so no script on the page compares the window with `top` or `parent`
12
+ * in a form a test runner's "frame-busting" rewrite matches (Cypress's
13
+ * `modifyObstructiveCode`, which would change the length of the React Server
14
+ * Components payload that carries the script). Two inline scripts and no
15
+ * hooks, so a Server Component renders it as is. The site's CSP allows the
16
+ * same origins with `withStudioCsp`.
11
17
  */
12
- import { type StudioBridgeProps as BridgeScriptProps } from "@decocms/blocks/preview";
18
+ import { type StudioPageInfo } from "@decocms/blocks/preview";
13
19
  import { type ReactNode } from "react";
14
- /** `site`, and optionally `editShortcut`, `page` and `nonce`; Studio's origins are the package's. */
15
- export interface StudioBridgeProps extends Omit<BridgeScriptProps, "trustedOrigins"> {
20
+ /** `site`, and optionally `editShortcut`, `page`, `nonce` and `studioOrigins`. */
21
+ export interface StudioBridgeProps {
22
+ /** The site's name in Deco (the "." shortcut sends it to Studio). */
23
+ site: string;
24
+ /**
25
+ * v7's editor shortcut: on a page that isn't framed, "." (or Ctrl+Shift+E)
26
+ * opens it in Studio, and Ctrl or Cmd+"." opens it in a new tab. On by
27
+ * default, as in v7; `false` turns it off.
28
+ */
29
+ editShortcut?: boolean;
30
+ /**
31
+ * The page being shown, when the layout knows it: the "." shortcut sends
32
+ * its path template (and id). A `StudioPage` rendered by the page wins (a
33
+ * Next.js root layout doesn't re-render per page).
34
+ */
35
+ page?: StudioPageInfo;
36
+ /** The CSP nonce for the inline scripts, when the site sends one. */
37
+ nonce?: string;
16
38
  /**
17
39
  * The Studio origins the Studio channel trusts, each an exact origin.
18
40
  * Default: `DECO_STUDIO_ORIGIN` (comma-separated), else Studio's production
@@ -23,7 +45,8 @@ export interface StudioBridgeProps extends Omit<BridgeScriptProps, "trustedOrigi
23
45
  }
24
46
  /**
25
47
  * Lets Studio's site editor select and edit sections on the page when Studio
26
- * frames it. Render it once, in the root layout's `<body>`.
48
+ * frames it, and "." open the page in Studio when it doesn't. Render it once,
49
+ * in the root layout's `<body>`.
27
50
  */
28
- export declare function StudioBridge({ studioOrigins: origins, ...props }: StudioBridgeProps): ReactNode;
51
+ export declare function StudioBridge({ site, editShortcut, page, nonce, studioOrigins: origins, }: StudioBridgeProps): ReactNode;
29
52
  //# sourceMappingURL=StudioBridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"StudioBridge.d.ts","sourceRoot":"","sources":["../src/StudioBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAEL,KAAK,iBAAiB,IAAI,iBAAiB,EAE5C,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAA2B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGhE,qGAAqG;AACrG,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,iBAAiB,EAAE,gBAAgB,CAAC;IAClF;;;;;OAKG;IACH,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,KAAK,EAAE,EAAE,iBAAiB,GAAG,SAAS,CAO/F"}
1
+ {"version":3,"file":"StudioBridge.d.ts","sourceRoot":"","sources":["../src/StudioBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC7E,OAAO,EAA2B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIhE,kFAAkF;AAClF,MAAM,WAAW,iBAAiB;IAChC,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,qEAAqE;IACrE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,aAAa,EAAE,OAAO,GACvB,EAAE,iBAAiB,GAAG,SAAS,CAO/B"}
@@ -1,22 +1,30 @@
1
1
  /**
2
- * `StudioBridge` of `@decocms/blocks/preview` with Studio's origins filled in
3
- * (`STUDIO_ORIGINS`, `./studio.ts`): the package owns where Studio runs, so a
4
- * site passes only its name. With it, the Studio channel (`StudioChannel`):
5
- * on every page, production included, framed by one of Studio's origins
6
- * (`studioOrigins` / `DECO_STUDIO_ORIGIN` / Studio's production and staging
7
- * origins, each exact) it loads the editor's own modules from that origin;
8
- * otherwise it does nothing. Two inline scripts and no hooks, so a Server
9
- * Component renders it as is. The site's CSP allows the same origins with
10
- * `withStudioCsp`.
2
+ * What a Next.js site renders for Deco Studio's site editor: the Studio
3
+ * channel (`StudioChannel` of `@decocms/blocks/preview`) and v7's "." edit
4
+ * shortcut (`./studioShortcut.ts`). On every page, production included,
5
+ * framed by one of Studio's origins (`studioOrigins` / `DECO_STUDIO_ORIGIN` /
6
+ * Studio's production and staging origins, each exact), the channel loads
7
+ * the editor's own modules from that origin; otherwise it does nothing. Not
8
+ * framed, "." opens the page in Studio's editor.
9
+ *
10
+ * v8 only: v7's framed handler (`scrollToComponent`, `editor::rerender`) is
11
+ * gone, so no script on the page compares the window with `top` or `parent`
12
+ * in a form a test runner's "frame-busting" rewrite matches (Cypress's
13
+ * `modifyObstructiveCode`, which would change the length of the React Server
14
+ * Components payload that carries the script). Two inline scripts and no
15
+ * hooks, so a Server Component renders it as is. The site's CSP allows the
16
+ * same origins with `withStudioCsp`.
11
17
  */
12
- import { StudioBridge as BridgeScript, StudioChannel, } from "@decocms/blocks/preview";
18
+ import { StudioChannel } from "@decocms/blocks/preview";
13
19
  import { createElement, Fragment } from "react";
14
- import { STUDIO_ORIGINS, studioOrigins } from "./studio.js";
20
+ import { studioOrigins } from "./studio.js";
21
+ import { StudioShortcut } from "./studioShortcut.js";
15
22
  /**
16
23
  * Lets Studio's site editor select and edit sections on the page when Studio
17
- * frames it. Render it once, in the root layout's `<body>`.
24
+ * frames it, and "." open the page in Studio when it doesn't. Render it once,
25
+ * in the root layout's `<body>`.
18
26
  */
19
- export function StudioBridge({ studioOrigins: origins, ...props }) {
20
- return createElement(Fragment, null, createElement(BridgeScript, { ...props, trustedOrigins: STUDIO_ORIGINS }), createElement(StudioChannel, { studioOrigins: studioOrigins(origins), nonce: props.nonce }));
27
+ export function StudioBridge({ site, editShortcut, page, nonce, studioOrigins: origins, }) {
28
+ return createElement(Fragment, null, createElement(StudioShortcut, { site, editShortcut, page, nonce }), createElement(StudioChannel, { studioOrigins: studioOrigins(origins), nonce }));
21
29
  }
22
30
  //# sourceMappingURL=StudioBridge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"StudioBridge.js","sourceRoot":"","sources":["../src/StudioBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,YAAY,IAAI,YAAY,EAE5B,aAAa,GACd,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAChE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAa5D;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,KAAK,EAAqB;IAClF,OAAO,aAAa,CAClB,QAAQ,EACR,IAAI,EACJ,aAAa,CAAC,YAAY,EAAE,EAAE,GAAG,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,CAAC,EACzE,aAAa,CAAC,aAAa,EAAE,EAAE,aAAa,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAC5F,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"StudioBridge.js","sourceRoot":"","sources":["../src/StudioBridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,aAAa,EAAuB,MAAM,yBAAyB,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA6BrD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,aAAa,EAAE,OAAO,GACJ;IAClB,OAAO,aAAa,CAClB,QAAQ,EACR,IAAI,EACJ,aAAa,CAAC,cAAc,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAClE,aAAa,CAAC,aAAa,EAAE,EAAE,aAAa,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,CAC/E,CAAC;AACJ,CAAC"}
package/dist/index.d.ts CHANGED
@@ -5,12 +5,11 @@
5
5
  * - `DraftPreviewIndicator`: the "Preview mode" badge on a page rendered from
6
6
  * a draft (`bindDraft` from `@decocms/nextjs/server`), outside Studio's frame.
7
7
  * - `StudioBridge`: one line in the root layout, `<StudioBridge site="my-store" />`,
8
- * lets Studio's site editor drive the page when Studio frames it
9
- * (`@decocms/blocks/preview`'s, answering `STUDIO_ORIGINS`). It renders an
10
- * inline script with no React hooks, so a Server Component renders it as is.
11
- * Outside Studio's frame, "." opens the page in Studio's editor. It also
12
- * mounts the Studio channel (`StudioChannel`), through which Studio loads its
13
- * own modules into the page when it frames it (`studioOrigins`).
8
+ * lets Studio's site editor drive the page when Studio frames it: it mounts
9
+ * the Studio channel (`StudioChannel` of `@decocms/blocks/preview`), through
10
+ * which Studio loads its own modules into the page (`studioOrigins`).
11
+ * Outside Studio's frame, "." opens the page in Studio's editor. Two inline
12
+ * scripts with no React hooks, so a Server Component renders it as is.
14
13
  * - `StudioPage`: rendered inside each CMS page, `<StudioPage pathTemplate={page.path} />`,
15
14
  * tells the bridge which page "." opens (the root layout doesn't re-render per page).
16
15
  * - `DRAFT_PARAM`, `DRAFT_COOKIE`, `VERSION_HEADER`, `draftResponseHeaders`,
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,KAAK,gBAAgB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAChE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7F,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,sBAAsB,EACtB,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,KAAK,gBAAgB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAChE,OAAO,EACL,qBAAqB,EACrB,KAAK,0BAA0B,GAChC,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7F,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,sBAAsB,EACtB,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -5,12 +5,11 @@
5
5
  * - `DraftPreviewIndicator`: the "Preview mode" badge on a page rendered from
6
6
  * a draft (`bindDraft` from `@decocms/nextjs/server`), outside Studio's frame.
7
7
  * - `StudioBridge`: one line in the root layout, `<StudioBridge site="my-store" />`,
8
- * lets Studio's site editor drive the page when Studio frames it
9
- * (`@decocms/blocks/preview`'s, answering `STUDIO_ORIGINS`). It renders an
10
- * inline script with no React hooks, so a Server Component renders it as is.
11
- * Outside Studio's frame, "." opens the page in Studio's editor. It also
12
- * mounts the Studio channel (`StudioChannel`), through which Studio loads its
13
- * own modules into the page when it frames it (`studioOrigins`).
8
+ * lets Studio's site editor drive the page when Studio frames it: it mounts
9
+ * the Studio channel (`StudioChannel` of `@decocms/blocks/preview`), through
10
+ * which Studio loads its own modules into the page (`studioOrigins`).
11
+ * Outside Studio's frame, "." opens the page in Studio's editor. Two inline
12
+ * scripts with no React hooks, so a Server Component renders it as is.
14
13
  * - `StudioPage`: rendered inside each CMS page, `<StudioPage pathTemplate={page.path} />`,
15
14
  * tells the bridge which page "." opens (the root layout doesn't re-render per page).
16
15
  * - `DRAFT_PARAM`, `DRAFT_COOKIE`, `VERSION_HEADER`, `draftResponseHeaders`,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAuB,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAyB,aAAa,EAAE,MAAM,UAAU,CAAC;AAChE,OAAO,EACL,qBAAqB,GAEtB,MAAM,4BAA6B,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7F,OAAO,EAAE,YAAY,EAA0B,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,sBAAsB,EACtB,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAuB,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAyB,aAAa,EAAE,MAAM,UAAU,CAAC;AAChE,OAAO,EACL,qBAAqB,GAEtB,MAAM,4BAA6B,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7F,OAAO,EAAE,YAAY,EAA0B,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,sBAAsB,EACtB,aAAa,EACb,cAAc,EACd,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
package/dist/studio.d.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Where Deco Studio runs: the origins its site editor frames a site from and
3
- * talks to it from (`postMessage`). One list, so the Studio bridge
4
- * (`StudioBridge`), `withDeco()`'s dev origins and a site's own
5
- * `frame-ancestors` never drift apart. This package's own copy: it never
3
+ * talks to it from (`postMessage`). One list, so `withDeco()`'s dev origins
4
+ * and a site's own `frame-ancestors` never drift apart. This package's own copy: it never
6
5
  * imports `@decocms/tanstack`.
7
6
  *
8
7
  * No module state and no framework code: safe in a browser bundle.
@@ -1 +1 @@
1
- {"version":3,"file":"studio.d.ts","sourceRoot":"","sources":["../src/studio.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAK1C,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAIlD,CAAC;AAEH,kCAAkC;AAClC,eAAO,MAAM,aAAa,+BAA+B,CAAC;AAE1D;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAGlD,CAAC;AAkBH;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAK3E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAazE"}
1
+ {"version":3,"file":"studio.d.ts","sourceRoot":"","sources":["../src/studio.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAK1C,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAIlD,CAAC;AAEH,kCAAkC;AAClC,eAAO,MAAM,aAAa,+BAA+B,CAAC;AAE1D;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAGlD,CAAC;AAkBH;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAK3E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAazE"}
package/dist/studio.js CHANGED
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Where Deco Studio runs: the origins its site editor frames a site from and
3
- * talks to it from (`postMessage`). One list, so the Studio bridge
4
- * (`StudioBridge`), `withDeco()`'s dev origins and a site's own
5
- * `frame-ancestors` never drift apart. This package's own copy: it never
3
+ * talks to it from (`postMessage`). One list, so `withDeco()`'s dev origins
4
+ * and a site's own `frame-ancestors` never drift apart. This package's own copy: it never
6
5
  * imports `@decocms/tanstack`.
7
6
  *
8
7
  * No module state and no framework code: safe in a browser bundle.
@@ -1 +1 @@
1
- {"version":3,"file":"studio.js","sourceRoot":"","sources":["../src/studio.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsB,MAAM,CAAC,MAAM,CAAC;IAC7D,4BAA4B;IAC5B,gCAAgC;IAChC,qBAAqB;IACrB,uBAAuB;CACxB,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAsB,MAAM,CAAC,MAAM,CAAC;IACrE,QAAQ;IACR,GAAG,cAAc;IACjB,uBAAuB;CACxB,CAAC,CAAC;AAEH,kCAAkC;AAClC,MAAM,CAAC,MAAM,aAAa,GAAG,4BAA4B,CAAC;AAE1D;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAsB,MAAM,CAAC,MAAM,CAAC;IACrE,aAAa;IACb,gCAAgC;CACjC,CAAC,CAAC;AAEH,gHAAgH;AAChH,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;YAAE,OAAO,EAAE,CAAC;QACrE,4EAA4E;QAC5E,uCAAuC;QACvC,IAAI,CAAC,kCAAkC,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAC;QACtE,OAAO,OAAO,KAAK,GAAG,CAAC,MAAM,IAAI,OAAO,KAAK,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAClF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,MAA0B;IACtD,MAAM,GAAG,GAAI,UAAyE,CAAC,OAAO;QAC5F,EAAE,GAAG,EAAE,kBAAkB,CAAC;IAC5B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC;IAC/E,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAiC;IAC9D,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAC9D,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,8EAA8E;IAC9E,yBAAyB;IACzB,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IACxF,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;IAC1B,OAAO,IAAI,KAAK,aAAa,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;AACjE,CAAC"}
1
+ {"version":3,"file":"studio.js","sourceRoot":"","sources":["../src/studio.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAsB,MAAM,CAAC,MAAM,CAAC;IAC7D,4BAA4B;IAC5B,gCAAgC;IAChC,qBAAqB;IACrB,uBAAuB;CACxB,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAsB,MAAM,CAAC,MAAM,CAAC;IACrE,QAAQ;IACR,GAAG,cAAc;IACjB,uBAAuB;CACxB,CAAC,CAAC;AAEH,kCAAkC;AAClC,MAAM,CAAC,MAAM,aAAa,GAAG,4BAA4B,CAAC;AAE1D;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAsB,MAAM,CAAC,MAAM,CAAC;IACrE,aAAa;IACb,gCAAgC;CACjC,CAAC,CAAC;AAEH,gHAAgH;AAChH,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;YAAE,OAAO,EAAE,CAAC;QACrE,4EAA4E;QAC5E,uCAAuC;QACvC,IAAI,CAAC,kCAAkC,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAC;QACtE,OAAO,OAAO,KAAK,GAAG,CAAC,MAAM,IAAI,OAAO,KAAK,GAAG,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAClF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,MAA0B;IACtD,MAAM,GAAG,GAAI,UAAyE,CAAC,OAAO;QAC5F,EAAE,GAAG,EAAE,kBAAkB,CAAC;IAC5B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC;IAC/E,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,MAAiC;IAC9D,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IAC9D,IAAI,GAAQ,CAAC;IACb,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,8EAA8E;IAC9E,yBAAyB;IACzB,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;IACxF,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;IAC1B,OAAO,IAAI,KAAK,aAAa,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;AACjE,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * v7's "." edit shortcut, alone: on a page that isn't framed, "." (or
3
+ * Ctrl+Shift+E) opens the page in Studio's editor (`/choose-editor`, which
4
+ * signs a visitor who isn't signed in in first), and Ctrl or Cmd+"." opens it
5
+ * in a new tab. In a frame it does nothing: Studio's editor reaches a framed
6
+ * page through the Studio channel (`StudioChannel`) only.
7
+ *
8
+ * Safe under a test runner that rewrites "frame-busting" code in the HTML it
9
+ * proxies (Cypress's `modifyObstructiveCode`): the script compares no window
10
+ * with `top` or `parent` in a form those rewrites match, so the HTML reaches
11
+ * the browser byte for byte (a rewrite changes the length of a React Server
12
+ * Components payload that carries the script and breaks the page).
13
+ *
14
+ * A key typed in a field is left alone, also in a field inside an open shadow
15
+ * root (a third-party widget's form): its events reach the document with the
16
+ * host as their target, so the script reads the field from the event's
17
+ * composed path.
18
+ *
19
+ * The page it sends: the last `StudioPage` marker the page rendered, else the
20
+ * `page` the binding passed, read at the keypress. An inline script with no
21
+ * hooks, so a Server Component renders it; its settings travel in `data-*`
22
+ * attributes (the same as v7's bridge's: `data-deco-studio-bridge`,
23
+ * `data-site`, `data-shortcut`, `data-path-template`, `data-page-id`) and the
24
+ * script itself is static.
25
+ *
26
+ * `@decocms/nextjs` and `@decocms/tanstack` each carry this file (neither
27
+ * imports the other); tests/studio-shortcut.test.ts keeps the two the same.
28
+ */
29
+ import { type ReactNode } from "react";
30
+ /** The page the "." shortcut opens in Studio. */
31
+ interface ShortcutPage {
32
+ /** The page block's `path`, as `matchRoute` matched it (`/products/:slug/p`, `/*`). */
33
+ pathTemplate?: string;
34
+ /** The page block's id, when the site knows it. */
35
+ id?: string;
36
+ }
37
+ export interface StudioShortcutProps {
38
+ /** The site's name in Deco (the shortcut sends it to Studio). */
39
+ site: string;
40
+ /** On by default; `false` turns it off. */
41
+ editShortcut?: boolean;
42
+ /** The page on screen, when the component that renders the shortcut knows it. */
43
+ page?: ShortcutPage;
44
+ /** The CSP nonce for the inline script, when the site sends one. */
45
+ nonce?: string;
46
+ }
47
+ /** The browser side. Static: everything it needs comes from the script element's attributes. */
48
+ export declare const STUDIO_SHORTCUT_SCRIPT = "(function(){\nvar s=document.currentScript,w=window,p=w.parent;\nif(!s||p!==w||w.__decoStudioShortcut||!s.hasAttribute(\"data-shortcut\"))return;\nw.__decoStudioShortcut=true;\nvar site=s.getAttribute(\"data-site\")||\"\";\nfunction page(){\n var all=document.querySelectorAll(\"[data-deco-page]\"),el=all.length?all[all.length-1]:null;\n if(!el||!el.getAttribute(\"data-path-template\")){el=document.querySelector(\"script[data-deco-studio-bridge]\")||s}\n return{t:el.getAttribute(\"data-path-template\")||\"\",id:el.getAttribute(\"data-page-id\")||\"\"};\n}\ndocument.addEventListener(\"keydown\",function(e){\n var t=e.composedPath?e.composedPath()[0]:e.target;\n if(e.defaultPrevented||(t&&(t.tagName===\"INPUT\"||t.tagName===\"TEXTAREA\"||t.tagName===\"SELECT\"||t.isContentEditable)))return;\n if(e.key!==\".\"&&!(e.ctrlKey&&e.shiftKey&&e.key===\"E\"))return;\n e.preventDefault();\n e.stopPropagation();\n var g=page(),h=new URL(\"https://studio.decocms.com/choose-editor\");\n h.searchParams.set(\"site\",site);\n h.searchParams.set(\"domain\",location.origin);\n if(g.id)h.searchParams.set(\"pageId\",g.id);\n h.searchParams.set(\"path\",location.pathname+location.search);\n h.searchParams.set(\"pathTemplate\",g.t||\"/*\");\n if((e.ctrlKey||e.metaKey)&&e.key===\".\"){w.open(h.toString(),\"_blank\");return}\n location.href=h.toString();\n});\n})();";
49
+ /** The "." shortcut's script. Render it once, in the `<body>`. */
50
+ export declare function StudioShortcut({ site, editShortcut, page, nonce, }: StudioShortcutProps): ReactNode;
51
+ export {};
52
+ //# sourceMappingURL=studioShortcut.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studioShortcut.d.ts","sourceRoot":"","sources":["../src/studioShortcut.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEtD,iDAAiD;AACjD,UAAU,YAAY;IACpB,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iFAAiF;IACjF,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,gGAAgG;AAChG,eAAO,MAAM,sBAAsB,s2CAyB7B,CAAC;AAEP,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,YAAmB,EACnB,IAAI,EACJ,KAAK,GACN,EAAE,mBAAmB,GAAG,SAAS,CAejC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * v7's "." edit shortcut, alone: on a page that isn't framed, "." (or
3
+ * Ctrl+Shift+E) opens the page in Studio's editor (`/choose-editor`, which
4
+ * signs a visitor who isn't signed in in first), and Ctrl or Cmd+"." opens it
5
+ * in a new tab. In a frame it does nothing: Studio's editor reaches a framed
6
+ * page through the Studio channel (`StudioChannel`) only.
7
+ *
8
+ * Safe under a test runner that rewrites "frame-busting" code in the HTML it
9
+ * proxies (Cypress's `modifyObstructiveCode`): the script compares no window
10
+ * with `top` or `parent` in a form those rewrites match, so the HTML reaches
11
+ * the browser byte for byte (a rewrite changes the length of a React Server
12
+ * Components payload that carries the script and breaks the page).
13
+ *
14
+ * A key typed in a field is left alone, also in a field inside an open shadow
15
+ * root (a third-party widget's form): its events reach the document with the
16
+ * host as their target, so the script reads the field from the event's
17
+ * composed path.
18
+ *
19
+ * The page it sends: the last `StudioPage` marker the page rendered, else the
20
+ * `page` the binding passed, read at the keypress. An inline script with no
21
+ * hooks, so a Server Component renders it; its settings travel in `data-*`
22
+ * attributes (the same as v7's bridge's: `data-deco-studio-bridge`,
23
+ * `data-site`, `data-shortcut`, `data-path-template`, `data-page-id`) and the
24
+ * script itself is static.
25
+ *
26
+ * `@decocms/nextjs` and `@decocms/tanstack` each carry this file (neither
27
+ * imports the other); tests/studio-shortcut.test.ts keeps the two the same.
28
+ */
29
+ import { createElement } from "react";
30
+ /** The browser side. Static: everything it needs comes from the script element's attributes. */
31
+ export const STUDIO_SHORTCUT_SCRIPT = `(function(){
32
+ var s=document.currentScript,w=window,p=w.parent;
33
+ if(!s||p!==w||w.__decoStudioShortcut||!s.hasAttribute("data-shortcut"))return;
34
+ w.__decoStudioShortcut=true;
35
+ var site=s.getAttribute("data-site")||"";
36
+ function page(){
37
+ var all=document.querySelectorAll("[data-deco-page]"),el=all.length?all[all.length-1]:null;
38
+ if(!el||!el.getAttribute("data-path-template")){el=document.querySelector("script[data-deco-studio-bridge]")||s}
39
+ return{t:el.getAttribute("data-path-template")||"",id:el.getAttribute("data-page-id")||""};
40
+ }
41
+ document.addEventListener("keydown",function(e){
42
+ var t=e.composedPath?e.composedPath()[0]:e.target;
43
+ if(e.defaultPrevented||(t&&(t.tagName==="INPUT"||t.tagName==="TEXTAREA"||t.tagName==="SELECT"||t.isContentEditable)))return;
44
+ if(e.key!=="."&&!(e.ctrlKey&&e.shiftKey&&e.key==="E"))return;
45
+ e.preventDefault();
46
+ e.stopPropagation();
47
+ var g=page(),h=new URL("https://studio.decocms.com/choose-editor");
48
+ h.searchParams.set("site",site);
49
+ h.searchParams.set("domain",location.origin);
50
+ if(g.id)h.searchParams.set("pageId",g.id);
51
+ h.searchParams.set("path",location.pathname+location.search);
52
+ h.searchParams.set("pathTemplate",g.t||"/*");
53
+ if((e.ctrlKey||e.metaKey)&&e.key==="."){w.open(h.toString(),"_blank");return}
54
+ location.href=h.toString();
55
+ });
56
+ })();`;
57
+ /** The "." shortcut's script. Render it once, in the `<body>`. */
58
+ export function StudioShortcut({ site, editShortcut = true, page, nonce, }) {
59
+ return createElement("script", {
60
+ "data-deco-studio-bridge": "",
61
+ "data-site": site,
62
+ "data-shortcut": editShortcut ? "" : undefined,
63
+ // Read on each keypress, so a client-side navigation that re-renders the
64
+ // script with the next page updates them.
65
+ "data-path-template": page?.pathTemplate || undefined,
66
+ "data-page-id": page?.id || undefined,
67
+ nonce,
68
+ // Browsers hide a nonce from the DOM after parsing, which React would
69
+ // report as a hydration mismatch.
70
+ suppressHydrationWarning: true,
71
+ dangerouslySetInnerHTML: { __html: STUDIO_SHORTCUT_SCRIPT },
72
+ });
73
+ }
74
+ //# sourceMappingURL=studioShortcut.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"studioShortcut.js","sourceRoot":"","sources":["../src/studioShortcut.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,aAAa,EAAkB,MAAM,OAAO,CAAC;AAqBtD,gGAAgG;AAChG,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;MAyBhC,CAAC;AAEP,kEAAkE;AAClE,MAAM,UAAU,cAAc,CAAC,EAC7B,IAAI,EACJ,YAAY,GAAG,IAAI,EACnB,IAAI,EACJ,KAAK,GACe;IACpB,OAAO,aAAa,CAAC,QAAQ,EAAE;QAC7B,yBAAyB,EAAE,EAAE;QAC7B,WAAW,EAAE,IAAI;QACjB,eAAe,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS;QAC9C,yEAAyE;QACzE,0CAA0C;QAC1C,oBAAoB,EAAE,IAAI,EAAE,YAAY,IAAI,SAAS;QACrD,cAAc,EAAE,IAAI,EAAE,EAAE,IAAI,SAAS;QACrC,KAAK;QACL,sEAAsE;QACtE,kCAAkC;QAClC,wBAAwB,EAAE,IAAI;QAC9B,uBAAuB,EAAE,EAAE,MAAM,EAAE,sBAAsB,EAAE;KAC5D,CAAC,CAAC;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/nextjs",
3
- "version": "8.13.0",
3
+ "version": "8.15.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -67,7 +67,7 @@
67
67
  }
68
68
  },
69
69
  "devDependencies": {
70
- "@decocms/blocks": "8.13.0",
70
+ "@decocms/blocks": "8.15.0",
71
71
  "@opentelemetry/api": "^1.9.1",
72
72
  "@types/react": "^19.0.0",
73
73
  "@types/react-dom": "^19.0.0",
@@ -1,24 +1,43 @@
1
1
  /**
2
- * `StudioBridge` of `@decocms/blocks/preview` with Studio's origins filled in
3
- * (`STUDIO_ORIGINS`, `./studio.ts`): the package owns where Studio runs, so a
4
- * site passes only its name. With it, the Studio channel (`StudioChannel`):
5
- * on every page, production included, framed by one of Studio's origins
6
- * (`studioOrigins` / `DECO_STUDIO_ORIGIN` / Studio's production and staging
7
- * origins, each exact) it loads the editor's own modules from that origin;
8
- * otherwise it does nothing. Two inline scripts and no hooks, so a Server
9
- * Component renders it as is. The site's CSP allows the same origins with
10
- * `withStudioCsp`.
2
+ * What a Next.js site renders for Deco Studio's site editor: the Studio
3
+ * channel (`StudioChannel` of `@decocms/blocks/preview`) and v7's "." edit
4
+ * shortcut (`./studioShortcut.ts`). On every page, production included,
5
+ * framed by one of Studio's origins (`studioOrigins` / `DECO_STUDIO_ORIGIN` /
6
+ * Studio's production and staging origins, each exact), the channel loads
7
+ * the editor's own modules from that origin; otherwise it does nothing. Not
8
+ * framed, "." opens the page in Studio's editor.
9
+ *
10
+ * v8 only: v7's framed handler (`scrollToComponent`, `editor::rerender`) is
11
+ * gone, so no script on the page compares the window with `top` or `parent`
12
+ * in a form a test runner's "frame-busting" rewrite matches (Cypress's
13
+ * `modifyObstructiveCode`, which would change the length of the React Server
14
+ * Components payload that carries the script). Two inline scripts and no
15
+ * hooks, so a Server Component renders it as is. The site's CSP allows the
16
+ * same origins with `withStudioCsp`.
11
17
  */
12
- import {
13
- StudioBridge as BridgeScript,
14
- type StudioBridgeProps as BridgeScriptProps,
15
- StudioChannel,
16
- } from "@decocms/blocks/preview";
18
+ import { StudioChannel, type StudioPageInfo } from "@decocms/blocks/preview";
17
19
  import { createElement, Fragment, type ReactNode } from "react";
18
- import { STUDIO_ORIGINS, studioOrigins } from "./studio.ts";
20
+ import { studioOrigins } from "./studio.ts";
21
+ import { StudioShortcut } from "./studioShortcut.ts";
19
22
 
20
- /** `site`, and optionally `editShortcut`, `page` and `nonce`; Studio's origins are the package's. */
21
- export interface StudioBridgeProps extends Omit<BridgeScriptProps, "trustedOrigins"> {
23
+ /** `site`, and optionally `editShortcut`, `page`, `nonce` and `studioOrigins`. */
24
+ export interface StudioBridgeProps {
25
+ /** The site's name in Deco (the "." shortcut sends it to Studio). */
26
+ site: string;
27
+ /**
28
+ * v7's editor shortcut: on a page that isn't framed, "." (or Ctrl+Shift+E)
29
+ * opens it in Studio, and Ctrl or Cmd+"." opens it in a new tab. On by
30
+ * default, as in v7; `false` turns it off.
31
+ */
32
+ editShortcut?: boolean;
33
+ /**
34
+ * The page being shown, when the layout knows it: the "." shortcut sends
35
+ * its path template (and id). A `StudioPage` rendered by the page wins (a
36
+ * Next.js root layout doesn't re-render per page).
37
+ */
38
+ page?: StudioPageInfo;
39
+ /** The CSP nonce for the inline scripts, when the site sends one. */
40
+ nonce?: string;
22
41
  /**
23
42
  * The Studio origins the Studio channel trusts, each an exact origin.
24
43
  * Default: `DECO_STUDIO_ORIGIN` (comma-separated), else Studio's production
@@ -30,13 +49,20 @@ export interface StudioBridgeProps extends Omit<BridgeScriptProps, "trustedOrigi
30
49
 
31
50
  /**
32
51
  * Lets Studio's site editor select and edit sections on the page when Studio
33
- * frames it. Render it once, in the root layout's `<body>`.
52
+ * frames it, and "." open the page in Studio when it doesn't. Render it once,
53
+ * in the root layout's `<body>`.
34
54
  */
35
- export function StudioBridge({ studioOrigins: origins, ...props }: StudioBridgeProps): ReactNode {
55
+ export function StudioBridge({
56
+ site,
57
+ editShortcut,
58
+ page,
59
+ nonce,
60
+ studioOrigins: origins,
61
+ }: StudioBridgeProps): ReactNode {
36
62
  return createElement(
37
63
  Fragment,
38
64
  null,
39
- createElement(BridgeScript, { ...props, trustedOrigins: STUDIO_ORIGINS }),
40
- createElement(StudioChannel, { studioOrigins: studioOrigins(origins), nonce: props.nonce }),
65
+ createElement(StudioShortcut, { site, editShortcut, page, nonce }),
66
+ createElement(StudioChannel, { studioOrigins: studioOrigins(origins), nonce }),
41
67
  );
42
68
  }
package/src/index.ts CHANGED
@@ -5,12 +5,11 @@
5
5
  * - `DraftPreviewIndicator`: the "Preview mode" badge on a page rendered from
6
6
  * a draft (`bindDraft` from `@decocms/nextjs/server`), outside Studio's frame.
7
7
  * - `StudioBridge`: one line in the root layout, `<StudioBridge site="my-store" />`,
8
- * lets Studio's site editor drive the page when Studio frames it
9
- * (`@decocms/blocks/preview`'s, answering `STUDIO_ORIGINS`). It renders an
10
- * inline script with no React hooks, so a Server Component renders it as is.
11
- * Outside Studio's frame, "." opens the page in Studio's editor. It also
12
- * mounts the Studio channel (`StudioChannel`), through which Studio loads its
13
- * own modules into the page when it frames it (`studioOrigins`).
8
+ * lets Studio's site editor drive the page when Studio frames it: it mounts
9
+ * the Studio channel (`StudioChannel` of `@decocms/blocks/preview`), through
10
+ * which Studio loads its own modules into the page (`studioOrigins`).
11
+ * Outside Studio's frame, "." opens the page in Studio's editor. Two inline
12
+ * scripts with no React hooks, so a Server Component renders it as is.
14
13
  * - `StudioPage`: rendered inside each CMS page, `<StudioPage pathTemplate={page.path} />`,
15
14
  * tells the bridge which page "." opens (the root layout doesn't re-render per page).
16
15
  * - `DRAFT_PARAM`, `DRAFT_COOKIE`, `VERSION_HEADER`, `draftResponseHeaders`,
package/src/studio.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Where Deco Studio runs: the origins its site editor frames a site from and
3
- * talks to it from (`postMessage`). One list, so the Studio bridge
4
- * (`StudioBridge`), `withDeco()`'s dev origins and a site's own
5
- * `frame-ancestors` never drift apart. This package's own copy: it never
3
+ * talks to it from (`postMessage`). One list, so `withDeco()`'s dev origins
4
+ * and a site's own `frame-ancestors` never drift apart. This package's own copy: it never
6
5
  * imports `@decocms/tanstack`.
7
6
  *
8
7
  * No module state and no framework code: safe in a browser bundle.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * v7's "." edit shortcut, alone: on a page that isn't framed, "." (or
3
+ * Ctrl+Shift+E) opens the page in Studio's editor (`/choose-editor`, which
4
+ * signs a visitor who isn't signed in in first), and Ctrl or Cmd+"." opens it
5
+ * in a new tab. In a frame it does nothing: Studio's editor reaches a framed
6
+ * page through the Studio channel (`StudioChannel`) only.
7
+ *
8
+ * Safe under a test runner that rewrites "frame-busting" code in the HTML it
9
+ * proxies (Cypress's `modifyObstructiveCode`): the script compares no window
10
+ * with `top` or `parent` in a form those rewrites match, so the HTML reaches
11
+ * the browser byte for byte (a rewrite changes the length of a React Server
12
+ * Components payload that carries the script and breaks the page).
13
+ *
14
+ * A key typed in a field is left alone, also in a field inside an open shadow
15
+ * root (a third-party widget's form): its events reach the document with the
16
+ * host as their target, so the script reads the field from the event's
17
+ * composed path.
18
+ *
19
+ * The page it sends: the last `StudioPage` marker the page rendered, else the
20
+ * `page` the binding passed, read at the keypress. An inline script with no
21
+ * hooks, so a Server Component renders it; its settings travel in `data-*`
22
+ * attributes (the same as v7's bridge's: `data-deco-studio-bridge`,
23
+ * `data-site`, `data-shortcut`, `data-path-template`, `data-page-id`) and the
24
+ * script itself is static.
25
+ *
26
+ * `@decocms/nextjs` and `@decocms/tanstack` each carry this file (neither
27
+ * imports the other); tests/studio-shortcut.test.ts keeps the two the same.
28
+ */
29
+ import { createElement, type ReactNode } from "react";
30
+
31
+ /** The page the "." shortcut opens in Studio. */
32
+ interface ShortcutPage {
33
+ /** The page block's `path`, as `matchRoute` matched it (`/products/:slug/p`, `/*`). */
34
+ pathTemplate?: string;
35
+ /** The page block's id, when the site knows it. */
36
+ id?: string;
37
+ }
38
+
39
+ export interface StudioShortcutProps {
40
+ /** The site's name in Deco (the shortcut sends it to Studio). */
41
+ site: string;
42
+ /** On by default; `false` turns it off. */
43
+ editShortcut?: boolean;
44
+ /** The page on screen, when the component that renders the shortcut knows it. */
45
+ page?: ShortcutPage;
46
+ /** The CSP nonce for the inline script, when the site sends one. */
47
+ nonce?: string;
48
+ }
49
+
50
+ /** The browser side. Static: everything it needs comes from the script element's attributes. */
51
+ export const STUDIO_SHORTCUT_SCRIPT = `(function(){
52
+ var s=document.currentScript,w=window,p=w.parent;
53
+ if(!s||p!==w||w.__decoStudioShortcut||!s.hasAttribute("data-shortcut"))return;
54
+ w.__decoStudioShortcut=true;
55
+ var site=s.getAttribute("data-site")||"";
56
+ function page(){
57
+ var all=document.querySelectorAll("[data-deco-page]"),el=all.length?all[all.length-1]:null;
58
+ if(!el||!el.getAttribute("data-path-template")){el=document.querySelector("script[data-deco-studio-bridge]")||s}
59
+ return{t:el.getAttribute("data-path-template")||"",id:el.getAttribute("data-page-id")||""};
60
+ }
61
+ document.addEventListener("keydown",function(e){
62
+ var t=e.composedPath?e.composedPath()[0]:e.target;
63
+ if(e.defaultPrevented||(t&&(t.tagName==="INPUT"||t.tagName==="TEXTAREA"||t.tagName==="SELECT"||t.isContentEditable)))return;
64
+ if(e.key!=="."&&!(e.ctrlKey&&e.shiftKey&&e.key==="E"))return;
65
+ e.preventDefault();
66
+ e.stopPropagation();
67
+ var g=page(),h=new URL("https://studio.decocms.com/choose-editor");
68
+ h.searchParams.set("site",site);
69
+ h.searchParams.set("domain",location.origin);
70
+ if(g.id)h.searchParams.set("pageId",g.id);
71
+ h.searchParams.set("path",location.pathname+location.search);
72
+ h.searchParams.set("pathTemplate",g.t||"/*");
73
+ if((e.ctrlKey||e.metaKey)&&e.key==="."){w.open(h.toString(),"_blank");return}
74
+ location.href=h.toString();
75
+ });
76
+ })();`;
77
+
78
+ /** The "." shortcut's script. Render it once, in the `<body>`. */
79
+ export function StudioShortcut({
80
+ site,
81
+ editShortcut = true,
82
+ page,
83
+ nonce,
84
+ }: StudioShortcutProps): ReactNode {
85
+ return createElement("script", {
86
+ "data-deco-studio-bridge": "",
87
+ "data-site": site,
88
+ "data-shortcut": editShortcut ? "" : undefined,
89
+ // Read on each keypress, so a client-side navigation that re-renders the
90
+ // script with the next page updates them.
91
+ "data-path-template": page?.pathTemplate || undefined,
92
+ "data-page-id": page?.id || undefined,
93
+ nonce,
94
+ // Browsers hide a nonce from the DOM after parsing, which React would
95
+ // report as a hydration mismatch.
96
+ suppressHydrationWarning: true,
97
+ dangerouslySetInnerHTML: { __html: STUDIO_SHORTCUT_SCRIPT },
98
+ });
99
+ }