@decocms/blocks 7.41.0 → 7.42.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/blocks",
3
- "version": "7.41.0",
3
+ "version": "7.42.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -68,6 +68,33 @@ describe("serializeRenderJson", () => {
68
68
  expect(out).toEqual([{ component: "a", props: {} }]);
69
69
  });
70
70
 
71
+ it("strips framework-injected keys (__page*, device, isMobile) from props", () => {
72
+ const out = serializeRenderJson(
73
+ [
74
+ {
75
+ component: "a",
76
+ props: {
77
+ title: "keep",
78
+ __pageUrl: "https://x/y",
79
+ __pagePath: "/y",
80
+ device: "mobile",
81
+ isMobile: true,
82
+ currentSearchParam: "q",
83
+ },
84
+ },
85
+ ],
86
+ // no projection → pass-through, but injected keys must still be stripped
87
+ );
88
+ expect(out).toEqual([{ component: "a", props: { title: "keep" } }]);
89
+ });
90
+
91
+ it("strips injected keys even when a projection rest-spreads them through", () => {
92
+ const out = serializeRenderJson([{ component: "a", props: { keep: 1, __pageUrl: "u", device: "d" } }], {
93
+ getSectionModule: () => ({ renderJson: (p: Record<string, unknown>) => p }), // identity passes __ through
94
+ });
95
+ expect(out).toEqual([{ component: "a", props: { keep: 1 } }]);
96
+ });
97
+
71
98
  it("interleaves deferred sections as { component, lazyUrl } by index", () => {
72
99
  const out = serializeRenderJson(
73
100
  [
@@ -85,6 +85,28 @@ export interface SerializeOptions {
85
85
  * `renderJson === false` (for a deferred section, dropping avoids emitting a
86
86
  * lazyUrl the app would fetch for nothing).
87
87
  */
88
+ /**
89
+ * Keys the section loader / mixins inject into props at request time — page
90
+ * context (`__pageUrl`/`__pagePath`, `runSectionLoaders`) and device
91
+ * (`device`/`isMobile`/`currentSearchParam`, the withDevice/withMobile/
92
+ * withSearchParam mixins). They are request-derived framework internals, NOT
93
+ * content, so they are stripped from the JSON: they would bust the ETag per
94
+ * request/device and leak internals the mobile app shouldn't cache. Any
95
+ * `__`-prefixed key is treated as internal by convention (mirrors `__section`).
96
+ */
97
+ const FRAMEWORK_INJECTED_KEYS = new Set(["device", "isMobile", "currentSearchParam"]);
98
+
99
+ function stripFrameworkKeys(props: Record<string, unknown>): Record<string, unknown> {
100
+ let out: Record<string, unknown> | null = null;
101
+ for (const k of Object.keys(props)) {
102
+ if (k.startsWith("__") || FRAMEWORK_INJECTED_KEYS.has(k)) {
103
+ out ??= { ...props };
104
+ delete out[k];
105
+ }
106
+ }
107
+ return out ?? props;
108
+ }
109
+
88
110
  export function serializeRenderJson(
89
111
  sections: SerializableSection[],
90
112
  opts: SerializeOptions = {},
@@ -104,7 +126,8 @@ export function serializeRenderJson(
104
126
  .filter((s) => !isDropped(s.component))
105
127
  .map((section) => {
106
128
  const rj = renderJsonOf(section.component);
107
- const props = typeof rj === "function" ? rj(section.props ?? {}) : section.props ?? {};
129
+ const projected = typeof rj === "function" ? rj(section.props ?? {}) : section.props ?? {};
130
+ const props = stripFrameworkKeys(projected);
108
131
  return { index: section.index, out: { component: section.component, props } as SerializedSection };
109
132
  });
110
133