webspresso 0.0.97 → 0.0.98
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/ADMIN.md +153 -5
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/CLI.md +195 -1
- package/.agents/PLUGINS.md +153 -9
- package/.agents/QUEUE.md +43 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +21 -12
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/index.js +12 -7
- package/core/orm/model.js +49 -7
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/manager.js +123 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +660 -10
- package/index.js +44 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/audit-log/admin-component.js +11 -0
- package/plugins/audit-log/api-handlers.js +34 -0
- package/plugins/audit-log/index.js +129 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/index.js +4 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +11 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +23 -0
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +3 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/file-router.js +210 -62
- package/src/helpers.js +958 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/server.js +629 -63
package/src/server.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const crypto = require('crypto');
|
|
9
|
+
const zlib = require('zlib');
|
|
9
10
|
const { AsyncLocalStorage } = require('async_hooks');
|
|
10
11
|
const express = require('express');
|
|
11
12
|
const helmet = require('helmet');
|
|
@@ -17,6 +18,9 @@ const { mountClientRuntime } = require('./client-runtime/mount');
|
|
|
17
18
|
const { resolveClientRuntime } = require('./client-runtime/resolve');
|
|
18
19
|
const { mountPages, detectLocale, loadI18n, createTranslator } = require('./file-router');
|
|
19
20
|
const { configureAssets, createHelpers, getScriptInjector } = require('./helpers');
|
|
21
|
+
const { createDevReloadHub } = require('./dev-reload');
|
|
22
|
+
const { createDevBundler } = require('./bundle');
|
|
23
|
+
const { registerClearHook } = require('./njk-frontmatter');
|
|
20
24
|
const { createPluginManager } = require('./plugin-manager');
|
|
21
25
|
const { createServiceRegistry } = require('./services');
|
|
22
26
|
const { scanRoutes } = require('./discovery/scan-routes');
|
|
@@ -25,6 +29,12 @@ const { mountDiscoveredRoutes } = require('./routing/mount-discovered-routes');
|
|
|
25
29
|
const { discoverModuleDetails } = require('./modules/module-discovery');
|
|
26
30
|
const { ShutdownManager, NodeHttpAdapter } = require('../core/shutdown');
|
|
27
31
|
const { createCompressionMiddleware } = require('../core/compression');
|
|
32
|
+
const {
|
|
33
|
+
createDevProfilerMiddleware,
|
|
34
|
+
attachProfilerToKnex,
|
|
35
|
+
formatDevBanner,
|
|
36
|
+
printDevBanner,
|
|
37
|
+
} = require('./dev');
|
|
28
38
|
const {
|
|
29
39
|
WebspressoError,
|
|
30
40
|
HttpError,
|
|
@@ -40,6 +50,35 @@ const {
|
|
|
40
50
|
// Async storage for tracking template render call stacks (circular extends guard)
|
|
41
51
|
const renderStackStorage = new AsyncLocalStorage();
|
|
42
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Fast hardware-accelerated Weak ETag generator using native zlib.crc32
|
|
55
|
+
* Up to 13x faster than Express default SHA-1, zero external dependencies.
|
|
56
|
+
* Compliant with RFC 7232 Section 2.3 (Weak Entity Tag).
|
|
57
|
+
* Format: W/"<len-hex>-<crc32-hex>"
|
|
58
|
+
*
|
|
59
|
+
* @param {Buffer|string} body - Response body
|
|
60
|
+
* @param {string} [encoding] - String encoding if body is string
|
|
61
|
+
* @returns {string} RFC 7232 formatted Weak ETag
|
|
62
|
+
*/
|
|
63
|
+
function fastETag(body, encoding) {
|
|
64
|
+
if (!body || body.length === 0) {
|
|
65
|
+
return 'W/"0-0"';
|
|
66
|
+
}
|
|
67
|
+
const buf = Buffer.isBuffer(body) ? body : Buffer.from(body, encoding || 'utf8');
|
|
68
|
+
if (buf.length === 0) {
|
|
69
|
+
return 'W/"0-0"';
|
|
70
|
+
}
|
|
71
|
+
if (typeof zlib.crc32 === 'function') {
|
|
72
|
+
return `W/"${buf.length.toString(16)}-${zlib.crc32(buf).toString(16)}"`;
|
|
73
|
+
}
|
|
74
|
+
// Graceful fallback for environments without zlib.crc32
|
|
75
|
+
let h1 = 0x811c9dc5;
|
|
76
|
+
for (let i = 0; i < buf.length; i++) {
|
|
77
|
+
h1 = Math.imul(h1 ^ buf[i], 0x01000193);
|
|
78
|
+
}
|
|
79
|
+
return `W/"${buf.length.toString(16)}-${(h1 >>> 0).toString(16)}"`;
|
|
80
|
+
}
|
|
81
|
+
|
|
43
82
|
/**
|
|
44
83
|
* Configure Nunjucks environment with circular extends guard and loader protection
|
|
45
84
|
* @param {string|string[]} templateDirs - Template directory paths
|
|
@@ -52,14 +91,21 @@ function configureSafeNunjucks(templateDirs, options = {}) {
|
|
|
52
91
|
const origRender = env.render.bind(env);
|
|
53
92
|
const origRenderString = env.renderString.bind(env);
|
|
54
93
|
|
|
94
|
+
// Production template cache to bypass loader traversal when caching is active
|
|
95
|
+
const productionTemplateCache = (!options.noCache) ? new Map() : null;
|
|
96
|
+
if (productionTemplateCache) {
|
|
97
|
+
registerClearHook(() => productionTemplateCache.clear());
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
function wrapTemplateRoot(tmpl, name) {
|
|
56
101
|
if (!tmpl || tmpl._safeWrapped) return;
|
|
57
102
|
tmpl._safeWrapped = true;
|
|
58
103
|
const origRoot = tmpl.rootRenderFunc;
|
|
59
104
|
if (typeof origRoot !== 'function') return;
|
|
60
105
|
|
|
106
|
+
const normalizedName = String(name || tmpl.path || 'anonymous');
|
|
107
|
+
|
|
61
108
|
tmpl.rootRenderFunc = function(e, context, frame, runtime, renderCb) {
|
|
62
|
-
const normalizedName = String(name || tmpl.path || 'anonymous');
|
|
63
109
|
const store = renderStackStorage.getStore();
|
|
64
110
|
if (store) {
|
|
65
111
|
store.stack.push(normalizedName);
|
|
@@ -69,8 +115,12 @@ function configureSafeNunjucks(templateDirs, options = {}) {
|
|
|
69
115
|
if (!finished) {
|
|
70
116
|
finished = true;
|
|
71
117
|
if (store) {
|
|
72
|
-
|
|
73
|
-
|
|
118
|
+
if (store.stack[store.stack.length - 1] === normalizedName) {
|
|
119
|
+
store.stack.pop();
|
|
120
|
+
} else {
|
|
121
|
+
const idx = store.stack.lastIndexOf(normalizedName);
|
|
122
|
+
if (idx !== -1) store.stack.splice(idx, 1);
|
|
123
|
+
}
|
|
74
124
|
}
|
|
75
125
|
}
|
|
76
126
|
renderCb(err, out);
|
|
@@ -79,8 +129,12 @@ function configureSafeNunjucks(templateDirs, options = {}) {
|
|
|
79
129
|
return origRoot.call(this, e, context, frame, runtime, done);
|
|
80
130
|
} catch (ex) {
|
|
81
131
|
if (store) {
|
|
82
|
-
|
|
83
|
-
|
|
132
|
+
if (store.stack[store.stack.length - 1] === normalizedName) {
|
|
133
|
+
store.stack.pop();
|
|
134
|
+
} else {
|
|
135
|
+
const idx = store.stack.lastIndexOf(normalizedName);
|
|
136
|
+
if (idx !== -1) store.stack.splice(idx, 1);
|
|
137
|
+
}
|
|
84
138
|
}
|
|
85
139
|
throw ex;
|
|
86
140
|
}
|
|
@@ -108,17 +162,28 @@ function configureSafeNunjucks(templateDirs, options = {}) {
|
|
|
108
162
|
}
|
|
109
163
|
}
|
|
110
164
|
|
|
165
|
+
if (!cb && !parentName && productionTemplateCache && typeof name === 'string') {
|
|
166
|
+
const cached = productionTemplateCache.get(name);
|
|
167
|
+
if (cached) return cached;
|
|
168
|
+
}
|
|
169
|
+
|
|
111
170
|
const wrappedCb = typeof cb === 'function' ? function(err, tmpl) {
|
|
112
|
-
if (tmpl) {
|
|
171
|
+
if (tmpl && !tmpl._safeWrapped) {
|
|
113
172
|
wrapTemplateRoot(tmpl, name);
|
|
114
173
|
}
|
|
174
|
+
if (productionTemplateCache && tmpl && !err && typeof name === 'string' && !parentName) {
|
|
175
|
+
productionTemplateCache.set(name, tmpl);
|
|
176
|
+
}
|
|
115
177
|
cb(err, tmpl);
|
|
116
178
|
} : undefined;
|
|
117
179
|
|
|
118
180
|
const res = origGetTemplate(name, eagerCompile, parentName, ignoreMissing, wrappedCb);
|
|
119
|
-
if (res) {
|
|
181
|
+
if (res && !res._safeWrapped) {
|
|
120
182
|
wrapTemplateRoot(res, name);
|
|
121
183
|
}
|
|
184
|
+
if (productionTemplateCache && res && typeof name === 'string' && !parentName) {
|
|
185
|
+
productionTemplateCache.set(name, res);
|
|
186
|
+
}
|
|
122
187
|
return res;
|
|
123
188
|
};
|
|
124
189
|
|
|
@@ -407,7 +472,13 @@ function createApp(options = {}) {
|
|
|
407
472
|
timeout: timeoutConfig = '30s',
|
|
408
473
|
auth: authManager = null,
|
|
409
474
|
setupRoutes,
|
|
475
|
+
dev: devOpts = {},
|
|
410
476
|
} = options;
|
|
477
|
+
|
|
478
|
+
// Auto-instrument Knex queries for dev profiler
|
|
479
|
+
if (options.db && options.db.knex) {
|
|
480
|
+
attachProfilerToKnex(options.db.knex);
|
|
481
|
+
}
|
|
411
482
|
|
|
412
483
|
if (!initialPagesDir && !options.pages && !options.api && !options.modules && !options.routes && !options.rootDir) {
|
|
413
484
|
throw new Error('pagesDir is required');
|
|
@@ -433,8 +504,11 @@ function createApp(options = {}) {
|
|
|
433
504
|
const pluginManager = createPluginManager();
|
|
434
505
|
|
|
435
506
|
// Configure asset manager
|
|
507
|
+
const projectBase = publicDir ? path.dirname(path.resolve(publicDir)) : process.cwd();
|
|
436
508
|
configureAssets({
|
|
437
509
|
publicDir: publicDir || 'public',
|
|
510
|
+
srcDir: options.srcDir || path.join(projectBase, 'src'),
|
|
511
|
+
pagesDir: pagesDir || 'pages',
|
|
438
512
|
...assetsConfig
|
|
439
513
|
});
|
|
440
514
|
|
|
@@ -495,16 +569,33 @@ function createApp(options = {}) {
|
|
|
495
569
|
const app = express();
|
|
496
570
|
app.serviceRegistry = serviceRegistry;
|
|
497
571
|
|
|
498
|
-
//
|
|
572
|
+
// Unified Request Context & Lifecycle Initializer middleware
|
|
573
|
+
const { renderStream } = require('../core/ssr/stream');
|
|
499
574
|
app.use((req, res, next) => {
|
|
575
|
+
// 1. Request draining check during graceful shutdown
|
|
576
|
+
if (shutdownManager.isShuttingDown) {
|
|
577
|
+
res.set('Connection', 'close');
|
|
578
|
+
if (!res.headersSent) {
|
|
579
|
+
if (preferJsonErrorResponse(req)) {
|
|
580
|
+
return res.status(503).json({ error: 'Service Unavailable', message: 'Server is shutting down', status: 503 });
|
|
581
|
+
}
|
|
582
|
+
return res.status(503).send('Server is shutting down');
|
|
583
|
+
}
|
|
584
|
+
res.on('finish', () => {
|
|
585
|
+
try {
|
|
586
|
+
if (req.socket && !req.socket.destroyed) {
|
|
587
|
+
req.socket.end();
|
|
588
|
+
}
|
|
589
|
+
} catch (e) {}
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 2. Correlation Request ID
|
|
500
594
|
const reqId = (req.headers && req.headers['x-request-id']) ? req.headers['x-request-id'] : crypto.randomUUID();
|
|
501
595
|
req.id = reqId;
|
|
502
596
|
res.setHeader('X-Request-Id', reqId);
|
|
503
|
-
next();
|
|
504
|
-
});
|
|
505
597
|
|
|
506
|
-
|
|
507
|
-
app.use((req, res, next) => {
|
|
598
|
+
// 3. Request DB, Services, and Context
|
|
508
599
|
if (options.db) {
|
|
509
600
|
req.db = options.db;
|
|
510
601
|
}
|
|
@@ -518,29 +609,40 @@ function createApp(options = {}) {
|
|
|
518
609
|
};
|
|
519
610
|
req.service = (name, input, opts) =>
|
|
520
611
|
serviceRegistry.call(name, input, req.context, opts);
|
|
521
|
-
next();
|
|
522
|
-
});
|
|
523
612
|
|
|
524
|
-
|
|
525
|
-
const { renderStream } = require('../core/ssr/stream');
|
|
526
|
-
app.use((req, res, next) => {
|
|
613
|
+
// 4. SSR Streaming Response Helper
|
|
527
614
|
res.renderStream = (templatePath, context = {}, renderOptions = {}) => {
|
|
528
615
|
return renderStream(res, templatePath, context, {
|
|
529
616
|
env: nunjucksEnv,
|
|
530
617
|
...renderOptions,
|
|
531
618
|
});
|
|
532
619
|
};
|
|
620
|
+
|
|
533
621
|
next();
|
|
534
622
|
});
|
|
535
623
|
|
|
536
624
|
// Async handler wrapper helper for automatic promise rejection handling
|
|
537
625
|
function wrapAsync(fn) {
|
|
538
626
|
if (typeof fn !== 'function') return fn;
|
|
627
|
+
if (fn._isWrappedAsync) return fn;
|
|
628
|
+
let wrapped;
|
|
539
629
|
if (fn.length === 4) {
|
|
540
|
-
|
|
630
|
+
wrapped = function(err, req, res, next) {
|
|
541
631
|
try {
|
|
542
632
|
const ret = fn.call(this, err, req, res, next);
|
|
543
|
-
if (ret &&
|
|
633
|
+
if (ret && ret.then) {
|
|
634
|
+
ret.catch(next);
|
|
635
|
+
}
|
|
636
|
+
return ret;
|
|
637
|
+
} catch (syncErr) {
|
|
638
|
+
return next(syncErr);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
} else {
|
|
642
|
+
wrapped = function(req, res, next) {
|
|
643
|
+
try {
|
|
644
|
+
const ret = fn.call(this, req, res, next);
|
|
645
|
+
if (ret && ret.then) {
|
|
544
646
|
ret.catch(next);
|
|
545
647
|
}
|
|
546
648
|
return ret;
|
|
@@ -549,51 +651,243 @@ function createApp(options = {}) {
|
|
|
549
651
|
}
|
|
550
652
|
};
|
|
551
653
|
}
|
|
552
|
-
|
|
654
|
+
wrapped._isWrappedAsync = true;
|
|
655
|
+
return wrapped;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function dispatchFastPath(handlers, req, res, next) {
|
|
659
|
+
let idx = 0;
|
|
660
|
+
function runNext(err) {
|
|
661
|
+
if (err) return next(err);
|
|
662
|
+
if (idx >= handlers.length) return next();
|
|
663
|
+
const fn = handlers[idx++];
|
|
553
664
|
try {
|
|
554
|
-
const ret = fn
|
|
555
|
-
if (ret && typeof ret.
|
|
556
|
-
ret.catch(
|
|
665
|
+
const ret = fn(req, res, runNext);
|
|
666
|
+
if (ret && typeof ret.then === 'function') {
|
|
667
|
+
ret.catch(runNext);
|
|
557
668
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
return next(syncErr);
|
|
669
|
+
} catch (e) {
|
|
670
|
+
runNext(e);
|
|
561
671
|
}
|
|
672
|
+
}
|
|
673
|
+
runNext();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const fastPathStaticByMethod = {
|
|
677
|
+
GET: new Map(),
|
|
678
|
+
POST: new Map(),
|
|
679
|
+
PUT: new Map(),
|
|
680
|
+
DELETE: new Map(),
|
|
681
|
+
PATCH: new Map(),
|
|
682
|
+
HEAD: new Map(),
|
|
683
|
+
OPTIONS: new Map(),
|
|
684
|
+
};
|
|
685
|
+
const fastPathStaticMap = new Map();
|
|
686
|
+
app.fastPathStaticMap = fastPathStaticMap;
|
|
687
|
+
app.fastPathStaticByMethod = fastPathStaticByMethod;
|
|
688
|
+
|
|
689
|
+
// Dynamic route registration list and LRU lookup cache
|
|
690
|
+
const dynamicRouteList = [];
|
|
691
|
+
const dynamicRouteMap = new Map();
|
|
692
|
+
const dynamicRouteLookupCache = new Map();
|
|
693
|
+
const prefixMiddlewares = [];
|
|
694
|
+
const MAX_DYNAMIC_CACHE_SIZE = 2000;
|
|
695
|
+
const NOT_FOUND_SENTINEL = Symbol('NOT_FOUND');
|
|
696
|
+
|
|
697
|
+
app.dynamicRouteList = dynamicRouteList;
|
|
698
|
+
app.dynamicRouteLookupCache = dynamicRouteLookupCache;
|
|
699
|
+
|
|
700
|
+
function getDynamicCache(key) {
|
|
701
|
+
const val = dynamicRouteLookupCache.get(key);
|
|
702
|
+
if (!val) return undefined;
|
|
703
|
+
dynamicRouteLookupCache.delete(key);
|
|
704
|
+
dynamicRouteLookupCache.set(key, val);
|
|
705
|
+
return val;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function setDynamicCache(key, val) {
|
|
709
|
+
if (dynamicRouteLookupCache.size >= MAX_DYNAMIC_CACHE_SIZE) {
|
|
710
|
+
const oldestKey = dynamicRouteLookupCache.keys().next().value;
|
|
711
|
+
dynamicRouteLookupCache.delete(oldestKey);
|
|
712
|
+
}
|
|
713
|
+
dynamicRouteLookupCache.set(key, val);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function extractRouteParams(paramNames, match) {
|
|
717
|
+
const params = {};
|
|
718
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
719
|
+
const val = match[i + 1];
|
|
720
|
+
if (val !== undefined) {
|
|
721
|
+
try {
|
|
722
|
+
params[paramNames[i]] = decodeURIComponent(val);
|
|
723
|
+
} catch (_) {
|
|
724
|
+
params[paramNames[i]] = val;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return params;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function compileRoutePattern(pattern) {
|
|
732
|
+
if (typeof pattern !== 'string') return null;
|
|
733
|
+
if (pattern === '*' || pattern === '/*' || pattern === '') return null;
|
|
734
|
+
if (pattern.includes('(') || pattern.includes(')')) return null;
|
|
735
|
+
|
|
736
|
+
const paramNames = [];
|
|
737
|
+
const parts = pattern.split('/');
|
|
738
|
+
let regexStr = '^';
|
|
739
|
+
let hasWildcard = false;
|
|
740
|
+
|
|
741
|
+
for (let i = 1; i < parts.length; i++) {
|
|
742
|
+
const part = parts[i];
|
|
743
|
+
if (!part) continue;
|
|
744
|
+
regexStr += '\\/';
|
|
745
|
+
if (part.startsWith(':')) {
|
|
746
|
+
const isOptional = part.endsWith('?');
|
|
747
|
+
const name = isOptional ? part.slice(1, -1) : part.slice(1);
|
|
748
|
+
paramNames.push(name);
|
|
749
|
+
regexStr += isOptional ? '([^/]+)?' : '([^/]+)';
|
|
750
|
+
} else if (part.startsWith('*')) {
|
|
751
|
+
const name = part.slice(1) || '0';
|
|
752
|
+
paramNames.push(name);
|
|
753
|
+
regexStr += '(.*)';
|
|
754
|
+
hasWildcard = true;
|
|
755
|
+
} else {
|
|
756
|
+
regexStr += part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (regexStr === '^') {
|
|
761
|
+
regexStr = '^\\/?$';
|
|
762
|
+
} else {
|
|
763
|
+
regexStr += '\\/?$';
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
pattern,
|
|
768
|
+
regex: new RegExp(regexStr),
|
|
769
|
+
paramNames,
|
|
770
|
+
hasWildcard,
|
|
562
771
|
};
|
|
563
772
|
}
|
|
564
773
|
|
|
565
|
-
// Wrap routing methods on app to catch uncaught async errors
|
|
774
|
+
// Wrap routing methods on app to catch uncaught async errors and register fast-path static and dynamic routes
|
|
566
775
|
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all'];
|
|
567
776
|
for (const method of HTTP_METHODS) {
|
|
568
777
|
const origMethod = app[method];
|
|
569
778
|
if (typeof origMethod === 'function') {
|
|
570
779
|
app[method] = function(path, ...handlers) {
|
|
780
|
+
if (handlers.length === 0) {
|
|
781
|
+
return origMethod.call(this, path);
|
|
782
|
+
}
|
|
571
783
|
const wrappedHandlers = handlers.flat().map((h) => wrapAsync(h));
|
|
784
|
+
|
|
785
|
+
const normPath = (typeof path === 'string' && path.length > 1 && path.endsWith('/'))
|
|
786
|
+
? path.slice(0, -1)
|
|
787
|
+
: path;
|
|
788
|
+
|
|
789
|
+
// Fast-path static registration for literal string paths without dynamic parameters or regex
|
|
790
|
+
if (
|
|
791
|
+
typeof normPath === 'string' &&
|
|
792
|
+
normPath.length > 0 &&
|
|
793
|
+
!normPath.includes(':') &&
|
|
794
|
+
!normPath.includes('*') &&
|
|
795
|
+
!normPath.includes('(') &&
|
|
796
|
+
!normPath.includes(')') &&
|
|
797
|
+
!normPath.includes('?')
|
|
798
|
+
) {
|
|
799
|
+
const uMethod = method.toUpperCase();
|
|
800
|
+
const routeInfo = { path: normPath, methods: { [method.toLowerCase()]: true } };
|
|
801
|
+
const registerStatic = (m) => {
|
|
802
|
+
const key = `${m} ${normPath}`;
|
|
803
|
+
let methodMap = fastPathStaticByMethod[m];
|
|
804
|
+
if (!methodMap) {
|
|
805
|
+
methodMap = new Map();
|
|
806
|
+
fastPathStaticByMethod[m] = methodMap;
|
|
807
|
+
}
|
|
808
|
+
const existing = methodMap.get(normPath);
|
|
809
|
+
if (existing) {
|
|
810
|
+
existing.handlers.push(...wrappedHandlers);
|
|
811
|
+
} else {
|
|
812
|
+
const entry = {
|
|
813
|
+
path: normPath,
|
|
814
|
+
handlers: [...wrappedHandlers],
|
|
815
|
+
routeInfo,
|
|
816
|
+
};
|
|
817
|
+
methodMap.set(normPath, entry);
|
|
818
|
+
fastPathStaticMap.set(key, entry);
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
if (uMethod === 'ALL') {
|
|
823
|
+
for (const m of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']) {
|
|
824
|
+
registerStatic(m);
|
|
825
|
+
}
|
|
826
|
+
} else {
|
|
827
|
+
registerStatic(uMethod);
|
|
828
|
+
}
|
|
829
|
+
dynamicRouteLookupCache.clear();
|
|
830
|
+
} else if (
|
|
831
|
+
typeof path === 'string' &&
|
|
832
|
+
path.length > 0 &&
|
|
833
|
+
(path.includes(':') || path.includes('*')) &&
|
|
834
|
+
path !== '*' &&
|
|
835
|
+
path !== '/*' &&
|
|
836
|
+
!path.includes('(') &&
|
|
837
|
+
!path.includes(')')
|
|
838
|
+
) {
|
|
839
|
+
const compiled = compileRoutePattern(path);
|
|
840
|
+
if (compiled) {
|
|
841
|
+
const uMethod = method.toUpperCase();
|
|
842
|
+
const routeInfo = { path, methods: { [method.toLowerCase()]: true } };
|
|
843
|
+
const registerDynamic = (m) => {
|
|
844
|
+
const key = `${m} ${path}`;
|
|
845
|
+
const existing = dynamicRouteMap.get(key);
|
|
846
|
+
if (existing) {
|
|
847
|
+
existing.handlers.push(...wrappedHandlers);
|
|
848
|
+
} else {
|
|
849
|
+
const entry = {
|
|
850
|
+
method: m,
|
|
851
|
+
path,
|
|
852
|
+
compiled,
|
|
853
|
+
handlers: [...wrappedHandlers],
|
|
854
|
+
routeInfo,
|
|
855
|
+
};
|
|
856
|
+
dynamicRouteMap.set(key, entry);
|
|
857
|
+
dynamicRouteList.push(entry);
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
if (uMethod === 'ALL') {
|
|
862
|
+
for (const m of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']) {
|
|
863
|
+
registerDynamic(m);
|
|
864
|
+
}
|
|
865
|
+
} else {
|
|
866
|
+
registerDynamic(uMethod);
|
|
867
|
+
}
|
|
868
|
+
dynamicRouteLookupCache.clear();
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
572
872
|
return origMethod.call(this, path, ...wrappedHandlers);
|
|
573
873
|
};
|
|
574
874
|
}
|
|
575
875
|
}
|
|
576
876
|
|
|
577
|
-
//
|
|
578
|
-
app.use(
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
res.on('finish', () => {
|
|
588
|
-
try {
|
|
589
|
-
if (req.socket && !req.socket.destroyed) {
|
|
590
|
-
req.socket.end();
|
|
591
|
-
}
|
|
592
|
-
} catch (e) {}
|
|
877
|
+
// Wrap app.use to track path-prefixed middlewares for fast-path dispatcher execution
|
|
878
|
+
const origAppUse = app.use.bind(app);
|
|
879
|
+
app.use = function(pathOrHandler, ...handlers) {
|
|
880
|
+
if (typeof pathOrHandler === 'string' && handlers.length > 0) {
|
|
881
|
+
const normPrefix = (pathOrHandler.length > 1 && pathOrHandler.endsWith('/'))
|
|
882
|
+
? pathOrHandler.slice(0, -1)
|
|
883
|
+
: pathOrHandler;
|
|
884
|
+
prefixMiddlewares.push({
|
|
885
|
+
prefix: normPrefix,
|
|
886
|
+
handlers: handlers.flat().map((h) => wrapAsync(h)),
|
|
593
887
|
});
|
|
594
888
|
}
|
|
595
|
-
|
|
596
|
-
}
|
|
889
|
+
return origAppUse(pathOrHandler, ...handlers);
|
|
890
|
+
};
|
|
597
891
|
|
|
598
892
|
// Security headers with Helmet
|
|
599
893
|
if (helmetConfig !== false) {
|
|
@@ -638,7 +932,63 @@ function createApp(options = {}) {
|
|
|
638
932
|
}
|
|
639
933
|
}
|
|
640
934
|
|
|
641
|
-
|
|
935
|
+
const helmetMw = helmet(finalConfig);
|
|
936
|
+
|
|
937
|
+
// Check if configuration uses purely static directives (no per-request dynamic functions)
|
|
938
|
+
let isDynamicCsp = false;
|
|
939
|
+
if (finalConfig.contentSecurityPolicy && finalConfig.contentSecurityPolicy.directives) {
|
|
940
|
+
for (const val of Object.values(finalConfig.contentSecurityPolicy.directives)) {
|
|
941
|
+
if (typeof val === 'function') {
|
|
942
|
+
isDynamicCsp = true;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
if (Array.isArray(val) && val.some((item) => typeof item === 'function')) {
|
|
946
|
+
isDynamicCsp = true;
|
|
947
|
+
break;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (!isDynamicCsp) {
|
|
953
|
+
const staticHeaders = {};
|
|
954
|
+
let removePoweredBy = false;
|
|
955
|
+
const dummyRes = {
|
|
956
|
+
setHeader(name, value) {
|
|
957
|
+
staticHeaders[name] = value;
|
|
958
|
+
},
|
|
959
|
+
removeHeader(name) {
|
|
960
|
+
if (name && name.toLowerCase() === 'x-powered-by') {
|
|
961
|
+
removePoweredBy = true;
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
try {
|
|
967
|
+
helmetMw({ headers: {} }, dummyRes, () => {});
|
|
968
|
+
} catch (_) {
|
|
969
|
+
// Fallback to standard helmet middleware if mock invocation fails
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const headerEntries = Object.entries(staticHeaders);
|
|
973
|
+
if (headerEntries.length > 0) {
|
|
974
|
+
if (removePoweredBy) {
|
|
975
|
+
app.disable('x-powered-by');
|
|
976
|
+
}
|
|
977
|
+
app.use((req, res, next) => {
|
|
978
|
+
for (let i = 0; i < headerEntries.length; i++) {
|
|
979
|
+
res.setHeader(headerEntries[i][0], headerEntries[i][1]);
|
|
980
|
+
}
|
|
981
|
+
if (removePoweredBy) {
|
|
982
|
+
res.removeHeader('X-Powered-By');
|
|
983
|
+
}
|
|
984
|
+
next();
|
|
985
|
+
});
|
|
986
|
+
} else {
|
|
987
|
+
app.use(helmetMw);
|
|
988
|
+
}
|
|
989
|
+
} else {
|
|
990
|
+
app.use(helmetMw);
|
|
991
|
+
}
|
|
642
992
|
}
|
|
643
993
|
|
|
644
994
|
// HTTP Response Compression
|
|
@@ -668,23 +1018,34 @@ function createApp(options = {}) {
|
|
|
668
1018
|
// Ensure extended query parser for nested filter/sort query parameters (?filter[field]=val)
|
|
669
1019
|
app.set('query parser', 'extended');
|
|
670
1020
|
|
|
1021
|
+
// Fast hardware-accelerated Weak ETag (CRC32 + Hex length) as default
|
|
1022
|
+
// Configurable via options.etag or options.server.etag
|
|
1023
|
+
const etagSetting = options.etag !== undefined
|
|
1024
|
+
? options.etag
|
|
1025
|
+
: (options.server?.etag !== undefined ? options.server.etag : 'fast');
|
|
1026
|
+
|
|
1027
|
+
if (etagSetting === false || etagSetting === null) {
|
|
1028
|
+
app.set('etag', false);
|
|
1029
|
+
} else if (etagSetting === 'fast' || etagSetting === true) {
|
|
1030
|
+
app.set('etag', fastETag);
|
|
1031
|
+
} else {
|
|
1032
|
+
app.set('etag', etagSetting);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
671
1035
|
// JSON body parser for API routes
|
|
672
1036
|
app.use(express.json());
|
|
673
1037
|
app.use(express.urlencoded({ extended: true }));
|
|
674
1038
|
|
|
675
1039
|
// Ensure default empty object for body on mutating requests if not populated
|
|
1040
|
+
// and halt processing if request has timed out (after body parsers)
|
|
676
1041
|
app.use((req, res, next) => {
|
|
1042
|
+
if (timeoutConfig !== false && req.timedout) return;
|
|
677
1043
|
if (req.body === undefined && (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH')) {
|
|
678
1044
|
req.body = {};
|
|
679
1045
|
}
|
|
680
1046
|
next();
|
|
681
1047
|
});
|
|
682
1048
|
|
|
683
|
-
// Halt processing if request has timed out (after body parsers)
|
|
684
|
-
if (timeoutConfig !== false) {
|
|
685
|
-
app.use(haltOnTimedout);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
1049
|
// Authentication middleware (if auth manager provided)
|
|
689
1050
|
let authMiddleware = null;
|
|
690
1051
|
if (authManager) {
|
|
@@ -713,18 +1074,70 @@ function createApp(options = {}) {
|
|
|
713
1074
|
|
|
714
1075
|
mountClientRuntime(app, clientRuntime);
|
|
715
1076
|
|
|
1077
|
+
// Development Hot / Live Reload Hub (SSE & CSS hot swap & Template Live Reload)
|
|
1078
|
+
const devReloadOption = options.devReload;
|
|
1079
|
+
const isDevReloadExplicit = devReloadOption === true || (typeof devReloadOption === 'object' && devReloadOption.enabled === true);
|
|
1080
|
+
const isDevReloadDisabled = devReloadOption === false || (typeof devReloadOption === 'object' && devReloadOption.enabled === false);
|
|
1081
|
+
const devReloadEnabled = isDevReloadExplicit || (!isDevReloadDisabled && isDev && !isTest);
|
|
1082
|
+
|
|
1083
|
+
const devReloadHub = createDevReloadHub({
|
|
1084
|
+
app,
|
|
1085
|
+
publicDir,
|
|
1086
|
+
pagesDir,
|
|
1087
|
+
viewsDir,
|
|
1088
|
+
options: typeof devReloadOption === 'object' ? devReloadOption : {},
|
|
1089
|
+
enabled: devReloadEnabled,
|
|
1090
|
+
logger: logging ? console : null,
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
if (devReloadHub) {
|
|
1094
|
+
app.devReloadHub = devReloadHub;
|
|
1095
|
+
app.broadcastDevReload = (type, payload) => devReloadHub.broadcast(type, payload);
|
|
1096
|
+
if (shutdownManager) {
|
|
1097
|
+
shutdownManager.registerDisposer('devReload', () => devReloadHub.close());
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Client-side JS incremental bundler (esbuild context() API)
|
|
1102
|
+
const bundleOption = options.bundle;
|
|
1103
|
+
const isBundleExplicit = bundleOption === true || (typeof bundleOption === 'object' && bundleOption.enabled !== false);
|
|
1104
|
+
const isBundleDisabled = bundleOption === false;
|
|
1105
|
+
|
|
1106
|
+
if (isBundleExplicit && !isBundleDisabled) {
|
|
1107
|
+
const bundlerOpts = typeof bundleOption === 'object' ? { ...bundleOption } : {};
|
|
1108
|
+
if (!bundlerOpts.cwd) bundlerOpts.cwd = rootDir;
|
|
1109
|
+
const devBundler = createDevBundler(bundlerOpts, devReloadHub);
|
|
1110
|
+
app.devBundler = devBundler;
|
|
1111
|
+
|
|
1112
|
+
if (devBundler.isAvailable() && bundlerOpts.watch !== false && isDev && !isTest) {
|
|
1113
|
+
devBundler.startWatch().catch((err) => {
|
|
1114
|
+
if (logging) console.error('[Webspresso Bundler] Watch error:', err.message);
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (shutdownManager) {
|
|
1119
|
+
shutdownManager.registerDisposer('devBundler', () => devBundler.dispose());
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
716
1123
|
// Configure Nunjucks with viewsDir priority and circular extension guard
|
|
717
1124
|
const templateDirs = [viewsDir, pagesDir].filter(Boolean);
|
|
718
1125
|
if (templateDirs.length === 0) {
|
|
719
1126
|
templateDirs.push(path.join(rootDir, 'pages'));
|
|
720
1127
|
}
|
|
721
1128
|
|
|
1129
|
+
const nunjucksOpts = options.nunjucks || {};
|
|
722
1130
|
const nunjucksEnv = configureSafeNunjucks(templateDirs, {
|
|
723
1131
|
autoescape: true,
|
|
724
1132
|
express: app,
|
|
725
1133
|
watch: isDev && !isTest,
|
|
726
|
-
noCache: isDev || isTest
|
|
1134
|
+
noCache: nunjucksOpts.noCache ?? (isDev || isTest),
|
|
1135
|
+
...nunjucksOpts,
|
|
727
1136
|
});
|
|
1137
|
+
|
|
1138
|
+
if (devReloadHub && typeof devReloadHub.attachNunjucks === 'function') {
|
|
1139
|
+
devReloadHub.attachNunjucks(nunjucksEnv);
|
|
1140
|
+
}
|
|
728
1141
|
|
|
729
1142
|
// Add custom Nunjucks filters
|
|
730
1143
|
nunjucksEnv.addFilter('json', (obj) => {
|
|
@@ -794,16 +1207,18 @@ function createApp(options = {}) {
|
|
|
794
1207
|
const pluginContext = { app, nunjucksEnv, options, middlewares, shutdownManager };
|
|
795
1208
|
pluginManager.registerSync(plugins, pluginContext);
|
|
796
1209
|
|
|
797
|
-
// Request logging middleware
|
|
1210
|
+
// Request logging & query profiling middleware
|
|
798
1211
|
if (logging) {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1212
|
+
const profilerMiddleware = createDevProfilerMiddleware({
|
|
1213
|
+
db: options.db ?? null,
|
|
1214
|
+
logger: options.logger || console,
|
|
1215
|
+
detectNPlusOne: devOpts.nPlusOne !== false,
|
|
1216
|
+
thresholdNPlusOne: devOpts.nPlusOneThreshold ?? 4,
|
|
1217
|
+
slowQueryMs: devOpts.slowQueryMs ?? 50,
|
|
1218
|
+
verbose: devOpts.verbose ?? false,
|
|
1219
|
+
colors: devOpts.colors !== false,
|
|
806
1220
|
});
|
|
1221
|
+
app.use(profilerMiddleware);
|
|
807
1222
|
}
|
|
808
1223
|
|
|
809
1224
|
// 1. Scan and compile discovered routes (src/pages, src/api, modules/*)
|
|
@@ -827,6 +1242,86 @@ function createApp(options = {}) {
|
|
|
827
1242
|
console.log('\nMounting routes:');
|
|
828
1243
|
}
|
|
829
1244
|
|
|
1245
|
+
// Fast-Path Static & Dynamic Route Dispatcher ($O(1) direct static & dynamic LRU lookup)
|
|
1246
|
+
// Eliminates Express linear layer regex matching (matchLayer) for static, dynamic, and 404 routes
|
|
1247
|
+
app.use(function fastPathDispatcher(req, res, next) {
|
|
1248
|
+
const rawPath = req.path || (req.url ? req.url.split('?')[0] : '/');
|
|
1249
|
+
const pathname = rawPath.length > 1 && rawPath.endsWith('/') ? rawPath.slice(0, -1) : rawPath;
|
|
1250
|
+
const method = req.method ? req.method.toUpperCase() : 'GET';
|
|
1251
|
+
|
|
1252
|
+
function getHandlersWithMiddlewares(routeHandlers) {
|
|
1253
|
+
if (prefixMiddlewares.length === 0) return routeHandlers;
|
|
1254
|
+
const applicable = [];
|
|
1255
|
+
for (let i = 0; i < prefixMiddlewares.length; i++) {
|
|
1256
|
+
const pm = prefixMiddlewares[i];
|
|
1257
|
+
if (pathname === pm.prefix || pathname.startsWith(pm.prefix + '/')) {
|
|
1258
|
+
applicable.push(...pm.handlers);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return applicable.length > 0 ? [...applicable, ...routeHandlers] : routeHandlers;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// 1. Static fast-path ($O(1) partitioned by method - zero string concat)
|
|
1265
|
+
const methodMap = fastPathStaticByMethod[method];
|
|
1266
|
+
let entry = methodMap ? methodMap.get(pathname) : undefined;
|
|
1267
|
+
if (!entry && method === 'HEAD') {
|
|
1268
|
+
entry = fastPathStaticByMethod.GET ? fastPathStaticByMethod.GET.get(pathname) : undefined;
|
|
1269
|
+
}
|
|
1270
|
+
if (entry) {
|
|
1271
|
+
if (!req.params) req.params = {};
|
|
1272
|
+
req.route = entry.routeInfo;
|
|
1273
|
+
return dispatchFastPath(getHandlersWithMiddlewares(entry.handlers), req, res, next);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// 2. Dynamic route fast-path with LRU caching
|
|
1277
|
+
if (dynamicRouteList.length > 0) {
|
|
1278
|
+
const cacheKey = `${method} ${pathname}`;
|
|
1279
|
+
const cached = getDynamicCache(cacheKey);
|
|
1280
|
+
|
|
1281
|
+
if (cached === NOT_FOUND_SENTINEL) {
|
|
1282
|
+
return next();
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (cached) {
|
|
1286
|
+
req.params = Object.assign({}, cached.params);
|
|
1287
|
+
req.route = cached.entry.routeInfo;
|
|
1288
|
+
return dispatchFastPath(getHandlersWithMiddlewares(cached.entry.handlers), req, res, next);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (method === 'HEAD') {
|
|
1292
|
+
const cachedGet = getDynamicCache(`GET ${pathname}`);
|
|
1293
|
+
if (cachedGet && cachedGet !== NOT_FOUND_SENTINEL) {
|
|
1294
|
+
req.params = Object.assign({}, cachedGet.params);
|
|
1295
|
+
req.route = cachedGet.entry.routeInfo;
|
|
1296
|
+
return dispatchFastPath(getHandlersWithMiddlewares(cachedGet.entry.handlers), req, res, next);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Linear pattern scan over registered dynamic routes (only once per unique pathname)
|
|
1301
|
+
for (let i = 0; i < dynamicRouteList.length; i++) {
|
|
1302
|
+
const entry = dynamicRouteList[i];
|
|
1303
|
+
if (entry.method !== method && !(method === 'HEAD' && entry.method === 'GET')) {
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const match = pathname.match(entry.compiled.regex);
|
|
1308
|
+
if (match) {
|
|
1309
|
+
const params = extractRouteParams(entry.compiled.paramNames, match);
|
|
1310
|
+
setDynamicCache(cacheKey, { entry, params });
|
|
1311
|
+
|
|
1312
|
+
req.params = Object.assign({}, params);
|
|
1313
|
+
req.route = entry.routeInfo;
|
|
1314
|
+
return dispatchFastPath(getHandlersWithMiddlewares(entry.handlers), req, res, next);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Memoize as not found so subsequent requests for this path bypass regex scanning
|
|
1319
|
+
setDynamicCache(cacheKey, NOT_FOUND_SENTINEL);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
next();
|
|
1323
|
+
});
|
|
1324
|
+
|
|
830
1325
|
// Mount discovered routes
|
|
831
1326
|
const { routeMetadata: discoveredMeta, registerDynamicDiscoveredRoutes } = mountDiscoveredRoutes(app, routeTable, {
|
|
832
1327
|
nunjucks: nunjucksEnv,
|
|
@@ -969,7 +1464,7 @@ function createApp(options = {}) {
|
|
|
969
1464
|
: null;
|
|
970
1465
|
|
|
971
1466
|
// 404 handler
|
|
972
|
-
|
|
1467
|
+
const notFoundHandler = async (req, res) => {
|
|
973
1468
|
res.status(404);
|
|
974
1469
|
const ctx = createErrorContext(req);
|
|
975
1470
|
|
|
@@ -1015,7 +1510,8 @@ function createApp(options = {}) {
|
|
|
1015
1510
|
} else {
|
|
1016
1511
|
res.json({ error: 'Not Found', status: 404 });
|
|
1017
1512
|
}
|
|
1018
|
-
}
|
|
1513
|
+
};
|
|
1514
|
+
app.use(notFoundHandler);
|
|
1019
1515
|
|
|
1020
1516
|
let customErrorHandler = null;
|
|
1021
1517
|
app.setErrorHandler = function(handler) {
|
|
@@ -1163,6 +1659,25 @@ function createApp(options = {}) {
|
|
|
1163
1659
|
app.listen = function(...args) {
|
|
1164
1660
|
const server = origListen(...args);
|
|
1165
1661
|
app.server = server;
|
|
1662
|
+
|
|
1663
|
+
// HTTP Server Socket Tuning: prevent 502 race conditions behind Cloudflare / Nginx / AWS ALB
|
|
1664
|
+
// Node.js defaults keepAliveTimeout to 5s, whereas Cloudflare/ALB default to 60s idle keep-alive.
|
|
1665
|
+
// Note: Node requires headersTimeout > keepAliveTimeout.
|
|
1666
|
+
const serverOpts = options.server || {};
|
|
1667
|
+
const keepAliveTimeout = serverOpts.keepAliveTimeout != null
|
|
1668
|
+
? serverOpts.keepAliveTimeout
|
|
1669
|
+
: 65000;
|
|
1670
|
+
const headersTimeout = serverOpts.headersTimeout != null
|
|
1671
|
+
? serverOpts.headersTimeout
|
|
1672
|
+
: Math.max(keepAliveTimeout + 1000, 66000);
|
|
1673
|
+
|
|
1674
|
+
if (typeof server.keepAliveTimeout === 'number') {
|
|
1675
|
+
server.keepAliveTimeout = keepAliveTimeout;
|
|
1676
|
+
}
|
|
1677
|
+
if (typeof server.headersTimeout === 'number') {
|
|
1678
|
+
server.headersTimeout = headersTimeout;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1166
1681
|
const adapter = new NodeHttpAdapter(server);
|
|
1167
1682
|
shutdownManager.registerAdapter(adapter);
|
|
1168
1683
|
|
|
@@ -1170,11 +1685,62 @@ function createApp(options = {}) {
|
|
|
1170
1685
|
shutdownManager.enableShutdownHooks();
|
|
1171
1686
|
}
|
|
1172
1687
|
|
|
1688
|
+
// Development startup banner
|
|
1689
|
+
const shouldShowBanner =
|
|
1690
|
+
devOpts.banner !== false &&
|
|
1691
|
+
options.banner !== false &&
|
|
1692
|
+
isDev &&
|
|
1693
|
+
!isTest;
|
|
1694
|
+
|
|
1695
|
+
if (shouldShowBanner) {
|
|
1696
|
+
server.once('listening', () => {
|
|
1697
|
+
printDevBanner({
|
|
1698
|
+
app,
|
|
1699
|
+
server,
|
|
1700
|
+
routeTable,
|
|
1701
|
+
options,
|
|
1702
|
+
db: options.db ?? null,
|
|
1703
|
+
serviceRegistry,
|
|
1704
|
+
pluginManager,
|
|
1705
|
+
logger: options.logger || console,
|
|
1706
|
+
showRouteTable: devOpts.routeTable !== false && options.routeTable !== false,
|
|
1707
|
+
});
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1173
1711
|
return server;
|
|
1174
1712
|
};
|
|
1713
|
+
|
|
1714
|
+
app.printDevBanner = function(opts = {}) {
|
|
1715
|
+
printDevBanner({
|
|
1716
|
+
app,
|
|
1717
|
+
server: app.server,
|
|
1718
|
+
routeTable,
|
|
1719
|
+
options,
|
|
1720
|
+
db: options.db ?? null,
|
|
1721
|
+
serviceRegistry,
|
|
1722
|
+
pluginManager,
|
|
1723
|
+
logger: options.logger || console,
|
|
1724
|
+
showRouteTable: true,
|
|
1725
|
+
...opts,
|
|
1726
|
+
});
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
app.formatDevBanner = function(opts = {}) {
|
|
1730
|
+
return formatDevBanner({
|
|
1731
|
+
app,
|
|
1732
|
+
server: app.server,
|
|
1733
|
+
routeTable,
|
|
1734
|
+
options,
|
|
1735
|
+
db: options.db ?? null,
|
|
1736
|
+
serviceRegistry,
|
|
1737
|
+
pluginManager,
|
|
1738
|
+
...opts,
|
|
1739
|
+
});
|
|
1740
|
+
};
|
|
1175
1741
|
|
|
1176
1742
|
return { app, nunjucksEnv, pluginManager, authMiddleware, shutdownManager };
|
|
1177
1743
|
}
|
|
1178
1744
|
|
|
1179
1745
|
// Export for use as library
|
|
1180
|
-
module.exports = { createApp };
|
|
1746
|
+
module.exports = { createApp, fastETag };
|