webspresso 0.0.100 → 0.0.102

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 (88) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +3 -1
  3. package/.agents/ARCHITECTURE.md +11 -0
  4. package/.agents/AUTH.md +63 -0
  5. package/.agents/CLI.md +81 -0
  6. package/.agents/CONVENTIONS.md +4 -0
  7. package/.agents/EMAIL.md +84 -2
  8. package/.agents/EVENTS.md +179 -0
  9. package/.agents/ORM.md +59 -0
  10. package/.agents/PLUGINS.md +32 -6
  11. package/.agents/ROUTING.md +163 -3
  12. package/.agents/skills/webspresso-framework/SKILL.md +32 -0
  13. package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
  14. package/AGENTS.md +7 -0
  15. package/README.md +18 -5
  16. package/bin/commands/add-tailwind.js +10 -0
  17. package/bin/commands/build.js +34 -0
  18. package/bin/commands/db-makemigrations.js +83 -0
  19. package/bin/commands/list.js +96 -0
  20. package/bin/commands/make-command.js +93 -0
  21. package/bin/commands/make-event.js +80 -0
  22. package/bin/commands/make-factory.js +72 -0
  23. package/bin/commands/make-mail.js +93 -0
  24. package/bin/commands/make-policy.js +106 -0
  25. package/bin/commands/routes.js +8 -0
  26. package/bin/commands/run.js +195 -0
  27. package/bin/webspresso.js +17 -0
  28. package/core/applySchema.js +18 -1
  29. package/core/auth/bouncer.js +196 -0
  30. package/core/auth/index.js +10 -0
  31. package/core/auth/middleware.js +15 -1
  32. package/core/commands/define-command.js +48 -0
  33. package/core/commands/index.js +18 -0
  34. package/core/commands/registry.js +156 -0
  35. package/core/commands/runner.js +76 -0
  36. package/core/compression/compressible.js +1 -1
  37. package/core/compression/middleware.js +35 -251
  38. package/core/content/service.js +8 -2
  39. package/core/events/discovery.js +142 -0
  40. package/core/events/dispatcher.js +552 -0
  41. package/core/events/index.js +23 -0
  42. package/core/events/signal.js +92 -0
  43. package/core/forms/flash.js +234 -0
  44. package/core/forms/form-builder.js +214 -0
  45. package/core/forms/index.js +16 -0
  46. package/core/mail/index.js +23 -0
  47. package/core/mail/mailable.js +250 -0
  48. package/core/mail/mailer.js +184 -0
  49. package/core/mail/transports.js +238 -0
  50. package/core/orm/factory.js +250 -0
  51. package/core/orm/index.js +6 -0
  52. package/core/orm/migrations/diff.js +185 -0
  53. package/core/orm/migrations/index.js +22 -4
  54. package/core/orm/model.js +4 -0
  55. package/index.d.ts +422 -1
  56. package/index.js +111 -16
  57. package/package.json +9 -5
  58. package/plugins/admin-panel/api.js +509 -3
  59. package/plugins/admin-panel/index.js +29 -0
  60. package/plugins/content/models/content-entry.js +4 -3
  61. package/plugins/content/models/content-type.js +4 -3
  62. package/plugins/cors.js +2 -2
  63. package/plugins/csrf/index.js +1 -1
  64. package/plugins/email/index.js +48 -0
  65. package/plugins/email/preview-handler.js +555 -0
  66. package/plugins/email/service.js +23 -12
  67. package/plugins/email/transport.js +30 -11
  68. package/plugins/file-manager/index.js +13 -0
  69. package/plugins/index.js +2 -0
  70. package/plugins/lazy.js +258 -0
  71. package/plugins/redirect/index.js +1 -1
  72. package/src/api/api-loader.js +18 -2
  73. package/src/api/validation-i18n.js +231 -0
  74. package/src/app-context.js +48 -6
  75. package/src/dev-reload/index.js +9 -1
  76. package/src/discovery/route-manifest.js +197 -0
  77. package/src/discovery/scan-routes.js +80 -5
  78. package/src/file-router.js +153 -91
  79. package/src/helpers.js +143 -0
  80. package/src/njk-frontmatter.js +17 -1
  81. package/src/pages/page-loader.js +1 -1
  82. package/src/plugin-manager.js +12 -0
  83. package/src/routing/cascading.js +14 -10
  84. package/src/routing/route-table.js +1 -1
  85. package/src/server.js +243 -34
  86. package/src/services/builtins/system.js +4 -6
  87. package/src/services/registry.js +12 -2
  88. package/src/template/precompile.js +247 -0
@@ -151,8 +151,9 @@ Outgoing Webhook Dispatcher and Management Plugin with zero external dependencie
151
151
 
152
152
  ### 2.4 `adminPanelPlugin` (`plugins/admin-panel`)
153
153
  Mithril.js SPA Admin Panel mounted at `/_admin`.
154
- - **Options**: `{ path: '/_admin', db, title: 'Webspresso Admin' }`
154
+ - **Options**: `{ path: '/_admin', db, title: 'Webspresso Admin', lazy: false }`
155
155
  - **Features**: Auto CRUD screens for ORM models, user staff auth, custom page registration, widgets, field renderers.
156
+ - **On-Demand Hydration (`lazy: true`)**: When set to `true`, the admin panel plugin wraps itself with `lazyPlugin()`, deferring route mounting and internal setup until the first HTTP request hits `/_admin`.
156
157
 
157
158
  ### 2.3 `contentPlugin` (`plugins/content`)
158
159
  Schema-driven headless CMS & inline content editing.
@@ -168,11 +169,12 @@ Multipart file upload manager with local / cloud storage providers.
168
169
 
169
170
  ### 2.5 `fileManagerPlugin` (`plugins/file-manager`)
170
171
  Filesystem-first media library, directory explorer, and modal asset picker for Admin Panel.
171
- - **Options**: `{ baseDir: './public/uploads', publicBasePath: '/uploads', apiPrefix: '/files', pageTitle: 'File Manager', maxUploadBytes: 10485760, allowedExtensions, allowedMimeTypes, includePage: true }`
172
+ - **Options**: `{ baseDir: './public/uploads', publicBasePath: '/uploads', apiPrefix: '/files', pageTitle: 'File Manager', maxUploadBytes: 10485760, allowedExtensions, allowedMimeTypes, includePage: true, lazy: false }`
172
173
  - **Features**:
173
174
  - Directory tree navigation (breadcrumbs), safe subfolder creation (`mkdir`), item rename, delete, and move.
174
175
  - Path traversal attack protection (`resolveSafePath`).
175
176
  - Grid (thumbnail gallery) & List (table) view modes.
177
+ - On-demand hydration via `lazy: true`.
176
178
  - Search by filename and type filter (Images, Documents, Videos, Audio, Archives, Code).
177
179
  - Media preview modal with copy URL and direct playback/preview.
178
180
  - "Choose from Media Library" modal picker integrated with `FileUploadField` and `zdb.file()` form fields.
@@ -662,7 +664,31 @@ Zero-dependency OAuth 2.0 & Social Login plugin supporting Google, GitHub, and c
662
664
  - `fsy.oauthUrl('google')` āž” `"/auth/google"`
663
665
  - `fsy.oauthUrl('github', { returnTo: '/dashboard' })` āž” `"/auth/github?returnTo=%2Fdashboard"`
664
666
 
665
-
666
-
667
-
668
-
667
+ ### 2.20 `lazyPlugin` / `defineLazyPlugin` (`plugins/lazy`)
668
+ Zero-dependency on-demand plugin hydration wrapper that defers heavy plugin initialization, route mounting, and service registration until the first matching HTTP request or service invocation.
669
+ - **Why**: Drastically slashes application cold start time and peak memory footprint when mounting heavy modules (such as admin panels, media libraries, documentation portals, or internal diagnostic tools) that are not needed on every server startup.
670
+ - **Usage**:
671
+ ```javascript
672
+ const { lazyPlugin, adminPanelPlugin, fileManagerPlugin } = require('webspresso/plugins');
673
+
674
+ // Option 1: Using built-in lazy option
675
+ adminPanelPlugin({ db, lazy: true });
676
+ fileManagerPlugin({ lazy: true });
677
+
678
+ // Option 2: Wrapping custom or 3rd-party plugins
679
+ const heavyPlugin = lazyPlugin(
680
+ () => require('./my-heavy-plugin')({ config: 123 }),
681
+ {
682
+ name: 'my-heavy-plugin',
683
+ prefix: '/heavy', // Hydrates when request path starts with /heavy
684
+ services: ['heavy.process'], // Hydrates when ctx.service('heavy.process') is called
685
+ helpers: ['heavyHelper'], // Hydrates when fsy.heavyHelper() is evaluated
686
+ events: ['heavy.*'], // Hydrates when matching event is dispatched
687
+ onHydrate: (plugin) => console.log('Plugin hydrated!'),
688
+ }
689
+ );
690
+ ```
691
+ - **How It Works**:
692
+ - Mounts a lightweight Express router proxy at `prefix`.
693
+ - Stubs placeholder services into `ServiceRegistry` and proxy helpers into Nunjucks context.
694
+ - Intercepts incoming requests/calls, executes the plugin factory function, runs `register(ctx)` and `onRoutesReady(ctx)`, replaces proxies seamlessly with the real router/services, and forwards the initial call with zero dropped requests.
@@ -119,6 +119,73 @@ module.exports = {
119
119
  > [!IMPORTANT]
120
120
  > **NEVER use raw Knex queries** like `req.db('notes')` in route handlers. **ALWAYS use repositories**: `req.db.getRepository('Note')`. This guarantees schema validation, lifecycle hooks, soft deletes, and query caching are respected.
121
121
 
122
+ ### 2.1 Smart API Validation i18n & Error Translation
123
+
124
+ Webspresso features built-in **Smart Dual-Layer Auto-Translation** for Zod schema validation errors with **zero runtime overhead on valid requests**.
125
+
126
+ #### How It Works:
127
+ 1. **Explicit Custom Keys**: Define dictionary translation keys directly inside Zod schemas (e.g. `z.string({ message: 'auth.email_required' })` or `z.string().min(5, 'auth.password_min')`).
128
+ 2. **Dynamic Fallbacks**: If no explicit key is matched, the engine falls back to convention-based candidate keys:
129
+ - `validation.fields.<field>.<code/type>` (e.g., `validation.fields.email.invalid_string`)
130
+ - `validation.<code/type>` (e.g., `validation.too_small`, `validation.required`, `validation.invalid_string.email`)
131
+ - `zod.<code/type>`
132
+ 3. **Parameter Interpolation**: Dynamic schema parameters (e.g. `{{ min }}`, `{{ max }}`, `{{ expected }}`, `{{ received }}`) are automatically interpolated into translated messages or inline custom template strings.
133
+ 4. **Context Injection**: `apiCtx` provides `{ locale, t, service, db, sse }` to route handlers for immediate localized responses.
134
+
135
+ #### Example Usage:
136
+
137
+ ```json
138
+ // pages/locales/tr.json
139
+ {
140
+ "auth": {
141
+ "email_required": "Lütfen geçerli bir e-posta adresi giriniz"
142
+ },
143
+ "validation": {
144
+ "required": "Bu alan zorunludur",
145
+ "too_small": "En az {{ min }} karakter olmalıdır"
146
+ }
147
+ }
148
+ ```
149
+
150
+ ```javascript
151
+ // pages/api/auth/register.post.js
152
+ 'use strict';
153
+ const { defineApi } = require('webspresso');
154
+
155
+ module.exports = defineApi({
156
+ schema: ({ z }) => ({
157
+ body: z.object({
158
+ // Explicit dictionary translation key
159
+ email: z.string({ message: 'auth.email_required' }).email(),
160
+ // Automatic fallback to validation.too_small with {{ min }} interpolation
161
+ password: z.string().min(8),
162
+ }),
163
+ }),
164
+ handler: async (req, res, { t, locale }) => {
165
+ // req.locale and t() are injected into handler context
166
+ return res.status(201).json({
167
+ success: true,
168
+ message: t('auth.registered_success', { email: req.input.body.email }),
169
+ });
170
+ },
171
+ });
172
+ ```
173
+
174
+ Client requests sending `Accept-Language: tr` or query param `?lang=tr` will automatically receive fully localized validation errors in the 400 Bad Request response:
175
+ ```json
176
+ {
177
+ "error": "Validation failed",
178
+ "issues": [
179
+ {
180
+ "field": "password",
181
+ "code": "too_small",
182
+ "key": "validation.too_small",
183
+ "message": "En az 8 karakter olmalıdır"
184
+ }
185
+ ]
186
+ }
187
+ ```
188
+
122
189
  ---
123
190
 
124
191
  ## 3. Dynamic Route Parameters (`[param]`)
@@ -326,7 +393,37 @@ const routeList = app.routes.list();
326
393
  // ]
327
394
  ```
328
395
 
329
- ### 6.5 Server-Sent Events (SSE) Streaming (`res.sse`)
396
+ ### 6.5 Cold Start & High-Performance Routing Architecture
397
+
398
+ Webspresso incorporates multi-tier optimizations to ensure sub-millisecond cold starts in production and instant dev-server boot times:
399
+
400
+ #### 1. Single-Pass Unified Route Discovery (`discoverAppRoutes`)
401
+ Instead of performing multiple independent directory traversals, `discoverAppRoutes(rootDir, options)` discovers Page routes, API routes, and Route Groups in a single traversal pass:
402
+ ```javascript
403
+ const { discoverAppRoutes } = require('webspresso');
404
+
405
+ const { routeTable, pageRoutes, apiRoutes } = discoverAppRoutes(__dirname);
406
+ ```
407
+ Passing `discoveredRoutes` to `createApp({ discoveredRoutes })` skips internal discovery completely.
408
+
409
+ #### 2. Dev-Mode On-Demand (JIT) Route Evaluation (`lazyRoutes: true`)
410
+ In development (`NODE_ENV !== 'production'`), `lazyRoutes: true` is enabled by default:
411
+ - Route paths and HTTP methods are registered immediately with lightweight proxy handlers.
412
+ - Route handler modules (`pages/**/*.js` / `pages/api/**/*.js`) are required and evaluated **on-demand on the first incoming HTTP request**.
413
+ - Result: Dev server boots in ~15ms even in applications with hundreds of routes.
414
+ - Can be explicitly controlled via `createApp({ lazyRoutes: false })` to force eager evaluation.
415
+
416
+ #### 3. Production Ahead-of-Time (AOT) Route Manifest (`routes.manifest.json`)
417
+ For maximum production performance, compile your route table during the build step:
418
+ ```bash
419
+ npx webspresso build
420
+ ```
421
+ This generates `.webspresso/routes.manifest.json`. When `createApp({ routesManifest: true })` is used (or when `.webspresso/routes.manifest.json` exists in `NODE_ENV === 'production'`), Webspresso:
422
+ - Bypasses filesystem directory scanning (`pages/`) entirely.
423
+ - Bypasses regex path compilation and route sorting at boot.
424
+ - Instantly binds pre-sorted routes from the manifest.
425
+
426
+ ### 6.6 Server-Sent Events (SSE) Streaming (`res.sse`)
330
427
  For real-time event streaming and LLM token outputs, use the native `res.sse()` helper (or `{ sse }` from `ctx`):
331
428
 
332
429
  ```javascript
@@ -352,7 +449,7 @@ module.exports = defineApi({
352
449
  });
353
450
  ```
354
451
 
355
- ### 6.6 Cascading Middleware (`_middleware.js`) & Nested Layouts (`_layout.njk`)
452
+ ### 6.7 Cascading Middleware (`_middleware.js`) & Nested Layouts (`_layout.njk`)
356
453
 
357
454
  Webspresso supports folder-level cascading middleware inheritance and nested layouts across directory trees:
358
455
 
@@ -435,7 +532,70 @@ In SSR streaming mode (`stream: true` or `renderStream`):
435
532
 
436
533
  ---
437
534
 
438
- ## 7. Summary Checklist for Agents
535
+ ## 7. Form Handling, Flash Messages & Old Input (AdonisJS & Django-inspired)
536
+
537
+ For classic SSR forms (POST-Redirect-GET), Webspresso provides built-in flash data propagation and Django-style ModelForm rendering helpers.
538
+
539
+ ### 7.1 Redirect with Old Input & Validation Errors
540
+ In your form submission handler (API or custom POST action):
541
+ ```javascript
542
+ // pages/api/auth/register.post.js
543
+ module.exports = {
544
+ handler: async (req, res) => {
545
+ const { email, password } = req.body;
546
+
547
+ if (!email.includes('@')) {
548
+ return res
549
+ .withInput() // Preserves req.body (sanitizes sensitive fields like password)
550
+ .withErrors({ email: 'Please enter a valid email address' })
551
+ .withFlash('warning', 'Please review the highlighted errors')
552
+ .redirect('/register');
553
+ }
554
+
555
+ await req.service('user.create', { email, password });
556
+ return res.withFlash('success', 'Account created! Please log in.').redirect('/login');
557
+ }
558
+ };
559
+ ```
560
+
561
+ ### 7.2 Nunjucks Template Helpers (`fsy`)
562
+ In your Nunjucks templates (`pages/*.njk`):
563
+ ```html
564
+ {% if fsy.hasFlash('success') %}
565
+ <div class="alert alert-success">{{ fsy.flash('success') }}</div>
566
+ {% endif %}
567
+
568
+ <form method="POST" action="/api/auth/register">
569
+ {{ fsy.csrf() | safe }}
570
+
571
+ <!-- Manual field with old value and inline error -->
572
+ <div class="mb-3">
573
+ <label class="form-label">Email</label>
574
+ <input type="email" name="email"
575
+ value="{{ fsy.old('email') }}"
576
+ class="form-control {% if fsy.hasError('email') %}is-invalid{% endif %}">
577
+ {% if fsy.hasError('email') %}
578
+ <div class="invalid-feedback">{{ fsy.error('email') }}</div>
579
+ {% endif %}
580
+ </div>
581
+
582
+ <!-- Or use the Django-inspired field helper (one-liner) -->
583
+ {{ fsy.field('email', { type: 'email', placeholder: 'you@example.com' }) | safe }}
584
+
585
+ <button type="submit" class="btn btn-primary">Register</button>
586
+ </form>
587
+ ```
588
+
589
+ ### 7.3 Django ModelForm Helper (`fsy.formFor`)
590
+ Generate an entire HTML form from an ORM model definition with automatic widgets and validation error mapping:
591
+ ```html
592
+ {# Automatically excludes id, created_at, updated_at and generates appropriate widgets #}
593
+ {{ fsy.formFor(req.db.getRepository('Article'), { action: '/articles', submitLabel: 'Publish' }) | safe }}
594
+ ```
595
+
596
+ ---
597
+
598
+ ## 8. Summary Checklist for Agents
439
599
 
440
600
  When a user asks to add or modify endpoints:
441
601
  1. Identify the URL and method (e.g. `POST /api/auth/register` or `POST /auth/login`).
@@ -42,11 +42,43 @@ webspresso start # Start production server
42
42
  webspresso doctor # Run system sanity checks
43
43
  webspresso db:migrate # Run pending Knex migrations
44
44
  webspresso db:seed # Run database seeders
45
+ webspresso add:tailwind # Add Tailwind CSS v4 (Oxide engine, zero-config)
45
46
  webspresso agents:init # Scaffold AI Agent rules and skills
46
47
  ```
47
48
 
48
49
  ---
49
50
 
51
+ ## šŸŽØ Tailwind CSS v4 Integration
52
+
53
+ Webspresso natively supports **Tailwind CSS v4** (Oxide engine, LightningCSS, zero-config CSS-first architecture).
54
+
55
+ ### Setup & Commands
56
+ ```bash
57
+ # Add Tailwind CSS v4 (recommended, zero-config CSS-first)
58
+ npx webspresso add tailwind --v4
59
+
60
+ # Or upgrade an existing Tailwind v3 project to v4
61
+ npx webspresso add tailwind --upgrade
62
+ ```
63
+
64
+ ### Architecture & Conventions
65
+ - **CSS-First Directives**: `src/input.css` uses modern v4 `@import`:
66
+ ```css
67
+ @import "tailwindcss";
68
+ ```
69
+ - **Zero Configuration**: No `tailwind.config.js`, `postcss.config.js`, or `autoprefixer` required.
70
+ - **Compiled Output**: Compiles into `public/css/style.css` via `@tailwindcss/cli`.
71
+ - **Scripts in `package.json`**:
72
+ - `"build:css"`: `@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --minify`
73
+ - `"watch:css"`: `@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --watch`
74
+ - `"dev"`: Automatically runs CSS watcher alongside the server: `npm run watch:css & node --watch server.js`
75
+ - **Template Integration**: Link stylesheet in Nunjucks layouts (`views/layout.njk`):
76
+ ```nunjucks
77
+ <link rel="stylesheet" href="{{ fsy.asset('/css/style.css') }}">
78
+ ```
79
+
80
+ ---
81
+
50
82
  ## šŸ—ļø Core Architecture Overview
51
83
 
52
84
  - **`pages/` & `pages/api/`**: File-based SSR templates (`.njk`), companion data loaders (`.js`), and method-suffixed API endpoints (`.get.js`, `.post.js`).
@@ -85,10 +85,10 @@ module.exports = definePage({
85
85
  {% extends "layout.njk" %}
86
86
 
87
87
  {% block content %}
88
- <div class="product-page">
89
- <h1>{{ product.name }}</h1>
90
- <p class="price">{{ fsy.formatCurrency(product.price) }}</p>
91
- <p>{{ product.description }}</p>
88
+ <div class="max-w-4xl mx-auto p-6 bg-white dark:bg-gray-900 rounded-xl shadow-sm">
89
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ product.name }}</h1>
90
+ <p class="text-lg font-semibold text-emerald-600 mt-2">{{ fsy.formatCurrency(product.price) }}</p>
91
+ <p class="text-gray-600 dark:text-gray-300 mt-4 leading-relaxed">{{ product.description }}</p>
92
92
  </div>
93
93
  {% endblock %}
94
94
  ```
package/AGENTS.md CHANGED
@@ -17,6 +17,7 @@ When tasked with a feature or bugfix, **immediately consult the corresponding to
17
17
  | **Admin Panel (Page, Widget, Action, UI)** | [`.agents/ADMIN.md`](.agents/ADMIN.md) | `plugins/admin-panel/` |
18
18
  | **Authentication (Session / JWT / Guard)** | [`.agents/AUTH.md`](.agents/AUTH.md) | `core/auth/` |
19
19
  | **Background Jobs & Worker Queue** | [`.agents/QUEUE.md`](.agents/QUEUE.md) | `jobs/` |
20
+ | **Decoupled Events & Signals** | [`.agents/EVENTS.md`](.agents/EVENTS.md) | `core/events/`, `listeners/` |
20
21
  | **API Signatures & Types Verification** | [`index.d.ts`](index.d.ts) | **Canonical Ground Truth** |
21
22
 
22
23
  ---
@@ -57,6 +58,12 @@ Every coding agent working on Webspresso **MUST** follow this 4-step execution d
57
58
  - **Graceful Shutdown & Lifecycle Management**: Configured via `createApp({ server: { shutdown: { enabled: true, mode: 'graceful'|'force', timeout: 10000 } } })`. Socket connection tracking, request draining with `Connection: close`, and reverse-order plugin disposer cleanup.
58
59
  - **HTTP Response Compression**: Zero-dependency streaming zlib compression configured via `createApp({ server: { compression: true } })` with Brotli (`br`), Gzip (`gzip`), Deflate (`deflate`), threshold buffering (1 KB), and route-level opt-out (`res.compress(false)`).
59
60
  - **Custom Error & 404 Pages**: Configured via `createApp({ errorPages: { notFound, serverError, timeout } })`. Supports template paths (e.g. `notFound: '404.njk'`) or custom handler functions, passing `{ fsy, locale, isDev, url, method }` to templates.
61
+ - **Cold Start & High-Performance Optimizations**:
62
+ - Unified single-pass route scanner (`discoverAppRoutes`).
63
+ - Dev-mode on-demand (JIT) route evaluation (`lazyRoutes: true`, dev boot ~15ms).
64
+ - Ahead-of-time (AOT) route table manifest (`webspresso build` -> `.webspresso/routes.manifest.json`).
65
+ - Ahead-of-time (AOT) Nunjucks template precompilation (`webspresso build` -> `.webspresso/templates.compiled.js`).
66
+ - On-demand lazy plugin hydration (`lazyPlugin`, `adminPanelPlugin({ lazy: true })`, `fileManagerPlugin({ lazy: true })`).
60
67
  - **Middleware & Hooks**: Route lifecycle hooks can be declared per-page or globally via `pages/_hooks.js`.
61
68
  - **API Endpoints & Routing Architecture**:
62
69
  - āš ļø **CRITICAL ANTI-PATTERN: NEVER create a `routes/` or `src/routes/` directory. NEVER use `express.Router()`, `app.get()`, or `app.post()` for application endpoints.**
package/README.md CHANGED
@@ -31,11 +31,12 @@ Open **`http://localhost:3000`** in your browser.
31
31
  | Layer | Capabilities |
32
32
  | :--- | :--- |
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
+ | **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. |
34
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`. |
35
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
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/`. |
37
- | **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. |
38
- | **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, and more. |
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
40
  | **DevOps & Production** | Built-in streaming zlib compression (`brotli`/`gzip`), socket connection draining & graceful shutdown, `webspresso deploy:check` production readiness auditor, and Docker scaffolding. |
40
41
 
41
42
  ---
@@ -154,13 +155,25 @@ app.listen(PORT, () => {
154
155
 
155
156
  ---
156
157
 
158
+ ## ⚔ High-Performance Architecture (Cold Start & AOT)
159
+
160
+ Webspresso is designed for blazing-fast cold starts and minimal resource consumption in both development and production:
161
+
162
+ 1. **Unified Single-Pass Route Discovery (`discoverAppRoutes`)**: Traverses the filesystem only once at startup, building the `RouteTable` and companion loaders in a single pass (saving 30-40% mount time).
163
+ 2. **Dev-Mode On-Demand (JIT) Route Evaluation**: In development, API routes and SSR companion `.js` configs are resolved on-demand when first requested. The dev server boots in **<100ms** regardless of project size.
164
+ 3. **Production AOT Route Manifest (`webspresso build`)**: The build step generates `.webspresso/routes.manifest.json`. In production, the server mounts pre-indexed routes with **zero filesystem directory scans**.
165
+ 4. **Nunjucks Template Precompilation (AOT / Zero-AST Parse)**: `webspresso build` precompiles `.njk` templates into JavaScript render functions (`.webspresso/templates.compiled.js`). Eliminates runtime disk I/O and template parsing overhead for ultra-low TTFB.
166
+ 5. **On-Demand Lazy Plugins (`lazyPlugin`)**: Heavy plugins (e.g. `adminPanelPlugin({ lazy: true })`, `fileManagerPlugin({ lazy: true })`) defer initialization, session setup, and route mounting until first access to their URL prefix or service invocation.
167
+
168
+ ---
169
+
157
170
  ## 🧰 Essential CLI Commands
158
171
 
159
172
  ```bash
160
173
  # Development & Builds
161
- npx webspresso dev # Start dev server with watch & live reload
162
- npx webspresso build # Compile production assets
163
- npx webspresso start # Run production server
174
+ npx webspresso dev # Start dev server with watch & JIT live reload
175
+ npx webspresso build # Compile production assets, AOT route manifest & precompiled templates
176
+ npx webspresso start # Run production server with zero-FS discovery
164
177
 
165
178
  # Database & Migrations
166
179
  npx webspresso db:migrate # Run pending database migrations
@@ -171,6 +171,16 @@ module.exports = {
171
171
  // Create placeholder CSS
172
172
  fs.writeFileSync('public/css/style.css', '/* Run npm run build:css */\n');
173
173
  console.log('āœ… Created public/css/style.css');
174
+
175
+ // Ensure public/css/style.css is in .gitignore
176
+ if (fs.existsSync('.gitignore')) {
177
+ let gitignore = fs.readFileSync('.gitignore', 'utf-8');
178
+ if (!gitignore.includes('public/css/style.css')) {
179
+ gitignore = gitignore.trimEnd() + '\npublic/css/style.css\n';
180
+ fs.writeFileSync('.gitignore', gitignore);
181
+ console.log('āœ… Added public/css/style.css to .gitignore');
182
+ }
183
+ }
174
184
 
175
185
  // Try to build CSS if tailwindcss / @tailwindcss/cli is already installed
176
186
  const tailwindBin = path.join(process.cwd(), 'node_modules', '.bin', 'tailwindcss');
@@ -15,6 +15,8 @@ function registerCommand(program) {
15
15
  .description('Build production assets (Tailwind CSS and esbuild JS with manifest.json)')
16
16
  .option('--no-css', 'Skip CSS build')
17
17
  .option('--no-js', 'Skip JS bundle build')
18
+ .option('--no-routes', 'Skip generating AOT route manifest')
19
+ .option('--no-templates', 'Skip precompiling Nunjucks templates')
18
20
  .option('--no-hash', 'Disable content hashing in asset filenames')
19
21
  .action(async (options = {}) => {
20
22
  const cwd = process.cwd();
@@ -33,6 +35,8 @@ function registerCommand(program) {
33
35
 
34
36
  let cssBuilt = false;
35
37
  let jsBuilt = false;
38
+ let routesBuilt = false;
39
+ let routeCount = 0;
36
40
 
37
41
  // 1. Build CSS (Tailwind)
38
42
  if (options.css !== false) {
@@ -90,9 +94,39 @@ function registerCommand(program) {
90
94
  }
91
95
  }
92
96
 
97
+ // 3. Build AOT Route Manifest (.webspresso/routes.manifest.json)
98
+ if (options.routes !== false) {
99
+ try {
100
+ const { generateRouteManifest } = require('../../src/discovery/route-manifest');
101
+ const result = generateRouteManifest({ rootDir: cwd });
102
+ routesBuilt = true;
103
+ routeCount = result.routeCount;
104
+ console.log(`šŸ—ŗļø AOT route manifest generated (${result.routeCount} routes) at: ${path.relative(cwd, result.manifestPath)}`);
105
+ } catch (err) {
106
+ console.error('āŒ Route manifest generation failed:', err.message);
107
+ }
108
+ }
109
+
110
+ // 4. Precompile Nunjucks Templates (.webspresso/templates.compiled.js)
111
+ let templatesBuilt = false;
112
+ let templateCount = 0;
113
+ if (options.templates !== false) {
114
+ try {
115
+ const { precompileTemplates } = require('../../src/template/precompile');
116
+ const result = precompileTemplates({ rootDir: cwd });
117
+ templatesBuilt = true;
118
+ templateCount = result.templateCount;
119
+ console.log(`šŸ“‘ Precompiled Nunjucks templates (${result.templateCount} templates) at: ${path.relative(cwd, result.manifestPath)}`);
120
+ } catch (err) {
121
+ console.error('āŒ Template precompilation failed:', err.message);
122
+ }
123
+ }
124
+
93
125
  console.log('\n✨ Asset build complete!\n');
94
126
  if (cssBuilt) console.log(' āœ“ CSS: public/css/style.css');
95
127
  if (jsBuilt) console.log(' āœ“ JS: public/dist/ & manifest.json');
128
+ if (routesBuilt) console.log(` āœ“ Routes: .webspresso/routes.manifest.json (${routeCount} routes)`);
129
+ if (templatesBuilt) console.log(` āœ“ Templates: .webspresso/templates.compiled.js (${templateCount} templates)`);
96
130
  });
97
131
  }
98
132
 
@@ -0,0 +1,83 @@
1
+ /**
2
+ * DB MakeMigrations Command (Django-inspired)
3
+ * Auto-detect model schema changes against live database and generate migrations
4
+ */
5
+
6
+ const path = require('path');
7
+ const { loadDbConfig, createDbInstance } = require('../utils/db');
8
+ const { makeMigrations } = require('../../core/orm/migrations/diff');
9
+
10
+ function registerCommand(program) {
11
+ program
12
+ .command('db:makemigrations')
13
+ .alias('makemigrations')
14
+ .description('Auto-detect model changes against live database and generate migration files')
15
+ .option('-c, --config <path>', 'Path to database config file (webspresso.db.js or knexfile.js)')
16
+ .option('-m, --models <path>', 'Models directory (overrides config.models)')
17
+ .option('--migrations <path>', 'Migrations directory (overrides config.migrations.directory)')
18
+ .option('-n, --name <name>', 'Custom migration name override')
19
+ .option('--include-drops', 'Generate dropColumn operations for columns removed from models')
20
+ .option('--dry-run', 'Preview migrations that would be created without writing files')
21
+ .action(async (options) => {
22
+ let knex = null;
23
+ try {
24
+ const { config, path: configPath } = loadDbConfig(options.config);
25
+ console.log(`\nšŸ“¦ Using config: ${configPath}\n`);
26
+
27
+ knex = await createDbInstance(config);
28
+
29
+ const modelsDir = options.models || config.models || './models';
30
+ const migrationDir = path.resolve(
31
+ process.cwd(),
32
+ options.migrations || config.migrations?.directory || './migrations'
33
+ );
34
+
35
+ console.log(`šŸ” Inspecting database and comparing with models in ${modelsDir}...`);
36
+
37
+ const result = await makeMigrations({
38
+ knex,
39
+ modelsDir,
40
+ migrationDir,
41
+ dryRun: Boolean(options.dryRun),
42
+ includeDrops: Boolean(options.includeDrops),
43
+ name: options.name || null,
44
+ });
45
+
46
+ if (!result.hasChanges) {
47
+ console.log('\n✨ No changes detected in models. Database schema is up to date.\n');
48
+ return;
49
+ }
50
+
51
+ const verb = options.dryRun ? 'Would generate' : 'Generated';
52
+ console.log(`\nāœ… ${verb} ${result.created.length} migration(s):\n`);
53
+
54
+ for (const change of result.created) {
55
+ console.log(` šŸ“„ ${change.file}`);
56
+ console.log(` Model: ${change.model} (table: ${change.table})`);
57
+ console.log(` Type: ${change.type}`);
58
+ if (change.addedColumns?.length > 0) {
59
+ console.log(` + Added column(s): ${change.addedColumns.join(', ')}`);
60
+ }
61
+ if (change.removedColumns?.length > 0) {
62
+ console.log(` - Dropped column(s): ${change.removedColumns.join(', ')}`);
63
+ }
64
+ console.log();
65
+ }
66
+
67
+ if (options.dryRun) {
68
+ console.log('šŸ’” Dry-run mode: No files were written to disk.\n');
69
+ } else {
70
+ console.log('šŸ“ Next step: Run "webspresso db:migrate" to apply the changes.\n');
71
+ }
72
+ } catch (err) {
73
+ console.error('\nāŒ Migration generation failed:', err.message);
74
+ process.exit(1);
75
+ } finally {
76
+ if (knex) {
77
+ await knex.destroy().catch(() => {});
78
+ }
79
+ }
80
+ });
81
+ }
82
+
83
+ module.exports = { registerCommand };
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso CLI - list Command
5
+ * Lists all built-in and custom project commands with descriptions
6
+ * @module bin/commands/list
7
+ */
8
+
9
+ const path = require('path');
10
+ const { createCommandRegistry } = require('../../core/commands');
11
+
12
+ function registerCommand(program) {
13
+ program
14
+ .command('list')
15
+ .alias('commands')
16
+ .description('List all available Webspresso and custom project management commands')
17
+ .option('-d, --dir <path>', 'Custom commands directory (default: ./commands)')
18
+ .action((options) => {
19
+ const rootDir = process.cwd();
20
+ const commandsDir = options.dir
21
+ ? path.resolve(rootDir, options.dir)
22
+ : path.resolve(rootDir, 'commands');
23
+
24
+ const customRegistry = createCommandRegistry({ commandsDir });
25
+ const customCommands = customRegistry.list();
26
+
27
+ console.log('');
28
+ console.log('ā˜• Webspresso CLI - Command Palette');
29
+ console.log('===================================');
30
+
31
+ const sections = [
32
+ {
33
+ title: 'šŸš€ Scaffolding & Code Generation',
34
+ commands: [
35
+ ['make:model <name>', 'Scaffold a new ORM model definition in models/'],
36
+ ['make:job <name>', 'Scaffold a background queue job worker in jobs/'],
37
+ ['make:policy <name>', 'Scaffold an authorization policy in policies/'],
38
+ ['make:factory <name>', 'Scaffold a test/seeder model factory in factories/'],
39
+ ['make:event <name>', 'Scaffold an event listener in listeners/'],
40
+ ['make:mail <name>', 'Scaffold a declarative Mailable class in emails/'],
41
+ ['make:command <name>', 'Scaffold a custom management command in commands/'],
42
+ ['page <path>', 'Scaffold a new file-based SSR page'],
43
+ ['api <path>', 'Scaffold a new file-based API route'],
44
+ ],
45
+ },
46
+ {
47
+ title: 'šŸ’¾ Database & Migrations',
48
+ commands: [
49
+ ['db:migrate', 'Run all pending Knex migrations'],
50
+ ['db:makemigrations', 'Auto-diff model schemas against database and generate migration'],
51
+ ['db:rollback', 'Rollback the last batch of migrations'],
52
+ ['db:status', 'Show database connection and migration status'],
53
+ ['db:scaffold', 'Generate initial create table migrations from models/'],
54
+ ['db:make <name>', 'Create an empty Knex migration file'],
55
+ ['seed', 'Run database seeders'],
56
+ ],
57
+ },
58
+ {
59
+ title: 'šŸ› ļø Server & Developer Experience',
60
+ commands: [
61
+ ['dev', 'Start local development server with hot-reload'],
62
+ ['start', 'Start production HTTP server'],
63
+ ['build', 'Build client assets and precompile templates'],
64
+ ['routes', 'Inspect all discovered SSR page and API routes'],
65
+ ['doctor', 'Diagnose project configuration, dependencies, and health'],
66
+ ['service [name]', 'Run and test services interactively without HTTP server'],
67
+ ['run <command>', 'Execute a custom management command with full DI context'],
68
+ ['list', 'Display this command index'],
69
+ ],
70
+ },
71
+ ];
72
+
73
+ for (const section of sections) {
74
+ console.log(`\n${section.title}:`);
75
+ for (const [cmd, desc] of section.commands) {
76
+ console.log(` ${cmd.padEnd(28)} ${desc}`);
77
+ }
78
+ }
79
+
80
+ if (customCommands.length > 0) {
81
+ console.log(`\n⚔ Custom Project Commands (${path.relative(rootDir, commandsDir) || 'commands/'}):`);
82
+ for (const cmd of customCommands) {
83
+ console.log(` ${('run ' + cmd.name).padEnd(28)} ${cmd.description || 'Custom management command'}`);
84
+ }
85
+ } else {
86
+ console.log('\n⚔ Custom Project Commands:');
87
+ console.log(' (None discovered. Create with: npx webspresso make:command <name>)');
88
+ }
89
+
90
+ console.log('\nUse "webspresso <command> --help" for more information about any command.\n');
91
+ });
92
+ }
93
+
94
+ module.exports = {
95
+ registerCommand,
96
+ };