vimp-engine 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vimp-engine",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "VIMP — движок-приложение (мастер, P2P-транспорт, Worker-хост, мета, MVC-каркас клиента)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,5 +1,6 @@
1
1
  import Publisher from '../../../lib/Publisher.js';
2
- import { ENGINE_VERSION } from '../../lib/engineVersion.js';
2
+ import { ENGINE_PACKAGE, ENGINE_VERSION } from '../../lib/engineVersion.js';
3
+ import { renderPackageLink } from '../../lib/footerLink.js';
3
4
 
4
5
  // Singleton LobbyView
5
6
 
@@ -35,14 +36,16 @@ export default class LobbyView {
35
36
  this._leaderboardTotal = document.getElementById(elems.leaderboardTotalId);
36
37
  this._myPlacement = document.getElementById(elems.myPlacementId);
37
38
 
38
- // футер лобби: строка статична на всё время жизни вкладки, поэтому
39
+ // футер лобби: содержимое статично на всё время жизни вкладки, поэтому
39
40
  // пишется здесь, а не через модель и publisher
40
41
  const version = document.getElementById(elems.versionId);
41
42
 
42
43
  if (version) {
43
- version.textContent = `vimp-engine ${ENGINE_VERSION}`;
44
+ version.textContent = ENGINE_VERSION;
44
45
  }
45
46
 
47
+ renderPackageLink(document.getElementById(elems.linkId), ENGINE_PACKAGE);
48
+
46
49
  // заголовок игры для "<TITLE> TOP-N" (SVG-ориентир) — задаётся controller'ом
47
50
  // при выборе игры (сама модель не хранит title манифеста)
48
51
  this._gameTitle = '';
@@ -7,5 +7,13 @@
7
7
  // бандлу (в npm-тарболле vimp-engine package.json есть всегда, поэтому
8
8
  // standalone SDK собирается у игры тем же импортом).
9
9
  import pkg from '../../../package.json' with { type: 'json' };
10
+ import { homepageOf } from '../../lib/packageLink.js';
10
11
 
11
12
  export const ENGINE_VERSION = pkg.version;
13
+
14
+ // метаданные для ссылки в футере лобби — та же форма, что мастер отдаёт про
15
+ // пакет игры (GameCatalog), чтобы оба футера строил один resolvePackageLink
16
+ export const ENGINE_PACKAGE = {
17
+ name: pkg.name,
18
+ homepage: homepageOf(pkg),
19
+ };
@@ -0,0 +1,29 @@
1
+ import { resolvePackageLink } from '../../lib/packageLink.js';
2
+
3
+ // Ссылка в футере лобби и формы входа. Один рендер на оба экрана: лобби
4
+ // подставляет пакет движка, форма входа — пакет игры из манифеста.
5
+ //
6
+ // Данных нет (standalone-манифест без метаданных пакета) — якорь гасится, а не
7
+ // прячется: ячейка футера остаётся пустой, и раскладка space-between не едет.
8
+
9
+ /**
10
+ * @param {HTMLAnchorElement|null} anchor - якорь футера
11
+ * @param {{name?: string, homepage?: string}|null} pkg - метаданные пакета
12
+ */
13
+ export function renderPackageLink(anchor, pkg) {
14
+ if (!anchor) {
15
+ return;
16
+ }
17
+
18
+ const link = resolvePackageLink(pkg);
19
+
20
+ if (!link) {
21
+ anchor.textContent = '';
22
+ anchor.removeAttribute('href');
23
+
24
+ return;
25
+ }
26
+
27
+ anchor.textContent = link.label;
28
+ anchor.href = link.url;
29
+ }
@@ -28,6 +28,7 @@ import VoteView from './components/view/Vote.js';
28
28
  import VoteCtrl from './components/controller/Vote.js';
29
29
  import { buildForm, mergeRoomDefaults, bindLiveErrors } from './lib/formBuilder.js';
30
30
  import { normalizeAuthParams } from './lib/authParams.js';
31
+ import { renderPackageLink } from './lib/footerLink.js';
31
32
  import { createGameActivator } from './lib/gameActivator.js';
32
33
  import createAutostart from './lib/autostart.js';
33
34
  import { getBootConfig, resolveBootConfig } from './boot.js';
@@ -422,18 +423,22 @@ socketMethods[PS_AUTH_DATA] = data => {
422
423
 
423
424
  document.getElementById('logo').textContent = texts?.title || 'VIMP';
424
425
 
425
- // версия пакета игры в футере формы входа. Здесь, а не в AuthView: его
426
- // elems приходят из authSchema.elems игрового плагина, и футер каркаса
427
- // не его забота. Игра, собранная до появления packageVersion, оставляет
428
- // строку пустой (#auth-link держит раскладку через space-between)
426
+ // футер формы входа: версия и ссылка пакета ИГРЫ. Здесь, а не в AuthView:
427
+ // его elems приходят из authSchema.elems игрового плагина, и футер каркаса
428
+ // не его забота. Метаданные кладёт в манифест мастер (GameCatalog читает
429
+ // package.json пакета); в standalone-манифесте их может не быть — тогда
430
+ // ячейки пустые, раскладку space-between это не ломает
429
431
  const authVersion = document.getElementById('auth-version');
430
432
 
431
433
  if (authVersion) {
432
- const packageVersion = activeGameManifest?.packageVersion;
433
-
434
- authVersion.textContent = packageVersion ? `v${packageVersion}` : '';
434
+ authVersion.textContent = activeGameManifest?.packageVersion ?? '';
435
435
  }
436
436
 
437
+ renderPackageLink(document.getElementById('auth-package-link'), {
438
+ name: activeGameManifest?.packageName,
439
+ homepage: activeGameManifest?.packageHomepage,
440
+ });
441
+
437
442
  // память клиента (localStorage) + принудительное значение поля с
438
443
  // единственным вариантом. Здесь, а не в AuthView: solo-путь ниже отвечает
439
444
  // хосту вообще без формы и обязан прийти к тем же значениям
@@ -473,7 +473,12 @@ body.hide-cursor {
473
473
  margin: 4px auto;
474
474
  }
475
475
 
476
- #auth-link {
476
+ /* футеры лобби и формы входа: одна и та же полоса из трёх ячеек —
477
+ ссылка на пакет, его версия, копирайт. Оба заполняет JS, поэтому вид
478
+ держится в одном месте (#lobby-footer лежит в секции lobby ниже только
479
+ разметкой) */
480
+ #auth-link,
481
+ #lobby-footer {
477
482
  font-size: 9px;
478
483
  height: 15px;
479
484
  margin: 0;
@@ -483,26 +488,31 @@ body.hide-cursor {
483
488
  justify-content: space-between;
484
489
  }
485
490
 
486
- #auth-link p {
491
+ #auth-link p,
492
+ #lobby-footer p {
487
493
  margin: 0;
488
494
  }
489
495
 
490
- #auth-link p a {
496
+ #auth-link p a,
497
+ #lobby-footer p a {
491
498
  color: #ff0;
492
499
  }
493
500
 
494
- /* три колонки: GitHub версия игры копирайт. Позициями заведует
495
- space-between, а не nth-child: средний элемент бывает пустым (игра
496
- собрана без packageVersion), и порядковые правила тогда съезжали бы */
497
- #auth-link p:first-child {
501
+ /* позициями заведует space-between, а не nth-child: любая из ячеек бывает
502
+ пустой пакета игры может не быть метаданных), и порядковые правила
503
+ тогда съезжали бы */
504
+ #auth-link p:first-child,
505
+ #lobby-footer p:first-child {
498
506
  text-align: left;
499
507
  }
500
508
 
501
- #auth-version {
509
+ #auth-version,
510
+ #lobby-version {
502
511
  text-align: center;
503
512
  }
504
513
 
505
- #auth-link p:last-child {
514
+ #auth-link p:last-child,
515
+ #lobby-footer p:last-child {
506
516
  text-align: right;
507
517
  }
508
518
 
@@ -783,18 +793,10 @@ body.hide-cursor {
783
793
  flex-shrink: 0;
784
794
  }
785
795
 
786
- /* версия пакета движка под панелями тем же мелким кеглем, что и футер
787
- формы входа (#auth-link) */
796
+ /* полосу футера рисуют общие с #auth-link правила в секции auth; здесь
797
+ только отступ от панелей */
788
798
  #lobby-footer {
789
- font-size: 9px;
790
- padding: 0 5px;
791
799
  margin: 6px 0 10px;
792
- text-align: center;
793
- text-shadow: none;
794
- }
795
-
796
- #lobby-footer p {
797
- margin: 0;
798
800
  }
799
801
 
800
802
  @media (max-width: 800px) {
@@ -55,18 +55,9 @@ const SHELL = [
55
55
  tag: 'div',
56
56
  id: 'auth-link',
57
57
  children: [
58
- {
59
- tag: 'p',
60
- children: [
61
- {
62
- tag: 'a',
63
- attrs: { href: 'https://github.com/lgick/vimp' },
64
- text: 'GitHub',
65
- },
66
- ],
67
- },
68
- // версия пакета игры (manifest.packageVersion) — текст пишет
69
- // main.js по AUTH_DATA
58
+ // ссылка и версия пакета ИГРЫ — заполняет main.js по AUTH_DATA
59
+ // из метаданных пакета в манифесте
60
+ { tag: 'p', children: [{ tag: 'a', id: 'auth-package-link' }] },
70
61
  { tag: 'p', id: 'auth-version' },
71
62
  { tag: 'p', text: `© ${new Date().getFullYear()} VIMP` },
72
63
  ],
@@ -15,9 +15,10 @@ div#auth
15
15
  div#auth-informs
16
16
 
17
17
  div#auth-link
18
+ //- ссылка и версия пакета ИГРЫ (мастер кладёт их в манифест, читая
19
+ //- package.json пакета) — проставляет main.js по AUTH_DATA; без данных
20
+ //- ячейки остаются пустыми
18
21
  p
19
- a(href='https://github.com/lgick/vimp') GitHub
20
- //- версия пакета игры (manifest.packageVersion): проставляет main.js,
21
- //- у игры, собранной до появления поля, строка остаётся пустой
22
+ a#auth-package-link
22
23
  p#auth-version
23
24
  p &copy; #{new Date().getFullYear()} VIMP
@@ -55,6 +55,10 @@ div#lobby(style='display:none')
55
55
  span#leaderboard-total
56
56
  ol#lobby-leaderboard-list
57
57
  p#lobby-my-placement
58
- //- футер: версию пакета движка проставляет LobbyView (engineVersion.js)
58
+ //- футер: ссылку и версию пакета движка проставляет LobbyView
59
+ //- (engineVersion.js + lib/packageLink.js)
59
60
  div#lobby-footer
61
+ p
62
+ a#lobby-link
60
63
  p#lobby-version
64
+ p &copy; #{new Date().getFullYear()} VIMP
@@ -98,8 +98,9 @@ export default {
98
98
  myPlacementId: 'lobby-my-placement',
99
99
 
100
100
  // футер: версия npm-пакета движка, запечённая в бандл при сборке
101
- // (client/lib/engineVersion.js)
101
+ // (client/lib/engineVersion.js), и ссылка на его страницу
102
102
  versionId: 'lobby-version',
103
+ linkId: 'lobby-link',
103
104
  },
104
105
 
105
106
  // создание комнаты (хост в этой же вкладке); лимит игроков/время
@@ -0,0 +1,126 @@
1
+ // Ссылка на пакет для футеров лобби и формы входа: движок показывает рядом
2
+ // npm-версию, и клик по соседней ячейке должен вести на страницу этого пакета.
3
+ //
4
+ // Источник — `homepage`/`repository` его package.json, но объявлены они не у
5
+ // всех (у @vimp-games/snakes нет ни того, ни другого), а footer без ссылки
6
+ // выглядит поломкой. Поэтому фолбэк — страница пакета на npm: она выводится из
7
+ // одного лишь имени, то есть существует всегда, и сама ведёт на репозиторий,
8
+ // если он объявлен.
9
+ //
10
+ // Модуль изоморфный: мастер зовёт homepageOf для пакета игры (GameCatalog),
11
+ // клиент — для собственного package.json движка (client/lib/engineVersion.js).
12
+
13
+ const NPM_PACKAGE_BASE = 'https://www.npmjs.com/package/';
14
+
15
+ /**
16
+ * Сырой адрес проекта из package.json: homepage, иначе repository.
17
+ * @param {Object} pkg - разобранный package.json
18
+ * @returns {string|null} адрес как объявлен (нормализует resolvePackageLink)
19
+ */
20
+ export function homepageOf(pkg) {
21
+ if (!pkg || typeof pkg !== 'object') {
22
+ return null;
23
+ }
24
+
25
+ const { homepage, repository } = pkg;
26
+
27
+ if (typeof homepage === 'string' && homepage.trim()) {
28
+ return homepage;
29
+ }
30
+
31
+ // repository бывает и строкой ("github:user/repo"), и объектом {type, url}
32
+ if (typeof repository === 'string') {
33
+ return repository;
34
+ }
35
+
36
+ if (repository && typeof repository.url === 'string') {
37
+ return repository.url;
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Ссылка футера по метаданным пакета.
45
+ * @param {{name?: string, homepage?: string}} [pkg] - имя пакета и сырой homepage
46
+ * @returns {{url: string, label: string}|null} null, если нет даже имени
47
+ */
48
+ export function resolvePackageLink(pkg) {
49
+ const { name, homepage } = pkg ?? {};
50
+ const declared = normalize(homepage);
51
+
52
+ if (declared) {
53
+ return { url: declared, label: labelFor(declared) };
54
+ }
55
+
56
+ if (typeof name === 'string' && name.trim()) {
57
+ return { url: `${NPM_PACKAGE_BASE}${name.trim()}`, label: 'npm' };
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ // приводит объявленный адрес к https-виду; всё, что после нормализации не
64
+ // http(s) (git@ без известной формы, file:, мусор), — не ссылка, а фолбэк
65
+ function normalize(raw) {
66
+ if (typeof raw !== 'string') {
67
+ return null;
68
+ }
69
+
70
+ let url = raw.trim();
71
+
72
+ if (!url) {
73
+ return null;
74
+ }
75
+
76
+ // npm-шорткаты: "github:user/repo", "gitlab:...", "bitbucket:..."
77
+ const shorthand = /^(github|gitlab|bitbucket):([\w.-]+\/[\w.-]+)$/.exec(url);
78
+
79
+ if (shorthand) {
80
+ const hosts = {
81
+ github: 'github.com',
82
+ gitlab: 'gitlab.com',
83
+ bitbucket: 'bitbucket.org',
84
+ };
85
+
86
+ return `https://${hosts[shorthand[1]]}/${shorthand[2]}`;
87
+ }
88
+
89
+ url = url.replace(/^git\+/, '');
90
+
91
+ // scp-форма git@host:user/repo — двоеточие здесь разделитель, не порт
92
+ const scp = /^git@([^:/]+):(.+)$/.exec(url);
93
+
94
+ if (scp) {
95
+ url = `https://${scp[1]}/${scp[2]}`;
96
+ }
97
+
98
+ url = url
99
+ .replace(/^ssh:\/\/git@/, 'https://')
100
+ .replace(/^ssh:\/\//, 'https://')
101
+ .replace(/^git:\/\//, 'https://');
102
+
103
+ if (!/^https?:\/\//.test(url)) {
104
+ return null;
105
+ }
106
+
107
+ return (
108
+ url
109
+ .replace(/\.git(?=$|[#?])/, '')
110
+ // npm сам дописывает "#readme" в homepage при генерации package.json
111
+ .replace(/#readme$/, '')
112
+ .replace(/\/+$/, '')
113
+ );
114
+ }
115
+
116
+ function labelFor(url) {
117
+ if (/^https?:\/\/(www\.)?github\.com\//.test(url)) {
118
+ return 'GitHub';
119
+ }
120
+
121
+ if (url.startsWith(NPM_PACKAGE_BASE)) {
122
+ return 'npm';
123
+ }
124
+
125
+ return 'Website';
126
+ }