kempo-server 1.9.13 → 1.9.15
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 +2 -0
- package/dist/index.js +1 -1
- package/dist/router.js +1 -1
- package/docs/getting-started.html +1 -1
- package/package.json +1 -1
- package/src/index.js +0 -2
- package/src/router.js +3 -3
- package/tests/config-flag.node-test.js +8 -14
- package/tests/getFlags.node-test.js +6 -6
- package/tests/mimeTypes-merge.node-test.js +4 -4
- package/tests/router-maxRescanAttempts.node-test.js +14 -10
- package/tests/router-wildcard-double-asterisk.node-test.js +5 -5
- package/tests/router-wildcard.node-test.js +4 -4
- package/tests/router.node-test.js +5 -5
package/README.md
CHANGED
|
@@ -342,6 +342,8 @@ Kempo Server supports several command line options to customize its behavior:
|
|
|
342
342
|
kempo-server --root public --port 8080 --host 0.0.0.0 --verbose
|
|
343
343
|
```
|
|
344
344
|
|
|
345
|
+
**Note:** To enable automatic rescanning for new files during development, set `maxRescanAttempts` in your config file (default is 3). Set to 0 to disable rescanning.
|
|
346
|
+
|
|
345
347
|
## Testing
|
|
346
348
|
|
|
347
349
|
This project uses the Kempo Testing Framework. Tests live in the `tests/` folder and follow these naming conventions:
|
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:"./",
|
|
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:"./",config:".config.json"},{p:"port",l:"logging",r:"root",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&&config.maxRescanAttempts>0&&!shouldSkipRescan(requestPath)){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"))}else 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"))})};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
|
|
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</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
package/src/index.js
CHANGED
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
|
|
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
|
|
379
|
-
if (!served &&
|
|
378
|
+
// If not served and rescanning is enabled (maxRescanAttempts > 0), try rescanning (with blacklist check)
|
|
379
|
+
if (!served && config.maxRescanAttempts > 0 && !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,11 @@ export default {
|
|
|
14
14
|
port: 3000,
|
|
15
15
|
logging: 2,
|
|
16
16
|
root: './',
|
|
17
|
-
scan: false,
|
|
18
17
|
config: '.config.json'
|
|
19
18
|
}, {
|
|
20
19
|
p: 'port',
|
|
21
20
|
l: 'logging',
|
|
22
21
|
r: 'root',
|
|
23
|
-
s: 'scan',
|
|
24
22
|
c: 'config'
|
|
25
23
|
});
|
|
26
24
|
|
|
@@ -43,13 +41,11 @@ export default {
|
|
|
43
41
|
port: 3000,
|
|
44
42
|
logging: 2,
|
|
45
43
|
root: './',
|
|
46
|
-
scan: false,
|
|
47
44
|
config: '.config.json'
|
|
48
45
|
}, {
|
|
49
46
|
p: 'port',
|
|
50
47
|
l: 'logging',
|
|
51
48
|
r: 'root',
|
|
52
|
-
s: 'scan',
|
|
53
49
|
c: 'config'
|
|
54
50
|
});
|
|
55
51
|
|
|
@@ -69,13 +65,11 @@ export default {
|
|
|
69
65
|
port: 3000,
|
|
70
66
|
logging: 2,
|
|
71
67
|
root: './',
|
|
72
|
-
scan: false,
|
|
73
68
|
config: '.config.json'
|
|
74
69
|
}, {
|
|
75
70
|
p: 'port',
|
|
76
71
|
l: 'logging',
|
|
77
72
|
r: 'root',
|
|
78
|
-
s: 'scan',
|
|
79
73
|
c: 'config'
|
|
80
74
|
});
|
|
81
75
|
|
|
@@ -103,7 +97,7 @@ export default {
|
|
|
103
97
|
|
|
104
98
|
const prev = process.cwd();
|
|
105
99
|
process.chdir(dir);
|
|
106
|
-
const flags = {root: '.', logging: 0,
|
|
100
|
+
const flags = {root: '.', logging: 0, rescan: false, config: '.config.json'};
|
|
107
101
|
const logFn = () => {};
|
|
108
102
|
const handler = await router(flags, logFn);
|
|
109
103
|
const server = http.createServer(handler);
|
|
@@ -142,7 +136,7 @@ export default {
|
|
|
142
136
|
|
|
143
137
|
const prev = process.cwd();
|
|
144
138
|
process.chdir(dir);
|
|
145
|
-
const flags = {root: '.', logging: 0,
|
|
139
|
+
const flags = {root: '.', logging: 0, rescan: false, config: 'dev.config.json'};
|
|
146
140
|
const logFn = () => {};
|
|
147
141
|
const handler = await router(flags, logFn);
|
|
148
142
|
const server = http.createServer(handler);
|
|
@@ -182,7 +176,7 @@ export default {
|
|
|
182
176
|
|
|
183
177
|
const prev = process.cwd();
|
|
184
178
|
process.chdir(dir);
|
|
185
|
-
const flags = {root: '.', logging: 0,
|
|
179
|
+
const flags = {root: '.', logging: 0, rescan: false, config: configPath};
|
|
186
180
|
const logFn = () => {};
|
|
187
181
|
const handler = await router(flags, logFn);
|
|
188
182
|
const server = http.createServer(handler);
|
|
@@ -214,7 +208,7 @@ export default {
|
|
|
214
208
|
const prev = process.cwd();
|
|
215
209
|
process.chdir(dir);
|
|
216
210
|
// Point to non-existent config file
|
|
217
|
-
const flags = {root: '.', logging: 0,
|
|
211
|
+
const flags = {root: '.', logging: 0, rescan: false, config: 'nonexistent.config.json'};
|
|
218
212
|
const logFn = () => {};
|
|
219
213
|
const handler = await router(flags, logFn);
|
|
220
214
|
const server = http.createServer(handler);
|
|
@@ -247,7 +241,7 @@ export default {
|
|
|
247
241
|
|
|
248
242
|
const prev = process.cwd();
|
|
249
243
|
process.chdir(dir);
|
|
250
|
-
const flags = {root: '.', logging: 0,
|
|
244
|
+
const flags = {root: '.', logging: 0, rescan: false, config: 'bad.config.json'};
|
|
251
245
|
const logFn = () => {};
|
|
252
246
|
const handler = await router(flags, logFn);
|
|
253
247
|
const server = http.createServer(handler);
|
|
@@ -287,7 +281,7 @@ export default {
|
|
|
287
281
|
|
|
288
282
|
const prev = process.cwd();
|
|
289
283
|
process.chdir(dir);
|
|
290
|
-
const flags = {root: '.', logging: 0,
|
|
284
|
+
const flags = {root: '.', logging: 0, rescan: false, config: 'partial.config.json'};
|
|
291
285
|
const logFn = () => {};
|
|
292
286
|
const handler = await router(flags, logFn);
|
|
293
287
|
const server = http.createServer(handler);
|
|
@@ -336,7 +330,7 @@ export default {
|
|
|
336
330
|
|
|
337
331
|
try {
|
|
338
332
|
// Try to use config file outside server root with relative path
|
|
339
|
-
const flags = {root: '.', logging: 0,
|
|
333
|
+
const flags = {root: '.', logging: 0, rescan: false, config: '../config-outside-root/outside.config.json'};
|
|
340
334
|
|
|
341
335
|
log('Test setup:');
|
|
342
336
|
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', '--
|
|
6
|
-
const flags = getFlags(args, {port: 3000,
|
|
5
|
+
const args = ['--port', '8080', '--verbose'];
|
|
6
|
+
const flags = getFlags(args, {port: 3000, verbose: false});
|
|
7
7
|
|
|
8
8
|
if(flags.port !== '8080') return fail('port not parsed');
|
|
9
|
-
if(flags.
|
|
9
|
+
if(flags.verbose !== true) return fail('verbose 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
|
-
const args = ['-p', '9090', '-
|
|
15
|
-
const flags = getFlags(args, {port: 3000,
|
|
14
|
+
const args = ['-p', '9090', '-v'];
|
|
15
|
+
const flags = getFlags(args, {port: 3000, verbose: false}, {p: 'port', v: 'verbose'});
|
|
16
16
|
|
|
17
17
|
if(flags.port !== '9090') return fail('short mapped value failed');
|
|
18
|
-
if(flags.
|
|
18
|
+
if(flags.verbose !== 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,
|
|
52
|
+
const flags = {root: dir, logging: 0, 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,
|
|
106
|
+
const flags = {root: dir, logging: 0, 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,
|
|
154
|
+
const flags = {root: dir, logging: 0, config: '.config.json'};
|
|
155
155
|
const logFn = () => {};
|
|
156
156
|
const handler = await router(flags, logFn);
|
|
157
157
|
const server = http.createServer(handler);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import http from 'http';
|
|
1
|
+
import http from 'http';
|
|
2
2
|
import {withTestDir} from './utils/test-dir.js';
|
|
3
3
|
import {write} from './utils/file-writer.js';
|
|
4
4
|
import {randomPort} from './utils/port.js';
|
|
@@ -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
|
|
13
|
+
const flags = {root: '.', logging: 0};
|
|
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
|
|
58
|
+
const flags = {root: '.', logging: 0};
|
|
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
|
|
106
|
+
const flags = {root: '.', logging: 0};
|
|
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
|
|
147
|
+
const flags = {root: '.', logging: 0};
|
|
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
|
|
209
|
+
const flags = {root: '.', logging: 0};
|
|
210
210
|
const logFn = () => {};
|
|
211
211
|
|
|
212
212
|
await write(dir, '.config.json', JSON.stringify({
|
|
@@ -242,13 +242,17 @@ export default {
|
|
|
242
242
|
pass('respects noRescanPaths patterns');
|
|
243
243
|
},
|
|
244
244
|
|
|
245
|
-
'
|
|
245
|
+
'maxRescanAttempts set to 0 disables 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
|
|
249
|
+
const flags = {root: '.', logging: 0};
|
|
250
250
|
const logFn = () => {};
|
|
251
251
|
|
|
252
|
+
await write(dir, '.config.json', JSON.stringify({
|
|
253
|
+
maxRescanAttempts: 0
|
|
254
|
+
}));
|
|
255
|
+
|
|
252
256
|
const handler = await router(flags, logFn);
|
|
253
257
|
const server = http.createServer(handler);
|
|
254
258
|
const port = randomPort();
|
|
@@ -268,12 +272,12 @@ export default {
|
|
|
268
272
|
if(stillMiss.res.statusCode !== 404) {
|
|
269
273
|
server.close();
|
|
270
274
|
process.chdir(prev);
|
|
271
|
-
return fail('should not rescan when
|
|
275
|
+
return fail('should not rescan when maxRescanAttempts is 0');
|
|
272
276
|
}
|
|
273
277
|
|
|
274
278
|
server.close();
|
|
275
279
|
process.chdir(prev);
|
|
276
280
|
});
|
|
277
|
-
pass('
|
|
281
|
+
pass('maxRescanAttempts: 0 disables rescan');
|
|
278
282
|
}
|
|
279
283
|
};
|
|
@@ -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
|
|
26
|
+
const flags = {root: 'docs', logging: 0};
|
|
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
|
|
87
|
+
const flags = {root: 'docs', logging: 0};
|
|
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
|
|
141
|
+
const flags = {root: 'docs', logging: 0};
|
|
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
|
|
191
|
+
const flags = {root: '.', logging: 0};
|
|
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
|
|
26
|
+
const flags = {root: 'docs', logging: 0};
|
|
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
|
|
87
|
+
const flags = {root: 'docs', logging: 0};
|
|
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
|
|
141
|
+
const flags = {root: 'docs', logging: 0};
|
|
142
142
|
const logFn = () => {};
|
|
143
143
|
|
|
144
144
|
// Configure wildcard route that overrides static file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import http from 'http';
|
|
1
|
+
import http from 'http';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import {withTestDir} from './utils/test-dir.js';
|
|
4
4
|
import {write} from './utils/file-writer.js';
|
|
@@ -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
|
|
16
|
+
const flags = {root: '.', logging: 0};
|
|
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
|
|
47
|
+
const flags = {root: '.', logging: 0};
|
|
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
|
|
80
|
+
const flags = {root: '.', logging: 0};
|
|
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
|
|
116
|
+
const flags = {root: '.', logging: 0};
|
|
117
117
|
const logFn = () => {};
|
|
118
118
|
const handler = await router(flags, logFn);
|
|
119
119
|
const server = http.createServer(handler);
|