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