Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"files": [
"test/jest-test-setup.js",
"test/helpers/{,!(fixtures)*/}*.js",
"test/mocks/**/*.js",
"test/**/*.test.js"
],
"env": {
Expand Down
12 changes: 6 additions & 6 deletions client/ErrorOverlayEntry.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* global __react_refresh_error_overlay__, __react_refresh_init_socket__ */
/* global __react_refresh_error_overlay__, __react_refresh_init_socket__, __resourceQuery */

const registerErrorEventHandlers = require('./errorEventHandlers');
const formatWebpackErrors = require('./formatWebpackErrors');
const errorEventHandlers = require('./utils/errorEventHandlers');
const formatWebpackErrors = require('./utils/formatWebpackErrors');

// Setup error states
let isHotReload = false;
Expand Down Expand Up @@ -71,13 +71,13 @@ function compileMessageHandler(message) {
}

// Registers handlers for compile errors
__react_refresh_init_socket__(compileMessageHandler);
__react_refresh_init_socket__(compileMessageHandler, __resourceQuery);
// Registers handlers for runtime errors
registerErrorEventHandlers.error(function handleError(error) {
errorEventHandlers.error(function handleError(error) {
hasRuntimeErrors = true;
__react_refresh_error_overlay__.handleRuntimeError(error);
});
registerErrorEventHandlers.unhandledRejection(function handleUnhandledPromiseRejection(error) {
errorEventHandlers.unhandledRejection(function handleUnhandledPromiseRejection(error) {
hasRuntimeErrors = true;
__react_refresh_error_overlay__.handleRuntimeError(error);
});
2 changes: 1 addition & 1 deletion client/LegacyWDSSocketEntry.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const SockJS = require('sockjs-client/dist/sockjs');
const safeThis = require('./safeThis');
const safeThis = require('./utils/safeThis');

/**
* A SockJS client adapted for use with webpack-dev-server.
Expand Down
2 changes: 1 addition & 1 deletion client/ReactRefreshEntry.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const safeThis = require('./safeThis');
const safeThis = require('./utils/safeThis');

if (process.env.NODE_ENV !== 'production' && typeof safeThis !== 'undefined') {
// Only inject the runtime if it hasn't been injected
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
20 changes: 6 additions & 14 deletions sockets/WDSSocket.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
/* global __webpack_dev_server_client__ */

const url = require('native-url');
const getResourceQuery = require('./utils/getResourceQuery');
const getSocketUrlParts = require('./utils/getSocketUrlParts');

/**
* Initializes a socket server for HMR for webpack-dev-server.
* @param {function(*): void} messageHandler A handler to consume Webpack compilation messages.
* @param {string} [resourceQuery] Webpack's `__resourceQuery` string.
* @returns {void}
*/
function initWDSSocket(messageHandler) {
function initWDSSocket(messageHandler, resourceQuery) {
if (typeof __webpack_dev_server_client__ !== 'undefined') {
// Get config overrides from webpack __resourceQuery global
const query = getResourceQuery();
const SocketClient = __webpack_dev_server_client__;
// TODO: Support usage of custom sockets after WDS 4.0 is released
// Ref: https://github.com/webpack/webpack-dev-server/pull/2055
const connection = new SocketClient(
url.format({
protocol: window.location.protocol,
hostname: query.sockHost || window.location.hostname,
port: query.sockPort || window.location.port,
pathname: query.sockPath || '/sockjs-node',
})
);

const urlParts = getSocketUrlParts(resourceQuery);
const connection = new SocketClient(url.format(urlParts));

connection.onMessage(function onSocketMessage(data) {
const message = JSON.parse(data);
Expand Down
22 changes: 22 additions & 0 deletions sockets/utils/getCurrentScriptSource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Gets the source (i.e. host) of the script currently running.
* @returns {string}
*/
function getCurrentScriptSource() {
// `document.currentScript` is the most accurate way to get the current running script,
// but is not supported in all browsers (most notably, IE).
if (document.currentScript) {
return document.currentScript.getAttribute('src');
}

// Fallback to getting all scripts running in the document.
const scriptElements = document.scripts || [];
const currentScript = scriptElements[scriptElements.length - 1];
if (currentScript) {
return currentScript.getAttribute('src');
}

throw new Error('Failed to get current script source!');
}

module.exports = getCurrentScriptSource;
94 changes: 94 additions & 0 deletions sockets/utils/getSocketUrlParts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const url = require('native-url');
const getCurrentScriptSource = require('./getCurrentScriptSource');
const parseQuery = require('./parseQuery');

/**
* @typedef {Object} SocketUrlParts
* @property {string} [auth]
* @property {string} [hostname]
* @property {string} [protocol]
* @property {string} [pathname]
* @property {string} [port]
*/

/**
* Parse current location and Webpack's `__resourceQuery` into parts that can create a valid socket URL.
* @param {string} [resourceQuery] The Webpack `__resourceQuery` string.
* @returns {SocketUrlParts} The parsed URL parts.
* @see https://webpack.js.org/api/module-variables/#__resourcequery-webpack-specific
*/
function getSocketUrlParts(resourceQuery) {
const scriptSource = getCurrentScriptSource();
const urlParts = url.parse(scriptSource);

/** @type {string | undefined} */
let auth;
let hostname = urlParts.hostname;
let protocol = urlParts.protocol;
let pathname = '/sockjs-node'; // This is hard-coded in WDS
let port = urlParts.port;

// FIXME:
// This is a hack to work-around `native-url`'s parse method,
// which filters out falsy values when concatenating the `auth` string.
// In reality, we need to check for both values to correctly inject them.
// Ref: GoogleChromeLabs/native-url#32
// The placeholder `baseURL` is to allow parsing of relative paths,
// and will have no effect if `scriptSource` is a proper URL.
const authUrlParts = new URL(scriptSource, 'http://foo.bar');
// Parse authentication credentials in case we need them
if (authUrlParts.username) {
auth = authUrlParts.username;

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

// Check for IPv4 and IPv6 host addresses that corresponds to `any`/`empty`.
// This is important because `hostname` can be empty for some hosts,
// such as `about:blank` or `file://` URLs.
const isEmptyHostname =
urlParts.hostname === '0.0.0.0' || urlParts.hostname === '::' || urlParts.hostname === null;

// We only re-assign the hostname if we are using HTTP/HTTPS protocols
if (
isEmptyHostname &&
window.location.hostname &&
window.location.protocol.indexOf('http') !== -1
) {
hostname = window.location.hostname;
}

// We only re-assign `protocol` when `hostname` is available and is empty,
// since otherwise we risk creating an invalid URL.
// We also do this when `https` is used as it mandates the use of secure sockets.
if (hostname && (isEmptyHostname || window.location.protocol === 'https:')) {
protocol = window.location.protocol;
}

// We only re-assign port when it is not available or `empty`
if (!port || port === '0') {
port = window.location.port;
}

// If the resource query is available,
// parse it and overwrite everything we received from the script host.
const parsedQuery = parseQuery(resourceQuery || '');
hostname = parsedQuery.sockHost || hostname;
pathname = parsedQuery.sockPath || pathname;
port = parsedQuery.sockPort || port;

return {
auth: auth,
hostname: hostname,
pathname: pathname,
protocol: protocol,
port: port,
};
}

module.exports = getSocketUrlParts;
20 changes: 9 additions & 11 deletions sockets/utils/getResourceQuery.js → sockets/utils/parseQuery.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
/* global __resourceQuery */

/**
* Parse webpack `__resourceQuery` string into an object.
* @see https://webpack.js.org/api/module-variables/#__resourcequery-webpack-specific
* @returns {*} The parsed query params.
* Parse a query string into an object.
* @param {string} [querystring] The query string.
* @returns {Record<string, string>} The parsed query object.
*/
function getResourceQuery() {
function parseQuery(querystring) {
let query = '';
if (typeof __resourceQuery === 'string') {
query = __resourceQuery;
if (typeof querystring === 'string') {
query = querystring;
}

/**
* Reduce __resourceQuery string such as `?foo1=bar1&foo2=bar2`:
* Transform query strings such as `?foo1=bar1&foo2=bar2`:
* - remove `?` from the start
* - split with `&`
* - split with `=`
* - split pairs with `=`
* The resulting format will be { foo1: 'bar1', foo2: 'bar2' }
*/
return query
Expand All @@ -32,4 +30,4 @@ function getResourceQuery() {
}, {});
}

module.exports = getResourceQuery;
module.exports = parseQuery;
2 changes: 1 addition & 1 deletion test/loader/loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ describe('loader', () => {
/*! namespace exports */
/*! export default [provided] [unused] [could be renamed] */
/*! other exports [not provided] [unused] */
/*! runtime requirements: module, __webpack_require__, module.id */
/*! runtime requirements: __webpack_require__, module.id, module */
/***/ ((module, __unused_webpack___webpack_exports__, __webpack_require__) => {

\\"use strict\\";
Expand Down
24 changes: 24 additions & 0 deletions test/mocks/location.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/** @type {Set<function(): void>} */
const cleanupHandlers = new Set();
afterEach(() => {
[...cleanupHandlers].map((callback) => callback());
});

const location = (href) => {
const originalLocation = global.window.location;

delete global.window.location;
global.window.location = new URL(href);

function mockRestore() {
global.window.location = originalLocation;
}

cleanupHandlers.add(mockRestore);

return {
mockRestore,
};
};

module.exports = location;
30 changes: 0 additions & 30 deletions test/unit/getResourceQuery.test.js

This file was deleted.

Loading