@nestjs-ssr/react 0.1.12 → 0.2.0

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/README.md CHANGED
@@ -1,122 +1,90 @@
1
- # @nestjs-ssr/react
1
+ # NestJS SSR
2
+
3
+ [![npm version](https://badge.fury.io/js/%40nestjs-ssr%2Freact.svg)](https://www.npmjs.com/package/@nestjs-ssr/react)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
5
 
3
6
  > **⚠️ Preview Release**
4
7
  > This package is currently in active development. The API may change between minor versions. Production use is not recommended yet.
5
8
 
6
- Server-side rendering for React in NestJS with full TypeScript support and type-safe props.
7
-
8
- ## Features
9
+ Server-rendered React for NestJS. Controllers return data, components render it.
9
10
 
10
- - **Type-Safe Props** - TypeScript validates controller returns match component props
11
- - **Zero Config** - Works out of the box with sensible defaults
12
- - **Streaming SSR** - Modern renderToPipeableStream support
13
- - **HMR in Development** - Powered by Vite for instant feedback
14
- - **Production Ready** - Code splitting, caching, and optimizations built-in
11
+ Clean Architecture: layers separated, dependencies inward, business logic framework-agnostic.
15
12
 
16
- ## Installation
13
+ ## When To Use
17
14
 
18
- ```bash
19
- npm install @nestjs-ssr/react
20
- npx nestjs-ssr # Set up your project automatically
21
- ```
15
+ **Use this when:**
22
16
 
23
- The CLI installs dependencies, creates entry files, and configures TypeScript/Vite for you.
17
+ - You have NestJS and want React instead of Handlebars/EJS
18
+ - Testable layers matter more than file-based routing
19
+ - You want feature modules (controller + service + view together)
24
20
 
25
- ## Usage
21
+ **Use Next.js when:**
26
22
 
27
- **1. Register the module**
23
+ - You're starting fresh without NestJS
24
+ - You want the React ecosystem's defaults
25
+ - File-based routing fits your mental model
28
26
 
29
- ```typescript
30
- // app.module.ts
31
- import { RenderModule } from '@nestjs-ssr/react';
27
+ ## Quick Start
32
28
 
33
- @Module({
34
- imports: [RenderModule],
35
- })
36
- export class AppModule {}
29
+ ```bash
30
+ npx nestjs-ssr init
37
31
  ```
38
32
 
39
- **2. Create a view component**
40
-
41
33
  ```typescript
42
- // src/views/home.tsx
43
- import type { PageProps } from '@nestjs-ssr/react';
44
-
45
- export interface HomeProps {
46
- message: string;
34
+ @Get(':id')
35
+ @Render(ProductDetail)
36
+ async getProduct(@Param('id') id: string) {
37
+ return { product: await this.productService.findById(id) };
47
38
  }
39
+ ```
48
40
 
49
- export default function Home(props: PageProps<HomeProps>) {
50
- return <h1>{props.message}</h1>;
41
+ ```tsx
42
+ export default function ProductDetail({
43
+ data,
44
+ }: PageProps<{ product: Product }>) {
45
+ return <h1>{data.product.name}</h1>;
51
46
  }
52
47
  ```
53
48
 
54
- **3. Use in a controller**
49
+ Type mismatch = build fails.
50
+
51
+ ## Test in Isolation
55
52
 
56
53
  ```typescript
57
- // app.controller.ts
58
- import { Controller, Get } from '@nestjs/common';
59
- import { Render } from '@nestjs-ssr/react';
60
- import Home from './views/home';
61
-
62
- @Controller()
63
- export class AppController {
64
- @Get()
65
- @Render(Home)
66
- getHome() {
67
- return { message: 'Hello World' }; // TypeScript validates this!
68
- }
69
- }
54
+ // Controller: no React
55
+ expect(await controller.getProduct('123')).toEqual({ product: { id: '123' } });
56
+
57
+ // Component: no NestJS
58
+ render(<ProductDetail data={{ product: mockProduct }} />);
70
59
  ```
71
60
 
72
- That's it! Run `npm run dev` and visit http://localhost:3000
61
+ ## Features
73
62
 
74
- ## API
63
+ **Rendering:**
75
64
 
76
- ### React Hooks
65
+ - Type-safe data flow from controller to component
66
+ - Hierarchical layouts (module → controller → method)
67
+ - Head tags via decorators (title, meta, OG, JSON-LD)
77
68
 
78
- ```typescript
79
- import { usePageContext, useParams, useQuery } from '@nestjs-ssr/react';
69
+ **Request Context:**
80
70
 
81
- function MyComponent() {
82
- const context = usePageContext(); // { url, path, query, params, ... }
83
- const params = useParams(); // Route params
84
- const query = useQuery(); // Query string
85
- return <div>User ID: {params.id}</div>;
86
- }
87
- ```
71
+ - Hooks: params, query, headers, session, user agent
72
+ - Whitelist what reaches the client
88
73
 
89
- ### Head Tags & SEO
74
+ **Development:**
90
75
 
91
- ```typescript
92
- import { Head } from '@nestjs-ssr/react';
93
-
94
- export default function MyPage(props: PageProps<MyProps>) {
95
- return (
96
- <>
97
- <Head>
98
- <title>My Page</title>
99
- <meta name="description" content="Page description" />
100
- </Head>
101
- <div>{props.content}</div>
102
- </>
103
- );
104
- }
105
- ```
76
+ - Integrated mode: one process, full refresh
77
+ - Proxy mode: separate Vite, true HMR
106
78
 
107
- ## Documentation
79
+ ## Docs
108
80
 
109
- - [Full Documentation](https://georgialexandrov.github.io/nestjs-ssr/)
110
- - [Examples](https://github.com/georgialexandrov/nestjs-ssr/tree/main/examples)
111
- - [GitHub](https://github.com/georgialexandrov/nestjs-ssr)
81
+ [Full documentation →](https://georgialexandrov.github.io/nest-ssr/)
112
82
 
113
- ## Requirements
83
+ ## Examples
114
84
 
115
- - Node.js 20+
116
- - NestJS 11+
117
- - React 19+
118
- - TypeScript 5+
85
+ **[Minimal](./examples/minimal/)** - Simplest setup with integrated Vite mode
86
+ **[Minimal HMR](./examples/minimal-hmr/)** - Dual-server architecture for full HMR
119
87
 
120
88
  ## License
121
89
 
122
- MIT © [Georgi Alexandrov](https://github.com/georgialexandrov)
90
+ MIT
package/dist/cli/init.js CHANGED
@@ -269,9 +269,9 @@ export default defineConfig({
269
269
  if (!args["skip-install"]) {
270
270
  consola.consola.start("Checking dependencies...");
271
271
  const requiredDeps = {
272
- "react": "^19.0.0",
272
+ react: "^19.0.0",
273
273
  "react-dom": "^19.0.0",
274
- "vite": "^7.0.0",
274
+ vite: "^7.0.0",
275
275
  "@vitejs/plugin-react": "^4.0.0"
276
276
  };
277
277
  const missingDeps = [];
package/dist/cli/init.mjs CHANGED
@@ -266,9 +266,9 @@ export default defineConfig({
266
266
  if (!args["skip-install"]) {
267
267
  consola.start("Checking dependencies...");
268
268
  const requiredDeps = {
269
- "react": "^19.0.0",
269
+ react: "^19.0.0",
270
270
  "react-dom": "^19.0.0",
271
- "vite": "^7.0.0",
271
+ vite: "^7.0.0",
272
272
  "@vitejs/plugin-react": "^4.0.0"
273
273
  };
274
274
  const missingDeps = [];
@@ -40,6 +40,21 @@ interface HeadData {
40
40
  content: string;
41
41
  [key: string]: any;
42
42
  }>;
43
+ /** Script tags for analytics, tracking, etc. */
44
+ scripts?: Array<{
45
+ src?: string;
46
+ async?: boolean;
47
+ defer?: boolean;
48
+ type?: string;
49
+ innerHTML?: string;
50
+ [key: string]: any;
51
+ }>;
52
+ /** JSON-LD structured data for search engines */
53
+ jsonLd?: Array<Record<string, any>>;
54
+ /** Attributes to add to <html> tag (e.g., lang, dir) */
55
+ htmlAttributes?: Record<string, string>;
56
+ /** Attributes to add to <body> tag (e.g., class, data-theme) */
57
+ bodyAttributes?: Record<string, string>;
43
58
  }
44
59
  /**
45
60
  * Response structure for SSR rendering
@@ -57,12 +72,16 @@ interface HeadData {
57
72
  * // Treated as: { props: { message: 'Hello' } }
58
73
  * }
59
74
  *
60
- * // Advanced case - with head data
75
+ * // Advanced case - with head data and layout props
61
76
  * @Render('views/user')
62
77
  * getUser(@Param('id') id: string) {
63
78
  * const user = await this.userService.findOne(id);
64
79
  * return {
65
80
  * props: { user },
81
+ * layoutProps: {
82
+ * title: user.name,
83
+ * subtitle: 'User Profile'
84
+ * },
66
85
  * head: {
67
86
  * title: `${user.name} - Profile`,
68
87
  * description: user.bio,
@@ -77,6 +96,17 @@ interface RenderResponse<T = any> {
77
96
  props: T;
78
97
  /** HTML head data (title, meta tags, links) */
79
98
  head?: HeadData;
99
+ /**
100
+ * Props passed to layout components (dynamic, per-request)
101
+ *
102
+ * These props are merged with static layout props from decorators:
103
+ * - Static props from @Layout decorator (controller level)
104
+ * - Static props from @Render decorator (method level)
105
+ * - Dynamic props from this field (highest priority)
106
+ *
107
+ * All layout components in the hierarchy receive the merged props.
108
+ */
109
+ layoutProps?: Record<string, any>;
80
110
  }
81
111
 
82
112
  /**
@@ -154,6 +184,32 @@ interface RenderConfig {
154
184
  * ```
155
185
  */
156
186
  vite?: ViteConfig;
187
+ /**
188
+ * Custom HTML template for SSR
189
+ * Provide either a file path or template string
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * // File path (absolute or relative to cwd)
194
+ * RenderModule.register({
195
+ * template: './src/views/custom-template.html'
196
+ * })
197
+ *
198
+ * // Template string
199
+ * RenderModule.register({
200
+ * template: `<!DOCTYPE html>
201
+ * <html>
202
+ * <head><!--styles--></head>
203
+ * <body>
204
+ * <div id="root"><!--app-html--></div>
205
+ * <!--initial-state-->
206
+ * <!--client-scripts-->
207
+ * </body>
208
+ * </html>`
209
+ * })
210
+ * ```
211
+ */
212
+ template?: string;
157
213
  /**
158
214
  * Custom error page component for development environment
159
215
  * Receives error details and renders custom error UI
@@ -186,6 +242,41 @@ interface RenderConfig {
186
242
  * ```
187
243
  */
188
244
  defaultHead?: HeadData;
245
+ /**
246
+ * HTTP headers to pass to client
247
+ * By default, no headers are passed for security
248
+ * Use this to opt-in to specific headers that are safe to expose
249
+ *
250
+ * Common safe headers: user-agent, accept-language, referer
251
+ * Security warning: Never include sensitive headers like authorization, cookie, etc.
252
+ *
253
+ * @default []
254
+ *
255
+ * @example
256
+ * ```typescript
257
+ * RenderModule.register({
258
+ * allowedHeaders: ['user-agent', 'accept-language', 'x-tenant-id', 'x-api-version']
259
+ * })
260
+ * ```
261
+ */
262
+ allowedHeaders?: string[];
263
+ /**
264
+ * Cookie names to pass to client
265
+ * By default, no cookies are passed to client for security
266
+ * Use this to opt-in to specific cookies that are safe to expose
267
+ *
268
+ * Security warning: Never include sensitive cookies like session tokens, auth cookies, etc.
269
+ *
270
+ * @default []
271
+ *
272
+ * @example
273
+ * ```typescript
274
+ * RenderModule.register({
275
+ * allowedCookies: ['theme', 'locale', 'consent']
276
+ * })
277
+ * ```
278
+ */
279
+ allowedCookies?: string[];
189
280
  }
190
281
  /**
191
282
  * Template parts for streaming SSR
@@ -291,11 +382,14 @@ declare class TemplateParserService {
291
382
  /**
292
383
  * Build inline script that provides initial state to the client
293
384
  *
294
- * Safely serializes data using serialize-javascript to avoid XSS vulnerabilities.
295
- * This library handles all edge cases including escaping dangerous characters,
296
- * functions, dates, regexes, and prevents prototype pollution.
385
+ * Safely serializes data using devalue to avoid XSS vulnerabilities.
386
+ * Devalue is designed specifically for SSR, handling complex types safely
387
+ * while being faster and more secure than alternatives.
297
388
  */
298
- buildInlineScripts(data: any, context: any, componentName: string): string;
389
+ buildInlineScripts(data: any, context: any, componentName: string, layouts?: Array<{
390
+ layout: any;
391
+ props?: any;
392
+ }>): string;
299
393
  /**
300
394
  * Get client script tag for hydration
301
395
  *
@@ -368,8 +462,18 @@ declare class RenderService {
368
462
  private isDevelopment;
369
463
  private ssrMode;
370
464
  private readonly entryServerPath;
371
- constructor(templateParser: TemplateParserService, streamingErrorHandler: StreamingErrorHandler, ssrMode?: SSRMode, defaultHead?: HeadData | undefined);
465
+ private rootLayout;
466
+ private rootLayoutChecked;
467
+ constructor(templateParser: TemplateParserService, streamingErrorHandler: StreamingErrorHandler, ssrMode?: SSRMode, defaultHead?: HeadData | undefined, customTemplate?: string);
372
468
  setViteServer(vite: ViteDevServer): void;
469
+ /**
470
+ * Get the root layout component if it exists
471
+ * Auto-discovers layout files at conventional paths:
472
+ * - src/views/layout.tsx
473
+ * - src/views/layout/index.tsx
474
+ * - src/views/_layout.tsx
475
+ */
476
+ getRootLayout(): Promise<any | null>;
373
477
  /**
374
478
  * Main render method that routes to string or stream mode
375
479
  */
@@ -392,7 +496,19 @@ declare class RenderService {
392
496
  declare class RenderInterceptor implements NestInterceptor {
393
497
  private reflector;
394
498
  private renderService;
395
- constructor(reflector: Reflector, renderService: RenderService);
499
+ private allowedHeaders?;
500
+ private allowedCookies?;
501
+ constructor(reflector: Reflector, renderService: RenderService, allowedHeaders?: string[] | undefined, allowedCookies?: string[] | undefined);
502
+ /**
503
+ * Resolve the layout hierarchy for a given route
504
+ * Hierarchy: Root Layout → Controller Layout → Method Layout → Page
505
+ *
506
+ * Props are merged in priority order:
507
+ * 1. Static props from @Layout decorator (base)
508
+ * 2. Static props from @Render decorator (override)
509
+ * 3. Dynamic props from controller return (final override)
510
+ */
511
+ private resolveLayoutChain;
396
512
  intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
397
513
  }
398
514
 
@@ -417,4 +533,4 @@ declare function ErrorPageDevelopment({ error, viewPath, phase, }: ErrorPageDeve
417
533
  */
418
534
  declare function ErrorPageProduction(): react_jsx_runtime.JSX.Element;
419
535
 
420
- export { ErrorPageDevelopment as E, type HeadData as H, RenderModule as R, StreamingErrorHandler as S, TemplateParserService as T, RenderService as a, RenderInterceptor as b, type RenderConfig as c, type SSRMode as d, type RenderResponse as e, ErrorPageProduction as f };
536
+ export { ErrorPageDevelopment as E, type HeadData as H, type RenderResponse as R, StreamingErrorHandler as S, TemplateParserService as T, RenderModule as a, RenderService as b, RenderInterceptor as c, type RenderConfig as d, type SSRMode as e, ErrorPageProduction as f };
@@ -40,6 +40,21 @@ interface HeadData {
40
40
  content: string;
41
41
  [key: string]: any;
42
42
  }>;
43
+ /** Script tags for analytics, tracking, etc. */
44
+ scripts?: Array<{
45
+ src?: string;
46
+ async?: boolean;
47
+ defer?: boolean;
48
+ type?: string;
49
+ innerHTML?: string;
50
+ [key: string]: any;
51
+ }>;
52
+ /** JSON-LD structured data for search engines */
53
+ jsonLd?: Array<Record<string, any>>;
54
+ /** Attributes to add to <html> tag (e.g., lang, dir) */
55
+ htmlAttributes?: Record<string, string>;
56
+ /** Attributes to add to <body> tag (e.g., class, data-theme) */
57
+ bodyAttributes?: Record<string, string>;
43
58
  }
44
59
  /**
45
60
  * Response structure for SSR rendering
@@ -57,12 +72,16 @@ interface HeadData {
57
72
  * // Treated as: { props: { message: 'Hello' } }
58
73
  * }
59
74
  *
60
- * // Advanced case - with head data
75
+ * // Advanced case - with head data and layout props
61
76
  * @Render('views/user')
62
77
  * getUser(@Param('id') id: string) {
63
78
  * const user = await this.userService.findOne(id);
64
79
  * return {
65
80
  * props: { user },
81
+ * layoutProps: {
82
+ * title: user.name,
83
+ * subtitle: 'User Profile'
84
+ * },
66
85
  * head: {
67
86
  * title: `${user.name} - Profile`,
68
87
  * description: user.bio,
@@ -77,6 +96,17 @@ interface RenderResponse<T = any> {
77
96
  props: T;
78
97
  /** HTML head data (title, meta tags, links) */
79
98
  head?: HeadData;
99
+ /**
100
+ * Props passed to layout components (dynamic, per-request)
101
+ *
102
+ * These props are merged with static layout props from decorators:
103
+ * - Static props from @Layout decorator (controller level)
104
+ * - Static props from @Render decorator (method level)
105
+ * - Dynamic props from this field (highest priority)
106
+ *
107
+ * All layout components in the hierarchy receive the merged props.
108
+ */
109
+ layoutProps?: Record<string, any>;
80
110
  }
81
111
 
82
112
  /**
@@ -154,6 +184,32 @@ interface RenderConfig {
154
184
  * ```
155
185
  */
156
186
  vite?: ViteConfig;
187
+ /**
188
+ * Custom HTML template for SSR
189
+ * Provide either a file path or template string
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * // File path (absolute or relative to cwd)
194
+ * RenderModule.register({
195
+ * template: './src/views/custom-template.html'
196
+ * })
197
+ *
198
+ * // Template string
199
+ * RenderModule.register({
200
+ * template: `<!DOCTYPE html>
201
+ * <html>
202
+ * <head><!--styles--></head>
203
+ * <body>
204
+ * <div id="root"><!--app-html--></div>
205
+ * <!--initial-state-->
206
+ * <!--client-scripts-->
207
+ * </body>
208
+ * </html>`
209
+ * })
210
+ * ```
211
+ */
212
+ template?: string;
157
213
  /**
158
214
  * Custom error page component for development environment
159
215
  * Receives error details and renders custom error UI
@@ -186,6 +242,41 @@ interface RenderConfig {
186
242
  * ```
187
243
  */
188
244
  defaultHead?: HeadData;
245
+ /**
246
+ * HTTP headers to pass to client
247
+ * By default, no headers are passed for security
248
+ * Use this to opt-in to specific headers that are safe to expose
249
+ *
250
+ * Common safe headers: user-agent, accept-language, referer
251
+ * Security warning: Never include sensitive headers like authorization, cookie, etc.
252
+ *
253
+ * @default []
254
+ *
255
+ * @example
256
+ * ```typescript
257
+ * RenderModule.register({
258
+ * allowedHeaders: ['user-agent', 'accept-language', 'x-tenant-id', 'x-api-version']
259
+ * })
260
+ * ```
261
+ */
262
+ allowedHeaders?: string[];
263
+ /**
264
+ * Cookie names to pass to client
265
+ * By default, no cookies are passed to client for security
266
+ * Use this to opt-in to specific cookies that are safe to expose
267
+ *
268
+ * Security warning: Never include sensitive cookies like session tokens, auth cookies, etc.
269
+ *
270
+ * @default []
271
+ *
272
+ * @example
273
+ * ```typescript
274
+ * RenderModule.register({
275
+ * allowedCookies: ['theme', 'locale', 'consent']
276
+ * })
277
+ * ```
278
+ */
279
+ allowedCookies?: string[];
189
280
  }
190
281
  /**
191
282
  * Template parts for streaming SSR
@@ -291,11 +382,14 @@ declare class TemplateParserService {
291
382
  /**
292
383
  * Build inline script that provides initial state to the client
293
384
  *
294
- * Safely serializes data using serialize-javascript to avoid XSS vulnerabilities.
295
- * This library handles all edge cases including escaping dangerous characters,
296
- * functions, dates, regexes, and prevents prototype pollution.
385
+ * Safely serializes data using devalue to avoid XSS vulnerabilities.
386
+ * Devalue is designed specifically for SSR, handling complex types safely
387
+ * while being faster and more secure than alternatives.
297
388
  */
298
- buildInlineScripts(data: any, context: any, componentName: string): string;
389
+ buildInlineScripts(data: any, context: any, componentName: string, layouts?: Array<{
390
+ layout: any;
391
+ props?: any;
392
+ }>): string;
299
393
  /**
300
394
  * Get client script tag for hydration
301
395
  *
@@ -368,8 +462,18 @@ declare class RenderService {
368
462
  private isDevelopment;
369
463
  private ssrMode;
370
464
  private readonly entryServerPath;
371
- constructor(templateParser: TemplateParserService, streamingErrorHandler: StreamingErrorHandler, ssrMode?: SSRMode, defaultHead?: HeadData | undefined);
465
+ private rootLayout;
466
+ private rootLayoutChecked;
467
+ constructor(templateParser: TemplateParserService, streamingErrorHandler: StreamingErrorHandler, ssrMode?: SSRMode, defaultHead?: HeadData | undefined, customTemplate?: string);
372
468
  setViteServer(vite: ViteDevServer): void;
469
+ /**
470
+ * Get the root layout component if it exists
471
+ * Auto-discovers layout files at conventional paths:
472
+ * - src/views/layout.tsx
473
+ * - src/views/layout/index.tsx
474
+ * - src/views/_layout.tsx
475
+ */
476
+ getRootLayout(): Promise<any | null>;
373
477
  /**
374
478
  * Main render method that routes to string or stream mode
375
479
  */
@@ -392,7 +496,19 @@ declare class RenderService {
392
496
  declare class RenderInterceptor implements NestInterceptor {
393
497
  private reflector;
394
498
  private renderService;
395
- constructor(reflector: Reflector, renderService: RenderService);
499
+ private allowedHeaders?;
500
+ private allowedCookies?;
501
+ constructor(reflector: Reflector, renderService: RenderService, allowedHeaders?: string[] | undefined, allowedCookies?: string[] | undefined);
502
+ /**
503
+ * Resolve the layout hierarchy for a given route
504
+ * Hierarchy: Root Layout → Controller Layout → Method Layout → Page
505
+ *
506
+ * Props are merged in priority order:
507
+ * 1. Static props from @Layout decorator (base)
508
+ * 2. Static props from @Render decorator (override)
509
+ * 3. Dynamic props from controller return (final override)
510
+ */
511
+ private resolveLayoutChain;
396
512
  intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
397
513
  }
398
514
 
@@ -417,4 +533,4 @@ declare function ErrorPageDevelopment({ error, viewPath, phase, }: ErrorPageDeve
417
533
  */
418
534
  declare function ErrorPageProduction(): react_jsx_runtime.JSX.Element;
419
535
 
420
- export { ErrorPageDevelopment as E, type HeadData as H, RenderModule as R, StreamingErrorHandler as S, TemplateParserService as T, RenderService as a, RenderInterceptor as b, type RenderConfig as c, type SSRMode as d, type RenderResponse as e, ErrorPageProduction as f };
536
+ export { ErrorPageDevelopment as E, type HeadData as H, type RenderResponse as R, StreamingErrorHandler as S, TemplateParserService as T, RenderModule as a, RenderService as b, RenderInterceptor as c, type RenderConfig as d, type SSRMode as e, ErrorPageProduction as f };