Skip to content

Commit

Permalink
fix: allow to use 80 port for dev server (#3487)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait committed Jun 28, 2021
1 parent b559738 commit 22f18eb
Show file tree
Hide file tree
Showing 20 changed files with 265 additions and 854 deletions.
39 changes: 6 additions & 33 deletions client-src/index.js
Expand Up @@ -12,43 +12,20 @@ const sendMessage = require('./utils/sendMessage');
const reloadApp = require('./utils/reloadApp');
const createSocketURL = require('./utils/createSocketURL');

const status = {
isUnloading: false,
currentHash: '',
};
const defaultOptions = {
const status = { isUnloading: false, currentHash: '' };
const options = {
hot: false,
hotReload: true,
liveReload: false,
initial: true,
progress: false,
overlay: false,
};
const parsedResourceQuery = parseURL(__resourceQuery);
const options = defaultOptions;

// Handle Node.js legacy format and `new URL()`
if (parsedResourceQuery.query) {
Object.assign(options, parsedResourceQuery.query);
} else if (parsedResourceQuery.searchParams) {
const paramsToObject = (entries) => {
const result = {};

for (const [key, value] of entries) {
result[key] = value;
}

return result;
};

Object.assign(
options,
paramsToObject(parsedResourceQuery.searchParams.entries())
);
if (parsedResourceQuery.logging) {
options.logging = parsedResourceQuery.logging;
}

const socketURL = createSocketURL(parsedResourceQuery);

function setAllLogLevel(level) {
// This is needed because the HMR logger operate separately from dev server logger
webpackHotLog.setLogLevel(
Expand All @@ -65,12 +42,6 @@ self.addEventListener('beforeunload', () => {
status.isUnloading = true;
});

if (typeof window !== 'undefined') {
const qs = window.location.search.toLowerCase();

options.hotReload = qs.indexOf('hotreload=false') === -1;
}

const onSocketMessage = {
hot() {
options.hot = true;
Expand Down Expand Up @@ -220,4 +191,6 @@ const onSocketMessage = {
},
};

const socketURL = createSocketURL(parsedResourceQuery);

socket(socketURL, onSocketMessage);
44 changes: 22 additions & 22 deletions client-src/utils/createSocketURL.js
Expand Up @@ -5,14 +5,13 @@ const url = require('url');
// We handle legacy API that is Node.js specific, and a newer API that implements the same WHATWG URL Standard used by web browsers
// Please look at https://nodejs.org/api/url.html#url_url_strings_and_url_objects
function createSocketURL(parsedURL) {
let { auth, hostname, protocol, port } = parsedURL;
let { hostname } = parsedURL;

// Node.js module parses it as `::`
// `new URL(urlString, [baseURLstring])` parses it as '[::]'
const isInAddrAny =
hostname === '0.0.0.0' || hostname === '::' || hostname === '[::]';

// check ipv4 and ipv6 `all hostname`
// why do we need this check?
// hostname n/a for file protocol (example, when using electron, ionic)
// see: https://github.com/webpack/webpack-dev-server/pull/384
Expand All @@ -24,54 +23,55 @@ function createSocketURL(parsedURL) {
hostname = self.location.hostname;
}

if (protocol === 'auto:') {
protocol = self.location.protocol;
}
let socketURLProtocol = parsedURL.protocol || 'ws:';

// `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 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;
if (
socketURLProtocol === 'auto:' ||
(hostname && isInAddrAny && self.location.protocol === 'https:')
) {
socketURLProtocol = self.location.protocol;
}

const socketURLProtocol = protocol.replace(
socketURLProtocol = socketURLProtocol.replace(
/^(?:http|.+-extension|file)/i,
'ws'
);

let socketURLAuth = '';

// `new URL(urlString, [baseURLstring])` doesn't have `auth` property
// Parse authentication credentials in case we need them
if (parsedURL.username) {
auth = parsedURL.username;
socketURLAuth = parsedURL.username;

// Since HTTP basic authentication does not allow empty username,
// we only include password if the username is not empty.
if (parsedURL.password) {
// Result: <username>:<password>
auth = auth.concat(':', parsedURL.password);
socketURLAuth = socketURLAuth.concat(':', parsedURL.password);
}
}

const socketURLAuth = auth;

// In case the host is a raw IPv6 address, it can be enclosed in
// the brackets as the brackets are needed in the final URL string.
// Need to remove those as url.format blindly adds its own set of brackets
// if the host string contains colons. That would lead to non-working
// double brackets (e.g. [[::]]) host
//
// All of these sock url params are optionally passed in through resourceQuery,
// All of these web socket url params are optionally passed in through resourceQuery,
// so we need to fall back to the default if they are not provided
const socketURLHostname = (hostname || 'localhost').replace(
/^\[(.*)\]$/,
'$1'
);
const socketURLHostname = (
hostname ||
self.location.hostname ||
'localhost'
).replace(/^\[(.*)\]$/, '$1');

if (!port || port === '0') {
port = self.location.port;
}
let socketURLPort = parsedURL.port;

const socketURLPort = port;
if (!socketURLPort || socketURLPort === '0') {
socketURLPort = self.location.port;
}

// If path is provided it'll be passed in via the resourceQuery as a
// query param so it has to be parsed out of the querystring in order for the
Expand Down
24 changes: 8 additions & 16 deletions client-src/utils/parseURL.js
Expand Up @@ -4,24 +4,16 @@ const url = require('url');
const getCurrentScriptSource = require('./getCurrentScriptSource');

function parseURL(resourceQuery) {
let options;
let options = {};

if (typeof resourceQuery === 'string' && resourceQuery !== '') {
// If this bundle is inlined, use the resource query to get the correct url.
// for backward compatibility we supports:
// - ?ws://0.0.0.0:8096&3Flogging=info
// - ?ws%3A%2F%2F192.168.0.5%3A8080%2F%3Flogging%3Dinfo
// Also we support `http` and `https` for backward compatibility too
options = url.parse(
decodeURIComponent(
resourceQuery
// strip leading `?` from query string to get a valid URL
.substr(1)
// replace first `&` with `?` to have a valid query string
.replace('&', '?')
),
true
);
const searchParams = resourceQuery.substr(1).split('&');

for (let i = 0; i < searchParams.length; i++) {
const pair = searchParams[i].split('=');

options[pair[0]] = decodeURIComponent(pair[1]);
}
} else {
// Else, get the url from the <script> this file was called with.
const scriptSource = getCurrentScriptSource();
Expand Down
7 changes: 2 additions & 5 deletions client-src/utils/reloadApp.js
Expand Up @@ -2,11 +2,8 @@

const { log } = require('./log');

function reloadApp(
{ hotReload, hot, liveReload },
{ isUnloading, currentHash }
) {
if (isUnloading || !hotReload) {
function reloadApp({ hot, liveReload }, { isUnloading, currentHash }) {
if (isUnloading) {
return;
}

Expand Down
110 changes: 73 additions & 37 deletions lib/utils/DevServerPlugin.js
@@ -1,8 +1,5 @@
'use strict';

const ipaddr = require('ipaddr.js');
const getSocketClientPath = require('./getSocketClientPath');

/**
* An Entry, it can be of type string or string[] or Object<string | string[],string>
* @typedef {(string[] | string | Object<string | string[],string>)} Entry
Expand All @@ -16,11 +13,66 @@ class DevServerPlugin {
this.options = options;
}

getWebsocketTransport() {
let ClientImplementation;
let clientImplementationFound = true;

const isKnownWebSocketServerImplementation =
typeof this.options.webSocketServer.type === 'string' &&
(this.options.webSocketServer.type === 'ws' ||
this.options.webSocketServer.type === 'sockjs');

let clientTransport;

if (typeof this.options.client.transport !== 'undefined') {
clientTransport = this.options.client.transport;
} else if (isKnownWebSocketServerImplementation) {
clientTransport = this.options.webSocketServer.type;
}

switch (typeof clientTransport) {
case 'string':
// could be 'sockjs', 'ws', or a path that should be required
if (clientTransport === 'sockjs') {
ClientImplementation = require.resolve(
'../../client/clients/SockJSClient'
);
} else if (clientTransport === 'ws') {
ClientImplementation = require.resolve(
'../../client/clients/WebsocketClient'
);
} else {
try {
// eslint-disable-next-line import/no-dynamic-require
ClientImplementation = require.resolve(clientTransport);
} catch (e) {
clientImplementationFound = false;
}
}
break;
default:
clientImplementationFound = false;
}

if (!clientImplementationFound) {
throw new Error(
`${
!isKnownWebSocketServerImplementation
? 'When you use custom web socket implementation you must explicitly specify client.transport. '
: ''
}client.transport must be a string denoting a default implementation (e.g. 'sockjs', 'ws') or a full path to a JS file via require.resolve(...) which exports a class `
);
}

return ClientImplementation;
}

/**
* @returns {string}
*/
getWebSocketURL() {
const { options } = this;
const searchParams = new URLSearchParams();

/** @type {"ws:" | "wss:" | "http:" | "https:" | "auto:"} */
let protocol;
Expand All @@ -32,6 +84,16 @@ class DevServerPlugin {
protocol = options.https ? 'wss:' : 'ws:';
}

searchParams.set('protocol', protocol);

if (typeof options.client.webSocketURL.username !== 'undefined') {
searchParams.set('username', options.client.webSocketURL.username);
}

if (typeof options.client.webSocketURL.password !== 'undefined') {
searchParams.set('password', options.client.webSocketURL.password);
}

/** @type {string} */
let hostname;

Expand Down Expand Up @@ -59,6 +121,8 @@ class DevServerPlugin {
hostname = '0.0.0.0';
}

searchParams.set('hostname', hostname);

/** @type {number | string} */
let port;

Expand All @@ -83,17 +147,18 @@ class DevServerPlugin {
}
// The `port` option is not specified or set to `auto`
else {
port = 0;
port = '0';
}

searchParams.set('port', String(port));

/** @type {string} */
let pathname = '';

// We are proxying dev server and need to specify custom `pathname`
if (typeof options.client.webSocketURL.pathname !== 'undefined') {
pathname = options.client.webSocketURL.pathname;
}

// Web socket server works on custom `path`
else if (
typeof options.webSocketServer.options.prefix !== 'undefined' ||
Expand All @@ -104,42 +169,13 @@ class DevServerPlugin {
options.webSocketServer.options.path;
}

/** @type {string} */
let username = '';

if (typeof options.client.webSocketURL.username !== 'undefined') {
username = options.client.webSocketURL.username;
}

/** @type {string} */
let password = '';

if (typeof options.client.webSocketURL.password !== 'undefined') {
password = options.client.webSocketURL.password;
}

const searchParams = new URLSearchParams();
searchParams.set('pathname', pathname);

if (typeof options.client.logging !== 'undefined') {
searchParams.set('logging', options.client.logging);
}

const searchParamsString = searchParams.toString();

return encodeURIComponent(
new URL(
`${protocol}//${username}${password ? `:${password}` : ''}${
username || password ? `@` : ''
}${ipaddr.IPv6.isIPv6(hostname) ? `[${hostname}]` : hostname}${
port ? `:${port}` : ''
}${pathname || '/'}${
searchParamsString ? `?${searchParamsString}` : ''
}`
).toString()
).replace(
/[!'()*]/g,
(character) => `%${character.charCodeAt(0).toString(16)}`
);
return searchParams.toString();
}

/**
Expand Down Expand Up @@ -315,7 +351,7 @@ class DevServerPlugin {
}

const providePlugin = new webpack.ProvidePlugin({
__webpack_dev_server_client__: getSocketClientPath(this.options),
__webpack_dev_server_client__: this.getWebsocketTransport(),
});

providePlugin.apply(compiler);
Expand Down
3 changes: 0 additions & 3 deletions lib/utils/getSocketClientPath.d.ts

This file was deleted.

0 comments on commit 22f18eb

Please sign in to comment.