@velox0/cerver 0.5.2 → 0.5.3

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/Makefile CHANGED
@@ -7,7 +7,8 @@ RUNTIME_SRCS = \
7
7
  runtime/router.c \
8
8
  runtime/static.c \
9
9
  runtime/mime.c \
10
- runtime/server.c
10
+ runtime/server.c \
11
+ runtime/fetch.c
11
12
 
12
13
  TEST_SRCS = runtime/tests/runtime_tests.c \
13
14
  runtime/tests/minunit.c
@@ -20,7 +21,7 @@ test-runtime: $(TEST_BIN)
20
21
 
21
22
  $(TEST_BIN): $(RUNTIME_SRCS) $(TEST_SRCS) runtime/cerver.h
22
23
  mkdir -p build
23
- $(CC) $(CFLAGS) -Iruntime -o $(TEST_BIN) $(RUNTIME_SRCS) $(TEST_SRCS) -pthread
24
+ $(CC) $(CFLAGS) -Iruntime -o $(TEST_BIN) $(RUNTIME_SRCS) $(TEST_SRCS) -pthread -lcurl
24
25
 
25
26
  clean:
26
27
  rm -rf build
@@ -143,6 +143,31 @@ function emitExpression(expr) {
143
143
  return '""';
144
144
  }
145
145
 
146
+ case "Fetch": {
147
+ /*
148
+ * Fetch is a special expression — it returns a call to cerver_fetch().
149
+ * The generated code will be: cerver_fetch(url, method, body, headers)
150
+ * where headers is either NULL or a stack-allocated array.
151
+ *
152
+ * Because the actual headers array setup requires multiple statements,
153
+ * simple inline expressions won't work for headers. We handle headers
154
+ * at the statement level in emitStatement instead.
155
+ * For inline expression contexts (like variable init), we emit without
156
+ * custom headers when headers are present — the statement emitter
157
+ * will handle the full form.
158
+ */
159
+ const urlCode = emitExpression(expr.url);
160
+ const methodCode = expr.method ? emitExpression(expr.method) : "NULL";
161
+ const bodyCode = expr.body ? emitExpression(expr.body) : "NULL";
162
+
163
+ if (expr.headers && expr.headers.length > 0) {
164
+ /* Flag that this needs statement-level emission */
165
+ return `__fetch_with_headers__`;
166
+ }
167
+
168
+ return `cerver_fetch(${urlCode}, ${methodCode}, ${bodyCode}, NULL)`;
169
+ }
170
+
146
171
  default:
147
172
  return '""';
148
173
  }
@@ -167,6 +192,45 @@ function isStringExpr(expr) {
167
192
  * Emit a statement into C code lines.
168
193
  * Returns an array of C source lines.
169
194
  */
195
+
196
+ /* Counter for unique fetch variable names */
197
+ let fetchVarCounter = 0;
198
+
199
+ /**
200
+ * Emit the headers array and cerver_fetch call for a Fetch expression.
201
+ * Returns { lines: string[], varName: string } where varName holds the result.
202
+ */
203
+ function emitFetchBlock(fetchExpr, pad, varName) {
204
+ const lines = [];
205
+ const urlCode = emitExpression(fetchExpr.url);
206
+ const methodCode = fetchExpr.method ? emitExpression(fetchExpr.method) : "NULL";
207
+ const bodyCode = fetchExpr.body ? emitExpression(fetchExpr.body) : "NULL";
208
+
209
+ if (fetchExpr.headers && fetchExpr.headers.length > 0) {
210
+ const count = fetchExpr.headers.length;
211
+ /* Build the headers array on the stack */
212
+ for (let i = 0; i < count; i++) {
213
+ const hKey = emitExpression(fetchExpr.headers[i].key);
214
+ const hVal = emitExpression(fetchExpr.headers[i].value);
215
+ /* Format: "Key: Value" */
216
+ lines.push(`${pad}char ${varName}_h${i}[512];`);
217
+ lines.push(`${pad}snprintf(${varName}_h${i}, sizeof(${varName}_h${i}), "%s: %s", ${hKey}, ${hVal});`);
218
+ }
219
+ /* NULL-terminated array */
220
+ lines.push(`${pad}const char *${varName}_hdrs[] = {`);
221
+ for (let i = 0; i < count; i++) {
222
+ lines.push(`${pad} ${varName}_h${i},`);
223
+ }
224
+ lines.push(`${pad} NULL`);
225
+ lines.push(`${pad}};`);
226
+ lines.push(`${pad}char *${varName} = cerver_fetch(${urlCode}, ${methodCode}, ${bodyCode}, ${varName}_hdrs);`);
227
+ } else {
228
+ lines.push(`${pad}char *${varName} = cerver_fetch(${urlCode}, ${methodCode}, ${bodyCode}, NULL);`);
229
+ }
230
+
231
+ return { lines, varName };
232
+ }
233
+
170
234
  function emitStatement(stmt, level) {
171
235
  if (!stmt) return [];
172
236
  const pad = indent(level);
@@ -175,9 +239,20 @@ function emitStatement(stmt, level) {
175
239
  switch (stmt.type) {
176
240
  case "Return": {
177
241
  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;`);
242
+
243
+ /* Check if the return value involves a fetch() call */
244
+ if (stmt.value && stmt.value.type === "Fetch") {
245
+ const tempName = `_fetch_res_${fetchVarCounter++}`;
246
+ const fetchBlock = emitFetchBlock(stmt.value, pad, tempName);
247
+ lines.push(...fetchBlock.lines);
248
+ lines.push(`${pad}${fnName}(res, ${stmt.status}, ${tempName});`);
249
+ lines.push(`${pad}free(${tempName});`);
250
+ lines.push(`${pad}return;`);
251
+ } else {
252
+ const valueCode = emitExpression(stmt.value);
253
+ lines.push(`${pad}${fnName}(res, ${stmt.status}, ${valueCode});`);
254
+ lines.push(`${pad}return;`);
255
+ }
181
256
  break;
182
257
  }
183
258
 
@@ -217,11 +292,17 @@ function emitStatement(stmt, level) {
217
292
  }
218
293
 
219
294
  case "Variable": {
220
- const val = emitExpression(stmt.initExpr);
221
- if (stmt.valueType === "number") {
222
- lines.push(`${pad}int ${stmt.name} = ${val};`);
295
+ /* Check if the variable is initialized with a fetch() call */
296
+ if (stmt.initExpr && stmt.initExpr.type === "Fetch") {
297
+ const fetchBlock = emitFetchBlock(stmt.initExpr, pad, stmt.name);
298
+ lines.push(...fetchBlock.lines);
223
299
  } else {
224
- lines.push(`${pad}const char *${stmt.name} = ${val};`);
300
+ const val = emitExpression(stmt.initExpr);
301
+ if (stmt.valueType === "number") {
302
+ lines.push(`${pad}int ${stmt.name} = ${val};`);
303
+ } else {
304
+ lines.push(`${pad}const char *${stmt.name} = ${val};`);
305
+ }
225
306
  }
226
307
  break;
227
308
  }
@@ -98,6 +98,7 @@ function generateRoutesC(routes) {
98
98
  lines.push('#include "cerver.h"');
99
99
  lines.push("#include <string.h>");
100
100
  lines.push("#include <stdlib.h>");
101
+ lines.push("#include <stdio.h>");
101
102
  lines.push("");
102
103
 
103
104
  /* ---- Route table (forward decls + table) ---- */
@@ -13,6 +13,23 @@ const { discoverAssets } = require("../assets/discover");
13
13
  const { generateEmbeddedAssets } = require("../assets/embed");
14
14
  const { compile: compileC } = require("../compiler/compile");
15
15
 
16
+ /**
17
+ * Recursively check if an IR node tree contains any Fetch expressions.
18
+ */
19
+ function irUsesFetch(node) {
20
+ if (!node || typeof node !== "object") return false;
21
+ if (node.type === "Fetch") return true;
22
+ for (const key of Object.keys(node)) {
23
+ const val = node[key];
24
+ if (Array.isArray(val)) {
25
+ if (val.some((item) => irUsesFetch(item))) return true;
26
+ } else if (val && typeof val === "object" && val.type) {
27
+ if (irUsesFetch(val)) return true;
28
+ }
29
+ }
30
+ return false;
31
+ }
32
+
16
33
  /**
17
34
  * Full build pipeline: parse → validate → IR → codegen → compile
18
35
  */
@@ -136,7 +153,17 @@ async function build(opts) {
136
153
 
137
154
  /* ---- 7. Compile ---- */
138
155
  console.log(" → compiling...");
139
- const binaryPath = compileC(distDir, runtimeDir, { static: opts.static });
156
+
157
+ /* Detect if any route uses fetch() — only link libcurl when needed */
158
+ const usesFetch = allRoutes.some((route) => irUsesFetch(route.handler));
159
+ if (usesFetch) {
160
+ console.log(" fetch() detected — linking libcurl");
161
+ }
162
+
163
+ const binaryPath = compileC(distDir, runtimeDir, {
164
+ static: opts.static,
165
+ usesFetch,
166
+ });
140
167
 
141
168
  /* ---- Done ---- */
142
169
  const elapsed = Date.now() - startTime;
@@ -63,6 +63,8 @@ function compile(distDir, runtimeDir, opts) {
63
63
  const runtimeSources = fs
64
64
  .readdirSync(runtimeDir)
65
65
  .filter((f) => f.endsWith(".c"))
66
+ /* Exclude fetch.c when not used — avoids libcurl dependency */
67
+ .filter((f) => f !== "fetch.c" || (opts && opts.usesFetch))
66
68
  .map((f) => path.join(runtimeDir, f));
67
69
 
68
70
  /* Build the compiler command with aggressive optimization */
@@ -79,6 +81,11 @@ function compile(distDir, runtimeDir, opts) {
79
81
  "-lpthread",
80
82
  ];
81
83
 
84
+ /* Link libcurl only when fetch() is used */
85
+ if (opts && opts.usesFetch) {
86
+ args.push("-lcurl");
87
+ }
88
+
82
89
  /* Add LTO if supported */
83
90
  if (supportsFlag(cc, "-flto")) {
84
91
  args.splice(1, 0, "-flto");
@@ -295,6 +295,44 @@ function transformCallExpr(node, ctx) {
295
295
  return IR.IRCall("res", method, args);
296
296
  }
297
297
 
298
+ /* fetch(url) or fetch(url, { method, body, headers }) */
299
+ if (
300
+ node.callee.type === "Identifier" &&
301
+ node.callee.name === "fetch"
302
+ ) {
303
+ const urlExpr = node.arguments[0]
304
+ ? transformExpression(node.arguments[0], ctx)
305
+ : IR.IRStringLiteral("");
306
+
307
+ let methodExpr = null;
308
+ let bodyExpr = null;
309
+ let headersArr = null;
310
+
311
+ /* Parse options object if present: fetch(url, { method, body, headers }) */
312
+ if (node.arguments[1] && node.arguments[1].type === "ObjectExpression") {
313
+ const opts = node.arguments[1];
314
+ for (const prop of opts.properties) {
315
+ const key = prop.key.name || prop.key.value;
316
+ if (key === "method") {
317
+ methodExpr = transformExpression(prop.value, ctx);
318
+ } else if (key === "body") {
319
+ bodyExpr = transformExpression(prop.value, ctx);
320
+ } else if (key === "headers" && prop.value.type === "ObjectExpression") {
321
+ headersArr = [];
322
+ for (const hProp of prop.value.properties) {
323
+ const hKey = hProp.key.name || hProp.key.value;
324
+ headersArr.push({
325
+ key: IR.IRStringLiteral(hKey),
326
+ value: transformExpression(hProp.value, ctx),
327
+ });
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+ return IR.IRFetch(urlExpr, methodExpr, bodyExpr, headersArr);
334
+ }
335
+
298
336
  /* String methods like str.toLowerCase(), includes() etc. — return as-is */
299
337
  if (node.callee.type === "MemberExpression") {
300
338
  const obj = transformExpression(node.callee.object, ctx);
package/lib/ir/types.js CHANGED
@@ -184,6 +184,19 @@ function IRCall(object, method, args) {
184
184
  };
185
185
  }
186
186
 
187
+ /**
188
+ * An outbound HTTP fetch call.
189
+ */
190
+ function IRFetch(url, method, body, headers) {
191
+ return {
192
+ type: "Fetch",
193
+ url, /* IRExpression — the URL */
194
+ method, /* IRExpression | null — "GET", "POST", etc. */
195
+ body, /* IRExpression | null — request body */
196
+ headers, /* Array<{key: IRExpression, value: IRExpression}> | null */
197
+ };
198
+ }
199
+
187
200
  module.exports = {
188
201
  IRRoute,
189
202
  IRHandler,
@@ -201,4 +214,5 @@ module.exports = {
201
214
  IRIdentifier,
202
215
  IRConcat,
203
216
  IRCall,
217
+ IRFetch,
204
218
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velox0/cerver",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
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
@@ -273,6 +273,26 @@ void cerver_trie_insert(void* trie, const char* pattern, const char
273
273
  cerver_handler_fn handler);
274
274
  void cerver_trie_free(void* trie);
275
275
 
276
+ /* ------------------------------------------------------------------ */
277
+ /* Fetch — outbound HTTP client (libcurl) */
278
+ /* ------------------------------------------------------------------ */
279
+
280
+ /**
281
+ * Perform a synchronous HTTP request.
282
+ *
283
+ * @param url Request URL (required).
284
+ * @param method HTTP method: "GET", "POST", "PUT", "DELETE", "PATCH"
285
+ * (NULL defaults to "GET").
286
+ * @param body Request body string (NULL for none).
287
+ * @param headers NULL-terminated array of "Key: Value" header strings,
288
+ * or NULL for no custom headers.
289
+ *
290
+ * @return Heap-allocated response body (caller must free()), or
291
+ * empty heap-allocated string on error.
292
+ */
293
+ char* cerver_fetch(const char* url, const char* method,
294
+ const char* body, const char** headers);
295
+
276
296
  /* ------------------------------------------------------------------ */
277
297
  /* MIME (internal) */
278
298
  /* ------------------------------------------------------------------ */
@@ -0,0 +1,163 @@
1
+ /*
2
+ * fetch.c — HTTP client for outbound API calls using libcurl.
3
+ *
4
+ * Provides cerver_fetch() which performs synchronous HTTP requests
5
+ * from within generated handler code. Supports GET/POST/PUT/DELETE,
6
+ * custom headers, and request bodies.
7
+ *
8
+ * The returned string is heap-allocated and must be freed by the caller.
9
+ */
10
+
11
+ #include "cerver.h"
12
+
13
+ #include <curl/curl.h>
14
+ #include <stdlib.h>
15
+ #include <string.h>
16
+ #include <stdio.h>
17
+
18
+ /* ------------------------------------------------------------------ */
19
+ /* Internal write callback for curl */
20
+ /* ------------------------------------------------------------------ */
21
+
22
+ typedef struct {
23
+ char* data;
24
+ size_t len;
25
+ size_t cap;
26
+ } cerver_fetch_buf_t;
27
+
28
+ static size_t fetch_write_cb(void* contents, size_t size, size_t nmemb,
29
+ void* userp) {
30
+ size_t realsize = size * nmemb;
31
+ cerver_fetch_buf_t* buf = (cerver_fetch_buf_t*)userp;
32
+
33
+ /* Grow buffer if needed */
34
+ while (buf->len + realsize + 1 > buf->cap) {
35
+ size_t newcap = buf->cap * 2;
36
+ if (newcap < 4096) newcap = 4096;
37
+ char* tmp = realloc(buf->data, newcap);
38
+ if (!tmp) return 0; /* signal error to curl */
39
+ buf->data = tmp;
40
+ buf->cap = newcap;
41
+ }
42
+
43
+ memcpy(buf->data + buf->len, contents, realsize);
44
+ buf->len += realsize;
45
+ buf->data[buf->len] = '\0';
46
+
47
+ return realsize;
48
+ }
49
+
50
+ /* ------------------------------------------------------------------ */
51
+ /* Global curl init (thread-safe, called once) */
52
+ /* ------------------------------------------------------------------ */
53
+
54
+ static pthread_once_t curl_init_once = PTHREAD_ONCE_INIT;
55
+
56
+ static void curl_global_init_once(void) {
57
+ curl_global_init(CURL_GLOBAL_DEFAULT);
58
+ }
59
+
60
+ /* ------------------------------------------------------------------ */
61
+ /* Public API */
62
+ /* ------------------------------------------------------------------ */
63
+
64
+ /**
65
+ * cerver_fetch — Perform a synchronous HTTP request.
66
+ *
67
+ * @param url The URL to request (required).
68
+ * @param method HTTP method: "GET", "POST", "PUT", "DELETE" (NULL = "GET").
69
+ * @param body Request body for POST/PUT (NULL for none).
70
+ * @param headers Array of "Key: Value" header strings (NULL-terminated, or NULL for none).
71
+ *
72
+ * @return Heap-allocated response body string (caller must free), or
73
+ * empty string "" (heap-allocated) on error.
74
+ */
75
+ char* cerver_fetch(const char* url, const char* method,
76
+ const char* body, const char** headers) {
77
+ if (!url) {
78
+ char* empty = malloc(1);
79
+ if (empty) empty[0] = '\0';
80
+ return empty;
81
+ }
82
+
83
+ /* Ensure global curl init */
84
+ pthread_once(&curl_init_once, curl_global_init_once);
85
+
86
+ CURL* curl = curl_easy_init();
87
+ if (!curl) {
88
+ char* empty = malloc(1);
89
+ if (empty) empty[0] = '\0';
90
+ return empty;
91
+ }
92
+
93
+ /* Response buffer */
94
+ cerver_fetch_buf_t buf;
95
+ buf.data = malloc(4096);
96
+ buf.len = 0;
97
+ buf.cap = 4096;
98
+ if (!buf.data) {
99
+ curl_easy_cleanup(curl);
100
+ char* empty = malloc(1);
101
+ if (empty) empty[0] = '\0';
102
+ return empty;
103
+ }
104
+ buf.data[0] = '\0';
105
+
106
+ /* Configure request */
107
+ curl_easy_setopt(curl, CURLOPT_URL, url);
108
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, fetch_write_cb);
109
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&buf);
110
+ curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
111
+ curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
112
+ curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
113
+ curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); /* thread-safe */
114
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, "cerver/1.0");
115
+
116
+ /* Set HTTP method */
117
+ if (method) {
118
+ if (strcmp(method, "POST") == 0) {
119
+ curl_easy_setopt(curl, CURLOPT_POST, 1L);
120
+ } else if (strcmp(method, "PUT") == 0) {
121
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
122
+ } else if (strcmp(method, "DELETE") == 0) {
123
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE");
124
+ } else if (strcmp(method, "PATCH") == 0) {
125
+ curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
126
+ }
127
+ /* GET is the default — no action needed */
128
+ }
129
+
130
+ /* Set request body */
131
+ if (body) {
132
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
133
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(body));
134
+ }
135
+
136
+ /* Set custom headers */
137
+ struct curl_slist* header_list = NULL;
138
+ if (headers) {
139
+ for (int i = 0; headers[i] != NULL; i++) {
140
+ header_list = curl_slist_append(header_list, headers[i]);
141
+ }
142
+ if (header_list) {
143
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list);
144
+ }
145
+ }
146
+
147
+ /* Perform the request */
148
+ CURLcode res = curl_easy_perform(curl);
149
+
150
+ if (res != CURLE_OK) {
151
+ fprintf(stderr, "cerver: fetch error: %s (url: %s)\n",
152
+ curl_easy_strerror(res), url);
153
+ /* Return empty string on error */
154
+ buf.data[0] = '\0';
155
+ buf.len = 0;
156
+ }
157
+
158
+ /* Cleanup */
159
+ if (header_list) curl_slist_free_all(header_list);
160
+ curl_easy_cleanup(curl);
161
+
162
+ return buf.data;
163
+ }