webspresso 0.0.108 → 0.0.110
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/LICENSE +21 -0
- package/README.md +3 -3
- package/bin/commands/add-esbuild.js +7 -3
- package/bin/commands/add-tailwind.js +9 -5
- package/bin/commands/build.js +20 -4
- package/bin/commands/dev.js +57 -7
- package/bin/commands/doctor.js +42 -1
- package/bin/commands/favicon-generate.js +10 -3
- package/bin/commands/new.js +8 -3
- package/bin/commands/start.js +3 -1
- package/bin/utils/add-command.js +16 -0
- package/core/app.js +45 -3
- package/core/auth/authenticate.js +3 -3
- package/core/auth/hash.js +167 -43
- package/core/auth/index.js +13 -1
- package/core/auth/jwt.js +21 -2
- package/core/auth/manager.js +63 -14
- package/core/auth/middleware.js +4 -2
- package/core/auth/oauth.js +5 -1
- package/core/auth/tokens.js +7 -2
- package/core/auth/totp.js +42 -6
- package/core/auth/user-agent.js +2 -0
- package/core/client/adapter/datastar.js +1401 -0
- package/core/client/api.js +1337 -0
- package/core/client/controller.js +1706 -0
- package/core/client/debug.js +367 -0
- package/core/client/devtool.js +1134 -0
- package/core/client/forms.js +1164 -0
- package/core/client/fragments.js +894 -0
- package/core/client/index.d.ts +59 -0
- package/core/client/index.js +391 -0
- package/core/client/lite.js +175 -0
- package/core/client/server-fragments.js +868 -0
- package/core/client/storage.js +339 -0
- package/core/client/utils.js +281 -0
- package/core/compression/middleware.js +277 -26
- package/core/context/app-context.js +25 -3
- package/core/context/request.js +32 -4
- package/core/errors/middleware.js +14 -0
- package/core/events/dispatcher.js +33 -0
- package/core/forms/flash.js +136 -102
- package/core/health/middleware.js +36 -6
- package/core/http/pipeline.js +45 -6
- package/core/index.js +25 -7
- package/core/kernel/view.js +41 -10
- package/core/metrics/middleware.js +30 -6
- package/core/metrics/registry.js +13 -5
- package/core/orm/cache/fingerprint.js +32 -11
- package/core/orm/cache/layer.js +19 -2
- package/core/orm/hidden.js +145 -0
- package/core/orm/model.js +48 -2
- package/core/orm/query-builder.js +343 -6
- package/core/orm/query-filters.js +14 -2
- package/core/orm/repository.js +64 -13
- package/core/orm/schema-helpers.js +9 -6
- package/core/orm/transaction.js +50 -6
- package/core/orm/types.js +4 -0
- package/core/queue/adapters/database.js +184 -2
- package/core/queue/adapters/memory.js +153 -5
- package/core/queue/adapters/redis.js +59 -4
- package/core/queue/job.js +14 -1
- package/core/queue/manager.js +115 -0
- package/core/resilience/circuit-breaker.js +27 -8
- package/core/services/builtins/exchange.js +174 -7
- package/core/services/builtins/file-manager.js +36 -0
- package/core/services/builtins/mail.js +27 -0
- package/core/services/builtins/queue.js +144 -0
- package/core/services/builtins/security.js +118 -0
- package/core/services/builtins/webhook.js +110 -0
- package/core/services/executor.js +38 -16
- package/core/services/index.js +4 -1
- package/core/sse/stream.js +28 -5
- package/core/utils/timeout.js +8 -1
- package/core/validation/compiler.js +10 -7
- package/index.d.ts +1320 -74
- package/index.js +128 -8
- package/package.json +75 -27
- package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
- package/plugins/admin-panel/index.js +19 -8
- package/plugins/csrf/index.js +3 -3
- package/plugins/data-exchange/export.js +214 -0
- package/plugins/data-exchange/import.js +48 -5
- package/plugins/data-exchange/index.js +70 -28
- package/plugins/data-exchange/preview.js +266 -0
- package/plugins/data-exchange/template.js +242 -0
- package/plugins/email/admin-component.js +783 -141
- package/plugins/email/api-handlers.js +51 -1
- package/plugins/email/index.js +5 -0
- package/plugins/email/service.js +48 -0
- package/plugins/file-manager/admin-component.js +1546 -70
- package/plugins/file-manager/api-handlers.js +166 -1
- package/plugins/file-manager/core.js +598 -8
- package/plugins/file-manager/index.js +8 -0
- package/plugins/index.js +7 -0
- package/plugins/mcp/auth.js +3 -3
- package/plugins/oauth/index.js +36 -3
- package/plugins/queue/admin-component.js +1092 -0
- package/plugins/queue/api-handlers.js +248 -0
- package/plugins/queue/index.js +69 -0
- package/plugins/rest-resources/index.js +7 -2
- package/plugins/security/admin-component.js +689 -0
- package/plugins/security/api-handlers.js +169 -0
- package/plugins/security/audit.js +153 -0
- package/plugins/security/bruteforce.js +244 -0
- package/plugins/security/firewall.js +459 -0
- package/plugins/security/headers.js +214 -0
- package/plugins/security/honeypot.js +234 -0
- package/plugins/security/incident-recorder.js +137 -0
- package/plugins/security/index.js +211 -0
- package/plugins/seo-checker/analyzer.js +20 -7
- package/plugins/upload/index.js +36 -0
- package/plugins/webhooks/admin-component.js +702 -175
- package/plugins/webhooks/api-handlers.js +75 -1
- package/plugins/webhooks/dispatcher.js +290 -11
- package/plugins/webhooks/index.js +37 -2
- package/plugins/webhooks/sender.js +56 -0
- package/plugins/xlsx/engine.js +19 -4
- package/src/api/api-loader.js +19 -1
- package/src/bundle/index.js +443 -17
- package/src/client-runtime/bootstrap-swup.js +9 -0
- package/src/client-runtime/mount.js +15 -20
- package/src/client-runtime/resolve.js +3 -2
- package/src/dev/api-typegen.js +1300 -0
- package/src/dev/banner.js +10 -1
- package/src/dev/colors.js +1 -1
- package/src/dev/index.js +7 -1
- package/src/dev/typegen.js +173 -17
- package/src/dev-reload/index.js +1 -1
- package/src/discovery/file-route-parser.js +17 -3
- package/src/discovery/scan-routes.js +4 -1
- package/src/file-router.js +253 -69
- package/src/helpers.js +46 -26
- package/src/njk-frontmatter.js +30 -4
- package/src/routing/cascading.js +26 -8
- package/src/routing/mount-discovered-routes.js +76 -0
- package/src/server.js +231 -70
- package/views/partials/webspresso-client-runtime.njk +1 -9
- package/.agents/ADMIN.md +0 -426
- package/.agents/AGENTS.md +0 -96
- package/.agents/ARCHITECTURE.md +0 -163
- package/.agents/AUTH.md +0 -581
- package/.agents/CACHE.md +0 -148
- package/.agents/CLI.md +0 -649
- package/.agents/COMPRESSION.md +0 -59
- package/.agents/CONTEXT.md +0 -152
- package/.agents/CONVENTIONS.md +0 -50
- package/.agents/EARLY_HINTS.md +0 -120
- package/.agents/EMAIL.md +0 -223
- package/.agents/ERRORS.md +0 -260
- package/.agents/EVENTS.md +0 -179
- package/.agents/FLAGS.md +0 -118
- package/.agents/HEALTH.md +0 -118
- package/.agents/LIMITS.md +0 -109
- package/.agents/LOGGING.md +0 -93
- package/.agents/MCP.md +0 -103
- package/.agents/METRICS.md +0 -127
- package/.agents/MIGRATION_JOBS.md +0 -95
- package/.agents/ORM.md +0 -700
- package/.agents/PLUGINS.md +0 -729
- package/.agents/QUEUE.md +0 -290
- package/.agents/RATE_LIMIT.md +0 -147
- package/.agents/REALTIME.md +0 -213
- package/.agents/RESILIENCE.md +0 -235
- package/.agents/ROUTING.md +0 -605
- package/.agents/SCALE_OUT.md +0 -109
- package/.agents/SECRETS.md +0 -117
- package/.agents/SERVICES.md +0 -341
- package/.agents/SHUTDOWN.md +0 -74
- package/.agents/STREAMING.md +0 -108
- package/.agents/WORKFLOW.md +0 -27
- package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
- package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
- package/.agents/skills/webspresso-framework/SKILL.md +0 -89
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
- package/.agents/skills/webspresso-services/SKILL.md +0 -134
- package/.cursorrules +0 -14
- package/AGENTS.md +0 -203
- package/CLAUDE.md +0 -54
- package/core/kernel/run-demo.js +0 -58
- package/plugins/admin-panel/styles.js +0 -9
- package/plugins/data-exchange/export-xlsx.js +0 -119
- package/src/client-runtime/bootstrap-alpine-swup.js +0 -34
package/src/helpers.js
CHANGED
|
@@ -19,15 +19,43 @@ dayjs.extend(utc);
|
|
|
19
19
|
dayjs.extend(timezone);
|
|
20
20
|
dayjs.extend(customParseFormat);
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Zero-allocation bounded Map LRU helpers
|
|
24
|
+
*/
|
|
25
|
+
function lruGet(map, key) {
|
|
26
|
+
const val = map.get(key);
|
|
27
|
+
if (val !== undefined) {
|
|
28
|
+
map.delete(key);
|
|
29
|
+
map.set(key, val);
|
|
30
|
+
}
|
|
31
|
+
return val;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function lruSet(map, key, val, maxSize) {
|
|
35
|
+
if (map.has(key)) {
|
|
36
|
+
map.delete(key);
|
|
37
|
+
} else if (map.size >= maxSize) {
|
|
38
|
+
const oldestKey = map.keys().next().value;
|
|
39
|
+
map.delete(oldestKey);
|
|
40
|
+
}
|
|
41
|
+
map.set(key, val);
|
|
42
|
+
}
|
|
43
|
+
|
|
22
44
|
// Cache for isPath regex patterns
|
|
23
45
|
const isPathRegexCache = new Map();
|
|
46
|
+
const MAX_IS_PATH_CACHE = 1000;
|
|
24
47
|
|
|
25
48
|
// Caches for manifest files and filesystem existence checks
|
|
26
49
|
const manifestFileCache = new Map();
|
|
50
|
+
const MAX_MANIFEST_CACHE = 50;
|
|
51
|
+
|
|
27
52
|
const diskExistenceCache = new Map();
|
|
53
|
+
const MAX_DISK_CACHE = 2000;
|
|
54
|
+
|
|
55
|
+
const MAX_ASSET_CACHE = 2000;
|
|
28
56
|
|
|
29
57
|
function cachedExistsSync(filePath) {
|
|
30
|
-
const cached = diskExistenceCache
|
|
58
|
+
const cached = lruGet(diskExistenceCache, filePath);
|
|
31
59
|
if (cached !== undefined) return cached;
|
|
32
60
|
let exists = false;
|
|
33
61
|
try {
|
|
@@ -35,9 +63,7 @@ function cachedExistsSync(filePath) {
|
|
|
35
63
|
} catch (_) {
|
|
36
64
|
exists = false;
|
|
37
65
|
}
|
|
38
|
-
|
|
39
|
-
diskExistenceCache.set(filePath, exists);
|
|
40
|
-
}
|
|
66
|
+
lruSet(diskExistenceCache, filePath, exists, MAX_DISK_CACHE);
|
|
41
67
|
return exists;
|
|
42
68
|
}
|
|
43
69
|
|
|
@@ -109,7 +135,7 @@ class AssetManager {
|
|
|
109
135
|
if (fs.existsSync(fullPath)) {
|
|
110
136
|
try {
|
|
111
137
|
const stats = fs.statSync(fullPath);
|
|
112
|
-
const cached = manifestFileCache
|
|
138
|
+
const cached = lruGet(manifestFileCache, fullPath);
|
|
113
139
|
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
114
140
|
this.manifest = cached.manifest;
|
|
115
141
|
this._lookupIndex = cached.lookupIndex;
|
|
@@ -118,12 +144,12 @@ class AssetManager {
|
|
|
118
144
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
119
145
|
this.manifest = JSON.parse(content);
|
|
120
146
|
this._buildLookupIndex();
|
|
121
|
-
manifestFileCache
|
|
147
|
+
lruSet(manifestFileCache, fullPath, {
|
|
122
148
|
mtimeMs: stats.mtimeMs,
|
|
123
149
|
size: stats.size,
|
|
124
150
|
manifest: this.manifest,
|
|
125
151
|
lookupIndex: this._lookupIndex,
|
|
126
|
-
});
|
|
152
|
+
}, MAX_MANIFEST_CACHE);
|
|
127
153
|
return;
|
|
128
154
|
} catch (_) {
|
|
129
155
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
@@ -145,7 +171,7 @@ class AssetManager {
|
|
|
145
171
|
* @returns {string}
|
|
146
172
|
*/
|
|
147
173
|
resolve(assetPath) {
|
|
148
|
-
const cached = this._resolveCache
|
|
174
|
+
const cached = lruGet(this._resolveCache, assetPath);
|
|
149
175
|
if (cached !== undefined) return cached;
|
|
150
176
|
|
|
151
177
|
// Remove leading slash for manifest lookup
|
|
@@ -188,9 +214,7 @@ class AssetManager {
|
|
|
188
214
|
result = this.prefix ? (this.prefix + file).replace(/\/+/g, '/') : file;
|
|
189
215
|
}
|
|
190
216
|
if (result !== undefined) {
|
|
191
|
-
|
|
192
|
-
this._resolveCache.set(assetPath, result);
|
|
193
|
-
}
|
|
217
|
+
lruSet(this._resolveCache, assetPath, result, MAX_ASSET_CACHE);
|
|
194
218
|
return result;
|
|
195
219
|
}
|
|
196
220
|
}
|
|
@@ -205,9 +229,7 @@ class AssetManager {
|
|
|
205
229
|
finalPath += `${separator}v=${this.version}`;
|
|
206
230
|
}
|
|
207
231
|
|
|
208
|
-
|
|
209
|
-
this._resolveCache.set(assetPath, finalPath);
|
|
210
|
-
}
|
|
232
|
+
lruSet(this._resolveCache, assetPath, finalPath, MAX_ASSET_CACHE);
|
|
211
233
|
return finalPath;
|
|
212
234
|
}
|
|
213
235
|
|
|
@@ -419,7 +441,7 @@ class AssetManager {
|
|
|
419
441
|
*/
|
|
420
442
|
hasPageJs(name) {
|
|
421
443
|
if (!name || typeof name !== 'string') return false;
|
|
422
|
-
const cached = this._hasJsCache
|
|
444
|
+
const cached = lruGet(this._hasJsCache, name);
|
|
423
445
|
if (cached !== undefined) return cached;
|
|
424
446
|
|
|
425
447
|
let clean = String(name).trim().replace(/^\/+/, '');
|
|
@@ -440,7 +462,7 @@ class AssetManager {
|
|
|
440
462
|
this.manifest['src/client/' + pagePath] ||
|
|
441
463
|
this.manifest['client/' + pagePath]
|
|
442
464
|
) {
|
|
443
|
-
|
|
465
|
+
lruSet(this._hasJsCache, name, true, MAX_ASSET_CACHE);
|
|
444
466
|
return true;
|
|
445
467
|
}
|
|
446
468
|
}
|
|
@@ -468,12 +490,12 @@ class AssetManager {
|
|
|
468
490
|
|
|
469
491
|
for (const p of diskChecks) {
|
|
470
492
|
if (cachedExistsSync(p)) {
|
|
471
|
-
|
|
493
|
+
lruSet(this._hasJsCache, name, true, MAX_ASSET_CACHE);
|
|
472
494
|
return true;
|
|
473
495
|
}
|
|
474
496
|
}
|
|
475
497
|
|
|
476
|
-
|
|
498
|
+
lruSet(this._hasJsCache, name, false, MAX_ASSET_CACHE);
|
|
477
499
|
return false;
|
|
478
500
|
}
|
|
479
501
|
|
|
@@ -484,7 +506,7 @@ class AssetManager {
|
|
|
484
506
|
*/
|
|
485
507
|
hasPageCss(name) {
|
|
486
508
|
if (!name || typeof name !== 'string') return false;
|
|
487
|
-
const cached = this._hasCssCache
|
|
509
|
+
const cached = lruGet(this._hasCssCache, name);
|
|
488
510
|
if (cached !== undefined) return cached;
|
|
489
511
|
|
|
490
512
|
let clean = String(name).trim().replace(/^\/+/, '');
|
|
@@ -505,7 +527,7 @@ class AssetManager {
|
|
|
505
527
|
this.manifest['src/client/' + pagePath] ||
|
|
506
528
|
this.manifest['client/' + pagePath]
|
|
507
529
|
) {
|
|
508
|
-
|
|
530
|
+
lruSet(this._hasCssCache, name, true, MAX_ASSET_CACHE);
|
|
509
531
|
return true;
|
|
510
532
|
}
|
|
511
533
|
}
|
|
@@ -529,12 +551,12 @@ class AssetManager {
|
|
|
529
551
|
|
|
530
552
|
for (const p of diskChecks) {
|
|
531
553
|
if (cachedExistsSync(p)) {
|
|
532
|
-
|
|
554
|
+
lruSet(this._hasCssCache, name, true, MAX_ASSET_CACHE);
|
|
533
555
|
return true;
|
|
534
556
|
}
|
|
535
557
|
}
|
|
536
558
|
|
|
537
|
-
|
|
559
|
+
lruSet(this._hasCssCache, name, false, MAX_ASSET_CACHE);
|
|
538
560
|
return false;
|
|
539
561
|
}
|
|
540
562
|
|
|
@@ -1054,12 +1076,10 @@ function createHelpers(ctx) {
|
|
|
1054
1076
|
const currentPath = req.path;
|
|
1055
1077
|
if (pattern === currentPath) return true;
|
|
1056
1078
|
if (pattern.includes('*')) {
|
|
1057
|
-
let regex = isPathRegexCache
|
|
1079
|
+
let regex = lruGet(isPathRegexCache, pattern);
|
|
1058
1080
|
if (regex === undefined) {
|
|
1059
1081
|
regex = globToRegExp(pattern, { separator: null });
|
|
1060
|
-
|
|
1061
|
-
isPathRegexCache.set(pattern, regex);
|
|
1062
|
-
}
|
|
1082
|
+
lruSet(isPathRegexCache, pattern, regex, MAX_IS_PATH_CACHE);
|
|
1063
1083
|
}
|
|
1064
1084
|
return regex.test(currentPath);
|
|
1065
1085
|
}
|
package/src/njk-frontmatter.js
CHANGED
|
@@ -17,6 +17,30 @@ const FRONTMATTER_BLOCK = /^---\r?\n([\s\S]*?)\r?\n---\r?\n/;
|
|
|
17
17
|
|
|
18
18
|
/** @typedef {{ metaPatch: Record<string, unknown>, dataPatch: Record<string, unknown> }} Patch */
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Zero-allocation bounded Map LRU helpers
|
|
22
|
+
*/
|
|
23
|
+
function lruGet(map, key) {
|
|
24
|
+
const val = map.get(key);
|
|
25
|
+
if (val !== undefined) {
|
|
26
|
+
map.delete(key);
|
|
27
|
+
map.set(key, val);
|
|
28
|
+
}
|
|
29
|
+
return val;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function lruSet(map, key, val, maxSize) {
|
|
33
|
+
if (map.has(key)) {
|
|
34
|
+
map.delete(key);
|
|
35
|
+
} else if (map.size >= maxSize) {
|
|
36
|
+
const oldestKey = map.keys().next().value;
|
|
37
|
+
map.delete(oldestKey);
|
|
38
|
+
}
|
|
39
|
+
map.set(key, val);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const MAX_ROUTE_CACHE = 2000;
|
|
43
|
+
|
|
20
44
|
const prodRouteCache = new Map();
|
|
21
45
|
/** @type {Map<string, { mtimeMs: number, cached: LoadedNjk }>} */
|
|
22
46
|
const devRouteCache = new Map();
|
|
@@ -117,7 +141,7 @@ function readAndParse(absPath, isDevLike = false) {
|
|
|
117
141
|
if (isDevLike) {
|
|
118
142
|
return loaded;
|
|
119
143
|
}
|
|
120
|
-
prodRouteCache
|
|
144
|
+
lruSet(prodRouteCache, absPath, loaded, MAX_ROUTE_CACHE);
|
|
121
145
|
return loaded;
|
|
122
146
|
}
|
|
123
147
|
|
|
@@ -127,7 +151,7 @@ function readAndParse(absPath, isDevLike = false) {
|
|
|
127
151
|
*/
|
|
128
152
|
function loadNjkRouteTemplate(absPath, isDev) {
|
|
129
153
|
if (!isDev) {
|
|
130
|
-
const cached = prodRouteCache
|
|
154
|
+
const cached = lruGet(prodRouteCache, absPath);
|
|
131
155
|
if (cached) {
|
|
132
156
|
return cached;
|
|
133
157
|
}
|
|
@@ -138,7 +162,7 @@ function loadNjkRouteTemplate(absPath, isDev) {
|
|
|
138
162
|
metaPatch: {},
|
|
139
163
|
dataPatch: {},
|
|
140
164
|
};
|
|
141
|
-
prodRouteCache
|
|
165
|
+
lruSet(prodRouteCache, absPath, empty, MAX_ROUTE_CACHE);
|
|
142
166
|
return empty;
|
|
143
167
|
}
|
|
144
168
|
return readAndParse(absPath, false);
|
|
@@ -156,10 +180,12 @@ function loadNjkRouteTemplate(absPath, isDev) {
|
|
|
156
180
|
const stat = fs.statSync(absPath);
|
|
157
181
|
const prev = devRouteCache.get(absPath);
|
|
158
182
|
if (prev && prev.mtimeMs >= stat.mtimeMs) {
|
|
183
|
+
devRouteCache.delete(absPath);
|
|
184
|
+
devRouteCache.set(absPath, prev);
|
|
159
185
|
return prev.cached;
|
|
160
186
|
}
|
|
161
187
|
const fresh = readAndParse(absPath, true);
|
|
162
|
-
devRouteCache
|
|
188
|
+
lruSet(devRouteCache, absPath, { mtimeMs: stat.mtimeMs, cached: fresh }, MAX_ROUTE_CACHE);
|
|
163
189
|
return fresh;
|
|
164
190
|
}
|
|
165
191
|
|
package/src/routing/cascading.js
CHANGED
|
@@ -18,6 +18,9 @@ const middlewareFileCache = new Map();
|
|
|
18
18
|
const layoutFileCache = new Map();
|
|
19
19
|
const errorFileCache = new Map();
|
|
20
20
|
const loadingFileCache = new Map();
|
|
21
|
+
// fullPath -> whether the template file contains `{% extends %}`
|
|
22
|
+
const extendsFileCache = new Map();
|
|
23
|
+
const EXTENDS_RE = /\{%\s*extends\s+/;
|
|
21
24
|
|
|
22
25
|
function shouldBypassCache(isDev) {
|
|
23
26
|
return !!isDev;
|
|
@@ -54,21 +57,35 @@ function getDirectoryChain(relativeFilePath) {
|
|
|
54
57
|
/**
|
|
55
58
|
* Check if a template content explicitly declares its own layout inheritance via `{% extends ... %}`
|
|
56
59
|
*
|
|
60
|
+
* When the body is not provided, the file is read once and the answer is cached per path
|
|
61
|
+
* (outside development), so rendering never does synchronous disk I/O per request.
|
|
62
|
+
*
|
|
57
63
|
* @param {string} [templateBody]
|
|
58
64
|
* @param {string} [fullPath]
|
|
65
|
+
* @param {boolean} [isDev=false]
|
|
59
66
|
* @returns {boolean}
|
|
60
67
|
*/
|
|
61
|
-
function templateHasExtends(templateBody, fullPath) {
|
|
62
|
-
if (typeof templateBody === 'string' &&
|
|
68
|
+
function templateHasExtends(templateBody, fullPath, isDev = false) {
|
|
69
|
+
if (typeof templateBody === 'string' && EXTENDS_RE.test(templateBody)) {
|
|
63
70
|
return true;
|
|
64
71
|
}
|
|
65
|
-
if (fullPath
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
72
|
+
if (!fullPath) return false;
|
|
73
|
+
|
|
74
|
+
if (!shouldBypassCache(isDev) && extendsFileCache.has(fullPath)) {
|
|
75
|
+
return extendsFileCache.get(fullPath);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let result = false;
|
|
79
|
+
try {
|
|
80
|
+
result = EXTENDS_RE.test(fs.readFileSync(fullPath, 'utf8'));
|
|
81
|
+
} catch (_) {
|
|
82
|
+
result = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!shouldBypassCache(isDev)) {
|
|
86
|
+
extendsFileCache.set(fullPath, result);
|
|
70
87
|
}
|
|
71
|
-
return
|
|
88
|
+
return result;
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
/**
|
|
@@ -476,6 +493,7 @@ function clearCascadingCaches() {
|
|
|
476
493
|
layoutFileCache.clear();
|
|
477
494
|
errorFileCache.clear();
|
|
478
495
|
loadingFileCache.clear();
|
|
496
|
+
extendsFileCache.clear();
|
|
479
497
|
}
|
|
480
498
|
|
|
481
499
|
module.exports = {
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
const { createPageHandler } = require('../pages/page-loader');
|
|
10
10
|
const { createApiHandler } = require('../api/api-loader');
|
|
11
11
|
const { getRouteSortMeta } = require('./route-table');
|
|
12
|
+
const { createFragmentRefreshHandler } = require('../../core/client/server-fragments');
|
|
13
|
+
const { createHelpers } = require('../helpers');
|
|
14
|
+
const { resolveMiddlewares } = require('../file-router');
|
|
12
15
|
|
|
13
16
|
const NOOP = () => {};
|
|
14
17
|
|
|
@@ -55,6 +58,79 @@ function mountDiscoveredRoutes(app, routeTable, context) {
|
|
|
55
58
|
log(` ${descriptor.method.toUpperCase().padEnd(6)} ${descriptor.path} -> ${descriptor.source || descriptor.file}`);
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
// Record discovered page entries for `/__webspresso/fragments` refresh handler
|
|
62
|
+
const discoveredPageEntries = routeTable.routes
|
|
63
|
+
.filter((r) => r.type === 'page')
|
|
64
|
+
.map((r) => ({
|
|
65
|
+
routePath: r.path,
|
|
66
|
+
file: r.file,
|
|
67
|
+
getPageMod: () => {
|
|
68
|
+
if (!r.file || !r.file.endsWith('.js')) return null;
|
|
69
|
+
try {
|
|
70
|
+
if (context.isDev && require.cache[require.resolve(r.file)]) {
|
|
71
|
+
delete require.cache[require.resolve(r.file)];
|
|
72
|
+
}
|
|
73
|
+
const mod = require(r.file);
|
|
74
|
+
return typeof mod === 'function' ? { load: mod } : mod.default || mod;
|
|
75
|
+
} catch (_e) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
cascadingMiddlewares: [],
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
app.__discoveredPageEntries = discoveredPageEntries;
|
|
83
|
+
|
|
84
|
+
if (!app.__fragmentRefreshMounted && context.nunjucks) {
|
|
85
|
+
app.__fragmentRefreshMounted = true;
|
|
86
|
+
const fragmentRefreshHandler = createFragmentRefreshHandler({
|
|
87
|
+
getPageEntries: () => [
|
|
88
|
+
...(app.__ssrPageEntries || []),
|
|
89
|
+
...(app.__discoveredPageEntries || []),
|
|
90
|
+
],
|
|
91
|
+
nunjucksEnv: context.nunjucks,
|
|
92
|
+
buildFsy: (req) => createHelpers({ req, res: req.res }),
|
|
93
|
+
buildContext: (req, res, { fsy }) => {
|
|
94
|
+
const ctx = {
|
|
95
|
+
req,
|
|
96
|
+
res,
|
|
97
|
+
db: context.db || null,
|
|
98
|
+
fsy,
|
|
99
|
+
params: req.params || {},
|
|
100
|
+
query: req.query || {},
|
|
101
|
+
user: req.user,
|
|
102
|
+
auth: req.auth,
|
|
103
|
+
session: req.session,
|
|
104
|
+
service: (name, input, opts) => {
|
|
105
|
+
if (context.serviceRegistry) {
|
|
106
|
+
return context.serviceRegistry.call(name, input, ctx, opts);
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`Services not configured. Cannot call service '${name}'.`);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
ctx.ctx = ctx;
|
|
112
|
+
req.context = ctx;
|
|
113
|
+
req.ctx = ctx;
|
|
114
|
+
req.req = req;
|
|
115
|
+
req.res = res;
|
|
116
|
+
if (context.db && !req.db) req.db = context.db;
|
|
117
|
+
if (!req.service) req.service = ctx.service;
|
|
118
|
+
return ctx;
|
|
119
|
+
},
|
|
120
|
+
resolveMiddlewareList: (mwList) => resolveMiddlewares(mwList, context.middlewares || {}),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const { rateLimit } = require('../../core/rate-limit');
|
|
124
|
+
const fragmentLimiter = rateLimit({
|
|
125
|
+
window: 60000,
|
|
126
|
+
limit: 300,
|
|
127
|
+
skip: () => process.env.NODE_ENV === 'test',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
app.get('/__webspresso/fragments', fragmentLimiter, fragmentRefreshHandler);
|
|
131
|
+
app.post('/__webspresso/fragments', fragmentLimiter, fragmentRefreshHandler);
|
|
132
|
+
}
|
|
133
|
+
|
|
58
134
|
// Phase 1: Mount static routes immediately
|
|
59
135
|
for (const r of staticRoutes) {
|
|
60
136
|
bindRoute(r);
|