webspresso 0.0.90 → 0.0.91
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 +358 -8
- package/bin/commands/service.js +193 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/middleware.js +9 -0
- package/core/compression/compressible.js +51 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +301 -0
- package/core/compression/negotiate.js +153 -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/orm/complexity.js +120 -0
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +45 -10
- package/core/orm/model.js +2 -0
- 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 +284 -0
- package/core/queue/adapters/memory.js +155 -0
- package/core/queue/adapters/redis.js +336 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +387 -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/index.d.ts +829 -52
- package/index.js +103 -0
- package/package.json +22 -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 +4 -1
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
- package/plugins/admin-panel/client/parts/08-record-list.js +17 -24
- 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 +90 -17
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- 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 +5 -1
- package/plugins/data-exchange/import.js +16 -2
- package/plugins/data-exchange/index.js +14 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +29 -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 +196 -21
- package/src/helpers.js +20 -2
- package/src/plugin-manager.js +124 -4
- package/src/server.js +304 -44
- 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 +140 -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 +288 -0
- package/src/services/validator.js +281 -0
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();
|
|
@@ -153,29 +167,7 @@ function getDefaultHelmetConfig(isDev) {
|
|
|
153
167
|
};
|
|
154
168
|
}
|
|
155
169
|
|
|
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
170
|
|
|
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
171
|
|
|
180
172
|
/**
|
|
181
173
|
* Shared CSS for built-in HTML error pages (viewport-safe, fluid type, dark mode)
|
|
@@ -420,9 +412,141 @@ function createApp(options = {}) {
|
|
|
420
412
|
|
|
421
413
|
const clientRuntime = resolveClientRuntime(options);
|
|
422
414
|
|
|
423
|
-
|
|
415
|
+
const shutdownConfig = {
|
|
416
|
+
enabled: true,
|
|
417
|
+
mode: 'graceful',
|
|
418
|
+
timeout: 10_000,
|
|
419
|
+
...(options.shutdown || options.server?.shutdown || {}),
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const shutdownManager = new ShutdownManager({
|
|
423
|
+
...shutdownConfig,
|
|
424
|
+
logger: logging ? console : null,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Services Layer initialization
|
|
428
|
+
const servicesDirOption = options.servicesDir ?? options.services?.dir;
|
|
429
|
+
const resolvedServicesDir = servicesDirOption
|
|
430
|
+
? path.resolve(servicesDirOption)
|
|
431
|
+
: (fs.existsSync(path.join(process.cwd(), 'services'))
|
|
432
|
+
? path.join(process.cwd(), 'services')
|
|
433
|
+
: null);
|
|
434
|
+
|
|
435
|
+
const serviceRegistry = createServiceRegistry({
|
|
436
|
+
servicesDir: resolvedServicesDir,
|
|
437
|
+
isDev,
|
|
438
|
+
logger: logging ? console : null,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Auto-register built-in auth services if database is present and service is not custom-defined
|
|
442
|
+
if (options.db) {
|
|
443
|
+
const { createAuthServices } = require('./services/builtins/auth');
|
|
444
|
+
const authServices = createAuthServices({ db: options.db });
|
|
445
|
+
for (const [name, def] of Object.entries(authServices)) {
|
|
446
|
+
if (!serviceRegistry.has(name)) {
|
|
447
|
+
serviceRegistry.register(name, def);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Auto-register built-in system services (health, cleanup, cache flush, info)
|
|
453
|
+
const { createSystemServices } = require('./services/builtins/system');
|
|
454
|
+
const systemServices = createSystemServices({
|
|
455
|
+
db: options.db ?? null,
|
|
456
|
+
serviceRegistry,
|
|
457
|
+
pluginManager,
|
|
458
|
+
});
|
|
459
|
+
for (const [name, def] of Object.entries(systemServices)) {
|
|
460
|
+
if (!serviceRegistry.has(name)) {
|
|
461
|
+
serviceRegistry.register(name, def);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
setAppContext({ db: options.db ?? null, shutdownManager, serviceRegistry });
|
|
424
466
|
|
|
425
467
|
const app = express();
|
|
468
|
+
app.serviceRegistry = serviceRegistry;
|
|
469
|
+
|
|
470
|
+
// Services caller middleware
|
|
471
|
+
app.use((req, res, next) => {
|
|
472
|
+
req.service = (name, input, opts) =>
|
|
473
|
+
serviceRegistry.call(name, input, { req, res, db: options.db ?? null, ...(req.context || {}) }, opts);
|
|
474
|
+
next();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// SSR Streaming Response Helper
|
|
478
|
+
const { renderStream } = require('../core/ssr/stream');
|
|
479
|
+
app.use((req, res, next) => {
|
|
480
|
+
res.renderStream = (templatePath, context = {}, renderOptions = {}) => {
|
|
481
|
+
return renderStream(res, templatePath, context, {
|
|
482
|
+
env: nunjucksEnv,
|
|
483
|
+
...renderOptions,
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
next();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Async handler wrapper helper for automatic promise rejection handling
|
|
490
|
+
function wrapAsync(fn) {
|
|
491
|
+
if (typeof fn !== 'function') return fn;
|
|
492
|
+
if (fn.length === 4) {
|
|
493
|
+
return function(err, req, res, next) {
|
|
494
|
+
try {
|
|
495
|
+
const ret = fn.call(this, err, req, res, next);
|
|
496
|
+
if (ret && typeof ret.catch === 'function') {
|
|
497
|
+
ret.catch(next);
|
|
498
|
+
}
|
|
499
|
+
return ret;
|
|
500
|
+
} catch (syncErr) {
|
|
501
|
+
return next(syncErr);
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return function(req, res, next) {
|
|
506
|
+
try {
|
|
507
|
+
const ret = fn.call(this, req, res, next);
|
|
508
|
+
if (ret && typeof ret.catch === 'function') {
|
|
509
|
+
ret.catch(next);
|
|
510
|
+
}
|
|
511
|
+
return ret;
|
|
512
|
+
} catch (syncErr) {
|
|
513
|
+
return next(syncErr);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Wrap routing methods on app to catch uncaught async errors
|
|
519
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all'];
|
|
520
|
+
for (const method of HTTP_METHODS) {
|
|
521
|
+
const origMethod = app[method];
|
|
522
|
+
if (typeof origMethod === 'function') {
|
|
523
|
+
app[method] = function(path, ...handlers) {
|
|
524
|
+
const wrappedHandlers = handlers.flat().map((h) => wrapAsync(h));
|
|
525
|
+
return origMethod.call(this, path, ...wrappedHandlers);
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Request draining check during graceful shutdown (zero overhead during normal operation)
|
|
531
|
+
app.use((req, res, next) => {
|
|
532
|
+
if (shutdownManager.isShuttingDown) {
|
|
533
|
+
res.set('Connection', 'close');
|
|
534
|
+
if (!res.headersSent) {
|
|
535
|
+
if (preferJsonErrorResponse(req)) {
|
|
536
|
+
return res.status(503).json({ error: 'Service Unavailable', message: 'Server is shutting down', status: 503 });
|
|
537
|
+
}
|
|
538
|
+
return res.status(503).send('Server is shutting down');
|
|
539
|
+
}
|
|
540
|
+
res.on('finish', () => {
|
|
541
|
+
try {
|
|
542
|
+
if (req.socket && !req.socket.destroyed) {
|
|
543
|
+
req.socket.end();
|
|
544
|
+
}
|
|
545
|
+
} catch (e) {}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
next();
|
|
549
|
+
});
|
|
426
550
|
|
|
427
551
|
// Security headers with Helmet
|
|
428
552
|
if (helmetConfig !== false) {
|
|
@@ -469,19 +593,46 @@ function createApp(options = {}) {
|
|
|
469
593
|
|
|
470
594
|
app.use(helmet(finalConfig));
|
|
471
595
|
}
|
|
596
|
+
|
|
597
|
+
// HTTP Response Compression
|
|
598
|
+
const compressionOpt = options.compression ?? options.server?.compression ?? false;
|
|
599
|
+
if (compressionOpt) {
|
|
600
|
+
const compressionConfig = typeof compressionOpt === 'object' ? compressionOpt : {};
|
|
601
|
+
if (compressionConfig.enabled !== false) {
|
|
602
|
+
app.use(createCompressionMiddleware(compressionConfig));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
472
605
|
|
|
473
606
|
// Request timeout middleware
|
|
474
607
|
if (timeoutConfig !== false) {
|
|
475
608
|
app.use(timeout(timeoutConfig));
|
|
476
609
|
}
|
|
477
610
|
|
|
478
|
-
// Trust proxy (for correct req.ip, req.protocol behind reverse proxy)
|
|
479
|
-
|
|
611
|
+
// Trust proxy (for correct req.ip, req.protocol behind reverse proxy; configurable via options.trustProxy)
|
|
612
|
+
const trustProxySetting = options.trustProxy !== undefined
|
|
613
|
+
? options.trustProxy
|
|
614
|
+
: (options.server?.trustProxy !== undefined ? options.server.trustProxy : 1);
|
|
615
|
+
if (trustProxySetting !== false) {
|
|
616
|
+
app.set('trust proxy', trustProxySetting);
|
|
617
|
+
} else {
|
|
618
|
+
app.set('trust proxy', false);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Ensure extended query parser for nested filter/sort query parameters (?filter[field]=val)
|
|
622
|
+
app.set('query parser', 'extended');
|
|
480
623
|
|
|
481
624
|
// JSON body parser for API routes
|
|
482
625
|
app.use(express.json());
|
|
483
626
|
app.use(express.urlencoded({ extended: true }));
|
|
484
627
|
|
|
628
|
+
// Ensure default empty object for body on mutating requests if not populated
|
|
629
|
+
app.use((req, res, next) => {
|
|
630
|
+
if (req.body === undefined && (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH')) {
|
|
631
|
+
req.body = {};
|
|
632
|
+
}
|
|
633
|
+
next();
|
|
634
|
+
});
|
|
635
|
+
|
|
485
636
|
// Halt processing if request has timed out (after body parsers)
|
|
486
637
|
if (timeoutConfig !== false) {
|
|
487
638
|
app.use(haltOnTimedout);
|
|
@@ -527,7 +678,21 @@ function createApp(options = {}) {
|
|
|
527
678
|
|
|
528
679
|
// Add custom Nunjucks filters
|
|
529
680
|
nunjucksEnv.addFilter('json', (obj) => {
|
|
530
|
-
|
|
681
|
+
const raw = JSON.stringify(obj, null, 2);
|
|
682
|
+
if (!raw) return '';
|
|
683
|
+
return raw
|
|
684
|
+
.replace(/</g, '\\u003c')
|
|
685
|
+
.replace(/>/g, '\\u003e')
|
|
686
|
+
.replace(/&/g, '\\u0026');
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Translation filter: {{ 'nav.home' | t }} or {{ 'cart.items' | t({ count: 5 }) }}
|
|
690
|
+
nunjucksEnv.addFilter('t', function(key, params, defaultVal) {
|
|
691
|
+
const tFunc = this.ctx?.t || this.ctx?.fsy?.t;
|
|
692
|
+
if (typeof tFunc === 'function') {
|
|
693
|
+
return tFunc(key, params, defaultVal);
|
|
694
|
+
}
|
|
695
|
+
return defaultVal !== undefined && defaultVal !== null ? defaultVal : key;
|
|
531
696
|
});
|
|
532
697
|
|
|
533
698
|
nunjucksEnv.addFilter('date', (date, format = 'short') => {
|
|
@@ -550,7 +715,7 @@ function createApp(options = {}) {
|
|
|
550
715
|
});
|
|
551
716
|
|
|
552
717
|
// Register plugins (sync) — middlewares is the same object later passed to mountPages
|
|
553
|
-
const pluginContext = { app, nunjucksEnv, options, middlewares };
|
|
718
|
+
const pluginContext = { app, nunjucksEnv, options, middlewares, shutdownManager };
|
|
554
719
|
pluginManager.registerSync(plugins, pluginContext);
|
|
555
720
|
|
|
556
721
|
// Request logging middleware
|
|
@@ -578,6 +743,7 @@ function createApp(options = {}) {
|
|
|
578
743
|
db: options.db ?? null,
|
|
579
744
|
clientRuntime,
|
|
580
745
|
pageAssets: options.pageAssets,
|
|
746
|
+
serviceRegistry,
|
|
581
747
|
});
|
|
582
748
|
|
|
583
749
|
// Set route metadata in plugin manager
|
|
@@ -620,6 +786,7 @@ function createApp(options = {}) {
|
|
|
620
786
|
pluginManager,
|
|
621
787
|
options,
|
|
622
788
|
clientRuntime,
|
|
789
|
+
middlewares,
|
|
623
790
|
});
|
|
624
791
|
}
|
|
625
792
|
|
|
@@ -639,14 +806,24 @@ function createApp(options = {}) {
|
|
|
639
806
|
const pluginHelpers = pluginManager.getHelpers();
|
|
640
807
|
Object.assign(fsy, pluginHelpers);
|
|
641
808
|
|
|
642
|
-
// Provide safe i18n translator helper
|
|
809
|
+
// Provide safe i18n translator helper (primary + fallback)
|
|
810
|
+
const defaultLocale = process.env.DEFAULT_LOCALE || 'en';
|
|
643
811
|
const translations = loadI18n(pagesDir, '', locale);
|
|
644
|
-
const
|
|
812
|
+
const fallbackTranslations = (locale !== defaultLocale)
|
|
813
|
+
? loadI18n(pagesDir, '', defaultLocale)
|
|
814
|
+
: undefined;
|
|
815
|
+
const t = createTranslator(translations, {
|
|
816
|
+
locale,
|
|
817
|
+
fallbackTranslations,
|
|
818
|
+
fallbackLocale: defaultLocale,
|
|
819
|
+
});
|
|
645
820
|
|
|
646
821
|
return {
|
|
647
822
|
fsy,
|
|
648
823
|
locale,
|
|
649
824
|
t,
|
|
825
|
+
service: (name, input, opts) =>
|
|
826
|
+
serviceRegistry.call(name, input, { req, res: {}, fsy, locale, t, db: options.db ?? null }, opts),
|
|
650
827
|
isDev,
|
|
651
828
|
url: req.url,
|
|
652
829
|
method: req.method,
|
|
@@ -725,12 +902,28 @@ function createApp(options = {}) {
|
|
|
725
902
|
}
|
|
726
903
|
});
|
|
727
904
|
|
|
728
|
-
|
|
729
|
-
app.
|
|
905
|
+
let customErrorHandler = null;
|
|
906
|
+
app.setErrorHandler = function(handler) {
|
|
907
|
+
customErrorHandler = typeof handler === 'function' ? handler : null;
|
|
908
|
+
return app;
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
// Central Error Handler
|
|
912
|
+
app.use(async (err, req, res, next) => {
|
|
730
913
|
if (res.headersSent) {
|
|
731
914
|
return next(err);
|
|
732
915
|
}
|
|
733
916
|
|
|
917
|
+
// Client connection aborted
|
|
918
|
+
if (err instanceof RequestAbortedError || req.aborted || (req.socket && req.socket.destroyed)) {
|
|
919
|
+
if (!res.headersSent && !res.writableEnded) {
|
|
920
|
+
try {
|
|
921
|
+
res.end();
|
|
922
|
+
} catch (_) {}
|
|
923
|
+
}
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
734
927
|
// Handle timeout errors
|
|
735
928
|
if (req.timedout) {
|
|
736
929
|
console.error('Request timed out:', req.method, req.url);
|
|
@@ -761,12 +954,41 @@ function createApp(options = {}) {
|
|
|
761
954
|
return res.json({ error: 'Request Timeout', status: 503 });
|
|
762
955
|
}
|
|
763
956
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
957
|
+
|
|
958
|
+
const normalized = normalizeError(err, isDev);
|
|
959
|
+
const status = normalized.status || 500;
|
|
960
|
+
|
|
961
|
+
// Attach custom headers from HttpError if present
|
|
962
|
+
if (normalized.headers && typeof normalized.headers === 'object') {
|
|
963
|
+
for (const [key, value] of Object.entries(normalized.headers)) {
|
|
964
|
+
res.setHeader(key, value);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Custom Error Handler hook (app.setErrorHandler)
|
|
969
|
+
if (customErrorHandler) {
|
|
970
|
+
try {
|
|
971
|
+
const handled = await customErrorHandler(err, req, res, next);
|
|
972
|
+
if (res.headersSent || handled === true) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
} catch (customErr) {
|
|
976
|
+
console.error('[webspresso] Error in custom error handler:', customErr);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// Log errors based on severity
|
|
981
|
+
if (status >= 500) {
|
|
982
|
+
console.error('Server error:', err);
|
|
983
|
+
} else if (isDev && status >= 400) {
|
|
984
|
+
console.warn(`[webspresso] HTTP ${status}:`, normalized.message);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
res.status(status);
|
|
767
988
|
const ctx = createErrorContext(req, {
|
|
768
|
-
error: isDev ? err : { message:
|
|
769
|
-
status
|
|
989
|
+
error: isDev ? err : { message: normalized.message },
|
|
990
|
+
status,
|
|
991
|
+
normalizedError: normalized,
|
|
770
992
|
});
|
|
771
993
|
|
|
772
994
|
// Custom handler function
|
|
@@ -777,7 +999,7 @@ function createApp(options = {}) {
|
|
|
777
999
|
// Custom or auto-discovered 500 template (pre-resolved at startup)
|
|
778
1000
|
const serverErrorTemplate = resolved500Template;
|
|
779
1001
|
|
|
780
|
-
if (serverErrorTemplate && !preferJsonErrorResponse(req)) {
|
|
1002
|
+
if (status >= 500 && serverErrorTemplate && !preferJsonErrorResponse(req)) {
|
|
781
1003
|
try {
|
|
782
1004
|
const html = nunjucksEnv.render(serverErrorTemplate, ctx);
|
|
783
1005
|
return res.send(html);
|
|
@@ -788,17 +1010,55 @@ function createApp(options = {}) {
|
|
|
788
1010
|
|
|
789
1011
|
// Default response
|
|
790
1012
|
if (!preferJsonErrorResponse(req)) {
|
|
791
|
-
res.send(default500Html(
|
|
1013
|
+
res.send(default500Html(normalized, isDev));
|
|
792
1014
|
} else {
|
|
793
|
-
res.json(
|
|
794
|
-
error: 'Internal Server Error',
|
|
795
|
-
status: err.status || 500,
|
|
796
|
-
...(isDev && { message: err.message, stack: err.stack })
|
|
797
|
-
});
|
|
1015
|
+
res.json(toErrorResponseObject(normalized, isDev));
|
|
798
1016
|
}
|
|
799
1017
|
});
|
|
1018
|
+
|
|
1019
|
+
// Decorate app with shutdown lifecycle helpers
|
|
1020
|
+
app.shutdownManager = shutdownManager;
|
|
1021
|
+
Object.defineProperty(app, 'isShuttingDown', {
|
|
1022
|
+
get: () => shutdownManager.isShuttingDown,
|
|
1023
|
+
configurable: true,
|
|
1024
|
+
enumerable: true,
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
app.onShutdown = function(fn) {
|
|
1028
|
+
shutdownManager.onShutdown(fn);
|
|
1029
|
+
return app;
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
app.close = function(reason) {
|
|
1033
|
+
return shutdownManager.close(reason);
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
app.enableShutdownHooks = function() {
|
|
1037
|
+
shutdownManager.enableShutdownHooks();
|
|
1038
|
+
return app;
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
app.disableShutdownHooks = function() {
|
|
1042
|
+
shutdownManager.disableShutdownHooks();
|
|
1043
|
+
return app;
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
// Wrap app.listen to register server adapter and signal hooks
|
|
1047
|
+
const origListen = app.listen.bind(app);
|
|
1048
|
+
app.listen = function(...args) {
|
|
1049
|
+
const server = origListen(...args);
|
|
1050
|
+
app.server = server;
|
|
1051
|
+
const adapter = new NodeHttpAdapter(server);
|
|
1052
|
+
shutdownManager.registerAdapter(adapter);
|
|
1053
|
+
|
|
1054
|
+
if (shutdownConfig.enabled !== false && !isTest) {
|
|
1055
|
+
shutdownManager.enableShutdownHooks();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
return server;
|
|
1059
|
+
};
|
|
800
1060
|
|
|
801
|
-
return { app, nunjucksEnv, pluginManager, authMiddleware };
|
|
1061
|
+
return { app, nunjucksEnv, pluginManager, authMiddleware, shutdownManager };
|
|
802
1062
|
}
|
|
803
1063
|
|
|
804
1064
|
// Export for use as library
|