@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "0.28.1",
3
+ "version": "0.28.3",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -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
- // Only use sync path for sections explicitly registered via registerSectionsSync.
474
- // DO NOT fallback to getResolvedComponent: that is populated server-side by
475
- // preloadSectionComponents but NOT on the client, causing hydration mismatches
476
- // (server renders <ul>, client renders <Suspense> for the same component).
477
- const SyncComp = getSyncComponent(section.component);
478
- if (SyncComp) {
479
- return (
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
- <Suspense fallback={null}>
497
- <LazyComponent {...section.props} />
498
- </Suspense>
497
+ {content}
499
498
  </SectionErrorBoundary>
500
499
  </section>
501
500
  );
@@ -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) {
@@ -11,6 +11,7 @@ export {
11
11
  loadCmsHomePage,
12
12
  loadCmsPage,
13
13
  loadDeferredSection,
14
+ setSectionChunkMap,
14
15
  } from "./cmsRoute";
15
16
  export { CmsPage, NotFoundPage } from "./components";
16
17
  export type { PageSeo } from "../cms/resolve";
@@ -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;