arui-scripts 23.5.0 → 23.6.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/.turbo/turbo-test.log +8 -7
- package/CHANGELOG.md +18 -0
- package/build/commands/util/yarn.js +1 -1
- package/build/configs/modules-css-manifest.d.ts +11 -0
- package/build/configs/modules-css-manifest.js +14 -0
- package/build/configs/modules.js +12 -1
- package/build/configs/process-assets-plugin-output.js +10 -4
- package/build/plugins/attribute-module-css.d.ts +35 -0
- package/build/plugins/attribute-module-css.js +109 -0
- package/docs/modules.md +226 -0
- package/package.json +1 -1
- package/tsconfig-local.tsbuildinfo +1 -1
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
ts-jest[versions] (WARN) Version 6.0.2 of typescript installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=4.3.0 <6.0.0-0). Please do not report issues in ts-jest if you are using unsupported versions.
|
|
2
2
|
ts-jest[versions] (WARN) Version 6.0.2 of typescript installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=4.3.0 <6.0.0-0). Please do not report issues in ts-jest if you are using unsupported versions.
|
|
3
3
|
ts-jest[versions] (WARN) Version 6.0.2 of typescript installed has not been tested with ts-jest. If you're experiencing issues, consider using a supported version (>=4.3.0 <6.0.0-0). Please do not report issues in ts-jest if you are using unsupported versions.
|
|
4
|
-
PASS src/templates/__tests__/dockerfile-templates.tests.ts (
|
|
4
|
+
PASS src/templates/__tests__/dockerfile-templates.tests.ts (34.516 s)
|
|
5
|
+
PASS src/plugins/postcss-global-variables/utils/__tests__/add-global-variable.tests.ts
|
|
5
6
|
PASS src/bin/__tests__/create-cli.tests.ts
|
|
6
7
|
PASS src/configs/app-configs/__tests__/update-with-presets.tests.ts
|
|
7
|
-
PASS src/plugins/postcss-global-variables/utils/__tests__/add-global-variable.tests.ts (32.977 s)
|
|
8
8
|
PASS src/configs/util/__tests__/create-watch-ignore-regex.tests.ts
|
|
9
9
|
PASS src/configs/util/__tests__/apply-overrides.tests.ts
|
|
10
10
|
PASS src/configs/app-configs/__tests__/update-with-env.tests.ts
|
|
11
|
+
PASS src/plugins/__tests__/attribute-module-css.tests.ts (42.07 s)
|
|
11
12
|
PASS src/configs/app-configs/__tests__/update-with-package.tests.ts
|
|
12
13
|
PASS src/configs/util/__tests__/compress-with-dcb.test.ts
|
|
13
14
|
PASS src/configs/util/__tests__/get-polyfills.tests.ts
|
|
14
15
|
PASS src/plugins/postcss-global-variables/utils/__tests__/parse-variables.tests.ts
|
|
15
16
|
PASS src/plugins/postcss-global-variables/utils/__tests__/get-media-query-name.tests.ts
|
|
16
17
|
PASS src/configs/app-configs/__tests__/validate-settings-keys.tests.ts
|
|
17
|
-
PASS src/configs/__tests__/
|
|
18
|
-
PASS src/configs/
|
|
18
|
+
PASS src/configs/util/__tests__/find-plugin.tests.ts (46.322 s)
|
|
19
|
+
PASS src/configs/__tests__/webpack-client-modules.tests.ts (11.433 s)
|
|
19
20
|
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
|
|
20
21
|
|
|
21
|
-
Test Suites:
|
|
22
|
-
Tests:
|
|
22
|
+
Test Suites: 16 passed, 16 total
|
|
23
|
+
Tests: 51 passed, 51 total
|
|
23
24
|
Snapshots: 0 total
|
|
24
|
-
Time:
|
|
25
|
+
Time: 49.282 s
|
|
25
26
|
Ran all test suites.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# arui-scripts
|
|
2
2
|
|
|
3
|
+
## 23.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#532](https://github.com/core-ds/arui-scripts/pull/532) [`b82a10c`](https://github.com/core-ds/arui-scripts/commit/b82a10c4e90e625ff538f47528de0869f798d6d4) Thanks [@heymdall-legal](https://github.com/heymdall-legal)! - CSS module-federation модулей теперь попадает в манифест сборки на уровне каждого модуля.
|
|
8
|
+
Раньше стили модуля не были привязаны к его записи в манифесте, из-за чего хост-сервер не мог
|
|
9
|
+
отдать их при серверном рендере, и модуль отрисовывался без стилей до загрузки на клиенте.
|
|
10
|
+
Теперь стили доступны в записи модуля и могут быть встроены при SSR. Изменение аддитивно: для
|
|
11
|
+
сборок без модулей и для не-SSR потребления манифест остаётся прежним.
|
|
12
|
+
|
|
13
|
+
Подробнее: [SSR-спецификация модулей, раздел 10](../docs/specs/ssr-spec.md#10-addendum-css-delivery-for-module-federation-modules)..
|
|
14
|
+
|
|
15
|
+
## 23.5.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- [#574](https://github.com/core-ds/arui-scripts/pull/574) [`2e48012`](https://github.com/core-ds/arui-scripts/commit/2e4801232e988fc8da639faac6e6e5fd716118e8) Thanks [@artemgafarov66](https://github.com/artemgafarov66)! - Исправлено создание ссылки на Yarn при сборке Docker-образа, если бинарник уже существует.
|
|
20
|
+
|
|
3
21
|
## 23.5.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Разделяемое хранилище css module-federation модулей.
|
|
3
|
+
*
|
|
4
|
+
* `AttributeModuleCssPlugin` наполняет его во время сборки (обходя граф чанков),
|
|
5
|
+
* а `processAssetsPluginOutput` читает при формировании json манифеста и
|
|
6
|
+
* дописывает css в записи соответствующих модулей.
|
|
7
|
+
*
|
|
8
|
+
* Map живёт на уровне модуля, поскольку плагин сборки и обработчик манифеста
|
|
9
|
+
* работают в одном процессе одной сборки (как и общий между сборками AssetsPlugin).
|
|
10
|
+
*/
|
|
11
|
+
export declare const modulesCssManifest: Map<string, string[]>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.modulesCssManifest = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Разделяемое хранилище css module-federation модулей.
|
|
6
|
+
*
|
|
7
|
+
* `AttributeModuleCssPlugin` наполняет его во время сборки (обходя граф чанков),
|
|
8
|
+
* а `processAssetsPluginOutput` читает при формировании json манифеста и
|
|
9
|
+
* дописывает css в записи соответствующих модулей.
|
|
10
|
+
*
|
|
11
|
+
* Map живёт на уровне модуля, поскольку плагин сборки и обработчик манифеста
|
|
12
|
+
* работают в одном процессе одной сборки (как и общий между сборками AssetsPlugin).
|
|
13
|
+
*/
|
|
14
|
+
exports.modulesCssManifest = new Map();
|
package/build/configs/modules.js
CHANGED
|
@@ -40,10 +40,12 @@ exports.getCssPrefixForModule = getCssPrefixForModule;
|
|
|
40
40
|
exports.getExposeLoadersFormCompatModules = getExposeLoadersFormCompatModules;
|
|
41
41
|
exports.patchWebpackConfigForCompat = patchWebpackConfigForCompat;
|
|
42
42
|
const rspack = __importStar(require("@rspack/core"));
|
|
43
|
+
const attribute_module_css_1 = require("../plugins/attribute-module-css");
|
|
43
44
|
const postcss_prefix_selector_1 = require("../plugins/postcss-prefix-selector");
|
|
44
45
|
const turn_off_split_remote_entry_1 = require("../plugins/turn-off-split-remote-entry");
|
|
45
46
|
const find_loader_1 = require("./util/find-loader");
|
|
46
47
|
const app_configs_1 = require("./app-configs");
|
|
48
|
+
const modules_css_manifest_1 = require("./modules-css-manifest");
|
|
47
49
|
function haveExposedDefaultModules() {
|
|
48
50
|
var _a;
|
|
49
51
|
return (_a = app_configs_1.configs.modules) === null || _a === void 0 ? void 0 : _a.exposes;
|
|
@@ -100,10 +102,19 @@ function patchMainRspackConfigForModules(webpackConf, mode) {
|
|
|
100
102
|
app_configs_1.configs.modules.shared,
|
|
101
103
|
exposes: isProvider ? app_configs_1.configs.modules.exposes : {},
|
|
102
104
|
shareScope: app_configs_1.configs.modules.shareScope,
|
|
103
|
-
}), new turn_off_split_remote_entry_1.TurnOffSplitRemoteEntry(getModuleFederationContainerName())
|
|
105
|
+
}), new turn_off_split_remote_entry_1.TurnOffSplitRemoteEntry(getModuleFederationContainerName()),
|
|
106
|
+
// на провайдере добавляем css чанки модулей в манифест
|
|
107
|
+
...getModuleCssPlugins(isProvider));
|
|
104
108
|
return webpackConf;
|
|
105
109
|
/* eslint-enable no-param-reassign */
|
|
106
110
|
}
|
|
111
|
+
function getModuleCssPlugins(isProvider) {
|
|
112
|
+
var _a;
|
|
113
|
+
if (!isProvider || !((_a = app_configs_1.configs.modules) === null || _a === void 0 ? void 0 : _a.exposes)) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
return [new attribute_module_css_1.AttributeModuleCssPlugin(app_configs_1.configs.modules.exposes, modules_css_manifest_1.modulesCssManifest)];
|
|
117
|
+
}
|
|
107
118
|
/** @deprecated используйте `patchMainRspackConfigForModules` */
|
|
108
119
|
exports.patchMainWebpackConfigForModules = patchMainRspackConfigForModules;
|
|
109
120
|
function getCssPrefixForModule(module) {
|
|
@@ -7,6 +7,7 @@ exports.processAssetsPluginOutput = processAssetsPluginOutput;
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const app_configs_1 = require("./app-configs");
|
|
9
9
|
const modules_1 = require("./modules");
|
|
10
|
+
const modules_css_manifest_1 = require("./modules-css-manifest");
|
|
10
11
|
function processAssetsPluginOutput(assets) {
|
|
11
12
|
var _a;
|
|
12
13
|
const adjustedAssets = assets;
|
|
@@ -23,10 +24,15 @@ function processAssetsPluginOutput(assets) {
|
|
|
23
24
|
if ((_b = (_a = app_configs_1.configs.compatModules) === null || _a === void 0 ? void 0 : _a.exposes) === null || _b === void 0 ? void 0 : _b[moduleName]) {
|
|
24
25
|
throw new Error(`Модуль ${moduleName} определен как module и как compat. Поменяйте название одного из модулей или удалите его`);
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
// css собирает AttributeModuleCssPlugin (обход графа чанков).
|
|
28
|
+
// Пути в манифесте держим с publicPath, как и remoteEntry.js.
|
|
29
|
+
const moduleCss = modules_css_manifest_1.modulesCssManifest.get(moduleName);
|
|
30
|
+
// css здесь массив (per-expose может быть несколько css-чанков), что шире
|
|
31
|
+
// строкового индекса типа Assets, но манифест сериализуется в json, а читатели
|
|
32
|
+
// (AruiAppManifest, createGetModulesMethod) уже принимают string | string[].
|
|
33
|
+
adjustedAssets[moduleName] = Object.assign({ mode: 'default', js: path_1.default.join(app_configs_1.configs.publicPath, modules_1.MODULES_ENTRY_NAME) }, (moduleCss && moduleCss.length > 0
|
|
34
|
+
? { css: moduleCss.map((file) => path_1.default.join(app_configs_1.configs.publicPath, file)) }
|
|
35
|
+
: {}));
|
|
30
36
|
});
|
|
31
37
|
const result = Object.assign(Object.assign({}, adjustedAssets), { __metadata__: {
|
|
32
38
|
version: app_configs_1.configs.version,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Compiler, type RspackPluginInstance } from '@rspack/core';
|
|
2
|
+
/** Формат значения одного экспоуза в конфиге module federation. */
|
|
3
|
+
type ExposeConfigValue = string | string[] | {
|
|
4
|
+
import: string | string[];
|
|
5
|
+
name?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Плагин добавляет css module-federation модулей в манифест сборки.
|
|
9
|
+
*
|
|
10
|
+
* По умолчанию для default-модулей `getModuleResources` отдаёт только `remoteEntry.js`,
|
|
11
|
+
* а css-чанки экспоузов остаются в безымянной ("") записи манифеста и не привязаны к
|
|
12
|
+
* конкретному модулю. Из-за этого хост-сервер при SSR не знает, какие стили встроить,
|
|
13
|
+
* и серверная разметка модуля отрисовывается без стилей до загрузки модуля на клиенте.
|
|
14
|
+
*
|
|
15
|
+
* Плагин обходит граф чанков от каждого экспоуза (async-блоки контейнерного энтрипоинта
|
|
16
|
+
* module federation), собирает css чанк-группы экспоуза (включая её синхронные части —
|
|
17
|
+
* splitChunks добавляет их в ту же чанк-группу) и записывает их в разделяемый map,
|
|
18
|
+
* откуда `processAssetsPluginOutput` дописывает css в запись модуля манифеста.
|
|
19
|
+
* Обход именно по графу чанков (а не по именам чанков) — имена вида
|
|
20
|
+
* `wmf-…_index_tsx.css` это деталь реализации. Css асинхронных чанков внутри экспоуза
|
|
21
|
+
* исключается: лениво подгружаемые компоненты не входят в SSR-html и грузят свой css
|
|
22
|
+
* на клиенте как и раньше (их чанк-группы — дочерние, мы в них не спускаемся).
|
|
23
|
+
*/
|
|
24
|
+
export declare class AttributeModuleCssPlugin implements RspackPluginInstance {
|
|
25
|
+
name: string;
|
|
26
|
+
/** map: import-путь экспоуза -> имя экспоуза (moduleId в манифесте) */
|
|
27
|
+
private importToExposeName;
|
|
28
|
+
/** разделяемый map: имя экспоуза -> css-файлы (сырые имена, без publicPath) */
|
|
29
|
+
private target;
|
|
30
|
+
constructor(exposes: Record<string, ExposeConfigValue>, target: Map<string, string[]>);
|
|
31
|
+
apply(compiler: Compiler): void;
|
|
32
|
+
private collect;
|
|
33
|
+
private getBlockExposeName;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AttributeModuleCssPlugin = void 0;
|
|
4
|
+
const core_1 = require("@rspack/core");
|
|
5
|
+
const CSS_FILE_REGEXP = /\.css(\?.*)?$/i;
|
|
6
|
+
/**
|
|
7
|
+
* Плагин добавляет css module-federation модулей в манифест сборки.
|
|
8
|
+
*
|
|
9
|
+
* По умолчанию для default-модулей `getModuleResources` отдаёт только `remoteEntry.js`,
|
|
10
|
+
* а css-чанки экспоузов остаются в безымянной ("") записи манифеста и не привязаны к
|
|
11
|
+
* конкретному модулю. Из-за этого хост-сервер при SSR не знает, какие стили встроить,
|
|
12
|
+
* и серверная разметка модуля отрисовывается без стилей до загрузки модуля на клиенте.
|
|
13
|
+
*
|
|
14
|
+
* Плагин обходит граф чанков от каждого экспоуза (async-блоки контейнерного энтрипоинта
|
|
15
|
+
* module federation), собирает css чанк-группы экспоуза (включая её синхронные части —
|
|
16
|
+
* splitChunks добавляет их в ту же чанк-группу) и записывает их в разделяемый map,
|
|
17
|
+
* откуда `processAssetsPluginOutput` дописывает css в запись модуля манифеста.
|
|
18
|
+
* Обход именно по графу чанков (а не по именам чанков) — имена вида
|
|
19
|
+
* `wmf-…_index_tsx.css` это деталь реализации. Css асинхронных чанков внутри экспоуза
|
|
20
|
+
* исключается: лениво подгружаемые компоненты не входят в SSR-html и грузят свой css
|
|
21
|
+
* на клиенте как и раньше (их чанк-группы — дочерние, мы в них не спускаемся).
|
|
22
|
+
*/
|
|
23
|
+
class AttributeModuleCssPlugin {
|
|
24
|
+
constructor(exposes, target) {
|
|
25
|
+
this.name = 'AttributeModuleCssPlugin';
|
|
26
|
+
this.target = target;
|
|
27
|
+
this.importToExposeName = new Map();
|
|
28
|
+
Object.entries(exposes).forEach(([exposeName, value]) => {
|
|
29
|
+
getExposeImports(value).forEach((importPath) => {
|
|
30
|
+
this.importToExposeName.set(importPath, exposeName);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
apply(compiler) {
|
|
35
|
+
compiler.hooks.thisCompilation.tap(this.name, (compilation) => {
|
|
36
|
+
compilation.hooks.processAssets.tap({
|
|
37
|
+
name: this.name,
|
|
38
|
+
// на этой стадии граф чанков и имена файлов уже финальные
|
|
39
|
+
stage: core_1.Compilation.PROCESS_ASSETS_STAGE_REPORT,
|
|
40
|
+
}, () => this.collect(compilation));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
collect(compilation) {
|
|
44
|
+
// watch-пересборки переиспользуют инстанс — очищаем предыдущий результат
|
|
45
|
+
this.target.clear();
|
|
46
|
+
const containerModule = findContainerEntryModule(compilation);
|
|
47
|
+
if (!containerModule) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
(containerModule.blocks || []).forEach((block) => {
|
|
51
|
+
var _a;
|
|
52
|
+
const exposeName = this.getBlockExposeName(block);
|
|
53
|
+
if (!exposeName) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const group = compilation.chunkGraph.getBlockChunkGroup(block);
|
|
57
|
+
if (!group) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const css = collectGroupCss(group);
|
|
61
|
+
if (css.length > 0) {
|
|
62
|
+
const existing = (_a = this.target.get(exposeName)) !== null && _a !== void 0 ? _a : [];
|
|
63
|
+
this.target.set(exposeName, unique([...existing, ...css]));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
getBlockExposeName(block) {
|
|
68
|
+
for (const dependency of block.dependencies) {
|
|
69
|
+
const { request } = dependency;
|
|
70
|
+
if (request && this.importToExposeName.has(request)) {
|
|
71
|
+
return this.importToExposeName.get(request);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.AttributeModuleCssPlugin = AttributeModuleCssPlugin;
|
|
78
|
+
function findContainerEntryModule(compilation) {
|
|
79
|
+
for (const module of compilation.modules) {
|
|
80
|
+
// идентификатор контейнерного энтрипоинта MF начинается с "container entry"
|
|
81
|
+
if (module.identifier().startsWith('container entry')) {
|
|
82
|
+
return module;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
function getExposeImports(value) {
|
|
88
|
+
if (typeof value === 'string') {
|
|
89
|
+
return [value];
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
return Array.isArray(value.import) ? value.import : [value.import];
|
|
95
|
+
}
|
|
96
|
+
function collectGroupCss(group) {
|
|
97
|
+
const files = new Set();
|
|
98
|
+
group.chunks.forEach((chunk) => {
|
|
99
|
+
chunk.files.forEach((file) => {
|
|
100
|
+
if (CSS_FILE_REGEXP.test(file)) {
|
|
101
|
+
files.add(file);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
return Array.from(files);
|
|
106
|
+
}
|
|
107
|
+
function unique(values) {
|
|
108
|
+
return Array.from(new Set(values));
|
|
109
|
+
}
|
package/docs/modules.md
CHANGED
|
@@ -332,6 +332,228 @@ export const MyAwesomeComponent = () => {
|
|
|
332
332
|
}
|
|
333
333
|
```
|
|
334
334
|
|
|
335
|
+
# Подключение модулей с использованием react.lazy
|
|
336
|
+
Для того чтобы подключить модуль используя механизм [suspense](https://react.dev/reference/react/Suspense), библиотека предоставляет функцию-хелпер `createLazyMounter`:
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
import React from 'react';
|
|
340
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
341
|
+
|
|
342
|
+
import {
|
|
343
|
+
createLazyMounter,
|
|
344
|
+
createModuleLoader,
|
|
345
|
+
MountableModule,
|
|
346
|
+
} from '@alfalab/scripts-modules';
|
|
347
|
+
|
|
348
|
+
type ModuleRunParams = {
|
|
349
|
+
username: string;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const loader = createModuleLoader<MountableModule<ModuleRunParams>>({
|
|
353
|
+
// ...
|
|
354
|
+
});
|
|
355
|
+
const LazyModule = React.lazy(createLazyMounter({
|
|
356
|
+
loader,
|
|
357
|
+
loaderParams: {}, // опционально
|
|
358
|
+
}));
|
|
359
|
+
|
|
360
|
+
const MyApp = () => (
|
|
361
|
+
<ErrorBoundary fallback={ <div>Ошибка!</div> }>
|
|
362
|
+
<React.Suspense fallback={ <div>Загрузка...</div> }>
|
|
363
|
+
<LazyModule
|
|
364
|
+
username="Unknown" // props определяется по ModuleRunParams
|
|
365
|
+
/>
|
|
366
|
+
</React.Suspense>
|
|
367
|
+
</ErrorBoundary>
|
|
368
|
+
);
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
У такого способа подключения есть ряд ограничений:
|
|
372
|
+
1. Нет поддержки shadow DOM
|
|
373
|
+
2. `loaderParams` будет передаваться в модуль только один раз, их изменения не будут приводить к перемонтированию модуля
|
|
374
|
+
3. Модуль никогда не будет удаляться из DOM.
|
|
375
|
+
4. Менее гибкая обработка ошибок - при подключении через `useModuleMounter` вы легко можете добавить механизм ретраев, с `lazy` - только показать сообщение об ошибке
|
|
376
|
+
|
|
377
|
+
Использовать такой способ стоит только для "статичных" модулей, которые всегда остаются на странице и при этом не зависят от loaderParams.
|
|
378
|
+
|
|
379
|
+
# Серверный рендеринг модулей
|
|
380
|
+
|
|
381
|
+
Если приложение-хост само рендерится на сервере (например через React 18 streaming), оно может
|
|
382
|
+
включить HTML модуля в первый ответ сервера. Для этого используется отдельный API
|
|
383
|
+
`createSsrMounter` из `@alfalab/scripts-modules/ssr`.
|
|
384
|
+
|
|
385
|
+
SSR модулей работает по схеме HTML-over-the-wire:
|
|
386
|
+
|
|
387
|
+
1. сервер хоста вызывает серверный эндпоинт модуля с флагом `ssr`;
|
|
388
|
+
2. сервер модуля возвращает обычные ресурсы, серверное состояние и готовый HTML модуля;
|
|
389
|
+
3. хост вставляет HTML модуля, инлайнит стили и сериализует payload ресурсов в страницу;
|
|
390
|
+
4. клиент читает встроенный payload, не делает повторный запрос за ресурсами и вызывает
|
|
391
|
+
`hydrate` модуля, если он экспортирован.
|
|
392
|
+
|
|
393
|
+
SSR поддерживается только для монтируемых модулей (`MountableModule`). Абстрактные модули,
|
|
394
|
+
модули-фабрики и `useShadowDom: true` остаются client-side сценариями.
|
|
395
|
+
|
|
396
|
+
## Серверный провайдер
|
|
397
|
+
|
|
398
|
+
SSR-модули используют кэш ресурсов в рамках одного HTTP-запроса. Поэтому в серверной точке
|
|
399
|
+
входа хоста нужно обернуть дерево в `ModuleSsrRequestProvider` из
|
|
400
|
+
`@alfalab/scripts-modules/ssr` и передать уникальный `requestId` для каждого запроса:
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
import { ModuleSsrRequestProvider } from '@alfalab/scripts-modules/ssr';
|
|
404
|
+
|
|
405
|
+
// Внутри обработчика HTTP-запроса хоста:
|
|
406
|
+
const requestId = crypto.randomUUID();
|
|
407
|
+
|
|
408
|
+
renderToPipeableStream(
|
|
409
|
+
<AppHtml>
|
|
410
|
+
<ModuleSsrRequestProvider requestId={requestId}>
|
|
411
|
+
<App />
|
|
412
|
+
</ModuleSsrRequestProvider>
|
|
413
|
+
</AppHtml>,
|
|
414
|
+
// ...
|
|
415
|
+
);
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Генерируйте `requestId` один раз на запрос, а не во время рендера: он должен оставаться
|
|
419
|
+
стабильным при повторных попытках рендера Suspense. Идентификаторы разных запросов должны
|
|
420
|
+
отличаться, иначе `moduleState` и ресурсы одного пользователя могут попасть в другой запрос.
|
|
421
|
+
На клиенте провайдер не нужен: он не создаёт DOM-узлов и не влияет на гидрацию. Если SSR-модуль
|
|
422
|
+
отрендерится на сервере без провайдера, `createSsrMounter` выбросит ошибку.
|
|
423
|
+
|
|
424
|
+
## Подключение SSR-модуля в приложении-потребителе
|
|
425
|
+
|
|
426
|
+
```tsx
|
|
427
|
+
import React from 'react';
|
|
428
|
+
|
|
429
|
+
import { createServerStateModuleFetcher } from '@alfalab/scripts-modules';
|
|
430
|
+
import { createSsrMounter } from '@alfalab/scripts-modules/ssr';
|
|
431
|
+
|
|
432
|
+
type ModuleRunParams = {
|
|
433
|
+
name: string;
|
|
434
|
+
counter: number;
|
|
435
|
+
onClick?: () => void;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
type ModuleSsrRunParams = {
|
|
439
|
+
name: string;
|
|
440
|
+
counter: number;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const { ModuleComponent } = createSsrMounter<ModuleRunParams, ModuleSsrRunParams>({
|
|
444
|
+
hostAppId: 'bar-app',
|
|
445
|
+
moduleId: 'ServerStateModule',
|
|
446
|
+
getModuleResources: createServerStateModuleFetcher({
|
|
447
|
+
baseUrl: 'https://example.com/foo-app',
|
|
448
|
+
}),
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
export const MyAwesomeComponent = () => {
|
|
452
|
+
const ssrRunParams = { name: 'Ivan', counter: 1 };
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<React.Suspense fallback={<div>pending...</div>}>
|
|
456
|
+
<ModuleComponent
|
|
457
|
+
instanceId='server-state-main'
|
|
458
|
+
ssrRunParams={ssrRunParams}
|
|
459
|
+
runParams={{
|
|
460
|
+
...ssrRunParams,
|
|
461
|
+
onClick: () => console.log('client-only callback'),
|
|
462
|
+
}}
|
|
463
|
+
/>
|
|
464
|
+
</React.Suspense>
|
|
465
|
+
);
|
|
466
|
+
};
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
`ssrRunParams` - это сериализуемое подмножество `runParams`, с которым модуль рендерится на
|
|
470
|
+
сервере. В него нельзя класть callback, ref, DOM-ноды и другие клиентские значения. Такие
|
|
471
|
+
значения передаются только в `runParams` и становятся доступны при `hydrate`, `mount` или
|
|
472
|
+
`update`.
|
|
473
|
+
|
|
474
|
+
Если один и тот же модуль рендерится на странице несколько раз, задавайте стабильный
|
|
475
|
+
`instanceId`, чтобы клиент нашел правильный встроенный payload для каждого инстанса. Он также
|
|
476
|
+
должен быть уникальным среди одновременно отрендеренных экземпляров одного модуля. Если не
|
|
477
|
+
передать `instanceId`, он вычисляется из `ssrRunParams`; поэтому у нескольких экземпляров с
|
|
478
|
+
одинаковыми параметрами обязательно задавайте разные явные идентификаторы.
|
|
479
|
+
|
|
480
|
+
## Что должен сделать автор SSR-модуля
|
|
481
|
+
|
|
482
|
+
На сервере модуля нужно добавить `renderToHtml` в описание модуля:
|
|
483
|
+
|
|
484
|
+
```tsx
|
|
485
|
+
import React from 'react';
|
|
486
|
+
import { renderToString } from 'react-dom/server';
|
|
487
|
+
|
|
488
|
+
import { createGetModulesExpress } from '@alfalab/scripts-server/build/express';
|
|
489
|
+
|
|
490
|
+
import { ServerStateModule } from './modules/server-state-module';
|
|
491
|
+
|
|
492
|
+
const modulesRouter = createGetModulesExpress({
|
|
493
|
+
ServerStateModule: {
|
|
494
|
+
mountMode: 'default',
|
|
495
|
+
version: '1.0.0',
|
|
496
|
+
getModuleState: async () => ({
|
|
497
|
+
baseUrl: 'https://example.com/foo-app',
|
|
498
|
+
someData: 'server data',
|
|
499
|
+
}),
|
|
500
|
+
renderToHtml: async ({ moduleState, ssrRunParams }) =>
|
|
501
|
+
renderToString(
|
|
502
|
+
<ServerStateModule
|
|
503
|
+
serverState={moduleState}
|
|
504
|
+
runParams={(ssrRunParams ?? {}) as Record<string, unknown>}
|
|
505
|
+
/>,
|
|
506
|
+
),
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
В клиентском экспорте модуля нужно добавить `hydrate`, а если модуль должен обновляться при
|
|
512
|
+
изменении `runParams` без полного перемонтирования - еще и `update`:
|
|
513
|
+
|
|
514
|
+
```tsx
|
|
515
|
+
import React from 'react';
|
|
516
|
+
import { createRoot, hydrateRoot } from 'react-dom/client';
|
|
517
|
+
|
|
518
|
+
import type { ModuleMountFunction, ModuleUnmountFunction } from '@alfalab/scripts-modules';
|
|
519
|
+
|
|
520
|
+
let root: ReturnType<typeof createRoot>;
|
|
521
|
+
|
|
522
|
+
export const mount: ModuleMountFunction = (targetNode, runParams, serverState) => {
|
|
523
|
+
root = createRoot(targetNode);
|
|
524
|
+
root.render(<App runParams={runParams} serverState={serverState} />);
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
export const hydrate: ModuleMountFunction = (targetNode, runParams, serverState) => {
|
|
528
|
+
root = hydrateRoot(targetNode, <App runParams={runParams} serverState={serverState} />);
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
export const update: ModuleMountFunction = (targetNode, runParams, serverState) => {
|
|
532
|
+
root.render(<App runParams={runParams} serverState={serverState} />);
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
export const unmount: ModuleUnmountFunction = () => {
|
|
536
|
+
root?.unmount();
|
|
537
|
+
};
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Если модуль вернул HTML, но не экспортирует `hydrate`, клиент очистит серверную разметку и
|
|
541
|
+
вызовет обычный `mount`. Это корректный fallback, но пользователь может увидеть мигание.
|
|
542
|
+
|
|
543
|
+
При клиентской навигации серверной разметки и встроенного payload может не быть. В этом случае
|
|
544
|
+
`createSsrMounter` создаст пустой outlet, получит ресурсы обычным `getModuleResources`-запросом
|
|
545
|
+
и вызовет `mount`. Таким образом, SSR-модуль остаётся совместимым с client-side переходами.
|
|
546
|
+
|
|
547
|
+
По умолчанию стили SSR-модуля инлайнятся в HTML хоста как `<style>` рядом с разметкой
|
|
548
|
+
модуля, чтобы Suspense-boundary раскрывался сразу с готовыми стилями. Если хосту важнее
|
|
549
|
+
меньший HTML и browser cache для CSS, можно передать `stylesMode: 'link'` в
|
|
550
|
+
`createSsrMounter`: сервер отдаст `<link rel='stylesheet' type='text/css'>` с SSR-атрибутами,
|
|
551
|
+
а клиент переиспользует эти теги и не добавит дубликаты. На React 18 Suspense-boundary может
|
|
552
|
+
раскрыться до окончания загрузки CSS, поэтому `inline` остается значением по умолчанию.
|
|
553
|
+
|
|
554
|
+
Подробное описание API, матрица поведения гидрации и дополнительные ограничения описаны в
|
|
555
|
+
README пакета `@alfalab/scripts-modules`.
|
|
556
|
+
|
|
335
557
|
# Изоляция стилей
|
|
336
558
|
Если ваши приложения активно используют глобальные стили (то есть не с css-modules или css-in-js), вы вполне
|
|
337
559
|
можете столкнуться с проблемой конфликтов стилей между модулями и приложением-потребителем.
|
|
@@ -585,6 +807,10 @@ const loader = createModuleLoader({
|
|
|
585
807
|
|
|
586
808
|
**Внимание!** Использование `resourcesCache: 'single-item'` не будет работать вместе с `useShadowDom` из-за особенностей работы со стилями.
|
|
587
809
|
|
|
810
|
+
## Кеширование при использовании `createLazyMounter`
|
|
811
|
+
|
|
812
|
+
Если вы используете `createLazyMounter` - модуль будет загружаться лишь один раз, не зависимо от используемых `loaderParams`.
|
|
813
|
+
|
|
588
814
|
# Другие типы модулей
|
|
589
815
|
|
|
590
816
|
Помимо создания монтируемых модулей, есть возможность создавать и другие типы модулей, более подходящие для некоторых вариантов использования.
|