@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
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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(
|
|
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
|
-
},
|
|
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(
|
|
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
|
-
},
|
|
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-
|
|
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:
|
|
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 -
|
|
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:
|
|
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?:
|
|
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
|
-
|
|
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-
|
|
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:
|
|
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 -
|
|
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:
|
|
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?:
|
|
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
|
-
|
|
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 };
|