@velox0/cerver 0.1.0 → 0.3.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.
@@ -0,0 +1,48 @@
1
+ name: Publish package
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ release:
7
+ types: [published]
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+ id-token: write
13
+
14
+ jobs:
15
+ publish:
16
+ name: Publish to npm
17
+ runs-on: ubuntu-latest
18
+
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Setup pnpm
24
+ uses: pnpm/action-setup@v4
25
+ with:
26
+ version: 10
27
+ run_install: false
28
+
29
+ - name: Setup Node.js
30
+ uses: actions/setup-node@v4
31
+ with:
32
+ node-version: 20
33
+ registry-url: https://registry.npmjs.org
34
+ cache: pnpm
35
+
36
+ - name: Install dependencies
37
+ run: pnpm install --frozen-lockfile
38
+
39
+ - name: Run tests
40
+ run: pnpm test
41
+
42
+ - name: Verify package contents
43
+ run: npm pack --dry-run
44
+
45
+ - name: Publish to npm
46
+ run: npm publish --access public --provenance
47
+ env:
48
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -10,32 +10,36 @@ Cerver takes a Next.js-style file-based routing structure (written in a strict s
10
10
  - **Microscopic Footprint**: Generated executables are typically ~50KB and start in milliseconds.
11
11
  - **Single-Binary Deployment**: Static assets (HTML, CSS, JS, images) are automatically minified and embedded directly into the executable as C byte arrays.
12
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`).
13
+ - **File-Based Routing**: Intuitive `app/routes/` directory structure, supporting dynamic segments (e.g., `/item/[id].js`).
14
14
 
15
15
  ## Getting Started
16
16
 
17
17
  1. Install globally (requires `gcc` or `clang` on your system):
18
- ```bash
19
- npm install -g cerver
20
- ```
18
+
19
+ ```bash
20
+ npm i @velox0/cerver@latest
21
+ ```
21
22
 
22
23
  2. Create a new project:
23
- ```bash
24
- cerver new my-fast-api
25
- cd my-fast-api
26
- ```
24
+
25
+ ```bash
26
+ cerver new my-fast-api
27
+ cd my-fast-api
28
+ ```
27
29
 
28
30
  3. Build and Run:
29
- ```bash
30
- cerver build
31
- cerver run
32
- ```
31
+
32
+ ```bash
33
+ cerver build
34
+ cerver run
35
+ ```
33
36
 
34
37
  ## Routing
35
38
 
36
39
  Routes are defined in the `app/routes/` directory.
37
40
 
38
41
  `app/routes/index.js` (maps to `/`)
42
+
39
43
  ```javascript
40
44
  export function GET(req, res) {
41
45
  return res.html(200, "<h1>Hello World!</h1>");
@@ -43,6 +47,7 @@ export function GET(req, res) {
43
47
  ```
44
48
 
45
49
  `app/routes/api/status.js` (maps to `/api/status`)
50
+
46
51
  ```javascript
47
52
  export function GET(req, res) {
48
53
  return res.json(200, '{"status": "online"}');
@@ -50,6 +55,7 @@ export function GET(req, res) {
50
55
  ```
51
56
 
52
57
  `app/routes/users/[id].js` (maps to `/users/:id`)
58
+
53
59
  ```javascript
54
60
  export function GET(req, res) {
55
61
  const userId = req.params.id;
@@ -62,6 +68,7 @@ export function GET(req, res) {
62
68
  Because Cerver compiles to C, the API surface is restricted.
63
69
 
64
70
  **Request (`req`)**
71
+
65
72
  - `req.path` — The request URL path
66
73
  - `req.method` — The HTTP method
67
74
  - `req.headers["user-agent"]` — Access request headers
@@ -69,6 +76,7 @@ Because Cerver compiles to C, the API surface is restricted.
69
76
  - `req.params.id` — Access dynamic path segments
70
77
 
71
78
  **Response (`res`)**
79
+
72
80
  - `res.text(status, string)` — Send plain text
73
81
  - `res.json(status, string)` — Send JSON
74
82
  - `res.html(status, string)` — Send HTML
@@ -76,6 +84,7 @@ Because Cerver compiles to C, the API surface is restricted.
76
84
  ## Supported JavaScript
77
85
 
78
86
  Cerver supports a strict, synchronous subset of JavaScript suitable for C code generation:
87
+
79
88
  - `if`/`else` statements
80
89
  - `const` / `let` variable declarations
81
90
  - String and Number literals
@@ -83,6 +92,7 @@ Cerver supports a strict, synchronous subset of JavaScript suitable for C code g
83
92
  - Basic comparisons (`===`, `!==`, `<`, `>`)
84
93
 
85
94
  **Not Supported (Compile-Time Errors):**
95
+
86
96
  - `async`/`await` and Promises
87
97
  - Loops (`for`, `while`)
88
98
  - Classes and the `new` keyword
@@ -92,13 +102,14 @@ Cerver supports a strict, synchronous subset of JavaScript suitable for C code g
92
102
  ## Configuration
93
103
 
94
104
  `cerver.config.js`:
105
+
95
106
  ```javascript
96
107
  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
- }
108
+ port: 8080, // Default port
109
+ embed: true, // Embed assets from public/ into the binary
110
+ minify: true, // Minify HTML/CSS/JS before embedding
111
+ compression: "none", // Future: pre-compress assets
112
+ };
102
113
  ```
103
114
 
104
115
  ## How It Works
package/bin/cerver.js CHANGED
@@ -32,6 +32,16 @@ program
32
32
  build(opts);
33
33
  });
34
34
 
35
+ program
36
+ .command("dev")
37
+ .description("Watch for changes, auto-rebuild, and restart the server")
38
+ .option("-p, --port <port>", "Override the port")
39
+ .option("--no-embed", "Serve static assets from the filesystem")
40
+ .action((opts) => {
41
+ const { dev } = require("../lib/commands/dev");
42
+ dev(opts);
43
+ });
44
+
35
45
  program
36
46
  .command("run")
37
47
  .description("Run the compiled binary")
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+
3
+ const zlib = require("zlib");
4
+
5
+ /**
6
+ * Compress content using gzip or brotli.
7
+ *
8
+ * Uses Node.js built-in zlib — no external dependencies needed.
9
+ *
10
+ * @param {Buffer} content - Raw content to compress
11
+ * @param {"gzip"|"brotli"} algorithm - Compression algorithm
12
+ * @returns {Promise<Buffer>} - Compressed content
13
+ */
14
+ function compressContent(content, algorithm) {
15
+ return new Promise((resolve, reject) => {
16
+ if (algorithm === "gzip") {
17
+ zlib.gzip(content, { level: 9 }, (err, result) => {
18
+ if (err) reject(err);
19
+ else resolve(result);
20
+ });
21
+ } else if (algorithm === "brotli") {
22
+ zlib.brotliCompress(
23
+ content,
24
+ {
25
+ params: {
26
+ [zlib.constants.BROTLI_PARAM_QUALITY]:
27
+ zlib.constants.BROTLI_MAX_QUALITY,
28
+ },
29
+ },
30
+ (err, result) => {
31
+ if (err) reject(err);
32
+ else resolve(result);
33
+ }
34
+ );
35
+ } else {
36
+ resolve(content);
37
+ }
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Check if a MIME type is worth compressing.
43
+ * Binary formats like PNG, JPEG, WOFF2 are already compressed.
44
+ */
45
+ function isCompressible(mime) {
46
+ return (
47
+ mime.startsWith("text/") ||
48
+ mime.includes("javascript") ||
49
+ mime.includes("json") ||
50
+ mime.includes("xml") ||
51
+ mime.includes("svg")
52
+ );
53
+ }
54
+
55
+ module.exports = { compressContent, isCompressible };
@@ -3,6 +3,7 @@
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
5
  const { minifyContent } = require("./minify");
6
+ const { compressContent, isCompressible } = require("./compress");
6
7
 
7
8
  /**
8
9
  * MIME type lookup for embedding.
@@ -73,10 +74,12 @@ function bufferToHexArray(buf) {
73
74
  *
74
75
  * @param {Array<{ filePath: string, servePath: string, ext: string }>} assets
75
76
  * @param {boolean} shouldMinify - Whether to minify text assets
77
+ * @param {string} compression - "none" | "gzip" | "brotli" | "both"
76
78
  * @returns {Promise<string>} - C source code
77
79
  */
78
- async function generateEmbeddedAssets(assets, shouldMinify) {
80
+ async function generateEmbeddedAssets(assets, shouldMinify, compression) {
79
81
  const lines = [];
82
+ const algo = compression || "none";
80
83
 
81
84
  lines.push("/* Auto-generated embedded assets — do not edit */");
82
85
  lines.push("");
@@ -94,7 +97,7 @@ async function generateEmbeddedAssets(assets, shouldMinify) {
94
97
  const name = varName(asset.servePath);
95
98
  const mime = mimeFromExt(asset.ext);
96
99
 
97
- /* Generate the byte array */
100
+ /* Generate the raw byte array */
98
101
  lines.push(`static const unsigned char ${name}[] = {`);
99
102
  lines.push(bufferToHexArray(content));
100
103
  lines.push("};");
@@ -103,14 +106,80 @@ async function generateEmbeddedAssets(assets, shouldMinify) {
103
106
  );
104
107
  lines.push("");
105
108
 
106
- assetEntries.push({ name, servePath: asset.servePath, mime });
109
+ /* Generate compressed variants if the content is compressible */
110
+ let gzName = "NULL";
111
+ let gzLen = "0";
112
+ let brName = "NULL";
113
+ let brLen = "0";
114
+
115
+ if (isCompressible(mime) && algo !== "none" && content.length > 256) {
116
+ if (algo === "gzip" || algo === "both") {
117
+ const gzData = await compressContent(content, "gzip");
118
+ /* Only embed if compression actually saves space */
119
+ if (gzData.length < content.length * 0.9) {
120
+ gzName = `${name}_gz`;
121
+ gzLen = `${name}_gz_len`;
122
+ lines.push(`static const unsigned char ${gzName}[] = {`);
123
+ lines.push(bufferToHexArray(gzData));
124
+ lines.push("};");
125
+ lines.push(
126
+ `static const unsigned int ${gzLen} = ${gzData.length};`
127
+ );
128
+ lines.push("");
129
+ }
130
+ }
131
+
132
+ if (algo === "brotli" || algo === "both") {
133
+ const brData = await compressContent(content, "brotli");
134
+ if (brData.length < content.length * 0.9) {
135
+ brName = `${name}_br`;
136
+ brLen = `${name}_br_len`;
137
+ lines.push(`static const unsigned char ${brName}[] = {`);
138
+ lines.push(bufferToHexArray(brData));
139
+ lines.push("};");
140
+ lines.push(
141
+ `static const unsigned int ${brLen} = ${brData.length};`
142
+ );
143
+ lines.push("");
144
+ }
145
+ }
146
+ }
147
+
148
+ assetEntries.push({
149
+ name,
150
+ servePath: asset.servePath,
151
+ mime,
152
+ gzName,
153
+ gzLen,
154
+ brName,
155
+ brLen,
156
+ });
157
+
158
+ /* Auto-alias: /any/path/index.html → /any/path */
159
+ if (asset.servePath.endsWith("/index.html")) {
160
+ const dirPath = asset.servePath === "/index.html"
161
+ ? "/"
162
+ : asset.servePath.slice(0, -"/index.html".length);
163
+ assetEntries.push({
164
+ name,
165
+ servePath: dirPath,
166
+ mime,
167
+ gzName,
168
+ gzLen,
169
+ brName,
170
+ brLen,
171
+ });
172
+ }
107
173
  }
108
174
 
109
175
  /* Generate the asset table */
110
176
  lines.push("static cerver_asset_t cerver_embedded_assets[] = {");
111
177
  for (const entry of assetEntries) {
112
178
  lines.push(
113
- ` { "${entry.servePath}", "${entry.mime}", ${entry.name}, ${entry.name}_len },`
179
+ ` { "${entry.servePath}", "${entry.mime}", ` +
180
+ `${entry.name}, ${entry.name}_len, ` +
181
+ `${entry.gzName}, ${entry.gzLen}, ` +
182
+ `${entry.brName}, ${entry.brLen} },`
114
183
  );
115
184
  }
116
185
  lines.push("};");
@@ -85,7 +85,7 @@ function generateServer(routes, config, assetsCode) {
85
85
  lines.push(" if (env_port) port = atoi(env_port);");
86
86
  lines.push("");
87
87
  lines.push(" cerver_server_t srv;");
88
- lines.push(" cerver_init(&srv, port);");
88
+ lines.push(` cerver_init(&srv, port, ${config.threads});`);
89
89
  lines.push("");
90
90
  lines.push(
91
91
  " cerver_add_routes(&srv, cerver_routes, cerver_route_count);"
@@ -78,9 +78,10 @@ async function build(opts) {
78
78
  );
79
79
  }
80
80
 
81
- assetsCode = await generateEmbeddedAssets(assets, config.minify);
81
+ assetsCode = await generateEmbeddedAssets(assets, config.minify, config.compression);
82
82
  console.log(
83
- ` ${assets.length} asset(s), ${(totalSize / 1024).toFixed(1)} KB total`
83
+ ` ${assets.length} asset(s), ${(totalSize / 1024).toFixed(1)} KB total` +
84
+ (config.compression !== "none" ? ` (${config.compression} compressed)` : "")
84
85
  );
85
86
  } else {
86
87
  console.log(" no assets found in public/");
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { spawn } = require("child_process");
6
+ const chokidar = require("chokidar");
7
+ const { build } = require("./build");
8
+
9
+ /**
10
+ * Dev mode: watch for changes, auto-rebuild, auto-restart.
11
+ */
12
+ async function dev(opts) {
13
+ const projectDir = process.cwd();
14
+ const binaryPath = path.join(projectDir, "dist", "server");
15
+
16
+ let serverProcess = null;
17
+ let building = false;
18
+ let pendingRebuild = false;
19
+
20
+ const port = opts.port || null;
21
+
22
+ /* ---- Initial build ---- */
23
+ async function rebuild() {
24
+ if (building) {
25
+ pendingRebuild = true;
26
+ return;
27
+ }
28
+
29
+ building = true;
30
+
31
+ /* Kill existing server */
32
+ if (serverProcess) {
33
+ console.log("\n ↻ restarting...\n");
34
+ serverProcess.kill("SIGTERM");
35
+ serverProcess = null;
36
+ /* Small delay for port release */
37
+ await new Promise((r) => setTimeout(r, 200));
38
+ }
39
+
40
+ try {
41
+ await build({
42
+ embed: opts.embed !== undefined ? opts.embed : true,
43
+ minify: false, /* Skip minification in dev for speed */
44
+ static: false,
45
+ });
46
+
47
+ /* Start the server */
48
+ startServer();
49
+ } catch (err) {
50
+ console.error(`\n ✗ build failed: ${err.message}\n`);
51
+ }
52
+
53
+ building = false;
54
+
55
+ /* If changes came in during build, rebuild again */
56
+ if (pendingRebuild) {
57
+ pendingRebuild = false;
58
+ await rebuild();
59
+ }
60
+ }
61
+
62
+ function startServer() {
63
+ if (!fs.existsSync(binaryPath)) return;
64
+
65
+ const env = { ...process.env };
66
+ if (port) env.CERVER_PORT = port;
67
+
68
+ serverProcess = spawn(binaryPath, [], {
69
+ stdio: "inherit",
70
+ env,
71
+ cwd: projectDir,
72
+ });
73
+
74
+ serverProcess.on("error", (err) => {
75
+ console.error(` ✗ server error: ${err.message}`);
76
+ serverProcess = null;
77
+ });
78
+
79
+ serverProcess.on("exit", (code, signal) => {
80
+ if (signal !== "SIGTERM" && signal !== "SIGINT") {
81
+ console.error(` ✗ server exited (code: ${code}, signal: ${signal})`);
82
+ }
83
+ serverProcess = null;
84
+ });
85
+ }
86
+
87
+ /* ---- Watch for changes ---- */
88
+ const watchPaths = [
89
+ path.join(projectDir, "app"),
90
+ path.join(projectDir, "public"),
91
+ path.join(projectDir, "cerver.config.js"),
92
+ ].filter((p) => fs.existsSync(p));
93
+
94
+ const watcher = chokidar.watch(watchPaths, {
95
+ ignored: [
96
+ /(^|[\/\\])\./, /* dotfiles */
97
+ /node_modules/,
98
+ /dist/,
99
+ ],
100
+ persistent: true,
101
+ ignoreInitial: true,
102
+ awaitWriteFinish: {
103
+ stabilityThreshold: 100,
104
+ pollInterval: 50,
105
+ },
106
+ });
107
+
108
+ /* Debounce: collect changes for 300ms before rebuilding */
109
+ let debounceTimer = null;
110
+
111
+ function scheduleRebuild(changedPath) {
112
+ const rel = path.relative(projectDir, changedPath);
113
+ console.log(` ⟐ changed: ${rel}`);
114
+
115
+ if (debounceTimer) clearTimeout(debounceTimer);
116
+ debounceTimer = setTimeout(() => {
117
+ debounceTimer = null;
118
+ rebuild();
119
+ }, 300);
120
+ }
121
+
122
+ watcher
123
+ .on("change", scheduleRebuild)
124
+ .on("add", scheduleRebuild)
125
+ .on("unlink", scheduleRebuild);
126
+
127
+ /* ---- Graceful shutdown ---- */
128
+ function shutdown() {
129
+ console.log("\n cerver dev: shutting down...");
130
+ watcher.close();
131
+ if (debounceTimer) clearTimeout(debounceTimer);
132
+ if (serverProcess) {
133
+ serverProcess.kill("SIGTERM");
134
+ }
135
+ process.exit(0);
136
+ }
137
+
138
+ process.on("SIGINT", shutdown);
139
+ process.on("SIGTERM", shutdown);
140
+
141
+ /* ---- Start ---- */
142
+ console.log("\n cerver dev — watching for changes\n");
143
+ await rebuild();
144
+ }
145
+
146
+ module.exports = { dev };
@@ -57,6 +57,7 @@ function compile(distDir, runtimeDir, opts) {
57
57
  serverC,
58
58
  ...runtimeSources,
59
59
  `-I${runtimeDir}`,
60
+ "-lpthread",
60
61
  ];
61
62
 
62
63
  if (opts && opts.static) {
package/lib/config.js CHANGED
@@ -8,6 +8,7 @@ const DEFAULTS = {
8
8
  embed: true,
9
9
  minify: true,
10
10
  compression: "none",
11
+ threads: 4,
11
12
  };
12
13
 
13
14
  /**
@@ -42,9 +43,12 @@ function loadConfig(projectDir) {
42
43
  if (typeof config.port !== "number" || config.port < 1 || config.port > 65535) {
43
44
  throw new Error(`cerver: invalid port ${config.port}`);
44
45
  }
45
- if (!["none", "gzip", "brotli"].includes(config.compression)) {
46
+ if (!["none", "gzip", "brotli", "both"].includes(config.compression)) {
46
47
  throw new Error(`cerver: unsupported compression "${config.compression}"`);
47
48
  }
49
+ if (!Number.isInteger(config.threads) || config.threads < 1 || config.threads > 64) {
50
+ throw new Error(`cerver: invalid thread count ${config.threads} (must be 1-64)`);
51
+ }
48
52
 
49
53
  return config;
50
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velox0/cerver",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Compile restricted JavaScript server logic into optimized native C binaries",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -18,11 +18,16 @@
18
18
  "binary",
19
19
  "framework"
20
20
  ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/velox0/cerver.git"
24
+ },
21
25
  "author": "Velox0",
22
26
  "license": "MIT",
23
27
  "dependencies": {
24
28
  "acorn": "^8.14.0",
25
29
  "acorn-walk": "^8.3.4",
30
+ "chokidar": "^3.6.0",
26
31
  "commander": "^13.1.0"
27
32
  }
28
33
  }
package/runtime/cerver.h CHANGED
@@ -11,6 +11,7 @@
11
11
 
12
12
  #include <stddef.h>
13
13
  #include <stdint.h>
14
+ #include <pthread.h>
14
15
 
15
16
  /* ------------------------------------------------------------------ */
16
17
  /* Limits */
@@ -22,8 +23,12 @@
22
23
  #define CERVER_MAX_PATH 2048
23
24
  #define CERVER_MAX_HEADER_VAL 4096
24
25
  #define CERVER_READ_BUF 8192
26
+ #define CERVER_READ_BUF_MAX (1 << 20) /* 1 MB hard limit */
25
27
  #define CERVER_MAX_ROUTES 256
26
28
 
29
+ #define CERVER_THREAD_POOL_DEFAULT 4
30
+ #define CERVER_TASK_QUEUE_SIZE 256
31
+
27
32
  /* ------------------------------------------------------------------ */
28
33
  /* Key-value pair (used for headers, query params, route params) */
29
34
  /* ------------------------------------------------------------------ */
@@ -125,6 +130,12 @@ typedef struct {
125
130
  const char *mime_type; /* e.g. "text/html" */
126
131
  const unsigned char *data;
127
132
  size_t data_len;
133
+
134
+ /* Pre-compressed variants (NULL if not available) */
135
+ const unsigned char *data_gz;
136
+ size_t data_gz_len;
137
+ const unsigned char *data_br;
138
+ size_t data_br_len;
128
139
  } cerver_asset_t;
129
140
 
130
141
  /* ------------------------------------------------------------------ */
@@ -140,10 +151,20 @@ typedef struct {
140
151
  int asset_count;
141
152
  const char *public_dir; /* NULL if embedded mode */
142
153
  volatile int running;
154
+
155
+ /* Thread pool */
156
+ int thread_count;
157
+ pthread_t *threads;
158
+ int task_queue[CERVER_TASK_QUEUE_SIZE];
159
+ int tq_head;
160
+ int tq_tail;
161
+ int tq_count;
162
+ pthread_mutex_t tq_mutex;
163
+ pthread_cond_t tq_cond;
143
164
  } cerver_server_t;
144
165
 
145
166
  /* Server lifecycle */
146
- int cerver_init(cerver_server_t *srv, int port);
167
+ int cerver_init(cerver_server_t *srv, int port, int threads);
147
168
  int cerver_add_routes(cerver_server_t *srv, cerver_route_t *routes, int count);
148
169
  int cerver_set_assets(cerver_server_t *srv, cerver_asset_t *assets, int count);
149
170
  void cerver_set_public_dir(cerver_server_t *srv, const char *dir);
package/runtime/router.c CHANGED
@@ -51,7 +51,7 @@ const char *cerver_req_header(const cerver_request_t *req, const char *key) {
51
51
  * Pattern segments starting with ':' are dynamic and extract values.
52
52
  *
53
53
  * Examples:
54
- * pattern="/art/:key" path="/art/sunset" → match, key="sunset"
54
+ * pattern="/items/:id" path="/items/123" → match, id="123"
55
55
  * pattern="/" path="/" → match
56
56
  * pattern="/api/data" path="/api/data" → match
57
57
  * pattern="/api/data" path="/api/other" → no match