@tramvai/module-render 2.141.1 → 2.142.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  import { __decorate } from 'tslib';
2
- import { Module, provide, commandLineListTokens, DI_TOKEN } from '@tramvai/core';
2
+ import { Module, provide, commandLineListTokens, DI_TOKEN, optional } from '@tramvai/core';
3
3
  import { STORE_TOKEN, LOGGER_TOKEN, CONTEXT_TOKEN } from '@tramvai/tokens-common';
4
- import { RESOURCES_REGISTRY, CUSTOM_RENDER, EXTEND_RENDER, RENDERER_CALLBACK, USE_REACT_STRICT_MODE, RENDER_MODE, MODERN_SATISFIES_TOKEN } from '@tramvai/tokens-render';
4
+ import { RESOURCES_REGISTRY, CUSTOM_RENDER, EXTEND_RENDER, RENDERER_CALLBACK, USE_REACT_STRICT_MODE, REACT_SERVER_RENDER_MODE, RENDER_MODE, MODERN_SATISFIES_TOKEN } from '@tramvai/tokens-render';
5
5
  export * from '@tramvai/tokens-render';
6
6
  import { beforeResolveHooksToken, PageErrorStore, setPageErrorEvent } from '@tramvai/module-router';
7
7
  export { PageErrorStore, setPageErrorEvent } from '@tramvai/module-router';
@@ -80,6 +80,7 @@ RenderModule = RenderModule_1 = __decorate([
80
80
  consumerContext: CONTEXT_TOKEN,
81
81
  di: DI_TOKEN,
82
82
  useStrictMode: USE_REACT_STRICT_MODE,
83
+ renderMode: optional(REACT_SERVER_RENDER_MODE),
83
84
  },
84
85
  multi: true,
85
86
  }),
@@ -3,7 +3,7 @@ import { createElement, StrictMode } from 'react';
3
3
  import { renderReact } from '../react/index.browser.js';
4
4
  import { renderer } from './renderer.browser.js';
5
5
 
6
- function rendering({ logger, consumerContext, customRender, extendRender, di, useStrictMode, rendererCallback, }) {
6
+ function rendering({ logger, consumerContext, customRender, extendRender, di, useStrictMode, rendererCallback, renderMode, }) {
7
7
  const log = logger('module-render');
8
8
  return new Promise((resolve, reject) => {
9
9
  let renderResult = renderReact({ di }, consumerContext);
@@ -34,7 +34,7 @@ function rendering({ logger, consumerContext, customRender, extendRender, di, us
34
34
  executeRendererCallbacks();
35
35
  resolve();
36
36
  };
37
- const params = { element: renderResult, container, callback, log };
37
+ const params = { element: renderResult, container, callback, log, renderMode };
38
38
  try {
39
39
  renderer(params);
40
40
  }
@@ -1,6 +1,6 @@
1
- import type { EXTEND_RENDER, RENDERER_CALLBACK, USE_REACT_STRICT_MODE } from '@tramvai/tokens-render';
1
+ import type { EXTEND_RENDER, REACT_SERVER_RENDER_MODE, RENDERER_CALLBACK, USE_REACT_STRICT_MODE } from '@tramvai/tokens-render';
2
2
  import type { ExtractDependencyType } from '@tinkoff/dippy';
3
- export declare function rendering({ logger, consumerContext, customRender, extendRender, di, useStrictMode, rendererCallback, }: {
3
+ export declare function rendering({ logger, consumerContext, customRender, extendRender, di, useStrictMode, rendererCallback, renderMode, }: {
4
4
  logger: any;
5
5
  consumerContext: any;
6
6
  extendRender?: ExtractDependencyType<typeof EXTEND_RENDER>;
@@ -8,4 +8,5 @@ export declare function rendering({ logger, consumerContext, customRender, exten
8
8
  di: any;
9
9
  useStrictMode: ExtractDependencyType<typeof USE_REACT_STRICT_MODE>;
10
10
  rendererCallback?: ExtractDependencyType<typeof RENDERER_CALLBACK>;
11
+ renderMode?: ExtractDependencyType<typeof REACT_SERVER_RENDER_MODE>;
11
12
  }): Promise<void>;
@@ -18,7 +18,7 @@ const ExecuteRenderCallback = ({ children, callback, }) => {
18
18
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
19
  return children;
20
20
  };
21
- const renderer = ({ element, container, callback, log }) => {
21
+ const renderer = ({ element, container, callback, log, renderMode }) => {
22
22
  if (process.env.__TRAMVAI_CONCURRENT_FEATURES && typeof hydrateRoot === 'function') {
23
23
  const wrappedElement = createElement(ExecuteRenderCallback, { callback }, element);
24
24
  let allErrors = new Map();
@@ -35,7 +35,7 @@ const renderer = ({ element, container, callback, log }) => {
35
35
  otherErrors,
36
36
  });
37
37
  });
38
- return startTransition(() => {
38
+ const hydrateRootFn = () => startTransition(() => {
39
39
  hydrateRoot(container, wrappedElement, {
40
40
  onRecoverableError: (error, errorInfo) => {
41
41
  var _a, _b;
@@ -54,6 +54,20 @@ const renderer = ({ element, container, callback, log }) => {
54
54
  },
55
55
  });
56
56
  });
57
+ if (renderMode === 'streaming') {
58
+ // we need to run hydration only after first chunk is sent to client
59
+ // https://github.com/reactwg/react-18/discussions/114
60
+ if (window.__TRAMVAI_DEFERRED_HYDRATION) {
61
+ hydrateRootFn();
62
+ }
63
+ else {
64
+ window.__TRAMVAI_DEFERRED_HYDRATION = hydrateRootFn;
65
+ }
66
+ }
67
+ else {
68
+ hydrateRootFn();
69
+ }
70
+ return;
57
71
  }
58
72
  const { hydrate } = require('react-dom');
59
73
  return hydrate(element, container, callback);
@@ -1,7 +1,10 @@
1
+ import type { ExtractDependencyType } from '@tinkoff/dippy';
2
+ import type { REACT_SERVER_RENDER_MODE } from '@tramvai/tokens-render';
1
3
  type Renderer = (params: {
2
4
  element: any;
3
5
  container: Element;
4
6
  callback: () => void;
5
7
  log: any;
8
+ renderMode?: ExtractDependencyType<typeof REACT_SERVER_RENDER_MODE>;
6
9
  }) => any;
7
10
  export { Renderer };
@@ -1,3 +1,4 @@
1
+ import type { WebpackStats } from '@tramvai/tokens-render';
1
2
  import { ChunkExtractor } from '@loadable/server';
2
3
  export declare const mapResourcesToSlots: (resources: any) => any;
3
4
  export declare class PageBuilder {
@@ -13,7 +14,8 @@ export declare class PageBuilder {
13
14
  private log;
14
15
  private fetchWebpackStats;
15
16
  private di;
16
- constructor({ renderSlots, pageService, resourcesRegistry, context, reactRender, htmlPageSchema, polyfillCondition, htmlAttrs, modern, renderFlowAfter, logger, fetchWebpackStats, di, }: {
17
+ private renderMode;
18
+ constructor({ renderSlots, pageService, resourcesRegistry, context, reactRender, htmlPageSchema, polyfillCondition, htmlAttrs, modern, renderFlowAfter, logger, fetchWebpackStats, di, renderMode, }: {
17
19
  renderSlots: any;
18
20
  pageService: any;
19
21
  resourcesRegistry: any;
@@ -27,6 +29,7 @@ export declare class PageBuilder {
27
29
  logger: any;
28
30
  fetchWebpackStats: any;
29
31
  di: any;
32
+ renderMode: any;
30
33
  });
31
34
  flow(): Promise<string>;
32
35
  dehydrateState(): void;
@@ -34,5 +37,8 @@ export declare class PageBuilder {
34
37
  preloadBlock(): void;
35
38
  generateHtml(): string;
36
39
  private renderSlots;
37
- renderApp(extractor: ChunkExtractor): Promise<void>;
40
+ renderApp({ extractor, stats }: {
41
+ extractor: ChunkExtractor;
42
+ stats: WebpackStats;
43
+ }): Promise<void>;
38
44
  }
@@ -1,6 +1,6 @@
1
1
  import flatten from '@tinkoff/utils/array/flatten';
2
2
  import { buildPage } from '@tinkoff/htmlpagebuilder';
3
- import { ResourceType, ResourceSlot } from '@tramvai/tokens-render';
3
+ import { ResourceSlot, ResourceType } from '@tramvai/tokens-render';
4
4
  import { safeStringify } from '@tramvai/safe-strings';
5
5
  import { ChunkExtractor } from '@loadable/server';
6
6
  import { bundleResource } from './blocks/bundleResource/bundleResource.es.js';
@@ -20,7 +20,7 @@ const mapResourcesToSlots = (resources) => resources.reduce((acc, resource) => {
20
20
  return acc;
21
21
  }, {});
22
22
  class PageBuilder {
23
- constructor({ renderSlots, pageService, resourcesRegistry, context, reactRender, htmlPageSchema, polyfillCondition, htmlAttrs, modern, renderFlowAfter, logger, fetchWebpackStats, di, }) {
23
+ constructor({ renderSlots, pageService, resourcesRegistry, context, reactRender, htmlPageSchema, polyfillCondition, htmlAttrs, modern, renderFlowAfter, logger, fetchWebpackStats, di, renderMode, }) {
24
24
  this.htmlAttrs = htmlAttrs;
25
25
  this.renderSlots = flatten(renderSlots || []);
26
26
  this.pageService = pageService;
@@ -34,12 +34,13 @@ class PageBuilder {
34
34
  this.log = logger('page-builder');
35
35
  this.fetchWebpackStats = fetchWebpackStats;
36
36
  this.di = di;
37
+ this.renderMode = renderMode;
37
38
  }
38
39
  async flow() {
39
40
  const stats = await this.fetchWebpackStats({ modern: this.modern });
40
41
  const extractor = new ChunkExtractor({ stats, entrypoints: [] });
41
42
  // first we render the application, because we need to extract information about the data used by the components
42
- await this.renderApp(extractor);
43
+ await this.renderApp({ extractor, stats });
43
44
  // load information and dependency for the current bundle and page
44
45
  await this.fetchChunksInfo(extractor);
45
46
  await Promise.all(this.renderFlowAfter.map((callback) => callback().catch((error) => {
@@ -53,9 +54,12 @@ class PageBuilder {
53
54
  return this.generateHtml();
54
55
  }
55
56
  dehydrateState() {
57
+ // for streaming we need to have initial state before application scripts,
58
+ // body end will be sent after suspended components will be resolved, but hydration will starl earlier
59
+ const slot = this.renderMode === 'streaming' ? ResourceSlot.HEAD_DYNAMIC_SCRIPTS : ResourceSlot.BODY_END;
56
60
  this.resourcesRegistry.register({
57
61
  type: ResourceType.asIs,
58
- slot: ResourceSlot.BODY_END,
62
+ slot,
59
63
  // String much better than big object, source https://v8.dev/blog/cost-of-javascript-2019#json
60
64
  payload: `<script id="__TRAMVAI_STATE__" type="application/json">${safeStringify(this.context.dehydrate().dispatcher)}</script>`,
61
65
  });
@@ -69,6 +73,7 @@ class PageBuilder {
69
73
  extractor,
70
74
  pageComponent,
71
75
  fetchWebpackStats: this.fetchWebpackStats,
76
+ renderMode: this.renderMode,
72
77
  }));
73
78
  this.resourcesRegistry.register(await polyfillResources({
74
79
  condition: this.polyfillCondition,
@@ -77,6 +82,11 @@ class PageBuilder {
77
82
  }));
78
83
  }
79
84
  preloadBlock() {
85
+ // looks like we don't need this scripts preload at all, but also it is official recommendation for streaming
86
+ // https://github.com/reactwg/react-18/discussions/114
87
+ if (this.renderMode === 'streaming') {
88
+ return;
89
+ }
80
90
  const preloadResources = addPreloadForCriticalJS(this.resourcesRegistry.getPageResources());
81
91
  this.resourcesRegistry.register(preloadResources);
82
92
  }
@@ -90,8 +100,8 @@ class PageBuilder {
90
100
  description: this.htmlPageSchema,
91
101
  });
92
102
  }
93
- async renderApp(extractor) {
94
- const html = await this.reactRender.render(extractor);
103
+ async renderApp({ extractor, stats }) {
104
+ const html = await this.reactRender.render({ extractor, stats });
95
105
  const appHtmlAttrs = formatAttributes(this.htmlAttrs, 'app');
96
106
  this.di.register({ provide: 'tramvai app html attributes', useValue: appHtmlAttrs });
97
107
  this.renderSlots = this.renderSlots.concat({
@@ -28,7 +28,7 @@ const mapResourcesToSlots = (resources) => resources.reduce((acc, resource) => {
28
28
  return acc;
29
29
  }, {});
30
30
  class PageBuilder {
31
- constructor({ renderSlots, pageService, resourcesRegistry, context, reactRender, htmlPageSchema, polyfillCondition, htmlAttrs, modern, renderFlowAfter, logger, fetchWebpackStats, di, }) {
31
+ constructor({ renderSlots, pageService, resourcesRegistry, context, reactRender, htmlPageSchema, polyfillCondition, htmlAttrs, modern, renderFlowAfter, logger, fetchWebpackStats, di, renderMode, }) {
32
32
  this.htmlAttrs = htmlAttrs;
33
33
  this.renderSlots = flatten__default["default"](renderSlots || []);
34
34
  this.pageService = pageService;
@@ -42,12 +42,13 @@ class PageBuilder {
42
42
  this.log = logger('page-builder');
43
43
  this.fetchWebpackStats = fetchWebpackStats;
44
44
  this.di = di;
45
+ this.renderMode = renderMode;
45
46
  }
46
47
  async flow() {
47
48
  const stats = await this.fetchWebpackStats({ modern: this.modern });
48
49
  const extractor = new server.ChunkExtractor({ stats, entrypoints: [] });
49
50
  // first we render the application, because we need to extract information about the data used by the components
50
- await this.renderApp(extractor);
51
+ await this.renderApp({ extractor, stats });
51
52
  // load information and dependency for the current bundle and page
52
53
  await this.fetchChunksInfo(extractor);
53
54
  await Promise.all(this.renderFlowAfter.map((callback) => callback().catch((error) => {
@@ -61,9 +62,12 @@ class PageBuilder {
61
62
  return this.generateHtml();
62
63
  }
63
64
  dehydrateState() {
65
+ // for streaming we need to have initial state before application scripts,
66
+ // body end will be sent after suspended components will be resolved, but hydration will starl earlier
67
+ const slot = this.renderMode === 'streaming' ? tokensRender.ResourceSlot.HEAD_DYNAMIC_SCRIPTS : tokensRender.ResourceSlot.BODY_END;
64
68
  this.resourcesRegistry.register({
65
69
  type: tokensRender.ResourceType.asIs,
66
- slot: tokensRender.ResourceSlot.BODY_END,
70
+ slot,
67
71
  // String much better than big object, source https://v8.dev/blog/cost-of-javascript-2019#json
68
72
  payload: `<script id="__TRAMVAI_STATE__" type="application/json">${safeStrings.safeStringify(this.context.dehydrate().dispatcher)}</script>`,
69
73
  });
@@ -77,6 +81,7 @@ class PageBuilder {
77
81
  extractor,
78
82
  pageComponent,
79
83
  fetchWebpackStats: this.fetchWebpackStats,
84
+ renderMode: this.renderMode,
80
85
  }));
81
86
  this.resourcesRegistry.register(await polyfill.polyfillResources({
82
87
  condition: this.polyfillCondition,
@@ -85,6 +90,11 @@ class PageBuilder {
85
90
  }));
86
91
  }
87
92
  preloadBlock() {
93
+ // looks like we don't need this scripts preload at all, but also it is official recommendation for streaming
94
+ // https://github.com/reactwg/react-18/discussions/114
95
+ if (this.renderMode === 'streaming') {
96
+ return;
97
+ }
88
98
  const preloadResources = preloadBlock.addPreloadForCriticalJS(this.resourcesRegistry.getPageResources());
89
99
  this.resourcesRegistry.register(preloadResources);
90
100
  }
@@ -98,8 +108,8 @@ class PageBuilder {
98
108
  description: this.htmlPageSchema,
99
109
  });
100
110
  }
101
- async renderApp(extractor) {
102
- const html = await this.reactRender.render(extractor);
111
+ async renderApp({ extractor, stats }) {
112
+ const html = await this.reactRender.render({ extractor, stats });
103
113
  const appHtmlAttrs = utils.formatAttributes(this.htmlAttrs, 'app');
104
114
  this.di.register({ provide: 'tramvai app html attributes', useValue: appHtmlAttrs });
105
115
  this.renderSlots = this.renderSlots.concat({
@@ -1,7 +1,7 @@
1
1
  import type { ExtractDependencyType } from '@tinkoff/dippy';
2
2
  import type { DI_TOKEN } from '@tramvai/core';
3
3
  import type { CONTEXT_TOKEN, LOGGER_TOKEN } from '@tramvai/module-common';
4
- import type { EXTEND_RENDER, CUSTOM_RENDER, REACT_SERVER_RENDER_MODE } from '@tramvai/tokens-render';
4
+ import type { EXTEND_RENDER, CUSTOM_RENDER, REACT_SERVER_RENDER_MODE, WebpackStats } from '@tramvai/tokens-render';
5
5
  import type { ChunkExtractor } from '@loadable/server';
6
6
  import type { SERVER_RESPONSE_STREAM, SERVER_RESPONSE_TASK_MANAGER } from '@tramvai/tokens-server-private';
7
7
  export declare class ReactRenderServer {
@@ -23,5 +23,8 @@ export declare class ReactRenderServer {
23
23
  responseTaskManager: any;
24
24
  responseStream: any;
25
25
  });
26
- render(extractor: ChunkExtractor): Promise<string>;
26
+ render({ extractor, stats, }: {
27
+ extractor: ChunkExtractor;
28
+ stats: WebpackStats;
29
+ }): Promise<string>;
27
30
  }
@@ -1,15 +1,51 @@
1
1
  import { Writable } from 'stream';
2
2
  import each from '@tinkoff/utils/array/each';
3
3
  import { renderReact } from '../react/index.es.js';
4
+ import { flushFiles } from './blocks/utils/flushFiles.es.js';
4
5
 
5
- const RENDER_TIMEOUT = 500;
6
+ // @todo customize
7
+ const RENDER_TIMEOUT = 30000;
6
8
  class HtmlWritable extends Writable {
7
- constructor({ responseTaskManager, responseStream, }) {
9
+ constructor({ responseTaskManager, responseStream, extractor, stats, }) {
8
10
  super();
11
+ this.alreadySentChunks = null;
9
12
  this.responseTaskManager = responseTaskManager;
10
13
  this.responseStream = responseStream;
14
+ this.extractor = extractor;
15
+ this.stats = stats;
11
16
  }
12
17
  _write(chunk, encoding, callback) {
18
+ if (!this.alreadySentChunks) {
19
+ // at first _write, all rendered lazy chunks will be saved here
20
+ this.alreadySentChunks = this.extractor.getMainAssets().map((entry) => entry.chunk);
21
+ }
22
+ else {
23
+ // then, lazy chunks from resolved suspended components will be here
24
+ const newChunks = this.extractor.getMainAssets().map((entry) => entry.chunk);
25
+ newChunks.forEach((c) => {
26
+ if (this.alreadySentChunks.includes(c)) {
27
+ return;
28
+ }
29
+ this.alreadySentChunks.push(c);
30
+ // @todo a lot of duplicate code with `bundleResource`?
31
+ const { publicPath } = this.stats;
32
+ const { scripts, styles } = flushFiles([c], this.stats);
33
+ const genHref = (href) => `${publicPath}${href}`;
34
+ const html = [];
35
+ // we need to inject styles and scripts for lazy components before selective hydration
36
+ // https://github.com/reactwg/react-18/discussions/114
37
+ styles.forEach((s) => {
38
+ html.push(`<link rel="stylesheet" href="${genHref(s)}" crossorigin="anonymous" data-critical="true" />`);
39
+ });
40
+ // synchronius script, we can't use async here, will lead to hydration missmatch
41
+ scripts.forEach((s) => {
42
+ html.push(`<script src="${genHref(s)}" charset="utf-8" crossorigin="anonymous" data-critical="true"></script>`);
43
+ });
44
+ this.responseTaskManager.push(async () => {
45
+ this.responseStream.push(html.join('\n'));
46
+ });
47
+ });
48
+ }
13
49
  const html = chunk.toString('utf-8');
14
50
  // delay writing HTML to response stream
15
51
  // @todo some priorities, to prevent conflicts with deferred actions scripts?
@@ -41,7 +77,7 @@ class ReactRenderServer {
41
77
  this.responseTaskManager = responseTaskManager;
42
78
  this.responseStream = responseStream;
43
79
  }
44
- render(extractor) {
80
+ render({ extractor, stats, }) {
45
81
  var _a;
46
82
  let renderResult = renderReact({ di: this.di }, this.context);
47
83
  each((render) => {
@@ -55,7 +91,12 @@ class ReactRenderServer {
55
91
  return new Promise((resolve, reject) => {
56
92
  const { renderToPipeableStream } = require('react-dom/server');
57
93
  const { responseTaskManager, responseStream, log } = this;
58
- const htmlWritable = new HtmlWritable({ responseTaskManager, responseStream });
94
+ const htmlWritable = new HtmlWritable({
95
+ responseTaskManager,
96
+ responseStream,
97
+ extractor,
98
+ stats,
99
+ });
59
100
  const allReadyDeferred = Deferred();
60
101
  const start = Date.now();
61
102
  // prevent sent reply before all suspended components are resolved
@@ -63,10 +104,17 @@ class ReactRenderServer {
63
104
  // eslint-disable-next-line promise/param-names
64
105
  return allReadyDeferred.promise;
65
106
  });
107
+ htmlWritable.on('finish', () => {
108
+ // here all suspended components are resolved
109
+ allReadyDeferred.resolve();
110
+ });
66
111
  log.info({
67
112
  event: 'streaming-render:start',
68
113
  });
69
114
  const { pipe, abort } = renderToPipeableStream(renderResult, {
115
+ // we need to run hydration only after first chunk is sent to client
116
+ // https://github.com/reactwg/react-18/discussions/114
117
+ bootstrapScriptContent: `typeof window.__TRAMVAI_DEFERRED_HYDRATION === 'function' ? window.__TRAMVAI_DEFERRED_HYDRATION() : window.__TRAMVAI_DEFERRED_HYDRATION = true;`,
70
118
  onShellReady() {
71
119
  log.info({
72
120
  event: 'streaming-render:shell-ready',
@@ -82,8 +130,6 @@ class ReactRenderServer {
82
130
  event: 'streaming-render:all-ready',
83
131
  duration: Date.now() - start,
84
132
  });
85
- // here all suspended components are resolved
86
- allReadyDeferred.resolve();
87
133
  },
88
134
  onError(error) {
89
135
  // error can be inside Suspense boundaries, this is not critical, continue rendering.
@@ -5,19 +5,55 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var stream = require('stream');
6
6
  var each = require('@tinkoff/utils/array/each');
7
7
  var index = require('../react/index.js');
8
+ var flushFiles = require('./blocks/utils/flushFiles.js');
8
9
 
9
10
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
10
11
 
11
12
  var each__default = /*#__PURE__*/_interopDefaultLegacy(each);
12
13
 
13
- const RENDER_TIMEOUT = 500;
14
+ // @todo customize
15
+ const RENDER_TIMEOUT = 30000;
14
16
  class HtmlWritable extends stream.Writable {
15
- constructor({ responseTaskManager, responseStream, }) {
17
+ constructor({ responseTaskManager, responseStream, extractor, stats, }) {
16
18
  super();
19
+ this.alreadySentChunks = null;
17
20
  this.responseTaskManager = responseTaskManager;
18
21
  this.responseStream = responseStream;
22
+ this.extractor = extractor;
23
+ this.stats = stats;
19
24
  }
20
25
  _write(chunk, encoding, callback) {
26
+ if (!this.alreadySentChunks) {
27
+ // at first _write, all rendered lazy chunks will be saved here
28
+ this.alreadySentChunks = this.extractor.getMainAssets().map((entry) => entry.chunk);
29
+ }
30
+ else {
31
+ // then, lazy chunks from resolved suspended components will be here
32
+ const newChunks = this.extractor.getMainAssets().map((entry) => entry.chunk);
33
+ newChunks.forEach((c) => {
34
+ if (this.alreadySentChunks.includes(c)) {
35
+ return;
36
+ }
37
+ this.alreadySentChunks.push(c);
38
+ // @todo a lot of duplicate code with `bundleResource`?
39
+ const { publicPath } = this.stats;
40
+ const { scripts, styles } = flushFiles.flushFiles([c], this.stats);
41
+ const genHref = (href) => `${publicPath}${href}`;
42
+ const html = [];
43
+ // we need to inject styles and scripts for lazy components before selective hydration
44
+ // https://github.com/reactwg/react-18/discussions/114
45
+ styles.forEach((s) => {
46
+ html.push(`<link rel="stylesheet" href="${genHref(s)}" crossorigin="anonymous" data-critical="true" />`);
47
+ });
48
+ // synchronius script, we can't use async here, will lead to hydration missmatch
49
+ scripts.forEach((s) => {
50
+ html.push(`<script src="${genHref(s)}" charset="utf-8" crossorigin="anonymous" data-critical="true"></script>`);
51
+ });
52
+ this.responseTaskManager.push(async () => {
53
+ this.responseStream.push(html.join('\n'));
54
+ });
55
+ });
56
+ }
21
57
  const html = chunk.toString('utf-8');
22
58
  // delay writing HTML to response stream
23
59
  // @todo some priorities, to prevent conflicts with deferred actions scripts?
@@ -49,7 +85,7 @@ class ReactRenderServer {
49
85
  this.responseTaskManager = responseTaskManager;
50
86
  this.responseStream = responseStream;
51
87
  }
52
- render(extractor) {
88
+ render({ extractor, stats, }) {
53
89
  var _a;
54
90
  let renderResult = index.renderReact({ di: this.di }, this.context);
55
91
  each__default["default"]((render) => {
@@ -63,7 +99,12 @@ class ReactRenderServer {
63
99
  return new Promise((resolve, reject) => {
64
100
  const { renderToPipeableStream } = require('react-dom/server');
65
101
  const { responseTaskManager, responseStream, log } = this;
66
- const htmlWritable = new HtmlWritable({ responseTaskManager, responseStream });
102
+ const htmlWritable = new HtmlWritable({
103
+ responseTaskManager,
104
+ responseStream,
105
+ extractor,
106
+ stats,
107
+ });
67
108
  const allReadyDeferred = Deferred();
68
109
  const start = Date.now();
69
110
  // prevent sent reply before all suspended components are resolved
@@ -71,10 +112,17 @@ class ReactRenderServer {
71
112
  // eslint-disable-next-line promise/param-names
72
113
  return allReadyDeferred.promise;
73
114
  });
115
+ htmlWritable.on('finish', () => {
116
+ // here all suspended components are resolved
117
+ allReadyDeferred.resolve();
118
+ });
74
119
  log.info({
75
120
  event: 'streaming-render:start',
76
121
  });
77
122
  const { pipe, abort } = renderToPipeableStream(renderResult, {
123
+ // we need to run hydration only after first chunk is sent to client
124
+ // https://github.com/reactwg/react-18/discussions/114
125
+ bootstrapScriptContent: `typeof window.__TRAMVAI_DEFERRED_HYDRATION === 'function' ? window.__TRAMVAI_DEFERRED_HYDRATION() : window.__TRAMVAI_DEFERRED_HYDRATION = true;`,
78
126
  onShellReady() {
79
127
  log.info({
80
128
  event: 'streaming-render:shell-ready',
@@ -90,8 +138,6 @@ class ReactRenderServer {
90
138
  event: 'streaming-render:all-ready',
91
139
  duration: Date.now() - start,
92
140
  });
93
- // here all suspended components are resolved
94
- allReadyDeferred.resolve();
95
141
  },
96
142
  onError(error) {
97
143
  // error can be inside Suspense boundaries, this is not critical, continue rendering.
@@ -1,9 +1,10 @@
1
1
  import type { ChunkExtractor } from '@loadable/server';
2
- import type { PageResource, FETCH_WEBPACK_STATS_TOKEN } from '@tramvai/tokens-render';
3
- export declare const bundleResource: ({ bundle, modern, extractor, pageComponent, fetchWebpackStats, }: {
2
+ import type { PageResource, FETCH_WEBPACK_STATS_TOKEN, REACT_SERVER_RENDER_MODE } from '@tramvai/tokens-render';
3
+ export declare const bundleResource: ({ bundle, modern, extractor, pageComponent, fetchWebpackStats, renderMode, }: {
4
4
  bundle: string;
5
5
  modern: boolean;
6
6
  extractor: ChunkExtractor;
7
7
  pageComponent?: string;
8
8
  fetchWebpackStats: typeof FETCH_WEBPACK_STATS_TOKEN;
9
+ renderMode: typeof REACT_SERVER_RENDER_MODE | null;
9
10
  }) => Promise<PageResource[]>;
@@ -5,6 +5,14 @@ import { isFileSystemPageComponent, fileSystemPageToWebpackChunkName } from '@tr
5
5
  import { PRELOAD_JS } from '../../constants/performance.es.js';
6
6
  import { flushFiles } from '../utils/flushFiles.es.js';
7
7
 
8
+ const asyncScriptAttrs = {
9
+ defer: null,
10
+ async: 'async',
11
+ };
12
+ const deferScriptAttrs = {
13
+ defer: 'defer',
14
+ async: null,
15
+ };
8
16
  let criticalChunks = [];
9
17
  try {
10
18
  criticalChunks = JSON.parse(process.env.__TRAMVAI_CRITICAL_CHUNKS);
@@ -12,7 +20,7 @@ try {
12
20
  catch (e) {
13
21
  // do nothing
14
22
  }
15
- const bundleResource = async ({ bundle, modern, extractor, pageComponent, fetchWebpackStats, }) => {
23
+ const bundleResource = async ({ bundle, modern, extractor, pageComponent, fetchWebpackStats, renderMode, }) => {
16
24
  // for file-system pages preload page chunk against bundle chunk
17
25
  const chunkNameFromBundle = isFileSystemPageComponent(pageComponent)
18
26
  ? fileSystemPageToWebpackChunkName(pageComponent)
@@ -37,13 +45,18 @@ const bundleResource = async ({ bundle, modern, extractor, pageComponent, fetchW
37
45
  payload: `window.ap = ${`"${process.env.ASSETS_PREFIX}"`};`,
38
46
  });
39
47
  }
48
+ // defer scripts is not suitable for React streaming, we need to ability to run them as early as possible
49
+ // https://github.com/reactwg/react-18/discussions/114
50
+ const scriptTypeAttr = renderMode === 'streaming' ? asyncScriptAttrs : deferScriptAttrs;
40
51
  styles.map((style) => result.push({
41
52
  type: ResourceType.style,
42
53
  slot: ResourceSlot.HEAD_CORE_STYLES,
43
54
  payload: genHref(style),
44
55
  attrs: {
45
56
  'data-critical': 'true',
46
- onload: `${PRELOAD_JS}()`,
57
+ // looks like we don't need this scripts preload at all, but also it is official recommendation for streaming
58
+ // https://github.com/reactwg/react-18/discussions/114
59
+ onload: renderMode === 'streaming' ? null : `${PRELOAD_JS}()`,
47
60
  },
48
61
  }));
49
62
  baseScripts.map((script) => result.push({
@@ -52,6 +65,7 @@ const bundleResource = async ({ bundle, modern, extractor, pageComponent, fetchW
52
65
  payload: genHref(script),
53
66
  attrs: {
54
67
  'data-critical': 'true',
68
+ ...scriptTypeAttr,
55
69
  },
56
70
  }));
57
71
  scripts.map((script) => result.push({
@@ -60,6 +74,7 @@ const bundleResource = async ({ bundle, modern, extractor, pageComponent, fetchW
60
74
  payload: genHref(script),
61
75
  attrs: {
62
76
  'data-critical': 'true',
77
+ ...scriptTypeAttr,
63
78
  },
64
79
  }));
65
80
  return result;
@@ -14,6 +14,14 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
14
14
  var has__default = /*#__PURE__*/_interopDefaultLegacy(has);
15
15
  var last__default = /*#__PURE__*/_interopDefaultLegacy(last);
16
16
 
17
+ const asyncScriptAttrs = {
18
+ defer: null,
19
+ async: 'async',
20
+ };
21
+ const deferScriptAttrs = {
22
+ defer: 'defer',
23
+ async: null,
24
+ };
17
25
  let criticalChunks = [];
18
26
  try {
19
27
  criticalChunks = JSON.parse(process.env.__TRAMVAI_CRITICAL_CHUNKS);
@@ -21,7 +29,7 @@ try {
21
29
  catch (e) {
22
30
  // do nothing
23
31
  }
24
- const bundleResource = async ({ bundle, modern, extractor, pageComponent, fetchWebpackStats, }) => {
32
+ const bundleResource = async ({ bundle, modern, extractor, pageComponent, fetchWebpackStats, renderMode, }) => {
25
33
  // for file-system pages preload page chunk against bundle chunk
26
34
  const chunkNameFromBundle = experiments.isFileSystemPageComponent(pageComponent)
27
35
  ? experiments.fileSystemPageToWebpackChunkName(pageComponent)
@@ -46,13 +54,18 @@ const bundleResource = async ({ bundle, modern, extractor, pageComponent, fetchW
46
54
  payload: `window.ap = ${`"${process.env.ASSETS_PREFIX}"`};`,
47
55
  });
48
56
  }
57
+ // defer scripts is not suitable for React streaming, we need to ability to run them as early as possible
58
+ // https://github.com/reactwg/react-18/discussions/114
59
+ const scriptTypeAttr = renderMode === 'streaming' ? asyncScriptAttrs : deferScriptAttrs;
49
60
  styles.map((style) => result.push({
50
61
  type: tokensRender.ResourceType.style,
51
62
  slot: tokensRender.ResourceSlot.HEAD_CORE_STYLES,
52
63
  payload: genHref(style),
53
64
  attrs: {
54
65
  'data-critical': 'true',
55
- onload: `${performance.PRELOAD_JS}()`,
66
+ // looks like we don't need this scripts preload at all, but also it is official recommendation for streaming
67
+ // https://github.com/reactwg/react-18/discussions/114
68
+ onload: renderMode === 'streaming' ? null : `${performance.PRELOAD_JS}()`,
56
69
  },
57
70
  }));
58
71
  baseScripts.map((script) => result.push({
@@ -61,6 +74,7 @@ const bundleResource = async ({ bundle, modern, extractor, pageComponent, fetchW
61
74
  payload: genHref(script),
62
75
  attrs: {
63
76
  'data-critical': 'true',
77
+ ...scriptTypeAttr,
64
78
  },
65
79
  }));
66
80
  scripts.map((script) => result.push({
@@ -69,6 +83,7 @@ const bundleResource = async ({ bundle, modern, extractor, pageComponent, fetchW
69
83
  payload: genHref(script),
70
84
  attrs: {
71
85
  'data-critical': 'true',
86
+ ...scriptTypeAttr,
72
87
  },
73
88
  }));
74
89
  return result;
package/lib/server.es.js CHANGED
@@ -3,9 +3,9 @@ import { Module, provide, commandLineListTokens, DI_TOKEN } from '@tramvai/core'
3
3
  import { CREATE_CACHE_TOKEN, LOGGER_TOKEN, REQUEST_MANAGER_TOKEN, RESPONSE_MANAGER_TOKEN, CONTEXT_TOKEN } from '@tramvai/tokens-common';
4
4
  import { PAGE_SERVICE_TOKEN } from '@tramvai/tokens-router';
5
5
  import { ClientHintsModule, USER_AGENT_TOKEN } from '@tramvai/module-client-hints';
6
- import { RESOURCES_REGISTRY, RESOURCE_INLINE_OPTIONS, BACK_FORWARD_CACHE_ENABLED, RENDER_SLOTS, POLYFILL_CONDITION, HTML_ATTRS, MODERN_SATISFIES_TOKEN, RENDER_FLOW_AFTER_TOKEN, FETCH_WEBPACK_STATS_TOKEN, CUSTOM_RENDER, EXTEND_RENDER, REACT_SERVER_RENDER_MODE, ResourceType } from '@tramvai/tokens-render';
6
+ import { RESOURCES_REGISTRY, RESOURCE_INLINE_OPTIONS, BACK_FORWARD_CACHE_ENABLED, RENDER_SLOTS, POLYFILL_CONDITION, HTML_ATTRS, MODERN_SATISFIES_TOKEN, RENDER_FLOW_AFTER_TOKEN, FETCH_WEBPACK_STATS_TOKEN, REACT_SERVER_RENDER_MODE, CUSTOM_RENDER, EXTEND_RENDER, ResourceType } from '@tramvai/tokens-render';
7
7
  export * from '@tramvai/tokens-render';
8
- import { Scope } from '@tinkoff/dippy';
8
+ import { Scope, optional } from '@tinkoff/dippy';
9
9
  import { satisfies } from '@tinkoff/user-agent';
10
10
  import { isRedirectFoundError } from '@tinkoff/errors';
11
11
  import { setPageErrorEvent, PageErrorStore, deserializeError } from '@tramvai/module-router';
@@ -180,7 +180,6 @@ Page Error Boundary will be rendered for the client`,
180
180
  pageService: PAGE_SERVICE_TOKEN,
181
181
  bfcacheEnabled: BACK_FORWARD_CACHE_ENABLED,
182
182
  },
183
- multi: true,
184
183
  }),
185
184
  provide({
186
185
  provide: 'htmlBuilder',
@@ -199,6 +198,7 @@ Page Error Boundary will be rendered for the client`,
199
198
  logger: LOGGER_TOKEN,
200
199
  fetchWebpackStats: FETCH_WEBPACK_STATS_TOKEN,
201
200
  di: DI_TOKEN,
201
+ renderMode: optional(REACT_SERVER_RENDER_MODE),
202
202
  },
203
203
  }),
204
204
  provide({
package/lib/server.js CHANGED
@@ -181,7 +181,6 @@ Page Error Boundary will be rendered for the client`,
181
181
  pageService: tokensRouter.PAGE_SERVICE_TOKEN,
182
182
  bfcacheEnabled: tokensRender.BACK_FORWARD_CACHE_ENABLED,
183
183
  },
184
- multi: true,
185
184
  }),
186
185
  core.provide({
187
186
  provide: 'htmlBuilder',
@@ -200,6 +199,7 @@ Page Error Boundary will be rendered for the client`,
200
199
  logger: tokensCommon.LOGGER_TOKEN,
201
200
  fetchWebpackStats: tokensRender.FETCH_WEBPACK_STATS_TOKEN,
202
201
  di: core.DI_TOKEN,
202
+ renderMode: dippy.optional(tokensRender.REACT_SERVER_RENDER_MODE),
203
203
  },
204
204
  }),
205
205
  core.provide({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tramvai/module-render",
3
- "version": "2.141.1",
3
+ "version": "2.142.0",
4
4
  "description": "",
5
5
  "browser": "lib/browser.js",
6
6
  "main": "lib/server.js",
@@ -26,13 +26,13 @@
26
26
  "@tinkoff/layout-factory": "0.3.8",
27
27
  "@tinkoff/errors": "0.3.8",
28
28
  "@tinkoff/url": "0.8.6",
29
- "@tinkoff/user-agent": "0.4.412",
30
- "@tramvai/module-client-hints": "2.141.1",
31
- "@tramvai/module-router": "2.141.1",
32
- "@tramvai/react": "2.141.1",
29
+ "@tinkoff/user-agent": "0.4.414",
30
+ "@tramvai/module-client-hints": "2.142.0",
31
+ "@tramvai/module-router": "2.142.0",
32
+ "@tramvai/react": "2.142.0",
33
33
  "@tramvai/safe-strings": "0.5.11",
34
- "@tramvai/tokens-render": "2.141.1",
35
- "@tramvai/experiments": "2.141.1",
34
+ "@tramvai/tokens-render": "2.142.0",
35
+ "@tramvai/experiments": "2.142.0",
36
36
  "@types/loadable__server": "^5.12.6",
37
37
  "node-fetch": "^2.6.1"
38
38
  },
@@ -40,14 +40,14 @@
40
40
  "@tinkoff/dippy": "0.8.15",
41
41
  "@tinkoff/utils": "^2.1.2",
42
42
  "@tinkoff/react-hooks": "0.1.6",
43
- "@tramvai/cli": "2.141.1",
44
- "@tramvai/core": "2.141.1",
45
- "@tramvai/module-common": "2.141.1",
46
- "@tramvai/state": "2.141.1",
47
- "@tramvai/test-helpers": "2.141.1",
48
- "@tramvai/tokens-common": "2.141.1",
49
- "@tramvai/tokens-router": "2.141.1",
50
- "@tramvai/tokens-server-private": "2.141.1",
43
+ "@tramvai/cli": "2.142.0",
44
+ "@tramvai/core": "2.142.0",
45
+ "@tramvai/module-common": "2.142.0",
46
+ "@tramvai/state": "2.142.0",
47
+ "@tramvai/test-helpers": "2.142.0",
48
+ "@tramvai/tokens-common": "2.142.0",
49
+ "@tramvai/tokens-router": "2.142.0",
50
+ "@tramvai/tokens-server-private": "2.142.0",
51
51
  "express": "^4.17.1",
52
52
  "prop-types": "^15.6.2",
53
53
  "react": ">=16.14.0",