@foxford/den 2.1.0 → 3.0.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.
@@ -6,7 +6,7 @@ function isViewAdapter(value) {
6
6
  return false;
7
7
  }
8
8
  const candidate = value;
9
- return typeof candidate.type === "string" && typeof candidate.createIsland === "function" && typeof candidate.installResolver === "function";
9
+ return typeof candidate.type === "string" && typeof candidate.createIsland === "function" && typeof candidate.renderIsland === "function" && typeof candidate.installResolver === "function";
10
10
  }
11
11
  function viewAdapterOf(layer) {
12
12
  return adapterOfConfig(layer.config);
@@ -14,7 +14,7 @@ function viewAdapterOf(layer) {
14
14
  function adapterOfConfig(config) {
15
15
  if (!isViewAdapter(config.adapter)) {
16
16
  throw new Error(
17
- `den/island: view-\u0441\u043B\u043E\u0439 \xAB${config.name}\xBB \u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D \u0431\u0435\u0437 \u0430\u0434\u0430\u043F\u0442\u0435\u0440\u0430 \u2014 \u043E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F ViewAdapter (type + createIsland + installResolver), \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 reactAdapter()`
17
+ `den/island: view-\u0441\u043B\u043E\u0439 \xAB${config.name}\xBB \u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D \u0431\u0435\u0437 \u0430\u0434\u0430\u043F\u0442\u0435\u0440\u0430 \u2014 \u043E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F ViewAdapter (type + createIsland + renderIsland + installResolver), \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 reactAdapter()`
18
18
  );
19
19
  }
20
20
  return config.adapter;
@@ -6,7 +6,7 @@ function isViewAdapter(value) {
6
6
  return false;
7
7
  }
8
8
  const candidate = value;
9
- return typeof candidate.type === "string" && typeof candidate.createIsland === "function" && typeof candidate.installResolver === "function";
9
+ return typeof candidate.type === "string" && typeof candidate.createIsland === "function" && typeof candidate.renderIsland === "function" && typeof candidate.installResolver === "function";
10
10
  }
11
11
  function viewAdapterOf(layer) {
12
12
  return adapterOfConfig(layer.config);
@@ -14,7 +14,7 @@ function viewAdapterOf(layer) {
14
14
  function adapterOfConfig(config) {
15
15
  if (!isViewAdapter(config.adapter)) {
16
16
  throw new Error(
17
- `den/island: view-\u0441\u043B\u043E\u0439 \xAB${config.name}\xBB \u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D \u0431\u0435\u0437 \u0430\u0434\u0430\u043F\u0442\u0435\u0440\u0430 \u2014 \u043E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F ViewAdapter (type + createIsland + installResolver), \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 reactAdapter()`
17
+ `den/island: view-\u0441\u043B\u043E\u0439 \xAB${config.name}\xBB \u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D \u0431\u0435\u0437 \u0430\u0434\u0430\u043F\u0442\u0435\u0440\u0430 \u2014 \u043E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F ViewAdapter (type + createIsland + renderIsland + installResolver), \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 reactAdapter()`
18
18
  );
19
19
  }
20
20
  return config.adapter;
package/index.cjs CHANGED
@@ -16,7 +16,7 @@ var _chunk5DJZZML5cjs = require('./chunk-5DJZZML5.cjs');
16
16
  require('./chunk-C7BM4DGX.cjs');
17
17
 
18
18
 
19
- var _chunkLJE7K7VDcjs = require('./chunk-LJE7K7VD.cjs');
19
+ var _chunkXOJ2VWX4cjs = require('./chunk-XOJ2VWX4.cjs');
20
20
  require('./chunk-A6ZPAM6Z.cjs');
21
21
 
22
22
 
@@ -717,4 +717,4 @@ function defineLayer(token, options) {
717
717
 
718
718
 
719
719
 
720
- exports.ActivatableToken = _chunk5DJZZML5cjs.ActivatableToken; exports.ContainerManagerImpl = ContainerManagerImpl; exports.ContainerManagerToken = _chunk5DJZZML5cjs.ContainerManagerToken; exports.GuardRunnerImpl = GuardRunnerImpl; exports.GuardRunnerToken = _chunk5DJZZML5cjs.GuardRunnerToken; exports.NavigationManagerImpl = NavigationManagerImpl; exports.NavigationManagerToken = _chunk5DJZZML5cjs.NavigationManagerToken; exports.RequestContextToken = _chunk5DJZZML5cjs.RequestContextToken; exports.SlotRegistry = _chunk5DJZZML5cjs.SlotRegistry; exports.SlotRegistryToken = _chunk5DJZZML5cjs.SlotRegistryToken; exports.StateCache = StateCache; exports.StateRegistry = StateRegistry; exports.StateRegistryToken = _chunk5DJZZML5cjs.StateRegistryToken; exports.StateSerializerImpl = StateSerializerImpl; exports.StateSerializerToken = _chunk5DJZZML5cjs.StateSerializerToken; exports.ViewLayerToken = _chunkLJE7K7VDcjs.ViewLayerToken; exports.createCore = createCore; exports.defaultGlobalResolver = defaultGlobalResolver; exports.defineActionGuard = defineActionGuard; exports.defineExtension = defineExtension; exports.defineGuard = defineGuard; exports.defineLayer = defineLayer; exports.defineLayout = defineLayout; exports.defineRepository = _chunk5DJZZML5cjs.defineRepository; exports.defineService = _chunk5DJZZML5cjs.defineService; exports.defineSlot = _chunk5DJZZML5cjs.defineSlot; exports.defineViewModel = _chunk5DJZZML5cjs.defineViewModel;
720
+ exports.ActivatableToken = _chunk5DJZZML5cjs.ActivatableToken; exports.ContainerManagerImpl = ContainerManagerImpl; exports.ContainerManagerToken = _chunk5DJZZML5cjs.ContainerManagerToken; exports.GuardRunnerImpl = GuardRunnerImpl; exports.GuardRunnerToken = _chunk5DJZZML5cjs.GuardRunnerToken; exports.NavigationManagerImpl = NavigationManagerImpl; exports.NavigationManagerToken = _chunk5DJZZML5cjs.NavigationManagerToken; exports.RequestContextToken = _chunk5DJZZML5cjs.RequestContextToken; exports.SlotRegistry = _chunk5DJZZML5cjs.SlotRegistry; exports.SlotRegistryToken = _chunk5DJZZML5cjs.SlotRegistryToken; exports.StateCache = StateCache; exports.StateRegistry = StateRegistry; exports.StateRegistryToken = _chunk5DJZZML5cjs.StateRegistryToken; exports.StateSerializerImpl = StateSerializerImpl; exports.StateSerializerToken = _chunk5DJZZML5cjs.StateSerializerToken; exports.ViewLayerToken = _chunkXOJ2VWX4cjs.ViewLayerToken; exports.createCore = createCore; exports.defaultGlobalResolver = defaultGlobalResolver; exports.defineActionGuard = defineActionGuard; exports.defineExtension = defineExtension; exports.defineGuard = defineGuard; exports.defineLayer = defineLayer; exports.defineLayout = defineLayout; exports.defineRepository = _chunk5DJZZML5cjs.defineRepository; exports.defineService = _chunk5DJZZML5cjs.defineService; exports.defineSlot = _chunk5DJZZML5cjs.defineSlot; exports.defineViewModel = _chunk5DJZZML5cjs.defineViewModel;
package/index.d.cts CHANGED
@@ -4,7 +4,7 @@ import { Container, Token } from '@foxford/ioc';
4
4
  import { DefinitionResolver } from './adapter.cjs';
5
5
  export { d as defineRepository, a as defineService, b as defineViewModel } from './define-repository-MDEWHqM7.cjs';
6
6
  export { D as DefineSlotOptions, S as SlotDefinition, d as defineSlot } from './define-slot-BKvArA02.cjs';
7
- export { V as ViewAdapter, a as ViewLayerDefinition, b as ViewLayerToken } from './view-adapter-zbHBVVqR.cjs';
7
+ export { V as ViewAdapter, a as ViewLayerDefinition, b as ViewLayerToken } from './view-adapter-BvDC5O4y.cjs';
8
8
 
9
9
  /**
10
10
  * Опции для создания Core Runtime.
package/index.d.ts CHANGED
@@ -4,7 +4,7 @@ import { Container, Token } from '@foxford/ioc';
4
4
  import { DefinitionResolver } from './adapter.js';
5
5
  export { d as defineRepository, a as defineService, b as defineViewModel } from './define-repository-Ca62_YCy.js';
6
6
  export { D as DefineSlotOptions, S as SlotDefinition, d as defineSlot } from './define-slot-Dil8Kr1A.js';
7
- export { V as ViewAdapter, a as ViewLayerDefinition, b as ViewLayerToken } from './view-adapter-CSPKr2xy.js';
7
+ export { V as ViewAdapter, a as ViewLayerDefinition, b as ViewLayerToken } from './view-adapter-DtNgyjIf.js';
8
8
 
9
9
  /**
10
10
  * Опции для создания Core Runtime.
package/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  import "./chunk-WBHHHICS.js";
17
17
  import {
18
18
  ViewLayerToken
19
- } from "./chunk-TCYGDT2A.js";
19
+ } from "./chunk-E23E2VUC.js";
20
20
  import "./chunk-TPBI6TOU.js";
21
21
  import {
22
22
  __async
package/island/index.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkLJE7K7VDcjs = require('../chunk-LJE7K7VD.cjs');
5
+ var _chunkXOJ2VWX4cjs = require('../chunk-XOJ2VWX4.cjs');
6
6
 
7
7
 
8
8
 
@@ -10,8 +10,14 @@ var _chunkLJE7K7VDcjs = require('../chunk-LJE7K7VD.cjs');
10
10
  var _chunkA6ZPAM6Zcjs = require('../chunk-A6ZPAM6Z.cjs');
11
11
 
12
12
 
13
+
13
14
  var _chunkXHYR3SGGcjs = require('../chunk-XHYR3SGG.cjs');
14
15
 
16
+ // src/island/document.ts
17
+ function isDocumentSource(value) {
18
+ return typeof value === "object" && value !== null && typeof value.describeDocument === "function";
19
+ }
20
+
15
21
  // src/island/lifecycle.ts
16
22
  var _logger = require('@foxford/logger');
17
23
 
@@ -28,6 +34,8 @@ function getAppContainer() {
28
34
  }
29
35
 
30
36
  // src/island/lifecycle.ts
37
+ var EMPTY_ROUTE = { params: {}, pathname: "", search: "" };
38
+ var REDIRECT_FOUND = 302;
31
39
  function createIslandContainer(descriptor, options = {}) {
32
40
  var _a, _b, _c, _d, _e;
33
41
  const { parent = getAppContainer() } = options;
@@ -68,13 +76,49 @@ function hydrateIslandState(container, descriptor, denState) {
68
76
  }
69
77
  }
70
78
  }
71
- function activateIslandViewModels(container, descriptor) {
79
+ function activateIslandViewModels(_0, _1) {
80
+ return _chunkXHYR3SGGcjs.__async.call(void 0, this, arguments, function* (container, descriptor, route = EMPTY_ROUTE) {
81
+ var _a;
82
+ yield Promise.all(
83
+ ((_a = descriptor.viewModels) != null ? _a : []).map((vm) => _chunkXHYR3SGGcjs.__async.call(void 0, null, null, function* () {
84
+ var _a2;
85
+ const instance = container.resolve(vm.token);
86
+ _chunkA6ZPAM6Zcjs.unitLogger.call(void 0, container, vm.token.description).debug(`\u0430\u043A\u0442\u0438\u0432\u0430\u0446\u0438\u044F VM (${route.pathname || "\u2014"})`);
87
+ yield (_a2 = instance.activate) == null ? void 0 : _a2.call(instance, route);
88
+ }))
89
+ );
90
+ });
91
+ }
92
+ function collectIslandState(container, descriptor) {
93
+ var _a, _b;
94
+ const state = {};
95
+ for (const vm of (_a = descriptor.viewModels) != null ? _a : []) {
96
+ const instance = container.resolve(vm.token);
97
+ if (typeof ((_b = instance.state) == null ? void 0 : _b.get) === "function") {
98
+ state[vm.token.description] = instance.state.get();
99
+ }
100
+ }
101
+ return state;
102
+ }
103
+ function collectDocumentContribution(container, descriptor) {
72
104
  var _a, _b;
105
+ const contribution = {};
73
106
  for (const vm of (_a = descriptor.viewModels) != null ? _a : []) {
74
107
  const instance = container.resolve(vm.token);
75
- _chunkA6ZPAM6Zcjs.unitLogger.call(void 0, container, vm.token.description).debug("\u0430\u043A\u0442\u0438\u0432\u0430\u0446\u0438\u044F VM (\u0431\u0435\u0437 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u043E\u0433\u043E denState)");
76
- void ((_b = instance.activate) == null ? void 0 : _b.call(instance));
108
+ if (!isDocumentSource(instance)) {
109
+ continue;
110
+ }
111
+ const part = instance.describeDocument();
112
+ if (part.head) {
113
+ contribution.head = _chunkXHYR3SGGcjs.__spreadValues.call(void 0, _chunkXHYR3SGGcjs.__spreadValues.call(void 0, {}, contribution.head), part.head);
114
+ }
115
+ const declared = part.status !== void 0 || part.redirect !== void 0;
116
+ if (declared && contribution.status === void 0) {
117
+ contribution.status = (_b = part.status) != null ? _b : REDIRECT_FOUND;
118
+ contribution.redirect = part.redirect;
119
+ }
77
120
  }
121
+ return contribution;
78
122
  }
79
123
  function resolveEagerUnits(container, descriptor) {
80
124
  var _a;
@@ -89,6 +133,38 @@ function resolveEagerUnits(container, descriptor) {
89
133
  }
90
134
  }
91
135
 
136
+ // src/island/render.ts
137
+ var STATUS_OK = 200;
138
+ function renderApp(descriptor, request) {
139
+ return _chunkXHYR3SGGcjs.__async.call(void 0, this, null, function* () {
140
+ var _a, _b;
141
+ const route = {
142
+ params: (_a = request.params) != null ? _a : {},
143
+ pathname: request.pathname,
144
+ search: (_b = request.search) != null ? _b : ""
145
+ };
146
+ const container = createIslandContainer(descriptor, { parent: request.parent });
147
+ if (container === null) {
148
+ throw new Error(`den/island: \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \xAB${descriptor.name}\xBB \u043D\u0435 \u0441\u043E\u0431\u0440\u0430\u043B\u043E\u0441\u044C \u2014 \u0440\u0435\u043D\u0434\u0435\u0440 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u0435\u043D`);
149
+ }
150
+ try {
151
+ yield activateIslandViewModels(container, descriptor, route);
152
+ const { head, redirect, status = STATUS_OK } = collectDocumentContribution(container, descriptor);
153
+ if (status !== STATUS_OK) {
154
+ return { head, html: "", redirect, state: {}, status };
155
+ }
156
+ const state = collectIslandState(container, descriptor);
157
+ const rendered = _chunkXOJ2VWX4cjs.resolveViewAdapter.call(void 0, container).renderIsland(descriptor, {
158
+ container,
159
+ slots: request.slots
160
+ });
161
+ return { head, headHtml: rendered.head, html: rendered.html, state, status };
162
+ } finally {
163
+ yield container.dispose();
164
+ }
165
+ });
166
+ }
167
+
92
168
  // src/island/slot-address.ts
93
169
  function slotAddress(app, slot) {
94
170
  return `${app}:${slot}`;
@@ -153,4 +229,8 @@ function normalizeRoutes(routes = []) {
153
229
 
154
230
 
155
231
 
156
- exports.ViewLayerToken = _chunkLJE7K7VDcjs.ViewLayerToken; exports.activateIslandViewModels = activateIslandViewModels; exports.createIslandContainer = createIslandContainer; exports.describeIsland = describeIsland; exports.getAppContainer = getAppContainer; exports.getIslandsSnapshot = getIslandsSnapshot; exports.hydrateIslandState = hydrateIslandState; exports.normalizeRoutes = normalizeRoutes; exports.registerIsland = registerIsland; exports.resolveEagerUnits = resolveEagerUnits; exports.resolveViewAdapter = _chunkLJE7K7VDcjs.resolveViewAdapter; exports.slotAddress = slotAddress; exports.subscribeIslands = subscribeIslands; exports.viewAdapterOf = _chunkLJE7K7VDcjs.viewAdapterOf;
232
+
233
+
234
+
235
+
236
+ exports.ViewLayerToken = _chunkXOJ2VWX4cjs.ViewLayerToken; exports.activateIslandViewModels = activateIslandViewModels; exports.collectDocumentContribution = collectDocumentContribution; exports.collectIslandState = collectIslandState; exports.createIslandContainer = createIslandContainer; exports.describeIsland = describeIsland; exports.getAppContainer = getAppContainer; exports.getIslandsSnapshot = getIslandsSnapshot; exports.hydrateIslandState = hydrateIslandState; exports.isDocumentSource = isDocumentSource; exports.normalizeRoutes = normalizeRoutes; exports.registerIsland = registerIsland; exports.renderApp = renderApp; exports.resolveEagerUnits = resolveEagerUnits; exports.resolveViewAdapter = _chunkXOJ2VWX4cjs.resolveViewAdapter; exports.slotAddress = slotAddress; exports.subscribeIslands = subscribeIslands; exports.viewAdapterOf = _chunkXOJ2VWX4cjs.viewAdapterOf;
@@ -1,9 +1,62 @@
1
- import { I as IslandDescriptor } from '../view-adapter-zbHBVVqR.cjs';
2
- export { V as ViewAdapter, a as ViewLayerDefinition, b as ViewLayerToken, r as resolveViewAdapter, v as viewAdapterOf } from '../view-adapter-zbHBVVqR.cjs';
1
+ import { I as IslandDescriptor } from '../view-adapter-BvDC5O4y.cjs';
2
+ export { R as RenderIslandOptions, c as RenderTree, S as ServerRender, d as ServerRenderResult, V as ViewAdapter, a as ViewLayerDefinition, b as ViewLayerToken, r as resolveViewAdapter, v as viewAdapterOf } from '../view-adapter-BvDC5O4y.cjs';
3
3
  import { Container } from '@foxford/ioc';
4
4
  import { g as GuardDefinition } from '../types-COwVwgzE.cjs';
5
5
  import '../define-slot-BKvArA02.cjs';
6
6
 
7
+ /** Вклад приложения в `<head>` документа. */
8
+ interface DocumentHead {
9
+ title?: string;
10
+ description?: string;
11
+ keywords?: string;
12
+ /** При отсутствии хост берёт `title`. */
13
+ ogTitle?: string;
14
+ /** При отсутствии хост берёт `description`. */
15
+ ogDescription?: string;
16
+ }
17
+ /** Что приложение сообщает хосту про страницу, которую оно нарисовало. */
18
+ interface DocumentContribution {
19
+ head?: DocumentHead;
20
+ /**
21
+ * Исход запроса статусом HTTP; не объявлен — `200`.
22
+ *
23
+ * Статусом, а не набором признаков вроде «страницы нет»: исход у запроса один, а признаки
24
+ * позволяют объявить сразу два взаимоисключающих. Тем же статусом исход едет по сети,
25
+ * поэтому у обоих транспортов дискриминатор общий.
26
+ *
27
+ * Нужен отдельно от исключения: `throw` означает, что приложение упало, и хост рисует
28
+ * плашку вместо него. Приложение, занявшее путь по префиксу (`/legal/*`), обязано уметь
29
+ * сказать `404` про несуществующий адрес внутри своего пространства.
30
+ */
31
+ status?: number;
32
+ /** Адрес для 3xx. Объявлен без статуса — исход `302`. */
33
+ redirect?: string;
34
+ }
35
+ /** VM, которой есть что сказать про документ. */
36
+ interface DocumentSource {
37
+ describeDocument(): DocumentContribution;
38
+ }
39
+ /**
40
+ * Реализует ли объект {@link DocumentSource}.
41
+ *
42
+ * @param value - Проверяемое значение
43
+ */
44
+ declare function isDocumentSource(value: unknown): value is DocumentSource;
45
+
46
+ /**
47
+ * Адрес, по которому рендерится остров.
48
+ *
49
+ * Приходит АРГУМЕНТОМ активации, а не через контейнер: контейнер живёт и на клиенте, где
50
+ * адреса в нём нет, и юнит, потребовавший его через `requires`, упал бы при резолве.
51
+ */
52
+ interface IslandRoute {
53
+ /** Путь без search, с ведущим слэшем: `/legal/general`. */
54
+ pathname: string;
55
+ /** Строка запроса без `?`; пустая, если её нет. */
56
+ search: string;
57
+ /** Параметры, выделенные маршрутом хоста. */
58
+ params: Record<string, string>;
59
+ }
7
60
  /** Опции сборки контейнера острова. */
8
61
  interface CreateIslandContainerOptions {
9
62
  /**
@@ -42,14 +95,36 @@ declare function createIslandContainer(descriptor: IslandDescriptor, options?: C
42
95
  */
43
96
  declare function hydrateIslandState(container: Container, descriptor: IslandDescriptor, denState: Record<string, unknown>): void;
44
97
  /**
45
- * Активирует VM острова самостоятельно когда серверного denState нет (ремоунт острова
46
- * без данных, чисто клиентский переход). С denState активация не нужна: состояние уже
47
- * приехало готовым.
98
+ * Активирует VM острова: каждая считает своё состояние по адресу.
99
+ *
100
+ * Параллельно, потому что VM одного острова друг о друге не знают; ждём все — состояние
101
+ * нужно целиком, а не по частям.
102
+ *
103
+ * @param container - Контейнер острова
104
+ * @param descriptor - Дескриптор острова
105
+ * @param route - Адрес, по которому рендерится остров
106
+ */
107
+ declare function activateIslandViewModels(container: Container, descriptor: IslandDescriptor, route?: IslandRoute): Promise<void>;
108
+ /**
109
+ * Снимает состояние VM острова — операция, обратная {@link hydrateIslandState}.
110
+ *
111
+ * @param container - Контейнер острова
112
+ * @param descriptor - Дескриптор острова
113
+ * @returns Состояние по ключам `token.description`
114
+ */
115
+ declare function collectIslandState(container: Container, descriptor: IslandDescriptor): Record<string, unknown>;
116
+ /**
117
+ * Собирает вклад острова в документ по всем его VM.
118
+ *
119
+ * Правила слияния два, и они следуют из смысла полей. `head` — набор полей, поэтому
120
+ * объявленное позже уточняет объявленное раньше. Исход (`status` вместе с `redirect`) —
121
+ * ОДНО решение, а не набор: побеждает первое объявленное, и берётся оно целиком, иначе
122
+ * статус одной VM склеился бы с адресом другой.
48
123
  *
49
124
  * @param container - Контейнер острова
50
125
  * @param descriptor - Дескриптор острова
51
126
  */
52
- declare function activateIslandViewModels(container: Container, descriptor: IslandDescriptor): void;
127
+ declare function collectDocumentContribution(container: Container, descriptor: IslandDescriptor): DocumentContribution;
53
128
  /**
54
129
  * Поднимает единицы, объявленные `eager`, — те, что обязаны жить с загрузки, а не с первого
55
130
  * резолва.
@@ -57,14 +132,59 @@ declare function activateIslandViewModels(container: Container, descriptor: Isla
57
132
  * Отдельным шагом, а не внутри сборки контейнера: контейнер строится и на сервере (в vike —
58
133
  * прямо в `renderToString`), а сервер живёт один рендер, и поднимать там WS-подписку или
59
134
  * прогрев кэша незачем — открытый сокет на каждый рендер это не «eager», это утечка. Зовёт
60
- * адаптер в точке монтирования, которой на сервере просто нет; серверный прогрев у приложения
61
- * называется `load`.
135
+ * адаптер в точке монтирования, которой на сервере просто нет; серверное состояние приложения
136
+ * считает активация VM.
62
137
  *
63
138
  * @param container - Контейнер острова
64
139
  * @param descriptor - Дескриптор острова
65
140
  */
66
141
  declare function resolveEagerUnits(container: Container, descriptor: IslandDescriptor): void;
67
142
 
143
+ /** Запрос на рендер приложения. */
144
+ interface RenderAppRequest {
145
+ /** Путь без search, с ведущим слэшем: `/legal/general`. */
146
+ pathname: string;
147
+ /** Строка запроса без `?`. */
148
+ search?: string;
149
+ /** Параметры, выделенные маршрутом хоста. */
150
+ params?: Record<string, string>;
151
+ /**
152
+ * Родитель контейнера приложения: через него приезжает всё, что настроил исполнитель, —
153
+ * логгер, конфиг HTTP, выданные возможности. По умолчанию — app-scope контейнер.
154
+ */
155
+ parent?: Container;
156
+ /** Внешние заполнители слотов; для ядра непрозрачны. */
157
+ slots?: Record<string, unknown>;
158
+ }
159
+ /** Ответ приложения: всё, что хосту нужно, чтобы собрать документ и ответить. */
160
+ interface RenderAppResult {
161
+ /**
162
+ * Исход запроса статусом HTTP. `200` — вот страница; всё остальное значит, что страницы
163
+ * по этому адресу нет, и тело такого ответа выбирает хост: страница ошибки у портала одна
164
+ * на все приложения.
165
+ */
166
+ status: number;
167
+ /** Разметка приложения; пустая при любом исходе, кроме `200`. */
168
+ html: string;
169
+ /** Состояние VM по ключам `token.description` — им гидрируется остров в браузере. */
170
+ state: Record<string, unknown>;
171
+ /** Вклад в `<head>` данными. */
172
+ head?: DocumentHead;
173
+ /** Вклад в `<head>` готовой разметкой — стили, собранные во время рендера. */
174
+ headHtml?: string;
175
+ /** Адрес для 3xx. */
176
+ redirect?: string;
177
+ }
178
+ /**
179
+ * Рисует приложение по адресу и отдаёт разметку вместе с состоянием.
180
+ *
181
+ * @param descriptor - Дескриптор приложения
182
+ * @param request - Адрес, родительский контейнер и заполнители слотов
183
+ * @returns Разметка, состояние и вклад в документ
184
+ * @throws Если контейнер приложения не собрался — исполнитель решает, чем это показать
185
+ */
186
+ declare function renderApp(descriptor: IslandDescriptor, request: RenderAppRequest): Promise<RenderAppResult>;
187
+
68
188
  /** Запись живого острова: имя + фактический состав его контейнера. */
69
189
  interface IslandRecord {
70
190
  /** Уникальный id инстанса острова (островов одного приложения может быть несколько). */
@@ -146,51 +266,4 @@ type AppRoute = string | RouteDeclaration;
146
266
  */
147
267
  declare function normalizeRoutes(routes?: ReadonlyArray<AppRoute>): RouteDeclaration[];
148
268
 
149
- /** Что приложение отдало серверным рендером. */
150
- interface ServerRenderResult {
151
- /** Разметка дерева — результат переданного рендера. */
152
- html: string;
153
- /**
154
- * Вклад в `<head>` ГОТОВОЙ разметкой: то, что рождается только во время рендера и
155
- * данными не описывается.
156
- *
157
- * Таковы стили CSS-in-JS: стороннее представление у них одно — тег со служебными
158
- * атрибутами, по которым клиентский рантайм узнаёт свои правила. Разобрав его на поля,
159
- * потеряешь регидратацию, и стили впрыснутся вторым экземпляром. Поэтому вклад в документ
160
- * ДАННЫМИ (`DocumentHead` у хостов) остаётся отдельным каналом, а это — разметка.
161
- *
162
- * Разметка приезжает в документ хоста ДОСЛОВНО, без экранирования — иначе служебные
163
- * атрибуты не пережили бы вставку. Отвечает за содержимое приложение: у сетевого
164
- * транспорта строка приходит из чужого процесса, и хост её не разбирает.
165
- */
166
- head?: string;
167
- }
168
- /** Рендер дерева в разметку. Его даёт тот, кто серверный рендер исполняет. */
169
- type RenderTree<Tree = unknown> = (tree: Tree) => string;
170
- /**
171
- * Серверный рендер приложения: приложению дают его дерево и рендер, оно возвращает
172
- * разметку и свой вклад в документ.
173
- *
174
- * Рендер приходит АРГУМЕНТОМ, потому что приложение им не владеет: страницу собирает тот,
175
- * кто держит документ, и приложений на ней может быть несколько. Отдав рендер аргументом,
176
- * приложение получает обычную функцию — состояние прохода живёт локальной переменной и
177
- * между запросами не утекает.
178
- *
179
- * Подпись одна на оба транспорта, и в этом смысл: у приложения в процессе хоста рендер
180
- * передаёт хост, у сетевого — его собственный сервер, а `html` и `head` едут ответом.
181
- * Перевод приложения между транспортами эту часть не задевает.
182
- *
183
- * `Tree` — дерево view-фреймворка, для ядра непрозрачное. View-адаптер сужает параметр
184
- * до своего типа (`ReactServerRender` в `@foxford/den-react`), и приложение пишет проход
185
- * в терминах СВОЕГО фреймворка.
186
- *
187
- * @example
188
- * serverRender: (tree, render) => {
189
- * const sheet = new ServerStyleSheet()
190
- *
191
- * return { head: sheet.getStyleTags(), html: render(sheet.collectStyles(tree)) }
192
- * }
193
- */
194
- type ServerRender<Tree = unknown> = (tree: Tree, render: RenderTree<Tree>) => ServerRenderResult;
195
-
196
- export { type AppRoute, type CreateIslandContainerOptions, IslandDescriptor, type IslandRecord, type RenderTree, type RouteDeclaration, type ServerRender, type ServerRenderResult, activateIslandViewModels, createIslandContainer, describeIsland, getAppContainer, getIslandsSnapshot, hydrateIslandState, normalizeRoutes, registerIsland, resolveEagerUnits, slotAddress, subscribeIslands };
269
+ export { type AppRoute, type CreateIslandContainerOptions, type DocumentContribution, type DocumentHead, type DocumentSource, IslandDescriptor, type IslandRecord, type IslandRoute, type RenderAppRequest, type RenderAppResult, type RouteDeclaration, activateIslandViewModels, collectDocumentContribution, collectIslandState, createIslandContainer, describeIsland, getAppContainer, getIslandsSnapshot, hydrateIslandState, isDocumentSource, normalizeRoutes, registerIsland, renderApp, resolveEagerUnits, slotAddress, subscribeIslands };
package/island/index.d.ts CHANGED
@@ -1,9 +1,62 @@
1
- import { I as IslandDescriptor } from '../view-adapter-CSPKr2xy.js';
2
- export { V as ViewAdapter, a as ViewLayerDefinition, b as ViewLayerToken, r as resolveViewAdapter, v as viewAdapterOf } from '../view-adapter-CSPKr2xy.js';
1
+ import { I as IslandDescriptor } from '../view-adapter-DtNgyjIf.js';
2
+ export { R as RenderIslandOptions, c as RenderTree, S as ServerRender, d as ServerRenderResult, V as ViewAdapter, a as ViewLayerDefinition, b as ViewLayerToken, r as resolveViewAdapter, v as viewAdapterOf } from '../view-adapter-DtNgyjIf.js';
3
3
  import { Container } from '@foxford/ioc';
4
4
  import { g as GuardDefinition } from '../types-COwVwgzE.js';
5
5
  import '../define-slot-Dil8Kr1A.js';
6
6
 
7
+ /** Вклад приложения в `<head>` документа. */
8
+ interface DocumentHead {
9
+ title?: string;
10
+ description?: string;
11
+ keywords?: string;
12
+ /** При отсутствии хост берёт `title`. */
13
+ ogTitle?: string;
14
+ /** При отсутствии хост берёт `description`. */
15
+ ogDescription?: string;
16
+ }
17
+ /** Что приложение сообщает хосту про страницу, которую оно нарисовало. */
18
+ interface DocumentContribution {
19
+ head?: DocumentHead;
20
+ /**
21
+ * Исход запроса статусом HTTP; не объявлен — `200`.
22
+ *
23
+ * Статусом, а не набором признаков вроде «страницы нет»: исход у запроса один, а признаки
24
+ * позволяют объявить сразу два взаимоисключающих. Тем же статусом исход едет по сети,
25
+ * поэтому у обоих транспортов дискриминатор общий.
26
+ *
27
+ * Нужен отдельно от исключения: `throw` означает, что приложение упало, и хост рисует
28
+ * плашку вместо него. Приложение, занявшее путь по префиксу (`/legal/*`), обязано уметь
29
+ * сказать `404` про несуществующий адрес внутри своего пространства.
30
+ */
31
+ status?: number;
32
+ /** Адрес для 3xx. Объявлен без статуса — исход `302`. */
33
+ redirect?: string;
34
+ }
35
+ /** VM, которой есть что сказать про документ. */
36
+ interface DocumentSource {
37
+ describeDocument(): DocumentContribution;
38
+ }
39
+ /**
40
+ * Реализует ли объект {@link DocumentSource}.
41
+ *
42
+ * @param value - Проверяемое значение
43
+ */
44
+ declare function isDocumentSource(value: unknown): value is DocumentSource;
45
+
46
+ /**
47
+ * Адрес, по которому рендерится остров.
48
+ *
49
+ * Приходит АРГУМЕНТОМ активации, а не через контейнер: контейнер живёт и на клиенте, где
50
+ * адреса в нём нет, и юнит, потребовавший его через `requires`, упал бы при резолве.
51
+ */
52
+ interface IslandRoute {
53
+ /** Путь без search, с ведущим слэшем: `/legal/general`. */
54
+ pathname: string;
55
+ /** Строка запроса без `?`; пустая, если её нет. */
56
+ search: string;
57
+ /** Параметры, выделенные маршрутом хоста. */
58
+ params: Record<string, string>;
59
+ }
7
60
  /** Опции сборки контейнера острова. */
8
61
  interface CreateIslandContainerOptions {
9
62
  /**
@@ -42,14 +95,36 @@ declare function createIslandContainer(descriptor: IslandDescriptor, options?: C
42
95
  */
43
96
  declare function hydrateIslandState(container: Container, descriptor: IslandDescriptor, denState: Record<string, unknown>): void;
44
97
  /**
45
- * Активирует VM острова самостоятельно когда серверного denState нет (ремоунт острова
46
- * без данных, чисто клиентский переход). С denState активация не нужна: состояние уже
47
- * приехало готовым.
98
+ * Активирует VM острова: каждая считает своё состояние по адресу.
99
+ *
100
+ * Параллельно, потому что VM одного острова друг о друге не знают; ждём все — состояние
101
+ * нужно целиком, а не по частям.
102
+ *
103
+ * @param container - Контейнер острова
104
+ * @param descriptor - Дескриптор острова
105
+ * @param route - Адрес, по которому рендерится остров
106
+ */
107
+ declare function activateIslandViewModels(container: Container, descriptor: IslandDescriptor, route?: IslandRoute): Promise<void>;
108
+ /**
109
+ * Снимает состояние VM острова — операция, обратная {@link hydrateIslandState}.
110
+ *
111
+ * @param container - Контейнер острова
112
+ * @param descriptor - Дескриптор острова
113
+ * @returns Состояние по ключам `token.description`
114
+ */
115
+ declare function collectIslandState(container: Container, descriptor: IslandDescriptor): Record<string, unknown>;
116
+ /**
117
+ * Собирает вклад острова в документ по всем его VM.
118
+ *
119
+ * Правила слияния два, и они следуют из смысла полей. `head` — набор полей, поэтому
120
+ * объявленное позже уточняет объявленное раньше. Исход (`status` вместе с `redirect`) —
121
+ * ОДНО решение, а не набор: побеждает первое объявленное, и берётся оно целиком, иначе
122
+ * статус одной VM склеился бы с адресом другой.
48
123
  *
49
124
  * @param container - Контейнер острова
50
125
  * @param descriptor - Дескриптор острова
51
126
  */
52
- declare function activateIslandViewModels(container: Container, descriptor: IslandDescriptor): void;
127
+ declare function collectDocumentContribution(container: Container, descriptor: IslandDescriptor): DocumentContribution;
53
128
  /**
54
129
  * Поднимает единицы, объявленные `eager`, — те, что обязаны жить с загрузки, а не с первого
55
130
  * резолва.
@@ -57,14 +132,59 @@ declare function activateIslandViewModels(container: Container, descriptor: Isla
57
132
  * Отдельным шагом, а не внутри сборки контейнера: контейнер строится и на сервере (в vike —
58
133
  * прямо в `renderToString`), а сервер живёт один рендер, и поднимать там WS-подписку или
59
134
  * прогрев кэша незачем — открытый сокет на каждый рендер это не «eager», это утечка. Зовёт
60
- * адаптер в точке монтирования, которой на сервере просто нет; серверный прогрев у приложения
61
- * называется `load`.
135
+ * адаптер в точке монтирования, которой на сервере просто нет; серверное состояние приложения
136
+ * считает активация VM.
62
137
  *
63
138
  * @param container - Контейнер острова
64
139
  * @param descriptor - Дескриптор острова
65
140
  */
66
141
  declare function resolveEagerUnits(container: Container, descriptor: IslandDescriptor): void;
67
142
 
143
+ /** Запрос на рендер приложения. */
144
+ interface RenderAppRequest {
145
+ /** Путь без search, с ведущим слэшем: `/legal/general`. */
146
+ pathname: string;
147
+ /** Строка запроса без `?`. */
148
+ search?: string;
149
+ /** Параметры, выделенные маршрутом хоста. */
150
+ params?: Record<string, string>;
151
+ /**
152
+ * Родитель контейнера приложения: через него приезжает всё, что настроил исполнитель, —
153
+ * логгер, конфиг HTTP, выданные возможности. По умолчанию — app-scope контейнер.
154
+ */
155
+ parent?: Container;
156
+ /** Внешние заполнители слотов; для ядра непрозрачны. */
157
+ slots?: Record<string, unknown>;
158
+ }
159
+ /** Ответ приложения: всё, что хосту нужно, чтобы собрать документ и ответить. */
160
+ interface RenderAppResult {
161
+ /**
162
+ * Исход запроса статусом HTTP. `200` — вот страница; всё остальное значит, что страницы
163
+ * по этому адресу нет, и тело такого ответа выбирает хост: страница ошибки у портала одна
164
+ * на все приложения.
165
+ */
166
+ status: number;
167
+ /** Разметка приложения; пустая при любом исходе, кроме `200`. */
168
+ html: string;
169
+ /** Состояние VM по ключам `token.description` — им гидрируется остров в браузере. */
170
+ state: Record<string, unknown>;
171
+ /** Вклад в `<head>` данными. */
172
+ head?: DocumentHead;
173
+ /** Вклад в `<head>` готовой разметкой — стили, собранные во время рендера. */
174
+ headHtml?: string;
175
+ /** Адрес для 3xx. */
176
+ redirect?: string;
177
+ }
178
+ /**
179
+ * Рисует приложение по адресу и отдаёт разметку вместе с состоянием.
180
+ *
181
+ * @param descriptor - Дескриптор приложения
182
+ * @param request - Адрес, родительский контейнер и заполнители слотов
183
+ * @returns Разметка, состояние и вклад в документ
184
+ * @throws Если контейнер приложения не собрался — исполнитель решает, чем это показать
185
+ */
186
+ declare function renderApp(descriptor: IslandDescriptor, request: RenderAppRequest): Promise<RenderAppResult>;
187
+
68
188
  /** Запись живого острова: имя + фактический состав его контейнера. */
69
189
  interface IslandRecord {
70
190
  /** Уникальный id инстанса острова (островов одного приложения может быть несколько). */
@@ -146,51 +266,4 @@ type AppRoute = string | RouteDeclaration;
146
266
  */
147
267
  declare function normalizeRoutes(routes?: ReadonlyArray<AppRoute>): RouteDeclaration[];
148
268
 
149
- /** Что приложение отдало серверным рендером. */
150
- interface ServerRenderResult {
151
- /** Разметка дерева — результат переданного рендера. */
152
- html: string;
153
- /**
154
- * Вклад в `<head>` ГОТОВОЙ разметкой: то, что рождается только во время рендера и
155
- * данными не описывается.
156
- *
157
- * Таковы стили CSS-in-JS: стороннее представление у них одно — тег со служебными
158
- * атрибутами, по которым клиентский рантайм узнаёт свои правила. Разобрав его на поля,
159
- * потеряешь регидратацию, и стили впрыснутся вторым экземпляром. Поэтому вклад в документ
160
- * ДАННЫМИ (`DocumentHead` у хостов) остаётся отдельным каналом, а это — разметка.
161
- *
162
- * Разметка приезжает в документ хоста ДОСЛОВНО, без экранирования — иначе служебные
163
- * атрибуты не пережили бы вставку. Отвечает за содержимое приложение: у сетевого
164
- * транспорта строка приходит из чужого процесса, и хост её не разбирает.
165
- */
166
- head?: string;
167
- }
168
- /** Рендер дерева в разметку. Его даёт тот, кто серверный рендер исполняет. */
169
- type RenderTree<Tree = unknown> = (tree: Tree) => string;
170
- /**
171
- * Серверный рендер приложения: приложению дают его дерево и рендер, оно возвращает
172
- * разметку и свой вклад в документ.
173
- *
174
- * Рендер приходит АРГУМЕНТОМ, потому что приложение им не владеет: страницу собирает тот,
175
- * кто держит документ, и приложений на ней может быть несколько. Отдав рендер аргументом,
176
- * приложение получает обычную функцию — состояние прохода живёт локальной переменной и
177
- * между запросами не утекает.
178
- *
179
- * Подпись одна на оба транспорта, и в этом смысл: у приложения в процессе хоста рендер
180
- * передаёт хост, у сетевого — его собственный сервер, а `html` и `head` едут ответом.
181
- * Перевод приложения между транспортами эту часть не задевает.
182
- *
183
- * `Tree` — дерево view-фреймворка, для ядра непрозрачное. View-адаптер сужает параметр
184
- * до своего типа (`ReactServerRender` в `@foxford/den-react`), и приложение пишет проход
185
- * в терминах СВОЕГО фреймворка.
186
- *
187
- * @example
188
- * serverRender: (tree, render) => {
189
- * const sheet = new ServerStyleSheet()
190
- *
191
- * return { head: sheet.getStyleTags(), html: render(sheet.collectStyles(tree)) }
192
- * }
193
- */
194
- type ServerRender<Tree = unknown> = (tree: Tree, render: RenderTree<Tree>) => ServerRenderResult;
195
-
196
- export { type AppRoute, type CreateIslandContainerOptions, IslandDescriptor, type IslandRecord, type RenderTree, type RouteDeclaration, type ServerRender, type ServerRenderResult, activateIslandViewModels, createIslandContainer, describeIsland, getAppContainer, getIslandsSnapshot, hydrateIslandState, normalizeRoutes, registerIsland, resolveEagerUnits, slotAddress, subscribeIslands };
269
+ export { type AppRoute, type CreateIslandContainerOptions, type DocumentContribution, type DocumentHead, type DocumentSource, IslandDescriptor, type IslandRecord, type IslandRoute, type RenderAppRequest, type RenderAppResult, type RouteDeclaration, activateIslandViewModels, collectDocumentContribution, collectIslandState, createIslandContainer, describeIsland, getAppContainer, getIslandsSnapshot, hydrateIslandState, isDocumentSource, normalizeRoutes, registerIsland, renderApp, resolveEagerUnits, slotAddress, subscribeIslands };
package/island/index.js CHANGED
@@ -2,16 +2,22 @@ import {
2
2
  ViewLayerToken,
3
3
  resolveViewAdapter,
4
4
  viewAdapterOf
5
- } from "../chunk-TCYGDT2A.js";
5
+ } from "../chunk-E23E2VUC.js";
6
6
  import {
7
7
  LoggerToken,
8
8
  logger,
9
9
  unitLogger
10
10
  } from "../chunk-TPBI6TOU.js";
11
11
  import {
12
+ __async,
12
13
  __spreadValues
13
14
  } from "../chunk-HJO26HIQ.js";
14
15
 
16
+ // src/island/document.ts
17
+ function isDocumentSource(value) {
18
+ return typeof value === "object" && value !== null && typeof value.describeDocument === "function";
19
+ }
20
+
15
21
  // src/island/lifecycle.ts
16
22
  import { log } from "@foxford/logger";
17
23
 
@@ -28,6 +34,8 @@ function getAppContainer() {
28
34
  }
29
35
 
30
36
  // src/island/lifecycle.ts
37
+ var EMPTY_ROUTE = { params: {}, pathname: "", search: "" };
38
+ var REDIRECT_FOUND = 302;
31
39
  function createIslandContainer(descriptor, options = {}) {
32
40
  var _a, _b, _c, _d, _e;
33
41
  const { parent = getAppContainer() } = options;
@@ -68,13 +76,49 @@ function hydrateIslandState(container, descriptor, denState) {
68
76
  }
69
77
  }
70
78
  }
71
- function activateIslandViewModels(container, descriptor) {
79
+ function activateIslandViewModels(_0, _1) {
80
+ return __async(this, arguments, function* (container, descriptor, route = EMPTY_ROUTE) {
81
+ var _a;
82
+ yield Promise.all(
83
+ ((_a = descriptor.viewModels) != null ? _a : []).map((vm) => __async(null, null, function* () {
84
+ var _a2;
85
+ const instance = container.resolve(vm.token);
86
+ unitLogger(container, vm.token.description).debug(`\u0430\u043A\u0442\u0438\u0432\u0430\u0446\u0438\u044F VM (${route.pathname || "\u2014"})`);
87
+ yield (_a2 = instance.activate) == null ? void 0 : _a2.call(instance, route);
88
+ }))
89
+ );
90
+ });
91
+ }
92
+ function collectIslandState(container, descriptor) {
72
93
  var _a, _b;
94
+ const state = {};
73
95
  for (const vm of (_a = descriptor.viewModels) != null ? _a : []) {
74
96
  const instance = container.resolve(vm.token);
75
- unitLogger(container, vm.token.description).debug("\u0430\u043A\u0442\u0438\u0432\u0430\u0446\u0438\u044F VM (\u0431\u0435\u0437 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u043E\u0433\u043E denState)");
76
- void ((_b = instance.activate) == null ? void 0 : _b.call(instance));
97
+ if (typeof ((_b = instance.state) == null ? void 0 : _b.get) === "function") {
98
+ state[vm.token.description] = instance.state.get();
99
+ }
77
100
  }
101
+ return state;
102
+ }
103
+ function collectDocumentContribution(container, descriptor) {
104
+ var _a, _b;
105
+ const contribution = {};
106
+ for (const vm of (_a = descriptor.viewModels) != null ? _a : []) {
107
+ const instance = container.resolve(vm.token);
108
+ if (!isDocumentSource(instance)) {
109
+ continue;
110
+ }
111
+ const part = instance.describeDocument();
112
+ if (part.head) {
113
+ contribution.head = __spreadValues(__spreadValues({}, contribution.head), part.head);
114
+ }
115
+ const declared = part.status !== void 0 || part.redirect !== void 0;
116
+ if (declared && contribution.status === void 0) {
117
+ contribution.status = (_b = part.status) != null ? _b : REDIRECT_FOUND;
118
+ contribution.redirect = part.redirect;
119
+ }
120
+ }
121
+ return contribution;
78
122
  }
79
123
  function resolveEagerUnits(container, descriptor) {
80
124
  var _a;
@@ -89,6 +133,38 @@ function resolveEagerUnits(container, descriptor) {
89
133
  }
90
134
  }
91
135
 
136
+ // src/island/render.ts
137
+ var STATUS_OK = 200;
138
+ function renderApp(descriptor, request) {
139
+ return __async(this, null, function* () {
140
+ var _a, _b;
141
+ const route = {
142
+ params: (_a = request.params) != null ? _a : {},
143
+ pathname: request.pathname,
144
+ search: (_b = request.search) != null ? _b : ""
145
+ };
146
+ const container = createIslandContainer(descriptor, { parent: request.parent });
147
+ if (container === null) {
148
+ throw new Error(`den/island: \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \xAB${descriptor.name}\xBB \u043D\u0435 \u0441\u043E\u0431\u0440\u0430\u043B\u043E\u0441\u044C \u2014 \u0440\u0435\u043D\u0434\u0435\u0440 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u0435\u043D`);
149
+ }
150
+ try {
151
+ yield activateIslandViewModels(container, descriptor, route);
152
+ const { head, redirect, status = STATUS_OK } = collectDocumentContribution(container, descriptor);
153
+ if (status !== STATUS_OK) {
154
+ return { head, html: "", redirect, state: {}, status };
155
+ }
156
+ const state = collectIslandState(container, descriptor);
157
+ const rendered = resolveViewAdapter(container).renderIsland(descriptor, {
158
+ container,
159
+ slots: request.slots
160
+ });
161
+ return { head, headHtml: rendered.head, html: rendered.html, state, status };
162
+ } finally {
163
+ yield container.dispose();
164
+ }
165
+ });
166
+ }
167
+
92
168
  // src/island/slot-address.ts
93
169
  function slotAddress(app, slot) {
94
170
  return `${app}:${slot}`;
@@ -141,13 +217,17 @@ function normalizeRoutes(routes = []) {
141
217
  export {
142
218
  ViewLayerToken,
143
219
  activateIslandViewModels,
220
+ collectDocumentContribution,
221
+ collectIslandState,
144
222
  createIslandContainer,
145
223
  describeIsland,
146
224
  getAppContainer,
147
225
  getIslandsSnapshot,
148
226
  hydrateIslandState,
227
+ isDocumentSource,
149
228
  normalizeRoutes,
150
229
  registerIsland,
230
+ renderApp,
151
231
  resolveEagerUnits,
152
232
  resolveViewAdapter,
153
233
  slotAddress,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxford/den",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "Den — декларативный метафреймворк Foxford (core runtime)",
5
5
  "keywords": [
6
6
  "foxford",
@@ -56,13 +56,13 @@
56
56
  "chunk-5DJZZML5.cjs",
57
57
  "chunk-A6ZPAM6Z.cjs",
58
58
  "chunk-C7BM4DGX.cjs",
59
+ "chunk-E23E2VUC.js",
59
60
  "chunk-HJO26HIQ.js",
60
- "chunk-LJE7K7VD.cjs",
61
- "chunk-TCYGDT2A.js",
62
61
  "chunk-TPBI6TOU.js",
63
62
  "chunk-WBHHHICS.js",
64
63
  "chunk-WRQC6BVJ.js",
65
64
  "chunk-XHYR3SGG.cjs",
65
+ "chunk-XOJ2VWX4.cjs",
66
66
  "define-repository-Ca62_YCy.d.ts",
67
67
  "define-repository-MDEWHqM7.d.cts",
68
68
  "define-slot-BKvArA02.d.cts",
@@ -79,9 +79,9 @@
79
79
  "package.json",
80
80
  "types-COwVwgzE.d.cts",
81
81
  "types-COwVwgzE.d.ts",
82
- "view-adapter-CSPKr2xy.d.ts",
83
- "view-adapter-zbHBVVqR.d.cts"
82
+ "view-adapter-BvDC5O4y.d.cts",
83
+ "view-adapter-DtNgyjIf.d.ts"
84
84
  ],
85
- "sha": "d6b3073",
85
+ "sha": "ba6123d",
86
86
  "scripts": {}
87
87
  }
@@ -54,7 +54,7 @@ interface IslandDescriptor {
54
54
  *
55
55
  * Поднимаются при МОНТИРОВАНИИ острова, а не при сборке контейнера: контейнер строится и на
56
56
  * сервере, а сервер живёт один рендер — открытая там WS-подписка это не «eager», а утечка.
57
- * Серверный прогрев у приложения называется `load`.
57
+ * Серверное состояние приложения считает активация VM.
58
58
  */
59
59
  eager?: ReadonlyArray<Token<unknown>>;
60
60
  /**
@@ -67,6 +67,60 @@ interface IslandDescriptor {
67
67
  slots?: Readonly<Record<string, SlotDefinition>>;
68
68
  }
69
69
 
70
+ /** Что приложение отдало серверным рендером. */
71
+ interface ServerRenderResult {
72
+ /** Разметка дерева — результат переданного рендера. */
73
+ html: string;
74
+ /**
75
+ * Вклад в `<head>` ГОТОВОЙ разметкой: то, что рождается только во время рендера и
76
+ * данными не описывается.
77
+ *
78
+ * Таковы стили CSS-in-JS: стороннее представление у них одно — тег со служебными
79
+ * атрибутами, по которым клиентский рантайм узнаёт свои правила. Разобрав его на поля,
80
+ * потеряешь регидратацию, и стили впрыснутся вторым экземпляром. Поэтому вклад в документ
81
+ * ДАННЫМИ (`DocumentHead` у хостов) остаётся отдельным каналом, а это — разметка.
82
+ *
83
+ * Разметка приезжает в документ хоста ДОСЛОВНО, без экранирования — иначе служебные
84
+ * атрибуты не пережили бы вставку. Отвечает за содержимое приложение: у сетевого
85
+ * транспорта строка приходит из чужого процесса, и хост её не разбирает.
86
+ */
87
+ head?: string;
88
+ }
89
+ /** Рендер дерева в разметку. Его даёт тот, кто серверный рендер исполняет. */
90
+ type RenderTree<Tree = unknown> = (tree: Tree) => string;
91
+ /**
92
+ * Серверный рендер приложения: приложению дают его дерево и рендер, оно возвращает
93
+ * разметку и свой вклад в документ.
94
+ *
95
+ * Рендер приходит АРГУМЕНТОМ, потому что приложение им не владеет: страницу собирает тот,
96
+ * кто держит документ, и приложений на ней может быть несколько. Отдав рендер аргументом,
97
+ * приложение получает обычную функцию — состояние прохода живёт локальной переменной и
98
+ * между запросами не утекает.
99
+ *
100
+ * Подпись одна на оба транспорта, и в этом смысл: у приложения в процессе хоста рендер
101
+ * передаёт хост, у сетевого — его собственный сервер, а `html` и `head` едут ответом.
102
+ * Перевод приложения между транспортами эту часть не задевает.
103
+ *
104
+ * `Tree` — дерево view-фреймворка, для ядра непрозрачное. View-адаптер сужает параметр
105
+ * до своего типа (`ReactServerRender` в `@foxford/den-react`), и приложение пишет проход
106
+ * в терминах СВОЕГО фреймворка.
107
+ *
108
+ * @example
109
+ * serverRender: (tree, render) => {
110
+ * const sheet = new ServerStyleSheet()
111
+ *
112
+ * return { head: sheet.getStyleTags(), html: render(sheet.collectStyles(tree)) }
113
+ * }
114
+ */
115
+ type ServerRender<Tree = unknown> = (tree: Tree, render: RenderTree<Tree>) => ServerRenderResult;
116
+
117
+ /** Что нужно адаптеру, чтобы отрисовать остров на сервере. */
118
+ interface RenderIslandOptions {
119
+ /** Контейнер острова — уже собранный, с активированными VM. */
120
+ container: Container;
121
+ /** Внешние заполнители слотов; для ядра непрозрачны, как и само дерево. */
122
+ slots?: Record<string, unknown>;
123
+ }
70
124
  /**
71
125
  * Порт view-адаптера. Реализуется пакетом фреймворка (`@foxford/den-react` и его аналоги),
72
126
  * объявляется приложением через `defineLayer(ViewLayerToken, { adapter })`.
@@ -82,6 +136,18 @@ interface ViewAdapter {
82
136
  * @returns Компонент фреймворка — для ядра непрозрачен
83
137
  */
84
138
  createIsland(descriptor: IslandDescriptor): unknown;
139
+ /**
140
+ * Отрисовывает остров в разметку на сервере.
141
+ *
142
+ * Единственная операция острова, которую ядро не может сделать само: `renderToString` —
143
+ * функция конкретного фреймворка, а ядро view-агностично. Здесь же адаптер исполняет
144
+ * `serverRender` приложения, если тот объявлен, — тип дерева известен только адаптеру.
145
+ *
146
+ * @param descriptor - Дескриптор острова
147
+ * @param options - Контейнер острова и заполнители слотов
148
+ * @returns Разметка и вклад в `<head>` готовой разметкой
149
+ */
150
+ renderIsland(descriptor: IslandDescriptor, options: RenderIslandOptions): ServerRenderResult;
85
151
  /**
86
152
  * Ставит стратегию резолва `define*` во view (`installResolver` из `@foxford/den/adapter`).
87
153
  * У каждого фреймворка она своя: у React — хук поверх контекста контейнера, у Vue —
@@ -119,4 +185,4 @@ declare function viewAdapterOf(layer: ViewLayerDefinition): ViewAdapter;
119
185
  */
120
186
  declare function resolveViewAdapter(container: Container): ViewAdapter;
121
187
 
122
- export { type IslandDescriptor as I, type ViewAdapter as V, type ViewLayerDefinition as a, ViewLayerToken as b, resolveViewAdapter as r, viewAdapterOf as v };
188
+ export { type IslandDescriptor as I, type RenderIslandOptions as R, type ServerRender as S, type ViewAdapter as V, type ViewLayerDefinition as a, ViewLayerToken as b, type RenderTree as c, type ServerRenderResult as d, resolveViewAdapter as r, viewAdapterOf as v };
@@ -54,7 +54,7 @@ interface IslandDescriptor {
54
54
  *
55
55
  * Поднимаются при МОНТИРОВАНИИ острова, а не при сборке контейнера: контейнер строится и на
56
56
  * сервере, а сервер живёт один рендер — открытая там WS-подписка это не «eager», а утечка.
57
- * Серверный прогрев у приложения называется `load`.
57
+ * Серверное состояние приложения считает активация VM.
58
58
  */
59
59
  eager?: ReadonlyArray<Token<unknown>>;
60
60
  /**
@@ -67,6 +67,60 @@ interface IslandDescriptor {
67
67
  slots?: Readonly<Record<string, SlotDefinition>>;
68
68
  }
69
69
 
70
+ /** Что приложение отдало серверным рендером. */
71
+ interface ServerRenderResult {
72
+ /** Разметка дерева — результат переданного рендера. */
73
+ html: string;
74
+ /**
75
+ * Вклад в `<head>` ГОТОВОЙ разметкой: то, что рождается только во время рендера и
76
+ * данными не описывается.
77
+ *
78
+ * Таковы стили CSS-in-JS: стороннее представление у них одно — тег со служебными
79
+ * атрибутами, по которым клиентский рантайм узнаёт свои правила. Разобрав его на поля,
80
+ * потеряешь регидратацию, и стили впрыснутся вторым экземпляром. Поэтому вклад в документ
81
+ * ДАННЫМИ (`DocumentHead` у хостов) остаётся отдельным каналом, а это — разметка.
82
+ *
83
+ * Разметка приезжает в документ хоста ДОСЛОВНО, без экранирования — иначе служебные
84
+ * атрибуты не пережили бы вставку. Отвечает за содержимое приложение: у сетевого
85
+ * транспорта строка приходит из чужого процесса, и хост её не разбирает.
86
+ */
87
+ head?: string;
88
+ }
89
+ /** Рендер дерева в разметку. Его даёт тот, кто серверный рендер исполняет. */
90
+ type RenderTree<Tree = unknown> = (tree: Tree) => string;
91
+ /**
92
+ * Серверный рендер приложения: приложению дают его дерево и рендер, оно возвращает
93
+ * разметку и свой вклад в документ.
94
+ *
95
+ * Рендер приходит АРГУМЕНТОМ, потому что приложение им не владеет: страницу собирает тот,
96
+ * кто держит документ, и приложений на ней может быть несколько. Отдав рендер аргументом,
97
+ * приложение получает обычную функцию — состояние прохода живёт локальной переменной и
98
+ * между запросами не утекает.
99
+ *
100
+ * Подпись одна на оба транспорта, и в этом смысл: у приложения в процессе хоста рендер
101
+ * передаёт хост, у сетевого — его собственный сервер, а `html` и `head` едут ответом.
102
+ * Перевод приложения между транспортами эту часть не задевает.
103
+ *
104
+ * `Tree` — дерево view-фреймворка, для ядра непрозрачное. View-адаптер сужает параметр
105
+ * до своего типа (`ReactServerRender` в `@foxford/den-react`), и приложение пишет проход
106
+ * в терминах СВОЕГО фреймворка.
107
+ *
108
+ * @example
109
+ * serverRender: (tree, render) => {
110
+ * const sheet = new ServerStyleSheet()
111
+ *
112
+ * return { head: sheet.getStyleTags(), html: render(sheet.collectStyles(tree)) }
113
+ * }
114
+ */
115
+ type ServerRender<Tree = unknown> = (tree: Tree, render: RenderTree<Tree>) => ServerRenderResult;
116
+
117
+ /** Что нужно адаптеру, чтобы отрисовать остров на сервере. */
118
+ interface RenderIslandOptions {
119
+ /** Контейнер острова — уже собранный, с активированными VM. */
120
+ container: Container;
121
+ /** Внешние заполнители слотов; для ядра непрозрачны, как и само дерево. */
122
+ slots?: Record<string, unknown>;
123
+ }
70
124
  /**
71
125
  * Порт view-адаптера. Реализуется пакетом фреймворка (`@foxford/den-react` и его аналоги),
72
126
  * объявляется приложением через `defineLayer(ViewLayerToken, { adapter })`.
@@ -82,6 +136,18 @@ interface ViewAdapter {
82
136
  * @returns Компонент фреймворка — для ядра непрозрачен
83
137
  */
84
138
  createIsland(descriptor: IslandDescriptor): unknown;
139
+ /**
140
+ * Отрисовывает остров в разметку на сервере.
141
+ *
142
+ * Единственная операция острова, которую ядро не может сделать само: `renderToString` —
143
+ * функция конкретного фреймворка, а ядро view-агностично. Здесь же адаптер исполняет
144
+ * `serverRender` приложения, если тот объявлен, — тип дерева известен только адаптеру.
145
+ *
146
+ * @param descriptor - Дескриптор острова
147
+ * @param options - Контейнер острова и заполнители слотов
148
+ * @returns Разметка и вклад в `<head>` готовой разметкой
149
+ */
150
+ renderIsland(descriptor: IslandDescriptor, options: RenderIslandOptions): ServerRenderResult;
85
151
  /**
86
152
  * Ставит стратегию резолва `define*` во view (`installResolver` из `@foxford/den/adapter`).
87
153
  * У каждого фреймворка она своя: у React — хук поверх контекста контейнера, у Vue —
@@ -119,4 +185,4 @@ declare function viewAdapterOf(layer: ViewLayerDefinition): ViewAdapter;
119
185
  */
120
186
  declare function resolveViewAdapter(container: Container): ViewAdapter;
121
187
 
122
- export { type IslandDescriptor as I, type ViewAdapter as V, type ViewLayerDefinition as a, ViewLayerToken as b, resolveViewAdapter as r, viewAdapterOf as v };
188
+ export { type IslandDescriptor as I, type RenderIslandOptions as R, type ServerRender as S, type ViewAdapter as V, type ViewLayerDefinition as a, ViewLayerToken as b, type RenderTree as c, type ServerRenderResult as d, resolveViewAdapter as r, viewAdapterOf as v };