@nestjs-ssr/react 0.1.12 → 0.2.1

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
@@ -32,6 +32,10 @@ var main = citty.defineCommand({
32
32
  type: "boolean",
33
33
  description: "Skip automatic dependency installation",
34
34
  default: false
35
+ },
36
+ integration: {
37
+ type: "string",
38
+ description: 'Integration type: "separate" (Vite as separate server) or "integrated" (Vite bundled with NestJS)'
35
39
  }
36
40
  },
37
41
  async run({ args }) {
@@ -39,6 +43,32 @@ var main = citty.defineCommand({
39
43
  const viewsDir = args.views;
40
44
  consola.consola.box("@nestjs-ssr/react initialization");
41
45
  consola.consola.start("Setting up your NestJS SSR React project...\n");
46
+ let integrationType = args.integration;
47
+ if (!integrationType) {
48
+ const response = await consola.consola.prompt("How do you want to run Vite during development?", {
49
+ type: "select",
50
+ options: [
51
+ {
52
+ label: "Separate server (Vite runs on its own port, e.g., 5173)",
53
+ value: "separate"
54
+ },
55
+ {
56
+ label: "Integrated with NestJS (Vite middleware runs within NestJS)",
57
+ value: "integrated"
58
+ }
59
+ ]
60
+ });
61
+ integrationType = response;
62
+ }
63
+ if (![
64
+ "separate",
65
+ "integrated"
66
+ ].includes(integrationType)) {
67
+ consola.consola.error(`Invalid integration type: "${integrationType}". Must be "separate" or "integrated"`);
68
+ process.exit(1);
69
+ }
70
+ consola.consola.info(`Using ${integrationType === "separate" ? "separate server" : "integrated"} mode
71
+ `);
42
72
  const templateLocations = [
43
73
  path.resolve(__dirname$1, "../../src/templates"),
44
74
  path.resolve(__dirname$1, "../templates")
@@ -76,6 +106,15 @@ var main = citty.defineCommand({
76
106
  fs.copyFileSync(entryServerSrc, entryServerDest);
77
107
  consola.consola.success(`Created ${viewsDir}/entry-server.tsx`);
78
108
  }
109
+ consola.consola.start("Creating index.html...");
110
+ const indexHtmlSrc = path.join(templateDir, "index.html");
111
+ const indexHtmlDest = path.join(cwd, viewsDir, "index.html");
112
+ if (fs.existsSync(indexHtmlDest) && !args.force) {
113
+ consola.consola.warn(`${viewsDir}/index.html already exists (use --force to overwrite)`);
114
+ } else {
115
+ fs.copyFileSync(indexHtmlSrc, indexHtmlDest);
116
+ consola.consola.success(`Created ${viewsDir}/index.html`);
117
+ }
79
118
  consola.consola.start("Configuring vite.config.js...");
80
119
  const viteConfigPath = path.join(cwd, "vite.config.js");
81
120
  const viteConfigTs = path.join(cwd, "vite.config.ts");
@@ -85,12 +124,28 @@ var main = citty.defineCommand({
85
124
  consola.consola.warn(`${useTypeScript ? "vite.config.ts" : "vite.config.js"} already exists`);
86
125
  consola.consola.info("Please manually add to your Vite config:");
87
126
  consola.consola.log(" import { resolve } from 'path';");
127
+ if (integrationType === "separate") {
128
+ consola.consola.log(" server: {");
129
+ consola.consola.log(" port: 5173,");
130
+ consola.consola.log(" strictPort: true,");
131
+ consola.consola.log(" hmr: { port: 5173 },");
132
+ consola.consola.log(" },");
133
+ }
88
134
  consola.consola.log(" build: {");
89
135
  consola.consola.log(" rollupOptions: {");
90
136
  consola.consola.log(` input: { client: resolve(__dirname, '${viewsDir}/entry-client.tsx') }`);
91
137
  consola.consola.log(" }");
92
138
  consola.consola.log(" }");
93
139
  } else {
140
+ const serverConfig = integrationType === "separate" ? ` server: {
141
+ port: 5173,
142
+ strictPort: true,
143
+ hmr: { port: 5173 },
144
+ },
145
+ ` : ` server: {
146
+ middlewareMode: true,
147
+ },
148
+ `;
94
149
  const viteConfig = `import { defineConfig } from 'vite';
95
150
  import react from '@vitejs/plugin-react';
96
151
  import { resolve } from 'path';
@@ -102,12 +157,7 @@ export default defineConfig({
102
157
  '@': resolve(__dirname, 'src'),
103
158
  },
104
159
  },
105
- server: {
106
- port: 5173,
107
- strictPort: true,
108
- hmr: { port: 5173 },
109
- },
110
- build: {
160
+ ${serverConfig}build: {
111
161
  outDir: 'dist/client',
112
162
  manifest: true,
113
163
  rollupOptions: {
@@ -249,6 +299,16 @@ export default defineConfig({
249
299
  packageJson.scripts["build:server"] = `vite build --ssr ${viewsDir}/entry-server.tsx --outDir dist/server`;
250
300
  shouldUpdate = true;
251
301
  }
302
+ if (integrationType === "separate") {
303
+ if (!packageJson.scripts["dev:client"]) {
304
+ packageJson.scripts["dev:client"] = "vite";
305
+ shouldUpdate = true;
306
+ }
307
+ if (!packageJson.scripts["dev:server"]) {
308
+ packageJson.scripts["dev:server"] = "nest start --watch";
309
+ shouldUpdate = true;
310
+ }
311
+ }
252
312
  const existingBuild = packageJson.scripts.build;
253
313
  const recommendedBuild = "pnpm build:client && pnpm build:server && nest build";
254
314
  if (!existingBuild) {
@@ -269,9 +329,9 @@ export default defineConfig({
269
329
  if (!args["skip-install"]) {
270
330
  consola.consola.start("Checking dependencies...");
271
331
  const requiredDeps = {
272
- "react": "^19.0.0",
332
+ react: "^19.0.0",
273
333
  "react-dom": "^19.0.0",
274
- "vite": "^7.0.0",
334
+ vite: "^7.0.0",
275
335
  "@vitejs/plugin-react": "^4.0.0"
276
336
  };
277
337
  const missingDeps = [];
@@ -311,9 +371,26 @@ export default defineConfig({
311
371
  }
312
372
  consola.consola.success("\nInitialization complete!");
313
373
  consola.consola.box("Next steps");
314
- consola.consola.info(`1. Create your first view component in ${viewsDir}/`);
315
- consola.consola.info("2. Render it from a NestJS controller using render.render()");
316
- consola.consola.info("3. Run your dev server with: pnpm start:dev");
374
+ consola.consola.info("1. Register RenderModule in your app.module.ts:");
375
+ consola.consola.log(' import { RenderModule } from "@nestjs-ssr/react";');
376
+ consola.consola.log(" @Module({");
377
+ consola.consola.log(" imports: [RenderModule.forRoot()],");
378
+ consola.consola.log(" })");
379
+ consola.consola.info(`
380
+ 2. Create your first view component in ${viewsDir}/`);
381
+ consola.consola.info("3. Add a controller method with the @Render decorator:");
382
+ consola.consola.log(' import { Render } from "@nestjs-ssr/react";');
383
+ consola.consola.log(" @Get()");
384
+ consola.consola.log(' @Render("YourComponent")');
385
+ consola.consola.log(' home() { return { props: { message: "Hello" } }; }');
386
+ if (integrationType === "separate") {
387
+ consola.consola.info("\n4. Start both servers:");
388
+ consola.consola.log(" Terminal 1: pnpm dev:client (Vite on port 5173)");
389
+ consola.consola.log(" Terminal 2: pnpm dev:server (NestJS)");
390
+ } else {
391
+ consola.consola.info("\n4. Start the dev server: pnpm start:dev");
392
+ consola.consola.info(" (Vite middleware will be integrated into NestJS)");
393
+ }
317
394
  }
318
395
  });
319
396
  citty.runMain(main);
package/dist/cli/init.mjs CHANGED
@@ -29,6 +29,10 @@ var main = defineCommand({
29
29
  type: "boolean",
30
30
  description: "Skip automatic dependency installation",
31
31
  default: false
32
+ },
33
+ integration: {
34
+ type: "string",
35
+ description: 'Integration type: "separate" (Vite as separate server) or "integrated" (Vite bundled with NestJS)'
32
36
  }
33
37
  },
34
38
  async run({ args }) {
@@ -36,6 +40,32 @@ var main = defineCommand({
36
40
  const viewsDir = args.views;
37
41
  consola.box("@nestjs-ssr/react initialization");
38
42
  consola.start("Setting up your NestJS SSR React project...\n");
43
+ let integrationType = args.integration;
44
+ if (!integrationType) {
45
+ const response = await consola.prompt("How do you want to run Vite during development?", {
46
+ type: "select",
47
+ options: [
48
+ {
49
+ label: "Separate server (Vite runs on its own port, e.g., 5173)",
50
+ value: "separate"
51
+ },
52
+ {
53
+ label: "Integrated with NestJS (Vite middleware runs within NestJS)",
54
+ value: "integrated"
55
+ }
56
+ ]
57
+ });
58
+ integrationType = response;
59
+ }
60
+ if (![
61
+ "separate",
62
+ "integrated"
63
+ ].includes(integrationType)) {
64
+ consola.error(`Invalid integration type: "${integrationType}". Must be "separate" or "integrated"`);
65
+ process.exit(1);
66
+ }
67
+ consola.info(`Using ${integrationType === "separate" ? "separate server" : "integrated"} mode
68
+ `);
39
69
  const templateLocations = [
40
70
  resolve(__dirname$1, "../../src/templates"),
41
71
  resolve(__dirname$1, "../templates")
@@ -73,6 +103,15 @@ var main = defineCommand({
73
103
  copyFileSync(entryServerSrc, entryServerDest);
74
104
  consola.success(`Created ${viewsDir}/entry-server.tsx`);
75
105
  }
106
+ consola.start("Creating index.html...");
107
+ const indexHtmlSrc = join(templateDir, "index.html");
108
+ const indexHtmlDest = join(cwd, viewsDir, "index.html");
109
+ if (existsSync(indexHtmlDest) && !args.force) {
110
+ consola.warn(`${viewsDir}/index.html already exists (use --force to overwrite)`);
111
+ } else {
112
+ copyFileSync(indexHtmlSrc, indexHtmlDest);
113
+ consola.success(`Created ${viewsDir}/index.html`);
114
+ }
76
115
  consola.start("Configuring vite.config.js...");
77
116
  const viteConfigPath = join(cwd, "vite.config.js");
78
117
  const viteConfigTs = join(cwd, "vite.config.ts");
@@ -82,12 +121,28 @@ var main = defineCommand({
82
121
  consola.warn(`${useTypeScript ? "vite.config.ts" : "vite.config.js"} already exists`);
83
122
  consola.info("Please manually add to your Vite config:");
84
123
  consola.log(" import { resolve } from 'path';");
124
+ if (integrationType === "separate") {
125
+ consola.log(" server: {");
126
+ consola.log(" port: 5173,");
127
+ consola.log(" strictPort: true,");
128
+ consola.log(" hmr: { port: 5173 },");
129
+ consola.log(" },");
130
+ }
85
131
  consola.log(" build: {");
86
132
  consola.log(" rollupOptions: {");
87
133
  consola.log(` input: { client: resolve(__dirname, '${viewsDir}/entry-client.tsx') }`);
88
134
  consola.log(" }");
89
135
  consola.log(" }");
90
136
  } else {
137
+ const serverConfig = integrationType === "separate" ? ` server: {
138
+ port: 5173,
139
+ strictPort: true,
140
+ hmr: { port: 5173 },
141
+ },
142
+ ` : ` server: {
143
+ middlewareMode: true,
144
+ },
145
+ `;
91
146
  const viteConfig = `import { defineConfig } from 'vite';
92
147
  import react from '@vitejs/plugin-react';
93
148
  import { resolve } from 'path';
@@ -99,12 +154,7 @@ export default defineConfig({
99
154
  '@': resolve(__dirname, 'src'),
100
155
  },
101
156
  },
102
- server: {
103
- port: 5173,
104
- strictPort: true,
105
- hmr: { port: 5173 },
106
- },
107
- build: {
157
+ ${serverConfig}build: {
108
158
  outDir: 'dist/client',
109
159
  manifest: true,
110
160
  rollupOptions: {
@@ -246,6 +296,16 @@ export default defineConfig({
246
296
  packageJson.scripts["build:server"] = `vite build --ssr ${viewsDir}/entry-server.tsx --outDir dist/server`;
247
297
  shouldUpdate = true;
248
298
  }
299
+ if (integrationType === "separate") {
300
+ if (!packageJson.scripts["dev:client"]) {
301
+ packageJson.scripts["dev:client"] = "vite";
302
+ shouldUpdate = true;
303
+ }
304
+ if (!packageJson.scripts["dev:server"]) {
305
+ packageJson.scripts["dev:server"] = "nest start --watch";
306
+ shouldUpdate = true;
307
+ }
308
+ }
249
309
  const existingBuild = packageJson.scripts.build;
250
310
  const recommendedBuild = "pnpm build:client && pnpm build:server && nest build";
251
311
  if (!existingBuild) {
@@ -266,9 +326,9 @@ export default defineConfig({
266
326
  if (!args["skip-install"]) {
267
327
  consola.start("Checking dependencies...");
268
328
  const requiredDeps = {
269
- "react": "^19.0.0",
329
+ react: "^19.0.0",
270
330
  "react-dom": "^19.0.0",
271
- "vite": "^7.0.0",
331
+ vite: "^7.0.0",
272
332
  "@vitejs/plugin-react": "^4.0.0"
273
333
  };
274
334
  const missingDeps = [];
@@ -308,9 +368,26 @@ export default defineConfig({
308
368
  }
309
369
  consola.success("\nInitialization complete!");
310
370
  consola.box("Next steps");
311
- consola.info(`1. Create your first view component in ${viewsDir}/`);
312
- consola.info("2. Render it from a NestJS controller using render.render()");
313
- consola.info("3. Run your dev server with: pnpm start:dev");
371
+ consola.info("1. Register RenderModule in your app.module.ts:");
372
+ consola.log(' import { RenderModule } from "@nestjs-ssr/react";');
373
+ consola.log(" @Module({");
374
+ consola.log(" imports: [RenderModule.forRoot()],");
375
+ consola.log(" })");
376
+ consola.info(`
377
+ 2. Create your first view component in ${viewsDir}/`);
378
+ consola.info("3. Add a controller method with the @Render decorator:");
379
+ consola.log(' import { Render } from "@nestjs-ssr/react";');
380
+ consola.log(" @Get()");
381
+ consola.log(' @Render("YourComponent")');
382
+ consola.log(' home() { return { props: { message: "Hello" } }; }');
383
+ if (integrationType === "separate") {
384
+ consola.info("\n4. Start both servers:");
385
+ consola.log(" Terminal 1: pnpm dev:client (Vite on port 5173)");
386
+ consola.log(" Terminal 2: pnpm dev:server (NestJS)");
387
+ } else {
388
+ consola.info("\n4. Start the dev server: pnpm start:dev");
389
+ consola.info(" (Vite middleware will be integrated into NestJS)");
390
+ }
314
391
  }
315
392
  });
316
393
  runMain(main);