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.
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +11 -0
- package/.agents/CONVENTIONS.md +4 -0
- package/.agents/ORM.md +13 -4
- package/.agents/PLUGINS.md +32 -6
- package/.agents/ROUTING.md +99 -2
- package/.agents/skills/webspresso-framework/SKILL.md +32 -0
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +4 -5
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
- package/AGENTS.md +6 -0
- package/README.md +20 -7
- package/bin/commands/add-tailwind.js +10 -0
- package/bin/commands/build.js +34 -0
- package/bin/commands/routes.js +8 -0
- package/core/applySchema.js +18 -1
- package/core/auth/jwt.js +10 -17
- package/core/auth/user-agent.js +61 -42
- package/core/compression/compression-stream.js +2 -2
- package/core/compression/middleware.js +1 -1
- package/core/compression/negotiate.js +11 -4
- package/core/events/dispatcher.js +56 -33
- package/core/openapi/orm-components.js +2 -3
- package/core/orm/cache/listeners.js +12 -2
- package/core/orm/json-fields.js +7 -1
- package/core/orm/model.js +99 -8
- package/core/orm/query-builder.js +71 -25
- package/core/orm/repository.js +42 -17
- package/core/orm/schema-helpers.js +49 -0
- package/core/orm/scopes.js +40 -9
- package/core/orm/types.js +12 -1
- package/core/queue/manager.js +5 -0
- package/core/validation/index.js +9 -3
- package/index.d.ts +178 -6
- package/index.js +71 -16
- package/package.json +6 -5
- package/plugins/admin-panel/index.js +12 -0
- package/plugins/audit-log/middleware.js +39 -4
- package/plugins/csrf/index.js +1 -1
- package/plugins/email/service.js +32 -1
- package/plugins/file-manager/api-handlers.js +10 -17
- package/plugins/file-manager/index.js +24 -9
- package/plugins/file-manager/security.js +18 -6
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +258 -0
- package/plugins/site-analytics/bot-patterns.js +3 -0
- package/plugins/site-analytics/client-error-handler.js +3 -1
- package/plugins/site-analytics/index.js +10 -1
- package/plugins/site-analytics/tracking.js +245 -46
- package/src/api/api-loader.js +18 -2
- package/src/api/validation-i18n.js +231 -0
- package/src/discovery/route-manifest.js +197 -0
- package/src/discovery/scan-routes.js +84 -8
- package/src/file-router.js +215 -97
- package/src/helpers.js +139 -26
- package/src/njk-frontmatter.js +17 -1
- package/src/plugin-manager.js +46 -13
- package/src/routing/route-table.js +15 -5
- package/src/server.js +86 -15
- package/src/services/builtins/system.js +4 -6
- package/src/services/registry.js +12 -2
- package/src/services/validator.js +57 -32
- 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 (
|
|
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
|
-
|
|
66
|
-
|
|
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.
|
|
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
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
if (
|
|
374
|
-
|
|
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
|
-
|
|
430
|
-
if (
|
|
431
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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, '&')
|
|
522
635
|
.replace(/"/g, '"')
|
package/src/njk-frontmatter.js
CHANGED
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
|
-
|
|
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
|
};
|
package/src/plugin-manager.js
CHANGED
|
@@ -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)
|
|
18
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 && !
|
|
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 => !
|
|
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
|
-
|
|
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.
|
|
76
|
+
return ma.routePath < mb.routePath ? -1 : (ma.routePath > mb.routePath ? 1 : 0);
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
/**
|