webspresso 0.0.90 → 0.0.93
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/README.md +98 -2509
- package/bin/commands/admin-password.js +8 -2
- package/bin/commands/api.js +7 -1
- package/bin/commands/favicon-generate.js +6 -4
- package/bin/commands/mcp.js +162 -0
- package/bin/commands/new.js +7 -1
- package/bin/commands/page.js +7 -1
- package/bin/commands/seed.js +7 -1
- package/bin/commands/service.js +199 -0
- package/bin/commands/skill.js +7 -1
- package/bin/webspresso.js +4 -0
- package/core/auth/middleware.js +9 -0
- package/core/compileSchema.js +1 -5
- package/core/compression/compressible.js +63 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +310 -0
- package/core/compression/negotiate.js +165 -0
- package/core/errors/base.js +31 -0
- package/core/errors/domain.js +193 -0
- package/core/errors/http.js +133 -0
- package/core/errors/index.js +77 -0
- package/core/errors/middleware.js +162 -0
- package/core/errors/normalize.js +171 -0
- package/core/kernel/app.js +2 -1
- package/core/kernel/index.js +5 -1
- package/core/orm/cache/fingerprint.js +23 -5
- package/core/orm/complexity.js +120 -0
- package/core/orm/eager-loader.js +54 -9
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +83 -12
- package/core/orm/model.js +13 -2
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/transaction.js +39 -5
- package/core/orm/utils.js +6 -3
- package/core/queue/adapters/database.js +317 -0
- package/core/queue/adapters/memory.js +176 -0
- package/core/queue/adapters/redis.js +357 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +413 -0
- package/core/realtime/adapters/redis.js +431 -0
- package/core/realtime/adapters/socket-io.js +203 -0
- package/core/realtime/adapters/sse.js +210 -0
- package/core/realtime/adapters/websocket.js +337 -0
- package/core/realtime/auth-manager.js +140 -0
- package/core/realtime/core.js +359 -0
- package/core/realtime/emitter.js +98 -0
- package/core/realtime/errors.js +71 -0
- package/core/realtime/index.js +79 -0
- package/core/realtime/reconnect-manager.js +118 -0
- package/core/realtime/registry.js +243 -0
- package/core/shutdown/http-adapter.js +134 -0
- package/core/shutdown/index.js +12 -0
- package/core/shutdown/shutdown-manager.js +312 -0
- package/core/ssr/index.js +13 -0
- package/core/ssr/stream.js +174 -0
- package/core/validation/index.js +70 -0
- package/index.d.ts +924 -53
- package/index.js +110 -1
- package/package.json +30 -2
- package/plugins/admin-panel/api.js +37 -4
- package/plugins/admin-panel/app.js +57 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +10 -2
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +51 -27
- package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
- package/plugins/admin-panel/client/parts/08-record-list.js +37 -26
- package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
- package/plugins/admin-panel/core/admin-module.js +2 -2
- package/plugins/admin-panel/core/api-extensions.js +149 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
- package/plugins/admin-panel/index.js +155 -23
- package/plugins/admin-panel/modules/bulk-actions.js +9 -2
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- package/plugins/admin-panel/modules/dashboard.js +8 -1
- package/plugins/admin-panel/modules/menu.js +85 -0
- package/plugins/admin-panel/modules/user-management.js +5 -2
- package/plugins/admin-panel/vendor/mithril.min.js +1 -0
- package/plugins/admin-panel/vendor/quill.min.js +7 -0
- package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
- package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
- package/plugins/basic-auth/index.js +293 -0
- package/plugins/data-exchange/export-xlsx.js +28 -2
- package/plugins/data-exchange/import.js +55 -16
- package/plugins/data-exchange/index.js +19 -0
- package/plugins/data-exchange/parse-table.js +14 -5
- package/plugins/data-exchange/record-selection.js +2 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +33 -0
- package/plugins/mcp/auth.js +104 -0
- package/plugins/mcp/discovery.js +88 -0
- package/plugins/mcp/index.js +208 -0
- package/plugins/mcp/prompts.js +149 -0
- package/plugins/mcp/providers/orm.js +325 -0
- package/plugins/mcp/providers/services.js +190 -0
- package/plugins/mcp/providers/system.js +87 -0
- package/plugins/mcp/server.js +472 -0
- package/plugins/mcp/transports/sse.js +139 -0
- package/plugins/mcp/transports/stdio.js +108 -0
- package/plugins/queue/index.js +108 -0
- package/plugins/realtime/index.js +150 -0
- package/plugins/recaptcha/index.js +11 -0
- package/plugins/redirect/index.js +9 -1
- package/plugins/rest-resources/index.js +280 -11
- package/plugins/upload/index.js +16 -0
- package/src/app-context.js +46 -6
- package/src/file-router.js +366 -130
- package/src/helpers.js +20 -2
- package/src/njk-frontmatter.js +24 -14
- package/src/plugin-manager.js +124 -4
- package/src/server.js +332 -54
- package/src/services/builtins/auth.js +273 -0
- package/src/services/builtins/exchange.js +229 -0
- package/src/services/builtins/mail.js +134 -0
- package/src/services/builtins/media.js +196 -0
- package/src/services/builtins/system.js +221 -0
- package/src/services/discovery.js +141 -0
- package/src/services/executor.js +292 -0
- package/src/services/index.js +70 -0
- package/src/services/memoize.js +256 -0
- package/src/services/registry.js +290 -0
- package/src/services/validator.js +230 -0
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
package/src/server.js
CHANGED
|
@@ -17,6 +17,20 @@ const { resolveClientRuntime } = require('./client-runtime/resolve');
|
|
|
17
17
|
const { mountPages, detectLocale, loadI18n, createTranslator } = require('./file-router');
|
|
18
18
|
const { configureAssets, createHelpers, getScriptInjector } = require('./helpers');
|
|
19
19
|
const { createPluginManager } = require('./plugin-manager');
|
|
20
|
+
const { createServiceRegistry } = require('./services');
|
|
21
|
+
const { ShutdownManager, NodeHttpAdapter } = require('../core/shutdown');
|
|
22
|
+
const { createCompressionMiddleware } = require('../core/compression');
|
|
23
|
+
const {
|
|
24
|
+
WebspressoError,
|
|
25
|
+
HttpError,
|
|
26
|
+
NotFoundError,
|
|
27
|
+
RouteNotFoundError,
|
|
28
|
+
RequestAbortedError,
|
|
29
|
+
ValidationError,
|
|
30
|
+
normalizeError,
|
|
31
|
+
toErrorResponseObject,
|
|
32
|
+
preferJsonErrorResponse,
|
|
33
|
+
} = require('../core/errors');
|
|
20
34
|
|
|
21
35
|
// Async storage for tracking template render call stacks (circular extends guard)
|
|
22
36
|
const renderStackStorage = new AsyncLocalStorage();
|
|
@@ -33,7 +47,7 @@ function configureSafeNunjucks(templateDirs, options = {}) {
|
|
|
33
47
|
const origRender = env.render.bind(env);
|
|
34
48
|
const origRenderString = env.renderString.bind(env);
|
|
35
49
|
|
|
36
|
-
function wrapTemplateRoot(tmpl, name
|
|
50
|
+
function wrapTemplateRoot(tmpl, name) {
|
|
37
51
|
if (!tmpl || tmpl._safeWrapped) return;
|
|
38
52
|
tmpl._safeWrapped = true;
|
|
39
53
|
const origRoot = tmpl.rootRenderFunc;
|
|
@@ -41,21 +55,28 @@ function configureSafeNunjucks(templateDirs, options = {}) {
|
|
|
41
55
|
|
|
42
56
|
tmpl.rootRenderFunc = function(e, context, frame, runtime, renderCb) {
|
|
43
57
|
const normalizedName = String(name || tmpl.path || 'anonymous');
|
|
44
|
-
store.
|
|
58
|
+
const store = renderStackStorage.getStore();
|
|
59
|
+
if (store) {
|
|
60
|
+
store.stack.push(normalizedName);
|
|
61
|
+
}
|
|
45
62
|
let finished = false;
|
|
46
63
|
const done = (err, out) => {
|
|
47
64
|
if (!finished) {
|
|
48
65
|
finished = true;
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
if (store) {
|
|
67
|
+
const idx = store.stack.lastIndexOf(normalizedName);
|
|
68
|
+
if (idx !== -1) store.stack.splice(idx, 1);
|
|
69
|
+
}
|
|
51
70
|
}
|
|
52
71
|
renderCb(err, out);
|
|
53
72
|
};
|
|
54
73
|
try {
|
|
55
74
|
return origRoot.call(this, e, context, frame, runtime, done);
|
|
56
75
|
} catch (ex) {
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
if (store) {
|
|
77
|
+
const idx = store.stack.lastIndexOf(normalizedName);
|
|
78
|
+
if (idx !== -1) store.stack.splice(idx, 1);
|
|
79
|
+
}
|
|
59
80
|
throw ex;
|
|
60
81
|
}
|
|
61
82
|
};
|
|
@@ -83,15 +104,15 @@ function configureSafeNunjucks(templateDirs, options = {}) {
|
|
|
83
104
|
}
|
|
84
105
|
|
|
85
106
|
const wrappedCb = typeof cb === 'function' ? function(err, tmpl) {
|
|
86
|
-
if (tmpl
|
|
87
|
-
wrapTemplateRoot(tmpl, name
|
|
107
|
+
if (tmpl) {
|
|
108
|
+
wrapTemplateRoot(tmpl, name);
|
|
88
109
|
}
|
|
89
110
|
cb(err, tmpl);
|
|
90
111
|
} : undefined;
|
|
91
112
|
|
|
92
113
|
const res = origGetTemplate(name, eagerCompile, parentName, ignoreMissing, wrappedCb);
|
|
93
|
-
if (res
|
|
94
|
-
wrapTemplateRoot(res, name
|
|
114
|
+
if (res) {
|
|
115
|
+
wrapTemplateRoot(res, name);
|
|
95
116
|
}
|
|
96
117
|
return res;
|
|
97
118
|
};
|
|
@@ -153,29 +174,7 @@ function getDefaultHelmetConfig(isDev) {
|
|
|
153
174
|
};
|
|
154
175
|
}
|
|
155
176
|
|
|
156
|
-
/**
|
|
157
|
-
* Use JSON error responses for `pages/api/*` routes, XHR requests, and clients that prefer JSON.
|
|
158
|
-
* @param {import('express').Request} req
|
|
159
|
-
* @returns {boolean}
|
|
160
|
-
*/
|
|
161
|
-
function preferJsonErrorResponse(req) {
|
|
162
|
-
if (!req) return false;
|
|
163
|
-
if (req.path && req.path.startsWith('/api')) return true;
|
|
164
|
-
if (req.xhr) return true;
|
|
165
|
-
|
|
166
|
-
if (typeof req.accepts === 'function') {
|
|
167
|
-
const preferred = req.accepts(['html', 'json']);
|
|
168
|
-
if (preferred === 'json') return true;
|
|
169
|
-
if (preferred === 'html') return false;
|
|
170
|
-
return !req.accepts('html');
|
|
171
|
-
}
|
|
172
177
|
|
|
173
|
-
const accept = req.headers && req.headers.accept;
|
|
174
|
-
if (accept && accept.includes('application/json') && !accept.includes('text/html')) {
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
178
|
|
|
180
179
|
/**
|
|
181
180
|
* Shared CSS for built-in HTML error pages (viewport-safe, fluid type, dark mode)
|
|
@@ -420,9 +419,152 @@ function createApp(options = {}) {
|
|
|
420
419
|
|
|
421
420
|
const clientRuntime = resolveClientRuntime(options);
|
|
422
421
|
|
|
423
|
-
|
|
422
|
+
const shutdownConfig = {
|
|
423
|
+
enabled: true,
|
|
424
|
+
mode: 'graceful',
|
|
425
|
+
timeout: 10_000,
|
|
426
|
+
...(options.shutdown || options.server?.shutdown || {}),
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const shutdownManager = new ShutdownManager({
|
|
430
|
+
...shutdownConfig,
|
|
431
|
+
logger: logging ? console : null,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Services Layer initialization
|
|
435
|
+
const servicesDirOption = options.servicesDir ?? options.services?.dir;
|
|
436
|
+
const resolvedServicesDir = servicesDirOption
|
|
437
|
+
? path.resolve(servicesDirOption)
|
|
438
|
+
: (fs.existsSync(path.join(process.cwd(), 'services'))
|
|
439
|
+
? path.join(process.cwd(), 'services')
|
|
440
|
+
: null);
|
|
441
|
+
|
|
442
|
+
const serviceRegistry = createServiceRegistry({
|
|
443
|
+
servicesDir: resolvedServicesDir,
|
|
444
|
+
isDev,
|
|
445
|
+
logger: logging ? console : null,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Auto-register built-in auth services if database is present and service is not custom-defined
|
|
449
|
+
if (options.db) {
|
|
450
|
+
const { createAuthServices } = require('./services/builtins/auth');
|
|
451
|
+
const authServices = createAuthServices({ db: options.db });
|
|
452
|
+
for (const [name, def] of Object.entries(authServices)) {
|
|
453
|
+
if (!serviceRegistry.has(name)) {
|
|
454
|
+
serviceRegistry.register(name, def);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Auto-register built-in system services (health, cleanup, cache flush, info)
|
|
460
|
+
const { createSystemServices } = require('./services/builtins/system');
|
|
461
|
+
const systemServices = createSystemServices({
|
|
462
|
+
db: options.db ?? null,
|
|
463
|
+
serviceRegistry,
|
|
464
|
+
pluginManager,
|
|
465
|
+
});
|
|
466
|
+
for (const [name, def] of Object.entries(systemServices)) {
|
|
467
|
+
if (!serviceRegistry.has(name)) {
|
|
468
|
+
serviceRegistry.register(name, def);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
setAppContext({ db: options.db ?? null, shutdownManager, serviceRegistry });
|
|
424
473
|
|
|
425
474
|
const app = express();
|
|
475
|
+
app.serviceRegistry = serviceRegistry;
|
|
476
|
+
|
|
477
|
+
// Request context & services caller middleware
|
|
478
|
+
app.use((req, res, next) => {
|
|
479
|
+
if (options.db) {
|
|
480
|
+
req.db = options.db;
|
|
481
|
+
}
|
|
482
|
+
req.context = {
|
|
483
|
+
req,
|
|
484
|
+
res,
|
|
485
|
+
db: options.db ?? null,
|
|
486
|
+
app,
|
|
487
|
+
serviceRegistry,
|
|
488
|
+
...(req.context || {}),
|
|
489
|
+
};
|
|
490
|
+
req.service = (name, input, opts) =>
|
|
491
|
+
serviceRegistry.call(name, input, req.context, opts);
|
|
492
|
+
next();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// SSR Streaming Response Helper
|
|
496
|
+
const { renderStream } = require('../core/ssr/stream');
|
|
497
|
+
app.use((req, res, next) => {
|
|
498
|
+
res.renderStream = (templatePath, context = {}, renderOptions = {}) => {
|
|
499
|
+
return renderStream(res, templatePath, context, {
|
|
500
|
+
env: nunjucksEnv,
|
|
501
|
+
...renderOptions,
|
|
502
|
+
});
|
|
503
|
+
};
|
|
504
|
+
next();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Async handler wrapper helper for automatic promise rejection handling
|
|
508
|
+
function wrapAsync(fn) {
|
|
509
|
+
if (typeof fn !== 'function') return fn;
|
|
510
|
+
if (fn.length === 4) {
|
|
511
|
+
return function(err, req, res, next) {
|
|
512
|
+
try {
|
|
513
|
+
const ret = fn.call(this, err, req, res, next);
|
|
514
|
+
if (ret && typeof ret.catch === 'function') {
|
|
515
|
+
ret.catch(next);
|
|
516
|
+
}
|
|
517
|
+
return ret;
|
|
518
|
+
} catch (syncErr) {
|
|
519
|
+
return next(syncErr);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
return function(req, res, next) {
|
|
524
|
+
try {
|
|
525
|
+
const ret = fn.call(this, req, res, next);
|
|
526
|
+
if (ret && typeof ret.catch === 'function') {
|
|
527
|
+
ret.catch(next);
|
|
528
|
+
}
|
|
529
|
+
return ret;
|
|
530
|
+
} catch (syncErr) {
|
|
531
|
+
return next(syncErr);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Wrap routing methods on app to catch uncaught async errors
|
|
537
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all'];
|
|
538
|
+
for (const method of HTTP_METHODS) {
|
|
539
|
+
const origMethod = app[method];
|
|
540
|
+
if (typeof origMethod === 'function') {
|
|
541
|
+
app[method] = function(path, ...handlers) {
|
|
542
|
+
const wrappedHandlers = handlers.flat().map((h) => wrapAsync(h));
|
|
543
|
+
return origMethod.call(this, path, ...wrappedHandlers);
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Request draining check during graceful shutdown (zero overhead during normal operation)
|
|
549
|
+
app.use((req, res, next) => {
|
|
550
|
+
if (shutdownManager.isShuttingDown) {
|
|
551
|
+
res.set('Connection', 'close');
|
|
552
|
+
if (!res.headersSent) {
|
|
553
|
+
if (preferJsonErrorResponse(req)) {
|
|
554
|
+
return res.status(503).json({ error: 'Service Unavailable', message: 'Server is shutting down', status: 503 });
|
|
555
|
+
}
|
|
556
|
+
return res.status(503).send('Server is shutting down');
|
|
557
|
+
}
|
|
558
|
+
res.on('finish', () => {
|
|
559
|
+
try {
|
|
560
|
+
if (req.socket && !req.socket.destroyed) {
|
|
561
|
+
req.socket.end();
|
|
562
|
+
}
|
|
563
|
+
} catch (e) {}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
next();
|
|
567
|
+
});
|
|
426
568
|
|
|
427
569
|
// Security headers with Helmet
|
|
428
570
|
if (helmetConfig !== false) {
|
|
@@ -469,19 +611,46 @@ function createApp(options = {}) {
|
|
|
469
611
|
|
|
470
612
|
app.use(helmet(finalConfig));
|
|
471
613
|
}
|
|
614
|
+
|
|
615
|
+
// HTTP Response Compression
|
|
616
|
+
const compressionOpt = options.compression ?? options.server?.compression ?? false;
|
|
617
|
+
if (compressionOpt) {
|
|
618
|
+
const compressionConfig = typeof compressionOpt === 'object' ? compressionOpt : {};
|
|
619
|
+
if (compressionConfig.enabled !== false) {
|
|
620
|
+
app.use(createCompressionMiddleware(compressionConfig));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
472
623
|
|
|
473
624
|
// Request timeout middleware
|
|
474
625
|
if (timeoutConfig !== false) {
|
|
475
626
|
app.use(timeout(timeoutConfig));
|
|
476
627
|
}
|
|
477
628
|
|
|
478
|
-
// Trust proxy (for correct req.ip, req.protocol behind reverse proxy)
|
|
479
|
-
|
|
629
|
+
// Trust proxy (for correct req.ip, req.protocol behind reverse proxy; configurable via options.trustProxy)
|
|
630
|
+
const trustProxySetting = options.trustProxy !== undefined
|
|
631
|
+
? options.trustProxy
|
|
632
|
+
: (options.server?.trustProxy !== undefined ? options.server.trustProxy : 1);
|
|
633
|
+
if (trustProxySetting !== false) {
|
|
634
|
+
app.set('trust proxy', trustProxySetting);
|
|
635
|
+
} else {
|
|
636
|
+
app.set('trust proxy', false);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Ensure extended query parser for nested filter/sort query parameters (?filter[field]=val)
|
|
640
|
+
app.set('query parser', 'extended');
|
|
480
641
|
|
|
481
642
|
// JSON body parser for API routes
|
|
482
643
|
app.use(express.json());
|
|
483
644
|
app.use(express.urlencoded({ extended: true }));
|
|
484
645
|
|
|
646
|
+
// Ensure default empty object for body on mutating requests if not populated
|
|
647
|
+
app.use((req, res, next) => {
|
|
648
|
+
if (req.body === undefined && (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH')) {
|
|
649
|
+
req.body = {};
|
|
650
|
+
}
|
|
651
|
+
next();
|
|
652
|
+
});
|
|
653
|
+
|
|
485
654
|
// Halt processing if request has timed out (after body parsers)
|
|
486
655
|
if (timeoutConfig !== false) {
|
|
487
656
|
app.use(haltOnTimedout);
|
|
@@ -527,7 +696,21 @@ function createApp(options = {}) {
|
|
|
527
696
|
|
|
528
697
|
// Add custom Nunjucks filters
|
|
529
698
|
nunjucksEnv.addFilter('json', (obj) => {
|
|
530
|
-
|
|
699
|
+
const raw = JSON.stringify(obj, null, 2);
|
|
700
|
+
if (!raw) return '';
|
|
701
|
+
return raw
|
|
702
|
+
.replace(/</g, '\\u003c')
|
|
703
|
+
.replace(/>/g, '\\u003e')
|
|
704
|
+
.replace(/&/g, '\\u0026');
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// Translation filter: {{ 'nav.home' | t }} or {{ 'cart.items' | t({ count: 5 }) }}
|
|
708
|
+
nunjucksEnv.addFilter('t', function(key, params, defaultVal) {
|
|
709
|
+
const tFunc = this.ctx?.t || this.ctx?.fsy?.t;
|
|
710
|
+
if (typeof tFunc === 'function') {
|
|
711
|
+
return tFunc(key, params, defaultVal);
|
|
712
|
+
}
|
|
713
|
+
return defaultVal !== undefined && defaultVal !== null ? defaultVal : key;
|
|
531
714
|
});
|
|
532
715
|
|
|
533
716
|
nunjucksEnv.addFilter('date', (date, format = 'short') => {
|
|
@@ -550,7 +733,7 @@ function createApp(options = {}) {
|
|
|
550
733
|
});
|
|
551
734
|
|
|
552
735
|
// Register plugins (sync) — middlewares is the same object later passed to mountPages
|
|
553
|
-
const pluginContext = { app, nunjucksEnv, options, middlewares };
|
|
736
|
+
const pluginContext = { app, nunjucksEnv, options, middlewares, shutdownManager };
|
|
554
737
|
pluginManager.registerSync(plugins, pluginContext);
|
|
555
738
|
|
|
556
739
|
// Request logging middleware
|
|
@@ -578,6 +761,7 @@ function createApp(options = {}) {
|
|
|
578
761
|
db: options.db ?? null,
|
|
579
762
|
clientRuntime,
|
|
580
763
|
pageAssets: options.pageAssets,
|
|
764
|
+
serviceRegistry,
|
|
581
765
|
});
|
|
582
766
|
|
|
583
767
|
// Set route metadata in plugin manager
|
|
@@ -620,6 +804,7 @@ function createApp(options = {}) {
|
|
|
620
804
|
pluginManager,
|
|
621
805
|
options,
|
|
622
806
|
clientRuntime,
|
|
807
|
+
middlewares,
|
|
623
808
|
});
|
|
624
809
|
}
|
|
625
810
|
|
|
@@ -639,14 +824,24 @@ function createApp(options = {}) {
|
|
|
639
824
|
const pluginHelpers = pluginManager.getHelpers();
|
|
640
825
|
Object.assign(fsy, pluginHelpers);
|
|
641
826
|
|
|
642
|
-
// Provide safe i18n translator helper
|
|
827
|
+
// Provide safe i18n translator helper (primary + fallback)
|
|
828
|
+
const defaultLocale = process.env.DEFAULT_LOCALE || 'en';
|
|
643
829
|
const translations = loadI18n(pagesDir, '', locale);
|
|
644
|
-
const
|
|
830
|
+
const fallbackTranslations = (locale !== defaultLocale)
|
|
831
|
+
? loadI18n(pagesDir, '', defaultLocale)
|
|
832
|
+
: undefined;
|
|
833
|
+
const t = createTranslator(translations, {
|
|
834
|
+
locale,
|
|
835
|
+
fallbackTranslations,
|
|
836
|
+
fallbackLocale: defaultLocale,
|
|
837
|
+
});
|
|
645
838
|
|
|
646
839
|
return {
|
|
647
840
|
fsy,
|
|
648
841
|
locale,
|
|
649
842
|
t,
|
|
843
|
+
service: (name, input, opts) =>
|
|
844
|
+
serviceRegistry.call(name, input, { req, res: {}, fsy, locale, t, db: options.db ?? null }, opts),
|
|
650
845
|
isDev,
|
|
651
846
|
url: req.url,
|
|
652
847
|
method: req.method,
|
|
@@ -725,12 +920,28 @@ function createApp(options = {}) {
|
|
|
725
920
|
}
|
|
726
921
|
});
|
|
727
922
|
|
|
728
|
-
|
|
729
|
-
app.
|
|
923
|
+
let customErrorHandler = null;
|
|
924
|
+
app.setErrorHandler = function(handler) {
|
|
925
|
+
customErrorHandler = typeof handler === 'function' ? handler : null;
|
|
926
|
+
return app;
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// Central Error Handler
|
|
930
|
+
app.use(async (err, req, res, next) => {
|
|
730
931
|
if (res.headersSent) {
|
|
731
932
|
return next(err);
|
|
732
933
|
}
|
|
733
934
|
|
|
935
|
+
// Client connection aborted
|
|
936
|
+
if (err instanceof RequestAbortedError || req.aborted || (req.socket && req.socket.destroyed)) {
|
|
937
|
+
if (!res.headersSent && !res.writableEnded) {
|
|
938
|
+
try {
|
|
939
|
+
res.end();
|
|
940
|
+
} catch (_) {}
|
|
941
|
+
}
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
734
945
|
// Handle timeout errors
|
|
735
946
|
if (req.timedout) {
|
|
736
947
|
console.error('Request timed out:', req.method, req.url);
|
|
@@ -761,12 +972,41 @@ function createApp(options = {}) {
|
|
|
761
972
|
return res.json({ error: 'Request Timeout', status: 503 });
|
|
762
973
|
}
|
|
763
974
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
975
|
+
|
|
976
|
+
const normalized = normalizeError(err, isDev);
|
|
977
|
+
const status = normalized.status || 500;
|
|
978
|
+
|
|
979
|
+
// Attach custom headers from HttpError if present
|
|
980
|
+
if (normalized.headers && typeof normalized.headers === 'object') {
|
|
981
|
+
for (const [key, value] of Object.entries(normalized.headers)) {
|
|
982
|
+
res.setHeader(key, value);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Custom Error Handler hook (app.setErrorHandler)
|
|
987
|
+
if (customErrorHandler) {
|
|
988
|
+
try {
|
|
989
|
+
const handled = await customErrorHandler(err, req, res, next);
|
|
990
|
+
if (res.headersSent || handled === true) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
} catch (customErr) {
|
|
994
|
+
console.error('[webspresso] Error in custom error handler:', customErr);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// Log errors based on severity
|
|
999
|
+
if (status >= 500) {
|
|
1000
|
+
console.error('Server error:', err);
|
|
1001
|
+
} else if (isDev && status >= 400) {
|
|
1002
|
+
console.warn(`[webspresso] HTTP ${status}:`, normalized.message);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
res.status(status);
|
|
767
1006
|
const ctx = createErrorContext(req, {
|
|
768
|
-
error: isDev ? err : { message:
|
|
769
|
-
status
|
|
1007
|
+
error: isDev ? err : { message: normalized.message },
|
|
1008
|
+
status,
|
|
1009
|
+
normalizedError: normalized,
|
|
770
1010
|
});
|
|
771
1011
|
|
|
772
1012
|
// Custom handler function
|
|
@@ -777,7 +1017,7 @@ function createApp(options = {}) {
|
|
|
777
1017
|
// Custom or auto-discovered 500 template (pre-resolved at startup)
|
|
778
1018
|
const serverErrorTemplate = resolved500Template;
|
|
779
1019
|
|
|
780
|
-
if (serverErrorTemplate && !preferJsonErrorResponse(req)) {
|
|
1020
|
+
if (status >= 500 && serverErrorTemplate && !preferJsonErrorResponse(req)) {
|
|
781
1021
|
try {
|
|
782
1022
|
const html = nunjucksEnv.render(serverErrorTemplate, ctx);
|
|
783
1023
|
return res.send(html);
|
|
@@ -788,17 +1028,55 @@ function createApp(options = {}) {
|
|
|
788
1028
|
|
|
789
1029
|
// Default response
|
|
790
1030
|
if (!preferJsonErrorResponse(req)) {
|
|
791
|
-
res.send(default500Html(
|
|
1031
|
+
res.send(default500Html(normalized, isDev));
|
|
792
1032
|
} else {
|
|
793
|
-
res.json(
|
|
794
|
-
error: 'Internal Server Error',
|
|
795
|
-
status: err.status || 500,
|
|
796
|
-
...(isDev && { message: err.message, stack: err.stack })
|
|
797
|
-
});
|
|
1033
|
+
res.json(toErrorResponseObject(normalized, isDev));
|
|
798
1034
|
}
|
|
799
1035
|
});
|
|
1036
|
+
|
|
1037
|
+
// Decorate app with shutdown lifecycle helpers
|
|
1038
|
+
app.shutdownManager = shutdownManager;
|
|
1039
|
+
Object.defineProperty(app, 'isShuttingDown', {
|
|
1040
|
+
get: () => shutdownManager.isShuttingDown,
|
|
1041
|
+
configurable: true,
|
|
1042
|
+
enumerable: true,
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
app.onShutdown = function(fn) {
|
|
1046
|
+
shutdownManager.onShutdown(fn);
|
|
1047
|
+
return app;
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
app.close = function(reason) {
|
|
1051
|
+
return shutdownManager.close(reason);
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
app.enableShutdownHooks = function() {
|
|
1055
|
+
shutdownManager.enableShutdownHooks();
|
|
1056
|
+
return app;
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
app.disableShutdownHooks = function() {
|
|
1060
|
+
shutdownManager.disableShutdownHooks();
|
|
1061
|
+
return app;
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
// Wrap app.listen to register server adapter and signal hooks
|
|
1065
|
+
const origListen = app.listen.bind(app);
|
|
1066
|
+
app.listen = function(...args) {
|
|
1067
|
+
const server = origListen(...args);
|
|
1068
|
+
app.server = server;
|
|
1069
|
+
const adapter = new NodeHttpAdapter(server);
|
|
1070
|
+
shutdownManager.registerAdapter(adapter);
|
|
1071
|
+
|
|
1072
|
+
if (shutdownConfig.enabled !== false && !isTest) {
|
|
1073
|
+
shutdownManager.enableShutdownHooks();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return server;
|
|
1077
|
+
};
|
|
800
1078
|
|
|
801
|
-
return { app, nunjucksEnv, pluginManager, authMiddleware };
|
|
1079
|
+
return { app, nunjucksEnv, pluginManager, authMiddleware, shutdownManager };
|
|
802
1080
|
}
|
|
803
1081
|
|
|
804
1082
|
// Export for use as library
|