@velox0/cerver 0.1.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 +111 -0
- package/bin/cerver.js +44 -0
- package/lib/assets/discover.js +50 -0
- package/lib/assets/embed.js +124 -0
- package/lib/assets/minify.js +83 -0
- package/lib/codegen/emit.js +249 -0
- package/lib/codegen/generator.js +111 -0
- package/lib/codegen/route_table.js +44 -0
- package/lib/commands/build.js +122 -0
- package/lib/commands/new.js +96 -0
- package/lib/commands/run.js +47 -0
- package/lib/compiler/compile.js +91 -0
- package/lib/config.js +52 -0
- package/lib/ir/transform.js +335 -0
- package/lib/ir/types.js +204 -0
- package/lib/parser/discover.js +74 -0
- package/lib/parser/parse.js +50 -0
- package/lib/validator/validate.js +179 -0
- package/package.json +28 -0
- package/runtime/cerver.h +185 -0
- package/runtime/http_parser.c +195 -0
- package/runtime/http_writer.c +137 -0
- package/runtime/mime.c +84 -0
- package/runtime/router.c +150 -0
- package/runtime/server.c +344 -0
- package/runtime/static.c +145 -0
- package/templates/cerver.config.js +6 -0
- package/templates/index.route.js +3 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const IR = require("./types");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Transform a validated AST into an IR route descriptor.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} ast - ESTree AST (validated)
|
|
9
|
+
* @param {string} urlPath - The route path (e.g. "/art/:key")
|
|
10
|
+
* @returns {IRRoute[]} — one IRRoute per exported handler (GET, POST)
|
|
11
|
+
*/
|
|
12
|
+
function transformFile(ast, urlPath) {
|
|
13
|
+
const routes = [];
|
|
14
|
+
|
|
15
|
+
/* Extract dynamic segment names from the URL pattern */
|
|
16
|
+
const params = [];
|
|
17
|
+
const paramRegex = /:([^/]+)/g;
|
|
18
|
+
let match;
|
|
19
|
+
while ((match = paramRegex.exec(urlPath)) !== null) {
|
|
20
|
+
params.push(match[1]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const node of ast.body) {
|
|
24
|
+
if (
|
|
25
|
+
node.type === "ExportNamedDeclaration" &&
|
|
26
|
+
node.declaration &&
|
|
27
|
+
node.declaration.type === "FunctionDeclaration"
|
|
28
|
+
) {
|
|
29
|
+
const funcDecl = node.declaration;
|
|
30
|
+
const method = funcDecl.id.name; /* "GET" or "POST" */
|
|
31
|
+
|
|
32
|
+
/* Get the parameter names (req, res) */
|
|
33
|
+
const reqName =
|
|
34
|
+
funcDecl.params[0] && funcDecl.params[0].type === "Identifier"
|
|
35
|
+
? funcDecl.params[0].name
|
|
36
|
+
: "req";
|
|
37
|
+
const resName =
|
|
38
|
+
funcDecl.params[1] && funcDecl.params[1].type === "Identifier"
|
|
39
|
+
? funcDecl.params[1].name
|
|
40
|
+
: "res";
|
|
41
|
+
|
|
42
|
+
/* Transform the function body */
|
|
43
|
+
const ctx = { reqName, resName };
|
|
44
|
+
const { variables, body } = transformBlock(funcDecl.body, ctx);
|
|
45
|
+
|
|
46
|
+
const handler = IR.IRHandler(variables, body);
|
|
47
|
+
routes.push(IR.IRRoute(method, urlPath, params, handler));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return routes;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Transform a block statement into IR variables and statements.
|
|
56
|
+
*/
|
|
57
|
+
function transformBlock(blockNode, ctx) {
|
|
58
|
+
const variables = [];
|
|
59
|
+
const body = [];
|
|
60
|
+
|
|
61
|
+
for (const stmt of blockNode.body) {
|
|
62
|
+
const result = transformStatement(stmt, ctx);
|
|
63
|
+
if (result) {
|
|
64
|
+
if (result.type === "Variable") {
|
|
65
|
+
variables.push(result);
|
|
66
|
+
} else {
|
|
67
|
+
body.push(result);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { variables, body };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Transform a single statement.
|
|
77
|
+
*/
|
|
78
|
+
function transformStatement(node, ctx) {
|
|
79
|
+
switch (node.type) {
|
|
80
|
+
case "ReturnStatement":
|
|
81
|
+
return transformReturn(node, ctx);
|
|
82
|
+
|
|
83
|
+
case "IfStatement":
|
|
84
|
+
return transformIf(node, ctx);
|
|
85
|
+
|
|
86
|
+
case "VariableDeclaration":
|
|
87
|
+
return transformVariableDecl(node, ctx);
|
|
88
|
+
|
|
89
|
+
case "ExpressionStatement":
|
|
90
|
+
/* Likely a standalone res.text() call or similar */
|
|
91
|
+
return transformExpression(node.expression, ctx);
|
|
92
|
+
|
|
93
|
+
default:
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Transform a return statement.
|
|
100
|
+
*
|
|
101
|
+
* Expected forms:
|
|
102
|
+
* return res.text(200, "hello")
|
|
103
|
+
* return res.json(200, '{"ok": true}')
|
|
104
|
+
* return res.html(200, "<h1>hi</h1>")
|
|
105
|
+
*/
|
|
106
|
+
function transformReturn(node, ctx) {
|
|
107
|
+
if (!node.argument) {
|
|
108
|
+
return IR.IRReturn("text", 200, IR.IRStringLiteral(""));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const arg = node.argument;
|
|
112
|
+
|
|
113
|
+
/* res.text(status, body) */
|
|
114
|
+
if (
|
|
115
|
+
arg.type === "CallExpression" &&
|
|
116
|
+
arg.callee.type === "MemberExpression" &&
|
|
117
|
+
arg.callee.object.type === "Identifier" &&
|
|
118
|
+
arg.callee.object.name === ctx.resName
|
|
119
|
+
) {
|
|
120
|
+
const method = arg.callee.property.name; /* "text", "json", "html" */
|
|
121
|
+
const responseType = ["text", "json", "html"].includes(method)
|
|
122
|
+
? method
|
|
123
|
+
: "text";
|
|
124
|
+
|
|
125
|
+
const status =
|
|
126
|
+
arg.arguments[0] && arg.arguments[0].type === "Literal"
|
|
127
|
+
? arg.arguments[0].value
|
|
128
|
+
: 200;
|
|
129
|
+
|
|
130
|
+
const bodyExpr = arg.arguments[1]
|
|
131
|
+
? transformExpression(arg.arguments[1], ctx)
|
|
132
|
+
: IR.IRStringLiteral("");
|
|
133
|
+
|
|
134
|
+
return IR.IRReturn(responseType, status, bodyExpr);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Plain expression return — treat as text */
|
|
138
|
+
return IR.IRReturn(
|
|
139
|
+
"text",
|
|
140
|
+
200,
|
|
141
|
+
transformExpression(arg, ctx)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Transform an if statement.
|
|
147
|
+
*/
|
|
148
|
+
function transformIf(node, ctx) {
|
|
149
|
+
const condition = transformExpression(node.test, ctx);
|
|
150
|
+
|
|
151
|
+
const thenBlock = node.consequent.type === "BlockStatement"
|
|
152
|
+
? transformBlock(node.consequent, ctx)
|
|
153
|
+
: { variables: [], body: [transformStatement(node.consequent, ctx)].filter(Boolean) };
|
|
154
|
+
|
|
155
|
+
let elseBody = null;
|
|
156
|
+
if (node.alternate) {
|
|
157
|
+
if (node.alternate.type === "IfStatement") {
|
|
158
|
+
/* else if → nested If */
|
|
159
|
+
elseBody = [transformStatement(node.alternate, ctx)];
|
|
160
|
+
} else if (node.alternate.type === "BlockStatement") {
|
|
161
|
+
const elseBlock = transformBlock(node.alternate, ctx);
|
|
162
|
+
elseBody = elseBlock.body;
|
|
163
|
+
} else {
|
|
164
|
+
elseBody = [transformStatement(node.alternate, ctx)].filter(Boolean);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return IR.IRIf(condition, thenBlock.body, elseBody);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Transform a variable declaration.
|
|
173
|
+
*/
|
|
174
|
+
function transformVariableDecl(node, ctx) {
|
|
175
|
+
/* For simplicity, handle the first declarator */
|
|
176
|
+
const decl = node.declarations[0];
|
|
177
|
+
if (!decl) return null;
|
|
178
|
+
|
|
179
|
+
const name = decl.id.name;
|
|
180
|
+
const initExpr = decl.init
|
|
181
|
+
? transformExpression(decl.init, ctx)
|
|
182
|
+
: IR.IRStringLiteral("");
|
|
183
|
+
|
|
184
|
+
/* Infer type */
|
|
185
|
+
let valueType = "string";
|
|
186
|
+
if (initExpr.type === "NumberLiteral") {
|
|
187
|
+
valueType = "number";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return IR.IRVariable(name, valueType, initExpr);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Transform an expression into an IR expression node.
|
|
195
|
+
*/
|
|
196
|
+
function transformExpression(node, ctx) {
|
|
197
|
+
switch (node.type) {
|
|
198
|
+
case "Literal":
|
|
199
|
+
if (typeof node.value === "number") {
|
|
200
|
+
return IR.IRNumberLiteral(node.value);
|
|
201
|
+
}
|
|
202
|
+
return IR.IRStringLiteral(String(node.value || ""));
|
|
203
|
+
|
|
204
|
+
case "Identifier":
|
|
205
|
+
return IR.IRIdentifier(node.name);
|
|
206
|
+
|
|
207
|
+
case "BinaryExpression":
|
|
208
|
+
return IR.IRComparison(
|
|
209
|
+
node.operator,
|
|
210
|
+
transformExpression(node.left, ctx),
|
|
211
|
+
transformExpression(node.right, ctx)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
case "LogicalExpression":
|
|
215
|
+
return IR.IRLogical(
|
|
216
|
+
node.operator,
|
|
217
|
+
transformExpression(node.left, ctx),
|
|
218
|
+
transformExpression(node.right, ctx)
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
case "UnaryExpression":
|
|
222
|
+
return IR.IRUnary(
|
|
223
|
+
node.operator,
|
|
224
|
+
transformExpression(node.argument, ctx)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
case "ConditionalExpression":
|
|
228
|
+
/* a ? b : c → IR.IRIf as expression — simplify to if/else for now */
|
|
229
|
+
return IR.IRComparison(
|
|
230
|
+
"?:",
|
|
231
|
+
transformExpression(node.test, ctx),
|
|
232
|
+
transformExpression(node.consequent, ctx)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
case "MemberExpression":
|
|
236
|
+
return transformMemberExpr(node, ctx);
|
|
237
|
+
|
|
238
|
+
case "CallExpression":
|
|
239
|
+
return transformCallExpr(node, ctx);
|
|
240
|
+
|
|
241
|
+
case "TemplateLiteral":
|
|
242
|
+
return transformTemplateLiteral(node, ctx);
|
|
243
|
+
|
|
244
|
+
case "AssignmentExpression":
|
|
245
|
+
/* Treat as a variable update — just return the right side for now */
|
|
246
|
+
return transformExpression(node.right, ctx);
|
|
247
|
+
|
|
248
|
+
default:
|
|
249
|
+
return IR.IRStringLiteral("");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Transform member expressions.
|
|
255
|
+
*
|
|
256
|
+
* Handles:
|
|
257
|
+
* req.params.key → IRParamAccess("key")
|
|
258
|
+
* req.query.x → IRQueryAccess("x")
|
|
259
|
+
* req.headers.x → IRHeaderAccess("x")
|
|
260
|
+
*/
|
|
261
|
+
function transformMemberExpr(node, ctx) {
|
|
262
|
+
/* req.params.key or req.query.key */
|
|
263
|
+
if (
|
|
264
|
+
node.object.type === "MemberExpression" &&
|
|
265
|
+
node.object.object.type === "Identifier" &&
|
|
266
|
+
node.object.object.name === ctx.reqName
|
|
267
|
+
) {
|
|
268
|
+
const group = node.object.property.name || node.object.property.value;
|
|
269
|
+
const key = node.property.name || node.property.value;
|
|
270
|
+
|
|
271
|
+
if (group === "params") return IR.IRParamAccess(key);
|
|
272
|
+
if (group === "query") return IR.IRQueryAccess(key);
|
|
273
|
+
if (group === "headers") return IR.IRHeaderAccess(key);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* req.method, req.path */
|
|
277
|
+
if (
|
|
278
|
+
node.object.type === "Identifier" &&
|
|
279
|
+
node.object.name === ctx.reqName
|
|
280
|
+
) {
|
|
281
|
+
const prop = node.property.name || node.property.value;
|
|
282
|
+
if (prop === "method") return IR.IRIdentifier("req->method");
|
|
283
|
+
if (prop === "path") return IR.IRIdentifier("req->path");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return IR.IRStringLiteral("");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Transform call expressions.
|
|
291
|
+
*/
|
|
292
|
+
function transformCallExpr(node, ctx) {
|
|
293
|
+
/* res.text(status, body), res.json(...), res.html(...) */
|
|
294
|
+
if (
|
|
295
|
+
node.callee.type === "MemberExpression" &&
|
|
296
|
+
node.callee.object.type === "Identifier" &&
|
|
297
|
+
node.callee.object.name === ctx.resName
|
|
298
|
+
) {
|
|
299
|
+
const method = node.callee.property.name;
|
|
300
|
+
const args = node.arguments.map((a) => transformExpression(a, ctx));
|
|
301
|
+
return IR.IRCall("res", method, args);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* String methods like str.toLowerCase(), includes() etc. — return as-is */
|
|
305
|
+
if (node.callee.type === "MemberExpression") {
|
|
306
|
+
const obj = transformExpression(node.callee.object, ctx);
|
|
307
|
+
const method = node.callee.property.name;
|
|
308
|
+
const args = node.arguments.map((a) => transformExpression(a, ctx));
|
|
309
|
+
return IR.IRCall(obj, method, args);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return IR.IRStringLiteral("");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Transform template literals into concatenation.
|
|
317
|
+
*/
|
|
318
|
+
function transformTemplateLiteral(node, ctx) {
|
|
319
|
+
const parts = [];
|
|
320
|
+
|
|
321
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
322
|
+
const quasi = node.quasis[i];
|
|
323
|
+
if (quasi.value.cooked) {
|
|
324
|
+
parts.push(IR.IRStringLiteral(quasi.value.cooked));
|
|
325
|
+
}
|
|
326
|
+
if (i < node.expressions.length) {
|
|
327
|
+
parts.push(transformExpression(node.expressions[i], ctx));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (parts.length === 1) return parts[0];
|
|
332
|
+
return IR.IRConcat(parts);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = { transformFile };
|
package/lib/ir/types.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* IR type definitions for the cerver compiler.
|
|
5
|
+
*
|
|
6
|
+
* These are plain object constructors for the intermediate representation
|
|
7
|
+
* that sits between the AST and C code generation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A complete route definition.
|
|
12
|
+
*/
|
|
13
|
+
function IRRoute(method, urlPath, params, handler) {
|
|
14
|
+
return {
|
|
15
|
+
type: "Route",
|
|
16
|
+
method, /* "GET" | "POST" */
|
|
17
|
+
urlPath, /* "/item/:id" */
|
|
18
|
+
params, /* ["id"] — extracted from dynamic segments */
|
|
19
|
+
handler, /* IRHandler */
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A route handler function body.
|
|
25
|
+
*/
|
|
26
|
+
function IRHandler(variables, body) {
|
|
27
|
+
return {
|
|
28
|
+
type: "Handler",
|
|
29
|
+
variables, /* IRVariable[] */
|
|
30
|
+
body, /* IRStatement[] */
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A local variable declaration.
|
|
36
|
+
*/
|
|
37
|
+
function IRVariable(name, valueType, initExpr) {
|
|
38
|
+
return {
|
|
39
|
+
type: "Variable",
|
|
40
|
+
name, /* C-safe variable name */
|
|
41
|
+
valueType, /* "string" | "number" */
|
|
42
|
+
initExpr, /* IRExpression — the initializer */
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* An if/else statement.
|
|
48
|
+
*/
|
|
49
|
+
function IRIf(condition, thenBody, elseBody) {
|
|
50
|
+
return {
|
|
51
|
+
type: "If",
|
|
52
|
+
condition, /* IRExpression */
|
|
53
|
+
thenBody, /* IRStatement[] */
|
|
54
|
+
elseBody, /* IRStatement[] | null */
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A return statement that sends an HTTP response.
|
|
60
|
+
*/
|
|
61
|
+
function IRReturn(responseType, status, value) {
|
|
62
|
+
return {
|
|
63
|
+
type: "Return",
|
|
64
|
+
responseType, /* "text" | "json" | "html" */
|
|
65
|
+
status, /* number */
|
|
66
|
+
value, /* IRExpression — the response body */
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A string literal.
|
|
72
|
+
*/
|
|
73
|
+
function IRStringLiteral(value) {
|
|
74
|
+
return {
|
|
75
|
+
type: "StringLiteral",
|
|
76
|
+
value,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A number literal.
|
|
82
|
+
*/
|
|
83
|
+
function IRNumberLiteral(value) {
|
|
84
|
+
return {
|
|
85
|
+
type: "NumberLiteral",
|
|
86
|
+
value,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A comparison expression.
|
|
92
|
+
*/
|
|
93
|
+
function IRComparison(operator, left, right) {
|
|
94
|
+
return {
|
|
95
|
+
type: "Comparison",
|
|
96
|
+
operator, /* "==" | "!=" | "===" | "!==" */
|
|
97
|
+
left, /* IRExpression */
|
|
98
|
+
right, /* IRExpression */
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A logical expression (&&, ||).
|
|
104
|
+
*/
|
|
105
|
+
function IRLogical(operator, left, right) {
|
|
106
|
+
return {
|
|
107
|
+
type: "Logical",
|
|
108
|
+
operator, /* "&&" | "||" */
|
|
109
|
+
left,
|
|
110
|
+
right,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* A unary expression (!, -).
|
|
116
|
+
*/
|
|
117
|
+
function IRUnary(operator, argument) {
|
|
118
|
+
return {
|
|
119
|
+
type: "Unary",
|
|
120
|
+
operator,
|
|
121
|
+
argument,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Access to a request parameter: req.params.key
|
|
127
|
+
*/
|
|
128
|
+
function IRParamAccess(paramName) {
|
|
129
|
+
return {
|
|
130
|
+
type: "ParamAccess",
|
|
131
|
+
paramName,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Access to a query parameter: req.query.key
|
|
137
|
+
*/
|
|
138
|
+
function IRQueryAccess(queryName) {
|
|
139
|
+
return {
|
|
140
|
+
type: "QueryAccess",
|
|
141
|
+
queryName,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Access to a request header: req.headers["user-agent"]
|
|
147
|
+
*/
|
|
148
|
+
function IRHeaderAccess(headerName) {
|
|
149
|
+
return {
|
|
150
|
+
type: "HeaderAccess",
|
|
151
|
+
headerName,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A variable reference.
|
|
157
|
+
*/
|
|
158
|
+
function IRIdentifier(name) {
|
|
159
|
+
return {
|
|
160
|
+
type: "Identifier",
|
|
161
|
+
name,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A template literal (string interpolation).
|
|
167
|
+
*/
|
|
168
|
+
function IRConcat(parts) {
|
|
169
|
+
return {
|
|
170
|
+
type: "Concat",
|
|
171
|
+
parts, /* IRExpression[] — mix of literals and expressions */
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A function call expression (for res.text, res.json, etc.)
|
|
177
|
+
*/
|
|
178
|
+
function IRCall(object, method, args) {
|
|
179
|
+
return {
|
|
180
|
+
type: "Call",
|
|
181
|
+
object,
|
|
182
|
+
method,
|
|
183
|
+
args,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
IRRoute,
|
|
189
|
+
IRHandler,
|
|
190
|
+
IRVariable,
|
|
191
|
+
IRIf,
|
|
192
|
+
IRReturn,
|
|
193
|
+
IRStringLiteral,
|
|
194
|
+
IRNumberLiteral,
|
|
195
|
+
IRComparison,
|
|
196
|
+
IRLogical,
|
|
197
|
+
IRUnary,
|
|
198
|
+
IRParamAccess,
|
|
199
|
+
IRQueryAccess,
|
|
200
|
+
IRHeaderAccess,
|
|
201
|
+
IRIdentifier,
|
|
202
|
+
IRConcat,
|
|
203
|
+
IRCall,
|
|
204
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Discover route files under app/routes/ and map them to URL paths.
|
|
8
|
+
*
|
|
9
|
+
* File-based routing convention:
|
|
10
|
+
* app/routes/index.js → /
|
|
11
|
+
* app/routes/page.js → /page
|
|
12
|
+
* app/routes/group/item.js → /group/item
|
|
13
|
+
* app/routes/item/[id].js → /item/:id
|
|
14
|
+
*
|
|
15
|
+
* @param {string} routesDir - Absolute path to app/routes/
|
|
16
|
+
* @returns {Array<{ filePath: string, urlPath: string }>}
|
|
17
|
+
*/
|
|
18
|
+
function discoverRoutes(routesDir) {
|
|
19
|
+
const routes = [];
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(routesDir)) {
|
|
22
|
+
return routes;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function walk(dir, prefix) {
|
|
26
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
27
|
+
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const fullPath = path.join(dir, entry.name);
|
|
30
|
+
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
walk(fullPath, prefix + "/" + entry.name);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!entry.name.endsWith(".js")) continue;
|
|
37
|
+
|
|
38
|
+
/* Build the URL path from the file path */
|
|
39
|
+
let urlPath;
|
|
40
|
+
const basename = entry.name.replace(/\.js$/, "");
|
|
41
|
+
|
|
42
|
+
if (basename === "index") {
|
|
43
|
+
urlPath = prefix || "/";
|
|
44
|
+
} else {
|
|
45
|
+
urlPath = prefix + "/" + basename;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Convert [param] to :param for dynamic segments */
|
|
49
|
+
urlPath = urlPath.replace(/\[([^\]]+)\]/g, ":$1");
|
|
50
|
+
|
|
51
|
+
/* Normalize double slashes */
|
|
52
|
+
urlPath = urlPath.replace(/\/+/g, "/");
|
|
53
|
+
|
|
54
|
+
routes.push({
|
|
55
|
+
filePath: fullPath,
|
|
56
|
+
urlPath,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
walk(routesDir, "");
|
|
62
|
+
|
|
63
|
+
/* Sort: static routes before dynamic ones for predictable matching */
|
|
64
|
+
routes.sort((a, b) => {
|
|
65
|
+
const aDynamic = a.urlPath.includes(":");
|
|
66
|
+
const bDynamic = b.urlPath.includes(":");
|
|
67
|
+
if (aDynamic !== bDynamic) return aDynamic ? 1 : -1;
|
|
68
|
+
return a.urlPath.localeCompare(b.urlPath);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return routes;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { discoverRoutes };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const acorn = require("acorn");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse a JavaScript file into an ESTree AST using Acorn.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} filePath - Absolute path to the .js file
|
|
10
|
+
* @returns {{ ast: object, source: string }}
|
|
11
|
+
*/
|
|
12
|
+
function parseFile(filePath) {
|
|
13
|
+
const source = fs.readFileSync(filePath, "utf8");
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const ast = acorn.parse(source, {
|
|
17
|
+
ecmaVersion: 2020,
|
|
18
|
+
sourceType: "module",
|
|
19
|
+
locations: true, /* line/column info for error reporting */
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return { ast, source };
|
|
23
|
+
} catch (err) {
|
|
24
|
+
const loc = err.loc
|
|
25
|
+
? `${filePath}:${err.loc.line}:${err.loc.column}`
|
|
26
|
+
: filePath;
|
|
27
|
+
throw new Error(`cerver: parse error at ${loc} — ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse a source string directly (for testing / config parsing).
|
|
33
|
+
*/
|
|
34
|
+
function parseSource(source, filename) {
|
|
35
|
+
try {
|
|
36
|
+
const ast = acorn.parse(source, {
|
|
37
|
+
ecmaVersion: 2020,
|
|
38
|
+
sourceType: "module",
|
|
39
|
+
locations: true,
|
|
40
|
+
});
|
|
41
|
+
return { ast, source };
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const loc = err.loc
|
|
44
|
+
? `${filename || "<source>"}:${err.loc.line}:${err.loc.column}`
|
|
45
|
+
: filename || "<source>";
|
|
46
|
+
throw new Error(`cerver: parse error at ${loc} — ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { parseFile, parseSource };
|