@esmx/router 3.0.0-rc.12

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 (61) hide show
  1. package/dist/history/abstract.d.ts +29 -0
  2. package/dist/history/abstract.mjs +107 -0
  3. package/dist/history/base.d.ts +79 -0
  4. package/dist/history/base.mjs +275 -0
  5. package/dist/history/html.d.ts +22 -0
  6. package/dist/history/html.mjs +181 -0
  7. package/dist/history/index.d.ts +7 -0
  8. package/dist/history/index.mjs +16 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.mjs +3 -0
  11. package/dist/matcher/create-matcher.d.ts +5 -0
  12. package/dist/matcher/create-matcher.mjs +218 -0
  13. package/dist/matcher/create-matcher.spec.d.ts +1 -0
  14. package/dist/matcher/create-matcher.spec.mjs +0 -0
  15. package/dist/matcher/index.d.ts +1 -0
  16. package/dist/matcher/index.mjs +1 -0
  17. package/dist/router.d.ts +111 -0
  18. package/dist/router.mjs +399 -0
  19. package/dist/task-pipe/index.d.ts +1 -0
  20. package/dist/task-pipe/index.mjs +1 -0
  21. package/dist/task-pipe/task.d.ts +30 -0
  22. package/dist/task-pipe/task.mjs +66 -0
  23. package/dist/utils/bom.d.ts +5 -0
  24. package/dist/utils/bom.mjs +10 -0
  25. package/dist/utils/encoding.d.ts +48 -0
  26. package/dist/utils/encoding.mjs +44 -0
  27. package/dist/utils/guards.d.ts +9 -0
  28. package/dist/utils/guards.mjs +12 -0
  29. package/dist/utils/index.d.ts +7 -0
  30. package/dist/utils/index.mjs +27 -0
  31. package/dist/utils/path.d.ts +60 -0
  32. package/dist/utils/path.mjs +264 -0
  33. package/dist/utils/path.spec.d.ts +1 -0
  34. package/dist/utils/path.spec.mjs +30 -0
  35. package/dist/utils/scroll.d.ts +25 -0
  36. package/dist/utils/scroll.mjs +59 -0
  37. package/dist/utils/utils.d.ts +16 -0
  38. package/dist/utils/utils.mjs +11 -0
  39. package/dist/utils/warn.d.ts +2 -0
  40. package/dist/utils/warn.mjs +12 -0
  41. package/package.json +66 -0
  42. package/src/history/abstract.ts +149 -0
  43. package/src/history/base.ts +408 -0
  44. package/src/history/html.ts +231 -0
  45. package/src/history/index.ts +20 -0
  46. package/src/index.ts +3 -0
  47. package/src/matcher/create-matcher.spec.ts +3 -0
  48. package/src/matcher/create-matcher.ts +293 -0
  49. package/src/matcher/index.ts +1 -0
  50. package/src/router.ts +521 -0
  51. package/src/task-pipe/index.ts +1 -0
  52. package/src/task-pipe/task.ts +97 -0
  53. package/src/utils/bom.ts +14 -0
  54. package/src/utils/encoding.ts +153 -0
  55. package/src/utils/guards.ts +25 -0
  56. package/src/utils/index.ts +27 -0
  57. package/src/utils/path.spec.ts +44 -0
  58. package/src/utils/path.ts +397 -0
  59. package/src/utils/scroll.ts +120 -0
  60. package/src/utils/utils.ts +30 -0
  61. package/src/utils/warn.ts +13 -0
@@ -0,0 +1,60 @@
1
+ import type { Route, RouterBase, RouterLocation, RouterRawLocation } from '../types';
2
+ /**
3
+ * 判断路径是否以 http 或 https 开头 或者直接是域名开头
4
+ */
5
+ export declare const regexDomain: RegExp;
6
+ /**
7
+ * 判断路径是否以 scheme 协议开头
8
+ */
9
+ export declare const regexScheme: RegExp;
10
+ /**
11
+ * 判断路径是否以 http(s) 协议开头
12
+ */
13
+ export declare const regexHttpScheme: RegExp;
14
+ /**
15
+ * 格式化路径 主要用于拼接嵌套路由的路径
16
+ * 返回的格式化后的路径如果有协议则以协议开头,如果没有协议则以/开头
17
+ */
18
+ export declare function normalizePath(path: string, parentPath?: string): string;
19
+ /**
20
+ * 路径解析方法
21
+ * @example 输入 https://www.google.com/test1/test2?a=1&b=2#123 输出 { pathname: '/test1/test2', query: { a: '1', b: '2' }, queryArray: { a: ['1'], b: ['2'] }, hash: '123' }
22
+ * 输入 /test1/test2?a=1&b=2#123 同样输出 { pathname: '/test1/test2', query: { a: '1', b: '2' }, queryArray: { a: ['1'], b: ['2'] }, hash: '123' }
23
+ */
24
+ export declare function parsePath(path?: string): {
25
+ pathname: string;
26
+ query: Record<string, string>;
27
+ queryArray: Record<string, string[]>;
28
+ hash: string;
29
+ };
30
+ /**
31
+ * 将path query hash合并为完整路径
32
+ * @example stringifyPath({ pathname: '/news', query: { a: '1' }, hash: '123' }) 输出 '/news?a=1#123'
33
+ */
34
+ export declare function stringifyPath({ pathname, query, queryArray, hash }?: {
35
+ pathname: string;
36
+ query: Record<string, string | undefined>;
37
+ queryArray: Record<string, string[]>;
38
+ hash: string;
39
+ }): string;
40
+ /**
41
+ * 标准化 RouterLocation 字段
42
+ */
43
+ export declare function normalizeLocation(rawLocation: RouterRawLocation, base?: RouterBase): RouterLocation & {
44
+ path: string;
45
+ base: string;
46
+ queryArray: Record<string, string[]>;
47
+ };
48
+ /**
49
+ * 判断路径是否以协议或域名开头
50
+ */
51
+ export declare function isPathWithProtocolOrDomain(location: RouterRawLocation): {
52
+ /**
53
+ * 是否以协议或域名开头
54
+ */
55
+ flag: boolean;
56
+ /**
57
+ * 虚假的 route 信息,内部跳转时无法信任,只有外站跳转时使用
58
+ */
59
+ route: Route;
60
+ };
@@ -0,0 +1,264 @@
1
+ import URLParse from "url-parse";
2
+ import {
3
+ decode,
4
+ decodeQuery,
5
+ encodeHash,
6
+ encodeQueryKey,
7
+ encodeQueryValue
8
+ } from "./encoding.mjs";
9
+ import { isValidValue } from "./utils.mjs";
10
+ import { warn } from "./warn.mjs";
11
+ export const regexDomain = /^(?:https?:\/\/|[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9](\/.*)?/i;
12
+ export const regexScheme = /^(?:[a-z][a-z\d+.-]*:.+)/i;
13
+ export const regexHttpScheme = /^(http(s)?:\/\/)/;
14
+ function removeDuplicateSlashes(url) {
15
+ if (url.includes("://")) {
16
+ const [base, path] = url.split("://");
17
+ const result2 = path.replace(/\/{2,}/g, "/");
18
+ return `${base}://${result2}`;
19
+ }
20
+ const result = url.replace(/\/{2,}/g, "/");
21
+ return result;
22
+ }
23
+ export function normalizePath(path, parentPath) {
24
+ let normalizedPath = parentPath ? `${parentPath}/${path}` : `${path}`;
25
+ if (!regexHttpScheme.test(normalizedPath) && !normalizedPath.startsWith("/")) {
26
+ normalizedPath = `/${normalizedPath}`;
27
+ }
28
+ return (
29
+ // 只有存在父级路由才进行路由拼接
30
+ removeDuplicateSlashes(normalizedPath).replace(/\/$/, "") || // 移除结尾的斜杠 /
31
+ "/"
32
+ );
33
+ }
34
+ export function parsePath(path = "") {
35
+ path = normalizePath(path);
36
+ const { pathname, query, hash } = new URLParse(path || "/");
37
+ const queryObj = {};
38
+ const queryArray = {};
39
+ if (query.length > 0) {
40
+ query.slice(1).split("&").forEach((item) => {
41
+ let [key = "", value = ""] = item.split("=");
42
+ key = decode(key);
43
+ value = decodeQuery(value);
44
+ if (key) {
45
+ queryObj[key] = value;
46
+ queryArray[key] = (queryArray[key] || []).concat(value);
47
+ }
48
+ });
49
+ }
50
+ return {
51
+ pathname,
52
+ query: queryObj,
53
+ queryArray,
54
+ hash: decode(hash)
55
+ };
56
+ }
57
+ export function stringifyPath({
58
+ pathname = "",
59
+ query = {},
60
+ queryArray = {},
61
+ hash = ""
62
+ } = {
63
+ pathname: "",
64
+ query: {},
65
+ queryArray: {},
66
+ hash: ""
67
+ }) {
68
+ const queryString = Object.entries(
69
+ Object.assign({}, query, queryArray)
70
+ ).reduce((acc, [key, value]) => {
71
+ let query2 = "";
72
+ const encodedKey = encodeQueryKey(key);
73
+ if (Array.isArray(value)) {
74
+ query2 = value.reduce((all, item) => {
75
+ if (!isValidValue(item)) return all;
76
+ const encodedValue = encodeQueryValue(item);
77
+ if (encodedValue) {
78
+ all = all ? `${all}&${encodedKey}=${encodedValue}` : `${encodedKey}=${encodedValue}`;
79
+ }
80
+ return all;
81
+ }, "");
82
+ } else {
83
+ const encodedValue = encodeQueryValue(value);
84
+ if (isValidValue(value)) {
85
+ query2 = `${encodedKey}=${encodedValue}`;
86
+ }
87
+ }
88
+ if (query2) {
89
+ acc = acc ? `${acc}&${query2}` : `?${query2}`;
90
+ }
91
+ return acc;
92
+ }, "");
93
+ const hashContent = hash.startsWith("#") ? hash.replace(/^#/, "") : hash;
94
+ const hashString = hashContent ? `#${encodeHash(hashContent)}` : "";
95
+ return `${pathname}${queryString}${hashString}`;
96
+ }
97
+ export function normalizeLocation(rawLocation, base = "") {
98
+ let pathname = "";
99
+ let query = {};
100
+ let queryArray = {};
101
+ let hash = "";
102
+ let params;
103
+ let state = {};
104
+ if (typeof rawLocation === "object") {
105
+ const parsedOption = parsePath(rawLocation.path);
106
+ pathname = parsedOption.pathname;
107
+ if (rawLocation.query || rawLocation.queryArray) {
108
+ queryArray = rawLocation.queryArray || {};
109
+ query = rawLocation.query || {};
110
+ } else {
111
+ queryArray = parsedOption.queryArray;
112
+ query = parsedOption.query;
113
+ }
114
+ hash = rawLocation.hash || parsedOption.hash;
115
+ params = rawLocation.params;
116
+ state = rawLocation.state || {};
117
+ } else {
118
+ const parsedOption = parsePath(rawLocation);
119
+ pathname = parsedOption.pathname;
120
+ query = parsedOption.query;
121
+ queryArray = parsedOption.queryArray;
122
+ hash = parsedOption.hash;
123
+ }
124
+ const fullPath = stringifyPath({
125
+ pathname,
126
+ query,
127
+ queryArray,
128
+ hash
129
+ });
130
+ const baseString = normalizePath(
131
+ typeof base === "function" ? base({
132
+ fullPath,
133
+ query,
134
+ queryArray,
135
+ hash
136
+ }) : base
137
+ );
138
+ let path = pathname;
139
+ if (regexDomain.test(baseString)) {
140
+ const { pathname: pathname2 } = new URLParse(baseString);
141
+ path = normalizePath(path.replace(new RegExp(`^(${pathname2})`), ""));
142
+ }
143
+ path = normalizePath(path.replace(new RegExp(`^(${baseString})`), ""));
144
+ const { query: realQuery, queryArray: realQueryArray } = parsePath(fullPath);
145
+ const res = {
146
+ base: baseString,
147
+ path,
148
+ query: realQuery,
149
+ queryArray: realQueryArray,
150
+ hash,
151
+ state
152
+ };
153
+ if (params) res.params = params;
154
+ return res;
155
+ }
156
+ export function isPathWithProtocolOrDomain(location) {
157
+ let url = "";
158
+ let state = {};
159
+ if (typeof location === "string") {
160
+ url = location;
161
+ } else {
162
+ state = location.state || {};
163
+ const {
164
+ path,
165
+ query: query2 = {},
166
+ queryArray: queryArray2,
167
+ hash: hash2 = "",
168
+ ...nLocation
169
+ } = normalizeLocation(location);
170
+ url = stringifyPath({
171
+ ...nLocation,
172
+ query: query2,
173
+ queryArray: queryArray2,
174
+ pathname: path,
175
+ hash: hash2
176
+ });
177
+ }
178
+ if (regexScheme.test(url) && !regexHttpScheme.test(url)) {
179
+ try {
180
+ const {
181
+ hash: hash2,
182
+ host: host2,
183
+ hostname: hostname2,
184
+ href: href2,
185
+ origin: origin2,
186
+ pathname: pathname2,
187
+ port: port2,
188
+ protocol: protocol2,
189
+ search: search2
190
+ } = new URL(url);
191
+ const route2 = {
192
+ hash: hash2,
193
+ host: host2,
194
+ hostname: hostname2,
195
+ href: href2,
196
+ origin: origin2,
197
+ pathname: pathname2,
198
+ port: port2,
199
+ protocol: protocol2,
200
+ search: search2,
201
+ params: {},
202
+ query: {},
203
+ queryArray: {},
204
+ state,
205
+ meta: {},
206
+ path: pathname2,
207
+ fullPath: url,
208
+ base: "",
209
+ matched: []
210
+ };
211
+ return {
212
+ flag: true,
213
+ route: route2
214
+ };
215
+ } catch (error) {
216
+ warn(error);
217
+ }
218
+ }
219
+ if (!/^https?:\/\//i.test(url)) {
220
+ url = `http://${url}`;
221
+ }
222
+ const {
223
+ hash,
224
+ host,
225
+ hostname,
226
+ href,
227
+ origin,
228
+ pathname,
229
+ port,
230
+ protocol,
231
+ query: search
232
+ } = new URLParse(url);
233
+ const { query = {}, queryArray } = normalizeLocation(url);
234
+ const route = {
235
+ href,
236
+ origin,
237
+ host,
238
+ protocol,
239
+ hostname,
240
+ port,
241
+ pathname,
242
+ search,
243
+ hash,
244
+ query,
245
+ queryArray,
246
+ params: {},
247
+ state,
248
+ meta: {},
249
+ path: pathname,
250
+ fullPath: `${pathname}${search}${hash}`,
251
+ base: "",
252
+ matched: []
253
+ };
254
+ if (regexDomain.test(url)) {
255
+ return {
256
+ flag: true,
257
+ route
258
+ };
259
+ }
260
+ return {
261
+ flag: false,
262
+ route
263
+ };
264
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { describe, it } from "vitest";
2
+ import {
3
+ normalizeLocation,
4
+ normalizePath
5
+ } from "./path.mjs";
6
+ describe("testing normalizeLocation", () => {
7
+ it("testing normal domain", ({ expect }) => {
8
+ expect(
9
+ normalizeLocation(
10
+ "http://localhost:5173/en/en/en/en/en",
11
+ "http://localhost:5173/en/"
12
+ ).path
13
+ ).toBe("/en/en/en/en");
14
+ });
15
+ it("testing path", ({ expect }) => {
16
+ expect(
17
+ normalizeLocation("/test1/test2?t=https://www-six.betafollowme.com").path
18
+ ).toBe("/test1/test2");
19
+ });
20
+ });
21
+ describe("testing normalizePath", () => {
22
+ it("testing normal domain", ({ expect }) => {
23
+ expect(normalizePath("test2", "test1")).toBe("/test1/test2");
24
+ });
25
+ it("testing path", ({ expect }) => {
26
+ expect(
27
+ normalizeLocation("/test1/test2?t=https://www-six.betafollowme.com").path
28
+ ).toBe("/test1/test2");
29
+ });
30
+ });
@@ -0,0 +1,25 @@
1
+ import type { RouterRawLocation, ScrollPosition, _ScrollPositionNormalized } from '../types';
2
+ /**
3
+ * 获取当前滚动位置
4
+ */
5
+ export declare const computeScrollPosition: () => _ScrollPositionNormalized;
6
+ /**
7
+ * 滚动到指定位置
8
+ */
9
+ export declare function scrollToPosition(position: ScrollPosition): void;
10
+ /**
11
+ * 存储的滚动位置
12
+ */
13
+ export declare const scrollPositions: Map<string, _ScrollPositionNormalized>;
14
+ /**
15
+ * 保存滚动位置
16
+ */
17
+ export declare function saveScrollPosition(key: string, scrollPosition: _ScrollPositionNormalized): void;
18
+ /**
19
+ * 获取存储的滚动位置
20
+ */
21
+ export declare function getSavedScrollPosition(key: string): _ScrollPositionNormalized | null;
22
+ /**
23
+ * 获取跳转配置的保持滚动位置参数
24
+ */
25
+ export declare function getKeepScrollPosition(location: RouterRawLocation): boolean;
@@ -0,0 +1,59 @@
1
+ import { warn } from "./warn.mjs";
2
+ export const computeScrollPosition = () => ({
3
+ left: window.scrollX,
4
+ top: window.scrollY
5
+ });
6
+ function getElementPosition(el, offset) {
7
+ const docRect = document.documentElement.getBoundingClientRect();
8
+ const elRect = el.getBoundingClientRect();
9
+ return {
10
+ behavior: offset.behavior,
11
+ left: elRect.left - docRect.left - (offset.left || 0),
12
+ top: elRect.top - docRect.top - (offset.top || 0)
13
+ };
14
+ }
15
+ export function scrollToPosition(position) {
16
+ let scrollToOptions;
17
+ if ("el" in position) {
18
+ const positionEl = position.el;
19
+ const isIdSelector = typeof positionEl === "string" && positionEl.startsWith("#");
20
+ const el = typeof positionEl === "string" ? isIdSelector ? document.getElementById(positionEl.slice(1)) : document.querySelector(positionEl) : positionEl;
21
+ if (!el) {
22
+ return;
23
+ }
24
+ scrollToOptions = getElementPosition(el, position);
25
+ } else {
26
+ scrollToOptions = position;
27
+ }
28
+ if ("scrollBehavior" in document.documentElement.style) {
29
+ window.scrollTo(scrollToOptions);
30
+ } else {
31
+ window.scrollTo(
32
+ scrollToOptions.left != null ? scrollToOptions.left : window.scrollX,
33
+ scrollToOptions.top != null ? scrollToOptions.top : window.scrollY
34
+ );
35
+ }
36
+ }
37
+ export const scrollPositions = /* @__PURE__ */ new Map();
38
+ const POSITION_KEY = "__scroll_position_key";
39
+ export function saveScrollPosition(key, scrollPosition) {
40
+ scrollPositions.set(key, scrollPosition);
41
+ const stateCopy = Object.assign({}, window.history.state);
42
+ stateCopy[POSITION_KEY] = scrollPosition;
43
+ try {
44
+ const protocolAndPath = window.location.protocol + "//" + window.location.host;
45
+ const absolutePath = window.location.href.replace(protocolAndPath, "");
46
+ window.history.replaceState(stateCopy, "", absolutePath);
47
+ } catch (error) {
48
+ warn(`Failed to save scroll position.`, error);
49
+ }
50
+ }
51
+ export function getSavedScrollPosition(key) {
52
+ const scroll = scrollPositions.get(key) || history.state[POSITION_KEY];
53
+ scrollPositions.delete(key);
54
+ return scroll || null;
55
+ }
56
+ export function getKeepScrollPosition(location) {
57
+ if (typeof location === "string") return false;
58
+ return location.keepScrollPosition || false;
59
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 是否在浏览器环境
3
+ */
4
+ export declare const inBrowser: boolean;
5
+ /**
6
+ * Symbol 是否可用
7
+ */
8
+ export declare const isSymbolAble: boolean;
9
+ /**
10
+ * 判断是否是 es module 对象
11
+ */
12
+ export declare function isESModule(obj: any): boolean;
13
+ /**
14
+ * 判断是否是合法的值
15
+ */
16
+ export declare function isValidValue(value: any): boolean;
@@ -0,0 +1,11 @@
1
+ export const inBrowser = typeof window !== "undefined";
2
+ export const isSymbolAble = typeof Symbol === "function" && typeof Symbol.toStringTag === "symbol";
3
+ export function isESModule(obj) {
4
+ return Boolean(obj.__esModule) || isSymbolAble && obj[Symbol.toStringTag] === "Module";
5
+ }
6
+ export function isValidValue(value) {
7
+ if (value === null) return false;
8
+ if (value === void 0) return false;
9
+ if (typeof value === "number" && isNaN(value)) return false;
10
+ return true;
11
+ }
@@ -0,0 +1,2 @@
1
+ export declare function assert(condition: boolean, message: string): void;
2
+ export declare function warn(...args: any[]): void;
@@ -0,0 +1,12 @@
1
+ export function assert(condition, message) {
2
+ if (!condition) {
3
+ throw new Error(`[@esmx/router] ${message}`);
4
+ }
5
+ }
6
+ export function warn(...args) {
7
+ console.log(
8
+ "%c ROUTER WARNING:",
9
+ "color: rgb(214, 77, 77); font-weight: bold;",
10
+ ...args
11
+ );
12
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@esmx/router",
3
+ "template": "library",
4
+ "scripts": {
5
+ "lint:js": "biome check --write --no-errors-on-unmatched",
6
+ "lint:css": "stylelint '**/*.{css,vue}' --fix --aei",
7
+ "lint:type": "tsc --noEmit",
8
+ "test": "vitest run --pass-with-no-tests",
9
+ "coverage": "vitest run --coverage --pass-with-no-tests",
10
+ "build": "unbuild"
11
+ },
12
+ "contributors": [
13
+ {
14
+ "name": "lzxb",
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"
33
+ },
34
+ "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"
45
+ },
46
+ "version": "3.0.0-rc.12",
47
+ "type": "module",
48
+ "private": false,
49
+ "exports": {
50
+ ".": {
51
+ "import": "./dist/index.mjs",
52
+ "types": "./dist/index.d.ts"
53
+ }
54
+ },
55
+ "module": "dist/index.mjs",
56
+ "types": "./dist/index.d.ts",
57
+ "files": [
58
+ "lib",
59
+ "src",
60
+ "dist",
61
+ "*.mjs",
62
+ "template",
63
+ "public"
64
+ ],
65
+ "gitHead": "7d2c2fc4fe2cc98ebdbc12560f19637ca04398e5"
66
+ }