@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
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Cerver
|
|
2
|
+
|
|
3
|
+
A lightweight, compile-time web framework that transpiles restricted JavaScript server logic into highly optimized native C HTTP server binaries.
|
|
4
|
+
|
|
5
|
+
Cerver takes a Next.js-style file-based routing structure (written in a strict subset of JavaScript), parses it, generates equivalent C code, embeds your static assets, and compiles it all into a single, standalone executable (~50KB) that runs with zero Node.js dependency.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Compile-Time Framework**: Your JavaScript is parsed and compiled to native C. There is no JavaScript engine (like V8) or interpreter included in the final binary.
|
|
10
|
+
- **Microscopic Footprint**: Generated executables are typically ~50KB and start in milliseconds.
|
|
11
|
+
- **Single-Binary Deployment**: Static assets (HTML, CSS, JS, images) are automatically minified and embedded directly into the executable as C byte arrays.
|
|
12
|
+
- **Native Performance**: Uses `kqueue` (macOS) or `epoll` (Linux) event loops for high-performance non-blocking I/O.
|
|
13
|
+
- **File-Based Routing**: Intuitive `app/routes/` directory structure, supporting dynamic segments (e.g., `/art/[key].js`).
|
|
14
|
+
|
|
15
|
+
## Getting Started
|
|
16
|
+
|
|
17
|
+
1. Install globally (requires `gcc` or `clang` on your system):
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g cerver
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
2. Create a new project:
|
|
23
|
+
```bash
|
|
24
|
+
cerver new my-fast-api
|
|
25
|
+
cd my-fast-api
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
3. Build and Run:
|
|
29
|
+
```bash
|
|
30
|
+
cerver build
|
|
31
|
+
cerver run
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Routing
|
|
35
|
+
|
|
36
|
+
Routes are defined in the `app/routes/` directory.
|
|
37
|
+
|
|
38
|
+
`app/routes/index.js` (maps to `/`)
|
|
39
|
+
```javascript
|
|
40
|
+
export function GET(req, res) {
|
|
41
|
+
return res.html(200, "<h1>Hello World!</h1>");
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`app/routes/api/status.js` (maps to `/api/status`)
|
|
46
|
+
```javascript
|
|
47
|
+
export function GET(req, res) {
|
|
48
|
+
return res.json(200, '{"status": "online"}');
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`app/routes/users/[id].js` (maps to `/users/:id`)
|
|
53
|
+
```javascript
|
|
54
|
+
export function GET(req, res) {
|
|
55
|
+
const userId = req.params.id;
|
|
56
|
+
return res.text(200, "User ID: " + userId);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## The Request & Response Objects
|
|
61
|
+
|
|
62
|
+
Because Cerver compiles to C, the API surface is restricted.
|
|
63
|
+
|
|
64
|
+
**Request (`req`)**
|
|
65
|
+
- `req.path` — The request URL path
|
|
66
|
+
- `req.method` — The HTTP method
|
|
67
|
+
- `req.headers["user-agent"]` — Access request headers
|
|
68
|
+
- `req.query.search` — Access URL query parameters
|
|
69
|
+
- `req.params.id` — Access dynamic path segments
|
|
70
|
+
|
|
71
|
+
**Response (`res`)**
|
|
72
|
+
- `res.text(status, string)` — Send plain text
|
|
73
|
+
- `res.json(status, string)` — Send JSON
|
|
74
|
+
- `res.html(status, string)` — Send HTML
|
|
75
|
+
|
|
76
|
+
## Supported JavaScript
|
|
77
|
+
|
|
78
|
+
Cerver supports a strict, synchronous subset of JavaScript suitable for C code generation:
|
|
79
|
+
- `if`/`else` statements
|
|
80
|
+
- `const` / `let` variable declarations
|
|
81
|
+
- String and Number literals
|
|
82
|
+
- Template literals
|
|
83
|
+
- Basic comparisons (`===`, `!==`, `<`, `>`)
|
|
84
|
+
|
|
85
|
+
**Not Supported (Compile-Time Errors):**
|
|
86
|
+
- `async`/`await` and Promises
|
|
87
|
+
- Loops (`for`, `while`)
|
|
88
|
+
- Classes and the `new` keyword
|
|
89
|
+
- `eval()`
|
|
90
|
+
- Runtime `import`/`require`
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
`cerver.config.js`:
|
|
95
|
+
```javascript
|
|
96
|
+
export default {
|
|
97
|
+
port: 8080, // Default port
|
|
98
|
+
embed: true, // Embed assets from public/ into the binary
|
|
99
|
+
minify: true, // Minify HTML/CSS/JS before embedding
|
|
100
|
+
compression: "none" // Future: pre-compress assets
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## How It Works
|
|
105
|
+
|
|
106
|
+
1. **Parser**: Uses Acorn to parse your JS route files into ASTs.
|
|
107
|
+
2. **Validator**: Scans the AST to ensure no unsupported JS features are used.
|
|
108
|
+
3. **IR**: Transforms the AST into an Intermediate Representation.
|
|
109
|
+
4. **Generator**: Emits optimized C code mapping directly to your JS logic.
|
|
110
|
+
5. **Asset Pipeline**: Scans the `public/` folder, minifies files, and converts them to C byte arrays.
|
|
111
|
+
6. **Compiler**: Invokes `gcc` or `clang` to compile the generated code and the Cerver runtime into a native binary.
|
package/bin/cerver.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const { Command } = require("commander");
|
|
6
|
+
const pkg = require("../package.json");
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name("cerver")
|
|
12
|
+
.description("Compile restricted JavaScript into native C server binaries")
|
|
13
|
+
.version(pkg.version);
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command("new <name>")
|
|
17
|
+
.description("Create a new cerver project")
|
|
18
|
+
.action((name) => {
|
|
19
|
+
const { newProject } = require("../lib/commands/new");
|
|
20
|
+
newProject(name);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command("build")
|
|
25
|
+
.description("Compile the project into a native binary")
|
|
26
|
+
.option("--embed", "Embed static assets into the binary", true)
|
|
27
|
+
.option("--no-embed", "Serve static assets from the filesystem")
|
|
28
|
+
.option("--static", "Produce a statically linked binary")
|
|
29
|
+
.option("--no-minify", "Skip asset minification")
|
|
30
|
+
.action((opts) => {
|
|
31
|
+
const { build } = require("../lib/commands/build");
|
|
32
|
+
build(opts);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command("run")
|
|
37
|
+
.description("Run the compiled binary")
|
|
38
|
+
.option("-p, --port <port>", "Override the port")
|
|
39
|
+
.action((opts) => {
|
|
40
|
+
const { run } = require("../lib/commands/run");
|
|
41
|
+
run(opts);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
program.parse();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Discover all static assets in the public/ directory.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} publicDir - Absolute path to public/
|
|
10
|
+
* @returns {Array<{ filePath: string, servePath: string, ext: string, size: number }>}
|
|
11
|
+
*/
|
|
12
|
+
function discoverAssets(publicDir) {
|
|
13
|
+
const assets = [];
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(publicDir)) {
|
|
16
|
+
return assets;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function walk(dir, prefix) {
|
|
20
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
21
|
+
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const fullPath = path.join(dir, entry.name);
|
|
24
|
+
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
walk(fullPath, prefix + "/" + entry.name);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (entry.name.startsWith(".")) continue; /* skip dotfiles */
|
|
31
|
+
|
|
32
|
+
const stat = fs.statSync(fullPath);
|
|
33
|
+
const servePath = prefix + "/" + entry.name;
|
|
34
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
35
|
+
|
|
36
|
+
assets.push({
|
|
37
|
+
filePath: fullPath,
|
|
38
|
+
servePath,
|
|
39
|
+
ext,
|
|
40
|
+
size: stat.size,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
walk(publicDir, "");
|
|
46
|
+
|
|
47
|
+
return assets;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { discoverAssets };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { minifyContent } = require("./minify");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* MIME type lookup for embedding.
|
|
9
|
+
*/
|
|
10
|
+
const MIME_MAP = {
|
|
11
|
+
".html": "text/html; charset=utf-8",
|
|
12
|
+
".htm": "text/html; charset=utf-8",
|
|
13
|
+
".css": "text/css; charset=utf-8",
|
|
14
|
+
".js": "application/javascript; charset=utf-8",
|
|
15
|
+
".json": "application/json; charset=utf-8",
|
|
16
|
+
".txt": "text/plain; charset=utf-8",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".jpg": "image/jpeg",
|
|
19
|
+
".jpeg": "image/jpeg",
|
|
20
|
+
".gif": "image/gif",
|
|
21
|
+
".svg": "image/svg+xml",
|
|
22
|
+
".ico": "image/x-icon",
|
|
23
|
+
".webp": "image/webp",
|
|
24
|
+
".woff": "font/woff",
|
|
25
|
+
".woff2": "font/woff2",
|
|
26
|
+
".ttf": "font/ttf",
|
|
27
|
+
".pdf": "application/pdf",
|
|
28
|
+
".mp4": "video/mp4",
|
|
29
|
+
".webm": "video/webm",
|
|
30
|
+
".mp3": "audio/mpeg",
|
|
31
|
+
".wav": "audio/wav",
|
|
32
|
+
".xml": "application/xml",
|
|
33
|
+
".md": "text/markdown; charset=utf-8",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function mimeFromExt(ext) {
|
|
37
|
+
return MIME_MAP[ext] || "application/octet-stream";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert a serve path to a C-safe variable name.
|
|
42
|
+
* e.g. "/static/styles.css" → "asset_static_styles_css"
|
|
43
|
+
*/
|
|
44
|
+
function varName(servePath) {
|
|
45
|
+
return (
|
|
46
|
+
"asset_" +
|
|
47
|
+
servePath
|
|
48
|
+
.replace(/^\//, "")
|
|
49
|
+
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
50
|
+
.replace(/_+/g, "_")
|
|
51
|
+
.replace(/_$/, "")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Convert a buffer to a C hex byte array string.
|
|
57
|
+
* e.g. { 0x3c, 0x21, 0x44, ... }
|
|
58
|
+
*/
|
|
59
|
+
function bufferToHexArray(buf) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
for (let i = 0; i < buf.length; i += 16) {
|
|
62
|
+
const slice = buf.slice(i, Math.min(i + 16, buf.length));
|
|
63
|
+
const hex = Array.from(slice)
|
|
64
|
+
.map((b) => "0x" + b.toString(16).padStart(2, "0"))
|
|
65
|
+
.join(", ");
|
|
66
|
+
lines.push(" " + hex + ",");
|
|
67
|
+
}
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate C source code for embedded assets.
|
|
73
|
+
*
|
|
74
|
+
* @param {Array<{ filePath: string, servePath: string, ext: string }>} assets
|
|
75
|
+
* @param {boolean} shouldMinify - Whether to minify text assets
|
|
76
|
+
* @returns {Promise<string>} - C source code
|
|
77
|
+
*/
|
|
78
|
+
async function generateEmbeddedAssets(assets, shouldMinify) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
|
|
81
|
+
lines.push("/* Auto-generated embedded assets — do not edit */");
|
|
82
|
+
lines.push("");
|
|
83
|
+
|
|
84
|
+
const assetEntries = [];
|
|
85
|
+
|
|
86
|
+
for (const asset of assets) {
|
|
87
|
+
let content = fs.readFileSync(asset.filePath);
|
|
88
|
+
|
|
89
|
+
/* Minify if applicable */
|
|
90
|
+
if (shouldMinify) {
|
|
91
|
+
content = await minifyContent(content, asset.ext);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const name = varName(asset.servePath);
|
|
95
|
+
const mime = mimeFromExt(asset.ext);
|
|
96
|
+
|
|
97
|
+
/* Generate the byte array */
|
|
98
|
+
lines.push(`static const unsigned char ${name}[] = {`);
|
|
99
|
+
lines.push(bufferToHexArray(content));
|
|
100
|
+
lines.push("};");
|
|
101
|
+
lines.push(
|
|
102
|
+
`static const unsigned int ${name}_len = ${content.length};`
|
|
103
|
+
);
|
|
104
|
+
lines.push("");
|
|
105
|
+
|
|
106
|
+
assetEntries.push({ name, servePath: asset.servePath, mime });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Generate the asset table */
|
|
110
|
+
lines.push("static cerver_asset_t cerver_embedded_assets[] = {");
|
|
111
|
+
for (const entry of assetEntries) {
|
|
112
|
+
lines.push(
|
|
113
|
+
` { "${entry.servePath}", "${entry.mime}", ${entry.name}, ${entry.name}_len },`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
lines.push("};");
|
|
117
|
+
lines.push(
|
|
118
|
+
`static const int cerver_embedded_asset_count = ${assetEntries.length};`
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { generateEmbeddedAssets, varName, mimeFromExt };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minify assets using available tools.
|
|
7
|
+
*
|
|
8
|
+
* Falls back gracefully if minification packages aren't installed.
|
|
9
|
+
* Minification is done in-memory — original files are not modified.
|
|
10
|
+
*
|
|
11
|
+
* @param {Buffer} content - File content
|
|
12
|
+
* @param {string} ext - File extension (e.g. ".html", ".css", ".js")
|
|
13
|
+
* @returns {Buffer} - Minified content (or original if minification unavailable)
|
|
14
|
+
*/
|
|
15
|
+
async function minifyContent(content, ext) {
|
|
16
|
+
const source = content.toString("utf8");
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
switch (ext) {
|
|
20
|
+
case ".html":
|
|
21
|
+
case ".htm": {
|
|
22
|
+
try {
|
|
23
|
+
const { minify } = require("html-minifier-terser");
|
|
24
|
+
const result = await minify(source, {
|
|
25
|
+
collapseWhitespace: true,
|
|
26
|
+
removeComments: true,
|
|
27
|
+
removeRedundantAttributes: true,
|
|
28
|
+
removeEmptyAttributes: true,
|
|
29
|
+
minifyCSS: true,
|
|
30
|
+
minifyJS: true,
|
|
31
|
+
});
|
|
32
|
+
return Buffer.from(result, "utf8");
|
|
33
|
+
} catch {
|
|
34
|
+
/* html-minifier-terser not installed — skip */
|
|
35
|
+
return content;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case ".css": {
|
|
40
|
+
try {
|
|
41
|
+
const { transform } = require("lightningcss");
|
|
42
|
+
const result = transform({
|
|
43
|
+
filename: "style.css",
|
|
44
|
+
code: content,
|
|
45
|
+
minify: true,
|
|
46
|
+
});
|
|
47
|
+
return Buffer.from(result.code);
|
|
48
|
+
} catch {
|
|
49
|
+
/* lightningcss not installed — try basic minification */
|
|
50
|
+
const minified = source
|
|
51
|
+
.replace(/\/\*[\s\S]*?\*\//g, "") /* remove comments */
|
|
52
|
+
.replace(/\s+/g, " ") /* collapse whitespace */
|
|
53
|
+
.replace(/\s*([{}:;,])\s*/g, "$1") /* remove space around symbols */
|
|
54
|
+
.trim();
|
|
55
|
+
return Buffer.from(minified, "utf8");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case ".js":
|
|
60
|
+
case ".mjs": {
|
|
61
|
+
try {
|
|
62
|
+
const { minify } = require("terser");
|
|
63
|
+
const result = await minify(source);
|
|
64
|
+
if (result.code) {
|
|
65
|
+
return Buffer.from(result.code, "utf8");
|
|
66
|
+
}
|
|
67
|
+
return content;
|
|
68
|
+
} catch {
|
|
69
|
+
/* terser not installed — skip */
|
|
70
|
+
return content;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
default:
|
|
75
|
+
return content;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
/* If minification fails for any reason, return original */
|
|
79
|
+
return content;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { minifyContent };
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* emit.js — Low-level C code emission helpers.
|
|
5
|
+
*
|
|
6
|
+
* Builds C source strings from IR nodes with proper formatting,
|
|
7
|
+
* escaping, and indentation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Escape a string for use as a C string literal.
|
|
12
|
+
*/
|
|
13
|
+
function escapeC(str) {
|
|
14
|
+
return str
|
|
15
|
+
.replace(/\\/g, "\\\\")
|
|
16
|
+
.replace(/"/g, '\\"')
|
|
17
|
+
.replace(/\n/g, "\\n")
|
|
18
|
+
.replace(/\r/g, "\\r")
|
|
19
|
+
.replace(/\t/g, "\\t")
|
|
20
|
+
.replace(/\0/g, "\\0");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate a C string literal from a JS string.
|
|
25
|
+
*/
|
|
26
|
+
function cString(str) {
|
|
27
|
+
return `"${escapeC(str)}"`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate an indented line.
|
|
32
|
+
*/
|
|
33
|
+
function indent(level) {
|
|
34
|
+
return " ".repeat(level);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert a URL path + method into a C-safe function name.
|
|
39
|
+
* e.g. "GET", "/item/:id" → "handle_GET_item_id"
|
|
40
|
+
*/
|
|
41
|
+
function handlerName(method, urlPath) {
|
|
42
|
+
const safe = urlPath
|
|
43
|
+
.replace(/^\//, "") /* remove leading slash */
|
|
44
|
+
.replace(/\//g, "_") /* / → _ */
|
|
45
|
+
.replace(/:/g, "") /* remove : from params */
|
|
46
|
+
.replace(/[^a-zA-Z0-9_]/g, "_")
|
|
47
|
+
.replace(/_+/g, "_") /* collapse multiple underscores */
|
|
48
|
+
.replace(/_$/, ""); /* remove trailing underscore */
|
|
49
|
+
|
|
50
|
+
if (!safe) return `handle_${method}_index`;
|
|
51
|
+
return `handle_${method}_${safe}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Emit a C expression from an IR expression node.
|
|
56
|
+
*/
|
|
57
|
+
function emitExpression(expr) {
|
|
58
|
+
if (!expr) return '""';
|
|
59
|
+
|
|
60
|
+
switch (expr.type) {
|
|
61
|
+
case "StringLiteral":
|
|
62
|
+
return cString(expr.value);
|
|
63
|
+
|
|
64
|
+
case "NumberLiteral":
|
|
65
|
+
return String(expr.value);
|
|
66
|
+
|
|
67
|
+
case "Identifier":
|
|
68
|
+
return expr.name;
|
|
69
|
+
|
|
70
|
+
case "ParamAccess":
|
|
71
|
+
return `cerver_req_param(req, ${cString(expr.paramName)})`;
|
|
72
|
+
|
|
73
|
+
case "QueryAccess":
|
|
74
|
+
return `cerver_req_query(req, ${cString(expr.queryName)})`;
|
|
75
|
+
|
|
76
|
+
case "HeaderAccess":
|
|
77
|
+
return `cerver_req_header(req, ${cString(expr.headerName)})`;
|
|
78
|
+
|
|
79
|
+
case "Comparison": {
|
|
80
|
+
const left = emitExpression(expr.left);
|
|
81
|
+
const right = emitExpression(expr.right);
|
|
82
|
+
|
|
83
|
+
/* String comparisons use strcmp */
|
|
84
|
+
if (isStringExpr(expr.left) || isStringExpr(expr.right)) {
|
|
85
|
+
if (expr.operator === "===" || expr.operator === "==") {
|
|
86
|
+
return `(strcmp(${left}, ${right}) == 0)`;
|
|
87
|
+
}
|
|
88
|
+
if (expr.operator === "!==" || expr.operator === "!=") {
|
|
89
|
+
return `(strcmp(${left}, ${right}) != 0)`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Numeric comparisons */
|
|
94
|
+
const cOp = expr.operator === "===" ? "==" : expr.operator === "!==" ? "!=" : expr.operator;
|
|
95
|
+
return `(${left} ${cOp} ${right})`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case "Logical": {
|
|
99
|
+
const left = emitExpression(expr.left);
|
|
100
|
+
const right = emitExpression(expr.right);
|
|
101
|
+
return `(${left} ${expr.operator} ${right})`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case "Unary": {
|
|
105
|
+
const arg = emitExpression(expr.argument);
|
|
106
|
+
return `(${expr.operator}${arg})`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case "Concat": {
|
|
110
|
+
/* For template literals, we generate snprintf into a stack buffer */
|
|
111
|
+
/* This is handled specially in emitStatement when part of a return */
|
|
112
|
+
/* For simple cases, just concatenate literals */
|
|
113
|
+
const parts = expr.parts.map(emitExpression);
|
|
114
|
+
/* If all parts are string literals, we can concatenate at compile time */
|
|
115
|
+
const allLiteral = expr.parts.every((p) => p.type === "StringLiteral");
|
|
116
|
+
if (allLiteral) {
|
|
117
|
+
return cString(expr.parts.map((p) => p.value).join(""));
|
|
118
|
+
}
|
|
119
|
+
/* Otherwise, we'll need a runtime sprintf — return a placeholder */
|
|
120
|
+
/* The generator will handle this at the statement level */
|
|
121
|
+
return `__concat_${parts.length}__`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
case "Call": {
|
|
125
|
+
/* res.text(status, body) etc. */
|
|
126
|
+
if (expr.object === "res") {
|
|
127
|
+
const fnName = `cerver_res_${expr.method}`;
|
|
128
|
+
const args = expr.args.map(emitExpression);
|
|
129
|
+
return `${fnName}(res, ${args.join(", ")})`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* String method calls — map to C equivalents */
|
|
133
|
+
if (expr.method === "toLowerCase" || expr.method === "toUpperCase") {
|
|
134
|
+
/* These would need a helper — for now return the object as-is */
|
|
135
|
+
return emitExpression(expr.object);
|
|
136
|
+
}
|
|
137
|
+
if (expr.method === "includes") {
|
|
138
|
+
const haystack = emitExpression(expr.object);
|
|
139
|
+
const needle = expr.args[0] ? emitExpression(expr.args[0]) : '""';
|
|
140
|
+
return `(strstr(${haystack}, ${needle}) != NULL)`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return '""';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
default:
|
|
147
|
+
return '""';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if an IR expression evaluates to a string type.
|
|
153
|
+
*/
|
|
154
|
+
function isStringExpr(expr) {
|
|
155
|
+
if (!expr) return false;
|
|
156
|
+
return (
|
|
157
|
+
expr.type === "StringLiteral" ||
|
|
158
|
+
expr.type === "ParamAccess" ||
|
|
159
|
+
expr.type === "QueryAccess" ||
|
|
160
|
+
expr.type === "HeaderAccess" ||
|
|
161
|
+
expr.type === "Concat" ||
|
|
162
|
+
expr.type === "Identifier"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Emit a statement into C code lines.
|
|
168
|
+
* Returns an array of C source lines.
|
|
169
|
+
*/
|
|
170
|
+
function emitStatement(stmt, level) {
|
|
171
|
+
if (!stmt) return [];
|
|
172
|
+
const pad = indent(level);
|
|
173
|
+
const lines = [];
|
|
174
|
+
|
|
175
|
+
switch (stmt.type) {
|
|
176
|
+
case "Return": {
|
|
177
|
+
const fnName = `cerver_res_${stmt.responseType}`;
|
|
178
|
+
const valueCode = emitExpression(stmt.value);
|
|
179
|
+
lines.push(`${pad}${fnName}(res, ${stmt.status}, ${valueCode});`);
|
|
180
|
+
lines.push(`${pad}return;`);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case "If": {
|
|
185
|
+
const cond = emitExpression(stmt.condition);
|
|
186
|
+
lines.push(`${pad}if (${cond}) {`);
|
|
187
|
+
|
|
188
|
+
if (stmt.thenBody) {
|
|
189
|
+
for (const s of stmt.thenBody) {
|
|
190
|
+
lines.push(...emitStatement(s, level + 1));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (stmt.elseBody && stmt.elseBody.length > 0) {
|
|
195
|
+
/* Check if it's an else-if */
|
|
196
|
+
if (stmt.elseBody.length === 1 && stmt.elseBody[0].type === "If") {
|
|
197
|
+
lines.push(`${pad}} else `);
|
|
198
|
+
/* Emit the else-if inline */
|
|
199
|
+
const elseIfLines = emitStatement(stmt.elseBody[0], level);
|
|
200
|
+
/* Remove leading whitespace from first line to make it "} else if" */
|
|
201
|
+
if (elseIfLines.length > 0) {
|
|
202
|
+
elseIfLines[0] = elseIfLines[0].trimStart();
|
|
203
|
+
lines[lines.length - 1] += elseIfLines[0];
|
|
204
|
+
lines.push(...elseIfLines.slice(1));
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
lines.push(`${pad}} else {`);
|
|
208
|
+
for (const s of stmt.elseBody) {
|
|
209
|
+
lines.push(...emitStatement(s, level + 1));
|
|
210
|
+
}
|
|
211
|
+
lines.push(`${pad}}`);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
lines.push(`${pad}}`);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case "Variable": {
|
|
220
|
+
const val = emitExpression(stmt.initExpr);
|
|
221
|
+
if (stmt.valueType === "number") {
|
|
222
|
+
lines.push(`${pad}int ${stmt.name} = ${val};`);
|
|
223
|
+
} else {
|
|
224
|
+
lines.push(`${pad}const char *${stmt.name} = ${val};`);
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case "Call": {
|
|
230
|
+
lines.push(`${pad}${emitExpression(stmt)};`);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
default:
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return lines;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
escapeC,
|
|
243
|
+
cString,
|
|
244
|
+
indent,
|
|
245
|
+
handlerName,
|
|
246
|
+
emitExpression,
|
|
247
|
+
emitStatement,
|
|
248
|
+
isStringExpr,
|
|
249
|
+
};
|