@nestjs-ssr/react 0.2.1 → 0.2.2

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
@@ -27,7 +27,7 @@ Clean Architecture: layers separated, dependencies inward, business logic framew
27
27
  ## Quick Start
28
28
 
29
29
  ```bash
30
- npx nestjs-ssr init
30
+ npx @nestjs-ssr/react init
31
31
  ```
32
32
 
33
33
  ```typescript
package/dist/cli/init.js CHANGED
@@ -41,8 +41,45 @@ var main = citty.defineCommand({
41
41
  async run({ args }) {
42
42
  const cwd = process.cwd();
43
43
  const viewsDir = args.views;
44
+ const packageJsonPath = path.join(cwd, "package.json");
45
+ const tsconfigPath = path.join(cwd, "tsconfig.json");
44
46
  consola.consola.box("@nestjs-ssr/react initialization");
45
47
  consola.consola.start("Setting up your NestJS SSR React project...\n");
48
+ if (!fs.existsSync(packageJsonPath)) {
49
+ consola.consola.error("No package.json found in current directory");
50
+ consola.consola.info("Please run this command from your NestJS project root");
51
+ process.exit(1);
52
+ }
53
+ try {
54
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
55
+ const allDeps = {
56
+ ...packageJson.dependencies,
57
+ ...packageJson.devDependencies
58
+ };
59
+ const requiredNestDeps = [
60
+ "@nestjs/core",
61
+ "@nestjs/common"
62
+ ];
63
+ const missingNestDeps = requiredNestDeps.filter((dep) => !allDeps[dep]);
64
+ if (missingNestDeps.length > 0) {
65
+ consola.consola.error("This does not appear to be a NestJS project. Missing packages:");
66
+ consola.consola.info(` ${missingNestDeps.join(", ")}`);
67
+ consola.consola.info("\nPlease install NestJS first:");
68
+ consola.consola.info(" npm install @nestjs/core @nestjs/common @nestjs/platform-express");
69
+ consola.consola.info("\nOr create a new NestJS project:");
70
+ consola.consola.info(" npm i -g @nestjs/cli");
71
+ consola.consola.info(" nest new my-project");
72
+ process.exit(1);
73
+ }
74
+ const mainTsPath2 = path.join(cwd, "src/main.ts");
75
+ if (!fs.existsSync(mainTsPath2)) {
76
+ consola.consola.warn("No src/main.ts file found");
77
+ consola.consola.info("Make sure your NestJS application has a main entry point");
78
+ }
79
+ } catch (error) {
80
+ consola.consola.error("Failed to validate package.json:", error);
81
+ process.exit(1);
82
+ }
46
83
  let integrationType = args.integration;
47
84
  if (!integrationType) {
48
85
  const response = await consola.consola.prompt("How do you want to run Vite during development?", {
@@ -79,7 +116,6 @@ var main = citty.defineCommand({
79
116
  consola.consola.info("Searched:", templateLocations);
80
117
  process.exit(1);
81
118
  }
82
- const tsconfigPath = path.join(cwd, "tsconfig.json");
83
119
  if (!fs.existsSync(tsconfigPath)) {
84
120
  consola.consola.error("No tsconfig.json found in project root");
85
121
  consola.consola.info("Please create a tsconfig.json file first");
@@ -115,13 +151,12 @@ var main = citty.defineCommand({
115
151
  fs.copyFileSync(indexHtmlSrc, indexHtmlDest);
116
152
  consola.consola.success(`Created ${viewsDir}/index.html`);
117
153
  }
118
- consola.consola.start("Configuring vite.config.js...");
119
- const viteConfigPath = path.join(cwd, "vite.config.js");
154
+ consola.consola.start("Configuring vite.config.ts...");
120
155
  const viteConfigTs = path.join(cwd, "vite.config.ts");
121
- const useTypeScript = fs.existsSync(viteConfigTs);
122
- const configPath = useTypeScript ? viteConfigTs : viteConfigPath;
123
- if (fs.existsSync(configPath)) {
124
- consola.consola.warn(`${useTypeScript ? "vite.config.ts" : "vite.config.js"} already exists`);
156
+ const viteConfigJs = path.join(cwd, "vite.config.js");
157
+ const existingConfig = fs.existsSync(viteConfigTs) || fs.existsSync(viteConfigJs);
158
+ if (existingConfig) {
159
+ consola.consola.warn("vite.config already exists");
125
160
  consola.consola.info("Please manually add to your Vite config:");
126
161
  consola.consola.log(" import { resolve } from 'path';");
127
162
  if (integrationType === "separate") {
@@ -133,7 +168,7 @@ var main = citty.defineCommand({
133
168
  }
134
169
  consola.consola.log(" build: {");
135
170
  consola.consola.log(" rollupOptions: {");
136
- consola.consola.log(` input: { client: resolve(__dirname, '${viewsDir}/entry-client.tsx') }`);
171
+ consola.consola.log(` input: { client: resolve(process.cwd(), '${viewsDir}/entry-client.tsx') }`);
137
172
  consola.consola.log(" }");
138
173
  consola.consola.log(" }");
139
174
  } else {
@@ -154,7 +189,7 @@ export default defineConfig({
154
189
  plugins: [react()],
155
190
  resolve: {
156
191
  alias: {
157
- '@': resolve(__dirname, 'src'),
192
+ '@': resolve(process.cwd(), 'src'),
158
193
  },
159
194
  },
160
195
  ${serverConfig}build: {
@@ -162,14 +197,26 @@ ${serverConfig}build: {
162
197
  manifest: true,
163
198
  rollupOptions: {
164
199
  input: {
165
- client: resolve(__dirname, '${viewsDir}/entry-client.tsx'),
200
+ client: resolve(process.cwd(), '${viewsDir}/entry-client.tsx'),
201
+ },
202
+ // Externalize optional native dependencies that shouldn't be bundled for browser
203
+ external: (id: string) => {
204
+ // Externalize fsevents - an optional macOS dependency
205
+ if (id.includes('/fsevents') || id.endsWith('fsevents')) {
206
+ return true;
207
+ }
208
+ // Externalize .node native addon files
209
+ if (id.endsWith('.node')) {
210
+ return true;
211
+ }
212
+ return false;
166
213
  },
167
214
  },
168
215
  },
169
216
  });
170
217
  `;
171
- fs.writeFileSync(viteConfigPath, viteConfig);
172
- consola.consola.success("Created vite.config.js");
218
+ fs.writeFileSync(viteConfigTs, viteConfig);
219
+ consola.consola.success("Created vite.config.ts");
173
220
  }
174
221
  consola.consola.start("Configuring tsconfig.json...");
175
222
  try {
@@ -191,14 +238,13 @@ ${serverConfig}build: {
191
238
  ];
192
239
  updated = true;
193
240
  }
194
- if (!tsconfig.include) {
195
- tsconfig.include = [];
196
- }
197
- const hasTsx = tsconfig.include.some((pattern) => pattern.includes("**/*.tsx"));
198
- if (!hasTsx) {
199
- if (!tsconfig.include.includes("src/**/*.tsx")) {
200
- tsconfig.include.push("src/**/*.tsx");
201
- updated = true;
241
+ if (tsconfig.include && tsconfig.include.length > 0) {
242
+ const hasTsx = tsconfig.include.some((pattern) => pattern.includes("**/*.tsx"));
243
+ if (!hasTsx) {
244
+ if (!tsconfig.include.includes("src/**/*.tsx")) {
245
+ tsconfig.include.push("src/**/*.tsx");
246
+ updated = true;
247
+ }
202
248
  }
203
249
  }
204
250
  if (!tsconfig.exclude) {
@@ -280,115 +326,240 @@ ${serverConfig}build: {
280
326
  } catch (error) {
281
327
  consola.consola.error("Failed to update nest-cli.json:", error);
282
328
  }
283
- consola.consola.start("Configuring build scripts...");
284
- const packageJsonPath = path.join(cwd, "package.json");
285
- if (!fs.existsSync(packageJsonPath)) {
286
- consola.consola.warn("No package.json found, skipping build script setup");
287
- } else {
288
- try {
289
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
290
- if (!packageJson.scripts) {
291
- packageJson.scripts = {};
329
+ consola.consola.start("Configuring main.ts...");
330
+ const mainTsPath = path.join(cwd, "src/main.ts");
331
+ try {
332
+ if (fs.existsSync(mainTsPath)) {
333
+ let mainTs = fs.readFileSync(mainTsPath, "utf-8");
334
+ if (mainTs.includes("enableShutdownHooks")) {
335
+ consola.consola.info("main.ts already has enableShutdownHooks()");
336
+ } else {
337
+ const createPattern = /(const\s+app\s*=\s*await\s+NestFactory\.create[^;]+;)/;
338
+ const match = mainTs.match(createPattern);
339
+ if (match) {
340
+ const createLine = match[1];
341
+ const replacement = `${createLine}
342
+
343
+ // Enable graceful shutdown for proper Vite cleanup
344
+ app.enableShutdownHooks();`;
345
+ mainTs = mainTs.replace(createLine, replacement);
346
+ fs.writeFileSync(mainTsPath, mainTs);
347
+ consola.consola.success("Added enableShutdownHooks() to main.ts");
348
+ } else {
349
+ consola.consola.warn("Could not find NestFactory.create in main.ts, please add manually:");
350
+ consola.consola.log(" app.enableShutdownHooks();");
351
+ }
292
352
  }
293
- let shouldUpdate = false;
294
- if (!packageJson.scripts["build:client"]) {
295
- packageJson.scripts["build:client"] = "vite build --ssrManifest --outDir dist/client";
296
- shouldUpdate = true;
353
+ }
354
+ } catch (error) {
355
+ consola.consola.warn("Failed to update main.ts:", error);
356
+ consola.consola.info("Please manually add to your main.ts after NestFactory.create():");
357
+ consola.consola.log(" app.enableShutdownHooks();");
358
+ }
359
+ consola.consola.start("Configuring app.module.ts...");
360
+ const appModulePath = path.join(cwd, "src/app.module.ts");
361
+ try {
362
+ if (fs.existsSync(appModulePath)) {
363
+ let appModule = fs.readFileSync(appModulePath, "utf-8");
364
+ if (appModule.includes("RenderModule")) {
365
+ consola.consola.info("app.module.ts already has RenderModule");
366
+ } else {
367
+ let updated = false;
368
+ const importStatement = "import { RenderModule } from '@nestjs-ssr/react';";
369
+ if (!appModule.includes(importStatement)) {
370
+ const nestImportPattern = /(import\s+.*from\s+['"]@nestjs\/[^'"]+['"];?\n)/g;
371
+ const matches = [
372
+ ...appModule.matchAll(nestImportPattern)
373
+ ];
374
+ if (matches.length > 0) {
375
+ const lastMatch = matches[matches.length - 1];
376
+ const insertPos = lastMatch.index + lastMatch[0].length;
377
+ appModule = appModule.slice(0, insertPos) + importStatement + "\n" + appModule.slice(insertPos);
378
+ updated = true;
379
+ } else {
380
+ const anyImportPattern = /(import\s+.*from\s+['"][^'"]+['"];?\n)/g;
381
+ const anyMatches = [
382
+ ...appModule.matchAll(anyImportPattern)
383
+ ];
384
+ if (anyMatches.length > 0) {
385
+ const lastMatch = anyMatches[anyMatches.length - 1];
386
+ const insertPos = lastMatch.index + lastMatch[0].length;
387
+ appModule = appModule.slice(0, insertPos) + importStatement + "\n" + appModule.slice(insertPos);
388
+ updated = true;
389
+ }
390
+ }
391
+ }
392
+ const importsPattern = /(imports:\s*\[)([^\]]*)/;
393
+ const importsMatch = appModule.match(importsPattern);
394
+ if (importsMatch) {
395
+ const existingImports = importsMatch[2].trim();
396
+ const renderModuleConfig = integrationType === "separate" ? "RenderModule.register({ vite: { mode: 'proxy', port: 5173 } })" : "RenderModule.forRoot()";
397
+ if (existingImports === "") {
398
+ appModule = appModule.replace(importsPattern, `$1${renderModuleConfig}`);
399
+ } else {
400
+ appModule = appModule.replace(importsPattern, `$1$2, ${renderModuleConfig}`);
401
+ }
402
+ updated = true;
403
+ }
404
+ if (updated) {
405
+ fs.writeFileSync(appModulePath, appModule);
406
+ consola.consola.success("Added RenderModule to app.module.ts");
407
+ } else {
408
+ consola.consola.warn("Could not automatically update app.module.ts, please add manually:");
409
+ consola.consola.log(` import { RenderModule } from '@nestjs-ssr/react';`);
410
+ consola.consola.log(" // In @Module imports:");
411
+ consola.consola.log(" RenderModule.forRoot()");
412
+ }
297
413
  }
298
- if (!packageJson.scripts["build:server"]) {
299
- packageJson.scripts["build:server"] = `vite build --ssr ${viewsDir}/entry-server.tsx --outDir dist/server`;
414
+ } else {
415
+ consola.consola.warn("No src/app.module.ts found");
416
+ consola.consola.info("Please manually add RenderModule to your app module");
417
+ }
418
+ } catch (error) {
419
+ consola.consola.warn("Failed to update app.module.ts:", error);
420
+ consola.consola.info("Please manually add to your app.module.ts:");
421
+ consola.consola.log(` import { RenderModule } from '@nestjs-ssr/react';`);
422
+ consola.consola.log(" // In @Module imports:");
423
+ consola.consola.log(" RenderModule.forRoot()");
424
+ }
425
+ consola.consola.start("Configuring build scripts...");
426
+ try {
427
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
428
+ if (!packageJson.scripts) {
429
+ packageJson.scripts = {};
430
+ }
431
+ let shouldUpdate = false;
432
+ if (!packageJson.scripts["build:client"]) {
433
+ packageJson.scripts["build:client"] = `vite build --ssrManifest --outDir dist/client && cp ${viewsDir}/index.html dist/client/index.html`;
434
+ shouldUpdate = true;
435
+ }
436
+ if (!packageJson.scripts["build:server"]) {
437
+ packageJson.scripts["build:server"] = `vite build --ssr ${viewsDir}/entry-server.tsx --outDir dist/server`;
438
+ shouldUpdate = true;
439
+ }
440
+ if (integrationType === "separate") {
441
+ if (!packageJson.scripts["dev:vite"]) {
442
+ packageJson.scripts["dev:vite"] = "vite --port 5173";
300
443
  shouldUpdate = true;
301
444
  }
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
- }
445
+ if (!packageJson.scripts["dev:nest"]) {
446
+ packageJson.scripts["dev:nest"] = "nest start --watch --watchAssets --preserveWatchOutput";
447
+ shouldUpdate = true;
311
448
  }
312
- const existingBuild = packageJson.scripts.build;
313
- const recommendedBuild = "pnpm build:client && pnpm build:server && nest build";
314
- if (!existingBuild) {
449
+ packageJson.scripts["start:dev"] = 'concurrently --raw -n vite,nest -c cyan,green "pnpm:dev:vite" "pnpm:dev:nest"';
450
+ shouldUpdate = true;
451
+ }
452
+ const existingBuild = packageJson.scripts.build;
453
+ const recommendedBuild = "nest build && pnpm build:client && pnpm build:server";
454
+ if (!existingBuild) {
455
+ packageJson.scripts.build = recommendedBuild;
456
+ shouldUpdate = true;
457
+ consola.consola.success("Created build script");
458
+ } else if (existingBuild !== recommendedBuild) {
459
+ if (!existingBuild.includes("build:client") || !existingBuild.includes("build:server")) {
460
+ consola.consola.warn(`Found existing build script: "${existingBuild}"`);
461
+ consola.consola.info(`Updating to: ${recommendedBuild}`);
315
462
  packageJson.scripts.build = recommendedBuild;
316
463
  shouldUpdate = true;
317
- } else if (!existingBuild.includes("build:client") || !existingBuild.includes("build:server")) {
318
- consola.consola.warn(`Found existing build script: "${existingBuild}"`);
319
- consola.consola.info("SSR requires building client and server bundles");
320
- consola.consola.info(`Recommended: ${recommendedBuild}`);
321
- consola.consola.info("Please manually update your build script in package.json");
322
464
  } else {
323
465
  consola.consola.info("Build scripts already configured");
324
466
  }
325
- if (shouldUpdate) {
326
- fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
327
- consola.consola.success("Updated build scripts in package.json");
467
+ } else {
468
+ consola.consola.info("Build scripts already configured");
469
+ }
470
+ if (shouldUpdate) {
471
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
472
+ consola.consola.success("Updated build scripts in package.json");
473
+ }
474
+ if (!args["skip-install"]) {
475
+ consola.consola.start("Checking dependencies...");
476
+ const requiredDeps = {
477
+ react: "^19.0.0",
478
+ "react-dom": "^19.0.0",
479
+ vite: "^7.0.0",
480
+ "@vitejs/plugin-react": "^4.0.0"
481
+ };
482
+ const requiredDevDeps = {
483
+ "@types/react": "^19.0.0",
484
+ "@types/react-dom": "^19.0.0"
485
+ };
486
+ if (integrationType === "separate") {
487
+ requiredDeps["http-proxy-middleware"] = "^3.0.0";
488
+ requiredDevDeps["concurrently"] = "^9.0.0";
328
489
  }
329
- if (!args["skip-install"]) {
330
- consola.consola.start("Checking dependencies...");
331
- const requiredDeps = {
332
- react: "^19.0.0",
333
- "react-dom": "^19.0.0",
334
- vite: "^7.0.0",
335
- "@vitejs/plugin-react": "^4.0.0"
336
- };
337
- const missingDeps = [];
338
- const allDeps = {
339
- ...packageJson.dependencies,
340
- ...packageJson.devDependencies
341
- };
342
- for (const [dep, version] of Object.entries(requiredDeps)) {
343
- if (!allDeps[dep]) {
344
- missingDeps.push(`${dep}@${version}`);
345
- }
490
+ const missingDeps = [];
491
+ const missingDevDeps = [];
492
+ const allDeps = {
493
+ ...packageJson.dependencies,
494
+ ...packageJson.devDependencies
495
+ };
496
+ for (const [dep, version] of Object.entries(requiredDeps)) {
497
+ if (!allDeps[dep]) {
498
+ missingDeps.push(`${dep}@${version}`);
346
499
  }
347
- if (missingDeps.length > 0) {
348
- consola.consola.info(`Missing dependencies: ${missingDeps.join(", ")}`);
349
- let packageManager = "npm";
350
- if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) packageManager = "pnpm";
351
- else if (fs.existsSync(path.join(cwd, "yarn.lock"))) packageManager = "yarn";
352
- const installCmd = packageManager === "npm" ? `npm install ${missingDeps.join(" ")}` : `${packageManager} add ${missingDeps.join(" ")}`;
353
- try {
354
- consola.consola.start(`Installing dependencies with ${packageManager}...`);
355
- child_process.execSync(installCmd, {
356
- cwd,
357
- stdio: "inherit"
358
- });
359
- consola.consola.success("Dependencies installed!");
360
- } catch (error) {
361
- consola.consola.error("Failed to install dependencies:", error);
362
- consola.consola.info(`Please manually run: ${installCmd}`);
363
- }
364
- } else {
365
- consola.consola.success("All required dependencies are already installed");
500
+ }
501
+ for (const [dep, version] of Object.entries(requiredDevDeps)) {
502
+ if (!allDeps[dep]) {
503
+ missingDevDeps.push(`${dep}@${version}`);
504
+ }
505
+ }
506
+ let packageManager = "npm";
507
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) packageManager = "pnpm";
508
+ else if (fs.existsSync(path.join(cwd, "yarn.lock"))) packageManager = "yarn";
509
+ if (missingDeps.length > 0) {
510
+ consola.consola.info(`Missing dependencies: ${missingDeps.join(", ")}`);
511
+ const installCmd = packageManager === "npm" ? `npm install ${missingDeps.join(" ")}` : `${packageManager} add ${missingDeps.join(" ")}`;
512
+ try {
513
+ consola.consola.start(`Installing dependencies with ${packageManager}...`);
514
+ child_process.execSync(installCmd, {
515
+ cwd,
516
+ stdio: "inherit"
517
+ });
518
+ consola.consola.success("Dependencies installed!");
519
+ } catch (error) {
520
+ consola.consola.error("Failed to install dependencies:", error);
521
+ consola.consola.info(`Please manually run: ${installCmd}`);
366
522
  }
367
523
  }
368
- } catch (error) {
369
- consola.consola.error("Failed to update package.json:", error);
524
+ if (missingDevDeps.length > 0) {
525
+ consola.consola.info(`Missing dev dependencies: ${missingDevDeps.join(", ")}`);
526
+ const installDevCmd = packageManager === "npm" ? `npm install -D ${missingDevDeps.join(" ")}` : `${packageManager} add -D ${missingDevDeps.join(" ")}`;
527
+ try {
528
+ consola.consola.start(`Installing dev dependencies with ${packageManager}...`);
529
+ child_process.execSync(installDevCmd, {
530
+ cwd,
531
+ stdio: "inherit"
532
+ });
533
+ consola.consola.success("Dev dependencies installed!");
534
+ } catch (error) {
535
+ consola.consola.error("Failed to install dev dependencies:", error);
536
+ consola.consola.info(`Please manually run: ${installDevCmd}`);
537
+ }
538
+ }
539
+ if (missingDeps.length === 0 && missingDevDeps.length === 0) {
540
+ consola.consola.success("All required dependencies are already installed");
541
+ }
370
542
  }
543
+ } catch (error) {
544
+ consola.consola.error("Failed to update package.json:", error);
371
545
  }
372
546
  consola.consola.success("\nInitialization complete!");
373
547
  consola.consola.box("Next steps");
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:");
548
+ consola.consola.info(`1. Create your first view component in ${viewsDir}/`);
549
+ consola.consola.info("2. Add a controller method with the @Render decorator:");
382
550
  consola.consola.log(' import { Render } from "@nestjs-ssr/react";');
383
551
  consola.consola.log(" @Get()");
384
- consola.consola.log(' @Render("YourComponent")');
385
- consola.consola.log(' home() { return { props: { message: "Hello" } }; }');
552
+ consola.consola.log(" @Render(Home)");
553
+ consola.consola.log(' home() { return { message: "Hello" }; }');
386
554
  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)");
555
+ consola.consola.info("\n3. Start development with HMR:");
556
+ consola.consola.log(" pnpm start:dev");
557
+ consola.consola.info(" This runs both Vite (port 5173) and NestJS concurrently");
558
+ consola.consola.info("\n Or run them separately:");
559
+ consola.consola.log(" Terminal 1: pnpm dev:vite");
560
+ consola.consola.log(" Terminal 2: pnpm dev:nest");
390
561
  } else {
391
- consola.consola.info("\n4. Start the dev server: pnpm start:dev");
562
+ consola.consola.info("\n3. Start the dev server: pnpm start:dev");
392
563
  consola.consola.info(" (Vite middleware will be integrated into NestJS)");
393
564
  }
394
565
  }