@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.
- package/.github/workflows/publish.yml +48 -0
- package/README.md +28 -17
- package/bin/cerver.js +10 -0
- package/lib/assets/compress.js +55 -0
- package/lib/assets/embed.js +73 -4
- package/lib/codegen/generator.js +1 -1
- package/lib/commands/build.js +3 -2
- package/lib/commands/dev.js +146 -0
- package/lib/compiler/compile.js +1 -0
- package/lib/config.js +5 -1
- package/package.json +6 -1
- package/runtime/cerver.h +22 -1
- package/runtime/router.c +1 -1
- package/runtime/server.c +238 -27
- package/runtime/static.c +78 -15
- package/test/run.js +355 -0
|
@@ -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., `/
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm i @velox0/cerver@latest
|
|
21
|
+
```
|
|
21
22
|
|
|
22
23
|
2. Create a new project:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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,
|
|
98
|
-
embed: true,
|
|
99
|
-
minify: true,
|
|
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 };
|
package/lib/assets/embed.js
CHANGED
|
@@ -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
|
-
|
|
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}",
|
|
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("};");
|
package/lib/codegen/generator.js
CHANGED
|
@@ -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(
|
|
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);"
|
package/lib/commands/build.js
CHANGED
|
@@ -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 };
|
package/lib/compiler/compile.js
CHANGED
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.
|
|
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="/
|
|
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
|