@esmx/router 3.0.0-rc.17 → 3.0.0-rc.19
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/LICENSE +1 -1
- package/README.md +70 -0
- package/README.zh-CN.md +70 -0
- package/dist/error.d.ts +23 -0
- package/dist/error.mjs +61 -0
- package/dist/increment-id.d.ts +7 -0
- package/dist/increment-id.mjs +11 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.mjs +14 -3
- package/dist/index.test.mjs +8 -0
- package/dist/location.d.ts +15 -0
- package/dist/location.mjs +53 -0
- package/dist/location.test.d.ts +8 -0
- package/dist/location.test.mjs +370 -0
- package/dist/matcher.d.ts +3 -0
- package/dist/matcher.mjs +44 -0
- package/dist/matcher.test.mjs +1492 -0
- package/dist/micro-app.d.ts +18 -0
- package/dist/micro-app.dom.test.d.ts +1 -0
- package/dist/micro-app.dom.test.mjs +532 -0
- package/dist/micro-app.mjs +80 -0
- package/dist/navigation.d.ts +43 -0
- package/dist/navigation.mjs +143 -0
- package/dist/navigation.test.d.ts +1 -0
- package/dist/navigation.test.mjs +681 -0
- package/dist/options.d.ts +4 -0
- package/dist/options.mjs +88 -0
- package/dist/route-task.d.ts +40 -0
- package/dist/route-task.mjs +75 -0
- package/dist/route-task.test.d.ts +1 -0
- package/dist/route-task.test.mjs +673 -0
- package/dist/route-transition.d.ts +53 -0
- package/dist/route-transition.mjs +307 -0
- package/dist/route-transition.test.d.ts +1 -0
- package/dist/route-transition.test.mjs +146 -0
- package/dist/route.d.ts +72 -0
- package/dist/route.mjs +194 -0
- package/dist/route.test.d.ts +1 -0
- package/dist/route.test.mjs +1664 -0
- package/dist/router-back.test.d.ts +1 -0
- package/dist/router-back.test.mjs +361 -0
- package/dist/router-forward.test.d.ts +1 -0
- package/dist/router-forward.test.mjs +376 -0
- package/dist/router-go.test.d.ts +1 -0
- package/dist/router-go.test.mjs +73 -0
- package/dist/router-guards-cleanup.test.d.ts +1 -0
- package/dist/router-guards-cleanup.test.mjs +437 -0
- package/dist/router-link.d.ts +10 -0
- package/dist/router-link.mjs +126 -0
- package/dist/router-push.test.d.ts +1 -0
- package/dist/router-push.test.mjs +115 -0
- package/dist/router-replace.test.d.ts +1 -0
- package/dist/router-replace.test.mjs +114 -0
- package/dist/router-resolve.test.d.ts +1 -0
- package/dist/router-resolve.test.mjs +393 -0
- package/dist/router-restart-app.dom.test.d.ts +1 -0
- package/dist/router-restart-app.dom.test.mjs +616 -0
- package/dist/router-window-navigation.test.d.ts +1 -0
- package/dist/router-window-navigation.test.mjs +359 -0
- package/dist/router.d.ts +109 -102
- package/dist/router.mjs +260 -361
- package/dist/types.d.ts +246 -0
- package/dist/types.mjs +18 -0
- package/dist/util.d.ts +26 -0
- package/dist/util.mjs +53 -0
- package/dist/util.test.d.ts +1 -0
- package/dist/util.test.mjs +1020 -0
- package/package.json +10 -13
- package/src/error.ts +84 -0
- package/src/increment-id.ts +12 -0
- package/src/index.test.ts +9 -0
- package/src/index.ts +54 -3
- package/src/location.test.ts +406 -0
- package/src/location.ts +96 -0
- package/src/matcher.test.ts +1685 -0
- package/src/matcher.ts +59 -0
- package/src/micro-app.dom.test.ts +708 -0
- package/src/micro-app.ts +101 -0
- package/src/navigation.test.ts +858 -0
- package/src/navigation.ts +195 -0
- package/src/options.ts +131 -0
- package/src/route-task.test.ts +901 -0
- package/src/route-task.ts +105 -0
- package/src/route-transition.test.ts +178 -0
- package/src/route-transition.ts +425 -0
- package/src/route.test.ts +2014 -0
- package/src/route.ts +308 -0
- package/src/router-back.test.ts +487 -0
- package/src/router-forward.test.ts +506 -0
- package/src/router-go.test.ts +91 -0
- package/src/router-guards-cleanup.test.ts +595 -0
- package/src/router-link.ts +235 -0
- package/src/router-push.test.ts +140 -0
- package/src/router-replace.test.ts +139 -0
- package/src/router-resolve.test.ts +475 -0
- package/src/router-restart-app.dom.test.ts +783 -0
- package/src/router-window-navigation.test.ts +457 -0
- package/src/router.ts +289 -470
- package/src/types.ts +341 -0
- package/src/util.test.ts +1262 -0
- package/src/util.ts +116 -0
- package/dist/history/abstract.d.ts +0 -29
- package/dist/history/abstract.mjs +0 -107
- package/dist/history/base.d.ts +0 -79
- package/dist/history/base.mjs +0 -275
- package/dist/history/html.d.ts +0 -22
- package/dist/history/html.mjs +0 -183
- package/dist/history/index.d.ts +0 -7
- package/dist/history/index.mjs +0 -16
- package/dist/matcher/create-matcher.d.ts +0 -5
- package/dist/matcher/create-matcher.mjs +0 -218
- package/dist/matcher/create-matcher.spec.mjs +0 -0
- package/dist/matcher/index.d.ts +0 -1
- package/dist/matcher/index.mjs +0 -1
- package/dist/task-pipe/index.d.ts +0 -1
- package/dist/task-pipe/index.mjs +0 -1
- package/dist/task-pipe/task.d.ts +0 -30
- package/dist/task-pipe/task.mjs +0 -66
- package/dist/utils/bom.d.ts +0 -5
- package/dist/utils/bom.mjs +0 -10
- package/dist/utils/encoding.d.ts +0 -48
- package/dist/utils/encoding.mjs +0 -44
- package/dist/utils/guards.d.ts +0 -9
- package/dist/utils/guards.mjs +0 -12
- package/dist/utils/index.d.ts +0 -7
- package/dist/utils/index.mjs +0 -27
- package/dist/utils/path.d.ts +0 -60
- package/dist/utils/path.mjs +0 -281
- package/dist/utils/path.spec.mjs +0 -27
- package/dist/utils/scroll.d.ts +0 -25
- package/dist/utils/scroll.mjs +0 -59
- package/dist/utils/utils.d.ts +0 -16
- package/dist/utils/utils.mjs +0 -11
- package/dist/utils/warn.d.ts +0 -2
- package/dist/utils/warn.mjs +0 -12
- package/src/history/abstract.ts +0 -149
- package/src/history/base.ts +0 -408
- package/src/history/html.ts +0 -228
- package/src/history/index.ts +0 -20
- package/src/matcher/create-matcher.spec.ts +0 -3
- package/src/matcher/create-matcher.ts +0 -293
- package/src/matcher/index.ts +0 -1
- package/src/task-pipe/index.ts +0 -1
- package/src/task-pipe/task.ts +0 -97
- package/src/utils/bom.ts +0 -14
- package/src/utils/encoding.ts +0 -153
- package/src/utils/guards.ts +0 -25
- package/src/utils/index.ts +0 -27
- package/src/utils/path.spec.ts +0 -32
- package/src/utils/path.ts +0 -417
- package/src/utils/scroll.ts +0 -120
- package/src/utils/utils.ts +0 -30
- package/src/utils/warn.ts +0 -13
- /package/dist/{matcher/create-matcher.spec.d.ts → index.test.d.ts} +0 -0
- /package/dist/{utils/path.spec.d.ts → matcher.test.d.ts} +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { PAGE_ID } from './increment-id';
|
|
2
|
+
import { RouterMode } from './types';
|
|
3
|
+
import type { Route, RouteState, RouterParsedOptions } 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 = Object.freeze({
|
|
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 = Object.freeze({
|
|
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 go(index: number): Promise<NavigationGoResult> {
|
|
90
|
+
if (this._promiseResolve) {
|
|
91
|
+
return Promise.resolve(null);
|
|
92
|
+
}
|
|
93
|
+
return new Promise<NavigationGoResult>((resolve) => {
|
|
94
|
+
this._promiseResolve = (url, state) => {
|
|
95
|
+
this._promiseResolve = null;
|
|
96
|
+
if (typeof url !== 'string') return resolve(null);
|
|
97
|
+
resolve({ type: 'success', url, state: state || {} });
|
|
98
|
+
};
|
|
99
|
+
setTimeout(this._promiseResolve, 80);
|
|
100
|
+
this._history.go(index);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
public forward(): Promise<NavigationGoResult> {
|
|
104
|
+
return this.go(1);
|
|
105
|
+
}
|
|
106
|
+
public back(): Promise<NavigationGoResult> {
|
|
107
|
+
return this.go(-1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public destroy() {
|
|
111
|
+
this._promiseResolve?.();
|
|
112
|
+
this._unSubscribePopState();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class MemoryHistory implements History {
|
|
117
|
+
private _entries: Array<{ state: any; url: string }> = [];
|
|
118
|
+
private _index = -1;
|
|
119
|
+
private get _curEntry() {
|
|
120
|
+
const idx = this._index;
|
|
121
|
+
if (idx < 0 || idx >= this.length) return null;
|
|
122
|
+
return this._entries[idx];
|
|
123
|
+
}
|
|
124
|
+
private readonly _popStateCbs = new Set<NavigationSubscribe>();
|
|
125
|
+
public scrollRestoration: ScrollRestoration = 'auto';
|
|
126
|
+
// Return null when no current entry to align with browser history.state behavior
|
|
127
|
+
// Browser history.state can be null when no state was provided
|
|
128
|
+
public get state() {
|
|
129
|
+
return this._curEntry?.state ?? null;
|
|
130
|
+
}
|
|
131
|
+
public get url() {
|
|
132
|
+
return this._curEntry?.url ?? '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
constructor() {
|
|
136
|
+
this.pushState(null, '', '/');
|
|
137
|
+
}
|
|
138
|
+
public get length() {
|
|
139
|
+
return this._entries.length;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public pushState(
|
|
143
|
+
data: any,
|
|
144
|
+
unused: string,
|
|
145
|
+
url?: string | URL | null
|
|
146
|
+
): void {
|
|
147
|
+
// Remove all entries after the current position
|
|
148
|
+
this._entries.splice(this._index + 1);
|
|
149
|
+
this._entries.push({ state: data, url: url?.toString() ?? this.url });
|
|
150
|
+
this._index = this._entries.length - 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public replaceState(
|
|
154
|
+
data: any,
|
|
155
|
+
unused: string,
|
|
156
|
+
url?: string | URL | null
|
|
157
|
+
): void {
|
|
158
|
+
const curEntry = this._curEntry;
|
|
159
|
+
if (!curEntry) return;
|
|
160
|
+
curEntry.state = { ...data };
|
|
161
|
+
if (url) curEntry.url = url.toString();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public back(): void {
|
|
165
|
+
this.go(-1);
|
|
166
|
+
}
|
|
167
|
+
public forward(): void {
|
|
168
|
+
this.go(1);
|
|
169
|
+
}
|
|
170
|
+
public go(delta?: number): void {
|
|
171
|
+
if (!delta) return;
|
|
172
|
+
const newIdx = this._index + delta;
|
|
173
|
+
if (newIdx < 0 || newIdx >= this.length) return;
|
|
174
|
+
this._index = newIdx;
|
|
175
|
+
const entry = this._curEntry!;
|
|
176
|
+
// Simulate the async popstate event of html history as closely as possible
|
|
177
|
+
setTimeout(() => {
|
|
178
|
+
this._popStateCbs.forEach((cb) => cb(entry.url, entry.state));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public onPopState(cb: NavigationSubscribe) {
|
|
183
|
+
if (typeof cb !== 'function') return () => {};
|
|
184
|
+
this._popStateCbs.add(cb);
|
|
185
|
+
return () => this._popStateCbs.delete(cb);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function subscribeHtmlHistory(cb: NavigationSubscribe) {
|
|
190
|
+
// Use history.state || {} to handle null state from browser history
|
|
191
|
+
// Browser history.state can be null, but we normalize it to empty object
|
|
192
|
+
const wrapper = () => cb(location.href, history.state || {});
|
|
193
|
+
window.addEventListener('popstate', wrapper);
|
|
194
|
+
return () => window.removeEventListener('popstate', wrapper);
|
|
195
|
+
}
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createMatcher } from './matcher';
|
|
2
|
+
import type { Router } from './router';
|
|
3
|
+
import { RouterMode } from './types';
|
|
4
|
+
import type { Route, RouterOptions, RouterParsedOptions } 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://www.esmnext.com/';
|
|
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://www.esmnext.com/`
|
|
50
|
+
);
|
|
51
|
+
base = new URL('https://www.esmnext.com/');
|
|
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 = Array.from(options.routes ?? []);
|
|
64
|
+
return Object.freeze<RouterParsedOptions>({
|
|
65
|
+
rootStyle: options.rootStyle || false,
|
|
66
|
+
root: options.root || '',
|
|
67
|
+
context: options.context || {},
|
|
68
|
+
req: options.req || null,
|
|
69
|
+
res: options.res || null,
|
|
70
|
+
layer: options.layer || false,
|
|
71
|
+
zIndex: options.zIndex || 10000,
|
|
72
|
+
base,
|
|
73
|
+
mode: isBrowser
|
|
74
|
+
? (options.mode ?? RouterMode.history)
|
|
75
|
+
: RouterMode.memory,
|
|
76
|
+
routes,
|
|
77
|
+
apps:
|
|
78
|
+
typeof options.apps === 'function'
|
|
79
|
+
? options.apps
|
|
80
|
+
: Object.assign({}, options.apps),
|
|
81
|
+
matcher: createMatcher(routes),
|
|
82
|
+
normalizeURL: options.normalizeURL ?? ((url) => url),
|
|
83
|
+
fallback: options.fallback ?? fallback,
|
|
84
|
+
handleBackBoundary: options.handleBackBoundary ?? (() => {}),
|
|
85
|
+
handleLayerClose: options.handleLayerClose ?? (() => {})
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function fallback(to: Route, from: Route | null, router: Router) {
|
|
90
|
+
const href = to.url.href;
|
|
91
|
+
|
|
92
|
+
// Server-side environment: handle application-level redirects and status codes
|
|
93
|
+
if (!isBrowser && router?.res) {
|
|
94
|
+
// Determine status code: prioritize route-specified code, default to 302 temporary redirect
|
|
95
|
+
let statusCode = 302;
|
|
96
|
+
|
|
97
|
+
// Validate redirect status code (3xx series)
|
|
98
|
+
const validRedirectCodes = [300, 301, 302, 303, 304, 307, 308];
|
|
99
|
+
if (to.statusCode && validRedirectCodes.includes(to.statusCode)) {
|
|
100
|
+
statusCode = to.statusCode;
|
|
101
|
+
} else if (to.statusCode) {
|
|
102
|
+
console.warn(
|
|
103
|
+
`Invalid redirect status code ${to.statusCode}, using default 302`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Set redirect response
|
|
108
|
+
router.res.statusCode = statusCode;
|
|
109
|
+
router.res.setHeader('Location', href);
|
|
110
|
+
router.res.end();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Client-side environment: handle browser navigation
|
|
115
|
+
if (isBrowser) {
|
|
116
|
+
if (to.isPush) {
|
|
117
|
+
try {
|
|
118
|
+
const newWindow = window.open(href);
|
|
119
|
+
if (!newWindow) {
|
|
120
|
+
location.href = href;
|
|
121
|
+
} else {
|
|
122
|
+
newWindow.opener = null; // Sever the relationship between the new window and the current one
|
|
123
|
+
}
|
|
124
|
+
return newWindow;
|
|
125
|
+
} catch {}
|
|
126
|
+
}
|
|
127
|
+
location.href = href;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Do nothing in a server environment without a res context
|
|
131
|
+
}
|