@nestjs-ssr/react 0.3.3 → 0.3.5
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.js +5 -5
- package/dist/cli/init.mjs +5 -5
- package/dist/client.d.mts +2 -2
- package/dist/client.d.ts +2 -2
- package/dist/client.js +50 -8
- package/dist/client.mjs +50 -8
- package/dist/{index-DdE--mA2.d.mts → index-CiYcz-1T.d.mts} +89 -4
- package/dist/{index-BzOLOiIZ.d.ts → index-Dq2qZSge.d.ts} +89 -4
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +90 -36
- package/dist/index.mjs +61 -7
- package/dist/render/index.d.mts +3 -3
- package/dist/render/index.d.ts +3 -3
- package/dist/render/index.js +58 -24
- package/dist/render/index.mjs +41 -6
- package/dist/{render-response.interface-CxbuKGnV.d.mts → render-response.interface-ClWJXKL4.d.mts} +19 -10
- package/dist/{render-response.interface-CxbuKGnV.d.ts → render-response.interface-ClWJXKL4.d.ts} +19 -10
- package/dist/templates/entry-client.tsx +69 -27
- package/dist/templates/entry-server.tsx +25 -8
- package/dist/{use-page-context-CGT9woWe.d.mts → use-page-context-CVC9DHcL.d.mts} +2 -1
- package/dist/{use-page-context-05ODF4zW.d.ts → use-page-context-DChgHhL9.d.ts} +2 -1
- package/etc/react.api.md +250 -262
- package/package.json +1 -1
- package/src/templates/entry-client.tsx +69 -27
- package/src/templates/entry-server.tsx +25 -8
package/dist/render/index.js
CHANGED
|
@@ -7,13 +7,12 @@ var path = require('path');
|
|
|
7
7
|
var devalue = require('devalue');
|
|
8
8
|
var escapeHtml = require('escape-html');
|
|
9
9
|
var server = require('react-dom/server');
|
|
10
|
-
var
|
|
10
|
+
var react = require('react');
|
|
11
11
|
var operators = require('rxjs/operators');
|
|
12
12
|
|
|
13
13
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
14
|
|
|
15
15
|
var escapeHtml__default = /*#__PURE__*/_interopDefault(escapeHtml);
|
|
16
|
-
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
17
16
|
|
|
18
17
|
var __defProp = Object.defineProperty;
|
|
19
18
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
@@ -315,9 +314,13 @@ var StringRenderer = class _StringRenderer {
|
|
|
315
314
|
throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
|
|
316
315
|
}
|
|
317
316
|
}
|
|
318
|
-
const { data: pageData, __context: pageContext } = data;
|
|
317
|
+
const { data: pageData, __context: pageContext, __layouts: layouts } = data;
|
|
319
318
|
const html = await renderModule.renderSegment(viewComponent, data);
|
|
320
319
|
const componentName = viewComponent.displayName || viewComponent.name || "Component";
|
|
320
|
+
const layoutMetadata = layouts ? layouts.map((l) => ({
|
|
321
|
+
name: l.layout.displayName || l.layout.name || "default",
|
|
322
|
+
props: l.props
|
|
323
|
+
})) : [];
|
|
321
324
|
if (context.isDevelopment) {
|
|
322
325
|
const duration = Date.now() - startTime;
|
|
323
326
|
this.logger.log(`[SSR] ${componentName} segment rendered in ${duration}ms`);
|
|
@@ -328,7 +331,8 @@ var StringRenderer = class _StringRenderer {
|
|
|
328
331
|
props: pageData,
|
|
329
332
|
swapTarget,
|
|
330
333
|
componentName,
|
|
331
|
-
context: pageContext
|
|
334
|
+
context: pageContext,
|
|
335
|
+
layouts: layoutMetadata
|
|
332
336
|
};
|
|
333
337
|
}
|
|
334
338
|
};
|
|
@@ -339,16 +343,18 @@ StringRenderer = _ts_decorate2([
|
|
|
339
343
|
typeof exports.TemplateParserService === "undefined" ? Object : exports.TemplateParserService
|
|
340
344
|
])
|
|
341
345
|
], StringRenderer);
|
|
346
|
+
|
|
347
|
+
// src/render/error-pages/error-page-development.tsx
|
|
342
348
|
function ErrorPageDevelopment({ error, viewPath, phase }) {
|
|
343
349
|
const stackLines = error.stack ? error.stack.split("\n").slice(1) : [];
|
|
344
|
-
return /* @__PURE__ */
|
|
350
|
+
return /* @__PURE__ */ React.createElement("html", {
|
|
345
351
|
lang: "en"
|
|
346
|
-
}, /* @__PURE__ */
|
|
352
|
+
}, /* @__PURE__ */ React.createElement("head", null, /* @__PURE__ */ React.createElement("meta", {
|
|
347
353
|
charSet: "UTF-8"
|
|
348
|
-
}), /* @__PURE__ */
|
|
354
|
+
}), /* @__PURE__ */ React.createElement("meta", {
|
|
349
355
|
name: "viewport",
|
|
350
356
|
content: "width=device-width, initial-scale=1.0"
|
|
351
|
-
}), /* @__PURE__ */
|
|
357
|
+
}), /* @__PURE__ */ React.createElement("title", null, `SSR Error - ${error.name}`), /* @__PURE__ */ React.createElement("style", {
|
|
352
358
|
dangerouslySetInnerHTML: {
|
|
353
359
|
__html: `
|
|
354
360
|
body {
|
|
@@ -399,28 +405,30 @@ function ErrorPageDevelopment({ error, viewPath, phase }) {
|
|
|
399
405
|
}
|
|
400
406
|
`
|
|
401
407
|
}
|
|
402
|
-
})), /* @__PURE__ */
|
|
408
|
+
})), /* @__PURE__ */ React.createElement("body", null, /* @__PURE__ */ React.createElement("div", {
|
|
403
409
|
className: "error-container"
|
|
404
|
-
}, /* @__PURE__ */
|
|
410
|
+
}, /* @__PURE__ */ React.createElement("h1", null, "Server-Side Rendering Error"), /* @__PURE__ */ React.createElement("div", {
|
|
405
411
|
className: "error-type"
|
|
406
|
-
}, error.name), /* @__PURE__ */
|
|
412
|
+
}, error.name), /* @__PURE__ */ React.createElement("div", {
|
|
407
413
|
className: "error-message"
|
|
408
|
-
}, error.message), /* @__PURE__ */
|
|
414
|
+
}, error.message), /* @__PURE__ */ React.createElement("h2", null, "Stack Trace"), /* @__PURE__ */ React.createElement("div", {
|
|
409
415
|
className: "stack-trace"
|
|
410
|
-
}, /* @__PURE__ */
|
|
416
|
+
}, /* @__PURE__ */ React.createElement("pre", null, stackLines.join("\n"))), /* @__PURE__ */ React.createElement("div", {
|
|
411
417
|
className: "meta"
|
|
412
|
-
}, /* @__PURE__ */
|
|
418
|
+
}, /* @__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")))));
|
|
413
419
|
}
|
|
414
420
|
__name(ErrorPageDevelopment, "ErrorPageDevelopment");
|
|
421
|
+
|
|
422
|
+
// src/render/error-pages/error-page-production.tsx
|
|
415
423
|
function ErrorPageProduction() {
|
|
416
|
-
return /* @__PURE__ */
|
|
424
|
+
return /* @__PURE__ */ React.createElement("html", {
|
|
417
425
|
lang: "en"
|
|
418
|
-
}, /* @__PURE__ */
|
|
426
|
+
}, /* @__PURE__ */ React.createElement("head", null, /* @__PURE__ */ React.createElement("meta", {
|
|
419
427
|
charSet: "UTF-8"
|
|
420
|
-
}), /* @__PURE__ */
|
|
428
|
+
}), /* @__PURE__ */ React.createElement("meta", {
|
|
421
429
|
name: "viewport",
|
|
422
430
|
content: "width=device-width, initial-scale=1.0"
|
|
423
|
-
}), /* @__PURE__ */
|
|
431
|
+
}), /* @__PURE__ */ React.createElement("title", null, "Error"), /* @__PURE__ */ React.createElement("style", {
|
|
424
432
|
dangerouslySetInnerHTML: {
|
|
425
433
|
__html: `
|
|
426
434
|
body {
|
|
@@ -447,9 +455,9 @@ function ErrorPageProduction() {
|
|
|
447
455
|
}
|
|
448
456
|
`
|
|
449
457
|
}
|
|
450
|
-
})), /* @__PURE__ */
|
|
458
|
+
})), /* @__PURE__ */ React.createElement("body", null, /* @__PURE__ */ React.createElement("div", {
|
|
451
459
|
className: "error-container"
|
|
452
|
-
}, /* @__PURE__ */
|
|
460
|
+
}, /* @__PURE__ */ React.createElement("h1", null, "500"), /* @__PURE__ */ React.createElement("p", null, "Internal Server Error"), /* @__PURE__ */ React.createElement("p", null, "Something went wrong while rendering this page."))));
|
|
453
461
|
}
|
|
454
462
|
__name(ErrorPageProduction, "ErrorPageProduction");
|
|
455
463
|
|
|
@@ -516,7 +524,7 @@ exports.StreamingErrorHandler = class _StreamingErrorHandler {
|
|
|
516
524
|
*/
|
|
517
525
|
renderDevelopmentErrorPage(error, viewPath, phase) {
|
|
518
526
|
const ErrorComponent = this.errorPageDevelopment || ErrorPageDevelopment;
|
|
519
|
-
const element =
|
|
527
|
+
const element = react.createElement(ErrorComponent, {
|
|
520
528
|
error,
|
|
521
529
|
viewPath,
|
|
522
530
|
phase
|
|
@@ -528,7 +536,7 @@ exports.StreamingErrorHandler = class _StreamingErrorHandler {
|
|
|
528
536
|
*/
|
|
529
537
|
renderProductionErrorPage() {
|
|
530
538
|
const ErrorComponent = this.errorPageProduction || ErrorPageProduction;
|
|
531
|
-
const element =
|
|
539
|
+
const element = react.createElement(ErrorComponent);
|
|
532
540
|
return "<!DOCTYPE html>\n" + server.renderToStaticMarkup(element);
|
|
533
541
|
}
|
|
534
542
|
/**
|
|
@@ -1075,11 +1083,13 @@ exports.RenderInterceptor = class RenderInterceptor {
|
|
|
1075
1083
|
renderService;
|
|
1076
1084
|
allowedHeaders;
|
|
1077
1085
|
allowedCookies;
|
|
1078
|
-
|
|
1086
|
+
contextFactory;
|
|
1087
|
+
constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory) {
|
|
1079
1088
|
this.reflector = reflector;
|
|
1080
1089
|
this.renderService = renderService;
|
|
1081
1090
|
this.allowedHeaders = allowedHeaders;
|
|
1082
1091
|
this.allowedCookies = allowedCookies;
|
|
1092
|
+
this.contextFactory = contextFactory;
|
|
1083
1093
|
}
|
|
1084
1094
|
/**
|
|
1085
1095
|
* Resolve the layout hierarchy for a given route
|
|
@@ -1219,6 +1229,14 @@ exports.RenderInterceptor = class RenderInterceptor {
|
|
|
1219
1229
|
renderContext.cookies = cookies;
|
|
1220
1230
|
}
|
|
1221
1231
|
}
|
|
1232
|
+
if (this.contextFactory) {
|
|
1233
|
+
const customContext = await this.contextFactory({
|
|
1234
|
+
req: request
|
|
1235
|
+
});
|
|
1236
|
+
if (customContext) {
|
|
1237
|
+
Object.assign(renderContext, customContext);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1222
1240
|
const renderResponse = isRenderResponse(data) ? data : {
|
|
1223
1241
|
props: data
|
|
1224
1242
|
};
|
|
@@ -1265,12 +1283,15 @@ exports.RenderInterceptor = _ts_decorate6([
|
|
|
1265
1283
|
_ts_param3(2, common.Inject("ALLOWED_HEADERS")),
|
|
1266
1284
|
_ts_param3(3, common.Optional()),
|
|
1267
1285
|
_ts_param3(3, common.Inject("ALLOWED_COOKIES")),
|
|
1286
|
+
_ts_param3(4, common.Optional()),
|
|
1287
|
+
_ts_param3(4, common.Inject("CONTEXT_FACTORY")),
|
|
1268
1288
|
_ts_metadata5("design:type", Function),
|
|
1269
1289
|
_ts_metadata5("design:paramtypes", [
|
|
1270
1290
|
typeof core.Reflector === "undefined" ? Object : core.Reflector,
|
|
1271
1291
|
typeof exports.RenderService === "undefined" ? Object : exports.RenderService,
|
|
1272
1292
|
Array,
|
|
1273
|
-
Array
|
|
1293
|
+
Array,
|
|
1294
|
+
typeof ContextFactory === "undefined" ? Object : ContextFactory
|
|
1274
1295
|
])
|
|
1275
1296
|
], exports.RenderInterceptor);
|
|
1276
1297
|
function _ts_decorate7(decorators, target, key, desc) {
|
|
@@ -1514,6 +1535,12 @@ exports.RenderModule = class _RenderModule {
|
|
|
1514
1535
|
provide: "ALLOWED_COOKIES",
|
|
1515
1536
|
useValue: config?.allowedCookies || []
|
|
1516
1537
|
});
|
|
1538
|
+
if (config?.context) {
|
|
1539
|
+
providers.push({
|
|
1540
|
+
provide: "CONTEXT_FACTORY",
|
|
1541
|
+
useValue: config.context
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1517
1544
|
return {
|
|
1518
1545
|
global: true,
|
|
1519
1546
|
module: _RenderModule,
|
|
@@ -1632,6 +1659,13 @@ exports.RenderModule = class _RenderModule {
|
|
|
1632
1659
|
inject: [
|
|
1633
1660
|
"RENDER_CONFIG"
|
|
1634
1661
|
]
|
|
1662
|
+
},
|
|
1663
|
+
{
|
|
1664
|
+
provide: "CONTEXT_FACTORY",
|
|
1665
|
+
useFactory: /* @__PURE__ */ __name((config) => config?.context, "useFactory"),
|
|
1666
|
+
inject: [
|
|
1667
|
+
"RENDER_CONFIG"
|
|
1668
|
+
]
|
|
1635
1669
|
}
|
|
1636
1670
|
];
|
|
1637
1671
|
return {
|
package/dist/render/index.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Injectable, Logger, Optional, Inject, Global, Module } from '@nestjs/common';
|
|
2
|
-
import { HttpAdapterHost, APP_INTERCEPTOR
|
|
2
|
+
import { Reflector, HttpAdapterHost, APP_INTERCEPTOR } from '@nestjs/core';
|
|
3
3
|
import { existsSync, readFileSync } from 'fs';
|
|
4
4
|
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
|
|
8
|
+
import { createElement } from 'react';
|
|
9
9
|
import { switchMap } from 'rxjs/operators';
|
|
10
10
|
|
|
11
11
|
var __defProp = Object.defineProperty;
|
|
@@ -308,9 +308,13 @@ var StringRenderer = class _StringRenderer {
|
|
|
308
308
|
throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
|
-
const { data: pageData, __context: pageContext } = data;
|
|
311
|
+
const { data: pageData, __context: pageContext, __layouts: layouts } = data;
|
|
312
312
|
const html = await renderModule.renderSegment(viewComponent, data);
|
|
313
313
|
const componentName = viewComponent.displayName || viewComponent.name || "Component";
|
|
314
|
+
const layoutMetadata = layouts ? layouts.map((l) => ({
|
|
315
|
+
name: l.layout.displayName || l.layout.name || "default",
|
|
316
|
+
props: l.props
|
|
317
|
+
})) : [];
|
|
314
318
|
if (context.isDevelopment) {
|
|
315
319
|
const duration = Date.now() - startTime;
|
|
316
320
|
this.logger.log(`[SSR] ${componentName} segment rendered in ${duration}ms`);
|
|
@@ -321,7 +325,8 @@ var StringRenderer = class _StringRenderer {
|
|
|
321
325
|
props: pageData,
|
|
322
326
|
swapTarget,
|
|
323
327
|
componentName,
|
|
324
|
-
context: pageContext
|
|
328
|
+
context: pageContext,
|
|
329
|
+
layouts: layoutMetadata
|
|
325
330
|
};
|
|
326
331
|
}
|
|
327
332
|
};
|
|
@@ -332,6 +337,8 @@ StringRenderer = _ts_decorate2([
|
|
|
332
337
|
typeof TemplateParserService === "undefined" ? Object : TemplateParserService
|
|
333
338
|
])
|
|
334
339
|
], StringRenderer);
|
|
340
|
+
|
|
341
|
+
// src/render/error-pages/error-page-development.tsx
|
|
335
342
|
function ErrorPageDevelopment({ error, viewPath, phase }) {
|
|
336
343
|
const stackLines = error.stack ? error.stack.split("\n").slice(1) : [];
|
|
337
344
|
return /* @__PURE__ */ React.createElement("html", {
|
|
@@ -405,6 +412,8 @@ function ErrorPageDevelopment({ error, viewPath, phase }) {
|
|
|
405
412
|
}, /* @__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")))));
|
|
406
413
|
}
|
|
407
414
|
__name(ErrorPageDevelopment, "ErrorPageDevelopment");
|
|
415
|
+
|
|
416
|
+
// src/render/error-pages/error-page-production.tsx
|
|
408
417
|
function ErrorPageProduction() {
|
|
409
418
|
return /* @__PURE__ */ React.createElement("html", {
|
|
410
419
|
lang: "en"
|
|
@@ -1068,11 +1077,13 @@ var RenderInterceptor = class {
|
|
|
1068
1077
|
renderService;
|
|
1069
1078
|
allowedHeaders;
|
|
1070
1079
|
allowedCookies;
|
|
1071
|
-
|
|
1080
|
+
contextFactory;
|
|
1081
|
+
constructor(reflector, renderService, allowedHeaders, allowedCookies, contextFactory) {
|
|
1072
1082
|
this.reflector = reflector;
|
|
1073
1083
|
this.renderService = renderService;
|
|
1074
1084
|
this.allowedHeaders = allowedHeaders;
|
|
1075
1085
|
this.allowedCookies = allowedCookies;
|
|
1086
|
+
this.contextFactory = contextFactory;
|
|
1076
1087
|
}
|
|
1077
1088
|
/**
|
|
1078
1089
|
* Resolve the layout hierarchy for a given route
|
|
@@ -1212,6 +1223,14 @@ var RenderInterceptor = class {
|
|
|
1212
1223
|
renderContext.cookies = cookies;
|
|
1213
1224
|
}
|
|
1214
1225
|
}
|
|
1226
|
+
if (this.contextFactory) {
|
|
1227
|
+
const customContext = await this.contextFactory({
|
|
1228
|
+
req: request
|
|
1229
|
+
});
|
|
1230
|
+
if (customContext) {
|
|
1231
|
+
Object.assign(renderContext, customContext);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1215
1234
|
const renderResponse = isRenderResponse(data) ? data : {
|
|
1216
1235
|
props: data
|
|
1217
1236
|
};
|
|
@@ -1258,12 +1277,15 @@ RenderInterceptor = _ts_decorate6([
|
|
|
1258
1277
|
_ts_param3(2, Inject("ALLOWED_HEADERS")),
|
|
1259
1278
|
_ts_param3(3, Optional()),
|
|
1260
1279
|
_ts_param3(3, Inject("ALLOWED_COOKIES")),
|
|
1280
|
+
_ts_param3(4, Optional()),
|
|
1281
|
+
_ts_param3(4, Inject("CONTEXT_FACTORY")),
|
|
1261
1282
|
_ts_metadata5("design:type", Function),
|
|
1262
1283
|
_ts_metadata5("design:paramtypes", [
|
|
1263
1284
|
typeof Reflector === "undefined" ? Object : Reflector,
|
|
1264
1285
|
typeof RenderService === "undefined" ? Object : RenderService,
|
|
1265
1286
|
Array,
|
|
1266
|
-
Array
|
|
1287
|
+
Array,
|
|
1288
|
+
typeof ContextFactory === "undefined" ? Object : ContextFactory
|
|
1267
1289
|
])
|
|
1268
1290
|
], RenderInterceptor);
|
|
1269
1291
|
function _ts_decorate7(decorators, target, key, desc) {
|
|
@@ -1507,6 +1529,12 @@ var RenderModule = class _RenderModule {
|
|
|
1507
1529
|
provide: "ALLOWED_COOKIES",
|
|
1508
1530
|
useValue: config?.allowedCookies || []
|
|
1509
1531
|
});
|
|
1532
|
+
if (config?.context) {
|
|
1533
|
+
providers.push({
|
|
1534
|
+
provide: "CONTEXT_FACTORY",
|
|
1535
|
+
useValue: config.context
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1510
1538
|
return {
|
|
1511
1539
|
global: true,
|
|
1512
1540
|
module: _RenderModule,
|
|
@@ -1625,6 +1653,13 @@ var RenderModule = class _RenderModule {
|
|
|
1625
1653
|
inject: [
|
|
1626
1654
|
"RENDER_CONFIG"
|
|
1627
1655
|
]
|
|
1656
|
+
},
|
|
1657
|
+
{
|
|
1658
|
+
provide: "CONTEXT_FACTORY",
|
|
1659
|
+
useFactory: /* @__PURE__ */ __name((config) => config?.context, "useFactory"),
|
|
1660
|
+
inject: [
|
|
1661
|
+
"RENDER_CONFIG"
|
|
1662
|
+
]
|
|
1628
1663
|
}
|
|
1629
1664
|
];
|
|
1630
1665
|
return {
|
package/dist/{render-response.interface-CxbuKGnV.d.mts → render-response.interface-ClWJXKL4.d.mts}
RENAMED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Contains safe request metadata that can be exposed to the client.
|
|
4
4
|
*
|
|
5
5
|
* Extend this interface to add app-specific properties (user, tenant, feature flags, etc.).
|
|
6
|
-
* Use module configuration to
|
|
6
|
+
* Use the `context` option in module configuration to enrich the context.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* // Basic usage - use as-is
|
|
@@ -32,19 +32,28 @@
|
|
|
32
32
|
* theme?: string; // From cookie
|
|
33
33
|
* }
|
|
34
34
|
*
|
|
35
|
-
* // Configure module to
|
|
36
|
-
*
|
|
35
|
+
* // Configure module with context factory to enrich context
|
|
36
|
+
* RenderModule.forRoot({
|
|
37
37
|
* allowedCookies: ['theme', 'locale'],
|
|
38
38
|
* allowedHeaders: ['x-tenant-id'],
|
|
39
|
+
* context: ({ req }) => ({
|
|
40
|
+
* user: req.user, // From Passport JWT strategy
|
|
41
|
+
* tenant: req.tenant,
|
|
42
|
+
* featureFlags: req.featureFlags,
|
|
43
|
+
* }),
|
|
39
44
|
* })
|
|
40
45
|
*
|
|
41
|
-
* //
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* // Or with async factory (use forRootAsync)
|
|
47
|
+
* RenderModule.forRootAsync({
|
|
48
|
+
* imports: [PermissionModule],
|
|
49
|
+
* inject: [PermissionService],
|
|
50
|
+
* useFactory: (permissionService) => ({
|
|
51
|
+
* context: async ({ req }) => ({
|
|
52
|
+
* user: req.user,
|
|
53
|
+
* permissions: await permissionService.getForUser(req.user),
|
|
54
|
+
* }),
|
|
55
|
+
* }),
|
|
56
|
+
* })
|
|
48
57
|
*/
|
|
49
58
|
interface RenderContext {
|
|
50
59
|
url: string;
|
package/dist/{render-response.interface-CxbuKGnV.d.ts → render-response.interface-ClWJXKL4.d.ts}
RENAMED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Contains safe request metadata that can be exposed to the client.
|
|
4
4
|
*
|
|
5
5
|
* Extend this interface to add app-specific properties (user, tenant, feature flags, etc.).
|
|
6
|
-
* Use module configuration to
|
|
6
|
+
* Use the `context` option in module configuration to enrich the context.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
9
|
* // Basic usage - use as-is
|
|
@@ -32,19 +32,28 @@
|
|
|
32
32
|
* theme?: string; // From cookie
|
|
33
33
|
* }
|
|
34
34
|
*
|
|
35
|
-
* // Configure module to
|
|
36
|
-
*
|
|
35
|
+
* // Configure module with context factory to enrich context
|
|
36
|
+
* RenderModule.forRoot({
|
|
37
37
|
* allowedCookies: ['theme', 'locale'],
|
|
38
38
|
* allowedHeaders: ['x-tenant-id'],
|
|
39
|
+
* context: ({ req }) => ({
|
|
40
|
+
* user: req.user, // From Passport JWT strategy
|
|
41
|
+
* tenant: req.tenant,
|
|
42
|
+
* featureFlags: req.featureFlags,
|
|
43
|
+
* }),
|
|
39
44
|
* })
|
|
40
45
|
*
|
|
41
|
-
* //
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* // Or with async factory (use forRootAsync)
|
|
47
|
+
* RenderModule.forRootAsync({
|
|
48
|
+
* imports: [PermissionModule],
|
|
49
|
+
* inject: [PermissionService],
|
|
50
|
+
* useFactory: (permissionService) => ({
|
|
51
|
+
* context: async ({ req }) => ({
|
|
52
|
+
* user: req.user,
|
|
53
|
+
* permissions: await permissionService.getForUser(req.user),
|
|
54
|
+
* }),
|
|
55
|
+
* }),
|
|
56
|
+
* })
|
|
48
57
|
*/
|
|
49
58
|
interface RenderContext {
|
|
50
59
|
url: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="@nestjs-ssr/react/global" />
|
|
2
|
+
|
|
2
3
|
import React, { StrictMode } from 'react';
|
|
3
4
|
import { hydrateRoot } from 'react-dom/client';
|
|
4
5
|
import {
|
|
@@ -10,6 +11,15 @@ const componentName = window.__COMPONENT_NAME__;
|
|
|
10
11
|
const initialProps = window.__INITIAL_STATE__ || {};
|
|
11
12
|
const renderContext = window.__CONTEXT__ || {};
|
|
12
13
|
|
|
14
|
+
// Auto-discover root layout using Vite's glob import (must match server-side discovery)
|
|
15
|
+
// @ts-ignore - Vite-specific API
|
|
16
|
+
const layoutModules = import.meta.glob('@/views/layout.tsx', {
|
|
17
|
+
eager: true,
|
|
18
|
+
}) as Record<string, { default: React.ComponentType<any> }>;
|
|
19
|
+
|
|
20
|
+
const layoutPath = Object.keys(layoutModules)[0];
|
|
21
|
+
const RootLayout = layoutPath ? layoutModules[layoutPath].default : null;
|
|
22
|
+
|
|
13
23
|
// Auto-import all view components using Vite's glob feature
|
|
14
24
|
// Exclude entry-client.tsx and entry-server.tsx from the glob
|
|
15
25
|
// @ts-ignore - Vite-specific API
|
|
@@ -105,46 +115,68 @@ function hasLayout(
|
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
/**
|
|
108
|
-
* Compose a component with its layout (and nested layouts if any)
|
|
109
|
-
* This must match the server-side composition in entry-server.tsx
|
|
118
|
+
* Compose a component with its layout (and nested layouts if any).
|
|
119
|
+
* This must match the server-side composition in entry-server.tsx.
|
|
120
|
+
*
|
|
121
|
+
* The layouts array is ordered [RootLayout, ControllerLayout, MethodLayout] (outer to inner).
|
|
122
|
+
* We iterate in REVERSE order because wrapping happens inside-out:
|
|
123
|
+
* - Start with Page
|
|
124
|
+
* - Wrap with innermost layout first (MethodLayout)
|
|
125
|
+
* - Then wrap with ControllerLayout
|
|
126
|
+
* - Finally wrap with RootLayout (outermost)
|
|
110
127
|
*/
|
|
111
128
|
function composeWithLayout(
|
|
112
129
|
ViewComponent: React.ComponentType<any>,
|
|
113
130
|
props: any,
|
|
131
|
+
context?: any,
|
|
132
|
+
layouts: Array<{ layout: React.ComponentType<any>; props?: any }> = [],
|
|
114
133
|
): React.ReactElement {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
while (hasLayout(currentComponent)) {
|
|
130
|
-
layoutChain.push({
|
|
131
|
-
Layout: currentComponent.layout,
|
|
132
|
-
layoutProps: currentComponent.layoutProps || {},
|
|
133
|
-
});
|
|
134
|
-
currentComponent = currentComponent.layout;
|
|
134
|
+
// Start with the page component
|
|
135
|
+
let result = <ViewComponent {...props} />;
|
|
136
|
+
|
|
137
|
+
// If no layouts passed, check if component has its own layout chain
|
|
138
|
+
if (layouts.length === 0 && hasLayout(ViewComponent)) {
|
|
139
|
+
let currentComponent: any = ViewComponent;
|
|
140
|
+
while (hasLayout(currentComponent)) {
|
|
141
|
+
layouts.push({
|
|
142
|
+
layout: currentComponent.layout,
|
|
143
|
+
props: currentComponent.layoutProps || {},
|
|
144
|
+
});
|
|
145
|
+
currentComponent = currentComponent.layout;
|
|
146
|
+
}
|
|
135
147
|
}
|
|
136
148
|
|
|
137
|
-
// Wrap
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
// Wrap with each layout in REVERSE order (innermost to outermost)
|
|
150
|
+
// This produces the correct nesting: RootLayout > ControllerLayout > Page
|
|
151
|
+
// Must match server-side wrapping with data-layout and data-outlet attributes
|
|
152
|
+
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
153
|
+
const { layout: Layout, props: layoutProps } = layouts[i];
|
|
154
|
+
const layoutName = Layout.displayName || Layout.name || 'Layout';
|
|
155
|
+
result = (
|
|
156
|
+
<div data-layout={layoutName}>
|
|
157
|
+
<Layout context={context} layoutProps={layoutProps}>
|
|
158
|
+
<div data-outlet={layoutName}>{result}</div>
|
|
159
|
+
</Layout>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
141
162
|
}
|
|
142
163
|
|
|
143
164
|
return result;
|
|
144
165
|
}
|
|
145
166
|
|
|
167
|
+
// Build layouts array - use RootLayout if it exists (matching server behavior)
|
|
168
|
+
const layouts: Array<{ layout: React.ComponentType<any>; props?: any }> = [];
|
|
169
|
+
if (RootLayout) {
|
|
170
|
+
layouts.push({ layout: RootLayout, props: {} });
|
|
171
|
+
}
|
|
172
|
+
|
|
146
173
|
// Compose the component with its layout (if any)
|
|
147
|
-
const composedElement = composeWithLayout(
|
|
174
|
+
const composedElement = composeWithLayout(
|
|
175
|
+
ViewComponent,
|
|
176
|
+
initialProps,
|
|
177
|
+
renderContext,
|
|
178
|
+
layouts,
|
|
179
|
+
);
|
|
148
180
|
|
|
149
181
|
// Wrap with providers to make context and navigation state available via hooks
|
|
150
182
|
const wrappedElement = (
|
|
@@ -160,8 +192,18 @@ hydrateRoot(
|
|
|
160
192
|
<StrictMode>{wrappedElement}</StrictMode>,
|
|
161
193
|
);
|
|
162
194
|
|
|
195
|
+
// Track if initial hydration is complete to ignore false popstate events
|
|
196
|
+
let hydrationComplete = false;
|
|
197
|
+
requestAnimationFrame(() => {
|
|
198
|
+
hydrationComplete = true;
|
|
199
|
+
});
|
|
200
|
+
|
|
163
201
|
// Handle browser back/forward navigation
|
|
164
202
|
window.addEventListener('popstate', async () => {
|
|
203
|
+
// Ignore popstate events that fire before hydration is complete
|
|
204
|
+
// (some browsers fire popstate on initial page load)
|
|
205
|
+
if (!hydrationComplete) return;
|
|
206
|
+
|
|
165
207
|
// Dynamically import navigate to avoid circular dependency with hydrate-segment
|
|
166
208
|
const { navigate } = await import('@nestjs-ssr/react/client');
|
|
167
209
|
// Re-navigate to the current URL (browser already updated location)
|
|
@@ -25,6 +25,13 @@ export function getRootLayout(): React.ComponentType<any> | null {
|
|
|
25
25
|
* Layouts are passed from the RenderInterceptor based on decorators.
|
|
26
26
|
* Each layout is wrapped with data-layout and data-outlet attributes
|
|
27
27
|
* for client-side navigation segment swapping.
|
|
28
|
+
*
|
|
29
|
+
* The layouts array is ordered [RootLayout, ControllerLayout, MethodLayout] (outer to inner).
|
|
30
|
+
* We iterate in REVERSE order because wrapping happens inside-out:
|
|
31
|
+
* - Start with Page
|
|
32
|
+
* - Wrap with innermost layout first (MethodLayout)
|
|
33
|
+
* - Then wrap with ControllerLayout
|
|
34
|
+
* - Finally wrap with RootLayout (outermost)
|
|
28
35
|
*/
|
|
29
36
|
function composeWithLayouts(
|
|
30
37
|
ViewComponent: React.ComponentType<any>,
|
|
@@ -35,11 +42,12 @@ function composeWithLayouts(
|
|
|
35
42
|
// Start with the page component
|
|
36
43
|
let result = <ViewComponent {...props} />;
|
|
37
44
|
|
|
38
|
-
// Wrap with each layout in
|
|
39
|
-
//
|
|
45
|
+
// Wrap with each layout in REVERSE order (innermost to outermost)
|
|
46
|
+
// This produces the correct nesting: RootLayout > ControllerLayout > Page
|
|
40
47
|
// Pass context to layouts so they can access path, params, etc. for navigation
|
|
41
48
|
// Each layout gets data-layout attribute and children are wrapped in data-outlet
|
|
42
|
-
for (
|
|
49
|
+
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
50
|
+
const { layout: Layout, props: layoutProps } = layouts[i];
|
|
43
51
|
const layoutName = Layout.displayName || Layout.name || 'Layout';
|
|
44
52
|
result = (
|
|
45
53
|
<div data-layout={layoutName}>
|
|
@@ -80,19 +88,28 @@ export function renderComponent(
|
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
/**
|
|
83
|
-
* Render
|
|
84
|
-
*
|
|
91
|
+
* Render a segment for client-side navigation.
|
|
92
|
+
* Includes any layouts below the swap target (e.g., nested layouts).
|
|
93
|
+
* The swap target's outlet will receive this rendered content.
|
|
85
94
|
*/
|
|
86
95
|
export function renderSegment(
|
|
87
96
|
ViewComponent: React.ComponentType<any>,
|
|
88
97
|
data: any,
|
|
89
98
|
) {
|
|
90
|
-
const { data: pageData, __context: context } = data;
|
|
99
|
+
const { data: pageData, __context: context, __layouts: layouts } = data;
|
|
100
|
+
|
|
101
|
+
// Compose with filtered layouts (layouts below the swap target)
|
|
102
|
+
const composedElement = composeWithLayouts(
|
|
103
|
+
ViewComponent,
|
|
104
|
+
pageData,
|
|
105
|
+
layouts,
|
|
106
|
+
context,
|
|
107
|
+
);
|
|
91
108
|
|
|
92
|
-
//
|
|
109
|
+
// Wrap with PageContextProvider to make context available via hooks
|
|
93
110
|
const element = (
|
|
94
111
|
<PageContextProvider context={context}>
|
|
95
|
-
|
|
112
|
+
{composedElement}
|
|
96
113
|
</PageContextProvider>
|
|
97
114
|
);
|
|
98
115
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { H as HeadData, R as RenderContext } from './render-response.interface-
|
|
1
|
+
import { H as HeadData, R as RenderContext } from './render-response.interface-ClWJXKL4.mjs';
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
@@ -85,6 +85,7 @@ declare function updatePageContext(context: RenderContext): void;
|
|
|
85
85
|
*
|
|
86
86
|
* @param isSegment - If true, this is a segment provider (for hydrated segments)
|
|
87
87
|
* and won't register its setter to avoid overwriting the root provider's.
|
|
88
|
+
* However, it will still receive broadcasts when context updates.
|
|
88
89
|
*/
|
|
89
90
|
declare function PageContextProvider({ context: initialContext, children, isSegment, }: {
|
|
90
91
|
context: RenderContext;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { H as HeadData, R as RenderContext } from './render-response.interface-
|
|
1
|
+
import { H as HeadData, R as RenderContext } from './render-response.interface-ClWJXKL4.js';
|
|
2
2
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
3
|
import React from 'react';
|
|
4
4
|
|
|
@@ -85,6 +85,7 @@ declare function updatePageContext(context: RenderContext): void;
|
|
|
85
85
|
*
|
|
86
86
|
* @param isSegment - If true, this is a segment provider (for hydrated segments)
|
|
87
87
|
* and won't register its setter to avoid overwriting the root provider's.
|
|
88
|
+
* However, it will still receive broadcasts when context updates.
|
|
88
89
|
*/
|
|
89
90
|
declare function PageContextProvider({ context: initialContext, children, isSegment, }: {
|
|
90
91
|
context: RenderContext;
|