@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 +3 -2
- package/lib/client/index.browser.js +2 -2
- package/lib/client/index.d.ts +3 -2
- package/lib/client/renderer.browser.js +16 -2
- package/lib/client/types.d.ts +3 -0
- package/lib/server/PageBuilder.d.ts +8 -2
- package/lib/server/PageBuilder.es.js +16 -6
- package/lib/server/PageBuilder.js +15 -5
- package/lib/server/ReactRenderServer.d.ts +5 -2
- package/lib/server/ReactRenderServer.es.js +52 -6
- package/lib/server/ReactRenderServer.js +52 -6
- package/lib/server/blocks/bundleResource/bundleResource.d.ts +3 -2
- package/lib/server/blocks/bundleResource/bundleResource.es.js +17 -2
- package/lib/server/blocks/bundleResource/bundleResource.js +17 -2
- package/lib/server.es.js +3 -3
- package/lib/server.js +1 -1
- package/package.json +15 -15
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
|
}
|
package/lib/client/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/lib/client/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
30
|
-
"@tramvai/module-client-hints": "2.
|
|
31
|
-
"@tramvai/module-router": "2.
|
|
32
|
-
"@tramvai/react": "2.
|
|
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.
|
|
35
|
-
"@tramvai/experiments": "2.
|
|
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.
|
|
44
|
-
"@tramvai/core": "2.
|
|
45
|
-
"@tramvai/module-common": "2.
|
|
46
|
-
"@tramvai/state": "2.
|
|
47
|
-
"@tramvai/test-helpers": "2.
|
|
48
|
-
"@tramvai/tokens-common": "2.
|
|
49
|
-
"@tramvai/tokens-router": "2.
|
|
50
|
-
"@tramvai/tokens-server-private": "2.
|
|
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",
|