Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/third_party/closure/goog/url/url.js
4058 views
1
/**
2
* @license
3
* Copyright The Closure Library Authors.
4
* SPDX-License-Identifier: Apache-2.0
5
*/
6
7
/**
8
* @fileoverview Class for parsing strings into URLs using browser native
9
* resolution.
10
*
11
* Use `resolveUrl` to resolve a url string with an optional base url string to
12
* URL. Will throw an error if the resulting URL would not be valid. This can
13
* be used in place of the [URL Web API][1] while providing support in IE and
14
* working around various inconsistencies in Edge.
15
*
16
* Use `resolveRelativeUrl` to resolve any relative URL into an absolute URL for
17
* the current location.
18
*
19
* Use `createUrl` to easily construct a new URL from an existing URL.
20
*
21
* This package attempts to follow the [WHATWG URL standard][2] where
22
* possible, deviating only when there are significant advantages to doing so
23
* such as splitting out searchParams from a property to a function call to
24
* allow the compiler to remove the relevant polyfill code if unused, or
25
* removing functionality that can cause confusion, unexpected
26
* results, or unnecessary code size increases to the package. This package
27
* also adds checks that are missing in some browsers (e.g. throwing errors when
28
* a potential URL doesn't have a protocol or hostname), and generally tries to
29
* ensure consistency among browsers while still accurately reporting how a
30
* browser will interpret a given URL.
31
*
32
* Unlike goog.URI, this package is NOT intended to be used with URLs that are
33
* "special", and is only guaranteed to return useful results for the schemes
34
* listed in the spec (http(s), ws(s), ftp, file, blob). Various browsers
35
* (Chrome included) do not correctly parse special URLs and the results will
36
* be inaccurate in those cases. If you need to parse URLs using these
37
* protocols, prefer to use goog.Uri (or goog.uri.utils) instead.
38
* [1]: https://developer.mozilla.org/en-US/docs/Web/API/URL
39
* [2]: https://url.spec.whatwg.org/
40
*/
41
goog.module('goog.url');
42
goog.module.declareLegacyNamespace();
43
44
const ConstString = goog.require('goog.string.Const');
45
const Tagname = goog.require('goog.dom.TagName');
46
const safe = goog.require('goog.dom.safe');
47
const uncheckedConversions = goog.require('goog.html.uncheckedconversions');
48
const {assert} = goog.require('goog.asserts');
49
const {concat: iterableConcat, map: iterableMap} = goog.require('goog.collections.iters');
50
const {createElement} = goog.require('goog.dom');
51
52
// Capture the native URL constructor before users have a chance to clobber it.
53
/** @type {?typeof URL} */
54
const NATIVE_URL = goog.global['URL'];
55
56
/** @define {boolean} */
57
const ASSUME_COMPLIANT_URL_API = goog.define(
58
'ASSUME_COMPLIANT_URL_API',
59
// TODO(user) narrow this down if earlier featureset years allow,
60
// if they get defined. FY2020 does NOT include Edge (EdgeHTML), which is
61
// good as workarounds are needed for spec compliance and a searchParams
62
// polyfill.
63
goog.FEATURESET_YEAR >= 2020);
64
65
let urlBase = goog.global?.document?.baseURI ||
66
// baseURI is not available in IE11 and earlier
67
goog.global.location?.href || '';
68
69
/**
70
* For testing only - this adjusts the base used in `resolveRelativeUrl`.
71
* @param {string} base
72
* Maybe this should just be visible to allow others using this package to test
73
* it?
74
* @package
75
*/
76
const setUrlBaseForTesting = function(base) {
77
urlBase = base;
78
};
79
80
exports.setUrlBaseForTesting = setUrlBaseForTesting;
81
82
83
/**
84
* Feature-detection for native URL parsing
85
* @type {boolean}
86
*/
87
const supportsNativeURLConstructor = {
88
// TODO(user) Does this work without JSCompiler?
89
/** @return {boolean} */
90
valueOf: function() {
91
if (ASSUME_COMPLIANT_URL_API) {
92
return true;
93
}
94
try {
95
new NATIVE_URL('http://example.com');
96
return true;
97
} catch (e) {
98
return false;
99
}
100
}
101
}.valueOf();
102
103
/**
104
* ReadonlySearchParams partially implements the URLSearchParams interface,
105
* excluding all mutability methods and some less-useful methods for reading the
106
* underlying data. Exclusions:
107
* - append
108
* - delete
109
* - set
110
* - sort
111
* - values()
112
* - entries()
113
* - forEach(...)
114
* @extends {Iterable<!Array<string>>}
115
* @record
116
*/
117
class ReadonlySearchParams {
118
/**
119
* @param {string} key The key to retrieve a value for. Must not be
120
* url-encoded.
121
* @return {?string} The value. If a key is specified more than once, the
122
* first value is returned (as per the spec). All values will be url-decoded
123
* already.
124
*/
125
get(key) {};
126
127
/**
128
* @param {string} key The key to retrieve all values for. Must not be
129
* url-encoded.
130
* @return {!Array<string>} The list of values for this key. Will return the
131
* empty array if there are no values for the key. All values will have
132
* been url-decoded already.
133
*/
134
getAll(key) {};
135
136
/**
137
* @param {string} key The key to search for. Must not be url-encoded.
138
* @return {boolean} True iff this key exists within the search params.
139
*/
140
has(key) {};
141
142
/**
143
* @return {string}
144
*/
145
toString() {};
146
}
147
148
exports.ReadonlySearchParams = ReadonlySearchParams;
149
150
/**
151
* A polyfill implementation of ReadonlySearchParams that is only used in older
152
* browsers that don't natively support searchParams. This includes IE and Edge
153
* (EdgeHTML).
154
* @implements {ReadonlySearchParams}
155
*/
156
class SearchParamsImpl {
157
/**
158
* @param {string} search The search string from URL resolution. May
159
* optionally begin with '?', and is expected to be URL-encoded.
160
*/
161
constructor(search) {
162
/** @private @const {!Map<string, !Array<string>>} */
163
this.paramMap_ = new Map();
164
if (search.indexOf('?') == 0) {
165
search = search.substring(1);
166
}
167
const params = search.split('&');
168
for (let p of params) {
169
let key = p;
170
let val = '';
171
const keyValueSplit = p.split('=');
172
const isKV = keyValueSplit.length > 1;
173
if (isKV) {
174
key = decodeURIComponent(keyValueSplit[0].replace('+', ' '));
175
val = decodeURIComponent(keyValueSplit[1].replace('+', ' '));
176
}
177
let entries = this.paramMap_.get(key);
178
if (entries == null) {
179
entries = [];
180
this.paramMap_.set(key, entries);
181
}
182
entries.push(val);
183
}
184
}
185
186
/**
187
* @override
188
*/
189
get(key) {
190
const values = this.paramMap_.get(key);
191
return values && values.length ? values[0] : null;
192
}
193
194
/**
195
* @override
196
*/
197
getAll(key) {
198
// As per the spec, this returns the "empty sequence" if the key is not
199
// found.
200
return [...(this.paramMap_.get(key) || [])];
201
}
202
203
/**
204
* @override
205
*/
206
has(key) {
207
return this.paramMap_.has(key);
208
}
209
210
/**
211
* @return {!IteratorIterable<!Array<string>>}
212
*/
213
[Symbol.iterator]() {
214
return iterableConcat(...iterableMap(this.paramMap_, (e) => {
215
const key = /** @const {string} */ (e[0]);
216
const values = /** @const {!Array<string>} */ (e[1]);
217
return iterableMap(values, (v) => {
218
return [key, v];
219
});
220
}));
221
}
222
223
/**
224
* @override
225
*/
226
toString() {
227
return iterableSearchParamsToString(this);
228
}
229
}
230
231
/**
232
* @param {!Iterable<!Array<string>>} iterable The iterable which acts like a
233
* URLSearchParams object (each iteration returns another key and value).
234
* Note that both keys and values must NOT be already URL encoded.
235
* @return {string} The serialized SearchParams, with all keys and values
236
* correctly encoded.
237
*/
238
const iterableSearchParamsToString = function(iterable) {
239
// Some characters are not form-encoded properly by encodeURIComponent, so we
240
// enumerate their replacements here for use later.
241
const encode = (s) => {
242
// Form encoding is defined [in the spec][1] but there are some values that
243
// are not encoded the right way by encodeURIComponent. Thus, we replace
244
// their representation in the resulting encoded string with their actual
245
// encoding.
246
// [1]: https://url.spec.whatwg.org/#urlencoded-serializing
247
return encodeURIComponent(s).replace(/[!()~']|(%20)/g, (c) => {
248
return {
249
'!': '%21',
250
'(': '%28',
251
')': '%29',
252
'%20': '+',
253
'\'': '%27',
254
'~': '%7E',
255
}[c];
256
});
257
};
258
return Array
259
.from(
260
iterable,
261
(keyValuePair) =>
262
encode(keyValuePair[0]) + '=' + encode(keyValuePair[1]))
263
.join('&');
264
};
265
266
/**
267
* UrlLike mirrors most of the public readonly interface of the URL object in
268
* the URL Web API.
269
* Notable exclusions:
270
* - toJson()
271
* - searchParams
272
*
273
* Instead of using the searchParams property, use `getSearchParams` from this
274
* package. This allows for the relevant code to be removed when inspection of
275
* search parameters is not needed.
276
* @record
277
*/
278
class UrlLike {
279
constructor() {
280
/**
281
* @const {string}
282
*/
283
this.href;
284
285
/**
286
* @const {string}
287
*/
288
this.protocol;
289
290
/**
291
* @const {string}
292
*/
293
this.username;
294
295
/**
296
* @const {string}
297
*/
298
this.password;
299
300
/**
301
* @const {string}
302
*/
303
this.host;
304
305
/**
306
* @const {string}
307
*/
308
this.hostname;
309
310
/**
311
* @const {string}
312
*/
313
this.port;
314
315
/**
316
* @const {string}
317
*/
318
this.origin;
319
320
/**
321
* @const {string}
322
*/
323
this.pathname;
324
325
/**
326
* @const {string}
327
*/
328
this.search;
329
330
/**
331
* @const {string}
332
*/
333
this.hash;
334
}
335
336
/** @return {string} */
337
toString() {};
338
}
339
340
exports.UrlLike = UrlLike;
341
342
/**
343
* This function is equivalent to 'new URL(href)' in newer browsers, and will
344
* automatically work around the Security Problems in IE, retrying the parse
345
* automatically while extracting the userinfo.
346
* @param {string} urlStr
347
* @return {!UrlLike} A canonicalized version of the information from the URL.
348
* Will throw if the resulting URL is invalid.
349
*/
350
const createAnchorElementInIE = function(urlStr) {
351
const aTag = createElement(Tagname.A);
352
353
let protocol;
354
try {
355
safe.setAnchorHref(
356
aTag,
357
uncheckedConversions.safeUrlFromStringKnownToSatisfyTypeContract(
358
ConstString.from(
359
'This url is attached to an Anchor tag that is NEVER attached ' +
360
' to the DOM and is not returned from this function.'),
361
urlStr));
362
// If the URL is actually invalid, trying to read from it will throw.
363
protocol = aTag.protocol;
364
} catch (e) {
365
// We catch and re-throw an error here as the default error in IE is
366
// simply 'Invalid argument.' with no useful information.
367
throw new Error(`${urlStr} is not a valid URL.`);
368
}
369
// The anchor tag will be created and assigned some values, but a URL missing
370
// a protocol and/or hostname is not valid in a browser and other browsers URL
371
// APIs reject them.
372
// '' : IE11.719.18362, IE11.0.9600
373
// ':' : IE11.??? (web testing version as of 04/03/2020)
374
// last char != ':': hunch...
375
if (protocol === '' || protocol === ':' ||
376
protocol[protocol.length - 1] != ':') {
377
throw new Error(`${urlStr} is not a valid URL.`);
378
}
379
if (!canonicalPortForProtocols.has(protocol)) {
380
throw new Error(`${urlStr} is not a valid URL.`);
381
}
382
if (!aTag.hostname) {
383
throw new Error(`${urlStr} is not a valid URL.`);
384
}
385
const href = aTag.href;
386
const urlLike = {
387
href,
388
protocol: aTag.protocol,
389
username: '',
390
password: '',
391
// Host, origin, and port assigned below after canonicalization.
392
hostname: aTag.hostname,
393
pathname: '/' + aTag.pathname,
394
search: aTag.search,
395
hash: aTag.hash,
396
toString: () => href,
397
};
398
// Canonicalize the port out from the URL if it matches
399
const canonicalPort = canonicalPortForProtocols.get(aTag.protocol);
400
if (canonicalPort === aTag.port) {
401
urlLike.host = urlLike.hostname;
402
urlLike.port = '';
403
// This does not work for blob and file protocol types - they are far more
404
// complicated.
405
urlLike.origin = urlLike.protocol + '//' + urlLike.hostname;
406
} else {
407
urlLike.host = aTag.host;
408
urlLike.port = aTag.port;
409
urlLike.origin =
410
urlLike.protocol + '//' + urlLike.hostname + ':' + urlLike.port;
411
}
412
return urlLike;
413
};
414
415
/**
416
* @param {?string} username
417
* @param {?string} password
418
* @return {string} The serialized userinfo string
419
*/
420
const assembleUserInfo = function(username, password) {
421
if (username && password) {
422
return username + ':' + password + '@';
423
} else if (username) {
424
return username + '@';
425
} else if (password) {
426
return ':' + password + '@';
427
} else {
428
return '';
429
}
430
};
431
432
/**
433
* This function wraps 'new URL(href)' in newer browsers adds common checks for
434
* parts of the URL spec (e.g. no protocol, no hostname for well-known protocols
435
* like HTTP(s) and WS(S)) that some browsers don't adhere to. It also adds
436
* origin construction for browsers that don't support it (Edge).
437
* @param {string} urlStr
438
* @return {!UrlLike}
439
*/
440
const urlParseWithCommonChecks = function(urlStr) {
441
let res;
442
try {
443
res = new NATIVE_URL(urlStr);
444
} catch (e) {
445
throw new Error(`${urlStr} is not a valid URL.`);
446
}
447
const canonicalPort = canonicalPortForProtocols.get(res.protocol);
448
if (!canonicalPort) {
449
throw new Error(`${urlStr} is not a valid URL.`);
450
}
451
if (!res.hostname) {
452
throw new Error(`${urlStr} is not a valid URL.`);
453
}
454
// For some protocols, Edge doen't know how to construct the origin.
455
if (res.origin != 'null') {
456
return res;
457
}
458
// We can't assign to the native object's origin property (it is ignored), so
459
// we make a copy here.
460
const urlLike = {
461
href: res.href,
462
protocol: res.protocol,
463
username: '',
464
password: '',
465
host: res.host,
466
port: res.port,
467
// origin assigned below after canonicalization.
468
hostname: res.hostname,
469
pathname: res.pathname,
470
search: res.search,
471
// We don't copy searchParams because Edge doesn't have it anyways.
472
hash: res.hash,
473
};
474
if (canonicalPort === res.port) {
475
// This does not work for blob and file protocol types - they are far more
476
// complicated.
477
urlLike.origin = res.protocol + '//' + res.hostname;
478
} else {
479
urlLike.origin = res.protocol + '//' + res.hostname + ':' + res.port;
480
}
481
return urlLike;
482
};
483
484
/**
485
* Resolves the given url string (with the optional base) into a URL object
486
* according to the [URL spec][https://url.spec.whatwg.org/]. Will throw an
487
* error if the resulting URL is invalid or if the browser can't or won't use
488
* that URL for some reason. Relative URLs are considered invalid without a base
489
* and will throw an error - please use `resolveRelativeUrl` instead for this
490
* use-case.
491
*
492
* Note that calling resolveUrl with both urlStr and baseStr may have surprising
493
* behavior. For example, any invocation with both parameters will never use the
494
* hash value from baseStr. Similarly, passing a path segment in urlStr will
495
* append (or replace) the path in baseStr, but will ALSO exclude the search and
496
* hash portions of baseStr from the resulting URL. See the unit tests
497
* (specifically testWithBase* test cases) for examples.
498
*
499
* Compatibility notes:
500
* - both IE (all versions) and Edge (EdgeHTML only) disallow URLs to have user
501
* information in them, and parsing those strings will throw an error.
502
* - FireFox disallows URLs with just a password in the userinfo.
503
* @param {string} urlStr A potential absolute URL as a string, or a relative
504
* URL if baseStr is provided.
505
* @param {string=} baseStr An optional base url as a string, only required if
506
* the url is relative.
507
* @return {!UrlLike} An object that describes the various parts of the URL if
508
* valid. Throws an error if invalid. While this object is the native URL
509
* object where possible, users should NOT rely on this property and instead
510
* treat it as a simple record.
511
*/
512
const resolveUrl = function(urlStr, baseStr) {
513
if (ASSUME_COMPLIANT_URL_API) {
514
// Safari throws a TypeError if you call the constructor with a second
515
// argument that isn't defined, so we can't pass baseStr all the time.
516
return baseStr ? new NATIVE_URL(urlStr, baseStr) : new NATIVE_URL(urlStr);
517
}
518
519
// Ideally, this should effectively become
520
// if Edge
521
// and the else should effectively become
522
// if IE
523
524
// TODO(user) Some use of FEATURESET_YEAR near here would help strip
525
// down the implementation even more for browsers we are more sure support the
526
// URL Web API (including Edge). 2019? Maybe?
527
528
if (supportsNativeURLConstructor) {
529
if (!baseStr) {
530
return urlParseWithCommonChecks(urlStr);
531
}
532
// Edge doesn't throw if baseStr is not a valid absolute URL when the
533
// urlStr is absolute. This is against the spec, so try and parse this with
534
// commonChecks (which will throw if baseStr is not a valid absolute URL).
535
const baseUrl = urlParseWithCommonChecks(baseStr);
536
537
// If urlStr is present and absolute, then only those values are used.
538
try {
539
return urlParseWithCommonChecks(urlStr);
540
} catch (e) {
541
// urlStr is not absolute. We shall give both pieces to the constructor
542
// below and see what it thinks.
543
}
544
return new NATIVE_URL(urlStr, baseUrl.href);
545
} else {
546
if (!baseStr) {
547
return createAnchorElementInIE(urlStr);
548
}
549
// It is against the spec to provide a baseStr that is not absolute.
550
const baseUrl = createAnchorElementInIE(baseStr);
551
552
// If urlStr is present and absolute, then only those values are used even
553
// if baseStr is defined. The spec says we must try and parse baseStr first
554
// (and error on it) before we do this though.
555
try {
556
return createAnchorElementInIE(urlStr);
557
} catch (e) {
558
// urlStr is not absolute. We shall assemble base pieces + url pieces
559
// below.
560
// Deliberate fallthrough
561
}
562
563
// If the base is present and absolute, check for special characters that
564
// help determine what parts of base we use vs the relative parts.
565
// This is similar to the [state machine][1] mentioned in the
566
// spec except we already know that urlStr is NOT absolute.
567
// [1]: https://url.spec.whatwg.org/#relative-state
568
const newBaseStr = baseUrl.protocol + '//' +
569
assembleUserInfo(baseUrl.username, baseUrl.password) + baseUrl.host;
570
let /** string */ href;
571
const firstChar = urlStr[0];
572
if (firstChar === '/' || firstChar === '\\') {
573
href = newBaseStr + urlStr;
574
} else if (firstChar === '?') {
575
href = newBaseStr + baseUrl.pathname + urlStr;
576
} else if (!firstChar || firstChar === '#') {
577
href = newBaseStr + baseUrl.pathname + baseUrl.search + urlStr;
578
} else {
579
// This doesn't start with any of the authority terminating characters,
580
// but other browsers treat it implicitly as an extension to the existing
581
// path, removing anything after the last '/' and appending urlStr to it.
582
const lastPathSeparator = baseUrl.pathname.lastIndexOf('/');
583
const path = lastPathSeparator > 0 ?
584
baseUrl.pathname.substring(0, lastPathSeparator) :
585
'';
586
href = newBaseStr + path + '/' + urlStr;
587
}
588
return createAnchorElementInIE(href);
589
}
590
};
591
592
exports.resolveUrl = resolveUrl;
593
594
/**
595
* Browsers will canonicalize a URL if the scheme has a "canonical" port for it.
596
* This maps schemes to their canonical port. These mappings are defined in the
597
* [spec][1].
598
*
599
* [1]: https://url.spec.whatwg.org/#url-miscellaneous
600
* @type {!Map<string,string>}
601
*/
602
const canonicalPortForProtocols = new Map([
603
['http:', '80'],
604
['https:', '443'],
605
['ws:', '80'],
606
['wss:', '443'],
607
['ftp:', '21'],
608
]);
609
610
/**
611
* Returns a URLSearchParams-like object for a given URL object. This is used
612
* instead of the native URL object's 'searchParams' property to allow the
613
* Closure Compiler to code-strip the polyfill if searchParams are never used.
614
* @param {!UrlLike|!URL} url The URL object to derive SearchParams for.
615
* @return {!ReadonlySearchParams} The URLSearchParams-like object for the URL.
616
* @suppress {strictMissingProperties} url.searchParams on union
617
*/
618
const getSearchParams = function(url) {
619
if (goog.FEATURESET_YEAR >= 2020 ||
620
(supportsNativeURLConstructor && url.searchParams)) {
621
return url.searchParams;
622
}
623
return new SearchParamsImpl(url.search);
624
};
625
626
exports.getSearchParams = getSearchParams;
627
628
/**
629
* Resolves the given relative URL string without requiring a specific base URL
630
* (unlike resolveUrl). Will resolve the relative URL against the current
631
* document's BaseURI, and the resulting URL WILL contain properties from
632
* this URI.
633
* @param {string} relativeURL A string which may be only a relative URL (i.e.
634
* has no protocol, userinfo, hostname, or port).
635
* @return {!UrlLike} A URL that is relative to the current document's Base URI
636
* with all the relevant relative parts from the input parameter.
637
*/
638
const resolveRelativeUrl = function(relativeURL) {
639
return resolveUrl(relativeURL, urlBase);
640
};
641
642
exports.resolveRelativeUrl = resolveRelativeUrl;
643
644
/**
645
* @record
646
*/
647
class UrlPrimitivePartsPartial {
648
constructor() {
649
/** @const {string|undefined} */
650
this.protocol;
651
652
/** @const {string|undefined} */
653
this.username;
654
655
/** @const {string|undefined} */
656
this.password;
657
658
/** @const {string|undefined} */
659
this.hostname;
660
661
/** @const {string|undefined} */
662
this.port;
663
664
/** @const {string|undefined} */
665
this.pathname;
666
667
/** @const {string|undefined} */
668
this.search;
669
670
/** @const {!Iterable<!Array<string>>|undefined} */
671
this.searchParams;
672
673
/** @const {string|undefined} */
674
this.hash;
675
}
676
}
677
678
exports.UrlPrimitivePartsPartial = UrlPrimitivePartsPartial;
679
680
/**
681
* Creates a new URL object from primitve parts, optionally allowing for some of
682
* those parts to be taken from a base URL object. Parts only accepts primitive
683
* parts of the URL (e.g will NOT accept origin or host) for simplicity, and
684
* only accepts either a search OR searchParams property, not both at the same
685
* time. The resulting URL-like string is then parsed by `resolveUrl`, and as
686
* such this method will also throw an error if the result is not a valid URL
687
* (unlike Object.assign and other similar combinations of object properties).
688
*
689
* This method does some validation of its inputs, and in general is NOT a good
690
* way to clone an existing URL object. For that purpose, prefer to use
691
* `resolveUrl(existingURLObject.href)`.
692
* @param {!UrlPrimitivePartsPartial}
693
* parts The parts that should be combined together to create a new URL.
694
* @param {!UrlLike=} base An optional base whose primitive parts are used if
695
* they are not specified in the parts param. If all required primitive
696
* parts (host, protocol) are specified in the parts param, this can be
697
* omitted.
698
* @return {!UrlLike} The resulting URL object if valid. Will throw an error if
699
* the resulting combination of parts and base is invalid.
700
*/
701
const createUrl = function(parts, base = undefined) {
702
assert(
703
!(parts.search && parts.searchParams),
704
'Only provide search or searchParams, not both');
705
// Alas we cannot use Object.assign as the native URL object will not let its
706
// properties be copied over.
707
let newParts = {};
708
if (base) {
709
newParts.protocol = base.protocol;
710
newParts.username = base.username;
711
newParts.password = base.password;
712
newParts.hostname = base.hostname;
713
newParts.port = base.port;
714
newParts.pathname = base.pathname;
715
newParts.search = base.search;
716
// Note we don't copy over searchParams here as we won't use it anyways.
717
// search should be available instead.
718
newParts.hash = base.hash;
719
}
720
Object.assign(newParts, parts);
721
722
// Check for spec compliance
723
if (newParts.port && newParts.port[0] === ':') {
724
throw new Error('port should not start with \':\'');
725
}
726
if (newParts.hash && newParts.hash[0] != '#') {
727
newParts.hash = '#' + newParts.hash;
728
}
729
// Manually assign search/searchParams from parts and clean up newParts so it
730
// only specifies a search property.
731
// precedence is as follows:
732
// parts.search
733
// parts.searchParams
734
// newParts.search (aka base.search)
735
if (parts.search) {
736
if (parts.search[0] != '?') {
737
newParts.search = '?' + parts.search;
738
}
739
// newParts.search is already equal to parts.search due to Object.assign
740
// above. searchParams will be undefined here as it isn't copied from base.
741
} else if (parts.searchParams) {
742
newParts.search = '?' + iterableSearchParamsToString(parts.searchParams);
743
// Not strictly necessary, but clear searchParams now we have serialized it.
744
newParts.searchParams = undefined;
745
}
746
747
let sb = '';
748
if (newParts.protocol) {
749
sb += newParts.protocol + '//';
750
}
751
752
const userinfo = assembleUserInfo(newParts.username, newParts.password);
753
sb += userinfo;
754
sb += newParts.hostname || '';
755
if (newParts.port) {
756
sb += ':' + newParts.port;
757
}
758
sb += newParts.pathname || '';
759
sb += newParts.search || '';
760
sb += newParts.hash || '';
761
return resolveUrl(sb);
762
};
763
764
exports.createUrl = createUrl;
765
766