webspresso 0.0.97 → 0.0.98

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.
Files changed (100) hide show
  1. package/.agents/ADMIN.md +153 -5
  2. package/.agents/ARCHITECTURE.md +10 -0
  3. package/.agents/CLI.md +195 -1
  4. package/.agents/PLUGINS.md +153 -9
  5. package/.agents/QUEUE.md +43 -0
  6. package/bin/commands/add-esbuild.js +80 -0
  7. package/bin/commands/add-tailwind.js +76 -31
  8. package/bin/commands/analytics-prune.js +57 -0
  9. package/bin/commands/api.js +207 -15
  10. package/bin/commands/build.js +99 -0
  11. package/bin/commands/dev.js +64 -35
  12. package/bin/commands/doctor.js +26 -0
  13. package/bin/commands/email-prune.js +47 -0
  14. package/bin/commands/make-job.js +86 -0
  15. package/bin/commands/make-model.js +250 -0
  16. package/bin/commands/new.js +45 -28
  17. package/bin/commands/openapi.js +102 -0
  18. package/bin/commands/page.js +6 -3
  19. package/bin/commands/queue-prune.js +59 -0
  20. package/bin/commands/routes.js +52 -0
  21. package/bin/commands/service.js +6 -3
  22. package/bin/commands/typegen.js +106 -0
  23. package/bin/commands/upload-prune.js +79 -0
  24. package/bin/webspresso.js +22 -0
  25. package/core/applySchema.js +13 -6
  26. package/core/auth/hash.js +21 -12
  27. package/core/compression/middleware.js +23 -17
  28. package/core/openapi/build-from-api-routes.js +160 -29
  29. package/core/orm/cache/fingerprint.js +15 -1
  30. package/core/orm/cache/memory-provider.js +27 -2
  31. package/core/orm/index.js +12 -7
  32. package/core/orm/model.js +49 -7
  33. package/core/queue/adapters/database.js +94 -0
  34. package/core/queue/adapters/memory.js +62 -0
  35. package/core/queue/adapters/redis.js +80 -0
  36. package/core/queue/manager.js +123 -0
  37. package/core/ssr/stream.js +11 -0
  38. package/index.d.ts +660 -10
  39. package/index.js +44 -96
  40. package/package.json +1 -1
  41. package/plugins/admin-panel/api.js +279 -17
  42. package/plugins/admin-panel/app.js +114 -22
  43. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  44. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  45. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
  46. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  47. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  48. package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
  49. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  50. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  51. package/plugins/admin-panel/core/admin-module.js +13 -7
  52. package/plugins/admin-panel/core/api-extensions.js +30 -2
  53. package/plugins/admin-panel/core/registry.js +96 -16
  54. package/plugins/admin-panel/index.js +96 -0
  55. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  56. package/plugins/admin-panel/modules/admin-users.js +16 -1
  57. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  58. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  59. package/plugins/admin-panel/modules/dashboard.js +283 -0
  60. package/plugins/admin-panel/modules/menu.js +56 -18
  61. package/plugins/admin-panel/modules/user-management.js +24 -6
  62. package/plugins/audit-log/admin-component.js +11 -0
  63. package/plugins/audit-log/api-handlers.js +34 -0
  64. package/plugins/audit-log/index.js +129 -3
  65. package/plugins/audit-log/purge.js +60 -10
  66. package/plugins/dashboard/app.js +152 -27
  67. package/plugins/dashboard/index.js +143 -39
  68. package/plugins/dashboard/styles.js +78 -0
  69. package/plugins/data-exchange/export-xlsx.js +9 -2
  70. package/plugins/data-exchange/import.js +9 -2
  71. package/plugins/data-exchange/index.js +6 -3
  72. package/plugins/email/index.js +132 -2
  73. package/plugins/email/purge.js +60 -10
  74. package/plugins/index.js +4 -0
  75. package/plugins/orm-cache-admin/index.js +4 -2
  76. package/plugins/queue/index.js +11 -0
  77. package/plugins/schema-explorer/admin-component.js +714 -0
  78. package/plugins/schema-explorer/relations-graph.js +205 -0
  79. package/plugins/schema-explorer.js +248 -69
  80. package/plugins/site-analytics/admin-component.js +23 -0
  81. package/plugins/site-analytics/api-handlers.js +42 -0
  82. package/plugins/site-analytics/index.js +147 -2
  83. package/plugins/site-analytics/purge.js +168 -0
  84. package/plugins/swagger.js +261 -13
  85. package/plugins/upload/index.js +26 -0
  86. package/plugins/upload/orphan-pruner.js +292 -0
  87. package/src/api/api-loader.js +3 -1
  88. package/src/bundle/index.js +517 -0
  89. package/src/bundle/manifest.js +165 -0
  90. package/src/dev/banner.js +296 -0
  91. package/src/dev/colors.js +70 -0
  92. package/src/dev/index.js +62 -0
  93. package/src/dev/profiler.js +354 -0
  94. package/src/dev/typegen.js +497 -0
  95. package/src/dev-reload/index.js +611 -0
  96. package/src/file-router.js +210 -62
  97. package/src/helpers.js +958 -98
  98. package/src/njk-frontmatter.js +12 -0
  99. package/src/pages/page-loader.js +61 -11
  100. package/src/server.js +629 -63
@@ -8,12 +8,13 @@ const path = require('path');
8
8
  const { ZodError } = require('zod');
9
9
  const { compileSchema, invalidateSchema } = require('../core/compileSchema');
10
10
  const { applySchema } = require('../core/applySchema');
11
- const { createHelpers } = require('./helpers');
11
+ const { createHelpers, getAssetManager } = require('./helpers');
12
12
  const {
13
13
  loadNjkRouteTemplate,
14
14
  parseNjkFrontmatter,
15
15
  frontmatterToPatches,
16
16
  clearNjkFrontmatterCaches,
17
+ registerClearHook,
17
18
  } = require('./njk-frontmatter');
18
19
  const { renderStream } = require('../core/ssr/stream');
19
20
 
@@ -27,12 +28,30 @@ const i18nCache = new Map();
27
28
  // Cache for merged i18n (key: `${pagesDir}::${routeDir}::${locale}`, value: { globalMtime, routeMtime, data })
28
29
  const mergedI18nCache = new Map();
29
30
 
31
+ // Cache for translator instances in production
32
+ const translatorCache = new Map();
33
+
34
+ function clearTranslatorCache() {
35
+ translatorCache.clear();
36
+ }
37
+
30
38
  // Cache for route configs in production
31
39
  const configCache = new Map();
32
40
 
33
41
  // Dev-only: avoid require() on every SSR request when the .js file is unchanged (mtime)
34
42
  const routeConfigDevCache = new Map();
35
43
 
44
+ // Cache for compiled frontmatter string templates (key: fullPath, value: compiled Template)
45
+ const compiledStringTemplateCache = new Map();
46
+
47
+ function clearCompiledStringTemplateCache() {
48
+ compiledStringTemplateCache.clear();
49
+ }
50
+
51
+ if (typeof registerClearHook === 'function') {
52
+ registerClearHook(clearCompiledStringTemplateCache);
53
+ }
54
+
36
55
  // Cache for API filename -> { method, baseName } (basename keys; stable per process)
37
56
  const methodFromFilenameCache = new Map();
38
57
 
@@ -43,6 +62,8 @@ const MAX_LOCALE_LEN = 16;
43
62
 
44
63
  let cachedSupportedLocalesRaw = null;
45
64
  let cachedSupportedLocaleSet = null;
65
+ let cachedDefaultLocaleKey = null;
66
+ let cachedDefaultLocale = 'en';
46
67
 
47
68
  /** @returns {Set<string>} */
48
69
  function parseSupportedLocaleSet() {
@@ -57,6 +78,27 @@ function parseSupportedLocaleSet() {
57
78
  return cachedSupportedLocaleSet;
58
79
  }
59
80
 
81
+ /**
82
+ * Get default locale matching supported set
83
+ * @param {Set<string>} [supported]
84
+ * @returns {string}
85
+ */
86
+ function getDefaultLocale(supported = parseSupportedLocaleSet()) {
87
+ const rawDef = process.env.DEFAULT_LOCALE || 'en';
88
+ const rawSupp = process.env.SUPPORTED_LOCALES || 'en';
89
+ const key = rawDef + '|' + rawSupp;
90
+ if (key === cachedDefaultLocaleKey && cachedDefaultLocale) {
91
+ return cachedDefaultLocale;
92
+ }
93
+ cachedDefaultLocaleKey = key;
94
+ const defaultCand = normalizeLocaleCandidate(rawDef);
95
+ cachedDefaultLocale =
96
+ pickMatchingLocale(defaultCand, supported)
97
+ ?? (supported.has('en') ? 'en' : [...supported][0])
98
+ ?? 'en';
99
+ return cachedDefaultLocale;
100
+ }
101
+
60
102
  /**
61
103
  * @param {string} raw
62
104
  * @returns {string|null}
@@ -271,29 +313,31 @@ function extractMethodFromFilename(filename) {
271
313
 
272
314
  /**
273
315
  * Whether `load()` return values for `stylesheets` and `scripts` are promoted to
274
- * `pageHead` in Nunjucks (see `createApp({ pageAssets })`).
275
- * @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean}|null|undefined} raw
276
- * @returns {{ enabled: boolean, stylesheets: boolean, scripts: boolean }}
316
+ * `pageHead` in Nunjucks (see `createApp({ pageAssets })`), with optional automatic
317
+ * per-route script/stylesheet discovery.
318
+ * @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean, auto?: boolean}|null|undefined} raw
319
+ * @returns {{ enabled: boolean, stylesheets: boolean, scripts: boolean, auto: boolean }}
277
320
  */
278
321
  function resolvePageAssets(raw) {
279
322
  if (raw === true) {
280
- return { enabled: true, stylesheets: true, scripts: true };
323
+ return { enabled: true, stylesheets: true, scripts: true, auto: true };
281
324
  }
282
325
  if (raw == null || raw === false) {
283
- return { enabled: false, stylesheets: false, scripts: false };
326
+ return { enabled: false, stylesheets: false, scripts: false, auto: false };
284
327
  }
285
328
  if (typeof raw === 'object') {
286
329
  const on = raw.enabled !== false;
287
330
  if (!on) {
288
- return { enabled: false, stylesheets: false, scripts: false };
331
+ return { enabled: false, stylesheets: false, scripts: false, auto: false };
289
332
  }
290
333
  return {
291
334
  enabled: true,
292
335
  stylesheets: raw.stylesheets !== false,
293
336
  scripts: raw.scripts !== false,
337
+ auto: raw.auto !== false,
294
338
  };
295
339
  }
296
- return { enabled: false, stylesheets: false, scripts: false };
340
+ return { enabled: false, stylesheets: false, scripts: false, auto: false };
297
341
  }
298
342
 
299
343
  /**
@@ -306,11 +350,13 @@ function toList(v) {
306
350
  }
307
351
 
308
352
  /**
309
- * @param {{ enabled: boolean, stylesheets: boolean, scripts: boolean }} cfg
353
+ * @param {{ enabled: boolean, stylesheets: boolean, scripts: boolean, auto?: boolean }} cfg
310
354
  * @param {Object} data
355
+ * @param {{ file?: string, routePath?: string }} [routeInfo={}]
356
+ * @param {Object|null} [assetManager=null]
311
357
  * @returns {{ data: Object, pageHead: { stylesheets: unknown[], scripts: unknown[] }|null, pageAssets: boolean }}
312
358
  */
313
- function applyPageAssetsToTemplateData(cfg, data) {
359
+ function applyPageAssetsToTemplateData(cfg, data, routeInfo = {}, assetManager = null) {
314
360
  if (!cfg || !cfg.enabled) {
315
361
  return { data, pageHead: null, pageAssets: false };
316
362
  }
@@ -325,6 +371,40 @@ function applyPageAssetsToTemplateData(cfg, data) {
325
371
  scriptItems = toList(out.scripts);
326
372
  delete out.scripts;
327
373
  }
374
+
375
+ // Auto-discover page assets from route if enabled
376
+ if (cfg.auto && assetManager && (routeInfo.file || routeInfo.routePath)) {
377
+ const candidates = typeof assetManager.getRouteAssetCandidates === 'function'
378
+ ? assetManager.getRouteAssetCandidates(routeInfo.file, routeInfo.routePath)
379
+ : [];
380
+
381
+ if (cfg.stylesheets) {
382
+ for (const cand of candidates) {
383
+ if (typeof assetManager.hasPageCss === 'function' && assetManager.hasPageCss(cand)) {
384
+ const cssUrl = assetManager.pageCssUrl(cand);
385
+ const already = styles.some((s) => (typeof s === 'string' ? s === cssUrl : s?.href === cssUrl));
386
+ if (!already) {
387
+ styles.push(cssUrl);
388
+ }
389
+ break;
390
+ }
391
+ }
392
+ }
393
+
394
+ if (cfg.scripts) {
395
+ for (const cand of candidates) {
396
+ if (typeof assetManager.hasPageJs === 'function' && assetManager.hasPageJs(cand)) {
397
+ const jsUrl = assetManager.pageJsUrl(cand);
398
+ const already = scriptItems.some((s) => (typeof s === 'string' ? s === jsUrl : s?.src === jsUrl));
399
+ if (!already) {
400
+ scriptItems.push({ src: jsUrl, defer: true });
401
+ }
402
+ break;
403
+ }
404
+ }
405
+ }
406
+ }
407
+
328
408
  return {
329
409
  data: out,
330
410
  pageHead: { stylesheets: styles, scripts: scriptItems },
@@ -492,15 +572,25 @@ function getPluralRules(locale) {
492
572
  return pr;
493
573
  }
494
574
 
575
+ const KEY_PARTS_CACHE = new Map();
576
+
495
577
  function resolveTranslationValue(dict, key) {
496
- if (!dict || typeof dict !== 'object') return undefined;
578
+ if (!dict || typeof dict !== 'object' || typeof key !== 'string') return undefined;
497
579
  if (dict[key] !== undefined) return dict[key];
580
+ if (!key.includes('.')) return undefined;
581
+
582
+ let parts = KEY_PARTS_CACHE.get(key);
583
+ if (!parts) {
584
+ parts = key.split('.');
585
+ if (KEY_PARTS_CACHE.size < 1000) {
586
+ KEY_PARTS_CACHE.set(key, parts);
587
+ }
588
+ }
498
589
 
499
- const parts = key.split('.');
500
590
  let curr = dict;
501
- for (const part of parts) {
591
+ for (let i = 0; i < parts.length; i++) {
502
592
  if (curr && typeof curr === 'object') {
503
- curr = curr[part];
593
+ curr = curr[parts[i]];
504
594
  } else {
505
595
  return undefined;
506
596
  }
@@ -726,25 +816,37 @@ function loadGlobalHooks(pagesDir, isDev) {
726
816
  * @param {string} hookName - Hook name
727
817
  * @param {Object} ctx - Context object
728
818
  */
729
- async function executeHook(hooks, hookName, ctx, ...extra) {
730
- if (hooks && typeof hooks[hookName] === 'function') {
731
- await hooks[hookName](ctx, ...extra);
819
+ function executeHook(hooks, hookName, ctx, ...extra) {
820
+ if (!hooks || typeof hooks[hookName] !== 'function') {
821
+ return;
732
822
  }
823
+ return hooks[hookName](ctx, ...extra);
824
+ }
825
+
826
+ function runHooks(globalHooks, routeHooks, hookName, ctx, ...extra) {
827
+ const gFn = globalHooks ? globalHooks[hookName] : undefined;
828
+ const rFn = routeHooks ? routeHooks[hookName] : undefined;
829
+ if (typeof gFn !== 'function' && typeof rFn !== 'function') return;
830
+ return (async () => {
831
+ if (typeof gFn === 'function') {
832
+ const gRes = gFn(ctx, ...extra);
833
+ if (gRes && typeof gRes.then === 'function') await gRes;
834
+ }
835
+ if (typeof rFn === 'function') {
836
+ const rRes = rFn(ctx, ...extra);
837
+ if (rRes && typeof rRes.then === 'function') await rRes;
838
+ }
839
+ })();
733
840
  }
734
841
 
735
842
  /**
736
843
  * Detect locale from request
737
844
  * @param {Object} req - Express request
845
+ * @param {Set<string>} [supported] - Optional precomputed supported locales set
846
+ * @param {string} [def] - Optional precomputed default locale
738
847
  * @returns {string} Locale code
739
848
  */
740
- function detectLocale(req) {
741
- const supported = parseSupportedLocaleSet();
742
- const defaultCand = normalizeLocaleCandidate(process.env.DEFAULT_LOCALE || 'en');
743
- const def =
744
- pickMatchingLocale(defaultCand, supported)
745
- ?? (supported.has('en') ? 'en' : [...supported][0])
746
- ?? 'en';
747
-
849
+ function detectLocale(req, supported = parseSupportedLocaleSet(), def = getDefaultLocale(supported)) {
748
850
  if (req.query && req.query.lang != null && req.query.lang !== '') {
749
851
  const q = normalizeLocaleCandidate(String(req.query.lang));
750
852
  const hit = pickMatchingLocale(q, supported);
@@ -1064,10 +1166,10 @@ function mountPages(app, options) {
1064
1166
  }
1065
1167
  }
1066
1168
  // Same instance as createApp({ db }) / getAppContext().db — available to handler & route middleware
1067
- if (db != null) {
1169
+ if (db != null && !req.db) {
1068
1170
  req.db = db;
1069
1171
  }
1070
- if (serviceRegistry != null) {
1172
+ if (serviceRegistry != null && !req.service) {
1071
1173
  req.service = (name, input, opts) =>
1072
1174
  serviceRegistry.call(name, input, { req, res, db }, opts);
1073
1175
  }
@@ -1159,20 +1261,38 @@ function mountPages(app, options) {
1159
1261
  }
1160
1262
  }
1161
1263
  // Detect locale
1162
- const locale = detectLocale(req);
1163
- const defaultLocale = process.env.DEFAULT_LOCALE || 'en';
1264
+ const supported = parseSupportedLocaleSet();
1265
+ const defaultLocale = getDefaultLocale(supported);
1266
+ const locale = detectLocale(req, supported, defaultLocale);
1164
1267
 
1165
1268
  // Load translations (primary + fallback)
1166
- const translations = loadI18n(absolutePagesDir, route.routeDir, locale, isDev);
1167
- const fallbackTranslations = (locale !== defaultLocale)
1168
- ? loadI18n(absolutePagesDir, route.routeDir, defaultLocale, isDev)
1169
- : EMPTY_OBJECT;
1170
-
1171
- const t = createTranslator(translations, {
1172
- locale,
1173
- fallbackTranslations,
1174
- fallbackLocale: defaultLocale,
1175
- });
1269
+ let t;
1270
+ if (!isDev) {
1271
+ const transKey = `${absolutePagesDir}::${route.routeDir}::${locale}::${defaultLocale}`;
1272
+ t = translatorCache.get(transKey);
1273
+ if (!t) {
1274
+ const translations = loadI18n(absolutePagesDir, route.routeDir, locale, false);
1275
+ const fallbackTranslations = (locale !== defaultLocale)
1276
+ ? loadI18n(absolutePagesDir, route.routeDir, defaultLocale, false)
1277
+ : EMPTY_OBJECT;
1278
+ t = createTranslator(translations, {
1279
+ locale,
1280
+ fallbackTranslations,
1281
+ fallbackLocale: defaultLocale,
1282
+ });
1283
+ translatorCache.set(transKey, t);
1284
+ }
1285
+ } else {
1286
+ const translations = loadI18n(absolutePagesDir, route.routeDir, locale, true);
1287
+ const fallbackTranslations = (locale !== defaultLocale)
1288
+ ? loadI18n(absolutePagesDir, route.routeDir, defaultLocale, true)
1289
+ : EMPTY_OBJECT;
1290
+ t = createTranslator(translations, {
1291
+ locale,
1292
+ fallbackTranslations,
1293
+ fallbackLocale: defaultLocale,
1294
+ });
1295
+ }
1176
1296
 
1177
1297
  // Load route config
1178
1298
  const config = isDev ? loadRouteConfig(route.configPath, true) : mountConfig;
@@ -1190,6 +1310,8 @@ function mountPages(app, options) {
1190
1310
  }
1191
1311
 
1192
1312
  const njkTpl = loadNjkRouteTemplate(route.fullPath, isDev);
1313
+ const metaTitle = t('meta.title');
1314
+ const metaDesc = t('meta.description');
1193
1315
 
1194
1316
  const ctx = {
1195
1317
  req,
@@ -1208,8 +1330,8 @@ function mountPages(app, options) {
1208
1330
  },
1209
1331
  data: { ...njkTpl.dataPatch },
1210
1332
  meta: {
1211
- title: t('meta.title') !== 'meta.title' ? t('meta.title') : null,
1212
- description: t('meta.description') !== 'meta.description' ? t('meta.description') : null,
1333
+ title: metaTitle !== 'meta.title' ? metaTitle : null,
1334
+ description: metaDesc !== 'meta.description' ? metaDesc : null,
1213
1335
  indexable: true,
1214
1336
  canonical: null,
1215
1337
  ...njkTpl.metaPatch,
@@ -1221,16 +1343,16 @@ function mountPages(app, options) {
1221
1343
  req.context = ctx;
1222
1344
 
1223
1345
  // Execute hooks: onRequest
1224
- await executeHook(globalHooks, 'onRequest', ctx);
1225
- await executeHook(routeHooks, 'onRequest', ctx);
1346
+ let hookPromise = runHooks(globalHooks, routeHooks, 'onRequest', ctx);
1347
+ if (hookPromise) await hookPromise;
1226
1348
 
1227
1349
  // Execute hooks: onRoute
1228
- await executeHook(globalHooks, 'onRoute', ctx);
1229
- await executeHook(routeHooks, 'onRoute', ctx);
1350
+ hookPromise = runHooks(globalHooks, routeHooks, 'onRoute', ctx);
1351
+ if (hookPromise) await hookPromise;
1230
1352
 
1231
1353
  // Execute hooks: beforeMiddleware
1232
- await executeHook(globalHooks, 'beforeMiddleware', ctx);
1233
- await executeHook(routeHooks, 'beforeMiddleware', ctx);
1354
+ hookPromise = runHooks(globalHooks, routeHooks, 'beforeMiddleware', ctx);
1355
+ if (hookPromise) await hookPromise;
1234
1356
 
1235
1357
  // Run route middleware (chain fixed at route registration; edit middleware in dev → restart)
1236
1358
  if (preResolvedPageMw.length) {
@@ -1245,8 +1367,8 @@ function mountPages(app, options) {
1245
1367
  }
1246
1368
 
1247
1369
  // Execute hooks: afterMiddleware
1248
- await executeHook(globalHooks, 'afterMiddleware', ctx);
1249
- await executeHook(routeHooks, 'afterMiddleware', ctx);
1370
+ hookPromise = runHooks(globalHooks, routeHooks, 'afterMiddleware', ctx);
1371
+ if (hookPromise) await hookPromise;
1250
1372
 
1251
1373
  // Execute hooks: beforeLoad
1252
1374
  if (pluginManager && db) {
@@ -1255,8 +1377,8 @@ function mountPages(app, options) {
1255
1377
  ctx.content = contentApi.getContentService(db);
1256
1378
  }
1257
1379
  }
1258
- await executeHook(globalHooks, 'beforeLoad', ctx);
1259
- await executeHook(routeHooks, 'beforeLoad', ctx);
1380
+ hookPromise = runHooks(globalHooks, routeHooks, 'beforeLoad', ctx);
1381
+ if (hookPromise) await hookPromise;
1260
1382
 
1261
1383
  // Run load function
1262
1384
  if (config?.load && typeof config.load === 'function') {
@@ -1267,8 +1389,8 @@ function mountPages(app, options) {
1267
1389
  }
1268
1390
 
1269
1391
  // Execute hooks: afterLoad
1270
- await executeHook(globalHooks, 'afterLoad', ctx);
1271
- await executeHook(routeHooks, 'afterLoad', ctx);
1392
+ hookPromise = runHooks(globalHooks, routeHooks, 'afterLoad', ctx);
1393
+ if (hookPromise) await hookPromise;
1272
1394
 
1273
1395
  // Run meta function
1274
1396
  if (config?.meta && typeof config.meta === 'function') {
@@ -1279,10 +1401,18 @@ function mountPages(app, options) {
1279
1401
  }
1280
1402
 
1281
1403
  // Execute hooks: beforeRender
1282
- await executeHook(globalHooks, 'beforeRender', ctx);
1283
- await executeHook(routeHooks, 'beforeRender', ctx);
1404
+ hookPromise = runHooks(globalHooks, routeHooks, 'beforeRender', ctx);
1405
+ if (hookPromise) await hookPromise;
1284
1406
 
1285
- const pageAssetBundle = applyPageAssetsToTemplateData(pageAssetsResolved, ctx.data);
1407
+ const pageAssetBundle = applyPageAssetsToTemplateData(
1408
+ pageAssetsResolved,
1409
+ ctx.data,
1410
+ {
1411
+ file: route.file,
1412
+ routePath: route.routePath,
1413
+ },
1414
+ ctx.fsy?.assetManager || getAssetManager()
1415
+ );
1286
1416
  ctx.data = pageAssetBundle.data;
1287
1417
  const renderContext = {
1288
1418
  ...ctx.data,
@@ -1316,10 +1446,24 @@ function mountPages(app, options) {
1316
1446
  });
1317
1447
  }
1318
1448
 
1319
- let html =
1320
- njkTpl.useStringRender && njkTpl.templateBody != null
1321
- ? nunjucks.renderString(njkTpl.templateBody, renderContext, { path: route.fullPath })
1322
- : nunjucks.render(templatePath, renderContext);
1449
+ let html;
1450
+ if (njkTpl.useStringRender && njkTpl.templateBody != null) {
1451
+ if (typeof nunjucks.Template === 'function') {
1452
+ let compiled = isDev ? null : compiledStringTemplateCache.get(route.fullPath);
1453
+ if (!compiled || (isDev && compiled._templateBody !== njkTpl.templateBody)) {
1454
+ compiled = new nunjucks.Template(njkTpl.templateBody, nunjucks, route.fullPath);
1455
+ compiled._templateBody = njkTpl.templateBody;
1456
+ compiledStringTemplateCache.set(route.fullPath, compiled);
1457
+ }
1458
+ html = compiled.render(renderContext);
1459
+ } else if (typeof nunjucks.renderString === 'function') {
1460
+ html = nunjucks.renderString(njkTpl.templateBody, renderContext, { path: route.fullPath });
1461
+ } else {
1462
+ html = nunjucks.render(templatePath, renderContext);
1463
+ }
1464
+ } else {
1465
+ html = nunjucks.render(templatePath, renderContext);
1466
+ }
1323
1467
 
1324
1468
  if (pluginManager) {
1325
1469
  const contentApi = pluginManager.getPluginAPI('content');
@@ -1330,8 +1474,8 @@ function mountPages(app, options) {
1330
1474
 
1331
1475
  // Execute hooks: afterRender
1332
1476
  ctx.html = html;
1333
- await executeHook(globalHooks, 'afterRender', ctx);
1334
- await executeHook(routeHooks, 'afterRender', ctx);
1477
+ hookPromise = runHooks(globalHooks, routeHooks, 'afterRender', ctx);
1478
+ if (hookPromise) await hookPromise;
1335
1479
 
1336
1480
  if (route.routePath === '/404' || route.file === '404.njk') {
1337
1481
  res.status(404);
@@ -1404,5 +1548,9 @@ module.exports = {
1404
1548
  frontmatterToPatches,
1405
1549
  loadNjkRouteTemplate,
1406
1550
  clearNjkFrontmatterCaches,
1551
+ compiledStringTemplateCache,
1552
+ clearCompiledStringTemplateCache,
1553
+ translatorCache,
1554
+ clearTranslatorCache,
1407
1555
  };
1408
1556