webspresso 0.0.95 → 0.0.97

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 (94) hide show
  1. package/.agents/ADMIN.md +225 -0
  2. package/.agents/AGENTS.md +72 -0
  3. package/.agents/ARCHITECTURE.md +119 -0
  4. package/.agents/AUTH.md +103 -0
  5. package/.agents/CLI.md +115 -0
  6. package/.agents/COMPRESSION.md +59 -0
  7. package/.agents/CONVENTIONS.md +46 -0
  8. package/.agents/EMAIL.md +141 -0
  9. package/.agents/ERRORS.md +201 -0
  10. package/.agents/MCP.md +103 -0
  11. package/.agents/ORM.md +230 -0
  12. package/.agents/PLUGINS.md +419 -0
  13. package/.agents/QUEUE.md +113 -0
  14. package/.agents/REALTIME.md +213 -0
  15. package/.agents/ROUTING.md +292 -0
  16. package/.agents/SERVICES.md +341 -0
  17. package/.agents/SHUTDOWN.md +67 -0
  18. package/.agents/STREAMING.md +73 -0
  19. package/.agents/WORKFLOW.md +27 -0
  20. package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
  21. package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
  22. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
  23. package/.agents/skills/webspresso-framework/SKILL.md +56 -0
  24. package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
  25. package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
  26. package/.agents/skills/webspresso-services/SKILL.md +101 -0
  27. package/.cursorrules +14 -0
  28. package/AGENTS.md +175 -0
  29. package/CLAUDE.md +54 -0
  30. package/README.md +18 -14
  31. package/bin/commands/admin-password.js +2 -1
  32. package/bin/commands/admin-setup.js +1 -1
  33. package/bin/commands/agents-init.js +209 -0
  34. package/bin/commands/api.js +114 -48
  35. package/bin/commands/audit-prune.js +1 -1
  36. package/bin/commands/doctor.js +35 -0
  37. package/bin/commands/favicon-generate.js +1 -1
  38. package/bin/commands/new.js +13 -2
  39. package/bin/commands/orm-map.js +5 -4
  40. package/bin/commands/page.js +75 -89
  41. package/bin/commands/polar-migrate.js +1 -1
  42. package/bin/commands/seed.js +1 -1
  43. package/bin/commands/service.js +77 -0
  44. package/bin/webspresso.js +2 -2
  45. package/core/auth/hash.js +10 -5
  46. package/core/compileSchema.js +1 -1
  47. package/core/errors/middleware.js +22 -2
  48. package/core/errors/normalize.js +29 -2
  49. package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
  50. package/index.d.ts +306 -0
  51. package/index.js +35 -1
  52. package/package.json +36 -14
  53. package/plugins/admin-panel/app.js +10 -9
  54. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
  55. package/plugins/admin-panel/core/api-extensions.js +4 -0
  56. package/plugins/basic-auth/index.js +3 -1
  57. package/plugins/content/client/inline-edit.js +12 -12
  58. package/plugins/cors.js +6 -7
  59. package/plugins/csrf/index.js +3 -2
  60. package/plugins/csv/engine.js +447 -0
  61. package/plugins/csv/index.js +94 -0
  62. package/plugins/csv/middleware.js +80 -0
  63. package/plugins/csv/services.js +138 -0
  64. package/plugins/file-manager/admin-component.js +613 -0
  65. package/plugins/file-manager/api-handlers.js +261 -0
  66. package/plugins/file-manager/core.js +601 -0
  67. package/plugins/file-manager/index.js +139 -0
  68. package/plugins/file-manager/security.js +217 -0
  69. package/plugins/index.js +45 -79
  70. package/plugins/mcp/transports/sse.js +4 -2
  71. package/plugins/site-analytics/admin-component.js +7 -8
  72. package/plugins/xlsx/engine.js +397 -0
  73. package/plugins/xlsx/index.js +91 -0
  74. package/plugins/xlsx/middleware.js +87 -0
  75. package/plugins/xlsx/services.js +118 -0
  76. package/src/api/api-loader.js +233 -0
  77. package/src/api/define-api.js +35 -0
  78. package/src/discovery/file-route-parser.js +201 -0
  79. package/src/discovery/scan-routes.js +265 -0
  80. package/src/file-router.js +9 -1
  81. package/src/modules/define-module.js +99 -0
  82. package/src/modules/module-discovery.js +117 -0
  83. package/src/pages/define-page.js +36 -0
  84. package/src/pages/page-loader.js +214 -0
  85. package/src/routing/mount-discovered-routes.js +78 -0
  86. package/src/routing/route-table.js +191 -0
  87. package/src/server.js +112 -15
  88. package/src/services/builtins/file-manager.js +178 -0
  89. package/src/services/discovery.js +6 -1
  90. package/src/services/index.js +6 -11
  91. package/bin/commands/skill.js +0 -187
  92. package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
  93. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
  94. package/templates/skills/webspresso-usage/SKILL.md +0 -48
@@ -0,0 +1,265 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Route Discovery Scanner
5
+ * Scans pages, api, and module directories to produce raw route descriptors
6
+ * @module src/discovery/scan-routes
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { parseFileRoute, isPrivateOrIgnored } = require('./file-route-parser');
12
+ const { discoverModuleDetails } = require('../modules/module-discovery');
13
+
14
+ const SUPPORTED_EXTENSIONS = new Set(['.js', '.njk']);
15
+
16
+ /**
17
+ * Safely scans a directory recursively for route files
18
+ * @param {string} dirPath - Absolute directory path
19
+ * @param {string} [baseDir=dirPath] - Base directory for computing relative paths
20
+ * @returns {Array<{ absolutePath: string, relativePath: string }>}
21
+ */
22
+ function scanDirSafely(dirPath, baseDir = dirPath) {
23
+ const results = [];
24
+ if (!fs.existsSync(dirPath)) return results;
25
+
26
+ try {
27
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
28
+
29
+ for (const entry of entries) {
30
+ const name = entry.name;
31
+ if (isPrivateOrIgnored(name)) continue;
32
+
33
+ const fullPath = path.join(dirPath, name);
34
+ const relPath = path.relative(baseDir, fullPath);
35
+
36
+ if (entry.isDirectory()) {
37
+ results.push(...scanDirSafely(fullPath, baseDir));
38
+ } else if (entry.isFile()) {
39
+ const ext = path.extname(name);
40
+ if (SUPPORTED_EXTENSIONS.has(ext)) {
41
+ results.push({
42
+ absolutePath: fullPath,
43
+ relativePath: relPath,
44
+ });
45
+ }
46
+ }
47
+ }
48
+ } catch (err) {
49
+ console.warn(`[webspresso] Failed to scan directory ${dirPath}:`, err.message);
50
+ }
51
+
52
+ return results;
53
+ }
54
+
55
+ /**
56
+ * Scans configured pages, api, and module directories
57
+ * @param {Object} options
58
+ * @param {string} [options.rootDir=process.cwd()] - Project root directory
59
+ * @param {boolean|Object} [options.pages] - Pages configuration
60
+ * @param {boolean|Object} [options.api] - API configuration
61
+ * @param {boolean|Object} [options.modules] - Modules configuration
62
+ * @returns {Array<Object>} Discovered route descriptors
63
+ */
64
+ function scanRoutes(options = {}) {
65
+ const rootDir = options.rootDir || process.cwd();
66
+ const descriptors = [];
67
+
68
+ // Helper to resolve directory paths
69
+ function resolveDirCandidate(candidates) {
70
+ for (const cand of candidates) {
71
+ const p = path.isAbsolute(cand) ? cand : path.join(rootDir, cand);
72
+ if (fs.existsSync(p)) return p;
73
+ }
74
+ return null;
75
+ }
76
+
77
+ // 1. Scan Global Pages
78
+ const pagesOpt = options.pages;
79
+ if (pagesOpt !== false) {
80
+ const explicitPagesDir = typeof pagesOpt === 'object' && pagesOpt.dir ? pagesOpt.dir : null;
81
+ const pagesPrefix = typeof pagesOpt === 'object' && pagesOpt.prefix ? pagesOpt.prefix : '';
82
+ const pagesDir = explicitPagesDir
83
+ ? (path.isAbsolute(explicitPagesDir) ? explicitPagesDir : path.join(rootDir, explicitPagesDir))
84
+ : resolveDirCandidate(['src/pages', 'pages']);
85
+
86
+ if (pagesDir && fs.existsSync(pagesDir)) {
87
+ const files = scanDirSafely(pagesDir);
88
+ const filesSet = new Set(files.map((f) => f.absolutePath));
89
+
90
+ for (const { absolutePath, relativePath } of files) {
91
+ // Skip .njk templates (handled by classic Nunjucks mountPages)
92
+ if (relativePath.endsWith('.njk')) {
93
+ continue;
94
+ }
95
+
96
+ // Skip non-js files
97
+ if (!relativePath.endsWith('.js')) {
98
+ continue;
99
+ }
100
+
101
+ // Skip api directory inside pages (handled by classic mountPages or explicit api scan)
102
+ const isUnderApi = relativePath.startsWith('api' + path.sep) || relativePath.startsWith('api/');
103
+ if (isUnderApi) {
104
+ continue;
105
+ }
106
+
107
+ // If a companion .njk file exists, this .js is a data loader for mountPages, not a standalone definePage route
108
+ const companionNjk = absolutePath.replace(/\.js$/, '.njk');
109
+ if (filesSet.has(companionNjk)) {
110
+ continue;
111
+ }
112
+
113
+ // Parse as standalone page route
114
+ const parsed = parseFileRoute(relativePath, {
115
+ type: 'page',
116
+ prefix: pagesPrefix,
117
+ });
118
+ if (parsed.isValid) {
119
+ descriptors.push({
120
+ type: 'page',
121
+ method: parsed.method,
122
+ path: parsed.path,
123
+ file: absolutePath,
124
+ source: path.relative(rootDir, absolutePath),
125
+ module: null,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ // 2. Scan Global API (e.g. src/api or api)
133
+ const apiOpt = options.api;
134
+ if (apiOpt !== false) {
135
+ const explicitApiDir = typeof apiOpt === 'object' && apiOpt.dir ? apiOpt.dir : null;
136
+ const apiPrefix = typeof apiOpt === 'object' && apiOpt.prefix ? apiOpt.prefix : '/api';
137
+ const apiDir = explicitApiDir
138
+ ? (path.isAbsolute(explicitApiDir) ? explicitApiDir : path.join(rootDir, explicitApiDir))
139
+ : resolveDirCandidate(['src/api', 'api']);
140
+
141
+ if (apiDir && fs.existsSync(apiDir)) {
142
+ const files = scanDirSafely(apiDir);
143
+ for (const { absolutePath, relativePath } of files) {
144
+ const parsed = parseFileRoute(relativePath, {
145
+ type: 'api',
146
+ prefix: apiPrefix,
147
+ });
148
+ if (parsed.isValid) {
149
+ descriptors.push({
150
+ type: 'api',
151
+ method: parsed.method,
152
+ path: parsed.path,
153
+ file: absolutePath,
154
+ source: path.relative(rootDir, absolutePath),
155
+ module: null,
156
+ });
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ // 3. Scan Modules (e.g. src/modules or modules)
163
+ const modulesOpt = options.modules;
164
+ if (modulesOpt !== false) {
165
+ const explicitModulesDir = typeof modulesOpt === 'object' && modulesOpt.dir ? modulesOpt.dir : null;
166
+ const modulesDir = explicitModulesDir
167
+ ? (path.isAbsolute(explicitModulesDir) ? explicitModulesDir : path.join(rootDir, explicitModulesDir))
168
+ : resolveDirCandidate(['src/modules', 'modules']);
169
+
170
+ if (modulesDir && fs.existsSync(modulesDir)) {
171
+ const entries = fs.readdirSync(modulesDir, { withFileTypes: true });
172
+
173
+ for (const entry of entries) {
174
+ if (!entry.isDirectory() || isPrivateOrIgnored(entry.name)) continue;
175
+
176
+ const moduleName = entry.name;
177
+ const modulePath = path.join(modulesDir, moduleName);
178
+
179
+ const details = discoverModuleDetails(modulePath, moduleName);
180
+ const moduleConfig = Object.assign({}, details.config, {
181
+ middlewares: details.middlewares,
182
+ services: details.services,
183
+ });
184
+
185
+ // Module Pages
186
+ const modPagesConfig = moduleConfig?.pages;
187
+ if (modPagesConfig !== false) {
188
+ const modPagesDirName = typeof modPagesConfig === 'object' && modPagesConfig.dir ? modPagesConfig.dir : 'pages';
189
+ const modPagesPrefix = typeof modPagesConfig === 'object' && modPagesConfig.prefix !== undefined
190
+ ? modPagesConfig.prefix
191
+ : `/${moduleName}`;
192
+ const modPagesDir = path.isAbsolute(modPagesDirName)
193
+ ? modPagesDirName
194
+ : path.join(modulePath, modPagesDirName);
195
+
196
+ if (fs.existsSync(modPagesDir)) {
197
+ const files = scanDirSafely(modPagesDir);
198
+ const modFilesSet = new Set(files.map((f) => f.absolutePath));
199
+
200
+ for (const { absolutePath, relativePath } of files) {
201
+ if (!relativePath.endsWith('.js')) continue;
202
+ if (modFilesSet.has(absolutePath.replace(/\.js$/, '.njk'))) continue;
203
+
204
+ const parsed = parseFileRoute(relativePath, {
205
+ type: 'page',
206
+ prefix: modPagesPrefix,
207
+ });
208
+ if (parsed.isValid) {
209
+ descriptors.push({
210
+ type: 'page',
211
+ method: parsed.method,
212
+ path: parsed.path,
213
+ file: absolutePath,
214
+ source: path.relative(rootDir, absolutePath),
215
+ module: moduleName,
216
+ moduleConfig,
217
+ });
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ // Module API
224
+ const modApiConfig = moduleConfig?.api;
225
+ if (modApiConfig !== false) {
226
+ const modApiDirName = typeof modApiConfig === 'object' && modApiConfig.dir ? modApiConfig.dir : 'api';
227
+ const modApiPrefix = typeof modApiConfig === 'object' && modApiConfig.prefix !== undefined
228
+ ? modApiConfig.prefix
229
+ : `/api/${moduleName}`;
230
+ const modApiDir = path.isAbsolute(modApiDirName)
231
+ ? modApiDirName
232
+ : path.join(modulePath, modApiDirName);
233
+
234
+ if (fs.existsSync(modApiDir)) {
235
+ const files = scanDirSafely(modApiDir);
236
+ for (const { absolutePath, relativePath } of files) {
237
+ const parsed = parseFileRoute(relativePath, {
238
+ type: 'api',
239
+ prefix: modApiPrefix,
240
+ });
241
+ if (parsed.isValid) {
242
+ descriptors.push({
243
+ type: 'api',
244
+ method: parsed.method,
245
+ path: parsed.path,
246
+ file: absolutePath,
247
+ source: path.relative(rootDir, absolutePath),
248
+ module: moduleName,
249
+ moduleConfig,
250
+ });
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ return descriptors;
260
+ }
261
+
262
+ module.exports = {
263
+ scanRoutes,
264
+ scanDirSafely,
265
+ };
@@ -753,7 +753,9 @@ function detectLocale(req) {
753
753
  }
754
754
  }
755
755
 
756
- const acceptLanguage = req.get('Accept-Language');
756
+ const acceptLanguage = typeof req.get === 'function'
757
+ ? req.get('Accept-Language')
758
+ : (req.headers && (req.headers['accept-language'] || req.headers['Accept-Language']));
757
759
  if (acceptLanguage) {
758
760
  const langPart = acceptLanguage.split(',')[0];
759
761
  const a = normalizeLocaleCandidate(langPart);
@@ -1026,6 +1028,9 @@ function mountPages(app, options) {
1026
1028
  /** Register API routes (shared by static phase and dynamic phase). */
1027
1029
  const registerApiRoutes = (routes) => {
1028
1030
  for (const route of routes) {
1031
+ if (options.skipExistingRoutes && options.skipExistingRoutes.has(route.method || 'GET', route.routePath)) {
1032
+ continue;
1033
+ }
1029
1034
  const handler = require(route.fullPath);
1030
1035
  const handlerFn = typeof handler === 'function' ? handler : handler.default || handler.handler;
1031
1036
  const routeMiddleware = handler.middleware;
@@ -1134,6 +1139,9 @@ function mountPages(app, options) {
1134
1139
  /** Register SSR GET routes (shared by static phase and dynamic phase). */
1135
1140
  const registerSsrRoutes = (routes) => {
1136
1141
  for (const route of routes) {
1142
+ if (options.skipExistingRoutes && options.skipExistingRoutes.has('GET', route.routePath)) {
1143
+ continue;
1144
+ }
1137
1145
  const mountConfig = loadRouteConfig(route.configPath, isDev);
1138
1146
  const preResolvedPageMw = mountConfig?.middleware
1139
1147
  ? resolveMiddlewares(mountConfig.middleware, middlewares)
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Module Definition Helper
5
+ * Creates feature-oriented modules compatible with Webspresso PluginManager
6
+ * @module src/modules/define-module
7
+ */
8
+
9
+ /**
10
+ * Defines a feature module with pages, api, services, middlewares, and lifecycle hooks
11
+ * @param {Object} config
12
+ * @param {string} config.name - Module unique name (e.g. 'auth', 'shop')
13
+ * @param {string} [config.version='1.0.0'] - Module version
14
+ * @param {string} [config.description] - Module description
15
+ * @param {Array<string>} [config.imports] - Dependent module names
16
+ * @param {Array<string>} [config.dependencies] - Alias for imports
17
+ * @param {boolean|Object} [config.pages] - Pages configuration ({ dir, prefix })
18
+ * @param {boolean|Object} [config.api] - API configuration ({ dir, prefix })
19
+ * @param {Object} [config.services] - Registered service definitions
20
+ * @param {Object} [config.middlewares] - Module-local named middleware functions
21
+ * @param {Object} [config.exports] - Public API methods
22
+ * @param {Object} [config.api] - Public API alias
23
+ * @param {Function} [config.onInit] - Initialization lifecycle hook
24
+ * @param {Function} [config.onDestroy] - Destruction lifecycle hook
25
+ * @returns {Function} Module factory function returning a Webspresso plugin object
26
+ */
27
+ function defineModule(config = {}) {
28
+ const {
29
+ name,
30
+ version = '1.0.0',
31
+ description = '',
32
+ imports = [],
33
+ dependencies = [],
34
+ pages,
35
+ api: apiConfig,
36
+ services = {},
37
+ middlewares = {},
38
+ exports: publicExports = {},
39
+ onInit,
40
+ onDestroy,
41
+ } = config;
42
+
43
+ if (!name || typeof name !== 'string' || !name.trim()) {
44
+ throw new Error('defineModule requires a valid non-empty "name" string');
45
+ }
46
+
47
+ const combinedDeps = Array.from(new Set([...(imports || []), ...(dependencies || [])]));
48
+
49
+ return function moduleFactory(userOptions = {}) {
50
+ return {
51
+ name,
52
+ version,
53
+ description,
54
+ dependencies: combinedDeps,
55
+ __isWebspressoModule: true,
56
+ moduleConfig: {
57
+ ...config,
58
+ ...userOptions,
59
+ },
60
+ api: typeof publicExports === 'object' && publicExports !== null ? publicExports : {},
61
+
62
+ register(ctx) {
63
+ // 1. Register module services into serviceRegistry
64
+ if (ctx.app && ctx.app.serviceRegistry && services && typeof services === 'object') {
65
+ for (const [sName, sDef] of Object.entries(services)) {
66
+ const qualifiedName = sName.includes('.') ? sName : `${name}.${sName}`;
67
+ if (!ctx.app.serviceRegistry.has(qualifiedName)) {
68
+ ctx.app.serviceRegistry.register(qualifiedName, sDef);
69
+ }
70
+ }
71
+ }
72
+
73
+ // 2. Register module-local middlewares into context
74
+ if (ctx.middlewares && middlewares && typeof middlewares === 'object') {
75
+ for (const [mwName, mwFn] of Object.entries(middlewares)) {
76
+ // Register with module prefix as well
77
+ ctx.middlewares[`${name}.${mwName}`] = mwFn;
78
+ }
79
+ }
80
+
81
+ // 3. Run onInit lifecycle hook if provided
82
+ if (typeof onInit === 'function') {
83
+ onInit(ctx);
84
+ }
85
+
86
+ // 4. Register onDestroy hook with ShutdownManager if provided
87
+ if (typeof onDestroy === 'function' && ctx.shutdownManager) {
88
+ ctx.shutdownManager.registerDisposer(onDestroy, {
89
+ name: `module:${name}`,
90
+ });
91
+ }
92
+ },
93
+ };
94
+ };
95
+ }
96
+
97
+ module.exports = {
98
+ defineModule,
99
+ };
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Module Discovery Utilities
5
+ * Auto-discovers services and middlewares within feature module directories
6
+ * @module src/modules/module-discovery
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { scanDirSafely, isPrivateOrIgnored } = require('../discovery/file-route-parser');
12
+
13
+ /**
14
+ * Converts a file path to camelCase or dot-separated identifier
15
+ * @param {string} str
16
+ * @returns {string}
17
+ */
18
+ function toCamelCase(str) {
19
+ return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase());
20
+ }
21
+
22
+ /**
23
+ * Discovers services, middlewares, and configuration for a module directory
24
+ * @param {string} modulePath - Absolute path to module folder (e.g. /app/modules/auth)
25
+ * @param {string} moduleName - Name of the module (e.g. 'auth')
26
+ * @returns {{ services: Record<string, any>, middlewares: Record<string, Function>, config: Object|null }}
27
+ */
28
+ function discoverModuleDetails(modulePath, moduleName) {
29
+ const discovered = {
30
+ services: {},
31
+ middlewares: {},
32
+ config: null,
33
+ };
34
+
35
+ if (!fs.existsSync(modulePath)) return discovered;
36
+
37
+ // 1. Check for module configuration file
38
+ const configCandidates = [
39
+ path.join(modulePath, `${moduleName}.module.js`),
40
+ path.join(modulePath, 'module.js'),
41
+ path.join(modulePath, 'index.js'),
42
+ ];
43
+ for (const cf of configCandidates) {
44
+ if (fs.existsSync(cf)) {
45
+ try {
46
+ const loaded = require(cf);
47
+ const evaluated = typeof loaded === 'function' ? loaded() : loaded;
48
+ discovered.config = evaluated && evaluated.moduleConfig ? evaluated.moduleConfig : evaluated;
49
+ } catch (err) {
50
+ console.warn(`[webspresso] Error loading module config ${cf}:`, err.message);
51
+ }
52
+ break;
53
+ }
54
+ }
55
+
56
+ // 2. Discover Services in modules/{name}/services/
57
+ const servicesDir = path.join(modulePath, 'services');
58
+ if (fs.existsSync(servicesDir)) {
59
+ const serviceFiles = scanDirSafely(servicesDir);
60
+ for (const { absolutePath, relativePath } of serviceFiles) {
61
+ if (!relativePath.endsWith('.js')) continue;
62
+ const baseName = relativePath.slice(0, -3).split(path.sep).join('.');
63
+ const qualifiedName = `${moduleName}.${baseName}`;
64
+ try {
65
+ const serviceModule = require(absolutePath);
66
+ const serviceDef = serviceModule.default || serviceModule;
67
+ discovered.services[qualifiedName] = serviceDef;
68
+
69
+ // Also add camelCase alias if hyphenated
70
+ const camelAlias = `${moduleName}.${toCamelCase(baseName)}`;
71
+ if (camelAlias !== qualifiedName) {
72
+ discovered.services[camelAlias] = serviceDef;
73
+ }
74
+ } catch (err) {
75
+ console.warn(`[webspresso] Error loading module service ${absolutePath}:`, err.message);
76
+ }
77
+ }
78
+ }
79
+
80
+ // 3. Discover Middlewares in modules/{name}/middleware/ or middlewares/
81
+ const mwDirs = [path.join(modulePath, 'middleware'), path.join(modulePath, 'middlewares')];
82
+ for (const mwDir of mwDirs) {
83
+ if (fs.existsSync(mwDir)) {
84
+ const mwFiles = scanDirSafely(mwDir);
85
+ for (const { absolutePath, relativePath } of mwFiles) {
86
+ if (!relativePath.endsWith('.js')) continue;
87
+ const rawName = path.basename(relativePath, '.js');
88
+ try {
89
+ const mwModule = require(absolutePath);
90
+ const mwFn = mwModule.default || mwModule;
91
+ if (typeof mwFn === 'function') {
92
+ discovered.middlewares[rawName] = mwFn;
93
+ discovered.middlewares[toCamelCase(rawName)] = mwFn;
94
+ }
95
+ } catch (err) {
96
+ console.warn(`[webspresso] Error loading module middleware ${absolutePath}:`, err.message);
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ // Merge explicitly defined middlewares from module config
103
+ if (discovered.config?.middlewares && typeof discovered.config.middlewares === 'object') {
104
+ Object.assign(discovered.middlewares, discovered.config.middlewares);
105
+ }
106
+
107
+ // Merge explicitly defined services from module config
108
+ if (discovered.config?.services && typeof discovered.config.services === 'object') {
109
+ Object.assign(discovered.services, discovered.config.services);
110
+ }
111
+
112
+ return discovered;
113
+ }
114
+
115
+ module.exports = {
116
+ discoverModuleDetails,
117
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Page Definition Helper
5
+ * @module src/pages/define-page
6
+ */
7
+
8
+ /**
9
+ * Defines a page route with load, head, and render lifecycle methods
10
+ * @param {Object} config
11
+ * @param {Array<string|Function>} [config.middleware] - Route-level middleware names or functions
12
+ * @param {Function} [config.load] - Async server data loader: async (ctx) => data
13
+ * @param {Function} [config.head] - Dynamic head metadata: (data, ctx) => ({ title, meta, ... })
14
+ * @param {Function} [config.render] - Component renderer (used for pure JS pages): (data, ctx) => string|Promise<string>
15
+ * @param {string|boolean} [config.layout] - Layout override
16
+ * @param {Object} [config.hooks] - Page lifecycle hooks
17
+ * @returns {Object}
18
+ */
19
+ function definePage(config = {}) {
20
+ if (typeof config === 'function') {
21
+ // If a loader function is passed directly
22
+ return {
23
+ load: config,
24
+ __isWebspressoPage: true,
25
+ };
26
+ }
27
+
28
+ return {
29
+ ...config,
30
+ __isWebspressoPage: true,
31
+ };
32
+ }
33
+
34
+ module.exports = {
35
+ definePage,
36
+ };