webspresso 0.0.107 → 0.0.108
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/AGENTS.md +4 -1
- package/.agents/CACHE.md +1 -0
- package/.agents/CONTEXT.md +152 -0
- package/.agents/ORM.md +74 -0
- package/.agents/SHUTDOWN.md +8 -1
- package/.agents/skills/webspresso-framework/SKILL.md +2 -1
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +9 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +18 -2
- package/.agents/skills/webspresso-services/SKILL.md +36 -3
- package/AGENTS.md +6 -3
- package/core/app.js +7 -5
- package/core/applySchema.js +3 -72
- package/core/cache/lru.js +10 -0
- package/core/cache/tagged-lru.js +4 -0
- package/core/compileSchema.js +3 -68
- package/core/compression/compressible.js +53 -6
- package/core/compression/negotiate.js +31 -12
- package/core/context/app-context.js +201 -0
- package/core/context/index.js +55 -0
- package/core/context/request.js +241 -0
- package/core/events/dispatcher.js +144 -15
- package/core/events/signal.js +9 -0
- package/core/health/probes.js +12 -2
- package/core/http/index.js +10 -1
- package/core/http/pipeline.js +519 -368
- package/core/index.js +24 -1
- package/core/orm/eager-loader.js +90 -55
- package/core/orm/index.js +3 -39
- package/core/orm/model.js +6 -1
- package/core/orm/schema-helpers.js +59 -0
- package/core/orm/transaction.js +109 -14
- package/core/plugins/index.js +8 -0
- package/core/plugins/plugin-manager.js +1098 -0
- package/core/queue/manager.js +55 -27
- package/core/rate-limit/stores/memory.js +23 -10
- package/core/resilience/circuit-breaker.js +34 -14
- package/core/schema-cache.js +2 -64
- package/core/services/builtins/auth.js +273 -0
- package/core/services/builtins/exchange.js +229 -0
- package/core/services/builtins/file-manager.js +178 -0
- package/core/services/builtins/mail.js +134 -0
- package/core/services/builtins/media.js +196 -0
- package/core/services/builtins/system.js +219 -0
- package/core/services/discovery.js +133 -0
- package/core/services/executor.js +629 -0
- package/core/services/index.js +110 -5
- package/core/services/loader.js +161 -0
- package/core/services/memoize.js +408 -0
- package/core/services/registry.js +731 -0
- package/core/services/validator.js +424 -0
- package/core/shutdown/shutdown-manager.js +146 -85
- package/core/ssr/stream.js +109 -16
- package/core/stream/text-stream.js +45 -4
- package/core/utils/index.js +110 -0
- package/core/utils/timeout.js +4 -0
- package/core/validation/apply.js +74 -0
- package/core/validation/cache.js +66 -0
- package/core/validation/compiler.js +77 -0
- package/core/validation/index.js +22 -1
- package/index.d.ts +77 -4
- package/index.js +64 -20
- package/package.json +1 -1
- package/src/app-context.js +4 -196
- package/src/dev/typegen.js +1 -1
- package/src/plugin-manager.js +5 -1086
- package/src/server.js +4 -1
- package/src/services/builtins/auth.js +2 -273
- package/src/services/builtins/exchange.js +2 -229
- package/src/services/builtins/file-manager.js +2 -178
- package/src/services/builtins/mail.js +2 -134
- package/src/services/builtins/media.js +2 -196
- package/src/services/builtins/system.js +2 -219
- package/src/services/discovery.js +2 -133
- package/src/services/executor.js +2 -575
- package/src/services/index.js +4 -109
- package/src/services/loader.js +1 -119
- package/src/services/memoize.js +2 -408
- package/src/services/registry.js +1 -711
- package/src/services/validator.js +2 -424
- package/core/utils/array.js +0 -12
- package/core/utils/base64url.js +0 -8
- package/core/utils/ensure-leading-slash.js +0 -6
- package/core/utils/escape-html.js +0 -7
- package/core/utils/escape-regexp.js +0 -7
- package/core/utils/get-client-ip.js +0 -6
- package/core/utils/hmac.js +0 -6
- package/core/utils/random.js +0 -9
- package/core/utils/safe-compare.js +0 -3
- package/core/utils/sha256.js +0 -6
- package/core/utils/strip-trailing-slash.js +0 -6
- package/core/utils/walk.js +0 -7
package/core/http/pipeline.js
CHANGED
|
@@ -6,34 +6,89 @@
|
|
|
6
6
|
* @module core/http/pipeline
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const fs = require('fs');
|
|
9
10
|
const path = require('path');
|
|
10
11
|
const { ZodError } = require('zod');
|
|
11
12
|
const { translateValidationIssues, getTranslatorForRequest } = require('../../src/api/validation-i18n');
|
|
13
|
+
const { runWithContext } = require('../context/request');
|
|
14
|
+
const { detectLocale, loadI18n, createTranslator, resolvePageAssets, applyPageAssetsToTemplateData } = require('../../src/file-router');
|
|
15
|
+
const { createHelpers, getAssetManager } = require('../../src/helpers');
|
|
16
|
+
const { sendEarlyHints } = require('../early-hints');
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
19
|
+
* Validates request input against compiled schema
|
|
20
|
+
* @param {Object} req - Request object
|
|
21
|
+
* @param {Object} compiledSchema - Compiled Zod schema object
|
|
22
|
+
*/
|
|
23
|
+
function validateRequestInput(req, compiledSchema) {
|
|
24
|
+
req.input = {
|
|
25
|
+
body: undefined,
|
|
26
|
+
params: undefined,
|
|
27
|
+
query: undefined,
|
|
28
|
+
headers: undefined,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (!compiledSchema) return;
|
|
32
|
+
|
|
33
|
+
if (compiledSchema.body && typeof compiledSchema.body.parse === 'function') {
|
|
34
|
+
req.input.body = compiledSchema.body.parse(req.body);
|
|
35
|
+
} else {
|
|
36
|
+
req.input.body = req.body;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (compiledSchema.params && typeof compiledSchema.params.parse === 'function') {
|
|
40
|
+
req.input.params = compiledSchema.params.parse(req.params);
|
|
41
|
+
} else {
|
|
42
|
+
req.input.params = req.params;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (compiledSchema.query && typeof compiledSchema.query.parse === 'function') {
|
|
46
|
+
req.input.query = compiledSchema.query.parse(req.query);
|
|
47
|
+
} else {
|
|
48
|
+
req.input.query = req.query;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (compiledSchema.headers && typeof compiledSchema.headers.parse === 'function') {
|
|
52
|
+
req.input.headers = compiledSchema.headers.parse(req.headers);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Runs a sequence of middleware functions.
|
|
58
|
+
* Supports Express-style (req, res, next), 2-arg modern async (req, res),
|
|
59
|
+
* error propagation, and response termination detection.
|
|
60
|
+
*
|
|
16
61
|
* @param {Function[]} middlewares
|
|
17
62
|
* @param {Object} req
|
|
18
63
|
* @param {Object} res
|
|
64
|
+
* @returns {Promise<void>}
|
|
19
65
|
*/
|
|
20
66
|
async function runMiddlewares(middlewares, req, res) {
|
|
21
|
-
if (!Array.isArray(middlewares)) return;
|
|
67
|
+
if (!Array.isArray(middlewares) || middlewares.length === 0) return;
|
|
22
68
|
|
|
23
69
|
for (const mw of middlewares) {
|
|
24
70
|
if (typeof mw !== 'function') continue;
|
|
71
|
+
|
|
72
|
+
// Fast-path: if response already finished/flushed, stop executing chain
|
|
73
|
+
if (res && (res.headersSent || res.writableEnded || res.finished)) return;
|
|
74
|
+
|
|
25
75
|
await new Promise((resolve, reject) => {
|
|
26
76
|
let settled = false;
|
|
77
|
+
const prevEnd = typeof res?.end === 'function' ? res.end : null;
|
|
78
|
+
|
|
27
79
|
const done = (err) => {
|
|
28
80
|
if (settled) return;
|
|
29
81
|
settled = true;
|
|
82
|
+
if (res && prevEnd && res.end !== prevEnd) {
|
|
83
|
+
res.end = prevEnd;
|
|
84
|
+
}
|
|
30
85
|
if (err) reject(err);
|
|
31
86
|
else resolve();
|
|
32
87
|
};
|
|
33
88
|
|
|
34
|
-
|
|
35
|
-
if (res && typeof res.end === 'function') {
|
|
89
|
+
if (prevEnd) {
|
|
36
90
|
res.end = function (...args) {
|
|
91
|
+
if (res && prevEnd) res.end = prevEnd;
|
|
37
92
|
const ret = prevEnd.apply(this, args);
|
|
38
93
|
done();
|
|
39
94
|
return ret;
|
|
@@ -41,33 +96,63 @@ async function runMiddlewares(middlewares, req, res) {
|
|
|
41
96
|
}
|
|
42
97
|
|
|
43
98
|
try {
|
|
44
|
-
mw(req, res, (err) => {
|
|
99
|
+
const ret = mw(req, res, (err) => {
|
|
45
100
|
done(err);
|
|
46
101
|
});
|
|
102
|
+
|
|
103
|
+
if (ret && typeof ret.then === 'function') {
|
|
104
|
+
ret.then(
|
|
105
|
+
() => {
|
|
106
|
+
// Auto-advance for 2-arg async middlewares or when response was sent
|
|
107
|
+
if (mw.length < 3 || (res && (res.headersSent || res.writableEnded || res.finished))) {
|
|
108
|
+
done();
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
(asyncErr) => {
|
|
112
|
+
// Asynchronous exception in async middleware caught and forwarded
|
|
113
|
+
done(asyncErr);
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
} else if (mw.length < 3 || (res && (res.headersSent || res.writableEnded || res.finished))) {
|
|
117
|
+
done();
|
|
118
|
+
}
|
|
47
119
|
} catch (syncErr) {
|
|
48
120
|
done(syncErr);
|
|
49
121
|
}
|
|
50
122
|
});
|
|
51
|
-
|
|
123
|
+
|
|
124
|
+
if (res && (res.headersSent || res.writableEnded || res.finished)) return;
|
|
52
125
|
}
|
|
53
126
|
}
|
|
54
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Creates a reusable pipeline runner function for a middleware array
|
|
130
|
+
* @param {Function[]} [middlewares=[]]
|
|
131
|
+
* @returns {(req: any, res: any) => Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
function createPipelineRunner(middlewares = []) {
|
|
134
|
+
const mws = Array.isArray(middlewares) ? middlewares.slice() : [];
|
|
135
|
+
return function pipelineRunner(req, res) {
|
|
136
|
+
return runMiddlewares(mws, req, res);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
55
140
|
/**
|
|
56
141
|
* Decorates an error with route execution metadata
|
|
57
142
|
* @param {Error} err
|
|
58
|
-
* @param {Object} descriptor
|
|
59
|
-
* @param {Object} req
|
|
143
|
+
* @param {Object} [descriptor]
|
|
144
|
+
* @param {Object} [req]
|
|
60
145
|
* @returns {Error}
|
|
61
146
|
*/
|
|
62
|
-
function enrichRouteError(err, descriptor, req) {
|
|
147
|
+
function enrichRouteError(err, descriptor = {}, req = {}) {
|
|
63
148
|
if (err && typeof err === 'object') {
|
|
64
|
-
if (!err.route) err.route = descriptor.path;
|
|
65
|
-
if (!err.method) err.method = descriptor
|
|
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
|
|
69
|
-
if (!err.phase) err.phase = descriptor
|
|
70
|
-
if (!err.requestId) err.requestId = req && (req.id || (req.headers && req.headers['x-request-id'])) || null;
|
|
149
|
+
if (!err.route && descriptor?.path) err.route = descriptor.path;
|
|
150
|
+
if (!err.method) err.method = descriptor?.method ? descriptor.method.toUpperCase() : (req?.method || 'GET');
|
|
151
|
+
if (!err.source && (descriptor?.source || descriptor?.file)) err.source = descriptor.source || descriptor.file;
|
|
152
|
+
if (!err.file && descriptor?.file) err.file = descriptor.file;
|
|
153
|
+
if (!err.module && descriptor?.module) err.module = descriptor.module;
|
|
154
|
+
if (!err.phase) err.phase = descriptor?.type === 'page' ? 'page' : 'handler';
|
|
155
|
+
if (!err.requestId) err.requestId = (req && (req.id || (req.headers && req.headers['x-request-id']))) || null;
|
|
71
156
|
}
|
|
72
157
|
return err;
|
|
73
158
|
}
|
|
@@ -87,7 +172,7 @@ function enrichRouteError(err, descriptor, req) {
|
|
|
87
172
|
*/
|
|
88
173
|
async function executeApiPipeline(params) {
|
|
89
174
|
const {
|
|
90
|
-
descriptor,
|
|
175
|
+
descriptor = {},
|
|
91
176
|
context = {},
|
|
92
177
|
apiDef,
|
|
93
178
|
handlerFn,
|
|
@@ -97,159 +182,171 @@ async function executeApiPipeline(params) {
|
|
|
97
182
|
res,
|
|
98
183
|
} = params;
|
|
99
184
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
185
|
+
try {
|
|
186
|
+
const appOptions = context.options || {};
|
|
187
|
+
const isDev = context.isDev !== undefined
|
|
188
|
+
? context.isDev
|
|
189
|
+
: (appOptions.isDev !== undefined ? appOptions.isDev : process.env.NODE_ENV === 'development');
|
|
190
|
+
const db = context.db || req.db;
|
|
191
|
+
const serviceRegistry = context.serviceRegistry;
|
|
192
|
+
|
|
193
|
+
// 1. Injections
|
|
194
|
+
if (db && !req.db) req.db = db;
|
|
195
|
+
if (!req.service && serviceRegistry) {
|
|
196
|
+
const getCallContext = (explicitCtx) => {
|
|
197
|
+
if (explicitCtx) return explicitCtx;
|
|
198
|
+
const ctx = req.context ? { ...req.context } : { req, res, db: req.db || db };
|
|
199
|
+
if (!ctx.req) ctx.req = req;
|
|
200
|
+
if (!ctx.res) ctx.res = res;
|
|
201
|
+
if (!ctx.db) ctx.db = req.db || db;
|
|
202
|
+
if (req.user !== undefined) ctx.user = req.user;
|
|
203
|
+
if (req.auth !== undefined) ctx.auth = req.auth;
|
|
204
|
+
if (req.session !== undefined) ctx.session = req.session;
|
|
205
|
+
return ctx;
|
|
206
|
+
};
|
|
207
|
+
req.service = (name, input, opts) => {
|
|
208
|
+
return serviceRegistry.call(name, input, getCallContext(opts?.ctx), opts);
|
|
209
|
+
};
|
|
210
|
+
req.service.call = req.service;
|
|
211
|
+
req.service.execute = req.service;
|
|
212
|
+
req.service.invalidate = (name, input, ctx) =>
|
|
213
|
+
serviceRegistry.invalidate(name, input, getCallContext(ctx));
|
|
214
|
+
req.service.clearCache = (name) =>
|
|
215
|
+
serviceRegistry.clearCache(name);
|
|
216
|
+
req.service.invalidateByTag = (tag, serviceName) =>
|
|
217
|
+
serviceRegistry.invalidateByTag(tag, serviceName);
|
|
218
|
+
req.service.invalidateByTags = (tags, serviceName) =>
|
|
219
|
+
serviceRegistry.invalidateByTags(tags, serviceName);
|
|
220
|
+
req.service.invalidateTag = req.service.invalidateByTag;
|
|
221
|
+
req.service.invalidateTags = req.service.invalidateByTags;
|
|
222
|
+
}
|
|
137
223
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return res.status(400).json({
|
|
155
|
-
error: 'Validation Error',
|
|
156
|
-
issues: translatedIssues,
|
|
224
|
+
// 2. Schema validation
|
|
225
|
+
const pagesDir = context.pagesDir || appOptions.pagesDir || 'pages';
|
|
226
|
+
const routeDir = descriptor._routeDir !== undefined
|
|
227
|
+
? descriptor._routeDir
|
|
228
|
+
: (descriptor._routeDir = descriptor.file ? path.dirname(descriptor.file) : '');
|
|
229
|
+
|
|
230
|
+
if (compiledSchema) {
|
|
231
|
+
try {
|
|
232
|
+
validateRequestInput(req, compiledSchema);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
if (err instanceof ZodError || err.name === 'ZodError' || Array.isArray(err?.issues)) {
|
|
235
|
+
const translatedIssues = translateValidationIssues(err.issues, {
|
|
236
|
+
req,
|
|
237
|
+
pagesDir,
|
|
238
|
+
routeDir,
|
|
239
|
+
isDev,
|
|
157
240
|
});
|
|
241
|
+
if (res && typeof res.status === 'function' && typeof res.json === 'function') {
|
|
242
|
+
return res.status(400).json({
|
|
243
|
+
error: 'Validation Error',
|
|
244
|
+
issues: translatedIssues,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const validationErr = new Error('Validation Error');
|
|
248
|
+
validationErr.status = 400;
|
|
249
|
+
validationErr.issues = translatedIssues;
|
|
250
|
+
throw validationErr;
|
|
158
251
|
}
|
|
159
|
-
|
|
160
|
-
validationErr.status = 400;
|
|
161
|
-
validationErr.issues = translatedIssues;
|
|
162
|
-
throw validationErr;
|
|
252
|
+
throw err;
|
|
163
253
|
}
|
|
164
|
-
|
|
254
|
+
} else {
|
|
255
|
+
req.input = {
|
|
256
|
+
body: req.body,
|
|
257
|
+
params: req.params,
|
|
258
|
+
query: req.query,
|
|
259
|
+
};
|
|
165
260
|
}
|
|
166
|
-
} else {
|
|
167
|
-
req.input = {
|
|
168
|
-
body: req.body,
|
|
169
|
-
params: req.params,
|
|
170
|
-
query: req.query,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
261
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
262
|
+
// 2.5 Feature Flag Guard
|
|
263
|
+
if (apiDef && apiDef.flag) {
|
|
264
|
+
const flagKey = typeof apiDef.flag === 'string' ? apiDef.flag : apiDef.flag.key;
|
|
265
|
+
const isEnabled = typeof req.hasFeature === 'function'
|
|
266
|
+
? req.hasFeature(flagKey)
|
|
267
|
+
: ((context.app?.flags || context.flags)?.isEnabled(flagKey, req) ?? false);
|
|
268
|
+
|
|
269
|
+
if (!isEnabled) {
|
|
270
|
+
const statusCode = (typeof apiDef.flag === 'object' && apiDef.flag.statusCode) || 404;
|
|
271
|
+
const message = (typeof apiDef.flag === 'object' && apiDef.flag.message) || (statusCode === 403 ? 'Forbidden' : 'Not Found');
|
|
272
|
+
|
|
273
|
+
if (res && typeof res.status === 'function') {
|
|
274
|
+
return res.status(statusCode).json({
|
|
275
|
+
error: statusCode === 403 ? 'Forbidden' : 'Not Found',
|
|
276
|
+
message,
|
|
277
|
+
feature: flagKey,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const notFound = new Error(message);
|
|
281
|
+
notFound.status = statusCode;
|
|
282
|
+
notFound.statusCode = statusCode;
|
|
283
|
+
throw notFound;
|
|
191
284
|
}
|
|
192
|
-
const notFound = new Error(message);
|
|
193
|
-
notFound.status = statusCode;
|
|
194
|
-
notFound.statusCode = statusCode;
|
|
195
|
-
throw notFound;
|
|
196
285
|
}
|
|
197
|
-
}
|
|
198
286
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
287
|
+
// 3. Build Context
|
|
288
|
+
const tr = getTranslatorForRequest(req, { pagesDir, routeDir, isDev });
|
|
289
|
+
|
|
290
|
+
const apiCtx = {
|
|
291
|
+
req,
|
|
292
|
+
res,
|
|
293
|
+
params: req.params || {},
|
|
294
|
+
query: req.query || {},
|
|
295
|
+
body: req.body || {},
|
|
296
|
+
input: req.input,
|
|
297
|
+
locale: tr.locale,
|
|
298
|
+
t: tr.t,
|
|
299
|
+
service: req.service
|
|
300
|
+
? req.service
|
|
301
|
+
: (name, input, opts) =>
|
|
302
|
+
serviceRegistry ? serviceRegistry.call(name, input, req.context || { req, res, db, user: req.user, session: req.session }, opts) : null,
|
|
303
|
+
module: descriptor.module || null,
|
|
304
|
+
config: appOptions,
|
|
305
|
+
logger: req.logger || console,
|
|
306
|
+
db,
|
|
307
|
+
user: req.user,
|
|
308
|
+
auth: req.auth,
|
|
309
|
+
session: req.session,
|
|
310
|
+
queue: req.queue || context.queue || null,
|
|
311
|
+
cache: req.cache || context.cache || null,
|
|
312
|
+
flags: req.flags || null,
|
|
313
|
+
hasFeature: typeof req.hasFeature === 'function' ? (k, c) => req.hasFeature(k, c) : () => false,
|
|
314
|
+
getVariant: typeof req.getVariant === 'function' ? (k, c) => req.getVariant(k, c) : () => null,
|
|
315
|
+
sse: (sourceOrOptions, maybeOptions) => (res && typeof res.sse === 'function' ? res.sse(sourceOrOptions, maybeOptions) : null),
|
|
316
|
+
streamText: (sourceOrCallback, maybeOptions) => (res && typeof res.streamText === 'function' ? res.streamText(sourceOrCallback, maybeOptions) : null),
|
|
317
|
+
streamJson: (sourceOrCallback, maybeOptions) => (res && typeof res.streamJson === 'function' ? res.streamJson(sourceOrCallback, maybeOptions) : null),
|
|
318
|
+
};
|
|
204
319
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
320
|
+
req.ctx = apiCtx;
|
|
321
|
+
if (res) {
|
|
322
|
+
res.service = apiCtx.service;
|
|
323
|
+
res.input = apiCtx.input;
|
|
324
|
+
res.module = apiCtx.module;
|
|
325
|
+
}
|
|
209
326
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
input: req.input,
|
|
217
|
-
locale: tr.locale,
|
|
218
|
-
t: tr.t,
|
|
219
|
-
service: req.service
|
|
220
|
-
? req.service
|
|
221
|
-
: (name, input, opts) =>
|
|
222
|
-
serviceRegistry ? serviceRegistry.call(name, input, req.context || { req, res, db, user: req.user, session: req.session }, opts) : null,
|
|
223
|
-
module: descriptor.module || null,
|
|
224
|
-
config: appOptions,
|
|
225
|
-
logger: console,
|
|
226
|
-
db,
|
|
227
|
-
flags: req.flags || null,
|
|
228
|
-
hasFeature: typeof req.hasFeature === 'function' ? (k, c) => req.hasFeature(k, c) : () => false,
|
|
229
|
-
getVariant: typeof req.getVariant === 'function' ? (k, c) => req.getVariant(k, c) : () => null,
|
|
230
|
-
sse: (sourceOrOptions, maybeOptions) => (res && typeof res.sse === 'function' ? res.sse(sourceOrOptions, maybeOptions) : null),
|
|
231
|
-
streamText: (sourceOrCallback, maybeOptions) => (res && typeof res.streamText === 'function' ? res.streamText(sourceOrCallback, maybeOptions) : null),
|
|
232
|
-
streamJson: (sourceOrCallback, maybeOptions) => (res && typeof res.streamJson === 'function' ? res.streamJson(sourceOrCallback, maybeOptions) : null),
|
|
233
|
-
};
|
|
327
|
+
// 4. Execute within Ambient Request Context
|
|
328
|
+
return await runWithContext(apiCtx, async () => {
|
|
329
|
+
if (middlewares.length > 0) {
|
|
330
|
+
await runMiddlewares(middlewares, req, res);
|
|
331
|
+
if (res && (res.headersSent || res.writableEnded || res.finished)) return;
|
|
332
|
+
}
|
|
234
333
|
|
|
235
|
-
|
|
236
|
-
if (res) {
|
|
237
|
-
res.service = apiCtx.service;
|
|
238
|
-
res.input = apiCtx.input;
|
|
239
|
-
res.module = apiCtx.module;
|
|
240
|
-
}
|
|
334
|
+
const result = await handlerFn(req, res, apiCtx);
|
|
241
335
|
|
|
242
|
-
|
|
336
|
+
// 5. Auto-respond if handler returned a value and response not yet sent
|
|
337
|
+
if (res && !res.headersSent && !res.writableEnded && !res.finished) {
|
|
338
|
+
if (result !== undefined && typeof res.json === 'function') {
|
|
339
|
+
res.json(result);
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
243
343
|
|
|
244
|
-
// 5. Auto-respond if handler returned a value and response not yet sent
|
|
245
|
-
if (res && !res.headersSent) {
|
|
246
|
-
if (result !== undefined && typeof res.json === 'function') {
|
|
247
|
-
res.json(result);
|
|
248
344
|
return result;
|
|
249
|
-
}
|
|
345
|
+
});
|
|
346
|
+
} catch (err) {
|
|
347
|
+
enrichRouteError(err, descriptor, req);
|
|
348
|
+
throw err;
|
|
250
349
|
}
|
|
251
|
-
|
|
252
|
-
return result;
|
|
253
350
|
}
|
|
254
351
|
|
|
255
352
|
/**
|
|
@@ -258,7 +355,7 @@ async function executeApiPipeline(params) {
|
|
|
258
355
|
*/
|
|
259
356
|
async function executePagePipeline(params) {
|
|
260
357
|
const {
|
|
261
|
-
descriptor,
|
|
358
|
+
descriptor = {},
|
|
262
359
|
context = {},
|
|
263
360
|
pageDef,
|
|
264
361
|
middlewares = [],
|
|
@@ -269,251 +366,305 @@ async function executePagePipeline(params) {
|
|
|
269
366
|
hasNjkTemplate,
|
|
270
367
|
} = params;
|
|
271
368
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
// 2. Middlewares
|
|
301
|
-
if (middlewares.length > 0) {
|
|
302
|
-
await runMiddlewares(middlewares, req, res);
|
|
303
|
-
if (res && res.headersSent) return;
|
|
304
|
-
}
|
|
369
|
+
try {
|
|
370
|
+
const appOptions = context.options || {};
|
|
371
|
+
const isDev = context.isDev !== undefined
|
|
372
|
+
? context.isDev
|
|
373
|
+
: (appOptions.isDev !== undefined ? appOptions.isDev : process.env.NODE_ENV === 'development');
|
|
374
|
+
const db = context.db || req.db;
|
|
375
|
+
const serviceRegistry = context.serviceRegistry;
|
|
376
|
+
const pagesDir = context.pagesDir || appOptions.pagesDir || 'pages';
|
|
377
|
+
|
|
378
|
+
// 1. Injections
|
|
379
|
+
if (db && !req.db) req.db = db;
|
|
380
|
+
if (!req.service && serviceRegistry) {
|
|
381
|
+
const baseContext = req.context || { req, res, db, user: req.user, session: req.session };
|
|
382
|
+
req.service = (name, input, opts) =>
|
|
383
|
+
serviceRegistry.call(name, input, baseContext, opts);
|
|
384
|
+
req.service.call = req.service;
|
|
385
|
+
req.service.execute = req.service;
|
|
386
|
+
req.service.invalidate = (name, input, ctx) =>
|
|
387
|
+
serviceRegistry.invalidate(name, input, ctx || baseContext);
|
|
388
|
+
req.service.clearCache = (name) =>
|
|
389
|
+
serviceRegistry.clearCache(name);
|
|
390
|
+
req.service.invalidateByTag = (tag, serviceName) =>
|
|
391
|
+
serviceRegistry.invalidateByTag(tag, serviceName);
|
|
392
|
+
req.service.invalidateByTags = (tags, serviceName) =>
|
|
393
|
+
serviceRegistry.invalidateByTags(tags, serviceName);
|
|
394
|
+
req.service.invalidateTag = req.service.invalidateByTag;
|
|
395
|
+
req.service.invalidateTags = req.service.invalidateByTags;
|
|
396
|
+
}
|
|
305
397
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
398
|
+
// 2. Build Page Context
|
|
399
|
+
const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 7331}`;
|
|
400
|
+
const locale = detectLocale(req);
|
|
401
|
+
const fsy = createHelpers({ req, res, baseUrl, locale });
|
|
402
|
+
|
|
403
|
+
const defaultLocale = process.env.DEFAULT_LOCALE || 'en';
|
|
404
|
+
let t;
|
|
405
|
+
const translatorCache = context.translatorCache;
|
|
406
|
+
if (!isDev && translatorCache) {
|
|
407
|
+
const transKey = `${pagesDir}::::${locale}::${defaultLocale}`;
|
|
408
|
+
t = translatorCache.get(transKey);
|
|
409
|
+
if (!t) {
|
|
410
|
+
const translations = loadI18n(pagesDir, '', locale, false);
|
|
411
|
+
const fallbackTranslations = locale !== defaultLocale ? loadI18n(pagesDir, '', defaultLocale, false) : undefined;
|
|
412
|
+
t = createTranslator(translations, {
|
|
413
|
+
locale,
|
|
414
|
+
fallbackTranslations,
|
|
415
|
+
fallbackLocale: defaultLocale,
|
|
416
|
+
});
|
|
417
|
+
translatorCache.set(transKey, t);
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
const translations = loadI18n(pagesDir, '', locale, isDev);
|
|
421
|
+
const fallbackTranslations = locale !== defaultLocale ? loadI18n(pagesDir, '', defaultLocale, isDev) : undefined;
|
|
324
422
|
t = createTranslator(translations, {
|
|
325
423
|
locale,
|
|
326
424
|
fallbackTranslations,
|
|
327
425
|
fallbackLocale: defaultLocale,
|
|
328
426
|
});
|
|
329
|
-
translatorCache.set(transKey, t);
|
|
330
427
|
}
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
428
|
+
|
|
429
|
+
const pageCtx = {
|
|
430
|
+
req,
|
|
431
|
+
res,
|
|
432
|
+
params: req.params || {},
|
|
433
|
+
query: req.query || {},
|
|
434
|
+
service: req.service
|
|
435
|
+
? req.service
|
|
436
|
+
: (name, input, opts) =>
|
|
437
|
+
serviceRegistry ? serviceRegistry.call(name, input, req.context || { req, res, db, user: req.user, session: req.session }, opts) : null,
|
|
438
|
+
module: descriptor.module || null,
|
|
439
|
+
config: appOptions,
|
|
440
|
+
logger: req.logger || console,
|
|
441
|
+
redirect: (url, status = 302) => {
|
|
442
|
+
if (res && typeof res.redirect === 'function') {
|
|
443
|
+
res.redirect(status, url);
|
|
444
|
+
}
|
|
445
|
+
return { __redirected: true };
|
|
446
|
+
},
|
|
447
|
+
error: (status = 500, message = 'Page Error') => {
|
|
448
|
+
const err = new Error(message);
|
|
449
|
+
err.status = status;
|
|
450
|
+
throw err;
|
|
451
|
+
},
|
|
452
|
+
earlyHints: (links, cb) => sendEarlyHints(res, links, cb),
|
|
453
|
+
renderStream: (tpl, data = {}, opts = {}) => {
|
|
454
|
+
if (res && typeof res.renderStream === 'function') {
|
|
455
|
+
return res.renderStream(tpl, data, opts);
|
|
456
|
+
}
|
|
457
|
+
const { renderStream } = require('../ssr/stream');
|
|
458
|
+
return renderStream(res, tpl, data, { env: nunjucks, ...opts });
|
|
459
|
+
},
|
|
460
|
+
streamText: (sourceOrCallback, maybeOptions) => {
|
|
461
|
+
if (res && typeof res.streamText === 'function') {
|
|
462
|
+
return res.streamText(sourceOrCallback, maybeOptions);
|
|
463
|
+
}
|
|
464
|
+
const { streamText } = require('../stream/text-stream');
|
|
465
|
+
return streamText(req, res, sourceOrCallback, maybeOptions);
|
|
466
|
+
},
|
|
467
|
+
streamJson: (sourceOrCallback, maybeOptions) => {
|
|
468
|
+
if (res && typeof res.streamJson === 'function') {
|
|
469
|
+
return res.streamJson(sourceOrCallback, maybeOptions);
|
|
470
|
+
}
|
|
471
|
+
const { streamJson } = require('../stream/text-stream');
|
|
472
|
+
return streamJson(req, res, sourceOrCallback, maybeOptions);
|
|
473
|
+
},
|
|
474
|
+
fsy,
|
|
335
475
|
locale,
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
476
|
+
t,
|
|
477
|
+
db,
|
|
478
|
+
user: req.user,
|
|
479
|
+
auth: req.auth,
|
|
480
|
+
session: req.session,
|
|
481
|
+
queue: req.queue || context.queue || null,
|
|
482
|
+
cache: req.cache || context.cache || null,
|
|
483
|
+
};
|
|
340
484
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
serviceRegistry ? serviceRegistry.call(name, input, req.context || { req, res, db, user: req.user, session: req.session }, opts) : null,
|
|
350
|
-
module: descriptor.module || null,
|
|
351
|
-
config: appOptions,
|
|
352
|
-
logger: console,
|
|
353
|
-
redirect: (url, status = 302) => {
|
|
354
|
-
if (res && typeof res.redirect === 'function') {
|
|
355
|
-
res.redirect(status, url);
|
|
485
|
+
req.ctx = pageCtx;
|
|
486
|
+
|
|
487
|
+
// 3. Execute within Ambient Request Context
|
|
488
|
+
return await runWithContext(pageCtx, async () => {
|
|
489
|
+
// 3.1. Middlewares
|
|
490
|
+
if (middlewares.length > 0) {
|
|
491
|
+
await runMiddlewares(middlewares, req, res);
|
|
492
|
+
if (res && (res.headersSent || res.writableEnded || res.finished)) return;
|
|
356
493
|
}
|
|
357
|
-
return { __redirected: true };
|
|
358
|
-
},
|
|
359
|
-
error: (status = 500, message = 'Page Error') => {
|
|
360
|
-
const err = new Error(message);
|
|
361
|
-
err.status = status;
|
|
362
|
-
throw err;
|
|
363
|
-
},
|
|
364
|
-
earlyHints: (links, cb) => sendEarlyHints(res, links, cb),
|
|
365
|
-
fsy,
|
|
366
|
-
locale,
|
|
367
|
-
t,
|
|
368
|
-
db,
|
|
369
|
-
};
|
|
370
494
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
495
|
+
// 4. Early Hints
|
|
496
|
+
const earlyHintsOpt = pageDef.earlyHints !== undefined
|
|
497
|
+
? pageDef.earlyHints
|
|
498
|
+
: (descriptor.earlyHints !== undefined
|
|
499
|
+
? descriptor.earlyHints
|
|
500
|
+
: (appOptions && (appOptions.earlyHints ?? appOptions.server?.earlyHints)));
|
|
501
|
+
|
|
502
|
+
if (earlyHintsOpt && earlyHintsOpt !== false && res && !res.headersSent && !res.writableEnded && !res.finished) {
|
|
503
|
+
const auto = typeof earlyHintsOpt === 'object' ? (earlyHintsOpt.auto !== false) : Boolean(earlyHintsOpt);
|
|
504
|
+
const linksToSend = [];
|
|
505
|
+
|
|
506
|
+
if (Array.isArray(earlyHintsOpt)) {
|
|
507
|
+
linksToSend.push(...earlyHintsOpt);
|
|
508
|
+
} else if (typeof earlyHintsOpt === 'object' && earlyHintsOpt.links) {
|
|
509
|
+
const customLinks = typeof earlyHintsOpt.links === 'function' ? earlyHintsOpt.links(req) : earlyHintsOpt.links;
|
|
510
|
+
if (Array.isArray(customLinks)) linksToSend.push(...customLinks);
|
|
511
|
+
else if (customLinks) linksToSend.push(customLinks);
|
|
512
|
+
}
|
|
389
513
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
514
|
+
if (auto) {
|
|
515
|
+
const assetManager = fsy?.assetManager || getAssetManager();
|
|
516
|
+
if (assetManager && typeof assetManager.getRouteAssetCandidates === 'function') {
|
|
517
|
+
const candidates = assetManager.getRouteAssetCandidates(
|
|
518
|
+
descriptor.source || descriptor.file,
|
|
519
|
+
descriptor.path
|
|
520
|
+
);
|
|
521
|
+
if (Array.isArray(candidates)) {
|
|
522
|
+
for (const cand of candidates) {
|
|
523
|
+
if (typeof assetManager.hasPageCss === 'function' && assetManager.hasPageCss(cand)) {
|
|
524
|
+
linksToSend.push(assetManager.pageCssUrl(cand));
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
for (const cand of candidates) {
|
|
529
|
+
if (typeof assetManager.hasPageJs === 'function' && assetManager.hasPageJs(cand)) {
|
|
530
|
+
linksToSend.push(assetManager.pageJsUrl(cand));
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
408
534
|
}
|
|
409
535
|
}
|
|
410
536
|
}
|
|
537
|
+
|
|
538
|
+
if (linksToSend.length > 0) {
|
|
539
|
+
sendEarlyHints(res, linksToSend);
|
|
540
|
+
}
|
|
411
541
|
}
|
|
412
|
-
}
|
|
413
542
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
543
|
+
// 5. Run load() lifecycle
|
|
544
|
+
let loadedData = {};
|
|
545
|
+
if (typeof pageDef.load === 'function') {
|
|
546
|
+
loadedData = (await pageDef.load(pageCtx)) || {};
|
|
547
|
+
if (loadedData.__redirected || (res && (res.headersSent || res.writableEnded || res.finished))) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
418
551
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
552
|
+
// 6. Run head() lifecycle
|
|
553
|
+
let headData = {};
|
|
554
|
+
if (typeof pageDef.head === 'function') {
|
|
555
|
+
headData = (await pageDef.head(loadedData, pageCtx)) || {};
|
|
556
|
+
}
|
|
427
557
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
558
|
+
// 7. Check for companion Nunjucks template
|
|
559
|
+
const templateExists = hasNjkTemplate !== undefined && hasNjkTemplate !== null
|
|
560
|
+
? hasNjkTemplate
|
|
561
|
+
: (njkCandidate ? fs.existsSync(njkCandidate) : false);
|
|
562
|
+
|
|
563
|
+
if (templateExists && nunjucks) {
|
|
564
|
+
let pageHead = headData;
|
|
565
|
+
let pageAssetsActive = false;
|
|
566
|
+
const pageAssetsOpt = context.pageAssets || (appOptions && appOptions.pageAssets);
|
|
567
|
+
if (pageAssetsOpt) {
|
|
568
|
+
const pageAssetsResolved = resolvePageAssets(pageAssetsOpt);
|
|
569
|
+
if (pageAssetsResolved.enabled) {
|
|
570
|
+
const pageAssetBundle = applyPageAssetsToTemplateData(
|
|
571
|
+
pageAssetsResolved,
|
|
572
|
+
loadedData,
|
|
573
|
+
{
|
|
574
|
+
file: descriptor.source || descriptor.file,
|
|
575
|
+
routePath: descriptor.path,
|
|
576
|
+
},
|
|
577
|
+
fsy?.assetManager || getAssetManager()
|
|
578
|
+
);
|
|
579
|
+
loadedData = pageAssetBundle.data;
|
|
580
|
+
pageHead = {
|
|
581
|
+
...headData,
|
|
582
|
+
stylesheets: [...(headData.stylesheets || []), ...(pageAssetBundle.pageHead?.stylesheets || [])],
|
|
583
|
+
scripts: [...(headData.scripts || []), ...(pageAssetBundle.pageHead?.scripts || [])],
|
|
584
|
+
};
|
|
585
|
+
pageAssetsActive = true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
433
588
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
if (pageAssetsOpt) {
|
|
445
|
-
const pageAssetsResolved = resolvePageAssets(pageAssetsOpt);
|
|
446
|
-
if (pageAssetsResolved.enabled) {
|
|
447
|
-
const pageAssetBundle = applyPageAssetsToTemplateData(
|
|
448
|
-
pageAssetsResolved,
|
|
449
|
-
loadedData,
|
|
450
|
-
{
|
|
451
|
-
file: descriptor.source || descriptor.file,
|
|
452
|
-
routePath: descriptor.path,
|
|
453
|
-
},
|
|
454
|
-
fsy?.assetManager || getAssetManager()
|
|
455
|
-
);
|
|
456
|
-
loadedData = pageAssetBundle.data;
|
|
457
|
-
pageHead = {
|
|
458
|
-
...headData,
|
|
459
|
-
stylesheets: [...(headData.stylesheets || []), ...(pageAssetBundle.pageHead?.stylesheets || [])],
|
|
460
|
-
scripts: [...(headData.scripts || []), ...(pageAssetBundle.pageHead?.scripts || [])],
|
|
589
|
+
const templateData = {
|
|
590
|
+
...loadedData,
|
|
591
|
+
head: pageHead,
|
|
592
|
+
pageHead,
|
|
593
|
+
pageAssets: pageAssetsActive || Boolean(loadedData.pageAssets),
|
|
594
|
+
fsy,
|
|
595
|
+
locale,
|
|
596
|
+
t,
|
|
597
|
+
params: req.params,
|
|
598
|
+
query: req.query,
|
|
461
599
|
};
|
|
462
|
-
pageAssetsActive = true;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
600
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
params: req.params,
|
|
475
|
-
query: req.query,
|
|
476
|
-
};
|
|
601
|
+
const isStreaming = Boolean(
|
|
602
|
+
pageDef.stream ||
|
|
603
|
+
descriptor.stream ||
|
|
604
|
+
loadedData.__stream ||
|
|
605
|
+
loadedData.stream ||
|
|
606
|
+
loadedData.defer ||
|
|
607
|
+
loadedData.__defer
|
|
608
|
+
);
|
|
477
609
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
610
|
+
if (isStreaming && res && !res.headersSent) {
|
|
611
|
+
const { renderStream } = require('../ssr/stream');
|
|
612
|
+
const streamDefer = loadedData.defer || loadedData.__defer || pageDef.defer || {};
|
|
613
|
+
return await renderStream(res, njkCandidate, templateData, {
|
|
614
|
+
env: nunjucks,
|
|
615
|
+
defer: streamDefer,
|
|
616
|
+
status: 200,
|
|
617
|
+
loadingTemplate: pageDef.loadingTemplate || descriptor.loadingTemplate,
|
|
618
|
+
layouts: pageDef.layouts || descriptor.layouts,
|
|
619
|
+
});
|
|
484
620
|
}
|
|
485
|
-
resolve(html);
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
621
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
622
|
+
return new Promise((resolve, reject) => {
|
|
623
|
+
nunjucks.render(njkCandidate, templateData, (err, html) => {
|
|
624
|
+
if (err) return reject(err);
|
|
625
|
+
if (res && !res.headersSent && !res.writableEnded && !res.finished) {
|
|
626
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
627
|
+
res.send(html);
|
|
628
|
+
}
|
|
629
|
+
resolve(html);
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
}
|
|
499
633
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
634
|
+
// 8. Render via definePage render() method
|
|
635
|
+
if (typeof pageDef.render === 'function') {
|
|
636
|
+
const html = await pageDef.render(loadedData, pageCtx);
|
|
637
|
+
if (res && !res.headersSent && !res.writableEnded && !res.finished) {
|
|
638
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
639
|
+
return res.send(html);
|
|
640
|
+
}
|
|
641
|
+
return html;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// 9. Fallback: if json or data
|
|
645
|
+
if (typeof loadedData === 'object' && loadedData !== null) {
|
|
646
|
+
if (res && !res.headersSent && !res.writableEnded && !res.finished && typeof res.json === 'function') {
|
|
647
|
+
return res.json(loadedData);
|
|
648
|
+
}
|
|
649
|
+
return loadedData;
|
|
650
|
+
}
|
|
507
651
|
|
|
508
|
-
|
|
509
|
-
|
|
652
|
+
if (res && !res.headersSent && !res.writableEnded && !res.finished && typeof res.send === 'function') {
|
|
653
|
+
return res.send(String(loadedData));
|
|
654
|
+
}
|
|
655
|
+
return String(loadedData);
|
|
656
|
+
});
|
|
657
|
+
} catch (err) {
|
|
658
|
+
enrichRouteError(err, descriptor, req);
|
|
659
|
+
throw err;
|
|
510
660
|
}
|
|
511
|
-
return String(loadedData);
|
|
512
661
|
}
|
|
513
662
|
|
|
514
663
|
module.exports = {
|
|
664
|
+
createPipelineRunner,
|
|
515
665
|
runMiddlewares,
|
|
516
666
|
enrichRouteError,
|
|
667
|
+
validateRequestInput,
|
|
517
668
|
executeApiPipeline,
|
|
518
669
|
executePagePipeline,
|
|
519
670
|
};
|