sv-router 0.7.2 → 0.8.1
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/package.json +20 -19
- package/src/RecursiveComponentTree.svelte +5 -8
- package/src/Router.svelte +2 -10
- package/src/actions.svelte.js +8 -3
- package/src/create-router.svelte.js +89 -22
- package/src/helpers/is-active.js +8 -4
- package/src/helpers/match-route.js +1 -0
- package/src/helpers/utils.js +22 -8
- package/src/index.d.ts +5 -2
- package/src/search-params.svelte.js +11 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Modern Svelte Routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -36,28 +36,29 @@
|
|
|
36
36
|
"esm-env": "^1.2.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@changesets/cli": "^2.29.
|
|
40
|
-
"@eslint/js": "^9.
|
|
41
|
-
"@sveltejs/vite-plugin-svelte": "^6.
|
|
42
|
-
"@testing-library/jest-dom": "^6.6.
|
|
39
|
+
"@changesets/cli": "^2.29.5",
|
|
40
|
+
"@eslint/js": "^9.32.0",
|
|
41
|
+
"@sveltejs/vite-plugin-svelte": "^6.1.0",
|
|
42
|
+
"@testing-library/jest-dom": "^6.6.4",
|
|
43
43
|
"@testing-library/svelte": "^5.2.8",
|
|
44
|
-
"@
|
|
45
|
-
"
|
|
46
|
-
"eslint
|
|
44
|
+
"@testing-library/user-event": "^14.6.1",
|
|
45
|
+
"@types/node": "^24.2.0",
|
|
46
|
+
"eslint": "^9.32.0",
|
|
47
|
+
"eslint-config-prettier": "^10.1.8",
|
|
47
48
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
48
|
-
"eslint-plugin-svelte": "^3.
|
|
49
|
-
"eslint-plugin-unicorn": "^
|
|
50
|
-
"globals": "^16.
|
|
51
|
-
"
|
|
52
|
-
"prettier": "^3.
|
|
53
|
-
"prettier-plugin-jsdoc": "^1.3.
|
|
49
|
+
"eslint-plugin-svelte": "^3.11.0",
|
|
50
|
+
"eslint-plugin-unicorn": "^60.0.0",
|
|
51
|
+
"globals": "^16.3.0",
|
|
52
|
+
"happy-dom": "^18.0.1",
|
|
53
|
+
"prettier": "^3.6.2",
|
|
54
|
+
"prettier-plugin-jsdoc": "^1.3.3",
|
|
54
55
|
"prettier-plugin-svelte": "^3.4.0",
|
|
55
|
-
"svelte-check": "^4.
|
|
56
|
+
"svelte-check": "^4.3.1",
|
|
56
57
|
"type-testing": "^0.2.0",
|
|
57
|
-
"typescript": "^5.
|
|
58
|
-
"typescript-eslint": "^8.
|
|
59
|
-
"vite": "^7.
|
|
60
|
-
"vitest": "^3.2.
|
|
58
|
+
"typescript": "^5.9.2",
|
|
59
|
+
"typescript-eslint": "^8.39.0",
|
|
60
|
+
"vite": "^7.1.0",
|
|
61
|
+
"vitest": "^3.2.4"
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
64
|
"svelte": "^5"
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { params } from './create-router.svelte.js';
|
|
3
2
|
import RecursiveComponentTree from './RecursiveComponentTree.svelte';
|
|
4
3
|
|
|
5
4
|
/** @type {{ tree: import('svelte').Component[] }} */
|
|
@@ -9,10 +8,8 @@
|
|
|
9
8
|
const restTree = $derived(tree.slice(1));
|
|
10
9
|
</script>
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
</FirstComponent>
|
|
18
|
-
{/key}
|
|
11
|
+
<FirstComponent>
|
|
12
|
+
{#if restTree.length > 0}
|
|
13
|
+
<RecursiveComponentTree tree={restTree}></RecursiveComponentTree>
|
|
14
|
+
{/if}
|
|
15
|
+
</FirstComponent>
|
package/src/Router.svelte
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { on } from 'svelte/events';
|
|
3
|
-
import {
|
|
4
|
-
import { join } from './helpers/utils.js';
|
|
3
|
+
import { componentTree, init, onGlobalClick, onNavigate } from './create-router.svelte.js';
|
|
5
4
|
import RecursiveComponentTree from './RecursiveComponentTree.svelte';
|
|
6
5
|
|
|
7
6
|
/** @type {{ base?: string }} */
|
|
8
7
|
let { base: basename } = $props();
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
base.name = (basename.startsWith('/') ? '' : '/') + basename;
|
|
12
|
-
const url = new URL(globalThis.location.href);
|
|
13
|
-
if (!url.pathname.startsWith(base.name)) {
|
|
14
|
-
url.pathname = join(base.name, url.pathname);
|
|
15
|
-
history.replaceState(history.state || {}, '', url.href);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
9
|
+
init(basename);
|
|
18
10
|
|
|
19
11
|
onNavigate();
|
|
20
12
|
|
package/src/actions.svelte.js
CHANGED
|
@@ -8,9 +8,14 @@ export function isActiveLink(node, { className = 'is-active', startsWith = false
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
$effect(() => {
|
|
11
|
-
let pathname
|
|
12
|
-
if (base.name) {
|
|
13
|
-
pathname =
|
|
11
|
+
let pathname;
|
|
12
|
+
if (base.name === '#') {
|
|
13
|
+
pathname = new URL(node.href).hash.slice(1);
|
|
14
|
+
} else {
|
|
15
|
+
pathname = new URL(node.href).pathname;
|
|
16
|
+
if (base.name) {
|
|
17
|
+
pathname = join(base.name, pathname);
|
|
18
|
+
}
|
|
14
19
|
}
|
|
15
20
|
const tokens = className.split(' ').filter(Boolean) ?? [];
|
|
16
21
|
if (startsWith ? location.pathname.startsWith(pathname) : location.pathname === pathname) {
|
|
@@ -14,23 +14,44 @@ import { syncSearchParams } from './search-params.svelte.js';
|
|
|
14
14
|
/** @type {import('./index.d.ts').Routes} */
|
|
15
15
|
let routes;
|
|
16
16
|
|
|
17
|
+
/** @type {{ name?: string }} */
|
|
18
|
+
export const base = {
|
|
19
|
+
name: undefined,
|
|
20
|
+
};
|
|
21
|
+
|
|
17
22
|
/** @type {{ value: import('svelte').Component[] }} */
|
|
18
23
|
export let componentTree = $state({ value: [] });
|
|
19
24
|
|
|
20
|
-
/** @type {{ value: Record<string, string> }} */
|
|
21
|
-
export let params = $state({ value: {} });
|
|
22
|
-
|
|
23
25
|
export let location = $state(updatedLocation());
|
|
24
26
|
|
|
27
|
+
/** @type {{ value: Record<string, string> }} */
|
|
28
|
+
let params = $state({ value: {} });
|
|
29
|
+
|
|
25
30
|
let meta = $state({ value: {} });
|
|
26
31
|
|
|
27
32
|
let navigationIndex = 0;
|
|
28
33
|
let pendingNavigationIndex = 0;
|
|
29
34
|
|
|
30
|
-
/** @
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
/** @param {string | undefined} basename */
|
|
36
|
+
export function init(basename) {
|
|
37
|
+
if (basename) {
|
|
38
|
+
const url = new URL(globalThis.location.toString());
|
|
39
|
+
if (basename === '#') {
|
|
40
|
+
base.name = '#';
|
|
41
|
+
if (!globalThis.location.href.includes('#')) {
|
|
42
|
+
url.hash = '/';
|
|
43
|
+
history.replaceState(history.state || {}, '', url.toString());
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
base.name = (basename.startsWith('/') ? '' : '/') + basename;
|
|
47
|
+
if (!url.pathname.startsWith(base.name)) {
|
|
48
|
+
url.pathname = join(base.name, url.pathname);
|
|
49
|
+
history.replaceState(history.state || {}, '', url.toString());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
Object.assign(location, updatedLocation());
|
|
54
|
+
}
|
|
34
55
|
|
|
35
56
|
/**
|
|
36
57
|
* @template {import('./index.d.ts').Routes} T
|
|
@@ -93,18 +114,39 @@ function navigate(path, options = {}) {
|
|
|
93
114
|
globalThis.history.go(path);
|
|
94
115
|
return;
|
|
95
116
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
117
|
+
|
|
118
|
+
path = constructPath(path, options.params);
|
|
99
119
|
if (options.search && !options.search.startsWith('?')) {
|
|
100
120
|
options.search = '?' + options.search;
|
|
101
121
|
}
|
|
102
|
-
if (options.hash && !options.hash.startsWith('#')) {
|
|
122
|
+
if (options.hash && !options.hash.startsWith('#') && base.name !== '#') {
|
|
103
123
|
options.hash = '#' + options.hash;
|
|
104
124
|
}
|
|
125
|
+
if (base.name === '#') {
|
|
126
|
+
path = new URL(path).hash;
|
|
127
|
+
}
|
|
105
128
|
onNavigate(path, options);
|
|
106
129
|
}
|
|
107
130
|
|
|
131
|
+
/** @param {string} [path] */
|
|
132
|
+
function getMatchPath(path) {
|
|
133
|
+
let matchPath = '';
|
|
134
|
+
|
|
135
|
+
if (path) {
|
|
136
|
+
matchPath = path;
|
|
137
|
+
} else if (base.name === '#') {
|
|
138
|
+
matchPath = globalThis.location.hash.slice(1);
|
|
139
|
+
} else {
|
|
140
|
+
matchPath = globalThis.location.pathname;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (base.name && matchPath.startsWith(base.name)) {
|
|
144
|
+
matchPath = matchPath.slice(base.name.length) || '/';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return stripBase(matchPath);
|
|
148
|
+
}
|
|
149
|
+
|
|
108
150
|
/**
|
|
109
151
|
* @param {string} [path]
|
|
110
152
|
* @param {import('./index.d.ts').NavigateOptions} options
|
|
@@ -117,21 +159,35 @@ export async function onNavigate(path, options = {}) {
|
|
|
117
159
|
navigationIndex++;
|
|
118
160
|
const currentNavigationIndex = navigationIndex;
|
|
119
161
|
|
|
120
|
-
|
|
162
|
+
let matchPath = getMatchPath(path);
|
|
121
163
|
const { match, layouts, hooks, meta: newMeta, params: newParams } = matchRoute(matchPath, routes);
|
|
122
164
|
|
|
123
|
-
|
|
165
|
+
let errorHooks = [];
|
|
166
|
+
for (const hook of hooks) {
|
|
124
167
|
try {
|
|
168
|
+
const { beforeLoad } = hook;
|
|
169
|
+
errorHooks.push(hook);
|
|
125
170
|
pendingNavigationIndex = currentNavigationIndex;
|
|
126
171
|
await beforeLoad?.({ pathname: matchPath, meta: newMeta, ...options });
|
|
127
|
-
} catch {
|
|
172
|
+
} catch (error) {
|
|
173
|
+
for (const { onError } of errorHooks) {
|
|
174
|
+
void onError?.(error, { pathname: matchPath, meta: newMeta, ...options });
|
|
175
|
+
}
|
|
128
176
|
return;
|
|
129
177
|
}
|
|
130
178
|
}
|
|
131
179
|
|
|
132
180
|
const fromBeforeLoadHook = new Error().stack?.includes('beforeLoad');
|
|
133
181
|
|
|
134
|
-
|
|
182
|
+
let routeComponents;
|
|
183
|
+
try {
|
|
184
|
+
routeComponents = await resolveRouteComponents(match ? [...layouts, match] : layouts);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
for (const { onError } of hooks) {
|
|
187
|
+
void onError?.(error, { pathname: matchPath, meta: newMeta, ...options });
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
135
191
|
if (
|
|
136
192
|
navigationIndex !== currentNavigationIndex ||
|
|
137
193
|
(fromBeforeLoadHook && pendingNavigationIndex + 1 !== currentNavigationIndex)
|
|
@@ -140,11 +196,20 @@ export async function onNavigate(path, options = {}) {
|
|
|
140
196
|
}
|
|
141
197
|
|
|
142
198
|
if (path) {
|
|
143
|
-
|
|
144
|
-
|
|
199
|
+
let url = new URL(globalThis.location.toString());
|
|
200
|
+
url.search = '';
|
|
201
|
+
if (options.search) url.search = options.search;
|
|
202
|
+
if (base.name === '#') {
|
|
203
|
+
url.hash = path;
|
|
204
|
+
} else {
|
|
205
|
+
if (options.hash) path += options.hash;
|
|
206
|
+
url.pathname = base.name ? join(base.name, path) : path;
|
|
207
|
+
}
|
|
145
208
|
const historyMethod = options.replace ? 'replaceState' : 'pushState';
|
|
146
|
-
|
|
147
|
-
|
|
209
|
+
globalThis.history[historyMethod](options.state || {}, '', url.toString());
|
|
210
|
+
syncSearchParams(options.search);
|
|
211
|
+
} else {
|
|
212
|
+
syncSearchParams(globalThis.location.search);
|
|
148
213
|
}
|
|
149
214
|
|
|
150
215
|
if (options.viewTransition && document.startViewTransition !== undefined) {
|
|
@@ -156,7 +221,6 @@ export async function onNavigate(path, options = {}) {
|
|
|
156
221
|
}
|
|
157
222
|
params.value = newParams;
|
|
158
223
|
meta.value = newMeta;
|
|
159
|
-
syncSearchParams();
|
|
160
224
|
Object.assign(location, updatedLocation());
|
|
161
225
|
|
|
162
226
|
if (options.scrollToTop !== false) {
|
|
@@ -179,13 +243,16 @@ export function onGlobalClick(event) {
|
|
|
179
243
|
const currentOrigin = globalThis.location.origin;
|
|
180
244
|
if (url.origin !== currentOrigin) return;
|
|
181
245
|
|
|
246
|
+
const path = base.name === '#' ? url.hash : url.pathname;
|
|
247
|
+
const hash = base.name === '#' ? undefined : url.hash;
|
|
248
|
+
|
|
182
249
|
event.preventDefault();
|
|
183
250
|
const { replace, state, scrollToTop, viewTransition } = anchor.dataset;
|
|
184
|
-
onNavigate(
|
|
251
|
+
onNavigate(path, {
|
|
185
252
|
replace: replace === '' || replace === 'true',
|
|
186
253
|
search: url.search,
|
|
187
254
|
state,
|
|
188
|
-
hash
|
|
255
|
+
hash,
|
|
189
256
|
scrollToTop: scrollToTop === 'false' ? false : /** @type ScrollBehavior */ (scrollToTop),
|
|
190
257
|
viewTransition: viewTransition === '' || viewTransition === 'true',
|
|
191
258
|
});
|
package/src/helpers/is-active.js
CHANGED
|
@@ -7,7 +7,7 @@ import { constructPath, join } from './utils.js';
|
|
|
7
7
|
* @returns {boolean}
|
|
8
8
|
*/
|
|
9
9
|
export function isActive(pathname, params) {
|
|
10
|
-
const p = base.name ? join(base.name, pathname) : pathname;
|
|
10
|
+
const p = base.name && base.name !== '#' ? join(base.name, pathname) : pathname;
|
|
11
11
|
return compare((a, b) => a === b, p, params);
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -17,7 +17,7 @@ export function isActive(pathname, params) {
|
|
|
17
17
|
* @returns {boolean}
|
|
18
18
|
*/
|
|
19
19
|
isActive.startsWith = (pathname, params) => {
|
|
20
|
-
const p = base.name ? join(base.name, pathname) : pathname;
|
|
20
|
+
const p = base.name && base.name !== '#' ? join(base.name, pathname) : pathname;
|
|
21
21
|
return compare((a, b) => a.startsWith(b), p, params);
|
|
22
22
|
};
|
|
23
23
|
|
|
@@ -33,11 +33,15 @@ function compare(compareFn, pathname, params) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
if (params) {
|
|
36
|
-
|
|
36
|
+
if (base.name === '#') {
|
|
37
|
+
return compareFn(location.pathname, new URL(constructPath(pathname, params)).hash.slice(1));
|
|
38
|
+
} else {
|
|
39
|
+
return compareFn(location.pathname, constructPath(pathname, params));
|
|
40
|
+
}
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
const pathParts = pathname.split('/').slice(1);
|
|
40
|
-
|
|
44
|
+
let routeParts = location.pathname.split('/').slice(1);
|
|
41
45
|
if (pathParts.length > routeParts.length) {
|
|
42
46
|
return false;
|
|
43
47
|
}
|
package/src/helpers/utils.js
CHANGED
|
@@ -6,13 +6,21 @@ import { base } from '../create-router.svelte.js';
|
|
|
6
6
|
* @returns {string}
|
|
7
7
|
*/
|
|
8
8
|
export function constructPath(path, params) {
|
|
9
|
-
if (
|
|
9
|
+
if (params) {
|
|
10
|
+
for (const key in params) {
|
|
11
|
+
path = path.replace(`:${key}`, params[key]);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (base.name === '#') {
|
|
16
|
+
const url = new URL(globalThis.location.toString());
|
|
17
|
+
url.hash = path;
|
|
18
|
+
url.search = '';
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
for (const key in params) {
|
|
13
|
-
result = result.replace(`:${key}`, params[key]);
|
|
20
|
+
return url.toString();
|
|
14
21
|
}
|
|
15
|
-
|
|
22
|
+
|
|
23
|
+
return path;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
/**
|
|
@@ -44,7 +52,10 @@ function resolveRouteComponent(input) {
|
|
|
44
52
|
* @returns {input is import('../index.d.ts').LazyRouteComponent}
|
|
45
53
|
*/
|
|
46
54
|
export function isLazyImport(input) {
|
|
47
|
-
return
|
|
55
|
+
return (
|
|
56
|
+
typeof input === 'function' &&
|
|
57
|
+
!!/\(\)\s?=>\s?(import|__vite_ssr_dynamic_import__)\(.*\)/.test(String(input))
|
|
58
|
+
);
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
/** @param {...string} parts */
|
|
@@ -74,10 +85,13 @@ export function stripBase(pathname) {
|
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
export function updatedLocation() {
|
|
88
|
+
const pathname =
|
|
89
|
+
base.name === '#' ? globalThis.location.hash.slice(1) : globalThis.location.pathname;
|
|
90
|
+
const hash = base.name === '#' ? '' : globalThis.location.hash;
|
|
77
91
|
return {
|
|
78
|
-
pathname
|
|
92
|
+
pathname,
|
|
79
93
|
search: globalThis.location.search,
|
|
80
94
|
state: history.state,
|
|
81
|
-
hash
|
|
95
|
+
hash,
|
|
82
96
|
};
|
|
83
97
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -41,11 +41,12 @@ export const searchParams: SearchParams;
|
|
|
41
41
|
type BaseProps = {};
|
|
42
42
|
|
|
43
43
|
export type LazyRouteComponent<Props extends BaseProps = BaseProps> = () => Promise<{
|
|
44
|
-
default: Component<Props>;
|
|
44
|
+
default: Component<Props> | Snippet<[Props]>;
|
|
45
45
|
}>;
|
|
46
46
|
|
|
47
47
|
export type RouteComponent<Props extends BaseProps = any> =
|
|
48
48
|
| Component<Props>
|
|
49
|
+
| Snippet<[Props]>
|
|
49
50
|
| LazyRouteComponent<Props>;
|
|
50
51
|
export type LayoutComponent = RouteComponent<{ children: Snippet }>;
|
|
51
52
|
export type Hooks = {
|
|
@@ -67,6 +68,8 @@ export type Hooks = {
|
|
|
67
68
|
afterLoad?(context: HooksContext): void | Promise<void>;
|
|
68
69
|
/** A function that will be called when the route is preloaded. */
|
|
69
70
|
onPreload?(context: HooksContext): void | Promise<void>;
|
|
71
|
+
/** A function that will be called when the route fails to load. */
|
|
72
|
+
onError?(error: unknown, context: HooksContext): void | Promise<void>;
|
|
70
73
|
};
|
|
71
74
|
|
|
72
75
|
export type Routes = {
|
|
@@ -240,7 +243,7 @@ export type NavigateOptions =
|
|
|
240
243
|
}
|
|
241
244
|
| undefined;
|
|
242
245
|
|
|
243
|
-
export type SearchParams = URLSearchParams & {
|
|
246
|
+
export type SearchParams = Omit<URLSearchParams, 'append' | 'delete' | 'set' | 'sort'> & {
|
|
244
247
|
append: (name: string, value: string, options?: { replace?: boolean }) => void;
|
|
245
248
|
delete: (name: string, value?: string, options?: { replace?: boolean }) => void;
|
|
246
249
|
set: (name: string, value: string, options?: { replace?: boolean }) => void;
|
|
@@ -55,22 +55,21 @@ const shell = {
|
|
|
55
55
|
|
|
56
56
|
export { shell as searchParams };
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (searchParams.toString()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
/** @param {string} [search] */
|
|
59
|
+
export function syncSearchParams(search) {
|
|
60
|
+
if (searchParams.toString() !== search) {
|
|
61
|
+
searchParams = new SvelteURLSearchParams();
|
|
62
|
+
const newSearchParams = new URLSearchParams(search);
|
|
63
|
+
for (const [key, value] of newSearchParams.entries()) {
|
|
64
|
+
searchParams.append(key, value);
|
|
65
|
+
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** @param {{ replace?: boolean }} [options] */
|
|
70
70
|
function updateUrlSearchParams(options) {
|
|
71
|
-
let url =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
71
|
+
let url = new URL(globalThis.location.toString());
|
|
72
|
+
url.search = searchParams.toString();
|
|
73
|
+
|
|
75
74
|
globalThis.history[options?.replace ? 'replaceState' : 'pushState']({}, '', url);
|
|
76
75
|
}
|