@nestjs-ssr/react 0.1.5 → 0.1.7

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
@@ -12,23 +12,29 @@ If you value [Uncle Bob's Clean Architecture](https://blog.cleancoder.com/uncle-
12
12
 
13
13
  **Clear Separation of Concerns:**
14
14
  ```typescript
15
+ // View component with typed props (Presentation Layer)
16
+ export interface UserProfileProps {
17
+ user: User;
18
+ }
19
+
20
+ export default function UserProfile(props: PageProps<UserProfileProps>) {
21
+ return <div>{props.user.name}</div>; // Pure presentation
22
+ }
23
+
15
24
  // Server logic stays in controllers (Application Layer)
25
+ import UserProfile from './views/user-profile';
26
+
16
27
  @Controller()
17
28
  export class UserController {
18
29
  constructor(private userService: UserService) {} // Proper DI
19
30
 
20
31
  @Get('/users/:id')
21
- @Render('views/user-profile')
32
+ @Render(UserProfile) // Type-safe! Cmd+Click to navigate
22
33
  async getUserProfile(@Param('id') id: string) {
23
34
  const user = await this.userService.findById(id); // Business logic
24
- return { user }; // Pure data - no rendering concerns
35
+ return { user }; // TypeScript validates this matches UserProfileProps
25
36
  }
26
37
  }
27
-
28
- // View logic stays in React components (Presentation Layer)
29
- export default function UserProfile({ data }: PageProps<{ user: User }>) {
30
- return <div>{data.user.name}</div>; // Pure presentation
31
- }
32
38
  ```
33
39
 
34
40
  **No Server/Client Confusion:**
@@ -73,24 +79,30 @@ export default function Page() {
73
79
 
74
80
  **NestJS SSR maintains boundaries:**
75
81
  ```tsx
82
+ // ✅ View: Client-only, pure presentation
83
+ export interface ProductsProps {
84
+ products: Product[];
85
+ }
86
+
87
+ export default function Products(props: PageProps<ProductsProps>) {
88
+ const [selected, setSelected] = useState(null);
89
+ return <ProductList products={props.products} onSelect={setSelected} />;
90
+ }
91
+
76
92
  // ✅ Controller: Server-only, testable, uses DI
93
+ import Products from './views/products';
94
+
77
95
  @Controller()
78
96
  export class ProductController {
79
97
  constructor(private productService: ProductService) {}
80
98
 
81
99
  @Get('/products')
82
100
  @UseGuards(AuthGuard) // Proper middleware
83
- @Render('views/products')
101
+ @Render(Products) // Type-safe component reference
84
102
  async list() {
85
103
  return { products: await this.productService.findAll() };
86
104
  }
87
105
  }
88
-
89
- // ✅ View: Client-only, pure presentation
90
- export default function Products({ data }: PageProps) {
91
- const [selected, setSelected] = useState(null);
92
- return <ProductList products={data.products} onSelect={setSelected} />;
93
- }
94
106
  ```
95
107
 
96
108
  ### Performance as a Bonus
@@ -105,6 +117,8 @@ Following clean architecture doesn't mean sacrificing performance. In our benchm
105
117
 
106
118
  ## Features
107
119
 
120
+ ✅ **Type-Safe Props** - Automatic validation of controller return types against component props
121
+ ✅ **IDE Navigation** - Cmd+Click on components to jump to view files
108
122
  ✅ **Architectural Integrity** - Respects SOLID and Clean Architecture principles
109
123
  ✅ **Dependency Injection** - Full NestJS DI throughout your application
110
124
  ✅ **Clear Boundaries** - Server code is server, client code is client
@@ -129,10 +143,15 @@ npm install @nestjs-ssr/react react react-dom vite
129
143
  // vite.config.ts
130
144
  import { defineConfig } from 'vite';
131
145
  import react from '@vitejs/plugin-react';
132
- import { viewRegistryPlugin } from '@nestjs-ssr/react/vite';
146
+ import { resolve } from 'path';
133
147
 
134
148
  export default defineConfig({
135
- plugins: [react(), viewRegistryPlugin()],
149
+ plugins: [react()],
150
+ resolve: {
151
+ alias: {
152
+ '@': resolve(__dirname, 'src'),
153
+ },
154
+ },
136
155
  });
137
156
  ```
138
157
 
@@ -151,18 +170,18 @@ import { RenderModule } from '@nestjs-ssr/react';
151
170
  export class AppModule {}
152
171
  ```
153
172
 
154
- ### 3. Create a View
173
+ ### 3. Create a View Component
155
174
 
156
175
  ```typescript
157
176
  // src/views/home.tsx
158
177
  import type { PageProps } from '@nestjs-ssr/react';
159
178
 
160
- interface HomeData {
179
+ export interface HomeProps {
161
180
  message: string;
162
181
  }
163
182
 
164
- export default function Home({ data }: PageProps<HomeData>) {
165
- return <h1>{data.message}</h1>;
183
+ export default function Home(props: PageProps<HomeProps>) {
184
+ return <h1>{props.message}</h1>;
166
185
  }
167
186
  ```
168
187
 
@@ -172,61 +191,21 @@ export default function Home({ data }: PageProps<HomeData>) {
172
191
  // app.controller.ts
173
192
  import { Controller, Get } from '@nestjs/common';
174
193
  import { Render } from '@nestjs-ssr/react';
194
+ import Home from './views/home';
175
195
 
176
196
  @Controller()
177
197
  export class AppController {
178
198
  @Get()
179
- @Render('views/home')
199
+ @Render(Home) // Type-safe! Cmd+Click to navigate to view
180
200
  getHome() {
181
201
  return { message: 'Hello from NestJS SSR!' };
182
202
  }
183
203
  }
184
204
  ```
185
205
 
186
- ### 5. Add Entry Files
206
+ That's it! No manual view registry or entry files needed - everything is handled automatically.
187
207
 
188
- Create these files in `src/view/`:
189
-
190
- **entry-client.tsx**:
191
- ```typescript
192
- import { StrictMode } from 'react';
193
- import { hydrateRoot } from 'react-dom/client';
194
- import { viewRegistry } from './view-registry.generated';
195
-
196
- const viewPath = window.__COMPONENT_PATH__;
197
- const initialProps = window.__INITIAL_STATE__ || {};
198
- const renderContext = window.__CONTEXT__ || {};
199
-
200
- const ViewComponent = viewRegistry[viewPath];
201
-
202
- if (!ViewComponent) {
203
- throw new Error(`View "${viewPath}" not found in registry`);
204
- }
205
-
206
- hydrateRoot(
207
- document.getElementById('root')!,
208
- <StrictMode>
209
- <ViewComponent data={initialProps} context={renderContext} />
210
- </StrictMode>
211
- );
212
- ```
213
-
214
- **entry-server.tsx**:
215
- ```typescript
216
- import { viewRegistry } from './view-registry.generated';
217
-
218
- export function render(viewPath: string, props: any, context: any) {
219
- const ViewComponent = viewRegistry[viewPath];
220
-
221
- if (!ViewComponent) {
222
- throw new Error(`View "${viewPath}" not found in registry`);
223
- }
224
-
225
- return <ViewComponent data={props} context={context} />;
226
- }
227
- ```
228
-
229
- ### 6. Run
208
+ ### 5. Run
230
209
 
231
210
  ```bash
232
211
  npm run dev
@@ -238,38 +217,46 @@ Visit [http://localhost:3000](http://localhost:3000) 🎉
238
217
 
239
218
  ### The `@Render` Decorator
240
219
 
241
- The decorator intercepts your controller's response and renders it with React:
220
+ The decorator takes a React component and automatically validates that your controller returns the correct props:
242
221
 
243
222
  ```typescript
223
+ import UserProfile from './views/user-profile';
224
+
244
225
  @Get('/users/:id')
245
- @Render('users/views/user-profile')
226
+ @Render(UserProfile) // Type-safe! Cmd+Click to navigate
246
227
  async getUser(@Param('id') id: string) {
247
228
  const user = await this.userService.findOne(id);
248
- return { user }; // Passed as `data` prop to component
229
+ return { user }; // TypeScript validates this matches component props
249
230
  }
250
231
  ```
251
232
 
252
233
  ### Type-Safe Props
253
234
 
254
- Components receive props with full TypeScript support:
235
+ Components receive props with full TypeScript support and validation:
255
236
 
256
237
  ```typescript
257
- import type { PageProps, RenderContext } from '@nestjs-ssr/react';
238
+ import type { PageProps } from '@nestjs-ssr/react';
258
239
 
259
- interface UserData {
240
+ export interface UserProfileProps {
260
241
  user: User;
261
242
  }
262
243
 
263
- export default function UserProfile({ data, context }: PageProps<UserData>) {
244
+ export default function UserProfile(props: PageProps<UserProfileProps>) {
264
245
  return (
265
246
  <div>
266
- <h1>{data.user.name}</h1>
267
- <p>Requested from: {context.path}</p>
247
+ <h1>{props.user.name}</h1>
248
+ <p>Requested from: {props.context.path}</p>
268
249
  </div>
269
250
  );
270
251
  }
271
252
  ```
272
253
 
254
+ **Benefits:**
255
+ - ✅ Build-time validation - wrong props = compilation error
256
+ - ✅ Cmd+Click navigation from controller to view file
257
+ - ✅ No manual type annotations needed
258
+ - ✅ Refactoring-friendly - rename props with confidence
259
+
273
260
  ### Request Context
274
261
 
275
262
  Every component receives the request context:
@@ -295,7 +295,7 @@ declare class TemplateParserService {
295
295
  * This library handles all edge cases including escaping dangerous characters,
296
296
  * functions, dates, regexes, and prevents prototype pollution.
297
297
  */
298
- buildInlineScripts(data: any, context: any, componentPath: string): string;
298
+ buildInlineScripts(data: any, context: any, componentName: string): string;
299
299
  /**
300
300
  * Get client script tag for hydration
301
301
  *
@@ -367,12 +367,14 @@ declare class RenderService {
367
367
  private serverManifest;
368
368
  private isDevelopment;
369
369
  private ssrMode;
370
+ private readonly entryServerPath;
371
+ private readonly entryClientPath;
370
372
  constructor(templateParser: TemplateParserService, streamingErrorHandler: StreamingErrorHandler, ssrMode?: SSRMode, defaultHead?: HeadData | undefined);
371
373
  setViteServer(vite: ViteDevServer): void;
372
374
  /**
373
375
  * Main render method that routes to string or stream mode
374
376
  */
375
- render(viewPath: string, data?: any, res?: Response, head?: HeadData): Promise<string | void>;
377
+ render(viewComponent: any, data?: any, res?: Response, head?: HeadData): Promise<string | void>;
376
378
  /**
377
379
  * Merge default head with page-specific head
378
380
  * Page-specific head values override defaults
@@ -295,7 +295,7 @@ declare class TemplateParserService {
295
295
  * This library handles all edge cases including escaping dangerous characters,
296
296
  * functions, dates, regexes, and prevents prototype pollution.
297
297
  */
298
- buildInlineScripts(data: any, context: any, componentPath: string): string;
298
+ buildInlineScripts(data: any, context: any, componentName: string): string;
299
299
  /**
300
300
  * Get client script tag for hydration
301
301
  *
@@ -367,12 +367,14 @@ declare class RenderService {
367
367
  private serverManifest;
368
368
  private isDevelopment;
369
369
  private ssrMode;
370
+ private readonly entryServerPath;
371
+ private readonly entryClientPath;
370
372
  constructor(templateParser: TemplateParserService, streamingErrorHandler: StreamingErrorHandler, ssrMode?: SSRMode, defaultHead?: HeadData | undefined);
371
373
  setViteServer(vite: ViteDevServer): void;
372
374
  /**
373
375
  * Main render method that routes to string or stream mode
374
376
  */
375
- render(viewPath: string, data?: any, res?: Response, head?: HeadData): Promise<string | void>;
377
+ render(viewComponent: any, data?: any, res?: Response, head?: HeadData): Promise<string | void>;
376
378
  /**
377
379
  * Merge default head with page-specific head
378
380
  * Page-specific head values override defaults
package/dist/index.d.mts CHANGED
@@ -1,8 +1,7 @@
1
- import { H as HeadData } from './index-Bptct1Q3.mjs';
2
- export { E as ErrorPageDevelopment, f as ErrorPageProduction, c as RenderConfig, b as RenderInterceptor, R as RenderModule, e as RenderResponse, a as RenderService, d as SSRMode, S as StreamingErrorHandler, T as TemplateParserService } from './index-Bptct1Q3.mjs';
3
- import * as _nestjs_common from '@nestjs/common';
4
- export { viewRegistryPlugin } from './vite/index.mjs';
5
- import 'react';
1
+ import { H as HeadData } from './index-Bpzo1KfR.mjs';
2
+ export { E as ErrorPageDevelopment, f as ErrorPageProduction, c as RenderConfig, b as RenderInterceptor, R as RenderModule, e as RenderResponse, a as RenderService, d as SSRMode, S as StreamingErrorHandler, T as TemplateParserService } from './index-Bpzo1KfR.mjs';
3
+ import React from 'react';
4
+ import '@nestjs/common';
6
5
  import 'vite';
7
6
  import 'express';
8
7
  import '@nestjs/core';
@@ -122,39 +121,49 @@ type PageProps<TProps = {}> = TProps & {
122
121
  };
123
122
 
124
123
  /**
125
- * Interface for view paths - augmented by the generated view registry.
126
- * This enables type-safe path validation in Render decorator.
124
+ * Extract the data type T from PageProps<T>.
125
+ * PageProps<T> = T & { head?, context }, so we extract T by removing those keys.
127
126
  */
128
- interface ViewPaths {
129
- }
127
+ type ExtractPagePropsData<P> = P extends PageProps<infer T> ? T : P extends {
128
+ head?: any;
129
+ context: any;
130
+ } ? Omit<P, 'head' | 'context'> : P;
130
131
  /**
131
- * Type-safe view path - union of all registered view paths.
132
- * This is populated via module augmentation from the generated view registry.
132
+ * Extract controller return type from a React component's props.
133
133
  */
134
- type ViewPath = keyof ViewPaths extends never ? string : keyof ViewPaths;
134
+ type ExtractComponentData<T> = T extends React.ComponentType<infer P> ? ExtractPagePropsData<P> : never;
135
135
  /**
136
136
  * Decorator to render a React component as the response.
137
- * Provides IDE autocomplete and type checking for view paths.
138
137
  *
139
- * Works the same as NestJS's @Render() decorator for template engines,
140
- * but renders React components with SSR instead.
138
+ * Import the component directly for Cmd+Click navigation in your IDE.
139
+ * TypeScript automatically validates your controller returns the correct props.
141
140
  *
142
- * @param viewPath - Path to the React component (e.g., 'users/views/user-list')
141
+ * @param component - The React component to render
143
142
  *
144
143
  * @example
145
144
  * ```typescript
145
+ * // Your view component (views/home.tsx)
146
+ * export interface HomeProps {
147
+ * message: string;
148
+ * }
149
+ * export default function Home(props: PageProps<HomeProps>) { ... }
150
+ *
151
+ * // Your controller - Cmd+Click on Home navigates to the view file!
152
+ * import Home from './views/home';
153
+ *
146
154
  * @Get()
147
- * @Render('users/views/user-list')
148
- * getUsers() {
149
- * return { users: [...] };
155
+ * @Render(Home) // Type-safe! Wrong props = build error
156
+ * getHome() {
157
+ * return { message: 'Hello' }; // ✅ Correct
158
+ * // return { wrong: 'prop' }; // ❌ Type error!
150
159
  * }
151
160
  * ```
152
161
  */
153
- declare const Render: (viewPath: ViewPath) => _nestjs_common.CustomDecorator<string>;
162
+ declare function Render<T extends React.ComponentType<any>>(component: T): <TMethod extends (...args: any[]) => ExtractComponentData<T> | Promise<ExtractComponentData<T>>>(target: any, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<TMethod>) => TypedPropertyDescriptor<TMethod> | void;
154
163
  /**
155
164
  * @deprecated Use `Render` instead. This alias will be removed in a future version.
156
165
  */
157
- declare const ReactRender: (viewPath: ViewPath) => _nestjs_common.CustomDecorator<string>;
166
+ declare const ReactRender: typeof Render;
158
167
 
159
168
  /**
160
169
  * Hook to access the full page context.
@@ -217,4 +226,4 @@ declare function useQuery(): Record<string, string | string[]>;
217
226
  */
218
227
  declare function useUserAgent(): string | undefined;
219
228
 
220
- export { HeadData, type PageProps, ReactRender, Render, type RenderContext, type ViewPath, type ViewPaths, usePageContext, useParams, useQuery, useUserAgent };
229
+ export { HeadData, type PageProps, ReactRender, Render, type RenderContext, usePageContext, useParams, useQuery, useUserAgent };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { H as HeadData } from './index-Bptct1Q3.js';
2
- export { E as ErrorPageDevelopment, f as ErrorPageProduction, c as RenderConfig, b as RenderInterceptor, R as RenderModule, e as RenderResponse, a as RenderService, d as SSRMode, S as StreamingErrorHandler, T as TemplateParserService } from './index-Bptct1Q3.js';
3
- import * as _nestjs_common from '@nestjs/common';
4
- export { viewRegistryPlugin } from './vite/index.js';
5
- import 'react';
1
+ import { H as HeadData } from './index-Bpzo1KfR.js';
2
+ export { E as ErrorPageDevelopment, f as ErrorPageProduction, c as RenderConfig, b as RenderInterceptor, R as RenderModule, e as RenderResponse, a as RenderService, d as SSRMode, S as StreamingErrorHandler, T as TemplateParserService } from './index-Bpzo1KfR.js';
3
+ import React from 'react';
4
+ import '@nestjs/common';
6
5
  import 'vite';
7
6
  import 'express';
8
7
  import '@nestjs/core';
@@ -122,39 +121,49 @@ type PageProps<TProps = {}> = TProps & {
122
121
  };
123
122
 
124
123
  /**
125
- * Interface for view paths - augmented by the generated view registry.
126
- * This enables type-safe path validation in Render decorator.
124
+ * Extract the data type T from PageProps<T>.
125
+ * PageProps<T> = T & { head?, context }, so we extract T by removing those keys.
127
126
  */
128
- interface ViewPaths {
129
- }
127
+ type ExtractPagePropsData<P> = P extends PageProps<infer T> ? T : P extends {
128
+ head?: any;
129
+ context: any;
130
+ } ? Omit<P, 'head' | 'context'> : P;
130
131
  /**
131
- * Type-safe view path - union of all registered view paths.
132
- * This is populated via module augmentation from the generated view registry.
132
+ * Extract controller return type from a React component's props.
133
133
  */
134
- type ViewPath = keyof ViewPaths extends never ? string : keyof ViewPaths;
134
+ type ExtractComponentData<T> = T extends React.ComponentType<infer P> ? ExtractPagePropsData<P> : never;
135
135
  /**
136
136
  * Decorator to render a React component as the response.
137
- * Provides IDE autocomplete and type checking for view paths.
138
137
  *
139
- * Works the same as NestJS's @Render() decorator for template engines,
140
- * but renders React components with SSR instead.
138
+ * Import the component directly for Cmd+Click navigation in your IDE.
139
+ * TypeScript automatically validates your controller returns the correct props.
141
140
  *
142
- * @param viewPath - Path to the React component (e.g., 'users/views/user-list')
141
+ * @param component - The React component to render
143
142
  *
144
143
  * @example
145
144
  * ```typescript
145
+ * // Your view component (views/home.tsx)
146
+ * export interface HomeProps {
147
+ * message: string;
148
+ * }
149
+ * export default function Home(props: PageProps<HomeProps>) { ... }
150
+ *
151
+ * // Your controller - Cmd+Click on Home navigates to the view file!
152
+ * import Home from './views/home';
153
+ *
146
154
  * @Get()
147
- * @Render('users/views/user-list')
148
- * getUsers() {
149
- * return { users: [...] };
155
+ * @Render(Home) // Type-safe! Wrong props = build error
156
+ * getHome() {
157
+ * return { message: 'Hello' }; // ✅ Correct
158
+ * // return { wrong: 'prop' }; // ❌ Type error!
150
159
  * }
151
160
  * ```
152
161
  */
153
- declare const Render: (viewPath: ViewPath) => _nestjs_common.CustomDecorator<string>;
162
+ declare function Render<T extends React.ComponentType<any>>(component: T): <TMethod extends (...args: any[]) => ExtractComponentData<T> | Promise<ExtractComponentData<T>>>(target: any, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<TMethod>) => TypedPropertyDescriptor<TMethod> | void;
154
163
  /**
155
164
  * @deprecated Use `Render` instead. This alias will be removed in a future version.
156
165
  */
157
- declare const ReactRender: (viewPath: ViewPath) => _nestjs_common.CustomDecorator<string>;
166
+ declare const ReactRender: typeof Render;
158
167
 
159
168
  /**
160
169
  * Hook to access the full page context.
@@ -217,4 +226,4 @@ declare function useQuery(): Record<string, string | string[]>;
217
226
  */
218
227
  declare function useUserAgent(): string | undefined;
219
228
 
220
- export { HeadData, type PageProps, ReactRender, Render, type RenderContext, type ViewPath, type ViewPaths, usePageContext, useParams, useQuery, useUserAgent };
229
+ export { HeadData, type PageProps, ReactRender, Render, type RenderContext, usePageContext, useParams, useQuery, useUserAgent };