webspresso 0.0.96 → 0.0.98

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 (121) hide show
  1. package/.agents/ADMIN.md +153 -5
  2. package/.agents/ARCHITECTURE.md +10 -0
  3. package/.agents/CLI.md +195 -1
  4. package/.agents/PLUGINS.md +153 -9
  5. package/.agents/QUEUE.md +43 -0
  6. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
  7. package/.agents/skills/webspresso-framework/SKILL.md +56 -0
  8. package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
  9. package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
  10. package/.agents/skills/webspresso-services/SKILL.md +101 -0
  11. package/bin/commands/add-esbuild.js +80 -0
  12. package/bin/commands/add-tailwind.js +76 -31
  13. package/bin/commands/admin-password.js +2 -1
  14. package/bin/commands/admin-setup.js +1 -1
  15. package/bin/commands/agents-init.js +51 -11
  16. package/bin/commands/analytics-prune.js +57 -0
  17. package/bin/commands/api.js +207 -15
  18. package/bin/commands/audit-prune.js +1 -1
  19. package/bin/commands/build.js +99 -0
  20. package/bin/commands/dev.js +64 -35
  21. package/bin/commands/doctor.js +26 -0
  22. package/bin/commands/email-prune.js +47 -0
  23. package/bin/commands/favicon-generate.js +1 -1
  24. package/bin/commands/make-job.js +86 -0
  25. package/bin/commands/make-model.js +250 -0
  26. package/bin/commands/new.js +45 -28
  27. package/bin/commands/openapi.js +102 -0
  28. package/bin/commands/orm-map.js +5 -4
  29. package/bin/commands/page.js +6 -3
  30. package/bin/commands/polar-migrate.js +1 -1
  31. package/bin/commands/queue-prune.js +59 -0
  32. package/bin/commands/routes.js +52 -0
  33. package/bin/commands/seed.js +1 -1
  34. package/bin/commands/service.js +6 -3
  35. package/bin/commands/typegen.js +106 -0
  36. package/bin/commands/upload-prune.js +79 -0
  37. package/bin/webspresso.js +22 -0
  38. package/core/applySchema.js +13 -6
  39. package/core/auth/hash.js +31 -17
  40. package/core/compression/middleware.js +23 -17
  41. package/core/openapi/build-from-api-routes.js +160 -29
  42. package/core/orm/cache/fingerprint.js +15 -1
  43. package/core/orm/cache/memory-provider.js +27 -2
  44. package/core/orm/index.js +12 -7
  45. package/core/orm/model.js +49 -7
  46. package/core/queue/adapters/database.js +94 -0
  47. package/core/queue/adapters/memory.js +62 -0
  48. package/core/queue/adapters/redis.js +80 -0
  49. package/core/queue/manager.js +123 -0
  50. package/core/ssr/stream.js +11 -0
  51. package/index.d.ts +660 -10
  52. package/index.js +44 -96
  53. package/package.json +32 -12
  54. package/plugins/admin-panel/api.js +279 -17
  55. package/plugins/admin-panel/app.js +124 -31
  56. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  57. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  58. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
  59. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  60. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  61. package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
  62. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  63. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  64. package/plugins/admin-panel/core/admin-module.js +13 -7
  65. package/plugins/admin-panel/core/api-extensions.js +34 -2
  66. package/plugins/admin-panel/core/registry.js +96 -16
  67. package/plugins/admin-panel/index.js +96 -0
  68. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  69. package/plugins/admin-panel/modules/admin-users.js +16 -1
  70. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  71. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  72. package/plugins/admin-panel/modules/dashboard.js +283 -0
  73. package/plugins/admin-panel/modules/menu.js +56 -18
  74. package/plugins/admin-panel/modules/user-management.js +24 -6
  75. package/plugins/audit-log/admin-component.js +11 -0
  76. package/plugins/audit-log/api-handlers.js +34 -0
  77. package/plugins/audit-log/index.js +129 -3
  78. package/plugins/audit-log/purge.js +60 -10
  79. package/plugins/basic-auth/index.js +3 -1
  80. package/plugins/content/client/inline-edit.js +12 -12
  81. package/plugins/cors.js +6 -7
  82. package/plugins/dashboard/app.js +152 -27
  83. package/plugins/dashboard/index.js +143 -39
  84. package/plugins/dashboard/styles.js +78 -0
  85. package/plugins/data-exchange/export-xlsx.js +9 -2
  86. package/plugins/data-exchange/import.js +9 -2
  87. package/plugins/data-exchange/index.js +6 -3
  88. package/plugins/email/index.js +132 -2
  89. package/plugins/email/purge.js +60 -10
  90. package/plugins/file-manager/core.js +1 -1
  91. package/plugins/file-manager/security.js +33 -23
  92. package/plugins/index.js +49 -87
  93. package/plugins/mcp/transports/sse.js +4 -2
  94. package/plugins/orm-cache-admin/index.js +4 -2
  95. package/plugins/queue/index.js +11 -0
  96. package/plugins/schema-explorer/admin-component.js +714 -0
  97. package/plugins/schema-explorer/relations-graph.js +205 -0
  98. package/plugins/schema-explorer.js +248 -69
  99. package/plugins/site-analytics/admin-component.js +30 -8
  100. package/plugins/site-analytics/api-handlers.js +42 -0
  101. package/plugins/site-analytics/index.js +147 -2
  102. package/plugins/site-analytics/purge.js +168 -0
  103. package/plugins/swagger.js +261 -13
  104. package/plugins/upload/index.js +26 -0
  105. package/plugins/upload/orphan-pruner.js +292 -0
  106. package/src/api/api-loader.js +3 -1
  107. package/src/bundle/index.js +517 -0
  108. package/src/bundle/manifest.js +165 -0
  109. package/src/dev/banner.js +296 -0
  110. package/src/dev/colors.js +70 -0
  111. package/src/dev/index.js +62 -0
  112. package/src/dev/profiler.js +354 -0
  113. package/src/dev/typegen.js +497 -0
  114. package/src/dev-reload/index.js +611 -0
  115. package/src/file-router.js +213 -63
  116. package/src/helpers.js +958 -98
  117. package/src/njk-frontmatter.js +12 -0
  118. package/src/pages/page-loader.js +61 -11
  119. package/src/server.js +629 -63
  120. package/src/services/discovery.js +6 -1
  121. package/src/services/index.js +6 -13
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: webspresso-admin-and-plugins
3
+ description: >-
4
+ Extend the Mithril.js Admin Panel SPA and configure official plugins in Webspresso.
5
+ Use when creating custom admin pages (component.js, page.json), custom field renderers,
6
+ dashboard widgets, actions, or configuring plugins like fileManager, xlsx, csv, polar, realtime, csrf, cors, and basicAuth.
7
+ ---
8
+
9
+ # Webspresso Admin Panel & Plugins Skill
10
+
11
+ The Admin Panel is a modular Mithril.js SPA mounted at `/_admin` powered by `adminPanelPlugin({ db })`.
12
+
13
+ ---
14
+
15
+ ## 1. Custom Admin Pages (`pagesDir` or `registerPageDir`)
16
+
17
+ Folder structure for auto-discovered admin pages:
18
+ ```text
19
+ admin/pages/
20
+ └── system-stats/
21
+ ├── page.json
22
+ └── component.js
23
+ ```
24
+
25
+ ### `page.json`
26
+ ```json
27
+ {
28
+ "id": "system-stats",
29
+ "label": "System Statistics",
30
+ "icon": "activity",
31
+ "order": 10,
32
+ "layout": true,
33
+ "permission": "admin"
34
+ }
35
+ ```
36
+
37
+ ### `component.js` (Clean Mithril Component)
38
+ ```javascript
39
+ module.exports = {
40
+ oninit(vnode) {
41
+ vnode.state.stats = null;
42
+ vnode.state.loading = true;
43
+ m.request({ method: 'GET', url: '/_admin/api/custom/stats' }).then((res) => {
44
+ vnode.state.stats = res;
45
+ vnode.state.loading = false;
46
+ });
47
+ },
48
+ view(vnode) {
49
+ if (vnode.state.loading) return m('.p-6', 'Loading stats...');
50
+ return m('.space-y-4', [
51
+ m('h2.text-xl.font-bold', 'System Overview'),
52
+ m('.grid.grid-cols-3.gap-4', [
53
+ m('.card.p-4.border.rounded', [
54
+ m('.text-sm.text-gray-500', 'Total Users'),
55
+ m('.text-2xl.font-semibold', vnode.state.stats.usersCount),
56
+ ]),
57
+ ]),
58
+ ]);
59
+ },
60
+ };
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 2. Admin Panel Extension Points
66
+
67
+ ```javascript
68
+ // Register custom widget in dashboard
69
+ registry.registerWidget('recent-signups', {
70
+ label: 'Recent Signups',
71
+ component: require('./widgets/recent-signups'),
72
+ width: 'half', // 'full' | 'half' | 'third'
73
+ });
74
+
75
+ // Register single record action button
76
+ registry.registerAction('User', {
77
+ name: 'impersonate',
78
+ label: 'Impersonate',
79
+ icon: 'user-check',
80
+ handler: async (record) => { ... },
81
+ });
82
+
83
+ // Register custom field display/edit renderer
84
+ registry.registerFieldRenderer('color-picker', {
85
+ display: (val) => m('.w-6.h-6.rounded', { style: { backgroundColor: val } }),
86
+ edit: (val, onChange) => m('input[type=color]', { value: val, onchange: (e) => onChange(e.target.value) }),
87
+ });
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 3. Official Plugin Catalog & Usage
93
+
94
+ In `createApp({ plugins: [...] })`:
95
+
96
+ ```javascript
97
+ const {
98
+ adminPanelPlugin,
99
+ fileManagerPlugin,
100
+ xlsxPlugin,
101
+ csvPlugin,
102
+ basicAuthPlugin,
103
+ corsPlugin,
104
+ csrfPlugin,
105
+ realtimePlugin,
106
+ polarPlugin,
107
+ } = require('webspresso/plugins');
108
+
109
+ createApp({
110
+ plugins: [
111
+ adminPanelPlugin({ db }),
112
+ fileManagerPlugin({ baseDir: './public/uploads', publicBasePath: '/uploads' }),
113
+ xlsxPlugin(),
114
+ csvPlugin(),
115
+ basicAuthPlugin({ users: { admin: process.env.ADMIN_BASIC_AUTH_PASS }, routes: ['/metrics'] }),
116
+ corsPlugin({ origin: ['https://example.com'], credentials: true }),
117
+ csrfPlugin({ cookie: true }),
118
+ realtimePlugin({ adapter: 'websocket' }),
119
+ polarPlugin({ apiKey: process.env.POLAR_ACCESS_TOKEN }),
120
+ ],
121
+ });
122
+ ```
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: webspresso-framework
3
+ description: >-
4
+ Core workflow and guide for Webspresso, a lightweight, zero-dependency-sprawl full-stack
5
+ Node.js SSR and API framework. Use when scaffolding, architecting, building, or debugging
6
+ Webspresso applications, configuring plugins, dual auth, response compression, graceful shutdown,
7
+ and error boundaries.
8
+ ---
9
+
10
+ # Webspresso Framework Master Guide
11
+
12
+ Webspresso is a lightweight, zero-dependency-sprawl full-stack Node.js SSR & API framework featuring Next.js/Nuxt-style file-based routing, an intuitive Knex-based ORM, a modular plugin ecosystem, and a customizable Mithril.js SPA Admin Panel.
13
+
14
+ ## 🛡️ 4-Step Mandatory Agent Workflow Protocol
15
+
16
+ 1. **Discover & Triage**: Identify relevant topic guides (`.agents/`) and inspect existing files in `pages/api/`, `models/`, `services/`.
17
+ 2. **Contract Adherence**: Export `{ schema, middleware, handler }` for APIs, `defineModel` for ORM, `defineService` for services.
18
+ 3. **Zero External Dependencies**: Core functionality relies strictly on native Node.js modules (`crypto`, `path`, `fs`, `zlib`, `events`). NEVER install ad-hoc packages for JWT, passport, express-session, or custom routers.
19
+ 4. **Targeted Verification**: Run specific Vitest unit tests (`npx vitest run tests/...`) to ensure zero regressions.
20
+
21
+ ---
22
+
23
+ ## ⚖️ Golden Rules & Anti-Patterns
24
+
25
+ | Category | ❌ NEVER DO THIS (Anti-Pattern) | ✅ ALWAYS DO THIS (Webspresso Way) |
26
+ | :--- | :--- | :--- |
27
+ | **Routing** | `routes/index.js`, `express.Router()`, `app.get('/api/users')` | `pages/api/users.get.js` exporting `{ schema, handler }` |
28
+ | **Database** | `db('users').where({ id }).select()` (Raw Knex) | `db.getRepository('User').findById(id)` |
29
+ | **Business Logic** | `controllers/UserController.js` | `services/user/get.js` → `ctx.service('user.get', input)` |
30
+ | **Auth** | `npm i jsonwebtoken passport express-session` | Built-in `req.auth`, `core/auth`, `middleware: ['jwt']` |
31
+ | **SSR Rendering**| `res.render('profile.njk', data)` inside handlers | Companion loader `pages/profile.js` exporting `async function load()` |
32
+ | **Exceptions** | `res.status(404).json({ error: 'Not found' })` | `throw new NotFoundError('User not found')` |
33
+ | **Background** | `setTimeout(...)`, `npm i bullmq` | `jobs/user/welcome.js` → `req.queue.dispatch('user.welcome', data)` |
34
+
35
+ ---
36
+
37
+ ## 🚀 CLI Commands
38
+
39
+ ```bash
40
+ webspresso dev # Start dev server with hot reload
41
+ webspresso start # Start production server
42
+ webspresso doctor # Run system sanity checks
43
+ webspresso db:migrate # Run pending Knex migrations
44
+ webspresso db:seed # Run database seeders
45
+ webspresso agents:init # Scaffold AI Agent rules and skills
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 🏗️ Core Architecture Overview
51
+
52
+ - **`pages/` & `pages/api/`**: File-based SSR templates (`.njk`), companion data loaders (`.js`), and method-suffixed API endpoints (`.get.js`, `.post.js`).
53
+ - **`services/`**: Auto-discovered domain services mapping to dot-separated names (`'user.create'`).
54
+ - **`models/`**: Schema-driven ORM models with `zdb` definitions, repositories, relations, scopes, and query cache.
55
+ - **`plugins/`**: Modular extension ecosystem (Admin Panel, Media Library, Polar Billing, CSV/XLSX, Realtime, CSRF, CORS, Basic Auth).
56
+ - **`views/`**: Nunjucks layouts, partials, and `fsy` template helpers.
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: webspresso-orm-and-database
3
+ description: >-
4
+ Define models, run queries, handle transactions, manage relations, and scaffold migrations in
5
+ Webspresso ORM. Use when writing database models (defineModel), using zdb schema builders,
6
+ querying repositories (db.getRepository), configuring query cache, soft deletes, and ambient transactions.
7
+ ---
8
+
9
+ # Webspresso ORM & Database Skill
10
+
11
+ ⚠️ **CRITICAL ANTI-PATTERN:** Never write raw Knex table queries (e.g. `db('users')` or `knex('users')`) in application code. Always use Repositories (`db.getRepository('ModelName')`).
12
+
13
+ ---
14
+
15
+ ## 1. Defining Models (`models/*.js`)
16
+
17
+ ```javascript
18
+ const { defineModel, zdb } = require('webspresso');
19
+
20
+ module.exports = defineModel({
21
+ name: 'Product',
22
+ table: 'products',
23
+ primaryKey: 'id',
24
+
25
+ schema: zdb.object({
26
+ id: zdb.id(),
27
+ name: zdb.string().min(1).max(255),
28
+ slug: zdb.string().unique(),
29
+ price: zdb.decimal({ precision: 10, scale: 2 }),
30
+ in_stock: zdb.boolean().default(true),
31
+ metadata: zdb.json().optional(),
32
+ user_id: zdb.foreignKey('users.id'),
33
+ created_at: zdb.timestamp().autoCreate(),
34
+ updated_at: zdb.timestamp().autoUpdate(),
35
+ deleted_at: zdb.timestamp().nullable(),
36
+ }),
37
+
38
+ relations: {
39
+ user: {
40
+ type: 'belongsTo',
41
+ model: 'User',
42
+ foreignKey: 'user_id',
43
+ },
44
+ reviews: {
45
+ type: 'hasMany',
46
+ model: 'Review',
47
+ foreignKey: 'product_id',
48
+ },
49
+ },
50
+
51
+ scopes: {
52
+ softDelete: true, // enables deleted_at filtering automatically
53
+ active: (query) => query.where('in_stock', true),
54
+ },
55
+
56
+ hidden: ['metadata.internal_notes'], // stripped from output
57
+
58
+ cache: 'auto', // query caching ('auto' | 'smart' | true)
59
+
60
+ admin: {
61
+ enabled: true,
62
+ label: 'Products',
63
+ icon: 'package',
64
+ columns: ['name', 'price', 'in_stock', 'created_at'],
65
+ search: ['name', 'slug'],
66
+ filters: ['in_stock', 'user_id'],
67
+ },
68
+ });
69
+ ```
70
+
71
+ ---
72
+
73
+ ## 2. Repositories API
74
+
75
+ ```javascript
76
+ const repo = db.getRepository('Product');
77
+
78
+ // Single Record Queries
79
+ const item = await repo.findById(id);
80
+ const item = await repo.findOne({ slug: 'espresso-blend' });
81
+
82
+ // Multiple Records & Filtering
83
+ const activeItems = await repo.find({ in_stock: true });
84
+
85
+ // Insert & Update (auto-validates with schema & triggers lifecycle hooks)
86
+ const created = await repo.create({
87
+ name: 'Espresso Blend',
88
+ slug: 'espresso-blend',
89
+ price: 14.99,
90
+ user_id: user.id,
91
+ });
92
+
93
+ const updated = await repo.update(created.id, { price: 16.99 });
94
+
95
+ // Deletion (respects softDelete scope)
96
+ await repo.delete(id);
97
+
98
+ // Fluent Query Builder with Eager Loading & Pagination
99
+ const results = await repo.query()
100
+ .where('price', '>=', 10)
101
+ .include('user', 'reviews')
102
+ .orderBy('created_at', 'desc')
103
+ .paginate({ page: 1, limit: 20 });
104
+ // Returns: { data: [...], pagination: { total, page, limit, totalPages } }
105
+ ```
106
+
107
+ ---
108
+
109
+ ## 3. Ambient Transactions (`AsyncLocalStorage`)
110
+
111
+ Inside `db.transaction()` or services declared with `transaction: true`, all repository operations automatically bind to the ambient transaction without manual `trx` passing:
112
+
113
+ ```javascript
114
+ await db.transaction(async () => {
115
+ const user = await db.getRepository('User').create(userData);
116
+ const account = await db.getRepository('Account').create({ userId: user.id, balance: 100 });
117
+ // Automatically commits if promise resolves, or rolls back if an exception is thrown
118
+ });
119
+ ```
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: webspresso-routing-and-api
3
+ description: >-
4
+ Build and declare file-based API endpoints and SSR pages in Webspresso using defineApi, definePage,
5
+ and defineModule. Use when writing route handlers, data loaders (load()), parameter brackets ([id]),
6
+ Zod input validation, response streaming, and route middleware.
7
+ ---
8
+
9
+ # Webspresso Routing & API Skill
10
+
11
+ Webspresso uses strictly file-based routing. Never use `express.Router()`, `app.get()`, or a `routes/` directory.
12
+
13
+ ---
14
+
15
+ ## 1. File-Based API Endpoints (`pages/api/...`)
16
+
17
+ All API routes live in `pages/api/` and use HTTP method extensions:
18
+ - `pages/api/notes.get.js` → `GET /api/notes`
19
+ - `pages/api/notes.post.js` → `POST /api/notes`
20
+ - `pages/api/notes/[id].get.js` → `GET /api/notes/:id`
21
+ - `pages/api/notes/[id].delete.js` → `DELETE /api/notes/:id`
22
+
23
+ ### Standard `defineApi` Contract
24
+
25
+ ```javascript
26
+ const { defineApi } = require('webspresso');
27
+
28
+ module.exports = defineApi({
29
+ schema: ({ z }) => ({
30
+ body: z.object({
31
+ title: z.string().min(1, 'Title is required'),
32
+ content: z.string().optional(),
33
+ }),
34
+ params: z.object({
35
+ id: z.string().optional(),
36
+ }),
37
+ query: z.object({
38
+ filter: z.string().optional(),
39
+ }),
40
+ }),
41
+ middleware: ['auth'], // optional named middleware string or array of middlewares
42
+ handler: async (req, res, ctx) => {
43
+ // 1. Validated input is automatically available in req.input
44
+ const { title, content } = req.input.body;
45
+
46
+ // 2. Repositories and Services are injected
47
+ const note = await req.db.getRepository('Note').create({ title, content });
48
+
49
+ // 3. Return response
50
+ return res.status(201).json(note);
51
+ },
52
+ });
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 2. SSR HTML Pages & Data Loaders (`pages/...`)
58
+
59
+ - **Template**: `pages/products/[id].njk`
60
+ - **Companion Loader**: `pages/products/[id].js`
61
+
62
+ ### Companion Loader (`definePage`)
63
+
64
+ ```javascript
65
+ const { definePage } = require('webspresso');
66
+
67
+ module.exports = definePage({
68
+ middleware: ['auth'], // optional
69
+ head: {
70
+ title: 'Product Details',
71
+ },
72
+ load: async ({ req, res, db, ctx }) => {
73
+ const product = await db.getRepository('Product').findById(req.params.id);
74
+ if (!product) {
75
+ throw new ctx.errors.NotFoundError('Product not found');
76
+ }
77
+ return { product, title: product.name };
78
+ },
79
+ });
80
+ ```
81
+
82
+ ### Template (`.njk`)
83
+
84
+ ```nunjucks
85
+ {% extends "layout.njk" %}
86
+
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>
92
+ </div>
93
+ {% endblock %}
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 3. Modular Domain Architecture (`defineModule`)
99
+
100
+ Encapsulate feature domains inside `src/modules/` or `modules/`:
101
+
102
+ ```javascript
103
+ const { defineModule } = require('webspresso');
104
+
105
+ module.exports = defineModule({
106
+ name: 'billing',
107
+ pages: './pages', // auto-discovers pages under /billing/*
108
+ api: './api', // auto-discovers API under /api/billing/*
109
+ services: './services', // auto-discovers services under 'billing.*'
110
+ middlewares: {
111
+ paidOnly: (req, res, next) => { ... },
112
+ },
113
+ });
114
+ ```
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: webspresso-services
3
+ description: >-
4
+ Encapsulate business logic, validations, transactions, and authorizations in Webspresso services.
5
+ Use when writing service files (services/domain/action.js), defining schemas (defineService),
6
+ configuring transaction: true, calling ctx.service() or req.service(), and adding RBAC guards.
7
+ ---
8
+
9
+ # Webspresso Services Layer Skill
10
+
11
+ ⚠️ **CRITICAL ANTI-PATTERN:** Never create a `controllers/` folder or controller classes. Encapsulate multi-step business logic in `services/` and invoke via `ctx.service()` or `req.service()`.
12
+
13
+ ---
14
+
15
+ ## 1. File-Based Auto-Discovery (`services/`)
16
+
17
+ Files in `services/` map directly to dot-separated service names:
18
+ - `services/user/create.js` → `'user.create'`
19
+ - `services/order/checkout.js` → `'order.checkout'`
20
+ - `services/billing/process-refund.js` → `'billing.process-refund'` (and `'billing.processRefund'`)
21
+
22
+ ---
23
+
24
+ ## 2. Defining Services (`defineService`)
25
+
26
+ ```javascript
27
+ const { defineService } = require('webspresso');
28
+
29
+ module.exports = defineService({
30
+ schema: ({ z }) => z.object({
31
+ userId: z.number().int().positive(),
32
+ amount: z.number().positive(),
33
+ reason: z.string().min(3),
34
+ }),
35
+
36
+ // Optional Declarative Auth / Role Guard
37
+ auth: ['admin', 'manager'], // or boolean true, or (ctx, input) => boolean
38
+
39
+ // Automatic ACID Transaction Propagation
40
+ transaction: true,
41
+
42
+ // Optional Service Result Cache
43
+ cache: {
44
+ ttl: 300, // seconds
45
+ key: (input) => `refund:${input.userId}:${input.amount}`,
46
+ },
47
+
48
+ // Execution Handler
49
+ handler: async (input, ctx) => {
50
+ // ctx.db, ctx.service, ctx.user, ctx.logger are automatically injected
51
+ const userRepo = ctx.db.getRepository('User');
52
+ const accountRepo = ctx.db.getRepository('Account');
53
+
54
+ const user = await userRepo.findById(input.userId);
55
+ if (!user) {
56
+ throw new ctx.errors.NotFoundError('User not found');
57
+ }
58
+
59
+ const account = await accountRepo.findOne({ userId: user.id });
60
+ const updatedAccount = await accountRepo.update(account.id, {
61
+ balance: account.balance + input.amount,
62
+ });
63
+
64
+ // Nested service call shares the same ambient transaction automatically
65
+ await ctx.service('audit.log', {
66
+ action: 'refund.processed',
67
+ userId: user.id,
68
+ amount: input.amount,
69
+ });
70
+
71
+ return {
72
+ success: true,
73
+ balance: updatedAccount.balance,
74
+ };
75
+ },
76
+ });
77
+ ```
78
+
79
+ ---
80
+
81
+ ## 3. Invoking Services
82
+
83
+ From SSR Companion Loaders:
84
+ ```javascript
85
+ module.exports = definePage({
86
+ load: async ({ ctx }) => {
87
+ const data = await ctx.service('order.summary', { userId: ctx.user.id });
88
+ return { data };
89
+ },
90
+ });
91
+ ```
92
+
93
+ From API Route Handlers:
94
+ ```javascript
95
+ module.exports = defineApi({
96
+ handler: async (req, res) => {
97
+ const result = await req.service('order.checkout', req.input.body);
98
+ return res.status(200).json(result);
99
+ },
100
+ });
101
+ ```
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Add esbuild Command
3
+ * Add esbuild client bundling and incremental watch to the project
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ function registerCommand(program) {
10
+ program
11
+ .command('add esbuild')
12
+ .alias('add:esbuild')
13
+ .description('Add esbuild client bundling and incremental watch to the project')
14
+ .action(async () => {
15
+ if (!fs.existsSync('package.json')) {
16
+ console.error('❌ Not a Webspresso project! Run this command in your project directory.');
17
+ process.exit(1);
18
+ }
19
+
20
+ console.log('\n⚡ Adding esbuild client bundler to your project...\n');
21
+
22
+ // Read package.json
23
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
24
+ packageJson.devDependencies = packageJson.devDependencies || {};
25
+ packageJson.devDependencies['esbuild'] = '^0.25.0';
26
+
27
+ packageJson.scripts = packageJson.scripts || {};
28
+ packageJson.scripts['build:js'] = 'esbuild src/client/main.js --bundle --outdir=public/js --minify --sourcemap';
29
+ packageJson.scripts['watch:js'] = 'esbuild src/client/main.js --bundle --outdir=public/js --sourcemap --watch';
30
+
31
+ fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
32
+ console.log('✅ Updated package.json (esbuild devDependency and build:js / watch:js scripts)');
33
+
34
+ // Create src/client directory
35
+ if (!fs.existsSync('src/client')) {
36
+ fs.mkdirSync('src/client', { recursive: true });
37
+ }
38
+
39
+ // Create src/client/main.js if not exists
40
+ const mainJsPath = path.join('src', 'client', 'main.js');
41
+ if (!fs.existsSync(mainJsPath)) {
42
+ const clientStarterCode = `// Client-side JavaScript entry point
43
+ console.log('⚡ Webspresso client script loaded');
44
+
45
+ document.addEventListener('DOMContentLoaded', () => {
46
+ // Client interactivity
47
+ });
48
+ `;
49
+ fs.writeFileSync(mainJsPath, clientStarterCode);
50
+ console.log('✅ Created src/client/main.js');
51
+ }
52
+
53
+ // Create public/js directory
54
+ if (!fs.existsSync(path.join('public', 'js'))) {
55
+ fs.mkdirSync(path.join('public', 'js'), { recursive: true });
56
+ }
57
+
58
+ // Update layout.njk to include script if not present
59
+ const layoutPath = path.join('views', 'layout.njk');
60
+ if (fs.existsSync(layoutPath)) {
61
+ let layoutContent = fs.readFileSync(layoutPath, 'utf-8');
62
+ if (!layoutContent.includes('/js/main.js')) {
63
+ layoutContent = layoutContent.replace(
64
+ /(<\/head>)/,
65
+ ' <script defer src="/js/main.js"></script>\n$1'
66
+ );
67
+ fs.writeFileSync(layoutPath, layoutContent);
68
+ console.log('✅ Updated views/layout.njk (<script defer src="/js/main.js"></script>)');
69
+ }
70
+ }
71
+
72
+ console.log('\n✅ esbuild setup complete!\n');
73
+ console.log('Next steps:');
74
+ console.log(' npm install');
75
+ console.log(' npm run build:js');
76
+ console.log(' npm run dev\n');
77
+ });
78
+ }
79
+
80
+ module.exports = { registerCommand };