@esmx/router 3.0.0-rc.12 → 3.0.0-rc.120

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 (105) hide show
  1. package/README.md +158 -0
  2. package/README.zh-CN.md +158 -0
  3. package/dist/error.d.ts +23 -0
  4. package/dist/error.mjs +64 -0
  5. package/dist/increment-id.d.ts +7 -0
  6. package/dist/increment-id.mjs +16 -0
  7. package/dist/index.d.ts +14 -3
  8. package/dist/index.mjs +13 -3
  9. package/dist/location.d.ts +22 -0
  10. package/dist/location.mjs +64 -0
  11. package/dist/matcher.d.ts +4 -0
  12. package/dist/matcher.mjs +49 -0
  13. package/dist/micro-app.d.ts +20 -0
  14. package/dist/micro-app.mjs +132 -0
  15. package/dist/navigation.d.ts +45 -0
  16. package/dist/navigation.mjs +153 -0
  17. package/dist/options.d.ts +4 -0
  18. package/dist/options.mjs +95 -0
  19. package/dist/route-task.d.ts +40 -0
  20. package/dist/route-task.mjs +77 -0
  21. package/dist/route-transition.d.ts +54 -0
  22. package/dist/route-transition.mjs +362 -0
  23. package/dist/route.d.ts +77 -0
  24. package/dist/route.mjs +223 -0
  25. package/dist/router-link.d.ts +10 -0
  26. package/dist/router-link.mjs +142 -0
  27. package/dist/router.d.ts +113 -102
  28. package/dist/router.mjs +321 -354
  29. package/dist/scroll.d.ts +33 -0
  30. package/dist/scroll.mjs +50 -0
  31. package/dist/types.d.ts +312 -0
  32. package/dist/types.mjs +18 -0
  33. package/dist/util.d.ts +32 -0
  34. package/dist/util.mjs +100 -0
  35. package/package.json +42 -15
  36. package/src/error.ts +84 -0
  37. package/src/increment-id.ts +12 -0
  38. package/src/index.ts +67 -3
  39. package/src/location.ts +124 -0
  40. package/src/matcher.ts +71 -0
  41. package/src/micro-app.ts +153 -0
  42. package/src/navigation.ts +202 -0
  43. package/src/options.ts +136 -0
  44. package/src/route-task.ts +102 -0
  45. package/src/route-transition.ts +480 -0
  46. package/src/route.ts +335 -0
  47. package/src/router-link.ts +241 -0
  48. package/src/router.ts +351 -467
  49. package/src/scroll.ts +106 -0
  50. package/src/types.ts +415 -0
  51. package/src/util.ts +184 -0
  52. package/dist/history/abstract.d.ts +0 -29
  53. package/dist/history/abstract.mjs +0 -107
  54. package/dist/history/base.d.ts +0 -79
  55. package/dist/history/base.mjs +0 -275
  56. package/dist/history/html.d.ts +0 -22
  57. package/dist/history/html.mjs +0 -181
  58. package/dist/history/index.d.ts +0 -7
  59. package/dist/history/index.mjs +0 -16
  60. package/dist/matcher/create-matcher.d.ts +0 -5
  61. package/dist/matcher/create-matcher.mjs +0 -218
  62. package/dist/matcher/create-matcher.spec.d.ts +0 -1
  63. package/dist/matcher/create-matcher.spec.mjs +0 -0
  64. package/dist/matcher/index.d.ts +0 -1
  65. package/dist/matcher/index.mjs +0 -1
  66. package/dist/task-pipe/index.d.ts +0 -1
  67. package/dist/task-pipe/index.mjs +0 -1
  68. package/dist/task-pipe/task.d.ts +0 -30
  69. package/dist/task-pipe/task.mjs +0 -66
  70. package/dist/utils/bom.d.ts +0 -5
  71. package/dist/utils/bom.mjs +0 -10
  72. package/dist/utils/encoding.d.ts +0 -48
  73. package/dist/utils/encoding.mjs +0 -44
  74. package/dist/utils/guards.d.ts +0 -9
  75. package/dist/utils/guards.mjs +0 -12
  76. package/dist/utils/index.d.ts +0 -7
  77. package/dist/utils/index.mjs +0 -27
  78. package/dist/utils/path.d.ts +0 -60
  79. package/dist/utils/path.mjs +0 -264
  80. package/dist/utils/path.spec.d.ts +0 -1
  81. package/dist/utils/path.spec.mjs +0 -30
  82. package/dist/utils/scroll.d.ts +0 -25
  83. package/dist/utils/scroll.mjs +0 -59
  84. package/dist/utils/utils.d.ts +0 -16
  85. package/dist/utils/utils.mjs +0 -11
  86. package/dist/utils/warn.d.ts +0 -2
  87. package/dist/utils/warn.mjs +0 -12
  88. package/src/history/abstract.ts +0 -149
  89. package/src/history/base.ts +0 -408
  90. package/src/history/html.ts +0 -231
  91. package/src/history/index.ts +0 -20
  92. package/src/matcher/create-matcher.spec.ts +0 -3
  93. package/src/matcher/create-matcher.ts +0 -293
  94. package/src/matcher/index.ts +0 -1
  95. package/src/task-pipe/index.ts +0 -1
  96. package/src/task-pipe/task.ts +0 -97
  97. package/src/utils/bom.ts +0 -14
  98. package/src/utils/encoding.ts +0 -153
  99. package/src/utils/guards.ts +0 -25
  100. package/src/utils/index.ts +0 -27
  101. package/src/utils/path.spec.ts +0 -44
  102. package/src/utils/path.ts +0 -397
  103. package/src/utils/scroll.ts +0 -120
  104. package/src/utils/utils.ts +0 -30
  105. package/src/utils/warn.ts +0 -13
package/src/error.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type { Route } from './route';
2
+
3
+ export class RouteError extends Error {
4
+ public readonly code: string;
5
+ public readonly to: Route;
6
+ public readonly from: Route | null;
7
+
8
+ constructor(
9
+ message: string,
10
+ code: string,
11
+ to: Route,
12
+ from: Route | null = null
13
+ ) {
14
+ super(message);
15
+ this.name = 'RouteError';
16
+ this.code = code;
17
+ this.to = to;
18
+ this.from = from;
19
+ }
20
+ }
21
+
22
+ export class RouteTaskCancelledError extends RouteError {
23
+ public readonly taskName: string;
24
+
25
+ constructor(taskName: string, to: Route, from: Route | null = null) {
26
+ super(
27
+ `Route task "${taskName}" was cancelled`,
28
+ 'ROUTE_TASK_CANCELLED',
29
+ to,
30
+ from
31
+ );
32
+ this.name = 'RouteTaskCancelledError';
33
+ this.taskName = taskName;
34
+ }
35
+ }
36
+
37
+ export class RouteTaskExecutionError extends RouteError {
38
+ public readonly taskName: string;
39
+ public readonly originalError: Error;
40
+
41
+ constructor(
42
+ taskName: string,
43
+ to: Route,
44
+ from: Route | null = null,
45
+ originalError?: unknown
46
+ ) {
47
+ const error =
48
+ originalError instanceof Error
49
+ ? originalError
50
+ : new Error(String(originalError));
51
+ const message = `Route task "${taskName}" failed${error.message ? `: ${error.message}` : ''}`;
52
+ super(message, 'ROUTE_TASK_EXECUTION_ERROR', to, from);
53
+ this.name = 'RouteTaskExecutionError';
54
+ this.taskName = taskName;
55
+ this.originalError = error;
56
+ }
57
+ }
58
+
59
+ export class RouteNavigationAbortedError extends RouteError {
60
+ public readonly taskName: string;
61
+
62
+ constructor(taskName: string, to: Route, from: Route | null = null) {
63
+ super(
64
+ `Navigation was aborted by task "${taskName}"`,
65
+ 'ROUTE_NAVIGATION_ABORTED',
66
+ to,
67
+ from
68
+ );
69
+ this.name = 'RouteNavigationAbortedError';
70
+ this.taskName = taskName;
71
+ }
72
+ }
73
+
74
+ export class RouteSelfRedirectionError extends RouteError {
75
+ constructor(fullPath: string, to: Route, from: Route | null = null) {
76
+ super(
77
+ `Detected a self-redirection to "${fullPath}". Aborting navigation.`,
78
+ 'ROUTE_SELF_REDIRECTION',
79
+ to,
80
+ from
81
+ );
82
+ this.name = 'RouteSelfRedirectionError';
83
+ }
84
+ }
@@ -0,0 +1,12 @@
1
+ export class IncrementId {
2
+ private value = 0;
3
+ public equal(id: number) {
4
+ return this.value === id;
5
+ }
6
+ public next() {
7
+ return ++this.value;
8
+ }
9
+ }
10
+
11
+ export const PAGE_ID = new IncrementId();
12
+ export const LAYER_ID = new IncrementId();
package/src/index.ts CHANGED
@@ -1,3 +1,67 @@
1
- export { createRouter } from './router';
2
- export * from './types';
3
- export * from './utils';
1
+ // Error types
2
+ export {
3
+ RouteError,
4
+ RouteNavigationAbortedError,
5
+ RouteSelfRedirectionError,
6
+ RouteTaskCancelledError,
7
+ RouteTaskExecutionError
8
+ } from './error';
9
+ export { Route } from './route';
10
+ export { Router } from './router';
11
+ export {
12
+ type RouteConfig,
13
+ // Hook function types
14
+ type RouteConfirmHook,
15
+ type RouteConfirmHookResult,
16
+ type RouteHandleHook,
17
+ type RouteHandleResult,
18
+ // Router Layer types
19
+ type RouteLayerOptions,
20
+ type RouteLayerResult,
21
+ // Route location and config types
22
+ type RouteLocation,
23
+ type RouteLocationInput,
24
+ type RouteMatcher,
25
+ type RouteMatchResult,
26
+ type RouteMatchType,
27
+ // Basic data types
28
+ type RouteMeta,
29
+ type RouteNotifyHook,
30
+ type RouteOptions,
31
+ type RouteParsedConfig,
32
+ type RouterLayerOptions,
33
+ type RouterLinkAttributes,
34
+ type RouterLinkProps,
35
+ type RouterLinkResolved,
36
+ // RouterLink types
37
+ type RouterLinkType,
38
+ // Router MicroApp types
39
+ type RouterMicroApp,
40
+ type RouterMicroAppCallback,
41
+ type RouterMicroAppOptions,
42
+ // Core enums
43
+ RouterMode,
44
+ // Router core types
45
+ type RouterOptions,
46
+ type RouterParsedOptions,
47
+ type RouteState,
48
+ RouteType,
49
+ type RouteVerifyHook
50
+ } from './types';
51
+
52
+ // =================== Re-exporting deprecated types ===================
53
+
54
+ import type { Router } from './router';
55
+ import type { Route, RouteLocation, RouteLocationInput } from './types';
56
+
57
+ /** @deprecated Use `Router` directly instead of `RouterInstance`. */
58
+ export type RouterInstance = Router;
59
+
60
+ /** @deprecated Use `RouteLocationInput` directly instead of `RouterRawLocation`. */
61
+ export type RouterRawLocation = RouteLocationInput;
62
+
63
+ /** @deprecated Use `RouteLocation` directly instead of `RouterLocation`. */
64
+ export type RouterLocation = RouteLocation;
65
+
66
+ /** @deprecated Use `Route` directly instead of `RouteRecord`. */
67
+ export type RouteRecord = Route;
@@ -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,71 @@
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
+ // At this point, children exist but none matched — if requireIndex is true, skip self-match
32
+ if (item.requireIndex && item.children.length) continue;
33
+ const result = item.match(matchPath);
34
+ if (result) {
35
+ matches.unshift(item);
36
+ if (typeof result === 'object') {
37
+ Object.assign(params, result.params);
38
+ }
39
+ return true;
40
+ }
41
+ }
42
+ return false;
43
+ };
44
+ collectMatchingRoutes(compiledRoutes);
45
+ return { matches: Object.freeze(matches), params };
46
+ };
47
+ }
48
+
49
+ export function createRouteMatches(
50
+ routes: RouteConfig[],
51
+ base = ''
52
+ ): RouteParsedConfig[] {
53
+ return routes.map((route: RouteConfig): RouteParsedConfig => {
54
+ const compilePath = joinPathname(route.path, base);
55
+ return {
56
+ ...route,
57
+ compilePath,
58
+ match: match(compilePath),
59
+ compile: compile(compilePath),
60
+ meta: route.meta || {},
61
+ requireIndex: route.requireIndex ?? false,
62
+ children: Array.isArray(route.children)
63
+ ? createRouteMatches(route.children, compilePath)
64
+ : []
65
+ };
66
+ });
67
+ }
68
+
69
+ export function joinPathname(pathname: string, base = '') {
70
+ return '/' + `${base}/${pathname}`.split('/').filter(Boolean).join('/');
71
+ }
@@ -0,0 +1,153 @@
1
+ import type { Router } from './router';
2
+ import type { RouterMicroAppCallback, RouterMicroAppOptions } from './types';
3
+ import { isBrowser, isPlainObject } from './util';
4
+
5
+ /**
6
+ * Gets the root container element by ID.
7
+ * If not found, creates a new div with the given ID and appends it to document.body.
8
+ *
9
+ * @param appId - The application container ID.
10
+ * @returns The resolved HTMLElement.
11
+ */
12
+ export function getRootElement(appId: string): HTMLElement {
13
+ const el = document.getElementById(appId);
14
+ if (el) {
15
+ return el;
16
+ }
17
+ const newEl = document.createElement('div');
18
+ newEl.id = appId;
19
+ document.body.appendChild(newEl);
20
+ return newEl;
21
+ }
22
+
23
+ export class MicroApp {
24
+ public app: RouterMicroAppOptions | null = null;
25
+ public root: HTMLElement | null = null;
26
+ private _factory: RouterMicroAppCallback | null = null;
27
+ private destroyed = false;
28
+
29
+ public _update(router: Router, force = false) {
30
+ if (this.destroyed) {
31
+ throw new Error('MicroApp has been destroyed');
32
+ }
33
+ const factory = this._getNextFactory(router);
34
+ if (!force && factory === this._factory) {
35
+ return;
36
+ }
37
+ const oldApp = this.app;
38
+ // Create the new application
39
+ const app = factory ? factory(router) : null;
40
+ if (isBrowser && app) {
41
+ let root: HTMLElement | null = this.root;
42
+ if (root === null) {
43
+ root = getRootElement(router.appId);
44
+ const { rootStyle } = router.parsedOptions;
45
+ if (root && isPlainObject(rootStyle)) {
46
+ Object.assign(root.style, router.parsedOptions.rootStyle);
47
+ }
48
+ this.root = root;
49
+ }
50
+ if (root) {
51
+ const isHydration = root.hasAttribute('data-ssr');
52
+ if (isHydration) {
53
+ const appRoot = root.firstElementChild as HTMLElement;
54
+ if (appRoot) {
55
+ if (app.hydration) {
56
+ app.hydration(appRoot);
57
+ } else {
58
+ throw new Error(
59
+ 'SSR content detected but hydration function not provided'
60
+ );
61
+ }
62
+ } else {
63
+ // No child elements (e.g., Vue 2 comment nodes), fallback to mount
64
+ const el = document.createElement('div');
65
+ root.appendChild(el);
66
+ try {
67
+ app.mount(el);
68
+ } catch (e) {
69
+ el.remove();
70
+ throw e;
71
+ }
72
+ }
73
+ // Remove data-ssr attribute after hydration
74
+ root.removeAttribute('data-ssr');
75
+ } else {
76
+ // Capture all existing children before inserting the new container.
77
+ // Old app may have created multiple sibling nodes during its lifecycle.
78
+ const oldChildren = Array.from(root.childNodes);
79
+ const el = document.createElement('div');
80
+ root.appendChild(el);
81
+ try {
82
+ app.mount(el);
83
+ } catch (e) {
84
+ el.remove();
85
+ throw e;
86
+ }
87
+ if (oldApp) {
88
+ try {
89
+ oldApp.unmount();
90
+ } catch (e) {
91
+ // eslint-disable-next-line no-console
92
+ console.error(
93
+ '[@esmx/router] MicroApp unmount failed during route transition. Check the framework unmount hook returned by your render function (Vue: app.unmount, React: root.unmount, etc.).',
94
+ e
95
+ );
96
+ }
97
+ }
98
+ // Remove old children that are still attached to the DOM.
99
+ // Some frameworks may have already removed their own nodes during unmount.
100
+ oldChildren.forEach((child) => {
101
+ if (child.parentNode) {
102
+ child.remove();
103
+ }
104
+ });
105
+ }
106
+ }
107
+ }
108
+ this.app = app;
109
+ this._factory = factory;
110
+ }
111
+
112
+ private _getNextFactory({
113
+ route,
114
+ options
115
+ }: Router): RouterMicroAppCallback | null {
116
+ if (!route.matched || route.matched.length === 0) {
117
+ return null;
118
+ }
119
+ const name = route.matched[0].app;
120
+ if (
121
+ typeof name === 'string' &&
122
+ options.apps &&
123
+ typeof options.apps === 'object'
124
+ ) {
125
+ return options.apps[name] || null;
126
+ }
127
+ if (typeof name === 'function') {
128
+ return name;
129
+ }
130
+ if (typeof options.apps === 'function') {
131
+ return options.apps;
132
+ }
133
+ return null;
134
+ }
135
+
136
+ public destroy() {
137
+ this.app?.unmount();
138
+ this._clearRoot();
139
+ this.app = null;
140
+ this.root = null;
141
+ this._factory = null;
142
+ this.destroyed = true;
143
+ }
144
+
145
+ private _clearRoot(): void {
146
+ if (!this.root) {
147
+ return;
148
+ }
149
+ Array.from(this.root.childNodes).forEach((child) => {
150
+ child.remove();
151
+ });
152
+ }
153
+ }