webspresso 0.0.97 โ 0.0.99
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.
- package/.agents/ADMIN.md +163 -5
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/AUTH.md +169 -0
- package/.agents/CLI.md +195 -1
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +221 -9
- package/.agents/QUEUE.md +127 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +21 -12
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +20 -7
- package/core/orm/model.js +53 -7
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +205 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +1188 -10
- package/index.js +88 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -59
- package/plugins/audit-log/api-handlers.js +148 -0
- package/plugins/audit-log/index.js +153 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +10 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +15 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +23 -0
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +4 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +347 -162
- package/src/helpers.js +967 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +635 -63
package/.agents/ROUTING.md
CHANGED
|
@@ -53,7 +53,10 @@ All routes are automatically discovered and registered from the `pages/` directo
|
|
|
53
53
|
| `pages/index.njk` | `GET` | `/` | Home page (SSR Nunjucks) |
|
|
54
54
|
| `pages/about.njk` | `GET` | `/about` | Static SSR page |
|
|
55
55
|
| `pages/notes/[id].njk` | `GET` | `/notes/:id` | Dynamic SSR route with param |
|
|
56
|
+
| `pages/(marketing)/about.njk` | `GET` | `/about` | Route Group page (omits `(marketing)`) |
|
|
57
|
+
| `pages/(auth)/login.njk` | `GET` | `/login` | Route Group with isolated layout & middleware |
|
|
56
58
|
| `pages/api/notes.get.js` | `GET` | `/api/notes` | List notes API endpoint |
|
|
59
|
+
| `pages/api/(v1)/notes.get.js` | `GET` | `/api/notes` | API route inside Route Group |
|
|
57
60
|
| `pages/api/notes.post.js` | `POST` | `/api/notes` | Create note API endpoint |
|
|
58
61
|
| `pages/api/notes/[id].get.js` | `GET` | `/api/notes/:id` | Get single note API endpoint |
|
|
59
62
|
| `pages/api/notes/[id].put.js` | `PUT` | `/api/notes/:id` | Update note API endpoint |
|
|
@@ -148,7 +151,50 @@ module.exports = {
|
|
|
148
151
|
|
|
149
152
|
---
|
|
150
153
|
|
|
151
|
-
## 4.
|
|
154
|
+
## 4. Route Groups `(group)` Syntax & Layout Isolation
|
|
155
|
+
|
|
156
|
+
Webspresso supports Next.js / Nuxt-style **Route Groups**. Any directory name wrapped in parentheses `(groupName)` is treated as an organizational group:
|
|
157
|
+
- **Clean Public URLs**: The parenthesized directory name is completely omitted from the generated URL path.
|
|
158
|
+
- **Layout & Middleware Isolation**: The directory hierarchy is preserved for cascading middleware (`_middleware.js`) and nested layouts (`_layout.njk`), allowing distinct shells and security guards without polluting public route URLs.
|
|
159
|
+
- **Nesting & Dynamic Route Support**: Route groups can be nested (e.g. `(shop)/(checkout)/cart.njk` โ `/cart`), combined with dynamic segments (e.g. `(app)/users/[id].njk` โ `/users/:id`), and applied to API routes (`pages/api/(v1)/items.get.js` โ `/api/items` or `pages/(marketing)/api/newsletter.post.js` โ `/api/newsletter`).
|
|
160
|
+
|
|
161
|
+
### 4.1 Route Group Structure Example
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
pages/
|
|
165
|
+
โโโ (marketing)/
|
|
166
|
+
โ โโโ _layout.njk # Marketing layout (Hero header, footer)
|
|
167
|
+
โ โโโ _middleware.js # Track analytics / marketing headers
|
|
168
|
+
โ โโโ index.njk -> GET /
|
|
169
|
+
โ โโโ about.njk -> GET /about
|
|
170
|
+
โ โโโ api/
|
|
171
|
+
โ โโโ newsletter.post.js -> POST /api/newsletter
|
|
172
|
+
โโโ (auth)/
|
|
173
|
+
โ โโโ _layout.njk # Centered card layout (no nav/footer)
|
|
174
|
+
โ โโโ _middleware.js # Guest-only guard (redirects authenticated users)
|
|
175
|
+
โ โโโ login.njk -> GET /login
|
|
176
|
+
โ โโโ register.njk -> GET /register
|
|
177
|
+
โโโ (app)/
|
|
178
|
+
โโโ _layout.njk # App shell layout with sidebar
|
|
179
|
+
โโโ _middleware.js # Strict session auth guard
|
|
180
|
+
โโโ dashboard.njk -> GET /dashboard
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 4.2 Programmatic Utilities
|
|
184
|
+
Webspresso exports core helpers to inspect and transform route group paths:
|
|
185
|
+
```javascript
|
|
186
|
+
const { isRouteGroupSegment, stripRouteGroups } = require('webspresso');
|
|
187
|
+
|
|
188
|
+
isRouteGroupSegment('(marketing)'); // true
|
|
189
|
+
isRouteGroupSegment('about'); // false
|
|
190
|
+
|
|
191
|
+
stripRouteGroups('/(marketing)/about'); // '/about'
|
|
192
|
+
stripRouteGroups('/(shop)/(checkout)/cart'); // '/cart'
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 5. SSR Page Routes (`load()` Loader)
|
|
152
198
|
|
|
153
199
|
For server-rendered Nunjucks HTML pages, use `.njk` templates and companion `.js` loader files (or inline `async function load` in a `.js` file):
|
|
154
200
|
|
|
@@ -174,7 +220,7 @@ module.exports = {
|
|
|
174
220
|
|
|
175
221
|
---
|
|
176
222
|
|
|
177
|
-
##
|
|
223
|
+
## 6. Module & File-Based Discovery (`src/pages`, `src/api`, `modules/{name}`)
|
|
178
224
|
|
|
179
225
|
Webspresso supports clean, domain-driven module discovery alongside root-level pages and APIs:
|
|
180
226
|
|
|
@@ -204,7 +250,7 @@ src/
|
|
|
204
250
|
auth-guard.js -> module-local named middleware 'authGuard'
|
|
205
251
|
```
|
|
206
252
|
|
|
207
|
-
###
|
|
253
|
+
### 6.1 `definePage()` Contract
|
|
208
254
|
```javascript
|
|
209
255
|
const { definePage } = require('webspresso');
|
|
210
256
|
|
|
@@ -230,7 +276,7 @@ module.exports = definePage({
|
|
|
230
276
|
});
|
|
231
277
|
```
|
|
232
278
|
|
|
233
|
-
###
|
|
279
|
+
### 6.2 `defineApi()` Contract
|
|
234
280
|
```javascript
|
|
235
281
|
const { defineApi, z } = require('webspresso');
|
|
236
282
|
|
|
@@ -252,7 +298,7 @@ module.exports = defineApi({
|
|
|
252
298
|
});
|
|
253
299
|
```
|
|
254
300
|
|
|
255
|
-
###
|
|
301
|
+
### 6.3 `defineModule()` Contract
|
|
256
302
|
```javascript
|
|
257
303
|
const { defineModule } = require('webspresso');
|
|
258
304
|
|
|
@@ -270,7 +316,7 @@ module.exports = defineModule({
|
|
|
270
316
|
});
|
|
271
317
|
```
|
|
272
318
|
|
|
273
|
-
###
|
|
319
|
+
### 6.4 Route Introspection (`app.routes.list()`)
|
|
274
320
|
Compiled routes are accessible at runtime for tooling, CLI inspection, and documentation:
|
|
275
321
|
```javascript
|
|
276
322
|
const routeList = app.routes.list();
|
|
@@ -280,9 +326,83 @@ const routeList = app.routes.list();
|
|
|
280
326
|
// ]
|
|
281
327
|
```
|
|
282
328
|
|
|
329
|
+
### 6.5 Server-Sent Events (SSE) Streaming (`res.sse`)
|
|
330
|
+
For real-time event streaming and LLM token outputs, use the native `res.sse()` helper (or `{ sse }` from `ctx`):
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
// File: pages/api/ai/chat.post.js
|
|
334
|
+
const { defineApi } = require('webspresso');
|
|
335
|
+
|
|
336
|
+
module.exports = defineApi({
|
|
337
|
+
handler: async (req, res, { sse }) => {
|
|
338
|
+
// 1. Controller mode with automatic keepalive & reconnection directives
|
|
339
|
+
const stream = sse({ heartbeat: 15000, retry: 3000 });
|
|
340
|
+
|
|
341
|
+
await stream.send({ delta: 'Hello ' });
|
|
342
|
+
await stream.send({ event: 'delta', data: { text: 'world!' }, id: '1' });
|
|
343
|
+
stream.close();
|
|
344
|
+
|
|
345
|
+
// 2. OR stream an async generator / LLM stream directly:
|
|
346
|
+
// return res.sse(async function*() {
|
|
347
|
+
// for await (const chunk of llmStream) {
|
|
348
|
+
// yield { token: chunk.text };
|
|
349
|
+
// }
|
|
350
|
+
// });
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### 6.6 Cascading Middleware (`_middleware.js`) & Nested Layouts (`_layout.njk`)
|
|
356
|
+
|
|
357
|
+
Webspresso supports folder-level cascading middleware inheritance and nested layouts across directory trees:
|
|
358
|
+
|
|
359
|
+
#### 1. Cascading Middleware (`_middleware.js`)
|
|
360
|
+
Any directory under `pages/` can declare a `_middleware.js` file:
|
|
361
|
+
- **Execution Order**: Middlewares execute in top-down ancestral order (from root down to route leaf) before route handlers and route-specific middleware.
|
|
362
|
+
- **Early Termination**: If any middleware sends headers (e.g. `res.status(401).json(...)` or `res.redirect('/login')`), execution halts immediately and downstream middlewares/handlers are skipped.
|
|
363
|
+
- **Export Formats**: Standard Express middleware function `(req, res, next)`, async function, named middleware string/array `['auth', 'cors']`, or middleware tuples with options `[['rateLimit', { max: 100 }]]`.
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
pages/
|
|
367
|
+
โโโ _middleware.js # Runs for ALL routes (adds global headers / request ID)
|
|
368
|
+
โโโ api/
|
|
369
|
+
โ โโโ admin/
|
|
370
|
+
โ โโโ _middleware.js # Protects all /api/admin/* endpoints
|
|
371
|
+
โ โโโ metrics.get.js # Inherits root + admin middleware
|
|
372
|
+
โโโ dashboard/
|
|
373
|
+
โโโ _middleware.js # Auth guard for /dashboard/* pages
|
|
374
|
+
โโโ settings.njk
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### 2. Nested Layouts (`_layout.njk` & `_layout.js`)
|
|
378
|
+
Pages under `pages/` automatically inherit ancestor layouts:
|
|
379
|
+
- **Wrapping Order**: Child template is rendered first, then wrapped by layouts from innermost to outermost using `{{ content | safe }}`.
|
|
380
|
+
- **Layout Loaders (`_layout.js`)**: Companion `_layout.js` files can export `async function load(req, ctx)` to supply shared data (e.g., site title, navigation, breadcrumbs) in top-down order before the page loader runs.
|
|
381
|
+
- **Opt-Out**: A page can opt out of automatic layout wrapping by setting `layout: false` in its companion `.js` config/loader, or by explicitly using `{% extends "..." %}` in its Nunjucks template.
|
|
382
|
+
|
|
383
|
+
```html
|
|
384
|
+
{# pages/_layout.njk #}
|
|
385
|
+
<!DOCTYPE html>
|
|
386
|
+
<html>
|
|
387
|
+
<head><title>{{ siteTitle }}</title></head>
|
|
388
|
+
<body>
|
|
389
|
+
<header>Global Nav</header>
|
|
390
|
+
{{ content | safe }}
|
|
391
|
+
</body>
|
|
392
|
+
</html>
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
```html
|
|
396
|
+
{# pages/dashboard/_layout.njk #}
|
|
397
|
+
<div class="dashboard-shell">
|
|
398
|
+
<aside>Dashboard Sidebar</aside>
|
|
399
|
+
<main>{{ content | safe }}</main>
|
|
400
|
+
</div>
|
|
401
|
+
```
|
|
402
|
+
|
|
283
403
|
---
|
|
284
404
|
|
|
285
|
-
##
|
|
405
|
+
## 7. Summary Checklist for Agents
|
|
286
406
|
|
|
287
407
|
When a user asks to add or modify endpoints:
|
|
288
408
|
1. Identify the URL and method (e.g. `POST /api/auth/register` or `POST /auth/login`).
|
package/AGENTS.md
CHANGED
|
@@ -142,6 +142,7 @@ Plugins expose `name`, `version`, `dependencies`, and lifecycle hooks (`register
|
|
|
142
142
|
- `restResourcePlugin`: Zero-boilerplate RESTful CRUD API generator directly from ORM models with eager loading, sanitization, filtering, pagination, and soft-delete scoping.
|
|
143
143
|
- `xlsxPlugin`: Excel spreadsheet generation, parsing, ORM streaming export (`res.xlsx`), and built-in services (`xlsx.generate`, `xlsx.parse`, `xlsx.exportModel`).
|
|
144
144
|
- `csvPlugin`: RFC 4180 CSV generator, parser, Excel UTF-8 BOM compatibility, formula sanitization, streaming writer (`res.csv`), and built-in services (`csv.generate`, `csv.parse`, `csv.exportModel`).
|
|
145
|
+
- `apiKeyPlugin`: Zero-dependency B2B & Developer API Key Management with SHA-256 secure storage, constant-time verification, granular wildcard scopes, named route middleware (`ctx.middlewares.apiKey`), and non-blocking usage tracking.
|
|
145
146
|
|
|
146
147
|
---
|
|
147
148
|
|
|
@@ -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 };
|
|
@@ -10,14 +10,26 @@ const { execSync } = require('child_process');
|
|
|
10
10
|
function registerCommand(program) {
|
|
11
11
|
program
|
|
12
12
|
.command('add tailwind')
|
|
13
|
+
.alias('add:tailwind')
|
|
13
14
|
.description('Add Tailwind CSS to the project with build process')
|
|
14
|
-
.
|
|
15
|
+
.option('--v4', 'Use Tailwind CSS v4 (Oxide engine, LightningCSS, zero postcss config)')
|
|
16
|
+
.option('--v3', 'Use Tailwind CSS v3 (legacy PostCSS engine)')
|
|
17
|
+
.option('-v, --tailwind-version <version>', 'Tailwind version (3 or 4)', '3')
|
|
18
|
+
.option('--upgrade', 'Upgrade existing Tailwind v3 project to Tailwind v4')
|
|
19
|
+
.action(async (...actionArgs) => {
|
|
20
|
+
const cmd = actionArgs[actionArgs.length - 1];
|
|
21
|
+
const options = (cmd && typeof cmd.opts === 'function')
|
|
22
|
+
? cmd.opts()
|
|
23
|
+
: (actionArgs.find(a => a && typeof a === 'object') || {});
|
|
24
|
+
|
|
15
25
|
if (!fs.existsSync('package.json')) {
|
|
16
26
|
console.error('โ Not a Webspresso project! Run this command in your project directory.');
|
|
17
27
|
process.exit(1);
|
|
18
28
|
}
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
const isV4 = Boolean(options.v4 || options.upgrade || options.tailwindVersion === '4');
|
|
31
|
+
|
|
32
|
+
console.log(`\n๐จ Adding Tailwind CSS ${isV4 ? 'v4 (Oxide engine, zero-config)' : 'v3'} to your project...\n`);
|
|
21
33
|
|
|
22
34
|
// Read package.json
|
|
23
35
|
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
|
@@ -27,17 +39,26 @@ function registerCommand(program) {
|
|
|
27
39
|
packageJson.devDependencies = {};
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
packageJson.devDependencies['tailwindcss'] = '^3.4.1';
|
|
31
|
-
packageJson.devDependencies['postcss'] = '^8.4.35';
|
|
32
|
-
packageJson.devDependencies['autoprefixer'] = '^10.4.17';
|
|
33
|
-
|
|
34
|
-
// Add build scripts
|
|
35
42
|
if (!packageJson.scripts) {
|
|
36
43
|
packageJson.scripts = {};
|
|
37
44
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
|
|
46
|
+
if (isV4) {
|
|
47
|
+
packageJson.devDependencies['@tailwindcss/cli'] = '^4.0.0';
|
|
48
|
+
packageJson.devDependencies['tailwindcss'] = '^4.0.0';
|
|
49
|
+
delete packageJson.devDependencies['postcss'];
|
|
50
|
+
delete packageJson.devDependencies['autoprefixer'];
|
|
51
|
+
|
|
52
|
+
packageJson.scripts['build:css'] = '@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --minify';
|
|
53
|
+
packageJson.scripts['watch:css'] = '@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --watch';
|
|
54
|
+
} else {
|
|
55
|
+
packageJson.devDependencies['tailwindcss'] = '^3.4.1';
|
|
56
|
+
packageJson.devDependencies['postcss'] = '^8.4.35';
|
|
57
|
+
packageJson.devDependencies['autoprefixer'] = '^10.4.17';
|
|
58
|
+
|
|
59
|
+
packageJson.scripts['build:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --minify';
|
|
60
|
+
packageJson.scripts['watch:css'] = 'tailwindcss -i ./src/input.css -o ./public/css/style.css --watch';
|
|
61
|
+
}
|
|
41
62
|
|
|
42
63
|
// Update dev script to include CSS watch
|
|
43
64
|
if (packageJson.scripts.dev) {
|
|
@@ -50,24 +71,48 @@ function registerCommand(program) {
|
|
|
50
71
|
}
|
|
51
72
|
|
|
52
73
|
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
|
|
53
|
-
console.log(
|
|
74
|
+
console.log(`โ
Updated package.json (${isV4 ? 'Tailwind v4' : 'Tailwind v3'})`);
|
|
54
75
|
|
|
55
76
|
// Create src directory if it doesn't exist
|
|
56
77
|
if (!fs.existsSync('src')) {
|
|
57
78
|
fs.mkdirSync('src', { recursive: true });
|
|
58
79
|
}
|
|
59
80
|
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
if (isV4) {
|
|
82
|
+
// Create v4 input.css
|
|
83
|
+
const inputCss = `@import "tailwindcss";
|
|
84
|
+
|
|
85
|
+
@source "../pages/**/*.{njk,js}";
|
|
86
|
+
@source "../views/**/*.njk";
|
|
87
|
+
@source "../src/**/*.js";
|
|
88
|
+
`;
|
|
89
|
+
fs.writeFileSync('src/input.css', inputCss);
|
|
90
|
+
console.log('โ
Created src/input.css (@import "tailwindcss"; + @source directives)');
|
|
91
|
+
|
|
92
|
+
// If upgrading from v3, clean up obsolete configs
|
|
93
|
+
if (fs.existsSync('postcss.config.js')) {
|
|
94
|
+
try {
|
|
95
|
+
fs.unlinkSync('postcss.config.js');
|
|
96
|
+
console.log('๐งน Removed obsolete postcss.config.js (not needed in v4)');
|
|
97
|
+
} catch (_) {}
|
|
98
|
+
}
|
|
99
|
+
if (fs.existsSync('tailwind.config.js')) {
|
|
100
|
+
try {
|
|
101
|
+
fs.renameSync('tailwind.config.js', 'tailwind.config.js.backup');
|
|
102
|
+
console.log('โน๏ธ Renamed tailwind.config.js to tailwind.config.js.backup (v4 is CSS-first)');
|
|
103
|
+
} catch (_) {}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
// Create v3 input.css
|
|
107
|
+
const inputCss = `@tailwind base;
|
|
62
108
|
@tailwind components;
|
|
63
109
|
@tailwind utilities;
|
|
64
110
|
`;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
111
|
+
fs.writeFileSync('src/input.css', inputCss);
|
|
112
|
+
console.log('โ
Created src/input.css');
|
|
113
|
+
|
|
114
|
+
// Create tailwind.config.js
|
|
115
|
+
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
71
116
|
module.exports = {
|
|
72
117
|
content: [
|
|
73
118
|
'./pages/**/*.{njk,js}',
|
|
@@ -80,21 +125,20 @@ module.exports = {
|
|
|
80
125
|
plugins: [],
|
|
81
126
|
}
|
|
82
127
|
`;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const postcssConfig = `module.exports = {
|
|
128
|
+
fs.writeFileSync('tailwind.config.js', tailwindConfig);
|
|
129
|
+
console.log('โ
Created tailwind.config.js');
|
|
130
|
+
|
|
131
|
+
// Create postcss.config.js
|
|
132
|
+
const postcssConfig = `module.exports = {
|
|
89
133
|
plugins: {
|
|
90
134
|
tailwindcss: {},
|
|
91
135
|
autoprefixer: {},
|
|
92
136
|
},
|
|
93
137
|
}
|
|
94
138
|
`;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
139
|
+
fs.writeFileSync('postcss.config.js', postcssConfig);
|
|
140
|
+
console.log('โ
Created postcss.config.js');
|
|
141
|
+
}
|
|
98
142
|
|
|
99
143
|
// Check if layout.njk exists and update it (before creating CSS)
|
|
100
144
|
const layoutPath = 'views/layout.njk';
|
|
@@ -128,9 +172,10 @@ module.exports = {
|
|
|
128
172
|
fs.writeFileSync('public/css/style.css', '/* Run npm run build:css */\n');
|
|
129
173
|
console.log('โ
Created public/css/style.css');
|
|
130
174
|
|
|
131
|
-
// Try to build CSS if tailwindcss is already installed
|
|
175
|
+
// Try to build CSS if tailwindcss / @tailwindcss/cli is already installed
|
|
132
176
|
const tailwindBin = path.join(process.cwd(), 'node_modules', '.bin', 'tailwindcss');
|
|
133
|
-
|
|
177
|
+
const tailwindCliBin = path.join(process.cwd(), 'node_modules', '.bin', '@tailwindcss/cli');
|
|
178
|
+
if (fs.existsSync(tailwindBin) || fs.existsSync(tailwindCliBin)) {
|
|
134
179
|
try {
|
|
135
180
|
console.log('\n๐จ Building Tailwind CSS from your templates...');
|
|
136
181
|
execSync('npm run build:css', { stdio: 'inherit', cwd: process.cwd() });
|
|
@@ -139,7 +184,7 @@ module.exports = {
|
|
|
139
184
|
console.log('\nโ ๏ธ CSS build failed. Run "npm run build:css" manually.\n');
|
|
140
185
|
}
|
|
141
186
|
} else {
|
|
142
|
-
console.log(
|
|
187
|
+
console.log(`\nโ
Tailwind CSS ${isV4 ? 'v4' : 'v3'} added successfully!\n`);
|
|
143
188
|
console.log('Next steps:');
|
|
144
189
|
console.log(' npm install');
|
|
145
190
|
console.log(' npm run build:css');
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prune old site analytics records (CLI)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { loadDbConfig, createDbInstance } = require('../utils/db');
|
|
6
|
+
|
|
7
|
+
function registerCommand(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('analytics:prune')
|
|
10
|
+
.description('Delete analytics page views and client errors older than the given retention window')
|
|
11
|
+
.option('--days <n>', 'Delete page views older than this many days', '90')
|
|
12
|
+
.option('--error-days <n>', 'Delete client errors older than this many days', '30')
|
|
13
|
+
.option('--table <name>', 'Page views table name', 'analytics_page_views')
|
|
14
|
+
.option('--errors-table <name>', 'Client errors table name', 'analytics_client_errors')
|
|
15
|
+
.option('-e, --env <environment>', 'Environment (development, production)', 'development')
|
|
16
|
+
.option('-c, --config <path>', 'Path to database config file')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
const days = parseInt(options.days, 10);
|
|
19
|
+
if (Number.isNaN(days) || days < 1) {
|
|
20
|
+
console.error('โ --days must be a positive integer');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const errorDays = parseInt(options.errorDays, 10);
|
|
25
|
+
if (Number.isNaN(errorDays) || errorDays < 1) {
|
|
26
|
+
console.error('โ --error-days must be a positive integer');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { config, path: configPath } = loadDbConfig(options.config);
|
|
31
|
+
console.log(`\n๐ฆ Using config: ${configPath}`);
|
|
32
|
+
console.log(` Environment: ${options.env}\n`);
|
|
33
|
+
|
|
34
|
+
const knex = await createDbInstance(config, options.env);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const { purgeAnalyticsData } = require('../../plugins/site-analytics/purge');
|
|
38
|
+
const result = await purgeAnalyticsData(knex, {
|
|
39
|
+
days,
|
|
40
|
+
errorDays,
|
|
41
|
+
tableName: options.table,
|
|
42
|
+
errorsTableName: options.errorsTable,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log(`โ
Analytics purge completed:`);
|
|
46
|
+
console.log(` - Page views deleted: ${result.viewsDeleted} (cutoff: ${result.viewsCutoff})`);
|
|
47
|
+
console.log(` - Client errors deleted: ${result.errorsDeleted} (cutoff: ${result.errorsCutoff})\n`);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('โ analytics:prune failed:', err.message);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
} finally {
|
|
52
|
+
await knex.destroy();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { registerCommand };
|