getSocketUrlParts.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. const getCurrentScriptSource = require('./getCurrentScriptSource.js');
  2. /**
  3. * @typedef {Object} SocketUrlParts
  4. * @property {string} [auth]
  5. * @property {string} hostname
  6. * @property {string} [protocol]
  7. * @property {string} pathname
  8. * @property {string} [port]
  9. */
  10. /**
  11. * Parse current location and Webpack's `__resourceQuery` into parts that can create a valid socket URL.
  12. * @param {string} [resourceQuery] The Webpack `__resourceQuery` string.
  13. * @param {import('./getWDSMetadata').WDSMetaObj} [metadata] The parsed WDS metadata object.
  14. * @returns {SocketUrlParts} The parsed URL parts.
  15. * @see https://webpack.js.org/api/module-variables/#__resourcequery-webpack-specific
  16. */
  17. function getSocketUrlParts(resourceQuery, metadata) {
  18. if (typeof metadata === 'undefined') {
  19. metadata = {};
  20. }
  21. /** @type {SocketUrlParts} */
  22. let urlParts = {};
  23. // If the resource query is available,
  24. // parse it and ignore everything we received from the script host.
  25. if (resourceQuery) {
  26. const parsedQuery = {};
  27. const searchParams = new URLSearchParams(resourceQuery.slice(1));
  28. searchParams.forEach(function (value, key) {
  29. parsedQuery[key] = value;
  30. });
  31. urlParts.hostname = parsedQuery.sockHost;
  32. urlParts.pathname = parsedQuery.sockPath;
  33. urlParts.port = parsedQuery.sockPort;
  34. // Make sure the protocol from resource query has a trailing colon
  35. if (parsedQuery.sockProtocol) {
  36. urlParts.protocol = parsedQuery.sockProtocol + ':';
  37. }
  38. } else {
  39. const scriptSource = getCurrentScriptSource();
  40. let url = {};
  41. try {
  42. // The placeholder `baseURL` with `window.location.href`,
  43. // is to allow parsing of path-relative or protocol-relative URLs,
  44. // and will have no effect if `scriptSource` is a fully valid URL.
  45. url = new URL(scriptSource, window.location.href);
  46. } catch (e) {
  47. // URL parsing failed, do nothing.
  48. // We will still proceed to see if we can recover using `resourceQuery`
  49. }
  50. // Parse authentication credentials in case we need them
  51. if (url.username) {
  52. // Since HTTP basic authentication does not allow empty username,
  53. // we only include password if the username is not empty.
  54. // Result: <username> or <username>:<password>
  55. urlParts.auth = url.username;
  56. if (url.password) {
  57. urlParts.auth += ':' + url.password;
  58. }
  59. }
  60. // `file://` URLs has `'null'` origin
  61. if (url.origin !== 'null') {
  62. urlParts.hostname = url.hostname;
  63. }
  64. urlParts.protocol = url.protocol;
  65. urlParts.port = url.port;
  66. }
  67. if (!urlParts.pathname) {
  68. if (metadata.version === 4) {
  69. // This is hard-coded in WDS v4
  70. urlParts.pathname = '/ws';
  71. } else {
  72. // This is hard-coded in WDS v3
  73. urlParts.pathname = '/sockjs-node';
  74. }
  75. }
  76. // Check for IPv4 and IPv6 host addresses that correspond to any/empty.
  77. // This is important because `hostname` can be empty for some hosts,
  78. // such as 'about:blank' or 'file://' URLs.
  79. const isEmptyHostname =
  80. urlParts.hostname === '0.0.0.0' || urlParts.hostname === '[::]' || !urlParts.hostname;
  81. // We only re-assign the hostname if it is empty,
  82. // and if we are using HTTP/HTTPS protocols.
  83. if (
  84. isEmptyHostname &&
  85. window.location.hostname &&
  86. window.location.protocol.indexOf('http') === 0
  87. ) {
  88. urlParts.hostname = window.location.hostname;
  89. }
  90. // We only re-assign `protocol` when `protocol` is unavailable,
  91. // or if `hostname` is available and is empty,
  92. // since otherwise we risk creating an invalid URL.
  93. // We also do this when 'https' is used as it mandates the use of secure sockets.
  94. if (
  95. !urlParts.protocol ||
  96. (urlParts.hostname && (isEmptyHostname || window.location.protocol === 'https:'))
  97. ) {
  98. urlParts.protocol = window.location.protocol;
  99. }
  100. // We only re-assign port when it is not available
  101. if (!urlParts.port) {
  102. urlParts.port = window.location.port;
  103. }
  104. if (!urlParts.hostname || !urlParts.pathname) {
  105. throw new Error(
  106. [
  107. '[React Refresh] Failed to get an URL for the socket connection.',
  108. "This usually means that the current executed script doesn't have a `src` attribute set.",
  109. 'You should either specify the socket path parameters under the `devServer` key in your Webpack config, or use the `overlay` option.',
  110. 'https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/docs/API.md#overlay',
  111. ].join('\n')
  112. );
  113. }
  114. return {
  115. auth: urlParts.auth,
  116. hostname: urlParts.hostname,
  117. pathname: urlParts.pathname,
  118. protocol: urlParts.protocol,
  119. port: urlParts.port || undefined,
  120. };
  121. }
  122. module.exports = getSocketUrlParts;