normalize-url 8.1.1 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +48 -1
- package/index.js +208 -34
- package/package.json +6 -8
- package/readme.md +51 -3
package/index.d.ts
CHANGED
|
@@ -4,6 +4,26 @@ export type Options = {
|
|
|
4
4
|
*/
|
|
5
5
|
readonly defaultProtocol?: 'https' | 'http';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
Protocols to normalize in addition to the built-in ones (`https`, `http`, `file`, `data`).
|
|
9
|
+
|
|
10
|
+
Useful for HTTP-like custom protocols such as Electron schemes or app-specific protocols.
|
|
11
|
+
|
|
12
|
+
The protocols should be specified without `:`.
|
|
13
|
+
|
|
14
|
+
@default undefined
|
|
15
|
+
|
|
16
|
+
@example
|
|
17
|
+
```
|
|
18
|
+
normalizeUrl('sindre://www.sorhus.com', {customProtocols: ['sindre']});
|
|
19
|
+
//=> 'sindre://sorhus.com'
|
|
20
|
+
|
|
21
|
+
normalizeUrl('sindre://www.sorhus.com/foo/', {customProtocols: ['sindre']});
|
|
22
|
+
//=> 'sindre://sorhus.com/foo'
|
|
23
|
+
```
|
|
24
|
+
*/
|
|
25
|
+
readonly customProtocols?: readonly string[];
|
|
26
|
+
|
|
7
27
|
/**
|
|
8
28
|
Prepends `defaultProtocol` to the URL if it's protocol-relative.
|
|
9
29
|
|
|
@@ -147,6 +167,8 @@ export type Options = {
|
|
|
147
167
|
/**
|
|
148
168
|
Removes query parameters that matches any of the provided strings or regexes.
|
|
149
169
|
|
|
170
|
+
Global and sticky regex flags are stripped.
|
|
171
|
+
|
|
150
172
|
@default [/^utm_\w+/i]
|
|
151
173
|
|
|
152
174
|
@example
|
|
@@ -182,6 +204,8 @@ export type Options = {
|
|
|
182
204
|
|
|
183
205
|
__Note__: It overrides the `removeQueryParameters` option.
|
|
184
206
|
|
|
207
|
+
Global and sticky regex flags are stripped.
|
|
208
|
+
|
|
185
209
|
@default undefined
|
|
186
210
|
|
|
187
211
|
@example
|
|
@@ -233,8 +257,11 @@ export type Options = {
|
|
|
233
257
|
|
|
234
258
|
/**
|
|
235
259
|
Removes the default directory index file from path that matches any of the provided strings or regexes.
|
|
260
|
+
|
|
236
261
|
When `true`, the regex `/^index\.[a-z]+$/` is used.
|
|
237
262
|
|
|
263
|
+
Global and sticky regex flags are stripped.
|
|
264
|
+
|
|
238
265
|
@default false
|
|
239
266
|
|
|
240
267
|
@example
|
|
@@ -279,6 +306,26 @@ export type Options = {
|
|
|
279
306
|
*/
|
|
280
307
|
readonly sortQueryParameters?: boolean;
|
|
281
308
|
|
|
309
|
+
/**
|
|
310
|
+
Controls how query parameters with empty values are formatted.
|
|
311
|
+
|
|
312
|
+
- `'preserve'` - Keep the original format (`?key` stays `?key`, `?key=` stays `?key=`). If the same key appears with both formats (`?a&a=`), all instances will use the format without `=`.
|
|
313
|
+
- `'always'` - Always include `=` for empty values (`?key` becomes `?key=`)
|
|
314
|
+
- `'never'` - Never include `=` for empty values (`?key=` becomes `?key`)
|
|
315
|
+
|
|
316
|
+
@default 'preserve'
|
|
317
|
+
|
|
318
|
+
@example
|
|
319
|
+
```
|
|
320
|
+
normalizeUrl('www.sindresorhus.com?a&b=', {emptyQueryValue: 'always'});
|
|
321
|
+
//=> 'http://sindresorhus.com/?a=&b='
|
|
322
|
+
|
|
323
|
+
normalizeUrl('www.sindresorhus.com?a&b=', {emptyQueryValue: 'never'});
|
|
324
|
+
//=> 'http://sindresorhus.com/?a&b'
|
|
325
|
+
```
|
|
326
|
+
*/
|
|
327
|
+
readonly emptyQueryValue?: 'preserve' | 'always' | 'never';
|
|
328
|
+
|
|
282
329
|
/**
|
|
283
330
|
Removes the entire URL path, leaving only the domain.
|
|
284
331
|
|
|
@@ -322,7 +369,7 @@ export type Options = {
|
|
|
322
369
|
/**
|
|
323
370
|
[Normalize](https://en.wikipedia.org/wiki/URL_normalization) a URL.
|
|
324
371
|
|
|
325
|
-
URLs with custom protocols are not normalized and just passed through by default. Supported protocols are: `https`, `http`, `file`, and `data`.
|
|
372
|
+
URLs with custom protocols are not normalized and just passed through by default. Supported protocols are: `https`, `http`, `file`, and `data`. Use the `customProtocols` option to add support for additional protocols.
|
|
326
373
|
|
|
327
374
|
Human-friendly URLs with basic auth (for example, `user:password@sindresorhus.com`) are not handled because basic auth conflicts with custom protocols. [Basic auth URLs are also deprecated.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#access_using_credentials_in_the_url)
|
|
328
375
|
|
package/index.js
CHANGED
|
@@ -2,7 +2,23 @@
|
|
|
2
2
|
const DATA_URL_DEFAULT_MIME_TYPE = 'text/plain';
|
|
3
3
|
const DATA_URL_DEFAULT_CHARSET = 'us-ascii';
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const encodedReservedCharactersPattern = '%(?:3A|2F|3F|23|5B|5D|40|21|24|26|27|28|29|2A|2B|2C|3B|3D)';
|
|
6
|
+
const temporaryEncodedReservedTokenBase = '__normalize_url_encoded_reserved__';
|
|
7
|
+
const temporaryEncodedReservedTokenPattern = /__normalize_url_encoded_reserved__(\d+)__/g;
|
|
8
|
+
const hasEncodedReservedCharactersRegex = new RegExp(encodedReservedCharactersPattern, 'i');
|
|
9
|
+
const encodedReservedCharactersRegex = new RegExp(encodedReservedCharactersPattern, 'gi');
|
|
10
|
+
|
|
11
|
+
const testParameter = (name, filters) => Array.isArray(filters) && filters.some(filter => {
|
|
12
|
+
if (filter instanceof RegExp) {
|
|
13
|
+
if (filter.flags.includes('g') || filter.flags.includes('y')) {
|
|
14
|
+
return new RegExp(filter.source, filter.flags.replaceAll(/[gy]/g, '')).test(name);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return filter.test(name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return filter === name;
|
|
21
|
+
});
|
|
6
22
|
|
|
7
23
|
const supportedProtocols = new Set([
|
|
8
24
|
'https:',
|
|
@@ -10,18 +26,157 @@ const supportedProtocols = new Set([
|
|
|
10
26
|
'file:',
|
|
11
27
|
]);
|
|
12
28
|
|
|
13
|
-
const
|
|
29
|
+
const normalizeCustomProtocolOption = protocol => {
|
|
30
|
+
if (typeof protocol !== 'string') {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const normalizedProtocol = protocol.trim().toLowerCase().replace(/:$/, '');
|
|
35
|
+
return normalizedProtocol === '' ? undefined : `${normalizedProtocol}:`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const getCustomProtocol = urlString => {
|
|
14
39
|
try {
|
|
15
40
|
const {protocol} = new URL(urlString);
|
|
41
|
+
const hasAuthority = urlString.slice(0, protocol.length + 2).toLowerCase() === `${protocol}//`;
|
|
42
|
+
|
|
43
|
+
if (protocol.endsWith(':')
|
|
44
|
+
&& (!protocol.includes('.') || hasAuthority)
|
|
45
|
+
&& !supportedProtocols.has(protocol)) {
|
|
46
|
+
return protocol;
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
16
49
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
50
|
+
return undefined;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const decodeQueryKey = value => {
|
|
54
|
+
try {
|
|
55
|
+
return decodeURIComponent(value.replaceAll('+', '%20'));
|
|
20
56
|
} catch {
|
|
21
|
-
|
|
57
|
+
// Match URLSearchParams behavior for malformed percent-encoding.
|
|
58
|
+
return new URLSearchParams(`${value}=`).keys().next().value;
|
|
22
59
|
}
|
|
23
60
|
};
|
|
24
61
|
|
|
62
|
+
const getKeysWithoutEquals = search => {
|
|
63
|
+
const keys = new Set();
|
|
64
|
+
if (!search) {
|
|
65
|
+
return keys;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const part of search.slice(1).split('&')) {
|
|
69
|
+
if (part && !part.includes('=')) {
|
|
70
|
+
keys.add(decodeQueryKey(part));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return keys;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const getTemporaryEncodedReservedTokenPrefix = search => {
|
|
78
|
+
let decodedSearch = search;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
decodedSearch = decodeURIComponent(search);
|
|
82
|
+
} catch {
|
|
83
|
+
decodedSearch = new URLSearchParams(search).toString();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const getUsedTokenIndexes = value => {
|
|
87
|
+
const indexes = new Set();
|
|
88
|
+
|
|
89
|
+
for (const match of value.matchAll(temporaryEncodedReservedTokenPattern)) {
|
|
90
|
+
indexes.add(Number.parseInt(match[1], 10));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return indexes;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const usedTokenIndexes = getUsedTokenIndexes(search);
|
|
97
|
+
for (const tokenIndex of getUsedTokenIndexes(decodedSearch)) {
|
|
98
|
+
usedTokenIndexes.add(tokenIndex);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let tokenIndex = 0;
|
|
102
|
+
while (usedTokenIndexes.has(tokenIndex)) {
|
|
103
|
+
tokenIndex++;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return `${temporaryEncodedReservedTokenBase}${tokenIndex}__`;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const sortSearchParameters = (searchParameters, encodedReservedTokenRegex) => {
|
|
110
|
+
if (!encodedReservedTokenRegex) {
|
|
111
|
+
searchParameters.sort();
|
|
112
|
+
return searchParameters.toString();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const getSortableKey = key => key.replace(encodedReservedTokenRegex, (_, hexCode) => String.fromCodePoint(Number.parseInt(hexCode, 16)));
|
|
116
|
+
const entries = [...searchParameters.entries()];
|
|
117
|
+
entries.sort(([leftKey], [rightKey]) => {
|
|
118
|
+
const left = getSortableKey(leftKey);
|
|
119
|
+
const right = getSortableKey(rightKey);
|
|
120
|
+
return left < right ? -1 : (left > right ? 1 : 0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return new URLSearchParams(entries).toString();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const decodeReservedTokens = (value, encodedReservedTokenRegex) => {
|
|
127
|
+
if (!encodedReservedTokenRegex) {
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return value.replace(encodedReservedTokenRegex, (_, hexCode) => String.fromCodePoint(Number.parseInt(hexCode, 16)));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const normalizeEmptyQueryParameters = (search, emptyQueryValue, originalSearch) => {
|
|
135
|
+
const isAlways = emptyQueryValue === 'always';
|
|
136
|
+
const isNever = emptyQueryValue === 'never';
|
|
137
|
+
const keysWithoutEquals = (isAlways || isNever) ? undefined : getKeysWithoutEquals(originalSearch);
|
|
138
|
+
|
|
139
|
+
const normalizeKey = key => key.replaceAll('+', '%20');
|
|
140
|
+
const formatEmptyValue = normalizedKey => {
|
|
141
|
+
if (isAlways) {
|
|
142
|
+
return `${normalizedKey}=`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (isNever) {
|
|
146
|
+
return normalizedKey;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return keysWithoutEquals.has(decodeQueryKey(normalizedKey)) ? normalizedKey : `${normalizedKey}=`;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const normalizeParameter = parameter => {
|
|
153
|
+
const equalIndex = parameter.indexOf('=');
|
|
154
|
+
|
|
155
|
+
if (equalIndex === -1) {
|
|
156
|
+
// Normalize + to %20 (+ means space in query strings)
|
|
157
|
+
return formatEmptyValue(normalizeKey(parameter));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const key = parameter.slice(0, equalIndex);
|
|
161
|
+
const value = parameter.slice(equalIndex + 1);
|
|
162
|
+
|
|
163
|
+
if (value === '') {
|
|
164
|
+
if (key === '') {
|
|
165
|
+
return '=';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Normalize + to %20 (+ means space in query strings)
|
|
169
|
+
return formatEmptyValue(normalizeKey(key));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Normalize + to %20 in key.
|
|
173
|
+
return `${normalizeKey(key)}=${value}`;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const parameters = search.slice(1).split('&').filter(Boolean);
|
|
177
|
+
return parameters.length === 0 ? '' : `?${parameters.map(x => normalizeParameter(x)).join('&')}`;
|
|
178
|
+
};
|
|
179
|
+
|
|
25
180
|
const normalizeDataURL = (urlString, {stripHash}) => {
|
|
26
181
|
const match = /^data:(?<type>[^,]*?),(?<data>[^#]*?)(?:#(?<hash>.*))?$/.exec(urlString);
|
|
27
182
|
|
|
@@ -38,7 +193,7 @@ const normalizeDataURL = (urlString, {stripHash}) => {
|
|
|
38
193
|
}
|
|
39
194
|
|
|
40
195
|
// Lowercase MIME type
|
|
41
|
-
const mimeType = mediaType.shift()
|
|
196
|
+
const mimeType = mediaType.shift().toLowerCase();
|
|
42
197
|
const attributes = mediaType
|
|
43
198
|
.map(attribute => {
|
|
44
199
|
let [key, value = ''] = attribute.split('=').map(string => string.trim());
|
|
@@ -88,6 +243,7 @@ export default function normalizeUrl(urlString, options) {
|
|
|
88
243
|
sortQueryParameters: true,
|
|
89
244
|
removePath: false,
|
|
90
245
|
transformPath: false,
|
|
246
|
+
emptyQueryValue: 'preserve',
|
|
91
247
|
...options,
|
|
92
248
|
};
|
|
93
249
|
|
|
@@ -103,7 +259,13 @@ export default function normalizeUrl(urlString, options) {
|
|
|
103
259
|
return normalizeDataURL(urlString, options);
|
|
104
260
|
}
|
|
105
261
|
|
|
106
|
-
|
|
262
|
+
const customProtocols = Array.isArray(options.customProtocols) ? options.customProtocols : [];
|
|
263
|
+
const normalizedCustomProtocols = new Set(customProtocols
|
|
264
|
+
.map(protocol => normalizeCustomProtocolOption(protocol))
|
|
265
|
+
.filter(Boolean));
|
|
266
|
+
|
|
267
|
+
const customProtocol = getCustomProtocol(urlString);
|
|
268
|
+
if (customProtocol && !normalizedCustomProtocols.has(customProtocol)) {
|
|
107
269
|
return urlString;
|
|
108
270
|
}
|
|
109
271
|
|
|
@@ -111,7 +273,7 @@ export default function normalizeUrl(urlString, options) {
|
|
|
111
273
|
const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString);
|
|
112
274
|
|
|
113
275
|
// Prepend protocol
|
|
114
|
-
if (!isRelativeUrl) {
|
|
276
|
+
if (!isRelativeUrl && !customProtocol) {
|
|
115
277
|
urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol);
|
|
116
278
|
}
|
|
117
279
|
|
|
@@ -166,13 +328,13 @@ export default function normalizeUrl(urlString, options) {
|
|
|
166
328
|
const protocolAtIndex = match.index;
|
|
167
329
|
const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex);
|
|
168
330
|
|
|
169
|
-
result += intermediate.
|
|
331
|
+
result += intermediate.replaceAll(/\/{2,}/g, '/');
|
|
170
332
|
result += protocol;
|
|
171
333
|
lastIndex = protocolAtIndex + protocol.length;
|
|
172
334
|
}
|
|
173
335
|
|
|
174
|
-
const remnant = urlObject.pathname.slice(lastIndex
|
|
175
|
-
result += remnant.
|
|
336
|
+
const remnant = urlObject.pathname.slice(lastIndex);
|
|
337
|
+
result += remnant.replaceAll(/\/{2,}/g, '/');
|
|
176
338
|
|
|
177
339
|
urlObject.pathname = result;
|
|
178
340
|
}
|
|
@@ -180,7 +342,7 @@ export default function normalizeUrl(urlString, options) {
|
|
|
180
342
|
// Decode URI octets
|
|
181
343
|
if (urlObject.pathname) {
|
|
182
344
|
try {
|
|
183
|
-
urlObject.pathname = decodeURI(urlObject.pathname).
|
|
345
|
+
urlObject.pathname = decodeURI(urlObject.pathname).replaceAll('\\', '%5C');
|
|
184
346
|
} catch {}
|
|
185
347
|
}
|
|
186
348
|
|
|
@@ -225,49 +387,61 @@ export default function normalizeUrl(urlString, options) {
|
|
|
225
387
|
}
|
|
226
388
|
}
|
|
227
389
|
|
|
390
|
+
// Capture original query params format before any searchParams modifications
|
|
391
|
+
const originalSearch = urlObject.search;
|
|
392
|
+
let encodedReservedTokenRegex;
|
|
393
|
+
|
|
394
|
+
if (options.sortQueryParameters && hasEncodedReservedCharactersRegex.test(originalSearch)) {
|
|
395
|
+
const encodedReservedTokenPrefix = getTemporaryEncodedReservedTokenPrefix(originalSearch);
|
|
396
|
+
urlObject.search = originalSearch.replaceAll(encodedReservedCharactersRegex, match => `${encodedReservedTokenPrefix}${match.slice(1).toUpperCase()}`);
|
|
397
|
+
encodedReservedTokenRegex = new RegExp(`${encodedReservedTokenPrefix}([0-9A-F]{2})`, 'g');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const hasKeepQueryParameters = Array.isArray(options.keepQueryParameters);
|
|
401
|
+
const {searchParams} = urlObject;
|
|
402
|
+
|
|
228
403
|
// Remove query unwanted parameters
|
|
229
|
-
if (Array.isArray(options.removeQueryParameters)) {
|
|
404
|
+
if (!hasKeepQueryParameters && Array.isArray(options.removeQueryParameters) && options.removeQueryParameters.length > 0) {
|
|
230
405
|
// eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.
|
|
231
|
-
for (const key of [...
|
|
232
|
-
if (testParameter(key, options.removeQueryParameters)) {
|
|
233
|
-
|
|
406
|
+
for (const key of [...searchParams.keys()]) {
|
|
407
|
+
if (testParameter(decodeReservedTokens(key, encodedReservedTokenRegex), options.removeQueryParameters)) {
|
|
408
|
+
searchParams.delete(key);
|
|
234
409
|
}
|
|
235
410
|
}
|
|
236
411
|
}
|
|
237
412
|
|
|
238
|
-
if (!
|
|
413
|
+
if (!hasKeepQueryParameters && options.removeQueryParameters === true) {
|
|
239
414
|
urlObject.search = '';
|
|
240
415
|
}
|
|
241
416
|
|
|
242
417
|
// Keep wanted query parameters
|
|
243
|
-
if (
|
|
418
|
+
if (hasKeepQueryParameters && options.keepQueryParameters.length > 0) {
|
|
244
419
|
// eslint-disable-next-line unicorn/no-useless-spread -- We are intentionally spreading to get a copy.
|
|
245
|
-
for (const key of [...
|
|
246
|
-
if (!testParameter(key, options.keepQueryParameters)) {
|
|
247
|
-
|
|
420
|
+
for (const key of [...searchParams.keys()]) {
|
|
421
|
+
if (!testParameter(decodeReservedTokens(key, encodedReservedTokenRegex), options.keepQueryParameters)) {
|
|
422
|
+
searchParams.delete(key);
|
|
248
423
|
}
|
|
249
424
|
}
|
|
425
|
+
} else if (hasKeepQueryParameters) {
|
|
426
|
+
urlObject.search = '';
|
|
250
427
|
}
|
|
251
428
|
|
|
252
429
|
// Sort query parameters
|
|
253
430
|
if (options.sortQueryParameters) {
|
|
254
|
-
|
|
255
|
-
urlObject.searchParams.sort();
|
|
431
|
+
urlObject.search = sortSearchParameters(urlObject.searchParams, encodedReservedTokenRegex);
|
|
256
432
|
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
} catch {}
|
|
433
|
+
// Sorting and serializing encode the search parameters, so we need to decode them again.
|
|
434
|
+
// Protect &%#? and %2B from decoding (would break URL structure or change meaning) by double-encoding them first.
|
|
435
|
+
urlObject.search = decodeURIComponent(urlObject.search.replaceAll(/%(?:26|23|3f|25|2b)/gi, match => `%25${match.slice(1)}`));
|
|
261
436
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
for (const part of partsWithoutEquals) {
|
|
265
|
-
const decoded = decodeURIComponent(part);
|
|
266
|
-
// Only replace at word boundaries to avoid partial matches
|
|
267
|
-
urlObject.search = urlObject.search.replace(`?${decoded}=`, `?${decoded}`).replace(`&${decoded}=`, `&${decoded}`);
|
|
437
|
+
if (encodedReservedTokenRegex) {
|
|
438
|
+
urlObject.search = urlObject.search.replace(encodedReservedTokenRegex, '%$1');
|
|
268
439
|
}
|
|
269
440
|
}
|
|
270
441
|
|
|
442
|
+
// Normalize empty query parameter values
|
|
443
|
+
urlObject.search = normalizeEmptyQueryParameters(urlObject.search, options.emptyQueryValue, originalSearch);
|
|
444
|
+
|
|
271
445
|
if (options.removeTrailingSlash) {
|
|
272
446
|
urlObject.pathname = urlObject.pathname.replace(/\/$/, '');
|
|
273
447
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "normalize-url",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Normalize a URL",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "sindresorhus/normalize-url",
|
|
@@ -17,11 +17,10 @@
|
|
|
17
17
|
},
|
|
18
18
|
"sideEffects": false,
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=
|
|
20
|
+
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"
|
|
24
|
-
"test": "c8 ava && tsd"
|
|
23
|
+
"test": "xo && ava && tsd"
|
|
25
24
|
},
|
|
26
25
|
"files": [
|
|
27
26
|
"index.js",
|
|
@@ -43,10 +42,9 @@
|
|
|
43
42
|
"canonical"
|
|
44
43
|
],
|
|
45
44
|
"devDependencies": {
|
|
46
|
-
"ava": "^
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"xo": "^0.52.4"
|
|
45
|
+
"ava": "^6.4.1",
|
|
46
|
+
"tsd": "^0.33.0",
|
|
47
|
+
"xo": "^1.2.3"
|
|
50
48
|
},
|
|
51
49
|
"c8": {
|
|
52
50
|
"reporter": [
|
package/readme.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
Useful when you need to display, store, deduplicate, sort, compare, etc, URLs.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
> [!NOTE]
|
|
8
|
+
> This package does **not** do URL sanitization. [Garbage in, garbage out.](https://en.wikipedia.org/wiki/Garbage_in,_garbage_out) If you use this in a server context and accept URLs as user input, it's up to you to protect against invalid URLs, [path traversal attacks](https://owasp.org/www-community/attacks/Path_Traversal), etc.
|
|
8
9
|
|
|
9
10
|
## Install
|
|
10
11
|
|
|
@@ -28,7 +29,7 @@ normalizeUrl('//www.sindresorhus.com:80/../baz?b=bar&a=foo');
|
|
|
28
29
|
|
|
29
30
|
### normalizeUrl(url, options?)
|
|
30
31
|
|
|
31
|
-
URLs with custom protocols are not normalized and just passed through by default. Supported protocols are: `https`, `http`, `file`, and `data`.
|
|
32
|
+
URLs with custom protocols are not normalized and just passed through by default. Supported protocols are: `https`, `http`, `file`, and `data`. Use the [`customProtocols`](#customprotocols) option to add support for additional protocols.
|
|
32
33
|
|
|
33
34
|
Human-friendly URLs with basic auth (for example, `user:password@sindresorhus.com`) are not handled because basic auth conflicts with custom protocols. [Basic auth URLs are also deprecated.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#access_using_credentials_in_the_url)
|
|
34
35
|
|
|
@@ -48,6 +49,25 @@ Type: `string`\
|
|
|
48
49
|
Default: `'http'`\
|
|
49
50
|
Values: `'https' | 'http'`
|
|
50
51
|
|
|
52
|
+
##### customProtocols
|
|
53
|
+
|
|
54
|
+
Type: `string[]`\
|
|
55
|
+
Default: `undefined`
|
|
56
|
+
|
|
57
|
+
Protocols to normalize in addition to the built-in ones (`https`, `http`, `file`, `data`).
|
|
58
|
+
|
|
59
|
+
Useful for HTTP-like custom protocols such as Electron schemes or app-specific protocols.
|
|
60
|
+
|
|
61
|
+
The protocols should be specified without `:`.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
normalizeUrl('sindre://www.sorhus.com', {customProtocols: ['sindre']});
|
|
65
|
+
//=> 'sindre://sorhus.com'
|
|
66
|
+
|
|
67
|
+
normalizeUrl('sindre://www.sorhus.com/foo/', {customProtocols: ['sindre']});
|
|
68
|
+
//=> 'sindre://sorhus.com/foo'
|
|
69
|
+
```
|
|
70
|
+
|
|
51
71
|
##### normalizeProtocol
|
|
52
72
|
|
|
53
73
|
Type: `boolean`\
|
|
@@ -187,6 +207,8 @@ Default: `[/^utm_\w+/i]`
|
|
|
187
207
|
|
|
188
208
|
Remove query parameters that matches any of the provided strings or regexes.
|
|
189
209
|
|
|
210
|
+
Global and sticky regex flags are stripped.
|
|
211
|
+
|
|
190
212
|
```js
|
|
191
213
|
normalizeUrl('www.sindresorhus.com?foo=bar&ref=test_ref', {
|
|
192
214
|
removeQueryParameters: ['ref']
|
|
@@ -221,6 +243,8 @@ Keeps only query parameters that matches any of the provided strings or regexes.
|
|
|
221
243
|
|
|
222
244
|
**Note:** It overrides the `removeQueryParameters` option.
|
|
223
245
|
|
|
246
|
+
Global and sticky regex flags are stripped.
|
|
247
|
+
|
|
224
248
|
```js
|
|
225
249
|
normalizeUrl('https://sindresorhus.com?foo=bar&ref=unicorn', {
|
|
226
250
|
keepQueryParameters: ['ref']
|
|
@@ -268,7 +292,11 @@ normalizeUrl('https://sindresorhus.com/', {removeSingleSlash: false});
|
|
|
268
292
|
Type: `boolean | Array<RegExp | string>`\
|
|
269
293
|
Default: `false`
|
|
270
294
|
|
|
271
|
-
Removes the default directory index file from path that matches any of the provided strings or regexes.
|
|
295
|
+
Removes the default directory index file from path that matches any of the provided strings or regexes.
|
|
296
|
+
|
|
297
|
+
When `true`, the regex `/^index\.[a-z]+$/` is used.
|
|
298
|
+
|
|
299
|
+
Global and sticky regex flags are stripped.
|
|
272
300
|
|
|
273
301
|
```js
|
|
274
302
|
normalizeUrl('www.sindresorhus.com/foo/default.php', {
|
|
@@ -307,6 +335,26 @@ normalizeUrl('www.sindresorhus.com?b=two&a=one&c=three', {
|
|
|
307
335
|
//=> 'http://sindresorhus.com/?b=two&a=one&c=three'
|
|
308
336
|
```
|
|
309
337
|
|
|
338
|
+
##### emptyQueryValue
|
|
339
|
+
|
|
340
|
+
Type: `string`\
|
|
341
|
+
Default: `'preserve'`\
|
|
342
|
+
Values: `'preserve' | 'always' | 'never'`
|
|
343
|
+
|
|
344
|
+
Controls how query parameters with empty values are formatted.
|
|
345
|
+
|
|
346
|
+
- `'preserve'` - Keep the original format (`?key` stays `?key`, `?key=` stays `?key=`). If the same key appears with both formats (`?a&a=`), all instances will use the format without `=`.
|
|
347
|
+
- `'always'` - Always include `=` for empty values (`?key` becomes `?key=`)
|
|
348
|
+
- `'never'` - Never include `=` for empty values (`?key=` becomes `?key`)
|
|
349
|
+
|
|
350
|
+
```js
|
|
351
|
+
normalizeUrl('www.sindresorhus.com?a&b=', {emptyQueryValue: 'always'});
|
|
352
|
+
//=> 'http://sindresorhus.com/?a=&b='
|
|
353
|
+
|
|
354
|
+
normalizeUrl('www.sindresorhus.com?a&b=', {emptyQueryValue: 'never'});
|
|
355
|
+
//=> 'http://sindresorhus.com/?a&b'
|
|
356
|
+
```
|
|
357
|
+
|
|
310
358
|
##### removePath
|
|
311
359
|
|
|
312
360
|
Type: `boolean`\
|