@tramvai/module-render 2.70.0 → 2.72.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/lib/browser.js +9 -233
- package/lib/client/index.browser.js +48 -0
- package/lib/client/renderer.browser.js +50 -0
- package/lib/react/index.browser.js +11 -0
- package/lib/react/index.es.js +11 -0
- package/lib/react/index.js +15 -0
- package/lib/react/pageErrorBoundary.browser.js +23 -0
- package/lib/react/pageErrorBoundary.es.js +23 -0
- package/lib/react/pageErrorBoundary.js +27 -0
- package/lib/react/root.browser.js +58 -0
- package/lib/react/root.es.js +58 -0
- package/lib/react/root.js +62 -0
- package/lib/resourcesInliner/externalFilesHelper.es.js +17 -0
- package/lib/resourcesInliner/externalFilesHelper.js +26 -0
- package/lib/resourcesInliner/fileProcessor.es.js +31 -0
- package/lib/resourcesInliner/fileProcessor.js +40 -0
- package/lib/resourcesInliner/resourcesInliner.es.js +204 -0
- package/lib/resourcesInliner/resourcesInliner.js +213 -0
- package/lib/resourcesInliner/tokens.es.js +15 -0
- package/lib/resourcesInliner/tokens.js +20 -0
- package/lib/resourcesRegistry/index.es.js +28 -0
- package/lib/resourcesRegistry/index.js +36 -0
- package/lib/server/PageBuilder.es.js +93 -0
- package/lib/server/PageBuilder.js +102 -0
- package/lib/server/ReactRenderServer.es.js +90 -0
- package/lib/server/ReactRenderServer.js +98 -0
- package/lib/server/blocks/bundleResource/bundleResource.es.js +62 -0
- package/lib/server/blocks/bundleResource/bundleResource.js +71 -0
- package/lib/server/blocks/polyfill.es.js +35 -0
- package/lib/server/blocks/polyfill.js +39 -0
- package/lib/{server_inline.inline.es.js → server/blocks/preload/onload.inline.es.js} +1 -1
- package/lib/{server_inline.inline.js → server/blocks/preload/onload.inline.js} +2 -0
- package/lib/server/blocks/preload/preloadBlock.es.js +21 -0
- package/lib/server/blocks/preload/preloadBlock.js +30 -0
- package/lib/server/blocks/utils/fetchWebpackStats.es.js +88 -0
- package/lib/server/blocks/utils/fetchWebpackStats.js +115 -0
- package/lib/server/blocks/utils/flushFiles.es.js +33 -0
- package/lib/server/blocks/utils/flushFiles.js +44 -0
- package/lib/server/blocks/utils/requireFunc.es.js +5 -0
- package/lib/server/blocks/utils/requireFunc.js +9 -0
- package/lib/server/constants/performance.es.js +3 -0
- package/lib/server/constants/performance.js +7 -0
- package/lib/server/htmlPageSchema.es.js +33 -0
- package/lib/server/htmlPageSchema.js +37 -0
- package/lib/server/utils.es.js +16 -0
- package/lib/server/utils.js +20 -0
- package/lib/server.es.js +18 -859
- package/lib/server.js +33 -909
- package/lib/shared/LayoutModule.browser.js +40 -0
- package/lib/shared/LayoutModule.es.js +40 -0
- package/lib/shared/LayoutModule.js +42 -0
- package/lib/shared/pageErrorStore.browser.js +19 -0
- package/lib/shared/pageErrorStore.es.js +19 -0
- package/lib/shared/pageErrorStore.js +26 -0
- package/lib/shared/providers.browser.js +18 -0
- package/lib/shared/providers.es.js +18 -0
- package/lib/shared/providers.js +22 -0
- package/package.json +23 -24
package/lib/server.es.js
CHANGED
|
@@ -1,869 +1,28 @@
|
|
|
1
1
|
import { __decorate } from 'tslib';
|
|
2
|
-
import {
|
|
2
|
+
import { createElement } from 'react';
|
|
3
3
|
import { renderToString } from 'react-dom/server';
|
|
4
4
|
import { Module, provide, commandLineListTokens, DI_TOKEN } from '@tramvai/core';
|
|
5
|
-
import {
|
|
5
|
+
import { CREATE_CACHE_TOKEN, LOGGER_TOKEN, REQUEST_MANAGER_TOKEN, RESPONSE_MANAGER_TOKEN, CONTEXT_TOKEN } from '@tramvai/tokens-common';
|
|
6
6
|
import { PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
|
|
7
7
|
import { ClientHintsModule, USER_AGENT_TOKEN } from '@tramvai/module-client-hints';
|
|
8
|
-
import {
|
|
8
|
+
import { RESOURCES_REGISTRY, RESOURCE_INLINE_OPTIONS, RENDER_SLOTS, POLYFILL_CONDITION, HTML_ATTRS, MODERN_SATISFIES_TOKEN, RENDER_FLOW_AFTER_TOKEN, CUSTOM_RENDER, EXTEND_RENDER, REACT_SERVER_RENDER_MODE, ResourceType } from '@tramvai/tokens-render';
|
|
9
9
|
export * from '@tramvai/tokens-render';
|
|
10
|
-
import {
|
|
10
|
+
import { Scope } from '@tinkoff/dippy';
|
|
11
11
|
import { WEB_FASTIFY_APP_BEFORE_ERROR_TOKEN } from '@tramvai/tokens-server-private';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { ROOT_ERROR_BOUNDARY_COMPONENT_TOKEN } from '@tramvai/react';
|
|
13
|
+
import { parse } from '@tinkoff/url';
|
|
14
14
|
import { satisfies } from '@tinkoff/user-agent';
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import
|
|
25
|
-
|
|
26
|
-
import { isFileSystemPageComponent, fileSystemPageToWebpackChunkName } from '@tramvai/experiments';
|
|
27
|
-
import uniq from '@tinkoff/utils/array/uniq';
|
|
28
|
-
import * as path from 'path';
|
|
29
|
-
import each from '@tinkoff/utils/array/each';
|
|
30
|
-
import path$1 from '@tinkoff/utils/object/path';
|
|
31
|
-
import { o as onload } from './server_inline.inline.es.js';
|
|
32
|
-
import { Writable } from 'stream';
|
|
33
|
-
import { jsx } from 'react/jsx-runtime';
|
|
34
|
-
import { createEvent, createReducer, useStore, Provider } from '@tramvai/state';
|
|
35
|
-
import { useUrl, usePageService } from '@tramvai/module-router';
|
|
36
|
-
import { composeLayoutOptions, createLayout } from '@tinkoff/layout-factory';
|
|
37
|
-
|
|
38
|
-
const thirtySeconds = 1000 * 30;
|
|
39
|
-
const getFileContentLength = async (url) => {
|
|
40
|
-
const info = await fetch(url, { method: 'HEAD', timeout: thirtySeconds });
|
|
41
|
-
return info.headers.get('content-length');
|
|
42
|
-
};
|
|
43
|
-
const getFile = async (url) => {
|
|
44
|
-
const fileResponse = await fetch(url, { timeout: thirtySeconds });
|
|
45
|
-
if (fileResponse.ok) {
|
|
46
|
-
const file = await fileResponse.text();
|
|
47
|
-
return file;
|
|
48
|
-
}
|
|
49
|
-
return undefined;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const URL_OCCURRENCES_RE = /(url\((['"]?))(.*?)(\2\))/gi;
|
|
53
|
-
const isAbsoluteUrl = (resourceUrl) => ['http://', 'https://', '//'].some((prefix) => startsWith(prefix, resourceUrl));
|
|
54
|
-
const toHttpsUrl = (resourceUrl) => {
|
|
55
|
-
if (resourceUrl.indexOf('//localhost') !== -1) {
|
|
56
|
-
return resourceUrl;
|
|
57
|
-
}
|
|
58
|
-
if (startsWith('http://', resourceUrl)) {
|
|
59
|
-
return resourceUrl.replace('http://', 'https://');
|
|
60
|
-
}
|
|
61
|
-
if (startsWith('//', resourceUrl)) {
|
|
62
|
-
return resourceUrl.replace('//', 'https://');
|
|
63
|
-
}
|
|
64
|
-
return resourceUrl;
|
|
65
|
-
};
|
|
66
|
-
const urlReplacerCreator = (resourceUrl) => (str, leftGroup, _, extractedUrl, rightGroup) => {
|
|
67
|
-
return isAbsoluteUrl(extractedUrl)
|
|
68
|
-
? str
|
|
69
|
-
: `${leftGroup}${resolve(toHttpsUrl(resourceUrl), extractedUrl)}${rightGroup}`;
|
|
70
|
-
};
|
|
71
|
-
const processFile = (resource, file) => {
|
|
72
|
-
if (resource.type === ResourceType.style) {
|
|
73
|
-
return file.replace(URL_OCCURRENCES_RE, urlReplacerCreator(resource.payload));
|
|
74
|
-
}
|
|
75
|
-
return file;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const INTERNAL_CACHE_SIZE = 50;
|
|
79
|
-
const ASSETS_PREFIX = process.env.NODE_ENV === 'development' &&
|
|
80
|
-
(process.env.ASSETS_PREFIX === 'static' || !process.env.ASSETS_PREFIX)
|
|
81
|
-
? `http://localhost:${process.env.PORT_STATIC}/dist/`
|
|
82
|
-
: process.env.ASSETS_PREFIX;
|
|
83
|
-
const getInlineType = (type) => {
|
|
84
|
-
switch (type) {
|
|
85
|
-
case ResourceType.style:
|
|
86
|
-
return ResourceType.inlineStyle;
|
|
87
|
-
case ResourceType.script:
|
|
88
|
-
return ResourceType.inlineScript;
|
|
89
|
-
default:
|
|
90
|
-
return type;
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
const getResourceUrl = (resource) => {
|
|
94
|
-
if (isEmpty(resource.payload) || !isAbsoluteUrl$1(resource.payload)) {
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
return resource.payload.startsWith('//')
|
|
98
|
-
? `https://${resource.payload.substr(2)}`
|
|
99
|
-
: resource.payload;
|
|
100
|
-
};
|
|
101
|
-
class ResourcesInliner {
|
|
102
|
-
constructor({ resourcesRegistryCache, resourceInlineThreshold, logger }) {
|
|
103
|
-
this.internalFilesCache = new Map();
|
|
104
|
-
this.runningRequests = new Set();
|
|
105
|
-
this.scheduleFileLoad = async (resource, resourceInlineThreshold) => {
|
|
106
|
-
const url = getResourceUrl(resource);
|
|
107
|
-
const requestKey = `file${url}`;
|
|
108
|
-
const filesCache = this.getFilesCache(url);
|
|
109
|
-
const result = filesCache.get(url);
|
|
110
|
-
if (result) {
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
if (!this.runningRequests.has(requestKey)) {
|
|
114
|
-
this.runningRequests.add(url);
|
|
115
|
-
try {
|
|
116
|
-
const file = await getFile(url);
|
|
117
|
-
if (file === undefined) {
|
|
118
|
-
this.resourcesRegistryCache.disabledUrlsCache.set(url, true);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const size = file.length;
|
|
122
|
-
if (size < resourceInlineThreshold) {
|
|
123
|
-
filesCache.set(url, processFile(resource, file));
|
|
124
|
-
}
|
|
125
|
-
this.resourcesRegistryCache.sizeCache.set(url, size);
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
this.log.warn({
|
|
129
|
-
event: 'file-load-failed',
|
|
130
|
-
url,
|
|
131
|
-
error,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
finally {
|
|
135
|
-
this.runningRequests.delete(requestKey);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
this.scheduleFileSizeLoad = async (resource, resourceInlineThreshold) => {
|
|
140
|
-
const url = getResourceUrl(resource);
|
|
141
|
-
const requestKey = `size${url}`;
|
|
142
|
-
const result = this.resourcesRegistryCache.sizeCache.get(url);
|
|
143
|
-
if (result) {
|
|
144
|
-
return result;
|
|
145
|
-
}
|
|
146
|
-
if (!this.runningRequests.has(requestKey)) {
|
|
147
|
-
this.runningRequests.add(requestKey);
|
|
148
|
-
try {
|
|
149
|
-
const contentLength = await getFileContentLength(url);
|
|
150
|
-
const size = isUndefined(contentLength) ? 0 : +contentLength;
|
|
151
|
-
if (size) {
|
|
152
|
-
this.resourcesRegistryCache.sizeCache.set(url, size);
|
|
153
|
-
}
|
|
154
|
-
if (size < resourceInlineThreshold) {
|
|
155
|
-
this.scheduleFileLoad(resource, resourceInlineThreshold);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
this.log.warn({
|
|
160
|
-
event: 'file-content-length-load-failed',
|
|
161
|
-
url,
|
|
162
|
-
error,
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
finally {
|
|
166
|
-
this.runningRequests.delete(requestKey);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
this.resourcesRegistryCache = resourcesRegistryCache;
|
|
171
|
-
this.resourceInlineThreshold = resourceInlineThreshold;
|
|
172
|
-
this.log = logger('resources-inliner');
|
|
173
|
-
}
|
|
174
|
-
getFilesCache(url) {
|
|
175
|
-
if (url.startsWith(ASSETS_PREFIX)) {
|
|
176
|
-
// internal resources are resources generated by the current app itself
|
|
177
|
-
// these kind of resources are pretty static and won't be changed while app is running
|
|
178
|
-
// so we can cache it with bare Map and do not care about how to cleanup cache from outdated entries
|
|
179
|
-
return this.internalFilesCache;
|
|
180
|
-
}
|
|
181
|
-
return this.resourcesRegistryCache.filesCache;
|
|
182
|
-
}
|
|
183
|
-
// check that resource's preload-link should be added to render
|
|
184
|
-
shouldAddResource(resource) {
|
|
185
|
-
if (resource.type !== ResourceType.preloadLink) {
|
|
186
|
-
// only checking preload-links
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
const url = getResourceUrl(resource);
|
|
190
|
-
if (isUndefined(url)) {
|
|
191
|
-
// if url is undefined that file is not in cache
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
// if file is residing in cache that means it will be inlined in page render
|
|
195
|
-
// therefore no need to have preload-link for the inlined resource
|
|
196
|
-
return !this.getFilesCache(url).has(url);
|
|
197
|
-
}
|
|
198
|
-
// method for check is passed resource should be inlined in HTML-page
|
|
199
|
-
shouldInline(resource) {
|
|
200
|
-
var _a;
|
|
201
|
-
if (!(((_a = this.resourceInlineThreshold) === null || _a === void 0 ? void 0 : _a.types) || []).includes(resource.type)) {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
const resourceInlineThreshold = this.resourceInlineThreshold.threshold;
|
|
205
|
-
if (isUndefined(resourceInlineThreshold)) {
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
const url = getResourceUrl(resource);
|
|
209
|
-
if (isUndefined(url) || this.resourcesRegistryCache.disabledUrlsCache.has(url)) {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
const filesCache = this.getFilesCache(url);
|
|
213
|
-
if (filesCache.has(url)) {
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
if (filesCache === this.internalFilesCache &&
|
|
217
|
-
this.internalFilesCache.size >= INTERNAL_CACHE_SIZE) {
|
|
218
|
-
// if we've exceeded limits for the internal resources cache ignore any new entries
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
if (!this.resourcesRegistryCache.sizeCache.has(url)) {
|
|
222
|
-
this.scheduleFileSizeLoad(resource, resourceInlineThreshold);
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
const size = this.resourcesRegistryCache.sizeCache.get(url);
|
|
226
|
-
if (size > resourceInlineThreshold) {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
this.scheduleFileLoad(resource, resourceInlineThreshold);
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
inlineResource(resource) {
|
|
233
|
-
const url = getResourceUrl(resource);
|
|
234
|
-
if (isUndefined(url)) {
|
|
235
|
-
// usually, it should not happen but anyway check it for safety
|
|
236
|
-
return [resource];
|
|
237
|
-
}
|
|
238
|
-
const text = this.getFilesCache(url).get(url);
|
|
239
|
-
if (isEmpty(text)) {
|
|
240
|
-
return [resource];
|
|
241
|
-
}
|
|
242
|
-
const result = [];
|
|
243
|
-
if (process.env.NODE_ENV === 'development') {
|
|
244
|
-
// html comment for debugging inlining in dev mode
|
|
245
|
-
result.push({
|
|
246
|
-
slot: resource.slot,
|
|
247
|
-
type: ResourceType.asIs,
|
|
248
|
-
payload: `<!-- Inlined file ${url} -->`,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
result.push({
|
|
252
|
-
...resource,
|
|
253
|
-
type: getInlineType(resource.type),
|
|
254
|
-
payload: text,
|
|
255
|
-
});
|
|
256
|
-
if (resource.type === ResourceType.style) {
|
|
257
|
-
// If we don't add data-href then extract-css-chunks-webpack-plugin
|
|
258
|
-
// will add link to resources to the html head (https://github.com/faceyspacey/extract-css-chunks-webpack-plugin/blob/master/src/index.js#L346)
|
|
259
|
-
// wherein link in case of css files plugin will look for a link tag, but we add a style tag
|
|
260
|
-
// so we can't use tag from above and have to generate new one
|
|
261
|
-
result.push({
|
|
262
|
-
slot: resource.slot,
|
|
263
|
-
type: ResourceType.style,
|
|
264
|
-
payload: null,
|
|
265
|
-
attrs: {
|
|
266
|
-
'data-href': resource.payload,
|
|
267
|
-
},
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
return result;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* @description
|
|
276
|
-
* Инлайнер ресурсов - используется на сервере для регистрации файлов, которые должны быть вставлены
|
|
277
|
-
* в итоговую html-страницу в виде ссылки на файл или заинлайнеными полностью
|
|
278
|
-
*/
|
|
279
|
-
const RESOURCE_INLINER = createToken('resourceInliner');
|
|
280
|
-
/**
|
|
281
|
-
* @description
|
|
282
|
-
* Кэш загруженных ресурсов.
|
|
283
|
-
*/
|
|
284
|
-
const RESOURCES_REGISTRY_CACHE = createToken('resourcesRegistryCache');
|
|
285
|
-
|
|
286
|
-
class ResourcesRegistry {
|
|
287
|
-
constructor({ resourceInliner }) {
|
|
288
|
-
this.resources = new Set();
|
|
289
|
-
this.resourceInliner = resourceInliner;
|
|
290
|
-
}
|
|
291
|
-
register(resourceOrResources) {
|
|
292
|
-
toArray(resourceOrResources).forEach((resource) => {
|
|
293
|
-
this.resources.add(resource);
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
getPageResources() {
|
|
297
|
-
return Array.from(this.resources.values())
|
|
298
|
-
.reduce((acc, resource) => {
|
|
299
|
-
if (this.resourceInliner.shouldInline(resource)) {
|
|
300
|
-
Array.prototype.push.apply(acc, this.resourceInliner.inlineResource(resource));
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
acc.push(resource);
|
|
304
|
-
}
|
|
305
|
-
return acc;
|
|
306
|
-
}, [])
|
|
307
|
-
.filter((resource) => this.resourceInliner.shouldAddResource(resource));
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const PRELOAD_JS = '__preloadJS';
|
|
312
|
-
|
|
313
|
-
const isJs = (file) => /\.js$/.test(file) && !/\.hot-update\.js$/.test(file);
|
|
314
|
-
const isCss = (file) => /\.css$/.test(file);
|
|
315
|
-
const getFilesByType = (files) => {
|
|
316
|
-
const scripts = files.filter(isJs);
|
|
317
|
-
const styles = files.filter(isCss);
|
|
318
|
-
return {
|
|
319
|
-
scripts,
|
|
320
|
-
styles,
|
|
321
|
-
};
|
|
322
|
-
};
|
|
323
|
-
const flushFiles = (chunks, webpackStats, { ignoreDependencies = false, } = {}) => {
|
|
324
|
-
// при использовании namedChunkGroups во все entry-файлы как зависимость попадает runtimeChunk
|
|
325
|
-
// что при повторных вызовах flushChunks вызовет дублирование подключения manifest.js
|
|
326
|
-
// из-за чего приложение может запускаться несколько раз
|
|
327
|
-
// без поля namedChunkGroups flushChunks вернет только сами ассеты для чанков, без зависимостей
|
|
328
|
-
const { assetsByChunkName, namedChunkGroups } = webpackStats;
|
|
329
|
-
const resolvedChunks = [];
|
|
330
|
-
for (const chunk of chunks) {
|
|
331
|
-
if (!ignoreDependencies && (namedChunkGroups === null || namedChunkGroups === void 0 ? void 0 : namedChunkGroups[chunk])) {
|
|
332
|
-
resolvedChunks.push(...namedChunkGroups[chunk].chunks);
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
resolvedChunks.push(chunk);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const files = flatten(uniq(resolvedChunks).map((chunk) => assetsByChunkName[chunk]));
|
|
339
|
-
return getFilesByType(files);
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
const requireFunc =
|
|
343
|
-
// @ts-ignore
|
|
344
|
-
typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require;
|
|
345
|
-
|
|
346
|
-
let appConfig;
|
|
347
|
-
try {
|
|
348
|
-
appConfig = require('@tramvai/cli/lib/external/config').appConfig;
|
|
349
|
-
}
|
|
350
|
-
catch (e) { }
|
|
351
|
-
let fetchStats = async () => {
|
|
352
|
-
throw new Error(`Unknown environment`);
|
|
353
|
-
};
|
|
354
|
-
if (process.env.NODE_ENV === 'development') {
|
|
355
|
-
fetchStats = async () => {
|
|
356
|
-
const { modern: configModern, staticHost, staticPort, output } = appConfig;
|
|
357
|
-
const getUrl = (filename) => `http://${staticHost}:${staticPort}/${output.client}/${filename}`;
|
|
358
|
-
const request = await fetch(getUrl(configModern ? 'stats.modern.json' : 'stats.json'));
|
|
359
|
-
const stats = await request.json();
|
|
360
|
-
// static - популярная заглушка в env.development.js файлах, надо игнорировать, как было раньше
|
|
361
|
-
const hasAssetsPrefix = process.env.ASSETS_PREFIX && process.env.ASSETS_PREFIX !== 'static';
|
|
362
|
-
const publicPath = hasAssetsPrefix ? process.env.ASSETS_PREFIX : stats.publicPath;
|
|
363
|
-
return {
|
|
364
|
-
...stats,
|
|
365
|
-
publicPath,
|
|
366
|
-
};
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
if (process.env.NODE_ENV === 'test') {
|
|
370
|
-
fetchStats = () => {
|
|
371
|
-
// mock for unit-testing as there is no real static return something just to make server render work
|
|
372
|
-
return Promise.resolve({ publicPath: 'http://localhost:4000/', assetsByChunkName: {} });
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
if (process.env.NODE_ENV === 'production') {
|
|
376
|
-
const SEARCH_PATHS = [process.cwd(), __dirname];
|
|
377
|
-
const webpackStats = (fileName) => {
|
|
378
|
-
let stats;
|
|
379
|
-
for (const dir of SEARCH_PATHS) {
|
|
380
|
-
try {
|
|
381
|
-
const statsPath = path.resolve(dir, fileName);
|
|
382
|
-
stats = requireFunc(statsPath);
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
catch (e) {
|
|
386
|
-
// ignore errors as this function is used to load stats for several optional destinations
|
|
387
|
-
// and these destinations may not have stats file
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if (!stats) {
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
if (!process.env.ASSETS_PREFIX) {
|
|
394
|
-
if (process.env.STATIC_PREFIX) {
|
|
395
|
-
throw new Error('Required env variable "ASSETS_PREFIX" is not set. Instead of using "STATIC_PREFIX" env please define "ASSETS_PREFIX: STATIC_PREFIX + /compiled"');
|
|
396
|
-
}
|
|
397
|
-
throw new Error('Required env variable "ASSETS_PREFIX" is not set');
|
|
398
|
-
}
|
|
399
|
-
return {
|
|
400
|
-
...stats,
|
|
401
|
-
publicPath: process.env.ASSETS_PREFIX,
|
|
402
|
-
};
|
|
403
|
-
};
|
|
404
|
-
const statsLegacy = webpackStats('stats.json');
|
|
405
|
-
const statsModern = webpackStats('stats.modern.json') || statsLegacy;
|
|
406
|
-
if (!statsLegacy) {
|
|
407
|
-
throw new Error(`Cannot find stats.json.
|
|
408
|
-
It should be placed in one of the next places:
|
|
409
|
-
${SEARCH_PATHS.join('\n\t')}
|
|
410
|
-
In case it happens on deployment:
|
|
411
|
-
- In case you are using two independent jobs for building app
|
|
412
|
-
- Either do not split build command by two independent jobs and use one common job with "tramvai build" command without --buildType
|
|
413
|
-
- Or copy stats.json (and stats.modern.json if present) file from client build output to server output by yourself in your CI
|
|
414
|
-
- Otherwise report issue to tramvai team
|
|
415
|
-
In case it happens locally:
|
|
416
|
-
- prefer to use command "tramvai start-prod" to test prod-build locally
|
|
417
|
-
- copy stats.json next to built server.js file
|
|
418
|
-
`);
|
|
419
|
-
}
|
|
420
|
-
fetchStats = (modern) => {
|
|
421
|
-
const stats = modern ? statsModern : statsLegacy;
|
|
422
|
-
return Promise.resolve(stats);
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
const fetchWebpackStats = async ({ modern, } = {}) => {
|
|
426
|
-
return fetchStats(modern);
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
const bundleResource = async ({ bundle, modern, extractor, pageComponent, }) => {
|
|
430
|
-
// for file-system pages preload page chunk against bundle chunk
|
|
431
|
-
const chunkNameFromBundle = isFileSystemPageComponent(pageComponent)
|
|
432
|
-
? fileSystemPageToWebpackChunkName(pageComponent)
|
|
433
|
-
: last(bundle.split('/'));
|
|
434
|
-
const webpackStats = await fetchWebpackStats({ modern });
|
|
435
|
-
const { publicPath, assetsByChunkName } = webpackStats;
|
|
436
|
-
const bundles = has('common-chunk', assetsByChunkName)
|
|
437
|
-
? ['common-chunk', chunkNameFromBundle]
|
|
438
|
-
: [chunkNameFromBundle];
|
|
439
|
-
const lazyChunks = extractor.getMainAssets().map((entry) => entry.chunk);
|
|
440
|
-
const { scripts: baseScripts } = flushFiles(['vendor'], webpackStats, {
|
|
441
|
-
ignoreDependencies: true,
|
|
442
|
-
});
|
|
443
|
-
const { scripts, styles } = flushFiles([...bundles, ...lazyChunks, 'platform'], webpackStats);
|
|
444
|
-
const genHref = (href) => `${publicPath}${href}`;
|
|
445
|
-
const result = [];
|
|
446
|
-
if (process.env.NODE_ENV === 'production' ||
|
|
447
|
-
(process.env.ASSETS_PREFIX && process.env.ASSETS_PREFIX !== 'static')) {
|
|
448
|
-
result.push({
|
|
449
|
-
type: ResourceType.inlineScript,
|
|
450
|
-
slot: ResourceSlot.HEAD_CORE_SCRIPTS,
|
|
451
|
-
payload: `window.ap = ${`"${process.env.ASSETS_PREFIX}"`};`,
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
styles.map((style) => result.push({
|
|
455
|
-
type: ResourceType.style,
|
|
456
|
-
slot: ResourceSlot.HEAD_CORE_STYLES,
|
|
457
|
-
payload: genHref(style),
|
|
458
|
-
attrs: {
|
|
459
|
-
'data-critical': 'true',
|
|
460
|
-
onload: `${PRELOAD_JS}()`,
|
|
461
|
-
},
|
|
462
|
-
}));
|
|
463
|
-
baseScripts.map((script) => result.push({
|
|
464
|
-
type: ResourceType.script,
|
|
465
|
-
slot: ResourceSlot.HEAD_CORE_SCRIPTS,
|
|
466
|
-
payload: genHref(script),
|
|
467
|
-
attrs: {
|
|
468
|
-
'data-critical': 'true',
|
|
469
|
-
},
|
|
470
|
-
}));
|
|
471
|
-
scripts.map((script) => result.push({
|
|
472
|
-
type: ResourceType.script,
|
|
473
|
-
slot: ResourceSlot.HEAD_CORE_SCRIPTS,
|
|
474
|
-
payload: genHref(script),
|
|
475
|
-
attrs: {
|
|
476
|
-
'data-critical': 'true',
|
|
477
|
-
},
|
|
478
|
-
}));
|
|
479
|
-
return result;
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
const polyfillResources = async ({ condition, modern, }) => {
|
|
483
|
-
const webpackStats = await fetchWebpackStats({ modern });
|
|
484
|
-
const { publicPath } = webpackStats;
|
|
485
|
-
// получает файл полифилла из stats.json\stats.modern.json.
|
|
486
|
-
// В зависимости от версии браузера будет использован полифилл из legacy или modern сборки,
|
|
487
|
-
// т.к. полифиллы для них могут отличаться на основании преобразований `@babel/preset-env`
|
|
488
|
-
const { scripts: polyfillScripts } = flushFiles(['polyfill'], webpackStats, {
|
|
489
|
-
ignoreDependencies: true,
|
|
490
|
-
});
|
|
491
|
-
const genHref = (href) => `${publicPath}${href}`;
|
|
492
|
-
const result = [];
|
|
493
|
-
polyfillScripts.forEach((script) => {
|
|
494
|
-
const href = genHref(script);
|
|
495
|
-
result.push({
|
|
496
|
-
type: ResourceType.inlineScript,
|
|
497
|
-
slot: ResourceSlot.HEAD_POLYFILLS,
|
|
498
|
-
payload: `(function (){
|
|
499
|
-
var con;
|
|
500
|
-
try {
|
|
501
|
-
con = ${condition};
|
|
502
|
-
} catch (e) {
|
|
503
|
-
con = true;
|
|
504
|
-
}
|
|
505
|
-
if (con) { document.write('<script defer="defer" charset="utf-8" data-critical="true" crossorigin="anonymous" src="${href}"><\\/script>')}
|
|
506
|
-
})()`,
|
|
507
|
-
});
|
|
508
|
-
});
|
|
509
|
-
return result;
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
const addPreloadForCriticalJS = (pageResources) => {
|
|
513
|
-
const jsUrls = [];
|
|
514
|
-
each((res) => {
|
|
515
|
-
if (res.type === 'script' && path$1(['attrs', 'data-critical'], res)) {
|
|
516
|
-
jsUrls.push(res.payload);
|
|
517
|
-
}
|
|
518
|
-
}, pageResources);
|
|
519
|
-
return {
|
|
520
|
-
type: ResourceType.inlineScript,
|
|
521
|
-
slot: ResourceSlot.HEAD_PERFORMANCE,
|
|
522
|
-
payload: `window.${PRELOAD_JS}=(${onload})([${jsUrls.map((url) => `"${url}"`).join(',')}])`,
|
|
523
|
-
};
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
const formatAttributes = (htmlAttrs, target) => {
|
|
527
|
-
if (!htmlAttrs) {
|
|
528
|
-
return '';
|
|
529
|
-
}
|
|
530
|
-
const targetAttrs = htmlAttrs.filter((item) => item.target === target);
|
|
531
|
-
const collectedAttrs = targetAttrs.reduce((acc, item) => ({ ...acc, ...item.attrs }), {});
|
|
532
|
-
const attrsString = Object.keys(collectedAttrs).reduce((acc, name) => {
|
|
533
|
-
if (collectedAttrs[name] === true) {
|
|
534
|
-
return `${acc} ${name}`;
|
|
535
|
-
}
|
|
536
|
-
return `${acc} ${name}="${collectedAttrs[name]}"`;
|
|
537
|
-
}, '');
|
|
538
|
-
return attrsString.trim();
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
/* eslint-disable sort-class-members/sort-class-members */
|
|
542
|
-
const mapResourcesToSlots = (resources) => resources.reduce((acc, resource) => {
|
|
543
|
-
const { slot } = resource;
|
|
544
|
-
if (Array.isArray(acc[slot])) {
|
|
545
|
-
acc[slot].push(resource);
|
|
546
|
-
}
|
|
547
|
-
else {
|
|
548
|
-
acc[slot] = [resource];
|
|
549
|
-
}
|
|
550
|
-
return acc;
|
|
551
|
-
}, {});
|
|
552
|
-
class PageBuilder {
|
|
553
|
-
constructor({ renderSlots, pageService, resourcesRegistry, context, reactRender, htmlPageSchema, polyfillCondition, htmlAttrs, modern, renderFlowAfter, logger, }) {
|
|
554
|
-
this.htmlAttrs = htmlAttrs;
|
|
555
|
-
this.renderSlots = flatten(renderSlots || []);
|
|
556
|
-
this.pageService = pageService;
|
|
557
|
-
this.context = context;
|
|
558
|
-
this.resourcesRegistry = resourcesRegistry;
|
|
559
|
-
this.reactRender = reactRender;
|
|
560
|
-
this.htmlPageSchema = htmlPageSchema;
|
|
561
|
-
this.polyfillCondition = polyfillCondition;
|
|
562
|
-
this.modern = modern;
|
|
563
|
-
this.renderFlowAfter = renderFlowAfter || [];
|
|
564
|
-
this.log = logger('page-builder');
|
|
565
|
-
}
|
|
566
|
-
async flow() {
|
|
567
|
-
const stats = await fetchWebpackStats({ modern: this.modern });
|
|
568
|
-
const extractor = new ChunkExtractor({ stats, entrypoints: [] });
|
|
569
|
-
// first we render the application, because we need to extract information about the data used by the components
|
|
570
|
-
await this.renderApp(extractor);
|
|
571
|
-
await Promise.all(this.renderFlowAfter.map((callback) => callback().catch((error) => {
|
|
572
|
-
this.log.warn({ event: 'render-flow-after-error', callback, error });
|
|
573
|
-
})));
|
|
574
|
-
this.dehydrateState();
|
|
575
|
-
// load information and dependency for the current bundle and page
|
|
576
|
-
await this.fetchChunksInfo(extractor);
|
|
577
|
-
this.preloadBlock();
|
|
578
|
-
return this.generateHtml();
|
|
579
|
-
}
|
|
580
|
-
dehydrateState() {
|
|
581
|
-
this.resourcesRegistry.register({
|
|
582
|
-
type: ResourceType.asIs,
|
|
583
|
-
slot: ResourceSlot.BODY_END,
|
|
584
|
-
// String much better than big object, source https://v8.dev/blog/cost-of-javascript-2019#json
|
|
585
|
-
payload: `<script id="__TRAMVAI_STATE__" type="application/json">${safeStringify(this.context.dehydrate().dispatcher)}</script>`,
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
async fetchChunksInfo(extractor) {
|
|
589
|
-
const { modern } = this;
|
|
590
|
-
const { bundle, pageComponent } = this.pageService.getConfig();
|
|
591
|
-
this.resourcesRegistry.register(await bundleResource({ bundle, modern, extractor, pageComponent }));
|
|
592
|
-
this.resourcesRegistry.register(await polyfillResources({
|
|
593
|
-
condition: this.polyfillCondition,
|
|
594
|
-
modern,
|
|
595
|
-
}));
|
|
596
|
-
}
|
|
597
|
-
preloadBlock() {
|
|
598
|
-
const preloadResources = addPreloadForCriticalJS(this.resourcesRegistry.getPageResources());
|
|
599
|
-
this.resourcesRegistry.register(preloadResources);
|
|
600
|
-
}
|
|
601
|
-
generateHtml() {
|
|
602
|
-
const resultSlotHandlers = mapResourcesToSlots([
|
|
603
|
-
...this.renderSlots,
|
|
604
|
-
...this.resourcesRegistry.getPageResources(),
|
|
605
|
-
]);
|
|
606
|
-
return buildPage({
|
|
607
|
-
slotHandlers: resultSlotHandlers,
|
|
608
|
-
description: this.htmlPageSchema,
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
async renderApp(extractor) {
|
|
612
|
-
const html = await this.reactRender.render(extractor);
|
|
613
|
-
this.renderSlots = this.renderSlots.concat({
|
|
614
|
-
type: ResourceType.asIs,
|
|
615
|
-
slot: ResourceSlot.REACT_RENDER,
|
|
616
|
-
payload: `<div ${formatAttributes(this.htmlAttrs, 'app')}>${html}</div>`,
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
/* eslint-enable sort-class-members/sort-class-members */
|
|
621
|
-
|
|
622
|
-
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, } = ResourceSlot;
|
|
623
|
-
const htmlPageSchemaFactory = ({ htmlAttrs, }) => {
|
|
624
|
-
return [
|
|
625
|
-
staticRender('<!DOCTYPE html>'),
|
|
626
|
-
staticRender(`<html ${formatAttributes(htmlAttrs, 'html')}>`),
|
|
627
|
-
staticRender('<head>'),
|
|
628
|
-
staticRender('<meta charset="UTF-8">'),
|
|
629
|
-
dynamicRender(HEAD_META),
|
|
630
|
-
dynamicRender(HEAD_PERFORMANCE),
|
|
631
|
-
dynamicRender(HEAD_CORE_STYLES),
|
|
632
|
-
dynamicRender(HEAD_POLYFILLS),
|
|
633
|
-
dynamicRender(HEAD_DYNAMIC_SCRIPTS),
|
|
634
|
-
dynamicRender(HEAD_CORE_SCRIPTS),
|
|
635
|
-
dynamicRender(HEAD_ANALYTICS),
|
|
636
|
-
dynamicRender(HEAD_ICONS),
|
|
637
|
-
staticRender('</head>'),
|
|
638
|
-
staticRender(`<body ${formatAttributes(htmlAttrs, 'body')}>`),
|
|
639
|
-
dynamicRender(BODY_START),
|
|
640
|
-
// react app
|
|
641
|
-
dynamicRender(REACT_RENDER),
|
|
642
|
-
dynamicRender(BODY_END),
|
|
643
|
-
dynamicRender(BODY_TAIL_ANALYTICS),
|
|
644
|
-
dynamicRender(BODY_TAIL),
|
|
645
|
-
staticRender('</body>'),
|
|
646
|
-
staticRender('</html>'),
|
|
647
|
-
];
|
|
648
|
-
};
|
|
649
|
-
|
|
650
|
-
function serializeError(error) {
|
|
651
|
-
return {
|
|
652
|
-
...error,
|
|
653
|
-
message: error.message,
|
|
654
|
-
stack: error.stack,
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
function deserializeError(serializedError) {
|
|
658
|
-
const error = new Error(serializedError.message);
|
|
659
|
-
Object.assign(error, serializedError);
|
|
660
|
-
return error;
|
|
661
|
-
}
|
|
662
|
-
const setPageErrorEvent = createEvent('setPageError');
|
|
663
|
-
const initialState = null;
|
|
664
|
-
const PageErrorStore = createReducer('pageError', initialState).on(setPageErrorEvent, (state, error) => error && serializeError(error));
|
|
665
|
-
|
|
666
|
-
const PageErrorBoundary = (props) => {
|
|
667
|
-
const { children } = props;
|
|
668
|
-
const pageService = useDi(PAGE_SERVICE_TOKEN);
|
|
669
|
-
const url = useUrl();
|
|
670
|
-
const serializedError = useStore(PageErrorStore);
|
|
671
|
-
const error = useMemo(() => {
|
|
672
|
-
return serializedError && deserializeError(serializedError);
|
|
673
|
-
}, [serializedError]);
|
|
674
|
-
const errorHandlers = useDi({ token: ERROR_BOUNDARY_TOKEN, optional: true });
|
|
675
|
-
const fallbackFromDi = useDi({ token: ERROR_BOUNDARY_FALLBACK_COMPONENT_TOKEN, optional: true });
|
|
676
|
-
const fallback = pageService.resolveComponentFromConfig('errorBoundary');
|
|
677
|
-
return (jsx(UniversalErrorBoundary, { url: url, error: error, errorHandlers: errorHandlers, fallback: fallback, fallbackFromDi: fallbackFromDi, children: children }));
|
|
678
|
-
};
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Result component structure:
|
|
682
|
-
*
|
|
683
|
-
* <Root>
|
|
684
|
-
* <RootComponent>
|
|
685
|
-
* <LayoutComponent>
|
|
686
|
-
* <NestedLayoutComponent>
|
|
687
|
-
* <ErrorBoundaryComponent>
|
|
688
|
-
* <PageComponent />
|
|
689
|
-
* </ErrorBoundaryComponent>
|
|
690
|
-
* </NestedLayoutComponent>
|
|
691
|
-
* </LayoutComponent>
|
|
692
|
-
* </RootComponent>
|
|
693
|
-
* </Root>
|
|
694
|
-
*
|
|
695
|
-
* All components separated for a few reasons:
|
|
696
|
-
* - Page subtree can be rendered independently when Layout and Nested Layout the same
|
|
697
|
-
* - Nested Layout can be rerendered only on its changes
|
|
698
|
-
* - Layout can be rendered only on its changes
|
|
699
|
-
*/
|
|
700
|
-
const LayoutRenderComponent = ({ children }) => {
|
|
701
|
-
const pageService = usePageService();
|
|
702
|
-
const LayoutComponent = pageService.resolveComponentFromConfig('layout');
|
|
703
|
-
const HeaderComponent = pageService.resolveComponentFromConfig('header');
|
|
704
|
-
const FooterComponent = pageService.resolveComponentFromConfig('footer');
|
|
705
|
-
const layout = useMemo(() => (jsx(LayoutComponent, { Header: HeaderComponent, Footer: FooterComponent, children: children })), [LayoutComponent, HeaderComponent, FooterComponent, children]);
|
|
706
|
-
return layout;
|
|
707
|
-
};
|
|
708
|
-
const NestedLayoutRenderComponent = ({ children }) => {
|
|
709
|
-
const pageService = usePageService();
|
|
710
|
-
const NestedLayoutComponent = pageService.resolveComponentFromConfig('nestedLayout');
|
|
711
|
-
const nestedLayout = useMemo(() => jsx(NestedLayoutComponent, { children: children }), [NestedLayoutComponent, children]);
|
|
712
|
-
return nestedLayout;
|
|
713
|
-
};
|
|
714
|
-
const PageRenderComponent = () => {
|
|
715
|
-
const pageService = usePageService();
|
|
716
|
-
const { pageComponent } = pageService.getConfig();
|
|
717
|
-
let PageComponent = pageService.getComponent(pageComponent);
|
|
718
|
-
if (!PageComponent) {
|
|
719
|
-
PageComponent = () => {
|
|
720
|
-
throw new Error(`Page component '${pageComponent}' not found`);
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
const page = useMemo(() => (jsx(PageErrorBoundary, { children: jsx(PageComponent, {}) })), [PageComponent]);
|
|
724
|
-
return page;
|
|
725
|
-
};
|
|
726
|
-
const Root = () => {
|
|
727
|
-
const pageRenderComponent = useMemo(() => jsx(PageRenderComponent, {}), []);
|
|
728
|
-
const nestedLayoutRenderComponent = useMemo(() => jsx(NestedLayoutRenderComponent, { children: pageRenderComponent }), [pageRenderComponent]);
|
|
729
|
-
return jsx(LayoutRenderComponent, { children: nestedLayoutRenderComponent });
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
function renderReact({ di }, context) {
|
|
733
|
-
const serverState = typeof window !== 'undefined' ? context.getState() : undefined;
|
|
734
|
-
return (jsx(Provider, { context: context, serverState: serverState, children: jsx(DIContext.Provider, { value: di, children: jsx(Root, {}) }) }));
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const RENDER_TIMEOUT = 500;
|
|
738
|
-
class HtmlWritable extends Writable {
|
|
739
|
-
constructor() {
|
|
740
|
-
super(...arguments);
|
|
741
|
-
this.chunks = [];
|
|
742
|
-
this.html = '';
|
|
743
|
-
}
|
|
744
|
-
getHtml() {
|
|
745
|
-
return this.html;
|
|
746
|
-
}
|
|
747
|
-
_write(chunk, encoding, callback) {
|
|
748
|
-
this.chunks.push(chunk);
|
|
749
|
-
callback();
|
|
750
|
-
}
|
|
751
|
-
_final(callback) {
|
|
752
|
-
this.html = Buffer.concat(this.chunks).toString();
|
|
753
|
-
callback();
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
class ReactRenderServer {
|
|
757
|
-
// eslint-disable-next-line sort-class-members/sort-class-members
|
|
758
|
-
constructor({ context, customRender, extendRender, di, renderMode, logger }) {
|
|
759
|
-
this.context = context;
|
|
760
|
-
this.customRender = customRender;
|
|
761
|
-
this.extendRender = extendRender;
|
|
762
|
-
this.di = di;
|
|
763
|
-
this.renderMode = renderMode;
|
|
764
|
-
this.log = logger('module-render');
|
|
765
|
-
}
|
|
766
|
-
render(extractor) {
|
|
767
|
-
var _a;
|
|
768
|
-
let renderResult = renderReact({ di: this.di }, this.context);
|
|
769
|
-
each((render) => {
|
|
770
|
-
renderResult = render(renderResult);
|
|
771
|
-
}, (_a = this.extendRender) !== null && _a !== void 0 ? _a : []);
|
|
772
|
-
renderResult = extractor.collectChunks(renderResult);
|
|
773
|
-
if (this.customRender) {
|
|
774
|
-
return this.customRender(renderResult);
|
|
775
|
-
}
|
|
776
|
-
if (process.env.__TRAMVAI_CONCURRENT_FEATURES && this.renderMode === 'streaming') {
|
|
777
|
-
return new Promise((resolve, reject) => {
|
|
778
|
-
const { renderToPipeableStream } = require('react-dom/server');
|
|
779
|
-
const htmlWritable = new HtmlWritable();
|
|
780
|
-
htmlWritable.on('finish', () => {
|
|
781
|
-
resolve(htmlWritable.getHtml());
|
|
782
|
-
});
|
|
783
|
-
const start = Date.now();
|
|
784
|
-
const { log } = this;
|
|
785
|
-
log.info({
|
|
786
|
-
event: 'streaming-render:start',
|
|
787
|
-
});
|
|
788
|
-
const { pipe, abort } = renderToPipeableStream(renderResult, {
|
|
789
|
-
onAllReady() {
|
|
790
|
-
log.info({
|
|
791
|
-
event: 'streaming-render:complete',
|
|
792
|
-
duration: Date.now() - start,
|
|
793
|
-
});
|
|
794
|
-
// here `write` will be called only once
|
|
795
|
-
pipe(htmlWritable);
|
|
796
|
-
},
|
|
797
|
-
onError(error) {
|
|
798
|
-
// error can be inside Suspense boundaries, this is not critical, continue rendering.
|
|
799
|
-
// for criticall errors, this callback will be called with `onShellError`,
|
|
800
|
-
// so this is a best place to error logging
|
|
801
|
-
log.error({
|
|
802
|
-
event: 'streaming-render:error',
|
|
803
|
-
error,
|
|
804
|
-
});
|
|
805
|
-
},
|
|
806
|
-
onShellError(error) {
|
|
807
|
-
// always critical error, abort rendering
|
|
808
|
-
reject(error);
|
|
809
|
-
},
|
|
810
|
-
});
|
|
811
|
-
setTimeout(() => {
|
|
812
|
-
abort();
|
|
813
|
-
reject(new Error('React renderToPipeableStream timeout exceeded'));
|
|
814
|
-
}, RENDER_TIMEOUT);
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
const { renderToString } = require('react-dom/server');
|
|
818
|
-
return Promise.resolve(renderToString(renderResult));
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
const RenderChildrenComponent = ({ children }) => children;
|
|
823
|
-
let LayoutModule = class LayoutModule {
|
|
824
|
-
};
|
|
825
|
-
LayoutModule = __decorate([
|
|
826
|
-
Module({
|
|
827
|
-
providers: [
|
|
828
|
-
{
|
|
829
|
-
provide: DEFAULT_LAYOUT_COMPONENT,
|
|
830
|
-
useFactory: ({ layoutOptions }) => {
|
|
831
|
-
const options = composeLayoutOptions(layoutOptions);
|
|
832
|
-
return createLayout(options);
|
|
833
|
-
},
|
|
834
|
-
deps: {
|
|
835
|
-
layoutOptions: { token: LAYOUT_OPTIONS, optional: true },
|
|
836
|
-
},
|
|
837
|
-
},
|
|
838
|
-
{
|
|
839
|
-
provide: 'componentDefaultList',
|
|
840
|
-
multi: true,
|
|
841
|
-
useFactory: (components) => ({
|
|
842
|
-
...components,
|
|
843
|
-
nestedLayoutDefault: RenderChildrenComponent,
|
|
844
|
-
}),
|
|
845
|
-
deps: {
|
|
846
|
-
layoutDefault: DEFAULT_LAYOUT_COMPONENT,
|
|
847
|
-
footerDefault: { token: DEFAULT_FOOTER_COMPONENT, optional: true },
|
|
848
|
-
headerDefault: { token: DEFAULT_HEADER_COMPONENT, optional: true },
|
|
849
|
-
errorBoundaryDefault: { token: DEFAULT_ERROR_BOUNDARY_COMPONENT, optional: true },
|
|
850
|
-
},
|
|
851
|
-
},
|
|
852
|
-
],
|
|
853
|
-
})
|
|
854
|
-
], LayoutModule);
|
|
855
|
-
|
|
856
|
-
const providers = [
|
|
857
|
-
provide({
|
|
858
|
-
provide: COMBINE_REDUCERS,
|
|
859
|
-
multi: true,
|
|
860
|
-
useValue: PageErrorStore,
|
|
861
|
-
}),
|
|
862
|
-
provide({
|
|
863
|
-
provide: TRAMVAI_RENDER_MODE,
|
|
864
|
-
useValue: 'ssr',
|
|
865
|
-
}),
|
|
866
|
-
];
|
|
15
|
+
import { ResourcesInliner } from './resourcesInliner/resourcesInliner.es.js';
|
|
16
|
+
import { RESOURCE_INLINER, RESOURCES_REGISTRY_CACHE } from './resourcesInliner/tokens.es.js';
|
|
17
|
+
import { ResourcesRegistry } from './resourcesRegistry/index.es.js';
|
|
18
|
+
import { PageBuilder } from './server/PageBuilder.es.js';
|
|
19
|
+
import { htmlPageSchemaFactory } from './server/htmlPageSchema.es.js';
|
|
20
|
+
import { ReactRenderServer } from './server/ReactRenderServer.es.js';
|
|
21
|
+
export { ReactRenderServer } from './server/ReactRenderServer.es.js';
|
|
22
|
+
import { LayoutModule } from './shared/LayoutModule.es.js';
|
|
23
|
+
import { providers } from './shared/providers.es.js';
|
|
24
|
+
import { setPageErrorEvent, PageErrorStore, deserializeError } from './shared/pageErrorStore.es.js';
|
|
25
|
+
export { PageErrorStore, deserializeError, serializeError, setPageErrorEvent } from './shared/pageErrorStore.es.js';
|
|
867
26
|
|
|
868
27
|
var RenderModule_1;
|
|
869
28
|
const REQUEST_TTL = 5 * 60 * 1000;
|
|
@@ -1126,4 +285,4 @@ RenderModule = RenderModule_1 = __decorate([
|
|
|
1126
285
|
})
|
|
1127
286
|
], RenderModule);
|
|
1128
287
|
|
|
1129
|
-
export { DEFAULT_POLYFILL_CONDITION,
|
|
288
|
+
export { DEFAULT_POLYFILL_CONDITION, RenderModule };
|