diff --git a/doc/api/url.md b/doc/api/url.md index 94551f162b8eef..c7bed64d9edacc 100644 --- a/doc/api/url.md +++ b/doc/api/url.md @@ -153,6 +153,45 @@ myURL = new URL('foo:Example.com/', 'https://example.org/'); // foo:Example.com/ ``` +#### Class Method: URL.from(input) + +* `input` {Object} + * `protocol` {string} any valid protocol. See [`url.protocol`][]. + * `username` {string} any valid username. See [`url.username`][]. + * `password` {string} any valid password. See [`url.password`][]. + * `host` {string} any valid host. See [`url.host`][]. + * `port` {string|number} any valid port. See [`url.port`][]. + * `query` {string} is a string representing the query. + It gets parsed into a valid [`URLSearchParams`][] and used inside of + the [`url.href`][] and [`url.search`][]. + * `path` {string[]} unlike [`url.pathname`][] it is not a string, + but rather an array representing the path. + * `fragment` {string} represents the fragment (part after `#`). + +Creates a new `URL` instance based on the input object. + +```js +const myURL = URL.from({ + protocol: 'http', + username: 'root', + password: '1234', + port: '3000', + host: 'localhost', + path: ['main'], + fragment: 'app', + query: 'el=%3Cdiv%20%2F%3E', +}); +// -> "http://root:1234@localhost:3000/main?el=%3Cdiv%20%2F%3E#app" +``` + +All properties are optional in case you want to build your `URL` object +gradually (note that you have to pass an empty object in this case anyway) + +```js +const myURL = URL.from({}); +// -> ":" +``` + #### url.hash * {string} @@ -1323,6 +1362,12 @@ console.log(myURL.origin); [`url.format()`]: #url_url_format_urlobject [`url.href`]: #url_url_href [`url.parse()`]: #url_url_parse_urlstring_parsequerystring_slashesdenotehost +[`url.protocol`]: #url_url_protocol +[`url.username`]: #url_url_username +[`url.password`]: #url_url_password +[`url.host`]: #url_url_host +[`url.port`]: #url_url_port +[`url.pathname`]: #url_url_pathname [`url.search`]: #url_url_search [`url.toJSON()`]: #url_url_tojson [`url.toString()`]: #url_url_tostring diff --git a/lib/internal/url.js b/lib/internal/url.js index a920511df489dc..17bbb300e9ac95 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -320,6 +320,65 @@ class URL { onParseError); } + static from(input) { + if (!input || typeof input !== 'object') + throw new ERR_INVALID_ARG_TYPE('input', 'Object', input); + + const { + username = '', + password = '', + host = null, + port = null, + query = null, + path = [], + fragment = null, + } = input; + const protocol = `${input.protocol || ''}:`; + + let flags = 0; + + if ( + protocol === 'file:' || + protocol === 'https:' || + protocol === 'wss:' || + protocol === 'http:' || + protocol === 'ftp:' || + protocol === 'ws:' || + protocol === 'gopher:' + ) + flags |= URL_FLAGS_SPECIAL; + else if (!host) + flags |= URL_FLAGS_CANNOT_BE_BASE; + + if (path.length) + flags |= URL_FLAGS_HAS_PATH; + if (host) + flags |= URL_FLAGS_HAS_HOST; + if (query) + flags |= URL_FLAGS_HAS_QUERY; + if (username) + flags |= URL_FLAGS_HAS_USERNAME; + if (password) + flags |= URL_FLAGS_HAS_PASSWORD; + if (fragment) + flags |= URL_FLAGS_HAS_FRAGMENT; + + // TODO: maybe don't need to support this since it was used for + // the host setter hack at src/node_url.cc:1816 + if (port == '-1') // eslint-disable-line eqeqeq + flags |= URL_FLAGS_IS_DEFAULT_SCHEME_PORT; + + const self = Object.create(URL.prototype); + self[context] = new URLContext(); + + onParseComplete.apply(self, [ + flags, protocol, username, password, + host, port, path, query, fragment + ]); + + return self; + } + get [special]() { return (this[context].flags & URL_FLAGS_SPECIAL) !== 0; } diff --git a/test/parallel/test-whatwg-url-from.js b/test/parallel/test-whatwg-url-from.js new file mode 100644 index 00000000000000..c693d901c08f43 --- /dev/null +++ b/test/parallel/test-whatwg-url-from.js @@ -0,0 +1,112 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const { URL } = require('url'); + +function t(expectedUrl, actualConfig) { + const url = URL.from(actualConfig); + assert.strictEqual(String(url), expectedUrl); +} + +assert.throws( + () => { URL.from(); }, + { + name: 'TypeError', + }, + 'when argument is ommited altogether' +); + +[ + ['undefined', undefined], + ['null', null], + ['false', false], + ['true', true], + ['0', 0], + ['42', 42], + ['NaN', NaN], + ['empty string', ''], + ['symbol', Symbol()], + ['class', class {}], + ['function', function() {}], + ['string', 'string'], +].forEach(([desc, arg]) => { + assert.throws( + () => { URL.from(arg); }, + { + name: 'TypeError', + }, + `when ${desc} passed` + ); +}); + +t(':', {}); + +t('https://nodejs.org', { + protocol: 'https', + host: 'nodejs.org' +}); + +t('https://root@site.com', { + protocol: 'https', + host: 'site.com', + username: 'root' +}); + +t('https://:1234@site.com', { + protocol: 'https', + host: 'site.com', + password: '1234' +}); + +t('https://root:1234@site.com', { + protocol: 'https', + host: 'site.com', + username: 'root', + password: '1234' +}); + +t('https://site.com?a=1&b=2', { + protocol: 'https', + host: 'site.com', + query: 'a=1&b=2' +}); + +t('https://site.com/one/two', { + protocol: 'https', + host: 'site.com', + path: ['one', 'two'] +}); + +t('http://localhost:3000', { + protocol: 'http', + host: 'localhost', + port: '3000', +}); + +t('http://localhost#fr', { + protocol: 'http', + host: 'localhost', + fragment: 'fr' +}); + +t('http://root:1234@localhost:3000/main?el=%3Cdiv%20%2F%3E#app', { + protocol: 'http', + username: 'root', + password: '1234', + port: '3000', + host: 'localhost', + path: ['main'], + fragment: 'app', + query: 'el=%3Cdiv%20%2F%3E' +}); + +t('https://nodejs.org', new Proxy({}, { + get(target, p) { + if (p === 'protocol') + return 'https'; + else if (p === 'host') + return 'nodejs.org'; + } +}));