kempo-server 1.9.13 → 1.9.14

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 CHANGED
@@ -336,10 +336,11 @@ Kempo Server supports several command line options to customize its behavior:
336
336
  - `--port <number>` - Set the port number (default: 3000)
337
337
  - `--host <address>` - Set the host address (default: localhost)
338
338
  - `--config <path>` - Set the configuration file path (default: `.config.json`)
339
+ - `--rescan` - Enable automatic rescanning for new files on 404 (useful for development)
339
340
  - `--verbose` - Enable verbose logging
340
341
 
341
342
  ```bash
342
- kempo-server --root public --port 8080 --host 0.0.0.0 --verbose
343
+ kempo-server --root public --port 8080 --host 0.0.0.0 --rescan --verbose
343
344
  ```
344
345
 
345
346
  ## Testing
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import http from"http";import router from"./router.js";import getFlags from"./getFlags.js";const flags=getFlags(process.argv.slice(2),{port:3e3,logging:2,root:"./",scan:!1,config:".config.json"},{p:"port",l:"logging",r:"root",s:"scan",c:"config"});if("string"==typeof flags.logging)switch(flags.logging.toLowerCase()){case"silent":flags.logging=0;break;case"minimal":flags.logging=1;break;case"verbose":flags.logging=3;break;case"debug":flags.logging=4;break;default:flags.logging=2}const log=(message,level=2)=>{level<=flags.logging&&console.log(message)};http.createServer(await router(flags,log)).listen(flags.port),log(`Server started at: http://localhost:${flags.port}`);
2
+ import http from"http";import router from"./router.js";import getFlags from"./getFlags.js";const flags=getFlags(process.argv.slice(2),{port:3e3,logging:2,root:"./",rescan:!1,config:".config.json"},{p:"port",l:"logging",r:"root",s:"rescan",c:"config"});if("string"==typeof flags.logging)switch(flags.logging.toLowerCase()){case"silent":flags.logging=0;break;case"minimal":flags.logging=1;break;case"verbose":flags.logging=3;break;case"debug":flags.logging=4;break;default:flags.logging=2}const log=(message,level=2)=>{level<=flags.logging&&console.log(message)};http.createServer(await router(flags,log)).listen(flags.port),log(`Server started at: http://localhost:${flags.port}`);
package/dist/router.js CHANGED
@@ -1 +1 @@
1
- import path from"path";import{readFile}from"fs/promises";import{pathToFileURL}from"url";import defaultConfig from"./defaultConfig.js";import getFiles from"./getFiles.js";import findFile from"./findFile.js";import serveFile from"./serveFile.js";import MiddlewareRunner from"./middlewareRunner.js";import ModuleCache from"./moduleCache.js";import{corsMiddleware,compressionMiddleware,rateLimitMiddleware,securityMiddleware,loggingMiddleware}from"./builtinMiddleware.js";export default async(flags,log)=>{log("Initializing router",3);const rootPath=path.isAbsolute(flags.root)?flags.root:path.join(process.cwd(),flags.root);log(`Root path: ${rootPath}`,3);let config=defaultConfig;try{const configFileName=flags.config||".config.json",configPath=path.isAbsolute(configFileName)?configFileName:path.join(rootPath,configFileName);if(log(`Config file name: ${configFileName}`,3),log(`Config path: ${configPath}`,3),path.isAbsolute(configFileName))log("Config file name is absolute, skipping validation",4);else{const relativeConfigPath=path.relative(rootPath,configPath);if(log(`Relative config path: ${relativeConfigPath}`,4),log(`Starts with '..': ${relativeConfigPath.startsWith("..")}`,4),relativeConfigPath.startsWith("..")||path.isAbsolute(relativeConfigPath))throw log("Validation failed - throwing error",4),new Error(`Config file must be within the server root directory. Config path: ${configPath}, Root path: ${rootPath}`);log("Validation passed",4)}log(`Loading config from: ${configPath}`,3);const configContent=await readFile(configPath,"utf8"),userConfig=JSON.parse(configContent);config={...defaultConfig,...userConfig,allowedMimes:{...defaultConfig.allowedMimes,...userConfig.allowedMimes||{}},middleware:{...defaultConfig.middleware,...userConfig.middleware||{}},customRoutes:{...defaultConfig.customRoutes,...userConfig.customRoutes||{}},cache:{...defaultConfig.cache,...userConfig.cache||{}}},log("User config loaded and merged with defaults",3)}catch(e){if(e.message.includes("Config file must be within the server root directory"))throw e;log("Using default config (no config file found)",3)}const dis=new Set(config.disallowedRegex);dis.add("^/\\..*"),dis.add("\\.config$"),dis.add("\\.git/"),config.disallowedRegex=[...dis],log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`,3);let files=await getFiles(rootPath,config,log);log(`Initial scan found ${files.length} files`,2);const middlewareRunner=new MiddlewareRunner;if(config.middleware?.cors?.enabled&&(middlewareRunner.use(corsMiddleware(config.middleware.cors)),log("CORS middleware enabled",3)),config.middleware?.compression?.enabled&&(middlewareRunner.use(compressionMiddleware(config.middleware.compression)),log("Compression middleware enabled",3)),config.middleware?.rateLimit?.enabled&&(middlewareRunner.use(rateLimitMiddleware(config.middleware.rateLimit)),log("Rate limit middleware enabled",3)),config.middleware?.security?.enabled&&(middlewareRunner.use(securityMiddleware(config.middleware.security)),log("Security middleware enabled",3)),config.middleware?.logging?.enabled&&(middlewareRunner.use(loggingMiddleware(config.middleware.logging,log)),log("Logging middleware enabled",3)),config.middleware?.custom&&config.middleware.custom.length>0){log(`Loading ${config.middleware.custom.length} custom middleware files`,3);for(const middlewarePath of config.middleware.custom)try{const resolvedPath=path.resolve(middlewarePath),customMiddleware=(await import(pathToFileURL(resolvedPath))).default;"function"==typeof customMiddleware?(middlewareRunner.use(customMiddleware(config.middleware)),log(`Custom middleware loaded: ${middlewarePath}`,3)):log(`Custom middleware error: ${middlewarePath} does not export a default function`,1)}catch(error){log(`Custom middleware error for ${middlewarePath}: ${error.message}`,1)}}let moduleCache=null;config.cache?.enabled&&(moduleCache=new ModuleCache(config.cache),log(`Module cache initialized: ${config.cache.maxSize} max modules, ${config.cache.maxMemoryMB}MB limit, ${config.cache.ttlMs}ms TTL`,2));const customRoutes=new Map,wildcardRoutes=new Map;if(config.customRoutes&&Object.keys(config.customRoutes).length>0){log(`Processing ${Object.keys(config.customRoutes).length} custom routes`,3);for(const[urlPath,filePath]of Object.entries(config.customRoutes))if(urlPath.includes("*")){const resolvedPath=path.isAbsolute(filePath)?filePath:path.resolve(rootPath,filePath);wildcardRoutes.set(urlPath,resolvedPath),log(`Wildcard route mapped: ${urlPath} -> ${resolvedPath}`,3)}else{const resolvedPath=path.isAbsolute(filePath)?filePath:path.resolve(rootPath,filePath);customRoutes.set(urlPath,resolvedPath),log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`,3)}}const matchWildcardRoute=(requestPath,pattern)=>{const regexPattern=pattern.replace(/\*\*/g,"(.+)").replace(/\*/g,"([^/]+)");return new RegExp(`^${regexPattern}$`).exec(requestPath)},rescanAttempts=new Map,dynamicNoRescanPaths=new Set,shouldSkipRescan=requestPath=>config.noRescanPaths.some(pattern=>new RegExp(pattern).test(requestPath))?(log(`Skipping rescan for configured pattern: ${requestPath}`,3),!0):!!dynamicNoRescanPaths.has(requestPath)&&(log(`Skipping rescan for dynamically blacklisted path: ${requestPath}`,3),!0),handler=async(req,res)=>{await middlewareRunner.run(req,res,async()=>{const requestPath=req.url.split("?")[0];log(`${req.method} ${requestPath}`,4),log(`customRoutes keys: ${Array.from(customRoutes.keys()).join(", ")}`,4);const normalizePath=p=>{try{let np=decodeURIComponent(p);return np.startsWith("/")||(np="/"+np),np.length>1&&np.endsWith("/")&&(np=np.slice(0,-1)),np}catch(e){log(`Warning: Failed to decode URI component "${p}": ${e.message}`,1);let np=p;return np.startsWith("/")||(np="/"+np),np.length>1&&np.endsWith("/")&&(np=np.slice(0,-1)),np}},normalizedRequestPath=normalizePath(requestPath);log(`Normalized requestPath: ${normalizedRequestPath}`,4);let matchedKey=null;for(const key of customRoutes.keys())if(normalizePath(key)===normalizedRequestPath){matchedKey=key;break}if(matchedKey){const customFilePath=customRoutes.get(matchedKey);log(`Serving custom route: ${normalizedRequestPath} -> ${customFilePath}`,3);try{const{stat:stat}=await import("fs/promises");try{await stat(customFilePath),log(`Custom route file exists: ${customFilePath}`,4)}catch(e){return log(`Custom route file does NOT exist: ${customFilePath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),void res.end("Custom route file not found")}const fileExtension=path.extname(customFilePath).toLowerCase().slice(1),mimeConfig=config.allowedMimes[fileExtension];let mimeType,encoding;"string"==typeof mimeConfig?(mimeType=mimeConfig,encoding=mimeType.startsWith("text/")?"utf8":void 0):(mimeType=mimeConfig?.mime||"application/octet-stream",encoding="utf8"===mimeConfig?.encoding?"utf8":void 0);const fileContent=await readFile(customFilePath,encoding);log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`,2);const contentType="utf8"===encoding&&mimeType.startsWith("text/")?`${mimeType}; charset=utf-8`:mimeType;return res.writeHead(200,{"Content-Type":contentType}),void res.end(fileContent)}catch(error){return log(`Error serving custom route ${normalizedRequestPath}: ${error.message}`,1),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Internal Server Error")}}const wildcardMatch=(requestPath=>{for(const[pattern,filePath]of wildcardRoutes){const matches=matchWildcardRoute(requestPath,pattern);if(matches)return{filePath:filePath,matches:matches}}return null})(requestPath);if(wildcardMatch){const resolvedFilePath=((filePath,matches)=>{let resolvedPath=filePath,matchIndex=1;for(;resolvedPath.includes("**")&&matchIndex<matches.length;)resolvedPath=resolvedPath.replace("**",matches[matchIndex]),matchIndex++;for(;resolvedPath.includes("*")&&matchIndex<matches.length;)resolvedPath=resolvedPath.replace("*",matches[matchIndex]),matchIndex++;return path.isAbsolute(resolvedPath)?resolvedPath:path.resolve(rootPath,resolvedPath)})(wildcardMatch.filePath,wildcardMatch.matches);log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`,3);try{const fileExtension=path.extname(resolvedFilePath).toLowerCase().slice(1),mimeConfig=config.allowedMimes[fileExtension];let mimeType,encoding;"string"==typeof mimeConfig?(mimeType=mimeConfig,encoding=mimeType.startsWith("text/")?"utf8":void 0):(mimeType=mimeConfig?.mime||"application/octet-stream",encoding="utf8"===mimeConfig?.encoding?"utf8":void 0);const fileContent=await readFile(resolvedFilePath,encoding);log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`,4);const contentType="utf8"===encoding&&mimeType.startsWith("text/")?`${mimeType}; charset=utf-8`:mimeType;return res.writeHead(200,{"Content-Type":contentType}),void res.end(fileContent)}catch(error){return log(`Error serving wildcard route ${requestPath}: ${error.message}`,1),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Internal Server Error")}}const served=await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache);if(served||!flags.scan||shouldSkipRescan(requestPath))served||(shouldSkipRescan(requestPath)?log(`404 - Skipped rescan for: ${requestPath}`,2):log(`404 - File not found: ${requestPath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),res.end("Not Found"));else{log("File not found, rescanning directory...",1),files=await getFiles(rootPath,config,log),log(`Rescan found ${files.length} files`,2);await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache)?rescanAttempts.delete(requestPath):((requestPath=>{const newAttempts=(rescanAttempts.get(requestPath)||0)+1;rescanAttempts.set(requestPath,newAttempts),newAttempts>=config.maxRescanAttempts&&(dynamicNoRescanPaths.add(requestPath),log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`,1)),log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`,3)})(requestPath),log(`404 - File not found after rescan: ${requestPath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),res.end("Not Found"))}})};return handler.moduleCache=moduleCache,handler.getStats=()=>moduleCache?.getStats()||null,handler.logCacheStats=()=>moduleCache?.logStats(log),handler.clearCache=()=>moduleCache?.clear(),handler};
1
+ import path from"path";import{readFile}from"fs/promises";import{pathToFileURL}from"url";import defaultConfig from"./defaultConfig.js";import getFiles from"./getFiles.js";import findFile from"./findFile.js";import serveFile from"./serveFile.js";import MiddlewareRunner from"./middlewareRunner.js";import ModuleCache from"./moduleCache.js";import{corsMiddleware,compressionMiddleware,rateLimitMiddleware,securityMiddleware,loggingMiddleware}from"./builtinMiddleware.js";export default async(flags,log)=>{log("Initializing router",3);const rootPath=path.isAbsolute(flags.root)?flags.root:path.join(process.cwd(),flags.root);log(`Root path: ${rootPath}`,3);let config=defaultConfig;try{const configFileName=flags.config||".config.json",configPath=path.isAbsolute(configFileName)?configFileName:path.join(rootPath,configFileName);if(log(`Config file name: ${configFileName}`,3),log(`Config path: ${configPath}`,3),path.isAbsolute(configFileName))log("Config file name is absolute, skipping validation",4);else{const relativeConfigPath=path.relative(rootPath,configPath);if(log(`Relative config path: ${relativeConfigPath}`,4),log(`Starts with '..': ${relativeConfigPath.startsWith("..")}`,4),relativeConfigPath.startsWith("..")||path.isAbsolute(relativeConfigPath))throw log("Validation failed - throwing error",4),new Error(`Config file must be within the server root directory. Config path: ${configPath}, Root path: ${rootPath}`);log("Validation passed",4)}log(`Loading config from: ${configPath}`,3);const configContent=await readFile(configPath,"utf8"),userConfig=JSON.parse(configContent);config={...defaultConfig,...userConfig,allowedMimes:{...defaultConfig.allowedMimes,...userConfig.allowedMimes||{}},middleware:{...defaultConfig.middleware,...userConfig.middleware||{}},customRoutes:{...defaultConfig.customRoutes,...userConfig.customRoutes||{}},cache:{...defaultConfig.cache,...userConfig.cache||{}}},log("User config loaded and merged with defaults",3)}catch(e){if(e.message.includes("Config file must be within the server root directory"))throw e;log("Using default config (no config file found)",3)}const dis=new Set(config.disallowedRegex);dis.add("^/\\..*"),dis.add("\\.config$"),dis.add("\\.git/"),config.disallowedRegex=[...dis],log(`Config loaded with ${config.disallowedRegex.length} disallowed patterns`,3);let files=await getFiles(rootPath,config,log);log(`Initial scan found ${files.length} files`,2);const middlewareRunner=new MiddlewareRunner;if(config.middleware?.cors?.enabled&&(middlewareRunner.use(corsMiddleware(config.middleware.cors)),log("CORS middleware enabled",3)),config.middleware?.compression?.enabled&&(middlewareRunner.use(compressionMiddleware(config.middleware.compression)),log("Compression middleware enabled",3)),config.middleware?.rateLimit?.enabled&&(middlewareRunner.use(rateLimitMiddleware(config.middleware.rateLimit)),log("Rate limit middleware enabled",3)),config.middleware?.security?.enabled&&(middlewareRunner.use(securityMiddleware(config.middleware.security)),log("Security middleware enabled",3)),config.middleware?.logging?.enabled&&(middlewareRunner.use(loggingMiddleware(config.middleware.logging,log)),log("Logging middleware enabled",3)),config.middleware?.custom&&config.middleware.custom.length>0){log(`Loading ${config.middleware.custom.length} custom middleware files`,3);for(const middlewarePath of config.middleware.custom)try{const resolvedPath=path.resolve(middlewarePath),customMiddleware=(await import(pathToFileURL(resolvedPath))).default;"function"==typeof customMiddleware?(middlewareRunner.use(customMiddleware(config.middleware)),log(`Custom middleware loaded: ${middlewarePath}`,3)):log(`Custom middleware error: ${middlewarePath} does not export a default function`,1)}catch(error){log(`Custom middleware error for ${middlewarePath}: ${error.message}`,1)}}let moduleCache=null;config.cache?.enabled&&(moduleCache=new ModuleCache(config.cache),log(`Module cache initialized: ${config.cache.maxSize} max modules, ${config.cache.maxMemoryMB}MB limit, ${config.cache.ttlMs}ms TTL`,2));const customRoutes=new Map,wildcardRoutes=new Map;if(config.customRoutes&&Object.keys(config.customRoutes).length>0){log(`Processing ${Object.keys(config.customRoutes).length} custom routes`,3);for(const[urlPath,filePath]of Object.entries(config.customRoutes))if(urlPath.includes("*")){const resolvedPath=path.isAbsolute(filePath)?filePath:path.resolve(rootPath,filePath);wildcardRoutes.set(urlPath,resolvedPath),log(`Wildcard route mapped: ${urlPath} -> ${resolvedPath}`,3)}else{const resolvedPath=path.isAbsolute(filePath)?filePath:path.resolve(rootPath,filePath);customRoutes.set(urlPath,resolvedPath),log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`,3)}}const matchWildcardRoute=(requestPath,pattern)=>{const regexPattern=pattern.replace(/\*\*/g,"(.+)").replace(/\*/g,"([^/]+)");return new RegExp(`^${regexPattern}$`).exec(requestPath)},rescanAttempts=new Map,dynamicNoRescanPaths=new Set,shouldSkipRescan=requestPath=>config.noRescanPaths.some(pattern=>new RegExp(pattern).test(requestPath))?(log(`Skipping rescan for configured pattern: ${requestPath}`,3),!0):!!dynamicNoRescanPaths.has(requestPath)&&(log(`Skipping rescan for dynamically blacklisted path: ${requestPath}`,3),!0),handler=async(req,res)=>{await middlewareRunner.run(req,res,async()=>{const requestPath=req.url.split("?")[0];log(`${req.method} ${requestPath}`,4),log(`customRoutes keys: ${Array.from(customRoutes.keys()).join(", ")}`,4);const normalizePath=p=>{try{let np=decodeURIComponent(p);return np.startsWith("/")||(np="/"+np),np.length>1&&np.endsWith("/")&&(np=np.slice(0,-1)),np}catch(e){log(`Warning: Failed to decode URI component "${p}": ${e.message}`,1);let np=p;return np.startsWith("/")||(np="/"+np),np.length>1&&np.endsWith("/")&&(np=np.slice(0,-1)),np}},normalizedRequestPath=normalizePath(requestPath);log(`Normalized requestPath: ${normalizedRequestPath}`,4);let matchedKey=null;for(const key of customRoutes.keys())if(normalizePath(key)===normalizedRequestPath){matchedKey=key;break}if(matchedKey){const customFilePath=customRoutes.get(matchedKey);log(`Serving custom route: ${normalizedRequestPath} -> ${customFilePath}`,3);try{const{stat:stat}=await import("fs/promises");try{await stat(customFilePath),log(`Custom route file exists: ${customFilePath}`,4)}catch(e){return log(`Custom route file does NOT exist: ${customFilePath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),void res.end("Custom route file not found")}const fileExtension=path.extname(customFilePath).toLowerCase().slice(1),mimeConfig=config.allowedMimes[fileExtension];let mimeType,encoding;"string"==typeof mimeConfig?(mimeType=mimeConfig,encoding=mimeType.startsWith("text/")?"utf8":void 0):(mimeType=mimeConfig?.mime||"application/octet-stream",encoding="utf8"===mimeConfig?.encoding?"utf8":void 0);const fileContent=await readFile(customFilePath,encoding);log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`,2);const contentType="utf8"===encoding&&mimeType.startsWith("text/")?`${mimeType}; charset=utf-8`:mimeType;return res.writeHead(200,{"Content-Type":contentType}),void res.end(fileContent)}catch(error){return log(`Error serving custom route ${normalizedRequestPath}: ${error.message}`,1),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Internal Server Error")}}const wildcardMatch=(requestPath=>{for(const[pattern,filePath]of wildcardRoutes){const matches=matchWildcardRoute(requestPath,pattern);if(matches)return{filePath:filePath,matches:matches}}return null})(requestPath);if(wildcardMatch){const resolvedFilePath=((filePath,matches)=>{let resolvedPath=filePath,matchIndex=1;for(;resolvedPath.includes("**")&&matchIndex<matches.length;)resolvedPath=resolvedPath.replace("**",matches[matchIndex]),matchIndex++;for(;resolvedPath.includes("*")&&matchIndex<matches.length;)resolvedPath=resolvedPath.replace("*",matches[matchIndex]),matchIndex++;return path.isAbsolute(resolvedPath)?resolvedPath:path.resolve(rootPath,resolvedPath)})(wildcardMatch.filePath,wildcardMatch.matches);log(`Serving wildcard route: ${requestPath} -> ${resolvedFilePath}`,3);try{const fileExtension=path.extname(resolvedFilePath).toLowerCase().slice(1),mimeConfig=config.allowedMimes[fileExtension];let mimeType,encoding;"string"==typeof mimeConfig?(mimeType=mimeConfig,encoding=mimeType.startsWith("text/")?"utf8":void 0):(mimeType=mimeConfig?.mime||"application/octet-stream",encoding="utf8"===mimeConfig?.encoding?"utf8":void 0);const fileContent=await readFile(resolvedFilePath,encoding);log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`,4);const contentType="utf8"===encoding&&mimeType.startsWith("text/")?`${mimeType}; charset=utf-8`:mimeType;return res.writeHead(200,{"Content-Type":contentType}),void res.end(fileContent)}catch(error){return log(`Error serving wildcard route ${requestPath}: ${error.message}`,1),res.writeHead(500,{"Content-Type":"text/plain"}),void res.end("Internal Server Error")}}const served=await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache);if(served||!flags.rescan||shouldSkipRescan(requestPath))served||(shouldSkipRescan(requestPath)?log(`404 - Skipped rescan for: ${requestPath}`,2):log(`404 - File not found: ${requestPath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),res.end("Not Found"));else{log("File not found, rescanning directory...",1),files=await getFiles(rootPath,config,log),log(`Rescan found ${files.length} files`,2);await serveFile(files,rootPath,requestPath,req.method,config,req,res,log,moduleCache)?rescanAttempts.delete(requestPath):((requestPath=>{const newAttempts=(rescanAttempts.get(requestPath)||0)+1;rescanAttempts.set(requestPath,newAttempts),newAttempts>config.maxRescanAttempts&&(dynamicNoRescanPaths.add(requestPath),log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`,1)),log(`Rescan attempt ${newAttempts}/${config.maxRescanAttempts} for: ${requestPath}`,3)})(requestPath),log(`404 - File not found after rescan: ${requestPath}`,1),res.writeHead(404,{"Content-Type":"text/plain"}),res.end("Not Found"))}})};return handler.moduleCache=moduleCache,handler.getStats=()=>moduleCache?.getStats()||null,handler.logCacheStats=()=>moduleCache?.logStats(log),handler.clearCache=()=>moduleCache?.clear(),handler};
@@ -60,7 +60,7 @@
60
60
 
61
61
  <h3>Configuration File Examples</h3>
62
62
  <p>You can specify different configuration files for different environments:</p>
63
- <pre><code class="hljs bash"># Development<br />kempo-server --root public --config dev.config.json<br /><br /># Staging<br />kempo-server --root public --config staging.config.json<br /><br /># Production with absolute path<br />kempo-server --root public --config /etc/kempo/production.config.json<br /><br /># Mix with other options<br />kempo-server --root dist --port 8080 --config production.config.json --scan</code></pre>
63
+ <pre><code class="hljs bash"># Development<br />kempo-server --root public --config dev.config.json<br /><br /># Staging<br />kempo-server --root public --config staging.config.json<br /><br /># Production with absolute path<br />kempo-server --root public --config /etc/kempo/production.config.json<br /><br /># Mix with other options<br />kempo-server --root dist --port 8080 --config production.config.json --rescan</code></pre>
64
64
 
65
65
  <h2>What's Next?</h2>
66
66
  <p>Now that you have Kempo Server running, explore these topics:</p>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kempo-server",
3
3
  "type": "module",
4
- "version": "1.9.13",
4
+ "version": "1.9.14",
5
5
  "description": "A lightweight, zero-dependency, file based routing server.",
6
6
  "main": "dist/index.js",
7
7
  "exports": {
package/src/index.js CHANGED
@@ -7,13 +7,13 @@ const flags = getFlags(process.argv.slice(2), {
7
7
  port: 3000,
8
8
  logging: 2,
9
9
  root: './',
10
- scan: false,
10
+ rescan: false,
11
11
  config: '.config.json'
12
12
  }, {
13
13
  p: 'port',
14
14
  l: 'logging',
15
15
  r: 'root',
16
- s: 'scan',
16
+ s: 'rescan',
17
17
  c: 'config'
18
18
  });
19
19
 
package/src/router.js CHANGED
@@ -254,7 +254,7 @@ export default async (flags, log) => {
254
254
  const newAttempts = currentAttempts + 1;
255
255
  rescanAttempts.set(requestPath, newAttempts);
256
256
 
257
- if (newAttempts >= config.maxRescanAttempts) {
257
+ if (newAttempts > config.maxRescanAttempts) {
258
258
  dynamicNoRescanPaths.add(requestPath);
259
259
  log(`Path ${requestPath} added to dynamic blacklist after ${newAttempts} failed attempts`, 1);
260
260
  }
@@ -375,8 +375,8 @@ export default async (flags, log) => {
375
375
  // Try to serve the file normally
376
376
  const served = await serveFile(files, rootPath, requestPath, req.method, config, req, res, log, moduleCache);
377
377
 
378
- // If not served and scan flag is enabled, try rescanning once (with blacklist check)
379
- if (!served && flags.scan && !shouldSkipRescan(requestPath)) {
378
+ // If not served and rescan flag is enabled, try rescanning once (with blacklist check)
379
+ if (!served && flags.rescan && !shouldSkipRescan(requestPath)) {
380
380
  log('File not found, rescanning directory...', 1);
381
381
  files = await getFiles(rootPath, config, log);
382
382
  log(`Rescan found ${files.length} files`, 2);
@@ -1,4 +1,4 @@
1
- import http from 'http';
1
+ import http from 'http';
2
2
  import path from 'path';
3
3
  import {withTempDir} from './utils/temp-dir.js';
4
4
  import {write} from './utils/file-writer.js';
@@ -14,13 +14,13 @@ export default {
14
14
  port: 3000,
15
15
  logging: 2,
16
16
  root: './',
17
- scan: false,
17
+ rescan: false,
18
18
  config: '.config.json'
19
19
  }, {
20
20
  p: 'port',
21
21
  l: 'logging',
22
22
  r: 'root',
23
- s: 'scan',
23
+ s: 'rescan',
24
24
  c: 'config'
25
25
  });
26
26
 
@@ -43,13 +43,13 @@ export default {
43
43
  port: 3000,
44
44
  logging: 2,
45
45
  root: './',
46
- scan: false,
46
+ rescan: false,
47
47
  config: '.config.json'
48
48
  }, {
49
49
  p: 'port',
50
50
  l: 'logging',
51
51
  r: 'root',
52
- s: 'scan',
52
+ s: 'rescan',
53
53
  c: 'config'
54
54
  });
55
55
 
@@ -69,13 +69,13 @@ export default {
69
69
  port: 3000,
70
70
  logging: 2,
71
71
  root: './',
72
- scan: false,
72
+ rescan: false,
73
73
  config: '.config.json'
74
74
  }, {
75
75
  p: 'port',
76
76
  l: 'logging',
77
77
  r: 'root',
78
- s: 'scan',
78
+ s: 'rescan',
79
79
  c: 'config'
80
80
  });
81
81
 
@@ -103,7 +103,7 @@ export default {
103
103
 
104
104
  const prev = process.cwd();
105
105
  process.chdir(dir);
106
- const flags = {root: '.', logging: 0, scan: false, config: '.config.json'};
106
+ const flags = {root: '.', logging: 0, rescan: false, config: '.config.json'};
107
107
  const logFn = () => {};
108
108
  const handler = await router(flags, logFn);
109
109
  const server = http.createServer(handler);
@@ -142,7 +142,7 @@ export default {
142
142
 
143
143
  const prev = process.cwd();
144
144
  process.chdir(dir);
145
- const flags = {root: '.', logging: 0, scan: false, config: 'dev.config.json'};
145
+ const flags = {root: '.', logging: 0, rescan: false, config: 'dev.config.json'};
146
146
  const logFn = () => {};
147
147
  const handler = await router(flags, logFn);
148
148
  const server = http.createServer(handler);
@@ -182,7 +182,7 @@ export default {
182
182
 
183
183
  const prev = process.cwd();
184
184
  process.chdir(dir);
185
- const flags = {root: '.', logging: 0, scan: false, config: configPath};
185
+ const flags = {root: '.', logging: 0, rescan: false, config: configPath};
186
186
  const logFn = () => {};
187
187
  const handler = await router(flags, logFn);
188
188
  const server = http.createServer(handler);
@@ -214,7 +214,7 @@ export default {
214
214
  const prev = process.cwd();
215
215
  process.chdir(dir);
216
216
  // Point to non-existent config file
217
- const flags = {root: '.', logging: 0, scan: false, config: 'nonexistent.config.json'};
217
+ const flags = {root: '.', logging: 0, rescan: false, config: 'nonexistent.config.json'};
218
218
  const logFn = () => {};
219
219
  const handler = await router(flags, logFn);
220
220
  const server = http.createServer(handler);
@@ -247,7 +247,7 @@ export default {
247
247
 
248
248
  const prev = process.cwd();
249
249
  process.chdir(dir);
250
- const flags = {root: '.', logging: 0, scan: false, config: 'bad.config.json'};
250
+ const flags = {root: '.', logging: 0, rescan: false, config: 'bad.config.json'};
251
251
  const logFn = () => {};
252
252
  const handler = await router(flags, logFn);
253
253
  const server = http.createServer(handler);
@@ -287,7 +287,7 @@ export default {
287
287
 
288
288
  const prev = process.cwd();
289
289
  process.chdir(dir);
290
- const flags = {root: '.', logging: 0, scan: false, config: 'partial.config.json'};
290
+ const flags = {root: '.', logging: 0, rescan: false, config: 'partial.config.json'};
291
291
  const logFn = () => {};
292
292
  const handler = await router(flags, logFn);
293
293
  const server = http.createServer(handler);
@@ -336,7 +336,7 @@ export default {
336
336
 
337
337
  try {
338
338
  // Try to use config file outside server root with relative path
339
- const flags = {root: '.', logging: 0, scan: false, config: '../config-outside-root/outside.config.json'};
339
+ const flags = {root: '.', logging: 0, rescan: false, config: '../config-outside-root/outside.config.json'};
340
340
 
341
341
  log('Test setup:');
342
342
  log('dir: ' + dir);
@@ -2,20 +2,20 @@ import getFlags from '../src/getFlags.js';
2
2
 
3
3
  export default {
4
4
  'parses long flags with values and booleans': async ({pass, fail}) => {
5
- const args = ['--port', '8080', '--scan'];
6
- const flags = getFlags(args, {port: 3000, scan: false});
5
+ const args = ['--port', '8080', '--rescan'];
6
+ const flags = getFlags(args, {port: 3000, rescan: false});
7
7
 
8
8
  if(flags.port !== '8080') return fail('port not parsed');
9
- if(flags.scan !== true) return fail('scan boolean not parsed');
9
+ if(flags.rescan !== true) return fail('rescan boolean not parsed');
10
10
 
11
11
  pass('parsed long flags');
12
12
  },
13
13
  'parses short flags using map and preserves defaults': async ({pass, fail}) => {
14
14
  const args = ['-p', '9090', '-s'];
15
- const flags = getFlags(args, {port: 3000, scan: false}, {p: 'port', s: 'scan'});
15
+ const flags = getFlags(args, {port: 3000, rescan: false}, {p: 'port', s: 'rescan'});
16
16
 
17
17
  if(flags.port !== '9090') return fail('short mapped value failed');
18
- if(flags.scan !== true) return fail('short mapped boolean failed');
18
+ if(flags.rescan !== true) return fail('short mapped boolean failed');
19
19
 
20
20
  pass('short flags parsed');
21
21
  },
@@ -1,4 +1,4 @@
1
- import defaultConfig from '../src/defaultConfig.js';
1
+ import defaultConfig from '../src/defaultConfig.js';
2
2
  import router from '../src/router.js';
3
3
  import http from 'http';
4
4
  import { mkdtemp, writeFile, rm } from 'fs/promises';
@@ -49,7 +49,7 @@ export default {
49
49
  if(mergedConfig.js.mime !== 'text/javascript' || mergedConfig.js.encoding !== 'utf8') return fail('js mime/encoding merge failed');
50
50
  if(mergedConfig.css.mime !== 'text/css' || mergedConfig.css.encoding !== 'utf8') return fail('css mime/encoding merge failed');
51
51
 
52
- const flags = {root: dir, logging: 0, scan: false, config: '.config.json'};
52
+ const flags = {root: dir, logging: 0, rescan: false, config: '.config.json'};
53
53
  const logFn = () => {};
54
54
  const handler = await router(flags, logFn);
55
55
  const server = http.createServer(handler);
@@ -103,7 +103,7 @@ export default {
103
103
  await write(dir, 'test.js', 'console.log("test");');
104
104
  await write(dir, 'test.custom', 'custom content');
105
105
 
106
- const flags = {root: dir, logging: 0, scan: false, config: '.config.json'};
106
+ const flags = {root: dir, logging: 0, rescan: false, config: '.config.json'};
107
107
  const logFn = () => {};
108
108
  const handler = await router(flags, logFn);
109
109
  const server = http.createServer(handler);
@@ -151,7 +151,7 @@ export default {
151
151
  await write(dir, 'test.png', 'fake png');
152
152
  await write(dir, 'test.custom', 'custom');
153
153
 
154
- const flags = {root: dir, logging: 0, scan: false, config: '.config.json'};
154
+ const flags = {root: dir, logging: 0, rescan: false, config: '.config.json'};
155
155
  const logFn = () => {};
156
156
  const handler = await router(flags, logFn);
157
157
  const server = http.createServer(handler);
@@ -10,7 +10,7 @@ export default {
10
10
  await withTestDir(async (dir) => {
11
11
  const prev = process.cwd();
12
12
  process.chdir(dir);
13
- const flags = {root: '.', logging: 0, scan: true};
13
+ const flags = {root: '.', logging: 0, rescan: true};
14
14
  const logFn = () => {};
15
15
 
16
16
  await write(dir, '.config.json', JSON.stringify({
@@ -55,7 +55,7 @@ export default {
55
55
  await withTestDir(async (dir) => {
56
56
  const prev = process.cwd();
57
57
  process.chdir(dir);
58
- const flags = {root: '.', logging: 0, scan: true};
58
+ const flags = {root: '.', logging: 0, rescan: true};
59
59
  const logFn = () => {};
60
60
 
61
61
  await write(dir, '.config.json', JSON.stringify({
@@ -103,7 +103,7 @@ export default {
103
103
  await withTestDir(async (dir) => {
104
104
  const prev = process.cwd();
105
105
  process.chdir(dir);
106
- const flags = {root: '.', logging: 0, scan: true};
106
+ const flags = {root: '.', logging: 0, rescan: true};
107
107
  const logFn = () => {};
108
108
 
109
109
  await write(dir, '.config.json', JSON.stringify({
@@ -144,7 +144,7 @@ export default {
144
144
  await withTestDir(async (dir) => {
145
145
  const prev = process.cwd();
146
146
  process.chdir(dir);
147
- const flags = {root: '.', logging: 0, scan: true};
147
+ const flags = {root: '.', logging: 0, rescan: true};
148
148
  const logFn = () => {};
149
149
 
150
150
  await write(dir, '.config.json', JSON.stringify({
@@ -206,7 +206,7 @@ export default {
206
206
  await withTestDir(async (dir) => {
207
207
  const prev = process.cwd();
208
208
  process.chdir(dir);
209
- const flags = {root: '.', logging: 0, scan: true};
209
+ const flags = {root: '.', logging: 0, rescan: true};
210
210
  const logFn = () => {};
211
211
 
212
212
  await write(dir, '.config.json', JSON.stringify({
@@ -242,11 +242,11 @@ export default {
242
242
  pass('respects noRescanPaths patterns');
243
243
  },
244
244
 
245
- 'scan flag disabled prevents rescanning': async ({pass, fail}) => {
245
+ 'rescan flag disabled prevents rescanning': async ({pass, fail}) => {
246
246
  await withTestDir(async (dir) => {
247
247
  const prev = process.cwd();
248
248
  process.chdir(dir);
249
- const flags = {root: '.', logging: 0, scan: false};
249
+ const flags = {root: '.', logging: 0, rescan: false};
250
250
  const logFn = () => {};
251
251
 
252
252
  const handler = await router(flags, logFn);
@@ -268,12 +268,12 @@ export default {
268
268
  if(stillMiss.res.statusCode !== 404) {
269
269
  server.close();
270
270
  process.chdir(prev);
271
- return fail('should not rescan when scan flag is disabled');
271
+ return fail('should not rescan when rescan flag is disabled');
272
272
  }
273
273
 
274
274
  server.close();
275
275
  process.chdir(prev);
276
276
  });
277
- pass('scan disabled prevents rescan');
277
+ pass('rescan disabled prevents rescan');
278
278
  }
279
279
  };
@@ -1,4 +1,4 @@
1
- import http from 'http';
1
+ import http from 'http';
2
2
  import path from 'path';
3
3
  import {withTempDir} from './utils/temp-dir.js';
4
4
  import {write} from './utils/file-writer.js';
@@ -23,7 +23,7 @@ export default {
23
23
  const prev = process.cwd();
24
24
  process.chdir(dir);
25
25
 
26
- const flags = {root: 'docs', logging: 0, scan: false};
26
+ const flags = {root: 'docs', logging: 0, rescan: false};
27
27
  const logFn = () => {};
28
28
 
29
29
  // Configure double asterisk wildcard route
@@ -84,7 +84,7 @@ export default {
84
84
  const prev = process.cwd();
85
85
  process.chdir(dir);
86
86
 
87
- const flags = {root: 'docs', logging: 0, scan: false};
87
+ const flags = {root: 'docs', logging: 0, rescan: false};
88
88
  const logFn = () => {};
89
89
 
90
90
  // Configure single asterisk wildcard route
@@ -138,7 +138,7 @@ export default {
138
138
  const prev = process.cwd();
139
139
  process.chdir(dir);
140
140
 
141
- const flags = {root: 'docs', logging: 0, scan: false};
141
+ const flags = {root: 'docs', logging: 0, rescan: false};
142
142
  const logFn = () => {};
143
143
 
144
144
  // Configure wildcard route that overrides static file
@@ -188,7 +188,7 @@ export default {
188
188
  process.chdir(dir);
189
189
 
190
190
  // Server root is the current directory (dir)
191
- const flags = {root: '.', logging: 0, scan: false};
191
+ const flags = {root: '.', logging: 0, rescan: false};
192
192
  const logFn = () => {};
193
193
 
194
194
  // Configure wildcard route to serve from ./src/**
@@ -1,4 +1,4 @@
1
- import http from 'http';
1
+ import http from 'http';
2
2
  import path from 'path';
3
3
  import {withTempDir} from './utils/temp-dir.js';
4
4
  import {write} from './utils/file-writer.js';
@@ -23,7 +23,7 @@ export default {
23
23
  const prev = process.cwd();
24
24
  process.chdir(dir);
25
25
 
26
- const flags = {root: 'docs', logging: 0, scan: false};
26
+ const flags = {root: 'docs', logging: 0, rescan: false};
27
27
  const logFn = () => {};
28
28
 
29
29
  // Configure double asterisk wildcard route
@@ -84,7 +84,7 @@ export default {
84
84
  const prev = process.cwd();
85
85
  process.chdir(dir);
86
86
 
87
- const flags = {root: 'docs', logging: 0, scan: false};
87
+ const flags = {root: 'docs', logging: 0, rescan: false};
88
88
  const logFn = () => {};
89
89
 
90
90
  // Configure single asterisk wildcard route
@@ -138,7 +138,7 @@ export default {
138
138
  const prev = process.cwd();
139
139
  process.chdir(dir);
140
140
 
141
- const flags = {root: 'docs', logging: 0, scan: false};
141
+ const flags = {root: 'docs', logging: 0, rescan: false};
142
142
  const logFn = () => {};
143
143
 
144
144
  // Configure wildcard route that overrides static file
@@ -13,7 +13,7 @@ export default {
13
13
  await withTestDir(async (dir) => {
14
14
  const prev = process.cwd();
15
15
  process.chdir(dir);
16
- const flags = {root: '.', logging: 0, scan: false};
16
+ const flags = {root: '.', logging: 0, rescan: false};
17
17
  const logFn = () => {};
18
18
  const handler = await router(flags, logFn);
19
19
  const server = http.createServer(handler);
@@ -44,7 +44,7 @@ export default {
44
44
  await withTestDir(async (dir) => {
45
45
  const prev = process.cwd();
46
46
  process.chdir(dir);
47
- const flags = {root: '.', logging: 0, scan: true};
47
+ const flags = {root: '.', logging: 0, rescan: true};
48
48
  const handler = await router(flags, log);
49
49
  const server = http.createServer(handler);
50
50
  const port = randomPort();
@@ -77,7 +77,7 @@ export default {
77
77
  const fileB = path.join(dir, 'b/1.txt');
78
78
  const prev = process.cwd();
79
79
  process.chdir(dir);
80
- const flags = {root: '.', logging: 0, scan: false};
80
+ const flags = {root: '.', logging: 0, rescan: false};
81
81
  const logFn = () => {};
82
82
  // write config before init
83
83
  await write(dir, '.config.json', JSON.stringify({
@@ -113,7 +113,7 @@ export default {
113
113
  await withTestDir(async (dir) => {
114
114
  const prev = process.cwd();
115
115
  process.chdir(dir);
116
- const flags = {root: '.', logging: 0, scan: false};
116
+ const flags = {root: '.', logging: 0, rescan: false};
117
117
  const logFn = () => {};
118
118
  const handler = await router(flags, logFn);
119
119
  const server = http.createServer(handler);