vimp-engine 0.15.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/core/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/client/components/view/Lobby.js +12 -0
- package/src/client/lib/engineVersion.js +19 -0
- package/src/client/lib/footerLink.js +29 -0
- package/src/client/main.js +17 -0
- package/src/client/style.css +42 -5
- package/src/client/views/gameShell.js +4 -10
- package/src/client/views/includes/auth.pug +5 -1
- package/src/client/views/includes/lobby.pug +61 -51
- package/src/config/lobby.js +5 -0
- package/src/lib/packageLink.js +126 -0
package/core/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "vimp-engine-core"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.4"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "VIMP — движковый каркас симуляции (физика, карта, снапшот-фрейминг, интерполяция/предикт/raycast-примитивы, нав-утилиты). Без wasm-bindgen — обёртки для WASM-ABI собирает game-crate."
|
|
6
6
|
license = "MIT"
|
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import Publisher from '../../../lib/Publisher.js';
|
|
2
|
+
import { ENGINE_PACKAGE, ENGINE_VERSION } from '../../lib/engineVersion.js';
|
|
3
|
+
import { renderPackageLink } from '../../lib/footerLink.js';
|
|
2
4
|
|
|
3
5
|
// Singleton LobbyView
|
|
4
6
|
|
|
@@ -34,6 +36,16 @@ export default class LobbyView {
|
|
|
34
36
|
this._leaderboardTotal = document.getElementById(elems.leaderboardTotalId);
|
|
35
37
|
this._myPlacement = document.getElementById(elems.myPlacementId);
|
|
36
38
|
|
|
39
|
+
// футер лобби: содержимое статично на всё время жизни вкладки, поэтому
|
|
40
|
+
// пишется здесь, а не через модель и publisher
|
|
41
|
+
const version = document.getElementById(elems.versionId);
|
|
42
|
+
|
|
43
|
+
if (version) {
|
|
44
|
+
version.textContent = ENGINE_VERSION;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
renderPackageLink(document.getElementById(elems.linkId), ENGINE_PACKAGE);
|
|
48
|
+
|
|
37
49
|
// заголовок игры для "<TITLE> TOP-N" (SVG-ориентир) — задаётся controller'ом
|
|
38
50
|
// при выборе игры (сама модель не хранит title манифеста)
|
|
39
51
|
this._gameTitle = '';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Версия npm-пакета движка, запечённая в бандл на сборке: сети для неё нет
|
|
2
|
+
// (у мастера нет version-эндпоинта), а лобби обязано показывать версию того
|
|
3
|
+
// кода, который сейчас исполняется, а не того, что развёрнут на сервере.
|
|
4
|
+
//
|
|
5
|
+
// Модуль лежит в client/lib, а не в src/config: config/* импортирует мастер
|
|
6
|
+
// под Node, а сюда тянется package.json пакета — он нужен только браузерному
|
|
7
|
+
// бандлу (в npm-тарболле vimp-engine package.json есть всегда, поэтому
|
|
8
|
+
// standalone SDK собирается у игры тем же импортом).
|
|
9
|
+
import pkg from '../../../package.json' with { type: 'json' };
|
|
10
|
+
import { homepageOf } from '../../lib/packageLink.js';
|
|
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
|
+
}
|
package/src/client/main.js
CHANGED
|
@@ -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,6 +423,22 @@ socketMethods[PS_AUTH_DATA] = data => {
|
|
|
422
423
|
|
|
423
424
|
document.getElementById('logo').textContent = texts?.title || 'VIMP';
|
|
424
425
|
|
|
426
|
+
// футер формы входа: версия и ссылка пакета ИГРЫ. Здесь, а не в AuthView:
|
|
427
|
+
// его elems приходят из authSchema.elems игрового плагина, и футер каркаса
|
|
428
|
+
// — не его забота. Метаданные кладёт в манифест мастер (GameCatalog читает
|
|
429
|
+
// package.json пакета); в standalone-манифесте их может не быть — тогда
|
|
430
|
+
// ячейки пустые, раскладку space-between это не ломает
|
|
431
|
+
const authVersion = document.getElementById('auth-version');
|
|
432
|
+
|
|
433
|
+
if (authVersion) {
|
|
434
|
+
authVersion.textContent = activeGameManifest?.packageVersion ?? '';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
renderPackageLink(document.getElementById('auth-package-link'), {
|
|
438
|
+
name: activeGameManifest?.packageName,
|
|
439
|
+
homepage: activeGameManifest?.packageHomepage,
|
|
440
|
+
});
|
|
441
|
+
|
|
425
442
|
// память клиента (localStorage) + принудительное значение поля с
|
|
426
443
|
// единственным вариантом. Здесь, а не в AuthView: solo-путь ниже отвечает
|
|
427
444
|
// хосту вообще без формы и обязан прийти к тем же значениям
|
package/src/client/style.css
CHANGED
|
@@ -473,7 +473,12 @@ body.hide-cursor {
|
|
|
473
473
|
margin: 4px auto;
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
-
|
|
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,19 +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
|
-
|
|
501
|
+
/* позициями заведует space-between, а не nth-child: любая из ячеек бывает
|
|
502
|
+
пустой (у пакета игры может не быть метаданных), и порядковые правила
|
|
503
|
+
тогда съезжали бы */
|
|
504
|
+
#auth-link p:first-child,
|
|
505
|
+
#lobby-footer p:first-child {
|
|
495
506
|
text-align: left;
|
|
496
507
|
}
|
|
497
508
|
|
|
498
|
-
#auth-
|
|
509
|
+
#auth-version,
|
|
510
|
+
#lobby-version {
|
|
511
|
+
text-align: center;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#auth-link p:last-child,
|
|
515
|
+
#lobby-footer p:last-child {
|
|
499
516
|
text-align: right;
|
|
500
517
|
}
|
|
501
518
|
|
|
@@ -712,6 +729,14 @@ body.hide-cursor {
|
|
|
712
729
|
color: #aaa;
|
|
713
730
|
}
|
|
714
731
|
|
|
732
|
+
/* колоночная обёртка панелей и футера: #lobby центрирует содержимое как
|
|
733
|
+
флекс-строка, поэтому футер обязан лежать в одном столбце с .lobby-grid,
|
|
734
|
+
а не быть её соседом */
|
|
735
|
+
.lobby-column {
|
|
736
|
+
display: flex;
|
|
737
|
+
flex-direction: column;
|
|
738
|
+
}
|
|
739
|
+
|
|
715
740
|
/* два столбца лобби (lobby-page-plan): настройки/создание слева, вкладки
|
|
716
741
|
Active Servers / Leaderboard справа; собственная ширина панелей
|
|
717
742
|
переопределяет фиксированный .card { width:256px } (id сильнее класса);
|
|
@@ -768,6 +793,12 @@ body.hide-cursor {
|
|
|
768
793
|
flex-shrink: 0;
|
|
769
794
|
}
|
|
770
795
|
|
|
796
|
+
/* полосу футера рисуют общие с #auth-link правила в секции auth; здесь —
|
|
797
|
+
только отступ от панелей */
|
|
798
|
+
#lobby-footer {
|
|
799
|
+
margin: 6px 0 10px;
|
|
800
|
+
}
|
|
801
|
+
|
|
771
802
|
@media (max-width: 800px) {
|
|
772
803
|
.lobby-grid {
|
|
773
804
|
flex-direction: column;
|
|
@@ -775,6 +806,12 @@ body.hide-cursor {
|
|
|
775
806
|
gap: unset;
|
|
776
807
|
}
|
|
777
808
|
|
|
809
|
+
/* в столбик обёртка тянется на всю ширину: иначе она сжалась бы по
|
|
810
|
+
содержимому и увела бы панели из центра */
|
|
811
|
+
.lobby-column {
|
|
812
|
+
width: 100%;
|
|
813
|
+
}
|
|
814
|
+
|
|
778
815
|
/* в столбик высоту строкой никто не задаёт — возвращаем слой в поток,
|
|
779
816
|
иначе absolute-слой схлопнулся бы в 0 */
|
|
780
817
|
.lobby-browser-inner {
|
|
@@ -55,16 +55,10 @@ const SHELL = [
|
|
|
55
55
|
tag: 'div',
|
|
56
56
|
id: 'auth-link',
|
|
57
57
|
children: [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
tag: 'a',
|
|
63
|
-
attrs: { href: 'https://github.com/lgick/vimp' },
|
|
64
|
-
text: 'GitHub',
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
},
|
|
58
|
+
// ссылка и версия пакета ИГРЫ — заполняет main.js по AUTH_DATA
|
|
59
|
+
// из метаданных пакета в манифесте
|
|
60
|
+
{ tag: 'p', children: [{ tag: 'a', id: 'auth-package-link' }] },
|
|
61
|
+
{ tag: 'p', id: 'auth-version' },
|
|
68
62
|
{ tag: 'p', text: `© ${new Date().getFullYear()} VIMP` },
|
|
69
63
|
],
|
|
70
64
|
},
|
|
@@ -15,6 +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
|
|
22
|
+
a#auth-package-link
|
|
23
|
+
p#auth-version
|
|
20
24
|
p © #{new Date().getFullYear()} VIMP
|
|
@@ -1,54 +1,64 @@
|
|
|
1
1
|
//- скрыт по умолчанию: открывается из main.js только после успешного логина
|
|
2
2
|
//- в #lobby-auth (Этап B2, LobbyAuthCtrl 'authenticated')
|
|
3
3
|
div#lobby(style='display:none')
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
//-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
div
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//-
|
|
40
|
-
div
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
4
|
+
//- колоночная обёртка: сам #lobby — флекс-строка, центрирующая содержимое,
|
|
5
|
+
//- и футер под панелями иначе встал бы третьей колонкой рядом с ними
|
|
6
|
+
div.lobby-column
|
|
7
|
+
div.lobby-grid
|
|
8
|
+
//- левая панель: профиль + форма создания комнаты (lobby-page-plan)
|
|
9
|
+
div#lobby-setup-panel.card
|
|
10
|
+
h2= 'VIMP'
|
|
11
|
+
//- бейдж входа (Этап B2): управляется LobbyAuthView, показывается
|
|
12
|
+
//- вместе с #lobby после успешной авторизации
|
|
13
|
+
div#lobby-user
|
|
14
|
+
div
|
|
15
|
+
span= 'Signed in as '
|
|
16
|
+
b#lobby-user-nick
|
|
17
|
+
input#lobby-user-logout(type='button', value='Sign out')
|
|
18
|
+
hr.lobby-divider
|
|
19
|
+
h3.lobby-section-title= 'HOST A GAME'
|
|
20
|
+
div#lobby-create
|
|
21
|
+
//- селектор игры (lobby-page-plan): рабочий с первой же игрой в
|
|
22
|
+
//- каталоге мастера, а не только когда их несколько
|
|
23
|
+
div.form-row
|
|
24
|
+
label.form-label(for='lobby-game')= 'Select Game'
|
|
25
|
+
select#lobby-game.field-select
|
|
26
|
+
div.form-row
|
|
27
|
+
label.form-label(for='lobby-name')= 'Server name'
|
|
28
|
+
input#lobby-name.field-text(type='text', maxlength='30')
|
|
29
|
+
//- поля настроек комнаты генерируются по явной схеме roomForm
|
|
30
|
+
//- манифеста активной игры (docs/en/plugin-api.md "Form schema")
|
|
31
|
+
div#lobby-fields
|
|
32
|
+
//- ошибки формы (formBuilder.renderFormErrors) и отказ загрузки
|
|
33
|
+
//- плагина выбранной игры — строкой в форме, а не в #tech-informer:
|
|
34
|
+
//- тот кроет вкладку непрозрачным слоем и рассчитан на терминальные
|
|
35
|
+
//- причины, а эти восстановимы (поправить поле, другая игра,
|
|
36
|
+
//- повторный клик). Над кнопкой — рядом с полями, к которым относятся
|
|
37
|
+
div#lobby-error.form-error
|
|
38
|
+
input#lobby-host(type='button', value='Create server')
|
|
39
|
+
//- правая панель: вкладки Active Servers / Leaderboard (lobby-page-plan)
|
|
40
|
+
div#lobby-browser-panel.card
|
|
41
|
+
//- абсолютный слой: высоту .card задаёт форма слева (stretch), а
|
|
42
|
+
//- список внутри скроллится, не раздувая карточку (lobby-page-plan)
|
|
43
|
+
div.lobby-browser-inner
|
|
44
|
+
div.lobby-tabs-header
|
|
45
|
+
input#btn-show-servers.lobby-tab-btn.active(type='button', value='Active Servers')
|
|
46
|
+
input#btn-show-leaderboard.lobby-tab-btn(type='button', value='Leaderboard')
|
|
47
|
+
div#lobby-servers-content
|
|
48
|
+
input#lobby-search.field-text(type='text', placeholder='Search servers…')
|
|
49
|
+
ul#lobby-list
|
|
50
|
+
p#lobby-empty= 'No servers available'
|
|
51
|
+
input#lobby-more(type='button', value='Load more')
|
|
52
|
+
div#lobby-leaderboard-content(style='display:none')
|
|
53
|
+
div.lobby-leaderboard-header
|
|
54
|
+
span#leaderboard-title
|
|
55
|
+
span#leaderboard-total
|
|
56
|
+
ol#lobby-leaderboard-list
|
|
57
|
+
p#lobby-my-placement
|
|
58
|
+
//- футер: ссылку и версию пакета движка проставляет LobbyView
|
|
59
|
+
//- (engineVersion.js + lib/packageLink.js)
|
|
60
|
+
div#lobby-footer
|
|
61
|
+
p
|
|
62
|
+
a#lobby-link
|
|
63
|
+
p#lobby-version
|
|
64
|
+
p © #{new Date().getFullYear()} VIMP
|
package/src/config/lobby.js
CHANGED
|
@@ -96,6 +96,11 @@ export default {
|
|
|
96
96
|
leaderboardTitleId: 'leaderboard-title',
|
|
97
97
|
leaderboardTotalId: 'leaderboard-total',
|
|
98
98
|
myPlacementId: 'lobby-my-placement',
|
|
99
|
+
|
|
100
|
+
// футер: версия npm-пакета движка, запечённая в бандл при сборке
|
|
101
|
+
// (client/lib/engineVersion.js), и ссылка на его страницу
|
|
102
|
+
versionId: 'lobby-version',
|
|
103
|
+
linkId: 'lobby-link',
|
|
99
104
|
},
|
|
100
105
|
|
|
101
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
|
+
}
|