webspresso 0.0.0
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 +373 -0
- package/bin/webspresso.js +790 -0
- package/index.js +34 -0
- package/package.json +59 -0
- package/src/file-router.js +520 -0
- package/src/helpers.js +275 -0
- package/src/server.js +180 -0
package/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso - Minimal file-based SSR framework for Node.js
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { createApp } = require('./src/server');
|
|
6
|
+
const {
|
|
7
|
+
mountPages,
|
|
8
|
+
filePathToRoute,
|
|
9
|
+
extractMethodFromFilename,
|
|
10
|
+
scanDirectory,
|
|
11
|
+
loadI18n,
|
|
12
|
+
createTranslator,
|
|
13
|
+
detectLocale
|
|
14
|
+
} = require('./src/file-router');
|
|
15
|
+
const { createHelpers, utils } = require('./src/helpers');
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
// Main API
|
|
19
|
+
createApp,
|
|
20
|
+
|
|
21
|
+
// Router utilities (for advanced use)
|
|
22
|
+
mountPages,
|
|
23
|
+
filePathToRoute,
|
|
24
|
+
extractMethodFromFilename,
|
|
25
|
+
scanDirectory,
|
|
26
|
+
loadI18n,
|
|
27
|
+
createTranslator,
|
|
28
|
+
detectLocale,
|
|
29
|
+
|
|
30
|
+
// Template helpers
|
|
31
|
+
createHelpers,
|
|
32
|
+
utils
|
|
33
|
+
};
|
|
34
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "webspresso",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"webspresso": "./bin/webspresso.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"test:watch": "vitest",
|
|
12
|
+
"test:coverage": "vitest run --coverage",
|
|
13
|
+
"release": "release-it"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"ssr",
|
|
17
|
+
"nunjucks",
|
|
18
|
+
"express",
|
|
19
|
+
"file-based-routing",
|
|
20
|
+
"i18n",
|
|
21
|
+
"framework"
|
|
22
|
+
],
|
|
23
|
+
"author": "cond",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/everytools/webspresso.git"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"index.js",
|
|
31
|
+
"bin/",
|
|
32
|
+
"src/"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"commander": "^11.1.0",
|
|
36
|
+
"express": "^4.18.2",
|
|
37
|
+
"inquirer": "^8.2.6",
|
|
38
|
+
"nunjucks": "^3.2.4"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"dotenv": "^16.0.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"dotenv": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@vitest/coverage-v8": "^1.2.0",
|
|
50
|
+
"chokidar": "^3.5.3",
|
|
51
|
+
"dotenv": "^16.3.1",
|
|
52
|
+
"release-it": "^17.11.0",
|
|
53
|
+
"supertest": "^6.3.4",
|
|
54
|
+
"vitest": "^1.2.0"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=18.0.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso File-Based Router
|
|
3
|
+
* Scans pages/ directory and registers routes automatically
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { createHelpers } = require('./helpers');
|
|
9
|
+
|
|
10
|
+
// Cache for i18n files (key: filePath, value: { mtime, data })
|
|
11
|
+
const i18nCache = new Map();
|
|
12
|
+
|
|
13
|
+
// Cache for route configs in production
|
|
14
|
+
const configCache = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert a file path to an Express route pattern
|
|
18
|
+
* @param {string} filePath - Relative path from pages/
|
|
19
|
+
* @param {string} ext - File extension (.njk or .js)
|
|
20
|
+
* @returns {string} Express route pattern
|
|
21
|
+
*/
|
|
22
|
+
function filePathToRoute(filePath, ext) {
|
|
23
|
+
// Remove extension
|
|
24
|
+
let route = filePath.replace(ext, '');
|
|
25
|
+
|
|
26
|
+
// Normalize path separators to forward slashes (handle both / and \)
|
|
27
|
+
route = route.split(path.sep).join('/');
|
|
28
|
+
route = route.split('\\').join('/'); // Also handle literal backslashes
|
|
29
|
+
|
|
30
|
+
// Handle index files
|
|
31
|
+
if (route.endsWith('/index')) {
|
|
32
|
+
route = route.slice(0, -6) || '/';
|
|
33
|
+
} else if (route === 'index') {
|
|
34
|
+
route = '/';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Convert [param] to :param
|
|
38
|
+
route = route.replace(/\[([^\]\.]+)\]/g, ':$1');
|
|
39
|
+
|
|
40
|
+
// Convert [...param] to * (catch-all)
|
|
41
|
+
route = route.replace(/\[\.\.\.([^\]]+)\]/g, '*');
|
|
42
|
+
|
|
43
|
+
// Ensure leading slash
|
|
44
|
+
if (!route.startsWith('/')) {
|
|
45
|
+
route = '/' + route;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return route;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract HTTP method from API filename
|
|
53
|
+
* @param {string} filename - Filename like health.get.js
|
|
54
|
+
* @returns {{ method: string, baseName: string }}
|
|
55
|
+
*/
|
|
56
|
+
function extractMethodFromFilename(filename) {
|
|
57
|
+
const methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
|
|
58
|
+
const parts = filename.replace('.js', '').split('.');
|
|
59
|
+
|
|
60
|
+
if (parts.length > 1) {
|
|
61
|
+
const lastPart = parts[parts.length - 1].toLowerCase();
|
|
62
|
+
if (methods.includes(lastPart)) {
|
|
63
|
+
return {
|
|
64
|
+
method: lastPart,
|
|
65
|
+
baseName: parts.slice(0, -1).join('.')
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { method: 'get', baseName: parts.join('.') };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Recursively scan a directory for files
|
|
75
|
+
* @param {string} dir - Directory to scan
|
|
76
|
+
* @param {string} baseDir - Base directory for relative paths
|
|
77
|
+
* @returns {string[]} Array of relative file paths
|
|
78
|
+
*/
|
|
79
|
+
function scanDirectory(dir, baseDir = dir) {
|
|
80
|
+
const files = [];
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(dir)) {
|
|
83
|
+
return files;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
87
|
+
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
const fullPath = path.join(dir, entry.name);
|
|
90
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
91
|
+
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
// Skip locales directories
|
|
94
|
+
if (entry.name === 'locales') continue;
|
|
95
|
+
files.push(...scanDirectory(fullPath, baseDir));
|
|
96
|
+
} else if (entry.isFile()) {
|
|
97
|
+
// Skip files starting with _
|
|
98
|
+
if (entry.name.startsWith('_')) continue;
|
|
99
|
+
files.push(relativePath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return files;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load i18n JSON file with caching
|
|
108
|
+
* @param {string} filePath - Path to JSON file
|
|
109
|
+
* @returns {Object} Parsed JSON or empty object
|
|
110
|
+
*/
|
|
111
|
+
function loadI18nFile(filePath) {
|
|
112
|
+
if (!fs.existsSync(filePath)) {
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const stats = fs.statSync(filePath);
|
|
118
|
+
const cached = i18nCache.get(filePath);
|
|
119
|
+
|
|
120
|
+
if (cached && cached.mtime >= stats.mtimeMs) {
|
|
121
|
+
return cached.data;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
125
|
+
const data = JSON.parse(content);
|
|
126
|
+
|
|
127
|
+
i18nCache.set(filePath, { mtime: stats.mtimeMs, data });
|
|
128
|
+
return data;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(`Error loading i18n file ${filePath}:`, err.message);
|
|
131
|
+
return {};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Load merged i18n translations for a route
|
|
137
|
+
* @param {string} pagesDir - Pages directory path
|
|
138
|
+
* @param {string} routeDir - Route directory path
|
|
139
|
+
* @param {string} locale - Locale code
|
|
140
|
+
* @returns {Object} Merged translations
|
|
141
|
+
*/
|
|
142
|
+
function loadI18n(pagesDir, routeDir, locale) {
|
|
143
|
+
// Load global translations
|
|
144
|
+
const globalPath = path.join(pagesDir, 'locales', `${locale}.json`);
|
|
145
|
+
const globalTranslations = loadI18nFile(globalPath);
|
|
146
|
+
|
|
147
|
+
// Load route-specific translations
|
|
148
|
+
const routePath = path.join(routeDir, 'locales', `${locale}.json`);
|
|
149
|
+
const routeTranslations = loadI18nFile(routePath);
|
|
150
|
+
|
|
151
|
+
// Merge: route-specific overrides global
|
|
152
|
+
return { ...globalTranslations, ...routeTranslations };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create a translation function
|
|
157
|
+
* @param {Object} translations - Translation object
|
|
158
|
+
* @returns {Function} Translation function t(key)
|
|
159
|
+
*/
|
|
160
|
+
function createTranslator(translations) {
|
|
161
|
+
return function t(key, params = {}) {
|
|
162
|
+
let value = translations[key];
|
|
163
|
+
|
|
164
|
+
if (value === undefined) {
|
|
165
|
+
// Try nested key lookup (e.g., "meta.title")
|
|
166
|
+
const parts = key.split('.');
|
|
167
|
+
value = translations;
|
|
168
|
+
for (const part of parts) {
|
|
169
|
+
if (value && typeof value === 'object') {
|
|
170
|
+
value = value[part];
|
|
171
|
+
} else {
|
|
172
|
+
value = undefined;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (value === undefined) {
|
|
179
|
+
return key; // Return key if translation not found
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Replace params like {{name}} in the translation
|
|
183
|
+
if (typeof value === 'string' && Object.keys(params).length > 0) {
|
|
184
|
+
for (const [paramKey, paramValue] of Object.entries(params)) {
|
|
185
|
+
value = value.replace(new RegExp(`{{\\s*${paramKey}\\s*}}`, 'g'), paramValue);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return value;
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Load route config module
|
|
195
|
+
* @param {string} configPath - Path to config .js file
|
|
196
|
+
* @param {boolean} isDev - Is development mode
|
|
197
|
+
* @returns {Object|null} Route config or null
|
|
198
|
+
*/
|
|
199
|
+
function loadRouteConfig(configPath, isDev) {
|
|
200
|
+
if (!fs.existsSync(configPath)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
// Clear cache in development mode
|
|
206
|
+
if (isDev && require.cache[require.resolve(configPath)]) {
|
|
207
|
+
delete require.cache[require.resolve(configPath)];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!isDev && configCache.has(configPath)) {
|
|
211
|
+
return configCache.get(configPath);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const config = require(configPath);
|
|
215
|
+
|
|
216
|
+
if (!isDev) {
|
|
217
|
+
configCache.set(configPath, config);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return config;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.error(`Error loading route config ${configPath}:`, err.message);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Load global hooks
|
|
229
|
+
* @param {string} pagesDir - Pages directory path
|
|
230
|
+
* @param {boolean} isDev - Is development mode
|
|
231
|
+
* @returns {Object|null} Global hooks or null
|
|
232
|
+
*/
|
|
233
|
+
function loadGlobalHooks(pagesDir, isDev) {
|
|
234
|
+
const hooksPath = path.join(pagesDir, '_hooks.js');
|
|
235
|
+
return loadRouteConfig(hooksPath, isDev);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Execute a hook if it exists
|
|
240
|
+
* @param {Object} hooks - Hooks object
|
|
241
|
+
* @param {string} hookName - Hook name
|
|
242
|
+
* @param {Object} ctx - Context object
|
|
243
|
+
*/
|
|
244
|
+
async function executeHook(hooks, hookName, ctx) {
|
|
245
|
+
if (hooks && typeof hooks[hookName] === 'function') {
|
|
246
|
+
await hooks[hookName](ctx);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Detect locale from request
|
|
252
|
+
* @param {Object} req - Express request
|
|
253
|
+
* @returns {string} Locale code
|
|
254
|
+
*/
|
|
255
|
+
function detectLocale(req) {
|
|
256
|
+
// 1. Check query parameter
|
|
257
|
+
if (req.query.lang) {
|
|
258
|
+
return req.query.lang;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 2. Check Accept-Language header
|
|
262
|
+
const acceptLanguage = req.get('Accept-Language');
|
|
263
|
+
if (acceptLanguage) {
|
|
264
|
+
const supported = (process.env.SUPPORTED_LOCALES || 'en').split(',');
|
|
265
|
+
const preferred = acceptLanguage.split(',')[0].split('-')[0].toLowerCase();
|
|
266
|
+
if (supported.includes(preferred)) {
|
|
267
|
+
return preferred;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 3. Default locale
|
|
272
|
+
return process.env.DEFAULT_LOCALE || 'en';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Mount all pages as routes on the Express app
|
|
277
|
+
* @param {Object} app - Express app
|
|
278
|
+
* @param {Object} options - Options
|
|
279
|
+
* @param {string} options.pagesDir - Pages directory path
|
|
280
|
+
* @param {Object} options.nunjucks - Nunjucks environment
|
|
281
|
+
* @param {boolean} options.silent - Suppress console output
|
|
282
|
+
*/
|
|
283
|
+
function mountPages(app, options) {
|
|
284
|
+
const { pagesDir, nunjucks, silent = false } = options;
|
|
285
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
286
|
+
const log = silent ? () => {} : console.log.bind(console);
|
|
287
|
+
|
|
288
|
+
// Get absolute path to pages directory
|
|
289
|
+
const absolutePagesDir = path.resolve(pagesDir);
|
|
290
|
+
|
|
291
|
+
// Load global hooks
|
|
292
|
+
const globalHooks = loadGlobalHooks(absolutePagesDir, isDev);
|
|
293
|
+
|
|
294
|
+
// Scan for files
|
|
295
|
+
const files = scanDirectory(absolutePagesDir);
|
|
296
|
+
|
|
297
|
+
// Separate SSR pages and API routes
|
|
298
|
+
const ssrRoutes = [];
|
|
299
|
+
const apiRoutes = [];
|
|
300
|
+
|
|
301
|
+
for (const file of files) {
|
|
302
|
+
const ext = path.extname(file);
|
|
303
|
+
const isApi = file.startsWith('api' + path.sep) || file.startsWith('api/');
|
|
304
|
+
|
|
305
|
+
if (isApi && ext === '.js') {
|
|
306
|
+
// API route
|
|
307
|
+
const { method, baseName } = extractMethodFromFilename(path.basename(file));
|
|
308
|
+
const dirPart = path.dirname(file);
|
|
309
|
+
const routePath = dirPart === '.'
|
|
310
|
+
? `/${baseName}`
|
|
311
|
+
: `/${dirPart}/${baseName}`.split(path.sep).join('/');
|
|
312
|
+
|
|
313
|
+
apiRoutes.push({
|
|
314
|
+
file,
|
|
315
|
+
method,
|
|
316
|
+
routePath: filePathToRoute(routePath.replace(/^\/api/, '/api'), ''),
|
|
317
|
+
fullPath: path.join(absolutePagesDir, file)
|
|
318
|
+
});
|
|
319
|
+
} else if (ext === '.njk') {
|
|
320
|
+
// SSR page
|
|
321
|
+
const routePath = filePathToRoute(file, '.njk');
|
|
322
|
+
const configPath = path.join(absolutePagesDir, file.replace('.njk', '.js'));
|
|
323
|
+
const routeDir = path.dirname(path.join(absolutePagesDir, file));
|
|
324
|
+
|
|
325
|
+
ssrRoutes.push({
|
|
326
|
+
file,
|
|
327
|
+
routePath,
|
|
328
|
+
fullPath: path.join(absolutePagesDir, file),
|
|
329
|
+
configPath,
|
|
330
|
+
routeDir
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Sort routes: specific routes first, then dynamic, then catch-all
|
|
336
|
+
const sortRoutes = (routes) => {
|
|
337
|
+
return routes.sort((a, b) => {
|
|
338
|
+
const aHasCatchAll = a.routePath.includes('*');
|
|
339
|
+
const bHasCatchAll = b.routePath.includes('*');
|
|
340
|
+
const aHasDynamic = a.routePath.includes(':');
|
|
341
|
+
const bHasDynamic = b.routePath.includes(':');
|
|
342
|
+
|
|
343
|
+
if (aHasCatchAll && !bHasCatchAll) return 1;
|
|
344
|
+
if (!aHasCatchAll && bHasCatchAll) return -1;
|
|
345
|
+
if (aHasDynamic && !bHasDynamic) return 1;
|
|
346
|
+
if (!aHasDynamic && bHasDynamic) return -1;
|
|
347
|
+
|
|
348
|
+
return a.routePath.localeCompare(b.routePath);
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// Register API routes
|
|
353
|
+
for (const route of sortRoutes(apiRoutes)) {
|
|
354
|
+
const handler = require(route.fullPath);
|
|
355
|
+
const handlerFn = typeof handler === 'function' ? handler : handler.default || handler.handler;
|
|
356
|
+
|
|
357
|
+
if (typeof handlerFn !== 'function') {
|
|
358
|
+
console.warn(`API route ${route.file} does not export a function`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
app[route.method](route.routePath, async (req, res, next) => {
|
|
363
|
+
try {
|
|
364
|
+
// Reload handler in dev mode
|
|
365
|
+
if (isDev && require.cache[require.resolve(route.fullPath)]) {
|
|
366
|
+
delete require.cache[require.resolve(route.fullPath)];
|
|
367
|
+
}
|
|
368
|
+
const currentHandler = isDev
|
|
369
|
+
? require(route.fullPath)
|
|
370
|
+
: handler;
|
|
371
|
+
const fn = typeof currentHandler === 'function'
|
|
372
|
+
? currentHandler
|
|
373
|
+
: currentHandler.default || currentHandler.handler;
|
|
374
|
+
|
|
375
|
+
await fn(req, res, next);
|
|
376
|
+
} catch (err) {
|
|
377
|
+
console.error(`API error ${route.routePath}:`, err);
|
|
378
|
+
res.status(500).json({ error: 'Internal Server Error' });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
log(` ${route.method.toUpperCase()} ${route.routePath} -> ${route.file}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Register SSR routes
|
|
386
|
+
for (const route of sortRoutes(ssrRoutes)) {
|
|
387
|
+
app.get(route.routePath, async (req, res, next) => {
|
|
388
|
+
try {
|
|
389
|
+
// Detect locale
|
|
390
|
+
const locale = detectLocale(req);
|
|
391
|
+
|
|
392
|
+
// Load translations
|
|
393
|
+
const translations = loadI18n(absolutePagesDir, route.routeDir, locale);
|
|
394
|
+
const t = createTranslator(translations);
|
|
395
|
+
|
|
396
|
+
// Load route config
|
|
397
|
+
const config = loadRouteConfig(route.configPath, isDev);
|
|
398
|
+
const routeHooks = config?.hooks || {};
|
|
399
|
+
|
|
400
|
+
// Create context
|
|
401
|
+
const ctx = {
|
|
402
|
+
req,
|
|
403
|
+
res,
|
|
404
|
+
path: route.routePath,
|
|
405
|
+
file: route.file,
|
|
406
|
+
routeDir: route.routeDir,
|
|
407
|
+
locale,
|
|
408
|
+
t,
|
|
409
|
+
data: {},
|
|
410
|
+
meta: {
|
|
411
|
+
title: t('meta.title') !== 'meta.title' ? t('meta.title') : null,
|
|
412
|
+
description: t('meta.description') !== 'meta.description' ? t('meta.description') : null,
|
|
413
|
+
indexable: true,
|
|
414
|
+
canonical: null
|
|
415
|
+
},
|
|
416
|
+
fsy: createHelpers({ req, res, locale })
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// Execute hooks: onRequest
|
|
420
|
+
await executeHook(globalHooks, 'onRequest', ctx);
|
|
421
|
+
await executeHook(routeHooks, 'onRequest', ctx);
|
|
422
|
+
|
|
423
|
+
// Execute hooks: onRoute
|
|
424
|
+
await executeHook(globalHooks, 'onRoute', ctx);
|
|
425
|
+
await executeHook(routeHooks, 'onRoute', ctx);
|
|
426
|
+
|
|
427
|
+
// Execute hooks: beforeMiddleware
|
|
428
|
+
await executeHook(globalHooks, 'beforeMiddleware', ctx);
|
|
429
|
+
await executeHook(routeHooks, 'beforeMiddleware', ctx);
|
|
430
|
+
|
|
431
|
+
// Run route middleware
|
|
432
|
+
if (config?.middleware && Array.isArray(config.middleware)) {
|
|
433
|
+
for (const mw of config.middleware) {
|
|
434
|
+
await new Promise((resolve, reject) => {
|
|
435
|
+
mw(req, res, (err) => {
|
|
436
|
+
if (err) reject(err);
|
|
437
|
+
else resolve();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Execute hooks: afterMiddleware
|
|
444
|
+
await executeHook(globalHooks, 'afterMiddleware', ctx);
|
|
445
|
+
await executeHook(routeHooks, 'afterMiddleware', ctx);
|
|
446
|
+
|
|
447
|
+
// Execute hooks: beforeLoad
|
|
448
|
+
await executeHook(globalHooks, 'beforeLoad', ctx);
|
|
449
|
+
await executeHook(routeHooks, 'beforeLoad', ctx);
|
|
450
|
+
|
|
451
|
+
// Run load function
|
|
452
|
+
if (config?.load && typeof config.load === 'function') {
|
|
453
|
+
const loadData = await config.load(req, ctx);
|
|
454
|
+
ctx.data = { ...ctx.data, ...loadData };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Execute hooks: afterLoad
|
|
458
|
+
await executeHook(globalHooks, 'afterLoad', ctx);
|
|
459
|
+
await executeHook(routeHooks, 'afterLoad', ctx);
|
|
460
|
+
|
|
461
|
+
// Run meta function
|
|
462
|
+
if (config?.meta && typeof config.meta === 'function') {
|
|
463
|
+
const metaData = await config.meta(req, ctx);
|
|
464
|
+
ctx.meta = { ...ctx.meta, ...metaData };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Execute hooks: beforeRender
|
|
468
|
+
await executeHook(globalHooks, 'beforeRender', ctx);
|
|
469
|
+
await executeHook(routeHooks, 'beforeRender', ctx);
|
|
470
|
+
|
|
471
|
+
// Render the template
|
|
472
|
+
const templatePath = route.file.split(path.sep).join('/');
|
|
473
|
+
const html = nunjucks.render(templatePath, {
|
|
474
|
+
...ctx.data,
|
|
475
|
+
meta: ctx.meta,
|
|
476
|
+
locale: ctx.locale,
|
|
477
|
+
t: ctx.t,
|
|
478
|
+
fsy: ctx.fsy,
|
|
479
|
+
req: {
|
|
480
|
+
path: req.path,
|
|
481
|
+
query: req.query,
|
|
482
|
+
params: req.params
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Execute hooks: afterRender
|
|
487
|
+
ctx.html = html;
|
|
488
|
+
await executeHook(globalHooks, 'afterRender', ctx);
|
|
489
|
+
await executeHook(routeHooks, 'afterRender', ctx);
|
|
490
|
+
|
|
491
|
+
res.send(ctx.html);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
console.error(`SSR error ${route.routePath}:`, err);
|
|
494
|
+
|
|
495
|
+
// Execute onError hook
|
|
496
|
+
const ctx = { req, res, error: err };
|
|
497
|
+
try {
|
|
498
|
+
await executeHook(globalHooks, 'onError', ctx, err);
|
|
499
|
+
} catch (hookErr) {
|
|
500
|
+
console.error('Error in onError hook:', hookErr);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
res.status(500).send('Internal Server Error');
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
log(` GET ${route.routePath} -> ${route.file}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
module.exports = {
|
|
512
|
+
mountPages,
|
|
513
|
+
filePathToRoute,
|
|
514
|
+
extractMethodFromFilename,
|
|
515
|
+
scanDirectory,
|
|
516
|
+
loadI18n,
|
|
517
|
+
createTranslator,
|
|
518
|
+
detectLocale
|
|
519
|
+
};
|
|
520
|
+
|