@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
@@ -0,0 +1,33 @@
1
+ /** Internal {@link ScrollToOptions | `ScrollToOptions`}: `left` and `top` properties always have values */
2
+ interface _ScrollPosition extends ScrollToOptions {
3
+ left: number;
4
+ top: number;
5
+ }
6
+ export interface ScrollPositionElement extends ScrollToOptions {
7
+ /**
8
+ * A valid CSS selector. Some special characters need to be escaped (https://mathiasbynens.be/notes/css-escapes).
9
+ * @example
10
+ * Here are some examples:
11
+ *
12
+ * - `.title`
13
+ * - `.content:first-child`
14
+ * - `#marker`
15
+ * - `#marker\~with\~symbols`
16
+ * - `#marker.with.dot`: Selects `class="with dot" id="marker"`, not `id="marker.with.dot"`
17
+ *
18
+ */
19
+ el: string | Element;
20
+ }
21
+ /** Scroll parameters */
22
+ export type ScrollPosition = ScrollToOptions | ScrollPositionElement;
23
+ /** Get current window scroll position */
24
+ export declare const winScrollPos: () => _ScrollPosition;
25
+ /** Scroll to specified position */
26
+ export declare function scrollToPosition(position: ScrollPosition): void;
27
+ /** Stored scroll positions */
28
+ export declare const scrollPositions: Map<string, _ScrollPosition>;
29
+ /** Save scroll position */
30
+ export declare function saveScrollPosition(key: string, scrollPosition?: _ScrollPosition): void;
31
+ /** Get saved scroll position */
32
+ export declare function getSavedScrollPosition(key: string, defaultValue?: _ScrollPosition | null): _ScrollPosition | null;
33
+ export {};
@@ -0,0 +1,50 @@
1
+ export const winScrollPos = () => ({
2
+ left: window.scrollX,
3
+ top: window.scrollY
4
+ });
5
+ function getElementPosition(el, offset) {
6
+ const docRect = document.documentElement.getBoundingClientRect();
7
+ const elRect = el.getBoundingClientRect();
8
+ return {
9
+ behavior: offset.behavior,
10
+ left: elRect.left - docRect.left - (offset.left || 0),
11
+ top: elRect.top - docRect.top - (offset.top || 0)
12
+ };
13
+ }
14
+ export function scrollToPosition(position) {
15
+ if ("el" in position) {
16
+ const positionEl = position.el;
17
+ const el = typeof positionEl === "string" ? document.querySelector(positionEl) : positionEl;
18
+ if (!el) return;
19
+ position = getElementPosition(el, position);
20
+ }
21
+ if ("scrollBehavior" in document.documentElement.style) {
22
+ window.scrollTo(position);
23
+ } else {
24
+ window.scrollTo(
25
+ Number.isFinite(position.left) ? position.left : window.scrollX,
26
+ Number.isFinite(position.top) ? position.top : window.scrollY
27
+ );
28
+ }
29
+ }
30
+ export const scrollPositions = /* @__PURE__ */ new Map();
31
+ const POSITION_KEY = "__scroll_position_key";
32
+ export function saveScrollPosition(key, scrollPosition = winScrollPos()) {
33
+ scrollPosition = { ...scrollPosition };
34
+ scrollPositions.set(key, scrollPosition);
35
+ try {
36
+ if (location.href !== key) return;
37
+ const stateCopy = {
38
+ ...history.state || {},
39
+ [POSITION_KEY]: scrollPosition
40
+ };
41
+ history.replaceState(stateCopy, "");
42
+ } catch (error) {
43
+ }
44
+ }
45
+ export function getSavedScrollPosition(key, defaultValue = null) {
46
+ var _a;
47
+ const scroll = scrollPositions.get(key) || ((_a = history.state) == null ? void 0 : _a[POSITION_KEY]);
48
+ scrollPositions.delete(key);
49
+ return scroll || defaultValue;
50
+ }
@@ -0,0 +1,312 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { MatchFunction } from 'path-to-regexp';
3
+ import type { Route } from './route';
4
+ import type { Router } from './router';
5
+ export type { Route };
6
+ export type Awaitable<T> = T | Promise<T>;
7
+ export declare enum RouterMode {
8
+ history = "history",
9
+ memory = "memory"
10
+ }
11
+ export declare enum RouteType {
12
+ push = "push",
13
+ replace = "replace",
14
+ restartApp = "restartApp",
15
+ go = "go",
16
+ forward = "forward",
17
+ back = "back",
18
+ unknown = "unknown",
19
+ pushWindow = "pushWindow",
20
+ replaceWindow = "replaceWindow",
21
+ pushLayer = "pushLayer"
22
+ }
23
+ export type RouteConfirmHookResult = void | false | RouteLocationInput | RouteHandleHook;
24
+ export type RouteConfirmHook = (to: Route, from: Route | null, router: Router) => Awaitable<RouteConfirmHookResult>;
25
+ export type RouteVerifyHook = (to: Route, from: Route | null, router: Router) => Awaitable<boolean>;
26
+ export type RouteHandleHook = (to: Route, from: Route | null, router: Router) => Awaitable<RouteHandleResult>;
27
+ export type RouteNotifyHook = (to: Route, from: Route | null, router: Router) => void;
28
+ export type RouteMeta = Record<string | symbol, unknown>;
29
+ export type RouteState = Record<string, unknown>;
30
+ export type RouteHandleResult = unknown | null | void;
31
+ /**
32
+ * Route matching type
33
+ * - 'route': Route-level matching, compare if route configurations are the same
34
+ * - 'exact': Exact matching, compare if paths are exactly the same
35
+ * - 'include': Include matching, check if current path contains target path
36
+ */
37
+ export type RouteMatchType = 'route' | 'exact' | 'include';
38
+ export interface RouteLocation {
39
+ path?: string;
40
+ url?: string | URL;
41
+ params?: Record<string, string>;
42
+ query?: Record<string, string | undefined>;
43
+ queryArray?: Record<string, string[] | undefined>;
44
+ hash?: string;
45
+ state?: RouteState;
46
+ /** When `true`, maintains current scroll position after navigation (default behavior scrolls to top) */
47
+ keepScrollPosition?: boolean;
48
+ statusCode?: number | null;
49
+ layer?: RouteLayerOptions | null;
50
+ confirm?: RouteConfirmHook | null;
51
+ }
52
+ export type RouteLocationInput = RouteLocation | string;
53
+ export interface RouteConfig {
54
+ /** Pass a URL-encoded path */
55
+ path: string;
56
+ component?: unknown;
57
+ children?: RouteConfig[];
58
+ redirect?: RouteLocationInput | RouteConfirmHook;
59
+ meta?: RouteMeta;
60
+ app?: string | RouterMicroAppCallback;
61
+ asyncComponent?: () => Promise<unknown>;
62
+ beforeEnter?: RouteConfirmHook;
63
+ beforeUpdate?: RouteConfirmHook;
64
+ beforeLeave?: RouteConfirmHook;
65
+ /** Mark this route as only effective in layer mode */
66
+ layer?: boolean;
67
+ /**
68
+ * Require an index child route to match the parent path
69
+ *
70
+ * When set to `true`, the parent route will not match its own path
71
+ * if no child route matches. The parent route requires a `path: ''`
72
+ * index child route to match the parent path itself.
73
+ *
74
+ * - `requireIndex: false` (default): If no child route matches,
75
+ * the matcher falls back to matching the parent route itself
76
+ * - `requireIndex: true`: If no child route matches, the parent
77
+ * route is skipped entirely (no fallback to self-match)
78
+ *
79
+ * @default false
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * // requireIndex: true - /A won't match without index child
84
+ * {
85
+ * path: '/A',
86
+ * requireIndex: true,
87
+ * children: [
88
+ * { path: '', component: IndexComponent }, // /A matches this
89
+ * { path: 'a', component: AComponent } // /A/a matches this
90
+ * ]
91
+ * }
92
+ *
93
+ * // Default behavior - /A matches even without index child
94
+ * {
95
+ * path: '/A',
96
+ * children: [
97
+ * { path: 'a', component: AComponent } // /A/a matches, /A also matches (parent self)
98
+ * ]
99
+ * }
100
+ * ```
101
+ */
102
+ requireIndex?: boolean;
103
+ /**
104
+ * Route override function for hybrid app development
105
+ *
106
+ * Note: Override is not executed during initial route loading
107
+ *
108
+ * @param to Target route
109
+ * @param from Source route
110
+ * @returns Function to execute instead of default routing, or void for default
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * override: (to, from) => {
115
+ * if (isInApp()) {
116
+ * return () => JSBridge.openNative();
117
+ * }
118
+ * }
119
+ * ```
120
+ */
121
+ override?: RouteConfirmHook;
122
+ }
123
+ export interface RouteParsedConfig extends RouteConfig {
124
+ /** Require an index child route to match the parent path (always has a value after parsing) */
125
+ requireIndex: boolean;
126
+ compilePath: string;
127
+ children: RouteParsedConfig[];
128
+ match: MatchFunction;
129
+ compile: (params?: Record<string, string>) => string;
130
+ }
131
+ export interface RouteMatchResult {
132
+ readonly matches: readonly RouteParsedConfig[];
133
+ readonly params: Record<string, string | string[]>;
134
+ }
135
+ export type RouteMatcher = (to: URL, base: URL, cb?: (item: RouteParsedConfig) => boolean) => RouteMatchResult;
136
+ /**
137
+ * Route constructor options interface
138
+ */
139
+ export interface RouteOptions {
140
+ /** Router parsed options */
141
+ options?: RouterParsedOptions;
142
+ /** Route type */
143
+ toType?: RouteType;
144
+ /** Target route location */
145
+ toInput?: RouteLocationInput;
146
+ /** Source URL */
147
+ from?: URL | null;
148
+ }
149
+ export interface RouteLayerOptions {
150
+ /**
151
+ * Layer zIndex value
152
+ * If not set, will automatically use incremental layer value (1000 + increment)
153
+ */
154
+ zIndex?: number;
155
+ /**
156
+ * Hook function to determine layer keep-alive behavior before route closure
157
+ * Determines whether the layer should remain open during navigation
158
+ * - 'exact': Keep layer alive only when navigating to the exact initial path (default)
159
+ * - 'include': Keep layer alive when navigating to paths that start with the initial path
160
+ * - function: Custom logic to determine if the layer should be kept alive
161
+ * @default 'exact'
162
+ * @example
163
+ * ```typescript
164
+ * // Default behavior - keep only when navigating back to initial path
165
+ * keepAlive: 'exact'
166
+ *
167
+ * // Keep layer alive for all sub-paths (paths starting with the initial path)
168
+ * keepAlive: 'include'
169
+ *
170
+ * // Custom logic
171
+ * keepAlive: (to, from, router) => to.query.keepLayer === 'true'
172
+ * ```
173
+ */
174
+ keepAlive?: 'exact' | 'include' | RouteVerifyHook;
175
+ /**
176
+ * @deprecated Use keepAlive instead
177
+ * Verification hook function before route closure
178
+ * @returns Return true to allow closure, false to prevent closure
179
+ */
180
+ shouldClose?: RouteVerifyHook;
181
+ /**
182
+ * Whether to automatically record route history
183
+ * @default true
184
+ */
185
+ autoPush?: boolean;
186
+ /**
187
+ * Route navigation mode control
188
+ * - When autoPush is true:
189
+ * - true: Use push mode (add new history record)
190
+ * - false: Use replace mode (replace current history record)
191
+ * @default true
192
+ */
193
+ push?: boolean;
194
+ /**
195
+ * Router options for creating the layer instance
196
+ * These options will be merged with the default router configuration
197
+ */
198
+ routerOptions?: RouterLayerOptions;
199
+ }
200
+ export type RouteLayerResult = {
201
+ type: 'close';
202
+ route: Route;
203
+ } | {
204
+ type: 'push';
205
+ route: Route;
206
+ } | {
207
+ type: 'success';
208
+ route: Route;
209
+ data?: any;
210
+ };
211
+ /**
212
+ * Router options for creating layer instances
213
+ * Excludes handler functions and layer fields which are handled internally by createLayer
214
+ */
215
+ export type RouterLayerOptions = Omit<RouterOptions, 'handleBackBoundary' | 'handleLayerClose' | 'layer'>;
216
+ export interface RouterMicroAppOptions {
217
+ mount: (el: HTMLElement) => void;
218
+ hydration?: (el: HTMLElement) => void;
219
+ unmount: () => void;
220
+ renderToString?: () => Awaitable<string>;
221
+ }
222
+ export type RouterMicroAppCallback = (router: Router) => RouterMicroAppOptions;
223
+ export type RouterMicroApp = Record<string, RouterMicroAppCallback | undefined> | RouterMicroAppCallback;
224
+ export interface RouterOptions {
225
+ /**
226
+ * Application mount container ID
227
+ * - Pure string ID, no '#' prefix needed
228
+ * - Client-side: uses document.getElementById(appId)
229
+ * - Server-side: generates <div id="${appId}"> wrapper
230
+ * - Defaults to 'app'
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * new Router({ appId: 'app' })
235
+ * ```
236
+ */
237
+ appId?: string;
238
+ context?: Record<string | symbol, unknown>;
239
+ data?: Record<string | symbol, unknown>;
240
+ routes?: RouteConfig[];
241
+ mode?: RouterMode;
242
+ /** Optional in browser, but required on server side */
243
+ base?: URL;
244
+ req?: IncomingMessage | null;
245
+ res?: ServerResponse | null;
246
+ apps?: RouterMicroApp;
247
+ normalizeURL?: (to: URL, from: URL | null) => URL;
248
+ fallback?: RouteHandleHook;
249
+ nextTick?: () => Awaitable<void>;
250
+ rootStyle?: Partial<CSSStyleDeclaration> | false | null;
251
+ layer?: boolean;
252
+ zIndex?: number;
253
+ handleBackBoundary?: (router: Router) => void;
254
+ handleLayerClose?: (router: Router, data?: any) => void;
255
+ resolveLink?: (link: RouterLinkResolved, props: RouterLinkProps) => RouterLinkResolved;
256
+ }
257
+ export interface RouterParsedOptions extends Readonly<Required<RouterOptions>> {
258
+ readonly compiledRoutes: readonly RouteParsedConfig[];
259
+ readonly matcher: RouteMatcher;
260
+ }
261
+ /**
262
+ * Router link navigation types
263
+ */
264
+ export type RouterLinkType = 'push' | 'replace' | 'pushWindow' | 'replaceWindow' | 'pushLayer';
265
+ /**
266
+ * Router link attributes generated by the source code
267
+ */
268
+ export interface RouterLinkAttributes {
269
+ href: string;
270
+ class: string;
271
+ target?: '_blank';
272
+ rel?: string;
273
+ }
274
+ /**
275
+ * Framework-agnostic link configuration interface
276
+ */
277
+ export interface RouterLinkProps {
278
+ to: RouteLocationInput;
279
+ type?: RouterLinkType;
280
+ /**
281
+ * @deprecated Use type='replace' instead
282
+ */
283
+ replace?: boolean;
284
+ exact?: RouteMatchType;
285
+ activeClass?: string;
286
+ event?: string | string[];
287
+ tag?: string;
288
+ layerOptions?: RouteLayerOptions;
289
+ /**
290
+ * Hook function called before navigation occurs
291
+ * @param event - The DOM event that triggered the navigation
292
+ * @param eventName - The name of the event that triggered navigation
293
+ *
294
+ * Can prevent default event to block the default navigation handling logic.
295
+ * Call event.preventDefault() to stop the navigation from proceeding.
296
+ */
297
+ beforeNavigate?: (event: Event, eventName: string) => void;
298
+ }
299
+ /**
300
+ * Framework-agnostic link resolution result
301
+ */
302
+ export interface RouterLinkResolved {
303
+ route: Route;
304
+ type: RouterLinkType;
305
+ isActive: boolean;
306
+ isExactActive: boolean;
307
+ isExternal: boolean;
308
+ tag: string;
309
+ attributes: RouterLinkAttributes;
310
+ navigate: (e: Event) => Promise<void>;
311
+ createEventHandlers: (format?: (eventType: string) => string) => Record<string, (e: Event) => Promise<void>>;
312
+ }
package/dist/types.mjs ADDED
@@ -0,0 +1,18 @@
1
+ export var RouterMode = /* @__PURE__ */ ((RouterMode2) => {
2
+ RouterMode2["history"] = "history";
3
+ RouterMode2["memory"] = "memory";
4
+ return RouterMode2;
5
+ })(RouterMode || {});
6
+ export var RouteType = /* @__PURE__ */ ((RouteType2) => {
7
+ RouteType2["push"] = "push";
8
+ RouteType2["replace"] = "replace";
9
+ RouteType2["restartApp"] = "restartApp";
10
+ RouteType2["go"] = "go";
11
+ RouteType2["forward"] = "forward";
12
+ RouteType2["back"] = "back";
13
+ RouteType2["unknown"] = "unknown";
14
+ RouteType2["pushWindow"] = "pushWindow";
15
+ RouteType2["replaceWindow"] = "replaceWindow";
16
+ RouteType2["pushLayer"] = "pushLayer";
17
+ return RouteType2;
18
+ })(RouteType || {});
package/dist/util.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { Route, RouteConfirmHookResult, RouteMatchType } from './types';
2
+ export declare const isBrowser: boolean;
3
+ export declare function isNotNullish(value: unknown): boolean;
4
+ export declare function isPlainObject(o: unknown): o is Record<string, any>;
5
+ /**
6
+ * Check if value is a valid non-empty plain object
7
+ * Only check enumerable string keys to ensure it's a proper plain object
8
+ * @param value Value to check
9
+ * @returns true if it's a valid non-empty plain object
10
+ */
11
+ export declare function isNonEmptyPlainObject(value: unknown): value is Record<string, any>;
12
+ export declare const removeFromArray: <T>(arr: T[], ele: T) => void;
13
+ export declare function isValidConfirmHookResult(result: unknown): result is Exclude<RouteConfirmHookResult, void>;
14
+ export declare function isUrlEqual(url1: URL, url2?: URL | null): boolean;
15
+ /**
16
+ * Compare if two routes match
17
+ *
18
+ * @param fromRoute First route object
19
+ * @param toRoute Second route object, may be null
20
+ * @param matchType Match type
21
+ * - 'route': Route-level matching, compare if route configurations are the same
22
+ * - 'exact': Exact matching, compare if full paths are the same
23
+ * - 'include': Include matching, check if route1 path starts with route2 path
24
+ * @returns Whether they match
25
+ */
26
+ export declare function isRouteMatched(fromRoute: Route, toRoute: Route | null, matchType: RouteMatchType): boolean;
27
+ export declare function decodeParams<T extends Record<string, string | string[]>>(params: T): T;
28
+ /**
29
+ * Validates that SSR renderToString output contains exactly one root HTML element.
30
+ * Non-production only - throws if validation fails.
31
+ */
32
+ export declare function validateSsrRootElement(html: string): void;
package/dist/util.mjs ADDED
@@ -0,0 +1,100 @@
1
+ export const isBrowser = typeof window === "object";
2
+ export function isNotNullish(value) {
3
+ return value !== void 0 && value !== null && !Number.isNaN(
4
+ value instanceof Number ? value.valueOf() : value
5
+ );
6
+ }
7
+ export function isPlainObject(o) {
8
+ return (o == null ? void 0 : o.constructor) === Object || Object.prototype.toString.call(o) === "[object Object]";
9
+ }
10
+ export function isNonEmptyPlainObject(value) {
11
+ if (!isPlainObject(value)) {
12
+ return false;
13
+ }
14
+ return Object.keys(value).length > 0;
15
+ }
16
+ export const removeFromArray = (arr, ele) => {
17
+ if (!Array.isArray(arr) || arr.length === 0) return;
18
+ const i = Number.isNaN(ele) ? (
19
+ // If ele is NaN, use findIndex to search for NaN, because NaN !== NaN, so we can't use indexOf directly
20
+ arr.findIndex((item) => Number.isNaN(item))
21
+ ) : arr.indexOf(ele);
22
+ if (i === -1) return;
23
+ arr.splice(i, 1);
24
+ };
25
+ export function isValidConfirmHookResult(result) {
26
+ return result === false || typeof result === "function" || typeof result === "string" || isPlainObject(result);
27
+ }
28
+ export function isUrlEqual(url1, url2) {
29
+ if (!url2) {
30
+ return false;
31
+ }
32
+ if (url1 === url2) {
33
+ return true;
34
+ }
35
+ (url1 = new URL(url1)).searchParams.sort();
36
+ (url2 = new URL(url2)).searchParams.sort();
37
+ url1.hash = url1.hash;
38
+ url2.hash = url2.hash;
39
+ return url1.href === url2.href;
40
+ }
41
+ export function isRouteMatched(fromRoute, toRoute, matchType) {
42
+ if (!toRoute) return false;
43
+ switch (matchType) {
44
+ case "route":
45
+ return fromRoute.config === toRoute.config;
46
+ case "exact":
47
+ return fromRoute.fullPath === toRoute.fullPath;
48
+ case "include":
49
+ return fromRoute.fullPath.startsWith(toRoute.fullPath);
50
+ default:
51
+ return false;
52
+ }
53
+ }
54
+ export function decodeParams(params) {
55
+ const result = {};
56
+ for (const key in params) {
57
+ const value = params[key];
58
+ if (Array.isArray(value)) {
59
+ result[key] = value.map(
60
+ (item) => decodeURIComponent(item)
61
+ );
62
+ } else {
63
+ result[key] = decodeURIComponent(value);
64
+ }
65
+ }
66
+ return result;
67
+ }
68
+ export function validateSsrRootElement(html) {
69
+ var _a;
70
+ const trimmed = html.trim();
71
+ const firstMatch = trimmed.match(/^<([a-zA-Z][^\s>]*)/);
72
+ const firstTag = firstMatch == null ? void 0 : firstMatch[1];
73
+ const lastTag = (_a = trimmed.match(/<\/([a-zA-Z][^\s>]*)>\s*$/)) == null ? void 0 : _a[1];
74
+ if (!firstTag || firstTag !== lastTag) {
75
+ throw new Error(
76
+ "SSR renderToString() must return exactly one root HTML element. Current output: " + trimmed.slice(0, 100)
77
+ );
78
+ }
79
+ const tagRe = new RegExp("<(/?)".concat(firstTag, "(?:\\s[^>]*)?(/?)>"), "g");
80
+ let depth = 0;
81
+ let rootCloseEnd = -1;
82
+ let tag = tagRe.exec(trimmed);
83
+ while (tag !== null) {
84
+ if (tag[1] === "/") {
85
+ depth--;
86
+ if (depth === 0) {
87
+ rootCloseEnd = tagRe.lastIndex;
88
+ break;
89
+ }
90
+ } else if (tag[2] !== "/") {
91
+ depth++;
92
+ }
93
+ tag = tagRe.exec(trimmed);
94
+ }
95
+ if (rootCloseEnd === -1 || trimmed.slice(rootCloseEnd).trim().length > 0) {
96
+ throw new Error(
97
+ "SSR renderToString() must return exactly one root HTML element. Current output: " + trimmed.slice(0, 100)
98
+ );
99
+ }
100
+ }
package/package.json CHANGED
@@ -1,9 +1,34 @@
1
1
  {
2
2
  "name": "@esmx/router",
3
+ "description": "Framework-agnostic universal router for browser and Node.js SSR, with micro-frontend and layer navigation support",
4
+ "keywords": [
5
+ "router",
6
+ "routing",
7
+ "ssr",
8
+ "micro-frontend",
9
+ "typescript",
10
+ "universal",
11
+ "navigation",
12
+ "esmx",
13
+ "esm",
14
+ "spa",
15
+ "framework",
16
+ "frontend"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/esmnext/esmx.git",
21
+ "directory": "packages/router"
22
+ },
23
+ "homepage": "https://github.com/esmnext/esmx",
24
+ "bugs": {
25
+ "url": "https://github.com/esmnext/esmx/issues"
26
+ },
27
+ "license": "MIT",
3
28
  "template": "library",
4
29
  "scripts": {
5
30
  "lint:js": "biome check --write --no-errors-on-unmatched",
6
- "lint:css": "stylelint '**/*.{css,vue}' --fix --aei",
31
+ "lint:css": "pnpm run lint:js",
7
32
  "lint:type": "tsc --noEmit",
8
33
  "test": "vitest run --pass-with-no-tests",
9
34
  "coverage": "vitest run --coverage --pass-with-no-tests",
@@ -28,22 +53,18 @@
28
53
  }
29
54
  ],
30
55
  "dependencies": {
31
- "path-to-regexp": "^6.2.2",
32
- "url-parse": "^1.5.10"
56
+ "path-to-regexp": "^6.2.2"
33
57
  },
34
58
  "devDependencies": {
35
- "@biomejs/biome": "1.9.4",
36
- "@esmx/lint": "3.0.0-rc.12",
37
- "@gez/lint": "3.0.0-rc.9",
38
- "@types/node": "22.13.10",
39
- "@types/url-parse": "^1.4.11",
40
- "@vitest/coverage-v8": "3.0.8",
41
- "stylelint": "16.15.0",
42
- "typescript": "5.8.2",
43
- "unbuild": "2.0.0",
44
- "vitest": "3.0.8"
59
+ "@biomejs/biome": "2.3.7",
60
+ "@types/node": "^24.0.0",
61
+ "@vitest/coverage-v8": "3.2.6",
62
+ "happy-dom": "^20.0.10",
63
+ "typescript": "5.9.3",
64
+ "unbuild": "3.6.1",
65
+ "vitest": "3.2.6"
45
66
  },
46
- "version": "3.0.0-rc.12",
67
+ "version": "3.0.0-rc.120",
47
68
  "type": "module",
48
69
  "private": false,
49
70
  "exports": {
@@ -62,5 +83,11 @@
62
83
  "template",
63
84
  "public"
64
85
  ],
65
- "gitHead": "7d2c2fc4fe2cc98ebdbc12560f19637ca04398e5"
86
+ "gitHead": "3e692857ccb65e7d1fdf689e30eb5d935e8bda77",
87
+ "engines": {
88
+ "node": ">=24"
89
+ },
90
+ "publishConfig": {
91
+ "access": "public"
92
+ }
66
93
  }