@nestjs-ssr/react 0.3.4 → 0.3.6
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/dist/client.d.mts +2 -2
- package/dist/client.d.ts +2 -2
- package/dist/client.js +50 -8
- package/dist/client.mjs +50 -8
- package/dist/{index-BzOLOiIZ.d.ts → index-CSvZfKpi.d.ts} +161 -8
- package/dist/{index-DdE--mA2.d.mts → index-ZpkYrPcK.d.mts} +161 -8
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +137 -35
- package/dist/index.mjs +138 -36
- package/dist/render/index.d.mts +3 -3
- package/dist/render/index.d.ts +3 -3
- package/dist/render/index.js +118 -35
- package/dist/render/index.mjs +119 -36
- package/dist/{render-response.interface-CxbuKGnV.d.mts → render-response.interface-ClWJXKL4.d.mts} +19 -10
- package/dist/{render-response.interface-CxbuKGnV.d.ts → render-response.interface-ClWJXKL4.d.ts} +19 -10
- package/dist/templates/entry-client.tsx +23 -4
- package/dist/templates/entry-server.tsx +25 -8
- package/dist/{use-page-context-CGT9woWe.d.mts → use-page-context-CVC9DHcL.d.mts} +2 -1
- package/dist/{use-page-context-05ODF4zW.d.ts → use-page-context-DChgHhL9.d.ts} +2 -1
- package/package.json +12 -1
- package/src/templates/entry-client.tsx +23 -4
- package/src/templates/entry-server.tsx +25 -8
|
@@ -115,8 +115,15 @@ function hasLayout(
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* Compose a component with its layout (and nested layouts if any)
|
|
119
|
-
* 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)
|
|
120
127
|
*/
|
|
121
128
|
function composeWithLayout(
|
|
122
129
|
ViewComponent: React.ComponentType<any>,
|
|
@@ -139,9 +146,11 @@ function composeWithLayout(
|
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
// Wrap with each layout in
|
|
149
|
+
// Wrap with each layout in REVERSE order (innermost to outermost)
|
|
150
|
+
// This produces the correct nesting: RootLayout > ControllerLayout > Page
|
|
143
151
|
// Must match server-side wrapping with data-layout and data-outlet attributes
|
|
144
|
-
for (
|
|
152
|
+
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
153
|
+
const { layout: Layout, props: layoutProps } = layouts[i];
|
|
145
154
|
const layoutName = Layout.displayName || Layout.name || 'Layout';
|
|
146
155
|
result = (
|
|
147
156
|
<div data-layout={layoutName}>
|
|
@@ -183,8 +192,18 @@ hydrateRoot(
|
|
|
183
192
|
<StrictMode>{wrappedElement}</StrictMode>,
|
|
184
193
|
);
|
|
185
194
|
|
|
195
|
+
// Track if initial hydration is complete to ignore false popstate events
|
|
196
|
+
let hydrationComplete = false;
|
|
197
|
+
requestAnimationFrame(() => {
|
|
198
|
+
hydrationComplete = true;
|
|
199
|
+
});
|
|
200
|
+
|
|
186
201
|
// Handle browser back/forward navigation
|
|
187
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
|
+
|
|
188
207
|
// Dynamically import navigate to avoid circular dependency with hydrate-segment
|
|
189
208
|
const { navigate } = await import('@nestjs-ssr/react/client');
|
|
190
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
|
|
39
|
-
//
|
|
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 (
|
|
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
|
|
84
|
-
*
|
|
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
|
-
//
|
|
109
|
+
// Wrap with PageContextProvider to make context available via hooks
|
|
93
110
|
const element = (
|
|
94
111
|
<PageContextProvider context={context}>
|
|
95
|
-
|
|
112
|
+
{composedElement}
|
|
96
113
|
</PageContextProvider>
|
|
97
114
|
);
|
|
98
115
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { H as HeadData, R as RenderContext } from './render-response.interface-
|
|
1
|
+
import { H as HeadData, R as RenderContext } from './render-response.interface-ClWJXKL4.mjs';
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
@@ -85,6 +85,7 @@ declare function updatePageContext(context: RenderContext): void;
|
|
|
85
85
|
*
|
|
86
86
|
* @param isSegment - If true, this is a segment provider (for hydrated segments)
|
|
87
87
|
* and won't register its setter to avoid overwriting the root provider's.
|
|
88
|
+
* However, it will still receive broadcasts when context updates.
|
|
88
89
|
*/
|
|
89
90
|
declare function PageContextProvider({ context: initialContext, children, isSegment, }: {
|
|
90
91
|
context: RenderContext;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { H as HeadData, R as RenderContext } from './render-response.interface-
|
|
1
|
+
import { H as HeadData, R as RenderContext } from './render-response.interface-ClWJXKL4.js';
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
@@ -85,6 +85,7 @@ declare function updatePageContext(context: RenderContext): void;
|
|
|
85
85
|
*
|
|
86
86
|
* @param isSegment - If true, this is a segment provider (for hydrated segments)
|
|
87
87
|
* and won't register its setter to avoid overwriting the root provider's.
|
|
88
|
+
* However, it will still receive broadcasts when context updates.
|
|
88
89
|
*/
|
|
89
90
|
declare function PageContextProvider({ context: initialContext, children, isSegment, }: {
|
|
90
91
|
context: RenderContext;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestjs-ssr/react",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "React SSR for NestJS that respects Clean Architecture. Proper DI, SOLID principles, clear separation of concerns.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nestjs",
|
|
@@ -112,6 +112,8 @@
|
|
|
112
112
|
"@nestjs/common": "^11.0.0",
|
|
113
113
|
"@nestjs/core": "^11.0.0",
|
|
114
114
|
"@nestjs/platform-express": "^11.0.0",
|
|
115
|
+
"@nestjs/platform-fastify": "^11.0.0",
|
|
116
|
+
"@fastify/static": "^8.0.0 || ^7.0.0",
|
|
115
117
|
"http-proxy-middleware": "^3.0.0 || ^2.0.0",
|
|
116
118
|
"react": "^19.0.0",
|
|
117
119
|
"react-dom": "^19.0.0",
|
|
@@ -119,6 +121,15 @@
|
|
|
119
121
|
"vite": "^7.0.0 || ^6.0.0"
|
|
120
122
|
},
|
|
121
123
|
"peerDependenciesMeta": {
|
|
124
|
+
"@nestjs/platform-express": {
|
|
125
|
+
"optional": true
|
|
126
|
+
},
|
|
127
|
+
"@nestjs/platform-fastify": {
|
|
128
|
+
"optional": true
|
|
129
|
+
},
|
|
130
|
+
"@fastify/static": {
|
|
131
|
+
"optional": true
|
|
132
|
+
},
|
|
122
133
|
"http-proxy-middleware": {
|
|
123
134
|
"optional": true
|
|
124
135
|
}
|
|
@@ -115,8 +115,15 @@ function hasLayout(
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* Compose a component with its layout (and nested layouts if any)
|
|
119
|
-
* 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)
|
|
120
127
|
*/
|
|
121
128
|
function composeWithLayout(
|
|
122
129
|
ViewComponent: React.ComponentType<any>,
|
|
@@ -139,9 +146,11 @@ function composeWithLayout(
|
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
// Wrap with each layout in
|
|
149
|
+
// Wrap with each layout in REVERSE order (innermost to outermost)
|
|
150
|
+
// This produces the correct nesting: RootLayout > ControllerLayout > Page
|
|
143
151
|
// Must match server-side wrapping with data-layout and data-outlet attributes
|
|
144
|
-
for (
|
|
152
|
+
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
153
|
+
const { layout: Layout, props: layoutProps } = layouts[i];
|
|
145
154
|
const layoutName = Layout.displayName || Layout.name || 'Layout';
|
|
146
155
|
result = (
|
|
147
156
|
<div data-layout={layoutName}>
|
|
@@ -183,8 +192,18 @@ hydrateRoot(
|
|
|
183
192
|
<StrictMode>{wrappedElement}</StrictMode>,
|
|
184
193
|
);
|
|
185
194
|
|
|
195
|
+
// Track if initial hydration is complete to ignore false popstate events
|
|
196
|
+
let hydrationComplete = false;
|
|
197
|
+
requestAnimationFrame(() => {
|
|
198
|
+
hydrationComplete = true;
|
|
199
|
+
});
|
|
200
|
+
|
|
186
201
|
// Handle browser back/forward navigation
|
|
187
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
|
+
|
|
188
207
|
// Dynamically import navigate to avoid circular dependency with hydrate-segment
|
|
189
208
|
const { navigate } = await import('@nestjs-ssr/react/client');
|
|
190
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
|
|
39
|
-
//
|
|
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 (
|
|
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
|
|
84
|
-
*
|
|
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
|
-
//
|
|
109
|
+
// Wrap with PageContextProvider to make context available via hooks
|
|
93
110
|
const element = (
|
|
94
111
|
<PageContextProvider context={context}>
|
|
95
|
-
|
|
112
|
+
{composedElement}
|
|
96
113
|
</PageContextProvider>
|
|
97
114
|
);
|
|
98
115
|
|