@decocms/start 0.26.0 → 0.26.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 +1 -1
- package/src/admin/render.ts +98 -25
- package/src/cms/index.ts +1 -0
- package/src/cms/resolve.ts +65 -0
package/package.json
CHANGED
package/src/admin/render.ts
CHANGED
|
@@ -1,20 +1,60 @@
|
|
|
1
1
|
import { createElement } from "react";
|
|
2
2
|
import { loadBlocks, withBlocksOverride } from "../cms/loader";
|
|
3
3
|
import { getSection } from "../cms/registry";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type MatcherContext,
|
|
6
|
+
type ResolvedSection,
|
|
7
|
+
resolvePageSections,
|
|
8
|
+
resolveValue,
|
|
9
|
+
WELL_KNOWN_TYPES,
|
|
10
|
+
} from "../cms/resolve";
|
|
11
|
+
import { runSingleSectionLoader } from "../cms/sectionLoaders";
|
|
5
12
|
import { buildHtmlShell } from "../sdk/htmlShell";
|
|
6
13
|
import { LIVE_CONTROLS_SCRIPT } from "./liveControls";
|
|
7
14
|
import { getPreviewWrapper } from "./setup";
|
|
8
15
|
|
|
9
16
|
export { setRenderShell, setPreviewWrapper } from "./setup";
|
|
10
17
|
|
|
18
|
+
// Cache the dynamic import — avoids re-importing per section render
|
|
19
|
+
let _renderToString: ((element: any) => string) | null = null;
|
|
20
|
+
async function getRenderToString() {
|
|
21
|
+
if (!_renderToString) {
|
|
22
|
+
const mod = await import("react-dom/server");
|
|
23
|
+
_renderToString = mod.renderToString;
|
|
24
|
+
}
|
|
25
|
+
return _renderToString;
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
function wrapInHtmlShell(sectionHtml: string): string {
|
|
12
29
|
return buildHtmlShell({ body: sectionHtml, script: LIVE_CONTROLS_SCRIPT });
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
/**
|
|
16
|
-
* Render a single
|
|
17
|
-
*
|
|
33
|
+
* Render a single ResolvedSection to an HTML string.
|
|
34
|
+
* Uses the pre-cached renderToString and the preview wrapper.
|
|
35
|
+
*/
|
|
36
|
+
async function renderResolvedSection(section: ResolvedSection): Promise<string> {
|
|
37
|
+
const sectionLoader = getSection(section.component);
|
|
38
|
+
if (!sectionLoader) {
|
|
39
|
+
return `<div style="padding:8px;color:orange;font-size:12px;border:1px dashed orange;margin:4px 0;">Unsupported: ${section.component}</div>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const renderToString = await getRenderToString();
|
|
44
|
+
const mod = await sectionLoader();
|
|
45
|
+
const element = createElement(mod.default, section.props);
|
|
46
|
+
const Wrapper = getPreviewWrapper();
|
|
47
|
+
const wrapped = Wrapper ? createElement(Wrapper, null, element) : element;
|
|
48
|
+
return renderToString(wrapped);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return `<div style="padding:8px;color:red;font-size:12px;">Error rendering ${section.component}: ${(error as Error).message}</div>`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Render a single raw section object (with __resolveType) to HTML.
|
|
56
|
+
* Kept for the single-section preview path where we don't go through
|
|
57
|
+
* resolvePageSections.
|
|
18
58
|
*/
|
|
19
59
|
async function renderOneSection(section: Record<string, unknown>): Promise<string> {
|
|
20
60
|
const resolveType = section.__resolveType as string | undefined;
|
|
@@ -27,7 +67,7 @@ async function renderOneSection(section: Record<string, unknown>): Promise<strin
|
|
|
27
67
|
|
|
28
68
|
try {
|
|
29
69
|
const { __resolveType: _, ...sectionProps } = section;
|
|
30
|
-
const
|
|
70
|
+
const renderToString = await getRenderToString();
|
|
31
71
|
const mod = await sectionLoader();
|
|
32
72
|
const element = createElement(mod.default, sectionProps);
|
|
33
73
|
const Wrapper = getPreviewWrapper();
|
|
@@ -38,6 +78,37 @@ async function renderOneSection(section: Record<string, unknown>): Promise<strin
|
|
|
38
78
|
}
|
|
39
79
|
}
|
|
40
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Build a MatcherContext from the preview request.
|
|
83
|
+
* Enables matchers (device, date, cookie, etc.) to evaluate correctly
|
|
84
|
+
* during preview resolution.
|
|
85
|
+
*/
|
|
86
|
+
function buildPreviewMatcherCtx(request: Request): MatcherContext {
|
|
87
|
+
const url = new URL(request.url);
|
|
88
|
+
const deviceHint = url.searchParams.get("deviceHint");
|
|
89
|
+
const path = url.searchParams.get("path") || "/";
|
|
90
|
+
|
|
91
|
+
let userAgent = request.headers.get("user-agent") ?? "";
|
|
92
|
+
if (deviceHint === "mobile" && !/mobile/i.test(userAgent)) {
|
|
93
|
+
userAgent += " Mobile";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
97
|
+
const cookies: Record<string, string> = {};
|
|
98
|
+
for (const part of cookieHeader.split(";")) {
|
|
99
|
+
const [k, ...v] = part.trim().split("=");
|
|
100
|
+
if (k) cookies[k.trim()] = v.join("=").trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
userAgent,
|
|
105
|
+
url: url.toString(),
|
|
106
|
+
path,
|
|
107
|
+
cookies,
|
|
108
|
+
request,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
41
112
|
/**
|
|
42
113
|
* Handles /live/previews/* -- renders sections to HTML for the admin preview.
|
|
43
114
|
*
|
|
@@ -121,28 +192,30 @@ export async function handleRender(request: Request): Promise<Response> {
|
|
|
121
192
|
}
|
|
122
193
|
}
|
|
123
194
|
|
|
124
|
-
// Page compositor: resolve + render all child sections
|
|
195
|
+
// Page compositor: resolve + render all child sections in parallel
|
|
125
196
|
if (component === WELL_KNOWN_TYPES.PAGE) {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
197
|
+
const matcherCtx = buildPreviewMatcherCtx(request);
|
|
198
|
+
|
|
199
|
+
// resolvePageSections uses the same strategy as resolveDecoPage:
|
|
200
|
+
// parallel section resolution, layout caching, in-flight dedup, memoization
|
|
201
|
+
const resolvedSections = await resolvePageSections(
|
|
202
|
+
props.sections,
|
|
203
|
+
matcherCtx,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Run section loaders in parallel — benefits from layout and cacheable caches
|
|
207
|
+
const enrichedSections = await Promise.all(
|
|
208
|
+
resolvedSections.map((section) =>
|
|
209
|
+
runSingleSectionLoader(section, request).catch(() => section),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Render all sections in parallel
|
|
214
|
+
const htmlParts = await Promise.all(
|
|
215
|
+
enrichedSections.map((section) => renderResolvedSection(section)),
|
|
216
|
+
);
|
|
144
217
|
|
|
145
|
-
return new Response(wrapInHtmlShell(htmlParts.join("\n")), {
|
|
218
|
+
return new Response(wrapInHtmlShell(htmlParts.filter(Boolean).join("\n")), {
|
|
146
219
|
status: 200,
|
|
147
220
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
148
221
|
});
|
|
@@ -163,7 +236,7 @@ export async function handleRender(request: Request): Promise<Response> {
|
|
|
163
236
|
try {
|
|
164
237
|
const resolvedProps = (await resolveValue(props)) as Record<string, unknown>;
|
|
165
238
|
const { __resolveType: _, ...cleanProps } = resolvedProps;
|
|
166
|
-
const
|
|
239
|
+
const renderToString = await getRenderToString();
|
|
167
240
|
const mod = await sectionLoader();
|
|
168
241
|
const element = createElement(mod.default, cleanProps);
|
|
169
242
|
const Wrapper = getPreviewWrapper();
|
package/src/cms/index.ts
CHANGED
package/src/cms/resolve.ts
CHANGED
|
@@ -1276,6 +1276,71 @@ export async function resolveDecoPage(
|
|
|
1276
1276
|
};
|
|
1277
1277
|
}
|
|
1278
1278
|
|
|
1279
|
+
/**
|
|
1280
|
+
* Resolve a raw sections array into ResolvedSection[] with the same
|
|
1281
|
+
* optimizations as resolveDecoPage: parallel resolution, layout caching,
|
|
1282
|
+
* in-flight dedup, and memoization.
|
|
1283
|
+
*
|
|
1284
|
+
* Unlike resolveDecoPage, this does NOT look up a page by path — the
|
|
1285
|
+
* caller provides the raw section array directly. Used by the admin
|
|
1286
|
+
* preview renderer where page data comes from the POST body.
|
|
1287
|
+
*
|
|
1288
|
+
* All sections are resolved eagerly (no deferred/async split) since
|
|
1289
|
+
* admin previews need the full rendered output.
|
|
1290
|
+
*/
|
|
1291
|
+
export async function resolvePageSections(
|
|
1292
|
+
rawSectionsInput: unknown,
|
|
1293
|
+
matcherCtx?: MatcherContext,
|
|
1294
|
+
): Promise<ResolvedSection[]> {
|
|
1295
|
+
ensureInitialized();
|
|
1296
|
+
|
|
1297
|
+
const ctx: MatcherContext = matcherCtx ?? {};
|
|
1298
|
+
const rctx: ResolveContext = { matcherCtx: ctx, memo: new Map(), depth: 0 };
|
|
1299
|
+
|
|
1300
|
+
let rawSections: unknown[];
|
|
1301
|
+
if (Array.isArray(rawSectionsInput)) {
|
|
1302
|
+
rawSections = rawSectionsInput;
|
|
1303
|
+
} else {
|
|
1304
|
+
rawSections = await resolveSectionsList(rawSectionsInput, rctx);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const eagerResults: Promise<ResolvedSection[]>[] = [];
|
|
1308
|
+
|
|
1309
|
+
for (const section of rawSections) {
|
|
1310
|
+
const promise = (async (): Promise<ResolvedSection[]> => {
|
|
1311
|
+
try {
|
|
1312
|
+
const layoutKey = isRawSectionLayout(section);
|
|
1313
|
+
|
|
1314
|
+
if (layoutKey) {
|
|
1315
|
+
const cached = getCachedResolvedLayout(layoutKey);
|
|
1316
|
+
if (cached) return cached;
|
|
1317
|
+
|
|
1318
|
+
const inflight = resolvedLayoutInflight.get(layoutKey);
|
|
1319
|
+
if (inflight) return inflight;
|
|
1320
|
+
|
|
1321
|
+
const p = resolveRawSection(section, rctx).then((results) => {
|
|
1322
|
+
setCachedResolvedLayout(layoutKey, results);
|
|
1323
|
+
return results;
|
|
1324
|
+
});
|
|
1325
|
+
resolvedLayoutInflight.set(layoutKey, p);
|
|
1326
|
+
p.finally(() => resolvedLayoutInflight.delete(layoutKey));
|
|
1327
|
+
return p;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
return resolveRawSection(section, rctx);
|
|
1331
|
+
} catch (e) {
|
|
1332
|
+
onResolveError(e, "section", "Preview section resolution");
|
|
1333
|
+
return [];
|
|
1334
|
+
}
|
|
1335
|
+
})();
|
|
1336
|
+
|
|
1337
|
+
eagerResults.push(promise);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const allResults = await Promise.all(eagerResults);
|
|
1341
|
+
return allResults.flat();
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1279
1344
|
/**
|
|
1280
1345
|
* Resolve a single deferred section's raw props into a fully resolved section.
|
|
1281
1346
|
* Called by the loadDeferredSection server function when a section scrolls into view.
|