kempo-server 1.8.0 → 1.8.1
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/router.js +1 -1
- package/dist/serveFile.js +1 -1
- package/package.json +1 -1
- package/src/router.js +14 -4
- package/src/serveFile.js +7 -2
- package/tests/builtinMiddleware-cors.node-test.js +2 -1
- package/tests/builtinMiddleware.node-test.js +5 -1
- package/tests/config-flag-cli.node-test.js +18 -14
- package/tests/config-flag.node-test.js +12 -9
- package/tests/customRoute-outside-root.node-test.js +4 -1
- package/tests/example-middleware.node-test.js +3 -1
- package/tests/getFiles.node-test.js +3 -1
- package/tests/index.node-test.js +6 -2
- package/tests/middlewareRunner.node-test.js +2 -1
- package/tests/mimeTypes-merge.node-test.js +8 -8
- package/tests/requestWrapper.node-test.js +1 -1
- package/tests/responseWrapper.node-test.js +2 -1
- package/tests/router-middleware.node-test.js +3 -1
- package/tests/router-wildcard-double-asterisk.node-test.js +5 -1
- package/tests/router-wildcard.node-test.js +5 -1
- package/tests/router.node-test.js +5 -1
- package/tests/serveFile.node-test.js +6 -2
- package/tests/utils/compression.js +4 -0
- package/tests/utils/cookie.js +1 -0
- package/tests/utils/env.js +6 -0
- package/tests/utils/file-writer.js +9 -0
- package/tests/utils/http.js +9 -0
- package/tests/utils/logging.js +1 -0
- package/tests/utils/mock-req.js +19 -0
- package/tests/utils/mock-res.js +21 -0
- package/tests/utils/port.js +1 -0
- package/tests/utils/process.js +7 -0
- package/tests/utils/string-utils.js +3 -0
- package/tests/utils/temp-dir.js +12 -0
- package/tests/utils/test-dir.js +24 -0
- package/tests/utils/test-file-path.js +9 -0
- package/tests/utils/test-scenario.js +29 -0
- package/tests/utils/timing.js +1 -0
- package/dist/utils/ensure-build.js +0 -1
- package/tests/test-utils.js +0 -151
- /package/tests/{ensure-build.js → utils/ensure-build.js} +0 -0
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=void 0):(mimeType=mimeConfig?.mime||"application/octet-stream",encoding="utf8"===mimeConfig?.encoding?"utf8":void 0);const fileContent=await readFile(customFilePath,encoding);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}`,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=void 0):(mimeType=mimeConfig?.mime||"application/octet-stream",encoding="utf8"===mimeConfig?.encoding?"utf8":void 0);const fileContent=await readFile(resolvedFilePath,encoding);return log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`,4),res.writeHead(200,{"Content-Type":mimeType}),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/dist/serveFile.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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 fileExtension=path.extname(file).toLowerCase().slice(1),mimeConfig=config.allowedMimes[fileExtension];let mimeType,encoding;"string"==typeof mimeConfig?(mimeType=mimeConfig,encoding=void 0):(mimeType=mimeConfig?.mime||"application/octet-stream",encoding="utf8"===mimeConfig?.encoding?"utf8":void 0);const fileContent=await readFile(file,encoding);
|
|
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 fileExtension=path.extname(file).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(file,encoding);log(`Serving ${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}),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
package/src/router.js
CHANGED
|
@@ -315,14 +315,19 @@ export default async (flags, log) => {
|
|
|
315
315
|
let mimeType, encoding;
|
|
316
316
|
if (typeof mimeConfig === 'string') {
|
|
317
317
|
mimeType = mimeConfig;
|
|
318
|
-
|
|
318
|
+
// Default to UTF-8 for text MIME types
|
|
319
|
+
encoding = mimeType.startsWith('text/') ? 'utf8' : undefined;
|
|
319
320
|
} else {
|
|
320
321
|
mimeType = mimeConfig?.mime || 'application/octet-stream';
|
|
321
322
|
encoding = mimeConfig?.encoding === 'utf8' ? 'utf8' : undefined;
|
|
322
323
|
}
|
|
323
324
|
const fileContent = await readFile(customFilePath, encoding);
|
|
324
325
|
log(`Serving custom file as ${mimeType} (${fileContent.length} bytes)`, 2);
|
|
325
|
-
|
|
326
|
+
// Add charset=utf-8 for text MIME types when using UTF-8 encoding
|
|
327
|
+
const contentType = encoding === 'utf8' && mimeType.startsWith('text/')
|
|
328
|
+
? `${mimeType}; charset=utf-8`
|
|
329
|
+
: mimeType;
|
|
330
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
326
331
|
res.end(fileContent);
|
|
327
332
|
return; // Successfully served custom route
|
|
328
333
|
} catch (error) {
|
|
@@ -344,14 +349,19 @@ export default async (flags, log) => {
|
|
|
344
349
|
let mimeType, encoding;
|
|
345
350
|
if (typeof mimeConfig === 'string') {
|
|
346
351
|
mimeType = mimeConfig;
|
|
347
|
-
|
|
352
|
+
// Default to UTF-8 for text MIME types
|
|
353
|
+
encoding = mimeType.startsWith('text/') ? 'utf8' : undefined;
|
|
348
354
|
} else {
|
|
349
355
|
mimeType = mimeConfig?.mime || 'application/octet-stream';
|
|
350
356
|
encoding = mimeConfig?.encoding === 'utf8' ? 'utf8' : undefined;
|
|
351
357
|
}
|
|
352
358
|
const fileContent = await readFile(resolvedFilePath, encoding);
|
|
353
359
|
log(`Serving wildcard file as ${mimeType} (${fileContent.length} bytes)`, 4);
|
|
354
|
-
|
|
360
|
+
// Add charset=utf-8 for text MIME types when using UTF-8 encoding
|
|
361
|
+
const contentType = encoding === 'utf8' && mimeType.startsWith('text/')
|
|
362
|
+
? `${mimeType}; charset=utf-8`
|
|
363
|
+
: mimeType;
|
|
364
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
355
365
|
res.end(fileContent);
|
|
356
366
|
return; // Successfully served wildcard route
|
|
357
367
|
} catch (error) {
|
package/src/serveFile.js
CHANGED
|
@@ -87,14 +87,19 @@ export default async (files, rootPath, requestPath, method, config, req, res, lo
|
|
|
87
87
|
let mimeType, encoding;
|
|
88
88
|
if (typeof mimeConfig === 'string') {
|
|
89
89
|
mimeType = mimeConfig;
|
|
90
|
-
|
|
90
|
+
// Default to UTF-8 for text MIME types
|
|
91
|
+
encoding = mimeType.startsWith('text/') ? 'utf8' : undefined;
|
|
91
92
|
} else {
|
|
92
93
|
mimeType = mimeConfig?.mime || 'application/octet-stream';
|
|
93
94
|
encoding = mimeConfig?.encoding === 'utf8' ? 'utf8' : undefined;
|
|
94
95
|
}
|
|
95
96
|
const fileContent = await readFile(file, encoding);
|
|
96
97
|
log(`Serving ${file} as ${mimeType} (${fileContent.length} bytes)`, 2);
|
|
97
|
-
|
|
98
|
+
// Add charset=utf-8 for text MIME types when using UTF-8 encoding
|
|
99
|
+
const contentType = encoding === 'utf8' && mimeType.startsWith('text/')
|
|
100
|
+
? `${mimeType}; charset=utf-8`
|
|
101
|
+
: mimeType;
|
|
102
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
98
103
|
res.end(fileContent);
|
|
99
104
|
return true; // Successfully served
|
|
100
105
|
} catch (error) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {corsMiddleware} from '../src/builtinMiddleware.js';
|
|
2
|
-
import {createMockReq
|
|
2
|
+
import {createMockReq} from './utils/mock-req.js';
|
|
3
|
+
import {createMockRes} from './utils/mock-res.js';
|
|
3
4
|
|
|
4
5
|
export default {
|
|
5
6
|
'cors origin array and non-OPTIONS continues to next': async ({pass, fail}) => {
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
|
-
import {createMockReq
|
|
2
|
+
import {createMockReq} from './utils/mock-req.js';
|
|
3
|
+
import {createMockRes} from './utils/mock-res.js';
|
|
4
|
+
import {bigString} from './utils/string-utils.js';
|
|
5
|
+
import {gzipSize} from './utils/compression.js';
|
|
6
|
+
import {setEnv} from './utils/env.js';
|
|
3
7
|
import {corsMiddleware, compressionMiddleware, rateLimitMiddleware, securityMiddleware, loggingMiddleware} from '../src/builtinMiddleware.js';
|
|
4
8
|
|
|
5
9
|
export default {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {startNode
|
|
1
|
+
import {startNode} from './utils/process.js';
|
|
2
|
+
import {randomPort} from './utils/port.js';
|
|
3
|
+
import {httpGet} from './utils/http.js';
|
|
4
|
+
import {withTempDir} from './utils/temp-dir.js';
|
|
5
|
+
import {write} from './utils/file-writer.js';
|
|
2
6
|
import path from 'path';
|
|
3
|
-
import ensureBuild from './ensure-build.js';
|
|
7
|
+
import ensureBuild from './utils/ensure-build.js';
|
|
4
8
|
|
|
5
9
|
ensureBuild();
|
|
6
10
|
|
|
@@ -29,8 +33,8 @@ export default {
|
|
|
29
33
|
if (res.statusCode !== 200) {
|
|
30
34
|
return fail('server should serve file with default config');
|
|
31
35
|
}
|
|
32
|
-
if (res.headers['content-type'] !== 'text/default') {
|
|
33
|
-
return fail('should use default config mime type');
|
|
36
|
+
if (res.headers['content-type'] !== 'text/default; charset=utf-8') {
|
|
37
|
+
return fail('should use default config mime type with charset');
|
|
34
38
|
}
|
|
35
39
|
if (body.toString() !== 'default config content') {
|
|
36
40
|
return fail('should serve correct content');
|
|
@@ -73,8 +77,8 @@ export default {
|
|
|
73
77
|
if (res.statusCode !== 200) {
|
|
74
78
|
return fail('server should serve file with custom config');
|
|
75
79
|
}
|
|
76
|
-
if (res.headers['content-type'] !== 'text/custom') {
|
|
77
|
-
return fail('should use custom config mime type');
|
|
80
|
+
if (res.headers['content-type'] !== 'text/custom; charset=utf-8') {
|
|
81
|
+
return fail('should use custom config mime type with charset');
|
|
78
82
|
}
|
|
79
83
|
if (body.toString() !== 'custom config content') {
|
|
80
84
|
return fail('should serve correct content');
|
|
@@ -117,8 +121,8 @@ export default {
|
|
|
117
121
|
if (res.statusCode !== 200) {
|
|
118
122
|
return fail('server should serve file with short flag config');
|
|
119
123
|
}
|
|
120
|
-
if (res.headers['content-type'] !== 'text/short') {
|
|
121
|
-
return fail('should use short flag config mime type');
|
|
124
|
+
if (res.headers['content-type'] !== 'text/short; charset=utf-8') {
|
|
125
|
+
return fail('should use short flag config mime type with charset');
|
|
122
126
|
}
|
|
123
127
|
if (body.toString() !== 'short flag content') {
|
|
124
128
|
return fail('should serve correct content');
|
|
@@ -162,8 +166,8 @@ export default {
|
|
|
162
166
|
if (res.statusCode !== 200) {
|
|
163
167
|
return fail('server should serve file with absolute path config');
|
|
164
168
|
}
|
|
165
|
-
if (res.headers['content-type'] !== 'text/absolute') {
|
|
166
|
-
return fail('should use absolute path config mime type');
|
|
169
|
+
if (res.headers['content-type'] !== 'text/absolute; charset=utf-8') {
|
|
170
|
+
return fail('should use absolute path config mime type with charset');
|
|
167
171
|
}
|
|
168
172
|
if (body.toString() !== 'absolute path content') {
|
|
169
173
|
return fail('should serve correct content');
|
|
@@ -199,8 +203,8 @@ export default {
|
|
|
199
203
|
if (res.statusCode !== 200) {
|
|
200
204
|
return fail('server should start with missing config');
|
|
201
205
|
}
|
|
202
|
-
if (res.headers['content-type'] !== 'text/html') {
|
|
203
|
-
return fail('should use default mime types');
|
|
206
|
+
if (res.headers['content-type'] !== 'text/html; charset=utf-8') {
|
|
207
|
+
return fail('should use default mime types with charset');
|
|
204
208
|
}
|
|
205
209
|
if (!body.toString().includes('<h1>Home</h1>')) {
|
|
206
210
|
return fail('should serve HTML content');
|
|
@@ -246,8 +250,8 @@ export default {
|
|
|
246
250
|
if (res.statusCode !== 200) {
|
|
247
251
|
return fail('custom route should work with config flag');
|
|
248
252
|
}
|
|
249
|
-
if (res.headers['content-type'] !== 'text/plain') {
|
|
250
|
-
return fail('should use config mime type');
|
|
253
|
+
if (res.headers['content-type'] !== 'text/plain; charset=utf-8') {
|
|
254
|
+
return fail('should use config mime type with charset');
|
|
251
255
|
}
|
|
252
256
|
if (body.toString() !== 'source file content') {
|
|
253
257
|
return fail('should serve custom route content');
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {withTempDir
|
|
3
|
+
import {withTempDir} from './utils/temp-dir.js';
|
|
4
|
+
import {write} from './utils/file-writer.js';
|
|
5
|
+
import {randomPort} from './utils/port.js';
|
|
6
|
+
import {httpGet} from './utils/http.js';
|
|
4
7
|
import router from '../src/router.js';
|
|
5
8
|
import getFlags from '../src/getFlags.js';
|
|
6
9
|
|
|
@@ -114,8 +117,8 @@ export default {
|
|
|
114
117
|
if (response.res.statusCode !== 200) {
|
|
115
118
|
return fail('custom mime type should be served');
|
|
116
119
|
}
|
|
117
|
-
if (response.res.headers['content-type'] !== 'text/custom') {
|
|
118
|
-
return fail('should use custom mime type');
|
|
120
|
+
if (response.res.headers['content-type'] !== 'text/custom; charset=utf-8') {
|
|
121
|
+
return fail('should use custom mime type with charset');
|
|
119
122
|
}
|
|
120
123
|
pass('default config file usage');
|
|
121
124
|
} finally {
|
|
@@ -153,8 +156,8 @@ export default {
|
|
|
153
156
|
if (response.res.statusCode !== 200) {
|
|
154
157
|
return fail('custom config should be loaded');
|
|
155
158
|
}
|
|
156
|
-
if (response.res.headers['content-type'] !== 'text/special') {
|
|
157
|
-
return fail('should use custom config mime type');
|
|
159
|
+
if (response.res.headers['content-type'] !== 'text/special; charset=utf-8') {
|
|
160
|
+
return fail('should use custom config mime type with charset');
|
|
158
161
|
}
|
|
159
162
|
pass('relative path config file usage');
|
|
160
163
|
} finally {
|
|
@@ -193,8 +196,8 @@ export default {
|
|
|
193
196
|
if (response.res.statusCode !== 200) {
|
|
194
197
|
return fail('absolute config path should work');
|
|
195
198
|
}
|
|
196
|
-
if (response.res.headers['content-type'] !== 'text/absolute') {
|
|
197
|
-
return fail('should use absolute config mime type');
|
|
199
|
+
if (response.res.headers['content-type'] !== 'text/absolute; charset=utf-8') {
|
|
200
|
+
return fail('should use absolute config mime type with charset');
|
|
198
201
|
}
|
|
199
202
|
pass('absolute path config file usage');
|
|
200
203
|
} finally {
|
|
@@ -308,8 +311,8 @@ export default {
|
|
|
308
311
|
if (customResponse.res.statusCode !== 200) {
|
|
309
312
|
return fail('custom mime type should work');
|
|
310
313
|
}
|
|
311
|
-
if (customResponse.res.headers['content-type'] !== 'text/custom') {
|
|
312
|
-
return fail('should use custom mime type');
|
|
314
|
+
if (customResponse.res.headers['content-type'] !== 'text/custom; charset=utf-8') {
|
|
315
|
+
return fail('should use custom mime type with charset');
|
|
313
316
|
}
|
|
314
317
|
pass('config merging with defaults');
|
|
315
318
|
} finally {
|
|
@@ -2,7 +2,10 @@ import router from '../src/router.js';
|
|
|
2
2
|
import http from 'http';
|
|
3
3
|
import { writeFile, mkdir } from 'fs/promises';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import {randomPort} from './utils/port.js';
|
|
6
|
+
import {httpGet} from './utils/http.js';
|
|
7
|
+
import {withTempDir} from './utils/temp-dir.js';
|
|
8
|
+
import {write} from './utils/file-writer.js';
|
|
6
9
|
|
|
7
10
|
/*
|
|
8
11
|
This test verifies that a customRoute pointing outside the rootPath is served
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import url from 'url';
|
|
3
|
-
import {createMockReq
|
|
3
|
+
import {createMockReq} from './utils/mock-req.js';
|
|
4
|
+
import {createMockRes} from './utils/mock-res.js';
|
|
5
|
+
import {setEnv} from './utils/env.js';
|
|
4
6
|
|
|
5
7
|
// import the middleware module by file path to avoid executing index.js
|
|
6
8
|
const examplePath = path.join(process.cwd(), 'tests', 'example-middleware.js');
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import getFiles from '../src/getFiles.js';
|
|
2
2
|
import defaultConfig from '../src/defaultConfig.js';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {withTestDir
|
|
4
|
+
import {withTestDir} from './utils/test-dir.js';
|
|
5
|
+
import {write} from './utils/file-writer.js';
|
|
6
|
+
import {log} from './utils/logging.js';
|
|
5
7
|
|
|
6
8
|
export default {
|
|
7
9
|
'scans directories recursively and filters by mime and disallowed': async ({pass, fail}) => {
|
package/tests/index.node-test.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {startNode
|
|
1
|
+
import {startNode} from './utils/process.js';
|
|
2
|
+
import {randomPort} from './utils/port.js';
|
|
3
|
+
import {httpGet} from './utils/http.js';
|
|
4
|
+
import {withTestDir} from './utils/test-dir.js';
|
|
5
|
+
import {write} from './utils/file-writer.js';
|
|
2
6
|
import path from 'path';
|
|
3
|
-
import ensureBuild from './ensure-build.js';
|
|
7
|
+
import ensureBuild from './utils/ensure-build.js';
|
|
4
8
|
|
|
5
9
|
ensureBuild();
|
|
6
10
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import MiddlewareRunner from '../src/middlewareRunner.js';
|
|
2
|
-
import {createMockReq
|
|
2
|
+
import {createMockReq} from './utils/mock-req.js';
|
|
3
|
+
import {createMockRes} from './utils/mock-res.js';
|
|
3
4
|
|
|
4
5
|
export default {
|
|
5
6
|
'runs middleware in order and calls finalHandler': async ({pass, fail}) => {
|
|
@@ -63,24 +63,24 @@ export default {
|
|
|
63
63
|
if(cssResponse.res.statusCode !== 200){
|
|
64
64
|
return fail('CSS file should be served');
|
|
65
65
|
}
|
|
66
|
-
if(cssResponse.res.headers['content-type'] !== 'text/css'){
|
|
67
|
-
return fail(`CSS should have text/css MIME type, got: ${cssResponse.res.headers['content-type']}`);
|
|
66
|
+
if(cssResponse.res.headers['content-type'] !== 'text/css; charset=utf-8'){
|
|
67
|
+
return fail(`CSS should have text/css MIME type with charset, got: ${cssResponse.res.headers['content-type']}`);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
const jsResponse = await httpGet(`http://localhost:${port}/test.js`);
|
|
71
71
|
if(jsResponse.res.statusCode !== 200){
|
|
72
72
|
return fail('JS file should be served');
|
|
73
73
|
}
|
|
74
|
-
if(jsResponse.res.headers['content-type'] !== 'text/javascript'){
|
|
75
|
-
return fail(`JS should have custom text/javascript MIME type, got: ${jsResponse.res.headers['content-type']}`);
|
|
74
|
+
if(jsResponse.res.headers['content-type'] !== 'text/javascript; charset=utf-8'){
|
|
75
|
+
return fail(`JS should have custom text/javascript MIME type with charset, got: ${jsResponse.res.headers['content-type']}`);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
const htmlResponse = await httpGet(`http://localhost:${port}/test.html`);
|
|
79
79
|
if(htmlResponse.res.statusCode !== 200){
|
|
80
80
|
return fail('HTML file should be served');
|
|
81
81
|
}
|
|
82
|
-
if(htmlResponse.res.headers['content-type'] !== 'text/html'){
|
|
83
|
-
return fail(`HTML should have text/html MIME type, got: ${htmlResponse.res.headers['content-type']}`);
|
|
82
|
+
if(htmlResponse.res.headers['content-type'] !== 'text/html; charset=utf-8'){
|
|
83
|
+
return fail(`HTML should have text/html MIME type with charset, got: ${htmlResponse.res.headers['content-type']}`);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
pass('allowedMimes merges correctly with defaults');
|
|
@@ -162,8 +162,8 @@ export default {
|
|
|
162
162
|
|
|
163
163
|
try {
|
|
164
164
|
const tests = [
|
|
165
|
-
{ file: 'test.css', expectedType: 'text/css' },
|
|
166
|
-
{ file: 'test.html', expectedType: 'text/html' },
|
|
165
|
+
{ file: 'test.css', expectedType: 'text/css; charset=utf-8' },
|
|
166
|
+
{ file: 'test.html', expectedType: 'text/html; charset=utf-8' },
|
|
167
167
|
{ file: 'test.json', expectedType: 'application/json' },
|
|
168
168
|
{ file: 'test.svg', expectedType: 'image/svg+xml' },
|
|
169
169
|
{ file: 'test.png', expectedType: 'image/png' },
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
|
-
import {withTestDir
|
|
2
|
+
import {withTestDir} from './utils/test-dir.js';
|
|
3
|
+
import {write} from './utils/file-writer.js';
|
|
4
|
+
import {randomPort} from './utils/port.js';
|
|
3
5
|
import router from '../src/router.js';
|
|
4
6
|
|
|
5
7
|
export default {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {withTempDir
|
|
3
|
+
import {withTempDir} from './utils/temp-dir.js';
|
|
4
|
+
import {write} from './utils/file-writer.js';
|
|
5
|
+
import {randomPort} from './utils/port.js';
|
|
6
|
+
import {httpGet} from './utils/http.js';
|
|
7
|
+
import {log} from './utils/logging.js';
|
|
4
8
|
import router from '../src/router.js';
|
|
5
9
|
|
|
6
10
|
export default {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {withTempDir
|
|
3
|
+
import {withTempDir} from './utils/temp-dir.js';
|
|
4
|
+
import {write} from './utils/file-writer.js';
|
|
5
|
+
import {randomPort} from './utils/port.js';
|
|
6
|
+
import {httpGet} from './utils/http.js';
|
|
7
|
+
import {log} from './utils/logging.js';
|
|
4
8
|
import router from '../src/router.js';
|
|
5
9
|
|
|
6
10
|
export default {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import {withTestDir
|
|
3
|
+
import {withTestDir} from './utils/test-dir.js';
|
|
4
|
+
import {write} from './utils/file-writer.js';
|
|
5
|
+
import {randomPort} from './utils/port.js';
|
|
6
|
+
import {httpGet} from './utils/http.js';
|
|
7
|
+
import {log} from './utils/logging.js';
|
|
4
8
|
import router from '../src/router.js';
|
|
5
9
|
import defaultConfig from '../src/defaultConfig.js';
|
|
6
10
|
|
|
@@ -2,7 +2,11 @@ import serveFile from '../src/serveFile.js';
|
|
|
2
2
|
import findFile from '../src/findFile.js';
|
|
3
3
|
import defaultConfig from '../src/defaultConfig.js';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import {createMockReq
|
|
5
|
+
import {createMockReq} from './utils/mock-req.js';
|
|
6
|
+
import {createMockRes} from './utils/mock-res.js';
|
|
7
|
+
import {withTestDir} from './utils/test-dir.js';
|
|
8
|
+
import {write} from './utils/file-writer.js';
|
|
9
|
+
import {log} from './utils/logging.js';
|
|
6
10
|
|
|
7
11
|
export default {
|
|
8
12
|
'serves static file with correct mime': async ({pass, fail}) => {
|
|
@@ -14,7 +18,7 @@ export default {
|
|
|
14
18
|
const ok = await serveFile(files, dir, '/index.html', 'GET', cfg, createMockReq(), res, log);
|
|
15
19
|
if(ok !== true) return fail('should serve');
|
|
16
20
|
if(res.statusCode !== 200) return fail('status');
|
|
17
|
-
if(res.getHeader('Content-Type') !== 'text/html') return fail('mime');
|
|
21
|
+
if(res.getHeader('Content-Type') !== 'text/html; charset=utf-8') return fail('mime');
|
|
18
22
|
});
|
|
19
23
|
pass('static');
|
|
20
24
|
} catch(e){ fail(e.message); }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const parseCookies = (setCookie) => Array.isArray(setCookie) ? setCookie : (setCookie ? [setCookie] : []);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const setEnv = (pairs, fn) => {
|
|
2
|
+
const prev = {};
|
|
3
|
+
Object.keys(pairs).forEach(k => { prev[k] = process.env[k]; process.env[k] = pairs[k]; });
|
|
4
|
+
const restore = () => { Object.keys(pairs).forEach(k => { if(prev[k] === undefined){ delete process.env[k]; } else { process.env[k] = prev[k]; } }); };
|
|
5
|
+
return fn().finally(restore);
|
|
6
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {writeFile, mkdir} from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export const write = async (root, rel, content = '') => {
|
|
5
|
+
const full = path.join(root, rel);
|
|
6
|
+
await mkdir(path.dirname(full), {recursive: true});
|
|
7
|
+
await writeFile(full, content);
|
|
8
|
+
return full;
|
|
9
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const httpGet = (url) => new Promise((resolve, reject) => {
|
|
2
|
+
import('http').then(({get}) => {
|
|
3
|
+
get(url, res => {
|
|
4
|
+
const chunks = [];
|
|
5
|
+
res.on('data', c => chunks.push(c));
|
|
6
|
+
res.on('end', () => resolve({res, body: Buffer.concat(chunks)}));
|
|
7
|
+
}).on('error', reject);
|
|
8
|
+
}).catch(reject);
|
|
9
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const log = (..._args) => {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {Readable} from 'stream';
|
|
2
|
+
|
|
3
|
+
export const createMockReq = ({method = 'GET', url = '/', headers = {}, body = null, remoteAddress = '127.0.0.1'} = {}) => {
|
|
4
|
+
const stream = new Readable({read(){}});
|
|
5
|
+
stream.method = method;
|
|
6
|
+
stream.url = url;
|
|
7
|
+
stream.headers = headers;
|
|
8
|
+
stream.socket = {remoteAddress};
|
|
9
|
+
if(body !== null && body !== undefined){
|
|
10
|
+
const data = typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body);
|
|
11
|
+
setImmediate(() => {
|
|
12
|
+
stream.emit('data', Buffer.from(data));
|
|
13
|
+
stream.emit('end');
|
|
14
|
+
});
|
|
15
|
+
} else {
|
|
16
|
+
setImmediate(() => stream.emit('end'));
|
|
17
|
+
}
|
|
18
|
+
return stream;
|
|
19
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const createMockRes = () => {
|
|
2
|
+
const headers = new Map();
|
|
3
|
+
let ended = false;
|
|
4
|
+
let statusCode = 200;
|
|
5
|
+
const chunks = [];
|
|
6
|
+
return {
|
|
7
|
+
get statusCode(){return statusCode;},
|
|
8
|
+
set statusCode(v){statusCode = v;},
|
|
9
|
+
setHeader: (k, v) => headers.set(k, v),
|
|
10
|
+
getHeader: (k) => headers.get(k),
|
|
11
|
+
writeHead: (code, hdrs = {}) => {
|
|
12
|
+
statusCode = code;
|
|
13
|
+
Object.entries(hdrs).forEach(([k, v]) => headers.set(k, v));
|
|
14
|
+
},
|
|
15
|
+
write: (chunk) => { if(chunk) chunks.push(Buffer.from(chunk)); return true; },
|
|
16
|
+
end: (chunk) => { if(chunk) chunks.push(Buffer.from(chunk)); ended = true; },
|
|
17
|
+
getHeaders: () => Object.fromEntries(headers),
|
|
18
|
+
getBody: () => Buffer.concat(chunks),
|
|
19
|
+
isEnded: () => ended
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const randomPort = () => 1024 + Math.floor(Math.random() * 50000);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const startNode = async (args, options = {}) => {
|
|
2
|
+
const {spawn} = await import('child_process');
|
|
3
|
+
const child = spawn(process.execPath, args, {stdio: ['ignore', 'pipe', 'pipe'], ...options});
|
|
4
|
+
child.stdout.setEncoding('utf8');
|
|
5
|
+
child.stderr.setEncoding('utf8');
|
|
6
|
+
return child;
|
|
7
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {mkdtemp, rm} from 'fs/promises';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export const withTempDir = async (fn) => {
|
|
6
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), 'kempo-tests-'));
|
|
7
|
+
try {
|
|
8
|
+
return await fn(dir);
|
|
9
|
+
} finally {
|
|
10
|
+
await rm(dir, {recursive: true, force: true});
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {mkdtemp, rm, cp} from 'fs/promises';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import {fileURLToPath} from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const TEST_SERVER_ROOT = path.join(__dirname, '..', 'test-server-root');
|
|
8
|
+
|
|
9
|
+
export const withTestDir = async (fn, {subdir = null} = {}) => {
|
|
10
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'kempo-tests-'));
|
|
11
|
+
try {
|
|
12
|
+
// Copy the test server root to the temporary directory
|
|
13
|
+
await cp(TEST_SERVER_ROOT, tempDir, {recursive: true});
|
|
14
|
+
let workingDir = tempDir;
|
|
15
|
+
|
|
16
|
+
if (subdir) {
|
|
17
|
+
workingDir = path.join(tempDir, subdir);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return await fn(workingDir);
|
|
21
|
+
} finally {
|
|
22
|
+
await rm(tempDir, {recursive: true, force: true});
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import {fileURLToPath} from 'url';
|
|
3
|
+
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const TEST_SERVER_ROOT = path.join(__dirname, '..', 'test-server-root');
|
|
6
|
+
|
|
7
|
+
export const getTestFilePath = (relativePath) => {
|
|
8
|
+
return path.join(TEST_SERVER_ROOT, relativePath);
|
|
9
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {writeFile, mkdir} from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export const prepareTestScenario = async (dir, scenario) => {
|
|
5
|
+
switch (scenario) {
|
|
6
|
+
case 'basic-server':
|
|
7
|
+
// Already has index.html, api/GET.js, etc.
|
|
8
|
+
break;
|
|
9
|
+
case 'wildcard-routes':
|
|
10
|
+
await write(dir, 'docs/.config.json', JSON.stringify({
|
|
11
|
+
customRoutes: { '/src/**': '../src/**' }
|
|
12
|
+
}));
|
|
13
|
+
break;
|
|
14
|
+
case 'middleware':
|
|
15
|
+
await write(dir, '.config.json', JSON.stringify({
|
|
16
|
+
middleware: { cors: {enabled: true} }
|
|
17
|
+
}));
|
|
18
|
+
break;
|
|
19
|
+
default:
|
|
20
|
+
// No special preparation needed
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const write = async (root, rel, content = '') => {
|
|
25
|
+
const full = path.join(root, rel);
|
|
26
|
+
await mkdir(path.dirname(full), {recursive: true});
|
|
27
|
+
await writeFile(full, content);
|
|
28
|
+
return full;
|
|
29
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const wait = ms => new Promise(r => setTimeout(r, ms));
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{execSync}from"child_process";import fs from"fs";import path from"path";let hasBuilt=!1;export default function ensureBuild(){if(hasBuilt)return;const distIndex=path.join(process.cwd(),"dist/index.js");if(!fs.existsSync(distIndex))try{execSync("npm run build",{stdio:"pipe"}),console.log("✓ Build completed successfully")}catch(error){throw console.error("✗ Build failed:",error.message),error}hasBuilt=!0}
|
package/tests/test-utils.js
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import {Readable} from 'stream';
|
|
2
|
-
import {mkdtemp, rm, writeFile, mkdir, cp} from 'fs/promises';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import {fileURLToPath} from 'url';
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const TEST_SERVER_ROOT = path.join(__dirname, 'test-server-root');
|
|
9
|
-
|
|
10
|
-
export const createMockReq = ({method = 'GET', url = '/', headers = {}, body = null, remoteAddress = '127.0.0.1'} = {}) => {
|
|
11
|
-
const stream = new Readable({read(){}});
|
|
12
|
-
stream.method = method;
|
|
13
|
-
stream.url = url;
|
|
14
|
-
stream.headers = headers;
|
|
15
|
-
stream.socket = {remoteAddress};
|
|
16
|
-
if(body !== null && body !== undefined){
|
|
17
|
-
const data = typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body);
|
|
18
|
-
setImmediate(() => {
|
|
19
|
-
stream.emit('data', Buffer.from(data));
|
|
20
|
-
stream.emit('end');
|
|
21
|
-
});
|
|
22
|
-
} else {
|
|
23
|
-
setImmediate(() => stream.emit('end'));
|
|
24
|
-
}
|
|
25
|
-
return stream;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const createMockRes = () => {
|
|
29
|
-
const headers = new Map();
|
|
30
|
-
let ended = false;
|
|
31
|
-
let statusCode = 200;
|
|
32
|
-
const chunks = [];
|
|
33
|
-
return {
|
|
34
|
-
get statusCode(){return statusCode;},
|
|
35
|
-
set statusCode(v){statusCode = v;},
|
|
36
|
-
setHeader: (k, v) => headers.set(k, v),
|
|
37
|
-
getHeader: (k) => headers.get(k),
|
|
38
|
-
writeHead: (code, hdrs = {}) => {
|
|
39
|
-
statusCode = code;
|
|
40
|
-
Object.entries(hdrs).forEach(([k, v]) => headers.set(k, v));
|
|
41
|
-
},
|
|
42
|
-
write: (chunk) => { if(chunk) chunks.push(Buffer.from(chunk)); return true; },
|
|
43
|
-
end: (chunk) => { if(chunk) chunks.push(Buffer.from(chunk)); ended = true; },
|
|
44
|
-
getHeaders: () => Object.fromEntries(headers),
|
|
45
|
-
getBody: () => Buffer.concat(chunks),
|
|
46
|
-
isEnded: () => ended
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// Legacy function for backward compatibility - will be deprecated
|
|
51
|
-
export const withTempDir = async (fn) => {
|
|
52
|
-
const dir = await mkdtemp(path.join(os.tmpdir(), 'kempo-tests-'));
|
|
53
|
-
try {
|
|
54
|
-
return await fn(dir);
|
|
55
|
-
} finally {
|
|
56
|
-
await rm(dir, {recursive: true, force: true});
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// New function that uses the persistent test server root as a base
|
|
61
|
-
export const withTestDir = async (fn, {subdir = null} = {}) => {
|
|
62
|
-
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'kempo-tests-'));
|
|
63
|
-
try {
|
|
64
|
-
// Copy the test server root to the temporary directory
|
|
65
|
-
await cp(TEST_SERVER_ROOT, tempDir, {recursive: true});
|
|
66
|
-
let workingDir = tempDir;
|
|
67
|
-
|
|
68
|
-
if (subdir) {
|
|
69
|
-
workingDir = path.join(tempDir, subdir);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return await fn(workingDir);
|
|
73
|
-
} finally {
|
|
74
|
-
await rm(tempDir, {recursive: true, force: true});
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// Helper to get the path to a file in the test server root
|
|
79
|
-
export const getTestFilePath = (relativePath) => {
|
|
80
|
-
return path.join(TEST_SERVER_ROOT, relativePath);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Helper to reset or prepare specific test scenarios
|
|
84
|
-
export const prepareTestScenario = async (dir, scenario) => {
|
|
85
|
-
switch (scenario) {
|
|
86
|
-
case 'basic-server':
|
|
87
|
-
// Already has index.html, api/GET.js, etc.
|
|
88
|
-
break;
|
|
89
|
-
case 'wildcard-routes':
|
|
90
|
-
await write(dir, 'docs/.config.json', JSON.stringify({
|
|
91
|
-
customRoutes: { '/src/**': '../src/**' }
|
|
92
|
-
}));
|
|
93
|
-
break;
|
|
94
|
-
case 'middleware':
|
|
95
|
-
await write(dir, '.config.json', JSON.stringify({
|
|
96
|
-
middleware: { cors: {enabled: true} }
|
|
97
|
-
}));
|
|
98
|
-
break;
|
|
99
|
-
default:
|
|
100
|
-
// No special preparation needed
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export const write = async (root, rel, content = '') => {
|
|
105
|
-
const full = path.join(root, rel);
|
|
106
|
-
await mkdir(path.dirname(full), {recursive: true});
|
|
107
|
-
await writeFile(full, content);
|
|
108
|
-
return full;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
export const wait = ms => new Promise(r => setTimeout(r, ms));
|
|
112
|
-
|
|
113
|
-
export const log = (..._args) => {};
|
|
114
|
-
|
|
115
|
-
export const bigString = (size = 5000) => 'x'.repeat(size);
|
|
116
|
-
|
|
117
|
-
export const randomPort = () => 1024 + Math.floor(Math.random() * 50000);
|
|
118
|
-
|
|
119
|
-
export const httpGet = (url) => new Promise((resolve, reject) => {
|
|
120
|
-
import('http').then(({get}) => {
|
|
121
|
-
get(url, res => {
|
|
122
|
-
const chunks = [];
|
|
123
|
-
res.on('data', c => chunks.push(c));
|
|
124
|
-
res.on('end', () => resolve({res, body: Buffer.concat(chunks)}));
|
|
125
|
-
}).on('error', reject);
|
|
126
|
-
}).catch(reject);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
export const startNode = async (args, options = {}) => {
|
|
130
|
-
const {spawn} = await import('child_process');
|
|
131
|
-
const child = spawn(process.execPath, args, {stdio: ['ignore', 'pipe', 'pipe'], ...options});
|
|
132
|
-
child.stdout.setEncoding('utf8');
|
|
133
|
-
child.stderr.setEncoding('utf8');
|
|
134
|
-
return child;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export const setEnv = (pairs, fn) => {
|
|
138
|
-
const prev = {};
|
|
139
|
-
Object.keys(pairs).forEach(k => { prev[k] = process.env[k]; process.env[k] = pairs[k]; });
|
|
140
|
-
const restore = () => { Object.keys(pairs).forEach(k => { if(prev[k] === undefined){ delete process.env[k]; } else { process.env[k] = prev[k]; } }); };
|
|
141
|
-
return fn().finally(restore);
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
export const toString = (buf) => Buffer.isBuffer(buf) ? buf.toString() : String(buf);
|
|
145
|
-
|
|
146
|
-
export const gzipSize = async (buf) => {
|
|
147
|
-
const {gzip} = await import('zlib');
|
|
148
|
-
return new Promise((res, rej) => gzip(buf, (e, out) => e ? rej(e) : res(out.length)));
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
export const parseCookies = (setCookie) => Array.isArray(setCookie) ? setCookie : (setCookie ? [setCookie] : []);
|
|
File without changes
|