@nestjs-ssr/react 0.1.11 → 0.2.0

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.
@@ -0,0 +1,262 @@
1
+ ## API Report File for "@nestjs-ssr/react"
2
+
3
+ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4
+
5
+ ```ts
6
+ import { CallHandler } from '@nestjs/common';
7
+ import { ComponentType } from 'react';
8
+ import { DynamicModule } from '@nestjs/common';
9
+ import { ExecutionContext } from '@nestjs/common';
10
+ import { NestInterceptor } from '@nestjs/common';
11
+ import { Observable } from 'rxjs';
12
+ import { default as React_2 } from 'react';
13
+ import * as react_jsx_runtime from 'react/jsx-runtime';
14
+ import { ReactNode } from 'react';
15
+ import { Reflector } from '@nestjs/core';
16
+ import { Response as Response_2 } from 'express';
17
+ import { ViteDevServer } from 'vite';
18
+
19
+ // Warning: (ae-forgotten-export) The symbol "ErrorPageDevelopmentProps" needs to be exported by the entry point index.d.ts
20
+ //
21
+ // @public
22
+ export function ErrorPageDevelopment({
23
+ error,
24
+ viewPath,
25
+ phase,
26
+ }: ErrorPageDevelopmentProps): react_jsx_runtime.JSX.Element;
27
+
28
+ // @public
29
+ export function ErrorPageProduction(): react_jsx_runtime.JSX.Element;
30
+
31
+ // @public
32
+ export interface HeadData {
33
+ bodyAttributes?: Record<string, string>;
34
+ canonical?: string;
35
+ description?: string;
36
+ htmlAttributes?: Record<string, string>;
37
+ jsonLd?: Array<Record<string, any>>;
38
+ keywords?: string;
39
+ links?: Array<{
40
+ rel: string;
41
+ href: string;
42
+ as?: string;
43
+ type?: string;
44
+ crossorigin?: string;
45
+ [key: string]: any;
46
+ }>;
47
+ meta?: Array<{
48
+ name?: string;
49
+ property?: string;
50
+ content: string;
51
+ [key: string]: any;
52
+ }>;
53
+ ogDescription?: string;
54
+ ogImage?: string;
55
+ ogTitle?: string;
56
+ scripts?: Array<{
57
+ src?: string;
58
+ async?: boolean;
59
+ defer?: boolean;
60
+ type?: string;
61
+ innerHTML?: string;
62
+ [key: string]: any;
63
+ }>;
64
+ title?: string;
65
+ }
66
+
67
+ // @public
68
+ export function Layout(
69
+ layout: LayoutComponent<any>,
70
+ options?: LayoutDecoratorOptions,
71
+ ): ClassDecorator;
72
+
73
+ // @public
74
+ export type LayoutComponent<TProps = {}> = ComponentType<LayoutProps<TProps>>;
75
+
76
+ // @public
77
+ export interface LayoutDecoratorOptions {
78
+ props?: Record<string, any>;
79
+ skipRoot?: boolean;
80
+ }
81
+
82
+ // @public
83
+ export interface LayoutProps<TProps = {}> {
84
+ children: ReactNode;
85
+ context?: RenderContext;
86
+ head?: HeadData;
87
+ layoutProps?: TProps;
88
+ }
89
+
90
+ // @public
91
+ export interface PageComponentWithLayout<TPageProps = {}, TLayoutProps = {}> {
92
+ (props: TPageProps): ReactNode;
93
+ layout?: LayoutComponent<TLayoutProps>;
94
+ layoutProps?: TLayoutProps;
95
+ }
96
+
97
+ // @public
98
+ export type PageProps<TProps = {}> = TProps & {
99
+ head?: HeadData;
100
+ context: RenderContext;
101
+ };
102
+
103
+ // Warning: (ae-forgotten-export) The symbol "RenderReturnType" needs to be exported by the entry point index.d.ts
104
+ // Warning: (ae-forgotten-export) The symbol "ExtractComponentData" needs to be exported by the entry point index.d.ts
105
+ //
106
+ // @public
107
+ export function Render<T extends React_2.ComponentType<any>>(
108
+ component: T,
109
+ options?: RenderOptions,
110
+ ): <
111
+ TMethod extends (
112
+ ...args: any[]
113
+ ) =>
114
+ | RenderReturnType<ExtractComponentData<T>>
115
+ | Promise<RenderReturnType<ExtractComponentData<T>>>,
116
+ >(
117
+ target: any,
118
+ propertyKey: string | symbol,
119
+ descriptor: TypedPropertyDescriptor<TMethod>,
120
+ ) => TypedPropertyDescriptor<TMethod> | void;
121
+
122
+ // @public
123
+ export interface RenderConfig {
124
+ defaultHead?: HeadData;
125
+ // Warning: (ae-forgotten-export) The symbol "ErrorPageDevelopmentProps$1" needs to be exported by the entry point index.d.ts
126
+ errorPageDevelopment?: ComponentType<ErrorPageDevelopmentProps$1>;
127
+ errorPageProduction?: ComponentType;
128
+ mode?: SSRMode;
129
+ template?: string;
130
+ timeout?: number;
131
+ // Warning: (ae-forgotten-export) The symbol "ViteConfig" needs to be exported by the entry point index.d.ts
132
+ vite?: ViteConfig;
133
+ }
134
+
135
+ // @public
136
+ export interface RenderContext {
137
+ // (undocumented)
138
+ [key: string]: any;
139
+ // (undocumented)
140
+ acceptLanguage?: string;
141
+ // (undocumented)
142
+ cookies?: Record<string, string>;
143
+ // (undocumented)
144
+ headers?: Record<string, string>;
145
+ // (undocumented)
146
+ method: string;
147
+ // (undocumented)
148
+ params: Record<string, string>;
149
+ // (undocumented)
150
+ path: string;
151
+ // (undocumented)
152
+ query: Record<string, string | string[]>;
153
+ // (undocumented)
154
+ referer?: string;
155
+ // (undocumented)
156
+ url: string;
157
+ // (undocumented)
158
+ userAgent?: string;
159
+ }
160
+
161
+ // @public (undocumented)
162
+ export class RenderInterceptor implements NestInterceptor {
163
+ constructor(reflector: Reflector, renderService: RenderService);
164
+ // (undocumented)
165
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
166
+ }
167
+
168
+ // @public (undocumented)
169
+ export class RenderModule {
170
+ static register(config?: RenderConfig): DynamicModule;
171
+ static registerAsync(options: {
172
+ imports?: any[];
173
+ inject?: any[];
174
+ useFactory: (...args: any[]) => Promise<RenderConfig> | RenderConfig;
175
+ }): DynamicModule;
176
+ }
177
+
178
+ // @public
179
+ export interface RenderOptions {
180
+ layout?: LayoutComponent<any> | false | null;
181
+ layoutProps?: Record<string, any>;
182
+ }
183
+
184
+ // @public
185
+ export interface RenderResponse<T = any> {
186
+ head?: HeadData;
187
+ layoutProps?: Record<string, any>;
188
+ props: T;
189
+ }
190
+
191
+ // @public (undocumented)
192
+ export class RenderService {
193
+ constructor(
194
+ templateParser: TemplateParserService,
195
+ streamingErrorHandler: StreamingErrorHandler,
196
+ ssrMode?: SSRMode,
197
+ defaultHead?: HeadData | undefined,
198
+ customTemplate?: string,
199
+ );
200
+ getRootLayout(): Promise<any | null>;
201
+ render(
202
+ viewComponent: any,
203
+ data?: any,
204
+ res?: Response_2,
205
+ head?: HeadData,
206
+ ): Promise<string | void>;
207
+ // (undocumented)
208
+ setViteServer(vite: ViteDevServer): void;
209
+ }
210
+
211
+ // @public
212
+ export type SSRMode = 'string' | 'stream';
213
+
214
+ // @public
215
+ export class StreamingErrorHandler {
216
+ constructor(
217
+ errorPageDevelopment?:
218
+ | ComponentType<ErrorPageDevelopmentProps$1>
219
+ | undefined,
220
+ errorPageProduction?: ComponentType | undefined,
221
+ );
222
+ handleShellError(
223
+ error: Error,
224
+ res: Response_2,
225
+ viewPath: string,
226
+ isDevelopment: boolean,
227
+ ): void;
228
+ handleStreamError(error: Error, viewPath: string): void;
229
+ }
230
+
231
+ // @public
232
+ export class TemplateParserService {
233
+ buildHeadTags(head?: HeadData): string;
234
+ buildInlineScripts(
235
+ data: any,
236
+ context: any,
237
+ componentName: string,
238
+ layouts?: Array<{
239
+ layout: any;
240
+ props?: any;
241
+ }>,
242
+ ): string;
243
+ getClientScriptTag(isDevelopment: boolean, manifest?: any): string;
244
+ getStylesheetTags(isDevelopment: boolean, manifest?: any): string;
245
+ // Warning: (ae-forgotten-export) The symbol "TemplateParts" needs to be exported by the entry point index.d.ts
246
+ parseTemplate(html: string): TemplateParts;
247
+ }
248
+
249
+ // @public
250
+ export function usePageContext(): RenderContext;
251
+
252
+ // @public
253
+ export function useParams(): Record<string, string>;
254
+
255
+ // @public
256
+ export function useQuery(): Record<string, string | string[]>;
257
+
258
+ // @public
259
+ export function useUserAgent(): string | undefined;
260
+
261
+ // (No @packageDocumentation comment for this package)
262
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs-ssr/react",
3
- "version": "0.1.11",
3
+ "version": "0.2.0",
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",
@@ -63,6 +63,7 @@
63
63
  },
64
64
  "files": [
65
65
  "dist",
66
+ "etc",
66
67
  "src/templates",
67
68
  "src/global.d.ts",
68
69
  "README.md",
@@ -78,8 +79,24 @@
78
79
  "test:watch": "vitest",
79
80
  "test:ui": "vitest --ui",
80
81
  "test:coverage": "vitest run --coverage",
81
- "prepublishOnly": "pnpm build && pnpm typecheck && pnpm test"
82
+ "size": "size-limit",
83
+ "api:extract": "api-extractor run --local --verbose",
84
+ "api:check": "api-extractor run --verbose",
85
+ "copy:readme": "cp ../../README.md README.md",
86
+ "prepublishOnly": "pnpm copy:readme && pnpm build && pnpm typecheck && pnpm test"
82
87
  },
88
+ "size-limit": [
89
+ {
90
+ "name": "Core (CJS)",
91
+ "path": "dist/index.js",
92
+ "limit": "30 KB"
93
+ },
94
+ {
95
+ "name": "Core (ESM)",
96
+ "path": "dist/index.mjs",
97
+ "limit": "30 KB"
98
+ }
99
+ ],
83
100
  "peerDependencies": {
84
101
  "@nestjs/common": "^11.0.0",
85
102
  "@nestjs/core": "^11.0.0",
@@ -98,13 +115,15 @@
98
115
  "dependencies": {
99
116
  "citty": "^0.1.6",
100
117
  "consola": "^3.4.2",
101
- "escape-html": "^1.0.3",
102
- "serialize-javascript": "^6.0.2"
118
+ "devalue": "^5.1.1",
119
+ "escape-html": "^1.0.3"
103
120
  },
104
121
  "devDependencies": {
122
+ "@microsoft/api-extractor": "^7.55.2",
105
123
  "@nestjs/common": "^11.0.0",
106
124
  "@nestjs/core": "^11.0.0",
107
125
  "@nestjs/platform-express": "^11.0.0",
126
+ "@nestjs/testing": "^11.1.9",
108
127
  "@testing-library/jest-dom": "^6.9.1",
109
128
  "@testing-library/react": "^16.3.0",
110
129
  "@types/escape-html": "^1.0.4",
@@ -112,17 +131,19 @@
112
131
  "@types/node": "^22.0.0",
113
132
  "@types/react": "^19.0.0",
114
133
  "@types/react-dom": "^19.0.0",
115
- "@types/serialize-javascript": "^5.0.4",
134
+ "@types/supertest": "^6.0.3",
116
135
  "@vitejs/plugin-react": "^4.7.0",
117
- "@vitest/ui": "^4.0.14",
136
+ "@vitest/coverage-v8": "^4.0.15",
137
+ "@vitest/ui": "^4.0.15",
118
138
  "happy-dom": "^20.0.11",
119
139
  "react": "^19.0.0",
120
140
  "react-dom": "^19.0.0",
121
141
  "rxjs": "^7.8.2",
142
+ "supertest": "^7.1.4",
122
143
  "tsup": "^8.0.0",
123
144
  "typescript": "^5.7.2",
124
145
  "vite": "^7.0.5",
125
- "vitest": "^4.0.14"
146
+ "vitest": "^4.0.15"
126
147
  },
127
148
  "publishConfig": {
128
149
  "access": "public"
package/src/global.d.ts CHANGED
@@ -32,7 +32,7 @@ declare global {
32
32
  import?: string;
33
33
  query?: string | Record<string, string | number | boolean>;
34
34
  as?: string;
35
- }
35
+ },
36
36
  ): Record<string, T>;
37
37
  }
38
38
  }
@@ -1,6 +1,7 @@
1
1
  /// <reference path="../global.d.ts" />
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
5
 
5
6
  const componentName = window.__COMPONENT_NAME__;
6
7
  const initialProps = window.__INITIAL_STATE__ || {};
@@ -8,7 +9,8 @@ const renderContext = window.__CONTEXT__ || {};
8
9
 
9
10
  // Auto-import all view components using Vite's glob feature
10
11
  // @ts-ignore - Vite-specific API
11
- const modules: Record<string, { default: React.ComponentType<any> }> = import.meta.glob('@/views/**/*.tsx', { eager: true });
12
+ const modules: Record<string, { default: React.ComponentType<any> }> =
13
+ import.meta.glob('@/views/**/*.tsx', { eager: true });
12
14
 
13
15
  // Build a map of components with their metadata
14
16
  // Filter out entry files and modules without default exports
@@ -26,7 +28,9 @@ const componentMap = Object.entries(modules)
26
28
  const component = module.default;
27
29
  const name = component.displayName || component.name;
28
30
  const filename = path.split('/').pop()?.replace('.tsx', '');
29
- const normalizedFilename = filename ? filename.charAt(0).toUpperCase() + filename.slice(1) : undefined;
31
+ const normalizedFilename = filename
32
+ ? filename.charAt(0).toUpperCase() + filename.slice(1)
33
+ : undefined;
30
34
 
31
35
  return { path, component, name, filename, normalizedFilename };
32
36
  });
@@ -40,7 +44,10 @@ let ViewComponent: React.ComponentType<any> | undefined;
40
44
 
41
45
  // Try exact name match first
42
46
  ViewComponent = componentMap.find(
43
- (c) => c.name === componentName || c.normalizedFilename === componentName || c.filename === componentName.toLowerCase()
47
+ (c) =>
48
+ c.name === componentName ||
49
+ c.normalizedFilename === componentName ||
50
+ c.filename === componentName.toLowerCase(),
44
51
  )?.component;
45
52
 
46
53
  // If no match found and component name looks like a generic/minified name (default, default_1, etc.)
@@ -56,7 +63,7 @@ if (!ViewComponent && /^default(_\d+)?$/.test(componentName)) {
56
63
 
57
64
  // Get all components with name "default" (anonymous functions), sorted by path for consistency
58
65
  const defaultComponents = componentMap
59
- .filter(c => c.name === 'default')
66
+ .filter((c) => c.name === 'default')
60
67
  .sort((a, b) => a.path.localeCompare(b.path));
61
68
 
62
69
  // Try to match by index
@@ -67,17 +74,77 @@ if (!ViewComponent && /^default(_\d+)?$/.test(componentName)) {
67
74
  }
68
75
 
69
76
  if (!ViewComponent) {
70
- const availableComponents = Object.entries(modules).map(([path, m]) => {
71
- const filename = path.split('/').pop()?.replace('.tsx', '');
72
- const name = m.default.displayName || m.default.name;
73
- return `${filename} (${name})`;
74
- }).join(', ');
75
- throw new Error(`Component "${componentName}" not found in views directory. Available: ${availableComponents}`);
77
+ const availableComponents = Object.entries(modules)
78
+ .map(([path, m]) => {
79
+ const filename = path.split('/').pop()?.replace('.tsx', '');
80
+ const name = m.default.displayName || m.default.name;
81
+ return `${filename} (${name})`;
82
+ })
83
+ .join(', ');
84
+ throw new Error(
85
+ `Component "${componentName}" not found in views directory. Available: ${availableComponents}`,
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Check if a component has a layout property
91
+ */
92
+ function hasLayout(
93
+ component: any,
94
+ ): component is { layout: React.ComponentType<any>; layoutProps?: any } {
95
+ return component && typeof component.layout === 'function';
96
+ }
97
+
98
+ /**
99
+ * Compose a component with its layout (and nested layouts if any)
100
+ * This must match the server-side composition in entry-server.tsx
101
+ */
102
+ function composeWithLayout(
103
+ ViewComponent: React.ComponentType<any>,
104
+ props: any,
105
+ ): React.ReactElement {
106
+ const element = <ViewComponent {...props} />;
107
+
108
+ // Check if component has a layout
109
+ if (!hasLayout(ViewComponent)) {
110
+ return element;
111
+ }
112
+
113
+ // Collect all layouts in the chain (innermost to outermost)
114
+ const layoutChain: Array<{
115
+ Layout: React.ComponentType<any>;
116
+ layoutProps: any;
117
+ }> = [];
118
+ let currentComponent: any = ViewComponent;
119
+
120
+ while (hasLayout(currentComponent)) {
121
+ layoutChain.push({
122
+ Layout: currentComponent.layout,
123
+ layoutProps: currentComponent.layoutProps || {},
124
+ });
125
+ currentComponent = currentComponent.layout;
126
+ }
127
+
128
+ // Wrap the element with layouts from innermost to outermost
129
+ let result = element;
130
+ for (const { Layout, layoutProps } of layoutChain) {
131
+ result = <Layout layoutProps={layoutProps}>{result}</Layout>;
132
+ }
133
+
134
+ return result;
76
135
  }
77
136
 
137
+ // Compose the component with its layout (if any)
138
+ const composedElement = composeWithLayout(ViewComponent, initialProps);
139
+
140
+ // Wrap with PageContextProvider to make context available via hooks
141
+ const wrappedElement = (
142
+ <PageContextProvider context={renderContext}>
143
+ {composedElement}
144
+ </PageContextProvider>
145
+ );
146
+
78
147
  hydrateRoot(
79
148
  document.getElementById('root')!,
80
- <StrictMode>
81
- <ViewComponent {...initialProps} context={renderContext} />
82
- </StrictMode>,
149
+ <StrictMode>{wrappedElement}</StrictMode>,
83
150
  );
@@ -1,10 +1,41 @@
1
1
  import React from 'react';
2
2
  import { renderToString } from 'react-dom/server';
3
+ import { PageContextProvider } from '../react/hooks/use-page-context';
4
+
5
+ /**
6
+ * Compose a component with its layouts from the interceptor
7
+ * Layouts are passed from the RenderInterceptor based on decorators
8
+ */
9
+ function composeWithLayouts(
10
+ ViewComponent: React.ComponentType<any>,
11
+ props: any,
12
+ layouts: Array<{ layout: React.ComponentType<any>; props?: any }> = [],
13
+ ): React.ReactElement {
14
+ // Start with the page component
15
+ let result = <ViewComponent {...props} />;
16
+
17
+ // Wrap with each layout in the chain (outermost to innermost in array)
18
+ // We iterate normally because layouts are already in correct order from interceptor
19
+ for (const { layout: Layout, props: layoutProps } of layouts) {
20
+ result = <Layout layoutProps={layoutProps}>{result}</Layout>;
21
+ }
22
+
23
+ return result;
24
+ }
3
25
 
4
26
  export function renderComponent(
5
27
  ViewComponent: React.ComponentType<any>,
6
28
  data: any,
7
29
  ) {
8
- const { data: pageData, __context: context } = data;
9
- return renderToString(<ViewComponent {...pageData} context={context} />);
30
+ const { data: pageData, __context: context, __layouts: layouts } = data;
31
+ const composedElement = composeWithLayouts(ViewComponent, pageData, layouts);
32
+
33
+ // Wrap with PageContextProvider to make context available via hooks
34
+ const wrappedElement = (
35
+ <PageContextProvider context={context}>
36
+ {composedElement}
37
+ </PageContextProvider>
38
+ );
39
+
40
+ return renderToString(wrappedElement);
10
41
  }
@@ -4,9 +4,6 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>NestJS React SSR</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com" />
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
10
7
  <!--styles-->
11
8
  </head>
12
9
  <body>