Skip to content

Commit dfcfc06

Browse files
committed
Add removePath and transformPath option
Closes #188
1 parent 7be700f commit dfcfc06

File tree

4 files changed

+135
-0
lines changed

4 files changed

+135
-0
lines changed

index.d.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,45 @@ export type Options = {
278278
```
279279
*/
280280
readonly sortQueryParameters?: boolean;
281+
282+
/**
283+
Removes the entire URL path, leaving only the domain.
284+
285+
@default false
286+
287+
@example
288+
```
289+
normalizeUrl('https://example.com/path/to/page', {
290+
removePath: true
291+
});
292+
//=> 'https://example.com'
293+
```
294+
*/
295+
readonly removePath?: boolean;
296+
297+
/**
298+
Custom function to transform the URL path components.
299+
300+
The function receives an array of non-empty path components and should return a modified array.
301+
302+
@default false
303+
304+
@example
305+
```
306+
// Keep only the first path component
307+
normalizeUrl('https://example.com/api/v1/users', {
308+
transformPath: (pathComponents) => pathComponents.slice(0, 1)
309+
});
310+
//=> 'https://example.com/api'
311+
312+
// Remove specific components
313+
normalizeUrl('https://example.com/admin/users', {
314+
transformPath: (pathComponents) => pathComponents.filter(c => c !== 'admin')
315+
});
316+
//=> 'https://example.com/users'
317+
```
318+
*/
319+
readonly transformPath?: (pathComponents: string[]) => string[];
281320
};
282321

283322
/**

index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export default function normalizeUrl(urlString, options) {
8989
removeDirectoryIndex: false,
9090
removeExplicitPort: false,
9191
sortQueryParameters: true,
92+
removePath: false,
93+
transformPath: false,
9294
...options,
9395
};
9496

@@ -200,6 +202,18 @@ export default function normalizeUrl(urlString, options) {
200202
}
201203
}
202204

205+
// Remove path
206+
if (options.removePath) {
207+
urlObject.pathname = '/';
208+
}
209+
210+
// Transform path components
211+
if (options.transformPath && typeof options.transformPath === 'function') {
212+
const pathComponents = urlObject.pathname.split('/').filter(Boolean);
213+
const newComponents = options.transformPath(pathComponents);
214+
urlObject.pathname = newComponents?.length > 0 ? `/${newComponents.join('/')}` : '/';
215+
}
216+
203217
if (urlObject.hostname) {
204218
// Remove trailing dot
205219
urlObject.hostname = urlObject.hostname.replace(/\.$/, '');

readme.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,41 @@ normalizeUrl('www.sindresorhus.com?b=two&a=one&c=three', {
307307
//=> 'http://sindresorhus.com/?b=two&a=one&c=three'
308308
```
309309

310+
##### removePath
311+
312+
Type: `boolean`\
313+
Default: `false`
314+
315+
Removes the entire URL path, leaving only the domain.
316+
317+
```js
318+
normalizeUrl('https://example.com/path/to/page', {
319+
removePath: true
320+
});
321+
//=> 'https://example.com'
322+
```
323+
324+
##### transformPath
325+
326+
Type: `Function`\
327+
Default: `false`
328+
329+
Custom function to transform the URL path components. The function receives an array of non-empty path components and should return a modified array.
330+
331+
```js
332+
// Keep only the first path component
333+
normalizeUrl('https://example.com/api/v1/users', {
334+
transformPath: (pathComponents) => pathComponents.slice(0, 1)
335+
});
336+
//=> 'https://example.com/api'
337+
338+
// Remove specific components
339+
normalizeUrl('https://example.com/admin/users', {
340+
transformPath: (pathComponents) => pathComponents.filter(c => c !== 'admin')
341+
});
342+
//=> 'https://example.com/users'
343+
```
344+
310345
## Related
311346

312347
- [compare-urls](https://github.com/sindresorhus/compare-urls) - Compare URLs by first normalizing them

test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,53 @@ test('encoded backslashes do not get decoded', t => {
439439
t.is(normalizeUrl('https://foo.com/something\\else/great'), 'https://foo.com/something/else/great');
440440
});
441441

442+
test('removePath option', t => {
443+
// Boolean: Remove entire path
444+
t.is(normalizeUrl('https://example.com/path/to/page', {removePath: true}), 'https://example.com');
445+
t.is(normalizeUrl('https://example.com/path/to/page?query=1', {removePath: true}), 'https://example.com/?query=1');
446+
t.is(normalizeUrl('https://example.com/path/to/page#hash', {removePath: true}), 'https://example.com/#hash');
447+
t.is(normalizeUrl('https://example.com/', {removePath: true}), 'https://example.com');
448+
t.is(normalizeUrl('https://example.com', {removePath: true}), 'https://example.com');
449+
450+
// With other options
451+
t.is(normalizeUrl('https://example.com/path/', {removePath: true, removeTrailingSlash: true}), 'https://example.com');
452+
t.is(normalizeUrl('https://www.example.com/path', {removePath: true, stripWWW: true}), 'https://example.com');
453+
});
454+
455+
test('transformPath option', t => {
456+
// Function: Keep only first path component
457+
const keepFirst = pathComponents => pathComponents.slice(0, 1);
458+
t.is(normalizeUrl('https://example.com/api/v1/users', {transformPath: keepFirst}), 'https://example.com/api');
459+
t.is(normalizeUrl('https://example.com/path/to/page', {transformPath: keepFirst}), 'https://example.com/path');
460+
t.is(normalizeUrl('https://example.com/', {transformPath: keepFirst}), 'https://example.com');
461+
462+
// Function: Remove specific component
463+
const removeAdmin = pathComponents => pathComponents.filter(c => c !== 'admin');
464+
t.is(normalizeUrl('https://example.com/admin/users', {transformPath: removeAdmin}), 'https://example.com/users');
465+
t.is(normalizeUrl('https://example.com/path/admin/page', {transformPath: removeAdmin}), 'https://example.com/path/page');
466+
467+
// Function: Custom logic
468+
const customLogic = pathComponents => {
469+
if (pathComponents[0] === 'api') {
470+
return pathComponents.slice(0, 1); // Keep /api only
471+
}
472+
return []; // Remove everything else
473+
};
474+
t.is(normalizeUrl('https://example.com/api/v1/users', {transformPath: customLogic}), 'https://example.com/api');
475+
t.is(normalizeUrl('https://example.com/other/path', {transformPath: customLogic}), 'https://example.com');
476+
477+
// Edge cases
478+
t.is(normalizeUrl('https://example.com/path', {transformPath: () => []}), 'https://example.com');
479+
t.is(normalizeUrl('https://example.com/path', {transformPath: () => null}), 'https://example.com');
480+
t.is(normalizeUrl('https://example.com/path', {transformPath: () => undefined}), 'https://example.com');
481+
482+
// Combining with removePath (removePath should take precedence)
483+
t.is(normalizeUrl('https://example.com/path/to/page', {
484+
removePath: true,
485+
transformPath: pathComponents => pathComponents.slice(0, 2)
486+
}), 'https://example.com');
487+
});
488+
442489
test('path-like query strings without equals signs are preserved', t => {
443490
// Issue #193 - Path-like query strings should not get '=' appended
444491
t.is(normalizeUrl('https://example.com/index.php?/Some/Route/To/Path/12345'), 'https://example.com/index.php?/Some/Route/To/Path/12345');

0 commit comments

Comments
 (0)