@nestjs-ssr/react 0.3.7 → 0.3.8
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 +56 -42
- package/dist/templates/entry-client.tsx +22 -2
- package/package.json +8 -4
- package/src/templates/entry-client.tsx +22 -2
package/README.md
CHANGED
|
@@ -3,70 +3,82 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@nestjs-ssr/react)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
React
|
|
6
|
+
**React SSR for NestJS. One app. One deploy. Full type safety from database to DOM.**
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
10
|
+
**[Documentation](https://georgialexandrov.github.io/nestjs-ssr/)** | **[Get Started](https://georgialexandrov.github.io/nestjs-ssr/guide/installation)**
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
npx @nestjs-ssr/react init
|
|
14
|
-
```
|
|
12
|
+
## The whole contract in two files
|
|
15
13
|
|
|
16
14
|
```typescript
|
|
17
|
-
|
|
18
|
-
@
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
56
|
+
Return the wrong shape and TypeScript catches it before the code runs.
|
|
33
57
|
|
|
34
|
-
##
|
|
58
|
+
## Nothing breaks
|
|
35
59
|
|
|
36
|
-
|
|
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
|
-
##
|
|
62
|
+
## Everything improves
|
|
45
63
|
|
|
46
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- Whitelist what reaches the client
|
|
57
|
-
|
|
58
|
-
**Development:**
|
|
73
|
+
```bash
|
|
74
|
+
npx @nestjs-ssr/react init
|
|
75
|
+
```
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
- Separate mode: standalone Vite server, true HMR
|
|
77
|
+
One command. Works with Express and Fastify.
|
|
62
78
|
|
|
63
79
|
## Requirements
|
|
64
80
|
|
|
65
|
-
|
|
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 -
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.8",
|
|
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",
|
|
@@ -141,17 +145,17 @@
|
|
|
141
145
|
"escape-html": "^1.0.3"
|
|
142
146
|
},
|
|
143
147
|
"devDependencies": {
|
|
144
|
-
"@microsoft/api-extractor": "^7.56.
|
|
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.
|
|
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.
|
|
158
|
+
"@types/node": "^25.2.2",
|
|
155
159
|
"@types/react": "^19.2.13",
|
|
156
160
|
"@types/react-dom": "^19.2.3",
|
|
157
161
|
"@types/supertest": "^6.0.3",
|
|
@@ -164,9 +164,29 @@ function composeWithLayout(
|
|
|
164
164
|
return result;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
// Build layouts array -
|
|
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
|
-
|
|
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
|
|