@nestjs-ssr/react 0.1.12 → 0.2.1

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.
@@ -1,4 +1,4 @@
1
- export { E as ErrorPageDevelopment, f as ErrorPageProduction, b as RenderInterceptor, R as RenderModule, a as RenderService, S as StreamingErrorHandler, T as TemplateParserService } from '../index-BiaVDe9J.mjs';
1
+ export { E as ErrorPageDevelopment, f as ErrorPageProduction, c as RenderInterceptor, a as RenderModule, b as RenderService, S as StreamingErrorHandler, T as TemplateParserService } from '../index-C5Knql-9.mjs';
2
2
  import '@nestjs/common';
3
3
  import 'react';
4
4
  import 'vite';
@@ -1,4 +1,4 @@
1
- export { E as ErrorPageDevelopment, f as ErrorPageProduction, b as RenderInterceptor, R as RenderModule, a as RenderService, S as StreamingErrorHandler, T as TemplateParserService } from '../index-BiaVDe9J.js';
1
+ export { E as ErrorPageDevelopment, f as ErrorPageProduction, c as RenderInterceptor, a as RenderModule, b as RenderService, S as StreamingErrorHandler, T as TemplateParserService } from '../index-C5Knql-9.js';
2
2
  import '@nestjs/common';
3
3
  import 'react';
4
4
  import 'vite';
@@ -4,7 +4,7 @@ var common = require('@nestjs/common');
4
4
  var core = require('@nestjs/core');
5
5
  var fs = require('fs');
6
6
  var path = require('path');
7
- var serialize = require('serialize-javascript');
7
+ var devalue = require('devalue');
8
8
  var escapeHtml = require('escape-html');
9
9
  var server = require('react-dom/server');
10
10
  var react = require('react');
@@ -12,7 +12,6 @@ var operators = require('rxjs/operators');
12
12
 
13
13
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
14
 
15
- var serialize__default = /*#__PURE__*/_interopDefault(serialize);
16
15
  var escapeHtml__default = /*#__PURE__*/_interopDefault(escapeHtml);
17
16
 
18
17
  var __defProp = Object.defineProperty;
@@ -105,21 +104,20 @@ exports.TemplateParserService = class TemplateParserService {
105
104
  /**
106
105
  * Build inline script that provides initial state to the client
107
106
  *
108
- * Safely serializes data using serialize-javascript to avoid XSS vulnerabilities.
109
- * This library handles all edge cases including escaping dangerous characters,
110
- * functions, dates, regexes, and prevents prototype pollution.
107
+ * Safely serializes data using devalue to avoid XSS vulnerabilities.
108
+ * Devalue is designed specifically for SSR, handling complex types safely
109
+ * while being faster and more secure than alternatives.
111
110
  */
112
- buildInlineScripts(data, context, componentName) {
111
+ buildInlineScripts(data, context, componentName, layouts) {
112
+ const layoutMetadata = layouts ? layouts.map((l) => ({
113
+ name: l.layout.displayName || l.layout.name || "default",
114
+ props: l.props
115
+ })) : [];
113
116
  return `<script>
114
- window.__INITIAL_STATE__ = ${serialize__default.default(data, {
115
- isJSON: true
116
- })};
117
- window.__CONTEXT__ = ${serialize__default.default(context, {
118
- isJSON: true
119
- })};
120
- window.__COMPONENT_NAME__ = ${serialize__default.default(componentName, {
121
- isJSON: true
122
- })};
117
+ window.__INITIAL_STATE__ = ${devalue.uneval(data)};
118
+ window.__CONTEXT__ = ${devalue.uneval(context)};
119
+ window.__COMPONENT_NAME__ = ${devalue.uneval(componentName)};
120
+ window.__LAYOUTS__ = ${devalue.uneval(layoutMetadata)};
123
121
  </script>`;
124
122
  }
125
123
  /**
@@ -428,12 +426,14 @@ exports.RenderService = class _RenderService {
428
426
  isDevelopment;
429
427
  ssrMode;
430
428
  entryServerPath;
431
- constructor(templateParser, streamingErrorHandler, ssrMode, defaultHead) {
429
+ rootLayout = void 0;
430
+ rootLayoutChecked = false;
431
+ constructor(templateParser, streamingErrorHandler, ssrMode, defaultHead, customTemplate) {
432
432
  this.templateParser = templateParser;
433
433
  this.streamingErrorHandler = streamingErrorHandler;
434
434
  this.defaultHead = defaultHead;
435
435
  this.isDevelopment = process.env.NODE_ENV !== "production";
436
- this.ssrMode = ssrMode || process.env.SSR_MODE || "string";
436
+ this.ssrMode = ssrMode || process.env.SSR_MODE || "stream";
437
437
  const absoluteServerPath = path.join(__dirname, "/templates/entry-server.tsx");
438
438
  const relativeServerPath = path.relative(process.cwd(), absoluteServerPath);
439
439
  if (relativeServerPath.startsWith("..")) {
@@ -441,36 +441,54 @@ exports.RenderService = class _RenderService {
441
441
  } else {
442
442
  this.entryServerPath = "/" + relativeServerPath.replace(/\\/g, "/");
443
443
  }
444
- let templatePath;
445
- if (this.isDevelopment) {
446
- const packageTemplatePaths = [
447
- path.join(__dirname, "../templates/index.html"),
448
- path.join(__dirname, "../src/templates/index.html"),
449
- path.join(__dirname, "../../src/templates/index.html")
450
- ];
451
- const localTemplatePath = path.join(process.cwd(), "src/views/index.html");
452
- const foundPackageTemplate = packageTemplatePaths.find((p) => fs.existsSync(p));
453
- if (foundPackageTemplate) {
454
- templatePath = foundPackageTemplate;
455
- } else if (fs.existsSync(localTemplatePath)) {
456
- templatePath = localTemplatePath;
444
+ if (customTemplate) {
445
+ if (customTemplate.includes("<!DOCTYPE") || customTemplate.includes("<html")) {
446
+ this.template = customTemplate;
447
+ this.logger.log(`\u2713 Loaded custom template (inline)`);
457
448
  } else {
458
- throw new Error(`Template file not found. Tried:
449
+ const customTemplatePath = customTemplate.startsWith("/") ? customTemplate : path.join(process.cwd(), customTemplate);
450
+ if (!fs.existsSync(customTemplatePath)) {
451
+ throw new Error(`Custom template file not found at ${customTemplatePath}`);
452
+ }
453
+ try {
454
+ this.template = fs.readFileSync(customTemplatePath, "utf-8");
455
+ this.logger.log(`\u2713 Loaded custom template from ${customTemplatePath}`);
456
+ } catch (error) {
457
+ throw new Error(`Failed to read custom template file at ${customTemplatePath}: ${error.message}`);
458
+ }
459
+ }
460
+ } else {
461
+ let templatePath;
462
+ if (this.isDevelopment) {
463
+ const packageTemplatePaths = [
464
+ path.join(__dirname, "../templates/index.html"),
465
+ path.join(__dirname, "../src/templates/index.html"),
466
+ path.join(__dirname, "../../src/templates/index.html")
467
+ ];
468
+ const localTemplatePath = path.join(process.cwd(), "src/views/index.html");
469
+ const foundPackageTemplate = packageTemplatePaths.find((p) => fs.existsSync(p));
470
+ if (foundPackageTemplate) {
471
+ templatePath = foundPackageTemplate;
472
+ } else if (fs.existsSync(localTemplatePath)) {
473
+ templatePath = localTemplatePath;
474
+ } else {
475
+ throw new Error(`Template file not found. Tried:
459
476
  ` + packageTemplatePaths.map((p) => ` - ${p} (package template)`).join("\n") + `
460
477
  - ${localTemplatePath} (local template)`);
478
+ }
479
+ } else {
480
+ templatePath = path.join(process.cwd(), "dist/client/index.html");
481
+ if (!fs.existsSync(templatePath)) {
482
+ throw new Error(`Template file not found at ${templatePath}. Make sure to run the build process first.`);
483
+ }
461
484
  }
462
- } else {
463
- templatePath = path.join(process.cwd(), "dist/client/index.html");
464
- if (!fs.existsSync(templatePath)) {
465
- throw new Error(`Template file not found at ${templatePath}. Make sure to run the build process first.`);
485
+ try {
486
+ this.template = fs.readFileSync(templatePath, "utf-8");
487
+ this.logger.log(`\u2713 Loaded template from ${templatePath}`);
488
+ } catch (error) {
489
+ throw new Error(`Failed to read template file at ${templatePath}: ${error.message}`);
466
490
  }
467
491
  }
468
- try {
469
- this.template = fs.readFileSync(templatePath, "utf-8");
470
- this.logger.log(`\u2713 Loaded template from ${templatePath}`);
471
- } catch (error) {
472
- throw new Error(`Failed to read template file at ${templatePath}: ${error.message}`);
473
- }
474
492
  if (!this.isDevelopment) {
475
493
  const manifestPath = path.join(process.cwd(), "dist/client/.vite/manifest.json");
476
494
  if (fs.existsSync(manifestPath)) {
@@ -490,6 +508,52 @@ exports.RenderService = class _RenderService {
490
508
  this.vite = vite;
491
509
  }
492
510
  /**
511
+ * Get the root layout component if it exists
512
+ * Auto-discovers layout files at conventional paths:
513
+ * - src/views/layout.tsx
514
+ * - src/views/layout/index.tsx
515
+ * - src/views/_layout.tsx
516
+ */
517
+ async getRootLayout() {
518
+ if (this.rootLayoutChecked) {
519
+ return this.rootLayout;
520
+ }
521
+ this.rootLayoutChecked = true;
522
+ const conventionalPaths = [
523
+ "src/views/layout.tsx",
524
+ "src/views/layout/index.tsx",
525
+ "src/views/_layout.tsx"
526
+ ];
527
+ try {
528
+ for (const path$1 of conventionalPaths) {
529
+ const absolutePath = path.join(process.cwd(), path$1);
530
+ if (!fs.existsSync(absolutePath)) {
531
+ continue;
532
+ }
533
+ this.logger.log(`\u2713 Found root layout at ${path$1}`);
534
+ if (this.vite) {
535
+ const layoutModule = await this.vite.ssrLoadModule("/" + path$1);
536
+ this.rootLayout = layoutModule.default;
537
+ return this.rootLayout;
538
+ } else {
539
+ const prodPath = path$1.replace("src/views", "dist/server/views").replace(".tsx", ".js");
540
+ const absoluteProdPath = path.join(process.cwd(), prodPath);
541
+ if (fs.existsSync(absoluteProdPath)) {
542
+ const layoutModule = await import(absoluteProdPath);
543
+ this.rootLayout = layoutModule.default;
544
+ return this.rootLayout;
545
+ }
546
+ }
547
+ }
548
+ this.rootLayout = null;
549
+ return null;
550
+ } catch (error) {
551
+ this.logger.warn(`\u26A0\uFE0F Error loading root layout: ${error.message}`);
552
+ this.rootLayout = null;
553
+ return null;
554
+ }
555
+ }
556
+ /**
493
557
  * Main render method that routes to string or stream mode
494
558
  */
495
559
  async render(viewComponent, data = {}, res, head) {
@@ -551,20 +615,19 @@ exports.RenderService = class _RenderService {
551
615
  throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
552
616
  }
553
617
  }
554
- const { data: pageData, __context: context } = data;
618
+ const { data: pageData, __context: context, __layouts: layouts } = data;
555
619
  const appHtml = await renderModule.renderComponent(viewComponent, data);
556
620
  const componentName = viewComponent.displayName || viewComponent.name || "Component";
621
+ const layoutMetadata = layouts ? layouts.map((l) => ({
622
+ name: l.layout.displayName || l.layout.name || "default",
623
+ props: l.props
624
+ })) : [];
557
625
  const initialStateScript = `
558
626
  <script>
559
- window.__INITIAL_STATE__ = ${serialize__default.default(pageData, {
560
- isJSON: true
561
- })};
562
- window.__CONTEXT__ = ${serialize__default.default(context, {
563
- isJSON: true
564
- })};
565
- window.__COMPONENT_NAME__ = ${serialize__default.default(componentName, {
566
- isJSON: true
567
- })};
627
+ window.__INITIAL_STATE__ = ${devalue.uneval(pageData)};
628
+ window.__CONTEXT__ = ${devalue.uneval(context)};
629
+ window.__COMPONENT_NAME__ = ${devalue.uneval(componentName)};
630
+ window.__LAYOUTS__ = ${devalue.uneval(layoutMetadata)};
568
631
  </script>
569
632
  `;
570
633
  let clientScript = "";
@@ -637,9 +700,9 @@ exports.RenderService = class _RenderService {
637
700
  throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
638
701
  }
639
702
  }
640
- const { data: pageData, __context: context } = data;
703
+ const { data: pageData, __context: context, __layouts: layouts } = data;
641
704
  const componentName = viewComponent.displayName || viewComponent.name || "Component";
642
- const inlineScripts = this.templateParser.buildInlineScripts(pageData, context, componentName);
705
+ const inlineScripts = this.templateParser.buildInlineScripts(pageData, context, componentName, layouts);
643
706
  const clientScript = this.templateParser.getClientScriptTag(this.isDevelopment, this.manifest);
644
707
  const stylesheetTags = this.templateParser.getStylesheetTags(this.isDevelopment, this.manifest);
645
708
  const headTags = this.templateParser.buildHeadTags(head);
@@ -695,15 +758,20 @@ exports.RenderService = _ts_decorate3([
695
758
  _ts_param2(2, common.Inject("SSR_MODE")),
696
759
  _ts_param2(3, common.Optional()),
697
760
  _ts_param2(3, common.Inject("DEFAULT_HEAD")),
761
+ _ts_param2(4, common.Optional()),
762
+ _ts_param2(4, common.Inject("CUSTOM_TEMPLATE")),
698
763
  _ts_metadata2("design:type", Function),
699
764
  _ts_metadata2("design:paramtypes", [
700
765
  typeof exports.TemplateParserService === "undefined" ? Object : exports.TemplateParserService,
701
766
  typeof exports.StreamingErrorHandler === "undefined" ? Object : exports.StreamingErrorHandler,
702
767
  typeof SSRMode === "undefined" ? Object : SSRMode,
703
- typeof HeadData === "undefined" ? Object : HeadData
768
+ typeof HeadData === "undefined" ? Object : HeadData,
769
+ String
704
770
  ])
705
771
  ], exports.RenderService);
706
772
  var RENDER_KEY = "render";
773
+ var RENDER_OPTIONS_KEY = "render_options";
774
+ var LAYOUT_KEY = "layout";
707
775
 
708
776
  // src/render/render.interceptor.ts
709
777
  function _ts_decorate4(decorators, target, key, desc) {
@@ -717,6 +785,12 @@ function _ts_metadata3(k, v) {
717
785
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
718
786
  }
719
787
  __name(_ts_metadata3, "_ts_metadata");
788
+ function _ts_param3(paramIndex, decorator) {
789
+ return function(target, key) {
790
+ decorator(target, key, paramIndex);
791
+ };
792
+ }
793
+ __name(_ts_param3, "_ts_param");
720
794
  function isRenderResponse(data) {
721
795
  return data && typeof data === "object" && "props" in data;
722
796
  }
@@ -727,9 +801,60 @@ exports.RenderInterceptor = class RenderInterceptor {
727
801
  }
728
802
  reflector;
729
803
  renderService;
730
- constructor(reflector, renderService) {
804
+ allowedHeaders;
805
+ allowedCookies;
806
+ constructor(reflector, renderService, allowedHeaders, allowedCookies) {
731
807
  this.reflector = reflector;
732
808
  this.renderService = renderService;
809
+ this.allowedHeaders = allowedHeaders;
810
+ this.allowedCookies = allowedCookies;
811
+ }
812
+ /**
813
+ * Resolve the layout hierarchy for a given route
814
+ * Hierarchy: Root Layout → Controller Layout → Method Layout → Page
815
+ *
816
+ * Props are merged in priority order:
817
+ * 1. Static props from @Layout decorator (base)
818
+ * 2. Static props from @Render decorator (override)
819
+ * 3. Dynamic props from controller return (final override)
820
+ */
821
+ async resolveLayoutChain(context, dynamicLayoutProps) {
822
+ const layouts = [];
823
+ const rootLayout = await this.renderService.getRootLayout();
824
+ if (rootLayout) {
825
+ layouts.push({
826
+ layout: rootLayout,
827
+ props: dynamicLayoutProps || {}
828
+ });
829
+ }
830
+ const controllerLayoutMeta = this.reflector.get(LAYOUT_KEY, context.getClass());
831
+ const renderOptions = this.reflector.get(RENDER_OPTIONS_KEY, context.getHandler());
832
+ if (renderOptions?.layout === null) {
833
+ return [];
834
+ } else if (renderOptions?.layout === false) {
835
+ return layouts;
836
+ }
837
+ if (controllerLayoutMeta) {
838
+ const mergedProps = {
839
+ ...controllerLayoutMeta.options?.props || {},
840
+ ...dynamicLayoutProps || {}
841
+ };
842
+ layouts.push({
843
+ layout: controllerLayoutMeta.layout,
844
+ props: mergedProps
845
+ });
846
+ }
847
+ if (renderOptions?.layout) {
848
+ const mergedProps = {
849
+ ...renderOptions.layoutProps || {},
850
+ ...dynamicLayoutProps || {}
851
+ };
852
+ layouts.push({
853
+ layout: renderOptions.layout,
854
+ props: mergedProps
855
+ });
856
+ }
857
+ return layouts;
733
858
  }
734
859
  intercept(context, next) {
735
860
  const viewPathOrComponent = this.reflector.get(RENDER_KEY, context.getHandler());
@@ -745,16 +870,36 @@ exports.RenderInterceptor = class RenderInterceptor {
745
870
  path: request.path,
746
871
  query: request.query,
747
872
  params: request.params,
748
- userAgent: request.headers["user-agent"],
749
- acceptLanguage: request.headers["accept-language"],
750
- referer: request.headers.referer
873
+ method: request.method
751
874
  };
875
+ if (this.allowedHeaders?.length) {
876
+ for (const headerName of this.allowedHeaders) {
877
+ const value = request.headers[headerName.toLowerCase()];
878
+ if (value) {
879
+ renderContext[headerName] = Array.isArray(value) ? value.join(", ") : value;
880
+ }
881
+ }
882
+ }
883
+ if (this.allowedCookies?.length && request.cookies) {
884
+ const cookies = {};
885
+ for (const cookieName of this.allowedCookies) {
886
+ const value = request.cookies[cookieName];
887
+ if (value !== void 0) {
888
+ cookies[cookieName] = value;
889
+ }
890
+ }
891
+ if (Object.keys(cookies).length > 0) {
892
+ renderContext.cookies = cookies;
893
+ }
894
+ }
752
895
  const renderResponse = isRenderResponse(data) ? data : {
753
896
  props: data
754
897
  };
898
+ const layoutChain = await this.resolveLayoutChain(context, renderResponse.layoutProps);
755
899
  const fullData = {
756
900
  data: renderResponse.props,
757
- __context: renderContext
901
+ __context: renderContext,
902
+ __layouts: layoutChain
758
903
  };
759
904
  try {
760
905
  const html = await this.renderService.render(viewPathOrComponent, fullData, response, renderResponse.head);
@@ -771,10 +916,16 @@ exports.RenderInterceptor = class RenderInterceptor {
771
916
  };
772
917
  exports.RenderInterceptor = _ts_decorate4([
773
918
  common.Injectable(),
919
+ _ts_param3(2, common.Optional()),
920
+ _ts_param3(2, common.Inject("ALLOWED_HEADERS")),
921
+ _ts_param3(3, common.Optional()),
922
+ _ts_param3(3, common.Inject("ALLOWED_COOKIES")),
774
923
  _ts_metadata3("design:type", Function),
775
924
  _ts_metadata3("design:paramtypes", [
776
925
  typeof core.Reflector === "undefined" ? Object : core.Reflector,
777
- typeof exports.RenderService === "undefined" ? Object : exports.RenderService
926
+ typeof exports.RenderService === "undefined" ? Object : exports.RenderService,
927
+ Array,
928
+ Array
778
929
  ])
779
930
  ], exports.RenderInterceptor);
780
931
  function _ts_decorate5(decorators, target, key, desc) {
@@ -788,12 +939,12 @@ function _ts_metadata4(k, v) {
788
939
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
789
940
  }
790
941
  __name(_ts_metadata4, "_ts_metadata");
791
- function _ts_param3(paramIndex, decorator) {
942
+ function _ts_param4(paramIndex, decorator) {
792
943
  return function(target, key) {
793
944
  decorator(target, key, paramIndex);
794
945
  };
795
946
  }
796
- __name(_ts_param3, "_ts_param");
947
+ __name(_ts_param4, "_ts_param");
797
948
  var ViteInitializerService = class _ViteInitializerService {
798
949
  static {
799
950
  __name(this, "ViteInitializerService");
@@ -909,8 +1060,8 @@ var ViteInitializerService = class _ViteInitializerService {
909
1060
  };
910
1061
  ViteInitializerService = _ts_decorate5([
911
1062
  common.Injectable(),
912
- _ts_param3(2, common.Optional()),
913
- _ts_param3(2, common.Inject("VITE_CONFIG")),
1063
+ _ts_param4(2, common.Optional()),
1064
+ _ts_param4(2, common.Inject("VITE_CONFIG")),
914
1065
  _ts_metadata4("design:type", Function),
915
1066
  _ts_metadata4("design:paramtypes", [
916
1067
  typeof exports.RenderService === "undefined" ? Object : exports.RenderService,
@@ -1003,6 +1154,20 @@ exports.RenderModule = class _RenderModule {
1003
1154
  useValue: config.defaultHead
1004
1155
  });
1005
1156
  }
1157
+ if (config?.template) {
1158
+ providers.push({
1159
+ provide: "CUSTOM_TEMPLATE",
1160
+ useValue: config.template
1161
+ });
1162
+ }
1163
+ providers.push({
1164
+ provide: "ALLOWED_HEADERS",
1165
+ useValue: config?.allowedHeaders || []
1166
+ });
1167
+ providers.push({
1168
+ provide: "ALLOWED_COOKIES",
1169
+ useValue: config?.allowedCookies || []
1170
+ });
1006
1171
  return {
1007
1172
  global: true,
1008
1173
  module: _RenderModule,
@@ -1095,6 +1260,30 @@ exports.RenderModule = class _RenderModule {
1095
1260
  inject: [
1096
1261
  "RENDER_CONFIG"
1097
1262
  ]
1263
+ },
1264
+ // Custom template provider - reads from config
1265
+ {
1266
+ provide: "CUSTOM_TEMPLATE",
1267
+ useFactory: /* @__PURE__ */ __name((config) => config?.template, "useFactory"),
1268
+ inject: [
1269
+ "RENDER_CONFIG"
1270
+ ]
1271
+ },
1272
+ // Allowed headers provider - reads from config
1273
+ {
1274
+ provide: "ALLOWED_HEADERS",
1275
+ useFactory: /* @__PURE__ */ __name((config) => config?.allowedHeaders || [], "useFactory"),
1276
+ inject: [
1277
+ "RENDER_CONFIG"
1278
+ ]
1279
+ },
1280
+ // Allowed cookies provider - reads from config
1281
+ {
1282
+ provide: "ALLOWED_COOKIES",
1283
+ useFactory: /* @__PURE__ */ __name((config) => config?.allowedCookies || [], "useFactory"),
1284
+ inject: [
1285
+ "RENDER_CONFIG"
1286
+ ]
1098
1287
  }
1099
1288
  ];
1100
1289
  return {