@decocms/start 0.28.1 → 0.28.2
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 +42 -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,25 @@ function mergeSections(resolved: ResolvedSection[], deferred: DeferredSection[])
|
|
|
409
414
|
// DecoPageRenderer — renders top-level resolved sections from a CMS page
|
|
410
415
|
// ---------------------------------------------------------------------------
|
|
411
416
|
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// IdleHydrationBoundary — delays hydration of below-fold sections until idle
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
function IdleHydrationBoundary({ children }: { children: ReactNode }) {
|
|
422
|
+
const [ready, setReady] = useState(typeof document === "undefined"); // SSR: always ready
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
if (typeof requestIdleCallback === "function") {
|
|
425
|
+
const id = requestIdleCallback(() => setReady(true));
|
|
426
|
+
return () => cancelIdleCallback(id);
|
|
427
|
+
}
|
|
428
|
+
// Fallback for browsers without requestIdleCallback
|
|
429
|
+
const id = setTimeout(() => setReady(true), 50);
|
|
430
|
+
return () => clearTimeout(id);
|
|
431
|
+
}, []);
|
|
432
|
+
// Before ready, return null — React preserves SSR HTML via Suspense fallback={null}
|
|
433
|
+
return ready ? <>{children}</> : null;
|
|
434
|
+
}
|
|
435
|
+
|
|
412
436
|
interface Props {
|
|
413
437
|
sections: ResolvedSection[];
|
|
414
438
|
deferredSections?: DeferredSection[];
|
|
@@ -470,32 +494,27 @@ export function DecoPageRenderer({
|
|
|
470
494
|
.replace(/\.tsx$/, "")
|
|
471
495
|
.replace(/^site-sections-/, "");
|
|
472
496
|
|
|
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.
|
|
497
|
+
// Unified render path: always use React.lazy + Suspense.
|
|
498
|
+
// For sync-registered components, getLazyComponent wraps them in a
|
|
499
|
+
// pre-fulfilled lazy (via syncThenable) so React renders them
|
|
500
|
+
// synchronously — same behavior as the old sync path, but with an
|
|
501
|
+
// identical tree structure on both server and client (always has
|
|
502
|
+
// <Suspense>). This prevents hydration mismatches when sites remove
|
|
503
|
+
// registerSectionsSync.
|
|
490
504
|
const LazyComponent = getLazyComponent(section.component);
|
|
491
505
|
if (!LazyComponent) return null;
|
|
492
506
|
|
|
507
|
+
const isAboveFold = item.originalIndex < 3;
|
|
508
|
+
const content = (
|
|
509
|
+
<Suspense fallback={null}>
|
|
510
|
+
<LazyComponent {...section.props} />
|
|
511
|
+
</Suspense>
|
|
512
|
+
);
|
|
513
|
+
|
|
493
514
|
return (
|
|
494
515
|
<section key={`${section.key}-${index}`} id={sectionId} data-manifest-key={section.key}>
|
|
495
516
|
<SectionErrorBoundary sectionKey={section.key} fallback={errFallback}>
|
|
496
|
-
<
|
|
497
|
-
<LazyComponent {...section.props} />
|
|
498
|
-
</Suspense>
|
|
517
|
+
{isAboveFold ? content : <IdleHydrationBoundary>{content}</IdleHydrationBoundary>}
|
|
499
518
|
</SectionErrorBoundary>
|
|
500
519
|
</section>
|
|
501
520
|
);
|
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;
|