@foxford/den 3.1.4 → 3.2.1

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/bin/den.cjs CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
 
4
- var _chunkZBB5SBEVcjs = require('../chunk-ZBB5SBEV.cjs');
4
+ var _chunkUB3GCX73cjs = require('../chunk-UB3GCX73.cjs');
5
+ require('../chunk-KQAB6C6Q.cjs');
5
6
  require('../chunk-A6ZPAM6Z.cjs');
6
7
 
7
8
 
@@ -69,11 +70,11 @@ function run(argv) {
69
70
  }
70
71
  const target = { base: options["base"], root: (_a = options["root"]) != null ? _a : process.cwd() };
71
72
  if (command === "build") {
72
- yield _chunkZBB5SBEVcjs.buildApp.call(void 0, target);
73
+ yield _chunkUB3GCX73cjs.buildApp.call(void 0, target);
73
74
  return;
74
75
  }
75
76
  if (command === "dev") {
76
- yield _chunkZBB5SBEVcjs.devApp.call(void 0, target);
77
+ yield _chunkUB3GCX73cjs.devApp.call(void 0, target);
77
78
  return;
78
79
  }
79
80
  fail(`\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \xAB${command}\xBB`);
package/bin/den.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  buildApp,
3
3
  devApp
4
- } from "../chunk-YECHY3NR.js";
4
+ } from "../chunk-URSWYSZF.js";
5
+ import "../chunk-RG7ZV5BD.js";
5
6
  import "../chunk-TPBI6TOU.js";
6
7
  import {
7
8
  __async
package/builder/index.cjs CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkZBB5SBEVcjs = require('../chunk-ZBB5SBEV.cjs');
5
+ var _chunkUB3GCX73cjs = require('../chunk-UB3GCX73.cjs');
6
+ require('../chunk-KQAB6C6Q.cjs');
6
7
  require('../chunk-A6ZPAM6Z.cjs');
7
8
  require('../chunk-T5QVCXVB.cjs');
8
9
 
9
10
 
10
11
 
11
12
 
12
- exports.buildApp = _chunkZBB5SBEVcjs.buildApp; exports.defineAppConfig = _chunkZBB5SBEVcjs.defineAppConfig; exports.devApp = _chunkZBB5SBEVcjs.devApp;
13
+ exports.buildApp = _chunkUB3GCX73cjs.buildApp; exports.defineAppConfig = _chunkUB3GCX73cjs.defineAppConfig; exports.devApp = _chunkUB3GCX73cjs.devApp;
@@ -53,6 +53,14 @@ interface AppServerConfig {
53
53
  * по parent-chain на любом рендере.
54
54
  */
55
55
  container?: (container: Container) => void;
56
+ /**
57
+ * Порт процесса приложения.
58
+ *
59
+ * `PORT` в окружении важнее: порт назначает тот, кто процесс поднимает, — в проде оркестратор.
60
+ * Объявление здесь — умолчание самого приложения, чтобы адрес не приходилось повторять в каждой
61
+ * команде запуска и в префиксе артефактов.
62
+ */
63
+ port?: number;
56
64
  /**
57
65
  * Расширение сетевого инстанса до начала прослушивания: Sentry, метрики, cors.
58
66
  *
@@ -99,18 +107,30 @@ type BuildMode = 'production' | 'development';
99
107
  interface BuildAppOptions {
100
108
  /** Корень приложения. */
101
109
  root: string;
102
- /** Префикс адресов артефактов: в проде CDN, в разработке развязка у хоста. */
110
+ /**
111
+ * Префикс адресов артефактов: в проде CDN.
112
+ *
113
+ * Не задан — в разработке подставляется адрес самого приложения (артефакты отдаёт оно),
114
+ * в проде корень.
115
+ */
103
116
  base?: string;
104
117
  /** Режим сборки; по умолчанию `production`. */
105
118
  mode?: BuildMode;
106
119
  }
120
+ /** Чем сборка закончилась — то, что нужно знать поднимающему процесс. */
121
+ interface AppBuildResult {
122
+ /** Префикс адресов артефактов, с которым собран клиентский выход. */
123
+ base: string;
124
+ /** Порт, который процесс будет слушать: окружение, затем умолчание приложения. */
125
+ port: number;
126
+ }
107
127
  /**
108
128
  * Собирает приложение целиком.
109
129
  *
110
130
  * @param options - Корень приложения и префикс адресов артефактов
111
131
  * @throws Если конфига нет либо пресет сообщил, что ему не хватает объявленного
112
132
  */
113
- declare function buildApp(options: BuildAppOptions): Promise<void>;
133
+ declare function buildApp(options: BuildAppOptions): Promise<AppBuildResult>;
114
134
 
115
135
  /** Наблюдение берёт те же опции, что сборка: путь у них один. */
116
136
  type DevAppOptions = BuildAppOptions;
@@ -53,6 +53,14 @@ interface AppServerConfig {
53
53
  * по parent-chain на любом рендере.
54
54
  */
55
55
  container?: (container: Container) => void;
56
+ /**
57
+ * Порт процесса приложения.
58
+ *
59
+ * `PORT` в окружении важнее: порт назначает тот, кто процесс поднимает, — в проде оркестратор.
60
+ * Объявление здесь — умолчание самого приложения, чтобы адрес не приходилось повторять в каждой
61
+ * команде запуска и в префиксе артефактов.
62
+ */
63
+ port?: number;
56
64
  /**
57
65
  * Расширение сетевого инстанса до начала прослушивания: Sentry, метрики, cors.
58
66
  *
@@ -99,18 +107,30 @@ type BuildMode = 'production' | 'development';
99
107
  interface BuildAppOptions {
100
108
  /** Корень приложения. */
101
109
  root: string;
102
- /** Префикс адресов артефактов: в проде CDN, в разработке развязка у хоста. */
110
+ /**
111
+ * Префикс адресов артефактов: в проде CDN.
112
+ *
113
+ * Не задан — в разработке подставляется адрес самого приложения (артефакты отдаёт оно),
114
+ * в проде корень.
115
+ */
103
116
  base?: string;
104
117
  /** Режим сборки; по умолчанию `production`. */
105
118
  mode?: BuildMode;
106
119
  }
120
+ /** Чем сборка закончилась — то, что нужно знать поднимающему процесс. */
121
+ interface AppBuildResult {
122
+ /** Префикс адресов артефактов, с которым собран клиентский выход. */
123
+ base: string;
124
+ /** Порт, который процесс будет слушать: окружение, затем умолчание приложения. */
125
+ port: number;
126
+ }
107
127
  /**
108
128
  * Собирает приложение целиком.
109
129
  *
110
130
  * @param options - Корень приложения и префикс адресов артефактов
111
131
  * @throws Если конфига нет либо пресет сообщил, что ему не хватает объявленного
112
132
  */
113
- declare function buildApp(options: BuildAppOptions): Promise<void>;
133
+ declare function buildApp(options: BuildAppOptions): Promise<AppBuildResult>;
114
134
 
115
135
  /** Наблюдение берёт те же опции, что сборка: путь у них один. */
116
136
  type DevAppOptions = BuildAppOptions;
package/builder/index.js CHANGED
@@ -2,7 +2,8 @@ import {
2
2
  buildApp,
3
3
  defineAppConfig,
4
4
  devApp
5
- } from "../chunk-YECHY3NR.js";
5
+ } from "../chunk-URSWYSZF.js";
6
+ import "../chunk-RG7ZV5BD.js";
6
7
  import "../chunk-TPBI6TOU.js";
7
8
  import "../chunk-6FNC3XMI.js";
8
9
  export {
@@ -0,0 +1,15 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }// src/serve/env.ts
2
+ var _fs = require('fs'); var _fs2 = _interopRequireDefault(_fs);
3
+ var _path = require('path'); var _path2 = _interopRequireDefault(_path);
4
+ var ENV_FILE = ".env";
5
+ function loadAppEnv(root) {
6
+ const file = _path2.default.join(root, ENV_FILE);
7
+ if (!_fs2.default.existsSync(file)) {
8
+ return;
9
+ }
10
+ process.loadEnvFile(file);
11
+ }
12
+
13
+
14
+
15
+ exports.loadAppEnv = loadAppEnv;
@@ -0,0 +1,15 @@
1
+ // src/serve/env.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ var ENV_FILE = ".env";
5
+ function loadAppEnv(root) {
6
+ const file = path.join(root, ENV_FILE);
7
+ if (!fs.existsSync(file)) {
8
+ return;
9
+ }
10
+ process.loadEnvFile(file);
11
+ }
12
+
13
+ export {
14
+ loadAppEnv
15
+ };
@@ -1,5 +1,8 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
2
2
 
3
+ var _chunkKQAB6C6Qcjs = require('./chunk-KQAB6C6Q.cjs');
4
+
5
+
3
6
  var _chunkA6ZPAM6Zcjs = require('./chunk-A6ZPAM6Z.cjs');
4
7
 
5
8
 
@@ -35,7 +38,7 @@ function collectAssets(outDir, base) {
35
38
  const css = [];
36
39
  const js = [];
37
40
  const seen = /* @__PURE__ */ new Set();
38
- const prefix = base.endsWith("/") ? base : `${base}/`;
41
+ const prefix = base === "" ? "" : base.endsWith("/") ? base : `${base}/`;
39
42
  function walk(key) {
40
43
  var _a, _b;
41
44
  if (seen.has(key)) {
@@ -64,6 +67,10 @@ function collectAssets(outDir, base) {
64
67
 
65
68
  // src/serve/port.ts
66
69
  var DEFAULT_PORT = 3e3;
70
+ function resolvePort(declared) {
71
+ var _a, _b;
72
+ return Number((_b = (_a = process.env["PORT"]) != null ? _a : declared) != null ? _b : DEFAULT_PORT);
73
+ }
67
74
 
68
75
  // src/builder/entries.ts
69
76
  var VIRTUAL_CLIENT = "virtual:den/client";
@@ -102,27 +109,34 @@ if (server.render) {
102
109
  export default descriptor
103
110
  `;
104
111
  }
105
- function generateServerProcess(configPath) {
106
- return `import { serveApp } from '@foxford/den/serve'
112
+ function generateServerProcess(configPath, base, mode) {
113
+ const assets = mode === "development" ? ` assets: { base: ${JSON.stringify(base)}, dir: fileURLToPath(new URL('./client', import.meta.url)) },
114
+ ` : "";
115
+ return `import { fileURLToPath } from 'node:url'
116
+
117
+ import { loadAppEnv, serveApp } from '@foxford/den/serve'
107
118
 
108
119
  import app from ${JSON.stringify(VIRTUAL_SERVER_APP)}
109
120
  import config from ${JSON.stringify(configPath)}
110
121
 
122
+ // \u0423\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044F \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u2014 \u0434\u043E \u043F\u0435\u0440\u0432\u043E\u0433\u043E \u0447\u0442\u0435\u043D\u0438\u044F: \u043F\u043E\u0440\u0442 \u0438 \u0430\u0434\u0440\u0435\u0441\u0430 \u0447\u0438\u0442\u0430\u044E\u0442\u0441\u044F \u043D\u0438\u0436\u0435
123
+ loadAppEnv(fileURLToPath(new URL('..', import.meta.url)))
124
+
111
125
  await serveApp(app, {
112
- configure: config.server?.configure,
126
+ ${assets} configure: config.server?.configure,
113
127
  host: process.env.HOST,
114
- port: Number(process.env.PORT ?? ${String(DEFAULT_PORT)}),
128
+ port: Number(process.env.PORT ?? config.server?.port ?? ${String(DEFAULT_PORT)}),
115
129
  })
116
130
  `;
117
131
  }
118
132
 
119
133
  // src/builder/plugin.ts
120
134
  function denAppModules(options) {
121
- const { assets, config, configPath, entry } = options;
135
+ const { assets, base, config, configPath, entry, mode } = options;
122
136
  const sources = {
123
137
  [VIRTUAL_CLIENT]: () => generateClientEntry(entry, config.preset.clientEntry),
124
138
  [VIRTUAL_SERVER_APP]: () => generateServerApp(entry, configPath, assets),
125
- [VIRTUAL_SERVER_PROCESS]: () => generateServerProcess(configPath)
139
+ [VIRTUAL_SERVER_PROCESS]: () => generateServerProcess(configPath, base, mode)
126
140
  };
127
141
  return {
128
142
  load: (id) => {
@@ -184,7 +198,9 @@ function viteConfigFor(options) {
184
198
  const { assets, base, config, configPath, entryFileNames, input, mode, outDir, root, target } = options;
185
199
  const entry = (_a = config.entry) != null ? _a : "src/index.ts";
186
200
  const composed = {
187
- base,
201
+ // Сборщику относительный префикс, если своего нет: ленивые чанки тогда грузятся от адреса
202
+ // уже загруженного чанка, и подмена CDN уводит за собой и их
203
+ base: base === "" ? "./" : base,
188
204
  build: {
189
205
  // Клиентский выход чистит свой каталог сам, серверные — нет: они пишут рядом с уже
190
206
  // собранным клиентским, и очистка снесла бы его вместе с манифестом
@@ -202,7 +218,10 @@ function viteConfigFor(options) {
202
218
  // Режим объявляем явно: плагины сборщика выбирают по нему рантайм фреймворка, а через
203
219
  // программный вызов он иначе не доедет.
204
220
  mode,
205
- plugins: [...config.preset.plugins, denAppModules({ assets, config, configPath, entry: _path2.default.join(root, entry) })],
221
+ plugins: [
222
+ ...config.preset.plugins,
223
+ denAppModules({ assets, base, config, configPath, entry: _path2.default.join(root, entry), mode })
224
+ ],
206
225
  root,
207
226
  ssr: { noExternal: (_c = (_b = config.server) == null ? void 0 : _b.noExternal) != null ? _c : [] }
208
227
  };
@@ -210,12 +229,15 @@ function viteConfigFor(options) {
210
229
  }
211
230
  function buildApp(options) {
212
231
  return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
213
- var _a, _b;
214
- const { base = "/", mode = "production" } = options;
232
+ var _a, _b, _c, _d;
233
+ const { mode = "production" } = options;
215
234
  process.env["NODE_ENV"] = mode;
216
235
  const root = _path2.default.resolve(options.root);
236
+ _chunkKQAB6C6Qcjs.loadAppEnv.call(void 0, root);
217
237
  const { config, configPath } = yield loadAppConfig(root);
218
- const refusal = (_b = (_a = config.preset).validate) == null ? void 0 : _b.call(_a, dependenciesOf(root));
238
+ const port = resolvePort((_a = config.server) == null ? void 0 : _a.port);
239
+ const base = (_b = options.base) != null ? _b : "";
240
+ const refusal = (_d = (_c = config.preset).validate) == null ? void 0 : _d.call(_c, dependenciesOf(root));
219
241
  if (refusal !== void 0 && refusal !== null) {
220
242
  throw new Error(`den: ${refusal}`);
221
243
  }
@@ -246,11 +268,11 @@ function buildApp(options) {
246
268
  target: "server"
247
269
  }))
248
270
  );
271
+ return { base, port };
249
272
  });
250
273
  }
251
274
 
252
275
  // src/builder/dev.ts
253
- var _child_process = require('child_process');
254
276
 
255
277
 
256
278
  var _url = require('url');
@@ -284,7 +306,7 @@ function assetsSummary(assets) {
284
306
  }
285
307
  function bannerLines(facts2) {
286
308
  var _a;
287
- const port = (_a = process.env["PORT"]) != null ? _a : String(DEFAULT_PORT);
309
+ const port = String((_a = facts2.port) != null ? _a : "");
288
310
  const network = networkAddress();
289
311
  const label = (title) => ` \u2192 ${title.padEnd(PAD)}`;
290
312
  return [
@@ -296,7 +318,10 @@ function bannerLines(facts2) {
296
318
  `${label("\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E")}http://localhost:${port}`,
297
319
  ...network === void 0 ? [] : [`${label("\u0432 \u0441\u0435\u0442\u0438")}http://${network}:${port}`],
298
320
  `${label("\u0440\u0443\u0447\u043A\u0438")}${ENDPOINTS.join(" ")}`,
299
- `${label("\u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B")}${assetsSummary(facts2.assets)}`,
321
+ // Префикс печатается, только когда он есть: при относительной сборке артефакты идут с
322
+ // адреса самого приложения, а он уже назван строкой выше
323
+ ...facts2.base === void 0 || facts2.base === "" ? [] : [`${label("\u043F\u0440\u0435\u0444\u0438\u043A\u0441")}${facts2.base}`],
324
+ `${label("\u0447\u0430\u043D\u043A\u0438")}${assetsSummary(facts2.assets)}`,
300
325
  `${label("\u0432\u044B\u0445\u043E\u0434\u044B")}${OUTPUTS.join(", ")}`,
301
326
  `${label("\u043F\u0440\u0430\u0432\u043A\u0438")}${facts2.watched.join(", ")}`,
302
327
  `${label("\u043A\u043B\u0430\u0432\u0438\u0448\u0438")}${SHORTCUTS.map(({ key, title }) => `${key} \u2014 ${title}`).join(", ")}`,
@@ -307,6 +332,63 @@ function helpLines() {
307
332
  return ["", " \u043A\u043B\u0430\u0432\u0438\u0448\u0430 + enter:", ...SHORTCUTS.map(({ key, title }) => ` ${key} ${title}`), ""];
308
333
  }
309
334
 
335
+ // src/builder/process.ts
336
+ var _child_process = require('child_process');
337
+ var SHUTDOWN_TIMEOUT_MS = 5e3;
338
+ function signalGroup(pid, signal) {
339
+ try {
340
+ process.kill(-pid, signal);
341
+ } catch (e) {
342
+ }
343
+ }
344
+ function appProcess(entry) {
345
+ let child = null;
346
+ function stop() {
347
+ return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
348
+ const running = child;
349
+ child = null;
350
+ if (!running || running.exitCode !== null || running.pid === void 0) {
351
+ return;
352
+ }
353
+ const { pid } = running;
354
+ yield new Promise((resolve) => {
355
+ const forced = setTimeout(() => signalGroup(pid, "SIGKILL"), SHUTDOWN_TIMEOUT_MS);
356
+ running.once("exit", () => {
357
+ clearTimeout(forced);
358
+ resolve();
359
+ });
360
+ signalGroup(pid, "SIGTERM");
361
+ });
362
+ });
363
+ }
364
+ function start(env) {
365
+ const started = _child_process.spawn.call(void 0, process.execPath, [...process.execArgv, entry], {
366
+ detached: true,
367
+ env: _chunkT5QVCXVBcjs.__spreadValues.call(void 0, _chunkT5QVCXVBcjs.__spreadValues.call(void 0, {}, process.env), env),
368
+ stdio: "inherit"
369
+ });
370
+ child = started;
371
+ started.on("exit", (code, signal) => {
372
+ if (child !== started) {
373
+ return;
374
+ }
375
+ child = null;
376
+ _chunkA6ZPAM6Zcjs.logger.error(`\u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F \u0441\u0430\u043C (\u043A\u043E\u0434 ${String(code)}, \u0441\u0438\u0433\u043D\u0430\u043B ${String(signal)})`);
377
+ });
378
+ }
379
+ for (const signal of ["SIGINT", "SIGTERM"]) {
380
+ process.once(signal, () => {
381
+ void stop().then(() => process.exit(0));
382
+ });
383
+ }
384
+ process.on("exit", () => {
385
+ if ((child == null ? void 0 : child.pid) !== void 0) {
386
+ signalGroup(child.pid, "SIGTERM");
387
+ }
388
+ });
389
+ return { start, stop };
390
+ }
391
+
310
392
  // src/builder/dev.ts
311
393
  var SERVER_ENTRY = "build/server.mjs";
312
394
  var WATCHED = ["src", "den.config.ts"];
@@ -315,7 +397,21 @@ function debugPattern(name) {
315
397
  return (name === void 0 ? DEBUG_SCOPES : [`${name}:*`, ...DEBUG_SCOPES]).join(",");
316
398
  }
317
399
  var DEBOUNCE_MS = 150;
318
- var SHUTDOWN_TIMEOUT_MS = 5e3;
400
+ function watchSources(root, onChange) {
401
+ let pending = null;
402
+ for (const target of WATCHED) {
403
+ const watched = _path2.default.join(root, target);
404
+ if (!_fs2.default.existsSync(watched)) {
405
+ continue;
406
+ }
407
+ _fs2.default.watch(watched, { recursive: true }, () => {
408
+ if (pending) {
409
+ clearTimeout(pending);
410
+ }
411
+ pending = setTimeout(onChange, DEBOUNCE_MS);
412
+ });
413
+ }
414
+ }
319
415
  function facts(root) {
320
416
  return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
321
417
  try {
@@ -338,52 +434,10 @@ function devApp(options) {
338
434
  return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
339
435
  _chunkA6ZPAM6Zcjs.logger.setDebug(true);
340
436
  const root = _path2.default.resolve(options.root);
341
- const entry = _path2.default.join(root, SERVER_ENTRY);
342
- let child = null;
343
- let pending = null;
344
437
  let building = false;
345
438
  let dirty = false;
346
439
  const describe = describer(root);
347
- function signalGroup(pid, signal) {
348
- try {
349
- process.kill(-pid, signal);
350
- } catch (e) {
351
- }
352
- }
353
- function stop() {
354
- return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
355
- const running = child;
356
- child = null;
357
- if (!running || running.exitCode !== null || running.pid === void 0) {
358
- return;
359
- }
360
- const { pid } = running;
361
- yield new Promise((resolve) => {
362
- const forced = setTimeout(() => signalGroup(pid, "SIGKILL"), SHUTDOWN_TIMEOUT_MS);
363
- running.once("exit", () => {
364
- clearTimeout(forced);
365
- resolve();
366
- });
367
- signalGroup(pid, "SIGTERM");
368
- });
369
- });
370
- }
371
- function start(name) {
372
- var _a;
373
- const started = _child_process.spawn.call(void 0, process.execPath, [...process.execArgv, entry], {
374
- detached: true,
375
- env: _chunkT5QVCXVBcjs.__spreadProps.call(void 0, _chunkT5QVCXVBcjs.__spreadValues.call(void 0, {}, process.env), { DEBUG: (_a = process.env["DEBUG"]) != null ? _a : debugPattern(name) }),
376
- stdio: "inherit"
377
- });
378
- child = started;
379
- started.on("exit", (code, signal) => {
380
- if (child !== started) {
381
- return;
382
- }
383
- child = null;
384
- _chunkA6ZPAM6Zcjs.logger.error(`\u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F \u0441\u0430\u043C (\u043A\u043E\u0434 ${String(code)}, \u0441\u0438\u0433\u043D\u0430\u043B ${String(signal)})`);
385
- });
386
- }
440
+ const app = appProcess(_path2.default.join(root, SERVER_ENTRY));
387
441
  function show(lines) {
388
442
  process.stdout.write(`${lines.join("\n")}
389
443
  `);
@@ -402,26 +456,27 @@ function devApp(options) {
402
456
  show(helpLines());
403
457
  }
404
458
  if (key === "q") {
405
- void stop().then(() => process.exit(0));
459
+ void app.stop().then(() => process.exit(0));
406
460
  }
407
461
  });
408
462
  }
409
463
  function rebuild() {
410
464
  return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
465
+ var _a;
411
466
  if (building) {
412
467
  dirty = true;
413
- return false;
468
+ return null;
414
469
  }
415
470
  building = true;
416
- let built2 = false;
471
+ let built = null;
417
472
  try {
418
473
  do {
419
474
  dirty = false;
420
- yield stop();
475
+ yield app.stop();
421
476
  try {
422
- yield buildApp(_chunkT5QVCXVBcjs.__spreadProps.call(void 0, _chunkT5QVCXVBcjs.__spreadValues.call(void 0, {}, options), { mode: "development" }));
423
- start((yield describe()).name);
424
- built2 = true;
477
+ const result = yield buildApp(_chunkT5QVCXVBcjs.__spreadProps.call(void 0, _chunkT5QVCXVBcjs.__spreadValues.call(void 0, {}, options), { mode: "development" }));
478
+ app.start({ DEBUG: (_a = process.env["DEBUG"]) != null ? _a : debugPattern((yield describe()).name) });
479
+ built = result;
425
480
  } catch (error) {
426
481
  _chunkA6ZPAM6Zcjs.logger.error("\u0441\u0431\u043E\u0440\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0448\u043B\u0430 \u2014 \u0441\u0435\u0440\u0432\u0438\u0441 \u041D\u0415 \u041F\u041E\u0414\u041D\u042F\u0422, \u0436\u0434\u0443 \u043F\u0440\u0430\u0432\u043A\u0438:", error);
427
482
  }
@@ -429,38 +484,25 @@ function devApp(options) {
429
484
  } finally {
430
485
  building = false;
431
486
  }
432
- return built2;
487
+ return built;
433
488
  });
434
489
  }
435
490
  const startedAt = Date.now();
436
- const built = yield rebuild();
491
+ const address = yield rebuild();
437
492
  const elapsed = Date.now() - startedAt;
438
- for (const target of WATCHED) {
439
- const watched = _path2.default.join(root, target);
440
- if (!_fs2.default.existsSync(watched)) {
441
- continue;
442
- }
443
- _fs2.default.watch(watched, { recursive: true }, () => {
444
- if (pending) {
445
- clearTimeout(pending);
446
- }
447
- pending = setTimeout(() => void rebuild(), DEBOUNCE_MS);
448
- });
449
- }
450
- if (built) {
451
- show(bannerLines(_chunkT5QVCXVBcjs.__spreadValues.call(void 0, { elapsedMs: elapsed, mode: "development", watched: WATCHED }, yield describe())));
493
+ watchSources(root, () => void rebuild());
494
+ if (address !== null) {
495
+ show(
496
+ bannerLines(_chunkT5QVCXVBcjs.__spreadValues.call(void 0, {
497
+ base: address.base,
498
+ elapsedMs: elapsed,
499
+ mode: "development",
500
+ port: address.port,
501
+ watched: WATCHED
502
+ }, yield describe()))
503
+ );
452
504
  }
453
505
  listenForKeys();
454
- for (const signal of ["SIGINT", "SIGTERM"]) {
455
- process.once(signal, () => {
456
- void stop().then(() => process.exit(0));
457
- });
458
- }
459
- process.on("exit", () => {
460
- if ((child == null ? void 0 : child.pid) !== void 0) {
461
- signalGroup(child.pid, "SIGTERM");
462
- }
463
- });
464
506
  });
465
507
  }
466
508
 
@@ -1,3 +1,6 @@
1
+ import {
2
+ loadAppEnv
3
+ } from "./chunk-RG7ZV5BD.js";
1
4
  import {
2
5
  logger
3
6
  } from "./chunk-TPBI6TOU.js";
@@ -35,7 +38,7 @@ function collectAssets(outDir, base) {
35
38
  const css = [];
36
39
  const js = [];
37
40
  const seen = /* @__PURE__ */ new Set();
38
- const prefix = base.endsWith("/") ? base : `${base}/`;
41
+ const prefix = base === "" ? "" : base.endsWith("/") ? base : `${base}/`;
39
42
  function walk(key) {
40
43
  var _a, _b;
41
44
  if (seen.has(key)) {
@@ -64,6 +67,10 @@ function collectAssets(outDir, base) {
64
67
 
65
68
  // src/serve/port.ts
66
69
  var DEFAULT_PORT = 3e3;
70
+ function resolvePort(declared) {
71
+ var _a, _b;
72
+ return Number((_b = (_a = process.env["PORT"]) != null ? _a : declared) != null ? _b : DEFAULT_PORT);
73
+ }
67
74
 
68
75
  // src/builder/entries.ts
69
76
  var VIRTUAL_CLIENT = "virtual:den/client";
@@ -102,27 +109,34 @@ if (server.render) {
102
109
  export default descriptor
103
110
  `;
104
111
  }
105
- function generateServerProcess(configPath) {
106
- return `import { serveApp } from '@foxford/den/serve'
112
+ function generateServerProcess(configPath, base, mode) {
113
+ const assets = mode === "development" ? ` assets: { base: ${JSON.stringify(base)}, dir: fileURLToPath(new URL('./client', import.meta.url)) },
114
+ ` : "";
115
+ return `import { fileURLToPath } from 'node:url'
116
+
117
+ import { loadAppEnv, serveApp } from '@foxford/den/serve'
107
118
 
108
119
  import app from ${JSON.stringify(VIRTUAL_SERVER_APP)}
109
120
  import config from ${JSON.stringify(configPath)}
110
121
 
122
+ // \u0423\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044F \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u2014 \u0434\u043E \u043F\u0435\u0440\u0432\u043E\u0433\u043E \u0447\u0442\u0435\u043D\u0438\u044F: \u043F\u043E\u0440\u0442 \u0438 \u0430\u0434\u0440\u0435\u0441\u0430 \u0447\u0438\u0442\u0430\u044E\u0442\u0441\u044F \u043D\u0438\u0436\u0435
123
+ loadAppEnv(fileURLToPath(new URL('..', import.meta.url)))
124
+
111
125
  await serveApp(app, {
112
- configure: config.server?.configure,
126
+ ${assets} configure: config.server?.configure,
113
127
  host: process.env.HOST,
114
- port: Number(process.env.PORT ?? ${String(DEFAULT_PORT)}),
128
+ port: Number(process.env.PORT ?? config.server?.port ?? ${String(DEFAULT_PORT)}),
115
129
  })
116
130
  `;
117
131
  }
118
132
 
119
133
  // src/builder/plugin.ts
120
134
  function denAppModules(options) {
121
- const { assets, config, configPath, entry } = options;
135
+ const { assets, base, config, configPath, entry, mode } = options;
122
136
  const sources = {
123
137
  [VIRTUAL_CLIENT]: () => generateClientEntry(entry, config.preset.clientEntry),
124
138
  [VIRTUAL_SERVER_APP]: () => generateServerApp(entry, configPath, assets),
125
- [VIRTUAL_SERVER_PROCESS]: () => generateServerProcess(configPath)
139
+ [VIRTUAL_SERVER_PROCESS]: () => generateServerProcess(configPath, base, mode)
126
140
  };
127
141
  return {
128
142
  load: (id) => {
@@ -184,7 +198,9 @@ function viteConfigFor(options) {
184
198
  const { assets, base, config, configPath, entryFileNames, input, mode, outDir, root, target } = options;
185
199
  const entry = (_a = config.entry) != null ? _a : "src/index.ts";
186
200
  const composed = {
187
- base,
201
+ // Сборщику относительный префикс, если своего нет: ленивые чанки тогда грузятся от адреса
202
+ // уже загруженного чанка, и подмена CDN уводит за собой и их
203
+ base: base === "" ? "./" : base,
188
204
  build: {
189
205
  // Клиентский выход чистит свой каталог сам, серверные — нет: они пишут рядом с уже
190
206
  // собранным клиентским, и очистка снесла бы его вместе с манифестом
@@ -202,7 +218,10 @@ function viteConfigFor(options) {
202
218
  // Режим объявляем явно: плагины сборщика выбирают по нему рантайм фреймворка, а через
203
219
  // программный вызов он иначе не доедет.
204
220
  mode,
205
- plugins: [...config.preset.plugins, denAppModules({ assets, config, configPath, entry: path2.join(root, entry) })],
221
+ plugins: [
222
+ ...config.preset.plugins,
223
+ denAppModules({ assets, base, config, configPath, entry: path2.join(root, entry), mode })
224
+ ],
206
225
  root,
207
226
  ssr: { noExternal: (_c = (_b = config.server) == null ? void 0 : _b.noExternal) != null ? _c : [] }
208
227
  };
@@ -210,12 +229,15 @@ function viteConfigFor(options) {
210
229
  }
211
230
  function buildApp(options) {
212
231
  return __async(this, null, function* () {
213
- var _a, _b;
214
- const { base = "/", mode = "production" } = options;
232
+ var _a, _b, _c, _d;
233
+ const { mode = "production" } = options;
215
234
  process.env["NODE_ENV"] = mode;
216
235
  const root = path2.resolve(options.root);
236
+ loadAppEnv(root);
217
237
  const { config, configPath } = yield loadAppConfig(root);
218
- const refusal = (_b = (_a = config.preset).validate) == null ? void 0 : _b.call(_a, dependenciesOf(root));
238
+ const port = resolvePort((_a = config.server) == null ? void 0 : _a.port);
239
+ const base = (_b = options.base) != null ? _b : "";
240
+ const refusal = (_d = (_c = config.preset).validate) == null ? void 0 : _d.call(_c, dependenciesOf(root));
219
241
  if (refusal !== void 0 && refusal !== null) {
220
242
  throw new Error(`den: ${refusal}`);
221
243
  }
@@ -246,11 +268,11 @@ function buildApp(options) {
246
268
  target: "server"
247
269
  }))
248
270
  );
271
+ return { base, port };
249
272
  });
250
273
  }
251
274
 
252
275
  // src/builder/dev.ts
253
- import { spawn } from "child_process";
254
276
  import fs3 from "fs";
255
277
  import path3 from "path";
256
278
  import { pathToFileURL } from "url";
@@ -284,7 +306,7 @@ function assetsSummary(assets) {
284
306
  }
285
307
  function bannerLines(facts2) {
286
308
  var _a;
287
- const port = (_a = process.env["PORT"]) != null ? _a : String(DEFAULT_PORT);
309
+ const port = String((_a = facts2.port) != null ? _a : "");
288
310
  const network = networkAddress();
289
311
  const label = (title) => ` \u2192 ${title.padEnd(PAD)}`;
290
312
  return [
@@ -296,7 +318,10 @@ function bannerLines(facts2) {
296
318
  `${label("\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E")}http://localhost:${port}`,
297
319
  ...network === void 0 ? [] : [`${label("\u0432 \u0441\u0435\u0442\u0438")}http://${network}:${port}`],
298
320
  `${label("\u0440\u0443\u0447\u043A\u0438")}${ENDPOINTS.join(" ")}`,
299
- `${label("\u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B")}${assetsSummary(facts2.assets)}`,
321
+ // Префикс печатается, только когда он есть: при относительной сборке артефакты идут с
322
+ // адреса самого приложения, а он уже назван строкой выше
323
+ ...facts2.base === void 0 || facts2.base === "" ? [] : [`${label("\u043F\u0440\u0435\u0444\u0438\u043A\u0441")}${facts2.base}`],
324
+ `${label("\u0447\u0430\u043D\u043A\u0438")}${assetsSummary(facts2.assets)}`,
300
325
  `${label("\u0432\u044B\u0445\u043E\u0434\u044B")}${OUTPUTS.join(", ")}`,
301
326
  `${label("\u043F\u0440\u0430\u0432\u043A\u0438")}${facts2.watched.join(", ")}`,
302
327
  `${label("\u043A\u043B\u0430\u0432\u0438\u0448\u0438")}${SHORTCUTS.map(({ key, title }) => `${key} \u2014 ${title}`).join(", ")}`,
@@ -307,6 +332,63 @@ function helpLines() {
307
332
  return ["", " \u043A\u043B\u0430\u0432\u0438\u0448\u0430 + enter:", ...SHORTCUTS.map(({ key, title }) => ` ${key} ${title}`), ""];
308
333
  }
309
334
 
335
+ // src/builder/process.ts
336
+ import { spawn } from "child_process";
337
+ var SHUTDOWN_TIMEOUT_MS = 5e3;
338
+ function signalGroup(pid, signal) {
339
+ try {
340
+ process.kill(-pid, signal);
341
+ } catch (e) {
342
+ }
343
+ }
344
+ function appProcess(entry) {
345
+ let child = null;
346
+ function stop() {
347
+ return __async(this, null, function* () {
348
+ const running = child;
349
+ child = null;
350
+ if (!running || running.exitCode !== null || running.pid === void 0) {
351
+ return;
352
+ }
353
+ const { pid } = running;
354
+ yield new Promise((resolve) => {
355
+ const forced = setTimeout(() => signalGroup(pid, "SIGKILL"), SHUTDOWN_TIMEOUT_MS);
356
+ running.once("exit", () => {
357
+ clearTimeout(forced);
358
+ resolve();
359
+ });
360
+ signalGroup(pid, "SIGTERM");
361
+ });
362
+ });
363
+ }
364
+ function start(env) {
365
+ const started = spawn(process.execPath, [...process.execArgv, entry], {
366
+ detached: true,
367
+ env: __spreadValues(__spreadValues({}, process.env), env),
368
+ stdio: "inherit"
369
+ });
370
+ child = started;
371
+ started.on("exit", (code, signal) => {
372
+ if (child !== started) {
373
+ return;
374
+ }
375
+ child = null;
376
+ logger.error(`\u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F \u0441\u0430\u043C (\u043A\u043E\u0434 ${String(code)}, \u0441\u0438\u0433\u043D\u0430\u043B ${String(signal)})`);
377
+ });
378
+ }
379
+ for (const signal of ["SIGINT", "SIGTERM"]) {
380
+ process.once(signal, () => {
381
+ void stop().then(() => process.exit(0));
382
+ });
383
+ }
384
+ process.on("exit", () => {
385
+ if ((child == null ? void 0 : child.pid) !== void 0) {
386
+ signalGroup(child.pid, "SIGTERM");
387
+ }
388
+ });
389
+ return { start, stop };
390
+ }
391
+
310
392
  // src/builder/dev.ts
311
393
  var SERVER_ENTRY = "build/server.mjs";
312
394
  var WATCHED = ["src", "den.config.ts"];
@@ -315,7 +397,21 @@ function debugPattern(name) {
315
397
  return (name === void 0 ? DEBUG_SCOPES : [`${name}:*`, ...DEBUG_SCOPES]).join(",");
316
398
  }
317
399
  var DEBOUNCE_MS = 150;
318
- var SHUTDOWN_TIMEOUT_MS = 5e3;
400
+ function watchSources(root, onChange) {
401
+ let pending = null;
402
+ for (const target of WATCHED) {
403
+ const watched = path3.join(root, target);
404
+ if (!fs3.existsSync(watched)) {
405
+ continue;
406
+ }
407
+ fs3.watch(watched, { recursive: true }, () => {
408
+ if (pending) {
409
+ clearTimeout(pending);
410
+ }
411
+ pending = setTimeout(onChange, DEBOUNCE_MS);
412
+ });
413
+ }
414
+ }
319
415
  function facts(root) {
320
416
  return __async(this, null, function* () {
321
417
  try {
@@ -338,52 +434,10 @@ function devApp(options) {
338
434
  return __async(this, null, function* () {
339
435
  logger.setDebug(true);
340
436
  const root = path3.resolve(options.root);
341
- const entry = path3.join(root, SERVER_ENTRY);
342
- let child = null;
343
- let pending = null;
344
437
  let building = false;
345
438
  let dirty = false;
346
439
  const describe = describer(root);
347
- function signalGroup(pid, signal) {
348
- try {
349
- process.kill(-pid, signal);
350
- } catch (e) {
351
- }
352
- }
353
- function stop() {
354
- return __async(this, null, function* () {
355
- const running = child;
356
- child = null;
357
- if (!running || running.exitCode !== null || running.pid === void 0) {
358
- return;
359
- }
360
- const { pid } = running;
361
- yield new Promise((resolve) => {
362
- const forced = setTimeout(() => signalGroup(pid, "SIGKILL"), SHUTDOWN_TIMEOUT_MS);
363
- running.once("exit", () => {
364
- clearTimeout(forced);
365
- resolve();
366
- });
367
- signalGroup(pid, "SIGTERM");
368
- });
369
- });
370
- }
371
- function start(name) {
372
- var _a;
373
- const started = spawn(process.execPath, [...process.execArgv, entry], {
374
- detached: true,
375
- env: __spreadProps(__spreadValues({}, process.env), { DEBUG: (_a = process.env["DEBUG"]) != null ? _a : debugPattern(name) }),
376
- stdio: "inherit"
377
- });
378
- child = started;
379
- started.on("exit", (code, signal) => {
380
- if (child !== started) {
381
- return;
382
- }
383
- child = null;
384
- logger.error(`\u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F \u0441\u0430\u043C (\u043A\u043E\u0434 ${String(code)}, \u0441\u0438\u0433\u043D\u0430\u043B ${String(signal)})`);
385
- });
386
- }
440
+ const app = appProcess(path3.join(root, SERVER_ENTRY));
387
441
  function show(lines) {
388
442
  process.stdout.write(`${lines.join("\n")}
389
443
  `);
@@ -402,26 +456,27 @@ function devApp(options) {
402
456
  show(helpLines());
403
457
  }
404
458
  if (key === "q") {
405
- void stop().then(() => process.exit(0));
459
+ void app.stop().then(() => process.exit(0));
406
460
  }
407
461
  });
408
462
  }
409
463
  function rebuild() {
410
464
  return __async(this, null, function* () {
465
+ var _a;
411
466
  if (building) {
412
467
  dirty = true;
413
- return false;
468
+ return null;
414
469
  }
415
470
  building = true;
416
- let built2 = false;
471
+ let built = null;
417
472
  try {
418
473
  do {
419
474
  dirty = false;
420
- yield stop();
475
+ yield app.stop();
421
476
  try {
422
- yield buildApp(__spreadProps(__spreadValues({}, options), { mode: "development" }));
423
- start((yield describe()).name);
424
- built2 = true;
477
+ const result = yield buildApp(__spreadProps(__spreadValues({}, options), { mode: "development" }));
478
+ app.start({ DEBUG: (_a = process.env["DEBUG"]) != null ? _a : debugPattern((yield describe()).name) });
479
+ built = result;
425
480
  } catch (error) {
426
481
  logger.error("\u0441\u0431\u043E\u0440\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0448\u043B\u0430 \u2014 \u0441\u0435\u0440\u0432\u0438\u0441 \u041D\u0415 \u041F\u041E\u0414\u041D\u042F\u0422, \u0436\u0434\u0443 \u043F\u0440\u0430\u0432\u043A\u0438:", error);
427
482
  }
@@ -429,38 +484,25 @@ function devApp(options) {
429
484
  } finally {
430
485
  building = false;
431
486
  }
432
- return built2;
487
+ return built;
433
488
  });
434
489
  }
435
490
  const startedAt = Date.now();
436
- const built = yield rebuild();
491
+ const address = yield rebuild();
437
492
  const elapsed = Date.now() - startedAt;
438
- for (const target of WATCHED) {
439
- const watched = path3.join(root, target);
440
- if (!fs3.existsSync(watched)) {
441
- continue;
442
- }
443
- fs3.watch(watched, { recursive: true }, () => {
444
- if (pending) {
445
- clearTimeout(pending);
446
- }
447
- pending = setTimeout(() => void rebuild(), DEBOUNCE_MS);
448
- });
449
- }
450
- if (built) {
451
- show(bannerLines(__spreadValues({ elapsedMs: elapsed, mode: "development", watched: WATCHED }, yield describe())));
493
+ watchSources(root, () => void rebuild());
494
+ if (address !== null) {
495
+ show(
496
+ bannerLines(__spreadValues({
497
+ base: address.base,
498
+ elapsedMs: elapsed,
499
+ mode: "development",
500
+ port: address.port,
501
+ watched: WATCHED
502
+ }, yield describe()))
503
+ );
452
504
  }
453
505
  listenForKeys();
454
- for (const signal of ["SIGINT", "SIGTERM"]) {
455
- process.once(signal, () => {
456
- void stop().then(() => process.exit(0));
457
- });
458
- }
459
- process.on("exit", () => {
460
- if ((child == null ? void 0 : child.pid) !== void 0) {
461
- signalGroup(child.pid, "SIGTERM");
462
- }
463
- });
464
506
  });
465
507
  }
466
508
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxford/den",
3
- "version": "3.1.4",
3
+ "version": "3.2.1",
4
4
  "description": "Den — декларативный метафреймворк Foxford (core runtime)",
5
5
  "keywords": [
6
6
  "foxford",
@@ -24,8 +24,8 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "fastify": "^5.11.2",
27
- "@foxford/logger": "^1.2.2",
28
- "@foxford/ioc": "^1.1.2"
27
+ "@foxford/ioc": "^1.1.2",
28
+ "@foxford/logger": "^1.2.2"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "vite": ">=5.0.0"
@@ -88,15 +88,17 @@
88
88
  "chunk-A6ZPAM6Z.cjs",
89
89
  "chunk-C7BM4DGX.cjs",
90
90
  "chunk-E23E2VUC.js",
91
+ "chunk-KQAB6C6Q.cjs",
91
92
  "chunk-MCN4GFEQ.js",
93
+ "chunk-RG7ZV5BD.js",
92
94
  "chunk-T5QVCXVB.cjs",
93
95
  "chunk-TPBI6TOU.js",
94
96
  "chunk-TPHJUDHG.js",
97
+ "chunk-UB3GCX73.cjs",
98
+ "chunk-URSWYSZF.js",
95
99
  "chunk-WBHHHICS.js",
96
100
  "chunk-XOJ2VWX4.cjs",
97
- "chunk-YECHY3NR.js",
98
101
  "chunk-Z5BL4DJA.cjs",
99
- "chunk-ZBB5SBEV.cjs",
100
102
  "chunk-ZOU6W6ZQ.cjs",
101
103
  "define-repository-CwAXed2X.d.cts",
102
104
  "define-repository-q_T4hqeP.d.ts",
@@ -125,6 +127,6 @@
125
127
  "view-adapter-B4gEb-ms.d.ts",
126
128
  "view-adapter-DJ7yB6Ml.d.cts"
127
129
  ],
128
- "sha": "d7b1436",
130
+ "sha": "57a2d9b",
129
131
  "scripts": {}
130
132
  }
package/serve/index.cjs CHANGED
@@ -1,5 +1,8 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
2
2
 
3
+ var _chunkKQAB6C6Qcjs = require('../chunk-KQAB6C6Q.cjs');
4
+
5
+
3
6
 
4
7
 
5
8
  var _chunkZOU6W6ZQcjs = require('../chunk-ZOU6W6ZQ.cjs');
@@ -16,6 +19,52 @@ var _chunkT5QVCXVBcjs = require('../chunk-T5QVCXVB.cjs');
16
19
  var _fastify = require('fastify'); var _fastify2 = _interopRequireDefault(_fastify);
17
20
  var _logger = require('@foxford/logger');
18
21
 
22
+ // src/serve/assets.ts
23
+ var _fs = require('fs');
24
+ var _path = require('path'); var _path2 = _interopRequireDefault(_path);
25
+ var MIME = {
26
+ ".css": "text/css",
27
+ ".gif": "image/gif",
28
+ ".jpeg": "image/jpeg",
29
+ ".jpg": "image/jpeg",
30
+ ".js": "text/javascript",
31
+ ".json": "application/json",
32
+ ".map": "application/json",
33
+ ".mjs": "text/javascript",
34
+ ".png": "image/png",
35
+ ".svg": "image/svg+xml",
36
+ ".ttf": "font/ttf",
37
+ ".webp": "image/webp",
38
+ ".woff": "font/woff",
39
+ ".woff2": "font/woff2"
40
+ };
41
+ var HASHED_DIR = "assets/";
42
+ function assetsPrefix(base) {
43
+ const { pathname } = new URL(base, "http://assets.invalid");
44
+ return pathname.endsWith("/") ? pathname : `${pathname}/`;
45
+ }
46
+ function assetFile(dir, relative) {
47
+ const file = _path2.default.normalize(_path2.default.join(dir, relative));
48
+ return file.startsWith(_path2.default.normalize(dir)) ? file : null;
49
+ }
50
+ function registerAssets(instance, source) {
51
+ const dir = _path2.default.resolve(source.dir);
52
+ const prefix = assetsPrefix(source.base);
53
+ instance.get(`${prefix}*`, (request, reply) => {
54
+ var _a;
55
+ const relative = request.params["*"];
56
+ const file = assetFile(dir, relative);
57
+ if (file === null) {
58
+ return reply.code(404).send();
59
+ }
60
+ const stat = _fs.existsSync.call(void 0, file) ? _fs.statSync.call(void 0, file) : null;
61
+ if (stat === null || !stat.isFile()) {
62
+ return reply.code(404).send();
63
+ }
64
+ return reply.header("access-control-allow-origin", "*").header("cache-control", relative.startsWith(HASHED_DIR) ? "public, max-age=31536000, immutable" : "no-cache").header("content-length", stat.size).type((_a = MIME[_path2.default.extname(file)]) != null ? _a : "application/octet-stream").send(_fs.createReadStream.call(void 0, file));
65
+ });
66
+ }
67
+
19
68
  // src/serve/plugin.ts
20
69
 
21
70
  var DEFAULT_SLOW_RENDER_MS = 1e3;
@@ -106,6 +155,9 @@ function serveApp(_0) {
106
155
  const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
107
156
  const instance = _fastify2.default.call(void 0, { logger: false });
108
157
  yield instance.register(appPlugin, { app, slowRenderMs: options.slowRenderMs });
158
+ if (options.assets) {
159
+ registerAssets(instance, options.assets);
160
+ }
109
161
  instance.addHook("onRequest", (req, _reply, done) => {
110
162
  serveLog.debug(`\u2192 ${req.method} ${req.url}`);
111
163
  done();
@@ -134,4 +186,6 @@ function serveApp(_0) {
134
186
 
135
187
 
136
188
 
137
- exports.appPlugin = appPlugin; exports.serveApp = serveApp;
189
+
190
+
191
+ exports.appPlugin = appPlugin; exports.loadAppEnv = _chunkKQAB6C6Qcjs.loadAppEnv; exports.registerAssets = registerAssets; exports.serveApp = serveApp;
package/serve/index.d.cts CHANGED
@@ -1,10 +1,25 @@
1
+ import { FastifyInstance, FastifyPluginAsync } from 'fastify';
1
2
  import { I as IslandDescriptor } from '../descriptor-Dujg_1on.cjs';
2
3
  import { A as AppRoute } from '../routes-B0hDrkiF.cjs';
3
- import { FastifyPluginAsync, FastifyInstance } from 'fastify';
4
4
  import '../define-slot-DYNCLDJY.cjs';
5
5
  import '../types-Dx5qGiwk.cjs';
6
6
  import '@foxford/ioc';
7
7
 
8
+ /** Что раздаётся и по какому префиксу. */
9
+ interface AppAssetsSource {
10
+ /** Префикс адресов артефактов — тот, с которым собран клиентский выход. */
11
+ base: string;
12
+ /** Каталог клиентского выхода. */
13
+ dir: string;
14
+ }
15
+ /**
16
+ * Заводит раздачу артефактов на инстансе.
17
+ *
18
+ * @param instance - Инстанс fastify
19
+ * @param source - Префикс адресов и каталог выхода
20
+ */
21
+ declare function registerAssets(instance: FastifyInstance, source: AppAssetsSource): void;
22
+
8
23
  /** Приложение, которое можно поднять сервисом: остров плюс его заявка на адреса. */
9
24
  type ServableApp = IslandDescriptor & {
10
25
  routes?: ReadonlyArray<AppRoute>;
@@ -26,6 +41,15 @@ interface AppPluginOptions {
26
41
  */
27
42
  declare const appPlugin: FastifyPluginAsync<AppPluginOptions>;
28
43
 
44
+ /**
45
+ * Кладёт умолчания из `<root>/.env` в окружение процесса.
46
+ *
47
+ * Отсутствие файла — норма, а не отказ: он локальный и в репозиторий не попадает.
48
+ *
49
+ * @param root - Корень приложения
50
+ */
51
+ declare function loadAppEnv(root: string): void;
52
+
29
53
  /** Настройки инстанса. */
30
54
  interface ServeAppOptions {
31
55
  /** Порт; `0` — свободный, фактический смотри в `server.addresses()`. */
@@ -44,6 +68,13 @@ interface ServeAppOptions {
44
68
  * который именно так и просит его уйти, а незакрытый инстанс рвёт запросы на середине.
45
69
  */
46
70
  gracefulShutdown?: boolean;
71
+ /**
72
+ * Клиентские артефакты приложения: префикс адресов и каталог выхода.
73
+ *
74
+ * Отдаёт их само приложение — адреса знает его сборка, а хост получает только ссылки. В
75
+ * проде перед процессом стоит CDN, и до него эти адреса не доходят.
76
+ */
77
+ assets?: AppAssetsSource;
47
78
  }
48
79
  /**
49
80
  * Поднимает приложение отдельным сервисом.
@@ -57,4 +88,4 @@ interface ServeAppOptions {
57
88
  */
58
89
  declare function serveApp(app: ServableApp, options?: ServeAppOptions): Promise<FastifyInstance>;
59
90
 
60
- export { type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, serveApp };
91
+ export { type AppAssetsSource, type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, loadAppEnv, registerAssets, serveApp };
package/serve/index.d.ts CHANGED
@@ -1,10 +1,25 @@
1
+ import { FastifyInstance, FastifyPluginAsync } from 'fastify';
1
2
  import { I as IslandDescriptor } from '../descriptor-DQ6Gi7A_.js';
2
3
  import { A as AppRoute } from '../routes-0uXJ0fux.js';
3
- import { FastifyPluginAsync, FastifyInstance } from 'fastify';
4
4
  import '../define-slot-Cngzae6i.js';
5
5
  import '../types-Dx5qGiwk.js';
6
6
  import '@foxford/ioc';
7
7
 
8
+ /** Что раздаётся и по какому префиксу. */
9
+ interface AppAssetsSource {
10
+ /** Префикс адресов артефактов — тот, с которым собран клиентский выход. */
11
+ base: string;
12
+ /** Каталог клиентского выхода. */
13
+ dir: string;
14
+ }
15
+ /**
16
+ * Заводит раздачу артефактов на инстансе.
17
+ *
18
+ * @param instance - Инстанс fastify
19
+ * @param source - Префикс адресов и каталог выхода
20
+ */
21
+ declare function registerAssets(instance: FastifyInstance, source: AppAssetsSource): void;
22
+
8
23
  /** Приложение, которое можно поднять сервисом: остров плюс его заявка на адреса. */
9
24
  type ServableApp = IslandDescriptor & {
10
25
  routes?: ReadonlyArray<AppRoute>;
@@ -26,6 +41,15 @@ interface AppPluginOptions {
26
41
  */
27
42
  declare const appPlugin: FastifyPluginAsync<AppPluginOptions>;
28
43
 
44
+ /**
45
+ * Кладёт умолчания из `<root>/.env` в окружение процесса.
46
+ *
47
+ * Отсутствие файла — норма, а не отказ: он локальный и в репозиторий не попадает.
48
+ *
49
+ * @param root - Корень приложения
50
+ */
51
+ declare function loadAppEnv(root: string): void;
52
+
29
53
  /** Настройки инстанса. */
30
54
  interface ServeAppOptions {
31
55
  /** Порт; `0` — свободный, фактический смотри в `server.addresses()`. */
@@ -44,6 +68,13 @@ interface ServeAppOptions {
44
68
  * который именно так и просит его уйти, а незакрытый инстанс рвёт запросы на середине.
45
69
  */
46
70
  gracefulShutdown?: boolean;
71
+ /**
72
+ * Клиентские артефакты приложения: префикс адресов и каталог выхода.
73
+ *
74
+ * Отдаёт их само приложение — адреса знает его сборка, а хост получает только ссылки. В
75
+ * проде перед процессом стоит CDN, и до него эти адреса не доходят.
76
+ */
77
+ assets?: AppAssetsSource;
47
78
  }
48
79
  /**
49
80
  * Поднимает приложение отдельным сервисом.
@@ -57,4 +88,4 @@ interface ServeAppOptions {
57
88
  */
58
89
  declare function serveApp(app: ServableApp, options?: ServeAppOptions): Promise<FastifyInstance>;
59
90
 
60
- export { type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, serveApp };
91
+ export { type AppAssetsSource, type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, loadAppEnv, registerAssets, serveApp };
package/serve/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import {
2
+ loadAppEnv
3
+ } from "../chunk-RG7ZV5BD.js";
1
4
  import {
2
5
  matchRoute,
3
6
  normalizeRoutes,
@@ -16,6 +19,52 @@ import {
16
19
  import Fastify from "fastify";
17
20
  import { log as log2 } from "@foxford/logger";
18
21
 
22
+ // src/serve/assets.ts
23
+ import { createReadStream, existsSync, statSync } from "fs";
24
+ import path from "path";
25
+ var MIME = {
26
+ ".css": "text/css",
27
+ ".gif": "image/gif",
28
+ ".jpeg": "image/jpeg",
29
+ ".jpg": "image/jpeg",
30
+ ".js": "text/javascript",
31
+ ".json": "application/json",
32
+ ".map": "application/json",
33
+ ".mjs": "text/javascript",
34
+ ".png": "image/png",
35
+ ".svg": "image/svg+xml",
36
+ ".ttf": "font/ttf",
37
+ ".webp": "image/webp",
38
+ ".woff": "font/woff",
39
+ ".woff2": "font/woff2"
40
+ };
41
+ var HASHED_DIR = "assets/";
42
+ function assetsPrefix(base) {
43
+ const { pathname } = new URL(base, "http://assets.invalid");
44
+ return pathname.endsWith("/") ? pathname : `${pathname}/`;
45
+ }
46
+ function assetFile(dir, relative) {
47
+ const file = path.normalize(path.join(dir, relative));
48
+ return file.startsWith(path.normalize(dir)) ? file : null;
49
+ }
50
+ function registerAssets(instance, source) {
51
+ const dir = path.resolve(source.dir);
52
+ const prefix = assetsPrefix(source.base);
53
+ instance.get(`${prefix}*`, (request, reply) => {
54
+ var _a;
55
+ const relative = request.params["*"];
56
+ const file = assetFile(dir, relative);
57
+ if (file === null) {
58
+ return reply.code(404).send();
59
+ }
60
+ const stat = existsSync(file) ? statSync(file) : null;
61
+ if (stat === null || !stat.isFile()) {
62
+ return reply.code(404).send();
63
+ }
64
+ return reply.header("access-control-allow-origin", "*").header("cache-control", relative.startsWith(HASHED_DIR) ? "public, max-age=31536000, immutable" : "no-cache").header("content-length", stat.size).type((_a = MIME[path.extname(file)]) != null ? _a : "application/octet-stream").send(createReadStream(file));
65
+ });
66
+ }
67
+
19
68
  // src/serve/plugin.ts
20
69
  import { log } from "@foxford/logger";
21
70
  var DEFAULT_SLOW_RENDER_MS = 1e3;
@@ -106,6 +155,9 @@ function serveApp(_0) {
106
155
  const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
107
156
  const instance = Fastify({ logger: false });
108
157
  yield instance.register(appPlugin, { app, slowRenderMs: options.slowRenderMs });
158
+ if (options.assets) {
159
+ registerAssets(instance, options.assets);
160
+ }
109
161
  instance.addHook("onRequest", (req, _reply, done) => {
110
162
  serveLog.debug(`\u2192 ${req.method} ${req.url}`);
111
163
  done();
@@ -133,5 +185,7 @@ function serveApp(_0) {
133
185
  }
134
186
  export {
135
187
  appPlugin,
188
+ loadAppEnv,
189
+ registerAssets,
136
190
  serveApp
137
191
  };