webspresso 0.0.101 → 0.0.103

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 (62) hide show
  1. package/.agents/AGENTS.md +1 -1
  2. package/.agents/ARCHITECTURE.md +11 -0
  3. package/.agents/CONVENTIONS.md +4 -0
  4. package/.agents/ORM.md +13 -4
  5. package/.agents/PLUGINS.md +32 -6
  6. package/.agents/ROUTING.md +99 -2
  7. package/.agents/skills/webspresso-framework/SKILL.md +32 -0
  8. package/.agents/skills/webspresso-orm-and-database/SKILL.md +4 -5
  9. package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
  10. package/AGENTS.md +6 -0
  11. package/README.md +20 -7
  12. package/bin/commands/add-tailwind.js +10 -0
  13. package/bin/commands/build.js +34 -0
  14. package/bin/commands/routes.js +8 -0
  15. package/core/applySchema.js +18 -1
  16. package/core/auth/jwt.js +10 -17
  17. package/core/auth/user-agent.js +61 -42
  18. package/core/compression/compression-stream.js +2 -2
  19. package/core/compression/middleware.js +1 -1
  20. package/core/compression/negotiate.js +11 -4
  21. package/core/events/dispatcher.js +56 -33
  22. package/core/openapi/orm-components.js +2 -3
  23. package/core/orm/cache/listeners.js +12 -2
  24. package/core/orm/json-fields.js +7 -1
  25. package/core/orm/model.js +99 -8
  26. package/core/orm/query-builder.js +71 -25
  27. package/core/orm/repository.js +42 -17
  28. package/core/orm/schema-helpers.js +49 -0
  29. package/core/orm/scopes.js +40 -9
  30. package/core/orm/types.js +12 -1
  31. package/core/queue/manager.js +5 -0
  32. package/core/validation/index.js +9 -3
  33. package/index.d.ts +178 -6
  34. package/index.js +71 -16
  35. package/package.json +6 -5
  36. package/plugins/admin-panel/index.js +12 -0
  37. package/plugins/audit-log/middleware.js +39 -4
  38. package/plugins/csrf/index.js +1 -1
  39. package/plugins/email/service.js +32 -1
  40. package/plugins/file-manager/api-handlers.js +10 -17
  41. package/plugins/file-manager/index.js +24 -9
  42. package/plugins/file-manager/security.js +18 -6
  43. package/plugins/index.js +2 -0
  44. package/plugins/lazy.js +258 -0
  45. package/plugins/site-analytics/bot-patterns.js +3 -0
  46. package/plugins/site-analytics/client-error-handler.js +3 -1
  47. package/plugins/site-analytics/index.js +10 -1
  48. package/plugins/site-analytics/tracking.js +245 -46
  49. package/src/api/api-loader.js +18 -2
  50. package/src/api/validation-i18n.js +231 -0
  51. package/src/discovery/route-manifest.js +197 -0
  52. package/src/discovery/scan-routes.js +84 -8
  53. package/src/file-router.js +215 -97
  54. package/src/helpers.js +139 -26
  55. package/src/njk-frontmatter.js +17 -1
  56. package/src/plugin-manager.js +46 -13
  57. package/src/routing/route-table.js +15 -5
  58. package/src/server.js +86 -15
  59. package/src/services/builtins/system.js +4 -6
  60. package/src/services/registry.js +12 -2
  61. package/src/services/validator.js +57 -32
  62. package/src/template/precompile.js +247 -0
package/src/helpers.js CHANGED
@@ -21,6 +21,25 @@ dayjs.extend(customParseFormat);
21
21
  // Cache for isPath regex patterns
22
22
  const isPathRegexCache = new Map();
23
23
 
24
+ // Caches for manifest files and filesystem existence checks
25
+ const manifestFileCache = new Map();
26
+ const diskExistenceCache = new Map();
27
+
28
+ function cachedExistsSync(filePath) {
29
+ const cached = diskExistenceCache.get(filePath);
30
+ if (cached !== undefined) return cached;
31
+ let exists = false;
32
+ try {
33
+ exists = fs.existsSync(filePath);
34
+ } catch (_) {
35
+ exists = false;
36
+ }
37
+ if (diskExistenceCache.size < 2000) {
38
+ diskExistenceCache.set(filePath, exists);
39
+ }
40
+ return exists;
41
+ }
42
+
24
43
  /**
25
44
  * Asset Manager - handles asset paths, versioning, and manifest
26
45
  */
@@ -34,6 +53,12 @@ class AssetManager {
34
53
  this.version = options.version || null;
35
54
  this.prefix = options.prefix || '';
36
55
 
56
+ // Per-instance fast-path caches
57
+ this._resolveCache = new Map();
58
+ this._hasJsCache = new Map();
59
+ this._hasCssCache = new Map();
60
+ this._lookupIndex = null;
61
+
37
62
  // Auto-detect manifest if not explicitly provided
38
63
  if (!this.manifestPath) {
39
64
  const candidates = [
@@ -42,7 +67,7 @@ class AssetManager {
42
67
  path.join(this.publicDir, '.vite', 'manifest.json'),
43
68
  ];
44
69
  for (const candidate of candidates) {
45
- if (fs.existsSync(candidate)) {
70
+ if (cachedExistsSync(candidate)) {
46
71
  this.manifestPath = candidate;
47
72
  break;
48
73
  }
@@ -54,6 +79,25 @@ class AssetManager {
54
79
  this.loadManifest();
55
80
  }
56
81
  }
82
+
83
+ /**
84
+ * Pre-build lookup index for fast O(1) fallback matching
85
+ */
86
+ _buildLookupIndex() {
87
+ if (!this.manifest || typeof this.manifest !== 'object') {
88
+ this._lookupIndex = null;
89
+ return;
90
+ }
91
+ const idx = new Map();
92
+ for (const [key, val] of Object.entries(this.manifest)) {
93
+ idx.set(key, val);
94
+ const base = path.basename(key);
95
+ if (!idx.has(base)) {
96
+ idx.set(base, val);
97
+ }
98
+ }
99
+ this._lookupIndex = idx;
100
+ }
57
101
 
58
102
  /**
59
103
  * Load asset manifest file (Vite, Webpack, etc.)
@@ -62,12 +106,35 @@ class AssetManager {
62
106
  try {
63
107
  const fullPath = path.resolve(this.manifestPath);
64
108
  if (fs.existsSync(fullPath)) {
65
- const content = fs.readFileSync(fullPath, 'utf-8');
66
- this.manifest = JSON.parse(content);
109
+ try {
110
+ const stats = fs.statSync(fullPath);
111
+ const cached = manifestFileCache.get(fullPath);
112
+ if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
113
+ this.manifest = cached.manifest;
114
+ this._lookupIndex = cached.lookupIndex;
115
+ return;
116
+ }
117
+ const content = fs.readFileSync(fullPath, 'utf-8');
118
+ this.manifest = JSON.parse(content);
119
+ this._buildLookupIndex();
120
+ manifestFileCache.set(fullPath, {
121
+ mtimeMs: stats.mtimeMs,
122
+ size: stats.size,
123
+ manifest: this.manifest,
124
+ lookupIndex: this._lookupIndex,
125
+ });
126
+ return;
127
+ } catch (_) {
128
+ const content = fs.readFileSync(fullPath, 'utf-8');
129
+ this.manifest = JSON.parse(content);
130
+ this._buildLookupIndex();
131
+ return;
132
+ }
67
133
  }
68
134
  } catch (err) {
69
135
  console.warn('Failed to load asset manifest:', err.message);
70
136
  this.manifest = null;
137
+ this._lookupIndex = null;
71
138
  }
72
139
  }
73
140
 
@@ -77,6 +144,9 @@ class AssetManager {
77
144
  * @returns {string}
78
145
  */
79
146
  resolve(assetPath) {
147
+ const cached = this._resolveCache.get(assetPath);
148
+ if (cached !== undefined) return cached;
149
+
80
150
  // Remove leading slash for manifest lookup
81
151
  const lookupPath = assetPath.replace(/^\//, '');
82
152
 
@@ -85,7 +155,12 @@ class AssetManager {
85
155
  // 1. Direct match with lookupPath or assetPath
86
156
  let entry = this.manifest[lookupPath] || this.manifest[assetPath];
87
157
 
88
- // 2. Flexible fallback: match by basename or trailing path
158
+ // 2. Fast lookup index match
159
+ if (!entry && this._lookupIndex) {
160
+ entry = this._lookupIndex.get(lookupPath) || this._lookupIndex.get(path.basename(lookupPath));
161
+ }
162
+
163
+ // 3. Flexible fallback: match by basename or trailing path
89
164
  if (!entry) {
90
165
  const baseName = path.basename(lookupPath);
91
166
  for (const [key, val] of Object.entries(this.manifest)) {
@@ -102,14 +177,20 @@ class AssetManager {
102
177
  }
103
178
 
104
179
  if (entry) {
180
+ let result;
105
181
  // Webpack string format (e.g. "/dist/app.xyz789.js")
106
182
  if (typeof entry === 'string') {
107
- return this.prefix + entry;
108
- }
109
- // Vite / esbuild metafile format (e.g. { file: "assets/app-xyz789.js" })
110
- if (entry.file) {
183
+ result = this.prefix + entry;
184
+ } else if (entry.file) {
185
+ // Vite / esbuild metafile format (e.g. { file: "assets/app-xyz789.js" })
111
186
  const file = entry.file.startsWith('/') ? entry.file : '/' + entry.file;
112
- return this.prefix ? (this.prefix + file).replace(/\/+/g, '/') : file;
187
+ result = this.prefix ? (this.prefix + file).replace(/\/+/g, '/') : file;
188
+ }
189
+ if (result !== undefined) {
190
+ if (this._resolveCache.size < 2000) {
191
+ this._resolveCache.set(assetPath, result);
192
+ }
193
+ return result;
113
194
  }
114
195
  }
115
196
  }
@@ -123,6 +204,9 @@ class AssetManager {
123
204
  finalPath += `${separator}v=${this.version}`;
124
205
  }
125
206
 
207
+ if (this._resolveCache.size < 2000) {
208
+ this._resolveCache.set(assetPath, finalPath);
209
+ }
126
210
  return finalPath;
127
211
  }
128
212
 
@@ -136,8 +220,11 @@ class AssetManager {
136
220
  /**
137
221
  * Generate CSS link tag
138
222
  */
139
- css(href, attributes = {}) {
223
+ css(href, attributes) {
140
224
  const resolvedHref = this.resolve(href);
225
+ if (!attributes || Object.keys(attributes).length === 0) {
226
+ return `<link rel="stylesheet" href="${this.escapeHtml(resolvedHref)}">`;
227
+ }
141
228
  const attrs = this.buildAttributes({
142
229
  rel: 'stylesheet',
143
230
  href: resolvedHref,
@@ -149,8 +236,11 @@ class AssetManager {
149
236
  /**
150
237
  * Generate JS script tag
151
238
  */
152
- js(src, attributes = {}) {
239
+ js(src, attributes) {
153
240
  const resolvedSrc = this.resolve(src);
241
+ if (!attributes || Object.keys(attributes).length === 0) {
242
+ return `<script src="${this.escapeHtml(resolvedSrc)}"></script>`;
243
+ }
154
244
  const attrs = this.buildAttributes({
155
245
  src: resolvedSrc,
156
246
  ...attributes
@@ -161,8 +251,11 @@ class AssetManager {
161
251
  /**
162
252
  * Generate image tag
163
253
  */
164
- img(src, alt = '', attributes = {}) {
254
+ img(src, alt = '', attributes) {
165
255
  const resolvedSrc = this.resolve(src);
256
+ if (!attributes || Object.keys(attributes).length === 0) {
257
+ return `<img src="${this.escapeHtml(resolvedSrc)}" alt="${this.escapeHtml(alt)}">`;
258
+ }
166
259
  const attrs = this.buildAttributes({
167
260
  src: resolvedSrc,
168
261
  alt,
@@ -325,6 +418,9 @@ class AssetManager {
325
418
  */
326
419
  hasPageJs(name) {
327
420
  if (!name || typeof name !== 'string') return false;
421
+ const cached = this._hasJsCache.get(name);
422
+ if (cached !== undefined) return cached;
423
+
328
424
  let clean = String(name).trim().replace(/^\/+/, '');
329
425
  if (clean.startsWith('pages/')) clean = clean.slice(6);
330
426
  if (clean.startsWith('dist/pages/')) clean = clean.slice(11);
@@ -343,6 +439,7 @@ class AssetManager {
343
439
  this.manifest['src/client/' + pagePath] ||
344
440
  this.manifest['client/' + pagePath]
345
441
  ) {
442
+ if (this._hasJsCache.size < 500) this._hasJsCache.set(name, true);
346
443
  return true;
347
444
  }
348
445
  }
@@ -369,13 +466,13 @@ class AssetManager {
369
466
  ];
370
467
 
371
468
  for (const p of diskChecks) {
372
- try {
373
- if (fs.existsSync(p)) {
374
- return true;
375
- }
376
- } catch (_) {}
469
+ if (cachedExistsSync(p)) {
470
+ if (this._hasJsCache.size < 500) this._hasJsCache.set(name, true);
471
+ return true;
472
+ }
377
473
  }
378
474
 
475
+ if (this._hasJsCache.size < 500) this._hasJsCache.set(name, false);
379
476
  return false;
380
477
  }
381
478
 
@@ -386,6 +483,9 @@ class AssetManager {
386
483
  */
387
484
  hasPageCss(name) {
388
485
  if (!name || typeof name !== 'string') return false;
486
+ const cached = this._hasCssCache.get(name);
487
+ if (cached !== undefined) return cached;
488
+
389
489
  let clean = String(name).trim().replace(/^\/+/, '');
390
490
  if (clean.startsWith('pages/')) clean = clean.slice(6);
391
491
  if (clean.startsWith('dist/pages/')) clean = clean.slice(11);
@@ -404,6 +504,7 @@ class AssetManager {
404
504
  this.manifest['src/client/' + pagePath] ||
405
505
  this.manifest['client/' + pagePath]
406
506
  ) {
507
+ if (this._hasCssCache.size < 500) this._hasCssCache.set(name, true);
407
508
  return true;
408
509
  }
409
510
  }
@@ -426,13 +527,13 @@ class AssetManager {
426
527
  ];
427
528
 
428
529
  for (const p of diskChecks) {
429
- try {
430
- if (fs.existsSync(p)) {
431
- return true;
432
- }
433
- } catch (_) {}
530
+ if (cachedExistsSync(p)) {
531
+ if (this._hasCssCache.size < 500) this._hasCssCache.set(name, true);
532
+ return true;
533
+ }
434
534
  }
435
535
 
536
+ if (this._hasCssCache.size < 500) this._hasCssCache.set(name, false);
436
537
  return false;
437
538
  }
438
539
 
@@ -507,16 +608,28 @@ class AssetManager {
507
608
  * Build HTML attributes string
508
609
  */
509
610
  buildAttributes(attrs) {
510
- return Object.entries(attrs)
511
- .filter(([k, v]) => v !== undefined && v !== null && v !== false && /^[a-zA-Z0-9_\-:]+$/.test(k))
512
- .map(([k, v]) => v === true ? k : `${k}="${this.escapeHtml(String(v))}"`)
513
- .join(' ');
611
+ if (!attrs || typeof attrs !== 'object') return '';
612
+ let result = '';
613
+ const keys = Object.keys(attrs);
614
+ for (let i = 0; i < keys.length; i++) {
615
+ const k = keys[i];
616
+ const v = attrs[k];
617
+ if (v === undefined || v === null || v === false || !/^[a-zA-Z0-9_\-:]+$/.test(k)) {
618
+ continue;
619
+ }
620
+ if (result) result += ' ';
621
+ result += v === true ? k : `${k}="${this.escapeHtml(String(v))}"`;
622
+ }
623
+ return result;
514
624
  }
515
625
 
516
626
  /**
517
627
  * Escape HTML special characters
518
628
  */
519
629
  escapeHtml(str) {
630
+ if (!str || typeof str !== 'string' || !/[&"'<>]/.test(str)) {
631
+ return str;
632
+ }
520
633
  return str
521
634
  .replace(/&/g, '&amp;')
522
635
  .replace(/"/g, '&quot;')
@@ -4,7 +4,14 @@
4
4
  */
5
5
 
6
6
  const fs = require('fs');
7
- const { parse } = require('yaml');
7
+
8
+ let _yamlParse = null;
9
+ function getYamlParse() {
10
+ if (!_yamlParse) {
11
+ _yamlParse = require('yaml').parse;
12
+ }
13
+ return _yamlParse;
14
+ }
8
15
 
9
16
  const FRONTMATTER_BLOCK = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/;
10
17
 
@@ -43,6 +50,7 @@ function parseNjkFrontmatter(content) {
43
50
  return { body, fm: {}, hasDelimiter: true };
44
51
  }
45
52
  try {
53
+ const parse = getYamlParse();
46
54
  const fm = parse(yamlText);
47
55
  return {
48
56
  body,
@@ -171,6 +179,13 @@ function clearNjkFrontmatterCaches() {
171
179
  }
172
180
  }
173
181
 
182
+ function seedProdRouteCache(entries) {
183
+ if (!entries || typeof entries !== 'object') return;
184
+ for (const [absPath, val] of Object.entries(entries)) {
185
+ prodRouteCache.set(absPath, val);
186
+ }
187
+ }
188
+
174
189
  module.exports = {
175
190
  parseNjkFrontmatter,
176
191
  frontmatterToPatches,
@@ -178,4 +193,5 @@ module.exports = {
178
193
  clearNjkFrontmatterCaches,
179
194
  registerClearHook,
180
195
  extractFrontmatterBlock,
196
+ seedProdRouteCache,
181
197
  };
@@ -7,20 +7,29 @@
7
7
  * Simple semver comparison utilities
8
8
  * (Lightweight alternative to full semver package)
9
9
  */
10
+ const semverCache = new Map();
11
+
10
12
  const semver = {
11
13
  /**
12
14
  * Parse version string to components
13
15
  */
14
16
  parse(version) {
15
17
  if (!version) return null;
18
+ const cached = semverCache.get(version);
19
+ if (cached !== undefined) return cached;
16
20
  const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
17
- if (!match) return null;
18
- return {
21
+ if (!match) {
22
+ if (semverCache.size < 200) semverCache.set(version, null);
23
+ return null;
24
+ }
25
+ const parsed = {
19
26
  major: parseInt(match[1], 10),
20
27
  minor: parseInt(match[2], 10),
21
28
  patch: parseInt(match[3], 10),
22
29
  prerelease: match[4] || null
23
30
  };
31
+ if (semverCache.size < 200) semverCache.set(version, parsed);
32
+ return parsed;
24
33
  },
25
34
 
26
35
  /**
@@ -93,21 +102,29 @@ const semver = {
93
102
  }
94
103
  };
95
104
 
105
+ const patternRegexCache = new Map();
106
+
96
107
  /**
97
108
  * Simple glob/minimatch pattern matching
98
109
  */
99
110
  function matchPattern(path, pattern) {
100
111
  if (pattern === '*' || pattern === '**') return true;
101
112
 
102
- // Convert glob to regex
103
- const regexPattern = pattern
104
- .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
105
- .replace(/\*\*/g, '{{GLOBSTAR}}') // Temp placeholder for **
106
- .replace(/\*/g, '[^/]*') // * matches anything except /
107
- .replace(/\{\{GLOBSTAR\}\}/g, '.*') // ** matches everything
108
- .replace(/\?/g, '.'); // ? matches single char
109
-
110
- const regex = new RegExp(`^${regexPattern}$`);
113
+ let regex = patternRegexCache.get(pattern);
114
+ if (!regex) {
115
+ // Convert glob to regex
116
+ const regexPattern = pattern
117
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
118
+ .replace(/\*\*/g, '{{GLOBSTAR}}') // Temp placeholder for **
119
+ .replace(/\*/g, '[^/]*') // * matches anything except /
120
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*') // ** matches everything
121
+ .replace(/\?/g, '.'); // ? matches single char
122
+
123
+ regex = new RegExp(`^${regexPattern}$`);
124
+ if (patternRegexCache.size < 500) {
125
+ patternRegexCache.set(pattern, regex);
126
+ }
127
+ }
111
128
  return regex.test(path);
112
129
  }
113
130
 
@@ -206,6 +223,8 @@ class PluginManager {
206
223
  * Resolve dependency order using topological sort
207
224
  */
208
225
  _resolveDependencyOrder(plugins) {
226
+ if (!plugins || plugins.length === 0) return [];
227
+
209
228
  const graph = new Map();
210
229
  const pluginMap = new Map();
211
230
 
@@ -228,6 +247,7 @@ class PluginManager {
228
247
 
229
248
  // Topological sort (Kahn's algorithm)
230
249
  const sorted = [];
250
+ const sortedNames = new Set();
231
251
  const noIncoming = [];
232
252
 
233
253
  // Find nodes with no dependencies
@@ -243,12 +263,13 @@ class PluginManager {
243
263
  while (noIncoming.length > 0) {
244
264
  const name = noIncoming.shift();
245
265
  sorted.push(pluginMap.get(name));
266
+ sortedNames.add(name);
246
267
 
247
268
  // Remove this node from all dependencies
248
269
  for (const [n, deps] of graph) {
249
270
  if (deps.has(name)) {
250
271
  deps.delete(name);
251
- if (deps.size === 0 && !sorted.find(p => p.name === n)) {
272
+ if (deps.size === 0 && !sortedNames.has(n)) {
252
273
  noIncoming.push(n);
253
274
  }
254
275
  }
@@ -257,7 +278,7 @@ class PluginManager {
257
278
 
258
279
  // Check for cycles - warn and exclude circular plugins
259
280
  if (sorted.length !== pluginMap.size) {
260
- const remaining = Array.from(pluginMap.keys()).filter(name => !sorted.find(p => p.name === name));
281
+ const remaining = Array.from(pluginMap.keys()).filter(name => !sortedNames.has(name));
261
282
  console.warn(
262
283
  `[plugin-manager] Circular dependency detected. Skipping plugins: ${remaining.join(', ')}`
263
284
  );
@@ -669,6 +690,18 @@ class PluginManager {
669
690
  * Get a plugin's API by name
670
691
  */
671
692
  getPluginAPI(name) {
693
+ const plugin = this.plugins.get(name);
694
+ if (plugin && plugin.isLazy && typeof plugin.hydrate === 'function' && !plugin.isHydrated) {
695
+ const real = plugin.hydrate();
696
+ if (real && real.api) {
697
+ const boundAPI = {};
698
+ for (const [key, value] of Object.entries(real.api)) {
699
+ boundAPI[key] = typeof value === 'function' ? value.bind(real) : value;
700
+ }
701
+ this.pluginAPIs.set(name, boundAPI);
702
+ }
703
+ return this.pluginAPIs.get(name) || (real && real.api) || plugin.api;
704
+ }
672
705
  return this.pluginAPIs.get(name);
673
706
  }
674
707
 
@@ -6,19 +6,24 @@
6
6
  * @module src/routing/route-table
7
7
  */
8
8
 
9
+ const routeSortMetaCache = new Map();
10
+
9
11
  /**
10
12
  * Computes route sorting priority metadata
11
13
  * @param {string} routePath
12
14
  * @returns {{ tier: number, literalSegCount: number, paramSegCount: number, depth: number, routePath: string }}
13
15
  */
14
16
  function getRouteSortMeta(routePath) {
17
+ const cached = routeSortMetaCache.get(routePath);
18
+ if (cached !== undefined) return cached;
19
+
15
20
  let hasStar = false;
16
21
  let hasColon = false;
17
22
  let depth = 0;
18
23
  let literalSegCount = 0;
19
24
  let paramSegCount = 0;
20
25
 
21
- const clean = routePath.split('?')[0];
26
+ const clean = routePath.includes('?') ? routePath.split('?')[0] : routePath;
22
27
  const segments = clean.split('/').filter(Boolean);
23
28
 
24
29
  for (const seg of segments) {
@@ -40,13 +45,18 @@ function getRouteSortMeta(routePath) {
40
45
  if (hasStar) tier = 2;
41
46
  else if (hasColon) tier = 1;
42
47
 
43
- return {
48
+ const meta = {
44
49
  tier,
45
50
  literalSegCount,
46
51
  paramSegCount,
47
52
  depth,
48
53
  routePath,
49
54
  };
55
+
56
+ if (routeSortMetaCache.size < 1000) {
57
+ routeSortMetaCache.set(routePath, meta);
58
+ }
59
+ return meta;
50
60
  }
51
61
 
52
62
  /**
@@ -56,14 +66,14 @@ function getRouteSortMeta(routePath) {
56
66
  * @returns {number}
57
67
  */
58
68
  function compareRouteOrder(a, b) {
59
- const ma = a._sortMeta || getRouteSortMeta(a.path);
60
- const mb = b._sortMeta || getRouteSortMeta(b.path);
69
+ const ma = a._sortMeta || (a._sortMeta = getRouteSortMeta(a.path));
70
+ const mb = b._sortMeta || (b._sortMeta = getRouteSortMeta(b.path));
61
71
 
62
72
  if (ma.tier !== mb.tier) return ma.tier - mb.tier;
63
73
  if (ma.literalSegCount !== mb.literalSegCount) return mb.literalSegCount - ma.literalSegCount;
64
74
  if (ma.depth !== mb.depth) return mb.depth - ma.depth;
65
75
  if (ma.paramSegCount !== mb.paramSegCount) return ma.paramSegCount - mb.paramSegCount;
66
- return ma.routePath.localeCompare(mb.routePath);
76
+ return ma.routePath < mb.routePath ? -1 : (ma.routePath > mb.routePath ? 1 : 0);
67
77
  }
68
78
 
69
79
  /**