@real-router/browser-plugin 0.8.0 → 0.9.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 -29
- package/dist/cjs/index.d.ts +7 -153
- 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 +7 -153
- 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 +4 -3
- package/src/constants.ts +2 -36
- package/src/factory.ts +27 -58
- package/src/index.ts +3 -1
- package/src/plugin.ts +54 -187
- package/src/types.ts +2 -179
- package/src/url-utils.ts +12 -80
- package/src/validation.ts +7 -63
- package/src/browser.ts +0 -128
- package/src/popstate-utils.ts +0 -60
package/src/plugin.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createPopstateHandler,
|
|
3
|
+
createPopstateLifecycle,
|
|
4
|
+
createStartInterceptor,
|
|
5
|
+
createReplaceHistoryState,
|
|
6
|
+
shouldReplaceHistory,
|
|
7
|
+
updateBrowserState,
|
|
8
|
+
} from "browser-env";
|
|
2
9
|
|
|
3
|
-
import { LOGGER_CONTEXT } from "./constants";
|
|
4
|
-
import { getRouteFromEvent, updateBrowserState } from "./popstate-utils";
|
|
5
10
|
import { buildUrl, urlToPath } from "./url-utils";
|
|
6
11
|
|
|
7
|
-
import type {
|
|
8
|
-
Browser,
|
|
9
|
-
BrowserPluginOptions,
|
|
10
|
-
RegExpCache,
|
|
11
|
-
SharedFactoryState,
|
|
12
|
-
URLParseOptions,
|
|
13
|
-
} from "./types";
|
|
12
|
+
import type { BrowserPluginOptions } from "./types";
|
|
14
13
|
import type {
|
|
15
14
|
NavigationOptions,
|
|
16
15
|
Params,
|
|
@@ -19,32 +18,20 @@ import type {
|
|
|
19
18
|
State,
|
|
20
19
|
Plugin,
|
|
21
20
|
} from "@real-router/core";
|
|
21
|
+
import type { Browser, SharedFactoryState } from "browser-env";
|
|
22
22
|
|
|
23
23
|
export class BrowserPlugin {
|
|
24
24
|
readonly #router: Router;
|
|
25
|
-
readonly #api: PluginApi;
|
|
26
|
-
readonly #options: BrowserPluginOptions;
|
|
27
25
|
readonly #browser: Browser;
|
|
28
|
-
readonly #regExpCache: RegExpCache;
|
|
29
|
-
readonly #prefix: string;
|
|
30
|
-
readonly #transitionOptions: {
|
|
31
|
-
source: string;
|
|
32
|
-
replace: true;
|
|
33
|
-
forceDeactivate?: boolean;
|
|
34
|
-
};
|
|
35
|
-
readonly #shared: SharedFactoryState;
|
|
36
|
-
|
|
37
|
-
#isTransitioning = false;
|
|
38
|
-
#deferredPopstateEvent: PopStateEvent | null = null;
|
|
39
26
|
readonly #removeStartInterceptor: () => void;
|
|
40
27
|
readonly #removeExtensions: () => void;
|
|
28
|
+
readonly #lifecycle: Pick<Plugin, "onStart" | "onStop" | "teardown">;
|
|
41
29
|
|
|
42
30
|
constructor(
|
|
43
31
|
router: Router,
|
|
44
32
|
api: PluginApi,
|
|
45
|
-
options: BrowserPluginOptions
|
|
33
|
+
options: Required<BrowserPluginOptions>,
|
|
46
34
|
browser: Browser,
|
|
47
|
-
regExpCache: RegExpCache,
|
|
48
35
|
transitionOptions: {
|
|
49
36
|
source: string;
|
|
50
37
|
replace: true;
|
|
@@ -53,199 +40,79 @@ export class BrowserPlugin {
|
|
|
53
40
|
shared: SharedFactoryState,
|
|
54
41
|
) {
|
|
55
42
|
this.#router = router;
|
|
56
|
-
this.#api = api;
|
|
57
|
-
this.#options = options;
|
|
58
43
|
this.#browser = browser;
|
|
59
|
-
this.#regExpCache = regExpCache;
|
|
60
|
-
this.#transitionOptions = transitionOptions;
|
|
61
|
-
this.#shared = shared;
|
|
62
|
-
|
|
63
|
-
const normalizedOptions = options as URLParseOptions;
|
|
64
44
|
|
|
65
|
-
this.#
|
|
45
|
+
this.#removeStartInterceptor = createStartInterceptor(api, browser);
|
|
66
46
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
(next, path) => next(path ?? this.#browser.getLocation(this.#options)),
|
|
70
|
-
);
|
|
47
|
+
const pluginBuildUrl = (route: string, params?: Params) => {
|
|
48
|
+
const path = router.buildPath(route, params);
|
|
71
49
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const path = this.#router.buildPath(route, params);
|
|
50
|
+
return buildUrl(path, options.base);
|
|
51
|
+
};
|
|
75
52
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
(this.#options as URLParseOptions).base,
|
|
79
|
-
this.#prefix,
|
|
80
|
-
);
|
|
81
|
-
},
|
|
53
|
+
this.#removeExtensions = api.extendRouter({
|
|
54
|
+
buildUrl: pluginBuildUrl,
|
|
82
55
|
matchUrl: (url: string) => {
|
|
83
|
-
const path = urlToPath(
|
|
84
|
-
url,
|
|
85
|
-
this.#options as URLParseOptions,
|
|
86
|
-
this.#regExpCache,
|
|
87
|
-
);
|
|
56
|
+
const path = urlToPath(url, options.base);
|
|
88
57
|
|
|
89
|
-
return path ?
|
|
58
|
+
return path ? api.matchPath(path) : undefined;
|
|
90
59
|
},
|
|
91
|
-
replaceHistoryState: (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
);
|
|
109
|
-
const url = this.#router.buildUrl(name, params);
|
|
60
|
+
replaceHistoryState: createReplaceHistoryState(
|
|
61
|
+
api,
|
|
62
|
+
router,
|
|
63
|
+
browser,
|
|
64
|
+
pluginBuildUrl,
|
|
65
|
+
),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const handler = createPopstateHandler({
|
|
69
|
+
router,
|
|
70
|
+
api,
|
|
71
|
+
browser,
|
|
72
|
+
transitionOptions,
|
|
73
|
+
loggerContext: "browser-plugin",
|
|
74
|
+
buildUrl: (name: string, params?: Params) =>
|
|
75
|
+
router.buildUrl(name, params),
|
|
76
|
+
});
|
|
110
77
|
|
|
111
|
-
|
|
78
|
+
this.#lifecycle = createPopstateLifecycle({
|
|
79
|
+
browser,
|
|
80
|
+
shared,
|
|
81
|
+
handler,
|
|
82
|
+
cleanup: () => {
|
|
83
|
+
this.#removeStartInterceptor();
|
|
84
|
+
this.#removeExtensions();
|
|
112
85
|
},
|
|
113
86
|
});
|
|
114
87
|
}
|
|
115
88
|
|
|
116
89
|
getPlugin(): Plugin {
|
|
117
90
|
return {
|
|
118
|
-
|
|
119
|
-
if (this.#shared.removePopStateListener) {
|
|
120
|
-
this.#shared.removePopStateListener();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
this.#shared.removePopStateListener = this.#browser.addPopstateListener(
|
|
124
|
-
(evt: PopStateEvent) => void this.#onPopState(evt),
|
|
125
|
-
);
|
|
126
|
-
},
|
|
127
|
-
|
|
128
|
-
onStop: () => {
|
|
129
|
-
if (this.#shared.removePopStateListener) {
|
|
130
|
-
this.#shared.removePopStateListener();
|
|
131
|
-
this.#shared.removePopStateListener = undefined;
|
|
132
|
-
}
|
|
133
|
-
},
|
|
91
|
+
...this.#lifecycle,
|
|
134
92
|
|
|
135
93
|
onTransitionSuccess: (
|
|
136
94
|
toState: State,
|
|
137
95
|
fromState: State | undefined,
|
|
138
96
|
navOptions: NavigationOptions,
|
|
139
97
|
) => {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
98
|
+
const replaceHistory = shouldReplaceHistory(
|
|
99
|
+
navOptions,
|
|
100
|
+
toState,
|
|
101
|
+
fromState,
|
|
102
|
+
this.#router,
|
|
103
|
+
);
|
|
144
104
|
|
|
145
105
|
const url = this.#router.buildUrl(toState.name, toState.params);
|
|
146
106
|
|
|
147
107
|
const shouldPreserveHash =
|
|
148
|
-
|
|
149
|
-
(!fromState || fromState.path === toState.path);
|
|
108
|
+
!fromState || fromState.path === toState.path;
|
|
150
109
|
|
|
151
110
|
const finalUrl = shouldPreserveHash
|
|
152
111
|
? url + this.#browser.getHash()
|
|
153
112
|
: url;
|
|
154
113
|
|
|
155
|
-
updateBrowserState(
|
|
156
|
-
toState,
|
|
157
|
-
finalUrl,
|
|
158
|
-
shouldReplaceHistory,
|
|
159
|
-
this.#browser,
|
|
160
|
-
);
|
|
161
|
-
},
|
|
162
|
-
|
|
163
|
-
teardown: () => {
|
|
164
|
-
if (this.#shared.removePopStateListener) {
|
|
165
|
-
this.#shared.removePopStateListener();
|
|
166
|
-
this.#shared.removePopStateListener = undefined;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
this.#removeStartInterceptor();
|
|
170
|
-
this.#removeExtensions();
|
|
114
|
+
updateBrowserState(toState, finalUrl, replaceHistory, this.#browser);
|
|
171
115
|
},
|
|
172
116
|
};
|
|
173
117
|
}
|
|
174
|
-
|
|
175
|
-
#processDeferredEvent(): void {
|
|
176
|
-
if (this.#deferredPopstateEvent) {
|
|
177
|
-
const event = this.#deferredPopstateEvent;
|
|
178
|
-
|
|
179
|
-
this.#deferredPopstateEvent = null;
|
|
180
|
-
console.warn(`[${LOGGER_CONTEXT}] Processing deferred popstate event`);
|
|
181
|
-
void this.#onPopState(event);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async #onPopState(evt: PopStateEvent): Promise<void> {
|
|
186
|
-
if (this.#isTransitioning) {
|
|
187
|
-
console.warn(
|
|
188
|
-
`[${LOGGER_CONTEXT}] Transition in progress, deferring popstate event`,
|
|
189
|
-
);
|
|
190
|
-
this.#deferredPopstateEvent = evt;
|
|
191
|
-
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
this.#isTransitioning = true;
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
const route = getRouteFromEvent(
|
|
199
|
-
evt,
|
|
200
|
-
this.#api,
|
|
201
|
-
this.#browser,
|
|
202
|
-
this.#options,
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
// eslint-disable-next-line unicorn/prefer-ternary
|
|
206
|
-
if (route) {
|
|
207
|
-
await this.#router.navigate(
|
|
208
|
-
route.name,
|
|
209
|
-
route.params,
|
|
210
|
-
this.#transitionOptions,
|
|
211
|
-
);
|
|
212
|
-
} else {
|
|
213
|
-
await this.#router.navigateToDefault({
|
|
214
|
-
...this.#transitionOptions,
|
|
215
|
-
reload: true,
|
|
216
|
-
replace: true,
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
} catch (error) {
|
|
220
|
-
if (!(error instanceof RouterError)) {
|
|
221
|
-
this.#recoverFromCriticalError(error);
|
|
222
|
-
}
|
|
223
|
-
} finally {
|
|
224
|
-
this.#isTransitioning = false;
|
|
225
|
-
this.#processDeferredEvent();
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
#recoverFromCriticalError(error: unknown): void {
|
|
230
|
-
console.error(`[${LOGGER_CONTEXT}] Critical error in onPopState`, error);
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
const currentState = this.#router.getState();
|
|
234
|
-
|
|
235
|
-
/* v8 ignore next -- @preserve: router always has state after start(); defensive guard for edge cases */
|
|
236
|
-
if (currentState) {
|
|
237
|
-
const url = this.#router.buildUrl(
|
|
238
|
-
currentState.name,
|
|
239
|
-
currentState.params,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
this.#browser.replaceState(currentState, url);
|
|
243
|
-
}
|
|
244
|
-
} catch (recoveryError) {
|
|
245
|
-
console.error(
|
|
246
|
-
`[${LOGGER_CONTEXT}] Failed to recover from critical error`,
|
|
247
|
-
recoveryError,
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
118
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// packages/browser-plugin/src/types.ts
|
|
2
2
|
|
|
3
|
-
import type { State } from "@real-router/core";
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
|
-
*
|
|
4
|
+
* Browser plugin configuration.
|
|
7
5
|
*/
|
|
8
|
-
interface
|
|
6
|
+
export interface BrowserPluginOptions {
|
|
9
7
|
/**
|
|
10
8
|
* Force deactivation of current route even if canDeactivate returns false.
|
|
11
9
|
*
|
|
@@ -20,178 +18,3 @@ interface BaseBrowserPluginOptions {
|
|
|
20
18
|
*/
|
|
21
19
|
base?: string;
|
|
22
20
|
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Hash-based routing configuration.
|
|
26
|
-
* Uses URL hash for navigation (e.g., example.com/#/path).
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```ts
|
|
30
|
-
* // Standard hash routing
|
|
31
|
-
* browserPluginFactory({ useHash: true })
|
|
32
|
-
* // → example.com/#/users
|
|
33
|
-
*
|
|
34
|
-
* // Hash routing with prefix
|
|
35
|
-
* browserPluginFactory({ useHash: true, hashPrefix: "!" })
|
|
36
|
-
* // → example.com/#!/users
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
export interface HashModeOptions extends BaseBrowserPluginOptions {
|
|
40
|
-
/**
|
|
41
|
-
* Enable hash-based routing
|
|
42
|
-
*/
|
|
43
|
-
useHash: true;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Prefix for hash (e.g., "!" for "#!/path").
|
|
47
|
-
* Only valid when useHash is true.
|
|
48
|
-
*
|
|
49
|
-
* @default ""
|
|
50
|
-
*/
|
|
51
|
-
hashPrefix?: string;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Not available in hash mode.
|
|
55
|
-
* Hash preservation only works with HTML5 History API.
|
|
56
|
-
* Use `useHash: false` to enable this option.
|
|
57
|
-
*/
|
|
58
|
-
preserveHash?: never;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* HTML5 History API routing configuration.
|
|
63
|
-
* Uses pushState/replaceState for navigation (e.g., example.com/path).
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* ```ts
|
|
67
|
-
* // Standard history routing
|
|
68
|
-
* browserPluginFactory({ useHash: false })
|
|
69
|
-
* // → example.com/users
|
|
70
|
-
*
|
|
71
|
-
* // Preserve URL hash fragments
|
|
72
|
-
* browserPluginFactory({ useHash: false, preserveHash: true })
|
|
73
|
-
* // → example.com/users#section
|
|
74
|
-
* ```
|
|
75
|
-
*/
|
|
76
|
-
export interface HistoryModeOptions extends BaseBrowserPluginOptions {
|
|
77
|
-
/**
|
|
78
|
-
* Disable hash-based routing (use HTML5 History API)
|
|
79
|
-
*
|
|
80
|
-
* @default false
|
|
81
|
-
*/
|
|
82
|
-
useHash?: false;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Preserve URL hash fragment on initial navigation.
|
|
86
|
-
* Only valid when useHash is false.
|
|
87
|
-
*
|
|
88
|
-
* @default true
|
|
89
|
-
*/
|
|
90
|
-
preserveHash?: boolean;
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Not available in history mode.
|
|
94
|
-
* Hash prefix only works with hash-based routing.
|
|
95
|
-
* Use `useHash: true` to enable this option.
|
|
96
|
-
*/
|
|
97
|
-
hashPrefix?: never;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Type-safe browser plugin configuration.
|
|
102
|
-
*
|
|
103
|
-
* Uses discriminated union to prevent conflicting options:
|
|
104
|
-
* - Hash mode (useHash: true): allows hashPrefix, forbids preserveHash
|
|
105
|
-
* - History mode (useHash: false): allows preserveHash, forbids hashPrefix
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* ```ts
|
|
109
|
-
* // ✅ Valid: Hash mode with prefix
|
|
110
|
-
* const config1: BrowserPluginOptions = {
|
|
111
|
-
* useHash: true,
|
|
112
|
-
* hashPrefix: "!"
|
|
113
|
-
* };
|
|
114
|
-
*
|
|
115
|
-
* // ✅ Valid: History mode with hash preservation
|
|
116
|
-
* const config2: BrowserPluginOptions = {
|
|
117
|
-
* useHash: false,
|
|
118
|
-
* preserveHash: true
|
|
119
|
-
* };
|
|
120
|
-
*
|
|
121
|
-
* // ❌ Error: Cannot use preserveHash with hash mode
|
|
122
|
-
* const config3: BrowserPluginOptions = {
|
|
123
|
-
* useHash: true,
|
|
124
|
-
* preserveHash: true // Type error!
|
|
125
|
-
* };
|
|
126
|
-
*
|
|
127
|
-
* // ❌ Error: Cannot use hashPrefix with history mode
|
|
128
|
-
* const config4: BrowserPluginOptions = {
|
|
129
|
-
* useHash: false,
|
|
130
|
-
* hashPrefix: "!" // Type error!
|
|
131
|
-
* };
|
|
132
|
-
* ```
|
|
133
|
-
*/
|
|
134
|
-
export type BrowserPluginOptions = HashModeOptions | HistoryModeOptions;
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Browser API abstraction for cross-environment compatibility.
|
|
138
|
-
* Provides same interface in browser and SSR contexts.
|
|
139
|
-
*/
|
|
140
|
-
export interface Browser {
|
|
141
|
-
/**
|
|
142
|
-
* Pushes new state to browser history
|
|
143
|
-
*
|
|
144
|
-
* @param state - History state object
|
|
145
|
-
* @param path - URL path
|
|
146
|
-
*/
|
|
147
|
-
pushState: (state: State, path: string) => void;
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Replaces current history state
|
|
151
|
-
*
|
|
152
|
-
* @param state - History state object
|
|
153
|
-
* @param path - URL path
|
|
154
|
-
*/
|
|
155
|
-
replaceState: (state: State, path: string) => void;
|
|
156
|
-
|
|
157
|
-
addPopstateListener: (fn: (evt: PopStateEvent) => void) => () => void;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Gets current location path respecting plugin options
|
|
161
|
-
*
|
|
162
|
-
* @param opts - Plugin options
|
|
163
|
-
* @returns Current path string
|
|
164
|
-
*/
|
|
165
|
-
getLocation: (opts: BrowserPluginOptions) => string;
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Gets current URL hash
|
|
169
|
-
*
|
|
170
|
-
* @returns Hash string (including #)
|
|
171
|
-
*/
|
|
172
|
-
getHash: () => string;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
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.
|
|
194
|
-
*/
|
|
195
|
-
export interface SharedFactoryState {
|
|
196
|
-
removePopStateListener: (() => void) | undefined;
|
|
197
|
-
}
|
package/src/url-utils.ts
CHANGED
|
@@ -1,43 +1,12 @@
|
|
|
1
1
|
// packages/browser-plugin/src/url-utils.ts
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
}
|
|
3
|
+
import { safeParseUrl } from "browser-env";
|
|
15
4
|
|
|
16
|
-
|
|
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
|
-
}
|
|
5
|
+
import { LOGGER_CONTEXT } from "./constants";
|
|
37
6
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const stripped = pathname.
|
|
7
|
+
export function extractPath(pathname: string, base: string): string {
|
|
8
|
+
if (base && pathname.startsWith(base)) {
|
|
9
|
+
const stripped = pathname.slice(base.length);
|
|
41
10
|
|
|
42
11
|
return stripped.startsWith("/") ? stripped : `/${stripped}`;
|
|
43
12
|
}
|
|
@@ -45,51 +14,14 @@ export function extractPath(
|
|
|
45
14
|
return pathname;
|
|
46
15
|
}
|
|
47
16
|
|
|
48
|
-
export function
|
|
49
|
-
|
|
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
|
-
}
|
|
17
|
+
export function buildUrl(path: string, base: string): string {
|
|
18
|
+
return base + path;
|
|
71
19
|
}
|
|
72
20
|
|
|
73
|
-
export function
|
|
74
|
-
|
|
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);
|
|
21
|
+
export function urlToPath(url: string, base: string): string | null {
|
|
22
|
+
const parsedUrl = safeParseUrl(url, LOGGER_CONTEXT);
|
|
91
23
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
24
|
+
return parsedUrl
|
|
25
|
+
? extractPath(parsedUrl.pathname, base) + parsedUrl.search
|
|
26
|
+
: null;
|
|
95
27
|
}
|
package/src/validation.ts
CHANGED
|
@@ -1,66 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createOptionsValidator } from "browser-env";
|
|
2
2
|
|
|
3
|
-
import
|
|
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
|
-
}
|
|
3
|
+
import { LOGGER_CONTEXT, defaultOptions } from "./constants";
|
|
37
4
|
|
|
38
|
-
|
|
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
|
-
}
|
|
5
|
+
import type { BrowserPluginOptions } from "./types";
|
|
64
6
|
|
|
65
|
-
|
|
66
|
-
|
|
7
|
+
export const validateOptions = createOptionsValidator<BrowserPluginOptions>(
|
|
8
|
+
defaultOptions,
|
|
9
|
+
LOGGER_CONTEXT,
|
|
10
|
+
);
|