webspresso 0.0.4 → 0.0.6
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/README.md +95 -2
- package/core/applySchema.js +48 -0
- package/core/compileSchema.js +68 -0
- package/package.json +6 -3
- package/src/plugin-manager.js +1 -0
- package/utils/schemaCache.js +59 -0
package/README.md
CHANGED
|
@@ -7,11 +7,12 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
|
|
|
7
7
|
- **File-Based Routing**: Create pages by adding `.njk` files to a `pages/` directory
|
|
8
8
|
- **Dynamic Routes**: Use `[param]` for dynamic params and `[...rest]` for catch-all routes
|
|
9
9
|
- **API Endpoints**: Add `.js` files to `pages/api/` with method suffixes (e.g., `health.get.js`)
|
|
10
|
+
- **Schema Validation**: Zod-based request validation for body, params, and query
|
|
10
11
|
- **Built-in i18n**: JSON-based translations with automatic locale detection
|
|
11
12
|
- **Lifecycle Hooks**: Global and route-level hooks for request processing
|
|
12
13
|
- **Template Helpers**: Laravel-inspired helper functions available in templates
|
|
13
14
|
- **Plugin System**: Extensible architecture with version control and inter-plugin communication
|
|
14
|
-
- **Built-in Plugins**:
|
|
15
|
+
- **Built-in Plugins**: Development dashboard, sitemap generator, analytics integration (Google, Yandex, Bing)
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
17
18
|
|
|
@@ -325,12 +326,13 @@ Webspresso has a built-in plugin system with version control and dependency mana
|
|
|
325
326
|
|
|
326
327
|
```javascript
|
|
327
328
|
const { createApp } = require('webspresso');
|
|
328
|
-
const { sitemapPlugin, analyticsPlugin } = require('webspresso/plugins');
|
|
329
|
+
const { sitemapPlugin, analyticsPlugin, dashboardPlugin } = require('webspresso/plugins');
|
|
329
330
|
|
|
330
331
|
const { app } = createApp({
|
|
331
332
|
pagesDir: './pages',
|
|
332
333
|
viewsDir: './views',
|
|
333
334
|
plugins: [
|
|
335
|
+
dashboardPlugin(), // Dev dashboard at /_webspresso
|
|
334
336
|
sitemapPlugin({
|
|
335
337
|
hostname: 'https://example.com',
|
|
336
338
|
exclude: ['/admin/*', '/api/*'],
|
|
@@ -357,6 +359,28 @@ const { app } = createApp({
|
|
|
357
359
|
|
|
358
360
|
### Built-in Plugins
|
|
359
361
|
|
|
362
|
+
**Dashboard Plugin:**
|
|
363
|
+
- Development dashboard at `/_webspresso`
|
|
364
|
+
- Monitor all routes (SSR pages and API endpoints)
|
|
365
|
+
- View loaded plugins and configuration
|
|
366
|
+
- Filter and search routes
|
|
367
|
+
- Only active in development mode (disabled in production)
|
|
368
|
+
|
|
369
|
+
```javascript
|
|
370
|
+
const { dashboardPlugin } = require('webspresso/plugins');
|
|
371
|
+
|
|
372
|
+
const { app } = createApp({
|
|
373
|
+
pagesDir: './pages',
|
|
374
|
+
plugins: [
|
|
375
|
+
dashboardPlugin() // Available at /_webspresso in dev mode
|
|
376
|
+
]
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Options:
|
|
381
|
+
- `path` - Custom dashboard path (default: `/_webspresso`)
|
|
382
|
+
- `enabled` - Force enable/disable (default: auto based on NODE_ENV)
|
|
383
|
+
|
|
360
384
|
**Sitemap Plugin:**
|
|
361
385
|
- Generates `/sitemap.xml` from routes automatically
|
|
362
386
|
- Excludes dynamic routes and API endpoints
|
|
@@ -471,6 +495,75 @@ Create `.js` files in `pages/api/` with optional method suffixes:
|
|
|
471
495
|
| `pages/api/echo.post.js` | `POST /api/echo` |
|
|
472
496
|
| `pages/api/users/[id].get.js` | `GET /api/users/:id` |
|
|
473
497
|
|
|
498
|
+
**Basic API Handler:**
|
|
499
|
+
|
|
500
|
+
```javascript
|
|
501
|
+
// pages/api/health.get.js
|
|
502
|
+
module.exports = async function handler(req, res) {
|
|
503
|
+
res.json({ status: 'ok' });
|
|
504
|
+
};
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**With Schema Validation:**
|
|
508
|
+
|
|
509
|
+
```javascript
|
|
510
|
+
// pages/api/posts.post.js
|
|
511
|
+
module.exports = {
|
|
512
|
+
schema: ({ z }) => ({
|
|
513
|
+
body: z.object({
|
|
514
|
+
title: z.string().min(3).max(100),
|
|
515
|
+
content: z.string(),
|
|
516
|
+
tags: z.array(z.string()).optional()
|
|
517
|
+
}),
|
|
518
|
+
query: z.object({
|
|
519
|
+
draft: z.coerce.boolean().default(false)
|
|
520
|
+
})
|
|
521
|
+
}),
|
|
522
|
+
|
|
523
|
+
async handler(req, res) {
|
|
524
|
+
// Validated & parsed data available in req.input
|
|
525
|
+
const { title, content, tags } = req.input.body;
|
|
526
|
+
const { draft } = req.input.query;
|
|
527
|
+
|
|
528
|
+
// Original req.body, req.query remain untouched
|
|
529
|
+
res.json({ success: true, title, draft });
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
**Schema Options:**
|
|
535
|
+
|
|
536
|
+
| Key | Description |
|
|
537
|
+
|-----|-------------|
|
|
538
|
+
| `body` | Validates `req.body` (POST/PUT/PATCH) |
|
|
539
|
+
| `params` | Validates route parameters (e.g., `:id`) |
|
|
540
|
+
| `query` | Validates query string parameters |
|
|
541
|
+
| `response` | Response schema (for documentation, not enforced) |
|
|
542
|
+
|
|
543
|
+
All schemas use [Zod](https://zod.dev) for validation. Invalid requests throw a `ZodError` which can be caught by error handlers.
|
|
544
|
+
|
|
545
|
+
**Dynamic Route with Params Validation:**
|
|
546
|
+
|
|
547
|
+
```javascript
|
|
548
|
+
// pages/api/users/[id].get.js
|
|
549
|
+
module.exports = {
|
|
550
|
+
schema: ({ z }) => ({
|
|
551
|
+
params: z.object({
|
|
552
|
+
id: z.string().uuid()
|
|
553
|
+
}),
|
|
554
|
+
query: z.object({
|
|
555
|
+
fields: z.string().optional()
|
|
556
|
+
})
|
|
557
|
+
}),
|
|
558
|
+
|
|
559
|
+
async handler(req, res) {
|
|
560
|
+
const { id } = req.input.params; // Validated UUID
|
|
561
|
+
const user = await getUser(id);
|
|
562
|
+
res.json(user);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
```
|
|
566
|
+
|
|
474
567
|
### Route Config
|
|
475
568
|
|
|
476
569
|
Add a `.js` file alongside your `.njk` file to configure the route:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Applicator
|
|
3
|
+
* Parses and validates request input against compiled schemas
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Apply compiled schema to request
|
|
8
|
+
* Parses body, params, and query against their respective schemas
|
|
9
|
+
* Stores validated results in req.input
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} req - Express request object
|
|
12
|
+
* @param {Object|null} compiledSchema - Compiled schema from compileSchema
|
|
13
|
+
* @returns {void}
|
|
14
|
+
* @throws {ZodError} If validation fails
|
|
15
|
+
*/
|
|
16
|
+
function applySchema(req, compiledSchema) {
|
|
17
|
+
// Initialize req.input
|
|
18
|
+
req.input = {
|
|
19
|
+
body: undefined,
|
|
20
|
+
params: undefined,
|
|
21
|
+
query: undefined
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// No schema means no validation
|
|
25
|
+
if (!compiledSchema) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parse body if schema exists
|
|
30
|
+
if (compiledSchema.body) {
|
|
31
|
+
req.input.body = compiledSchema.body.parse(req.body);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Parse params if schema exists
|
|
35
|
+
if (compiledSchema.params) {
|
|
36
|
+
req.input.params = compiledSchema.params.parse(req.params);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Parse query if schema exists
|
|
40
|
+
if (compiledSchema.query) {
|
|
41
|
+
req.input.query = compiledSchema.query.parse(req.query);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
applySchema
|
|
47
|
+
};
|
|
48
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Compiler
|
|
3
|
+
* Compiles schema definitions from API files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const z = require('zod');
|
|
7
|
+
const schemaCache = require('../utils/schemaCache');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compile schema from an API module
|
|
11
|
+
* @param {string} filePath - Absolute file path to API module
|
|
12
|
+
* @param {Object} apiModule - The loaded API module
|
|
13
|
+
* @returns {Object|null} Compiled schema object or null if no schema
|
|
14
|
+
*/
|
|
15
|
+
function compileSchema(filePath, apiModule) {
|
|
16
|
+
// Return cached schema if exists
|
|
17
|
+
if (schemaCache.has(filePath)) {
|
|
18
|
+
return schemaCache.get(filePath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check if module exports schema
|
|
22
|
+
const schemaFn = apiModule.schema;
|
|
23
|
+
|
|
24
|
+
// Schema is optional
|
|
25
|
+
if (schemaFn === undefined) {
|
|
26
|
+
schemaCache.set(filePath, null);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Schema must be a function
|
|
31
|
+
if (typeof schemaFn !== 'function') {
|
|
32
|
+
throw new Error(`Schema in ${filePath} must be a function`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Call schema function with { z }
|
|
36
|
+
const compiled = schemaFn({ z });
|
|
37
|
+
|
|
38
|
+
// Validate compiled schema structure
|
|
39
|
+
if (compiled !== null && typeof compiled !== 'object') {
|
|
40
|
+
throw new Error(`Schema function in ${filePath} must return an object or null`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Cache and return
|
|
44
|
+
schemaCache.set(filePath, compiled);
|
|
45
|
+
return compiled;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Clear schema cache for a file (for hot-reload)
|
|
50
|
+
* @param {string} filePath - Absolute file path
|
|
51
|
+
*/
|
|
52
|
+
function invalidateSchema(filePath) {
|
|
53
|
+
schemaCache.del(filePath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Clear all cached schemas
|
|
58
|
+
*/
|
|
59
|
+
function clearAllSchemas() {
|
|
60
|
+
schemaCache.clear();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
compileSchema,
|
|
65
|
+
invalidateSchema,
|
|
66
|
+
clearAllSchemas
|
|
67
|
+
};
|
|
68
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,14 +29,17 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"index.js",
|
|
31
31
|
"bin/",
|
|
32
|
-
"src/"
|
|
32
|
+
"src/",
|
|
33
|
+
"utils/",
|
|
34
|
+
"core/"
|
|
33
35
|
],
|
|
34
36
|
"dependencies": {
|
|
35
37
|
"commander": "^11.1.0",
|
|
36
38
|
"express": "^4.18.2",
|
|
37
39
|
"helmet": "^7.2.0",
|
|
38
40
|
"inquirer": "^8.2.6",
|
|
39
|
-
"nunjucks": "^3.2.4"
|
|
41
|
+
"nunjucks": "^3.2.4",
|
|
42
|
+
"zod": "^3.23.0"
|
|
40
43
|
},
|
|
41
44
|
"peerDependencies": {
|
|
42
45
|
"dotenv": "^16.0.0"
|
package/src/plugin-manager.js
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Cache
|
|
3
|
+
* Caches compiled Zod schemas per file path
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const cache = new Map();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get cached schema for a file path
|
|
10
|
+
* @param {string} filePath - Absolute file path
|
|
11
|
+
* @returns {Object|undefined} Compiled schema or undefined
|
|
12
|
+
*/
|
|
13
|
+
function get(filePath) {
|
|
14
|
+
return cache.get(filePath);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set compiled schema for a file path
|
|
19
|
+
* @param {string} filePath - Absolute file path
|
|
20
|
+
* @param {Object} schema - Compiled schema object
|
|
21
|
+
*/
|
|
22
|
+
function set(filePath, schema) {
|
|
23
|
+
cache.set(filePath, schema);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if schema exists in cache
|
|
28
|
+
* @param {string} filePath - Absolute file path
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
function has(filePath) {
|
|
32
|
+
return cache.has(filePath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Clear all cached schemas
|
|
37
|
+
* Useful for development hot-reload
|
|
38
|
+
*/
|
|
39
|
+
function clear() {
|
|
40
|
+
cache.clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Delete a specific schema from cache
|
|
45
|
+
* @param {string} filePath - Absolute file path
|
|
46
|
+
* @returns {boolean} True if deleted
|
|
47
|
+
*/
|
|
48
|
+
function del(filePath) {
|
|
49
|
+
return cache.delete(filePath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
get,
|
|
54
|
+
set,
|
|
55
|
+
has,
|
|
56
|
+
clear,
|
|
57
|
+
del
|
|
58
|
+
};
|
|
59
|
+
|