Skip to content

Commit 92d0dbb

Browse files
authored
feat: provide normalizeUrl helper (#13539)
Provides people a way to normalize a raw URL that could contain SvelteKit-internal data. One use case would be that you want to use middleware in front of, but outside of SvelteKit Extracted from #13477
1 parent 0800da1 commit 92d0dbb

File tree

4 files changed

+129
-0
lines changed

4 files changed

+129
-0
lines changed

.changeset/tough-mangos-develop.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: provide `normalizeUrl` helper

packages/kit/src/exports/index.js

+54
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { HttpError, Redirect, ActionFailure } from '../runtime/control.js';
22
import { BROWSER, DEV } from 'esm-env';
3+
import {
4+
add_data_suffix,
5+
add_resolution_suffix,
6+
has_data_suffix,
7+
has_resolution_suffix,
8+
strip_data_suffix,
9+
strip_resolution_suffix
10+
} from '../runtime/pathname.js';
311

412
export { VERSION } from '../version.js';
513

@@ -207,3 +215,49 @@ export function fail(status, data) {
207215
export function isActionFailure(e) {
208216
return e instanceof ActionFailure;
209217
}
218+
219+
/**
220+
* Strips possible SvelteKit-internal suffixes and trailing slashes from the URL pathname.
221+
* Returns the normalized URL as well as a method for adding the potential suffix back
222+
* based on a new pathname (possibly including search) or URL.
223+
* ```js
224+
* import { normalizeUrl } from '@sveltejs/kit';
225+
*
226+
* const { url, denormalize } = normalizeUrl('/blog/post/__data.json');
227+
* console.log(url.pathname); // /blog/post
228+
* console.log(denormalize('/blog/post/a')); // /blog/post/a/__data.json
229+
* ```
230+
* @param {URL | string} url
231+
* @returns {{ url: URL, wasNormalized: boolean, denormalize: (url?: string | URL) => URL }}
232+
*/
233+
export function normalizeUrl(url) {
234+
url = new URL(url, 'http://internal');
235+
236+
const is_route_resolution = has_resolution_suffix(url.pathname);
237+
const is_data_request = has_data_suffix(url.pathname);
238+
const has_trailing_slash = url.pathname !== '/' && url.pathname.endsWith('/');
239+
240+
if (is_route_resolution) {
241+
url.pathname = strip_resolution_suffix(url.pathname);
242+
} else if (is_data_request) {
243+
url.pathname = strip_data_suffix(url.pathname);
244+
} else if (has_trailing_slash) {
245+
url.pathname = url.pathname.slice(0, -1);
246+
}
247+
248+
return {
249+
url,
250+
wasNormalized: is_data_request || is_route_resolution || has_trailing_slash,
251+
denormalize: (new_url = url) => {
252+
new_url = new URL(new_url, url);
253+
if (is_route_resolution) {
254+
new_url.pathname = add_resolution_suffix(new_url.pathname);
255+
} else if (is_data_request) {
256+
new_url.pathname = add_data_suffix(new_url.pathname);
257+
} else if (has_trailing_slash && !new_url.pathname.endsWith('/')) {
258+
new_url.pathname += '/';
259+
}
260+
return new_url;
261+
}
262+
};
263+
}
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { normalizeUrl } from './index.js';
2+
import { assert, describe, it } from 'vitest';
3+
4+
describe('normalizeUrl', () => {
5+
it('noop for regular url', () => {
6+
const original = new URL('http://example.com/foo/bar');
7+
const { url, wasNormalized, denormalize } = normalizeUrl(original);
8+
9+
assert.equal(wasNormalized, false);
10+
assert.equal(url.href, original.href);
11+
assert.equal(denormalize().href, original.href);
12+
assert.equal(denormalize('/baz').href, 'http://example.com/baz');
13+
assert.equal(
14+
denormalize('?some=query#hash').href,
15+
'http://example.com/foo/bar?some=query#hash'
16+
);
17+
assert.equal(denormalize('http://somethingelse.com/').href, 'http://somethingelse.com/');
18+
assert.equal(
19+
denormalize(new URL('http://somethingelse.com/')).href,
20+
'http://somethingelse.com/'
21+
);
22+
});
23+
24+
it('should normalize trailing slash', () => {
25+
const original = new URL('http://example.com/foo/bar/');
26+
const { url, wasNormalized, denormalize } = normalizeUrl(original);
27+
28+
assert.equal(wasNormalized, true);
29+
assert.equal(url.href, original.href.slice(0, -1));
30+
assert.equal(denormalize().href, original.href);
31+
assert.equal(denormalize('/baz').href, 'http://example.com/baz/');
32+
});
33+
34+
it('should normalize data request route', () => {
35+
const original = new URL('http://example.com/foo/__data.json');
36+
const { url, wasNormalized, denormalize } = normalizeUrl(original);
37+
38+
assert.equal(wasNormalized, true);
39+
assert.equal(url.href, 'http://example.com/foo');
40+
assert.equal(denormalize().href, original.href);
41+
assert.equal(denormalize('/baz').href, 'http://example.com/baz/__data.json');
42+
});
43+
44+
it('should normalize route request route', () => {
45+
const original = new URL('http://example.com/foo/__route.js');
46+
const { url, wasNormalized, denormalize } = normalizeUrl(original);
47+
48+
assert.equal(wasNormalized, true);
49+
assert.equal(url.href, 'http://example.com/foo');
50+
assert.equal(denormalize().href, original.href);
51+
assert.equal(denormalize('/baz').href, 'http://example.com/baz/__route.js');
52+
});
53+
});

packages/kit/types/index.d.ts

+17
Original file line numberDiff line numberDiff line change
@@ -2005,6 +2005,23 @@ declare module '@sveltejs/kit' {
20052005
* @param e The object to check.
20062006
* */
20072007
export function isActionFailure(e: unknown): e is ActionFailure;
2008+
/**
2009+
* Strips possible SvelteKit-internal suffixes and trailing slashes from the URL pathname.
2010+
* Returns the normalized URL as well as a method for adding the potential suffix back
2011+
* based on a new pathname (possibly including search) or URL.
2012+
* ```js
2013+
* import { normalizeUrl } from '@sveltejs/kit';
2014+
*
2015+
* const { url, denormalize } = normalizeUrl('/blog/post/__data.json');
2016+
* console.log(url.pathname); // /blog/post
2017+
* console.log(denormalize('/blog/post/a')); // /blog/post/a/__data.json
2018+
* ```
2019+
* */
2020+
export function normalizeUrl(url: URL | string): {
2021+
url: URL;
2022+
wasNormalized: boolean;
2023+
denormalize: (url?: string | URL) => URL;
2024+
};
20082025
export type LessThan<TNumber extends number, TArray extends any[] = []> = TNumber extends TArray["length"] ? TArray[number] : LessThan<TNumber, [...TArray, TArray["length"]]>;
20092026
export type NumericRange<TStart extends number, TEnd extends number> = Exclude<TEnd | LessThan<TEnd>, LessThan<TStart>>;
20102027
export const VERSION: string;

0 commit comments

Comments
 (0)