webspresso 0.0.107 → 0.0.109

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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -3
  3. package/bin/commands/add-esbuild.js +7 -3
  4. package/bin/commands/add-tailwind.js +9 -5
  5. package/bin/commands/build.js +20 -4
  6. package/bin/commands/dev.js +57 -7
  7. package/bin/commands/doctor.js +42 -1
  8. package/bin/commands/favicon-generate.js +10 -3
  9. package/bin/commands/new.js +8 -3
  10. package/bin/commands/start.js +3 -1
  11. package/bin/utils/add-command.js +16 -0
  12. package/core/app.js +52 -8
  13. package/core/applySchema.js +3 -72
  14. package/core/auth/authenticate.js +3 -3
  15. package/core/auth/hash.js +167 -43
  16. package/core/auth/index.js +13 -1
  17. package/core/auth/jwt.js +21 -2
  18. package/core/auth/manager.js +63 -14
  19. package/core/auth/middleware.js +4 -2
  20. package/core/auth/oauth.js +5 -1
  21. package/core/auth/tokens.js +7 -2
  22. package/core/auth/totp.js +42 -6
  23. package/core/auth/user-agent.js +2 -0
  24. package/core/cache/lru.js +10 -0
  25. package/core/cache/tagged-lru.js +4 -0
  26. package/core/client/adapter/datastar.js +1254 -0
  27. package/core/client/api.js +1033 -0
  28. package/core/client/controller.js +1081 -0
  29. package/core/client/debug.js +367 -0
  30. package/core/client/forms.js +1021 -0
  31. package/core/client/fragments.js +738 -0
  32. package/core/client/index.d.ts +40 -0
  33. package/core/client/index.js +347 -0
  34. package/core/client/lite.js +164 -0
  35. package/core/client/server-fragments.js +859 -0
  36. package/core/client/utils.js +144 -0
  37. package/core/compileSchema.js +3 -68
  38. package/core/compression/compressible.js +53 -6
  39. package/core/compression/middleware.js +277 -26
  40. package/core/compression/negotiate.js +31 -12
  41. package/core/context/app-context.js +223 -0
  42. package/core/context/index.js +55 -0
  43. package/core/context/request.js +269 -0
  44. package/core/errors/middleware.js +14 -0
  45. package/core/events/dispatcher.js +177 -15
  46. package/core/events/signal.js +9 -0
  47. package/core/forms/flash.js +136 -102
  48. package/core/health/middleware.js +36 -6
  49. package/core/health/probes.js +12 -2
  50. package/core/http/index.js +10 -1
  51. package/core/http/pipeline.js +558 -368
  52. package/core/index.js +45 -8
  53. package/core/kernel/view.js +41 -10
  54. package/core/metrics/middleware.js +30 -6
  55. package/core/metrics/registry.js +13 -5
  56. package/core/orm/cache/fingerprint.js +32 -11
  57. package/core/orm/cache/layer.js +19 -2
  58. package/core/orm/eager-loader.js +90 -55
  59. package/core/orm/hidden.js +145 -0
  60. package/core/orm/index.js +3 -39
  61. package/core/orm/model.js +54 -3
  62. package/core/orm/query-builder.js +343 -6
  63. package/core/orm/query-filters.js +14 -2
  64. package/core/orm/repository.js +64 -13
  65. package/core/orm/schema-helpers.js +68 -6
  66. package/core/orm/transaction.js +157 -18
  67. package/core/orm/types.js +4 -0
  68. package/core/plugins/index.js +8 -0
  69. package/core/plugins/plugin-manager.js +1098 -0
  70. package/core/queue/adapters/database.js +184 -2
  71. package/core/queue/adapters/memory.js +153 -5
  72. package/core/queue/adapters/redis.js +59 -4
  73. package/core/queue/job.js +14 -1
  74. package/core/queue/manager.js +170 -27
  75. package/core/rate-limit/stores/memory.js +23 -10
  76. package/core/resilience/circuit-breaker.js +61 -22
  77. package/core/schema-cache.js +2 -64
  78. package/core/services/builtins/auth.js +273 -0
  79. package/core/services/builtins/exchange.js +396 -0
  80. package/core/services/builtins/file-manager.js +214 -0
  81. package/core/services/builtins/mail.js +161 -0
  82. package/core/services/builtins/media.js +196 -0
  83. package/core/services/builtins/queue.js +144 -0
  84. package/core/services/builtins/security.js +118 -0
  85. package/core/services/builtins/system.js +219 -0
  86. package/core/services/builtins/webhook.js +110 -0
  87. package/core/services/discovery.js +133 -0
  88. package/core/services/executor.js +651 -0
  89. package/core/services/index.js +113 -5
  90. package/core/services/loader.js +161 -0
  91. package/core/services/memoize.js +408 -0
  92. package/core/services/registry.js +731 -0
  93. package/core/services/validator.js +424 -0
  94. package/core/shutdown/shutdown-manager.js +146 -85
  95. package/core/sse/stream.js +28 -5
  96. package/core/ssr/stream.js +109 -16
  97. package/core/stream/text-stream.js +45 -4
  98. package/core/utils/index.js +110 -0
  99. package/core/utils/timeout.js +12 -1
  100. package/core/validation/apply.js +74 -0
  101. package/core/validation/cache.js +66 -0
  102. package/core/validation/compiler.js +80 -0
  103. package/core/validation/index.js +22 -1
  104. package/index.d.ts +1201 -72
  105. package/index.js +188 -28
  106. package/package.json +75 -26
  107. package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
  108. package/plugins/admin-panel/index.js +19 -8
  109. package/plugins/csrf/index.js +3 -3
  110. package/plugins/data-exchange/export.js +214 -0
  111. package/plugins/data-exchange/import.js +48 -5
  112. package/plugins/data-exchange/index.js +70 -28
  113. package/plugins/data-exchange/preview.js +266 -0
  114. package/plugins/data-exchange/template.js +242 -0
  115. package/plugins/email/admin-component.js +783 -141
  116. package/plugins/email/api-handlers.js +51 -1
  117. package/plugins/email/index.js +5 -0
  118. package/plugins/email/service.js +48 -0
  119. package/plugins/file-manager/admin-component.js +1546 -70
  120. package/plugins/file-manager/api-handlers.js +166 -1
  121. package/plugins/file-manager/core.js +598 -8
  122. package/plugins/file-manager/index.js +8 -0
  123. package/plugins/index.js +7 -0
  124. package/plugins/mcp/auth.js +3 -3
  125. package/plugins/oauth/index.js +36 -3
  126. package/plugins/queue/admin-component.js +1092 -0
  127. package/plugins/queue/api-handlers.js +248 -0
  128. package/plugins/queue/index.js +69 -0
  129. package/plugins/rest-resources/index.js +7 -2
  130. package/plugins/security/admin-component.js +689 -0
  131. package/plugins/security/api-handlers.js +169 -0
  132. package/plugins/security/audit.js +153 -0
  133. package/plugins/security/bruteforce.js +244 -0
  134. package/plugins/security/firewall.js +459 -0
  135. package/plugins/security/headers.js +214 -0
  136. package/plugins/security/honeypot.js +234 -0
  137. package/plugins/security/incident-recorder.js +137 -0
  138. package/plugins/security/index.js +211 -0
  139. package/plugins/seo-checker/analyzer.js +20 -7
  140. package/plugins/upload/index.js +36 -0
  141. package/plugins/webhooks/admin-component.js +702 -175
  142. package/plugins/webhooks/api-handlers.js +75 -1
  143. package/plugins/webhooks/dispatcher.js +290 -11
  144. package/plugins/webhooks/index.js +37 -2
  145. package/plugins/webhooks/sender.js +56 -0
  146. package/plugins/xlsx/engine.js +19 -4
  147. package/src/api/api-loader.js +19 -1
  148. package/src/app-context.js +4 -196
  149. package/src/bundle/index.js +443 -17
  150. package/src/client-runtime/mount.js +9 -2
  151. package/src/dev/api-typegen.js +1300 -0
  152. package/src/dev/banner.js +10 -1
  153. package/src/dev/colors.js +1 -1
  154. package/src/dev/index.js +7 -1
  155. package/src/dev/typegen.js +174 -18
  156. package/src/dev-reload/index.js +1 -1
  157. package/src/discovery/file-route-parser.js +17 -3
  158. package/src/discovery/scan-routes.js +4 -1
  159. package/src/file-router.js +249 -69
  160. package/src/helpers.js +46 -26
  161. package/src/njk-frontmatter.js +30 -4
  162. package/src/plugin-manager.js +5 -1086
  163. package/src/routing/cascading.js +26 -8
  164. package/src/routing/mount-discovered-routes.js +69 -0
  165. package/src/server.js +238 -64
  166. package/src/services/builtins/auth.js +2 -273
  167. package/src/services/builtins/exchange.js +2 -229
  168. package/src/services/builtins/file-manager.js +2 -178
  169. package/src/services/builtins/mail.js +2 -134
  170. package/src/services/builtins/media.js +2 -196
  171. package/src/services/builtins/system.js +2 -219
  172. package/src/services/discovery.js +2 -133
  173. package/src/services/executor.js +2 -575
  174. package/src/services/index.js +4 -109
  175. package/src/services/loader.js +1 -119
  176. package/src/services/memoize.js +2 -408
  177. package/src/services/registry.js +1 -711
  178. package/src/services/validator.js +2 -424
  179. package/.agents/ADMIN.md +0 -426
  180. package/.agents/AGENTS.md +0 -93
  181. package/.agents/ARCHITECTURE.md +0 -163
  182. package/.agents/AUTH.md +0 -581
  183. package/.agents/CACHE.md +0 -147
  184. package/.agents/CLI.md +0 -649
  185. package/.agents/COMPRESSION.md +0 -59
  186. package/.agents/CONVENTIONS.md +0 -50
  187. package/.agents/EARLY_HINTS.md +0 -120
  188. package/.agents/EMAIL.md +0 -223
  189. package/.agents/ERRORS.md +0 -260
  190. package/.agents/EVENTS.md +0 -179
  191. package/.agents/FLAGS.md +0 -118
  192. package/.agents/HEALTH.md +0 -118
  193. package/.agents/LIMITS.md +0 -109
  194. package/.agents/LOGGING.md +0 -93
  195. package/.agents/MCP.md +0 -103
  196. package/.agents/METRICS.md +0 -127
  197. package/.agents/MIGRATION_JOBS.md +0 -95
  198. package/.agents/ORM.md +0 -626
  199. package/.agents/PLUGINS.md +0 -729
  200. package/.agents/QUEUE.md +0 -290
  201. package/.agents/RATE_LIMIT.md +0 -147
  202. package/.agents/REALTIME.md +0 -213
  203. package/.agents/RESILIENCE.md +0 -235
  204. package/.agents/ROUTING.md +0 -605
  205. package/.agents/SCALE_OUT.md +0 -109
  206. package/.agents/SECRETS.md +0 -117
  207. package/.agents/SERVICES.md +0 -341
  208. package/.agents/SHUTDOWN.md +0 -67
  209. package/.agents/STREAMING.md +0 -108
  210. package/.agents/WORKFLOW.md +0 -27
  211. package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
  212. package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
  213. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
  214. package/.agents/skills/webspresso-framework/SKILL.md +0 -88
  215. package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -118
  216. package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -114
  217. package/.agents/skills/webspresso-services/SKILL.md +0 -101
  218. package/.cursorrules +0 -14
  219. package/AGENTS.md +0 -200
  220. package/CLAUDE.md +0 -54
  221. package/core/kernel/run-demo.js +0 -58
  222. package/core/utils/array.js +0 -12
  223. package/core/utils/base64url.js +0 -8
  224. package/core/utils/ensure-leading-slash.js +0 -6
  225. package/core/utils/escape-html.js +0 -7
  226. package/core/utils/escape-regexp.js +0 -7
  227. package/core/utils/get-client-ip.js +0 -6
  228. package/core/utils/hmac.js +0 -6
  229. package/core/utils/random.js +0 -9
  230. package/core/utils/safe-compare.js +0 -3
  231. package/core/utils/sha256.js +0 -6
  232. package/core/utils/strip-trailing-slash.js +0 -6
  233. package/core/utils/walk.js +0 -7
  234. package/plugins/admin-panel/styles.js +0 -9
  235. package/plugins/data-exchange/export-xlsx.js +0 -119
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Webspresso Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Node.js >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg?style=flat-square)](https://nodejs.org)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)
7
7
 
8
- A batteries-included, full-stack Node.js framework combining Next.js/Nuxt-style file-based routing, server-rendered Nunjucks streaming, a Knex-based ORM with ambient transactions, zero-dependency dual authentication, background queues, and a customizable Mithril.js SPA Admin Panel.
8
+ A batteries-included, full-stack Node.js framework combining Next.js/Nuxt-style file-based routing, server-rendered Nunjucks streaming, a Knex-based ORM with ambient transactions, built-in native dual authentication, background queues, and a customizable Mithril.js SPA Admin Panel.
9
9
 
10
10
  ---
11
11
 
@@ -33,10 +33,10 @@ Open **`http://localhost:7331`** in your browser.
33
33
  | **Routing & SSR** | 100% file-based routing (`pages/`), method suffixes (`.get.js`, `.post.js`), nested layouts (`_layout.njk`), directory-level error boundaries (`_error.njk`), loading skeletons (`_loading.njk`), and out-of-order chunked response streaming (`res.renderStream`). |
34
34
  | **Performance & Cold Start** | Single-pass unified route discovery, dev mode JIT on-demand route evaluation, production AOT route manifest (`webspresso build`), AOT Nunjucks template precompilation (zero AST parsing at runtime), and on-demand lazy plugin mounting. |
35
35
  | **Database & ORM** | Built-in Knex ORM with `defineModel`, `zdb` schema builder, Repository pattern (`db.getRepository('Model')`), relations (`belongsTo`, `hasMany`), cascade soft-delete & restore, and ambient transactions via `AsyncLocalStorage`. |
36
- | **Dual Auth & Sessions** | Stateful cookies for SSR + stateless HS256 JWT rotation for APIs, magic links, active device management & remote session revocation, CSRF protection, and zero external auth dependencies. |
36
+ | **Dual Auth & Sessions** | Stateful cookies for SSR + stateless HS256 JWT rotation for APIs, magic links, active device management & remote session revocation, CSRF protection, and built-in native auth. |
37
37
  | **Services & Queues** | File-based auto-discovery in `services/` with declarative Zod validation, role-based access guards, and ACID transaction propagation. Background worker queues in `jobs/`. |
38
38
  | **Admin Panel SPA** | Integrated Mithril.js single-page admin panel mounted at `/_admin` with auto-layout wrapping, CRUD views, custom dashboard widgets, and custom field renderers. Supports on-demand lazy mounting (`adminPanelPlugin({ lazy: true })`). |
39
- | **Plugin Ecosystem** | 20+ official plugins: Polar.sh billing, Webhooks dispatcher with HMAC signatures, Realtime (WebSocket/SSE), Site Analytics, API Keys, File Manager, CSV/Excel generation, On-demand Lazy Plugins (`lazyPlugin`), and more. |
39
+ | **Plugin Ecosystem** | 20+ official plugins: Polar.sh billing, Webhooks dispatcher with HMAC signatures, Realtime client layer (WebSocket/SSE/Socket.IO adapters), Site Analytics, API Keys, File Manager, CSV/Excel generation, On-demand Lazy Plugins (`lazyPlugin`), and more. |
40
40
  | **DevOps & Production** | Built-in streaming zlib compression (`brotli`/`gzip`), socket connection draining & graceful shutdown, `webspresso deploy:check` production readiness auditor, and Docker scaffolding. |
41
41
 
42
42
  ---
@@ -5,11 +5,15 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const { getAddCommand } = require('../utils/add-command');
8
9
 
9
10
  function registerCommand(program) {
10
- program
11
- .command('add esbuild')
12
- .alias('add:esbuild')
11
+ configureCommand(program.command('add:esbuild'));
12
+ configureCommand(getAddCommand(program).command('esbuild'));
13
+ }
14
+
15
+ function configureCommand(command) {
16
+ command
13
17
  .description('Add esbuild client bundling and incremental watch to the project')
14
18
  .action(async () => {
15
19
  if (!fs.existsSync('package.json')) {
@@ -6,11 +6,15 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const { execSync } = require('child_process');
9
+ const { getAddCommand } = require('../utils/add-command');
9
10
 
10
11
  function registerCommand(program) {
11
- program
12
- .command('add tailwind')
13
- .alias('add:tailwind')
12
+ configureCommand(program.command('add:tailwind'));
13
+ configureCommand(getAddCommand(program).command('tailwind'));
14
+ }
15
+
16
+ function configureCommand(command) {
17
+ command
14
18
  .description('Add Tailwind CSS to the project with build process')
15
19
  .option('--v4', 'Use Tailwind CSS v4 (Oxide engine, LightningCSS, zero postcss config)')
16
20
  .option('--v3', 'Use Tailwind CSS v3 (legacy PostCSS engine)')
@@ -65,9 +69,9 @@ function registerCommand(program) {
65
69
  packageJson.scripts.dev = 'npm run watch:css & node --watch server.js';
66
70
  }
67
71
 
68
- // Update start script to build CSS
72
+ // Update start script to build CSS (webspresso build runs Tailwind) and start cross-platform
69
73
  if (packageJson.scripts.start) {
70
- packageJson.scripts.start = 'npm run build:css && NODE_ENV=production node server.js';
74
+ packageJson.scripts.start = 'webspresso build && webspresso start';
71
75
  }
72
76
 
73
77
  fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
@@ -6,7 +6,8 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const { execSync } = require('child_process');
9
- const { buildBundle } = require('../../src/bundle');
9
+ const { buildBundle, findPageEntryPoints } = require('../../src/bundle');
10
+ const { generateTypes, checkTemplateFormContracts } = require('../../src/dev/typegen');
10
11
 
11
12
  function registerCommand(program) {
12
13
  program
@@ -56,11 +57,14 @@ function registerCommand(program) {
56
57
  }
57
58
  }
58
59
 
59
- // 2. Build JS Bundle with manifest.json (esbuild)
60
+ // 2. Build JS Bundle with manifest.json (esbuild or fallback)
60
61
  if (options.js !== false) {
61
- const hasClientEntry = fs.existsSync(path.join(cwd, 'src', 'client', 'main.js')) ||
62
+ const discoveredClientPages = findPageEntryPoints(cwd);
63
+ const hasClientEntry =
64
+ fs.existsSync(path.join(cwd, 'src', 'client', 'main.js')) ||
62
65
  fs.existsSync(path.join(cwd, 'src', 'client', 'main.ts')) ||
63
- fs.existsSync(path.join(cwd, 'src', 'client', 'index.js'));
66
+ fs.existsSync(path.join(cwd, 'src', 'client', 'index.js')) ||
67
+ Object.keys(discoveredClientPages).length > 0;
64
68
  const hasEsbuild = Boolean(
65
69
  (packageJson.devDependencies && packageJson.devDependencies.esbuild) ||
66
70
  (packageJson.dependencies && packageJson.dependencies.esbuild)
@@ -122,6 +126,18 @@ function registerCommand(program) {
122
126
  }
123
127
  }
124
128
 
129
+ // 5. Generate TypeScript & API Route Manifest (.webspresso/types.d.ts & .webspresso/api.d.ts) and check HTML form contracts
130
+ try {
131
+ generateTypes({ rootDir: cwd, silent: true });
132
+ const formCheck = checkTemplateFormContracts(cwd);
133
+ for (const w of formCheck.warnings) {
134
+ console.warn(`⚠️ [Form Check] ${w.message}`);
135
+ }
136
+ for (const e of formCheck.errors) {
137
+ console.warn(`❌ [Form Contract Mismatch] ${e.message}`);
138
+ }
139
+ } catch (_) {}
140
+
125
141
  console.log('\n✨ Asset build complete!\n');
126
142
  if (cssBuilt) console.log(' ✓ CSS: public/css/style.css');
127
143
  if (jsBuilt) console.log(' ✓ JS: public/dist/ & manifest.json');
@@ -4,24 +4,60 @@
4
4
  */
5
5
 
6
6
  const fs = require('fs');
7
+ const path = require('path');
7
8
  const { spawn } = require('child_process');
8
9
 
9
10
  /**
10
11
  * Build node --watch arguments with additional watch paths
12
+ * @param {string} [projectRoot=process.cwd()]
11
13
  * @returns {string[]} Node arguments
12
14
  */
13
- function buildWatchArgs() {
15
+ function buildWatchArgs(projectRoot = process.cwd()) {
14
16
  const args = ['--watch'];
15
17
 
16
- // Add watch paths for server-side code directories (services, models, config, pages)
17
- // Note: views contains Nunjucks templates which are live-reloaded in-memory by devReloadHub
18
- const watchPaths = ['pages', 'models', 'services', 'config'];
18
+ // Add watch paths for server-side code directories:
19
+ // - pages: File-based routing and companion loaders
20
+ // - models: ORM database models
21
+ // - services: Business logic services
22
+ // - jobs: Background queues and scheduled cron workers
23
+ // - listeners: Decoupled events and signals
24
+ // - modules / src/modules: Modular domain packages
25
+ // - plugins / src/plugins: Local plugin extensions
26
+ // - config: App config and credentials
27
+ // - migrations: DB migration definitions
28
+ const watchPaths = [
29
+ 'pages',
30
+ 'models',
31
+ 'services',
32
+ 'jobs',
33
+ 'listeners',
34
+ 'modules',
35
+ 'plugins',
36
+ 'config',
37
+ 'migrations',
38
+ 'src/models',
39
+ 'src/services',
40
+ 'src/jobs',
41
+ 'src/listeners',
42
+ 'src/modules',
43
+ 'src/plugins',
44
+ ];
19
45
 
20
46
  for (const dir of watchPaths) {
21
- if (fs.existsSync(dir)) {
47
+ const fullPath = path.isAbsolute(dir) ? dir : path.join(projectRoot, dir);
48
+ if (fs.existsSync(fullPath)) {
22
49
  args.push(`--watch-path=./${dir}`);
23
50
  }
24
51
  }
52
+
53
+ // Also watch .env files if present
54
+ const envFiles = ['.env', '.env.local', '.env.development'];
55
+ for (const envFile of envFiles) {
56
+ const fullPath = path.join(projectRoot, envFile);
57
+ if (fs.existsSync(fullPath)) {
58
+ args.push(`--watch-path=./${envFile}`);
59
+ }
60
+ }
25
61
 
26
62
  args.push('server.js');
27
63
  return args;
@@ -31,7 +67,7 @@ function registerCommand(program) {
31
67
  program
32
68
  .command('dev')
33
69
  .description('Start development server')
34
- .option('-p, --port <port>', 'Port number', '7331')
70
+ .option('-p, --port <port>', 'Port number (default: $PORT or 7331)')
35
71
  .option('--no-css', 'Skip CSS watch (if Tailwind is set up)')
36
72
  .option('--no-js', 'Skip client JS watch (if esbuild / watch:js is set up)')
37
73
  .action((options) => {
@@ -40,6 +76,7 @@ function registerCommand(program) {
40
76
  process.exit(1);
41
77
  }
42
78
 
79
+ options.port = options.port || process.env.PORT || '7331';
43
80
  process.env.PORT = options.port;
44
81
  process.env.NODE_ENV = 'development';
45
82
 
@@ -74,6 +111,19 @@ function registerCommand(program) {
74
111
  if (shouldWatchCss) watchingLabels.push('CSS');
75
112
  if (shouldWatchJs) watchingLabels.push('JS (esbuild)');
76
113
 
114
+ // Generate initial .webspresso/types.d.ts and .webspresso/api.d.ts and warn on dynamic/mismatched HTML form fields
115
+ try {
116
+ const { generateTypes, checkTemplateFormContracts } = require('../../src/dev/typegen');
117
+ generateTypes({ rootDir: process.cwd(), silent: true });
118
+ const formCheck = checkTemplateFormContracts(process.cwd());
119
+ for (const w of formCheck.warnings) {
120
+ console.warn(`⚠️ [Form Check] ${w.message}`);
121
+ }
122
+ for (const e of formCheck.errors) {
123
+ console.warn(`❌ [Form Contract Mismatch] ${e.message}`);
124
+ }
125
+ } catch (_) {}
126
+
77
127
  console.log(`\n🚀 Starting development server on port ${options.port}...`);
78
128
  console.log(` Watching: ${watchingLabels.join(', ')}\n`);
79
129
 
@@ -124,4 +174,4 @@ function registerCommand(program) {
124
174
  });
125
175
  }
126
176
 
127
- module.exports = { registerCommand };
177
+ module.exports = { registerCommand, buildWatchArgs };
@@ -5,6 +5,13 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
8
+ const { isClientBrowserFile } = require('../../src/discovery/file-route-parser');
9
+ const {
10
+ discoverClientControllers,
11
+ findDefaultEntryPoints,
12
+ validateClientBundleBoundaries,
13
+ } = require('../../src/bundle');
14
+ const { checkTemplateFormContracts } = require('../../src/dev/typegen');
8
15
 
9
16
  /**
10
17
  * @param {string} enginesNode - e.g. ">=18.0.0", "^20.1.0"
@@ -136,13 +143,17 @@ function registerCommand(program) {
136
143
  return name;
137
144
  }
138
145
 
139
- const VALID_API_METHODS = ['.get.js', '.post.js', '.put.js', '.patch.js', '.delete.js'];
146
+ const VALID_API_METHODS = [
147
+ '.get.js', '.post.js', '.put.js', '.patch.js', '.delete.js',
148
+ '.get.ts', '.post.ts', '.put.ts', '.patch.ts', '.delete.ts',
149
+ ];
140
150
 
141
151
  function scanRoutes(dirPath, relativeDir = '', isApi = false) {
142
152
  const children = fs.readdirSync(dirPath);
143
153
  const kept = [];
144
154
 
145
155
  for (const child of children) {
156
+ if (isClientBrowserFile(child)) continue;
146
157
  const fullPath = path.join(dirPath, child);
147
158
  const relPath = relativeDir ? path.join(relativeDir, child) : child;
148
159
  const stats = fs.statSync(fullPath);
@@ -215,6 +226,36 @@ function registerCommand(program) {
215
226
  warnings += 1;
216
227
  }
217
228
 
229
+ console.log('\nClient Behavior & Form Contracts');
230
+ console.log('--------------------------------');
231
+ try {
232
+ const controllers = discoverClientControllers(cwd);
233
+ const entries = findDefaultEntryPoints(cwd);
234
+ validateClientBundleBoundaries(entries, cwd);
235
+ line('✓', `Client controllers verified (${controllers.length} controller(s), zero boundary leaks or collisions)`);
236
+ } catch (clientErr) {
237
+ line('✗', clientErr.message || String(clientErr));
238
+ errors += 1;
239
+ }
240
+
241
+ try {
242
+ const formCheck = checkTemplateFormContracts(cwd);
243
+ for (const w of formCheck.warnings) {
244
+ line('⚠', w.message);
245
+ warnings += 1;
246
+ }
247
+ for (const e of formCheck.errors) {
248
+ line('✗', e.message);
249
+ errors += 1;
250
+ }
251
+ if (formCheck.errors.length === 0 && formCheck.warnings.length === 0) {
252
+ line('✓', `Static HTML form contracts verified (${formCheck.checkedFormsCount} API form(s) checked)`);
253
+ }
254
+ } catch (formErr) {
255
+ line('⚠', `Form contract check skipped: ${formErr.message}`);
256
+ warnings += 1;
257
+ }
258
+
218
259
  console.log('\nTailwind CSS');
219
260
  console.log('------------');
220
261
  const inputCssPath = path.join(cwd, 'src', 'input.css');
@@ -6,9 +6,16 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  function getSharp() {
9
- return (process.env.NODE_ENV === 'test' || process.env.VITEST)
10
- ? require('../../tests/mocks/sharp')
11
- : require('sharp');
9
+ if (process.env.NODE_ENV === 'test' || process.env.VITEST) {
10
+ return require('../../tests/mocks/sharp');
11
+ }
12
+ try {
13
+ return require('sharp');
14
+ } catch {
15
+ throw new Error(
16
+ "The 'sharp' package is required to generate favicons. Install it with: npm install sharp"
17
+ );
18
+ }
12
19
  }
13
20
 
14
21
  const APPLE_SIZES = [57, 60, 72, 76, 114, 120, 144, 152, 180];
@@ -12,6 +12,8 @@ const inquirer = {
12
12
  };
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
+
16
+ const FRAMEWORK_VERSION = require('../../package.json').version;
15
17
  const { runInstallation, startDevServer } = require('../utils/project');
16
18
  const { getSeedFileTemplate } = require('../utils/seed');
17
19
  const { scaffoldAgentsFiles } = require('./agents-init');
@@ -250,10 +252,13 @@ function registerCommand(program) {
250
252
  main: 'server.js',
251
253
  scripts: {
252
254
  dev: 'webspresso dev',
253
- start: 'NODE_ENV=production node server.js'
255
+ build: 'webspresso build',
256
+ // `webspresso start` sets NODE_ENV=production cross-platform; build first so the
257
+ // AOT route manifest and precompiled templates are used in production.
258
+ start: 'webspresso build && webspresso start'
254
259
  },
255
260
  dependencies: {
256
- webspresso: '*',
261
+ webspresso: `^${FRAMEWORK_VERSION}`,
257
262
  dotenv: '^16.3.1',
258
263
  zod: '^3.23.0'
259
264
  }
@@ -728,7 +733,7 @@ module.exports = {
728
733
  }
729
734
 
730
735
  updatedPackageJson.scripts.dev = 'webspresso dev';
731
- updatedPackageJson.scripts.start = 'npm run build:css && NODE_ENV=production node server.js';
736
+ updatedPackageJson.scripts.start = 'webspresso build && webspresso start';
732
737
 
733
738
  fs.writeFileSync(
734
739
  path.join(projectPath, 'package.json'),
@@ -10,13 +10,15 @@ function registerCommand(program) {
10
10
  program
11
11
  .command('start')
12
12
  .description('Start production server')
13
- .option('-p, --port <port>', 'Port number', '7331')
13
+ .option('-p, --port <port>', 'Port number (default: $PORT or 7331)')
14
14
  .action((options) => {
15
15
  if (!fs.existsSync('server.js')) {
16
16
  console.error('❌ server.js not found! Make sure you are in a Webspresso project.');
17
17
  process.exit(1);
18
18
  }
19
19
 
20
+ // Respect the platform-provided PORT (Cloud Run, Render, Heroku...) unless --port is given
21
+ options.port = options.port || process.env.PORT || '7331';
20
22
  process.env.PORT = options.port;
21
23
  process.env.NODE_ENV = 'production';
22
24
 
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared `add` parent command
3
+ * Lets integrations register as `webspresso add <name>` subcommands without
4
+ * colliding with each other (each also registers its own `add:<name>` form).
5
+ */
6
+
7
+ function getAddCommand(program) {
8
+ const existing = program.commands.find((cmd) => cmd.name() === 'add');
9
+ if (existing) return existing;
10
+
11
+ return program
12
+ .command('add')
13
+ .description('Add an integration to the project (e.g. add tailwind, add esbuild)');
14
+ }
15
+
16
+ module.exports = { getAddCommand };
package/core/app.js CHANGED
@@ -22,8 +22,9 @@ const {
22
22
  } = require('./resilience');
23
23
  const { createTaggedLruCache } = require('./cache');
24
24
  const { createEventDispatcher, loadListeners } = require('./events');
25
- const { createPluginManager } = require('../src/plugin-manager');
26
- const { createServiceRegistry } = require('../src/services');
25
+ const { createPluginManager } = require('./plugins');
26
+ const { createServiceRegistry } = require('./services');
27
+ const { getContext } = require('./context/request');
27
28
 
28
29
  let isWebspressoPackageCache = null;
29
30
  function checkIsWebspressoPackage() {
@@ -153,7 +154,7 @@ class CoreApp {
153
154
 
154
155
  // Auto-register built-in auth services if database is present
155
156
  if (options.db) {
156
- const { createAuthServices } = require('../src/services/builtins/auth');
157
+ const { createAuthServices } = require('./services/builtins/auth');
157
158
  const authServices = createAuthServices({ db: options.db });
158
159
  for (const [name, def] of Object.entries(authServices)) {
159
160
  if (!this.serviceRegistry.has(name)) {
@@ -163,7 +164,7 @@ class CoreApp {
163
164
  }
164
165
 
165
166
  // Auto-register built-in system services (health, cleanup, cache flush, info)
166
- const { createSystemServices } = require('../src/services/builtins/system');
167
+ const { createSystemServices } = require('./services/builtins/system');
167
168
  const systemServices = createSystemServices({
168
169
  db: options.db ?? null,
169
170
  serviceRegistry: this.serviceRegistry,
@@ -175,9 +176,52 @@ class CoreApp {
175
176
  }
176
177
  }
177
178
 
179
+ // 6.5. Background Queue & Jobs auto-discovery
180
+ let resolvedQueue = options.queue;
181
+ if (resolvedQueue === undefined) {
182
+ const candidateJobsDir = options.jobsDir
183
+ ? (path.isAbsolute(options.jobsDir) ? options.jobsDir : path.join(this.rootDir, options.jobsDir))
184
+ : (fs.existsSync(path.join(this.rootDir, 'jobs'))
185
+ ? path.join(this.rootDir, 'jobs')
186
+ : (fs.existsSync(path.join(this.rootDir, 'src/jobs'))
187
+ ? path.join(this.rootDir, 'src/jobs')
188
+ : null));
189
+ if (candidateJobsDir && fs.existsSync(candidateJobsDir)) {
190
+ const { createQueueManager } = require('./queue');
191
+ resolvedQueue = createQueueManager({
192
+ jobsDir: candidateJobsDir,
193
+ autoStart: options.queueAutoStart !== undefined ? Boolean(options.queueAutoStart) : (process.env.NODE_ENV !== 'test'),
194
+ timeouts: options.timeouts,
195
+ adapter: options.queueAdapter,
196
+ });
197
+ }
198
+ } else if (resolvedQueue && typeof resolvedQueue === 'object' && typeof resolvedQueue.dispatch !== 'function') {
199
+ const { createQueueManager } = require('./queue');
200
+ resolvedQueue = createQueueManager({
201
+ autoStart: options.queueAutoStart !== undefined ? Boolean(options.queueAutoStart) : (process.env.NODE_ENV !== 'test'),
202
+ timeouts: options.timeouts,
203
+ ...resolvedQueue,
204
+ });
205
+ }
206
+
207
+ if (resolvedQueue && typeof resolvedQueue !== 'boolean') {
208
+ this.queue = resolvedQueue;
209
+ if (this.shutdownManager) {
210
+ this.shutdownManager.registerDisposer('queue', () => {
211
+ if (typeof resolvedQueue.drain === 'function') {
212
+ return resolvedQueue.drain();
213
+ } else if (typeof resolvedQueue.stop === 'function') {
214
+ return resolvedQueue.stop();
215
+ }
216
+ });
217
+ }
218
+ } else {
219
+ this.queue = null;
220
+ }
221
+
178
222
  // 7. Events Subsystem
179
223
  this.events = options.events || createEventDispatcher({
180
- queue: options.queue || null,
224
+ queue: this.queue,
181
225
  logger: this.logger,
182
226
  });
183
227
  this.shutdownManager.onShutdown(() => {
@@ -237,9 +281,8 @@ class CoreApp {
237
281
  this.flags = null;
238
282
  }
239
283
 
240
- // 10. Database & Queue
284
+ // 10. Database
241
285
  this.db = options.db ?? null;
242
- this.queue = options.queue ?? null;
243
286
 
244
287
  // 10. Module Services discovery (if modules directory exists)
245
288
  this.moduleMiddlewares = {};
@@ -414,7 +457,8 @@ class CoreApp {
414
457
  callOpts = maybeOpts || {};
415
458
  } else {
416
459
  callOpts = ctxOrOpts || {};
417
- const base = callOpts.context || callOpts.ctx || this.context;
460
+ const ambientStore = getContext();
461
+ const base = callOpts.context || callOpts.ctx || ambientStore || this.context;
418
462
  if (callOpts.user !== undefined || callOpts.auth !== undefined) {
419
463
  callCtx = Object.create(base);
420
464
  if (callOpts.user !== undefined) callCtx.user = callOpts.user;
@@ -1,73 +1,4 @@
1
- /**
2
- * Schema Applicator
3
- * Parses and validates request input against compiled schemas
4
- */
5
-
6
- const EMPTY_INPUT = Object.freeze({
7
- body: undefined,
8
- params: undefined,
9
- query: undefined,
10
- });
11
-
12
- /**
13
- * Apply compiled schema to request
14
- * Parses body, params, and query against their respective schemas
15
- * Stores validated results in req.input
16
- *
17
- * @param {Object} req - Express request object
18
- * @param {Object|null} compiledSchema - Compiled schema from compileSchema
19
- * @returns {void}
20
- * @throws {ZodError} If validation fails
21
- */
22
- function applySchema(req, compiledSchema) {
23
- // No schema means no validation
24
- if (!compiledSchema) {
25
- req.input = EMPTY_INPUT;
26
- return;
27
- }
28
-
29
- // Initialize req.input
30
- req.input = {
31
- body: undefined,
32
- params: undefined,
33
- query: undefined,
34
- };
35
-
36
- // Parse body if schema exists
37
- if (compiledSchema.body) {
38
- req.input.body = compiledSchema.body.parse(req.body);
39
- }
40
-
41
- // Parse params if schema exists
42
- if (compiledSchema.params) {
43
- req.input.params = compiledSchema.params.parse(req.params);
44
- }
45
-
46
- // Parse query if schema exists
47
- if (compiledSchema.query) {
48
- req.input.query = compiledSchema.query.parse(req.query);
49
- }
50
- }
51
-
52
- let _translateValidationIssues = null;
53
- function getTranslateValidationIssues() {
54
- if (!_translateValidationIssues) {
55
- try {
56
- _translateValidationIssues = require('../src/api/validation-i18n').translateValidationIssues;
57
- } catch {
58
- _translateValidationIssues = (issues) => issues;
59
- }
60
- }
61
- return _translateValidationIssues;
62
- }
63
-
64
- function translateValidationIssues(issues, options) {
65
- return getTranslateValidationIssues()(issues, options);
66
- }
67
-
68
- module.exports = {
69
- applySchema,
70
- translateValidationIssues,
71
- };
72
-
1
+ 'use strict';
73
2
 
3
+ // Backward-compatibility shim
4
+ module.exports = require('./validation/apply');
@@ -25,10 +25,10 @@ async function authenticateRequest(req, res, authManager) {
25
25
  req.auth = authManager.createRequestAuth(req, res);
26
26
  }
27
27
 
28
- // 1. Check Bearer JWT token header first
28
+ // 1. Check Bearer JWT token header first (case-insensitive per RFC 6750)
29
29
  const authHeader = req.headers?.authorization || req.headers?.Authorization;
30
- if (authHeader && authHeader.startsWith('Bearer ')) {
31
- const token = authHeader.substring(7).trim();
30
+ if (authHeader && /^Bearer\s+/i.test(authHeader)) {
31
+ const token = authHeader.replace(/^Bearer\s+/i, '').trim();
32
32
  try {
33
33
  const payload = authManager.verifyJwt(token);
34
34
  req.token = token;