@nestjs-ssr/react 0.2.0 → 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.
@@ -5,7 +5,7 @@ import { join, relative } from 'path';
5
5
  import { uneval } from 'devalue';
6
6
  import escapeHtml from 'escape-html';
7
7
  import { renderToStaticMarkup } from 'react-dom/server';
8
- import { createElement } from 'react';
8
+ import React, { createElement } from 'react';
9
9
  import { switchMap } from 'rxjs/operators';
10
10
 
11
11
  var __defProp = Object.defineProperty;
@@ -185,8 +185,6 @@ window.__LAYOUTS__ = ${uneval(layoutMetadata)};
185
185
  TemplateParserService = _ts_decorate([
186
186
  Injectable()
187
187
  ], TemplateParserService);
188
-
189
- // src/render/error-pages/error-page-development.tsx
190
188
  function ErrorPageDevelopment({ error, viewPath, phase }) {
191
189
  const stackLines = error.stack ? error.stack.split("\n").slice(1) : [];
192
190
  return /* @__PURE__ */ React.createElement("html", {
@@ -260,8 +258,6 @@ function ErrorPageDevelopment({ error, viewPath, phase }) {
260
258
  }, /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "View Path:"), " ", viewPath), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "Error Phase:"), " ", phase === "shell" ? "Shell (before streaming started)" : "Streaming (during content delivery)"), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("strong", null, "Environment:"), " Development")))));
261
259
  }
262
260
  __name(ErrorPageDevelopment, "ErrorPageDevelopment");
263
-
264
- // src/render/error-pages/error-page-production.tsx
265
261
  function ErrorPageProduction() {
266
262
  return /* @__PURE__ */ React.createElement("html", {
267
263
  lang: "en"
@@ -671,60 +667,79 @@ var RenderService = class _RenderService {
671
667
  async renderToStream(viewComponent, data = {}, res, head) {
672
668
  const startTime = Date.now();
673
669
  let shellReadyTime = 0;
674
- try {
675
- let template = this.template;
676
- if (this.vite) {
677
- template = await this.vite.transformIndexHtml("/", template);
678
- }
679
- const templateParts = this.templateParser.parseTemplate(template);
680
- let renderModule;
681
- if (this.vite) {
682
- renderModule = await this.vite.ssrLoadModule(this.entryServerPath);
683
- } else {
684
- if (this.serverManifest) {
685
- const manifestEntry = Object.entries(this.serverManifest).find(([key, value]) => value.isEntry && key.includes("entry-server"));
686
- if (manifestEntry) {
687
- const [, entry] = manifestEntry;
688
- const serverPath = join(process.cwd(), "dist/server", entry.file);
689
- renderModule = await import(serverPath);
670
+ return new Promise((resolve, reject) => {
671
+ const executeStream = /* @__PURE__ */ __name(async () => {
672
+ let template = this.template;
673
+ if (this.vite) {
674
+ template = await this.vite.transformIndexHtml("/", template);
675
+ }
676
+ const templateParts = this.templateParser.parseTemplate(template);
677
+ let renderModule;
678
+ if (this.vite) {
679
+ renderModule = await this.vite.ssrLoadModule(this.entryServerPath);
680
+ } else {
681
+ if (this.serverManifest) {
682
+ const manifestEntry = Object.entries(this.serverManifest).find(([key, value]) => value.isEntry && key.includes("entry-server"));
683
+ if (manifestEntry) {
684
+ const [, entry] = manifestEntry;
685
+ const serverPath = join(process.cwd(), "dist/server", entry.file);
686
+ renderModule = await import(serverPath);
687
+ } else {
688
+ throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
689
+ }
690
690
  } else {
691
691
  throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
692
692
  }
693
- } else {
694
- throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
695
693
  }
696
- }
697
- const { data: pageData, __context: context, __layouts: layouts } = data;
698
- const componentName = viewComponent.displayName || viewComponent.name || "Component";
699
- const inlineScripts = this.templateParser.buildInlineScripts(pageData, context, componentName, layouts);
700
- const clientScript = this.templateParser.getClientScriptTag(this.isDevelopment, this.manifest);
701
- const stylesheetTags = this.templateParser.getStylesheetTags(this.isDevelopment, this.manifest);
702
- const headTags = this.templateParser.buildHeadTags(head);
703
- let didError = false;
704
- const { pipe, abort } = renderModule.renderComponentStream(viewComponent, data, {
705
- onShellReady: /* @__PURE__ */ __name(() => {
706
- shellReadyTime = Date.now();
707
- res.statusCode = didError ? 500 : 200;
708
- res.setHeader("Content-Type", "text/html; charset=utf-8");
709
- let htmlStart = templateParts.htmlStart;
710
- htmlStart = htmlStart.replace("<!--styles-->", stylesheetTags);
711
- htmlStart = htmlStart.replace("<!--head-meta-->", headTags);
712
- res.write(htmlStart);
713
- res.write(templateParts.rootStart);
714
- pipe(res);
715
- if (this.isDevelopment) {
716
- const ttfb = shellReadyTime - startTime;
717
- this.logger.log(`[SSR] ${componentName} shell ready in ${ttfb}ms (stream mode - TTFB)`);
694
+ const { data: pageData, __context: context, __layouts: layouts } = data;
695
+ const componentName = viewComponent.displayName || viewComponent.name || "Component";
696
+ const inlineScripts = this.templateParser.buildInlineScripts(pageData, context, componentName, layouts);
697
+ const clientScript = this.templateParser.getClientScriptTag(this.isDevelopment, this.manifest);
698
+ const stylesheetTags = this.templateParser.getStylesheetTags(this.isDevelopment, this.manifest);
699
+ const headTags = this.templateParser.buildHeadTags(head);
700
+ let didError = false;
701
+ let shellErrorOccurred = false;
702
+ const { PassThrough } = await import('stream');
703
+ const reactStream = new PassThrough();
704
+ let allReadyFired = false;
705
+ const { pipe, abort } = renderModule.renderComponentStream(viewComponent, data, {
706
+ onShellReady: /* @__PURE__ */ __name(() => {
707
+ shellReadyTime = Date.now();
708
+ if (!res.headersSent) {
709
+ res.statusCode = didError ? 500 : 200;
710
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
711
+ }
712
+ let htmlStart = templateParts.htmlStart;
713
+ htmlStart = htmlStart.replace("<!--styles-->", stylesheetTags);
714
+ htmlStart = htmlStart.replace("<!--head-meta-->", headTags);
715
+ res.write(htmlStart);
716
+ res.write(templateParts.rootStart);
717
+ pipe(reactStream);
718
+ reactStream.pipe(res, {
719
+ end: false
720
+ });
721
+ if (this.isDevelopment) {
722
+ const ttfb = shellReadyTime - startTime;
723
+ this.logger.log(`[SSR] ${componentName} shell ready in ${ttfb}ms (stream mode - TTFB)`);
724
+ }
725
+ }, "onShellReady"),
726
+ onShellError: /* @__PURE__ */ __name((error) => {
727
+ shellErrorOccurred = true;
728
+ this.streamingErrorHandler.handleShellError(error, res, componentName, this.isDevelopment);
729
+ resolve();
730
+ }, "onShellError"),
731
+ onError: /* @__PURE__ */ __name((error) => {
732
+ didError = true;
733
+ this.streamingErrorHandler.handleStreamError(error, componentName);
734
+ }, "onError"),
735
+ onAllReady: /* @__PURE__ */ __name(() => {
736
+ allReadyFired = true;
737
+ }, "onAllReady")
738
+ });
739
+ reactStream.on("end", () => {
740
+ if (shellErrorOccurred) {
741
+ return;
718
742
  }
719
- }, "onShellReady"),
720
- onShellError: /* @__PURE__ */ __name((error) => {
721
- this.streamingErrorHandler.handleShellError(error, res, componentName, this.isDevelopment);
722
- }, "onShellError"),
723
- onError: /* @__PURE__ */ __name((error) => {
724
- didError = true;
725
- this.streamingErrorHandler.handleStreamError(error, componentName);
726
- }, "onError"),
727
- onAllReady: /* @__PURE__ */ __name(() => {
728
743
  res.write(inlineScripts);
729
744
  res.write(clientScript);
730
745
  res.write(templateParts.rootEnd);
@@ -733,17 +748,25 @@ var RenderService = class _RenderService {
733
748
  if (this.isDevelopment) {
734
749
  const totalTime = Date.now() - startTime;
735
750
  const streamTime = Date.now() - shellReadyTime;
736
- this.logger.log(`[SSR] ${componentName} streaming complete in ${totalTime}ms total (${streamTime}ms streaming)`);
751
+ const viaAllReady = allReadyFired ? " (onAllReady fired)" : " (onAllReady never fired)";
752
+ this.logger.log(`[SSR] ${componentName} streaming complete in ${totalTime}ms total (${streamTime}ms streaming)${viaAllReady}`);
737
753
  }
738
- }, "onAllReady")
754
+ resolve();
755
+ });
756
+ reactStream.on("error", (error) => {
757
+ reject(error);
758
+ });
759
+ res.on("close", () => {
760
+ abort();
761
+ resolve();
762
+ });
763
+ }, "executeStream");
764
+ executeStream().catch((error) => {
765
+ const componentName = typeof viewComponent === "function" ? viewComponent.name : String(viewComponent);
766
+ this.streamingErrorHandler.handleShellError(error, res, componentName, this.isDevelopment);
767
+ resolve();
739
768
  });
740
- res.on("close", () => {
741
- abort();
742
- });
743
- } catch (error) {
744
- const componentName = typeof viewComponent === "function" ? viewComponent.name : String(viewComponent);
745
- this.streamingErrorHandler.handleShellError(error, res, componentName, this.isDevelopment);
746
- }
769
+ });
747
770
  }
748
771
  };
749
772
  RenderService = _ts_decorate3([
@@ -859,6 +882,9 @@ var RenderInterceptor = class {
859
882
  const httpContext = context.switchToHttp();
860
883
  const request = httpContext.getRequest();
861
884
  const response = httpContext.getResponse();
885
+ if (typeof data === "string") {
886
+ return data;
887
+ }
862
888
  const renderContext = {
863
889
  url: request.url,
864
890
  path: request.path,
@@ -949,11 +975,23 @@ var ViteInitializerService = class _ViteInitializerService {
949
975
  viteMode;
950
976
  vitePort;
951
977
  viteServer = null;
978
+ isShuttingDown = false;
952
979
  constructor(renderService, httpAdapterHost, viteConfig) {
953
980
  this.renderService = renderService;
954
981
  this.httpAdapterHost = httpAdapterHost;
955
982
  this.viteMode = viteConfig?.mode || "embedded";
956
983
  this.vitePort = viteConfig?.port || 5173;
984
+ this.registerSignalHandlers();
985
+ }
986
+ registerSignalHandlers() {
987
+ const cleanup = /* @__PURE__ */ __name(async (signal) => {
988
+ if (this.isShuttingDown) return;
989
+ this.isShuttingDown = true;
990
+ this.logger.log(`Received ${signal}, closing Vite server...`);
991
+ await this.closeViteServer();
992
+ }, "cleanup");
993
+ process.once("SIGTERM", () => cleanup("SIGTERM"));
994
+ process.once("SIGINT", () => cleanup("SIGINT"));
957
995
  }
958
996
  async onModuleInit() {
959
997
  const isDevelopment = process.env.NODE_ENV !== "production";
@@ -1042,9 +1080,23 @@ var ViteInitializerService = class _ViteInitializerService {
1042
1080
  * This prevents port conflicts on hot reload
1043
1081
  */
1044
1082
  async onModuleDestroy() {
1083
+ await this.closeViteServer();
1084
+ }
1085
+ /**
1086
+ * Cleanup: Close Vite server on application shutdown
1087
+ * Belt-and-suspenders approach with onModuleDestroy
1088
+ */
1089
+ async onApplicationShutdown() {
1090
+ await this.closeViteServer();
1091
+ }
1092
+ async closeViteServer() {
1093
+ if (this.isShuttingDown && !this.viteServer) return;
1094
+ this.isShuttingDown = true;
1045
1095
  if (this.viteServer) {
1046
1096
  try {
1097
+ this.renderService.setViteServer(null);
1047
1098
  await this.viteServer.close();
1099
+ this.viteServer = null;
1048
1100
  this.logger.log("\u2713 Vite server closed");
1049
1101
  } catch (error) {
1050
1102
  this.logger.warn(`Failed to close Vite server: ${error.message}`);
@@ -0,0 +1,104 @@
1
+ /**
2
+ * HTML head data for SEO and page metadata
3
+ */
4
+ interface HeadData {
5
+ /** Page title (appears in browser tab and search results) */
6
+ title?: string;
7
+ /** Page description for search engines */
8
+ description?: string;
9
+ /** Page keywords (legacy, less important for modern SEO) */
10
+ keywords?: string;
11
+ /** Canonical URL for duplicate content */
12
+ canonical?: string;
13
+ /** Open Graph title for social media sharing */
14
+ ogTitle?: string;
15
+ /** Open Graph description for social media sharing */
16
+ ogDescription?: string;
17
+ /** Open Graph image URL for social media previews */
18
+ ogImage?: string;
19
+ /** Additional link tags (fonts, icons, preloads, etc.) */
20
+ links?: Array<{
21
+ rel: string;
22
+ href: string;
23
+ as?: string;
24
+ type?: string;
25
+ crossorigin?: string;
26
+ [key: string]: any;
27
+ }>;
28
+ /** Additional meta tags */
29
+ meta?: Array<{
30
+ name?: string;
31
+ property?: string;
32
+ content: string;
33
+ [key: string]: any;
34
+ }>;
35
+ /** Script tags for analytics, tracking, etc. */
36
+ scripts?: Array<{
37
+ src?: string;
38
+ async?: boolean;
39
+ defer?: boolean;
40
+ type?: string;
41
+ innerHTML?: string;
42
+ [key: string]: any;
43
+ }>;
44
+ /** JSON-LD structured data for search engines */
45
+ jsonLd?: Array<Record<string, any>>;
46
+ /** Attributes to add to <html> tag (e.g., lang, dir) */
47
+ htmlAttributes?: Record<string, string>;
48
+ /** Attributes to add to <body> tag (e.g., class, data-theme) */
49
+ bodyAttributes?: Record<string, string>;
50
+ }
51
+ /**
52
+ * Response structure for SSR rendering
53
+ *
54
+ * Can be returned from controllers decorated with @Render.
55
+ * For backwards compatibility, controllers can also return plain objects
56
+ * which will be auto-wrapped as { props: data }.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * // Simple case - just props (auto-wrapped)
61
+ * @Render('views/home')
62
+ * getHome() {
63
+ * return { message: 'Hello' };
64
+ * // Treated as: { props: { message: 'Hello' } }
65
+ * }
66
+ *
67
+ * // Advanced case - with head data and layout props
68
+ * @Render('views/user')
69
+ * getUser(@Param('id') id: string) {
70
+ * const user = await this.userService.findOne(id);
71
+ * return {
72
+ * props: { user },
73
+ * layoutProps: {
74
+ * title: user.name,
75
+ * subtitle: 'User Profile'
76
+ * },
77
+ * head: {
78
+ * title: `${user.name} - Profile`,
79
+ * description: user.bio,
80
+ * ogImage: user.avatar
81
+ * }
82
+ * };
83
+ * }
84
+ * ```
85
+ */
86
+ interface RenderResponse<T = any> {
87
+ /** Props passed to the React component */
88
+ props: T;
89
+ /** HTML head data (title, meta tags, links) */
90
+ head?: HeadData;
91
+ /**
92
+ * Props passed to layout components (dynamic, per-request)
93
+ *
94
+ * These props are merged with static layout props from decorators:
95
+ * - Static props from @Layout decorator (controller level)
96
+ * - Static props from @Render decorator (method level)
97
+ * - Dynamic props from this field (highest priority)
98
+ *
99
+ * All layout components in the hierarchy receive the merged props.
100
+ */
101
+ layoutProps?: Record<string, any>;
102
+ }
103
+
104
+ export type { HeadData as H, RenderResponse as R };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * HTML head data for SEO and page metadata
3
+ */
4
+ interface HeadData {
5
+ /** Page title (appears in browser tab and search results) */
6
+ title?: string;
7
+ /** Page description for search engines */
8
+ description?: string;
9
+ /** Page keywords (legacy, less important for modern SEO) */
10
+ keywords?: string;
11
+ /** Canonical URL for duplicate content */
12
+ canonical?: string;
13
+ /** Open Graph title for social media sharing */
14
+ ogTitle?: string;
15
+ /** Open Graph description for social media sharing */
16
+ ogDescription?: string;
17
+ /** Open Graph image URL for social media previews */
18
+ ogImage?: string;
19
+ /** Additional link tags (fonts, icons, preloads, etc.) */
20
+ links?: Array<{
21
+ rel: string;
22
+ href: string;
23
+ as?: string;
24
+ type?: string;
25
+ crossorigin?: string;
26
+ [key: string]: any;
27
+ }>;
28
+ /** Additional meta tags */
29
+ meta?: Array<{
30
+ name?: string;
31
+ property?: string;
32
+ content: string;
33
+ [key: string]: any;
34
+ }>;
35
+ /** Script tags for analytics, tracking, etc. */
36
+ scripts?: Array<{
37
+ src?: string;
38
+ async?: boolean;
39
+ defer?: boolean;
40
+ type?: string;
41
+ innerHTML?: string;
42
+ [key: string]: any;
43
+ }>;
44
+ /** JSON-LD structured data for search engines */
45
+ jsonLd?: Array<Record<string, any>>;
46
+ /** Attributes to add to <html> tag (e.g., lang, dir) */
47
+ htmlAttributes?: Record<string, string>;
48
+ /** Attributes to add to <body> tag (e.g., class, data-theme) */
49
+ bodyAttributes?: Record<string, string>;
50
+ }
51
+ /**
52
+ * Response structure for SSR rendering
53
+ *
54
+ * Can be returned from controllers decorated with @Render.
55
+ * For backwards compatibility, controllers can also return plain objects
56
+ * which will be auto-wrapped as { props: data }.
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * // Simple case - just props (auto-wrapped)
61
+ * @Render('views/home')
62
+ * getHome() {
63
+ * return { message: 'Hello' };
64
+ * // Treated as: { props: { message: 'Hello' } }
65
+ * }
66
+ *
67
+ * // Advanced case - with head data and layout props
68
+ * @Render('views/user')
69
+ * getUser(@Param('id') id: string) {
70
+ * const user = await this.userService.findOne(id);
71
+ * return {
72
+ * props: { user },
73
+ * layoutProps: {
74
+ * title: user.name,
75
+ * subtitle: 'User Profile'
76
+ * },
77
+ * head: {
78
+ * title: `${user.name} - Profile`,
79
+ * description: user.bio,
80
+ * ogImage: user.avatar
81
+ * }
82
+ * };
83
+ * }
84
+ * ```
85
+ */
86
+ interface RenderResponse<T = any> {
87
+ /** Props passed to the React component */
88
+ props: T;
89
+ /** HTML head data (title, meta tags, links) */
90
+ head?: HeadData;
91
+ /**
92
+ * Props passed to layout components (dynamic, per-request)
93
+ *
94
+ * These props are merged with static layout props from decorators:
95
+ * - Static props from @Layout decorator (controller level)
96
+ * - Static props from @Render decorator (method level)
97
+ * - Dynamic props from this field (highest priority)
98
+ *
99
+ * All layout components in the hierarchy receive the merged props.
100
+ */
101
+ layoutProps?: Record<string, any>;
102
+ }
103
+
104
+ export type { HeadData as H, RenderResponse as R };
@@ -1,16 +1,19 @@
1
- /// <reference path="../global.d.ts" />
1
+ /// <reference types="@nestjs-ssr/react/global" />
2
2
  import React, { StrictMode } from 'react';
3
3
  import { hydrateRoot } from 'react-dom/client';
4
- import { PageContextProvider } from '../react/hooks/use-page-context';
4
+ import { PageContextProvider } from '@nestjs-ssr/react/client';
5
5
 
6
6
  const componentName = window.__COMPONENT_NAME__;
7
7
  const initialProps = window.__INITIAL_STATE__ || {};
8
8
  const renderContext = window.__CONTEXT__ || {};
9
9
 
10
10
  // Auto-import all view components using Vite's glob feature
11
+ // Exclude entry-client.tsx and entry-server.tsx from the glob
11
12
  // @ts-ignore - Vite-specific API
12
13
  const modules: Record<string, { default: React.ComponentType<any> }> =
13
- import.meta.glob('@/views/**/*.tsx', { eager: true });
14
+ import.meta.glob(['@/views/**/*.tsx', '!@/views/entry-*.tsx'], {
15
+ eager: true,
16
+ });
14
17
 
15
18
  // Build a map of components with their metadata
16
19
  // Filter out entry files and modules without default exports
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
- import { renderToString } from 'react-dom/server';
3
- import { PageContextProvider } from '../react/hooks/use-page-context';
2
+ import { renderToString, renderToPipeableStream } from 'react-dom/server';
3
+ import { PageContextProvider } from '@nestjs-ssr/react';
4
4
 
5
5
  /**
6
6
  * Compose a component with its layouts from the interceptor
@@ -23,6 +23,10 @@ function composeWithLayouts(
23
23
  return result;
24
24
  }
25
25
 
26
+ /**
27
+ * String-based SSR (mode: 'string')
28
+ * Simple, synchronous rendering
29
+ */
26
30
  export function renderComponent(
27
31
  ViewComponent: React.ComponentType<any>,
28
32
  data: any,
@@ -39,3 +43,30 @@ export function renderComponent(
39
43
 
40
44
  return renderToString(wrappedElement);
41
45
  }
46
+
47
+ /**
48
+ * Streaming SSR (mode: 'stream' - default)
49
+ * Modern approach with progressive rendering and Suspense support
50
+ */
51
+ export function renderComponentStream(
52
+ ViewComponent: React.ComponentType<any>,
53
+ data: any,
54
+ callbacks?: {
55
+ onShellReady?: () => void;
56
+ onShellError?: (error: unknown) => void;
57
+ onError?: (error: unknown) => void;
58
+ onAllReady?: () => void;
59
+ },
60
+ ) {
61
+ const { data: pageData, __context: context, __layouts: layouts } = data;
62
+ const composedElement = composeWithLayouts(ViewComponent, pageData, layouts);
63
+
64
+ // Wrap with PageContextProvider to make context available via hooks
65
+ const wrappedElement = (
66
+ <PageContextProvider context={context}>
67
+ {composedElement}
68
+ </PageContextProvider>
69
+ );
70
+
71
+ return renderToPipeableStream(wrappedElement, callbacks);
72
+ }
@@ -1,14 +1,17 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>NestJS React SSR</title>
7
- <!--styles-->
8
- </head>
9
- <body>
10
- <div id="root"><!--app-html--></div>
11
- <!--initial-state-->
12
- <!--client-scripts-->
13
- </body>
14
- </html>
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <!--head-meta-->
8
+ <!--styles-->
9
+ </head>
10
+
11
+ <body>
12
+ <div id="root"><!--app-html--></div>
13
+ <!--initial-state-->
14
+ <!--client-scripts-->
15
+ </body>
16
+
17
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs-ssr/react",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",
@@ -47,6 +47,11 @@
47
47
  "import": "./dist/index.mjs",
48
48
  "require": "./dist/index.js"
49
49
  },
50
+ "./client": {
51
+ "types": "./dist/client.d.ts",
52
+ "import": "./dist/client.mjs",
53
+ "require": "./dist/client.js"
54
+ },
50
55
  "./render": {
51
56
  "types": "./dist/render/index.d.ts",
52
57
  "import": "./dist/render/index.mjs",
@@ -79,6 +84,12 @@
79
84
  "test:watch": "vitest",
80
85
  "test:ui": "vitest --ui",
81
86
  "test:coverage": "vitest run --coverage",
87
+ "test:integration": "pnpm test:integration:setup && pnpm test:integration:dev && pnpm test:integration:prod",
88
+ "test:integration:setup": "tsx test/integration/setup/create-fixtures.ts",
89
+ "test:integration:dev": "TEST_MODE=dev playwright test -c test/integration/playwright.config.ts",
90
+ "test:integration:prod": "TEST_MODE=prod playwright test -c test/integration/playwright.config.ts",
91
+ "test:integration:run": "pnpm test:integration:dev",
92
+ "test:integration:clean": "rm -rf test/integration/fixtures/*/",
82
93
  "size": "size-limit",
83
94
  "api:extract": "api-extractor run --local --verbose",
84
95
  "api:check": "api-extractor run --verbose",
@@ -120,6 +131,7 @@
120
131
  },
121
132
  "devDependencies": {
122
133
  "@microsoft/api-extractor": "^7.55.2",
134
+ "@playwright/test": "^1.49.0",
123
135
  "@nestjs/common": "^11.0.0",
124
136
  "@nestjs/core": "^11.0.0",
125
137
  "@nestjs/platform-express": "^11.0.0",
@@ -1,16 +1,19 @@
1
- /// <reference path="../global.d.ts" />
1
+ /// <reference types="@nestjs-ssr/react/global" />
2
2
  import React, { StrictMode } from 'react';
3
3
  import { hydrateRoot } from 'react-dom/client';
4
- import { PageContextProvider } from '../react/hooks/use-page-context';
4
+ import { PageContextProvider } from '@nestjs-ssr/react/client';
5
5
 
6
6
  const componentName = window.__COMPONENT_NAME__;
7
7
  const initialProps = window.__INITIAL_STATE__ || {};
8
8
  const renderContext = window.__CONTEXT__ || {};
9
9
 
10
10
  // Auto-import all view components using Vite's glob feature
11
+ // Exclude entry-client.tsx and entry-server.tsx from the glob
11
12
  // @ts-ignore - Vite-specific API
12
13
  const modules: Record<string, { default: React.ComponentType<any> }> =
13
- import.meta.glob('@/views/**/*.tsx', { eager: true });
14
+ import.meta.glob(['@/views/**/*.tsx', '!@/views/entry-*.tsx'], {
15
+ eager: true,
16
+ });
14
17
 
15
18
  // Build a map of components with their metadata
16
19
  // Filter out entry files and modules without default exports