@foxford/den 3.0.0 → 3.1.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/README.mdx +215 -2
- package/chunk-6ZFVV3AV.cjs +214 -0
- package/chunk-7M5OAOQ4.js +214 -0
- package/chunk-CZVYVNLI.cjs +45 -0
- package/chunk-KJ6NDOOL.js +45 -0
- package/{chunk-5DJZZML5.cjs → chunk-P4QJCHMM.cjs} +12 -29
- package/{chunk-WRQC6BVJ.js → chunk-PZVUKTZG.js} +3 -20
- package/define.cjs +4 -4
- package/define.js +3 -3
- package/den-serve.js +146 -0
- package/descriptor-DZDNH7R5.d.ts +70 -0
- package/descriptor-h0JE9jFx.d.cts +70 -0
- package/index.cjs +16 -15
- package/index.d.cts +2 -1
- package/index.d.ts +2 -1
- package/index.js +11 -10
- package/island/index.cjs +10 -156
- package/island/index.d.cts +32 -29
- package/island/index.d.ts +32 -29
- package/island/index.js +17 -163
- package/package.json +27 -10
- package/routes-BqIoqbt1.d.cts +64 -0
- package/routes-CWpIqVAM.d.ts +64 -0
- package/serve/index.cjs +136 -0
- package/serve/index.d.cts +60 -0
- package/serve/index.d.ts +60 -0
- package/serve/index.js +136 -0
- package/{view-adapter-BvDC5O4y.d.cts → view-adapter-CPI8056c.d.ts} +4 -69
- package/{view-adapter-DtNgyjIf.d.ts → view-adapter-D2597RJZ.d.cts} +4 -69
- package/chunk-A6ZPAM6Z.cjs +0 -26
- package/chunk-TPBI6TOU.js +0 -26
package/README.mdx
CHANGED
|
@@ -4,6 +4,219 @@ title: Den
|
|
|
4
4
|
|
|
5
5
|
# @foxford/den
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Декларативный метафреймворк Foxford: приложение объявляет, из чего оно состоит, а исполняет
|
|
8
|
+
это движок. Ядро host-агностично — про Vike и Next знают отдельные пакеты
|
|
9
|
+
(`@foxford/den-vike`, `@foxford/den-next`), про React — `@foxford/den-react`.
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Здесь примеры. Почему устроено именно так — в [SPECIFICATION.md](./SPECIFICATION.md).
|
|
12
|
+
|
|
13
|
+
## Единицы приложения
|
|
14
|
+
|
|
15
|
+
Слои объявляются `define*`-функциями и складываются в контейнер. Зависимости приходят через
|
|
16
|
+
`requires` и резолвятся движком, поэтому единица не знает, кто её создаёт.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// content-page.repository.ts
|
|
20
|
+
import { defineRepository } from '@foxford/den/define'
|
|
21
|
+
|
|
22
|
+
export default defineRepository(ContentPageRepositoryToken, {
|
|
23
|
+
create: () => ({
|
|
24
|
+
getPage: async (path: string) => fetch(`/api/pages${path}`).then((res) => res.json()),
|
|
25
|
+
}),
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// content-page.vm.ts
|
|
31
|
+
import { defineViewModel } from '@foxford/den/define'
|
|
32
|
+
import { ViewModel } from '@foxford/vm'
|
|
33
|
+
|
|
34
|
+
import type { IslandRoute } from '@foxford/den/island'
|
|
35
|
+
|
|
36
|
+
export class ContentPageVM extends ViewModel<ReturnType<typeof makeState>> {
|
|
37
|
+
constructor(private readonly service: ContentPageService) {
|
|
38
|
+
super(makeState())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Зовёт движок при рендере страницы — на сервере и в браузере одинаково. */
|
|
42
|
+
async activate(route: IslandRoute): Promise<void> {
|
|
43
|
+
this.state.set(await loadContentPage(this.service, route.pathname))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default defineViewModel(ContentPageVmToken, {
|
|
48
|
+
create: ([service]) => new ContentPageVM(service),
|
|
49
|
+
requires: [ContentPageServiceToken] as const,
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Заявка на адреса и её параметры
|
|
54
|
+
|
|
55
|
+
Приложение объявляет, какие пути берётся обслуживать. Заявка — **регулярка**: она строго
|
|
56
|
+
выразительнее шаблона (`'.*'` как ловец остатка, префиксы, альтернативы), и порядок в списке
|
|
57
|
+
значим — хост отдаёт путь первому заявившему, поэтому частные записи ставятся выше общих.
|
|
58
|
+
|
|
59
|
+
Параметры адреса — именованные группы. Писать их руками не нужно, для этого `pathPattern`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { pathPattern } from '@foxford/den/island'
|
|
63
|
+
|
|
64
|
+
routes: [
|
|
65
|
+
pathPattern('/course/:id'), // → '^/course/(?<id>[^/]+)$'
|
|
66
|
+
'^/legal', // обычная регулярка рядом
|
|
67
|
+
]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Выделяет их `matchRoute` — один матчер на всех: по нему хост решает, кому отдать адрес,
|
|
71
|
+
а рантайм приложения — своё ли это. Разойдись эти два ответа, приложение получало бы запросы
|
|
72
|
+
по путям, которых не заявляло. Значения раскодированы (`/legal/%D0%BE...` → `оферта`).
|
|
73
|
+
|
|
74
|
+
## Шов рендера: разметка и состояние одним вызовом
|
|
75
|
+
|
|
76
|
+
`renderApp` — то, чем хост спрашивает у приложения страницу. Внутри движок поднимает контейнер,
|
|
77
|
+
активирует VM по адресу, рисует дерево и снимает состояние; дескриптор при этом остаётся
|
|
78
|
+
декларацией без поведения.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { renderApp } from '@foxford/den/island'
|
|
82
|
+
|
|
83
|
+
const { status, html, state, head, headHtml, redirect } = await renderApp(app, {
|
|
84
|
+
pathname: '/legal/general',
|
|
85
|
+
search: '?from=footer',
|
|
86
|
+
context: { cookies, headers }, // реквизиты браузерного запроса
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`status` — исход запроса: `200` значит «вот страница», всё остальное значит, что страницы по
|
|
91
|
+
этому адресу нет, и тело такого ответа выбирает хост. `state` едет в документ и гидрирует
|
|
92
|
+
остров в браузере, `headHtml` — стили, собранные во время рендера.
|
|
93
|
+
|
|
94
|
+
### Вклад в документ
|
|
95
|
+
|
|
96
|
+
Заголовок и исход сообщает VM — необязательным методом `describeDocument`. Полем дескриптора
|
|
97
|
+
это быть не может: после активации только VM знает, нашлась ли страница по адресу.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
export class ContentPageVM extends ViewModel<State> {
|
|
101
|
+
/** В ручку ходим ЗДЕСЬ. */
|
|
102
|
+
async activate(route: IslandRoute): Promise<void> {
|
|
103
|
+
this.state.set(await loadContentPage(this.service, route.pathname))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** А здесь только решаем по тому, что уже приехало. */
|
|
107
|
+
describeDocument(): DocumentContribution {
|
|
108
|
+
const { page } = this.state.get()
|
|
109
|
+
|
|
110
|
+
if (page === null) {
|
|
111
|
+
return { status: 404 } // хост ответит честным 404
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { head: { description: page.description, title: page.title } }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`describeDocument` синхронный намеренно. Приложению, которому «есть ли такая страница» известно
|
|
120
|
+
только от бэкенда, второй поход не нужен: движок ждёт активацию ДО сбора вклада, поэтому к
|
|
121
|
+
моменту вызова ответ уже лежит в состоянии. Разреши мы тут асинхронность — у рендера появилась
|
|
122
|
+
бы вторая фаза походов в сеть, а логика загрузки разъехалась бы по двум местам.
|
|
123
|
+
|
|
124
|
+
Редирект объявляется там же: `{ redirect: '/user/login' }` — без статуса это `302`.
|
|
125
|
+
|
|
126
|
+
## Сетевой транспорт: то же самое по HTTP
|
|
127
|
+
|
|
128
|
+
Приложение поднимается отдельным сервисом **без единой строки своего серверного кода** —
|
|
129
|
+
дескриптор тот же, что и в процессе хоста.
|
|
130
|
+
|
|
131
|
+
```jsonc
|
|
132
|
+
// package.json приложения
|
|
133
|
+
"scripts": {
|
|
134
|
+
"dev": "tsup --watch src --onSuccess \"den-serve build/index.js\"",
|
|
135
|
+
"start": "den-serve build/index.js --port 3342"
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`den-serve <модуль>` берёт дефолтный экспорт указанного модуля и поднимает вокруг него рантайм:
|
|
140
|
+
|
|
141
|
+
| Ручка | Ответ |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `GET /_render?url=<path+search>` | `200` + ответ `renderApp` телом |
|
|
144
|
+
| `GET /_routes` | `200` + массив regex-строк заявки на адреса |
|
|
145
|
+
| `GET /_health` | `200 ok` |
|
|
146
|
+
|
|
147
|
+
**Статус HTTP описывает транспорт, а исход страницы едет полем `status` в теле.** `200` значит
|
|
148
|
+
«приложение ответило» — хоть `404`, хоть редиректом. Всё остальное значит «до приложения не
|
|
149
|
+
дошли»: `400` — сломанный запрос, `404` — неизвестная ручка или путь вне заявки, `500` — рендер
|
|
150
|
+
упал. Так `404` от приложения не путается с `404` от неверно настроенного прокси.
|
|
151
|
+
|
|
152
|
+
Браузерные заголовки рантайм читает только из `X-Forwarded-*` и кладёт в `RequestContextToken`;
|
|
153
|
+
`Cookie` — обычным именем.
|
|
154
|
+
|
|
155
|
+
### Расширение снаружи: пример с Sentry
|
|
156
|
+
|
|
157
|
+
Основа — fastify, и отдаётся она плагином, поэтому Sentry, метрики, cors и трейсинг ставит
|
|
158
|
+
ПОТРЕБИТЕЛЬ. Своих опций под каждую такую потребность пакет не заводит.
|
|
159
|
+
|
|
160
|
+
Инициализация Sentry должна пройти раньше всего остального кода, поэтому она живёт отдельным
|
|
161
|
+
модулем и грузится флагом `--import`, а не из точки входа:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// instrument.js — грузится раньше приложения
|
|
165
|
+
import * as Sentry from '@sentry/node'
|
|
166
|
+
|
|
167
|
+
Sentry.init({
|
|
168
|
+
dsn: process.env.SENTRY_DSN,
|
|
169
|
+
environment: process.env.NODE_ENV,
|
|
170
|
+
tracesSampleRate: 0.1,
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Обработчик ошибок вешается уже на готовый инстанс — это и есть `configure`:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// configure.js — дефолтный экспорт получает инстанс fastify до прослушивания
|
|
178
|
+
import * as Sentry from '@sentry/node'
|
|
179
|
+
|
|
180
|
+
export default (instance) => {
|
|
181
|
+
Sentry.setupFastifyErrorHandler(instance)
|
|
182
|
+
|
|
183
|
+
instance.get('/metrics', () => renderMetrics())
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```jsonc
|
|
188
|
+
// package.json приложения — своего серверного кода по-прежнему ноль
|
|
189
|
+
"scripts": {
|
|
190
|
+
"start": "den-serve build/index.js --configure ./configure.js --port 3342"
|
|
191
|
+
},
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Запуск с инструментацией: `NODE_OPTIONS='--import ./instrument.js' pnpm start`.
|
|
195
|
+
|
|
196
|
+
Если сервер нужен свой — общий порт с чужими ручками, свой порядок плагинов, — берите плагин
|
|
197
|
+
и монтируйте куда угодно, хоть под префиксом:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import Fastify from 'fastify'
|
|
201
|
+
import * as Sentry from '@sentry/node'
|
|
202
|
+
import { appPlugin } from '@foxford/den/serve'
|
|
203
|
+
|
|
204
|
+
const instance = Fastify()
|
|
205
|
+
Sentry.setupFastifyErrorHandler(instance)
|
|
206
|
+
|
|
207
|
+
await instance.register(appPlugin, { app, prefix: '/den' })
|
|
208
|
+
await instance.listen({ port: 3342 })
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`serveApp` поверх плагина добавляет логирование запросов (в терминах `@foxford/logger`, корень —
|
|
212
|
+
имя приложения, рендер дольше порога уходит в `warn`) и закрытие по `SIGTERM`/`SIGINT`.
|
|
213
|
+
|
|
214
|
+
## Подпути
|
|
215
|
+
|
|
216
|
+
| Подпуть | Что там |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| `@foxford/den` | рантайм, `define*`, менеджеры контейнеров и состояния |
|
|
219
|
+
| `@foxford/den/define` | те же `define*` без сайд-эффекта установки резолвера |
|
|
220
|
+
| `@foxford/den/adapter` | шов резолва для адаптеров хостов |
|
|
221
|
+
| `@foxford/den/island` | остров: жизненный цикл, шов `renderApp`, порт `ViewAdapter` |
|
|
222
|
+
| `@foxford/den/serve` | сетевой транспорт: `serveApp`, `appPlugin`, команда `den-serve` |
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
var _chunkXOJ2VWX4cjs = require('./chunk-XOJ2VWX4.cjs');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
var _chunkCZVYVNLIcjs = require('./chunk-CZVYVNLI.cjs');
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
var _chunkXHYR3SGGcjs = require('./chunk-XHYR3SGG.cjs');
|
|
14
|
+
|
|
15
|
+
// src/island/document.ts
|
|
16
|
+
function isDocumentSource(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && typeof value.describeDocument === "function";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/island/runtime.ts
|
|
21
|
+
var _ioc = require('@foxford/ioc');
|
|
22
|
+
var appContainer = null;
|
|
23
|
+
function getAppContainer() {
|
|
24
|
+
var _a;
|
|
25
|
+
if (appContainer === null) {
|
|
26
|
+
appContainer = new (0, _ioc.Container)();
|
|
27
|
+
((_a = _chunkCZVYVNLIcjs.logger.getLogger("island:runtime")) != null ? _a : _chunkCZVYVNLIcjs.logger).debug("app-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440 \u0441\u043E\u0437\u0434\u0430\u043D");
|
|
28
|
+
}
|
|
29
|
+
return appContainer;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/island/lifecycle.ts
|
|
33
|
+
var _logger = require('@foxford/logger');
|
|
34
|
+
var EMPTY_ROUTE = { params: {}, pathname: "", search: "" };
|
|
35
|
+
var REDIRECT_FOUND = 302;
|
|
36
|
+
function createIslandContainer(descriptor, options = {}) {
|
|
37
|
+
var _a, _b, _c, _d, _e;
|
|
38
|
+
const { parent = getAppContainer() } = options;
|
|
39
|
+
const appLog = (_b = _logger.log.getLogger((_a = descriptor.name) != null ? _a : "app")) != null ? _b : _chunkCZVYVNLIcjs.logger;
|
|
40
|
+
const islandLog = (_c = appLog.getLogger("client")) != null ? _c : appLog;
|
|
41
|
+
try {
|
|
42
|
+
const container = parent.createChild();
|
|
43
|
+
container.bind(_chunkCZVYVNLIcjs.LoggerToken).toValue(islandLog);
|
|
44
|
+
descriptor.viewLayer.register(container);
|
|
45
|
+
const viewModels = (_d = descriptor.viewModels) != null ? _d : [];
|
|
46
|
+
const slots = Object.entries((_e = descriptor.slots) != null ? _e : {});
|
|
47
|
+
islandLog.info(
|
|
48
|
+
`\u043E\u0441\u0442\u0440\u043E\u0432: \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440 \u0441\u043E\u0437\u0434\u0430\u043D (units=${descriptor.units.length}, vms=${viewModels.length}, slots=${slots.length})`
|
|
49
|
+
);
|
|
50
|
+
for (const unit of descriptor.units) {
|
|
51
|
+
unit.register(container);
|
|
52
|
+
}
|
|
53
|
+
for (const vm of viewModels) {
|
|
54
|
+
vm.register(container);
|
|
55
|
+
}
|
|
56
|
+
for (const [name, slot] of slots) {
|
|
57
|
+
slot.register(container, name);
|
|
58
|
+
}
|
|
59
|
+
return container;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
islandLog.error("\u043E\u0441\u0442\u0440\u043E\u0432: \u0441\u0431\u043E\u0440\u043A\u0430 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0430 \u0443\u043F\u0430\u043B\u0430", error);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function hydrateIslandState(container, descriptor, denState) {
|
|
66
|
+
var _a, _b;
|
|
67
|
+
for (const vm of (_a = descriptor.viewModels) != null ? _a : []) {
|
|
68
|
+
const instance = container.resolve(vm.token);
|
|
69
|
+
const saved = denState[vm.token.description];
|
|
70
|
+
if (saved !== void 0 && typeof ((_b = instance.state) == null ? void 0 : _b.set) === "function") {
|
|
71
|
+
_chunkCZVYVNLIcjs.unitLogger.call(void 0, container, vm.token.description).debug("\u0433\u0438\u0434\u0440\u0430\u0446\u0438\u044F VM \u0438\u0437 denState");
|
|
72
|
+
instance.state.set(saved);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function activateIslandViewModels(_0, _1) {
|
|
77
|
+
return _chunkXHYR3SGGcjs.__async.call(void 0, this, arguments, function* (container, descriptor, route = EMPTY_ROUTE) {
|
|
78
|
+
var _a;
|
|
79
|
+
yield Promise.all(
|
|
80
|
+
((_a = descriptor.viewModels) != null ? _a : []).map((vm) => _chunkXHYR3SGGcjs.__async.call(void 0, null, null, function* () {
|
|
81
|
+
var _a2;
|
|
82
|
+
const instance = container.resolve(vm.token);
|
|
83
|
+
_chunkCZVYVNLIcjs.unitLogger.call(void 0, container, vm.token.description).debug(`\u0430\u043A\u0442\u0438\u0432\u0430\u0446\u0438\u044F VM (${route.pathname || "\u2014"})`);
|
|
84
|
+
yield (_a2 = instance.activate) == null ? void 0 : _a2.call(instance, route);
|
|
85
|
+
}))
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function collectIslandState(container, descriptor) {
|
|
90
|
+
var _a, _b;
|
|
91
|
+
const state = {};
|
|
92
|
+
for (const vm of (_a = descriptor.viewModels) != null ? _a : []) {
|
|
93
|
+
const instance = container.resolve(vm.token);
|
|
94
|
+
if (typeof ((_b = instance.state) == null ? void 0 : _b.get) === "function") {
|
|
95
|
+
state[vm.token.description] = instance.state.get();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return state;
|
|
99
|
+
}
|
|
100
|
+
function collectDocumentContribution(container, descriptor) {
|
|
101
|
+
var _a, _b;
|
|
102
|
+
const contribution = {};
|
|
103
|
+
for (const vm of (_a = descriptor.viewModels) != null ? _a : []) {
|
|
104
|
+
const instance = container.resolve(vm.token);
|
|
105
|
+
if (!isDocumentSource(instance)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const part = instance.describeDocument();
|
|
109
|
+
if (part.head) {
|
|
110
|
+
contribution.head = _chunkXHYR3SGGcjs.__spreadValues.call(void 0, _chunkXHYR3SGGcjs.__spreadValues.call(void 0, {}, contribution.head), part.head);
|
|
111
|
+
}
|
|
112
|
+
const declared = part.status !== void 0 || part.redirect !== void 0;
|
|
113
|
+
if (declared && contribution.status === void 0) {
|
|
114
|
+
contribution.status = (_b = part.status) != null ? _b : REDIRECT_FOUND;
|
|
115
|
+
contribution.redirect = part.redirect;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return contribution;
|
|
119
|
+
}
|
|
120
|
+
function resolveEagerUnits(container, descriptor) {
|
|
121
|
+
var _a;
|
|
122
|
+
for (const token of (_a = descriptor.eager) != null ? _a : []) {
|
|
123
|
+
const instance = container.resolve(token);
|
|
124
|
+
_chunkCZVYVNLIcjs.unitLogger.call(void 0, container, token.description).debug("eager: \u0435\u0434\u0438\u043D\u0438\u0446\u0430 \u043F\u043E\u0434\u043D\u044F\u0442\u0430 \u043F\u0440\u0438 \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0438");
|
|
125
|
+
if (instance instanceof Promise) {
|
|
126
|
+
instance.catch(
|
|
127
|
+
(error) => _chunkCZVYVNLIcjs.unitLogger.call(void 0, container, token.description).error("eager: \u0435\u0434\u0438\u043D\u0438\u0446\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0430\u0441\u044C", error)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/island/render.ts
|
|
134
|
+
var STATUS_OK = 200;
|
|
135
|
+
function renderApp(descriptor, request) {
|
|
136
|
+
return _chunkXHYR3SGGcjs.__async.call(void 0, this, null, function* () {
|
|
137
|
+
var _a, _b;
|
|
138
|
+
const route = {
|
|
139
|
+
params: (_a = request.params) != null ? _a : {},
|
|
140
|
+
pathname: request.pathname,
|
|
141
|
+
search: (_b = request.search) != null ? _b : ""
|
|
142
|
+
};
|
|
143
|
+
const container = createIslandContainer(descriptor, { parent: request.parent });
|
|
144
|
+
if (container === null) {
|
|
145
|
+
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`);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
if (request.context) {
|
|
149
|
+
container.bind(_chunkCZVYVNLIcjs.RequestContextToken).toValue(request.context);
|
|
150
|
+
}
|
|
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
|
+
|
|
168
|
+
// src/island/routes.ts
|
|
169
|
+
function normalizeRoutes(routes = []) {
|
|
170
|
+
return routes.map((route) => typeof route === "string" ? { source: route } : route);
|
|
171
|
+
}
|
|
172
|
+
function matchRoute(routes = [], pathname) {
|
|
173
|
+
for (const route of normalizeRoutes(routes)) {
|
|
174
|
+
const found = new RegExp(route.source).exec(pathname);
|
|
175
|
+
if (found !== null) {
|
|
176
|
+
return { params: decodeParams(found.groups), route };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function decodeParams(groups) {
|
|
182
|
+
const params = {};
|
|
183
|
+
for (const [name, value] of Object.entries(groups != null ? groups : {})) {
|
|
184
|
+
if (value === void 0) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
params[name] = decodeURIComponent(value);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
params[name] = value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return params;
|
|
194
|
+
}
|
|
195
|
+
var REGEXP_SPECIALS = /[.*+?^${}()|[\]\\]/g;
|
|
196
|
+
var PARAM_SEGMENT = /\/:([A-Za-z_$][\w$]*)/g;
|
|
197
|
+
function pathPattern(pattern) {
|
|
198
|
+
return `^${pattern.replace(REGEXP_SPECIALS, "\\$&").replace(PARAM_SEGMENT, "/(?<$1>[^/]+)")}$`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
exports.isDocumentSource = isDocumentSource; exports.getAppContainer = getAppContainer; exports.createIslandContainer = createIslandContainer; exports.hydrateIslandState = hydrateIslandState; exports.activateIslandViewModels = activateIslandViewModels; exports.collectIslandState = collectIslandState; exports.collectDocumentContribution = collectDocumentContribution; exports.resolveEagerUnits = resolveEagerUnits; exports.renderApp = renderApp; exports.normalizeRoutes = normalizeRoutes; exports.matchRoute = matchRoute; exports.pathPattern = pathPattern;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveViewAdapter
|
|
3
|
+
} from "./chunk-E23E2VUC.js";
|
|
4
|
+
import {
|
|
5
|
+
LoggerToken,
|
|
6
|
+
RequestContextToken,
|
|
7
|
+
logger,
|
|
8
|
+
unitLogger
|
|
9
|
+
} from "./chunk-KJ6NDOOL.js";
|
|
10
|
+
import {
|
|
11
|
+
__async,
|
|
12
|
+
__spreadValues
|
|
13
|
+
} from "./chunk-HJO26HIQ.js";
|
|
14
|
+
|
|
15
|
+
// src/island/document.ts
|
|
16
|
+
function isDocumentSource(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && typeof value.describeDocument === "function";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/island/runtime.ts
|
|
21
|
+
import { Container } from "@foxford/ioc";
|
|
22
|
+
var appContainer = null;
|
|
23
|
+
function getAppContainer() {
|
|
24
|
+
var _a;
|
|
25
|
+
if (appContainer === null) {
|
|
26
|
+
appContainer = new Container();
|
|
27
|
+
((_a = logger.getLogger("island:runtime")) != null ? _a : logger).debug("app-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440 \u0441\u043E\u0437\u0434\u0430\u043D");
|
|
28
|
+
}
|
|
29
|
+
return appContainer;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/island/lifecycle.ts
|
|
33
|
+
import { log } from "@foxford/logger";
|
|
34
|
+
var EMPTY_ROUTE = { params: {}, pathname: "", search: "" };
|
|
35
|
+
var REDIRECT_FOUND = 302;
|
|
36
|
+
function createIslandContainer(descriptor, options = {}) {
|
|
37
|
+
var _a, _b, _c, _d, _e;
|
|
38
|
+
const { parent = getAppContainer() } = options;
|
|
39
|
+
const appLog = (_b = log.getLogger((_a = descriptor.name) != null ? _a : "app")) != null ? _b : logger;
|
|
40
|
+
const islandLog = (_c = appLog.getLogger("client")) != null ? _c : appLog;
|
|
41
|
+
try {
|
|
42
|
+
const container = parent.createChild();
|
|
43
|
+
container.bind(LoggerToken).toValue(islandLog);
|
|
44
|
+
descriptor.viewLayer.register(container);
|
|
45
|
+
const viewModels = (_d = descriptor.viewModels) != null ? _d : [];
|
|
46
|
+
const slots = Object.entries((_e = descriptor.slots) != null ? _e : {});
|
|
47
|
+
islandLog.info(
|
|
48
|
+
`\u043E\u0441\u0442\u0440\u043E\u0432: \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440 \u0441\u043E\u0437\u0434\u0430\u043D (units=${descriptor.units.length}, vms=${viewModels.length}, slots=${slots.length})`
|
|
49
|
+
);
|
|
50
|
+
for (const unit of descriptor.units) {
|
|
51
|
+
unit.register(container);
|
|
52
|
+
}
|
|
53
|
+
for (const vm of viewModels) {
|
|
54
|
+
vm.register(container);
|
|
55
|
+
}
|
|
56
|
+
for (const [name, slot] of slots) {
|
|
57
|
+
slot.register(container, name);
|
|
58
|
+
}
|
|
59
|
+
return container;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
islandLog.error("\u043E\u0441\u0442\u0440\u043E\u0432: \u0441\u0431\u043E\u0440\u043A\u0430 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0430 \u0443\u043F\u0430\u043B\u0430", error);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function hydrateIslandState(container, descriptor, denState) {
|
|
66
|
+
var _a, _b;
|
|
67
|
+
for (const vm of (_a = descriptor.viewModels) != null ? _a : []) {
|
|
68
|
+
const instance = container.resolve(vm.token);
|
|
69
|
+
const saved = denState[vm.token.description];
|
|
70
|
+
if (saved !== void 0 && typeof ((_b = instance.state) == null ? void 0 : _b.set) === "function") {
|
|
71
|
+
unitLogger(container, vm.token.description).debug("\u0433\u0438\u0434\u0440\u0430\u0446\u0438\u044F VM \u0438\u0437 denState");
|
|
72
|
+
instance.state.set(saved);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function activateIslandViewModels(_0, _1) {
|
|
77
|
+
return __async(this, arguments, function* (container, descriptor, route = EMPTY_ROUTE) {
|
|
78
|
+
var _a;
|
|
79
|
+
yield Promise.all(
|
|
80
|
+
((_a = descriptor.viewModels) != null ? _a : []).map((vm) => __async(null, null, function* () {
|
|
81
|
+
var _a2;
|
|
82
|
+
const instance = container.resolve(vm.token);
|
|
83
|
+
unitLogger(container, vm.token.description).debug(`\u0430\u043A\u0442\u0438\u0432\u0430\u0446\u0438\u044F VM (${route.pathname || "\u2014"})`);
|
|
84
|
+
yield (_a2 = instance.activate) == null ? void 0 : _a2.call(instance, route);
|
|
85
|
+
}))
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function collectIslandState(container, descriptor) {
|
|
90
|
+
var _a, _b;
|
|
91
|
+
const state = {};
|
|
92
|
+
for (const vm of (_a = descriptor.viewModels) != null ? _a : []) {
|
|
93
|
+
const instance = container.resolve(vm.token);
|
|
94
|
+
if (typeof ((_b = instance.state) == null ? void 0 : _b.get) === "function") {
|
|
95
|
+
state[vm.token.description] = instance.state.get();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return state;
|
|
99
|
+
}
|
|
100
|
+
function collectDocumentContribution(container, descriptor) {
|
|
101
|
+
var _a, _b;
|
|
102
|
+
const contribution = {};
|
|
103
|
+
for (const vm of (_a = descriptor.viewModels) != null ? _a : []) {
|
|
104
|
+
const instance = container.resolve(vm.token);
|
|
105
|
+
if (!isDocumentSource(instance)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const part = instance.describeDocument();
|
|
109
|
+
if (part.head) {
|
|
110
|
+
contribution.head = __spreadValues(__spreadValues({}, contribution.head), part.head);
|
|
111
|
+
}
|
|
112
|
+
const declared = part.status !== void 0 || part.redirect !== void 0;
|
|
113
|
+
if (declared && contribution.status === void 0) {
|
|
114
|
+
contribution.status = (_b = part.status) != null ? _b : REDIRECT_FOUND;
|
|
115
|
+
contribution.redirect = part.redirect;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return contribution;
|
|
119
|
+
}
|
|
120
|
+
function resolveEagerUnits(container, descriptor) {
|
|
121
|
+
var _a;
|
|
122
|
+
for (const token of (_a = descriptor.eager) != null ? _a : []) {
|
|
123
|
+
const instance = container.resolve(token);
|
|
124
|
+
unitLogger(container, token.description).debug("eager: \u0435\u0434\u0438\u043D\u0438\u0446\u0430 \u043F\u043E\u0434\u043D\u044F\u0442\u0430 \u043F\u0440\u0438 \u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0438");
|
|
125
|
+
if (instance instanceof Promise) {
|
|
126
|
+
instance.catch(
|
|
127
|
+
(error) => unitLogger(container, token.description).error("eager: \u0435\u0434\u0438\u043D\u0438\u0446\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0430\u0441\u044C", error)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/island/render.ts
|
|
134
|
+
var STATUS_OK = 200;
|
|
135
|
+
function renderApp(descriptor, request) {
|
|
136
|
+
return __async(this, null, function* () {
|
|
137
|
+
var _a, _b;
|
|
138
|
+
const route = {
|
|
139
|
+
params: (_a = request.params) != null ? _a : {},
|
|
140
|
+
pathname: request.pathname,
|
|
141
|
+
search: (_b = request.search) != null ? _b : ""
|
|
142
|
+
};
|
|
143
|
+
const container = createIslandContainer(descriptor, { parent: request.parent });
|
|
144
|
+
if (container === null) {
|
|
145
|
+
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`);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
if (request.context) {
|
|
149
|
+
container.bind(RequestContextToken).toValue(request.context);
|
|
150
|
+
}
|
|
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
|
+
|
|
168
|
+
// src/island/routes.ts
|
|
169
|
+
function normalizeRoutes(routes = []) {
|
|
170
|
+
return routes.map((route) => typeof route === "string" ? { source: route } : route);
|
|
171
|
+
}
|
|
172
|
+
function matchRoute(routes = [], pathname) {
|
|
173
|
+
for (const route of normalizeRoutes(routes)) {
|
|
174
|
+
const found = new RegExp(route.source).exec(pathname);
|
|
175
|
+
if (found !== null) {
|
|
176
|
+
return { params: decodeParams(found.groups), route };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function decodeParams(groups) {
|
|
182
|
+
const params = {};
|
|
183
|
+
for (const [name, value] of Object.entries(groups != null ? groups : {})) {
|
|
184
|
+
if (value === void 0) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
params[name] = decodeURIComponent(value);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
params[name] = value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return params;
|
|
194
|
+
}
|
|
195
|
+
var REGEXP_SPECIALS = /[.*+?^${}()|[\]\\]/g;
|
|
196
|
+
var PARAM_SEGMENT = /\/:([A-Za-z_$][\w$]*)/g;
|
|
197
|
+
function pathPattern(pattern) {
|
|
198
|
+
return `^${pattern.replace(REGEXP_SPECIALS, "\\$&").replace(PARAM_SEGMENT, "/(?<$1>[^/]+)")}$`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export {
|
|
202
|
+
isDocumentSource,
|
|
203
|
+
getAppContainer,
|
|
204
|
+
createIslandContainer,
|
|
205
|
+
hydrateIslandState,
|
|
206
|
+
activateIslandViewModels,
|
|
207
|
+
collectIslandState,
|
|
208
|
+
collectDocumentContribution,
|
|
209
|
+
resolveEagerUnits,
|
|
210
|
+
renderApp,
|
|
211
|
+
normalizeRoutes,
|
|
212
|
+
matchRoute,
|
|
213
|
+
pathPattern
|
|
214
|
+
};
|