@decocms/tanstack 7.22.1 → 7.22.2

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.22.1",
3
+ "version": "7.22.2",
4
4
  "type": "module",
5
5
  "description": "Deco framework binding for TanStack Start + Cloudflare Workers",
6
6
  "repository": {
@@ -24,9 +24,9 @@
24
24
  "lint:unused": "knip"
25
25
  },
26
26
  "dependencies": {
27
- "@decocms/blocks": "7.22.1",
28
- "@decocms/blocks-admin": "7.22.1",
29
- "@decocms/blocks-cli": "7.22.1",
27
+ "@decocms/blocks": "7.22.2",
28
+ "@decocms/blocks-admin": "7.22.2",
29
+ "@decocms/blocks-cli": "7.22.2",
30
30
  "@deco-cx/warp-node": "^0.3.16",
31
31
  "fast-json-patch": "^3.1.0",
32
32
  "ws": "^8.18.0"
@@ -1,7 +1,44 @@
1
1
  import { registerCacheableSections, registerSectionLoader } from "@decocms/blocks/cms";
2
2
  import type { ResolvedSection } from "@decocms/blocks/cms";
3
3
  import { describe, expect, it } from "vitest";
4
- import { runSectionLoadersWithSeo } from "./cmsRoute";
4
+ import {
5
+ parseLoadCmsHomePageInput,
6
+ parseLoadCmsPageInput,
7
+ runSectionLoadersWithSeo,
8
+ } from "./cmsRoute";
9
+
10
+ describe("parseLoadCmsPageInput (#292)", () => {
11
+ it("treats a bare string as the path with resolveGlobals defaulting to true (back-compat)", () => {
12
+ expect(parseLoadCmsPageInput("/produto/tenis")).toEqual({
13
+ path: "/produto/tenis",
14
+ resolveGlobals: true,
15
+ });
16
+ });
17
+
18
+ it("accepts { path, resolveGlobals } and preserves an explicit false", () => {
19
+ expect(parseLoadCmsPageInput({ path: "/produto/tenis", resolveGlobals: false })).toEqual({
20
+ path: "/produto/tenis",
21
+ resolveGlobals: false,
22
+ });
23
+ });
24
+
25
+ it("defaults resolveGlobals to true when omitted from the object form", () => {
26
+ expect(parseLoadCmsPageInput({ path: "/produto/tenis" })).toEqual({
27
+ path: "/produto/tenis",
28
+ resolveGlobals: true,
29
+ });
30
+ });
31
+ });
32
+
33
+ describe("parseLoadCmsHomePageInput (#292)", () => {
34
+ it("defaults resolveGlobals to true when called with no data (back-compat)", () => {
35
+ expect(parseLoadCmsHomePageInput(undefined)).toEqual({ resolveGlobals: true });
36
+ });
37
+
38
+ it("preserves an explicit resolveGlobals: false", () => {
39
+ expect(parseLoadCmsHomePageInput({ resolveGlobals: false })).toEqual({ resolveGlobals: false });
40
+ });
41
+ });
5
42
 
6
43
  /**
7
44
  * Regression guard for #355: body sections and the SEO section used to be
@@ -62,6 +62,7 @@ import { normalizeUrlsInObject } from "@decocms/blocks/sdk/normalizeUrls";
62
62
  import { type Device, detectDevice } from "@decocms/blocks/sdk/useDevice";
63
63
  import { derivePageUrl, isClientNavigation } from "./pageUrl";
64
64
  import { dedupeGlobals, resolveSiteGlobals } from "./withSiteGlobals";
65
+ import type { SiteGlobalRef } from "./withSiteGlobals";
65
66
 
66
67
  const isServer = typeof document === "undefined";
67
68
 
@@ -85,6 +86,12 @@ export function setSectionChunkMap(map: Record<string, string>): void {
85
86
  type PageResult = Awaited<ReturnType<typeof loadCmsPageInternal>>;
86
87
  const pageInflight = new Map<string, Promise<PageResult>>();
87
88
 
89
+ /** Same shape as `resolveSiteGlobals()`'s empty result — used when `resolveGlobals: false`. */
90
+ const EMPTY_GLOBALS: { resolvedSections: ResolvedSection[]; rawRefs: SiteGlobalRef[] } = {
91
+ resolvedSections: [],
92
+ rawRefs: [],
93
+ };
94
+
88
95
  /** One year — the cohort is sticky; the `pct` fingerprint handles re-rolls. */
89
96
  const SEGMENT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
90
97
 
@@ -154,7 +161,7 @@ export async function runSectionLoadersWithSeo(
154
161
  };
155
162
  }
156
163
 
157
- async function loadCmsPageInternal(fullPath: string) {
164
+ async function loadCmsPageInternal(fullPath: string, resolveGlobals: boolean) {
158
165
  const [basePath] = fullPath.split("?");
159
166
  // On client-side navigation getRequestUrl() is the /_serverFn/... endpoint,
160
167
  // not the page being loaded — derivePageUrl rebuilds the real page URL so
@@ -188,7 +195,7 @@ async function loadCmsPageInternal(fullPath: string) {
188
195
  // empty client-bundled `blocks.gen.ts`.
189
196
  const [{ enrichedSections, enrichedSeoSection }, globals] = await Promise.all([
190
197
  runSectionLoadersWithSeo(page.resolvedSections, page.seoSection, request),
191
- resolveSiteGlobals(),
198
+ resolveGlobals ? resolveSiteGlobals() : Promise.resolve(EMPTY_GLOBALS),
192
199
  ]);
193
200
 
194
201
  // Page sections take precedence over globals — dedupe drops any global
@@ -228,10 +235,20 @@ async function loadCmsPageInternal(fullPath: string) {
228
235
  };
229
236
  }
230
237
 
238
+ /** Accepted by `loadCmsPage` — a bare path (back-compat) or `{ path, resolveGlobals }`. */
239
+ export type LoadCmsPageInput = string | { path: string; resolveGlobals?: boolean };
240
+
241
+ /** @internal exported for tests */
242
+ export function parseLoadCmsPageInput(data: unknown): { path: string; resolveGlobals: boolean } {
243
+ if (typeof data === "string") return { path: data, resolveGlobals: true };
244
+ const obj = data as { path: string; resolveGlobals?: boolean };
245
+ return { path: obj.path, resolveGlobals: obj.resolveGlobals ?? true };
246
+ }
247
+
231
248
  export const loadCmsPage = createServerFn({ method: "GET" })
232
- .inputValidator((data: unknown) => data as string)
249
+ .inputValidator((data: unknown) => data as LoadCmsPageInput)
233
250
  .handler(async (ctx) => {
234
- const fullPath = ctx.data;
251
+ const { path: fullPath, resolveGlobals } = parseLoadCmsPageInput(ctx.data);
235
252
 
236
253
  // Use the full path (including query string) as the dedup key.
237
254
  // Using basePath only caused /s?q=a and /s?q=b to share one promise,
@@ -241,67 +258,84 @@ export const loadCmsPage = createServerFn({ method: "GET" })
241
258
  // for the same path may carry deferredSections; sharing that promise with a
242
259
  // concurrent SPA transition would smuggle those deferred sections in, causing
243
260
  // loadDeferredSection to run without the per-request commerce app context.
244
- // Use separate dedup buckets for SSR vs client-nav.
261
+ // Use separate dedup buckets for SSR vs client-nav. `resolveGlobals` is also
262
+ // part of the key — otherwise a `resolveGlobals: false` request could return
263
+ // (or share a promise with) a `resolveGlobals: true` response for the same path.
245
264
  const clientNav = isClientNavigation(fullPath, getRequestUrl());
246
- const inflightKey = clientNav ? `__nav:${fullPath}` : fullPath;
265
+ const inflightKey =
266
+ (clientNav ? `__nav:${fullPath}` : fullPath) + (resolveGlobals ? "" : "|noGlobals");
247
267
  const existing = pageInflight.get(inflightKey);
248
268
  if (existing) return existing;
249
269
 
250
- const promise = loadCmsPageInternal(fullPath).finally(() => pageInflight.delete(inflightKey));
270
+ const promise = loadCmsPageInternal(fullPath, resolveGlobals).finally(() =>
271
+ pageInflight.delete(inflightKey),
272
+ );
251
273
  pageInflight.set(inflightKey, promise);
252
274
  return promise;
253
275
  });
254
276
 
277
+ /** Accepted by `loadCmsHomePage` — optional so existing no-arg callers keep working. */
278
+ export type LoadCmsHomePageInput = { resolveGlobals?: boolean } | undefined;
279
+
280
+ /** @internal exported for tests */
281
+ export function parseLoadCmsHomePageInput(data: unknown): { resolveGlobals: boolean } {
282
+ const obj = (data ?? {}) as { resolveGlobals?: boolean };
283
+ return { resolveGlobals: obj.resolveGlobals ?? true };
284
+ }
285
+
255
286
  /**
256
287
  * Same as loadCmsPage but hardcoded to "/" path.
257
288
  * Avoids passing data through the server function for the homepage.
258
289
  */
259
- export const loadCmsHomePage = createServerFn({ method: "GET" }).handler(async () => {
260
- const request = getRequest();
261
- const ua = getRequestHeader("user-agent") ?? "";
262
- const serverUrl = getRequestUrl();
263
- const matcherCtx: MatcherContext = {
264
- userAgent: ua,
265
- url: serverUrl.toString(),
266
- path: "/",
267
- cookies: getCookies(),
268
- request,
269
- isClientNavigation: isClientNavigation("/", serverUrl),
270
- flags: [],
271
- };
272
- const page = await resolveDecoPage("/", matcherCtx);
273
- if (!page) return null;
274
- const flags = persistFlags(matcherCtx);
275
- const [{ enrichedSections, enrichedSeoSection }, globals] = await Promise.all([
276
- runSectionLoadersWithSeo(page.resolvedSections, page.seoSection, request),
277
- resolveSiteGlobals(),
278
- ]);
279
-
280
- const mergedSections: ResolvedSection[] = [
281
- ...dedupeGlobals(globals.resolvedSections, enrichedSections),
282
- ...enrichedSections,
283
- ];
284
-
285
- const eagerKeys = mergedSections.map((s) => s.component);
286
- await preloadSectionComponents(eagerKeys);
287
-
288
- const device = detectDevice(ua);
289
- const seo = buildPageSeo(page.seoSection, enrichedSeoSection, mergedSections);
290
-
291
- const { seoSection: _seo, ...pageData } = page;
292
-
293
- return {
294
- ...pageData,
295
- resolvedSections: normalizeUrlsInObject(mergedSections),
296
- deferredSections: normalizeUrlsInObject(page.deferredSections),
297
- pagePath: "/",
298
- pageUrl: serverUrl.toString(),
299
- seo,
300
- device,
301
- flags,
302
- siteGlobals: { rawRefs: globals.rawRefs },
303
- };
304
- });
290
+ export const loadCmsHomePage = createServerFn({ method: "GET" })
291
+ .inputValidator((data: unknown) => data as LoadCmsHomePageInput)
292
+ .handler(async (ctx) => {
293
+ const { resolveGlobals } = parseLoadCmsHomePageInput(ctx.data);
294
+ const request = getRequest();
295
+ const ua = getRequestHeader("user-agent") ?? "";
296
+ const serverUrl = getRequestUrl();
297
+ const matcherCtx: MatcherContext = {
298
+ userAgent: ua,
299
+ url: serverUrl.toString(),
300
+ path: "/",
301
+ cookies: getCookies(),
302
+ request,
303
+ isClientNavigation: isClientNavigation("/", serverUrl),
304
+ flags: [],
305
+ };
306
+ const page = await resolveDecoPage("/", matcherCtx);
307
+ if (!page) return null;
308
+ const flags = persistFlags(matcherCtx);
309
+ const [{ enrichedSections, enrichedSeoSection }, globals] = await Promise.all([
310
+ runSectionLoadersWithSeo(page.resolvedSections, page.seoSection, request),
311
+ resolveGlobals ? resolveSiteGlobals() : Promise.resolve(EMPTY_GLOBALS),
312
+ ]);
313
+
314
+ const mergedSections: ResolvedSection[] = [
315
+ ...dedupeGlobals(globals.resolvedSections, enrichedSections),
316
+ ...enrichedSections,
317
+ ];
318
+
319
+ const eagerKeys = mergedSections.map((s) => s.component);
320
+ await preloadSectionComponents(eagerKeys);
321
+
322
+ const device = detectDevice(ua);
323
+ const seo = buildPageSeo(page.seoSection, enrichedSeoSection, mergedSections);
324
+
325
+ const { seoSection: _seo, ...pageData } = page;
326
+
327
+ return {
328
+ ...pageData,
329
+ resolvedSections: normalizeUrlsInObject(mergedSections),
330
+ deferredSections: normalizeUrlsInObject(page.deferredSections),
331
+ pagePath: "/",
332
+ pageUrl: serverUrl.toString(),
333
+ seo,
334
+ device,
335
+ flags,
336
+ siteGlobals: { rawRefs: globals.rawRefs },
337
+ };
338
+ });
305
339
 
306
340
  // ---------------------------------------------------------------------------
307
341
  // Deferred section loader — resolves + enriches a single section on demand
@@ -448,6 +482,16 @@ export interface CmsRouteOptions {
448
482
  * - `false`: Everything runs on client only.
449
483
  */
450
484
  ssr?: boolean | "data-only";
485
+ /**
486
+ * Resolve `site.theme + site.global + site.pageSections` (the CMS `Site`
487
+ * block) and merge them into every page's sections.
488
+ *
489
+ * Default: `true`. Set to `false` to restore pre-6.7.4 behavior for sites
490
+ * that don't use the `Site` block and don't want it silently activated
491
+ * (see #292) — this also skips the `Theme` section's `<style>` injection,
492
+ * so the site's own `:root` CSS fallbacks apply instead.
493
+ */
494
+ resolveGlobals?: boolean;
451
495
  }
452
496
 
453
497
  type CmsPageLoaderData = {
@@ -658,6 +702,7 @@ export function cmsRouteConfig(options: CmsRouteOptions) {
658
702
  pendingMs = 200,
659
703
  pendingMinMs = 300,
660
704
  ssr: ssrMode,
705
+ resolveGlobals,
661
706
  } = options;
662
707
 
663
708
  const ignoreSet = new Set(ignoreSearchParams);
@@ -689,7 +734,9 @@ export function cmsRouteConfig(options: CmsRouteOptions) {
689
734
  const searchStr = deps.search
690
735
  ? "?" + new URLSearchParams(deps.search as Record<string, string>).toString()
691
736
  : "";
692
- const page = await loadCmsPage({ data: basePath + searchStr });
737
+ const page = await loadCmsPage({
738
+ data: { path: basePath + searchStr, resolveGlobals },
739
+ });
693
740
  if (!page) return page;
694
741
 
695
742
  if (!isServer && page.resolvedSections) {
@@ -746,12 +793,21 @@ export function cmsHomeRouteConfig(options: {
746
793
  pendingMs?: number;
747
794
  /** Minimum display time (ms) for pending component. Default: 300. */
748
795
  pendingMinMs?: number;
796
+ /** See `CmsRouteOptions.resolveGlobals` — default `true`. */
797
+ resolveGlobals?: boolean;
749
798
  }) {
750
- const { defaultTitle, defaultDescription, siteName, pendingMs = 200, pendingMinMs = 300 } = options;
799
+ const {
800
+ defaultTitle,
801
+ defaultDescription,
802
+ siteName,
803
+ pendingMs = 200,
804
+ pendingMinMs = 300,
805
+ resolveGlobals,
806
+ } = options;
751
807
 
752
808
  return {
753
809
  loader: async () => {
754
- const page = await loadCmsHomePage();
810
+ const page = await loadCmsHomePage({ data: { resolveGlobals } });
755
811
  if (!page) return page;
756
812
 
757
813
  if (!isServer && page.resolvedSections) {