@nestjs-ssr/react 0.3.3 → 0.3.5

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.
@@ -1,4 +1,5 @@
1
1
  /// <reference types="@nestjs-ssr/react/global" />
2
+
2
3
  import React, { StrictMode } from 'react';
3
4
  import { hydrateRoot } from 'react-dom/client';
4
5
  import {
@@ -10,6 +11,15 @@ const componentName = window.__COMPONENT_NAME__;
10
11
  const initialProps = window.__INITIAL_STATE__ || {};
11
12
  const renderContext = window.__CONTEXT__ || {};
12
13
 
14
+ // Auto-discover root layout using Vite's glob import (must match server-side discovery)
15
+ // @ts-ignore - Vite-specific API
16
+ const layoutModules = import.meta.glob('@/views/layout.tsx', {
17
+ eager: true,
18
+ }) as Record<string, { default: React.ComponentType<any> }>;
19
+
20
+ const layoutPath = Object.keys(layoutModules)[0];
21
+ const RootLayout = layoutPath ? layoutModules[layoutPath].default : null;
22
+
13
23
  // Auto-import all view components using Vite's glob feature
14
24
  // Exclude entry-client.tsx and entry-server.tsx from the glob
15
25
  // @ts-ignore - Vite-specific API
@@ -105,46 +115,68 @@ function hasLayout(
105
115
  }
106
116
 
107
117
  /**
108
- * Compose a component with its layout (and nested layouts if any)
109
- * This must match the server-side composition in entry-server.tsx
118
+ * Compose a component with its layout (and nested layouts if any).
119
+ * This must match the server-side composition in entry-server.tsx.
120
+ *
121
+ * The layouts array is ordered [RootLayout, ControllerLayout, MethodLayout] (outer to inner).
122
+ * We iterate in REVERSE order because wrapping happens inside-out:
123
+ * - Start with Page
124
+ * - Wrap with innermost layout first (MethodLayout)
125
+ * - Then wrap with ControllerLayout
126
+ * - Finally wrap with RootLayout (outermost)
110
127
  */
111
128
  function composeWithLayout(
112
129
  ViewComponent: React.ComponentType<any>,
113
130
  props: any,
131
+ context?: any,
132
+ layouts: Array<{ layout: React.ComponentType<any>; props?: any }> = [],
114
133
  ): React.ReactElement {
115
- const element = <ViewComponent {...props} />;
116
-
117
- // Check if component has a layout
118
- if (!hasLayout(ViewComponent)) {
119
- return element;
120
- }
121
-
122
- // Collect all layouts in the chain (innermost to outermost)
123
- const layoutChain: Array<{
124
- Layout: React.ComponentType<any>;
125
- layoutProps: any;
126
- }> = [];
127
- let currentComponent: any = ViewComponent;
128
-
129
- while (hasLayout(currentComponent)) {
130
- layoutChain.push({
131
- Layout: currentComponent.layout,
132
- layoutProps: currentComponent.layoutProps || {},
133
- });
134
- currentComponent = currentComponent.layout;
134
+ // Start with the page component
135
+ let result = <ViewComponent {...props} />;
136
+
137
+ // If no layouts passed, check if component has its own layout chain
138
+ if (layouts.length === 0 && hasLayout(ViewComponent)) {
139
+ let currentComponent: any = ViewComponent;
140
+ while (hasLayout(currentComponent)) {
141
+ layouts.push({
142
+ layout: currentComponent.layout,
143
+ props: currentComponent.layoutProps || {},
144
+ });
145
+ currentComponent = currentComponent.layout;
146
+ }
135
147
  }
136
148
 
137
- // Wrap the element with layouts from innermost to outermost
138
- let result = element;
139
- for (const { Layout, layoutProps } of layoutChain) {
140
- result = <Layout layoutProps={layoutProps}>{result}</Layout>;
149
+ // Wrap with each layout in REVERSE order (innermost to outermost)
150
+ // This produces the correct nesting: RootLayout > ControllerLayout > Page
151
+ // Must match server-side wrapping with data-layout and data-outlet attributes
152
+ for (let i = layouts.length - 1; i >= 0; i--) {
153
+ const { layout: Layout, props: layoutProps } = layouts[i];
154
+ const layoutName = Layout.displayName || Layout.name || 'Layout';
155
+ result = (
156
+ <div data-layout={layoutName}>
157
+ <Layout context={context} layoutProps={layoutProps}>
158
+ <div data-outlet={layoutName}>{result}</div>
159
+ </Layout>
160
+ </div>
161
+ );
141
162
  }
142
163
 
143
164
  return result;
144
165
  }
145
166
 
167
+ // Build layouts array - use RootLayout if it exists (matching server behavior)
168
+ const layouts: Array<{ layout: React.ComponentType<any>; props?: any }> = [];
169
+ if (RootLayout) {
170
+ layouts.push({ layout: RootLayout, props: {} });
171
+ }
172
+
146
173
  // Compose the component with its layout (if any)
147
- const composedElement = composeWithLayout(ViewComponent, initialProps);
174
+ const composedElement = composeWithLayout(
175
+ ViewComponent,
176
+ initialProps,
177
+ renderContext,
178
+ layouts,
179
+ );
148
180
 
149
181
  // Wrap with providers to make context and navigation state available via hooks
150
182
  const wrappedElement = (
@@ -160,8 +192,18 @@ hydrateRoot(
160
192
  <StrictMode>{wrappedElement}</StrictMode>,
161
193
  );
162
194
 
195
+ // Track if initial hydration is complete to ignore false popstate events
196
+ let hydrationComplete = false;
197
+ requestAnimationFrame(() => {
198
+ hydrationComplete = true;
199
+ });
200
+
163
201
  // Handle browser back/forward navigation
164
202
  window.addEventListener('popstate', async () => {
203
+ // Ignore popstate events that fire before hydration is complete
204
+ // (some browsers fire popstate on initial page load)
205
+ if (!hydrationComplete) return;
206
+
165
207
  // Dynamically import navigate to avoid circular dependency with hydrate-segment
166
208
  const { navigate } = await import('@nestjs-ssr/react/client');
167
209
  // Re-navigate to the current URL (browser already updated location)
@@ -25,6 +25,13 @@ export function getRootLayout(): React.ComponentType<any> | null {
25
25
  * Layouts are passed from the RenderInterceptor based on decorators.
26
26
  * Each layout is wrapped with data-layout and data-outlet attributes
27
27
  * for client-side navigation segment swapping.
28
+ *
29
+ * The layouts array is ordered [RootLayout, ControllerLayout, MethodLayout] (outer to inner).
30
+ * We iterate in REVERSE order because wrapping happens inside-out:
31
+ * - Start with Page
32
+ * - Wrap with innermost layout first (MethodLayout)
33
+ * - Then wrap with ControllerLayout
34
+ * - Finally wrap with RootLayout (outermost)
28
35
  */
29
36
  function composeWithLayouts(
30
37
  ViewComponent: React.ComponentType<any>,
@@ -35,11 +42,12 @@ function composeWithLayouts(
35
42
  // Start with the page component
36
43
  let result = <ViewComponent {...props} />;
37
44
 
38
- // Wrap with each layout in the chain (outermost to innermost in array)
39
- // We iterate normally because layouts are already in correct order from interceptor
45
+ // Wrap with each layout in REVERSE order (innermost to outermost)
46
+ // This produces the correct nesting: RootLayout > ControllerLayout > Page
40
47
  // Pass context to layouts so they can access path, params, etc. for navigation
41
48
  // Each layout gets data-layout attribute and children are wrapped in data-outlet
42
- for (const { layout: Layout, props: layoutProps } of layouts) {
49
+ for (let i = layouts.length - 1; i >= 0; i--) {
50
+ const { layout: Layout, props: layoutProps } = layouts[i];
43
51
  const layoutName = Layout.displayName || Layout.name || 'Layout';
44
52
  result = (
45
53
  <div data-layout={layoutName}>
@@ -80,19 +88,28 @@ export function renderComponent(
80
88
  }
81
89
 
82
90
  /**
83
- * Render just the page component for segment navigation.
84
- * No layout wrappers - the layout already exists on the client.
91
+ * Render a segment for client-side navigation.
92
+ * Includes any layouts below the swap target (e.g., nested layouts).
93
+ * The swap target's outlet will receive this rendered content.
85
94
  */
86
95
  export function renderSegment(
87
96
  ViewComponent: React.ComponentType<any>,
88
97
  data: any,
89
98
  ) {
90
- const { data: pageData, __context: context } = data;
99
+ const { data: pageData, __context: context, __layouts: layouts } = data;
100
+
101
+ // Compose with filtered layouts (layouts below the swap target)
102
+ const composedElement = composeWithLayouts(
103
+ ViewComponent,
104
+ pageData,
105
+ layouts,
106
+ context,
107
+ );
91
108
 
92
- // Render just the page component, no layout wrappers
109
+ // Wrap with PageContextProvider to make context available via hooks
93
110
  const element = (
94
111
  <PageContextProvider context={context}>
95
- <ViewComponent {...pageData} />
112
+ {composedElement}
96
113
  </PageContextProvider>
97
114
  );
98
115