@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.
Files changed (58) hide show
  1. package/lib/browser.js +9 -233
  2. package/lib/client/index.browser.js +48 -0
  3. package/lib/client/renderer.browser.js +50 -0
  4. package/lib/react/index.browser.js +11 -0
  5. package/lib/react/index.es.js +11 -0
  6. package/lib/react/index.js +15 -0
  7. package/lib/react/pageErrorBoundary.browser.js +23 -0
  8. package/lib/react/pageErrorBoundary.es.js +23 -0
  9. package/lib/react/pageErrorBoundary.js +27 -0
  10. package/lib/react/root.browser.js +58 -0
  11. package/lib/react/root.es.js +58 -0
  12. package/lib/react/root.js +62 -0
  13. package/lib/resourcesInliner/externalFilesHelper.es.js +17 -0
  14. package/lib/resourcesInliner/externalFilesHelper.js +26 -0
  15. package/lib/resourcesInliner/fileProcessor.es.js +31 -0
  16. package/lib/resourcesInliner/fileProcessor.js +40 -0
  17. package/lib/resourcesInliner/resourcesInliner.es.js +204 -0
  18. package/lib/resourcesInliner/resourcesInliner.js +213 -0
  19. package/lib/resourcesInliner/tokens.es.js +15 -0
  20. package/lib/resourcesInliner/tokens.js +20 -0
  21. package/lib/resourcesRegistry/index.es.js +28 -0
  22. package/lib/resourcesRegistry/index.js +36 -0
  23. package/lib/server/PageBuilder.es.js +93 -0
  24. package/lib/server/PageBuilder.js +102 -0
  25. package/lib/server/ReactRenderServer.es.js +90 -0
  26. package/lib/server/ReactRenderServer.js +98 -0
  27. package/lib/server/blocks/bundleResource/bundleResource.es.js +62 -0
  28. package/lib/server/blocks/bundleResource/bundleResource.js +71 -0
  29. package/lib/server/blocks/polyfill.es.js +35 -0
  30. package/lib/server/blocks/polyfill.js +39 -0
  31. package/lib/{server_inline.inline.es.js → server/blocks/preload/onload.inline.es.js} +1 -1
  32. package/lib/{server_inline.inline.js → server/blocks/preload/onload.inline.js} +2 -0
  33. package/lib/server/blocks/preload/preloadBlock.es.js +21 -0
  34. package/lib/server/blocks/preload/preloadBlock.js +30 -0
  35. package/lib/server/blocks/utils/fetchWebpackStats.es.js +88 -0
  36. package/lib/server/blocks/utils/fetchWebpackStats.js +115 -0
  37. package/lib/server/blocks/utils/flushFiles.es.js +33 -0
  38. package/lib/server/blocks/utils/flushFiles.js +44 -0
  39. package/lib/server/blocks/utils/requireFunc.es.js +5 -0
  40. package/lib/server/blocks/utils/requireFunc.js +9 -0
  41. package/lib/server/constants/performance.es.js +3 -0
  42. package/lib/server/constants/performance.js +7 -0
  43. package/lib/server/htmlPageSchema.es.js +33 -0
  44. package/lib/server/htmlPageSchema.js +37 -0
  45. package/lib/server/utils.es.js +16 -0
  46. package/lib/server/utils.js +20 -0
  47. package/lib/server.es.js +18 -859
  48. package/lib/server.js +33 -909
  49. package/lib/shared/LayoutModule.browser.js +40 -0
  50. package/lib/shared/LayoutModule.es.js +40 -0
  51. package/lib/shared/LayoutModule.js +42 -0
  52. package/lib/shared/pageErrorStore.browser.js +19 -0
  53. package/lib/shared/pageErrorStore.es.js +19 -0
  54. package/lib/shared/pageErrorStore.js +26 -0
  55. package/lib/shared/providers.browser.js +18 -0
  56. package/lib/shared/providers.es.js +18 -0
  57. package/lib/shared/providers.js +22 -0
  58. package/package.json +23 -24
package/lib/server.es.js CHANGED
@@ -1,869 +1,28 @@
1
1
  import { __decorate } from 'tslib';
2
- import { useMemo, createElement } from 'react';
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 { COMBINE_REDUCERS, CREATE_CACHE_TOKEN, LOGGER_TOKEN, REQUEST_MANAGER_TOKEN, RESPONSE_MANAGER_TOKEN, CONTEXT_TOKEN } from '@tramvai/tokens-common';
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 { ResourceType, ResourceSlot, DEFAULT_LAYOUT_COMPONENT, LAYOUT_OPTIONS, DEFAULT_FOOTER_COMPONENT, DEFAULT_HEADER_COMPONENT, DEFAULT_ERROR_BOUNDARY_COMPONENT, TRAMVAI_RENDER_MODE, 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 } from '@tramvai/tokens-render';
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 { createToken, Scope } from '@tinkoff/dippy';
10
+ import { Scope } from '@tinkoff/dippy';
11
11
  import { WEB_FASTIFY_APP_BEFORE_ERROR_TOKEN } from '@tramvai/tokens-server-private';
12
- import { useDi, ERROR_BOUNDARY_TOKEN, ERROR_BOUNDARY_FALLBACK_COMPONENT_TOKEN, UniversalErrorBoundary, DIContext, ROOT_ERROR_BOUNDARY_COMPONENT_TOKEN } from '@tramvai/react';
13
- import { resolve, isAbsoluteUrl as isAbsoluteUrl$1, parse } from '@tinkoff/url';
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 isUndefined from '@tinkoff/utils/is/undefined';
16
- import isEmpty from '@tinkoff/utils/is/empty';
17
- import fetch from 'node-fetch';
18
- import startsWith from '@tinkoff/utils/string/startsWith';
19
- import toArray from '@tinkoff/utils/array/toArray';
20
- import flatten from '@tinkoff/utils/array/flatten';
21
- import { buildPage, staticRender, dynamicRender } from '@tinkoff/htmlpagebuilder';
22
- import { safeStringify } from '@tramvai/safe-strings';
23
- import { ChunkExtractor } from '@loadable/server';
24
- import has from '@tinkoff/utils/object/has';
25
- import last from '@tinkoff/utils/array/last';
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, PageErrorStore, ReactRenderServer, RenderModule, deserializeError, serializeError, setPageErrorEvent };
288
+ export { DEFAULT_POLYFILL_CONDITION, RenderModule };