@real-router/browser-plugin 0.6.3 → 0.7.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/README.md +11 -20
- package/dist/cjs/index.d.ts +4 -62
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/metafile-cjs.json +1 -1
- package/dist/esm/index.d.mts +4 -62
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/metafile-esm.json +1 -1
- package/package.json +3 -3
- package/src/browser.ts +19 -168
- package/src/constants.ts +0 -2
- package/src/factory.ts +88 -0
- package/src/index.ts +4 -10
- package/src/plugin.ts +197 -418
- package/src/popstate-utils.ts +60 -0
- package/src/types.ts +25 -50
- package/src/url-utils.ts +95 -0
- package/src/validation.ts +66 -0
- package/src/utils.ts +0 -294
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isStateStrict as isState } from "type-guards";
|
|
2
|
+
|
|
3
|
+
import type { BrowserPluginOptions, Browser } from "./types";
|
|
4
|
+
import type { PluginApi, State, Params } from "@real-router/core";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extracts route name and params from a popstate event.
|
|
8
|
+
*
|
|
9
|
+
* - If history.state is a valid router state → returns name/params from it
|
|
10
|
+
* - If not (e.g. manually entered URL) → matches current URL against route tree
|
|
11
|
+
* - Returns undefined if no route matches
|
|
12
|
+
*
|
|
13
|
+
* @param evt - PopStateEvent from browser
|
|
14
|
+
* @param api - PluginApi instance
|
|
15
|
+
* @param browser - Browser API instance
|
|
16
|
+
* @param options - Browser plugin options
|
|
17
|
+
* @returns Route identifier or undefined
|
|
18
|
+
*/
|
|
19
|
+
export function getRouteFromEvent(
|
|
20
|
+
evt: PopStateEvent,
|
|
21
|
+
api: PluginApi,
|
|
22
|
+
browser: Browser,
|
|
23
|
+
options: BrowserPluginOptions,
|
|
24
|
+
): { name: string; params: Params } | undefined {
|
|
25
|
+
if (isState(evt.state)) {
|
|
26
|
+
return { name: evt.state.name, params: evt.state.params };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const state = api.matchPath(browser.getLocation(options));
|
|
30
|
+
|
|
31
|
+
return state ? { name: state.name, params: state.params } : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Updates browser state (pushState or replaceState)
|
|
36
|
+
*
|
|
37
|
+
* @param state - Router state
|
|
38
|
+
* @param url - URL to set
|
|
39
|
+
* @param replace - Whether to replace instead of push
|
|
40
|
+
* @param browser - Browser API instance
|
|
41
|
+
*/
|
|
42
|
+
export function updateBrowserState(
|
|
43
|
+
state: State,
|
|
44
|
+
url: string,
|
|
45
|
+
replace: boolean,
|
|
46
|
+
browser: Browser,
|
|
47
|
+
): void {
|
|
48
|
+
const historyState = {
|
|
49
|
+
meta: state.meta,
|
|
50
|
+
name: state.name,
|
|
51
|
+
params: state.params,
|
|
52
|
+
path: state.path,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (replace) {
|
|
56
|
+
browser.replaceState(historyState, url);
|
|
57
|
+
} else {
|
|
58
|
+
browser.pushState(historyState, url);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// packages/browser-plugin/
|
|
1
|
+
// packages/browser-plugin/src/types.ts
|
|
2
2
|
|
|
3
3
|
import type { State } from "@real-router/core";
|
|
4
4
|
|
|
@@ -19,14 +19,6 @@ interface BaseBrowserPluginOptions {
|
|
|
19
19
|
* @default ""
|
|
20
20
|
*/
|
|
21
21
|
base?: string;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Merge new state with existing history.state when updating.
|
|
25
|
-
* Useful for preserving external state set by other code.
|
|
26
|
-
*
|
|
27
|
-
* @default false
|
|
28
|
-
*/
|
|
29
|
-
mergeState?: boolean;
|
|
30
22
|
}
|
|
31
23
|
|
|
32
24
|
/**
|
|
@@ -146,51 +138,23 @@ export type BrowserPluginOptions = HashModeOptions | HistoryModeOptions;
|
|
|
146
138
|
* Provides same interface in browser and SSR contexts.
|
|
147
139
|
*/
|
|
148
140
|
export interface Browser {
|
|
149
|
-
/**
|
|
150
|
-
* Gets base path from current browser location
|
|
151
|
-
*
|
|
152
|
-
* @returns Current pathname
|
|
153
|
-
*/
|
|
154
|
-
getBase: () => string;
|
|
155
|
-
|
|
156
141
|
/**
|
|
157
142
|
* Pushes new state to browser history
|
|
158
143
|
*
|
|
159
144
|
* @param state - History state object
|
|
160
|
-
* @param title - Document title (usually ignored by browsers)
|
|
161
145
|
* @param path - URL path
|
|
162
146
|
*/
|
|
163
|
-
pushState: (state:
|
|
147
|
+
pushState: (state: State, path: string) => void;
|
|
164
148
|
|
|
165
149
|
/**
|
|
166
150
|
* Replaces current history state
|
|
167
151
|
*
|
|
168
152
|
* @param state - History state object
|
|
169
|
-
* @param title - Document title (usually ignored by browsers)
|
|
170
153
|
* @param path - URL path
|
|
171
154
|
*/
|
|
172
|
-
replaceState: (
|
|
173
|
-
state: HistoryState,
|
|
174
|
-
title: string | null,
|
|
175
|
-
path: string,
|
|
176
|
-
) => void;
|
|
155
|
+
replaceState: (state: State, path: string) => void;
|
|
177
156
|
|
|
178
|
-
|
|
179
|
-
* Adds popstate/hashchange event listeners.
|
|
180
|
-
* Overloaded to support both PopStateEvent and HashChangeEvent.
|
|
181
|
-
*
|
|
182
|
-
* @param fn - Event handler
|
|
183
|
-
* @param opts - Plugin options
|
|
184
|
-
* @returns Cleanup function to remove listeners
|
|
185
|
-
*/
|
|
186
|
-
addPopstateListener: ((
|
|
187
|
-
fn: (evt: PopStateEvent) => void,
|
|
188
|
-
opts: BrowserPluginOptions,
|
|
189
|
-
) => () => void) &
|
|
190
|
-
((
|
|
191
|
-
fn: (evt: HashChangeEvent) => void,
|
|
192
|
-
opts: BrowserPluginOptions,
|
|
193
|
-
) => () => void);
|
|
157
|
+
addPopstateListener: (fn: (evt: PopStateEvent) => void) => () => void;
|
|
194
158
|
|
|
195
159
|
/**
|
|
196
160
|
* Gets current location path respecting plugin options
|
|
@@ -200,13 +164,6 @@ export interface Browser {
|
|
|
200
164
|
*/
|
|
201
165
|
getLocation: (opts: BrowserPluginOptions) => string;
|
|
202
166
|
|
|
203
|
-
/**
|
|
204
|
-
* Gets current history state with validation
|
|
205
|
-
*
|
|
206
|
-
* @returns Valid history state or undefined
|
|
207
|
-
*/
|
|
208
|
-
getState: () => HistoryState | undefined;
|
|
209
|
-
|
|
210
167
|
/**
|
|
211
168
|
* Gets current URL hash
|
|
212
169
|
*
|
|
@@ -216,7 +173,25 @@ export interface Browser {
|
|
|
216
173
|
}
|
|
217
174
|
|
|
218
175
|
/**
|
|
219
|
-
*
|
|
220
|
-
*
|
|
176
|
+
* Subset of BrowserPluginOptions needed for URL parsing operations.
|
|
177
|
+
* Intentionally a flat interface (not a discriminated union) because this is an
|
|
178
|
+
* internal type for pure functions — the calling code in plugin.ts already works
|
|
179
|
+
* with validated BrowserPluginOptions and passes correct values.
|
|
180
|
+
*/
|
|
181
|
+
export interface URLParseOptions {
|
|
182
|
+
readonly useHash: boolean;
|
|
183
|
+
readonly base: string;
|
|
184
|
+
readonly hashPrefix: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface RegExpCache {
|
|
188
|
+
get: (pattern: string) => RegExp;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Shared mutable state across BrowserPlugin instances created by the same factory.
|
|
193
|
+
* Enables cleanup of a previous instance's popstate listener when the factory is reused.
|
|
221
194
|
*/
|
|
222
|
-
export
|
|
195
|
+
export interface SharedFactoryState {
|
|
196
|
+
removePopStateListener: (() => void) | undefined;
|
|
197
|
+
}
|
package/src/url-utils.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// packages/browser-plugin/src/url-utils.ts
|
|
2
|
+
|
|
3
|
+
import { LOGGER_CONTEXT } from "./constants";
|
|
4
|
+
|
|
5
|
+
import type { URLParseOptions, RegExpCache } from "./types";
|
|
6
|
+
|
|
7
|
+
const escapeRegExpCache = new Map<string, string>();
|
|
8
|
+
|
|
9
|
+
export const escapeRegExp = (str: string): string => {
|
|
10
|
+
const cached = escapeRegExpCache.get(str);
|
|
11
|
+
|
|
12
|
+
if (cached !== undefined) {
|
|
13
|
+
return cached;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const escaped = str.replaceAll(/[$()*+.?[\\\]^{|}-]/g, String.raw`\$&`);
|
|
17
|
+
|
|
18
|
+
escapeRegExpCache.set(str, escaped);
|
|
19
|
+
|
|
20
|
+
return escaped;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function extractPath(
|
|
24
|
+
pathname: string,
|
|
25
|
+
hash: string,
|
|
26
|
+
options: URLParseOptions,
|
|
27
|
+
regExpCache: RegExpCache,
|
|
28
|
+
): string {
|
|
29
|
+
if (options.useHash) {
|
|
30
|
+
const escapedHashPrefix = escapeRegExp(options.hashPrefix);
|
|
31
|
+
const path = escapedHashPrefix
|
|
32
|
+
? hash.replace(regExpCache.get(`^#${escapedHashPrefix}`), "")
|
|
33
|
+
: hash.slice(1);
|
|
34
|
+
|
|
35
|
+
return path || "/";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (options.base) {
|
|
39
|
+
const escapedBase = escapeRegExp(options.base);
|
|
40
|
+
const stripped = pathname.replace(regExpCache.get(`^${escapedBase}`), "");
|
|
41
|
+
|
|
42
|
+
return stripped.startsWith("/") ? stripped : `/${stripped}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return pathname;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function urlToPath(
|
|
49
|
+
url: string,
|
|
50
|
+
options: URLParseOptions,
|
|
51
|
+
regExpCache: RegExpCache,
|
|
52
|
+
): string | null {
|
|
53
|
+
try {
|
|
54
|
+
const parsedUrl = new URL(url, globalThis.location.origin);
|
|
55
|
+
|
|
56
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
57
|
+
console.warn(`[${LOGGER_CONTEXT}] Invalid URL protocol in ${url}`);
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
extractPath(parsedUrl.pathname, parsedUrl.hash, options, regExpCache) +
|
|
64
|
+
parsedUrl.search
|
|
65
|
+
);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn(`[${LOGGER_CONTEXT}] Could not parse url ${url}`, error);
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildUrl(path: string, base: string, prefix: string): string {
|
|
74
|
+
return base + prefix + path;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createRegExpCache(): RegExpCache {
|
|
78
|
+
const cache = new Map<string, RegExp>();
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
get(pattern: string): RegExp {
|
|
82
|
+
const cached = cache.get(pattern);
|
|
83
|
+
|
|
84
|
+
if (cached !== undefined) {
|
|
85
|
+
return cached;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const newRegExp = new RegExp(pattern);
|
|
89
|
+
|
|
90
|
+
cache.set(pattern, newRegExp);
|
|
91
|
+
|
|
92
|
+
return newRegExp;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type DefaultBrowserPluginOptions, LOGGER_CONTEXT } from "./constants";
|
|
2
|
+
|
|
3
|
+
import type { BrowserPluginOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
function isDefaultOptionKey(
|
|
6
|
+
key: string,
|
|
7
|
+
defaults: DefaultBrowserPluginOptions,
|
|
8
|
+
): key is keyof DefaultBrowserPluginOptions {
|
|
9
|
+
return key in defaults;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function validateOptionType(
|
|
13
|
+
key: keyof DefaultBrowserPluginOptions,
|
|
14
|
+
value: unknown,
|
|
15
|
+
expectedType: string,
|
|
16
|
+
): boolean {
|
|
17
|
+
const actualType = typeof value;
|
|
18
|
+
|
|
19
|
+
if (actualType !== expectedType && value !== undefined) {
|
|
20
|
+
console.warn(
|
|
21
|
+
`[${LOGGER_CONTEXT}] Invalid type for '${key}': expected ${expectedType}, got ${actualType}`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateOptions(
|
|
31
|
+
opts: Partial<BrowserPluginOptions> | undefined,
|
|
32
|
+
defaultOptions: DefaultBrowserPluginOptions,
|
|
33
|
+
): boolean {
|
|
34
|
+
if (!opts) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let hasInvalidTypes = false;
|
|
39
|
+
|
|
40
|
+
for (const key of Object.keys(opts)) {
|
|
41
|
+
if (isDefaultOptionKey(key, defaultOptions)) {
|
|
42
|
+
const expectedType = typeof defaultOptions[key];
|
|
43
|
+
const value = opts[key];
|
|
44
|
+
const isValid = validateOptionType(key, value, expectedType);
|
|
45
|
+
|
|
46
|
+
if (!isValid) {
|
|
47
|
+
hasInvalidTypes = true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (opts.useHash === true && "preserveHash" in opts) {
|
|
53
|
+
console.warn(`[${LOGGER_CONTEXT}] preserveHash ignored in hash mode`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (opts.useHash === false && "hashPrefix" in opts) {
|
|
57
|
+
const optsRecord = opts as unknown as Record<string, unknown>;
|
|
58
|
+
const hashPrefix = optsRecord.hashPrefix;
|
|
59
|
+
|
|
60
|
+
if (hashPrefix !== undefined && hashPrefix !== "") {
|
|
61
|
+
console.warn(`[${LOGGER_CONTEXT}] hashPrefix ignored in history mode`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return hasInvalidTypes;
|
|
66
|
+
}
|
package/src/utils.ts
DELETED
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
// packages/browser-plugin/modules/utils.ts
|
|
2
|
-
|
|
3
|
-
import { errorCodes } from "@real-router/core";
|
|
4
|
-
import { isStateStrict as isState } from "type-guards";
|
|
5
|
-
|
|
6
|
-
import { type DefaultBrowserPluginOptions, LOGGER_CONTEXT } from "./constants";
|
|
7
|
-
|
|
8
|
-
import type { BrowserPluginOptions, HistoryState, Browser } from "./types";
|
|
9
|
-
import type {
|
|
10
|
-
PluginApi,
|
|
11
|
-
Router,
|
|
12
|
-
NavigationOptions,
|
|
13
|
-
RouterError,
|
|
14
|
-
State,
|
|
15
|
-
} from "@real-router/core";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* No-op function for default callbacks
|
|
19
|
-
*/
|
|
20
|
-
export const noop = (): void => undefined;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Cache for escaped RegExp strings
|
|
24
|
-
*/
|
|
25
|
-
const escapeRegExpCache = new Map<string, string>();
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Escapes special RegExp characters in a string.
|
|
29
|
-
* Used to safely build RegExp from user-provided strings (hashPrefix, base).
|
|
30
|
-
*
|
|
31
|
-
* @param str - String to escape
|
|
32
|
-
* @returns Escaped string safe for RegExp construction
|
|
33
|
-
*/
|
|
34
|
-
export const escapeRegExp = (str: string): string => {
|
|
35
|
-
const cached = escapeRegExpCache.get(str);
|
|
36
|
-
|
|
37
|
-
if (cached !== undefined) {
|
|
38
|
-
return cached;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const escaped = str.replaceAll(/[$()*+.?[\\\]^{|}-]/g, String.raw`\$&`);
|
|
42
|
-
|
|
43
|
-
escapeRegExpCache.set(str, escaped);
|
|
44
|
-
|
|
45
|
-
return escaped;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Creates state from popstate event
|
|
50
|
-
*
|
|
51
|
-
* @param evt - PopStateEvent from browser
|
|
52
|
-
* @param api - PluginApi instance
|
|
53
|
-
* @param browser - Browser API instance
|
|
54
|
-
* @param options - Browser plugin options
|
|
55
|
-
* @returns Router state or undefined
|
|
56
|
-
*/
|
|
57
|
-
export function createStateFromEvent(
|
|
58
|
-
evt: PopStateEvent,
|
|
59
|
-
api: PluginApi,
|
|
60
|
-
browser: Browser,
|
|
61
|
-
options: BrowserPluginOptions,
|
|
62
|
-
): State | undefined {
|
|
63
|
-
const isNewState = !isState(evt.state);
|
|
64
|
-
|
|
65
|
-
if (isNewState) {
|
|
66
|
-
return api.matchPath(browser.getLocation(options));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return api.makeState(
|
|
70
|
-
evt.state.name,
|
|
71
|
-
evt.state.params,
|
|
72
|
-
evt.state.path,
|
|
73
|
-
{
|
|
74
|
-
...evt.state.meta,
|
|
75
|
-
params: evt.state.meta?.params ?? {},
|
|
76
|
-
},
|
|
77
|
-
evt.state.meta?.id,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Checks if transition should be skipped (same states)
|
|
83
|
-
*
|
|
84
|
-
* @param newState - New state from event
|
|
85
|
-
* @param currentState - Current router state
|
|
86
|
-
* @param router - Router instance
|
|
87
|
-
* @returns true if transition should be skipped
|
|
88
|
-
*/
|
|
89
|
-
export function shouldSkipTransition(
|
|
90
|
-
newState: State | undefined,
|
|
91
|
-
currentState: State | undefined,
|
|
92
|
-
router: Router,
|
|
93
|
-
): boolean {
|
|
94
|
-
if (!newState) {
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return !!(
|
|
99
|
-
currentState && router.areStatesEqual(newState, currentState, false)
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Handles missing state by navigating to default route
|
|
105
|
-
*
|
|
106
|
-
* @param router - Router instance
|
|
107
|
-
* @param api - Plugin API instance
|
|
108
|
-
* @param transitionOptions - Options for transition
|
|
109
|
-
* @returns true if handled, false if no default route
|
|
110
|
-
*/
|
|
111
|
-
export function handleMissingState(
|
|
112
|
-
router: Router,
|
|
113
|
-
api: PluginApi,
|
|
114
|
-
transitionOptions: NavigationOptions,
|
|
115
|
-
): boolean {
|
|
116
|
-
const routerOptions = api.getOptions();
|
|
117
|
-
const { defaultRoute } = routerOptions;
|
|
118
|
-
|
|
119
|
-
if (!defaultRoute) {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
void router.navigateToDefault({
|
|
124
|
-
...transitionOptions,
|
|
125
|
-
reload: true,
|
|
126
|
-
replace: true,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Updates browser state (pushState or replaceState)
|
|
134
|
-
*
|
|
135
|
-
* @param state - Router state
|
|
136
|
-
* @param url - URL to set
|
|
137
|
-
* @param replace - Whether to replace instead of push
|
|
138
|
-
* @param browser - Browser API instance
|
|
139
|
-
* @param options - Browser plugin options
|
|
140
|
-
*/
|
|
141
|
-
export function updateBrowserState(
|
|
142
|
-
state: State,
|
|
143
|
-
url: string,
|
|
144
|
-
replace: boolean,
|
|
145
|
-
browser: Browser,
|
|
146
|
-
options: BrowserPluginOptions,
|
|
147
|
-
): void {
|
|
148
|
-
const trimmedState: HistoryState = {
|
|
149
|
-
meta: state.meta,
|
|
150
|
-
name: state.name,
|
|
151
|
-
params: state.params,
|
|
152
|
-
path: state.path,
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const finalState: HistoryState =
|
|
156
|
-
options.mergeState && browser.getState()
|
|
157
|
-
? { ...browser.getState(), ...trimmedState }
|
|
158
|
-
: trimmedState;
|
|
159
|
-
|
|
160
|
-
if (replace) {
|
|
161
|
-
browser.replaceState(finalState, "", url);
|
|
162
|
-
} else {
|
|
163
|
-
browser.pushState(finalState, "", url);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Handles transition result (success or error)
|
|
169
|
-
*
|
|
170
|
-
* Success case is handled by the router FSM chain (TRANSITION_SUCCESS event).
|
|
171
|
-
* This function only handles error cases that need URL restoration.
|
|
172
|
-
*
|
|
173
|
-
* @param err - Router error or undefined if successful
|
|
174
|
-
* @param toState - Target state
|
|
175
|
-
* @param fromState - Source state
|
|
176
|
-
* @param isNewState - Whether this is a new state (not from history)
|
|
177
|
-
* @param router - Router instance
|
|
178
|
-
* @param browser - Browser API instance
|
|
179
|
-
* @param options - Browser plugin options
|
|
180
|
-
*/
|
|
181
|
-
export function handleTransitionResult(
|
|
182
|
-
err: RouterError | undefined,
|
|
183
|
-
toState: State | undefined,
|
|
184
|
-
fromState: State | undefined,
|
|
185
|
-
isNewState: boolean,
|
|
186
|
-
router: Router,
|
|
187
|
-
browser: Browser,
|
|
188
|
-
options: BrowserPluginOptions,
|
|
189
|
-
): void {
|
|
190
|
-
// Success case handled by the router FSM chain (TRANSITION_SUCCESS event)
|
|
191
|
-
if (!err) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Handle CANNOT_DEACTIVATE - restore previous URL
|
|
196
|
-
if (
|
|
197
|
-
err.code === errorCodes.CANNOT_DEACTIVATE &&
|
|
198
|
-
toState &&
|
|
199
|
-
fromState &&
|
|
200
|
-
!isNewState
|
|
201
|
-
) {
|
|
202
|
-
const url = router.buildUrl(fromState.name, fromState.params);
|
|
203
|
-
|
|
204
|
-
updateBrowserState(fromState, url, true, browser, options);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Type guard to check if a key exists in default options
|
|
210
|
-
*/
|
|
211
|
-
function isDefaultOptionKey(
|
|
212
|
-
key: string,
|
|
213
|
-
defaults: DefaultBrowserPluginOptions,
|
|
214
|
-
): key is keyof DefaultBrowserPluginOptions {
|
|
215
|
-
return key in defaults;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Validates that an option value has the correct type
|
|
220
|
-
*/
|
|
221
|
-
function validateOptionType(
|
|
222
|
-
key: keyof DefaultBrowserPluginOptions,
|
|
223
|
-
value: unknown,
|
|
224
|
-
expectedType: string,
|
|
225
|
-
): boolean {
|
|
226
|
-
const actualType = typeof value;
|
|
227
|
-
|
|
228
|
-
if (actualType !== expectedType && value !== undefined) {
|
|
229
|
-
console.warn(
|
|
230
|
-
`[${LOGGER_CONTEXT}] Invalid type for '${key}': expected ${expectedType}, got ${actualType}`,
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return true;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Validates browser plugin options and warns about conflicting configurations.
|
|
241
|
-
* TypeScript types prevent conflicts at compile-time, but runtime validation
|
|
242
|
-
* is needed for JavaScript users and dynamic configurations.
|
|
243
|
-
*
|
|
244
|
-
* IMPORTANT: This validates only user-provided options, not merged defaults.
|
|
245
|
-
*
|
|
246
|
-
* @returns true if invalid types detected, false otherwise
|
|
247
|
-
*/
|
|
248
|
-
export function validateOptions(
|
|
249
|
-
opts: Partial<BrowserPluginOptions> | undefined,
|
|
250
|
-
defaultOptions: DefaultBrowserPluginOptions,
|
|
251
|
-
): boolean {
|
|
252
|
-
if (!opts) {
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
let hasInvalidTypes = false;
|
|
257
|
-
|
|
258
|
-
// Validate option types against defaults
|
|
259
|
-
// Using Object.keys ensures we only check properties that actually exist
|
|
260
|
-
for (const key of Object.keys(opts)) {
|
|
261
|
-
if (isDefaultOptionKey(key, defaultOptions)) {
|
|
262
|
-
const expectedType = typeof defaultOptions[key];
|
|
263
|
-
const value = opts[key];
|
|
264
|
-
const isValid = validateOptionType(key, value, expectedType);
|
|
265
|
-
|
|
266
|
-
if (!isValid) {
|
|
267
|
-
hasInvalidTypes = true;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Check for hash mode conflicts
|
|
273
|
-
// Runtime validation for JS users - TypeScript prevents this at compile time
|
|
274
|
-
|
|
275
|
-
if (opts.useHash === true && "preserveHash" in opts) {
|
|
276
|
-
console.warn(`[${LOGGER_CONTEXT}] preserveHash ignored in hash mode`);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Check for history mode conflicts
|
|
280
|
-
// Runtime validation for JS users - TypeScript prevents this at compile time
|
|
281
|
-
|
|
282
|
-
if (opts.useHash === false && "hashPrefix" in opts) {
|
|
283
|
-
// Single type assertion needed: TypeScript narrows opts to HistoryModeOptions
|
|
284
|
-
// where hashPrefix is 'never', but we need to check it at runtime for JS users
|
|
285
|
-
const optsRecord = opts as unknown as Record<string, unknown>;
|
|
286
|
-
const hashPrefix = optsRecord.hashPrefix;
|
|
287
|
-
|
|
288
|
-
if (hashPrefix !== undefined && hashPrefix !== "") {
|
|
289
|
-
console.warn(`[${LOGGER_CONTEXT}] hashPrefix ignored in history mode`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return hasInvalidTypes;
|
|
294
|
-
}
|