@esmx/router 3.0.0-rc.103

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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/README.zh-CN.md +158 -0
  4. package/dist/error.d.ts +23 -0
  5. package/dist/error.mjs +64 -0
  6. package/dist/increment-id.d.ts +7 -0
  7. package/dist/increment-id.mjs +16 -0
  8. package/dist/index.d.ts +14 -0
  9. package/dist/index.mjs +13 -0
  10. package/dist/location.d.ts +22 -0
  11. package/dist/location.mjs +64 -0
  12. package/dist/matcher.d.ts +4 -0
  13. package/dist/matcher.mjs +46 -0
  14. package/dist/micro-app.d.ts +18 -0
  15. package/dist/micro-app.mjs +85 -0
  16. package/dist/navigation.d.ts +45 -0
  17. package/dist/navigation.mjs +153 -0
  18. package/dist/options.d.ts +4 -0
  19. package/dist/options.mjs +94 -0
  20. package/dist/route-task.d.ts +40 -0
  21. package/dist/route-task.mjs +77 -0
  22. package/dist/route-transition.d.ts +53 -0
  23. package/dist/route-transition.mjs +356 -0
  24. package/dist/route.d.ts +77 -0
  25. package/dist/route.mjs +223 -0
  26. package/dist/router-link.d.ts +10 -0
  27. package/dist/router-link.mjs +139 -0
  28. package/dist/router.d.ts +122 -0
  29. package/dist/router.mjs +355 -0
  30. package/dist/scroll.d.ts +33 -0
  31. package/dist/scroll.mjs +49 -0
  32. package/dist/types.d.ts +282 -0
  33. package/dist/types.mjs +18 -0
  34. package/dist/util.d.ts +27 -0
  35. package/dist/util.mjs +67 -0
  36. package/package.json +62 -0
  37. package/src/error.ts +84 -0
  38. package/src/increment-id.ts +12 -0
  39. package/src/index.ts +67 -0
  40. package/src/location.ts +124 -0
  41. package/src/matcher.ts +68 -0
  42. package/src/micro-app.ts +101 -0
  43. package/src/navigation.ts +202 -0
  44. package/src/options.ts +135 -0
  45. package/src/route-task.ts +102 -0
  46. package/src/route-transition.ts +472 -0
  47. package/src/route.ts +335 -0
  48. package/src/router-link.ts +238 -0
  49. package/src/router.ts +395 -0
  50. package/src/scroll.ts +106 -0
  51. package/src/types.ts +381 -0
  52. package/src/util.ts +133 -0
@@ -0,0 +1,124 @@
1
+ import type { RouteLocation, RouteLocationInput } from './types';
2
+ import { isNotNullish } from './util';
3
+
4
+ /**
5
+ * Normalizes a URL input into a URL object.
6
+ * @param url - The URL or string to normalize.
7
+ * @param base - The base URL to resolve against if the input is relative.
8
+ * @returns A URL object.
9
+ */
10
+ export function normalizeURL(url: string | URL, base: URL): URL {
11
+ if (url instanceof URL) {
12
+ return url;
13
+ }
14
+
15
+ // Handle protocol-relative URLs (e.g., //example.com)
16
+ if (url.startsWith('//')) {
17
+ // Use the current base URL's protocol for security and consistency
18
+ const protocol = base.protocol;
19
+ return new URL(`${protocol}${url}`);
20
+ }
21
+
22
+ // Handle root-relative paths
23
+ if (url.startsWith('/')) {
24
+ const newBase = new URL('.', base);
25
+ const parsed = new URL(url, newBase);
26
+ // This ensures that the path is resolved relative to the base's path directory.
27
+ parsed.pathname = newBase.pathname.slice(0, -1) + parsed.pathname;
28
+ return parsed;
29
+ }
30
+
31
+ try {
32
+ // Try to parse as an absolute URL.
33
+ // This is the WHATWG standard approach (new URL()) and works consistently across all modern browsers and Node.js.
34
+ // We use a try-catch block because the standard URL constructor throws an error for invalid URLs.
35
+ //
36
+ // NOTE: While `URL.parse()` might be observed in Chromium-based browsers (e.g., Chrome, Edge),
37
+ // it is a non-standard, legacy feature implemented by the V8 engine for Node.js compatibility.
38
+ // It is not part of the WHATWG URL Standard and is not supported by other browsers like Firefox or Safari.
39
+ // Therefore, relying on it would compromise cross-browser compatibility.
40
+ return new URL(url);
41
+ } catch (e) {
42
+ // Otherwise, parse as a relative URL
43
+ return new URL(url, base);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Parses a RouteLocationInput object into a full URL.
49
+ * @param toInput - The route location input.
50
+ * @param baseURL - The base URL to resolve against.
51
+ * @returns The parsed URL object.
52
+ */
53
+ export function parseLocation(toInput: RouteLocationInput, baseURL: URL): URL {
54
+ if (typeof toInput === 'string') {
55
+ return normalizeURL(toInput, baseURL);
56
+ }
57
+ const url = normalizeURL(toInput.path ?? toInput.url ?? '', baseURL);
58
+ const searchParams = url.searchParams;
59
+
60
+ // Priority: queryArray > query > query in path
61
+ const mergedQuery: Record<string, string | string[]> = {};
62
+
63
+ // First, add query values
64
+ if (toInput.query) {
65
+ Object.entries(toInput.query).forEach(([key, value]) => {
66
+ if (typeof value !== 'undefined') {
67
+ mergedQuery[key] = value;
68
+ }
69
+ });
70
+ }
71
+
72
+ // Then, add queryArray values (higher priority)
73
+ if (toInput.queryArray) {
74
+ Object.entries(toInput.queryArray).forEach(([key, value]) => {
75
+ if (typeof value !== 'undefined') {
76
+ mergedQuery[key] = value;
77
+ }
78
+ });
79
+ }
80
+
81
+ Object.entries(mergedQuery).forEach(([key, value]) => {
82
+ searchParams.delete(key); // Clear previous params with the same name
83
+ value = Array.isArray(value) ? value : [value];
84
+ value
85
+ .filter((v) => isNotNullish(v) && !Number.isNaN(v))
86
+ .forEach((v) => {
87
+ searchParams.append(key, String(v));
88
+ });
89
+ });
90
+
91
+ // Set the hash (URL fragment identifier)
92
+ if (toInput.hash) {
93
+ url.hash = toInput.hash;
94
+ }
95
+
96
+ return url;
97
+ }
98
+
99
+ /**
100
+ * Resolves RouteLocationInput with fallback from previous route
101
+ * @param toInput - The route location input
102
+ * @param from - The previous route URL (optional)
103
+ * @returns Resolved RouteLocation object
104
+ */
105
+ export function resolveRouteLocationInput(
106
+ toInput: RouteLocationInput = '/',
107
+ from: URL | null = null
108
+ ): RouteLocation {
109
+ if (typeof toInput === 'string') {
110
+ return { path: toInput };
111
+ }
112
+
113
+ if (
114
+ toInput &&
115
+ typeof toInput === 'object' &&
116
+ typeof toInput.path !== 'string' &&
117
+ typeof toInput.url !== 'string' &&
118
+ from !== null
119
+ ) {
120
+ return { ...toInput, url: from.href };
121
+ }
122
+
123
+ return toInput;
124
+ }
package/src/matcher.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { compile, match } from 'path-to-regexp';
2
+ import type { RouteConfig, RouteMatcher, RouteParsedConfig } from './types';
3
+
4
+ export function createMatcher(
5
+ routes: RouteConfig[],
6
+ compiledRoutes = createRouteMatches(routes)
7
+ ): RouteMatcher {
8
+ return (
9
+ toURL: URL,
10
+ baseURL: URL,
11
+ cb?: (item: RouteParsedConfig) => boolean
12
+ ) => {
13
+ const matchPath = toURL.pathname.substring(baseURL.pathname.length - 1);
14
+ const matches: RouteParsedConfig[] = [];
15
+ const params: Record<string, string | string[]> = {};
16
+ const collectMatchingRoutes = (
17
+ routes: RouteParsedConfig[]
18
+ ): boolean => {
19
+ for (const item of routes) {
20
+ if (cb && !cb(item)) {
21
+ continue;
22
+ }
23
+ // Depth-first traversal
24
+ if (
25
+ item.children.length &&
26
+ collectMatchingRoutes(item.children)
27
+ ) {
28
+ matches.unshift(item);
29
+ return true;
30
+ }
31
+ const result = item.match(matchPath);
32
+ if (result) {
33
+ matches.unshift(item);
34
+ if (typeof result === 'object') {
35
+ Object.assign(params, result.params);
36
+ }
37
+ return true;
38
+ }
39
+ }
40
+ return false;
41
+ };
42
+ collectMatchingRoutes(compiledRoutes);
43
+ return { matches: Object.freeze(matches), params };
44
+ };
45
+ }
46
+
47
+ export function createRouteMatches(
48
+ routes: RouteConfig[],
49
+ base = ''
50
+ ): RouteParsedConfig[] {
51
+ return routes.map((route: RouteConfig): RouteParsedConfig => {
52
+ const compilePath = joinPathname(route.path, base);
53
+ return {
54
+ ...route,
55
+ compilePath,
56
+ match: match(compilePath),
57
+ compile: compile(compilePath),
58
+ meta: route.meta || {},
59
+ children: Array.isArray(route.children)
60
+ ? createRouteMatches(route.children, compilePath)
61
+ : []
62
+ };
63
+ });
64
+ }
65
+
66
+ export function joinPathname(pathname: string, base = '') {
67
+ return '/' + `${base}/${pathname}`.split('/').filter(Boolean).join('/');
68
+ }
@@ -0,0 +1,101 @@
1
+ import type { Router } from './router';
2
+ import type { RouterMicroAppCallback, RouterMicroAppOptions } from './types';
3
+ import { isBrowser, isPlainObject } from './util';
4
+
5
+ /**
6
+ * Resolves the root container element.
7
+ * Supports a DOM selector string or a direct HTMLElement.
8
+ *
9
+ * @param rootConfig - The root container configuration, can be a selector string or an HTMLElement.
10
+ * @returns The resolved HTMLElement.
11
+ */
12
+ export function resolveRootElement(
13
+ rootConfig?: string | HTMLElement
14
+ ): HTMLElement {
15
+ let el: HTMLElement | null = null;
16
+ // Direct HTMLElement provided
17
+ if (rootConfig instanceof HTMLElement) {
18
+ el = rootConfig;
19
+ }
20
+ if (typeof rootConfig === 'string' && rootConfig) {
21
+ try {
22
+ el = document.querySelector(rootConfig);
23
+ } catch (error) {
24
+ console.warn(`Failed to resolve root element: ${rootConfig}`);
25
+ }
26
+ }
27
+ if (el === null) {
28
+ el = document.createElement('div');
29
+ }
30
+ return el;
31
+ }
32
+
33
+ export class MicroApp {
34
+ public app: RouterMicroAppOptions | null = null;
35
+ public root: HTMLElement | null = null;
36
+ private _factory: RouterMicroAppCallback | null = null;
37
+
38
+ public _update(router: Router, force = false) {
39
+ const factory = this._getNextFactory(router);
40
+ if (!force && factory === this._factory) {
41
+ return;
42
+ }
43
+ const oldApp = this.app;
44
+ // Create the new application
45
+ const app = factory ? factory(router) : null;
46
+ if (isBrowser && app) {
47
+ let root: HTMLElement | null = this.root;
48
+ if (root === null) {
49
+ root = resolveRootElement(router.root);
50
+ const { rootStyle } = router.parsedOptions;
51
+ if (root && isPlainObject(rootStyle)) {
52
+ Object.assign(root.style, router.parsedOptions.rootStyle);
53
+ }
54
+ }
55
+ if (root) {
56
+ app.mount(root);
57
+ if (root.parentNode === null) {
58
+ document.body.appendChild(root);
59
+ }
60
+ this.root = root;
61
+ }
62
+ if (oldApp) {
63
+ oldApp.unmount();
64
+ }
65
+ }
66
+ this.app = app;
67
+ this._factory = factory;
68
+ }
69
+
70
+ private _getNextFactory({
71
+ route,
72
+ options
73
+ }: Router): RouterMicroAppCallback | null {
74
+ if (!route.matched || route.matched.length === 0) {
75
+ return null;
76
+ }
77
+ const name = route.matched[0].app;
78
+ if (
79
+ typeof name === 'string' &&
80
+ options.apps &&
81
+ typeof options.apps === 'object'
82
+ ) {
83
+ return options.apps[name] || null;
84
+ }
85
+ if (typeof name === 'function') {
86
+ return name;
87
+ }
88
+ if (typeof options.apps === 'function') {
89
+ return options.apps;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ public destroy() {
95
+ this.app?.unmount();
96
+ this.app = null;
97
+ this.root?.remove();
98
+ this.root = null;
99
+ this._factory = null;
100
+ }
101
+ }
@@ -0,0 +1,202 @@
1
+ import { PAGE_ID } from './increment-id';
2
+ import type { RouterParsedOptions, RouteState } from './types';
3
+ import { RouterMode } from './types';
4
+
5
+ type NavigationSubscribe = (url: string, state: RouteState) => void;
6
+ type NavigationGoResult = null | {
7
+ type: 'success';
8
+ url: string;
9
+ state: RouteState;
10
+ };
11
+
12
+ const PAGE_ID_KEY = '__pageId__';
13
+
14
+ export class Navigation {
15
+ public readonly options: RouterParsedOptions;
16
+ private readonly _history: History | MemoryHistory;
17
+ private readonly _unSubscribePopState: () => void;
18
+ private _promiseResolve:
19
+ | ((url?: string | null, state?: RouteState) => void)
20
+ | null = null;
21
+
22
+ public constructor(
23
+ options: RouterParsedOptions,
24
+ onUpdated?: NavigationSubscribe
25
+ ) {
26
+ const history: History =
27
+ options.mode === RouterMode.history
28
+ ? window.history
29
+ : new MemoryHistory();
30
+ const onPopStateChange: NavigationSubscribe = (url, state) => {
31
+ const dispatchEvent = this._promiseResolve || onUpdated;
32
+ dispatchEvent?.(url, state);
33
+ };
34
+ const subscribePopState =
35
+ history instanceof MemoryHistory
36
+ ? history.onPopState(onPopStateChange)
37
+ : subscribeHtmlHistory(onPopStateChange);
38
+ this.options = options;
39
+ this._history = history;
40
+ this._unSubscribePopState = subscribePopState;
41
+ }
42
+ public get length(): number {
43
+ return this._history.length;
44
+ }
45
+
46
+ private _push(
47
+ history: History,
48
+ data: any,
49
+ url?: string | URL | null
50
+ ): RouteState {
51
+ const state = {
52
+ ...(data || {}),
53
+ [PAGE_ID_KEY]: PAGE_ID.next()
54
+ };
55
+ history.pushState(state, '', url);
56
+ return state;
57
+ }
58
+
59
+ private _replace(
60
+ history: History,
61
+ data: any,
62
+ url?: string | URL | null
63
+ ): RouteState {
64
+ const oldId = history.state?.[PAGE_ID_KEY];
65
+ const state = {
66
+ ...(data || {}),
67
+ [PAGE_ID_KEY]: typeof oldId === 'number' ? oldId : PAGE_ID.next()
68
+ };
69
+ history.replaceState(state, '', url);
70
+ return state;
71
+ }
72
+
73
+ public push(data: any, url?: string | URL | null): RouteState {
74
+ return this._push(this._history, data, url);
75
+ }
76
+
77
+ public replace(data: any, url?: string | URL | null): RouteState {
78
+ return this._replace(this._history, data, url);
79
+ }
80
+
81
+ public pushHistoryState(data: any, url?: string | URL | null) {
82
+ this._push(history, data, url);
83
+ }
84
+
85
+ public replaceHistoryState(data: any, url?: string | URL | null) {
86
+ this._replace(history, data, url);
87
+ }
88
+
89
+ public backHistoryState() {
90
+ return this._go(history, -1);
91
+ }
92
+
93
+ private _go(history: History, index: number): Promise<NavigationGoResult> {
94
+ if (this._promiseResolve) {
95
+ return Promise.resolve(null);
96
+ }
97
+ return new Promise<NavigationGoResult>((resolve) => {
98
+ this._promiseResolve = (url, state) => {
99
+ this._promiseResolve = null;
100
+ if (typeof url !== 'string') return resolve(null);
101
+ resolve({ type: 'success', url, state: state || {} });
102
+ };
103
+ setTimeout(this._promiseResolve, 80);
104
+ history.go(index);
105
+ });
106
+ }
107
+ public go(delta?: number): Promise<NavigationGoResult> {
108
+ return this._go(this._history, delta || 0);
109
+ }
110
+ public forward(): Promise<NavigationGoResult> {
111
+ return this.go(1);
112
+ }
113
+ public back(): Promise<NavigationGoResult> {
114
+ return this.go(-1);
115
+ }
116
+
117
+ public destroy() {
118
+ this._promiseResolve?.();
119
+ this._unSubscribePopState();
120
+ }
121
+ }
122
+
123
+ export class MemoryHistory implements History {
124
+ private _entries: Array<{ state: any; url: string }> = [];
125
+ private _index = -1;
126
+ private get _curEntry() {
127
+ const idx = this._index;
128
+ if (idx < 0 || idx >= this.length) return null;
129
+ return this._entries[idx];
130
+ }
131
+ private readonly _popStateCbs = new Set<NavigationSubscribe>();
132
+ public scrollRestoration: ScrollRestoration = 'auto';
133
+ // Return null when no current entry to align with browser history.state behavior
134
+ // Browser history.state can be null when no state was provided
135
+ public get state() {
136
+ return this._curEntry?.state ?? null;
137
+ }
138
+ public get url() {
139
+ return this._curEntry?.url ?? '';
140
+ }
141
+
142
+ constructor() {
143
+ this.pushState(null, '', '/');
144
+ }
145
+ public get length() {
146
+ return this._entries.length;
147
+ }
148
+
149
+ public pushState(
150
+ data: any,
151
+ unused: string,
152
+ url?: string | URL | null
153
+ ): void {
154
+ // Remove all entries after the current position
155
+ this._entries.splice(this._index + 1);
156
+ this._entries.push({ state: data, url: url?.toString() ?? this.url });
157
+ this._index = this._entries.length - 1;
158
+ }
159
+
160
+ public replaceState(
161
+ data: any,
162
+ unused: string,
163
+ url?: string | URL | null
164
+ ): void {
165
+ const curEntry = this._curEntry;
166
+ if (!curEntry) return;
167
+ curEntry.state = { ...data };
168
+ if (url) curEntry.url = url.toString();
169
+ }
170
+
171
+ public back(): void {
172
+ this.go(-1);
173
+ }
174
+ public forward(): void {
175
+ this.go(1);
176
+ }
177
+ public go(delta?: number): void {
178
+ if (!delta) return;
179
+ const newIdx = this._index + delta;
180
+ if (newIdx < 0 || newIdx >= this.length) return;
181
+ this._index = newIdx;
182
+ const entry = this._curEntry!;
183
+ // Simulate the async popstate event of html history as closely as possible
184
+ setTimeout(() => {
185
+ this._popStateCbs.forEach((cb) => cb(entry.url, entry.state));
186
+ });
187
+ }
188
+
189
+ public onPopState(cb: NavigationSubscribe) {
190
+ if (typeof cb !== 'function') return () => {};
191
+ this._popStateCbs.add(cb);
192
+ return () => this._popStateCbs.delete(cb);
193
+ }
194
+ }
195
+
196
+ function subscribeHtmlHistory(cb: NavigationSubscribe) {
197
+ // Use history.state || {} to handle null state from browser history
198
+ // Browser history.state can be null, but we normalize it to empty object
199
+ const wrapper = () => cb(location.href, history.state || {});
200
+ window.addEventListener('popstate', wrapper);
201
+ return () => window.removeEventListener('popstate', wrapper);
202
+ }
package/src/options.ts ADDED
@@ -0,0 +1,135 @@
1
+ import { createMatcher, createRouteMatches } from './matcher';
2
+ import type { Router } from './router';
3
+ import type { Route, RouterOptions, RouterParsedOptions } from './types';
4
+ import { RouterMode } from './types';
5
+ import { isBrowser } from './util';
6
+
7
+ /**
8
+ * Gets the base URL object for the router.
9
+ * @param options - Router options.
10
+ * @returns The processed URL object.
11
+ */
12
+ function getBaseUrl(options: RouterOptions): URL {
13
+ // Determine the URL source
14
+ let sourceUrl: string | URL;
15
+
16
+ if (options.base) {
17
+ sourceUrl = options.base;
18
+ } else if (isBrowser) {
19
+ sourceUrl = location.origin;
20
+ } else if (options.req) {
21
+ // Server-side: try to get it from the req object
22
+ const { req } = options;
23
+ const protocol =
24
+ req.headers['x-forwarded-proto'] ||
25
+ req.headers['x-forwarded-protocol'] ||
26
+ (req.socket && 'encrypted' in req.socket && req.socket.encrypted
27
+ ? 'https'
28
+ : 'http');
29
+ const host =
30
+ req.headers['x-forwarded-host'] ||
31
+ req.headers.host ||
32
+ req.headers['x-real-ip'] ||
33
+ 'localhost';
34
+ const port = req.headers['x-forwarded-port'];
35
+ const path = req.url || '';
36
+
37
+ sourceUrl = `${protocol}://${host}${port ? `:${port}` : ''}${path}`;
38
+ } else {
39
+ sourceUrl = 'https://esmx.dev/';
40
+ }
41
+
42
+ // Parse the URL, falling back to a default on failure.
43
+ // Use a try-catch block with the standard URL constructor for robustness.
44
+ let base: URL;
45
+ try {
46
+ base = new URL('.', sourceUrl);
47
+ } catch (e) {
48
+ console.warn(
49
+ `Failed to parse base URL '${sourceUrl}', using default: https://esmx.dev/`
50
+ );
51
+ base = new URL('https://esmx.dev/');
52
+ }
53
+
54
+ // Clean up and return
55
+ base.search = base.hash = '';
56
+ return base;
57
+ }
58
+
59
+ export function parsedOptions(
60
+ options: RouterOptions = {}
61
+ ): RouterParsedOptions {
62
+ const base = getBaseUrl(options);
63
+ const routes = options.routes ?? [];
64
+ const compiledRoutes = createRouteMatches(routes);
65
+ return Object.freeze<RouterParsedOptions>({
66
+ rootStyle: options.rootStyle || false,
67
+ root: options.root || '',
68
+ context: options.context || {},
69
+ data: options.data || {},
70
+ req: options.req || null,
71
+ res: options.res || null,
72
+ layer: options.layer || false,
73
+ zIndex: options.zIndex || 10000,
74
+ base,
75
+ mode: isBrowser
76
+ ? (options.mode ?? RouterMode.history)
77
+ : RouterMode.memory,
78
+ routes,
79
+ apps:
80
+ typeof options.apps === 'function'
81
+ ? options.apps
82
+ : Object.assign({}, options.apps),
83
+ compiledRoutes,
84
+ matcher: createMatcher(routes, compiledRoutes),
85
+ normalizeURL: options.normalizeURL ?? ((url) => url),
86
+ fallback: options.fallback ?? fallback,
87
+ nextTick: options.nextTick ?? (() => {}),
88
+ handleBackBoundary: options.handleBackBoundary ?? (() => {}),
89
+ handleLayerClose: options.handleLayerClose ?? (() => {})
90
+ });
91
+ }
92
+
93
+ export function fallback(to: Route, from: Route | null, router: Router) {
94
+ const href = to.url.href;
95
+
96
+ // Server-side environment: handle application-level redirects and status codes
97
+ if (!isBrowser && router?.res) {
98
+ // Determine status code: prioritize route-specified code, default to 302 temporary redirect
99
+ let statusCode = 302;
100
+
101
+ // Validate redirect status code (3xx series)
102
+ const validRedirectCodes = [300, 301, 302, 303, 304, 307, 308];
103
+ if (to.statusCode && validRedirectCodes.includes(to.statusCode)) {
104
+ statusCode = to.statusCode;
105
+ } else if (to.statusCode) {
106
+ console.warn(
107
+ `Invalid redirect status code ${to.statusCode}, using default 302`
108
+ );
109
+ }
110
+
111
+ // Set redirect response
112
+ router.res.statusCode = statusCode;
113
+ router.res.setHeader('Location', href);
114
+ router.res.end();
115
+ return;
116
+ }
117
+
118
+ // Client-side environment: handle browser navigation
119
+ if (isBrowser) {
120
+ if (to.isPush) {
121
+ try {
122
+ const newWindow = window.open(href);
123
+ if (!newWindow) {
124
+ location.href = href;
125
+ } else {
126
+ newWindow.opener = null; // Sever the relationship between the new window and the current one
127
+ }
128
+ return newWindow;
129
+ } catch {}
130
+ }
131
+ location.href = href;
132
+ }
133
+
134
+ // Do nothing in a server environment without a res context
135
+ }