@decocms/start 0.29.2 → 0.30.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 +1 -1
- package/src/cms/resolve.test.ts +102 -1
- package/src/cms/resolve.ts +15 -1
- package/src/routes/cmsRoute.ts +27 -0
- package/src/routes/index.ts +1 -0
package/package.json
CHANGED
package/src/cms/resolve.test.ts
CHANGED
|
@@ -19,7 +19,7 @@ vi.mock("./registry", () => ({
|
|
|
19
19
|
getSection: vi.fn(),
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
|
-
import { resolveDeferredSectionFull } from "./resolve";
|
|
22
|
+
import { resolveDeferredSectionFull, resolveSectionsList } from "./resolve";
|
|
23
23
|
import { runSingleSectionLoader } from "./sectionLoaders";
|
|
24
24
|
import { normalizeUrlsInObject } from "../sdk/normalizeUrls";
|
|
25
25
|
import type { DeferredSection } from "./resolve";
|
|
@@ -65,3 +65,104 @@ describe("resolveDeferredSectionFull", () => {
|
|
|
65
65
|
expect(result).toEqual(input); // mock passes through
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// resolveSectionsList — page-level variant wrapper support
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
describe("resolveSectionsList", () => {
|
|
74
|
+
const makeRctx = (matcherCtx = {}) => ({
|
|
75
|
+
routeParams: {},
|
|
76
|
+
matcherCtx,
|
|
77
|
+
memo: new Map(),
|
|
78
|
+
depth: 0,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns array as-is when value is already an array", async () => {
|
|
82
|
+
const sections = [{ __resolveType: "section-A" }, { __resolveType: "section-B" }];
|
|
83
|
+
const result = await resolveSectionsList(sections, makeRctx());
|
|
84
|
+
expect(result).toEqual(sections);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns empty array for null/undefined/non-object", async () => {
|
|
88
|
+
expect(await resolveSectionsList(null, makeRctx())).toEqual([]);
|
|
89
|
+
expect(await resolveSectionsList(undefined, makeRctx())).toEqual([]);
|
|
90
|
+
expect(await resolveSectionsList("string", makeRctx())).toEqual([]);
|
|
91
|
+
expect(await resolveSectionsList(42, makeRctx())).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("resolves page-level variant wrapper without __resolveType", async () => {
|
|
95
|
+
// Simulates CMS admin wrapping all sections in a device variant
|
|
96
|
+
// Rule has no __resolveType → evaluateMatcher returns true (match-all)
|
|
97
|
+
const sectionsArray = [
|
|
98
|
+
{ __resolveType: "Header - 01" },
|
|
99
|
+
{ __resolveType: "site/sections/Account/PersonalData.tsx" },
|
|
100
|
+
{ __resolveType: "Footer - 01" },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const variantWrapper = {
|
|
104
|
+
variants: [
|
|
105
|
+
{
|
|
106
|
+
rule: { mobile: true, tablet: true, desktop: true },
|
|
107
|
+
value: sectionsArray,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const result = await resolveSectionsList(variantWrapper, makeRctx());
|
|
113
|
+
expect(result).toEqual(sectionsArray);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("returns empty when no variant matches in page-level wrapper", async () => {
|
|
117
|
+
// All variants have __resolveType in rule → evaluateMatcher returns false
|
|
118
|
+
// (unregistered matcher defaults to false)
|
|
119
|
+
const variantWrapper = {
|
|
120
|
+
variants: [
|
|
121
|
+
{
|
|
122
|
+
rule: { __resolveType: "website/matchers/device.ts", mobile: true },
|
|
123
|
+
value: [{ __resolveType: "MobileOnly" }],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const result = await resolveSectionsList(variantWrapper, makeRctx());
|
|
129
|
+
expect(result).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("picks first matching variant in page-level wrapper", async () => {
|
|
133
|
+
const desktopSections = [{ __resolveType: "DesktopLayout" }];
|
|
134
|
+
const mobileSections = [{ __resolveType: "MobileLayout" }];
|
|
135
|
+
|
|
136
|
+
const variantWrapper = {
|
|
137
|
+
variants: [
|
|
138
|
+
{
|
|
139
|
+
// No __resolveType → evaluateMatcher returns true (first match wins)
|
|
140
|
+
rule: { desktop: true },
|
|
141
|
+
value: desktopSections,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
rule: { mobile: true },
|
|
145
|
+
value: mobileSections,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = await resolveSectionsList(variantWrapper, makeRctx());
|
|
151
|
+
expect(result).toEqual(desktopSections);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("returns empty for object without __resolveType and without variants", async () => {
|
|
155
|
+
const result = await resolveSectionsList({ someKey: "value" }, makeRctx());
|
|
156
|
+
expect(result).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("respects max depth limit (20)", async () => {
|
|
160
|
+
// Build 21 levels of nested variant wrappers to exceed MAX_RESOLVE_DEPTH=20
|
|
161
|
+
let wrapper: any = [{ __resolveType: "deep" }];
|
|
162
|
+
for (let i = 0; i < 21; i++) {
|
|
163
|
+
wrapper = { variants: [{ rule: {}, value: wrapper }] };
|
|
164
|
+
}
|
|
165
|
+
const result = await resolveSectionsList(wrapper, makeRctx());
|
|
166
|
+
expect(result).toEqual([]);
|
|
167
|
+
});
|
|
168
|
+
});
|
package/src/cms/resolve.ts
CHANGED
|
@@ -1015,7 +1015,7 @@ function resolveSectionShallow(
|
|
|
1015
1015
|
* to get the raw section array without resolving each individual section.
|
|
1016
1016
|
* This allows the eager/deferred split to happen before section resolution.
|
|
1017
1017
|
*/
|
|
1018
|
-
async function resolveSectionsList(
|
|
1018
|
+
export async function resolveSectionsList(
|
|
1019
1019
|
value: unknown,
|
|
1020
1020
|
rctx: ResolveContext,
|
|
1021
1021
|
depth = 0,
|
|
@@ -1026,6 +1026,20 @@ async function resolveSectionsList(
|
|
|
1026
1026
|
|
|
1027
1027
|
const obj = value as Record<string, unknown>;
|
|
1028
1028
|
const rt = obj.__resolveType as string | undefined;
|
|
1029
|
+
|
|
1030
|
+
// Page-level variant wrapper without __resolveType
|
|
1031
|
+
// (CMS admin wraps ALL sections in a variant object with { variants: [...] })
|
|
1032
|
+
if (!rt && Array.isArray(obj.variants)) {
|
|
1033
|
+
const variants = obj.variants as Array<{ value: unknown; rule?: unknown }>;
|
|
1034
|
+
for (const variant of variants) {
|
|
1035
|
+
const rule = variant.rule as Record<string, unknown> | undefined;
|
|
1036
|
+
if (evaluateMatcher(rule, rctx.matcherCtx)) {
|
|
1037
|
+
return resolveSectionsList(variant.value, rctx, depth + 1);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return [];
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1029
1043
|
if (!rt) return [];
|
|
1030
1044
|
|
|
1031
1045
|
// Multivariate flags — evaluate matchers and recurse into matched variant
|
package/src/routes/cmsRoute.ts
CHANGED
|
@@ -228,6 +228,33 @@ export const loadDeferredSection = createServerFn({ method: "POST" })
|
|
|
228
228
|
return normalizeUrlsInObject(enriched);
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Pre-wrapped deferred section loader for IntersectionObserver-based rendering
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Convenience wrapper around `loadDeferredSection` that matches the
|
|
237
|
+
* `loadDeferredSectionFn` prop signature of `DecoPageRenderer`.
|
|
238
|
+
*
|
|
239
|
+
* Pass this directly to `<DecoPageRenderer loadDeferredSectionFn={deferredSectionLoader} />`
|
|
240
|
+
* to enable IntersectionObserver-based lazy loading of deferred sections.
|
|
241
|
+
*/
|
|
242
|
+
export const deferredSectionLoader = async ({
|
|
243
|
+
component,
|
|
244
|
+
rawProps,
|
|
245
|
+
pagePath,
|
|
246
|
+
pageUrl,
|
|
247
|
+
}: {
|
|
248
|
+
component: string;
|
|
249
|
+
rawProps: Record<string, unknown>;
|
|
250
|
+
pagePath: string;
|
|
251
|
+
pageUrl?: string;
|
|
252
|
+
}): Promise<ResolvedSection | null> => {
|
|
253
|
+
return loadDeferredSection({
|
|
254
|
+
data: { component, rawProps, pagePath, pageUrl },
|
|
255
|
+
});
|
|
256
|
+
};
|
|
257
|
+
|
|
231
258
|
// ---------------------------------------------------------------------------
|
|
232
259
|
// Default pending component — shown during SPA navigation while loader runs
|
|
233
260
|
// ---------------------------------------------------------------------------
|