@foxford/den 3.1.3 → 3.2.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/bin/den.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
 
4
- var _chunkZBB5SBEVcjs = require('../chunk-ZBB5SBEV.cjs');
4
+ var _chunkRRR2DJMNcjs = require('../chunk-RRR2DJMN.cjs');
5
5
  require('../chunk-A6ZPAM6Z.cjs');
6
6
 
7
7
 
@@ -69,11 +69,11 @@ function run(argv) {
69
69
  }
70
70
  const target = { base: options["base"], root: (_a = options["root"]) != null ? _a : process.cwd() };
71
71
  if (command === "build") {
72
- yield _chunkZBB5SBEVcjs.buildApp.call(void 0, target);
72
+ yield _chunkRRR2DJMNcjs.buildApp.call(void 0, target);
73
73
  return;
74
74
  }
75
75
  if (command === "dev") {
76
- yield _chunkZBB5SBEVcjs.devApp.call(void 0, target);
76
+ yield _chunkRRR2DJMNcjs.devApp.call(void 0, target);
77
77
  return;
78
78
  }
79
79
  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,7 @@
1
1
  import {
2
2
  buildApp,
3
3
  devApp
4
- } from "../chunk-YECHY3NR.js";
4
+ } from "../chunk-BMI5HNGE.js";
5
5
  import "../chunk-TPBI6TOU.js";
6
6
  import {
7
7
  __async
package/builder/index.cjs CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkZBB5SBEVcjs = require('../chunk-ZBB5SBEV.cjs');
5
+ var _chunkRRR2DJMNcjs = require('../chunk-RRR2DJMN.cjs');
6
6
  require('../chunk-A6ZPAM6Z.cjs');
7
7
  require('../chunk-T5QVCXVB.cjs');
8
8
 
9
9
 
10
10
 
11
11
 
12
- exports.buildApp = _chunkZBB5SBEVcjs.buildApp; exports.defineAppConfig = _chunkZBB5SBEVcjs.defineAppConfig; exports.devApp = _chunkZBB5SBEVcjs.devApp;
12
+ exports.buildApp = _chunkRRR2DJMNcjs.buildApp; exports.defineAppConfig = _chunkRRR2DJMNcjs.defineAppConfig; exports.devApp = _chunkRRR2DJMNcjs.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,7 @@ import {
2
2
  buildApp,
3
3
  defineAppConfig,
4
4
  devApp
5
- } from "../chunk-YECHY3NR.js";
5
+ } from "../chunk-BMI5HNGE.js";
6
6
  import "../chunk-TPBI6TOU.js";
7
7
  import "../chunk-6FNC3XMI.js";
8
8
  export {
@@ -35,7 +35,7 @@ function collectAssets(outDir, base) {
35
35
  const css = [];
36
36
  const js = [];
37
37
  const seen = /* @__PURE__ */ new Set();
38
- const prefix = base.endsWith("/") ? base : `${base}/`;
38
+ const prefix = base === "" ? "" : base.endsWith("/") ? base : `${base}/`;
39
39
  function walk(key) {
40
40
  var _a, _b;
41
41
  if (seen.has(key)) {
@@ -64,6 +64,10 @@ function collectAssets(outDir, base) {
64
64
 
65
65
  // src/serve/port.ts
66
66
  var DEFAULT_PORT = 3e3;
67
+ function resolvePort(declared) {
68
+ var _a, _b;
69
+ return Number((_b = (_a = process.env["PORT"]) != null ? _a : declared) != null ? _b : DEFAULT_PORT);
70
+ }
67
71
 
68
72
  // src/builder/entries.ts
69
73
  var VIRTUAL_CLIENT = "virtual:den/client";
@@ -102,27 +106,29 @@ if (server.render) {
102
106
  export default descriptor
103
107
  `;
104
108
  }
105
- function generateServerProcess(configPath) {
106
- return `import { serveApp } from '@foxford/den/serve'
109
+ function generateServerProcess(configPath, base, mode) {
110
+ const assets = mode === "development" ? ` assets: { base: ${JSON.stringify(base)}, dir: fileURLToPath(new URL('./client', import.meta.url)) },
111
+ ` : "";
112
+ return `${mode === "development" ? "import { fileURLToPath } from 'node:url'\n\n" : ""}import { serveApp } from '@foxford/den/serve'
107
113
 
108
114
  import app from ${JSON.stringify(VIRTUAL_SERVER_APP)}
109
115
  import config from ${JSON.stringify(configPath)}
110
116
 
111
117
  await serveApp(app, {
112
- configure: config.server?.configure,
118
+ ${assets} configure: config.server?.configure,
113
119
  host: process.env.HOST,
114
- port: Number(process.env.PORT ?? ${String(DEFAULT_PORT)}),
120
+ port: Number(process.env.PORT ?? config.server?.port ?? ${String(DEFAULT_PORT)}),
115
121
  })
116
122
  `;
117
123
  }
118
124
 
119
125
  // src/builder/plugin.ts
120
126
  function denAppModules(options) {
121
- const { assets, config, configPath, entry } = options;
127
+ const { assets, base, config, configPath, entry, mode } = options;
122
128
  const sources = {
123
129
  [VIRTUAL_CLIENT]: () => generateClientEntry(entry, config.preset.clientEntry),
124
130
  [VIRTUAL_SERVER_APP]: () => generateServerApp(entry, configPath, assets),
125
- [VIRTUAL_SERVER_PROCESS]: () => generateServerProcess(configPath)
131
+ [VIRTUAL_SERVER_PROCESS]: () => generateServerProcess(configPath, base, mode)
126
132
  };
127
133
  return {
128
134
  load: (id) => {
@@ -184,7 +190,9 @@ function viteConfigFor(options) {
184
190
  const { assets, base, config, configPath, entryFileNames, input, mode, outDir, root, target } = options;
185
191
  const entry = (_a = config.entry) != null ? _a : "src/index.ts";
186
192
  const composed = {
187
- base,
193
+ // Сборщику относительный префикс, если своего нет: ленивые чанки тогда грузятся от адреса
194
+ // уже загруженного чанка, и подмена CDN уводит за собой и их
195
+ base: base === "" ? "./" : base,
188
196
  build: {
189
197
  // Клиентский выход чистит свой каталог сам, серверные — нет: они пишут рядом с уже
190
198
  // собранным клиентским, и очистка снесла бы его вместе с манифестом
@@ -202,7 +210,10 @@ function viteConfigFor(options) {
202
210
  // Режим объявляем явно: плагины сборщика выбирают по нему рантайм фреймворка, а через
203
211
  // программный вызов он иначе не доедет.
204
212
  mode,
205
- plugins: [...config.preset.plugins, denAppModules({ assets, config, configPath, entry: path2.join(root, entry) })],
213
+ plugins: [
214
+ ...config.preset.plugins,
215
+ denAppModules({ assets, base, config, configPath, entry: path2.join(root, entry), mode })
216
+ ],
206
217
  root,
207
218
  ssr: { noExternal: (_c = (_b = config.server) == null ? void 0 : _b.noExternal) != null ? _c : [] }
208
219
  };
@@ -210,12 +221,14 @@ function viteConfigFor(options) {
210
221
  }
211
222
  function buildApp(options) {
212
223
  return __async(this, null, function* () {
213
- var _a, _b;
214
- const { base = "/", mode = "production" } = options;
224
+ var _a, _b, _c, _d;
225
+ const { mode = "production" } = options;
215
226
  process.env["NODE_ENV"] = mode;
216
227
  const root = path2.resolve(options.root);
217
228
  const { config, configPath } = yield loadAppConfig(root);
218
- const refusal = (_b = (_a = config.preset).validate) == null ? void 0 : _b.call(_a, dependenciesOf(root));
229
+ const port = resolvePort((_a = config.server) == null ? void 0 : _a.port);
230
+ const base = (_b = options.base) != null ? _b : "";
231
+ const refusal = (_d = (_c = config.preset).validate) == null ? void 0 : _d.call(_c, dependenciesOf(root));
219
232
  if (refusal !== void 0 && refusal !== null) {
220
233
  throw new Error(`den: ${refusal}`);
221
234
  }
@@ -246,11 +259,11 @@ function buildApp(options) {
246
259
  target: "server"
247
260
  }))
248
261
  );
262
+ return { base, port };
249
263
  });
250
264
  }
251
265
 
252
266
  // src/builder/dev.ts
253
- import { spawn } from "child_process";
254
267
  import fs3 from "fs";
255
268
  import path3 from "path";
256
269
  import { pathToFileURL } from "url";
@@ -284,7 +297,7 @@ function assetsSummary(assets) {
284
297
  }
285
298
  function bannerLines(facts2) {
286
299
  var _a;
287
- const port = (_a = process.env["PORT"]) != null ? _a : String(DEFAULT_PORT);
300
+ const port = String((_a = facts2.port) != null ? _a : "");
288
301
  const network = networkAddress();
289
302
  const label = (title) => ` \u2192 ${title.padEnd(PAD)}`;
290
303
  return [
@@ -296,7 +309,8 @@ function bannerLines(facts2) {
296
309
  `${label("\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E")}http://localhost:${port}`,
297
310
  ...network === void 0 ? [] : [`${label("\u0432 \u0441\u0435\u0442\u0438")}http://${network}:${port}`],
298
311
  `${label("\u0440\u0443\u0447\u043A\u0438")}${ENDPOINTS.join(" ")}`,
299
- `${label("\u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B")}${assetsSummary(facts2.assets)}`,
312
+ ...facts2.base === void 0 ? [] : [`${label("\u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B")}${facts2.base}`],
313
+ `${label("\u0447\u0430\u043D\u043A\u0438")}${assetsSummary(facts2.assets)}`,
300
314
  `${label("\u0432\u044B\u0445\u043E\u0434\u044B")}${OUTPUTS.join(", ")}`,
301
315
  `${label("\u043F\u0440\u0430\u0432\u043A\u0438")}${facts2.watched.join(", ")}`,
302
316
  `${label("\u043A\u043B\u0430\u0432\u0438\u0448\u0438")}${SHORTCUTS.map(({ key, title }) => `${key} \u2014 ${title}`).join(", ")}`,
@@ -307,6 +321,63 @@ function helpLines() {
307
321
  return ["", " \u043A\u043B\u0430\u0432\u0438\u0448\u0430 + enter:", ...SHORTCUTS.map(({ key, title }) => ` ${key} ${title}`), ""];
308
322
  }
309
323
 
324
+ // src/builder/process.ts
325
+ import { spawn } from "child_process";
326
+ var SHUTDOWN_TIMEOUT_MS = 5e3;
327
+ function signalGroup(pid, signal) {
328
+ try {
329
+ process.kill(-pid, signal);
330
+ } catch (e) {
331
+ }
332
+ }
333
+ function appProcess(entry) {
334
+ let child = null;
335
+ function stop() {
336
+ return __async(this, null, function* () {
337
+ const running = child;
338
+ child = null;
339
+ if (!running || running.exitCode !== null || running.pid === void 0) {
340
+ return;
341
+ }
342
+ const { pid } = running;
343
+ yield new Promise((resolve) => {
344
+ const forced = setTimeout(() => signalGroup(pid, "SIGKILL"), SHUTDOWN_TIMEOUT_MS);
345
+ running.once("exit", () => {
346
+ clearTimeout(forced);
347
+ resolve();
348
+ });
349
+ signalGroup(pid, "SIGTERM");
350
+ });
351
+ });
352
+ }
353
+ function start(env) {
354
+ const started = spawn(process.execPath, [...process.execArgv, entry], {
355
+ detached: true,
356
+ env: __spreadValues(__spreadValues({}, process.env), env),
357
+ stdio: "inherit"
358
+ });
359
+ child = started;
360
+ started.on("exit", (code, signal) => {
361
+ if (child !== started) {
362
+ return;
363
+ }
364
+ child = null;
365
+ 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)})`);
366
+ });
367
+ }
368
+ for (const signal of ["SIGINT", "SIGTERM"]) {
369
+ process.once(signal, () => {
370
+ void stop().then(() => process.exit(0));
371
+ });
372
+ }
373
+ process.on("exit", () => {
374
+ if ((child == null ? void 0 : child.pid) !== void 0) {
375
+ signalGroup(child.pid, "SIGTERM");
376
+ }
377
+ });
378
+ return { start, stop };
379
+ }
380
+
310
381
  // src/builder/dev.ts
311
382
  var SERVER_ENTRY = "build/server.mjs";
312
383
  var WATCHED = ["src", "den.config.ts"];
@@ -315,7 +386,21 @@ function debugPattern(name) {
315
386
  return (name === void 0 ? DEBUG_SCOPES : [`${name}:*`, ...DEBUG_SCOPES]).join(",");
316
387
  }
317
388
  var DEBOUNCE_MS = 150;
318
- var SHUTDOWN_TIMEOUT_MS = 5e3;
389
+ function watchSources(root, onChange) {
390
+ let pending = null;
391
+ for (const target of WATCHED) {
392
+ const watched = path3.join(root, target);
393
+ if (!fs3.existsSync(watched)) {
394
+ continue;
395
+ }
396
+ fs3.watch(watched, { recursive: true }, () => {
397
+ if (pending) {
398
+ clearTimeout(pending);
399
+ }
400
+ pending = setTimeout(onChange, DEBOUNCE_MS);
401
+ });
402
+ }
403
+ }
319
404
  function facts(root) {
320
405
  return __async(this, null, function* () {
321
406
  try {
@@ -338,52 +423,10 @@ function devApp(options) {
338
423
  return __async(this, null, function* () {
339
424
  logger.setDebug(true);
340
425
  const root = path3.resolve(options.root);
341
- const entry = path3.join(root, SERVER_ENTRY);
342
- let child = null;
343
- let pending = null;
344
426
  let building = false;
345
427
  let dirty = false;
346
428
  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
- }
429
+ const app = appProcess(path3.join(root, SERVER_ENTRY));
387
430
  function show(lines) {
388
431
  process.stdout.write(`${lines.join("\n")}
389
432
  `);
@@ -402,26 +445,27 @@ function devApp(options) {
402
445
  show(helpLines());
403
446
  }
404
447
  if (key === "q") {
405
- void stop().then(() => process.exit(0));
448
+ void app.stop().then(() => process.exit(0));
406
449
  }
407
450
  });
408
451
  }
409
452
  function rebuild() {
410
453
  return __async(this, null, function* () {
454
+ var _a;
411
455
  if (building) {
412
456
  dirty = true;
413
- return false;
457
+ return null;
414
458
  }
415
459
  building = true;
416
- let built2 = false;
460
+ let built = null;
417
461
  try {
418
462
  do {
419
463
  dirty = false;
420
- yield stop();
464
+ yield app.stop();
421
465
  try {
422
- yield buildApp(__spreadProps(__spreadValues({}, options), { mode: "development" }));
423
- start((yield describe()).name);
424
- built2 = true;
466
+ const result = yield buildApp(__spreadProps(__spreadValues({}, options), { mode: "development" }));
467
+ app.start({ DEBUG: (_a = process.env["DEBUG"]) != null ? _a : debugPattern((yield describe()).name) });
468
+ built = result;
425
469
  } catch (error) {
426
470
  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
471
  }
@@ -429,38 +473,25 @@ function devApp(options) {
429
473
  } finally {
430
474
  building = false;
431
475
  }
432
- return built2;
476
+ return built;
433
477
  });
434
478
  }
435
479
  const startedAt = Date.now();
436
- const built = yield rebuild();
480
+ const address = yield rebuild();
437
481
  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())));
482
+ watchSources(root, () => void rebuild());
483
+ if (address !== null) {
484
+ show(
485
+ bannerLines(__spreadValues({
486
+ base: address.base,
487
+ elapsedMs: elapsed,
488
+ mode: "development",
489
+ port: address.port,
490
+ watched: WATCHED
491
+ }, yield describe()))
492
+ );
452
493
  }
453
494
  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
495
  });
465
496
  }
466
497
 
@@ -35,7 +35,7 @@ function collectAssets(outDir, base) {
35
35
  const css = [];
36
36
  const js = [];
37
37
  const seen = /* @__PURE__ */ new Set();
38
- const prefix = base.endsWith("/") ? base : `${base}/`;
38
+ const prefix = base === "" ? "" : base.endsWith("/") ? base : `${base}/`;
39
39
  function walk(key) {
40
40
  var _a, _b;
41
41
  if (seen.has(key)) {
@@ -64,6 +64,10 @@ function collectAssets(outDir, base) {
64
64
 
65
65
  // src/serve/port.ts
66
66
  var DEFAULT_PORT = 3e3;
67
+ function resolvePort(declared) {
68
+ var _a, _b;
69
+ return Number((_b = (_a = process.env["PORT"]) != null ? _a : declared) != null ? _b : DEFAULT_PORT);
70
+ }
67
71
 
68
72
  // src/builder/entries.ts
69
73
  var VIRTUAL_CLIENT = "virtual:den/client";
@@ -102,27 +106,29 @@ if (server.render) {
102
106
  export default descriptor
103
107
  `;
104
108
  }
105
- function generateServerProcess(configPath) {
106
- return `import { serveApp } from '@foxford/den/serve'
109
+ function generateServerProcess(configPath, base, mode) {
110
+ const assets = mode === "development" ? ` assets: { base: ${JSON.stringify(base)}, dir: fileURLToPath(new URL('./client', import.meta.url)) },
111
+ ` : "";
112
+ return `${mode === "development" ? "import { fileURLToPath } from 'node:url'\n\n" : ""}import { serveApp } from '@foxford/den/serve'
107
113
 
108
114
  import app from ${JSON.stringify(VIRTUAL_SERVER_APP)}
109
115
  import config from ${JSON.stringify(configPath)}
110
116
 
111
117
  await serveApp(app, {
112
- configure: config.server?.configure,
118
+ ${assets} configure: config.server?.configure,
113
119
  host: process.env.HOST,
114
- port: Number(process.env.PORT ?? ${String(DEFAULT_PORT)}),
120
+ port: Number(process.env.PORT ?? config.server?.port ?? ${String(DEFAULT_PORT)}),
115
121
  })
116
122
  `;
117
123
  }
118
124
 
119
125
  // src/builder/plugin.ts
120
126
  function denAppModules(options) {
121
- const { assets, config, configPath, entry } = options;
127
+ const { assets, base, config, configPath, entry, mode } = options;
122
128
  const sources = {
123
129
  [VIRTUAL_CLIENT]: () => generateClientEntry(entry, config.preset.clientEntry),
124
130
  [VIRTUAL_SERVER_APP]: () => generateServerApp(entry, configPath, assets),
125
- [VIRTUAL_SERVER_PROCESS]: () => generateServerProcess(configPath)
131
+ [VIRTUAL_SERVER_PROCESS]: () => generateServerProcess(configPath, base, mode)
126
132
  };
127
133
  return {
128
134
  load: (id) => {
@@ -184,7 +190,9 @@ function viteConfigFor(options) {
184
190
  const { assets, base, config, configPath, entryFileNames, input, mode, outDir, root, target } = options;
185
191
  const entry = (_a = config.entry) != null ? _a : "src/index.ts";
186
192
  const composed = {
187
- base,
193
+ // Сборщику относительный префикс, если своего нет: ленивые чанки тогда грузятся от адреса
194
+ // уже загруженного чанка, и подмена CDN уводит за собой и их
195
+ base: base === "" ? "./" : base,
188
196
  build: {
189
197
  // Клиентский выход чистит свой каталог сам, серверные — нет: они пишут рядом с уже
190
198
  // собранным клиентским, и очистка снесла бы его вместе с манифестом
@@ -202,7 +210,10 @@ function viteConfigFor(options) {
202
210
  // Режим объявляем явно: плагины сборщика выбирают по нему рантайм фреймворка, а через
203
211
  // программный вызов он иначе не доедет.
204
212
  mode,
205
- plugins: [...config.preset.plugins, denAppModules({ assets, config, configPath, entry: _path2.default.join(root, entry) })],
213
+ plugins: [
214
+ ...config.preset.plugins,
215
+ denAppModules({ assets, base, config, configPath, entry: _path2.default.join(root, entry), mode })
216
+ ],
206
217
  root,
207
218
  ssr: { noExternal: (_c = (_b = config.server) == null ? void 0 : _b.noExternal) != null ? _c : [] }
208
219
  };
@@ -210,12 +221,14 @@ function viteConfigFor(options) {
210
221
  }
211
222
  function buildApp(options) {
212
223
  return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
213
- var _a, _b;
214
- const { base = "/", mode = "production" } = options;
224
+ var _a, _b, _c, _d;
225
+ const { mode = "production" } = options;
215
226
  process.env["NODE_ENV"] = mode;
216
227
  const root = _path2.default.resolve(options.root);
217
228
  const { config, configPath } = yield loadAppConfig(root);
218
- const refusal = (_b = (_a = config.preset).validate) == null ? void 0 : _b.call(_a, dependenciesOf(root));
229
+ const port = resolvePort((_a = config.server) == null ? void 0 : _a.port);
230
+ const base = (_b = options.base) != null ? _b : "";
231
+ const refusal = (_d = (_c = config.preset).validate) == null ? void 0 : _d.call(_c, dependenciesOf(root));
219
232
  if (refusal !== void 0 && refusal !== null) {
220
233
  throw new Error(`den: ${refusal}`);
221
234
  }
@@ -246,11 +259,11 @@ function buildApp(options) {
246
259
  target: "server"
247
260
  }))
248
261
  );
262
+ return { base, port };
249
263
  });
250
264
  }
251
265
 
252
266
  // src/builder/dev.ts
253
- var _child_process = require('child_process');
254
267
 
255
268
 
256
269
  var _url = require('url');
@@ -284,7 +297,7 @@ function assetsSummary(assets) {
284
297
  }
285
298
  function bannerLines(facts2) {
286
299
  var _a;
287
- const port = (_a = process.env["PORT"]) != null ? _a : String(DEFAULT_PORT);
300
+ const port = String((_a = facts2.port) != null ? _a : "");
288
301
  const network = networkAddress();
289
302
  const label = (title) => ` \u2192 ${title.padEnd(PAD)}`;
290
303
  return [
@@ -296,7 +309,8 @@ function bannerLines(facts2) {
296
309
  `${label("\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E")}http://localhost:${port}`,
297
310
  ...network === void 0 ? [] : [`${label("\u0432 \u0441\u0435\u0442\u0438")}http://${network}:${port}`],
298
311
  `${label("\u0440\u0443\u0447\u043A\u0438")}${ENDPOINTS.join(" ")}`,
299
- `${label("\u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B")}${assetsSummary(facts2.assets)}`,
312
+ ...facts2.base === void 0 ? [] : [`${label("\u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B")}${facts2.base}`],
313
+ `${label("\u0447\u0430\u043D\u043A\u0438")}${assetsSummary(facts2.assets)}`,
300
314
  `${label("\u0432\u044B\u0445\u043E\u0434\u044B")}${OUTPUTS.join(", ")}`,
301
315
  `${label("\u043F\u0440\u0430\u0432\u043A\u0438")}${facts2.watched.join(", ")}`,
302
316
  `${label("\u043A\u043B\u0430\u0432\u0438\u0448\u0438")}${SHORTCUTS.map(({ key, title }) => `${key} \u2014 ${title}`).join(", ")}`,
@@ -307,6 +321,63 @@ function helpLines() {
307
321
  return ["", " \u043A\u043B\u0430\u0432\u0438\u0448\u0430 + enter:", ...SHORTCUTS.map(({ key, title }) => ` ${key} ${title}`), ""];
308
322
  }
309
323
 
324
+ // src/builder/process.ts
325
+ var _child_process = require('child_process');
326
+ var SHUTDOWN_TIMEOUT_MS = 5e3;
327
+ function signalGroup(pid, signal) {
328
+ try {
329
+ process.kill(-pid, signal);
330
+ } catch (e) {
331
+ }
332
+ }
333
+ function appProcess(entry) {
334
+ let child = null;
335
+ function stop() {
336
+ return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
337
+ const running = child;
338
+ child = null;
339
+ if (!running || running.exitCode !== null || running.pid === void 0) {
340
+ return;
341
+ }
342
+ const { pid } = running;
343
+ yield new Promise((resolve) => {
344
+ const forced = setTimeout(() => signalGroup(pid, "SIGKILL"), SHUTDOWN_TIMEOUT_MS);
345
+ running.once("exit", () => {
346
+ clearTimeout(forced);
347
+ resolve();
348
+ });
349
+ signalGroup(pid, "SIGTERM");
350
+ });
351
+ });
352
+ }
353
+ function start(env) {
354
+ const started = _child_process.spawn.call(void 0, process.execPath, [...process.execArgv, entry], {
355
+ detached: true,
356
+ env: _chunkT5QVCXVBcjs.__spreadValues.call(void 0, _chunkT5QVCXVBcjs.__spreadValues.call(void 0, {}, process.env), env),
357
+ stdio: "inherit"
358
+ });
359
+ child = started;
360
+ started.on("exit", (code, signal) => {
361
+ if (child !== started) {
362
+ return;
363
+ }
364
+ child = null;
365
+ _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)})`);
366
+ });
367
+ }
368
+ for (const signal of ["SIGINT", "SIGTERM"]) {
369
+ process.once(signal, () => {
370
+ void stop().then(() => process.exit(0));
371
+ });
372
+ }
373
+ process.on("exit", () => {
374
+ if ((child == null ? void 0 : child.pid) !== void 0) {
375
+ signalGroup(child.pid, "SIGTERM");
376
+ }
377
+ });
378
+ return { start, stop };
379
+ }
380
+
310
381
  // src/builder/dev.ts
311
382
  var SERVER_ENTRY = "build/server.mjs";
312
383
  var WATCHED = ["src", "den.config.ts"];
@@ -315,7 +386,21 @@ function debugPattern(name) {
315
386
  return (name === void 0 ? DEBUG_SCOPES : [`${name}:*`, ...DEBUG_SCOPES]).join(",");
316
387
  }
317
388
  var DEBOUNCE_MS = 150;
318
- var SHUTDOWN_TIMEOUT_MS = 5e3;
389
+ function watchSources(root, onChange) {
390
+ let pending = null;
391
+ for (const target of WATCHED) {
392
+ const watched = _path2.default.join(root, target);
393
+ if (!_fs2.default.existsSync(watched)) {
394
+ continue;
395
+ }
396
+ _fs2.default.watch(watched, { recursive: true }, () => {
397
+ if (pending) {
398
+ clearTimeout(pending);
399
+ }
400
+ pending = setTimeout(onChange, DEBOUNCE_MS);
401
+ });
402
+ }
403
+ }
319
404
  function facts(root) {
320
405
  return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
321
406
  try {
@@ -338,52 +423,10 @@ function devApp(options) {
338
423
  return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
339
424
  _chunkA6ZPAM6Zcjs.logger.setDebug(true);
340
425
  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
426
  let building = false;
345
427
  let dirty = false;
346
428
  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
- }
429
+ const app = appProcess(_path2.default.join(root, SERVER_ENTRY));
387
430
  function show(lines) {
388
431
  process.stdout.write(`${lines.join("\n")}
389
432
  `);
@@ -402,26 +445,27 @@ function devApp(options) {
402
445
  show(helpLines());
403
446
  }
404
447
  if (key === "q") {
405
- void stop().then(() => process.exit(0));
448
+ void app.stop().then(() => process.exit(0));
406
449
  }
407
450
  });
408
451
  }
409
452
  function rebuild() {
410
453
  return _chunkT5QVCXVBcjs.__async.call(void 0, this, null, function* () {
454
+ var _a;
411
455
  if (building) {
412
456
  dirty = true;
413
- return false;
457
+ return null;
414
458
  }
415
459
  building = true;
416
- let built2 = false;
460
+ let built = null;
417
461
  try {
418
462
  do {
419
463
  dirty = false;
420
- yield stop();
464
+ yield app.stop();
421
465
  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;
466
+ const result = yield buildApp(_chunkT5QVCXVBcjs.__spreadProps.call(void 0, _chunkT5QVCXVBcjs.__spreadValues.call(void 0, {}, options), { mode: "development" }));
467
+ app.start({ DEBUG: (_a = process.env["DEBUG"]) != null ? _a : debugPattern((yield describe()).name) });
468
+ built = result;
425
469
  } catch (error) {
426
470
  _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
471
  }
@@ -429,38 +473,25 @@ function devApp(options) {
429
473
  } finally {
430
474
  building = false;
431
475
  }
432
- return built2;
476
+ return built;
433
477
  });
434
478
  }
435
479
  const startedAt = Date.now();
436
- const built = yield rebuild();
480
+ const address = yield rebuild();
437
481
  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())));
482
+ watchSources(root, () => void rebuild());
483
+ if (address !== null) {
484
+ show(
485
+ bannerLines(_chunkT5QVCXVBcjs.__spreadValues.call(void 0, {
486
+ base: address.base,
487
+ elapsedMs: elapsed,
488
+ mode: "development",
489
+ port: address.port,
490
+ watched: WATCHED
491
+ }, yield describe()))
492
+ );
452
493
  }
453
494
  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
495
  });
465
496
  }
466
497
 
package/island/index.cjs CHANGED
@@ -37,6 +37,19 @@ function onIslandNavigate(node, onNavigate) {
37
37
  node.removeEventListener(NAVIGATE_EVENT, listener);
38
38
  };
39
39
  }
40
+ var DOCUMENT_EVENT = "den:document";
41
+ function publishIslandDocument(node, contribution) {
42
+ node.dispatchEvent(new CustomEvent(DOCUMENT_EVENT, { bubbles: true, detail: contribution }));
43
+ }
44
+ function onIslandDocument(node, onDocument) {
45
+ const listener = (event) => {
46
+ onDocument(event.detail);
47
+ };
48
+ node.addEventListener(DOCUMENT_EVENT, listener);
49
+ return () => {
50
+ node.removeEventListener(DOCUMENT_EVENT, listener);
51
+ };
52
+ }
40
53
 
41
54
  // src/island/slot-address.ts
42
55
  function slotAddress(app, slot) {
@@ -106,4 +119,7 @@ function describeIsland(descriptor, denState) {
106
119
 
107
120
 
108
121
 
109
- exports.NAVIGATE_EVENT = NAVIGATE_EVENT; exports.ViewLayerToken = _chunkXOJ2VWX4cjs.ViewLayerToken; exports.activateIslandViewModels = _chunkZOU6W6ZQcjs.activateIslandViewModels; exports.collectDocumentContribution = _chunkZOU6W6ZQcjs.collectDocumentContribution; exports.collectIslandState = _chunkZOU6W6ZQcjs.collectIslandState; exports.createIslandContainer = _chunkZOU6W6ZQcjs.createIslandContainer; exports.describeIsland = describeIsland; exports.getAppContainer = _chunkZOU6W6ZQcjs.getAppContainer; exports.getIslandsSnapshot = getIslandsSnapshot; exports.hydrateIslandState = _chunkZOU6W6ZQcjs.hydrateIslandState; exports.isDocumentSource = _chunkZOU6W6ZQcjs.isDocumentSource; exports.matchRoute = _chunkZOU6W6ZQcjs.matchRoute; exports.navigateIsland = navigateIsland; exports.normalizeRoutes = _chunkZOU6W6ZQcjs.normalizeRoutes; exports.onIslandNavigate = onIslandNavigate; exports.pathPattern = _chunkZOU6W6ZQcjs.pathPattern; exports.registerIsland = registerIsland; exports.renderApp = _chunkZOU6W6ZQcjs.renderApp; exports.resolveEagerUnits = _chunkZOU6W6ZQcjs.resolveEagerUnits; exports.resolveViewAdapter = _chunkXOJ2VWX4cjs.resolveViewAdapter; exports.slotAddress = slotAddress; exports.subscribeIslands = subscribeIslands; exports.viewAdapterOf = _chunkXOJ2VWX4cjs.viewAdapterOf;
122
+
123
+
124
+
125
+ exports.DOCUMENT_EVENT = DOCUMENT_EVENT; exports.NAVIGATE_EVENT = NAVIGATE_EVENT; exports.ViewLayerToken = _chunkXOJ2VWX4cjs.ViewLayerToken; exports.activateIslandViewModels = _chunkZOU6W6ZQcjs.activateIslandViewModels; exports.collectDocumentContribution = _chunkZOU6W6ZQcjs.collectDocumentContribution; exports.collectIslandState = _chunkZOU6W6ZQcjs.collectIslandState; exports.createIslandContainer = _chunkZOU6W6ZQcjs.createIslandContainer; exports.describeIsland = describeIsland; exports.getAppContainer = _chunkZOU6W6ZQcjs.getAppContainer; exports.getIslandsSnapshot = getIslandsSnapshot; exports.hydrateIslandState = _chunkZOU6W6ZQcjs.hydrateIslandState; exports.isDocumentSource = _chunkZOU6W6ZQcjs.isDocumentSource; exports.matchRoute = _chunkZOU6W6ZQcjs.matchRoute; exports.navigateIsland = navigateIsland; exports.normalizeRoutes = _chunkZOU6W6ZQcjs.normalizeRoutes; exports.onIslandDocument = onIslandDocument; exports.onIslandNavigate = onIslandNavigate; exports.pathPattern = _chunkZOU6W6ZQcjs.pathPattern; exports.publishIslandDocument = publishIslandDocument; exports.registerIsland = registerIsland; exports.renderApp = _chunkZOU6W6ZQcjs.renderApp; exports.resolveEagerUnits = _chunkZOU6W6ZQcjs.resolveEagerUnits; exports.resolveViewAdapter = _chunkXOJ2VWX4cjs.resolveViewAdapter; exports.slotAddress = slotAddress; exports.subscribeIslands = subscribeIslands; exports.viewAdapterOf = _chunkXOJ2VWX4cjs.viewAdapterOf;
@@ -182,6 +182,26 @@ declare function navigateIsland(node: EventTarget, route: IslandRoute): void;
182
182
  * @returns Отписка
183
183
  */
184
184
  declare function onIslandNavigate(node: EventTarget, onNavigate: (route: IslandRoute) => void): () => void;
185
+ /** Имя события, которым приложение сообщает свой вклад в документ. */
186
+ declare const DOCUMENT_EVENT = "den:document";
187
+ /**
188
+ * Сообщает хосту вклад приложения в документ.
189
+ *
190
+ * Зовётся ПОСЛЕ активации: вклад считается по состоянию VM, а состояние нового адреса приезжает
191
+ * асинхронно. Событие всплывает — хост слушает документ одним слушателем и не ведёт учёт узлов.
192
+ *
193
+ * @param node - Узел приложения
194
+ * @param contribution - Вклад в документ, собранный движком
195
+ */
196
+ declare function publishIslandDocument(node: EventTarget, contribution: DocumentContribution): void;
197
+ /**
198
+ * Подписывает хост на вклад приложений в документ.
199
+ *
200
+ * @param node - Узел, который слышит всплывающие события (обычно документ)
201
+ * @param onDocument - Что делать с вкладом
202
+ * @returns Отписка
203
+ */
204
+ declare function onIslandDocument(node: EventTarget, onDocument: (contribution: DocumentContribution) => void): () => void;
185
205
 
186
206
  /** Запрос на рендер приложения. */
187
207
  interface RenderAppRequest {
@@ -297,4 +317,4 @@ declare function getAppContainer(): Container;
297
317
  */
298
318
  declare function slotAddress(app: string, slot: string): string;
299
319
 
300
- export { AppAssets, type CreateIslandContainerOptions, type DocumentContribution, type DocumentHead, type DocumentSource, IslandDescriptor, type IslandRecord, type IslandRoute, NAVIGATE_EVENT, type RenderAppRequest, type RenderAppResult, activateIslandViewModels, collectDocumentContribution, collectIslandState, createIslandContainer, describeIsland, getAppContainer, getIslandsSnapshot, hydrateIslandState, isDocumentSource, navigateIsland, onIslandNavigate, registerIsland, renderApp, resolveEagerUnits, slotAddress, subscribeIslands };
320
+ export { AppAssets, type CreateIslandContainerOptions, DOCUMENT_EVENT, type DocumentContribution, type DocumentHead, type DocumentSource, IslandDescriptor, type IslandRecord, type IslandRoute, NAVIGATE_EVENT, type RenderAppRequest, type RenderAppResult, activateIslandViewModels, collectDocumentContribution, collectIslandState, createIslandContainer, describeIsland, getAppContainer, getIslandsSnapshot, hydrateIslandState, isDocumentSource, navigateIsland, onIslandDocument, onIslandNavigate, publishIslandDocument, registerIsland, renderApp, resolveEagerUnits, slotAddress, subscribeIslands };
package/island/index.d.ts CHANGED
@@ -182,6 +182,26 @@ declare function navigateIsland(node: EventTarget, route: IslandRoute): void;
182
182
  * @returns Отписка
183
183
  */
184
184
  declare function onIslandNavigate(node: EventTarget, onNavigate: (route: IslandRoute) => void): () => void;
185
+ /** Имя события, которым приложение сообщает свой вклад в документ. */
186
+ declare const DOCUMENT_EVENT = "den:document";
187
+ /**
188
+ * Сообщает хосту вклад приложения в документ.
189
+ *
190
+ * Зовётся ПОСЛЕ активации: вклад считается по состоянию VM, а состояние нового адреса приезжает
191
+ * асинхронно. Событие всплывает — хост слушает документ одним слушателем и не ведёт учёт узлов.
192
+ *
193
+ * @param node - Узел приложения
194
+ * @param contribution - Вклад в документ, собранный движком
195
+ */
196
+ declare function publishIslandDocument(node: EventTarget, contribution: DocumentContribution): void;
197
+ /**
198
+ * Подписывает хост на вклад приложений в документ.
199
+ *
200
+ * @param node - Узел, который слышит всплывающие события (обычно документ)
201
+ * @param onDocument - Что делать с вкладом
202
+ * @returns Отписка
203
+ */
204
+ declare function onIslandDocument(node: EventTarget, onDocument: (contribution: DocumentContribution) => void): () => void;
185
205
 
186
206
  /** Запрос на рендер приложения. */
187
207
  interface RenderAppRequest {
@@ -297,4 +317,4 @@ declare function getAppContainer(): Container;
297
317
  */
298
318
  declare function slotAddress(app: string, slot: string): string;
299
319
 
300
- export { AppAssets, type CreateIslandContainerOptions, type DocumentContribution, type DocumentHead, type DocumentSource, IslandDescriptor, type IslandRecord, type IslandRoute, NAVIGATE_EVENT, type RenderAppRequest, type RenderAppResult, activateIslandViewModels, collectDocumentContribution, collectIslandState, createIslandContainer, describeIsland, getAppContainer, getIslandsSnapshot, hydrateIslandState, isDocumentSource, navigateIsland, onIslandNavigate, registerIsland, renderApp, resolveEagerUnits, slotAddress, subscribeIslands };
320
+ export { AppAssets, type CreateIslandContainerOptions, DOCUMENT_EVENT, type DocumentContribution, type DocumentHead, type DocumentSource, IslandDescriptor, type IslandRecord, type IslandRoute, NAVIGATE_EVENT, type RenderAppRequest, type RenderAppResult, activateIslandViewModels, collectDocumentContribution, collectIslandState, createIslandContainer, describeIsland, getAppContainer, getIslandsSnapshot, hydrateIslandState, isDocumentSource, navigateIsland, onIslandDocument, onIslandNavigate, publishIslandDocument, registerIsland, renderApp, resolveEagerUnits, slotAddress, subscribeIslands };
package/island/index.js CHANGED
@@ -37,6 +37,19 @@ function onIslandNavigate(node, onNavigate) {
37
37
  node.removeEventListener(NAVIGATE_EVENT, listener);
38
38
  };
39
39
  }
40
+ var DOCUMENT_EVENT = "den:document";
41
+ function publishIslandDocument(node, contribution) {
42
+ node.dispatchEvent(new CustomEvent(DOCUMENT_EVENT, { bubbles: true, detail: contribution }));
43
+ }
44
+ function onIslandDocument(node, onDocument) {
45
+ const listener = (event) => {
46
+ onDocument(event.detail);
47
+ };
48
+ node.addEventListener(DOCUMENT_EVENT, listener);
49
+ return () => {
50
+ node.removeEventListener(DOCUMENT_EVENT, listener);
51
+ };
52
+ }
40
53
 
41
54
  // src/island/slot-address.ts
42
55
  function slotAddress(app, slot) {
@@ -83,6 +96,7 @@ function describeIsland(descriptor, denState) {
83
96
  };
84
97
  }
85
98
  export {
99
+ DOCUMENT_EVENT,
86
100
  NAVIGATE_EVENT,
87
101
  ViewLayerToken,
88
102
  activateIslandViewModels,
@@ -97,8 +111,10 @@ export {
97
111
  matchRoute,
98
112
  navigateIsland,
99
113
  normalizeRoutes,
114
+ onIslandDocument,
100
115
  onIslandNavigate,
101
116
  pathPattern,
117
+ publishIslandDocument,
102
118
  registerIsland,
103
119
  renderApp,
104
120
  resolveEagerUnits,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxford/den",
3
- "version": "3.1.3",
3
+ "version": "3.2.0",
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/ioc": "^1.1.2",
28
- "@foxford/logger": "^1.2.2"
27
+ "@foxford/logger": "^1.2.2",
28
+ "@foxford/ioc": "^1.1.2"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "vite": ">=5.0.0"
@@ -86,17 +86,17 @@
86
86
  "chunk-6FNC3XMI.js",
87
87
  "chunk-6QV4L6R2.cjs",
88
88
  "chunk-A6ZPAM6Z.cjs",
89
+ "chunk-BMI5HNGE.js",
89
90
  "chunk-C7BM4DGX.cjs",
90
91
  "chunk-E23E2VUC.js",
91
92
  "chunk-MCN4GFEQ.js",
93
+ "chunk-RRR2DJMN.cjs",
92
94
  "chunk-T5QVCXVB.cjs",
93
95
  "chunk-TPBI6TOU.js",
94
96
  "chunk-TPHJUDHG.js",
95
97
  "chunk-WBHHHICS.js",
96
98
  "chunk-XOJ2VWX4.cjs",
97
- "chunk-YECHY3NR.js",
98
99
  "chunk-Z5BL4DJA.cjs",
99
- "chunk-ZBB5SBEV.cjs",
100
100
  "chunk-ZOU6W6ZQ.cjs",
101
101
  "define-repository-CwAXed2X.d.cts",
102
102
  "define-repository-q_T4hqeP.d.ts",
@@ -125,6 +125,6 @@
125
125
  "view-adapter-B4gEb-ms.d.ts",
126
126
  "view-adapter-DJ7yB6Ml.d.cts"
127
127
  ],
128
- "sha": "c633e3b",
128
+ "sha": "c382a3d",
129
129
  "scripts": {}
130
130
  }
package/serve/index.cjs CHANGED
@@ -16,6 +16,52 @@ var _chunkT5QVCXVBcjs = require('../chunk-T5QVCXVB.cjs');
16
16
  var _fastify = require('fastify'); var _fastify2 = _interopRequireDefault(_fastify);
17
17
  var _logger = require('@foxford/logger');
18
18
 
19
+ // src/serve/assets.ts
20
+ var _fs = require('fs');
21
+ var _path = require('path'); var _path2 = _interopRequireDefault(_path);
22
+ var MIME = {
23
+ ".css": "text/css",
24
+ ".gif": "image/gif",
25
+ ".jpeg": "image/jpeg",
26
+ ".jpg": "image/jpeg",
27
+ ".js": "text/javascript",
28
+ ".json": "application/json",
29
+ ".map": "application/json",
30
+ ".mjs": "text/javascript",
31
+ ".png": "image/png",
32
+ ".svg": "image/svg+xml",
33
+ ".ttf": "font/ttf",
34
+ ".webp": "image/webp",
35
+ ".woff": "font/woff",
36
+ ".woff2": "font/woff2"
37
+ };
38
+ var HASHED_DIR = "assets/";
39
+ function assetsPrefix(base) {
40
+ const { pathname } = new URL(base, "http://assets.invalid");
41
+ return pathname.endsWith("/") ? pathname : `${pathname}/`;
42
+ }
43
+ function assetFile(dir, relative) {
44
+ const file = _path2.default.normalize(_path2.default.join(dir, relative));
45
+ return file.startsWith(_path2.default.normalize(dir)) ? file : null;
46
+ }
47
+ function registerAssets(instance, source) {
48
+ const dir = _path2.default.resolve(source.dir);
49
+ const prefix = assetsPrefix(source.base);
50
+ instance.get(`${prefix}*`, (request, reply) => {
51
+ var _a;
52
+ const relative = request.params["*"];
53
+ const file = assetFile(dir, relative);
54
+ if (file === null) {
55
+ return reply.code(404).send();
56
+ }
57
+ const stat = _fs.existsSync.call(void 0, file) ? _fs.statSync.call(void 0, file) : null;
58
+ if (stat === null || !stat.isFile()) {
59
+ return reply.code(404).send();
60
+ }
61
+ 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));
62
+ });
63
+ }
64
+
19
65
  // src/serve/plugin.ts
20
66
 
21
67
  var DEFAULT_SLOW_RENDER_MS = 1e3;
@@ -106,6 +152,9 @@ function serveApp(_0) {
106
152
  const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
107
153
  const instance = _fastify2.default.call(void 0, { logger: false });
108
154
  yield instance.register(appPlugin, { app, slowRenderMs: options.slowRenderMs });
155
+ if (options.assets) {
156
+ registerAssets(instance, options.assets);
157
+ }
109
158
  instance.addHook("onRequest", (req, _reply, done) => {
110
159
  serveLog.debug(`\u2192 ${req.method} ${req.url}`);
111
160
  done();
@@ -134,4 +183,5 @@ function serveApp(_0) {
134
183
 
135
184
 
136
185
 
137
- exports.appPlugin = appPlugin; exports.serveApp = serveApp;
186
+
187
+ exports.appPlugin = appPlugin; 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>;
@@ -44,6 +59,13 @@ interface ServeAppOptions {
44
59
  * который именно так и просит его уйти, а незакрытый инстанс рвёт запросы на середине.
45
60
  */
46
61
  gracefulShutdown?: boolean;
62
+ /**
63
+ * Клиентские артефакты приложения: префикс адресов и каталог выхода.
64
+ *
65
+ * Отдаёт их само приложение — адреса знает его сборка, а хост получает только ссылки. В
66
+ * проде перед процессом стоит CDN, и до него эти адреса не доходят.
67
+ */
68
+ assets?: AppAssetsSource;
47
69
  }
48
70
  /**
49
71
  * Поднимает приложение отдельным сервисом.
@@ -57,4 +79,4 @@ interface ServeAppOptions {
57
79
  */
58
80
  declare function serveApp(app: ServableApp, options?: ServeAppOptions): Promise<FastifyInstance>;
59
81
 
60
- export { type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, serveApp };
82
+ export { type AppAssetsSource, type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, 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>;
@@ -44,6 +59,13 @@ interface ServeAppOptions {
44
59
  * который именно так и просит его уйти, а незакрытый инстанс рвёт запросы на середине.
45
60
  */
46
61
  gracefulShutdown?: boolean;
62
+ /**
63
+ * Клиентские артефакты приложения: префикс адресов и каталог выхода.
64
+ *
65
+ * Отдаёт их само приложение — адреса знает его сборка, а хост получает только ссылки. В
66
+ * проде перед процессом стоит CDN, и до него эти адреса не доходят.
67
+ */
68
+ assets?: AppAssetsSource;
47
69
  }
48
70
  /**
49
71
  * Поднимает приложение отдельным сервисом.
@@ -57,4 +79,4 @@ interface ServeAppOptions {
57
79
  */
58
80
  declare function serveApp(app: ServableApp, options?: ServeAppOptions): Promise<FastifyInstance>;
59
81
 
60
- export { type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, serveApp };
82
+ export { type AppAssetsSource, type AppPluginOptions, type ServableApp, type ServeAppOptions, appPlugin, registerAssets, serveApp };
package/serve/index.js CHANGED
@@ -16,6 +16,52 @@ import {
16
16
  import Fastify from "fastify";
17
17
  import { log as log2 } from "@foxford/logger";
18
18
 
19
+ // src/serve/assets.ts
20
+ import { createReadStream, existsSync, statSync } from "fs";
21
+ import path from "path";
22
+ var MIME = {
23
+ ".css": "text/css",
24
+ ".gif": "image/gif",
25
+ ".jpeg": "image/jpeg",
26
+ ".jpg": "image/jpeg",
27
+ ".js": "text/javascript",
28
+ ".json": "application/json",
29
+ ".map": "application/json",
30
+ ".mjs": "text/javascript",
31
+ ".png": "image/png",
32
+ ".svg": "image/svg+xml",
33
+ ".ttf": "font/ttf",
34
+ ".webp": "image/webp",
35
+ ".woff": "font/woff",
36
+ ".woff2": "font/woff2"
37
+ };
38
+ var HASHED_DIR = "assets/";
39
+ function assetsPrefix(base) {
40
+ const { pathname } = new URL(base, "http://assets.invalid");
41
+ return pathname.endsWith("/") ? pathname : `${pathname}/`;
42
+ }
43
+ function assetFile(dir, relative) {
44
+ const file = path.normalize(path.join(dir, relative));
45
+ return file.startsWith(path.normalize(dir)) ? file : null;
46
+ }
47
+ function registerAssets(instance, source) {
48
+ const dir = path.resolve(source.dir);
49
+ const prefix = assetsPrefix(source.base);
50
+ instance.get(`${prefix}*`, (request, reply) => {
51
+ var _a;
52
+ const relative = request.params["*"];
53
+ const file = assetFile(dir, relative);
54
+ if (file === null) {
55
+ return reply.code(404).send();
56
+ }
57
+ const stat = existsSync(file) ? statSync(file) : null;
58
+ if (stat === null || !stat.isFile()) {
59
+ return reply.code(404).send();
60
+ }
61
+ 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));
62
+ });
63
+ }
64
+
19
65
  // src/serve/plugin.ts
20
66
  import { log } from "@foxford/logger";
21
67
  var DEFAULT_SLOW_RENDER_MS = 1e3;
@@ -106,6 +152,9 @@ function serveApp(_0) {
106
152
  const serveLog = (_b = appLog.getLogger("serve")) != null ? _b : appLog;
107
153
  const instance = Fastify({ logger: false });
108
154
  yield instance.register(appPlugin, { app, slowRenderMs: options.slowRenderMs });
155
+ if (options.assets) {
156
+ registerAssets(instance, options.assets);
157
+ }
109
158
  instance.addHook("onRequest", (req, _reply, done) => {
110
159
  serveLog.debug(`\u2192 ${req.method} ${req.url}`);
111
160
  done();
@@ -133,5 +182,6 @@ function serveApp(_0) {
133
182
  }
134
183
  export {
135
184
  appPlugin,
185
+ registerAssets,
136
186
  serveApp
137
187
  };