@esmx/router 3.0.0-rc.18 → 3.0.0-rc.20
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 -30
- 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/types/index.d.ts +0 -694
- package/dist/types/index.mjs +0 -6
- 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 -282
- 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 -292
- 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/types/index.ts +0 -858
- 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 -418
- 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
package/package.json
CHANGED
|
@@ -28,23 +28,20 @@
|
|
|
28
28
|
}
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"
|
|
32
|
-
"path-to-regexp": "^6.2.2",
|
|
33
|
-
"url-parse": "^1.5.10"
|
|
31
|
+
"path-to-regexp": "^6.2.2"
|
|
34
32
|
},
|
|
35
33
|
"devDependencies": {
|
|
36
34
|
"@biomejs/biome": "1.9.4",
|
|
37
|
-
"@esmx/lint": "3.0.0-rc.
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"stylelint": "16.15.0",
|
|
35
|
+
"@esmx/lint": "3.0.0-rc.20",
|
|
36
|
+
"@types/node": "22.15.18",
|
|
37
|
+
"@vitest/coverage-v8": "3.1.3",
|
|
38
|
+
"happy-dom": "^18.0.1",
|
|
39
|
+
"stylelint": "16.19.1",
|
|
43
40
|
"typescript": "5.8.2",
|
|
44
|
-
"unbuild": "
|
|
45
|
-
"vitest": "3.
|
|
41
|
+
"unbuild": "3.5.0",
|
|
42
|
+
"vitest": "3.1.3"
|
|
46
43
|
},
|
|
47
|
-
"version": "3.0.0-rc.
|
|
44
|
+
"version": "3.0.0-rc.20",
|
|
48
45
|
"type": "module",
|
|
49
46
|
"private": false,
|
|
50
47
|
"exports": {
|
|
@@ -63,5 +60,5 @@
|
|
|
63
60
|
"template",
|
|
64
61
|
"public"
|
|
65
62
|
],
|
|
66
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "4c6490c23cbc148cb189b3f2cdae930eed901607"
|
|
67
64
|
}
|
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,9 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Router as IndexRouter } from './index';
|
|
3
|
+
import { Router } from './router';
|
|
4
|
+
|
|
5
|
+
describe('index exports', () => {
|
|
6
|
+
it('should export Router correctly', () => {
|
|
7
|
+
expect(IndexRouter).toBe(Router);
|
|
8
|
+
});
|
|
9
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export
|
|
3
|
-
export
|
|
1
|
+
export { Router } from './router';
|
|
2
|
+
export { Route } from './route';
|
|
3
|
+
export { RouteTransition, ROUTE_TYPE_HANDLERS } from './route-transition';
|
|
4
|
+
export {
|
|
5
|
+
// Utility types
|
|
6
|
+
type Awaitable,
|
|
7
|
+
// Core enums
|
|
8
|
+
RouterMode,
|
|
9
|
+
RouteType,
|
|
10
|
+
// Hook function types
|
|
11
|
+
type RouteConfirmHook,
|
|
12
|
+
type RouteConfirmHookResult,
|
|
13
|
+
type RouteVerifyHook,
|
|
14
|
+
type RouteHandleHook,
|
|
15
|
+
type RouteNotifyHook,
|
|
16
|
+
// Basic data types
|
|
17
|
+
type RouteMeta,
|
|
18
|
+
type RouteState,
|
|
19
|
+
type RouteHandleResult,
|
|
20
|
+
type RouteMatchType,
|
|
21
|
+
// Route location and config types
|
|
22
|
+
type RouteLocation,
|
|
23
|
+
type RouteLocationInput,
|
|
24
|
+
type RouteConfig,
|
|
25
|
+
type RouteParsedConfig,
|
|
26
|
+
type RouteMatchResult,
|
|
27
|
+
type RouteMatcher,
|
|
28
|
+
type RouteOptions,
|
|
29
|
+
// Router Layer types
|
|
30
|
+
type RouteLayerOptions,
|
|
31
|
+
type RouteLayerResult,
|
|
32
|
+
type RouterLayerOptions,
|
|
33
|
+
// Router MicroApp types
|
|
34
|
+
type RouterMicroApp,
|
|
35
|
+
type RouterMicroAppCallback,
|
|
36
|
+
type RouterMicroAppOptions,
|
|
37
|
+
// Router core types
|
|
38
|
+
type RouterOptions,
|
|
39
|
+
type RouterParsedOptions,
|
|
40
|
+
// RouterLink types
|
|
41
|
+
type RouterLinkType,
|
|
42
|
+
type RouterLinkAttributes,
|
|
43
|
+
type RouterLinkProps,
|
|
44
|
+
type RouterLinkResolved
|
|
45
|
+
} from './types';
|
|
46
|
+
|
|
47
|
+
// Error types
|
|
48
|
+
export {
|
|
49
|
+
RouteError,
|
|
50
|
+
RouteTaskCancelledError,
|
|
51
|
+
RouteTaskExecutionError,
|
|
52
|
+
RouteNavigationAbortedError,
|
|
53
|
+
RouteSelfRedirectionError
|
|
54
|
+
} from './error';
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { normalizeURL, parseLocation } from './location';
|
|
3
|
+
import type { RouteLocationInput } from './types';
|
|
4
|
+
|
|
5
|
+
declare module 'vitest' {
|
|
6
|
+
interface ToEqURLMatchers {
|
|
7
|
+
toEqURL(expected: URL | string): void;
|
|
8
|
+
}
|
|
9
|
+
interface Assertion extends ToEqURLMatchers {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
expect.extend({
|
|
13
|
+
toEqURL: (received: URL, expected: URL | string) => {
|
|
14
|
+
if (!(received instanceof URL)) {
|
|
15
|
+
return {
|
|
16
|
+
message: () => `expected ${received} to be an instance of URL`,
|
|
17
|
+
pass: false
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
(received = new URL(received)).searchParams.sort();
|
|
21
|
+
(expected = new URL(expected)).searchParams.sort();
|
|
22
|
+
// biome-ignore lint/correctness/noSelfAssign:
|
|
23
|
+
received.hash = received.hash;
|
|
24
|
+
// biome-ignore lint/correctness/noSelfAssign:
|
|
25
|
+
expected.hash = expected.hash;
|
|
26
|
+
return {
|
|
27
|
+
message: () => `expected ${received.href} to be ${expected.href}`,
|
|
28
|
+
pass: received.href === expected.href
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('normalizeURL', () => {
|
|
34
|
+
const testCases: Array<{
|
|
35
|
+
input: string | URL;
|
|
36
|
+
base: string;
|
|
37
|
+
expected: string;
|
|
38
|
+
description: string;
|
|
39
|
+
}> = [
|
|
40
|
+
{
|
|
41
|
+
input: '//example.com/path',
|
|
42
|
+
base: 'https://github.com',
|
|
43
|
+
expected: 'http://example.com/path',
|
|
44
|
+
description:
|
|
45
|
+
'should handle protocol-relative URLs (starting with //)'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
input: 'http://github.com/path?a#h',
|
|
49
|
+
base: 'http://example.com',
|
|
50
|
+
expected: 'http://github.com/path?a#h',
|
|
51
|
+
description: 'should handle absolute URLs'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
input: '/path',
|
|
55
|
+
base: 'http://example.com/en/',
|
|
56
|
+
expected: 'http://example.com/en/path',
|
|
57
|
+
description: 'should handle relative paths with a base URL'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
input: 'github.com',
|
|
61
|
+
base: 'http://example.com',
|
|
62
|
+
expected: 'http://example.com/github.com',
|
|
63
|
+
description: 'should treat bare domains as relative paths'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
input: new URL('http://example.com/path'),
|
|
67
|
+
base: 'http://example.com',
|
|
68
|
+
expected: 'http://example.com/path',
|
|
69
|
+
description: 'should handle URL objects'
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
input: '-a://example.com',
|
|
73
|
+
base: 'http://example.com',
|
|
74
|
+
expected: 'http://example.com/-a://example.com',
|
|
75
|
+
description:
|
|
76
|
+
'should treat strings that fail to parse as a protocol as relative paths'
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
testCases.forEach(({ input, base, expected, description }) => {
|
|
81
|
+
test(description, () => {
|
|
82
|
+
const result = normalizeURL(input, new URL(base));
|
|
83
|
+
expect(result).toEqURL(expected);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('parseLocation', () => {
|
|
89
|
+
const testCases: Array<{
|
|
90
|
+
input: RouteLocationInput;
|
|
91
|
+
base: string;
|
|
92
|
+
expected: string;
|
|
93
|
+
description: string;
|
|
94
|
+
}> = [
|
|
95
|
+
{
|
|
96
|
+
input: '/products',
|
|
97
|
+
base: 'http://example.com',
|
|
98
|
+
expected: 'http://example.com/products',
|
|
99
|
+
description: 'should handle string paths'
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
input: { path: '/products' },
|
|
103
|
+
base: 'http://example.com',
|
|
104
|
+
expected: 'http://example.com/products',
|
|
105
|
+
description: 'should handle objects with a path property'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
input: { url: '/products' },
|
|
109
|
+
base: 'http://example.com',
|
|
110
|
+
expected: 'http://example.com/products',
|
|
111
|
+
description: 'should handle objects with a url property'
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
input: {
|
|
115
|
+
path: '/products',
|
|
116
|
+
query: { id: '123', category: 'electronics' }
|
|
117
|
+
},
|
|
118
|
+
base: 'http://example.com',
|
|
119
|
+
expected: 'http://example.com/products?id=123&category=electronics',
|
|
120
|
+
description: 'should handle objects with query parameters'
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
input: { path: '/products', query: { id: '123' }, hash: 'details' },
|
|
124
|
+
base: 'http://example.com',
|
|
125
|
+
expected: 'http://example.com/products?id=123#details',
|
|
126
|
+
description: 'should handle objects with a hash'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
input: { path: '/products', queryArray: { tag: ['new', 'sale'] } },
|
|
130
|
+
base: 'http://example.com',
|
|
131
|
+
expected: 'http://example.com/products?tag=new&tag=sale',
|
|
132
|
+
description: 'should handle objects with queryArray'
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
input: {
|
|
136
|
+
path: '/products',
|
|
137
|
+
query: { id: '123', category: 'electronics' },
|
|
138
|
+
queryArray: { tag: ['new', 'sale'] },
|
|
139
|
+
hash: 'details'
|
|
140
|
+
},
|
|
141
|
+
base: 'http://example.com',
|
|
142
|
+
expected:
|
|
143
|
+
'http://example.com/products?id=123&category=electronics&tag=new&tag=sale#details',
|
|
144
|
+
description: 'should handle complex objects with all properties'
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
input: {
|
|
148
|
+
path: '/products',
|
|
149
|
+
hash: '#a?a'
|
|
150
|
+
},
|
|
151
|
+
base: 'http://example.com',
|
|
152
|
+
expected: 'http://example.com/products#a?a',
|
|
153
|
+
description: 'should handle special hash characters correctly'
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
input: {
|
|
157
|
+
path: '/products',
|
|
158
|
+
hash: '#a?a#b'
|
|
159
|
+
},
|
|
160
|
+
base: 'http://example.com',
|
|
161
|
+
expected: 'http://example.com/products#a?a#b',
|
|
162
|
+
description: 'should handle special hash characters correctly'
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
input: {
|
|
166
|
+
path: '/products',
|
|
167
|
+
query: {
|
|
168
|
+
id: null,
|
|
169
|
+
category: void 0,
|
|
170
|
+
symbol: Symbol(),
|
|
171
|
+
fn: async () => '',
|
|
172
|
+
obj: { a: 10 },
|
|
173
|
+
big: 12345678901234567891234567890123456789n,
|
|
174
|
+
a: Number.NaN,
|
|
175
|
+
b: '',
|
|
176
|
+
c: '0',
|
|
177
|
+
d: 0,
|
|
178
|
+
e: 1
|
|
179
|
+
} as any as Record<string, string>
|
|
180
|
+
},
|
|
181
|
+
base: 'http://example.com',
|
|
182
|
+
expected: `http://example.com/products?symbol=Symbol()&fn=${String(
|
|
183
|
+
async () => ''
|
|
184
|
+
)}&obj=${String({})}&big=12345678901234567891234567890123456789&b&c=0&d=0&e=1`,
|
|
185
|
+
description:
|
|
186
|
+
'should ignore null, undefined, and NaN query parameters'
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
input: { path: '/products', queryArray: { tag: [] } },
|
|
190
|
+
base: 'http://example.com',
|
|
191
|
+
expected: 'http://example.com/products',
|
|
192
|
+
description: 'should handle empty queryArray'
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
input: {
|
|
196
|
+
path: '/products?id=path&a',
|
|
197
|
+
query: { id: 'query' }
|
|
198
|
+
},
|
|
199
|
+
base: 'http://example.com',
|
|
200
|
+
expected: 'http://example.com/products?id=query&a',
|
|
201
|
+
description: 'query value should override query parameter in path'
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
input: {
|
|
205
|
+
path: '/products?id=path&a',
|
|
206
|
+
query: { id: 'query' },
|
|
207
|
+
queryArray: { id: ['queryArray'] }
|
|
208
|
+
},
|
|
209
|
+
base: 'http://example.com',
|
|
210
|
+
expected: 'http://example.com/products?id=queryArray&a',
|
|
211
|
+
description:
|
|
212
|
+
'queryArray value should override query and path parameters'
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
input: {
|
|
216
|
+
path: '/products?id=path&a',
|
|
217
|
+
queryArray: { id: ['queryArray'] }
|
|
218
|
+
},
|
|
219
|
+
base: 'http://example.com',
|
|
220
|
+
expected: 'http://example.com/products?id=queryArray&a',
|
|
221
|
+
description:
|
|
222
|
+
'queryArray value should override query parameter in path'
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
input: {
|
|
226
|
+
path: '?a&a=&a&a',
|
|
227
|
+
query: { a: '' },
|
|
228
|
+
queryArray: { a: ['', ''] }
|
|
229
|
+
},
|
|
230
|
+
base: 'http://example.com',
|
|
231
|
+
expected: 'http://example.com?a&a',
|
|
232
|
+
description:
|
|
233
|
+
'should handle empty strings and duplicate query parameters correctly'
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
input: { path: '/products?id=123', url: '/products?id=456' },
|
|
237
|
+
base: 'http://example.com',
|
|
238
|
+
expected: 'http://example.com/products?id=123',
|
|
239
|
+
description:
|
|
240
|
+
'path should take priority over url when both are present'
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
input: { url: '/products?id=456' },
|
|
244
|
+
base: 'http://example.com',
|
|
245
|
+
expected: 'http://example.com/products?id=456',
|
|
246
|
+
description: 'url should be used when path is not present'
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
input: {},
|
|
250
|
+
base: 'http://example.com',
|
|
251
|
+
expected: 'http://example.com/',
|
|
252
|
+
description: 'empty input object should default to base URL'
|
|
253
|
+
}
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
testCases.forEach(({ input, base, expected, description }) => {
|
|
257
|
+
test(description, () => {
|
|
258
|
+
const result = parseLocation(input, new URL(base));
|
|
259
|
+
expect(result).toEqURL(expected);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('normalizeURL more', () => {
|
|
265
|
+
describe.for(
|
|
266
|
+
// biome-ignore format:
|
|
267
|
+
Object.entries({
|
|
268
|
+
'https://www.esmx.dev': {
|
|
269
|
+
'/': 'https://www.esmx.dev/',
|
|
270
|
+
'/new': 'https://www.esmx.dev/new',
|
|
271
|
+
'/new/': 'https://www.esmx.dev/new/',
|
|
272
|
+
'/new/100': 'https://www.esmx.dev/new/100',
|
|
273
|
+
'/new/100/': 'https://www.esmx.dev/new/100/',
|
|
274
|
+
'..': 'https://www.esmx.dev/',
|
|
275
|
+
'../': 'https://www.esmx.dev/',
|
|
276
|
+
'../new': 'https://www.esmx.dev/new',
|
|
277
|
+
'../new/': 'https://www.esmx.dev/new/',
|
|
278
|
+
'../new/100': 'https://www.esmx.dev/new/100',
|
|
279
|
+
'../new/100/': 'https://www.esmx.dev/new/100/',
|
|
280
|
+
'': 'https://www.esmx.dev/',
|
|
281
|
+
'new': 'https://www.esmx.dev/new',
|
|
282
|
+
'new/': 'https://www.esmx.dev/new/',
|
|
283
|
+
'new/100': 'https://www.esmx.dev/new/100',
|
|
284
|
+
'new/100/': 'https://www.esmx.dev/new/100/',
|
|
285
|
+
'.': 'https://www.esmx.dev/',
|
|
286
|
+
'./': 'https://www.esmx.dev/',
|
|
287
|
+
'./new': 'https://www.esmx.dev/new',
|
|
288
|
+
'./new/': 'https://www.esmx.dev/new/',
|
|
289
|
+
'./new/100': 'https://www.esmx.dev/new/100',
|
|
290
|
+
'./new/100/': 'https://www.esmx.dev/new/100/',
|
|
291
|
+
'.a': 'https://www.esmx.dev/.a',
|
|
292
|
+
'..a': 'https://www.esmx.dev/..a',
|
|
293
|
+
'.a/': 'https://www.esmx.dev/.a/',
|
|
294
|
+
'..a/': 'https://www.esmx.dev/..a/',
|
|
295
|
+
'new/../.': 'https://www.esmx.dev/',
|
|
296
|
+
},
|
|
297
|
+
'https://www.esmx.dev/': {
|
|
298
|
+
'/': 'https://www.esmx.dev/',
|
|
299
|
+
'/new': 'https://www.esmx.dev/new',
|
|
300
|
+
'/new/': 'https://www.esmx.dev/new/',
|
|
301
|
+
'/new/100': 'https://www.esmx.dev/new/100',
|
|
302
|
+
'/new/100/': 'https://www.esmx.dev/new/100/',
|
|
303
|
+
'..': 'https://www.esmx.dev/',
|
|
304
|
+
'../': 'https://www.esmx.dev/',
|
|
305
|
+
'../new': 'https://www.esmx.dev/new',
|
|
306
|
+
'../new/': 'https://www.esmx.dev/new/',
|
|
307
|
+
'../new/100': 'https://www.esmx.dev/new/100',
|
|
308
|
+
'../new/100/': 'https://www.esmx.dev/new/100/',
|
|
309
|
+
'': 'https://www.esmx.dev/',
|
|
310
|
+
'new': 'https://www.esmx.dev/new',
|
|
311
|
+
'new/': 'https://www.esmx.dev/new/',
|
|
312
|
+
'new/100': 'https://www.esmx.dev/new/100',
|
|
313
|
+
'new/100/': 'https://www.esmx.dev/new/100/',
|
|
314
|
+
'.': 'https://www.esmx.dev/',
|
|
315
|
+
'./': 'https://www.esmx.dev/',
|
|
316
|
+
'./new': 'https://www.esmx.dev/new',
|
|
317
|
+
'./new/': 'https://www.esmx.dev/new/',
|
|
318
|
+
'./new/100': 'https://www.esmx.dev/new/100',
|
|
319
|
+
'./new/100/': 'https://www.esmx.dev/new/100/',
|
|
320
|
+
'.a': 'https://www.esmx.dev/.a',
|
|
321
|
+
'..a': 'https://www.esmx.dev/..a',
|
|
322
|
+
'.a/': 'https://www.esmx.dev/.a/',
|
|
323
|
+
'..a/': 'https://www.esmx.dev/..a/',
|
|
324
|
+
'new/../.': 'https://www.esmx.dev/',
|
|
325
|
+
},
|
|
326
|
+
'https://www.esmx.dev/a/b/c': {
|
|
327
|
+
'/': 'https://www.esmx.dev/a/b/',
|
|
328
|
+
'/new': 'https://www.esmx.dev/a/b/new',
|
|
329
|
+
'/new/': 'https://www.esmx.dev/a/b/new/',
|
|
330
|
+
'/new/100': 'https://www.esmx.dev/a/b/new/100',
|
|
331
|
+
'/new/100/': 'https://www.esmx.dev/a/b/new/100/',
|
|
332
|
+
'..': 'https://www.esmx.dev/a/',
|
|
333
|
+
'../': 'https://www.esmx.dev/a/',
|
|
334
|
+
'../new': 'https://www.esmx.dev/a/new',
|
|
335
|
+
'../new/': 'https://www.esmx.dev/a/new/',
|
|
336
|
+
'../new/100': 'https://www.esmx.dev/a/new/100',
|
|
337
|
+
'../new/100/': 'https://www.esmx.dev/a/new/100/',
|
|
338
|
+
'': 'https://www.esmx.dev/a/b/c',
|
|
339
|
+
'new': 'https://www.esmx.dev/a/b/new',
|
|
340
|
+
'new/': 'https://www.esmx.dev/a/b/new/',
|
|
341
|
+
'new/100': 'https://www.esmx.dev/a/b/new/100',
|
|
342
|
+
'new/100/': 'https://www.esmx.dev/a/b/new/100/',
|
|
343
|
+
'.': 'https://www.esmx.dev/a/b/',
|
|
344
|
+
'./': 'https://www.esmx.dev/a/b/',
|
|
345
|
+
'./new': 'https://www.esmx.dev/a/b/new',
|
|
346
|
+
'./new/': 'https://www.esmx.dev/a/b/new/',
|
|
347
|
+
'./new/100': 'https://www.esmx.dev/a/b/new/100',
|
|
348
|
+
'./new/100/': 'https://www.esmx.dev/a/b/new/100/',
|
|
349
|
+
'.a': 'https://www.esmx.dev/a/b/.a',
|
|
350
|
+
'..a': 'https://www.esmx.dev/a/b/..a',
|
|
351
|
+
'.a/': 'https://www.esmx.dev/a/b/.a/',
|
|
352
|
+
'..a/': 'https://www.esmx.dev/a/b/..a/',
|
|
353
|
+
'new/../.': 'https://www.esmx.dev/a/b/',
|
|
354
|
+
'new/.././a/../../x/': 'https://www.esmx.dev/a/x/',
|
|
355
|
+
},
|
|
356
|
+
'https://www.esmx.dev/a/b/c/': {
|
|
357
|
+
'/': 'https://www.esmx.dev/a/b/c/',
|
|
358
|
+
'/new': 'https://www.esmx.dev/a/b/c/new',
|
|
359
|
+
'/new/': 'https://www.esmx.dev/a/b/c/new/',
|
|
360
|
+
'/new/100': 'https://www.esmx.dev/a/b/c/new/100',
|
|
361
|
+
'/new/100/': 'https://www.esmx.dev/a/b/c/new/100/',
|
|
362
|
+
'..': 'https://www.esmx.dev/a/b/',
|
|
363
|
+
'../': 'https://www.esmx.dev/a/b/',
|
|
364
|
+
'../new': 'https://www.esmx.dev/a/b/new',
|
|
365
|
+
'../new/': 'https://www.esmx.dev/a/b/new/',
|
|
366
|
+
'../new/100': 'https://www.esmx.dev/a/b/new/100',
|
|
367
|
+
'../new/100/': 'https://www.esmx.dev/a/b/new/100/',
|
|
368
|
+
'': 'https://www.esmx.dev/a/b/c/',
|
|
369
|
+
'new': 'https://www.esmx.dev/a/b/c/new',
|
|
370
|
+
'new/': 'https://www.esmx.dev/a/b/c/new/',
|
|
371
|
+
'new/100': 'https://www.esmx.dev/a/b/c/new/100',
|
|
372
|
+
'new/100/': 'https://www.esmx.dev/a/b/c/new/100/',
|
|
373
|
+
'.': 'https://www.esmx.dev/a/b/c/',
|
|
374
|
+
'./': 'https://www.esmx.dev/a/b/c/',
|
|
375
|
+
'./new': 'https://www.esmx.dev/a/b/c/new',
|
|
376
|
+
'./new/': 'https://www.esmx.dev/a/b/c/new/',
|
|
377
|
+
'./new/100': 'https://www.esmx.dev/a/b/c/new/100',
|
|
378
|
+
'./new/100/': 'https://www.esmx.dev/a/b/c/new/100/',
|
|
379
|
+
'.a': 'https://www.esmx.dev/a/b/c/.a',
|
|
380
|
+
'..a': 'https://www.esmx.dev/a/b/c/..a',
|
|
381
|
+
'.a/': 'https://www.esmx.dev/a/b/c/.a/',
|
|
382
|
+
'..a/': 'https://www.esmx.dev/a/b/c/..a/',
|
|
383
|
+
'new/.././': 'https://www.esmx.dev/a/b/c/',
|
|
384
|
+
'new/.././a/../../x/': 'https://www.esmx.dev/a/b/x/',
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
)(`base: $0`, ([base, cases]) => {
|
|
388
|
+
test.each(Object.entries(cases))(`input: $0`, (input, expected) => {
|
|
389
|
+
const url = normalizeURL(input, new URL(base));
|
|
390
|
+
expect(url).toEqURL(expected);
|
|
391
|
+
|
|
392
|
+
const pathSuffix = '?a&b=1&c=2&a=&a=4&base=10#hash';
|
|
393
|
+
const urlWithSuffix = normalizeURL(
|
|
394
|
+
input + pathSuffix,
|
|
395
|
+
new URL(base)
|
|
396
|
+
);
|
|
397
|
+
expect(urlWithSuffix).toEqURL(expected + pathSuffix);
|
|
398
|
+
|
|
399
|
+
const urlWithBaseSuffix = normalizeURL(
|
|
400
|
+
input + pathSuffix,
|
|
401
|
+
new URL(base + '?base=base#base')
|
|
402
|
+
);
|
|
403
|
+
expect(urlWithBaseSuffix).toEqURL(expected + pathSuffix);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
});
|