@@ -7,7 +7,6 @@ import { formatProblem, createOverlay } from "./overlay.js";
77import { log , logEnabledFeatures , setLogLevel } from "./utils/log.js" ;
88import sendMessage from "./utils/sendMessage.js" ;
99import reloadApp from "./utils/reloadApp.js" ;
10- import createSocketURL from "./utils/createSocketURL.js" ;
1110import { isProgressSupported , defineProgressElement } from "./progress.js" ;
1211
1312/**
@@ -406,6 +405,168 @@ const onSocketMessage = {
406405 } ,
407406} ;
408407
408+ /**
409+ * @param {{ protocol?: string, auth?: string, hostname?: string, port?: string, pathname?: string, search?: string, hash?: string, slashes?: boolean } } objURL
410+ * @returns {string }
411+ */
412+ const formatURL = ( objURL ) => {
413+ let protocol = objURL . protocol || "" ;
414+
415+ if ( protocol && protocol . substr ( - 1 ) !== ":" ) {
416+ protocol += ":" ;
417+ }
418+
419+ let auth = objURL . auth || "" ;
420+
421+ if ( auth ) {
422+ auth = encodeURIComponent ( auth ) ;
423+ auth = auth . replace ( / % 3 A / i, ":" ) ;
424+ auth += "@" ;
425+ }
426+
427+ let host = "" ;
428+
429+ if ( objURL . hostname ) {
430+ host =
431+ auth +
432+ ( objURL . hostname . indexOf ( ":" ) === - 1
433+ ? objURL . hostname
434+ : `[${ objURL . hostname } ]` ) ;
435+
436+ if ( objURL . port ) {
437+ host += `:${ objURL . port } ` ;
438+ }
439+ }
440+
441+ let pathname = objURL . pathname || "" ;
442+
443+ if ( objURL . slashes ) {
444+ host = `//${ host || "" } ` ;
445+
446+ if ( pathname && pathname . charAt ( 0 ) !== "/" ) {
447+ pathname = `/${ pathname } ` ;
448+ }
449+ } else if ( ! host ) {
450+ host = "" ;
451+ }
452+
453+ let search = objURL . search || "" ;
454+
455+ if ( search && search . charAt ( 0 ) !== "?" ) {
456+ search = `?${ search } ` ;
457+ }
458+
459+ let hash = objURL . hash || "" ;
460+
461+ if ( hash && hash . charAt ( 0 ) !== "#" ) {
462+ hash = `#${ hash } ` ;
463+ }
464+
465+ pathname = pathname . replace (
466+ / [ ? # ] / g,
467+ /**
468+ * @param {string } match
469+ * @returns {string }
470+ */
471+ ( match ) => encodeURIComponent ( match ) ,
472+ ) ;
473+ search = search . replace ( "#" , "%23" ) ;
474+
475+ return `${ protocol } ${ host } ${ pathname } ${ search } ${ hash } ` ;
476+ } ;
477+
478+ /**
479+ * @param {URL & { fromCurrentScript?: boolean } } parsedURL
480+ * @returns {string }
481+ */
482+ const createSocketURL = ( parsedURL ) => {
483+ let { hostname } = parsedURL ;
484+
485+ // Node.js module parses it as `::`
486+ // `new URL(urlString, [baseURLString])` parses it as '[::]'
487+ const isInAddrAny =
488+ hostname === "0.0.0.0" || hostname === "::" || hostname === "[::]" ;
489+
490+ // why do we need this check?
491+ // hostname n/a for file protocol (example, when using electron, ionic)
492+ // see: https://github.com/webpack/webpack-dev-server/pull/384
493+ if (
494+ isInAddrAny &&
495+ self . location . hostname &&
496+ self . location . protocol . indexOf ( "http" ) === 0
497+ ) {
498+ hostname = self . location . hostname ;
499+ }
500+
501+ let socketURLProtocol = parsedURL . protocol || self . location . protocol ;
502+
503+ // When https is used in the app, secure web sockets are always necessary because the browser doesn't accept non-secure web sockets.
504+ if (
505+ socketURLProtocol === "auto:" ||
506+ ( hostname && isInAddrAny && self . location . protocol === "https:" )
507+ ) {
508+ socketURLProtocol = self . location . protocol ;
509+ }
510+
511+ socketURLProtocol = socketURLProtocol . replace (
512+ / ^ (?: h t t p | .+ - e x t e n s i o n | f i l e ) / i,
513+ "ws" ,
514+ ) ;
515+
516+ let socketURLAuth = "" ;
517+
518+ // `new URL(urlString, [baseURLstring])` doesn't have `auth` property
519+ // Parse authentication credentials in case we need them
520+ if ( parsedURL . username ) {
521+ socketURLAuth = parsedURL . username ;
522+
523+ // Since HTTP basic authentication does not allow empty username,
524+ // we only include password if the username is not empty.
525+ if ( parsedURL . password ) {
526+ // Result: <username>:<password>
527+ socketURLAuth = socketURLAuth . concat ( ":" , parsedURL . password ) ;
528+ }
529+ }
530+
531+ // In case the host is a raw IPv6 address, it can be enclosed in
532+ // the brackets as the brackets are needed in the final URL string.
533+ // Need to remove those as url.format blindly adds its own set of brackets
534+ // if the host string contains colons. That would lead to non-working
535+ // double brackets (e.g. [[::]]) host
536+ //
537+ // All of these web socket url params are optionally passed in through resourceQuery,
538+ // so we need to fall back to the default if they are not provided
539+ const socketURLHostname = (
540+ hostname ||
541+ self . location . hostname ||
542+ "localhost"
543+ ) . replace ( / ^ \[ ( .* ) \] $ / , "$1" ) ;
544+
545+ let socketURLPort = parsedURL . port ;
546+
547+ if ( ! socketURLPort || socketURLPort === "0" ) {
548+ socketURLPort = self . location . port ;
549+ }
550+
551+ // If path is provided it'll be passed in via the resourceQuery as a
552+ // query param so it has to be parsed out of the querystring in order for the
553+ // client to open the socket to the correct location.
554+ let socketURLPathname = "/ws" ;
555+
556+ if ( parsedURL . pathname && ! parsedURL . fromCurrentScript ) {
557+ socketURLPathname = parsedURL . pathname ;
558+ }
559+
560+ return formatURL ( {
561+ protocol : socketURLProtocol ,
562+ auth : socketURLAuth ,
563+ hostname : socketURLHostname ,
564+ port : socketURLPort ,
565+ pathname : socketURLPathname ,
566+ slashes : true ,
567+ } ) ;
568+ } ;
569+
409570const socketURL = createSocketURL ( parsedResourceQuery ) ;
410571
411572socket ( socketURL , onSocketMessage , options . reconnect ) ;
0 commit comments