@velox0/cerver 0.4.3 → 0.5.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 CHANGED
@@ -16,7 +16,7 @@ Cerver takes a Next.js-style file-based routing structure (written in a strict s
16
16
  - **Microscopic Footprint**: Generated executables are tiny and start in milliseconds.
17
17
  - **Single-Binary Deployment**: Static assets (HTML, CSS, JS, images) are automatically minified and embedded directly into the executable as C byte arrays.
18
18
  - **Native Performance**: Uses `kqueue` (macOS) or `epoll` (Linux) event loops for high-performance non-blocking I/O.
19
- - **File-Based Routing**: Intuitive `app/routes/` directory structure, supporting dynamic segments (e.g., `/item/[id].js`).
19
+ - **File-Based Routing**: Intuitive `routes/` directory structure, supporting dynamic segments (e.g., `/item/[id].js`).
20
20
 
21
21
  ## Benchmarks (Autocannon)
22
22
 
@@ -57,9 +57,9 @@ cerver run
57
57
 
58
58
  ## Routing
59
59
 
60
- Routes are defined in the `app/routes/` directory.
60
+ Routes are defined in the `routes/` directory.
61
61
 
62
- `app/routes/index.js` (maps to `/`)
62
+ `routes/index.js` (maps to `/`)
63
63
 
64
64
  ```javascript
65
65
  export function GET(req, res) {
@@ -67,7 +67,7 @@ export function GET(req, res) {
67
67
  }
68
68
  ```
69
69
 
70
- `app/routes/api/status.js` (maps to `/api/status`)
70
+ `routes/api/status.js` (maps to `/api/status`)
71
71
 
72
72
  ```javascript
73
73
  export function GET(req, res) {
@@ -75,7 +75,7 @@ export function GET(req, res) {
75
75
  }
76
76
  ```
77
77
 
78
- `app/routes/users/[id].js` (maps to `/users/:id`)
78
+ `routes/users/[id].js` (maps to `/users/:id`)
79
79
 
80
80
  ```javascript
81
81
  export function GET(req, res) {
@@ -84,6 +84,16 @@ export function GET(req, res) {
84
84
  }
85
85
  ```
86
86
 
87
+ ### Clean URL Asset Mapping
88
+
89
+ Cerver has a special asset routing convention that prevents folder clutter:
90
+ - **Root Index**: `public/index.html` is automatically served at the root `/`.
91
+ - **Directory Indexing**: Nested `index.html` files inside directories (e.g. `public/page/index.html`) are **not** implicitly aliased to `/page` (they remain at `/page/index.html`).
92
+ - **Clean Folder Slugs**: If an HTML file has the exact same name as its parent directory, it is mapped to the clean directory URL. For example:
93
+ - `public/about/about.html` maps to `/about` (and `/about/`)
94
+ - `public/blog/posts/posts.html` maps to `/blog/posts` (and `/blog/posts/`)
95
+ - **Other Static Assets**: All other assets like CSS, JS, images, and non-directory-matching HTML files serve directly at their file paths (e.g., `public/about/style.css` maps to `/about/style.css`).
96
+
87
97
  ## The Request & Response Objects
88
98
 
89
99
  Because Cerver compiles to C, the API surface is restricted.
@@ -82,6 +82,8 @@ async function generateEmbeddedAssets(assets, shouldMinify, compression) {
82
82
  const algo = compression || "none";
83
83
 
84
84
  lines.push("/* Auto-generated embedded assets — do not edit */");
85
+ lines.push('#include "cerver.h"');
86
+ lines.push("#include <stddef.h>");
85
87
  lines.push("");
86
88
 
87
89
  const assetEntries = [];
@@ -155,14 +157,37 @@ async function generateEmbeddedAssets(assets, shouldMinify, compression) {
155
157
  brLen,
156
158
  });
157
159
 
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);
160
+ /* Auto-alias rule:
161
+ 1. /index.html → /
162
+ 2. /path/to/name/name.html /path/to/name
163
+ */
164
+ let shouldAlias = false;
165
+ let aliasPath = null;
166
+
167
+ if (asset.servePath === "/index.html") {
168
+ shouldAlias = true;
169
+ aliasPath = "/";
170
+ } else if (asset.servePath.endsWith(".html")) {
171
+ const parts = asset.servePath.split("/");
172
+ if (parts.length >= 3) {
173
+ const fileNameWithExt = parts[parts.length - 1];
174
+ const parentDir = parts[parts.length - 2];
175
+ const baseName = fileNameWithExt.slice(0, -5); // Remove ".html"
176
+ if (baseName === parentDir) {
177
+ shouldAlias = true;
178
+ const lengthToRemove = fileNameWithExt.length + 1; // filename plus the slash before it
179
+ aliasPath = asset.servePath.slice(0, -lengthToRemove);
180
+ if (aliasPath === "") {
181
+ aliasPath = "/";
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ if (shouldAlias && aliasPath !== null) {
163
188
  assetEntries.push({
164
189
  name,
165
- servePath: dirPath,
190
+ servePath: aliasPath,
166
191
  mime,
167
192
  gzName,
168
193
  gzLen,
@@ -173,7 +198,7 @@ async function generateEmbeddedAssets(assets, shouldMinify, compression) {
173
198
  }
174
199
 
175
200
  /* Generate the asset table */
176
- lines.push("static cerver_asset_t cerver_embedded_assets[] = {");
201
+ lines.push("cerver_asset_t cerver_embedded_assets[] = {");
177
202
  for (const entry of assetEntries) {
178
203
  lines.push(
179
204
  ` { "${entry.servePath}", "${entry.mime}", ` +
@@ -185,7 +210,7 @@ async function generateEmbeddedAssets(assets, shouldMinify, compression) {
185
210
  }
186
211
  lines.push("};");
187
212
  lines.push(
188
- `static const int cerver_embedded_asset_count = ${assetEntries.length};`
213
+ `const int cerver_embedded_asset_count = ${assetEntries.length};`
189
214
  );
190
215
 
191
216
  return lines.join("\n");
@@ -40,7 +40,7 @@ function generateDispatch(routes) {
40
40
 
41
41
  /* Generate the dispatch function */
42
42
  lines.push(
43
- "static cerver_handler_fn cerver_generated_dispatch(cerver_request_t *req) {"
43
+ "cerver_handler_fn cerver_generated_dispatch(cerver_request_t *req) {"
44
44
  );
45
45
  lines.push(" const char *path = req->path;");
46
46
  lines.push(" size_t path_len = 0;");
@@ -5,33 +5,36 @@ const { generateRouteTable } = require("./route_table");
5
5
  const { generateDispatch } = require("./dispatch_gen");
6
6
 
7
7
  /**
8
- * Generate the complete C server source file from IR routes.
8
+ * Generate server.c content.
9
9
  *
10
- * @param {IRRoute[]} routes - All IR routes
11
- * @param {object} config - Build config (port, embed, etc.)
12
- * @param {string|null} assetsCode - Generated embedded assets C code, or null
13
- * @returns {string} - Complete C source file
10
+ * @param {object} config - Build config (port, threads, etc.)
11
+ * @param {boolean} hasAssets - Whether embedding assets is enabled
12
+ * @returns {string} - C source for server.c
14
13
  */
15
- function generateServer(routes, config, assetsCode) {
14
+ function generateServerC(config, hasAssets) {
16
15
  const lines = [];
17
16
 
18
- /* ---- Header ---- */
19
17
  lines.push("/*");
20
18
  lines.push(" * Auto-generated by cerver — do not edit.");
21
19
  lines.push(" */");
22
20
  lines.push("");
23
21
  lines.push('#include "cerver.h"');
24
- lines.push("");
25
22
  lines.push("#include <stdio.h>");
26
23
  lines.push("#include <stdlib.h>");
27
- lines.push("#include <string.h>");
28
24
  lines.push("");
29
25
 
30
- /* ---- Embedded assets (if any) ---- */
31
- if (assetsCode) {
32
- lines.push("/* ---- Embedded Assets ---- */");
33
- lines.push("");
34
- lines.push(assetsCode);
26
+ /* Routing externs */
27
+ lines.push("/* Routing and Handler declarations */");
28
+ lines.push("extern cerver_route_t cerver_routes[];");
29
+ lines.push("extern const int cerver_route_count;");
30
+ lines.push("extern cerver_handler_fn cerver_generated_dispatch(cerver_request_t *req);");
31
+ lines.push("");
32
+
33
+ /* Asset declarations */
34
+ if (hasAssets) {
35
+ lines.push("/* Embedded Assets */");
36
+ lines.push("extern cerver_asset_t cerver_embedded_assets[];");
37
+ lines.push("extern const int cerver_embedded_asset_count;");
35
38
  lines.push("");
36
39
  } else {
37
40
  lines.push("/* No embedded assets */");
@@ -40,6 +43,63 @@ function generateServer(routes, config, assetsCode) {
40
43
  lines.push("");
41
44
  }
42
45
 
46
+ /* Main function */
47
+ lines.push("/* ---- Entry Point ---- */");
48
+ lines.push("");
49
+ lines.push("int main(int argc, char *argv[]) {");
50
+ lines.push(" (void)argc;");
51
+ lines.push(" (void)argv;");
52
+ lines.push("");
53
+ lines.push(` int port = ${config.port};`);
54
+ lines.push("");
55
+ lines.push(" /* Allow PORT env override */");
56
+ lines.push(' const char *env_port = getenv("CERVER_PORT");');
57
+ lines.push(' if (!env_port) env_port = getenv("PORT");');
58
+ lines.push(" if (env_port) port = atoi(env_port);");
59
+ lines.push("");
60
+ lines.push(" cerver_server_t srv;");
61
+ lines.push(` cerver_init(&srv, port, ${config.threads});`);
62
+ lines.push("");
63
+ lines.push(
64
+ " cerver_add_routes(&srv, cerver_routes, cerver_route_count);"
65
+ );
66
+ lines.push("");
67
+ lines.push(" /* Use generated compile-time dispatch for fast routing */");
68
+ lines.push(" cerver_set_dispatch(&srv, cerver_generated_dispatch);");
69
+ lines.push("");
70
+ if (hasAssets) {
71
+ lines.push(
72
+ " cerver_set_assets(&srv, cerver_embedded_assets, cerver_embedded_asset_count);"
73
+ );
74
+ } else {
75
+ lines.push(' cerver_set_public_dir(&srv, "./public");');
76
+ }
77
+ lines.push("");
78
+ lines.push(" return cerver_listen(&srv);");
79
+ lines.push("}");
80
+ lines.push("");
81
+
82
+ return lines.join("\n");
83
+ }
84
+
85
+ /**
86
+ * Generate routes.c content.
87
+ *
88
+ * @param {IRRoute[]} routes - All IR routes
89
+ * @returns {string} - C source for routes.c
90
+ */
91
+ function generateRoutesC(routes) {
92
+ const lines = [];
93
+
94
+ lines.push("/*");
95
+ lines.push(" * Auto-generated route handler logic — do not edit.");
96
+ lines.push(" */");
97
+ lines.push("");
98
+ lines.push('#include "cerver.h"');
99
+ lines.push("#include <string.h>");
100
+ lines.push("#include <stdlib.h>");
101
+ lines.push("");
102
+
43
103
  /* ---- Route table (forward decls + table) ---- */
44
104
  lines.push("/* ---- Routes ---- */");
45
105
  lines.push("");
@@ -76,47 +136,22 @@ function generateServer(routes, config, assetsCode) {
76
136
  lines.push("");
77
137
  lines.push(generateDispatch(routes));
78
138
 
79
- /* ---- Main function ---- */
80
- lines.push("/* ---- Entry Point ---- */");
81
- lines.push("");
82
- lines.push("int main(int argc, char *argv[]) {");
83
- lines.push(" (void)argc;");
84
- lines.push(" (void)argv;");
85
- lines.push("");
86
- lines.push(` int port = ${config.port};`);
87
- lines.push("");
88
- lines.push(" /* Allow PORT env override */");
89
- lines.push(' const char *env_port = getenv("CERVER_PORT");');
90
- lines.push(' if (!env_port) env_port = getenv("PORT");');
91
- lines.push(" if (env_port) port = atoi(env_port);");
92
- lines.push("");
93
- lines.push(" cerver_server_t srv;");
94
- lines.push(` cerver_init(&srv, port, ${config.threads});`);
95
- lines.push("");
96
- lines.push(
97
- " cerver_add_routes(&srv, cerver_routes, cerver_route_count);"
98
- );
99
- lines.push("");
100
-
101
- /* Wire up generated dispatch */
102
- lines.push(" /* Use generated compile-time dispatch for fast routing */");
103
- lines.push(" cerver_set_dispatch(&srv, cerver_generated_dispatch);");
104
- lines.push("");
105
-
106
- if (assetsCode) {
107
- lines.push(
108
- " cerver_set_assets(&srv, cerver_embedded_assets, cerver_embedded_asset_count);"
109
- );
110
- } else {
111
- lines.push(' cerver_set_public_dir(&srv, "./public");');
112
- }
113
-
114
- lines.push("");
115
- lines.push(" return cerver_listen(&srv);");
116
- lines.push("}");
117
- lines.push("");
118
-
119
139
  return lines.join("\n");
120
140
  }
121
141
 
122
- module.exports = { generateServer };
142
+ /**
143
+ * Generate the complete C server source structures from IR routes.
144
+ *
145
+ * @param {IRRoute[]} routes - All IR routes
146
+ * @param {object} config - Build config (port, embed, etc.)
147
+ * @param {boolean} hasAssets - Whether embedding assets is enabled
148
+ * @returns {{ serverC: string, routesC: string }} - Generated files object
149
+ */
150
+ function generateServer(routes, config, hasAssets) {
151
+ return {
152
+ serverC: generateServerC(config, hasAssets),
153
+ routesC: generateRoutesC(routes),
154
+ };
155
+ }
156
+
157
+ module.exports = { generateServer, generateServerC, generateRoutesC };
@@ -25,7 +25,7 @@ function generateRouteTable(routes) {
25
25
  lines.push("");
26
26
 
27
27
  /* Route table array */
28
- lines.push("static cerver_route_t cerver_routes[] = {");
28
+ lines.push("cerver_route_t cerver_routes[] = {");
29
29
  for (const route of routes) {
30
30
  const name = handlerName(route.method, route.urlPath);
31
31
  lines.push(
@@ -34,7 +34,7 @@ function generateRouteTable(routes) {
34
34
  }
35
35
  lines.push("};");
36
36
  lines.push(
37
- `static const int cerver_route_count = ${routes.length};`
37
+ `const int cerver_route_count = ${routes.length};`
38
38
  );
39
39
  lines.push("");
40
40
 
@@ -3,7 +3,7 @@
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
5
 
6
- const { loadConfig } = require("../config");
6
+ const { loadConfig, findProjectRoot } = require("../config");
7
7
  const { discoverRoutes } = require("../parser/discover");
8
8
  const { parseFile } = require("../parser/parse");
9
9
  const { validate } = require("../validator/validate");
@@ -17,7 +17,13 @@ const { compile: compileC } = require("../compiler/compile");
17
17
  * Full build pipeline: parse → validate → IR → codegen → compile
18
18
  */
19
19
  async function build(opts) {
20
- const projectDir = process.cwd();
20
+ const projectDir = findProjectRoot();
21
+ if (!projectDir) {
22
+ console.log("Not a cerver project");
23
+ process.exit(1);
24
+ }
25
+ process.chdir(projectDir);
26
+
21
27
  const startTime = Date.now();
22
28
 
23
29
  console.log("\n cerver build\n");
@@ -35,12 +41,15 @@ async function build(opts) {
35
41
  );
36
42
 
37
43
  /* ---- 2. Discover routes ---- */
38
- const routesDir = path.join(projectDir, "app", "routes");
44
+ let routesDir = path.join(projectDir, "routes");
45
+ if (!fs.existsSync(routesDir)) {
46
+ routesDir = path.join(projectDir, "app", "routes");
47
+ }
39
48
  console.log(" → discovering routes...");
40
49
  const routeFiles = discoverRoutes(routesDir);
41
50
 
42
51
  if (routeFiles.length === 0) {
43
- console.warn(" ⚠ no route files found in app/routes/");
52
+ console.warn(` ⚠ no route files found in ${path.relative(projectDir, routesDir)}/`);
44
53
  } else {
45
54
  for (const r of routeFiles) {
46
55
  console.log(` ${r.urlPath} ← ${path.relative(projectDir, r.filePath)}`);
@@ -81,7 +90,7 @@ async function build(opts) {
81
90
  assetsCode = await generateEmbeddedAssets(assets, config.minify, config.compression);
82
91
  console.log(
83
92
  ` ${assets.length} asset(s), ${(totalSize / 1024).toFixed(1)} KB total` +
84
- (config.compression !== "none" ? ` (${config.compression} compressed)` : "")
93
+ (config.compression !== "none" ? ` (${config.compression} compressed)` : "")
85
94
  );
86
95
  } else {
87
96
  console.log(" no assets found in public/");
@@ -90,15 +99,29 @@ async function build(opts) {
90
99
 
91
100
  /* ---- 5. Generate C source ---- */
92
101
  console.log(" → generating C source...");
93
- const cSource = generateServer(allRoutes, config, assetsCode);
102
+ const { serverC, routesC } = generateServer(allRoutes, config, !!assetsCode);
94
103
 
95
104
  /* Write to dist/ */
96
105
  const distDir = path.join(projectDir, "dist");
97
106
  fs.mkdirSync(distDir, { recursive: true });
98
107
 
99
108
  const serverCPath = path.join(distDir, "server.c");
100
- fs.writeFileSync(serverCPath, cSource);
101
- console.log(` wrote ${(cSource.length / 1024).toFixed(1)} KB → dist/server.c`);
109
+ fs.writeFileSync(serverCPath, serverC);
110
+ console.log(` wrote ${(serverC.length / 1024).toFixed(1)} KB → dist/server.c`);
111
+
112
+ const routesCPath = path.join(distDir, "routes.c");
113
+ fs.writeFileSync(routesCPath, routesC);
114
+ console.log(` wrote ${(routesC.length / 1024).toFixed(1)} KB → dist/routes.c`);
115
+
116
+ const assetsCPath = path.join(distDir, "assets.c");
117
+ if (assetsCode) {
118
+ fs.writeFileSync(assetsCPath, assetsCode);
119
+ console.log(` wrote ${(assetsCode.length / 1024).toFixed(1)} KB → dist/assets.c`);
120
+ } else {
121
+ if (fs.existsSync(assetsCPath)) {
122
+ fs.unlinkSync(assetsCPath);
123
+ }
124
+ }
102
125
 
103
126
  /* ---- 6. Copy runtime headers ---- */
104
127
  const runtimeDir = path.join(__dirname, "..", "..", "runtime");
@@ -5,12 +5,19 @@ const path = require("path");
5
5
  const { spawn } = require("child_process");
6
6
  const chokidar = require("chokidar");
7
7
  const { build } = require("./build");
8
+ const { findProjectRoot } = require("../config");
8
9
 
9
10
  /**
10
11
  * Dev mode: watch for changes, auto-rebuild, auto-restart.
11
12
  */
12
13
  async function dev(opts) {
13
- const projectDir = process.cwd();
14
+ const projectDir = findProjectRoot();
15
+ if (!projectDir) {
16
+ console.log("Not a cerver project");
17
+ process.exit(1);
18
+ }
19
+ process.chdir(projectDir);
20
+
14
21
  const binaryPath = path.join(projectDir, "dist", "server");
15
22
 
16
23
  let serverProcess = null;
@@ -97,7 +104,7 @@ async function dev(opts) {
97
104
 
98
105
  /* ---- Watch for changes ---- */
99
106
  const watchPaths = [
100
- path.join(projectDir, "app"),
107
+ path.join(projectDir, "routes"),
101
108
  path.join(projectDir, "public"),
102
109
  path.join(projectDir, "cerver.config.js"),
103
110
  ].filter((p) => fs.existsSync(p));