Skip to content

Commit cc12f13

Browse files
authored
fix: add WDS v4 compatibility (#483)
1 parent 2cff3ee commit cc12f13

File tree

8 files changed

+114
-107
lines changed

8 files changed

+114
-107
lines changed

sockets/WDSSocket.js

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const getSocketUrlParts = require('./utils/getSocketUrlParts.js');
44
const getUrlFromParts = require('./utils/getUrlFromParts');
5+
const getWDSMetadata = require('./utils/getWDSMetadata');
56

67
/**
78
* Initializes a socket server for HMR for webpack-dev-server.
@@ -16,18 +17,10 @@ function initWDSSocket(messageHandler, resourceQuery) {
1617
SocketClient = __webpack_dev_server_client__.default;
1718
}
1819

19-
const urlParts = getSocketUrlParts(resourceQuery);
20+
const wdsMeta = getWDSMetadata(SocketClient);
21+
const urlParts = getSocketUrlParts(resourceQuery, wdsMeta);
2022

21-
let enforceWs = false;
22-
if (
23-
typeof SocketClient.name !== 'undefined' &&
24-
SocketClient.name !== null &&
25-
SocketClient.name.toLowerCase().includes('websocket')
26-
) {
27-
enforceWs = true;
28-
}
29-
30-
const connection = new SocketClient(getUrlFromParts(urlParts, enforceWs));
23+
const connection = new SocketClient(getUrlFromParts(urlParts, wdsMeta));
3124

3225
connection.onMessage(function onSocketMessage(data) {
3326
const message = JSON.parse(data);

sockets/utils/getSocketUrlParts.js

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
const getCurrentScriptSource = require('./getCurrentScriptSource.js');
2-
const parseQuery = require('./parseQuery.js');
32

43
/**
54
* @typedef {Object} SocketUrlParts
@@ -13,10 +12,15 @@ const parseQuery = require('./parseQuery.js');
1312
/**
1413
* Parse current location and Webpack's `__resourceQuery` into parts that can create a valid socket URL.
1514
* @param {string} [resourceQuery] The Webpack `__resourceQuery` string.
15+
* @param {import('./getWDSMetadata').WDSMetaObj} [metadata] The parsed WDS metadata object.
1616
* @returns {SocketUrlParts} The parsed URL parts.
1717
* @see https://webpack.js.org/api/module-variables/#__resourcequery-webpack-specific
1818
*/
19-
function getSocketUrlParts(resourceQuery) {
19+
function getSocketUrlParts(resourceQuery, metadata) {
20+
if (typeof metadata === 'undefined') {
21+
metadata = {};
22+
}
23+
2024
const scriptSource = getCurrentScriptSource();
2125

2226
let url = {};
@@ -36,24 +40,52 @@ function getSocketUrlParts(resourceQuery) {
3640
let hostname = url.hostname;
3741
/** @type {string | undefined} */
3842
let protocol = url.protocol;
39-
let pathname = '/sockjs-node'; // This is hard-coded in WDS
4043
/** @type {string | undefined} */
4144
let port = url.port;
4245

46+
// This is hard-coded in WDS v3
47+
let pathname = '/sockjs-node';
48+
if (metadata.version === 4) {
49+
// This is hard-coded in WDS v4
50+
pathname = '/ws';
51+
}
52+
4353
// Parse authentication credentials in case we need them
4454
if (url.username) {
4555
// Since HTTP basic authentication does not allow empty username,
4656
// we only include password if the username is not empty.
47-
// Result: <username>:<password>
48-
auth = [url.username, url.password].filter(Boolean).join(':');
57+
// Result: <username> or <username>:<password>
58+
auth = url.username;
59+
if (url.password) {
60+
auth += ':' + url.password;
61+
}
4962
}
5063

51-
// Check for IPv4 and IPv6 host addresses that corresponds to `any`/`empty`.
52-
// This is important because `hostname` can be empty for some hosts,
53-
// such as `about:blank` or `file://` URLs.
54-
const isEmptyHostname = url.hostname === '0.0.0.0' || url.hostname === '[::]' || !url.hostname;
64+
// If the resource query is available,
65+
// parse it and overwrite everything we received from the script host.
66+
const parsedQuery = {};
67+
if (resourceQuery) {
68+
const searchParams = new URLSearchParams(resourceQuery.slice(1));
69+
searchParams.forEach(function (value, key) {
70+
parsedQuery[key] = value;
71+
});
72+
}
5573

56-
// We only re-assign the hostname if we are using HTTP/HTTPS protocols
74+
hostname = parsedQuery.sockHost || hostname;
75+
pathname = parsedQuery.sockPath || pathname;
76+
port = parsedQuery.sockPort || port;
77+
78+
// Make sure the protocol from resource query has a trailing colon
79+
if (parsedQuery.sockProtocol) {
80+
protocol = parsedQuery.sockProtocol + ':';
81+
}
82+
83+
// Check for IPv4 and IPv6 host addresses that corresponds to any/empty.
84+
// This is important because `hostname` can be empty for some hosts,
85+
// such as 'about:blank' or 'file://' URLs.
86+
const isEmptyHostname = hostname === '0.0.0.0' || hostname === '[::]' || !hostname;
87+
// We only re-assign the hostname if it is empty,
88+
// and if we are using HTTP/HTTPS protocols.
5789
if (
5890
isEmptyHostname &&
5991
window.location.hostname &&
@@ -64,7 +96,7 @@ function getSocketUrlParts(resourceQuery) {
6496

6597
// We only re-assign `protocol` when `hostname` is available and is empty,
6698
// since otherwise we risk creating an invalid URL.
67-
// We also do this when `https` is used as it mandates the use of secure sockets.
99+
// We also do this when 'https' is used as it mandates the use of secure sockets.
68100
if (hostname && (isEmptyHostname || window.location.protocol === 'https:')) {
69101
protocol = window.location.protocol;
70102
}
@@ -74,18 +106,6 @@ function getSocketUrlParts(resourceQuery) {
74106
port = window.location.port;
75107
}
76108

77-
// If the resource query is available,
78-
// parse it and overwrite everything we received from the script host.
79-
const parsedQuery = parseQuery(resourceQuery || '');
80-
hostname = parsedQuery.sockHost || hostname;
81-
pathname = parsedQuery.sockPath || pathname;
82-
port = parsedQuery.sockPort || port;
83-
84-
// Make sure the protocol from resource query has a trailing colon
85-
if (parsedQuery.sockProtocol) {
86-
protocol = parsedQuery.sockProtocol + ':';
87-
}
88-
89109
if (!hostname || !pathname || !port) {
90110
throw new Error(
91111
[

sockets/utils/getUrlFromParts.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
/**
22
* Create a valid URL from parsed URL parts.
33
* @param {import('./getSocketUrlParts').SocketUrlParts} urlParts The parsed URL parts.
4-
* @param {boolean} [enforceWs] Enforce using the WebSocket protocols.
4+
* @param {import('./getWDSMetadata').WDSMetaObj} [metadata] The parsed WDS metadata object.
55
* @returns {string} The generated URL.
66
*/
7-
function urlFromParts(urlParts, enforceWs) {
7+
function urlFromParts(urlParts, metadata) {
8+
if (typeof metadata === 'undefined') {
9+
metadata = {};
10+
}
11+
812
let fullProtocol = 'http:';
913
if (urlParts.protocol) {
1014
fullProtocol = urlParts.protocol;
1115
}
12-
if (enforceWs) {
16+
if (metadata.enforceWs) {
1317
fullProtocol = fullProtocol.replace(/^(?:http|.+-extension|file)/i, 'ws');
1418
}
1519

sockets/utils/getWDSMetadata.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* @typedef {Object} WDSMetaObj
3+
* @property {boolean} enforceWs
4+
* @property {number} version
5+
*/
6+
7+
/**
8+
* Derives WDS metadata from a compatible socket client.
9+
* @param {Function} SocketClient A WDS socket client (SockJS/WebSocket).
10+
* @returns {WDSMetaObj} The parsed WDS metadata object.
11+
*/
12+
function getWDSMetadata(SocketClient) {
13+
let enforceWs = false;
14+
if (
15+
typeof SocketClient.name !== 'undefined' &&
16+
SocketClient.name !== null &&
17+
SocketClient.name.toLowerCase().includes('websocket')
18+
) {
19+
enforceWs = true;
20+
}
21+
22+
let version;
23+
// WDS versions <=3.5.0
24+
if (!('onMessage' in SocketClient.prototype)) {
25+
version = 3;
26+
} else {
27+
// WDS versions >=3.5.0 <4
28+
if (
29+
'getClientPath' in SocketClient ||
30+
Object.getPrototypeOf(SocketClient).name === 'BaseClient'
31+
) {
32+
version = 3;
33+
} else {
34+
version = 4;
35+
}
36+
}
37+
38+
return {
39+
enforceWs: enforceWs,
40+
version: version,
41+
};
42+
}
43+
44+
module.exports = getWDSMetadata;

sockets/utils/parseQuery.js

Lines changed: 0 additions & 33 deletions
This file was deleted.

test/sockets/getSocketUrlParts.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,16 @@ describe('getSocketUrlParts', () => {
222222
protocol: 'http:',
223223
});
224224
});
225+
226+
it('should force /ws when metadata.version is 4', () => {
227+
getCurrentScriptSource.mockImplementationOnce(() => 'http://localhost:8080');
228+
229+
expect(getSocketUrlParts(undefined, { version: 4 })).toStrictEqual({
230+
auth: undefined,
231+
hostname: 'localhost',
232+
pathname: '/ws',
233+
port: '8080',
234+
protocol: 'http:',
235+
});
236+
});
225237
});

test/sockets/getUrlFromParts.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('getUrlFromParts', () => {
8989
).toStrictEqual('http://username:password@localhost:8080/sockjs-node');
9090
});
9191

92-
it('should force WS when enforceWs is true and protocol is HTTP', () => {
92+
it('should force WS when metadata.enforceWs is true and protocol is HTTP', () => {
9393
expect(
9494
getUrlFromParts(
9595
{
@@ -99,12 +99,12 @@ describe('getUrlFromParts', () => {
9999
port: '8080',
100100
protocol: 'http:',
101101
},
102-
true
102+
{ enforceWs: true }
103103
)
104104
).toStrictEqual('ws://localhost:8080/sockjs-node');
105105
});
106106

107-
it('should force WSS when enforceWs is true and protocol is HTTPS', () => {
107+
it('should force WSS when metadata.enforceWs is true and protocol is HTTPS', () => {
108108
expect(
109109
getUrlFromParts(
110110
{
@@ -114,7 +114,7 @@ describe('getUrlFromParts', () => {
114114
port: '8080',
115115
protocol: 'https:',
116116
},
117-
true
117+
{ enforceWs: true }
118118
)
119119
).toStrictEqual('wss://localhost:8080/sockjs-node');
120120
});

test/sockets/parseQuery.test.js

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)