@nestjs-ssr/react 0.3.4 → 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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Injectable, Logger, Optional, Inject, Global, Module, SetMetadata } from '@nestjs/common';
2
- import { HttpAdapterHost, APP_INTERCEPTOR, Reflector } from '@nestjs/core';
2
+ import { Reflector, HttpAdapterHost, APP_INTERCEPTOR } from '@nestjs/core';
3
3
  import { existsSync, readFileSync } from 'fs';
4
4
  import { join, relative } from 'path';
5
5
  import { uneval } from 'devalue';
@@ -308,9 +308,13 @@ var StringRenderer = class _StringRenderer {
308
308
  throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
309
309
  }
310
310
  }
311
- const { data: pageData, __context: pageContext } = data;
311
+ const { data: pageData, __context: pageContext, __layouts: layouts } = data;
312
312
  const html = await renderModule.renderSegment(viewComponent, data);
313
313
  const componentName = viewComponent.displayName || viewComponent.name || "Component";
314
+ const layoutMetadata = layouts ? layouts.map((l) => ({
315
+ name: l.layout.displayName || l.layout.name || "default",
316
+ props: l.props
317
+ })) : [];
314
318
  if (context.isDevelopment) {
315
319
  const duration = Date.now() - startTime;
316
320
  this.logger.log(`[SSR] ${componentName} segment rendered in ${duration}ms`);
@@ -321,7 +325,8 @@ var StringRenderer = class _StringRenderer {
321
325
  props: pageData,
322
326
  swapTarget,
323
327
  componentName,
324
- context: pageContext
328
+ context: pageContext,
329
+ layouts: layoutMetadata
325
330
  };
326
331
  }
327
332
  };
@@ -1090,11 +1095,13 @@ var RenderInterceptor = class {
1090
1095
  renderService;
1091
1096
  allowedHeaders;
1092
1097
  allowedCookies;
1093
- constructor(reflector, renderService, allowedHeaders, allowedCookies) {
1098
+ contextFactory;
1099
+ constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory) {
1094
1100
  this.reflector = reflector;
1095
1101
  this.renderService = renderService;
1096
1102
  this.allowedHeaders = allowedHeaders;
1097
1103
  this.allowedCookies = allowedCookies;
1104
+ this.contextFactory = contextFactory;
1098
1105
  }
1099
1106
  /**
1100
1107
  * Resolve the layout hierarchy for a given route
@@ -1234,6 +1241,14 @@ var RenderInterceptor = class {
1234
1241
  renderContext.cookies = cookies;
1235
1242
  }
1236
1243
  }
1244
+ if (this.contextFactory) {
1245
+ const customContext = await this.contextFactory({
1246
+ req: request
1247
+ });
1248
+ if (customContext) {
1249
+ Object.assign(renderContext, customContext);
1250
+ }
1251
+ }
1237
1252
  const renderResponse = isRenderResponse(data) ? data : {
1238
1253
  props: data
1239
1254
  };
@@ -1280,12 +1295,15 @@ RenderInterceptor = _ts_decorate6([
1280
1295
  _ts_param3(2, Inject("ALLOWED_HEADERS")),
1281
1296
  _ts_param3(3, Optional()),
1282
1297
  _ts_param3(3, Inject("ALLOWED_COOKIES")),
1298
+ _ts_param3(4, Optional()),
1299
+ _ts_param3(4, Inject("CONTEXT_FACTORY")),
1283
1300
  _ts_metadata5("design:type", Function),
1284
1301
  _ts_metadata5("design:paramtypes", [
1285
1302
  typeof Reflector === "undefined" ? Object : Reflector,
1286
1303
  typeof RenderService === "undefined" ? Object : RenderService,
1287
1304
  Array,
1288
- Array
1305
+ Array,
1306
+ typeof ContextFactory === "undefined" ? Object : ContextFactory
1289
1307
  ])
1290
1308
  ], RenderInterceptor);
1291
1309
  function _ts_decorate7(decorators, target, key, desc) {
@@ -1529,6 +1547,12 @@ var RenderModule = class _RenderModule {
1529
1547
  provide: "ALLOWED_COOKIES",
1530
1548
  useValue: config?.allowedCookies || []
1531
1549
  });
1550
+ if (config?.context) {
1551
+ providers.push({
1552
+ provide: "CONTEXT_FACTORY",
1553
+ useValue: config.context
1554
+ });
1555
+ }
1532
1556
  return {
1533
1557
  global: true,
1534
1558
  module: _RenderModule,
@@ -1647,6 +1671,13 @@ var RenderModule = class _RenderModule {
1647
1671
  inject: [
1648
1672
  "RENDER_CONFIG"
1649
1673
  ]
1674
+ },
1675
+ {
1676
+ provide: "CONTEXT_FACTORY",
1677
+ useFactory: /* @__PURE__ */ __name((config) => config?.context, "useFactory"),
1678
+ inject: [
1679
+ "RENDER_CONFIG"
1680
+ ]
1650
1681
  }
1651
1682
  ];
1652
1683
  return {
@@ -1709,9 +1740,28 @@ var PageContext = getOrCreateContext();
1709
1740
  function registerPageContextState(setter) {
1710
1741
  }
1711
1742
  __name(registerPageContextState, "registerPageContextState");
1743
+ var segmentSetters = /* @__PURE__ */ new Set();
1744
+ function registerSegmentSetter(setter) {
1745
+ segmentSetters.add(setter);
1746
+ }
1747
+ __name(registerSegmentSetter, "registerSegmentSetter");
1748
+ function unregisterSegmentSetter(setter) {
1749
+ segmentSetters.delete(setter);
1750
+ }
1751
+ __name(unregisterSegmentSetter, "unregisterSegmentSetter");
1752
+ function broadcastToSegments(context) {
1753
+ segmentSetters.forEach((setter) => setter(context));
1754
+ }
1755
+ __name(broadcastToSegments, "broadcastToSegments");
1712
1756
  function PageContextProvider({ context: initialContext, children, isSegment = false }) {
1713
1757
  const [context, setContext] = useState(initialContext);
1714
1758
  useEffect(() => {
1759
+ if (!isSegment) {
1760
+ return void 0;
1761
+ } else {
1762
+ registerSegmentSetter(setContext);
1763
+ return () => unregisterSegmentSetter(setContext);
1764
+ }
1715
1765
  }, [
1716
1766
  isSegment
1717
1767
  ]);
@@ -1,9 +1,9 @@
1
- export { E as ErrorPageDevelopment, e as ErrorPageProduction, b as RenderInterceptor, R as RenderModule, a as RenderService, S as StreamingErrorHandler, T as TemplateParserService } from '../index-DdE--mA2.mjs';
1
+ export { E as ErrorPageDevelopment, e as ErrorPageProduction, b as RenderInterceptor, R as RenderModule, a as RenderService, S as StreamingErrorHandler, T as TemplateParserService } from '../index-CiYcz-1T.mjs';
2
2
  import '@nestjs/common';
3
3
  import 'react';
4
- import '../render-response.interface-CxbuKGnV.mjs';
5
- import 'vite';
6
4
  import 'express';
5
+ import '../render-response.interface-ClWJXKL4.mjs';
6
+ import 'vite';
7
7
  import '@nestjs/core';
8
8
  import 'rxjs';
9
9
  import 'react/jsx-runtime';
@@ -1,9 +1,9 @@
1
- export { E as ErrorPageDevelopment, e as ErrorPageProduction, b as RenderInterceptor, R as RenderModule, a as RenderService, S as StreamingErrorHandler, T as TemplateParserService } from '../index-BzOLOiIZ.js';
1
+ export { E as ErrorPageDevelopment, e as ErrorPageProduction, b as RenderInterceptor, R as RenderModule, a as RenderService, S as StreamingErrorHandler, T as TemplateParserService } from '../index-Dq2qZSge.js';
2
2
  import '@nestjs/common';
3
3
  import 'react';
4
- import '../render-response.interface-CxbuKGnV.js';
5
- import 'vite';
6
4
  import 'express';
5
+ import '../render-response.interface-ClWJXKL4.js';
6
+ import 'vite';
7
7
  import '@nestjs/core';
8
8
  import 'rxjs';
9
9
  import 'react/jsx-runtime';
@@ -314,9 +314,13 @@ var StringRenderer = class _StringRenderer {
314
314
  throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
315
315
  }
316
316
  }
317
- const { data: pageData, __context: pageContext } = data;
317
+ const { data: pageData, __context: pageContext, __layouts: layouts } = data;
318
318
  const html = await renderModule.renderSegment(viewComponent, data);
319
319
  const componentName = viewComponent.displayName || viewComponent.name || "Component";
320
+ const layoutMetadata = layouts ? layouts.map((l) => ({
321
+ name: l.layout.displayName || l.layout.name || "default",
322
+ props: l.props
323
+ })) : [];
320
324
  if (context.isDevelopment) {
321
325
  const duration = Date.now() - startTime;
322
326
  this.logger.log(`[SSR] ${componentName} segment rendered in ${duration}ms`);
@@ -327,7 +331,8 @@ var StringRenderer = class _StringRenderer {
327
331
  props: pageData,
328
332
  swapTarget,
329
333
  componentName,
330
- context: pageContext
334
+ context: pageContext,
335
+ layouts: layoutMetadata
331
336
  };
332
337
  }
333
338
  };
@@ -1078,11 +1083,13 @@ exports.RenderInterceptor = class RenderInterceptor {
1078
1083
  renderService;
1079
1084
  allowedHeaders;
1080
1085
  allowedCookies;
1081
- constructor(reflector, renderService, allowedHeaders, allowedCookies) {
1086
+ contextFactory;
1087
+ constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory) {
1082
1088
  this.reflector = reflector;
1083
1089
  this.renderService = renderService;
1084
1090
  this.allowedHeaders = allowedHeaders;
1085
1091
  this.allowedCookies = allowedCookies;
1092
+ this.contextFactory = contextFactory;
1086
1093
  }
1087
1094
  /**
1088
1095
  * Resolve the layout hierarchy for a given route
@@ -1222,6 +1229,14 @@ exports.RenderInterceptor = class RenderInterceptor {
1222
1229
  renderContext.cookies = cookies;
1223
1230
  }
1224
1231
  }
1232
+ if (this.contextFactory) {
1233
+ const customContext = await this.contextFactory({
1234
+ req: request
1235
+ });
1236
+ if (customContext) {
1237
+ Object.assign(renderContext, customContext);
1238
+ }
1239
+ }
1225
1240
  const renderResponse = isRenderResponse(data) ? data : {
1226
1241
  props: data
1227
1242
  };
@@ -1268,12 +1283,15 @@ exports.RenderInterceptor = _ts_decorate6([
1268
1283
  _ts_param3(2, common.Inject("ALLOWED_HEADERS")),
1269
1284
  _ts_param3(3, common.Optional()),
1270
1285
  _ts_param3(3, common.Inject("ALLOWED_COOKIES")),
1286
+ _ts_param3(4, common.Optional()),
1287
+ _ts_param3(4, common.Inject("CONTEXT_FACTORY")),
1271
1288
  _ts_metadata5("design:type", Function),
1272
1289
  _ts_metadata5("design:paramtypes", [
1273
1290
  typeof core.Reflector === "undefined" ? Object : core.Reflector,
1274
1291
  typeof exports.RenderService === "undefined" ? Object : exports.RenderService,
1275
1292
  Array,
1276
- Array
1293
+ Array,
1294
+ typeof ContextFactory === "undefined" ? Object : ContextFactory
1277
1295
  ])
1278
1296
  ], exports.RenderInterceptor);
1279
1297
  function _ts_decorate7(decorators, target, key, desc) {
@@ -1517,6 +1535,12 @@ exports.RenderModule = class _RenderModule {
1517
1535
  provide: "ALLOWED_COOKIES",
1518
1536
  useValue: config?.allowedCookies || []
1519
1537
  });
1538
+ if (config?.context) {
1539
+ providers.push({
1540
+ provide: "CONTEXT_FACTORY",
1541
+ useValue: config.context
1542
+ });
1543
+ }
1520
1544
  return {
1521
1545
  global: true,
1522
1546
  module: _RenderModule,
@@ -1635,6 +1659,13 @@ exports.RenderModule = class _RenderModule {
1635
1659
  inject: [
1636
1660
  "RENDER_CONFIG"
1637
1661
  ]
1662
+ },
1663
+ {
1664
+ provide: "CONTEXT_FACTORY",
1665
+ useFactory: /* @__PURE__ */ __name((config) => config?.context, "useFactory"),
1666
+ inject: [
1667
+ "RENDER_CONFIG"
1668
+ ]
1638
1669
  }
1639
1670
  ];
1640
1671
  return {
@@ -1,5 +1,5 @@
1
1
  import { Injectable, Logger, Optional, Inject, Global, Module } from '@nestjs/common';
2
- import { HttpAdapterHost, APP_INTERCEPTOR, Reflector } from '@nestjs/core';
2
+ import { Reflector, HttpAdapterHost, APP_INTERCEPTOR } from '@nestjs/core';
3
3
  import { existsSync, readFileSync } from 'fs';
4
4
  import { join, relative } from 'path';
5
5
  import { uneval } from 'devalue';
@@ -308,9 +308,13 @@ var StringRenderer = class _StringRenderer {
308
308
  throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
309
309
  }
310
310
  }
311
- const { data: pageData, __context: pageContext } = data;
311
+ const { data: pageData, __context: pageContext, __layouts: layouts } = data;
312
312
  const html = await renderModule.renderSegment(viewComponent, data);
313
313
  const componentName = viewComponent.displayName || viewComponent.name || "Component";
314
+ const layoutMetadata = layouts ? layouts.map((l) => ({
315
+ name: l.layout.displayName || l.layout.name || "default",
316
+ props: l.props
317
+ })) : [];
314
318
  if (context.isDevelopment) {
315
319
  const duration = Date.now() - startTime;
316
320
  this.logger.log(`[SSR] ${componentName} segment rendered in ${duration}ms`);
@@ -321,7 +325,8 @@ var StringRenderer = class _StringRenderer {
321
325
  props: pageData,
322
326
  swapTarget,
323
327
  componentName,
324
- context: pageContext
328
+ context: pageContext,
329
+ layouts: layoutMetadata
325
330
  };
326
331
  }
327
332
  };
@@ -1072,11 +1077,13 @@ var RenderInterceptor = class {
1072
1077
  renderService;
1073
1078
  allowedHeaders;
1074
1079
  allowedCookies;
1075
- constructor(reflector, renderService, allowedHeaders, allowedCookies) {
1080
+ contextFactory;
1081
+ constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory) {
1076
1082
  this.reflector = reflector;
1077
1083
  this.renderService = renderService;
1078
1084
  this.allowedHeaders = allowedHeaders;
1079
1085
  this.allowedCookies = allowedCookies;
1086
+ this.contextFactory = contextFactory;
1080
1087
  }
1081
1088
  /**
1082
1089
  * Resolve the layout hierarchy for a given route
@@ -1216,6 +1223,14 @@ var RenderInterceptor = class {
1216
1223
  renderContext.cookies = cookies;
1217
1224
  }
1218
1225
  }
1226
+ if (this.contextFactory) {
1227
+ const customContext = await this.contextFactory({
1228
+ req: request
1229
+ });
1230
+ if (customContext) {
1231
+ Object.assign(renderContext, customContext);
1232
+ }
1233
+ }
1219
1234
  const renderResponse = isRenderResponse(data) ? data : {
1220
1235
  props: data
1221
1236
  };
@@ -1262,12 +1277,15 @@ RenderInterceptor = _ts_decorate6([
1262
1277
  _ts_param3(2, Inject("ALLOWED_HEADERS")),
1263
1278
  _ts_param3(3, Optional()),
1264
1279
  _ts_param3(3, Inject("ALLOWED_COOKIES")),
1280
+ _ts_param3(4, Optional()),
1281
+ _ts_param3(4, Inject("CONTEXT_FACTORY")),
1265
1282
  _ts_metadata5("design:type", Function),
1266
1283
  _ts_metadata5("design:paramtypes", [
1267
1284
  typeof Reflector === "undefined" ? Object : Reflector,
1268
1285
  typeof RenderService === "undefined" ? Object : RenderService,
1269
1286
  Array,
1270
- Array
1287
+ Array,
1288
+ typeof ContextFactory === "undefined" ? Object : ContextFactory
1271
1289
  ])
1272
1290
  ], RenderInterceptor);
1273
1291
  function _ts_decorate7(decorators, target, key, desc) {
@@ -1511,6 +1529,12 @@ var RenderModule = class _RenderModule {
1511
1529
  provide: "ALLOWED_COOKIES",
1512
1530
  useValue: config?.allowedCookies || []
1513
1531
  });
1532
+ if (config?.context) {
1533
+ providers.push({
1534
+ provide: "CONTEXT_FACTORY",
1535
+ useValue: config.context
1536
+ });
1537
+ }
1514
1538
  return {
1515
1539
  global: true,
1516
1540
  module: _RenderModule,
@@ -1629,6 +1653,13 @@ var RenderModule = class _RenderModule {
1629
1653
  inject: [
1630
1654
  "RENDER_CONFIG"
1631
1655
  ]
1656
+ },
1657
+ {
1658
+ provide: "CONTEXT_FACTORY",
1659
+ useFactory: /* @__PURE__ */ __name((config) => config?.context, "useFactory"),
1660
+ inject: [
1661
+ "RENDER_CONFIG"
1662
+ ]
1632
1663
  }
1633
1664
  ];
1634
1665
  return {
@@ -3,7 +3,7 @@
3
3
  * Contains safe request metadata that can be exposed to the client.
4
4
  *
5
5
  * Extend this interface to add app-specific properties (user, tenant, feature flags, etc.).
6
- * Use module configuration to pass additional headers or cookies safely.
6
+ * Use the `context` option in module configuration to enrich the context.
7
7
  *
8
8
  * @example
9
9
  * // Basic usage - use as-is
@@ -32,19 +32,28 @@
32
32
  * theme?: string; // From cookie
33
33
  * }
34
34
  *
35
- * // Configure module to pass specific cookies/headers
36
- * ReactSSRModule.forRoot({
35
+ * // Configure module with context factory to enrich context
36
+ * RenderModule.forRoot({
37
37
  * allowedCookies: ['theme', 'locale'],
38
38
  * allowedHeaders: ['x-tenant-id'],
39
+ * context: ({ req }) => ({
40
+ * user: req.user, // From Passport JWT strategy
41
+ * tenant: req.tenant,
42
+ * featureFlags: req.featureFlags,
43
+ * }),
39
44
  * })
40
45
  *
41
- * // Use in interceptor/controller
42
- * const context: AppRenderContext = {
43
- * ...baseContext,
44
- * user: req.user,
45
- * tenant: req.tenant,
46
- * featureFlags: await featureFlagService.getFlags(req),
47
- * };
46
+ * // Or with async factory (use forRootAsync)
47
+ * RenderModule.forRootAsync({
48
+ * imports: [PermissionModule],
49
+ * inject: [PermissionService],
50
+ * useFactory: (permissionService) => ({
51
+ * context: async ({ req }) => ({
52
+ * user: req.user,
53
+ * permissions: await permissionService.getForUser(req.user),
54
+ * }),
55
+ * }),
56
+ * })
48
57
  */
49
58
  interface RenderContext {
50
59
  url: string;
@@ -3,7 +3,7 @@
3
3
  * Contains safe request metadata that can be exposed to the client.
4
4
  *
5
5
  * Extend this interface to add app-specific properties (user, tenant, feature flags, etc.).
6
- * Use module configuration to pass additional headers or cookies safely.
6
+ * Use the `context` option in module configuration to enrich the context.
7
7
  *
8
8
  * @example
9
9
  * // Basic usage - use as-is
@@ -32,19 +32,28 @@
32
32
  * theme?: string; // From cookie
33
33
  * }
34
34
  *
35
- * // Configure module to pass specific cookies/headers
36
- * ReactSSRModule.forRoot({
35
+ * // Configure module with context factory to enrich context
36
+ * RenderModule.forRoot({
37
37
  * allowedCookies: ['theme', 'locale'],
38
38
  * allowedHeaders: ['x-tenant-id'],
39
+ * context: ({ req }) => ({
40
+ * user: req.user, // From Passport JWT strategy
41
+ * tenant: req.tenant,
42
+ * featureFlags: req.featureFlags,
43
+ * }),
39
44
  * })
40
45
  *
41
- * // Use in interceptor/controller
42
- * const context: AppRenderContext = {
43
- * ...baseContext,
44
- * user: req.user,
45
- * tenant: req.tenant,
46
- * featureFlags: await featureFlagService.getFlags(req),
47
- * };
46
+ * // Or with async factory (use forRootAsync)
47
+ * RenderModule.forRootAsync({
48
+ * imports: [PermissionModule],
49
+ * inject: [PermissionService],
50
+ * useFactory: (permissionService) => ({
51
+ * context: async ({ req }) => ({
52
+ * user: req.user,
53
+ * permissions: await permissionService.getForUser(req.user),
54
+ * }),
55
+ * }),
56
+ * })
48
57
  */
49
58
  interface RenderContext {
50
59
  url: string;
@@ -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 the chain
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 (const { layout: Layout, props: layoutProps } of layouts) {
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 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
 
@@ -1,4 +1,4 @@
1
- import { H as HeadData, R as RenderContext } from './render-response.interface-CxbuKGnV.mjs';
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-CxbuKGnV.js';
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.4",
3
+ "version": "0.3.5",
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",
@@ -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 the chain
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 (const { layout: Layout, props: layoutProps } of layouts) {
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)