webspresso 0.0.102 → 0.0.104
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/ORM.md +13 -4
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +4 -5
- package/README.md +2 -2
- package/bin/commands/dev.js +3 -2
- package/bin/utils/orm-map-html.js +2 -8
- package/core/auth/jwt.js +12 -21
- package/core/auth/oauth.js +38 -14
- package/core/auth/totp.js +3 -15
- package/core/auth/user-agent.js +59 -60
- package/core/compression/compression-stream.js +2 -2
- package/core/compression/middleware.js +1 -1
- package/core/compression/negotiate.js +11 -4
- package/core/content/renderer.js +1 -13
- package/core/errors/middleware.js +2 -8
- package/core/events/dispatcher.js +56 -33
- package/core/forms/form-builder.js +1 -9
- package/core/openapi/orm-components.js +2 -3
- package/core/orm/cache/listeners.js +12 -2
- package/core/orm/index.js +5 -0
- 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/query-filters.js +393 -0
- 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/shutdown/shutdown-manager.js +16 -16
- package/core/utils/crypto.js +73 -0
- package/core/utils/escape-html.js +7 -0
- package/core/utils/fetch.js +120 -0
- package/core/utils/get-client-ip.js +6 -0
- package/core/utils/html.js +72 -0
- package/core/utils/ip.js +86 -0
- package/core/utils/safe-compare.js +3 -0
- package/core/validation/index.js +9 -3
- package/index.d.ts +33 -6
- package/index.js +10 -0
- package/package.json +2 -1
- package/plugins/admin-panel/auth.js +15 -7
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
- package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
- package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
- package/plugins/admin-panel/core/api-extensions.js +1 -75
- package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
- package/plugins/api-key/crypto.js +1 -24
- package/plugins/audit-log/api-handlers.js +11 -1
- package/plugins/audit-log/middleware.js +50 -6
- package/plugins/basic-auth/index.js +2 -26
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +3 -15
- package/plugins/email/service.js +32 -1
- package/plugins/file-manager/admin-component.js +3 -1
- package/plugins/file-manager/api-handlers.js +10 -17
- package/plugins/file-manager/core.js +34 -4
- package/plugins/file-manager/index.js +11 -9
- package/plugins/file-manager/security.js +18 -6
- package/plugins/mcp/auth.js +1 -13
- package/plugins/mcp/index.js +23 -1
- package/plugins/mcp/transports/sse.js +10 -1
- package/plugins/orm-cache-admin/admin-component.js +3 -2
- package/plugins/polar/src/api.js +9 -2
- package/plugins/polar/src/webhooks.js +3 -8
- package/plugins/recaptcha/helpers.js +1 -7
- package/plugins/recaptcha/verify.js +37 -23
- package/plugins/redirect/index.js +51 -3
- package/plugins/rest-resources/index.js +1 -207
- package/plugins/schema-explorer/admin-component.js +4 -2
- package/plugins/schema-explorer.js +40 -29
- package/plugins/seo-checker/index.js +5 -2
- 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 +265 -63
- package/plugins/sitemap.js +18 -15
- package/plugins/swagger.js +9 -14
- package/plugins/webhooks/crypto.js +1 -18
- package/plugins/webhooks/dispatcher.js +6 -1
- package/plugins/webhooks/matcher.js +25 -10
- package/plugins/webhooks/sender.js +32 -2
- package/src/api/api-loader.js +1 -1
- package/src/dev-reload/index.js +47 -6
- package/src/discovery/scan-routes.js +14 -4
- package/src/file-router.js +62 -6
- package/src/helpers.js +157 -32
- package/src/plugin-manager.js +34 -13
- package/src/routing/route-table.js +14 -4
- package/src/server.js +25 -9
- package/src/services/validator.js +57 -32
|
@@ -32,7 +32,8 @@ const { signPayload } = require('./crypto');
|
|
|
32
32
|
* timestamp: number
|
|
33
33
|
* }>}
|
|
34
34
|
*/
|
|
35
|
-
async function sendWebhook(options) {
|
|
35
|
+
async function sendWebhook(options = {}) {
|
|
36
|
+
const opts = options || {};
|
|
36
37
|
const {
|
|
37
38
|
url,
|
|
38
39
|
event,
|
|
@@ -43,7 +44,7 @@ async function sendWebhook(options) {
|
|
|
43
44
|
timeoutMs = 10000,
|
|
44
45
|
wrapPayload = true,
|
|
45
46
|
maxResponseBodyLength = 2048,
|
|
46
|
-
} =
|
|
47
|
+
} = opts;
|
|
47
48
|
|
|
48
49
|
if (!url || typeof url !== 'string') {
|
|
49
50
|
return {
|
|
@@ -57,6 +58,32 @@ async function sendWebhook(options) {
|
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
let parsedUrl;
|
|
62
|
+
try {
|
|
63
|
+
parsedUrl = new URL(url);
|
|
64
|
+
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
statusCode: 0,
|
|
68
|
+
responseBody: '',
|
|
69
|
+
durationMs: 0,
|
|
70
|
+
error: `Invalid webhook URL protocol: "${parsedUrl.protocol}". Only http: and https: are supported.`,
|
|
71
|
+
deliveryId,
|
|
72
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
statusCode: 0,
|
|
79
|
+
responseBody: '',
|
|
80
|
+
durationMs: 0,
|
|
81
|
+
error: 'Invalid webhook URL: destination URL is malformed.',
|
|
82
|
+
deliveryId,
|
|
83
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
60
87
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
61
88
|
|
|
62
89
|
// Construct request body
|
|
@@ -116,6 +143,9 @@ async function sendWebhook(options) {
|
|
|
116
143
|
timeoutId = setTimeout(() => {
|
|
117
144
|
controller.abort();
|
|
118
145
|
}, timeoutMs);
|
|
146
|
+
if (timeoutId && timeoutId.unref) {
|
|
147
|
+
timeoutId.unref();
|
|
148
|
+
}
|
|
119
149
|
|
|
120
150
|
const response = await fetch(url, {
|
|
121
151
|
method: 'POST',
|
package/src/api/api-loader.js
CHANGED
|
@@ -95,7 +95,7 @@ function createApiHandler(descriptor, context) {
|
|
|
95
95
|
// 1. Reload module in development
|
|
96
96
|
if (isDev) {
|
|
97
97
|
try {
|
|
98
|
-
const resolvedPath = require.resolve(descriptor.file);
|
|
98
|
+
const resolvedPath = descriptor._resolvedPath || (descriptor._resolvedPath = require.resolve(descriptor.file));
|
|
99
99
|
if (require.cache[resolvedPath]) {
|
|
100
100
|
delete require.cache[resolvedPath];
|
|
101
101
|
cachedMiddlewares = null;
|
package/src/dev-reload/index.js
CHANGED
|
@@ -15,6 +15,8 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
|
|
|
15
15
|
|
|
16
16
|
var es = null;
|
|
17
17
|
var reconnectTimer = null;
|
|
18
|
+
var wasConnected = false;
|
|
19
|
+
var reconnectDelay = 200;
|
|
18
20
|
|
|
19
21
|
function reloadCss(targetFile) {
|
|
20
22
|
var links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
@@ -166,6 +168,36 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
|
|
|
166
168
|
document.addEventListener('keydown', onKeyDown);
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
function safeReload(reason) {
|
|
172
|
+
clearOverlay();
|
|
173
|
+
var reasonMsg = reason ? ' (' + reason + ')' : '';
|
|
174
|
+
console.log('[Webspresso] 🔄 Reloading page' + reasonMsg + '...');
|
|
175
|
+
|
|
176
|
+
// Ping server with lightweight HEAD request to make sure it is ready before full reload.
|
|
177
|
+
// This prevents browser "This site can't be reached" (ERR_CONNECTION_REFUSED) screens
|
|
178
|
+
// when the dev server is in the middle of restarting.
|
|
179
|
+
function pingAndReload() {
|
|
180
|
+
if (typeof window.fetch !== 'function') {
|
|
181
|
+
window.location.reload();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fetch(window.location.href, { method: 'HEAD', cache: 'no-store' })
|
|
186
|
+
.then(function(res) {
|
|
187
|
+
if (res.ok || res.status < 500) {
|
|
188
|
+
window.location.reload();
|
|
189
|
+
} else {
|
|
190
|
+
setTimeout(pingAndReload, 150);
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
.catch(function() {
|
|
194
|
+
setTimeout(pingAndReload, 150);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
setTimeout(pingAndReload, 30);
|
|
199
|
+
}
|
|
200
|
+
|
|
169
201
|
function connect() {
|
|
170
202
|
if (es) {
|
|
171
203
|
try { es.close(); } catch (_) {}
|
|
@@ -173,6 +205,16 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
|
|
|
173
205
|
|
|
174
206
|
es = new window.EventSource('${SSE_ENDPOINT_PATH}');
|
|
175
207
|
|
|
208
|
+
es.addEventListener('open', function() {
|
|
209
|
+
reconnectDelay = 200;
|
|
210
|
+
if (wasConnected) {
|
|
211
|
+
console.log('[Webspresso] 🔄 Dev server reconnected, reloading page...');
|
|
212
|
+
safeReload('server reconnected');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
wasConnected = true;
|
|
216
|
+
});
|
|
217
|
+
|
|
176
218
|
es.addEventListener('reload', function(e) {
|
|
177
219
|
try {
|
|
178
220
|
var data = JSON.parse(e.data);
|
|
@@ -180,10 +222,7 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
|
|
|
180
222
|
clearOverlay();
|
|
181
223
|
reloadCss(data.file);
|
|
182
224
|
} else if (data.type === 'full') {
|
|
183
|
-
|
|
184
|
-
var reasonMsg = data.reason ? ' (' + data.reason + ')' : '';
|
|
185
|
-
console.log('[Webspresso] 🔄 Reloading page' + reasonMsg + '...');
|
|
186
|
-
window.location.reload();
|
|
225
|
+
safeReload(data.reason);
|
|
187
226
|
} else if (data.type === 'error') {
|
|
188
227
|
showOverlay(data);
|
|
189
228
|
} else if (data.type === 'clear-error') {
|
|
@@ -195,12 +234,14 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
|
|
|
195
234
|
});
|
|
196
235
|
|
|
197
236
|
es.onerror = function() {
|
|
198
|
-
es.close();
|
|
237
|
+
try { es.close(); } catch (_) {}
|
|
199
238
|
if (!reconnectTimer) {
|
|
239
|
+
var delay = reconnectDelay;
|
|
240
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, 1500);
|
|
200
241
|
reconnectTimer = setTimeout(function() {
|
|
201
242
|
reconnectTimer = null;
|
|
202
243
|
connect();
|
|
203
|
-
},
|
|
244
|
+
}, delay);
|
|
204
245
|
}
|
|
205
246
|
};
|
|
206
247
|
}
|
|
@@ -94,6 +94,19 @@ function scanDirSafely(dirPath, baseDir = dirPath, checkWarnings = false) {
|
|
|
94
94
|
return results;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
let isWebspressoPackageCache = null;
|
|
98
|
+
function checkIsWebspressoPackage() {
|
|
99
|
+
if (isWebspressoPackageCache === null) {
|
|
100
|
+
try {
|
|
101
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
|
|
102
|
+
isWebspressoPackageCache = Boolean(pkg && pkg.name === 'webspresso');
|
|
103
|
+
} catch (_) {
|
|
104
|
+
isWebspressoPackageCache = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return isWebspressoPackageCache;
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
/**
|
|
98
111
|
* Scans configured pages, api, and module directories
|
|
99
112
|
* @param {Object} options
|
|
@@ -106,10 +119,7 @@ function scanDirSafely(dirPath, baseDir = dirPath, checkWarnings = false) {
|
|
|
106
119
|
function scanRoutes(options = {}) {
|
|
107
120
|
const rootDir = options.rootDir || (() => {
|
|
108
121
|
if (process.env.NODE_ENV === 'test') {
|
|
109
|
-
|
|
110
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
|
|
111
|
-
if (pkg.name === 'webspresso') return process.cwd();
|
|
112
|
-
} catch (_) {}
|
|
122
|
+
if (checkIsWebspressoPackage()) return process.cwd();
|
|
113
123
|
return path.resolve(__dirname, '../..');
|
|
114
124
|
}
|
|
115
125
|
return process.cwd();
|
package/src/file-router.js
CHANGED
|
@@ -190,6 +190,10 @@ const translatorCache = new Map();
|
|
|
190
190
|
|
|
191
191
|
function clearTranslatorCache() {
|
|
192
192
|
translatorCache.clear();
|
|
193
|
+
intlNumberFormatCache.clear();
|
|
194
|
+
intlDateTimeFormatCache.clear();
|
|
195
|
+
intlRelativeTimeFormatCache.clear();
|
|
196
|
+
filePathToRouteCache.clear();
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
// Cache for route configs in production
|
|
@@ -215,6 +219,48 @@ const methodFromFilenameCache = new Map();
|
|
|
215
219
|
// Cache for translation param replacement regexes
|
|
216
220
|
const paramRegexCache = new Map();
|
|
217
221
|
|
|
222
|
+
// Caches for file-to-route resolution and Intl formatters
|
|
223
|
+
const filePathToRouteCache = new Map();
|
|
224
|
+
const intlNumberFormatCache = new Map();
|
|
225
|
+
const intlDateTimeFormatCache = new Map();
|
|
226
|
+
const intlRelativeTimeFormatCache = new Map();
|
|
227
|
+
|
|
228
|
+
function getCachedNumberFormat(locale, opts) {
|
|
229
|
+
const key = opts ? `${locale}::${JSON.stringify(opts)}` : locale;
|
|
230
|
+
let fmt = intlNumberFormatCache.get(key);
|
|
231
|
+
if (!fmt) {
|
|
232
|
+
fmt = new Intl.NumberFormat(locale, opts);
|
|
233
|
+
if (intlNumberFormatCache.size < 500) {
|
|
234
|
+
intlNumberFormatCache.set(key, fmt);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return fmt;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getCachedDateTimeFormat(locale, opts) {
|
|
241
|
+
const key = opts ? `${locale}::${JSON.stringify(opts)}` : locale;
|
|
242
|
+
let fmt = intlDateTimeFormatCache.get(key);
|
|
243
|
+
if (!fmt) {
|
|
244
|
+
fmt = new Intl.DateTimeFormat(locale, opts);
|
|
245
|
+
if (intlDateTimeFormatCache.size < 500) {
|
|
246
|
+
intlDateTimeFormatCache.set(key, fmt);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return fmt;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getCachedRelativeTimeFormat(locale, opts) {
|
|
253
|
+
const key = opts ? `${locale}::${JSON.stringify(opts)}` : locale;
|
|
254
|
+
let fmt = intlRelativeTimeFormatCache.get(key);
|
|
255
|
+
if (!fmt) {
|
|
256
|
+
fmt = new Intl.RelativeTimeFormat(locale, opts);
|
|
257
|
+
if (intlRelativeTimeFormatCache.size < 500) {
|
|
258
|
+
intlRelativeTimeFormatCache.set(key, fmt);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return fmt;
|
|
262
|
+
}
|
|
263
|
+
|
|
218
264
|
const MAX_LOCALE_LEN = 16;
|
|
219
265
|
|
|
220
266
|
let cachedSupportedLocalesRaw = null;
|
|
@@ -404,6 +450,12 @@ function isApiRouteFile(file) {
|
|
|
404
450
|
* @returns {string} Express route pattern
|
|
405
451
|
*/
|
|
406
452
|
function filePathToRoute(filePath, ext) {
|
|
453
|
+
const cacheKey = filePath + '::' + ext;
|
|
454
|
+
const cached = filePathToRouteCache.get(cacheKey);
|
|
455
|
+
if (cached !== undefined) {
|
|
456
|
+
return cached;
|
|
457
|
+
}
|
|
458
|
+
|
|
407
459
|
// Remove extension
|
|
408
460
|
let route = filePath.replace(ext, '');
|
|
409
461
|
|
|
@@ -429,6 +481,10 @@ function filePathToRoute(filePath, ext) {
|
|
|
429
481
|
route = '/' + route;
|
|
430
482
|
}
|
|
431
483
|
|
|
484
|
+
if (filePathToRouteCache.size < 5000) {
|
|
485
|
+
filePathToRouteCache.set(cacheKey, route);
|
|
486
|
+
}
|
|
487
|
+
|
|
432
488
|
return route;
|
|
433
489
|
}
|
|
434
490
|
|
|
@@ -886,8 +942,8 @@ function createTranslator(translations = {}, options = EMPTY_OBJECT) {
|
|
|
886
942
|
return value;
|
|
887
943
|
}
|
|
888
944
|
|
|
889
|
-
// Replace params like {{name}} in the translation
|
|
890
|
-
if (interpolationParams && interpolationParams !== EMPTY_OBJECT && Object.keys(interpolationParams).length > 0) {
|
|
945
|
+
// Replace params like {{name}} in the translation (skip if no placeholders in template)
|
|
946
|
+
if (value.includes('{{') && interpolationParams && interpolationParams !== EMPTY_OBJECT && Object.keys(interpolationParams).length > 0) {
|
|
891
947
|
for (const [paramKey, paramValue] of Object.entries(interpolationParams)) {
|
|
892
948
|
let regex = paramRegexCache.get(paramKey);
|
|
893
949
|
if (regex === undefined) {
|
|
@@ -911,7 +967,7 @@ function createTranslator(translations = {}, options = EMPTY_OBJECT) {
|
|
|
911
967
|
t.number = function formatNumber(num, numOpts) {
|
|
912
968
|
if (num === null || num === undefined || Number.isNaN(Number(num))) return '';
|
|
913
969
|
try {
|
|
914
|
-
return
|
|
970
|
+
return getCachedNumberFormat(locale, numOpts).format(Number(num));
|
|
915
971
|
} catch {
|
|
916
972
|
return String(num);
|
|
917
973
|
}
|
|
@@ -921,7 +977,7 @@ function createTranslator(translations = {}, options = EMPTY_OBJECT) {
|
|
|
921
977
|
t.currency = function formatCurrency(amount, currency = 'USD', currOpts = {}) {
|
|
922
978
|
if (amount === null || amount === undefined || Number.isNaN(Number(amount))) return '';
|
|
923
979
|
try {
|
|
924
|
-
return
|
|
980
|
+
return getCachedNumberFormat(locale, { style: 'currency', currency, ...currOpts }).format(Number(amount));
|
|
925
981
|
} catch {
|
|
926
982
|
return `${amount} ${currency}`;
|
|
927
983
|
}
|
|
@@ -933,7 +989,7 @@ function createTranslator(translations = {}, options = EMPTY_OBJECT) {
|
|
|
933
989
|
try {
|
|
934
990
|
const d = date instanceof Date ? date : new Date(date);
|
|
935
991
|
if (Number.isNaN(d.getTime())) return '';
|
|
936
|
-
return
|
|
992
|
+
return getCachedDateTimeFormat(locale, dateOpts).format(d);
|
|
937
993
|
} catch {
|
|
938
994
|
return String(date);
|
|
939
995
|
}
|
|
@@ -943,7 +999,7 @@ function createTranslator(translations = {}, options = EMPTY_OBJECT) {
|
|
|
943
999
|
t.relativeTime = function formatRelativeTime(val, unit = 'day', relOpts = { numeric: 'auto' }) {
|
|
944
1000
|
if (val === null || val === undefined || Number.isNaN(Number(val))) return '';
|
|
945
1001
|
try {
|
|
946
|
-
return
|
|
1002
|
+
return getCachedRelativeTimeFormat(locale, relOpts).format(Number(val), unit);
|
|
947
1003
|
} catch {
|
|
948
1004
|
return `${val} ${unit}`;
|
|
949
1005
|
}
|
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,22 +608,26 @@ 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) {
|
|
520
|
-
return str
|
|
521
|
-
.replace(/&/g, '&')
|
|
522
|
-
.replace(/"/g, '"')
|
|
523
|
-
.replace(/'/g, ''')
|
|
524
|
-
.replace(/</g, '<')
|
|
525
|
-
.replace(/>/g, '>');
|
|
630
|
+
return require('../core/utils/html').escapeHtml(str);
|
|
526
631
|
}
|
|
527
632
|
}
|
|
528
633
|
|
|
@@ -2091,6 +2196,26 @@ const utils = {
|
|
|
2091
2196
|
|
|
2092
2197
|
pageAssets(nameOrAttrs, attrs) {
|
|
2093
2198
|
return getAssetManager().pageAssets(nameOrAttrs, attrs);
|
|
2199
|
+
},
|
|
2200
|
+
|
|
2201
|
+
fetchWithTimeout(url, options) {
|
|
2202
|
+
return require('../core/utils/fetch').fetchWithTimeout(url, options);
|
|
2203
|
+
},
|
|
2204
|
+
|
|
2205
|
+
safeCompare(a, b) {
|
|
2206
|
+
return require('../core/utils/crypto').safeCompare(a, b);
|
|
2207
|
+
},
|
|
2208
|
+
|
|
2209
|
+
escapeHtml(value) {
|
|
2210
|
+
return require('../core/utils/html').escapeHtml(value);
|
|
2211
|
+
},
|
|
2212
|
+
|
|
2213
|
+
unescapeHtml(value) {
|
|
2214
|
+
return require('../core/utils/html').unescapeHtml(value);
|
|
2215
|
+
},
|
|
2216
|
+
|
|
2217
|
+
getClientIp(req, fallback) {
|
|
2218
|
+
return require('../core/utils/ip').getClientIp(req, fallback);
|
|
2094
2219
|
}
|
|
2095
2220
|
};
|
|
2096
2221
|
|