@esmx/router 3.0.0-rc.12 → 3.0.0-rc.122
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 +21 -0
- package/README.md +158 -0
- package/README.zh-CN.md +158 -0
- package/dist/error.d.ts +23 -0
- package/dist/error.mjs +64 -0
- package/dist/increment-id.d.ts +7 -0
- package/dist/increment-id.mjs +16 -0
- package/dist/index.d.ts +14 -3
- package/dist/index.mjs +13 -3
- package/dist/location.d.ts +22 -0
- package/dist/location.mjs +64 -0
- package/dist/matcher.d.ts +4 -0
- package/dist/matcher.mjs +49 -0
- package/dist/micro-app.d.ts +20 -0
- package/dist/micro-app.mjs +132 -0
- package/dist/navigation.d.ts +45 -0
- package/dist/navigation.mjs +153 -0
- package/dist/options.d.ts +4 -0
- package/dist/options.mjs +95 -0
- package/dist/route-task.d.ts +40 -0
- package/dist/route-task.mjs +77 -0
- package/dist/route-transition.d.ts +54 -0
- package/dist/route-transition.mjs +362 -0
- package/dist/route.d.ts +77 -0
- package/dist/route.mjs +223 -0
- package/dist/router-link.d.ts +10 -0
- package/dist/router-link.mjs +142 -0
- package/dist/router.d.ts +113 -102
- package/dist/router.mjs +321 -354
- package/dist/scroll.d.ts +33 -0
- package/dist/scroll.mjs +50 -0
- package/dist/types.d.ts +312 -0
- package/dist/types.mjs +18 -0
- package/dist/util.d.ts +32 -0
- package/dist/util.mjs +100 -0
- package/package.json +89 -62
- package/src/error.ts +84 -0
- package/src/increment-id.ts +12 -0
- package/src/index.ts +67 -3
- package/src/location.ts +124 -0
- package/src/matcher.ts +71 -0
- package/src/micro-app.ts +153 -0
- package/src/navigation.ts +202 -0
- package/src/options.ts +136 -0
- package/src/route-task.ts +102 -0
- package/src/route-transition.ts +480 -0
- package/src/route.ts +335 -0
- package/src/router-link.ts +241 -0
- package/src/router.ts +351 -467
- package/src/scroll.ts +106 -0
- package/src/types.ts +415 -0
- package/src/util.ts +184 -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 -181
- 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.d.ts +0 -1
- 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 -264
- package/dist/utils/path.spec.d.ts +0 -1
- package/dist/utils/path.spec.mjs +0 -30
- 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 -231
- 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 -44
- package/src/utils/path.ts +0 -397
- package/src/utils/scroll.ts +0 -120
- package/src/utils/utils.ts +0 -30
- package/src/utils/warn.ts +0 -13
package/package.json
CHANGED
|
@@ -1,66 +1,93 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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",
|
|
28
|
+
"template": "library",
|
|
29
|
+
"contributors": [
|
|
30
|
+
{
|
|
31
|
+
"name": "lzxb",
|
|
32
|
+
"url": "https://github.com/lzxb"
|
|
11
33
|
},
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"url": "https://github.com/lzxb"
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
"name": "RockShi1994",
|
|
19
|
-
"url": "https://github.com/RockShi1994"
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"name": "jerrychan7",
|
|
23
|
-
"url": "https://github.com/jerrychan7"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"name": "wesloong",
|
|
27
|
-
"url": "https://github.com/wesloong"
|
|
28
|
-
}
|
|
29
|
-
],
|
|
30
|
-
"dependencies": {
|
|
31
|
-
"path-to-regexp": "^6.2.2",
|
|
32
|
-
"url-parse": "^1.5.10"
|
|
34
|
+
{
|
|
35
|
+
"name": "RockShi1994",
|
|
36
|
+
"url": "https://github.com/RockShi1994"
|
|
33
37
|
},
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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"
|
|
38
|
+
{
|
|
39
|
+
"name": "jerrychan7",
|
|
40
|
+
"url": "https://github.com/jerrychan7"
|
|
45
41
|
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
"types": "
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
42
|
+
{
|
|
43
|
+
"name": "wesloong",
|
|
44
|
+
"url": "https://github.com/wesloong"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"path-to-regexp": "^6.2.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@biomejs/biome": "2.3.7",
|
|
52
|
+
"@types/node": "^24.0.0",
|
|
53
|
+
"@vitest/coverage-v8": "3.2.6",
|
|
54
|
+
"happy-dom": "^20.0.10",
|
|
55
|
+
"typescript": "5.9.3",
|
|
56
|
+
"unbuild": "3.6.1",
|
|
57
|
+
"vitest": "3.2.6"
|
|
58
|
+
},
|
|
59
|
+
"version": "3.0.0-rc.122",
|
|
60
|
+
"type": "module",
|
|
61
|
+
"private": false,
|
|
62
|
+
"exports": {
|
|
63
|
+
".": {
|
|
64
|
+
"import": "./dist/index.mjs",
|
|
65
|
+
"types": "./dist/index.d.ts"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"module": "dist/index.mjs",
|
|
69
|
+
"types": "./dist/index.d.ts",
|
|
70
|
+
"files": [
|
|
71
|
+
"lib",
|
|
72
|
+
"src",
|
|
73
|
+
"dist",
|
|
74
|
+
"*.mjs",
|
|
75
|
+
"template",
|
|
76
|
+
"public"
|
|
77
|
+
],
|
|
78
|
+
"gitHead": "3e692857ccb65e7d1fdf689e30eb5d935e8bda77",
|
|
79
|
+
"engines": {
|
|
80
|
+
"node": ">=24"
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {
|
|
86
|
+
"lint:js": "biome check --write --no-errors-on-unmatched",
|
|
87
|
+
"lint:css": "pnpm run lint:js",
|
|
88
|
+
"lint:type": "tsc --noEmit",
|
|
89
|
+
"test": "vitest run --pass-with-no-tests",
|
|
90
|
+
"coverage": "vitest run --coverage --pass-with-no-tests",
|
|
91
|
+
"build": "unbuild"
|
|
92
|
+
}
|
|
93
|
+
}
|
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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
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;
|
package/src/location.ts
ADDED
|
@@ -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
|
+
}
|
package/src/micro-app.ts
ADDED
|
@@ -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
|
+
}
|