webspresso 0.0.108 → 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 (178) 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 +45 -3
  13. package/core/auth/authenticate.js +3 -3
  14. package/core/auth/hash.js +167 -43
  15. package/core/auth/index.js +13 -1
  16. package/core/auth/jwt.js +21 -2
  17. package/core/auth/manager.js +63 -14
  18. package/core/auth/middleware.js +4 -2
  19. package/core/auth/oauth.js +5 -1
  20. package/core/auth/tokens.js +7 -2
  21. package/core/auth/totp.js +42 -6
  22. package/core/auth/user-agent.js +2 -0
  23. package/core/client/adapter/datastar.js +1254 -0
  24. package/core/client/api.js +1033 -0
  25. package/core/client/controller.js +1081 -0
  26. package/core/client/debug.js +367 -0
  27. package/core/client/forms.js +1021 -0
  28. package/core/client/fragments.js +738 -0
  29. package/core/client/index.d.ts +40 -0
  30. package/core/client/index.js +347 -0
  31. package/core/client/lite.js +164 -0
  32. package/core/client/server-fragments.js +859 -0
  33. package/core/client/utils.js +144 -0
  34. package/core/compression/middleware.js +277 -26
  35. package/core/context/app-context.js +25 -3
  36. package/core/context/request.js +32 -4
  37. package/core/errors/middleware.js +14 -0
  38. package/core/events/dispatcher.js +33 -0
  39. package/core/forms/flash.js +136 -102
  40. package/core/health/middleware.js +36 -6
  41. package/core/http/pipeline.js +45 -6
  42. package/core/index.js +21 -7
  43. package/core/kernel/view.js +41 -10
  44. package/core/metrics/middleware.js +30 -6
  45. package/core/metrics/registry.js +13 -5
  46. package/core/orm/cache/fingerprint.js +32 -11
  47. package/core/orm/cache/layer.js +19 -2
  48. package/core/orm/hidden.js +145 -0
  49. package/core/orm/model.js +48 -2
  50. package/core/orm/query-builder.js +343 -6
  51. package/core/orm/query-filters.js +14 -2
  52. package/core/orm/repository.js +64 -13
  53. package/core/orm/schema-helpers.js +9 -6
  54. package/core/orm/transaction.js +50 -6
  55. package/core/orm/types.js +4 -0
  56. package/core/queue/adapters/database.js +184 -2
  57. package/core/queue/adapters/memory.js +153 -5
  58. package/core/queue/adapters/redis.js +59 -4
  59. package/core/queue/job.js +14 -1
  60. package/core/queue/manager.js +115 -0
  61. package/core/resilience/circuit-breaker.js +27 -8
  62. package/core/services/builtins/exchange.js +174 -7
  63. package/core/services/builtins/file-manager.js +36 -0
  64. package/core/services/builtins/mail.js +27 -0
  65. package/core/services/builtins/queue.js +144 -0
  66. package/core/services/builtins/security.js +118 -0
  67. package/core/services/builtins/webhook.js +110 -0
  68. package/core/services/executor.js +38 -16
  69. package/core/services/index.js +4 -1
  70. package/core/sse/stream.js +28 -5
  71. package/core/utils/timeout.js +8 -1
  72. package/core/validation/compiler.js +10 -7
  73. package/index.d.ts +1127 -71
  74. package/index.js +124 -8
  75. package/package.json +75 -26
  76. package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
  77. package/plugins/admin-panel/index.js +19 -8
  78. package/plugins/csrf/index.js +3 -3
  79. package/plugins/data-exchange/export.js +214 -0
  80. package/plugins/data-exchange/import.js +48 -5
  81. package/plugins/data-exchange/index.js +70 -28
  82. package/plugins/data-exchange/preview.js +266 -0
  83. package/plugins/data-exchange/template.js +242 -0
  84. package/plugins/email/admin-component.js +783 -141
  85. package/plugins/email/api-handlers.js +51 -1
  86. package/plugins/email/index.js +5 -0
  87. package/plugins/email/service.js +48 -0
  88. package/plugins/file-manager/admin-component.js +1546 -70
  89. package/plugins/file-manager/api-handlers.js +166 -1
  90. package/plugins/file-manager/core.js +598 -8
  91. package/plugins/file-manager/index.js +8 -0
  92. package/plugins/index.js +7 -0
  93. package/plugins/mcp/auth.js +3 -3
  94. package/plugins/oauth/index.js +36 -3
  95. package/plugins/queue/admin-component.js +1092 -0
  96. package/plugins/queue/api-handlers.js +248 -0
  97. package/plugins/queue/index.js +69 -0
  98. package/plugins/rest-resources/index.js +7 -2
  99. package/plugins/security/admin-component.js +689 -0
  100. package/plugins/security/api-handlers.js +169 -0
  101. package/plugins/security/audit.js +153 -0
  102. package/plugins/security/bruteforce.js +244 -0
  103. package/plugins/security/firewall.js +459 -0
  104. package/plugins/security/headers.js +214 -0
  105. package/plugins/security/honeypot.js +234 -0
  106. package/plugins/security/incident-recorder.js +137 -0
  107. package/plugins/security/index.js +211 -0
  108. package/plugins/seo-checker/analyzer.js +20 -7
  109. package/plugins/upload/index.js +36 -0
  110. package/plugins/webhooks/admin-component.js +702 -175
  111. package/plugins/webhooks/api-handlers.js +75 -1
  112. package/plugins/webhooks/dispatcher.js +290 -11
  113. package/plugins/webhooks/index.js +37 -2
  114. package/plugins/webhooks/sender.js +56 -0
  115. package/plugins/xlsx/engine.js +19 -4
  116. package/src/api/api-loader.js +19 -1
  117. package/src/bundle/index.js +443 -17
  118. package/src/client-runtime/mount.js +9 -2
  119. package/src/dev/api-typegen.js +1300 -0
  120. package/src/dev/banner.js +10 -1
  121. package/src/dev/colors.js +1 -1
  122. package/src/dev/index.js +7 -1
  123. package/src/dev/typegen.js +173 -17
  124. package/src/dev-reload/index.js +1 -1
  125. package/src/discovery/file-route-parser.js +17 -3
  126. package/src/discovery/scan-routes.js +4 -1
  127. package/src/file-router.js +249 -69
  128. package/src/helpers.js +46 -26
  129. package/src/njk-frontmatter.js +30 -4
  130. package/src/routing/cascading.js +26 -8
  131. package/src/routing/mount-discovered-routes.js +69 -0
  132. package/src/server.js +237 -66
  133. package/.agents/ADMIN.md +0 -426
  134. package/.agents/AGENTS.md +0 -96
  135. package/.agents/ARCHITECTURE.md +0 -163
  136. package/.agents/AUTH.md +0 -581
  137. package/.agents/CACHE.md +0 -148
  138. package/.agents/CLI.md +0 -649
  139. package/.agents/COMPRESSION.md +0 -59
  140. package/.agents/CONTEXT.md +0 -152
  141. package/.agents/CONVENTIONS.md +0 -50
  142. package/.agents/EARLY_HINTS.md +0 -120
  143. package/.agents/EMAIL.md +0 -223
  144. package/.agents/ERRORS.md +0 -260
  145. package/.agents/EVENTS.md +0 -179
  146. package/.agents/FLAGS.md +0 -118
  147. package/.agents/HEALTH.md +0 -118
  148. package/.agents/LIMITS.md +0 -109
  149. package/.agents/LOGGING.md +0 -93
  150. package/.agents/MCP.md +0 -103
  151. package/.agents/METRICS.md +0 -127
  152. package/.agents/MIGRATION_JOBS.md +0 -95
  153. package/.agents/ORM.md +0 -700
  154. package/.agents/PLUGINS.md +0 -729
  155. package/.agents/QUEUE.md +0 -290
  156. package/.agents/RATE_LIMIT.md +0 -147
  157. package/.agents/REALTIME.md +0 -213
  158. package/.agents/RESILIENCE.md +0 -235
  159. package/.agents/ROUTING.md +0 -605
  160. package/.agents/SCALE_OUT.md +0 -109
  161. package/.agents/SECRETS.md +0 -117
  162. package/.agents/SERVICES.md +0 -341
  163. package/.agents/SHUTDOWN.md +0 -74
  164. package/.agents/STREAMING.md +0 -108
  165. package/.agents/WORKFLOW.md +0 -27
  166. package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
  167. package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
  168. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
  169. package/.agents/skills/webspresso-framework/SKILL.md +0 -89
  170. package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
  171. package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
  172. package/.agents/skills/webspresso-services/SKILL.md +0 -134
  173. package/.cursorrules +0 -14
  174. package/AGENTS.md +0 -203
  175. package/CLAUDE.md +0 -54
  176. package/core/kernel/run-demo.js +0 -58
  177. package/plugins/admin-panel/styles.js +0 -9
  178. 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
@@ -176,9 +176,52 @@ class CoreApp {
176
176
  }
177
177
  }
178
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
+
179
222
  // 7. Events Subsystem
180
223
  this.events = options.events || createEventDispatcher({
181
- queue: options.queue || null,
224
+ queue: this.queue,
182
225
  logger: this.logger,
183
226
  });
184
227
  this.shutdownManager.onShutdown(() => {
@@ -238,9 +281,8 @@ class CoreApp {
238
281
  this.flags = null;
239
282
  }
240
283
 
241
- // 10. Database & Queue
284
+ // 10. Database
242
285
  this.db = options.db ?? null;
243
- this.queue = options.queue ?? null;
244
286
 
245
287
  // 10. Module Services discovery (if modules directory exists)
246
288
  this.moduleMiddlewares = {};
@@ -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;