@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 CHANGED
@@ -1,7 +1,7 @@
1
- export { a as PageContextProvider, P as PageProps, c as createSSRHooks, j as updatePageContext, i as useCookie, h as useCookies, g as useHeader, f as useHeaders, u as usePageContext, b as useParams, d as useQuery, e as useRequest } from './use-page-context-CGT9woWe.mjs';
1
+ export { a as PageContextProvider, P as PageProps, c as createSSRHooks, j as updatePageContext, i as useCookie, h as useCookies, g as useHeader, f as useHeaders, u as usePageContext, b as useParams, d as useQuery, e as useRequest } from './use-page-context-CVC9DHcL.mjs';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React from 'react';
4
- export { R as RenderContext } from './render-response.interface-CxbuKGnV.mjs';
4
+ export { R as RenderContext } from './render-response.interface-ClWJXKL4.mjs';
5
5
 
6
6
  /**
7
7
  * Provider component for navigation state.
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export { a as PageContextProvider, P as PageProps, c as createSSRHooks, j as updatePageContext, i as useCookie, h as useCookies, g as useHeader, f as useHeaders, u as usePageContext, b as useParams, d as useQuery, e as useRequest } from './use-page-context-05ODF4zW.js';
1
+ export { a as PageContextProvider, P as PageProps, c as createSSRHooks, j as updatePageContext, i as useCookie, h as useCookies, g as useHeader, f as useHeaders, u as usePageContext, b as useParams, d as useQuery, e as useRequest } from './use-page-context-DChgHhL9.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React from 'react';
4
- export { R as RenderContext } from './render-response.interface-CxbuKGnV.js';
4
+ export { R as RenderContext } from './render-response.interface-ClWJXKL4.js';
5
5
 
6
6
  /**
7
7
  * Provider component for navigation state.
package/dist/client.js CHANGED
@@ -28,11 +28,31 @@ function updatePageContext(context) {
28
28
  setPageContextState?.(context);
29
29
  }
30
30
  __name(updatePageContext, "updatePageContext");
31
+ var segmentSetters = /* @__PURE__ */ new Set();
32
+ function registerSegmentSetter(setter) {
33
+ segmentSetters.add(setter);
34
+ }
35
+ __name(registerSegmentSetter, "registerSegmentSetter");
36
+ function unregisterSegmentSetter(setter) {
37
+ segmentSetters.delete(setter);
38
+ }
39
+ __name(unregisterSegmentSetter, "unregisterSegmentSetter");
40
+ function broadcastToSegments(context) {
41
+ segmentSetters.forEach((setter) => setter(context));
42
+ }
43
+ __name(broadcastToSegments, "broadcastToSegments");
31
44
  function PageContextProvider({ context: initialContext, children, isSegment = false }) {
32
45
  const [context, setContext] = React2.useState(initialContext);
33
46
  React2.useEffect(() => {
34
47
  if (!isSegment) {
35
- registerPageContextState(setContext);
48
+ registerPageContextState((newContext) => {
49
+ setContext(newContext);
50
+ broadcastToSegments(newContext);
51
+ });
52
+ return void 0;
53
+ } else {
54
+ registerSegmentSetter(setContext);
55
+ return () => unregisterSegmentSetter(setContext);
36
56
  }
37
57
  }, [
38
58
  isSegment
@@ -239,7 +259,7 @@ var useHeader = defaultHooks.useHeader;
239
259
  var useCookies = defaultHooks.useCookies;
240
260
  var useCookie = defaultHooks.useCookie;
241
261
  var rootRegistry = /* @__PURE__ */ new WeakMap();
242
- function hydrateSegment(outlet, componentName, props) {
262
+ function hydrateSegment(outlet, componentName, props, layouts) {
243
263
  const modules = window.__MODULES__;
244
264
  if (!modules) {
245
265
  console.warn("[navigation] Module registry not available for segment hydration. Make sure entry-client.tsx exports window.__MODULES__.");
@@ -254,10 +274,11 @@ function hydrateSegment(outlet, componentName, props) {
254
274
  return;
255
275
  }
256
276
  const context = window.__CONTEXT__ || {};
277
+ const composedElement = composeWithLayouts(ViewComponent, props, layouts || [], context, modules);
257
278
  const element = /* @__PURE__ */ React2__default.default.createElement(PageContextProvider, {
258
279
  context,
259
280
  isSegment: true
260
- }, /* @__PURE__ */ React2__default.default.createElement(ViewComponent, props));
281
+ }, composedElement);
261
282
  let wrapper = outlet.querySelector("[data-segment-root]");
262
283
  if (wrapper) {
263
284
  const existingRoot = rootRegistry.get(wrapper);
@@ -275,6 +296,27 @@ function hydrateSegment(outlet, componentName, props) {
275
296
  rootRegistry.set(wrapper, root);
276
297
  }
277
298
  __name(hydrateSegment, "hydrateSegment");
299
+ function composeWithLayouts(ViewComponent, props, layouts, context, modules) {
300
+ let result = /* @__PURE__ */ React2__default.default.createElement(ViewComponent, props);
301
+ for (let i = layouts.length - 1; i >= 0; i--) {
302
+ const { name: layoutName, props: layoutProps } = layouts[i];
303
+ const Layout = resolveComponent(layoutName, modules);
304
+ if (!Layout) {
305
+ console.warn(`[navigation] Layout "${layoutName}" not found for hydration`);
306
+ continue;
307
+ }
308
+ result = /* @__PURE__ */ React2__default.default.createElement("div", {
309
+ "data-layout": layoutName
310
+ }, /* @__PURE__ */ React2__default.default.createElement(Layout, {
311
+ context,
312
+ layoutProps
313
+ }, /* @__PURE__ */ React2__default.default.createElement("div", {
314
+ "data-outlet": layoutName
315
+ }, result)));
316
+ }
317
+ return result;
318
+ }
319
+ __name(composeWithLayouts, "composeWithLayouts");
278
320
  function resolveComponent(name, modules) {
279
321
  const componentMap = Object.entries(modules).filter(([path, module]) => {
280
322
  const filename = path.split("/").pop();
@@ -342,8 +384,12 @@ async function navigate(url, options = {}) {
342
384
  return;
343
385
  }
344
386
  const outlet = await swapContent(response.html, response.swapTarget);
387
+ if (response.context) {
388
+ updatePageContext(response.context);
389
+ window.__CONTEXT__ = response.context;
390
+ }
345
391
  if (outlet) {
346
- hydrateSegment(outlet, response.componentName, response.props);
392
+ hydrateSegment(outlet, response.componentName, response.props, response.layouts);
347
393
  }
348
394
  if (replace) {
349
395
  history.replaceState({
@@ -354,10 +400,6 @@ async function navigate(url, options = {}) {
354
400
  url
355
401
  }, "", url);
356
402
  }
357
- if (response.context) {
358
- updatePageContext(response.context);
359
- window.__CONTEXT__ = response.context;
360
- }
361
403
  if (response.head) {
362
404
  updateHead(response.head);
363
405
  }
package/dist/client.mjs CHANGED
@@ -22,11 +22,31 @@ function updatePageContext(context) {
22
22
  setPageContextState?.(context);
23
23
  }
24
24
  __name(updatePageContext, "updatePageContext");
25
+ var segmentSetters = /* @__PURE__ */ new Set();
26
+ function registerSegmentSetter(setter) {
27
+ segmentSetters.add(setter);
28
+ }
29
+ __name(registerSegmentSetter, "registerSegmentSetter");
30
+ function unregisterSegmentSetter(setter) {
31
+ segmentSetters.delete(setter);
32
+ }
33
+ __name(unregisterSegmentSetter, "unregisterSegmentSetter");
34
+ function broadcastToSegments(context) {
35
+ segmentSetters.forEach((setter) => setter(context));
36
+ }
37
+ __name(broadcastToSegments, "broadcastToSegments");
25
38
  function PageContextProvider({ context: initialContext, children, isSegment = false }) {
26
39
  const [context, setContext] = useState(initialContext);
27
40
  useEffect(() => {
28
41
  if (!isSegment) {
29
- registerPageContextState(setContext);
42
+ registerPageContextState((newContext) => {
43
+ setContext(newContext);
44
+ broadcastToSegments(newContext);
45
+ });
46
+ return void 0;
47
+ } else {
48
+ registerSegmentSetter(setContext);
49
+ return () => unregisterSegmentSetter(setContext);
30
50
  }
31
51
  }, [
32
52
  isSegment
@@ -233,7 +253,7 @@ var useHeader = defaultHooks.useHeader;
233
253
  var useCookies = defaultHooks.useCookies;
234
254
  var useCookie = defaultHooks.useCookie;
235
255
  var rootRegistry = /* @__PURE__ */ new WeakMap();
236
- function hydrateSegment(outlet, componentName, props) {
256
+ function hydrateSegment(outlet, componentName, props, layouts) {
237
257
  const modules = window.__MODULES__;
238
258
  if (!modules) {
239
259
  console.warn("[navigation] Module registry not available for segment hydration. Make sure entry-client.tsx exports window.__MODULES__.");
@@ -248,10 +268,11 @@ function hydrateSegment(outlet, componentName, props) {
248
268
  return;
249
269
  }
250
270
  const context = window.__CONTEXT__ || {};
271
+ const composedElement = composeWithLayouts(ViewComponent, props, layouts || [], context, modules);
251
272
  const element = /* @__PURE__ */ React2.createElement(PageContextProvider, {
252
273
  context,
253
274
  isSegment: true
254
- }, /* @__PURE__ */ React2.createElement(ViewComponent, props));
275
+ }, composedElement);
255
276
  let wrapper = outlet.querySelector("[data-segment-root]");
256
277
  if (wrapper) {
257
278
  const existingRoot = rootRegistry.get(wrapper);
@@ -269,6 +290,27 @@ function hydrateSegment(outlet, componentName, props) {
269
290
  rootRegistry.set(wrapper, root);
270
291
  }
271
292
  __name(hydrateSegment, "hydrateSegment");
293
+ function composeWithLayouts(ViewComponent, props, layouts, context, modules) {
294
+ let result = /* @__PURE__ */ React2.createElement(ViewComponent, props);
295
+ for (let i = layouts.length - 1; i >= 0; i--) {
296
+ const { name: layoutName, props: layoutProps } = layouts[i];
297
+ const Layout = resolveComponent(layoutName, modules);
298
+ if (!Layout) {
299
+ console.warn(`[navigation] Layout "${layoutName}" not found for hydration`);
300
+ continue;
301
+ }
302
+ result = /* @__PURE__ */ React2.createElement("div", {
303
+ "data-layout": layoutName
304
+ }, /* @__PURE__ */ React2.createElement(Layout, {
305
+ context,
306
+ layoutProps
307
+ }, /* @__PURE__ */ React2.createElement("div", {
308
+ "data-outlet": layoutName
309
+ }, result)));
310
+ }
311
+ return result;
312
+ }
313
+ __name(composeWithLayouts, "composeWithLayouts");
272
314
  function resolveComponent(name, modules) {
273
315
  const componentMap = Object.entries(modules).filter(([path, module]) => {
274
316
  const filename = path.split("/").pop();
@@ -336,8 +378,12 @@ async function navigate(url, options = {}) {
336
378
  return;
337
379
  }
338
380
  const outlet = await swapContent(response.html, response.swapTarget);
381
+ if (response.context) {
382
+ updatePageContext(response.context);
383
+ window.__CONTEXT__ = response.context;
384
+ }
339
385
  if (outlet) {
340
- hydrateSegment(outlet, response.componentName, response.props);
386
+ hydrateSegment(outlet, response.componentName, response.props, response.layouts);
341
387
  }
342
388
  if (replace) {
343
389
  history.replaceState({
@@ -348,10 +394,6 @@ async function navigate(url, options = {}) {
348
394
  url
349
395
  }, "", url);
350
396
  }
351
- if (response.context) {
352
- updatePageContext(response.context);
353
- window.__CONTEXT__ = response.context;
354
- }
355
397
  if (response.head) {
356
398
  updateHead(response.head);
357
399
  }
@@ -1,12 +1,113 @@
1
1
  import { DynamicModule, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2
2
  import { ComponentType } from 'react';
3
- import { H as HeadData, R as RenderContext } from './render-response.interface-CxbuKGnV.js';
3
+ import { H as HeadData, R as RenderContext } from './render-response.interface-ClWJXKL4.js';
4
+ import { ServerResponse } from 'http';
4
5
  import { ViteDevServer } from 'vite';
5
- import { Response } from 'express';
6
6
  import { Reflector } from '@nestjs/core';
7
7
  import { Observable } from 'rxjs';
8
8
  import * as react_jsx_runtime from 'react/jsx-runtime';
9
9
 
10
+ /**
11
+ * Common HTTP request interface that works with both Express and Fastify.
12
+ * This represents the minimal interface needed for SSR context building.
13
+ */
14
+ interface SSRRequest {
15
+ /** Full request URL including query string */
16
+ url: string;
17
+ /** HTTP method (GET, POST, etc.) */
18
+ method: string;
19
+ /** Request headers */
20
+ headers: Record<string, string | string[] | undefined>;
21
+ /** URL path (Express: path, Fastify: routeOptions.url or url without query) */
22
+ path?: string;
23
+ /** Parsed query parameters */
24
+ query?: Record<string, string | string[] | undefined>;
25
+ /** Route parameters */
26
+ params?: Record<string, string>;
27
+ /** Parsed cookies (requires cookie-parser middleware) */
28
+ cookies?: Record<string, string>;
29
+ /**
30
+ * User object populated by authentication middleware (e.g., Passport).
31
+ * Type is `unknown` since the shape depends on your auth strategy.
32
+ */
33
+ user?: unknown;
34
+ /** Allow any additional properties for framework-specific extensions */
35
+ [key: string]: unknown;
36
+ }
37
+ /**
38
+ * Minimal interface for the raw Node.js response.
39
+ * This is a subset of ServerResponse that we actually use for streaming SSR.
40
+ */
41
+ interface RawServerResponse {
42
+ statusCode: number;
43
+ headersSent: boolean;
44
+ writableEnded: boolean;
45
+ setHeader(name: string, value: string | number | readonly string[]): void;
46
+ write(chunk: string | Buffer): boolean;
47
+ end(data?: string | Buffer): void;
48
+ on?(event: string, listener: (...args: any[]) => void): this;
49
+ }
50
+ /**
51
+ * Common HTTP response interface that works with both Express and Fastify.
52
+ * For streaming SSR, we access the raw Node.js ServerResponse.
53
+ */
54
+ interface SSRResponse {
55
+ /** HTTP status code (optional - Fastify has it on raw) */
56
+ statusCode?: number;
57
+ /** Whether headers have been sent (Express) */
58
+ headersSent?: boolean;
59
+ /** Whether headers have been sent (Fastify uses 'sent') */
60
+ sent?: boolean;
61
+ /** Whether the response stream has ended */
62
+ writableEnded?: boolean;
63
+ /** Set a response header */
64
+ setHeader?(name: string, value: string | number | readonly string[]): void;
65
+ /** Write data to the response */
66
+ write?(chunk: string | Buffer): boolean;
67
+ /** End the response */
68
+ end?(data?: string | Buffer): void;
69
+ /** Event listener for 'close' event */
70
+ on?(event: string, listener: (...args: any[]) => void): this;
71
+ /** Raw Node.js response (Fastify) - uses minimal interface for easier testing */
72
+ raw?: RawServerResponse | ServerResponse;
73
+ /** Allow additional properties */
74
+ [key: string]: unknown;
75
+ }
76
+
77
+ /**
78
+ * Custom context properties that can be added via context factory.
79
+ * Allows any properties to be merged into RenderContext.
80
+ */
81
+ type CustomContextProperties = Record<string, unknown>;
82
+ /**
83
+ * Context factory function signature
84
+ * Called for each request to build custom context properties
85
+ *
86
+ * @param params - Object containing the HTTP request (Express or Fastify)
87
+ * @returns Custom context properties to merge into RenderContext (sync or async)
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * // Simple: pass user from Passport
92
+ * context: ({ req }) => ({ user: req.user })
93
+ *
94
+ * // With CLS
95
+ * context: ({ req }) => ({
96
+ * user: req.user,
97
+ * tenant: cls.get('tenant'),
98
+ * })
99
+ *
100
+ * // Async with services
101
+ * context: async ({ req }) => ({
102
+ * user: req.user,
103
+ * permissions: await permissionService.getForUser(req.user),
104
+ * })
105
+ * ```
106
+ */
107
+ type ContextFactory<TRequest extends SSRRequest = SSRRequest> = (params: {
108
+ /** HTTP request object (Express Request or Fastify FastifyRequest) */
109
+ req: TRequest;
110
+ }) => CustomContextProperties | Promise<CustomContextProperties>;
10
111
  /**
11
112
  * SSR rendering mode configuration
12
113
  */
@@ -173,6 +274,52 @@ interface RenderConfig {
173
274
  * ```
174
275
  */
175
276
  allowedCookies?: string[];
277
+ /**
278
+ * Context factory function to enrich RenderContext with custom properties
279
+ *
280
+ * This is called for each request and the result is merged into the base
281
+ * RenderContext (url, path, query, params, method, headers, cookies).
282
+ *
283
+ * Use this to add user data, feature flags, tenant info, or any other
284
+ * request-scoped data that should be available in React components via usePageContext().
285
+ *
286
+ * Similar to @nestjs/graphql context option - you choose how to get the data:
287
+ * - From request object (req.user from Passport)
288
+ * - From CLS (nestjs-cls)
289
+ * - From request-scoped services
290
+ *
291
+ * @example
292
+ * ```typescript
293
+ * // Simple: pass user from Passport JWT strategy
294
+ * RenderModule.forRoot({
295
+ * context: ({ req }) => ({
296
+ * user: req.user,
297
+ * }),
298
+ * })
299
+ *
300
+ * // With nestjs-cls
301
+ * RenderModule.forRoot({
302
+ * context: ({ req }) => ({
303
+ * user: req.user,
304
+ * tenant: cls.get('tenant'),
305
+ * featureFlags: cls.get('featureFlags'),
306
+ * }),
307
+ * })
308
+ *
309
+ * // Async with services (use forRootAsync)
310
+ * RenderModule.forRootAsync({
311
+ * imports: [PermissionModule],
312
+ * inject: [PermissionService],
313
+ * useFactory: (permissionService: PermissionService) => ({
314
+ * context: async ({ req }) => ({
315
+ * user: req.user,
316
+ * permissions: await permissionService.getForUser(req.user),
317
+ * }),
318
+ * }),
319
+ * })
320
+ * ```
321
+ */
322
+ context?: ContextFactory;
176
323
  }
177
324
  /**
178
325
  * Template parts for streaming SSR
@@ -206,6 +353,11 @@ interface SegmentResponse {
206
353
  componentName: string;
207
354
  /** Page context for updating hooks (path, params, query, etc.) */
208
355
  context?: RenderContext;
356
+ /** Layouts below the swap target that need to be composed on client */
357
+ layouts?: Array<{
358
+ name: string;
359
+ props?: any;
360
+ }>;
209
361
  }
210
362
 
211
363
  declare class RenderModule {
@@ -402,7 +554,7 @@ declare class StreamingErrorHandler {
402
554
  * Handle error that occurred before shell was ready
403
555
  * Can still set HTTP status code and send error page
404
556
  */
405
- handleShellError(error: Error, res: Response, viewPath: string, isDevelopment: boolean): void;
557
+ handleShellError(error: Error, res: SSRResponse, viewPath: string, isDevelopment: boolean): void;
406
558
  /**
407
559
  * Handle error that occurred during streaming
408
560
  * Headers already sent, can only log the error
@@ -468,11 +620,11 @@ declare class StreamRenderer {
468
620
  *
469
621
  * @param viewComponent - The React component to render
470
622
  * @param data - Data to pass to the component
471
- * @param res - Express response object (required for streaming)
623
+ * @param res - HTTP response object (Express or Fastify)
472
624
  * @param context - Render context with Vite and manifest info
473
625
  * @param head - Head data for SEO tags
474
626
  */
475
- render(viewComponent: any, data: any, res: Response, context: StreamRenderContext, head?: HeadData): Promise<void>;
627
+ render(viewComponent: any, data: any, res: SSRResponse, context: StreamRenderContext, head?: HeadData): Promise<void>;
476
628
  }
477
629
 
478
630
  /**
@@ -537,7 +689,7 @@ declare class RenderService {
537
689
  * - Better TTFB, progressive rendering
538
690
  * - Requires response object
539
691
  */
540
- render(viewComponent: any, data?: any, res?: Response, head?: HeadData): Promise<string | void>;
692
+ render(viewComponent: any, data?: any, res?: SSRResponse, head?: HeadData): Promise<string | void>;
541
693
  /**
542
694
  * Render a segment for client-side navigation.
543
695
  * Always uses string mode (streaming not supported for segments).
@@ -555,7 +707,8 @@ declare class RenderInterceptor implements NestInterceptor {
555
707
  private renderService;
556
708
  private allowedHeaders?;
557
709
  private allowedCookies?;
558
- constructor(reflector: Reflector, renderService: RenderService, allowedHeaders?: string[] | undefined, allowedCookies?: string[] | undefined);
710
+ private contextFactory?;
711
+ constructor(reflector: Reflector, renderService: RenderService, allowedHeaders?: string[] | undefined, allowedCookies?: string[] | undefined, contextFactory?: ContextFactory | undefined);
559
712
  /**
560
713
  * Resolve the layout hierarchy for a given route
561
714
  * Hierarchy: Root Layout → Controller Layout → Method Layout → Page
@@ -606,4 +759,4 @@ declare function ErrorPageDevelopment({ error, viewPath, phase, }: ErrorPageDeve
606
759
  */
607
760
  declare function ErrorPageProduction(): react_jsx_runtime.JSX.Element;
608
761
 
609
- export { ErrorPageDevelopment as E, RenderModule as R, StreamingErrorHandler as S, TemplateParserService as T, RenderService as a, RenderInterceptor as b, type RenderConfig as c, type SSRMode as d, ErrorPageProduction as e };
762
+ export { type ContextFactory as C, ErrorPageDevelopment as E, RenderModule as R, StreamingErrorHandler as S, TemplateParserService as T, RenderService as a, RenderInterceptor as b, type RenderConfig as c, type SSRMode as d, ErrorPageProduction as e };
@@ -1,12 +1,113 @@
1
1
  import { DynamicModule, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2
2
  import { ComponentType } from 'react';
3
- import { H as HeadData, R as RenderContext } from './render-response.interface-CxbuKGnV.mjs';
3
+ import { H as HeadData, R as RenderContext } from './render-response.interface-ClWJXKL4.mjs';
4
+ import { ServerResponse } from 'http';
4
5
  import { ViteDevServer } from 'vite';
5
- import { Response } from 'express';
6
6
  import { Reflector } from '@nestjs/core';
7
7
  import { Observable } from 'rxjs';
8
8
  import * as react_jsx_runtime from 'react/jsx-runtime';
9
9
 
10
+ /**
11
+ * Common HTTP request interface that works with both Express and Fastify.
12
+ * This represents the minimal interface needed for SSR context building.
13
+ */
14
+ interface SSRRequest {
15
+ /** Full request URL including query string */
16
+ url: string;
17
+ /** HTTP method (GET, POST, etc.) */
18
+ method: string;
19
+ /** Request headers */
20
+ headers: Record<string, string | string[] | undefined>;
21
+ /** URL path (Express: path, Fastify: routeOptions.url or url without query) */
22
+ path?: string;
23
+ /** Parsed query parameters */
24
+ query?: Record<string, string | string[] | undefined>;
25
+ /** Route parameters */
26
+ params?: Record<string, string>;
27
+ /** Parsed cookies (requires cookie-parser middleware) */
28
+ cookies?: Record<string, string>;
29
+ /**
30
+ * User object populated by authentication middleware (e.g., Passport).
31
+ * Type is `unknown` since the shape depends on your auth strategy.
32
+ */
33
+ user?: unknown;
34
+ /** Allow any additional properties for framework-specific extensions */
35
+ [key: string]: unknown;
36
+ }
37
+ /**
38
+ * Minimal interface for the raw Node.js response.
39
+ * This is a subset of ServerResponse that we actually use for streaming SSR.
40
+ */
41
+ interface RawServerResponse {
42
+ statusCode: number;
43
+ headersSent: boolean;
44
+ writableEnded: boolean;
45
+ setHeader(name: string, value: string | number | readonly string[]): void;
46
+ write(chunk: string | Buffer): boolean;
47
+ end(data?: string | Buffer): void;
48
+ on?(event: string, listener: (...args: any[]) => void): this;
49
+ }
50
+ /**
51
+ * Common HTTP response interface that works with both Express and Fastify.
52
+ * For streaming SSR, we access the raw Node.js ServerResponse.
53
+ */
54
+ interface SSRResponse {
55
+ /** HTTP status code (optional - Fastify has it on raw) */
56
+ statusCode?: number;
57
+ /** Whether headers have been sent (Express) */
58
+ headersSent?: boolean;
59
+ /** Whether headers have been sent (Fastify uses 'sent') */
60
+ sent?: boolean;
61
+ /** Whether the response stream has ended */
62
+ writableEnded?: boolean;
63
+ /** Set a response header */
64
+ setHeader?(name: string, value: string | number | readonly string[]): void;
65
+ /** Write data to the response */
66
+ write?(chunk: string | Buffer): boolean;
67
+ /** End the response */
68
+ end?(data?: string | Buffer): void;
69
+ /** Event listener for 'close' event */
70
+ on?(event: string, listener: (...args: any[]) => void): this;
71
+ /** Raw Node.js response (Fastify) - uses minimal interface for easier testing */
72
+ raw?: RawServerResponse | ServerResponse;
73
+ /** Allow additional properties */
74
+ [key: string]: unknown;
75
+ }
76
+
77
+ /**
78
+ * Custom context properties that can be added via context factory.
79
+ * Allows any properties to be merged into RenderContext.
80
+ */
81
+ type CustomContextProperties = Record<string, unknown>;
82
+ /**
83
+ * Context factory function signature
84
+ * Called for each request to build custom context properties
85
+ *
86
+ * @param params - Object containing the HTTP request (Express or Fastify)
87
+ * @returns Custom context properties to merge into RenderContext (sync or async)
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * // Simple: pass user from Passport
92
+ * context: ({ req }) => ({ user: req.user })
93
+ *
94
+ * // With CLS
95
+ * context: ({ req }) => ({
96
+ * user: req.user,
97
+ * tenant: cls.get('tenant'),
98
+ * })
99
+ *
100
+ * // Async with services
101
+ * context: async ({ req }) => ({
102
+ * user: req.user,
103
+ * permissions: await permissionService.getForUser(req.user),
104
+ * })
105
+ * ```
106
+ */
107
+ type ContextFactory<TRequest extends SSRRequest = SSRRequest> = (params: {
108
+ /** HTTP request object (Express Request or Fastify FastifyRequest) */
109
+ req: TRequest;
110
+ }) => CustomContextProperties | Promise<CustomContextProperties>;
10
111
  /**
11
112
  * SSR rendering mode configuration
12
113
  */
@@ -173,6 +274,52 @@ interface RenderConfig {
173
274
  * ```
174
275
  */
175
276
  allowedCookies?: string[];
277
+ /**
278
+ * Context factory function to enrich RenderContext with custom properties
279
+ *
280
+ * This is called for each request and the result is merged into the base
281
+ * RenderContext (url, path, query, params, method, headers, cookies).
282
+ *
283
+ * Use this to add user data, feature flags, tenant info, or any other
284
+ * request-scoped data that should be available in React components via usePageContext().
285
+ *
286
+ * Similar to @nestjs/graphql context option - you choose how to get the data:
287
+ * - From request object (req.user from Passport)
288
+ * - From CLS (nestjs-cls)
289
+ * - From request-scoped services
290
+ *
291
+ * @example
292
+ * ```typescript
293
+ * // Simple: pass user from Passport JWT strategy
294
+ * RenderModule.forRoot({
295
+ * context: ({ req }) => ({
296
+ * user: req.user,
297
+ * }),
298
+ * })
299
+ *
300
+ * // With nestjs-cls
301
+ * RenderModule.forRoot({
302
+ * context: ({ req }) => ({
303
+ * user: req.user,
304
+ * tenant: cls.get('tenant'),
305
+ * featureFlags: cls.get('featureFlags'),
306
+ * }),
307
+ * })
308
+ *
309
+ * // Async with services (use forRootAsync)
310
+ * RenderModule.forRootAsync({
311
+ * imports: [PermissionModule],
312
+ * inject: [PermissionService],
313
+ * useFactory: (permissionService: PermissionService) => ({
314
+ * context: async ({ req }) => ({
315
+ * user: req.user,
316
+ * permissions: await permissionService.getForUser(req.user),
317
+ * }),
318
+ * }),
319
+ * })
320
+ * ```
321
+ */
322
+ context?: ContextFactory;
176
323
  }
177
324
  /**
178
325
  * Template parts for streaming SSR
@@ -206,6 +353,11 @@ interface SegmentResponse {
206
353
  componentName: string;
207
354
  /** Page context for updating hooks (path, params, query, etc.) */
208
355
  context?: RenderContext;
356
+ /** Layouts below the swap target that need to be composed on client */
357
+ layouts?: Array<{
358
+ name: string;
359
+ props?: any;
360
+ }>;
209
361
  }
210
362
 
211
363
  declare class RenderModule {
@@ -402,7 +554,7 @@ declare class StreamingErrorHandler {
402
554
  * Handle error that occurred before shell was ready
403
555
  * Can still set HTTP status code and send error page
404
556
  */
405
- handleShellError(error: Error, res: Response, viewPath: string, isDevelopment: boolean): void;
557
+ handleShellError(error: Error, res: SSRResponse, viewPath: string, isDevelopment: boolean): void;
406
558
  /**
407
559
  * Handle error that occurred during streaming
408
560
  * Headers already sent, can only log the error
@@ -468,11 +620,11 @@ declare class StreamRenderer {
468
620
  *
469
621
  * @param viewComponent - The React component to render
470
622
  * @param data - Data to pass to the component
471
- * @param res - Express response object (required for streaming)
623
+ * @param res - HTTP response object (Express or Fastify)
472
624
  * @param context - Render context with Vite and manifest info
473
625
  * @param head - Head data for SEO tags
474
626
  */
475
- render(viewComponent: any, data: any, res: Response, context: StreamRenderContext, head?: HeadData): Promise<void>;
627
+ render(viewComponent: any, data: any, res: SSRResponse, context: StreamRenderContext, head?: HeadData): Promise<void>;
476
628
  }
477
629
 
478
630
  /**
@@ -537,7 +689,7 @@ declare class RenderService {
537
689
  * - Better TTFB, progressive rendering
538
690
  * - Requires response object
539
691
  */
540
- render(viewComponent: any, data?: any, res?: Response, head?: HeadData): Promise<string | void>;
692
+ render(viewComponent: any, data?: any, res?: SSRResponse, head?: HeadData): Promise<string | void>;
541
693
  /**
542
694
  * Render a segment for client-side navigation.
543
695
  * Always uses string mode (streaming not supported for segments).
@@ -555,7 +707,8 @@ declare class RenderInterceptor implements NestInterceptor {
555
707
  private renderService;
556
708
  private allowedHeaders?;
557
709
  private allowedCookies?;
558
- constructor(reflector: Reflector, renderService: RenderService, allowedHeaders?: string[] | undefined, allowedCookies?: string[] | undefined);
710
+ private contextFactory?;
711
+ constructor(reflector: Reflector, renderService: RenderService, allowedHeaders?: string[] | undefined, allowedCookies?: string[] | undefined, contextFactory?: ContextFactory | undefined);
559
712
  /**
560
713
  * Resolve the layout hierarchy for a given route
561
714
  * Hierarchy: Root Layout → Controller Layout → Method Layout → Page
@@ -606,4 +759,4 @@ declare function ErrorPageDevelopment({ error, viewPath, phase, }: ErrorPageDeve
606
759
  */
607
760
  declare function ErrorPageProduction(): react_jsx_runtime.JSX.Element;
608
761
 
609
- export { ErrorPageDevelopment as E, RenderModule as R, StreamingErrorHandler as S, TemplateParserService as T, RenderService as a, RenderInterceptor as b, type RenderConfig as c, type SSRMode as d, ErrorPageProduction as e };
762
+ export { type ContextFactory as C, ErrorPageDevelopment as E, RenderModule as R, StreamingErrorHandler as S, TemplateParserService as T, RenderService as a, RenderInterceptor as b, type RenderConfig as c, type SSRMode as d, ErrorPageProduction as e };