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.
Files changed (184) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -3
  3. package/bin/commands/add-esbuild.js +7 -3
  4. package/bin/commands/add-tailwind.js +9 -5
  5. package/bin/commands/build.js +20 -4
  6. package/bin/commands/dev.js +57 -7
  7. package/bin/commands/doctor.js +42 -1
  8. package/bin/commands/favicon-generate.js +10 -3
  9. package/bin/commands/new.js +8 -3
  10. package/bin/commands/start.js +3 -1
  11. package/bin/utils/add-command.js +16 -0
  12. package/core/app.js +45 -3
  13. package/core/auth/authenticate.js +3 -3
  14. package/core/auth/hash.js +167 -43
  15. package/core/auth/index.js +13 -1
  16. package/core/auth/jwt.js +21 -2
  17. package/core/auth/manager.js +63 -14
  18. package/core/auth/middleware.js +4 -2
  19. package/core/auth/oauth.js +5 -1
  20. package/core/auth/tokens.js +7 -2
  21. package/core/auth/totp.js +42 -6
  22. package/core/auth/user-agent.js +2 -0
  23. package/core/client/adapter/datastar.js +1401 -0
  24. package/core/client/api.js +1337 -0
  25. package/core/client/controller.js +1706 -0
  26. package/core/client/debug.js +367 -0
  27. package/core/client/devtool.js +1134 -0
  28. package/core/client/forms.js +1164 -0
  29. package/core/client/fragments.js +894 -0
  30. package/core/client/index.d.ts +59 -0
  31. package/core/client/index.js +391 -0
  32. package/core/client/lite.js +175 -0
  33. package/core/client/server-fragments.js +868 -0
  34. package/core/client/storage.js +339 -0
  35. package/core/client/utils.js +281 -0
  36. package/core/compression/middleware.js +277 -26
  37. package/core/context/app-context.js +25 -3
  38. package/core/context/request.js +32 -4
  39. package/core/errors/middleware.js +14 -0
  40. package/core/events/dispatcher.js +33 -0
  41. package/core/forms/flash.js +136 -102
  42. package/core/health/middleware.js +36 -6
  43. package/core/http/pipeline.js +45 -6
  44. package/core/index.js +25 -7
  45. package/core/kernel/view.js +41 -10
  46. package/core/metrics/middleware.js +30 -6
  47. package/core/metrics/registry.js +13 -5
  48. package/core/orm/cache/fingerprint.js +32 -11
  49. package/core/orm/cache/layer.js +19 -2
  50. package/core/orm/hidden.js +145 -0
  51. package/core/orm/model.js +48 -2
  52. package/core/orm/query-builder.js +343 -6
  53. package/core/orm/query-filters.js +14 -2
  54. package/core/orm/repository.js +64 -13
  55. package/core/orm/schema-helpers.js +9 -6
  56. package/core/orm/transaction.js +50 -6
  57. package/core/orm/types.js +4 -0
  58. package/core/queue/adapters/database.js +184 -2
  59. package/core/queue/adapters/memory.js +153 -5
  60. package/core/queue/adapters/redis.js +59 -4
  61. package/core/queue/job.js +14 -1
  62. package/core/queue/manager.js +115 -0
  63. package/core/resilience/circuit-breaker.js +27 -8
  64. package/core/services/builtins/exchange.js +174 -7
  65. package/core/services/builtins/file-manager.js +36 -0
  66. package/core/services/builtins/mail.js +27 -0
  67. package/core/services/builtins/queue.js +144 -0
  68. package/core/services/builtins/security.js +118 -0
  69. package/core/services/builtins/webhook.js +110 -0
  70. package/core/services/executor.js +38 -16
  71. package/core/services/index.js +4 -1
  72. package/core/sse/stream.js +28 -5
  73. package/core/utils/timeout.js +8 -1
  74. package/core/validation/compiler.js +10 -7
  75. package/index.d.ts +1320 -74
  76. package/index.js +128 -8
  77. package/package.json +75 -27
  78. package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
  79. package/plugins/admin-panel/index.js +19 -8
  80. package/plugins/csrf/index.js +3 -3
  81. package/plugins/data-exchange/export.js +214 -0
  82. package/plugins/data-exchange/import.js +48 -5
  83. package/plugins/data-exchange/index.js +70 -28
  84. package/plugins/data-exchange/preview.js +266 -0
  85. package/plugins/data-exchange/template.js +242 -0
  86. package/plugins/email/admin-component.js +783 -141
  87. package/plugins/email/api-handlers.js +51 -1
  88. package/plugins/email/index.js +5 -0
  89. package/plugins/email/service.js +48 -0
  90. package/plugins/file-manager/admin-component.js +1546 -70
  91. package/plugins/file-manager/api-handlers.js +166 -1
  92. package/plugins/file-manager/core.js +598 -8
  93. package/plugins/file-manager/index.js +8 -0
  94. package/plugins/index.js +7 -0
  95. package/plugins/mcp/auth.js +3 -3
  96. package/plugins/oauth/index.js +36 -3
  97. package/plugins/queue/admin-component.js +1092 -0
  98. package/plugins/queue/api-handlers.js +248 -0
  99. package/plugins/queue/index.js +69 -0
  100. package/plugins/rest-resources/index.js +7 -2
  101. package/plugins/security/admin-component.js +689 -0
  102. package/plugins/security/api-handlers.js +169 -0
  103. package/plugins/security/audit.js +153 -0
  104. package/plugins/security/bruteforce.js +244 -0
  105. package/plugins/security/firewall.js +459 -0
  106. package/plugins/security/headers.js +214 -0
  107. package/plugins/security/honeypot.js +234 -0
  108. package/plugins/security/incident-recorder.js +137 -0
  109. package/plugins/security/index.js +211 -0
  110. package/plugins/seo-checker/analyzer.js +20 -7
  111. package/plugins/upload/index.js +36 -0
  112. package/plugins/webhooks/admin-component.js +702 -175
  113. package/plugins/webhooks/api-handlers.js +75 -1
  114. package/plugins/webhooks/dispatcher.js +290 -11
  115. package/plugins/webhooks/index.js +37 -2
  116. package/plugins/webhooks/sender.js +56 -0
  117. package/plugins/xlsx/engine.js +19 -4
  118. package/src/api/api-loader.js +19 -1
  119. package/src/bundle/index.js +443 -17
  120. package/src/client-runtime/bootstrap-swup.js +9 -0
  121. package/src/client-runtime/mount.js +15 -20
  122. package/src/client-runtime/resolve.js +3 -2
  123. package/src/dev/api-typegen.js +1300 -0
  124. package/src/dev/banner.js +10 -1
  125. package/src/dev/colors.js +1 -1
  126. package/src/dev/index.js +7 -1
  127. package/src/dev/typegen.js +173 -17
  128. package/src/dev-reload/index.js +1 -1
  129. package/src/discovery/file-route-parser.js +17 -3
  130. package/src/discovery/scan-routes.js +4 -1
  131. package/src/file-router.js +253 -69
  132. package/src/helpers.js +46 -26
  133. package/src/njk-frontmatter.js +30 -4
  134. package/src/routing/cascading.js +26 -8
  135. package/src/routing/mount-discovered-routes.js +76 -0
  136. package/src/server.js +231 -70
  137. package/views/partials/webspresso-client-runtime.njk +1 -9
  138. package/.agents/ADMIN.md +0 -426
  139. package/.agents/AGENTS.md +0 -96
  140. package/.agents/ARCHITECTURE.md +0 -163
  141. package/.agents/AUTH.md +0 -581
  142. package/.agents/CACHE.md +0 -148
  143. package/.agents/CLI.md +0 -649
  144. package/.agents/COMPRESSION.md +0 -59
  145. package/.agents/CONTEXT.md +0 -152
  146. package/.agents/CONVENTIONS.md +0 -50
  147. package/.agents/EARLY_HINTS.md +0 -120
  148. package/.agents/EMAIL.md +0 -223
  149. package/.agents/ERRORS.md +0 -260
  150. package/.agents/EVENTS.md +0 -179
  151. package/.agents/FLAGS.md +0 -118
  152. package/.agents/HEALTH.md +0 -118
  153. package/.agents/LIMITS.md +0 -109
  154. package/.agents/LOGGING.md +0 -93
  155. package/.agents/MCP.md +0 -103
  156. package/.agents/METRICS.md +0 -127
  157. package/.agents/MIGRATION_JOBS.md +0 -95
  158. package/.agents/ORM.md +0 -700
  159. package/.agents/PLUGINS.md +0 -729
  160. package/.agents/QUEUE.md +0 -290
  161. package/.agents/RATE_LIMIT.md +0 -147
  162. package/.agents/REALTIME.md +0 -213
  163. package/.agents/RESILIENCE.md +0 -235
  164. package/.agents/ROUTING.md +0 -605
  165. package/.agents/SCALE_OUT.md +0 -109
  166. package/.agents/SECRETS.md +0 -117
  167. package/.agents/SERVICES.md +0 -341
  168. package/.agents/SHUTDOWN.md +0 -74
  169. package/.agents/STREAMING.md +0 -108
  170. package/.agents/WORKFLOW.md +0 -27
  171. package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
  172. package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
  173. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
  174. package/.agents/skills/webspresso-framework/SKILL.md +0 -89
  175. package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
  176. package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
  177. package/.agents/skills/webspresso-services/SKILL.md +0 -134
  178. package/.cursorrules +0 -14
  179. package/AGENTS.md +0 -203
  180. package/CLAUDE.md +0 -54
  181. package/core/kernel/run-demo.js +0 -58
  182. package/plugins/admin-panel/styles.js +0 -9
  183. package/plugins/data-exchange/export-xlsx.js +0 -119
  184. 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.get(filePath);
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
- if (diskExistenceCache.size < 2000) {
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.get(fullPath);
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.set(fullPath, {
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.get(assetPath);
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
- if (this._resolveCache.size < 2000) {
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
- if (this._resolveCache.size < 2000) {
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.get(name);
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
- if (this._hasJsCache.size < 500) this._hasJsCache.set(name, true);
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
- if (this._hasJsCache.size < 500) this._hasJsCache.set(name, true);
493
+ lruSet(this._hasJsCache, name, true, MAX_ASSET_CACHE);
472
494
  return true;
473
495
  }
474
496
  }
475
497
 
476
- if (this._hasJsCache.size < 500) this._hasJsCache.set(name, false);
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.get(name);
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
- if (this._hasCssCache.size < 500) this._hasCssCache.set(name, true);
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
- if (this._hasCssCache.size < 500) this._hasCssCache.set(name, true);
554
+ lruSet(this._hasCssCache, name, true, MAX_ASSET_CACHE);
533
555
  return true;
534
556
  }
535
557
  }
536
558
 
537
- if (this._hasCssCache.size < 500) this._hasCssCache.set(name, false);
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.get(pattern);
1079
+ let regex = lruGet(isPathRegexCache, pattern);
1058
1080
  if (regex === undefined) {
1059
1081
  regex = globToRegExp(pattern, { separator: null });
1060
- if (isPathRegexCache.size < 500) {
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
  }
@@ -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.set(absPath, loaded);
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.get(absPath);
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.set(absPath, empty);
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.set(absPath, { mtimeMs: stat.mtimeMs, cached: fresh });
188
+ lruSet(devRouteCache, absPath, { mtimeMs: stat.mtimeMs, cached: fresh }, MAX_ROUTE_CACHE);
163
189
  return fresh;
164
190
  }
165
191
 
@@ -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' && /\{%\s*extends\s+/.test(templateBody)) {
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 && fs.existsSync(fullPath)) {
66
- try {
67
- const content = fs.readFileSync(fullPath, 'utf8');
68
- return /\{%\s*extends\s+/.test(content);
69
- } catch (_) {}
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 false;
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);