webspresso 0.0.95 → 0.0.96
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 +225 -0
- package/.agents/AGENTS.md +72 -0
- package/.agents/ARCHITECTURE.md +119 -0
- package/.agents/AUTH.md +103 -0
- package/.agents/CLI.md +115 -0
- package/.agents/COMPRESSION.md +59 -0
- package/.agents/CONVENTIONS.md +46 -0
- package/.agents/EMAIL.md +141 -0
- package/.agents/ERRORS.md +201 -0
- package/.agents/MCP.md +103 -0
- package/.agents/ORM.md +230 -0
- package/.agents/PLUGINS.md +419 -0
- package/.agents/QUEUE.md +113 -0
- package/.agents/REALTIME.md +213 -0
- package/.agents/ROUTING.md +292 -0
- package/.agents/SERVICES.md +341 -0
- package/.agents/SHUTDOWN.md +67 -0
- package/.agents/STREAMING.md +73 -0
- package/.agents/WORKFLOW.md +27 -0
- package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
- package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
- package/.cursorrules +14 -0
- package/AGENTS.md +175 -0
- package/CLAUDE.md +54 -0
- package/README.md +18 -14
- package/bin/commands/agents-init.js +169 -0
- package/bin/commands/api.js +114 -48
- package/bin/commands/doctor.js +35 -0
- package/bin/commands/new.js +13 -2
- package/bin/commands/page.js +75 -89
- package/bin/commands/service.js +77 -0
- package/bin/webspresso.js +2 -2
- package/core/compileSchema.js +1 -1
- package/core/errors/middleware.js +22 -2
- package/core/errors/normalize.js +29 -2
- package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
- package/index.d.ts +306 -0
- package/index.js +35 -1
- package/package.json +5 -3
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
- package/plugins/csrf/index.js +3 -2
- package/plugins/csv/engine.js +447 -0
- package/plugins/csv/index.js +94 -0
- package/plugins/csv/middleware.js +80 -0
- package/plugins/csv/services.js +138 -0
- package/plugins/file-manager/admin-component.js +613 -0
- package/plugins/file-manager/api-handlers.js +261 -0
- package/plugins/file-manager/core.js +601 -0
- package/plugins/file-manager/index.js +139 -0
- package/plugins/file-manager/security.js +207 -0
- package/plugins/index.js +8 -0
- package/plugins/xlsx/engine.js +397 -0
- package/plugins/xlsx/index.js +91 -0
- package/plugins/xlsx/middleware.js +87 -0
- package/plugins/xlsx/services.js +118 -0
- package/src/api/api-loader.js +233 -0
- package/src/api/define-api.js +35 -0
- package/src/discovery/file-route-parser.js +201 -0
- package/src/discovery/scan-routes.js +265 -0
- package/src/file-router.js +6 -0
- package/src/modules/define-module.js +99 -0
- package/src/modules/module-discovery.js +117 -0
- package/src/pages/define-page.js +36 -0
- package/src/pages/page-loader.js +214 -0
- package/src/routing/mount-discovered-routes.js +78 -0
- package/src/routing/route-table.js +191 -0
- package/src/server.js +112 -15
- package/src/services/builtins/file-manager.js +178 -0
- package/src/services/index.js +2 -0
- package/bin/commands/skill.js +0 -187
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
- package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
- package/templates/skills/webspresso-usage/SKILL.md +0 -48
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso API Loader
|
|
5
|
+
* Handles validation and execution of defineApi() endpoints
|
|
6
|
+
* @module src/api/api-loader
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { ZodError } = require('zod');
|
|
10
|
+
const { z: zForApi } = require('../../core/validation');
|
|
11
|
+
const { resolveMiddlewares } = require('../file-router');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compiles a schema definition from an API module
|
|
15
|
+
* @param {Object|Function} schemaDef
|
|
16
|
+
* @returns {Object|null}
|
|
17
|
+
*/
|
|
18
|
+
function compileApiSchema(schemaDef) {
|
|
19
|
+
if (!schemaDef) return null;
|
|
20
|
+
if (typeof schemaDef === 'function') {
|
|
21
|
+
try {
|
|
22
|
+
return schemaDef({ z: zForApi });
|
|
23
|
+
} catch (err) {
|
|
24
|
+
throw new Error(`Failed to compile API schema: ${err.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (typeof schemaDef === 'object') {
|
|
28
|
+
return schemaDef;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Validates request input against compiled schema
|
|
35
|
+
* @param {import('express').Request} req
|
|
36
|
+
* @param {Object} compiledSchema
|
|
37
|
+
*/
|
|
38
|
+
function validateRequestInput(req, compiledSchema) {
|
|
39
|
+
req.input = {
|
|
40
|
+
body: undefined,
|
|
41
|
+
params: undefined,
|
|
42
|
+
query: undefined,
|
|
43
|
+
headers: undefined,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (!compiledSchema) return;
|
|
47
|
+
|
|
48
|
+
if (compiledSchema.body && typeof compiledSchema.body.parse === 'function') {
|
|
49
|
+
req.input.body = compiledSchema.body.parse(req.body);
|
|
50
|
+
} else {
|
|
51
|
+
req.input.body = req.body;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (compiledSchema.params && typeof compiledSchema.params.parse === 'function') {
|
|
55
|
+
req.input.params = compiledSchema.params.parse(req.params);
|
|
56
|
+
} else {
|
|
57
|
+
req.input.params = req.params;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (compiledSchema.query && typeof compiledSchema.query.parse === 'function') {
|
|
61
|
+
req.input.query = compiledSchema.query.parse(req.query);
|
|
62
|
+
} else {
|
|
63
|
+
req.input.query = req.query;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (compiledSchema.headers && typeof compiledSchema.headers.parse === 'function') {
|
|
67
|
+
req.input.headers = compiledSchema.headers.parse(req.headers);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates an Express route handler for a discovered API descriptor
|
|
73
|
+
* @param {Object} descriptor - Route descriptor
|
|
74
|
+
* @param {Object} context - Server context
|
|
75
|
+
* @returns {Function}
|
|
76
|
+
*/
|
|
77
|
+
function createApiHandler(descriptor, context) {
|
|
78
|
+
const {
|
|
79
|
+
middlewares = {},
|
|
80
|
+
serviceRegistry = null,
|
|
81
|
+
db = null,
|
|
82
|
+
options: appOptions = {},
|
|
83
|
+
} = context;
|
|
84
|
+
|
|
85
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
86
|
+
let cachedMiddlewares = null;
|
|
87
|
+
let cachedApiDef = null;
|
|
88
|
+
let cachedHandlerFn = null;
|
|
89
|
+
let cachedCompiledSchema = null;
|
|
90
|
+
|
|
91
|
+
return async (req, res, next) => {
|
|
92
|
+
try {
|
|
93
|
+
// 1. Reload module in development
|
|
94
|
+
if (isDev) {
|
|
95
|
+
try {
|
|
96
|
+
const resolvedPath = require.resolve(descriptor.file);
|
|
97
|
+
if (require.cache[resolvedPath]) {
|
|
98
|
+
delete require.cache[resolvedPath];
|
|
99
|
+
cachedMiddlewares = null;
|
|
100
|
+
cachedApiDef = null;
|
|
101
|
+
cachedHandlerFn = null;
|
|
102
|
+
cachedCompiledSchema = null;
|
|
103
|
+
}
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let apiDef = cachedApiDef;
|
|
108
|
+
let handlerFn = cachedHandlerFn;
|
|
109
|
+
let compiledSchema = cachedCompiledSchema;
|
|
110
|
+
|
|
111
|
+
if (!apiDef) {
|
|
112
|
+
let apiModule = {};
|
|
113
|
+
try {
|
|
114
|
+
apiModule = require(descriptor.file);
|
|
115
|
+
} catch (loadErr) {
|
|
116
|
+
return next(loadErr);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
apiDef = typeof apiModule === 'function' ? { handler: apiModule } : apiModule.default || apiModule;
|
|
120
|
+
handlerFn = apiDef.handler || (typeof apiModule === 'function' ? apiModule : null);
|
|
121
|
+
|
|
122
|
+
if (typeof handlerFn !== 'function') {
|
|
123
|
+
throw new Error(`API file "${descriptor.source}" does not export a valid handler function`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
compiledSchema = compileApiSchema(apiDef.schema);
|
|
127
|
+
|
|
128
|
+
if (!isDev) {
|
|
129
|
+
cachedApiDef = apiDef;
|
|
130
|
+
cachedHandlerFn = handlerFn;
|
|
131
|
+
cachedCompiledSchema = compiledSchema;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 2. Injections
|
|
136
|
+
if (db) req.db = db;
|
|
137
|
+
if (serviceRegistry) {
|
|
138
|
+
req.service = (name, input, opts) =>
|
|
139
|
+
serviceRegistry.call(name, input, { req, res, db }, opts);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 3. Schema validation
|
|
143
|
+
if (compiledSchema) {
|
|
144
|
+
try {
|
|
145
|
+
validateRequestInput(req, compiledSchema);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (err instanceof ZodError || err.name === 'ZodError' || Array.isArray(err?.issues)) {
|
|
148
|
+
return res.status(400).json({
|
|
149
|
+
error: 'Validation Error',
|
|
150
|
+
issues: err.issues,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
req.input = {
|
|
157
|
+
body: req.body,
|
|
158
|
+
params: req.params,
|
|
159
|
+
query: req.query,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 4. Resolve and execute middlewares (module-local first, cached across requests)
|
|
164
|
+
if (!cachedMiddlewares) {
|
|
165
|
+
const moduleMiddlewares = descriptor.moduleConfig?.middlewares || {};
|
|
166
|
+
const combinedRegistry = Object.assign({}, middlewares, moduleMiddlewares);
|
|
167
|
+
cachedMiddlewares = resolveMiddlewares(apiDef.middleware, combinedRegistry);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const mw of cachedMiddlewares) {
|
|
171
|
+
if (typeof mw !== 'function') continue;
|
|
172
|
+
await new Promise((resolve, reject) => {
|
|
173
|
+
try {
|
|
174
|
+
mw(req, res, (err) => {
|
|
175
|
+
if (err) reject(err);
|
|
176
|
+
else resolve();
|
|
177
|
+
});
|
|
178
|
+
} catch (syncErr) {
|
|
179
|
+
reject(syncErr);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
if (res.headersSent) return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 5. Execute handler
|
|
186
|
+
const apiCtx = {
|
|
187
|
+
req,
|
|
188
|
+
res,
|
|
189
|
+
params: req.params || {},
|
|
190
|
+
query: req.query || {},
|
|
191
|
+
body: req.body || {},
|
|
192
|
+
input: req.input,
|
|
193
|
+
service: (name, input, opts) =>
|
|
194
|
+
serviceRegistry ? serviceRegistry.call(name, input, { req, res, db }, opts) : null,
|
|
195
|
+
module: descriptor.module || null,
|
|
196
|
+
config: appOptions,
|
|
197
|
+
logger: console,
|
|
198
|
+
db,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
req.ctx = apiCtx;
|
|
202
|
+
res.service = apiCtx.service;
|
|
203
|
+
res.input = apiCtx.input;
|
|
204
|
+
res.module = apiCtx.module;
|
|
205
|
+
|
|
206
|
+
const result = await handlerFn(req, res, apiCtx);
|
|
207
|
+
|
|
208
|
+
// 6. Auto-respond if handler returned a value and response not yet sent
|
|
209
|
+
if (!res.headersSent) {
|
|
210
|
+
if (result !== undefined) {
|
|
211
|
+
return res.json(result);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (err && typeof err === 'object') {
|
|
216
|
+
if (!err.route) err.route = descriptor.path;
|
|
217
|
+
if (!err.method) err.method = descriptor.method ? descriptor.method.toUpperCase() : (req.method || 'GET');
|
|
218
|
+
if (!err.source) err.source = descriptor.source || descriptor.file;
|
|
219
|
+
if (!err.file) err.file = descriptor.file;
|
|
220
|
+
if (!err.module) err.module = descriptor.module || null;
|
|
221
|
+
if (!err.phase) err.phase = 'handler';
|
|
222
|
+
if (!err.requestId) err.requestId = req.id || (req.headers && req.headers['x-request-id']) || null;
|
|
223
|
+
}
|
|
224
|
+
return next(err);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
createApiHandler,
|
|
231
|
+
compileApiSchema,
|
|
232
|
+
validateRequestInput,
|
|
233
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso API Definition Helper
|
|
5
|
+
* @module src/api/define-api
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Defines an API route with schema validation, response metadata, and handler
|
|
10
|
+
* @param {Object} config
|
|
11
|
+
* @param {string} [config.description] - Endpoint description for OpenAPI
|
|
12
|
+
* @param {Array<string>} [config.tags] - Endpoint tags for OpenAPI
|
|
13
|
+
* @param {Array<string|Function>} [config.middleware] - Named or functional middlewares
|
|
14
|
+
* @param {Object|Function} [config.schema] - Request schemas (params, query, body, headers)
|
|
15
|
+
* @param {Object} [config.response] - Expected response schemas by status code
|
|
16
|
+
* @param {Function} config.handler - Request handler: async (req, ctx) => data
|
|
17
|
+
* @returns {Object}
|
|
18
|
+
*/
|
|
19
|
+
function defineApi(config = {}) {
|
|
20
|
+
if (typeof config === 'function') {
|
|
21
|
+
return {
|
|
22
|
+
handler: config,
|
|
23
|
+
__isWebspressoApi: true,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
...config,
|
|
29
|
+
__isWebspressoApi: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
defineApi,
|
|
35
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso File Route Parser
|
|
5
|
+
* Converts relative filesystem paths into Express route paths and HTTP methods
|
|
6
|
+
* @module src/discovery/file-route-parser
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const SUPPORTED_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'options', 'head']);
|
|
13
|
+
const SUPPORTED_EXTENSIONS = new Set(['.js', '.njk']);
|
|
14
|
+
const METHOD_SUFFIX_REGEX = /\.([a-zA-Z]+)\.js$/;
|
|
15
|
+
const TEMPORARY_FILE_REGEX = /(?:~|\.swp|\.tmp|\.bak|\.DS_Store)$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Safely scans a directory recursively for route files
|
|
19
|
+
* @param {string} dirPath - Absolute directory path
|
|
20
|
+
* @param {string} [baseDir=dirPath] - Base directory for computing relative paths
|
|
21
|
+
* @returns {Array<{ absolutePath: string, relativePath: string }>}
|
|
22
|
+
*/
|
|
23
|
+
function scanDirSafely(dirPath, baseDir = dirPath) {
|
|
24
|
+
const results = [];
|
|
25
|
+
if (!fs.existsSync(dirPath)) return results;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
29
|
+
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const name = entry.name;
|
|
32
|
+
if (isPrivateOrIgnored(name)) continue;
|
|
33
|
+
|
|
34
|
+
const fullPath = path.join(dirPath, name);
|
|
35
|
+
const relPath = path.relative(baseDir, fullPath);
|
|
36
|
+
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
results.push(...scanDirSafely(fullPath, baseDir));
|
|
39
|
+
} else if (entry.isFile()) {
|
|
40
|
+
const ext = path.extname(name);
|
|
41
|
+
if (SUPPORTED_EXTENSIONS.has(ext)) {
|
|
42
|
+
results.push({
|
|
43
|
+
absolutePath: fullPath,
|
|
44
|
+
relativePath: relPath,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.warn(`[webspresso] Failed to scan directory ${dirPath}:`, err.message);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Checks if a filename or directory name should be ignored (private or temporary)
|
|
58
|
+
* @param {string} name - File or directory basename
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
function isPrivateOrIgnored(name) {
|
|
62
|
+
if (!name || typeof name !== 'string') return true;
|
|
63
|
+
// Hidden files/directories (.git, .DS_Store, .login.js.swp)
|
|
64
|
+
if (name.startsWith('.')) return true;
|
|
65
|
+
// Private files/directories (_components, _helpers, _private.js)
|
|
66
|
+
if (name.startsWith('_')) return true;
|
|
67
|
+
// Temporary editor files
|
|
68
|
+
if (TEMPORARY_FILE_REGEX.test(name)) return true;
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Converts parameter markers [id] -> :id and catch-all [...path] -> *
|
|
74
|
+
* @param {string} segment
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
function transformDynamicSegment(segment) {
|
|
78
|
+
if (!segment || typeof segment !== 'string') return segment || '';
|
|
79
|
+
if (segment.startsWith('[...') && segment.endsWith(']')) {
|
|
80
|
+
const paramName = segment.slice(4, -1).trim();
|
|
81
|
+
return paramName ? `*${paramName}` : '*';
|
|
82
|
+
}
|
|
83
|
+
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
84
|
+
const paramName = segment.slice(1, -1).trim();
|
|
85
|
+
return paramName ? `:${paramName}` : segment;
|
|
86
|
+
}
|
|
87
|
+
return segment;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Normalizes a URL prefix to have a leading slash and no trailing slash
|
|
92
|
+
* @param {string} [prefix='']
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
function normalizePrefix(prefix = '') {
|
|
96
|
+
if (!prefix || typeof prefix !== 'string') return '';
|
|
97
|
+
let clean = prefix.trim().replace(/\\/g, '/');
|
|
98
|
+
clean = clean.replace(/\/+/g, '/');
|
|
99
|
+
if (clean === '/' || !clean) return '';
|
|
100
|
+
if (!clean.startsWith('/')) clean = '/' + clean;
|
|
101
|
+
if (clean.endsWith('/')) clean = clean.slice(0, -1);
|
|
102
|
+
return clean === '/' ? '' : clean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parses a relative file path for an API or Page route
|
|
107
|
+
* @param {string} relativePath - Relative path from directory root (e.g. 'users/[id].get.js' or 'blog/[slug].js')
|
|
108
|
+
* @param {Object} [options]
|
|
109
|
+
* @param {string} [options.type='page'] - 'page' | 'api'
|
|
110
|
+
* @param {string} [options.prefix=''] - Base prefix (e.g. '/api' or '/auth')
|
|
111
|
+
* @returns {{ path: string, method: string, isValid: boolean, isPrivate: boolean, originalMethodPart?: string }}
|
|
112
|
+
*/
|
|
113
|
+
function parseFileRoute(relativePath, options = {}) {
|
|
114
|
+
if (!relativePath || typeof relativePath !== 'string') {
|
|
115
|
+
return { path: '', method: '', isValid: false, isPrivate: false };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { type = 'page', prefix = '' } = options;
|
|
119
|
+
|
|
120
|
+
// Normalize path separators to forward slashes
|
|
121
|
+
const normalizedPath = relativePath.split(path.sep).join('/').replace(/\\/g, '/');
|
|
122
|
+
const segments = normalizedPath.split('/').filter(Boolean);
|
|
123
|
+
|
|
124
|
+
// Check if any segment is private or ignored
|
|
125
|
+
for (const seg of segments) {
|
|
126
|
+
if (isPrivateOrIgnored(seg)) {
|
|
127
|
+
return { path: '', method: '', isValid: false, isPrivate: true };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const filename = segments[segments.length - 1];
|
|
132
|
+
if (!filename) {
|
|
133
|
+
return { path: '', method: '', isValid: false, isPrivate: false };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let method = 'get';
|
|
137
|
+
let cleanFilename = filename;
|
|
138
|
+
let originalMethodPart = null;
|
|
139
|
+
|
|
140
|
+
// Check for HTTP method suffix: e.g. health.get.js, [id].patch.js
|
|
141
|
+
const methodMatch = filename.match(METHOD_SUFFIX_REGEX);
|
|
142
|
+
if (methodMatch) {
|
|
143
|
+
const rawMethod = methodMatch[1].toLowerCase();
|
|
144
|
+
originalMethodPart = rawMethod;
|
|
145
|
+
if (SUPPORTED_METHODS.has(rawMethod)) {
|
|
146
|
+
method = rawMethod;
|
|
147
|
+
cleanFilename = filename.replace(`.${methodMatch[1]}.js`, '');
|
|
148
|
+
} else {
|
|
149
|
+
// Invalid HTTP method in filename
|
|
150
|
+
return { path: '', method: rawMethod, isValid: false, isPrivate: false, invalidMethod: true };
|
|
151
|
+
}
|
|
152
|
+
} else if (filename.endsWith('.js')) {
|
|
153
|
+
cleanFilename = filename.slice(0, -3);
|
|
154
|
+
} else if (filename.endsWith('.njk')) {
|
|
155
|
+
cleanFilename = filename.slice(0, -4);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Replace last segment with cleanFilename
|
|
159
|
+
const routeSegments = [...segments.slice(0, -1), cleanFilename];
|
|
160
|
+
|
|
161
|
+
// Process dynamic brackets in segments
|
|
162
|
+
const transformedSegments = routeSegments.map(transformDynamicSegment);
|
|
163
|
+
|
|
164
|
+
// Handle 'index' at the end of path
|
|
165
|
+
if (transformedSegments[transformedSegments.length - 1] === 'index') {
|
|
166
|
+
transformedSegments.pop();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let routePath = '/' + transformedSegments.join('/');
|
|
170
|
+
if (routePath === '//' || routePath === '') {
|
|
171
|
+
routePath = '/';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Combine with prefix
|
|
175
|
+
const cleanPrefix = normalizePrefix(prefix);
|
|
176
|
+
if (cleanPrefix) {
|
|
177
|
+
if (routePath === '/') {
|
|
178
|
+
routePath = cleanPrefix;
|
|
179
|
+
} else {
|
|
180
|
+
routePath = `${cleanPrefix}${routePath}`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
path: routePath,
|
|
186
|
+
method: method.toUpperCase(),
|
|
187
|
+
isValid: true,
|
|
188
|
+
isPrivate: false,
|
|
189
|
+
originalMethodPart,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
parseFileRoute,
|
|
195
|
+
isPrivateOrIgnored,
|
|
196
|
+
normalizePrefix,
|
|
197
|
+
transformDynamicSegment,
|
|
198
|
+
scanDirSafely,
|
|
199
|
+
SUPPORTED_METHODS,
|
|
200
|
+
SUPPORTED_EXTENSIONS,
|
|
201
|
+
};
|