@antha/multiplayer-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-CC0 +121 -0
- package/LICENSE-MIT +21 -0
- package/README.md +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/multiplayer-api/errors.d.ts +13 -0
- package/dist/multiplayer-api/errors.js +15 -0
- package/dist/multiplayer-api/multiplayer-api.d.ts +215 -0
- package/dist/multiplayer-api/multiplayer-api.js +123 -0
- package/dist/multiplayer-api/multiplayer-client.d.ts +25 -0
- package/dist/multiplayer-api/multiplayer-client.js +18 -0
- package/dist/multiplayer-api/multiplayer-controller.d.ts +417 -0
- package/dist/multiplayer-api/multiplayer-controller.js +331 -0
- package/dist/multiplayer-api/multiplayer-socket-messages.d.ts +141 -0
- package/dist/multiplayer-api/multiplayer-socket-messages.js +114 -0
- package/dist/multiplayer-id.d.ts +59 -0
- package/dist/multiplayer-id.js +38 -0
- package/dist/room-handler-server/mock-room-handler-server-api-client.d.ts +47 -0
- package/dist/room-handler-server/mock-room-handler-server-api-client.js +107 -0
- package/dist/room-handler-server/multiplayer-room-handler.d.ts +121 -0
- package/dist/room-handler-server/multiplayer-room-handler.js +216 -0
- package/dist/webrtc/web-rtc-communication.d.ts +64 -0
- package/dist/webrtc/web-rtc-communication.js +54 -0
- package/dist/webrtc/webrtc-controller.d.ts +104 -0
- package/dist/webrtc/webrtc-controller.js +170 -0
- package/dist/webrtc/webrtc-multiplayer-controller.d.ts +263 -0
- package/dist/webrtc/webrtc-multiplayer-controller.js +397 -0
- package/package.json +62 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { assert, waitUntil } from '@augment-vir/assert';
|
|
2
|
+
import { PromiseQueue, ensureErrorAndPrependMessage, extractErrorMessage, filterMap, getObjectTypedValues, log, makeWritable, mergeDefinedProperties, randomString, removeDuplicates, stringify, } from '@augment-vir/common';
|
|
3
|
+
import { ListenTarget, defineTypedCustomEvent } from 'typed-event-target';
|
|
4
|
+
import { multiplayerConnectWebSocket, } from '../multiplayer-api/multiplayer-api.js';
|
|
5
|
+
import { createMultiplayerId } from '../multiplayer-id.js';
|
|
6
|
+
import { MultiplayerWebSocketMessageType } from './web-rtc-communication.js';
|
|
7
|
+
import { WebrtcConnectEvent, WebrtcController, WebrtcMessageEvent } from './webrtc-controller.js';
|
|
8
|
+
/**
|
|
9
|
+
* An event that is omitted from {@link WebrtcController} when a WebRTC message is received.
|
|
10
|
+
*
|
|
11
|
+
* @category Internal
|
|
12
|
+
*/
|
|
13
|
+
export class WebrtcMultiplayerMessageEvent extends defineTypedCustomEvent()('webrtc-multiplayer-message') {
|
|
14
|
+
sourceClientId;
|
|
15
|
+
constructor(sourceClientId, detail) {
|
|
16
|
+
super({
|
|
17
|
+
detail,
|
|
18
|
+
});
|
|
19
|
+
this.sourceClientId = sourceClientId;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* An event that is omitted from {@link WebrtcMultiplayerController} when the multiplayer room host
|
|
24
|
+
* is updated.
|
|
25
|
+
*
|
|
26
|
+
* @category Internal
|
|
27
|
+
*/
|
|
28
|
+
export class WebrtcMultiplayerConnectionUpdateEvent extends defineTypedCustomEvent()('webrtc-multiplayer-connection-update') {
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A helper for creating a new empty room.
|
|
32
|
+
*
|
|
33
|
+
* @category Internal
|
|
34
|
+
*/
|
|
35
|
+
export function createNewRoom(params = {}) {
|
|
36
|
+
return mergeDefinedProperties({
|
|
37
|
+
roomId: createMultiplayerId.room(),
|
|
38
|
+
roomName: '',
|
|
39
|
+
roomPassword: '',
|
|
40
|
+
}, params);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A controller that connects to the multiplayer api and establishes a WebRTC connection to the
|
|
44
|
+
* selected room, or, if the room does not exist yet, creates the room and becomes the host.
|
|
45
|
+
*
|
|
46
|
+
* Make sure, after constructing this class, to call
|
|
47
|
+
* {@link WebrtcMultiplayerController.initConnection} when you're ready to being the connection.
|
|
48
|
+
*
|
|
49
|
+
* @category Internal
|
|
50
|
+
*/
|
|
51
|
+
export class WebrtcMultiplayerController extends ListenTarget {
|
|
52
|
+
gameId;
|
|
53
|
+
multiplayerApiClient;
|
|
54
|
+
stunServerUrls;
|
|
55
|
+
multiplayerRoom;
|
|
56
|
+
clientId;
|
|
57
|
+
shouldAllowConnectionCheck;
|
|
58
|
+
hostClientId;
|
|
59
|
+
/**
|
|
60
|
+
* Connections between multiple WebRTC peers.
|
|
61
|
+
*
|
|
62
|
+
* A connection with the current client's id is the init connection.
|
|
63
|
+
*/
|
|
64
|
+
connections = {};
|
|
65
|
+
webSocket;
|
|
66
|
+
clientSecret = randomString(32);
|
|
67
|
+
isDestroyed = false;
|
|
68
|
+
constructor(gameId, multiplayerApiClient, stunServerUrls, multiplayerRoom,
|
|
69
|
+
/** The randomized client id for this controller and client. */
|
|
70
|
+
clientId = createMultiplayerId.client(),
|
|
71
|
+
/**
|
|
72
|
+
* This is fired when a WebRTC peer attempts to connect to the host client (this will only
|
|
73
|
+
* be fired if your client is the host). Return `true` to accept the connection. Return
|
|
74
|
+
* `false` to reject it.
|
|
75
|
+
*
|
|
76
|
+
* @default accept all connections
|
|
77
|
+
*/
|
|
78
|
+
shouldAllowConnectionCheck = () => true) {
|
|
79
|
+
super();
|
|
80
|
+
this.gameId = gameId;
|
|
81
|
+
this.multiplayerApiClient = multiplayerApiClient;
|
|
82
|
+
this.stunServerUrls = stunServerUrls;
|
|
83
|
+
this.multiplayerRoom = multiplayerRoom;
|
|
84
|
+
this.clientId = clientId;
|
|
85
|
+
this.shouldAllowConnectionCheck = shouldAllowConnectionCheck;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* If this controller is the host, it'll behave differently:
|
|
89
|
+
*
|
|
90
|
+
* - Hosts hold WebRTC connections to all clients (non-hosts only hold a WebRTC connection to the
|
|
91
|
+
* host).
|
|
92
|
+
* - Hosts hold a WebSocket connection to the signal server so they can receive more clients at
|
|
93
|
+
* any time.
|
|
94
|
+
*/
|
|
95
|
+
isHost() {
|
|
96
|
+
return this.hostClientId === this.clientId;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get all connected client ids.
|
|
100
|
+
*
|
|
101
|
+
* - For host clients, this indicates how many member clients are connected to the host client,
|
|
102
|
+
* _not_ including the host itself.
|
|
103
|
+
* - For non-host clients, this only lists the local connection used to reach the host.
|
|
104
|
+
*
|
|
105
|
+
* For host clients, this does ont include the host client id whereas
|
|
106
|
+
* {@link WebrtcMultiplayerController.getAllClientIds} does.
|
|
107
|
+
*/
|
|
108
|
+
getConnectedClientIds() {
|
|
109
|
+
const connectedClientIds = filterMap(getObjectTypedValues(this.connections), (connection) => connection.clientId, (clientId, connection) => {
|
|
110
|
+
return connection.isConnected;
|
|
111
|
+
});
|
|
112
|
+
return connectedClientIds;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get all room client ids.
|
|
116
|
+
*
|
|
117
|
+
* - For host clients, this indicates how many clients are connected to the room, including the
|
|
118
|
+
* host client itself.
|
|
119
|
+
* - For non-host clients, this includes the member client and the host client once connected.
|
|
120
|
+
*
|
|
121
|
+
* For host clients, this includes the host client id whereas
|
|
122
|
+
* {@link WebrtcMultiplayerController.getConnectedClientIds} does not.
|
|
123
|
+
*/
|
|
124
|
+
getAllClientIds() {
|
|
125
|
+
const connectedClientIds = this.getConnectedClientIds();
|
|
126
|
+
const allClients = this.isHost()
|
|
127
|
+
? [
|
|
128
|
+
...connectedClientIds,
|
|
129
|
+
this.clientId,
|
|
130
|
+
]
|
|
131
|
+
: this.hostClientId && this.isConnected()
|
|
132
|
+
? [
|
|
133
|
+
...connectedClientIds,
|
|
134
|
+
this.clientId,
|
|
135
|
+
this.hostClientId,
|
|
136
|
+
]
|
|
137
|
+
: connectedClientIds;
|
|
138
|
+
return removeDuplicates(allClients);
|
|
139
|
+
}
|
|
140
|
+
/** Indicates whether ths client is connected to a multiplayer room. */
|
|
141
|
+
isConnected() {
|
|
142
|
+
return this.isHost() || !!this.getConnectedClientIds().length;
|
|
143
|
+
}
|
|
144
|
+
/** Destroy this controller and clean everything up. */
|
|
145
|
+
destroy() {
|
|
146
|
+
makeWritable(this).isDestroyed = true;
|
|
147
|
+
Object.values(this.connections).forEach((connection) => connection.destroy());
|
|
148
|
+
void this.webSocket?.close();
|
|
149
|
+
this.connectionQueue.destroy();
|
|
150
|
+
super.destroy();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Send a message to the room participants.
|
|
154
|
+
*
|
|
155
|
+
* - If the current client is the room host, this message is sent to all other room clients.
|
|
156
|
+
* - If the current client is just a room member (not the host), the message is sent to the host.
|
|
157
|
+
*/
|
|
158
|
+
sendMessage(data) {
|
|
159
|
+
/**
|
|
160
|
+
* When this client is the host, this set of connections will be the all the member clients.
|
|
161
|
+
* When this client is a member client, there will only be one connection and it will be the
|
|
162
|
+
* host client.
|
|
163
|
+
*/
|
|
164
|
+
Object.values(this.connections).forEach((connection) => {
|
|
165
|
+
if (!connection.isConnected) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
connection.sendMessage(data);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
log.error(extractErrorMessage(error));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/** Send a message to just a single client. This is only allowed on a host client. */
|
|
177
|
+
sendToOnlyOneClient(clientId, data) {
|
|
178
|
+
if (!this.isHost()) {
|
|
179
|
+
log.error(new Error('Cannot send to an individual client as not a host.'));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const client = this.connections[clientId];
|
|
183
|
+
if (!client || !client.isConnected) {
|
|
184
|
+
log.error(new Error(`Cannot send to missing or disconnected client ('${clientId}')`));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
client.sendMessage(data);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Call this to connect to the multiplayer server.
|
|
191
|
+
*
|
|
192
|
+
* @returns Whether or not the connection was initialized (it won't be initialized, for example,
|
|
193
|
+
* if the WebRTC connections already exist).
|
|
194
|
+
*/
|
|
195
|
+
async initConnection() {
|
|
196
|
+
if (Object.values(this.connections).length) {
|
|
197
|
+
// connections already exist
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
const newConnection = this.createNewConnection(this.clientId);
|
|
201
|
+
const newOffer = await newConnection.createOffer(this.stunServerUrls);
|
|
202
|
+
const webSocket = await this.setupWebSocket();
|
|
203
|
+
const reply = await webSocket.sendAndWaitForReply({
|
|
204
|
+
message: {
|
|
205
|
+
messageId: createMultiplayerId.socketMessage(),
|
|
206
|
+
type: MultiplayerWebSocketMessageType.Offer,
|
|
207
|
+
clientId: this.clientId,
|
|
208
|
+
clientSecret: this.clientSecret,
|
|
209
|
+
data: newOffer,
|
|
210
|
+
...this.multiplayerRoom,
|
|
211
|
+
},
|
|
212
|
+
replyCheck(message) {
|
|
213
|
+
return message.type === MultiplayerWebSocketMessageType.OfferResult;
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
assert.strictEquals(reply.type, MultiplayerWebSocketMessageType.OfferResult);
|
|
217
|
+
/**
|
|
218
|
+
* `hostClientId` will be set by the already attached listener. We just need to wait until
|
|
219
|
+
* it does, because we need to know who the host is before calling `sendHostPing`.
|
|
220
|
+
*/
|
|
221
|
+
await waitUntil.isDefined(() => this.hostClientId);
|
|
222
|
+
this.sendHostPing();
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
sendHostPing() {
|
|
226
|
+
if (this.isHost() && this.webSocket) {
|
|
227
|
+
this.webSocket.send({
|
|
228
|
+
messageId: createMultiplayerId.socketMessage(),
|
|
229
|
+
type: MultiplayerWebSocketMessageType.HostPing,
|
|
230
|
+
clientCount: this.getAllClientIds().length,
|
|
231
|
+
clientId: this.clientId,
|
|
232
|
+
clientSecret: this.clientSecret,
|
|
233
|
+
...this.multiplayerRoom,
|
|
234
|
+
});
|
|
235
|
+
setTimeout(() => this.sendHostPing(), 1000);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
connectionQueue = new PromiseQueue();
|
|
239
|
+
async setupWebSocket() {
|
|
240
|
+
if (this.webSocket &&
|
|
241
|
+
(this.webSocket.readyState === WebSocket.OPEN ||
|
|
242
|
+
this.webSocket.readyState === WebSocket.CONNECTING)) {
|
|
243
|
+
return this.webSocket;
|
|
244
|
+
}
|
|
245
|
+
const webSocket = await this.multiplayerApiClient.connectWebSocket(multiplayerConnectWebSocket, {
|
|
246
|
+
searchParams: {
|
|
247
|
+
gameId: [this.gameId],
|
|
248
|
+
},
|
|
249
|
+
listeners: {
|
|
250
|
+
message: async ({ message, webSocket }) => {
|
|
251
|
+
try {
|
|
252
|
+
if (message.type === MultiplayerWebSocketMessageType.Offer) {
|
|
253
|
+
if (!this.isHost()) {
|
|
254
|
+
throw new Error('Non-host multiplayer client received a WebRTC offer.');
|
|
255
|
+
}
|
|
256
|
+
const baseAnswerMessageProperties = {
|
|
257
|
+
messageId: message.messageId,
|
|
258
|
+
type: MultiplayerWebSocketMessageType.Answer,
|
|
259
|
+
roomId: message.roomId,
|
|
260
|
+
roomName: message.roomName,
|
|
261
|
+
clientId: message.clientId,
|
|
262
|
+
};
|
|
263
|
+
await this.connectionQueue.add(async () => {
|
|
264
|
+
if (!this.shouldAllowConnectionCheck({
|
|
265
|
+
connectingClientId: message.clientId,
|
|
266
|
+
controller: this,
|
|
267
|
+
})) {
|
|
268
|
+
log.warning('offer rejected');
|
|
269
|
+
webSocket.send({
|
|
270
|
+
...baseAnswerMessageProperties,
|
|
271
|
+
data: {
|
|
272
|
+
rejected: true,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
log.faint('received offer');
|
|
278
|
+
const newConnection = this.createNewConnection(message.clientId);
|
|
279
|
+
const answer = await newConnection.createAnswer(message.data, this.stunServerUrls);
|
|
280
|
+
webSocket.send({
|
|
281
|
+
...baseAnswerMessageProperties,
|
|
282
|
+
data: answer,
|
|
283
|
+
});
|
|
284
|
+
await waitUntil.isTrue(() => newConnection.isConnected);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else if (message.type === MultiplayerWebSocketMessageType.Answer) {
|
|
288
|
+
if (this.isHost()) {
|
|
289
|
+
throw new Error('Host multiplayer client received a WebRTC answer.');
|
|
290
|
+
}
|
|
291
|
+
/** A connection with the current id is the init connection. */
|
|
292
|
+
const initConnection = this.connections[this.clientId];
|
|
293
|
+
if ('rejected' in message.data) {
|
|
294
|
+
log.warning('offer was rejected');
|
|
295
|
+
this.destroy();
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
log.faint('received answer');
|
|
299
|
+
if (!initConnection) {
|
|
300
|
+
throw new Error('Cannot accept answer, no init connection found.');
|
|
301
|
+
}
|
|
302
|
+
await initConnection.acceptAnswer(message.data);
|
|
303
|
+
/**
|
|
304
|
+
* This client does not need a WebSocket connection anymore if
|
|
305
|
+
* it is not the host.
|
|
306
|
+
*/
|
|
307
|
+
await webSocket.close();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else if (message.type === MultiplayerWebSocketMessageType.OfferResult) {
|
|
311
|
+
if (message.hostClientId !== this.hostClientId) {
|
|
312
|
+
makeWritable(this).hostClientId = message.hostClientId;
|
|
313
|
+
if (this.isHost()) {
|
|
314
|
+
const initConnection = this.connections[this.clientId];
|
|
315
|
+
/**
|
|
316
|
+
* Remove the init connection since it won't be used now
|
|
317
|
+
* that this instance is the host.
|
|
318
|
+
*/
|
|
319
|
+
delete this.connections[this.clientId];
|
|
320
|
+
initConnection?.destroy();
|
|
321
|
+
}
|
|
322
|
+
this.dispatch(new WebrtcMultiplayerConnectionUpdateEvent({
|
|
323
|
+
detail: {
|
|
324
|
+
newHost: message.hostClientId,
|
|
325
|
+
},
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else if (message.type === MultiplayerWebSocketMessageType.Error) {
|
|
330
|
+
throw new Error(message.errorMessage);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
throw new Error(`Unexpected ${WebrtcMultiplayerController.name} WebSocket message type: ${message.type}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
log.error(ensureErrorAndPrependMessage(error, `WebSocket message failed: ${stringify(message)}`));
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
error: (error) => {
|
|
341
|
+
log.error(error);
|
|
342
|
+
},
|
|
343
|
+
close: () => {
|
|
344
|
+
this.webSocket = undefined;
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
this.webSocket = webSocket;
|
|
349
|
+
return webSocket;
|
|
350
|
+
}
|
|
351
|
+
createNewConnection(clientId) {
|
|
352
|
+
const newController = new WebrtcController(clientId);
|
|
353
|
+
this.connections[clientId] = newController;
|
|
354
|
+
newController.listen(WebrtcConnectEvent, (event) => {
|
|
355
|
+
const connectionEstablished = event.detail;
|
|
356
|
+
if (connectionEstablished) {
|
|
357
|
+
if (clientId !== this.clientId) {
|
|
358
|
+
this.dispatch(new WebrtcMultiplayerConnectionUpdateEvent({
|
|
359
|
+
detail: {
|
|
360
|
+
newMember: clientId,
|
|
361
|
+
},
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
newController.destroy();
|
|
367
|
+
delete this.connections[clientId];
|
|
368
|
+
if (this.isHost()) {
|
|
369
|
+
this.dispatch(new WebrtcMultiplayerConnectionUpdateEvent({
|
|
370
|
+
detail: {
|
|
371
|
+
lostMember: clientId,
|
|
372
|
+
},
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
this.dispatch(new WebrtcMultiplayerConnectionUpdateEvent({
|
|
377
|
+
detail: {
|
|
378
|
+
lostHost: clientId,
|
|
379
|
+
},
|
|
380
|
+
}));
|
|
381
|
+
/**
|
|
382
|
+
* If this member client has lost connection to its host, we've got to get it
|
|
383
|
+
* back!
|
|
384
|
+
*/
|
|
385
|
+
void this.initConnection();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
newController.listen(WebrtcMessageEvent, (event) => {
|
|
390
|
+
const sourceId = clientId === this.clientId ? this.hostClientId : clientId;
|
|
391
|
+
if (sourceId) {
|
|
392
|
+
this.dispatch(new WebrtcMultiplayerMessageEvent(sourceId, event.detail));
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
return newController;
|
|
396
|
+
}
|
|
397
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@antha/multiplayer-core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Core functionalities for Antha multiplayer mods.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vir",
|
|
7
|
+
"antha",
|
|
8
|
+
"WebRTC",
|
|
9
|
+
"signal server",
|
|
10
|
+
"multiplayer",
|
|
11
|
+
"gaming"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/electrovir/antha",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/electrovir/antha/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/electrovir/antha.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "(MIT OR CC0-1.0)",
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "electrovir",
|
|
24
|
+
"url": "https://github.com/electrovir"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "dist/index.js",
|
|
29
|
+
"module": "dist/index.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"compile": "virmator compile",
|
|
33
|
+
"docs": "virmator docs",
|
|
34
|
+
"test": "virmator test web",
|
|
35
|
+
"test:coverage": "npm test coverage",
|
|
36
|
+
"test:docs": "virmator docs check"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@augment-vir/assert": "^31.73.1",
|
|
40
|
+
"@augment-vir/common": "^31.73.1",
|
|
41
|
+
"@rest-vir/api": "^2.2.0",
|
|
42
|
+
"@sinclair/typebox": "^0.34.49",
|
|
43
|
+
"date-vir": "^8.5.0",
|
|
44
|
+
"object-shape-tester": "^6.14.0",
|
|
45
|
+
"type-fest": "^5.7.0",
|
|
46
|
+
"typed-event-target": "^4.3.1",
|
|
47
|
+
"url-vir": "^2.1.9"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@augment-vir/test": "^31.73.1",
|
|
51
|
+
"@web/dev-server-esbuild": "^1.0.5",
|
|
52
|
+
"@web/test-runner": "^0.20.2",
|
|
53
|
+
"@web/test-runner-playwright": "^0.11.1",
|
|
54
|
+
"istanbul-smart-text-reporter": "^1.1.5"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=22"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
}
|
|
62
|
+
}
|