@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 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
+ };