webspresso 0.0.2 → 0.0.4
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 +261 -5
- package/index.js +25 -2
- package/package.json +1 -1
- package/src/file-router.js +80 -6
- package/src/helpers.js +206 -1
- package/src/plugin-manager.js +451 -0
- package/src/server.js +163 -55
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ A minimal, file-based SSR framework for Node.js with Nunjucks templating.
|
|
|
10
10
|
- **Built-in i18n**: JSON-based translations with automatic locale detection
|
|
11
11
|
- **Lifecycle Hooks**: Global and route-level hooks for request processing
|
|
12
12
|
- **Template Helpers**: Laravel-inspired helper functions available in templates
|
|
13
|
+
- **Plugin System**: Extensible architecture with version control and inter-plugin communication
|
|
14
|
+
- **Built-in Plugins**: Sitemap generator, analytics integration (Google, Yandex, Bing)
|
|
13
15
|
|
|
14
16
|
## Installation
|
|
15
17
|
|
|
@@ -184,13 +186,267 @@ Creates and configures the Express app.
|
|
|
184
186
|
- `true` or `undefined`: Use default secure configuration
|
|
185
187
|
- `false`: Disable Helmet
|
|
186
188
|
- `Object`: Custom Helmet configuration (merged with defaults)
|
|
189
|
+
- `middlewares` (optional): Named middleware registry for routes
|
|
187
190
|
|
|
188
|
-
**
|
|
189
|
-
- CSP disabled in development, enabled in production
|
|
190
|
-
- HSTS, XSS protection, frame guard, and other security headers enabled
|
|
191
|
-
- Tailwind CSS inline styles allowed in CSP
|
|
191
|
+
**Example with middlewares:**
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
```javascript
|
|
194
|
+
const { createApp } = require('webspresso');
|
|
195
|
+
|
|
196
|
+
const { app } = createApp({
|
|
197
|
+
pagesDir: './pages',
|
|
198
|
+
viewsDir: './views',
|
|
199
|
+
middlewares: {
|
|
200
|
+
auth: (req, res, next) => {
|
|
201
|
+
if (!req.session?.user) {
|
|
202
|
+
return res.redirect('/login');
|
|
203
|
+
}
|
|
204
|
+
next();
|
|
205
|
+
},
|
|
206
|
+
admin: (req, res, next) => {
|
|
207
|
+
if (req.session?.user?.role !== 'admin') {
|
|
208
|
+
return res.status(403).send('Forbidden');
|
|
209
|
+
}
|
|
210
|
+
next();
|
|
211
|
+
},
|
|
212
|
+
rateLimit: require('express-rate-limit')({ windowMs: 60000, max: 100 })
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Then use in route configs by name:
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
// pages/admin/index.js
|
|
221
|
+
module.exports = {
|
|
222
|
+
middleware: ['auth', 'admin'], // Use named middlewares
|
|
223
|
+
load(req, ctx) { ... }
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// pages/api/data.get.js
|
|
227
|
+
module.exports = {
|
|
228
|
+
middleware: ['auth', 'rateLimit'],
|
|
229
|
+
handler: (req, res) => res.json({ data: 'protected' })
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Custom Error Pages:**
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
const { createApp } = require('webspresso');
|
|
237
|
+
|
|
238
|
+
const { app } = createApp({
|
|
239
|
+
pagesDir: './pages',
|
|
240
|
+
viewsDir: './views',
|
|
241
|
+
errorPages: {
|
|
242
|
+
// Option 1: Custom handler function
|
|
243
|
+
notFound: (req, res) => {
|
|
244
|
+
res.render('errors/404.njk', { url: req.url });
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
// Option 2: Template path (rendered with Nunjucks)
|
|
248
|
+
serverError: 'errors/500.njk'
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Error templates receive these variables:
|
|
254
|
+
- `404.njk`: `{ url, method }`
|
|
255
|
+
- `500.njk`: `{ error, status, isDev }`
|
|
256
|
+
|
|
257
|
+
**Asset Management:**
|
|
258
|
+
|
|
259
|
+
Configure asset handling with versioning and manifest support:
|
|
260
|
+
|
|
261
|
+
```javascript
|
|
262
|
+
const { createApp } = require('webspresso');
|
|
263
|
+
const path = require('path');
|
|
264
|
+
|
|
265
|
+
const { app } = createApp({
|
|
266
|
+
pagesDir: './pages',
|
|
267
|
+
viewsDir: './views',
|
|
268
|
+
publicDir: './public',
|
|
269
|
+
assets: {
|
|
270
|
+
// Option 1: Simple versioning (cache busting)
|
|
271
|
+
version: '1.2.3', // or process.env.APP_VERSION
|
|
272
|
+
|
|
273
|
+
// Option 2: Manifest file (Vite, Webpack, etc.)
|
|
274
|
+
manifestPath: path.join(__dirname, 'public/.vite/manifest.json'),
|
|
275
|
+
|
|
276
|
+
// URL prefix for assets
|
|
277
|
+
prefix: '/static'
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Use asset helpers in templates:
|
|
283
|
+
|
|
284
|
+
```njk
|
|
285
|
+
{# Using fsy helpers (auto-resolved) #}
|
|
286
|
+
<link rel="stylesheet" href="{{ fsy.asset('/css/style.css') }}">
|
|
287
|
+
|
|
288
|
+
{# Or generate full HTML tags #}
|
|
289
|
+
{{ fsy.css('/css/style.css') | safe }}
|
|
290
|
+
{{ fsy.js('/js/app.js', { defer: true, type: 'module' }) | safe }}
|
|
291
|
+
{{ fsy.img('/images/logo.png', 'Site Logo', { class: 'logo', loading: 'lazy' }) | safe }}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Asset helpers available in `fsy`:
|
|
295
|
+
- `asset(path)` - Returns versioned/manifest-resolved URL
|
|
296
|
+
- `css(href, attrs)` - Generates `<link>` tag
|
|
297
|
+
- `js(src, attrs)` - Generates `<script>` tag
|
|
298
|
+
- `img(src, alt, attrs)` - Generates `<img>` tag
|
|
299
|
+
|
|
300
|
+
**Manifest Support:**
|
|
301
|
+
|
|
302
|
+
Works with Vite and Webpack manifest formats:
|
|
303
|
+
|
|
304
|
+
```json
|
|
305
|
+
// Vite manifest format (.vite/manifest.json)
|
|
306
|
+
{
|
|
307
|
+
"css/style.css": { "file": "assets/style-abc123.css" },
|
|
308
|
+
"js/app.js": { "file": "assets/app-xyz789.js" }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Webpack manifest format
|
|
312
|
+
{
|
|
313
|
+
"/css/style.css": "/dist/style.abc123.css",
|
|
314
|
+
"/js/app.js": "/dist/app.xyz789.js"
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Returns:** `{ app, nunjucksEnv, pluginManager }`
|
|
319
|
+
|
|
320
|
+
## Plugin System
|
|
321
|
+
|
|
322
|
+
Webspresso has a built-in plugin system with version control and dependency management.
|
|
323
|
+
|
|
324
|
+
### Using Plugins
|
|
325
|
+
|
|
326
|
+
```javascript
|
|
327
|
+
const { createApp } = require('webspresso');
|
|
328
|
+
const { sitemapPlugin, analyticsPlugin } = require('webspresso/plugins');
|
|
329
|
+
|
|
330
|
+
const { app } = createApp({
|
|
331
|
+
pagesDir: './pages',
|
|
332
|
+
viewsDir: './views',
|
|
333
|
+
plugins: [
|
|
334
|
+
sitemapPlugin({
|
|
335
|
+
hostname: 'https://example.com',
|
|
336
|
+
exclude: ['/admin/*', '/api/*'],
|
|
337
|
+
i18n: true,
|
|
338
|
+
locales: ['en', 'tr']
|
|
339
|
+
}),
|
|
340
|
+
analyticsPlugin({
|
|
341
|
+
google: {
|
|
342
|
+
measurementId: 'G-XXXXXXXXXX',
|
|
343
|
+
verificationCode: 'xxxxx'
|
|
344
|
+
},
|
|
345
|
+
yandex: {
|
|
346
|
+
counterId: '12345678',
|
|
347
|
+
verificationCode: 'xxxxx'
|
|
348
|
+
},
|
|
349
|
+
bing: {
|
|
350
|
+
uetId: '12345678',
|
|
351
|
+
verificationCode: 'xxxxx'
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
]
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Built-in Plugins
|
|
359
|
+
|
|
360
|
+
**Sitemap Plugin:**
|
|
361
|
+
- Generates `/sitemap.xml` from routes automatically
|
|
362
|
+
- Excludes dynamic routes and API endpoints
|
|
363
|
+
- Supports i18n with hreflang tags
|
|
364
|
+
- Generates `/robots.txt`
|
|
365
|
+
|
|
366
|
+
**Analytics Plugin:**
|
|
367
|
+
- Google Analytics (GA4) and Google Ads
|
|
368
|
+
- Google Tag Manager
|
|
369
|
+
- Yandex.Metrika
|
|
370
|
+
- Microsoft/Bing UET
|
|
371
|
+
- Facebook Pixel
|
|
372
|
+
- Verification meta tags for all services
|
|
373
|
+
|
|
374
|
+
Template helpers from analytics plugin:
|
|
375
|
+
|
|
376
|
+
```njk
|
|
377
|
+
<head>
|
|
378
|
+
{{ fsy.verificationTags() | safe }}
|
|
379
|
+
{{ fsy.analyticsHead() | safe }}
|
|
380
|
+
</head>
|
|
381
|
+
<body>
|
|
382
|
+
{{ fsy.analyticsBodyOpen() | safe }}
|
|
383
|
+
...
|
|
384
|
+
</body>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Individual helpers: `gtag()`, `gtm()`, `gtmNoscript()`, `yandexMetrika()`, `bingUET()`, `facebookPixel()`, `allAnalytics()`
|
|
388
|
+
|
|
389
|
+
### Creating Custom Plugins
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
const myPlugin = {
|
|
393
|
+
name: 'my-plugin',
|
|
394
|
+
version: '1.0.0',
|
|
395
|
+
|
|
396
|
+
// Optional: depend on other plugins
|
|
397
|
+
dependencies: {
|
|
398
|
+
'analytics': '^1.0.0'
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
// Optional: expose API for other plugins
|
|
402
|
+
api: {
|
|
403
|
+
getData() { return this.data; }
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
// Called during registration
|
|
407
|
+
register(ctx) {
|
|
408
|
+
// Access Express app
|
|
409
|
+
ctx.app.use((req, res, next) => next());
|
|
410
|
+
|
|
411
|
+
// Add template helpers
|
|
412
|
+
ctx.addHelper('myHelper', () => 'Hello!');
|
|
413
|
+
|
|
414
|
+
// Add Nunjucks filters
|
|
415
|
+
ctx.addFilter('myFilter', (val) => val.toUpperCase());
|
|
416
|
+
|
|
417
|
+
// Use other plugins
|
|
418
|
+
const analytics = ctx.usePlugin('analytics');
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
// Called after all routes are mounted
|
|
422
|
+
onRoutesReady(ctx) {
|
|
423
|
+
// Access route metadata
|
|
424
|
+
console.log('Routes:', ctx.routes);
|
|
425
|
+
|
|
426
|
+
// Add custom routes
|
|
427
|
+
ctx.addRoute('get', '/my-route', (req, res) => {
|
|
428
|
+
res.json({ hello: 'world' });
|
|
429
|
+
});
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
// Called before server starts
|
|
433
|
+
onReady(ctx) {
|
|
434
|
+
console.log('Server ready!');
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// Use as factory function for configuration
|
|
439
|
+
function myPluginFactory(options = {}) {
|
|
440
|
+
return {
|
|
441
|
+
name: 'my-plugin',
|
|
442
|
+
version: '1.0.0',
|
|
443
|
+
_options: options,
|
|
444
|
+
register(ctx) {
|
|
445
|
+
// ctx.options contains the passed options
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
```
|
|
194
450
|
|
|
195
451
|
## File-Based Routing
|
|
196
452
|
|
package/index.js
CHANGED
|
@@ -12,7 +12,19 @@ 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');
|
|
22
|
+
const {
|
|
23
|
+
PluginManager,
|
|
24
|
+
createPluginManager,
|
|
25
|
+
getPluginManager,
|
|
26
|
+
resetPluginManager
|
|
27
|
+
} = require('./src/plugin-manager');
|
|
16
28
|
|
|
17
29
|
module.exports = {
|
|
18
30
|
// Main API
|
|
@@ -29,6 +41,17 @@ module.exports = {
|
|
|
29
41
|
|
|
30
42
|
// Template helpers
|
|
31
43
|
createHelpers,
|
|
32
|
-
utils
|
|
44
|
+
utils,
|
|
45
|
+
|
|
46
|
+
// Asset management
|
|
47
|
+
AssetManager,
|
|
48
|
+
configureAssets,
|
|
49
|
+
getAssetManager,
|
|
50
|
+
|
|
51
|
+
// Plugin system
|
|
52
|
+
PluginManager,
|
|
53
|
+
createPluginManager,
|
|
54
|
+
getPluginManager,
|
|
55
|
+
resetPluginManager
|
|
33
56
|
};
|
|
34
57
|
|
package/package.json
CHANGED
package/src/file-router.js
CHANGED
|
@@ -272,16 +272,50 @@ 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
|
|
313
|
+
* @param {Object} options.pluginManager - Plugin manager instance
|
|
281
314
|
* @param {boolean} options.silent - Suppress console output
|
|
315
|
+
* @returns {Array} Route metadata for plugins
|
|
282
316
|
*/
|
|
283
317
|
function mountPages(app, options) {
|
|
284
|
-
const { pagesDir, nunjucks, silent = false } = options;
|
|
318
|
+
const { pagesDir, nunjucks, middlewares = {}, pluginManager = null, silent = false } = options;
|
|
285
319
|
const isDev = process.env.NODE_ENV !== 'production';
|
|
286
320
|
const log = silent ? () => {} : console.log.bind(console);
|
|
287
321
|
|
|
@@ -353,6 +387,7 @@ function mountPages(app, options) {
|
|
|
353
387
|
for (const route of sortRoutes(apiRoutes)) {
|
|
354
388
|
const handler = require(route.fullPath);
|
|
355
389
|
const handlerFn = typeof handler === 'function' ? handler : handler.default || handler.handler;
|
|
390
|
+
const routeMiddleware = handler.middleware;
|
|
356
391
|
|
|
357
392
|
if (typeof handlerFn !== 'function') {
|
|
358
393
|
console.warn(`API route ${route.file} does not export a function`);
|
|
@@ -372,6 +407,20 @@ function mountPages(app, options) {
|
|
|
372
407
|
? currentHandler
|
|
373
408
|
: currentHandler.default || currentHandler.handler;
|
|
374
409
|
|
|
410
|
+
// Run middleware if defined
|
|
411
|
+
const mwConfig = isDev ? currentHandler.middleware : routeMiddleware;
|
|
412
|
+
if (mwConfig) {
|
|
413
|
+
const resolvedMw = resolveMiddlewares(mwConfig, middlewares);
|
|
414
|
+
for (const mw of resolvedMw) {
|
|
415
|
+
await new Promise((resolve, reject) => {
|
|
416
|
+
mw(req, res, (err) => {
|
|
417
|
+
if (err) reject(err);
|
|
418
|
+
else resolve();
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
375
424
|
await fn(req, res, next);
|
|
376
425
|
} catch (err) {
|
|
377
426
|
console.error(`API error ${route.routePath}:`, err);
|
|
@@ -397,7 +446,10 @@ function mountPages(app, options) {
|
|
|
397
446
|
const config = loadRouteConfig(route.configPath, isDev);
|
|
398
447
|
const routeHooks = config?.hooks || {};
|
|
399
448
|
|
|
400
|
-
// Create context
|
|
449
|
+
// Create context with plugin helpers merged
|
|
450
|
+
const baseHelpers = createHelpers({ req, res, locale });
|
|
451
|
+
const pluginHelpers = pluginManager ? pluginManager.getHelpers() : {};
|
|
452
|
+
|
|
401
453
|
const ctx = {
|
|
402
454
|
req,
|
|
403
455
|
res,
|
|
@@ -413,7 +465,7 @@ function mountPages(app, options) {
|
|
|
413
465
|
indexable: true,
|
|
414
466
|
canonical: null
|
|
415
467
|
},
|
|
416
|
-
fsy:
|
|
468
|
+
fsy: { ...baseHelpers, ...pluginHelpers }
|
|
417
469
|
};
|
|
418
470
|
|
|
419
471
|
// Execute hooks: onRequest
|
|
@@ -429,8 +481,9 @@ function mountPages(app, options) {
|
|
|
429
481
|
await executeHook(routeHooks, 'beforeMiddleware', ctx);
|
|
430
482
|
|
|
431
483
|
// Run route middleware
|
|
432
|
-
if (config?.middleware
|
|
433
|
-
|
|
484
|
+
if (config?.middleware) {
|
|
485
|
+
const resolvedMiddlewares = resolveMiddlewares(config.middleware, middlewares);
|
|
486
|
+
for (const mw of resolvedMiddlewares) {
|
|
434
487
|
await new Promise((resolve, reject) => {
|
|
435
488
|
mw(req, res, (err) => {
|
|
436
489
|
if (err) reject(err);
|
|
@@ -506,6 +559,26 @@ function mountPages(app, options) {
|
|
|
506
559
|
|
|
507
560
|
log(` GET ${route.routePath} -> ${route.file}`);
|
|
508
561
|
}
|
|
562
|
+
|
|
563
|
+
// Return route metadata for plugins
|
|
564
|
+
const routeMetadata = [
|
|
565
|
+
...ssrRoutes.map(r => ({
|
|
566
|
+
type: 'ssr',
|
|
567
|
+
method: 'get',
|
|
568
|
+
pattern: r.routePath,
|
|
569
|
+
file: r.file,
|
|
570
|
+
isDynamic: r.routePath.includes(':') || r.routePath.includes('*')
|
|
571
|
+
})),
|
|
572
|
+
...apiRoutes.map(r => ({
|
|
573
|
+
type: 'api',
|
|
574
|
+
method: r.method,
|
|
575
|
+
pattern: r.routePath,
|
|
576
|
+
file: r.file,
|
|
577
|
+
isDynamic: r.routePath.includes(':') || r.routePath.includes('*')
|
|
578
|
+
}))
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
return routeMetadata;
|
|
509
582
|
}
|
|
510
583
|
|
|
511
584
|
module.exports = {
|
|
@@ -515,6 +588,7 @@ module.exports = {
|
|
|
515
588
|
scanDirectory,
|
|
516
589
|
loadI18n,
|
|
517
590
|
createTranslator,
|
|
518
|
-
detectLocale
|
|
591
|
+
detectLocale,
|
|
592
|
+
resolveMiddlewares
|
|
519
593
|
};
|
|
520
594
|
|
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
|
|