@decocms/start 0.29.0 → 0.29.1

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/start",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -239,6 +239,22 @@ export async function runSectionLoaders(
239
239
  sections: ResolvedSection[],
240
240
  request: Request,
241
241
  ): Promise<ResolvedSection[]> {
242
+ // Dev warning: detect likely layout sections not registered via registerLayoutSections.
243
+ // Without registration, Header/Footer won't be cached across navigations.
244
+ if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
245
+ for (const s of sections) {
246
+ const key = s.component.toLowerCase();
247
+ if (
248
+ (key.includes("header") || key.includes("footer")) &&
249
+ !layoutSections.has(s.component)
250
+ ) {
251
+ console.warn(
252
+ `[SectionLoaders] "${s.component}" looks like a layout section but is not in registerLayoutSections(). ` +
253
+ `Add it to registerLayoutSections() in setup.ts for consistent caching across navigations.`,
254
+ );
255
+ }
256
+ }
257
+ }
242
258
  return Promise.all(sections.map((section) => runSingleSectionLoader(section, request)));
243
259
  }
244
260
 
@@ -27,6 +27,25 @@ describe("validateDeferredSectionInput", () => {
27
27
  expect(result.pageUrl).toBe("https://store.com/home?ref=nav");
28
28
  });
29
29
 
30
+ it("accepts input with index for SPA navigation ordering", () => {
31
+ const result = validateDeferredSectionInput({
32
+ component: "site/sections/Shelf.tsx",
33
+ rawProps: {},
34
+ pagePath: "/",
35
+ index: 5,
36
+ });
37
+ expect(result.index).toBe(5);
38
+ });
39
+
40
+ it("index is undefined when not provided", () => {
41
+ const result = validateDeferredSectionInput({
42
+ component: "site/sections/Shelf.tsx",
43
+ rawProps: {},
44
+ pagePath: "/",
45
+ });
46
+ expect(result.index).toBeUndefined();
47
+ });
48
+
30
49
  it("throws on null input", () => {
31
50
  expect(() => validateDeferredSectionInput(null)).toThrow("Expected an object");
32
51
  });
@@ -85,6 +104,17 @@ describe("validateDeferredSectionInput", () => {
85
104
  }),
86
105
  ).toThrow("pageUrl");
87
106
  });
107
+
108
+ it("throws when index is not a number", () => {
109
+ expect(() =>
110
+ validateDeferredSectionInput({
111
+ component: "X",
112
+ rawProps: {},
113
+ pagePath: "/",
114
+ index: "three" as any,
115
+ }),
116
+ ).toThrow("index");
117
+ });
88
118
  });
89
119
 
90
120
  describe("createSectionValidator", () => {
@@ -21,6 +21,8 @@ export interface DeferredSectionInput {
21
21
  rawProps: Record<string, unknown>;
22
22
  pagePath: string;
23
23
  pageUrl?: string;
24
+ /** Original position in the page section list — for correct ordering after resolution. */
25
+ index?: number;
24
26
  }
25
27
 
26
28
  /**
@@ -57,11 +59,18 @@ export function validateDeferredSectionInput(data: unknown): DeferredSectionInpu
57
59
  );
58
60
  }
59
61
 
62
+ if (obj.index !== undefined && typeof obj.index !== "number") {
63
+ throw new Error(
64
+ "[validateDeferredSectionInput] Invalid 'index' field (expected number or undefined)",
65
+ );
66
+ }
67
+
60
68
  return {
61
69
  component: obj.component as string,
62
70
  rawProps: obj.rawProps as Record<string, unknown>,
63
71
  pagePath: obj.pagePath as string,
64
72
  pageUrl: obj.pageUrl as string | undefined,
73
+ index: obj.index as number | undefined,
65
74
  };
66
75
  }
67
76
 
@@ -193,10 +193,17 @@ export const loadCmsHomePage = createServerFn({ method: "GET" }).handler(async (
193
193
  export const loadDeferredSection = createServerFn({ method: "POST" })
194
194
  .inputValidator(
195
195
  (data: unknown) =>
196
- data as { component: string; rawProps: Record<string, any>; pagePath: string; pageUrl?: string },
196
+ data as {
197
+ component: string;
198
+ rawProps: Record<string, any>;
199
+ pagePath: string;
200
+ pageUrl?: string;
201
+ /** Original position in the page section list — preserved for correct SPA ordering. */
202
+ index?: number;
203
+ },
197
204
  )
198
205
  .handler(async (ctx) => {
199
- const { component, rawProps, pagePath, pageUrl } = ctx.data;
206
+ const { component, rawProps, pagePath, pageUrl, index } = ctx.data;
200
207
 
201
208
  const originRequest = getRequest();
202
209
  const serverUrl = getRequestUrl().toString();
@@ -211,6 +218,9 @@ export const loadDeferredSection = createServerFn({ method: "POST" })
211
218
  const section = await resolveDeferredSection(component, rawProps, pagePath, matcherCtx);
212
219
  if (!section) return null;
213
220
 
221
+ // Preserve original index for correct ordering in SPA navigation merge
222
+ if (index !== undefined) section.index = index;
223
+
214
224
  const request = new Request(pageUrl || serverUrl, {
215
225
  headers: originRequest.headers,
216
226
  });
@@ -510,17 +520,33 @@ export function cmsRouteConfig(options: CmsRouteOptions) {
510
520
  // SSR: create unawaited promises for TanStack native streaming.
511
521
  // Each deferred section becomes a promise that TanStack streams
512
522
  // via SSR chunked transfer — all resolved in the SAME request.
523
+ //
524
+ // IMPORTANT: We call resolveDeferredSectionFull directly instead of
525
+ // loadDeferredSection (server function). Server functions serialize
526
+ // their return via JSON-RPC — TanStack only streams promises that
527
+ // are directly in the loader return, not serialized server fn results.
513
528
  if (isServer && page.deferredSections?.length) {
529
+ const originRequest = getRequest();
530
+ const serverUrl = getRequestUrl();
531
+ const matcherCtx: MatcherContext = {
532
+ userAgent: getRequestHeader("user-agent") ?? "",
533
+ url: page.pageUrl ?? serverUrl.toString(),
534
+ path: page.pagePath ?? basePath,
535
+ cookies: getCookies(),
536
+ request: originRequest,
537
+ };
538
+ const deferredRequest = new Request(page.pageUrl ?? serverUrl.toString(), {
539
+ headers: originRequest.headers,
540
+ });
541
+
514
542
  const deferredPromises: Record<string, Promise<ResolvedSection | null>> = {};
515
543
  for (const ds of page.deferredSections) {
516
- deferredPromises[`d_${ds.index}`] = loadDeferredSection({
517
- data: {
518
- component: ds.component,
519
- rawProps: ds.rawProps,
520
- pagePath: page.pagePath ?? basePath,
521
- pageUrl: page.pageUrl,
522
- },
523
- }).catch((e) => {
544
+ deferredPromises[`d_${ds.index}`] = resolveDeferredSectionFull(
545
+ ds,
546
+ page.pagePath ?? basePath,
547
+ deferredRequest,
548
+ matcherCtx,
549
+ ).catch((e) => {
524
550
  console.error(`[CMS] Deferred section "${ds.component}" failed:`, e);
525
551
  return null;
526
552
  });
@@ -539,6 +565,7 @@ export function cmsRouteConfig(options: CmsRouteOptions) {
539
565
  rawProps: ds.rawProps,
540
566
  pagePath: page.pagePath ?? basePath,
541
567
  pageUrl: page.pageUrl,
568
+ index: ds.index,
542
569
  },
543
570
  }).catch(() => null),
544
571
  ),
@@ -601,16 +628,24 @@ export function cmsHomeRouteConfig(options: {
601
628
 
602
629
  // SSR: create unawaited promises for TanStack native streaming
603
630
  if (isServer && page.deferredSections?.length) {
631
+ const originRequest = getRequest();
632
+ const serverUrl = getRequestUrl();
633
+ const matcherCtx: MatcherContext = {
634
+ userAgent: getRequestHeader("user-agent") ?? "",
635
+ url: page.pageUrl ?? serverUrl.toString(),
636
+ path: "/",
637
+ cookies: getCookies(),
638
+ request: originRequest,
639
+ };
640
+ const deferredRequest = new Request(page.pageUrl ?? serverUrl.toString(), {
641
+ headers: originRequest.headers,
642
+ });
643
+
604
644
  const deferredPromises: Record<string, Promise<ResolvedSection | null>> = {};
605
645
  for (const ds of page.deferredSections) {
606
- deferredPromises[`d_${ds.index}`] = loadDeferredSection({
607
- data: {
608
- component: ds.component,
609
- rawProps: ds.rawProps,
610
- pagePath: "/",
611
- pageUrl: page.pageUrl,
612
- },
613
- }).catch((e) => {
646
+ deferredPromises[`d_${ds.index}`] = resolveDeferredSectionFull(
647
+ ds, "/", deferredRequest, matcherCtx,
648
+ ).catch((e) => {
614
649
  console.error(`[CMS] Deferred section "${ds.component}" failed:`, e);
615
650
  return null;
616
651
  });
@@ -628,6 +663,7 @@ export function cmsHomeRouteConfig(options: {
628
663
  rawProps: ds.rawProps,
629
664
  pagePath: "/",
630
665
  pageUrl: page.pageUrl,
666
+ index: ds.index,
631
667
  },
632
668
  }).catch(() => null),
633
669
  ),