@decocms/start 0.29.1 → 0.29.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "0.29.1",
3
+ "version": "0.29.2",
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",
@@ -172,21 +172,34 @@ export function SectionRenderer({ section }: { section: Section | null | undefin
172
172
  return <Comp {...(section.props ?? {})} />;
173
173
  }
174
174
 
175
+ // Sync path: render directly if available — avoids React.lazy SSR streaming issue
176
+ const options = getSectionOptions(section.Component);
177
+ const isClientOnly = options?.clientOnly === true;
178
+ const SyncComp = getSyncComponent(section.Component);
179
+ if (SyncComp && !isClientOnly) {
180
+ return createElement(SyncComp, section.props ?? {});
181
+ }
182
+
175
183
  const Lazy = getLazyComponent(section.Component);
176
184
  if (!Lazy) {
177
185
  console.warn(`[SectionRenderer] No component registered for: ${section.Component}`);
178
186
  return null;
179
187
  }
180
188
 
181
- // Use the section's registered loadingFallback (if available) instead of
182
- // the generic NestedSectionFallback. This lets parent sections (e.g.
183
- // NotFoundChallenge) show a meaningful skeleton for nested children
184
- // (e.g. MountedPDP) while the lazy chunk loads.
185
- const options = getSectionOptions(section.Component);
186
189
  const fallback = options?.loadingFallback
187
190
  ? createElement(options.loadingFallback, section.props ?? {})
188
191
  : <NestedSectionFallback />;
189
192
 
193
+ if (isClientOnly) {
194
+ return (
195
+ <ClientOnly fallback={fallback}>
196
+ <Suspense fallback={null}>
197
+ <Lazy {...(section.props ?? {})} />
198
+ </Suspense>
199
+ </ClientOnly>
200
+ );
201
+ }
202
+
190
203
  return (
191
204
  <Suspense fallback={fallback}>
192
205
  <Lazy {...(section.props ?? {})} />
@@ -501,20 +514,39 @@ export function DecoPageRenderer({
501
514
  <Await promise={promise}>
502
515
  {(resolved) => {
503
516
  if (!resolved) return null;
504
- const LazyComponent = getLazyComponent(resolved.component);
505
- if (!LazyComponent) return null;
506
517
  const resolvedOptions = getSectionOptions(resolved.component);
507
518
  const isClientOnly = resolvedOptions?.clientOnly === true;
519
+ const SyncComp = getSyncComponent(resolved.component);
508
520
  const sectionId = resolved.key
509
521
  .replace(/\//g, "-")
510
522
  .replace(/\.tsx$/, "")
511
523
  .replace(/^site-sections-/, "");
512
524
 
513
- const inner = (
514
- <Suspense fallback={null}>
515
- <LazyComponent {...resolved.props} />
516
- </Suspense>
517
- );
525
+ let inner: ReactNode;
526
+
527
+ if (SyncComp && !isClientOnly) {
528
+ // Sync path: direct render, no lazy/Suspense.
529
+ inner = createElement(SyncComp, resolved.props);
530
+ } else {
531
+ const LazyComponent = getLazyComponent(resolved.component);
532
+ if (!LazyComponent) return null;
533
+
534
+ const fallbackEl = resolvedOptions?.loadingFallback
535
+ ? createElement(resolvedOptions.loadingFallback, resolved.props)
536
+ : null;
537
+
538
+ inner = isClientOnly ? (
539
+ <ClientOnly fallback={fallbackEl}>
540
+ <Suspense fallback={null}>
541
+ <LazyComponent {...resolved.props} />
542
+ </Suspense>
543
+ </ClientOnly>
544
+ ) : (
545
+ <Suspense fallback={null}>
546
+ <LazyComponent {...resolved.props} />
547
+ </Suspense>
548
+ );
549
+ }
518
550
 
519
551
  return (
520
552
  <section
@@ -522,11 +554,7 @@ export function DecoPageRenderer({
522
554
  data-manifest-key={resolved.key}
523
555
  style={{ animation: "decoFadeIn 0.3s ease-out" }}
524
556
  >
525
- {isClientOnly ? (
526
- <ClientOnly fallback={null}>{inner}</ClientOnly>
527
- ) : (
528
- inner
529
- )}
557
+ {inner}
530
558
  </section>
531
559
  );
532
560
  }}
@@ -565,34 +593,36 @@ export function DecoPageRenderer({
565
593
  .replace(/\.tsx$/, "")
566
594
  .replace(/^site-sections-/, "");
567
595
 
568
- // Unified render path: always use React.lazy + Suspense.
569
- // For sync-registered components, getLazyComponent wraps them in a
570
- // pre-fulfilled lazy (via syncThenable) so React renders them
571
- // synchronously — same behavior as the old sync path, but with an
572
- // identical tree structure on both server and client (always has
573
- // <Suspense>). This prevents hydration mismatches when sites remove
574
- // registerSectionsSync.
575
- const LazyComponent = getLazyComponent(section.component);
576
- if (!LazyComponent) return null;
577
-
578
- // ClientOnly sections: render only on client, no SSR, no hydration mismatch.
579
- // Used for analytics scripts, GTM, third-party widgets.
580
596
  const isClientOnly = options?.clientOnly === true;
581
- const fallbackEl = options?.loadingFallback
582
- ? createElement(options.loadingFallback, section.props)
583
- : null;
584
-
585
- const content = isClientOnly ? (
586
- <ClientOnly fallback={fallbackEl}>
597
+ const SyncComponent = getSyncComponent(section.component);
598
+
599
+ let content: ReactNode;
600
+
601
+ if (SyncComponent && !isClientOnly) {
602
+ // Sync path: render directly — no Suspense, no lazy.
603
+ // React SSR streaming ignores syncThenable's pre-fulfilled status
604
+ // and creates empty <template> placeholders. Direct createElement avoids this.
605
+ content = createElement(SyncComponent, section.props);
606
+ } else {
607
+ const LazyComponent = getLazyComponent(section.component);
608
+ if (!LazyComponent) return null;
609
+
610
+ const fallbackEl = options?.loadingFallback
611
+ ? createElement(options.loadingFallback, section.props)
612
+ : null;
613
+
614
+ content = isClientOnly ? (
615
+ <ClientOnly fallback={fallbackEl}>
616
+ <Suspense fallback={null}>
617
+ <LazyComponent {...section.props} />
618
+ </Suspense>
619
+ </ClientOnly>
620
+ ) : (
587
621
  <Suspense fallback={null}>
588
622
  <LazyComponent {...section.props} />
589
623
  </Suspense>
590
- </ClientOnly>
591
- ) : (
592
- <Suspense fallback={null}>
593
- <LazyComponent {...section.props} />
594
- </Suspense>
595
- );
624
+ );
625
+ }
596
626
 
597
627
  // Dev warning: eager section not sync-registered may blank during hydration
598
628
  if (isDev && !isClientOnly && !getSyncComponent(section.component)) {