@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.
@@ -7,13 +7,12 @@ var path = require('path');
7
7
  var devalue = require('devalue');
8
8
  var escapeHtml = require('escape-html');
9
9
  var server = require('react-dom/server');
10
- var React = require('react');
10
+ var react = require('react');
11
11
  var operators = require('rxjs/operators');
12
12
 
13
13
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
14
 
15
15
  var escapeHtml__default = /*#__PURE__*/_interopDefault(escapeHtml);
16
- var React__default = /*#__PURE__*/_interopDefault(React);
17
16
 
18
17
  var __defProp = Object.defineProperty;
19
18
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
@@ -315,9 +314,13 @@ var StringRenderer = class _StringRenderer {
315
314
  throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
316
315
  }
317
316
  }
318
- const { data: pageData, __context: pageContext } = data;
317
+ const { data: pageData, __context: pageContext, __layouts: layouts } = data;
319
318
  const html = await renderModule.renderSegment(viewComponent, data);
320
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
+ })) : [];
321
324
  if (context.isDevelopment) {
322
325
  const duration = Date.now() - startTime;
323
326
  this.logger.log(`[SSR] ${componentName} segment rendered in ${duration}ms`);
@@ -328,7 +331,8 @@ var StringRenderer = class _StringRenderer {
328
331
  props: pageData,
329
332
  swapTarget,
330
333
  componentName,
331
- context: pageContext
334
+ context: pageContext,
335
+ layouts: layoutMetadata
332
336
  };
333
337
  }
334
338
  };
@@ -339,16 +343,18 @@ StringRenderer = _ts_decorate2([
339
343
  typeof exports.TemplateParserService === "undefined" ? Object : exports.TemplateParserService
340
344
  ])
341
345
  ], StringRenderer);
346
+
347
+ // src/render/error-pages/error-page-development.tsx
342
348
  function ErrorPageDevelopment({ error, viewPath, phase }) {
343
349
  const stackLines = error.stack ? error.stack.split("\n").slice(1) : [];
344
- return /* @__PURE__ */ React__default.default.createElement("html", {
350
+ return /* @__PURE__ */ React.createElement("html", {
345
351
  lang: "en"
346
- }, /* @__PURE__ */ React__default.default.createElement("head", null, /* @__PURE__ */ React__default.default.createElement("meta", {
352
+ }, /* @__PURE__ */ React.createElement("head", null, /* @__PURE__ */ React.createElement("meta", {
347
353
  charSet: "UTF-8"
348
- }), /* @__PURE__ */ React__default.default.createElement("meta", {
354
+ }), /* @__PURE__ */ React.createElement("meta", {
349
355
  name: "viewport",
350
356
  content: "width=device-width, initial-scale=1.0"
351
- }), /* @__PURE__ */ React__default.default.createElement("title", null, `SSR Error - ${error.name}`), /* @__PURE__ */ React__default.default.createElement("style", {
357
+ }), /* @__PURE__ */ React.createElement("title", null, `SSR Error - ${error.name}`), /* @__PURE__ */ React.createElement("style", {
352
358
  dangerouslySetInnerHTML: {
353
359
  __html: `
354
360
  body {
@@ -399,28 +405,30 @@ function ErrorPageDevelopment({ error, viewPath, phase }) {
399
405
  }
400
406
  `
401
407
  }
402
- })), /* @__PURE__ */ React__default.default.createElement("body", null, /* @__PURE__ */ React__default.default.createElement("div", {
408
+ })), /* @__PURE__ */ React.createElement("body", null, /* @__PURE__ */ React.createElement("div", {
403
409
  className: "error-container"
404
- }, /* @__PURE__ */ React__default.default.createElement("h1", null, "Server-Side Rendering Error"), /* @__PURE__ */ React__default.default.createElement("div", {
410
+ }, /* @__PURE__ */ React.createElement("h1", null, "Server-Side Rendering Error"), /* @__PURE__ */ React.createElement("div", {
405
411
  className: "error-type"
406
- }, error.name), /* @__PURE__ */ React__default.default.createElement("div", {
412
+ }, error.name), /* @__PURE__ */ React.createElement("div", {
407
413
  className: "error-message"
408
- }, error.message), /* @__PURE__ */ React__default.default.createElement("h2", null, "Stack Trace"), /* @__PURE__ */ React__default.default.createElement("div", {
414
+ }, error.message), /* @__PURE__ */ React.createElement("h2", null, "Stack Trace"), /* @__PURE__ */ React.createElement("div", {
409
415
  className: "stack-trace"
410
- }, /* @__PURE__ */ React__default.default.createElement("pre", null, stackLines.join("\n"))), /* @__PURE__ */ React__default.default.createElement("div", {
416
+ }, /* @__PURE__ */ React.createElement("pre", null, stackLines.join("\n"))), /* @__PURE__ */ React.createElement("div", {
411
417
  className: "meta"
412
- }, /* @__PURE__ */ React__default.default.createElement("p", null, /* @__PURE__ */ React__default.default.createElement("strong", null, "View Path:"), " ", viewPath), /* @__PURE__ */ React__default.default.createElement("p", null, /* @__PURE__ */ React__default.default.createElement("strong", null, "Error Phase:"), " ", phase === "shell" ? "Shell (before streaming started)" : "Streaming (during content delivery)"), /* @__PURE__ */ React__default.default.createElement("p", null, /* @__PURE__ */ React__default.default.createElement("strong", null, "Environment:"), " Development")))));
418
+ }, /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "View Path:"), " ", viewPath), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "Error Phase:"), " ", phase === "shell" ? "Shell (before streaming started)" : "Streaming (during content delivery)"), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "Environment:"), " Development")))));
413
419
  }
414
420
  __name(ErrorPageDevelopment, "ErrorPageDevelopment");
421
+
422
+ // src/render/error-pages/error-page-production.tsx
415
423
  function ErrorPageProduction() {
416
- return /* @__PURE__ */ React__default.default.createElement("html", {
424
+ return /* @__PURE__ */ React.createElement("html", {
417
425
  lang: "en"
418
- }, /* @__PURE__ */ React__default.default.createElement("head", null, /* @__PURE__ */ React__default.default.createElement("meta", {
426
+ }, /* @__PURE__ */ React.createElement("head", null, /* @__PURE__ */ React.createElement("meta", {
419
427
  charSet: "UTF-8"
420
- }), /* @__PURE__ */ React__default.default.createElement("meta", {
428
+ }), /* @__PURE__ */ React.createElement("meta", {
421
429
  name: "viewport",
422
430
  content: "width=device-width, initial-scale=1.0"
423
- }), /* @__PURE__ */ React__default.default.createElement("title", null, "Error"), /* @__PURE__ */ React__default.default.createElement("style", {
431
+ }), /* @__PURE__ */ React.createElement("title", null, "Error"), /* @__PURE__ */ React.createElement("style", {
424
432
  dangerouslySetInnerHTML: {
425
433
  __html: `
426
434
  body {
@@ -447,9 +455,9 @@ function ErrorPageProduction() {
447
455
  }
448
456
  `
449
457
  }
450
- })), /* @__PURE__ */ React__default.default.createElement("body", null, /* @__PURE__ */ React__default.default.createElement("div", {
458
+ })), /* @__PURE__ */ React.createElement("body", null, /* @__PURE__ */ React.createElement("div", {
451
459
  className: "error-container"
452
- }, /* @__PURE__ */ React__default.default.createElement("h1", null, "500"), /* @__PURE__ */ React__default.default.createElement("p", null, "Internal Server Error"), /* @__PURE__ */ React__default.default.createElement("p", null, "Something went wrong while rendering this page."))));
460
+ }, /* @__PURE__ */ React.createElement("h1", null, "500"), /* @__PURE__ */ React.createElement("p", null, "Internal Server Error"), /* @__PURE__ */ React.createElement("p", null, "Something went wrong while rendering this page."))));
453
461
  }
454
462
  __name(ErrorPageProduction, "ErrorPageProduction");
455
463
 
@@ -516,7 +524,7 @@ exports.StreamingErrorHandler = class _StreamingErrorHandler {
516
524
  */
517
525
  renderDevelopmentErrorPage(error, viewPath, phase) {
518
526
  const ErrorComponent = this.errorPageDevelopment || ErrorPageDevelopment;
519
- const element = React.createElement(ErrorComponent, {
527
+ const element = react.createElement(ErrorComponent, {
520
528
  error,
521
529
  viewPath,
522
530
  phase
@@ -528,7 +536,7 @@ exports.StreamingErrorHandler = class _StreamingErrorHandler {
528
536
  */
529
537
  renderProductionErrorPage() {
530
538
  const ErrorComponent = this.errorPageProduction || ErrorPageProduction;
531
- const element = React.createElement(ErrorComponent);
539
+ const element = react.createElement(ErrorComponent);
532
540
  return "<!DOCTYPE html>\n" + server.renderToStaticMarkup(element);
533
541
  }
534
542
  /**
@@ -1075,11 +1083,13 @@ exports.RenderInterceptor = class RenderInterceptor {
1075
1083
  renderService;
1076
1084
  allowedHeaders;
1077
1085
  allowedCookies;
1078
- constructor(reflector, renderService, allowedHeaders, allowedCookies) {
1086
+ contextFactory;
1087
+ constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory) {
1079
1088
  this.reflector = reflector;
1080
1089
  this.renderService = renderService;
1081
1090
  this.allowedHeaders = allowedHeaders;
1082
1091
  this.allowedCookies = allowedCookies;
1092
+ this.contextFactory = contextFactory;
1083
1093
  }
1084
1094
  /**
1085
1095
  * Resolve the layout hierarchy for a given route
@@ -1219,6 +1229,14 @@ exports.RenderInterceptor = class RenderInterceptor {
1219
1229
  renderContext.cookies = cookies;
1220
1230
  }
1221
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
+ }
1222
1240
  const renderResponse = isRenderResponse(data) ? data : {
1223
1241
  props: data
1224
1242
  };
@@ -1265,12 +1283,15 @@ exports.RenderInterceptor = _ts_decorate6([
1265
1283
  _ts_param3(2, common.Inject("ALLOWED_HEADERS")),
1266
1284
  _ts_param3(3, common.Optional()),
1267
1285
  _ts_param3(3, common.Inject("ALLOWED_COOKIES")),
1286
+ _ts_param3(4, common.Optional()),
1287
+ _ts_param3(4, common.Inject("CONTEXT_FACTORY")),
1268
1288
  _ts_metadata5("design:type", Function),
1269
1289
  _ts_metadata5("design:paramtypes", [
1270
1290
  typeof core.Reflector === "undefined" ? Object : core.Reflector,
1271
1291
  typeof exports.RenderService === "undefined" ? Object : exports.RenderService,
1272
1292
  Array,
1273
- Array
1293
+ Array,
1294
+ typeof ContextFactory === "undefined" ? Object : ContextFactory
1274
1295
  ])
1275
1296
  ], exports.RenderInterceptor);
1276
1297
  function _ts_decorate7(decorators, target, key, desc) {
@@ -1514,6 +1535,12 @@ exports.RenderModule = class _RenderModule {
1514
1535
  provide: "ALLOWED_COOKIES",
1515
1536
  useValue: config?.allowedCookies || []
1516
1537
  });
1538
+ if (config?.context) {
1539
+ providers.push({
1540
+ provide: "CONTEXT_FACTORY",
1541
+ useValue: config.context
1542
+ });
1543
+ }
1517
1544
  return {
1518
1545
  global: true,
1519
1546
  module: _RenderModule,
@@ -1632,6 +1659,13 @@ exports.RenderModule = class _RenderModule {
1632
1659
  inject: [
1633
1660
  "RENDER_CONFIG"
1634
1661
  ]
1662
+ },
1663
+ {
1664
+ provide: "CONTEXT_FACTORY",
1665
+ useFactory: /* @__PURE__ */ __name((config) => config?.context, "useFactory"),
1666
+ inject: [
1667
+ "RENDER_CONFIG"
1668
+ ]
1635
1669
  }
1636
1670
  ];
1637
1671
  return {
@@ -1,11 +1,11 @@
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';
6
6
  import escapeHtml from 'escape-html';
7
7
  import { renderToStaticMarkup } from 'react-dom/server';
8
- import React, { createElement } from 'react';
8
+ import { createElement } from 'react';
9
9
  import { switchMap } from 'rxjs/operators';
10
10
 
11
11
  var __defProp = Object.defineProperty;
@@ -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
  };
@@ -332,6 +337,8 @@ StringRenderer = _ts_decorate2([
332
337
  typeof TemplateParserService === "undefined" ? Object : TemplateParserService
333
338
  ])
334
339
  ], StringRenderer);
340
+
341
+ // src/render/error-pages/error-page-development.tsx
335
342
  function ErrorPageDevelopment({ error, viewPath, phase }) {
336
343
  const stackLines = error.stack ? error.stack.split("\n").slice(1) : [];
337
344
  return /* @__PURE__ */ React.createElement("html", {
@@ -405,6 +412,8 @@ function ErrorPageDevelopment({ error, viewPath, phase }) {
405
412
  }, /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "View Path:"), " ", viewPath), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "Error Phase:"), " ", phase === "shell" ? "Shell (before streaming started)" : "Streaming (during content delivery)"), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "Environment:"), " Development")))));
406
413
  }
407
414
  __name(ErrorPageDevelopment, "ErrorPageDevelopment");
415
+
416
+ // src/render/error-pages/error-page-production.tsx
408
417
  function ErrorPageProduction() {
409
418
  return /* @__PURE__ */ React.createElement("html", {
410
419
  lang: "en"
@@ -1068,11 +1077,13 @@ var RenderInterceptor = class {
1068
1077
  renderService;
1069
1078
  allowedHeaders;
1070
1079
  allowedCookies;
1071
- constructor(reflector, renderService, allowedHeaders, allowedCookies) {
1080
+ contextFactory;
1081
+ constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory) {
1072
1082
  this.reflector = reflector;
1073
1083
  this.renderService = renderService;
1074
1084
  this.allowedHeaders = allowedHeaders;
1075
1085
  this.allowedCookies = allowedCookies;
1086
+ this.contextFactory = contextFactory;
1076
1087
  }
1077
1088
  /**
1078
1089
  * Resolve the layout hierarchy for a given route
@@ -1212,6 +1223,14 @@ var RenderInterceptor = class {
1212
1223
  renderContext.cookies = cookies;
1213
1224
  }
1214
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
+ }
1215
1234
  const renderResponse = isRenderResponse(data) ? data : {
1216
1235
  props: data
1217
1236
  };
@@ -1258,12 +1277,15 @@ RenderInterceptor = _ts_decorate6([
1258
1277
  _ts_param3(2, Inject("ALLOWED_HEADERS")),
1259
1278
  _ts_param3(3, Optional()),
1260
1279
  _ts_param3(3, Inject("ALLOWED_COOKIES")),
1280
+ _ts_param3(4, Optional()),
1281
+ _ts_param3(4, Inject("CONTEXT_FACTORY")),
1261
1282
  _ts_metadata5("design:type", Function),
1262
1283
  _ts_metadata5("design:paramtypes", [
1263
1284
  typeof Reflector === "undefined" ? Object : Reflector,
1264
1285
  typeof RenderService === "undefined" ? Object : RenderService,
1265
1286
  Array,
1266
- Array
1287
+ Array,
1288
+ typeof ContextFactory === "undefined" ? Object : ContextFactory
1267
1289
  ])
1268
1290
  ], RenderInterceptor);
1269
1291
  function _ts_decorate7(decorators, target, key, desc) {
@@ -1507,6 +1529,12 @@ var RenderModule = class _RenderModule {
1507
1529
  provide: "ALLOWED_COOKIES",
1508
1530
  useValue: config?.allowedCookies || []
1509
1531
  });
1532
+ if (config?.context) {
1533
+ providers.push({
1534
+ provide: "CONTEXT_FACTORY",
1535
+ useValue: config.context
1536
+ });
1537
+ }
1510
1538
  return {
1511
1539
  global: true,
1512
1540
  module: _RenderModule,
@@ -1625,6 +1653,13 @@ var RenderModule = class _RenderModule {
1625
1653
  inject: [
1626
1654
  "RENDER_CONFIG"
1627
1655
  ]
1656
+ },
1657
+ {
1658
+ provide: "CONTEXT_FACTORY",
1659
+ useFactory: /* @__PURE__ */ __name((config) => config?.context, "useFactory"),
1660
+ inject: [
1661
+ "RENDER_CONFIG"
1662
+ ]
1628
1663
  }
1629
1664
  ];
1630
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;
@@ -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
 
@@ -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;