@decocms/start 1.1.0 → 1.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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",
@@ -17,6 +17,7 @@
17
17
  "./sdk/useScript": "./src/sdk/useScript.ts",
18
18
  "./sdk/signal": "./src/sdk/signal.ts",
19
19
  "./sdk/clx": "./src/sdk/clx.ts",
20
+ "./sdk/retry": "./src/sdk/retry.ts",
20
21
  "./sdk/useId": "./src/sdk/useId.ts",
21
22
  "./sdk/cookie": "./src/sdk/cookie.ts",
22
23
  "./sdk/cookiePassthrough": "./src/sdk/cookiePassthrough.ts",
@@ -77,7 +77,7 @@ export function applySectionConventions(input: ApplySectionConventionsInput): vo
77
77
  }
78
78
 
79
79
  if (eagerSections.length > 0) {
80
- const existing = getAsyncRenderingConfig();
80
+ const existing = getAsyncRenderingConfig() ?? {};
81
81
  setAsyncRenderingConfig({
82
82
  ...existing,
83
83
  alwaysEager: [...(existing.alwaysEager ?? []), ...eagerSections],
@@ -0,0 +1,23 @@
1
+ import {
2
+ createMemoryHistory,
3
+ createRootRoute,
4
+ createRouter,
5
+ RouterContextProvider,
6
+ } from "@tanstack/react-router";
7
+ import type { ReactNode } from "react";
8
+
9
+ const rootRoute = createRootRoute();
10
+
11
+ const previewRouter = createRouter({
12
+ routeTree: rootRoute,
13
+ history: createMemoryHistory({ initialEntries: ["/"] }),
14
+ });
15
+
16
+ /**
17
+ * Default preview wrapper for admin iframe rendering.
18
+ * Provides a TanStack Router context with memory history so components
19
+ * that depend on router hooks (Link, useNavigate, etc.) work in previews.
20
+ */
21
+ export default function PreviewProviders({ children }: { children: ReactNode }) {
22
+ return <RouterContextProvider router={previewRouter as any}>{children}</RouterContextProvider>;
23
+ }
@@ -0,0 +1,73 @@
1
+ import { lazy, Suspense, type ComponentType } from "react";
2
+ import { getSection } from "../cms/registry";
3
+
4
+ const componentCache = new Map<string, ComponentType<any>>();
5
+
6
+ function getOrCreateLazy(resolveType: string): ComponentType<any> | null {
7
+ if (componentCache.has(resolveType)) {
8
+ return componentCache.get(resolveType)!;
9
+ }
10
+
11
+ const loader = getSection(resolveType);
12
+ if (!loader) return null;
13
+
14
+ const LazyComponent = lazy(async () => {
15
+ const mod = await loader();
16
+ return { default: mod.default };
17
+ });
18
+
19
+ componentCache.set(resolveType, LazyComponent);
20
+ return LazyComponent;
21
+ }
22
+
23
+ interface SectionLike {
24
+ __resolveType?: string;
25
+ Component?: ComponentType<any>;
26
+ props?: Record<string, unknown>;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ /**
31
+ * Renders a Section-type prop from the CMS.
32
+ *
33
+ * Handles both the old Deco format ({ Component, props }) and
34
+ * the new @decocms/start format ({ __resolveType, ...props }).
35
+ */
36
+ export default function RenderSection({
37
+ section,
38
+ fallback,
39
+ }: {
40
+ section: SectionLike | null | undefined;
41
+ fallback?: React.ReactNode;
42
+ }) {
43
+ if (!section) return null;
44
+
45
+ if (section.Component) {
46
+ const { Component, props = {}, ...overrides } = section;
47
+ const mergedProps = { ...(props as Record<string, unknown>), ...overrides };
48
+ if (typeof Component === "string") {
49
+ const Comp = getOrCreateLazy(Component);
50
+ if (!Comp) return <>{fallback ?? null}</>;
51
+ return (
52
+ <Suspense fallback={fallback ?? null}>
53
+ <Comp {...mergedProps} />
54
+ </Suspense>
55
+ );
56
+ }
57
+ return <Component {...mergedProps} />;
58
+ }
59
+
60
+ if (section.__resolveType) {
61
+ const Comp = getOrCreateLazy(section.__resolveType);
62
+ if (!Comp) return <>{fallback ?? null}</>;
63
+
64
+ const { __resolveType: _, ...props } = section;
65
+ return (
66
+ <Suspense fallback={fallback ?? null}>
67
+ <Comp {...props} />
68
+ </Suspense>
69
+ );
70
+ }
71
+
72
+ return null;
73
+ }
@@ -9,3 +9,5 @@ export { SectionErrorBoundary } from "./SectionErrorFallback";
9
9
  export { NavigationProgress } from "./NavigationProgress";
10
10
  export { StableOutlet } from "./StableOutlet";
11
11
  export { DecoRootLayout, type DecoRootLayoutProps } from "./DecoRootLayout";
12
+ export { default as RenderSection } from "./RenderSection";
13
+ export { default as PreviewProviders } from "./PreviewProviders";
@@ -0,0 +1,45 @@
1
+ export const CONNECTION_CLOSED_MESSAGE = "connection closed before message completed";
2
+
3
+ function sleep(ms: number): Promise<void> {
4
+ return new Promise((resolve) => setTimeout(resolve, ms));
5
+ }
6
+
7
+ /**
8
+ * Simple retry utility — replaces cockatiel to avoid module-level AbortController
9
+ * (cockatiel's abort.js creates new AbortController() at module scope, which is
10
+ * forbidden in Cloudflare Workers global scope).
11
+ *
12
+ * Retries up to maxAttempts when the error matches the predicate.
13
+ * Uses exponential backoff: delay = min(initialDelay * exponent^attempt, maxDelay).
14
+ */
15
+ export function retryExceptionOr500() {
16
+ return {
17
+ execute: async <T>(fn: () => Promise<T>): Promise<T> => {
18
+ const maxAttempts = 3;
19
+ const initialDelay = 100;
20
+ const maxDelay = 5000;
21
+ const exponent = 2;
22
+
23
+ let lastErr: unknown;
24
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
25
+ try {
26
+ return await fn();
27
+ } catch (err) {
28
+ const message = err instanceof Error ? err.message : String(err);
29
+ if (!message.includes(CONNECTION_CLOSED_MESSAGE)) {
30
+ throw err;
31
+ }
32
+ lastErr = err;
33
+ try {
34
+ console.error("retrying...", err);
35
+ } catch (_) {}
36
+ if (attempt < maxAttempts - 1) {
37
+ const delay = Math.min(initialDelay * Math.pow(exponent, attempt), maxDelay);
38
+ await sleep(delay);
39
+ }
40
+ }
41
+ }
42
+ throw lastErr;
43
+ },
44
+ };
45
+ }