Repository: sindresorhus/normalize-url
Pull request #120 (2020-09-28 17:41:09)

Add `removeSingleSlash` option
Decided it's better to have a separate option than to overload a boolean type with non-boolean values.

Fixes: https://github.com/sindresorhus/normalize-url/issues/119

Edit: Updated to invert the option as it seems more in the spirit of this module.


Patch:
diff --git a/index.d.ts b/index.d.ts
index 2b73768..74f3403 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -137,7 +137,7 @@ declare namespace normalizeUrl {
 		/**
 		Removes trailing slash.
 
-		__Note__: Trailing slash is always removed if the URL doesn't have a pathname.
+		__Note__: Trailing slash is always removed if the URL doesn't have a pathname unless the `removeSingleSlash` option is set to `false`.
 
 		@default true
 
@@ -155,6 +155,22 @@ declare namespace normalizeUrl {
 		*/
 		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.
diff --git a/index.js b/index.js
index e615cf8..8d4f989 100644
--- a/index.js
+++ b/index.js
@@ -70,6 +70,7 @@ const normalizeUrl = (urlString, options) => {
 		stripWWW: true,
 		removeQueryParameters: [/^utm_\w+/i],
 		removeTrailingSlash: true,
+		removeSingleSlash: true,
 		removeDirectoryIndex: false,
 		sortQueryParameters: true,
 		...options
@@ -177,8 +178,8 @@ const normalizeUrl = (urlString, options) => {
 	// Take advantage of many of the Node `url` normalizations
 	urlString = urlObj.toString();
 
-	// Remove ending `/`
-	if ((options.removeTrailingSlash || urlObj.pathname === '/') && urlObj.hash === '') {
+	// Remove ending `/` unless removeSingleSlash is false
+	if ((options.removeTrailingSlash || urlObj.pathname === '/') && urlObj.hash === '' && options.removeSingleSlash) {
 		urlString = urlString.replace(/\/$/, '');
 	}
 
diff --git a/readme.md b/readme.md
index d09d3f6..5a03c80 100644
--- a/readme.md
+++ b/readme.md
@@ -171,7 +171,7 @@ Default: `true`
 
 Remove trailing slash.
 
-**Note:** Trailing slash is always removed if the URL doesn't have a pathname.
+**Note:** Trailing slash is always removed if the URL doesn't have a pathname unless the `removeSingleSlash` option is set to `false`.
 
 ```js
 normalizeUrl('http://sindresorhus.com/redirect/');
@@ -184,6 +184,22 @@ normalizeUrl('http://sindresorhus.com/', {removeTrailingSlash: false});
 //=> 'http://sindresorhus.com'
 ```
 
+##### removeSingleSlash
+
+Type: `boolean`\
+Default: `true`
+
+Remove a sole `/` pathname in the output. This option is independant of `removeTrailingSlash`.
+
+```js
+normalizeUrl('https://sindresorhus.com/');
+//=> 'https://sindresorhus.com'
+
+normalizeUrl('https://sindresorhus.com/', {removeSingleSlash: false});
+//=> 'https://sindresorhus.com/'
+```
+
+
 ##### removeDirectoryIndex
 
 Type: `boolean | Array<RegExp | string>`\


Test patch:
diff --git a/test.js b/test.js
index fc5b5c2..1d4fc08 100644
--- a/test.js
+++ b/test.js
@@ -124,6 +124,24 @@ test('removeTrailingSlash option', t => {
 	t.is(normalizeUrl('http://sindresorhus.com/redirect/'), 'http://sindresorhus.com/redirect');
 	t.is(normalizeUrl('http://sindresorhus.com/redirect/', options), 'http://sindresorhus.com/redirect/');
 	t.is(normalizeUrl('http://sindresorhus.com/#/', options), 'http://sindresorhus.com/#/');
+	t.is(normalizeUrl('http://sindresorhus.com/?unicorns=true'), 'http://sindresorhus.com/?unicorns=true');
+	t.is(normalizeUrl('http://sindresorhus.com/?unicorns=true', options), 'http://sindresorhus.com/?unicorns=true');
+});
+
+test('removeSingleSlash option', t => {
+	const options = {removeSingleSlash: false};
+	t.is(normalizeUrl('https://sindresorhus.com/', options), 'https://sindresorhus.com/');
+	t.is(normalizeUrl('https://sindresorhus.com/redirect/', options), 'https://sindresorhus.com/redirect');
+	t.is(normalizeUrl('https://sindresorhus.com/#/', options), 'https://sindresorhus.com/#/');
+	t.is(normalizeUrl('https://sindresorhus.com/?unicorns=true', options), 'https://sindresorhus.com/?unicorns=true');
+});
+
+test('removeSingleSlash option combined with removeTrailingSlash option', t => {
+	const options = {removeTrailingSlash: true, removeSingleSlash: false};
+	t.is(normalizeUrl('https://sindresorhus.com/', options), 'https://sindresorhus.com/');
+	t.is(normalizeUrl('https://sindresorhus.com/redirect/', options), 'https://sindresorhus.com/redirect');
+	t.is(normalizeUrl('https://sindresorhus.com/#/', options), 'https://sindresorhus.com/#/');
+	t.is(normalizeUrl('https://sindresorhus.com/?unicorns=true', options), 'https://sindresorhus.com/?unicorns=true');
 });
 
 test('removeDirectoryIndex option', t => {