webspresso 0.0.104 → 0.0.106

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 (233) hide show
  1. package/.agents/AGENTS.md +21 -2
  2. package/.agents/ARCHITECTURE.md +23 -0
  3. package/.agents/CACHE.md +147 -0
  4. package/.agents/CLI.md +220 -8
  5. package/.agents/EARLY_HINTS.md +120 -0
  6. package/.agents/FLAGS.md +118 -0
  7. package/.agents/HEALTH.md +118 -0
  8. package/.agents/LIMITS.md +109 -0
  9. package/.agents/LOGGING.md +93 -0
  10. package/.agents/METRICS.md +127 -0
  11. package/.agents/MIGRATION_JOBS.md +95 -0
  12. package/.agents/ORM.md +155 -1
  13. package/.agents/PLUGINS.md +38 -3
  14. package/.agents/QUEUE.md +42 -0
  15. package/.agents/RATE_LIMIT.md +147 -0
  16. package/.agents/RESILIENCE.md +235 -0
  17. package/.agents/SCALE_OUT.md +109 -0
  18. package/.agents/SECRETS.md +117 -0
  19. package/AGENTS.md +16 -0
  20. package/README.md +2 -2
  21. package/bin/commands/add-aws.js +396 -0
  22. package/bin/commands/add-ci.js +523 -0
  23. package/bin/commands/add-cloudflare.js +240 -0
  24. package/bin/commands/add-cloudrun.js +381 -0
  25. package/bin/commands/add-docker.js +6 -6
  26. package/bin/commands/add-migration-job.js +396 -0
  27. package/bin/commands/assets-sync.js +209 -0
  28. package/bin/commands/client-generate.js +52 -0
  29. package/bin/commands/console.js +53 -0
  30. package/bin/commands/db-migrate.js +47 -2
  31. package/bin/commands/db-studio.js +84 -0
  32. package/bin/commands/deploy-check.js +242 -13
  33. package/bin/commands/dev.js +1 -1
  34. package/bin/commands/export.js +87 -0
  35. package/bin/commands/new.js +5 -5
  36. package/bin/commands/openapi.js +1 -1
  37. package/bin/commands/secrets-pull.js +208 -0
  38. package/bin/commands/start.js +1 -1
  39. package/bin/utils/db.js +7 -1
  40. package/bin/utils/project.js +2 -2
  41. package/bin/webspresso.js +22 -0
  42. package/core/app.js +421 -0
  43. package/core/auth/authenticate.js +93 -0
  44. package/core/auth/hash.js +5 -4
  45. package/core/auth/index.js +34 -4
  46. package/core/auth/jwt.js +5 -54
  47. package/core/auth/magic-link.js +4 -5
  48. package/core/auth/manager.js +53 -10
  49. package/core/auth/middleware.js +16 -68
  50. package/core/auth/oauth.js +8 -17
  51. package/core/auth/redis-session.js +1008 -0
  52. package/core/auth/sessions.js +156 -0
  53. package/core/auth/totp.js +2 -1
  54. package/core/cache/byte-size.js +123 -0
  55. package/core/cache/cdn.js +296 -0
  56. package/core/cache/index.js +33 -0
  57. package/core/cache/lru.js +477 -0
  58. package/core/cache/tagged-lru.js +354 -0
  59. package/core/commands/registry.js +6 -20
  60. package/core/console/commands.js +148 -0
  61. package/core/console/context.js +260 -0
  62. package/core/console/eval.js +70 -0
  63. package/core/console/index.js +22 -0
  64. package/core/console/repl.js +102 -0
  65. package/core/early-hints/index.js +267 -0
  66. package/core/errors/domain.js +49 -0
  67. package/core/errors/http.js +31 -0
  68. package/core/errors/index.js +10 -0
  69. package/core/errors/normalize.js +88 -2
  70. package/core/events/discovery.js +6 -24
  71. package/core/events/dispatcher.js +9 -7
  72. package/core/events/signal.js +7 -0
  73. package/core/export/discovery.js +191 -0
  74. package/core/export/exporter.js +277 -0
  75. package/core/export/index.js +24 -0
  76. package/core/flags/evaluator.js +252 -0
  77. package/core/flags/hasher.js +37 -0
  78. package/core/flags/index.js +21 -0
  79. package/core/flags/manager.js +263 -0
  80. package/core/flags/store.js +126 -0
  81. package/core/health/index.js +41 -0
  82. package/core/health/middleware.js +213 -0
  83. package/core/health/normalize.js +140 -0
  84. package/core/health/probes.js +203 -0
  85. package/core/http/adapter.js +74 -0
  86. package/core/http/express-adapter.js +46 -0
  87. package/core/http/hono-adapter.js +531 -0
  88. package/core/http/index.js +41 -0
  89. package/core/http/pipeline.js +485 -0
  90. package/core/index.js +336 -0
  91. package/core/limits/index.js +34 -0
  92. package/core/limits/middleware.js +325 -0
  93. package/core/limits/normalize.js +222 -0
  94. package/core/logger/formatters.js +264 -0
  95. package/core/logger/index.js +260 -0
  96. package/core/logger/trace.js +137 -0
  97. package/core/metrics/collectors.js +125 -0
  98. package/core/metrics/index.js +45 -0
  99. package/core/metrics/middleware.js +230 -0
  100. package/core/metrics/registry.js +366 -0
  101. package/core/orm/batch.js +70 -0
  102. package/core/orm/cache/fingerprint.js +2 -2
  103. package/core/orm/cache/memory-provider.js +27 -110
  104. package/core/orm/d1.js +265 -0
  105. package/core/orm/index.js +176 -13
  106. package/core/orm/json-fields.js +22 -8
  107. package/core/orm/migrate-cloud.js +194 -0
  108. package/core/orm/model.js +10 -0
  109. package/core/orm/pool.js +97 -0
  110. package/core/orm/query-builder.js +72 -9
  111. package/core/orm/repository.js +318 -19
  112. package/core/orm/schema-helpers.js +8 -10
  113. package/core/orm/scopes.js +321 -1
  114. package/core/orm/transaction.js +10 -2
  115. package/core/orm/utils.js +4 -86
  116. package/core/queue/adapters/cloudflare.js +131 -0
  117. package/core/queue/adapters/database.js +4 -8
  118. package/core/queue/adapters/pubsub.js +400 -0
  119. package/core/queue/adapters/sqs.js +419 -0
  120. package/core/queue/adapters/wait-until.js +77 -0
  121. package/core/queue/index.js +8 -0
  122. package/core/queue/manager.js +66 -41
  123. package/core/rate-limit/index.js +22 -0
  124. package/core/rate-limit/limiter.js +219 -0
  125. package/core/rate-limit/stores/memory.js +129 -0
  126. package/core/rate-limit/stores/redis.js +151 -0
  127. package/core/rate-limit/utils.js +112 -0
  128. package/core/resilience/circuit-breaker.js +422 -0
  129. package/core/resilience/errors.js +63 -0
  130. package/core/resilience/index.js +53 -0
  131. package/core/resilience/registry.js +97 -0
  132. package/core/resilience/retry.js +203 -0
  133. package/core/schema-cache.js +9 -2
  134. package/core/secrets/index.js +113 -0
  135. package/core/secrets/manager.js +308 -0
  136. package/core/secrets/providers/aws.js +288 -0
  137. package/core/secrets/providers/env.js +44 -0
  138. package/core/secrets/providers/gcp.js +307 -0
  139. package/core/secrets/providers/memory.js +76 -0
  140. package/core/shutdown/http-adapter.js +4 -1
  141. package/core/shutdown/shutdown-manager.js +129 -19
  142. package/core/stream/index.js +19 -0
  143. package/core/stream/text-stream.js +326 -0
  144. package/core/studio/index.js +43 -0
  145. package/core/studio/inspector.js +258 -0
  146. package/core/studio/server.js +325 -0
  147. package/core/studio/ui.js +681 -0
  148. package/core/utils/array.js +12 -0
  149. package/core/utils/base64url.js +8 -0
  150. package/core/utils/crypto.js +118 -0
  151. package/core/utils/ensure-leading-slash.js +6 -0
  152. package/core/utils/escape-regexp.js +7 -0
  153. package/core/utils/fetch.js +6 -5
  154. package/core/utils/fs.js +144 -0
  155. package/core/utils/hmac.js +6 -0
  156. package/core/utils/json.js +98 -0
  157. package/core/utils/object.js +106 -0
  158. package/core/utils/random.js +9 -0
  159. package/core/utils/regex.js +77 -0
  160. package/core/utils/sha256.js +6 -0
  161. package/core/utils/string.js +87 -0
  162. package/core/utils/strip-trailing-slash.js +6 -0
  163. package/core/utils/time.js +96 -0
  164. package/core/utils/timeout.js +348 -0
  165. package/core/utils/url.js +106 -0
  166. package/core/utils/walk.js +7 -0
  167. package/index.d.ts +1549 -2
  168. package/index.js +272 -2
  169. package/package.json +3 -2
  170. package/plugins/admin-panel/core/api-extensions.js +2 -5
  171. package/plugins/admin-panel/index.js +13 -6
  172. package/plugins/admin-panel/modules/menu.js +1 -1
  173. package/plugins/api-key/crypto.js +3 -3
  174. package/plugins/api-key/index.js +3 -2
  175. package/plugins/api-key/scopes.js +5 -7
  176. package/plugins/audit-log/api-handlers.js +6 -15
  177. package/plugins/audit-log/index.js +3 -1
  178. package/plugins/audit-log/parse.js +3 -3
  179. package/plugins/basic-auth/index.js +3 -2
  180. package/plugins/cors.js +3 -2
  181. package/plugins/csrf/index.js +9 -12
  182. package/plugins/csv/index.js +8 -5
  183. package/plugins/dashboard/app.js +2 -2
  184. package/plugins/dashboard/index.js +5 -5
  185. package/plugins/data-exchange/record-selection.js +2 -5
  186. package/plugins/email/api-handlers.js +5 -18
  187. package/plugins/email/auth-bridge.js +3 -2
  188. package/plugins/email/index.js +11 -9
  189. package/plugins/health-check.js +2 -1
  190. package/plugins/index.js +5 -0
  191. package/plugins/lazy.js +5 -6
  192. package/plugins/mcp/discovery.js +18 -17
  193. package/plugins/polar/src/api.js +2 -1
  194. package/plugins/polar/src/webhooks.js +5 -6
  195. package/plugins/queue/index.js +18 -2
  196. package/plugins/rate-limit/index.js +3 -1
  197. package/plugins/redirect/index.js +16 -9
  198. package/plugins/rls/index.js +170 -0
  199. package/plugins/seo-checker/index.js +2 -2
  200. package/plugins/site-analytics/index.js +3 -1
  201. package/plugins/site-analytics/tracking.js +2 -2
  202. package/plugins/sitemap.js +3 -2
  203. package/plugins/swagger.js +12 -5
  204. package/plugins/upload/gcs-provider.js +148 -0
  205. package/plugins/upload/index.js +136 -77
  206. package/plugins/upload/orphan-pruner.js +4 -3
  207. package/plugins/upload/s3-provider.js +257 -0
  208. package/plugins/webhooks/crypto.js +6 -11
  209. package/plugins/webhooks/dispatcher.js +8 -8
  210. package/plugins/webhooks/index.js +33 -23
  211. package/plugins/webhooks/matcher.js +7 -11
  212. package/plugins/webhooks/sender.js +8 -11
  213. package/plugins/xlsx/index.js +7 -5
  214. package/src/api/api-loader.js +12 -95
  215. package/src/api/define-api.js +1 -0
  216. package/src/app-context.js +41 -5
  217. package/src/client-runtime/constants.js +10 -0
  218. package/src/client-runtime/mount.js +2 -3
  219. package/src/dev/banner.js +6 -2
  220. package/src/dev/client-gen.js +458 -0
  221. package/src/discovery/scan-routes.js +5 -2
  222. package/src/file-router.js +3 -3
  223. package/src/helpers.js +212 -24
  224. package/src/modules/module-discovery.js +1 -9
  225. package/src/pages/page-loader.js +13 -154
  226. package/src/plugin-manager.js +99 -18
  227. package/src/server.js +335 -133
  228. package/src/services/discovery.js +19 -40
  229. package/src/services/executor.js +95 -25
  230. package/src/services/memoize.js +3 -25
  231. package/src/services/registry.js +15 -1
  232. package/src/services/validator.js +38 -20
  233. package/src/template/precompile.js +8 -19
@@ -0,0 +1,485 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso HTTP Pipeline Layer
5
+ * Framework-agnostic execution pipelines for API endpoints and SSR routes
6
+ * @module core/http/pipeline
7
+ */
8
+
9
+ const path = require('path');
10
+ const { ZodError } = require('zod');
11
+ const { translateValidationIssues, getTranslatorForRequest } = require('../../src/api/validation-i18n');
12
+
13
+ /**
14
+ * Runs a sequence of middleware functions
15
+ * Supports Express-style (req, res, next)
16
+ * @param {Function[]} middlewares
17
+ * @param {Object} req
18
+ * @param {Object} res
19
+ */
20
+ async function runMiddlewares(middlewares, req, res) {
21
+ if (!Array.isArray(middlewares)) return;
22
+
23
+ for (const mw of middlewares) {
24
+ if (typeof mw !== 'function') continue;
25
+ await new Promise((resolve, reject) => {
26
+ let settled = false;
27
+ const done = (err) => {
28
+ if (settled) return;
29
+ settled = true;
30
+ if (err) reject(err);
31
+ else resolve();
32
+ };
33
+
34
+ const prevEnd = res?.end;
35
+ if (res && typeof res.end === 'function') {
36
+ res.end = function (...args) {
37
+ const ret = prevEnd.apply(this, args);
38
+ done();
39
+ return ret;
40
+ };
41
+ }
42
+
43
+ try {
44
+ mw(req, res, (err) => {
45
+ done(err);
46
+ });
47
+ } catch (syncErr) {
48
+ done(syncErr);
49
+ }
50
+ });
51
+ if (res && res.headersSent) return;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Decorates an error with route execution metadata
57
+ * @param {Error} err
58
+ * @param {Object} descriptor
59
+ * @param {Object} req
60
+ * @returns {Error}
61
+ */
62
+ function enrichRouteError(err, descriptor, req) {
63
+ if (err && typeof err === 'object') {
64
+ if (!err.route) err.route = descriptor.path;
65
+ if (!err.method) err.method = descriptor.method ? descriptor.method.toUpperCase() : (req.method || 'GET');
66
+ if (!err.source) err.source = descriptor.source || descriptor.file;
67
+ if (!err.file) err.file = descriptor.file;
68
+ if (!err.module) err.module = descriptor.module || null;
69
+ if (!err.phase) err.phase = descriptor && descriptor.type === 'page' ? 'page' : 'handler';
70
+ if (!err.requestId) err.requestId = req && (req.id || (req.headers && req.headers['x-request-id'])) || null;
71
+ }
72
+ return err;
73
+ }
74
+
75
+ /**
76
+ * Executes an API endpoint pipeline
77
+ * @param {Object} params
78
+ * @param {Object} params.descriptor
79
+ * @param {Object} params.context
80
+ * @param {Object} params.apiDef
81
+ * @param {Function} params.handlerFn
82
+ * @param {Object} [params.compiledSchema]
83
+ * @param {Function[]} [params.middlewares]
84
+ * @param {Object} params.req
85
+ * @param {Object} params.res
86
+ * @returns {Promise<any>}
87
+ */
88
+ async function executeApiPipeline(params) {
89
+ const {
90
+ descriptor,
91
+ context = {},
92
+ apiDef,
93
+ handlerFn,
94
+ compiledSchema,
95
+ middlewares = [],
96
+ req,
97
+ res,
98
+ } = params;
99
+
100
+ const appOptions = context.options || {};
101
+ const isDev = context.isDev !== undefined
102
+ ? context.isDev
103
+ : (appOptions.isDev !== undefined ? appOptions.isDev : process.env.NODE_ENV === 'development');
104
+ const db = context.db || req.db;
105
+ const serviceRegistry = context.serviceRegistry;
106
+
107
+ // 1. Injections
108
+ if (db && !req.db) req.db = db;
109
+ if (!req.service && serviceRegistry) {
110
+ req.service = (name, input, opts) => {
111
+ const baseContext = req.context || { req, res, db, user: req.user, session: req.session };
112
+ return serviceRegistry.call(name, input, baseContext, opts);
113
+ };
114
+ }
115
+
116
+ // 2. Schema validation
117
+ if (compiledSchema) {
118
+ try {
119
+ const { validateRequestInput } = require('../../src/api/api-loader');
120
+ validateRequestInput(req, compiledSchema);
121
+ } catch (err) {
122
+ if (err instanceof ZodError || err.name === 'ZodError' || Array.isArray(err?.issues)) {
123
+ const pagesDir = context.pagesDir || appOptions.pagesDir || 'pages';
124
+ const routeDir = descriptor.file ? path.dirname(descriptor.file) : '';
125
+ const translatedIssues = translateValidationIssues(err.issues, {
126
+ req,
127
+ pagesDir,
128
+ routeDir,
129
+ isDev,
130
+ });
131
+ if (res && typeof res.status === 'function' && typeof res.json === 'function') {
132
+ return res.status(400).json({
133
+ error: 'Validation Error',
134
+ issues: translatedIssues,
135
+ });
136
+ }
137
+ const validationErr = new Error('Validation Error');
138
+ validationErr.status = 400;
139
+ validationErr.issues = translatedIssues;
140
+ throw validationErr;
141
+ }
142
+ throw err;
143
+ }
144
+ } else {
145
+ req.input = {
146
+ body: req.body,
147
+ params: req.params,
148
+ query: req.query,
149
+ };
150
+ }
151
+
152
+ // 2.5 Feature Flag Guard
153
+ if (apiDef && apiDef.flag) {
154
+ const flagKey = typeof apiDef.flag === 'string' ? apiDef.flag : apiDef.flag.key;
155
+ const isEnabled = typeof req.hasFeature === 'function'
156
+ ? req.hasFeature(flagKey)
157
+ : ((context.app?.flags || context.flags)?.isEnabled(flagKey, req) ?? false);
158
+
159
+ if (!isEnabled) {
160
+ const statusCode = (typeof apiDef.flag === 'object' && apiDef.flag.statusCode) || 404;
161
+ const message = (typeof apiDef.flag === 'object' && apiDef.flag.message) || (statusCode === 403 ? 'Forbidden' : 'Not Found');
162
+
163
+ if (res && typeof res.status === 'function') {
164
+ return res.status(statusCode).json({
165
+ error: statusCode === 403 ? 'Forbidden' : 'Not Found',
166
+ message,
167
+ feature: flagKey,
168
+ });
169
+ }
170
+ const notFound = new Error(message);
171
+ notFound.status = statusCode;
172
+ notFound.statusCode = statusCode;
173
+ throw notFound;
174
+ }
175
+ }
176
+
177
+ // 3. Middlewares
178
+ if (middlewares.length > 0) {
179
+ await runMiddlewares(middlewares, req, res);
180
+ if (res && res.headersSent) return;
181
+ }
182
+
183
+ // 4. Build Context & Execute
184
+ const pagesDir = context.pagesDir || appOptions.pagesDir || 'pages';
185
+ const routeDir = descriptor.file ? path.dirname(descriptor.file) : '';
186
+ const tr = getTranslatorForRequest(req, { pagesDir, routeDir, isDev });
187
+
188
+ const apiCtx = {
189
+ req,
190
+ res,
191
+ params: req.params || {},
192
+ query: req.query || {},
193
+ body: req.body || {},
194
+ input: req.input,
195
+ locale: tr.locale,
196
+ t: tr.t,
197
+ service: req.service
198
+ ? req.service
199
+ : (name, input, opts) =>
200
+ serviceRegistry ? serviceRegistry.call(name, input, req.context || { req, res, db, user: req.user, session: req.session }, opts) : null,
201
+ module: descriptor.module || null,
202
+ config: appOptions,
203
+ logger: console,
204
+ db,
205
+ flags: req.flags || null,
206
+ hasFeature: typeof req.hasFeature === 'function' ? (k, c) => req.hasFeature(k, c) : () => false,
207
+ getVariant: typeof req.getVariant === 'function' ? (k, c) => req.getVariant(k, c) : () => null,
208
+ sse: (sourceOrOptions, maybeOptions) => (res && typeof res.sse === 'function' ? res.sse(sourceOrOptions, maybeOptions) : null),
209
+ streamText: (sourceOrCallback, maybeOptions) => (res && typeof res.streamText === 'function' ? res.streamText(sourceOrCallback, maybeOptions) : null),
210
+ streamJson: (sourceOrCallback, maybeOptions) => (res && typeof res.streamJson === 'function' ? res.streamJson(sourceOrCallback, maybeOptions) : null),
211
+ };
212
+
213
+ req.ctx = apiCtx;
214
+ if (res) {
215
+ res.service = apiCtx.service;
216
+ res.input = apiCtx.input;
217
+ res.module = apiCtx.module;
218
+ }
219
+
220
+ const result = await handlerFn(req, res, apiCtx);
221
+
222
+ // 5. Auto-respond if handler returned a value and response not yet sent
223
+ if (res && !res.headersSent) {
224
+ if (result !== undefined && typeof res.json === 'function') {
225
+ res.json(result);
226
+ return result;
227
+ }
228
+ }
229
+
230
+ return result;
231
+ }
232
+
233
+ /**
234
+ * Executes an SSR page route pipeline
235
+ * @param {Object} params
236
+ */
237
+ async function executePagePipeline(params) {
238
+ const {
239
+ descriptor,
240
+ context = {},
241
+ pageDef,
242
+ middlewares = [],
243
+ req,
244
+ res,
245
+ nunjucks,
246
+ njkCandidate,
247
+ hasNjkTemplate,
248
+ } = params;
249
+
250
+ const appOptions = context.options || {};
251
+ const isDev = context.isDev !== undefined
252
+ ? context.isDev
253
+ : (appOptions.isDev !== undefined ? appOptions.isDev : process.env.NODE_ENV === 'development');
254
+ const db = context.db || req.db;
255
+ const serviceRegistry = context.serviceRegistry;
256
+ const pagesDir = context.pagesDir || appOptions.pagesDir || 'pages';
257
+
258
+ // 1. Injections
259
+ if (db && !req.db) req.db = db;
260
+ if (!req.service && serviceRegistry) {
261
+ const baseContext = req.context || { req, res, db, user: req.user, session: req.session };
262
+ req.service = (name, input, opts) =>
263
+ serviceRegistry.call(name, input, baseContext, opts);
264
+ }
265
+
266
+ // 2. Middlewares
267
+ if (middlewares.length > 0) {
268
+ await runMiddlewares(middlewares, req, res);
269
+ if (res && res.headersSent) return;
270
+ }
271
+
272
+ // 3. Build Page Context
273
+ const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 7331}`;
274
+ const { detectLocale, loadI18n, createTranslator, resolvePageAssets, applyPageAssetsToTemplateData } = require('../../src/file-router');
275
+ const { createHelpers, getAssetManager } = require('../../src/helpers');
276
+ const { sendEarlyHints } = require('../early-hints');
277
+
278
+ const locale = detectLocale(req);
279
+ const fsy = createHelpers({ req, res, baseUrl, locale });
280
+
281
+ const defaultLocale = process.env.DEFAULT_LOCALE || 'en';
282
+ let t;
283
+ const translatorCache = context.translatorCache;
284
+ if (!isDev && translatorCache) {
285
+ const transKey = `${pagesDir}::::${locale}::${defaultLocale}`;
286
+ t = translatorCache.get(transKey);
287
+ if (!t) {
288
+ const translations = loadI18n(pagesDir, '', locale, false);
289
+ const fallbackTranslations = locale !== defaultLocale ? loadI18n(pagesDir, '', defaultLocale, false) : undefined;
290
+ t = createTranslator(translations, {
291
+ locale,
292
+ fallbackTranslations,
293
+ fallbackLocale: defaultLocale,
294
+ });
295
+ translatorCache.set(transKey, t);
296
+ }
297
+ } else {
298
+ const translations = loadI18n(pagesDir, '', locale, isDev);
299
+ const fallbackTranslations = locale !== defaultLocale ? loadI18n(pagesDir, '', defaultLocale, isDev) : undefined;
300
+ t = createTranslator(translations, {
301
+ locale,
302
+ fallbackTranslations,
303
+ fallbackLocale: defaultLocale,
304
+ });
305
+ }
306
+
307
+ const pageCtx = {
308
+ req,
309
+ res,
310
+ params: req.params || {},
311
+ query: req.query || {},
312
+ service: req.service
313
+ ? req.service
314
+ : (name, input, opts) =>
315
+ serviceRegistry ? serviceRegistry.call(name, input, req.context || { req, res, db, user: req.user, session: req.session }, opts) : null,
316
+ module: descriptor.module || null,
317
+ config: appOptions,
318
+ logger: console,
319
+ redirect: (url, status = 302) => {
320
+ if (res && typeof res.redirect === 'function') {
321
+ res.redirect(status, url);
322
+ }
323
+ return { __redirected: true };
324
+ },
325
+ error: (status = 500, message = 'Page Error') => {
326
+ const err = new Error(message);
327
+ err.status = status;
328
+ throw err;
329
+ },
330
+ earlyHints: (links, cb) => sendEarlyHints(res, links, cb),
331
+ fsy,
332
+ locale,
333
+ t,
334
+ db,
335
+ };
336
+
337
+ // 4. Early Hints
338
+ const earlyHintsOpt = pageDef.earlyHints !== undefined
339
+ ? pageDef.earlyHints
340
+ : (descriptor.earlyHints !== undefined
341
+ ? descriptor.earlyHints
342
+ : (appOptions && (appOptions.earlyHints ?? appOptions.server?.earlyHints)));
343
+
344
+ if (earlyHintsOpt && earlyHintsOpt !== false && res && !res.headersSent) {
345
+ const auto = typeof earlyHintsOpt === 'object' ? (earlyHintsOpt.auto !== false) : Boolean(earlyHintsOpt);
346
+ const linksToSend = [];
347
+
348
+ if (Array.isArray(earlyHintsOpt)) {
349
+ linksToSend.push(...earlyHintsOpt);
350
+ } else if (typeof earlyHintsOpt === 'object' && earlyHintsOpt.links) {
351
+ const customLinks = typeof earlyHintsOpt.links === 'function' ? earlyHintsOpt.links(req) : earlyHintsOpt.links;
352
+ if (Array.isArray(customLinks)) linksToSend.push(...customLinks);
353
+ else if (customLinks) linksToSend.push(customLinks);
354
+ }
355
+
356
+ if (auto) {
357
+ const assetManager = fsy?.assetManager || getAssetManager();
358
+ if (assetManager && typeof assetManager.getRouteAssetCandidates === 'function') {
359
+ const candidates = assetManager.getRouteAssetCandidates(
360
+ descriptor.source || descriptor.file,
361
+ descriptor.path
362
+ );
363
+ if (Array.isArray(candidates)) {
364
+ for (const cand of candidates) {
365
+ if (typeof assetManager.hasPageCss === 'function' && assetManager.hasPageCss(cand)) {
366
+ linksToSend.push(assetManager.pageCssUrl(cand));
367
+ break;
368
+ }
369
+ }
370
+ for (const cand of candidates) {
371
+ if (typeof assetManager.hasPageJs === 'function' && assetManager.hasPageJs(cand)) {
372
+ linksToSend.push(assetManager.pageJsUrl(cand));
373
+ break;
374
+ }
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ if (linksToSend.length > 0) {
381
+ sendEarlyHints(res, linksToSend);
382
+ }
383
+ }
384
+
385
+ // 5. Run load() lifecycle
386
+ let loadedData = {};
387
+ if (typeof pageDef.load === 'function') {
388
+ loadedData = (await pageDef.load(pageCtx)) || {};
389
+ if (loadedData.__redirected || (res && res.headersSent)) {
390
+ return;
391
+ }
392
+ }
393
+
394
+ // 6. Run head() lifecycle
395
+ let headData = {};
396
+ if (typeof pageDef.head === 'function') {
397
+ headData = (await pageDef.head(loadedData, pageCtx)) || {};
398
+ }
399
+
400
+ // 7. Check for companion Nunjucks template
401
+ const fs = require('fs');
402
+ const templateExists = hasNjkTemplate !== undefined && hasNjkTemplate !== null
403
+ ? hasNjkTemplate
404
+ : (njkCandidate ? fs.existsSync(njkCandidate) : false);
405
+
406
+ if (templateExists && nunjucks) {
407
+ let pageHead = headData;
408
+ let pageAssetsActive = false;
409
+ const pageAssetsOpt = context.pageAssets || (appOptions && appOptions.pageAssets);
410
+ if (pageAssetsOpt) {
411
+ const pageAssetsResolved = resolvePageAssets(pageAssetsOpt);
412
+ if (pageAssetsResolved.enabled) {
413
+ const pageAssetBundle = applyPageAssetsToTemplateData(
414
+ pageAssetsResolved,
415
+ loadedData,
416
+ {
417
+ file: descriptor.source || descriptor.file,
418
+ routePath: descriptor.path,
419
+ },
420
+ fsy?.assetManager || getAssetManager()
421
+ );
422
+ loadedData = pageAssetBundle.data;
423
+ pageHead = {
424
+ ...headData,
425
+ stylesheets: [...(headData.stylesheets || []), ...(pageAssetBundle.pageHead?.stylesheets || [])],
426
+ scripts: [...(headData.scripts || []), ...(pageAssetBundle.pageHead?.scripts || [])],
427
+ };
428
+ pageAssetsActive = true;
429
+ }
430
+ }
431
+
432
+ const templateData = {
433
+ ...loadedData,
434
+ head: pageHead,
435
+ pageHead,
436
+ pageAssets: pageAssetsActive || Boolean(loadedData.pageAssets),
437
+ fsy,
438
+ locale,
439
+ t,
440
+ params: req.params,
441
+ query: req.query,
442
+ };
443
+
444
+ return new Promise((resolve, reject) => {
445
+ nunjucks.render(njkCandidate, templateData, (err, html) => {
446
+ if (err) return reject(err);
447
+ if (res) {
448
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
449
+ res.send(html);
450
+ }
451
+ resolve(html);
452
+ });
453
+ });
454
+ }
455
+
456
+ // 8. Render via definePage render() method
457
+ if (typeof pageDef.render === 'function') {
458
+ const html = await pageDef.render(loadedData, pageCtx);
459
+ if (res) {
460
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
461
+ return res.send(html);
462
+ }
463
+ return html;
464
+ }
465
+
466
+ // 9. Fallback: if json or data
467
+ if (typeof loadedData === 'object' && loadedData !== null) {
468
+ if (res && typeof res.json === 'function') {
469
+ return res.json(loadedData);
470
+ }
471
+ return loadedData;
472
+ }
473
+
474
+ if (res && typeof res.send === 'function') {
475
+ return res.send(String(loadedData));
476
+ }
477
+ return String(loadedData);
478
+ }
479
+
480
+ module.exports = {
481
+ runMiddlewares,
482
+ enrichRouteError,
483
+ executeApiPipeline,
484
+ executePagePipeline,
485
+ };