webspresso 0.0.105 → 0.0.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/AGENTS.md +15 -2
- package/.agents/CLI.md +219 -7
- package/.agents/FLAGS.md +118 -0
- package/.agents/LOGGING.md +93 -0
- package/.agents/METRICS.md +127 -0
- package/.agents/MIGRATION_JOBS.md +95 -0
- package/.agents/PLUGINS.md +10 -3
- package/.agents/QUEUE.md +42 -0
- package/.agents/RATE_LIMIT.md +147 -0
- package/.agents/SCALE_OUT.md +109 -0
- package/.agents/SECRETS.md +117 -0
- package/AGENTS.md +10 -0
- package/bin/commands/add-aws.js +396 -0
- package/bin/commands/add-ci.js +523 -0
- package/bin/commands/add-cloudflare.js +240 -0
- package/bin/commands/add-cloudrun.js +381 -0
- package/bin/commands/add-migration-job.js +396 -0
- package/bin/commands/assets-sync.js +209 -0
- package/bin/commands/client-generate.js +52 -0
- package/bin/commands/console.js +53 -0
- package/bin/commands/db-migrate.js +47 -2
- package/bin/commands/db-studio.js +84 -0
- package/bin/commands/deploy-check.js +242 -13
- package/bin/commands/export.js +87 -0
- package/bin/commands/secrets-pull.js +208 -0
- package/bin/utils/db.js +7 -1
- package/bin/webspresso.js +39 -7
- package/core/app.js +453 -0
- package/core/auth/authenticate.js +93 -0
- package/core/auth/index.js +38 -5
- package/core/auth/jwt.js +85 -13
- package/core/auth/magic-link.js +23 -5
- package/core/auth/manager.js +57 -6
- package/core/auth/middleware.js +16 -68
- package/core/auth/redis-session.js +1045 -0
- package/core/auth/session-manager.js +28 -2
- package/core/auth/sessions.js +308 -22
- package/core/cache/cdn.js +296 -0
- package/core/cache/index.js +12 -0
- package/core/cache/tagged-lru.js +9 -1
- package/core/console/commands.js +148 -0
- package/core/console/context.js +260 -0
- package/core/console/eval.js +73 -0
- package/core/console/index.js +22 -0
- package/core/console/repl.js +102 -0
- package/core/content/service.js +1 -0
- package/core/errors/domain.js +20 -1
- package/core/errors/http.js +1 -0
- package/core/events/dispatcher.js +7 -0
- package/core/events/signal.js +7 -0
- package/core/export/discovery.js +191 -0
- package/core/export/exporter.js +277 -0
- package/core/export/index.js +24 -0
- package/core/flags/evaluator.js +252 -0
- package/core/flags/hasher.js +37 -0
- package/core/flags/index.js +21 -0
- package/core/flags/manager.js +263 -0
- package/core/flags/store.js +126 -0
- package/core/health/middleware.js +3 -3
- package/core/http/adapter.js +74 -0
- package/core/http/express-adapter.js +46 -0
- package/core/http/hono-adapter.js +531 -0
- package/core/http/index.js +41 -0
- package/core/http/pipeline.js +519 -0
- package/core/index.js +347 -0
- package/core/limits/middleware.js +9 -1
- package/core/logger/formatters.js +264 -0
- package/core/logger/index.js +260 -0
- package/core/logger/trace.js +137 -0
- package/core/mail/mailer.js +1 -1
- package/core/metrics/collectors.js +125 -0
- package/core/metrics/index.js +45 -0
- package/core/metrics/middleware.js +230 -0
- package/core/metrics/registry.js +366 -0
- package/core/orm/d1.js +265 -0
- package/core/orm/eager-loader.js +81 -1
- package/core/orm/index.js +122 -8
- package/core/orm/json-fields.js +44 -13
- package/core/orm/migrate-cloud.js +194 -0
- package/core/orm/model.js +12 -0
- package/core/orm/pool.js +97 -0
- package/core/orm/query-builder.js +42 -15
- package/core/orm/repository.js +59 -12
- package/core/orm/scopes.js +54 -4
- package/core/queue/adapters/cloudflare.js +131 -0
- package/core/queue/adapters/pubsub.js +400 -0
- package/core/queue/adapters/sqs.js +419 -0
- package/core/queue/adapters/wait-until.js +77 -0
- package/core/queue/index.js +8 -0
- package/core/queue/manager.js +11 -0
- package/core/rate-limit/index.js +22 -0
- package/core/rate-limit/limiter.js +219 -0
- package/core/rate-limit/stores/memory.js +129 -0
- package/core/rate-limit/stores/redis.js +151 -0
- package/core/rate-limit/utils.js +112 -0
- package/core/resilience/circuit-breaker.js +31 -0
- package/core/secrets/index.js +113 -0
- package/core/secrets/manager.js +308 -0
- package/core/secrets/providers/aws.js +288 -0
- package/core/secrets/providers/env.js +44 -0
- package/core/secrets/providers/gcp.js +307 -0
- package/core/secrets/providers/memory.js +76 -0
- package/core/services/index.js +9 -0
- package/core/shutdown/shutdown-manager.js +66 -38
- package/core/stream/index.js +19 -0
- package/core/stream/text-stream.js +326 -0
- package/core/studio/index.js +43 -0
- package/core/studio/inspector.js +266 -0
- package/core/studio/server.js +341 -0
- package/core/studio/ui.js +681 -0
- package/core/utils/string.js +77 -16
- package/core/validation/index.js +18 -0
- package/index.d.ts +1106 -10
- package/index.js +129 -3
- package/package.json +20 -8
- package/plugins/admin-panel/client/parts/07-model-list.js +5 -2
- package/plugins/admin-panel/index.js +36 -16
- package/plugins/admin-panel/modules/user-management.js +3 -2
- package/plugins/api-key/index.js +3 -2
- package/plugins/audit-log/index.js +3 -1
- package/plugins/basic-auth/index.js +3 -2
- package/plugins/cors.js +3 -2
- package/plugins/csrf/index.js +6 -4
- package/plugins/csv/index.js +8 -5
- package/plugins/dashboard/index.js +27 -7
- package/plugins/email/index.js +11 -9
- package/plugins/index.js +3 -0
- package/plugins/queue/index.js +18 -2
- package/plugins/redirect/index.js +7 -2
- package/plugins/seo-checker/index.js +2 -2
- package/plugins/site-analytics/index.js +3 -1
- package/plugins/swagger.js +8 -2
- package/plugins/upload/gcs-provider.js +148 -0
- package/plugins/upload/index.js +132 -74
- package/plugins/upload/s3-provider.js +257 -0
- package/plugins/webhooks/index.js +27 -23
- package/plugins/xlsx/index.js +7 -5
- package/src/api/api-loader.js +12 -95
- package/src/api/define-api.js +1 -0
- package/src/app-context.js +9 -1
- package/src/client-runtime/constants.js +10 -0
- package/src/client-runtime/mount.js +2 -3
- package/src/dev/client-gen.js +458 -0
- package/src/discovery/scan-routes.js +5 -2
- package/src/file-router.js +14 -3
- package/src/helpers.js +53 -0
- package/src/pages/page-loader.js +12 -205
- package/src/plugin-manager.js +280 -47
- package/src/server.js +253 -169
- package/src/services/discovery.js +9 -1
- package/src/services/executor.js +271 -49
- package/src/services/index.js +52 -3
- package/src/services/loader.js +120 -0
- package/src/services/memoize.js +223 -49
- package/src/services/registry.js +501 -95
- package/src/services/validator.js +202 -33
package/src/server.js
CHANGED
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
createTimeoutMiddleware,
|
|
18
18
|
} = require('../core/utils/timeout');
|
|
19
19
|
|
|
20
|
+
const { createCoreApp } = require('../core/app');
|
|
20
21
|
const { setAppContext } = require('./app-context');
|
|
21
22
|
const { mountClientRuntime } = require('./client-runtime/mount');
|
|
22
23
|
const { resolveClientRuntime } = require('./client-runtime/resolve');
|
|
@@ -71,15 +72,18 @@ const renderStackStorage = new AsyncLocalStorage();
|
|
|
71
72
|
|
|
72
73
|
let isWebspressoPackageCache = null;
|
|
73
74
|
function checkIsWebspressoPackage() {
|
|
75
|
+
if (process.env.__WEBSPRESSO_TESTING__ === 'true') return true;
|
|
74
76
|
if (isWebspressoPackageCache === null) {
|
|
75
77
|
try {
|
|
76
78
|
const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
|
|
77
|
-
|
|
79
|
+
if (pkg && pkg.name === 'webspresso') {
|
|
80
|
+
isWebspressoPackageCache = true;
|
|
81
|
+
}
|
|
78
82
|
} catch (_) {
|
|
79
83
|
isWebspressoPackageCache = false;
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
|
-
return isWebspressoPackageCache;
|
|
86
|
+
return isWebspressoPackageCache === true;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
/**
|
|
@@ -623,7 +627,7 @@ function createApp(options = {}) {
|
|
|
623
627
|
attachProfilerToKnex(options.db.knex);
|
|
624
628
|
}
|
|
625
629
|
|
|
626
|
-
if (!initialPagesDir && !options.pages && !options.api && !options.modules && !options.routes && !options.rootDir) {
|
|
630
|
+
if (!initialPagesDir && !options.pages && !options.api && !options.modules && !options.routes && !options.rootDir && options.adapter !== 'hono') {
|
|
627
631
|
throw new Error('pagesDir is required');
|
|
628
632
|
}
|
|
629
633
|
|
|
@@ -634,6 +638,7 @@ function createApp(options = {}) {
|
|
|
634
638
|
}
|
|
635
639
|
return process.cwd();
|
|
636
640
|
})();
|
|
641
|
+
|
|
637
642
|
let pagesDir = initialPagesDir;
|
|
638
643
|
if (!pagesDir) {
|
|
639
644
|
if (typeof options.pages === 'string') {
|
|
@@ -652,8 +657,14 @@ function createApp(options = {}) {
|
|
|
652
657
|
// Create Express application instance
|
|
653
658
|
const app = express();
|
|
654
659
|
|
|
655
|
-
// Create
|
|
656
|
-
const
|
|
660
|
+
// Create or adopt Core Application instance (services, db, events, cache, breakers, shutdown)
|
|
661
|
+
const core = options.core || createCoreApp({ ...options, plugins: [] });
|
|
662
|
+
const pluginManager = core.pluginManager;
|
|
663
|
+
const shutdownManager = core.shutdownManager;
|
|
664
|
+
const appBreakers = core.breakers;
|
|
665
|
+
const serviceRegistry = core.serviceRegistry;
|
|
666
|
+
const eventDispatcher = core.events;
|
|
667
|
+
const appCache = core.cache;
|
|
657
668
|
|
|
658
669
|
// Configure asset manager
|
|
659
670
|
const projectBase = publicDir ? path.dirname(path.resolve(publicDir)) : process.cwd();
|
|
@@ -676,98 +687,7 @@ function createApp(options = {}) {
|
|
|
676
687
|
...(options.shutdown || options.server?.shutdown || {}),
|
|
677
688
|
};
|
|
678
689
|
|
|
679
|
-
const shutdownManager = new ShutdownManager({
|
|
680
|
-
...shutdownConfig,
|
|
681
|
-
logger: logging ? console : null,
|
|
682
|
-
});
|
|
683
|
-
|
|
684
690
|
try {
|
|
685
|
-
// Circuit Breakers Layer initialization (isolated per application instance)
|
|
686
|
-
const appBreakers = options.breakers instanceof CircuitBreakerRegistry
|
|
687
|
-
? options.breakers
|
|
688
|
-
: new CircuitBreakerRegistry();
|
|
689
|
-
app.breakers = appBreakers;
|
|
690
|
-
app.getBreaker = (name, opts) => appBreakers.getBreaker(name, opts);
|
|
691
|
-
app.breaker = (name, opts) => appBreakers.getBreaker(name, opts);
|
|
692
|
-
shutdownManager.onShutdown(() => {
|
|
693
|
-
appBreakers.resetAll();
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
// Services Layer initialization
|
|
697
|
-
const servicesDirOption = options.servicesDir ?? options.services?.dir;
|
|
698
|
-
const resolvedServicesDir = servicesDirOption
|
|
699
|
-
? path.resolve(servicesDirOption)
|
|
700
|
-
: (fs.existsSync(path.join(process.cwd(), 'services'))
|
|
701
|
-
? path.join(process.cwd(), 'services')
|
|
702
|
-
: null);
|
|
703
|
-
|
|
704
|
-
const serviceRegistry = createServiceRegistry({
|
|
705
|
-
servicesDir: resolvedServicesDir,
|
|
706
|
-
isDev,
|
|
707
|
-
logger: logging ? console : null,
|
|
708
|
-
timeouts,
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
// Auto-register built-in auth services if database is present and service is not custom-defined
|
|
712
|
-
if (options.db) {
|
|
713
|
-
const { createAuthServices } = require('./services/builtins/auth');
|
|
714
|
-
const authServices = createAuthServices({ db: options.db });
|
|
715
|
-
for (const [name, def] of Object.entries(authServices)) {
|
|
716
|
-
if (!serviceRegistry.has(name)) {
|
|
717
|
-
serviceRegistry.register(name, def);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Auto-register built-in system services (health, cleanup, cache flush, info)
|
|
723
|
-
const { createSystemServices } = require('./services/builtins/system');
|
|
724
|
-
const systemServices = createSystemServices({
|
|
725
|
-
db: options.db ?? null,
|
|
726
|
-
serviceRegistry,
|
|
727
|
-
pluginManager,
|
|
728
|
-
});
|
|
729
|
-
for (const [name, def] of Object.entries(systemServices)) {
|
|
730
|
-
if (!serviceRegistry.has(name)) {
|
|
731
|
-
serviceRegistry.register(name, def);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Events Subsystem (AdonisJS Events & Django Signals-inspired)
|
|
736
|
-
const { createEventDispatcher, loadListeners } = require('../core/events');
|
|
737
|
-
const eventDispatcher = options.events || createEventDispatcher({
|
|
738
|
-
queue: options.queue || null,
|
|
739
|
-
logger: logging ? console : null,
|
|
740
|
-
});
|
|
741
|
-
shutdownManager.onShutdown(() => {
|
|
742
|
-
eventDispatcher.removeAllListeners();
|
|
743
|
-
});
|
|
744
|
-
|
|
745
|
-
// Auto-bridge ORM lifecycle events (ModelEvents) to app.events ('model:created', etc.)
|
|
746
|
-
if (options.db) {
|
|
747
|
-
const { ModelEvents } = require('../core/orm/events');
|
|
748
|
-
eventDispatcher.bridgeOrm(ModelEvents);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// Auto-discover listeners from listeners/ or options.listenersDir
|
|
752
|
-
const listenersDirOption = options.listenersDir ?? options.listeners?.dir;
|
|
753
|
-
const resolvedListenersDir = listenersDirOption
|
|
754
|
-
? path.resolve(listenersDirOption)
|
|
755
|
-
: (fs.existsSync(path.join(rootDir, 'listeners'))
|
|
756
|
-
? path.join(rootDir, 'listeners')
|
|
757
|
-
: null);
|
|
758
|
-
if (resolvedListenersDir) {
|
|
759
|
-
loadListeners(resolvedListenersDir, eventDispatcher, { logger: logging ? console : null });
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// Native High-Performance In-Memory Cache (LRU + Tagged Invalidation + Byte Budget)
|
|
763
|
-
const { createTaggedLruCache } = require('../core/cache');
|
|
764
|
-
const cacheOpt = options.cache ?? options.server?.cache;
|
|
765
|
-
const appCache = cacheOpt === false ? null : createTaggedLruCache(typeof cacheOpt === 'object' ? cacheOpt : {});
|
|
766
|
-
|
|
767
|
-
if (appCache) {
|
|
768
|
-
shutdownManager.onShutdown(() => appCache.clear());
|
|
769
|
-
}
|
|
770
|
-
|
|
771
691
|
setAppContext({
|
|
772
692
|
db: options.db ?? null,
|
|
773
693
|
shutdownManager,
|
|
@@ -776,17 +696,31 @@ function createApp(options = {}) {
|
|
|
776
696
|
cache: appCache,
|
|
777
697
|
});
|
|
778
698
|
|
|
699
|
+
app.core = core;
|
|
779
700
|
app.serviceRegistry = serviceRegistry;
|
|
780
701
|
app.pluginManager = pluginManager;
|
|
702
|
+
app.whenReady = () => pluginManager.whenReady();
|
|
781
703
|
app.events = eventDispatcher;
|
|
782
704
|
app.cache = appCache;
|
|
705
|
+
app.breakers = appBreakers;
|
|
706
|
+
app.getBreaker = (name, opts) => core.getBreaker(name, opts);
|
|
707
|
+
app.breaker = (name, opts) => core.breaker(name, opts);
|
|
708
|
+
app.service = (name, input, opts) => core.service(name, input, opts);
|
|
709
|
+
app.service.call = (name, input, opts) => app.service(name, input, opts);
|
|
710
|
+
app.service.execute = (name, input, opts) => app.service(name, input, opts);
|
|
711
|
+
app.service.invalidate = (name, input, ctx) => serviceRegistry.invalidate(name, input, ctx);
|
|
712
|
+
app.service.clearCache = (name) => serviceRegistry.clearCache(name);
|
|
713
|
+
app.service.invalidateByTag = (tag, serviceName) => serviceRegistry.invalidateByTag(tag, serviceName);
|
|
714
|
+
app.service.invalidateByTags = (tags, serviceName) => serviceRegistry.invalidateByTags(tags, serviceName);
|
|
715
|
+
app.service.invalidateTag = app.service.invalidateByTag;
|
|
716
|
+
app.service.invalidateTags = app.service.invalidateByTags;
|
|
783
717
|
if (options.queue) {
|
|
784
718
|
app.queue = options.queue;
|
|
785
719
|
}
|
|
786
720
|
|
|
787
|
-
// Unified Request Context & Lifecycle Initializer middleware
|
|
788
721
|
const { renderStream } = require('../core/ssr/stream');
|
|
789
722
|
const { createSseStream } = require('../core/sse/stream');
|
|
723
|
+
const { streamText, streamJson } = require('../core/stream/text-stream');
|
|
790
724
|
app.use((req, res, next) => {
|
|
791
725
|
// 1. Request draining check during graceful shutdown (allow health/readiness probes through)
|
|
792
726
|
const isHealthEndpoint = req.path === '/healthz' || req.path === '/readyz' || req.path === '/health' ||
|
|
@@ -825,24 +759,27 @@ function createApp(options = {}) {
|
|
|
825
759
|
req.breakers = appBreakers;
|
|
826
760
|
req.breaker = (name, opts) => appBreakers.getBreaker(name, opts);
|
|
827
761
|
req.retry = retry;
|
|
828
|
-
req.context = {
|
|
762
|
+
req.context = core.createContext({
|
|
829
763
|
req,
|
|
830
764
|
res,
|
|
831
|
-
db: options.db ?? null,
|
|
832
|
-
queue: options.queue ?? null,
|
|
833
|
-
events: eventDispatcher,
|
|
834
|
-
cache: appCache,
|
|
835
|
-
breakers: appBreakers,
|
|
836
|
-
getBreaker: (name, opts) => appBreakers.getBreaker(name, opts),
|
|
837
|
-
breaker: (name, opts) => appBreakers.getBreaker(name, opts),
|
|
838
|
-
retry,
|
|
839
765
|
app,
|
|
840
766
|
assetManager: appAssetManager,
|
|
841
|
-
serviceRegistry,
|
|
842
767
|
...(req.context || {}),
|
|
843
|
-
};
|
|
768
|
+
});
|
|
844
769
|
req.service = (name, input, opts) =>
|
|
845
|
-
|
|
770
|
+
core.service(name, input, req.context, opts);
|
|
771
|
+
req.service.call = (name, input, opts) => req.service(name, input, opts);
|
|
772
|
+
req.service.execute = (name, input, opts) => req.service(name, input, opts);
|
|
773
|
+
req.service.invalidate = (name, input, ctx) =>
|
|
774
|
+
serviceRegistry.invalidate(name, input, ctx || req.context);
|
|
775
|
+
req.service.clearCache = (name) =>
|
|
776
|
+
serviceRegistry.clearCache(name);
|
|
777
|
+
req.service.invalidateByTag = (tag, serviceName) =>
|
|
778
|
+
serviceRegistry.invalidateByTag(tag, serviceName);
|
|
779
|
+
req.service.invalidateByTags = (tags, serviceName) =>
|
|
780
|
+
serviceRegistry.invalidateByTags(tags, serviceName);
|
|
781
|
+
req.service.invalidateTag = req.service.invalidateByTag;
|
|
782
|
+
req.service.invalidateTags = req.service.invalidateByTags;
|
|
846
783
|
|
|
847
784
|
// 4. SSR Streaming Response Helper
|
|
848
785
|
res.renderStream = (templatePath, context = {}, renderOptions = {}) => {
|
|
@@ -857,6 +794,15 @@ function createApp(options = {}) {
|
|
|
857
794
|
return createSseStream(req, res, sourceOrOptions, maybeOptions);
|
|
858
795
|
};
|
|
859
796
|
|
|
797
|
+
// 6. Text and NDJSON Streaming Helpers (AI / LLM Tokens)
|
|
798
|
+
res.streamText = (sourceOrCallback, maybeOptions) => {
|
|
799
|
+
return streamText(req, res, sourceOrCallback, maybeOptions);
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
res.streamJson = (sourceOrCallback, maybeOptions) => {
|
|
803
|
+
return streamJson(req, res, sourceOrCallback, maybeOptions);
|
|
804
|
+
};
|
|
805
|
+
|
|
860
806
|
next();
|
|
861
807
|
});
|
|
862
808
|
|
|
@@ -1251,7 +1197,27 @@ function createApp(options = {}) {
|
|
|
1251
1197
|
// HTTP 103 Early Hints
|
|
1252
1198
|
const earlyHintsOpt = options.earlyHints ?? options.server?.earlyHints ?? false;
|
|
1253
1199
|
app.use(createEarlyHintsMiddleware(earlyHintsOpt));
|
|
1254
|
-
|
|
1200
|
+
|
|
1201
|
+
// Structured Cloud Logging & Distributed Trace Context
|
|
1202
|
+
const { createLogger, createLogMiddleware, CloudLogger } = require('../core/logger');
|
|
1203
|
+
let cloudLogger = null;
|
|
1204
|
+
if (options.logger instanceof CloudLogger) {
|
|
1205
|
+
cloudLogger = options.logger;
|
|
1206
|
+
} else if (options.logger && typeof options.logger === 'object' && options.logger !== console) {
|
|
1207
|
+
cloudLogger = createLogger(options.logger);
|
|
1208
|
+
} else if (options.logger === true || (!isTest && (process.env.K_SERVICE || process.env.AWS_EXECUTION_ENV))) {
|
|
1209
|
+
cloudLogger = createLogger(typeof options.logger === 'object' ? options.logger : {});
|
|
1210
|
+
} else if (options.logger) {
|
|
1211
|
+
cloudLogger = options.logger;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
if (cloudLogger) {
|
|
1215
|
+
app.logger = cloudLogger;
|
|
1216
|
+
if (cloudLogger instanceof CloudLogger) {
|
|
1217
|
+
app.use(createLogMiddleware(cloudLogger, typeof options.logger === 'object' ? options.logger : {}));
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1255
1221
|
// Attach normalized timeouts to app and incoming requests
|
|
1256
1222
|
app.set('timeouts', timeouts);
|
|
1257
1223
|
app.use((req, res, next) => {
|
|
@@ -1320,6 +1286,19 @@ function createApp(options = {}) {
|
|
|
1320
1286
|
queue: options.queue,
|
|
1321
1287
|
shutdownManager,
|
|
1322
1288
|
});
|
|
1289
|
+
|
|
1290
|
+
// Cloud Metrics & Distributed Tracing (/metrics)
|
|
1291
|
+
const { setupMetricsMiddleware } = require('../core/metrics');
|
|
1292
|
+
const metricsOpt = options.metrics ?? options.server?.metrics;
|
|
1293
|
+
const metricsManager = setupMetricsMiddleware(app, metricsOpt, {
|
|
1294
|
+
db: options.db,
|
|
1295
|
+
cache: options.cache,
|
|
1296
|
+
breakers: core.breakers,
|
|
1297
|
+
shutdownManager,
|
|
1298
|
+
});
|
|
1299
|
+
if (metricsManager) {
|
|
1300
|
+
app.metrics = metricsManager.registry;
|
|
1301
|
+
}
|
|
1323
1302
|
|
|
1324
1303
|
// Authentication middleware (if auth manager provided)
|
|
1325
1304
|
let authMiddleware = null;
|
|
@@ -1333,6 +1312,27 @@ function createApp(options = {}) {
|
|
|
1333
1312
|
middlewares.jwt = authMiddleware.jwt;
|
|
1334
1313
|
}
|
|
1335
1314
|
|
|
1315
|
+
// Feature Flags & Canary Traffic Splitter
|
|
1316
|
+
const flagsOpt = options.flags;
|
|
1317
|
+
if (flagsOpt) {
|
|
1318
|
+
const { createFeatureFlagManager } = require('../core/flags');
|
|
1319
|
+
const flagManager = createFeatureFlagManager(typeof flagsOpt === 'object' ? flagsOpt : { flags: flagsOpt });
|
|
1320
|
+
app.flags = flagManager;
|
|
1321
|
+
app.use(flagManager.middleware());
|
|
1322
|
+
} else {
|
|
1323
|
+
app.use((req, res, next) => {
|
|
1324
|
+
req.hasFeature = () => false;
|
|
1325
|
+
req.getVariant = () => null;
|
|
1326
|
+
req.flags = {
|
|
1327
|
+
isEnabled: () => false,
|
|
1328
|
+
getVariant: () => null,
|
|
1329
|
+
evaluate: () => ({ enabled: false, variant: null, reason: 'no_flags_configured' }),
|
|
1330
|
+
all: () => ({}),
|
|
1331
|
+
};
|
|
1332
|
+
next();
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
1336
|
// Bouncer authorization middleware (AdonisJS Bouncer & Django Permissions-inspired)
|
|
1337
1337
|
app.use(createBouncerMiddleware());
|
|
1338
1338
|
|
|
@@ -1472,34 +1472,27 @@ function createApp(options = {}) {
|
|
|
1472
1472
|
return d.toString();
|
|
1473
1473
|
});
|
|
1474
1474
|
|
|
1475
|
-
//
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
: (fs.existsSync(path.join(rootDir, 'src/modules')) ? path.join(rootDir, 'src/modules') : (fs.existsSync(path.join(rootDir, 'modules')) ? path.join(rootDir, 'modules') : null));
|
|
1479
|
-
|
|
1480
|
-
if (modulesDirCandidate && fs.existsSync(modulesDirCandidate) && options.modules !== false) {
|
|
1481
|
-
try {
|
|
1482
|
-
const entries = fs.readdirSync(modulesDirCandidate, { withFileTypes: true });
|
|
1483
|
-
for (const entry of entries) {
|
|
1484
|
-
if (entry.isDirectory() && !entry.name.startsWith('_') && !entry.name.startsWith('.')) {
|
|
1485
|
-
const details = discoverModuleDetails(path.join(modulesDirCandidate, entry.name), entry.name);
|
|
1486
|
-
for (const [sName, sDef] of Object.entries(details.services)) {
|
|
1487
|
-
if (!serviceRegistry.has(sName)) {
|
|
1488
|
-
serviceRegistry.register(sName, sDef);
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
for (const [mName, mFn] of Object.entries(details.middlewares)) {
|
|
1492
|
-
middlewares[`${entry.name}.${mName}`] = mFn;
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
} catch (mErr) {
|
|
1497
|
-
console.warn('[webspresso] Warning discovering modules:', mErr.message);
|
|
1498
|
-
}
|
|
1475
|
+
// Merge module middlewares discovered by core
|
|
1476
|
+
if (core.moduleMiddlewares && typeof core.moduleMiddlewares === 'object') {
|
|
1477
|
+
Object.assign(middlewares, core.moduleMiddlewares);
|
|
1499
1478
|
}
|
|
1500
1479
|
|
|
1480
|
+
const { createExpressHttpAdapter } = require('../core/http');
|
|
1481
|
+
const httpAdapter = createExpressHttpAdapter(app, options);
|
|
1482
|
+
app.http = httpAdapter;
|
|
1483
|
+
|
|
1501
1484
|
// Register plugins (sync) — middlewares is the same object later passed to mountPages
|
|
1502
|
-
const pluginContext = {
|
|
1485
|
+
const pluginContext = {
|
|
1486
|
+
app,
|
|
1487
|
+
core,
|
|
1488
|
+
http: httpAdapter,
|
|
1489
|
+
nunjucksEnv,
|
|
1490
|
+
options,
|
|
1491
|
+
middlewares,
|
|
1492
|
+
shutdownManager,
|
|
1493
|
+
events: eventDispatcher,
|
|
1494
|
+
serviceRegistry,
|
|
1495
|
+
};
|
|
1503
1496
|
pluginManager.registerSync(plugins, pluginContext);
|
|
1504
1497
|
|
|
1505
1498
|
// Request logging & query profiling middleware
|
|
@@ -1682,35 +1675,11 @@ function createApp(options = {}) {
|
|
|
1682
1675
|
const allRouteMeta = [...discoveredMeta, ...classicRouteMeta];
|
|
1683
1676
|
pluginManager.setRoutes(allRouteMeta);
|
|
1684
1677
|
|
|
1685
|
-
// Call onRoutesReady hook
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
app,
|
|
1691
|
-
nunjucksEnv,
|
|
1692
|
-
options,
|
|
1693
|
-
middlewares,
|
|
1694
|
-
db: options.db ?? null,
|
|
1695
|
-
routes: pluginManager.routes,
|
|
1696
|
-
usePlugin: (n) => pluginManager.getPluginAPI(n),
|
|
1697
|
-
addHelper: (n, fn) => pluginManager.registeredHelpers.set(n, fn),
|
|
1698
|
-
addFilter: (n, fn) => pluginManager.registeredFilters.set(n, fn),
|
|
1699
|
-
addRoute: (method, path, ...handlers) => {
|
|
1700
|
-
// Log route for debugging (only in development)
|
|
1701
|
-
if (process.env.NODE_ENV !== 'production' && !isTest) {
|
|
1702
|
-
console.log(` ${method.toUpperCase().padEnd(6)} ${path}`);
|
|
1703
|
-
}
|
|
1704
|
-
app[method.toLowerCase()](path, ...handlers);
|
|
1705
|
-
}
|
|
1706
|
-
};
|
|
1707
|
-
try {
|
|
1708
|
-
plugin.onRoutesReady(ctx);
|
|
1709
|
-
} catch (err) {
|
|
1710
|
-
console.warn(`[plugin-manager] Plugin "${name}" onRoutesReady() failed:`, err.message);
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1678
|
+
// Call onRoutesReady hook on plugins with full unified context and mount routes
|
|
1679
|
+
pluginManager.onRoutesReadySync({
|
|
1680
|
+
...pluginContext,
|
|
1681
|
+
pagesDir,
|
|
1682
|
+
});
|
|
1714
1683
|
|
|
1715
1684
|
if (typeof setupRoutes === 'function') {
|
|
1716
1685
|
setupRoutes(app, {
|
|
@@ -1914,9 +1883,17 @@ function createApp(options = {}) {
|
|
|
1914
1883
|
|
|
1915
1884
|
// Log errors based on severity
|
|
1916
1885
|
if (status >= 500) {
|
|
1917
|
-
|
|
1886
|
+
if (req && req.logger && typeof req.logger.error === 'function') {
|
|
1887
|
+
req.logger.error('Server error: ' + (err.message || String(err)), { error: err });
|
|
1888
|
+
} else {
|
|
1889
|
+
console.error('Server error:', err);
|
|
1890
|
+
}
|
|
1918
1891
|
} else if (isDev && status >= 400) {
|
|
1919
|
-
|
|
1892
|
+
if (req && req.logger && typeof req.logger.warn === 'function') {
|
|
1893
|
+
req.logger.warn(`HTTP ${status}: ${normalized.message}`);
|
|
1894
|
+
} else {
|
|
1895
|
+
console.warn(`[webspresso] HTTP ${status}:`, normalized.message);
|
|
1896
|
+
}
|
|
1920
1897
|
}
|
|
1921
1898
|
|
|
1922
1899
|
res.status(status);
|
|
@@ -2062,16 +2039,54 @@ function createApp(options = {}) {
|
|
|
2062
2039
|
...opts,
|
|
2063
2040
|
});
|
|
2064
2041
|
};
|
|
2065
|
-
|
|
2042
|
+
|
|
2043
|
+
const originalHandle = typeof app.handle === 'function' ? app.handle.bind(app) : null;
|
|
2044
|
+
app.handle = async function(reqOrWebReq, resOrEnv, maybeNext) {
|
|
2045
|
+
if (typeof Request !== 'undefined' && reqOrWebReq instanceof Request) {
|
|
2046
|
+
return handleWebRequest(app, reqOrWebReq, resOrEnv);
|
|
2047
|
+
}
|
|
2048
|
+
return originalHandle ? originalHandle(reqOrWebReq, resOrEnv, maybeNext) : null;
|
|
2049
|
+
};
|
|
2050
|
+
|
|
2051
|
+
app.inject = async function(injectOpts = {}) {
|
|
2052
|
+
const method = injectOpts.method || 'GET';
|
|
2053
|
+
const url = injectOpts.url || '/';
|
|
2054
|
+
const req = new Request(`http://localhost${url}`, {
|
|
2055
|
+
method,
|
|
2056
|
+
headers: injectOpts.headers || {},
|
|
2057
|
+
body: injectOpts.body && method !== 'GET' && method !== 'HEAD'
|
|
2058
|
+
? (typeof injectOpts.body === 'string' ? injectOpts.body : JSON.stringify(injectOpts.body))
|
|
2059
|
+
: undefined,
|
|
2060
|
+
});
|
|
2061
|
+
const res = await app.handle(req);
|
|
2062
|
+
const text = await res.text();
|
|
2063
|
+
const headersObj = {};
|
|
2064
|
+
if (res.headers && typeof res.headers.forEach === 'function') {
|
|
2065
|
+
res.headers.forEach((val, key) => {
|
|
2066
|
+
headersObj[key] = val;
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
return {
|
|
2070
|
+
statusCode: res.status,
|
|
2071
|
+
status: res.status,
|
|
2072
|
+
headers: headersObj,
|
|
2073
|
+
body: text,
|
|
2074
|
+
text,
|
|
2075
|
+
json: () => JSON.parse(text),
|
|
2076
|
+
};
|
|
2077
|
+
};
|
|
2078
|
+
|
|
2066
2079
|
Object.assign(app, {
|
|
2067
2080
|
app,
|
|
2081
|
+
core,
|
|
2082
|
+
http: httpAdapter,
|
|
2068
2083
|
nunjucksEnv,
|
|
2069
2084
|
pluginManager,
|
|
2070
2085
|
authMiddleware,
|
|
2071
2086
|
shutdownManager,
|
|
2072
2087
|
events: eventDispatcher,
|
|
2073
2088
|
timeouts,
|
|
2074
|
-
close: (reason) =>
|
|
2089
|
+
close: (reason) => core.close(reason),
|
|
2075
2090
|
});
|
|
2076
2091
|
return app;
|
|
2077
2092
|
} catch (err) {
|
|
@@ -2084,5 +2099,74 @@ function createApp(options = {}) {
|
|
|
2084
2099
|
}
|
|
2085
2100
|
}
|
|
2086
2101
|
|
|
2102
|
+
/**
|
|
2103
|
+
* Handle a Web Standard Request against an Express application and return a Web Standard Response
|
|
2104
|
+
* @param {import('express').Application} app
|
|
2105
|
+
* @param {Request} webReq
|
|
2106
|
+
* @param {Object} [env]
|
|
2107
|
+
* @returns {Promise<Response>}
|
|
2108
|
+
*/
|
|
2109
|
+
async function handleWebRequest(app, webReq, env = {}) {
|
|
2110
|
+
const http = require('http');
|
|
2111
|
+
const { Socket } = require('net');
|
|
2112
|
+
|
|
2113
|
+
return new Promise(async (resolve, reject) => {
|
|
2114
|
+
try {
|
|
2115
|
+
const socket = new Socket();
|
|
2116
|
+
const nodeReq = new http.IncomingMessage(socket);
|
|
2117
|
+
const url = new URL(webReq.url, 'http://localhost');
|
|
2118
|
+
nodeReq.method = webReq.method || 'GET';
|
|
2119
|
+
nodeReq.url = url.pathname + url.search;
|
|
2120
|
+
|
|
2121
|
+
if (webReq.headers && typeof webReq.headers.entries === 'function') {
|
|
2122
|
+
for (const [k, v] of webReq.headers.entries()) {
|
|
2123
|
+
nodeReq.headers[k.toLowerCase()] = v;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
const nodeRes = new http.ServerResponse(nodeReq);
|
|
2128
|
+
const chunks = [];
|
|
2129
|
+
nodeRes.write = (chunk) => {
|
|
2130
|
+
if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2131
|
+
return true;
|
|
2132
|
+
};
|
|
2133
|
+
nodeRes.end = (chunk) => {
|
|
2134
|
+
if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2135
|
+
const bodyBuffer = Buffer.concat(chunks);
|
|
2136
|
+
const headers = new Headers();
|
|
2137
|
+
const rawHeaders = typeof nodeRes.getHeaders === 'function' ? nodeRes.getHeaders() : {};
|
|
2138
|
+
for (const [k, v] of Object.entries(rawHeaders)) {
|
|
2139
|
+
if (Array.isArray(v)) {
|
|
2140
|
+
for (const item of v) headers.append(k, item);
|
|
2141
|
+
} else if (v !== undefined) {
|
|
2142
|
+
headers.set(k, String(v));
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
const response = new Response(bodyBuffer, {
|
|
2146
|
+
status: nodeRes.statusCode || 200,
|
|
2147
|
+
statusText: nodeRes.statusMessage || 'OK',
|
|
2148
|
+
headers,
|
|
2149
|
+
});
|
|
2150
|
+
resolve(response);
|
|
2151
|
+
};
|
|
2152
|
+
|
|
2153
|
+
app(nodeReq, nodeRes, (err) => {
|
|
2154
|
+
if (err) reject(err);
|
|
2155
|
+
else nodeRes.end();
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
if (webReq.body) {
|
|
2159
|
+
try {
|
|
2160
|
+
const arrayBuf = await webReq.arrayBuffer();
|
|
2161
|
+
nodeReq.push(Buffer.from(arrayBuf));
|
|
2162
|
+
} catch (_) {}
|
|
2163
|
+
}
|
|
2164
|
+
nodeReq.push(null);
|
|
2165
|
+
} catch (err) {
|
|
2166
|
+
reject(err);
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2087
2171
|
// Export for use as library
|
|
2088
|
-
module.exports = { createApp, fastETag };
|
|
2172
|
+
module.exports = { createApp, fastETag, handleWebRequest };
|
|
@@ -22,7 +22,15 @@ function isValidServiceFile(filename) {
|
|
|
22
22
|
if (filename.startsWith('.') || filename.startsWith('_')) {
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
|
-
if (
|
|
25
|
+
if (
|
|
26
|
+
filename.endsWith('.test.js') ||
|
|
27
|
+
filename.endsWith('.spec.js') ||
|
|
28
|
+
filename.endsWith('.test.mjs') ||
|
|
29
|
+
filename.endsWith('.spec.mjs') ||
|
|
30
|
+
filename.endsWith('.test.cjs') ||
|
|
31
|
+
filename.endsWith('.spec.cjs') ||
|
|
32
|
+
filename.endsWith('.d.ts')
|
|
33
|
+
) {
|
|
26
34
|
return false;
|
|
27
35
|
}
|
|
28
36
|
const ext = path.extname(filename);
|