webspresso 0.0.90 → 0.0.91
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 +358 -8
- package/bin/commands/service.js +193 -0
- package/bin/webspresso.js +2 -0
- package/core/auth/middleware.js +9 -0
- package/core/compression/compressible.js +51 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +301 -0
- package/core/compression/negotiate.js +153 -0
- package/core/errors/base.js +31 -0
- package/core/errors/domain.js +193 -0
- package/core/errors/http.js +133 -0
- package/core/errors/index.js +77 -0
- package/core/errors/middleware.js +162 -0
- package/core/errors/normalize.js +171 -0
- package/core/orm/complexity.js +120 -0
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +45 -10
- package/core/orm/model.js +2 -0
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/transaction.js +39 -5
- package/core/orm/utils.js +6 -3
- package/core/queue/adapters/database.js +284 -0
- package/core/queue/adapters/memory.js +155 -0
- package/core/queue/adapters/redis.js +336 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +387 -0
- package/core/realtime/adapters/redis.js +431 -0
- package/core/realtime/adapters/socket-io.js +203 -0
- package/core/realtime/adapters/sse.js +210 -0
- package/core/realtime/adapters/websocket.js +337 -0
- package/core/realtime/auth-manager.js +140 -0
- package/core/realtime/core.js +359 -0
- package/core/realtime/emitter.js +98 -0
- package/core/realtime/errors.js +71 -0
- package/core/realtime/index.js +79 -0
- package/core/realtime/reconnect-manager.js +118 -0
- package/core/realtime/registry.js +243 -0
- package/core/shutdown/http-adapter.js +134 -0
- package/core/shutdown/index.js +12 -0
- package/core/shutdown/shutdown-manager.js +312 -0
- package/core/ssr/index.js +13 -0
- package/core/ssr/stream.js +174 -0
- package/index.d.ts +829 -52
- package/index.js +103 -0
- package/package.json +22 -2
- package/plugins/admin-panel/api.js +37 -4
- package/plugins/admin-panel/app.js +57 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
- package/plugins/admin-panel/client/parts/08-record-list.js +17 -24
- package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
- package/plugins/admin-panel/core/admin-module.js +2 -2
- package/plugins/admin-panel/core/api-extensions.js +149 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
- package/plugins/admin-panel/index.js +90 -17
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- package/plugins/admin-panel/modules/menu.js +85 -0
- package/plugins/admin-panel/modules/user-management.js +5 -2
- package/plugins/admin-panel/vendor/mithril.min.js +1 -0
- package/plugins/admin-panel/vendor/quill.min.js +7 -0
- package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
- package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
- package/plugins/basic-auth/index.js +293 -0
- package/plugins/data-exchange/export-xlsx.js +5 -1
- package/plugins/data-exchange/import.js +16 -2
- package/plugins/data-exchange/index.js +14 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +29 -0
- package/plugins/queue/index.js +108 -0
- package/plugins/realtime/index.js +150 -0
- package/plugins/recaptcha/index.js +11 -0
- package/plugins/redirect/index.js +9 -1
- package/plugins/rest-resources/index.js +280 -11
- package/plugins/upload/index.js +16 -0
- package/src/app-context.js +46 -6
- package/src/file-router.js +196 -21
- package/src/helpers.js +20 -2
- package/src/plugin-manager.js +124 -4
- package/src/server.js +304 -44
- package/src/services/builtins/auth.js +273 -0
- package/src/services/builtins/exchange.js +229 -0
- package/src/services/builtins/mail.js +134 -0
- package/src/services/builtins/media.js +196 -0
- package/src/services/builtins/system.js +221 -0
- package/src/services/discovery.js +140 -0
- package/src/services/executor.js +292 -0
- package/src/services/index.js +70 -0
- package/src/services/memoize.js +256 -0
- package/src/services/registry.js +288 -0
- package/src/services/validator.js +281 -0
package/src/file-router.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
frontmatterToPatches,
|
|
16
16
|
clearNjkFrontmatterCaches,
|
|
17
17
|
} = require('./njk-frontmatter');
|
|
18
|
+
const { renderStream } = require('../core/ssr/stream');
|
|
18
19
|
|
|
19
20
|
const NOOP = () => {};
|
|
20
21
|
const EMPTY_OBJECT = Object.freeze({});
|
|
@@ -432,33 +433,103 @@ function loadI18n(pagesDir, routeDir, locale) {
|
|
|
432
433
|
return data;
|
|
433
434
|
}
|
|
434
435
|
|
|
435
|
-
|
|
436
|
-
|
|
436
|
+
// Cache for Intl.PluralRules instances
|
|
437
|
+
const pluralRulesCache = new Map();
|
|
438
|
+
|
|
439
|
+
function getPluralRules(locale) {
|
|
440
|
+
let pr = pluralRulesCache.get(locale);
|
|
441
|
+
if (!pr) {
|
|
442
|
+
try {
|
|
443
|
+
pr = new Intl.PluralRules(locale);
|
|
444
|
+
} catch {
|
|
445
|
+
pr = new Intl.PluralRules('en');
|
|
446
|
+
}
|
|
447
|
+
if (pluralRulesCache.size < 100) {
|
|
448
|
+
pluralRulesCache.set(locale, pr);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return pr;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function resolveTranslationValue(dict, key) {
|
|
455
|
+
if (!dict || typeof dict !== 'object') return undefined;
|
|
456
|
+
if (dict[key] !== undefined) return dict[key];
|
|
457
|
+
|
|
458
|
+
const parts = key.split('.');
|
|
459
|
+
let curr = dict;
|
|
460
|
+
for (const part of parts) {
|
|
461
|
+
if (curr && typeof curr === 'object') {
|
|
462
|
+
curr = curr[part];
|
|
463
|
+
} else {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return curr;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Create a translation helper function with pluralization, fallback, and formatting support
|
|
472
|
+
* @param {Object} translations - Primary locale translations dictionary
|
|
473
|
+
* @param {Object|string} [options] - Options object or locale string
|
|
474
|
+
* @param {string} [options.locale='en'] - Active locale code
|
|
475
|
+
* @param {Object} [options.fallbackTranslations] - Fallback translations dictionary
|
|
476
|
+
* @param {string} [options.fallbackLocale='en'] - Fallback locale code
|
|
477
|
+
* @returns {Function} Translation function `t`
|
|
478
|
+
*/
|
|
479
|
+
function createTranslator(translations = {}, options = EMPTY_OBJECT) {
|
|
480
|
+
const opts = typeof options === 'string' ? { locale: options } : (options || EMPTY_OBJECT);
|
|
481
|
+
const locale = opts.locale || 'en';
|
|
482
|
+
const fallbackTranslations = opts.fallbackTranslations || null;
|
|
483
|
+
|
|
484
|
+
function t(key, params = EMPTY_OBJECT, defaultVal = null) {
|
|
437
485
|
const defaultValue = typeof params === 'string' ? params : defaultVal;
|
|
438
486
|
const interpolationParams = typeof params === 'object' && params !== null ? params : EMPTY_OBJECT;
|
|
439
487
|
|
|
440
|
-
let value = translations
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
488
|
+
let value = resolveTranslationValue(translations, key);
|
|
489
|
+
|
|
490
|
+
// If missing in primary locale, fallback to fallback translations
|
|
491
|
+
if (value === undefined && fallbackTranslations) {
|
|
492
|
+
value = resolveTranslationValue(fallbackTranslations, key);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Handle Pluralization if value is an object (or when count is passed)
|
|
496
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
497
|
+
const count = interpolationParams.count !== undefined
|
|
498
|
+
? Number(interpolationParams.count)
|
|
499
|
+
: (interpolationParams.n !== undefined
|
|
500
|
+
? Number(interpolationParams.n)
|
|
501
|
+
: (interpolationParams.cardinal !== undefined ? Number(interpolationParams.cardinal) : undefined));
|
|
502
|
+
|
|
503
|
+
if (count !== undefined && !Number.isNaN(count)) {
|
|
504
|
+
const countStr = String(count);
|
|
505
|
+
if (typeof value[countStr] === 'string') {
|
|
506
|
+
value = value[countStr];
|
|
507
|
+
} else if (count === 0 && typeof value.zero === 'string') {
|
|
508
|
+
value = value.zero;
|
|
449
509
|
} else {
|
|
450
|
-
|
|
451
|
-
|
|
510
|
+
const category = getPluralRules(locale).select(count);
|
|
511
|
+
if (typeof value[category] === 'string') {
|
|
512
|
+
value = value[category];
|
|
513
|
+
} else if (typeof value.other === 'string') {
|
|
514
|
+
value = value.other;
|
|
515
|
+
} else {
|
|
516
|
+
const firstString = Object.values(value).find((v) => typeof v === 'string');
|
|
517
|
+
value = firstString !== undefined ? firstString : key;
|
|
518
|
+
}
|
|
452
519
|
}
|
|
453
520
|
}
|
|
454
521
|
}
|
|
455
|
-
|
|
522
|
+
|
|
456
523
|
if (value === undefined) {
|
|
457
524
|
return defaultValue !== null && defaultValue !== undefined ? defaultValue : key;
|
|
458
525
|
}
|
|
459
|
-
|
|
526
|
+
|
|
527
|
+
if (typeof value !== 'string') {
|
|
528
|
+
return value;
|
|
529
|
+
}
|
|
530
|
+
|
|
460
531
|
// Replace params like {{name}} in the translation
|
|
461
|
-
if (
|
|
532
|
+
if (interpolationParams && interpolationParams !== EMPTY_OBJECT && Object.keys(interpolationParams).length > 0) {
|
|
462
533
|
for (const [paramKey, paramValue] of Object.entries(interpolationParams)) {
|
|
463
534
|
let regex = paramRegexCache.get(paramKey);
|
|
464
535
|
if (regex === undefined) {
|
|
@@ -468,12 +539,84 @@ function createTranslator(translations = {}) {
|
|
|
468
539
|
paramRegexCache.set(paramKey, regex);
|
|
469
540
|
}
|
|
470
541
|
}
|
|
471
|
-
value = value.replace(regex, paramValue);
|
|
542
|
+
value = value.replace(regex, () => String(paramValue));
|
|
472
543
|
}
|
|
473
544
|
}
|
|
474
|
-
|
|
545
|
+
|
|
475
546
|
return value;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Attach metadata & native formatting helpers to `t`
|
|
550
|
+
t.locale = locale;
|
|
551
|
+
t.translations = translations;
|
|
552
|
+
|
|
553
|
+
t.number = function formatNumber(num, numOpts) {
|
|
554
|
+
if (num === null || num === undefined || Number.isNaN(Number(num))) return '';
|
|
555
|
+
try {
|
|
556
|
+
return new Intl.NumberFormat(locale, numOpts).format(Number(num));
|
|
557
|
+
} catch {
|
|
558
|
+
return String(num);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
t.formatNumber = t.number;
|
|
562
|
+
|
|
563
|
+
t.currency = function formatCurrency(amount, currency = 'USD', currOpts = {}) {
|
|
564
|
+
if (amount === null || amount === undefined || Number.isNaN(Number(amount))) return '';
|
|
565
|
+
try {
|
|
566
|
+
return new Intl.NumberFormat(locale, { style: 'currency', currency, ...currOpts }).format(Number(amount));
|
|
567
|
+
} catch {
|
|
568
|
+
return `${amount} ${currency}`;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
t.formatCurrency = t.currency;
|
|
572
|
+
|
|
573
|
+
t.date = function formatDate(date, dateOpts = { dateStyle: 'medium' }) {
|
|
574
|
+
if (!date) return '';
|
|
575
|
+
try {
|
|
576
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
577
|
+
if (Number.isNaN(d.getTime())) return '';
|
|
578
|
+
return new Intl.DateTimeFormat(locale, dateOpts).format(d);
|
|
579
|
+
} catch {
|
|
580
|
+
return String(date);
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
t.formatDate = t.date;
|
|
584
|
+
|
|
585
|
+
t.relativeTime = function formatRelativeTime(val, unit = 'day', relOpts = { numeric: 'auto' }) {
|
|
586
|
+
if (val === null || val === undefined || Number.isNaN(Number(val))) return '';
|
|
587
|
+
try {
|
|
588
|
+
return new Intl.RelativeTimeFormat(locale, relOpts).format(Number(val), unit);
|
|
589
|
+
} catch {
|
|
590
|
+
return `${val} ${unit}`;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
t.formatRelativeTime = t.relativeTime;
|
|
594
|
+
|
|
595
|
+
t.plural = function inlinePlural(count, forms, params = {}) {
|
|
596
|
+
if (!forms || typeof forms !== 'object') return '';
|
|
597
|
+
const num = Number(count);
|
|
598
|
+
const countStr = String(num);
|
|
599
|
+
let template = forms[countStr];
|
|
600
|
+
if (typeof template !== 'string') {
|
|
601
|
+
if (num === 0 && typeof forms.zero === 'string') {
|
|
602
|
+
template = forms.zero;
|
|
603
|
+
} else {
|
|
604
|
+
const category = getPluralRules(locale).select(num);
|
|
605
|
+
template = forms[category] || forms.other || Object.values(forms)[0] || '';
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const allParams = { count: num, ...params };
|
|
609
|
+
return template.replace(/{{\s*(\w+)\s*}}/g, (_, k) => String(allParams[k] ?? ''));
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
t.has = function hasKey(key) {
|
|
613
|
+
if (resolveTranslationValue(translations, key) !== undefined) return true;
|
|
614
|
+
if (fallbackTranslations && resolveTranslationValue(fallbackTranslations, key) !== undefined) return true;
|
|
615
|
+
return false;
|
|
476
616
|
};
|
|
617
|
+
t.exists = t.has;
|
|
618
|
+
|
|
619
|
+
return t;
|
|
477
620
|
}
|
|
478
621
|
|
|
479
622
|
/**
|
|
@@ -677,6 +820,7 @@ function mountPages(app, options) {
|
|
|
677
820
|
db = null,
|
|
678
821
|
clientRuntime: clientRuntimeOpt = null,
|
|
679
822
|
pageAssets: pageAssetsOpt = null,
|
|
823
|
+
serviceRegistry = null,
|
|
680
824
|
} = options;
|
|
681
825
|
const pageAssetsResolved = resolvePageAssets(pageAssetsOpt);
|
|
682
826
|
const clientRuntime = clientRuntimeOpt && typeof clientRuntimeOpt === 'object'
|
|
@@ -851,6 +995,10 @@ function mountPages(app, options) {
|
|
|
851
995
|
if (db != null) {
|
|
852
996
|
req.db = db;
|
|
853
997
|
}
|
|
998
|
+
if (serviceRegistry != null) {
|
|
999
|
+
req.service = (name, input, opts) =>
|
|
1000
|
+
serviceRegistry.call(name, input, { req, res, db }, opts);
|
|
1001
|
+
}
|
|
854
1002
|
|
|
855
1003
|
// Reload handler in dev mode
|
|
856
1004
|
if (isDev && require.cache[require.resolve(route.fullPath)]) {
|
|
@@ -928,17 +1076,26 @@ function mountPages(app, options) {
|
|
|
928
1076
|
try {
|
|
929
1077
|
// Detect locale
|
|
930
1078
|
const locale = detectLocale(req);
|
|
1079
|
+
const defaultLocale = process.env.DEFAULT_LOCALE || 'en';
|
|
931
1080
|
|
|
932
|
-
// Load translations
|
|
1081
|
+
// Load translations (primary + fallback)
|
|
933
1082
|
const translations = loadI18n(absolutePagesDir, route.routeDir, locale);
|
|
934
|
-
const
|
|
1083
|
+
const fallbackTranslations = (locale !== defaultLocale)
|
|
1084
|
+
? loadI18n(absolutePagesDir, route.routeDir, defaultLocale)
|
|
1085
|
+
: EMPTY_OBJECT;
|
|
1086
|
+
|
|
1087
|
+
const t = createTranslator(translations, {
|
|
1088
|
+
locale,
|
|
1089
|
+
fallbackTranslations,
|
|
1090
|
+
fallbackLocale: defaultLocale,
|
|
1091
|
+
});
|
|
935
1092
|
|
|
936
1093
|
// Load route config
|
|
937
1094
|
const config = loadRouteConfig(route.configPath, isDev);
|
|
938
1095
|
const routeHooks = config?.hooks || EMPTY_OBJECT;
|
|
939
1096
|
|
|
940
1097
|
// Create context with plugin helpers merged
|
|
941
|
-
const baseHelpers = createHelpers({ req, res, locale });
|
|
1098
|
+
const baseHelpers = createHelpers({ req, res, locale, t });
|
|
942
1099
|
const pluginHelpers = pluginManager ? pluginManager.getHelpers() : EMPTY_OBJECT;
|
|
943
1100
|
if (pluginManager) {
|
|
944
1101
|
const contentApi = pluginManager.getPluginAPI('content');
|
|
@@ -959,6 +1116,12 @@ function mountPages(app, options) {
|
|
|
959
1116
|
routeDir: route.routeDir,
|
|
960
1117
|
locale,
|
|
961
1118
|
t,
|
|
1119
|
+
service: (name, input, opts) => {
|
|
1120
|
+
if (serviceRegistry) {
|
|
1121
|
+
return serviceRegistry.call(name, input, ctx, opts);
|
|
1122
|
+
}
|
|
1123
|
+
throw new Error(`Services not configured. Cannot call service '${name}'.`);
|
|
1124
|
+
},
|
|
962
1125
|
data: { ...njkTpl.dataPatch },
|
|
963
1126
|
meta: {
|
|
964
1127
|
title: t('meta.title') !== 'meta.title' ? t('meta.title') : null,
|
|
@@ -1055,6 +1218,18 @@ function mountPages(app, options) {
|
|
|
1055
1218
|
|
|
1056
1219
|
// Render the template
|
|
1057
1220
|
const templatePath = route.file.split(path.sep).join('/');
|
|
1221
|
+
|
|
1222
|
+
// SSR Streaming mode (chunked transfer)
|
|
1223
|
+
if (config?.stream || ctx.data?.__stream || ctx.defer) {
|
|
1224
|
+
const streamDefer = ctx.defer || ctx.data?.__defer || {};
|
|
1225
|
+
const status = (route.routePath === '/404' || route.file === '404.njk') ? 404 : 200;
|
|
1226
|
+
return await renderStream(res, templatePath, renderContext, {
|
|
1227
|
+
env: nunjucks,
|
|
1228
|
+
defer: streamDefer,
|
|
1229
|
+
status,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1058
1233
|
let html =
|
|
1059
1234
|
njkTpl.useStringRender && njkTpl.templateBody != null
|
|
1060
1235
|
? nunjucks.renderString(njkTpl.templateBody, renderContext, { path: route.fullPath })
|
package/src/helpers.js
CHANGED
|
@@ -140,7 +140,7 @@ class AssetManager {
|
|
|
140
140
|
*/
|
|
141
141
|
buildAttributes(attrs) {
|
|
142
142
|
return Object.entries(attrs)
|
|
143
|
-
.filter(([
|
|
143
|
+
.filter(([k, v]) => v !== undefined && v !== null && v !== false && /^[a-zA-Z0-9_\-:]+$/.test(k))
|
|
144
144
|
.map(([k, v]) => v === true ? k : `${k}="${this.escapeHtml(String(v))}"`)
|
|
145
145
|
.join(' ');
|
|
146
146
|
}
|
|
@@ -186,9 +186,27 @@ function getAssetManager() {
|
|
|
186
186
|
* @returns {Object} fsy helper object
|
|
187
187
|
*/
|
|
188
188
|
function createHelpers(ctx) {
|
|
189
|
-
const { req, res, baseUrl = process.env.BASE_URL || 'http://localhost:3000' } = ctx;
|
|
189
|
+
const { req, res, baseUrl = process.env.BASE_URL || 'http://localhost:3000', locale = 'en', t = null } = ctx || {};
|
|
190
190
|
|
|
191
191
|
return {
|
|
192
|
+
/** Active request locale */
|
|
193
|
+
locale,
|
|
194
|
+
|
|
195
|
+
/** Active i18n translator function */
|
|
196
|
+
t,
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Generate URL for switching to a target locale with current query parameters preserved
|
|
200
|
+
* @param {string} targetLocale - Locale code to switch to (e.g. 'en', 'tr')
|
|
201
|
+
* @param {string} [customPath] - Optional custom path (defaults to current req.path)
|
|
202
|
+
* @returns {string} URL string with updated locale query param
|
|
203
|
+
*/
|
|
204
|
+
localeUrl(targetLocale, customPath) {
|
|
205
|
+
const p = customPath || (req && req.path ? req.path : '/');
|
|
206
|
+
const q = { ...(req && req.query ? req.query : {}), lang: targetLocale };
|
|
207
|
+
return this.url(p, q);
|
|
208
|
+
},
|
|
209
|
+
|
|
192
210
|
/**
|
|
193
211
|
* Build a URL path with optional query parameters
|
|
194
212
|
* @param {string} path - URL path
|
package/src/plugin-manager.js
CHANGED
|
@@ -123,20 +123,37 @@ class PluginManager {
|
|
|
123
123
|
this.routes = []; // Collected route metadata
|
|
124
124
|
this.customRoutes = []; // Routes added by plugins
|
|
125
125
|
this.cspDirectives = new Map(); // directive -> Set of sources (from plugins)
|
|
126
|
+
this.disposers = []; // Array<{ name: string, fn: Function }>
|
|
126
127
|
this.app = null;
|
|
127
128
|
this.nunjucksEnv = null;
|
|
129
|
+
this.shutdownManager = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Register a plugin disposer
|
|
134
|
+
* @param {string} name
|
|
135
|
+
* @param {Function} fn
|
|
136
|
+
*/
|
|
137
|
+
registerDisposer(name, fn) {
|
|
138
|
+
if (typeof fn === 'function') {
|
|
139
|
+
this.disposers.push({ name: name || 'plugin', fn });
|
|
140
|
+
if (this.shutdownManager && typeof this.shutdownManager.registerDisposer === 'function') {
|
|
141
|
+
this.shutdownManager.registerDisposer(name, fn);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
128
144
|
}
|
|
129
145
|
|
|
130
146
|
/**
|
|
131
147
|
* Register plugins with the manager (async version)
|
|
132
148
|
* @param {Array} plugins - Array of plugin definitions or factory functions
|
|
133
|
-
* @param {Object} context - Context object `{ app, nunjucksEnv, options, middlewares? }`
|
|
149
|
+
* @param {Object} context - Context object `{ app, nunjucksEnv, options, middlewares?, shutdownManager? }`
|
|
134
150
|
*/
|
|
135
151
|
async register(plugins, context) {
|
|
136
152
|
if (!plugins || !Array.isArray(plugins)) return;
|
|
137
153
|
|
|
138
154
|
this.app = context.app;
|
|
139
155
|
this.nunjucksEnv = context.nunjucksEnv;
|
|
156
|
+
this.shutdownManager = context.shutdownManager || null;
|
|
140
157
|
|
|
141
158
|
// Normalize plugins (handle factory functions)
|
|
142
159
|
const normalizedPlugins = plugins.map(p => {
|
|
@@ -159,13 +176,14 @@ class PluginManager {
|
|
|
159
176
|
/**
|
|
160
177
|
* Register plugins with the manager (sync version)
|
|
161
178
|
* @param {Array} plugins - Array of plugin definitions or factory functions
|
|
162
|
-
* @param {Object} context - Context object `{ app, nunjucksEnv, options, middlewares? }`
|
|
179
|
+
* @param {Object} context - Context object `{ app, nunjucksEnv, options, middlewares?, shutdownManager? }`
|
|
163
180
|
*/
|
|
164
181
|
registerSync(plugins, context) {
|
|
165
182
|
if (!plugins || !Array.isArray(plugins)) return;
|
|
166
183
|
|
|
167
184
|
this.app = context.app;
|
|
168
185
|
this.nunjucksEnv = context.nunjucksEnv;
|
|
186
|
+
this.shutdownManager = context.shutdownManager || null;
|
|
169
187
|
|
|
170
188
|
// Normalize plugins (handle factory functions)
|
|
171
189
|
const normalizedPlugins = plugins.map(p => {
|
|
@@ -276,10 +294,32 @@ class PluginManager {
|
|
|
276
294
|
this._collectCspDirectives(plugin.csp);
|
|
277
295
|
}
|
|
278
296
|
|
|
297
|
+
let hasDisposer = false;
|
|
298
|
+
|
|
299
|
+
// Call setup hook if provided
|
|
300
|
+
if (typeof plugin.setup === 'function') {
|
|
301
|
+
try {
|
|
302
|
+
const result = await plugin.setup(ctx.app || ctx);
|
|
303
|
+
if (typeof result === 'function') {
|
|
304
|
+
this.registerDisposer(plugin.name, result);
|
|
305
|
+
hasDisposer = true;
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.warn(`[plugin-manager] Plugin "${plugin.name}" setup() failed:`, err.message);
|
|
309
|
+
this.plugins.delete(plugin.name);
|
|
310
|
+
if (plugin.api) this.pluginAPIs.delete(plugin.name);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
279
315
|
// Call register hook
|
|
280
316
|
if (typeof plugin.register === 'function') {
|
|
281
317
|
try {
|
|
282
|
-
await plugin.register(ctx);
|
|
318
|
+
const result = await plugin.register(ctx);
|
|
319
|
+
if (typeof result === 'function') {
|
|
320
|
+
this.registerDisposer(plugin.name, result);
|
|
321
|
+
hasDisposer = true;
|
|
322
|
+
}
|
|
283
323
|
} catch (err) {
|
|
284
324
|
console.warn(`[plugin-manager] Plugin "${plugin.name}" register() failed:`, err.message);
|
|
285
325
|
this.plugins.delete(plugin.name);
|
|
@@ -288,6 +328,15 @@ class PluginManager {
|
|
|
288
328
|
}
|
|
289
329
|
}
|
|
290
330
|
|
|
331
|
+
// Support explicit shutdown or dispose methods if not already registered
|
|
332
|
+
if (!hasDisposer) {
|
|
333
|
+
if (typeof plugin.shutdown === 'function') {
|
|
334
|
+
this.registerDisposer(plugin.name, () => plugin.shutdown(ctx.app || ctx));
|
|
335
|
+
} else if (typeof plugin.dispose === 'function') {
|
|
336
|
+
this.registerDisposer(plugin.name, () => plugin.dispose(ctx));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
291
340
|
// Apply registered helpers to nunjucks
|
|
292
341
|
this._applyHelpersAndFilters();
|
|
293
342
|
}
|
|
@@ -320,10 +369,32 @@ class PluginManager {
|
|
|
320
369
|
this._collectCspDirectives(plugin.csp);
|
|
321
370
|
}
|
|
322
371
|
|
|
372
|
+
let hasDisposer = false;
|
|
373
|
+
|
|
374
|
+
// Call setup hook if provided
|
|
375
|
+
if (typeof plugin.setup === 'function') {
|
|
376
|
+
try {
|
|
377
|
+
const result = plugin.setup(ctx.app || ctx);
|
|
378
|
+
if (typeof result === 'function') {
|
|
379
|
+
this.registerDisposer(plugin.name, result);
|
|
380
|
+
hasDisposer = true;
|
|
381
|
+
}
|
|
382
|
+
} catch (err) {
|
|
383
|
+
console.warn(`[plugin-manager] Plugin "${plugin.name}" setup() failed:`, err.message);
|
|
384
|
+
this.plugins.delete(plugin.name);
|
|
385
|
+
if (plugin.api) this.pluginAPIs.delete(plugin.name);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
323
390
|
// Call register hook (sync - if plugin has async register, it won't wait)
|
|
324
391
|
if (typeof plugin.register === 'function') {
|
|
325
392
|
try {
|
|
326
|
-
plugin.register(ctx);
|
|
393
|
+
const result = plugin.register(ctx);
|
|
394
|
+
if (typeof result === 'function') {
|
|
395
|
+
this.registerDisposer(plugin.name, result);
|
|
396
|
+
hasDisposer = true;
|
|
397
|
+
}
|
|
327
398
|
} catch (err) {
|
|
328
399
|
console.warn(`[plugin-manager] Plugin "${plugin.name}" register() failed:`, err.message);
|
|
329
400
|
this.plugins.delete(plugin.name);
|
|
@@ -332,6 +403,15 @@ class PluginManager {
|
|
|
332
403
|
}
|
|
333
404
|
}
|
|
334
405
|
|
|
406
|
+
// Support explicit shutdown or dispose methods if not already registered
|
|
407
|
+
if (!hasDisposer) {
|
|
408
|
+
if (typeof plugin.shutdown === 'function') {
|
|
409
|
+
this.registerDisposer(plugin.name, () => plugin.shutdown(ctx.app || ctx));
|
|
410
|
+
} else if (typeof plugin.dispose === 'function') {
|
|
411
|
+
this.registerDisposer(plugin.name, () => plugin.dispose(ctx));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
335
415
|
// Apply registered helpers to nunjucks
|
|
336
416
|
this._applyHelpersAndFilters();
|
|
337
417
|
}
|
|
@@ -421,6 +501,22 @@ class PluginManager {
|
|
|
421
501
|
// Script Injection API
|
|
422
502
|
// ============================================
|
|
423
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Register a cleanup / disposer hook for this plugin
|
|
506
|
+
* @param {Function} fn
|
|
507
|
+
*/
|
|
508
|
+
onDispose(fn) {
|
|
509
|
+
self.registerDisposer(plugin.name, fn);
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Alias for onDispose
|
|
514
|
+
* @param {Function} fn
|
|
515
|
+
*/
|
|
516
|
+
onShutdown(fn) {
|
|
517
|
+
self.registerDisposer(plugin.name, fn);
|
|
518
|
+
},
|
|
519
|
+
|
|
424
520
|
/**
|
|
425
521
|
* Inject content into head section
|
|
426
522
|
* @param {string} content - HTML/script content
|
|
@@ -583,6 +679,30 @@ class PluginManager {
|
|
|
583
679
|
return this.plugins.has(name);
|
|
584
680
|
}
|
|
585
681
|
|
|
682
|
+
/**
|
|
683
|
+
* Get all registered disposers
|
|
684
|
+
* @returns {Array<{ name: string, fn: Function }>}
|
|
685
|
+
*/
|
|
686
|
+
getDisposers() {
|
|
687
|
+
return this.disposers.slice();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Execute all registered plugin disposers in reverse registration order
|
|
692
|
+
* @returns {Promise<void>}
|
|
693
|
+
*/
|
|
694
|
+
async dispose() {
|
|
695
|
+
const reverseDisposers = this.disposers.slice().reverse();
|
|
696
|
+
for (const { name, fn } of reverseDisposers) {
|
|
697
|
+
try {
|
|
698
|
+
await fn();
|
|
699
|
+
} catch (err) {
|
|
700
|
+
console.warn(`[plugin-manager] Plugin "${name}" disposer failed:`, err.message);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
this.disposers = [];
|
|
704
|
+
}
|
|
705
|
+
|
|
586
706
|
/**
|
|
587
707
|
* Get all registered plugin names
|
|
588
708
|
*/
|