@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
|
@@ -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
|
|
package/src/routes/cmsRoute.ts
CHANGED
|
@@ -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 {
|
|
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}`] =
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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}`] =
|
|
607
|
-
|
|
608
|
-
|
|
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
|
),
|