webspresso 0.0.2 → 0.0.3
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 +127 -4
- package/index.js +13 -2
- package/package.json +1 -1
- package/src/file-router.js +53 -4
- package/src/helpers.js +206 -1
- package/src/server.js +126 -52
package/README.md
CHANGED
|
@@ -184,11 +184,134 @@ Creates and configures the Express app.
|
|
|
184
184
|
- `true` or `undefined`: Use default secure configuration
|
|
185
185
|
- `false`: Disable Helmet
|
|
186
186
|
- `Object`: Custom Helmet configuration (merged with defaults)
|
|
187
|
+
- `middlewares` (optional): Named middleware registry for routes
|
|
187
188
|
|
|
188
|
-
**
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
**Example with middlewares:**
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
const { createApp } = require('webspresso');
|
|
193
|
+
|
|
194
|
+
const { app } = createApp({
|
|
195
|
+
pagesDir: './pages',
|
|
196
|
+
viewsDir: './views',
|
|
197
|
+
middlewares: {
|
|
198
|
+
auth: (req, res, next) => {
|
|
199
|
+
if (!req.session?.user) {
|
|
200
|
+
return res.redirect('/login');
|
|
201
|
+
}
|
|
202
|
+
next();
|
|
203
|
+
},
|
|
204
|
+
admin: (req, res, next) => {
|
|
205
|
+
if (req.session?.user?.role !== 'admin') {
|
|
206
|
+
return res.status(403).send('Forbidden');
|
|
207
|
+
}
|
|
208
|
+
next();
|
|
209
|
+
},
|
|
210
|
+
rateLimit: require('express-rate-limit')({ windowMs: 60000, max: 100 })
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Then use in route configs by name:
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
// pages/admin/index.js
|
|
219
|
+
module.exports = {
|
|
220
|
+
middleware: ['auth', 'admin'], // Use named middlewares
|
|
221
|
+
load(req, ctx) { ... }
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// pages/api/data.get.js
|
|
225
|
+
module.exports = {
|
|
226
|
+
middleware: ['auth', 'rateLimit'],
|
|
227
|
+
handler: (req, res) => res.json({ data: 'protected' })
|
|
228
|
+
};
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Custom Error Pages:**
|
|
232
|
+
|
|
233
|
+
```javascript
|
|
234
|
+
const { createApp } = require('webspresso');
|
|
235
|
+
|
|
236
|
+
const { app } = createApp({
|
|
237
|
+
pagesDir: './pages',
|
|
238
|
+
viewsDir: './views',
|
|
239
|
+
errorPages: {
|
|
240
|
+
// Option 1: Custom handler function
|
|
241
|
+
notFound: (req, res) => {
|
|
242
|
+
res.render('errors/404.njk', { url: req.url });
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// Option 2: Template path (rendered with Nunjucks)
|
|
246
|
+
serverError: 'errors/500.njk'
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Error templates receive these variables:
|
|
252
|
+
- `404.njk`: `{ url, method }`
|
|
253
|
+
- `500.njk`: `{ error, status, isDev }`
|
|
254
|
+
|
|
255
|
+
**Asset Management:**
|
|
256
|
+
|
|
257
|
+
Configure asset handling with versioning and manifest support:
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
const { createApp } = require('webspresso');
|
|
261
|
+
const path = require('path');
|
|
262
|
+
|
|
263
|
+
const { app } = createApp({
|
|
264
|
+
pagesDir: './pages',
|
|
265
|
+
viewsDir: './views',
|
|
266
|
+
publicDir: './public',
|
|
267
|
+
assets: {
|
|
268
|
+
// Option 1: Simple versioning (cache busting)
|
|
269
|
+
version: '1.2.3', // or process.env.APP_VERSION
|
|
270
|
+
|
|
271
|
+
// Option 2: Manifest file (Vite, Webpack, etc.)
|
|
272
|
+
manifestPath: path.join(__dirname, 'public/.vite/manifest.json'),
|
|
273
|
+
|
|
274
|
+
// URL prefix for assets
|
|
275
|
+
prefix: '/static'
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Use asset helpers in templates:
|
|
281
|
+
|
|
282
|
+
```njk
|
|
283
|
+
{# Using fsy helpers (auto-resolved) #}
|
|
284
|
+
<link rel="stylesheet" href="{{ fsy.asset('/css/style.css') }}">
|
|
285
|
+
|
|
286
|
+
{# Or generate full HTML tags #}
|
|
287
|
+
{{ fsy.css('/css/style.css') | safe }}
|
|
288
|
+
{{ fsy.js('/js/app.js', { defer: true, type: 'module' }) | safe }}
|
|
289
|
+
{{ fsy.img('/images/logo.png', 'Site Logo', { class: 'logo', loading: 'lazy' }) | safe }}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Asset helpers available in `fsy`:
|
|
293
|
+
- `asset(path)` - Returns versioned/manifest-resolved URL
|
|
294
|
+
- `css(href, attrs)` - Generates `<link>` tag
|
|
295
|
+
- `js(src, attrs)` - Generates `<script>` tag
|
|
296
|
+
- `img(src, alt, attrs)` - Generates `<img>` tag
|
|
297
|
+
|
|
298
|
+
**Manifest Support:**
|
|
299
|
+
|
|
300
|
+
Works with Vite and Webpack manifest formats:
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
// Vite manifest format (.vite/manifest.json)
|
|
304
|
+
{
|
|
305
|
+
"css/style.css": { "file": "assets/style-abc123.css" },
|
|
306
|
+
"js/app.js": { "file": "assets/app-xyz789.js" }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Webpack manifest format
|
|
310
|
+
{
|
|
311
|
+
"/css/style.css": "/dist/style.abc123.css",
|
|
312
|
+
"/js/app.js": "/dist/app.xyz789.js"
|
|
313
|
+
}
|
|
314
|
+
```
|
|
192
315
|
|
|
193
316
|
**Returns:** `{ app, nunjucksEnv }`
|
|
194
317
|
|
package/index.js
CHANGED
|
@@ -12,7 +12,13 @@ const {
|
|
|
12
12
|
createTranslator,
|
|
13
13
|
detectLocale
|
|
14
14
|
} = require('./src/file-router');
|
|
15
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
createHelpers,
|
|
17
|
+
utils,
|
|
18
|
+
AssetManager,
|
|
19
|
+
configureAssets,
|
|
20
|
+
getAssetManager
|
|
21
|
+
} = require('./src/helpers');
|
|
16
22
|
|
|
17
23
|
module.exports = {
|
|
18
24
|
// Main API
|
|
@@ -29,6 +35,11 @@ module.exports = {
|
|
|
29
35
|
|
|
30
36
|
// Template helpers
|
|
31
37
|
createHelpers,
|
|
32
|
-
utils
|
|
38
|
+
utils,
|
|
39
|
+
|
|
40
|
+
// Asset management
|
|
41
|
+
AssetManager,
|
|
42
|
+
configureAssets,
|
|
43
|
+
getAssetManager
|
|
33
44
|
};
|
|
34
45
|
|
package/package.json
CHANGED
package/src/file-router.js
CHANGED
|
@@ -272,16 +272,48 @@ function detectLocale(req) {
|
|
|
272
272
|
return process.env.DEFAULT_LOCALE || 'en';
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Resolve middleware from config - supports both functions and named strings
|
|
277
|
+
* @param {Array} middlewareConfig - Array of middleware functions or names
|
|
278
|
+
* @param {Object} middlewareRegistry - Named middleware registry
|
|
279
|
+
* @returns {Array} Array of resolved middleware functions
|
|
280
|
+
*/
|
|
281
|
+
function resolveMiddlewares(middlewareConfig, middlewareRegistry = {}) {
|
|
282
|
+
if (!middlewareConfig || !Array.isArray(middlewareConfig)) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return middlewareConfig.map((mw, index) => {
|
|
287
|
+
if (typeof mw === 'function') {
|
|
288
|
+
return mw;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (typeof mw === 'string') {
|
|
292
|
+
const resolved = middlewareRegistry[mw];
|
|
293
|
+
if (!resolved) {
|
|
294
|
+
throw new Error(`Middleware "${mw}" not found in registry. Available: ${Object.keys(middlewareRegistry).join(', ') || 'none'}`);
|
|
295
|
+
}
|
|
296
|
+
if (typeof resolved !== 'function') {
|
|
297
|
+
throw new Error(`Middleware "${mw}" must be a function`);
|
|
298
|
+
}
|
|
299
|
+
return resolved;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
throw new Error(`Invalid middleware at index ${index}: must be a function or string name`);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
275
306
|
/**
|
|
276
307
|
* Mount all pages as routes on the Express app
|
|
277
308
|
* @param {Object} app - Express app
|
|
278
309
|
* @param {Object} options - Options
|
|
279
310
|
* @param {string} options.pagesDir - Pages directory path
|
|
280
311
|
* @param {Object} options.nunjucks - Nunjucks environment
|
|
312
|
+
* @param {Object} options.middlewares - Named middleware registry
|
|
281
313
|
* @param {boolean} options.silent - Suppress console output
|
|
282
314
|
*/
|
|
283
315
|
function mountPages(app, options) {
|
|
284
|
-
const { pagesDir, nunjucks, silent = false } = options;
|
|
316
|
+
const { pagesDir, nunjucks, middlewares = {}, silent = false } = options;
|
|
285
317
|
const isDev = process.env.NODE_ENV !== 'production';
|
|
286
318
|
const log = silent ? () => {} : console.log.bind(console);
|
|
287
319
|
|
|
@@ -353,6 +385,7 @@ function mountPages(app, options) {
|
|
|
353
385
|
for (const route of sortRoutes(apiRoutes)) {
|
|
354
386
|
const handler = require(route.fullPath);
|
|
355
387
|
const handlerFn = typeof handler === 'function' ? handler : handler.default || handler.handler;
|
|
388
|
+
const routeMiddleware = handler.middleware;
|
|
356
389
|
|
|
357
390
|
if (typeof handlerFn !== 'function') {
|
|
358
391
|
console.warn(`API route ${route.file} does not export a function`);
|
|
@@ -372,6 +405,20 @@ function mountPages(app, options) {
|
|
|
372
405
|
? currentHandler
|
|
373
406
|
: currentHandler.default || currentHandler.handler;
|
|
374
407
|
|
|
408
|
+
// Run middleware if defined
|
|
409
|
+
const mwConfig = isDev ? currentHandler.middleware : routeMiddleware;
|
|
410
|
+
if (mwConfig) {
|
|
411
|
+
const resolvedMw = resolveMiddlewares(mwConfig, middlewares);
|
|
412
|
+
for (const mw of resolvedMw) {
|
|
413
|
+
await new Promise((resolve, reject) => {
|
|
414
|
+
mw(req, res, (err) => {
|
|
415
|
+
if (err) reject(err);
|
|
416
|
+
else resolve();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
375
422
|
await fn(req, res, next);
|
|
376
423
|
} catch (err) {
|
|
377
424
|
console.error(`API error ${route.routePath}:`, err);
|
|
@@ -429,8 +476,9 @@ function mountPages(app, options) {
|
|
|
429
476
|
await executeHook(routeHooks, 'beforeMiddleware', ctx);
|
|
430
477
|
|
|
431
478
|
// Run route middleware
|
|
432
|
-
if (config?.middleware
|
|
433
|
-
|
|
479
|
+
if (config?.middleware) {
|
|
480
|
+
const resolvedMiddlewares = resolveMiddlewares(config.middleware, middlewares);
|
|
481
|
+
for (const mw of resolvedMiddlewares) {
|
|
434
482
|
await new Promise((resolve, reject) => {
|
|
435
483
|
mw(req, res, (err) => {
|
|
436
484
|
if (err) reject(err);
|
|
@@ -515,6 +563,7 @@ module.exports = {
|
|
|
515
563
|
scanDirectory,
|
|
516
564
|
loadI18n,
|
|
517
565
|
createTranslator,
|
|
518
|
-
detectLocale
|
|
566
|
+
detectLocale,
|
|
567
|
+
resolveMiddlewares
|
|
519
568
|
};
|
|
520
569
|
|
package/src/helpers.js
CHANGED
|
@@ -4,6 +4,167 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const querystring = require('querystring');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Asset Manager - handles asset paths, versioning, and manifest
|
|
12
|
+
*/
|
|
13
|
+
class AssetManager {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.publicDir = options.publicDir || 'public';
|
|
16
|
+
this.manifestPath = options.manifestPath || null;
|
|
17
|
+
this.manifest = null;
|
|
18
|
+
this.version = options.version || null;
|
|
19
|
+
this.prefix = options.prefix || '';
|
|
20
|
+
|
|
21
|
+
// Load manifest if path provided
|
|
22
|
+
if (this.manifestPath) {
|
|
23
|
+
this.loadManifest();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load asset manifest file (Vite, Webpack, etc.)
|
|
29
|
+
*/
|
|
30
|
+
loadManifest() {
|
|
31
|
+
try {
|
|
32
|
+
const fullPath = path.resolve(this.manifestPath);
|
|
33
|
+
if (fs.existsSync(fullPath)) {
|
|
34
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
35
|
+
this.manifest = JSON.parse(content);
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.warn('Failed to load asset manifest:', err.message);
|
|
39
|
+
this.manifest = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve asset path from manifest or add version
|
|
45
|
+
* @param {string} assetPath - Asset path
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
resolve(assetPath) {
|
|
49
|
+
// Remove leading slash for manifest lookup
|
|
50
|
+
const lookupPath = assetPath.replace(/^\//, '');
|
|
51
|
+
|
|
52
|
+
// Check manifest first
|
|
53
|
+
if (this.manifest) {
|
|
54
|
+
// Vite manifest format
|
|
55
|
+
if (this.manifest[lookupPath]) {
|
|
56
|
+
const entry = this.manifest[lookupPath];
|
|
57
|
+
const resolved = typeof entry === 'string' ? entry : entry.file;
|
|
58
|
+
return this.prefix + '/' + resolved;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Webpack manifest format (direct mapping)
|
|
62
|
+
if (this.manifest[assetPath]) {
|
|
63
|
+
return this.prefix + this.manifest[assetPath];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Add version query string if provided
|
|
68
|
+
let finalPath = assetPath.startsWith('/') ? assetPath : '/' + assetPath;
|
|
69
|
+
finalPath = this.prefix + finalPath;
|
|
70
|
+
|
|
71
|
+
if (this.version) {
|
|
72
|
+
const separator = finalPath.includes('?') ? '&' : '?';
|
|
73
|
+
finalPath += `${separator}v=${this.version}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return finalPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get asset URL
|
|
81
|
+
*/
|
|
82
|
+
asset(assetPath) {
|
|
83
|
+
return this.resolve(assetPath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate CSS link tag
|
|
88
|
+
*/
|
|
89
|
+
css(href, attributes = {}) {
|
|
90
|
+
const resolvedHref = this.resolve(href);
|
|
91
|
+
const attrs = this.buildAttributes({
|
|
92
|
+
rel: 'stylesheet',
|
|
93
|
+
href: resolvedHref,
|
|
94
|
+
...attributes
|
|
95
|
+
});
|
|
96
|
+
return `<link ${attrs}>`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate JS script tag
|
|
101
|
+
*/
|
|
102
|
+
js(src, attributes = {}) {
|
|
103
|
+
const resolvedSrc = this.resolve(src);
|
|
104
|
+
const attrs = this.buildAttributes({
|
|
105
|
+
src: resolvedSrc,
|
|
106
|
+
...attributes
|
|
107
|
+
});
|
|
108
|
+
return `<script ${attrs}></script>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate image tag
|
|
113
|
+
*/
|
|
114
|
+
img(src, alt = '', attributes = {}) {
|
|
115
|
+
const resolvedSrc = this.resolve(src);
|
|
116
|
+
const attrs = this.buildAttributes({
|
|
117
|
+
src: resolvedSrc,
|
|
118
|
+
alt,
|
|
119
|
+
...attributes
|
|
120
|
+
});
|
|
121
|
+
return `<img ${attrs}>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build HTML attributes string
|
|
126
|
+
*/
|
|
127
|
+
buildAttributes(attrs) {
|
|
128
|
+
return Object.entries(attrs)
|
|
129
|
+
.filter(([_, v]) => v !== undefined && v !== null && v !== false)
|
|
130
|
+
.map(([k, v]) => v === true ? k : `${k}="${this.escapeHtml(String(v))}"`)
|
|
131
|
+
.join(' ');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Escape HTML special characters
|
|
136
|
+
*/
|
|
137
|
+
escapeHtml(str) {
|
|
138
|
+
return str
|
|
139
|
+
.replace(/&/g, '&')
|
|
140
|
+
.replace(/"/g, '"')
|
|
141
|
+
.replace(/'/g, ''')
|
|
142
|
+
.replace(/</g, '<')
|
|
143
|
+
.replace(/>/g, '>');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Global asset manager instance
|
|
148
|
+
let globalAssetManager = null;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Configure global asset manager
|
|
152
|
+
* @param {Object} options - Asset manager options
|
|
153
|
+
*/
|
|
154
|
+
function configureAssets(options = {}) {
|
|
155
|
+
globalAssetManager = new AssetManager(options);
|
|
156
|
+
return globalAssetManager;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get or create asset manager
|
|
161
|
+
*/
|
|
162
|
+
function getAssetManager() {
|
|
163
|
+
if (!globalAssetManager) {
|
|
164
|
+
globalAssetManager = new AssetManager();
|
|
165
|
+
}
|
|
166
|
+
return globalAssetManager;
|
|
167
|
+
}
|
|
7
168
|
|
|
8
169
|
/**
|
|
9
170
|
* Create the fsy helper object bound to the current request context
|
|
@@ -217,6 +378,47 @@ function createHelpers(ctx) {
|
|
|
217
378
|
return regex.test(currentPath);
|
|
218
379
|
}
|
|
219
380
|
return false;
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
// Asset helpers
|
|
384
|
+
/**
|
|
385
|
+
* Get asset URL with versioning/manifest support
|
|
386
|
+
* @param {string} assetPath - Asset path
|
|
387
|
+
* @returns {string}
|
|
388
|
+
*/
|
|
389
|
+
asset(assetPath) {
|
|
390
|
+
return getAssetManager().asset(assetPath);
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Generate CSS link tag
|
|
395
|
+
* @param {string} href - CSS file path
|
|
396
|
+
* @param {Object} attrs - Additional attributes
|
|
397
|
+
* @returns {string}
|
|
398
|
+
*/
|
|
399
|
+
css(href, attrs = {}) {
|
|
400
|
+
return getAssetManager().css(href, attrs);
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Generate JS script tag
|
|
405
|
+
* @param {string} src - JS file path
|
|
406
|
+
* @param {Object} attrs - Additional attributes
|
|
407
|
+
* @returns {string}
|
|
408
|
+
*/
|
|
409
|
+
js(src, attrs = {}) {
|
|
410
|
+
return getAssetManager().js(src, attrs);
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Generate image tag
|
|
415
|
+
* @param {string} src - Image file path
|
|
416
|
+
* @param {string} alt - Alt text
|
|
417
|
+
* @param {Object} attrs - Additional attributes
|
|
418
|
+
* @returns {string}
|
|
419
|
+
*/
|
|
420
|
+
img(src, alt = '', attrs = {}) {
|
|
421
|
+
return getAssetManager().img(src, alt, attrs);
|
|
220
422
|
}
|
|
221
423
|
};
|
|
222
424
|
}
|
|
@@ -270,6 +472,9 @@ const utils = {
|
|
|
270
472
|
|
|
271
473
|
module.exports = {
|
|
272
474
|
createHelpers,
|
|
273
|
-
utils
|
|
475
|
+
utils,
|
|
476
|
+
AssetManager,
|
|
477
|
+
configureAssets,
|
|
478
|
+
getAssetManager
|
|
274
479
|
};
|
|
275
480
|
|
package/src/server.js
CHANGED
|
@@ -8,6 +8,7 @@ const helmet = require('helmet');
|
|
|
8
8
|
const nunjucks = require('nunjucks');
|
|
9
9
|
|
|
10
10
|
const { mountPages } = require('./file-router');
|
|
11
|
+
const { configureAssets } = require('./helpers');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Get default Helmet configuration
|
|
@@ -51,6 +52,62 @@ function getDefaultHelmetConfig(isDev) {
|
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Default 404 page HTML
|
|
57
|
+
*/
|
|
58
|
+
function default404Html() {
|
|
59
|
+
return `<!DOCTYPE html>
|
|
60
|
+
<html>
|
|
61
|
+
<head>
|
|
62
|
+
<title>404 - Not Found</title>
|
|
63
|
+
<style>
|
|
64
|
+
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
|
|
65
|
+
.container { text-align: center; }
|
|
66
|
+
h1 { font-size: 4rem; margin: 0; color: #333; }
|
|
67
|
+
p { color: #666; margin: 1rem 0; }
|
|
68
|
+
a { color: #0066cc; text-decoration: none; }
|
|
69
|
+
a:hover { text-decoration: underline; }
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<div class="container">
|
|
74
|
+
<h1>404</h1>
|
|
75
|
+
<p>Page not found</p>
|
|
76
|
+
<a href="/">← Back to Home</a>
|
|
77
|
+
</div>
|
|
78
|
+
</body>
|
|
79
|
+
</html>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Default 500 page HTML
|
|
84
|
+
*/
|
|
85
|
+
function default500Html(err, isDev) {
|
|
86
|
+
return `<!DOCTYPE html>
|
|
87
|
+
<html>
|
|
88
|
+
<head>
|
|
89
|
+
<title>500 - Server Error</title>
|
|
90
|
+
<style>
|
|
91
|
+
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
|
|
92
|
+
.container { text-align: center; }
|
|
93
|
+
h1 { font-size: 4rem; margin: 0; color: #333; }
|
|
94
|
+
p { color: #666; margin: 1rem 0; }
|
|
95
|
+
pre { background: #fff; padding: 1rem; border-radius: 4px; text-align: left; overflow: auto; max-width: 600px; }
|
|
96
|
+
a { color: #0066cc; text-decoration: none; }
|
|
97
|
+
a:hover { text-decoration: underline; }
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
<div class="container">
|
|
102
|
+
<h1>500</h1>
|
|
103
|
+
<p>Internal Server Error</p>
|
|
104
|
+
${isDev && err ? `<pre>${err.stack || err.message}</pre>` : ''}
|
|
105
|
+
<a href="/">← Back to Home</a>
|
|
106
|
+
</div>
|
|
107
|
+
</body>
|
|
108
|
+
</html>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
54
111
|
/**
|
|
55
112
|
* Create and configure the Express app
|
|
56
113
|
* @param {Object} options - Configuration options
|
|
@@ -59,6 +116,14 @@ function getDefaultHelmetConfig(isDev) {
|
|
|
59
116
|
* @param {string} options.publicDir - Path to public/static directory
|
|
60
117
|
* @param {boolean} options.logging - Enable request logging (default: isDev)
|
|
61
118
|
* @param {Object|boolean} options.helmet - Helmet configuration (default: auto-configured, false to disable)
|
|
119
|
+
* @param {Object} options.middlewares - Named middleware registry for route configs
|
|
120
|
+
* @param {Object} options.assets - Asset manager configuration
|
|
121
|
+
* @param {string} options.assets.manifestPath - Path to asset manifest file (Vite, Webpack)
|
|
122
|
+
* @param {string} options.assets.version - Asset version for cache busting
|
|
123
|
+
* @param {string} options.assets.prefix - URL prefix for assets
|
|
124
|
+
* @param {Object} options.errorPages - Custom error page handlers
|
|
125
|
+
* @param {Function|string} options.errorPages.notFound - Custom 404 handler or template path
|
|
126
|
+
* @param {Function|string} options.errorPages.serverError - Custom 500 handler or template path
|
|
62
127
|
* @returns {Object} { app, nunjucksEnv }
|
|
63
128
|
*/
|
|
64
129
|
function createApp(options = {}) {
|
|
@@ -71,9 +136,18 @@ function createApp(options = {}) {
|
|
|
71
136
|
viewsDir,
|
|
72
137
|
publicDir,
|
|
73
138
|
logging = isDev && !isTest,
|
|
74
|
-
helmet: helmetConfig
|
|
139
|
+
helmet: helmetConfig,
|
|
140
|
+
middlewares = {},
|
|
141
|
+
assets: assetsConfig = {},
|
|
142
|
+
errorPages = {}
|
|
75
143
|
} = options;
|
|
76
144
|
|
|
145
|
+
// Configure asset manager
|
|
146
|
+
configureAssets({
|
|
147
|
+
publicDir: publicDir || 'public',
|
|
148
|
+
...assetsConfig
|
|
149
|
+
});
|
|
150
|
+
|
|
77
151
|
if (!pagesDir) {
|
|
78
152
|
throw new Error('pagesDir is required');
|
|
79
153
|
}
|
|
@@ -158,73 +232,73 @@ function createApp(options = {}) {
|
|
|
158
232
|
mountPages(app, {
|
|
159
233
|
pagesDir,
|
|
160
234
|
nunjucks: nunjucksEnv,
|
|
235
|
+
middlewares,
|
|
161
236
|
silent: isTest
|
|
162
237
|
});
|
|
163
238
|
|
|
164
239
|
// 404 handler
|
|
165
240
|
app.use((req, res) => {
|
|
166
241
|
res.status(404);
|
|
242
|
+
|
|
243
|
+
// Custom handler function
|
|
244
|
+
if (typeof errorPages.notFound === 'function') {
|
|
245
|
+
return errorPages.notFound(req, res);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Custom template
|
|
249
|
+
if (typeof errorPages.notFound === 'string') {
|
|
250
|
+
try {
|
|
251
|
+
const html = nunjucksEnv.render(errorPages.notFound, {
|
|
252
|
+
url: req.url,
|
|
253
|
+
method: req.method
|
|
254
|
+
});
|
|
255
|
+
return res.send(html);
|
|
256
|
+
} catch (e) {
|
|
257
|
+
console.error('Error rendering 404 template:', e);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Default response
|
|
167
262
|
if (req.accepts('html')) {
|
|
168
|
-
res.send(
|
|
169
|
-
<!DOCTYPE html>
|
|
170
|
-
<html>
|
|
171
|
-
<head>
|
|
172
|
-
<title>404 - Not Found</title>
|
|
173
|
-
<style>
|
|
174
|
-
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
|
|
175
|
-
.container { text-align: center; }
|
|
176
|
-
h1 { font-size: 4rem; margin: 0; color: #333; }
|
|
177
|
-
p { color: #666; margin: 1rem 0; }
|
|
178
|
-
a { color: #0066cc; text-decoration: none; }
|
|
179
|
-
a:hover { text-decoration: underline; }
|
|
180
|
-
</style>
|
|
181
|
-
</head>
|
|
182
|
-
<body>
|
|
183
|
-
<div class="container">
|
|
184
|
-
<h1>404</h1>
|
|
185
|
-
<p>Page not found</p>
|
|
186
|
-
<a href="/">← Back to Home</a>
|
|
187
|
-
</div>
|
|
188
|
-
</body>
|
|
189
|
-
</html>
|
|
190
|
-
`);
|
|
263
|
+
res.send(default404Html());
|
|
191
264
|
} else {
|
|
192
|
-
res.json({ error: 'Not Found' });
|
|
265
|
+
res.json({ error: 'Not Found', status: 404 });
|
|
193
266
|
}
|
|
194
267
|
});
|
|
195
268
|
|
|
196
269
|
// Error handler
|
|
197
270
|
app.use((err, req, res, next) => {
|
|
198
271
|
console.error('Server error:', err);
|
|
199
|
-
res.status(500);
|
|
272
|
+
res.status(err.status || 500);
|
|
273
|
+
|
|
274
|
+
// Custom handler function
|
|
275
|
+
if (typeof errorPages.serverError === 'function') {
|
|
276
|
+
return errorPages.serverError(err, req, res);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Custom template
|
|
280
|
+
if (typeof errorPages.serverError === 'string') {
|
|
281
|
+
try {
|
|
282
|
+
const html = nunjucksEnv.render(errorPages.serverError, {
|
|
283
|
+
error: isDev ? err : { message: 'Internal Server Error' },
|
|
284
|
+
status: err.status || 500,
|
|
285
|
+
isDev
|
|
286
|
+
});
|
|
287
|
+
return res.send(html);
|
|
288
|
+
} catch (e) {
|
|
289
|
+
console.error('Error rendering 500 template:', e);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Default response
|
|
200
294
|
if (req.accepts('html')) {
|
|
201
|
-
res.send(
|
|
202
|
-
<!DOCTYPE html>
|
|
203
|
-
<html>
|
|
204
|
-
<head>
|
|
205
|
-
<title>500 - Server Error</title>
|
|
206
|
-
<style>
|
|
207
|
-
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
|
|
208
|
-
.container { text-align: center; }
|
|
209
|
-
h1 { font-size: 4rem; margin: 0; color: #333; }
|
|
210
|
-
p { color: #666; margin: 1rem 0; }
|
|
211
|
-
pre { background: #fff; padding: 1rem; border-radius: 4px; text-align: left; overflow: auto; max-width: 600px; }
|
|
212
|
-
a { color: #0066cc; text-decoration: none; }
|
|
213
|
-
a:hover { text-decoration: underline; }
|
|
214
|
-
</style>
|
|
215
|
-
</head>
|
|
216
|
-
<body>
|
|
217
|
-
<div class="container">
|
|
218
|
-
<h1>500</h1>
|
|
219
|
-
<p>Internal Server Error</p>
|
|
220
|
-
${isDev ? `<pre>${err.stack || err.message}</pre>` : ''}
|
|
221
|
-
<a href="/">← Back to Home</a>
|
|
222
|
-
</div>
|
|
223
|
-
</body>
|
|
224
|
-
</html>
|
|
225
|
-
`);
|
|
295
|
+
res.send(default500Html(err, isDev));
|
|
226
296
|
} else {
|
|
227
|
-
res.json({
|
|
297
|
+
res.json({
|
|
298
|
+
error: 'Internal Server Error',
|
|
299
|
+
status: err.status || 500,
|
|
300
|
+
...(isDev && { message: err.message, stack: err.stack })
|
|
301
|
+
});
|
|
228
302
|
}
|
|
229
303
|
});
|
|
230
304
|
|