kempo-server 1.3.0 → 1.4.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.
Files changed (44) hide show
  1. package/.github/copilot-instructions.md +96 -0
  2. package/README.md +212 -4
  3. package/docs/configuration.html +119 -0
  4. package/docs/examples.html +201 -0
  5. package/docs/getting-started.html +72 -0
  6. package/docs/index.html +53 -330
  7. package/docs/manifest.json +87 -0
  8. package/docs/media/hexagon.svg +22 -0
  9. package/docs/media/icon-maskable.png +0 -0
  10. package/docs/media/icon.svg +44 -0
  11. package/docs/media/icon128.png +0 -0
  12. package/docs/media/icon144.png +0 -0
  13. package/docs/media/icon152.png +0 -0
  14. package/docs/media/icon16-48.svg +21 -0
  15. package/docs/media/icon16.png +0 -0
  16. package/docs/media/icon192.png +0 -0
  17. package/docs/media/icon256.png +0 -0
  18. package/docs/media/icon32.png +0 -0
  19. package/docs/media/icon384.png +0 -0
  20. package/docs/media/icon48.png +0 -0
  21. package/docs/media/icon512.png +0 -0
  22. package/docs/media/icon64.png +0 -0
  23. package/docs/media/icon72.png +0 -0
  24. package/docs/media/icon96.png +0 -0
  25. package/docs/media/kempo-fist.svg +21 -0
  26. package/docs/middleware.html +147 -0
  27. package/docs/request-response.html +95 -0
  28. package/docs/routing.html +77 -0
  29. package/package.json +8 -3
  30. package/tests/builtinMiddleware-cors.node-test.js +17 -0
  31. package/tests/builtinMiddleware.node-test.js +74 -0
  32. package/tests/defaultConfig.node-test.js +13 -0
  33. package/tests/example-middleware.node-test.js +31 -0
  34. package/tests/findFile.node-test.js +46 -0
  35. package/tests/getFiles.node-test.js +25 -0
  36. package/tests/getFlags.node-test.js +30 -0
  37. package/tests/index.node-test.js +23 -0
  38. package/tests/middlewareRunner.node-test.js +18 -0
  39. package/tests/requestWrapper.node-test.js +51 -0
  40. package/tests/responseWrapper.node-test.js +74 -0
  41. package/tests/router-middleware.node-test.js +46 -0
  42. package/tests/router.node-test.js +88 -0
  43. package/tests/serveFile.node-test.js +52 -0
  44. package/tests/test-utils.js +106 -0
@@ -0,0 +1,88 @@
1
+ import http from 'http';
2
+ import path from 'path';
3
+ import {withTempDir, write, expect, randomPort, httpGet, log} from './test-utils.js';
4
+ import router from '../router.js';
5
+ import defaultConfig from '../defaultConfig.js';
6
+
7
+ export default {
8
+ 'serves static files and 404s unknown': async ({pass, fail, log}) => {
9
+ try {
10
+ await withTempDir(async (dir) => {
11
+ await write(dir, 'index.html', '<h1>Home</h1>');
12
+ const prev = process.cwd();
13
+ process.chdir(dir);
14
+ const flags = {root: '.', logging: 0, scan: false};
15
+ const logFn = () => {};
16
+ const handler = await router(flags, logFn);
17
+ const server = http.createServer(handler);
18
+ const port = randomPort();
19
+ await new Promise(r => server.listen(port, r));
20
+ await new Promise(r => setTimeout(r, 50));
21
+ try {
22
+ const ok = await httpGet(`http://localhost:${port}/index.html`);
23
+ log('ok status: ' + ok.res.statusCode);
24
+ if(ok.res.statusCode !== 200) throw new Error('static 200');
25
+ const miss = await httpGet(`http://localhost:${port}/nope`);
26
+ log('miss status: ' + miss.res.statusCode);
27
+ if(miss.res.statusCode !== 404) throw new Error('404');
28
+ } finally {
29
+ server.close();
30
+ process.chdir(prev);
31
+ }
32
+ });
33
+ pass('static + 404');
34
+ } catch(e){ fail(e.message); }
35
+ },
36
+ 'rescan on 404 when enabled and not blacklisted': async ({pass, fail, log}) => {
37
+ try {
38
+ await withTempDir(async (dir) => {
39
+ const prev = process.cwd();
40
+ process.chdir(dir);
41
+ const flags = {root: '.', logging: 0, scan: true};
42
+ const handler = await router(flags, log);
43
+ const server = http.createServer(handler);
44
+ const port = randomPort();
45
+ await new Promise(r => server.listen(port, r));
46
+ await new Promise(r => setTimeout(r, 50));
47
+ try {
48
+ const miss1 = await httpGet(`http://localhost:${port}/late.html`);
49
+ log('first miss: ' + miss1.res.statusCode);
50
+ if(miss1.res.statusCode !== 404) throw new Error('first 404');
51
+ await write(dir, 'late.html', 'later');
52
+ const hit = await httpGet(`http://localhost:${port}/late.html`);
53
+ log('hit after rescan: ' + hit.res.statusCode);
54
+ if(hit.res.statusCode !== 200) throw new Error('served after rescan');
55
+ } finally { server.close(); process.chdir(prev); }
56
+ });
57
+ pass('rescan');
58
+ } catch(e){ fail(e.message); }
59
+ },
60
+ 'custom and wildcard routes serve mapped files': async ({pass, fail, log}) => {
61
+ try {
62
+ await withTempDir(async (dir) => {
63
+ const fileA = await write(dir, 'a.txt', 'A');
64
+ const fileB = await write(dir, 'b/1.txt', 'B1');
65
+ const prev = process.cwd();
66
+ process.chdir(dir);
67
+ const flags = {root: '.', logging: 0, scan: false};
68
+ const logFn = () => {};
69
+ // write config before init
70
+ await write(dir, '.config.json', JSON.stringify({customRoutes: {'/a': fileA, 'b/*': path.join(dir, 'b/*')}}));
71
+ const handler = await router(flags, logFn);
72
+ const server = http.createServer(handler);
73
+ const port = randomPort();
74
+ await new Promise(r => server.listen(port, r));
75
+ await new Promise(r => setTimeout(r, 50));
76
+ try {
77
+ const r1 = await httpGet(`http://localhost:${port}/a`);
78
+ log('custom status: ' + r1.res.statusCode);
79
+ if(r1.body.toString() !== 'A') throw new Error('custom route');
80
+ const r2 = await httpGet(`http://localhost:${port}/b/1.txt`);
81
+ log('wildcard status: ' + r2.res.statusCode);
82
+ if(r2.body.toString() !== 'B1') throw new Error('wildcard');
83
+ } finally { server.close(); process.chdir(prev); }
84
+ });
85
+ pass('custom+wildcard');
86
+ } catch(e){ fail(e.message); }
87
+ }
88
+ };
@@ -0,0 +1,52 @@
1
+ import serveFile from '../serveFile.js';
2
+ import findFile from '../findFile.js';
3
+ import defaultConfig from '../defaultConfig.js';
4
+ import path from 'path';
5
+ import {createMockReq, createMockRes, withTempDir, write, expect, log} from './test-utils.js';
6
+
7
+ export default {
8
+ 'serves static file with correct mime': async ({pass, fail}) => {
9
+ try {
10
+ await withTempDir(async (dir) => {
11
+ const cfg = JSON.parse(JSON.stringify(defaultConfig));
12
+ await write(dir, 'index.html', '<h1>Hi</h1>');
13
+ const files = [path.join(dir, 'index.html')];
14
+ const res = createMockRes();
15
+ const ok = await serveFile(files, dir, '/index.html', 'GET', cfg, createMockReq(), res, log);
16
+ expect(ok === true, 'should serve');
17
+ expect(res.statusCode === 200, 'status');
18
+ expect(res.getHeader('Content-Type') === 'text/html', 'mime');
19
+ });
20
+ pass('static');
21
+ } catch(e){ fail(e.message); }
22
+ },
23
+ 'executes route files by calling default export': async ({pass, fail}) => {
24
+ try {
25
+ await withTempDir(async (dir) => {
26
+ const cfg = JSON.parse(JSON.stringify(defaultConfig));
27
+ await write(dir, 'api/GET.js', "export default async (req, res) => { res.status(201).json({ok:true, params:req.params}); }\n");
28
+ const files = [path.join(dir, 'api/GET.js')];
29
+ const res = createMockRes();
30
+ const ok = await serveFile(files, dir, '/api', 'GET', cfg, createMockReq(), res, log);
31
+ expect(ok === true, 'served route');
32
+ expect(res.statusCode === 201, 'route status');
33
+ expect(res.getBody().toString().includes('ok'), 'body contains ok');
34
+ });
35
+ pass('route exec');
36
+ } catch(e){ fail(e.message); }
37
+ },
38
+ 'handles route file without default function': async ({pass, fail}) => {
39
+ try {
40
+ await withTempDir(async (dir) => {
41
+ const cfg = JSON.parse(JSON.stringify(defaultConfig));
42
+ await write(dir, 'api/GET.js', "export const x = 1;\n");
43
+ const files = [path.join(dir, 'api/GET.js')];
44
+ const res = createMockRes();
45
+ const ok = await serveFile(files, dir, '/api', 'GET', cfg, createMockReq(), res, log);
46
+ expect(ok === true, 'handled');
47
+ expect(res.statusCode === 500, '500');
48
+ });
49
+ pass('route no default');
50
+ } catch(e){ fail(e.message); }
51
+ }
52
+ };
@@ -0,0 +1,106 @@
1
+ import {Readable} from 'stream';
2
+ import {mkdtemp, rm, writeFile, mkdir} from 'fs/promises';
3
+ import os from 'os';
4
+ import path from 'path';
5
+
6
+ export const createMockReq = ({method = 'GET', url = '/', headers = {}, body = null, remoteAddress = '127.0.0.1'} = {}) => {
7
+ const stream = new Readable({read(){}});
8
+ stream.method = method;
9
+ stream.url = url;
10
+ stream.headers = headers;
11
+ stream.socket = {remoteAddress};
12
+ if(body !== null && body !== undefined){
13
+ const data = typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body);
14
+ setImmediate(() => {
15
+ stream.emit('data', Buffer.from(data));
16
+ stream.emit('end');
17
+ });
18
+ } else {
19
+ setImmediate(() => stream.emit('end'));
20
+ }
21
+ return stream;
22
+ };
23
+
24
+ export const createMockRes = () => {
25
+ const headers = new Map();
26
+ let ended = false;
27
+ let statusCode = 200;
28
+ const chunks = [];
29
+ return {
30
+ get statusCode(){return statusCode;},
31
+ set statusCode(v){statusCode = v;},
32
+ setHeader: (k, v) => headers.set(k, v),
33
+ getHeader: (k) => headers.get(k),
34
+ writeHead: (code, hdrs = {}) => {
35
+ statusCode = code;
36
+ Object.entries(hdrs).forEach(([k, v]) => headers.set(k, v));
37
+ },
38
+ write: (chunk) => { if(chunk) chunks.push(Buffer.from(chunk)); return true; },
39
+ end: (chunk) => { if(chunk) chunks.push(Buffer.from(chunk)); ended = true; },
40
+ getHeaders: () => Object.fromEntries(headers),
41
+ getBody: () => Buffer.concat(chunks),
42
+ isEnded: () => ended
43
+ };
44
+ };
45
+
46
+ export const withTempDir = async (fn) => {
47
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'kempo-tests-'));
48
+ try {
49
+ return await fn(dir);
50
+ } finally {
51
+ await rm(dir, {recursive: true, force: true});
52
+ }
53
+ };
54
+
55
+ export const write = async (root, rel, content = '') => {
56
+ const full = path.join(root, rel);
57
+ await mkdir(path.dirname(full), {recursive: true});
58
+ await writeFile(full, content);
59
+ return full;
60
+ };
61
+
62
+ export const wait = ms => new Promise(r => setTimeout(r, ms));
63
+
64
+ export const log = (..._args) => {};
65
+
66
+ export const bigString = (size = 5000) => 'x'.repeat(size);
67
+
68
+ export const randomPort = () => 1024 + Math.floor(Math.random() * 50000);
69
+
70
+ export const httpGet = (url) => new Promise((resolve, reject) => {
71
+ import('http').then(({get}) => {
72
+ get(url, res => {
73
+ const chunks = [];
74
+ res.on('data', c => chunks.push(c));
75
+ res.on('end', () => resolve({res, body: Buffer.concat(chunks)}));
76
+ }).on('error', reject);
77
+ }).catch(reject);
78
+ });
79
+
80
+ export const startNode = async (args, options = {}) => {
81
+ const {spawn} = await import('child_process');
82
+ const child = spawn(process.execPath, args, {stdio: ['ignore', 'pipe', 'pipe'], ...options});
83
+ child.stdout.setEncoding('utf8');
84
+ child.stderr.setEncoding('utf8');
85
+ return child;
86
+ };
87
+
88
+ export const setEnv = (pairs, fn) => {
89
+ const prev = {};
90
+ Object.keys(pairs).forEach(k => { prev[k] = process.env[k]; process.env[k] = pairs[k]; });
91
+ const restore = () => { Object.keys(pairs).forEach(k => { if(prev[k] === undefined){ delete process.env[k]; } else { process.env[k] = prev[k]; } }); };
92
+ return fn().finally(restore);
93
+ };
94
+
95
+ export const expect = (condition, message) => {
96
+ if(!condition){ throw new Error(message); }
97
+ };
98
+
99
+ export const toString = (buf) => Buffer.isBuffer(buf) ? buf.toString() : String(buf);
100
+
101
+ export const gzipSize = async (buf) => {
102
+ const {gzip} = await import('zlib');
103
+ return new Promise((res, rej) => gzip(buf, (e, out) => e ? rej(e) : res(out.length)));
104
+ };
105
+
106
+ export const parseCookies = (setCookie) => Array.isArray(setCookie) ? setCookie : (setCookie ? [setCookie] : []);