kempo-server 1.7.6 → 1.7.8

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.6",
4
+ "version": "1.7.8",
5
5
  "description": "A lightweight, zero-dependency, file based routing server.",
6
6
  "main": "dist/index.js",
7
7
  "exports": {
package/src/router.js CHANGED
@@ -16,9 +16,9 @@ import {
16
16
  } from './builtinMiddleware.js';
17
17
 
18
18
  export default async (flags, log) => {
19
- log('Initializing router', 2);
19
+ log('Initializing router', 3);
20
20
  const rootPath = path.isAbsolute(flags.root) ? flags.root : path.join(process.cwd(), flags.root);
21
- log(`Root path: ${rootPath}`, 2);
21
+ log(`Root path: ${rootPath}`, 3);
22
22
 
23
23
  let config = defaultConfig;
24
24
  try {
@@ -28,25 +28,25 @@ export default async (flags, log) => {
28
28
  ? configFileName
29
29
  : path.join(rootPath, configFileName);
30
30
 
31
- log(`Config file name: ${configFileName}`, 2);
32
- log(`Config path: ${configPath}`, 2);
31
+ log(`Config file name: ${configFileName}`, 3);
32
+ log(`Config path: ${configPath}`, 3);
33
33
 
34
34
  // Validate that config file is within the server root directory
35
35
  // Allow absolute paths (user explicitly specified location)
36
36
  if (!path.isAbsolute(configFileName)) {
37
37
  const relativeConfigPath = path.relative(rootPath, configPath);
38
- log(`Relative config path: ${relativeConfigPath}`, 2);
39
- log(`Starts with '..': ${relativeConfigPath.startsWith('..')}`, 2);
38
+ log(`Relative config path: ${relativeConfigPath}`, 4);
39
+ log(`Starts with '..': ${relativeConfigPath.startsWith('..')}`, 4);
40
40
  if (relativeConfigPath.startsWith('..') || path.isAbsolute(relativeConfigPath)) {
41
- log(`Validation failed - throwing error`, 2);
41
+ log(`Validation failed - throwing error`, 4);
42
42
  throw new Error(`Config file must be within the server root directory. Config path: ${configPath}, Root path: ${rootPath}`);
43
43
  }
44
- log(`Validation passed`, 2);
44
+ log(`Validation passed`, 4);
45
45
  } else {
46
- log(`Config file name is absolute, skipping validation`, 2);
46
+ log(`Config file name is absolute, skipping validation`, 4);
47
47
  }
48
48
 
49
- log(`Loading config from: ${configPath}`, 2);
49
+ log(`Loading config from: ${configPath}`, 3);
50
50
  const configContent = await readFile(configPath, 'utf8');
51
51
  const userConfig = JSON.parse(configContent);
52
52
  config = {
@@ -70,14 +70,14 @@ export default async (flags, log) => {
70
70
  ...userConfig.cache
71
71
  }
72
72
  };
73
- log('User config loaded and merged with defaults', 2);
73
+ log('User config loaded and merged with defaults', 3);
74
74
  } catch (e){
75
75
  // Only fall back to default config for file reading/parsing errors
76
76
  // Let validation errors propagate up
77
77
  if (e.message.includes('Config file must be within the server root directory')) {
78
78
  throw e;
79
79
  }
80
- log('Using default config (no config file found)', 2);
80
+ log('Using default config (no config file found)', 3);
81
81
  }
82
82
 
83
83
  /*
@@ -88,10 +88,10 @@ export default async (flags, log) => {
88
88
  dis.add("\\.config$");
89
89
  dis.add("\\.git/");
90
90
  config.disallowedRegex = [...dis];
91
- log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`, 2);
91
+ log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`, 3);
92
92
 
93
93
  let files = await getFiles(rootPath, config, log);
94
- log(`Initial scan found ${files.length} files`, 1);
94
+ log(`Initial scan found ${files.length} files`, 2);
95
95
 
96
96
  // Initialize middleware runner
97
97
  const middlewareRunner = new MiddlewareRunner();
@@ -99,32 +99,32 @@ export default async (flags, log) => {
99
99
  // Load built-in middleware based on config
100
100
  if (config.middleware?.cors?.enabled) {
101
101
  middlewareRunner.use(corsMiddleware(config.middleware.cors));
102
- log('CORS middleware enabled', 2);
102
+ log('CORS middleware enabled', 3);
103
103
  }
104
104
 
105
105
  if (config.middleware?.compression?.enabled) {
106
106
  middlewareRunner.use(compressionMiddleware(config.middleware.compression));
107
- log('Compression middleware enabled', 2);
107
+ log('Compression middleware enabled', 3);
108
108
  }
109
109
 
110
110
  if (config.middleware?.rateLimit?.enabled) {
111
111
  middlewareRunner.use(rateLimitMiddleware(config.middleware.rateLimit));
112
- log('Rate limit middleware enabled', 2);
112
+ log('Rate limit middleware enabled', 3);
113
113
  }
114
114
 
115
115
  if (config.middleware?.security?.enabled) {
116
116
  middlewareRunner.use(securityMiddleware(config.middleware.security));
117
- log('Security middleware enabled', 2);
117
+ log('Security middleware enabled', 3);
118
118
  }
119
119
 
120
120
  if (config.middleware?.logging?.enabled) {
121
121
  middlewareRunner.use(loggingMiddleware(config.middleware.logging, log));
122
- log('Logging middleware enabled', 2);
122
+ log('Logging middleware enabled', 3);
123
123
  }
124
124
 
125
125
  // Load custom middleware files
126
126
  if (config.middleware?.custom && config.middleware.custom.length > 0) {
127
- log(`Loading ${config.middleware.custom.length} custom middleware files`, 2);
127
+ log(`Loading ${config.middleware.custom.length} custom middleware files`, 3);
128
128
 
129
129
  for (const middlewarePath of config.middleware.custom) {
130
130
  try {
@@ -134,7 +134,7 @@ export default async (flags, log) => {
134
134
 
135
135
  if (typeof customMiddleware === 'function') {
136
136
  middlewareRunner.use(customMiddleware(config.middleware));
137
- log(`Custom middleware loaded: ${middlewarePath}`, 2);
137
+ log(`Custom middleware loaded: ${middlewarePath}`, 3);
138
138
  } else {
139
139
  log(`Custom middleware error: ${middlewarePath} does not export a default function`, 1);
140
140
  }
@@ -159,7 +159,7 @@ export default async (flags, log) => {
159
159
  const wildcardRoutes = new Map();
160
160
 
161
161
  if (config.customRoutes && Object.keys(config.customRoutes).length > 0) {
162
- log(`Processing ${Object.keys(config.customRoutes).length} custom routes`, 2);
162
+ log(`Processing ${Object.keys(config.customRoutes).length} custom routes`, 3);
163
163
  for (const [urlPath, filePath] of Object.entries(config.customRoutes)) {
164
164
  // Check if this is a wildcard route
165
165
  if (urlPath.includes('*')) {
@@ -167,12 +167,12 @@ export default async (flags, log) => {
167
167
  const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(rootPath, filePath);
168
168
  // Store wildcard routes separately for pattern matching
169
169
  wildcardRoutes.set(urlPath, resolvedPath);
170
- log(`Wildcard route mapped: ${urlPath} -> ${resolvedPath}`, 2);
170
+ log(`Wildcard route mapped: ${urlPath} -> ${resolvedPath}`, 3);
171
171
  } else {
172
172
  // Resolve the file path relative to rootPath if relative, otherwise use absolute path
173
173
  const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(rootPath, filePath);
174
174
  customRoutes.set(urlPath, resolvedPath);
175
- log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`, 2);
175
+ log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`, 3);
176
176
  }
177
177
  }
178
178
  }
@@ -259,27 +259,36 @@ export default async (flags, log) => {
259
259
  log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`, 1);
260
260
  }
261
261
 
262
- log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`, 2);
262
+ log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`, 3);
263
263
  return newAttempts;
264
264
  };
265
265
 
266
266
  const requestHandler = async (req, res) => {
267
267
  await middlewareRunner.run(req, res, async () => {
268
268
  const requestPath = req.url.split('?')[0];
269
- log(`${req.method} ${requestPath}`, 0);
269
+ log(`${req.method} ${requestPath}`, 4);
270
270
 
271
271
 
272
272
  // Check custom routes first (allow outside rootPath)
273
- log(`customRoutes keys: ${Array.from(customRoutes.keys()).join(', ')}`, 1);
273
+ log(`customRoutes keys: ${Array.from(customRoutes.keys()).join(', ')}`, 4);
274
274
  // Normalize requestPath and keys for matching
275
275
  const normalizePath = p => {
276
- let np = decodeURIComponent(p);
277
- if (!np.startsWith('/')) np = '/' + np;
278
- if (np.length > 1 && np.endsWith('/')) np = np.slice(0, -1);
279
- return np;
276
+ try {
277
+ let np = decodeURIComponent(p);
278
+ if (!np.startsWith('/')) np = '/' + np;
279
+ if (np.length > 1 && np.endsWith('/')) np = np.slice(0, -1);
280
+ return np;
281
+ } catch (e) {
282
+ log(`Warning: Failed to decode URI component "${p}": ${e.message}`, 1);
283
+ // Return the original path if decoding fails
284
+ let np = p;
285
+ if (!np.startsWith('/')) np = '/' + np;
286
+ if (np.length > 1 && np.endsWith('/')) np = np.slice(0, -1);
287
+ return np;
288
+ }
280
289
  };
281
290
  const normalizedRequestPath = normalizePath(requestPath);
282
- log(`Normalized requestPath: ${normalizedRequestPath}`, 1);
291
+ log(`Normalized requestPath: ${normalizedRequestPath}`, 4);
283
292
  let matchedKey = null;
284
293
  for (const key of customRoutes.keys()) {
285
294
  if (normalizePath(key) === normalizedRequestPath) {
@@ -289,14 +298,14 @@ export default async (flags, log) => {
289
298
  }
290
299
  if (matchedKey) {
291
300
  const customFilePath = customRoutes.get(matchedKey);
292
- log(`Serving custom route: ${normalizedRequestPath} -> ${customFilePath}`, 2);
301
+ log(`Serving custom route: ${normalizedRequestPath} -> ${customFilePath}`, 3);
293
302
  try {
294
303
  const { stat } = await import('fs/promises');
295
304
  try {
296
305
  await stat(customFilePath);
297
- log(`Custom route file exists: ${customFilePath}`, 2);
306
+ log(`Custom route file exists: ${customFilePath}`, 4);
298
307
  } catch (e) {
299
- log(`Custom route file does NOT exist: ${customFilePath}`, 0);
308
+ log(`Custom route file does NOT exist: ${customFilePath}`, 1);
300
309
  res.writeHead(404, { 'Content-Type': 'text/plain' });
301
310
  res.end('Custom route file not found');
302
311
  return;
@@ -309,7 +318,7 @@ export default async (flags, log) => {
309
318
  res.end(fileContent);
310
319
  return; // Successfully served custom route
311
320
  } catch (error) {
312
- log(`Error serving custom route ${normalizedRequestPath}: ${error.message}`, 0);
321
+ log(`Error serving custom route ${normalizedRequestPath}: ${error.message}`, 1);
313
322
  res.writeHead(500, { 'Content-Type': 'text/plain' });
314
323
  res.end('Internal Server Error');
315
324
  return;
@@ -320,17 +329,17 @@ export default async (flags, log) => {
320
329
  const wildcardMatch = findWildcardRoute(requestPath);
321
330
  if (wildcardMatch) {
322
331
  const resolvedFilePath = resolveWildcardPath(wildcardMatch.filePath, wildcardMatch.matches);
323
- log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`, 2);
332
+ log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`, 3);
324
333
  try {
325
334
  const fileContent = await readFile(resolvedFilePath);
326
335
  const fileExtension = path.extname(resolvedFilePath).toLowerCase().slice(1);
327
336
  const mimeType = config.allowedMimes[fileExtension] || 'application/octet-stream';
328
- log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`, 2);
337
+ log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`, 4);
329
338
  res.writeHead(200, { 'Content-Type': mimeType });
330
339
  res.end(fileContent);
331
340
  return; // Successfully served wildcard route
332
341
  } catch (error) {
333
- log(`Error serving wildcard route ${requestPath}: ${error.message}`, 0);
342
+ log(`Error serving wildcard route ${requestPath}: ${error.message}`, 1);
334
343
  res.writeHead(500, { 'Content-Type': 'text/plain' });
335
344
  res.end('Internal Server Error');
336
345
  return;
@@ -13,8 +13,8 @@ export default {
13
13
  const fileC = await write(dir, 'src/deep/nested/folder/file.js', 'export const deep = true');
14
14
 
15
15
  // Create files in docs directory that should be ignored when custom routes match
16
- await write(dir, 'docs/src/components/Button.js', 'WRONG FILE');
17
- await write(dir, 'docs/src/utils/helpers/format.js', 'WRONG FILE');
16
+ await write(dir, 'docs/src/components/Button.js', '// WRONG FILE');
17
+ await write(dir, 'docs/src/utils/helpers/format.js', '// WRONG FILE');
18
18
 
19
19
  const prev = process.cwd();
20
20
  process.chdir(dir);
@@ -70,12 +70,12 @@ export default {
70
70
  'single asterisk only matches single path segments': async ({pass, fail, log}) => {
71
71
  try {
72
72
  await withTempDir(async (dir) => {
73
- // Create nested file structure
74
- await write(dir, 'src/file.js', 'single level');
75
- await write(dir, 'src/nested/file.js', 'nested level');
73
+ // Create files at different nesting levels
74
+ await write(dir, 'src/file.js', '// single level');
75
+ await write(dir, 'src/nested/file.js', '// nested level');
76
76
 
77
- // Create fallback file in docs
78
- await write(dir, 'docs/src/nested/file.js', 'fallback file');
77
+ // Create docs directory with fallback file
78
+ await write(dir, 'docs/src/nested/file.js', '// fallback file');
79
79
 
80
80
  const prev = process.cwd();
81
81
  process.chdir(dir);
@@ -103,13 +103,13 @@ export default {
103
103
  const r1 = await httpGet(`http://localhost:${port}/src/file.js`);
104
104
  log('single level status: ' + r1.res.statusCode);
105
105
  if(r1.res.statusCode !== 200) throw new Error('single level 200');
106
- if(r1.body.toString() !== 'single level') throw new Error('single level content');
106
+ if(r1.body.toString() !== '// single level') throw new Error('single level content');
107
107
 
108
108
  // Nested level should NOT work with single asterisk, should fall back to docs
109
109
  const r2 = await httpGet(`http://localhost:${port}/src/nested/file.js`);
110
110
  log('nested level status: ' + r2.res.statusCode);
111
111
  if(r2.res.statusCode !== 200) throw new Error('nested level 200');
112
- if(r2.body.toString() !== 'fallback file') throw new Error('nested level should use fallback');
112
+ if(r2.body.toString() !== '// fallback file') throw new Error('nested level should use fallback');
113
113
 
114
114
  } finally {
115
115
  server.close();
@@ -125,12 +125,12 @@ export default {
125
125
  'wildcard routes take precedence over static files': async ({pass, fail, log}) => {
126
126
  try {
127
127
  await withTempDir(async (dir) => {
128
- // Create source files
129
- await write(dir, 'custom/data.json', '{"source": "custom"}');
130
-
131
- // Create static files in docs that should be overridden
128
+ // Create static file in docs
132
129
  await write(dir, 'docs/api/data.json', '{"source": "static"}');
133
130
 
131
+ // Create custom file outside docs
132
+ await write(dir, 'custom/data.json', '{"source": "custom"}');
133
+
134
134
  const prev = process.cwd();
135
135
  process.chdir(dir);
136
136
 
@@ -174,13 +174,11 @@ export default {
174
174
  'wildcard routes serve nested files from server root': async ({pass, fail, log}) => {
175
175
  try {
176
176
  await withTempDir(async (dir) => {
177
- // Create nested file structure in src directory
178
- const importFile = await write(dir, 'src/components/Import.js', 'export default ImportComponent');
179
- const utilsFile = await write(dir, 'src/utils/helpers.js', 'export const helpers = {}');
180
- const deepFile = await write(dir, 'src/deep/nested/file.js', 'export const nested = true');
181
-
182
- // Create index.html in server root
183
- await write(dir, 'index.html', '<h1>Home</h1>');
177
+ // Create additional test files needed for this specific test
178
+ await write(dir, 'src/components/Import.js', 'export default ImportComponent');
179
+ await write(dir, 'src/utils/helpers.js', 'export const helpers = {}');
180
+ await write(dir, 'src/deep/nested/file.js', 'export const deep = true');
181
+ await write(dir, 'index.html', '<html><body>Test Index</body></html>');
184
182
 
185
183
  const prev = process.cwd();
186
184
  process.chdir(dir);
@@ -220,6 +218,7 @@ export default {
220
218
  const r3 = await httpGet(`http://localhost:${port}/src/deep/nested/file.js`);
221
219
  log('deep nested file status: ' + r3.res.statusCode);
222
220
  if(r3.res.statusCode !== 200) throw new Error(`Expected 200 for deep nested file, got ${r3.res.statusCode}`);
221
+ if(r3.body.toString() !== 'export const deep = true') throw new Error('Deep nested file content mismatch');
223
222
 
224
223
  // Test that index.html still works (non-wildcard route)
225
224
  const r4 = await httpGet(`http://localhost:${port}/index.html`);
@@ -13,8 +13,8 @@ export default {
13
13
  const fileC = await write(dir, 'src/deep/nested/folder/file.js', 'export const deep = true');
14
14
 
15
15
  // Create files in docs directory that should be ignored when custom routes match
16
- await write(dir, 'docs/src/components/Button.js', 'WRONG FILE');
17
- await write(dir, 'docs/src/utils/helpers/format.js', 'WRONG FILE');
16
+ await write(dir, 'docs/src/components/Button.js', '// WRONG FILE');
17
+ await write(dir, 'docs/src/utils/helpers/format.js', '// WRONG FILE');
18
18
 
19
19
  const prev = process.cwd();
20
20
  process.chdir(dir);
@@ -70,12 +70,12 @@ export default {
70
70
  'single asterisk only matches single path segments': async ({pass, fail, log}) => {
71
71
  try {
72
72
  await withTempDir(async (dir) => {
73
- // Create nested file structure
74
- await write(dir, 'src/file.js', 'single level');
75
- await write(dir, 'src/nested/file.js', 'nested level');
73
+ // Create files at different nesting levels
74
+ await write(dir, 'src/file.js', '// single level');
75
+ await write(dir, 'src/nested/file.js', '// nested level');
76
76
 
77
- // Create fallback file in docs
78
- await write(dir, 'docs/src/nested/file.js', 'fallback file');
77
+ // Create docs directory with fallback file
78
+ await write(dir, 'docs/src/nested/file.js', '// fallback file');
79
79
 
80
80
  const prev = process.cwd();
81
81
  process.chdir(dir);
@@ -103,13 +103,13 @@ export default {
103
103
  const r1 = await httpGet(`http://localhost:${port}/src/file.js`);
104
104
  log('single level status: ' + r1.res.statusCode);
105
105
  if(r1.res.statusCode !== 200) throw new Error('single level 200');
106
- if(r1.body.toString() !== 'single level') throw new Error('single level content');
106
+ if(r1.body.toString() !== '// single level') throw new Error('single level content');
107
107
 
108
108
  // Nested level should NOT work with single asterisk, should fall back to docs
109
109
  const r2 = await httpGet(`http://localhost:${port}/src/nested/file.js`);
110
110
  log('nested level status: ' + r2.res.statusCode);
111
111
  if(r2.res.statusCode !== 200) throw new Error('nested level 200');
112
- if(r2.body.toString() !== 'fallback file') throw new Error('nested level should use fallback');
112
+ if(r2.body.toString() !== '// fallback file') throw new Error('nested level should use fallback');
113
113
 
114
114
  } finally {
115
115
  server.close();
@@ -125,12 +125,12 @@ export default {
125
125
  'wildcard routes take precedence over static files': async ({pass, fail, log}) => {
126
126
  try {
127
127
  await withTempDir(async (dir) => {
128
- // Create source files
129
- await write(dir, 'custom/data.json', '{"source": "custom"}');
130
-
131
- // Create static files in docs that should be overridden
128
+ // Create static file in docs
132
129
  await write(dir, 'docs/api/data.json', '{"source": "static"}');
133
130
 
131
+ // Create custom file outside docs
132
+ await write(dir, 'custom/data.json', '{"source": "custom"}');
133
+
134
134
  const prev = process.cwd();
135
135
  process.chdir(dir);
136
136
 
@@ -103,5 +103,51 @@ export default {
103
103
  process.chdir(prev);
104
104
  });
105
105
  pass('custom+wildcard');
106
+ },
107
+
108
+ 'handles malformed URLs gracefully': async ({pass, fail, log}) => {
109
+ await withTestDir(async (dir) => {
110
+ const prev = process.cwd();
111
+ process.chdir(dir);
112
+ const flags = {root: '.', logging: 0, scan: false};
113
+ const logFn = () => {};
114
+ const handler = await router(flags, logFn);
115
+ const server = http.createServer(handler);
116
+ const port = randomPort();
117
+ await new Promise(r => server.listen(port, r));
118
+ await new Promise(r => setTimeout(r, 50));
119
+
120
+ try {
121
+ // Test various malformed URLs that could cause decodeURIComponent to throw
122
+ const malformedUrls = [
123
+ '/test%', // Incomplete percent encoding
124
+ '/test%2', // Incomplete percent encoding
125
+ '/test%G1', // Invalid hex characters
126
+ '/test%ZZ', // Invalid hex characters
127
+ '/test%1G', // Invalid hex characters
128
+ '/%E0%A4%A', // Incomplete UTF-8 sequence
129
+ '/%C0%80' // Overlong UTF-8 encoding
130
+ ];
131
+
132
+ for (const url of malformedUrls) {
133
+ log(`Testing malformed URL: ${url}`);
134
+ const response = await httpGet(`http://localhost:${port}${url}`);
135
+ // Server should handle the request gracefully (return 404 or serve content)
136
+ // and not crash with URIError
137
+ if (response.res.statusCode !== 404 && response.res.statusCode !== 200) {
138
+ throw new Error(`Unexpected status code ${response.res.statusCode} for URL ${url}`);
139
+ }
140
+ log(`URL ${url} handled gracefully with status ${response.res.statusCode}`);
141
+ }
142
+
143
+ server.close();
144
+ process.chdir(prev);
145
+ pass('malformed URLs handled gracefully');
146
+ } catch (e) {
147
+ server.close();
148
+ process.chdir(prev);
149
+ fail(`Error handling malformed URL: ${e.message}`);
150
+ }
151
+ });
106
152
  }
107
153
  };
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "customRoutes": {
3
- "/src/*": "../src/$1"
4
- },
5
- "routePreference": "customRoute"
3
+ "/src/**": "../src/**"
4
+ }
6
5
  }
@@ -1 +1 @@
1
- WRONG FILE
1
+ // WRONG FILE
@@ -1 +1 @@
1
- fallback file
1
+ // fallback file
@@ -1 +1 @@
1
- WRONG FILE
1
+ // WRONG FILE
@@ -0,0 +1 @@
1
+ export const Import = () => 'imported';
@@ -0,0 +1 @@
1
+ export const nested = true
@@ -1 +1 @@
1
- single level
1
+ // single level
@@ -1 +1 @@
1
- nested level
1
+ // nested level
@@ -0,0 +1 @@
1
+ export const helpers = {}
@@ -1,118 +0,0 @@
1
- # Test Server Root
2
-
3
- This directory contains the persistent test server structure used by unit tests instead of creating temporary files for each test run. Tests use `withTestDir()` to get a temporary copy of this structure, maintaining isolation while providing consistent base files.
4
-
5
- ## Directory Structure
6
-
7
- ```
8
- test-server-root/
9
- ├── README.md # This file
10
- ├── .config.json # Basic config with middleware settings
11
- ├── index.html # Basic home page: "<h1>Home</h1>"
12
- ├── late.html # For rescan tests: "later"
13
- ├── hello.html # For CLI tests: "hello world"
14
- ├── a.txt # Test file: "A"
15
- ├── b/
16
- │ └── 1.txt # Test file: "B1"
17
- ├── api/
18
- │ ├── GET.js # Route handler returning {ok:true, params}
19
- │ └── no-default.js # Route file without default export
20
- ├── src/
21
- │ ├── file.js # Content: "single level"
22
- │ ├── file.txt # Content: "custom"
23
- │ ├── nested/
24
- │ │ └── file.js # Content: "nested level"
25
- │ ├── components/
26
- │ │ └── Button.js # Content: "export default Button"
27
- │ ├── utils/
28
- │ │ └── helpers/
29
- │ │ └── format.js # Content: "export const format = () => {}"
30
- │ └── deep/
31
- │ └── nested/
32
- │ └── folder/
33
- │ └── file.js # Content: "export const deep = true"
34
- ├── docs/
35
- │ ├── .config.json # Config with custom routes to ../src/**
36
- │ ├── api/
37
- │ │ └── data.json # Content: {"source": "static"}
38
- │ └── src/
39
- │ ├── nested/
40
- │ │ └── file.js # Content: "fallback file"
41
- │ ├── components/
42
- │ │ └── Button.js # Content: "WRONG FILE"
43
- │ └── utils/
44
- │ └── helpers/
45
- │ └── format.js # Content: "WRONG FILE"
46
- ├── custom/
47
- │ └── data.json # Content: {"source": "custom"}
48
- └── public/
49
- └── src/
50
- └── file.txt # Content: "static"
51
- ```
52
-
53
- ## Configuration Files
54
-
55
- ### Root .config.json
56
- Basic configuration for middleware testing:
57
- ```json
58
- {
59
- "middleware": {
60
- "cors": {}
61
- }
62
- }
63
- ```
64
-
65
- ### docs/.config.json
66
- Configuration for wildcard routing tests:
67
- ```json
68
- {
69
- "customRoutes": {
70
- "/src/*": "../src/$1"
71
- },
72
- "routePreference": "customRoute"
73
- }
74
- ```
75
-
76
- ## Usage in Tests
77
-
78
- ### Simple Tests
79
- Use `withTestDir()` for tests that can leverage the existing structure:
80
-
81
- ```javascript
82
- await withTestDir(async (dir) => {
83
- // dir contains a copy of all the above files
84
- // Use existing files or create additional ones as needed
85
- const handler = await router({root: dir}, log);
86
- // ... test the handler
87
- });
88
- ```
89
-
90
- ### Subdirectory Tests
91
- Use the `subdir` option to work within a specific folder:
92
-
93
- ```javascript
94
- await withTestDir(async (dir) => {
95
- // dir points to test-server-root/docs/
96
- process.chdir(dir);
97
- const handler = await router({root: '.'}, log);
98
- // ... test from docs folder perspective
99
- }, {subdir: 'docs'});
100
- ```
101
-
102
- ### Complex Tests
103
- For tests requiring very specific or complex structures, continue using `withTempDir()`:
104
-
105
- ```javascript
106
- await withTempDir(async (dir) => {
107
- await write(dir, 'very/specific/structure.js', 'content');
108
- // ... custom test setup
109
- });
110
- ```
111
-
112
- ## Benefits
113
-
114
- 1. **Consistency** - All tests start with the same base structure
115
- 2. **Speed** - No need to recreate common files for each test
116
- 3. **Maintainability** - Centralized test file management
117
- 4. **Documentation** - Clear visibility of what test files exist
118
- 5. **Isolation** - Each test still gets its own temporary copy