From 3da8214ca97af306af4ffa58a26c7a4b5b33e753 Mon Sep 17 00:00:00 2001 From: evilebottnawi Date: Wed, 2 Jun 2021 17:54:44 +0300 Subject: [PATCH 1/2] feat: allow to set `client.webSocketURL.protocol` --- client-src/utils/createSocketURL.js | 6 +++++- lib/options.json | 12 ++++++++++++ lib/utils/DevServerPlugin.js | 13 ++++++++++--- lib/utils/normalizeOptions.js | 3 ++- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/client-src/utils/createSocketURL.js b/client-src/utils/createSocketURL.js index 67c5cabf16..525aecd4cb 100644 --- a/client-src/utils/createSocketURL.js +++ b/client-src/utils/createSocketURL.js @@ -24,8 +24,12 @@ function createSocketURL(parsedURL) { hostname = self.location.hostname; } + if (protocol === 'auto:') { + protocol = self.location.protocol; + } + // `hostname` can be empty when the script path is relative. In that case, specifying a protocol would result in an invalid URL. - // When https is used in the app, secure websockets are always necessary because the browser doesn't accept non-secure websockets. + // When https is used in the app, secure web sockets are always necessary because the browser doesn't accept non-secure web sockets. if (hostname && isInAddrAny && self.location.protocol === 'https:') { protocol = self.location.protocol; } diff --git a/lib/options.json b/lib/options.json index d546d03f37..d5a5a0a983 100644 --- a/lib/options.json +++ b/lib/options.json @@ -290,6 +290,18 @@ "type": "object", "additionalProperties": false, "properties": { + "protocol": { + "anyOf": [ + { + "enum": ["auto"] + }, + { + "type": "string", + "minLength": 1 + } + ], + "description": "Tells clients connected to devServer to use the provided protocol." + }, "host": { "type": "string", "minLength": 1, diff --git a/lib/utils/DevServerPlugin.js b/lib/utils/DevServerPlugin.js index 2df1f3e2f0..42a71261c1 100644 --- a/lib/utils/DevServerPlugin.js +++ b/lib/utils/DevServerPlugin.js @@ -28,8 +28,15 @@ class DevServerPlugin { apply(compiler) { const { options } = this; - /** @type {"ws" | "wss"} */ - const protocol = options.https ? 'wss' : 'ws'; + /** @type {"ws:" | "wss:" | "http:" | "https:" | "auto:"} */ + let protocol; + + // We are proxying dev server and need to specify custom `host` + if (typeof options.client.webSocketURL.protocol !== 'undefined') { + protocol = options.client.webSocketURL.protocol; + } else { + protocol = options.https ? 'wss:' : 'ws:'; + } /** @type {string} */ let host; @@ -111,7 +118,7 @@ class DevServerPlugin { const webSocketURL = encodeURIComponent( new URL( - `${protocol}://${ipaddr.IPv6.isIPv6(host) ? `[${host}]` : host}${ + `${protocol}//${ipaddr.IPv6.isIPv6(host) ? `[${host}]` : host}${ port ? `:${port}` : '' }${path || '/'}${ Object.keys(searchParams).length > 0 diff --git a/lib/utils/normalizeOptions.js b/lib/utils/normalizeOptions.js index 35a1089696..538a90feac 100644 --- a/lib/utils/normalizeOptions.js +++ b/lib/utils/normalizeOptions.js @@ -120,8 +120,9 @@ function normalizeOptions(compiler, options, logger) { const parsedURL = new URL(options.client.webSocketURL); options.client.webSocketURL = { + protocol: parsedURL.protocol, host: parsedURL.hostname, - port: Number(parsedURL.port), + port: parsedURL.port.length > 0 ? Number(parsedURL.port) : '', path: parsedURL.pathname, }; } else if (typeof options.client.webSocketURL.port === 'string') { From 689a173bb1b75fc26454ad485e4e15bd53bdb0f8 Mon Sep 17 00:00:00 2001 From: evilebottnawi Date: Wed, 2 Jun 2021 18:18:21 +0300 Subject: [PATCH 2/2] test: more --- .../validate-options.test.js.snap.webpack4 | 2 +- .../validate-options.test.js.snap.webpack5 | 2 +- test/e2e/web-socket-server-and-url.test.js | 161 +++++++++++++++--- 3 files changed, 135 insertions(+), 30 deletions(-) diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack4 b/test/__snapshots__/validate-options.test.js.snap.webpack4 index a4f107f387..4dd9133ba1 100644 --- a/test/__snapshots__/validate-options.test.js.snap.webpack4 +++ b/test/__snapshots__/validate-options.test.js.snap.webpack4 @@ -156,7 +156,7 @@ exports[`options validate should throw an error on the "client" option with '{"w exports[`options validate should throw an error on the "client" option with '{"webSocketURL":{"port":true}}' value 1`] = ` "ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - configuration.client.webSocketURL should be one of these: - non-empty string | object { host?, port?, path? } + non-empty string | object { protocol?, host?, port?, path? } -> When using dev server and you're proxying dev-server, the client script does not always know where to connect to. Details: * configuration.client.webSocketURL.port should be one of these: diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack5 b/test/__snapshots__/validate-options.test.js.snap.webpack5 index a4f107f387..4dd9133ba1 100644 --- a/test/__snapshots__/validate-options.test.js.snap.webpack5 +++ b/test/__snapshots__/validate-options.test.js.snap.webpack5 @@ -156,7 +156,7 @@ exports[`options validate should throw an error on the "client" option with '{"w exports[`options validate should throw an error on the "client" option with '{"webSocketURL":{"port":true}}' value 1`] = ` "ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - configuration.client.webSocketURL should be one of these: - non-empty string | object { host?, port?, path? } + non-empty string | object { protocol?, host?, port?, path? } -> When using dev server and you're proxying dev-server, the client script does not always know where to connect to. Details: * configuration.client.webSocketURL.port should be one of these: diff --git a/test/e2e/web-socket-server-and-url.test.js b/test/e2e/web-socket-server-and-url.test.js index 2cc0d286f2..d4f6230ce6 100644 --- a/test/e2e/web-socket-server-and-url.test.js +++ b/test/e2e/web-socket-server-and-url.test.js @@ -245,7 +245,7 @@ for (const webSocketServerType of webSocketServerTypes) { }); }); - describe('should work with custom client port and path', () => { + describe('should work with the "client.webSocketURL.protocol" option', () => { beforeAll((done) => { const options = { webSocketServer: webSocketServerType, @@ -253,8 +253,7 @@ for (const webSocketServerType of webSocketServerTypes) { host: '0.0.0.0', client: { webSocketURL: { - path: '/foo/test/bar/', - port: port3, + protocol: 'ws:', }, }, }; @@ -265,11 +264,11 @@ for (const webSocketServerType of webSocketServerTypes) { afterAll(testServer.close); describe('browser client', () => { - it('uses correct port and path', (done) => { + it('should work', (done) => { runBrowser().then(({ page, browser }) => { - waitForTest(browser, page, /foo\/test\/bar/, (websocketUrl) => { + waitForTest(browser, page, /ws/, (websocketUrl) => { expect(websocketUrl).toContain( - `${websocketUrlProtocol}://localhost:${port3}/foo/test/bar` + `${websocketUrlProtocol}://localhost:${port2}/ws` ); done(); @@ -281,7 +280,7 @@ for (const webSocketServerType of webSocketServerTypes) { }); }); - describe('should work with custom client port', () => { + describe('should work with the "client.webSocketURL.protocol" option using "auto:" value', () => { beforeAll((done) => { const options = { webSocketServer: webSocketServerType, @@ -289,7 +288,7 @@ for (const webSocketServerType of webSocketServerTypes) { host: '0.0.0.0', client: { webSocketURL: { - port: port3, + protocol: 'auto:', }, }, }; @@ -300,11 +299,11 @@ for (const webSocketServerType of webSocketServerTypes) { afterAll(testServer.close); describe('browser client', () => { - it('uses correct port and path', (done) => { + it('should work', (done) => { runBrowser().then(({ page, browser }) => { waitForTest(browser, page, /ws/, (websocketUrl) => { expect(websocketUrl).toContain( - `${websocketUrlProtocol}://localhost:${port3}/ws` + `${websocketUrlProtocol}://localhost:${port2}/ws` ); done(); @@ -316,7 +315,7 @@ for (const webSocketServerType of webSocketServerTypes) { }); }); - describe('should work with custom client port as string', () => { + describe('should work with the "client.webSocketURL.protocol" option using "http:" value and covert to "ws"', () => { beforeAll((done) => { const options = { webSocketServer: webSocketServerType, @@ -324,7 +323,7 @@ for (const webSocketServerType of webSocketServerTypes) { host: '0.0.0.0', client: { webSocketURL: { - port: `${port3}`, + protocol: 'http:', }, }, }; @@ -335,11 +334,11 @@ for (const webSocketServerType of webSocketServerTypes) { afterAll(testServer.close); describe('browser client', () => { - it('uses correct port and path', (done) => { + it('should work', (done) => { runBrowser().then(({ page, browser }) => { waitForTest(browser, page, /ws/, (websocketUrl) => { expect(websocketUrl).toContain( - `${websocketUrlProtocol}://localhost:${port3}/ws` + `${websocketUrlProtocol}://localhost:${port2}/ws` ); done(); @@ -351,16 +350,79 @@ for (const webSocketServerType of webSocketServerTypes) { }); }); - describe('should work with custom client.webSocketURL.port and webSocketServer.options.port both as string', () => { + describe('should work with the "client.webSocketURL.host" option', () => { beforeAll((done) => { const options = { - webSocketServer: { - type: webSocketServerType, - options: { - host: '0.0.0.0', - port: `${port2}`, + webSocketServer: webSocketServerType, + port: port2, + host: '0.0.0.0', + client: { + webSocketURL: { + host: 'myhost.test', }, }, + }; + testServer.startAwaitingCompilation(config, options, done); + }); + + afterAll(testServer.close); + + describe('browser client', () => { + it('should work', (done) => { + runBrowser().then(({ page, browser }) => { + waitForTest(browser, page, /ws/, (websocketUrl) => { + expect(websocketUrl).toContain( + `${websocketUrlProtocol}://myhost.test:${port2}/ws` + ); + + done(); + }); + + page.goto(`http://localhost:${port2}/main`); + }); + }); + }); + }); + + describe('should work with the "client.webSocketURL.port" option', () => { + beforeAll((done) => { + const options = { + webSocketServer: webSocketServerType, + port: port2, + host: '0.0.0.0', + client: { + webSocketURL: { + port: port3, + }, + }, + }; + + testServer.startAwaitingCompilation(config, options, done); + }); + + afterAll(testServer.close); + + describe('browser client', () => { + it('should work', (done) => { + runBrowser().then(({ page, browser }) => { + waitForTest(browser, page, /ws/, (websocketUrl) => { + expect(websocketUrl).toContain( + `${websocketUrlProtocol}://localhost:${port3}/ws` + ); + + done(); + }); + + page.goto(`http://localhost:${port2}/main`); + }); + }); + }); + }); + + describe('should work with the "client.webSocketURL.port" option as "string"', () => { + beforeAll((done) => { + const options = { + webSocketServer: webSocketServerType, port: port2, host: '0.0.0.0', client: { @@ -376,7 +438,7 @@ for (const webSocketServerType of webSocketServerTypes) { afterAll(testServer.close); describe('browser client', () => { - it('uses correct port and path', (done) => { + it('should work', (done) => { runBrowser().then(({ page, browser }) => { waitForTest(browser, page, /ws/, (websocketUrl) => { expect(websocketUrl).toContain( @@ -392,7 +454,7 @@ for (const webSocketServerType of webSocketServerTypes) { }); }); - describe('should work with custom client host', () => { + describe('should work with "client.webSocketURL.port" and "client.webSocketURL.path" options', () => { beforeAll((done) => { const options = { webSocketServer: webSocketServerType, @@ -400,21 +462,64 @@ for (const webSocketServerType of webSocketServerTypes) { host: '0.0.0.0', client: { webSocketURL: { - host: 'myhost.test', + path: '/foo/test/bar/', + port: port3, }, }, }; + testServer.startAwaitingCompilation(config, options, done); }); afterAll(testServer.close); describe('browser client', () => { - it('uses correct host', (done) => { + it('should work', (done) => { + runBrowser().then(({ page, browser }) => { + waitForTest(browser, page, /foo\/test\/bar/, (websocketUrl) => { + expect(websocketUrl).toContain( + `${websocketUrlProtocol}://localhost:${port3}/foo/test/bar` + ); + + done(); + }); + + page.goto(`http://localhost:${port2}/main`); + }); + }); + }); + }); + + describe('should work with "client.webSocketURL.port" and "webSocketServer.options.port" options as string', () => { + beforeAll((done) => { + const options = { + webSocketServer: { + type: webSocketServerType, + options: { + host: '0.0.0.0', + port: `${port2}`, + }, + }, + port: port2, + host: '0.0.0.0', + client: { + webSocketURL: { + port: `${port3}`, + }, + }, + }; + + testServer.startAwaitingCompilation(config, options, done); + }); + + afterAll(testServer.close); + + describe('browser client', () => { + it('should work', (done) => { runBrowser().then(({ page, browser }) => { waitForTest(browser, page, /ws/, (websocketUrl) => { expect(websocketUrl).toContain( - `${websocketUrlProtocol}://myhost.test:${port2}/ws` + `${websocketUrlProtocol}://localhost:${port3}/ws` ); done(); @@ -426,7 +531,7 @@ for (const webSocketServerType of webSocketServerTypes) { }); }); - describe('should work with custom client host, port, and path', () => { + describe('should work with "client.webSocketURL.host", "webSocketServer.options.port" and "webSocketServer.options.path" options', () => { beforeAll((done) => { const options = { webSocketServer: webSocketServerType, @@ -463,7 +568,7 @@ for (const webSocketServerType of webSocketServerTypes) { }); }); - describe('should work with the "client.webSocketURL" option and custom client path', () => { + describe('should work with the "client.webSocketURL" option as "string"', () => { beforeAll((done) => { const options = { webSocketServer: webSocketServerType, @@ -480,7 +585,7 @@ for (const webSocketServerType of webSocketServerTypes) { afterAll(testServer.close); describe('browser client', () => { - it('uses the correct webSocketURL hostname and path', (done) => { + it('should work', (done) => { runBrowser().then(({ page, browser }) => { waitForTest(browser, page, /foo\/test\/bar/, (websocketUrl) => { expect(websocketUrl).toContain(