kempo-server 1.7.3 → 1.7.4
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/dist/defaultConfig.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default{allowedMimes:{html:"text/html",htm:"text/html",shtml:"text/html",css:"text/css",xml:"text/xml",gif:"image/gif",jpeg:"image/jpeg",jpg:"image/jpeg",js:"application/javascript",mjs:"application/javascript",json:"application/json",webp:"image/webp",png:"image/png",svg:"image/svg+xml",svgz:"image/svg+xml",ico:"image/x-icon",webm:"video/webm",mp4:"video/mp4",m4v:"video/mp4",ogv:"video/ogg",mp3:"audio/mpeg",ogg:"audio/ogg",wav:"audio/wav",woff:"font/woff",woff2:"font/woff2",ttf:"font/ttf",otf:"font/otf",eot:"application/vnd.ms-fontobject",pdf:"application/pdf",txt:"text/plain",webmanifest:"application/manifest+json",md:"text/markdown",csv:"text/csv",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",avif:"image/avif",wasm:"application/wasm"},disallowedRegex:["^/\\..*","\\.config$","\\.env$","\\.git/","\\.htaccess$","\\.htpasswd$","^/node_modules/","^/vendor/","\\.log$","\\.bak$","\\.sql$","\\.ini$","password","config\\.php$","wp-config\\.php$","\\.DS_Store$"],routeFiles:["GET.js","POST.js","PUT.js","DELETE.js","HEAD.js","OPTIONS.js","PATCH.js","CONNECT.js","TRACE.js","index.js"],noRescanPaths:["^\\.well-known/","/favicon\\.ico$","/robots\\.txt$","/sitemap\\.xml$","/apple-touch-icon","/android-chrome-","/browserconfig\\.xml$","/manifest\\.json$","\\.map$","/__webpack_hmr$","/hot-update\\.","/sockjs-node/"],maxRescanAttempts:3,customRoutes:{},middleware:{cors:{enabled:!1,origin:"*",methods:["GET","POST","PUT","DELETE","OPTIONS"],headers:["Content-Type","Authorization"]},compression:{enabled:!1,threshold:1024},rateLimit:{enabled:!1,maxRequests:100,windowMs:6e4,message:"Too many requests"},security:{enabled:!0,headers:{"X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","X-XSS-Protection":"1; mode=block"}},logging:{enabled:!0,includeUserAgent:!1,includeResponseTime:!0},custom:[]}};
|
|
1
|
+
export default{allowedMimes:{html:"text/html",htm:"text/html",shtml:"text/html",css:"text/css",xml:"text/xml",gif:"image/gif",jpeg:"image/jpeg",jpg:"image/jpeg",js:"application/javascript",mjs:"application/javascript",json:"application/json",webp:"image/webp",png:"image/png",svg:"image/svg+xml",svgz:"image/svg+xml",ico:"image/x-icon",webm:"video/webm",mp4:"video/mp4",m4v:"video/mp4",ogv:"video/ogg",mp3:"audio/mpeg",ogg:"audio/ogg",wav:"audio/wav",woff:"font/woff",woff2:"font/woff2",ttf:"font/ttf",otf:"font/otf",eot:"application/vnd.ms-fontobject",pdf:"application/pdf",txt:"text/plain",webmanifest:"application/manifest+json",md:"text/markdown",csv:"text/csv",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",xls:"application/vnd.ms-excel",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",ppt:"application/vnd.ms-powerpoint",pptx:"application/vnd.openxmlformats-officedocument.presentationml.presentation",avif:"image/avif",wasm:"application/wasm"},disallowedRegex:["^/\\..*","\\.config$","\\.env$","\\.git/","\\.htaccess$","\\.htpasswd$","^/node_modules/","^/vendor/","\\.log$","\\.bak$","\\.sql$","\\.ini$","password","config\\.php$","wp-config\\.php$","\\.DS_Store$"],routeFiles:["GET.js","POST.js","PUT.js","DELETE.js","HEAD.js","OPTIONS.js","PATCH.js","CONNECT.js","TRACE.js","index.js"],noRescanPaths:["^\\.well-known/","/favicon\\.ico$","/robots\\.txt$","/sitemap\\.xml$","/apple-touch-icon","/android-chrome-","/browserconfig\\.xml$","/manifest\\.json$","\\.map$","/__webpack_hmr$","/hot-update\\.","/sockjs-node/"],maxRescanAttempts:3,customRoutes:{},middleware:{cors:{enabled:!1,origin:"*",methods:["GET","POST","PUT","DELETE","OPTIONS"],headers:["Content-Type","Authorization"]},compression:{enabled:!1,threshold:1024},rateLimit:{enabled:!1,maxRequests:100,windowMs:6e4,message:"Too many requests"},security:{enabled:!0,headers:{"X-Content-Type-Options":"nosniff","X-Frame-Options":"DENY","X-XSS-Protection":"1; mode=block"}},logging:{enabled:!0,includeUserAgent:!1,includeResponseTime:!0},custom:[]},cache:{enabled:!1,maxSize:100,maxMemoryMB:50,ttlMs:3e5,maxHeapUsagePercent:70,memoryCheckInterval:3e4,watchFiles:!0,enableMemoryMonitoring:!0}};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{watch}from"fs";import path from"path";export default class ModuleCache{constructor(config={}){this.cache=new Map,this.watchers=new Map,this.maxSize=config.maxSize||100,this.maxMemoryMB=config.maxMemoryMB||50,this.ttlMs=config.ttlMs||3e5,this.maxHeapUsagePercent=config.maxHeapUsagePercent||70,this.memoryCheckInterval=config.memoryCheckInterval||3e4,this.watchFiles=!1!==config.watchFiles,this.currentMemoryMB=0,this.stats={hits:0,misses:0,evictions:0,fileChanges:0},!1!==config.enableMemoryMonitoring&&this.startMemoryMonitoring()}startMemoryMonitoring(){this.memoryTimer=setInterval(()=>{const usage=process.memoryUsage(),heapPercent=usage.heapUsed/usage.heapTotal*100;if(heapPercent>this.maxHeapUsagePercent){const clearedCount=this.clearExpiredEntries();for(this.stats.evictions+=clearedCount;this.cache.size>0&&heapPercent>this.maxHeapUsagePercent;)this.evictOldest()}},this.memoryCheckInterval)}destroy(){this.memoryTimer&&clearInterval(this.memoryTimer);for(const watcher of this.watchers.values())watcher.close();this.watchers.clear(),this.cache.clear()}get(filePath,stats){const entry=this.cache.get(filePath);if(!entry)return this.stats.misses++,null;return Date.now()-entry.timestamp>this.ttlMs||entry.mtime<stats.mtime?(this.delete(filePath),this.stats.misses++,null):(this.cache.delete(filePath),this.cache.set(filePath,entry),this.stats.hits++,entry.module)}set(filePath,module,stats,estimatedSizeKB=1){const sizeInMB=estimatedSizeKB/1024;for(;this.cache.size>=this.maxSize||this.currentMemoryMB+sizeInMB>this.maxMemoryMB;)this.evictOldest();const entry={module:module,mtime:stats.mtime,timestamp:Date.now(),sizeKB:estimatedSizeKB,filePath:filePath};this.cache.set(filePath,entry),this.currentMemoryMB+=sizeInMB,this.watchFiles&&this.setupFileWatcher(filePath)}delete(filePath){const entry=this.cache.get(filePath);if(entry){this.cache.delete(filePath),this.currentMemoryMB-=entry.sizeKB/1024;const watcher=this.watchers.get(filePath);return watcher&&(watcher.close(),this.watchers.delete(filePath)),!0}return!1}clear(){const size=this.cache.size;this.cache.clear(),this.currentMemoryMB=0;for(const watcher of this.watchers.values())watcher.close();this.watchers.clear(),this.stats.evictions+=size}evictOldest(){if(0===this.cache.size)return;const[oldestKey,oldestEntry]=this.cache.entries().next().value;this.delete(oldestKey),this.stats.evictions++}clearExpiredEntries(){const now=Date.now();let clearedCount=0;for(const[filePath,entry]of this.cache.entries())now-entry.timestamp>this.ttlMs&&(this.delete(filePath),clearedCount++);return clearedCount}setupFileWatcher(filePath){if(!this.watchers.has(filePath))try{const watcher=watch(filePath,eventType=>{"change"===eventType&&(this.delete(filePath),this.stats.fileChanges++)});watcher.on("error",error=>{this.delete(filePath)}),this.watchers.set(filePath,watcher)}catch(error){console.warn(`Could not watch file ${filePath}: ${error.message}`)}}getStats(){const memoryUsage=process.memoryUsage();return{cache:{size:this.cache.size,maxSize:this.maxSize,memoryUsageMB:Math.round(100*this.currentMemoryMB)/100,maxMemoryMB:this.maxMemoryMB,watchersActive:this.watchers.size},stats:{...this.stats},memory:{heapUsedMB:Math.round(memoryUsage.heapUsed/1024/1024*100)/100,heapTotalMB:Math.round(memoryUsage.heapTotal/1024/1024*100)/100,heapUsagePercent:Math.round(memoryUsage.heapUsed/memoryUsage.heapTotal*100),rssMB:Math.round(memoryUsage.rss/1024/1024*100)/100},config:{ttlMs:this.ttlMs,maxHeapUsagePercent:this.maxHeapUsagePercent,watchFiles:this.watchFiles}}}getHitRate(){const total=this.stats.hits+this.stats.misses;return 0===total?0:Math.round(this.stats.hits/total*100)}logStats(log){const stats=this.getStats();log(`Cache Stats: ${stats.cache.size}/${stats.cache.maxSize} entries, ${stats.cache.memoryUsageMB}/${stats.cache.maxMemoryMB}MB, ${this.getHitRate()}% hit rate`,2)}getCachedFiles(){return Array.from(this.cache.keys()).map(filePath=>({path:filePath,relativePath:path.relative(process.cwd(),filePath),age:Date.now()-this.cache.get(filePath).timestamp,sizeKB:this.cache.get(filePath).sizeKB}))}}
|
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{corsMiddleware,compressionMiddleware,rateLimitMiddleware,securityMiddleware,loggingMiddleware}from"./builtinMiddleware.js";export default async(flags,log)=>{log("Initializing router",2);const rootPath=path.join(process.cwd(),flags.root);log(`Root path: ${rootPath}`,2);let config=defaultConfig;try{const configFileName=flags.config||".config.json",configPath=path.isAbsolute(configFileName)?configFileName:path.join(rootPath,configFileName);log(`Loading config from: ${configPath}`,2);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}},log("User config loaded and merged with defaults",2)}catch(e){log("Using default config (no config file found)",2)}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`,2);let files=await getFiles(rootPath,config,log);log(`Initial scan found ${files.length} files`,1);const middlewareRunner=new MiddlewareRunner;if(config.middleware?.cors?.enabled&&(middlewareRunner.use(corsMiddleware(config.middleware.cors)),log("CORS middleware enabled",2)),config.middleware?.compression?.enabled&&(middlewareRunner.use(compressionMiddleware(config.middleware.compression)),log("Compression middleware enabled",2)),config.middleware?.rateLimit?.enabled&&(middlewareRunner.use(rateLimitMiddleware(config.middleware.rateLimit)),log("Rate limit middleware enabled",2)),config.middleware?.security?.enabled&&(middlewareRunner.use(securityMiddleware(config.middleware.security)),log("Security middleware enabled",2)),config.middleware?.logging?.enabled&&(middlewareRunner.use(loggingMiddleware(config.middleware.logging,log)),log("Logging middleware enabled",2)),config.middleware?.custom&&config.middleware.custom.length>0){log(`Loading ${config.middleware.custom.length} custom middleware files`,2);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}`,2)):log(`Custom middleware error: ${middlewarePath} does not export a default function`,1)}catch(error){log(`Custom middleware error for ${middlewarePath}: ${error.message}`,1)}}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`,2);for(const[urlPath,filePath]of Object.entries(config.customRoutes))
|
|
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",2);const rootPath=path.isAbsolute(flags.root)?flags.root:path.join(process.cwd(),flags.root);log(`Root path: ${rootPath}`,2);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}`,2),log(`Config path: ${configPath}`,2),path.isAbsolute(configFileName))log("Config file name is absolute, skipping validation",2);else{const relativeConfigPath=path.relative(rootPath,configPath);if(log(`Relative config path: ${relativeConfigPath}`,2),log(`Starts with '..': ${relativeConfigPath.startsWith("..")}`,2),relativeConfigPath.startsWith("..")||path.isAbsolute(relativeConfigPath))throw log("Validation failed - throwing error",2),new Error(`Config file must be within the server root directory. Config path: ${configPath}, Root path: ${rootPath}`);log("Validation passed",2)}log(`Loading config from: ${configPath}`,2);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",2)}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)",2)}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`,2);let files=await getFiles(rootPath,config,log);log(`Initial scan found ${files.length} files`,1);const middlewareRunner=new MiddlewareRunner;if(config.middleware?.cors?.enabled&&(middlewareRunner.use(corsMiddleware(config.middleware.cors)),log("CORS middleware enabled",2)),config.middleware?.compression?.enabled&&(middlewareRunner.use(compressionMiddleware(config.middleware.compression)),log("Compression middleware enabled",2)),config.middleware?.rateLimit?.enabled&&(middlewareRunner.use(rateLimitMiddleware(config.middleware.rateLimit)),log("Rate limit middleware enabled",2)),config.middleware?.security?.enabled&&(middlewareRunner.use(securityMiddleware(config.middleware.security)),log("Security middleware enabled",2)),config.middleware?.logging?.enabled&&(middlewareRunner.use(loggingMiddleware(config.middleware.logging,log)),log("Logging middleware enabled",2)),config.middleware?.custom&&config.middleware.custom.length>0){log(`Loading ${config.middleware.custom.length} custom middleware files`,2);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}`,2)):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`,2);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}`,2)}else{const resolvedPath=path.isAbsolute(filePath)?filePath:path.resolve(rootPath,filePath);customRoutes.set(urlPath,resolvedPath),log(`Custom route mapped: ${urlPath} -> ${resolvedPath}`,2)}}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}`,0),log(`customRoutes keys: ${Array.from(customRoutes.keys()).join(", ")}`,1);const normalizePath=p=>{let np=decodeURIComponent(p);return np.startsWith("/")||(np="/"+np),np.length>1&&np.endsWith("/")&&(np=np.slice(0,-1)),np},normalizedRequestPath=normalizePath(requestPath);log(`Normalized requestPath: ${normalizedRequestPath}`,1);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}`,2);try{const{stat:stat}=await import("fs/promises");try{await stat(customFilePath),log(`Custom route file exists: ${customFilePath}`,2)}catch(e){return log(`Custom route file does NOT exist: ${customFilePath}`,0),res.writeHead(404,{"Content-Type":"text/plain"}),void res.end("Custom route file not found")}const fileContent=await readFile(customFilePath),fileExtension=path.extname(customFilePath).toLowerCase().slice(1),mimeType=config.allowedMimes[fileExtension]||"application/octet-stream";return log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`,2),res.writeHead(200,{"Content-Type":mimeType}),void res.end(fileContent)}catch(error){return log(`Error serving custom route ${normalizedRequestPath}: ${error.message}`,0),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}`,2);try{const fileContent=await readFile(resolvedFilePath),fileExtension=path.extname(resolvedFilePath).toLowerCase().slice(1),mimeType=config.allowedMimes[fileExtension]||"application/octet-stream";return log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`,2),res.writeHead(200,{"Content-Type":mimeType}),void res.end(fileContent)}catch(error){return log(`Error serving wildcard route ${requestPath}: ${error.message}`,0),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{(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}`,2)})(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)||(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};
|
package/dist/serveFile.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import path from"path";import{readFile}from"fs/promises";import{pathToFileURL}from"url";import findFile from"./findFile.js";import createRequestWrapper from"./requestWrapper.js";import createResponseWrapper from"./responseWrapper.js";export default async(files,rootPath,requestPath,method,config,req,res,log)=>{log(`Attempting to serve: ${requestPath}`,3);const[file,params]=await findFile(files,rootPath,requestPath,method,log);if(!file)return log(`No file found for: ${requestPath}`,3),!1;const fileName=path.basename(file);if(log(`Found file: ${file}`,2),config.routeFiles.includes(fileName)){log(`Executing route file: ${fileName}`,2);try{const fileUrl=pathToFileURL(file).href
|
|
1
|
+
import path from"path";import{readFile,stat}from"fs/promises";import{pathToFileURL}from"url";import findFile from"./findFile.js";import createRequestWrapper from"./requestWrapper.js";import createResponseWrapper from"./responseWrapper.js";export default async(files,rootPath,requestPath,method,config,req,res,log,moduleCache=null)=>{log(`Attempting to serve: ${requestPath}`,3);const[file,params]=await findFile(files,rootPath,requestPath,method,log);if(!file)return log(`No file found for: ${requestPath}`,3),!1;const fileName=path.basename(file);if(log(`Found file: ${file}`,2),config.routeFiles.includes(fileName)){log(`Executing route file: ${fileName}`,2);try{let module;if(moduleCache&&config.cache?.enabled){const fileStats=await stat(file);if(module=moduleCache.get(file,fileStats),module)log(`Using cached module: ${fileName}`,3);else{const fileUrl=pathToFileURL(file).href+`?t=${Date.now()}`;log(`Loading module from: ${fileUrl}`,3),module=await import(fileUrl);const estimatedSizeKB=fileStats.size/1024;moduleCache.set(file,module,fileStats,estimatedSizeKB),log(`Cached module: ${fileName} (${estimatedSizeKB.toFixed(1)}KB)`,3)}}else{const fileUrl=pathToFileURL(file).href+`?t=${Date.now()}`;log(`Loading module from: ${fileUrl}`,3),module=await import(fileUrl)}if("function"==typeof module.default){log(`Executing route function with params: ${JSON.stringify(params)}`,3);const enhancedRequest=createRequestWrapper(req,params),enhancedResponse=createResponseWrapper(res);return moduleCache&&(enhancedRequest._kempoCache=moduleCache),await module.default(enhancedRequest,enhancedResponse),log(`Route executed successfully: ${fileName}`,2),!0}return log(`Route file does not export a function: ${fileName}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),res.end("Route file does not export a function"),!0}catch(error){return log(`Error loading route file ${fileName}: ${error.message}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),res.end("Internal Server Error"),!0}}else{log(`Serving static file: ${fileName}`,2);try{const fileContent=await readFile(file),fileExtension=path.extname(file).toLowerCase().slice(1),mimeType=config.allowedMimes[fileExtension]||"application/octet-stream";return log(`Serving ${file} as ${mimeType} (${fileContent.length} bytes)`,2),res.writeHead(200,{"Content-Type":mimeType}),res.end(fileContent),!0}catch(error){return log(`Error reading file ${file}: ${error.message}`,0),res.writeHead(500,{"Content-Type":"text/plain"}),res.end("Internal Server Error"),!0}}};
|
package/package.json
CHANGED
|
@@ -169,5 +169,71 @@ export default {
|
|
|
169
169
|
} catch(e){
|
|
170
170
|
fail(e.message);
|
|
171
171
|
}
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
'wildcard routes serve nested files from server root': async ({pass, fail, log}) => {
|
|
175
|
+
try {
|
|
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>');
|
|
184
|
+
|
|
185
|
+
const prev = process.cwd();
|
|
186
|
+
process.chdir(dir);
|
|
187
|
+
|
|
188
|
+
// Server root is the current directory (dir)
|
|
189
|
+
const flags = {root: '.', logging: 0, scan: false};
|
|
190
|
+
const logFn = () => {};
|
|
191
|
+
|
|
192
|
+
// Configure wildcard route to serve from ./src/**
|
|
193
|
+
const config = {
|
|
194
|
+
customRoutes: {
|
|
195
|
+
'/src/**': './src/**'
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
await write(dir, '.config.json', JSON.stringify(config));
|
|
200
|
+
const handler = await router(flags, logFn);
|
|
201
|
+
const server = http.createServer(handler);
|
|
202
|
+
const port = randomPort();
|
|
203
|
+
|
|
204
|
+
await new Promise(r => server.listen(port, r));
|
|
205
|
+
await new Promise(r => setTimeout(r, 50));
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
// Test that /src/components/Import.js is served correctly
|
|
209
|
+
const r1 = await httpGet(`http://localhost:${port}/src/components/Import.js`);
|
|
210
|
+
log('Import.js status: ' + r1.res.statusCode);
|
|
211
|
+
if(r1.res.statusCode !== 200) throw new Error(`Expected 200, got ${r1.res.statusCode}`);
|
|
212
|
+
if(r1.body.toString() !== 'export default ImportComponent') throw new Error('Import.js content mismatch');
|
|
213
|
+
|
|
214
|
+
// Test deeper nested file
|
|
215
|
+
const r2 = await httpGet(`http://localhost:${port}/src/utils/helpers.js`);
|
|
216
|
+
log('helpers.js status: ' + r2.res.statusCode);
|
|
217
|
+
if(r2.res.statusCode !== 200) throw new Error(`Expected 200 for helpers.js, got ${r2.res.statusCode}`);
|
|
218
|
+
|
|
219
|
+
// Test very deeply nested file
|
|
220
|
+
const r3 = await httpGet(`http://localhost:${port}/src/deep/nested/file.js`);
|
|
221
|
+
log('deep nested file status: ' + r3.res.statusCode);
|
|
222
|
+
if(r3.res.statusCode !== 200) throw new Error(`Expected 200 for deep nested file, got ${r3.res.statusCode}`);
|
|
223
|
+
|
|
224
|
+
// Test that index.html still works (non-wildcard route)
|
|
225
|
+
const r4 = await httpGet(`http://localhost:${port}/index.html`);
|
|
226
|
+
log('index.html status: ' + r4.res.statusCode);
|
|
227
|
+
if(r4.res.statusCode !== 200) throw new Error(`Expected 200 for index.html, got ${r4.res.statusCode}`);
|
|
228
|
+
|
|
229
|
+
} finally {
|
|
230
|
+
server.close();
|
|
231
|
+
process.chdir(prev);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
pass('wildcard routes serve nested files from server root');
|
|
235
|
+
} catch(e){
|
|
236
|
+
fail(e.message);
|
|
237
|
+
}
|
|
172
238
|
}
|
|
173
239
|
};
|