@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxford/den",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Den — декларативный метафреймворк Foxford (core runtime)",
5
5
  "keywords": [
6
6
  "foxford",
@@ -19,9 +19,13 @@
19
19
  }
20
20
  ],
21
21
  "type": "module",
22
+ "bin": {
23
+ "den-serve": "./den-serve.js"
24
+ },
22
25
  "dependencies": {
23
- "@foxford/ioc": "^1.1.2",
24
- "@foxford/logger": "^1.2.2"
26
+ "fastify": "^5.11.2",
27
+ "@foxford/logger": "^1.2.2",
28
+ "@foxford/ioc": "^1.1.2"
25
29
  },
26
30
  "main": "./index.cjs",
27
31
  "module": "./index.js",
@@ -45,6 +49,11 @@
45
49
  "import": "./island/index.js",
46
50
  "require": "./island/index.cjs",
47
51
  "types": "./island/index.d.ts"
52
+ },
53
+ "./serve": {
54
+ "import": "./serve/index.js",
55
+ "require": "./serve/index.cjs",
56
+ "types": "./serve/index.d.ts"
48
57
  }
49
58
  },
50
59
  "files": [
@@ -53,14 +62,16 @@
53
62
  "adapter.d.cts",
54
63
  "adapter.d.ts",
55
64
  "adapter.js",
56
- "chunk-5DJZZML5.cjs",
57
- "chunk-A6ZPAM6Z.cjs",
65
+ "chunk-6ZFVV3AV.cjs",
66
+ "chunk-7M5OAOQ4.js",
58
67
  "chunk-C7BM4DGX.cjs",
68
+ "chunk-CZVYVNLI.cjs",
59
69
  "chunk-E23E2VUC.js",
60
70
  "chunk-HJO26HIQ.js",
61
- "chunk-TPBI6TOU.js",
71
+ "chunk-KJ6NDOOL.js",
72
+ "chunk-P4QJCHMM.cjs",
73
+ "chunk-PZVUKTZG.js",
62
74
  "chunk-WBHHHICS.js",
63
- "chunk-WRQC6BVJ.js",
64
75
  "chunk-XHYR3SGG.cjs",
65
76
  "chunk-XOJ2VWX4.cjs",
66
77
  "define-repository-Ca62_YCy.d.ts",
@@ -71,17 +82,23 @@
71
82
  "define.d.cts",
72
83
  "define.d.ts",
73
84
  "define.js",
85
+ "den-serve.js",
86
+ "descriptor-DZDNH7R5.d.ts",
87
+ "descriptor-h0JE9jFx.d.cts",
74
88
  "index.cjs",
75
89
  "index.d.cts",
76
90
  "index.d.ts",
77
91
  "index.js",
78
92
  "island",
79
93
  "package.json",
94
+ "routes-BqIoqbt1.d.cts",
95
+ "routes-CWpIqVAM.d.ts",
96
+ "serve",
80
97
  "types-COwVwgzE.d.cts",
81
98
  "types-COwVwgzE.d.ts",
82
- "view-adapter-BvDC5O4y.d.cts",
83
- "view-adapter-DtNgyjIf.d.ts"
99
+ "view-adapter-CPI8056c.d.ts",
100
+ "view-adapter-D2597RJZ.d.cts"
84
101
  ],
85
- "sha": "ba6123d",
102
+ "sha": "46356bf",
86
103
  "scripts": {}
87
104
  }
@@ -0,0 +1,64 @@
1
+ import { g as GuardDefinition } from './types-COwVwgzE.cjs';
2
+
3
+ /** Маршрут с политикой доступа. */
4
+ interface RouteDeclaration {
5
+ /** Regex-источник адреса (`'^/cabinet/billing'`, `'.*'`). */
6
+ source: string;
7
+ /**
8
+ * Guard-ы этого маршрута — поверх guard-ов приложения и хоста. Пусто — маршрут открыт
9
+ * настолько, насколько открыто само приложение.
10
+ */
11
+ guards?: ReadonlyArray<GuardDefinition>;
12
+ }
13
+ /**
14
+ * Заявка на адрес: голая строка — публичный маршрут, объект — маршрут со своей политикой.
15
+ * Обе формы лежат в одном массиве, и порядок значим так же, как раньше: хост раздаёт путь
16
+ * первому заявившему, поэтому частные записи ставятся выше общих.
17
+ */
18
+ type AppRoute = string | RouteDeclaration;
19
+ /**
20
+ * Приводит заявки к одной форме — чтобы хост и dev-инструменты не разбирали union каждый сам.
21
+ *
22
+ * @param routes - Заявки приложения (строки и/или записи)
23
+ * @returns Записи маршрутов в порядке объявления
24
+ */
25
+ declare function normalizeRoutes(routes?: ReadonlyArray<AppRoute>): RouteDeclaration[];
26
+ /** Совпадение адреса с заявкой: сама запись и выделенные ею параметры. */
27
+ interface RouteMatch {
28
+ route: RouteDeclaration;
29
+ /** Именованные группы совпавшего маршрута; пусто, если их нет. */
30
+ params: Record<string, string>;
31
+ }
32
+ /**
33
+ * Ищет первую заявку, которой принадлежит путь, и выделяет её параметры.
34
+ *
35
+ * Один матчер на всех, потому что совпадений в системе два и они обязаны сходиться: хост решает
36
+ * по нему, кому отдать адрес, а рантайм приложения — своё ли это. Разойдись они, приложение
37
+ * получало бы запросы по путям, которых не заявляло.
38
+ *
39
+ * Параметры — именованные группы регулярки (`(?<slug>[^/]+)`); писать их руками не нужно,
40
+ * для этого есть {@link pathPattern}. Безымянные группы в параметры не попадают: у них нет
41
+ * имени, под которым их ждать.
42
+ *
43
+ * @param routes - Заявки приложения в порядке объявления
44
+ * @param pathname - Путь без search
45
+ * @returns Совпадение либо `null`
46
+ */
47
+ declare function matchRoute(routes: ReadonlyArray<AppRoute> | undefined, pathname: string): RouteMatch | null;
48
+ /**
49
+ * Делает из шаблона адреса источник регулярки с именованными группами:
50
+ * `'/:foo/:bar'` → `'^/(?<foo>[^/]+)/(?<bar>[^/]+)$'`.
51
+ *
52
+ * Отдельным помощником, а не распознаванием `:` прямо в `source`: заявка объявляется
53
+ * регуляркой, и в ней двоеточие — обычный символ. Угадывай мы синтаксис по содержимому
54
+ * строки — законная регулярка однажды молча сменила бы смысл.
55
+ *
56
+ * Шаблон привязан к обоим концам (`^…$`): `'/:slug'` означает один сегмент, а не «путь,
57
+ * который где-то внутри похож на сегмент».
58
+ *
59
+ * @param pattern - Шаблон адреса с сегментами-параметрами
60
+ * @returns Источник для {@link RouteDeclaration.source}
61
+ */
62
+ declare function pathPattern(pattern: string): string;
63
+
64
+ export { type AppRoute as A, type RouteDeclaration as R, type RouteMatch as a, matchRoute as m, normalizeRoutes as n, pathPattern as p };
@@ -0,0 +1,64 @@
1
+ import { g as GuardDefinition } from './types-COwVwgzE.js';
2
+
3
+ /** Маршрут с политикой доступа. */
4
+ interface RouteDeclaration {
5
+ /** Regex-источник адреса (`'^/cabinet/billing'`, `'.*'`). */
6
+ source: string;
7
+ /**
8
+ * Guard-ы этого маршрута — поверх guard-ов приложения и хоста. Пусто — маршрут открыт
9
+ * настолько, насколько открыто само приложение.
10
+ */
11
+ guards?: ReadonlyArray<GuardDefinition>;
12
+ }
13
+ /**
14
+ * Заявка на адрес: голая строка — публичный маршрут, объект — маршрут со своей политикой.
15
+ * Обе формы лежат в одном массиве, и порядок значим так же, как раньше: хост раздаёт путь
16
+ * первому заявившему, поэтому частные записи ставятся выше общих.
17
+ */
18
+ type AppRoute = string | RouteDeclaration;
19
+ /**
20
+ * Приводит заявки к одной форме — чтобы хост и dev-инструменты не разбирали union каждый сам.
21
+ *
22
+ * @param routes - Заявки приложения (строки и/или записи)
23
+ * @returns Записи маршрутов в порядке объявления
24
+ */
25
+ declare function normalizeRoutes(routes?: ReadonlyArray<AppRoute>): RouteDeclaration[];
26
+ /** Совпадение адреса с заявкой: сама запись и выделенные ею параметры. */
27
+ interface RouteMatch {
28
+ route: RouteDeclaration;
29
+ /** Именованные группы совпавшего маршрута; пусто, если их нет. */
30
+ params: Record<string, string>;
31
+ }
32
+ /**
33
+ * Ищет первую заявку, которой принадлежит путь, и выделяет её параметры.
34
+ *
35
+ * Один матчер на всех, потому что совпадений в системе два и они обязаны сходиться: хост решает
36
+ * по нему, кому отдать адрес, а рантайм приложения — своё ли это. Разойдись они, приложение
37
+ * получало бы запросы по путям, которых не заявляло.
38
+ *
39
+ * Параметры — именованные группы регулярки (`(?<slug>[^/]+)`); писать их руками не нужно,
40
+ * для этого есть {@link pathPattern}. Безымянные группы в параметры не попадают: у них нет
41
+ * имени, под которым их ждать.
42
+ *
43
+ * @param routes - Заявки приложения в порядке объявления
44
+ * @param pathname - Путь без search
45
+ * @returns Совпадение либо `null`
46
+ */
47
+ declare function matchRoute(routes: ReadonlyArray<AppRoute> | undefined, pathname: string): RouteMatch | null;
48
+ /**
49
+ * Делает из шаблона адреса источник регулярки с именованными группами:
50
+ * `'/:foo/:bar'` → `'^/(?<foo>[^/]+)/(?<bar>[^/]+)$'`.
51
+ *
52
+ * Отдельным помощником, а не распознаванием `:` прямо в `source`: заявка объявляется
53
+ * регуляркой, и в ней двоеточие — обычный символ. Угадывай мы синтаксис по содержимому
54
+ * строки — законная регулярка однажды молча сменила бы смысл.
55
+ *
56
+ * Шаблон привязан к обоим концам (`^…$`): `'/:slug'` означает один сегмент, а не «путь,
57
+ * который где-то внутри похож на сегмент».
58
+ *
59
+ * @param pattern - Шаблон адреса с сегментами-параметрами
60
+ * @returns Источник для {@link RouteDeclaration.source}
61
+ */
62
+ declare function pathPattern(pattern: string): string;
63
+
64
+ export { type AppRoute as A, type RouteDeclaration as R, type RouteMatch as a, matchRoute as m, normalizeRoutes as n, pathPattern as p };
@@ -0,0 +1,136 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
2
+
3
+
4
+
5
+ var _chunk6ZFVV3AVcjs = require('../chunk-6ZFVV3AV.cjs');
6
+ require('../chunk-XOJ2VWX4.cjs');
7
+
8
+
9
+ var _chunkCZVYVNLIcjs = require('../chunk-CZVYVNLI.cjs');
10
+
11
+
12
+ var _chunkXHYR3SGGcjs = require('../chunk-XHYR3SGG.cjs');
13
+
14
+ // src/serve/index.ts
15
+ var _fastify = require('fastify'); var _fastify2 = _interopRequireDefault(_fastify);
16
+ var _logger = require('@foxford/logger');
17
+
18
+ // src/serve/plugin.ts
19
+
20
+ var DEFAULT_SLOW_RENDER_MS = 1e3;
21
+ function claim(app) {
22
+ return _chunk6ZFVV3AVcjs.normalizeRoutes.call(void 0, app.routes).map((route) => route.source);
23
+ }
24
+ function matchClaim(app, pathname) {
25
+ var _a, _b;
26
+ if (claim(app).length === 0) {
27
+ return {};
28
+ }
29
+ return (_b = (_a = _chunk6ZFVV3AVcjs.matchRoute.call(void 0, app.routes, pathname)) == null ? void 0 : _a.params) != null ? _b : null;
30
+ }
31
+ function parseCookies(header) {
32
+ const cookies = {};
33
+ for (const part of (header != null ? header : "").split(";")) {
34
+ const eq = part.indexOf("=");
35
+ if (eq < 1) {
36
+ continue;
37
+ }
38
+ const name = part.slice(0, eq).trim();
39
+ if (name && !(name in cookies)) {
40
+ cookies[name] = part.slice(eq + 1).trim();
41
+ }
42
+ }
43
+ return cookies;
44
+ }
45
+ function browserContext(req) {
46
+ const headers = {};
47
+ const forwarded = {
48
+ "accept-language": req.headers["x-forwarded-accept-language"],
49
+ host: req.headers["x-forwarded-host"]
50
+ };
51
+ for (const [name, value] of Object.entries(forwarded)) {
52
+ if (typeof value === "string" && value !== "") {
53
+ headers[name] = value;
54
+ }
55
+ }
56
+ const cookie = req.headers.cookie;
57
+ if (typeof cookie === "string" && cookie !== "") {
58
+ headers.cookie = cookie;
59
+ }
60
+ return { cookies: parseCookies(cookie), headers };
61
+ }
62
+ var appPlugin = (instance, options) => {
63
+ var _a, _b;
64
+ const { app, slowRenderMs = DEFAULT_SLOW_RENDER_MS } = options;
65
+ const appLog = (_a = _logger.log.getLogger(app.name)) != null ? _a : _chunkCZVYVNLIcjs.logger;
66
+ const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
67
+ instance.get("/_health", () => "ok");
68
+ instance.get("/_routes", (_req, reply) => {
69
+ void reply.header("cache-control", "no-store");
70
+ return claim(app);
71
+ });
72
+ instance.get("/_render", (req, reply) => _chunkXHYR3SGGcjs.__async.call(void 0, null, null, function* () {
73
+ const target = req.query.url;
74
+ if (typeof target !== "string" || !target.startsWith("/")) {
75
+ return reply.code(400).send({ error: "\u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D \u0438\u043B\u0438 \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 url" });
76
+ }
77
+ const { pathname, search } = new URL(target, "http://localhost");
78
+ const params = matchClaim(app, pathname);
79
+ if (params === null) {
80
+ serveLog.error(`\u043F\u0443\u0442\u044C \xAB${pathname}\xBB \u0432\u043D\u0435 \u0437\u0430\u044F\u0432\u043A\u0438 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F`);
81
+ return reply.code(404).send({ error: `\u043F\u0443\u0442\u044C \xAB${pathname}\xBB \u0432\u043D\u0435 \u0437\u0430\u044F\u0432\u043A\u0438 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F` });
82
+ }
83
+ const startedAt = Date.now();
84
+ try {
85
+ const result = yield _chunk6ZFVV3AVcjs.renderApp.call(void 0, app, { context: browserContext(req), params, pathname, search });
86
+ const elapsed = Date.now() - startedAt;
87
+ if (elapsed >= slowRenderMs) {
88
+ serveLog.warn(`\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u044B\u0439 \u0440\u0435\u043D\u0434\u0435\u0440 \xAB${pathname}\xBB: ${String(elapsed)}\u043C\u0441`);
89
+ }
90
+ return result;
91
+ } catch (error) {
92
+ serveLog.error(`\u0440\u0435\u043D\u0434\u0435\u0440 \xAB${pathname}\xBB \u0443\u043F\u0430\u043B \u0437\u0430 ${String(Date.now() - startedAt)}\u043C\u0441`, error);
93
+ return reply.code(500).send({ error: error instanceof Error ? error.message : String(error) });
94
+ }
95
+ }));
96
+ return Promise.resolve();
97
+ };
98
+
99
+ // src/serve/index.ts
100
+ var SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
101
+ function serveApp(_0) {
102
+ return _chunkXHYR3SGGcjs.__async.call(void 0, this, arguments, function* (app, options = {}) {
103
+ var _a, _b, _c, _d;
104
+ const appLog = (_a = _logger.log.getLogger(app.name)) != null ? _a : _chunkCZVYVNLIcjs.logger;
105
+ const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
106
+ const instance = _fastify2.default.call(void 0, { logger: false });
107
+ yield instance.register(appPlugin, { app, slowRenderMs: options.slowRenderMs });
108
+ instance.addHook("onRequest", (req, _reply, done) => {
109
+ serveLog.debug(`\u2192 ${req.method} ${req.url}`);
110
+ done();
111
+ });
112
+ instance.addHook("onResponse", (req, reply, done) => {
113
+ const elapsed = reply.elapsedTime.toFixed(1);
114
+ serveLog.info(`\u2190 ${String(reply.statusCode)} ${req.method} ${req.url} ${elapsed}\u043C\u0441`);
115
+ done();
116
+ });
117
+ yield (_c = options.configure) == null ? void 0 : _c.call(options, instance);
118
+ yield instance.listen({ host: options.host, port: (_d = options.port) != null ? _d : 0 });
119
+ if (options.gracefulShutdown !== false) {
120
+ for (const signal of SHUTDOWN_SIGNALS) {
121
+ process.once(signal, () => {
122
+ serveLog.info(`${signal}: \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u0438\u043D\u0441\u0442\u0430\u043D\u0441`);
123
+ void instance.close();
124
+ });
125
+ }
126
+ }
127
+ serveLog.info(
128
+ `\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \xAB${app.name}\xBB \u0441\u043B\u0443\u0448\u0430\u0435\u0442 ${instance.addresses().map((a) => String(a.port)).join(", ")}`
129
+ );
130
+ return instance;
131
+ });
132
+ }
133
+
134
+
135
+
136
+ exports.appPlugin = appPlugin; exports.serveApp = serveApp;
@@ -0,0 +1,60 @@
1
+ import { I as IslandDescriptor } from '../descriptor-h0JE9jFx.cjs';
2
+ import { A as AppRoute } from '../routes-BqIoqbt1.cjs';
3
+ import { FastifyPluginAsync, FastifyInstance } from 'fastify';
4
+ import '../define-slot-BKvArA02.cjs';
5
+ import '../types-COwVwgzE.cjs';
6
+ import '@foxford/ioc';
7
+
8
+ /** Приложение, которое можно поднять сервисом: остров плюс его заявка на адреса. */
9
+ type ServableApp = IslandDescriptor & {
10
+ routes?: ReadonlyArray<AppRoute>;
11
+ };
12
+ /** Настройки плагина. */
13
+ interface AppPluginOptions {
14
+ app: ServableApp;
15
+ /** Порог, выше которого рендер считается медленным и уходит в `warn`, мс. */
16
+ slowRenderMs?: number;
17
+ }
18
+ /**
19
+ * Регистрирует ручки приложения на инстансе fastify: `/_render`, `/_routes`, `/_health`.
20
+ *
21
+ * Плагином, поэтому монтируется и в свой сервер, и в чужой — под префиксом, рядом с ручками
22
+ * потребителя, вместе с его плагинами.
23
+ *
24
+ * @param instance - Инстанс fastify
25
+ * @param options - Приложение и порог медленного рендера
26
+ */
27
+ declare const appPlugin: FastifyPluginAsync<AppPluginOptions>;
28
+
29
+ /** Настройки инстанса. */
30
+ interface ServeAppOptions {
31
+ /** Порт; `0` — свободный, фактический смотри в `server.addresses()`. */
32
+ port?: number;
33
+ /** Интерфейс прослушивания. */
34
+ host?: string;
35
+ /** Порог, выше которого рендер считается медленным и уходит в `warn`, мс. */
36
+ slowRenderMs?: number;
37
+ /**
38
+ * Расширение инстанса ДО прослушивания: сюда потребитель регистрирует свои плагины.
39
+ * Вызывается после ручек приложения, поэтому хуки видят и их.
40
+ */
41
+ configure?: (instance: FastifyInstance) => void | Promise<void>;
42
+ /**
43
+ * Закрывать инстанс по `SIGTERM`/`SIGINT`. По умолчанию да: сервис живёт в оркестраторе,
44
+ * который именно так и просит его уйти, а незакрытый инстанс рвёт запросы на середине.
45
+ */
46
+ gracefulShutdown?: boolean;
47
+ }
48
+ /**
49
+ * Поднимает приложение отдельным сервисом.
50
+ *
51
+ * Сахар над {@link appPlugin}: если сервер нужен свой (общий порт с чужими ручками, свой
52
+ * порядок плагинов), бери плагин и регистрируй сам.
53
+ *
54
+ * @param app - Дескриптор приложения вместе с его заявкой на адреса
55
+ * @param options - Порт, расширение инстанса, поведение при остановке
56
+ * @returns Слушающий инстанс fastify
57
+ */
58
+ declare function serveApp(app: ServableApp, options?: ServeAppOptions): Promise<FastifyInstance>;
59
+
60
+ export { type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, serveApp };
@@ -0,0 +1,60 @@
1
+ import { I as IslandDescriptor } from '../descriptor-DZDNH7R5.js';
2
+ import { A as AppRoute } from '../routes-CWpIqVAM.js';
3
+ import { FastifyPluginAsync, FastifyInstance } from 'fastify';
4
+ import '../define-slot-Dil8Kr1A.js';
5
+ import '../types-COwVwgzE.js';
6
+ import '@foxford/ioc';
7
+
8
+ /** Приложение, которое можно поднять сервисом: остров плюс его заявка на адреса. */
9
+ type ServableApp = IslandDescriptor & {
10
+ routes?: ReadonlyArray<AppRoute>;
11
+ };
12
+ /** Настройки плагина. */
13
+ interface AppPluginOptions {
14
+ app: ServableApp;
15
+ /** Порог, выше которого рендер считается медленным и уходит в `warn`, мс. */
16
+ slowRenderMs?: number;
17
+ }
18
+ /**
19
+ * Регистрирует ручки приложения на инстансе fastify: `/_render`, `/_routes`, `/_health`.
20
+ *
21
+ * Плагином, поэтому монтируется и в свой сервер, и в чужой — под префиксом, рядом с ручками
22
+ * потребителя, вместе с его плагинами.
23
+ *
24
+ * @param instance - Инстанс fastify
25
+ * @param options - Приложение и порог медленного рендера
26
+ */
27
+ declare const appPlugin: FastifyPluginAsync<AppPluginOptions>;
28
+
29
+ /** Настройки инстанса. */
30
+ interface ServeAppOptions {
31
+ /** Порт; `0` — свободный, фактический смотри в `server.addresses()`. */
32
+ port?: number;
33
+ /** Интерфейс прослушивания. */
34
+ host?: string;
35
+ /** Порог, выше которого рендер считается медленным и уходит в `warn`, мс. */
36
+ slowRenderMs?: number;
37
+ /**
38
+ * Расширение инстанса ДО прослушивания: сюда потребитель регистрирует свои плагины.
39
+ * Вызывается после ручек приложения, поэтому хуки видят и их.
40
+ */
41
+ configure?: (instance: FastifyInstance) => void | Promise<void>;
42
+ /**
43
+ * Закрывать инстанс по `SIGTERM`/`SIGINT`. По умолчанию да: сервис живёт в оркестраторе,
44
+ * который именно так и просит его уйти, а незакрытый инстанс рвёт запросы на середине.
45
+ */
46
+ gracefulShutdown?: boolean;
47
+ }
48
+ /**
49
+ * Поднимает приложение отдельным сервисом.
50
+ *
51
+ * Сахар над {@link appPlugin}: если сервер нужен свой (общий порт с чужими ручками, свой
52
+ * порядок плагинов), бери плагин и регистрируй сам.
53
+ *
54
+ * @param app - Дескриптор приложения вместе с его заявкой на адреса
55
+ * @param options - Порт, расширение инстанса, поведение при остановке
56
+ * @returns Слушающий инстанс fastify
57
+ */
58
+ declare function serveApp(app: ServableApp, options?: ServeAppOptions): Promise<FastifyInstance>;
59
+
60
+ export { type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, serveApp };
package/serve/index.js ADDED
@@ -0,0 +1,136 @@
1
+ import {
2
+ matchRoute,
3
+ normalizeRoutes,
4
+ renderApp
5
+ } from "../chunk-7M5OAOQ4.js";
6
+ import "../chunk-E23E2VUC.js";
7
+ import {
8
+ logger
9
+ } from "../chunk-KJ6NDOOL.js";
10
+ import {
11
+ __async
12
+ } from "../chunk-HJO26HIQ.js";
13
+
14
+ // src/serve/index.ts
15
+ import Fastify from "fastify";
16
+ import { log as log2 } from "@foxford/logger";
17
+
18
+ // src/serve/plugin.ts
19
+ import { log } from "@foxford/logger";
20
+ var DEFAULT_SLOW_RENDER_MS = 1e3;
21
+ function claim(app) {
22
+ return normalizeRoutes(app.routes).map((route) => route.source);
23
+ }
24
+ function matchClaim(app, pathname) {
25
+ var _a, _b;
26
+ if (claim(app).length === 0) {
27
+ return {};
28
+ }
29
+ return (_b = (_a = matchRoute(app.routes, pathname)) == null ? void 0 : _a.params) != null ? _b : null;
30
+ }
31
+ function parseCookies(header) {
32
+ const cookies = {};
33
+ for (const part of (header != null ? header : "").split(";")) {
34
+ const eq = part.indexOf("=");
35
+ if (eq < 1) {
36
+ continue;
37
+ }
38
+ const name = part.slice(0, eq).trim();
39
+ if (name && !(name in cookies)) {
40
+ cookies[name] = part.slice(eq + 1).trim();
41
+ }
42
+ }
43
+ return cookies;
44
+ }
45
+ function browserContext(req) {
46
+ const headers = {};
47
+ const forwarded = {
48
+ "accept-language": req.headers["x-forwarded-accept-language"],
49
+ host: req.headers["x-forwarded-host"]
50
+ };
51
+ for (const [name, value] of Object.entries(forwarded)) {
52
+ if (typeof value === "string" && value !== "") {
53
+ headers[name] = value;
54
+ }
55
+ }
56
+ const cookie = req.headers.cookie;
57
+ if (typeof cookie === "string" && cookie !== "") {
58
+ headers.cookie = cookie;
59
+ }
60
+ return { cookies: parseCookies(cookie), headers };
61
+ }
62
+ var appPlugin = (instance, options) => {
63
+ var _a, _b;
64
+ const { app, slowRenderMs = DEFAULT_SLOW_RENDER_MS } = options;
65
+ const appLog = (_a = log.getLogger(app.name)) != null ? _a : logger;
66
+ const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
67
+ instance.get("/_health", () => "ok");
68
+ instance.get("/_routes", (_req, reply) => {
69
+ void reply.header("cache-control", "no-store");
70
+ return claim(app);
71
+ });
72
+ instance.get("/_render", (req, reply) => __async(null, null, function* () {
73
+ const target = req.query.url;
74
+ if (typeof target !== "string" || !target.startsWith("/")) {
75
+ return reply.code(400).send({ error: "\u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043D \u0438\u043B\u0438 \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 url" });
76
+ }
77
+ const { pathname, search } = new URL(target, "http://localhost");
78
+ const params = matchClaim(app, pathname);
79
+ if (params === null) {
80
+ serveLog.error(`\u043F\u0443\u0442\u044C \xAB${pathname}\xBB \u0432\u043D\u0435 \u0437\u0430\u044F\u0432\u043A\u0438 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F`);
81
+ return reply.code(404).send({ error: `\u043F\u0443\u0442\u044C \xAB${pathname}\xBB \u0432\u043D\u0435 \u0437\u0430\u044F\u0432\u043A\u0438 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F` });
82
+ }
83
+ const startedAt = Date.now();
84
+ try {
85
+ const result = yield renderApp(app, { context: browserContext(req), params, pathname, search });
86
+ const elapsed = Date.now() - startedAt;
87
+ if (elapsed >= slowRenderMs) {
88
+ serveLog.warn(`\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u044B\u0439 \u0440\u0435\u043D\u0434\u0435\u0440 \xAB${pathname}\xBB: ${String(elapsed)}\u043C\u0441`);
89
+ }
90
+ return result;
91
+ } catch (error) {
92
+ serveLog.error(`\u0440\u0435\u043D\u0434\u0435\u0440 \xAB${pathname}\xBB \u0443\u043F\u0430\u043B \u0437\u0430 ${String(Date.now() - startedAt)}\u043C\u0441`, error);
93
+ return reply.code(500).send({ error: error instanceof Error ? error.message : String(error) });
94
+ }
95
+ }));
96
+ return Promise.resolve();
97
+ };
98
+
99
+ // src/serve/index.ts
100
+ var SHUTDOWN_SIGNALS = ["SIGTERM", "SIGINT"];
101
+ function serveApp(_0) {
102
+ return __async(this, arguments, function* (app, options = {}) {
103
+ var _a, _b, _c, _d;
104
+ const appLog = (_a = log2.getLogger(app.name)) != null ? _a : logger;
105
+ const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
106
+ const instance = Fastify({ logger: false });
107
+ yield instance.register(appPlugin, { app, slowRenderMs: options.slowRenderMs });
108
+ instance.addHook("onRequest", (req, _reply, done) => {
109
+ serveLog.debug(`\u2192 ${req.method} ${req.url}`);
110
+ done();
111
+ });
112
+ instance.addHook("onResponse", (req, reply, done) => {
113
+ const elapsed = reply.elapsedTime.toFixed(1);
114
+ serveLog.info(`\u2190 ${String(reply.statusCode)} ${req.method} ${req.url} ${elapsed}\u043C\u0441`);
115
+ done();
116
+ });
117
+ yield (_c = options.configure) == null ? void 0 : _c.call(options, instance);
118
+ yield instance.listen({ host: options.host, port: (_d = options.port) != null ? _d : 0 });
119
+ if (options.gracefulShutdown !== false) {
120
+ for (const signal of SHUTDOWN_SIGNALS) {
121
+ process.once(signal, () => {
122
+ serveLog.info(`${signal}: \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u0438\u043D\u0441\u0442\u0430\u043D\u0441`);
123
+ void instance.close();
124
+ });
125
+ }
126
+ }
127
+ serveLog.info(
128
+ `\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \xAB${app.name}\xBB \u0441\u043B\u0443\u0448\u0430\u0435\u0442 ${instance.addresses().map((a) => String(a.port)).join(", ")}`
129
+ );
130
+ return instance;
131
+ });
132
+ }
133
+ export {
134
+ appPlugin,
135
+ serveApp
136
+ };