@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/test/run.js DELETED
@@ -1,366 +0,0 @@
1
- "use strict";
2
-
3
- const assert = require("assert/strict");
4
- const fs = require("fs");
5
- const os = require("os");
6
- const path = require("path");
7
- const zlib = require("zlib");
8
-
9
- const { discoverAssets } = require("../lib/assets/discover");
10
- const { compressContent, isCompressible } = require("../lib/assets/compress");
11
- const { generateEmbeddedAssets, mimeFromExt, varName } = require("../lib/assets/embed");
12
- const { minifyContent } = require("../lib/assets/minify");
13
- const {
14
- cString,
15
- emitExpression,
16
- emitStatement,
17
- handlerName,
18
- } = require("../lib/codegen/emit");
19
- const { generateRouteTable } = require("../lib/codegen/route_table");
20
- const { loadConfig } = require("../lib/config");
21
- const IR = require("../lib/ir/types");
22
- const { transformFile } = require("../lib/ir/transform");
23
- const { discoverRoutes } = require("../lib/parser/discover");
24
- const { parseSource } = require("../lib/parser/parse");
25
- const { validate } = require("../lib/validator/validate");
26
-
27
- const tests = [];
28
-
29
- function test(name, fn) {
30
- tests.push({ name, fn });
31
- }
32
-
33
- function tempDir() {
34
- return fs.mkdtempSync(path.join(os.tmpdir(), "cerver-test-"));
35
- }
36
-
37
- function writeFile(filePath, content) {
38
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
39
- fs.writeFileSync(filePath, content);
40
- }
41
-
42
- function cleanup(dir) {
43
- fs.rmSync(dir, { recursive: true, force: true });
44
- }
45
-
46
- function parseAndValidate(source, filename = "route.js") {
47
- const { ast } = parseSource(source, filename);
48
- validate(ast, filename, source);
49
- return ast;
50
- }
51
-
52
- test("discoverRoutes maps file routes and sorts dynamic routes last", () => {
53
- const dir = tempDir();
54
- try {
55
- const routesDir = path.join(dir, "app", "routes");
56
- writeFile(path.join(routesDir, "index.js"), "export function GET(req, res) {}");
57
- writeFile(path.join(routesDir, "about.js"), "export function GET(req, res) {}");
58
- writeFile(path.join(routesDir, "blog", "index.js"), "export function GET(req, res) {}");
59
- writeFile(path.join(routesDir, "blog", "[slug].js"), "export function GET(req, res) {}");
60
- writeFile(path.join(routesDir, "blog", "draft.txt"), "ignored");
61
-
62
- const routes = discoverRoutes(routesDir).map((route) => ({
63
- filePath: path.relative(routesDir, route.filePath),
64
- urlPath: route.urlPath,
65
- }));
66
-
67
- assert.deepEqual(routes, [
68
- { filePath: "index.js", urlPath: "/" },
69
- { filePath: "about.js", urlPath: "/about" },
70
- { filePath: path.join("blog", "index.js"), urlPath: "/blog" },
71
- { filePath: path.join("blog", "[slug].js"), urlPath: "/blog/:slug" },
72
- ]);
73
- } finally {
74
- cleanup(dir);
75
- }
76
- });
77
-
78
- test("discoverRoutes returns an empty list when the routes directory is missing", () => {
79
- const dir = tempDir();
80
- try {
81
- assert.deepEqual(discoverRoutes(path.join(dir, "missing")), []);
82
- } finally {
83
- cleanup(dir);
84
- }
85
- });
86
-
87
- test("validate accepts supported handler syntax", () => {
88
- const source = `
89
- export function GET(req, res) {
90
- const id = req.params.id;
91
- if (id === "42") {
92
- return res.json(200, '{"ok":true}');
93
- }
94
- return res.text(404, "missing");
95
- }
96
- `;
97
-
98
- assert.doesNotThrow(() => parseAndValidate(source));
99
- });
100
-
101
- test("validate rejects unsupported HTTP methods and async handlers", () => {
102
- const badMethod = parseSource(
103
- "export function PUT(req, res) { return res.text(200, 'no'); }",
104
- "bad-method.js"
105
- );
106
- assert.throws(
107
- () => validate(badMethod.ast, "bad-method.js", badMethod.source),
108
- /exported function "PUT" is not a valid HTTP method/
109
- );
110
-
111
- const asyncHandler = parseSource(
112
- "export async function GET(req, res) { await work(); return res.text(200, 'ok'); }",
113
- "async-route.js"
114
- );
115
- assert.throws(
116
- () => validate(asyncHandler.ast, "async-route.js", asyncHandler.source),
117
- /async functions are not supported[\s\S]*async\/await is not supported/
118
- );
119
- });
120
-
121
- test("transformFile produces route IR for params, query, headers, and template returns", () => {
122
- const source = `
123
- export function GET(req, res) {
124
- const userId = req.params.id;
125
- if (req.query.preview === "true" && req.headers["x-mode"] !== "off") {
126
- return res.html(200, \`<h1>\${userId}</h1>\`);
127
- }
128
- return res.text(404, "missing");
129
- }
130
- `;
131
- const ast = parseAndValidate(source, "users.js");
132
- const routes = transformFile(ast, "/users/:id");
133
-
134
- assert.equal(routes.length, 1);
135
- assert.equal(routes[0].method, "GET");
136
- assert.equal(routes[0].urlPath, "/users/:id");
137
- assert.deepEqual(routes[0].params, ["id"]);
138
-
139
- const variable = routes[0].handler.variables[0];
140
- assert.equal(variable.name, "userId");
141
- assert.equal(variable.initExpr.type, "ParamAccess");
142
- assert.equal(variable.initExpr.paramName, "id");
143
-
144
- const [ifStmt, fallback] = routes[0].handler.body;
145
- assert.equal(ifStmt.type, "If");
146
- assert.equal(ifStmt.condition.type, "Logical");
147
- assert.equal(ifStmt.condition.left.left.type, "QueryAccess");
148
- assert.equal(ifStmt.condition.right.left.type, "HeaderAccess");
149
- assert.equal(ifStmt.thenBody[0].responseType, "html");
150
- assert.equal(ifStmt.thenBody[0].value.type, "Concat");
151
- assert.equal(fallback.status, 404);
152
- });
153
-
154
- test("emit helpers escape C strings and map IR expressions", () => {
155
- assert.equal(cString('a"b\\c\n'), '"a\\"b\\\\c\\n"');
156
- assert.equal(handlerName("GET", "/"), "handle_GET_index");
157
- assert.equal(handlerName("POST", "/users/:id"), "handle_POST_users_id");
158
-
159
- const comparison = IR.IRComparison(
160
- "===",
161
- IR.IRParamAccess("id"),
162
- IR.IRStringLiteral("42")
163
- );
164
- assert.equal(
165
- emitExpression(comparison),
166
- '(strcmp(cerver_req_param(req, "id"), "42") == 0)'
167
- );
168
-
169
- assert.deepEqual(
170
- emitStatement(IR.IRReturn("text", 201, IR.IRStringLiteral("created")), 1),
171
- [
172
- ' cerver_res_text(res, 201, "created");',
173
- " return;",
174
- ]
175
- );
176
- });
177
-
178
- test("generateRouteTable emits forward declarations, entries, and count", () => {
179
- const routes = [
180
- IR.IRRoute("GET", "/", [], IR.IRHandler([], [])),
181
- IR.IRRoute("POST", "/users/:id", ["id"], IR.IRHandler([], [])),
182
- ];
183
-
184
- const code = generateRouteTable(routes);
185
-
186
- assert.match(code, /static void handle_GET_index\(cerver_request_t \*req, cerver_response_t \*res\);/);
187
- assert.match(code, /static cerver_route_t cerver_routes\[\] = \{/);
188
- assert.match(code, /\{ "GET", "\/", handle_GET_index \},/);
189
- assert.match(code, /\{ "POST", "\/users\/:id", handle_POST_users_id \},/);
190
- assert.match(code, /static const int cerver_route_count = 2;/);
191
- });
192
-
193
- test("generateDispatch generates correct parameter extraction and termination", () => {
194
- const { generateDispatch } = require("../lib/codegen/dispatch_gen");
195
- const routes = [
196
- IR.IRRoute("GET", "/users/:id/profile", ["id"], IR.IRHandler([], [])),
197
- ];
198
- const code = generateDispatch(routes);
199
- assert.match(code, /req->params\[req->params_count\]\.key = "id";/);
200
- assert.match(code, /req->params\[req->params_count\]\.value = seg1_start;/);
201
- assert.match(code, /\(\(char\*\)seg1_start\)\[seg1_len\] = '\\0';/);
202
- });
203
-
204
- test("loadConfig merges defaults and supports export default configs", () => {
205
- const dir = tempDir();
206
- try {
207
- assert.deepEqual(loadConfig(dir), {
208
- port: 8080,
209
- embed: true,
210
- minify: true,
211
- compression: "none",
212
- threads: 4,
213
- });
214
-
215
- writeFile(
216
- path.join(dir, "cerver.config.js"),
217
- 'export default { port: 3001, embed: false, minify: false, compression: "gzip" };\n'
218
- );
219
-
220
- assert.deepEqual(loadConfig(dir), {
221
- port: 3001,
222
- embed: false,
223
- minify: false,
224
- compression: "gzip",
225
- threads: 4,
226
- });
227
- } finally {
228
- cleanup(dir);
229
- }
230
- });
231
-
232
- test("loadConfig rejects invalid ports and compression values", () => {
233
- const dir = tempDir();
234
- try {
235
- writeFile(path.join(dir, "cerver.config.js"), "module.exports = { port: 70000 };\n");
236
- assert.throws(() => loadConfig(dir), /invalid port 70000/);
237
-
238
- writeFile(path.join(dir, "cerver.config.js"), 'module.exports = { compression: "zip" };\n');
239
- assert.throws(() => loadConfig(dir), /unsupported compression "zip"/);
240
- } finally {
241
- cleanup(dir);
242
- }
243
- });
244
-
245
- test("discoverAssets maps public files and skips dotfiles", () => {
246
- const dir = tempDir();
247
- try {
248
- const publicDir = path.join(dir, "public");
249
- writeFile(path.join(publicDir, "index.html"), "<h1>Hello</h1>");
250
- writeFile(path.join(publicDir, ".secret"), "ignore me");
251
- writeFile(path.join(publicDir, "css", "app.css"), "body { color: red; }");
252
-
253
- const assets = discoverAssets(publicDir)
254
- .map((asset) => ({
255
- servePath: asset.servePath,
256
- ext: asset.ext,
257
- size: asset.size,
258
- }))
259
- .sort((a, b) => a.servePath.localeCompare(b.servePath));
260
-
261
- assert.deepEqual(assets, [
262
- { servePath: "/css/app.css", ext: ".css", size: Buffer.byteLength("body { color: red; }") },
263
- { servePath: "/index.html", ext: ".html", size: Buffer.byteLength("<h1>Hello</h1>") },
264
- ]);
265
- } finally {
266
- cleanup(dir);
267
- }
268
- });
269
-
270
- test("asset helpers generate stable names and MIME types", () => {
271
- assert.equal(varName("/static/app.min.css"), "asset_static_app_min_css");
272
- assert.equal(mimeFromExt(".json"), "application/json; charset=utf-8");
273
- assert.equal(mimeFromExt(".unknown"), "application/octet-stream");
274
- });
275
-
276
- test("minifyContent minifies CSS content", async () => {
277
- const source = Buffer.from("/* comment */\nbody { color: red; margin: 0; }\n", "utf8");
278
- const minified = await minifyContent(source, ".css");
279
- const text = minified.toString("utf8");
280
-
281
- assert.ok(text.length < source.length);
282
- assert.doesNotMatch(text, /comment/);
283
- assert.match(text, /color:red/);
284
- });
285
-
286
- test("compression helpers identify compressible MIME types and round-trip gzip", async () => {
287
- const source = Buffer.from("hello cerver ".repeat(80), "utf8");
288
-
289
- assert.equal(isCompressible("text/css; charset=utf-8"), true);
290
- assert.equal(isCompressible("application/json; charset=utf-8"), true);
291
- assert.equal(isCompressible("image/png"), false);
292
-
293
- const compressed = await compressContent(source, "gzip");
294
- assert.ok(compressed.length < source.length);
295
- assert.equal(zlib.gunzipSync(compressed).toString("utf8"), source.toString("utf8"));
296
- });
297
-
298
- test("generateEmbeddedAssets emits C arrays and asset table entries", async () => {
299
- const dir = tempDir();
300
- try {
301
- const filePath = path.join(dir, "public", "css", "app.css");
302
- const content = "body { color: red; }";
303
- writeFile(filePath, content);
304
-
305
- const code = await generateEmbeddedAssets(
306
- [{ filePath, servePath: "/css/app.css", ext: ".css" }],
307
- false
308
- );
309
-
310
- assert.match(code, /static const unsigned char asset_css_app_css\[\] = \{/);
311
- assert.match(
312
- code,
313
- new RegExp(`static const unsigned int asset_css_app_css_len = ${Buffer.byteLength(content)};`)
314
- );
315
- assert.match(
316
- code,
317
- /\{ "\/css\/app\.css", "text\/css; charset=utf-8", asset_css_app_css, asset_css_app_css_len, .* \},/
318
- );
319
- assert.match(code, /static const int cerver_embedded_asset_count = 1;/);
320
- } finally {
321
- cleanup(dir);
322
- }
323
- });
324
-
325
- test("generateEmbeddedAssets can emit gzip variants for compressible assets", async () => {
326
- const dir = tempDir();
327
- try {
328
- const filePath = path.join(dir, "public", "css", "app.css");
329
- const content = "body { color: red; }\n".repeat(80);
330
- writeFile(filePath, content);
331
-
332
- const code = await generateEmbeddedAssets(
333
- [{ filePath, servePath: "/css/app.css", ext: ".css" }],
334
- false,
335
- "gzip"
336
- );
337
-
338
- assert.match(code, /static const unsigned char asset_css_app_css_gz\[\] = \{/);
339
- assert.match(code, /static const unsigned int asset_css_app_css_gz_len = \d+;/);
340
- assert.match(
341
- code,
342
- /\{ "\/css\/app\.css", "text\/css; charset=utf-8", asset_css_app_css, asset_css_app_css_len, asset_css_app_css_gz, asset_css_app_css_gz_len, NULL, 0, NULL, 0 \},/
343
- );
344
- } finally {
345
- cleanup(dir);
346
- }
347
- });
348
-
349
- (async () => {
350
- let passed = 0;
351
-
352
- for (const { name, fn } of tests) {
353
- try {
354
- await fn();
355
- passed += 1;
356
- console.log(`ok ${passed} - ${name}`);
357
- } catch (err) {
358
- console.error(`not ok ${passed + 1} - ${name}`);
359
- console.error(err && err.stack ? err.stack : err);
360
- process.exitCode = 1;
361
- return;
362
- }
363
- }
364
-
365
- console.log(`\n${passed} test(s) passed`);
366
- })();