@velox0/cerver 0.4.3 → 0.5.2-nightly.20260524.1
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 +15 -5
- package/lib/assets/embed.js +33 -8
- package/lib/codegen/dispatch_gen.js +1 -1
- package/lib/codegen/generator.js +90 -55
- package/lib/codegen/route_table.js +2 -2
- package/lib/commands/build.js +37 -12
- package/lib/commands/dev.js +50 -15
- package/lib/commands/new.js +667 -258
- package/lib/commands/run.js +8 -1
- package/lib/compiler/compile.js +13 -6
- package/lib/config.js +20 -1
- package/lib/parser/discover.js +6 -6
- package/package.json +1 -1
- package/runtime/cerver.h +3 -2
- package/runtime/http_writer.c +8 -8
- package/runtime/router.c +17 -15
- package/runtime/server.c +16 -0
- package/runtime/static.c +68 -12
- package/runtime/tests/runtime_tests.c +84 -5
- package/.github/workflows/ci.yml +0 -35
- package/.github/workflows/publish.yml +0 -50
- package/test/run.js +0 -366
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 `
|
|
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 `
|
|
60
|
+
Routes are defined in the `routes/` directory.
|
|
61
61
|
|
|
62
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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.
|
package/lib/assets/embed.js
CHANGED
|
@@ -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:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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:
|
|
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("
|
|
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
|
-
`
|
|
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
|
-
"
|
|
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;");
|
package/lib/codegen/generator.js
CHANGED
|
@@ -5,33 +5,36 @@ const { generateRouteTable } = require("./route_table");
|
|
|
5
5
|
const { generateDispatch } = require("./dispatch_gen");
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Generate
|
|
8
|
+
* Generate server.c content.
|
|
9
9
|
*
|
|
10
|
-
* @param {
|
|
11
|
-
* @param {
|
|
12
|
-
* @
|
|
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
|
|
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
|
-
/*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
-
`
|
|
37
|
+
`const int cerver_route_count = ${routes.length};`
|
|
38
38
|
);
|
|
39
39
|
lines.push("");
|
|
40
40
|
|
package/lib/commands/build.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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(
|
|
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)}`);
|
|
@@ -70,18 +79,20 @@ async function build(opts) {
|
|
|
70
79
|
const assets = discoverAssets(publicDir);
|
|
71
80
|
|
|
72
81
|
if (assets.length > 0) {
|
|
73
|
-
|
|
74
|
-
for (
|
|
75
|
-
|
|
82
|
+
const totalSize = assets.reduce((sum, a) => sum + a.size, 0);
|
|
83
|
+
for (let i = 0; i < Math.min(assets.length, 5); i++) {
|
|
84
|
+
const a = assets[i];
|
|
76
85
|
console.log(
|
|
77
86
|
` ${a.servePath} (${(a.size / 1024).toFixed(1)} KB)`
|
|
78
87
|
);
|
|
79
88
|
}
|
|
80
|
-
|
|
89
|
+
if (assets.length > 5) {
|
|
90
|
+
console.log(` ... and ${assets.length - 5} more assets`);
|
|
91
|
+
}
|
|
81
92
|
assetsCode = await generateEmbeddedAssets(assets, config.minify, config.compression);
|
|
82
93
|
console.log(
|
|
83
94
|
` ${assets.length} asset(s), ${(totalSize / 1024).toFixed(1)} KB total` +
|
|
84
|
-
|
|
95
|
+
(config.compression !== "none" ? ` (${config.compression} compressed)` : "")
|
|
85
96
|
);
|
|
86
97
|
} else {
|
|
87
98
|
console.log(" no assets found in public/");
|
|
@@ -90,15 +101,29 @@ async function build(opts) {
|
|
|
90
101
|
|
|
91
102
|
/* ---- 5. Generate C source ---- */
|
|
92
103
|
console.log(" → generating C source...");
|
|
93
|
-
const
|
|
104
|
+
const { serverC, routesC } = generateServer(allRoutes, config, !!assetsCode);
|
|
94
105
|
|
|
95
106
|
/* Write to dist/ */
|
|
96
107
|
const distDir = path.join(projectDir, "dist");
|
|
97
108
|
fs.mkdirSync(distDir, { recursive: true });
|
|
98
109
|
|
|
99
110
|
const serverCPath = path.join(distDir, "server.c");
|
|
100
|
-
fs.writeFileSync(serverCPath,
|
|
101
|
-
console.log(` wrote ${(
|
|
111
|
+
fs.writeFileSync(serverCPath, serverC);
|
|
112
|
+
console.log(` wrote ${(serverC.length / 1024).toFixed(1)} KB → dist/server.c`);
|
|
113
|
+
|
|
114
|
+
const routesCPath = path.join(distDir, "routes.c");
|
|
115
|
+
fs.writeFileSync(routesCPath, routesC);
|
|
116
|
+
console.log(` wrote ${(routesC.length / 1024).toFixed(1)} KB → dist/routes.c`);
|
|
117
|
+
|
|
118
|
+
const assetsCPath = path.join(distDir, "assets.c");
|
|
119
|
+
if (assetsCode) {
|
|
120
|
+
fs.writeFileSync(assetsCPath, assetsCode);
|
|
121
|
+
console.log(` wrote ${(assetsCode.length / 1024).toFixed(1)} KB → dist/assets.c`);
|
|
122
|
+
} else {
|
|
123
|
+
if (fs.existsSync(assetsCPath)) {
|
|
124
|
+
fs.unlinkSync(assetsCPath);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
102
127
|
|
|
103
128
|
/* ---- 6. Copy runtime headers ---- */
|
|
104
129
|
const runtimeDir = path.join(__dirname, "..", "..", "runtime");
|
package/lib/commands/dev.js
CHANGED
|
@@ -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 =
|
|
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;
|
|
@@ -32,10 +39,15 @@ async function dev(opts) {
|
|
|
32
39
|
/* Kill existing server */
|
|
33
40
|
if (serverProcess) {
|
|
34
41
|
console.log("\n ↻ restarting...\n");
|
|
35
|
-
serverProcess
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
const procToKill = serverProcess;
|
|
43
|
+
try {
|
|
44
|
+
procToKill.kill("SIGTERM");
|
|
45
|
+
serverProcess = null;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error(` ✗ failed to kill server: ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
/* Wait for old process to fully exit so the port is released */
|
|
50
|
+
await waitForExit(procToKill, 2000);
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
try {
|
|
@@ -66,29 +78,50 @@ async function dev(opts) {
|
|
|
66
78
|
const env = { ...process.env };
|
|
67
79
|
if (port) env.CERVER_PORT = port;
|
|
68
80
|
|
|
69
|
-
|
|
81
|
+
const proc = spawn(binaryPath, [], {
|
|
70
82
|
stdio: "inherit",
|
|
71
83
|
env,
|
|
72
84
|
cwd: projectDir,
|
|
73
85
|
});
|
|
74
86
|
|
|
75
|
-
serverProcess
|
|
87
|
+
serverProcess = proc;
|
|
88
|
+
|
|
89
|
+
proc.on("error", (err) => {
|
|
76
90
|
console.error(` ✗ server error: ${err.message}`);
|
|
77
|
-
serverProcess = null;
|
|
91
|
+
if (serverProcess === proc) serverProcess = null;
|
|
78
92
|
});
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
proc.on("exit", (code, signal) => {
|
|
95
|
+
/* Only warn on truly unexpected exits — not intentional kills */
|
|
96
|
+
if (serverProcess === proc && signal !== "SIGTERM" && signal !== "SIGINT" && code !== 0) {
|
|
82
97
|
console.error(` ✗ server exited (code: ${code}, signal: ${signal})`);
|
|
83
98
|
}
|
|
84
|
-
serverProcess = null;
|
|
99
|
+
if (serverProcess === proc) serverProcess = null;
|
|
85
100
|
});
|
|
86
101
|
}
|
|
87
102
|
|
|
88
|
-
function waitForExit(proc) {
|
|
103
|
+
function waitForExit(proc, timeoutMs = 2000) {
|
|
89
104
|
if (!proc) return Promise.resolve();
|
|
105
|
+
if (proc.exitCode !== null || proc.signalCode !== null) {
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
}
|
|
90
108
|
return new Promise((resolve) => {
|
|
91
|
-
|
|
109
|
+
let resolved = false;
|
|
110
|
+
const done = () => {
|
|
111
|
+
if (resolved) return;
|
|
112
|
+
resolved = true;
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
proc.removeListener("exit", done);
|
|
115
|
+
proc.removeListener("close", done);
|
|
116
|
+
proc.removeListener("error", done);
|
|
117
|
+
resolve();
|
|
118
|
+
};
|
|
119
|
+
const timer = setTimeout(() => {
|
|
120
|
+
if (resolved) return;
|
|
121
|
+
/* Process did not exit in time — force kill */
|
|
122
|
+
try { proc.kill("SIGKILL"); } catch (_) {}
|
|
123
|
+
done();
|
|
124
|
+
}, timeoutMs);
|
|
92
125
|
proc.once("exit", done);
|
|
93
126
|
proc.once("close", done);
|
|
94
127
|
proc.once("error", done);
|
|
@@ -97,7 +130,7 @@ async function dev(opts) {
|
|
|
97
130
|
|
|
98
131
|
/* ---- Watch for changes ---- */
|
|
99
132
|
const watchPaths = [
|
|
100
|
-
path.join(projectDir, "
|
|
133
|
+
path.join(projectDir, "routes"),
|
|
101
134
|
path.join(projectDir, "public"),
|
|
102
135
|
path.join(projectDir, "cerver.config.js"),
|
|
103
136
|
].filter((p) => fs.existsSync(p));
|
|
@@ -139,7 +172,9 @@ async function dev(opts) {
|
|
|
139
172
|
watcher.close();
|
|
140
173
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
141
174
|
if (serverProcess) {
|
|
142
|
-
|
|
175
|
+
try {
|
|
176
|
+
serverProcess.kill("SIGTERM");
|
|
177
|
+
} catch (_) {}
|
|
143
178
|
await waitForExit(serverProcess);
|
|
144
179
|
}
|
|
145
180
|
process.exit(0);
|