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.
Files changed (156) hide show
  1. package/.agents/AGENTS.md +15 -2
  2. package/.agents/CLI.md +219 -7
  3. package/.agents/FLAGS.md +118 -0
  4. package/.agents/LOGGING.md +93 -0
  5. package/.agents/METRICS.md +127 -0
  6. package/.agents/MIGRATION_JOBS.md +95 -0
  7. package/.agents/PLUGINS.md +10 -3
  8. package/.agents/QUEUE.md +42 -0
  9. package/.agents/RATE_LIMIT.md +147 -0
  10. package/.agents/SCALE_OUT.md +109 -0
  11. package/.agents/SECRETS.md +117 -0
  12. package/AGENTS.md +10 -0
  13. package/bin/commands/add-aws.js +396 -0
  14. package/bin/commands/add-ci.js +523 -0
  15. package/bin/commands/add-cloudflare.js +240 -0
  16. package/bin/commands/add-cloudrun.js +381 -0
  17. package/bin/commands/add-migration-job.js +396 -0
  18. package/bin/commands/assets-sync.js +209 -0
  19. package/bin/commands/client-generate.js +52 -0
  20. package/bin/commands/console.js +53 -0
  21. package/bin/commands/db-migrate.js +47 -2
  22. package/bin/commands/db-studio.js +84 -0
  23. package/bin/commands/deploy-check.js +242 -13
  24. package/bin/commands/export.js +87 -0
  25. package/bin/commands/secrets-pull.js +208 -0
  26. package/bin/utils/db.js +7 -1
  27. package/bin/webspresso.js +39 -7
  28. package/core/app.js +453 -0
  29. package/core/auth/authenticate.js +93 -0
  30. package/core/auth/index.js +38 -5
  31. package/core/auth/jwt.js +85 -13
  32. package/core/auth/magic-link.js +23 -5
  33. package/core/auth/manager.js +57 -6
  34. package/core/auth/middleware.js +16 -68
  35. package/core/auth/redis-session.js +1045 -0
  36. package/core/auth/session-manager.js +28 -2
  37. package/core/auth/sessions.js +308 -22
  38. package/core/cache/cdn.js +296 -0
  39. package/core/cache/index.js +12 -0
  40. package/core/cache/tagged-lru.js +9 -1
  41. package/core/console/commands.js +148 -0
  42. package/core/console/context.js +260 -0
  43. package/core/console/eval.js +73 -0
  44. package/core/console/index.js +22 -0
  45. package/core/console/repl.js +102 -0
  46. package/core/content/service.js +1 -0
  47. package/core/errors/domain.js +20 -1
  48. package/core/errors/http.js +1 -0
  49. package/core/events/dispatcher.js +7 -0
  50. package/core/events/signal.js +7 -0
  51. package/core/export/discovery.js +191 -0
  52. package/core/export/exporter.js +277 -0
  53. package/core/export/index.js +24 -0
  54. package/core/flags/evaluator.js +252 -0
  55. package/core/flags/hasher.js +37 -0
  56. package/core/flags/index.js +21 -0
  57. package/core/flags/manager.js +263 -0
  58. package/core/flags/store.js +126 -0
  59. package/core/health/middleware.js +3 -3
  60. package/core/http/adapter.js +74 -0
  61. package/core/http/express-adapter.js +46 -0
  62. package/core/http/hono-adapter.js +531 -0
  63. package/core/http/index.js +41 -0
  64. package/core/http/pipeline.js +519 -0
  65. package/core/index.js +347 -0
  66. package/core/limits/middleware.js +9 -1
  67. package/core/logger/formatters.js +264 -0
  68. package/core/logger/index.js +260 -0
  69. package/core/logger/trace.js +137 -0
  70. package/core/mail/mailer.js +1 -1
  71. package/core/metrics/collectors.js +125 -0
  72. package/core/metrics/index.js +45 -0
  73. package/core/metrics/middleware.js +230 -0
  74. package/core/metrics/registry.js +366 -0
  75. package/core/orm/d1.js +265 -0
  76. package/core/orm/eager-loader.js +81 -1
  77. package/core/orm/index.js +122 -8
  78. package/core/orm/json-fields.js +44 -13
  79. package/core/orm/migrate-cloud.js +194 -0
  80. package/core/orm/model.js +12 -0
  81. package/core/orm/pool.js +97 -0
  82. package/core/orm/query-builder.js +42 -15
  83. package/core/orm/repository.js +59 -12
  84. package/core/orm/scopes.js +54 -4
  85. package/core/queue/adapters/cloudflare.js +131 -0
  86. package/core/queue/adapters/pubsub.js +400 -0
  87. package/core/queue/adapters/sqs.js +419 -0
  88. package/core/queue/adapters/wait-until.js +77 -0
  89. package/core/queue/index.js +8 -0
  90. package/core/queue/manager.js +11 -0
  91. package/core/rate-limit/index.js +22 -0
  92. package/core/rate-limit/limiter.js +219 -0
  93. package/core/rate-limit/stores/memory.js +129 -0
  94. package/core/rate-limit/stores/redis.js +151 -0
  95. package/core/rate-limit/utils.js +112 -0
  96. package/core/resilience/circuit-breaker.js +31 -0
  97. package/core/secrets/index.js +113 -0
  98. package/core/secrets/manager.js +308 -0
  99. package/core/secrets/providers/aws.js +288 -0
  100. package/core/secrets/providers/env.js +44 -0
  101. package/core/secrets/providers/gcp.js +307 -0
  102. package/core/secrets/providers/memory.js +76 -0
  103. package/core/services/index.js +9 -0
  104. package/core/shutdown/shutdown-manager.js +66 -38
  105. package/core/stream/index.js +19 -0
  106. package/core/stream/text-stream.js +326 -0
  107. package/core/studio/index.js +43 -0
  108. package/core/studio/inspector.js +266 -0
  109. package/core/studio/server.js +341 -0
  110. package/core/studio/ui.js +681 -0
  111. package/core/utils/string.js +77 -16
  112. package/core/validation/index.js +18 -0
  113. package/index.d.ts +1106 -10
  114. package/index.js +129 -3
  115. package/package.json +20 -8
  116. package/plugins/admin-panel/client/parts/07-model-list.js +5 -2
  117. package/plugins/admin-panel/index.js +36 -16
  118. package/plugins/admin-panel/modules/user-management.js +3 -2
  119. package/plugins/api-key/index.js +3 -2
  120. package/plugins/audit-log/index.js +3 -1
  121. package/plugins/basic-auth/index.js +3 -2
  122. package/plugins/cors.js +3 -2
  123. package/plugins/csrf/index.js +6 -4
  124. package/plugins/csv/index.js +8 -5
  125. package/plugins/dashboard/index.js +27 -7
  126. package/plugins/email/index.js +11 -9
  127. package/plugins/index.js +3 -0
  128. package/plugins/queue/index.js +18 -2
  129. package/plugins/redirect/index.js +7 -2
  130. package/plugins/seo-checker/index.js +2 -2
  131. package/plugins/site-analytics/index.js +3 -1
  132. package/plugins/swagger.js +8 -2
  133. package/plugins/upload/gcs-provider.js +148 -0
  134. package/plugins/upload/index.js +132 -74
  135. package/plugins/upload/s3-provider.js +257 -0
  136. package/plugins/webhooks/index.js +27 -23
  137. package/plugins/xlsx/index.js +7 -5
  138. package/src/api/api-loader.js +12 -95
  139. package/src/api/define-api.js +1 -0
  140. package/src/app-context.js +9 -1
  141. package/src/client-runtime/constants.js +10 -0
  142. package/src/client-runtime/mount.js +2 -3
  143. package/src/dev/client-gen.js +458 -0
  144. package/src/discovery/scan-routes.js +5 -2
  145. package/src/file-router.js +14 -3
  146. package/src/helpers.js +53 -0
  147. package/src/pages/page-loader.js +12 -205
  148. package/src/plugin-manager.js +280 -47
  149. package/src/server.js +253 -169
  150. package/src/services/discovery.js +9 -1
  151. package/src/services/executor.js +271 -49
  152. package/src/services/index.js +52 -3
  153. package/src/services/loader.js +120 -0
  154. package/src/services/memoize.js +223 -49
  155. package/src/services/registry.js +501 -95
  156. 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
- isWebspressoPackageCache = Boolean(pkg && pkg.name === 'webspresso');
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 plugin manager
656
- const pluginManager = createPluginManager();
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
- serviceRegistry.call(name, input, req.context, opts);
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
- // Auto-discover module services and middlewares if modules directory exists
1476
- const modulesDirCandidate = options.modules?.dir
1477
- ? (path.isAbsolute(options.modules.dir) ? options.modules.dir : path.join(rootDir, options.modules.dir))
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 = { app, nunjucksEnv, options, middlewares, shutdownManager, events: eventDispatcher };
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 synchronously (plugins should not be async in this phase)
1686
- // and mount any custom routes added by plugins
1687
- for (const [name, plugin] of pluginManager.plugins) {
1688
- if (typeof plugin.onRoutesReady === 'function') {
1689
- const ctx = {
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
- console.error('Server error:', err);
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
- console.warn(`[webspresso] HTTP ${status}:`, normalized.message);
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) => shutdownManager.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 (filename.endsWith('.test.js') || filename.endsWith('.spec.js') || filename.endsWith('.d.ts')) {
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);