webspresso 0.0.74 → 0.1.0-alpha.0

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 (124) hide show
  1. package/README.md +340 -9
  2. package/adapters/bun/index.js +22 -0
  3. package/adapters/cloudflare/index.js +138 -0
  4. package/adapters/node/index.js +75 -0
  5. package/adapters/types.js +16 -0
  6. package/bin/commands/add-deploy.js +84 -0
  7. package/bin/commands/add-tailwind.js +3 -3
  8. package/bin/commands/build.js +57 -0
  9. package/bin/commands/dev.js +5 -0
  10. package/bin/commands/orm-map.js +139 -0
  11. package/bin/commands/skill.js +22 -8
  12. package/bin/utils/orm-map-html.js +689 -0
  13. package/bin/utils/orm-map-load.js +85 -0
  14. package/bin/utils/orm-map-snapshot.js +179 -0
  15. package/bin/utils/resolve-webspresso-orm.js +23 -0
  16. package/bin/webspresso.js +9 -1
  17. package/core/auth/manager.js +14 -1
  18. package/core/auth/middleware.js +17 -98
  19. package/core/build/README.md +39 -0
  20. package/core/build/cache/build-cache.js +79 -0
  21. package/core/build/config/load-build-config.js +39 -0
  22. package/core/build/errors/build-error.js +35 -0
  23. package/core/build/graph/build-graph.js +63 -0
  24. package/core/build/graph/hash.js +46 -0
  25. package/core/build/index.js +128 -0
  26. package/core/build/phases/01-discover.js +89 -0
  27. package/core/build/phases/02-analyze.js +177 -0
  28. package/core/build/phases/03-compile/index.js +112 -0
  29. package/core/build/phases/03-compile/middleware.js +42 -0
  30. package/core/build/phases/03-compile/models.js +37 -0
  31. package/core/build/phases/03-compile/plugins.js +90 -0
  32. package/core/build/phases/03-compile/routes-api.js +103 -0
  33. package/core/build/phases/03-compile/routes-ssr.js +38 -0
  34. package/core/build/phases/03-compile/templates.js +180 -0
  35. package/core/build/phases/04-manifest.js +66 -0
  36. package/core/build/phases/05-bundle.js +122 -0
  37. package/core/build/phases/06-validate.js +67 -0
  38. package/core/build/runtime/create-app-from-manifest-node.js +27 -0
  39. package/core/build/runtime/create-app-from-manifest.js +43 -0
  40. package/core/build/runtime/create-worker-app.js +271 -0
  41. package/core/build/runtime/mount-manifest.js +372 -0
  42. package/core/build/runtime/resolve-worker-db.js +32 -0
  43. package/core/build/types.d.ts +109 -0
  44. package/core/kernel/app.js +96 -0
  45. package/core/kernel/base-repository.js +143 -0
  46. package/core/kernel/events.js +101 -0
  47. package/core/kernel/flow.js +22 -0
  48. package/core/kernel/index.js +17 -0
  49. package/core/kernel/plugin.js +23 -0
  50. package/core/kernel/plugins/sample-seo.js +26 -0
  51. package/core/kernel/run-demo.js +58 -0
  52. package/core/kernel/view.js +167 -0
  53. package/core/openapi/build-from-api-routes.js +8 -2
  54. package/core/orm/d1-knex-client.js +64 -0
  55. package/core/orm/index.js +126 -49
  56. package/core/orm/model.js +3 -1
  57. package/core/url-path-normalize.js +30 -0
  58. package/index.d.ts +259 -21
  59. package/index.js +25 -2
  60. package/package.json +42 -11
  61. package/plugins/admin-panel/api.js +43 -15
  62. package/plugins/admin-panel/client/README.md +39 -0
  63. package/plugins/admin-panel/client/load-parts.js +74 -0
  64. package/plugins/admin-panel/client/manifest.parts.json +12 -0
  65. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +150 -0
  66. package/plugins/admin-panel/client/parts/02-filter-components.js +554 -0
  67. package/plugins/admin-panel/client/parts/03-pagination-intro.js +70 -0
  68. package/plugins/admin-panel/client/parts/04-field-renderers.js +287 -0
  69. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +335 -0
  70. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +125 -0
  71. package/plugins/admin-panel/client/parts/07-model-list.js +596 -0
  72. package/plugins/admin-panel/client/parts/08-record-list.js +536 -0
  73. package/plugins/admin-panel/client/parts/09-record-form.js +170 -0
  74. package/plugins/admin-panel/client/parts/10-export-registry.js +11 -0
  75. package/plugins/admin-panel/client/verify-spa-parts.js +32 -0
  76. package/plugins/admin-panel/client/vite.config.example.mjs +22 -0
  77. package/plugins/admin-panel/components.js +4 -2640
  78. package/plugins/admin-panel/core/api-extensions.js +101 -11
  79. package/plugins/admin-panel/index.js +14 -15
  80. package/plugins/admin-panel/lib/is-rich-text-empty.js +23 -0
  81. package/plugins/admin-panel/lib/sanitize-rich-html.js +106 -0
  82. package/plugins/admin-panel/modules/dashboard.js +3 -2
  83. package/plugins/admin-panel/modules/user-management.js +90 -20
  84. package/plugins/audit-log/index.js +8 -2
  85. package/plugins/audit-log/middleware.js +72 -10
  86. package/plugins/data-exchange/import.js +21 -29
  87. package/plugins/index.js +4 -0
  88. package/plugins/rate-limit/index.js +132 -0
  89. package/plugins/redirect/index.js +204 -0
  90. package/plugins/rest-resources/index.js +2 -1
  91. package/plugins/swagger.js +2 -1
  92. package/plugins/upload/index.js +27 -101
  93. package/plugins/upload/local-file-provider.js +6 -2
  94. package/src/app-context.js +2 -23
  95. package/src/client-runtime/mount.js +14 -14
  96. package/src/file-router.js +124 -290
  97. package/src/helpers.js +0 -10
  98. package/src/http/compat-app.js +240 -0
  99. package/src/http/context.js +329 -0
  100. package/src/http/cookies.js +103 -0
  101. package/src/http/errors.js +31 -0
  102. package/src/http/index.js +28 -0
  103. package/src/http/middleware.js +102 -0
  104. package/src/http/multipart.js +61 -0
  105. package/src/http/node-serve.js +33 -0
  106. package/src/http/secure-headers.js +128 -0
  107. package/src/http/session.js +47 -0
  108. package/src/njk-frontmatter.js +156 -0
  109. package/src/plugin-manager.js +4 -2
  110. package/src/router-edge.js +325 -0
  111. package/src/server.js +192 -309
  112. package/templates/deploy/cloudflare/src/worker.js +5 -0
  113. package/templates/deploy/cloudflare/stubs/empty.mjs +2 -0
  114. package/templates/deploy/cloudflare/webspresso.build.js.example +10 -0
  115. package/templates/deploy/cloudflare/webspresso.db.js.example +24 -0
  116. package/templates/deploy/cloudflare/wrangler.toml +26 -0
  117. package/templates/deploy/docker/.dockerignore +8 -0
  118. package/templates/deploy/docker/Dockerfile +17 -0
  119. package/templates/deploy/docker/docker-compose.yml +9 -0
  120. package/templates/deploy/node/webspresso.build.js.example +7 -0
  121. package/templates/deploy/pm2/ecosystem.config.js +17 -0
  122. package/templates/skills/webspresso-usage/REFERENCE-framework.md +276 -0
  123. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +93 -0
  124. package/templates/skills/webspresso-usage/SKILL.md +32 -278
package/README.md CHANGED
@@ -5,6 +5,20 @@
5
5
 
6
6
  A minimal, file-based SSR framework for Node.js with Nunjucks templating.
7
7
 
8
+ > **Current release:** `0.1.0-alpha.0` — Hono-based HTTP stack, production build compiler, and Cloudflare Workers adapter. See **[What's new](#whats-new)** and **[CHANGELOG.md](CHANGELOG.md)**.
9
+
10
+ ## What's new
11
+
12
+ Highlights in **`0.1.0-alpha.0`** (see **[CHANGELOG.md](CHANGELOG.md)** for the full list):
13
+
14
+ | Area | Summary |
15
+ |------|---------|
16
+ | **HTTP runtime** | **Hono** under the hood; handlers still use **`(req, res, next)`** and **`app.listen()`** via compat wrappers. |
17
+ | **Production build** | **`webspresso build --adapter node\|cloudflare\|bun`** — discover → analyze → compile → manifest → bundle → validate; output under **`.webspresso/`**. |
18
+ | **Cloudflare Workers** | **`webspresso add deploy --provider cloudflare`** scaffolds Wrangler + **`webspresso.build.js`**; build emits **precompiled `templates.mjs`** (walks **`extends` / `include`** from `views/`). Worker entry uses **`createWorkerApp`** via **`webspresso/build/runtime/create-app-from-manifest`** (not the Node server bundle). |
19
+ | **D1 on Workers** | Wrangler **`env.DB`** → **`req.db`** / **`getDb()`** when the generated entry wires **`knex`** + **`knex-cloudflare-d1`**. |
20
+ | **Package exports** | Subpaths: **`webspresso/build`**, **`webspresso/core/auth`**, **`webspresso/core/orm`**, **`webspresso/plugins/*`**, plus split manifest helpers (**`create-app-from-manifest`** vs **`create-app-from-manifest-node`**). |
21
+
8
22
  ## Features
9
23
 
10
24
  - **File-Based Routing**: Create pages by adding `.njk` files to a `pages/` directory
@@ -16,9 +30,11 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
16
30
  - **Template Helpers**: Laravel-inspired helper functions available in templates
17
31
  - **Plugin System**: Extensible architecture with version control and inter-plugin communication
18
32
  - **Built-in Plugins**: Development dashboard, sitemap generator, SEO checker, analytics integration (Google, Yandex, Bing), self-hosted site analytics, optional Swagger UI for HTTP APIs, configurable HTTP health probe endpoint, optional REST CRUD routes from ORM models, optional admin UI for ORM query cache metrics and purge, optional **admin-only spreadsheet exchange** (Excel export, CSV/XLSX import via `dataExchangePlugin`)
19
- - **Session authentication** (optional): `createAuth` / `quickAuth` in **`webspresso/core/auth`** — pass the manager to **`createApp({ auth })`** for `express-session`, `req.user` / `req.auth`, remember-me tokens, and policy-style authorization. Full walkthrough: **[`doc/index.html#authentication`](doc/index.html#authentication)**.
33
+ - **Session authentication** (optional): `createAuth` / `quickAuth` in **`webspresso/core/auth`** — pass the manager to **`createApp({ auth })`** for encrypted cookie sessions (`hono-sessions`), `req.user` / `req.auth`, remember-me tokens, and policy-style authorization. Full walkthrough: **[`doc/index.html#authentication`](doc/index.html#authentication)**.
20
34
  - **Optional client runtime** (Alpine.js + [swup](https://swup.js.org/)): **`createApp({ clientRuntime: { alpine, swup } })`** serves scripts under **`/__webspresso/client-runtime/`** and exposes **`clientRuntime`** in Nunjucks; layouts can include **`views/partials/webspresso-client-runtime.njk`**. Env overrides: **`WEBSPRESSO_ALPINE`**, **`WEBSPRESSO_SWUP`**. Demo: **`examples/alpine-swup-demo/`**. Details: **[`doc/index.html#client-runtime`](doc/index.html#client-runtime)**.
21
35
  - **TypeScript**: Published **`index.d.ts`** (via `package.json` `"types"`) for `createApp`, ORM, plugins, and router helpers — use from TS/JS with IDE autocomplete; runtime stays CommonJS
36
+ - **Application kernel (optional)**: In-process **`kernel`** API (`require('webspresso').kernel`) — event bus (`dispatch` / `publish`), **`kernel.createApp()`** (namespaced differently from SSR **`createApp`**), **`definePlugin`** / **`defineFlow`**, minimal **`{{ }}` view resolver**, and simulated **`BaseRepository`** with `orm.<resource>.*` events. Ships as **`core/kernel/`** on npm. Demo: **`node core/kernel/run-demo.js`**. Docs: **[`doc/index.html#application-kernel`](doc/index.html#application-kernel)**.
37
+ - **Production builds & Cloudflare Workers**: **`webspresso build --adapter cloudflare`** emits a Wrangler-ready worker (manifest, precompiled Nunjucks, static assets). Full guide: **[`doc/index.html#cloudflare-workers`](doc/index.html#cloudflare-workers)** · summary below in **[Deployment](#deployment)**.
22
38
 
23
39
  ## Installation
24
40
 
@@ -36,10 +52,56 @@ The npm package ships with **[`index.d.ts`](index.d.ts)** so consumers get typin
36
52
  import { createApp, defineModel, zdb } from 'webspresso';
37
53
  ```
38
54
 
39
- Install **`@types/express`** in your app if you want full **`Express.Application`** / **`Request`** / **`Response`** inference when you touch `createApp().app` or write middleware. **`knex`** and **`zod`** bring their own types.
55
+ **`index.d.ts`** exports **`WebspressoCompatApp`**, **`WebspressoRequest`**, and **`WebspressoResponse`** for route handlers and middleware (Express-shaped API on Hono). Install **`hono`** in your app if you need Hono core types when extending `createApp().app`. **`knex`** and **`zod`** bring their own types.
40
56
 
41
57
  Framework development (this repo): run **`npm run check:types`** to typecheck the declarations against a small smoke file (`tests/ts-smoke/`).
42
58
 
59
+ ### Migrating from the Express-based Webspresso stack
60
+
61
+ Major versions use **Hono** instead of Express. Breaking changes:
62
+
63
+ | Before (Express) | After (Hono) |
64
+ |----------------|--------------|
65
+ | `createApp().app` is `express.Application` | `createApp().app` is **`WebspressoCompatApp`** (Hono + `listen`, `get`, `post`, …) |
66
+ | `express-session`, `cookie-parser`, `helmet`, `multer` | Built into Webspresso (`hono-sessions`, secure-headers, `parseBody` / upload plugin) |
67
+ | `app.listen(port)` via Express | Same call — implemented with **`@hono/node-server`** |
68
+ | Custom middleware `(req, res, next)` | Same signature; `req` / `res` are compat wrappers |
69
+ | `supertest` in tests | Use **`app.fetch`** or project test helper (`tests/helpers/http.js`) |
70
+
71
+ **`server.js` scaffold** (from `webspresso new`) still uses `app.listen(PORT, callback)` — no change required for basic apps.
72
+
73
+ **Rate limiting:** optional peer **`hono-rate-limiter`**; built-in **`rateLimitPlugin`** provides in-memory limiters for file routes.
74
+
75
+ **Raw Hono:** `createApp().app._hono` exposes the underlying Hono instance for advanced routing.
76
+
77
+ ### Package subpath exports
78
+
79
+ The npm **`exports`** field exposes focused entry points (CommonJS, with types on the root):
80
+
81
+ | Import | Use |
82
+ |--------|-----|
83
+ | `webspresso` | `createApp`, router utils, ORM re-exports, plugins index |
84
+ | `webspresso/build` | `runBuild`, `BuildConfig`, build diagnostics |
85
+ | `webspresso/build/runtime/create-app-from-manifest` | **Cloudflare Worker** manifest bootstrap → **`createWorkerApp`** |
86
+ | `webspresso/build/runtime/create-app-from-manifest-node` | **Node** manifest bootstrap (full `server.js` feature set) |
87
+ | `webspresso/core/auth` | `createAuth`, `quickAuth`, session middleware |
88
+ | `webspresso/core/orm` | `defineModel`, `createDatabase`, `zdb` |
89
+ | `webspresso/plugins/*` | Individual built-in plugins |
90
+
91
+ Use the **worker** manifest path in generated **`.webspresso/worker/index.mjs`** so Wrangler does not bundle bcrypt, admin plugins, or filesystem route scanning.
92
+
93
+ ## Application kernel (`kernel`)
94
+
95
+ Do **not** confuse **`kernel.createApp()`** with the package root **`createApp`** used for SSR—it returns a different object (event bus, optional flows, and a minimal view resolver). It does **not** modify Knex ORM behavior or HTTP routing.
96
+
97
+ ```javascript
98
+ const { kernel } = require('webspresso');
99
+ const app = kernel.createApp();
100
+ app.events.on('orm.post.afterCreate', async (ctx) => { /* ... */ });
101
+ ```
102
+
103
+ See **[`doc/index.html#application-kernel`](doc/index.html#application-kernel)** · source modules: [`core/kernel/`](core/kernel/). Cursor skill: **`REFERENCE-kernel.md`** (installed via `webspresso skill --preset webspresso`).
104
+
43
105
  ## Quick Start
44
106
 
45
107
  ### Using CLI (Recommended)
@@ -234,7 +296,7 @@ webspresso doctor --strict
234
296
 
235
297
  Scaffold a **Cursor Agent Skill**: creates `.cursor/skills/<name>/SKILL.md` with valid YAML frontmatter (`name`, `description`) and a short markdown template for AI tooling. Use `--global` to write under `~/.cursor/skills/` instead of the current project.
236
298
 
237
- **Bundled preset:** `--preset webspresso` copies the full **Webspresso usage** reference skill (framework routing, ORM, plugins, CLI, pitfalls) into `.cursor/skills/webspresso-usage/SKILL.md` — no prompts.
299
+ **Bundled preset:** `--preset webspresso` copies **`SKILL.md`** (short index), **`REFERENCE-framework.md`** (SSR `createApp`, routes, ORM, auth, plugins, CLI), and **`REFERENCE-kernel.md`** (`kernel` event bus / flows) into **`.cursor/skills/webspresso-usage/`** — no prompts.
238
300
 
239
301
  ```bash
240
302
  webspresso skill my-workflow
@@ -270,6 +332,59 @@ npm run watch:css # Watch and rebuild CSS on changes
270
332
  npm run dev # Starts both CSS watch and dev server
271
333
  ```
272
334
 
335
+ ### `webspresso add deploy`
336
+
337
+ Scaffold deployment provider files in the current project.
338
+
339
+ ```bash
340
+ # Cloudflare Workers + Wrangler + D1 example config
341
+ webspresso add deploy --provider cloudflare
342
+
343
+ # Docker or PM2 (Node process)
344
+ webspresso add deploy --provider docker
345
+ webspresso add deploy --provider pm2
346
+
347
+ # Multiple providers at once
348
+ webspresso add deploy --provider cloudflare,docker
349
+ ```
350
+
351
+ | Provider | Files created |
352
+ |----------|----------------|
353
+ | `cloudflare` | `wrangler.toml`, `webspresso.build.js`, optional `webspresso.db.js` (D1), legacy `src/worker.js` stub |
354
+ | `docker` | `Dockerfile`, `.dockerignore`, `docker-compose.yml` |
355
+ | `pm2` | `ecosystem.config.js` |
356
+
357
+ After scaffolding, run **`webspresso build --adapter <name>`** then deploy with the provider’s tool (Wrangler, Docker, PM2). See **[Deployment](#deployment)** and **[`doc/index.html#cloudflare-workers`](doc/index.html#cloudflare-workers)**.
358
+
359
+ ### `webspresso build`
360
+
361
+ Compile routes into a **build manifest** and write adapter-specific output under **`.webspresso/`**.
362
+
363
+ ```bash
364
+ # Node production entry (default when webspresso.build.js sets adapter: 'node')
365
+ webspresso build
366
+ webspresso build --adapter node
367
+
368
+ # Cloudflare Workers (Wrangler bundles .webspresso/worker/)
369
+ webspresso build --adapter cloudflare
370
+
371
+ # Manifest only — skip esbuild pre-bundle (Cloudflare always skips; Wrangler bundles)
372
+ webspresso build --adapter node --skip-bundle
373
+
374
+ # Fail CI on validation warnings (edge-incompatible imports, unresolved templates, …)
375
+ webspresso build --adapter cloudflare --fail-on-warnings
376
+ ```
377
+
378
+ | Adapter | Output directory | Runtime entry |
379
+ |---------|------------------|---------------|
380
+ | `node` | `.webspresso/server/` | `index.mjs` + `manifest.json` + `handlers.mjs` |
381
+ | `cloudflare` | `.webspresso/worker/` | same layout + **`templates.mjs`** (precompiled Nunjucks) |
382
+ | `bun` | `.webspresso/server/` (Bun adapter) | experimental |
383
+
384
+ Configure defaults in **`webspresso.build.js`** at the project root (see templates under `templates/deploy/`).
385
+
386
+ **Build phases** (summary): discover `pages/` → analyze edge imports & Nunjucks graph → compile handlers + inline i18n → write **`manifest.json`** → bundle adapter output (Node: optional esbuild; Cloudflare: **`templates.mjs`** + skip framework esbuild) → validate. Compiler internals: [`core/build/README.md`](core/build/README.md).
387
+
273
388
  ### `webspresso favicon:generate <source.png>`
274
389
 
275
390
  Generate favicon PNG files and `favicons.njk` partial from a single source PNG.
@@ -356,7 +471,7 @@ my-app/
356
471
 
357
472
  ### `createApp(options)`
358
473
 
359
- Creates and configures the Express app.
474
+ Creates and configures the **Hono-based compat app** (`WebspressoCompatApp`).
360
475
 
361
476
  **Options:**
362
477
  - `pagesDir` (required): Path to pages directory
@@ -371,7 +486,7 @@ Creates and configures the Express app.
371
486
  - `middlewares` (optional): Named middleware registry for routes
372
487
  - `clientRuntime` (optional): **`{ alpine?: boolean | object, swup?: boolean | object }`**. When either flag is enabled, mounts vendored Alpine 3 / swup 4 (+ Head + Scripts plugins) at **`/__webspresso/client-runtime/*`** and passes resolved **`{ alpine, swup }`** into SSR templates as **`clientRuntime`**. Override with env **`WEBSPRESSO_ALPINE`** / **`WEBSPRESSO_SWUP`** (`1` or `true`). Package exports **`resolveClientRuntime`** and **`CLIENT_RUNTIME_BASE`**. See **[Client runtime](#client-runtime)** below and **[`doc/index.html#client-runtime`](doc/index.html#client-runtime)**.
373
488
  - `auth` (optional): `AuthManager` from **`createAuth()`** / **`quickAuth()`** (`require('webspresso/core/auth')`). Registers session + cookie parsing, attaches **`req.auth`** / **`req.user`**, and injects named route middleware **`auth`** and **`guest`** (do not reuse those names for custom handlers if you pass `auth`). See **[`doc/index.html#authentication`](doc/index.html#authentication)**.
374
- - `setupRoutes(app, ctx)` (optional): Register custom Express routes after file routes and plugin `onRoutesReady`, before the 404 handler — use for login/logout handlers when using the auth module; **`ctx.authMiddleware`** exposes `requireAuth`, `requireGuest`, etc.; **`ctx.clientRuntime`** is **`{ alpine, swup }`**
489
+ - `setupRoutes(app, ctx)` (optional): Register custom routes on the compat **`app`** after file routes and plugin `onRoutesReady`, before the 404 handler — use for login/logout handlers when using the auth module; **`ctx.authMiddleware`** exposes `requireAuth`, `requireGuest`, etc.; **`ctx.clientRuntime`** is **`{ alpine, swup }`**
375
490
 
376
491
  **Example with middlewares:**
377
492
 
@@ -394,7 +509,7 @@ const { app } = createApp({
394
509
  }
395
510
  next();
396
511
  },
397
- rateLimit: require('express-rate-limit')({ windowMs: 60000, max: 100 })
512
+ rateLimit: require('hono-rate-limiter')({ windowMs: 60_000, limit: 100 })
398
513
  }
399
514
  });
400
515
  ```
@@ -501,7 +616,7 @@ createApp({
501
616
  | `getDb()` | Same instance as `req.db`; **throws** if no `db` was passed to `createApp` |
502
617
  | `hasDb()` | `true` if `createApp` was given `db` |
503
618
  | `getAppContext()` | `{ db }` — `db` may be `null` |
504
- | `attachDbMiddleware` | Express middleware to populate `req.db` for non–file-router routes |
619
+ | `attachDbMiddleware` | Compat middleware to populate `req.db` for non–file-router routes |
505
620
  | `resetAppContext()` | Clears context (mainly for tests) |
506
621
  | `setAppContext(partial)` | Low-level merge; normally only `createApp` uses this |
507
622
 
@@ -533,6 +648,14 @@ Error templates receive these variables:
533
648
  - `500.njk`: `{ error, status, isDev }`
534
649
  - `503.njk`: `{ url, method, isDev }`
535
650
 
651
+ **How errors reach this handler**
652
+
653
+ Unhandled errors from file-based routes (`pages/**/*.njk` `load()` / middleware / render, and `pages/api/**/*.js` handlers) are forwarded with `next(err)`, so they go through the central error handler. That means `errorPages.serverError` and `errorPages.timeout` apply to those failures as well (not only to routes you add with `setupRoutes`).
654
+
655
+ - **`pages/_hooks.js` `onError(ctx, err)`** runs **before** the central handler (for both SSR and API file routes). Use it for logging or APM (`Sentry.captureException`, `newrelic.noticeError`, etc.). The error is also on **`ctx.error`**.
656
+
657
+ - **JSON vs HTML:** Requests whose path starts with **`/api`** always get a **JSON** error body from the default branch (and the default **string** `serverError` / `timeout` Nunjucks templates are skipped for those paths). Other clients use **`Accept`** as before: prefer HTML when the client accepts HTML.
658
+
536
659
  **Request Timeout:**
537
660
 
538
661
  Configure request timeout with `connect-timeout`:
@@ -682,6 +805,22 @@ Options:
682
805
  - `path` - Custom dashboard path (default: `/_webspresso`)
683
806
  - `enabled` - Force enable/disable (default: auto based on NODE_ENV)
684
807
 
808
+ **Redirect plugin:**
809
+ - Runs in `register()` **before** file-based routes, so configured paths override SSR pages.
810
+ - Rules: `from` (string or `RegExp`), `to` (path or URL), optional `status` (301/302/303/307/308), optional `methods` (`'*'` or a list; default plugin methods are `GET` and `HEAD` only).
811
+ - `preserveQuery` (default `true`) appends the request query when `to` has no `?`. External `to` values require `allowExternal: true`.
812
+
813
+ ```javascript
814
+ const { redirectPlugin } = require('webspresso/plugins');
815
+
816
+ redirectPlugin({
817
+ rules: [
818
+ { from: '/old-blog', to: '/blog', status: 301 },
819
+ { from: /^\/wiki\/(.*)$/, to: '/docs' },
820
+ ],
821
+ });
822
+ ```
823
+
685
824
  **Sitemap Plugin:**
686
825
  - Generates `/sitemap.xml` from routes automatically
687
826
  - **Dynamic Database Content**: Generate URLs from database records
@@ -768,7 +907,7 @@ Options:
768
907
  - `userManagement` - Site-user admin UI (`enabled`, `model` matching ORM user table, optional `fields` map). SPA routes: `/_admin/users`, `/_admin/users/new`, …; APIs: `/_admin/api/users*`. Admin staff still use **`admin_users`** / `/_admin` login; this is separate from **`req.user`** on the public site
769
908
  - `configure` - Callback `(registry) => void` for manual setup
770
909
 
771
- See **`doc/index.html#admin-user-management`** and **Session authentication** in **`.cursor/skills/webspresso-usage/SKILL.md`** for the split between **`adminUser`** and **`createApp({ auth })`**.
910
+ See **`doc/index.html#admin-user-management`** and **Session authentication** in **`.cursor/skills/webspresso-usage/REFERENCE-framework.md`** for the split between **`adminUser`** and **`createApp({ auth })`**.
772
911
 
773
912
  **Custom Admin Pages (registerModule):**
774
913
 
@@ -2190,6 +2329,197 @@ const app = createApp({
2190
2329
 
2191
2330
  In production, keep the plugin disabled or protect it with `authorize` / your own middleware.
2192
2331
 
2332
+ ## Deployment
2333
+
2334
+ Webspresso can compile your `pages/` tree into a **static route manifest** and ship a **production entry** for Node or **Cloudflare Workers**. Development still uses `webspresso dev` + `server.js`; production uses **`webspresso build`** + the target adapter.
2335
+
2336
+ Long-form reference: **[`doc/index.html#deployment`](doc/index.html#deployment)** · Cloudflare walkthrough: **[`doc/index.html#cloudflare-workers`](doc/index.html#cloudflare-workers)**.
2337
+
2338
+ ### Quick comparison
2339
+
2340
+ | | Node (`adapter: 'node'`) | Cloudflare (`adapter: 'cloudflare'`) |
2341
+ |--|--------------------------|--------------------------------------|
2342
+ | **Dev** | `webspresso dev` | `webspresso build --adapter cloudflare` then `npx wrangler dev` |
2343
+ | **Deploy** | `node server.js` or Docker / PM2 | `npx wrangler deploy` |
2344
+ | **Full `createApp`** | Yes — plugins, auth, file-router scan | **Edge worker runtime** — manifest routes only |
2345
+ | **ORM / Knex** | All supported drivers | **D1** via `webspresso.db.js` + `knex-cloudflare-d1` (optional) |
2346
+ | **Auth (`core/auth`)** | bcrypt sessions, remember-me | **Not on Workers** — use external auth or Node adapter |
2347
+ | **Plugins** | All built-ins | Edge-compatible only (no admin panel, upload, data-exchange) |
2348
+ | **Nunjucks** | Filesystem + watch | **Precompiled at build** (Workers disallow runtime `eval`) |
2349
+ | **Static files** | `publicDir` via Hono static | Wrangler **`[assets]`** binding |
2350
+
2351
+ ### Node production
2352
+
2353
+ 1. `webspresso add deploy --provider docker` or `pm2` (optional).
2354
+ 2. `webspresso build --adapter node` → `.webspresso/server/`.
2355
+ 3. Run `node .webspresso/server/index.mjs` or use your process manager / container with `NODE_ENV=production`.
2356
+
2357
+ The generated entry calls **`createAppFromManifest`** with the full Node server (same feature set as `server.js` + manifest mode).
2358
+
2359
+ ### Cloudflare Workers
2360
+
2361
+ #### Prerequisites
2362
+
2363
+ - [Wrangler](https://developers.cloudflare.com/workers/wrangler/) (`npm i -D wrangler`)
2364
+ - Cloudflare account (for deploy)
2365
+ - Project with `pages/`, `views/` (layouts), and `public/` (CSS built with `npm run build:css`)
2366
+
2367
+ #### 1. Scaffold
2368
+
2369
+ ```bash
2370
+ webspresso add deploy --provider cloudflare
2371
+ ```
2372
+
2373
+ Creates **`wrangler.toml`**, **`webspresso.build.js`**, and optional **`webspresso.db.js`** (D1). Ensure `wrangler.toml` includes:
2374
+
2375
+ ```toml
2376
+ compatibility_flags = ["nodejs_compat"]
2377
+ ```
2378
+
2379
+ (The template ships this flag — required for some Node polyfills in the worker bundle.)
2380
+
2381
+ #### 2. Configure build
2382
+
2383
+ **`webspresso.build.js`** (example):
2384
+
2385
+ ```javascript
2386
+ /** @type {import('webspresso/build').BuildConfig} */
2387
+ module.exports = {
2388
+ adapter: 'cloudflare',
2389
+ pagesDir: 'pages',
2390
+ viewsDir: 'views',
2391
+ publicDir: 'public',
2392
+ };
2393
+ ```
2394
+
2395
+ #### 3. Build
2396
+
2397
+ ```bash
2398
+ npm run build:css # Tailwind → public/css/style.css
2399
+ webspresso build --adapter cloudflare
2400
+ ```
2401
+
2402
+ **Output** (`.webspresso/worker/`):
2403
+
2404
+ | Artifact | Purpose |
2405
+ |----------|---------|
2406
+ | `manifest.json` | Routes, templates metadata, i18n blobs, build id |
2407
+ | `handlers.mjs` | Compiled API handlers + SSR route configs |
2408
+ | `templates.mjs` | **Precompiled** Nunjucks (`index.njk`, `layout.njk`, …) |
2409
+ | `index.mjs` | Worker entry (`fetch` → `createAppFromManifest`) |
2410
+ | `assets/public/` | Copy of `public/` for Wrangler **Assets** |
2411
+
2412
+ Cloudflare builds **skip** the framework’s esbuild pre-bundle; **Wrangler** bundles `index.mjs` + dependencies when you run `wrangler dev` / `deploy`.
2413
+
2414
+ #### 4. Local worker dev
2415
+
2416
+ ```bash
2417
+ npx wrangler dev
2418
+ # or: npx wrangler dev --port 8788
2419
+ ```
2420
+
2421
+ `webspresso dev --adapter cloudflare` only reminds you to build + run Wrangler — use the two commands above for local Workers testing.
2422
+
2423
+ #### 5. Deploy
2424
+
2425
+ ```bash
2426
+ npx wrangler deploy
2427
+ ```
2428
+
2429
+ Configure `name`, routes, and D1 in **`wrangler.toml`**.
2430
+
2431
+ #### D1 database (optional)
2432
+
2433
+ When `webspresso.db.js` is present, the template wires **D1** for production:
2434
+
2435
+ ```javascript
2436
+ // webspresso.db.js — excerpt
2437
+ module.exports = {
2438
+ development: { client: 'better-sqlite3', connection: { filename: './dev.sqlite3' }, useNullAsDefault: true },
2439
+ production: {
2440
+ client: process.env.WEBSPRESSO_D1_REMOTE ? 'd1-remote' : 'd1',
2441
+ connection: process.env.WEBSPRESSO_D1_REMOTE
2442
+ ? { accountId: process.env.CF_ACCOUNT_ID, databaseId: process.env.CF_D1_DATABASE_ID, apiToken: process.env.CF_API_TOKEN }
2443
+ : {},
2444
+ migrations: { directory: './migrations' },
2445
+ },
2446
+ };
2447
+ ```
2448
+
2449
+ - **Local D1:** `wrangler dev` uses a local D1 emulator; run migrations with `webspresso db:migrate`.
2450
+ - **Remote D1:** `WEBSPRESSO_D1_REMOTE=1` plus `CF_*` env vars for CLI migrations against the remote database.
2451
+
2452
+ Pass **`env.DB`** from Wrangler into the worker via bindings (template `[[d1_databases]]`).
2453
+
2454
+ #### What works on Workers
2455
+
2456
+ - SSR pages from manifest (`pages/*.njk` + `views/` layouts)
2457
+ - API routes compiled into `handlers.mjs`
2458
+ - i18n JSON baked into the manifest
2459
+ - Static assets via **`[assets]`**
2460
+ - Zod validation on API routes
2461
+ - Optional D1 + ORM when configured — API handlers get **`req.db`** from the **`DB`** binding (same as Node when `createApp({ db })` was used)
2462
+
2463
+ **D1 in API routes (Workers):**
2464
+
2465
+ ```javascript
2466
+ // pages/api/notes.get.js — env.DB resolved at worker cold start
2467
+ module.exports = async function handler(req, res) {
2468
+ if (!req.db) return res.status(503).json({ error: 'Database not configured' });
2469
+ const notes = await req.db.getRepository('Note').query().orderBy('created_at', 'desc').list();
2470
+ res.json(notes);
2471
+ };
2472
+ ```
2473
+
2474
+ Install **`knex`** and **`knex-cloudflare-d1`** in the project; the scaffolded worker entry passes them via **`dbRuntime`** (see generated **`.webspresso/worker/index.mjs`**).
2475
+
2476
+ #### Limitations (build will error or warn)
2477
+
2478
+ | Not supported on Cloudflare adapter | Alternative |
2479
+ |-----------------------------------|-------------|
2480
+ | `admin-panel`, `upload`, `data-exchange` plugins | Deploy on **Node** or split admin to a separate service |
2481
+ | `createApp({ auth })` / bcrypt | External auth (JWT, Cloudflare Access) or Node adapter |
2482
+ | In-memory session store | KV, D1, or cookie-only JWT |
2483
+ | `clientRuntime` (Alpine/swup) in worker entry | Disabled in generated entry; enable only on Node |
2484
+ | Runtime filesystem route scan | Use **`webspresso build`** manifest instead |
2485
+ | `better-sqlite3`, `pg`, `mysql2` in worker bundle | **D1** or HTTP API to a Node backend |
2486
+
2487
+ Build-time validation reports **`WS_BUILD_PLUGIN_UNSUPPORTED`**, **`WS_BUILD_EDGE_INCOMPATIBLE`**, and **`WS_BUILD_SESSION_MEMORY`** when applicable.
2488
+
2489
+ #### Troubleshooting
2490
+
2491
+ | Symptom | Likely cause | Fix |
2492
+ |---------|----------------|-----|
2493
+ | `Could not resolve "crypto"` / `fs` during **webspresso** esbuild | Wrong adapter or old build path | Use `--adapter cloudflare` (skips framework esbuild); let Wrangler bundle |
2494
+ | `bcrypt` / `node-pre-gyp` / `aws-sdk` in **Wrangler** bundle | Full `server.js` pulled into worker | Ensure entry imports **`webspresso/build/runtime/create-app-from-manifest`** (worker-only), not Node manifest path |
2495
+ | `EvalError: Code generation from strings disallowed` | Nunjucks compiled at runtime | Rebuild — need **`templates.mjs`** from current `webspresso build` |
2496
+ | `layout.njk` not found at runtime | Missing parent template in precompile | Keep layouts in **`views/`**; rebuild — build walks **`extends` / `include`** and precompiles the full graph into **`templates.mjs`** |
2497
+ | Empty CSS on worker | Assets not built | Run **`npm run build:css`** before `webspresso build` |
2498
+ | `Address already in use` on `wrangler dev` | Port taken | `npx wrangler dev --port 8788` |
2499
+
2500
+ #### Architecture (Cloudflare)
2501
+
2502
+ ```
2503
+ pages/ + views/ → webspresso build → .webspresso/worker/
2504
+ ├── manifest.json
2505
+ ├── handlers.mjs
2506
+ ├── templates.mjs (precompiled)
2507
+ └── index.mjs → createWorkerApp()
2508
+ wrangler dev|deploy → Workers runtime → fetch(request, env)
2509
+ ```
2510
+
2511
+ Worker runtime uses **`createWorkerApp`** (`core/build/runtime/create-worker-app.js`): Hono compat app, manifest route mounting, precompiled Nunjucks — **without** `src/server.js`, file-router `fs` scan, or auth.
2512
+
2513
+ ### Docker / PM2
2514
+
2515
+ ```bash
2516
+ webspresso add deploy --provider docker # Dockerfile + compose
2517
+ webspresso add deploy --provider pm2 # ecosystem.config.js
2518
+ webspresso build --adapter node
2519
+ ```
2520
+
2521
+ Run the generated **`.webspresso/server/index.mjs`** inside your container or PM2 process (set `NODE_ENV=production`, expose `PORT`).
2522
+
2193
2523
  ## Development
2194
2524
 
2195
2525
  Native addons (**better-sqlite3**, **bcrypt**, **sharp**) are compiled for your current Node ABI. After switching Node major versions (e.g. nvm, fnm, Volta), run **`npm run rebuild:native`** or a clean install: `rm -rf node_modules && npm ci`. **chokidar** is not ABI-tied like those drivers; if file watching misbehaves, reinstall dependencies. The repo includes [`.nvmrc`](.nvmrc) (Node 20 LTS) as a known-good default for this project.
@@ -2210,8 +2540,9 @@ npm run test:watch
2210
2540
  # Run tests with coverage
2211
2541
  npm run test:coverage
2212
2542
 
2213
- # Micro-benchmarks (Vitest bench; also runs in CI on the test matrix)
2543
+ # Micro-benchmarks (Vitest bench; CI: main push uploads benchmark-baseline artifact, PRs compare against it)
2214
2544
  npm run bench
2545
+ # Local baseline + compare: npm run bench:ci:local
2215
2546
  ```
2216
2547
 
2217
2548
  ## License
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Bun adapter stub — future
3
+ * @module adapters/bun
4
+ */
5
+
6
+ const nodeAdapter = require('../node');
7
+
8
+ module.exports = {
9
+ ...nodeAdapter,
10
+ name: 'bun',
11
+ version: '0.1.0',
12
+ capabilities: {
13
+ ...nodeAdapter.capabilities,
14
+ fetch: true,
15
+ listen: true,
16
+ },
17
+
18
+ bundleOptions(manifest, outputDir) {
19
+ const base = nodeAdapter.bundleOptions(manifest, outputDir);
20
+ return { ...base, target: 'esnext' };
21
+ },
22
+ };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Cloudflare Workers deployment adapter
3
+ * @module adapters/cloudflare
4
+ */
5
+
6
+ const path = require('path');
7
+ const { capabilityList } = require('../types');
8
+
9
+ const capabilities = {
10
+ fetch: true,
11
+ listen: false,
12
+ fs: false,
13
+ nativeModules: false,
14
+ d1: true,
15
+ kv: true,
16
+ r2: true,
17
+ assets: true,
18
+ };
19
+
20
+ const NODE_ONLY = new Set(['admin-panel', 'adminPanelPlugin', 'upload', 'uploadPlugin', 'data-exchange']);
21
+
22
+ module.exports = {
23
+ name: 'cloudflare',
24
+ version: '1.0.0',
25
+ capabilities,
26
+
27
+ /**
28
+ * @param {import('../../core/build/types').WebspressoManifest} manifest
29
+ */
30
+ validate(manifest) {
31
+ /** @type {object[]} */
32
+ const errors = [];
33
+ /** @type {object[]} */
34
+ const warnings = [];
35
+
36
+ for (const p of manifest.plugins || []) {
37
+ if (!p.edgeCompatible) {
38
+ errors.push({
39
+ code: 'WS_BUILD_PLUGIN_UNSUPPORTED',
40
+ message: `Plugin "${p.name}" is not edge-compatible`,
41
+ hint: 'Remove from config/app.js or deploy on Node adapter.',
42
+ });
43
+ }
44
+ }
45
+
46
+ if (manifest.middleware?.session?.store === 'memory') {
47
+ errors.push({
48
+ code: 'WS_BUILD_SESSION_MEMORY',
49
+ message: 'In-memory sessions are not supported on Cloudflare Workers',
50
+ hint: 'Use KV/D1 session store or cookie-only JWT.',
51
+ });
52
+ }
53
+
54
+ return { ok: errors.length === 0, errors, warnings };
55
+ },
56
+
57
+ /**
58
+ * @param {import('../../core/build/types').WebspressoManifest} _manifest
59
+ * @param {{ cwd?: string }} [ctx]
60
+ */
61
+ generateEntry(_manifest, ctx) {
62
+ const appRoot = ctx?.cwd || process.cwd();
63
+ const modulePaths = [path.join(appRoot, 'node_modules'), appRoot];
64
+ return `/**
65
+ * Webspresso Cloudflare Worker entry — generated by webspresso build
66
+ */
67
+ import knex from 'knex';
68
+ import d1Base from 'knex-cloudflare-d1';
69
+ import createD1KnexClient from 'webspresso/core/orm/d1-knex-client.js';
70
+ import { createAppFromManifest } from 'webspresso/build/runtime/create-app-from-manifest';
71
+ const d1Client = createD1KnexClient(d1Base);
72
+ import manifest from './manifest.json' assert { type: 'json' };
73
+ import { handlers } from './handlers.mjs';
74
+ import precompiledTemplates from './templates.mjs';
75
+
76
+ const modulePaths = ${JSON.stringify(modulePaths)};
77
+
78
+ /** @type {{ app: import('webspresso').WebspressoCompatApp } | null} */
79
+ let cached = null;
80
+
81
+ function getApp(env) {
82
+ if (!cached) {
83
+ cached = createAppFromManifest({
84
+ manifest,
85
+ handlers,
86
+ pagesDir: 'pages',
87
+ bindings: env,
88
+ modulePaths,
89
+ dbRuntime: { knex, d1Client },
90
+ precompiledTemplates,
91
+ clientRuntime: { alpine: false, swup: false },
92
+ logging: false,
93
+ });
94
+ }
95
+ return cached.app;
96
+ }
97
+
98
+ export default {
99
+ fetch(request, env, ctx) {
100
+ return getApp(env).fetch(request, env, ctx);
101
+ },
102
+ };
103
+ `;
104
+ },
105
+
106
+ /**
107
+ * @param {import('../../core/build/types').WebspressoManifest} _manifest
108
+ * @param {string} _outputDir
109
+ */
110
+ bundleOptions(_manifest, _outputDir) {
111
+ return {
112
+ platform: 'node',
113
+ format: 'esm',
114
+ target: 'es2022',
115
+ conditions: ['worker', 'import', 'require'],
116
+ mainFields: ['module', 'main'],
117
+ define: {
118
+ 'process.env.NODE_ENV': '"production"',
119
+ },
120
+ external: [
121
+ 'bcrypt',
122
+ 'better-sqlite3',
123
+ 'sharp',
124
+ 'mysql2',
125
+ 'pg',
126
+ 'knex',
127
+ 'sqlite3',
128
+ 'mysql',
129
+ 'tedious',
130
+ 'oracledb',
131
+ 'fsevents',
132
+ 'chokidar',
133
+ ],
134
+ };
135
+ },
136
+ };
137
+
138
+ module.exports.capabilityNames = capabilityList(capabilities);