@decocms/start 0.28.1 → 0.28.3
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/hooks/DecoPageRenderer.tsx +22 -23
- package/src/routes/cmsRoute.ts +24 -0
- package/src/routes/index.ts +1 -0
- package/src/vite/plugin.js +21 -0
package/package.json
CHANGED
|
@@ -42,15 +42,20 @@ function syncThenable(mod: {
|
|
|
42
42
|
|
|
43
43
|
function getLazyComponent(key: string) {
|
|
44
44
|
if (!lazyCache.has(key)) {
|
|
45
|
+
// If sync-registered, wrap in a pre-fulfilled lazy so React.lazy
|
|
46
|
+
// resolves synchronously — same tree structure as lazy-only path.
|
|
47
|
+
const sync = getSyncComponent(key);
|
|
48
|
+
if (sync) {
|
|
49
|
+
lazyCache.set(key, lazy(() => syncThenable({ default: sync })));
|
|
50
|
+
return lazyCache.get(key)!;
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
const registry = getSectionRegistry();
|
|
46
54
|
const loader = registry[key];
|
|
47
55
|
if (!loader) return null;
|
|
48
56
|
lazyCache.set(
|
|
49
57
|
key,
|
|
50
58
|
lazy(() => {
|
|
51
|
-
// If already resolved (from preloadSectionComponents on server,
|
|
52
|
-
// or from route loader on client SPA), return a sync thenable.
|
|
53
|
-
// React reads thenable.status/value synchronously — no Suspense.
|
|
54
59
|
const resolved = getResolvedComponent(key);
|
|
55
60
|
if (resolved) {
|
|
56
61
|
return syncThenable({ default: resolved });
|
|
@@ -409,6 +414,7 @@ function mergeSections(resolved: ResolvedSection[], deferred: DeferredSection[])
|
|
|
409
414
|
// DecoPageRenderer — renders top-level resolved sections from a CMS page
|
|
410
415
|
// ---------------------------------------------------------------------------
|
|
411
416
|
|
|
417
|
+
|
|
412
418
|
interface Props {
|
|
413
419
|
sections: ResolvedSection[];
|
|
414
420
|
deferredSections?: DeferredSection[];
|
|
@@ -470,32 +476,25 @@ export function DecoPageRenderer({
|
|
|
470
476
|
.replace(/\.tsx$/, "")
|
|
471
477
|
.replace(/^site-sections-/, "");
|
|
472
478
|
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
<section key={`${section.key}-${index}`} id={sectionId} data-manifest-key={section.key}>
|
|
481
|
-
<SectionErrorBoundary sectionKey={section.key} fallback={errFallback}>
|
|
482
|
-
<SyncComp {...section.props} />
|
|
483
|
-
</SectionErrorBoundary>
|
|
484
|
-
</section>
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Fallback: React.lazy with syncThenable for pre-loaded modules.
|
|
489
|
-
// fallback={null} preserves server HTML during hydration.
|
|
479
|
+
// Unified render path: always use React.lazy + Suspense.
|
|
480
|
+
// For sync-registered components, getLazyComponent wraps them in a
|
|
481
|
+
// pre-fulfilled lazy (via syncThenable) so React renders them
|
|
482
|
+
// synchronously — same behavior as the old sync path, but with an
|
|
483
|
+
// identical tree structure on both server and client (always has
|
|
484
|
+
// <Suspense>). This prevents hydration mismatches when sites remove
|
|
485
|
+
// registerSectionsSync.
|
|
490
486
|
const LazyComponent = getLazyComponent(section.component);
|
|
491
487
|
if (!LazyComponent) return null;
|
|
488
|
+
const content = (
|
|
489
|
+
<Suspense fallback={null}>
|
|
490
|
+
<LazyComponent {...section.props} />
|
|
491
|
+
</Suspense>
|
|
492
|
+
);
|
|
492
493
|
|
|
493
494
|
return (
|
|
494
495
|
<section key={`${section.key}-${index}`} id={sectionId} data-manifest-key={section.key}>
|
|
495
496
|
<SectionErrorBoundary sectionKey={section.key} fallback={errFallback}>
|
|
496
|
-
|
|
497
|
-
<LazyComponent {...section.props} />
|
|
498
|
-
</Suspense>
|
|
497
|
+
{content}
|
|
499
498
|
</SectionErrorBoundary>
|
|
500
499
|
</section>
|
|
501
500
|
);
|
package/src/routes/cmsRoute.ts
CHANGED
|
@@ -50,6 +50,19 @@ import { type Device, detectDevice } from "../sdk/useDevice";
|
|
|
50
50
|
|
|
51
51
|
const isServer = typeof document === "undefined";
|
|
52
52
|
|
|
53
|
+
// Section chunk manifest — maps section keys to chunk filenames.
|
|
54
|
+
// Generated by decoVitePlugin's generateBundle hook. Used to emit
|
|
55
|
+
// <link rel="modulepreload"> hints for eager sections.
|
|
56
|
+
let sectionChunkMap: Record<string, string> = {};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Set the section chunk manifest. Called by the site's worker entry
|
|
60
|
+
* or setup after loading the manifest.
|
|
61
|
+
*/
|
|
62
|
+
export function setSectionChunkMap(map: Record<string, string>): void {
|
|
63
|
+
sectionChunkMap = map;
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
// ---------------------------------------------------------------------------
|
|
54
67
|
// Server function — loads a CMS page, runs section loaders, detects cache
|
|
55
68
|
// ---------------------------------------------------------------------------
|
|
@@ -251,6 +264,7 @@ type CmsPageLoaderData = {
|
|
|
251
264
|
cacheProfile?: CacheProfile;
|
|
252
265
|
seo?: PageSeo;
|
|
253
266
|
device?: Device;
|
|
267
|
+
resolvedSections?: Array<{ component: string }>;
|
|
254
268
|
} | null;
|
|
255
269
|
|
|
256
270
|
// ---------------------------------------------------------------------------
|
|
@@ -386,6 +400,16 @@ function buildHead(
|
|
|
386
400
|
links.push({ rel: "canonical", href: canonical });
|
|
387
401
|
}
|
|
388
402
|
|
|
403
|
+
// Modulepreload hints for eager section chunks
|
|
404
|
+
if (loaderData?.resolvedSections) {
|
|
405
|
+
for (const s of loaderData.resolvedSections) {
|
|
406
|
+
const chunkFile = sectionChunkMap[s.component];
|
|
407
|
+
if (chunkFile) {
|
|
408
|
+
links.push({ rel: "modulepreload", href: `/${chunkFile}` });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
389
413
|
// JSON-LD structured data — rendered as <script type="application/ld+json"> in <head>
|
|
390
414
|
const scripts: Array<{ type: string; children: string }> = [];
|
|
391
415
|
if (seo?.jsonLDs?.length) {
|
package/src/routes/index.ts
CHANGED
package/src/vite/plugin.js
CHANGED
|
@@ -88,6 +88,27 @@ export function decoVitePlugin() {
|
|
|
88
88
|
env.optimizeDeps.esbuildOptions.jsxImportSource = "react";
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
|
+
|
|
92
|
+
generateBundle(_, bundle) {
|
|
93
|
+
// Build a mapping from section key to chunk filename.
|
|
94
|
+
// Sites use this to emit <link rel="modulepreload"> for eager sections.
|
|
95
|
+
const map = {};
|
|
96
|
+
for (const [fileName, chunk] of Object.entries(bundle)) {
|
|
97
|
+
if (chunk.type === "chunk" && chunk.facadeModuleId) {
|
|
98
|
+
const match = chunk.facadeModuleId.match(/\/(sections\/.+\.tsx)$/);
|
|
99
|
+
if (match) {
|
|
100
|
+
map["site/" + match[1]] = fileName;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (Object.keys(map).length > 0) {
|
|
105
|
+
this.emitFile({
|
|
106
|
+
type: "asset",
|
|
107
|
+
fileName: "section-chunks.json",
|
|
108
|
+
source: JSON.stringify(map),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
},
|
|
91
112
|
};
|
|
92
113
|
|
|
93
114
|
return plugin;
|