@tramvai/module-render 2.20.0 → 2.21.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.
package/lib/server.es.js CHANGED
@@ -824,24 +824,38 @@ RenderModule = RenderModule_1 = __decorate([
824
824
  html = await htmlBuilder.flow();
825
825
  }
826
826
  catch (error) {
827
- const requestInfo = {
828
- ip: requestManager.getClientIp(),
829
- requestId: requestManager.getHeader('x-request-id'),
830
- url: requestManager.getUrl(),
831
- };
832
- log.error({
833
- event: 'send-server-error',
834
- message: 'Page render error, switch to fallback',
835
- error,
836
- requestInfo,
837
- });
838
- // Assuming that there was an error when rendering the page, try to render again with ErrorBoundary
839
- context.dispatch(setPageErrorEvent(error));
840
- html = await htmlBuilder.flow();
827
+ // assuming that there was an error when rendering the page, try to render again with ErrorBoundary
828
+ try {
829
+ log.info({ event: 'render-page-boundary-start' });
830
+ context.dispatch(setPageErrorEvent(error));
831
+ html = await htmlBuilder.flow();
832
+ log.info({ event: 'render-page-boundary-success' });
833
+ }
834
+ catch (e) {
835
+ log.warn({ event: 'render-page-boundary-error', error: e });
836
+ // pass page render error to default error handler,
837
+ // send-server-error event will be logged with this error
838
+ throw error;
839
+ }
841
840
  }
842
841
  const pageRenderError = context.getState(PageErrorStore);
842
+ // log send-server-error only after successful Page Boundary render,
843
+ // otherwise this event will be logged in default error handler
843
844
  if (pageRenderError) {
844
845
  const status = pageRenderError.status || pageRenderError.httpStatus || 500;
846
+ if (status >= 500) {
847
+ const requestInfo = {
848
+ ip: requestManager.getClientIp(),
849
+ requestId: requestManager.getHeader('x-request-id'),
850
+ url: requestManager.getUrl(),
851
+ };
852
+ log.error({
853
+ event: 'send-server-error',
854
+ message: 'Page render error, switch to page boundary',
855
+ error: deserializeError(pageRenderError),
856
+ requestInfo,
857
+ });
858
+ }
845
859
  responseManager.setStatus(status);
846
860
  }
847
861
  // Проставляем не кэширующие заголовки
@@ -940,9 +954,26 @@ RenderModule = RenderModule_1 = __decorate([
940
954
  }
941
955
  let body;
942
956
  try {
943
- log.info({ event: 'render-root-boundary' });
957
+ log.info({ event: 'render-root-boundary-start' });
944
958
  body = renderToString(createElement(RootErrorBoundary, { error, url: parse(request.url) }));
945
- reply.status(error.httpStatus || error.status || 500);
959
+ log.info({ event: 'render-root-boundary-success' });
960
+ const status = error.status || error.httpStatus || 500;
961
+ // log send-server-error only after successful Root Boundary render,
962
+ // otherwise this event will be logged in default error handler
963
+ if (status >= 500) {
964
+ const requestInfo = {
965
+ ip: request.headers['x-real-ip'],
966
+ requestId: request.headers['x-request-id'],
967
+ url: request.url,
968
+ };
969
+ log.error({
970
+ event: 'send-server-error',
971
+ message: 'Page render error, switch to root boundary',
972
+ error,
973
+ requestInfo,
974
+ });
975
+ }
976
+ reply.status(status);
946
977
  reply.header('Content-Type', 'text/html; charset=utf-8');
947
978
  reply.header('Content-Length', Buffer.byteLength(body, 'utf8'));
948
979
  reply.header('Cache-Control', 'no-cache, no-store, must-revalidate');
package/lib/server.js CHANGED
@@ -860,24 +860,38 @@ exports.RenderModule = RenderModule_1 = tslib.__decorate([
860
860
  html = await htmlBuilder.flow();
861
861
  }
862
862
  catch (error) {
863
- const requestInfo = {
864
- ip: requestManager.getClientIp(),
865
- requestId: requestManager.getHeader('x-request-id'),
866
- url: requestManager.getUrl(),
867
- };
868
- log.error({
869
- event: 'send-server-error',
870
- message: 'Page render error, switch to fallback',
871
- error,
872
- requestInfo,
873
- });
874
- // Assuming that there was an error when rendering the page, try to render again with ErrorBoundary
875
- context.dispatch(setPageErrorEvent(error));
876
- html = await htmlBuilder.flow();
863
+ // assuming that there was an error when rendering the page, try to render again with ErrorBoundary
864
+ try {
865
+ log.info({ event: 'render-page-boundary-start' });
866
+ context.dispatch(setPageErrorEvent(error));
867
+ html = await htmlBuilder.flow();
868
+ log.info({ event: 'render-page-boundary-success' });
869
+ }
870
+ catch (e) {
871
+ log.warn({ event: 'render-page-boundary-error', error: e });
872
+ // pass page render error to default error handler,
873
+ // send-server-error event will be logged with this error
874
+ throw error;
875
+ }
877
876
  }
878
877
  const pageRenderError = context.getState(PageErrorStore);
878
+ // log send-server-error only after successful Page Boundary render,
879
+ // otherwise this event will be logged in default error handler
879
880
  if (pageRenderError) {
880
881
  const status = pageRenderError.status || pageRenderError.httpStatus || 500;
882
+ if (status >= 500) {
883
+ const requestInfo = {
884
+ ip: requestManager.getClientIp(),
885
+ requestId: requestManager.getHeader('x-request-id'),
886
+ url: requestManager.getUrl(),
887
+ };
888
+ log.error({
889
+ event: 'send-server-error',
890
+ message: 'Page render error, switch to page boundary',
891
+ error: deserializeError(pageRenderError),
892
+ requestInfo,
893
+ });
894
+ }
881
895
  responseManager.setStatus(status);
882
896
  }
883
897
  // Проставляем не кэширующие заголовки
@@ -976,9 +990,26 @@ exports.RenderModule = RenderModule_1 = tslib.__decorate([
976
990
  }
977
991
  let body;
978
992
  try {
979
- log.info({ event: 'render-root-boundary' });
993
+ log.info({ event: 'render-root-boundary-start' });
980
994
  body = server$1.renderToString(react.createElement(RootErrorBoundary, { error, url: url.parse(request.url) }));
981
- reply.status(error.httpStatus || error.status || 500);
995
+ log.info({ event: 'render-root-boundary-success' });
996
+ const status = error.status || error.httpStatus || 500;
997
+ // log send-server-error only after successful Root Boundary render,
998
+ // otherwise this event will be logged in default error handler
999
+ if (status >= 500) {
1000
+ const requestInfo = {
1001
+ ip: request.headers['x-real-ip'],
1002
+ requestId: request.headers['x-request-id'],
1003
+ url: request.url,
1004
+ };
1005
+ log.error({
1006
+ event: 'send-server-error',
1007
+ message: 'Page render error, switch to root boundary',
1008
+ error,
1009
+ requestInfo,
1010
+ });
1011
+ }
1012
+ reply.status(status);
982
1013
  reply.header('Content-Type', 'text/html; charset=utf-8');
983
1014
  reply.header('Content-Length', Buffer.byteLength(body, 'utf8'));
984
1015
  reply.header('Cache-Control', 'no-cache, no-store, must-revalidate');
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@tramvai/module-render",
3
- "version": "2.20.0",
3
+ "version": "2.21.1",
4
4
  "description": "",
5
5
  "browser": "lib/browser.js",
6
6
  "main": "lib/server.js",
7
7
  "typings": "lib/server.d.ts",
8
8
  "files": [
9
9
  "lib",
10
- "__migrations__"
10
+ "__migrations__",
11
+ "tests.js",
12
+ "tests.d.ts"
11
13
  ],
12
14
  "sideEffects": false,
13
15
  "repository": {
@@ -24,13 +26,13 @@
24
26
  "@tinkoff/htmlpagebuilder": "0.5.2",
25
27
  "@tinkoff/layout-factory": "0.3.2",
26
28
  "@tinkoff/url": "0.8.2",
27
- "@tinkoff/user-agent": "0.4.51",
28
- "@tramvai/module-client-hints": "2.20.0",
29
- "@tramvai/module-router": "2.20.0",
30
- "@tramvai/react": "2.20.0",
29
+ "@tinkoff/user-agent": "0.4.54",
30
+ "@tramvai/module-client-hints": "2.21.1",
31
+ "@tramvai/module-router": "2.21.1",
32
+ "@tramvai/react": "2.21.1",
31
33
  "@tramvai/safe-strings": "0.5.2",
32
- "@tramvai/tokens-render": "2.20.0",
33
- "@tramvai/experiments": "2.20.0",
34
+ "@tramvai/tokens-render": "2.21.1",
35
+ "@tramvai/experiments": "2.21.1",
34
36
  "@types/loadable__server": "^5.12.6",
35
37
  "node-fetch": "^2.6.1"
36
38
  },
@@ -38,14 +40,14 @@
38
40
  "@tinkoff/dippy": "0.8.2",
39
41
  "@tinkoff/utils": "^2.1.2",
40
42
  "@tinkoff/react-hooks": "0.1.2",
41
- "@tramvai/cli": "2.20.0",
42
- "@tramvai/core": "2.20.0",
43
- "@tramvai/module-common": "2.20.0",
44
- "@tramvai/state": "2.20.0",
45
- "@tramvai/test-helpers": "2.20.0",
46
- "@tramvai/tokens-common": "2.20.0",
47
- "@tramvai/tokens-router": "2.20.0",
48
- "@tramvai/tokens-server-private": "2.20.0",
43
+ "@tramvai/cli": "2.21.1",
44
+ "@tramvai/core": "2.21.1",
45
+ "@tramvai/module-common": "2.21.1",
46
+ "@tramvai/state": "2.21.1",
47
+ "@tramvai/test-helpers": "2.21.1",
48
+ "@tramvai/tokens-common": "2.21.1",
49
+ "@tramvai/tokens-router": "2.21.1",
50
+ "@tramvai/tokens-server-private": "2.21.1",
49
51
  "express": "^4.17.1",
50
52
  "prop-types": "^15.6.2",
51
53
  "react": ">=16.14.0",
package/tests.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { getDiWrapper } from "@tramvai/test-helpers";
2
+ type Options = Parameters<typeof getDiWrapper>[0];
3
+ declare const testPageResources: (options: Options) => {
4
+ render: () => {
5
+ parsed: import("node-html-parser").HTMLElement;
6
+ body: string;
7
+ head: string;
8
+ application: string;
9
+ };
10
+ di: import("@tinkoff/dippy").Container;
11
+ runLine: (line: import("@tinkoff/dippy").MultiTokenInterface<import("@tramvai/core").Command>) => Promise<any[]>;
12
+ };
13
+ export { testPageResources };
package/tests.js ADDED
@@ -0,0 +1,267 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var flatten = require('@tinkoff/utils/array/flatten');
6
+ var testHelpers = require('@tramvai/test-helpers');
7
+ var core = require('@tramvai/core');
8
+ var tokensRender = require('@tramvai/tokens-render');
9
+ var htmlpagebuilder = require('@tinkoff/htmlpagebuilder');
10
+ var toArray = require('@tinkoff/utils/array/toArray');
11
+ require('@tinkoff/utils/is/undefined');
12
+ require('@tinkoff/utils/is/empty');
13
+ require('@tinkoff/url');
14
+ require('node-fetch');
15
+ require('@tinkoff/utils/string/startsWith');
16
+ var dippy = require('@tinkoff/dippy');
17
+ require('@tramvai/safe-strings');
18
+ require('@loadable/server');
19
+ require('@tinkoff/utils/object/has');
20
+ require('@tinkoff/utils/array/last');
21
+ require('@tramvai/experiments');
22
+ require('@tinkoff/utils/array/uniq');
23
+ var path = require('path');
24
+ require('@tinkoff/utils/array/each');
25
+ require('@tinkoff/utils/object/path');
26
+
27
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
28
+
29
+ function _interopNamespace(e) {
30
+ if (e && e.__esModule) return e;
31
+ var n = Object.create(null);
32
+ if (e) {
33
+ Object.keys(e).forEach(function (k) {
34
+ if (k !== 'default') {
35
+ var d = Object.getOwnPropertyDescriptor(e, k);
36
+ Object.defineProperty(n, k, d.get ? d : {
37
+ enumerable: true,
38
+ get: function () { return e[k]; }
39
+ });
40
+ }
41
+ });
42
+ }
43
+ n["default"] = e;
44
+ return n;
45
+ }
46
+
47
+ var flatten__default = /*#__PURE__*/_interopDefaultLegacy(flatten);
48
+ var toArray__default = /*#__PURE__*/_interopDefaultLegacy(toArray);
49
+ var path__namespace = /*#__PURE__*/_interopNamespace(path);
50
+
51
+ class ResourcesRegistry {
52
+ constructor({ resourceInliner }) {
53
+ this.resources = new Set();
54
+ this.resourceInliner = resourceInliner;
55
+ }
56
+ register(resourceOrResources) {
57
+ toArray__default["default"](resourceOrResources).forEach((resource) => {
58
+ this.resources.add(resource);
59
+ });
60
+ }
61
+ getPageResources() {
62
+ return Array.from(this.resources.values())
63
+ .reduce((acc, resource) => {
64
+ if (this.resourceInliner.shouldInline(resource)) {
65
+ Array.prototype.push.apply(acc, this.resourceInliner.inlineResource(resource));
66
+ }
67
+ else {
68
+ acc.push(resource);
69
+ }
70
+ return acc;
71
+ }, [])
72
+ .filter((resource) => this.resourceInliner.shouldAddResource(resource));
73
+ }
74
+ }
75
+
76
+ process.env.NODE_ENV === 'development' &&
77
+ (process.env.ASSETS_PREFIX === 'static' || !process.env.ASSETS_PREFIX)
78
+ ? `http://localhost:${process.env.PORT_STATIC}/dist/`
79
+ : process.env.ASSETS_PREFIX;
80
+
81
+ /**
82
+ * @description
83
+ * Инлайнер ресурсов - используется на сервере для регистрации файлов, которые должны быть вставлены
84
+ * в итоговую html-страницу в виде ссылки на файл или заинлайнеными полностью
85
+ */
86
+ const RESOURCE_INLINER = dippy.createToken('resourceInliner');
87
+ /**
88
+ * @description
89
+ * Кэш загруженных ресурсов.
90
+ */
91
+ dippy.createToken('resourcesRegistryCache');
92
+
93
+ const requireFunc =
94
+ // @ts-ignore
95
+ typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require;
96
+
97
+ let appConfig;
98
+ try {
99
+ appConfig = require('@tramvai/cli/lib/external/config').appConfig;
100
+ }
101
+ catch (e) { }
102
+ if (process.env.NODE_ENV === 'development') ;
103
+ if (process.env.NODE_ENV === 'test') ;
104
+ if (process.env.NODE_ENV === 'production') {
105
+ const SEARCH_PATHS = [process.cwd(), __dirname];
106
+ const webpackStats = (fileName) => {
107
+ let stats;
108
+ for (const dir of SEARCH_PATHS) {
109
+ try {
110
+ const statsPath = path__namespace.resolve(dir, fileName);
111
+ stats = requireFunc(statsPath);
112
+ break;
113
+ }
114
+ catch (e) {
115
+ // ignore errors as this function is used to load stats for several optional destinations
116
+ // and these destinations may not have stats file
117
+ }
118
+ }
119
+ if (!stats) {
120
+ return;
121
+ }
122
+ if (!process.env.ASSETS_PREFIX) {
123
+ if (process.env.STATIC_PREFIX) {
124
+ throw new Error('Required env variable "ASSETS_PREFIX" is not set. Instead of using "STATIC_PREFIX" env please define "ASSETS_PREFIX: STATIC_PREFIX + /compiled"');
125
+ }
126
+ throw new Error('Required env variable "ASSETS_PREFIX" is not set');
127
+ }
128
+ return {
129
+ ...stats,
130
+ publicPath: process.env.ASSETS_PREFIX,
131
+ };
132
+ };
133
+ const statsLegacy = webpackStats('stats.json');
134
+ webpackStats('stats.modern.json') || statsLegacy;
135
+ if (!statsLegacy) {
136
+ throw new Error(`Cannot find stats.json.
137
+ It should be placed in one of the next places:
138
+ ${SEARCH_PATHS.join('\n\t')}
139
+ In case it happens on deployment:
140
+ - In case you are using two independent jobs for building app
141
+ - Either do not split build command by two independent jobs and use one common job with "tramvai build" command without --buildType
142
+ - Or copy stats.json (and stats.modern.json if present) file from client build output to server output by yourself in your CI
143
+ - Otherwise report issue to tramvai team
144
+ In case it happens locally:
145
+ - prefer to use command "tramvai start-prod" to test prod-build locally
146
+ - copy stats.json next to built server.js file
147
+ `);
148
+ }
149
+ }
150
+
151
+ const formatAttributes = (htmlAttrs, target) => {
152
+ if (!htmlAttrs) {
153
+ return '';
154
+ }
155
+ const targetAttrs = htmlAttrs.filter((item) => item.target === target);
156
+ const collectedAttrs = targetAttrs.reduce((acc, item) => ({ ...acc, ...item.attrs }), {});
157
+ const attrsString = Object.keys(collectedAttrs).reduce((acc, name) => {
158
+ if (collectedAttrs[name] === true) {
159
+ return `${acc} ${name}`;
160
+ }
161
+ return `${acc} ${name}="${collectedAttrs[name]}"`;
162
+ }, '');
163
+ return attrsString.trim();
164
+ };
165
+
166
+ /* eslint-disable sort-class-members/sort-class-members */
167
+ const mapResourcesToSlots = (resources) => resources.reduce((acc, resource) => {
168
+ const { slot } = resource;
169
+ acc[slot] = Array.isArray(acc[slot]) ? [...acc[slot], resource] : [resource];
170
+ return acc;
171
+ }, {});
172
+ /* eslint-enable sort-class-members/sort-class-members */
173
+
174
+ const { REACT_RENDER, HEAD_CORE_SCRIPTS, HEAD_DYNAMIC_SCRIPTS, HEAD_META, HEAD_POLYFILLS, HEAD_CORE_STYLES, HEAD_PERFORMANCE, HEAD_ANALYTICS, BODY_START, BODY_END, HEAD_ICONS, BODY_TAIL_ANALYTICS, BODY_TAIL, } = tokensRender.ResourceSlot;
175
+ const htmlPageSchemaFactory = ({ htmlAttrs, }) => {
176
+ return [
177
+ htmlpagebuilder.staticRender('<!DOCTYPE html>'),
178
+ htmlpagebuilder.staticRender(`<html ${formatAttributes(htmlAttrs, 'html')}>`),
179
+ htmlpagebuilder.staticRender('<head>'),
180
+ htmlpagebuilder.staticRender('<meta charset="UTF-8">'),
181
+ htmlpagebuilder.dynamicRender(HEAD_META),
182
+ htmlpagebuilder.dynamicRender(HEAD_PERFORMANCE),
183
+ htmlpagebuilder.dynamicRender(HEAD_CORE_STYLES),
184
+ htmlpagebuilder.dynamicRender(HEAD_POLYFILLS),
185
+ htmlpagebuilder.dynamicRender(HEAD_DYNAMIC_SCRIPTS),
186
+ htmlpagebuilder.dynamicRender(HEAD_CORE_SCRIPTS),
187
+ htmlpagebuilder.dynamicRender(HEAD_ANALYTICS),
188
+ htmlpagebuilder.dynamicRender(HEAD_ICONS),
189
+ htmlpagebuilder.staticRender('</head>'),
190
+ htmlpagebuilder.staticRender(`<body ${formatAttributes(htmlAttrs, 'body')}>`),
191
+ htmlpagebuilder.dynamicRender(BODY_START),
192
+ // react app
193
+ htmlpagebuilder.dynamicRender(REACT_RENDER),
194
+ htmlpagebuilder.dynamicRender(BODY_END),
195
+ htmlpagebuilder.dynamicRender(BODY_TAIL_ANALYTICS),
196
+ htmlpagebuilder.dynamicRender(BODY_TAIL),
197
+ htmlpagebuilder.staticRender('</body>'),
198
+ htmlpagebuilder.staticRender('</html>'),
199
+ ];
200
+ };
201
+
202
+ const testPageResources = (options) => {
203
+ var _a;
204
+ const { modules, providers = [] } = options;
205
+ const { di, runLine } = testHelpers.getDiWrapper({
206
+ di: options.di,
207
+ modules,
208
+ providers: [
209
+ {
210
+ provide: 'htmlPageSchema',
211
+ useFactory: htmlPageSchemaFactory,
212
+ deps: {
213
+ htmlAttrs: tokensRender.HTML_ATTRS,
214
+ },
215
+ },
216
+ {
217
+ provide: tokensRender.HTML_ATTRS,
218
+ useValue: {
219
+ target: 'html',
220
+ attrs: {
221
+ class: 'no-js',
222
+ lang: 'ru',
223
+ },
224
+ },
225
+ multi: true,
226
+ },
227
+ ...providers,
228
+ core.provide({
229
+ provide: tokensRender.RESOURCES_REGISTRY,
230
+ useClass: ResourcesRegistry,
231
+ deps: {
232
+ resourceInliner: RESOURCE_INLINER,
233
+ },
234
+ }),
235
+ core.provide({
236
+ provide: RESOURCE_INLINER,
237
+ useValue: {
238
+ shouldInline() {
239
+ return false;
240
+ },
241
+ shouldAddResource() {
242
+ return true;
243
+ },
244
+ inlineResource() {
245
+ return [];
246
+ },
247
+ },
248
+ }),
249
+ ],
250
+ });
251
+ const renderSlots = flatten__default["default"]((_a = di.get({ token: tokensRender.RENDER_SLOTS, optional: true })) !== null && _a !== void 0 ? _a : []);
252
+ const resourcesRegistry = di.get(tokensRender.RESOURCES_REGISTRY);
253
+ const render = () => {
254
+ const rawHtml = htmlpagebuilder.buildPage({
255
+ slotHandlers: mapResourcesToSlots([...renderSlots, ...resourcesRegistry.getPageResources()]),
256
+ description: di.get('htmlPageSchema'),
257
+ });
258
+ return testHelpers.parseHtml(rawHtml, {});
259
+ };
260
+ return {
261
+ render,
262
+ di,
263
+ runLine,
264
+ };
265
+ };
266
+
267
+ exports.testPageResources = testPageResources;