@laravel/echo-react
Version:
React hooks for seamless integration with Laravel Echo.
1,033 lines (1,032 loc) • 25.7 kB
JavaScript
import H from "pusher-js";
import { useCallback as d, useRef as f, useEffect as x } from "react";
class I {
constructor() {
this.notificationCreatedEvent = ".Illuminate\\Notifications\\Events\\BroadcastNotificationCreated";
}
/**
* Listen for a whisper event on the channel instance.
*/
listenForWhisper(t, e) {
return this.listen(".client-" + t, e);
}
/**
* Listen for an event on the channel instance.
*/
notification(t) {
return this.listen(this.notificationCreatedEvent, t);
}
/**
* Stop listening for notification events on the channel instance.
*/
stopListeningForNotification(t) {
return this.stopListening(this.notificationCreatedEvent, t);
}
/**
* Stop listening for a whisper event on the channel instance.
*/
stopListeningForWhisper(t, e) {
return this.stopListening(".client-" + t, e);
}
}
class A {
/**
* Create a new class instance.
*/
constructor(t) {
this.namespace = t;
}
/**
* Format the given event name.
*/
format(t) {
return [".", "\\"].includes(t.charAt(0)) ? t.substring(1) : (this.namespace && (t = this.namespace + "." + t), t.replace(/\./g, "\\"));
}
/**
* Set the event namespace.
*/
setNamespace(t) {
this.namespace = t;
}
}
function B(s) {
try {
new s();
} catch (t) {
if (t instanceof Error && t.message.includes("is not a constructor"))
return !1;
}
return !0;
}
class T extends I {
/**
* Create a new class instance.
*/
constructor(t, e, n) {
super(), this.name = e, this.pusher = t, this.options = n, this.eventFormatter = new A(this.options.namespace), this.subscribe();
}
/**
* Subscribe to a Pusher channel.
*/
subscribe() {
this.subscription = this.pusher.subscribe(this.name);
}
/**
* Unsubscribe from a Pusher channel.
*/
unsubscribe() {
this.pusher.unsubscribe(this.name);
}
/**
* Listen for an event on the channel instance.
*/
listen(t, e) {
return this.on(this.eventFormatter.format(t), e), this;
}
/**
* Listen for all events on the channel instance.
*/
listenToAll(t) {
return this.subscription.bind_global((e, n) => {
if (e.startsWith("pusher:"))
return;
let i = String(this.options.namespace ?? "").replace(
/\./g,
"\\"
), r = e.startsWith(i) ? e.substring(i.length + 1) : "." + e;
t(r, n);
}), this;
}
/**
* Stop listening for an event on the channel instance.
*/
stopListening(t, e) {
return e ? this.subscription.unbind(
this.eventFormatter.format(t),
e
) : this.subscription.unbind(this.eventFormatter.format(t)), this;
}
/**
* Stop listening for all events on the channel instance.
*/
stopListeningToAll(t) {
return t ? this.subscription.unbind_global(t) : this.subscription.unbind_global(), this;
}
/**
* Register a callback to be called anytime a subscription succeeds.
*/
subscribed(t) {
return this.on("pusher:subscription_succeeded", () => {
t();
}), this;
}
/**
* Register a callback to be called anytime a subscription error occurs.
*/
error(t) {
return this.on("pusher:subscription_error", (e) => {
t(e);
}), this;
}
/**
* Bind a channel to an event.
*/
on(t, e) {
return this.subscription.bind(t, e), this;
}
}
class V extends T {
/**
* Send a whisper event to other clients in the channel.
*/
whisper(t, e) {
return this.pusher.channels.channels[this.name].trigger(
`client-${t}`,
e
), this;
}
}
class q extends T {
/**
* Send a whisper event to other clients in the channel.
*/
whisper(t, e) {
return this.pusher.channels.channels[this.name].trigger(
`client-${t}`,
e
), this;
}
}
class U extends V {
/**
* Register a callback to be called anytime the member list changes.
*/
here(t) {
return this.on("pusher:subscription_succeeded", (e) => {
t(Object.keys(e.members).map((n) => e.members[n]));
}), this;
}
/**
* Listen for someone joining the channel.
*/
joining(t) {
return this.on("pusher:member_added", (e) => {
t(e.info);
}), this;
}
/**
* Send a whisper event to other clients in the channel.
*/
whisper(t, e) {
return this.pusher.channels.channels[this.name].trigger(
`client-${t}`,
e
), this;
}
/**
* Listen for someone leaving the channel.
*/
leaving(t) {
return this.on("pusher:member_removed", (e) => {
t(e.info);
}), this;
}
}
class L extends I {
/**
* Create a new class instance.
*/
constructor(t, e, n) {
super(), this.events = {}, this.listeners = {}, this.name = e, this.socket = t, this.options = n, this.eventFormatter = new A(this.options.namespace), this.subscribe();
}
/**
* Subscribe to a Socket.io channel.
*/
subscribe() {
this.socket.emit("subscribe", {
channel: this.name,
auth: this.options.auth || {}
});
}
/**
* Unsubscribe from channel and ubind event callbacks.
*/
unsubscribe() {
this.unbind(), this.socket.emit("unsubscribe", {
channel: this.name,
auth: this.options.auth || {}
});
}
/**
* Listen for an event on the channel instance.
*/
listen(t, e) {
return this.on(this.eventFormatter.format(t), e), this;
}
/**
* Stop listening for an event on the channel instance.
*/
stopListening(t, e) {
return this.unbindEvent(this.eventFormatter.format(t), e), this;
}
/**
* Register a callback to be called anytime a subscription succeeds.
*/
subscribed(t) {
return this.on("connect", (e) => {
t(e);
}), this;
}
/**
* Register a callback to be called anytime an error occurs.
*/
error(t) {
return this;
}
/**
* Bind the channel's socket to an event and store the callback.
*/
on(t, e) {
return this.listeners[t] = this.listeners[t] || [], this.events[t] || (this.events[t] = (n, i) => {
this.name === n && this.listeners[t] && this.listeners[t].forEach((r) => r(i));
}, this.socket.on(t, this.events[t])), this.listeners[t].push(e), this;
}
/**
* Unbind the channel's socket from all stored event callbacks.
*/
unbind() {
Object.keys(this.events).forEach((t) => {
this.unbindEvent(t);
});
}
/**
* Unbind the listeners for the given event.
*/
unbindEvent(t, e) {
this.listeners[t] = this.listeners[t] || [], e && (this.listeners[t] = this.listeners[t].filter(
(n) => n !== e
)), (!e || this.listeners[t].length === 0) && (this.events[t] && (this.socket.removeListener(t, this.events[t]), delete this.events[t]), delete this.listeners[t]);
}
}
class O extends L {
/**
* Send a whisper event to other clients in the channel.
*/
whisper(t, e) {
return this.socket.emit("client event", {
channel: this.name,
event: `client-${t}`,
data: e
}), this;
}
}
class K extends O {
/**
* Register a callback to be called anytime the member list changes.
*/
here(t) {
return this.on("presence:subscribed", (e) => {
t(e.map((n) => n.user_info));
}), this;
}
/**
* Listen for someone joining the channel.
*/
joining(t) {
return this.on(
"presence:joining",
(e) => t(e.user_info)
), this;
}
/**
* Send a whisper event to other clients in the channel.
*/
whisper(t, e) {
return this.socket.emit("client event", {
channel: this.name,
event: `client-${t}`,
data: e
}), this;
}
/**
* Listen for someone leaving the channel.
*/
leaving(t) {
return this.on(
"presence:leaving",
(e) => t(e.user_info)
), this;
}
}
class E extends I {
/**
* Subscribe to a channel.
*/
subscribe() {
}
/**
* Unsubscribe from a channel.
*/
unsubscribe() {
}
/**
* Listen for an event on the channel instance.
*/
listen(t, e) {
return this;
}
/**
* Listen for all events on the channel instance.
*/
listenToAll(t) {
return this;
}
/**
* Stop listening for an event on the channel instance.
*/
stopListening(t, e) {
return this;
}
/**
* Register a callback to be called anytime a subscription succeeds.
*/
subscribed(t) {
return this;
}
/**
* Register a callback to be called anytime an error occurs.
*/
error(t) {
return this;
}
/**
* Bind a channel to an event.
*/
on(t, e) {
return this;
}
}
class j extends E {
/**
* Send a whisper event to other clients in the channel.
*/
whisper(t, e) {
return this;
}
}
class N extends E {
/**
* Send a whisper event to other clients in the channel.
*/
whisper(t, e) {
return this;
}
}
class X extends j {
/**
* Register a callback to be called anytime the member list changes.
*/
here(t) {
return this;
}
/**
* Listen for someone joining the channel.
*/
joining(t) {
return this;
}
/**
* Send a whisper event to other clients in the channel.
*/
whisper(t, e) {
return this;
}
/**
* Listen for someone leaving the channel.
*/
leaving(t) {
return this;
}
}
const F = class $ {
/**
* Create a new class instance.
*/
constructor(t) {
this.setOptions(t), this.connect();
}
/**
* Merge the custom options with the defaults.
*/
setOptions(t) {
this.options = {
...$._defaultOptions,
...t,
broadcaster: t.broadcaster
};
let e = this.csrfToken();
e && (this.options.auth.headers["X-CSRF-TOKEN"] = e, this.options.userAuthentication.headers["X-CSRF-TOKEN"] = e), e = this.options.bearerToken, e && (this.options.auth.headers.Authorization = "Bearer " + e, this.options.userAuthentication.headers.Authorization = "Bearer " + e);
}
/**
* Extract the CSRF token from the page.
*/
csrfToken() {
var t, e;
return typeof window < "u" && (t = window.Laravel) != null && t.csrfToken ? window.Laravel.csrfToken : this.options.csrfToken ? this.options.csrfToken : typeof document < "u" && typeof document.querySelector == "function" ? ((e = document.querySelector('meta[name="csrf-token"]')) == null ? void 0 : e.getAttribute("content")) ?? null : null;
}
};
F._defaultOptions = {
auth: {
headers: {}
},
authEndpoint: "/broadcasting/auth",
userAuthentication: {
endpoint: "/broadcasting/user-auth",
headers: {}
},
csrfToken: null,
bearerToken: null,
host: null,
key: null,
namespace: "App.Events"
};
let _ = F;
class w extends _ {
constructor() {
super(...arguments), this.channels = {};
}
/**
* Create a fresh Pusher connection.
*/
connect() {
if (typeof this.options.client < "u")
this.pusher = this.options.client;
else if (this.options.Pusher)
this.pusher = new this.options.Pusher(
this.options.key,
this.options
);
else if (typeof window < "u" && typeof window.Pusher < "u")
this.pusher = new window.Pusher(this.options.key, this.options);
else
throw new Error(
"Pusher client not found. Should be globally available or passed via options.client"
);
}
/**
* Sign in the user via Pusher user authentication (https://pusher.com/docs/channels/using_channels/user-authentication/).
*/
signin() {
this.pusher.signin();
}
/**
* Listen for an event on a channel instance.
*/
listen(t, e, n) {
return this.channel(t).listen(e, n);
}
/**
* Get a channel instance by name.
*/
channel(t) {
return this.channels[t] || (this.channels[t] = new T(
this.pusher,
t,
this.options
)), this.channels[t];
}
/**
* Get a private channel instance by name.
*/
privateChannel(t) {
return this.channels["private-" + t] || (this.channels["private-" + t] = new V(
this.pusher,
"private-" + t,
this.options
)), this.channels["private-" + t];
}
/**
* Get a private encrypted channel instance by name.
*/
encryptedPrivateChannel(t) {
return this.channels["private-encrypted-" + t] || (this.channels["private-encrypted-" + t] = new q(
this.pusher,
"private-encrypted-" + t,
this.options
)), this.channels["private-encrypted-" + t];
}
/**
* Get a presence channel instance by name.
*/
presenceChannel(t) {
return this.channels["presence-" + t] || (this.channels["presence-" + t] = new U(
this.pusher,
"presence-" + t,
this.options
)), this.channels["presence-" + t];
}
/**
* Leave the given channel, as well as its private and presence variants.
*/
leave(t) {
[
t,
"private-" + t,
"private-encrypted-" + t,
"presence-" + t
].forEach((e) => {
this.leaveChannel(e);
});
}
/**
* Leave the given channel.
*/
leaveChannel(t) {
this.channels[t] && (this.channels[t].unsubscribe(), delete this.channels[t]);
}
/**
* Get the socket ID for the connection.
*/
socketId() {
return this.pusher.connection.socket_id;
}
/**
* Disconnect Pusher connection.
*/
disconnect() {
this.pusher.disconnect();
}
}
class Q extends _ {
constructor() {
super(...arguments), this.channels = {};
}
/**
* Create a fresh Socket.io connection.
*/
connect() {
let t = this.getSocketIO();
this.socket = t(
this.options.host ?? void 0,
this.options
), this.socket.io.on("reconnect", () => {
Object.values(this.channels).forEach((e) => {
e.subscribe();
});
});
}
/**
* Get socket.io module from global scope or options.
*/
getSocketIO() {
if (typeof this.options.client < "u")
return this.options.client;
if (typeof window < "u" && typeof window.io < "u")
return window.io;
throw new Error(
"Socket.io client not found. Should be globally available or passed via options.client"
);
}
/**
* Listen for an event on a channel instance.
*/
listen(t, e, n) {
return this.channel(t).listen(e, n);
}
/**
* Get a channel instance by name.
*/
channel(t) {
return this.channels[t] || (this.channels[t] = new L(
this.socket,
t,
this.options
)), this.channels[t];
}
/**
* Get a private channel instance by name.
*/
privateChannel(t) {
return this.channels["private-" + t] || (this.channels["private-" + t] = new O(
this.socket,
"private-" + t,
this.options
)), this.channels["private-" + t];
}
/**
* Get a presence channel instance by name.
*/
presenceChannel(t) {
return this.channels["presence-" + t] || (this.channels["presence-" + t] = new K(
this.socket,
"presence-" + t,
this.options
)), this.channels["presence-" + t];
}
/**
* Leave the given channel, as well as its private and presence variants.
*/
leave(t) {
[t, "private-" + t, "presence-" + t].forEach((e) => {
this.leaveChannel(e);
});
}
/**
* Leave the given channel.
*/
leaveChannel(t) {
this.channels[t] && (this.channels[t].unsubscribe(), delete this.channels[t]);
}
/**
* Get the socket ID for the connection.
*/
socketId() {
return this.socket.id;
}
/**
* Disconnect Socketio connection.
*/
disconnect() {
this.socket.disconnect();
}
}
class S extends _ {
constructor() {
super(...arguments), this.channels = {};
}
/**
* Create a fresh connection.
*/
connect() {
}
/**
* Listen for an event on a channel instance.
*/
listen(t, e, n) {
return new E();
}
/**
* Get a channel instance by name.
*/
channel(t) {
return new E();
}
/**
* Get a private channel instance by name.
*/
privateChannel(t) {
return new j();
}
/**
* Get a private encrypted channel instance by name.
*/
encryptedPrivateChannel(t) {
return new N();
}
/**
* Get a presence channel instance by name.
*/
presenceChannel(t) {
return new X();
}
/**
* Leave the given channel, as well as its private and presence variants.
*/
leave(t) {
}
/**
* Leave the given channel.
*/
leaveChannel(t) {
}
/**
* Get the socket ID for the connection.
*/
socketId() {
return "fake-socket-id";
}
/**
* Disconnect the connection.
*/
disconnect() {
}
}
class W {
/**
* Create a new class instance.
*/
constructor(t) {
this.options = t, this.connect(), this.options.withoutInterceptors || this.registerInterceptors();
}
/**
* Get a channel instance by name.
*/
channel(t) {
return this.connector.channel(t);
}
/**
* Create a new connection.
*/
connect() {
if (this.options.broadcaster === "reverb")
this.connector = new w({
...this.options,
cluster: ""
});
else if (this.options.broadcaster === "pusher")
this.connector = new w(this.options);
else if (this.options.broadcaster === "ably")
this.connector = new w({
...this.options,
cluster: "",
broadcaster: "pusher"
});
else if (this.options.broadcaster === "socket.io")
this.connector = new Q(this.options);
else if (this.options.broadcaster === "null")
this.connector = new S(this.options);
else if (typeof this.options.broadcaster == "function" && B(this.options.broadcaster))
this.connector = new this.options.broadcaster(this.options);
else
throw new Error(
`Broadcaster ${typeof this.options.broadcaster} ${String(this.options.broadcaster)} is not supported.`
);
}
/**
* Disconnect from the Echo server.
*/
disconnect() {
this.connector.disconnect();
}
/**
* Get a presence channel instance by name.
*/
join(t) {
return this.connector.presenceChannel(t);
}
/**
* Leave the given channel, as well as its private and presence variants.
*/
leave(t) {
this.connector.leave(t);
}
/**
* Leave the given channel.
*/
leaveChannel(t) {
this.connector.leaveChannel(t);
}
/**
* Leave all channels.
*/
leaveAllChannels() {
for (const t in this.connector.channels)
this.leaveChannel(t);
}
/**
* Listen for an event on a channel instance.
*/
listen(t, e, n) {
return this.connector.listen(t, e, n);
}
/**
* Get a private channel instance by name.
*/
private(t) {
return this.connector.privateChannel(t);
}
/**
* Get a private encrypted channel instance by name.
*/
encryptedPrivate(t) {
if (this.connectorSupportsEncryptedPrivateChannels(this.connector))
return this.connector.encryptedPrivateChannel(t);
throw new Error(
`Broadcaster ${typeof this.options.broadcaster} ${String(
this.options.broadcaster
)} does not support encrypted private channels.`
);
}
connectorSupportsEncryptedPrivateChannels(t) {
return t instanceof w || t instanceof S;
}
/**
* Get the Socket ID for the connection.
*/
socketId() {
return this.connector.socketId();
}
/**
* Register 3rd party request interceptiors. These are used to automatically
* send a connections socket id to a Laravel app with a X-Socket-Id header.
*/
registerInterceptors() {
typeof Vue < "u" && Vue != null && Vue.http && this.registerVueRequestInterceptor(), typeof axios == "function" && this.registerAxiosRequestInterceptor(), typeof jQuery == "function" && this.registerjQueryAjaxSetup(), typeof Turbo == "object" && this.registerTurboRequestInterceptor();
}
/**
* Register a Vue HTTP interceptor to add the X-Socket-ID header.
*/
registerVueRequestInterceptor() {
Vue.http.interceptors.push(
(t, e) => {
this.socketId() && t.headers.set("X-Socket-ID", this.socketId()), e();
}
);
}
/**
* Register an Axios HTTP interceptor to add the X-Socket-ID header.
*/
registerAxiosRequestInterceptor() {
axios.interceptors.request.use(
(t) => (this.socketId() && (t.headers["X-Socket-Id"] = this.socketId()), t)
);
}
/**
* Register jQuery AjaxPrefilter to add the X-Socket-ID header.
*/
registerjQueryAjaxSetup() {
typeof jQuery.ajax < "u" && jQuery.ajaxPrefilter(
(t, e, n) => {
this.socketId() && n.setRequestHeader("X-Socket-Id", this.socketId());
}
);
}
/**
* Register the Turbo Request interceptor to add the X-Socket-ID header.
*/
registerTurboRequestInterceptor() {
document.addEventListener(
"turbo:before-fetch-request",
(t) => {
t.detail.fetchOptions.headers["X-Socket-Id"] = this.socketId();
}
);
}
}
let b = null, p = null;
const z = () => {
if (b)
return b;
if (!p)
throw new Error(
"Echo has not been configured. Please call `configureEcho()`."
);
return p.Pusher ?? (p.Pusher = H), b = new W(p), b;
}, J = (s) => {
p = {
...{
reverb: {
broadcaster: "reverb",
key: (typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_REVERB_APP_KEY : undefined),
wsHost: (typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_REVERB_HOST : undefined),
wsPort: (typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_REVERB_PORT : undefined),
wssPort: (typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_REVERB_PORT : undefined),
forceTLS: ((typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_REVERB_SCHEME : undefined) ?? "https") === "https",
enabledTransports: ["ws", "wss"]
},
pusher: {
broadcaster: "pusher",
key: (typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_PUSHER_APP_KEY : undefined),
cluster: (typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_PUSHER_APP_CLUSTER : undefined),
forceTLS: !0,
wsHost: (typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_PUSHER_HOST : undefined),
wsPort: (typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_PUSHER_PORT : undefined),
wssPort: (typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_PUSHER_PORT : undefined),
enabledTransports: ["ws", "wss"]
},
"socket.io": {
broadcaster: "socket.io",
host: (typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_SOCKET_IO_HOST : undefined)
},
null: {
broadcaster: "null"
},
ably: {
broadcaster: "pusher",
key: (typeof import.meta.env !== 'undefined' ? import.meta.env.VITE_ABLY_PUBLIC_KEY : undefined),
wsHost: "realtime-pusher.ably.io",
wsPort: 443,
disableStats: !0,
encrypted: !0
}
}[s.broadcaster],
...s
}, b && (b = null);
}, y = () => z(), Z = () => p !== null, C = (s) => Array.isArray(s) ? s : [s], h = {}, Y = (s) => {
const t = y();
return s.visibility === "presence" ? t.join(s.name) : s.visibility === "private" ? t.private(s.name) : t.channel(s.name);
}, D = (s, t) => {
h[s.id] && (h[s.id].count -= 1, !(h[s.id].count > 0) && (t ? y().leave(s.name) : y().leaveChannel(s.id), delete h[s.id]));
}, R = (s) => {
if (h[s.id])
return h[s.id].count += 1, h[s.id].connection;
const t = Y(s);
return h[s.id] = {
count: 1,
connection: t
}, t;
}, g = (s, t = [], e = () => {
}, n = [], i = "private") => {
const r = {
name: s,
id: ["private", "presence"].includes(i) ? `${i}-${s}` : s,
visibility: i
}, c = d(e, n), a = f(!1), u = f(!1), l = f(
R(r)
), v = C(t), o = d(() => {
a.current && (v.forEach((m) => {
l.current.stopListening(m, c);
}), a.current = !1);
}, n), P = d(() => {
a.current || (v.forEach((m) => {
l.current.listen(m, c);
}), a.current = !0);
}, n), k = d((m = !1) => {
o(), D(r, m);
}, n);
return x(() => (u.current && (l.current = R(r)), u.current = !0, P(), k), n), {
/**
* Leave the channel
*/
leaveChannel: k,
/**
* Leave the channel and also its associated private and presence channels
*/
leave: () => k(!0),
/**
* Stop listening for event(s) without leaving the channel
*/
stopListening: o,
/**
* Listen for event(s)
*/
listen: P,
/**
* Channel instance
*/
channel: () => l.current
};
}, tt = (s, t = () => {
}, e = [], n = []) => {
const i = g(
s,
[],
t,
n,
"private"
), r = f(
C(e).map((o) => o.includes(".") ? [o, o.replace(/\./g, "\\")] : [o, o.replace(/\\/g, ".")]).flat()
), c = f(!1), a = f(!1), u = d(
(o) => {
c.current && (r.current.length === 0 || r.current.includes(o.type)) && t(o);
},
n.concat(r.current).concat([t])
), l = d(() => {
c.current || (a.current || i.channel().notification(u), c.current = !0, a.current = !0);
}, [u]), v = d(() => {
c.current && (i.channel().stopListeningForNotification(u), c.current = !1);
}, [u]);
return x(() => (l(), () => v()), n.concat(r.current)), {
...i,
/**
* Stop listening for notification events
*/
stopListening: v,
/**
* Listen for notification events
*/
listen: l
};
}, et = (s, t = [], e = () => {
}, n = []) => g(
s,
t,
e,
n,
"presence"
), st = (s, t = [], e = () => {
}, n = []) => g(
s,
t,
e,
n,
"public"
), nt = (s, t, e = [], n = () => {
}, i = []) => g(
`${s}.${t}`,
C(e).map((r) => r.startsWith(".") ? r : `.${r}`),
n,
i,
"private"
);
export {
J as configureEcho,
y as echo,
Z as echoIsConfigured,
g as useEcho,
nt as useEchoModel,
tt as useEchoNotification,
et as useEchoPresence,
st as useEchoPublic
};
//# sourceMappingURL=index.js.map