kempo-server 1.8.1 → 1.8.3
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/responseWrapper.js +1 -1
- package/dist/router.js +1 -1
- package/package.json +1 -1
- package/src/responseWrapper.js +16 -4
- package/src/router.js +4 -4
- package/tests/responseWrapper.node-test.js +3 -3
package/dist/responseWrapper.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export function createResponseWrapper(response){let sent=!1;const enhancedResponse={...response,status(code){if(sent)throw new Error("Cannot set status after response has been sent");return response.statusCode=code,enhancedResponse},set(field,value){if(sent)throw new Error("Cannot set headers after response has been sent");return"object"==typeof field?Object.entries(field).forEach(([key,val])=>{response.setHeader(key,val)}):response.setHeader(field,value),enhancedResponse},get:field=>response.getHeader(field),type(contentType){if(sent)throw new Error("Cannot set content type after response has been sent");const mimeType={html:"text/html",json:"application/json",xml:"application/xml",text:"text/plain",css:"text/css",js:"application/javascript"}[contentType]||contentType;return response.setHeader("Content-Type",mimeType),enhancedResponse},json(obj){if(sent)throw new Error("Cannot send response after it has already been sent");sent=!0,response.setHeader("Content-Type","application/json");try{const jsonString=JSON.stringify(obj);response.end(jsonString)}catch(error){throw new Error("Failed to stringify object to JSON")}return enhancedResponse},send(data){if(sent)throw new Error("Cannot send response after it has already been sent");
|
|
1
|
+
export function createResponseWrapper(response){let sent=!1;const enhancedResponse={...response,status(code){if(sent)throw new Error("Cannot set status after response has been sent");return response.statusCode=code,enhancedResponse},set(field,value){if(sent)throw new Error("Cannot set headers after response has been sent");return"object"==typeof field?Object.entries(field).forEach(([key,val])=>{response.setHeader(key,val)}):response.setHeader(field,value),enhancedResponse},get:field=>response.getHeader(field),type(contentType){if(sent)throw new Error("Cannot set content type after response has been sent");const mimeType={html:"text/html",json:"application/json",xml:"application/xml",text:"text/plain",css:"text/css",js:"application/javascript"}[contentType]||contentType;return response.setHeader("Content-Type",mimeType),enhancedResponse},json(obj){if(sent)throw new Error("Cannot send response after it has already been sent");sent=!0,response.setHeader("Content-Type","application/json");try{const jsonString=JSON.stringify(obj);response.end(jsonString)}catch(error){throw new Error("Failed to stringify object to JSON")}return enhancedResponse},send(data){if(sent)throw new Error("Cannot send response after it has already been sent");if(sent=!0,null==data)return response.end(),enhancedResponse;if("object"==typeof data)response.setHeader("Content-Type","application/json"),response.end(JSON.stringify(data));else if("string"==typeof data){if(response.getHeader("Content-Type")){const contentType=response.getHeader("Content-Type");contentType&&contentType.startsWith("text/")&&!contentType.includes("charset=")&&response.setHeader("Content-Type",`${contentType}; charset=utf-8`)}else response.setHeader("Content-Type","text/html; charset=utf-8");response.end(data)}else if(Buffer.isBuffer(data))response.end(data);else{if(response.getHeader("Content-Type")){const contentType=response.getHeader("Content-Type");contentType&&contentType.startsWith("text/")&&!contentType.includes("charset=")&&response.setHeader("Content-Type",`${contentType}; charset=utf-8`)}else response.setHeader("Content-Type","text/plain; charset=utf-8");response.end(String(data))}return enhancedResponse},html(htmlString){if(sent)throw new Error("Cannot send response after it has already been sent");return sent=!0,response.setHeader("Content-Type","text/html; charset=utf-8"),response.end(htmlString),enhancedResponse},text(textString){if(sent)throw new Error("Cannot send response after it has already been sent");return sent=!0,response.setHeader("Content-Type","text/plain; charset=utf-8"),response.end(String(textString)),enhancedResponse},redirect(url,statusCode=302){if(sent)throw new Error("Cannot redirect after response has been sent");return sent=!0,response.statusCode=statusCode,response.setHeader("Location",url),response.end(),enhancedResponse},cookie(name,value,options={}){if(sent)throw new Error("Cannot set cookies after response has been sent");let cookieString=`${name}=${encodeURIComponent(value)}`;options.maxAge&&(cookieString+=`; Max-Age=${options.maxAge}`),options.domain&&(cookieString+=`; Domain=${options.domain}`),options.path&&(cookieString+=`; Path=${options.path}`),options.secure&&(cookieString+="; Secure"),options.httpOnly&&(cookieString+="; HttpOnly"),options.sameSite&&(cookieString+=`; SameSite=${options.sameSite}`);const existingCookies=response.getHeader("Set-Cookie")||[],cookies=Array.isArray(existingCookies)?existingCookies:[existingCookies];return cookies.push(cookieString),response.setHeader("Set-Cookie",cookies),enhancedResponse},clearCookie(name,options={}){return this.cookie(name,"",{...options,maxAge:0})}};return enhancedResponse}export default createResponseWrapper;
|
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{(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("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};
|
|
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{(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("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/package.json
CHANGED
package/src/responseWrapper.js
CHANGED
|
@@ -103,7 +103,13 @@ export function createResponseWrapper(response) {
|
|
|
103
103
|
} else if (typeof data === 'string') {
|
|
104
104
|
// If Content-Type not set, default to text/html for strings
|
|
105
105
|
if (!response.getHeader('Content-Type')) {
|
|
106
|
-
response.setHeader('Content-Type', 'text/html');
|
|
106
|
+
response.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
107
|
+
} else {
|
|
108
|
+
// If Content-Type is set and is a text type, add charset if not present
|
|
109
|
+
const contentType = response.getHeader('Content-Type');
|
|
110
|
+
if (contentType && contentType.startsWith('text/') && !contentType.includes('charset=')) {
|
|
111
|
+
response.setHeader('Content-Type', `${contentType}; charset=utf-8`);
|
|
112
|
+
}
|
|
107
113
|
}
|
|
108
114
|
response.end(data);
|
|
109
115
|
} else if (Buffer.isBuffer(data)) {
|
|
@@ -112,7 +118,13 @@ export function createResponseWrapper(response) {
|
|
|
112
118
|
} else {
|
|
113
119
|
// Convert to string
|
|
114
120
|
if (!response.getHeader('Content-Type')) {
|
|
115
|
-
response.setHeader('Content-Type', 'text/plain');
|
|
121
|
+
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
122
|
+
} else {
|
|
123
|
+
// If Content-Type is set and is a text type, add charset if not present
|
|
124
|
+
const contentType = response.getHeader('Content-Type');
|
|
125
|
+
if (contentType && contentType.startsWith('text/') && !contentType.includes('charset=')) {
|
|
126
|
+
response.setHeader('Content-Type', `${contentType}; charset=utf-8`);
|
|
127
|
+
}
|
|
116
128
|
}
|
|
117
129
|
response.end(String(data));
|
|
118
130
|
}
|
|
@@ -127,7 +139,7 @@ export function createResponseWrapper(response) {
|
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
sent = true;
|
|
130
|
-
response.setHeader('Content-Type', 'text/html');
|
|
142
|
+
response.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
131
143
|
response.end(htmlString);
|
|
132
144
|
return enhancedResponse;
|
|
133
145
|
},
|
|
@@ -139,7 +151,7 @@ export function createResponseWrapper(response) {
|
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
sent = true;
|
|
142
|
-
response.setHeader('Content-Type', 'text/plain');
|
|
154
|
+
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
143
155
|
response.end(String(textString));
|
|
144
156
|
return enhancedResponse;
|
|
145
157
|
},
|
package/src/router.js
CHANGED
|
@@ -55,19 +55,19 @@ export default async (flags, log) => {
|
|
|
55
55
|
// Deep merge nested objects
|
|
56
56
|
allowedMimes: {
|
|
57
57
|
...defaultConfig.allowedMimes,
|
|
58
|
-
...userConfig.allowedMimes
|
|
58
|
+
...(userConfig.allowedMimes || {})
|
|
59
59
|
},
|
|
60
60
|
middleware: {
|
|
61
61
|
...defaultConfig.middleware,
|
|
62
|
-
...userConfig.middleware
|
|
62
|
+
...(userConfig.middleware || {})
|
|
63
63
|
},
|
|
64
64
|
customRoutes: {
|
|
65
65
|
...defaultConfig.customRoutes,
|
|
66
|
-
...userConfig.customRoutes
|
|
66
|
+
...(userConfig.customRoutes || {})
|
|
67
67
|
},
|
|
68
68
|
cache: {
|
|
69
69
|
...defaultConfig.cache,
|
|
70
|
-
...userConfig.cache
|
|
70
|
+
...(userConfig.cache || {})
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
73
|
log('User config loaded and merged with defaults', 3);
|
|
@@ -29,7 +29,7 @@ export default {
|
|
|
29
29
|
const res1 = createMockRes();
|
|
30
30
|
createResponseWrapper(res1).send('hello');
|
|
31
31
|
// Content-Type defaults to text/html for string when not set
|
|
32
|
-
if(res1.getHeader('Content-Type') !== 'text/html') return fail('string content-type');
|
|
32
|
+
if(res1.getHeader('Content-Type') !== 'text/html; charset=utf-8') return fail('string content-type');
|
|
33
33
|
if(res1.getBody().toString() !== 'hello') return fail('string body');
|
|
34
34
|
|
|
35
35
|
const res2 = createMockRes();
|
|
@@ -51,11 +51,11 @@ export default {
|
|
|
51
51
|
'html and text helpers': async ({pass, fail}) => {
|
|
52
52
|
const r1 = createMockRes();
|
|
53
53
|
createResponseWrapper(r1).html('<h1>Ok</h1>');
|
|
54
|
-
if(r1.getHeader('Content-Type') !== 'text/html') return fail('html type');
|
|
54
|
+
if(r1.getHeader('Content-Type') !== 'text/html; charset=utf-8') return fail('html type');
|
|
55
55
|
|
|
56
56
|
const r2 = createMockRes();
|
|
57
57
|
createResponseWrapper(r2).text('plain');
|
|
58
|
-
if(r2.getHeader('Content-Type') !== 'text/plain') return fail('text type');
|
|
58
|
+
if(r2.getHeader('Content-Type') !== 'text/plain; charset=utf-8') return fail('text type');
|
|
59
59
|
|
|
60
60
|
pass('helpers');
|
|
61
61
|
},
|