diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c546fbd..b207add 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,12 +10,12 @@ jobs: fail-fast: false matrix: node-version: + - 16 - 14 - 12 - - 10 steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/.gitignore b/.gitignore index 748ccb6..a042fbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules yarn.lock .nyc_output +coverage diff --git a/index.d.ts b/index.d.ts index ca40f8f..b2ab84e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,249 +1,247 @@ -declare namespace normalizeUrl { - interface Options { - /** - @default 'http:' - */ - readonly defaultProtocol?: string; +export interface Options { + /** + @default 'http:' + */ + readonly defaultProtocol?: string; - /** - Prepends `defaultProtocol` to the URL if it's protocol-relative. + /** + Prepends `defaultProtocol` to the URL if it's protocol-relative. - @default true + @default true - @example - ``` - normalizeUrl('//sindresorhus.com:80/'); - //=> 'http://sindresorhus.com' + @example + ``` + normalizeUrl('//sindresorhus.com:80/'); + //=> 'http://sindresorhus.com' - normalizeUrl('//sindresorhus.com:80/', {normalizeProtocol: false}); - //=> '//sindresorhus.com' - ``` - */ - readonly normalizeProtocol?: boolean; + normalizeUrl('//sindresorhus.com:80/', {normalizeProtocol: false}); + //=> '//sindresorhus.com' + ``` + */ + readonly normalizeProtocol?: boolean; - /** - Normalizes `https:` URLs to `http:`. + /** + Normalizes `https:` URLs to `http:`. - @default false + @default false - @example - ``` - normalizeUrl('https://sindresorhus.com:80/'); - //=> 'https://sindresorhus.com' + @example + ``` + normalizeUrl('https://sindresorhus.com:80/'); + //=> 'https://sindresorhus.com' - normalizeUrl('https://sindresorhus.com:80/', {forceHttp: true}); - //=> 'http://sindresorhus.com' - ``` - */ - readonly forceHttp?: boolean; + normalizeUrl('https://sindresorhus.com:80/', {forceHttp: true}); + //=> 'http://sindresorhus.com' + ``` + */ + readonly forceHttp?: boolean; - /** - Normalizes `http:` URLs to `https:`. + /** + Normalizes `http:` URLs to `https:`. - This option can't be used with the `forceHttp` option at the same time. + This option can't be used with the `forceHttp` option at the same time. - @default false + @default false - @example - ``` - normalizeUrl('https://sindresorhus.com:80/'); - //=> 'https://sindresorhus.com' + @example + ``` + normalizeUrl('https://sindresorhus.com:80/'); + //=> 'https://sindresorhus.com' - normalizeUrl('http://sindresorhus.com:80/', {forceHttps: true}); - //=> 'https://sindresorhus.com' - ``` - */ - readonly forceHttps?: boolean; + normalizeUrl('http://sindresorhus.com:80/', {forceHttps: true}); + //=> 'https://sindresorhus.com' + ``` + */ + readonly forceHttps?: boolean; - /** - Strip the [authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) part of a URL. + /** + Strip the [authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) part of a URL. - @default true + @default true - @example - ``` - normalizeUrl('user:password@sindresorhus.com'); - //=> 'https://sindresorhus.com' + @example + ``` + normalizeUrl('user:password@sindresorhus.com'); + //=> 'https://sindresorhus.com' - normalizeUrl('user:password@sindresorhus.com', {stripAuthentication: false}); - //=> 'https://user:password@sindresorhus.com' - ``` - */ - readonly stripAuthentication?: boolean; + normalizeUrl('user:password@sindresorhus.com', {stripAuthentication: false}); + //=> 'https://user:password@sindresorhus.com' + ``` + */ + readonly stripAuthentication?: boolean; - /** - Removes hash from the URL. - - @default false + /** + Removes hash from the URL. - @example - ``` - normalizeUrl('sindresorhus.com/about.html#contact'); - //=> 'http://sindresorhus.com/about.html#contact' + @default false - normalizeUrl('sindresorhus.com/about.html#contact', {stripHash: true}); - //=> 'http://sindresorhus.com/about.html' - ``` - */ - readonly stripHash?: boolean; + @example + ``` + normalizeUrl('sindresorhus.com/about.html#contact'); + //=> 'http://sindresorhus.com/about.html#contact' - /** - Removes HTTP(S) protocol from an URL `http://sindresorhus.com` → `sindresorhus.com`. + normalizeUrl('sindresorhus.com/about.html#contact', {stripHash: true}); + //=> 'http://sindresorhus.com/about.html' + ``` + */ + readonly stripHash?: boolean; - @default false + /** + Removes HTTP(S) protocol from an URL `http://sindresorhus.com` → `sindresorhus.com`. - @example - ``` - normalizeUrl('https://sindresorhus.com'); - //=> 'https://sindresorhus.com' + @default false - normalizeUrl('sindresorhus.com', {stripProtocol: true}); - //=> 'sindresorhus.com' - ``` - */ - readonly stripProtocol?: boolean; + @example + ``` + normalizeUrl('https://sindresorhus.com'); + //=> 'https://sindresorhus.com' - /** - Strip the [text fragment](https://web.dev/text-fragments/) part of the URL + normalizeUrl('sindresorhus.com', {stripProtocol: true}); + //=> 'sindresorhus.com' + ``` + */ + readonly stripProtocol?: boolean; - __Note:__ The text fragment will always be removed if the `stripHash` option is set to `true`, as the hash contains the text fragment. + /** + Strip the [text fragment](https://web.dev/text-fragments/) part of the URL - @default true + __Note:__ The text fragment will always be removed if the `stripHash` option is set to `true`, as the hash contains the text fragment. - @example - ``` - normalizeUrl('http://sindresorhus.com/about.html#:~:text=hello'); - //=> 'http://sindresorhus.com/about.html#' + @default true - normalizeUrl('http://sindresorhus.com/about.html#section:~:text=hello'); - //=> 'http://sindresorhus.com/about.html#section' + @example + ``` + normalizeUrl('http://sindresorhus.com/about.html#:~:text=hello'); + //=> 'http://sindresorhus.com/about.html#' - normalizeUrl('http://sindresorhus.com/about.html#:~:text=hello', {stripTextFragment: false}); - //=> 'http://sindresorhus.com/about.html#:~:text=hello' + normalizeUrl('http://sindresorhus.com/about.html#section:~:text=hello'); + //=> 'http://sindresorhus.com/about.html#section' - normalizeUrl('http://sindresorhus.com/about.html#section:~:text=hello', {stripTextFragment: false}); - //=> 'http://sindresorhus.com/about.html#section:~:text=hello' - ``` - */ - readonly stripTextFragment?: boolean; - - /** - Removes `www.` from the URL. + normalizeUrl('http://sindresorhus.com/about.html#:~:text=hello', {stripTextFragment: false}); + //=> 'http://sindresorhus.com/about.html#:~:text=hello' - @default true + normalizeUrl('http://sindresorhus.com/about.html#section:~:text=hello', {stripTextFragment: false}); + //=> 'http://sindresorhus.com/about.html#section:~:text=hello' + ``` + */ + readonly stripTextFragment?: boolean; + + /** + Removes `www.` from the URL. - @example - ``` - normalizeUrl('http://www.sindresorhus.com'); - //=> 'http://sindresorhus.com' + @default true - normalizeUrl('http://www.sindresorhus.com', {stripWWW: false}); - //=> 'http://www.sindresorhus.com' - ``` - */ - readonly stripWWW?: boolean; - - /** - Removes query parameters that matches any of the provided strings or regexes. - - @default [/^utm_\w+/i] - - @example - ``` - normalizeUrl('www.sindresorhus.com?foo=bar&ref=test_ref', { - removeQueryParameters: ['ref'] - }); - //=> 'http://sindresorhus.com/?foo=bar' - ``` - - If a boolean is provided, `true` will remove all the query parameters. - - ``` - normalizeUrl('www.sindresorhus.com?foo=bar', { - removeQueryParameters: true - }); - //=> 'http://sindresorhus.com' - ``` - - `false` will not remove any query parameter. - - ``` - normalizeUrl('www.sindresorhus.com?foo=bar&utm_medium=test&ref=test_ref', { - removeQueryParameters: false - }); - //=> 'http://www.sindresorhus.com/?foo=bar&ref=test_ref&utm_medium=test' - ``` - */ - readonly removeQueryParameters?: ReadonlyArray | boolean; - - /** - Removes trailing slash. - - __Note__: Trailing slash is always removed if the URL doesn't have a pathname unless the `removeSingleSlash` option is set to `false`. - - @default true - - @example - ``` - normalizeUrl('http://sindresorhus.com/redirect/'); - //=> 'http://sindresorhus.com/redirect' - - normalizeUrl('http://sindresorhus.com/redirect/', {removeTrailingSlash: false}); - //=> 'http://sindresorhus.com/redirect/' - - normalizeUrl('http://sindresorhus.com/', {removeTrailingSlash: false}); - //=> 'http://sindresorhus.com' - ``` - */ - readonly removeTrailingSlash?: boolean; - - /** - Remove a sole `/` pathname in the output. This option is independant of `removeTrailingSlash`. - - @default true - - @example - ``` - normalizeUrl('https://sindresorhus.com/'); - //=> 'https://sindresorhus.com' - - normalizeUrl('https://sindresorhus.com/', {removeSingleSlash: false}); - //=> 'https://sindresorhus.com/' - ``` - */ - readonly removeSingleSlash?: boolean; - - /** - Removes the default directory index file from path that matches any of the provided strings or regexes. - When `true`, the regex `/^index\.[a-z]+$/` is used. - - @default false - - @example - ``` - normalizeUrl('www.sindresorhus.com/foo/default.php', { - removeDirectoryIndex: [/^default\.[a-z]+$/] - }); - //=> 'http://sindresorhus.com/foo' - ``` - */ - readonly removeDirectoryIndex?: ReadonlyArray; - - /** - Sorts the query parameters alphabetically by key. - - @default true - - @example - ``` - normalizeUrl('www.sindresorhus.com?b=two&a=one&c=three', { - sortQueryParameters: false - }); - //=> 'http://sindresorhus.com/?b=two&a=one&c=three' - ``` - */ - readonly sortQueryParameters?: boolean; - } + @example + ``` + normalizeUrl('http://www.sindresorhus.com'); + //=> 'http://sindresorhus.com' + + normalizeUrl('http://www.sindresorhus.com', {stripWWW: false}); + //=> 'http://www.sindresorhus.com' + ``` + */ + readonly stripWWW?: boolean; + + /** + Removes query parameters that matches any of the provided strings or regexes. + + @default [/^utm_\w+/i] + + @example + ``` + normalizeUrl('www.sindresorhus.com?foo=bar&ref=test_ref', { + removeQueryParameters: ['ref'] + }); + //=> 'http://sindresorhus.com/?foo=bar' + ``` + + If a boolean is provided, `true` will remove all the query parameters. + + ``` + normalizeUrl('www.sindresorhus.com?foo=bar', { + removeQueryParameters: true + }); + //=> 'http://sindresorhus.com' + ``` + + `false` will not remove any query parameter. + + ``` + normalizeUrl('www.sindresorhus.com?foo=bar&utm_medium=test&ref=test_ref', { + removeQueryParameters: false + }); + //=> 'http://www.sindresorhus.com/?foo=bar&ref=test_ref&utm_medium=test' + ``` + */ + readonly removeQueryParameters?: ReadonlyArray | boolean; + + /** + Removes trailing slash. + + __Note__: Trailing slash is always removed if the URL doesn't have a pathname unless the `removeSingleSlash` option is set to `false`. + + @default true + + @example + ``` + normalizeUrl('http://sindresorhus.com/redirect/'); + //=> 'http://sindresorhus.com/redirect' + + normalizeUrl('http://sindresorhus.com/redirect/', {removeTrailingSlash: false}); + //=> 'http://sindresorhus.com/redirect/' + + normalizeUrl('http://sindresorhus.com/', {removeTrailingSlash: false}); + //=> 'http://sindresorhus.com' + ``` + */ + readonly removeTrailingSlash?: boolean; + + /** + Remove a sole `/` pathname in the output. This option is independant of `removeTrailingSlash`. + + @default true + + @example + ``` + normalizeUrl('https://sindresorhus.com/'); + //=> 'https://sindresorhus.com' + + normalizeUrl('https://sindresorhus.com/', {removeSingleSlash: false}); + //=> 'https://sindresorhus.com/' + ``` + */ + readonly removeSingleSlash?: boolean; + + /** + Removes the default directory index file from path that matches any of the provided strings or regexes. + When `true`, the regex `/^index\.[a-z]+$/` is used. + + @default false + + @example + ``` + normalizeUrl('www.sindresorhus.com/foo/default.php', { + removeDirectoryIndex: [/^default\.[a-z]+$/] + }); + //=> 'http://sindresorhus.com/foo' + ``` + */ + readonly removeDirectoryIndex?: ReadonlyArray; + + /** + Sorts the query parameters alphabetically by key. + + @default true + + @example + ``` + normalizeUrl('www.sindresorhus.com?b=two&a=one&c=three', { + sortQueryParameters: false + }); + //=> 'http://sindresorhus.com/?b=two&a=one&c=three' + ``` + */ + readonly sortQueryParameters?: boolean; } /** @@ -253,7 +251,7 @@ declare namespace normalizeUrl { @example ``` -import normalizeUrl = require('normalize-url'); +import normalizeUrl from 'normalize-url'; normalizeUrl('sindresorhus.com'); //=> 'http://sindresorhus.com' @@ -262,6 +260,4 @@ normalizeUrl('//www.sindresorhus.com:80/../baz?b=bar&a=foo'); //=> 'http://sindresorhus.com/baz?a=foo&b=bar' ``` */ -declare function normalizeUrl(url: string, options?: normalizeUrl.Options): string; - -export = normalizeUrl; +export default function normalizeUrl(url: string, options?: Options): string; diff --git a/index.js b/index.js index c9340ab..3a879d4 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,3 @@ -'use strict'; - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs const DATA_URL_DEFAULT_MIME_TYPE = 'text/plain'; const DATA_URL_DEFAULT_CHARSET = 'us-ascii'; @@ -52,14 +50,14 @@ const normalizeDataURL = (urlString, {stripHash}) => { normalizedMediaType.push('base64'); } - if (normalizedMediaType.length !== 0 || (mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE)) { + if (normalizedMediaType.length > 0 || (mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE)) { normalizedMediaType.unshift(mimeType); } return `data:${normalizedMediaType.join(';')},${isBase64 ? data.trim() : data}${hash ? `#${hash}` : ''}`; }; -const normalizeUrl = (urlString, options) => { +export default function normalizeUrl(urlString, options) { options = { defaultProtocol: 'http:', normalizeProtocol: true, @@ -96,43 +94,43 @@ const normalizeUrl = (urlString, options) => { urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol); } - const urlObj = new URL(urlString); + const urlObject = new URL(urlString); if (options.forceHttp && options.forceHttps) { throw new Error('The `forceHttp` and `forceHttps` options cannot be used together'); } - if (options.forceHttp && urlObj.protocol === 'https:') { - urlObj.protocol = 'http:'; + if (options.forceHttp && urlObject.protocol === 'https:') { + urlObject.protocol = 'http:'; } - if (options.forceHttps && urlObj.protocol === 'http:') { - urlObj.protocol = 'https:'; + if (options.forceHttps && urlObject.protocol === 'http:') { + urlObject.protocol = 'https:'; } // Remove auth if (options.stripAuthentication) { - urlObj.username = ''; - urlObj.password = ''; + urlObject.username = ''; + urlObject.password = ''; } // Remove hash if (options.stripHash) { - urlObj.hash = ''; + urlObject.hash = ''; } else if (options.stripTextFragment) { - urlObj.hash = urlObj.hash.replace(/#?:~:text.*?$/i, ''); + urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, ''); } // Remove duplicate slashes if not preceded by a protocol - if (urlObj.pathname) { - urlObj.pathname = urlObj.pathname.replace(/(? { } if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) { - let pathComponents = urlObj.pathname.split('/'); + let pathComponents = urlObject.pathname.split('/'); const lastComponent = pathComponents[pathComponents.length - 1]; if (testParameter(lastComponent, options.removeDirectoryIndex)) { - pathComponents = pathComponents.slice(0, pathComponents.length - 1); - urlObj.pathname = pathComponents.slice(1).join('/') + '/'; + pathComponents = pathComponents.slice(0, -1); + urlObject.pathname = pathComponents.slice(1).join('/') + '/'; } } - if (urlObj.hostname) { + if (urlObject.hostname) { // Remove trailing dot - urlObj.hostname = urlObj.hostname.replace(/\.$/, ''); + urlObject.hostname = urlObject.hostname.replace(/\.$/, ''); // Remove `www.` - if (options.stripWWW && /^www\.(?!www\.)(?:[a-z\-\d]{1,63})\.(?:[a-z.\-\d]{2,63})$/.test(urlObj.hostname)) { + if (options.stripWWW && /^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(urlObject.hostname)) { // Each label should be max 63 at length (min: 1). // Source: https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names // Each TLD should be up to 63 characters long (min: 2). // It is technically possible to have a single character TLD, but none currently exist. - urlObj.hostname = urlObj.hostname.replace(/^www\./, ''); + urlObject.hostname = urlObject.hostname.replace(/^www\./, ''); } } // Remove query unwanted parameters if (Array.isArray(options.removeQueryParameters)) { - for (const key of [...urlObj.searchParams.keys()]) { + for (const key of [...urlObject.searchParams.keys()]) { if (testParameter(key, options.removeQueryParameters)) { - urlObj.searchParams.delete(key); + urlObject.searchParams.delete(key); } } } if (options.removeQueryParameters === true) { - urlObj.search = ''; + urlObject.search = ''; } // Sort query parameters if (options.sortQueryParameters) { - urlObj.searchParams.sort(); + urlObject.searchParams.sort(); } if (options.removeTrailingSlash) { - urlObj.pathname = urlObj.pathname.replace(/\/$/, ''); + urlObject.pathname = urlObject.pathname.replace(/\/$/, ''); } const oldUrlString = urlString; // Take advantage of many of the Node `url` normalizations - urlString = urlObj.toString(); + urlString = urlObject.toString(); - if (!options.removeSingleSlash && urlObj.pathname === '/' && !oldUrlString.endsWith('/') && urlObj.hash === '') { + if (!options.removeSingleSlash && urlObject.pathname === '/' && !oldUrlString.endsWith('/') && urlObject.hash === '') { urlString = urlString.replace(/\/$/, ''); } // Remove ending `/` unless removeSingleSlash is false - if ((options.removeTrailingSlash || urlObj.pathname === '/') && urlObj.hash === '' && options.removeSingleSlash) { + if ((options.removeTrailingSlash || urlObject.pathname === '/') && urlObject.hash === '' && options.removeSingleSlash) { urlString = urlString.replace(/\/$/, ''); } @@ -211,6 +209,4 @@ const normalizeUrl = (urlString, options) => { } return urlString; -}; - -module.exports = normalizeUrl; +} diff --git a/index.test-d.ts b/index.test-d.ts index 9326b37..f0175ff 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,5 +1,5 @@ import {expectType} from 'tsd'; -import normalizeUrl = require('.'); +import normalizeUrl from './index.js'; expectType(normalizeUrl('sindresorhus.com')); expectType(normalizeUrl('HTTP://xn--xample-hva.com:80/?b=bar&a=foo')); diff --git a/package.json b/package.json index 2670e4a..4326c5c 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "email": "sindresorhus@gmail.com", "url": "https://sindresorhus.com" }, + "type": "module", + "exports": "./index.js", "engines": { - "node": ">=10" + "node": ">=12.20" }, "scripts": { "test": "xo && nyc ava && tsd" @@ -36,10 +38,10 @@ "canonical" ], "devDependencies": { - "ava": "^2.4.0", - "nyc": "^15.0.0", - "tsd": "^0.11.0", - "xo": "^0.25.3" + "ava": "^3.15.0", + "nyc": "^15.1.0", + "tsd": "^0.17.0", + "xo": "^0.40.3" }, "nyc": { "reporter": [ diff --git a/readme.md b/readme.md index 4b29b29..f99f4f4 100644 --- a/readme.md +++ b/readme.md @@ -15,7 +15,7 @@ $ npm install normalize-url ## Usage ```js -const normalizeUrl = require('normalize-url'); +import normalizeUrl from 'normalize-url'; normalizeUrl('sindresorhus.com'); //=> 'http://sindresorhus.com' diff --git a/test.js b/test.js index 4aa6822..bb950b3 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,5 @@ import test from 'ava'; -import normalizeUrl from '.'; +import normalizeUrl from './index.js'; test('main', t => { t.is(normalizeUrl('sindresorhus.com'), 'http://sindresorhus.com'); @@ -152,7 +152,9 @@ test('forceHttp option', t => { test('forceHttp option with forceHttps', t => { t.throws(() => { normalizeUrl('https://www.sindresorhus.com', {forceHttp: true, forceHttps: true}); - }, 'The `forceHttp` and `forceHttps` options cannot be used together'); + }, { + message: 'The `forceHttp` and `forceHttps` options cannot be used together' + }); }); test('forceHttps option', t => { @@ -266,15 +268,21 @@ test('sortQueryParameters option', t => { test('invalid urls', t => { t.throws(() => { normalizeUrl('http://'); - }, 'Invalid URL: http://'); + }, { + message: 'Invalid URL' + }); t.throws(() => { normalizeUrl('/'); - }, 'Invalid URL: /'); + }, { + message: 'Invalid URL' + }); t.throws(() => { normalizeUrl('/relative/path/'); - }, 'Invalid URL: /relative/path/'); + }, { + message: 'Invalid URL' + }); }); test('remove duplicate pathname slashes', t => { @@ -301,7 +309,9 @@ test('remove duplicate pathname slashes', t => { test('data URL', t => { // Invalid URL. - t.throws(() => normalizeUrl('data:'), 'Invalid URL: data:'); + t.throws(() => normalizeUrl('data:'), { + message: 'Invalid URL: data:' + }); // Strip default MIME type t.is(normalizeUrl('data:text/plain,foo'), 'data:,foo'); @@ -364,7 +374,9 @@ test('prevents homograph attack', t => { test('view-source URL', t => { t.throws(() => { normalizeUrl('view-source:https://www.sindresorhus.com'); - }, '`view-source:` is not supported as it is a non-standard protocol'); + }, { + message: '`view-source:` is not supported as it is a non-standard protocol' + }); }); test('does not have exponential performance for data URLs', t => {