@decocms/tanstack 7.77.11 → 7.77.13

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/tanstack",
3
- "version": "7.77.11",
3
+ "version": "7.77.13",
4
4
  "type": "module",
5
5
  "description": "Deco framework binding for TanStack Start + Cloudflare Workers",
6
6
  "repository": {
@@ -27,9 +27,9 @@
27
27
  "lint:unused": "knip"
28
28
  },
29
29
  "dependencies": {
30
- "@decocms/blocks": "7.77.11",
31
- "@decocms/blocks-admin": "7.77.11",
32
- "@decocms/blocks-cli": "7.77.11",
30
+ "@decocms/blocks": "7.77.13",
31
+ "@decocms/blocks-admin": "7.77.13",
32
+ "@decocms/blocks-cli": "7.77.13",
33
33
  "@deco-cx/warp-node": "^0.3.16",
34
34
  "fast-json-patch": "^3.1.0",
35
35
  "ws": "^8.18.0"
@@ -2,8 +2,9 @@ import {
2
2
  loadRedirects,
3
3
  matchExactRedirect,
4
4
  matchPatternRedirect,
5
+ setRedirectSemantics,
5
6
  } from "@decocms/blocks/sdk/redirects";
6
- import { beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
8
  import { clearRedirectCache, lookupExactRedirect } from "./kvRedirects";
8
9
 
9
10
  /** Minimal KV binding that records every read. */
@@ -39,7 +40,7 @@ describe("lookupExactRedirect", () => {
39
40
  });
40
41
 
41
42
  it("caches a HIT so the same path costs one KV read", async () => {
42
- const kv = makeKV({ [`redirect:${ID}:/old`]: '{"to":"/new","status":302}' });
43
+ const kv = makeKV({ [`redirect:${ID}:/old`]: '{"to":"/new","status":307}' });
43
44
  await lookupExactRedirect(enabled(kv), "/old");
44
45
  await lookupExactRedirect(enabled(kv), "/old");
45
46
  expect(kv.get).toHaveBeenCalledTimes(1);
@@ -88,10 +89,45 @@ describe("lookupExactRedirect", () => {
88
89
  await expect(lookupExactRedirect(enabled(kv), "/old")).resolves.toBeNull();
89
90
  });
90
91
 
91
- it("defaults an unknown status to 302 rather than inventing a permanent redirect", async () => {
92
+ it("answers an unknown status as temporary rather than inventing a permanent redirect", async () => {
92
93
  const kv = makeKV({ [`redirect:${ID}:/old`]: '{"to":"/new"}' });
93
94
  await expect(lookupExactRedirect(enabled(kv), "/old")).resolves.toMatchObject({ status: 302 });
94
95
  });
96
+
97
+ it("looks the request path up under its folded key", async () => {
98
+ const kv = makeKV({ [`redirect:${ID}:/old`]: '{"from":"/Old/","to":"/new","status":307}' });
99
+ await expect(lookupExactRedirect(enabled(kv), "/OLD/")).resolves.toMatchObject({
100
+ to: "/new",
101
+ status: 302,
102
+ });
103
+ });
104
+ });
105
+
106
+ describe("lookupExactRedirect — semantics", () => {
107
+ afterEach(() => setRedirectSemantics("legacy"));
108
+ const value = JSON.stringify([
109
+ { from: "/meia/p", to: "/a", status: 307 },
110
+ { from: "/Meia/p/", to: "/b", status: 307 },
111
+ ]);
112
+
113
+ it("legacy serves the last rule under the key, temporary as 302", async () => {
114
+ const kv = makeKV({ [`redirect:${ID}:/meia/p`]: value });
115
+ await expect(lookupExactRedirect(enabled(kv), "/meia/p")).resolves.toMatchObject({
116
+ to: "/b",
117
+ status: 302,
118
+ });
119
+ });
120
+
121
+ it("fresh serves the rule written byte for byte, temporary as 307", async () => {
122
+ setRedirectSemantics("fresh");
123
+ const kv = makeKV({ [`redirect:${ID}:/meia/p`]: value });
124
+ await expect(lookupExactRedirect(enabled(kv), "/meia/p")).resolves.toMatchObject({
125
+ to: "/a",
126
+ status: 307,
127
+ });
128
+ clearRedirectCache();
129
+ await expect(lookupExactRedirect(enabled(kv), "/MEIA/p")).resolves.toBeNull();
130
+ });
95
131
  });
96
132
 
97
133
  describe("precedence: exact (KV) over glob (memory)", () => {
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { getDeploymentId, redirectKey, type StoredRedirect } from "@decocms/blocks/cms";
21
- import type { Redirect } from "@decocms/blocks/sdk/redirects";
21
+ import { normalizePath, type Redirect, selectExactRedirect } from "@decocms/blocks/sdk/redirects";
22
22
  import { getFastDeployKV } from "./kvHydration";
23
23
 
24
24
  /**
@@ -39,8 +39,10 @@ const TTL_MS = 60_000;
39
39
  * the whole point of this module is to stop holding an unbounded list. */
40
40
  const MAX_ENTRIES = 2_000;
41
41
 
42
+ type StoredValue = StoredRedirect | StoredRedirect[];
43
+
42
44
  interface CacheEntry {
43
- value: StoredRedirect | null;
45
+ value: StoredValue | null;
44
46
  expiresAt: number;
45
47
  }
46
48
 
@@ -53,7 +55,7 @@ function cache(): Map<string, CacheEntry> {
53
55
  }
54
56
 
55
57
  /** Insertion-ordered LRU touch + evict. */
56
- function remember(key: string, value: StoredRedirect | null): void {
58
+ function remember(key: string, value: StoredValue | null): void {
57
59
  const c = cache();
58
60
  c.delete(key);
59
61
  c.set(key, { value, expiresAt: Date.now() + TTL_MS });
@@ -80,8 +82,10 @@ export function clearRedirectCache(): void {
80
82
  }
81
83
 
82
84
  /**
83
- * Look up one exact redirect for `normalizedPath` (already through
84
- * `normalizePath` — the key contract).
85
+ * Look up one exact redirect for the request's `pathname`. The key is
86
+ * `normalizePath(pathname)` (the key contract, the same in both semantics);
87
+ * which of the rules stored there answers — if any — is the semantics' call
88
+ * (`selectExactRedirect`).
85
89
  *
86
90
  * Returns `null` for "no redirect", including every case where the lookup
87
91
  * cannot run (fast-deploy off, no deployment id, KV error). A KV failure must
@@ -90,7 +94,7 @@ export function clearRedirectCache(): void {
90
94
  */
91
95
  export async function lookupExactRedirect(
92
96
  env: Record<string, unknown>,
93
- normalizedPath: string,
97
+ pathname: string,
94
98
  ): Promise<Redirect | null> {
95
99
  const kv = getFastDeployKV(env);
96
100
  if (!kv) return null;
@@ -98,28 +102,40 @@ export async function lookupExactRedirect(
98
102
  const deploymentId = getDeploymentId(env);
99
103
  if (!deploymentId) return null;
100
104
 
101
- const key = redirectKey(deploymentId, normalizedPath);
105
+ const key = redirectKey(deploymentId, normalizePath(pathname));
102
106
  const cached = peek(key);
103
107
  const stored = cached !== undefined ? cached.value : await read(kv, key);
104
108
  if (cached === undefined) remember(key, stored);
105
109
  if (!stored) return null;
106
110
 
107
- return { from: normalizedPath, to: stored.to, status: stored.status };
111
+ return selectExactRedirect(stored, pathname);
108
112
  }
109
113
 
110
114
  async function read(
111
115
  kv: { get(key: string): Promise<string | null> },
112
116
  key: string,
113
- ): Promise<StoredRedirect | null> {
117
+ ): Promise<StoredValue | null> {
114
118
  try {
115
119
  const raw = await kv.get(key);
116
120
  if (raw === null) return null;
117
- const parsed = JSON.parse(raw) as StoredRedirect;
121
+ const parsed = JSON.parse(raw) as unknown;
118
122
  // A malformed value is a miss, not a throw — never let bad data in one key
119
123
  // take down every request for that path.
120
- if (!parsed || typeof parsed.to !== "string") return null;
121
- return { to: parsed.to, status: parsed.status === 301 ? 301 : 302 };
124
+ const rules = (Array.isArray(parsed) ? parsed : [parsed])
125
+ .map(toStored)
126
+ .filter((r): r is StoredRedirect => r !== null);
127
+ if (rules.length === 0) return null;
128
+ return rules.length === 1 ? rules[0] : rules;
122
129
  } catch {
123
130
  return null;
124
131
  }
125
132
  }
133
+
134
+ function toStored(value: unknown): StoredRedirect | null {
135
+ const parsed = value as Partial<StoredRedirect> | null;
136
+ if (!parsed || typeof parsed !== "object" || typeof parsed.to !== "string") return null;
137
+ const stored: StoredRedirect = { to: parsed.to, status: parsed.status === 301 ? 301 : 307 };
138
+ if (typeof parsed.from === "string") stored.from = parsed.from;
139
+ if (parsed.discardQueryParameters === true) stored.discardQueryParameters = true;
140
+ return stored;
141
+ }
@@ -230,7 +230,7 @@ describe("CMS redirects", () => {
230
230
  expect(res.headers.get("Location")).toBe("/new");
231
231
  });
232
232
 
233
- it("redirects ?asJson requests too (does not fall through to page resolver)", async () => {
233
+ it("fresh: redirects ?asJson requests too, keeping the query (does not fall through to page resolver)", async () => {
234
234
  setBlocks({
235
235
  "redirect-1": {
236
236
  __resolveType: "website/loaders/redirect.ts",
@@ -239,6 +239,7 @@ describe("CMS redirects", () => {
239
239
  });
240
240
  const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
241
241
  observability: false,
242
+ redirects: { semantics: "fresh" },
242
243
  });
243
244
  const res = await worker.fetch(
244
245
  new Request("https://example.com/old?asJson"),
@@ -246,10 +247,83 @@ describe("CMS redirects", () => {
246
247
  MOCK_CTX,
247
248
  );
248
249
  expect(res.status).toBe(301);
249
- expect(res.headers.get("Location")).toBe("/new");
250
+ // The query travels with the redirect, so the client that follows it
251
+ // still gets JSON for the page that moved.
252
+ expect(res.headers.get("Location")).toBe("/new?asJson");
250
253
  });
251
254
 
252
- it("URI-encodes the Location header for non-ASCII destinations", async () => {
255
+ it("fresh: appends the request's query string to Location unless the rule discards it", async () => {
256
+ setBlocks({
257
+ "redirect-1": {
258
+ __resolveType: "website/loaders/redirects.ts",
259
+ redirects: [
260
+ { from: "/old", to: "/new", type: "permanent" },
261
+ { from: "/strip", to: "/clean?ref=1", type: "permanent", discardQueryParameters: true },
262
+ ],
263
+ },
264
+ });
265
+ const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
266
+ observability: false,
267
+ redirects: { semantics: "fresh" },
268
+ });
269
+ const kept = await worker.fetch(
270
+ new Request("https://example.com/old?utm_source=qa&gclid=x"),
271
+ EMPTY_ENV,
272
+ MOCK_CTX,
273
+ );
274
+ expect(kept.headers.get("Location")).toBe("/new?gclid=x&utm_source=qa");
275
+
276
+ const dropped = await worker.fetch(
277
+ new Request("https://example.com/strip?utm_source=qa"),
278
+ EMPTY_ENV,
279
+ MOCK_CTX,
280
+ );
281
+ expect(dropped.headers.get("Location")).toBe("/clean?ref=1");
282
+ });
283
+
284
+ it("fresh: matches a rule scoped to a query before the bare path, and case-sensitively", async () => {
285
+ setBlocks({
286
+ "redirect-1": {
287
+ __resolveType: "website/loaders/redirects.ts",
288
+ redirects: [
289
+ { from: "/kit/p", to: "/camisa/p", type: "permanent" },
290
+ { from: "/kit/p?skuId=4432", to: "/roupas/kits", type: "permanent" },
291
+ { from: "/Calca/p", to: "/calca/p", type: "permanent" },
292
+ ],
293
+ },
294
+ });
295
+ const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
296
+ observability: false,
297
+ redirects: { semantics: "fresh" },
298
+ });
299
+ const scoped = await worker.fetch(
300
+ new Request("https://example.com/kit/p?skuId=4432"),
301
+ EMPTY_ENV,
302
+ MOCK_CTX,
303
+ );
304
+ expect(scoped.headers.get("Location")).toBe("/roupas/kits?skuId=4432");
305
+
306
+ const bare = await worker.fetch(new Request("https://example.com/kit/p"), EMPTY_ENV, MOCK_CTX);
307
+ expect(bare.headers.get("Location")).toBe("/camisa/p");
308
+
309
+ // `/Calca/p -> /calca/p` is a redirect, not a loop: the lower-case URL
310
+ // has no rule and renders.
311
+ const upper = await worker.fetch(
312
+ new Request("https://example.com/Calca/p"),
313
+ EMPTY_ENV,
314
+ MOCK_CTX,
315
+ );
316
+ expect(upper.status).toBe(301);
317
+ expect(upper.headers.get("Location")).toBe("/calca/p");
318
+ const lower = await worker.fetch(
319
+ new Request("https://example.com/calca/p"),
320
+ EMPTY_ENV,
321
+ MOCK_CTX,
322
+ );
323
+ expect(lower.status).toBe(200);
324
+ });
325
+
326
+ it("fresh: URI-encodes the Location header for non-ASCII destinations", async () => {
253
327
  setBlocks({
254
328
  "redirect-1": {
255
329
  __resolveType: "website/loaders/redirect.ts",
@@ -258,13 +332,14 @@ describe("CMS redirects", () => {
258
332
  });
259
333
  const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
260
334
  observability: false,
335
+ redirects: { semantics: "fresh" },
261
336
  });
262
337
  const res = await worker.fetch(new Request("https://example.com/promo"), EMPTY_ENV, MOCK_CTX);
263
- expect(res.status).toBe(302);
338
+ expect(res.status).toBe(307);
264
339
  expect(res.headers.get("Location")).toBe("/promo%C3%A7%C3%A3o");
265
340
  });
266
341
 
267
- it("returns a 302 redirect for a temporary redirect block", async () => {
342
+ it("fresh: returns a 307 redirect for a temporary redirect block", async () => {
268
343
  setBlocks({
269
344
  "redirect-1": {
270
345
  __resolveType: "website/loaders/redirect.ts",
@@ -273,9 +348,10 @@ describe("CMS redirects", () => {
273
348
  });
274
349
  const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
275
350
  observability: false,
351
+ redirects: { semantics: "fresh" },
276
352
  });
277
353
  const res = await worker.fetch(new Request("https://example.com/promo"), EMPTY_ENV, MOCK_CTX);
278
- expect(res.status).toBe(302);
354
+ expect(res.status).toBe(307);
279
355
  expect(res.headers.get("Location")).toBe("/sale");
280
356
  });
281
357
 
@@ -316,6 +392,45 @@ describe("CMS redirects", () => {
316
392
  expect(res2.status).toBe(301);
317
393
  expect(res2.headers.get("Location")).toBe("/v2");
318
394
  });
395
+
396
+ it("legacy (default): folds case and trailing slash, drops the query, 302 for temporary", async () => {
397
+ setBlocks({
398
+ "redirect-1": {
399
+ __resolveType: "website/loaders/redirects.ts",
400
+ redirects: [
401
+ { from: "/baixeoapp", to: "/baixe-o-app", type: "permanent" },
402
+ { from: "/promo", to: "/promoção", type: "temporary" },
403
+ { from: "/Calca/p", to: "/calca/p", type: "permanent" },
404
+ ],
405
+ },
406
+ });
407
+ const worker = createDecoWorkerEntry(MOCK_SERVER_ENTRY, {
408
+ observability: false,
409
+ });
410
+ const folded = await worker.fetch(
411
+ new Request("https://example.com/BaixeoApp/?utm_source=a&asJson"),
412
+ EMPTY_ENV,
413
+ MOCK_CTX,
414
+ );
415
+ expect(folded.status).toBe(301);
416
+ expect(folded.headers.get("Location")).toBe("/baixe-o-app");
417
+
418
+ const temporary = await worker.fetch(
419
+ new Request("https://example.com/promo"),
420
+ EMPTY_ENV,
421
+ MOCK_CTX,
422
+ );
423
+ expect(temporary.status).toBe(302);
424
+ expect(temporary.headers.get("Location")).toBe("/promo%C3%A7%C3%A3o");
425
+
426
+ // Folded, this rule would redirect /calca/p to itself forever — dropped.
427
+ const loop = await worker.fetch(
428
+ new Request("https://example.com/calca/p"),
429
+ EMPTY_ENV,
430
+ MOCK_CTX,
431
+ );
432
+ expect(loop.status).toBe(200);
433
+ });
319
434
  });
320
435
 
321
436
  describe("degraded origin (anti-cache-poisoning)", () => {
@@ -78,11 +78,14 @@ import {
78
78
  import { setRuntimeEnv } from "@decocms/blocks/sdk/otelAdapters";
79
79
  import { parseTraceparent } from "@decocms/blocks/sdk/otelHttpTracer";
80
80
  import {
81
+ getRedirectSemantics,
81
82
  loadRedirects,
82
83
  matchExactRedirect,
83
84
  matchPatternRedirect,
84
- normalizePath,
85
85
  type RedirectMap,
86
+ type RedirectSemantics,
87
+ redirectLocation,
88
+ setRedirectSemantics,
86
89
  } from "@decocms/blocks/sdk/redirects";
87
90
  import { RequestContext } from "@decocms/blocks/sdk/requestContext";
88
91
  import { createResponseCache } from "@decocms/blocks/sdk/responseCache";
@@ -261,6 +264,16 @@ export interface DecoWorkerEntryOptions {
261
264
  */
262
265
  renderJson?: boolean;
263
266
 
267
+ /**
268
+ * `?renderJson` strips the request-derived keys the section mixins inject
269
+ * (`device`, `isMobile`, `currentSearchParam`) from every section. A section
270
+ * whose own loader returns one of them as content (Fresh: `device:
271
+ * ctx.device`, which production's `?renderJson` carries) can keep it here:
272
+ * resolveType suffix (matched like `renderJson.sectionsToIgnore`) → keys to
273
+ * preserve. Example: `{ "Product/ProductDetails.tsx": ["device"] }`.
274
+ */
275
+ renderJsonKeepKeys?: Record<string, readonly string[]>;
276
+
264
277
  /**
265
278
  * Serve the raw resolved page as JSON on `?asJson` (legacy admin-preview
266
279
  * path). Set to `false` to disable it site-wide.
@@ -615,6 +628,23 @@ export interface DecoWorkerEntryOptions {
615
628
  * @default `Deco/<version> (+https://deco.cx)` — see DECO_USER_AGENT.
616
629
  */
617
630
  outboundUserAgent?: string | false;
631
+
632
+ /**
633
+ * How CMS redirects (`website/loaders/redirect(s).ts`, `redirectsFromCsv`)
634
+ * match and answer — see `@decocms/blocks/sdk/redirects`.
635
+ *
636
+ * - `"legacy"` (default): case- and trailing-slash-insensitive match,
637
+ * temporary → 302, the request's query dropped from `Location`. What every
638
+ * `@decocms/*` site has answered so far.
639
+ * - `"fresh"`: what the Fresh site a migration came from answered — source
640
+ * matched byte for byte (`pathname + search` first), temporary → 307, the
641
+ * request's query appended to `Location` unless `discardQueryParameters`.
642
+ *
643
+ * Both drop self-redirects and never double-encode `Location`.
644
+ *
645
+ * @default { semantics: "legacy" }
646
+ */
647
+ redirects?: { semantics?: RedirectSemantics };
618
648
  }
619
649
 
620
650
  // ---------------------------------------------------------------------------
@@ -1153,6 +1183,7 @@ export function createDecoWorkerEntry(
1153
1183
  extraBypassPaths = [],
1154
1184
  fingerprintedAssetPattern = FINGERPRINTED_ASSET_RE,
1155
1185
  stripTrackingParams: shouldStripTracking = true,
1186
+ redirects: redirectsOpt,
1156
1187
  previewShell: customPreviewShell,
1157
1188
  cacheVersionEnv = "BUILD_HASH",
1158
1189
  securityHeaders: securityHeadersOpt,
@@ -1168,6 +1199,10 @@ export function createDecoWorkerEntry(
1168
1199
  speculationRules: speculationRulesOpt,
1169
1200
  } = options;
1170
1201
 
1202
+ // Before the first request builds the redirect map — the map keys its rules
1203
+ // for the semantics in force when it is built.
1204
+ setRedirectSemantics(redirectsOpt?.semantics ?? "legacy");
1205
+
1171
1206
  // Speculation Rules — store the site-wide config in the shared singleton so
1172
1207
  // DecoRootLayout (SSR) emits the tag in <head>. Inert unless the site opts in.
1173
1208
  setSpeculationRules(speculationRulesOpt);
@@ -2313,7 +2348,9 @@ export function createDecoWorkerEntry(
2313
2348
  // ?asJson and commerce proxy so every request path respects redirects.
2314
2349
  // Revision-keyed so cross-bundle setBlocks() calls are detected even when
2315
2350
  // onChange listeners don't fire across Vite split-bundle module instances.
2316
- const currentRevision = getRevision();
2351
+ // The semantics are part of the key: a map keys its rules for the
2352
+ // semantics in force when it is built.
2353
+ const currentRevision = `${getRevision()}:${getRedirectSemantics()}`;
2317
2354
  if (_redirectMapRevision !== currentRevision) {
2318
2355
  _redirectMap = loadRedirects(loadBlocks());
2319
2356
  _redirectMapRevision = currentRevision;
@@ -2323,14 +2360,16 @@ export function createDecoWorkerEntry(
2323
2360
  // from the decofile, so the in-memory exact hit is the non-fast-deploy
2324
2361
  // case. Going straight to `matchRedirect` and only then to KV would let a
2325
2362
  // glob win over an exact rule, inverting the precedence.
2363
+ // Query-scoped rules (`/x?map=ft`) live in memory only, so the KV lookup
2364
+ // stays one read, keyed by pathname.
2326
2365
  const cmsRedirect =
2327
- matchExactRedirect(url.pathname, _redirectMap!) ??
2328
- (await lookupExactRedirect(env as Record<string, unknown>, normalizePath(url.pathname))) ??
2366
+ matchExactRedirect(url.pathname, _redirectMap!, url.search) ??
2367
+ (await lookupExactRedirect(env as Record<string, unknown>, url.pathname)) ??
2329
2368
  matchPatternRedirect(url.pathname, _redirectMap!);
2330
2369
  if (cmsRedirect) {
2331
2370
  return new Response(null, {
2332
2371
  status: cmsRedirect.status,
2333
- headers: { Location: encodeURI(cmsRedirect.to) },
2372
+ headers: { Location: redirectLocation(cmsRedirect, url.search) },
2334
2373
  });
2335
2374
  }
2336
2375
 
@@ -2398,6 +2437,16 @@ export function createDecoWorkerEntry(
2398
2437
  const getSectionModule = (component: string) => ({
2399
2438
  renderJson: getSectionOptions(component)?.renderJson,
2400
2439
  });
2440
+ const keepEntries = Object.entries(options.renderJsonKeepKeys ?? {});
2441
+ const keepInjectedKeys =
2442
+ keepEntries.length === 0
2443
+ ? undefined
2444
+ : (component: string) => {
2445
+ const keys = keepEntries.flatMap(([suffix, k]) =>
2446
+ component.endsWith(suffix) ? k : [],
2447
+ );
2448
+ return keys.length > 0 ? keys : undefined;
2449
+ };
2401
2450
 
2402
2451
  const jsonHeaders = {
2403
2452
  ...corsHeaders,
@@ -2435,7 +2484,7 @@ export function createDecoWorkerEntry(
2435
2484
  }
2436
2485
  const [serialized] = serializeRenderJson(
2437
2486
  [{ component: resolved.component, props: resolved.props }],
2438
- { getSectionModule, sectionsToIgnore: ignoreSuffixes },
2487
+ { getSectionModule, sectionsToIgnore: ignoreSuffixes, keepInjectedKeys },
2439
2488
  );
2440
2489
  if (!serialized) {
2441
2490
  return Response.json(
@@ -2466,6 +2515,7 @@ export function createDecoWorkerEntry(
2466
2515
  sectionsToIgnore: ignoreSuffixes,
2467
2516
  deferred: page.deferredSections.map((d) => ({ component: d.component, index: d.index })),
2468
2517
  lazyUrlFor,
2518
+ keepInjectedKeys,
2469
2519
  },
2470
2520
  );
2471
2521