@anvil-js/client 0.0.1 → 0.0.3

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.
@@ -0,0 +1,589 @@
1
+ 'use strict';
2
+
3
+ var uuid = require('uuid');
4
+
5
+ // src/ws/codec.ts
6
+ var ConnectionCodec = class {
7
+ /** typeId → packetName (frames we receive) */
8
+ incoming = /* @__PURE__ */ new Map();
9
+ /** packetName → typeId (frames we send) */
10
+ outgoing = /* @__PURE__ */ new Map();
11
+ nextOutgoingTypeId = 100;
12
+ constructor() {
13
+ this.incoming.set(0, "connection.ConnectionRegisterPacketTypeIdPacket");
14
+ this.outgoing.set("connection.ConnectionRegisterPacketTypeIdPacket", 0);
15
+ }
16
+ /**
17
+ * Decode a single binary frame. Returns null for registration
18
+ * frames (which mutate the codec in place) and for unknown
19
+ * typeIds.
20
+ */
21
+ decode(buffer) {
22
+ const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
23
+ if (view.byteLength < 4) return null;
24
+ const dv = new DataView(view.buffer, view.byteOffset, view.byteLength);
25
+ let off = 0;
26
+ const typeId = dv.getInt32(off);
27
+ off += 4;
28
+ const name = this.incoming.get(typeId);
29
+ if (!name) return null;
30
+ const idLen = dv.getInt32(off);
31
+ off += 4;
32
+ const id = new TextDecoder("utf-8").decode(view.subarray(off, off + idLen));
33
+ off += idLen;
34
+ const payloadLen = dv.getInt32(off);
35
+ off += 4;
36
+ const payloadJson = new TextDecoder("utf-8").decode(view.subarray(off, off + payloadLen));
37
+ if (name === "connection.ConnectionRegisterPacketTypeIdPacket") {
38
+ const reg = JSON.parse(payloadJson);
39
+ this.incoming.set(reg.b, reg.a);
40
+ return null;
41
+ }
42
+ return { type: name, id, payload: JSON.parse(payloadJson) };
43
+ }
44
+ /**
45
+ * Encode a packet to a binary frame. If the packet name has
46
+ * not been registered yet, a fresh typeId is allocated and
47
+ * a registration frame is yielded first.
48
+ */
49
+ encode(name, payload, id = uuid.v4()) {
50
+ const frames = [];
51
+ if (!this.outgoing.has(name)) {
52
+ this.outgoing.set(name, this.nextOutgoingTypeId++);
53
+ frames.push(
54
+ ...this.encode(
55
+ "connection.ConnectionRegisterPacketTypeIdPacket",
56
+ { a: name, b: this.outgoing.get(name) },
57
+ ""
58
+ )
59
+ );
60
+ }
61
+ const typeId = this.outgoing.get(name);
62
+ const payloadBytes = new TextEncoder().encode(JSON.stringify(payload));
63
+ const idBytes = id ? new TextEncoder().encode(id) : new Uint8Array(0);
64
+ const total = 4 + 4 + idBytes.length + 4 + payloadBytes.length;
65
+ const buf = new Uint8Array(total);
66
+ const dv = new DataView(buf.buffer);
67
+ let off = 0;
68
+ dv.setInt32(off, typeId);
69
+ off += 4;
70
+ dv.setInt32(off, idBytes.length);
71
+ off += 4;
72
+ buf.set(idBytes, off);
73
+ off += idBytes.length;
74
+ dv.setInt32(off, payloadBytes.length);
75
+ off += 4;
76
+ buf.set(payloadBytes, off);
77
+ frames.push(buf);
78
+ return frames;
79
+ }
80
+ /** Register a packet type up-front (returns the typeId assigned). */
81
+ registerOutgoing(name) {
82
+ if (!this.outgoing.has(name)) {
83
+ this.outgoing.set(name, this.nextOutgoingTypeId++);
84
+ }
85
+ return this.outgoing.get(name);
86
+ }
87
+ };
88
+
89
+ // src/ws/client.ts
90
+ var AnvilWsClient = class {
91
+ ws = null;
92
+ codec = new ConnectionCodec();
93
+ config;
94
+ listeners = /* @__PURE__ */ new Map();
95
+ pendingRequests = /* @__PURE__ */ new Map();
96
+ state = "disconnected";
97
+ bootstrapReceived = false;
98
+ reconnectTimer = null;
99
+ defaultRequestTimeoutMs = 1e4;
100
+ constructor(config) {
101
+ this.config = {
102
+ maxProtocolVersion: 9,
103
+ autoReconnect: true,
104
+ reconnectDelayMs: 2e3,
105
+ authenticationToken: "",
106
+ ...config
107
+ };
108
+ }
109
+ // ----- Lifecycle -----
110
+ connect() {
111
+ return new Promise((resolve, reject) => {
112
+ if (this.state === "ready" || this.state === "connected") {
113
+ resolve();
114
+ return;
115
+ }
116
+ this.state = "connecting";
117
+ const params = new URLSearchParams();
118
+ params.set("essential-user-uuid", this.config.userUuid);
119
+ params.set("essential-user-name", this.config.userName);
120
+ params.set("essential-max-protocol-version", String(this.config.maxProtocolVersion));
121
+ if (this.config.authenticationToken) {
122
+ params.set("essential-authentication-token", this.config.authenticationToken);
123
+ }
124
+ const url = `${this.config.url}?${params.toString()}`;
125
+ this.ws = new WebSocket(url);
126
+ this.ws.binaryType = "arraybuffer";
127
+ this.ws.onopen = () => {
128
+ this.state = "connected";
129
+ this.emit("open", void 0);
130
+ this.preRegisterOutgoing();
131
+ resolve();
132
+ };
133
+ this.ws.onerror = (e) => {
134
+ this.emit("error", new Error("WebSocket error"));
135
+ reject(new Error("WebSocket error"));
136
+ };
137
+ this.ws.onclose = (ev) => {
138
+ this.state = "disconnected";
139
+ this.emit("close", { code: ev.code, reason: ev.reason });
140
+ for (const [, req] of this.pendingRequests) {
141
+ clearTimeout(req.timer);
142
+ req.reject(new Error("Connection closed"));
143
+ }
144
+ this.pendingRequests.clear();
145
+ if (this.config.autoReconnect && this.bootstrapReceived) {
146
+ this.scheduleReconnect();
147
+ }
148
+ };
149
+ this.ws.onmessage = (ev) => {
150
+ this.handleFrame(ev.data);
151
+ };
152
+ });
153
+ }
154
+ disconnect() {
155
+ this.config.autoReconnect = false;
156
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
157
+ if (this.ws) {
158
+ this.ws.close(1e3, "Client disconnect");
159
+ this.ws = null;
160
+ }
161
+ }
162
+ scheduleReconnect() {
163
+ if (this.reconnectTimer) return;
164
+ this.reconnectTimer = setTimeout(() => {
165
+ this.reconnectTimer = null;
166
+ this.connect().catch(() => {
167
+ });
168
+ }, this.config.reconnectDelayMs);
169
+ }
170
+ preRegisterOutgoing() {
171
+ const TYPES = [
172
+ "chat.ClientChatChannelMessageCreatePacket",
173
+ "chat.ClientChatChannelMessagesRetrievePacket",
174
+ "chat.ClientChatChannelCreatePacket",
175
+ "chat.ClientChatChannelMessageUpdatePacket",
176
+ "chat.ChatChannelMessageDeletePacket",
177
+ "chat.ClientChatChannelMessageReportPacket",
178
+ "chat.ChatChannelMemberAddPacket",
179
+ "chat.ChatChannelMemberRemovePacket",
180
+ "chat.ClientChatChannelMutePacket",
181
+ "chat.ClientChatChannelReadStatePacket",
182
+ "chat.ClientChatChannelMessageReadStatePacket",
183
+ "relationships.ClientRelationshipCreatePacket",
184
+ "relationships.RelationshipDeletePacket",
185
+ "relationships.ClientLookupUuidByNamePacket",
186
+ "profile.ClientProfileRequestPacket",
187
+ "profile.ClientProfileActivityPacket",
188
+ "cosmetic.ClientCosmeticRequestPacket",
189
+ "cosmetic.ClientCosmeticBulkRequestUnlockStatePacket",
190
+ "cosmetic.ClientCosmeticAnimationTriggerPacket",
191
+ "skin.ClientSkinCreatePacket",
192
+ "skin.ClientSkinUpdateLastUsedStatePacket",
193
+ "skin.ClientSkinUpdateFavoriteStatePacket",
194
+ "skin.ClientSelectedSkinsRequestPacket",
195
+ "cosmetic.outfit.ClientCosmeticOutfitCreatePacket",
196
+ "cosmetic.outfit.ClientCosmeticOutfitSelectPacket",
197
+ "cosmetic.outfit.ClientCosmeticOutfitEquippedCosmeticsUpdatePacket",
198
+ "cosmetic.emote.ClientCosmeticEmoteWheelUpdatePacket",
199
+ "cosmetic.emote.ClientCosmeticEmoteWheelSelectPacket",
200
+ "wardrobe.ClientWardrobeSettingsPacket",
201
+ "checkout.ClientCheckoutCosmeticsPacket",
202
+ "coins.ClientCoinsBalancePacket",
203
+ "notices.ClientNoticeRequestPacket",
204
+ "serverdiscovery.ClientServerDiscoveryRequestPacket",
205
+ "knownservers.ClientKnownServersRequestPacket",
206
+ "multiplayer.ClientMultiplayerIceRelayPacket",
207
+ "pingproxy.ClientPingProxyRequestPacket",
208
+ "social.ClientCommunityRulesAgreedPacket"
209
+ ];
210
+ for (const t of TYPES) this.codec.registerOutgoing(t);
211
+ }
212
+ // ----- Frame handling -----
213
+ handleFrame(data) {
214
+ const packet = this.codec.decode(data);
215
+ if (!packet) return;
216
+ this.emit("raw", packet);
217
+ this.dispatch(packet);
218
+ }
219
+ dispatch(packet) {
220
+ if (packet.id && this.pendingRequests.has(packet.id)) {
221
+ const req = this.pendingRequests.get(packet.id);
222
+ this.pendingRequests.delete(packet.id);
223
+ clearTimeout(req.timer);
224
+ req.resolve(packet);
225
+ return;
226
+ }
227
+ if (!this.bootstrapReceived && packet.type === "social.ServerCommunityRulesStatePacket") {
228
+ this.bootstrapReceived = true;
229
+ this.state = "ready";
230
+ this.emit("bootstrap", void 0);
231
+ }
232
+ switch (packet.type) {
233
+ case "chat.ServerChatChannelMessagePacket": {
234
+ const msgs = packet.payload.a || [];
235
+ for (const m of msgs) {
236
+ this.emit("chat", {
237
+ id: m.a,
238
+ channelId: m.b ?? 0,
239
+ senderUuid: m.c,
240
+ content: m.d,
241
+ replyToId: m.e ?? null,
242
+ editedAt: m.f ?? null,
243
+ deleted: m.g ?? false,
244
+ createdAt: m.h ?? Date.now()
245
+ });
246
+ }
247
+ break;
248
+ }
249
+ case "relationships.ServerRelationshipPopulatePacket": {
250
+ const rels = packet.payload.a || [];
251
+ for (const r of rels) {
252
+ this.emit("friendAdded", {
253
+ userA: r.a,
254
+ userB: r.b,
255
+ type: r.c,
256
+ status: r.d,
257
+ since: r.e
258
+ });
259
+ }
260
+ break;
261
+ }
262
+ case "relationships.ServerRelationshipDeletedPacket": {
263
+ const dels = packet.payload.a || [];
264
+ for (const d of dels) {
265
+ this.emit("friendRemoved", { userA: d.a, userB: d.b, type: d.c });
266
+ }
267
+ break;
268
+ }
269
+ case "profile.ServerProfileStatusPacket": {
270
+ this.emit("profileStatus", {
271
+ uuid: packet.payload.a,
272
+ status: packet.payload.b,
273
+ lastOnlineTimestamp: packet.payload.lastOnlineTimestamp ?? 0
274
+ });
275
+ break;
276
+ }
277
+ case "profile.ServerProfileActivityPacket": {
278
+ this.emit("profileActivity", {
279
+ uuid: packet.payload.a,
280
+ activity: packet.payload.b,
281
+ metadata: packet.payload.c
282
+ });
283
+ break;
284
+ }
285
+ case "cosmetic.ServerCosmeticAnimationTriggerPacket": {
286
+ this.emit("cosmeticAnimation", {
287
+ userUuid: packet.payload.a,
288
+ cosmeticId: packet.payload.b,
289
+ animationId: packet.payload.c
290
+ });
291
+ break;
292
+ }
293
+ case "notices.ServerNoticePopulatePacket": {
294
+ const items = packet.payload.items || packet.payload.a || [];
295
+ for (const n of items) {
296
+ this.emit("notice", { id: n.a, title: n.b, body: n.c, category: n.d });
297
+ }
298
+ break;
299
+ }
300
+ case "cosmetic.ServerCosmeticsUserUnlockedPacket": {
301
+ this.emit("cosmeticsUnlocked", {
302
+ userUuid: packet.payload.c,
303
+ unlockedIds: packet.payload.a ?? [],
304
+ gifted: packet.payload.b,
305
+ unlockMap: packet.payload.d ?? {}
306
+ });
307
+ break;
308
+ }
309
+ case "cosmetic.ServerCosmeticsUserEquippedPacket": {
310
+ this.emit("equippedUpdate", {
311
+ userUuid: packet.payload.a,
312
+ equipped: packet.payload.b
313
+ });
314
+ break;
315
+ }
316
+ case "cosmetic.ServerCosmeticPlayerSettingsPacket": {
317
+ this.emit("playerSettings", {
318
+ userUuid: packet.payload.a,
319
+ settings: packet.payload.b
320
+ });
321
+ break;
322
+ }
323
+ case "cosmetic.ServerCosmeticsSkinTexturePacket": {
324
+ this.emit("skinTexture", {
325
+ userUuid: packet.payload.a,
326
+ skinTexture: packet.payload.b
327
+ });
328
+ break;
329
+ }
330
+ case "upnp.ServerUPnPSessionPopulatePacket": {
331
+ const sessions = packet.payload.a || [];
332
+ for (const s of sessions) {
333
+ this.emit("upnpSession", {
334
+ hostUuid: s.a,
335
+ ip: s.b,
336
+ port: s.c,
337
+ privacy: s.d,
338
+ worldName: s.e,
339
+ protocolVersion: s.f,
340
+ invites: s.g ?? [],
341
+ createdAt: s.h,
342
+ rawStatus: s.i
343
+ });
344
+ }
345
+ break;
346
+ }
347
+ case "upnp.ServerUPnPSessionRemovePacket": {
348
+ const removed = packet.payload.a || [];
349
+ for (const hostUuid of removed) {
350
+ this.emit("upnpSessionRemoved", { hostUuid });
351
+ }
352
+ break;
353
+ }
354
+ case "upnp.ServerUPnPSessionInviteAddPacket": {
355
+ this.emit("upnpInvite", { hostUuid: packet.payload.a });
356
+ break;
357
+ }
358
+ case "serverdiscovery.ServerServerDiscoveryResponsePacket": {
359
+ this.emit("serverList", {
360
+ recommended: packet.payload.recommended || [],
361
+ featured: packet.payload.featured || []
362
+ });
363
+ break;
364
+ }
365
+ }
366
+ }
367
+ // ----- Request/response -----
368
+ sendRequest(type, payload, timeoutMs = this.defaultRequestTimeoutMs) {
369
+ return new Promise((resolve, reject) => {
370
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
371
+ reject(new Error("WebSocket not connected"));
372
+ return;
373
+ }
374
+ const id = crypto.randomUUID();
375
+ const timer = setTimeout(() => {
376
+ this.pendingRequests.delete(id);
377
+ reject(new Error(`Request ${type} timed out`));
378
+ }, timeoutMs);
379
+ this.pendingRequests.set(id, { resolve, reject, type, timer });
380
+ for (const frame of this.codec.encode(type, payload, id)) {
381
+ this.ws.send(frame);
382
+ }
383
+ });
384
+ }
385
+ // ----- High-level API -----
386
+ // Chat
387
+ sendChatMessage(channelId, content, replyToId) {
388
+ return this.sendRequest("chat.ClientChatChannelMessageCreatePacket", {
389
+ a: channelId,
390
+ b: content,
391
+ c: replyToId ?? null
392
+ });
393
+ }
394
+ retrieveChatMessages(channelId, limit = 50, beforeMessageId) {
395
+ return this.sendRequest("chat.ClientChatChannelMessagesRetrievePacket", {
396
+ a: channelId,
397
+ b: limit,
398
+ c: beforeMessageId ?? null
399
+ });
400
+ }
401
+ createChannel(type, name, memberUuids = []) {
402
+ return this.sendRequest("chat.ClientChatChannelCreatePacket", {
403
+ a: type,
404
+ b: name,
405
+ c: memberUuids
406
+ });
407
+ }
408
+ deleteChatMessage(messageId) {
409
+ return this.sendRequest("chat.ChatChannelMessageDeletePacket", { a: messageId });
410
+ }
411
+ // Social
412
+ addFriend(targetUuid) {
413
+ return this.sendRequest("relationships.ClientRelationshipCreatePacket", {
414
+ a: targetUuid,
415
+ b: "FRIENDS"
416
+ });
417
+ }
418
+ removeFriend(targetUuid) {
419
+ return this.sendRequest("relationships.RelationshipDeletePacket", {
420
+ a: targetUuid,
421
+ b: "FRIENDS"
422
+ });
423
+ }
424
+ blockUser(targetUuid) {
425
+ return this.sendRequest("relationships.ClientRelationshipCreatePacket", {
426
+ a: targetUuid,
427
+ b: "BLOCKED"
428
+ });
429
+ }
430
+ lookupUuidByName(username) {
431
+ return this.sendRequest("relationships.ClientLookupUuidByNamePacket", { username });
432
+ }
433
+ // Profile
434
+ getProfile(uuid) {
435
+ return this.sendRequest("profile.ClientProfileRequestPacket", { a: uuid });
436
+ }
437
+ setActivity(activity, metadata = {}) {
438
+ return this.sendRequest("profile.ClientProfileActivityPacket", { a: activity, c: metadata });
439
+ }
440
+ // Cosmetics
441
+ listCosmetics() {
442
+ return this.sendRequest("cosmetic.ClientCosmeticRequestPacket", {});
443
+ }
444
+ triggerCosmeticAnimation(cosmeticId, animationId) {
445
+ return this.sendRequest("cosmetic.ClientCosmeticAnimationTriggerPacket", {
446
+ a: cosmeticId,
447
+ b: animationId
448
+ });
449
+ }
450
+ unlockCosmetics(cosmeticIds) {
451
+ return this.sendRequest("checkout.ClientCheckoutCosmeticsPacket", {
452
+ cosmetic_ids: cosmeticIds
453
+ });
454
+ }
455
+ // Skins
456
+ createSkin(name, model, hash) {
457
+ return this.sendRequest("skin.ClientSkinCreatePacket", { a: name, b: model, c: hash });
458
+ }
459
+ selectLastUsedSkin(skinId) {
460
+ return this.sendRequest("skin.ClientSkinUpdateLastUsedStatePacket", { a: skinId });
461
+ }
462
+ favoriteSkin(skinId, favorited) {
463
+ return this.sendRequest("skin.ClientSkinUpdateFavoriteStatePacket", {
464
+ a: skinId,
465
+ b: favorited
466
+ });
467
+ }
468
+ // Outfits
469
+ createOutfit(name, equippedCosmetics, settings = {}) {
470
+ return this.sendRequest("cosmetic.outfit.ClientCosmeticOutfitCreatePacket", {
471
+ name,
472
+ skin_id: null,
473
+ equipped_cosmetics: equippedCosmetics,
474
+ cosmetic_settings: settings
475
+ });
476
+ }
477
+ selectOutfit(outfitId) {
478
+ return this.sendRequest("cosmetic.outfit.ClientCosmeticOutfitSelectPacket", { a: outfitId });
479
+ }
480
+ setEquippedCosmetic(outfitId, slot, cosmeticId) {
481
+ return this.sendRequest("cosmetic.outfit.ClientCosmeticOutfitEquippedCosmeticsUpdatePacket", {
482
+ a: outfitId,
483
+ b: slot,
484
+ c: cosmeticId
485
+ });
486
+ }
487
+ // Emote wheel
488
+ updateEmoteWheel(wheelId, slots) {
489
+ return this.sendRequest("cosmetic.emote.ClientCosmeticEmoteWheelUpdatePacket", {
490
+ a: wheelId,
491
+ b: slots
492
+ });
493
+ }
494
+ selectEmoteWheel(wheelId) {
495
+ return this.sendRequest("cosmetic.emote.ClientCosmeticEmoteWheelSelectPacket", { a: wheelId });
496
+ }
497
+ // Wardrobe settings
498
+ getWardrobeSettings() {
499
+ return this.sendRequest("wardrobe.ClientWardrobeSettingsPacket", {});
500
+ }
501
+ // Discovery
502
+ listNotices() {
503
+ return this.sendRequest("notices.ClientNoticeRequestPacket", {});
504
+ }
505
+ listServerDiscovery() {
506
+ return this.sendRequest("serverdiscovery.ClientServerDiscoveryRequestPacket", {});
507
+ }
508
+ listKnownServers() {
509
+ return this.sendRequest("knownservers.ClientKnownServersRequestPacket", {});
510
+ }
511
+ // Multiplayer
512
+ relayIcePacket(targetUuid, payload) {
513
+ return this.sendRequest("multiplayer.ClientMultiplayerIceRelayPacket", {
514
+ a: targetUuid,
515
+ b: Array.from(payload)
516
+ });
517
+ }
518
+ pingServer(host, port, protocolVersion) {
519
+ return this.sendRequest("pingproxy.ClientPingProxyRequestPacket", {
520
+ a: host,
521
+ b: port,
522
+ c: protocolVersion ?? 0
523
+ });
524
+ }
525
+ // UPnP server hosting
526
+ createSession(ip, port, privacy, worldName, protocolVersion) {
527
+ return this.sendRequest("upnp.ClientUPnPSessionCreatePacket", {
528
+ a: ip,
529
+ b: port,
530
+ c: privacy,
531
+ d: protocolVersion,
532
+ e: worldName
533
+ });
534
+ }
535
+ closeSession() {
536
+ return this.sendRequest("upnp.ClientUPnPSessionClosePacket", {});
537
+ }
538
+ updateSession(patch) {
539
+ return this.sendRequest("upnp.ClientUPnPSessionUpdatePacket", {
540
+ a: patch.ip,
541
+ b: patch.port,
542
+ c: patch.privacy
543
+ });
544
+ }
545
+ inviteToSession(inviteeUuids) {
546
+ return this.sendRequest("upnp.ClientUPnPSessionInvitesAddPacket", { a: inviteeUuids });
547
+ }
548
+ revokeSessionInvites(inviteeUuids) {
549
+ return this.sendRequest("upnp.ClientUPnPSessionInvitesRemovePacket", { a: inviteeUuids });
550
+ }
551
+ pushServerStatus(rawStatus) {
552
+ return this.sendRequest("upnp.ClientUPnPSessionPingProxyUpdatePacket", { a: rawStatus });
553
+ }
554
+ // Social invite
555
+ inviteFriendToServer(targetUuid, address) {
556
+ return this.sendRequest("social.ClientSocialInviteRequestPacket", {
557
+ a: targetUuid,
558
+ b: address
559
+ });
560
+ }
561
+ // ----- Event emitter -----
562
+ on(event, listener) {
563
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
564
+ this.listeners.get(event).add(listener);
565
+ return () => this.off(event, listener);
566
+ }
567
+ off(event, listener) {
568
+ this.listeners.get(event)?.delete(listener);
569
+ }
570
+ emit(event, data) {
571
+ for (const l of this.listeners.get(event) ?? []) {
572
+ try {
573
+ l(data);
574
+ } catch (e) {
575
+ }
576
+ }
577
+ }
578
+ isReady() {
579
+ return this.state === "ready";
580
+ }
581
+ isConnected() {
582
+ return this.state === "connected" || this.state === "ready";
583
+ }
584
+ };
585
+
586
+ exports.AnvilWsClient = AnvilWsClient;
587
+ exports.ConnectionCodec = ConnectionCodec;
588
+ //# sourceMappingURL=index.cjs.map
589
+ //# sourceMappingURL=index.cjs.map