webspresso 0.0.103 → 0.0.105

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 (165) hide show
  1. package/.agents/AGENTS.md +6 -0
  2. package/.agents/ARCHITECTURE.md +23 -0
  3. package/.agents/CACHE.md +147 -0
  4. package/.agents/CLI.md +1 -1
  5. package/.agents/EARLY_HINTS.md +120 -0
  6. package/.agents/HEALTH.md +118 -0
  7. package/.agents/LIMITS.md +109 -0
  8. package/.agents/ORM.md +155 -1
  9. package/.agents/PLUGINS.md +28 -0
  10. package/.agents/RESILIENCE.md +235 -0
  11. package/AGENTS.md +6 -0
  12. package/README.md +2 -2
  13. package/bin/commands/add-docker.js +6 -6
  14. package/bin/commands/dev.js +4 -3
  15. package/bin/commands/new.js +5 -5
  16. package/bin/commands/openapi.js +1 -1
  17. package/bin/commands/start.js +1 -1
  18. package/bin/utils/orm-map-html.js +2 -8
  19. package/bin/utils/project.js +2 -2
  20. package/core/auth/hash.js +5 -4
  21. package/core/auth/jwt.js +6 -57
  22. package/core/auth/magic-link.js +4 -5
  23. package/core/auth/manager.js +3 -5
  24. package/core/auth/oauth.js +45 -30
  25. package/core/auth/totp.js +5 -16
  26. package/core/auth/user-agent.js +1 -21
  27. package/core/cache/byte-size.js +123 -0
  28. package/core/cache/index.js +21 -0
  29. package/core/cache/lru.js +477 -0
  30. package/core/cache/tagged-lru.js +346 -0
  31. package/core/commands/registry.js +6 -20
  32. package/core/content/renderer.js +1 -13
  33. package/core/early-hints/index.js +267 -0
  34. package/core/errors/domain.js +48 -0
  35. package/core/errors/http.js +31 -0
  36. package/core/errors/index.js +10 -0
  37. package/core/errors/middleware.js +2 -8
  38. package/core/errors/normalize.js +88 -2
  39. package/core/events/discovery.js +6 -24
  40. package/core/events/dispatcher.js +2 -7
  41. package/core/forms/form-builder.js +1 -9
  42. package/core/health/index.js +41 -0
  43. package/core/health/middleware.js +213 -0
  44. package/core/health/normalize.js +140 -0
  45. package/core/health/probes.js +203 -0
  46. package/core/limits/index.js +34 -0
  47. package/core/limits/middleware.js +317 -0
  48. package/core/limits/normalize.js +222 -0
  49. package/core/orm/batch.js +70 -0
  50. package/core/orm/cache/fingerprint.js +2 -2
  51. package/core/orm/cache/memory-provider.js +27 -110
  52. package/core/orm/index.js +76 -8
  53. package/core/orm/json-fields.js +22 -8
  54. package/core/orm/model.js +10 -0
  55. package/core/orm/query-builder.js +63 -9
  56. package/core/orm/query-filters.js +393 -0
  57. package/core/orm/repository.js +318 -19
  58. package/core/orm/schema-helpers.js +8 -10
  59. package/core/orm/scopes.js +321 -1
  60. package/core/orm/transaction.js +10 -2
  61. package/core/orm/utils.js +4 -86
  62. package/core/queue/adapters/database.js +4 -8
  63. package/core/queue/manager.js +55 -41
  64. package/core/resilience/circuit-breaker.js +391 -0
  65. package/core/resilience/errors.js +63 -0
  66. package/core/resilience/index.js +53 -0
  67. package/core/resilience/registry.js +97 -0
  68. package/core/resilience/retry.js +203 -0
  69. package/core/schema-cache.js +9 -2
  70. package/core/shutdown/http-adapter.js +4 -1
  71. package/core/shutdown/shutdown-manager.js +132 -31
  72. package/core/utils/array.js +12 -0
  73. package/core/utils/base64url.js +8 -0
  74. package/core/utils/crypto.js +191 -0
  75. package/core/utils/ensure-leading-slash.js +6 -0
  76. package/core/utils/escape-html.js +7 -0
  77. package/core/utils/escape-regexp.js +7 -0
  78. package/core/utils/fetch.js +121 -0
  79. package/core/utils/fs.js +144 -0
  80. package/core/utils/get-client-ip.js +6 -0
  81. package/core/utils/hmac.js +6 -0
  82. package/core/utils/html.js +72 -0
  83. package/core/utils/ip.js +86 -0
  84. package/core/utils/json.js +98 -0
  85. package/core/utils/object.js +106 -0
  86. package/core/utils/random.js +9 -0
  87. package/core/utils/regex.js +77 -0
  88. package/core/utils/safe-compare.js +3 -0
  89. package/core/utils/sha256.js +6 -0
  90. package/core/utils/string.js +87 -0
  91. package/core/utils/strip-trailing-slash.js +6 -0
  92. package/core/utils/time.js +96 -0
  93. package/core/utils/timeout.js +348 -0
  94. package/core/utils/url.js +106 -0
  95. package/core/utils/walk.js +7 -0
  96. package/index.d.ts +627 -2
  97. package/index.js +158 -0
  98. package/package.json +1 -2
  99. package/plugins/admin-panel/auth.js +15 -7
  100. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
  101. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
  102. package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
  103. package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
  104. package/plugins/admin-panel/core/api-extensions.js +3 -80
  105. package/plugins/admin-panel/index.js +1 -1
  106. package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
  107. package/plugins/admin-panel/modules/menu.js +1 -1
  108. package/plugins/api-key/crypto.js +3 -26
  109. package/plugins/api-key/scopes.js +5 -7
  110. package/plugins/audit-log/api-handlers.js +8 -7
  111. package/plugins/audit-log/middleware.js +11 -2
  112. package/plugins/audit-log/parse.js +3 -3
  113. package/plugins/basic-auth/index.js +2 -26
  114. package/plugins/cors.js +2 -2
  115. package/plugins/csrf/index.js +7 -23
  116. package/plugins/dashboard/app.js +2 -2
  117. package/plugins/dashboard/index.js +5 -5
  118. package/plugins/data-exchange/record-selection.js +2 -5
  119. package/plugins/email/api-handlers.js +5 -18
  120. package/plugins/email/auth-bridge.js +3 -2
  121. package/plugins/file-manager/admin-component.js +3 -1
  122. package/plugins/file-manager/core.js +34 -4
  123. package/plugins/health-check.js +2 -1
  124. package/plugins/index.js +2 -0
  125. package/plugins/lazy.js +5 -6
  126. package/plugins/mcp/auth.js +1 -13
  127. package/plugins/mcp/discovery.js +18 -17
  128. package/plugins/mcp/index.js +23 -1
  129. package/plugins/mcp/transports/sse.js +10 -1
  130. package/plugins/orm-cache-admin/admin-component.js +3 -2
  131. package/plugins/polar/src/api.js +11 -3
  132. package/plugins/polar/src/webhooks.js +7 -13
  133. package/plugins/rate-limit/index.js +3 -1
  134. package/plugins/recaptcha/helpers.js +1 -7
  135. package/plugins/recaptcha/verify.js +37 -23
  136. package/plugins/redirect/index.js +57 -7
  137. package/plugins/rest-resources/index.js +1 -207
  138. package/plugins/rls/index.js +170 -0
  139. package/plugins/schema-explorer/admin-component.js +4 -2
  140. package/plugins/schema-explorer.js +40 -29
  141. package/plugins/seo-checker/index.js +5 -2
  142. package/plugins/site-analytics/tracking.js +21 -18
  143. package/plugins/sitemap.js +21 -17
  144. package/plugins/swagger.js +13 -17
  145. package/plugins/upload/index.js +4 -3
  146. package/plugins/upload/orphan-pruner.js +4 -3
  147. package/plugins/webhooks/crypto.js +6 -28
  148. package/plugins/webhooks/dispatcher.js +14 -9
  149. package/plugins/webhooks/index.js +6 -0
  150. package/plugins/webhooks/matcher.js +28 -17
  151. package/plugins/webhooks/sender.js +40 -13
  152. package/src/app-context.js +41 -5
  153. package/src/dev/banner.js +6 -2
  154. package/src/dev-reload/index.js +47 -6
  155. package/src/file-router.js +1 -3
  156. package/src/helpers.js +180 -33
  157. package/src/modules/module-discovery.js +1 -9
  158. package/src/pages/page-loader.js +53 -1
  159. package/src/plugin-manager.js +90 -17
  160. package/src/server.js +137 -26
  161. package/src/services/discovery.js +19 -40
  162. package/src/services/executor.js +86 -25
  163. package/src/services/memoize.js +3 -25
  164. package/src/services/registry.js +6 -0
  165. package/src/template/precompile.js +8 -19
package/src/helpers.js CHANGED
@@ -11,6 +11,7 @@ const relativeTime = require('dayjs/plugin/relativeTime');
11
11
  const utc = require('dayjs/plugin/utc');
12
12
  const timezone = require('dayjs/plugin/timezone');
13
13
  const customParseFormat = require('dayjs/plugin/customParseFormat');
14
+ const { globToRegExp } = require('../core/utils/regex');
14
15
 
15
16
  // Extend dayjs with plugins
16
17
  dayjs.extend(relativeTime);
@@ -627,15 +628,7 @@ class AssetManager {
627
628
  * Escape HTML special characters
628
629
  */
629
630
  escapeHtml(str) {
630
- if (!str || typeof str !== 'string' || !/[&"'<>]/.test(str)) {
631
- return str;
632
- }
633
- return str
634
- .replace(/&/g, '&amp;')
635
- .replace(/"/g, '&quot;')
636
- .replace(/'/g, '&#39;')
637
- .replace(/</g, '&lt;')
638
- .replace(/>/g, '&gt;');
631
+ return require('../core/utils/html').escapeHtml(str);
639
632
  }
640
633
  }
641
634
 
@@ -667,7 +660,15 @@ function getAssetManager() {
667
660
  * @returns {Object} fsy helper object
668
661
  */
669
662
  function createHelpers(ctx) {
670
- const { req, res, baseUrl = process.env.BASE_URL || 'http://localhost:3000', locale = 'en', t = null } = ctx || {};
663
+ const { req, res, baseUrl = process.env.BASE_URL || 'http://localhost:7331', locale = 'en', t = null } = ctx || {};
664
+ const resolveAssetManager = () => ctx?.assetManager || ctx?.app?.assetManager || req?.app?.assetManager || getAssetManager();
665
+ const assetManager = new Proxy({}, {
666
+ get(_, prop) {
667
+ const mgr = resolveAssetManager();
668
+ const val = mgr[prop];
669
+ return typeof val === 'function' ? val.bind(mgr) : val;
670
+ }
671
+ });
671
672
 
672
673
  return {
673
674
  /** Active request locale */
@@ -996,16 +997,6 @@ function createHelpers(ctx) {
996
997
  return `<script type="application/ld+json">${json}</script>`;
997
998
  },
998
999
 
999
- /**
1000
- * Get the path to a static asset
1001
- * @param {string} path - Asset path
1002
- * @returns {string}
1003
- */
1004
- asset(path) {
1005
- const assetPath = path.startsWith('/') ? path : `/${path}`;
1006
- return assetPath;
1007
- },
1008
-
1009
1000
  /**
1010
1001
  * Get the current locale
1011
1002
  * @returns {string}
@@ -1033,7 +1024,7 @@ function createHelpers(ctx) {
1033
1024
  if (pattern.includes('*')) {
1034
1025
  let regex = isPathRegexCache.get(pattern);
1035
1026
  if (regex === undefined) {
1036
- regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
1027
+ regex = globToRegExp(pattern, { separator: null });
1037
1028
  if (isPathRegexCache.size < 500) {
1038
1029
  isPathRegexCache.set(pattern, regex);
1039
1030
  }
@@ -1050,7 +1041,7 @@ function createHelpers(ctx) {
1050
1041
  * @returns {string}
1051
1042
  */
1052
1043
  asset(assetPath) {
1053
- return getAssetManager().asset(assetPath);
1044
+ return assetManager.asset(assetPath);
1054
1045
  },
1055
1046
 
1056
1047
  /**
@@ -1060,7 +1051,7 @@ function createHelpers(ctx) {
1060
1051
  * @returns {string}
1061
1052
  */
1062
1053
  css(href, attrs = {}) {
1063
- return getAssetManager().css(href, attrs);
1054
+ return assetManager.css(href, attrs);
1064
1055
  },
1065
1056
 
1066
1057
  /**
@@ -1070,7 +1061,7 @@ function createHelpers(ctx) {
1070
1061
  * @returns {string}
1071
1062
  */
1072
1063
  js(src, attrs = {}) {
1073
- return getAssetManager().js(src, attrs);
1064
+ return assetManager.js(src, attrs);
1074
1065
  },
1075
1066
 
1076
1067
  /**
@@ -1081,7 +1072,7 @@ function createHelpers(ctx) {
1081
1072
  * @returns {string}
1082
1073
  */
1083
1074
  img(src, alt = '', attrs = {}) {
1084
- return getAssetManager().img(src, alt, attrs);
1075
+ return assetManager.img(src, alt, attrs);
1085
1076
  },
1086
1077
 
1087
1078
  /**
@@ -1101,7 +1092,7 @@ function createHelpers(ctx) {
1101
1092
  * @returns {string}
1102
1093
  */
1103
1094
  pageJs(name, attrs = {}) {
1104
- return getAssetManager().pageJs(name, attrs);
1095
+ return assetManager.pageJs(name, attrs);
1105
1096
  },
1106
1097
 
1107
1098
  /**
@@ -1110,7 +1101,7 @@ function createHelpers(ctx) {
1110
1101
  * @returns {string}
1111
1102
  */
1112
1103
  pageJsUrl(name) {
1113
- return getAssetManager().pageJsUrl(name);
1104
+ return assetManager.pageJsUrl(name);
1114
1105
  },
1115
1106
 
1116
1107
  /**
@@ -1120,7 +1111,7 @@ function createHelpers(ctx) {
1120
1111
  * @returns {string}
1121
1112
  */
1122
1113
  pageCss(name, attrs = {}) {
1123
- return getAssetManager().pageCss(name, attrs);
1114
+ return assetManager.pageCss(name, attrs);
1124
1115
  },
1125
1116
 
1126
1117
  /**
@@ -1129,7 +1120,7 @@ function createHelpers(ctx) {
1129
1120
  * @returns {string}
1130
1121
  */
1131
1122
  pageCssUrl(name) {
1132
- return getAssetManager().pageCssUrl(name);
1123
+ return assetManager.pageCssUrl(name);
1133
1124
  },
1134
1125
 
1135
1126
  /**
@@ -1138,7 +1129,7 @@ function createHelpers(ctx) {
1138
1129
  * @returns {boolean}
1139
1130
  */
1140
1131
  hasPageJs(name) {
1141
- return getAssetManager().hasPageJs(name);
1132
+ return assetManager.hasPageJs(name);
1142
1133
  },
1143
1134
 
1144
1135
  /**
@@ -1147,7 +1138,7 @@ function createHelpers(ctx) {
1147
1138
  * @returns {boolean}
1148
1139
  */
1149
1140
  hasPageCss(name) {
1150
- return getAssetManager().hasPageCss(name);
1141
+ return assetManager.hasPageCss(name);
1151
1142
  },
1152
1143
 
1153
1144
  /**
@@ -1158,12 +1149,12 @@ function createHelpers(ctx) {
1158
1149
  */
1159
1150
  pageAssets(nameOrAttrs, attrs) {
1160
1151
  if (typeof nameOrAttrs === 'string' && nameOrAttrs.trim()) {
1161
- return getAssetManager().pageAssets(nameOrAttrs, attrs);
1152
+ return assetManager.pageAssets(nameOrAttrs, attrs);
1162
1153
  }
1163
1154
  const opts = (typeof nameOrAttrs === 'object' && nameOrAttrs !== null) ? nameOrAttrs : (attrs || {});
1164
1155
  const routeFile = req?.context?.file;
1165
1156
  const routePath = req?.context?.path || req?.path;
1166
- return getAssetManager().pageAssetsForRoute(routeFile, routePath, opts);
1157
+ return assetManager.pageAssetsForRoute(routeFile, routePath, opts);
1167
1158
  },
1168
1159
 
1169
1160
  /**
@@ -2204,6 +2195,162 @@ const utils = {
2204
2195
 
2205
2196
  pageAssets(nameOrAttrs, attrs) {
2206
2197
  return getAssetManager().pageAssets(nameOrAttrs, attrs);
2198
+ },
2199
+
2200
+ fetchWithTimeout(url, options) {
2201
+ return require('../core/utils/fetch').fetchWithTimeout(url, options);
2202
+ },
2203
+
2204
+ safeCompare(a, b) {
2205
+ return require('../core/utils/crypto').safeCompare(a, b);
2206
+ },
2207
+
2208
+ sha256(data, encoding) {
2209
+ return require('../core/utils/crypto').sha256(data, encoding);
2210
+ },
2211
+
2212
+ hmacSha256(data, secret, encoding) {
2213
+ return require('../core/utils/crypto').hmacSha256(data, secret, encoding);
2214
+ },
2215
+
2216
+ randomToken(byteLength, encoding) {
2217
+ return require('../core/utils/crypto').randomToken(byteLength, encoding);
2218
+ },
2219
+
2220
+ randomId(prefix, byteLength, encoding) {
2221
+ return require('../core/utils/crypto').randomId(prefix, byteLength, encoding);
2222
+ },
2223
+
2224
+ base64UrlEncode(input) {
2225
+ return require('../core/utils/crypto').base64UrlEncode(input);
2226
+ },
2227
+
2228
+ base64UrlDecode(input) {
2229
+ return require('../core/utils/crypto').base64UrlDecode(input);
2230
+ },
2231
+
2232
+ escapeHtml(value) {
2233
+ return require('../core/utils/html').escapeHtml(value);
2234
+ },
2235
+
2236
+ unescapeHtml(value) {
2237
+ return require('../core/utils/html').unescapeHtml(value);
2238
+ },
2239
+
2240
+ walkDirectory(dir, options) {
2241
+ return require('../core/utils/fs').walkDirectory(dir, options);
2242
+ },
2243
+
2244
+ walkDirectoryDetailed(dir, options) {
2245
+ return require('../core/utils/fs').walkDirectoryDetailed(dir, options);
2246
+ },
2247
+
2248
+ getClientIp(req, fallback) {
2249
+ return require('../core/utils/ip').getClientIp(req, fallback);
2250
+ },
2251
+
2252
+ stripTrailingSlash(input, options) {
2253
+ return require('../core/utils/url').stripTrailingSlash(input, options);
2254
+ },
2255
+
2256
+ ensureLeadingSlash(input) {
2257
+ return require('../core/utils/url').ensureLeadingSlash(input);
2258
+ },
2259
+
2260
+ ensureTrailingSlash(input) {
2261
+ return require('../core/utils/url').ensureTrailingSlash(input);
2262
+ },
2263
+
2264
+ stripLeadingSlash(input) {
2265
+ return require('../core/utils/url').stripLeadingSlash(input);
2266
+ },
2267
+
2268
+ joinUrl(base, ...paths) {
2269
+ return require('../core/utils/url').joinUrl(base, ...paths);
2270
+ },
2271
+
2272
+ escapeRegExp(str) {
2273
+ return require('../core/utils/regex').escapeRegExp(str);
2274
+ },
2275
+
2276
+ escapeRegex(str) {
2277
+ return require('../core/utils/regex').escapeRegExp(str);
2278
+ },
2279
+
2280
+ globToRegExp(pattern, options) {
2281
+ return require('../core/utils/regex').globToRegExp(pattern, options);
2282
+ },
2283
+
2284
+ parseDurationSec(duration, defaultSec) {
2285
+ return require('../core/utils/time').parseDurationSec(duration, defaultSec);
2286
+ },
2287
+
2288
+ parseDuration(duration, defaultSec) {
2289
+ return require('../core/utils/time').parseDurationSec(duration, defaultSec);
2290
+ },
2291
+
2292
+ parseDurationMs(ttl, defaultMs) {
2293
+ return require('../core/utils/time').parseDurationMs(ttl, defaultMs);
2294
+ },
2295
+
2296
+ parseTtlMs(ttl, defaultMs) {
2297
+ return require('../core/utils/time').parseDurationMs(ttl, defaultMs);
2298
+ },
2299
+
2300
+ sleep(ms) {
2301
+ return require('../core/utils/time').sleep(ms);
2302
+ },
2303
+
2304
+ safeJsonParse(input, fallback, reviver) {
2305
+ return require('../core/utils/json').safeJsonParse(input, fallback, reviver);
2306
+ },
2307
+
2308
+ safeJsonStringify(value, space, fallback) {
2309
+ return require('../core/utils/json').safeJsonStringify(value, space, fallback);
2310
+ },
2311
+
2312
+ isValidJson(str) {
2313
+ return require('../core/utils/json').isValidJson(str);
2314
+ },
2315
+
2316
+ toCamelCase(str) {
2317
+ return require('../core/utils/string').toCamelCase(str);
2318
+ },
2319
+
2320
+ snakeToCamel(str) {
2321
+ return require('../core/utils/string').snakeToCamel(str);
2322
+ },
2323
+
2324
+ toSnakeCase(str) {
2325
+ return require('../core/utils/string').toSnakeCase(str);
2326
+ },
2327
+
2328
+ camelToSnake(str) {
2329
+ return require('../core/utils/string').camelToSnake(str);
2330
+ },
2331
+
2332
+ toKebabCase(str) {
2333
+ return require('../core/utils/string').toKebabCase(str);
2334
+ },
2335
+
2336
+ toPascalCase(str) {
2337
+ return require('../core/utils/string').toPascalCase(str);
2338
+ },
2339
+
2340
+ ensureArray(value) {
2341
+ return require('../core/utils/object').ensureArray(value);
2342
+ },
2343
+
2344
+ pick(obj, keys) {
2345
+ return require('../core/utils/object').pick(obj, keys);
2346
+ },
2347
+
2348
+ omit(obj, keys) {
2349
+ return require('../core/utils/object').omit(obj, keys);
2350
+ },
2351
+
2352
+ deepClone(obj) {
2353
+ return require('../core/utils/object').deepClone(obj);
2207
2354
  }
2208
2355
  };
2209
2356
 
@@ -9,15 +9,7 @@
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
11
  const { scanDirSafely, isPrivateOrIgnored } = require('../discovery/file-route-parser');
12
-
13
- /**
14
- * Converts a file path to camelCase or dot-separated identifier
15
- * @param {string} str
16
- * @returns {string}
17
- */
18
- function toCamelCase(str) {
19
- return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase());
20
- }
12
+ const { toCamelCase } = require('../../core/utils/string');
21
13
 
22
14
  /**
23
15
  * Discovers services, middlewares, and configuration for a module directory
@@ -18,6 +18,7 @@ const {
18
18
  resolvePageAssets,
19
19
  applyPageAssetsToTemplateData,
20
20
  } = require('../file-router');
21
+ const { sendEarlyHints } = require('../../core/early-hints');
21
22
 
22
23
  const EMPTY_ARRAY = Object.freeze([]);
23
24
 
@@ -113,7 +114,7 @@ function createPageHandler(descriptor, context) {
113
114
  }
114
115
 
115
116
  // 3. Build Page Context
116
- const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
117
+ const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 7331}`;
117
118
  const locale = detectLocale(req);
118
119
  const fsy = createHelpers({ req, res, baseUrl, locale });
119
120
 
@@ -162,12 +163,63 @@ function createPageHandler(descriptor, context) {
162
163
  err.status = status;
163
164
  throw err;
164
165
  },
166
+ earlyHints: (links, cb) => sendEarlyHints(res, links, cb),
165
167
  fsy,
166
168
  locale,
167
169
  t,
168
170
  db,
169
171
  };
170
172
 
173
+ // 3.5. Dispatch Early Hints (if configured or enabled) before load()
174
+ const earlyHintsOpt = pageDef.earlyHints !== undefined
175
+ ? pageDef.earlyHints
176
+ : (descriptor.earlyHints !== undefined
177
+ ? descriptor.earlyHints
178
+ : (appOptions && (appOptions.earlyHints ?? appOptions.server?.earlyHints)));
179
+
180
+ if (earlyHintsOpt && earlyHintsOpt !== false && !res.headersSent) {
181
+ const auto = typeof earlyHintsOpt === 'object' ? (earlyHintsOpt.auto !== false) : Boolean(earlyHintsOpt);
182
+ const linksToSend = [];
183
+
184
+ // Explicit links from page definition or app earlyHints config
185
+ if (Array.isArray(earlyHintsOpt)) {
186
+ linksToSend.push(...earlyHintsOpt);
187
+ } else if (typeof earlyHintsOpt === 'object' && earlyHintsOpt.links) {
188
+ const customLinks = typeof earlyHintsOpt.links === 'function' ? earlyHintsOpt.links(req) : earlyHintsOpt.links;
189
+ if (Array.isArray(customLinks)) linksToSend.push(...customLinks);
190
+ else if (customLinks) linksToSend.push(customLinks);
191
+ }
192
+
193
+ // Auto-discover route CSS & JS assets via AssetManager
194
+ if (auto) {
195
+ const assetManager = fsy?.assetManager || getAssetManager();
196
+ if (assetManager && typeof assetManager.getRouteAssetCandidates === 'function') {
197
+ const candidates = assetManager.getRouteAssetCandidates(
198
+ descriptor.source || descriptor.file,
199
+ descriptor.path
200
+ );
201
+ if (Array.isArray(candidates)) {
202
+ for (const cand of candidates) {
203
+ if (typeof assetManager.hasPageCss === 'function' && assetManager.hasPageCss(cand)) {
204
+ linksToSend.push(assetManager.pageCssUrl(cand));
205
+ break;
206
+ }
207
+ }
208
+ for (const cand of candidates) {
209
+ if (typeof assetManager.hasPageJs === 'function' && assetManager.hasPageJs(cand)) {
210
+ linksToSend.push(assetManager.pageJsUrl(cand));
211
+ break;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ if (linksToSend.length > 0) {
219
+ sendEarlyHints(res, linksToSend);
220
+ }
221
+ }
222
+
171
223
  // 4. Run load() lifecycle
172
224
  let loadedData = {};
173
225
  if (typeof pageDef.load === 'function') {
@@ -7,6 +7,8 @@
7
7
  * Simple semver comparison utilities
8
8
  * (Lightweight alternative to full semver package)
9
9
  */
10
+ const { globToRegExp } = require('../core/utils/regex');
11
+
10
12
  const semverCache = new Map();
11
13
 
12
14
  const semver = {
@@ -112,15 +114,7 @@ function matchPattern(path, pattern) {
112
114
 
113
115
  let regex = patternRegexCache.get(pattern);
114
116
  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}$`);
117
+ regex = globToRegExp(pattern, { separator: '/' });
124
118
  if (patternRegexCache.size < 500) {
125
119
  patternRegexCache.set(pattern, regex);
126
120
  }
@@ -327,8 +321,7 @@ class PluginManager {
327
321
  }
328
322
  } catch (err) {
329
323
  console.warn(`[plugin-manager] Plugin "${plugin.name}" setup() failed:`, err.message);
330
- this.plugins.delete(plugin.name);
331
- if (plugin.api) this.pluginAPIs.delete(plugin.name);
324
+ await this._rollbackPlugin(plugin.name, plugin, ctx);
332
325
  return;
333
326
  }
334
327
  }
@@ -343,8 +336,7 @@ class PluginManager {
343
336
  }
344
337
  } catch (err) {
345
338
  console.warn(`[plugin-manager] Plugin "${plugin.name}" register() failed:`, err.message);
346
- this.plugins.delete(plugin.name);
347
- if (plugin.api) this.pluginAPIs.delete(plugin.name);
339
+ await this._rollbackPlugin(plugin.name, plugin, ctx);
348
340
  return;
349
341
  }
350
342
  }
@@ -362,6 +354,89 @@ class PluginManager {
362
354
  this._applyHelpersAndFilters();
363
355
  }
364
356
 
357
+ /**
358
+ * Roll back any partially acquired state or disposers for a failed plugin
359
+ * @private
360
+ */
361
+ async _rollbackPlugin(name, plugin, ctx) {
362
+ this.plugins.delete(name);
363
+ if (plugin && plugin.api) {
364
+ this.pluginAPIs.delete(name);
365
+ }
366
+
367
+ const remainingDisposers = [];
368
+ const rollbackDisposers = [];
369
+ for (const d of this.disposers) {
370
+ if (d.name === name) {
371
+ rollbackDisposers.push(d);
372
+ } else {
373
+ remainingDisposers.push(d);
374
+ }
375
+ }
376
+ this.disposers = remainingDisposers;
377
+
378
+ // Run disposers registered so far by this plugin in reverse order
379
+ for (let i = rollbackDisposers.length - 1; i >= 0; i--) {
380
+ try {
381
+ const res = rollbackDisposers[i].fn();
382
+ if (res && typeof res.then === 'function') {
383
+ await res;
384
+ }
385
+ } catch (cleanupErr) {
386
+ console.warn(`[plugin-manager] Rollback disposer for "${name}" failed:`, cleanupErr?.message);
387
+ }
388
+ }
389
+
390
+ if (plugin && typeof plugin.dispose === 'function') {
391
+ try {
392
+ const res = plugin.dispose(ctx);
393
+ if (res && typeof res.then === 'function') {
394
+ await res;
395
+ }
396
+ } catch (cleanupErr) {
397
+ console.warn(`[plugin-manager] Rollback dispose() for "${name}" failed:`, cleanupErr?.message);
398
+ }
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Roll back partially acquired state or disposers for a failed plugin (sync)
404
+ * @private
405
+ */
406
+ _rollbackPluginSync(name, plugin, ctx) {
407
+ this.plugins.delete(name);
408
+ if (plugin && plugin.api) {
409
+ this.pluginAPIs.delete(name);
410
+ }
411
+
412
+ const remainingDisposers = [];
413
+ const rollbackDisposers = [];
414
+ for (const d of this.disposers) {
415
+ if (d.name === name) {
416
+ rollbackDisposers.push(d);
417
+ } else {
418
+ remainingDisposers.push(d);
419
+ }
420
+ }
421
+ this.disposers = remainingDisposers;
422
+
423
+ for (let i = rollbackDisposers.length - 1; i >= 0; i--) {
424
+ try {
425
+ rollbackDisposers[i].fn();
426
+ } catch (cleanupErr) {
427
+ console.warn(`[plugin-manager] Rollback disposer for "${name}" failed:`, cleanupErr?.message);
428
+ }
429
+ }
430
+
431
+ if (plugin && typeof plugin.dispose === 'function') {
432
+ try {
433
+ plugin.dispose(ctx);
434
+ } catch (cleanupErr) {
435
+ console.warn(`[plugin-manager] Rollback dispose() for "${name}" failed:`, cleanupErr?.message);
436
+ }
437
+ }
438
+ }
439
+
365
440
  /**
366
441
  * Register a single plugin (sync)
367
442
  */
@@ -402,8 +477,7 @@ class PluginManager {
402
477
  }
403
478
  } catch (err) {
404
479
  console.warn(`[plugin-manager] Plugin "${plugin.name}" setup() failed:`, err.message);
405
- this.plugins.delete(plugin.name);
406
- if (plugin.api) this.pluginAPIs.delete(plugin.name);
480
+ this._rollbackPluginSync(plugin.name, plugin, ctx);
407
481
  return;
408
482
  }
409
483
  }
@@ -418,8 +492,7 @@ class PluginManager {
418
492
  }
419
493
  } catch (err) {
420
494
  console.warn(`[plugin-manager] Plugin "${plugin.name}" register() failed:`, err.message);
421
- this.plugins.delete(plugin.name);
422
- if (plugin.api) this.pluginAPIs.delete(plugin.name);
495
+ this._rollbackPluginSync(plugin.name, plugin, ctx);
423
496
  return;
424
497
  }
425
498
  }