@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.
@@ -3,12 +3,19 @@
3
3
  const { execFileSync } = require("child_process");
4
4
  const path = require("path");
5
5
  const fs = require("fs");
6
+ const { findProjectRoot } = require("../config");
6
7
 
7
8
  /**
8
9
  * Run the compiled binary from dist/.
9
10
  */
10
11
  function run(opts) {
11
- const projectDir = process.cwd();
12
+ const projectDir = findProjectRoot();
13
+ if (!projectDir) {
14
+ console.log("Not a cerver project");
15
+ process.exit(1);
16
+ }
17
+ process.chdir(projectDir);
18
+
12
19
  const binaryPath = path.join(projectDir, "dist", "server");
13
20
 
14
21
  if (!fs.existsSync(binaryPath)) {
@@ -45,12 +45,17 @@ function supportsFlag(cc, flag) {
45
45
  */
46
46
  function compile(distDir, runtimeDir, opts) {
47
47
  const cc = detectCompiler();
48
- const serverC = path.join(distDir, "server.c");
49
48
  const outputBin = path.join(distDir, "server");
50
49
 
51
- if (!fs.existsSync(serverC)) {
50
+ /* Collect all generated source files from distDir */
51
+ const generatedSources = fs
52
+ .readdirSync(distDir)
53
+ .filter((f) => f.endsWith(".c"))
54
+ .map((f) => path.join(distDir, f));
55
+
56
+ if (generatedSources.length === 0) {
52
57
  throw new Error(
53
- `cerver: generated source not found at ${serverC}`
58
+ `cerver: no generated C source files found in ${distDir}`
54
59
  );
55
60
  }
56
61
 
@@ -68,7 +73,7 @@ function compile(distDir, runtimeDir, opts) {
68
73
  "-Wno-unused-parameter",
69
74
  "-o",
70
75
  outputBin,
71
- serverC,
76
+ ...generatedSources,
72
77
  ...runtimeSources,
73
78
  `-I${runtimeDir}`,
74
79
  "-lpthread",
package/lib/config.js CHANGED
@@ -53,4 +53,23 @@ function loadConfig(projectDir) {
53
53
  return config;
54
54
  }
55
55
 
56
- module.exports = { loadConfig, DEFAULTS };
56
+ function findProjectRoot(startDir = process.cwd()) {
57
+ let currentDir = startDir;
58
+
59
+ while (true) {
60
+ if (fs.existsSync(path.join(currentDir, "cerver.config.js"))) {
61
+ return currentDir;
62
+ }
63
+
64
+ const parentDir = path.dirname(currentDir);
65
+ if (parentDir === currentDir) {
66
+ break;
67
+ }
68
+ currentDir = parentDir;
69
+ }
70
+
71
+ return null;
72
+ }
73
+
74
+ module.exports = { loadConfig, findProjectRoot, DEFAULTS };
75
+
@@ -4,15 +4,15 @@ const fs = require("fs");
4
4
  const path = require("path");
5
5
 
6
6
  /**
7
- * Discover route files under app/routes/ and map them to URL paths.
7
+ * Discover route files under routes/ and map them to URL paths.
8
8
  *
9
9
  * File-based routing convention:
10
- * app/routes/index.js → /
11
- * app/routes/page.js → /page
12
- * app/routes/group/item.js → /group/item
13
- * app/routes/item/[id].js → /item/:id
10
+ * routes/index.js → /
11
+ * routes/page.js → /page
12
+ * routes/group/item.js → /group/item
13
+ * routes/item/[id].js → /item/:id
14
14
  *
15
- * @param {string} routesDir - Absolute path to app/routes/
15
+ * @param {string} routesDir - Absolute path to routes/
16
16
  * @returns {Array<{ filePath: string, urlPath: string }>}
17
17
  */
18
18
  function discoverRoutes(routesDir) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velox0/cerver",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Compile restricted JavaScript server logic into optimized native C binaries",
5
5
  "main": "bin/cerver.js",
6
6
  "bin": {
package/runtime/cerver.h CHANGED
@@ -238,7 +238,7 @@ struct cerver_server {
238
238
  cerver_worker_t* workers;
239
239
 
240
240
  /* Route trie for radix/trie-based routing */
241
- void* route_trie;
241
+ void* route_trie;
242
242
  };
243
243
 
244
244
  /* Server lifecycle */
@@ -269,7 +269,8 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive);
269
269
  int cerver_route_match(const cerver_route_t* route, cerver_request_t* req);
270
270
  cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req);
271
271
  void* cerver_trie_create(void);
272
- void cerver_trie_insert(void* trie, const char* pattern, const char* method, cerver_handler_fn handler);
272
+ void cerver_trie_insert(void* trie, const char* pattern, const char* method,
273
+ cerver_handler_fn handler);
273
274
  void cerver_trie_free(void* trie);
274
275
 
275
276
  /* ------------------------------------------------------------------ */
@@ -25,7 +25,7 @@ static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count
25
25
  #include <sys/socket.h>
26
26
  static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count) {
27
27
  off_t len = (off_t)count;
28
- int res = sendfile(in_fd, out_fd, offset, &len, NULL, 0);
28
+ int res = sendfile(in_fd, out_fd, offset, &len, NULL, 0);
29
29
  if (res == 0) {
30
30
  return (ssize_t)len;
31
31
  }
@@ -35,8 +35,8 @@ static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count
35
35
  /* Fallback to read-write copy if not a socket or unsupported on this descriptor type */
36
36
  char buf[8192];
37
37
  if (lseek(in_fd, offset, SEEK_SET) == -1) return -1;
38
- size_t to_read = count > sizeof(buf) ? sizeof(buf) : count;
39
- ssize_t n_read = read(in_fd, buf, to_read);
38
+ size_t to_read = count > sizeof(buf) ? sizeof(buf) : count;
39
+ ssize_t n_read = read(in_fd, buf, to_read);
40
40
  if (n_read <= 0) return n_read;
41
41
 
42
42
  size_t written = 0;
@@ -54,8 +54,8 @@ static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count
54
54
  static ssize_t cerver_sendfile(int out_fd, int in_fd, off_t offset, size_t count) {
55
55
  char buf[8192];
56
56
  if (lseek(in_fd, offset, SEEK_SET) == -1) return -1;
57
- size_t to_read = count > sizeof(buf) ? sizeof(buf) : count;
58
- ssize_t n_read = read(in_fd, buf, to_read);
57
+ size_t to_read = count > sizeof(buf) ? sizeof(buf) : count;
58
+ ssize_t n_read = read(in_fd, buf, to_read);
59
59
  if (n_read <= 0) return n_read;
60
60
 
61
61
  size_t written = 0;
@@ -156,7 +156,7 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
156
156
  */
157
157
  if (res->_body_owned == 3) {
158
158
  /* Send header first */
159
- size_t header_total = (size_t)hlen;
159
+ size_t header_total = (size_t)hlen;
160
160
  size_t header_written = 0;
161
161
  while (header_written < header_total) {
162
162
  ssize_t n = write(fd, header + header_written, header_total - header_written);
@@ -169,7 +169,7 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
169
169
 
170
170
  /* Zero-copy body sending via sendfile(2) */
171
171
  size_t body_total = res->body_len;
172
- size_t body_sent = 0;
172
+ size_t body_sent = 0;
173
173
  while (body_sent < body_total) {
174
174
  ssize_t n = cerver_sendfile(fd, res->_file_fd, (off_t)body_sent, body_total - body_sent);
175
175
  if (n < 0) {
@@ -183,7 +183,7 @@ int cerver_write_response(int fd, const cerver_response_t* res, int keepalive) {
183
183
  if ((size_t)hlen + res->body_len <= sizeof(header)) {
184
184
  /* Small response optimization: copy body into header buffer and send in one syscall */
185
185
  memcpy(header + hlen, res->body, res->body_len);
186
- size_t total = (size_t)hlen + res->body_len;
186
+ size_t total = (size_t)hlen + res->body_len;
187
187
  size_t written = 0;
188
188
  while (written < total) {
189
189
  ssize_t n = write(fd, header + written, total - written);
package/runtime/router.c CHANGED
@@ -108,9 +108,9 @@ int cerver_route_match(const cerver_route_t* route, cerver_request_t* req) {
108
108
  const char* pp = pattern; /* pattern pointer */
109
109
  const char* rp = path; /* request path pointer */
110
110
 
111
- int saved_params = req->params_count;
111
+ int saved_params = req->params_count;
112
112
  char* param_slashes[CERVER_MAX_PARAMS];
113
- int param_slash_count = 0;
113
+ int param_slash_count = 0;
114
114
 
115
115
  /* Skip leading '/' */
116
116
  if (*pp == '/') pp++;
@@ -186,18 +186,18 @@ typedef struct trie_node trie_node_t;
186
186
 
187
187
  struct trie_node {
188
188
  char* segment;
189
- int is_param;
189
+ int is_param;
190
190
  char* param_name;
191
191
 
192
192
  struct {
193
- const char* method;
193
+ const char* method;
194
194
  cerver_handler_fn handler;
195
195
  } handlers[16];
196
196
  int handler_count;
197
197
 
198
198
  trie_node_t** children;
199
- int children_count;
200
- int children_cap;
199
+ int children_count;
200
+ int children_cap;
201
201
  };
202
202
 
203
203
  void* cerver_trie_create(void) {
@@ -210,17 +210,18 @@ static trie_node_t* trie_create_node(const char* segment, size_t len) {
210
210
  if (node && segment) {
211
211
  node->segment = trie_strndup(segment, len);
212
212
  if (node->segment[0] == ':') {
213
- node->is_param = 1;
213
+ node->is_param = 1;
214
214
  node->param_name = trie_strndup(node->segment + 1, len - 1);
215
215
  }
216
216
  }
217
217
  return node;
218
218
  }
219
219
 
220
- void cerver_trie_insert(void* trie, const char* pattern, const char* method, cerver_handler_fn handler) {
220
+ void cerver_trie_insert(void* trie, const char* pattern, const char* method,
221
+ cerver_handler_fn handler) {
221
222
  if (!trie) return;
222
223
  trie_node_t* curr = (trie_node_t*)trie;
223
- const char* p = pattern;
224
+ const char* p = pattern;
224
225
  while (*p == '/') p++;
225
226
 
226
227
  while (*p) {
@@ -246,7 +247,7 @@ void cerver_trie_insert(void* trie, const char* pattern, const char* method, cer
246
247
  child = trie_create_node(seg_start, len);
247
248
  if (curr->children_count >= curr->children_cap) {
248
249
  curr->children_cap = curr->children_cap == 0 ? 4 : curr->children_cap * 2;
249
- curr->children = realloc(curr->children, curr->children_cap * sizeof(trie_node_t*));
250
+ curr->children = realloc(curr->children, curr->children_cap * sizeof(trie_node_t*));
250
251
  }
251
252
  curr->children[curr->children_count++] = child;
252
253
  }
@@ -257,7 +258,7 @@ void cerver_trie_insert(void* trie, const char* pattern, const char* method, cer
257
258
 
258
259
  // Add handler to leaf
259
260
  if (curr->handler_count < 16) {
260
- curr->handlers[curr->handler_count].method = method;
261
+ curr->handlers[curr->handler_count].method = method;
261
262
  curr->handlers[curr->handler_count].handler = handler;
262
263
  curr->handler_count++;
263
264
  }
@@ -275,7 +276,8 @@ void cerver_trie_free(void* trie) {
275
276
  free(node);
276
277
  }
277
278
 
278
- static int trie_match_recursive(trie_node_t* node, const char* path, cerver_request_t* req, cerver_handler_fn* out_handler, int param_start_idx) {
279
+ static int trie_match_recursive(trie_node_t* node, const char* path, cerver_request_t* req,
280
+ cerver_handler_fn* out_handler, int param_start_idx) {
279
281
  // Skip leading slashes
280
282
  while (*path == '/') path++;
281
283
 
@@ -283,7 +285,7 @@ static int trie_match_recursive(trie_node_t* node, const char* path, cerver_requ
283
285
  // Check if node has a handler for req->method
284
286
  for (int i = 0; i < node->handler_count; i++) {
285
287
  if (strcmp(node->handlers[i].method, req->method) == 0) {
286
- *out_handler = node->handlers[i].handler;
288
+ *out_handler = node->handlers[i].handler;
287
289
  req->params_count = param_start_idx;
288
290
  return 1;
289
291
  }
@@ -313,7 +315,7 @@ static int trie_match_recursive(trie_node_t* node, const char* path, cerver_requ
313
315
  trie_node_t* child = node->children[i];
314
316
  if (child->is_param) {
315
317
  if (param_start_idx < CERVER_MAX_PARAMS) {
316
- req->params[param_start_idx].key = child->param_name;
318
+ req->params[param_start_idx].key = child->param_name;
317
319
  req->params[param_start_idx].value = seg_start;
318
320
  }
319
321
  if (trie_match_recursive(child, path, req, out_handler, param_start_idx + 1)) {
@@ -340,7 +342,7 @@ cerver_handler_fn cerver_dispatch(cerver_server_t* srv, cerver_request_t* req) {
340
342
  if (!srv->route_trie) return NULL;
341
343
 
342
344
  cerver_handler_fn handler = NULL;
343
- req->params_count = 0;
345
+ req->params_count = 0;
344
346
  if (trie_match_recursive((trie_node_t*)srv->route_trie, req->path, req, &handler, 0)) {
345
347
  // NUL-terminate extracted values in-place inside req->path
346
348
  for (int i = 0; i < req->params_count; i++) {
package/runtime/server.c CHANGED
@@ -771,6 +771,22 @@ int cerver_listen(cerver_server_t* srv) {
771
771
  printf("cerver: %d acceptor(s), %d connection workers, keep-alive max %d req/conn\n",
772
772
  acceptor_count, pool_size, CERVER_KEEPALIVE_MAX);
773
773
 
774
+ for (int i = 0; i < srv->route_count; i++) {
775
+ const char* method = srv->routes[i].method;
776
+ const char* color = "\x1B[35m";
777
+ if (strcmp(method, "GET") == 0) {
778
+ color = "\x1B[32m";
779
+ } else if (strcmp(method, "POST") == 0) {
780
+ color = "\x1B[33m";
781
+ } else if (strcmp(method, "PUT") == 0) {
782
+ color = "\x1B[36m";
783
+ } else if (strcmp(method, "DELETE") == 0) {
784
+ color = "\x1B[31m";
785
+ }
786
+ printf(" → Mapped {%s, %s%s\x1B[0m} route\n", srv->routes[i].pattern, color, method);
787
+ }
788
+ fflush(stdout);
789
+
774
790
  release_acceptors();
775
791
 
776
792
  pthread_attr_destroy(&attr);
package/runtime/static.c CHANGED
@@ -85,6 +85,64 @@ static void add_cache_headers(cerver_response_t* res, const char* path) {
85
85
  }
86
86
  }
87
87
 
88
+ /* ------------------------------------------------------------------ */
89
+ /* Helper to resolve fallback paths for directory/clean-URL routes. */
90
+ /* - "/" -> "/index.html" */
91
+ /* - "/page" or "/page/" -> "/page/page.html" */
92
+ /* ------------------------------------------------------------------ */
93
+
94
+ static void get_fallback_path(const char* path, char* out, size_t out_len) {
95
+ if (strcmp(path, "/") == 0 || strcmp(path, "") == 0) {
96
+ snprintf(out, out_len, "/index.html");
97
+ return;
98
+ }
99
+
100
+ size_t len = strlen(path);
101
+ while (len > 0 && path[len - 1] == '/') {
102
+ len--;
103
+ }
104
+
105
+ if (len == 0) {
106
+ snprintf(out, out_len, "/index.html");
107
+ return;
108
+ }
109
+
110
+ int last_slash = -1;
111
+ for (int i = (int)len - 1; i >= 0; i--) {
112
+ if (path[i] == '/') {
113
+ last_slash = i;
114
+ break;
115
+ }
116
+ }
117
+
118
+ size_t segment_len = len - (last_slash + 1);
119
+ if (segment_len == 0) {
120
+ snprintf(out, out_len, "/index.html");
121
+ return;
122
+ }
123
+
124
+ /* Extract the prefix and segment safely */
125
+ char prefix[1024];
126
+ if (len < sizeof(prefix)) {
127
+ memcpy(prefix, path, len);
128
+ prefix[len] = '\0';
129
+ } else {
130
+ snprintf(out, out_len, "/index.html");
131
+ return;
132
+ }
133
+
134
+ char segment[256];
135
+ if (segment_len < sizeof(segment)) {
136
+ memcpy(segment, path + last_slash + 1, segment_len);
137
+ segment[segment_len] = '\0';
138
+ } else {
139
+ snprintf(out, out_len, "/index.html");
140
+ return;
141
+ }
142
+
143
+ snprintf(out, out_len, "%s/%s.html", prefix, segment);
144
+ }
145
+
88
146
  /* ------------------------------------------------------------------ */
89
147
  /* Serve from embedded assets — hash-accelerated lookup */
90
148
  /* ------------------------------------------------------------------ */
@@ -110,19 +168,15 @@ static int serve_embedded(cerver_server_t* srv, cerver_request_t* req, cerver_re
110
168
  }
111
169
  }
112
170
 
113
- /* Try with /index.html appended (for directory-like paths) */
171
+ /* Try with fallback path (for directory-like paths) */
114
172
  if (!found) {
115
- char index_path[CERVER_MAX_PATH];
116
- size_t plen = strlen(path);
117
- if (plen > 0 && path[plen - 1] == '/') {
118
- snprintf(index_path, sizeof(index_path), "%sindex.html", path);
119
- } else {
120
- snprintf(index_path, sizeof(index_path), "%s/index.html", path);
121
- }
173
+ char fallback_path[CERVER_MAX_PATH];
174
+ get_fallback_path(path, fallback_path, sizeof(fallback_path));
122
175
 
123
- uint32_t idx_hash = fnv1a(index_path);
176
+ uint32_t idx_hash = fnv1a(fallback_path);
124
177
  for (int i = 0; i < srv->asset_count; i++) {
125
- if (fnv1a(srv->assets[i].path) == idx_hash && strcmp(srv->assets[i].path, index_path) == 0) {
178
+ if (fnv1a(srv->assets[i].path) == idx_hash &&
179
+ strcmp(srv->assets[i].path, fallback_path) == 0) {
126
180
  found = &srv->assets[i];
127
181
  break;
128
182
  }
@@ -168,10 +222,12 @@ static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_
168
222
  char full_path[CERVER_MAX_PATH * 2];
169
223
  snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
170
224
 
171
- /* Check if it's a directory — try index.html */
225
+ /* Check if it's a directory — try fallback path */
172
226
  struct stat st;
173
227
  if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
174
- snprintf(full_path, sizeof(full_path), "%s%s/index.html", srv->public_dir, path);
228
+ char fallback_path[CERVER_MAX_PATH];
229
+ get_fallback_path(path, fallback_path, sizeof(fallback_path));
230
+ snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, fallback_path);
175
231
  if (stat(full_path, &st) != 0) return -1;
176
232
  }
177
233
 
@@ -288,9 +288,9 @@ static void test_static_embedded_index_fallback(void) {
288
288
  cerver_server_t srv;
289
289
  cerver_init(&srv, 8080, 1);
290
290
 
291
- static const unsigned char data[] = "docs";
291
+ static const unsigned char data[] = "about";
292
292
  cerver_asset_t assets[1];
293
- assets[0].path = "/docs/index.html";
293
+ assets[0].path = "/about/about.html";
294
294
  assets[0].mime_type = "text/html";
295
295
  assets[0].data = data;
296
296
  assets[0].data_len = sizeof(data) - 1;
@@ -305,13 +305,91 @@ static void test_static_embedded_index_fallback(void) {
305
305
  memset(&req, 0, sizeof(req));
306
306
  memset(&res, 0, sizeof(res));
307
307
  strcpy(req.method, "GET");
308
- strcpy(req.path, "/docs/");
308
+ strcpy(req.path, "/about/");
309
309
 
310
310
  MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
311
311
  MU_ASSERT(res.body == (const char*)data);
312
312
  MU_ASSERT_STREQ("text/html", res.content_type);
313
313
  }
314
314
 
315
+ static void test_static_filesystem_directory_fallback(void) {
316
+ char dir_template[] = "/tmp/cerver-test-XXXXXX";
317
+ char* dir = mkdtemp(dir_template);
318
+ MU_ASSERT(dir != NULL);
319
+
320
+ /* Create public/index.html (maps to /) */
321
+ char file_path[PATH_MAX];
322
+ snprintf(file_path, sizeof(file_path), "%s/index.html", dir);
323
+ MU_ASSERT_EQ_INT(0, write_file(file_path, "root index", 10));
324
+
325
+ /* Create public/about/about.html (maps to /about or /about/) */
326
+ char sub_dir[PATH_MAX];
327
+ snprintf(sub_dir, sizeof(sub_dir), "%s/about", dir);
328
+ MU_ASSERT_EQ_INT(0, mkdir(sub_dir, 0700));
329
+
330
+ char file_path2[PATH_MAX];
331
+ snprintf(file_path2, sizeof(file_path2), "%s/about/about.html", dir);
332
+ MU_ASSERT_EQ_INT(0, write_file(file_path2, "about content", 13));
333
+
334
+ /* Create public/about/index.html (should NOT map to /about, since index.html under subdirectory
335
+ * doesn't alias) */
336
+ char file_path3[PATH_MAX];
337
+ snprintf(file_path3, sizeof(file_path3), "%s/about/index.html", dir);
338
+ MU_ASSERT_EQ_INT(0, write_file(file_path3, "about index", 11));
339
+
340
+ cerver_server_t srv;
341
+ cerver_init(&srv, 8080, 1);
342
+ cerver_set_public_dir(&srv, dir);
343
+
344
+ /* Test / -> index.html fallback */
345
+ {
346
+ cerver_request_t req;
347
+ cerver_response_t res;
348
+ memset(&req, 0, sizeof(req));
349
+ memset(&res, 0, sizeof(res));
350
+ strcpy(req.method, "GET");
351
+ strcpy(req.path, "/");
352
+
353
+ MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
354
+ MU_ASSERT_EQ_SIZE(10, res.body_len);
355
+ if (res._body_owned == 3 && res._file_fd >= 0) close(res._file_fd);
356
+ }
357
+
358
+ /* Test /about -> about/about.html fallback */
359
+ {
360
+ cerver_request_t req;
361
+ cerver_response_t res;
362
+ memset(&req, 0, sizeof(req));
363
+ memset(&res, 0, sizeof(res));
364
+ strcpy(req.method, "GET");
365
+ strcpy(req.path, "/about");
366
+
367
+ MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
368
+ MU_ASSERT_EQ_SIZE(13, res.body_len);
369
+ if (res._body_owned == 3 && res._file_fd >= 0) close(res._file_fd);
370
+ }
371
+
372
+ /* Test /about/ -> about/about.html fallback */
373
+ {
374
+ cerver_request_t req;
375
+ cerver_response_t res;
376
+ memset(&req, 0, sizeof(req));
377
+ memset(&res, 0, sizeof(res));
378
+ strcpy(req.method, "GET");
379
+ strcpy(req.path, "/about/");
380
+
381
+ MU_ASSERT_EQ_INT(0, cerver_serve_static(&srv, &req, &res));
382
+ MU_ASSERT_EQ_SIZE(13, res.body_len);
383
+ if (res._body_owned == 3 && res._file_fd >= 0) close(res._file_fd);
384
+ }
385
+
386
+ unlink(file_path);
387
+ unlink(file_path2);
388
+ unlink(file_path3);
389
+ rmdir(sub_dir);
390
+ rmdir(dir);
391
+ }
392
+
315
393
  static void test_static_filesystem_small(void) {
316
394
  char dir_template[] = "/tmp/cerver-test-XXXXXX";
317
395
  char* dir = mkdtemp(dir_template);
@@ -343,7 +421,7 @@ static void test_static_filesystem_small(void) {
343
421
  MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
344
422
  close(fds[1]);
345
423
 
346
- char out[1024];
424
+ char out[1024];
347
425
  ssize_t n = read_all(fds[0], out, sizeof(out));
348
426
  MU_ASSERT(n > 0);
349
427
  close(fds[0]);
@@ -395,7 +473,7 @@ static void test_static_filesystem_large(void) {
395
473
  MU_ASSERT_EQ_INT(0, cerver_write_response(fds[1], &res, 1));
396
474
  close(fds[1]);
397
475
 
398
- char out[35000];
476
+ char out[35000];
399
477
  ssize_t n = read_all(fds[0], out, sizeof(out));
400
478
  MU_ASSERT(n > 0);
401
479
  close(fds[0]);
@@ -457,6 +535,7 @@ int main(void) {
457
535
  mu_run("static_embedded_index_fallback", test_static_embedded_index_fallback);
458
536
  mu_run("static_filesystem_small", test_static_filesystem_small);
459
537
  mu_run("static_filesystem_large", test_static_filesystem_large);
538
+ mu_run("static_filesystem_directory_fallback", test_static_filesystem_directory_fallback);
460
539
  mu_run("static_rejects_unsafe_path", test_static_rejects_unsafe_path);
461
540
  mu_run("stat_cache_store_lookup", test_stat_cache_store_lookup);
462
541
  mu_run("cerver_init_fields", test_cerver_init_fields);
@@ -1,35 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- pull_request:
6
-
7
- jobs:
8
- tests:
9
- name: Tests
10
- runs-on: ubuntu-latest
11
-
12
- steps:
13
- - name: Checkout
14
- uses: actions/checkout@v6
15
-
16
- - name: Setup pnpm
17
- uses: pnpm/action-setup@v6
18
- with:
19
- version: 10
20
- run_install: false
21
-
22
- - name: Setup Node.js
23
- uses: actions/setup-node@v6
24
- with:
25
- node-version: 24
26
- cache: pnpm
27
-
28
- - name: Install dependencies
29
- run: pnpm install --frozen-lockfile
30
-
31
- - name: Run runtime tests
32
- run: make test-runtime
33
-
34
- - name: Run JS tests
35
- run: pnpm test
@@ -1,50 +0,0 @@
1
- name: Publish package
2
-
3
- on:
4
- push:
5
- tags:
6
- - "v*"
7
- workflow_dispatch:
8
-
9
- permissions:
10
- contents: write
11
- id-token: write
12
-
13
- jobs:
14
- publish:
15
- name: Publish to npm
16
- runs-on: ubuntu-latest
17
-
18
- steps:
19
- - name: Checkout
20
- uses: actions/checkout@v6
21
-
22
- - name: Setup pnpm
23
- uses: pnpm/action-setup@v6
24
- with:
25
- version: 10
26
- run_install: false
27
-
28
- - name: Setup Node.js
29
- uses: actions/setup-node@v6
30
- with:
31
- node-version: 24
32
- registry-url: https://registry.npmjs.org
33
- cache: pnpm
34
-
35
- - name: Install dependencies
36
- run: pnpm install --frozen-lockfile
37
-
38
- - name: Run runtime tests
39
- run: make test-runtime
40
-
41
- - name: Run tests
42
- run: pnpm test
43
-
44
- - name: Verify package contents
45
- run: npm pack --dry-run
46
-
47
- - name: Publish to npm
48
- run: npm publish --access public --provenance
49
- env:
50
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}