webspresso 0.0.97 → 0.0.99
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/.agents/ADMIN.md +163 -5
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/AUTH.md +169 -0
- package/.agents/CLI.md +195 -1
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +221 -9
- package/.agents/QUEUE.md +127 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +21 -12
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +20 -7
- package/core/orm/model.js +53 -7
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +205 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +1188 -10
- package/index.js +88 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -59
- package/plugins/audit-log/api-handlers.js +148 -0
- package/plugins/audit-log/index.js +153 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +10 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +15 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +23 -0
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +4 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +347 -162
- package/src/helpers.js +967 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +635 -63
package/src/file-router.js
CHANGED
|
@@ -8,14 +8,28 @@ 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');
|
|
20
|
+
const {
|
|
21
|
+
loadCascadingMiddlewares,
|
|
22
|
+
runMiddlewareChain,
|
|
23
|
+
findCascadingLayouts,
|
|
24
|
+
executeCascadingLayoutLoads,
|
|
25
|
+
applyCascadingLayouts,
|
|
26
|
+
templateHasExtends,
|
|
27
|
+
} = require('./routing/cascading');
|
|
28
|
+
const {
|
|
29
|
+
isMiddlewareFactory,
|
|
30
|
+
resolveNamedMiddleware,
|
|
31
|
+
resolveMiddlewares,
|
|
32
|
+
} = require('./routing/middleware-resolver');
|
|
19
33
|
|
|
20
34
|
const NOOP = () => {};
|
|
21
35
|
const EMPTY_OBJECT = Object.freeze({});
|
|
@@ -27,12 +41,30 @@ const i18nCache = new Map();
|
|
|
27
41
|
// Cache for merged i18n (key: `${pagesDir}::${routeDir}::${locale}`, value: { globalMtime, routeMtime, data })
|
|
28
42
|
const mergedI18nCache = new Map();
|
|
29
43
|
|
|
44
|
+
// Cache for translator instances in production
|
|
45
|
+
const translatorCache = new Map();
|
|
46
|
+
|
|
47
|
+
function clearTranslatorCache() {
|
|
48
|
+
translatorCache.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
// Cache for route configs in production
|
|
31
52
|
const configCache = new Map();
|
|
32
53
|
|
|
33
54
|
// Dev-only: avoid require() on every SSR request when the .js file is unchanged (mtime)
|
|
34
55
|
const routeConfigDevCache = new Map();
|
|
35
56
|
|
|
57
|
+
// Cache for compiled frontmatter string templates (key: fullPath, value: compiled Template)
|
|
58
|
+
const compiledStringTemplateCache = new Map();
|
|
59
|
+
|
|
60
|
+
function clearCompiledStringTemplateCache() {
|
|
61
|
+
compiledStringTemplateCache.clear();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof registerClearHook === 'function') {
|
|
65
|
+
registerClearHook(clearCompiledStringTemplateCache);
|
|
66
|
+
}
|
|
67
|
+
|
|
36
68
|
// Cache for API filename -> { method, baseName } (basename keys; stable per process)
|
|
37
69
|
const methodFromFilenameCache = new Map();
|
|
38
70
|
|
|
@@ -43,6 +75,8 @@ const MAX_LOCALE_LEN = 16;
|
|
|
43
75
|
|
|
44
76
|
let cachedSupportedLocalesRaw = null;
|
|
45
77
|
let cachedSupportedLocaleSet = null;
|
|
78
|
+
let cachedDefaultLocaleKey = null;
|
|
79
|
+
let cachedDefaultLocale = 'en';
|
|
46
80
|
|
|
47
81
|
/** @returns {Set<string>} */
|
|
48
82
|
function parseSupportedLocaleSet() {
|
|
@@ -57,6 +91,27 @@ function parseSupportedLocaleSet() {
|
|
|
57
91
|
return cachedSupportedLocaleSet;
|
|
58
92
|
}
|
|
59
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Get default locale matching supported set
|
|
96
|
+
* @param {Set<string>} [supported]
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
function getDefaultLocale(supported = parseSupportedLocaleSet()) {
|
|
100
|
+
const rawDef = process.env.DEFAULT_LOCALE || 'en';
|
|
101
|
+
const rawSupp = process.env.SUPPORTED_LOCALES || 'en';
|
|
102
|
+
const key = rawDef + '|' + rawSupp;
|
|
103
|
+
if (key === cachedDefaultLocaleKey && cachedDefaultLocale) {
|
|
104
|
+
return cachedDefaultLocale;
|
|
105
|
+
}
|
|
106
|
+
cachedDefaultLocaleKey = key;
|
|
107
|
+
const defaultCand = normalizeLocaleCandidate(rawDef);
|
|
108
|
+
cachedDefaultLocale =
|
|
109
|
+
pickMatchingLocale(defaultCand, supported)
|
|
110
|
+
?? (supported.has('en') ? 'en' : [...supported][0])
|
|
111
|
+
?? 'en';
|
|
112
|
+
return cachedDefaultLocale;
|
|
113
|
+
}
|
|
114
|
+
|
|
60
115
|
/**
|
|
61
116
|
* @param {string} raw
|
|
62
117
|
* @returns {string|null}
|
|
@@ -131,6 +186,73 @@ function escapeRegExp(s) {
|
|
|
131
186
|
return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
132
187
|
}
|
|
133
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Checks if a path segment is a route group, e.g. '(marketing)', '(auth)'.
|
|
191
|
+
* Route groups are enclosed in parentheses and do not affect the public URL path.
|
|
192
|
+
* @param {string} segment
|
|
193
|
+
* @returns {boolean}
|
|
194
|
+
*/
|
|
195
|
+
function isRouteGroupSegment(segment) {
|
|
196
|
+
return (
|
|
197
|
+
typeof segment === 'string' &&
|
|
198
|
+
segment.length > 2 &&
|
|
199
|
+
segment.charCodeAt(0) === 40 /* '(' */ &&
|
|
200
|
+
segment.charCodeAt(segment.length - 1) === 41 /* ')' */
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Strips route group segments from a slash-separated route path.
|
|
206
|
+
* E.g., '(marketing)/about' -> 'about'
|
|
207
|
+
* '/(auth)/login' -> '/login'
|
|
208
|
+
* '/(marketing)/(sub)/page' -> '/page'
|
|
209
|
+
* '(marketing)/index' -> 'index'
|
|
210
|
+
* @param {string} route
|
|
211
|
+
* @returns {string}
|
|
212
|
+
*/
|
|
213
|
+
function stripRouteGroups(route) {
|
|
214
|
+
if (!route || typeof route !== 'string' || !route.includes('(')) {
|
|
215
|
+
return route;
|
|
216
|
+
}
|
|
217
|
+
const hadLeadingSlash = route.startsWith('/');
|
|
218
|
+
const rawSegments = route.split('/');
|
|
219
|
+
const filtered = rawSegments.filter((seg) => !isRouteGroupSegment(seg));
|
|
220
|
+
|
|
221
|
+
if (filtered.length === 0 || (filtered.length === 1 && filtered[0] === '')) {
|
|
222
|
+
return hadLeadingSlash ? '/' : '';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let res = filtered.join('/');
|
|
226
|
+
if (hadLeadingSlash && !res.startsWith('/')) {
|
|
227
|
+
res = '/' + res;
|
|
228
|
+
}
|
|
229
|
+
return res;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Checks if a file path belongs to an API route, accounting for optional parent route groups.
|
|
234
|
+
* E.g., 'api/users.get.js' -> true
|
|
235
|
+
* 'api/(v1)/users.get.js' -> true
|
|
236
|
+
* '(marketing)/api/newsletter.post.js' -> true
|
|
237
|
+
* 'about.njk' -> false
|
|
238
|
+
* @param {string} file
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
function isApiRouteFile(file) {
|
|
242
|
+
if (!file || typeof file !== 'string') return false;
|
|
243
|
+
const normalized = file.split(/[/\\]/).join('/');
|
|
244
|
+
if (normalized.startsWith('api/')) return true;
|
|
245
|
+
const parts = normalized.split('/');
|
|
246
|
+
const apiIndex = parts.indexOf('api');
|
|
247
|
+
if (apiIndex > 0) {
|
|
248
|
+
const prefix = parts.slice(0, apiIndex);
|
|
249
|
+
if (prefix.every(isRouteGroupSegment)) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
134
256
|
/**
|
|
135
257
|
* Convert a file path to an Express route pattern
|
|
136
258
|
* @param {string} filePath - Relative path from pages/
|
|
@@ -144,11 +266,14 @@ function filePathToRoute(filePath, ext) {
|
|
|
144
266
|
// Normalize path separators to forward slashes (handle both / and \)
|
|
145
267
|
route = route.split(path.sep).join('/');
|
|
146
268
|
route = route.split('\\').join('/'); // Also handle literal backslashes
|
|
269
|
+
|
|
270
|
+
// Strip route group segments (e.g. (marketing), (auth), (dashboard))
|
|
271
|
+
route = stripRouteGroups(route);
|
|
147
272
|
|
|
148
273
|
// Handle index files
|
|
149
274
|
if (route.endsWith('/index')) {
|
|
150
275
|
route = route.slice(0, -6) || '/';
|
|
151
|
-
} else if (route === 'index') {
|
|
276
|
+
} else if (route === 'index' || route === '') {
|
|
152
277
|
route = '/';
|
|
153
278
|
}
|
|
154
279
|
|
|
@@ -271,29 +396,31 @@ function extractMethodFromFilename(filename) {
|
|
|
271
396
|
|
|
272
397
|
/**
|
|
273
398
|
* Whether `load()` return values for `stylesheets` and `scripts` are promoted to
|
|
274
|
-
* `pageHead` in Nunjucks (see `createApp({ pageAssets })`)
|
|
275
|
-
*
|
|
276
|
-
* @
|
|
399
|
+
* `pageHead` in Nunjucks (see `createApp({ pageAssets })`), with optional automatic
|
|
400
|
+
* per-route script/stylesheet discovery.
|
|
401
|
+
* @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean, auto?: boolean}|null|undefined} raw
|
|
402
|
+
* @returns {{ enabled: boolean, stylesheets: boolean, scripts: boolean, auto: boolean }}
|
|
277
403
|
*/
|
|
278
404
|
function resolvePageAssets(raw) {
|
|
279
405
|
if (raw === true) {
|
|
280
|
-
return { enabled: true, stylesheets: true, scripts: true };
|
|
406
|
+
return { enabled: true, stylesheets: true, scripts: true, auto: true };
|
|
281
407
|
}
|
|
282
408
|
if (raw == null || raw === false) {
|
|
283
|
-
return { enabled: false, stylesheets: false, scripts: false };
|
|
409
|
+
return { enabled: false, stylesheets: false, scripts: false, auto: false };
|
|
284
410
|
}
|
|
285
411
|
if (typeof raw === 'object') {
|
|
286
412
|
const on = raw.enabled !== false;
|
|
287
413
|
if (!on) {
|
|
288
|
-
return { enabled: false, stylesheets: false, scripts: false };
|
|
414
|
+
return { enabled: false, stylesheets: false, scripts: false, auto: false };
|
|
289
415
|
}
|
|
290
416
|
return {
|
|
291
417
|
enabled: true,
|
|
292
418
|
stylesheets: raw.stylesheets !== false,
|
|
293
419
|
scripts: raw.scripts !== false,
|
|
420
|
+
auto: raw.auto !== false,
|
|
294
421
|
};
|
|
295
422
|
}
|
|
296
|
-
return { enabled: false, stylesheets: false, scripts: false };
|
|
423
|
+
return { enabled: false, stylesheets: false, scripts: false, auto: false };
|
|
297
424
|
}
|
|
298
425
|
|
|
299
426
|
/**
|
|
@@ -306,11 +433,13 @@ function toList(v) {
|
|
|
306
433
|
}
|
|
307
434
|
|
|
308
435
|
/**
|
|
309
|
-
* @param {{ enabled: boolean, stylesheets: boolean, scripts: boolean }} cfg
|
|
436
|
+
* @param {{ enabled: boolean, stylesheets: boolean, scripts: boolean, auto?: boolean }} cfg
|
|
310
437
|
* @param {Object} data
|
|
438
|
+
* @param {{ file?: string, routePath?: string }} [routeInfo={}]
|
|
439
|
+
* @param {Object|null} [assetManager=null]
|
|
311
440
|
* @returns {{ data: Object, pageHead: { stylesheets: unknown[], scripts: unknown[] }|null, pageAssets: boolean }}
|
|
312
441
|
*/
|
|
313
|
-
function applyPageAssetsToTemplateData(cfg, data) {
|
|
442
|
+
function applyPageAssetsToTemplateData(cfg, data, routeInfo = {}, assetManager = null) {
|
|
314
443
|
if (!cfg || !cfg.enabled) {
|
|
315
444
|
return { data, pageHead: null, pageAssets: false };
|
|
316
445
|
}
|
|
@@ -325,6 +454,40 @@ function applyPageAssetsToTemplateData(cfg, data) {
|
|
|
325
454
|
scriptItems = toList(out.scripts);
|
|
326
455
|
delete out.scripts;
|
|
327
456
|
}
|
|
457
|
+
|
|
458
|
+
// Auto-discover page assets from route if enabled
|
|
459
|
+
if (cfg.auto && assetManager && (routeInfo.file || routeInfo.routePath)) {
|
|
460
|
+
const candidates = typeof assetManager.getRouteAssetCandidates === 'function'
|
|
461
|
+
? assetManager.getRouteAssetCandidates(routeInfo.file, routeInfo.routePath)
|
|
462
|
+
: [];
|
|
463
|
+
|
|
464
|
+
if (cfg.stylesheets) {
|
|
465
|
+
for (const cand of candidates) {
|
|
466
|
+
if (typeof assetManager.hasPageCss === 'function' && assetManager.hasPageCss(cand)) {
|
|
467
|
+
const cssUrl = assetManager.pageCssUrl(cand);
|
|
468
|
+
const already = styles.some((s) => (typeof s === 'string' ? s === cssUrl : s?.href === cssUrl));
|
|
469
|
+
if (!already) {
|
|
470
|
+
styles.push(cssUrl);
|
|
471
|
+
}
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (cfg.scripts) {
|
|
478
|
+
for (const cand of candidates) {
|
|
479
|
+
if (typeof assetManager.hasPageJs === 'function' && assetManager.hasPageJs(cand)) {
|
|
480
|
+
const jsUrl = assetManager.pageJsUrl(cand);
|
|
481
|
+
const already = scriptItems.some((s) => (typeof s === 'string' ? s === jsUrl : s?.src === jsUrl));
|
|
482
|
+
if (!already) {
|
|
483
|
+
scriptItems.push({ src: jsUrl, defer: true });
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
328
491
|
return {
|
|
329
492
|
data: out,
|
|
330
493
|
pageHead: { stylesheets: styles, scripts: scriptItems },
|
|
@@ -492,15 +655,25 @@ function getPluralRules(locale) {
|
|
|
492
655
|
return pr;
|
|
493
656
|
}
|
|
494
657
|
|
|
658
|
+
const KEY_PARTS_CACHE = new Map();
|
|
659
|
+
|
|
495
660
|
function resolveTranslationValue(dict, key) {
|
|
496
|
-
if (!dict || typeof dict !== 'object') return undefined;
|
|
661
|
+
if (!dict || typeof dict !== 'object' || typeof key !== 'string') return undefined;
|
|
497
662
|
if (dict[key] !== undefined) return dict[key];
|
|
663
|
+
if (!key.includes('.')) return undefined;
|
|
664
|
+
|
|
665
|
+
let parts = KEY_PARTS_CACHE.get(key);
|
|
666
|
+
if (!parts) {
|
|
667
|
+
parts = key.split('.');
|
|
668
|
+
if (KEY_PARTS_CACHE.size < 1000) {
|
|
669
|
+
KEY_PARTS_CACHE.set(key, parts);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
498
672
|
|
|
499
|
-
const parts = key.split('.');
|
|
500
673
|
let curr = dict;
|
|
501
|
-
for (
|
|
674
|
+
for (let i = 0; i < parts.length; i++) {
|
|
502
675
|
if (curr && typeof curr === 'object') {
|
|
503
|
-
curr = curr[
|
|
676
|
+
curr = curr[parts[i]];
|
|
504
677
|
} else {
|
|
505
678
|
return undefined;
|
|
506
679
|
}
|
|
@@ -726,25 +899,37 @@ function loadGlobalHooks(pagesDir, isDev) {
|
|
|
726
899
|
* @param {string} hookName - Hook name
|
|
727
900
|
* @param {Object} ctx - Context object
|
|
728
901
|
*/
|
|
729
|
-
|
|
730
|
-
if (hooks
|
|
731
|
-
|
|
902
|
+
function executeHook(hooks, hookName, ctx, ...extra) {
|
|
903
|
+
if (!hooks || typeof hooks[hookName] !== 'function') {
|
|
904
|
+
return;
|
|
732
905
|
}
|
|
906
|
+
return hooks[hookName](ctx, ...extra);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function runHooks(globalHooks, routeHooks, hookName, ctx, ...extra) {
|
|
910
|
+
const gFn = globalHooks ? globalHooks[hookName] : undefined;
|
|
911
|
+
const rFn = routeHooks ? routeHooks[hookName] : undefined;
|
|
912
|
+
if (typeof gFn !== 'function' && typeof rFn !== 'function') return;
|
|
913
|
+
return (async () => {
|
|
914
|
+
if (typeof gFn === 'function') {
|
|
915
|
+
const gRes = gFn(ctx, ...extra);
|
|
916
|
+
if (gRes && typeof gRes.then === 'function') await gRes;
|
|
917
|
+
}
|
|
918
|
+
if (typeof rFn === 'function') {
|
|
919
|
+
const rRes = rFn(ctx, ...extra);
|
|
920
|
+
if (rRes && typeof rRes.then === 'function') await rRes;
|
|
921
|
+
}
|
|
922
|
+
})();
|
|
733
923
|
}
|
|
734
924
|
|
|
735
925
|
/**
|
|
736
926
|
* Detect locale from request
|
|
737
927
|
* @param {Object} req - Express request
|
|
928
|
+
* @param {Set<string>} [supported] - Optional precomputed supported locales set
|
|
929
|
+
* @param {string} [def] - Optional precomputed default locale
|
|
738
930
|
* @returns {string} Locale code
|
|
739
931
|
*/
|
|
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
|
-
|
|
932
|
+
function detectLocale(req, supported = parseSupportedLocaleSet(), def = getDefaultLocale(supported)) {
|
|
748
933
|
if (req.query && req.query.lang != null && req.query.lang !== '') {
|
|
749
934
|
const q = normalizeLocaleCandidate(String(req.query.lang));
|
|
750
935
|
const hit = pickMatchingLocale(q, supported);
|
|
@@ -768,86 +953,6 @@ function detectLocale(req) {
|
|
|
768
953
|
return def;
|
|
769
954
|
}
|
|
770
955
|
|
|
771
|
-
/**
|
|
772
|
-
* True when the registry entry is (options) => (req, res, next) => …
|
|
773
|
-
* Express handlers typically have length >= 2 (req, res) or 3 (req, res, next).
|
|
774
|
-
*/
|
|
775
|
-
function isMiddlewareFactory(fn) {
|
|
776
|
-
return typeof fn === 'function' && fn.length <= 1;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/**
|
|
780
|
-
* Resolve a named middleware from createApp({ middlewares }).
|
|
781
|
-
* @param {string} name
|
|
782
|
-
* @param {Function} entry
|
|
783
|
-
* @param {boolean} fromTuple - true when route used ['name', options]
|
|
784
|
-
* @param {unknown} tupleOptions - second element of the tuple (only when fromTuple)
|
|
785
|
-
* @param {Object} middlewareRegistry - for error messages
|
|
786
|
-
* @returns {Function} Express middleware
|
|
787
|
-
*/
|
|
788
|
-
function resolveNamedMiddleware(name, entry, fromTuple, tupleOptions, middlewareRegistry) {
|
|
789
|
-
if (!entry) {
|
|
790
|
-
throw new Error(`Middleware "${name}" not found in registry. Available: ${Object.keys(middlewareRegistry).join(', ') || 'none'}`);
|
|
791
|
-
}
|
|
792
|
-
if (typeof entry !== 'function') {
|
|
793
|
-
throw new Error(`Middleware "${name}" must be a function`);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
if (fromTuple) {
|
|
797
|
-
if (!isMiddlewareFactory(entry)) {
|
|
798
|
-
throw new Error(
|
|
799
|
-
`Middleware "${name}" must be a factory (options) => (req, res, next) => … when using ["${name}", options] tuple form`
|
|
800
|
-
);
|
|
801
|
-
}
|
|
802
|
-
const produced = entry(tupleOptions);
|
|
803
|
-
if (typeof produced !== 'function') {
|
|
804
|
-
throw new Error(`Middleware factory "${name}" must return an Express middleware function`);
|
|
805
|
-
}
|
|
806
|
-
return produced;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
if (isMiddlewareFactory(entry)) {
|
|
810
|
-
const produced = entry({});
|
|
811
|
-
if (typeof produced !== 'function') {
|
|
812
|
-
throw new Error(`Middleware factory "${name}" must return an Express middleware function`);
|
|
813
|
-
}
|
|
814
|
-
return produced;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
return entry;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
/**
|
|
821
|
-
* Resolve middleware from config — functions, string names, or [name, options] tuples
|
|
822
|
-
* @param {Array} middlewareConfig - middleware functions, names, or ['name', options] tuples
|
|
823
|
-
* @param {Object} middlewareRegistry - Named middleware registry (plain handlers or option factories)
|
|
824
|
-
* @returns {Array} Array of resolved middleware functions
|
|
825
|
-
*/
|
|
826
|
-
function resolveMiddlewares(middlewareConfig, middlewareRegistry = EMPTY_OBJECT) {
|
|
827
|
-
if (!middlewareConfig || !Array.isArray(middlewareConfig) || middlewareConfig.length === 0) {
|
|
828
|
-
return EMPTY_ARRAY;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
return middlewareConfig.map((mw, index) => {
|
|
832
|
-
if (typeof mw === 'function') {
|
|
833
|
-
return mw;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
if (typeof mw === 'string') {
|
|
837
|
-
return resolveNamedMiddleware(mw, middlewareRegistry[mw], false, undefined, middlewareRegistry);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
if (Array.isArray(mw) && mw.length === 2 && typeof mw[0] === 'string') {
|
|
841
|
-
const name = mw[0];
|
|
842
|
-
return resolveNamedMiddleware(name, middlewareRegistry[name], true, mw[1], middlewareRegistry);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
throw new Error(
|
|
846
|
-
`Invalid middleware at index ${index}: must be a function, string name, or [name, options] tuple`
|
|
847
|
-
);
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
|
|
851
956
|
/**
|
|
852
957
|
* Mount all pages as routes on the Express app
|
|
853
958
|
* @param {Object} app - Express app
|
|
@@ -971,7 +1076,7 @@ function mountPages(app, options) {
|
|
|
971
1076
|
|
|
972
1077
|
for (const file of files) {
|
|
973
1078
|
const ext = path.extname(file);
|
|
974
|
-
const isApi =
|
|
1079
|
+
const isApi = isApiRouteFile(file);
|
|
975
1080
|
|
|
976
1081
|
if (isApi && ext === '.js') {
|
|
977
1082
|
// API route
|
|
@@ -1064,10 +1169,10 @@ function mountPages(app, options) {
|
|
|
1064
1169
|
}
|
|
1065
1170
|
}
|
|
1066
1171
|
// Same instance as createApp({ db }) / getAppContext().db — available to handler & route middleware
|
|
1067
|
-
if (db != null) {
|
|
1172
|
+
if (db != null && !req.db) {
|
|
1068
1173
|
req.db = db;
|
|
1069
1174
|
}
|
|
1070
|
-
if (serviceRegistry != null) {
|
|
1175
|
+
if (serviceRegistry != null && !req.service) {
|
|
1071
1176
|
req.service = (name, input, opts) =>
|
|
1072
1177
|
serviceRegistry.call(name, input, { req, res, db }, opts);
|
|
1073
1178
|
}
|
|
@@ -1107,19 +1212,32 @@ function mountPages(app, options) {
|
|
|
1107
1212
|
throw err;
|
|
1108
1213
|
}
|
|
1109
1214
|
|
|
1215
|
+
// Run cascading folder middlewares (_middleware.js)
|
|
1216
|
+
const cascadingMw = loadCascadingMiddlewares(absolutePagesDir, route.file, middlewares, isDev);
|
|
1217
|
+
if (cascadingMw.length) {
|
|
1218
|
+
await runMiddlewareChain(cascadingMw, req, res);
|
|
1219
|
+
if (res.headersSent) return;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1110
1222
|
// Run middleware if defined (resolved at route registration — required for stateful middleware like express-rate-limit)
|
|
1111
1223
|
if (preResolvedMw.length) {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
mw(req, res, (err) => {
|
|
1115
|
-
if (err) reject(err);
|
|
1116
|
-
else resolve();
|
|
1117
|
-
});
|
|
1118
|
-
});
|
|
1119
|
-
}
|
|
1224
|
+
await runMiddlewareChain(preResolvedMw, req, res);
|
|
1225
|
+
if (res.headersSent) return;
|
|
1120
1226
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1227
|
+
const apiCtx = Object.assign((err) => next(err), {
|
|
1228
|
+
req,
|
|
1229
|
+
res,
|
|
1230
|
+
next,
|
|
1231
|
+
params: req.params || {},
|
|
1232
|
+
query: req.query || {},
|
|
1233
|
+
body: req.body || {},
|
|
1234
|
+
input: req.input,
|
|
1235
|
+
service: req.service,
|
|
1236
|
+
db,
|
|
1237
|
+
sse: res.sse,
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
await fn(req, res, apiCtx);
|
|
1123
1241
|
} catch (err) {
|
|
1124
1242
|
console.error(`API error ${route.routePath}:`, err);
|
|
1125
1243
|
const hookCtx = { req, res, error: err };
|
|
@@ -1159,20 +1277,38 @@ function mountPages(app, options) {
|
|
|
1159
1277
|
}
|
|
1160
1278
|
}
|
|
1161
1279
|
// Detect locale
|
|
1162
|
-
const
|
|
1163
|
-
const defaultLocale =
|
|
1280
|
+
const supported = parseSupportedLocaleSet();
|
|
1281
|
+
const defaultLocale = getDefaultLocale(supported);
|
|
1282
|
+
const locale = detectLocale(req, supported, defaultLocale);
|
|
1164
1283
|
|
|
1165
1284
|
// Load translations (primary + fallback)
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1285
|
+
let t;
|
|
1286
|
+
if (!isDev) {
|
|
1287
|
+
const transKey = `${absolutePagesDir}::${route.routeDir}::${locale}::${defaultLocale}`;
|
|
1288
|
+
t = translatorCache.get(transKey);
|
|
1289
|
+
if (!t) {
|
|
1290
|
+
const translations = loadI18n(absolutePagesDir, route.routeDir, locale, false);
|
|
1291
|
+
const fallbackTranslations = (locale !== defaultLocale)
|
|
1292
|
+
? loadI18n(absolutePagesDir, route.routeDir, defaultLocale, false)
|
|
1293
|
+
: EMPTY_OBJECT;
|
|
1294
|
+
t = createTranslator(translations, {
|
|
1295
|
+
locale,
|
|
1296
|
+
fallbackTranslations,
|
|
1297
|
+
fallbackLocale: defaultLocale,
|
|
1298
|
+
});
|
|
1299
|
+
translatorCache.set(transKey, t);
|
|
1300
|
+
}
|
|
1301
|
+
} else {
|
|
1302
|
+
const translations = loadI18n(absolutePagesDir, route.routeDir, locale, true);
|
|
1303
|
+
const fallbackTranslations = (locale !== defaultLocale)
|
|
1304
|
+
? loadI18n(absolutePagesDir, route.routeDir, defaultLocale, true)
|
|
1305
|
+
: EMPTY_OBJECT;
|
|
1306
|
+
t = createTranslator(translations, {
|
|
1307
|
+
locale,
|
|
1308
|
+
fallbackTranslations,
|
|
1309
|
+
fallbackLocale: defaultLocale,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1176
1312
|
|
|
1177
1313
|
// Load route config
|
|
1178
1314
|
const config = isDev ? loadRouteConfig(route.configPath, true) : mountConfig;
|
|
@@ -1190,6 +1326,8 @@ function mountPages(app, options) {
|
|
|
1190
1326
|
}
|
|
1191
1327
|
|
|
1192
1328
|
const njkTpl = loadNjkRouteTemplate(route.fullPath, isDev);
|
|
1329
|
+
const metaTitle = t('meta.title');
|
|
1330
|
+
const metaDesc = t('meta.description');
|
|
1193
1331
|
|
|
1194
1332
|
const ctx = {
|
|
1195
1333
|
req,
|
|
@@ -1208,8 +1346,8 @@ function mountPages(app, options) {
|
|
|
1208
1346
|
},
|
|
1209
1347
|
data: { ...njkTpl.dataPatch },
|
|
1210
1348
|
meta: {
|
|
1211
|
-
title:
|
|
1212
|
-
description:
|
|
1349
|
+
title: metaTitle !== 'meta.title' ? metaTitle : null,
|
|
1350
|
+
description: metaDesc !== 'meta.description' ? metaDesc : null,
|
|
1213
1351
|
indexable: true,
|
|
1214
1352
|
canonical: null,
|
|
1215
1353
|
...njkTpl.metaPatch,
|
|
@@ -1221,32 +1359,33 @@ function mountPages(app, options) {
|
|
|
1221
1359
|
req.context = ctx;
|
|
1222
1360
|
|
|
1223
1361
|
// Execute hooks: onRequest
|
|
1224
|
-
|
|
1225
|
-
|
|
1362
|
+
let hookPromise = runHooks(globalHooks, routeHooks, 'onRequest', ctx);
|
|
1363
|
+
if (hookPromise) await hookPromise;
|
|
1226
1364
|
|
|
1227
1365
|
// Execute hooks: onRoute
|
|
1228
|
-
|
|
1229
|
-
|
|
1366
|
+
hookPromise = runHooks(globalHooks, routeHooks, 'onRoute', ctx);
|
|
1367
|
+
if (hookPromise) await hookPromise;
|
|
1230
1368
|
|
|
1231
1369
|
// Execute hooks: beforeMiddleware
|
|
1232
|
-
|
|
1233
|
-
|
|
1370
|
+
hookPromise = runHooks(globalHooks, routeHooks, 'beforeMiddleware', ctx);
|
|
1371
|
+
if (hookPromise) await hookPromise;
|
|
1234
1372
|
|
|
1373
|
+
// Run cascading folder middlewares (_middleware.js)
|
|
1374
|
+
const cascadingMw = loadCascadingMiddlewares(absolutePagesDir, route.file, middlewares, isDev);
|
|
1375
|
+
if (cascadingMw.length) {
|
|
1376
|
+
await runMiddlewareChain(cascadingMw, req, res);
|
|
1377
|
+
if (res.headersSent) return;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1235
1380
|
// Run route middleware (chain fixed at route registration; edit middleware in dev → restart)
|
|
1236
1381
|
if (preResolvedPageMw.length) {
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
mw(req, res, (err) => {
|
|
1240
|
-
if (err) reject(err);
|
|
1241
|
-
else resolve();
|
|
1242
|
-
});
|
|
1243
|
-
});
|
|
1244
|
-
}
|
|
1382
|
+
await runMiddlewareChain(preResolvedPageMw, req, res);
|
|
1383
|
+
if (res.headersSent) return;
|
|
1245
1384
|
}
|
|
1246
1385
|
|
|
1247
1386
|
// Execute hooks: afterMiddleware
|
|
1248
|
-
|
|
1249
|
-
|
|
1387
|
+
hookPromise = runHooks(globalHooks, routeHooks, 'afterMiddleware', ctx);
|
|
1388
|
+
if (hookPromise) await hookPromise;
|
|
1250
1389
|
|
|
1251
1390
|
// Execute hooks: beforeLoad
|
|
1252
1391
|
if (pluginManager && db) {
|
|
@@ -1255,9 +1394,17 @@ function mountPages(app, options) {
|
|
|
1255
1394
|
ctx.content = contentApi.getContentService(db);
|
|
1256
1395
|
}
|
|
1257
1396
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1397
|
+
hookPromise = runHooks(globalHooks, routeHooks, 'beforeLoad', ctx);
|
|
1398
|
+
if (hookPromise) await hookPromise;
|
|
1260
1399
|
|
|
1400
|
+
// Find cascading layouts (_layout.njk)
|
|
1401
|
+
const cascadingLayouts = findCascadingLayouts(absolutePagesDir, route.file, isDev);
|
|
1402
|
+
|
|
1403
|
+
// Run cascading _layout.js load() functions in top-down order (outermost first)
|
|
1404
|
+
if (cascadingLayouts.length > 0) {
|
|
1405
|
+
await executeCascadingLayoutLoads(cascadingLayouts, req, ctx, isDev);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1261
1408
|
// Run load function
|
|
1262
1409
|
if (config?.load && typeof config.load === 'function') {
|
|
1263
1410
|
const loadData = await config.load(req, ctx);
|
|
@@ -1267,8 +1414,8 @@ function mountPages(app, options) {
|
|
|
1267
1414
|
}
|
|
1268
1415
|
|
|
1269
1416
|
// Execute hooks: afterLoad
|
|
1270
|
-
|
|
1271
|
-
|
|
1417
|
+
hookPromise = runHooks(globalHooks, routeHooks, 'afterLoad', ctx);
|
|
1418
|
+
if (hookPromise) await hookPromise;
|
|
1272
1419
|
|
|
1273
1420
|
// Run meta function
|
|
1274
1421
|
if (config?.meta && typeof config.meta === 'function') {
|
|
@@ -1279,10 +1426,18 @@ function mountPages(app, options) {
|
|
|
1279
1426
|
}
|
|
1280
1427
|
|
|
1281
1428
|
// Execute hooks: beforeRender
|
|
1282
|
-
|
|
1283
|
-
|
|
1429
|
+
hookPromise = runHooks(globalHooks, routeHooks, 'beforeRender', ctx);
|
|
1430
|
+
if (hookPromise) await hookPromise;
|
|
1284
1431
|
|
|
1285
|
-
const pageAssetBundle = applyPageAssetsToTemplateData(
|
|
1432
|
+
const pageAssetBundle = applyPageAssetsToTemplateData(
|
|
1433
|
+
pageAssetsResolved,
|
|
1434
|
+
ctx.data,
|
|
1435
|
+
{
|
|
1436
|
+
file: route.file,
|
|
1437
|
+
routePath: route.routePath,
|
|
1438
|
+
},
|
|
1439
|
+
ctx.fsy?.assetManager || getAssetManager()
|
|
1440
|
+
);
|
|
1286
1441
|
ctx.data = pageAssetBundle.data;
|
|
1287
1442
|
const renderContext = {
|
|
1288
1443
|
...ctx.data,
|
|
@@ -1316,10 +1471,34 @@ function mountPages(app, options) {
|
|
|
1316
1471
|
});
|
|
1317
1472
|
}
|
|
1318
1473
|
|
|
1319
|
-
let html
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
:
|
|
1474
|
+
let html;
|
|
1475
|
+
if (njkTpl.useStringRender && njkTpl.templateBody != null) {
|
|
1476
|
+
if (typeof nunjucks.Template === 'function') {
|
|
1477
|
+
let compiled = isDev ? null : compiledStringTemplateCache.get(route.fullPath);
|
|
1478
|
+
if (!compiled || (isDev && compiled._templateBody !== njkTpl.templateBody)) {
|
|
1479
|
+
compiled = new nunjucks.Template(njkTpl.templateBody, nunjucks, route.fullPath);
|
|
1480
|
+
compiled._templateBody = njkTpl.templateBody;
|
|
1481
|
+
compiledStringTemplateCache.set(route.fullPath, compiled);
|
|
1482
|
+
}
|
|
1483
|
+
html = compiled.render(renderContext);
|
|
1484
|
+
} else if (typeof nunjucks.renderString === 'function') {
|
|
1485
|
+
html = nunjucks.renderString(njkTpl.templateBody, renderContext, { path: route.fullPath });
|
|
1486
|
+
} else {
|
|
1487
|
+
html = nunjucks.render(templatePath, renderContext);
|
|
1488
|
+
}
|
|
1489
|
+
} else {
|
|
1490
|
+
html = nunjucks.render(templatePath, renderContext);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Apply cascading nested layouts if not opted-out and template does not extend another template
|
|
1494
|
+
const shouldApplyLayout = config?.layout !== false &&
|
|
1495
|
+
ctx.data?.layout !== false &&
|
|
1496
|
+
ctx.meta?.layout !== false &&
|
|
1497
|
+
!templateHasExtends(njkTpl.templateBody, route.fullPath);
|
|
1498
|
+
|
|
1499
|
+
if (shouldApplyLayout && cascadingLayouts.length > 0) {
|
|
1500
|
+
html = applyCascadingLayouts(nunjucks, cascadingLayouts, html, renderContext, isDev);
|
|
1501
|
+
}
|
|
1323
1502
|
|
|
1324
1503
|
if (pluginManager) {
|
|
1325
1504
|
const contentApi = pluginManager.getPluginAPI('content');
|
|
@@ -1330,8 +1509,8 @@ function mountPages(app, options) {
|
|
|
1330
1509
|
|
|
1331
1510
|
// Execute hooks: afterRender
|
|
1332
1511
|
ctx.html = html;
|
|
1333
|
-
|
|
1334
|
-
|
|
1512
|
+
hookPromise = runHooks(globalHooks, routeHooks, 'afterRender', ctx);
|
|
1513
|
+
if (hookPromise) await hookPromise;
|
|
1335
1514
|
|
|
1336
1515
|
if (route.routePath === '/404' || route.file === '404.njk') {
|
|
1337
1516
|
res.status(404);
|
|
@@ -1391,6 +1570,8 @@ module.exports = {
|
|
|
1391
1570
|
mountPages,
|
|
1392
1571
|
filePathToRoute,
|
|
1393
1572
|
extractMethodFromFilename,
|
|
1573
|
+
isRouteGroupSegment,
|
|
1574
|
+
stripRouteGroups,
|
|
1394
1575
|
scanDirectory,
|
|
1395
1576
|
loadI18n,
|
|
1396
1577
|
createTranslator,
|
|
@@ -1404,5 +1585,9 @@ module.exports = {
|
|
|
1404
1585
|
frontmatterToPatches,
|
|
1405
1586
|
loadNjkRouteTemplate,
|
|
1406
1587
|
clearNjkFrontmatterCaches,
|
|
1588
|
+
compiledStringTemplateCache,
|
|
1589
|
+
clearCompiledStringTemplateCache,
|
|
1590
|
+
translatorCache,
|
|
1591
|
+
clearTranslatorCache,
|
|
1407
1592
|
};
|
|
1408
1593
|
|