kempo-server 1.7.0 → 1.7.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kempo-server",
3
3
  "type": "module",
4
- "version": "1.7.0",
4
+ "version": "1.7.1",
5
5
  "description": "A lightweight, zero-dependency, file based routing server.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
package/src/router.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
 
18
18
  export default async (flags, log) => {
19
19
  log('Initializing router', 2);
20
- const rootPath = path.join(process.cwd(), flags.root);
20
+ const rootPath = path.isAbsolute(flags.root) ? flags.root : path.join(process.cwd(), flags.root);
21
21
  log(`Root path: ${rootPath}`, 2);
22
22
 
23
23
  let config = defaultConfig;
@@ -136,27 +136,19 @@ export default async (flags, log) => {
136
136
 
137
137
  if (config.customRoutes && Object.keys(config.customRoutes).length > 0) {
138
138
  log(`Processing ${Object.keys(config.customRoutes).length} custom routes`, 2);
139
-
140
139
  for (const [urlPath, filePath] of Object.entries(config.customRoutes)) {
141
- try {
142
- // Check if this is a wildcard route
143
- if (urlPath.includes('*')) {
144
- // Store wildcard routes separately for pattern matching
145
- wildcardRoutes.set(urlPath, filePath);
146
- log(`Wildcard route mapped: ${urlPath} -> ${filePath}`, 2);
147
- } else {
148
- // Resolve the file path relative to the current working directory
149
- const resolvedPath = path.resolve(filePath);
150
-
151
- // Check if the file exists (we'll do this async)
152
- const { stat } = await import('fs/promises');
153
- await stat(resolvedPath);
154
-
155
- customRoutes.set(urlPath, resolvedPath);
156
- log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`, 2);
157
- }
158
- } catch (error) {
159
- log(`Custom route error for ${urlPath} -> ${filePath}: ${error.message}`, 1);
140
+ // Check if this is a wildcard route
141
+ if (urlPath.includes('*')) {
142
+ // Resolve the file path relative to the current working directory
143
+ const resolvedPath = path.resolve(filePath);
144
+ // Store wildcard routes separately for pattern matching
145
+ wildcardRoutes.set(urlPath, resolvedPath);
146
+ log(`Wildcard route mapped: ${urlPath} -> ${resolvedPath}`, 2);
147
+ } else {
148
+ // Resolve the file path relative to the current working directory
149
+ const resolvedPath = path.resolve(filePath);
150
+ customRoutes.set(urlPath, resolvedPath);
151
+ log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`, 2);
160
152
  }
161
153
  }
162
154
  }
@@ -247,39 +239,63 @@ export default async (flags, log) => {
247
239
  const requestPath = req.url.split('?')[0];
248
240
  log(`${req.method} ${requestPath}`, 0);
249
241
 
250
- // Check custom routes first
251
- if (customRoutes.has(requestPath)) {
252
- const customFilePath = customRoutes.get(requestPath);
253
- log(`Serving custom route: ${requestPath} -> ${customFilePath}`, 2);
254
-
242
+
243
+ // Check custom routes first (allow outside rootPath)
244
+ log(`customRoutes keys: ${Array.from(customRoutes.keys()).join(', ')}`, 1);
245
+ // Normalize requestPath and keys for matching
246
+ const normalizePath = p => {
247
+ let np = decodeURIComponent(p);
248
+ if (!np.startsWith('/')) np = '/' + np;
249
+ if (np.length > 1 && np.endsWith('/')) np = np.slice(0, -1);
250
+ return np;
251
+ };
252
+ const normalizedRequestPath = normalizePath(requestPath);
253
+ log(`Normalized requestPath: ${normalizedRequestPath}`, 1);
254
+ let matchedKey = null;
255
+ for (const key of customRoutes.keys()) {
256
+ if (normalizePath(key) === normalizedRequestPath) {
257
+ matchedKey = key;
258
+ break;
259
+ }
260
+ }
261
+ if (matchedKey) {
262
+ const customFilePath = customRoutes.get(matchedKey);
263
+ log(`Serving custom route: ${normalizedRequestPath} -> ${customFilePath}`, 2);
255
264
  try {
265
+ const { stat } = await import('fs/promises');
266
+ try {
267
+ await stat(customFilePath);
268
+ log(`Custom route file exists: ${customFilePath}`, 2);
269
+ } catch (e) {
270
+ log(`Custom route file does NOT exist: ${customFilePath}`, 0);
271
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
272
+ res.end('Custom route file not found');
273
+ return;
274
+ }
256
275
  const fileContent = await readFile(customFilePath);
257
276
  const fileExtension = path.extname(customFilePath).toLowerCase().slice(1);
258
277
  const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
259
-
260
278
  log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`, 2);
261
279
  res.writeHead(200, { 'Content-Type': mimeType });
262
280
  res.end(fileContent);
263
281
  return; // Successfully served custom route
264
282
  } catch (error) {
265
- log(`Error serving custom route ${requestPath}: ${error.message}`, 0);
283
+ log(`Error serving custom route ${normalizedRequestPath}: ${error.message}`, 0);
266
284
  res.writeHead(500, { 'Content-Type': 'text/plain' });
267
285
  res.end('Internal Server Error');
268
286
  return;
269
287
  }
270
288
  }
271
-
272
- // Check wildcard routes
289
+
290
+ // Check wildcard routes (allow outside rootPath)
273
291
  const wildcardMatch = findWildcardRoute(requestPath);
274
292
  if (wildcardMatch) {
275
293
  const resolvedFilePath = resolveWildcardPath(wildcardMatch.filePath, wildcardMatch.matches);
276
294
  log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`, 2);
277
-
278
295
  try {
279
296
  const fileContent = await readFile(resolvedFilePath);
280
297
  const fileExtension = path.extname(resolvedFilePath).toLowerCase().slice(1);
281
298
  const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
282
-
283
299
  log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`, 2);
284
300
  res.writeHead(200, { 'Content-Type': mimeType });
285
301
  res.end(fileContent);
@@ -0,0 +1,81 @@
1
+ import router from '../src/router.js';
2
+ import http from 'http';
3
+ import { writeFile, mkdir } from 'fs/promises';
4
+ import path from 'path';
5
+ import { randomPort, httpGet, withTempDir, write } from './test-utils.js';
6
+
7
+ /*
8
+ This test verifies that a customRoute pointing outside the rootPath is served
9
+ instead of a static file inside the rootPath, if both exist.
10
+ */
11
+
12
+ export default {
13
+ 'customRoute outside rootPath takes precedence over static file': async ({pass, fail, log}) => {
14
+ try {
15
+ await withTempDir(async (dir) => {
16
+ // Print initial working directory
17
+ log('Initial process.cwd(): ' + process.cwd());
18
+
19
+ // Setup: create static file in rootPath
20
+ const rootDir = path.join(dir, 'public');
21
+ await mkdir(path.join(rootDir, 'src'), { recursive: true });
22
+ await writeFile(path.join(rootDir, 'src', 'file.txt'), 'static');
23
+ // Create custom file outside rootPath, matching resolved customRoute
24
+ const customFilePath = path.resolve(dir, '..', 'src', 'file.txt');
25
+ await mkdir(path.dirname(customFilePath), { recursive: true });
26
+ await writeFile(customFilePath, 'custom');
27
+ log('Custom file path: ' + customFilePath);
28
+
29
+ // Check file existence
30
+ try {
31
+ const { stat } = await import('fs/promises');
32
+ await stat(customFilePath);
33
+ log('Custom file exists at setup');
34
+ } catch (e) {
35
+ log('Custom file does NOT exist at setup');
36
+ }
37
+
38
+ // Write config with customRoute pointing outside rootPath
39
+ const config = {
40
+ customRoutes: {
41
+ '/src/file.txt': '../src/file.txt'
42
+ }
43
+ };
44
+ await writeFile(path.join(rootDir, '.config.json'), JSON.stringify(config));
45
+ log('Config written: ' + JSON.stringify(config));
46
+
47
+ // Set working directory to temp dir so relative paths resolve correctly
48
+ const prevCwd = process.cwd();
49
+ process.chdir(dir);
50
+ const flags = { root: rootDir, logging: 4 };
51
+ const logFn = (...args) => log(args.map(String).join(' '));
52
+ log('Starting server with flags: ' + JSON.stringify(flags));
53
+ log('process.cwd() before router: ' + process.cwd());
54
+ const handler = await router(flags, logFn);
55
+ const server = http.createServer(handler);
56
+ const port = randomPort();
57
+ await new Promise(r => server.listen(port, r));
58
+ await new Promise(r => setTimeout(r, 50));
59
+
60
+ try {
61
+ // Print process.cwd() before request
62
+ log('process.cwd() before request: ' + process.cwd());
63
+ // Request the file
64
+ const requestUrl = `http://localhost:${port}/src/file.txt`;
65
+ log('Requesting URL: ' + requestUrl);
66
+ const response = await httpGet(requestUrl);
67
+ log('status: ' + response.res.statusCode);
68
+ log('response body: ' + response.body.toString());
69
+ if(response.res.statusCode !== 200) throw new Error('status not 200');
70
+ if(response.body.toString() !== 'custom') throw new Error('did not serve customRoute file');
71
+ } finally {
72
+ server.close();
73
+ process.chdir(prevCwd);
74
+ }
75
+ });
76
+ pass('customRoute outside rootPath is served');
77
+ } catch(e){
78
+ fail(e.message);
79
+ }
80
+ }
81
+ };