@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/dist/render/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { Injectable, Logger, Optional, Inject, Global, Module } from '@nestjs/co
|
|
|
2
2
|
import { HttpAdapterHost, APP_INTERCEPTOR, Reflector } from '@nestjs/core';
|
|
3
3
|
import { existsSync, readFileSync } from 'fs';
|
|
4
4
|
import { join, relative } from 'path';
|
|
5
|
-
import
|
|
5
|
+
import { uneval } from 'devalue';
|
|
6
6
|
import escapeHtml from 'escape-html';
|
|
7
7
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
8
8
|
import { createElement } from 'react';
|
|
@@ -98,21 +98,20 @@ var TemplateParserService = class {
|
|
|
98
98
|
/**
|
|
99
99
|
* Build inline script that provides initial state to the client
|
|
100
100
|
*
|
|
101
|
-
* Safely serializes data using
|
|
102
|
-
*
|
|
103
|
-
*
|
|
101
|
+
* Safely serializes data using devalue to avoid XSS vulnerabilities.
|
|
102
|
+
* Devalue is designed specifically for SSR, handling complex types safely
|
|
103
|
+
* while being faster and more secure than alternatives.
|
|
104
104
|
*/
|
|
105
|
-
buildInlineScripts(data, context, componentName) {
|
|
105
|
+
buildInlineScripts(data, context, componentName, layouts) {
|
|
106
|
+
const layoutMetadata = layouts ? layouts.map((l) => ({
|
|
107
|
+
name: l.layout.displayName || l.layout.name || "default",
|
|
108
|
+
props: l.props
|
|
109
|
+
})) : [];
|
|
106
110
|
return `<script>
|
|
107
|
-
window.__INITIAL_STATE__ = ${
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
window.
|
|
111
|
-
isJSON: true
|
|
112
|
-
})};
|
|
113
|
-
window.__COMPONENT_NAME__ = ${serialize(componentName, {
|
|
114
|
-
isJSON: true
|
|
115
|
-
})};
|
|
111
|
+
window.__INITIAL_STATE__ = ${uneval(data)};
|
|
112
|
+
window.__CONTEXT__ = ${uneval(context)};
|
|
113
|
+
window.__COMPONENT_NAME__ = ${uneval(componentName)};
|
|
114
|
+
window.__LAYOUTS__ = ${uneval(layoutMetadata)};
|
|
116
115
|
</script>`;
|
|
117
116
|
}
|
|
118
117
|
/**
|
|
@@ -421,12 +420,14 @@ var RenderService = class _RenderService {
|
|
|
421
420
|
isDevelopment;
|
|
422
421
|
ssrMode;
|
|
423
422
|
entryServerPath;
|
|
424
|
-
|
|
423
|
+
rootLayout = void 0;
|
|
424
|
+
rootLayoutChecked = false;
|
|
425
|
+
constructor(templateParser, streamingErrorHandler, ssrMode, defaultHead, customTemplate) {
|
|
425
426
|
this.templateParser = templateParser;
|
|
426
427
|
this.streamingErrorHandler = streamingErrorHandler;
|
|
427
428
|
this.defaultHead = defaultHead;
|
|
428
429
|
this.isDevelopment = process.env.NODE_ENV !== "production";
|
|
429
|
-
this.ssrMode = ssrMode || process.env.SSR_MODE || "
|
|
430
|
+
this.ssrMode = ssrMode || process.env.SSR_MODE || "stream";
|
|
430
431
|
const absoluteServerPath = join(__dirname, "/templates/entry-server.tsx");
|
|
431
432
|
const relativeServerPath = relative(process.cwd(), absoluteServerPath);
|
|
432
433
|
if (relativeServerPath.startsWith("..")) {
|
|
@@ -434,36 +435,54 @@ var RenderService = class _RenderService {
|
|
|
434
435
|
} else {
|
|
435
436
|
this.entryServerPath = "/" + relativeServerPath.replace(/\\/g, "/");
|
|
436
437
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
join(__dirname, "../src/templates/index.html"),
|
|
442
|
-
join(__dirname, "../../src/templates/index.html")
|
|
443
|
-
];
|
|
444
|
-
const localTemplatePath = join(process.cwd(), "src/views/index.html");
|
|
445
|
-
const foundPackageTemplate = packageTemplatePaths.find((p) => existsSync(p));
|
|
446
|
-
if (foundPackageTemplate) {
|
|
447
|
-
templatePath = foundPackageTemplate;
|
|
448
|
-
} else if (existsSync(localTemplatePath)) {
|
|
449
|
-
templatePath = localTemplatePath;
|
|
438
|
+
if (customTemplate) {
|
|
439
|
+
if (customTemplate.includes("<!DOCTYPE") || customTemplate.includes("<html")) {
|
|
440
|
+
this.template = customTemplate;
|
|
441
|
+
this.logger.log(`\u2713 Loaded custom template (inline)`);
|
|
450
442
|
} else {
|
|
451
|
-
|
|
443
|
+
const customTemplatePath = customTemplate.startsWith("/") ? customTemplate : join(process.cwd(), customTemplate);
|
|
444
|
+
if (!existsSync(customTemplatePath)) {
|
|
445
|
+
throw new Error(`Custom template file not found at ${customTemplatePath}`);
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
this.template = readFileSync(customTemplatePath, "utf-8");
|
|
449
|
+
this.logger.log(`\u2713 Loaded custom template from ${customTemplatePath}`);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
throw new Error(`Failed to read custom template file at ${customTemplatePath}: ${error.message}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
let templatePath;
|
|
456
|
+
if (this.isDevelopment) {
|
|
457
|
+
const packageTemplatePaths = [
|
|
458
|
+
join(__dirname, "../templates/index.html"),
|
|
459
|
+
join(__dirname, "../src/templates/index.html"),
|
|
460
|
+
join(__dirname, "../../src/templates/index.html")
|
|
461
|
+
];
|
|
462
|
+
const localTemplatePath = join(process.cwd(), "src/views/index.html");
|
|
463
|
+
const foundPackageTemplate = packageTemplatePaths.find((p) => existsSync(p));
|
|
464
|
+
if (foundPackageTemplate) {
|
|
465
|
+
templatePath = foundPackageTemplate;
|
|
466
|
+
} else if (existsSync(localTemplatePath)) {
|
|
467
|
+
templatePath = localTemplatePath;
|
|
468
|
+
} else {
|
|
469
|
+
throw new Error(`Template file not found. Tried:
|
|
452
470
|
` + packageTemplatePaths.map((p) => ` - ${p} (package template)`).join("\n") + `
|
|
453
471
|
- ${localTemplatePath} (local template)`);
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
templatePath = join(process.cwd(), "dist/client/index.html");
|
|
475
|
+
if (!existsSync(templatePath)) {
|
|
476
|
+
throw new Error(`Template file not found at ${templatePath}. Make sure to run the build process first.`);
|
|
477
|
+
}
|
|
454
478
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
479
|
+
try {
|
|
480
|
+
this.template = readFileSync(templatePath, "utf-8");
|
|
481
|
+
this.logger.log(`\u2713 Loaded template from ${templatePath}`);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
throw new Error(`Failed to read template file at ${templatePath}: ${error.message}`);
|
|
459
484
|
}
|
|
460
485
|
}
|
|
461
|
-
try {
|
|
462
|
-
this.template = readFileSync(templatePath, "utf-8");
|
|
463
|
-
this.logger.log(`\u2713 Loaded template from ${templatePath}`);
|
|
464
|
-
} catch (error) {
|
|
465
|
-
throw new Error(`Failed to read template file at ${templatePath}: ${error.message}`);
|
|
466
|
-
}
|
|
467
486
|
if (!this.isDevelopment) {
|
|
468
487
|
const manifestPath = join(process.cwd(), "dist/client/.vite/manifest.json");
|
|
469
488
|
if (existsSync(manifestPath)) {
|
|
@@ -483,6 +502,52 @@ var RenderService = class _RenderService {
|
|
|
483
502
|
this.vite = vite;
|
|
484
503
|
}
|
|
485
504
|
/**
|
|
505
|
+
* Get the root layout component if it exists
|
|
506
|
+
* Auto-discovers layout files at conventional paths:
|
|
507
|
+
* - src/views/layout.tsx
|
|
508
|
+
* - src/views/layout/index.tsx
|
|
509
|
+
* - src/views/_layout.tsx
|
|
510
|
+
*/
|
|
511
|
+
async getRootLayout() {
|
|
512
|
+
if (this.rootLayoutChecked) {
|
|
513
|
+
return this.rootLayout;
|
|
514
|
+
}
|
|
515
|
+
this.rootLayoutChecked = true;
|
|
516
|
+
const conventionalPaths = [
|
|
517
|
+
"src/views/layout.tsx",
|
|
518
|
+
"src/views/layout/index.tsx",
|
|
519
|
+
"src/views/_layout.tsx"
|
|
520
|
+
];
|
|
521
|
+
try {
|
|
522
|
+
for (const path of conventionalPaths) {
|
|
523
|
+
const absolutePath = join(process.cwd(), path);
|
|
524
|
+
if (!existsSync(absolutePath)) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
this.logger.log(`\u2713 Found root layout at ${path}`);
|
|
528
|
+
if (this.vite) {
|
|
529
|
+
const layoutModule = await this.vite.ssrLoadModule("/" + path);
|
|
530
|
+
this.rootLayout = layoutModule.default;
|
|
531
|
+
return this.rootLayout;
|
|
532
|
+
} else {
|
|
533
|
+
const prodPath = path.replace("src/views", "dist/server/views").replace(".tsx", ".js");
|
|
534
|
+
const absoluteProdPath = join(process.cwd(), prodPath);
|
|
535
|
+
if (existsSync(absoluteProdPath)) {
|
|
536
|
+
const layoutModule = await import(absoluteProdPath);
|
|
537
|
+
this.rootLayout = layoutModule.default;
|
|
538
|
+
return this.rootLayout;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
this.rootLayout = null;
|
|
543
|
+
return null;
|
|
544
|
+
} catch (error) {
|
|
545
|
+
this.logger.warn(`\u26A0\uFE0F Error loading root layout: ${error.message}`);
|
|
546
|
+
this.rootLayout = null;
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
486
551
|
* Main render method that routes to string or stream mode
|
|
487
552
|
*/
|
|
488
553
|
async render(viewComponent, data = {}, res, head) {
|
|
@@ -544,20 +609,19 @@ var RenderService = class _RenderService {
|
|
|
544
609
|
throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
|
|
545
610
|
}
|
|
546
611
|
}
|
|
547
|
-
const { data: pageData, __context: context } = data;
|
|
612
|
+
const { data: pageData, __context: context, __layouts: layouts } = data;
|
|
548
613
|
const appHtml = await renderModule.renderComponent(viewComponent, data);
|
|
549
614
|
const componentName = viewComponent.displayName || viewComponent.name || "Component";
|
|
615
|
+
const layoutMetadata = layouts ? layouts.map((l) => ({
|
|
616
|
+
name: l.layout.displayName || l.layout.name || "default",
|
|
617
|
+
props: l.props
|
|
618
|
+
})) : [];
|
|
550
619
|
const initialStateScript = `
|
|
551
620
|
<script>
|
|
552
|
-
window.__INITIAL_STATE__ = ${
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
window.
|
|
556
|
-
isJSON: true
|
|
557
|
-
})};
|
|
558
|
-
window.__COMPONENT_NAME__ = ${serialize(componentName, {
|
|
559
|
-
isJSON: true
|
|
560
|
-
})};
|
|
621
|
+
window.__INITIAL_STATE__ = ${uneval(pageData)};
|
|
622
|
+
window.__CONTEXT__ = ${uneval(context)};
|
|
623
|
+
window.__COMPONENT_NAME__ = ${uneval(componentName)};
|
|
624
|
+
window.__LAYOUTS__ = ${uneval(layoutMetadata)};
|
|
561
625
|
</script>
|
|
562
626
|
`;
|
|
563
627
|
let clientScript = "";
|
|
@@ -630,9 +694,9 @@ var RenderService = class _RenderService {
|
|
|
630
694
|
throw new Error("Server bundle not found in manifest. Run `pnpm build:server` to generate the server bundle.");
|
|
631
695
|
}
|
|
632
696
|
}
|
|
633
|
-
const { data: pageData, __context: context } = data;
|
|
697
|
+
const { data: pageData, __context: context, __layouts: layouts } = data;
|
|
634
698
|
const componentName = viewComponent.displayName || viewComponent.name || "Component";
|
|
635
|
-
const inlineScripts = this.templateParser.buildInlineScripts(pageData, context, componentName);
|
|
699
|
+
const inlineScripts = this.templateParser.buildInlineScripts(pageData, context, componentName, layouts);
|
|
636
700
|
const clientScript = this.templateParser.getClientScriptTag(this.isDevelopment, this.manifest);
|
|
637
701
|
const stylesheetTags = this.templateParser.getStylesheetTags(this.isDevelopment, this.manifest);
|
|
638
702
|
const headTags = this.templateParser.buildHeadTags(head);
|
|
@@ -688,15 +752,20 @@ RenderService = _ts_decorate3([
|
|
|
688
752
|
_ts_param2(2, Inject("SSR_MODE")),
|
|
689
753
|
_ts_param2(3, Optional()),
|
|
690
754
|
_ts_param2(3, Inject("DEFAULT_HEAD")),
|
|
755
|
+
_ts_param2(4, Optional()),
|
|
756
|
+
_ts_param2(4, Inject("CUSTOM_TEMPLATE")),
|
|
691
757
|
_ts_metadata2("design:type", Function),
|
|
692
758
|
_ts_metadata2("design:paramtypes", [
|
|
693
759
|
typeof TemplateParserService === "undefined" ? Object : TemplateParserService,
|
|
694
760
|
typeof StreamingErrorHandler === "undefined" ? Object : StreamingErrorHandler,
|
|
695
761
|
typeof SSRMode === "undefined" ? Object : SSRMode,
|
|
696
|
-
typeof HeadData === "undefined" ? Object : HeadData
|
|
762
|
+
typeof HeadData === "undefined" ? Object : HeadData,
|
|
763
|
+
String
|
|
697
764
|
])
|
|
698
765
|
], RenderService);
|
|
699
766
|
var RENDER_KEY = "render";
|
|
767
|
+
var RENDER_OPTIONS_KEY = "render_options";
|
|
768
|
+
var LAYOUT_KEY = "layout";
|
|
700
769
|
|
|
701
770
|
// src/render/render.interceptor.ts
|
|
702
771
|
function _ts_decorate4(decorators, target, key, desc) {
|
|
@@ -710,6 +779,12 @@ function _ts_metadata3(k, v) {
|
|
|
710
779
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
711
780
|
}
|
|
712
781
|
__name(_ts_metadata3, "_ts_metadata");
|
|
782
|
+
function _ts_param3(paramIndex, decorator) {
|
|
783
|
+
return function(target, key) {
|
|
784
|
+
decorator(target, key, paramIndex);
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
__name(_ts_param3, "_ts_param");
|
|
713
788
|
function isRenderResponse(data) {
|
|
714
789
|
return data && typeof data === "object" && "props" in data;
|
|
715
790
|
}
|
|
@@ -720,9 +795,60 @@ var RenderInterceptor = class {
|
|
|
720
795
|
}
|
|
721
796
|
reflector;
|
|
722
797
|
renderService;
|
|
723
|
-
|
|
798
|
+
allowedHeaders;
|
|
799
|
+
allowedCookies;
|
|
800
|
+
constructor(reflector, renderService, allowedHeaders, allowedCookies) {
|
|
724
801
|
this.reflector = reflector;
|
|
725
802
|
this.renderService = renderService;
|
|
803
|
+
this.allowedHeaders = allowedHeaders;
|
|
804
|
+
this.allowedCookies = allowedCookies;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Resolve the layout hierarchy for a given route
|
|
808
|
+
* Hierarchy: Root Layout → Controller Layout → Method Layout → Page
|
|
809
|
+
*
|
|
810
|
+
* Props are merged in priority order:
|
|
811
|
+
* 1. Static props from @Layout decorator (base)
|
|
812
|
+
* 2. Static props from @Render decorator (override)
|
|
813
|
+
* 3. Dynamic props from controller return (final override)
|
|
814
|
+
*/
|
|
815
|
+
async resolveLayoutChain(context, dynamicLayoutProps) {
|
|
816
|
+
const layouts = [];
|
|
817
|
+
const rootLayout = await this.renderService.getRootLayout();
|
|
818
|
+
if (rootLayout) {
|
|
819
|
+
layouts.push({
|
|
820
|
+
layout: rootLayout,
|
|
821
|
+
props: dynamicLayoutProps || {}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
const controllerLayoutMeta = this.reflector.get(LAYOUT_KEY, context.getClass());
|
|
825
|
+
const renderOptions = this.reflector.get(RENDER_OPTIONS_KEY, context.getHandler());
|
|
826
|
+
if (renderOptions?.layout === null) {
|
|
827
|
+
return [];
|
|
828
|
+
} else if (renderOptions?.layout === false) {
|
|
829
|
+
return layouts;
|
|
830
|
+
}
|
|
831
|
+
if (controllerLayoutMeta) {
|
|
832
|
+
const mergedProps = {
|
|
833
|
+
...controllerLayoutMeta.options?.props || {},
|
|
834
|
+
...dynamicLayoutProps || {}
|
|
835
|
+
};
|
|
836
|
+
layouts.push({
|
|
837
|
+
layout: controllerLayoutMeta.layout,
|
|
838
|
+
props: mergedProps
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
if (renderOptions?.layout) {
|
|
842
|
+
const mergedProps = {
|
|
843
|
+
...renderOptions.layoutProps || {},
|
|
844
|
+
...dynamicLayoutProps || {}
|
|
845
|
+
};
|
|
846
|
+
layouts.push({
|
|
847
|
+
layout: renderOptions.layout,
|
|
848
|
+
props: mergedProps
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
return layouts;
|
|
726
852
|
}
|
|
727
853
|
intercept(context, next) {
|
|
728
854
|
const viewPathOrComponent = this.reflector.get(RENDER_KEY, context.getHandler());
|
|
@@ -738,16 +864,36 @@ var RenderInterceptor = class {
|
|
|
738
864
|
path: request.path,
|
|
739
865
|
query: request.query,
|
|
740
866
|
params: request.params,
|
|
741
|
-
|
|
742
|
-
acceptLanguage: request.headers["accept-language"],
|
|
743
|
-
referer: request.headers.referer
|
|
867
|
+
method: request.method
|
|
744
868
|
};
|
|
869
|
+
if (this.allowedHeaders?.length) {
|
|
870
|
+
for (const headerName of this.allowedHeaders) {
|
|
871
|
+
const value = request.headers[headerName.toLowerCase()];
|
|
872
|
+
if (value) {
|
|
873
|
+
renderContext[headerName] = Array.isArray(value) ? value.join(", ") : value;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
if (this.allowedCookies?.length && request.cookies) {
|
|
878
|
+
const cookies = {};
|
|
879
|
+
for (const cookieName of this.allowedCookies) {
|
|
880
|
+
const value = request.cookies[cookieName];
|
|
881
|
+
if (value !== void 0) {
|
|
882
|
+
cookies[cookieName] = value;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (Object.keys(cookies).length > 0) {
|
|
886
|
+
renderContext.cookies = cookies;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
745
889
|
const renderResponse = isRenderResponse(data) ? data : {
|
|
746
890
|
props: data
|
|
747
891
|
};
|
|
892
|
+
const layoutChain = await this.resolveLayoutChain(context, renderResponse.layoutProps);
|
|
748
893
|
const fullData = {
|
|
749
894
|
data: renderResponse.props,
|
|
750
|
-
__context: renderContext
|
|
895
|
+
__context: renderContext,
|
|
896
|
+
__layouts: layoutChain
|
|
751
897
|
};
|
|
752
898
|
try {
|
|
753
899
|
const html = await this.renderService.render(viewPathOrComponent, fullData, response, renderResponse.head);
|
|
@@ -764,10 +910,16 @@ var RenderInterceptor = class {
|
|
|
764
910
|
};
|
|
765
911
|
RenderInterceptor = _ts_decorate4([
|
|
766
912
|
Injectable(),
|
|
913
|
+
_ts_param3(2, Optional()),
|
|
914
|
+
_ts_param3(2, Inject("ALLOWED_HEADERS")),
|
|
915
|
+
_ts_param3(3, Optional()),
|
|
916
|
+
_ts_param3(3, Inject("ALLOWED_COOKIES")),
|
|
767
917
|
_ts_metadata3("design:type", Function),
|
|
768
918
|
_ts_metadata3("design:paramtypes", [
|
|
769
919
|
typeof Reflector === "undefined" ? Object : Reflector,
|
|
770
|
-
typeof RenderService === "undefined" ? Object : RenderService
|
|
920
|
+
typeof RenderService === "undefined" ? Object : RenderService,
|
|
921
|
+
Array,
|
|
922
|
+
Array
|
|
771
923
|
])
|
|
772
924
|
], RenderInterceptor);
|
|
773
925
|
function _ts_decorate5(decorators, target, key, desc) {
|
|
@@ -781,12 +933,12 @@ function _ts_metadata4(k, v) {
|
|
|
781
933
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
782
934
|
}
|
|
783
935
|
__name(_ts_metadata4, "_ts_metadata");
|
|
784
|
-
function
|
|
936
|
+
function _ts_param4(paramIndex, decorator) {
|
|
785
937
|
return function(target, key) {
|
|
786
938
|
decorator(target, key, paramIndex);
|
|
787
939
|
};
|
|
788
940
|
}
|
|
789
|
-
__name(
|
|
941
|
+
__name(_ts_param4, "_ts_param");
|
|
790
942
|
var ViteInitializerService = class _ViteInitializerService {
|
|
791
943
|
static {
|
|
792
944
|
__name(this, "ViteInitializerService");
|
|
@@ -902,8 +1054,8 @@ var ViteInitializerService = class _ViteInitializerService {
|
|
|
902
1054
|
};
|
|
903
1055
|
ViteInitializerService = _ts_decorate5([
|
|
904
1056
|
Injectable(),
|
|
905
|
-
|
|
906
|
-
|
|
1057
|
+
_ts_param4(2, Optional()),
|
|
1058
|
+
_ts_param4(2, Inject("VITE_CONFIG")),
|
|
907
1059
|
_ts_metadata4("design:type", Function),
|
|
908
1060
|
_ts_metadata4("design:paramtypes", [
|
|
909
1061
|
typeof RenderService === "undefined" ? Object : RenderService,
|
|
@@ -996,6 +1148,20 @@ var RenderModule = class _RenderModule {
|
|
|
996
1148
|
useValue: config.defaultHead
|
|
997
1149
|
});
|
|
998
1150
|
}
|
|
1151
|
+
if (config?.template) {
|
|
1152
|
+
providers.push({
|
|
1153
|
+
provide: "CUSTOM_TEMPLATE",
|
|
1154
|
+
useValue: config.template
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
providers.push({
|
|
1158
|
+
provide: "ALLOWED_HEADERS",
|
|
1159
|
+
useValue: config?.allowedHeaders || []
|
|
1160
|
+
});
|
|
1161
|
+
providers.push({
|
|
1162
|
+
provide: "ALLOWED_COOKIES",
|
|
1163
|
+
useValue: config?.allowedCookies || []
|
|
1164
|
+
});
|
|
999
1165
|
return {
|
|
1000
1166
|
global: true,
|
|
1001
1167
|
module: _RenderModule,
|
|
@@ -1088,6 +1254,30 @@ var RenderModule = class _RenderModule {
|
|
|
1088
1254
|
inject: [
|
|
1089
1255
|
"RENDER_CONFIG"
|
|
1090
1256
|
]
|
|
1257
|
+
},
|
|
1258
|
+
// Custom template provider - reads from config
|
|
1259
|
+
{
|
|
1260
|
+
provide: "CUSTOM_TEMPLATE",
|
|
1261
|
+
useFactory: /* @__PURE__ */ __name((config) => config?.template, "useFactory"),
|
|
1262
|
+
inject: [
|
|
1263
|
+
"RENDER_CONFIG"
|
|
1264
|
+
]
|
|
1265
|
+
},
|
|
1266
|
+
// Allowed headers provider - reads from config
|
|
1267
|
+
{
|
|
1268
|
+
provide: "ALLOWED_HEADERS",
|
|
1269
|
+
useFactory: /* @__PURE__ */ __name((config) => config?.allowedHeaders || [], "useFactory"),
|
|
1270
|
+
inject: [
|
|
1271
|
+
"RENDER_CONFIG"
|
|
1272
|
+
]
|
|
1273
|
+
},
|
|
1274
|
+
// Allowed cookies provider - reads from config
|
|
1275
|
+
{
|
|
1276
|
+
provide: "ALLOWED_COOKIES",
|
|
1277
|
+
useFactory: /* @__PURE__ */ __name((config) => config?.allowedCookies || [], "useFactory"),
|
|
1278
|
+
inject: [
|
|
1279
|
+
"RENDER_CONFIG"
|
|
1280
|
+
]
|
|
1091
1281
|
}
|
|
1092
1282
|
];
|
|
1093
1283
|
return {
|
|
@@ -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
|
}
|
|
@@ -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>
|