@nestjs-ssr/react 0.3.7 → 0.3.9

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
@@ -3,70 +3,82 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@nestjs-ssr/react)](https://www.npmjs.com/package/@nestjs-ssr/react)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- React as a view layer for NestJS. Controllers return data. Components render it. One app.
6
+ **React SSR for NestJS. One app. One deploy. Full type safety from database to DOM.**
7
7
 
8
- **[Documentation](https://georgialexandrov.github.io/nestjs-ssr/)** | **[Getting Started](https://georgialexandrov.github.io/nestjs-ssr/guide/installation)**
8
+ No separate frontend. No second router. No type boundary you maintain by hand. Controllers return data, components render it, TypeScript enforces the contract.
9
9
 
10
- ## Quick Start
10
+ **[Documentation](https://georgialexandrov.github.io/nestjs-ssr/)** | **[Get Started](https://georgialexandrov.github.io/nestjs-ssr/guide/installation)**
11
11
 
12
- ```bash
13
- npx @nestjs-ssr/react init
14
- ```
12
+ ## The whole contract in two files
15
13
 
16
14
  ```typescript
17
- @Get(':id')
18
- @Render(ProductDetail)
19
- async getProduct(@Param('id') id: string) {
20
- return { product: await this.productService.findById(id) };
15
+ // recipes.controller.ts
16
+ import { Controller, Get, Param } from '@nestjs/common';
17
+ import { Render, Layout } from '@nestjs-ssr/react';
18
+ import { RecipesLayout } from './views/recipes-layout';
19
+ import { RecipeDetail } from './views/recipe-detail';
20
+
21
+ @Controller('recipes')
22
+ @Layout(RecipesLayout)
23
+ export class RecipesController {
24
+ @Get(':slug')
25
+ @Render(RecipeDetail)
26
+ getRecipe(@Param('slug') slug: string) {
27
+ const recipe = this.recipes.findBySlug(slug);
28
+ return {
29
+ recipe,
30
+ chef: this.chefs.findById(recipe.chefId),
31
+ head: { title: recipe.name },
32
+ };
33
+ }
21
34
  }
22
35
  ```
23
36
 
24
37
  ```tsx
25
- export default function ProductDetail({
26
- product,
27
- }: PageProps<{ product: Product }>) {
28
- return <h1>{product.name}</h1>;
38
+ // recipe-detail.tsx
39
+ import { PageProps } from '@nestjs-ssr/react';
40
+
41
+ export default function RecipeDetail({
42
+ recipe,
43
+ chef,
44
+ }: PageProps<RecipeDetailProps>) {
45
+ return (
46
+ <article>
47
+ <h1>{recipe.name}</h1>
48
+ <p>{recipe.description}</p>
49
+ <IngredientList items={recipe.ingredients} />
50
+ <ChefCard chef={chef} />
51
+ </article>
52
+ );
29
53
  }
30
54
  ```
31
55
 
32
- Type mismatch = build fails.
56
+ Return the wrong shape and TypeScript catches it before the code runs.
33
57
 
34
- ## Test in Isolation
58
+ ## Nothing breaks
35
59
 
36
- ```typescript
37
- // Controller: no React
38
- expect(await controller.getProduct('123')).toEqual({ product: { id: '123' } });
39
-
40
- // Component: no NestJS
41
- render(<ProductDetail data={{ product: mockProduct }} />);
42
- ```
60
+ Your NestJS app stays exactly as it is. Routing, guards, pipes, interceptors, services, modules, testing — all unchanged. You're adding a view layer, not rewriting your backend.
43
61
 
44
- ## What You Get
62
+ ## Everything improves
45
63
 
46
- **Rendering:**
64
+ - **One type, both sides** — controller return type is the component's props. Change one, the other breaks at build time.
65
+ - **Layouts that stay put** — nested layouts persist across navigations. Header, sidebar, shell — rendered once, never re-mounted.
66
+ - **No full reloads** — link clicks fetch only the changed segment. State, scroll position, animations stay alive.
67
+ - **Stream or string** — `renderToString` for simplicity, `renderToPipeableStream` for performance. Suspense boundaries stream as they resolve.
68
+ - **SEO out of the box** — title, meta, Open Graph, JSON-LD. Return `head` from your controller.
69
+ - **Vite HMR** — instant updates, no page refresh. Works with Express and Fastify.
47
70
 
48
- - Type-safe data flow from controller to component
49
- - Hierarchical layouts (root → controller → method)
50
- - Head tags (title, meta, OG, JSON-LD)
51
- - Stream or string mode
71
+ ## Add it to an existing NestJS app
52
72
 
53
- **Request Context:**
54
-
55
- - Hooks: `useParams()`, `useQuery()`, `useHeader()`, `useCookie()`
56
- - Whitelist what reaches the client
57
-
58
- **Development:**
73
+ ```bash
74
+ npx @nestjs-ssr/react init
75
+ ```
59
76
 
60
- - Integrated mode: Vite inside NestJS, one process
61
- - Separate mode: standalone Vite server, true HMR
77
+ One command. Works with Express and Fastify.
62
78
 
63
79
  ## Requirements
64
80
 
65
- - Node.js 20+
66
- - NestJS 11+
67
- - React 19+
68
- - Vite 6+
69
- - TypeScript 5+
81
+ Node.js 20+ / NestJS 11+ / React 19+ / Vite 6+ / TypeScript 5+
70
82
 
71
83
  ## Documentation
72
84
 
@@ -74,6 +86,8 @@ render(<ProductDetail data={{ product: mockProduct }} />);
74
86
 
75
87
  - [Installation](https://georgialexandrov.github.io/nestjs-ssr/guide/installation)
76
88
  - [Rendering](https://georgialexandrov.github.io/nestjs-ssr/guide/rendering)
89
+ - [Layouts](https://georgialexandrov.github.io/nestjs-ssr/guide/layouts)
90
+ - [Client-Side Navigation](https://georgialexandrov.github.io/nestjs-ssr/guide/navigation)
77
91
  - [Request Context](https://georgialexandrov.github.io/nestjs-ssr/guide/request-context)
78
92
  - [Configuration](https://georgialexandrov.github.io/nestjs-ssr/guide/configuration)
79
93
  - [API Reference](https://georgialexandrov.github.io/nestjs-ssr/guide/api)
@@ -164,9 +164,29 @@ function composeWithLayout(
164
164
  return result;
165
165
  }
166
166
 
167
- // Build layouts array - use RootLayout if it exists (matching server behavior)
167
+ // Build layouts array from server-provided __LAYOUTS__ data
168
+ // This ensures controller-level layouts (e.g., @Layout(RecipesLayout)) are
169
+ // included during hydration on hard refresh, not just the auto-discovered root layout
170
+ const layoutsData = window.__LAYOUTS__ || [];
168
171
  const layouts: Array<{ layout: React.ComponentType<any>; props?: any }> = [];
169
- if (RootLayout) {
172
+
173
+ for (const { name: layoutName, props: layoutProps } of layoutsData) {
174
+ const layoutEntry = componentMap.find(
175
+ (c) =>
176
+ c.name === layoutName ||
177
+ c.normalizedFilename === layoutName ||
178
+ c.filename === layoutName.toLowerCase(),
179
+ );
180
+ if (layoutEntry) {
181
+ layouts.push({ layout: layoutEntry.component, props: layoutProps || {} });
182
+ } else if (layoutName === 'RootLayout' && RootLayout) {
183
+ // Fallback: if the auto-discovered root layout wasn't in componentMap by name
184
+ layouts.push({ layout: RootLayout, props: layoutProps || {} });
185
+ }
186
+ }
187
+
188
+ // Fallback: if no __LAYOUTS__ data, use auto-discovered RootLayout
189
+ if (layouts.length === 0 && RootLayout) {
170
190
  layouts.push({ layout: RootLayout, props: {} });
171
191
  }
172
192
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs-ssr/react",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "React SSR for NestJS that respects Clean Architecture. Proper DI, SOLID principles, clear separation of concerns.",
5
5
  "keywords": [
6
6
  "nestjs",
@@ -90,6 +90,10 @@
90
90
  "test:integration:prod": "TEST_MODE=prod playwright test -c test/integration/playwright.config.ts",
91
91
  "test:integration:run": "pnpm test:integration:dev",
92
92
  "test:integration:clean": "rm -rf test/integration/fixtures/*/",
93
+ "test:e2e:setup": "tsx test/e2e/setup/create-fixtures.ts",
94
+ "test:e2e:dev": "TEST_MODE=dev playwright test -c test/e2e/playwright.config.ts",
95
+ "test:e2e:prod": "TEST_MODE=prod playwright test -c test/e2e/playwright.config.ts",
96
+ "test:e2e:clean": "rm -rf test/e2e/fixtures/*/",
93
97
  "size": "size-limit",
94
98
  "api:extract": "api-extractor run --local --verbose",
95
99
  "api:check": "api-extractor run --verbose",
@@ -135,30 +139,30 @@
135
139
  }
136
140
  },
137
141
  "dependencies": {
138
- "citty": "^0.2.0",
142
+ "citty": "^0.2.1",
139
143
  "consola": "^3.4.2",
140
144
  "devalue": "^5.6.2",
141
145
  "escape-html": "^1.0.3"
142
146
  },
143
147
  "devDependencies": {
144
- "@microsoft/api-extractor": "^7.56.2",
148
+ "@microsoft/api-extractor": "^7.56.3",
145
149
  "@nestjs/common": "^11.1.13",
146
150
  "@nestjs/core": "^11.1.13",
147
151
  "@nestjs/platform-express": "^11.1.13",
148
152
  "@nestjs/testing": "^11.1.13",
149
- "@playwright/test": "^1.58.1",
153
+ "@playwright/test": "^1.58.2",
150
154
  "@testing-library/jest-dom": "^6.9.1",
151
155
  "@testing-library/react": "^16.3.2",
152
156
  "@types/escape-html": "^1.0.4",
153
157
  "@types/express": "^5.0.6",
154
- "@types/node": "^25.2.1",
155
- "@types/react": "^19.2.13",
158
+ "@types/node": "^25.2.3",
159
+ "@types/react": "^19.2.14",
156
160
  "@types/react-dom": "^19.2.3",
157
161
  "@types/supertest": "^6.0.3",
158
- "@vitejs/plugin-react": "^5.1.3",
162
+ "@vitejs/plugin-react": "^5.1.4",
159
163
  "@vitest/coverage-v8": "^4.0.18",
160
164
  "@vitest/ui": "^4.0.18",
161
- "happy-dom": "^20.5.0",
165
+ "happy-dom": "^20.6.1",
162
166
  "react": "^19.2.4",
163
167
  "react-dom": "^19.2.4",
164
168
  "rxjs": "^7.8.2",
@@ -164,9 +164,29 @@ function composeWithLayout(
164
164
  return result;
165
165
  }
166
166
 
167
- // Build layouts array - use RootLayout if it exists (matching server behavior)
167
+ // Build layouts array from server-provided __LAYOUTS__ data
168
+ // This ensures controller-level layouts (e.g., @Layout(RecipesLayout)) are
169
+ // included during hydration on hard refresh, not just the auto-discovered root layout
170
+ const layoutsData = window.__LAYOUTS__ || [];
168
171
  const layouts: Array<{ layout: React.ComponentType<any>; props?: any }> = [];
169
- if (RootLayout) {
172
+
173
+ for (const { name: layoutName, props: layoutProps } of layoutsData) {
174
+ const layoutEntry = componentMap.find(
175
+ (c) =>
176
+ c.name === layoutName ||
177
+ c.normalizedFilename === layoutName ||
178
+ c.filename === layoutName.toLowerCase(),
179
+ );
180
+ if (layoutEntry) {
181
+ layouts.push({ layout: layoutEntry.component, props: layoutProps || {} });
182
+ } else if (layoutName === 'RootLayout' && RootLayout) {
183
+ // Fallback: if the auto-discovered root layout wasn't in componentMap by name
184
+ layouts.push({ layout: RootLayout, props: layoutProps || {} });
185
+ }
186
+ }
187
+
188
+ // Fallback: if no __LAYOUTS__ data, use auto-discovered RootLayout
189
+ if (layouts.length === 0 && RootLayout) {
170
190
  layouts.push({ layout: RootLayout, props: {} });
171
191
  }
172
192