vimp-engine 0.26.0 → 0.28.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 +1 -1
- package/src/client/components/controller/Games.js +22 -1
- package/src/client/components/model/Games.js +83 -1
- package/src/client/components/view/Games.js +180 -1
- package/src/client/main.js +4 -6
- package/src/client/style.css +30 -20
- package/src/client/views/includes/games.pug +14 -14
- package/src/config/lobby.js +26 -5
- package/src/devtools/pluginLoader.js +4 -2
- package/src/lib/loadGamePackage.js +78 -11
package/package.json
CHANGED
|
@@ -25,14 +25,18 @@ export default class GamesCtrl {
|
|
|
25
25
|
|
|
26
26
|
vp.on('open-mine', 'openMine', this);
|
|
27
27
|
vp.on('open-moderation', 'openModeration', this);
|
|
28
|
+
vp.on('lookup', 'lookup', this);
|
|
28
29
|
vp.on('submit', 'submit', this);
|
|
29
30
|
vp.on('update-version', 'updateVersion', this);
|
|
31
|
+
vp.on('delete', 'remove', this);
|
|
32
|
+
vp.on('restore', 'restore', this);
|
|
30
33
|
vp.on('filter', 'filter', this);
|
|
31
34
|
vp.on('stage', 'stage', this);
|
|
32
35
|
vp.on('approve', 'approve', this);
|
|
33
36
|
vp.on('reject', 'reject', this);
|
|
34
37
|
vp.on('disable', 'disable', this);
|
|
35
38
|
vp.on('load-versions', 'loadVersions', this);
|
|
39
|
+
vp.on('set-author', 'setAuthor', this);
|
|
36
40
|
|
|
37
41
|
model.publisher.on('staged', 'staged', this);
|
|
38
42
|
}
|
|
@@ -55,12 +59,17 @@ export default class GamesCtrl {
|
|
|
55
59
|
this._model.loadMine();
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
// «Moderation» — отдельная страница панели: карточка «My games» в этом
|
|
63
|
+
// режиме не видна, и запрос за её данными был бы холостым
|
|
58
64
|
openModeration() {
|
|
59
65
|
this._view.show(true);
|
|
60
|
-
this._model.loadMine();
|
|
61
66
|
this._model.loadAdmin();
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
lookup({ packageName, version }) {
|
|
70
|
+
this._model.lookup(packageName, version);
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
submit(form) {
|
|
65
74
|
this._model.submit(form);
|
|
66
75
|
}
|
|
@@ -69,6 +78,14 @@ export default class GamesCtrl {
|
|
|
69
78
|
this._model.requestVersion(id, version);
|
|
70
79
|
}
|
|
71
80
|
|
|
81
|
+
remove({ id, scope }) {
|
|
82
|
+
this._model.remove(id, scope);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
restore({ id }) {
|
|
86
|
+
this._model.restore(id);
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
filter(status) {
|
|
73
90
|
this._model.setFilter(status);
|
|
74
91
|
}
|
|
@@ -93,6 +110,10 @@ export default class GamesCtrl {
|
|
|
93
110
|
this._model.loadVersions(id);
|
|
94
111
|
}
|
|
95
112
|
|
|
113
|
+
setAuthor({ id, nick }) {
|
|
114
|
+
this._model.setAuthor(id, nick);
|
|
115
|
+
}
|
|
116
|
+
|
|
96
117
|
staged(data) {
|
|
97
118
|
this.publisher.emit('staged', data);
|
|
98
119
|
}
|
|
@@ -99,6 +99,28 @@ export default class GamesModel {
|
|
|
99
99
|
this._emitAdmin();
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// разбор пакета для формы заявки: мастер качает тарболл, читает манифест
|
|
103
|
+
// и отдаёт то, что раньше человек вводил руками. Отдельный запрос, а не
|
|
104
|
+
// часть submit: предпросмотр обязан быть виден ДО отправки заявки
|
|
105
|
+
async lookup(packageName, version) {
|
|
106
|
+
const query = new URLSearchParams({ package: packageName });
|
|
107
|
+
|
|
108
|
+
if (version) {
|
|
109
|
+
query.set('version', version);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { ok, json } = await this._request(
|
|
113
|
+
`${this._config.urls.lookup}?${query}`,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (!ok) {
|
|
117
|
+
this._fail('mine', json);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.publisher.emit('looked-up', json);
|
|
122
|
+
}
|
|
123
|
+
|
|
102
124
|
async submit(form) {
|
|
103
125
|
const { ok, json } = await this._request(this._config.urls.submit, {
|
|
104
126
|
method: 'POST',
|
|
@@ -133,6 +155,52 @@ export default class GamesModel {
|
|
|
133
155
|
await this.loadMine();
|
|
134
156
|
}
|
|
135
157
|
|
|
158
|
+
// удаление игры. scope решает, чей список перечитать: карточка автора и
|
|
159
|
+
// очередь модерации показывают разные проекции одной строки.
|
|
160
|
+
//
|
|
161
|
+
// Удаление обратимо: игра уходит в графу Deleted очереди модерации и
|
|
162
|
+
// возвращается оттуда `restore` — подтверждения второй кнопкой оно
|
|
163
|
+
// поэтому не требует
|
|
164
|
+
async remove(id, scope = 'mine') {
|
|
165
|
+
const { ok, json } = await this._request(this._config.urls.remove(id), {
|
|
166
|
+
method: 'DELETE',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!ok) {
|
|
170
|
+
this._fail(scope, json);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (scope === 'admin') {
|
|
175
|
+
await this.loadAdmin();
|
|
176
|
+
|
|
177
|
+
// после loadAdmin, а не до: renderAdmin чистит строку панели (см.
|
|
178
|
+
// moderate). Снятая с раздачи последняя игра оставляет лобби без
|
|
179
|
+
// каталога, и сказать об этом обязан тот же тик
|
|
180
|
+
if (json?.warning) {
|
|
181
|
+
this.publisher.emit('warning', { scope: 'admin', code: json.warning });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await this.loadMine();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// возврат мягко удалённой игры из графы Deleted (только админ)
|
|
191
|
+
async restore(id) {
|
|
192
|
+
const { ok, json } = await this._request(this._config.urls.restore(id), {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!ok) {
|
|
197
|
+
this._fail('admin', json);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await this.loadAdmin();
|
|
202
|
+
}
|
|
203
|
+
|
|
136
204
|
// «Test»: мастер качает версию и кладёт её в каталог не раздаваемой.
|
|
137
205
|
// Манифест едет наружу — по нему вкладка админа поднимает комнату
|
|
138
206
|
async stage(id, version) {
|
|
@@ -170,9 +238,23 @@ export default class GamesModel {
|
|
|
170
238
|
}
|
|
171
239
|
}
|
|
172
240
|
|
|
241
|
+
// переназначение автора — тот же PATCH, что и решение модератора: наружу
|
|
242
|
+
// едет ник, пустой означает «снять автора»
|
|
243
|
+
async setAuthor(id, nick) {
|
|
244
|
+
await this.moderate(id, { authorNick: nick === '' ? null : nick });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// графа, в которой стоит игра. Мягко удалённая уходит в свою графу
|
|
248
|
+
// ЦЕЛИКОМ, сохраняя прежний статус: восстановление возвращает её туда,
|
|
249
|
+
// где она была, а показывать её ещё и в Pending/Published значило бы
|
|
250
|
+
// предлагать модерировать то, чего у игроков уже нет
|
|
251
|
+
_bucketOf(game) {
|
|
252
|
+
return game.deletedAt ? 'deleted' : game.status;
|
|
253
|
+
}
|
|
254
|
+
|
|
173
255
|
_emitAdmin() {
|
|
174
256
|
this.publisher.emit('admin-changed', {
|
|
175
|
-
games: this._all.filter(game => game
|
|
257
|
+
games: this._all.filter(game => this._bucketOf(game) === this._filter),
|
|
176
258
|
filter: this._filter,
|
|
177
259
|
versions: this._versions,
|
|
178
260
|
});
|
|
@@ -19,6 +19,8 @@ const ERROR_MESSAGES = {
|
|
|
19
19
|
gameExists: 'A game with this id already exists',
|
|
20
20
|
tooManyGames: 'Too many submissions from one author',
|
|
21
21
|
unknownGame: 'Game is not in the registry',
|
|
22
|
+
gamePublished: 'Published game — ask an admin to disable it first',
|
|
23
|
+
unknownUser: 'No player with this nick',
|
|
22
24
|
invalidGameId: 'Invalid game id',
|
|
23
25
|
invalidPackageName: 'Invalid npm package name',
|
|
24
26
|
invalidVersion: 'Invalid version',
|
|
@@ -71,6 +73,19 @@ export default class GamesView {
|
|
|
71
73
|
this._mineList = document.getElementById(elems.mineListId);
|
|
72
74
|
this._submitForm = document.getElementById(elems.submitFormId);
|
|
73
75
|
this._submitError = document.getElementById(elems.submitErrorId);
|
|
76
|
+
this._submitBtn = document.getElementById(elems.submitBtnId);
|
|
77
|
+
this._lookupBtn = document.getElementById(elems.lookupBtnId);
|
|
78
|
+
this._preview = document.getElementById(elems.previewId);
|
|
79
|
+
this._versionList = document.getElementById(elems.versionListId);
|
|
80
|
+
|
|
81
|
+
this._mine = document.getElementById(elems.mineId);
|
|
82
|
+
this._title = document.getElementById(elems.titleId);
|
|
83
|
+
this._switch = document.getElementById(elems.switchBtnId);
|
|
84
|
+
// текущая страница панели: от неё зависит, куда уводит переключатель
|
|
85
|
+
this._isModeration = false;
|
|
86
|
+
// последний разобранный `${packageName}@${version}` — защита от парного
|
|
87
|
+
// разбора одного и того же пакета, см. _emitLookup
|
|
88
|
+
this._lastLookup = null;
|
|
74
89
|
|
|
75
90
|
this._moderation = document.getElementById(elems.moderationId);
|
|
76
91
|
this._adminList = document.getElementById(elems.adminListId);
|
|
@@ -99,17 +114,34 @@ export default class GamesView {
|
|
|
99
114
|
this._openMine.onclick = () => this.publisher.emit('open-mine');
|
|
100
115
|
this._openModeration.onclick = () => this.publisher.emit('open-moderation');
|
|
101
116
|
this._close.onclick = () => this.hide();
|
|
117
|
+
// переключатель страниц панели: без него выход из «Moderation» в
|
|
118
|
+
// «My games» шёл бы только через лобби
|
|
119
|
+
this._switch.onclick = () =>
|
|
120
|
+
this.publisher.emit(this._isModeration ? 'open-mine' : 'open-moderation');
|
|
102
121
|
|
|
103
122
|
this._submitForm.onsubmit = e => {
|
|
104
123
|
e.preventDefault();
|
|
105
124
|
this.publisher.emit('submit', this._readForm());
|
|
106
125
|
};
|
|
107
126
|
|
|
127
|
+
const packageField = this._fields.get('packageName');
|
|
128
|
+
|
|
129
|
+
// разбор пакета по кнопке И по уходу из поля: заполнять форму
|
|
130
|
+
// предпросмотром — обычный путь, а не отдельное действие
|
|
131
|
+
this._lookupBtn.onclick = () => this._emitLookup();
|
|
132
|
+
packageField.onblur = () => this._emitLookup();
|
|
133
|
+
// правка пакета обесценивает показанный предпросмотр: заявка ушла бы с
|
|
134
|
+
// разобранным ранее пакетом, а человек читал бы карточку другого
|
|
135
|
+
packageField.oninput = () => this.clearPreview();
|
|
136
|
+
|
|
137
|
+
this.clearPreview();
|
|
138
|
+
|
|
108
139
|
this._renderFilters();
|
|
109
140
|
|
|
110
141
|
const mp = model.publisher;
|
|
111
142
|
|
|
112
143
|
mp.on('submitted', 'clearForm', this);
|
|
144
|
+
mp.on('looked-up', 'renderPreview', this);
|
|
113
145
|
mp.on('mine-changed', 'renderMine', this);
|
|
114
146
|
mp.on('admin-changed', 'renderAdmin', this);
|
|
115
147
|
mp.on('error', 'renderError', this);
|
|
@@ -120,12 +152,21 @@ export default class GamesView {
|
|
|
120
152
|
// (main.js по LobbyAuthModel.getRole())
|
|
121
153
|
setAdmin(isAdmin) {
|
|
122
154
|
this._openModeration.style.display = isAdmin ? '' : 'none';
|
|
155
|
+
this._switch.style.display = isAdmin ? '' : 'none';
|
|
123
156
|
}
|
|
124
157
|
|
|
158
|
+
// Панель — две страницы, а не одна: «My games» и «Moderation»
|
|
159
|
+
// показываются по одной. Общая шапка (заголовок, переключатель и
|
|
160
|
+
// «Back to lobby») живёт над карточками и переживает переключение
|
|
125
161
|
show(moderation = false) {
|
|
162
|
+
this._isModeration = moderation;
|
|
126
163
|
this._panel.style.display = 'flex';
|
|
127
164
|
this._lobby.style.display = 'none';
|
|
165
|
+
this._mine.style.display = moderation ? 'none' : 'block';
|
|
128
166
|
this._moderation.style.display = moderation ? 'block' : 'none';
|
|
167
|
+
this._title.textContent = moderation ? 'Moderation' : 'My games';
|
|
168
|
+
// подпись переключателя называет ту страницу, куда он уводит
|
|
169
|
+
this._switch.value = moderation ? 'My games' : 'Moderation';
|
|
129
170
|
}
|
|
130
171
|
|
|
131
172
|
hide() {
|
|
@@ -145,6 +186,68 @@ export default class GamesView {
|
|
|
145
186
|
this._fields.forEach(field => {
|
|
146
187
|
field.value = '';
|
|
147
188
|
});
|
|
189
|
+
this.clearPreview();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// предпросмотра нет — отправлять нечего: мастер всё равно откажет, а
|
|
193
|
+
// человек не увидит, за какую игру он ручается
|
|
194
|
+
clearPreview() {
|
|
195
|
+
this._preview.textContent = '';
|
|
196
|
+
this._versionList.textContent = '';
|
|
197
|
+
this._submitBtn.disabled = true;
|
|
198
|
+
// предпросмотра нет — значит, и запрета на повторный разбор быть не
|
|
199
|
+
// должно: правка поля обесценивает прошлый вердикт
|
|
200
|
+
this._lastLookup = null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Карточка разобранного пакета: то, что раньше человек вводил руками.
|
|
205
|
+
* @param {Object} data - Ответ мастера на /games/lookup.
|
|
206
|
+
*/
|
|
207
|
+
renderPreview({ id, title, version, versions, repoUrl, engineApi, errors }) {
|
|
208
|
+
this._preview.textContent = '';
|
|
209
|
+
this._submitError.textContent = '';
|
|
210
|
+
|
|
211
|
+
this._preview.appendChild(
|
|
212
|
+
this._line(`${title ? `${title} · ` : ''}${id ?? '—'}`, 'games-item-title'),
|
|
213
|
+
);
|
|
214
|
+
this._preview.appendChild(this._line(`Version: ${version ?? '—'}`));
|
|
215
|
+
this._preview.appendChild(this._line(`Engine API: ${engineApi ?? '—'}`));
|
|
216
|
+
this._appendRepo(this._preview, repoUrl);
|
|
217
|
+
|
|
218
|
+
// список опубликованных версий — подсказка поля, а не отдельный контрол
|
|
219
|
+
this._versionList.textContent = '';
|
|
220
|
+
(versions || []).forEach(value => {
|
|
221
|
+
const option = document.createElement('option');
|
|
222
|
+
|
|
223
|
+
option.value = value;
|
|
224
|
+
this._versionList.appendChild(option);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// резолвнутая мастером версия: 'latest' в поле осталась бы ссылкой,
|
|
228
|
+
// которая завтра означает другой пакет
|
|
229
|
+
if (version) {
|
|
230
|
+
this._fields.get('version').value = version;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// разрешённая версия в поле меняет `${packageName}@${version}`, и
|
|
234
|
+
// следующий blur прошёл бы как «другой пакет»: запрет повтора обновляется
|
|
235
|
+
// фактическим ответом мастера
|
|
236
|
+
this._lastLookup = `${this._fields.get('packageName').value.trim()}@${
|
|
237
|
+
this._fields.get('version').value.trim()
|
|
238
|
+
}`;
|
|
239
|
+
|
|
240
|
+
const problems = errors || [];
|
|
241
|
+
|
|
242
|
+
if (problems.length) {
|
|
243
|
+
renderFormErrors(
|
|
244
|
+
this._submitError,
|
|
245
|
+
problems.map(error => ({ name: 'package', label: 'package', error })),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// заявка на заведомо нерабочий пакет реестру не нужна
|
|
250
|
+
this._submitBtn.disabled = problems.length > 0 || !id;
|
|
148
251
|
}
|
|
149
252
|
|
|
150
253
|
renderMine(games) {
|
|
@@ -184,6 +287,13 @@ export default class GamesView {
|
|
|
184
287
|
|
|
185
288
|
item.appendChild(version);
|
|
186
289
|
item.appendChild(send);
|
|
290
|
+
item.appendChild(
|
|
291
|
+
this._button(
|
|
292
|
+
'Delete',
|
|
293
|
+
() => this.publisher.emit('delete', { id: game.id, scope: 'mine' }),
|
|
294
|
+
'games-delete-btn',
|
|
295
|
+
),
|
|
296
|
+
);
|
|
187
297
|
this._mineList.appendChild(item);
|
|
188
298
|
});
|
|
189
299
|
}
|
|
@@ -229,9 +339,41 @@ export default class GamesView {
|
|
|
229
339
|
'games-item-title',
|
|
230
340
|
),
|
|
231
341
|
);
|
|
342
|
+
|
|
343
|
+
// Мягко удалённая игра модерации не подлежит: у игроков её уже нет, а
|
|
344
|
+
// решение по ней приняли. Из всей карточки ей нужны срок полного
|
|
345
|
+
// удаления и возврат — остальные кнопки правили бы то, чего не видно
|
|
346
|
+
if (game.deletedAt) {
|
|
347
|
+
item.appendChild(this._line(`Author: ${game.authorNick ?? game.authorUserId ?? '—'}`));
|
|
348
|
+
this._appendRepo(item, game.repoUrl);
|
|
349
|
+
item.appendChild(this._line(this._statusLine(game), 'games-purge-line'));
|
|
350
|
+
item.appendChild(
|
|
351
|
+
this._button('Restore', () => this.publisher.emit('restore', { id: game.id })),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
return item;
|
|
355
|
+
}
|
|
356
|
+
|
|
232
357
|
// у игр, засеянных миграцией, автора нет вовсе: без запасного прочерка
|
|
233
358
|
// в строке печаталось бы литеральное "null"
|
|
234
359
|
item.appendChild(this._line(`Author: ${game.authorNick ?? game.authorUserId ?? '—'}`));
|
|
360
|
+
|
|
361
|
+
// переназначение автора: игры платформы засеяны без него, и «My games»
|
|
362
|
+
// у их автора пуст, пока админ не проставит ник здесь. Пустое поле —
|
|
363
|
+
// снять автора
|
|
364
|
+
const author = document.createElement('input');
|
|
365
|
+
|
|
366
|
+
author.type = 'text';
|
|
367
|
+
author.className = 'field-text games-author-input';
|
|
368
|
+
author.placeholder = 'Author nick';
|
|
369
|
+
author.value = game.authorNick ?? '';
|
|
370
|
+
|
|
371
|
+
item.appendChild(author);
|
|
372
|
+
item.appendChild(
|
|
373
|
+
this._button('Set author', () =>
|
|
374
|
+
this.publisher.emit('set-author', { id: game.id, nick: author.value.trim() }),
|
|
375
|
+
),
|
|
376
|
+
);
|
|
235
377
|
this._appendRepo(item, game.repoUrl);
|
|
236
378
|
item.appendChild(
|
|
237
379
|
this._line(
|
|
@@ -282,6 +424,13 @@ export default class GamesView {
|
|
|
282
424
|
this.publisher.emit('load-versions', { id: game.id }),
|
|
283
425
|
),
|
|
284
426
|
);
|
|
427
|
+
item.appendChild(
|
|
428
|
+
this._button(
|
|
429
|
+
'Delete',
|
|
430
|
+
() => this.publisher.emit('delete', { id: game.id, scope: 'admin' }),
|
|
431
|
+
'games-delete-btn',
|
|
432
|
+
),
|
|
433
|
+
);
|
|
285
434
|
|
|
286
435
|
return item;
|
|
287
436
|
}
|
|
@@ -310,6 +459,16 @@ export default class GamesView {
|
|
|
310
459
|
const status = STATUS_TITLES[game.status] ?? game.status;
|
|
311
460
|
const date = game.createdAt ? new Date(game.createdAt).toLocaleDateString() : '';
|
|
312
461
|
|
|
462
|
+
// у удалённой игры прежний статус — след того, куда её вернёт Restore, —
|
|
463
|
+
// а вести отсчёт человеку нужно от срока полного удаления: только он
|
|
464
|
+
// ограничен во времени
|
|
465
|
+
if (game.deletedAt) {
|
|
466
|
+
const deleted = new Date(game.deletedAt).toLocaleDateString();
|
|
467
|
+
const purge = game.purgeAt ? new Date(game.purgeAt).toLocaleDateString() : '—';
|
|
468
|
+
|
|
469
|
+
return `Deleted ${deleted} (was ${status}); removed for good on ${purge}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
313
472
|
return date ? `Status: ${status} (submitted ${date})` : `Status: ${status}`;
|
|
314
473
|
}
|
|
315
474
|
|
|
@@ -331,13 +490,17 @@ export default class GamesView {
|
|
|
331
490
|
});
|
|
332
491
|
}
|
|
333
492
|
|
|
334
|
-
_button(value, onclick) {
|
|
493
|
+
_button(value, onclick, className) {
|
|
335
494
|
const btn = document.createElement('input');
|
|
336
495
|
|
|
337
496
|
btn.type = 'button';
|
|
338
497
|
btn.value = value;
|
|
339
498
|
btn.onclick = onclick;
|
|
340
499
|
|
|
500
|
+
if (className) {
|
|
501
|
+
btn.className = className;
|
|
502
|
+
}
|
|
503
|
+
|
|
341
504
|
return btn;
|
|
342
505
|
}
|
|
343
506
|
|
|
@@ -353,6 +516,22 @@ export default class GamesView {
|
|
|
353
516
|
return line;
|
|
354
517
|
}
|
|
355
518
|
|
|
519
|
+
// Один и тот же пакет не разбирается дважды: blur поля и клик по «Load»
|
|
520
|
+
// приходят парой (mousedown → blur → click), а каждый разбор стоит мастеру
|
|
521
|
+
// скачанного тарболла и единицы общего лимитера заявок (5/мин на человека)
|
|
522
|
+
_emitLookup() {
|
|
523
|
+
const packageName = this._fields.get('packageName').value.trim();
|
|
524
|
+
const version = this._fields.get('version').value.trim();
|
|
525
|
+
const ref = `${packageName}@${version}`;
|
|
526
|
+
|
|
527
|
+
if (!packageName || ref === this._lastLookup) {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
this._lastLookup = ref;
|
|
532
|
+
this.publisher.emit('lookup', { packageName, version });
|
|
533
|
+
}
|
|
534
|
+
|
|
356
535
|
_readForm() {
|
|
357
536
|
const form = {};
|
|
358
537
|
|
package/src/client/main.js
CHANGED
|
@@ -1978,11 +1978,6 @@ function populateRoomForm(manifest) {
|
|
|
1978
1978
|
}
|
|
1979
1979
|
}
|
|
1980
1980
|
|
|
1981
|
-
// роли, которым лобби показывает кнопку «Moderation» (master-game-registry,
|
|
1982
|
-
// этап 4). Это подсказка интерфейсу: доступ к данным проверяет мастер, а
|
|
1983
|
-
// запись — auth-сервис, перечитывая роль из БД
|
|
1984
|
-
const ADMIN_ROLES = ['admin', 'superadmin'];
|
|
1985
|
-
|
|
1986
1981
|
// каталог манифестов по id: форма и leaderboard селектора игр, а также
|
|
1987
1982
|
// активация игры перед созданием комнаты и входом в чужую
|
|
1988
1983
|
const gamesById = new Map(
|
|
@@ -2240,7 +2235,10 @@ function initLobby() {
|
|
|
2240
2235
|
const gamesView = new GamesView(gamesModel, lobbyConfig.games);
|
|
2241
2236
|
const games = new GamesCtrl(gamesModel, gamesView);
|
|
2242
2237
|
|
|
2243
|
-
|
|
2238
|
+
// кнопку «Moderation» лобби показывает роли admin (master-game-registry,
|
|
2239
|
+
// этап 4). Это подсказка интерфейсу: доступ к данным проверяет мастер, а
|
|
2240
|
+
// запись — auth-сервис, перечитывая роль из БД
|
|
2241
|
+
games.setAdmin(lobbyAuthModel.getRole() === 'admin');
|
|
2244
2242
|
|
|
2245
2243
|
// «Test»: манифест застейдженной версии кладётся в каталог вкладки, и
|
|
2246
2244
|
// админ поднимает по нему комнату обычной кнопкой Create server
|
package/src/client/style.css
CHANGED
|
@@ -217,9 +217,9 @@ body.hide-cursor {
|
|
|
217
217
|
background: rgba(25, 25, 25, 0.9);
|
|
218
218
|
border-radius: 5px;
|
|
219
219
|
border: 3px solid #fff;
|
|
220
|
-
overflow:
|
|
220
|
+
overflow: auto;
|
|
221
221
|
width: 90%;
|
|
222
|
-
max-width:
|
|
222
|
+
max-width: 500px;
|
|
223
223
|
font-size: 10px;
|
|
224
224
|
line-height: 12px;
|
|
225
225
|
color: #fff;
|
|
@@ -438,7 +438,7 @@ body.hide-cursor {
|
|
|
438
438
|
background-color: #333;
|
|
439
439
|
border: 1px solid #eee;
|
|
440
440
|
padding: 10px;
|
|
441
|
-
margin:
|
|
441
|
+
margin: 5px auto 0;
|
|
442
442
|
text-shadow: 1px 1px 1px #000;
|
|
443
443
|
font-size: 12px;
|
|
444
444
|
line-height: 15px;
|
|
@@ -549,15 +549,11 @@ body.hide-cursor {
|
|
|
549
549
|
padding: 0 10px;
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
-
/* ширина — контентная область правой панели лобби (её ширина минус
|
|
553
|
-
собственные поля .lobby-browser-inner), под которой футер и стоит;
|
|
554
|
-
max-width, а не width: в одну колонку он обязан ужиматься по экрану */
|
|
555
552
|
#lobby-footer {
|
|
556
|
-
width:
|
|
557
|
-
max-width: calc(var(--lobby-browser-width) - 2 * var(--lobby-panel-pad));
|
|
553
|
+
width: calc(var(--lobby-width) + var(--lobby-panel-pad));
|
|
558
554
|
font-size: 10px;
|
|
559
555
|
padding: 0;
|
|
560
|
-
margin: 0 var(--lobby-panel-pad) 0 0;
|
|
556
|
+
margin: 0 calc(var(--lobby-panel-pad) / 2) 0 0;
|
|
561
557
|
}
|
|
562
558
|
|
|
563
559
|
/* три равные ячейки: середину держит не space-between, а сама равность —
|
|
@@ -670,11 +666,7 @@ body.hide-cursor {
|
|
|
670
666
|
/*lobby*/
|
|
671
667
|
|
|
672
668
|
#lobby {
|
|
673
|
-
|
|
674
|
-
(секция auth выше), поэтому числа живут в одном месте — иначе смена
|
|
675
|
-
ширины любой панели молча разъезжает вёрстку */
|
|
676
|
-
--lobby-setup-width: 410px;
|
|
677
|
-
--lobby-browser-width: 430px;
|
|
669
|
+
--lobby-width: 370px;
|
|
678
670
|
--lobby-panel-pad: 10px;
|
|
679
671
|
|
|
680
672
|
background-color: #222;
|
|
@@ -832,12 +824,12 @@ body.hide-cursor {
|
|
|
832
824
|
высоты более высокой панели */
|
|
833
825
|
.lobby-grid {
|
|
834
826
|
display: flex;
|
|
835
|
-
gap:
|
|
827
|
+
gap: 5px;
|
|
836
828
|
justify-content: center;
|
|
837
829
|
}
|
|
838
830
|
|
|
839
831
|
#lobby-setup-panel {
|
|
840
|
-
width: var(--lobby-
|
|
832
|
+
width: var(--lobby-width);
|
|
841
833
|
}
|
|
842
834
|
|
|
843
835
|
/* высоту правой .card задаёт форма слева: обе панели тянутся до высоты
|
|
@@ -846,9 +838,8 @@ body.hide-cursor {
|
|
|
846
838
|
вынесен в абсолютный слой .lobby-browser-inner (его высота не участвует в
|
|
847
839
|
расчёте высоты строки) */
|
|
848
840
|
#lobby-browser-panel {
|
|
849
|
-
width: var(--lobby-
|
|
841
|
+
width: var(--lobby-width);
|
|
850
842
|
position: relative;
|
|
851
|
-
padding: 0;
|
|
852
843
|
}
|
|
853
844
|
|
|
854
845
|
.lobby-browser-inner {
|
|
@@ -1081,11 +1072,17 @@ body.hide-cursor {
|
|
|
1081
1072
|
text-align: left;
|
|
1082
1073
|
}
|
|
1083
1074
|
|
|
1084
|
-
/* заголовок
|
|
1075
|
+
/* заголовок панели, переключатель страниц и кнопка возврата в одной
|
|
1076
|
+
строке; шапка лежит НАД карточками, поэтому ширину и поля повторяет
|
|
1077
|
+
за ними — иначе flex-end колонки увёл бы её вправо от карточки */
|
|
1085
1078
|
.games-head {
|
|
1086
1079
|
display: flex;
|
|
1087
1080
|
align-items: center;
|
|
1088
1081
|
justify-content: space-between;
|
|
1082
|
+
gap: 8px;
|
|
1083
|
+
width: 700px;
|
|
1084
|
+
padding: 0 10px;
|
|
1085
|
+
box-sizing: border-box;
|
|
1089
1086
|
}
|
|
1090
1087
|
|
|
1091
1088
|
.games-list {
|
|
@@ -1141,6 +1138,18 @@ body.hide-cursor {
|
|
|
1141
1138
|
color: #eee;
|
|
1142
1139
|
}
|
|
1143
1140
|
|
|
1141
|
+
/* удаление обратимо (игра уходит в графу Deleted на 30 суток), но выделяется
|
|
1142
|
+
среди соседних кнопок: рядом стоит правка версии, а не действие того же веса */
|
|
1143
|
+
.games-delete-btn {
|
|
1144
|
+
color: #d66;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/* срок полного удаления — единственное, что в карточке удалённой игры
|
|
1148
|
+
ограничено во времени */
|
|
1149
|
+
.games-purge-line {
|
|
1150
|
+
color: #d66;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1144
1153
|
/* кнопки входа в панель стоят рядом с Sign out в бейдже пользователя */
|
|
1145
1154
|
.lobby-user-actions {
|
|
1146
1155
|
display: flex;
|
|
@@ -1150,7 +1159,8 @@ body.hide-cursor {
|
|
|
1150
1159
|
|
|
1151
1160
|
@media (max-width: 900px) {
|
|
1152
1161
|
#games-mine,
|
|
1153
|
-
#games-moderation
|
|
1162
|
+
#games-moderation,
|
|
1163
|
+
.games-head {
|
|
1154
1164
|
width: 100%;
|
|
1155
1165
|
}
|
|
1156
1166
|
}
|
|
@@ -5,32 +5,32 @@
|
|
|
5
5
|
//- Подписи — по-английски, как и остальное лобби
|
|
6
6
|
div#games-panel(style='display:none')
|
|
7
7
|
div.lobby-column
|
|
8
|
+
//- Шапка панели, а не карточки: карточки «My games» и «Moderation»
|
|
9
|
+
//- показываются по одной (панель — это две разные страницы), и
|
|
10
|
+
//- кнопка выхода в лобби обязана пережить переключение между ними
|
|
11
|
+
div.games-head
|
|
12
|
+
h3#games-title.lobby-section-title= 'My games'
|
|
13
|
+
input#games-switch(type='button', value='Moderation', style='display:none')
|
|
14
|
+
input#games-close(type='button', value='Back to lobby')
|
|
15
|
+
|
|
8
16
|
div.card#games-mine
|
|
9
|
-
div.games-head
|
|
10
|
-
h3.lobby-section-title= 'My games'
|
|
11
|
-
input#games-close(type='button', value='Back to lobby')
|
|
12
17
|
ul#games-mine-list.games-list
|
|
18
|
+
//- Заявка спрашивает только пакет и версию: id, title и репозиторий
|
|
19
|
+
//- мастер читает из самого пакета и показывает предпросмотром
|
|
13
20
|
form#games-submit-form
|
|
14
|
-
div.form-row
|
|
15
|
-
label.form-label(for='games-field-id')= 'Game id'
|
|
16
|
-
input#games-field-id.field-text(type='text', name='id', maxlength='32', autocomplete='off')
|
|
17
21
|
div.form-row
|
|
18
22
|
label.form-label(for='games-field-package')= 'npm package'
|
|
19
23
|
input#games-field-package.field-text(type='text', name='packageName', maxlength='214', autocomplete='off')
|
|
24
|
+
input#games-lookup(type='button', value='Load')
|
|
20
25
|
div.form-row
|
|
21
26
|
label.form-label(for='games-field-version')= 'Version'
|
|
22
|
-
input#games-field-version.field-text(type='text', name='version', maxlength='64', autocomplete='off')
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
input#games-field-repo.field-text(type='text', name='repoUrl', maxlength='256', autocomplete='off')
|
|
26
|
-
div.form-row
|
|
27
|
-
label.form-label(for='games-field-title')= 'Title'
|
|
28
|
-
input#games-field-title.field-text(type='text', name='title', maxlength='64', autocomplete='off')
|
|
27
|
+
input#games-field-version.field-text(type='text', name='version', maxlength='64', autocomplete='off', placeholder='latest', list='games-version-list')
|
|
28
|
+
datalist#games-version-list
|
|
29
|
+
div#games-preview.games-preview
|
|
29
30
|
div#games-submit-error.form-error
|
|
30
31
|
input#games-submit(type='submit', value='Submit for review')
|
|
31
32
|
|
|
32
33
|
div.card#games-moderation(style='display:none')
|
|
33
|
-
h3.lobby-section-title= 'Moderation'
|
|
34
34
|
div#games-filters.games-filters
|
|
35
35
|
ul#games-admin-list.games-list
|
|
36
36
|
div#games-admin-error.form-error
|
package/src/config/lobby.js
CHANGED
|
@@ -201,10 +201,16 @@ export default {
|
|
|
201
201
|
urls: {
|
|
202
202
|
// заявки вызывающего со статусами и замечаниями модератора
|
|
203
203
|
mine: '/games/mine',
|
|
204
|
+
// разбор npm-пакета для формы заявки: id, title, версии и репозиторий
|
|
205
|
+
// мастер читает сам — человек вводит только пакет и версию
|
|
206
|
+
lookup: '/games/lookup',
|
|
204
207
|
// заявка на новую игру платформы (валидируется мастером до записи)
|
|
205
208
|
submit: '/games/submit',
|
|
206
209
|
// заявка на новую версию уже заведённой игры
|
|
207
210
|
version: id => `/games/mine/${encodeURIComponent(id)}/version`,
|
|
211
|
+
// удаление игры: и «My games» (автор), и «Moderation» (админ) ходят
|
|
212
|
+
// одним URL — право решает auth
|
|
213
|
+
remove: id => `/games/mine/${encodeURIComponent(id)}`,
|
|
208
214
|
// очередь модерации целиком плюс локальное состояние на этом мастере
|
|
209
215
|
admin: '/admin/games',
|
|
210
216
|
// манифесты застейдженных версий — по ним админ поднимает тестовую
|
|
@@ -212,19 +218,26 @@ export default {
|
|
|
212
218
|
staged: '/admin/games/manifest.json',
|
|
213
219
|
// «Test»: скачать версию и положить её в каталог не раздаваемой
|
|
214
220
|
stage: id => `/admin/games/${encodeURIComponent(id)}/stage`,
|
|
221
|
+
// возврат мягко удалённой игры из графы Deleted (только админ)
|
|
222
|
+
restore: id => `/admin/games/${encodeURIComponent(id)}/restore`,
|
|
215
223
|
// решение модератора
|
|
216
224
|
moderate: id => `/admin/games/${encodeURIComponent(id)}`,
|
|
217
225
|
// что опубликовано в npm — индикатор «есть версия новее»
|
|
218
226
|
versions: id => `/admin/games/${encodeURIComponent(id)}/versions`,
|
|
219
227
|
},
|
|
220
228
|
|
|
221
|
-
// фильтры очереди модерации: id
|
|
222
|
-
//
|
|
229
|
+
// фильтры («графы») очереди модерации: id -> подпись кнопки. Первые
|
|
230
|
+
// четыре — статусы реестра, и значения обязаны совпадать со статусами
|
|
231
|
+
// auth-сервиса. Пятая графа стоит особняком: 'deleted' статусом НЕ
|
|
232
|
+
// является (мягко удалённая игра сохраняет свой прежний статус, иначе
|
|
233
|
+
// восстанавливать было бы не во что) — под неё попадает всё с
|
|
234
|
+
// непустым deletedAt, см. GamesModel._bucketOf
|
|
223
235
|
statuses: [
|
|
224
236
|
{ id: 'pending', title: 'Pending' },
|
|
225
237
|
{ id: 'approved', title: 'Published' },
|
|
226
238
|
{ id: 'rejected', title: 'Rejected' },
|
|
227
239
|
{ id: 'disabled', title: 'Disabled' },
|
|
240
|
+
{ id: 'deleted', title: 'Deleted' },
|
|
228
241
|
],
|
|
229
242
|
defaultStatus: 'pending',
|
|
230
243
|
|
|
@@ -247,13 +260,21 @@ export default {
|
|
|
247
260
|
submitFormId: 'games-submit-form',
|
|
248
261
|
submitErrorId: 'games-submit-error',
|
|
249
262
|
submitBtnId: 'games-submit',
|
|
263
|
+
// форма спрашивает ровно две вещи; id, title и репозиторий приезжают
|
|
264
|
+
// предпросмотром из разобранного пакета
|
|
250
265
|
fieldIds: {
|
|
251
|
-
id: 'games-field-id',
|
|
252
266
|
packageName: 'games-field-package',
|
|
253
267
|
version: 'games-field-version',
|
|
254
|
-
repoUrl: 'games-field-repo',
|
|
255
|
-
title: 'games-field-title',
|
|
256
268
|
},
|
|
269
|
+
lookupBtnId: 'games-lookup',
|
|
270
|
+
previewId: 'games-preview',
|
|
271
|
+
versionListId: 'games-version-list',
|
|
272
|
+
|
|
273
|
+
// карточки панели показываются по одной; заголовок общий, как и
|
|
274
|
+
// переключатель страниц (виден только админу)
|
|
275
|
+
mineId: 'games-mine',
|
|
276
|
+
titleId: 'games-title',
|
|
277
|
+
switchBtnId: 'games-switch',
|
|
257
278
|
|
|
258
279
|
// модерация
|
|
259
280
|
moderationId: 'games-moderation',
|
|
@@ -54,12 +54,14 @@ export async function loadGameForSim({ game = null, core = null } = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// путь --game указывает либо на пакет игры, либо прямо на манифест; сама
|
|
57
|
-
// загрузка общая с dedicated-сервером (lib/loadGamePackage.js)
|
|
57
|
+
// загрузка общая с dedicated-сервером (lib/loadGamePackage.js).
|
|
58
|
+
// client: true — прогону виртуальные клиенты обязательны, и отказ на
|
|
59
|
+
// загрузке пакета лучше отказа посреди сценария
|
|
58
60
|
async function loadFromManifest(game, core) {
|
|
59
61
|
const target = game.endsWith('.json')
|
|
60
62
|
? path.resolve(game)
|
|
61
63
|
: path.resolve(game, 'dist');
|
|
62
|
-
const pkg = await loadGamePackage(target, { core });
|
|
64
|
+
const pkg = await loadGamePackage(target, { core, client: true });
|
|
63
65
|
|
|
64
66
|
return { ...pkg, source: pkg.manifestPath };
|
|
65
67
|
}
|
|
@@ -21,10 +21,17 @@ import { ENGINE_CAPABILITIES } from './capabilities.js';
|
|
|
21
21
|
* @param {Object} [options]
|
|
22
22
|
* @param {string} [options.core] - Путь к node-сборке ядра, перекрывающий
|
|
23
23
|
* entries.wasmNode манифеста.
|
|
24
|
-
* @
|
|
25
|
-
*
|
|
24
|
+
* @param {(boolean|'lazy')} [options.client] - Когда грузить клиентскую
|
|
25
|
+
* половину: 'lazy' (умолчание) — только по вызову loadClientPlugin(),
|
|
26
|
+
* true — сразу вместе с host-половиной.
|
|
27
|
+
* @returns {Promise<Object>} { id, manifest, hostPlugin, clientPlugin,
|
|
28
|
+
* loadClientPlugin, wasmUrl, distDir, manifestPath }. В ленивом режиме
|
|
29
|
+
* clientPlugin равен null, а половину отдаёт loadClientPlugin().
|
|
26
30
|
*/
|
|
27
|
-
export async function loadGamePackage(
|
|
31
|
+
export async function loadGamePackage(
|
|
32
|
+
distDir,
|
|
33
|
+
{ core = null, client = 'lazy' } = {},
|
|
34
|
+
) {
|
|
28
35
|
// манифест под нестандартным именем нужен headless-прогону (--game
|
|
29
36
|
// <path>/manifest-variant.json); dedicated-сервер всегда даёт каталог
|
|
30
37
|
const manifestPath = distDir.endsWith('.json')
|
|
@@ -47,18 +54,53 @@ export async function loadGamePackage(distDir, { core = null } = {}) {
|
|
|
47
54
|
baseDir,
|
|
48
55
|
manifest.entries.host,
|
|
49
56
|
assetsBase,
|
|
50
|
-
|
|
51
|
-
const clientPlugin = await importDefault(
|
|
52
|
-
baseDir,
|
|
53
|
-
manifest.entries.client,
|
|
54
|
-
assetsBase,
|
|
57
|
+
manifestPath,
|
|
55
58
|
);
|
|
56
59
|
|
|
60
|
+
// Клиентская половина в Node нужна только виртуальным клиентам
|
|
61
|
+
// headless-прогона; dedicated-сервер её не касается вовсе (в браузер она
|
|
62
|
+
// едет статикой, и pixi.js там резолвится import map'ом). Грузить её
|
|
63
|
+
// безусловно значило бы требовать от игры самодостаточной клиентской
|
|
64
|
+
// сборки — а правило контракта A1 держит pixi.js внешним намеренно.
|
|
65
|
+
//
|
|
66
|
+
// Размен ленивого режима: расхождение engineApi между манифестом и
|
|
67
|
+
// клиентской сборкой в Node-контуре ничего не ломает — клиента собирает
|
|
68
|
+
// браузер, совместимость манифеста уже проверена checkPluginCompatibility
|
|
69
|
+
// выше, а структуру пакета проверяет checkGamePackage на стороне
|
|
70
|
+
// GameStore.
|
|
71
|
+
const clientPlugin =
|
|
72
|
+
client === true
|
|
73
|
+
? await importDefault(
|
|
74
|
+
baseDir,
|
|
75
|
+
manifest.entries.client,
|
|
76
|
+
assetsBase,
|
|
77
|
+
manifestPath,
|
|
78
|
+
)
|
|
79
|
+
: null;
|
|
80
|
+
|
|
57
81
|
assertPluginMatchesManifest(manifest, {
|
|
58
82
|
host: hostPlugin,
|
|
59
|
-
client: clientPlugin,
|
|
83
|
+
...(clientPlugin ? { client: clientPlugin } : {}),
|
|
60
84
|
});
|
|
61
85
|
|
|
86
|
+
// одна форма результата при любом режиме: потребитель зовёт
|
|
87
|
+
// loadClientPlugin() и не разбирается, была ли половина уже импортирована
|
|
88
|
+
let clientLoad = clientPlugin ? Promise.resolve(clientPlugin) : null;
|
|
89
|
+
const loadClientPlugin = () => {
|
|
90
|
+
clientLoad ??= importDefault(
|
|
91
|
+
baseDir,
|
|
92
|
+
manifest.entries.client,
|
|
93
|
+
assetsBase,
|
|
94
|
+
manifestPath,
|
|
95
|
+
).then(plugin => {
|
|
96
|
+
assertPluginMatchesManifest(manifest, { client: plugin });
|
|
97
|
+
|
|
98
|
+
return plugin;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return clientLoad;
|
|
102
|
+
};
|
|
103
|
+
|
|
62
104
|
const nodeCore = core ?? manifest.entries.wasmNode ?? null;
|
|
63
105
|
|
|
64
106
|
if (!nodeCore) {
|
|
@@ -88,6 +130,7 @@ export async function loadGamePackage(distDir, { core = null } = {}) {
|
|
|
88
130
|
manifest,
|
|
89
131
|
hostPlugin,
|
|
90
132
|
clientPlugin,
|
|
133
|
+
loadClientPlugin,
|
|
91
134
|
wasmUrl: pathToFileURL(corePath).href,
|
|
92
135
|
distDir: baseDir,
|
|
93
136
|
manifestPath,
|
|
@@ -195,10 +238,34 @@ function resolveNames(names) {
|
|
|
195
238
|
);
|
|
196
239
|
}
|
|
197
240
|
|
|
198
|
-
|
|
241
|
+
// Половина плагина, загружаемая в Node, обязана быть самодостаточной:
|
|
242
|
+
// node_modules резолвятся вверх по дереву от файла, а пакет игры лежит там,
|
|
243
|
+
// куда его положил реестр (VIMP_GAMES_DIR), — вне node_modules движка.
|
|
244
|
+
// Сырой ERR_MODULE_NOT_FOUND называет только ненайденный пакет и молчит о
|
|
245
|
+
// том, чья половина и какого пакета игры его просит
|
|
246
|
+
function importDefault(baseDir, entry, assetsBase, manifestPath) {
|
|
199
247
|
const file = path.resolve(baseDir, stripBase(entry, assetsBase));
|
|
200
248
|
|
|
201
|
-
return import(pathToFileURL(file).href).then(
|
|
249
|
+
return import(pathToFileURL(file).href).then(
|
|
250
|
+
module => module.default,
|
|
251
|
+
err => {
|
|
252
|
+
if (err?.code !== 'ERR_MODULE_NOT_FOUND') {
|
|
253
|
+
throw err;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// имя ненайденного пакета — из текста ошибки; не нашлось — не
|
|
257
|
+
// подставляем
|
|
258
|
+
const missing = /Cannot find package '([^']+)'/.exec(err.message)?.[1];
|
|
259
|
+
|
|
260
|
+
throw new Error(
|
|
261
|
+
`${manifestPath}: entry '${entry}'` +
|
|
262
|
+
`${missing ? ` imports '${missing}', which` : ''} cannot be ` +
|
|
263
|
+
'resolved by Node — a plugin half loaded in Node must be ' +
|
|
264
|
+
'self-contained; external dependencies (pixi.js) are allowed only ' +
|
|
265
|
+
'in the client half and only in the browser',
|
|
266
|
+
);
|
|
267
|
+
},
|
|
268
|
+
);
|
|
202
269
|
}
|
|
203
270
|
|
|
204
271
|
// entries.host/client — URL-ы, какими их видит браузер (`assetsBase` + путь
|