@nestjs-ssr/react 0.1.12 → 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.
- package/README.md +53 -85
- package/dist/cli/init.js +2 -2
- package/dist/cli/init.mjs +2 -2
- package/dist/{index-BiaVDe9J.d.mts → index-C5Knql-9.d.mts} +124 -8
- package/dist/{index-BiaVDe9J.d.ts → index-C5Knql-9.d.ts} +124 -8
- package/dist/index.d.mts +377 -69
- package/dist/index.d.ts +377 -69
- package/dist/index.js +460 -89
- package/dist/index.mjs +459 -85
- package/dist/render/index.d.mts +1 -1
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +253 -64
- package/dist/render/index.mjs +253 -63
- package/dist/templates/entry-client.tsx +80 -13
- package/dist/templates/entry-server.tsx +33 -2
- package/dist/templates/index.html +0 -3
- package/etc/react.api.md +262 -0
- package/package.json +28 -7
- package/src/global.d.ts +1 -1
- package/src/templates/entry-client.tsx +80 -13
- package/src/templates/entry-server.tsx +33 -2
- package/src/templates/index.html +0 -3
package/etc/react.api.md
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
102
|
-
"
|
|
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/
|
|
134
|
+
"@types/supertest": "^6.0.3",
|
|
116
135
|
"@vitejs/plugin-react": "^4.7.0",
|
|
117
|
-
"@vitest/
|
|
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.
|
|
146
|
+
"vitest": "^4.0.15"
|
|
126
147
|
},
|
|
127
148
|
"publishConfig": {
|
|
128
149
|
"access": "public"
|
package/src/global.d.ts
CHANGED
|
@@ -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> }> =
|
|
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
|
|
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) =>
|
|
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)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/templates/index.html
CHANGED
|
@@ -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>
|