@shuvi/router 2.0.0-dev.16 → 2.0.0-dev.18

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.
@@ -0,0 +1,18 @@
1
+ import { IRouteMatch, PartialLocation } from './types';
2
+ import { IRouteBaseObject } from './matchRoutes';
3
+ /**
4
+ * Static Route Matching Function
5
+ *
6
+ * Optimization features:
7
+ * 1. O(1) exact match - direct HashMap lookup
8
+ * 2. Precompiled results - pre-calculate all match results at startup
9
+ * 3. Zero regex - pure string matching
10
+ * 4. Memory friendly - matcher instance reuse
11
+ * 5. Type safe - full TypeScript support
12
+ *
13
+ * @param routes Route configuration array
14
+ * @param location Location to match
15
+ * @param basename Base path
16
+ * @returns Match result or null
17
+ */
18
+ export declare function matchStaticRoutes<T extends IRouteBaseObject>(routes: T[], location: string | PartialLocation, basename?: string): IRouteMatch<T>[] | null;
@@ -0,0 +1,210 @@
1
+ import { joinPaths, normalizeBase, resolvePath, stripBase } from './utils';
2
+ /**
3
+ * Static Route Matcher Class
4
+ */
5
+ class StaticRouteMatcher {
6
+ constructor(routes) {
7
+ this.exactMatchMap = new Map();
8
+ this.allRoutes = routes;
9
+ this.prefixTree = this.buildPrefixTree(routes);
10
+ this.precompileMatches();
11
+ }
12
+ /**
13
+ * Build prefix tree
14
+ */
15
+ buildPrefixTree(routes) {
16
+ const root = {
17
+ exactRoutes: new Map(),
18
+ children: new Map(),
19
+ routes: []
20
+ };
21
+ // Flatten all routes
22
+ const flatRoutes = this.flattenRoutes(routes);
23
+ for (const { path, routeChain } of flatRoutes) {
24
+ // Only process static routes
25
+ if (this.isStaticPath(path)) {
26
+ this.insertIntoTree(root, path, routeChain);
27
+ }
28
+ }
29
+ return root;
30
+ }
31
+ /**
32
+ * Flatten route structure
33
+ */
34
+ flattenRoutes(routes, parentPath = '', parentRoutes = []) {
35
+ const result = [];
36
+ routes.forEach(route => {
37
+ let fullPath;
38
+ if (route.path === '') {
39
+ fullPath = parentPath;
40
+ }
41
+ else {
42
+ fullPath = joinPaths([parentPath, route.path]);
43
+ }
44
+ const routeChain = [...parentRoutes, route];
45
+ result.push({ path: fullPath, routeChain });
46
+ // Recursively process child routes
47
+ if (route.children) {
48
+ result.push(...this.flattenRoutes(route.children, fullPath, routeChain));
49
+ }
50
+ });
51
+ return result;
52
+ }
53
+ /**
54
+ * Check if path is static
55
+ */
56
+ isStaticPath(path) {
57
+ return !path.includes(':') && !path.includes('*') && !path.includes('(');
58
+ }
59
+ /**
60
+ * Insert route into prefix tree
61
+ */
62
+ insertIntoTree(node, path, routes) {
63
+ // Store exact match
64
+ node.exactRoutes.set(path, routes);
65
+ // Build prefix tree for prefix matching
66
+ const segments = path.split('/').filter(Boolean);
67
+ let currentNode = node;
68
+ for (const segment of segments) {
69
+ if (!currentNode.children.has(segment)) {
70
+ currentNode.children.set(segment, {
71
+ exactRoutes: new Map(),
72
+ children: new Map(),
73
+ routes: []
74
+ });
75
+ }
76
+ currentNode = currentNode.children.get(segment);
77
+ }
78
+ // Store routes at leaf node
79
+ currentNode.routes = routes;
80
+ }
81
+ /**
82
+ * Precompile all possible match results
83
+ */
84
+ precompileMatches() {
85
+ for (const [path, routeChain] of this.prefixTree.exactRoutes) {
86
+ const matches = this.buildMatches(routeChain, path);
87
+ this.exactMatchMap.set(path, {
88
+ matches,
89
+ pathname: path,
90
+ params: {} // Static routes have no parameters
91
+ });
92
+ }
93
+ }
94
+ /**
95
+ * Build match results
96
+ */
97
+ buildMatches(routeChain, matchedPathname) {
98
+ const matches = [];
99
+ let currentMatchedPath = '/';
100
+ for (let i = 0; i < routeChain.length; i++) {
101
+ const route = routeChain[i];
102
+ if (route.path === '') {
103
+ // Simulate the exact behavior of matchPathname + joinPaths
104
+ // When remainingPathname is '/' and path is '', matchPathname returns { pathname: '/' }
105
+ // Then joinPaths([currentPath, '/']) adds trailing slash
106
+ currentMatchedPath = joinPaths([currentMatchedPath, '/']);
107
+ }
108
+ else {
109
+ // For non-empty path, use the path directly
110
+ currentMatchedPath = joinPaths([currentMatchedPath, route.path]);
111
+ }
112
+ matches.push({
113
+ route,
114
+ pathname: currentMatchedPath,
115
+ params: Object.freeze({}) // Static routes have no parameters
116
+ });
117
+ }
118
+ return matches;
119
+ }
120
+ /**
121
+ * Match pathname
122
+ */
123
+ match(pathname) {
124
+ // 1. Exact match (O(1))
125
+ const exactMatch = this.exactMatchMap.get(pathname);
126
+ if (exactMatch) {
127
+ return exactMatch;
128
+ }
129
+ // 2. Prefix match (for handling trailing slash etc.)
130
+ return this.findPrefixMatch(pathname);
131
+ }
132
+ /**
133
+ * Find prefix match
134
+ */
135
+ findPrefixMatch(pathname) {
136
+ // Try adding/removing trailing slash
137
+ const alternatives = [
138
+ pathname,
139
+ pathname === '/' ? pathname : pathname.replace(/\/$/, ''),
140
+ pathname.endsWith('/') ? pathname : pathname + '/'
141
+ ];
142
+ for (const alt of alternatives) {
143
+ const match = this.exactMatchMap.get(alt);
144
+ if (match) {
145
+ return match;
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ /**
151
+ * Get all static route paths (for debugging)
152
+ */
153
+ getAllStaticPaths() {
154
+ return Array.from(this.exactMatchMap.keys()).sort();
155
+ }
156
+ /**
157
+ * Get matching statistics
158
+ */
159
+ getStats() {
160
+ return {
161
+ totalStaticRoutes: this.exactMatchMap.size,
162
+ totalRoutes: this.allRoutes.length,
163
+ staticRatio: (this.exactMatchMap.size / this.allRoutes.length * 100).toFixed(1) + '%'
164
+ };
165
+ }
166
+ }
167
+ // Global matcher cache
168
+ const staticMatcherCache = new WeakMap();
169
+ /**
170
+ * Static Route Matching Function
171
+ *
172
+ * Optimization features:
173
+ * 1. O(1) exact match - direct HashMap lookup
174
+ * 2. Precompiled results - pre-calculate all match results at startup
175
+ * 3. Zero regex - pure string matching
176
+ * 4. Memory friendly - matcher instance reuse
177
+ * 5. Type safe - full TypeScript support
178
+ *
179
+ * @param routes Route configuration array
180
+ * @param location Location to match
181
+ * @param basename Base path
182
+ * @returns Match result or null
183
+ */
184
+ export function matchStaticRoutes(routes, location, basename = '') {
185
+ // Normalize input
186
+ if (typeof location === 'string') {
187
+ location = resolvePath(location);
188
+ }
189
+ let pathname = location.pathname || '/';
190
+ // Handle basename
191
+ if (basename) {
192
+ const normalizedBasename = normalizeBase(basename);
193
+ const pathnameWithoutBase = stripBase(pathname, normalizedBasename);
194
+ if (pathnameWithoutBase) {
195
+ pathname = pathnameWithoutBase;
196
+ }
197
+ else {
198
+ return null;
199
+ }
200
+ }
201
+ // Get or create matcher
202
+ let matcher = staticMatcherCache.get(routes);
203
+ if (!matcher) {
204
+ matcher = new StaticRouteMatcher(routes);
205
+ staticMatcherCache.set(routes, matcher);
206
+ }
207
+ // Execute matching
208
+ const result = matcher.match(pathname);
209
+ return result ? result.matches : null;
210
+ }
package/esm/router.d.ts CHANGED
@@ -4,6 +4,7 @@ interface IRouterOptions<RouteRecord extends IPartialRouteRecord> {
4
4
  history: History;
5
5
  routes: RouteRecord[];
6
6
  caseSensitive?: boolean;
7
+ staticMode?: boolean;
7
8
  }
8
9
  export declare const createRouter: <RouteRecord extends IRouteRecord>(options: IRouterOptions<RouteRecord>) => IRouter<RouteRecord>;
9
10
  export {};
package/esm/router.js CHANGED
@@ -5,6 +5,7 @@ import { createEvents, resolvePath } from './utils';
5
5
  import { isError, isFunction } from './utils/error';
6
6
  import { runQueue } from './utils/async';
7
7
  import { getRedirectFromRoutes } from './getRedirectFromRoutes';
8
+ import { matchStaticRoutes } from './matchStaticRoutes';
8
9
  const START = {
9
10
  matches: [],
10
11
  params: {},
@@ -17,11 +18,12 @@ const START = {
17
18
  redirected: false
18
19
  };
19
20
  class Router {
20
- constructor({ history, routes }) {
21
+ constructor({ history, routes, staticMode }) {
21
22
  this._pending = null;
22
23
  this._cancleHandler = null;
23
24
  this._ready = false;
24
25
  this._readyDefer = createDefer();
26
+ this._staticMode = false;
25
27
  this._listeners = createEvents();
26
28
  this._beforeEachs = createEvents();
27
29
  this._beforeResolves = createEvents();
@@ -74,7 +76,7 @@ class Router {
74
76
  };
75
77
  this.match = (to) => {
76
78
  const { _routes: routes } = this;
77
- const matches = matchRoutes(routes, to);
79
+ const matches = this._staticMode ? matchStaticRoutes(routes, to) : matchRoutes(routes, to);
78
80
  return matches || [];
79
81
  };
80
82
  this.replaceRoutes = (routes) => {
@@ -102,6 +104,7 @@ class Router {
102
104
  this._history = history;
103
105
  this._routes = createRoutesFromArray(routes);
104
106
  this._current = START;
107
+ this._staticMode = !!staticMode;
105
108
  this._history.doTransition = this._doTransition.bind(this);
106
109
  }
107
110
  get ready() {
@@ -0,0 +1,18 @@
1
+ import { IRouteMatch, PartialLocation } from './types';
2
+ import { IRouteBaseObject } from './matchRoutes';
3
+ /**
4
+ * Static Route Matching Function
5
+ *
6
+ * Optimization features:
7
+ * 1. O(1) exact match - direct HashMap lookup
8
+ * 2. Precompiled results - pre-calculate all match results at startup
9
+ * 3. Zero regex - pure string matching
10
+ * 4. Memory friendly - matcher instance reuse
11
+ * 5. Type safe - full TypeScript support
12
+ *
13
+ * @param routes Route configuration array
14
+ * @param location Location to match
15
+ * @param basename Base path
16
+ * @returns Match result or null
17
+ */
18
+ export declare function matchStaticRoutes<T extends IRouteBaseObject>(routes: T[], location: string | PartialLocation, basename?: string): IRouteMatch<T>[] | null;
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchStaticRoutes = matchStaticRoutes;
4
+ const utils_1 = require("./utils");
5
+ /**
6
+ * Static Route Matcher Class
7
+ */
8
+ class StaticRouteMatcher {
9
+ constructor(routes) {
10
+ this.exactMatchMap = new Map();
11
+ this.allRoutes = routes;
12
+ this.prefixTree = this.buildPrefixTree(routes);
13
+ this.precompileMatches();
14
+ }
15
+ /**
16
+ * Build prefix tree
17
+ */
18
+ buildPrefixTree(routes) {
19
+ const root = {
20
+ exactRoutes: new Map(),
21
+ children: new Map(),
22
+ routes: []
23
+ };
24
+ // Flatten all routes
25
+ const flatRoutes = this.flattenRoutes(routes);
26
+ for (const { path, routeChain } of flatRoutes) {
27
+ // Only process static routes
28
+ if (this.isStaticPath(path)) {
29
+ this.insertIntoTree(root, path, routeChain);
30
+ }
31
+ }
32
+ return root;
33
+ }
34
+ /**
35
+ * Flatten route structure
36
+ */
37
+ flattenRoutes(routes, parentPath = '', parentRoutes = []) {
38
+ const result = [];
39
+ routes.forEach(route => {
40
+ let fullPath;
41
+ if (route.path === '') {
42
+ fullPath = parentPath;
43
+ }
44
+ else {
45
+ fullPath = (0, utils_1.joinPaths)([parentPath, route.path]);
46
+ }
47
+ const routeChain = [...parentRoutes, route];
48
+ result.push({ path: fullPath, routeChain });
49
+ // Recursively process child routes
50
+ if (route.children) {
51
+ result.push(...this.flattenRoutes(route.children, fullPath, routeChain));
52
+ }
53
+ });
54
+ return result;
55
+ }
56
+ /**
57
+ * Check if path is static
58
+ */
59
+ isStaticPath(path) {
60
+ return !path.includes(':') && !path.includes('*') && !path.includes('(');
61
+ }
62
+ /**
63
+ * Insert route into prefix tree
64
+ */
65
+ insertIntoTree(node, path, routes) {
66
+ // Store exact match
67
+ node.exactRoutes.set(path, routes);
68
+ // Build prefix tree for prefix matching
69
+ const segments = path.split('/').filter(Boolean);
70
+ let currentNode = node;
71
+ for (const segment of segments) {
72
+ if (!currentNode.children.has(segment)) {
73
+ currentNode.children.set(segment, {
74
+ exactRoutes: new Map(),
75
+ children: new Map(),
76
+ routes: []
77
+ });
78
+ }
79
+ currentNode = currentNode.children.get(segment);
80
+ }
81
+ // Store routes at leaf node
82
+ currentNode.routes = routes;
83
+ }
84
+ /**
85
+ * Precompile all possible match results
86
+ */
87
+ precompileMatches() {
88
+ for (const [path, routeChain] of this.prefixTree.exactRoutes) {
89
+ const matches = this.buildMatches(routeChain, path);
90
+ this.exactMatchMap.set(path, {
91
+ matches,
92
+ pathname: path,
93
+ params: {} // Static routes have no parameters
94
+ });
95
+ }
96
+ }
97
+ /**
98
+ * Build match results
99
+ */
100
+ buildMatches(routeChain, matchedPathname) {
101
+ const matches = [];
102
+ let currentMatchedPath = '/';
103
+ for (let i = 0; i < routeChain.length; i++) {
104
+ const route = routeChain[i];
105
+ if (route.path === '') {
106
+ // Simulate the exact behavior of matchPathname + joinPaths
107
+ // When remainingPathname is '/' and path is '', matchPathname returns { pathname: '/' }
108
+ // Then joinPaths([currentPath, '/']) adds trailing slash
109
+ currentMatchedPath = (0, utils_1.joinPaths)([currentMatchedPath, '/']);
110
+ }
111
+ else {
112
+ // For non-empty path, use the path directly
113
+ currentMatchedPath = (0, utils_1.joinPaths)([currentMatchedPath, route.path]);
114
+ }
115
+ matches.push({
116
+ route,
117
+ pathname: currentMatchedPath,
118
+ params: Object.freeze({}) // Static routes have no parameters
119
+ });
120
+ }
121
+ return matches;
122
+ }
123
+ /**
124
+ * Match pathname
125
+ */
126
+ match(pathname) {
127
+ // 1. Exact match (O(1))
128
+ const exactMatch = this.exactMatchMap.get(pathname);
129
+ if (exactMatch) {
130
+ return exactMatch;
131
+ }
132
+ // 2. Prefix match (for handling trailing slash etc.)
133
+ return this.findPrefixMatch(pathname);
134
+ }
135
+ /**
136
+ * Find prefix match
137
+ */
138
+ findPrefixMatch(pathname) {
139
+ // Try adding/removing trailing slash
140
+ const alternatives = [
141
+ pathname,
142
+ pathname === '/' ? pathname : pathname.replace(/\/$/, ''),
143
+ pathname.endsWith('/') ? pathname : pathname + '/'
144
+ ];
145
+ for (const alt of alternatives) {
146
+ const match = this.exactMatchMap.get(alt);
147
+ if (match) {
148
+ return match;
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+ /**
154
+ * Get all static route paths (for debugging)
155
+ */
156
+ getAllStaticPaths() {
157
+ return Array.from(this.exactMatchMap.keys()).sort();
158
+ }
159
+ /**
160
+ * Get matching statistics
161
+ */
162
+ getStats() {
163
+ return {
164
+ totalStaticRoutes: this.exactMatchMap.size,
165
+ totalRoutes: this.allRoutes.length,
166
+ staticRatio: (this.exactMatchMap.size / this.allRoutes.length * 100).toFixed(1) + '%'
167
+ };
168
+ }
169
+ }
170
+ // Global matcher cache
171
+ const staticMatcherCache = new WeakMap();
172
+ /**
173
+ * Static Route Matching Function
174
+ *
175
+ * Optimization features:
176
+ * 1. O(1) exact match - direct HashMap lookup
177
+ * 2. Precompiled results - pre-calculate all match results at startup
178
+ * 3. Zero regex - pure string matching
179
+ * 4. Memory friendly - matcher instance reuse
180
+ * 5. Type safe - full TypeScript support
181
+ *
182
+ * @param routes Route configuration array
183
+ * @param location Location to match
184
+ * @param basename Base path
185
+ * @returns Match result or null
186
+ */
187
+ function matchStaticRoutes(routes, location, basename = '') {
188
+ // Normalize input
189
+ if (typeof location === 'string') {
190
+ location = (0, utils_1.resolvePath)(location);
191
+ }
192
+ let pathname = location.pathname || '/';
193
+ // Handle basename
194
+ if (basename) {
195
+ const normalizedBasename = (0, utils_1.normalizeBase)(basename);
196
+ const pathnameWithoutBase = (0, utils_1.stripBase)(pathname, normalizedBasename);
197
+ if (pathnameWithoutBase) {
198
+ pathname = pathnameWithoutBase;
199
+ }
200
+ else {
201
+ return null;
202
+ }
203
+ }
204
+ // Get or create matcher
205
+ let matcher = staticMatcherCache.get(routes);
206
+ if (!matcher) {
207
+ matcher = new StaticRouteMatcher(routes);
208
+ staticMatcherCache.set(routes, matcher);
209
+ }
210
+ // Execute matching
211
+ const result = matcher.match(pathname);
212
+ return result ? result.matches : null;
213
+ }
package/lib/router.d.ts CHANGED
@@ -4,6 +4,7 @@ interface IRouterOptions<RouteRecord extends IPartialRouteRecord> {
4
4
  history: History;
5
5
  routes: RouteRecord[];
6
6
  caseSensitive?: boolean;
7
+ staticMode?: boolean;
7
8
  }
8
9
  export declare const createRouter: <RouteRecord extends IRouteRecord>(options: IRouterOptions<RouteRecord>) => IRouter<RouteRecord>;
9
10
  export {};
package/lib/router.js CHANGED
@@ -8,6 +8,7 @@ const utils_1 = require("./utils");
8
8
  const error_1 = require("./utils/error");
9
9
  const async_1 = require("./utils/async");
10
10
  const getRedirectFromRoutes_1 = require("./getRedirectFromRoutes");
11
+ const matchStaticRoutes_1 = require("./matchStaticRoutes");
11
12
  const START = {
12
13
  matches: [],
13
14
  params: {},
@@ -20,11 +21,12 @@ const START = {
20
21
  redirected: false
21
22
  };
22
23
  class Router {
23
- constructor({ history, routes }) {
24
+ constructor({ history, routes, staticMode }) {
24
25
  this._pending = null;
25
26
  this._cancleHandler = null;
26
27
  this._ready = false;
27
28
  this._readyDefer = (0, defer_1.createDefer)();
29
+ this._staticMode = false;
28
30
  this._listeners = (0, utils_1.createEvents)();
29
31
  this._beforeEachs = (0, utils_1.createEvents)();
30
32
  this._beforeResolves = (0, utils_1.createEvents)();
@@ -77,7 +79,7 @@ class Router {
77
79
  };
78
80
  this.match = (to) => {
79
81
  const { _routes: routes } = this;
80
- const matches = (0, matchRoutes_1.matchRoutes)(routes, to);
82
+ const matches = this._staticMode ? (0, matchStaticRoutes_1.matchStaticRoutes)(routes, to) : (0, matchRoutes_1.matchRoutes)(routes, to);
81
83
  return matches || [];
82
84
  };
83
85
  this.replaceRoutes = (routes) => {
@@ -105,6 +107,7 @@ class Router {
105
107
  this._history = history;
106
108
  this._routes = (0, createRoutesFromArray_1.createRoutesFromArray)(routes);
107
109
  this._current = START;
110
+ this._staticMode = !!staticMode;
108
111
  this._history.doTransition = this._doTransition.bind(this);
109
112
  }
110
113
  get ready() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shuvi/router",
3
- "version": "2.0.0-dev.16",
3
+ "version": "2.0.0-dev.18",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/shuvijs/shuvi.git",
@@ -28,7 +28,7 @@
28
28
  "node": ">= 16.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@shuvi/utils": "2.0.0-dev.16",
31
+ "@shuvi/utils": "2.0.0-dev.18",
32
32
  "query-string": "6.13.8"
33
33
  }
34
34
  }